Skip to main content

qualia_client_core/
social_mesh.rs

1//! Social ↔ mesh bridge — bring the SocialWebNet mesh up from identity + the peer store.
2//!
3//! [`crate::node_identity::NodeIdentity`] holds this node's WireGuard secret; the
4//! [`crate::social_peers`] store holds accepted peers (their WireGuard public key, overlay
5//! address, and last-known endpoint). This module joins the two: it reports which peers are
6//! *dialable* (have the material to form a tunnel) for the UI, and — on native targets — builds a
7//! live [`SocialWebNet`](qualia_core_db::p2p::social_webnet::SocialWebNet) with a tunnel per
8//! reachable peer.
9//!
10//! The split mirrors the crate boundary: `qualia-core-db` owns the mesh *mechanism* (sockets,
11//! `boringtun`), this module owns the *social binding* (identity + peer records → mesh). The pure
12//! [`dialability`] report needs neither sockets nor `boringtun`, so it compiles everywhere
13//! (including the `wasm32` studio build); [`build_node_mesh`] is native-only.
14
15use crate::social_peers::SocialPeer;
16
17/// Whether a peer record carries a syntactically valid WireGuard public key (64 hex chars).
18///
19/// This is the minimum needed to address the peer on the mesh; it does not prove reachability.
20fn valid_wg_pubkey(hex_key: &str) -> bool {
21    hex_key.len() == 64 && hex_key.bytes().all(|b| b.is_ascii_hexdigit())
22}
23
24/// A per-peer report of whether the mesh can bring up a tunnel to them, and what (if anything) is
25/// missing. Drives the Connect/Directory UI's "can I reach this peer?" affordance.
26#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
27pub struct PeerMeshReport {
28    /// The peer's DID.
29    pub did: String,
30    /// Friendly label.
31    pub display_name: String,
32    /// The peering is switched on ([`SocialPeer::active`]).
33    pub active: bool,
34    /// The peer has a usable WireGuard public key.
35    pub has_wg_key: bool,
36    /// A transport endpoint is known, so we can dial immediately (vs. waiting for the peer to
37    /// reach us first, then learning their endpoint by roaming).
38    pub has_endpoint: bool,
39    /// Overall: can a tunnel be brought up *now* (active + key + endpoint)?
40    pub dialable_now: bool,
41    /// Can a tunnel form at all once the peer initiates (active + key, endpoint learned by roaming)?
42    pub reachable: bool,
43    /// Human-readable note on what is missing, or empty when `dialable_now`.
44    pub note: String,
45}
46
47/// Compute the dialability report for a set of peers — a pure, side-effect-free view usable on any
48/// target (native or wasm).
49pub fn dialability(peers: &[SocialPeer]) -> Vec<PeerMeshReport> {
50    peers
51        .iter()
52        .map(|p| {
53            let has_wg_key = valid_wg_pubkey(&p.wireguard_pubkey_hex);
54            let has_endpoint = p
55                .endpoint
56                .as_deref()
57                .map(|e| e.parse::<std::net::SocketAddr>().is_ok())
58                .unwrap_or(false);
59            let reachable = p.active && has_wg_key;
60            let dialable_now = reachable && has_endpoint;
61            let note = if !p.active {
62                "peering is switched off".to_string()
63            } else if !has_wg_key {
64                "no valid WireGuard public key".to_string()
65            } else if !has_endpoint {
66                "endpoint unknown — will connect when the peer reaches us (roaming)".to_string()
67            } else {
68                String::new()
69            };
70            PeerMeshReport {
71                did: p.did.clone(),
72                display_name: p.display_name.clone(),
73                active: p.active,
74                has_wg_key,
75                has_endpoint,
76                dialable_now,
77                reachable,
78                note,
79            }
80        })
81        .collect()
82}
83
84// ===========================================================================
85// Native mesh construction.
86// ===========================================================================
87
88#[cfg(not(target_arch = "wasm32"))]
89mod native {
90    use super::*;
91    use std::net::{IpAddr, SocketAddr};
92    use std::time::Duration;
93
94    use qualia_core_db::p2p::mesh_service::MeshService;
95    use qualia_core_db::p2p::social_webnet::SocialWebNet;
96    use qualia_core_db::p2p::wireguard_userspace::WgKeypair;
97
98    use crate::node_identity::NodeIdentity;
99
100    /// The outcome of adding one peer to the mesh.
101    #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
102    pub struct PeerAddOutcome {
103        pub did: String,
104        /// A tunnel was created for this peer.
105        pub added: bool,
106        /// The local socket the tunnel bound (advertise this back so the peer can reach us).
107        pub local_addr: Option<String>,
108        /// The endpoint we will dial, if known now.
109        pub endpoint: Option<String>,
110        /// Why the peer was skipped, or empty when `added`.
111        pub note: String,
112    }
113
114    /// Build a live [`SocialWebNet`] for this node, adding a tunnel for every *reachable* peer
115    /// (active + valid WireGuard key). Inactive or keyless peers are skipped with a note. Peers
116    /// whose endpoint is known are pre-pointed; the rest wait to learn it by roaming.
117    ///
118    /// Returns the mesh and a per-peer outcome list. Bringing up the handshake and pumping is the
119    /// caller's job (the mesh is a passive, caller-driven state machine — see
120    /// [`SocialWebNet::pump`]).
121    pub fn build_node_mesh(
122        identity: &NodeIdentity,
123        peers: &[SocialPeer],
124        bind_ip: IpAddr,
125        read_timeout: Option<Duration>,
126    ) -> Result<(SocialWebNet, Vec<PeerAddOutcome>), String> {
127        let keys = WgKeypair::from_secret_bytes(identity.wg_secret);
128        let mut mesh = SocialWebNet::new(keys, bind_ip, read_timeout);
129        let mut outcomes = Vec::with_capacity(peers.len());
130
131        for p in peers {
132            if !p.active {
133                outcomes.push(PeerAddOutcome {
134                    did: p.did.clone(),
135                    added: false,
136                    local_addr: None,
137                    endpoint: None,
138                    note: "peering is switched off".into(),
139                });
140                continue;
141            }
142            let endpoint: Option<SocketAddr> = p.endpoint.as_deref().and_then(|e| e.parse().ok());
143            match mesh.add_peer(&p.did, &p.wireguard_pubkey_hex, endpoint) {
144                Ok(local) => outcomes.push(PeerAddOutcome {
145                    did: p.did.clone(),
146                    added: true,
147                    local_addr: Some(local.to_string()),
148                    endpoint: endpoint.map(|e| e.to_string()),
149                    note: String::new(),
150                }),
151                Err(e) => outcomes.push(PeerAddOutcome {
152                    did: p.did.clone(),
153                    added: false,
154                    local_addr: None,
155                    endpoint: None,
156                    note: e,
157                }),
158            }
159        }
160        Ok((mesh, outcomes))
161    }
162
163    /// Build the mesh from identity + peers and start it running on its own thread.
164    ///
165    /// Convenience over [`build_node_mesh`] + [`MeshService::spawn`]: returns the running service
166    /// (drive it with `send`/`try_recv`/`initiate_handshake`) alongside the per-peer add outcomes.
167    /// This is the top of the stack the desktop process holds to run the SocialWebNet.
168    pub fn start_node_mesh_service(
169        identity: &NodeIdentity,
170        peers: &[SocialPeer],
171        bind_ip: IpAddr,
172        read_timeout: Option<Duration>,
173    ) -> Result<(MeshService, Vec<PeerAddOutcome>), String> {
174        let (mesh, outcomes) = build_node_mesh(identity, peers, bind_ip, read_timeout)?;
175        Ok((MeshService::spawn(mesh), outcomes))
176    }
177}
178
179#[cfg(not(target_arch = "wasm32"))]
180pub use native::{build_node_mesh, start_node_mesh_service, PeerAddOutcome};
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    fn peer(did: &str, wg: &str, endpoint: Option<&str>, active: bool) -> SocialPeer {
187        SocialPeer {
188            did: did.into(),
189            display_name: did.into(),
190            wireguard_pubkey_hex: wg.into(),
191            overlay_addr: "fd00::1".into(),
192            endpoint: endpoint.map(|e| e.into()),
193            relation_type: "spc:Collaboration".into(),
194            added_at: 0,
195            active,
196            envelope_pubkey_hex: None,
197        }
198    }
199
200    const GOOD_KEY: &str = "aa11bb22cc33dd44ee55ff6677889900aa11bb22cc33dd44ee55ff6677889900";
201
202    #[test]
203    fn dialability_classifies_peers() {
204        let peers = vec![
205            peer("did:wf:now", GOOD_KEY, Some("203.0.113.5:51820"), true), // dialable now
206            peer("did:wf:roam", GOOD_KEY, None, true), // reachable, wait for roaming
207            peer("did:wf:off", GOOD_KEY, Some("203.0.113.6:51820"), false), // inactive
208            peer("did:wf:nokey", "zz", Some("203.0.113.7:51820"), true), // bad key
209        ];
210        let r = dialability(&peers);
211
212        let now = &r[0];
213        assert!(now.dialable_now && now.reachable && now.has_endpoint && now.note.is_empty());
214
215        let roam = &r[1];
216        assert!(roam.reachable && !roam.dialable_now && !roam.has_endpoint);
217        assert!(roam.note.contains("roaming"));
218
219        let off = &r[2];
220        assert!(!off.reachable && !off.dialable_now);
221        assert!(off.note.contains("switched off"));
222
223        let nokey = &r[3];
224        assert!(!nokey.has_wg_key && !nokey.reachable);
225        assert!(nokey.note.contains("WireGuard"));
226    }
227
228    /// End-to-end social binding: two nodes' identities + peer records (pointing at each other)
229    /// build two live meshes that complete a handshake and carry an inner IPv6 packet over
230    /// loopback. Proves an *accepted peer record* is sufficient to form a real tunnel.
231    #[cfg(not(target_arch = "wasm32"))]
232    #[test]
233    fn accepted_peer_records_form_a_real_tunnel() {
234        use crate::node_identity::NodeIdentity;
235        use qualia_core_db::p2p::social_webnet::MeshPacket;
236        use qualia_core_db::p2p::wireguard_userspace::WgKeypair;
237        use std::time::Duration;
238
239        // Two node identities (in-memory; distinct wg secrets).
240        let a_id = NodeIdentity {
241            ed25519_secret: [1u8; 32],
242            wg_secret: [2u8; 32],
243        };
244        let b_id = NodeIdentity {
245            ed25519_secret: [3u8; 32],
246            wg_secret: [4u8; 32],
247        };
248        let a_wg = WgKeypair::from_secret_bytes(a_id.wg_secret).public_hex();
249        let b_wg = WgKeypair::from_secret_bytes(b_id.wg_secret).public_hex();
250
251        // Each node's peer store holds the other, keyed by DID, with the other's real WG pubkey.
252        let a_peers = vec![peer("did:wf:bob", &b_wg, None, true)];
253        let b_peers = vec![peer("did:wf:alice", &a_wg, None, true)];
254
255        let to = Some(Duration::from_millis(300));
256        let ip = "127.0.0.1".parse().unwrap();
257        let (mut a, a_out) = build_node_mesh(&a_id, &a_peers, ip, to).unwrap();
258        let (mut b, b_out) = build_node_mesh(&b_id, &b_peers, ip, to).unwrap();
259        assert!(
260            a_out[0].added && b_out[0].added,
261            "both peers added to their meshes"
262        );
263
264        // Exchange the bound endpoints (coordination-plane step) and connect.
265        let a_local: std::net::SocketAddr = a_out[0].local_addr.clone().unwrap().parse().unwrap();
266        let b_local: std::net::SocketAddr = b_out[0].local_addr.clone().unwrap().parse().unwrap();
267        a.set_peer_endpoint("did:wf:bob", b_local).unwrap();
268        b.set_peer_endpoint("did:wf:alice", a_local).unwrap();
269
270        a.initiate_handshake("did:wf:bob").unwrap();
271        for _ in 0..20 {
272            if a.has_session("did:wf:bob") {
273                break;
274            }
275            let _ = b.pump_all();
276            let _ = a.pump_all();
277        }
278        assert!(
279            a.has_session("did:wf:bob"),
280            "handshake completed from the peer records"
281        );
282        let _ = b.pump_all();
283
284        // Carry a packet A→B, addressed by the peer's DID.
285        let payload = {
286            let body = b"from an accepted peer record";
287            let mut p = vec![0u8; 40 + body.len()];
288            p[0] = 0x60;
289            p[4..6].copy_from_slice(&(body.len() as u16).to_be_bytes());
290            p[6] = 17;
291            p[7] = 64;
292            p[8] = 0xfd;
293            p[23] = 0x01;
294            p[24] = 0xfd;
295            p[39] = 0x02;
296            p[40..].copy_from_slice(body);
297            p
298        };
299        assert!(a.send_to("did:wf:bob", &payload).unwrap());
300
301        let mut got: Option<MeshPacket> = None;
302        for _ in 0..10 {
303            for evt in b.pump_all() {
304                if let Ok(pkt) = evt {
305                    got = Some(pkt);
306                }
307            }
308            if got.is_some() {
309                break;
310            }
311        }
312        let pkt = got.expect("B received the packet");
313        assert_eq!(pkt.peer_id, "did:wf:alice");
314        assert_eq!(pkt.inner, payload);
315    }
316
317    /// The running-service convenience: build from identity + peers and get a live `MeshService`
318    /// with the peer already added.
319    #[cfg(not(target_arch = "wasm32"))]
320    #[test]
321    fn start_node_mesh_service_runs_with_the_peer() {
322        use crate::node_identity::NodeIdentity;
323        use crate::social_mesh::start_node_mesh_service;
324        use std::time::Duration;
325
326        let id = NodeIdentity {
327            ed25519_secret: [5u8; 32],
328            wg_secret: [6u8; 32],
329        };
330        let peers = vec![peer("did:wf:peer", GOOD_KEY, None, true)];
331        let (svc, outcomes) = start_node_mesh_service(
332            &id,
333            &peers,
334            "127.0.0.1".parse().unwrap(),
335            Some(Duration::from_millis(50)),
336        )
337        .unwrap();
338
339        assert!(outcomes[0].added, "peer added to the running mesh");
340        assert_eq!(svc.peers().unwrap(), vec!["did:wf:peer".to_string()]);
341        // Clean shutdown (also exercised by Drop).
342        drop(svc);
343    }
344}