1use std::sync::Arc;
9
10use crate::ipc::MessageTarget;
11use crate::mesh::{MeshTransport, TransportListener};
12use crate::mesh_admit::{
13 AdmitContext, AdmitHello, Admission, DialIdentity, AdmissionGate, ChannelBinding, ChannelKind, HelloFailure,
14 PeerClass, PeerLimits, Refusal,
15};
16use crate::mesh_assess::{AssessmentEnvelope, AssessmentTransport};
17use crate::mesh_delivery::PeerCtx;
18use crate::mesh_ipc::{MeshIpcEnvelope, Scope};
19use crate::mesh_limits::{IpSlot, Limits, HANDSHAKE_TIMEOUT, MAX_CONNECTIONS, ROUTE_CHECK};
20use crate::mesh_noise::{
21 noise_static_public, EncryptedChannel, NoiseChannel, NoiseConfig, PassthroughChannel,
22};
23use crate::mesh_runtime::{MeshRuntime, RouteTally};
24use crate::mesh_leaf::{Prepare, MAX_LEAF_FRAME};
25use weftos_leaf_types::link::{parse_parent_scope, SignedPublish, FRAME_MAGIC, ACK_MAGIC};
26
27pub fn transport_for(name: &str, seed_peer: Option<&str>) -> Box<dyn MeshTransport> {
32 match name {
33 "ws" | "websocket" => Box::new(crate::mesh_ws::WsTransport),
34 #[cfg(feature = "quic")]
35 "quic" => Box::new(crate::mesh_quic::QuicTransport),
36 #[cfg(not(feature = "quic"))]
37 "quic" => {
38 match seed_peer {
39 Some(peer) => tracing::error!(
40 peer = %peer,
41 "seed peer uses quic but binary built without `quic` feature; falling back to tcp"
42 ),
43 None => tracing::error!(
44 "mesh transport=quic requested but clawft-kernel built without `quic` feature; falling back to tcp"
45 ),
46 }
47 Box::new(crate::mesh_tcp::TcpTransport)
48 }
49 _ => {
50 let _ = seed_peer;
51 Box::new(crate::mesh_tcp::TcpTransport)
52 }
53 }
54}
55
56pub async fn serve_listener(
65 runtime: Arc<MeshRuntime>,
66 listener: Box<dyn TransportListener>,
67 noise: Option<Arc<NoiseConfig>>,
68 transport_name: &str,
69 listen_addr: &str,
70 gate: Arc<dyn AdmissionGate>,
71) {
72 serve_listener_with(
73 runtime,
74 listener,
75 noise,
76 transport_name,
77 listen_addr,
78 gate,
79 Limits::default(),
80 )
81 .await;
82}
83
84pub async fn serve_listener_with(
86 runtime: Arc<MeshRuntime>,
87 mut listener: Box<dyn TransportListener>,
88 noise: Option<Arc<NoiseConfig>>,
89 transport_name: &str,
90 listen_addr: &str,
91 gate: Arc<dyn AdmissionGate>,
92 limits: Limits,
93) {
94 let bind = listener
95 .local_addr()
96 .map(|a| a.to_string())
97 .unwrap_or_else(|_| listen_addr.to_owned());
98 tracing::info!(
99 transport = transport_name,
100 addr = %bind,
101 noise = noise.is_some(),
102 "mesh listener started"
103 );
104
105 let mut conns = tokio::task::JoinSet::new();
109 if let Some(ingress) = runtime.leaf_ingress().cloned()
110 && let Ok(mesh_addr) = bind.parse::<std::net::SocketAddr>()
111 && let Some(port) = mesh_addr.port().checked_add(2) {
112 let leaf_addr = std::net::SocketAddr::new(mesh_addr.ip(), port);
113 match crate::mesh_tcp::TcpTransport.listen(&leaf_addr.to_string()).await {
114 Ok(leaf_listener) => {
115 let leaf_rt = Arc::clone(&runtime);
116 conns.spawn(async move { serve_signed_leaf_listener(leaf_rt, leaf_listener, limits).await });
117 conns.spawn(async move {
118 if let Err(e) = ingress.serve_discovery(mesh_addr, leaf_addr).await {
119 tracing::error!(error = %e, "leaf discovery stopped");
120 }
121 });
122 tracing::info!(addr = %leaf_addr, "dedicated certified leaf listener started");
123 }
124 Err(e) => tracing::error!(addr = %leaf_addr, error = %e, "certified leaf listener unavailable; no leaf discovery started"),
125 }
126 }
127 let slots = Arc::new(tokio::sync::Semaphore::new(MAX_CONNECTIONS));
128 let per_ip = Arc::new(std::sync::Mutex::new(std::collections::HashMap::new()));
129 loop {
130 while conns.try_join_next().is_some() {}
131 match listener.accept().await {
132 Ok((stream, peer_addr)) => {
133 let Ok(permit) = Arc::clone(&slots).try_acquire_owned() else {
134 tracing::warn!(peer = %peer_addr, max = MAX_CONNECTIONS,
135 "mesh connection cap reached, dropping");
136 continue;
137 };
138 let Some(ip_slot) = IpSlot::acquire(&per_ip, peer_addr.ip(), limits.per_ip) else {
141 tracing::warn!(peer = %peer_addr, max = limits.per_ip,
142 "mesh per-IP connection cap reached, dropping");
143 continue;
144 };
145 let rt = Arc::clone(&runtime);
146 let nc = noise.clone();
147 let gate = Arc::clone(&gate);
148 conns.spawn(async move {
149 serve_connection(rt, stream, peer_addr, nc, gate, limits, false).await;
150 drop(permit);
151 drop(ip_slot);
152 });
153 }
154 Err(e) => {
155 tracing::warn!(error = %e, "mesh accept error");
156 }
157 }
158 }
159}
160
161async fn serve_signed_leaf_listener(runtime: Arc<MeshRuntime>, mut listener: Box<dyn TransportListener>, limits: Limits) {
165 let slots = Arc::new(tokio::sync::Semaphore::new(MAX_CONNECTIONS));
166 let per_ip = Arc::new(std::sync::Mutex::new(std::collections::HashMap::new()));
167 let mut conns = tokio::task::JoinSet::new();
168 loop {
169 while conns.try_join_next().is_some() {}
170 match listener.accept().await {
171 Ok((stream, peer)) => {
172 let Ok(permit) = Arc::clone(&slots).try_acquire_owned() else { continue };
173 let Some(ip_slot) = IpSlot::acquire(&per_ip, peer.ip(), limits.per_ip) else { continue };
174 let rt = Arc::clone(&runtime);
175 conns.spawn(async move {
176 serve_connection(rt, stream, peer, None, Arc::new(crate::mesh_admit::AllowAll), limits, true).await;
177 drop(permit);
178 drop(ip_slot);
179 });
180 }
181 Err(e) => tracing::warn!(error = %e, "certified leaf accept error"),
182 }
183 }
184}
185
186pub(crate) struct Active {
188 pub(crate) bound: Option<String>,
190 pub(crate) limits: PeerLimits,
191 pub(crate) trust_scope: bool,
192 pub(crate) admitted: bool,
195 pub(crate) class: PeerClass,
196 pub(crate) remote_static: Option<Vec<u8>>,
197 pub(crate) signed_leaf: bool,
199 pub(crate) leaf_cert: Option<weftos_leaf_types::link::LeafCertificate>,
200}
201
202impl Active {
203 fn peer_ctx(&self) -> PeerCtx {
205 PeerCtx {
206 peer_id: self.bound.clone().unwrap_or_default(),
207 node_verified: self.admitted,
208 class: self.class,
209 remote_static: self.remote_static.clone(),
210 src_scope: None,
211 }
212 }
213}
214
215fn unix_now() -> u64 {
216 std::time::SystemTime::now()
217 .duration_since(std::time::UNIX_EPOCH)
218 .map(|d| d.as_secs())
219 .unwrap_or(0)
220}
221
222async fn admit_first_frame(
225 channel: &dyn EncryptedChannel,
226 kind: ChannelKind,
227 gate: &dyn AdmissionGate,
228 data: Vec<u8>,
229) -> Result<(Active, Option<Vec<u8>>), Refusal> {
230 let legacy = AdmitContext { class: PeerClass::Legacy, channel: kind };
231 let binding = channel.handshake_hash().map(|h| ChannelBinding {
232 handshake_hash: h,
233 remote_static: channel.remote_static_key(),
234 });
235 let (decision, bound, rest) = match AdmitHello::parse_frame(&data) {
236 None => (
237 gate.admit_unverified(&HelloFailure::Missing, &legacy).await,
238 None,
239 Some(data),
240 ),
241 Some(Err(f)) => (gate.admit_unverified(&f, &legacy).await, None, None),
242 Some(Ok(hello)) => match hello.verify(binding.as_ref(), unix_now()) {
243 Ok(v) => {
244 let ctx = AdmitContext { class: v.class(), channel: kind };
245 (gate.admit(&v, &ctx).await, Some(v.node_id), None)
246 }
247 Err(f) => (gate.admit_unverified(&f, &legacy).await, None, None),
248 },
249 };
250 match decision {
251 Admission::Refuse(r) => Err(r),
252 Admission::Admit(g) => Ok((
253 Active {
254 admitted: g.admitted && bound.is_some(),
255 trust_scope: g.trust_scope && g.admitted && bound.is_some(),
256 limits: g.limits,
257 bound,
258 class: g.class,
259 remote_static: channel.remote_static_key().map(<[u8]>::to_vec),
260 signed_leaf: false,
261 leaf_cert: None,
262 },
263 rest,
264 )),
265 }
266}
267
268pub(crate) fn claimed_source(data: &[u8]) -> Option<String> {
271 if let Ok(env) = MeshIpcEnvelope::from_bytes(data) {
272 return Some(env.source_node);
273 }
274 AssessmentTransport::try_extract_payload(data)
275 .ok()
276 .flatten()
277 .and_then(|p| AssessmentEnvelope::from_bytes(&p).ok())
278 .map(|e| e.source_node)
279}
280
281pub(crate) fn screen_frame(data: Vec<u8>, act: &Active) -> Option<Vec<u8>> {
288 match MeshIpcEnvelope::from_bytes(&data) {
289 Ok(mut env) => {
290 if !act.admitted {
293 let protected = match (&env.message.target, &env.message.payload) {
294 (MessageTarget::Topic(t), _) if t.starts_with("substrate/") || t.starts_with("mesh.leaf.") => true,
295 (MessageTarget::Topic(t), crate::ipc::MessagePayload::Json(v)) if t == "ipc.publish" || t == "mesh.subscribe" =>
296 v.get("topic").and_then(|t| t.as_str()).is_some_and(|t| t.starts_with("mesh.leaf.") || t.starts_with("substrate/")),
297 _ => false,
298 };
299 if protected {
300 tracing::warn!("dropping unsigned legacy leaf frame");
301 return None;
302 }
303 }
304 if let Some(id) = &act.bound
305 && &env.source_node != id
306 {
307 tracing::warn!(claimed = %env.source_node, verified = %id,
308 "dropping envelope: source_node differs from admitted node id");
309 return None;
310 }
311 if act.limits == PeerLimits::Leaf {
312 let id = act.bound.as_deref().unwrap_or_default();
317 let allowed = matches!(&env.message.target, MessageTarget::Topic(t)
318 if t == "mesh.subscribe" || t.starts_with(&format!("substrate/{id}/")));
319 if !allowed {
320 tracing::warn!(node = id, "dropping leaf envelope outside substrate/<id>/");
321 return None;
322 }
323 }
324 if env.src_scope.is_some() && !act.trust_scope {
325 env.src_scope = None;
326 return env.to_bytes().ok();
327 }
328 Some(data)
329 }
330 Err(_) => {
331 if act.limits == PeerLimits::Leaf {
332 return None;
333 }
334 if let Some(id) = &act.bound {
335 let src = AssessmentTransport::try_extract_payload(&data)
336 .ok()
337 .flatten()
338 .and_then(|p| AssessmentEnvelope::from_bytes(&p).ok())
339 .map(|e| e.source_node);
340 if src.is_some_and(|s| &s != id) {
341 tracing::warn!(verified = %id, "dropping assessment frame: source_node mismatch");
342 return None;
343 }
344 }
345 Some(data)
346 }
347 }
348}
349
350fn leaf_routed_envelope(frame: &SignedPublish) -> Option<Vec<u8>> {
354 let id = frame.cert.leaf_id();
355 let mut env = MeshIpcEnvelope::from_bytes(&frame.payload).ok()?;
356 if env.source_node != id { return None; }
357 env.src_scope = None;
361 let (user_id, project_id) = parse_parent_scope(&frame.cert.parent_scope).ok()?;
362 env.dest_scope = Some(Scope { user_id: user_id.to_owned(), project_id: project_id.map(str::to_owned) });
363 let crate::ipc::MessagePayload::Json(v) = &env.message.payload else { return None };
364 let inner_topic = v.get("topic")?.as_str()?;
365 match (&env.message.target, frame.target.as_str()) {
366 (MessageTarget::Topic(t), "mesh.subscribe") if t == "mesh.subscribe"
367 && inner_topic == weftos_leaf_types::push_topic(&id) => env.to_bytes().ok(),
368 (MessageTarget::Topic(t), target) if t == "ipc.publish" && inner_topic == target
369 && (target == format!("mesh.leaf.{id}.input") || target == format!("mesh.leaf.{id}.announce")) => {
370 let message = v.get("message")?.as_str()?;
371 let payload = match serde_json::from_str::<serde_json::Value>(message) {
372 Ok(json) => crate::ipc::MessagePayload::Json(json),
373 Err(_) => crate::ipc::MessagePayload::Text(message.to_owned()),
374 };
375 env.message.target = MessageTarget::Topic(target.to_owned());
376 env.message.payload = payload;
377 env.to_bytes().ok()
378 }
379 _ => None,
380 }
381}
382
383async fn serve_connection(
387 rt: Arc<MeshRuntime>,
388 stream: Box<dyn crate::mesh::MeshStream>,
389 peer_addr: std::net::SocketAddr,
390 nc: Option<Arc<NoiseConfig>>,
391 gate: Arc<dyn AdmissionGate>,
392 limits: Limits,
393 leaf_only: bool,
394) {
395 tracing::info!(
396 peer = %peer_addr,
397 noise = nc.is_some(),
398 "mesh peer connected"
399 );
400
401 let channel: Box<dyn EncryptedChannel> = match &nc {
403 Some(cfg) => match tokio::time::timeout(
404 HANDSHAKE_TIMEOUT,
405 NoiseChannel::respond(stream, cfg),
406 )
407 .await
408 .unwrap_or_else(|_| Err(crate::mesh::MeshError::Handshake("handshake timed out".into())))
409 {
410 Ok(ch) => {
411 tracing::info!(peer = %peer_addr, "noise handshake complete");
412 Box::new(ch)
413 }
414 Err(e) => {
415 tracing::warn!(peer = %peer_addr, error = %e, "noise handshake failed, dropping");
416 return;
417 }
418 },
419 None => Box::new(PassthroughChannel::new(stream)),
420 };
421 let kind = if nc.is_some() { ChannelKind::Noise } else { ChannelKind::Passthrough };
422 let (out_tx, out_rx) = tokio::sync::mpsc::channel::<Vec<u8>>(256);
423 pump(&rt, channel, kind, &peer_addr.to_string(), &*gate, limits, out_tx, out_rx, None, RouteTally::default(), false, leaf_only).await;
424}
425
426#[allow(clippy::too_many_arguments)]
438async fn pump(
439 rt: &Arc<MeshRuntime>,
440 mut channel: Box<dyn EncryptedChannel>,
441 kind: ChannelKind,
442 peer_addr: &str,
443 gate: &dyn AdmissionGate,
444 limits: Limits,
445 out_tx: tokio::sync::mpsc::Sender<Vec<u8>>,
446 mut out_rx: tokio::sync::mpsc::Receiver<Vec<u8>>,
447 mut active: Option<Active>,
448 tally: RouteTally,
449 dialled: bool,
450 leaf_only: bool,
451) {
452 let _routes = ConnRoutes { rt: Arc::clone(rt), tx: out_tx.clone() };
455 let strict = gate.strict();
470 let mut routed = tally.live() > 0;
475 let mut route_check = tokio::time::interval(ROUTE_CHECK);
476 route_check.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
477 let mut last_activity = tokio::time::Instant::now();
480 let mut mismatched = 0u32;
484 loop {
485 let limit = if active.is_none() {
492 Some(limits.first_frame)
493 } else {
494 (strict || dialled).then_some(limits.idle)
495 };
496 let timer = async {
497 match limit {
498 Some(d) => tokio::time::sleep_until(last_activity + d).await,
499 None => std::future::pending::<()>().await,
500 }
501 };
502 tokio::select! {
503 _ = timer => {
504 tracing::warn!(peer = %peer_addr, "mesh connection timed out, dropping");
505 break;
506 }
507 _ = route_check.tick() => {
508 if routed && tally.live() == 0 {
509 tracing::info!(peer = %peer_addr, "mesh route removed (disconnect or revocation), closing");
510 break;
511 }
512 if let Some(id) = active.as_ref().and_then(|a| a.bound.as_deref())
513 && gate.is_revoked(id)
514 {
515 tracing::warn!(peer = %peer_addr, node = id, "mesh peer revoked, closing");
516 break;
517 }
518 if let Some(cert) = active.as_ref().and_then(|a| a.leaf_cert.as_ref())
519 && !rt.leaf_ingress().is_some_and(|ingress| ingress.still_enrolled(cert, unix_now())) {
520 tracing::warn!(peer = %peer_addr, node = %cert.leaf_id(), "certified leaf enrollment expired or revoked; closing route");
521 break;
522 }
523 }
524 inbound = channel.recv_encrypted() => match inbound {
525 Ok(data) => {
526 last_activity = tokio::time::Instant::now();
527 if leaf_only && !data.starts_with(FRAME_MAGIC) {
528 tracing::warn!(peer = %peer_addr, "non-certified frame on leaf-only listener");
529 break;
530 }
531 if data.starts_with(FRAME_MAGIC) {
532 if data.len() > MAX_LEAF_FRAME { break; }
533 let Some(ingress) = rt.leaf_ingress() else {
534 tracing::warn!(peer = %peer_addr, "signed leaf frame but no registry installed");
535 break;
536 };
537 let Ok(frame) = weftos_leaf_types::decode::<SignedPublish>(&data[FRAME_MAGIC.len()..]) else { break };
538 let id = frame.cert.leaf_id();
539 if active.as_ref().is_some_and(|a| !a.signed_leaf || a.bound.as_deref() != Some(&id)
540 || a.leaf_cert.as_ref() != Some(&frame.cert)) { break; }
541 let _guard = ingress.lock.lock().await;
542 let prepared = match ingress.prepare(&frame, unix_now()) {
543 Ok(p) => p,
544 Err(e) => { tracing::warn!(peer = %peer_addr, node = %id, error = %e, "leaf frame refused"); break; }
545 };
546 let Some(routed_frame) = leaf_routed_envelope(&frame) else {
547 tracing::warn!(peer = %peer_addr, node = %id, "leaf payload does not match signed target");
548 break;
549 };
550 if active.is_none() {
551 if !rt.register_authenticated_as(id.clone(), out_tx.clone(), true, PeerClass::Leaf, &tally) {
552 tracing::warn!(peer = %peer_addr, node = %id, "leaf route refused");
553 break;
554 }
555 active = Some(Active {
556 bound: Some(id.clone()), limits: PeerLimits::Leaf, trust_scope: false,
557 admitted: true, class: PeerClass::Leaf, remote_static: None, signed_leaf: true,
558 leaf_cert: Some(frame.cert.clone()),
559 });
560 routed = true;
561 }
562 if prepared == Prepare::New || frame.target == "mesh.subscribe" {
566 let ctx = active.as_ref().expect("set above").peer_ctx();
567 if let Err(e) = rt.handle_incoming_tallied(&routed_frame, out_tx.clone(), Some(&ctx), Some(&tally)).await {
568 tracing::warn!(peer = %peer_addr, node = %id, error = %e, "leaf delivery failed");
569 break;
570 }
571 if prepared == Prepare::New
572 && let Err(e) = ingress.commit(&frame) {
573 tracing::error!(node = %id, error = %e, "leaf replay floor commit failed; no ACK");
574 break;
575 }
576 }
577 let ack = ingress.ack(&frame);
578 let Ok(mut ack_bytes) = weftos_leaf_types::encode(&ack) else { break };
579 let mut wire = Vec::with_capacity(ACK_MAGIC.len() + ack_bytes.len());
580 wire.extend_from_slice(ACK_MAGIC);
581 wire.append(&mut ack_bytes);
582 if channel.send_encrypted(&wire).await.is_err() { break; }
583 continue;
584 }
585 if active.as_ref().is_some_and(|a| a.signed_leaf) {
586 tracing::warn!(peer = %peer_addr, "signed leaf sent an unsigned frame");
587 break;
588 }
589 let reciprocal = active.is_none() && serde_json::from_slice::<serde_json::Value>(&data)
590 .ok().is_some_and(|v| v.get("reciprocal").and_then(|v| v.as_bool()) == Some(true));
591 let frame = match active {
592 Some(_) => Some(data),
593 None => match admit_first_frame(&*channel, kind, gate, data).await {
594 Ok((act, rest)) => {
595 tracing::info!(peer = %peer_addr,
596 node = act.bound.as_deref().unwrap_or("-"), "mesh peer admitted");
597 if act.admitted
603 && let Some(id) = act.bound.as_deref()
604 && !rt.register_authenticated_as(
605 id.to_owned(), out_tx.clone(), true, act.class, &tally)
606 {
607 tracing::warn!(peer = %peer_addr, node = id,
608 "route refused for admitted peer, closing");
609 break;
610 }
611 if reciprocal {
612 let Some(auth) = rt.authentication() else { break };
613 let Some(hash) = channel.handshake_hash() else { break };
614 if !act.admitted { break; }
615 let reply = auth.identity.hello(hash, &auth.noise_static, unix_now());
616 if channel.send_encrypted(&reply.to_bytes()).await.is_err() { break; }
617 }
618 active = Some(act);
619 rest
620 }
621 Err(r) => {
622 tracing::warn!(peer = %peer_addr, code = r.code, detail = %r.detail,
623 "mesh peer refused, dropping");
624 break;
625 }
626 },
627 };
628 let Some(frame) = frame else { continue };
629 if dialled
630 && let Some(act) = active.as_mut()
631 && act.bound.is_none()
632 {
633 let Some(claimed) = claimed_source(&frame) else {
640 tracing::warn!(peer = %peer_addr,
641 "dialled peer sent a frame naming no node id before binding, dropping");
642 continue;
643 };
644 if rt.route_is_foreign(&claimed, &out_tx) {
645 tracing::warn!(peer = %peer_addr, claimed = %claimed,
646 "dialled peer claims an id routed elsewhere, dropping frame");
647 mismatched += 1;
648 if mismatched >= MAX_ID_MISMATCHES {
649 tracing::warn!(peer = %peer_addr, "dialled peer keeps claiming routed ids, closing");
650 break;
651 }
652 continue;
653 }
654 act.bound = Some(claimed);
655 }
656 let Some(act) = active.as_ref() else { break };
657 if let Some(id) = act.bound.as_deref()
660 && gate.is_revoked(id)
661 {
662 tracing::warn!(peer = %peer_addr, node = id, "mesh peer revoked, closing");
663 break;
664 }
665 let screened = screen_frame(frame, act);
666 if screened.is_none() && dialled && act.bound.is_some() {
667 mismatched += 1;
670 if mismatched >= MAX_ID_MISMATCHES {
671 tracing::warn!(peer = %peer_addr, node = act.bound.as_deref().unwrap_or("-"),
672 "dialled peer keeps speaking for other ids, closing");
673 break;
674 }
675 }
676 if let Some(frame) = screened {
677 let ctx = act.peer_ctx();
678 if let Err(e) = rt.handle_incoming_tallied(&frame, out_tx.clone(), Some(&ctx), Some(&tally)).await {
679 tracing::debug!(error = %e, "mesh message handling error");
680 }
681 routed = routed || tally.live() > 0;
682 }
683 }
684 Err(_) => break,
685 },
686 outbound = out_rx.recv() => match outbound {
687 Some(data) => {
688 if let Some(cert) = active.as_ref().and_then(|a| a.leaf_cert.as_ref())
689 && !rt.leaf_ingress().is_some_and(|ingress| ingress.still_enrolled(cert, unix_now())) {
690 tracing::warn!(peer = %peer_addr, node = %cert.leaf_id(), "certified leaf revoked before outbound push; closing route");
691 break;
692 }
693 if !dialled {
694 last_activity = tokio::time::Instant::now();
695 }
696 if channel.send_encrypted(&data).await.is_err() {
697 break;
698 }
699 }
700 None => break,
701 },
702 }
703 }
704}
705
706struct ConnRoutes {
709 rt: Arc<MeshRuntime>,
710 tx: tokio::sync::mpsc::Sender<Vec<u8>>,
711}
712
713impl Drop for ConnRoutes {
714 fn drop(&mut self) {
715 self.rt.disconnect_channel(&self.tx);
716 }
717}
718
719const MAX_ID_MISMATCHES: u32 = 3;
722
723const SEED_BACKOFF_BASE: std::time::Duration = std::time::Duration::from_secs(1);
727const SEED_BACKOFF_MAX: std::time::Duration = std::time::Duration::from_secs(60);
728const SEED_IDLE: std::time::Duration = std::time::Duration::from_secs(300);
730const SEED_STABLE: std::time::Duration = std::time::Duration::from_secs(30);
732
733pub(crate) fn seed_backoff(
735 failures: u32,
736 base: std::time::Duration,
737 max: std::time::Duration,
738) -> std::time::Duration {
739 let nominal = base.saturating_mul(1u32 << failures.min(16)).min(max);
740 let jitter = rand::random::<f64>() * 0.5 + 0.5;
741 nominal.mul_f64(jitter)
742}
743
744#[derive(Clone, Copy)]
746pub(crate) struct SeedTiming {
747 pub base: std::time::Duration,
748 pub max: std::time::Duration,
749 pub stable: std::time::Duration,
750 pub idle: std::time::Duration,
752}
753
754impl Default for SeedTiming {
755 fn default() -> Self {
756 Self { base: SEED_BACKOFF_BASE, max: SEED_BACKOFF_MAX, stable: SEED_STABLE, idle: SEED_IDLE }
757 }
758}
759
760#[derive(Debug, Clone, PartialEq, Eq)]
769pub struct SeedSpec {
770 pub addr: String,
772 pub node_id: Option<String>,
774}
775
776pub fn parse_seed(entry: &str) -> Result<SeedSpec, String> {
778 let entry = entry.trim();
779 let (addr, id) = match entry.rsplit_once('#') {
780 Some((a, i)) => (a, Some(i)),
781 None => (entry, None),
782 };
783 if addr.is_empty() || addr.chars().any(|c| c.is_whitespace() || c.is_control() || c == '#') {
784 return Err(format!("seed address {addr:?} is not usable"));
785 }
786 let node_id = match id {
787 None => None,
788 Some(i) if i.is_empty() || i.len() > 128 => {
789 return Err(format!("seed {addr}: pinned node id must be 1 to 128 characters"));
790 }
791 Some(i) if !i.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | ':' | '-')) => {
792 return Err(format!("seed {addr}: pinned node id has characters a node id cannot"));
793 }
794 Some(i) => Some(i.to_owned()),
795 };
796 Ok(SeedSpec { addr: addr.to_owned(), node_id })
797}
798
799pub fn seed_specs(entries: &[String]) -> Vec<SeedSpec> {
803 let mut seen: std::collections::HashMap<String, Option<String>> = Default::default();
804 let mut out = Vec::new();
805 for e in entries {
806 match parse_seed(e) {
807 Err(why) => tracing::warn!(seed = %e.split('#').next().unwrap_or(""), "{why}; seed skipped"),
808 Ok(spec) => match seen.get(&spec.addr) {
809 None => {
810 seen.insert(spec.addr.clone(), spec.node_id.clone());
811 out.push(spec);
812 }
813 Some(first) if *first != spec.node_id => tracing::warn!(peer = %spec.addr,
814 "seed listed twice with different pinned ids; the first is used"),
815 Some(_) => {}
816 },
817 }
818 }
819 out
820}
821
822pub fn connect_seeds(
832 runtime: &Arc<MeshRuntime>,
833 seed_peers: &[String],
834 transport_name: &str,
835 noise: Option<Arc<NoiseConfig>>,
836 identity: Option<Arc<DialIdentity>>,
837) -> Vec<tokio::task::JoinHandle<()>> {
838 connect_seeds_with(runtime, seed_peers, transport_name, noise, identity, SeedTiming::default())
839}
840
841pub(crate) fn connect_seeds_with(
842 runtime: &Arc<MeshRuntime>,
843 seed_peers: &[String],
844 transport_name: &str,
845 noise: Option<Arc<NoiseConfig>>,
846 identity: Option<Arc<DialIdentity>>,
847 timing: SeedTiming,
848) -> Vec<tokio::task::JoinHandle<()>> {
849 seed_specs(seed_peers)
850 .into_iter()
851 .map(|SeedSpec { addr, node_id: expected }| {
852 let rt = Arc::downgrade(runtime);
855 let transport_name = transport_name.to_owned();
856 let nc = noise.clone();
857 let identity = identity.clone();
858 tokio::spawn(async move {
859 let mut failures = 0u32;
860 loop {
861 let Some(rt) = rt.upgrade() else { return };
862 let started = tokio::time::Instant::now();
863 dial_seed_once(&rt, &addr, expected.as_deref(), &transport_name, nc.clone(), identity.clone(), timing.idle).await;
864 drop(rt);
865 if started.elapsed() >= timing.stable {
866 failures = 0;
867 }
868 let wait = seed_backoff(failures, timing.base, timing.max);
869 failures = failures.saturating_add(1);
870 tracing::info!(peer = %addr, retry_in_ms = wait.as_millis() as u64,
871 "seed connection down, will redial");
872 tokio::time::sleep(wait).await;
873 }
874 })
875 })
876 .collect()
877}
878
879async fn dial_seed_once(
882 rt: &Arc<MeshRuntime>,
883 addr: &str,
884 expected: Option<&str>,
885 transport_name: &str,
886 nc: Option<Arc<NoiseConfig>>,
887 identity: Option<Arc<DialIdentity>>,
888 idle: std::time::Duration,
889) {
890 let auth = rt.authentication();
891 let strict_seed = crate::mesh_admit_gate::nested_peer_ceiling_active()
892 || auth.is_some_and(|a| a.require_authenticated_seeds);
893 if strict_seed && (expected.is_none() || nc.is_none() || identity.is_none()
894 || !auth.is_some_and(|a| a.gate.strict())) {
895 tracing::warn!(peer = %addr, "nested seed refused: missing pin, Noise, identity or enforcing gate");
896 return;
897 }
898 let transport = transport_for(transport_name, Some(addr));
899 let stream = match transport.connect(addr).await {
900 Ok(s) => s,
901 Err(e) => {
902 tracing::warn!(peer = %addr, error = %e, "failed to connect to seed peer");
903 return;
904 }
905 };
906 let mut channel: Box<dyn EncryptedChannel> = match &nc {
908 Some(cfg) => match tokio::time::timeout(HANDSHAKE_TIMEOUT, NoiseChannel::initiate(stream, cfg))
909 .await
910 .unwrap_or_else(|_| {
911 Err(crate::mesh::MeshError::Handshake("handshake timed out".into()))
912 }) {
913 Ok(ch) => {
914 tracing::info!(peer = %addr, "noise handshake complete (initiator)");
915 Box::new(ch)
916 }
917 Err(e) => {
918 tracing::warn!(peer = %addr, error = %e, "noise handshake failed");
919 return;
920 }
921 },
922 None => Box::new(PassthroughChannel::new(stream)),
923 };
924 let kind = if nc.is_some() { ChannelKind::Noise } else { ChannelKind::Passthrough };
925
926 if let (Some(id), Some(cfg), Some(hash)) =
930 (&identity, &nc, channel.handshake_hash().map(<[u8]>::to_vec))
931 {
932 match noise_static_public(&cfg.local_private_key) {
933 Some(stat) => {
934 let hello = id.hello(&hash, &stat, unix_now());
935 let hello_bytes = if strict_seed {
936 let mut wire = serde_json::to_value(&hello).expect("hello serializes");
939 wire["reciprocal"] = serde_json::Value::Bool(true);
940 serde_json::to_vec(&wire).expect("hello serializes")
941 } else { hello.to_bytes() };
942 if channel.send_encrypted(&hello_bytes).await.is_err() {
943 tracing::warn!(peer = %addr, "failed to send admission hello");
944 return;
945 }
946 }
947 None => tracing::warn!(peer = %addr, "cannot derive noise static key"),
948 }
949 }
950
951 if strict_seed {
952 let gate = &*auth.expect("checked above").gate;
953 let admission = async {
954 let frame = channel.recv_encrypted().await.map_err(|e| e.to_string())?;
955 let (active, rest) = admit_first_frame(&*channel, kind, gate, frame).await
956 .map_err(|e| e.detail)?;
957 if !active.admitted || active.bound.as_deref() != expected || rest.is_some() {
958 return Err("seed did not prove the granted identity under admission".to_owned());
959 }
960 Ok(active)
961 };
962 let active = match tokio::time::timeout(HANDSHAKE_TIMEOUT, admission).await {
963 Ok(Ok(active)) => active,
964 _ => { tracing::warn!(peer = %addr, "reciprocal seed admission failed"); return; }
965 };
966 let (out_tx, out_rx) = tokio::sync::mpsc::channel(256);
967 let tally = RouteTally::default();
968 let id = active.bound.as_ref().expect("verified above");
969 if !rt.register_authenticated_as(id.clone(), out_tx.clone(), true, active.class, &tally) { return; }
970 rt.add_peer_tallied(addr.to_owned(), out_tx.clone(), &tally);
973 pump(rt, channel, kind, addr, gate, Limits { idle, ..Limits::default() },
974 out_tx, out_rx, Some(active), tally, true, false).await;
975 return;
976 }
977
978 let (out_tx, out_rx) = tokio::sync::mpsc::channel(256);
979 if let Some(id) = expected
981 && rt.route_is_foreign(id, &out_tx)
982 {
983 tracing::warn!(peer = %addr, pinned = %id,
984 "pinned seed id is routed through another connection, not dialling");
985 return;
986 }
987 let tally = RouteTally::default();
988 rt.add_peer_tallied(addr.to_owned(), out_tx.clone(), &tally);
989 tracing::info!(peer = %addr, noise = nc.is_some(), "connected to seed peer");
990 let active = Active {
995 bound: expected.map(str::to_owned),
996 limits: PeerLimits::None,
997 trust_scope: false,
998 admitted: false,
999 class: PeerClass::Legacy,
1000 remote_static: channel.remote_static_key().map(<[u8]>::to_vec),
1001 signed_leaf: false,
1002 leaf_cert: None,
1003 };
1004 let limits = Limits { idle, ..Limits::default() };
1005 pump(rt, channel, kind, addr, &crate::mesh_admit::AllowAll, limits, out_tx, out_rx, Some(active), tally, true, false).await;
1006}
1007
1008#[cfg(test)]
1009mod tests {
1010 use super::*;
1011 use crate::error::KernelResult;
1012 use crate::ipc::{KernelMessage, MessagePayload, MessageTarget};
1013 use crate::mesh_delivery::{LocalDelivery, PeerCtx};
1014 use crate::mesh_ipc::{MeshIpcEnvelope, Scope};
1015 use std::sync::Mutex;
1016
1017 #[derive(Default)]
1018 struct Recorder {
1019 got: Mutex<Vec<(Option<Scope>, KernelMessage)>>,
1020 }
1021
1022 #[async_trait::async_trait]
1023 impl LocalDelivery for Recorder {
1024 async fn deliver(
1025 &self,
1026 _from: &PeerCtx,
1027 scope: Option<&Scope>,
1028 msg: KernelMessage,
1029 ) -> KernelResult<()> {
1030 self.got.lock().unwrap().push((scope.cloned(), msg));
1031 Ok(())
1032 }
1033 }
1034
1035 #[tokio::test]
1036 async fn serve_listener_delivers_scoped_envelope_to_local_delivery() {
1037 let rec = Arc::new(Recorder::default());
1038 let mut rt = MeshRuntime::new("node-b".into());
1039 rt.set_local_delivery(rec.clone());
1040 let rt = Arc::new(rt);
1041
1042 let transport = crate::mesh_tcp::TcpTransport;
1043 let listener = transport.listen("127.0.0.1:0").await.unwrap();
1044 let addr = listener.local_addr().unwrap();
1045 let server = tokio::spawn(serve_listener(
1046 Arc::clone(&rt),
1047 listener,
1048 None,
1049 "tcp",
1050 "127.0.0.1:0",
1051 Arc::new(crate::mesh_admit::AllowAll),
1052 ));
1053
1054 let mut client = transport.connect(&addr.to_string()).await.unwrap();
1055 let msg = KernelMessage::text(0, MessageTarget::Topic("t.scoped".into()), "hi");
1056 let mut env = MeshIpcEnvelope::new("node-a".into(), "node-b".into(), msg);
1057 env.dest_scope = Some(Scope {
1058 user_id: "a".repeat(32),
1059 project_id: None,
1060 });
1061 client.send(&env.to_bytes().unwrap()).await.unwrap();
1062
1063 for _ in 0..100 {
1064 if !rec.got.lock().unwrap().is_empty() {
1065 break;
1066 }
1067 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
1068 }
1069 server.abort();
1070
1071 let got = rec.got.lock().unwrap();
1072 assert_eq!(got.len(), 1, "envelope not delivered");
1073 assert_eq!(got[0].0.as_ref().unwrap().user_id, "a".repeat(32));
1074 assert!(matches!(&got[0].1.payload, MessagePayload::Text(s) if s == "hi"));
1075 assert!(rt.peer_ids().contains(&"node-a".to_string()));
1077 }
1078}
1079
1080#[cfg(test)]
1081#[path = "mesh_serve_tests.rs"]
1082mod serve_tests;
1083
1084#[cfg(test)]
1085#[path = "mesh_nested_dial_tests.rs"]
1086mod nested_dial_tests;