Skip to main content

qualia_core_db/p2p/
social_webnet.rs

1// SocialWebNet — the managed userspace-WireGuard mesh keyed by peer identity.
2//
3// `wireguard_runtime::WgTunnel` is a single point-to-point tunnel. `SocialWebNet` is the layer
4// above it: the node's whole set of tunnels, one per peer, keyed by a stable peer id (the
5// pairwise DID the coordination plane uses). It is the mechanism the *social* layer drives —
6// the address book / connection-identifier exchange decides *who* to peer with and hands this
7// mesh the peer's WireGuard public key and (once known) endpoint; this mesh owns the crypto
8// state and the sockets.
9//
10// Separation of concerns:
11//   * **This module (core-db)** owns the mesh mechanism: bind sockets, build `Tunn`s, drive
12//     handshakes/timers, route inner packets to/from the right peer. It speaks raw keys and
13//     endpoints, so it is testable with zero social/identity dependencies (two meshes on
14//     loopback, below).
15//   * **The client-core social layer** binds this to identity: it feeds `add_peer` from the
16//     `social_peers` store + `node_identity`, and learns endpoints from `connection_identifier`
17//     rendezvous hints. That wiring lives there, not here.
18//
19// Socket model: **one UDP socket per peer** (each tunnel binds its own port). This is the
20// straightforward, fully-working design — each peer's coordination record advertises that peer's
21// port. WireGuard's own kernel implementation instead multiplexes all peers over a single socket,
22// demultiplexing by the receiver-index in each datagram; adopting that here is a pure efficiency
23// refinement (fewer sockets) and is noted as future work — it does not change correctness.
24//
25// Native-only (`boringtun` does not build for wasm32); WASM peers use a relay.
26#![cfg(not(target_arch = "wasm32"))]
27
28use std::collections::HashMap;
29use std::net::{IpAddr, SocketAddr};
30use std::time::Duration;
31
32use super::wireguard_runtime::{TunnelEvent, WgTunnel};
33use super::wireguard_userspace::{public_key_from_hex, WgKeypair};
34
35/// A decrypted inner IPv6 packet, tagged with the peer it came from.
36#[derive(Debug)]
37pub struct MeshPacket {
38    /// The peer id (pairwise DID) the packet arrived from.
39    pub peer_id: String,
40    /// The decrypted inner IPv6 packet.
41    pub inner: Vec<u8>,
42}
43
44/// The node's managed WireGuard mesh: a set of peer tunnels sharing this node's static keypair.
45pub struct SocialWebNet {
46    /// This node's WireGuard static keypair (shared by every tunnel).
47    keys: WgKeypair,
48    /// The IP to bind each per-peer socket on (port is always OS-chosen).
49    bind_ip: IpAddr,
50    /// peer id (pairwise DID) → its tunnel.
51    tunnels: HashMap<String, WgTunnel>,
52    /// Monotonic WireGuard session index handed to each new tunnel (must be distinct per tunnel).
53    next_index: u32,
54    /// Read timeout applied to each peer socket so pumping never blocks the mesh loop.
55    read_timeout: Option<Duration>,
56}
57
58impl SocialWebNet {
59    /// Create an empty mesh for this node. `bind_ip` is where per-peer sockets bind (e.g.
60    /// `0.0.0.0` / `::` in production, `127.0.0.1` in tests); `read_timeout` bounds each
61    /// [`pump`](SocialWebNet::pump) so a quiet peer does not stall the loop.
62    pub fn new(keys: WgKeypair, bind_ip: IpAddr, read_timeout: Option<Duration>) -> SocialWebNet {
63        SocialWebNet {
64            keys,
65            bind_ip,
66            tunnels: HashMap::new(),
67            next_index: 1,
68            read_timeout,
69        }
70    }
71
72    /// This node's WireGuard public key as lowercase hex — what peers need to address it.
73    pub fn public_key_hex(&self) -> String {
74        self.keys.public_hex()
75    }
76
77    /// The peer ids currently in the mesh.
78    pub fn peers(&self) -> Vec<String> {
79        self.tunnels.keys().cloned().collect()
80    }
81
82    /// Add a peer to the mesh: bind a fresh socket and build its tunnel.
83    ///
84    /// `peer_pubkey_hex` is the peer's WireGuard public key (64 hex chars — as stored on a
85    /// `SocialPeer` / carried in a `ConnectionIdentifier`). `endpoint` is where to send to if
86    /// already known (else `None`, and it is learned by roaming from the first authenticated
87    /// packet). Returns the local socket address bound for this peer, so the caller can advertise
88    /// it back through the coordination plane. Idempotent-ish: adding an existing peer id replaces
89    /// its tunnel (a fresh socket + handshake state).
90    pub fn add_peer(
91        &mut self,
92        peer_id: &str,
93        peer_pubkey_hex: &str,
94        endpoint: Option<SocketAddr>,
95    ) -> Result<SocketAddr, String> {
96        let peer_public = public_key_from_hex(peer_pubkey_hex)?;
97        let index = self.next_index;
98        self.next_index = self.next_index.wrapping_add(1);
99
100        let bind_addr = SocketAddr::new(self.bind_ip, 0);
101        let mut tunnel = WgTunnel::bind(&self.keys, peer_public, bind_addr, index)?;
102        tunnel.set_read_timeout(self.read_timeout)?;
103        if let Some(ep) = endpoint {
104            tunnel.set_peer_endpoint(ep);
105        }
106        let local = tunnel.local_addr()?;
107        self.tunnels.insert(peer_id.to_string(), tunnel);
108        Ok(local)
109    }
110
111    /// Remove a peer and drop its tunnel/socket. Returns whether the peer was present.
112    pub fn remove_peer(&mut self, peer_id: &str) -> bool {
113        self.tunnels.remove(peer_id).is_some()
114    }
115
116    fn tunnel_mut(&mut self, peer_id: &str) -> Result<&mut WgTunnel, String> {
117        self.tunnels
118            .get_mut(peer_id)
119            .ok_or_else(|| format!("unknown peer '{peer_id}'"))
120    }
121
122    /// The local socket address bound for a peer, if present.
123    pub fn local_addr(&self, peer_id: &str) -> Option<SocketAddr> {
124        self.tunnels.get(peer_id).and_then(|t| t.local_addr().ok())
125    }
126
127    /// Point a peer's tunnel at `addr` (e.g. once its endpoint is learned from the coordination plane).
128    pub fn set_peer_endpoint(&mut self, peer_id: &str, addr: SocketAddr) -> Result<(), String> {
129        self.tunnel_mut(peer_id)?.set_peer_endpoint(addr);
130        Ok(())
131    }
132
133    /// Whether a live WireGuard session exists with a peer.
134    pub fn has_session(&self, peer_id: &str) -> bool {
135        self.tunnels.get(peer_id).is_some_and(|t| t.has_session())
136    }
137
138    /// Start the handshake with a peer (initiator side). Requires the peer's endpoint to be set.
139    pub fn initiate_handshake(&mut self, peer_id: &str) -> Result<(), String> {
140        self.tunnel_mut(peer_id)?.initiate_handshake()
141    }
142
143    /// Encrypt and send one inner IPv6 packet to a peer. See [`WgTunnel::send_packet`] for the
144    /// pre-session behaviour (a handshake init is sent and the caller retries once established).
145    pub fn send_to(&mut self, peer_id: &str, inner: &[u8]) -> Result<bool, String> {
146        self.tunnel_mut(peer_id)?.send_packet(inner)
147    }
148
149    /// Frame `payload` as an overlay IPv6/UDP datagram addressed to `dst_port` (from `src_port`) and
150    /// send it to a peer. This is the application-message path over the mesh: the receiver recovers
151    /// `(src_port, dst_port, payload)` with
152    /// [`mesh_datagram::decode_datagram`](super::mesh_datagram::decode_datagram) on the
153    /// [`MeshPacket::inner`] it pumps. See [`super::mesh_datagram::ports`] for well-known ports.
154    pub fn send_datagram(
155        &mut self,
156        peer_id: &str,
157        src_port: u16,
158        dst_port: u16,
159        payload: &[u8],
160    ) -> Result<bool, String> {
161        let pkt = super::mesh_datagram::encode_datagram(src_port, dst_port, payload);
162        self.send_to(peer_id, &pkt)
163    }
164
165    /// Pump one datagram for a single peer. See [`WgTunnel::pump`].
166    pub fn pump(&mut self, peer_id: &str) -> Result<TunnelEvent, String> {
167        self.tunnel_mut(peer_id)?.pump()
168    }
169
170    /// Pump every peer once, returning any decrypted inner packets (tagged by peer). Control-only
171    /// traffic and idle sockets produce nothing. A per-peer error is surfaced against that peer id
172    /// rather than aborting the whole sweep.
173    pub fn pump_all(&mut self) -> Vec<Result<MeshPacket, (String, String)>> {
174        let ids: Vec<String> = self.tunnels.keys().cloned().collect();
175        let mut out = Vec::new();
176        for id in ids {
177            match self.pump(&id) {
178                Ok(TunnelEvent::InnerPacket(inner)) => {
179                    out.push(Ok(MeshPacket { peer_id: id, inner }))
180                }
181                Ok(_) => {}
182                Err(e) => out.push(Err((id, e))),
183            }
184        }
185        out
186    }
187
188    /// Drive WireGuard timers for every peer once (call ~1 Hz). Per-peer errors are collected, not
189    /// fatal.
190    pub fn tick_all(&mut self) -> Vec<(String, String)> {
191        let ids: Vec<String> = self.tunnels.keys().cloned().collect();
192        let mut errs = Vec::new();
193        for id in ids {
194            if let Ok(t) = self.tunnel_mut(&id) {
195                if let Err(e) = t.tick() {
196                    errs.push((id, e));
197                }
198            }
199        }
200        errs
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207    use crate::p2p::wireguard_userspace::generate_keypair;
208
209    fn v6(payload: &[u8]) -> Vec<u8> {
210        let total = 40 + payload.len();
211        let mut p = vec![0u8; total];
212        p[0] = 0x60;
213        p[4..6].copy_from_slice(&(payload.len() as u16).to_be_bytes());
214        p[6] = 17;
215        p[7] = 64;
216        p[8] = 0xfd;
217        p[23] = 0x01;
218        p[24] = 0xfd;
219        p[39] = 0x02;
220        p[40..].copy_from_slice(payload);
221        p
222    }
223
224    /// Two full meshes, each holding the other as its single peer, over real loopback sockets:
225    /// bring up the tunnel via the mesh API and carry an inner IPv6 packet A→B by peer id.
226    #[test]
227    fn two_meshes_peer_and_exchange_by_id() {
228        let a_keys = generate_keypair();
229        let b_keys = generate_keypair();
230        let a_pub = a_keys.public_hex();
231        let b_pub = b_keys.public_hex();
232
233        let to = Some(Duration::from_millis(300));
234        let mut a = SocialWebNet::new(a_keys, "127.0.0.1".parse().unwrap(), to);
235        let mut b = SocialWebNet::new(b_keys, "127.0.0.1".parse().unwrap(), to);
236
237        // Each adds the other by peer id; endpoints unknown until both sockets are bound.
238        let a_local = a.add_peer("did:wf:bob", &b_pub, None).expect("A adds B");
239        let b_local = b.add_peer("did:wf:alice", &a_pub, None).expect("B adds A");
240
241        // Exchange the freshly-bound endpoints (the coordination plane's job in production).
242        a.set_peer_endpoint("did:wf:bob", b_local).unwrap();
243        b.set_peer_endpoint("did:wf:alice", a_local).unwrap();
244
245        // A initiates; drive both meshes until A holds a session.
246        a.initiate_handshake("did:wf:bob").expect("A initiates");
247        for _ in 0..20 {
248            if a.has_session("did:wf:bob") {
249                break;
250            }
251            let _ = b.pump_all();
252            let _ = a.pump_all();
253        }
254        assert!(
255            a.has_session("did:wf:bob"),
256            "A established a session with B"
257        );
258        let _ = b.pump_all(); // B consumes the keepalive to establish too
259        assert!(
260            b.has_session("did:wf:alice"),
261            "B established a session with A"
262        );
263
264        // A sends an inner IPv6 packet addressed to peer id "did:wf:bob".
265        let payload = v6(b"mesh packet by peer id");
266        assert!(a.send_to("did:wf:bob", &payload).expect("A sends"));
267
268        let mut got = None;
269        for _ in 0..10 {
270            for evt in b.pump_all() {
271                if let Ok(pkt) = evt {
272                    assert_eq!(pkt.peer_id, "did:wf:alice", "tagged with the sending peer");
273                    got = Some(pkt.inner);
274                }
275            }
276            if got.is_some() {
277                break;
278            }
279        }
280        assert_eq!(got.expect("B received the inner packet"), payload);
281    }
282
283    /// Two meshes exchange an *application datagram* (framed IPv6/UDP), and the receiver recovers the
284    /// ports + payload — proving the app-message layer rides on the tunnel end-to-end.
285    #[test]
286    fn two_meshes_exchange_an_application_datagram() {
287        use super::super::mesh_datagram::{decode_datagram, ports};
288
289        let a_keys = generate_keypair();
290        let b_keys = generate_keypair();
291        let (a_pub, b_pub) = (a_keys.public_hex(), b_keys.public_hex());
292
293        let to = Some(Duration::from_millis(300));
294        let mut a = SocialWebNet::new(a_keys, "127.0.0.1".parse().unwrap(), to);
295        let mut b = SocialWebNet::new(b_keys, "127.0.0.1".parse().unwrap(), to);
296
297        let a_local = a.add_peer("b", &b_pub, None).unwrap();
298        let b_local = b.add_peer("a", &a_pub, None).unwrap();
299        a.set_peer_endpoint("b", b_local).unwrap();
300        b.set_peer_endpoint("a", a_local).unwrap();
301
302        a.initiate_handshake("b").unwrap();
303        for _ in 0..20 {
304            if a.has_session("b") {
305                break;
306            }
307            let _ = b.pump_all();
308            let _ = a.pump_all();
309        }
310        assert!(a.has_session("b"));
311        let _ = b.pump_all();
312
313        // A sends a CHAT datagram to B.
314        assert!(a
315            .send_datagram("b", ports::CHAT, ports::CHAT, b"hi over the app layer")
316            .unwrap());
317
318        let mut got = None;
319        for _ in 0..10 {
320            for evt in b.pump_all() {
321                if let Ok(pkt) = evt {
322                    got = decode_datagram(&pkt.inner);
323                }
324            }
325            if got.is_some() {
326                break;
327            }
328        }
329        let d = got.expect("B decoded the datagram");
330        assert_eq!(d.dst_port, ports::CHAT, "demuxes on the chat port");
331        assert_eq!(d.payload, b"hi over the app layer");
332    }
333
334    #[test]
335    fn add_and_remove_peer_tracks_membership() {
336        let keys = generate_keypair();
337        let peer = generate_keypair();
338        let mut mesh = SocialWebNet::new(keys, "127.0.0.1".parse().unwrap(), None);
339
340        assert!(mesh.peers().is_empty());
341        mesh.add_peer("did:wf:x", &peer.public_hex(), None).unwrap();
342        assert_eq!(mesh.peers(), vec!["did:wf:x".to_string()]);
343        assert!(mesh.local_addr("did:wf:x").is_some());
344        assert!(!mesh.has_session("did:wf:x"), "no handshake yet");
345
346        assert!(mesh.remove_peer("did:wf:x"));
347        assert!(!mesh.remove_peer("did:wf:x"), "second remove is a no-op");
348        assert!(mesh.peers().is_empty());
349    }
350
351    #[test]
352    fn bad_peer_pubkey_is_rejected() {
353        let keys = generate_keypair();
354        let mut mesh = SocialWebNet::new(keys, "127.0.0.1".parse().unwrap(), None);
355        let err = mesh.add_peer("did:wf:x", "not-hex", None).unwrap_err();
356        assert!(err.contains("hex"), "surfaces the key-parse error: {err}");
357    }
358}