1#![cfg(not(target_arch = "wasm32"))]
21
22use std::net::SocketAddr;
23use std::sync::mpsc::{channel, Receiver, RecvTimeoutError, Sender, TryRecvError};
24use std::thread::JoinHandle;
25use std::time::{Duration, Instant};
26
27use super::social_webnet::{MeshPacket, SocialWebNet};
28
29const TICK_INTERVAL: Duration = Duration::from_millis(1000);
31const IDLE_SLEEP: Duration = Duration::from_millis(5);
33
34enum Command {
36 AddPeer {
37 peer_id: String,
38 peer_pubkey_hex: String,
39 endpoint: Option<SocketAddr>,
40 reply: Sender<Result<SocketAddr, String>>,
41 },
42 SetEndpoint {
43 peer_id: String,
44 addr: SocketAddr,
45 reply: Sender<Result<(), String>>,
46 },
47 InitiateHandshake {
48 peer_id: String,
49 reply: Sender<Result<(), String>>,
50 },
51 Send {
52 peer_id: String,
53 inner: Vec<u8>,
54 reply: Sender<Result<bool, String>>,
55 },
56 RemovePeer {
57 peer_id: String,
58 reply: Sender<bool>,
59 },
60 Peers {
61 reply: Sender<Vec<String>>,
62 },
63 HasSession {
64 peer_id: String,
65 reply: Sender<bool>,
66 },
67 Shutdown,
68}
69
70#[derive(Clone)]
76pub struct MeshControl {
77 cmd_tx: Sender<Command>,
78}
79
80impl MeshControl {
81 fn request<T>(&self, make: impl FnOnce(Sender<T>) -> Command) -> Result<T, String> {
82 let (tx, rx) = channel::<T>();
83 self.cmd_tx
84 .send(make(tx))
85 .map_err(|_| "mesh thread is gone".to_string())?;
86 rx.recv()
87 .map_err(|_| "mesh thread dropped the reply".to_string())
88 }
89
90 pub fn add_peer(
92 &self,
93 peer_id: &str,
94 peer_pubkey_hex: &str,
95 endpoint: Option<SocketAddr>,
96 ) -> Result<SocketAddr, String> {
97 self.request(|reply| Command::AddPeer {
98 peer_id: peer_id.to_string(),
99 peer_pubkey_hex: peer_pubkey_hex.to_string(),
100 endpoint,
101 reply,
102 })?
103 }
104
105 pub fn set_peer_endpoint(&self, peer_id: &str, addr: SocketAddr) -> Result<(), String> {
107 self.request(|reply| Command::SetEndpoint {
108 peer_id: peer_id.to_string(),
109 addr,
110 reply,
111 })?
112 }
113
114 pub fn initiate_handshake(&self, peer_id: &str) -> Result<(), String> {
116 self.request(|reply| Command::InitiateHandshake {
117 peer_id: peer_id.to_string(),
118 reply,
119 })?
120 }
121
122 pub fn send(&self, peer_id: &str, inner: Vec<u8>) -> Result<bool, String> {
124 self.request(|reply| Command::Send {
125 peer_id: peer_id.to_string(),
126 inner,
127 reply,
128 })?
129 }
130
131 pub fn remove_peer(&self, peer_id: &str) -> Result<bool, String> {
133 self.request(|reply| Command::RemovePeer {
134 peer_id: peer_id.to_string(),
135 reply,
136 })
137 }
138
139 pub fn peers(&self) -> Result<Vec<String>, String> {
141 self.request(|reply| Command::Peers { reply })
142 }
143
144 pub fn has_session(&self, peer_id: &str) -> Result<bool, String> {
146 self.request(|reply| Command::HasSession {
147 peer_id: peer_id.to_string(),
148 reply,
149 })
150 }
151
152 pub fn wait_for_session(&self, peer_id: &str, timeout: Duration) -> bool {
154 let deadline = Instant::now() + timeout;
155 loop {
156 if self.has_session(peer_id).unwrap_or(false) {
157 return true;
158 }
159 if Instant::now() >= deadline {
160 return false;
161 }
162 std::thread::sleep(Duration::from_millis(10));
163 }
164 }
165}
166
167pub struct MeshService {
171 cmd_tx: Sender<Command>,
172 inbound_rx: Receiver<MeshPacket>,
173 handle: Option<JoinHandle<()>>,
174}
175
176impl MeshService {
177 pub fn spawn(mut mesh: SocialWebNet) -> MeshService {
180 let (cmd_tx, cmd_rx) = channel::<Command>();
181 let (inbound_tx, inbound_rx) = channel::<MeshPacket>();
182
183 let handle = std::thread::Builder::new()
184 .name("socialwebnet-mesh".into())
185 .spawn(move || run(&mut mesh, &cmd_rx, &inbound_tx))
186 .expect("spawn mesh thread");
187
188 MeshService {
189 cmd_tx,
190 inbound_rx,
191 handle: Some(handle),
192 }
193 }
194
195 pub fn control(&self) -> MeshControl {
198 MeshControl {
199 cmd_tx: self.cmd_tx.clone(),
200 }
201 }
202
203 fn request<T>(&self, make: impl FnOnce(Sender<T>) -> Command) -> Result<T, String> {
204 let (tx, rx) = channel::<T>();
205 self.cmd_tx
206 .send(make(tx))
207 .map_err(|_| "mesh thread is gone".to_string())?;
208 rx.recv()
209 .map_err(|_| "mesh thread dropped the reply".to_string())
210 }
211
212 pub fn add_peer(
214 &self,
215 peer_id: &str,
216 peer_pubkey_hex: &str,
217 endpoint: Option<SocketAddr>,
218 ) -> Result<SocketAddr, String> {
219 self.request(|reply| Command::AddPeer {
220 peer_id: peer_id.to_string(),
221 peer_pubkey_hex: peer_pubkey_hex.to_string(),
222 endpoint,
223 reply,
224 })?
225 }
226
227 pub fn set_peer_endpoint(&self, peer_id: &str, addr: SocketAddr) -> Result<(), String> {
229 self.request(|reply| Command::SetEndpoint {
230 peer_id: peer_id.to_string(),
231 addr,
232 reply,
233 })?
234 }
235
236 pub fn initiate_handshake(&self, peer_id: &str) -> Result<(), String> {
238 self.request(|reply| Command::InitiateHandshake {
239 peer_id: peer_id.to_string(),
240 reply,
241 })?
242 }
243
244 pub fn send(&self, peer_id: &str, inner: Vec<u8>) -> Result<bool, String> {
246 self.request(|reply| Command::Send {
247 peer_id: peer_id.to_string(),
248 inner,
249 reply,
250 })?
251 }
252
253 pub fn remove_peer(&self, peer_id: &str) -> Result<bool, String> {
255 self.request(|reply| Command::RemovePeer {
256 peer_id: peer_id.to_string(),
257 reply,
258 })
259 }
260
261 pub fn peers(&self) -> Result<Vec<String>, String> {
263 self.request(|reply| Command::Peers { reply })
264 }
265
266 pub fn has_session(&self, peer_id: &str) -> Result<bool, String> {
268 self.request(|reply| Command::HasSession {
269 peer_id: peer_id.to_string(),
270 reply,
271 })
272 }
273
274 pub fn try_recv(&self) -> Option<MeshPacket> {
276 self.inbound_rx.try_recv().ok()
277 }
278
279 pub fn recv_timeout(&self, timeout: Duration) -> Option<MeshPacket> {
281 match self.inbound_rx.recv_timeout(timeout) {
282 Ok(pkt) => Some(pkt),
283 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => None,
284 }
285 }
286
287 pub fn wait_for_session(&self, peer_id: &str, timeout: Duration) -> bool {
290 let deadline = Instant::now() + timeout;
291 loop {
292 if self.has_session(peer_id).unwrap_or(false) {
293 return true;
294 }
295 if Instant::now() >= deadline {
296 return false;
297 }
298 std::thread::sleep(Duration::from_millis(10));
299 }
300 }
301
302 pub fn shutdown(&mut self) {
304 let _ = self.cmd_tx.send(Command::Shutdown);
305 if let Some(h) = self.handle.take() {
306 let _ = h.join();
307 }
308 }
309}
310
311impl Drop for MeshService {
312 fn drop(&mut self) {
313 self.shutdown();
314 }
315}
316
317fn run(mesh: &mut SocialWebNet, cmd_rx: &Receiver<Command>, inbound_tx: &Sender<MeshPacket>) {
319 let mut last_tick = Instant::now();
320 loop {
321 loop {
323 match cmd_rx.try_recv() {
324 Ok(Command::Shutdown) | Err(TryRecvError::Disconnected) => return,
325 Ok(cmd) => handle_command(mesh, cmd),
326 Err(TryRecvError::Empty) => break,
327 }
328 }
329
330 let had_peers = !mesh.peers().is_empty();
333 for evt in mesh.pump_all() {
334 if let Ok(pkt) = evt {
335 if inbound_tx.send(pkt).is_err() {
336 return; }
338 }
339 }
340
341 if last_tick.elapsed() >= TICK_INTERVAL {
343 let _ = mesh.tick_all();
344 last_tick = Instant::now();
345 }
346
347 if !had_peers {
349 std::thread::sleep(IDLE_SLEEP);
350 }
351 }
352}
353
354fn handle_command(mesh: &mut SocialWebNet, cmd: Command) {
355 match cmd {
356 Command::AddPeer {
357 peer_id,
358 peer_pubkey_hex,
359 endpoint,
360 reply,
361 } => {
362 let _ = reply.send(mesh.add_peer(&peer_id, &peer_pubkey_hex, endpoint));
363 }
364 Command::SetEndpoint {
365 peer_id,
366 addr,
367 reply,
368 } => {
369 let _ = reply.send(mesh.set_peer_endpoint(&peer_id, addr));
370 }
371 Command::InitiateHandshake { peer_id, reply } => {
372 let _ = reply.send(mesh.initiate_handshake(&peer_id));
373 }
374 Command::Send {
375 peer_id,
376 inner,
377 reply,
378 } => {
379 let _ = reply.send(mesh.send_to(&peer_id, &inner));
380 }
381 Command::RemovePeer { peer_id, reply } => {
382 let _ = reply.send(mesh.remove_peer(&peer_id));
383 }
384 Command::Peers { reply } => {
385 let _ = reply.send(mesh.peers());
386 }
387 Command::HasSession { peer_id, reply } => {
388 let _ = reply.send(mesh.has_session(&peer_id));
389 }
390 Command::Shutdown => {}
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397 use crate::p2p::wireguard_userspace::generate_keypair;
398
399 fn v6(body: &[u8]) -> Vec<u8> {
400 let mut p = vec![0u8; 40 + body.len()];
401 p[0] = 0x60;
402 p[4..6].copy_from_slice(&(body.len() as u16).to_be_bytes());
403 p[6] = 17;
404 p[7] = 64;
405 p[8] = 0xfd;
406 p[23] = 0x01;
407 p[24] = 0xfd;
408 p[39] = 0x02;
409 p[40..].copy_from_slice(body);
410 p
411 }
412
413 #[test]
417 fn two_services_peer_and_deliver_over_channels() {
418 let a_keys = generate_keypair();
419 let b_keys = generate_keypair();
420 let a_pub = a_keys.public_hex();
421 let b_pub = b_keys.public_hex();
422
423 let to = Some(Duration::from_millis(50));
424 let ip = "127.0.0.1".parse().unwrap();
425 let a = MeshService::spawn(SocialWebNet::new(a_keys, ip, to));
426 let b = MeshService::spawn(SocialWebNet::new(b_keys, ip, to));
427
428 let a_local = a.add_peer("did:wf:bob", &b_pub, None).expect("A adds B");
430 let b_local = b.add_peer("did:wf:alice", &a_pub, None).expect("B adds A");
431 assert_eq!(a.peers().unwrap(), vec!["did:wf:bob".to_string()]);
432
433 a.set_peer_endpoint("did:wf:bob", b_local).unwrap();
435 b.set_peer_endpoint("did:wf:alice", a_local).unwrap();
436 a.initiate_handshake("did:wf:bob").unwrap();
437
438 assert!(
439 a.wait_for_session("did:wf:bob", Duration::from_secs(10)),
440 "session established via the service threads"
441 );
442
443 let payload = v6(b"packet over the running mesh service");
445 assert!(a.send("did:wf:bob", payload.clone()).unwrap());
446
447 let pkt = b
448 .recv_timeout(Duration::from_secs(10))
449 .expect("B received the inner packet");
450 assert_eq!(pkt.peer_id, "did:wf:alice");
451 assert_eq!(pkt.inner, payload);
452 }
453
454 #[test]
455 fn shutdown_joins_the_thread() {
456 let keys = generate_keypair();
457 let ip = "127.0.0.1".parse().unwrap();
458 let mut svc = MeshService::spawn(SocialWebNet::new(keys, ip, None));
459 assert!(svc.peers().unwrap().is_empty());
460 svc.shutdown();
461 assert!(svc.peers().is_err(), "no commands after shutdown");
463 }
464}