1use std::net::{SocketAddr, ToSocketAddrs};
40use std::time::{Duration, Instant};
41
42use clap::Subcommand;
43use sha2::{Digest, Sha256};
44
45use qualia_core_db::p2p::mesh_datagram::{self, ports};
46use qualia_core_db::p2p::wireguard_runtime::{TunnelEvent, WgTunnel};
47use qualia_core_db::p2p::wireguard_userspace::WgKeypair;
48
49#[derive(Subcommand, Debug)]
50pub enum MeshAction {
51 Keygen,
53 Listen {
55 #[arg(long)]
57 pass: String,
58 #[arg(long, default_value_t = 51820)]
60 port: u16,
61 #[arg(long, default_value_t = 0)]
63 seconds: u64,
64 },
65 Connect {
67 #[arg(long)]
69 pass: String,
70 #[arg(long)]
72 peer: String,
73 #[arg(long, default_value = "hello from the SocialWebNet probe")]
75 message: String,
76 #[arg(long, default_value_t = 1)]
78 count: u32,
79 #[arg(long, default_value_t = 15)]
81 timeout: u64,
82 },
83}
84
85fn derive_secret(role: &str, pass: &str) -> [u8; 32] {
87 let mut hasher = Sha256::new();
88 hasher.update(b"qualia-mesh-probe:v1:");
89 hasher.update(role.as_bytes());
90 hasher.update(b":");
91 hasher.update(pass.as_bytes());
92 let digest = hasher.finalize();
93 let mut out = [0u8; 32];
94 out.copy_from_slice(&digest);
95 out
96}
97
98pub fn run(action: &MeshAction) -> Result<(), Box<dyn std::error::Error>> {
99 match action {
100 MeshAction::Keygen => {
101 let kp = qualia_core_db::p2p::wireguard_userspace::generate_keypair();
102 println!("WireGuard keypair (random):");
103 println!(" secret : {}", hex_lower(&kp.private_bytes()));
104 println!(" public : {}", kp.public_hex());
105 Ok(())
106 }
107 MeshAction::Listen {
108 pass,
109 port,
110 seconds,
111 } => run_listen(pass, *port, *seconds),
112 MeshAction::Connect {
113 pass,
114 peer,
115 message,
116 count,
117 timeout,
118 } => run_connect(pass, peer, message, *count, *timeout),
119 }
120}
121
122fn hex_lower(bytes: &[u8]) -> String {
123 let mut s = String::with_capacity(bytes.len() * 2);
124 for b in bytes {
125 s.push_str(&format!("{b:02x}"));
126 }
127 s
128}
129
130fn run_listen(pass: &str, port: u16, seconds: u64) -> Result<(), Box<dyn std::error::Error>> {
131 let my_keys = WgKeypair::from_secret_bytes(derive_secret("a", pass));
133 let peer_keys = WgKeypair::from_secret_bytes(derive_secret("b", pass));
134
135 let bind: SocketAddr = format!("0.0.0.0:{port}").parse()?;
136 let mut tunnel = WgTunnel::bind(&my_keys, peer_keys.public_bytes().into(), bind, 1)?;
137 tunnel.set_read_timeout(Some(Duration::from_millis(500)))?;
138
139 println!("SocialWebNet probe — LISTEN (role A)");
140 println!(" my WG public : {}", my_keys.public_hex());
141 println!(" bound : {}", tunnel.local_addr()?);
142 println!(" expecting peer WG public : {}", peer_keys.public_hex());
143 if seconds == 0 {
144 println!(" waiting for a connection… (Ctrl-C to stop)");
145 } else {
146 println!(" waiting up to {seconds}s for a connection…");
147 }
148
149 let deadline = if seconds == 0 {
150 None
151 } else {
152 Some(Instant::now() + Duration::from_secs(seconds))
153 };
154 let mut announced_session = false;
155
156 loop {
157 if let Some(d) = deadline {
158 if Instant::now() >= d {
159 println!(" (timeout reached; exiting)");
160 return Ok(());
161 }
162 }
163 match tunnel.pump()? {
164 TunnelEvent::InnerPacket(inner) => match mesh_datagram::decode_datagram(&inner) {
165 Some(d) => println!(
166 " ← received {} bytes on port {}: \"{}\"",
167 d.payload.len(),
168 d.dst_port,
169 String::from_utf8_lossy(&d.payload)
170 ),
171 None => println!(
172 " ← received a {}-byte inner packet (not a UDP datagram)",
173 inner.len()
174 ),
175 },
176 TunnelEvent::Progressed => {
177 if !announced_session && tunnel.has_session() {
178 announced_session = true;
179 println!(
180 " ✓ handshake complete with {:?} — tunnel is up",
181 tunnel
182 .peer_endpoint()
183 .map(|e| e.to_string())
184 .unwrap_or_default()
185 );
186 }
187 }
188 TunnelEvent::Idle => {
189 let _ = tunnel.tick();
191 }
192 }
193 }
194}
195
196fn run_connect(
197 pass: &str,
198 peer: &str,
199 message: &str,
200 count: u32,
201 timeout: u64,
202) -> Result<(), Box<dyn std::error::Error>> {
203 let my_keys = WgKeypair::from_secret_bytes(derive_secret("b", pass));
205 let peer_keys = WgKeypair::from_secret_bytes(derive_secret("a", pass));
206
207 let endpoint = peer
208 .to_socket_addrs()?
209 .next()
210 .ok_or_else(|| format!("could not resolve peer address '{peer}'"))?;
211
212 let bind: SocketAddr = "0.0.0.0:0".parse()?;
213 let mut tunnel = WgTunnel::bind(&my_keys, peer_keys.public_bytes().into(), bind, 2)?;
214 tunnel.set_read_timeout(Some(Duration::from_millis(500)))?;
215 tunnel.set_peer_endpoint(endpoint);
216
217 println!("SocialWebNet probe — CONNECT (role B)");
218 println!(" my WG public : {}", my_keys.public_hex());
219 println!(
220 " peer : {endpoint} (WG public {})",
221 peer_keys.public_hex()
222 );
223 println!(" initiating handshake…");
224 tunnel.initiate_handshake()?;
225
226 let deadline = Instant::now() + Duration::from_secs(timeout);
230 while !tunnel.has_session() {
231 if Instant::now() >= deadline {
232 return Err(format!(
233 "handshake did not complete within {timeout}s — check the peer address, that the \
234 listener is running with the same --pass, and that UDP is reachable (NAT/firewall)"
235 )
236 .into());
237 }
238 let _ = tunnel.pump()?;
239 let _ = tunnel.tick()?;
240 }
241 println!(" ✓ handshake complete — tunnel is up");
242
243 for i in 1..=count {
246 let packet = mesh_datagram::encode_datagram(ports::CHAT, ports::CHAT, message.as_bytes());
247 let transmitted = tunnel.send_packet(&packet)?;
248 if transmitted {
249 println!(" → sent [{i}/{count}]: \"{message}\"");
250 } else {
251 println!(" → queued [{i}/{count}] (awaiting session): \"{message}\"");
252 }
253 let until = Instant::now() + Duration::from_millis(900);
255 while Instant::now() < until {
256 let _ = tunnel.pump()?;
257 }
258 }
259 println!(" done. Delivery is best-effort UDP; the listener prints each datagram it decrypts.");
260 Ok(())
261}