Skip to main content

thenodes/network/
peer_manager.rs

1// src/network/peer_manager.rs
2
3use parking_lot::RwLock as ParkingRwLock;
4use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
5use std::net::SocketAddr;
6use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
7use std::sync::{Arc, Weak};
8use std::time::Instant;
9use tokio::sync::mpsc::Sender;
10use tokio::sync::Mutex;
11
12use crate::config::{Config, DeliveryConfig};
13use crate::events::model::LogLevel;
14use crate::network::connection::{ConnectionPolicy, RouteHealth, RouteKind};
15use crate::network::delivery::{DeliveryClass, DeliveryOutcome, MessageId};
16use crate::network::events::emit_network_event;
17use crate::network::message::{Message, MessageType, Payload};
18use crate::network::{Peer, PeerSource, PeerStore};
19use crate::plugin_host::manager::PluginManager;
20use crate::realms::RealmInfo;
21
22#[derive(Clone, Debug)]
23struct DeliveryAttemptState {
24    class: DeliveryClass,
25    attempts: u32,
26    outcome: Option<DeliveryOutcome>,
27    expires_at: std::time::Instant,
28}
29
30#[derive(Clone, Debug)]
31struct OrderedDeliveryBuffer {
32    next_expected: u64,
33    pending: BTreeMap<u64, Message>,
34    /// Last time this scope received an in-sequence or buffered message.
35    /// Used by `prune_ordered_inbound` to evict stale scopes.
36    last_activity: std::time::Instant,
37}
38
39#[derive(Clone, Debug)]
40struct RouteHealthRecord {
41    health: RouteHealth,
42    last_success: Option<Instant>,
43    last_failure: Option<Instant>,
44}
45
46impl Default for RouteHealthRecord {
47    fn default() -> Self {
48        Self {
49            health: RouteHealth::Unknown,
50            last_success: None,
51            last_failure: None,
52        }
53    }
54}
55
56#[derive(Clone)]
57struct ConnectionLifecycleRuntime {
58    policy: ConnectionPolicy,
59    config: Config,
60    local_node_id: String,
61    peer_store: PeerStore,
62    plugin_manager: Weak<PluginManager>,
63    allow_console: bool,
64}
65
66/// Which transport is currently preferred for reaching a given peer.
67///
68/// Set by the UDP listener on handshake completion (ADR-0004 §2.3) and by the TCP listener
69/// on connect.  Defaults to `Tcp` when only a TCP connection is present.
70#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
71#[serde(rename_all = "lowercase")]
72pub enum TransportKind {
73    Tcp,
74    Udp,
75}
76
77/// Record for a pending relay-coordinated hole-punch request (ADR-0005 Phase 3).
78/// Keyed by `target_node_id` (the responder's node ID) in `PeerManager::pending_punches`.
79#[derive(Clone, Debug)]
80pub struct PendingPunch {
81    pub attempt_id: String,
82    /// Node ID of the peer that initiated the punch request.
83    pub initiator_node_id: String,
84    pub responder_node_id: String,
85    pub timeout_ms: u64,
86}
87
88#[derive(Clone, Debug)]
89pub struct ObservedUdpAddrRecord {
90    pub addr: String,
91    pub observed_at: std::time::Instant,
92    pub observer_node_id: Option<String>,
93    pub request_nonce: Option<[u8; 8]>,
94}
95
96#[derive(Clone, Debug)]
97pub struct PendingObservation {
98    pub observer_node_id: String,
99    pub expected_source: SocketAddr,
100    pub nonce: [u8; 8],
101    pub requested_at: std::time::Instant,
102}
103
104/// Handle to the live UDP socket, Noise session map, and static private key.
105/// Stored in `PeerManager` once `spawn_udp_listener` has run.  Phase 3 punch execution
106/// reads from this to send UDP probes and begin Noise handshakes.
107#[cfg(feature = "noise")]
108#[derive(Clone)]
109pub(crate) struct UdpHandle {
110    pub socket: Arc<tokio::net::UdpSocket>,
111    pub sessions: crate::network::udp_listener::UdpSessions,
112    pub static_private: Vec<u8>,
113}
114
115#[derive(Clone)]
116#[allow(clippy::type_complexity)]
117pub struct PeerManager {
118    peers: Arc<Mutex<HashMap<SocketAddr, Sender<String>>>>,
119    node_ids: Arc<Mutex<HashMap<String, SocketAddr>>>, // node_id -> addr
120    // Mapping of a peer's advertised listening address (host:port) -> node_id.
121    // Lets us suppress redundant outbound dials when an inbound connection already exists.
122    listen_addrs: Arc<Mutex<HashMap<String, String>>>,
123    // Relay stats
124    relay_forwarded: Arc<AtomicU64>,
125    relay_dropped: Arc<AtomicU64>,
126    // Relay bindings: (from_node_id, to_node_id) -> BindingPrefs
127    relay_bindings: Arc<Mutex<HashMap<(String, String), BindingPrefs>>>,
128    // Store-and-forward queue: to_node_id -> Vec<(serialized_message_json, Option<u64>, Option<String /*origin*/>)>
129    relay_queue: Arc<Mutex<HashMap<String, Vec<(String, Option<u64>, Option<String>)>>>>,
130    // Store-and-forward queue caps (configurable via network.relay)
131    relay_queue_max_per_target: Arc<AtomicUsize>,
132    relay_queue_max_global: Arc<AtomicUsize>,
133    // Track last seen sequence per (from,to) for ordering/dedup
134    relay_last_sequence: Arc<Mutex<HashMap<(String, String), u64>>>,
135    capabilities_by_node: Arc<Mutex<HashMap<String, Vec<String>>>>,
136    // Map binding_id -> (from,to)
137    relay_binding_ids: Arc<Mutex<HashMap<String, (String, String)>>>,
138    // In-flight reliable forwards keyed by (from,to,sequence)
139    reliable_inflight: Arc<Mutex<HashSet<(String, String, u64)>>>,
140    // Framework delivery state (ADR-0006)
141    delivery_attempts: Arc<Mutex<HashMap<String, DeliveryAttemptState>>>,
142    delivery_dedup: Arc<Mutex<HashMap<(String, String), std::time::Instant>>>,
143    delivery_next_ordering_sequence: Arc<Mutex<HashMap<(String, String, String), u64>>>,
144    delivery_ordered_inbound: Arc<Mutex<HashMap<(String, String, String), OrderedDeliveryBuffer>>>,
145    relay_next_sequence: Arc<Mutex<HashMap<(String, String), u64>>>,
146    delivery_dedup_window_secs: Arc<AtomicU64>,
147    delivery_ordered_buffer_limit: Arc<AtomicUsize>,
148    route_health: Arc<Mutex<HashMap<(String, RouteKind), RouteHealthRecord>>>,
149    last_peer_activity: Arc<Mutex<HashMap<String, Instant>>>,
150    lifecycle_generations: Arc<Mutex<HashMap<String, u64>>>,
151    next_lifecycle_generation: Arc<AtomicU64>,
152    reconnecting_peers: Arc<Mutex<HashSet<String>>>,
153    lifecycle_runtime: Arc<Mutex<Option<ConnectionLifecycleRuntime>>>,
154    // ── UDP transport (ADR-0004) ──────────────────────────────────────────────────
155    /// Active transport kind per peer (node_id → TransportKind).
156    transport_kind: Arc<Mutex<HashMap<String, TransportKind>>>,
157    /// UDP listen addresses advertised by peers in their HELLO (node_id → "ip:port").
158    udp_listen_addrs: Arc<Mutex<HashMap<String, String>>>,
159    /// Active Noise session IDs keyed by node_id (set on handshake completion).
160    udp_session_ids: Arc<Mutex<HashMap<String, [u8; 8]>>>,
161    // ── NAT traversal / observed addresses (ADR-0005) ─────────────────────────────────────────
162    /// Own node's most recently observed public UDP address (from TNCF OBSERVE_RESP).
163    own_observed_addr: Arc<Mutex<Option<ObservedUdpAddrRecord>>>,
164    /// Fresh observed public UDP address samples keyed by observer identity.
165    own_observed_addr_samples: Arc<Mutex<HashMap<String, ObservedUdpAddrRecord>>>,
166    /// Observed public UDP addresses reported for remote peers (from their HELLO or OBSERVE_RESP).
167    /// The timestamp records when this node last received that observation data.
168    udp_observed_addrs: Arc<Mutex<HashMap<String, ObservedUdpAddrRecord>>>,
169    /// Gateway-reported external IP address discovered via UPnP-IGD.
170    gateway_external_ip: Arc<ParkingRwLock<Option<String>>>,
171    /// Public TCP address to advertise in HELLO once a direct inbound path is confirmed.
172    public_tcp_hello_addr: Arc<ParkingRwLock<Option<String>>>,
173    /// Lightweight capability hint for likely direct inbound TCP reachability.
174    direct_tcp_hint: Arc<AtomicBool>,
175    /// Last emitted NAT diagnostic summary; used to suppress duplicate events.
176    nat_diagnostic_summary: Arc<ParkingRwLock<Option<String>>>,
177    /// Pending punch requests keyed by correlation id.  (ADR-0005 Phase 3)
178    pending_punches: Arc<Mutex<HashMap<String, PendingPunch>>>,
179    pending_observations: Arc<Mutex<HashMap<[u8; 8], PendingObservation>>>,
180    /// UDP socket handle — set by `set_udp_handle` after `spawn_udp_listener`.  (Phase 3)
181    #[cfg(feature = "noise")]
182    udp_handle: Arc<Mutex<Option<UdpHandle>>>,
183    /// NatState for cookie and probe configuration.  Set by `set_nat_state` in main.  (Phase 3)
184    #[cfg(feature = "noise")]
185    nat_state:
186        Arc<tokio::sync::RwLock<Option<std::sync::Arc<crate::network::nat_traversal::NatState>>>>,
187}
188
189impl Default for PeerManager {
190    fn default() -> Self {
191        Self::new()
192    }
193}
194
195impl PeerManager {
196    const DEFAULT_QUEUE_MAX_PER_TARGET: usize = 1024;
197    const DEFAULT_QUEUE_MAX_GLOBAL: usize = 8192;
198
199    pub fn new() -> Self {
200        Self {
201            peers: Arc::new(Mutex::new(HashMap::new())),
202            node_ids: Arc::new(Mutex::new(HashMap::new())),
203            listen_addrs: Arc::new(Mutex::new(HashMap::new())),
204            capabilities_by_node: Arc::new(Mutex::new(HashMap::new())),
205            relay_forwarded: Arc::new(AtomicU64::new(0)),
206            relay_dropped: Arc::new(AtomicU64::new(0)),
207            relay_bindings: Arc::new(Mutex::new(HashMap::new())),
208            relay_queue: Arc::new(Mutex::new(HashMap::new())),
209            relay_queue_max_per_target: Arc::new(AtomicUsize::new(
210                Self::DEFAULT_QUEUE_MAX_PER_TARGET,
211            )),
212            relay_queue_max_global: Arc::new(AtomicUsize::new(Self::DEFAULT_QUEUE_MAX_GLOBAL)),
213            relay_binding_ids: Arc::new(Mutex::new(HashMap::new())),
214            relay_last_sequence: Arc::new(Mutex::new(HashMap::new())),
215            reliable_inflight: Arc::new(Mutex::new(HashSet::new())),
216            delivery_attempts: Arc::new(Mutex::new(HashMap::new())),
217            delivery_dedup: Arc::new(Mutex::new(HashMap::new())),
218            delivery_next_ordering_sequence: Arc::new(Mutex::new(HashMap::new())),
219            delivery_ordered_inbound: Arc::new(Mutex::new(HashMap::new())),
220            relay_next_sequence: Arc::new(Mutex::new(HashMap::new())),
221            delivery_dedup_window_secs: Arc::new(AtomicU64::new(3600)),
222            delivery_ordered_buffer_limit: Arc::new(AtomicUsize::new(1024)),
223            route_health: Arc::new(Mutex::new(HashMap::new())),
224            last_peer_activity: Arc::new(Mutex::new(HashMap::new())),
225            lifecycle_generations: Arc::new(Mutex::new(HashMap::new())),
226            next_lifecycle_generation: Arc::new(AtomicU64::new(1)),
227            reconnecting_peers: Arc::new(Mutex::new(HashSet::new())),
228            lifecycle_runtime: Arc::new(Mutex::new(None)),
229            transport_kind: Arc::new(Mutex::new(HashMap::new())),
230            udp_listen_addrs: Arc::new(Mutex::new(HashMap::new())),
231            udp_session_ids: Arc::new(Mutex::new(HashMap::new())),
232            own_observed_addr: Arc::new(Mutex::new(None)),
233            own_observed_addr_samples: Arc::new(Mutex::new(HashMap::new())),
234            udp_observed_addrs: Arc::new(Mutex::new(HashMap::new())),
235            gateway_external_ip: Arc::new(ParkingRwLock::new(None)),
236            public_tcp_hello_addr: Arc::new(ParkingRwLock::new(None)),
237            direct_tcp_hint: Arc::new(AtomicBool::new(false)),
238            nat_diagnostic_summary: Arc::new(ParkingRwLock::new(None)),
239            pending_punches: Arc::new(Mutex::new(HashMap::new())),
240            pending_observations: Arc::new(Mutex::new(HashMap::new())),
241            #[cfg(feature = "noise")]
242            udp_handle: Arc::new(Mutex::new(None)),
243            #[cfg(feature = "noise")]
244            nat_state: Arc::new(tokio::sync::RwLock::new(None)),
245        }
246    }
247
248    pub fn set_relay_queue_caps(
249        &self,
250        per_target: Option<usize>,
251        global: Option<usize>,
252    ) -> (usize, usize) {
253        let per_target_cap = per_target
254            .unwrap_or(Self::DEFAULT_QUEUE_MAX_PER_TARGET)
255            .max(1);
256        let global_cap = global.unwrap_or(Self::DEFAULT_QUEUE_MAX_GLOBAL).max(1);
257        self.relay_queue_max_per_target
258            .store(per_target_cap, Ordering::Relaxed);
259        self.relay_queue_max_global
260            .store(global_cap, Ordering::Relaxed);
261        (per_target_cap, global_cap)
262    }
263
264    pub fn relay_queue_caps(&self) -> (usize, usize) {
265        (
266            self.relay_queue_max_per_target.load(Ordering::Relaxed),
267            self.relay_queue_max_global.load(Ordering::Relaxed),
268        )
269    }
270
271    pub fn set_delivery_config(&self, delivery: Option<DeliveryConfig>) {
272        let delivery = delivery.unwrap_or_default();
273        self.delivery_dedup_window_secs.store(
274            delivery.dedup_window_secs.unwrap_or(3600),
275            Ordering::Relaxed,
276        );
277        self.delivery_ordered_buffer_limit.store(
278            delivery
279                .ordered_max_buffered_messages
280                .unwrap_or(1024)
281                .max(1),
282            Ordering::Relaxed,
283        );
284    }
285
286    pub fn delivery_dedup_window_secs(&self) -> u64 {
287        self.delivery_dedup_window_secs.load(Ordering::Relaxed)
288    }
289
290    pub fn delivery_ordered_buffer_limit(&self) -> usize {
291        self.delivery_ordered_buffer_limit.load(Ordering::Relaxed)
292    }
293
294    pub async fn configure_connection_lifecycle(
295        &self,
296        config: Config,
297        local_node_id: String,
298        plugin_manager: &Arc<PluginManager>,
299        peer_store: PeerStore,
300        allow_console: bool,
301    ) {
302        let runtime = ConnectionLifecycleRuntime {
303            policy: ConnectionPolicy::from_network_config(&config),
304            config,
305            local_node_id,
306            peer_store,
307            plugin_manager: Arc::downgrade(plugin_manager),
308            allow_console,
309        };
310        *self.lifecycle_runtime.lock().await = Some(runtime);
311
312        let peers: Vec<(String, SocketAddr)> = self
313            .node_ids
314            .lock()
315            .await
316            .iter()
317            .map(|(node_id, addr)| (node_id.clone(), *addr))
318            .collect();
319        for (node_id, addr) in peers {
320            self.start_liveness_monitor(node_id, Some(addr)).await;
321        }
322    }
323
324    /// Returns a list of currently connected peer addresses
325    pub async fn list_peers(&self) -> Vec<SocketAddr> {
326        let peers = self.peers.lock().await;
327        peers.keys().cloned().collect()
328    }
329
330    /// Returns a list of currently connected peer node IDs
331    pub async fn list_node_ids(&self) -> Vec<String> {
332        let ids = self.node_ids.lock().await;
333        ids.keys().cloned().collect()
334    }
335
336    // Capability registry keyed by node id
337    pub async fn set_peer_capabilities(&self, node_id: &str, caps: Option<Vec<String>>) {
338        let mut map = self.capabilities_by_node.lock().await;
339        match caps {
340            Some(v) => {
341                map.insert(node_id.to_string(), v);
342            }
343            None => {
344                map.remove(node_id);
345            }
346        }
347    }
348
349    pub async fn peer_has_capability(&self, node_id: &str, cap: &str) -> bool {
350        let map = self.capabilities_by_node.lock().await;
351        map.get(node_id)
352            .map(|v| v.iter().any(|c| c == cap))
353            .unwrap_or(false)
354    }
355
356    /// Add a peer with its message sender channel
357    pub async fn add_peer(
358        &self,
359        addr: SocketAddr,
360        sender: Sender<String>,
361        node_id: String,
362    ) -> Result<(), String> {
363        // Prevent duplicate node IDs
364        {
365            let ids = self.node_ids.lock().await;
366            if ids.contains_key(&node_id) {
367                return Err(format!("duplicate node id {} already connected", node_id));
368            }
369        }
370        self.peers.lock().await.insert(addr, sender);
371        self.node_ids.lock().await.insert(node_id.clone(), addr);
372        self.mark_route_success(&node_id, RouteKind::Tcp).await;
373        self.last_peer_activity
374            .lock()
375            .await
376            .insert(node_id.clone(), Instant::now());
377        if let Some(runtime) = self.lifecycle_runtime.lock().await.clone() {
378            runtime.peer_store.reset_reconnect(&addr).await;
379        }
380        // Drain any queued store-and-forward frames for this node_id
381        let queued = {
382            let mut q = self.relay_queue.lock().await;
383            // Purge expired before removing
384            if let Some(v) = q.get_mut(&node_id) {
385                v.retain(|(_, exp, _)| exp.map(|e| e > current_unix_ts()).unwrap_or(true));
386            }
387            q.remove(&node_id)
388        };
389        if let Some(frames) = queued {
390            if let Some(sndr) = self.peers.lock().await.get(&addr).cloned() {
391                for (frame, _exp, _origin) in frames {
392                    // Use try_send to avoid blocking if receiver is slow; drop frame on backpressure
393                    let _ = sndr.try_send(frame);
394                }
395            }
396        }
397        self.start_liveness_monitor(node_id, Some(addr)).await;
398        Ok(())
399    }
400
401    /// Check if a node id already exists among connected peers
402    pub async fn has_node_id(&self, node_id: &str) -> bool {
403        self.node_ids.lock().await.contains_key(node_id)
404    }
405
406    /// Check if an address is already connected
407    pub async fn has_addr(&self, addr: &SocketAddr) -> bool {
408        self.peers.lock().await.contains_key(addr)
409    }
410
411    /// Remove peer by address (cleanup node_id mapping) and return node_id if found
412    pub async fn remove_peer(&self, addr: &SocketAddr) -> Option<String> {
413        let removed = self.peers.lock().await.remove(addr).is_some();
414        if removed {
415            let mut ids = self.node_ids.lock().await;
416            let remove_key: Option<String> =
417                ids.iter()
418                    .find_map(|(k, v)| if v == addr { Some(k.clone()) } else { None });
419            if let Some(ref k) = remove_key {
420                ids.remove(k);
421            }
422            drop(ids);
423            if let Some(ref dup_node_id) = remove_key {
424                if self.udp_session_id_for(dup_node_id).await.is_some() {
425                    self.transport_kind
426                        .lock()
427                        .await
428                        .insert(dup_node_id.clone(), TransportKind::Udp);
429                } else {
430                    self.lifecycle_generations.lock().await.remove(dup_node_id);
431                    self.transport_kind.lock().await.remove(dup_node_id);
432                    self.last_peer_activity.lock().await.remove(dup_node_id);
433                }
434                self.pending_punches.lock().await.retain(|_, pending| {
435                    pending.initiator_node_id != *dup_node_id
436                        && pending.responder_node_id != *dup_node_id
437                });
438                self.pending_observations
439                    .lock()
440                    .await
441                    .retain(|_, pending| pending.observer_node_id != *dup_node_id);
442            }
443            return remove_key;
444        }
445        None
446    }
447
448    pub async fn handle_peer_disconnected(&self, addr: &SocketAddr) -> Option<String> {
449        let node_id = self.remove_peer(addr).await?;
450        self.mark_route_unresponsive(&node_id, RouteKind::Tcp).await;
451        let reconnect_addr = self
452            .tcp_listen_addr_for(&node_id)
453            .await
454            .and_then(|candidate| candidate.parse().ok())
455            .unwrap_or(*addr);
456        self.schedule_reconnect(node_id.clone(), reconnect_addr);
457        Some(node_id)
458    }
459
460    pub async fn route_health(&self, node_id: &str, kind: RouteKind) -> RouteHealth {
461        let health = self.route_health.lock().await;
462        let Some(record) = health.get(&(node_id.to_string(), kind)) else {
463            return RouteHealth::Unknown;
464        };
465        if record.health == RouteHealth::Unresponsive {
466            return RouteHealth::Unresponsive;
467        }
468        match (record.last_success, record.last_failure) {
469            (None, None) => RouteHealth::Unknown,
470            (Some(_), None) => RouteHealth::Healthy,
471            (None, Some(_)) => RouteHealth::Suspect,
472            (Some(success), Some(failure)) if success >= failure => RouteHealth::Healthy,
473            (Some(_), Some(_)) => RouteHealth::Suspect,
474        }
475    }
476
477    pub async fn mark_route_success(&self, node_id: &str, kind: RouteKind) {
478        let mut health = self.route_health.lock().await;
479        let record = health
480            .entry((node_id.to_string(), kind))
481            .or_insert_with(RouteHealthRecord::default);
482        record.health = RouteHealth::Healthy;
483        record.last_success = Some(Instant::now());
484        Self::prune_stale_route_health(&mut health);
485    }
486
487    pub async fn mark_route_failure(&self, node_id: &str, kind: RouteKind) {
488        let mut health = self.route_health.lock().await;
489        let record = health
490            .entry((node_id.to_string(), kind))
491            .or_insert_with(RouteHealthRecord::default);
492        // An ordinary delivery/heartbeat failure must not resurrect a route that the
493        // liveness monitor already excluded; only `mark_route_success` may clear
494        // `Unresponsive` (ADR-0007: "unresponsive candidates are excluded").
495        if record.health != RouteHealth::Unresponsive {
496            record.health = RouteHealth::Suspect;
497        }
498        record.last_failure = Some(Instant::now());
499        Self::prune_stale_route_health(&mut health);
500    }
501
502    /// Bound `route_health` growth by evicting entries with no activity in over an hour.
503    ///
504    /// `node_id` is peer-supplied, so without this the map would grow without bound as
505    /// short-lived or malicious peers churn through distinct ids (a resource-exhaustion
506    /// risk). Kept separate from peer teardown so a route's last-known health (in
507    /// particular `Unresponsive`) remains queryable for a while after a peer disconnects.
508    fn prune_stale_route_health(health: &mut HashMap<(String, RouteKind), RouteHealthRecord>) {
509        const RETENTION: std::time::Duration = std::time::Duration::from_secs(3600);
510        health.retain(|_, record| {
511            record
512                .last_success
513                .is_some_and(|t| t.elapsed() <= RETENTION)
514                || record
515                    .last_failure
516                    .is_some_and(|t| t.elapsed() <= RETENTION)
517        });
518    }
519
520    pub async fn mark_route_unresponsive(&self, node_id: &str, kind: RouteKind) {
521        let mut health = self.route_health.lock().await;
522        let record = health
523            .entry((node_id.to_string(), kind))
524            .or_insert_with(RouteHealthRecord::default);
525        record.health = RouteHealth::Unresponsive;
526        record.last_failure = Some(Instant::now());
527    }
528
529    pub async fn record_peer_activity(&self, node_id: &str, kind: RouteKind) {
530        self.last_peer_activity
531            .lock()
532            .await
533            .insert(node_id.to_string(), Instant::now());
534        self.mark_route_success(node_id, kind).await;
535    }
536
537    pub async fn record_peer_activity_on_preferred_route(&self, node_id: &str) {
538        let kind = match self.get_transport_kind(node_id).await {
539            Some(TransportKind::Udp) => RouteKind::Udp,
540            Some(TransportKind::Tcp) => RouteKind::Tcp,
541            None if self.has_node_id(node_id).await => RouteKind::Tcp,
542            None => return,
543        };
544        self.record_peer_activity(node_id, kind).await;
545    }
546
547    async fn start_liveness_monitor(&self, node_id: String, tcp_addr: Option<SocketAddr>) {
548        let Some(runtime) = self.lifecycle_runtime.lock().await.clone() else {
549            return;
550        };
551        let generation = self
552            .next_lifecycle_generation
553            .fetch_add(1, Ordering::Relaxed);
554        {
555            let mut generations = self.lifecycle_generations.lock().await;
556            if generations.contains_key(&node_id) {
557                return;
558            }
559            generations.insert(node_id.clone(), generation);
560        }
561
562        let peer_manager = self.clone();
563        tokio::spawn(async move {
564            let mut ticker = tokio::time::interval_at(
565                tokio::time::Instant::now() + runtime.policy.heartbeat_interval,
566                runtime.policy.heartbeat_interval,
567            );
568            loop {
569                ticker.tick().await;
570                let is_current = peer_manager
571                    .lifecycle_generations
572                    .lock()
573                    .await
574                    .get(&node_id)
575                    .is_some_and(|current| *current == generation);
576                if !is_current {
577                    break;
578                }
579
580                let last_activity = peer_manager
581                    .last_peer_activity
582                    .lock()
583                    .await
584                    .get(&node_id)
585                    .copied();
586                if last_activity
587                    .is_none_or(|seen| seen.elapsed() >= runtime.policy.heartbeat_timeout)
588                {
589                    peer_manager
590                        .mark_peer_unresponsive(&node_id, tcp_addr)
591                        .await;
592                    break;
593                }
594
595                let heartbeat = Message::new(
596                    &runtime.local_node_id,
597                    &node_id,
598                    MessageType::Heartbeat,
599                    Some(Payload::Text("liveness_probe_v1".to_string())),
600                    runtime.config.realm.clone(),
601                );
602                let (kind, result) = if peer_manager.has_node_id(&node_id).await {
603                    (
604                        RouteKind::Tcp,
605                        peer_manager
606                            .send_to_node_id(&node_id, heartbeat.as_json())
607                            .await,
608                    )
609                } else if peer_manager.udp_session_id_for(&node_id).await.is_some() {
610                    (
611                        RouteKind::Udp,
612                        peer_manager
613                            .send_udp_message_to_node(&node_id, heartbeat.as_json().as_bytes())
614                            .await,
615                    )
616                } else {
617                    peer_manager
618                        .mark_peer_unresponsive(&node_id, tcp_addr)
619                        .await;
620                    break;
621                };
622                if result.is_err() {
623                    peer_manager.mark_route_failure(&node_id, kind).await;
624                }
625            }
626
627            let mut generations = peer_manager.lifecycle_generations.lock().await;
628            if generations
629                .get(&node_id)
630                .is_some_and(|current| *current == generation)
631            {
632                generations.remove(&node_id);
633            }
634        });
635    }
636
637    async fn mark_peer_unresponsive(&self, node_id: &str, tcp_addr: Option<SocketAddr>) {
638        self.mark_route_unresponsive(node_id, RouteKind::Tcp).await;
639        if self.udp_session_id_for(node_id).await.is_some() {
640            self.mark_route_unresponsive(node_id, RouteKind::Udp).await;
641        }
642        self.udp_session_ids.lock().await.remove(node_id);
643        self.transport_kind.lock().await.remove(node_id);
644        let active_tcp_addr = self.node_ids.lock().await.get(node_id).copied();
645        if let Some(addr) = active_tcp_addr {
646            self.remove_peer(&addr).await;
647        } else {
648            self.lifecycle_generations.lock().await.remove(node_id);
649            self.last_peer_activity.lock().await.remove(node_id);
650        }
651
652        emit_network_event(
653            "peer_manager",
654            LogLevel::Warn,
655            "peer_unresponsive",
656            tcp_addr.map(|addr| addr.to_string()),
657            Some(format!("node_id={node_id}")),
658            self.lifecycle_runtime
659                .lock()
660                .await
661                .as_ref()
662                .is_some_and(|runtime| runtime.allow_console),
663        );
664
665        let reconnect_addr = self
666            .tcp_listen_addr_for(node_id)
667            .await
668            .and_then(|candidate| candidate.parse().ok())
669            .or(tcp_addr);
670        if let Some(addr) = reconnect_addr {
671            self.schedule_reconnect(node_id.to_string(), addr);
672        }
673    }
674
675    fn schedule_reconnect(&self, node_id: String, addr: SocketAddr) {
676        let peer_manager = self.clone();
677        tokio::spawn(async move {
678            let Some(runtime) = peer_manager.lifecycle_runtime.lock().await.clone() else {
679                return;
680            };
681            {
682                let mut reconnecting = peer_manager.reconnecting_peers.lock().await;
683                if !reconnecting.insert(node_id.clone()) {
684                    return;
685                }
686            }
687            runtime.peer_store.insert(addr, PeerSource::Handshake).await;
688
689            loop {
690                if peer_manager.has_node_id(&node_id).await {
691                    break;
692                }
693                let Some(attempt) = runtime
694                    .peer_store
695                    .next_reconnect_attempt(&addr, runtime.policy.reconnect_max_attempts)
696                    .await
697                else {
698                    emit_network_event(
699                        "peer_manager",
700                        LogLevel::Warn,
701                        "reconnect_exhausted",
702                        Some(addr.to_string()),
703                        Some(format!("node_id={node_id}")),
704                        runtime.allow_console,
705                    );
706                    break;
707                };
708
709                tokio::time::sleep(runtime.policy.reconnect_delay_with_random_jitter(attempt))
710                    .await;
711                if peer_manager.has_node_id(&node_id).await {
712                    break;
713                }
714                let Some(plugin_manager) = runtime.plugin_manager.upgrade() else {
715                    break;
716                };
717                let peer = Peer::new(format!("reconnect-{node_id}"), addr.to_string());
718                let result = crate::network::transport::connect_to_peer(
719                    crate::network::transport::ConnectToPeerParams {
720                        peer: &peer,
721                        our_realm: runtime
722                            .config
723                            .realm
724                            .clone()
725                            .unwrap_or_else(RealmInfo::default),
726                        our_port: runtime.config.port,
727                        peer_manager: peer_manager.clone(),
728                        plugin_manager,
729                        allow_console: runtime.allow_console,
730                        config: runtime.config.clone(),
731                        local_node_id: runtime.local_node_id.clone(),
732                        peer_store: Some(runtime.peer_store.clone()),
733                    },
734                )
735                .await;
736
737                if result.is_err() || !peer_manager.has_node_id(&node_id).await {
738                    runtime.peer_store.mark_failure(&addr).await;
739                    peer_manager
740                        .mark_route_failure(&node_id, RouteKind::Tcp)
741                        .await;
742                    continue;
743                }
744                break;
745            }
746            peer_manager
747                .reconnecting_peers
748                .lock()
749                .await
750                .remove(&node_id);
751        });
752    }
753
754    pub async fn broadcast(&self, message: &str) {
755        let peers = self.peers.lock().await;
756        for (addr, sender) in peers.iter() {
757            if let Err(e) = sender.send(message.to_string()).await {
758                eprintln!("❌ Failed to send to {}: {}", addr, e);
759            }
760        }
761    }
762
763    /// Get a cloned sender for a connected peer by its socket address
764    pub async fn get_sender_by_addr(&self, addr: &SocketAddr) -> Option<Sender<String>> {
765        self.peers.lock().await.get(addr).cloned()
766    }
767
768    /// Get a cloned sender for a connected peer by its node id
769    pub async fn get_sender_by_node_id(&self, node_id: &str) -> Option<Sender<String>> {
770        let addr_opt = { self.node_ids.lock().await.get(node_id).cloned() };
771        if let Some(addr) = addr_opt {
772            return self.peers.lock().await.get(&addr).cloned();
773        }
774        None
775    }
776
777    /// Send a serialized message line (without trailing newline) to a peer by address
778    pub async fn send_to_addr(&self, addr: &SocketAddr, message: String) -> Result<(), String> {
779        if let Some(sender) = self.get_sender_by_addr(addr).await {
780            sender
781                .try_send(message)
782                .map_err(|e| format!("send_to_addr failed: {}", e))
783        } else {
784            Err("peer sender not found".to_string())
785        }
786    }
787
788    /// Send a serialized message line (without trailing newline) to a peer by node id
789    pub async fn send_to_node_id(&self, node_id: &str, message: String) -> Result<(), String> {
790        if let Some(sender) = self.get_sender_by_node_id(node_id).await {
791            sender
792                .try_send(message)
793                .map_err(|e| format!("send_to_node_id failed: {}", e))
794        } else {
795            Err("peer sender not found".to_string())
796        }
797    }
798
799    /// Record a peer's advertised listening address (from its HELLO) for suppression logic.
800    pub async fn add_listen_addr(&self, listen_addr: &str, node_id: &str) {
801        self.listen_addrs
802            .lock()
803            .await
804            .insert(listen_addr.to_string(), node_id.to_string());
805    }
806
807    /// Returns true if we already have a peer whose advertised listening address matches.
808    pub async fn has_listen_addr(&self, listen_addr: &str) -> bool {
809        self.listen_addrs.lock().await.contains_key(listen_addr)
810    }
811
812    /// Return the TCP listen address advertised by a peer in its HELLO, if known.
813    ///
814    /// The `listen_addrs` map is indexed by address; this performs a reverse lookup
815    /// by node_id.  Used by the connection policy to resolve TCP addresses for
816    /// peers that are not currently connected.
817    pub async fn tcp_listen_addr_for(&self, node_id: &str) -> Option<String> {
818        let map = self.listen_addrs.lock().await;
819        map.iter().find_map(|(addr, nid)| {
820            if nid == node_id {
821                Some(addr.clone())
822            } else {
823                None
824            }
825        })
826    }
827
828    pub async fn reconnect_known_peers(
829        &self,
830        plugin_manager: Arc<PluginManager>,
831        peer_store: PeerStore,
832        config: &Config,
833    ) {
834        let mut targets: BTreeSet<SocketAddr> = peer_store.all().await.into_iter().collect();
835        let mut parse_failures: Vec<String> = Vec::new();
836
837        if let Some(bootstrap) = &config.bootstrap_nodes {
838            for entry in bootstrap {
839                match entry.parse::<SocketAddr>() {
840                    Ok(addr) => {
841                        targets.insert(addr);
842                    }
843                    Err(err) => parse_failures.push(format!("{} ({})", entry, err)),
844                }
845            }
846        }
847
848        if targets.is_empty() {
849            println!(
850                "{}No stored peer addresses to retry after promotion.",
851                crate::constants::ICON_PLACEHOLDER
852            );
853            return;
854        }
855
856        let total_candidates = targets.len();
857        let connected: HashSet<SocketAddr> = self.list_peers().await.into_iter().collect();
858
859        let realm = config.realm.clone().unwrap_or_else(RealmInfo::default);
860        let local_node_id = config
861            .node
862            .as_ref()
863            .map(|n| n.resolve_node_id())
864            .unwrap_or_else(|| "unknown-node".to_string());
865
866        let mut attempted = 0usize;
867        let mut succeeded = 0usize;
868        let mut skipped_connected = 0usize;
869
870        println!(
871            "{}Retrying {} known peer(s)...",
872            crate::constants::ICON_PLACEHOLDER,
873            total_candidates
874        );
875
876        let peer_manager = self.clone();
877        for addr in targets {
878            if connected.contains(&addr) {
879                skipped_connected += 1;
880                continue;
881            }
882            attempted += 1;
883            let addr_str = addr.to_string();
884            let peer = Peer::new(format!("retry-{}", addr_str), addr_str.clone());
885            match crate::network::transport::connect_to_peer(
886                crate::network::transport::ConnectToPeerParams {
887                    peer: &peer,
888                    our_realm: realm.clone(),
889                    our_port: config.port,
890                    peer_manager: peer_manager.clone(),
891                    plugin_manager: plugin_manager.clone(),
892                    allow_console: true,
893                    config: config.clone(),
894                    local_node_id: local_node_id.clone(),
895                    peer_store: None,
896                },
897            )
898            .await
899            {
900                Ok(()) => {
901                    succeeded += 1;
902                    println!(
903                        "{}Connected to {}",
904                        crate::constants::ICON_PLACEHOLDER,
905                        addr
906                    );
907                }
908                Err(err) => {
909                    println!(
910                        "{}Failed to connect to {}: {}",
911                        crate::constants::ICON_PLACEHOLDER,
912                        addr,
913                        err
914                    );
915                }
916            }
917        }
918
919        println!(
920            "{}Reconnect summary: {} candidate(s), {} attempted, {} succeeded, {} already connected.",
921            crate::constants::ICON_PLACEHOLDER,
922            total_candidates,
923            attempted,
924            succeeded,
925            skipped_connected
926        );
927
928        if !parse_failures.is_empty() {
929            println!(
930                "{}Skipped invalid addresses:",
931                crate::constants::ICON_PLACEHOLDER
932            );
933            for detail in parse_failures {
934                println!("  {}", detail);
935            }
936        }
937    }
938
939    // Relay counters
940    pub fn inc_relay_forwarded(&self) {
941        self.relay_forwarded.fetch_add(1, Ordering::Relaxed);
942    }
943    pub fn inc_relay_dropped(&self) {
944        self.relay_dropped.fetch_add(1, Ordering::Relaxed);
945    }
946    pub fn relay_stats(&self) -> (u64, u64) {
947        (
948            self.relay_forwarded.load(Ordering::Relaxed),
949            self.relay_dropped.load(Ordering::Relaxed),
950        )
951    }
952
953    // Reliable QoS helpers
954    pub async fn add_inflight(&self, from: &str, to: &str, seq: u64) {
955        self.reliable_inflight
956            .lock()
957            .await
958            .insert((from.to_string(), to.to_string(), seq));
959    }
960
961    pub async fn remove_inflight(&self, from: &str, to: &str, seq: u64) {
962        self.reliable_inflight
963            .lock()
964            .await
965            .remove(&(from.to_string(), to.to_string(), seq));
966    }
967
968    pub async fn is_inflight(&self, from: &str, to: &str, seq: u64) -> bool {
969        self.reliable_inflight
970            .lock()
971            .await
972            .contains(&(from.to_string(), to.to_string(), seq))
973    }
974
975    pub async fn record_delivery_attempt(
976        &self,
977        message_id: &MessageId,
978        class: DeliveryClass,
979        expires_at: std::time::Instant,
980    ) {
981        self.delivery_attempts.lock().await.insert(
982            message_id.as_str().to_string(),
983            DeliveryAttemptState {
984                class,
985                attempts: 0,
986                outcome: None,
987                expires_at,
988            },
989        );
990    }
991
992    pub async fn increment_delivery_attempts(&self, message_id: &MessageId) -> Option<u32> {
993        let mut attempts = self.delivery_attempts.lock().await;
994        let state = attempts.get_mut(message_id.as_str())?;
995        state.attempts = state.attempts.saturating_add(1);
996        Some(state.attempts)
997    }
998
999    pub async fn set_delivery_outcome(&self, message_id: &MessageId, outcome: DeliveryOutcome) {
1000        if let Some(state) = self
1001            .delivery_attempts
1002            .lock()
1003            .await
1004            .get_mut(message_id.as_str())
1005        {
1006            state.outcome = Some(outcome);
1007        }
1008    }
1009
1010    pub async fn delivery_outcome(&self, message_id: &MessageId) -> Option<DeliveryOutcome> {
1011        self.delivery_attempts
1012            .lock()
1013            .await
1014            .get(message_id.as_str())
1015            .and_then(|state| state.outcome)
1016    }
1017
1018    pub async fn clear_delivery_attempt(&self, message_id: &MessageId) {
1019        self.delivery_attempts
1020            .lock()
1021            .await
1022            .remove(message_id.as_str());
1023    }
1024
1025    pub async fn delivery_expired(&self, message_id: &MessageId) -> bool {
1026        self.delivery_attempts
1027            .lock()
1028            .await
1029            .get(message_id.as_str())
1030            .map(|state| std::time::Instant::now() >= state.expires_at)
1031            .unwrap_or(false)
1032    }
1033
1034    pub async fn delivery_class_for(&self, message_id: &MessageId) -> Option<DeliveryClass> {
1035        self.delivery_attempts
1036            .lock()
1037            .await
1038            .get(message_id.as_str())
1039            .map(|state| state.class)
1040    }
1041
1042    pub async fn prune_delivery_dedup(&self, max_age: std::time::Duration) {
1043        let mut dedup = self.delivery_dedup.lock().await;
1044        let now = std::time::Instant::now();
1045        dedup.retain(|_, seen_at| now.duration_since(*seen_at) <= max_age);
1046    }
1047
1048    pub async fn record_delivery_receipt(
1049        &self,
1050        origin_node_id: &str,
1051        message_id: &MessageId,
1052    ) -> bool {
1053        let key = (origin_node_id.to_string(), message_id.as_str().to_string());
1054        let mut dedup = self.delivery_dedup.lock().await;
1055        let is_new = !dedup.contains_key(&key);
1056        dedup.insert(key, std::time::Instant::now());
1057        is_new
1058    }
1059
1060    pub async fn next_ordering_sequence(&self, from: &str, to: &str, ordering_key: &str) -> u64 {
1061        let key = (from.to_string(), to.to_string(), ordering_key.to_string());
1062        let mut sequences = self.delivery_next_ordering_sequence.lock().await;
1063        let next = sequences.get(&key).copied().unwrap_or(1);
1064        sequences.insert(key, next.saturating_add(1));
1065        next
1066    }
1067
1068    pub async fn accept_ordered_incoming(
1069        &self,
1070        origin_node_id: &str,
1071        destination_node_id: &str,
1072        ordering_key: &str,
1073        ordering_sequence: u64,
1074        message: Message,
1075        max_buffered_messages: usize,
1076    ) -> Vec<Message> {
1077        let key = (
1078            origin_node_id.to_string(),
1079            destination_node_id.to_string(),
1080            ordering_key.to_string(),
1081        );
1082
1083        let mut buffers = self.delivery_ordered_inbound.lock().await;
1084        let bucket = buffers.entry(key).or_insert_with(|| OrderedDeliveryBuffer {
1085            next_expected: 1,
1086            pending: BTreeMap::new(),
1087            last_activity: std::time::Instant::now(),
1088        });
1089
1090        if ordering_sequence < bucket.next_expected {
1091            return Vec::new();
1092        }
1093
1094        if ordering_sequence > bucket.next_expected {
1095            if bucket.pending.len() < max_buffered_messages {
1096                bucket.pending.insert(ordering_sequence, message);
1097                bucket.last_activity = std::time::Instant::now();
1098            }
1099            return Vec::new();
1100        }
1101
1102        bucket.last_activity = std::time::Instant::now();
1103        let mut ready = vec![message];
1104        bucket.next_expected = bucket.next_expected.saturating_add(1);
1105        while let Some(next_message) = bucket.pending.remove(&bucket.next_expected) {
1106            ready.push(next_message);
1107            bucket.next_expected = bucket.next_expected.saturating_add(1);
1108        }
1109
1110        ready
1111    }
1112
1113    /// Evict ordered-delivery scopes that have not seen any activity within `max_age`.
1114    /// This prevents stale scopes (where the gap message never arrived) from accumulating
1115    /// in memory indefinitely.
1116    pub async fn prune_ordered_inbound(&self, max_age: std::time::Duration) {
1117        let mut buffers = self.delivery_ordered_inbound.lock().await;
1118        let now = std::time::Instant::now();
1119        buffers.retain(|_, bucket| now.duration_since(bucket.last_activity) <= max_age);
1120    }
1121
1122    pub async fn next_relay_sequence(&self, from: &str, to: &str) -> u64 {
1123        let key = (from.to_string(), to.to_string());
1124        let mut sequences = self.relay_next_sequence.lock().await;
1125        let next = sequences.get(&key).copied().unwrap_or(1);
1126        sequences.insert(key, next.saturating_add(1));
1127        next
1128    }
1129
1130    // Relay binding management
1131    pub async fn set_binding(
1132        &self,
1133        from_node_id: &str,
1134        to_node_id: &str,
1135        store_forward: bool,
1136        expires_at: Option<u64>,
1137        qos: Option<String>,
1138    ) {
1139        self.relay_bindings.lock().await.insert(
1140            (from_node_id.to_string(), to_node_id.to_string()),
1141            BindingPrefs {
1142                store_forward,
1143                expires_at,
1144                qos,
1145            },
1146        );
1147    }
1148
1149    pub async fn binding_store_forward(&self, from_node_id: &str, to_node_id: &str) -> bool {
1150        self.relay_bindings
1151            .lock()
1152            .await
1153            .get(&(from_node_id.to_string(), to_node_id.to_string()))
1154            .map(|b| b.store_forward)
1155            .unwrap_or(false)
1156    }
1157
1158    pub async fn binding_expires_at(&self, from_node_id: &str, to_node_id: &str) -> Option<u64> {
1159        self.relay_bindings
1160            .lock()
1161            .await
1162            .get(&(from_node_id.to_string(), to_node_id.to_string()))
1163            .and_then(|b| b.expires_at)
1164    }
1165
1166    pub async fn binding_qos(&self, from_node_id: &str, to_node_id: &str) -> Option<String> {
1167        self.relay_bindings
1168            .lock()
1169            .await
1170            .get(&(from_node_id.to_string(), to_node_id.to_string()))
1171            .and_then(|b| b.qos.clone())
1172    }
1173
1174    pub async fn last_sequence(&self, from_node_id: &str, to_node_id: &str) -> Option<u64> {
1175        self.relay_last_sequence
1176            .lock()
1177            .await
1178            .get(&(from_node_id.to_string(), to_node_id.to_string()))
1179            .cloned()
1180    }
1181
1182    pub async fn update_sequence(&self, from_node_id: &str, to_node_id: &str, seq: u64) {
1183        self.relay_last_sequence
1184            .lock()
1185            .await
1186            .insert((from_node_id.to_string(), to_node_id.to_string()), seq);
1187    }
1188
1189    pub async fn is_bound(&self, from_node_id: &str, to_node_id: &str) -> bool {
1190        self.relay_bindings
1191            .lock()
1192            .await
1193            .contains_key(&(from_node_id.to_string(), to_node_id.to_string()))
1194    }
1195
1196    pub async fn add_binding_id(&self, binding_id: &str, from_node_id: &str, to_node_id: &str) {
1197        self.relay_binding_ids.lock().await.insert(
1198            binding_id.to_string(),
1199            (from_node_id.to_string(), to_node_id.to_string()),
1200        );
1201    }
1202
1203    pub async fn remove_binding_by_id(&self, binding_id: &str) -> Option<(String, String)> {
1204        let mut ids = self.relay_binding_ids.lock().await;
1205        let pair = ids.remove(binding_id);
1206        if let Some((from, to)) = &pair {
1207            self.relay_bindings
1208                .lock()
1209                .await
1210                .remove(&(from.clone(), to.clone()));
1211        }
1212        pair
1213    }
1214
1215    /// List bindings for a given `from` node id, returning `(to, binding_id)` pairs.
1216    pub async fn list_bindings_for_from(
1217        &self,
1218        from_node_id: &str,
1219    ) -> Vec<(String, Option<String>)> {
1220        let bindings = self.relay_bindings.lock().await;
1221        let mut results: Vec<(String, Option<String>)> = Vec::new();
1222        // Build reverse index of (from,to) -> binding_id for quick lookup
1223        let ids = self.relay_binding_ids.lock().await;
1224        for (from, to) in bindings.keys() {
1225            if from == from_node_id {
1226                let bid = ids.iter().find_map(|(id, pair)| {
1227                    if pair.0 == *from && pair.1 == *to {
1228                        Some(id.clone())
1229                    } else {
1230                        None
1231                    }
1232                });
1233                results.push((to.clone(), bid));
1234            }
1235        }
1236        results
1237    }
1238
1239    pub async fn enqueue_store_forward(
1240        &self,
1241        to_node_id: &str,
1242        message_json: String,
1243        expires_at: Option<u64>,
1244        priority_front: bool,
1245        soft_drop_bulk: bool,
1246        origin_from: Option<String>,
1247    ) -> bool {
1248        let (max_queue_per_target, max_queue_global) = self.relay_queue_caps();
1249        // Accumulate notifications to send after releasing queue lock to avoid await while locked
1250        let mut to_notify: Vec<(String, crate::network::message::Reason)> = Vec::new();
1251        // If the incoming frame is already expired, notify origin immediately and drop.
1252        if let Some(exp) = expires_at {
1253            if exp <= current_unix_ts() {
1254                if let Some(o) = origin_from.as_ref() {
1255                    if self.has_node_id(o).await {
1256                        let notify = crate::network::message::Message::new(
1257                            o,
1258                            o,
1259                            crate::network::message::MessageType::RelayNotify {
1260                                notif_type: crate::network::message::Reason::Timeout,
1261                                binding_id: None,
1262                                detail: Some(format!("target={}", to_node_id)),
1263                            },
1264                            None,
1265                            None,
1266                        );
1267                        let _ = self.send_to_node_id(o, notify.as_json()).await;
1268                    }
1269                }
1270                return false;
1271            }
1272        }
1273        let mut q = self.relay_queue.lock().await;
1274        // Purge expired entries for this target first
1275        // Note: we may later surface purge events; for now we do not track a flag.
1276        if let Some(v) = q.get_mut(to_node_id) {
1277            // Drop expired entries and notify their origins of timeout
1278            let now = current_unix_ts();
1279            let mut kept: Vec<(String, Option<u64>, Option<String>)> = Vec::with_capacity(v.len());
1280            for (frame, exp, origin) in v.drain(..) {
1281                if exp.map(|e| e > now).unwrap_or(true) {
1282                    kept.push((frame, exp, origin));
1283                } else if let Some(o) = origin.as_ref() {
1284                    to_notify.push((o.clone(), crate::network::message::Reason::Timeout));
1285                }
1286            }
1287            *v = kept;
1288            // If any entries were purged, mark
1289            // (cheap check: compare lengths after retain by cloning len before)
1290            // Note: exact count not required; boolean is sufficient for notify decisions upstream.
1291            // We cannot easily read previous length without cloning; use can_enqueue check separately.
1292            // Enforce cap by dropping oldest if exceeding limit
1293            if v.len() >= max_queue_per_target {
1294                let drop_count = v.len() + 1 - max_queue_per_target;
1295                for _ in 0..drop_count {
1296                    let removed = v.remove(0);
1297                    if let Some(o) = removed.2.as_ref() {
1298                        to_notify.push((o.clone(), crate::network::message::Reason::Overload));
1299                    }
1300                }
1301            }
1302        }
1303        let entry = q.entry(to_node_id.to_string()).or_insert_with(Vec::new);
1304        if entry.len() >= max_queue_per_target {
1305            // If soft_drop_bulk, drop the incoming frame silently
1306            if soft_drop_bulk {
1307                return false;
1308            }
1309            // Otherwise drop oldest to make room
1310            let removed = entry.remove(0);
1311            if let Some(o) = removed.2.as_ref() {
1312                to_notify.push((o.clone(), crate::network::message::Reason::Overload));
1313            }
1314        }
1315        if priority_front {
1316            entry.insert(0, (message_json, expires_at, origin_from));
1317        } else {
1318            entry.push((message_json, expires_at, origin_from));
1319        }
1320
1321        // Enforce global cap by dropping oldest across targets (simple round-robin)
1322        let mut total_len: usize = q.values().map(|v| v.len()).sum();
1323        if total_len > max_queue_global {
1324            let mut keys: Vec<String> = q.keys().cloned().collect();
1325            keys.sort();
1326            let mut idx = 0usize;
1327            while total_len > max_queue_global && !keys.is_empty() {
1328                let k = &keys[idx % keys.len()];
1329                if let Some(vec) = q.get_mut(k) {
1330                    if !vec.is_empty() {
1331                        let removed = vec.remove(0);
1332                        if let Some(o) = removed.2.as_ref() {
1333                            to_notify.push((o.clone(), crate::network::message::Reason::Overload));
1334                        }
1335                        total_len -= 1;
1336                    } else {
1337                        // remove empty key from rotation to avoid tight loop
1338                        keys.remove(idx % keys.len());
1339                        continue;
1340                    }
1341                }
1342                idx += 1;
1343            }
1344        }
1345        drop(q);
1346
1347        // Send accumulated notifications after releasing the queue lock
1348        for (o, reason) in to_notify {
1349            if self.has_node_id(&o).await {
1350                let notify = crate::network::message::Message::new(
1351                    &o,
1352                    &o,
1353                    crate::network::message::MessageType::RelayNotify {
1354                        notif_type: reason,
1355                        binding_id: None,
1356                        detail: Some(format!("target={}", to_node_id)),
1357                    },
1358                    None,
1359                    None,
1360                );
1361                let _ = self.send_to_node_id(&o, notify.as_json()).await;
1362            }
1363        }
1364        true
1365    }
1366
1367    pub async fn can_enqueue_store_forward(&self, to_node_id: &str) -> bool {
1368        let (max_queue_per_target, max_queue_global) = self.relay_queue_caps();
1369        let q = self.relay_queue.lock().await;
1370        let per_target_ok = q
1371            .get(to_node_id)
1372            .map(|v| v.len() < max_queue_per_target)
1373            .unwrap_or(true);
1374        let total_len: usize = q.values().map(|v| v.len()).sum();
1375        let global_ok = total_len < max_queue_global;
1376        per_target_ok && global_ok
1377    }
1378
1379    /// Helper to inspect queue contents for a target (primarily for tests).
1380    pub async fn test_get_queue_for(
1381        &self,
1382        to_node_id: &str,
1383    ) -> Vec<(String, Option<u64>, Option<String>)> {
1384        let q = self.relay_queue.lock().await;
1385        q.get(to_node_id).cloned().unwrap_or_default()
1386    }
1387
1388    // ── UDP transport accessors (ADR-0004) ──────────────────────────────────────
1389
1390    /// Set the preferred transport kind for a peer identified by `node_id`.
1391    pub async fn set_transport_kind(&self, node_id: &str, kind: TransportKind) {
1392        self.transport_kind
1393            .lock()
1394            .await
1395            .insert(node_id.to_string(), kind);
1396    }
1397
1398    /// Get the current transport kind for a peer, or `None` if not recorded.
1399    pub async fn get_transport_kind(&self, node_id: &str) -> Option<TransportKind> {
1400        self.transport_kind.lock().await.get(node_id).cloned()
1401    }
1402
1403    /// Record a UDP listen address advertised in the peer's HELLO message.
1404    pub async fn add_udp_listen_addr(&self, node_id: &str, addr: &str) {
1405        self.udp_listen_addrs
1406            .lock()
1407            .await
1408            .insert(node_id.to_string(), addr.to_string());
1409    }
1410
1411    /// Retrieve the UDP listen address for a peer, if known.
1412    pub async fn udp_listen_addr_for(&self, node_id: &str) -> Option<String> {
1413        self.udp_listen_addrs.lock().await.get(node_id).cloned()
1414    }
1415
1416    /// Store the active Noise session ID for a peer (set on handshake completion).
1417    pub async fn set_udp_session_id(&self, node_id: &str, session_id: [u8; 8]) {
1418        self.udp_session_ids
1419            .lock()
1420            .await
1421            .insert(node_id.to_string(), session_id);
1422        self.record_peer_activity(node_id, RouteKind::Udp).await;
1423        self.start_liveness_monitor(node_id.to_string(), None).await;
1424    }
1425
1426    /// Retrieve the active Noise session ID for a peer.
1427    pub async fn udp_session_id_for(&self, node_id: &str) -> Option<[u8; 8]> {
1428        self.udp_session_ids.lock().await.get(node_id).copied()
1429    }
1430
1431    #[cfg(feature = "noise")]
1432    pub async fn ensure_udp_session(
1433        &self,
1434        node_id: &str,
1435        local_node_id: &str,
1436        timeout: std::time::Duration,
1437    ) -> Result<[u8; 8], String> {
1438        if let Some(session_id) = self.udp_session_id_for(node_id).await {
1439            return Ok(session_id);
1440        }
1441
1442        let target_addr = self
1443            .udp_listen_addr_for(node_id)
1444            .await
1445            .ok_or_else(|| format!("udp listen addr not found for {node_id}"))?
1446            .parse::<SocketAddr>()
1447            .map_err(|err| format!("invalid udp listen addr for {node_id}: {err}"))?;
1448
1449        let handle = self
1450            .udp_handle
1451            .lock()
1452            .await
1453            .clone()
1454            .ok_or_else(|| "udp handle not initialized".to_string())?;
1455
1456        crate::network::udp_listener::connect_udp(
1457            target_addr,
1458            &handle.socket,
1459            &handle.sessions,
1460            &handle.static_private,
1461            local_node_id,
1462        )
1463        .await
1464        .map_err(|err| err.to_string())?;
1465
1466        let deadline = std::time::Instant::now() + timeout;
1467        loop {
1468            if let Some(session_id) = self.udp_session_id_for(node_id).await {
1469                return Ok(session_id);
1470            }
1471            if std::time::Instant::now() >= deadline {
1472                return Err(format!("timed out establishing udp session for {node_id}"));
1473            }
1474            tokio::time::sleep(std::time::Duration::from_millis(25)).await;
1475        }
1476    }
1477
1478    #[cfg(not(feature = "noise"))]
1479    pub async fn ensure_udp_session(
1480        &self,
1481        node_id: &str,
1482        _local_node_id: &str,
1483        _timeout: std::time::Duration,
1484    ) -> Result<[u8; 8], String> {
1485        Err(format!(
1486            "udp transport is unavailable without the noise feature for {node_id}"
1487        ))
1488    }
1489
1490    #[cfg(feature = "noise")]
1491    pub async fn send_udp_message_to_node(
1492        &self,
1493        node_id: &str,
1494        payload: &[u8],
1495    ) -> Result<(), String> {
1496        let session_id = self
1497            .udp_session_id_for(node_id)
1498            .await
1499            .ok_or_else(|| format!("udp session not found for {node_id}"))?;
1500        let handle = self
1501            .udp_handle
1502            .lock()
1503            .await
1504            .clone()
1505            .ok_or_else(|| "udp handle not initialized".to_string())?;
1506
1507        crate::network::udp_listener::send_udp(
1508            &session_id,
1509            payload,
1510            &handle.socket,
1511            &handle.sessions,
1512        )
1513        .await
1514        .map_err(|err| err.to_string())
1515    }
1516
1517    #[cfg(not(feature = "noise"))]
1518    pub async fn send_udp_message_to_node(
1519        &self,
1520        node_id: &str,
1521        _payload: &[u8],
1522    ) -> Result<(), String> {
1523        Err(format!(
1524            "udp transport is unavailable without the noise feature for {node_id}"
1525        ))
1526    }
1527
1528    /// Dispatch a decrypted UDP payload from `node_id` to the event/plugin system.
1529    ///
1530    /// Phase 1: emits a structured log event.  Full plugin routing is Phase 2+.
1531    pub async fn dispatch_udp_payload(&self, node_id: &str, payload: &[u8]) {
1532        use crate::events::{
1533            dispatcher,
1534            model::{LogEvent, LogLevel, SystemEvent},
1535        };
1536        let mut meta = dispatcher::meta("udp", LogLevel::Debug);
1537        meta.corr_id = Some(dispatcher::correlation_id());
1538        dispatcher::emit(LogEvent::System(SystemEvent {
1539            meta,
1540            action: "udp_payload_received".into(),
1541            detail: Some(format!("node_id={} bytes={}", node_id, payload.len())),
1542        }));
1543    }
1544
1545    // ── NAT traversal accessors (ADR-0005) ──────────────────────────────────────
1546
1547    /// Store the own node's most recently observed public UDP address.
1548    pub async fn set_own_observed_addr(
1549        &self,
1550        addr: &str,
1551        observer_node_id: Option<&str>,
1552        request_nonce: Option<[u8; 8]>,
1553    ) {
1554        let record = ObservedUdpAddrRecord {
1555            addr: addr.to_string(),
1556            observed_at: std::time::Instant::now(),
1557            observer_node_id: observer_node_id.map(str::to_string),
1558            request_nonce,
1559        };
1560        *self.own_observed_addr.lock().await = Some(record.clone());
1561        let sample_key = observer_node_id
1562            .map(str::to_string)
1563            .or_else(|| request_nonce.map(hex::encode))
1564            .unwrap_or_else(|| addr.to_string());
1565        self.own_observed_addr_samples
1566            .lock()
1567            .await
1568            .insert(sample_key, record);
1569    }
1570
1571    /// Return own observed UDP address if it was seen less than `max_age_secs` ago.
1572    pub async fn own_udp_observed_addr_if_fresh(&self, max_age_secs: u64) -> Option<String> {
1573        self.own_observed_addr
1574            .lock()
1575            .await
1576            .as_ref()
1577            .filter(|record| record.observed_at.elapsed().as_secs() < max_age_secs)
1578            .map(|record| record.addr.clone())
1579    }
1580
1581    /// Return all fresh local UDP observation samples.
1582    pub async fn own_udp_observed_records_if_fresh(
1583        &self,
1584        max_age_secs: u64,
1585    ) -> Vec<ObservedUdpAddrRecord> {
1586        self.own_observed_addr_samples
1587            .lock()
1588            .await
1589            .values()
1590            .filter(|record| record.observed_at.elapsed().as_secs() < max_age_secs)
1591            .cloned()
1592            .collect()
1593    }
1594
1595    /// Return the configured NAT-observation freshness window in seconds.
1596    pub async fn nat_observation_refresh_secs(&self) -> u64 {
1597        #[cfg(feature = "noise")]
1598        {
1599            if let Some(state) = self.nat_state.read().await.clone() {
1600                return state.refresh_secs;
1601            }
1602        }
1603        300
1604    }
1605
1606    /// Store the observed public UDP address for a remote peer.
1607    pub async fn add_udp_observed_addr(
1608        &self,
1609        node_id: &str,
1610        addr: &str,
1611        observer_node_id: Option<&str>,
1612        request_nonce: Option<[u8; 8]>,
1613    ) {
1614        self.udp_observed_addrs.lock().await.insert(
1615            node_id.to_string(),
1616            ObservedUdpAddrRecord {
1617                addr: addr.to_string(),
1618                observed_at: std::time::Instant::now(),
1619                observer_node_id: observer_node_id.map(str::to_string),
1620                request_nonce,
1621            },
1622        );
1623    }
1624
1625    /// Retrieve the observed public UDP address for a remote peer.
1626    pub async fn udp_observed_addr_for(&self, node_id: &str) -> Option<String> {
1627        self.udp_observed_addrs
1628            .lock()
1629            .await
1630            .get(node_id)
1631            .map(|record| record.addr.clone())
1632    }
1633
1634    pub async fn udp_observed_record_for_if_fresh(
1635        &self,
1636        node_id: &str,
1637        max_age_secs: u64,
1638    ) -> Option<ObservedUdpAddrRecord> {
1639        self.udp_observed_addrs
1640            .lock()
1641            .await
1642            .get(node_id)
1643            .filter(|record| record.observed_at.elapsed().as_secs() < max_age_secs)
1644            .cloned()
1645    }
1646
1647    /// Retrieve the observed public UDP address for a remote peer if it is still fresh.
1648    pub async fn udp_observed_addr_for_if_fresh(
1649        &self,
1650        node_id: &str,
1651        max_age_secs: u64,
1652    ) -> Option<String> {
1653        self.udp_observed_addrs
1654            .lock()
1655            .await
1656            .get(node_id)
1657            .and_then(|record| {
1658                if record.observed_at.elapsed().as_secs() < max_age_secs {
1659                    Some(record.addr.clone())
1660                } else {
1661                    None
1662                }
1663            })
1664    }
1665
1666    /// Reverse-lookup: return the node_id for a connected peer's TCP socket address.
1667    pub async fn node_id_for_addr(&self, addr: &SocketAddr) -> Option<String> {
1668        self.node_ids
1669            .lock()
1670            .await
1671            .iter()
1672            .find_map(|(nid, a)| if a == addr { Some(nid.clone()) } else { None })
1673    }
1674
1675    /// Record a pending relay-coordinated hole-punch request.
1676    pub async fn add_pending_punch(
1677        &self,
1678        attempt_id: &str,
1679        responder_node_id: &str,
1680        initiator_node_id: &str,
1681        timeout_ms: u64,
1682    ) {
1683        self.pending_punches.lock().await.insert(
1684            attempt_id.to_string(),
1685            PendingPunch {
1686                attempt_id: attempt_id.to_string(),
1687                initiator_node_id: initiator_node_id.to_string(),
1688                responder_node_id: responder_node_id.to_string(),
1689                timeout_ms,
1690            },
1691        );
1692    }
1693
1694    /// Retrieve a pending punch record by attempt id.
1695    pub async fn get_pending_punch(&self, attempt_id: &str) -> Option<PendingPunch> {
1696        self.pending_punches.lock().await.get(attempt_id).cloned()
1697    }
1698
1699    /// Remove a pending punch record by attempt id.
1700    pub async fn remove_pending_punch(&self, attempt_id: &str) {
1701        self.pending_punches.lock().await.remove(attempt_id);
1702    }
1703
1704    pub async fn register_pending_observation(
1705        &self,
1706        observer_node_id: &str,
1707        expected_source: SocketAddr,
1708        nonce: [u8; 8],
1709    ) {
1710        self.pending_observations.lock().await.insert(
1711            nonce,
1712            PendingObservation {
1713                observer_node_id: observer_node_id.to_string(),
1714                expected_source,
1715                nonce,
1716                requested_at: std::time::Instant::now(),
1717            },
1718        );
1719    }
1720
1721    pub async fn pending_observation(&self, nonce: &[u8; 8]) -> Option<PendingObservation> {
1722        self.pending_observations.lock().await.get(nonce).cloned()
1723    }
1724
1725    pub async fn remove_pending_observation(&self, nonce: &[u8; 8]) -> Option<PendingObservation> {
1726        self.pending_observations.lock().await.remove(nonce)
1727    }
1728
1729    pub fn set_gateway_external_ip(&self, external_ip: Option<String>) {
1730        *self.gateway_external_ip.write() = external_ip;
1731    }
1732
1733    pub fn gateway_external_ip(&self) -> Option<String> {
1734        self.gateway_external_ip.read().clone()
1735    }
1736
1737    pub fn set_public_tcp_hello_addr(&self, addr: Option<String>) {
1738        *self.public_tcp_hello_addr.write() = addr;
1739    }
1740
1741    pub fn public_tcp_hello_addr(&self) -> Option<String> {
1742        self.public_tcp_hello_addr.read().clone()
1743    }
1744
1745    pub fn set_direct_tcp_hint(&self, enabled: bool) {
1746        self.direct_tcp_hint.store(enabled, Ordering::Relaxed);
1747    }
1748
1749    pub fn direct_tcp_hint(&self) -> bool {
1750        self.direct_tcp_hint.load(Ordering::Relaxed)
1751    }
1752
1753    pub fn replace_nat_diagnostic_summary(&self, summary: Option<String>) -> bool {
1754        let mut guard = self.nat_diagnostic_summary.write();
1755        if *guard == summary {
1756            false
1757        } else {
1758            *guard = summary;
1759            true
1760        }
1761    }
1762
1763    /// Register the live UDP socket and Noise session map (called after `spawn_udp_listener`).
1764    #[cfg(feature = "noise")]
1765    pub async fn set_udp_handle(
1766        &self,
1767        socket: std::sync::Arc<tokio::net::UdpSocket>,
1768        sessions: crate::network::udp_listener::UdpSessions,
1769        static_private: Vec<u8>,
1770    ) {
1771        *self.udp_handle.lock().await = Some(UdpHandle {
1772            socket,
1773            sessions,
1774            static_private,
1775        });
1776    }
1777
1778    /// Register the `NatState` built from config (called in main.rs).
1779    #[cfg(feature = "noise")]
1780    pub async fn set_nat_state(
1781        &self,
1782        state: std::sync::Arc<crate::network::nat_traversal::NatState>,
1783    ) {
1784        *self.nat_state.write().await = Some(state);
1785    }
1786
1787    /// Dispatch `PunchCoordinate` to the nat_traversal rendezvous handler.
1788    #[cfg(feature = "noise")]
1789    pub async fn handle_punch_coordinate_msg(
1790        &self,
1791        attempt_id: &str,
1792        from_node_id: &str,
1793        target_node_id: &str,
1794        timeout_ms: u64,
1795    ) {
1796        let nat = self.nat_state.read().await.clone();
1797        if let Some(state) = nat {
1798            crate::network::nat_traversal::handle_punch_coordinate(
1799                self,
1800                &state,
1801                attempt_id,
1802                from_node_id,
1803                target_node_id,
1804                timeout_ms,
1805            )
1806            .await;
1807        }
1808    }
1809
1810    /// Dispatch `PunchReady` to the nat_traversal rendezvous handler.
1811    #[cfg(feature = "noise")]
1812    pub async fn handle_punch_ready_msg(
1813        &self,
1814        attempt_id: &str,
1815        responder_node_id: &str,
1816        target_node_id: &str,
1817        ok: bool,
1818    ) {
1819        let nat = self.nat_state.read().await.clone();
1820        if let Some(state) = nat {
1821            crate::network::nat_traversal::handle_punch_ready(
1822                self,
1823                &state,
1824                attempt_id,
1825                responder_node_id,
1826                target_node_id,
1827                ok,
1828            )
1829            .await;
1830        }
1831    }
1832
1833    /// Execute a hole-punch window on receipt of `PunchGo`.  Spawns a background task.
1834    #[cfg(feature = "noise")]
1835    pub async fn handle_punch_go_msg(
1836        &self,
1837        local_node_id: &str,
1838        punch: crate::network::nat_traversal::PunchGoParams,
1839    ) {
1840        let nat = self.nat_state.read().await.clone();
1841        let handle = self.udp_handle.lock().await.clone();
1842        if let (Some(state), Some(h)) = (nat, handle) {
1843            let pm = self.clone();
1844            let local_node_id = local_node_id.to_string();
1845            tokio::spawn(async move {
1846                crate::network::nat_traversal::execute_punch_window(
1847                    &pm,
1848                    &h.socket,
1849                    &h.sessions,
1850                    &h.static_private,
1851                    &state,
1852                    &local_node_id,
1853                    punch,
1854                )
1855                .await;
1856            });
1857        }
1858    }
1859}
1860
1861#[derive(Clone, Debug)]
1862struct BindingPrefs {
1863    store_forward: bool,
1864    // Unix epoch seconds; if None, no automatic expiry
1865    expires_at: Option<u64>,
1866    // Optional QoS hint
1867    qos: Option<String>,
1868}
1869
1870fn current_unix_ts() -> u64 {
1871    use std::time::{SystemTime, UNIX_EPOCH};
1872    SystemTime::now()
1873        .duration_since(UNIX_EPOCH)
1874        .unwrap_or_default()
1875        .as_secs()
1876}