1use crate::social_peers::SocialPeer;
16
17fn valid_wg_pubkey(hex_key: &str) -> bool {
21 hex_key.len() == 64 && hex_key.bytes().all(|b| b.is_ascii_hexdigit())
22}
23
24#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
27pub struct PeerMeshReport {
28 pub did: String,
30 pub display_name: String,
32 pub active: bool,
34 pub has_wg_key: bool,
36 pub has_endpoint: bool,
39 pub dialable_now: bool,
41 pub reachable: bool,
43 pub note: String,
45}
46
47pub 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#[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 #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
102 pub struct PeerAddOutcome {
103 pub did: String,
104 pub added: bool,
106 pub local_addr: Option<String>,
108 pub endpoint: Option<String>,
110 pub note: String,
112 }
113
114 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 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), peer("did:wf:roam", GOOD_KEY, None, true), peer("did:wf:off", GOOD_KEY, Some("203.0.113.6:51820"), false), peer("did:wf:nokey", "zz", Some("203.0.113.7:51820"), true), ];
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 #[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 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 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 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 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 #[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 drop(svc);
343 }
344}