Skip to main content

rns_net/
driver.rs

1//! Driver loop: receives events, drives the TransportEngine, dispatches actions.
2
3use std::collections::HashMap;
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::{Arc, Mutex};
6use std::time::{Duration, Instant};
7
8use rns_core::packet::RawPacket;
9use rns_core::transport::announce_verify_queue::{AnnounceVerifyQueue, OverflowPolicy};
10use rns_core::transport::tables::PathEntry;
11use rns_core::transport::types::{InterfaceId, TransportAction, TransportConfig};
12use rns_core::transport::TransportEngine;
13use rns_crypto::{OsRng, Rng};
14
15#[cfg(feature = "rns-hooks")]
16use crate::provider_bridge::ProviderBridge;
17#[cfg(feature = "rns-hooks")]
18use rns_hooks::{create_hook_slots, EngineAccess, HookContext, HookManager, HookPoint, HookSlot};
19
20#[cfg(feature = "rns-hooks")]
21use crate::event::BackbonePeerHookEvent;
22use crate::event::{
23    BackbonePeerPoolMemberStatus, BackbonePeerPoolStatus, BackbonePeerStateEntry, BlackholeInfo,
24    DrainStatus, Event, EventReceiver, InterfaceStatsResponse, KnownDestinationEntry,
25    LifecycleState, LocalDestinationEntry, NextHopResponse, PathTableEntry, QueryRequest,
26    QueryResponse, RateTableEntry, RuntimeConfigApplyMode, RuntimeConfigEntry, RuntimeConfigError,
27    RuntimeConfigErrorCode, RuntimeConfigSource, RuntimeConfigValue, SingleInterfaceStat,
28};
29use crate::holepunch::orchestrator::{HolePunchManager, HolePunchManagerAction};
30use crate::ifac;
31#[cfg(all(feature = "iface-auto", test))]
32use crate::interface::auto::AutoRuntime;
33#[cfg(feature = "iface-auto")]
34use crate::interface::auto::AutoRuntimeConfigHandle;
35#[cfg(feature = "iface-backbone")]
36use crate::interface::backbone::{
37    start_client, BackboneClientConfig, BackboneClientRuntime, BackboneClientRuntimeConfigHandle,
38    BackbonePeerStateHandle, BackboneRuntimeConfigHandle,
39};
40#[cfg(all(feature = "iface-backbone", target_os = "linux", test))]
41use crate::interface::backbone::{BackboneAbuseConfig, BackboneServerRuntime};
42#[cfg(all(feature = "iface-i2p", test))]
43use crate::interface::i2p::I2pRuntime;
44#[cfg(feature = "iface-i2p")]
45use crate::interface::i2p::I2pRuntimeConfigHandle;
46#[cfg(all(feature = "iface-pipe", test))]
47use crate::interface::pipe::PipeRuntime;
48#[cfg(feature = "iface-pipe")]
49use crate::interface::pipe::PipeRuntimeConfigHandle;
50#[cfg(all(feature = "iface-rnode", test))]
51use crate::interface::rnode::RNodeSubConfig;
52#[cfg(feature = "iface-rnode")]
53use crate::interface::rnode::{validate_sub_config, RNodeRuntime, RNodeRuntimeConfigHandle};
54#[cfg(feature = "iface-tcp")]
55use crate::interface::tcp::TcpClientRuntimeConfigHandle;
56#[cfg(all(feature = "iface-tcp", test))]
57use crate::interface::tcp_server::TcpServerRuntime;
58#[cfg(feature = "iface-tcp")]
59use crate::interface::tcp_server::TcpServerRuntimeConfigHandle;
60#[cfg(all(feature = "iface-udp", test))]
61use crate::interface::udp::UdpRuntime;
62#[cfg(feature = "iface-udp")]
63use crate::interface::udp::UdpRuntimeConfigHandle;
64use crate::interface::{InterfaceEntry, InterfaceStats};
65use crate::link_manager::{LinkManager, LinkManagerAction};
66use crate::time;
67
68const DEFAULT_KNOWN_DESTINATIONS_TTL: f64 = 48.0 * 60.0 * 60.0;
69const DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES: usize = 8192;
70const DEFAULT_RATE_LIMITER_TTL_SECS: f64 = 48.0 * 60.0 * 60.0;
71const DEFAULT_TICK_INTERVAL_MS: u64 = 1000;
72const DEFAULT_KNOWN_DESTINATIONS_CLEANUP_INTERVAL_TICKS: u32 = 3600;
73const DEFAULT_ANNOUNCE_CACHE_CLEANUP_INTERVAL_TICKS: u32 = 3600;
74const DEFAULT_ANNOUNCE_CACHE_CLEANUP_BATCH_SIZE: usize = 10_000;
75const DEFAULT_DISCOVERY_CLEANUP_INTERVAL_TICKS: u32 = 3600;
76const DEFAULT_MANAGEMENT_ANNOUNCE_INTERVAL_SECS: f64 = 300.0;
77const SEND_RETRY_BACKOFF_MIN: Duration = Duration::from_millis(25);
78const SEND_RETRY_BACKOFF_MAX: Duration = Duration::from_millis(1000);
79
80fn inject_transport_header(raw: &[u8], next_hop: &[u8; 16]) -> Vec<u8> {
81    if raw.len() < 18 {
82        return raw.to_vec();
83    }
84
85    let new_flags = (rns_core::constants::HEADER_2 << 6)
86        | (rns_core::constants::TRANSPORT_TRANSPORT << 4)
87        | (raw[0] & 0x0F);
88
89    let mut new_raw = Vec::with_capacity(raw.len() + 16);
90    new_raw.push(new_flags);
91    new_raw.push(raw[1]);
92    new_raw.extend_from_slice(next_hop);
93    new_raw.extend_from_slice(&raw[2..]);
94    new_raw
95}
96
97fn recover_mutex_guard<'a, T>(mutex: &'a Mutex<T>, label: &str) -> std::sync::MutexGuard<'a, T> {
98    match mutex.lock() {
99        Ok(guard) => guard,
100        Err(poisoned) => {
101            log::error!("recovering from poisoned mutex: {}", label);
102            poisoned.into_inner()
103        }
104    }
105}
106
107#[derive(Debug, Clone, Copy)]
108pub(crate) struct RuntimeConfigDefaults {
109    pub(crate) tick_interval_ms: u64,
110    pub(crate) known_destinations_ttl: f64,
111    pub(crate) rate_limiter_ttl_secs: f64,
112    pub(crate) known_destinations_cleanup_interval_ticks: u32,
113    pub(crate) announce_cache_cleanup_interval_ticks: u32,
114    pub(crate) announce_cache_cleanup_batch_size: usize,
115    pub(crate) discovery_cleanup_interval_ticks: u32,
116    pub(crate) management_announce_interval_secs: f64,
117    pub(crate) direct_connect_policy: crate::event::HolePunchPolicy,
118    #[cfg(feature = "rns-hooks")]
119    pub(crate) provider_queue_max_events: usize,
120    #[cfg(feature = "rns-hooks")]
121    pub(crate) provider_queue_max_bytes: usize,
122}
123
124#[cfg(feature = "iface-backbone")]
125#[derive(Debug, Clone)]
126pub(crate) struct BackboneDiscoveryRuntime {
127    pub(crate) discoverable: bool,
128    pub(crate) config: crate::discovery::DiscoveryConfig,
129    pub(crate) transport_enabled: bool,
130    pub(crate) ifac_netname: Option<String>,
131    pub(crate) ifac_netkey: Option<String>,
132}
133
134#[cfg(feature = "iface-backbone")]
135#[derive(Debug, Clone)]
136pub(crate) struct BackboneDiscoveryRuntimeHandle {
137    pub(crate) interface_name: String,
138    pub(crate) current: BackboneDiscoveryRuntime,
139    pub(crate) startup: BackboneDiscoveryRuntime,
140}
141
142#[cfg(feature = "iface-tcp")]
143#[derive(Debug, Clone)]
144pub(crate) struct TcpServerDiscoveryRuntime {
145    pub(crate) discoverable: bool,
146    pub(crate) config: crate::discovery::DiscoveryConfig,
147    pub(crate) transport_enabled: bool,
148    pub(crate) ifac_netname: Option<String>,
149    pub(crate) ifac_netkey: Option<String>,
150}
151
152#[cfg(feature = "iface-tcp")]
153#[derive(Debug, Clone)]
154pub(crate) struct TcpServerDiscoveryRuntimeHandle {
155    pub(crate) interface_name: String,
156    pub(crate) current: TcpServerDiscoveryRuntime,
157    pub(crate) startup: TcpServerDiscoveryRuntime,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub(crate) struct IfacRuntimeConfig {
162    pub(crate) netname: Option<String>,
163    pub(crate) netkey: Option<String>,
164    pub(crate) size: usize,
165}
166
167#[cfg(feature = "iface-backbone")]
168#[derive(Debug, Clone)]
169pub struct BackbonePeerPoolSettings {
170    pub max_connected: usize,
171    pub failure_threshold: usize,
172    pub failure_window: Duration,
173    pub cooldown: Duration,
174}
175
176#[cfg(feature = "iface-backbone")]
177pub(crate) struct BackbonePeerPoolCandidateConfig {
178    pub(crate) client: BackboneClientConfig,
179    pub(crate) mode: u8,
180    pub(crate) ingress_control: rns_core::transport::types::IngressControlConfig,
181    pub(crate) ifac_runtime: IfacRuntimeConfig,
182    pub(crate) ifac_enabled: bool,
183    pub(crate) interface_type_name: String,
184}
185
186#[cfg(feature = "iface-backbone")]
187struct BackbonePeerPool {
188    settings: BackbonePeerPoolSettings,
189    candidates: Vec<BackbonePeerPoolCandidate>,
190}
191
192#[cfg(feature = "iface-backbone")]
193struct BackbonePeerPoolCandidate {
194    config: BackbonePeerPoolCandidateConfig,
195    active_id: Option<InterfaceId>,
196    failures: Vec<f64>,
197    retry_after: Option<f64>,
198    cooldown_until: Option<f64>,
199    last_error: Option<String>,
200}
201
202/// Thin wrapper providing `EngineAccess` for a `TransportEngine` + Driver interfaces.
203#[cfg(feature = "rns-hooks")]
204struct EngineRef<'a> {
205    engine: &'a TransportEngine,
206    interfaces: &'a HashMap<InterfaceId, InterfaceEntry>,
207    link_manager: &'a LinkManager,
208    now: f64,
209}
210
211#[cfg(feature = "rns-hooks")]
212impl<'a> EngineAccess for EngineRef<'a> {
213    fn has_path(&self, dest: &[u8; 16]) -> bool {
214        self.engine.has_path(dest)
215    }
216    fn hops_to(&self, dest: &[u8; 16]) -> Option<u8> {
217        self.engine.hops_to(dest)
218    }
219    fn next_hop(&self, dest: &[u8; 16]) -> Option<[u8; 16]> {
220        self.engine.next_hop(dest)
221    }
222    fn is_blackholed(&self, identity: &[u8; 16]) -> bool {
223        self.engine.is_blackholed(identity, self.now)
224    }
225    fn interface_name(&self, id: u64) -> Option<String> {
226        self.interfaces
227            .get(&InterfaceId(id))
228            .map(|e| e.info.name.clone())
229    }
230    fn interface_mode(&self, id: u64) -> Option<u8> {
231        self.interfaces.get(&InterfaceId(id)).map(|e| e.info.mode)
232    }
233    fn identity_hash(&self) -> Option<[u8; 16]> {
234        self.engine.identity_hash().copied()
235    }
236    fn announce_rate(&self, id: u64) -> Option<i32> {
237        self.interfaces
238            .get(&InterfaceId(id))
239            .map(|e| (e.stats.outgoing_announce_freq() * 1000.0) as i32)
240    }
241    fn link_state(&self, link_hash: &[u8; 16]) -> Option<u8> {
242        use rns_core::link::types::LinkState;
243        self.link_manager.link_state(link_hash).map(|s| match s {
244            LinkState::Pending => 0,
245            LinkState::Handshake => 1,
246            LinkState::Active => 2,
247            LinkState::Stale => 3,
248            LinkState::Closed => 4,
249        })
250    }
251}
252
253/// Extract the 16-byte destination hash from a raw packet header.
254///
255/// HEADER_1 (raw[0] & 0x40 == 0): dest at bytes 2..18
256/// HEADER_2 (raw[0] & 0x40 != 0): dest at bytes 18..34 (after transport ID)
257#[cfg(any(test, feature = "rns-hooks"))]
258fn extract_dest_hash(raw: &[u8]) -> [u8; 16] {
259    let mut dest = [0u8; 16];
260    if raw.is_empty() {
261        return dest;
262    }
263    let is_header2 = raw[0] & 0x40 != 0;
264    let start = if is_header2 { 18 } else { 2 };
265    let end = start + 16;
266    if raw.len() >= end {
267        dest.copy_from_slice(&raw[start..end]);
268    }
269    dest
270}
271
272/// Execute a hook chain on disjoint Driver fields (avoids &mut self borrow conflict).
273#[cfg(feature = "rns-hooks")]
274fn run_hook_inner(
275    programs: &mut [rns_hooks::LoadedProgram],
276    hook_manager: &Option<HookManager>,
277    engine_access: &dyn EngineAccess,
278    ctx: &HookContext,
279    now: f64,
280    provider_events_enabled: bool,
281) -> Option<rns_hooks::ExecuteResult> {
282    if programs.is_empty() {
283        return None;
284    }
285    let mgr = hook_manager.as_ref()?;
286    mgr.run_chain_with_provider_events(programs, ctx, engine_access, now, provider_events_enabled)
287}
288
289#[cfg(feature = "rns-hooks")]
290fn backbone_peer_hook_context(event: &BackbonePeerHookEvent) -> HookContext<'_> {
291    HookContext::BackbonePeer {
292        server_interface_id: event.server_interface_id.0,
293        peer_interface_id: event.peer_interface_id.map(|id| id.0),
294        peer_ip: event.peer_ip,
295        peer_port: event.peer_port,
296        connected_for: event.connected_for,
297        had_received_data: event.had_received_data,
298        penalty_level: event.penalty_level,
299        blacklist_for: event.blacklist_for,
300    }
301}
302
303/// Convert a Vec of ActionWire into TransportActions for dispatch.
304#[cfg(feature = "rns-hooks")]
305fn convert_injected_actions(actions: Vec<rns_hooks::ActionWire>) -> Vec<TransportAction> {
306    actions
307        .into_iter()
308        .map(|a| {
309            use rns_hooks::ActionWire;
310            match a {
311                ActionWire::SendOnInterface { interface, raw } => {
312                    TransportAction::SendOnInterface {
313                        interface: InterfaceId(interface),
314                        raw,
315                    }
316                }
317                ActionWire::BroadcastOnAllInterfaces {
318                    raw,
319                    exclude,
320                    has_exclude,
321                } => TransportAction::BroadcastOnAllInterfaces {
322                    raw,
323                    exclude: if has_exclude != 0 {
324                        Some(InterfaceId(exclude))
325                    } else {
326                        None
327                    },
328                },
329                ActionWire::DeliverLocal {
330                    destination_hash,
331                    raw,
332                    packet_hash,
333                    receiving_interface,
334                } => TransportAction::DeliverLocal {
335                    destination_hash,
336                    raw,
337                    packet_hash,
338                    receiving_interface: InterfaceId(receiving_interface),
339                },
340                ActionWire::PathUpdated {
341                    destination_hash,
342                    hops,
343                    next_hop,
344                    interface,
345                } => TransportAction::PathUpdated {
346                    destination_hash,
347                    hops,
348                    next_hop,
349                    interface: InterfaceId(interface),
350                },
351                ActionWire::CacheAnnounce { packet_hash, raw } => {
352                    TransportAction::CacheAnnounce { packet_hash, raw }
353                }
354                ActionWire::TunnelEstablished {
355                    tunnel_id,
356                    interface,
357                } => TransportAction::TunnelEstablished {
358                    tunnel_id,
359                    interface: InterfaceId(interface),
360                },
361                ActionWire::TunnelSynthesize {
362                    interface,
363                    data,
364                    dest_hash,
365                } => TransportAction::TunnelSynthesize {
366                    interface: InterfaceId(interface),
367                    data,
368                    dest_hash,
369                },
370                ActionWire::ForwardToLocalClients {
371                    raw,
372                    exclude,
373                    has_exclude,
374                } => TransportAction::ForwardToLocalClients {
375                    raw,
376                    exclude: if has_exclude != 0 {
377                        Some(InterfaceId(exclude))
378                    } else {
379                        None
380                    },
381                },
382                ActionWire::ForwardPlainBroadcast {
383                    raw,
384                    to_local,
385                    exclude,
386                    has_exclude,
387                } => TransportAction::ForwardPlainBroadcast {
388                    raw,
389                    to_local: to_local != 0,
390                    exclude: if has_exclude != 0 {
391                        Some(InterfaceId(exclude))
392                    } else {
393                        None
394                    },
395                },
396                ActionWire::AnnounceReceived {
397                    destination_hash,
398                    identity_hash,
399                    public_key,
400                    name_hash,
401                    random_hash,
402                    app_data,
403                    hops,
404                    receiving_interface,
405                } => TransportAction::AnnounceReceived {
406                    destination_hash,
407                    identity_hash,
408                    public_key,
409                    name_hash,
410                    random_hash,
411                    app_data,
412                    hops,
413                    receiving_interface: InterfaceId(receiving_interface),
414                },
415            }
416        })
417        .collect()
418}
419
420/// Infer the interface type string from a dynamic interface's name.
421/// Dynamic interfaces (TCP server clients, backbone peers, auto peers, local server clients)
422/// include their type in the name prefix set at construction.
423fn infer_interface_type(name: &str) -> String {
424    if name.starts_with("TCPServerInterface") {
425        "TCPServerClientInterface".to_string()
426    } else if name.starts_with("BackboneInterface") {
427        "BackboneInterface".to_string()
428    } else if name.starts_with("LocalInterface") {
429        "LocalServerClientInterface".to_string()
430    } else {
431        // AutoInterface peers use "{group_name}:{peer_addr}" format where
432        // group_name is the config section name (typically "AutoInterface" or similar).
433        "AutoInterface".to_string()
434    }
435}
436
437pub use crate::common::callbacks::Callbacks;
438
439#[derive(Clone)]
440struct SharedAnnounceRecord {
441    name_hash: [u8; 10],
442    identity_prv_key: [u8; 64],
443    app_data: Option<Vec<u8>>,
444}
445
446#[derive(Debug, Clone)]
447pub(crate) struct KnownDestinationState {
448    announced: crate::destination::AnnouncedIdentity,
449    was_used: bool,
450    last_used_at: Option<f64>,
451    retained: bool,
452}
453
454/// The driver loop. Owns the engine and all interface entries.
455pub struct Driver {
456    pub(crate) engine: TransportEngine,
457    pub(crate) interfaces: HashMap<InterfaceId, InterfaceEntry>,
458    pub(crate) rng: OsRng,
459    pub(crate) rx: EventReceiver,
460    pub(crate) callbacks: Box<dyn Callbacks>,
461    pub(crate) started: f64,
462    pub(crate) lifecycle_state: LifecycleState,
463    pub(crate) drain_started_at: Option<Instant>,
464    pub(crate) drain_deadline: Option<Instant>,
465    pub(crate) listener_controls: Vec<crate::interface::ListenerControl>,
466    pub(crate) announce_cache: Option<crate::announce_cache::AnnounceCache>,
467    /// Destination hash for rnstransport.tunnel.synthesize (PLAIN).
468    pub(crate) tunnel_synth_dest: [u8; 16],
469    /// Transport identity (optional, needed for tunnel synthesis).
470    pub(crate) transport_identity: Option<rns_crypto::identity::Identity>,
471    /// Link manager: handles link lifecycle, request/response.
472    pub(crate) link_manager: LinkManager,
473    /// Management configuration for ACL checks.
474    pub(crate) management_config: crate::management::ManagementConfig,
475    /// Last time management announces were emitted.
476    pub(crate) last_management_announce: f64,
477    /// Whether initial management announce has been sent (delayed 5s after start).
478    pub(crate) initial_announce_sent: bool,
479    /// Cache of known announced identities and lifecycle state, keyed by destination hash.
480    pub(crate) known_destinations: HashMap<[u8; 16], KnownDestinationState>,
481    /// TTL for known destinations without an active path, in seconds.
482    pub(crate) known_destinations_ttl: f64,
483    /// Maximum number of retained known destinations.
484    pub(crate) known_destinations_max_entries: usize,
485    /// TTL for announce rate-limiter entries without an active path, in seconds.
486    pub(crate) rate_limiter_ttl_secs: f64,
487    /// Destination hash for rnstransport.path.request (PLAIN).
488    pub(crate) path_request_dest: [u8; 16],
489    /// Proof strategies per destination hash.
490    /// Maps dest_hash → (strategy, optional signing identity for generating proofs).
491    pub(crate) proof_strategies: HashMap<
492        [u8; 16],
493        (
494            rns_core::types::ProofStrategy,
495            Option<rns_crypto::identity::Identity>,
496        ),
497    >,
498    /// Tracked sent packets for proof matching: packet_hash → (dest_hash, sent_time).
499    pub(crate) sent_packets: HashMap<[u8; 32], ([u8; 16], f64)>,
500    /// Completed proofs for probe polling: packet_hash → (rtt_seconds, received_time).
501    pub(crate) completed_proofs: HashMap<[u8; 32], (f64, f64)>,
502    /// Locally registered destinations: hash → dest_type.
503    pub(crate) local_destinations: HashMap<[u8; 16], u8>,
504    /// Latest explicit SINGLE announces to replay after shared-client reconnect.
505    shared_announces: HashMap<[u8; 16], SharedAnnounceRecord>,
506    /// Shared local interfaces that went down and should replay announces on reconnect.
507    shared_reconnect_pending: HashMap<InterfaceId, bool>,
508    /// Hole-punch manager for direct P2P connections.
509    pub(crate) holepunch_manager: HolePunchManager,
510    /// Event sender for worker threads to send results back to the driver loop.
511    pub(crate) event_tx: crate::event::EventSender,
512    /// Maximum queued outbound frames per interface writer worker.
513    pub(crate) interface_writer_queue_capacity: usize,
514    /// Shared timer interval used by the node timer thread.
515    pub(crate) tick_interval_ms: Arc<AtomicU64>,
516    /// Runtime-config handles for backbone server interfaces, keyed by config name.
517    #[cfg(feature = "iface-backbone")]
518    pub(crate) backbone_runtime: HashMap<String, BackboneRuntimeConfigHandle>,
519    /// Live peer-state handles for backbone server interfaces, keyed by config name.
520    #[cfg(feature = "iface-backbone")]
521    pub(crate) backbone_peer_state: HashMap<String, BackbonePeerStateHandle>,
522    /// Runtime-config handles for backbone client interfaces, keyed by config name.
523    #[cfg(feature = "iface-backbone")]
524    pub(crate) backbone_client_runtime: HashMap<String, BackboneClientRuntimeConfigHandle>,
525    /// Runtime-config state for backbone discovery metadata, keyed by config name.
526    #[cfg(feature = "iface-backbone")]
527    pub(crate) backbone_discovery_runtime: HashMap<String, BackboneDiscoveryRuntimeHandle>,
528    /// Ordered outbound Backbone peer pool, if enabled.
529    #[cfg(feature = "iface-backbone")]
530    backbone_peer_pool: Option<BackbonePeerPool>,
531    /// Runtime-config handles for TCP server interfaces, keyed by config name.
532    #[cfg(feature = "iface-tcp")]
533    pub(crate) tcp_server_runtime: HashMap<String, TcpServerRuntimeConfigHandle>,
534    /// Runtime-config handles for TCP client interfaces, keyed by config name.
535    #[cfg(feature = "iface-tcp")]
536    pub(crate) tcp_client_runtime: HashMap<String, TcpClientRuntimeConfigHandle>,
537    /// Runtime-config state for TCP server discovery metadata, keyed by config name.
538    #[cfg(feature = "iface-tcp")]
539    pub(crate) tcp_server_discovery_runtime: HashMap<String, TcpServerDiscoveryRuntimeHandle>,
540    /// Runtime-config handles for UDP interfaces, keyed by config name.
541    #[cfg(feature = "iface-udp")]
542    pub(crate) udp_runtime: HashMap<String, UdpRuntimeConfigHandle>,
543    /// Runtime-config handles for Auto interfaces, keyed by config name.
544    #[cfg(feature = "iface-auto")]
545    pub(crate) auto_runtime: HashMap<String, AutoRuntimeConfigHandle>,
546    /// Runtime-config handles for I2P interfaces, keyed by config name.
547    #[cfg(feature = "iface-i2p")]
548    pub(crate) i2p_runtime: HashMap<String, I2pRuntimeConfigHandle>,
549    /// Runtime-config handles for Pipe interfaces, keyed by config name.
550    #[cfg(feature = "iface-pipe")]
551    pub(crate) pipe_runtime: HashMap<String, PipeRuntimeConfigHandle>,
552    /// Runtime-config handles for RNode interfaces, keyed by config name.
553    #[cfg(feature = "iface-rnode")]
554    pub(crate) rnode_runtime: HashMap<String, RNodeRuntimeConfigHandle>,
555    /// Startup/default interface metadata for generic cross-cutting runtime config.
556    pub(crate) interface_runtime_defaults:
557        HashMap<String, rns_core::transport::types::InterfaceInfo>,
558    /// Current IFAC runtime config for static interfaces that support IFAC mutation.
559    pub(crate) interface_ifac_runtime: HashMap<String, IfacRuntimeConfig>,
560    /// Startup/default IFAC runtime config for static interfaces.
561    pub(crate) interface_ifac_runtime_defaults: HashMap<String, IfacRuntimeConfig>,
562    /// Storage for discovered interfaces.
563    pub(crate) discovered_interfaces: crate::discovery::DiscoveredInterfaceStorage,
564    /// Required stamp value for accepting discovered interfaces.
565    pub(crate) discovery_required_value: u8,
566    /// Name hash for interface discovery announces ("rnstransport.discovery.interface").
567    pub(crate) discovery_name_hash: [u8; 10],
568    /// Destination hash for the probe responder (if respond_to_probes is enabled).
569    pub(crate) probe_responder_hash: Option<[u8; 16]>,
570    /// Whether interface discovery is enabled.
571    pub(crate) discover_interfaces: bool,
572    /// Announcer for discoverable interfaces (None if nothing to announce).
573    pub(crate) interface_announcer: Option<crate::discovery::InterfaceAnnouncer>,
574    /// Shared async announce verification queue.
575    pub(crate) announce_verify_queue: Arc<Mutex<AnnounceVerifyQueue>>,
576    /// Whether inbound announces should be verified off the driver thread.
577    pub(crate) async_announce_verification: bool,
578    /// Tick counter for periodic discovery cleanup (every ~3600 ticks = ~1 hour).
579    pub(crate) discovery_cleanup_counter: u32,
580    /// Runtime-configurable discovery cleanup interval.
581    pub(crate) discovery_cleanup_interval_ticks: u32,
582    /// Tick counter for periodic MEMSTATS logging (every 300 ticks = ~5 min).
583    pub(crate) memory_stats_counter: u32,
584    /// Tick counter for periodic memory/cache cleanup (every ~3600 ticks = ~1 hour).
585    pub(crate) cache_cleanup_counter: u32,
586    /// Tick counter for incremental announce-cache cleanup scheduling.
587    pub(crate) announce_cache_cleanup_counter: u32,
588    /// Runtime-configurable cleanup interval for known destinations.
589    pub(crate) known_destinations_cleanup_interval_ticks: u32,
590    /// Count of known-destination cap evictions since start.
591    pub(crate) known_destinations_cap_evict_count: usize,
592    /// Runtime-configurable interval for starting announce cache cleanup.
593    pub(crate) announce_cache_cleanup_interval_ticks: u32,
594    /// When set, announce cache cleanup is in progress (contains active packet hashes).
595    pub(crate) cache_cleanup_active_hashes: Option<Vec<[u8; 32]>>,
596    /// Directory iterator for incremental announce cache cleanup.
597    pub(crate) cache_cleanup_entries: Option<std::fs::ReadDir>,
598    /// Running total of files removed during current cache cleanup cycle.
599    pub(crate) cache_cleanup_removed: usize,
600    /// Runtime-configurable announce cache cleanup batch size.
601    pub(crate) announce_cache_cleanup_batch_size: usize,
602    /// Runtime-configurable management announce interval.
603    pub(crate) management_announce_interval_secs: f64,
604    /// Startup/default runtime-config values.
605    pub(crate) runtime_config_defaults: RuntimeConfigDefaults,
606    /// Hook slots for the WASM hook system (one per HookPoint).
607    #[cfg(feature = "rns-hooks")]
608    pub(crate) hook_slots: [HookSlot; HookPoint::COUNT],
609    /// WASM hook manager (runtime + linker). None if initialization failed.
610    #[cfg(feature = "rns-hooks")]
611    pub(crate) hook_manager: Option<HookManager>,
612    #[cfg(feature = "rns-hooks")]
613    pub(crate) provider_bridge: Option<ProviderBridge>,
614}
615
616impl Driver {
617    /// Create a new driver.
618    pub fn new(
619        config: TransportConfig,
620        rx: EventReceiver,
621        tx: crate::event::EventSender,
622        callbacks: Box<dyn Callbacks>,
623    ) -> Self {
624        let announce_queue_max_entries = config.announce_queue_max_entries;
625        let tunnel_synth_dest = rns_core::destination::destination_hash(
626            "rnstransport",
627            &["tunnel", "synthesize"],
628            None,
629        );
630        let path_request_dest =
631            rns_core::destination::destination_hash("rnstransport", &["path", "request"], None);
632        let discovery_name_hash = crate::discovery::discovery_name_hash();
633        let mut engine = TransportEngine::new(config);
634        engine.register_destination(tunnel_synth_dest, rns_core::constants::DESTINATION_PLAIN);
635        // Register path request destination so inbound path requests are delivered locally
636        engine.register_destination(path_request_dest, rns_core::constants::DESTINATION_PLAIN);
637        // Note: discovery destination is NOT registered as local — it's a SINGLE destination
638        // whose hash depends on the sender's identity. We match it by name_hash instead.
639        let mut local_destinations = HashMap::new();
640        local_destinations.insert(tunnel_synth_dest, rns_core::constants::DESTINATION_PLAIN);
641        local_destinations.insert(path_request_dest, rns_core::constants::DESTINATION_PLAIN);
642        let runtime_config_defaults = RuntimeConfigDefaults {
643            tick_interval_ms: DEFAULT_TICK_INTERVAL_MS,
644            known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
645            rate_limiter_ttl_secs: DEFAULT_RATE_LIMITER_TTL_SECS,
646            known_destinations_cleanup_interval_ticks:
647                DEFAULT_KNOWN_DESTINATIONS_CLEANUP_INTERVAL_TICKS,
648            announce_cache_cleanup_interval_ticks: DEFAULT_ANNOUNCE_CACHE_CLEANUP_INTERVAL_TICKS,
649            announce_cache_cleanup_batch_size: DEFAULT_ANNOUNCE_CACHE_CLEANUP_BATCH_SIZE,
650            discovery_cleanup_interval_ticks: DEFAULT_DISCOVERY_CLEANUP_INTERVAL_TICKS,
651            management_announce_interval_secs: DEFAULT_MANAGEMENT_ANNOUNCE_INTERVAL_SECS,
652            direct_connect_policy: crate::event::HolePunchPolicy::default(),
653            #[cfg(feature = "rns-hooks")]
654            provider_queue_max_events: crate::provider_bridge::ProviderBridgeConfig::default()
655                .queue_max_events,
656            #[cfg(feature = "rns-hooks")]
657            provider_queue_max_bytes: crate::provider_bridge::ProviderBridgeConfig::default()
658                .queue_max_bytes,
659        };
660        Driver {
661            engine,
662            interfaces: HashMap::new(),
663            rng: OsRng,
664            rx,
665            callbacks,
666            started: time::now(),
667            lifecycle_state: LifecycleState::Active,
668            drain_started_at: None,
669            drain_deadline: None,
670            listener_controls: Vec::new(),
671            announce_cache: None,
672            tunnel_synth_dest,
673            transport_identity: None,
674            link_manager: LinkManager::new(),
675            management_config: Default::default(),
676            last_management_announce: 0.0,
677            initial_announce_sent: false,
678            known_destinations: HashMap::new(),
679            known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
680            known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
681            rate_limiter_ttl_secs: DEFAULT_RATE_LIMITER_TTL_SECS,
682            path_request_dest,
683            proof_strategies: HashMap::new(),
684            sent_packets: HashMap::new(),
685            completed_proofs: HashMap::new(),
686            local_destinations,
687            shared_announces: HashMap::new(),
688            shared_reconnect_pending: HashMap::new(),
689            holepunch_manager: HolePunchManager::new(
690                vec![],
691                rns_core::holepunch::ProbeProtocol::Rnsp,
692                None,
693            ),
694            event_tx: tx,
695            interface_writer_queue_capacity: crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
696            tick_interval_ms: Arc::new(AtomicU64::new(DEFAULT_TICK_INTERVAL_MS)),
697            #[cfg(feature = "iface-backbone")]
698            backbone_runtime: HashMap::new(),
699            #[cfg(feature = "iface-backbone")]
700            backbone_peer_state: HashMap::new(),
701            #[cfg(feature = "iface-backbone")]
702            backbone_client_runtime: HashMap::new(),
703            #[cfg(feature = "iface-backbone")]
704            backbone_discovery_runtime: HashMap::new(),
705            #[cfg(feature = "iface-backbone")]
706            backbone_peer_pool: None,
707            #[cfg(feature = "iface-tcp")]
708            tcp_server_runtime: HashMap::new(),
709            #[cfg(feature = "iface-tcp")]
710            tcp_client_runtime: HashMap::new(),
711            #[cfg(feature = "iface-tcp")]
712            tcp_server_discovery_runtime: HashMap::new(),
713            #[cfg(feature = "iface-udp")]
714            udp_runtime: HashMap::new(),
715            #[cfg(feature = "iface-auto")]
716            auto_runtime: HashMap::new(),
717            #[cfg(feature = "iface-i2p")]
718            i2p_runtime: HashMap::new(),
719            #[cfg(feature = "iface-pipe")]
720            pipe_runtime: HashMap::new(),
721            #[cfg(feature = "iface-rnode")]
722            rnode_runtime: HashMap::new(),
723            interface_runtime_defaults: HashMap::new(),
724            interface_ifac_runtime: HashMap::new(),
725            interface_ifac_runtime_defaults: HashMap::new(),
726            discovered_interfaces: crate::discovery::DiscoveredInterfaceStorage::new(
727                std::env::temp_dir().join("rns-discovered-interfaces"),
728            ),
729            discovery_required_value: crate::discovery::DEFAULT_STAMP_VALUE,
730            discovery_name_hash,
731            probe_responder_hash: None,
732            discover_interfaces: false,
733            interface_announcer: None,
734            announce_verify_queue: Arc::new(Mutex::new(AnnounceVerifyQueue::new(
735                announce_queue_max_entries,
736            ))),
737            async_announce_verification: false,
738            discovery_cleanup_counter: 0,
739            discovery_cleanup_interval_ticks: runtime_config_defaults
740                .discovery_cleanup_interval_ticks,
741            memory_stats_counter: 0,
742            cache_cleanup_counter: 0,
743            announce_cache_cleanup_counter: 0,
744            known_destinations_cleanup_interval_ticks: runtime_config_defaults
745                .known_destinations_cleanup_interval_ticks,
746            known_destinations_cap_evict_count: 0,
747            announce_cache_cleanup_interval_ticks: runtime_config_defaults
748                .announce_cache_cleanup_interval_ticks,
749            cache_cleanup_active_hashes: None,
750            cache_cleanup_entries: None,
751            cache_cleanup_removed: 0,
752            announce_cache_cleanup_batch_size: runtime_config_defaults
753                .announce_cache_cleanup_batch_size,
754            management_announce_interval_secs: runtime_config_defaults
755                .management_announce_interval_secs,
756            runtime_config_defaults,
757            #[cfg(feature = "rns-hooks")]
758            hook_slots: create_hook_slots(),
759            #[cfg(feature = "rns-hooks")]
760            hook_manager: HookManager::new().ok(),
761            #[cfg(feature = "rns-hooks")]
762            provider_bridge: None,
763        }
764    }
765
766    pub fn set_announce_verify_queue_config(
767        &mut self,
768        max_entries: usize,
769        max_bytes: usize,
770        max_stale_secs: f64,
771        overflow_policy: OverflowPolicy,
772    ) {
773        self.announce_verify_queue = Arc::new(Mutex::new(AnnounceVerifyQueue::with_limits(
774            max_entries,
775            max_bytes,
776            max_stale_secs,
777            overflow_policy,
778        )));
779    }
780
781    fn wrap_interface_writer(
782        &self,
783        interface_id: InterfaceId,
784        interface_name: &str,
785        writer: Box<dyn crate::interface::Writer>,
786    ) -> (
787        Box<dyn crate::interface::Writer>,
788        crate::interface::AsyncWriterMetrics,
789    ) {
790        crate::interface::wrap_async_writer(
791            writer,
792            interface_id,
793            interface_name,
794            self.event_tx.clone(),
795            self.interface_writer_queue_capacity,
796        )
797    }
798
799    fn upsert_known_destination(
800        &mut self,
801        dest_hash: [u8; 16],
802        announced: crate::destination::AnnouncedIdentity,
803    ) {
804        if let Some(existing) = self.known_destinations.get_mut(&dest_hash) {
805            existing.announced = announced;
806            return;
807        }
808
809        self.enforce_known_destination_cap(true);
810        self.known_destinations.insert(
811            dest_hash,
812            KnownDestinationState {
813                announced,
814                was_used: false,
815                last_used_at: None,
816                retained: false,
817            },
818        );
819    }
820
821    fn known_destination_entry(
822        dest_hash: [u8; 16],
823        state: &KnownDestinationState,
824    ) -> KnownDestinationEntry {
825        KnownDestinationEntry {
826            dest_hash,
827            identity_hash: state.announced.identity_hash.0,
828            public_key: state.announced.public_key,
829            app_data: state.announced.app_data.clone(),
830            hops: state.announced.hops,
831            received_at: state.announced.received_at,
832            receiving_interface: state.announced.receiving_interface,
833            was_used: state.was_used,
834            last_used_at: state.last_used_at,
835            retained: state.retained,
836        }
837    }
838
839    fn known_destination_entries(&self) -> Vec<KnownDestinationEntry> {
840        let mut entries: Vec<_> = self
841            .known_destinations
842            .iter()
843            .map(|(dest_hash, state)| Self::known_destination_entry(*dest_hash, state))
844            .collect();
845        entries.sort_by(|a, b| a.dest_hash.cmp(&b.dest_hash));
846        entries
847    }
848
849    fn mark_known_destination_used(&mut self, dest_hash: &[u8; 16]) -> bool {
850        let Some(state) = self.known_destinations.get_mut(dest_hash) else {
851            return false;
852        };
853        state.was_used = true;
854        state.last_used_at = Some(time::now());
855        true
856    }
857
858    fn retain_known_destination(&mut self, dest_hash: &[u8; 16]) -> bool {
859        let Some(state) = self.known_destinations.get_mut(dest_hash) else {
860            return false;
861        };
862        state.retained = true;
863        true
864    }
865
866    fn unretain_known_destination(&mut self, dest_hash: &[u8; 16]) -> bool {
867        let Some(state) = self.known_destinations.get_mut(dest_hash) else {
868            return false;
869        };
870        state.retained = false;
871        true
872    }
873
874    fn known_destination_announced(
875        &self,
876        dest_hash: &[u8; 16],
877    ) -> Option<crate::destination::AnnouncedIdentity> {
878        self.known_destinations
879            .get(dest_hash)
880            .map(|state| state.announced.clone())
881    }
882
883    fn known_destination_relevance_time(state: &KnownDestinationState) -> f64 {
884        state.last_used_at.unwrap_or(state.announced.received_at)
885    }
886
887    fn begin_drain(&mut self, timeout: Duration) {
888        let now = Instant::now();
889        let deadline = now + timeout;
890        match self.lifecycle_state {
891            LifecycleState::Active => {
892                self.lifecycle_state = LifecycleState::Draining;
893                self.drain_started_at = Some(now);
894                self.drain_deadline = Some(deadline);
895                log::info!(
896                    "driver entering drain mode with {:.3}s timeout",
897                    timeout.as_secs_f64()
898                );
899                self.stop_listener_accepts();
900            }
901            LifecycleState::Draining => {
902                self.drain_deadline = Some(deadline);
903                log::info!(
904                    "driver drain deadline updated to {:.3}s from now",
905                    timeout.as_secs_f64()
906                );
907                self.stop_listener_accepts();
908            }
909            LifecycleState::Stopping | LifecycleState::Stopped => {
910                log::debug!(
911                    "ignoring BeginDrain while lifecycle state is {:?}",
912                    self.lifecycle_state
913                );
914            }
915        }
916    }
917
918    fn is_draining(&self) -> bool {
919        matches!(self.lifecycle_state, LifecycleState::Draining)
920    }
921
922    pub fn register_listener_control(&mut self, control: crate::interface::ListenerControl) {
923        self.listener_controls.push(control);
924    }
925
926    fn stop_listener_accepts(&mut self) {
927        for control in &self.listener_controls {
928            control.request_stop();
929        }
930        #[cfg(feature = "rns-hooks")]
931        if let Some(bridge) = self.provider_bridge.as_ref() {
932            bridge.stop_accepting();
933        }
934    }
935
936    fn reject_new_work(&self, op: &str) {
937        log::info!("rejecting {} while node is draining", op);
938    }
939
940    fn drain_error(&self, op: &str) -> String {
941        format!("cannot {} while node is draining", op)
942    }
943
944    fn drain_status(&self) -> DrainStatus {
945        let now = Instant::now();
946        let active_links = self.link_manager.link_count();
947        let active_resource_transfers = self.link_manager.resource_transfer_count();
948        let active_holepunch_sessions = self.holepunch_manager.session_count();
949        let interface_writer_queued_frames = self
950            .interfaces
951            .values()
952            .map(|entry| {
953                entry
954                    .async_writer_metrics
955                    .as_ref()
956                    .map(|metrics| metrics.queued_frames())
957                    .unwrap_or(0)
958            })
959            .sum();
960        #[cfg(feature = "rns-hooks")]
961        let (provider_backlog_events, provider_consumer_queued_events) = self
962            .provider_bridge
963            .as_ref()
964            .map(|bridge| {
965                let stats = bridge.stats();
966                (
967                    stats.backlog_len,
968                    stats
969                        .consumers
970                        .iter()
971                        .map(|consumer| consumer.queue_len)
972                        .sum(),
973                )
974            })
975            .unwrap_or((0, 0));
976        #[cfg(not(feature = "rns-hooks"))]
977        let (provider_backlog_events, provider_consumer_queued_events) = (0, 0);
978        let drain_age_seconds = self
979            .drain_started_at
980            .map(|started| started.elapsed().as_secs_f64());
981        let deadline_remaining_seconds = self.drain_deadline.map(|deadline| {
982            deadline
983                .checked_duration_since(now)
984                .map(|remaining| remaining.as_secs_f64())
985                .unwrap_or(0.0)
986        });
987        let detail = match self.lifecycle_state {
988            LifecycleState::Active => Some("node is accepting normal work".into()),
989            LifecycleState::Draining => {
990                let mut remaining = Vec::new();
991                if active_links > 0 {
992                    remaining.push(format!("{active_links} link(s)"));
993                }
994                if active_resource_transfers > 0 {
995                    remaining.push(format!("{active_resource_transfers} resource transfer(s)"));
996                }
997                if active_holepunch_sessions > 0 {
998                    remaining.push(format!("{active_holepunch_sessions} hole-punch session(s)"));
999                }
1000                if interface_writer_queued_frames > 0 {
1001                    remaining.push(format!(
1002                        "{interface_writer_queued_frames} queued interface writer frame(s)"
1003                    ));
1004                }
1005                if provider_backlog_events > 0 {
1006                    remaining.push(format!(
1007                        "{provider_backlog_events} provider backlog event(s)"
1008                    ));
1009                }
1010                if provider_consumer_queued_events > 0 {
1011                    remaining.push(format!(
1012                        "{provider_consumer_queued_events} queued provider consumer event(s)"
1013                    ));
1014                }
1015                Some(if remaining.is_empty() {
1016                    "node is draining existing work; no active links, resource transfers, hole-punch sessions, or queued writer/provider work remain".into()
1017                } else {
1018                    format!(
1019                        "node is draining existing work; {} still active",
1020                        remaining.join(", ")
1021                    )
1022                })
1023            }
1024            LifecycleState::Stopping => Some("node is tearing down remaining work".into()),
1025            LifecycleState::Stopped => Some("node is stopped".into()),
1026        };
1027
1028        DrainStatus {
1029            state: self.lifecycle_state,
1030            drain_age_seconds,
1031            deadline_remaining_seconds,
1032            drain_complete: !matches!(self.lifecycle_state, LifecycleState::Draining)
1033                || (active_links == 0
1034                    && active_resource_transfers == 0
1035                    && active_holepunch_sessions == 0
1036                    && interface_writer_queued_frames == 0
1037                    && provider_backlog_events == 0
1038                    && provider_consumer_queued_events == 0),
1039            interface_writer_queued_frames,
1040            provider_backlog_events,
1041            provider_consumer_queued_events,
1042            detail,
1043        }
1044    }
1045
1046    fn enforce_drain_deadline(&mut self) {
1047        if !matches!(self.lifecycle_state, LifecycleState::Draining) {
1048            return;
1049        }
1050        let Some(deadline) = self.drain_deadline else {
1051            return;
1052        };
1053        if Instant::now() < deadline {
1054            return;
1055        }
1056
1057        log::info!("driver drain deadline reached; tearing down remaining links");
1058        self.lifecycle_state = LifecycleState::Stopping;
1059        let resource_actions = self.link_manager.cancel_all_resources(&mut self.rng);
1060        self.dispatch_link_actions(resource_actions);
1061        let link_actions = self.link_manager.teardown_all_links();
1062        self.dispatch_link_actions(link_actions);
1063        let cleanup_actions = self.link_manager.tick(&mut self.rng);
1064        self.dispatch_link_actions(cleanup_actions);
1065        self.holepunch_manager.abort_all_sessions();
1066    }
1067
1068    fn enforce_known_destination_cap(&mut self, for_insert: bool) -> usize {
1069        if self.known_destinations_max_entries == usize::MAX {
1070            return 0;
1071        }
1072
1073        let mut evicted = 0usize;
1074        while if for_insert {
1075            self.known_destinations.len() >= self.known_destinations_max_entries
1076        } else {
1077            self.known_destinations.len() > self.known_destinations_max_entries
1078        } {
1079            let active_dests = self.engine.active_destination_hashes();
1080            let candidate = self
1081                .oldest_known_destination(false, &active_dests)
1082                .or_else(|| self.oldest_known_destination(true, &active_dests));
1083            let Some(dest_hash) = candidate else {
1084                break;
1085            };
1086            if self.known_destinations.remove(&dest_hash).is_some() {
1087                evicted += 1;
1088                self.known_destinations_cap_evict_count += 1;
1089            } else {
1090                break;
1091            }
1092        }
1093        evicted
1094    }
1095
1096    fn oldest_known_destination(
1097        &self,
1098        include_protected: bool,
1099        active_dests: &std::collections::BTreeSet<[u8; 16]>,
1100    ) -> Option<[u8; 16]> {
1101        self.known_destinations
1102            .iter()
1103            .filter(|(dest_hash, state)| {
1104                include_protected
1105                    || (!active_dests.contains(*dest_hash)
1106                        && !self.local_destinations.contains_key(*dest_hash)
1107                        && !state.retained)
1108            })
1109            .min_by(|a, b| {
1110                Self::known_destination_relevance_time(a.1)
1111                    .partial_cmp(&Self::known_destination_relevance_time(b.1))
1112                    .unwrap_or(std::cmp::Ordering::Equal)
1113                    .then_with(|| a.0.cmp(b.0))
1114            })
1115            .map(|(dest_hash, _)| *dest_hash)
1116    }
1117
1118    #[cfg(feature = "rns-hooks")]
1119    fn provider_events_enabled(&self) -> bool {
1120        self.provider_bridge.is_some()
1121    }
1122
1123    #[cfg(feature = "rns-hooks")]
1124    fn run_backbone_peer_hook(
1125        &mut self,
1126        attach_point: &str,
1127        point: HookPoint,
1128        event: &BackbonePeerHookEvent,
1129    ) {
1130        let ctx = backbone_peer_hook_context(event);
1131        let now = time::now();
1132        let engine_ref = EngineRef {
1133            engine: &self.engine,
1134            interfaces: &self.interfaces,
1135            link_manager: &self.link_manager,
1136            now,
1137        };
1138        let provider_events_enabled = self.provider_events_enabled();
1139        if let Some(ref e) = run_hook_inner(
1140            &mut self.hook_slots[point as usize].programs,
1141            &self.hook_manager,
1142            &engine_ref,
1143            &ctx,
1144            now,
1145            provider_events_enabled,
1146        ) {
1147            self.forward_hook_side_effects(attach_point, e);
1148        }
1149    }
1150
1151    #[cfg(feature = "iface-backbone")]
1152    fn make_discoverable_interface(
1153        runtime: &BackboneDiscoveryRuntimeHandle,
1154    ) -> crate::discovery::DiscoverableInterface {
1155        crate::discovery::DiscoverableInterface {
1156            interface_name: runtime.interface_name.clone(),
1157            config: runtime.current.config.clone(),
1158            transport_enabled: runtime.current.transport_enabled,
1159            ifac_netname: runtime.current.ifac_netname.clone(),
1160            ifac_netkey: runtime.current.ifac_netkey.clone(),
1161        }
1162    }
1163
1164    #[cfg(feature = "iface-backbone")]
1165    fn sync_backbone_discovery_runtime(
1166        &mut self,
1167        interface_name: &str,
1168    ) -> Result<(), RuntimeConfigError> {
1169        let handle = self
1170            .backbone_discovery_runtime
1171            .get(interface_name)
1172            .ok_or(RuntimeConfigError {
1173                code: RuntimeConfigErrorCode::NotFound,
1174                message: format!("backbone interface '{}' not found", interface_name),
1175            })?
1176            .clone();
1177
1178        if handle.current.discoverable {
1179            let iface = Self::make_discoverable_interface(&handle);
1180            if let Some(announcer) = self.interface_announcer.as_mut() {
1181                announcer.upsert_interface(iface);
1182            } else if let Some(identity) = self.transport_identity.as_ref() {
1183                self.interface_announcer = Some(crate::discovery::InterfaceAnnouncer::new(
1184                    *identity.hash(),
1185                    vec![iface],
1186                ));
1187            }
1188        } else if let Some(announcer) = self.interface_announcer.as_mut() {
1189            announcer.remove_interface(interface_name);
1190            if announcer.is_empty() {
1191                self.interface_announcer = None;
1192            }
1193        }
1194
1195        Ok(())
1196    }
1197
1198    #[cfg(feature = "iface-tcp")]
1199    fn make_tcp_server_discoverable_interface(
1200        runtime: &TcpServerDiscoveryRuntimeHandle,
1201    ) -> crate::discovery::DiscoverableInterface {
1202        crate::discovery::DiscoverableInterface {
1203            interface_name: runtime.interface_name.clone(),
1204            config: runtime.current.config.clone(),
1205            transport_enabled: runtime.current.transport_enabled,
1206            ifac_netname: runtime.current.ifac_netname.clone(),
1207            ifac_netkey: runtime.current.ifac_netkey.clone(),
1208        }
1209    }
1210
1211    #[cfg(feature = "iface-tcp")]
1212    fn sync_tcp_server_discovery_runtime(
1213        &mut self,
1214        interface_name: &str,
1215    ) -> Result<(), RuntimeConfigError> {
1216        let handle = self
1217            .tcp_server_discovery_runtime
1218            .get(interface_name)
1219            .ok_or(RuntimeConfigError {
1220                code: RuntimeConfigErrorCode::NotFound,
1221                message: format!("tcp server interface '{}' not found", interface_name),
1222            })?
1223            .clone();
1224
1225        if handle.current.discoverable {
1226            let iface = Self::make_tcp_server_discoverable_interface(&handle);
1227            if let Some(announcer) = self.interface_announcer.as_mut() {
1228                announcer.upsert_interface(iface);
1229            } else if let Some(identity) = self.transport_identity.as_ref() {
1230                self.interface_announcer = Some(crate::discovery::InterfaceAnnouncer::new(
1231                    *identity.hash(),
1232                    vec![iface],
1233                ));
1234            }
1235        } else if let Some(announcer) = self.interface_announcer.as_mut() {
1236            announcer.remove_interface(interface_name);
1237            if announcer.is_empty() {
1238                self.interface_announcer = None;
1239            }
1240        }
1241
1242        Ok(())
1243    }
1244
1245    #[cfg(feature = "rns-hooks")]
1246    fn update_hook_program<F>(
1247        &mut self,
1248        name: &str,
1249        attach_point: &str,
1250        mut update: F,
1251    ) -> Result<(), String>
1252    where
1253        F: FnMut(&mut rns_hooks::LoadedProgram),
1254    {
1255        let point_idx = crate::config::parse_hook_point(attach_point)
1256            .ok_or_else(|| format!("unknown hook point '{}'", attach_point))?;
1257        let program = self.hook_slots[point_idx]
1258            .programs
1259            .iter_mut()
1260            .find(|program| program.name == name)
1261            .ok_or_else(|| format!("hook '{}' not found at point '{}'", name, attach_point))?;
1262        update(program);
1263        Ok(())
1264    }
1265
1266    pub(crate) fn set_tick_interval_handle(&mut self, tick_interval_ms: Arc<AtomicU64>) {
1267        self.tick_interval_ms = tick_interval_ms;
1268    }
1269
1270    pub(crate) fn set_packet_hashlist_max_entries(&mut self, max_entries: usize) {
1271        self.engine.set_packet_hashlist_max_entries(max_entries);
1272    }
1273
1274    fn build_shared_announce_raw(
1275        &mut self,
1276        dest_hash: &[u8; 16],
1277        record: &SharedAnnounceRecord,
1278        path_response: bool,
1279    ) -> Option<Vec<u8>> {
1280        let identity = rns_crypto::identity::Identity::from_private_key(&record.identity_prv_key);
1281
1282        let mut random_hash = [0u8; 10];
1283        self.rng.fill_bytes(&mut random_hash[..5]);
1284        let now_secs = std::time::SystemTime::now()
1285            .duration_since(std::time::UNIX_EPOCH)
1286            .ok()?
1287            .as_secs();
1288        random_hash[5..10].copy_from_slice(&now_secs.to_be_bytes()[3..8]);
1289
1290        let (announce_data, _has_ratchet) = rns_core::announce::AnnounceData::pack(
1291            &identity,
1292            dest_hash,
1293            &record.name_hash,
1294            &random_hash,
1295            None,
1296            record.app_data.as_deref(),
1297        )
1298        .ok()?;
1299
1300        let flags = rns_core::packet::PacketFlags {
1301            header_type: rns_core::constants::HEADER_1,
1302            context_flag: rns_core::constants::FLAG_UNSET,
1303            transport_type: rns_core::constants::TRANSPORT_BROADCAST,
1304            destination_type: rns_core::constants::DESTINATION_SINGLE,
1305            packet_type: rns_core::constants::PACKET_TYPE_ANNOUNCE,
1306        };
1307        let context = if path_response {
1308            rns_core::constants::CONTEXT_PATH_RESPONSE
1309        } else {
1310            rns_core::constants::CONTEXT_NONE
1311        };
1312
1313        rns_core::packet::RawPacket::pack(flags, 0, dest_hash, None, context, &announce_data)
1314            .ok()
1315            .map(|packet| packet.raw)
1316    }
1317
1318    fn replay_shared_announces(&mut self) {
1319        let records: Vec<([u8; 16], SharedAnnounceRecord)> = self
1320            .shared_announces
1321            .iter()
1322            .map(|(dest_hash, record)| (*dest_hash, record.clone()))
1323            .collect();
1324        for (dest_hash, record) in records {
1325            if let Some(raw) = self.build_shared_announce_raw(&dest_hash, &record, true) {
1326                let event = Event::SendOutbound {
1327                    raw,
1328                    dest_type: rns_core::constants::DESTINATION_SINGLE,
1329                    attached_interface: None,
1330                };
1331                match event {
1332                    Event::SendOutbound {
1333                        raw,
1334                        dest_type,
1335                        attached_interface,
1336                    } => match RawPacket::unpack(&raw) {
1337                        Ok(packet) => {
1338                            let actions = self.engine.handle_outbound(
1339                                &packet,
1340                                dest_type,
1341                                attached_interface,
1342                                time::now(),
1343                            );
1344                            self.dispatch_all(actions);
1345                        }
1346                        Err(e) => {
1347                            log::warn!(
1348                                "Shared announce replay failed for {:02x?}: {:?}",
1349                                &dest_hash[..4],
1350                                e
1351                            );
1352                        }
1353                    },
1354                    _ => unreachable!(),
1355                }
1356            }
1357        }
1358    }
1359
1360    fn handle_shared_interface_down(&mut self, id: InterfaceId) {
1361        let dropped_paths = self.engine.drop_paths_for_interface(id);
1362        let dropped_reverse = self.engine.drop_reverse_for_interface(id);
1363        let dropped_links = self.engine.drop_links_for_interface(id);
1364        self.engine.drop_announce_queues();
1365        let link_actions = self.link_manager.teardown_all_links();
1366        self.dispatch_link_actions(link_actions);
1367        self.shared_reconnect_pending.insert(id, true);
1368        log::info!(
1369            "[{}] cleared shared state: {} paths, {} reverse entries, {} transport links",
1370            id.0,
1371            dropped_paths,
1372            dropped_reverse,
1373            dropped_links
1374        );
1375    }
1376
1377    #[cfg(feature = "iface-backbone")]
1378    pub(crate) fn register_backbone_runtime(&mut self, handle: BackboneRuntimeConfigHandle) {
1379        self.backbone_runtime
1380            .insert(handle.interface_name.clone(), handle);
1381    }
1382
1383    #[cfg(feature = "iface-backbone")]
1384    pub(crate) fn register_backbone_peer_state(&mut self, handle: BackbonePeerStateHandle) {
1385        self.backbone_peer_state
1386            .insert(handle.interface_name.clone(), handle);
1387    }
1388
1389    #[cfg(feature = "iface-backbone")]
1390    pub(crate) fn register_backbone_client_runtime(
1391        &mut self,
1392        handle: BackboneClientRuntimeConfigHandle,
1393    ) {
1394        self.backbone_client_runtime
1395            .insert(handle.interface_name.clone(), handle);
1396    }
1397
1398    #[cfg(feature = "iface-backbone")]
1399    pub(crate) fn register_backbone_discovery_runtime(
1400        &mut self,
1401        handle: BackboneDiscoveryRuntimeHandle,
1402    ) {
1403        self.backbone_discovery_runtime
1404            .insert(handle.interface_name.clone(), handle);
1405    }
1406
1407    #[cfg(feature = "iface-backbone")]
1408    pub(crate) fn configure_backbone_peer_pool(
1409        &mut self,
1410        settings: BackbonePeerPoolSettings,
1411        candidates: Vec<BackbonePeerPoolCandidateConfig>,
1412    ) {
1413        if settings.max_connected == 0 || candidates.is_empty() {
1414            self.backbone_peer_pool = None;
1415            return;
1416        }
1417        self.backbone_peer_pool = Some(BackbonePeerPool {
1418            settings,
1419            candidates: candidates
1420                .into_iter()
1421                .map(|config| BackbonePeerPoolCandidate {
1422                    config,
1423                    active_id: None,
1424                    failures: Vec::new(),
1425                    retry_after: None,
1426                    cooldown_until: None,
1427                    last_error: None,
1428                })
1429                .collect(),
1430        });
1431        self.maintain_backbone_peer_pool();
1432    }
1433
1434    #[cfg(feature = "iface-backbone")]
1435    fn maintain_backbone_peer_pool(&mut self) {
1436        let Some(pool) = self.backbone_peer_pool.as_mut() else {
1437            return;
1438        };
1439        let now = time::now();
1440        for candidate in &mut pool.candidates {
1441            if candidate.cooldown_until.is_some_and(|until| until <= now) {
1442                candidate.cooldown_until = None;
1443                candidate.retry_after = None;
1444            }
1445        }
1446
1447        loop {
1448            let Some(pool) = self.backbone_peer_pool.as_ref() else {
1449                return;
1450            };
1451            let active = pool
1452                .candidates
1453                .iter()
1454                .filter(|candidate| candidate.active_id.is_some())
1455                .count();
1456            if active >= pool.settings.max_connected {
1457                return;
1458            }
1459            let next = pool.candidates.iter().position(|candidate| {
1460                candidate.active_id.is_none()
1461                    && candidate
1462                        .cooldown_until
1463                        .map(|until| until <= now)
1464                        .unwrap_or(true)
1465                    && candidate
1466                        .retry_after
1467                        .map(|retry_after| retry_after <= now)
1468                        .unwrap_or(true)
1469            });
1470            let Some(index) = next else {
1471                return;
1472            };
1473            if let Err(err) = self.start_backbone_peer_pool_candidate(index) {
1474                self.record_backbone_peer_pool_failure(index, err.to_string());
1475            }
1476        }
1477    }
1478
1479    #[cfg(feature = "iface-backbone")]
1480    fn start_backbone_peer_pool_candidate(&mut self, index: usize) -> std::io::Result<()> {
1481        let Some(pool) = self.backbone_peer_pool.as_ref() else {
1482            return Ok(());
1483        };
1484        let Some(candidate) = pool.candidates.get(index) else {
1485            return Ok(());
1486        };
1487        let mut client = candidate.config.client.clone();
1488        client.max_reconnect_tries = Some(0);
1489        if let Ok(mut runtime) = client.runtime.lock() {
1490            runtime.max_reconnect_tries = Some(0);
1491        }
1492        let id = client.interface_id;
1493        let name = client.name.clone();
1494        let mode = candidate.config.mode;
1495        let ingress_control = candidate.config.ingress_control;
1496        let ifac_runtime = candidate.config.ifac_runtime.clone();
1497        let ifac_enabled = candidate.config.ifac_enabled;
1498        let interface_type_name = candidate.config.interface_type_name.clone();
1499        let writer = start_client(client.clone(), self.event_tx.clone())?;
1500        let info = rns_core::transport::types::InterfaceInfo {
1501            id,
1502            name: name.clone(),
1503            mode,
1504            out_capable: true,
1505            in_capable: true,
1506            bitrate: Some(1_000_000_000),
1507            announce_rate_target: None,
1508            announce_rate_grace: 0,
1509            announce_rate_penalty: 0.0,
1510            announce_cap: rns_core::constants::ANNOUNCE_CAP,
1511            is_local_client: false,
1512            wants_tunnel: false,
1513            tunnel_id: None,
1514            mtu: 65535,
1515            ingress_control,
1516            ia_freq: 0.0,
1517            started: time::now(),
1518        };
1519        let (writer, async_writer_metrics) = self.wrap_interface_writer(id, &name, writer);
1520        let ifac_state = if ifac_enabled {
1521            Some(ifac::derive_ifac(
1522                ifac_runtime.netname.as_deref(),
1523                ifac_runtime.netkey.as_deref(),
1524                ifac_runtime.size,
1525            ))
1526        } else {
1527            None
1528        };
1529        self.register_backbone_client_runtime(BackboneClientRuntimeConfigHandle {
1530            interface_name: name.clone(),
1531            runtime: Arc::clone(&client.runtime),
1532            startup: BackboneClientRuntime::from_config(&client),
1533        });
1534        self.register_interface_runtime_defaults(&info);
1535        self.register_interface_ifac_runtime(&name, ifac_runtime);
1536        self.engine.register_interface(info.clone());
1537        self.interfaces.insert(
1538            id,
1539            InterfaceEntry {
1540                id,
1541                info,
1542                writer,
1543                async_writer_metrics: Some(async_writer_metrics),
1544                enabled: true,
1545                online: false,
1546                dynamic: false,
1547                ifac: ifac_state,
1548                stats: InterfaceStats {
1549                    started: time::now(),
1550                    ..Default::default()
1551                },
1552                interface_type: interface_type_name,
1553                send_retry_at: None,
1554                send_retry_backoff: Duration::ZERO,
1555            },
1556        );
1557
1558        if let Some(pool) = self.backbone_peer_pool.as_mut() {
1559            if let Some(candidate) = pool.candidates.get_mut(index) {
1560                candidate.active_id = Some(id);
1561                candidate.retry_after = None;
1562                candidate.last_error = None;
1563            }
1564        }
1565        Ok(())
1566    }
1567
1568    #[cfg(feature = "iface-backbone")]
1569    fn record_backbone_peer_pool_failure(&mut self, index: usize, error: String) {
1570        let Some(pool) = self.backbone_peer_pool.as_mut() else {
1571            return;
1572        };
1573        let Some(candidate) = pool.candidates.get_mut(index) else {
1574            return;
1575        };
1576        let now = time::now();
1577        let window = pool.settings.failure_window.as_secs_f64();
1578        candidate.failures.retain(|ts| now - *ts <= window);
1579        candidate.failures.push(now);
1580        candidate.last_error = Some(error);
1581        candidate.active_id = None;
1582        if candidate.failures.len() >= pool.settings.failure_threshold {
1583            candidate.cooldown_until = Some(now + pool.settings.cooldown.as_secs_f64());
1584            candidate.retry_after = None;
1585        } else {
1586            let reconnect_wait = candidate
1587                .config
1588                .client
1589                .runtime
1590                .lock()
1591                .map(|runtime| runtime.reconnect_wait)
1592                .unwrap_or(candidate.config.client.reconnect_wait);
1593            candidate.retry_after = Some(now + reconnect_wait.as_secs_f64());
1594        }
1595    }
1596
1597    #[cfg(feature = "iface-backbone")]
1598    fn handle_backbone_peer_pool_down(&mut self, id: InterfaceId) {
1599        let Some(index) = self.backbone_peer_pool.as_ref().and_then(|pool| {
1600            pool.candidates
1601                .iter()
1602                .position(|candidate| candidate.active_id == Some(id))
1603        }) else {
1604            return;
1605        };
1606
1607        if let Some(entry) = self.interfaces.remove(&id) {
1608            let name = entry.info.name;
1609            self.interface_runtime_defaults.remove(&name);
1610            self.interface_ifac_runtime.remove(&name);
1611            self.interface_ifac_runtime_defaults.remove(&name);
1612            self.backbone_client_runtime.remove(&name);
1613            self.engine.deregister_interface(id);
1614        }
1615        self.record_backbone_peer_pool_failure(index, "interface down".into());
1616        self.maintain_backbone_peer_pool();
1617    }
1618
1619    #[cfg(feature = "iface-backbone")]
1620    fn backbone_peer_pool_status(&self) -> Option<BackbonePeerPoolStatus> {
1621        let pool = self.backbone_peer_pool.as_ref()?;
1622        let now = time::now();
1623        let mut active_count = 0usize;
1624        let mut standby_count = 0usize;
1625        let mut cooldown_count = 0usize;
1626        let members = pool
1627            .candidates
1628            .iter()
1629            .map(|candidate| {
1630                let (state, cooldown_remaining_seconds) =
1631                    if let Some(until) = candidate.cooldown_until {
1632                        cooldown_count += 1;
1633                        ("cooldown".to_string(), Some((until - now).max(0.0)))
1634                    } else if let Some(id) = candidate.active_id {
1635                        active_count += 1;
1636                        let online = self
1637                            .interfaces
1638                            .get(&id)
1639                            .map(|entry| entry.online)
1640                            .unwrap_or(false);
1641                        (
1642                            if online { "active" } else { "connecting" }.to_string(),
1643                            None,
1644                        )
1645                    } else {
1646                        standby_count += 1;
1647                        ("standby".to_string(), None)
1648                    };
1649                BackbonePeerPoolMemberStatus {
1650                    name: candidate.config.client.name.clone(),
1651                    remote: format!(
1652                        "{}:{}",
1653                        candidate.config.client.target_host, candidate.config.client.target_port
1654                    ),
1655                    state,
1656                    interface_id: candidate.active_id.map(|id| id.0),
1657                    failure_count: candidate.failures.len(),
1658                    last_error: candidate.last_error.clone(),
1659                    cooldown_remaining_seconds,
1660                }
1661            })
1662            .collect();
1663        Some(BackbonePeerPoolStatus {
1664            max_connected: pool.settings.max_connected,
1665            active_count,
1666            standby_count,
1667            cooldown_count,
1668            members,
1669        })
1670    }
1671
1672    #[cfg(feature = "iface-backbone")]
1673    fn list_backbone_peer_state(
1674        &self,
1675        interface_name: Option<&str>,
1676    ) -> Vec<BackbonePeerStateEntry> {
1677        let mut names: Vec<&String> = match interface_name {
1678            Some(name) => self
1679                .backbone_peer_state
1680                .keys()
1681                .filter(|candidate| candidate.as_str() == name)
1682                .collect(),
1683            None => self.backbone_peer_state.keys().collect(),
1684        };
1685        names.sort();
1686
1687        let mut entries = Vec::new();
1688        for name in names {
1689            if let Some(handle) = self.backbone_peer_state.get(name) {
1690                entries.extend(
1691                    recover_mutex_guard(&handle.peer_state, "backbone peer state").list(name),
1692                );
1693            }
1694        }
1695        entries.sort_by(|a, b| {
1696            a.interface_name
1697                .cmp(&b.interface_name)
1698                .then_with(|| a.peer_ip.cmp(&b.peer_ip))
1699        });
1700        entries
1701    }
1702
1703    #[cfg(feature = "iface-backbone")]
1704    fn list_backbone_interfaces(&self) -> Vec<crate::event::BackboneInterfaceEntry> {
1705        let mut entries: Vec<_> = self
1706            .backbone_peer_state
1707            .values()
1708            .map(|handle| crate::event::BackboneInterfaceEntry {
1709                interface_id: handle.interface_id,
1710                interface_name: handle.interface_name.clone(),
1711            })
1712            .collect();
1713        entries.sort_by(|a, b| a.interface_name.cmp(&b.interface_name));
1714        entries
1715    }
1716
1717    #[cfg(feature = "iface-backbone")]
1718    fn clear_backbone_peer_state(
1719        &mut self,
1720        interface_name: &str,
1721        peer_ip: std::net::IpAddr,
1722    ) -> bool {
1723        self.backbone_peer_state
1724            .get(interface_name)
1725            .map(|handle| {
1726                recover_mutex_guard(&handle.peer_state, "backbone peer state").clear(peer_ip)
1727            })
1728            .unwrap_or(false)
1729    }
1730
1731    fn blacklist_backbone_peer(
1732        &mut self,
1733        interface_name: &str,
1734        peer_ip: std::net::IpAddr,
1735        duration: std::time::Duration,
1736        reason: String,
1737        penalty_level: u8,
1738    ) -> bool {
1739        let capped_duration = self
1740            .backbone_runtime
1741            .get(interface_name)
1742            .and_then(|handle| {
1743                handle
1744                    .runtime
1745                    .lock()
1746                    .ok()
1747                    .map(|runtime| runtime.abuse.max_penalty_duration)
1748            })
1749            .flatten()
1750            .map(|max| duration.min(max))
1751            .unwrap_or(duration);
1752        let Some(handle) = self.backbone_peer_state.get(interface_name) else {
1753            return false;
1754        };
1755        let ok = recover_mutex_guard(&handle.peer_state, "backbone peer state").blacklist(
1756            peer_ip,
1757            capped_duration,
1758            reason,
1759        );
1760        if ok {
1761            #[cfg(feature = "rns-hooks")]
1762            self.run_backbone_peer_hook(
1763                "BackbonePeerPenalty",
1764                HookPoint::BackbonePeerPenalty,
1765                &BackbonePeerHookEvent {
1766                    server_interface_id: self
1767                        .interfaces
1768                        .iter()
1769                        .find(|(_, entry)| entry.info.name == interface_name)
1770                        .map(|(id, _)| *id)
1771                        .unwrap_or(InterfaceId(0)),
1772                    peer_interface_id: None,
1773                    peer_ip,
1774                    peer_port: 0,
1775                    connected_for: Duration::ZERO,
1776                    had_received_data: false,
1777                    penalty_level,
1778                    blacklist_for: capped_duration,
1779                },
1780            );
1781            #[cfg(not(feature = "rns-hooks"))]
1782            let _ = (peer_ip, capped_duration, penalty_level);
1783        }
1784        ok
1785    }
1786
1787    #[cfg(feature = "iface-tcp")]
1788    pub(crate) fn register_tcp_server_runtime(&mut self, handle: TcpServerRuntimeConfigHandle) {
1789        self.tcp_server_runtime
1790            .insert(handle.interface_name.clone(), handle);
1791    }
1792
1793    #[cfg(feature = "iface-tcp")]
1794    pub(crate) fn register_tcp_client_runtime(&mut self, handle: TcpClientRuntimeConfigHandle) {
1795        self.tcp_client_runtime
1796            .insert(handle.interface_name.clone(), handle);
1797    }
1798
1799    #[cfg(feature = "iface-tcp")]
1800    pub(crate) fn register_tcp_server_discovery_runtime(
1801        &mut self,
1802        handle: TcpServerDiscoveryRuntimeHandle,
1803    ) {
1804        self.tcp_server_discovery_runtime
1805            .insert(handle.interface_name.clone(), handle);
1806    }
1807
1808    #[cfg(feature = "iface-udp")]
1809    pub(crate) fn register_udp_runtime(&mut self, handle: UdpRuntimeConfigHandle) {
1810        self.udp_runtime
1811            .insert(handle.interface_name.clone(), handle);
1812    }
1813
1814    #[cfg(feature = "iface-auto")]
1815    pub(crate) fn register_auto_runtime(&mut self, handle: AutoRuntimeConfigHandle) {
1816        self.auto_runtime
1817            .insert(handle.interface_name.clone(), handle);
1818    }
1819
1820    #[cfg(feature = "iface-i2p")]
1821    pub(crate) fn register_i2p_runtime(&mut self, handle: I2pRuntimeConfigHandle) {
1822        self.i2p_runtime
1823            .insert(handle.interface_name.clone(), handle);
1824    }
1825
1826    #[cfg(feature = "iface-pipe")]
1827    pub(crate) fn register_pipe_runtime(&mut self, handle: PipeRuntimeConfigHandle) {
1828        self.pipe_runtime
1829            .insert(handle.interface_name.clone(), handle);
1830    }
1831
1832    #[cfg(feature = "iface-rnode")]
1833    pub(crate) fn register_rnode_runtime(&mut self, handle: RNodeRuntimeConfigHandle) {
1834        self.rnode_runtime
1835            .insert(handle.interface_name.clone(), handle);
1836    }
1837
1838    pub(crate) fn register_interface_runtime_defaults(
1839        &mut self,
1840        info: &rns_core::transport::types::InterfaceInfo,
1841    ) {
1842        self.interface_runtime_defaults
1843            .entry(info.name.clone())
1844            .or_insert_with(|| info.clone());
1845    }
1846
1847    pub(crate) fn register_interface_ifac_runtime(
1848        &mut self,
1849        interface_name: &str,
1850        startup: IfacRuntimeConfig,
1851    ) {
1852        self.interface_ifac_runtime_defaults
1853            .entry(interface_name.to_string())
1854            .or_insert_with(|| startup.clone());
1855        self.interface_ifac_runtime
1856            .entry(interface_name.to_string())
1857            .or_insert(startup);
1858    }
1859
1860    fn runtime_config_entry(&self, key: &str) -> Option<RuntimeConfigEntry> {
1861        let defaults = self.runtime_config_defaults;
1862        let make_entry = |key: &str,
1863                          value: RuntimeConfigValue,
1864                          default: RuntimeConfigValue,
1865                          apply_mode: RuntimeConfigApplyMode,
1866                          description: &str| RuntimeConfigEntry {
1867            key: key.to_string(),
1868            source: if value == default {
1869                RuntimeConfigSource::Startup
1870            } else {
1871                RuntimeConfigSource::RuntimeOverride
1872            },
1873            value,
1874            default,
1875            apply_mode,
1876            description: Some(description.to_string()),
1877        };
1878
1879        match key {
1880            "global.tick_interval_ms" => Some(make_entry(
1881                key,
1882                RuntimeConfigValue::Int(self.tick_interval_ms.load(Ordering::Relaxed) as i64),
1883                RuntimeConfigValue::Int(defaults.tick_interval_ms as i64),
1884                RuntimeConfigApplyMode::Immediate,
1885                "Driver tick interval in milliseconds.",
1886            )),
1887            "global.known_destinations_ttl_secs" => Some(make_entry(
1888                key,
1889                RuntimeConfigValue::Float(self.known_destinations_ttl),
1890                RuntimeConfigValue::Float(defaults.known_destinations_ttl),
1891                RuntimeConfigApplyMode::Immediate,
1892                "TTL for known destinations without an active path.",
1893            )),
1894            "global.rate_limiter_ttl_secs" => Some(make_entry(
1895                key,
1896                RuntimeConfigValue::Float(self.rate_limiter_ttl_secs),
1897                RuntimeConfigValue::Float(defaults.rate_limiter_ttl_secs),
1898                RuntimeConfigApplyMode::Immediate,
1899                "TTL for announce rate-limiter entries without an active path.",
1900            )),
1901            "global.known_destinations_cleanup_interval_ticks" => Some(make_entry(
1902                key,
1903                RuntimeConfigValue::Int(self.known_destinations_cleanup_interval_ticks as i64),
1904                RuntimeConfigValue::Int(defaults.known_destinations_cleanup_interval_ticks as i64),
1905                RuntimeConfigApplyMode::Immediate,
1906                "Tick interval between known-destinations cleanup passes.",
1907            )),
1908            "global.announce_cache_cleanup_interval_ticks" => Some(make_entry(
1909                key,
1910                RuntimeConfigValue::Int(self.announce_cache_cleanup_interval_ticks as i64),
1911                RuntimeConfigValue::Int(defaults.announce_cache_cleanup_interval_ticks as i64),
1912                RuntimeConfigApplyMode::Immediate,
1913                "Tick interval between announce-cache cleanup cycles.",
1914            )),
1915            "global.announce_cache_cleanup_batch_size" => Some(make_entry(
1916                key,
1917                RuntimeConfigValue::Int(self.announce_cache_cleanup_batch_size as i64),
1918                RuntimeConfigValue::Int(defaults.announce_cache_cleanup_batch_size as i64),
1919                RuntimeConfigApplyMode::Immediate,
1920                "Number of announce-cache entries processed per cleanup tick.",
1921            )),
1922            "global.discovery_cleanup_interval_ticks" => Some(make_entry(
1923                key,
1924                RuntimeConfigValue::Int(self.discovery_cleanup_interval_ticks as i64),
1925                RuntimeConfigValue::Int(defaults.discovery_cleanup_interval_ticks as i64),
1926                RuntimeConfigApplyMode::Immediate,
1927                "Tick interval between discovered-interface cleanup passes.",
1928            )),
1929            "global.management_announce_interval_secs" => Some(make_entry(
1930                key,
1931                RuntimeConfigValue::Float(self.management_announce_interval_secs),
1932                RuntimeConfigValue::Float(defaults.management_announce_interval_secs),
1933                RuntimeConfigApplyMode::Immediate,
1934                "Interval between management announces in seconds.",
1935            )),
1936            "global.direct_connect_policy" => Some(make_entry(
1937                key,
1938                RuntimeConfigValue::String(Self::holepunch_policy_name(
1939                    self.holepunch_manager.policy(),
1940                )),
1941                RuntimeConfigValue::String(Self::holepunch_policy_name(
1942                    defaults.direct_connect_policy,
1943                )),
1944                RuntimeConfigApplyMode::Immediate,
1945                "Policy for incoming direct-connect proposals.",
1946            )),
1947            #[cfg(feature = "rns-hooks")]
1948            "provider.queue_max_events" => {
1949                let value = self
1950                    .provider_bridge
1951                    .as_ref()
1952                    .map(|b| b.queue_max_events())
1953                    .unwrap_or(defaults.provider_queue_max_events);
1954                Some(make_entry(
1955                    key,
1956                    RuntimeConfigValue::Int(value as i64),
1957                    RuntimeConfigValue::Int(defaults.provider_queue_max_events as i64),
1958                    RuntimeConfigApplyMode::Immediate,
1959                    "Max queued events in the provider bridge.",
1960                ))
1961            }
1962            #[cfg(feature = "rns-hooks")]
1963            "provider.queue_max_bytes" => {
1964                let value = self
1965                    .provider_bridge
1966                    .as_ref()
1967                    .map(|b| b.queue_max_bytes())
1968                    .unwrap_or(defaults.provider_queue_max_bytes);
1969                Some(make_entry(
1970                    key,
1971                    RuntimeConfigValue::Int(value as i64),
1972                    RuntimeConfigValue::Int(defaults.provider_queue_max_bytes as i64),
1973                    RuntimeConfigApplyMode::Immediate,
1974                    "Max queued bytes in the provider bridge.",
1975                ))
1976            }
1977            _ => {
1978                #[cfg(feature = "iface-backbone")]
1979                if let Some(entry) = self.backbone_runtime_entry(key) {
1980                    return Some(entry);
1981                }
1982                #[cfg(feature = "iface-backbone")]
1983                if let Some(entry) = self.backbone_client_runtime_entry(key) {
1984                    return Some(entry);
1985                }
1986                #[cfg(feature = "iface-tcp")]
1987                if let Some(entry) = self.tcp_server_runtime_entry(key) {
1988                    return Some(entry);
1989                }
1990                #[cfg(feature = "iface-tcp")]
1991                if let Some(entry) = self.tcp_client_runtime_entry(key) {
1992                    return Some(entry);
1993                }
1994                #[cfg(feature = "iface-udp")]
1995                if let Some(entry) = self.udp_runtime_entry(key) {
1996                    return Some(entry);
1997                }
1998                #[cfg(feature = "iface-auto")]
1999                if let Some(entry) = self.auto_runtime_entry(key) {
2000                    return Some(entry);
2001                }
2002                #[cfg(feature = "iface-i2p")]
2003                if let Some(entry) = self.i2p_runtime_entry(key) {
2004                    return Some(entry);
2005                }
2006                #[cfg(feature = "iface-pipe")]
2007                if let Some(entry) = self.pipe_runtime_entry(key) {
2008                    return Some(entry);
2009                }
2010                #[cfg(feature = "iface-rnode")]
2011                if let Some(entry) = self.rnode_runtime_entry(key) {
2012                    return Some(entry);
2013                }
2014                if let Some(entry) = self.generic_interface_runtime_entry(key) {
2015                    return Some(entry);
2016                }
2017                None
2018            }
2019        }
2020    }
2021
2022    fn list_runtime_config(&self) -> Vec<RuntimeConfigEntry> {
2023        let mut entries: Vec<RuntimeConfigEntry> = [
2024            "global.tick_interval_ms",
2025            "global.known_destinations_ttl_secs",
2026            "global.rate_limiter_ttl_secs",
2027            "global.known_destinations_cleanup_interval_ticks",
2028            "global.announce_cache_cleanup_interval_ticks",
2029            "global.announce_cache_cleanup_batch_size",
2030            "global.discovery_cleanup_interval_ticks",
2031            "global.management_announce_interval_secs",
2032            "global.direct_connect_policy",
2033        ]
2034        .into_iter()
2035        .filter_map(|key| self.runtime_config_entry(key))
2036        .collect();
2037
2038        #[cfg(feature = "rns-hooks")]
2039        {
2040            entries.extend(
2041                ["provider.queue_max_events", "provider.queue_max_bytes"]
2042                    .into_iter()
2043                    .filter_map(|key| self.runtime_config_entry(key)),
2044            );
2045        }
2046        #[cfg(feature = "iface-backbone")]
2047        {
2048            entries.extend(self.list_backbone_runtime_config());
2049            entries.extend(self.list_backbone_client_runtime_config());
2050        }
2051        #[cfg(feature = "iface-tcp")]
2052        {
2053            entries.extend(self.list_tcp_server_runtime_config());
2054            entries.extend(self.list_tcp_client_runtime_config());
2055        }
2056        #[cfg(feature = "iface-udp")]
2057        {
2058            entries.extend(self.list_udp_runtime_config());
2059        }
2060        #[cfg(feature = "iface-auto")]
2061        {
2062            entries.extend(self.list_auto_runtime_config());
2063        }
2064        #[cfg(feature = "iface-i2p")]
2065        {
2066            entries.extend(self.list_i2p_runtime_config());
2067        }
2068        #[cfg(feature = "iface-pipe")]
2069        {
2070            entries.extend(self.list_pipe_runtime_config());
2071        }
2072        #[cfg(feature = "iface-rnode")]
2073        {
2074            entries.extend(self.list_rnode_runtime_config());
2075        }
2076        entries.extend(self.list_generic_interface_runtime_config());
2077
2078        entries
2079    }
2080
2081    fn holepunch_policy_name(policy: crate::event::HolePunchPolicy) -> String {
2082        match policy {
2083            crate::event::HolePunchPolicy::Reject => "reject".to_string(),
2084            crate::event::HolePunchPolicy::AcceptAll => "accept_all".to_string(),
2085            crate::event::HolePunchPolicy::AskApp => "ask_app".to_string(),
2086        }
2087    }
2088
2089    fn parse_holepunch_policy(value: &RuntimeConfigValue) -> Option<crate::event::HolePunchPolicy> {
2090        match value {
2091            RuntimeConfigValue::String(s) => match s.to_ascii_lowercase().as_str() {
2092                "reject" => Some(crate::event::HolePunchPolicy::Reject),
2093                "accept_all" | "acceptall" => Some(crate::event::HolePunchPolicy::AcceptAll),
2094                "ask_app" | "askapp" => Some(crate::event::HolePunchPolicy::AskApp),
2095                _ => None,
2096            },
2097            _ => None,
2098        }
2099    }
2100
2101    fn expect_u64(value: RuntimeConfigValue, key: &str) -> Result<u64, RuntimeConfigError> {
2102        match value {
2103            RuntimeConfigValue::Int(v) if v >= 0 => Ok(v as u64),
2104            RuntimeConfigValue::Int(_) => Err(RuntimeConfigError {
2105                code: RuntimeConfigErrorCode::InvalidValue,
2106                message: format!("{} must be >= 0", key),
2107            }),
2108            _ => Err(RuntimeConfigError {
2109                code: RuntimeConfigErrorCode::InvalidType,
2110                message: format!("{} expects an integer", key),
2111            }),
2112        }
2113    }
2114
2115    fn expect_f64(value: RuntimeConfigValue, key: &str) -> Result<f64, RuntimeConfigError> {
2116        match value {
2117            RuntimeConfigValue::Float(v) if v >= 0.0 => Ok(v),
2118            RuntimeConfigValue::Int(v) if v >= 0 => Ok(v as f64),
2119            RuntimeConfigValue::Float(_) | RuntimeConfigValue::Int(_) => Err(RuntimeConfigError {
2120                code: RuntimeConfigErrorCode::InvalidValue,
2121                message: format!("{} must be >= 0", key),
2122            }),
2123            _ => Err(RuntimeConfigError {
2124                code: RuntimeConfigErrorCode::InvalidType,
2125                message: format!("{} expects a numeric value", key),
2126            }),
2127        }
2128    }
2129
2130    fn expect_i64(value: RuntimeConfigValue, key: &str) -> Result<i64, RuntimeConfigError> {
2131        match value {
2132            RuntimeConfigValue::Int(v) => Ok(v),
2133            _ => Err(RuntimeConfigError {
2134                code: RuntimeConfigErrorCode::InvalidType,
2135                message: format!("{} expects an integer", key),
2136            }),
2137        }
2138    }
2139
2140    fn expect_bool(value: RuntimeConfigValue, key: &str) -> Result<bool, RuntimeConfigError> {
2141        match value {
2142            RuntimeConfigValue::Bool(v) => Ok(v),
2143            _ => Err(RuntimeConfigError {
2144                code: RuntimeConfigErrorCode::InvalidType,
2145                message: format!("{} expects a boolean", key),
2146            }),
2147        }
2148    }
2149
2150    fn expect_string(value: RuntimeConfigValue, key: &str) -> Result<String, RuntimeConfigError> {
2151        match value {
2152            RuntimeConfigValue::String(v) => Ok(v),
2153            _ => Err(RuntimeConfigError {
2154                code: RuntimeConfigErrorCode::InvalidType,
2155                message: format!("{} expects a string", key),
2156            }),
2157        }
2158    }
2159
2160    fn expect_optional_f64(
2161        value: RuntimeConfigValue,
2162        key: &str,
2163    ) -> Result<Option<f64>, RuntimeConfigError> {
2164        match value {
2165            RuntimeConfigValue::Null => Ok(None),
2166            RuntimeConfigValue::Float(v) => Ok(Some(v)),
2167            RuntimeConfigValue::Int(v) => Ok(Some(v as f64)),
2168            _ => Err(RuntimeConfigError {
2169                code: RuntimeConfigErrorCode::InvalidType,
2170                message: format!("{} expects a numeric value or null", key),
2171            }),
2172        }
2173    }
2174
2175    fn expect_optional_string(
2176        value: RuntimeConfigValue,
2177        key: &str,
2178    ) -> Result<Option<String>, RuntimeConfigError> {
2179        match value {
2180            RuntimeConfigValue::Null => Ok(None),
2181            RuntimeConfigValue::String(v) => Ok(Some(v)),
2182            _ => Err(RuntimeConfigError {
2183                code: RuntimeConfigErrorCode::InvalidType,
2184                message: format!("{} expects a string or null", key),
2185            }),
2186        }
2187    }
2188
2189    #[cfg(feature = "iface-backbone")]
2190    fn split_backbone_runtime_key<'a>(
2191        &self,
2192        key: &'a str,
2193    ) -> Result<(&'a str, &'a str), RuntimeConfigError> {
2194        let rest = key.strip_prefix("backbone.").ok_or(RuntimeConfigError {
2195            code: RuntimeConfigErrorCode::UnknownKey,
2196            message: format!("unknown runtime-config key '{}'", key),
2197        })?;
2198        rest.split_once('.').ok_or(RuntimeConfigError {
2199            code: RuntimeConfigErrorCode::UnknownKey,
2200            message: format!("unknown runtime-config key '{}'", key),
2201        })
2202    }
2203
2204    #[cfg(feature = "iface-backbone")]
2205    fn set_optional_duration(
2206        value: RuntimeConfigValue,
2207        key: &str,
2208    ) -> Result<Option<Duration>, RuntimeConfigError> {
2209        let secs = Self::expect_f64(value, key)?;
2210        if secs == 0.0 {
2211            Ok(None)
2212        } else {
2213            Ok(Some(Duration::from_secs_f64(secs)))
2214        }
2215    }
2216
2217    #[cfg(feature = "iface-backbone")]
2218    fn set_optional_usize(
2219        value: RuntimeConfigValue,
2220        key: &str,
2221    ) -> Result<Option<usize>, RuntimeConfigError> {
2222        let raw = Self::expect_u64(value, key)?;
2223        if raw == 0 {
2224            Ok(None)
2225        } else {
2226            Ok(Some(raw as usize))
2227        }
2228    }
2229
2230    #[cfg(feature = "iface-backbone")]
2231    fn set_backbone_runtime_config(
2232        &mut self,
2233        key: &str,
2234        value: RuntimeConfigValue,
2235    ) -> Result<(), RuntimeConfigError> {
2236        let (name, setting) = self.split_backbone_runtime_key(key)?;
2237        if matches!(
2238            setting,
2239            "discoverable"
2240                | "discovery_name"
2241                | "announce_interval_secs"
2242                | "reachable_on"
2243                | "stamp_value"
2244                | "latitude"
2245                | "longitude"
2246                | "height"
2247        ) {
2248            return self.set_backbone_discovery_runtime_config(key, value);
2249        }
2250        let handle = self.backbone_runtime.get(name).ok_or(RuntimeConfigError {
2251            code: RuntimeConfigErrorCode::NotFound,
2252            message: format!("backbone interface '{}' not found", name),
2253        })?;
2254        let mut runtime = recover_mutex_guard(&handle.runtime, "backbone runtime");
2255        match setting {
2256            "idle_timeout_secs" => {
2257                runtime.idle_timeout = Self::set_optional_duration(value, key)?;
2258                Ok(())
2259            }
2260            "write_stall_timeout_secs" => {
2261                runtime.write_stall_timeout = Self::set_optional_duration(value, key)?;
2262                Ok(())
2263            }
2264            "max_penalty_duration_secs" => {
2265                runtime.abuse.max_penalty_duration = Self::set_optional_duration(value, key)?;
2266                Ok(())
2267            }
2268            "max_connections" => {
2269                runtime.max_connections = Self::set_optional_usize(value, key)?;
2270                Ok(())
2271            }
2272            _ => Err(RuntimeConfigError {
2273                code: RuntimeConfigErrorCode::UnknownKey,
2274                message: format!("unknown runtime-config key '{}'", key),
2275            }),
2276        }
2277    }
2278
2279    #[cfg(feature = "iface-backbone")]
2280    fn split_backbone_discovery_runtime_key<'a>(
2281        &self,
2282        key: &'a str,
2283    ) -> Result<(&'a str, &'a str), RuntimeConfigError> {
2284        let rest = key.strip_prefix("backbone.").ok_or(RuntimeConfigError {
2285            code: RuntimeConfigErrorCode::UnknownKey,
2286            message: format!("unknown runtime-config key '{}'", key),
2287        })?;
2288        rest.split_once('.').ok_or(RuntimeConfigError {
2289            code: RuntimeConfigErrorCode::UnknownKey,
2290            message: format!("unknown runtime-config key '{}'", key),
2291        })
2292    }
2293
2294    #[cfg(feature = "iface-backbone")]
2295    fn set_backbone_discovery_runtime_config(
2296        &mut self,
2297        key: &str,
2298        value: RuntimeConfigValue,
2299    ) -> Result<(), RuntimeConfigError> {
2300        let (name, setting) = self.split_backbone_discovery_runtime_key(key)?;
2301        let handle = self
2302            .backbone_discovery_runtime
2303            .get_mut(name)
2304            .ok_or(RuntimeConfigError {
2305                code: RuntimeConfigErrorCode::NotFound,
2306                message: format!("backbone interface '{}' not found", name),
2307            })?;
2308        match setting {
2309            "discoverable" => {
2310                handle.current.discoverable = Self::expect_bool(value, key)?;
2311            }
2312            "discovery_name" => {
2313                handle.current.config.discovery_name = Self::expect_string(value, key)?;
2314            }
2315            "announce_interval_secs" => {
2316                let secs = Self::expect_u64(value, key)?;
2317                if secs < 300 {
2318                    return Err(RuntimeConfigError {
2319                        code: RuntimeConfigErrorCode::InvalidValue,
2320                        message: format!("{} must be >= 300", key),
2321                    });
2322                }
2323                handle.current.config.announce_interval = secs;
2324            }
2325            "reachable_on" => {
2326                handle.current.config.reachable_on = Self::expect_optional_string(value, key)?;
2327            }
2328            "stamp_value" => {
2329                let raw = Self::expect_u64(value, key)?;
2330                if raw > u8::MAX as u64 {
2331                    return Err(RuntimeConfigError {
2332                        code: RuntimeConfigErrorCode::InvalidValue,
2333                        message: format!("{} must be <= {}", key, u8::MAX),
2334                    });
2335                }
2336                handle.current.config.stamp_value = raw as u8;
2337            }
2338            "latitude" => {
2339                handle.current.config.latitude = Self::expect_optional_f64(value, key)?;
2340            }
2341            "longitude" => {
2342                handle.current.config.longitude = Self::expect_optional_f64(value, key)?;
2343            }
2344            "height" => {
2345                handle.current.config.height = Self::expect_optional_f64(value, key)?;
2346            }
2347            _ => {
2348                return Err(RuntimeConfigError {
2349                    code: RuntimeConfigErrorCode::UnknownKey,
2350                    message: format!("unknown runtime-config key '{}'", key),
2351                });
2352            }
2353        }
2354        self.sync_backbone_discovery_runtime(name)
2355    }
2356
2357    #[cfg(feature = "iface-backbone")]
2358    fn reset_backbone_discovery_runtime_config(
2359        &mut self,
2360        key: &str,
2361    ) -> Result<(), RuntimeConfigError> {
2362        let (name, setting) = self.split_backbone_discovery_runtime_key(key)?;
2363        let handle = self
2364            .backbone_discovery_runtime
2365            .get_mut(name)
2366            .ok_or(RuntimeConfigError {
2367                code: RuntimeConfigErrorCode::NotFound,
2368                message: format!("backbone interface '{}' not found", name),
2369            })?;
2370        match setting {
2371            "discoverable" => handle.current.discoverable = handle.startup.discoverable,
2372            "discovery_name" => {
2373                handle.current.config.discovery_name = handle.startup.config.discovery_name.clone()
2374            }
2375            "announce_interval_secs" => {
2376                handle.current.config.announce_interval = handle.startup.config.announce_interval
2377            }
2378            "reachable_on" => {
2379                handle.current.config.reachable_on = handle.startup.config.reachable_on.clone()
2380            }
2381            "stamp_value" => handle.current.config.stamp_value = handle.startup.config.stamp_value,
2382            "latitude" => handle.current.config.latitude = handle.startup.config.latitude,
2383            "longitude" => handle.current.config.longitude = handle.startup.config.longitude,
2384            "height" => handle.current.config.height = handle.startup.config.height,
2385            _ => {
2386                return Err(RuntimeConfigError {
2387                    code: RuntimeConfigErrorCode::UnknownKey,
2388                    message: format!("unknown runtime-config key '{}'", key),
2389                });
2390            }
2391        }
2392        self.sync_backbone_discovery_runtime(name)
2393    }
2394
2395    #[cfg(feature = "iface-backbone")]
2396    fn reset_backbone_runtime_config(&mut self, key: &str) -> Result<(), RuntimeConfigError> {
2397        let (name, setting) = self.split_backbone_runtime_key(key)?;
2398        if matches!(
2399            setting,
2400            "discoverable"
2401                | "discovery_name"
2402                | "announce_interval_secs"
2403                | "reachable_on"
2404                | "stamp_value"
2405                | "latitude"
2406                | "longitude"
2407                | "height"
2408        ) {
2409            return self.reset_backbone_discovery_runtime_config(key);
2410        }
2411        let handle = self.backbone_runtime.get(name).ok_or(RuntimeConfigError {
2412            code: RuntimeConfigErrorCode::NotFound,
2413            message: format!("backbone interface '{}' not found", name),
2414        })?;
2415        let mut runtime = recover_mutex_guard(&handle.runtime, "backbone runtime");
2416        let startup = handle.startup.clone();
2417        match setting {
2418            "idle_timeout_secs" => runtime.idle_timeout = startup.idle_timeout,
2419            "write_stall_timeout_secs" => runtime.write_stall_timeout = startup.write_stall_timeout,
2420            "max_penalty_duration_secs" => {
2421                runtime.abuse.max_penalty_duration = startup.abuse.max_penalty_duration
2422            }
2423            "max_connections" => runtime.max_connections = startup.max_connections,
2424            _ => {
2425                return Err(RuntimeConfigError {
2426                    code: RuntimeConfigErrorCode::UnknownKey,
2427                    message: format!("unknown runtime-config key '{}'", key),
2428                })
2429            }
2430        }
2431        Ok(())
2432    }
2433
2434    #[cfg(feature = "iface-backbone")]
2435    fn list_backbone_client_runtime_config(&self) -> Vec<RuntimeConfigEntry> {
2436        let mut entries = Vec::new();
2437        let mut names: Vec<&String> = self.backbone_client_runtime.keys().collect();
2438        names.sort();
2439        for name in names {
2440            for suffix in [
2441                "connect_timeout_secs",
2442                "reconnect_wait_secs",
2443                "max_reconnect_tries",
2444            ] {
2445                let key = format!("backbone_client.{}.{}", name, suffix);
2446                if let Some(entry) = self.backbone_client_runtime_entry(&key) {
2447                    entries.push(entry);
2448                }
2449            }
2450        }
2451        entries
2452    }
2453
2454    #[cfg(feature = "iface-backbone")]
2455    fn backbone_client_runtime_entry(&self, key: &str) -> Option<RuntimeConfigEntry> {
2456        let rest = key.strip_prefix("backbone_client.")?;
2457        let (name, setting) = rest.split_once('.')?;
2458        let handle = self.backbone_client_runtime.get(name)?;
2459        let current = recover_mutex_guard(&handle.runtime, "backbone client runtime").clone();
2460        let startup = handle.startup.clone();
2461        let make_entry = |value: RuntimeConfigValue,
2462                          default: RuntimeConfigValue,
2463                          description: &str|
2464         -> RuntimeConfigEntry {
2465            RuntimeConfigEntry {
2466                key: key.to_string(),
2467                source: if value == default {
2468                    RuntimeConfigSource::Startup
2469                } else {
2470                    RuntimeConfigSource::RuntimeOverride
2471                },
2472                value,
2473                default,
2474                apply_mode: RuntimeConfigApplyMode::NextReconnect,
2475                description: Some(description.to_string()),
2476            }
2477        };
2478        match setting {
2479            "connect_timeout_secs" => Some(make_entry(
2480                RuntimeConfigValue::Float(current.connect_timeout.as_secs_f64()),
2481                RuntimeConfigValue::Float(startup.connect_timeout.as_secs_f64()),
2482                "Backbone client connect timeout in seconds; applies on the next reconnect.",
2483            )),
2484            "reconnect_wait_secs" => Some(make_entry(
2485                RuntimeConfigValue::Float(current.reconnect_wait.as_secs_f64()),
2486                RuntimeConfigValue::Float(startup.reconnect_wait.as_secs_f64()),
2487                "Delay between backbone client reconnect attempts in seconds.",
2488            )),
2489            "max_reconnect_tries" => Some(make_entry(
2490                RuntimeConfigValue::Int(current.max_reconnect_tries.unwrap_or(0) as i64),
2491                RuntimeConfigValue::Int(startup.max_reconnect_tries.unwrap_or(0) as i64),
2492                "Maximum backbone client reconnect attempts; 0 disables the cap.",
2493            )),
2494            _ => None,
2495        }
2496    }
2497
2498    #[cfg(feature = "iface-backbone")]
2499    fn split_backbone_client_runtime_key<'a>(
2500        &self,
2501        key: &'a str,
2502    ) -> Result<(&'a str, &'a str), RuntimeConfigError> {
2503        let rest = key
2504            .strip_prefix("backbone_client.")
2505            .ok_or(RuntimeConfigError {
2506                code: RuntimeConfigErrorCode::UnknownKey,
2507                message: format!("unknown runtime-config key '{}'", key),
2508            })?;
2509        rest.split_once('.').ok_or(RuntimeConfigError {
2510            code: RuntimeConfigErrorCode::UnknownKey,
2511            message: format!("unknown runtime-config key '{}'", key),
2512        })
2513    }
2514
2515    #[cfg(feature = "iface-backbone")]
2516    fn set_backbone_client_runtime_config(
2517        &mut self,
2518        key: &str,
2519        value: RuntimeConfigValue,
2520    ) -> Result<(), RuntimeConfigError> {
2521        let (name, setting) = self.split_backbone_client_runtime_key(key)?;
2522        let handle = self
2523            .backbone_client_runtime
2524            .get(name)
2525            .ok_or(RuntimeConfigError {
2526                code: RuntimeConfigErrorCode::NotFound,
2527                message: format!("backbone client interface '{}' not found", name),
2528            })?;
2529        let mut runtime = recover_mutex_guard(&handle.runtime, "backbone client runtime");
2530        match setting {
2531            "connect_timeout_secs" => {
2532                runtime.connect_timeout = Duration::from_secs_f64(Self::expect_f64(value, key)?);
2533                Ok(())
2534            }
2535            "reconnect_wait_secs" => {
2536                runtime.reconnect_wait = Duration::from_secs_f64(Self::expect_f64(value, key)?);
2537                Ok(())
2538            }
2539            "max_reconnect_tries" => {
2540                runtime.max_reconnect_tries = match Self::expect_u64(value, key)? {
2541                    0 => None,
2542                    raw => Some(raw as u32),
2543                };
2544                Ok(())
2545            }
2546            _ => Err(RuntimeConfigError {
2547                code: RuntimeConfigErrorCode::UnknownKey,
2548                message: format!("unknown runtime-config key '{}'", key),
2549            }),
2550        }
2551    }
2552
2553    #[cfg(feature = "iface-backbone")]
2554    fn reset_backbone_client_runtime_config(
2555        &mut self,
2556        key: &str,
2557    ) -> Result<(), RuntimeConfigError> {
2558        let (name, setting) = self.split_backbone_client_runtime_key(key)?;
2559        let handle = self
2560            .backbone_client_runtime
2561            .get(name)
2562            .ok_or(RuntimeConfigError {
2563                code: RuntimeConfigErrorCode::NotFound,
2564                message: format!("backbone client interface '{}' not found", name),
2565            })?;
2566        let mut runtime = recover_mutex_guard(&handle.runtime, "backbone client runtime");
2567        let startup = handle.startup.clone();
2568        match setting {
2569            "connect_timeout_secs" => runtime.connect_timeout = startup.connect_timeout,
2570            "reconnect_wait_secs" => runtime.reconnect_wait = startup.reconnect_wait,
2571            "max_reconnect_tries" => runtime.max_reconnect_tries = startup.max_reconnect_tries,
2572            _ => {
2573                return Err(RuntimeConfigError {
2574                    code: RuntimeConfigErrorCode::UnknownKey,
2575                    message: format!("unknown runtime-config key '{}'", key),
2576                })
2577            }
2578        }
2579        Ok(())
2580    }
2581
2582    #[cfg(feature = "iface-tcp")]
2583    fn list_tcp_server_runtime_config(&self) -> Vec<RuntimeConfigEntry> {
2584        let mut entries = Vec::new();
2585        let mut names: Vec<&String> = self.tcp_server_runtime.keys().collect();
2586        names.sort();
2587        for name in names {
2588            for suffix in [
2589                "max_connections",
2590                "discoverable",
2591                "discovery_name",
2592                "announce_interval_secs",
2593                "reachable_on",
2594                "stamp_value",
2595                "latitude",
2596                "longitude",
2597                "height",
2598            ] {
2599                let key = format!("tcp_server.{}.{}", name, suffix);
2600                if let Some(entry) = self.tcp_server_runtime_entry(&key) {
2601                    entries.push(entry);
2602                }
2603            }
2604        }
2605        entries
2606    }
2607
2608    #[cfg(feature = "iface-tcp")]
2609    fn list_tcp_client_runtime_config(&self) -> Vec<RuntimeConfigEntry> {
2610        let mut entries = Vec::new();
2611        let mut names: Vec<&String> = self.tcp_client_runtime.keys().collect();
2612        names.sort();
2613        for name in names {
2614            for suffix in [
2615                "connect_timeout_secs",
2616                "reconnect_wait_secs",
2617                "max_reconnect_tries",
2618            ] {
2619                let key = format!("tcp_client.{}.{}", name, suffix);
2620                if let Some(entry) = self.tcp_client_runtime_entry(&key) {
2621                    entries.push(entry);
2622                }
2623            }
2624        }
2625        entries
2626    }
2627
2628    #[cfg(feature = "iface-tcp")]
2629    fn tcp_client_runtime_entry(&self, key: &str) -> Option<RuntimeConfigEntry> {
2630        let rest = key.strip_prefix("tcp_client.")?;
2631        let (name, setting) = rest.split_once('.')?;
2632        let handle = self.tcp_client_runtime.get(name)?;
2633        let current = recover_mutex_guard(&handle.runtime, "tcp client runtime").clone();
2634        let startup = handle.startup.clone();
2635        let make_entry = |value: RuntimeConfigValue,
2636                          default: RuntimeConfigValue,
2637                          description: &str|
2638         -> RuntimeConfigEntry {
2639            RuntimeConfigEntry {
2640                key: key.to_string(),
2641                source: if value == default {
2642                    RuntimeConfigSource::Startup
2643                } else {
2644                    RuntimeConfigSource::RuntimeOverride
2645                },
2646                value,
2647                default,
2648                apply_mode: RuntimeConfigApplyMode::NextReconnect,
2649                description: Some(description.to_string()),
2650            }
2651        };
2652        match setting {
2653            "connect_timeout_secs" => Some(make_entry(
2654                RuntimeConfigValue::Float(current.connect_timeout.as_secs_f64()),
2655                RuntimeConfigValue::Float(startup.connect_timeout.as_secs_f64()),
2656                "TCP client connect timeout in seconds; applies on the next reconnect.",
2657            )),
2658            "reconnect_wait_secs" => Some(make_entry(
2659                RuntimeConfigValue::Float(current.reconnect_wait.as_secs_f64()),
2660                RuntimeConfigValue::Float(startup.reconnect_wait.as_secs_f64()),
2661                "Delay between TCP client reconnect attempts in seconds.",
2662            )),
2663            "max_reconnect_tries" => Some(make_entry(
2664                RuntimeConfigValue::Int(current.max_reconnect_tries.unwrap_or(0) as i64),
2665                RuntimeConfigValue::Int(startup.max_reconnect_tries.unwrap_or(0) as i64),
2666                "Maximum TCP client reconnect attempts; 0 disables the cap.",
2667            )),
2668            _ => None,
2669        }
2670    }
2671
2672    #[cfg(feature = "iface-tcp")]
2673    fn split_tcp_client_runtime_key<'a>(
2674        &self,
2675        key: &'a str,
2676    ) -> Result<(&'a str, &'a str), RuntimeConfigError> {
2677        let rest = key.strip_prefix("tcp_client.").ok_or(RuntimeConfigError {
2678            code: RuntimeConfigErrorCode::UnknownKey,
2679            message: format!("unknown runtime-config key '{}'", key),
2680        })?;
2681        rest.split_once('.').ok_or(RuntimeConfigError {
2682            code: RuntimeConfigErrorCode::UnknownKey,
2683            message: format!("unknown runtime-config key '{}'", key),
2684        })
2685    }
2686
2687    #[cfg(feature = "iface-tcp")]
2688    fn set_tcp_client_runtime_config(
2689        &mut self,
2690        key: &str,
2691        value: RuntimeConfigValue,
2692    ) -> Result<(), RuntimeConfigError> {
2693        let (name, setting) = self.split_tcp_client_runtime_key(key)?;
2694        let handle = self
2695            .tcp_client_runtime
2696            .get(name)
2697            .ok_or(RuntimeConfigError {
2698                code: RuntimeConfigErrorCode::NotFound,
2699                message: format!("tcp client interface '{}' not found", name),
2700            })?;
2701        let mut runtime = recover_mutex_guard(&handle.runtime, "tcp client runtime");
2702        match setting {
2703            "connect_timeout_secs" => {
2704                runtime.connect_timeout = Duration::from_secs_f64(Self::expect_f64(value, key)?);
2705                Ok(())
2706            }
2707            "reconnect_wait_secs" => {
2708                runtime.reconnect_wait = Duration::from_secs_f64(Self::expect_f64(value, key)?);
2709                Ok(())
2710            }
2711            "max_reconnect_tries" => {
2712                runtime.max_reconnect_tries = match Self::expect_u64(value, key)? {
2713                    0 => None,
2714                    raw => Some(raw as u32),
2715                };
2716                Ok(())
2717            }
2718            _ => Err(RuntimeConfigError {
2719                code: RuntimeConfigErrorCode::UnknownKey,
2720                message: format!("unknown runtime-config key '{}'", key),
2721            }),
2722        }
2723    }
2724
2725    #[cfg(feature = "iface-tcp")]
2726    fn reset_tcp_client_runtime_config(&mut self, key: &str) -> Result<(), RuntimeConfigError> {
2727        let (name, setting) = self.split_tcp_client_runtime_key(key)?;
2728        let handle = self
2729            .tcp_client_runtime
2730            .get(name)
2731            .ok_or(RuntimeConfigError {
2732                code: RuntimeConfigErrorCode::NotFound,
2733                message: format!("tcp client interface '{}' not found", name),
2734            })?;
2735        let mut runtime = recover_mutex_guard(&handle.runtime, "tcp client runtime");
2736        let startup = handle.startup.clone();
2737        match setting {
2738            "connect_timeout_secs" => runtime.connect_timeout = startup.connect_timeout,
2739            "reconnect_wait_secs" => runtime.reconnect_wait = startup.reconnect_wait,
2740            "max_reconnect_tries" => runtime.max_reconnect_tries = startup.max_reconnect_tries,
2741            _ => {
2742                return Err(RuntimeConfigError {
2743                    code: RuntimeConfigErrorCode::UnknownKey,
2744                    message: format!("unknown runtime-config key '{}'", key),
2745                })
2746            }
2747        }
2748        Ok(())
2749    }
2750
2751    #[cfg(feature = "iface-udp")]
2752    fn list_udp_runtime_config(&self) -> Vec<RuntimeConfigEntry> {
2753        let mut entries = Vec::new();
2754        let mut names: Vec<&String> = self.udp_runtime.keys().collect();
2755        names.sort();
2756        for name in names {
2757            for suffix in ["forward_ip", "forward_port"] {
2758                let key = format!("udp.{}.{}", name, suffix);
2759                if let Some(entry) = self.udp_runtime_entry(&key) {
2760                    entries.push(entry);
2761                }
2762            }
2763        }
2764        entries
2765    }
2766
2767    #[cfg(feature = "iface-udp")]
2768    fn udp_runtime_entry(&self, key: &str) -> Option<RuntimeConfigEntry> {
2769        let rest = key.strip_prefix("udp.")?;
2770        let (name, setting) = rest.split_once('.')?;
2771        let handle = self.udp_runtime.get(name)?;
2772        let current = recover_mutex_guard(&handle.runtime, "udp runtime").clone();
2773        let startup = handle.startup.clone();
2774        let make_entry = |value: RuntimeConfigValue,
2775                          default: RuntimeConfigValue,
2776                          description: &str|
2777         -> RuntimeConfigEntry {
2778            RuntimeConfigEntry {
2779                key: key.to_string(),
2780                source: if value == default {
2781                    RuntimeConfigSource::Startup
2782                } else {
2783                    RuntimeConfigSource::RuntimeOverride
2784                },
2785                value,
2786                default,
2787                apply_mode: RuntimeConfigApplyMode::Immediate,
2788                description: Some(description.to_string()),
2789            }
2790        };
2791        match setting {
2792            "forward_ip" => Some(make_entry(
2793                current
2794                    .forward_ip
2795                    .clone()
2796                    .map(RuntimeConfigValue::String)
2797                    .unwrap_or(RuntimeConfigValue::Null),
2798                startup
2799                    .forward_ip
2800                    .clone()
2801                    .map(RuntimeConfigValue::String)
2802                    .unwrap_or(RuntimeConfigValue::Null),
2803                "Outbound UDP destination IP or hostname; null clears it.",
2804            )),
2805            "forward_port" => Some(make_entry(
2806                current
2807                    .forward_port
2808                    .map(|value| RuntimeConfigValue::Int(value as i64))
2809                    .unwrap_or(RuntimeConfigValue::Null),
2810                startup
2811                    .forward_port
2812                    .map(|value| RuntimeConfigValue::Int(value as i64))
2813                    .unwrap_or(RuntimeConfigValue::Null),
2814                "Outbound UDP destination port; null clears it.",
2815            )),
2816            _ => None,
2817        }
2818    }
2819
2820    #[cfg(feature = "iface-udp")]
2821    fn split_udp_runtime_key<'a>(
2822        &self,
2823        key: &'a str,
2824    ) -> Result<(&'a str, &'a str), RuntimeConfigError> {
2825        let rest = key.strip_prefix("udp.").ok_or(RuntimeConfigError {
2826            code: RuntimeConfigErrorCode::UnknownKey,
2827            message: format!("unknown runtime-config key '{}'", key),
2828        })?;
2829        rest.split_once('.').ok_or(RuntimeConfigError {
2830            code: RuntimeConfigErrorCode::UnknownKey,
2831            message: format!("unknown runtime-config key '{}'", key),
2832        })
2833    }
2834
2835    #[cfg(feature = "iface-udp")]
2836    fn set_udp_runtime_config(
2837        &mut self,
2838        key: &str,
2839        value: RuntimeConfigValue,
2840    ) -> Result<(), RuntimeConfigError> {
2841        let (name, setting) = self.split_udp_runtime_key(key)?;
2842        let handle = self.udp_runtime.get(name).ok_or(RuntimeConfigError {
2843            code: RuntimeConfigErrorCode::NotFound,
2844            message: format!("udp interface '{}' not found", name),
2845        })?;
2846        let mut runtime = recover_mutex_guard(&handle.runtime, "udp runtime");
2847        match setting {
2848            "forward_ip" => {
2849                runtime.forward_ip = Self::expect_optional_string(value, key)?;
2850                Ok(())
2851            }
2852            "forward_port" => {
2853                runtime.forward_port = match value {
2854                    RuntimeConfigValue::Null => None,
2855                    other => Some(Self::expect_u64(other, key)? as u16),
2856                };
2857                Ok(())
2858            }
2859            _ => Err(RuntimeConfigError {
2860                code: RuntimeConfigErrorCode::UnknownKey,
2861                message: format!("unknown runtime-config key '{}'", key),
2862            }),
2863        }
2864    }
2865
2866    #[cfg(feature = "iface-udp")]
2867    fn reset_udp_runtime_config(&mut self, key: &str) -> Result<(), RuntimeConfigError> {
2868        let (name, setting) = self.split_udp_runtime_key(key)?;
2869        let handle = self.udp_runtime.get(name).ok_or(RuntimeConfigError {
2870            code: RuntimeConfigErrorCode::NotFound,
2871            message: format!("udp interface '{}' not found", name),
2872        })?;
2873        let mut runtime = recover_mutex_guard(&handle.runtime, "udp runtime");
2874        let startup = handle.startup.clone();
2875        match setting {
2876            "forward_ip" => runtime.forward_ip = startup.forward_ip,
2877            "forward_port" => runtime.forward_port = startup.forward_port,
2878            _ => {
2879                return Err(RuntimeConfigError {
2880                    code: RuntimeConfigErrorCode::UnknownKey,
2881                    message: format!("unknown runtime-config key '{}'", key),
2882                })
2883            }
2884        }
2885        Ok(())
2886    }
2887
2888    #[cfg(feature = "iface-auto")]
2889    fn list_auto_runtime_config(&self) -> Vec<RuntimeConfigEntry> {
2890        let mut entries = Vec::new();
2891        let mut names: Vec<&String> = self.auto_runtime.keys().collect();
2892        names.sort();
2893        for name in names {
2894            for suffix in [
2895                "announce_interval_secs",
2896                "peer_timeout_secs",
2897                "peer_job_interval_secs",
2898            ] {
2899                let key = format!("auto.{}.{}", name, suffix);
2900                if let Some(entry) = self.auto_runtime_entry(&key) {
2901                    entries.push(entry);
2902                }
2903            }
2904        }
2905        entries
2906    }
2907
2908    #[cfg(feature = "iface-auto")]
2909    fn auto_runtime_entry(&self, key: &str) -> Option<RuntimeConfigEntry> {
2910        let rest = key.strip_prefix("auto.")?;
2911        let (name, setting) = rest.split_once('.')?;
2912        let handle = self.auto_runtime.get(name)?;
2913        let current = recover_mutex_guard(&handle.runtime, "auto runtime").clone();
2914        let startup = handle.startup.clone();
2915        let make_entry = |value: RuntimeConfigValue,
2916                          default: RuntimeConfigValue,
2917                          description: &str|
2918         -> RuntimeConfigEntry {
2919            RuntimeConfigEntry {
2920                key: key.to_string(),
2921                source: if value == default {
2922                    RuntimeConfigSource::Startup
2923                } else {
2924                    RuntimeConfigSource::RuntimeOverride
2925                },
2926                value,
2927                default,
2928                apply_mode: RuntimeConfigApplyMode::Immediate,
2929                description: Some(description.to_string()),
2930            }
2931        };
2932        match setting {
2933            "announce_interval_secs" => Some(make_entry(
2934                RuntimeConfigValue::Float(current.announce_interval_secs),
2935                RuntimeConfigValue::Float(startup.announce_interval_secs),
2936                "Interval between multicast discovery announces in seconds.",
2937            )),
2938            "peer_timeout_secs" => Some(make_entry(
2939                RuntimeConfigValue::Float(current.peer_timeout_secs),
2940                RuntimeConfigValue::Float(startup.peer_timeout_secs),
2941                "How long an Auto peer may stay quiet before being culled.",
2942            )),
2943            "peer_job_interval_secs" => Some(make_entry(
2944                RuntimeConfigValue::Float(current.peer_job_interval_secs),
2945                RuntimeConfigValue::Float(startup.peer_job_interval_secs),
2946                "Interval between Auto peer maintenance passes.",
2947            )),
2948            _ => None,
2949        }
2950    }
2951
2952    #[cfg(feature = "iface-auto")]
2953    fn split_auto_runtime_key<'a>(
2954        &self,
2955        key: &'a str,
2956    ) -> Result<(&'a str, &'a str), RuntimeConfigError> {
2957        let rest = key.strip_prefix("auto.").ok_or(RuntimeConfigError {
2958            code: RuntimeConfigErrorCode::UnknownKey,
2959            message: format!("unknown runtime-config key '{}'", key),
2960        })?;
2961        rest.split_once('.').ok_or(RuntimeConfigError {
2962            code: RuntimeConfigErrorCode::UnknownKey,
2963            message: format!("unknown runtime-config key '{}'", key),
2964        })
2965    }
2966
2967    #[cfg(feature = "iface-auto")]
2968    fn set_auto_runtime_config(
2969        &mut self,
2970        key: &str,
2971        value: RuntimeConfigValue,
2972    ) -> Result<(), RuntimeConfigError> {
2973        let (name, setting) = self.split_auto_runtime_key(key)?;
2974        let handle = self.auto_runtime.get(name).ok_or(RuntimeConfigError {
2975            code: RuntimeConfigErrorCode::NotFound,
2976            message: format!("auto interface '{}' not found", name),
2977        })?;
2978        let mut runtime = recover_mutex_guard(&handle.runtime, "auto runtime");
2979        match setting {
2980            "announce_interval_secs" => {
2981                runtime.announce_interval_secs = Self::expect_f64(value, key)?.max(0.1)
2982            }
2983            "peer_timeout_secs" => {
2984                runtime.peer_timeout_secs = Self::expect_f64(value, key)?.max(0.1)
2985            }
2986            "peer_job_interval_secs" => {
2987                runtime.peer_job_interval_secs = Self::expect_f64(value, key)?.max(0.1)
2988            }
2989            _ => {
2990                return Err(RuntimeConfigError {
2991                    code: RuntimeConfigErrorCode::UnknownKey,
2992                    message: format!("unknown runtime-config key '{}'", key),
2993                });
2994            }
2995        }
2996        Ok(())
2997    }
2998
2999    #[cfg(feature = "iface-auto")]
3000    fn reset_auto_runtime_config(&mut self, key: &str) -> Result<(), RuntimeConfigError> {
3001        let (name, setting) = self.split_auto_runtime_key(key)?;
3002        let handle = self.auto_runtime.get(name).ok_or(RuntimeConfigError {
3003            code: RuntimeConfigErrorCode::NotFound,
3004            message: format!("auto interface '{}' not found", name),
3005        })?;
3006        let mut runtime = recover_mutex_guard(&handle.runtime, "auto runtime");
3007        let startup = handle.startup.clone();
3008        match setting {
3009            "announce_interval_secs" => {
3010                runtime.announce_interval_secs = startup.announce_interval_secs
3011            }
3012            "peer_timeout_secs" => runtime.peer_timeout_secs = startup.peer_timeout_secs,
3013            "peer_job_interval_secs" => {
3014                runtime.peer_job_interval_secs = startup.peer_job_interval_secs
3015            }
3016            _ => {
3017                return Err(RuntimeConfigError {
3018                    code: RuntimeConfigErrorCode::UnknownKey,
3019                    message: format!("unknown runtime-config key '{}'", key),
3020                });
3021            }
3022        }
3023        Ok(())
3024    }
3025
3026    #[cfg(feature = "iface-i2p")]
3027    fn list_i2p_runtime_config(&self) -> Vec<RuntimeConfigEntry> {
3028        let mut entries = Vec::new();
3029        let mut names: Vec<&String> = self.i2p_runtime.keys().collect();
3030        names.sort();
3031        for name in names {
3032            let key = format!("i2p.{}.reconnect_wait_secs", name);
3033            if let Some(entry) = self.i2p_runtime_entry(&key) {
3034                entries.push(entry);
3035            }
3036        }
3037        entries
3038    }
3039
3040    #[cfg(feature = "iface-i2p")]
3041    fn i2p_runtime_entry(&self, key: &str) -> Option<RuntimeConfigEntry> {
3042        let rest = key.strip_prefix("i2p.")?;
3043        let (name, setting) = rest.split_once('.')?;
3044        let handle = self.i2p_runtime.get(name)?;
3045        let current = recover_mutex_guard(&handle.runtime, "i2p runtime").clone();
3046        let startup = handle.startup.clone();
3047        match setting {
3048            "reconnect_wait_secs" => Some(RuntimeConfigEntry {
3049                key: key.to_string(),
3050                source: if current.reconnect_wait == startup.reconnect_wait {
3051                    RuntimeConfigSource::Startup
3052                } else {
3053                    RuntimeConfigSource::RuntimeOverride
3054                },
3055                value: RuntimeConfigValue::Float(current.reconnect_wait.as_secs_f64()),
3056                default: RuntimeConfigValue::Float(startup.reconnect_wait.as_secs_f64()),
3057                apply_mode: RuntimeConfigApplyMode::NextReconnect,
3058                description: Some(
3059                    "Delay before retrying outbound I2P peer connections.".to_string(),
3060                ),
3061            }),
3062            _ => None,
3063        }
3064    }
3065
3066    #[cfg(feature = "iface-i2p")]
3067    fn split_i2p_runtime_key<'a>(
3068        &self,
3069        key: &'a str,
3070    ) -> Result<(&'a str, &'a str), RuntimeConfigError> {
3071        let rest = key.strip_prefix("i2p.").ok_or(RuntimeConfigError {
3072            code: RuntimeConfigErrorCode::UnknownKey,
3073            message: format!("unknown runtime-config key '{}'", key),
3074        })?;
3075        rest.split_once('.').ok_or(RuntimeConfigError {
3076            code: RuntimeConfigErrorCode::UnknownKey,
3077            message: format!("unknown runtime-config key '{}'", key),
3078        })
3079    }
3080
3081    #[cfg(feature = "iface-i2p")]
3082    fn set_i2p_runtime_config(
3083        &mut self,
3084        key: &str,
3085        value: RuntimeConfigValue,
3086    ) -> Result<(), RuntimeConfigError> {
3087        let (name, setting) = self.split_i2p_runtime_key(key)?;
3088        let handle = self.i2p_runtime.get(name).ok_or(RuntimeConfigError {
3089            code: RuntimeConfigErrorCode::NotFound,
3090            message: format!("i2p interface '{}' not found", name),
3091        })?;
3092        let mut runtime = recover_mutex_guard(&handle.runtime, "i2p runtime");
3093        match setting {
3094            "reconnect_wait_secs" => {
3095                runtime.reconnect_wait =
3096                    Duration::from_secs_f64(Self::expect_f64(value, key)?.max(0.1));
3097                Ok(())
3098            }
3099            _ => Err(RuntimeConfigError {
3100                code: RuntimeConfigErrorCode::UnknownKey,
3101                message: format!("unknown runtime-config key '{}'", key),
3102            }),
3103        }
3104    }
3105
3106    #[cfg(feature = "iface-i2p")]
3107    fn reset_i2p_runtime_config(&mut self, key: &str) -> Result<(), RuntimeConfigError> {
3108        let (name, setting) = self.split_i2p_runtime_key(key)?;
3109        let handle = self.i2p_runtime.get(name).ok_or(RuntimeConfigError {
3110            code: RuntimeConfigErrorCode::NotFound,
3111            message: format!("i2p interface '{}' not found", name),
3112        })?;
3113        let mut runtime = recover_mutex_guard(&handle.runtime, "i2p runtime");
3114        let startup = handle.startup.clone();
3115        match setting {
3116            "reconnect_wait_secs" => runtime.reconnect_wait = startup.reconnect_wait,
3117            _ => {
3118                return Err(RuntimeConfigError {
3119                    code: RuntimeConfigErrorCode::UnknownKey,
3120                    message: format!("unknown runtime-config key '{}'", key),
3121                });
3122            }
3123        }
3124        Ok(())
3125    }
3126
3127    #[cfg(feature = "iface-pipe")]
3128    fn list_pipe_runtime_config(&self) -> Vec<RuntimeConfigEntry> {
3129        let mut entries = Vec::new();
3130        let mut names: Vec<&String> = self.pipe_runtime.keys().collect();
3131        names.sort();
3132        for name in names {
3133            let key = format!("pipe.{}.respawn_delay_secs", name);
3134            if let Some(entry) = self.pipe_runtime_entry(&key) {
3135                entries.push(entry);
3136            }
3137        }
3138        entries
3139    }
3140
3141    #[cfg(feature = "iface-pipe")]
3142    fn pipe_runtime_entry(&self, key: &str) -> Option<RuntimeConfigEntry> {
3143        let rest = key.strip_prefix("pipe.")?;
3144        let (name, setting) = rest.split_once('.')?;
3145        let handle = self.pipe_runtime.get(name)?;
3146        let current = recover_mutex_guard(&handle.runtime, "pipe runtime").clone();
3147        let startup = handle.startup.clone();
3148        match setting {
3149            "respawn_delay_secs" => Some(RuntimeConfigEntry {
3150                key: key.to_string(),
3151                source: if current.respawn_delay == startup.respawn_delay {
3152                    RuntimeConfigSource::Startup
3153                } else {
3154                    RuntimeConfigSource::RuntimeOverride
3155                },
3156                value: RuntimeConfigValue::Float(current.respawn_delay.as_secs_f64()),
3157                default: RuntimeConfigValue::Float(startup.respawn_delay.as_secs_f64()),
3158                apply_mode: RuntimeConfigApplyMode::NextReconnect,
3159                description: Some(
3160                    "Delay before respawning the pipe subprocess after exit.".to_string(),
3161                ),
3162            }),
3163            _ => None,
3164        }
3165    }
3166
3167    #[cfg(feature = "iface-pipe")]
3168    fn split_pipe_runtime_key<'a>(
3169        &self,
3170        key: &'a str,
3171    ) -> Result<(&'a str, &'a str), RuntimeConfigError> {
3172        let rest = key.strip_prefix("pipe.").ok_or(RuntimeConfigError {
3173            code: RuntimeConfigErrorCode::UnknownKey,
3174            message: format!("unknown runtime-config key '{}'", key),
3175        })?;
3176        rest.split_once('.').ok_or(RuntimeConfigError {
3177            code: RuntimeConfigErrorCode::UnknownKey,
3178            message: format!("unknown runtime-config key '{}'", key),
3179        })
3180    }
3181
3182    #[cfg(feature = "iface-pipe")]
3183    fn set_pipe_runtime_config(
3184        &mut self,
3185        key: &str,
3186        value: RuntimeConfigValue,
3187    ) -> Result<(), RuntimeConfigError> {
3188        let (name, setting) = self.split_pipe_runtime_key(key)?;
3189        let handle = self.pipe_runtime.get(name).ok_or(RuntimeConfigError {
3190            code: RuntimeConfigErrorCode::NotFound,
3191            message: format!("pipe interface '{}' not found", name),
3192        })?;
3193        let mut runtime = recover_mutex_guard(&handle.runtime, "pipe runtime");
3194        match setting {
3195            "respawn_delay_secs" => {
3196                runtime.respawn_delay =
3197                    Duration::from_secs_f64(Self::expect_f64(value, key)?.max(0.1));
3198                Ok(())
3199            }
3200            _ => Err(RuntimeConfigError {
3201                code: RuntimeConfigErrorCode::UnknownKey,
3202                message: format!("unknown runtime-config key '{}'", key),
3203            }),
3204        }
3205    }
3206
3207    #[cfg(feature = "iface-pipe")]
3208    fn reset_pipe_runtime_config(&mut self, key: &str) -> Result<(), RuntimeConfigError> {
3209        let (name, setting) = self.split_pipe_runtime_key(key)?;
3210        let handle = self.pipe_runtime.get(name).ok_or(RuntimeConfigError {
3211            code: RuntimeConfigErrorCode::NotFound,
3212            message: format!("pipe interface '{}' not found", name),
3213        })?;
3214        let mut runtime = recover_mutex_guard(&handle.runtime, "pipe runtime");
3215        let startup = handle.startup.clone();
3216        match setting {
3217            "respawn_delay_secs" => runtime.respawn_delay = startup.respawn_delay,
3218            _ => {
3219                return Err(RuntimeConfigError {
3220                    code: RuntimeConfigErrorCode::UnknownKey,
3221                    message: format!("unknown runtime-config key '{}'", key),
3222                });
3223            }
3224        }
3225        Ok(())
3226    }
3227
3228    #[cfg(feature = "iface-rnode")]
3229    fn list_rnode_runtime_config(&self) -> Vec<RuntimeConfigEntry> {
3230        let mut entries = Vec::new();
3231        let mut names: Vec<&String> = self.rnode_runtime.keys().collect();
3232        names.sort();
3233        for name in names {
3234            for suffix in [
3235                "frequency_hz",
3236                "bandwidth_hz",
3237                "txpower_dbm",
3238                "spreading_factor",
3239                "coding_rate",
3240                "st_alock_pct",
3241                "lt_alock_pct",
3242            ] {
3243                let key = format!("rnode.{}.{}", name, suffix);
3244                if let Some(entry) = self.rnode_runtime_entry(&key) {
3245                    entries.push(entry);
3246                }
3247            }
3248        }
3249        entries
3250    }
3251
3252    #[cfg(feature = "iface-rnode")]
3253    fn rnode_runtime_entry(&self, key: &str) -> Option<RuntimeConfigEntry> {
3254        let rest = key.strip_prefix("rnode.")?;
3255        let (name, setting) = rest.split_once('.')?;
3256        let handle = self.rnode_runtime.get(name)?;
3257        let current = recover_mutex_guard(&handle.runtime, "rnode runtime").clone();
3258        let startup = handle.startup.clone();
3259        let make_entry = |value: RuntimeConfigValue,
3260                          default: RuntimeConfigValue,
3261                          description: &str|
3262         -> RuntimeConfigEntry {
3263            RuntimeConfigEntry {
3264                key: key.to_string(),
3265                source: if value == default {
3266                    RuntimeConfigSource::Startup
3267                } else {
3268                    RuntimeConfigSource::RuntimeOverride
3269                },
3270                value,
3271                default,
3272                apply_mode: RuntimeConfigApplyMode::Immediate,
3273                description: Some(description.to_string()),
3274            }
3275        };
3276        match setting {
3277            "frequency_hz" => Some(make_entry(
3278                RuntimeConfigValue::Int(current.sub.frequency as i64),
3279                RuntimeConfigValue::Int(startup.sub.frequency as i64),
3280                "RNode radio frequency in Hz.",
3281            )),
3282            "bandwidth_hz" => Some(make_entry(
3283                RuntimeConfigValue::Int(current.sub.bandwidth as i64),
3284                RuntimeConfigValue::Int(startup.sub.bandwidth as i64),
3285                "RNode radio bandwidth in Hz.",
3286            )),
3287            "txpower_dbm" => Some(make_entry(
3288                RuntimeConfigValue::Int(current.sub.txpower as i64),
3289                RuntimeConfigValue::Int(startup.sub.txpower as i64),
3290                "RNode transmit power in dBm.",
3291            )),
3292            "spreading_factor" => Some(make_entry(
3293                RuntimeConfigValue::Int(current.sub.spreading_factor as i64),
3294                RuntimeConfigValue::Int(startup.sub.spreading_factor as i64),
3295                "RNode LoRa spreading factor.",
3296            )),
3297            "coding_rate" => Some(make_entry(
3298                RuntimeConfigValue::Int(current.sub.coding_rate as i64),
3299                RuntimeConfigValue::Int(startup.sub.coding_rate as i64),
3300                "RNode LoRa coding rate.",
3301            )),
3302            "st_alock_pct" => Some(make_entry(
3303                current
3304                    .sub
3305                    .st_alock
3306                    .map(|value| RuntimeConfigValue::Float(value as f64))
3307                    .unwrap_or(RuntimeConfigValue::Null),
3308                startup
3309                    .sub
3310                    .st_alock
3311                    .map(|value| RuntimeConfigValue::Float(value as f64))
3312                    .unwrap_or(RuntimeConfigValue::Null),
3313                "RNode short-term airtime lock percent; null clears it.",
3314            )),
3315            "lt_alock_pct" => Some(make_entry(
3316                current
3317                    .sub
3318                    .lt_alock
3319                    .map(|value| RuntimeConfigValue::Float(value as f64))
3320                    .unwrap_or(RuntimeConfigValue::Null),
3321                startup
3322                    .sub
3323                    .lt_alock
3324                    .map(|value| RuntimeConfigValue::Float(value as f64))
3325                    .unwrap_or(RuntimeConfigValue::Null),
3326                "RNode long-term airtime lock percent; null clears it.",
3327            )),
3328            _ => None,
3329        }
3330    }
3331
3332    #[cfg(feature = "iface-rnode")]
3333    fn split_rnode_runtime_key<'a>(
3334        &self,
3335        key: &'a str,
3336    ) -> Result<(&'a str, &'a str), RuntimeConfigError> {
3337        let rest = key.strip_prefix("rnode.").ok_or(RuntimeConfigError {
3338            code: RuntimeConfigErrorCode::UnknownKey,
3339            message: format!("unknown runtime-config key '{}'", key),
3340        })?;
3341        rest.split_once('.').ok_or(RuntimeConfigError {
3342            code: RuntimeConfigErrorCode::UnknownKey,
3343            message: format!("unknown runtime-config key '{}'", key),
3344        })
3345    }
3346
3347    #[cfg(feature = "iface-rnode")]
3348    fn apply_rnode_runtime(runtime: &mut RNodeRuntime) -> Result<(), RuntimeConfigError> {
3349        if let Some(err) = validate_sub_config(&runtime.sub) {
3350            return Err(RuntimeConfigError {
3351                code: RuntimeConfigErrorCode::InvalidValue,
3352                message: err,
3353            });
3354        }
3355        if let Some(writer) = runtime.writer.clone() {
3356            crate::interface::rnode::configure_subinterface(&writer, 0, &runtime.sub, false)
3357                .map_err(|e| RuntimeConfigError {
3358                    code: RuntimeConfigErrorCode::ApplyFailed,
3359                    message: format!("failed to apply RNode config: {}", e),
3360                })?;
3361        }
3362        Ok(())
3363    }
3364
3365    #[cfg(feature = "iface-rnode")]
3366    fn set_rnode_runtime_config(
3367        &mut self,
3368        key: &str,
3369        value: RuntimeConfigValue,
3370    ) -> Result<(), RuntimeConfigError> {
3371        let (name, setting) = self.split_rnode_runtime_key(key)?;
3372        let handle = self.rnode_runtime.get(name).ok_or(RuntimeConfigError {
3373            code: RuntimeConfigErrorCode::NotFound,
3374            message: format!("rnode interface '{}' not found", name),
3375        })?;
3376        let mut runtime = recover_mutex_guard(&handle.runtime, "rnode runtime");
3377        let old = runtime.sub.clone();
3378        match setting {
3379            "frequency_hz" => runtime.sub.frequency = Self::expect_u64(value, key)? as u32,
3380            "bandwidth_hz" => runtime.sub.bandwidth = Self::expect_u64(value, key)? as u32,
3381            "txpower_dbm" => runtime.sub.txpower = Self::expect_i64(value, key)? as i8,
3382            "spreading_factor" => {
3383                runtime.sub.spreading_factor = Self::expect_u64(value, key)? as u8
3384            }
3385            "coding_rate" => runtime.sub.coding_rate = Self::expect_u64(value, key)? as u8,
3386            "st_alock_pct" => {
3387                runtime.sub.st_alock = match value {
3388                    RuntimeConfigValue::Null => None,
3389                    other => Some(Self::expect_f64(other, key)? as f32),
3390                };
3391            }
3392            "lt_alock_pct" => {
3393                runtime.sub.lt_alock = match value {
3394                    RuntimeConfigValue::Null => None,
3395                    other => Some(Self::expect_f64(other, key)? as f32),
3396                };
3397            }
3398            _ => {
3399                return Err(RuntimeConfigError {
3400                    code: RuntimeConfigErrorCode::UnknownKey,
3401                    message: format!("unknown runtime-config key '{}'", key),
3402                });
3403            }
3404        }
3405        if let Err(err) = Self::apply_rnode_runtime(&mut runtime) {
3406            runtime.sub = old;
3407            return Err(err);
3408        }
3409        Ok(())
3410    }
3411
3412    #[cfg(feature = "iface-rnode")]
3413    fn reset_rnode_runtime_config(&mut self, key: &str) -> Result<(), RuntimeConfigError> {
3414        let (name, setting) = self.split_rnode_runtime_key(key)?;
3415        let handle = self.rnode_runtime.get(name).ok_or(RuntimeConfigError {
3416            code: RuntimeConfigErrorCode::NotFound,
3417            message: format!("rnode interface '{}' not found", name),
3418        })?;
3419        let mut runtime = recover_mutex_guard(&handle.runtime, "rnode runtime");
3420        let old = runtime.sub.clone();
3421        let startup = handle.startup.clone();
3422        match setting {
3423            "frequency_hz" => runtime.sub.frequency = startup.sub.frequency,
3424            "bandwidth_hz" => runtime.sub.bandwidth = startup.sub.bandwidth,
3425            "txpower_dbm" => runtime.sub.txpower = startup.sub.txpower,
3426            "spreading_factor" => runtime.sub.spreading_factor = startup.sub.spreading_factor,
3427            "coding_rate" => runtime.sub.coding_rate = startup.sub.coding_rate,
3428            "st_alock_pct" => runtime.sub.st_alock = startup.sub.st_alock,
3429            "lt_alock_pct" => runtime.sub.lt_alock = startup.sub.lt_alock,
3430            _ => {
3431                return Err(RuntimeConfigError {
3432                    code: RuntimeConfigErrorCode::UnknownKey,
3433                    message: format!("unknown runtime-config key '{}'", key),
3434                });
3435            }
3436        }
3437        if let Err(err) = Self::apply_rnode_runtime(&mut runtime) {
3438            runtime.sub = old;
3439            return Err(err);
3440        }
3441        Ok(())
3442    }
3443
3444    fn list_generic_interface_runtime_config(&self) -> Vec<RuntimeConfigEntry> {
3445        let mut entries = Vec::new();
3446        let mut names: Vec<String> = self
3447            .interfaces
3448            .values()
3449            .map(|entry| entry.info.name.clone())
3450            .collect();
3451        names.sort();
3452        names.dedup();
3453        for name in names {
3454            for suffix in [
3455                "enabled",
3456                "mode",
3457                "announce_rate_target",
3458                "announce_rate_grace",
3459                "announce_rate_penalty",
3460                "announce_cap",
3461                "ingress_control",
3462                "ic_max_held_announces",
3463                "ic_burst_hold",
3464                "ic_burst_freq_new",
3465                "ic_burst_freq",
3466                "ic_new_time",
3467                "ic_burst_penalty",
3468                "ic_held_release_interval",
3469            ] {
3470                let key = format!("interface.{}.{}", name, suffix);
3471                if let Some(entry) = self.generic_interface_runtime_entry(&key) {
3472                    entries.push(entry);
3473                }
3474            }
3475            if self.interface_ifac_runtime.contains_key(&name) {
3476                for suffix in ["ifac_netname", "ifac_passphrase", "ifac_size_bytes"] {
3477                    let key = format!("interface.{}.{}", name, suffix);
3478                    if let Some(entry) = self.generic_interface_runtime_entry(&key) {
3479                        entries.push(entry);
3480                    }
3481                }
3482            }
3483        }
3484        entries
3485    }
3486
3487    fn generic_interface_runtime_entry(&self, key: &str) -> Option<RuntimeConfigEntry> {
3488        let rest = key.strip_prefix("interface.")?;
3489        let (name, setting) = rest.rsplit_once('.')?;
3490        let make_entry = |value: RuntimeConfigValue,
3491                          default: RuntimeConfigValue,
3492                          apply_mode: RuntimeConfigApplyMode,
3493                          description: &str|
3494         -> RuntimeConfigEntry {
3495            RuntimeConfigEntry {
3496                key: key.to_string(),
3497                source: if value == default {
3498                    RuntimeConfigSource::Startup
3499                } else {
3500                    RuntimeConfigSource::RuntimeOverride
3501                },
3502                value,
3503                default,
3504                apply_mode,
3505                description: Some(description.to_string()),
3506            }
3507        };
3508        match setting {
3509            "enabled" => {
3510                let entry = self
3511                    .interfaces
3512                    .values()
3513                    .find(|entry| entry.info.name == name)?;
3514                Some(make_entry(
3515                    RuntimeConfigValue::Bool(entry.enabled),
3516                    RuntimeConfigValue::Bool(true),
3517                    RuntimeConfigApplyMode::Immediate,
3518                    "Administrative enable/disable state for this interface.",
3519                ))
3520            }
3521            "ifac_netname" => {
3522                let current = self.interface_ifac_runtime.get(name)?;
3523                let startup = self.interface_ifac_runtime_defaults.get(name)?;
3524                Some(make_entry(
3525                    current
3526                        .netname
3527                        .clone()
3528                        .map(RuntimeConfigValue::String)
3529                        .unwrap_or(RuntimeConfigValue::Null),
3530                    startup
3531                        .netname
3532                        .clone()
3533                        .map(RuntimeConfigValue::String)
3534                        .unwrap_or(RuntimeConfigValue::Null),
3535                    RuntimeConfigApplyMode::Immediate,
3536                    "IFAC network name for this interface; null clears it.",
3537                ))
3538            }
3539            "ifac_passphrase" => {
3540                let current = self.interface_ifac_runtime.get(name)?;
3541                let startup = self.interface_ifac_runtime_defaults.get(name)?;
3542                let current_value = current
3543                    .netkey
3544                    .as_ref()
3545                    .map(|_| RuntimeConfigValue::String("<redacted>".to_string()))
3546                    .unwrap_or(RuntimeConfigValue::Null);
3547                let default_value = startup
3548                    .netkey
3549                    .as_ref()
3550                    .map(|_| RuntimeConfigValue::String("<redacted>".to_string()))
3551                    .unwrap_or(RuntimeConfigValue::Null);
3552                Some(RuntimeConfigEntry {
3553                    key: key.to_string(),
3554                    source: if current.netkey == startup.netkey {
3555                        RuntimeConfigSource::Startup
3556                    } else {
3557                        RuntimeConfigSource::RuntimeOverride
3558                    },
3559                    value: current_value,
3560                    default: default_value,
3561                    apply_mode: RuntimeConfigApplyMode::Immediate,
3562                    description: Some(
3563                        "IFAC passphrase for this interface; write-only, set a string to change it or null to clear it."
3564                            .to_string(),
3565                    ),
3566                })
3567            }
3568            "ifac_size_bytes" => {
3569                let current = self.interface_ifac_runtime.get(name)?;
3570                let startup = self.interface_ifac_runtime_defaults.get(name)?;
3571                Some(make_entry(
3572                    RuntimeConfigValue::Int(current.size as i64),
3573                    RuntimeConfigValue::Int(startup.size as i64),
3574                    RuntimeConfigApplyMode::Immediate,
3575                    "IFAC size in bytes; applies when IFAC is enabled.",
3576                ))
3577            }
3578            "mode" => {
3579                let (_, current, startup) = self.interface_runtime_infos_by_name(name)?;
3580                Some(make_entry(
3581                    RuntimeConfigValue::String(Self::interface_mode_name(current.mode)),
3582                    RuntimeConfigValue::String(Self::interface_mode_name(startup.mode)),
3583                    RuntimeConfigApplyMode::Immediate,
3584                    "Routing mode for this interface.",
3585                ))
3586            }
3587            "announce_rate_target" => {
3588                let (_, current, startup) = self.interface_runtime_infos_by_name(name)?;
3589                Some(make_entry(
3590                    current
3591                        .announce_rate_target
3592                        .map(RuntimeConfigValue::Float)
3593                        .unwrap_or(RuntimeConfigValue::Null),
3594                    startup
3595                        .announce_rate_target
3596                        .map(RuntimeConfigValue::Float)
3597                        .unwrap_or(RuntimeConfigValue::Null),
3598                    RuntimeConfigApplyMode::Immediate,
3599                    "Optional announce rate target in announces/sec; null disables it.",
3600                ))
3601            }
3602            "announce_rate_grace" => {
3603                let (_, current, startup) = self.interface_runtime_infos_by_name(name)?;
3604                Some(make_entry(
3605                    RuntimeConfigValue::Int(current.announce_rate_grace as i64),
3606                    RuntimeConfigValue::Int(startup.announce_rate_grace as i64),
3607                    RuntimeConfigApplyMode::Immediate,
3608                    "Announce rate grace period in announces.",
3609                ))
3610            }
3611            "announce_rate_penalty" => {
3612                let (_, current, startup) = self.interface_runtime_infos_by_name(name)?;
3613                Some(make_entry(
3614                    RuntimeConfigValue::Float(current.announce_rate_penalty),
3615                    RuntimeConfigValue::Float(startup.announce_rate_penalty),
3616                    RuntimeConfigApplyMode::Immediate,
3617                    "Announce rate penalty multiplier.",
3618                ))
3619            }
3620            "announce_cap" => {
3621                let (_, current, startup) = self.interface_runtime_infos_by_name(name)?;
3622                Some(make_entry(
3623                    RuntimeConfigValue::Float(current.announce_cap),
3624                    RuntimeConfigValue::Float(startup.announce_cap),
3625                    RuntimeConfigApplyMode::Immediate,
3626                    "Fraction of bitrate reserved for announces.",
3627                ))
3628            }
3629            "ingress_control" => {
3630                let (_, current, startup) = self.interface_runtime_infos_by_name(name)?;
3631                Some(make_entry(
3632                    RuntimeConfigValue::Bool(current.ingress_control.enabled),
3633                    RuntimeConfigValue::Bool(startup.ingress_control.enabled),
3634                    RuntimeConfigApplyMode::Immediate,
3635                    "Whether ingress control is enabled for this interface.",
3636                ))
3637            }
3638            "ic_max_held_announces" => {
3639                let (_, current, startup) = self.interface_runtime_infos_by_name(name)?;
3640                Some(make_entry(
3641                    RuntimeConfigValue::Int(current.ingress_control.max_held_announces as i64),
3642                    RuntimeConfigValue::Int(startup.ingress_control.max_held_announces as i64),
3643                    RuntimeConfigApplyMode::Immediate,
3644                    "Maximum held announces retained while ingress control is limiting this interface.",
3645                ))
3646            }
3647            "ic_burst_hold" => {
3648                let (_, current, startup) = self.interface_runtime_infos_by_name(name)?;
3649                Some(make_entry(
3650                    RuntimeConfigValue::Float(current.ingress_control.burst_hold),
3651                    RuntimeConfigValue::Float(startup.ingress_control.burst_hold),
3652                    RuntimeConfigApplyMode::Immediate,
3653                    "Seconds to keep ingress-control burst state active before releasing held announces.",
3654                ))
3655            }
3656            "ic_burst_freq_new" => {
3657                let (_, current, startup) = self.interface_runtime_infos_by_name(name)?;
3658                Some(make_entry(
3659                    RuntimeConfigValue::Float(current.ingress_control.burst_freq_new),
3660                    RuntimeConfigValue::Float(startup.ingress_control.burst_freq_new),
3661                    RuntimeConfigApplyMode::Immediate,
3662                    "Announce frequency threshold for new interfaces.",
3663                ))
3664            }
3665            "ic_burst_freq" => {
3666                let (_, current, startup) = self.interface_runtime_infos_by_name(name)?;
3667                Some(make_entry(
3668                    RuntimeConfigValue::Float(current.ingress_control.burst_freq),
3669                    RuntimeConfigValue::Float(startup.ingress_control.burst_freq),
3670                    RuntimeConfigApplyMode::Immediate,
3671                    "Announce frequency threshold for established interfaces.",
3672                ))
3673            }
3674            "ic_new_time" => {
3675                let (_, current, startup) = self.interface_runtime_infos_by_name(name)?;
3676                Some(make_entry(
3677                    RuntimeConfigValue::Float(current.ingress_control.new_time),
3678                    RuntimeConfigValue::Float(startup.ingress_control.new_time),
3679                    RuntimeConfigApplyMode::Immediate,
3680                    "Seconds after interface start that ingress control uses the new-interface burst threshold.",
3681                ))
3682            }
3683            "ic_burst_penalty" => {
3684                let (_, current, startup) = self.interface_runtime_infos_by_name(name)?;
3685                Some(make_entry(
3686                    RuntimeConfigValue::Float(current.ingress_control.burst_penalty),
3687                    RuntimeConfigValue::Float(startup.ingress_control.burst_penalty),
3688                    RuntimeConfigApplyMode::Immediate,
3689                    "Seconds to wait after a burst before releasing held announces.",
3690                ))
3691            }
3692            "ic_held_release_interval" => {
3693                let (_, current, startup) = self.interface_runtime_infos_by_name(name)?;
3694                Some(make_entry(
3695                    RuntimeConfigValue::Float(current.ingress_control.held_release_interval),
3696                    RuntimeConfigValue::Float(startup.ingress_control.held_release_interval),
3697                    RuntimeConfigApplyMode::Immediate,
3698                    "Seconds between held announce releases.",
3699                ))
3700            }
3701            _ => None,
3702        }
3703    }
3704
3705    fn split_generic_interface_runtime_key<'a>(
3706        &self,
3707        key: &'a str,
3708    ) -> Result<(&'a str, &'a str), RuntimeConfigError> {
3709        let rest = key.strip_prefix("interface.").ok_or(RuntimeConfigError {
3710            code: RuntimeConfigErrorCode::UnknownKey,
3711            message: format!("unknown runtime-config key '{}'", key),
3712        })?;
3713        rest.rsplit_once('.').ok_or(RuntimeConfigError {
3714            code: RuntimeConfigErrorCode::UnknownKey,
3715            message: format!("unknown runtime-config key '{}'", key),
3716        })
3717    }
3718
3719    fn interface_runtime_infos_by_name(
3720        &self,
3721        name: &str,
3722    ) -> Option<(
3723        rns_core::transport::types::InterfaceId,
3724        &rns_core::transport::types::InterfaceInfo,
3725        &rns_core::transport::types::InterfaceInfo,
3726    )> {
3727        let (id, entry) = self
3728            .interfaces
3729            .iter()
3730            .find(|(_, entry)| entry.info.name == name)?;
3731        let startup = self.interface_runtime_defaults.get(name)?;
3732        Some((*id, &entry.info, startup))
3733    }
3734
3735    fn interface_mode_name(mode: u8) -> String {
3736        match mode {
3737            rns_core::constants::MODE_FULL => "full".to_string(),
3738            rns_core::constants::MODE_ACCESS_POINT => "access_point".to_string(),
3739            rns_core::constants::MODE_POINT_TO_POINT => "point_to_point".to_string(),
3740            rns_core::constants::MODE_ROAMING => "roaming".to_string(),
3741            rns_core::constants::MODE_BOUNDARY => "boundary".to_string(),
3742            rns_core::constants::MODE_GATEWAY => "gateway".to_string(),
3743            _ => mode.to_string(),
3744        }
3745    }
3746
3747    fn parse_interface_mode(value: &RuntimeConfigValue) -> Option<u8> {
3748        match value {
3749            RuntimeConfigValue::Int(v) if *v >= 0 && *v <= u8::MAX as i64 => Some(*v as u8),
3750            RuntimeConfigValue::String(s) => match s.to_ascii_lowercase().as_str() {
3751                "full" => Some(rns_core::constants::MODE_FULL),
3752                "access_point" | "accesspoint" | "ap" => {
3753                    Some(rns_core::constants::MODE_ACCESS_POINT)
3754                }
3755                "point_to_point" | "pointtopoint" | "ptp" => {
3756                    Some(rns_core::constants::MODE_POINT_TO_POINT)
3757                }
3758                "roaming" => Some(rns_core::constants::MODE_ROAMING),
3759                "boundary" => Some(rns_core::constants::MODE_BOUNDARY),
3760                "gateway" | "gw" => Some(rns_core::constants::MODE_GATEWAY),
3761                _ => None,
3762            },
3763            _ => None,
3764        }
3765    }
3766
3767    fn apply_interface_ifac_runtime(entry: &mut InterfaceEntry, config: &IfacRuntimeConfig) {
3768        entry.ifac = if config.netname.is_some() || config.netkey.is_some() {
3769            Some(ifac::derive_ifac(
3770                config.netname.as_deref(),
3771                config.netkey.as_deref(),
3772                config.size,
3773            ))
3774        } else {
3775            None
3776        };
3777    }
3778
3779    fn set_generic_interface_runtime_config(
3780        &mut self,
3781        key: &str,
3782        value: RuntimeConfigValue,
3783    ) -> Result<(), RuntimeConfigError> {
3784        let (name, setting) = self.split_generic_interface_runtime_key(key)?;
3785        let (id, _) = self
3786            .interfaces
3787            .iter()
3788            .find(|(_, entry)| entry.info.name == name)
3789            .map(|(id, entry)| (*id, entry))
3790            .ok_or(RuntimeConfigError {
3791                code: RuntimeConfigErrorCode::NotFound,
3792                message: format!("interface '{}' not found", name),
3793            })?;
3794        let entry = self.interfaces.get_mut(&id).ok_or(RuntimeConfigError {
3795            code: RuntimeConfigErrorCode::NotFound,
3796            message: format!("interface '{}' not found", name),
3797        })?;
3798        match setting {
3799            "enabled" => {
3800                entry.enabled = Self::expect_bool(value, key)?;
3801            }
3802            "ifac_netname" => {
3803                let runtime =
3804                    self.interface_ifac_runtime
3805                        .get_mut(name)
3806                        .ok_or(RuntimeConfigError {
3807                            code: RuntimeConfigErrorCode::UnknownKey,
3808                            message: format!("unknown runtime-config key '{}'", key),
3809                        })?;
3810                runtime.netname = match value {
3811                    RuntimeConfigValue::Null => None,
3812                    RuntimeConfigValue::String(value) => Some(value),
3813                    _ => {
3814                        return Err(RuntimeConfigError {
3815                            code: RuntimeConfigErrorCode::InvalidType,
3816                            message: format!("{} expects a string or null", key),
3817                        })
3818                    }
3819                };
3820                Self::apply_interface_ifac_runtime(entry, runtime);
3821            }
3822            "ifac_passphrase" => {
3823                let runtime =
3824                    self.interface_ifac_runtime
3825                        .get_mut(name)
3826                        .ok_or(RuntimeConfigError {
3827                            code: RuntimeConfigErrorCode::UnknownKey,
3828                            message: format!("unknown runtime-config key '{}'", key),
3829                        })?;
3830                runtime.netkey = match value {
3831                    RuntimeConfigValue::Null => None,
3832                    RuntimeConfigValue::String(value) => Some(value),
3833                    _ => {
3834                        return Err(RuntimeConfigError {
3835                            code: RuntimeConfigErrorCode::InvalidType,
3836                            message: format!("{} expects a string or null", key),
3837                        })
3838                    }
3839                };
3840                Self::apply_interface_ifac_runtime(entry, runtime);
3841            }
3842            "ifac_size_bytes" => {
3843                let runtime =
3844                    self.interface_ifac_runtime
3845                        .get_mut(name)
3846                        .ok_or(RuntimeConfigError {
3847                            code: RuntimeConfigErrorCode::UnknownKey,
3848                            message: format!("unknown runtime-config key '{}'", key),
3849                        })?;
3850                runtime.size =
3851                    (Self::expect_u64(value, key)? as usize).max(crate::ifac::IFAC_MIN_SIZE);
3852                Self::apply_interface_ifac_runtime(entry, runtime);
3853            }
3854            "mode" => {
3855                entry.info.mode = Self::parse_interface_mode(&value).ok_or(RuntimeConfigError {
3856                    code: RuntimeConfigErrorCode::InvalidValue,
3857                    message: format!("{} must be a valid interface mode", key),
3858                })?;
3859            }
3860            "announce_rate_target" => {
3861                entry.info.announce_rate_target = match value {
3862                    RuntimeConfigValue::Null => None,
3863                    RuntimeConfigValue::Float(v) if v >= 0.0 => Some(v),
3864                    RuntimeConfigValue::Int(v) if v >= 0 => Some(v as f64),
3865                    RuntimeConfigValue::Float(_) | RuntimeConfigValue::Int(_) => {
3866                        return Err(RuntimeConfigError {
3867                            code: RuntimeConfigErrorCode::InvalidValue,
3868                            message: format!("{} must be >= 0", key),
3869                        })
3870                    }
3871                    _ => {
3872                        return Err(RuntimeConfigError {
3873                            code: RuntimeConfigErrorCode::InvalidType,
3874                            message: format!("{} expects a numeric value or null", key),
3875                        })
3876                    }
3877                };
3878            }
3879            "announce_rate_grace" => {
3880                entry.info.announce_rate_grace = Self::expect_u64(value, key)? as u32
3881            }
3882            "announce_rate_penalty" => {
3883                entry.info.announce_rate_penalty = Self::expect_f64(value, key)?
3884            }
3885            "announce_cap" => entry.info.announce_cap = Self::expect_f64(value, key)?,
3886            "ingress_control" => {
3887                entry.info.ingress_control.enabled = Self::expect_bool(value, key)?
3888            }
3889            "ic_max_held_announces" => {
3890                entry.info.ingress_control.max_held_announces =
3891                    Self::expect_u64(value, key)? as usize
3892            }
3893            "ic_burst_hold" => {
3894                entry.info.ingress_control.burst_hold = Self::expect_f64(value, key)?
3895            }
3896            "ic_burst_freq_new" => {
3897                entry.info.ingress_control.burst_freq_new = Self::expect_f64(value, key)?
3898            }
3899            "ic_burst_freq" => {
3900                entry.info.ingress_control.burst_freq = Self::expect_f64(value, key)?
3901            }
3902            "ic_new_time" => entry.info.ingress_control.new_time = Self::expect_f64(value, key)?,
3903            "ic_burst_penalty" => {
3904                entry.info.ingress_control.burst_penalty = Self::expect_f64(value, key)?
3905            }
3906            "ic_held_release_interval" => {
3907                entry.info.ingress_control.held_release_interval = Self::expect_f64(value, key)?
3908            }
3909            _ => {
3910                return Err(RuntimeConfigError {
3911                    code: RuntimeConfigErrorCode::UnknownKey,
3912                    message: format!("unknown runtime-config key '{}'", key),
3913                })
3914            }
3915        }
3916        let info = entry.info.clone();
3917        self.engine.register_interface(info);
3918        Ok(())
3919    }
3920
3921    fn reset_generic_interface_runtime_config(
3922        &mut self,
3923        key: &str,
3924    ) -> Result<(), RuntimeConfigError> {
3925        let (name, setting) = self.split_generic_interface_runtime_key(key)?;
3926        let startup =
3927            self.interface_runtime_defaults
3928                .get(name)
3929                .cloned()
3930                .ok_or(RuntimeConfigError {
3931                    code: RuntimeConfigErrorCode::NotFound,
3932                    message: format!("interface '{}' not found", name),
3933                })?;
3934        let entry = self
3935            .interfaces
3936            .values_mut()
3937            .find(|entry| entry.info.name == name)
3938            .ok_or(RuntimeConfigError {
3939                code: RuntimeConfigErrorCode::NotFound,
3940                message: format!("interface '{}' not found", name),
3941            })?;
3942        match setting {
3943            "enabled" => entry.enabled = true,
3944            "ifac_netname" => {
3945                let startup_ifac =
3946                    self.interface_ifac_runtime_defaults
3947                        .get(name)
3948                        .ok_or(RuntimeConfigError {
3949                            code: RuntimeConfigErrorCode::UnknownKey,
3950                            message: format!("unknown runtime-config key '{}'", key),
3951                        })?;
3952                let runtime =
3953                    self.interface_ifac_runtime
3954                        .get_mut(name)
3955                        .ok_or(RuntimeConfigError {
3956                            code: RuntimeConfigErrorCode::UnknownKey,
3957                            message: format!("unknown runtime-config key '{}'", key),
3958                        })?;
3959                runtime.netname = startup_ifac.netname.clone();
3960                Self::apply_interface_ifac_runtime(entry, runtime);
3961            }
3962            "ifac_passphrase" => {
3963                let startup_ifac =
3964                    self.interface_ifac_runtime_defaults
3965                        .get(name)
3966                        .ok_or(RuntimeConfigError {
3967                            code: RuntimeConfigErrorCode::UnknownKey,
3968                            message: format!("unknown runtime-config key '{}'", key),
3969                        })?;
3970                let runtime =
3971                    self.interface_ifac_runtime
3972                        .get_mut(name)
3973                        .ok_or(RuntimeConfigError {
3974                            code: RuntimeConfigErrorCode::UnknownKey,
3975                            message: format!("unknown runtime-config key '{}'", key),
3976                        })?;
3977                runtime.netkey = startup_ifac.netkey.clone();
3978                Self::apply_interface_ifac_runtime(entry, runtime);
3979            }
3980            "ifac_size_bytes" => {
3981                let startup_ifac =
3982                    self.interface_ifac_runtime_defaults
3983                        .get(name)
3984                        .ok_or(RuntimeConfigError {
3985                            code: RuntimeConfigErrorCode::UnknownKey,
3986                            message: format!("unknown runtime-config key '{}'", key),
3987                        })?;
3988                let runtime =
3989                    self.interface_ifac_runtime
3990                        .get_mut(name)
3991                        .ok_or(RuntimeConfigError {
3992                            code: RuntimeConfigErrorCode::UnknownKey,
3993                            message: format!("unknown runtime-config key '{}'", key),
3994                        })?;
3995                runtime.size = startup_ifac.size;
3996                Self::apply_interface_ifac_runtime(entry, runtime);
3997            }
3998            "mode" => entry.info.mode = startup.mode,
3999            "announce_rate_target" => {
4000                entry.info.announce_rate_target = startup.announce_rate_target
4001            }
4002            "announce_rate_grace" => entry.info.announce_rate_grace = startup.announce_rate_grace,
4003            "announce_rate_penalty" => {
4004                entry.info.announce_rate_penalty = startup.announce_rate_penalty
4005            }
4006            "announce_cap" => entry.info.announce_cap = startup.announce_cap,
4007            "ingress_control" => {
4008                entry.info.ingress_control.enabled = startup.ingress_control.enabled
4009            }
4010            "ic_max_held_announces" => {
4011                entry.info.ingress_control.max_held_announces =
4012                    startup.ingress_control.max_held_announces
4013            }
4014            "ic_burst_hold" => {
4015                entry.info.ingress_control.burst_hold = startup.ingress_control.burst_hold
4016            }
4017            "ic_burst_freq_new" => {
4018                entry.info.ingress_control.burst_freq_new = startup.ingress_control.burst_freq_new
4019            }
4020            "ic_burst_freq" => {
4021                entry.info.ingress_control.burst_freq = startup.ingress_control.burst_freq
4022            }
4023            "ic_new_time" => entry.info.ingress_control.new_time = startup.ingress_control.new_time,
4024            "ic_burst_penalty" => {
4025                entry.info.ingress_control.burst_penalty = startup.ingress_control.burst_penalty
4026            }
4027            "ic_held_release_interval" => {
4028                entry.info.ingress_control.held_release_interval =
4029                    startup.ingress_control.held_release_interval
4030            }
4031            _ => {
4032                return Err(RuntimeConfigError {
4033                    code: RuntimeConfigErrorCode::UnknownKey,
4034                    message: format!("unknown runtime-config key '{}'", key),
4035                })
4036            }
4037        }
4038        let info = entry.info.clone();
4039        self.engine.register_interface(info);
4040        Ok(())
4041    }
4042
4043    #[cfg(feature = "iface-tcp")]
4044    fn tcp_server_runtime_entry(&self, key: &str) -> Option<RuntimeConfigEntry> {
4045        let rest = key.strip_prefix("tcp_server.")?;
4046        let (name, setting) = rest.split_once('.')?;
4047        if matches!(
4048            setting,
4049            "discoverable"
4050                | "discovery_name"
4051                | "announce_interval_secs"
4052                | "reachable_on"
4053                | "stamp_value"
4054                | "latitude"
4055                | "longitude"
4056                | "height"
4057        ) {
4058            let handle = self.tcp_server_discovery_runtime.get(name)?;
4059            let current = &handle.current;
4060            let startup = &handle.startup;
4061            let make_entry = |value: RuntimeConfigValue,
4062                              default: RuntimeConfigValue,
4063                              apply_mode: RuntimeConfigApplyMode,
4064                              description: &str|
4065             -> RuntimeConfigEntry {
4066                RuntimeConfigEntry {
4067                    key: key.to_string(),
4068                    source: if value == default {
4069                        RuntimeConfigSource::Startup
4070                    } else {
4071                        RuntimeConfigSource::RuntimeOverride
4072                    },
4073                    value,
4074                    default,
4075                    apply_mode,
4076                    description: Some(description.to_string()),
4077                }
4078            };
4079            return match setting {
4080                "discoverable" => Some(make_entry(
4081                    RuntimeConfigValue::Bool(current.discoverable),
4082                    RuntimeConfigValue::Bool(startup.discoverable),
4083                    RuntimeConfigApplyMode::Immediate,
4084                    "Whether this TCP server interface is advertised through interface discovery.",
4085                )),
4086                "discovery_name" => Some(make_entry(
4087                    RuntimeConfigValue::String(current.config.discovery_name.clone()),
4088                    RuntimeConfigValue::String(startup.config.discovery_name.clone()),
4089                    RuntimeConfigApplyMode::Immediate,
4090                    "Human-readable discovery name advertised for this TCP server interface.",
4091                )),
4092                "announce_interval_secs" => Some(make_entry(
4093                    RuntimeConfigValue::Int(current.config.announce_interval as i64),
4094                    RuntimeConfigValue::Int(startup.config.announce_interval as i64),
4095                    RuntimeConfigApplyMode::Immediate,
4096                    "Discovery announce interval for this TCP server interface in seconds.",
4097                )),
4098                "reachable_on" => Some(make_entry(
4099                    current
4100                        .config
4101                        .reachable_on
4102                        .clone()
4103                        .map(RuntimeConfigValue::String)
4104                        .unwrap_or(RuntimeConfigValue::Null),
4105                    startup
4106                        .config
4107                        .reachable_on
4108                        .clone()
4109                        .map(RuntimeConfigValue::String)
4110                        .unwrap_or(RuntimeConfigValue::Null),
4111                    RuntimeConfigApplyMode::Immediate,
4112                    "Reachable hostname or IP advertised for this TCP server interface; null clears it.",
4113                )),
4114                "stamp_value" => Some(make_entry(
4115                    RuntimeConfigValue::Int(current.config.stamp_value as i64),
4116                    RuntimeConfigValue::Int(startup.config.stamp_value as i64),
4117                    RuntimeConfigApplyMode::Immediate,
4118                    "Discovery proof-of-work stamp cost for this TCP server interface.",
4119                )),
4120                "latitude" => Some(make_entry(
4121                    current
4122                        .config
4123                        .latitude
4124                        .map(RuntimeConfigValue::Float)
4125                        .unwrap_or(RuntimeConfigValue::Null),
4126                    startup
4127                        .config
4128                        .latitude
4129                        .map(RuntimeConfigValue::Float)
4130                        .unwrap_or(RuntimeConfigValue::Null),
4131                    RuntimeConfigApplyMode::Immediate,
4132                    "Latitude advertised for this TCP server interface; null clears it.",
4133                )),
4134                "longitude" => Some(make_entry(
4135                    current
4136                        .config
4137                        .longitude
4138                        .map(RuntimeConfigValue::Float)
4139                        .unwrap_or(RuntimeConfigValue::Null),
4140                    startup
4141                        .config
4142                        .longitude
4143                        .map(RuntimeConfigValue::Float)
4144                        .unwrap_or(RuntimeConfigValue::Null),
4145                    RuntimeConfigApplyMode::Immediate,
4146                    "Longitude advertised for this TCP server interface; null clears it.",
4147                )),
4148                "height" => Some(make_entry(
4149                    current
4150                        .config
4151                        .height
4152                        .map(RuntimeConfigValue::Float)
4153                        .unwrap_or(RuntimeConfigValue::Null),
4154                    startup
4155                        .config
4156                        .height
4157                        .map(RuntimeConfigValue::Float)
4158                        .unwrap_or(RuntimeConfigValue::Null),
4159                    RuntimeConfigApplyMode::Immediate,
4160                    "Height advertised for this TCP server interface; null clears it.",
4161                )),
4162                _ => None,
4163            };
4164        }
4165
4166        let handle = self.tcp_server_runtime.get(name)?;
4167        let current = recover_mutex_guard(&handle.runtime, "tcp server runtime").clone();
4168        let startup = handle.startup.clone();
4169        match setting {
4170            "max_connections" => Some(RuntimeConfigEntry {
4171                key: key.to_string(),
4172                value: RuntimeConfigValue::Int(current.max_connections.unwrap_or(0) as i64),
4173                default: RuntimeConfigValue::Int(startup.max_connections.unwrap_or(0) as i64),
4174                source: if current.max_connections == startup.max_connections {
4175                    RuntimeConfigSource::Startup
4176                } else {
4177                    RuntimeConfigSource::RuntimeOverride
4178                },
4179                apply_mode: RuntimeConfigApplyMode::NewConnectionsOnly,
4180                description: Some(
4181                    "Maximum simultaneous inbound TCP server connections; 0 disables the cap."
4182                        .to_string(),
4183                ),
4184            }),
4185            _ => None,
4186        }
4187    }
4188
4189    #[cfg(feature = "iface-tcp")]
4190    fn split_tcp_server_runtime_key<'a>(
4191        &self,
4192        key: &'a str,
4193    ) -> Result<(&'a str, &'a str), RuntimeConfigError> {
4194        let rest = key.strip_prefix("tcp_server.").ok_or(RuntimeConfigError {
4195            code: RuntimeConfigErrorCode::UnknownKey,
4196            message: format!("unknown runtime-config key '{}'", key),
4197        })?;
4198        rest.split_once('.').ok_or(RuntimeConfigError {
4199            code: RuntimeConfigErrorCode::UnknownKey,
4200            message: format!("unknown runtime-config key '{}'", key),
4201        })
4202    }
4203
4204    #[cfg(feature = "iface-tcp")]
4205    fn set_tcp_server_runtime_config(
4206        &mut self,
4207        key: &str,
4208        value: RuntimeConfigValue,
4209    ) -> Result<(), RuntimeConfigError> {
4210        let (name, setting) = self.split_tcp_server_runtime_key(key)?;
4211        if matches!(
4212            setting,
4213            "discoverable"
4214                | "discovery_name"
4215                | "announce_interval_secs"
4216                | "reachable_on"
4217                | "stamp_value"
4218                | "latitude"
4219                | "longitude"
4220                | "height"
4221        ) {
4222            return self.set_tcp_server_discovery_runtime_config(key, value);
4223        }
4224        let handle = self
4225            .tcp_server_runtime
4226            .get(name)
4227            .ok_or(RuntimeConfigError {
4228                code: RuntimeConfigErrorCode::NotFound,
4229                message: format!("tcp server interface '{}' not found", name),
4230            })?;
4231        let mut runtime = recover_mutex_guard(&handle.runtime, "tcp server runtime");
4232        match setting {
4233            "max_connections" => {
4234                runtime.max_connections = Self::set_optional_usize(value, key)?;
4235                Ok(())
4236            }
4237            _ => Err(RuntimeConfigError {
4238                code: RuntimeConfigErrorCode::UnknownKey,
4239                message: format!("unknown runtime-config key '{}'", key),
4240            }),
4241        }
4242    }
4243
4244    #[cfg(feature = "iface-tcp")]
4245    fn reset_tcp_server_runtime_config(&mut self, key: &str) -> Result<(), RuntimeConfigError> {
4246        let (name, setting) = self.split_tcp_server_runtime_key(key)?;
4247        if matches!(
4248            setting,
4249            "discoverable"
4250                | "discovery_name"
4251                | "announce_interval_secs"
4252                | "reachable_on"
4253                | "stamp_value"
4254                | "latitude"
4255                | "longitude"
4256                | "height"
4257        ) {
4258            return self.reset_tcp_server_discovery_runtime_config(key);
4259        }
4260        let handle = self
4261            .tcp_server_runtime
4262            .get(name)
4263            .ok_or(RuntimeConfigError {
4264                code: RuntimeConfigErrorCode::NotFound,
4265                message: format!("tcp server interface '{}' not found", name),
4266            })?;
4267        let mut runtime = recover_mutex_guard(&handle.runtime, "tcp server runtime");
4268        let startup = handle.startup.clone();
4269        match setting {
4270            "max_connections" => runtime.max_connections = startup.max_connections,
4271            _ => {
4272                return Err(RuntimeConfigError {
4273                    code: RuntimeConfigErrorCode::UnknownKey,
4274                    message: format!("unknown runtime-config key '{}'", key),
4275                })
4276            }
4277        }
4278        Ok(())
4279    }
4280
4281    #[cfg(feature = "iface-tcp")]
4282    fn set_tcp_server_discovery_runtime_config(
4283        &mut self,
4284        key: &str,
4285        value: RuntimeConfigValue,
4286    ) -> Result<(), RuntimeConfigError> {
4287        let (name, setting) = self.split_tcp_server_runtime_key(key)?;
4288        let handle = self
4289            .tcp_server_discovery_runtime
4290            .get_mut(name)
4291            .ok_or(RuntimeConfigError {
4292                code: RuntimeConfigErrorCode::NotFound,
4293                message: format!("tcp server interface '{}' not found", name),
4294            })?;
4295        match setting {
4296            "discoverable" => handle.current.discoverable = Self::expect_bool(value, key)?,
4297            "discovery_name" => {
4298                handle.current.config.discovery_name = Self::expect_string(value, key)?
4299            }
4300            "announce_interval_secs" => {
4301                let secs = Self::expect_u64(value, key)?;
4302                if secs < 300 {
4303                    return Err(RuntimeConfigError {
4304                        code: RuntimeConfigErrorCode::InvalidValue,
4305                        message: format!("{} must be >= 300", key),
4306                    });
4307                }
4308                handle.current.config.announce_interval = secs;
4309            }
4310            "reachable_on" => {
4311                handle.current.config.reachable_on = Self::expect_optional_string(value, key)?
4312            }
4313            "stamp_value" => {
4314                let raw = Self::expect_u64(value, key)?;
4315                if raw > u8::MAX as u64 {
4316                    return Err(RuntimeConfigError {
4317                        code: RuntimeConfigErrorCode::InvalidValue,
4318                        message: format!("{} must be <= {}", key, u8::MAX),
4319                    });
4320                }
4321                handle.current.config.stamp_value = raw as u8;
4322            }
4323            "latitude" => handle.current.config.latitude = Self::expect_optional_f64(value, key)?,
4324            "longitude" => handle.current.config.longitude = Self::expect_optional_f64(value, key)?,
4325            "height" => handle.current.config.height = Self::expect_optional_f64(value, key)?,
4326            _ => {
4327                return Err(RuntimeConfigError {
4328                    code: RuntimeConfigErrorCode::UnknownKey,
4329                    message: format!("unknown runtime-config key '{}'", key),
4330                })
4331            }
4332        }
4333        self.sync_tcp_server_discovery_runtime(name)
4334    }
4335
4336    #[cfg(feature = "iface-tcp")]
4337    fn reset_tcp_server_discovery_runtime_config(
4338        &mut self,
4339        key: &str,
4340    ) -> Result<(), RuntimeConfigError> {
4341        let (name, setting) = self.split_tcp_server_runtime_key(key)?;
4342        let handle = self
4343            .tcp_server_discovery_runtime
4344            .get_mut(name)
4345            .ok_or(RuntimeConfigError {
4346                code: RuntimeConfigErrorCode::NotFound,
4347                message: format!("tcp server interface '{}' not found", name),
4348            })?;
4349        match setting {
4350            "discoverable" => handle.current.discoverable = handle.startup.discoverable,
4351            "discovery_name" => {
4352                handle.current.config.discovery_name = handle.startup.config.discovery_name.clone()
4353            }
4354            "announce_interval_secs" => {
4355                handle.current.config.announce_interval = handle.startup.config.announce_interval
4356            }
4357            "reachable_on" => {
4358                handle.current.config.reachable_on = handle.startup.config.reachable_on.clone()
4359            }
4360            "stamp_value" => handle.current.config.stamp_value = handle.startup.config.stamp_value,
4361            "latitude" => handle.current.config.latitude = handle.startup.config.latitude,
4362            "longitude" => handle.current.config.longitude = handle.startup.config.longitude,
4363            "height" => handle.current.config.height = handle.startup.config.height,
4364            _ => {
4365                return Err(RuntimeConfigError {
4366                    code: RuntimeConfigErrorCode::UnknownKey,
4367                    message: format!("unknown runtime-config key '{}'", key),
4368                })
4369            }
4370        }
4371        self.sync_tcp_server_discovery_runtime(name)
4372    }
4373
4374    #[cfg(feature = "iface-backbone")]
4375    fn list_backbone_runtime_config(&self) -> Vec<RuntimeConfigEntry> {
4376        let mut entries = Vec::new();
4377        let mut names: Vec<&String> = self.backbone_runtime.keys().collect();
4378        names.sort();
4379        for name in names {
4380            for suffix in [
4381                "idle_timeout_secs",
4382                "write_stall_timeout_secs",
4383                "max_penalty_duration_secs",
4384                "max_connections",
4385            ] {
4386                let key = format!("backbone.{}.{}", name, suffix);
4387                if let Some(entry) = self.backbone_runtime_entry(&key) {
4388                    entries.push(entry);
4389                }
4390            }
4391            for suffix in [
4392                "discoverable",
4393                "discovery_name",
4394                "announce_interval_secs",
4395                "reachable_on",
4396                "stamp_value",
4397                "latitude",
4398                "longitude",
4399                "height",
4400            ] {
4401                let key = format!("backbone.{}.{}", name, suffix);
4402                if let Some(entry) = self.backbone_runtime_entry(&key) {
4403                    entries.push(entry);
4404                }
4405            }
4406        }
4407        entries
4408    }
4409
4410    #[cfg(feature = "iface-backbone")]
4411    fn backbone_runtime_entry(&self, key: &str) -> Option<RuntimeConfigEntry> {
4412        let rest = key.strip_prefix("backbone.")?;
4413        let (name, setting) = rest.split_once('.')?;
4414
4415        let make_entry = |value: RuntimeConfigValue,
4416                          default: RuntimeConfigValue,
4417                          apply_mode: RuntimeConfigApplyMode,
4418                          description: &str| RuntimeConfigEntry {
4419            key: key.to_string(),
4420            source: if value == default {
4421                RuntimeConfigSource::Startup
4422            } else {
4423                RuntimeConfigSource::RuntimeOverride
4424            },
4425            value,
4426            default,
4427            apply_mode,
4428            description: Some(description.to_string()),
4429        };
4430
4431        if matches!(
4432            setting,
4433            "discoverable"
4434                | "discovery_name"
4435                | "announce_interval_secs"
4436                | "reachable_on"
4437                | "stamp_value"
4438                | "latitude"
4439                | "longitude"
4440                | "height"
4441        ) {
4442            let handle = self.backbone_discovery_runtime.get(name)?;
4443            let current = &handle.current;
4444            let startup = &handle.startup;
4445            return match setting {
4446                "discoverable" => Some(make_entry(
4447                    RuntimeConfigValue::Bool(current.discoverable),
4448                    RuntimeConfigValue::Bool(startup.discoverable),
4449                    RuntimeConfigApplyMode::Immediate,
4450                    "Whether this backbone interface is advertised through interface discovery.",
4451                )),
4452                "discovery_name" => Some(make_entry(
4453                    RuntimeConfigValue::String(current.config.discovery_name.clone()),
4454                    RuntimeConfigValue::String(startup.config.discovery_name.clone()),
4455                    RuntimeConfigApplyMode::Immediate,
4456                    "Human-readable discovery name advertised for this backbone interface.",
4457                )),
4458                "announce_interval_secs" => Some(make_entry(
4459                    RuntimeConfigValue::Int(current.config.announce_interval as i64),
4460                    RuntimeConfigValue::Int(startup.config.announce_interval as i64),
4461                    RuntimeConfigApplyMode::Immediate,
4462                    "Discovery announce interval for this backbone interface in seconds.",
4463                )),
4464                "reachable_on" => Some(make_entry(
4465                    current
4466                        .config
4467                        .reachable_on
4468                        .clone()
4469                        .map(RuntimeConfigValue::String)
4470                        .unwrap_or(RuntimeConfigValue::Null),
4471                    startup
4472                        .config
4473                        .reachable_on
4474                        .clone()
4475                        .map(RuntimeConfigValue::String)
4476                        .unwrap_or(RuntimeConfigValue::Null),
4477                    RuntimeConfigApplyMode::Immediate,
4478                    "Reachable hostname or IP advertised for this backbone interface; null clears it.",
4479                )),
4480                "stamp_value" => Some(make_entry(
4481                    RuntimeConfigValue::Int(current.config.stamp_value as i64),
4482                    RuntimeConfigValue::Int(startup.config.stamp_value as i64),
4483                    RuntimeConfigApplyMode::Immediate,
4484                    "Discovery proof-of-work stamp cost for this backbone interface.",
4485                )),
4486                "latitude" => Some(make_entry(
4487                    current
4488                        .config
4489                        .latitude
4490                        .map(RuntimeConfigValue::Float)
4491                        .unwrap_or(RuntimeConfigValue::Null),
4492                    startup
4493                        .config
4494                        .latitude
4495                        .map(RuntimeConfigValue::Float)
4496                        .unwrap_or(RuntimeConfigValue::Null),
4497                    RuntimeConfigApplyMode::Immediate,
4498                    "Latitude advertised for this backbone interface; null clears it.",
4499                )),
4500                "longitude" => Some(make_entry(
4501                    current
4502                        .config
4503                        .longitude
4504                        .map(RuntimeConfigValue::Float)
4505                        .unwrap_or(RuntimeConfigValue::Null),
4506                    startup
4507                        .config
4508                        .longitude
4509                        .map(RuntimeConfigValue::Float)
4510                        .unwrap_or(RuntimeConfigValue::Null),
4511                    RuntimeConfigApplyMode::Immediate,
4512                    "Longitude advertised for this backbone interface; null clears it.",
4513                )),
4514                "height" => Some(make_entry(
4515                    current
4516                        .config
4517                        .height
4518                        .map(RuntimeConfigValue::Float)
4519                        .unwrap_or(RuntimeConfigValue::Null),
4520                    startup
4521                        .config
4522                        .height
4523                        .map(RuntimeConfigValue::Float)
4524                        .unwrap_or(RuntimeConfigValue::Null),
4525                    RuntimeConfigApplyMode::Immediate,
4526                    "Height advertised for this backbone interface; null clears it.",
4527                )),
4528                _ => None,
4529            };
4530        }
4531
4532        if let Some(handle) = self.backbone_runtime.get(name) {
4533            let current = recover_mutex_guard(&handle.runtime, "backbone runtime").clone();
4534            let startup = handle.startup.clone();
4535            return match setting {
4536                "idle_timeout_secs" => Some(make_entry(
4537                    RuntimeConfigValue::Float(current.idle_timeout.map(|d| d.as_secs_f64()).unwrap_or(0.0)),
4538                    RuntimeConfigValue::Float(startup.idle_timeout.map(|d| d.as_secs_f64()).unwrap_or(0.0)),
4539                    RuntimeConfigApplyMode::Immediate,
4540                    "Disconnect silent inbound peers after this many seconds; 0 disables the timeout.",
4541                )),
4542                "write_stall_timeout_secs" => Some(make_entry(
4543                    RuntimeConfigValue::Float(current.write_stall_timeout.map(|d| d.as_secs_f64()).unwrap_or(0.0)),
4544                    RuntimeConfigValue::Float(startup.write_stall_timeout.map(|d| d.as_secs_f64()).unwrap_or(0.0)),
4545                    RuntimeConfigApplyMode::Immediate,
4546                    "Disconnect peers whose send buffer remains unwritable for this many seconds; 0 disables the timeout.",
4547                )),
4548                "max_penalty_duration_secs" => Some(make_entry(
4549                    RuntimeConfigValue::Float(current.abuse.max_penalty_duration.map(|d| d.as_secs_f64()).unwrap_or(0.0)),
4550                    RuntimeConfigValue::Float(startup.abuse.max_penalty_duration.map(|d| d.as_secs_f64()).unwrap_or(0.0)),
4551                    RuntimeConfigApplyMode::Immediate,
4552                    "Maximum accepted backbone blacklist duration; 0 means no cap.",
4553                )),
4554                "max_connections" => Some(make_entry(
4555                    RuntimeConfigValue::Int(current.max_connections.unwrap_or(0) as i64),
4556                    RuntimeConfigValue::Int(startup.max_connections.unwrap_or(0) as i64),
4557                    RuntimeConfigApplyMode::NewConnectionsOnly,
4558                    "Maximum simultaneous inbound backbone connections; 0 disables the cap.",
4559                )),
4560                _ => None,
4561            };
4562        }
4563
4564        None
4565    }
4566
4567    #[cfg(feature = "rns-hooks")]
4568    fn forward_hook_side_effects(&mut self, attach_point: &str, exec: &rns_hooks::ExecuteResult) {
4569        if !exec.injected_actions.is_empty() {
4570            self.dispatch_all(convert_injected_actions(exec.injected_actions.clone()));
4571        }
4572        if let Some(ref bridge) = self.provider_bridge {
4573            for event in &exec.provider_events {
4574                bridge.emit_event(
4575                    attach_point,
4576                    event.hook_name.clone(),
4577                    event.payload_type.clone(),
4578                    event.payload.clone(),
4579                );
4580            }
4581        }
4582    }
4583
4584    #[cfg(feature = "rns-hooks")]
4585    fn collect_hook_side_effects(
4586        &mut self,
4587        attach_point: &str,
4588        exec: &rns_hooks::ExecuteResult,
4589        out: &mut Vec<TransportAction>,
4590    ) {
4591        if !exec.injected_actions.is_empty() {
4592            out.extend(convert_injected_actions(exec.injected_actions.clone()));
4593        }
4594        if let Some(ref bridge) = self.provider_bridge {
4595            for event in &exec.provider_events {
4596                bridge.emit_event(
4597                    attach_point,
4598                    event.hook_name.clone(),
4599                    event.payload_type.clone(),
4600                    event.payload.clone(),
4601                );
4602            }
4603        }
4604    }
4605
4606    /// Set the probe addresses, protocol, and optional device for hole punching.
4607    pub fn set_probe_config(
4608        &mut self,
4609        addrs: Vec<std::net::SocketAddr>,
4610        protocol: rns_core::holepunch::ProbeProtocol,
4611        device: Option<String>,
4612    ) {
4613        self.holepunch_manager = HolePunchManager::new(addrs, protocol, device);
4614    }
4615
4616    fn handle_frame_event(&mut self, interface_id: InterfaceId, data: Vec<u8>) {
4617        if data.len() > 2 && (data[0] & 0x03) == 0x01 {
4618            log::debug!(
4619                "Announce:frame from iface {} (len={}, flags=0x{:02x})",
4620                interface_id.0,
4621                data.len(),
4622                data[0]
4623            );
4624        }
4625        if let Some(entry) = self.interfaces.get(&interface_id) {
4626            if !entry.enabled || !entry.online {
4627                return;
4628            }
4629        }
4630        if let Some(entry) = self.interfaces.get_mut(&interface_id) {
4631            entry.stats.rxb += data.len() as u64;
4632            entry.stats.rx_packets += 1;
4633        }
4634
4635        let packet = if let Some(entry) = self.interfaces.get(&interface_id) {
4636            if let Some(ref ifac_state) = entry.ifac {
4637                match ifac::unmask_inbound(&data, ifac_state) {
4638                    Some(unmasked) => unmasked,
4639                    None => {
4640                        log::debug!("[{}] IFAC rejected packet", interface_id.0);
4641                        return;
4642                    }
4643                }
4644            } else {
4645                if data.len() > 2 && data[0] & 0x80 == 0x80 {
4646                    log::debug!(
4647                        "[{}] dropping packet with IFAC flag on non-IFAC interface",
4648                        interface_id.0
4649                    );
4650                    return;
4651                }
4652                data
4653            }
4654        } else {
4655            data
4656        };
4657
4658        #[cfg(feature = "rns-hooks")]
4659        {
4660            let pkt_ctx = rns_hooks::PacketContext {
4661                flags: if packet.is_empty() { 0 } else { packet[0] },
4662                hops: if packet.len() > 1 { packet[1] } else { 0 },
4663                destination_hash: extract_dest_hash(&packet),
4664                context: 0,
4665                packet_hash: [0; 32],
4666                interface_id: interface_id.0,
4667                data_offset: 0,
4668                data_len: packet.len() as u32,
4669            };
4670            let ctx = HookContext::Packet {
4671                ctx: &pkt_ctx,
4672                raw: &packet,
4673            };
4674            let now = time::now();
4675            let engine_ref = EngineRef {
4676                engine: &self.engine,
4677                interfaces: &self.interfaces,
4678                link_manager: &self.link_manager,
4679                now,
4680            };
4681            let provider_events_enabled = self.provider_events_enabled();
4682            if let Some(ref e) = run_hook_inner(
4683                &mut self.hook_slots[HookPoint::PreIngress as usize].programs,
4684                &self.hook_manager,
4685                &engine_ref,
4686                &ctx,
4687                now,
4688                provider_events_enabled,
4689            ) {
4690                self.forward_hook_side_effects("PreIngress", e);
4691                if e.hook_result.as_ref().is_some_and(|r| r.is_drop()) {
4692                    return;
4693                }
4694            }
4695        }
4696
4697        if packet.len() > 2 && (packet[0] & 0x03) == 0x01 {
4698            let now = time::now();
4699            if let Some(entry) = self.interfaces.get_mut(&interface_id) {
4700                entry.stats.record_incoming_announce(now);
4701            }
4702        }
4703
4704        if let Some(entry) = self.interfaces.get(&interface_id) {
4705            self.engine
4706                .update_interface_freq(interface_id, entry.stats.incoming_announce_freq());
4707        }
4708
4709        let actions = if self.async_announce_verification {
4710            let mut announce_queue = self
4711                .announce_verify_queue
4712                .lock()
4713                .unwrap_or_else(|poisoned| poisoned.into_inner());
4714            self.engine.handle_inbound_with_announce_queue(
4715                &packet,
4716                interface_id,
4717                time::now(),
4718                &mut self.rng,
4719                Some(&mut announce_queue),
4720            )
4721        } else {
4722            self.engine
4723                .handle_inbound(&packet, interface_id, time::now(), &mut self.rng)
4724        };
4725
4726        #[cfg(feature = "rns-hooks")]
4727        {
4728            let pkt_ctx = rns_hooks::PacketContext {
4729                flags: if packet.is_empty() { 0 } else { packet[0] },
4730                hops: if packet.len() > 1 { packet[1] } else { 0 },
4731                destination_hash: extract_dest_hash(&packet),
4732                context: 0,
4733                packet_hash: [0; 32],
4734                interface_id: interface_id.0,
4735                data_offset: 0,
4736                data_len: packet.len() as u32,
4737            };
4738            let ctx = HookContext::Packet {
4739                ctx: &pkt_ctx,
4740                raw: &packet,
4741            };
4742            let now = time::now();
4743            let engine_ref = EngineRef {
4744                engine: &self.engine,
4745                interfaces: &self.interfaces,
4746                link_manager: &self.link_manager,
4747                now,
4748            };
4749            let provider_events_enabled = self.provider_events_enabled();
4750            if let Some(ref e) = run_hook_inner(
4751                &mut self.hook_slots[HookPoint::PreDispatch as usize].programs,
4752                &self.hook_manager,
4753                &engine_ref,
4754                &ctx,
4755                now,
4756                provider_events_enabled,
4757            ) {
4758                self.forward_hook_side_effects("PreDispatch", e);
4759            }
4760        }
4761
4762        self.dispatch_all(actions);
4763    }
4764
4765    fn handle_announce_verified_event(
4766        &mut self,
4767        key: rns_core::transport::announce_verify_queue::AnnounceVerifyKey,
4768        validated: rns_core::announce::ValidatedAnnounce,
4769        sig_cache_key: [u8; 32],
4770    ) {
4771        let pending = {
4772            let mut announce_queue = self
4773                .announce_verify_queue
4774                .lock()
4775                .unwrap_or_else(|poisoned| poisoned.into_inner());
4776            announce_queue.complete_success(&key)
4777        };
4778        if let Some(pending) = pending {
4779            let actions = self.engine.complete_verified_announce(
4780                pending,
4781                validated,
4782                sig_cache_key,
4783                time::now(),
4784                &mut self.rng,
4785            );
4786            self.dispatch_all(actions);
4787        }
4788    }
4789
4790    fn handle_tick_event(&mut self) {
4791        #[cfg(feature = "rns-hooks")]
4792        {
4793            let ctx = HookContext::Tick;
4794            let now = time::now();
4795            let engine_ref = EngineRef {
4796                engine: &self.engine,
4797                interfaces: &self.interfaces,
4798                link_manager: &self.link_manager,
4799                now,
4800            };
4801            let provider_events_enabled = self.provider_events_enabled();
4802            if let Some(ref e) = run_hook_inner(
4803                &mut self.hook_slots[HookPoint::Tick as usize].programs,
4804                &self.hook_manager,
4805                &engine_ref,
4806                &ctx,
4807                now,
4808                provider_events_enabled,
4809            ) {
4810                self.forward_hook_side_effects("Tick", e);
4811            }
4812        }
4813
4814        let now = time::now();
4815        for (id, entry) in &self.interfaces {
4816            self.engine
4817                .update_interface_freq(*id, entry.stats.incoming_announce_freq());
4818        }
4819        let actions = self.engine.tick(now, &mut self.rng);
4820        self.dispatch_all(actions);
4821        let link_actions = self.link_manager.tick(&mut self.rng);
4822        self.dispatch_link_actions(link_actions);
4823        self.enforce_drain_deadline();
4824        {
4825            let tx = self.get_event_sender();
4826            let hp_actions = self.holepunch_manager.tick(&tx);
4827            self.dispatch_holepunch_actions(hp_actions);
4828        }
4829        self.tick_management_announces(now);
4830        self.sent_packets
4831            .retain(|_, (_, sent_time)| now - *sent_time < 60.0);
4832        self.completed_proofs
4833            .retain(|_, (_, received)| now - *received < 120.0);
4834
4835        self.tick_discovery_announcer(now);
4836        #[cfg(feature = "iface-backbone")]
4837        self.maintain_backbone_peer_pool();
4838
4839        self.memory_stats_counter += 1;
4840        if self.memory_stats_counter >= 300 {
4841            self.memory_stats_counter = 0;
4842            self.log_memory_stats();
4843        }
4844
4845        if self.discover_interfaces {
4846            self.discovery_cleanup_counter += 1;
4847            if self.discovery_cleanup_counter >= self.discovery_cleanup_interval_ticks {
4848                self.discovery_cleanup_counter = 0;
4849                if let Ok(removed) = self.discovered_interfaces.cleanup() {
4850                    if removed > 0 {
4851                        log::info!("Discovery cleanup: removed {} stale entries", removed);
4852                    }
4853                }
4854            }
4855        }
4856
4857        self.cache_cleanup_counter += 1;
4858        if self.cache_cleanup_counter >= self.known_destinations_cleanup_interval_ticks {
4859            self.cache_cleanup_counter = 0;
4860
4861            let active_dests = self.engine.active_destination_hashes();
4862            let ttl = self.known_destinations_ttl;
4863            let kd_before = self.known_destinations.len();
4864            self.known_destinations.retain(|k, state| {
4865                active_dests.contains(k)
4866                    || self.local_destinations.contains_key(k)
4867                    || state.retained
4868                    || now - Self::known_destination_relevance_time(state) < ttl
4869            });
4870            let kd_removed = kd_before - self.known_destinations.len();
4871            let kd_evicted = self.enforce_known_destination_cap(false);
4872            let rl_removed =
4873                self.engine
4874                    .cull_rate_limiter(&active_dests, now, self.rate_limiter_ttl_secs);
4875
4876            if kd_removed > 0 || kd_evicted > 0 || rl_removed > 0 {
4877                log::info!(
4878                    "Memory cleanup: removed {} known_destinations, evicted {} known_destinations, {} rate_limiter entries",
4879                    kd_removed, kd_evicted, rl_removed
4880                );
4881            }
4882        }
4883
4884        self.announce_cache_cleanup_counter += 1;
4885        if self.announce_cache_cleanup_counter >= self.announce_cache_cleanup_interval_ticks {
4886            self.announce_cache_cleanup_counter = 0;
4887            if self.announce_cache.is_some() && self.cache_cleanup_active_hashes.is_none() {
4888                self.cache_cleanup_active_hashes = Some(self.engine.active_packet_hashes());
4889                self.cache_cleanup_entries = None;
4890                self.cache_cleanup_removed = 0;
4891            }
4892        }
4893
4894        if self.cache_cleanup_active_hashes.is_some() {
4895            if let Some(ref cache) = self.announce_cache {
4896                if self.cache_cleanup_entries.is_none() {
4897                    match cache.entries() {
4898                        Ok(entries) => self.cache_cleanup_entries = Some(entries),
4899                        Err(e) => {
4900                            log::warn!("Announce cache cleanup failed to open directory: {}", e);
4901                            self.cache_cleanup_active_hashes = None;
4902                            self.cache_cleanup_entries = None;
4903                        }
4904                    }
4905                }
4906            }
4907
4908            if let Some(ref cache) = self.announce_cache {
4909                let Some(active_hashes) = self.cache_cleanup_active_hashes.as_ref() else {
4910                    self.cache_cleanup_entries = None;
4911                    return;
4912                };
4913                let entries = match self.cache_cleanup_entries.as_mut() {
4914                    Some(entries) => entries,
4915                    None => return,
4916                };
4917                match cache.clean_batch(
4918                    active_hashes,
4919                    entries,
4920                    self.announce_cache_cleanup_batch_size,
4921                ) {
4922                    Ok((removed, finished)) => {
4923                        self.cache_cleanup_removed += removed;
4924                        if finished {
4925                            if self.cache_cleanup_removed > 0 {
4926                                log::info!(
4927                                    "Announce cache cleanup complete: removed {} stale files",
4928                                    self.cache_cleanup_removed
4929                                );
4930                            }
4931                            self.cache_cleanup_active_hashes = None;
4932                            self.cache_cleanup_entries = None;
4933                        }
4934                    }
4935                    Err(e) => {
4936                        log::warn!("Announce cache cleanup failed: {}", e);
4937                        self.cache_cleanup_active_hashes = None;
4938                        self.cache_cleanup_entries = None;
4939                    }
4940                }
4941            } else {
4942                self.cache_cleanup_active_hashes = None;
4943                self.cache_cleanup_entries = None;
4944            }
4945        }
4946    }
4947
4948    fn handle_interface_up_event(
4949        &mut self,
4950        id: InterfaceId,
4951        new_writer: Option<Box<dyn crate::interface::Writer>>,
4952        info: Option<rns_core::transport::types::InterfaceInfo>,
4953    ) {
4954        let wants_tunnel;
4955        let mut replay_shared_announces = false;
4956        if let Some(mut info) = info {
4957            log::info!("[{}] dynamic interface registered", id.0);
4958            wants_tunnel = info.wants_tunnel;
4959            let iface_type = infer_interface_type(&info.name);
4960            info.started = time::now();
4961            self.register_interface_runtime_defaults(&info);
4962            self.engine.register_interface(info.clone());
4963            if let Some(writer) = new_writer {
4964                let (writer, async_writer_metrics) =
4965                    self.wrap_interface_writer(id, &info.name, writer);
4966                self.interfaces.insert(
4967                    id,
4968                    InterfaceEntry {
4969                        id,
4970                        info,
4971                        writer,
4972                        async_writer_metrics: Some(async_writer_metrics),
4973                        enabled: true,
4974                        online: true,
4975                        dynamic: true,
4976                        ifac: None,
4977                        stats: InterfaceStats {
4978                            started: time::now(),
4979                            ..Default::default()
4980                        },
4981                        interface_type: iface_type,
4982                        send_retry_at: None,
4983                        send_retry_backoff: Duration::ZERO,
4984                    },
4985                );
4986            }
4987            self.callbacks.on_interface_up(id);
4988            #[cfg(feature = "rns-hooks")]
4989            {
4990                let ctx = HookContext::Interface { interface_id: id.0 };
4991                let now = time::now();
4992                let engine_ref = EngineRef {
4993                    engine: &self.engine,
4994                    interfaces: &self.interfaces,
4995                    link_manager: &self.link_manager,
4996                    now,
4997                };
4998                let provider_events_enabled = self.provider_events_enabled();
4999                if let Some(ref e) = run_hook_inner(
5000                    &mut self.hook_slots[HookPoint::InterfaceUp as usize].programs,
5001                    &self.hook_manager,
5002                    &engine_ref,
5003                    &ctx,
5004                    now,
5005                    provider_events_enabled,
5006                ) {
5007                    self.forward_hook_side_effects("InterfaceUp", e);
5008                }
5009            }
5010        } else {
5011            let is_local_client = self
5012                .interfaces
5013                .get(&id)
5014                .map(|entry| entry.info.is_local_client)
5015                .unwrap_or(false);
5016            replay_shared_announces =
5017                is_local_client && self.shared_reconnect_pending.remove(&id).unwrap_or(false);
5018            let interface_name = self
5019                .interfaces
5020                .get(&id)
5021                .map(|entry| entry.info.name.clone())
5022                .unwrap_or_else(|| format!("iface-{}", id.0));
5023            let wrapped_writer =
5024                new_writer.map(|writer| self.wrap_interface_writer(id, &interface_name, writer));
5025            if let Some(entry) = self.interfaces.get_mut(&id) {
5026                log::info!("[{}] interface online", id.0);
5027                wants_tunnel = entry.info.wants_tunnel;
5028                entry.online = true;
5029                if let Some((writer, async_writer_metrics)) = wrapped_writer {
5030                    log::info!("[{}] writer refreshed after reconnect", id.0);
5031                    entry.writer = writer;
5032                    entry.async_writer_metrics = Some(async_writer_metrics);
5033                }
5034                self.callbacks.on_interface_up(id);
5035                #[cfg(feature = "rns-hooks")]
5036                {
5037                    let ctx = HookContext::Interface { interface_id: id.0 };
5038                    let now = time::now();
5039                    let engine_ref = EngineRef {
5040                        engine: &self.engine,
5041                        interfaces: &self.interfaces,
5042                        link_manager: &self.link_manager,
5043                        now,
5044                    };
5045                    let provider_events_enabled = self.provider_events_enabled();
5046                    if let Some(ref e) = run_hook_inner(
5047                        &mut self.hook_slots[HookPoint::InterfaceUp as usize].programs,
5048                        &self.hook_manager,
5049                        &engine_ref,
5050                        &ctx,
5051                        now,
5052                        provider_events_enabled,
5053                    ) {
5054                        self.forward_hook_side_effects("InterfaceUp", e);
5055                    }
5056                }
5057            } else {
5058                wants_tunnel = false;
5059            }
5060        }
5061
5062        if wants_tunnel {
5063            self.synthesize_tunnel_for_interface(id);
5064        }
5065        if replay_shared_announces {
5066            self.replay_shared_announces();
5067        }
5068    }
5069
5070    fn handle_interface_down_event(&mut self, id: InterfaceId) {
5071        if let Some(entry) = self.interfaces.get(&id) {
5072            if let Some(tunnel_id) = entry.info.tunnel_id {
5073                self.engine.void_tunnel_interface(&tunnel_id);
5074            }
5075        }
5076
5077        if let Some(entry) = self.interfaces.get(&id) {
5078            let is_dynamic = entry.dynamic;
5079            let is_local_client = entry.info.is_local_client;
5080            let interface_name = entry.info.name.clone();
5081            if is_dynamic {
5082                log::info!("[{}] dynamic interface removed", id.0);
5083                self.interface_runtime_defaults.remove(&interface_name);
5084                self.engine.deregister_interface(id);
5085                self.interfaces.remove(&id);
5086            } else {
5087                log::info!("[{}] interface offline", id.0);
5088                if let Some(entry) = self.interfaces.get_mut(&id) {
5089                    entry.online = false;
5090                } else {
5091                    log::warn!(
5092                        "interface {} disappeared while handling interface-down",
5093                        id.0
5094                    );
5095                    return;
5096                }
5097                if is_local_client {
5098                    self.handle_shared_interface_down(id);
5099                }
5100            }
5101            self.callbacks.on_interface_down(id);
5102            #[cfg(feature = "rns-hooks")]
5103            {
5104                let ctx = HookContext::Interface { interface_id: id.0 };
5105                let now = time::now();
5106                let engine_ref = EngineRef {
5107                    engine: &self.engine,
5108                    interfaces: &self.interfaces,
5109                    link_manager: &self.link_manager,
5110                    now,
5111                };
5112                let provider_events_enabled = self.provider_events_enabled();
5113                if let Some(ref e) = run_hook_inner(
5114                    &mut self.hook_slots[HookPoint::InterfaceDown as usize].programs,
5115                    &self.hook_manager,
5116                    &engine_ref,
5117                    &ctx,
5118                    now,
5119                    provider_events_enabled,
5120                ) {
5121                    self.forward_hook_side_effects("InterfaceDown", e);
5122                }
5123            }
5124        }
5125        #[cfg(feature = "iface-backbone")]
5126        self.handle_backbone_peer_pool_down(id);
5127    }
5128
5129    fn known_destination_route_hint(&self, dest_hash: &[u8; 16]) -> Option<(InterfaceId, u8)> {
5130        let announced = &self.known_destinations.get(dest_hash)?.announced;
5131        let iface = announced.receiving_interface;
5132        if iface.0 == 0 {
5133            return None;
5134        }
5135
5136        self.interfaces
5137            .get(&iface)
5138            .filter(|entry| entry.online)
5139            .map(|_| (iface, announced.hops))
5140    }
5141
5142    fn handle_send_outbound_event(
5143        &mut self,
5144        raw: Vec<u8>,
5145        dest_type: u8,
5146        attached_interface: Option<InterfaceId>,
5147    ) {
5148        if self.is_draining() {
5149            self.reject_new_work("send outbound packet");
5150            return;
5151        }
5152        match RawPacket::unpack(&raw) {
5153            Ok(packet) => {
5154                let is_announce =
5155                    packet.flags.packet_type == rns_core::constants::PACKET_TYPE_ANNOUNCE;
5156                if is_announce {
5157                    log::debug!(
5158                        "SendOutbound: ANNOUNCE for {:02x?} (len={}, dest_type={}, attached={:?})",
5159                        &packet.destination_hash[..4],
5160                        raw.len(),
5161                        dest_type,
5162                        attached_interface
5163                    );
5164                }
5165                if packet.flags.packet_type == rns_core::constants::PACKET_TYPE_DATA {
5166                    self.sent_packets
5167                        .insert(packet.packet_hash, (packet.destination_hash, time::now()));
5168                }
5169                let actions = self.engine.handle_outbound(
5170                    &packet,
5171                    dest_type,
5172                    attached_interface,
5173                    time::now(),
5174                );
5175                if is_announce {
5176                    log::debug!(
5177                        "SendOutbound: announce routed to {} actions: {:?}",
5178                        actions.len(),
5179                        actions
5180                            .iter()
5181                            .map(|a| match a {
5182                                TransportAction::SendOnInterface { interface, .. } =>
5183                                    format!("SendOn({})", interface.0),
5184                                TransportAction::BroadcastOnAllInterfaces { .. } =>
5185                                    "BroadcastAll".to_string(),
5186                                _ => "other".to_string(),
5187                            })
5188                            .collect::<Vec<_>>()
5189                    );
5190                }
5191                self.dispatch_all(actions);
5192            }
5193            Err(e) => {
5194                log::warn!("SendOutbound: failed to unpack packet: {:?}", e);
5195            }
5196        }
5197    }
5198
5199    /// Run the event loop. Blocks until Shutdown or all senders are dropped.
5200    pub fn run(&mut self) {
5201        loop {
5202            let event = match self.rx.recv() {
5203                Ok(e) => e,
5204                Err(_) => break, // all senders dropped
5205            };
5206
5207            match event {
5208                Event::Frame { interface_id, data } => {
5209                    self.handle_frame_event(interface_id, data);
5210                }
5211                Event::AnnounceVerified {
5212                    key,
5213                    validated,
5214                    sig_cache_key,
5215                } => {
5216                    self.handle_announce_verified_event(key, validated, sig_cache_key);
5217                }
5218                Event::AnnounceVerifyFailed { key, .. } => {
5219                    let mut announce_queue = self
5220                        .announce_verify_queue
5221                        .lock()
5222                        .unwrap_or_else(|poisoned| poisoned.into_inner());
5223                    let _ = announce_queue.complete_failure(&key);
5224                }
5225                Event::Tick => self.handle_tick_event(),
5226                Event::BeginDrain { timeout } => {
5227                    self.begin_drain(timeout);
5228                }
5229                Event::InterfaceUp(id, new_writer, info) => {
5230                    self.handle_interface_up_event(id, new_writer, info);
5231                }
5232                Event::InterfaceDown(id) => self.handle_interface_down_event(id),
5233                Event::SendOutbound {
5234                    raw,
5235                    dest_type,
5236                    attached_interface,
5237                } => self.handle_send_outbound_event(raw, dest_type, attached_interface),
5238                Event::RegisterDestination {
5239                    dest_hash,
5240                    dest_type,
5241                } => {
5242                    self.engine.register_destination(dest_hash, dest_type);
5243                    self.local_destinations.insert(dest_hash, dest_type);
5244                }
5245                Event::StoreSharedAnnounce {
5246                    dest_hash,
5247                    name_hash,
5248                    identity_prv_key,
5249                    app_data,
5250                } => {
5251                    self.shared_announces.insert(
5252                        dest_hash,
5253                        SharedAnnounceRecord {
5254                            name_hash,
5255                            identity_prv_key,
5256                            app_data,
5257                        },
5258                    );
5259                }
5260                Event::DeregisterDestination { dest_hash } => {
5261                    self.engine.deregister_destination(&dest_hash);
5262                    self.local_destinations.remove(&dest_hash);
5263                    self.shared_announces.remove(&dest_hash);
5264                }
5265                Event::Query(request, response_tx) => {
5266                    let response = self.handle_query_mut(request);
5267                    let _ = response_tx.send(response);
5268                }
5269                Event::DeregisterLinkDestination { dest_hash } => {
5270                    self.link_manager.deregister_link_destination(&dest_hash);
5271                }
5272                Event::RegisterLinkDestination {
5273                    dest_hash,
5274                    sig_prv_bytes,
5275                    sig_pub_bytes,
5276                    resource_strategy,
5277                } => {
5278                    let sig_prv =
5279                        rns_crypto::ed25519::Ed25519PrivateKey::from_bytes(&sig_prv_bytes);
5280                    let strat = match resource_strategy {
5281                        1 => crate::link_manager::ResourceStrategy::AcceptAll,
5282                        2 => crate::link_manager::ResourceStrategy::AcceptApp,
5283                        _ => crate::link_manager::ResourceStrategy::AcceptNone,
5284                    };
5285                    self.link_manager.register_link_destination(
5286                        dest_hash,
5287                        sig_prv,
5288                        sig_pub_bytes,
5289                        strat,
5290                    );
5291                    // Also register in transport engine so inbound packets are delivered locally
5292                    self.engine
5293                        .register_destination(dest_hash, rns_core::constants::DESTINATION_SINGLE);
5294                    self.local_destinations
5295                        .insert(dest_hash, rns_core::constants::DESTINATION_SINGLE);
5296                }
5297                Event::RegisterRequestHandler {
5298                    path,
5299                    allowed_list,
5300                    handler,
5301                } => {
5302                    self.link_manager.register_request_handler(
5303                        &path,
5304                        allowed_list,
5305                        move |link_id, p, data, remote| handler(link_id, p, data, remote),
5306                    );
5307                }
5308                Event::CreateLink {
5309                    dest_hash,
5310                    dest_sig_pub_bytes,
5311                    response_tx,
5312                } => {
5313                    if self.is_draining() {
5314                        self.reject_new_work("create link");
5315                        let _ = (dest_hash, dest_sig_pub_bytes);
5316                        let _ = response_tx.send([0u8; 16]);
5317                        continue;
5318                    }
5319                    let next_hop_interface = self.engine.next_hop_interface(&dest_hash);
5320                    let recalled_route_hint = if next_hop_interface.is_none() {
5321                        self.known_destination_route_hint(&dest_hash)
5322                    } else {
5323                        None
5324                    };
5325                    if recalled_route_hint.is_some() {
5326                        let _ = self.mark_known_destination_used(&dest_hash);
5327                    }
5328                    let attached_interface =
5329                        next_hop_interface.or(recalled_route_hint.map(|(iface, _)| iface));
5330                    let hops = self
5331                        .engine
5332                        .hops_to(&dest_hash)
5333                        .or_else(|| recalled_route_hint.map(|(_, hops)| hops))
5334                        .unwrap_or(0);
5335                    let mtu = attached_interface
5336                        .and_then(|iface_id| self.interfaces.get(&iface_id))
5337                        .map(|entry| entry.info.mtu)
5338                        .unwrap_or(rns_core::constants::MTU as u32);
5339                    let (link_id, mut link_actions) = self.link_manager.create_link(
5340                        &dest_hash,
5341                        &dest_sig_pub_bytes,
5342                        hops,
5343                        mtu,
5344                        &mut self.rng,
5345                    );
5346                    if next_hop_interface.is_none() {
5347                        if let Some(iface) = attached_interface {
5348                            for action in &mut link_actions {
5349                                if let LinkManagerAction::SendPacket {
5350                                    dest_type,
5351                                    attached_interface,
5352                                    ..
5353                                } = action
5354                                {
5355                                    if *dest_type == rns_core::constants::DESTINATION_LINK
5356                                        && attached_interface.is_none()
5357                                    {
5358                                        *attached_interface = Some(iface);
5359                                    }
5360                                }
5361                            }
5362                        }
5363                    }
5364                    let _ = response_tx.send(link_id);
5365                    self.dispatch_link_actions(link_actions);
5366                }
5367                Event::SendRequest {
5368                    link_id,
5369                    path,
5370                    data,
5371                } => {
5372                    if self.is_draining() {
5373                        self.reject_new_work("send link request");
5374                        let _ = (link_id, path, data);
5375                        continue;
5376                    }
5377                    let link_actions =
5378                        self.link_manager
5379                            .send_request(&link_id, &path, &data, &mut self.rng);
5380                    self.dispatch_link_actions(link_actions);
5381                }
5382                Event::IdentifyOnLink {
5383                    link_id,
5384                    identity_prv_key,
5385                } => {
5386                    if self.is_draining() {
5387                        self.reject_new_work("identify on link");
5388                        let _ = (link_id, identity_prv_key);
5389                        continue;
5390                    }
5391                    let identity =
5392                        rns_crypto::identity::Identity::from_private_key(&identity_prv_key);
5393                    let link_actions =
5394                        self.link_manager
5395                            .identify(&link_id, &identity, &mut self.rng);
5396                    self.dispatch_link_actions(link_actions);
5397                }
5398                Event::TeardownLink { link_id } => {
5399                    let link_actions = self.link_manager.teardown_link(&link_id);
5400                    self.dispatch_link_actions(link_actions);
5401                }
5402                Event::SendResource {
5403                    link_id,
5404                    data,
5405                    metadata,
5406                } => {
5407                    if self.is_draining() {
5408                        self.reject_new_work("send resource");
5409                        let _ = (link_id, data, metadata);
5410                        continue;
5411                    }
5412                    let link_actions = self.link_manager.send_resource(
5413                        &link_id,
5414                        &data,
5415                        metadata.as_deref(),
5416                        &mut self.rng,
5417                    );
5418                    self.dispatch_link_actions(link_actions);
5419                }
5420                Event::SetResourceStrategy { link_id, strategy } => {
5421                    use crate::link_manager::ResourceStrategy;
5422                    let strat = match strategy {
5423                        0 => ResourceStrategy::AcceptNone,
5424                        1 => ResourceStrategy::AcceptAll,
5425                        2 => ResourceStrategy::AcceptApp,
5426                        _ => ResourceStrategy::AcceptNone,
5427                    };
5428                    self.link_manager.set_resource_strategy(&link_id, strat);
5429                }
5430                Event::AcceptResource {
5431                    link_id,
5432                    resource_hash,
5433                    accept,
5434                } => {
5435                    if self.is_draining() && accept {
5436                        self.reject_new_work("accept resource");
5437                        let _ = (link_id, resource_hash, accept);
5438                        continue;
5439                    }
5440                    let link_actions = self.link_manager.accept_resource(
5441                        &link_id,
5442                        &resource_hash,
5443                        accept,
5444                        &mut self.rng,
5445                    );
5446                    self.dispatch_link_actions(link_actions);
5447                }
5448                Event::SendChannelMessage {
5449                    link_id,
5450                    msgtype,
5451                    payload,
5452                    response_tx,
5453                } => {
5454                    if self.is_draining() {
5455                        self.reject_new_work("send channel message");
5456                        let _ = response_tx.send(Err(self.drain_error("send channel message")));
5457                        continue;
5458                    }
5459                    match self.link_manager.send_channel_message(
5460                        &link_id,
5461                        msgtype,
5462                        &payload,
5463                        &mut self.rng,
5464                    ) {
5465                        Ok(link_actions) => {
5466                            self.dispatch_link_actions(link_actions);
5467                            let _ = response_tx.send(Ok(()));
5468                        }
5469                        Err(err) => {
5470                            let _ = response_tx.send(Err(err));
5471                        }
5472                    }
5473                }
5474                Event::SendOnLink {
5475                    link_id,
5476                    data,
5477                    context,
5478                } => {
5479                    if self.is_draining() {
5480                        self.reject_new_work("send link payload");
5481                        let _ = (link_id, data, context);
5482                        continue;
5483                    }
5484                    let link_actions =
5485                        self.link_manager
5486                            .send_on_link(&link_id, &data, context, &mut self.rng);
5487                    self.dispatch_link_actions(link_actions);
5488                }
5489                Event::RequestPath { dest_hash } => {
5490                    if self.is_draining() {
5491                        self.reject_new_work("request path");
5492                        let _ = dest_hash;
5493                        continue;
5494                    }
5495                    self.handle_request_path(dest_hash);
5496                }
5497                Event::RegisterProofStrategy {
5498                    dest_hash,
5499                    strategy,
5500                    signing_key,
5501                } => {
5502                    let identity = signing_key
5503                        .map(|key| rns_crypto::identity::Identity::from_private_key(&key));
5504                    self.proof_strategies
5505                        .insert(dest_hash, (strategy, identity));
5506                }
5507                Event::ProposeDirectConnect { link_id } => {
5508                    if self.is_draining() {
5509                        self.reject_new_work("propose direct connect");
5510                        let _ = link_id;
5511                        continue;
5512                    }
5513                    let derived_key = self.link_manager.get_derived_key(&link_id);
5514                    if let Some(dk) = derived_key {
5515                        let tx = self.get_event_sender();
5516                        let hp_actions =
5517                            self.holepunch_manager
5518                                .propose(link_id, &dk, &mut self.rng, &tx);
5519                        self.dispatch_holepunch_actions(hp_actions);
5520                    } else {
5521                        log::warn!(
5522                            "Cannot propose direct connect: no derived key for link {:02x?}",
5523                            &link_id[..4]
5524                        );
5525                    }
5526                }
5527                Event::SetDirectConnectPolicy { policy } => {
5528                    self.holepunch_manager.set_policy(policy);
5529                }
5530                Event::HolePunchProbeResult {
5531                    link_id,
5532                    session_id,
5533                    observed_addr,
5534                    socket,
5535                    probe_server,
5536                } => {
5537                    let hp_actions = self.holepunch_manager.handle_probe_result(
5538                        link_id,
5539                        session_id,
5540                        observed_addr,
5541                        socket,
5542                        probe_server,
5543                    );
5544                    self.dispatch_holepunch_actions(hp_actions);
5545                }
5546                Event::HolePunchProbeFailed {
5547                    link_id,
5548                    session_id,
5549                } => {
5550                    let hp_actions = self
5551                        .holepunch_manager
5552                        .handle_probe_failed(link_id, session_id);
5553                    self.dispatch_holepunch_actions(hp_actions);
5554                }
5555                Event::LoadHook {
5556                    name,
5557                    wasm_bytes,
5558                    attach_point,
5559                    priority,
5560                    response_tx,
5561                } => {
5562                    #[cfg(feature = "rns-hooks")]
5563                    {
5564                        let result = (|| -> Result<(), String> {
5565                            let point_idx = crate::config::parse_hook_point(&attach_point)
5566                                .ok_or_else(|| format!("unknown hook point '{}'", attach_point))?;
5567                            let mgr = self
5568                                .hook_manager
5569                                .as_ref()
5570                                .ok_or_else(|| "hook manager not available".to_string())?;
5571                            let program = mgr
5572                                .compile(name.clone(), &wasm_bytes, priority)
5573                                .map_err(|e| format!("compile error: {}", e))?;
5574                            self.hook_slots[point_idx].attach(program);
5575                            log::info!(
5576                                "Loaded hook '{}' at point {} (priority {})",
5577                                name,
5578                                attach_point,
5579                                priority
5580                            );
5581                            Ok(())
5582                        })();
5583                        let _ = response_tx.send(result);
5584                    }
5585                    #[cfg(not(feature = "rns-hooks"))]
5586                    {
5587                        let _ = (name, wasm_bytes, attach_point, priority);
5588                        let _ = response_tx.send(Err("hooks not enabled".to_string()));
5589                    }
5590                }
5591                Event::UnloadHook {
5592                    name,
5593                    attach_point,
5594                    response_tx,
5595                } => {
5596                    #[cfg(feature = "rns-hooks")]
5597                    {
5598                        let result = (|| -> Result<(), String> {
5599                            let point_idx = crate::config::parse_hook_point(&attach_point)
5600                                .ok_or_else(|| format!("unknown hook point '{}'", attach_point))?;
5601                            match self.hook_slots[point_idx].detach(&name) {
5602                                Some(_) => {
5603                                    log::info!(
5604                                        "Unloaded hook '{}' from point {}",
5605                                        name,
5606                                        attach_point
5607                                    );
5608                                    Ok(())
5609                                }
5610                                None => Err(format!(
5611                                    "hook '{}' not found at point '{}'",
5612                                    name, attach_point
5613                                )),
5614                            }
5615                        })();
5616                        let _ = response_tx.send(result);
5617                    }
5618                    #[cfg(not(feature = "rns-hooks"))]
5619                    {
5620                        let _ = (name, attach_point);
5621                        let _ = response_tx.send(Err("hooks not enabled".to_string()));
5622                    }
5623                }
5624                Event::ReloadHook {
5625                    name,
5626                    attach_point,
5627                    wasm_bytes,
5628                    response_tx,
5629                } => {
5630                    #[cfg(feature = "rns-hooks")]
5631                    {
5632                        let result = (|| -> Result<(), String> {
5633                            let point_idx = crate::config::parse_hook_point(&attach_point)
5634                                .ok_or_else(|| format!("unknown hook point '{}'", attach_point))?;
5635                            let old =
5636                                self.hook_slots[point_idx].detach(&name).ok_or_else(|| {
5637                                    format!("hook '{}' not found at point '{}'", name, attach_point)
5638                                })?;
5639                            let priority = old.priority;
5640                            let mgr = match self.hook_manager.as_ref() {
5641                                Some(m) => m,
5642                                None => {
5643                                    self.hook_slots[point_idx].attach(old);
5644                                    return Err("hook manager not available".to_string());
5645                                }
5646                            };
5647                            match mgr.compile(name.clone(), &wasm_bytes, priority) {
5648                                Ok(program) => {
5649                                    self.hook_slots[point_idx].attach(program);
5650                                    log::info!(
5651                                        "Reloaded hook '{}' at point {} (priority {})",
5652                                        name,
5653                                        attach_point,
5654                                        priority
5655                                    );
5656                                    Ok(())
5657                                }
5658                                Err(e) => {
5659                                    self.hook_slots[point_idx].attach(old);
5660                                    Err(format!("compile error: {}", e))
5661                                }
5662                            }
5663                        })();
5664                        let _ = response_tx.send(result);
5665                    }
5666                    #[cfg(not(feature = "rns-hooks"))]
5667                    {
5668                        let _ = (name, attach_point, wasm_bytes);
5669                        let _ = response_tx.send(Err("hooks not enabled".to_string()));
5670                    }
5671                }
5672                Event::SetHookEnabled {
5673                    name,
5674                    attach_point,
5675                    enabled,
5676                    response_tx,
5677                } => {
5678                    #[cfg(feature = "rns-hooks")]
5679                    {
5680                        let result = self.update_hook_program(&name, &attach_point, |program| {
5681                            program.enabled = enabled;
5682                        });
5683                        if result.is_ok() {
5684                            log::info!(
5685                                "{} hook '{}' at point {}",
5686                                if enabled { "Enabled" } else { "Disabled" },
5687                                name,
5688                                attach_point,
5689                            );
5690                        }
5691                        let _ = response_tx.send(result);
5692                    }
5693                    #[cfg(not(feature = "rns-hooks"))]
5694                    {
5695                        let _ = (name, attach_point, enabled);
5696                        let _ = response_tx.send(Err("hooks not enabled".to_string()));
5697                    }
5698                }
5699                Event::SetHookPriority {
5700                    name,
5701                    attach_point,
5702                    priority,
5703                    response_tx,
5704                } => {
5705                    #[cfg(feature = "rns-hooks")]
5706                    {
5707                        let result = self.update_hook_program(&name, &attach_point, |program| {
5708                            program.priority = priority;
5709                        });
5710                        if result.is_ok() {
5711                            if let Some(point_idx) = crate::config::parse_hook_point(&attach_point)
5712                            {
5713                                self.hook_slots[point_idx]
5714                                    .programs
5715                                    .sort_by(|a, b| b.priority.cmp(&a.priority));
5716                                log::info!(
5717                                    "Updated hook '{}' at point {} to priority {}",
5718                                    name,
5719                                    attach_point,
5720                                    priority,
5721                                );
5722                            } else {
5723                                log::error!(
5724                                    "hook point '{}' became invalid during priority update",
5725                                    attach_point
5726                                );
5727                            }
5728                        }
5729                        let _ = response_tx.send(result);
5730                    }
5731                    #[cfg(not(feature = "rns-hooks"))]
5732                    {
5733                        let _ = (name, attach_point, priority);
5734                        let _ = response_tx.send(Err("hooks not enabled".to_string()));
5735                    }
5736                }
5737                Event::ListHooks { response_tx } => {
5738                    #[cfg(feature = "rns-hooks")]
5739                    {
5740                        let hook_point_names = [
5741                            "PreIngress",
5742                            "PreDispatch",
5743                            "AnnounceReceived",
5744                            "PathUpdated",
5745                            "AnnounceRetransmit",
5746                            "LinkRequestReceived",
5747                            "LinkEstablished",
5748                            "LinkClosed",
5749                            "InterfaceUp",
5750                            "InterfaceDown",
5751                            "InterfaceConfigChanged",
5752                            "BackbonePeerConnected",
5753                            "BackbonePeerDisconnected",
5754                            "BackbonePeerIdleTimeout",
5755                            "BackbonePeerWriteStall",
5756                            "BackbonePeerPenalty",
5757                            "SendOnInterface",
5758                            "BroadcastOnAllInterfaces",
5759                            "DeliverLocal",
5760                            "TunnelSynthesize",
5761                            "Tick",
5762                        ];
5763                        let mut infos = Vec::new();
5764                        for (idx, slot) in self.hook_slots.iter().enumerate() {
5765                            let point_name = hook_point_names.get(idx).unwrap_or(&"Unknown");
5766                            for prog in &slot.programs {
5767                                infos.push(crate::event::HookInfo {
5768                                    name: prog.name.clone(),
5769                                    attach_point: point_name.to_string(),
5770                                    priority: prog.priority,
5771                                    enabled: prog.enabled,
5772                                    consecutive_traps: prog.consecutive_traps,
5773                                });
5774                            }
5775                        }
5776                        let _ = response_tx.send(infos);
5777                    }
5778                    #[cfg(not(feature = "rns-hooks"))]
5779                    {
5780                        let _ = response_tx.send(Vec::new());
5781                    }
5782                }
5783                Event::InterfaceConfigChanged(id) => {
5784                    #[cfg(feature = "rns-hooks")]
5785                    {
5786                        let ctx = HookContext::Interface { interface_id: id.0 };
5787                        let now = time::now();
5788                        let engine_ref = EngineRef {
5789                            engine: &self.engine,
5790                            interfaces: &self.interfaces,
5791                            link_manager: &self.link_manager,
5792                            now,
5793                        };
5794                        let provider_events_enabled = self.provider_events_enabled();
5795                        if let Some(ref e) = run_hook_inner(
5796                            &mut self.hook_slots[HookPoint::InterfaceConfigChanged as usize]
5797                                .programs,
5798                            &self.hook_manager,
5799                            &engine_ref,
5800                            &ctx,
5801                            now,
5802                            provider_events_enabled,
5803                        ) {
5804                            self.forward_hook_side_effects("InterfaceConfigChanged", e);
5805                        }
5806                    }
5807                    #[cfg(not(feature = "rns-hooks"))]
5808                    let _ = id;
5809                }
5810                Event::BackbonePeerConnected {
5811                    server_interface_id,
5812                    peer_interface_id,
5813                    peer_ip,
5814                    peer_port,
5815                } => {
5816                    #[cfg(feature = "rns-hooks")]
5817                    {
5818                        self.run_backbone_peer_hook(
5819                            "BackbonePeerConnected",
5820                            HookPoint::BackbonePeerConnected,
5821                            &BackbonePeerHookEvent {
5822                                server_interface_id,
5823                                peer_interface_id: Some(peer_interface_id),
5824                                peer_ip,
5825                                peer_port,
5826                                connected_for: Duration::ZERO,
5827                                had_received_data: false,
5828                                penalty_level: 0,
5829                                blacklist_for: Duration::ZERO,
5830                            },
5831                        );
5832                    }
5833                    #[cfg(not(feature = "rns-hooks"))]
5834                    let _ = (server_interface_id, peer_interface_id, peer_ip, peer_port);
5835                }
5836                Event::BackbonePeerDisconnected {
5837                    server_interface_id,
5838                    peer_interface_id,
5839                    peer_ip,
5840                    peer_port,
5841                    connected_for,
5842                    had_received_data,
5843                } => {
5844                    #[cfg(feature = "rns-hooks")]
5845                    {
5846                        self.run_backbone_peer_hook(
5847                            "BackbonePeerDisconnected",
5848                            HookPoint::BackbonePeerDisconnected,
5849                            &BackbonePeerHookEvent {
5850                                server_interface_id,
5851                                peer_interface_id: Some(peer_interface_id),
5852                                peer_ip,
5853                                peer_port,
5854                                connected_for,
5855                                had_received_data,
5856                                penalty_level: 0,
5857                                blacklist_for: Duration::ZERO,
5858                            },
5859                        );
5860                    }
5861                    #[cfg(not(feature = "rns-hooks"))]
5862                    let _ = (
5863                        server_interface_id,
5864                        peer_interface_id,
5865                        peer_ip,
5866                        peer_port,
5867                        connected_for,
5868                        had_received_data,
5869                    );
5870                }
5871                Event::BackbonePeerIdleTimeout {
5872                    server_interface_id,
5873                    peer_interface_id,
5874                    peer_ip,
5875                    peer_port,
5876                    connected_for,
5877                } => {
5878                    #[cfg(feature = "rns-hooks")]
5879                    {
5880                        self.run_backbone_peer_hook(
5881                            "BackbonePeerIdleTimeout",
5882                            HookPoint::BackbonePeerIdleTimeout,
5883                            &BackbonePeerHookEvent {
5884                                server_interface_id,
5885                                peer_interface_id: Some(peer_interface_id),
5886                                peer_ip,
5887                                peer_port,
5888                                connected_for,
5889                                had_received_data: false,
5890                                penalty_level: 0,
5891                                blacklist_for: Duration::ZERO,
5892                            },
5893                        );
5894                    }
5895                    #[cfg(not(feature = "rns-hooks"))]
5896                    let _ = (
5897                        server_interface_id,
5898                        peer_interface_id,
5899                        peer_ip,
5900                        peer_port,
5901                        connected_for,
5902                    );
5903                }
5904                Event::BackbonePeerWriteStall {
5905                    server_interface_id,
5906                    peer_interface_id,
5907                    peer_ip,
5908                    peer_port,
5909                    connected_for,
5910                } => {
5911                    #[cfg(feature = "rns-hooks")]
5912                    {
5913                        self.run_backbone_peer_hook(
5914                            "BackbonePeerWriteStall",
5915                            HookPoint::BackbonePeerWriteStall,
5916                            &BackbonePeerHookEvent {
5917                                server_interface_id,
5918                                peer_interface_id: Some(peer_interface_id),
5919                                peer_ip,
5920                                peer_port,
5921                                connected_for,
5922                                had_received_data: false,
5923                                penalty_level: 0,
5924                                blacklist_for: Duration::ZERO,
5925                            },
5926                        );
5927                    }
5928                    #[cfg(not(feature = "rns-hooks"))]
5929                    let _ = (
5930                        server_interface_id,
5931                        peer_interface_id,
5932                        peer_ip,
5933                        peer_port,
5934                        connected_for,
5935                    );
5936                }
5937                Event::BackbonePeerPenalty {
5938                    server_interface_id,
5939                    peer_ip,
5940                    penalty_level,
5941                    blacklist_for,
5942                } => {
5943                    #[cfg(feature = "rns-hooks")]
5944                    {
5945                        self.run_backbone_peer_hook(
5946                            "BackbonePeerPenalty",
5947                            HookPoint::BackbonePeerPenalty,
5948                            &BackbonePeerHookEvent {
5949                                server_interface_id,
5950                                peer_interface_id: None,
5951                                peer_ip,
5952                                peer_port: 0,
5953                                connected_for: Duration::ZERO,
5954                                had_received_data: false,
5955                                penalty_level,
5956                                blacklist_for,
5957                            },
5958                        );
5959                    }
5960                    #[cfg(not(feature = "rns-hooks"))]
5961                    let _ = (server_interface_id, peer_ip, penalty_level, blacklist_for);
5962                }
5963                Event::Shutdown => {
5964                    self.lifecycle_state = LifecycleState::Stopped;
5965                    break;
5966                }
5967            }
5968        }
5969    }
5970
5971    fn handle_interface_stats_query(&self) -> QueryResponse {
5972        let mut interfaces = Vec::new();
5973        let mut total_rxb: u64 = 0;
5974        let mut total_txb: u64 = 0;
5975        for entry in self.interfaces.values() {
5976            total_rxb += entry.stats.rxb;
5977            total_txb += entry.stats.txb;
5978            interfaces.push(SingleInterfaceStat {
5979                id: entry.info.id.0,
5980                name: entry.info.name.clone(),
5981                status: entry.online && entry.enabled,
5982                mode: entry.info.mode,
5983                rxb: entry.stats.rxb,
5984                txb: entry.stats.txb,
5985                rx_packets: entry.stats.rx_packets,
5986                tx_packets: entry.stats.tx_packets,
5987                bitrate: entry.info.bitrate,
5988                ifac_size: entry.ifac.as_ref().map(|s| s.size),
5989                started: entry.stats.started,
5990                ia_freq: entry.stats.incoming_announce_freq(),
5991                oa_freq: entry.stats.outgoing_announce_freq(),
5992                interface_type: entry.interface_type.clone(),
5993            });
5994        }
5995        interfaces.sort_by(|a, b| a.name.cmp(&b.name));
5996        QueryResponse::InterfaceStats(InterfaceStatsResponse {
5997            interfaces,
5998            transport_id: self.engine.identity_hash().copied(),
5999            transport_enabled: self.engine.transport_enabled(),
6000            transport_uptime: time::now() - self.started,
6001            total_rxb,
6002            total_txb,
6003            probe_responder: self.probe_responder_hash,
6004            #[cfg(feature = "iface-backbone")]
6005            backbone_peer_pool: self.backbone_peer_pool_status(),
6006            #[cfg(not(feature = "iface-backbone"))]
6007            backbone_peer_pool: None,
6008        })
6009    }
6010
6011    fn handle_path_table_query(&self, max_hops: Option<u8>) -> QueryResponse {
6012        let entries: Vec<PathTableEntry> = self
6013            .engine
6014            .path_table_entries()
6015            .filter(|(_, entry)| max_hops.is_none_or(|max| entry.hops <= max))
6016            .map(|(hash, entry)| {
6017                let iface_name = self
6018                    .interfaces
6019                    .get(&entry.receiving_interface)
6020                    .map(|e| e.info.name.clone())
6021                    .or_else(|| {
6022                        self.engine
6023                            .interface_info(&entry.receiving_interface)
6024                            .map(|i| i.name.clone())
6025                    })
6026                    .unwrap_or_default();
6027                PathTableEntry {
6028                    hash: *hash,
6029                    timestamp: entry.timestamp,
6030                    via: entry.next_hop,
6031                    hops: entry.hops,
6032                    expires: entry.expires,
6033                    interface: entry.receiving_interface,
6034                    interface_name: iface_name,
6035                }
6036            })
6037            .collect();
6038        QueryResponse::PathTable(entries)
6039    }
6040
6041    fn handle_runtime_config_query(&self, request: QueryRequest) -> Option<QueryResponse> {
6042        match request {
6043            QueryRequest::ListRuntimeConfig => {
6044                Some(QueryResponse::RuntimeConfigList(self.list_runtime_config()))
6045            }
6046            QueryRequest::GetRuntimeConfig { key } => Some(QueryResponse::RuntimeConfigEntry(
6047                self.runtime_config_entry(&key),
6048            )),
6049            QueryRequest::BackbonePeerState { interface_name } => {
6050                Some(QueryResponse::BackbonePeerState(
6051                    self.list_backbone_peer_state(interface_name.as_deref()),
6052                ))
6053            }
6054            QueryRequest::SetRuntimeConfig { .. } => {
6055                Some(QueryResponse::RuntimeConfigSet(Err(RuntimeConfigError {
6056                    code: RuntimeConfigErrorCode::Unsupported,
6057                    message: "mutating runtime config is handled separately".to_string(),
6058                })))
6059            }
6060            QueryRequest::ResetRuntimeConfig { .. } => {
6061                Some(QueryResponse::RuntimeConfigReset(Err(RuntimeConfigError {
6062                    code: RuntimeConfigErrorCode::Unsupported,
6063                    message: "mutating runtime config is handled separately".to_string(),
6064                })))
6065            }
6066            QueryRequest::ClearBackbonePeerState { .. } => {
6067                Some(QueryResponse::ClearBackbonePeerState(false))
6068            }
6069            QueryRequest::BlacklistBackbonePeer { .. } => {
6070                Some(QueryResponse::BlacklistBackbonePeer(false))
6071            }
6072            _ => None,
6073        }
6074    }
6075
6076    fn handle_mutation_query(&mut self, request: QueryRequest) -> Option<QueryResponse> {
6077        match request {
6078            QueryRequest::BlackholeIdentity {
6079                identity_hash,
6080                duration_hours,
6081                reason,
6082            } => {
6083                let now = time::now();
6084                self.engine
6085                    .blackhole_identity(identity_hash, now, duration_hours, reason);
6086                Some(QueryResponse::BlackholeResult(true))
6087            }
6088            QueryRequest::UnblackholeIdentity { identity_hash } => Some(
6089                QueryResponse::UnblackholeResult(self.engine.unblackhole_identity(&identity_hash)),
6090            ),
6091            QueryRequest::DropPath { dest_hash } => {
6092                Some(QueryResponse::DropPath(self.engine.drop_path(&dest_hash)))
6093            }
6094            QueryRequest::DropAllVia { transport_hash } => Some(QueryResponse::DropAllVia(
6095                self.engine.drop_all_via(&transport_hash),
6096            )),
6097            QueryRequest::DropAnnounceQueues => {
6098                self.engine.drop_announce_queues();
6099                Some(QueryResponse::DropAnnounceQueues)
6100            }
6101            QueryRequest::ClearBackbonePeerState {
6102                interface_name,
6103                peer_ip,
6104            } => Some(QueryResponse::ClearBackbonePeerState(
6105                self.clear_backbone_peer_state(&interface_name, peer_ip),
6106            )),
6107            QueryRequest::BlacklistBackbonePeer {
6108                interface_name,
6109                peer_ip,
6110                duration,
6111                reason,
6112                penalty_level,
6113            } => Some(QueryResponse::BlacklistBackbonePeer(
6114                self.blacklist_backbone_peer(
6115                    &interface_name,
6116                    peer_ip,
6117                    duration,
6118                    reason,
6119                    penalty_level,
6120                ),
6121            )),
6122            QueryRequest::InjectPath {
6123                dest_hash,
6124                next_hop,
6125                hops,
6126                expires,
6127                interface_name,
6128                packet_hash,
6129            } => {
6130                let iface_id = self
6131                    .interfaces
6132                    .iter()
6133                    .find(|(_, entry)| entry.info.name == interface_name)
6134                    .map(|(id, _)| *id);
6135                Some(match iface_id {
6136                    Some(id) => {
6137                        let entry = PathEntry {
6138                            timestamp: time::now(),
6139                            next_hop,
6140                            hops,
6141                            expires,
6142                            random_blobs: Vec::new(),
6143                            receiving_interface: id,
6144                            packet_hash,
6145                            announce_raw: None,
6146                        };
6147                        self.engine.inject_path(dest_hash, entry);
6148                        QueryResponse::InjectPath(true)
6149                    }
6150                    None => QueryResponse::InjectPath(false),
6151                })
6152            }
6153            QueryRequest::InjectIdentity {
6154                dest_hash,
6155                identity_hash,
6156                public_key,
6157                app_data,
6158                hops,
6159                received_at,
6160            } => {
6161                self.upsert_known_destination(
6162                    dest_hash,
6163                    crate::destination::AnnouncedIdentity {
6164                        dest_hash: rns_core::types::DestHash(dest_hash),
6165                        identity_hash: rns_core::types::IdentityHash(identity_hash),
6166                        public_key,
6167                        app_data,
6168                        hops,
6169                        received_at,
6170                        receiving_interface: rns_core::transport::types::InterfaceId(0),
6171                    },
6172                );
6173                Some(QueryResponse::InjectIdentity(true))
6174            }
6175            QueryRequest::RestoreKnownDestination(entry) => {
6176                self.known_destinations.insert(
6177                    entry.dest_hash,
6178                    KnownDestinationState {
6179                        announced: crate::destination::AnnouncedIdentity {
6180                            dest_hash: rns_core::types::DestHash(entry.dest_hash),
6181                            identity_hash: rns_core::types::IdentityHash(entry.identity_hash),
6182                            public_key: entry.public_key,
6183                            app_data: entry.app_data,
6184                            hops: entry.hops,
6185                            received_at: entry.received_at,
6186                            receiving_interface: entry.receiving_interface,
6187                        },
6188                        was_used: entry.was_used,
6189                        last_used_at: entry.last_used_at,
6190                        retained: entry.retained,
6191                    },
6192                );
6193                Some(QueryResponse::RestoreKnownDestination(true))
6194            }
6195            QueryRequest::RetainKnownDestination { dest_hash } => Some(
6196                QueryResponse::RetainKnownDestination(self.retain_known_destination(&dest_hash)),
6197            ),
6198            QueryRequest::UnretainKnownDestination { dest_hash } => {
6199                Some(QueryResponse::UnretainKnownDestination(
6200                    self.unretain_known_destination(&dest_hash),
6201                ))
6202            }
6203            QueryRequest::MarkKnownDestinationUsed { dest_hash } => {
6204                Some(QueryResponse::MarkKnownDestinationUsed(
6205                    self.mark_known_destination_used(&dest_hash),
6206                ))
6207            }
6208            _ => None,
6209        }
6210    }
6211
6212    fn runtime_config_query_fallback(request: &QueryRequest) -> QueryResponse {
6213        match request {
6214            QueryRequest::ListRuntimeConfig => QueryResponse::RuntimeConfigList(Vec::new()),
6215            QueryRequest::GetRuntimeConfig { .. } => QueryResponse::RuntimeConfigEntry(None),
6216            QueryRequest::BackbonePeerState { .. } => QueryResponse::BackbonePeerState(Vec::new()),
6217            QueryRequest::SetRuntimeConfig { key, .. } => {
6218                QueryResponse::RuntimeConfigSet(Err(RuntimeConfigError {
6219                    code: RuntimeConfigErrorCode::ApplyFailed,
6220                    message: format!(
6221                        "internal error: no response generated for runtime-config set '{}'",
6222                        key
6223                    ),
6224                }))
6225            }
6226            QueryRequest::ResetRuntimeConfig { key } => {
6227                QueryResponse::RuntimeConfigReset(Err(RuntimeConfigError {
6228                    code: RuntimeConfigErrorCode::ApplyFailed,
6229                    message: format!(
6230                        "internal error: no response generated for runtime-config reset '{}'",
6231                        key
6232                    ),
6233                }))
6234            }
6235            QueryRequest::ClearBackbonePeerState { .. } => {
6236                QueryResponse::ClearBackbonePeerState(false)
6237            }
6238            QueryRequest::BlacklistBackbonePeer { .. } => {
6239                QueryResponse::BlacklistBackbonePeer(false)
6240            }
6241            _ => QueryResponse::RuntimeConfigEntry(None),
6242        }
6243    }
6244
6245    fn mutation_query_fallback(request: &QueryRequest) -> QueryResponse {
6246        match request {
6247            QueryRequest::BlackholeIdentity { .. } => QueryResponse::BlackholeResult(false),
6248            QueryRequest::UnblackholeIdentity { .. } => QueryResponse::UnblackholeResult(false),
6249            QueryRequest::DropPath { .. } => QueryResponse::DropPath(false),
6250            QueryRequest::DropAllVia { .. } => QueryResponse::DropAllVia(0),
6251            QueryRequest::DropAnnounceQueues => QueryResponse::DropAnnounceQueues,
6252            QueryRequest::ClearBackbonePeerState { .. } => {
6253                QueryResponse::ClearBackbonePeerState(false)
6254            }
6255            QueryRequest::BlacklistBackbonePeer { .. } => {
6256                QueryResponse::BlacklistBackbonePeer(false)
6257            }
6258            QueryRequest::InjectPath { .. } => QueryResponse::InjectPath(false),
6259            QueryRequest::InjectIdentity { .. } => QueryResponse::InjectIdentity(false),
6260            QueryRequest::RestoreKnownDestination(..) => {
6261                QueryResponse::RestoreKnownDestination(false)
6262            }
6263            QueryRequest::RetainKnownDestination { .. } => {
6264                QueryResponse::RetainKnownDestination(false)
6265            }
6266            QueryRequest::UnretainKnownDestination { .. } => {
6267                QueryResponse::UnretainKnownDestination(false)
6268            }
6269            QueryRequest::MarkKnownDestinationUsed { .. } => {
6270                QueryResponse::MarkKnownDestinationUsed(false)
6271            }
6272            _ => QueryResponse::InjectIdentity(false),
6273        }
6274    }
6275
6276    /// Handle a query request and produce a response.
6277    fn handle_query(&self, request: QueryRequest) -> QueryResponse {
6278        match request {
6279            QueryRequest::InterfaceStats => self.handle_interface_stats_query(),
6280            QueryRequest::BackboneInterfaces => {
6281                QueryResponse::BackboneInterfaces(self.list_backbone_interfaces())
6282            }
6283            QueryRequest::ProviderBridgeStats => {
6284                #[cfg(feature = "rns-hooks")]
6285                {
6286                    QueryResponse::ProviderBridgeStats(
6287                        self.provider_bridge.as_ref().map(|bridge| bridge.stats()),
6288                    )
6289                }
6290                #[cfg(not(feature = "rns-hooks"))]
6291                {
6292                    QueryResponse::ProviderBridgeStats(None::<crate::event::ProviderBridgeStats>)
6293                }
6294            }
6295            QueryRequest::DrainStatus => QueryResponse::DrainStatus(self.drain_status()),
6296            QueryRequest::PathTable { max_hops } => self.handle_path_table_query(max_hops),
6297            QueryRequest::RateTable => {
6298                let entries: Vec<RateTableEntry> = self
6299                    .engine
6300                    .rate_limiter()
6301                    .entries()
6302                    .map(|(hash, entry)| RateTableEntry {
6303                        hash: *hash,
6304                        last: entry.last,
6305                        rate_violations: entry.rate_violations,
6306                        blocked_until: entry.blocked_until,
6307                        timestamps: entry.timestamps.clone(),
6308                    })
6309                    .collect();
6310                QueryResponse::RateTable(entries)
6311            }
6312            QueryRequest::NextHop { dest_hash } => {
6313                let resp = self
6314                    .engine
6315                    .next_hop(&dest_hash)
6316                    .map(|next_hop| NextHopResponse {
6317                        next_hop,
6318                        hops: self.engine.hops_to(&dest_hash).unwrap_or(0),
6319                        interface: self
6320                            .engine
6321                            .next_hop_interface(&dest_hash)
6322                            .unwrap_or(InterfaceId(0)),
6323                    });
6324                QueryResponse::NextHop(resp)
6325            }
6326            QueryRequest::NextHopIfName { dest_hash } => {
6327                let name = self
6328                    .engine
6329                    .next_hop_interface(&dest_hash)
6330                    .and_then(|id| self.interfaces.get(&id))
6331                    .map(|entry| entry.info.name.clone());
6332                QueryResponse::NextHopIfName(name)
6333            }
6334            QueryRequest::LinkCount => QueryResponse::LinkCount(
6335                self.engine.link_table_count() + self.link_manager.link_count(),
6336            ),
6337            QueryRequest::DropPath { .. } => {
6338                // Mutating queries are handled by handle_query_mut
6339                QueryResponse::DropPath(false)
6340            }
6341            QueryRequest::DropAllVia { .. } => QueryResponse::DropAllVia(0),
6342            QueryRequest::DropAnnounceQueues => QueryResponse::DropAnnounceQueues,
6343            QueryRequest::TransportIdentity => {
6344                QueryResponse::TransportIdentity(self.engine.identity_hash().copied())
6345            }
6346            QueryRequest::GetBlackholed => {
6347                let now = time::now();
6348                let entries: Vec<BlackholeInfo> = self
6349                    .engine
6350                    .blackholed_entries()
6351                    .filter(|(_, e)| e.expires == 0.0 || e.expires > now)
6352                    .map(|(hash, entry)| BlackholeInfo {
6353                        identity_hash: *hash,
6354                        created: entry.created,
6355                        expires: entry.expires,
6356                        reason: entry.reason.clone(),
6357                    })
6358                    .collect();
6359                QueryResponse::Blackholed(entries)
6360            }
6361            QueryRequest::BlackholeIdentity { .. } | QueryRequest::UnblackholeIdentity { .. } => {
6362                // Mutating queries handled by handle_query_mut
6363                QueryResponse::BlackholeResult(false)
6364            }
6365            QueryRequest::InjectPath { .. } => {
6366                // Mutating queries handled by handle_query_mut
6367                QueryResponse::InjectPath(false)
6368            }
6369            QueryRequest::InjectIdentity { .. } => {
6370                // Mutating queries handled by handle_query_mut
6371                QueryResponse::InjectIdentity(false)
6372            }
6373            QueryRequest::RestoreKnownDestination(..) => {
6374                QueryResponse::RestoreKnownDestination(false)
6375            }
6376            QueryRequest::RetainKnownDestination { .. } => {
6377                QueryResponse::RetainKnownDestination(false)
6378            }
6379            QueryRequest::UnretainKnownDestination { .. } => {
6380                QueryResponse::UnretainKnownDestination(false)
6381            }
6382            QueryRequest::MarkKnownDestinationUsed { .. } => {
6383                QueryResponse::MarkKnownDestinationUsed(false)
6384            }
6385            QueryRequest::HasPath { dest_hash } => {
6386                QueryResponse::HasPath(self.engine.has_path(&dest_hash))
6387            }
6388            QueryRequest::HopsTo { dest_hash } => {
6389                QueryResponse::HopsTo(self.engine.hops_to(&dest_hash))
6390            }
6391            QueryRequest::RecallIdentity { .. } => QueryResponse::RecallIdentity(None),
6392            QueryRequest::KnownDestinations => {
6393                QueryResponse::KnownDestinations(self.known_destination_entries())
6394            }
6395            QueryRequest::LocalDestinations => {
6396                let entries: Vec<LocalDestinationEntry> = self
6397                    .local_destinations
6398                    .iter()
6399                    .map(|(hash, dest_type)| LocalDestinationEntry {
6400                        hash: *hash,
6401                        dest_type: *dest_type,
6402                    })
6403                    .collect();
6404                QueryResponse::LocalDestinations(entries)
6405            }
6406            QueryRequest::Links => QueryResponse::Links(self.link_manager.link_entries()),
6407            QueryRequest::Resources => {
6408                QueryResponse::Resources(self.link_manager.resource_entries())
6409            }
6410            QueryRequest::DiscoveredInterfaces {
6411                only_available,
6412                only_transport,
6413            } => {
6414                let mut interfaces = self.discovered_interfaces.list().unwrap_or_default();
6415                crate::discovery::filter_and_sort_interfaces(
6416                    &mut interfaces,
6417                    only_available,
6418                    only_transport,
6419                );
6420                QueryResponse::DiscoveredInterfaces(interfaces)
6421            }
6422            request @ (QueryRequest::ListRuntimeConfig
6423            | QueryRequest::GetRuntimeConfig { .. }
6424            | QueryRequest::BackbonePeerState { .. }
6425            | QueryRequest::SetRuntimeConfig { .. }
6426            | QueryRequest::ResetRuntimeConfig { .. }
6427            | QueryRequest::ClearBackbonePeerState { .. }
6428            | QueryRequest::BlacklistBackbonePeer { .. }) => {
6429                let fallback = Self::runtime_config_query_fallback(&request);
6430                self.handle_runtime_config_query(request)
6431                    .unwrap_or_else(|| {
6432                        log::error!(
6433                            "runtime-config query branch unexpectedly returned no response"
6434                        );
6435                        fallback
6436                    })
6437            }
6438            // Mutating queries handled by handle_query_mut
6439            QueryRequest::SendProbe { .. } => QueryResponse::SendProbe(None),
6440            QueryRequest::CheckProof { .. } => QueryResponse::CheckProof(None),
6441        }
6442    }
6443
6444    /// Handle a mutating query request.
6445    fn handle_query_mut(&mut self, request: QueryRequest) -> QueryResponse {
6446        match request {
6447            request @ (QueryRequest::BlackholeIdentity { .. }
6448            | QueryRequest::UnblackholeIdentity { .. }
6449            | QueryRequest::DropPath { .. }
6450            | QueryRequest::DropAllVia { .. }
6451            | QueryRequest::DropAnnounceQueues
6452            | QueryRequest::ClearBackbonePeerState { .. }
6453            | QueryRequest::BlacklistBackbonePeer { .. }
6454            | QueryRequest::InjectPath { .. }
6455            | QueryRequest::InjectIdentity { .. }
6456            | QueryRequest::RestoreKnownDestination(..)
6457            | QueryRequest::RetainKnownDestination { .. }
6458            | QueryRequest::UnretainKnownDestination { .. }
6459            | QueryRequest::MarkKnownDestinationUsed { .. }) => {
6460                let fallback = Self::mutation_query_fallback(&request);
6461                self.handle_mutation_query(request).unwrap_or_else(|| {
6462                    log::error!("mutation query branch unexpectedly returned no response");
6463                    fallback
6464                })
6465            }
6466            QueryRequest::RecallIdentity { dest_hash } => {
6467                let recalled = self.known_destination_announced(&dest_hash);
6468                if recalled.is_some() {
6469                    let _ = self.mark_known_destination_used(&dest_hash);
6470                }
6471                QueryResponse::RecallIdentity(recalled)
6472            }
6473            QueryRequest::DrainStatus => QueryResponse::DrainStatus(self.drain_status()),
6474            QueryRequest::SendProbe {
6475                dest_hash,
6476                payload_size,
6477            } => {
6478                // Look up the identity for this destination hash
6479                let announced = self.known_destination_announced(&dest_hash);
6480                match announced {
6481                    Some(recalled) => {
6482                        let _ = self.mark_known_destination_used(&dest_hash);
6483                        // Encrypt random payload with remote public key
6484                        let remote_id =
6485                            rns_crypto::identity::Identity::from_public_key(&recalled.public_key);
6486                        let mut payload = vec![0u8; payload_size];
6487                        self.rng.fill_bytes(&mut payload);
6488                        match remote_id.encrypt(&payload, &mut self.rng) {
6489                            Ok(ciphertext) => {
6490                                // Build DATA SINGLE BROADCAST packet to dest_hash
6491                                let flags = rns_core::packet::PacketFlags {
6492                                    header_type: rns_core::constants::HEADER_1,
6493                                    context_flag: rns_core::constants::FLAG_UNSET,
6494                                    transport_type: rns_core::constants::TRANSPORT_BROADCAST,
6495                                    destination_type: rns_core::constants::DESTINATION_SINGLE,
6496                                    packet_type: rns_core::constants::PACKET_TYPE_DATA,
6497                                };
6498                                match RawPacket::pack(
6499                                    flags,
6500                                    0,
6501                                    &dest_hash,
6502                                    None,
6503                                    rns_core::constants::CONTEXT_NONE,
6504                                    &ciphertext,
6505                                ) {
6506                                    Ok(packet) => {
6507                                        let packet_hash = packet.packet_hash;
6508                                        let hops = self.engine.hops_to(&dest_hash).unwrap_or(0);
6509                                        // Track for proof matching
6510                                        self.sent_packets
6511                                            .insert(packet_hash, (dest_hash, time::now()));
6512                                        // Send via engine
6513                                        let actions = self.engine.handle_outbound(
6514                                            &packet,
6515                                            rns_core::constants::DESTINATION_SINGLE,
6516                                            None,
6517                                            time::now(),
6518                                        );
6519                                        self.dispatch_all(actions);
6520                                        log::debug!(
6521                                            "Sent probe ({} bytes) to {:02x?}",
6522                                            payload_size,
6523                                            &dest_hash[..4],
6524                                        );
6525                                        QueryResponse::SendProbe(Some((packet_hash, hops)))
6526                                    }
6527                                    Err(_) => {
6528                                        log::warn!("Failed to pack probe packet");
6529                                        QueryResponse::SendProbe(None)
6530                                    }
6531                                }
6532                            }
6533                            Err(_) => {
6534                                log::warn!("Failed to encrypt probe payload");
6535                                QueryResponse::SendProbe(None)
6536                            }
6537                        }
6538                    }
6539                    None => {
6540                        log::debug!("No known identity for probe dest {:02x?}", &dest_hash[..4]);
6541                        QueryResponse::SendProbe(None)
6542                    }
6543                }
6544            }
6545            QueryRequest::CheckProof { packet_hash } => {
6546                match self.completed_proofs.remove(&packet_hash) {
6547                    Some((rtt, _received)) => QueryResponse::CheckProof(Some(rtt)),
6548                    None => QueryResponse::CheckProof(None),
6549                }
6550            }
6551            QueryRequest::SetRuntimeConfig { key, value } => {
6552                let result = match key.as_str() {
6553                    "global.tick_interval_ms" => match Self::expect_u64(value, &key) {
6554                        Ok(value) => {
6555                            let clamped = value.clamp(100, 10_000);
6556                            self.tick_interval_ms.store(clamped, Ordering::Relaxed);
6557                            Ok(())
6558                        }
6559                        Err(err) => Err(err),
6560                    },
6561                    "global.known_destinations_ttl_secs" => match Self::expect_f64(value, &key) {
6562                        Ok(value) => {
6563                            self.known_destinations_ttl = value;
6564                            Ok(())
6565                        }
6566                        Err(err) => Err(err),
6567                    },
6568                    "global.rate_limiter_ttl_secs" => match Self::expect_f64(value, &key) {
6569                        Ok(value) if value >= 0.0 => {
6570                            self.rate_limiter_ttl_secs = value;
6571                            Ok(())
6572                        }
6573                        Ok(_) => Err(RuntimeConfigError {
6574                            code: RuntimeConfigErrorCode::InvalidValue,
6575                            message: format!("{} must be >= 0", key),
6576                        }),
6577                        Err(err) => Err(err),
6578                    },
6579                    "global.known_destinations_cleanup_interval_ticks" => {
6580                        match Self::expect_u64(value, &key) {
6581                            Ok(value) if value > 0 => {
6582                                self.known_destinations_cleanup_interval_ticks = value as u32;
6583                                Ok(())
6584                            }
6585                            Ok(_) => Err(RuntimeConfigError {
6586                                code: RuntimeConfigErrorCode::InvalidValue,
6587                                message: format!("{} must be >= 1", key),
6588                            }),
6589                            Err(err) => Err(err),
6590                        }
6591                    }
6592                    "global.announce_cache_cleanup_interval_ticks" => {
6593                        match Self::expect_u64(value, &key) {
6594                            Ok(value) if value > 0 => {
6595                                self.announce_cache_cleanup_interval_ticks = value as u32;
6596                                Ok(())
6597                            }
6598                            Ok(_) => Err(RuntimeConfigError {
6599                                code: RuntimeConfigErrorCode::InvalidValue,
6600                                message: format!("{} must be >= 1", key),
6601                            }),
6602                            Err(err) => Err(err),
6603                        }
6604                    }
6605                    "global.announce_cache_cleanup_batch_size" => {
6606                        match Self::expect_u64(value, &key) {
6607                            Ok(value) if value > 0 => {
6608                                self.announce_cache_cleanup_batch_size = value as usize;
6609                                Ok(())
6610                            }
6611                            Ok(_) => Err(RuntimeConfigError {
6612                                code: RuntimeConfigErrorCode::InvalidValue,
6613                                message: format!("{} must be >= 1", key),
6614                            }),
6615                            Err(err) => Err(err),
6616                        }
6617                    }
6618                    "global.discovery_cleanup_interval_ticks" => {
6619                        match Self::expect_u64(value, &key) {
6620                            Ok(value) if value > 0 => {
6621                                self.discovery_cleanup_interval_ticks = value as u32;
6622                                Ok(())
6623                            }
6624                            Ok(_) => Err(RuntimeConfigError {
6625                                code: RuntimeConfigErrorCode::InvalidValue,
6626                                message: format!("{} must be >= 1", key),
6627                            }),
6628                            Err(err) => Err(err),
6629                        }
6630                    }
6631                    "global.management_announce_interval_secs" => {
6632                        match Self::expect_f64(value, &key) {
6633                            Ok(value) => {
6634                                self.management_announce_interval_secs = value;
6635                                Ok(())
6636                            }
6637                            Err(err) => Err(err),
6638                        }
6639                    }
6640                    "global.direct_connect_policy" => {
6641                        let policy = match Self::parse_holepunch_policy(&value) {
6642                            Some(policy) => policy,
6643                            None => {
6644                                return QueryResponse::RuntimeConfigSet(Err(RuntimeConfigError {
6645                                    code: RuntimeConfigErrorCode::InvalidValue,
6646                                    message: format!(
6647                                        "{} must be one of: reject, accept_all, ask_app",
6648                                        key
6649                                    ),
6650                                }))
6651                            }
6652                        };
6653                        self.holepunch_manager.set_policy(policy);
6654                        Ok(())
6655                    }
6656                    #[cfg(feature = "rns-hooks")]
6657                    "provider.queue_max_events" => match Self::expect_u64(value, &key) {
6658                        Ok(v) if v > 0 => {
6659                            if let Some(ref bridge) = self.provider_bridge {
6660                                bridge.set_queue_max_events(v as usize);
6661                            }
6662                            Ok(())
6663                        }
6664                        Ok(_) => Err(RuntimeConfigError {
6665                            code: RuntimeConfigErrorCode::InvalidValue,
6666                            message: format!("{} must be >= 1", key),
6667                        }),
6668                        Err(err) => Err(err),
6669                    },
6670                    #[cfg(feature = "rns-hooks")]
6671                    "provider.queue_max_bytes" => match Self::expect_u64(value, &key) {
6672                        Ok(v) if v > 0 => {
6673                            if let Some(ref bridge) = self.provider_bridge {
6674                                bridge.set_queue_max_bytes(v as usize);
6675                            }
6676                            Ok(())
6677                        }
6678                        Ok(_) => Err(RuntimeConfigError {
6679                            code: RuntimeConfigErrorCode::InvalidValue,
6680                            message: format!("{} must be >= 1", key),
6681                        }),
6682                        Err(err) => Err(err),
6683                    },
6684                    #[cfg(feature = "iface-backbone")]
6685                    _ if key.starts_with("backbone.") => {
6686                        self.set_backbone_runtime_config(&key, value)
6687                    }
6688                    #[cfg(feature = "iface-backbone")]
6689                    _ if key.starts_with("backbone_client.") => {
6690                        self.set_backbone_client_runtime_config(&key, value)
6691                    }
6692                    #[cfg(feature = "iface-tcp")]
6693                    _ if key.starts_with("tcp_server.") => {
6694                        self.set_tcp_server_runtime_config(&key, value)
6695                    }
6696                    #[cfg(feature = "iface-tcp")]
6697                    _ if key.starts_with("tcp_client.") => {
6698                        self.set_tcp_client_runtime_config(&key, value)
6699                    }
6700                    #[cfg(feature = "iface-udp")]
6701                    _ if key.starts_with("udp.") => self.set_udp_runtime_config(&key, value),
6702                    #[cfg(feature = "iface-auto")]
6703                    _ if key.starts_with("auto.") => self.set_auto_runtime_config(&key, value),
6704                    #[cfg(feature = "iface-i2p")]
6705                    _ if key.starts_with("i2p.") => self.set_i2p_runtime_config(&key, value),
6706                    #[cfg(feature = "iface-pipe")]
6707                    _ if key.starts_with("pipe.") => self.set_pipe_runtime_config(&key, value),
6708                    #[cfg(feature = "iface-rnode")]
6709                    _ if key.starts_with("rnode.") => self.set_rnode_runtime_config(&key, value),
6710                    _ if key.starts_with("interface.") => {
6711                        self.set_generic_interface_runtime_config(&key, value)
6712                    }
6713                    _ => {
6714                        return QueryResponse::RuntimeConfigSet(Err(RuntimeConfigError {
6715                            code: RuntimeConfigErrorCode::UnknownKey,
6716                            message: format!("unknown runtime-config key '{}'", key),
6717                        }))
6718                    }
6719                };
6720
6721                QueryResponse::RuntimeConfigSet(match result {
6722                    Ok(()) => self.runtime_config_entry(&key).ok_or(RuntimeConfigError {
6723                        code: RuntimeConfigErrorCode::ApplyFailed,
6724                        message: format!("failed to read back runtime-config key '{}'", key),
6725                    }),
6726                    Err(err) => Err(err),
6727                })
6728            }
6729            QueryRequest::ResetRuntimeConfig { key } => {
6730                let defaults = self.runtime_config_defaults;
6731                let result = match key.as_str() {
6732                    "global.tick_interval_ms" => {
6733                        self.tick_interval_ms
6734                            .store(defaults.tick_interval_ms, Ordering::Relaxed);
6735                        Ok(())
6736                    }
6737                    "global.known_destinations_ttl_secs" => {
6738                        self.known_destinations_ttl = defaults.known_destinations_ttl;
6739                        Ok(())
6740                    }
6741                    "global.rate_limiter_ttl_secs" => {
6742                        self.rate_limiter_ttl_secs = defaults.rate_limiter_ttl_secs;
6743                        Ok(())
6744                    }
6745                    "global.known_destinations_cleanup_interval_ticks" => {
6746                        self.known_destinations_cleanup_interval_ticks =
6747                            defaults.known_destinations_cleanup_interval_ticks;
6748                        Ok(())
6749                    }
6750                    "global.announce_cache_cleanup_interval_ticks" => {
6751                        self.announce_cache_cleanup_interval_ticks =
6752                            defaults.announce_cache_cleanup_interval_ticks;
6753                        Ok(())
6754                    }
6755                    "global.announce_cache_cleanup_batch_size" => {
6756                        self.announce_cache_cleanup_batch_size =
6757                            defaults.announce_cache_cleanup_batch_size;
6758                        Ok(())
6759                    }
6760                    "global.discovery_cleanup_interval_ticks" => {
6761                        self.discovery_cleanup_interval_ticks =
6762                            defaults.discovery_cleanup_interval_ticks;
6763                        Ok(())
6764                    }
6765                    "global.management_announce_interval_secs" => {
6766                        self.management_announce_interval_secs =
6767                            defaults.management_announce_interval_secs;
6768                        Ok(())
6769                    }
6770                    "global.direct_connect_policy" => {
6771                        self.holepunch_manager
6772                            .set_policy(defaults.direct_connect_policy);
6773                        Ok(())
6774                    }
6775                    #[cfg(feature = "rns-hooks")]
6776                    "provider.queue_max_events" => {
6777                        if let Some(ref bridge) = self.provider_bridge {
6778                            bridge.set_queue_max_events(defaults.provider_queue_max_events);
6779                        }
6780                        Ok(())
6781                    }
6782                    #[cfg(feature = "rns-hooks")]
6783                    "provider.queue_max_bytes" => {
6784                        if let Some(ref bridge) = self.provider_bridge {
6785                            bridge.set_queue_max_bytes(defaults.provider_queue_max_bytes);
6786                        }
6787                        Ok(())
6788                    }
6789                    #[cfg(feature = "iface-backbone")]
6790                    _ if key.starts_with("backbone.") => self.reset_backbone_runtime_config(&key),
6791                    #[cfg(feature = "iface-backbone")]
6792                    _ if key.starts_with("backbone_client.") => {
6793                        self.reset_backbone_client_runtime_config(&key)
6794                    }
6795                    #[cfg(feature = "iface-tcp")]
6796                    _ if key.starts_with("tcp_server.") => {
6797                        self.reset_tcp_server_runtime_config(&key)
6798                    }
6799                    #[cfg(feature = "iface-tcp")]
6800                    _ if key.starts_with("tcp_client.") => {
6801                        self.reset_tcp_client_runtime_config(&key)
6802                    }
6803                    #[cfg(feature = "iface-udp")]
6804                    _ if key.starts_with("udp.") => self.reset_udp_runtime_config(&key),
6805                    #[cfg(feature = "iface-auto")]
6806                    _ if key.starts_with("auto.") => self.reset_auto_runtime_config(&key),
6807                    #[cfg(feature = "iface-i2p")]
6808                    _ if key.starts_with("i2p.") => self.reset_i2p_runtime_config(&key),
6809                    #[cfg(feature = "iface-pipe")]
6810                    _ if key.starts_with("pipe.") => self.reset_pipe_runtime_config(&key),
6811                    #[cfg(feature = "iface-rnode")]
6812                    _ if key.starts_with("rnode.") => self.reset_rnode_runtime_config(&key),
6813                    _ if key.starts_with("interface.") => {
6814                        self.reset_generic_interface_runtime_config(&key)
6815                    }
6816                    _ => {
6817                        return QueryResponse::RuntimeConfigReset(Err(RuntimeConfigError {
6818                            code: RuntimeConfigErrorCode::UnknownKey,
6819                            message: format!("unknown runtime-config key '{}'", key),
6820                        }))
6821                    }
6822                };
6823
6824                QueryResponse::RuntimeConfigReset(match result {
6825                    Ok(()) => self.runtime_config_entry(&key).ok_or(RuntimeConfigError {
6826                        code: RuntimeConfigErrorCode::ApplyFailed,
6827                        message: format!("failed to read back runtime-config key '{}'", key),
6828                    }),
6829                    Err(err) => Err(err),
6830                })
6831            }
6832            other => self.handle_query(other),
6833        }
6834    }
6835
6836    /// Handle a tunnel synthesis packet delivered locally.
6837    fn handle_tunnel_synth_delivery(&mut self, raw: &[u8]) {
6838        // Extract the data payload from the raw packet
6839        let packet = match RawPacket::unpack(raw) {
6840            Ok(p) => p,
6841            Err(_) => return,
6842        };
6843
6844        match rns_core::transport::tunnel::validate_tunnel_synthesize_data(&packet.data) {
6845            Ok(validated) => {
6846                // Find the interface this tunnel belongs to by computing the expected
6847                // tunnel_id for each interface with wants_tunnel
6848                let iface_id = self
6849                    .interfaces
6850                    .iter()
6851                    .find(|(_, entry)| entry.info.wants_tunnel && entry.online && entry.enabled)
6852                    .map(|(id, _)| *id);
6853
6854                if let Some(iface) = iface_id {
6855                    let now = time::now();
6856                    let tunnel_actions = self.engine.handle_tunnel(validated.tunnel_id, iface, now);
6857                    self.dispatch_all(tunnel_actions);
6858                }
6859            }
6860            Err(e) => {
6861                log::debug!("Tunnel synthesis validation failed: {}", e);
6862            }
6863        }
6864    }
6865
6866    /// Synthesize a tunnel on an interface that wants it.
6867    ///
6868    /// Called when an interface with `wants_tunnel` comes up.
6869    fn synthesize_tunnel_for_interface(&mut self, interface: InterfaceId) {
6870        if let Some(ref identity) = self.transport_identity {
6871            let actions = self
6872                .engine
6873                .synthesize_tunnel(identity, interface, &mut self.rng);
6874            self.dispatch_all(actions);
6875        }
6876    }
6877
6878    /// Build and send a path request packet for a destination.
6879    fn handle_request_path(&mut self, dest_hash: [u8; 16]) {
6880        // Build path request data: dest_hash(16) || [transport_id(16)] || random_tag(16)
6881        let mut data = Vec::with_capacity(48);
6882        data.extend_from_slice(&dest_hash);
6883
6884        if self.engine.transport_enabled() {
6885            if let Some(id_hash) = self.engine.identity_hash() {
6886                data.extend_from_slice(id_hash);
6887            }
6888        }
6889
6890        // Random tag (16 bytes)
6891        let mut tag = [0u8; 16];
6892        self.rng.fill_bytes(&mut tag);
6893        data.extend_from_slice(&tag);
6894
6895        // Build as BROADCAST DATA PLAIN packet to rnstransport.path.request
6896        let flags = rns_core::packet::PacketFlags {
6897            header_type: rns_core::constants::HEADER_1,
6898            context_flag: rns_core::constants::FLAG_UNSET,
6899            transport_type: rns_core::constants::TRANSPORT_BROADCAST,
6900            destination_type: rns_core::constants::DESTINATION_PLAIN,
6901            packet_type: rns_core::constants::PACKET_TYPE_DATA,
6902        };
6903
6904        if let Ok(packet) = RawPacket::pack(
6905            flags,
6906            0,
6907            &self.path_request_dest,
6908            None,
6909            rns_core::constants::CONTEXT_NONE,
6910            &data,
6911        ) {
6912            let actions = self.engine.handle_outbound(
6913                &packet,
6914                rns_core::constants::DESTINATION_PLAIN,
6915                None,
6916                time::now(),
6917            );
6918            self.dispatch_all(actions);
6919        }
6920    }
6921
6922    /// Check if we should generate a proof for a delivered packet,
6923    /// and if so, sign and send it.
6924    fn maybe_generate_proof(&mut self, dest_hash: [u8; 16], packet_hash: &[u8; 32]) {
6925        use rns_core::types::ProofStrategy;
6926
6927        let (strategy, identity) = match self.proof_strategies.get(&dest_hash) {
6928            Some((s, id)) => (*s, id.as_ref()),
6929            None => return,
6930        };
6931
6932        let should_prove = match strategy {
6933            ProofStrategy::ProveAll => true,
6934            ProofStrategy::ProveApp => self.callbacks.on_proof_requested(
6935                rns_core::types::DestHash(dest_hash),
6936                rns_core::types::PacketHash(*packet_hash),
6937            ),
6938            ProofStrategy::ProveNone => false,
6939        };
6940
6941        if !should_prove {
6942            return;
6943        }
6944
6945        let identity = match identity {
6946            Some(id) => id,
6947            None => {
6948                log::warn!(
6949                    "Cannot generate proof for {:02x?}: no signing key",
6950                    &dest_hash[..4]
6951                );
6952                return;
6953            }
6954        };
6955
6956        // Sign the packet hash to create the proof
6957        let signature = match identity.sign(packet_hash) {
6958            Ok(sig) => sig,
6959            Err(e) => {
6960                log::warn!("Failed to sign proof for {:02x?}: {:?}", &dest_hash[..4], e);
6961                return;
6962            }
6963        };
6964
6965        // Build explicit proof: [packet_hash:32][signature:64]
6966        let mut proof_data = Vec::with_capacity(96);
6967        proof_data.extend_from_slice(packet_hash);
6968        proof_data.extend_from_slice(&signature);
6969
6970        // Address the proof to the truncated packet hash (first 16 bytes),
6971        // matching Python's ProofDestination (Packet.py:390-394).
6972        // Transport nodes create reverse_table entries keyed by truncated
6973        // packet hash when forwarding data, so this allows proofs to be
6974        // routed back to the sender via the reverse path.
6975        let mut proof_dest = [0u8; 16];
6976        proof_dest.copy_from_slice(&packet_hash[..16]);
6977
6978        let flags = rns_core::packet::PacketFlags {
6979            header_type: rns_core::constants::HEADER_1,
6980            context_flag: rns_core::constants::FLAG_UNSET,
6981            transport_type: rns_core::constants::TRANSPORT_BROADCAST,
6982            destination_type: rns_core::constants::DESTINATION_SINGLE,
6983            packet_type: rns_core::constants::PACKET_TYPE_PROOF,
6984        };
6985
6986        if let Ok(packet) = RawPacket::pack(
6987            flags,
6988            0,
6989            &proof_dest,
6990            None,
6991            rns_core::constants::CONTEXT_NONE,
6992            &proof_data,
6993        ) {
6994            let actions = self.engine.handle_outbound(
6995                &packet,
6996                rns_core::constants::DESTINATION_SINGLE,
6997                None,
6998                time::now(),
6999            );
7000            self.dispatch_all(actions);
7001            log::debug!(
7002                "Generated proof for packet on dest {:02x?}",
7003                &dest_hash[..4]
7004            );
7005        }
7006    }
7007
7008    /// Handle an inbound proof packet: validate and fire on_proof callback.
7009    fn handle_inbound_proof(
7010        &mut self,
7011        dest_hash: [u8; 16],
7012        proof_data: &[u8],
7013        _raw_packet_hash: &[u8; 32],
7014    ) {
7015        // Reticulum supports both proof formats:
7016        // - explicit: [packet_hash:32][signature:64]
7017        // - implicit: [signature:64], keyed by proof destination hash
7018        let (tracked_hash, signature): ([u8; 32], &[u8]) = if proof_data.len() >= 96 {
7019            let mut tracked_hash = [0u8; 32];
7020            tracked_hash.copy_from_slice(&proof_data[..32]);
7021            (tracked_hash, &proof_data[32..96])
7022        } else if proof_data.len() == 64 {
7023            let mut candidates = self
7024                .sent_packets
7025                .iter()
7026                .filter_map(|(packet_hash, _)| {
7027                    if packet_hash[..16] == dest_hash {
7028                        Some(*packet_hash)
7029                    } else {
7030                        None
7031                    }
7032                })
7033                .collect::<Vec<_>>();
7034
7035            if candidates.is_empty() {
7036                log::debug!(
7037                    "Implicit proof for unknown packet prefix {:02x?} on dest {:02x?}",
7038                    &dest_hash[..4],
7039                    &dest_hash[..4]
7040                );
7041                return;
7042            }
7043
7044            // Multiple matches are extremely unlikely (16-byte truncated hash).
7045            // Use the newest tracked packet for deterministic behavior.
7046            if candidates.len() > 1 {
7047                candidates.sort_by(|a, b| {
7048                    let ta = self
7049                        .sent_packets
7050                        .get(a)
7051                        .map(|(_, t)| *t)
7052                        .unwrap_or_default();
7053                    let tb = self
7054                        .sent_packets
7055                        .get(b)
7056                        .map(|(_, t)| *t)
7057                        .unwrap_or_default();
7058                    tb.partial_cmp(&ta).unwrap_or(core::cmp::Ordering::Equal)
7059                });
7060                log::debug!(
7061                    "Implicit proof matched {} candidates for prefix {:02x?}; using newest",
7062                    candidates.len(),
7063                    &dest_hash[..4]
7064                );
7065            }
7066
7067            (candidates[0], &proof_data[..64])
7068        } else {
7069            log::debug!("Unsupported proof length: {} bytes", proof_data.len());
7070            return;
7071        };
7072
7073        // Look up the tracked sent packet
7074        if let Some((tracked_dest, sent_time)) = self.sent_packets.remove(&tracked_hash) {
7075            // Validate the proof signature using the destination's public key
7076            // (matches Python's PacketReceipt.validate_proof behavior)
7077            if let Some(announced) = self.known_destination_announced(&tracked_dest) {
7078                let identity =
7079                    rns_crypto::identity::Identity::from_public_key(&announced.public_key);
7080                let mut sig = [0u8; 64];
7081                sig.copy_from_slice(signature);
7082                if !identity.verify(&sig, &tracked_hash) {
7083                    log::debug!("Proof signature invalid for {:02x?}", &tracked_hash[..4],);
7084                    return;
7085                }
7086                let _ = self.mark_known_destination_used(&tracked_dest);
7087            } else {
7088                log::debug!(
7089                    "No known identity for dest {:02x?}, accepting proof without signature check",
7090                    &tracked_dest[..4],
7091                );
7092            }
7093
7094            let now = time::now();
7095            let rtt = now - sent_time;
7096            log::debug!(
7097                "Proof received for {:02x?} rtt={:.3}s",
7098                &tracked_hash[..4],
7099                rtt,
7100            );
7101            self.completed_proofs.insert(tracked_hash, (rtt, now));
7102            self.callbacks.on_proof(
7103                rns_core::types::DestHash(tracked_dest),
7104                rns_core::types::PacketHash(tracked_hash),
7105                rtt,
7106            );
7107        } else {
7108            log::debug!(
7109                "Proof for unknown packet {:02x?} on dest {:02x?}",
7110                &tracked_hash[..4],
7111                &dest_hash[..4],
7112            );
7113        }
7114    }
7115
7116    fn interface_send_deferred(entry: &InterfaceEntry, now: Instant) -> bool {
7117        matches!(entry.send_retry_at, Some(retry_at) if now < retry_at)
7118    }
7119
7120    fn record_send_result(
7121        entry: &mut InterfaceEntry,
7122        result: &std::io::Result<()>,
7123        context: &str,
7124        interface_id: InterfaceId,
7125    ) {
7126        match result {
7127            Ok(()) => {
7128                entry.send_retry_at = None;
7129                entry.send_retry_backoff = Duration::ZERO;
7130            }
7131            Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
7132                let next_backoff = if entry.send_retry_backoff.is_zero() {
7133                    SEND_RETRY_BACKOFF_MIN
7134                } else {
7135                    (entry.send_retry_backoff * 2).min(SEND_RETRY_BACKOFF_MAX)
7136                };
7137                entry.send_retry_backoff = next_backoff;
7138                entry.send_retry_at = Some(Instant::now() + next_backoff);
7139                log::debug!(
7140                    "[{}] {} deferred after WouldBlock; retry in {:?}",
7141                    interface_id.0,
7142                    context,
7143                    next_backoff
7144                );
7145            }
7146            Err(e) => {
7147                entry.send_retry_at = None;
7148                entry.send_retry_backoff = Duration::ZERO;
7149                log::warn!("[{}] {} failed: {}", interface_id.0, context, e);
7150            }
7151        }
7152    }
7153
7154    fn dispatch_send_on_interface_action(
7155        &mut self,
7156        interface: InterfaceId,
7157        raw: Vec<u8>,
7158        _hook_injected: &mut Vec<TransportAction>,
7159    ) {
7160        #[cfg(feature = "rns-hooks")]
7161        {
7162            let pkt_ctx = rns_hooks::PacketContext {
7163                flags: if raw.is_empty() { 0 } else { raw[0] },
7164                hops: if raw.len() > 1 { raw[1] } else { 0 },
7165                destination_hash: extract_dest_hash(&raw),
7166                context: 0,
7167                packet_hash: [0; 32],
7168                interface_id: interface.0,
7169                data_offset: 0,
7170                data_len: raw.len() as u32,
7171            };
7172            let ctx = HookContext::Packet {
7173                ctx: &pkt_ctx,
7174                raw: &raw,
7175            };
7176            let now = time::now();
7177            let engine_ref = EngineRef {
7178                engine: &self.engine,
7179                interfaces: &self.interfaces,
7180                link_manager: &self.link_manager,
7181                now,
7182            };
7183            let provider_events_enabled = self.provider_events_enabled();
7184            if let Some(ref e) = run_hook_inner(
7185                &mut self.hook_slots[HookPoint::SendOnInterface as usize].programs,
7186                &self.hook_manager,
7187                &engine_ref,
7188                &ctx,
7189                now,
7190                provider_events_enabled,
7191            ) {
7192                self.collect_hook_side_effects("SendOnInterface", e, _hook_injected);
7193                if e.hook_result.as_ref().is_some_and(|r| r.is_drop()) {
7194                    return;
7195                }
7196            }
7197        }
7198
7199        let is_announce = raw.len() > 2 && (raw[0] & 0x03) == 0x01;
7200        if is_announce {
7201            log::debug!(
7202                "Announce:dispatching to iface {} (len={}, online={})",
7203                interface.0,
7204                raw.len(),
7205                self.interfaces
7206                    .get(&interface)
7207                    .map(|e| e.online && e.enabled)
7208                    .unwrap_or(false)
7209            );
7210        }
7211        if let Some(entry) = self.interfaces.get_mut(&interface) {
7212            if entry.online && entry.enabled {
7213                if Self::interface_send_deferred(entry, Instant::now()) {
7214                    return;
7215                }
7216                let data = if let Some(ref ifac_state) = entry.ifac {
7217                    ifac::mask_outbound(&raw, ifac_state)
7218                } else {
7219                    raw
7220                };
7221                entry.stats.txb += data.len() as u64;
7222                entry.stats.tx_packets += 1;
7223                if is_announce {
7224                    entry.stats.record_outgoing_announce(time::now());
7225                }
7226                let send_result = entry.writer.send_frame(&data);
7227                let sent_ok = send_result.is_ok();
7228                Self::record_send_result(entry, &send_result, "send", interface);
7229                if sent_ok && is_announce {
7230                    let header_type = (data[0] >> 6) & 0x03;
7231                    let dest_start = if header_type == 1 { 18usize } else { 2usize };
7232                    let dest_preview = if data.len() >= dest_start + 4 {
7233                        format!("{:02x?}", &data[dest_start..dest_start + 4])
7234                    } else {
7235                        "??".into()
7236                    };
7237                    log::debug!(
7238                        "Announce:SENT on iface {} (len={}, h={}, dest=[{}])",
7239                        interface.0,
7240                        data.len(),
7241                        header_type,
7242                        dest_preview
7243                    );
7244                }
7245            }
7246        }
7247    }
7248
7249    fn dispatch_broadcast_action(
7250        &mut self,
7251        raw: Vec<u8>,
7252        exclude: Option<InterfaceId>,
7253        _hook_injected: &mut Vec<TransportAction>,
7254    ) {
7255        #[cfg(feature = "rns-hooks")]
7256        {
7257            let pkt_ctx = rns_hooks::PacketContext {
7258                flags: if raw.is_empty() { 0 } else { raw[0] },
7259                hops: if raw.len() > 1 { raw[1] } else { 0 },
7260                destination_hash: extract_dest_hash(&raw),
7261                context: 0,
7262                packet_hash: [0; 32],
7263                interface_id: 0,
7264                data_offset: 0,
7265                data_len: raw.len() as u32,
7266            };
7267            let ctx = HookContext::Packet {
7268                ctx: &pkt_ctx,
7269                raw: &raw,
7270            };
7271            let now = time::now();
7272            let engine_ref = EngineRef {
7273                engine: &self.engine,
7274                interfaces: &self.interfaces,
7275                link_manager: &self.link_manager,
7276                now,
7277            };
7278            let provider_events_enabled = self.provider_events_enabled();
7279            if let Some(ref e) = run_hook_inner(
7280                &mut self.hook_slots[HookPoint::BroadcastOnAllInterfaces as usize].programs,
7281                &self.hook_manager,
7282                &engine_ref,
7283                &ctx,
7284                now,
7285                provider_events_enabled,
7286            ) {
7287                self.collect_hook_side_effects("BroadcastOnAllInterfaces", e, _hook_injected);
7288                if e.hook_result.as_ref().is_some_and(|r| r.is_drop()) {
7289                    return;
7290                }
7291            }
7292        }
7293
7294        let is_announce = raw.len() > 2 && (raw[0] & 0x03) == 0x01;
7295        for entry in self.interfaces.values_mut() {
7296            if entry.online && entry.enabled && Some(entry.id) != exclude {
7297                if Self::interface_send_deferred(entry, Instant::now()) {
7298                    continue;
7299                }
7300                let data = if let Some(ref ifac_state) = entry.ifac {
7301                    ifac::mask_outbound(&raw, ifac_state)
7302                } else {
7303                    raw.clone()
7304                };
7305                entry.stats.txb += data.len() as u64;
7306                entry.stats.tx_packets += 1;
7307                if is_announce {
7308                    entry.stats.record_outgoing_announce(time::now());
7309                }
7310                let send_result = entry.writer.send_frame(&data);
7311                Self::record_send_result(entry, &send_result, "broadcast", entry.id);
7312            }
7313        }
7314    }
7315
7316    fn dispatch_deliver_local_action(
7317        &mut self,
7318        destination_hash: [u8; 16],
7319        raw: Vec<u8>,
7320        packet_hash: [u8; 32],
7321        receiving_interface: InterfaceId,
7322        _hook_injected: &mut Vec<TransportAction>,
7323    ) {
7324        #[cfg(feature = "rns-hooks")]
7325        {
7326            let pkt_ctx = rns_hooks::PacketContext {
7327                flags: 0,
7328                hops: 0,
7329                destination_hash,
7330                context: 0,
7331                packet_hash,
7332                interface_id: receiving_interface.0,
7333                data_offset: 0,
7334                data_len: raw.len() as u32,
7335            };
7336            let ctx = HookContext::Packet {
7337                ctx: &pkt_ctx,
7338                raw: &raw,
7339            };
7340            let now = time::now();
7341            let engine_ref = EngineRef {
7342                engine: &self.engine,
7343                interfaces: &self.interfaces,
7344                link_manager: &self.link_manager,
7345                now,
7346            };
7347            let provider_events_enabled = self.provider_events_enabled();
7348            if let Some(ref e) = run_hook_inner(
7349                &mut self.hook_slots[HookPoint::DeliverLocal as usize].programs,
7350                &self.hook_manager,
7351                &engine_ref,
7352                &ctx,
7353                now,
7354                provider_events_enabled,
7355            ) {
7356                self.collect_hook_side_effects("DeliverLocal", e, _hook_injected);
7357                if e.hook_result.as_ref().is_some_and(|r| r.is_drop()) {
7358                    return;
7359                }
7360            }
7361        }
7362
7363        if destination_hash == self.tunnel_synth_dest {
7364            self.handle_tunnel_synth_delivery(&raw);
7365        } else if destination_hash == self.path_request_dest {
7366            if let Ok(packet) = RawPacket::unpack(&raw) {
7367                let actions =
7368                    self.engine
7369                        .handle_path_request(&packet.data, receiving_interface, time::now());
7370                self.dispatch_all(actions);
7371            }
7372        } else if self.link_manager.is_link_destination(&destination_hash) {
7373            let link_actions = self.link_manager.handle_local_delivery(
7374                destination_hash,
7375                &raw,
7376                packet_hash,
7377                receiving_interface,
7378                &mut self.rng,
7379            );
7380            if link_actions.is_empty() {
7381                if let Ok(packet) = RawPacket::unpack(&raw) {
7382                    if packet.flags.packet_type == rns_core::constants::PACKET_TYPE_PROOF {
7383                        self.handle_inbound_proof(destination_hash, &packet.data, &packet_hash);
7384                        return;
7385                    }
7386                }
7387                self.maybe_generate_proof(destination_hash, &packet_hash);
7388                self.callbacks.on_local_delivery(
7389                    rns_core::types::DestHash(destination_hash),
7390                    raw,
7391                    rns_core::types::PacketHash(packet_hash),
7392                );
7393            } else {
7394                self.dispatch_link_actions(link_actions);
7395            }
7396        } else {
7397            if let Ok(packet) = RawPacket::unpack(&raw) {
7398                if packet.flags.packet_type == rns_core::constants::PACKET_TYPE_PROOF {
7399                    self.handle_inbound_proof(destination_hash, &packet.data, &packet_hash);
7400                    return;
7401                }
7402            }
7403            self.maybe_generate_proof(destination_hash, &packet_hash);
7404            self.callbacks.on_local_delivery(
7405                rns_core::types::DestHash(destination_hash),
7406                raw,
7407                rns_core::types::PacketHash(packet_hash),
7408            );
7409        }
7410    }
7411
7412    /// Dispatch a list of transport actions.
7413    fn dispatch_all(&mut self, actions: Vec<TransportAction>) {
7414        #[cfg(feature = "rns-hooks")]
7415        let mut hook_injected: Vec<TransportAction> = Vec::new();
7416        #[cfg(not(feature = "rns-hooks"))]
7417        let mut hook_injected: Vec<TransportAction> = Vec::new();
7418
7419        for action in actions {
7420            match action {
7421                TransportAction::SendOnInterface { interface, raw } => {
7422                    self.dispatch_send_on_interface_action(interface, raw, &mut hook_injected);
7423                }
7424                TransportAction::BroadcastOnAllInterfaces { raw, exclude } => {
7425                    self.dispatch_broadcast_action(raw, exclude, &mut hook_injected);
7426                }
7427                TransportAction::DeliverLocal {
7428                    destination_hash,
7429                    raw,
7430                    packet_hash,
7431                    receiving_interface,
7432                } => {
7433                    self.dispatch_deliver_local_action(
7434                        destination_hash,
7435                        raw,
7436                        packet_hash,
7437                        receiving_interface,
7438                        &mut hook_injected,
7439                    );
7440                }
7441                TransportAction::AnnounceReceived {
7442                    destination_hash,
7443                    identity_hash,
7444                    public_key,
7445                    name_hash,
7446                    app_data,
7447                    hops,
7448                    receiving_interface,
7449                    ..
7450                } => {
7451                    #[cfg(feature = "rns-hooks")]
7452                    {
7453                        let ctx = HookContext::Announce {
7454                            destination_hash,
7455                            hops,
7456                            interface_id: receiving_interface.0,
7457                        };
7458                        let now = time::now();
7459                        let engine_ref = EngineRef {
7460                            engine: &self.engine,
7461                            interfaces: &self.interfaces,
7462                            link_manager: &self.link_manager,
7463                            now,
7464                        };
7465                        let provider_events_enabled = self.provider_events_enabled();
7466                        {
7467                            let exec = run_hook_inner(
7468                                &mut self.hook_slots[HookPoint::AnnounceReceived as usize].programs,
7469                                &self.hook_manager,
7470                                &engine_ref,
7471                                &ctx,
7472                                now,
7473                                provider_events_enabled,
7474                            );
7475                            if let Some(ref e) = exec {
7476                                self.collect_hook_side_effects(
7477                                    "AnnounceReceived",
7478                                    e,
7479                                    &mut hook_injected,
7480                                );
7481                                if e.hook_result.as_ref().map_or(false, |r| r.is_drop()) {
7482                                    continue;
7483                                }
7484                            }
7485                        }
7486                    }
7487
7488                    // Check if this is a discovery announce (matched by name_hash
7489                    // since discovery is a SINGLE destination — its dest hash varies
7490                    // with the sender's identity).
7491                    if name_hash == self.discovery_name_hash {
7492                        if self.discover_interfaces {
7493                            if let Some(ref app_data) = app_data {
7494                                if let Some(mut discovered) =
7495                                    crate::discovery::parse_interface_announce(
7496                                        app_data,
7497                                        &identity_hash,
7498                                        hops,
7499                                        self.discovery_required_value,
7500                                    )
7501                                {
7502                                    // Check if we already have this interface
7503                                    if let Ok(Some(existing)) =
7504                                        self.discovered_interfaces.load(&discovered.discovery_hash)
7505                                    {
7506                                        discovered.discovered = existing.discovered;
7507                                        discovered.heard_count = existing.heard_count + 1;
7508                                    }
7509                                    if let Err(e) = self.discovered_interfaces.store(&discovered) {
7510                                        log::warn!("Failed to store discovered interface: {}", e);
7511                                    } else {
7512                                        log::debug!(
7513                                            "Discovered interface '{}' ({}) at {}:{} [stamp={}]",
7514                                            discovered.name,
7515                                            discovered.interface_type,
7516                                            discovered.reachable_on.as_deref().unwrap_or("?"),
7517                                            discovered
7518                                                .port
7519                                                .map(|p| p.to_string())
7520                                                .unwrap_or_else(|| "?".into()),
7521                                            discovered.stamp_value,
7522                                        );
7523                                    }
7524                                }
7525                            }
7526                        }
7527                        // Still cache the identity and notify callbacks
7528                    }
7529
7530                    // Cache the announced identity
7531                    let announced = crate::destination::AnnouncedIdentity {
7532                        dest_hash: rns_core::types::DestHash(destination_hash),
7533                        identity_hash: rns_core::types::IdentityHash(identity_hash),
7534                        public_key,
7535                        app_data: app_data.clone(),
7536                        hops,
7537                        received_at: time::now(),
7538                        receiving_interface,
7539                    };
7540                    self.upsert_known_destination(destination_hash, announced.clone());
7541                    log::info!(
7542                        "Announce:validated dest={:02x}{:02x}{:02x}{:02x}.. hops={}",
7543                        destination_hash[0],
7544                        destination_hash[1],
7545                        destination_hash[2],
7546                        destination_hash[3],
7547                        hops,
7548                    );
7549                    self.callbacks.on_announce(announced);
7550                }
7551                TransportAction::PathUpdated {
7552                    destination_hash,
7553                    hops,
7554                    interface,
7555                    ..
7556                } => {
7557                    #[cfg(feature = "rns-hooks")]
7558                    {
7559                        let ctx = HookContext::Announce {
7560                            destination_hash,
7561                            hops,
7562                            interface_id: interface.0,
7563                        };
7564                        let now = time::now();
7565                        let engine_ref = EngineRef {
7566                            engine: &self.engine,
7567                            interfaces: &self.interfaces,
7568                            link_manager: &self.link_manager,
7569                            now,
7570                        };
7571                        let provider_events_enabled = self.provider_events_enabled();
7572                        if let Some(ref e) = run_hook_inner(
7573                            &mut self.hook_slots[HookPoint::PathUpdated as usize].programs,
7574                            &self.hook_manager,
7575                            &engine_ref,
7576                            &ctx,
7577                            now,
7578                            provider_events_enabled,
7579                        ) {
7580                            self.collect_hook_side_effects("PathUpdated", e, &mut hook_injected);
7581                        }
7582                    }
7583                    #[cfg(not(feature = "rns-hooks"))]
7584                    let _ = interface;
7585
7586                    let _ = self.mark_known_destination_used(&destination_hash);
7587                    self.callbacks
7588                        .on_path_updated(rns_core::types::DestHash(destination_hash), hops);
7589                }
7590                TransportAction::ForwardToLocalClients { raw, exclude } => {
7591                    for entry in self.interfaces.values_mut() {
7592                        if entry.online
7593                            && entry.enabled
7594                            && entry.info.is_local_client
7595                            && Some(entry.id) != exclude
7596                        {
7597                            if Self::interface_send_deferred(entry, Instant::now()) {
7598                                continue;
7599                            }
7600                            let data = if let Some(ref ifac_state) = entry.ifac {
7601                                ifac::mask_outbound(&raw, ifac_state)
7602                            } else {
7603                                raw.clone()
7604                            };
7605                            entry.stats.txb += data.len() as u64;
7606                            entry.stats.tx_packets += 1;
7607                            let send_result = entry.writer.send_frame(&data);
7608                            Self::record_send_result(
7609                                entry,
7610                                &send_result,
7611                                "forward to local client",
7612                                entry.id,
7613                            );
7614                        }
7615                    }
7616                }
7617                TransportAction::ForwardPlainBroadcast {
7618                    raw,
7619                    to_local,
7620                    exclude,
7621                } => {
7622                    for entry in self.interfaces.values_mut() {
7623                        if entry.online
7624                            && entry.enabled
7625                            && entry.info.is_local_client == to_local
7626                            && Some(entry.id) != exclude
7627                        {
7628                            if Self::interface_send_deferred(entry, Instant::now()) {
7629                                continue;
7630                            }
7631                            let data = if let Some(ref ifac_state) = entry.ifac {
7632                                ifac::mask_outbound(&raw, ifac_state)
7633                            } else {
7634                                raw.clone()
7635                            };
7636                            entry.stats.txb += data.len() as u64;
7637                            entry.stats.tx_packets += 1;
7638                            let send_result = entry.writer.send_frame(&data);
7639                            Self::record_send_result(
7640                                entry,
7641                                &send_result,
7642                                "forward plain broadcast",
7643                                entry.id,
7644                            );
7645                        }
7646                    }
7647                }
7648                TransportAction::CacheAnnounce { packet_hash, raw } => {
7649                    if let Some(ref cache) = self.announce_cache {
7650                        if let Err(e) = cache.store(&packet_hash, &raw, None) {
7651                            log::warn!("Failed to cache announce: {}", e);
7652                        }
7653                    }
7654                }
7655                TransportAction::TunnelSynthesize {
7656                    interface,
7657                    data,
7658                    dest_hash,
7659                } => {
7660                    #[cfg(feature = "rns-hooks")]
7661                    {
7662                        let pkt_ctx = rns_hooks::PacketContext {
7663                            flags: 0,
7664                            hops: 0,
7665                            destination_hash: dest_hash,
7666                            context: 0,
7667                            packet_hash: [0; 32],
7668                            interface_id: interface.0,
7669                            data_offset: 0,
7670                            data_len: data.len() as u32,
7671                        };
7672                        let ctx = HookContext::Packet {
7673                            ctx: &pkt_ctx,
7674                            raw: &data,
7675                        };
7676                        let now = time::now();
7677                        let engine_ref = EngineRef {
7678                            engine: &self.engine,
7679                            interfaces: &self.interfaces,
7680                            link_manager: &self.link_manager,
7681                            now,
7682                        };
7683                        let provider_events_enabled = self.provider_events_enabled();
7684                        {
7685                            let exec = run_hook_inner(
7686                                &mut self.hook_slots[HookPoint::TunnelSynthesize as usize].programs,
7687                                &self.hook_manager,
7688                                &engine_ref,
7689                                &ctx,
7690                                now,
7691                                provider_events_enabled,
7692                            );
7693                            if let Some(ref e) = exec {
7694                                self.collect_hook_side_effects(
7695                                    "TunnelSynthesize",
7696                                    e,
7697                                    &mut hook_injected,
7698                                );
7699                                if e.hook_result.as_ref().map_or(false, |r| r.is_drop()) {
7700                                    continue;
7701                                }
7702                            }
7703                        }
7704                    }
7705                    // Pack as BROADCAST DATA PLAIN packet and send on interface
7706                    let flags = rns_core::packet::PacketFlags {
7707                        header_type: rns_core::constants::HEADER_1,
7708                        context_flag: rns_core::constants::FLAG_UNSET,
7709                        transport_type: rns_core::constants::TRANSPORT_BROADCAST,
7710                        destination_type: rns_core::constants::DESTINATION_PLAIN,
7711                        packet_type: rns_core::constants::PACKET_TYPE_DATA,
7712                    };
7713                    if let Ok(packet) = rns_core::packet::RawPacket::pack(
7714                        flags,
7715                        0,
7716                        &dest_hash,
7717                        None,
7718                        rns_core::constants::CONTEXT_NONE,
7719                        &data,
7720                    ) {
7721                        if let Some(entry) = self.interfaces.get_mut(&interface) {
7722                            if entry.online && entry.enabled {
7723                                let raw = if let Some(ref ifac_state) = entry.ifac {
7724                                    ifac::mask_outbound(&packet.raw, ifac_state)
7725                                } else {
7726                                    packet.raw
7727                                };
7728                                entry.stats.txb += raw.len() as u64;
7729                                entry.stats.tx_packets += 1;
7730                                if let Err(e) = entry.writer.send_frame(&raw) {
7731                                    log::warn!(
7732                                        "[{}] tunnel synthesize send failed: {}",
7733                                        entry.info.id.0,
7734                                        e
7735                                    );
7736                                }
7737                            }
7738                        }
7739                    }
7740                }
7741                TransportAction::TunnelEstablished {
7742                    tunnel_id,
7743                    interface,
7744                } => {
7745                    log::info!(
7746                        "Tunnel established: {:02x?} on interface {}",
7747                        &tunnel_id[..4],
7748                        interface.0
7749                    );
7750                }
7751                TransportAction::AnnounceRetransmit {
7752                    destination_hash,
7753                    hops,
7754                    interface,
7755                } => {
7756                    #[cfg(feature = "rns-hooks")]
7757                    {
7758                        let ctx = HookContext::Announce {
7759                            destination_hash,
7760                            hops,
7761                            interface_id: interface.map(|i| i.0).unwrap_or(0),
7762                        };
7763                        let now = time::now();
7764                        let engine_ref = EngineRef {
7765                            engine: &self.engine,
7766                            interfaces: &self.interfaces,
7767                            link_manager: &self.link_manager,
7768                            now,
7769                        };
7770                        let provider_events_enabled = self.provider_events_enabled();
7771                        if let Some(ref e) = run_hook_inner(
7772                            &mut self.hook_slots[HookPoint::AnnounceRetransmit as usize].programs,
7773                            &self.hook_manager,
7774                            &engine_ref,
7775                            &ctx,
7776                            now,
7777                            provider_events_enabled,
7778                        ) {
7779                            self.collect_hook_side_effects(
7780                                "AnnounceRetransmit",
7781                                e,
7782                                &mut hook_injected,
7783                            );
7784                        }
7785                    }
7786                    #[cfg(not(feature = "rns-hooks"))]
7787                    {
7788                        let _ = (destination_hash, hops, interface);
7789                    }
7790                }
7791                TransportAction::LinkRequestReceived {
7792                    link_id,
7793                    destination_hash: _,
7794                    receiving_interface,
7795                } => {
7796                    #[cfg(feature = "rns-hooks")]
7797                    {
7798                        let ctx = HookContext::Link {
7799                            link_id,
7800                            interface_id: receiving_interface.0,
7801                        };
7802                        let now = time::now();
7803                        let engine_ref = EngineRef {
7804                            engine: &self.engine,
7805                            interfaces: &self.interfaces,
7806                            link_manager: &self.link_manager,
7807                            now,
7808                        };
7809                        let provider_events_enabled = self.provider_events_enabled();
7810                        if let Some(ref e) = run_hook_inner(
7811                            &mut self.hook_slots[HookPoint::LinkRequestReceived as usize].programs,
7812                            &self.hook_manager,
7813                            &engine_ref,
7814                            &ctx,
7815                            now,
7816                            provider_events_enabled,
7817                        ) {
7818                            self.collect_hook_side_effects(
7819                                "LinkRequestReceived",
7820                                e,
7821                                &mut hook_injected,
7822                            );
7823                        }
7824                    }
7825                    #[cfg(not(feature = "rns-hooks"))]
7826                    {
7827                        let _ = (link_id, receiving_interface);
7828                    }
7829                }
7830                TransportAction::LinkEstablished { link_id, interface } => {
7831                    #[cfg(feature = "rns-hooks")]
7832                    {
7833                        let ctx = HookContext::Link {
7834                            link_id,
7835                            interface_id: interface.0,
7836                        };
7837                        let now = time::now();
7838                        let engine_ref = EngineRef {
7839                            engine: &self.engine,
7840                            interfaces: &self.interfaces,
7841                            link_manager: &self.link_manager,
7842                            now,
7843                        };
7844                        let provider_events_enabled = self.provider_events_enabled();
7845                        if let Some(ref e) = run_hook_inner(
7846                            &mut self.hook_slots[HookPoint::LinkEstablished as usize].programs,
7847                            &self.hook_manager,
7848                            &engine_ref,
7849                            &ctx,
7850                            now,
7851                            provider_events_enabled,
7852                        ) {
7853                            self.collect_hook_side_effects(
7854                                "LinkEstablished",
7855                                e,
7856                                &mut hook_injected,
7857                            );
7858                        }
7859                    }
7860                    #[cfg(not(feature = "rns-hooks"))]
7861                    {
7862                        let _ = (link_id, interface);
7863                    }
7864                }
7865                TransportAction::LinkClosed { link_id } => {
7866                    #[cfg(feature = "rns-hooks")]
7867                    {
7868                        let ctx = HookContext::Link {
7869                            link_id,
7870                            interface_id: 0,
7871                        };
7872                        let now = time::now();
7873                        let engine_ref = EngineRef {
7874                            engine: &self.engine,
7875                            interfaces: &self.interfaces,
7876                            link_manager: &self.link_manager,
7877                            now,
7878                        };
7879                        let provider_events_enabled = self.provider_events_enabled();
7880                        if let Some(ref e) = run_hook_inner(
7881                            &mut self.hook_slots[HookPoint::LinkClosed as usize].programs,
7882                            &self.hook_manager,
7883                            &engine_ref,
7884                            &ctx,
7885                            now,
7886                            provider_events_enabled,
7887                        ) {
7888                            self.collect_hook_side_effects("LinkClosed", e, &mut hook_injected);
7889                        }
7890                    }
7891                    #[cfg(not(feature = "rns-hooks"))]
7892                    {
7893                        let _ = link_id;
7894                    }
7895                }
7896            }
7897        }
7898
7899        // Dispatch any actions injected by hooks during action processing
7900        #[cfg(feature = "rns-hooks")]
7901        if !hook_injected.is_empty() {
7902            self.dispatch_all(hook_injected);
7903        }
7904    }
7905
7906    /// Dispatch link manager actions.
7907    fn dispatch_link_actions(&mut self, actions: Vec<LinkManagerAction>) {
7908        #[cfg(feature = "rns-hooks")]
7909        let mut hook_injected: Vec<TransportAction> = Vec::new();
7910
7911        for action in actions {
7912            match action {
7913                LinkManagerAction::SendPacket {
7914                    mut raw,
7915                    dest_type,
7916                    mut attached_interface,
7917                } => {
7918                    if dest_type == rns_core::constants::DESTINATION_LINK
7919                        && attached_interface.is_none()
7920                    {
7921                        if let Ok(packet) = RawPacket::unpack(&raw) {
7922                            let link_id = packet.destination_hash;
7923                            if let Some((iface, transport_id)) =
7924                                self.link_manager.get_link_route_hint(&link_id)
7925                            {
7926                                attached_interface = Some(iface);
7927                                if packet.flags.header_type == rns_core::constants::HEADER_1 {
7928                                    if let Some(next_hop) = transport_id {
7929                                        raw = inject_transport_header(&packet.raw, &next_hop);
7930                                        log::debug!(
7931                                            "Link SendPacket rewrite: link={:02x?} iface={} header=1->2 tid={:02x?}",
7932                                            &link_id[..4],
7933                                            iface.0,
7934                                            &next_hop[..4]
7935                                        );
7936                                    } else {
7937                                        log::debug!(
7938                                            "Link SendPacket route: link={:02x?} iface={} header=1 (no transport_id)",
7939                                            &link_id[..4],
7940                                            iface.0
7941                                        );
7942                                    }
7943                                }
7944                            } else {
7945                                log::debug!(
7946                                    "Link SendPacket no route hint: link={:02x?}",
7947                                    &link_id[..4]
7948                                );
7949                            }
7950                        }
7951                    }
7952
7953                    // Route through the transport engine's outbound path
7954                    match RawPacket::unpack(&raw) {
7955                        Ok(packet) => {
7956                            if packet.flags.packet_type == rns_core::constants::PACKET_TYPE_DATA {
7957                                self.sent_packets.insert(
7958                                    packet.packet_hash,
7959                                    (packet.destination_hash, time::now()),
7960                                );
7961                            }
7962                            let transport_actions = self.engine.handle_outbound(
7963                                &packet,
7964                                dest_type,
7965                                attached_interface,
7966                                time::now(),
7967                            );
7968                            self.dispatch_all(transport_actions);
7969                        }
7970                        Err(e) => {
7971                            log::warn!("LinkManager SendPacket: failed to unpack: {:?}", e);
7972                        }
7973                    }
7974                }
7975                LinkManagerAction::LinkEstablished {
7976                    link_id,
7977                    dest_hash,
7978                    rtt,
7979                    is_initiator,
7980                } => {
7981                    #[cfg(feature = "rns-hooks")]
7982                    {
7983                        let ctx = HookContext::Link {
7984                            link_id,
7985                            interface_id: 0,
7986                        };
7987                        let now = time::now();
7988                        let engine_ref = EngineRef {
7989                            engine: &self.engine,
7990                            interfaces: &self.interfaces,
7991                            link_manager: &self.link_manager,
7992                            now,
7993                        };
7994                        let provider_events_enabled = self.provider_events_enabled();
7995                        if let Some(ref e) = run_hook_inner(
7996                            &mut self.hook_slots[HookPoint::LinkEstablished as usize].programs,
7997                            &self.hook_manager,
7998                            &engine_ref,
7999                            &ctx,
8000                            now,
8001                            provider_events_enabled,
8002                        ) {
8003                            self.collect_hook_side_effects(
8004                                "LinkEstablished",
8005                                e,
8006                                &mut hook_injected,
8007                            );
8008                        }
8009                    }
8010                    log::info!(
8011                        "Link established: {:02x?} rtt={:.3}s initiator={}",
8012                        &link_id[..4],
8013                        rtt,
8014                        is_initiator,
8015                    );
8016                    self.callbacks.on_link_established(
8017                        rns_core::types::LinkId(link_id),
8018                        rns_core::types::DestHash(dest_hash),
8019                        rtt,
8020                        is_initiator,
8021                    );
8022                }
8023                LinkManagerAction::LinkClosed { link_id, reason } => {
8024                    #[cfg(feature = "rns-hooks")]
8025                    {
8026                        let ctx = HookContext::Link {
8027                            link_id,
8028                            interface_id: 0,
8029                        };
8030                        let now = time::now();
8031                        let engine_ref = EngineRef {
8032                            engine: &self.engine,
8033                            interfaces: &self.interfaces,
8034                            link_manager: &self.link_manager,
8035                            now,
8036                        };
8037                        let provider_events_enabled = self.provider_events_enabled();
8038                        if let Some(ref e) = run_hook_inner(
8039                            &mut self.hook_slots[HookPoint::LinkClosed as usize].programs,
8040                            &self.hook_manager,
8041                            &engine_ref,
8042                            &ctx,
8043                            now,
8044                            provider_events_enabled,
8045                        ) {
8046                            self.collect_hook_side_effects("LinkClosed", e, &mut hook_injected);
8047                        }
8048                    }
8049                    log::info!("Link closed: {:02x?} reason={:?}", &link_id[..4], reason);
8050                    self.holepunch_manager.link_closed(&link_id);
8051                    self.callbacks
8052                        .on_link_closed(rns_core::types::LinkId(link_id), reason);
8053                }
8054                LinkManagerAction::RemoteIdentified {
8055                    link_id,
8056                    identity_hash,
8057                    public_key,
8058                } => {
8059                    log::debug!(
8060                        "Remote identified on link {:02x?}: {:02x?}",
8061                        &link_id[..4],
8062                        &identity_hash[..4],
8063                    );
8064                    self.callbacks.on_remote_identified(
8065                        rns_core::types::LinkId(link_id),
8066                        rns_core::types::IdentityHash(identity_hash),
8067                        public_key,
8068                    );
8069                }
8070                LinkManagerAction::RegisterLinkDest { link_id } => {
8071                    // Register the link_id as a LINK destination in the transport engine
8072                    self.engine
8073                        .register_destination(link_id, rns_core::constants::DESTINATION_LINK);
8074                }
8075                LinkManagerAction::DeregisterLinkDest { link_id } => {
8076                    self.engine.deregister_destination(&link_id);
8077                }
8078                LinkManagerAction::ManagementRequest {
8079                    link_id,
8080                    path_hash,
8081                    data,
8082                    request_id,
8083                    remote_identity,
8084                } => {
8085                    self.handle_management_request(
8086                        link_id,
8087                        path_hash,
8088                        data,
8089                        request_id,
8090                        remote_identity,
8091                    );
8092                }
8093                LinkManagerAction::ResourceReceived {
8094                    link_id,
8095                    data,
8096                    metadata,
8097                } => {
8098                    self.callbacks.on_resource_received(
8099                        rns_core::types::LinkId(link_id),
8100                        data,
8101                        metadata,
8102                    );
8103                }
8104                LinkManagerAction::ResourceCompleted { link_id } => {
8105                    self.callbacks
8106                        .on_resource_completed(rns_core::types::LinkId(link_id));
8107                }
8108                LinkManagerAction::ResourceFailed { link_id, error } => {
8109                    log::debug!("Resource failed on link {:02x?}: {}", &link_id[..4], error);
8110                    self.callbacks
8111                        .on_resource_failed(rns_core::types::LinkId(link_id), error);
8112                }
8113                LinkManagerAction::ResourceProgress {
8114                    link_id,
8115                    received,
8116                    total,
8117                } => {
8118                    self.callbacks.on_resource_progress(
8119                        rns_core::types::LinkId(link_id),
8120                        received,
8121                        total,
8122                    );
8123                }
8124                LinkManagerAction::ResourceAcceptQuery {
8125                    link_id,
8126                    resource_hash,
8127                    transfer_size,
8128                    has_metadata,
8129                } => {
8130                    let accept = self.callbacks.on_resource_accept_query(
8131                        rns_core::types::LinkId(link_id),
8132                        resource_hash.clone(),
8133                        transfer_size,
8134                        has_metadata,
8135                    );
8136                    let accept_actions = self.link_manager.accept_resource(
8137                        &link_id,
8138                        &resource_hash,
8139                        accept,
8140                        &mut self.rng,
8141                    );
8142                    // Re-dispatch (recursive but bounded: accept_resource won't produce more AcceptQuery)
8143                    self.dispatch_link_actions(accept_actions);
8144                }
8145                LinkManagerAction::ChannelMessageReceived {
8146                    link_id,
8147                    msgtype,
8148                    payload,
8149                } => {
8150                    // Intercept hole-punch signaling messages (0xFE00..=0xFE04)
8151                    if HolePunchManager::is_holepunch_message(msgtype) {
8152                        let derived_key = self.link_manager.get_derived_key(&link_id);
8153                        let tx = self.get_event_sender();
8154                        let (handled, hp_actions) = self.holepunch_manager.handle_signal(
8155                            link_id,
8156                            msgtype,
8157                            payload,
8158                            derived_key.as_deref(),
8159                            &tx,
8160                        );
8161                        if handled {
8162                            self.dispatch_holepunch_actions(hp_actions);
8163                        }
8164                    } else {
8165                        self.callbacks.on_channel_message(
8166                            rns_core::types::LinkId(link_id),
8167                            msgtype,
8168                            payload,
8169                        );
8170                    }
8171                }
8172                LinkManagerAction::LinkDataReceived {
8173                    link_id,
8174                    context,
8175                    data,
8176                } => {
8177                    self.callbacks
8178                        .on_link_data(rns_core::types::LinkId(link_id), context, data);
8179                }
8180                LinkManagerAction::ResponseReceived {
8181                    link_id,
8182                    request_id,
8183                    data,
8184                } => {
8185                    self.callbacks
8186                        .on_response(rns_core::types::LinkId(link_id), request_id, data);
8187                }
8188                LinkManagerAction::LinkRequestReceived {
8189                    link_id,
8190                    receiving_interface,
8191                } => {
8192                    #[cfg(feature = "rns-hooks")]
8193                    {
8194                        let ctx = HookContext::Link {
8195                            link_id,
8196                            interface_id: receiving_interface.0,
8197                        };
8198                        let now = time::now();
8199                        let engine_ref = EngineRef {
8200                            engine: &self.engine,
8201                            interfaces: &self.interfaces,
8202                            link_manager: &self.link_manager,
8203                            now,
8204                        };
8205                        let provider_events_enabled = self.provider_events_enabled();
8206                        if let Some(ref e) = run_hook_inner(
8207                            &mut self.hook_slots[HookPoint::LinkRequestReceived as usize].programs,
8208                            &self.hook_manager,
8209                            &engine_ref,
8210                            &ctx,
8211                            now,
8212                            provider_events_enabled,
8213                        ) {
8214                            self.collect_hook_side_effects(
8215                                "LinkRequestReceived",
8216                                e,
8217                                &mut hook_injected,
8218                            );
8219                        }
8220                    }
8221                    #[cfg(not(feature = "rns-hooks"))]
8222                    {
8223                        let _ = (link_id, receiving_interface);
8224                    }
8225                }
8226            }
8227        }
8228
8229        // Dispatch any actions injected by hooks during action processing
8230        #[cfg(feature = "rns-hooks")]
8231        if !hook_injected.is_empty() {
8232            self.dispatch_all(hook_injected);
8233        }
8234    }
8235
8236    /// Dispatch hole-punch manager actions.
8237    fn dispatch_holepunch_actions(&mut self, actions: Vec<HolePunchManagerAction>) {
8238        for action in actions {
8239            match action {
8240                HolePunchManagerAction::SendChannelMessage {
8241                    link_id,
8242                    msgtype,
8243                    payload,
8244                } => {
8245                    if let Ok(link_actions) = self.link_manager.send_channel_message(
8246                        &link_id,
8247                        msgtype,
8248                        &payload,
8249                        &mut self.rng,
8250                    ) {
8251                        self.dispatch_link_actions(link_actions);
8252                    }
8253                }
8254                HolePunchManagerAction::DirectConnectEstablished {
8255                    link_id,
8256                    session_id,
8257                    interface_id,
8258                    rtt,
8259                    mtu,
8260                } => {
8261                    log::info!(
8262                        "Direct connection established for link {:02x?} session {:02x?} iface {} rtt={:.1}ms mtu={}",
8263                        &link_id[..4], &session_id[..4], interface_id.0, rtt * 1000.0, mtu
8264                    );
8265                    // Redirect the link's path to use the direct interface
8266                    self.engine
8267                        .redirect_path(&link_id, interface_id, time::now());
8268                    // Update the link's RTT and MTU to reflect the direct path
8269                    self.link_manager.set_link_rtt(&link_id, rtt);
8270                    self.link_manager.set_link_mtu(&link_id, mtu);
8271                    // Reset inbound timer — set_rtt shortens the keepalive/stale
8272                    // intervals, so without this the link goes stale immediately
8273                    self.link_manager.record_link_inbound(&link_id);
8274                    // Flush holepunch signaling messages from the channel window
8275                    self.link_manager.flush_channel_tx(&link_id);
8276                    self.callbacks.on_direct_connect_established(
8277                        rns_core::types::LinkId(link_id),
8278                        interface_id,
8279                    );
8280                }
8281                HolePunchManagerAction::DirectConnectFailed {
8282                    link_id,
8283                    session_id,
8284                    reason,
8285                } => {
8286                    log::debug!(
8287                        "Direct connection failed for link {:02x?} session {:02x?} reason={}",
8288                        &link_id[..4],
8289                        &session_id[..4],
8290                        reason
8291                    );
8292                    self.callbacks
8293                        .on_direct_connect_failed(rns_core::types::LinkId(link_id), reason);
8294                }
8295            }
8296        }
8297    }
8298
8299    /// Get an event sender for worker threads to send results back to the driver.
8300    ///
8301    /// This is a bit of a workaround since the driver owns the receiver.
8302    /// We store a clone of the sender when the driver is created.
8303    fn get_event_sender(&self) -> crate::event::EventSender {
8304        // The driver doesn't directly have a sender, but node.rs creates the channel
8305        // and passes rx to the driver. We need to store a sender clone.
8306        // For now we use an internal sender that was set during construction.
8307        self.event_tx.clone()
8308    }
8309
8310    /// Delay before first management announce after startup.
8311    const MANAGEMENT_ANNOUNCE_DELAY: f64 = 5.0;
8312
8313    /// Tick the discovery announcer: start stamp generation if due, send announce if ready.
8314    fn tick_discovery_announcer(&mut self, now: f64) {
8315        let announcer = match self.interface_announcer.as_mut() {
8316            Some(a) => a,
8317            None => return,
8318        };
8319
8320        announcer.maybe_start(now);
8321
8322        let stamp_result = match announcer.poll_ready() {
8323            Some(r) => r,
8324            None => return,
8325        };
8326
8327        if !announcer.contains_interface(&stamp_result.interface_name) {
8328            log::debug!(
8329                "Discovery: dropping completed stamp for removed interface '{}'",
8330                stamp_result.interface_name
8331            );
8332            return;
8333        }
8334
8335        let identity = match self.transport_identity.as_ref() {
8336            Some(id) => id,
8337            None => {
8338                log::warn!("Discovery: stamp ready but no transport identity");
8339                return;
8340            }
8341        };
8342
8343        // Discovery is a SINGLE destination — the dest hash includes the transport identity
8344        let identity_hash = identity.hash();
8345        let disc_dest = rns_core::destination::destination_hash(
8346            crate::discovery::APP_NAME,
8347            &["discovery", "interface"],
8348            Some(&identity_hash),
8349        );
8350        let name_hash = self.discovery_name_hash;
8351        let mut random_hash = [0u8; 10];
8352        self.rng.fill_bytes(&mut random_hash);
8353
8354        let (announce_data, _) = match rns_core::announce::AnnounceData::pack(
8355            identity,
8356            &disc_dest,
8357            &name_hash,
8358            &random_hash,
8359            None,
8360            Some(&stamp_result.app_data),
8361        ) {
8362            Ok(v) => v,
8363            Err(e) => {
8364                log::warn!("Discovery: failed to pack announce: {}", e);
8365                return;
8366            }
8367        };
8368
8369        let flags = rns_core::packet::PacketFlags {
8370            header_type: rns_core::constants::HEADER_1,
8371            context_flag: rns_core::constants::FLAG_UNSET,
8372            transport_type: rns_core::constants::TRANSPORT_BROADCAST,
8373            destination_type: rns_core::constants::DESTINATION_SINGLE,
8374            packet_type: rns_core::constants::PACKET_TYPE_ANNOUNCE,
8375        };
8376
8377        let packet = match RawPacket::pack(
8378            flags,
8379            0,
8380            &disc_dest,
8381            None,
8382            rns_core::constants::CONTEXT_NONE,
8383            &announce_data,
8384        ) {
8385            Ok(p) => p,
8386            Err(e) => {
8387                log::warn!("Discovery: failed to pack packet: {}", e);
8388                return;
8389            }
8390        };
8391
8392        let outbound_actions = self.engine.handle_outbound(
8393            &packet,
8394            rns_core::constants::DESTINATION_SINGLE,
8395            None,
8396            now,
8397        );
8398        log::debug!(
8399            "Discovery announce sent for interface '{}' ({} actions, dest={:02x?})",
8400            stamp_result.interface_name,
8401            outbound_actions.len(),
8402            &disc_dest[..4],
8403        );
8404        self.dispatch_all(outbound_actions);
8405    }
8406
8407    /// Read RSS from /proc/self/statm (Linux only).
8408    fn rss_mb() -> Option<f64> {
8409        let statm = std::fs::read_to_string("/proc/self/statm").ok()?;
8410        let rss_pages: u64 = statm.split_whitespace().nth(1)?.parse().ok()?;
8411        Some(rss_pages as f64 * 4096.0 / (1024.0 * 1024.0))
8412    }
8413
8414    fn parse_proc_kib(contents: &str, key: &str) -> Option<u64> {
8415        contents.lines().find_map(|line| {
8416            let value = line.strip_prefix(key)?;
8417            value.split_whitespace().next()?.parse().ok()
8418        })
8419    }
8420
8421    fn proc_status_mb() -> Option<(f64, f64, f64, f64)> {
8422        let status = std::fs::read_to_string("/proc/self/status").ok()?;
8423        let vm_rss = Self::parse_proc_kib(&status, "VmRSS:")? as f64 / 1024.0;
8424        let vm_hwm = Self::parse_proc_kib(&status, "VmHWM:")? as f64 / 1024.0;
8425        let vm_data = Self::parse_proc_kib(&status, "VmData:")? as f64 / 1024.0;
8426        let vm_swap = Self::parse_proc_kib(&status, "VmSwap:").unwrap_or(0) as f64 / 1024.0;
8427        Some((vm_rss, vm_hwm, vm_data, vm_swap))
8428    }
8429
8430    fn smaps_rollup_mb() -> Option<(f64, f64, f64, f64, f64, f64, f64, f64)> {
8431        let smaps = std::fs::read_to_string("/proc/self/smaps_rollup").ok()?;
8432        let rss_kib = Self::parse_proc_kib(&smaps, "Rss:")?;
8433        let anon_kib = Self::parse_proc_kib(&smaps, "Anonymous:")?;
8434        let shared_clean_kib = Self::parse_proc_kib(&smaps, "Shared_Clean:").unwrap_or(0);
8435        let shared_dirty_kib = Self::parse_proc_kib(&smaps, "Shared_Dirty:").unwrap_or(0);
8436        let private_clean_kib = Self::parse_proc_kib(&smaps, "Private_Clean:").unwrap_or(0);
8437        let private_dirty_kib = Self::parse_proc_kib(&smaps, "Private_Dirty:").unwrap_or(0);
8438        let swap_kib = Self::parse_proc_kib(&smaps, "Swap:").unwrap_or(0);
8439        let file_est_kib = rss_kib.saturating_sub(anon_kib);
8440        Some((
8441            rss_kib as f64 / 1024.0,
8442            anon_kib as f64 / 1024.0,
8443            file_est_kib as f64 / 1024.0,
8444            shared_clean_kib as f64 / 1024.0,
8445            shared_dirty_kib as f64 / 1024.0,
8446            private_clean_kib as f64 / 1024.0,
8447            private_dirty_kib as f64 / 1024.0,
8448            swap_kib as f64 / 1024.0,
8449        ))
8450    }
8451
8452    /// Log sizes of all major collections for memory growth diagnostics.
8453    fn log_memory_stats(&self) {
8454        let rss = Self::rss_mb()
8455            .map(|v| format!("{:.1}", v))
8456            .unwrap_or_else(|| "N/A".into());
8457        let (vm_rss, vm_hwm, vm_data, vm_swap) = Self::proc_status_mb()
8458            .map(|(rss, hwm, data, swap)| {
8459                (
8460                    format!("{rss:.1}"),
8461                    format!("{hwm:.1}"),
8462                    format!("{data:.1}"),
8463                    format!("{swap:.1}"),
8464                )
8465            })
8466            .unwrap_or_else(|| ("N/A".into(), "N/A".into(), "N/A".into(), "N/A".into()));
8467        let (
8468            smaps_rss,
8469            smaps_anon,
8470            smaps_file_est,
8471            smaps_shared_clean,
8472            smaps_shared_dirty,
8473            smaps_private_clean,
8474            smaps_private_dirty,
8475            smaps_swap,
8476        ) = Self::smaps_rollup_mb()
8477            .map(
8478                |(
8479                    rss,
8480                    anon,
8481                    file_est,
8482                    shared_clean,
8483                    shared_dirty,
8484                    private_clean,
8485                    private_dirty,
8486                    swap,
8487                )| {
8488                    (
8489                        format!("{rss:.1}"),
8490                        format!("{anon:.1}"),
8491                        format!("{file_est:.1}"),
8492                        format!("{shared_clean:.1}"),
8493                        format!("{shared_dirty:.1}"),
8494                        format!("{private_clean:.1}"),
8495                        format!("{private_dirty:.1}"),
8496                        format!("{swap:.1}"),
8497                    )
8498                },
8499            )
8500            .unwrap_or_else(|| {
8501                (
8502                    "N/A".into(),
8503                    "N/A".into(),
8504                    "N/A".into(),
8505                    "N/A".into(),
8506                    "N/A".into(),
8507                    "N/A".into(),
8508                    "N/A".into(),
8509                    "N/A".into(),
8510                )
8511            });
8512        log::info!(
8513            "MEMSTATS rss_mb={} vmrss_mb={} vmhwm_mb={} vmdata_mb={} vmswap_mb={} smaps_rss_mb={} smaps_anon_mb={} smaps_file_est_mb={} smaps_shared_clean_mb={} smaps_shared_dirty_mb={} smaps_private_clean_mb={} smaps_private_dirty_mb={} smaps_swap_mb={} known_dest={} known_dest_cap_evict={} path={} path_cap_evict={} announce={} reverse={}              link={} held_ann={} hashlist={} sig_cache={} ann_verify_q={} rate_lim={} blackhole={} tunnel={} ann_q_ifaces={} ann_q_nonempty={} ann_q_entries={} ann_q_bytes={} ann_q_iface_drop={}              pr_tags={} disc_pr={} sent_pkt={} completed={} local_dest={}              shared_ann={} lm_links={} hp_sessions={} proof_strat={}",
8514            rss,
8515            vm_rss,
8516            vm_hwm,
8517            vm_data,
8518            vm_swap,
8519            smaps_rss,
8520            smaps_anon,
8521            smaps_file_est,
8522            smaps_shared_clean,
8523            smaps_shared_dirty,
8524            smaps_private_clean,
8525            smaps_private_dirty,
8526            smaps_swap,
8527            self.known_destinations.len(),
8528            self.known_destinations_cap_evict_count,
8529            self.engine.path_table_count(),
8530            self.engine.path_destination_cap_evict_count(),
8531            self.engine.announce_table_count(),
8532            self.engine.reverse_table_count(),
8533            self.engine.link_table_count(),
8534            self.engine.held_announces_count(),
8535            self.engine.packet_hashlist_len(),
8536            self.engine.announce_sig_cache_len(),
8537            self.announce_verify_queue
8538                .lock()
8539                .map(|queue| queue.len())
8540                .unwrap_or(0),
8541            self.engine.rate_limiter_count(),
8542            self.engine.blackholed_count(),
8543            self.engine.tunnel_count(),
8544            self.engine.announce_queue_count(),
8545            self.engine.nonempty_announce_queue_count(),
8546            self.engine.queued_announce_count(),
8547            self.engine.queued_announce_bytes(),
8548            self.engine.announce_queue_interface_cap_drop_count(),
8549            self.engine.discovery_pr_tags_count(),
8550            self.engine.discovery_path_requests_count(),
8551            self.sent_packets.len(),
8552            self.completed_proofs.len(),
8553            self.local_destinations.len(),
8554            self.shared_announces.len(),
8555            self.link_manager.link_count(),
8556            self.holepunch_manager.session_count(),
8557            self.proof_strategies.len(),
8558        );
8559    }
8560
8561    /// Emit management and/or blackhole announces if enabled and due.
8562    fn tick_management_announces(&mut self, now: f64) {
8563        if self.transport_identity.is_none() {
8564            return;
8565        }
8566
8567        let uptime = now - self.started;
8568
8569        // Wait for initial delay
8570        if !self.initial_announce_sent {
8571            if uptime < Self::MANAGEMENT_ANNOUNCE_DELAY {
8572                return;
8573            }
8574            self.initial_announce_sent = true;
8575            self.emit_management_announces(now);
8576            return;
8577        }
8578
8579        // Periodic re-announce
8580        if now - self.last_management_announce >= self.management_announce_interval_secs {
8581            self.emit_management_announces(now);
8582        }
8583    }
8584
8585    /// Emit management/blackhole announce packets through the engine outbound path.
8586    fn emit_management_announces(&mut self, now: f64) {
8587        use crate::management;
8588
8589        self.last_management_announce = now;
8590
8591        let identity = match self.transport_identity {
8592            Some(ref id) => id,
8593            None => return,
8594        };
8595
8596        // Build announce packets first (immutable borrow of identity), then dispatch
8597        let mgmt_raw = if self.management_config.enable_remote_management {
8598            management::build_management_announce(identity, &mut self.rng)
8599        } else {
8600            None
8601        };
8602
8603        let bh_raw = if self.management_config.publish_blackhole {
8604            management::build_blackhole_announce(identity, &mut self.rng)
8605        } else {
8606            None
8607        };
8608
8609        let probe_raw = if self.probe_responder_hash.is_some() {
8610            management::build_probe_announce(identity, &mut self.rng)
8611        } else {
8612            None
8613        };
8614
8615        if let Some(raw) = mgmt_raw {
8616            if let Ok(packet) = RawPacket::unpack(&raw) {
8617                let actions = self.engine.handle_outbound(
8618                    &packet,
8619                    rns_core::constants::DESTINATION_SINGLE,
8620                    None,
8621                    now,
8622                );
8623                self.dispatch_all(actions);
8624                log::debug!("Emitted management destination announce");
8625            }
8626        }
8627
8628        if let Some(raw) = bh_raw {
8629            if let Ok(packet) = RawPacket::unpack(&raw) {
8630                let actions = self.engine.handle_outbound(
8631                    &packet,
8632                    rns_core::constants::DESTINATION_SINGLE,
8633                    None,
8634                    now,
8635                );
8636                self.dispatch_all(actions);
8637                log::debug!("Emitted blackhole info announce");
8638            }
8639        }
8640
8641        if let Some(raw) = probe_raw {
8642            if let Ok(packet) = RawPacket::unpack(&raw) {
8643                let actions = self.engine.handle_outbound(
8644                    &packet,
8645                    rns_core::constants::DESTINATION_SINGLE,
8646                    None,
8647                    now,
8648                );
8649                self.dispatch_all(actions);
8650                log::debug!("Emitted probe responder announce");
8651            }
8652        }
8653    }
8654
8655    /// Handle a management request by querying engine state and sending a response.
8656    fn handle_management_request(
8657        &mut self,
8658        link_id: [u8; 16],
8659        path_hash: [u8; 16],
8660        data: Vec<u8>,
8661        request_id: [u8; 16],
8662        remote_identity: Option<([u8; 16], [u8; 64])>,
8663    ) {
8664        use crate::management;
8665
8666        // ACL check for /status and /path (ALLOW_LIST), /list is ALLOW_ALL
8667        let is_restricted = path_hash == management::status_path_hash()
8668            || path_hash == management::path_path_hash();
8669
8670        if is_restricted && !self.management_config.remote_management_allowed.is_empty() {
8671            match remote_identity {
8672                Some((identity_hash, _)) => {
8673                    if !self
8674                        .management_config
8675                        .remote_management_allowed
8676                        .contains(&identity_hash)
8677                    {
8678                        log::debug!("Management request denied: identity not in allowed list");
8679                        return;
8680                    }
8681                }
8682                None => {
8683                    log::debug!("Management request denied: peer not identified");
8684                    return;
8685                }
8686            }
8687        }
8688
8689        let response_data = if path_hash == management::status_path_hash() {
8690            {
8691                let views: Vec<&dyn management::InterfaceStatusView> = self
8692                    .interfaces
8693                    .values()
8694                    .map(|e| e as &dyn management::InterfaceStatusView)
8695                    .collect();
8696                management::handle_status_request(
8697                    &data,
8698                    &self.engine,
8699                    &views,
8700                    self.started,
8701                    self.probe_responder_hash,
8702                )
8703            }
8704        } else if path_hash == management::path_path_hash() {
8705            management::handle_path_request(&data, &self.engine)
8706        } else if path_hash == management::list_path_hash() {
8707            management::handle_blackhole_list_request(&self.engine)
8708        } else {
8709            log::warn!("Unknown management path_hash: {:02x?}", &path_hash[..4]);
8710            None
8711        };
8712
8713        if let Some(response) = response_data {
8714            let actions = self.link_manager.send_management_response(
8715                &link_id,
8716                &request_id,
8717                &response,
8718                &mut self.rng,
8719            );
8720            self.dispatch_link_actions(actions);
8721        }
8722    }
8723}
8724
8725#[cfg(test)]
8726mod tests {
8727    use super::*;
8728    use crate::event;
8729    use crate::interface::Writer;
8730    use rns_core::announce::AnnounceData;
8731    use rns_core::constants;
8732    use rns_core::packet::PacketFlags;
8733    use rns_core::transport::types::InterfaceInfo;
8734    use rns_crypto::identity::Identity;
8735    use std::io;
8736    use std::sync::mpsc;
8737    use std::sync::{Arc, Mutex};
8738    use std::thread;
8739    use std::time::{Duration, Instant};
8740
8741    struct MockWriter {
8742        sent: Arc<Mutex<Vec<Vec<u8>>>>,
8743    }
8744
8745    impl MockWriter {
8746        fn new() -> (Self, Arc<Mutex<Vec<Vec<u8>>>>) {
8747            let sent = Arc::new(Mutex::new(Vec::new()));
8748            (MockWriter { sent: sent.clone() }, sent)
8749        }
8750    }
8751
8752    impl Writer for MockWriter {
8753        fn send_frame(&mut self, data: &[u8]) -> io::Result<()> {
8754            self.sent.lock().unwrap().push(data.to_vec());
8755            Ok(())
8756        }
8757    }
8758
8759    struct BlockingWriter {
8760        entered_tx: std::sync::mpsc::Sender<()>,
8761        release_rx: std::sync::mpsc::Receiver<()>,
8762    }
8763
8764    impl Writer for BlockingWriter {
8765        fn send_frame(&mut self, _data: &[u8]) -> io::Result<()> {
8766            let _ = self.entered_tx.send(());
8767            let _ = self.release_rx.recv();
8768            Ok(())
8769        }
8770    }
8771
8772    struct WouldBlockWriter {
8773        attempts: Arc<Mutex<usize>>,
8774    }
8775
8776    impl WouldBlockWriter {
8777        fn new() -> (Self, Arc<Mutex<usize>>) {
8778            let attempts = Arc::new(Mutex::new(0));
8779            (
8780                WouldBlockWriter {
8781                    attempts: attempts.clone(),
8782                },
8783                attempts,
8784            )
8785        }
8786    }
8787
8788    impl Writer for WouldBlockWriter {
8789        fn send_frame(&mut self, _data: &[u8]) -> io::Result<()> {
8790            *self.attempts.lock().unwrap() += 1;
8791            Err(io::Error::new(
8792                io::ErrorKind::WouldBlock,
8793                "intentional stall",
8794            ))
8795        }
8796    }
8797
8798    fn wait_for_sent_len(sent: &Arc<Mutex<Vec<Vec<u8>>>>, expected: usize) {
8799        let deadline = Instant::now() + Duration::from_millis(200);
8800        while Instant::now() < deadline {
8801            if sent.lock().unwrap().len() == expected {
8802                return;
8803            }
8804            thread::sleep(Duration::from_millis(5));
8805        }
8806        assert_eq!(sent.lock().unwrap().len(), expected);
8807    }
8808
8809    use rns_core::types::{DestHash, IdentityHash, LinkId as TypedLinkId, PacketHash};
8810
8811    struct MockCallbacks {
8812        announces: Arc<Mutex<Vec<(DestHash, u8)>>>,
8813        paths: Arc<Mutex<Vec<(DestHash, u8)>>>,
8814        deliveries: Arc<Mutex<Vec<DestHash>>>,
8815        iface_ups: Arc<Mutex<Vec<InterfaceId>>>,
8816        iface_downs: Arc<Mutex<Vec<InterfaceId>>>,
8817        link_established: Arc<Mutex<Vec<(TypedLinkId, f64, bool)>>>,
8818        link_closed: Arc<Mutex<Vec<TypedLinkId>>>,
8819        remote_identified: Arc<Mutex<Vec<(TypedLinkId, IdentityHash)>>>,
8820        resources_received: Arc<Mutex<Vec<(TypedLinkId, Vec<u8>)>>>,
8821        resource_completed: Arc<Mutex<Vec<TypedLinkId>>>,
8822        resource_failed: Arc<Mutex<Vec<(TypedLinkId, String)>>>,
8823        channel_messages: Arc<Mutex<Vec<(TypedLinkId, u16, Vec<u8>)>>>,
8824        link_data: Arc<Mutex<Vec<(TypedLinkId, u8, Vec<u8>)>>>,
8825        responses: Arc<Mutex<Vec<(TypedLinkId, [u8; 16], Vec<u8>)>>>,
8826        proofs: Arc<Mutex<Vec<(DestHash, PacketHash, f64)>>>,
8827        proof_requested: Arc<Mutex<Vec<(DestHash, PacketHash)>>>,
8828    }
8829
8830    impl MockCallbacks {
8831        fn new() -> (
8832            Self,
8833            Arc<Mutex<Vec<(DestHash, u8)>>>,
8834            Arc<Mutex<Vec<(DestHash, u8)>>>,
8835            Arc<Mutex<Vec<DestHash>>>,
8836            Arc<Mutex<Vec<InterfaceId>>>,
8837            Arc<Mutex<Vec<InterfaceId>>>,
8838        ) {
8839            let announces = Arc::new(Mutex::new(Vec::new()));
8840            let paths = Arc::new(Mutex::new(Vec::new()));
8841            let deliveries = Arc::new(Mutex::new(Vec::new()));
8842            let iface_ups = Arc::new(Mutex::new(Vec::new()));
8843            let iface_downs = Arc::new(Mutex::new(Vec::new()));
8844            (
8845                MockCallbacks {
8846                    announces: announces.clone(),
8847                    paths: paths.clone(),
8848                    deliveries: deliveries.clone(),
8849                    iface_ups: iface_ups.clone(),
8850                    iface_downs: iface_downs.clone(),
8851                    link_established: Arc::new(Mutex::new(Vec::new())),
8852                    link_closed: Arc::new(Mutex::new(Vec::new())),
8853                    remote_identified: Arc::new(Mutex::new(Vec::new())),
8854                    resources_received: Arc::new(Mutex::new(Vec::new())),
8855                    resource_completed: Arc::new(Mutex::new(Vec::new())),
8856                    resource_failed: Arc::new(Mutex::new(Vec::new())),
8857                    channel_messages: Arc::new(Mutex::new(Vec::new())),
8858                    link_data: Arc::new(Mutex::new(Vec::new())),
8859                    responses: Arc::new(Mutex::new(Vec::new())),
8860                    proofs: Arc::new(Mutex::new(Vec::new())),
8861                    proof_requested: Arc::new(Mutex::new(Vec::new())),
8862                },
8863                announces,
8864                paths,
8865                deliveries,
8866                iface_ups,
8867                iface_downs,
8868            )
8869        }
8870
8871        fn with_link_tracking() -> (
8872            Self,
8873            Arc<Mutex<Vec<(TypedLinkId, f64, bool)>>>,
8874            Arc<Mutex<Vec<TypedLinkId>>>,
8875            Arc<Mutex<Vec<(TypedLinkId, IdentityHash)>>>,
8876        ) {
8877            let link_established = Arc::new(Mutex::new(Vec::new()));
8878            let link_closed = Arc::new(Mutex::new(Vec::new()));
8879            let remote_identified = Arc::new(Mutex::new(Vec::new()));
8880            (
8881                MockCallbacks {
8882                    announces: Arc::new(Mutex::new(Vec::new())),
8883                    paths: Arc::new(Mutex::new(Vec::new())),
8884                    deliveries: Arc::new(Mutex::new(Vec::new())),
8885                    iface_ups: Arc::new(Mutex::new(Vec::new())),
8886                    iface_downs: Arc::new(Mutex::new(Vec::new())),
8887                    link_established: link_established.clone(),
8888                    link_closed: link_closed.clone(),
8889                    remote_identified: remote_identified.clone(),
8890                    resources_received: Arc::new(Mutex::new(Vec::new())),
8891                    resource_completed: Arc::new(Mutex::new(Vec::new())),
8892                    resource_failed: Arc::new(Mutex::new(Vec::new())),
8893                    channel_messages: Arc::new(Mutex::new(Vec::new())),
8894                    link_data: Arc::new(Mutex::new(Vec::new())),
8895                    responses: Arc::new(Mutex::new(Vec::new())),
8896                    proofs: Arc::new(Mutex::new(Vec::new())),
8897                    proof_requested: Arc::new(Mutex::new(Vec::new())),
8898                },
8899                link_established,
8900                link_closed,
8901                remote_identified,
8902            )
8903        }
8904    }
8905
8906    fn new_test_driver() -> Driver {
8907        let transport_config = TransportConfig {
8908            transport_enabled: false,
8909            identity_hash: None,
8910            prefer_shorter_path: false,
8911            max_paths_per_destination: 1,
8912            packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
8913            max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
8914            max_path_destinations: usize::MAX,
8915            max_tunnel_destinations_total: usize::MAX,
8916            destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
8917            announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
8918            announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
8919            announce_sig_cache_enabled: true,
8920            announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
8921            announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
8922            announce_queue_max_entries: 256,
8923            announce_queue_max_interfaces: 1024,
8924        };
8925        let (callbacks, _, _, _, _, _) = MockCallbacks::new();
8926        let (tx, rx) = event::channel();
8927        let mut driver = Driver::new(transport_config, rx, tx, Box::new(callbacks));
8928        driver.set_tick_interval_handle(Arc::new(AtomicU64::new(1000)));
8929        driver
8930    }
8931
8932    fn make_announced_identity(
8933        dest_hash: [u8; 16],
8934        received_at: f64,
8935        receiving_interface: InterfaceId,
8936    ) -> crate::destination::AnnouncedIdentity {
8937        crate::destination::AnnouncedIdentity {
8938            dest_hash: rns_core::types::DestHash(dest_hash),
8939            identity_hash: rns_core::types::IdentityHash([dest_hash[0]; 16]),
8940            public_key: [dest_hash[0]; 64],
8941            app_data: None,
8942            hops: 1,
8943            received_at,
8944            receiving_interface,
8945        }
8946    }
8947
8948    fn make_known_destination_state(
8949        dest_hash: [u8; 16],
8950        received_at: f64,
8951        receiving_interface: InterfaceId,
8952    ) -> KnownDestinationState {
8953        KnownDestinationState {
8954            announced: make_announced_identity(dest_hash, received_at, receiving_interface),
8955            was_used: false,
8956            last_used_at: None,
8957            retained: false,
8958        }
8959    }
8960
8961    #[cfg(feature = "iface-backbone")]
8962    fn make_pool_candidate(name: &str, port: u16, id: u64) -> BackbonePeerPoolCandidateConfig {
8963        let mut client = BackboneClientConfig {
8964            name: name.to_string(),
8965            target_host: "127.0.0.1".to_string(),
8966            target_port: port,
8967            interface_id: InterfaceId(id),
8968            reconnect_wait: Duration::from_millis(10),
8969            max_reconnect_tries: Some(0),
8970            connect_timeout: Duration::from_millis(50),
8971            transport_identity: None,
8972            ..BackboneClientConfig::default()
8973        };
8974        client.runtime = Arc::new(Mutex::new(BackboneClientRuntime::from_config(&client)));
8975        BackbonePeerPoolCandidateConfig {
8976            client,
8977            mode: constants::MODE_FULL,
8978            ingress_control: rns_core::transport::types::IngressControlConfig::disabled(),
8979            ifac_runtime: IfacRuntimeConfig {
8980                netname: None,
8981                netkey: None,
8982                size: 16,
8983            },
8984            ifac_enabled: false,
8985            interface_type_name: "BackboneInterface".to_string(),
8986        }
8987    }
8988
8989    #[cfg(feature = "iface-backbone")]
8990    #[test]
8991    fn backbone_peer_pool_respects_max_connected_order() {
8992        let listener_a = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
8993        let listener_b = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
8994        let port_a = listener_a.local_addr().unwrap().port();
8995        let port_b = listener_b.local_addr().unwrap().port();
8996        let mut driver = new_test_driver();
8997
8998        driver.configure_backbone_peer_pool(
8999            BackbonePeerPoolSettings {
9000                max_connected: 1,
9001                failure_threshold: 3,
9002                failure_window: Duration::from_secs(60),
9003                cooldown: Duration::from_secs(60),
9004            },
9005            vec![
9006                make_pool_candidate("first", port_a, 7001),
9007                make_pool_candidate("second", port_b, 7002),
9008            ],
9009        );
9010
9011        let status = driver.backbone_peer_pool_status().unwrap();
9012        assert_eq!(status.max_connected, 1);
9013        assert_eq!(status.active_count, 1);
9014        assert_eq!(status.standby_count, 1);
9015        assert_eq!(status.members[0].name, "first");
9016        assert_eq!(status.members[0].interface_id, Some(7001));
9017        assert_eq!(status.members[1].name, "second");
9018        assert_eq!(status.members[1].state, "standby");
9019        drop(listener_a);
9020        drop(listener_b);
9021    }
9022
9023    #[cfg(feature = "iface-backbone")]
9024    #[test]
9025    fn backbone_peer_pool_cools_down_failed_peer_and_tries_next() {
9026        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9027        let reachable_port = listener.local_addr().unwrap().port();
9028        let mut driver = new_test_driver();
9029
9030        driver.configure_backbone_peer_pool(
9031            BackbonePeerPoolSettings {
9032                max_connected: 1,
9033                failure_threshold: 1,
9034                failure_window: Duration::from_secs(60),
9035                cooldown: Duration::from_secs(60),
9036            },
9037            vec![
9038                make_pool_candidate("failed", 1, 7011),
9039                make_pool_candidate("replacement", reachable_port, 7012),
9040            ],
9041        );
9042
9043        let status = driver.backbone_peer_pool_status().unwrap();
9044        assert_eq!(status.active_count, 1);
9045        assert_eq!(status.cooldown_count, 1);
9046        assert_eq!(status.members[0].name, "failed");
9047        assert_eq!(status.members[0].state, "cooldown");
9048        assert_eq!(status.members[0].failure_count, 1);
9049        assert_eq!(status.members[1].name, "replacement");
9050        assert_eq!(status.members[1].interface_id, Some(7012));
9051        drop(listener);
9052    }
9053
9054    #[cfg(feature = "iface-backbone")]
9055    #[test]
9056    fn backbone_peer_pool_rotates_after_runtime_disconnect() {
9057        let listener_a = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9058        let listener_b = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9059        let port_a = listener_a.local_addr().unwrap().port();
9060        let port_b = listener_b.local_addr().unwrap().port();
9061        let mut driver = new_test_driver();
9062
9063        driver.configure_backbone_peer_pool(
9064            BackbonePeerPoolSettings {
9065                max_connected: 1,
9066                failure_threshold: 1,
9067                failure_window: Duration::from_secs(60),
9068                cooldown: Duration::from_secs(60),
9069            },
9070            vec![
9071                make_pool_candidate("first", port_a, 7021),
9072                make_pool_candidate("second", port_b, 7022),
9073            ],
9074        );
9075        driver.handle_backbone_peer_pool_down(InterfaceId(7021));
9076
9077        let status = driver.backbone_peer_pool_status().unwrap();
9078        assert_eq!(status.active_count, 1);
9079        assert_eq!(status.cooldown_count, 1);
9080        assert_eq!(status.members[0].state, "cooldown");
9081        assert_eq!(status.members[1].interface_id, Some(7022));
9082        drop(listener_a);
9083        drop(listener_b);
9084    }
9085
9086    #[cfg(feature = "iface-backbone")]
9087    fn register_test_backbone(driver: &mut Driver, name: &str) {
9088        let startup = BackboneServerRuntime {
9089            max_connections: Some(8),
9090            idle_timeout: Some(Duration::from_secs(10)),
9091            write_stall_timeout: Some(Duration::from_secs(30)),
9092            abuse: BackboneAbuseConfig {
9093                max_penalty_duration: Some(Duration::from_secs(3600)),
9094            },
9095        };
9096        let peer_state = Arc::new(std::sync::Mutex::new(
9097            crate::interface::backbone::BackbonePeerMonitor::new(),
9098        ));
9099        driver.register_backbone_runtime(BackboneRuntimeConfigHandle {
9100            interface_name: name.to_string(),
9101            runtime: Arc::new(std::sync::Mutex::new(startup.clone())),
9102            startup,
9103        });
9104        driver.register_backbone_peer_state(BackbonePeerStateHandle {
9105            interface_id: InterfaceId(1),
9106            interface_name: name.to_string(),
9107            peer_state,
9108        });
9109    }
9110
9111    #[cfg(feature = "iface-backbone")]
9112    fn register_test_backbone_client(driver: &mut Driver, name: &str) {
9113        let startup = BackboneClientRuntime {
9114            reconnect_wait: Duration::from_secs(5),
9115            max_reconnect_tries: Some(3),
9116            connect_timeout: Duration::from_secs(5),
9117        };
9118        driver.register_backbone_client_runtime(BackboneClientRuntimeConfigHandle {
9119            interface_name: name.to_string(),
9120            runtime: Arc::new(std::sync::Mutex::new(startup.clone())),
9121            startup,
9122        });
9123    }
9124
9125    #[cfg(feature = "iface-backbone")]
9126    fn register_test_backbone_discovery(driver: &mut Driver, name: &str, discoverable: bool) {
9127        let startup = BackboneDiscoveryRuntime {
9128            discoverable,
9129            config: crate::discovery::DiscoveryConfig {
9130                discovery_name: name.to_string(),
9131                announce_interval: 3600,
9132                stamp_value: crate::discovery::DEFAULT_STAMP_VALUE,
9133                reachable_on: None,
9134                interface_type: "BackboneInterface".to_string(),
9135                listen_port: Some(4242),
9136                latitude: None,
9137                longitude: None,
9138                height: None,
9139            },
9140            transport_enabled: true,
9141            ifac_netname: None,
9142            ifac_netkey: None,
9143        };
9144        driver.register_backbone_discovery_runtime(BackboneDiscoveryRuntimeHandle {
9145            interface_name: name.to_string(),
9146            current: startup.clone(),
9147            startup,
9148        });
9149    }
9150
9151    #[cfg(feature = "iface-tcp")]
9152    fn register_test_tcp_server(driver: &mut Driver, name: &str) {
9153        let startup = TcpServerRuntime {
9154            max_connections: Some(4),
9155        };
9156        driver.register_tcp_server_runtime(TcpServerRuntimeConfigHandle {
9157            interface_name: name.to_string(),
9158            runtime: Arc::new(std::sync::Mutex::new(startup.clone())),
9159            startup,
9160        });
9161    }
9162
9163    #[cfg(feature = "iface-tcp")]
9164    fn register_test_tcp_server_discovery(driver: &mut Driver, name: &str, discoverable: bool) {
9165        let startup = TcpServerDiscoveryRuntime {
9166            discoverable,
9167            config: crate::discovery::DiscoveryConfig {
9168                discovery_name: name.to_string(),
9169                announce_interval: 3600,
9170                stamp_value: crate::discovery::DEFAULT_STAMP_VALUE,
9171                reachable_on: None,
9172                interface_type: "TCPServerInterface".to_string(),
9173                listen_port: Some(4242),
9174                latitude: None,
9175                longitude: None,
9176                height: None,
9177            },
9178            transport_enabled: true,
9179            ifac_netname: None,
9180            ifac_netkey: None,
9181        };
9182        driver.register_tcp_server_discovery_runtime(TcpServerDiscoveryRuntimeHandle {
9183            interface_name: name.to_string(),
9184            current: startup.clone(),
9185            startup,
9186        });
9187    }
9188
9189    #[cfg(feature = "iface-tcp")]
9190    fn register_test_tcp_client(driver: &mut Driver, name: &str) {
9191        let startup = crate::interface::tcp::TcpClientRuntime {
9192            target_host: "127.0.0.1".into(),
9193            target_port: 4242,
9194            reconnect_wait: Duration::from_secs(5),
9195            max_reconnect_tries: Some(3),
9196            connect_timeout: Duration::from_secs(5),
9197        };
9198        driver.register_tcp_client_runtime(crate::interface::tcp::TcpClientRuntimeConfigHandle {
9199            interface_name: name.to_string(),
9200            runtime: Arc::new(std::sync::Mutex::new(startup.clone())),
9201            startup,
9202        });
9203    }
9204
9205    #[cfg(feature = "iface-udp")]
9206    fn register_test_udp(driver: &mut Driver, name: &str) {
9207        let startup = UdpRuntime {
9208            forward_ip: Some("127.0.0.1".into()),
9209            forward_port: Some(4242),
9210        };
9211        driver.register_udp_runtime(UdpRuntimeConfigHandle {
9212            interface_name: name.to_string(),
9213            runtime: Arc::new(std::sync::Mutex::new(startup.clone())),
9214            startup,
9215        });
9216    }
9217
9218    fn register_test_generic_interface(driver: &mut Driver, id: u64, name: &str) {
9219        let mut info = make_interface_info(id);
9220        info.name = name.to_string();
9221        info.mode = rns_core::constants::MODE_FULL;
9222        info.announce_rate_target = Some(1.5);
9223        info.announce_rate_grace = 2;
9224        info.announce_rate_penalty = 0.25;
9225        info.announce_cap = 0.05;
9226        info.ingress_control.enabled = true;
9227        driver.register_interface_runtime_defaults(&info);
9228        driver.register_interface_ifac_runtime(
9229            &info.name,
9230            IfacRuntimeConfig {
9231                netname: None,
9232                netkey: None,
9233                size: 16,
9234            },
9235        );
9236        driver.engine.register_interface(info.clone());
9237        let (writer, _) = MockWriter::new();
9238        driver.interfaces.insert(
9239            InterfaceId(id),
9240            InterfaceEntry {
9241                id: InterfaceId(id),
9242                info,
9243                writer: Box::new(writer),
9244                async_writer_metrics: None,
9245                enabled: true,
9246                online: true,
9247                dynamic: false,
9248                ifac: None,
9249                stats: InterfaceStats {
9250                    started: time::now(),
9251                    ..Default::default()
9252                },
9253                interface_type: "TestInterface".to_string(),
9254                send_retry_at: None,
9255                send_retry_backoff: Duration::ZERO,
9256            },
9257        );
9258    }
9259
9260    #[cfg(feature = "iface-auto")]
9261    fn register_test_auto(driver: &mut Driver, name: &str) {
9262        let startup = AutoRuntime {
9263            announce_interval_secs: 1.6,
9264            peer_timeout_secs: 22.0,
9265            peer_job_interval_secs: 4.0,
9266        };
9267        driver.register_auto_runtime(AutoRuntimeConfigHandle {
9268            interface_name: name.to_string(),
9269            runtime: Arc::new(std::sync::Mutex::new(startup.clone())),
9270            startup,
9271        });
9272    }
9273
9274    #[cfg(feature = "iface-i2p")]
9275    fn register_test_i2p(driver: &mut Driver, name: &str) {
9276        let startup = I2pRuntime {
9277            reconnect_wait: Duration::from_secs(15),
9278        };
9279        driver.register_i2p_runtime(I2pRuntimeConfigHandle {
9280            interface_name: name.to_string(),
9281            runtime: Arc::new(std::sync::Mutex::new(startup.clone())),
9282            startup,
9283        });
9284    }
9285
9286    #[cfg(feature = "iface-pipe")]
9287    fn register_test_pipe(driver: &mut Driver, name: &str) {
9288        let startup = PipeRuntime {
9289            respawn_delay: Duration::from_secs(5),
9290        };
9291        driver.register_pipe_runtime(PipeRuntimeConfigHandle {
9292            interface_name: name.to_string(),
9293            runtime: Arc::new(std::sync::Mutex::new(startup.clone())),
9294            startup,
9295        });
9296    }
9297
9298    #[cfg(feature = "iface-rnode")]
9299    fn register_test_rnode(driver: &mut Driver, name: &str) {
9300        let startup = RNodeRuntime {
9301            sub: RNodeSubConfig {
9302                name: name.to_string(),
9303                frequency: 868_000_000,
9304                bandwidth: 125_000,
9305                txpower: 7,
9306                spreading_factor: 8,
9307                coding_rate: 5,
9308                flow_control: false,
9309                st_alock: None,
9310                lt_alock: None,
9311            },
9312            writer: None,
9313        };
9314        driver.register_rnode_runtime(RNodeRuntimeConfigHandle {
9315            interface_name: name.to_string(),
9316            runtime: Arc::new(std::sync::Mutex::new(startup.clone())),
9317            startup,
9318        });
9319    }
9320
9321    impl Callbacks for MockCallbacks {
9322        fn on_announce(&mut self, announced: crate::destination::AnnouncedIdentity) {
9323            self.announces
9324                .lock()
9325                .unwrap()
9326                .push((announced.dest_hash, announced.hops));
9327        }
9328
9329        fn on_path_updated(&mut self, dest_hash: DestHash, hops: u8) {
9330            self.paths.lock().unwrap().push((dest_hash, hops));
9331        }
9332
9333        fn on_local_delivery(
9334            &mut self,
9335            dest_hash: DestHash,
9336            _raw: Vec<u8>,
9337            _packet_hash: PacketHash,
9338        ) {
9339            self.deliveries.lock().unwrap().push(dest_hash);
9340        }
9341
9342        fn on_interface_up(&mut self, id: InterfaceId) {
9343            self.iface_ups.lock().unwrap().push(id);
9344        }
9345
9346        fn on_interface_down(&mut self, id: InterfaceId) {
9347            self.iface_downs.lock().unwrap().push(id);
9348        }
9349
9350        fn on_link_established(
9351            &mut self,
9352            link_id: TypedLinkId,
9353            _dest_hash: DestHash,
9354            rtt: f64,
9355            is_initiator: bool,
9356        ) {
9357            self.link_established
9358                .lock()
9359                .unwrap()
9360                .push((link_id, rtt, is_initiator));
9361        }
9362
9363        fn on_link_closed(
9364            &mut self,
9365            link_id: TypedLinkId,
9366            _reason: Option<rns_core::link::TeardownReason>,
9367        ) {
9368            self.link_closed.lock().unwrap().push(link_id);
9369        }
9370
9371        fn on_remote_identified(
9372            &mut self,
9373            link_id: TypedLinkId,
9374            identity_hash: IdentityHash,
9375            _public_key: [u8; 64],
9376        ) {
9377            self.remote_identified
9378                .lock()
9379                .unwrap()
9380                .push((link_id, identity_hash));
9381        }
9382
9383        fn on_resource_received(
9384            &mut self,
9385            link_id: TypedLinkId,
9386            data: Vec<u8>,
9387            _metadata: Option<Vec<u8>>,
9388        ) {
9389            self.resources_received
9390                .lock()
9391                .unwrap()
9392                .push((link_id, data));
9393        }
9394
9395        fn on_resource_completed(&mut self, link_id: TypedLinkId) {
9396            self.resource_completed.lock().unwrap().push(link_id);
9397        }
9398
9399        fn on_resource_failed(&mut self, link_id: TypedLinkId, error: String) {
9400            self.resource_failed.lock().unwrap().push((link_id, error));
9401        }
9402
9403        fn on_channel_message(&mut self, link_id: TypedLinkId, msgtype: u16, payload: Vec<u8>) {
9404            self.channel_messages
9405                .lock()
9406                .unwrap()
9407                .push((link_id, msgtype, payload));
9408        }
9409
9410        fn on_link_data(&mut self, link_id: TypedLinkId, context: u8, data: Vec<u8>) {
9411            self.link_data
9412                .lock()
9413                .unwrap()
9414                .push((link_id, context, data));
9415        }
9416
9417        fn on_response(&mut self, link_id: TypedLinkId, request_id: [u8; 16], data: Vec<u8>) {
9418            self.responses
9419                .lock()
9420                .unwrap()
9421                .push((link_id, request_id, data));
9422        }
9423
9424        fn on_proof(&mut self, dest_hash: DestHash, packet_hash: PacketHash, rtt: f64) {
9425            self.proofs
9426                .lock()
9427                .unwrap()
9428                .push((dest_hash, packet_hash, rtt));
9429        }
9430
9431        fn on_proof_requested(&mut self, dest_hash: DestHash, packet_hash: PacketHash) -> bool {
9432            self.proof_requested
9433                .lock()
9434                .unwrap()
9435                .push((dest_hash, packet_hash));
9436            true
9437        }
9438    }
9439
9440    fn make_interface_info(id: u64) -> InterfaceInfo {
9441        InterfaceInfo {
9442            id: InterfaceId(id),
9443            name: format!("test-{}", id),
9444            mode: constants::MODE_FULL,
9445            out_capable: true,
9446            in_capable: true,
9447            bitrate: None,
9448            announce_rate_target: None,
9449            announce_rate_grace: 0,
9450            announce_rate_penalty: 0.0,
9451            announce_cap: rns_core::constants::ANNOUNCE_CAP,
9452            is_local_client: false,
9453            wants_tunnel: false,
9454            tunnel_id: None,
9455            mtu: constants::MTU as u32,
9456            ia_freq: 0.0,
9457            started: 0.0,
9458            ingress_control: rns_core::transport::types::IngressControlConfig::disabled(),
9459        }
9460    }
9461
9462    fn make_entry(id: u64, writer: Box<dyn Writer>, online: bool) -> InterfaceEntry {
9463        InterfaceEntry {
9464            id: InterfaceId(id),
9465            info: make_interface_info(id),
9466            writer,
9467            async_writer_metrics: None,
9468            enabled: true,
9469            online,
9470            dynamic: false,
9471            ifac: None,
9472            stats: InterfaceStats::default(),
9473            interface_type: String::new(),
9474            send_retry_at: None,
9475            send_retry_backoff: Duration::ZERO,
9476        }
9477    }
9478
9479    /// Build a valid announce packet that the engine will accept.
9480    fn build_announce_packet(identity: &Identity) -> Vec<u8> {
9481        let dest_hash =
9482            rns_core::destination::destination_hash("test", &["app"], Some(identity.hash()));
9483        let name_hash = rns_core::destination::name_hash("test", &["app"]);
9484        let random_hash = [0x42u8; 10];
9485
9486        let (announce_data, _has_ratchet) =
9487            AnnounceData::pack(identity, &dest_hash, &name_hash, &random_hash, None, None).unwrap();
9488
9489        let flags = PacketFlags {
9490            header_type: constants::HEADER_1,
9491            context_flag: constants::FLAG_UNSET,
9492            transport_type: constants::TRANSPORT_BROADCAST,
9493            destination_type: constants::DESTINATION_SINGLE,
9494            packet_type: constants::PACKET_TYPE_ANNOUNCE,
9495        };
9496
9497        let packet = RawPacket::pack(
9498            flags,
9499            0,
9500            &dest_hash,
9501            None,
9502            constants::CONTEXT_NONE,
9503            &announce_data,
9504        )
9505        .unwrap();
9506        packet.raw
9507    }
9508
9509    #[test]
9510    fn process_inbound_frame() {
9511        let (tx, rx) = event::channel();
9512        let (cbs, announces, _, _, _, _) = MockCallbacks::new();
9513        let mut driver = Driver::new(
9514            TransportConfig {
9515                transport_enabled: false,
9516                identity_hash: None,
9517                prefer_shorter_path: false,
9518                max_paths_per_destination: 1,
9519                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9520                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9521                max_path_destinations: usize::MAX,
9522                max_tunnel_destinations_total: usize::MAX,
9523                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9524                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9525                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9526                announce_sig_cache_enabled: true,
9527                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9528                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9529                announce_queue_max_entries: 256,
9530                announce_queue_max_interfaces: 1024,
9531            },
9532            rx,
9533            tx.clone(),
9534            Box::new(cbs),
9535        );
9536        let info = make_interface_info(1);
9537        driver.engine.register_interface(info.clone());
9538        let (writer, _sent) = MockWriter::new();
9539        driver
9540            .interfaces
9541            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
9542
9543        let identity = Identity::new(&mut OsRng);
9544        let announce_raw = build_announce_packet(&identity);
9545
9546        // Send frame then shutdown
9547        tx.send(Event::Frame {
9548            interface_id: InterfaceId(1),
9549            data: announce_raw,
9550        })
9551        .unwrap();
9552        tx.send(Event::Shutdown).unwrap();
9553        driver.run();
9554
9555        assert_eq!(announces.lock().unwrap().len(), 1);
9556    }
9557
9558    #[test]
9559    fn dispatch_send() {
9560        let (tx, rx) = event::channel();
9561        let (cbs, _, _, _, _, _) = MockCallbacks::new();
9562        let mut driver = Driver::new(
9563            TransportConfig {
9564                transport_enabled: false,
9565                identity_hash: None,
9566                prefer_shorter_path: false,
9567                max_paths_per_destination: 1,
9568                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9569                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9570                max_path_destinations: usize::MAX,
9571                max_tunnel_destinations_total: usize::MAX,
9572                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9573                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9574                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9575                announce_sig_cache_enabled: true,
9576                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9577                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9578                announce_queue_max_entries: 256,
9579                announce_queue_max_interfaces: 1024,
9580            },
9581            rx,
9582            tx.clone(),
9583            Box::new(cbs),
9584        );
9585        let (writer, sent) = MockWriter::new();
9586        driver
9587            .interfaces
9588            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
9589
9590        driver.dispatch_all(vec![TransportAction::SendOnInterface {
9591            interface: InterfaceId(1),
9592            raw: vec![0x01, 0x02, 0x03],
9593        }]);
9594
9595        assert_eq!(sent.lock().unwrap().len(), 1);
9596        assert_eq!(sent.lock().unwrap()[0], vec![0x01, 0x02, 0x03]);
9597
9598        drop(tx);
9599    }
9600
9601    #[test]
9602    fn dispatch_broadcast() {
9603        let (tx, rx) = event::channel();
9604        let (cbs, _, _, _, _, _) = MockCallbacks::new();
9605        let mut driver = Driver::new(
9606            TransportConfig {
9607                transport_enabled: false,
9608                identity_hash: None,
9609                prefer_shorter_path: false,
9610                max_paths_per_destination: 1,
9611                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9612                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9613                max_path_destinations: usize::MAX,
9614                max_tunnel_destinations_total: usize::MAX,
9615                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9616                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9617                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9618                announce_sig_cache_enabled: true,
9619                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9620                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9621                announce_queue_max_entries: 256,
9622                announce_queue_max_interfaces: 1024,
9623            },
9624            rx,
9625            tx.clone(),
9626            Box::new(cbs),
9627        );
9628
9629        let (w1, sent1) = MockWriter::new();
9630        let (w2, sent2) = MockWriter::new();
9631        driver
9632            .interfaces
9633            .insert(InterfaceId(1), make_entry(1, Box::new(w1), true));
9634        driver
9635            .interfaces
9636            .insert(InterfaceId(2), make_entry(2, Box::new(w2), true));
9637
9638        driver.dispatch_all(vec![TransportAction::BroadcastOnAllInterfaces {
9639            raw: vec![0xAA],
9640            exclude: None,
9641        }]);
9642
9643        assert_eq!(sent1.lock().unwrap().len(), 1);
9644        assert_eq!(sent2.lock().unwrap().len(), 1);
9645
9646        drop(tx);
9647    }
9648
9649    #[test]
9650    fn dispatch_broadcast_exclude() {
9651        let (tx, rx) = event::channel();
9652        let (cbs, _, _, _, _, _) = MockCallbacks::new();
9653        let mut driver = Driver::new(
9654            TransportConfig {
9655                transport_enabled: false,
9656                identity_hash: None,
9657                prefer_shorter_path: false,
9658                max_paths_per_destination: 1,
9659                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9660                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9661                max_path_destinations: usize::MAX,
9662                max_tunnel_destinations_total: usize::MAX,
9663                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9664                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9665                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9666                announce_sig_cache_enabled: true,
9667                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9668                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9669                announce_queue_max_entries: 256,
9670                announce_queue_max_interfaces: 1024,
9671            },
9672            rx,
9673            tx.clone(),
9674            Box::new(cbs),
9675        );
9676
9677        let (w1, sent1) = MockWriter::new();
9678        let (w2, sent2) = MockWriter::new();
9679        driver
9680            .interfaces
9681            .insert(InterfaceId(1), make_entry(1, Box::new(w1), true));
9682        driver
9683            .interfaces
9684            .insert(InterfaceId(2), make_entry(2, Box::new(w2), true));
9685
9686        driver.dispatch_all(vec![TransportAction::BroadcastOnAllInterfaces {
9687            raw: vec![0xBB],
9688            exclude: Some(InterfaceId(1)),
9689        }]);
9690
9691        assert_eq!(sent1.lock().unwrap().len(), 0); // excluded
9692        assert_eq!(sent2.lock().unwrap().len(), 1);
9693
9694        drop(tx);
9695    }
9696
9697    #[test]
9698    fn tick_event() {
9699        let (tx, rx) = event::channel();
9700        let (cbs, _, _, _, _, _) = MockCallbacks::new();
9701        let mut driver = Driver::new(
9702            TransportConfig {
9703                transport_enabled: true,
9704                identity_hash: Some([0x42; 16]),
9705                prefer_shorter_path: false,
9706                max_paths_per_destination: 1,
9707                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9708                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9709                max_path_destinations: usize::MAX,
9710                max_tunnel_destinations_total: usize::MAX,
9711                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9712                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9713                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9714                announce_sig_cache_enabled: true,
9715                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9716                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9717                announce_queue_max_entries: 256,
9718                announce_queue_max_interfaces: 1024,
9719            },
9720            rx,
9721            tx.clone(),
9722            Box::new(cbs),
9723        );
9724        let info = make_interface_info(1);
9725        driver.engine.register_interface(info.clone());
9726        let (writer, _sent) = MockWriter::new();
9727        driver
9728            .interfaces
9729            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
9730
9731        // Send Tick then Shutdown
9732        tx.send(Event::Tick).unwrap();
9733        tx.send(Event::Shutdown).unwrap();
9734        driver.run();
9735        // No crash = tick was processed successfully
9736    }
9737
9738    #[test]
9739    fn shutdown_event() {
9740        let (tx, rx) = event::channel();
9741        let (cbs, _, _, _, _, _) = MockCallbacks::new();
9742        let mut driver = Driver::new(
9743            TransportConfig {
9744                transport_enabled: false,
9745                identity_hash: None,
9746                prefer_shorter_path: false,
9747                max_paths_per_destination: 1,
9748                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9749                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9750                max_path_destinations: usize::MAX,
9751                max_tunnel_destinations_total: usize::MAX,
9752                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9753                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9754                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9755                announce_sig_cache_enabled: true,
9756                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9757                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9758                announce_queue_max_entries: 256,
9759                announce_queue_max_interfaces: 1024,
9760            },
9761            rx,
9762            tx.clone(),
9763            Box::new(cbs),
9764        );
9765
9766        tx.send(Event::Shutdown).unwrap();
9767        driver.run(); // Should return immediately
9768    }
9769
9770    #[test]
9771    fn begin_drain_updates_driver_status() {
9772        let (tx, rx) = event::channel();
9773        let (cbs, _, _, _, _, _) = MockCallbacks::new();
9774        let mut driver = Driver::new(
9775            TransportConfig {
9776                transport_enabled: false,
9777                identity_hash: None,
9778                prefer_shorter_path: false,
9779                max_paths_per_destination: 1,
9780                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9781                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9782                max_path_destinations: usize::MAX,
9783                max_tunnel_destinations_total: usize::MAX,
9784                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9785                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9786                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9787                announce_sig_cache_enabled: true,
9788                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9789                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9790                announce_queue_max_entries: 256,
9791                announce_queue_max_interfaces: 1024,
9792            },
9793            rx,
9794            tx,
9795            Box::new(cbs),
9796        );
9797
9798        driver.begin_drain(Duration::from_secs(3));
9799
9800        let QueryResponse::DrainStatus(status) = driver.handle_query(QueryRequest::DrainStatus)
9801        else {
9802            panic!("expected drain status response");
9803        };
9804        assert_eq!(status.state, LifecycleState::Draining);
9805        assert!(status.drain_complete);
9806        assert!(status.drain_age_seconds.is_some());
9807        assert!(status.deadline_remaining_seconds.is_some());
9808        assert_eq!(
9809            status.detail.as_deref(),
9810            Some("node is draining existing work; no active links, resource transfers, hole-punch sessions, or queued writer/provider work remain")
9811        );
9812    }
9813
9814    #[test]
9815    fn begin_drain_with_pending_link_reports_incomplete_status() {
9816        let (tx, rx) = event::channel();
9817        let (cbs, _, _, _, _, _) = MockCallbacks::new();
9818        let mut driver = Driver::new(
9819            TransportConfig {
9820                transport_enabled: false,
9821                identity_hash: None,
9822                prefer_shorter_path: false,
9823                max_paths_per_destination: 1,
9824                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9825                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9826                max_path_destinations: usize::MAX,
9827                max_tunnel_destinations_total: usize::MAX,
9828                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9829                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9830                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9831                announce_sig_cache_enabled: true,
9832                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9833                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9834                announce_queue_max_entries: 256,
9835                announce_queue_max_interfaces: 1024,
9836            },
9837            rx,
9838            tx,
9839            Box::new(cbs),
9840        );
9841
9842        let _ = driver.link_manager.create_link(
9843            &[0xDD; 16],
9844            &[0x11; 32],
9845            1,
9846            rns_core::constants::MTU as u32,
9847            &mut OsRng,
9848        );
9849
9850        driver.begin_drain(Duration::from_secs(3));
9851
9852        let QueryResponse::DrainStatus(status) = driver.handle_query(QueryRequest::DrainStatus)
9853        else {
9854            panic!("expected drain status response");
9855        };
9856        assert_eq!(status.state, LifecycleState::Draining);
9857        assert!(!status.drain_complete);
9858        assert!(status
9859            .detail
9860            .unwrap_or_default()
9861            .contains("1 link(s) still active"));
9862    }
9863
9864    #[test]
9865    fn begin_drain_with_queued_writer_frames_reports_incomplete_status() {
9866        let (tx, rx) = event::channel();
9867        let (cbs, _, _, _, _, _) = MockCallbacks::new();
9868        let mut driver = Driver::new(
9869            TransportConfig {
9870                transport_enabled: false,
9871                identity_hash: None,
9872                prefer_shorter_path: false,
9873                max_paths_per_destination: 1,
9874                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9875                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9876                max_path_destinations: usize::MAX,
9877                max_tunnel_destinations_total: usize::MAX,
9878                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9879                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9880                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9881                announce_sig_cache_enabled: true,
9882                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9883                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9884                announce_queue_max_entries: 256,
9885                announce_queue_max_interfaces: 1024,
9886            },
9887            rx,
9888            tx,
9889            Box::new(cbs),
9890        );
9891
9892        let info = make_interface_info(77);
9893        let (entered_tx, entered_rx) = std::sync::mpsc::channel();
9894        let (release_tx, release_rx) = std::sync::mpsc::channel();
9895        let (writer, async_writer_metrics) = crate::interface::wrap_async_writer(
9896            Box::new(BlockingWriter {
9897                entered_tx,
9898                release_rx,
9899            }),
9900            InterfaceId(77),
9901            &info.name,
9902            driver.event_tx.clone(),
9903            1,
9904        );
9905
9906        driver.interfaces.insert(
9907            InterfaceId(77),
9908            InterfaceEntry {
9909                id: InterfaceId(77),
9910                info,
9911                writer,
9912                async_writer_metrics: Some(async_writer_metrics),
9913                enabled: true,
9914                online: true,
9915                dynamic: false,
9916                ifac: None,
9917                stats: InterfaceStats::default(),
9918                interface_type: "TestInterface".to_string(),
9919                send_retry_at: None,
9920                send_retry_backoff: Duration::ZERO,
9921            },
9922        );
9923
9924        driver.dispatch_all(vec![TransportAction::SendOnInterface {
9925            interface: InterfaceId(77),
9926            raw: vec![0x01],
9927        }]);
9928        entered_rx.recv_timeout(Duration::from_secs(1)).unwrap();
9929        driver.dispatch_all(vec![TransportAction::SendOnInterface {
9930            interface: InterfaceId(77),
9931            raw: vec![0x02],
9932        }]);
9933
9934        driver.begin_drain(Duration::from_secs(3));
9935
9936        let QueryResponse::DrainStatus(status) = driver.handle_query(QueryRequest::DrainStatus)
9937        else {
9938            panic!("expected drain status response");
9939        };
9940        assert_eq!(status.state, LifecycleState::Draining);
9941        assert!(!status.drain_complete);
9942        assert_eq!(status.interface_writer_queued_frames, 1);
9943        assert!(status
9944            .detail
9945            .unwrap_or_default()
9946            .contains("queued interface writer frame"));
9947
9948        let _ = release_tx.send(());
9949    }
9950
9951    #[test]
9952    fn enforce_drain_deadline_tears_down_remaining_links() {
9953        let (tx, rx) = event::channel();
9954        let (cbs, _, _, _, _, _) = MockCallbacks::new();
9955        let mut driver = Driver::new(
9956            TransportConfig {
9957                transport_enabled: false,
9958                identity_hash: None,
9959                prefer_shorter_path: false,
9960                max_paths_per_destination: 1,
9961                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9962                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9963                max_path_destinations: usize::MAX,
9964                max_tunnel_destinations_total: usize::MAX,
9965                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9966                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9967                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9968                announce_sig_cache_enabled: true,
9969                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9970                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9971                announce_queue_max_entries: 256,
9972                announce_queue_max_interfaces: 1024,
9973            },
9974            rx,
9975            tx,
9976            Box::new(cbs),
9977        );
9978
9979        let _ = driver.link_manager.create_link(
9980            &[0xDD; 16],
9981            &[0x11; 32],
9982            1,
9983            rns_core::constants::MTU as u32,
9984            &mut OsRng,
9985        );
9986        driver.begin_drain(Duration::ZERO);
9987
9988        driver.enforce_drain_deadline();
9989
9990        assert_eq!(driver.lifecycle_state, LifecycleState::Stopping);
9991        assert_eq!(driver.link_manager.link_count(), 0);
9992        let QueryResponse::DrainStatus(status) = driver.handle_query(QueryRequest::DrainStatus)
9993        else {
9994            panic!("expected drain status response");
9995        };
9996        assert!(status.drain_complete);
9997        assert_eq!(status.state, LifecycleState::Stopping);
9998    }
9999
10000    #[test]
10001    fn begin_drain_with_holepunch_session_reports_incomplete_status_and_deadline_aborts_it() {
10002        let (tx, rx) = event::channel();
10003        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10004        let mut driver = Driver::new(
10005            TransportConfig {
10006                transport_enabled: false,
10007                identity_hash: None,
10008                prefer_shorter_path: false,
10009                max_paths_per_destination: 1,
10010                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10011                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10012                max_path_destinations: usize::MAX,
10013                max_tunnel_destinations_total: usize::MAX,
10014                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10015                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10016                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10017                announce_sig_cache_enabled: true,
10018                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10019                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10020                announce_queue_max_entries: 256,
10021                announce_queue_max_interfaces: 1024,
10022            },
10023            rx,
10024            tx,
10025            Box::new(cbs),
10026        );
10027        driver.holepunch_manager = crate::holepunch::orchestrator::HolePunchManager::new(
10028            vec!["127.0.0.1:4343".parse().unwrap()],
10029            rns_core::holepunch::ProbeProtocol::Rnsp,
10030            None,
10031        );
10032
10033        let _ = driver.holepunch_manager.propose(
10034            [0x44; 16],
10035            &[0xAA; 32],
10036            &mut OsRng,
10037            &driver.get_event_sender(),
10038        );
10039        assert_eq!(driver.holepunch_manager.session_count(), 1);
10040
10041        driver.begin_drain(Duration::from_secs(3));
10042
10043        let QueryResponse::DrainStatus(status) = driver.handle_query(QueryRequest::DrainStatus)
10044        else {
10045            panic!("expected drain status response");
10046        };
10047        assert_eq!(status.state, LifecycleState::Draining);
10048        assert!(!status.drain_complete);
10049        assert!(status
10050            .detail
10051            .unwrap_or_default()
10052            .contains("1 hole-punch session(s) still active"));
10053
10054        driver.begin_drain(Duration::ZERO);
10055        driver.enforce_drain_deadline();
10056
10057        assert_eq!(driver.holepunch_manager.session_count(), 0);
10058        let QueryResponse::DrainStatus(status) = driver.handle_query(QueryRequest::DrainStatus)
10059        else {
10060            panic!("expected drain status response");
10061        };
10062        assert!(status.drain_complete);
10063        assert_eq!(status.state, LifecycleState::Stopping);
10064    }
10065
10066    #[test]
10067    fn begin_drain_event_is_processed_by_run_loop() {
10068        let (tx, rx) = event::channel();
10069        let tx_query = tx.clone();
10070        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10071        let mut driver = Driver::new(
10072            TransportConfig {
10073                transport_enabled: false,
10074                identity_hash: None,
10075                prefer_shorter_path: false,
10076                max_paths_per_destination: 1,
10077                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10078                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10079                max_path_destinations: usize::MAX,
10080                max_tunnel_destinations_total: usize::MAX,
10081                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10082                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10083                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10084                announce_sig_cache_enabled: true,
10085                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10086                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10087                announce_queue_max_entries: 256,
10088                announce_queue_max_interfaces: 1024,
10089            },
10090            rx,
10091            tx.clone(),
10092            Box::new(cbs),
10093        );
10094
10095        let handle = std::thread::spawn(move || driver.run());
10096        tx.send(Event::BeginDrain {
10097            timeout: Duration::from_secs(2),
10098        })
10099        .unwrap();
10100        let (resp_tx, resp_rx) = std::sync::mpsc::channel();
10101        tx_query
10102            .send(Event::Query(QueryRequest::DrainStatus, resp_tx))
10103            .unwrap();
10104        let status = match resp_rx.recv().unwrap() {
10105            QueryResponse::DrainStatus(status) => status,
10106            other => panic!("expected drain status response, got {:?}", other),
10107        };
10108        assert_eq!(status.state, LifecycleState::Draining);
10109        tx_query.send(Event::Shutdown).unwrap();
10110        handle.join().unwrap();
10111    }
10112
10113    #[test]
10114    fn send_channel_message_returns_error_while_draining() {
10115        let (tx, rx) = event::channel();
10116        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10117        let mut driver = Driver::new(
10118            TransportConfig {
10119                transport_enabled: false,
10120                identity_hash: None,
10121                prefer_shorter_path: false,
10122                max_paths_per_destination: 1,
10123                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10124                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10125                max_path_destinations: usize::MAX,
10126                max_tunnel_destinations_total: usize::MAX,
10127                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10128                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10129                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10130                announce_sig_cache_enabled: true,
10131                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10132                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10133                announce_queue_max_entries: 256,
10134                announce_queue_max_interfaces: 1024,
10135            },
10136            rx,
10137            tx.clone(),
10138            Box::new(cbs),
10139        );
10140
10141        tx.send(Event::BeginDrain {
10142            timeout: Duration::from_secs(2),
10143        })
10144        .unwrap();
10145        let (resp_tx, resp_rx) = mpsc::channel();
10146        tx.send(Event::SendChannelMessage {
10147            link_id: [0xAA; 16],
10148            msgtype: 7,
10149            payload: b"drain".to_vec(),
10150            response_tx: resp_tx,
10151        })
10152        .unwrap();
10153        tx.send(Event::Shutdown).unwrap();
10154        driver.run();
10155
10156        let response = resp_rx.recv().unwrap();
10157        assert_eq!(
10158            response,
10159            Err("cannot send channel message while node is draining".into())
10160        );
10161    }
10162
10163    #[test]
10164    fn send_outbound_is_ignored_while_draining() {
10165        let (tx, rx) = event::channel();
10166        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10167        let mut driver = Driver::new(
10168            TransportConfig {
10169                transport_enabled: false,
10170                identity_hash: None,
10171                prefer_shorter_path: false,
10172                max_paths_per_destination: 1,
10173                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10174                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10175                max_path_destinations: usize::MAX,
10176                max_tunnel_destinations_total: usize::MAX,
10177                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10178                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10179                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10180                announce_sig_cache_enabled: true,
10181                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10182                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10183                announce_queue_max_entries: 256,
10184                announce_queue_max_interfaces: 1024,
10185            },
10186            rx,
10187            tx.clone(),
10188            Box::new(cbs),
10189        );
10190        let identity = Identity::new(&mut OsRng);
10191        let info = make_interface_info(1);
10192        driver.engine.register_interface(info);
10193        let (writer, sent) = MockWriter::new();
10194        driver
10195            .interfaces
10196            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
10197
10198        tx.send(Event::BeginDrain {
10199            timeout: Duration::from_secs(2),
10200        })
10201        .unwrap();
10202        tx.send(Event::SendOutbound {
10203            raw: build_announce_packet(&identity),
10204            dest_type: constants::DESTINATION_SINGLE,
10205            attached_interface: None,
10206        })
10207        .unwrap();
10208        tx.send(Event::Shutdown).unwrap();
10209        driver.run();
10210
10211        assert!(sent.lock().unwrap().is_empty());
10212        assert!(driver.sent_packets.is_empty());
10213    }
10214
10215    #[test]
10216    fn request_path_is_ignored_while_draining() {
10217        let (tx, rx) = event::channel();
10218        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10219        let mut driver = Driver::new(
10220            TransportConfig {
10221                transport_enabled: false,
10222                identity_hash: None,
10223                prefer_shorter_path: false,
10224                max_paths_per_destination: 1,
10225                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10226                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10227                max_path_destinations: usize::MAX,
10228                max_tunnel_destinations_total: usize::MAX,
10229                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10230                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10231                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10232                announce_sig_cache_enabled: true,
10233                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10234                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10235                announce_queue_max_entries: 256,
10236                announce_queue_max_interfaces: 1024,
10237            },
10238            rx,
10239            tx.clone(),
10240            Box::new(cbs),
10241        );
10242        let info = make_interface_info(1);
10243        driver.engine.register_interface(info);
10244        let (writer, sent) = MockWriter::new();
10245        driver
10246            .interfaces
10247            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
10248
10249        tx.send(Event::BeginDrain {
10250            timeout: Duration::from_secs(2),
10251        })
10252        .unwrap();
10253        tx.send(Event::RequestPath {
10254            dest_hash: [0xAA; 16],
10255        })
10256        .unwrap();
10257        tx.send(Event::Shutdown).unwrap();
10258        driver.run();
10259
10260        assert!(sent.lock().unwrap().is_empty());
10261    }
10262
10263    #[test]
10264    fn create_link_returns_zero_link_id_while_draining() {
10265        let (tx, rx) = event::channel();
10266        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10267        let mut driver = Driver::new(
10268            TransportConfig {
10269                transport_enabled: false,
10270                identity_hash: None,
10271                prefer_shorter_path: false,
10272                max_paths_per_destination: 1,
10273                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10274                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10275                max_path_destinations: usize::MAX,
10276                max_tunnel_destinations_total: usize::MAX,
10277                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10278                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10279                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10280                announce_sig_cache_enabled: true,
10281                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10282                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10283                announce_queue_max_entries: 256,
10284                announce_queue_max_interfaces: 1024,
10285            },
10286            rx,
10287            tx.clone(),
10288            Box::new(cbs),
10289        );
10290
10291        tx.send(Event::BeginDrain {
10292            timeout: Duration::from_secs(2),
10293        })
10294        .unwrap();
10295        let (resp_tx, resp_rx) = mpsc::channel();
10296        tx.send(Event::CreateLink {
10297            dest_hash: [0xAB; 16],
10298            dest_sig_pub_bytes: [0xCD; 32],
10299            response_tx: resp_tx,
10300        })
10301        .unwrap();
10302        tx.send(Event::Shutdown).unwrap();
10303        driver.run();
10304
10305        assert_eq!(resp_rx.recv().unwrap(), [0u8; 16]);
10306    }
10307
10308    #[test]
10309    fn announce_callback() {
10310        let (tx, rx) = event::channel();
10311        let (cbs, announces, paths, _, _, _) = MockCallbacks::new();
10312        let mut driver = Driver::new(
10313            TransportConfig {
10314                transport_enabled: false,
10315                identity_hash: None,
10316                prefer_shorter_path: false,
10317                max_paths_per_destination: 1,
10318                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10319                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10320                max_path_destinations: usize::MAX,
10321                max_tunnel_destinations_total: usize::MAX,
10322                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10323                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10324                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10325                announce_sig_cache_enabled: true,
10326                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10327                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10328                announce_queue_max_entries: 256,
10329                announce_queue_max_interfaces: 1024,
10330            },
10331            rx,
10332            tx.clone(),
10333            Box::new(cbs),
10334        );
10335        let info = make_interface_info(1);
10336        driver.engine.register_interface(info.clone());
10337        let (writer, _sent) = MockWriter::new();
10338        driver
10339            .interfaces
10340            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
10341
10342        let identity = Identity::new(&mut OsRng);
10343        let announce_raw = build_announce_packet(&identity);
10344
10345        tx.send(Event::Frame {
10346            interface_id: InterfaceId(1),
10347            data: announce_raw,
10348        })
10349        .unwrap();
10350        tx.send(Event::Shutdown).unwrap();
10351        driver.run();
10352
10353        let ann = announces.lock().unwrap();
10354        assert_eq!(ann.len(), 1);
10355        // Hops should be 1 (incremented from 0 by handle_inbound)
10356        assert_eq!(ann[0].1, 1);
10357
10358        let p = paths.lock().unwrap();
10359        assert_eq!(p.len(), 1);
10360    }
10361
10362    #[test]
10363    fn dispatch_skips_offline_interface() {
10364        let (tx, rx) = event::channel();
10365        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10366        let mut driver = Driver::new(
10367            TransportConfig {
10368                transport_enabled: false,
10369                identity_hash: None,
10370                prefer_shorter_path: false,
10371                max_paths_per_destination: 1,
10372                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10373                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10374                max_path_destinations: usize::MAX,
10375                max_tunnel_destinations_total: usize::MAX,
10376                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10377                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10378                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10379                announce_sig_cache_enabled: true,
10380                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10381                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10382                announce_queue_max_entries: 256,
10383                announce_queue_max_interfaces: 1024,
10384            },
10385            rx,
10386            tx.clone(),
10387            Box::new(cbs),
10388        );
10389
10390        let (w1, sent1) = MockWriter::new();
10391        let (w2, sent2) = MockWriter::new();
10392        driver
10393            .interfaces
10394            .insert(InterfaceId(1), make_entry(1, Box::new(w1), false)); // offline
10395        driver
10396            .interfaces
10397            .insert(InterfaceId(2), make_entry(2, Box::new(w2), true));
10398
10399        // Direct send to offline interface: should be skipped
10400        driver.dispatch_all(vec![TransportAction::SendOnInterface {
10401            interface: InterfaceId(1),
10402            raw: vec![0x01],
10403        }]);
10404        assert_eq!(sent1.lock().unwrap().len(), 0);
10405
10406        // Broadcast: only online interface should receive
10407        driver.dispatch_all(vec![TransportAction::BroadcastOnAllInterfaces {
10408            raw: vec![0x02],
10409            exclude: None,
10410        }]);
10411        assert_eq!(sent1.lock().unwrap().len(), 0); // still offline
10412        assert_eq!(sent2.lock().unwrap().len(), 1);
10413
10414        drop(tx);
10415    }
10416
10417    #[test]
10418    fn interface_up_refreshes_writer() {
10419        let (tx, rx) = event::channel();
10420        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10421        let mut driver = Driver::new(
10422            TransportConfig {
10423                transport_enabled: false,
10424                identity_hash: None,
10425                prefer_shorter_path: false,
10426                max_paths_per_destination: 1,
10427                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10428                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10429                max_path_destinations: usize::MAX,
10430                max_tunnel_destinations_total: usize::MAX,
10431                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10432                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10433                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10434                announce_sig_cache_enabled: true,
10435                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10436                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10437                announce_queue_max_entries: 256,
10438                announce_queue_max_interfaces: 1024,
10439            },
10440            rx,
10441            tx.clone(),
10442            Box::new(cbs),
10443        );
10444
10445        let (w_old, sent_old) = MockWriter::new();
10446        driver
10447            .interfaces
10448            .insert(InterfaceId(1), make_entry(1, Box::new(w_old), false));
10449
10450        // Simulate reconnect: InterfaceUp with new writer
10451        let (w_new, sent_new) = MockWriter::new();
10452        tx.send(Event::InterfaceUp(
10453            InterfaceId(1),
10454            Some(Box::new(w_new)),
10455            None,
10456        ))
10457        .unwrap();
10458        tx.send(Event::Shutdown).unwrap();
10459        driver.run();
10460
10461        // Interface should be online now
10462        assert!(driver.interfaces[&InterfaceId(1)].online);
10463
10464        // Send via the (now-refreshed) interface
10465        driver.dispatch_all(vec![TransportAction::SendOnInterface {
10466            interface: InterfaceId(1),
10467            raw: vec![0xFF],
10468        }]);
10469
10470        // Old writer should not have received anything
10471        assert_eq!(sent_old.lock().unwrap().len(), 0);
10472        // New writer should have received the data
10473        wait_for_sent_len(&sent_new, 1);
10474        assert_eq!(sent_new.lock().unwrap()[0], vec![0xFF]);
10475
10476        drop(tx);
10477    }
10478
10479    #[test]
10480    fn dynamic_interface_register() {
10481        let (tx, rx) = event::channel();
10482        let (cbs, _, _, _, iface_ups, _) = MockCallbacks::new();
10483        let mut driver = Driver::new(
10484            TransportConfig {
10485                transport_enabled: false,
10486                identity_hash: None,
10487                prefer_shorter_path: false,
10488                max_paths_per_destination: 1,
10489                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10490                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10491                max_path_destinations: usize::MAX,
10492                max_tunnel_destinations_total: usize::MAX,
10493                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10494                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10495                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10496                announce_sig_cache_enabled: true,
10497                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10498                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10499                announce_queue_max_entries: 256,
10500                announce_queue_max_interfaces: 1024,
10501            },
10502            rx,
10503            tx.clone(),
10504            Box::new(cbs),
10505        );
10506
10507        let info = make_interface_info(100);
10508        let (writer, sent) = MockWriter::new();
10509
10510        // InterfaceUp with InterfaceInfo = new dynamic interface
10511        tx.send(Event::InterfaceUp(
10512            InterfaceId(100),
10513            Some(Box::new(writer)),
10514            Some(info),
10515        ))
10516        .unwrap();
10517        tx.send(Event::Shutdown).unwrap();
10518        driver.run();
10519
10520        // Should be registered and online
10521        assert!(driver.interfaces.contains_key(&InterfaceId(100)));
10522        assert!(driver.interfaces[&InterfaceId(100)].online);
10523        assert!(driver.interfaces[&InterfaceId(100)].dynamic);
10524
10525        // Callback should have fired
10526        assert_eq!(iface_ups.lock().unwrap().len(), 1);
10527        assert_eq!(iface_ups.lock().unwrap()[0], InterfaceId(100));
10528
10529        // Can send to it
10530        driver.dispatch_all(vec![TransportAction::SendOnInterface {
10531            interface: InterfaceId(100),
10532            raw: vec![0x42],
10533        }]);
10534        wait_for_sent_len(&sent, 1);
10535
10536        drop(tx);
10537    }
10538
10539    #[test]
10540    fn dynamic_interface_deregister() {
10541        let (tx, rx) = event::channel();
10542        let (cbs, _, _, _, _, iface_downs) = MockCallbacks::new();
10543        let mut driver = Driver::new(
10544            TransportConfig {
10545                transport_enabled: false,
10546                identity_hash: None,
10547                prefer_shorter_path: false,
10548                max_paths_per_destination: 1,
10549                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10550                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10551                max_path_destinations: usize::MAX,
10552                max_tunnel_destinations_total: usize::MAX,
10553                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10554                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10555                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10556                announce_sig_cache_enabled: true,
10557                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10558                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10559                announce_queue_max_entries: 256,
10560                announce_queue_max_interfaces: 1024,
10561            },
10562            rx,
10563            tx.clone(),
10564            Box::new(cbs),
10565        );
10566
10567        // Register a dynamic interface
10568        let info = make_interface_info(200);
10569        driver.engine.register_interface(info.clone());
10570        let (writer, _sent) = MockWriter::new();
10571        driver.interfaces.insert(
10572            InterfaceId(200),
10573            InterfaceEntry {
10574                id: InterfaceId(200),
10575                info,
10576                writer: Box::new(writer),
10577                async_writer_metrics: None,
10578                enabled: true,
10579                online: true,
10580                dynamic: true,
10581                ifac: None,
10582                stats: InterfaceStats::default(),
10583                interface_type: String::new(),
10584                send_retry_at: None,
10585                send_retry_backoff: Duration::ZERO,
10586            },
10587        );
10588
10589        // InterfaceDown for dynamic → should be removed entirely
10590        tx.send(Event::InterfaceDown(InterfaceId(200))).unwrap();
10591        tx.send(Event::Shutdown).unwrap();
10592        driver.run();
10593
10594        assert!(!driver.interfaces.contains_key(&InterfaceId(200)));
10595        assert_eq!(iface_downs.lock().unwrap().len(), 1);
10596        assert_eq!(iface_downs.lock().unwrap()[0], InterfaceId(200));
10597    }
10598
10599    #[test]
10600    fn send_wouldblock_is_backed_off_between_dispatches() {
10601        let (tx, rx) = event::channel();
10602        let (cbs, ..) = MockCallbacks::new();
10603        let mut driver = Driver::new(
10604            TransportConfig {
10605                transport_enabled: false,
10606                identity_hash: None,
10607                prefer_shorter_path: false,
10608                max_paths_per_destination: 1,
10609                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10610                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10611                max_path_destinations: usize::MAX,
10612                max_tunnel_destinations_total: usize::MAX,
10613                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10614                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10615                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10616                announce_sig_cache_enabled: true,
10617                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10618                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10619                announce_queue_max_entries: 256,
10620                announce_queue_max_interfaces: 1024,
10621            },
10622            rx,
10623            tx,
10624            Box::new(cbs),
10625        );
10626        let (writer, attempts) = WouldBlockWriter::new();
10627        driver
10628            .interfaces
10629            .insert(InterfaceId(7), make_entry(7, Box::new(writer), true));
10630
10631        let action = TransportAction::SendOnInterface {
10632            interface: InterfaceId(7),
10633            raw: vec![0x01, 0x00, 0x42],
10634        };
10635        driver.dispatch_all(vec![action.clone()]);
10636        assert_eq!(*attempts.lock().unwrap(), 1);
10637
10638        driver.dispatch_all(vec![action.clone()]);
10639        assert_eq!(
10640            *attempts.lock().unwrap(),
10641            1,
10642            "second dispatch should be deferred during backoff"
10643        );
10644
10645        let entry = driver.interfaces.get_mut(&InterfaceId(7)).unwrap();
10646        entry.send_retry_at = Some(Instant::now() - Duration::from_millis(1));
10647        driver.dispatch_all(vec![action]);
10648        assert_eq!(*attempts.lock().unwrap(), 2);
10649    }
10650
10651    #[test]
10652    fn interface_callbacks_fire() {
10653        let (tx, rx) = event::channel();
10654        let (cbs, _, _, _, iface_ups, iface_downs) = MockCallbacks::new();
10655        let mut driver = Driver::new(
10656            TransportConfig {
10657                transport_enabled: false,
10658                identity_hash: None,
10659                prefer_shorter_path: false,
10660                max_paths_per_destination: 1,
10661                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10662                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10663                max_path_destinations: usize::MAX,
10664                max_tunnel_destinations_total: usize::MAX,
10665                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10666                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10667                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10668                announce_sig_cache_enabled: true,
10669                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10670                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10671                announce_queue_max_entries: 256,
10672                announce_queue_max_interfaces: 1024,
10673            },
10674            rx,
10675            tx.clone(),
10676            Box::new(cbs),
10677        );
10678
10679        // Static interface
10680        let (writer, _) = MockWriter::new();
10681        driver
10682            .interfaces
10683            .insert(InterfaceId(1), make_entry(1, Box::new(writer), false));
10684
10685        tx.send(Event::InterfaceUp(InterfaceId(1), None, None))
10686            .unwrap();
10687        tx.send(Event::InterfaceDown(InterfaceId(1))).unwrap();
10688        tx.send(Event::Shutdown).unwrap();
10689        driver.run();
10690
10691        assert_eq!(iface_ups.lock().unwrap().len(), 1);
10692        assert_eq!(iface_downs.lock().unwrap().len(), 1);
10693        // Static interface should still exist but be offline
10694        assert!(driver.interfaces.contains_key(&InterfaceId(1)));
10695        assert!(!driver.interfaces[&InterfaceId(1)].online);
10696    }
10697
10698    // =========================================================================
10699    // New tests for Phase 6a
10700    // =========================================================================
10701
10702    #[test]
10703    fn frame_updates_rx_stats() {
10704        let (tx, rx) = event::channel();
10705        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10706        let mut driver = Driver::new(
10707            TransportConfig {
10708                transport_enabled: false,
10709                identity_hash: None,
10710                prefer_shorter_path: false,
10711                max_paths_per_destination: 1,
10712                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10713                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10714                max_path_destinations: usize::MAX,
10715                max_tunnel_destinations_total: usize::MAX,
10716                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10717                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10718                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10719                announce_sig_cache_enabled: true,
10720                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10721                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10722                announce_queue_max_entries: 256,
10723                announce_queue_max_interfaces: 1024,
10724            },
10725            rx,
10726            tx.clone(),
10727            Box::new(cbs),
10728        );
10729        let info = make_interface_info(1);
10730        driver.engine.register_interface(info.clone());
10731        let (writer, _sent) = MockWriter::new();
10732        driver
10733            .interfaces
10734            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
10735
10736        let identity = Identity::new(&mut OsRng);
10737        let announce_raw = build_announce_packet(&identity);
10738        let announce_len = announce_raw.len() as u64;
10739
10740        tx.send(Event::Frame {
10741            interface_id: InterfaceId(1),
10742            data: announce_raw,
10743        })
10744        .unwrap();
10745        tx.send(Event::Shutdown).unwrap();
10746        driver.run();
10747
10748        let stats = &driver.interfaces[&InterfaceId(1)].stats;
10749        assert_eq!(stats.rxb, announce_len);
10750        assert_eq!(stats.rx_packets, 1);
10751    }
10752
10753    #[test]
10754    fn send_updates_tx_stats() {
10755        let (tx, rx) = event::channel();
10756        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10757        let mut driver = Driver::new(
10758            TransportConfig {
10759                transport_enabled: false,
10760                identity_hash: None,
10761                prefer_shorter_path: false,
10762                max_paths_per_destination: 1,
10763                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10764                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10765                max_path_destinations: usize::MAX,
10766                max_tunnel_destinations_total: usize::MAX,
10767                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10768                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10769                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10770                announce_sig_cache_enabled: true,
10771                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10772                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10773                announce_queue_max_entries: 256,
10774                announce_queue_max_interfaces: 1024,
10775            },
10776            rx,
10777            tx.clone(),
10778            Box::new(cbs),
10779        );
10780        let (writer, _sent) = MockWriter::new();
10781        driver
10782            .interfaces
10783            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
10784
10785        driver.dispatch_all(vec![TransportAction::SendOnInterface {
10786            interface: InterfaceId(1),
10787            raw: vec![0x01, 0x02, 0x03],
10788        }]);
10789
10790        let stats = &driver.interfaces[&InterfaceId(1)].stats;
10791        assert_eq!(stats.txb, 3);
10792        assert_eq!(stats.tx_packets, 1);
10793
10794        drop(tx);
10795    }
10796
10797    #[test]
10798    fn broadcast_updates_tx_stats() {
10799        let (tx, rx) = event::channel();
10800        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10801        let mut driver = Driver::new(
10802            TransportConfig {
10803                transport_enabled: false,
10804                identity_hash: None,
10805                prefer_shorter_path: false,
10806                max_paths_per_destination: 1,
10807                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10808                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10809                max_path_destinations: usize::MAX,
10810                max_tunnel_destinations_total: usize::MAX,
10811                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10812                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10813                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10814                announce_sig_cache_enabled: true,
10815                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10816                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10817                announce_queue_max_entries: 256,
10818                announce_queue_max_interfaces: 1024,
10819            },
10820            rx,
10821            tx.clone(),
10822            Box::new(cbs),
10823        );
10824        let (w1, _s1) = MockWriter::new();
10825        let (w2, _s2) = MockWriter::new();
10826        driver
10827            .interfaces
10828            .insert(InterfaceId(1), make_entry(1, Box::new(w1), true));
10829        driver
10830            .interfaces
10831            .insert(InterfaceId(2), make_entry(2, Box::new(w2), true));
10832
10833        driver.dispatch_all(vec![TransportAction::BroadcastOnAllInterfaces {
10834            raw: vec![0xAA, 0xBB],
10835            exclude: None,
10836        }]);
10837
10838        // Both interfaces should have tx stats updated
10839        assert_eq!(driver.interfaces[&InterfaceId(1)].stats.txb, 2);
10840        assert_eq!(driver.interfaces[&InterfaceId(1)].stats.tx_packets, 1);
10841        assert_eq!(driver.interfaces[&InterfaceId(2)].stats.txb, 2);
10842        assert_eq!(driver.interfaces[&InterfaceId(2)].stats.tx_packets, 1);
10843
10844        drop(tx);
10845    }
10846
10847    #[test]
10848    fn query_interface_stats() {
10849        let (tx, rx) = event::channel();
10850        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10851        let mut driver = Driver::new(
10852            TransportConfig {
10853                transport_enabled: true,
10854                identity_hash: Some([0x42; 16]),
10855                prefer_shorter_path: false,
10856                max_paths_per_destination: 1,
10857                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10858                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10859                max_path_destinations: usize::MAX,
10860                max_tunnel_destinations_total: usize::MAX,
10861                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10862                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10863                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10864                announce_sig_cache_enabled: true,
10865                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10866                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10867                announce_queue_max_entries: 256,
10868                announce_queue_max_interfaces: 1024,
10869            },
10870            rx,
10871            tx.clone(),
10872            Box::new(cbs),
10873        );
10874        let (writer, _sent) = MockWriter::new();
10875        driver
10876            .interfaces
10877            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
10878
10879        let (resp_tx, resp_rx) = mpsc::channel();
10880        tx.send(Event::Query(QueryRequest::InterfaceStats, resp_tx))
10881            .unwrap();
10882        tx.send(Event::Shutdown).unwrap();
10883        driver.run();
10884
10885        let resp = resp_rx.recv().unwrap();
10886        match resp {
10887            QueryResponse::InterfaceStats(stats) => {
10888                assert_eq!(stats.interfaces.len(), 1);
10889                assert_eq!(stats.interfaces[0].name, "test-1");
10890                assert!(stats.interfaces[0].status);
10891                assert_eq!(stats.transport_id, Some([0x42; 16]));
10892                assert!(stats.transport_enabled);
10893            }
10894            _ => panic!("unexpected response"),
10895        }
10896    }
10897
10898    #[test]
10899    fn query_path_table() {
10900        let (tx, rx) = event::channel();
10901        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10902        let mut driver = Driver::new(
10903            TransportConfig {
10904                transport_enabled: false,
10905                identity_hash: None,
10906                prefer_shorter_path: false,
10907                max_paths_per_destination: 1,
10908                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10909                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10910                max_path_destinations: usize::MAX,
10911                max_tunnel_destinations_total: usize::MAX,
10912                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10913                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10914                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10915                announce_sig_cache_enabled: true,
10916                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10917                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10918                announce_queue_max_entries: 256,
10919                announce_queue_max_interfaces: 1024,
10920            },
10921            rx,
10922            tx.clone(),
10923            Box::new(cbs),
10924        );
10925        let info = make_interface_info(1);
10926        driver.engine.register_interface(info);
10927        let (writer, _sent) = MockWriter::new();
10928        driver
10929            .interfaces
10930            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
10931
10932        // Feed an announce to create a path entry
10933        let identity = Identity::new(&mut OsRng);
10934        let announce_raw = build_announce_packet(&identity);
10935        tx.send(Event::Frame {
10936            interface_id: InterfaceId(1),
10937            data: announce_raw,
10938        })
10939        .unwrap();
10940
10941        let (resp_tx, resp_rx) = mpsc::channel();
10942        tx.send(Event::Query(
10943            QueryRequest::PathTable { max_hops: None },
10944            resp_tx,
10945        ))
10946        .unwrap();
10947        tx.send(Event::Shutdown).unwrap();
10948        driver.run();
10949
10950        let resp = resp_rx.recv().unwrap();
10951        match resp {
10952            QueryResponse::PathTable(entries) => {
10953                assert_eq!(entries.len(), 1);
10954                assert_eq!(entries[0].hops, 1);
10955            }
10956            _ => panic!("unexpected response"),
10957        }
10958    }
10959
10960    #[test]
10961    fn query_drop_path() {
10962        let (tx, rx) = event::channel();
10963        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10964        let mut driver = Driver::new(
10965            TransportConfig {
10966                transport_enabled: false,
10967                identity_hash: None,
10968                prefer_shorter_path: false,
10969                max_paths_per_destination: 1,
10970                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10971                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10972                max_path_destinations: usize::MAX,
10973                max_tunnel_destinations_total: usize::MAX,
10974                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10975                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10976                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10977                announce_sig_cache_enabled: true,
10978                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10979                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10980                announce_queue_max_entries: 256,
10981                announce_queue_max_interfaces: 1024,
10982            },
10983            rx,
10984            tx.clone(),
10985            Box::new(cbs),
10986        );
10987        let info = make_interface_info(1);
10988        driver.engine.register_interface(info);
10989        let (writer, _sent) = MockWriter::new();
10990        driver
10991            .interfaces
10992            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
10993
10994        // Feed an announce to create a path entry
10995        let identity = Identity::new(&mut OsRng);
10996        let announce_raw = build_announce_packet(&identity);
10997        let dest_hash =
10998            rns_core::destination::destination_hash("test", &["app"], Some(identity.hash()));
10999
11000        tx.send(Event::Frame {
11001            interface_id: InterfaceId(1),
11002            data: announce_raw,
11003        })
11004        .unwrap();
11005
11006        let (resp_tx, resp_rx) = mpsc::channel();
11007        tx.send(Event::Query(QueryRequest::DropPath { dest_hash }, resp_tx))
11008            .unwrap();
11009        tx.send(Event::Shutdown).unwrap();
11010        driver.run();
11011
11012        let resp = resp_rx.recv().unwrap();
11013        match resp {
11014            QueryResponse::DropPath(dropped) => {
11015                assert!(dropped);
11016            }
11017            _ => panic!("unexpected response"),
11018        }
11019    }
11020
11021    #[test]
11022    fn send_outbound_event() {
11023        let (tx, rx) = event::channel();
11024        let (cbs, _, _, _, _, _) = MockCallbacks::new();
11025        let mut driver = Driver::new(
11026            TransportConfig {
11027                transport_enabled: false,
11028                identity_hash: None,
11029                prefer_shorter_path: false,
11030                max_paths_per_destination: 1,
11031                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
11032                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
11033                max_path_destinations: usize::MAX,
11034                max_tunnel_destinations_total: usize::MAX,
11035                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
11036                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
11037                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
11038                announce_sig_cache_enabled: true,
11039                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
11040                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
11041                announce_queue_max_entries: 256,
11042                announce_queue_max_interfaces: 1024,
11043            },
11044            rx,
11045            tx.clone(),
11046            Box::new(cbs),
11047        );
11048        let (writer, sent) = MockWriter::new();
11049        let info = make_interface_info(1);
11050        driver.engine.register_interface(info);
11051        driver
11052            .interfaces
11053            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
11054
11055        // Build a DATA packet to a destination
11056        let dest = [0xAA; 16];
11057        let flags = PacketFlags {
11058            header_type: constants::HEADER_1,
11059            context_flag: constants::FLAG_UNSET,
11060            transport_type: constants::TRANSPORT_BROADCAST,
11061            destination_type: constants::DESTINATION_PLAIN,
11062            packet_type: constants::PACKET_TYPE_DATA,
11063        };
11064        let packet =
11065            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"hello").unwrap();
11066
11067        tx.send(Event::SendOutbound {
11068            raw: packet.raw,
11069            dest_type: constants::DESTINATION_PLAIN,
11070            attached_interface: None,
11071        })
11072        .unwrap();
11073        tx.send(Event::Shutdown).unwrap();
11074        driver.run();
11075
11076        // PLAIN packet should be broadcast on all interfaces
11077        assert_eq!(sent.lock().unwrap().len(), 1);
11078    }
11079
11080    #[test]
11081    fn register_destination_and_deliver() {
11082        let (tx, rx) = event::channel();
11083        let (cbs, _, _, deliveries, _, _) = MockCallbacks::new();
11084        let mut driver = Driver::new(
11085            TransportConfig {
11086                transport_enabled: false,
11087                identity_hash: None,
11088                prefer_shorter_path: false,
11089                max_paths_per_destination: 1,
11090                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
11091                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
11092                max_path_destinations: usize::MAX,
11093                max_tunnel_destinations_total: usize::MAX,
11094                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
11095                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
11096                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
11097                announce_sig_cache_enabled: true,
11098                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
11099                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
11100                announce_queue_max_entries: 256,
11101                announce_queue_max_interfaces: 1024,
11102            },
11103            rx,
11104            tx.clone(),
11105            Box::new(cbs),
11106        );
11107        let info = make_interface_info(1);
11108        driver.engine.register_interface(info);
11109        let (writer, _sent) = MockWriter::new();
11110        driver
11111            .interfaces
11112            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
11113
11114        let dest = [0xBB; 16];
11115
11116        // Register destination then send a data packet to it
11117        tx.send(Event::RegisterDestination {
11118            dest_hash: dest,
11119            dest_type: constants::DESTINATION_SINGLE,
11120        })
11121        .unwrap();
11122
11123        let flags = PacketFlags {
11124            header_type: constants::HEADER_1,
11125            context_flag: constants::FLAG_UNSET,
11126            transport_type: constants::TRANSPORT_BROADCAST,
11127            destination_type: constants::DESTINATION_SINGLE,
11128            packet_type: constants::PACKET_TYPE_DATA,
11129        };
11130        let packet =
11131            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"data").unwrap();
11132        tx.send(Event::Frame {
11133            interface_id: InterfaceId(1),
11134            data: packet.raw,
11135        })
11136        .unwrap();
11137        tx.send(Event::Shutdown).unwrap();
11138        driver.run();
11139
11140        assert_eq!(deliveries.lock().unwrap().len(), 1);
11141        assert_eq!(deliveries.lock().unwrap()[0], DestHash(dest));
11142    }
11143
11144    #[test]
11145    fn query_transport_identity() {
11146        let (tx, rx) = event::channel();
11147        let (cbs, _, _, _, _, _) = MockCallbacks::new();
11148        let mut driver = Driver::new(
11149            TransportConfig {
11150                transport_enabled: true,
11151                identity_hash: Some([0xAA; 16]),
11152                prefer_shorter_path: false,
11153                max_paths_per_destination: 1,
11154                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
11155                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
11156                max_path_destinations: usize::MAX,
11157                max_tunnel_destinations_total: usize::MAX,
11158                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
11159                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
11160                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
11161                announce_sig_cache_enabled: true,
11162                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
11163                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
11164                announce_queue_max_entries: 256,
11165                announce_queue_max_interfaces: 1024,
11166            },
11167            rx,
11168            tx.clone(),
11169            Box::new(cbs),
11170        );
11171
11172        let (resp_tx, resp_rx) = mpsc::channel();
11173        tx.send(Event::Query(QueryRequest::TransportIdentity, resp_tx))
11174            .unwrap();
11175        tx.send(Event::Shutdown).unwrap();
11176        driver.run();
11177
11178        match resp_rx.recv().unwrap() {
11179            QueryResponse::TransportIdentity(Some(hash)) => {
11180                assert_eq!(hash, [0xAA; 16]);
11181            }
11182            _ => panic!("unexpected response"),
11183        }
11184    }
11185
11186    #[test]
11187    fn query_link_count() {
11188        let (tx, rx) = event::channel();
11189        let (cbs, _, _, _, _, _) = MockCallbacks::new();
11190        let mut driver = Driver::new(
11191            TransportConfig {
11192                transport_enabled: false,
11193                identity_hash: None,
11194                prefer_shorter_path: false,
11195                max_paths_per_destination: 1,
11196                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
11197                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
11198                max_path_destinations: usize::MAX,
11199                max_tunnel_destinations_total: usize::MAX,
11200                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
11201                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
11202                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
11203                announce_sig_cache_enabled: true,
11204                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
11205                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
11206                announce_queue_max_entries: 256,
11207                announce_queue_max_interfaces: 1024,
11208            },
11209            rx,
11210            tx.clone(),
11211            Box::new(cbs),
11212        );
11213
11214        let (resp_tx, resp_rx) = mpsc::channel();
11215        tx.send(Event::Query(QueryRequest::LinkCount, resp_tx))
11216            .unwrap();
11217        tx.send(Event::Shutdown).unwrap();
11218        driver.run();
11219
11220        match resp_rx.recv().unwrap() {
11221            QueryResponse::LinkCount(count) => assert_eq!(count, 0),
11222            _ => panic!("unexpected response"),
11223        }
11224    }
11225
11226    #[test]
11227    fn query_rate_table() {
11228        let (tx, rx) = event::channel();
11229        let (cbs, _, _, _, _, _) = MockCallbacks::new();
11230        let mut driver = Driver::new(
11231            TransportConfig {
11232                transport_enabled: false,
11233                identity_hash: None,
11234                prefer_shorter_path: false,
11235                max_paths_per_destination: 1,
11236                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
11237                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
11238                max_path_destinations: usize::MAX,
11239                max_tunnel_destinations_total: usize::MAX,
11240                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
11241                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
11242                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
11243                announce_sig_cache_enabled: true,
11244                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
11245                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
11246                announce_queue_max_entries: 256,
11247                announce_queue_max_interfaces: 1024,
11248            },
11249            rx,
11250            tx.clone(),
11251            Box::new(cbs),
11252        );
11253
11254        let (resp_tx, resp_rx) = mpsc::channel();
11255        tx.send(Event::Query(QueryRequest::RateTable, resp_tx))
11256            .unwrap();
11257        tx.send(Event::Shutdown).unwrap();
11258        driver.run();
11259
11260        match resp_rx.recv().unwrap() {
11261            QueryResponse::RateTable(entries) => assert!(entries.is_empty()),
11262            _ => panic!("unexpected response"),
11263        }
11264    }
11265
11266    #[test]
11267    fn query_next_hop() {
11268        let (tx, rx) = event::channel();
11269        let (cbs, _, _, _, _, _) = MockCallbacks::new();
11270        let mut driver = Driver::new(
11271            TransportConfig {
11272                transport_enabled: false,
11273                identity_hash: None,
11274                prefer_shorter_path: false,
11275                max_paths_per_destination: 1,
11276                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
11277                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
11278                max_path_destinations: usize::MAX,
11279                max_tunnel_destinations_total: usize::MAX,
11280                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
11281                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
11282                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
11283                announce_sig_cache_enabled: true,
11284                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
11285                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
11286                announce_queue_max_entries: 256,
11287                announce_queue_max_interfaces: 1024,
11288            },
11289            rx,
11290            tx.clone(),
11291            Box::new(cbs),
11292        );
11293
11294        let dest = [0xBB; 16];
11295        let (resp_tx, resp_rx) = mpsc::channel();
11296        tx.send(Event::Query(
11297            QueryRequest::NextHop { dest_hash: dest },
11298            resp_tx,
11299        ))
11300        .unwrap();
11301        tx.send(Event::Shutdown).unwrap();
11302        driver.run();
11303
11304        match resp_rx.recv().unwrap() {
11305            QueryResponse::NextHop(None) => {}
11306            _ => panic!("unexpected response"),
11307        }
11308    }
11309
11310    #[test]
11311    fn query_next_hop_if_name() {
11312        let (tx, rx) = event::channel();
11313        let (cbs, _, _, _, _, _) = MockCallbacks::new();
11314        let mut driver = Driver::new(
11315            TransportConfig {
11316                transport_enabled: false,
11317                identity_hash: None,
11318                prefer_shorter_path: false,
11319                max_paths_per_destination: 1,
11320                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
11321                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
11322                max_path_destinations: usize::MAX,
11323                max_tunnel_destinations_total: usize::MAX,
11324                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
11325                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
11326                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
11327                announce_sig_cache_enabled: true,
11328                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
11329                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
11330                announce_queue_max_entries: 256,
11331                announce_queue_max_interfaces: 1024,
11332            },
11333            rx,
11334            tx.clone(),
11335            Box::new(cbs),
11336        );
11337
11338        let dest = [0xCC; 16];
11339        let (resp_tx, resp_rx) = mpsc::channel();
11340        tx.send(Event::Query(
11341            QueryRequest::NextHopIfName { dest_hash: dest },
11342            resp_tx,
11343        ))
11344        .unwrap();
11345        tx.send(Event::Shutdown).unwrap();
11346        driver.run();
11347
11348        match resp_rx.recv().unwrap() {
11349            QueryResponse::NextHopIfName(None) => {}
11350            _ => panic!("unexpected response"),
11351        }
11352    }
11353
11354    #[test]
11355    fn query_drop_all_via() {
11356        let (tx, rx) = event::channel();
11357        let (cbs, _, _, _, _, _) = MockCallbacks::new();
11358        let mut driver = Driver::new(
11359            TransportConfig {
11360                transport_enabled: false,
11361                identity_hash: None,
11362                prefer_shorter_path: false,
11363                max_paths_per_destination: 1,
11364                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
11365                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
11366                max_path_destinations: usize::MAX,
11367                max_tunnel_destinations_total: usize::MAX,
11368                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
11369                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
11370                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
11371                announce_sig_cache_enabled: true,
11372                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
11373                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
11374                announce_queue_max_entries: 256,
11375                announce_queue_max_interfaces: 1024,
11376            },
11377            rx,
11378            tx.clone(),
11379            Box::new(cbs),
11380        );
11381
11382        let transport = [0xDD; 16];
11383        let (resp_tx, resp_rx) = mpsc::channel();
11384        tx.send(Event::Query(
11385            QueryRequest::DropAllVia {
11386                transport_hash: transport,
11387            },
11388            resp_tx,
11389        ))
11390        .unwrap();
11391        tx.send(Event::Shutdown).unwrap();
11392        driver.run();
11393
11394        match resp_rx.recv().unwrap() {
11395            QueryResponse::DropAllVia(count) => assert_eq!(count, 0),
11396            _ => panic!("unexpected response"),
11397        }
11398    }
11399
11400    #[test]
11401    fn query_drop_announce_queues() {
11402        let (tx, rx) = event::channel();
11403        let (cbs, _, _, _, _, _) = MockCallbacks::new();
11404        let mut driver = Driver::new(
11405            TransportConfig {
11406                transport_enabled: false,
11407                identity_hash: None,
11408                prefer_shorter_path: false,
11409                max_paths_per_destination: 1,
11410                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
11411                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
11412                max_path_destinations: usize::MAX,
11413                max_tunnel_destinations_total: usize::MAX,
11414                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
11415                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
11416                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
11417                announce_sig_cache_enabled: true,
11418                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
11419                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
11420                announce_queue_max_entries: 256,
11421                announce_queue_max_interfaces: 1024,
11422            },
11423            rx,
11424            tx.clone(),
11425            Box::new(cbs),
11426        );
11427
11428        let (resp_tx, resp_rx) = mpsc::channel();
11429        tx.send(Event::Query(QueryRequest::DropAnnounceQueues, resp_tx))
11430            .unwrap();
11431        tx.send(Event::Shutdown).unwrap();
11432        driver.run();
11433
11434        match resp_rx.recv().unwrap() {
11435            QueryResponse::DropAnnounceQueues => {}
11436            _ => panic!("unexpected response"),
11437        }
11438    }
11439
11440    // =========================================================================
11441    // Phase 7e: Link wiring integration tests
11442    // =========================================================================
11443
11444    #[test]
11445    fn register_link_dest_event() {
11446        let (tx, rx) = event::channel();
11447        let (cbs, _, _, _, _, _) = MockCallbacks::new();
11448        let mut driver = Driver::new(
11449            TransportConfig {
11450                transport_enabled: false,
11451                identity_hash: None,
11452                prefer_shorter_path: false,
11453                max_paths_per_destination: 1,
11454                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
11455                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
11456                max_path_destinations: usize::MAX,
11457                max_tunnel_destinations_total: usize::MAX,
11458                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
11459                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
11460                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
11461                announce_sig_cache_enabled: true,
11462                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
11463                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
11464                announce_queue_max_entries: 256,
11465                announce_queue_max_interfaces: 1024,
11466            },
11467            rx,
11468            tx.clone(),
11469            Box::new(cbs),
11470        );
11471        let info = make_interface_info(1);
11472        driver.engine.register_interface(info);
11473        let (writer, _sent) = MockWriter::new();
11474        driver
11475            .interfaces
11476            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
11477
11478        let mut rng = OsRng;
11479        let sig_prv = rns_crypto::ed25519::Ed25519PrivateKey::generate(&mut rng);
11480        let sig_pub_bytes = sig_prv.public_key().public_bytes();
11481        let sig_prv_bytes = sig_prv.private_bytes();
11482        let dest_hash = [0xDD; 16];
11483
11484        tx.send(Event::RegisterLinkDestination {
11485            dest_hash,
11486            sig_prv_bytes,
11487            sig_pub_bytes,
11488            resource_strategy: 0,
11489        })
11490        .unwrap();
11491        tx.send(Event::Shutdown).unwrap();
11492        driver.run();
11493
11494        // Link manager should know about the destination
11495        assert!(driver.link_manager.is_link_destination(&dest_hash));
11496    }
11497
11498    #[test]
11499    fn create_link_event() {
11500        let (tx, rx) = event::channel();
11501        let (cbs, _link_established, _, _) = MockCallbacks::with_link_tracking();
11502        let mut driver = Driver::new(
11503            TransportConfig {
11504                transport_enabled: false,
11505                identity_hash: None,
11506                prefer_shorter_path: false,
11507                max_paths_per_destination: 1,
11508                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
11509                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
11510                max_path_destinations: usize::MAX,
11511                max_tunnel_destinations_total: usize::MAX,
11512                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
11513                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
11514                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
11515                announce_sig_cache_enabled: true,
11516                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
11517                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
11518                announce_queue_max_entries: 256,
11519                announce_queue_max_interfaces: 1024,
11520            },
11521            rx,
11522            tx.clone(),
11523            Box::new(cbs),
11524        );
11525        let info = make_interface_info(1);
11526        driver.engine.register_interface(info);
11527        let (writer, _sent) = MockWriter::new();
11528        driver
11529            .interfaces
11530            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
11531
11532        let dest_hash = [0xDD; 16];
11533        let dummy_sig_pub = [0xAA; 32];
11534
11535        let (resp_tx, resp_rx) = mpsc::channel();
11536        tx.send(Event::CreateLink {
11537            dest_hash,
11538            dest_sig_pub_bytes: dummy_sig_pub,
11539            response_tx: resp_tx,
11540        })
11541        .unwrap();
11542        tx.send(Event::Shutdown).unwrap();
11543        driver.run();
11544
11545        // Should have received a link_id
11546        let link_id = resp_rx.recv().unwrap();
11547        assert_ne!(link_id, [0u8; 16]);
11548
11549        // Link should be in pending state in the manager
11550        assert_eq!(driver.link_manager.link_count(), 1);
11551
11552        // The LINKREQUEST packet won't be sent on the wire without a path
11553        // to the destination (DESTINATION_LINK requires a known path or
11554        // attached_interface). In a real scenario, the path would exist from
11555        // an announce received earlier.
11556    }
11557
11558    #[test]
11559    fn create_link_uses_known_destination_interface_without_path() {
11560        let (tx, rx) = event::channel();
11561        let (cbs, _link_established, _, _) = MockCallbacks::with_link_tracking();
11562        let mut driver = Driver::new(
11563            TransportConfig {
11564                transport_enabled: false,
11565                identity_hash: None,
11566                prefer_shorter_path: false,
11567                max_paths_per_destination: 1,
11568                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
11569                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
11570                max_path_destinations: usize::MAX,
11571                max_tunnel_destinations_total: usize::MAX,
11572                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
11573                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
11574                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
11575                announce_sig_cache_enabled: true,
11576                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
11577                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
11578                announce_queue_max_entries: 256,
11579                announce_queue_max_interfaces: 1024,
11580            },
11581            rx,
11582            tx.clone(),
11583            Box::new(cbs),
11584        );
11585        for id in [1, 2] {
11586            driver.engine.register_interface(make_interface_info(id));
11587        }
11588        let (writer, sent) = MockWriter::new();
11589        let (writer2, sent2) = MockWriter::new();
11590        driver
11591            .interfaces
11592            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
11593        driver
11594            .interfaces
11595            .insert(InterfaceId(2), make_entry(2, Box::new(writer2), true));
11596
11597        let dest_hash = [0xD1; 16];
11598        driver.known_destinations.insert(
11599            dest_hash,
11600            make_known_destination_state(dest_hash, 10.0, InterfaceId(2)),
11601        );
11602
11603        let dummy_sig_pub = [0xA1; 32];
11604        let (resp_tx, resp_rx) = mpsc::channel();
11605        tx.send(Event::CreateLink {
11606            dest_hash,
11607            dest_sig_pub_bytes: dummy_sig_pub,
11608            response_tx: resp_tx,
11609        })
11610        .unwrap();
11611        tx.send(Event::Shutdown).unwrap();
11612        driver.run();
11613
11614        let link_id = resp_rx.recv().unwrap();
11615        assert_ne!(link_id, [0u8; 16]);
11616        assert_eq!(driver.link_manager.link_count(), 1);
11617
11618        let sent_packets = sent.lock().unwrap();
11619        let sent_packets2 = sent2.lock().unwrap();
11620        assert!(
11621            sent_packets.is_empty(),
11622            "LINKREQUEST should not broadcast to unrelated interfaces when a known destination interface exists"
11623        );
11624        assert_eq!(sent_packets2.len(), 1);
11625        let flags = PacketFlags::unpack(sent_packets2[0][0] & 0x7F);
11626        assert_eq!(flags.packet_type, constants::PACKET_TYPE_LINKREQUEST);
11627        assert_eq!(extract_dest_hash(&sent_packets2[0]), dest_hash);
11628    }
11629
11630    #[test]
11631    fn create_link_ignores_sentinel_known_destination_interface() {
11632        let (tx, rx) = event::channel();
11633        let (cbs, _link_established, _, _) = MockCallbacks::with_link_tracking();
11634        let mut driver = Driver::new(
11635            TransportConfig {
11636                transport_enabled: false,
11637                identity_hash: None,
11638                prefer_shorter_path: false,
11639                max_paths_per_destination: 1,
11640                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
11641                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
11642                max_path_destinations: usize::MAX,
11643                max_tunnel_destinations_total: usize::MAX,
11644                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
11645                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
11646                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
11647                announce_sig_cache_enabled: true,
11648                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
11649                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
11650                announce_queue_max_entries: 256,
11651                announce_queue_max_interfaces: 1024,
11652            },
11653            rx,
11654            tx.clone(),
11655            Box::new(cbs),
11656        );
11657        for id in [1, 2] {
11658            driver.engine.register_interface(make_interface_info(id));
11659        }
11660        let (writer, sent) = MockWriter::new();
11661        let (writer2, sent2) = MockWriter::new();
11662        driver
11663            .interfaces
11664            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
11665        driver
11666            .interfaces
11667            .insert(InterfaceId(2), make_entry(2, Box::new(writer2), true));
11668
11669        let dest_hash = [0xD2; 16];
11670        driver.known_destinations.insert(
11671            dest_hash,
11672            make_known_destination_state(dest_hash, 10.0, InterfaceId(0)),
11673        );
11674
11675        let dummy_sig_pub = [0xA2; 32];
11676        let (resp_tx, resp_rx) = mpsc::channel();
11677        tx.send(Event::CreateLink {
11678            dest_hash,
11679            dest_sig_pub_bytes: dummy_sig_pub,
11680            response_tx: resp_tx,
11681        })
11682        .unwrap();
11683        tx.send(Event::Shutdown).unwrap();
11684        driver.run();
11685
11686        let link_id = resp_rx.recv().unwrap();
11687        assert_ne!(link_id, [0u8; 16]);
11688        assert_eq!(driver.link_manager.link_count(), 1);
11689
11690        let sent_packets = sent.lock().unwrap();
11691        let sent_packets2 = sent2.lock().unwrap();
11692        assert!(
11693            sent_packets.len() == 1 && sent_packets2.len() == 1,
11694            "sentinel InterfaceId(0) must not suppress the default broadcast behavior"
11695        );
11696        let flags = PacketFlags::unpack(sent_packets[0][0] & 0x7F);
11697        assert_eq!(flags.packet_type, constants::PACKET_TYPE_LINKREQUEST);
11698    }
11699
11700    #[test]
11701    fn deliver_local_routes_to_link_manager() {
11702        // Verify that DeliverLocal for a registered link destination goes to
11703        // the link manager instead of the callbacks.
11704        let (tx, rx) = event::channel();
11705        let (cbs, _, _, _, _, _) = MockCallbacks::new();
11706        let mut driver = Driver::new(
11707            TransportConfig {
11708                transport_enabled: false,
11709                identity_hash: None,
11710                prefer_shorter_path: false,
11711                max_paths_per_destination: 1,
11712                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
11713                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
11714                max_path_destinations: usize::MAX,
11715                max_tunnel_destinations_total: usize::MAX,
11716                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
11717                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
11718                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
11719                announce_sig_cache_enabled: true,
11720                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
11721                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
11722                announce_queue_max_entries: 256,
11723                announce_queue_max_interfaces: 1024,
11724            },
11725            rx,
11726            tx.clone(),
11727            Box::new(cbs),
11728        );
11729        let info = make_interface_info(1);
11730        driver.engine.register_interface(info);
11731        let (writer, _sent) = MockWriter::new();
11732        driver
11733            .interfaces
11734            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
11735
11736        // Register a link destination
11737        let mut rng = OsRng;
11738        let sig_prv = rns_crypto::ed25519::Ed25519PrivateKey::generate(&mut rng);
11739        let sig_pub_bytes = sig_prv.public_key().public_bytes();
11740        let dest_hash = [0xEE; 16];
11741        driver.link_manager.register_link_destination(
11742            dest_hash,
11743            sig_prv,
11744            sig_pub_bytes,
11745            crate::link_manager::ResourceStrategy::AcceptNone,
11746        );
11747
11748        // dispatch_all with a DeliverLocal for that dest should route to link_manager
11749        // (not to callbacks). We can't easily test this via run() since we need
11750        // a valid LINKREQUEST, but we can check is_link_destination works.
11751        assert!(driver.link_manager.is_link_destination(&dest_hash));
11752
11753        // Non-link destination should go to callbacks
11754        assert!(!driver.link_manager.is_link_destination(&[0xFF; 16]));
11755
11756        drop(tx);
11757    }
11758
11759    #[test]
11760    fn teardown_link_event() {
11761        let (tx, rx) = event::channel();
11762        let (cbs, _, link_closed, _) = MockCallbacks::with_link_tracking();
11763        let mut driver = Driver::new(
11764            TransportConfig {
11765                transport_enabled: false,
11766                identity_hash: None,
11767                prefer_shorter_path: false,
11768                max_paths_per_destination: 1,
11769                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
11770                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
11771                max_path_destinations: usize::MAX,
11772                max_tunnel_destinations_total: usize::MAX,
11773                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
11774                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
11775                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
11776                announce_sig_cache_enabled: true,
11777                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
11778                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
11779                announce_queue_max_entries: 256,
11780                announce_queue_max_interfaces: 1024,
11781            },
11782            rx,
11783            tx.clone(),
11784            Box::new(cbs),
11785        );
11786        let info = make_interface_info(1);
11787        driver.engine.register_interface(info);
11788        let (writer, _sent) = MockWriter::new();
11789        driver
11790            .interfaces
11791            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
11792
11793        // Create a link first
11794        let (resp_tx, resp_rx) = mpsc::channel();
11795        tx.send(Event::CreateLink {
11796            dest_hash: [0xDD; 16],
11797            dest_sig_pub_bytes: [0xAA; 32],
11798            response_tx: resp_tx,
11799        })
11800        .unwrap();
11801        // Then tear it down
11802        // We can't receive resp_rx yet since driver.run() hasn't started,
11803        // but we know the link_id will be created. Send teardown after CreateLink.
11804        // Actually, we need to get the link_id first. Let's use a two-phase approach.
11805        tx.send(Event::Shutdown).unwrap();
11806        driver.run();
11807
11808        let link_id = resp_rx.recv().unwrap();
11809        assert_ne!(link_id, [0u8; 16]);
11810        assert_eq!(driver.link_manager.link_count(), 1);
11811
11812        // Now restart with same driver (just use events directly since driver loop exited)
11813        let teardown_actions = driver.link_manager.teardown_link(&link_id);
11814        driver.dispatch_link_actions(teardown_actions);
11815
11816        // Callback should have been called
11817        assert_eq!(link_closed.lock().unwrap().len(), 1);
11818        assert_eq!(link_closed.lock().unwrap()[0], TypedLinkId(link_id));
11819    }
11820
11821    #[test]
11822    fn link_count_includes_link_manager() {
11823        let (tx, rx) = event::channel();
11824        let (cbs, _, _, _, _, _) = MockCallbacks::new();
11825        let mut driver = Driver::new(
11826            TransportConfig {
11827                transport_enabled: false,
11828                identity_hash: None,
11829                prefer_shorter_path: false,
11830                max_paths_per_destination: 1,
11831                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
11832                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
11833                max_path_destinations: usize::MAX,
11834                max_tunnel_destinations_total: usize::MAX,
11835                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
11836                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
11837                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
11838                announce_sig_cache_enabled: true,
11839                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
11840                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
11841                announce_queue_max_entries: 256,
11842                announce_queue_max_interfaces: 1024,
11843            },
11844            rx,
11845            tx.clone(),
11846            Box::new(cbs),
11847        );
11848        let info = make_interface_info(1);
11849        driver.engine.register_interface(info);
11850        let (writer, _sent) = MockWriter::new();
11851        driver
11852            .interfaces
11853            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
11854
11855        // Create a link via link_manager directly
11856        let mut rng = OsRng;
11857        let dummy_sig = [0xAA; 32];
11858        driver.link_manager.create_link(
11859            &[0xDD; 16],
11860            &dummy_sig,
11861            1,
11862            constants::MTU as u32,
11863            &mut rng,
11864        );
11865
11866        // Query link count — should include link_manager links
11867        let (resp_tx, resp_rx) = mpsc::channel();
11868        tx.send(Event::Query(QueryRequest::LinkCount, resp_tx))
11869            .unwrap();
11870        tx.send(Event::Shutdown).unwrap();
11871        driver.run();
11872
11873        match resp_rx.recv().unwrap() {
11874            QueryResponse::LinkCount(count) => assert_eq!(count, 1),
11875            _ => panic!("unexpected response"),
11876        }
11877    }
11878
11879    #[test]
11880    fn register_request_handler_event() {
11881        let (tx, rx) = event::channel();
11882        let (cbs, _, _, _, _, _) = MockCallbacks::new();
11883        let mut driver = Driver::new(
11884            TransportConfig {
11885                transport_enabled: false,
11886                identity_hash: None,
11887                prefer_shorter_path: false,
11888                max_paths_per_destination: 1,
11889                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
11890                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
11891                max_path_destinations: usize::MAX,
11892                max_tunnel_destinations_total: usize::MAX,
11893                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
11894                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
11895                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
11896                announce_sig_cache_enabled: true,
11897                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
11898                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
11899                announce_queue_max_entries: 256,
11900                announce_queue_max_interfaces: 1024,
11901            },
11902            rx,
11903            tx.clone(),
11904            Box::new(cbs),
11905        );
11906
11907        tx.send(Event::RegisterRequestHandler {
11908            path: "/status".to_string(),
11909            allowed_list: None,
11910            handler: Box::new(|_link_id, _path, _data, _remote| Some(b"OK".to_vec())),
11911        })
11912        .unwrap();
11913        tx.send(Event::Shutdown).unwrap();
11914        driver.run();
11915
11916        // Handler should be registered (we can't directly query the count,
11917        // but at least verify no crash)
11918    }
11919
11920    // Phase 8c: Management announce timing tests
11921
11922    #[test]
11923    fn management_announces_emitted_after_delay() {
11924        let (tx, rx) = event::channel();
11925        let (cbs, _announces, _, _, _, _) = MockCallbacks::new();
11926        let identity = Identity::new(&mut OsRng);
11927        let identity_hash = *identity.hash();
11928        let mut driver = Driver::new(
11929            TransportConfig {
11930                transport_enabled: true,
11931                identity_hash: Some(identity_hash),
11932                prefer_shorter_path: false,
11933                max_paths_per_destination: 1,
11934                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
11935                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
11936                max_path_destinations: usize::MAX,
11937                max_tunnel_destinations_total: usize::MAX,
11938                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
11939                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
11940                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
11941                announce_sig_cache_enabled: true,
11942                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
11943                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
11944                announce_queue_max_entries: 256,
11945                announce_queue_max_interfaces: 1024,
11946            },
11947            rx,
11948            tx.clone(),
11949            Box::new(cbs),
11950        );
11951
11952        // Register interface so announces can be sent
11953        let info = make_interface_info(1);
11954        driver.engine.register_interface(info.clone());
11955        let (writer, sent) = MockWriter::new();
11956        driver
11957            .interfaces
11958            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
11959
11960        // Enable management announces
11961        driver.management_config.enable_remote_management = true;
11962        driver.transport_identity = Some(identity);
11963
11964        // Set started time to 10 seconds ago so the 5s delay has passed
11965        driver.started = time::now() - 10.0;
11966
11967        // Send Tick then Shutdown
11968        tx.send(Event::Tick).unwrap();
11969        tx.send(Event::Shutdown).unwrap();
11970        driver.run();
11971
11972        // Should have sent at least one packet (the management announce)
11973        let sent_packets = sent.lock().unwrap();
11974        assert!(
11975            !sent_packets.is_empty(),
11976            "Management announce should be sent after startup delay"
11977        );
11978    }
11979
11980    #[test]
11981    fn runtime_config_list_contains_global_keys() {
11982        let driver = new_test_driver();
11983        let response = driver.handle_query(QueryRequest::ListRuntimeConfig);
11984        let QueryResponse::RuntimeConfigList(entries) = response else {
11985            panic!("expected runtime config list");
11986        };
11987        let keys: Vec<String> = entries.into_iter().map(|entry| entry.key).collect();
11988        assert!(keys.contains(&"global.tick_interval_ms".to_string()));
11989        assert!(keys.contains(&"global.known_destinations_ttl_secs".to_string()));
11990        assert!(keys.contains(&"global.rate_limiter_ttl_secs".to_string()));
11991        assert!(keys.contains(&"global.direct_connect_policy".to_string()));
11992    }
11993
11994    #[test]
11995    fn runtime_config_set_and_reset_tick_interval() {
11996        let mut driver = new_test_driver();
11997
11998        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11999            key: "global.tick_interval_ms".into(),
12000            value: RuntimeConfigValue::Int(250),
12001        });
12002        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12003            panic!("expected runtime config set success");
12004        };
12005        assert_eq!(entry.key, "global.tick_interval_ms");
12006        assert_eq!(entry.value, RuntimeConfigValue::Int(250));
12007        assert_eq!(driver.tick_interval_ms.load(Ordering::Relaxed), 250);
12008
12009        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
12010            key: "global.tick_interval_ms".into(),
12011        });
12012        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
12013            panic!("expected runtime config reset success");
12014        };
12015        assert_eq!(entry.value, RuntimeConfigValue::Int(1000));
12016        assert_eq!(driver.tick_interval_ms.load(Ordering::Relaxed), 1000);
12017    }
12018
12019    #[test]
12020    fn runtime_config_rejects_invalid_policy() {
12021        let mut driver = new_test_driver();
12022        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12023            key: "global.direct_connect_policy".into(),
12024            value: RuntimeConfigValue::String("bogus".into()),
12025        });
12026        let QueryResponse::RuntimeConfigSet(Err(err)) = response else {
12027            panic!("expected runtime config set failure");
12028        };
12029        assert_eq!(err.code, RuntimeConfigErrorCode::InvalidValue);
12030    }
12031
12032    #[test]
12033    fn runtime_config_set_and_reset_rate_limiter_ttl() {
12034        let mut driver = new_test_driver();
12035
12036        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12037            key: "global.rate_limiter_ttl_secs".into(),
12038            value: RuntimeConfigValue::Float(600.0),
12039        });
12040        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12041            panic!("expected runtime config set success");
12042        };
12043        assert_eq!(entry.value, RuntimeConfigValue::Float(600.0));
12044        assert_eq!(driver.rate_limiter_ttl_secs, 600.0);
12045
12046        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
12047            key: "global.rate_limiter_ttl_secs".into(),
12048        });
12049        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
12050            panic!("expected runtime config reset success");
12051        };
12052        assert_eq!(
12053            entry.value,
12054            RuntimeConfigValue::Float(DEFAULT_RATE_LIMITER_TTL_SECS)
12055        );
12056        assert_eq!(driver.rate_limiter_ttl_secs, DEFAULT_RATE_LIMITER_TTL_SECS);
12057    }
12058
12059    #[cfg(feature = "iface-backbone")]
12060    #[test]
12061    fn runtime_config_lists_backbone_keys() {
12062        let mut driver = new_test_driver();
12063        register_test_backbone(&mut driver, "public");
12064        register_test_backbone_client(&mut driver, "uplink");
12065        register_test_backbone_discovery(&mut driver, "public", false);
12066        let response = driver.handle_query(QueryRequest::ListRuntimeConfig);
12067        let QueryResponse::RuntimeConfigList(entries) = response else {
12068            panic!("expected runtime config list");
12069        };
12070        let keys: Vec<String> = entries.into_iter().map(|entry| entry.key).collect();
12071        assert!(keys.contains(&"backbone.public.idle_timeout_secs".to_string()));
12072        assert!(keys.contains(&"backbone.public.write_stall_timeout_secs".to_string()));
12073        assert!(keys.contains(&"backbone.public.max_connections".to_string()));
12074        assert!(keys.contains(&"backbone.public.discoverable".to_string()));
12075        assert!(keys.contains(&"backbone.public.discovery_name".to_string()));
12076        assert!(keys.contains(&"backbone.public.latitude".to_string()));
12077        assert!(keys.contains(&"backbone.public.longitude".to_string()));
12078        assert!(keys.contains(&"backbone.public.height".to_string()));
12079        assert!(keys.contains(&"backbone_client.uplink.connect_timeout_secs".to_string()));
12080        assert!(keys.contains(&"backbone_client.uplink.reconnect_wait_secs".to_string()));
12081        assert!(keys.contains(&"backbone_client.uplink.max_reconnect_tries".to_string()));
12082    }
12083
12084    #[cfg(feature = "iface-backbone")]
12085    #[test]
12086    fn runtime_config_sets_backbone_values() {
12087        let mut driver = new_test_driver();
12088        register_test_backbone(&mut driver, "public");
12089        register_test_backbone_discovery(&mut driver, "public", false);
12090        driver.transport_identity = Some(rns_crypto::identity::Identity::new(&mut OsRng));
12091
12092        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12093            key: "backbone.public.idle_timeout_secs".into(),
12094            value: RuntimeConfigValue::Float(2.5),
12095        });
12096        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12097            panic!("expected runtime config set success");
12098        };
12099        assert_eq!(entry.value, RuntimeConfigValue::Float(2.5));
12100
12101        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12102            key: "backbone.public.write_stall_timeout_secs".into(),
12103            value: RuntimeConfigValue::Float(15.0),
12104        });
12105        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12106            panic!("expected runtime config set success");
12107        };
12108        assert_eq!(entry.value, RuntimeConfigValue::Float(15.0));
12109
12110        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12111            key: "backbone.public.max_connections".into(),
12112            value: RuntimeConfigValue::Int(0),
12113        });
12114        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12115            panic!("expected runtime config set success");
12116        };
12117        assert_eq!(entry.value, RuntimeConfigValue::Int(0));
12118
12119        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
12120            key: "backbone.public.max_connections".into(),
12121        });
12122        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
12123            panic!("expected runtime config reset success");
12124        };
12125        assert_eq!(entry.value, RuntimeConfigValue::Int(8));
12126
12127        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
12128            key: "backbone.public.write_stall_timeout_secs".into(),
12129        });
12130        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
12131            panic!("expected runtime config reset success");
12132        };
12133        assert_eq!(entry.value, RuntimeConfigValue::Float(30.0));
12134
12135        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12136            key: "backbone.public.discoverable".into(),
12137            value: RuntimeConfigValue::Bool(true),
12138        });
12139        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12140            panic!("expected runtime config set success");
12141        };
12142        assert_eq!(entry.value, RuntimeConfigValue::Bool(true));
12143        assert!(driver
12144            .interface_announcer
12145            .as_ref()
12146            .map(|announcer| announcer.contains_interface("public"))
12147            .unwrap_or(false));
12148
12149        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12150            key: "backbone.public.discovery_name".into(),
12151            value: RuntimeConfigValue::String("Public Backbone".into()),
12152        });
12153        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12154            panic!("expected runtime config set success");
12155        };
12156        assert_eq!(
12157            entry.value,
12158            RuntimeConfigValue::String("Public Backbone".into())
12159        );
12160
12161        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12162            key: "backbone.public.latitude".into(),
12163            value: RuntimeConfigValue::Float(45.4642),
12164        });
12165        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12166            panic!("expected runtime config set success");
12167        };
12168        assert_eq!(entry.value, RuntimeConfigValue::Float(45.4642));
12169
12170        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12171            key: "backbone.public.longitude".into(),
12172            value: RuntimeConfigValue::Float(9.19),
12173        });
12174        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12175            panic!("expected runtime config set success");
12176        };
12177        assert_eq!(entry.value, RuntimeConfigValue::Float(9.19));
12178
12179        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12180            key: "backbone.public.height".into(),
12181            value: RuntimeConfigValue::Int(120),
12182        });
12183        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12184            panic!("expected runtime config set success");
12185        };
12186        assert_eq!(entry.value, RuntimeConfigValue::Float(120.0));
12187
12188        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
12189            key: "backbone.public.discoverable".into(),
12190        });
12191        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
12192            panic!("expected runtime config reset success");
12193        };
12194        assert_eq!(entry.value, RuntimeConfigValue::Bool(false));
12195        assert!(driver.interface_announcer.is_none());
12196
12197        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
12198            key: "backbone.public.latitude".into(),
12199        });
12200        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
12201            panic!("expected runtime config reset success");
12202        };
12203        assert_eq!(entry.value, RuntimeConfigValue::Null);
12204    }
12205
12206    #[cfg(feature = "iface-backbone")]
12207    #[test]
12208    fn runtime_config_sets_backbone_client_values() {
12209        let mut driver = new_test_driver();
12210        register_test_backbone_client(&mut driver, "uplink");
12211
12212        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12213            key: "backbone_client.uplink.connect_timeout_secs".into(),
12214            value: RuntimeConfigValue::Float(2.5),
12215        });
12216        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12217            panic!("expected runtime config set success");
12218        };
12219        assert_eq!(entry.value, RuntimeConfigValue::Float(2.5));
12220
12221        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12222            key: "backbone_client.uplink.max_reconnect_tries".into(),
12223            value: RuntimeConfigValue::Int(0),
12224        });
12225        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12226            panic!("expected runtime config set success");
12227        };
12228        assert_eq!(entry.value, RuntimeConfigValue::Int(0));
12229
12230        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
12231            key: "backbone_client.uplink.connect_timeout_secs".into(),
12232        });
12233        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
12234            panic!("expected runtime config reset success");
12235        };
12236        assert_eq!(entry.value, RuntimeConfigValue::Float(5.0));
12237    }
12238
12239    #[cfg(feature = "iface-backbone")]
12240    #[test]
12241    fn backbone_peer_state_query_lists_entries() {
12242        let mut driver = new_test_driver();
12243        register_test_backbone(&mut driver, "public");
12244        driver
12245            .backbone_peer_state
12246            .get("public")
12247            .unwrap()
12248            .peer_state
12249            .lock()
12250            .unwrap()
12251            .seed_entry(BackbonePeerStateEntry {
12252                interface_name: "public".into(),
12253                peer_ip: "203.0.113.10".parse().unwrap(),
12254                connected_count: 1,
12255                blacklisted_remaining_secs: Some(120.0),
12256                blacklist_reason: Some("repeated idle timeouts".into()),
12257                reject_count: 7,
12258            });
12259
12260        let response = driver.handle_query(QueryRequest::BackbonePeerState {
12261            interface_name: Some("public".into()),
12262        });
12263        let QueryResponse::BackbonePeerState(entries) = response else {
12264            panic!("expected backbone peer state list");
12265        };
12266        assert_eq!(entries.len(), 1);
12267        assert_eq!(entries[0].peer_ip.to_string(), "203.0.113.10");
12268        assert_eq!(entries[0].connected_count, 1);
12269        assert_eq!(entries[0].reject_count, 7);
12270        assert_eq!(
12271            entries[0].blacklist_reason.as_deref(),
12272            Some("repeated idle timeouts")
12273        );
12274        assert!(entries[0].blacklisted_remaining_secs.unwrap() > 0.0);
12275    }
12276
12277    #[cfg(feature = "iface-backbone")]
12278    #[test]
12279    fn backbone_peer_state_clear_removes_entry() {
12280        let mut driver = new_test_driver();
12281        register_test_backbone(&mut driver, "public");
12282        driver
12283            .backbone_peer_state
12284            .get("public")
12285            .unwrap()
12286            .peer_state
12287            .lock()
12288            .unwrap()
12289            .seed_entry(BackbonePeerStateEntry {
12290                interface_name: "public".into(),
12291                peer_ip: "203.0.113.11".parse().unwrap(),
12292                connected_count: 0,
12293                blacklisted_remaining_secs: None,
12294                blacklist_reason: None,
12295                reject_count: 0,
12296            });
12297
12298        let response = driver.handle_query_mut(QueryRequest::ClearBackbonePeerState {
12299            interface_name: "public".into(),
12300            peer_ip: "203.0.113.11".parse().unwrap(),
12301        });
12302        let QueryResponse::ClearBackbonePeerState(true) = response else {
12303            panic!("expected successful peer-state clear");
12304        };
12305
12306        let response = driver.handle_query(QueryRequest::BackbonePeerState {
12307            interface_name: Some("public".into()),
12308        });
12309        let QueryResponse::BackbonePeerState(entries) = response else {
12310            panic!("expected backbone peer state list");
12311        };
12312        assert!(entries.is_empty());
12313    }
12314
12315    #[cfg(feature = "iface-backbone")]
12316    #[test]
12317    fn backbone_peer_blacklist_sets_blacklist() {
12318        let mut driver = new_test_driver();
12319        register_test_backbone(&mut driver, "public");
12320        driver
12321            .backbone_peer_state
12322            .get("public")
12323            .unwrap()
12324            .peer_state
12325            .lock()
12326            .unwrap()
12327            .seed_entry(BackbonePeerStateEntry {
12328                interface_name: "public".into(),
12329                peer_ip: "203.0.113.50".parse().unwrap(),
12330                connected_count: 1,
12331                blacklisted_remaining_secs: None,
12332                blacklist_reason: None,
12333                reject_count: 0,
12334            });
12335
12336        let response = driver.handle_query_mut(QueryRequest::BlacklistBackbonePeer {
12337            interface_name: "public".into(),
12338            peer_ip: "203.0.113.50".parse().unwrap(),
12339            duration: Duration::from_secs(300),
12340            reason: "sentinel blacklist".into(),
12341            penalty_level: 2,
12342        });
12343        let QueryResponse::BlacklistBackbonePeer(true) = response else {
12344            panic!("expected successful blacklist");
12345        };
12346
12347        // Verify the peer is now blacklisted
12348        let response = driver.handle_query(QueryRequest::BackbonePeerState {
12349            interface_name: Some("public".into()),
12350        });
12351        let QueryResponse::BackbonePeerState(entries) = response else {
12352            panic!("expected backbone peer state list");
12353        };
12354        let entry = entries
12355            .iter()
12356            .find(|e| e.peer_ip == "203.0.113.50".parse::<std::net::IpAddr>().unwrap())
12357            .expect("expected entry for blacklisted peer");
12358        assert!(entry.blacklisted_remaining_secs.is_some());
12359        let remaining = entry.blacklisted_remaining_secs.unwrap();
12360        assert!(remaining > 290.0 && remaining <= 300.0);
12361        assert_eq!(
12362            entry.blacklist_reason.as_deref(),
12363            Some("sentinel blacklist")
12364        );
12365    }
12366
12367    #[cfg(feature = "iface-backbone")]
12368    #[test]
12369    fn backbone_peer_blacklist_unknown_interface_returns_false() {
12370        let mut driver = new_test_driver();
12371        let response = driver.handle_query_mut(QueryRequest::BlacklistBackbonePeer {
12372            interface_name: "nonexistent".into(),
12373            peer_ip: "203.0.113.50".parse().unwrap(),
12374            duration: Duration::from_secs(60),
12375            reason: "sentinel blacklist".into(),
12376            penalty_level: 1,
12377        });
12378        let QueryResponse::BlacklistBackbonePeer(false) = response else {
12379            panic!("expected false for unknown interface");
12380        };
12381    }
12382
12383    #[cfg(feature = "iface-backbone")]
12384    #[test]
12385    fn backbone_peer_blacklist_creates_entry_for_unknown_ip() {
12386        let mut driver = new_test_driver();
12387        register_test_backbone(&mut driver, "public");
12388
12389        // Blacklist an IP that has no existing peer state
12390        let response = driver.handle_query_mut(QueryRequest::BlacklistBackbonePeer {
12391            interface_name: "public".into(),
12392            peer_ip: "198.51.100.1".parse().unwrap(),
12393            duration: Duration::from_secs(120),
12394            reason: "sentinel blacklist".into(),
12395            penalty_level: 1,
12396        });
12397        let QueryResponse::BlacklistBackbonePeer(true) = response else {
12398            panic!("expected successful blacklist for new IP");
12399        };
12400
12401        let response = driver.handle_query(QueryRequest::BackbonePeerState {
12402            interface_name: Some("public".into()),
12403        });
12404        let QueryResponse::BackbonePeerState(entries) = response else {
12405            panic!("expected backbone peer state list");
12406        };
12407        let entry = entries
12408            .iter()
12409            .find(|e| e.peer_ip == "198.51.100.1".parse::<std::net::IpAddr>().unwrap())
12410            .expect("expected entry for newly blacklisted IP");
12411        assert!(entry.blacklisted_remaining_secs.is_some());
12412    }
12413
12414    #[cfg(feature = "iface-tcp")]
12415    #[test]
12416    fn runtime_config_lists_tcp_server_keys() {
12417        let mut driver = new_test_driver();
12418        register_test_tcp_server(&mut driver, "public");
12419        register_test_tcp_server_discovery(&mut driver, "public", false);
12420        let response = driver.handle_query(QueryRequest::ListRuntimeConfig);
12421        let QueryResponse::RuntimeConfigList(entries) = response else {
12422            panic!("expected runtime config list");
12423        };
12424        let keys: Vec<String> = entries.into_iter().map(|entry| entry.key).collect();
12425        assert!(keys.contains(&"tcp_server.public.max_connections".to_string()));
12426        assert!(keys.contains(&"tcp_server.public.discoverable".to_string()));
12427        assert!(keys.contains(&"tcp_server.public.discovery_name".to_string()));
12428    }
12429
12430    #[cfg(feature = "iface-tcp")]
12431    #[test]
12432    fn runtime_config_sets_tcp_server_values() {
12433        let mut driver = new_test_driver();
12434        register_test_tcp_server(&mut driver, "public");
12435        register_test_tcp_server_discovery(&mut driver, "public", false);
12436        driver.transport_identity = Some(rns_crypto::identity::Identity::new(&mut OsRng));
12437
12438        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12439            key: "tcp_server.public.max_connections".into(),
12440            value: RuntimeConfigValue::Int(0),
12441        });
12442        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12443            panic!("expected runtime config set success");
12444        };
12445        assert_eq!(entry.value, RuntimeConfigValue::Int(0));
12446
12447        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
12448            key: "tcp_server.public.max_connections".into(),
12449        });
12450        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
12451            panic!("expected runtime config reset success");
12452        };
12453        assert_eq!(entry.value, RuntimeConfigValue::Int(4));
12454
12455        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12456            key: "tcp_server.public.discoverable".into(),
12457            value: RuntimeConfigValue::Bool(true),
12458        });
12459        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12460            panic!("expected runtime config set success");
12461        };
12462        assert_eq!(entry.value, RuntimeConfigValue::Bool(true));
12463
12464        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12465            key: "tcp_server.public.latitude".into(),
12466            value: RuntimeConfigValue::Float(41.9028),
12467        });
12468        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12469            panic!("expected runtime config set success");
12470        };
12471        assert_eq!(entry.value, RuntimeConfigValue::Float(41.9028));
12472
12473        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
12474            key: "tcp_server.public.latitude".into(),
12475        });
12476        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
12477            panic!("expected runtime config reset success");
12478        };
12479        assert_eq!(entry.value, RuntimeConfigValue::Null);
12480    }
12481
12482    #[cfg(feature = "iface-tcp")]
12483    #[test]
12484    fn runtime_config_lists_tcp_client_keys() {
12485        let mut driver = new_test_driver();
12486        register_test_tcp_client(&mut driver, "uplink");
12487        let response = driver.handle_query(QueryRequest::ListRuntimeConfig);
12488        let QueryResponse::RuntimeConfigList(entries) = response else {
12489            panic!("expected runtime config list");
12490        };
12491        let keys: Vec<String> = entries.into_iter().map(|entry| entry.key).collect();
12492        assert!(keys.contains(&"tcp_client.uplink.connect_timeout_secs".to_string()));
12493        assert!(keys.contains(&"tcp_client.uplink.reconnect_wait_secs".to_string()));
12494        assert!(keys.contains(&"tcp_client.uplink.max_reconnect_tries".to_string()));
12495    }
12496
12497    #[cfg(feature = "iface-tcp")]
12498    #[test]
12499    fn runtime_config_sets_tcp_client_values() {
12500        let mut driver = new_test_driver();
12501        register_test_tcp_client(&mut driver, "uplink");
12502
12503        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12504            key: "tcp_client.uplink.connect_timeout_secs".into(),
12505            value: RuntimeConfigValue::Float(2.5),
12506        });
12507        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12508            panic!("expected runtime config set success");
12509        };
12510        assert_eq!(entry.value, RuntimeConfigValue::Float(2.5));
12511
12512        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12513            key: "tcp_client.uplink.max_reconnect_tries".into(),
12514            value: RuntimeConfigValue::Int(0),
12515        });
12516        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12517            panic!("expected runtime config set success");
12518        };
12519        assert_eq!(entry.value, RuntimeConfigValue::Int(0));
12520
12521        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
12522            key: "tcp_client.uplink.connect_timeout_secs".into(),
12523        });
12524        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
12525            panic!("expected runtime config reset success");
12526        };
12527        assert_eq!(entry.value, RuntimeConfigValue::Float(5.0));
12528    }
12529
12530    #[cfg(feature = "iface-udp")]
12531    #[test]
12532    fn runtime_config_lists_udp_keys() {
12533        let mut driver = new_test_driver();
12534        register_test_udp(&mut driver, "lan");
12535        let response = driver.handle_query(QueryRequest::ListRuntimeConfig);
12536        let QueryResponse::RuntimeConfigList(entries) = response else {
12537            panic!("expected runtime config list");
12538        };
12539        let keys: Vec<String> = entries.into_iter().map(|entry| entry.key).collect();
12540        assert!(keys.contains(&"udp.lan.forward_ip".to_string()));
12541        assert!(keys.contains(&"udp.lan.forward_port".to_string()));
12542    }
12543
12544    #[cfg(feature = "iface-udp")]
12545    #[test]
12546    fn runtime_config_sets_udp_values() {
12547        let mut driver = new_test_driver();
12548        register_test_udp(&mut driver, "lan");
12549
12550        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12551            key: "udp.lan.forward_ip".into(),
12552            value: RuntimeConfigValue::String("192.168.1.10".into()),
12553        });
12554        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12555            panic!("expected set ok");
12556        };
12557        assert_eq!(
12558            entry.value,
12559            RuntimeConfigValue::String("192.168.1.10".into())
12560        );
12561
12562        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12563            key: "udp.lan.forward_port".into(),
12564            value: RuntimeConfigValue::Null,
12565        });
12566        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12567            panic!("expected set ok");
12568        };
12569        assert_eq!(entry.value, RuntimeConfigValue::Null);
12570
12571        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
12572            key: "udp.lan.forward_port".into(),
12573        });
12574        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
12575            panic!("expected reset ok");
12576        };
12577        assert_eq!(entry.value, RuntimeConfigValue::Int(4242));
12578    }
12579
12580    #[cfg(feature = "iface-auto")]
12581    #[test]
12582    fn runtime_config_lists_auto_keys() {
12583        let mut driver = new_test_driver();
12584        register_test_auto(&mut driver, "lan");
12585        let response = driver.handle_query(QueryRequest::ListRuntimeConfig);
12586        let QueryResponse::RuntimeConfigList(entries) = response else {
12587            panic!("expected runtime config list");
12588        };
12589        let keys: Vec<String> = entries.into_iter().map(|entry| entry.key).collect();
12590        assert!(keys.contains(&"auto.lan.announce_interval_secs".to_string()));
12591        assert!(keys.contains(&"auto.lan.peer_timeout_secs".to_string()));
12592        assert!(keys.contains(&"auto.lan.peer_job_interval_secs".to_string()));
12593    }
12594
12595    #[cfg(feature = "iface-auto")]
12596    #[test]
12597    fn runtime_config_sets_auto_values() {
12598        let mut driver = new_test_driver();
12599        register_test_auto(&mut driver, "lan");
12600
12601        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12602            key: "auto.lan.announce_interval_secs".into(),
12603            value: RuntimeConfigValue::Float(2.5),
12604        });
12605        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12606            panic!("expected set ok");
12607        };
12608        assert_eq!(entry.value, RuntimeConfigValue::Float(2.5));
12609
12610        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12611            key: "auto.lan.peer_timeout_secs".into(),
12612            value: RuntimeConfigValue::Float(30.0),
12613        });
12614        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12615            panic!("expected set ok");
12616        };
12617        assert_eq!(entry.value, RuntimeConfigValue::Float(30.0));
12618
12619        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
12620            key: "auto.lan.peer_job_interval_secs".into(),
12621        });
12622        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
12623            panic!("expected reset ok");
12624        };
12625        assert_eq!(entry.value, RuntimeConfigValue::Float(4.0));
12626    }
12627
12628    #[cfg(feature = "iface-i2p")]
12629    #[test]
12630    fn runtime_config_lists_i2p_keys() {
12631        let mut driver = new_test_driver();
12632        register_test_i2p(&mut driver, "anon");
12633        let response = driver.handle_query(QueryRequest::ListRuntimeConfig);
12634        let QueryResponse::RuntimeConfigList(entries) = response else {
12635            panic!("expected runtime config list");
12636        };
12637        let keys: Vec<String> = entries.into_iter().map(|entry| entry.key).collect();
12638        assert!(keys.contains(&"i2p.anon.reconnect_wait_secs".to_string()));
12639    }
12640
12641    #[cfg(feature = "iface-i2p")]
12642    #[test]
12643    fn runtime_config_sets_i2p_values() {
12644        let mut driver = new_test_driver();
12645        register_test_i2p(&mut driver, "anon");
12646
12647        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12648            key: "i2p.anon.reconnect_wait_secs".into(),
12649            value: RuntimeConfigValue::Float(3.5),
12650        });
12651        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12652            panic!("expected set ok");
12653        };
12654        assert_eq!(entry.value, RuntimeConfigValue::Float(3.5));
12655
12656        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
12657            key: "i2p.anon.reconnect_wait_secs".into(),
12658        });
12659        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
12660            panic!("expected reset ok");
12661        };
12662        assert_eq!(entry.value, RuntimeConfigValue::Float(15.0));
12663    }
12664
12665    #[cfg(feature = "iface-pipe")]
12666    #[test]
12667    fn runtime_config_lists_pipe_keys() {
12668        let mut driver = new_test_driver();
12669        register_test_pipe(&mut driver, "worker");
12670        let response = driver.handle_query(QueryRequest::ListRuntimeConfig);
12671        let QueryResponse::RuntimeConfigList(entries) = response else {
12672            panic!("expected runtime config list");
12673        };
12674        let keys: Vec<String> = entries.into_iter().map(|entry| entry.key).collect();
12675        assert!(keys.contains(&"pipe.worker.respawn_delay_secs".to_string()));
12676    }
12677
12678    #[cfg(feature = "iface-pipe")]
12679    #[test]
12680    fn runtime_config_sets_pipe_values() {
12681        let mut driver = new_test_driver();
12682        register_test_pipe(&mut driver, "worker");
12683
12684        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12685            key: "pipe.worker.respawn_delay_secs".into(),
12686            value: RuntimeConfigValue::Float(2.0),
12687        });
12688        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12689            panic!("expected set ok");
12690        };
12691        assert_eq!(entry.value, RuntimeConfigValue::Float(2.0));
12692
12693        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
12694            key: "pipe.worker.respawn_delay_secs".into(),
12695        });
12696        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
12697            panic!("expected reset ok");
12698        };
12699        assert_eq!(entry.value, RuntimeConfigValue::Float(5.0));
12700    }
12701
12702    #[cfg(feature = "iface-rnode")]
12703    #[test]
12704    fn runtime_config_lists_rnode_keys() {
12705        let mut driver = new_test_driver();
12706        register_test_rnode(&mut driver, "radio");
12707        let response = driver.handle_query(QueryRequest::ListRuntimeConfig);
12708        let QueryResponse::RuntimeConfigList(entries) = response else {
12709            panic!("expected runtime config list");
12710        };
12711        let keys: Vec<String> = entries.into_iter().map(|entry| entry.key).collect();
12712        assert!(keys.contains(&"rnode.radio.frequency_hz".to_string()));
12713        assert!(keys.contains(&"rnode.radio.bandwidth_hz".to_string()));
12714        assert!(keys.contains(&"rnode.radio.txpower_dbm".to_string()));
12715        assert!(keys.contains(&"rnode.radio.spreading_factor".to_string()));
12716        assert!(keys.contains(&"rnode.radio.coding_rate".to_string()));
12717        assert!(keys.contains(&"rnode.radio.st_alock_pct".to_string()));
12718        assert!(keys.contains(&"rnode.radio.lt_alock_pct".to_string()));
12719    }
12720
12721    #[cfg(feature = "iface-rnode")]
12722    #[test]
12723    fn runtime_config_sets_rnode_values() {
12724        let mut driver = new_test_driver();
12725        register_test_rnode(&mut driver, "radio");
12726
12727        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12728            key: "rnode.radio.frequency_hz".into(),
12729            value: RuntimeConfigValue::Int(915_000_000),
12730        });
12731        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12732            panic!("expected set ok");
12733        };
12734        assert_eq!(entry.value, RuntimeConfigValue::Int(915_000_000));
12735
12736        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12737            key: "rnode.radio.st_alock_pct".into(),
12738            value: RuntimeConfigValue::Float(12.5),
12739        });
12740        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12741            panic!("expected set ok");
12742        };
12743        assert_eq!(entry.value, RuntimeConfigValue::Float(12.5));
12744
12745        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
12746            key: "rnode.radio.frequency_hz".into(),
12747        });
12748        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
12749            panic!("expected reset ok");
12750        };
12751        assert_eq!(entry.value, RuntimeConfigValue::Int(868_000_000));
12752    }
12753
12754    #[test]
12755    fn runtime_config_lists_generic_interface_keys() {
12756        let mut driver = new_test_driver();
12757        register_test_generic_interface(&mut driver, 1, "public");
12758        let response = driver.handle_query(QueryRequest::ListRuntimeConfig);
12759        let QueryResponse::RuntimeConfigList(entries) = response else {
12760            panic!("expected runtime config list");
12761        };
12762        let keys: Vec<String> = entries.into_iter().map(|entry| entry.key).collect();
12763        assert!(keys.contains(&"interface.public.enabled".to_string()));
12764        assert!(keys.contains(&"interface.public.mode".to_string()));
12765        assert!(keys.contains(&"interface.public.announce_rate_target".to_string()));
12766        assert!(keys.contains(&"interface.public.announce_rate_grace".to_string()));
12767        assert!(keys.contains(&"interface.public.announce_rate_penalty".to_string()));
12768        assert!(keys.contains(&"interface.public.announce_cap".to_string()));
12769        assert!(keys.contains(&"interface.public.ingress_control".to_string()));
12770        assert!(keys.contains(&"interface.public.ic_max_held_announces".to_string()));
12771        assert!(keys.contains(&"interface.public.ic_burst_hold".to_string()));
12772        assert!(keys.contains(&"interface.public.ic_burst_freq_new".to_string()));
12773        assert!(keys.contains(&"interface.public.ic_burst_freq".to_string()));
12774        assert!(keys.contains(&"interface.public.ic_new_time".to_string()));
12775        assert!(keys.contains(&"interface.public.ic_burst_penalty".to_string()));
12776        assert!(keys.contains(&"interface.public.ic_held_release_interval".to_string()));
12777        assert!(keys.contains(&"interface.public.ifac_netname".to_string()));
12778        assert!(keys.contains(&"interface.public.ifac_passphrase".to_string()));
12779        assert!(keys.contains(&"interface.public.ifac_size_bytes".to_string()));
12780    }
12781
12782    #[test]
12783    fn runtime_config_sets_generic_interface_values() {
12784        let mut driver = new_test_driver();
12785        register_test_generic_interface(&mut driver, 1, "public");
12786
12787        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12788            key: "interface.public.enabled".into(),
12789            value: RuntimeConfigValue::Bool(false),
12790        });
12791        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12792            panic!("expected set ok");
12793        };
12794        assert_eq!(entry.value, RuntimeConfigValue::Bool(false));
12795        assert!(!driver.interfaces.get(&InterfaceId(1)).unwrap().enabled);
12796
12797        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12798            key: "interface.public.announce_cap".into(),
12799            value: RuntimeConfigValue::Float(0.15),
12800        });
12801        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12802            panic!("expected set ok");
12803        };
12804        assert_eq!(entry.value, RuntimeConfigValue::Float(0.15));
12805        assert_eq!(
12806            driver
12807                .engine
12808                .interface_info(&InterfaceId(1))
12809                .unwrap()
12810                .announce_cap,
12811            0.15
12812        );
12813
12814        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12815            key: "interface.public.mode".into(),
12816            value: RuntimeConfigValue::String("gateway".into()),
12817        });
12818        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12819            panic!("expected set ok");
12820        };
12821        assert_eq!(entry.value, RuntimeConfigValue::String("gateway".into()));
12822
12823        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
12824            key: "interface.public.mode".into(),
12825        });
12826        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
12827            panic!("expected reset ok");
12828        };
12829        assert_eq!(entry.value, RuntimeConfigValue::String("full".into()));
12830
12831        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12832            key: "interface.public.ic_max_held_announces".into(),
12833            value: RuntimeConfigValue::Int(17),
12834        });
12835        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12836            panic!("expected set ok");
12837        };
12838        assert_eq!(entry.value, RuntimeConfigValue::Int(17));
12839        assert_eq!(
12840            driver
12841                .engine
12842                .interface_info(&InterfaceId(1))
12843                .unwrap()
12844                .ingress_control
12845                .max_held_announces,
12846            17
12847        );
12848
12849        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12850            key: "interface.public.ic_burst_hold".into(),
12851            value: RuntimeConfigValue::Float(1.5),
12852        });
12853        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12854            panic!("expected set ok");
12855        };
12856        assert_eq!(entry.value, RuntimeConfigValue::Float(1.5));
12857
12858        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12859            key: "interface.public.ic_burst_freq_new".into(),
12860            value: RuntimeConfigValue::Float(2.5),
12861        });
12862        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12863            panic!("expected set ok");
12864        };
12865        assert_eq!(entry.value, RuntimeConfigValue::Float(2.5));
12866
12867        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12868            key: "interface.public.ic_burst_freq".into(),
12869            value: RuntimeConfigValue::Float(3.5),
12870        });
12871        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12872            panic!("expected set ok");
12873        };
12874        assert_eq!(entry.value, RuntimeConfigValue::Float(3.5));
12875
12876        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12877            key: "interface.public.ic_new_time".into(),
12878            value: RuntimeConfigValue::Float(4.5),
12879        });
12880        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12881            panic!("expected set ok");
12882        };
12883        assert_eq!(entry.value, RuntimeConfigValue::Float(4.5));
12884
12885        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12886            key: "interface.public.ic_burst_penalty".into(),
12887            value: RuntimeConfigValue::Float(5.5),
12888        });
12889        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12890            panic!("expected set ok");
12891        };
12892        assert_eq!(entry.value, RuntimeConfigValue::Float(5.5));
12893
12894        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12895            key: "interface.public.ic_held_release_interval".into(),
12896            value: RuntimeConfigValue::Float(6.5),
12897        });
12898        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12899            panic!("expected set ok");
12900        };
12901        assert_eq!(entry.value, RuntimeConfigValue::Float(6.5));
12902
12903        let ingress_control = driver
12904            .engine
12905            .interface_info(&InterfaceId(1))
12906            .unwrap()
12907            .ingress_control;
12908        assert_eq!(ingress_control.burst_hold, 1.5);
12909        assert_eq!(ingress_control.burst_freq_new, 2.5);
12910        assert_eq!(ingress_control.burst_freq, 3.5);
12911        assert_eq!(ingress_control.new_time, 4.5);
12912        assert_eq!(ingress_control.burst_penalty, 5.5);
12913        assert_eq!(ingress_control.held_release_interval, 6.5);
12914
12915        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
12916            key: "interface.public.ic_max_held_announces".into(),
12917        });
12918        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
12919            panic!("expected reset ok");
12920        };
12921        assert_eq!(
12922            entry.value,
12923            RuntimeConfigValue::Int(rns_core::constants::IC_MAX_HELD_ANNOUNCES as i64)
12924        );
12925
12926        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
12927            key: "interface.public.enabled".into(),
12928        });
12929        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
12930            panic!("expected reset ok");
12931        };
12932        assert_eq!(entry.value, RuntimeConfigValue::Bool(true));
12933        assert!(driver.interfaces.get(&InterfaceId(1)).unwrap().enabled);
12934
12935        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12936            key: "interface.public.ifac_netname".into(),
12937            value: RuntimeConfigValue::String("mesh".into()),
12938        });
12939        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12940            panic!("expected set ok");
12941        };
12942        assert_eq!(entry.value, RuntimeConfigValue::String("mesh".into()));
12943        assert_eq!(
12944            driver
12945                .interfaces
12946                .get(&InterfaceId(1))
12947                .unwrap()
12948                .ifac
12949                .as_ref()
12950                .unwrap()
12951                .size,
12952            16
12953        );
12954
12955        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12956            key: "interface.public.ifac_passphrase".into(),
12957            value: RuntimeConfigValue::String("secret".into()),
12958        });
12959        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12960            panic!("expected set ok");
12961        };
12962        assert_eq!(entry.value, RuntimeConfigValue::String("<redacted>".into()));
12963
12964        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12965            key: "interface.public.ifac_size_bytes".into(),
12966            value: RuntimeConfigValue::Int(24),
12967        });
12968        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12969            panic!("expected set ok");
12970        };
12971        assert_eq!(entry.value, RuntimeConfigValue::Int(24));
12972        let ifac = driver
12973            .interfaces
12974            .get(&InterfaceId(1))
12975            .unwrap()
12976            .ifac
12977            .as_ref()
12978            .unwrap();
12979        assert_eq!(ifac.size, 24);
12980
12981        let response = driver.handle_query(QueryRequest::GetRuntimeConfig {
12982            key: "interface.public.ifac_passphrase".into(),
12983        });
12984        let QueryResponse::RuntimeConfigEntry(Some(entry)) = response else {
12985            panic!("expected runtime config entry");
12986        };
12987        assert_eq!(entry.value, RuntimeConfigValue::String("<redacted>".into()));
12988
12989        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
12990            key: "interface.public.ifac_netname".into(),
12991        });
12992        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
12993            panic!("expected reset ok");
12994        };
12995        assert_eq!(entry.value, RuntimeConfigValue::Null);
12996        assert!(driver
12997            .interfaces
12998            .get(&InterfaceId(1))
12999            .unwrap()
13000            .ifac
13001            .is_some());
13002
13003        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
13004            key: "interface.public.ifac_passphrase".into(),
13005        });
13006        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
13007            panic!("expected reset ok");
13008        };
13009        assert_eq!(entry.value, RuntimeConfigValue::Null);
13010        assert!(driver
13011            .interfaces
13012            .get(&InterfaceId(1))
13013            .unwrap()
13014            .ifac
13015            .is_none());
13016    }
13017
13018    #[cfg(feature = "rns-hooks")]
13019    #[test]
13020    fn runtime_config_sets_provider_bridge_values() {
13021        let mut driver = new_test_driver();
13022
13023        let dir = tempfile::tempdir().unwrap();
13024        let socket_path = dir.path().join("provider.sock");
13025        let bridge = crate::provider_bridge::ProviderBridge::start(
13026            crate::provider_bridge::ProviderBridgeConfig {
13027                enabled: true,
13028                socket_path,
13029                queue_max_events: 1024,
13030                queue_max_bytes: 1024 * 1024,
13031                ..Default::default()
13032            },
13033        )
13034        .unwrap();
13035        driver.runtime_config_defaults.provider_queue_max_events = 1024;
13036        driver.runtime_config_defaults.provider_queue_max_bytes = 1024 * 1024;
13037        driver.provider_bridge = Some(bridge);
13038
13039        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
13040            key: "provider.queue_max_events".into(),
13041            value: RuntimeConfigValue::Int(4096),
13042        });
13043        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
13044            panic!("expected set ok");
13045        };
13046        assert_eq!(entry.value, RuntimeConfigValue::Int(4096));
13047        assert_eq!(entry.source, RuntimeConfigSource::RuntimeOverride,);
13048
13049        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
13050            key: "provider.queue_max_bytes".into(),
13051            value: RuntimeConfigValue::Int(2 * 1024 * 1024),
13052        });
13053        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
13054            panic!("expected set ok");
13055        };
13056        assert_eq!(entry.value, RuntimeConfigValue::Int(2 * 1024 * 1024));
13057
13058        // Reject zero values
13059        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
13060            key: "provider.queue_max_events".into(),
13061            value: RuntimeConfigValue::Int(0),
13062        });
13063        assert!(matches!(response, QueryResponse::RuntimeConfigSet(Err(_))));
13064
13065        // Reset
13066        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
13067            key: "provider.queue_max_events".into(),
13068        });
13069        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
13070            panic!("expected reset ok");
13071        };
13072        assert_eq!(entry.value, RuntimeConfigValue::Int(1024));
13073        assert_eq!(entry.source, RuntimeConfigSource::Startup);
13074
13075        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
13076            key: "provider.queue_max_bytes".into(),
13077        });
13078        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
13079            panic!("expected reset ok");
13080        };
13081        assert_eq!(entry.value, RuntimeConfigValue::Int(1024 * 1024));
13082    }
13083
13084    #[test]
13085    fn disabled_interface_drops_ingress_and_egress() {
13086        let (tx, rx) = event::channel();
13087        let (cbs, _, _, _, _, _) = MockCallbacks::new();
13088        let mut driver = Driver::new(
13089            TransportConfig {
13090                transport_enabled: false,
13091                identity_hash: None,
13092                prefer_shorter_path: false,
13093                max_paths_per_destination: 1,
13094                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
13095                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
13096                max_path_destinations: usize::MAX,
13097                max_tunnel_destinations_total: usize::MAX,
13098                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
13099                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
13100                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
13101                announce_sig_cache_enabled: true,
13102                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
13103                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
13104                announce_queue_max_entries: 256,
13105                announce_queue_max_interfaces: 1024,
13106            },
13107            rx,
13108            tx.clone(),
13109            Box::new(cbs),
13110        );
13111        let info = make_interface_info(1);
13112        driver.register_interface_runtime_defaults(&info);
13113        driver.engine.register_interface(info.clone());
13114        let (writer, sent) = MockWriter::new();
13115        driver.interfaces.insert(
13116            InterfaceId(1),
13117            InterfaceEntry {
13118                id: InterfaceId(1),
13119                info,
13120                writer: Box::new(writer),
13121                async_writer_metrics: None,
13122                enabled: false,
13123                online: true,
13124                dynamic: false,
13125                ifac: None,
13126                stats: InterfaceStats::default(),
13127                interface_type: String::new(),
13128                send_retry_at: None,
13129                send_retry_backoff: Duration::ZERO,
13130            },
13131        );
13132
13133        driver.dispatch_all(vec![TransportAction::SendOnInterface {
13134            interface: InterfaceId(1),
13135            raw: vec![0x00, 0x01, 0x42],
13136        }]);
13137        assert!(sent.lock().unwrap().is_empty());
13138
13139        tx.send(Event::Frame {
13140            interface_id: InterfaceId(1),
13141            data: vec![0x00, 0x01, 0x42],
13142        })
13143        .unwrap();
13144        tx.send(Event::Shutdown).unwrap();
13145        driver.run();
13146
13147        let entry = driver.interfaces.get(&InterfaceId(1)).unwrap();
13148        assert_eq!(entry.stats.rxb, 0);
13149        assert_eq!(entry.stats.rx_packets, 0);
13150    }
13151
13152    #[test]
13153    fn management_announces_not_emitted_when_disabled() {
13154        let (tx, rx) = event::channel();
13155        let (cbs, _, _, _, _, _) = MockCallbacks::new();
13156        let identity = Identity::new(&mut OsRng);
13157        let identity_hash = *identity.hash();
13158        let mut driver = Driver::new(
13159            TransportConfig {
13160                transport_enabled: true,
13161                identity_hash: Some(identity_hash),
13162                prefer_shorter_path: false,
13163                max_paths_per_destination: 1,
13164                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
13165                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
13166                max_path_destinations: usize::MAX,
13167                max_tunnel_destinations_total: usize::MAX,
13168                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
13169                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
13170                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
13171                announce_sig_cache_enabled: true,
13172                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
13173                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
13174                announce_queue_max_entries: 256,
13175                announce_queue_max_interfaces: 1024,
13176            },
13177            rx,
13178            tx.clone(),
13179            Box::new(cbs),
13180        );
13181
13182        let info = make_interface_info(1);
13183        driver.engine.register_interface(info.clone());
13184        let (writer, sent) = MockWriter::new();
13185        driver
13186            .interfaces
13187            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
13188
13189        // Management announces disabled (default)
13190        driver.transport_identity = Some(identity);
13191        driver.started = time::now() - 10.0;
13192
13193        tx.send(Event::Tick).unwrap();
13194        tx.send(Event::Shutdown).unwrap();
13195        driver.run();
13196
13197        // Should NOT have sent any packets
13198        let sent_packets = sent.lock().unwrap();
13199        assert!(
13200            sent_packets.is_empty(),
13201            "No announces should be sent when management is disabled"
13202        );
13203    }
13204
13205    #[test]
13206    fn management_announces_not_emitted_before_delay() {
13207        let (tx, rx) = event::channel();
13208        let (cbs, _, _, _, _, _) = MockCallbacks::new();
13209        let identity = Identity::new(&mut OsRng);
13210        let identity_hash = *identity.hash();
13211        let mut driver = Driver::new(
13212            TransportConfig {
13213                transport_enabled: true,
13214                identity_hash: Some(identity_hash),
13215                prefer_shorter_path: false,
13216                max_paths_per_destination: 1,
13217                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
13218                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
13219                max_path_destinations: usize::MAX,
13220                max_tunnel_destinations_total: usize::MAX,
13221                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
13222                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
13223                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
13224                announce_sig_cache_enabled: true,
13225                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
13226                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
13227                announce_queue_max_entries: 256,
13228                announce_queue_max_interfaces: 1024,
13229            },
13230            rx,
13231            tx.clone(),
13232            Box::new(cbs),
13233        );
13234
13235        let info = make_interface_info(1);
13236        driver.engine.register_interface(info.clone());
13237        let (writer, sent) = MockWriter::new();
13238        driver
13239            .interfaces
13240            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
13241
13242        driver.management_config.enable_remote_management = true;
13243        driver.transport_identity = Some(identity);
13244        // Started just now - delay hasn't passed
13245        driver.started = time::now();
13246
13247        tx.send(Event::Tick).unwrap();
13248        tx.send(Event::Shutdown).unwrap();
13249        driver.run();
13250
13251        let sent_packets = sent.lock().unwrap();
13252        assert!(sent_packets.is_empty(), "No announces before startup delay");
13253    }
13254
13255    // =========================================================================
13256    // Phase 9c: Announce + Discovery tests
13257    // =========================================================================
13258
13259    #[test]
13260    fn announce_received_populates_known_destinations() {
13261        let (tx, rx) = event::channel();
13262        let (cbs, _, _, _, _, _) = MockCallbacks::new();
13263        let mut driver = Driver::new(
13264            TransportConfig {
13265                transport_enabled: false,
13266                identity_hash: None,
13267                prefer_shorter_path: false,
13268                max_paths_per_destination: 1,
13269                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
13270                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
13271                max_path_destinations: usize::MAX,
13272                max_tunnel_destinations_total: usize::MAX,
13273                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
13274                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
13275                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
13276                announce_sig_cache_enabled: true,
13277                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
13278                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
13279                announce_queue_max_entries: 256,
13280                announce_queue_max_interfaces: 1024,
13281            },
13282            rx,
13283            tx.clone(),
13284            Box::new(cbs),
13285        );
13286        let info = make_interface_info(1);
13287        driver.engine.register_interface(info);
13288        let (writer, _sent) = MockWriter::new();
13289        driver
13290            .interfaces
13291            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
13292
13293        let identity = Identity::new(&mut OsRng);
13294        let announce_raw = build_announce_packet(&identity);
13295
13296        let dest_hash =
13297            rns_core::destination::destination_hash("test", &["app"], Some(identity.hash()));
13298
13299        tx.send(Event::Frame {
13300            interface_id: InterfaceId(1),
13301            data: announce_raw,
13302        })
13303        .unwrap();
13304        tx.send(Event::Shutdown).unwrap();
13305        driver.run();
13306
13307        // known_destinations should be populated
13308        assert!(driver.known_destinations.contains_key(&dest_hash));
13309        let recalled = &driver.known_destinations[&dest_hash];
13310        assert_eq!(recalled.announced.dest_hash.0, dest_hash);
13311        assert_eq!(recalled.announced.identity_hash.0, *identity.hash());
13312        assert_eq!(
13313            &recalled.announced.public_key,
13314            &identity.get_public_key().unwrap()
13315        );
13316        assert_eq!(recalled.announced.hops, 1);
13317    }
13318
13319    #[test]
13320    fn known_destinations_cleanup_respects_ttl() {
13321        let (tx, rx) = event::channel();
13322        let (cbs, _, _, _, _, _) = MockCallbacks::new();
13323        let mut driver = Driver::new(
13324            TransportConfig {
13325                transport_enabled: false,
13326                identity_hash: None,
13327                prefer_shorter_path: false,
13328                max_paths_per_destination: 1,
13329                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
13330                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
13331                max_path_destinations: usize::MAX,
13332                max_tunnel_destinations_total: usize::MAX,
13333                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
13334                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
13335                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
13336                announce_sig_cache_enabled: true,
13337                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
13338                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
13339                announce_queue_max_entries: 256,
13340                announce_queue_max_interfaces: 1024,
13341            },
13342            rx,
13343            tx.clone(),
13344            Box::new(cbs),
13345        );
13346
13347        driver.known_destinations_ttl = 10.0;
13348        driver.cache_cleanup_counter = 3599;
13349
13350        let stale_dest = [0x11; 16];
13351        let fresh_dest = [0x22; 16];
13352        driver.known_destinations.insert(
13353            stale_dest,
13354            KnownDestinationState {
13355                announced: crate::destination::AnnouncedIdentity {
13356                    dest_hash: rns_core::types::DestHash(stale_dest),
13357                    identity_hash: rns_core::types::IdentityHash([0x33; 16]),
13358                    public_key: [0x44; 64],
13359                    app_data: None,
13360                    hops: 1,
13361                    received_at: time::now() - 20.0,
13362                    receiving_interface: InterfaceId(1),
13363                },
13364                was_used: false,
13365                last_used_at: None,
13366                retained: false,
13367            },
13368        );
13369        driver.known_destinations.insert(
13370            fresh_dest,
13371            KnownDestinationState {
13372                announced: crate::destination::AnnouncedIdentity {
13373                    dest_hash: rns_core::types::DestHash(fresh_dest),
13374                    identity_hash: rns_core::types::IdentityHash([0x55; 16]),
13375                    public_key: [0x66; 64],
13376                    app_data: None,
13377                    hops: 1,
13378                    received_at: time::now() - 5.0,
13379                    receiving_interface: InterfaceId(1),
13380                },
13381                was_used: false,
13382                last_used_at: None,
13383                retained: false,
13384            },
13385        );
13386
13387        tx.send(Event::Tick).unwrap();
13388        tx.send(Event::Shutdown).unwrap();
13389        driver.run();
13390
13391        assert!(!driver.known_destinations.contains_key(&stale_dest));
13392        assert!(driver.known_destinations.contains_key(&fresh_dest));
13393    }
13394
13395    #[test]
13396    fn known_destinations_cap_prefers_evicting_oldest_non_active_non_local() {
13397        let mut driver = new_test_driver();
13398        driver.known_destinations_max_entries = 2;
13399        driver.engine.register_interface(make_interface_info(1));
13400
13401        let active_dest = [0x11; 16];
13402        let evictable_dest = [0x22; 16];
13403        let new_dest = [0x33; 16];
13404
13405        driver.engine.inject_path(
13406            active_dest,
13407            PathEntry {
13408                timestamp: 100.0,
13409                next_hop: [0x44; 16],
13410                hops: 1,
13411                expires: 1000.0,
13412                random_blobs: Vec::new(),
13413                receiving_interface: InterfaceId(1),
13414                packet_hash: [0x55; 32],
13415                announce_raw: None,
13416            },
13417        );
13418
13419        driver.upsert_known_destination(
13420            active_dest,
13421            make_announced_identity(active_dest, 10.0, InterfaceId(1)),
13422        );
13423        driver.upsert_known_destination(
13424            evictable_dest,
13425            make_announced_identity(evictable_dest, 20.0, InterfaceId(1)),
13426        );
13427        driver.upsert_known_destination(
13428            new_dest,
13429            make_announced_identity(new_dest, 30.0, InterfaceId(1)),
13430        );
13431
13432        assert!(driver.known_destinations.contains_key(&active_dest));
13433        assert!(!driver.known_destinations.contains_key(&evictable_dest));
13434        assert!(driver.known_destinations.contains_key(&new_dest));
13435        assert_eq!(driver.known_destinations_cap_evict_count, 1);
13436    }
13437
13438    #[test]
13439    fn known_destinations_cap_falls_back_to_oldest_overall_when_all_protected() {
13440        let mut driver = new_test_driver();
13441        driver.known_destinations_max_entries = 2;
13442
13443        let local_oldest = [0x41; 16];
13444        let local_newer = [0x42; 16];
13445        let new_dest = [0x43; 16];
13446        driver
13447            .local_destinations
13448            .insert(local_oldest, rns_core::constants::DESTINATION_SINGLE);
13449        driver
13450            .local_destinations
13451            .insert(local_newer, rns_core::constants::DESTINATION_SINGLE);
13452
13453        driver.upsert_known_destination(
13454            local_oldest,
13455            make_announced_identity(local_oldest, 10.0, InterfaceId(1)),
13456        );
13457        driver.upsert_known_destination(
13458            local_newer,
13459            make_announced_identity(local_newer, 20.0, InterfaceId(1)),
13460        );
13461        driver.upsert_known_destination(
13462            new_dest,
13463            make_announced_identity(new_dest, 30.0, InterfaceId(1)),
13464        );
13465
13466        assert!(!driver.known_destinations.contains_key(&local_oldest));
13467        assert!(driver.known_destinations.contains_key(&local_newer));
13468        assert!(driver.known_destinations.contains_key(&new_dest));
13469        assert_eq!(driver.known_destinations_cap_evict_count, 1);
13470    }
13471
13472    #[test]
13473    fn known_destinations_cap_update_existing_entry_does_not_evict() {
13474        let mut driver = new_test_driver();
13475        driver.known_destinations_max_entries = 1;
13476
13477        let dest = [0x61; 16];
13478        driver.upsert_known_destination(dest, make_announced_identity(dest, 10.0, InterfaceId(1)));
13479        driver.upsert_known_destination(dest, make_announced_identity(dest, 20.0, InterfaceId(2)));
13480
13481        assert_eq!(driver.known_destinations.len(), 1);
13482        assert_eq!(
13483            driver.known_destinations[&dest]
13484                .announced
13485                .receiving_interface,
13486            InterfaceId(2)
13487        );
13488        assert_eq!(driver.known_destinations_cap_evict_count, 0);
13489    }
13490
13491    #[test]
13492    fn known_destinations_cleanup_enforces_cap() {
13493        let (tx, rx) = event::channel();
13494        let (cbs, _, _, _, _, _) = MockCallbacks::new();
13495        let mut driver = Driver::new(
13496            TransportConfig {
13497                transport_enabled: false,
13498                identity_hash: None,
13499                prefer_shorter_path: false,
13500                max_paths_per_destination: 1,
13501                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
13502                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
13503                max_path_destinations: usize::MAX,
13504                max_tunnel_destinations_total: usize::MAX,
13505                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
13506                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
13507                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
13508                announce_sig_cache_enabled: true,
13509                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
13510                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
13511                announce_queue_max_entries: 256,
13512                announce_queue_max_interfaces: 1024,
13513            },
13514            rx,
13515            tx.clone(),
13516            Box::new(cbs),
13517        );
13518
13519        driver.known_destinations_ttl = 1000.0;
13520        driver.known_destinations_max_entries = 2;
13521        driver.cache_cleanup_counter = 3599;
13522        let now = time::now();
13523        driver.known_destinations.insert(
13524            [0x71; 16],
13525            make_known_destination_state([0x71; 16], now - 30.0, InterfaceId(1)),
13526        );
13527        driver.known_destinations.insert(
13528            [0x72; 16],
13529            make_known_destination_state([0x72; 16], now - 20.0, InterfaceId(1)),
13530        );
13531        driver.known_destinations.insert(
13532            [0x73; 16],
13533            make_known_destination_state([0x73; 16], now - 10.0, InterfaceId(1)),
13534        );
13535
13536        tx.send(Event::Tick).unwrap();
13537        tx.send(Event::Shutdown).unwrap();
13538        driver.run();
13539
13540        assert_eq!(driver.known_destinations.len(), 2);
13541        assert!(!driver.known_destinations.contains_key(&[0x71; 16]));
13542        assert_eq!(driver.known_destinations_cap_evict_count, 1);
13543    }
13544
13545    #[test]
13546    fn recall_identity_marks_known_destination_used() {
13547        let mut driver = new_test_driver();
13548        let dest = [0x81; 16];
13549        driver.upsert_known_destination(dest, make_announced_identity(dest, 10.0, InterfaceId(1)));
13550
13551        let response = driver.handle_query_mut(QueryRequest::RecallIdentity { dest_hash: dest });
13552        assert!(matches!(response, QueryResponse::RecallIdentity(Some(_))));
13553
13554        let entry = driver.known_destinations.get(&dest).unwrap();
13555        assert!(entry.was_used);
13556        assert!(entry.last_used_at.is_some());
13557    }
13558
13559    #[test]
13560    fn retained_known_destination_survives_cleanup() {
13561        let (tx, rx) = event::channel();
13562        let (cbs, _, _, _, _, _) = MockCallbacks::new();
13563        let mut driver = Driver::new(
13564            TransportConfig {
13565                transport_enabled: false,
13566                identity_hash: None,
13567                prefer_shorter_path: false,
13568                max_paths_per_destination: 1,
13569                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
13570                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
13571                max_path_destinations: usize::MAX,
13572                max_tunnel_destinations_total: usize::MAX,
13573                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
13574                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
13575                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
13576                announce_sig_cache_enabled: true,
13577                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
13578                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
13579                announce_queue_max_entries: 256,
13580                announce_queue_max_interfaces: 1024,
13581            },
13582            rx,
13583            tx.clone(),
13584            Box::new(cbs),
13585        );
13586        driver.known_destinations_ttl = 10.0;
13587        driver.cache_cleanup_counter = 3599;
13588
13589        let dest = [0x82; 16];
13590        driver.upsert_known_destination(
13591            dest,
13592            make_announced_identity(dest, time::now() - 30.0, InterfaceId(1)),
13593        );
13594        assert!(driver.retain_known_destination(&dest));
13595
13596        tx.send(Event::Tick).unwrap();
13597        tx.send(Event::Shutdown).unwrap();
13598        driver.run();
13599
13600        assert!(driver.known_destinations.contains_key(&dest));
13601    }
13602
13603    #[test]
13604    fn used_known_destination_cleanup_uses_last_used_time() {
13605        let (tx, rx) = event::channel();
13606        let (cbs, _, _, _, _, _) = MockCallbacks::new();
13607        let mut driver = Driver::new(
13608            TransportConfig {
13609                transport_enabled: false,
13610                identity_hash: None,
13611                prefer_shorter_path: false,
13612                max_paths_per_destination: 1,
13613                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
13614                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
13615                max_path_destinations: usize::MAX,
13616                max_tunnel_destinations_total: usize::MAX,
13617                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
13618                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
13619                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
13620                announce_sig_cache_enabled: true,
13621                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
13622                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
13623                announce_queue_max_entries: 256,
13624                announce_queue_max_interfaces: 1024,
13625            },
13626            rx,
13627            tx.clone(),
13628            Box::new(cbs),
13629        );
13630        driver.known_destinations_ttl = 10.0;
13631        driver.cache_cleanup_counter = 3599;
13632
13633        let dest = [0x83; 16];
13634        driver.known_destinations.insert(
13635            dest,
13636            KnownDestinationState {
13637                announced: make_announced_identity(dest, time::now() - 50.0, InterfaceId(1)),
13638                was_used: true,
13639                last_used_at: Some(time::now() - 5.0),
13640                retained: false,
13641            },
13642        );
13643
13644        tx.send(Event::Tick).unwrap();
13645        tx.send(Event::Shutdown).unwrap();
13646        driver.run();
13647
13648        assert!(driver.known_destinations.contains_key(&dest));
13649    }
13650
13651    #[test]
13652    fn query_has_path() {
13653        let (tx, rx) = event::channel();
13654        let (cbs, _, _, _, _, _) = MockCallbacks::new();
13655        let mut driver = Driver::new(
13656            TransportConfig {
13657                transport_enabled: false,
13658                identity_hash: None,
13659                prefer_shorter_path: false,
13660                max_paths_per_destination: 1,
13661                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
13662                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
13663                max_path_destinations: usize::MAX,
13664                max_tunnel_destinations_total: usize::MAX,
13665                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
13666                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
13667                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
13668                announce_sig_cache_enabled: true,
13669                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
13670                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
13671                announce_queue_max_entries: 256,
13672                announce_queue_max_interfaces: 1024,
13673            },
13674            rx,
13675            tx.clone(),
13676            Box::new(cbs),
13677        );
13678        let info = make_interface_info(1);
13679        driver.engine.register_interface(info);
13680        let (writer, _sent) = MockWriter::new();
13681        driver
13682            .interfaces
13683            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
13684
13685        // No path yet
13686        let (resp_tx, resp_rx) = mpsc::channel();
13687        tx.send(Event::Query(
13688            QueryRequest::HasPath {
13689                dest_hash: [0xAA; 16],
13690            },
13691            resp_tx,
13692        ))
13693        .unwrap();
13694
13695        // Feed an announce to create a path
13696        let identity = Identity::new(&mut OsRng);
13697        let announce_raw = build_announce_packet(&identity);
13698        let dest_hash =
13699            rns_core::destination::destination_hash("test", &["app"], Some(identity.hash()));
13700        tx.send(Event::Frame {
13701            interface_id: InterfaceId(1),
13702            data: announce_raw,
13703        })
13704        .unwrap();
13705
13706        let (resp_tx2, resp_rx2) = mpsc::channel();
13707        tx.send(Event::Query(QueryRequest::HasPath { dest_hash }, resp_tx2))
13708            .unwrap();
13709
13710        tx.send(Event::Shutdown).unwrap();
13711        driver.run();
13712
13713        // First query — no path
13714        match resp_rx.recv().unwrap() {
13715            QueryResponse::HasPath(false) => {}
13716            other => panic!("expected HasPath(false), got {:?}", other),
13717        }
13718
13719        // Second query — path exists
13720        match resp_rx2.recv().unwrap() {
13721            QueryResponse::HasPath(true) => {}
13722            other => panic!("expected HasPath(true), got {:?}", other),
13723        }
13724    }
13725
13726    #[test]
13727    fn query_hops_to() {
13728        let (tx, rx) = event::channel();
13729        let (cbs, _, _, _, _, _) = MockCallbacks::new();
13730        let mut driver = Driver::new(
13731            TransportConfig {
13732                transport_enabled: false,
13733                identity_hash: None,
13734                prefer_shorter_path: false,
13735                max_paths_per_destination: 1,
13736                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
13737                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
13738                max_path_destinations: usize::MAX,
13739                max_tunnel_destinations_total: usize::MAX,
13740                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
13741                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
13742                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
13743                announce_sig_cache_enabled: true,
13744                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
13745                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
13746                announce_queue_max_entries: 256,
13747                announce_queue_max_interfaces: 1024,
13748            },
13749            rx,
13750            tx.clone(),
13751            Box::new(cbs),
13752        );
13753        let info = make_interface_info(1);
13754        driver.engine.register_interface(info);
13755        let (writer, _sent) = MockWriter::new();
13756        driver
13757            .interfaces
13758            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
13759
13760        // Feed an announce
13761        let identity = Identity::new(&mut OsRng);
13762        let announce_raw = build_announce_packet(&identity);
13763        let dest_hash =
13764            rns_core::destination::destination_hash("test", &["app"], Some(identity.hash()));
13765
13766        tx.send(Event::Frame {
13767            interface_id: InterfaceId(1),
13768            data: announce_raw,
13769        })
13770        .unwrap();
13771
13772        let (resp_tx, resp_rx) = mpsc::channel();
13773        tx.send(Event::Query(QueryRequest::HopsTo { dest_hash }, resp_tx))
13774            .unwrap();
13775        tx.send(Event::Shutdown).unwrap();
13776        driver.run();
13777
13778        match resp_rx.recv().unwrap() {
13779            QueryResponse::HopsTo(Some(1)) => {}
13780            other => panic!("expected HopsTo(Some(1)), got {:?}", other),
13781        }
13782    }
13783
13784    #[test]
13785    fn query_recall_identity() {
13786        let (tx, rx) = event::channel();
13787        let (cbs, _, _, _, _, _) = MockCallbacks::new();
13788        let mut driver = Driver::new(
13789            TransportConfig {
13790                transport_enabled: false,
13791                identity_hash: None,
13792                prefer_shorter_path: false,
13793                max_paths_per_destination: 1,
13794                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
13795                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
13796                max_path_destinations: usize::MAX,
13797                max_tunnel_destinations_total: usize::MAX,
13798                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
13799                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
13800                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
13801                announce_sig_cache_enabled: true,
13802                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
13803                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
13804                announce_queue_max_entries: 256,
13805                announce_queue_max_interfaces: 1024,
13806            },
13807            rx,
13808            tx.clone(),
13809            Box::new(cbs),
13810        );
13811        let info = make_interface_info(1);
13812        driver.engine.register_interface(info);
13813        let (writer, _sent) = MockWriter::new();
13814        driver
13815            .interfaces
13816            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
13817
13818        let identity = Identity::new(&mut OsRng);
13819        let announce_raw = build_announce_packet(&identity);
13820        let dest_hash =
13821            rns_core::destination::destination_hash("test", &["app"], Some(identity.hash()));
13822
13823        tx.send(Event::Frame {
13824            interface_id: InterfaceId(1),
13825            data: announce_raw,
13826        })
13827        .unwrap();
13828
13829        // Recall identity
13830        let (resp_tx, resp_rx) = mpsc::channel();
13831        tx.send(Event::Query(
13832            QueryRequest::RecallIdentity { dest_hash },
13833            resp_tx,
13834        ))
13835        .unwrap();
13836
13837        // Also recall unknown destination
13838        let (resp_tx2, resp_rx2) = mpsc::channel();
13839        tx.send(Event::Query(
13840            QueryRequest::RecallIdentity {
13841                dest_hash: [0xFF; 16],
13842            },
13843            resp_tx2,
13844        ))
13845        .unwrap();
13846
13847        tx.send(Event::Shutdown).unwrap();
13848        driver.run();
13849
13850        match resp_rx.recv().unwrap() {
13851            QueryResponse::RecallIdentity(Some(recalled)) => {
13852                assert_eq!(recalled.dest_hash.0, dest_hash);
13853                assert_eq!(recalled.identity_hash.0, *identity.hash());
13854                assert_eq!(recalled.public_key, identity.get_public_key().unwrap());
13855                assert_eq!(recalled.hops, 1);
13856            }
13857            other => panic!("expected RecallIdentity(Some(..)), got {:?}", other),
13858        }
13859
13860        match resp_rx2.recv().unwrap() {
13861            QueryResponse::RecallIdentity(None) => {}
13862            other => panic!("expected RecallIdentity(None), got {:?}", other),
13863        }
13864    }
13865
13866    #[test]
13867    fn request_path_sends_packet() {
13868        let (tx, rx) = event::channel();
13869        let (cbs, _, _, _, _, _) = MockCallbacks::new();
13870        let mut driver = Driver::new(
13871            TransportConfig {
13872                transport_enabled: false,
13873                identity_hash: None,
13874                prefer_shorter_path: false,
13875                max_paths_per_destination: 1,
13876                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
13877                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
13878                max_path_destinations: usize::MAX,
13879                max_tunnel_destinations_total: usize::MAX,
13880                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
13881                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
13882                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
13883                announce_sig_cache_enabled: true,
13884                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
13885                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
13886                announce_queue_max_entries: 256,
13887                announce_queue_max_interfaces: 1024,
13888            },
13889            rx,
13890            tx.clone(),
13891            Box::new(cbs),
13892        );
13893        let info = make_interface_info(1);
13894        driver.engine.register_interface(info);
13895        let (writer, sent) = MockWriter::new();
13896        driver
13897            .interfaces
13898            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
13899
13900        // Send path request
13901        tx.send(Event::RequestPath {
13902            dest_hash: [0xAA; 16],
13903        })
13904        .unwrap();
13905        tx.send(Event::Shutdown).unwrap();
13906        driver.run();
13907
13908        // Should have sent a packet on the wire (broadcast)
13909        let sent_packets = sent.lock().unwrap();
13910        assert!(
13911            !sent_packets.is_empty(),
13912            "Path request should be sent on wire"
13913        );
13914
13915        // Verify the sent packet is a DATA PLAIN BROADCAST packet
13916        let raw = &sent_packets[0];
13917        let flags = rns_core::packet::PacketFlags::unpack(raw[0] & 0x7F);
13918        assert_eq!(flags.packet_type, constants::PACKET_TYPE_DATA);
13919        assert_eq!(flags.destination_type, constants::DESTINATION_PLAIN);
13920        assert_eq!(flags.transport_type, constants::TRANSPORT_BROADCAST);
13921    }
13922
13923    #[test]
13924    fn request_path_includes_transport_id() {
13925        let (tx, rx) = event::channel();
13926        let (cbs, _, _, _, _, _) = MockCallbacks::new();
13927        let mut driver = Driver::new(
13928            TransportConfig {
13929                transport_enabled: true,
13930                identity_hash: Some([0xBB; 16]),
13931                prefer_shorter_path: false,
13932                max_paths_per_destination: 1,
13933                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
13934                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
13935                max_path_destinations: usize::MAX,
13936                max_tunnel_destinations_total: usize::MAX,
13937                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
13938                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
13939                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
13940                announce_sig_cache_enabled: true,
13941                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
13942                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
13943                announce_queue_max_entries: 256,
13944                announce_queue_max_interfaces: 1024,
13945            },
13946            rx,
13947            tx.clone(),
13948            Box::new(cbs),
13949        );
13950        let info = make_interface_info(1);
13951        driver.engine.register_interface(info);
13952        let (writer, sent) = MockWriter::new();
13953        driver
13954            .interfaces
13955            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
13956
13957        tx.send(Event::RequestPath {
13958            dest_hash: [0xAA; 16],
13959        })
13960        .unwrap();
13961        tx.send(Event::Shutdown).unwrap();
13962        driver.run();
13963
13964        let sent_packets = sent.lock().unwrap();
13965        assert!(!sent_packets.is_empty());
13966
13967        // Unpack the packet to check data length includes transport_id
13968        let raw = &sent_packets[0];
13969        if let Ok(packet) = RawPacket::unpack(raw) {
13970            // Data: dest_hash(16) + transport_id(16) + random_tag(16) = 48 bytes
13971            assert_eq!(
13972                packet.data.len(),
13973                48,
13974                "Path request data should be 48 bytes with transport_id"
13975            );
13976            assert_eq!(
13977                &packet.data[..16],
13978                &[0xAA; 16],
13979                "First 16 bytes should be dest_hash"
13980            );
13981            assert_eq!(
13982                &packet.data[16..32],
13983                &[0xBB; 16],
13984                "Next 16 bytes should be transport_id"
13985            );
13986        } else {
13987            panic!("Could not unpack sent packet");
13988        }
13989    }
13990
13991    #[test]
13992    fn path_request_dest_registered() {
13993        let (tx, rx) = event::channel();
13994        let (cbs, _, _, _, _, _) = MockCallbacks::new();
13995        let driver = Driver::new(
13996            TransportConfig {
13997                transport_enabled: false,
13998                identity_hash: None,
13999                prefer_shorter_path: false,
14000                max_paths_per_destination: 1,
14001                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14002                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14003                max_path_destinations: usize::MAX,
14004                max_tunnel_destinations_total: usize::MAX,
14005                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14006                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14007                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14008                announce_sig_cache_enabled: true,
14009                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14010                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14011                announce_queue_max_entries: 256,
14012                announce_queue_max_interfaces: 1024,
14013            },
14014            rx,
14015            tx.clone(),
14016            Box::new(cbs),
14017        );
14018
14019        // The path request dest should be registered as a local PLAIN destination
14020        let expected_dest =
14021            rns_core::destination::destination_hash("rnstransport", &["path", "request"], None);
14022        assert_eq!(driver.path_request_dest, expected_dest);
14023
14024        drop(tx);
14025    }
14026
14027    // =========================================================================
14028    // Phase 9d: send_packet + proofs tests
14029    // =========================================================================
14030
14031    #[test]
14032    fn register_proof_strategy_event() {
14033        let (tx, rx) = event::channel();
14034        let (cbs, _, _, _, _, _) = MockCallbacks::new();
14035        let mut driver = Driver::new(
14036            TransportConfig {
14037                transport_enabled: false,
14038                identity_hash: None,
14039                prefer_shorter_path: false,
14040                max_paths_per_destination: 1,
14041                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14042                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14043                max_path_destinations: usize::MAX,
14044                max_tunnel_destinations_total: usize::MAX,
14045                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14046                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14047                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14048                announce_sig_cache_enabled: true,
14049                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14050                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14051                announce_queue_max_entries: 256,
14052                announce_queue_max_interfaces: 1024,
14053            },
14054            rx,
14055            tx.clone(),
14056            Box::new(cbs),
14057        );
14058
14059        let dest = [0xAA; 16];
14060        let identity = Identity::new(&mut OsRng);
14061        let prv_key = identity.get_private_key().unwrap();
14062
14063        tx.send(Event::RegisterProofStrategy {
14064            dest_hash: dest,
14065            strategy: rns_core::types::ProofStrategy::ProveAll,
14066            signing_key: Some(prv_key),
14067        })
14068        .unwrap();
14069        tx.send(Event::Shutdown).unwrap();
14070        driver.run();
14071
14072        assert!(driver.proof_strategies.contains_key(&dest));
14073        let (strategy, ref id_opt) = driver.proof_strategies[&dest];
14074        assert_eq!(strategy, rns_core::types::ProofStrategy::ProveAll);
14075        assert!(id_opt.is_some());
14076    }
14077
14078    #[test]
14079    fn register_proof_strategy_prove_none_no_identity() {
14080        let (tx, rx) = event::channel();
14081        let (cbs, _, _, _, _, _) = MockCallbacks::new();
14082        let mut driver = Driver::new(
14083            TransportConfig {
14084                transport_enabled: false,
14085                identity_hash: None,
14086                prefer_shorter_path: false,
14087                max_paths_per_destination: 1,
14088                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14089                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14090                max_path_destinations: usize::MAX,
14091                max_tunnel_destinations_total: usize::MAX,
14092                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14093                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14094                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14095                announce_sig_cache_enabled: true,
14096                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14097                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14098                announce_queue_max_entries: 256,
14099                announce_queue_max_interfaces: 1024,
14100            },
14101            rx,
14102            tx.clone(),
14103            Box::new(cbs),
14104        );
14105
14106        let dest = [0xBB; 16];
14107        tx.send(Event::RegisterProofStrategy {
14108            dest_hash: dest,
14109            strategy: rns_core::types::ProofStrategy::ProveNone,
14110            signing_key: None,
14111        })
14112        .unwrap();
14113        tx.send(Event::Shutdown).unwrap();
14114        driver.run();
14115
14116        assert!(driver.proof_strategies.contains_key(&dest));
14117        let (strategy, ref id_opt) = driver.proof_strategies[&dest];
14118        assert_eq!(strategy, rns_core::types::ProofStrategy::ProveNone);
14119        assert!(id_opt.is_none());
14120    }
14121
14122    #[test]
14123    fn send_outbound_tracks_sent_packets() {
14124        let (tx, rx) = event::channel();
14125        let (cbs, _, _, _, _, _) = MockCallbacks::new();
14126        let mut driver = Driver::new(
14127            TransportConfig {
14128                transport_enabled: false,
14129                identity_hash: None,
14130                prefer_shorter_path: false,
14131                max_paths_per_destination: 1,
14132                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14133                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14134                max_path_destinations: usize::MAX,
14135                max_tunnel_destinations_total: usize::MAX,
14136                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14137                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14138                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14139                announce_sig_cache_enabled: true,
14140                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14141                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14142                announce_queue_max_entries: 256,
14143                announce_queue_max_interfaces: 1024,
14144            },
14145            rx,
14146            tx.clone(),
14147            Box::new(cbs),
14148        );
14149        let info = make_interface_info(1);
14150        driver.engine.register_interface(info);
14151        let (writer, _sent) = MockWriter::new();
14152        driver
14153            .interfaces
14154            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
14155
14156        // Build a DATA packet
14157        let dest = [0xCC; 16];
14158        let flags = PacketFlags {
14159            header_type: constants::HEADER_1,
14160            context_flag: constants::FLAG_UNSET,
14161            transport_type: constants::TRANSPORT_BROADCAST,
14162            destination_type: constants::DESTINATION_PLAIN,
14163            packet_type: constants::PACKET_TYPE_DATA,
14164        };
14165        let packet =
14166            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"test data").unwrap();
14167        let expected_hash = packet.packet_hash;
14168
14169        tx.send(Event::SendOutbound {
14170            raw: packet.raw,
14171            dest_type: constants::DESTINATION_PLAIN,
14172            attached_interface: None,
14173        })
14174        .unwrap();
14175        tx.send(Event::Shutdown).unwrap();
14176        driver.run();
14177
14178        // Should be tracking the sent packet
14179        assert!(driver.sent_packets.contains_key(&expected_hash));
14180        let (tracked_dest, _sent_time) = &driver.sent_packets[&expected_hash];
14181        assert_eq!(tracked_dest, &dest);
14182    }
14183
14184    #[test]
14185    fn prove_all_generates_proof_on_delivery() {
14186        let (tx, rx) = event::channel();
14187        let (cbs, _, _, deliveries, _, _) = MockCallbacks::new();
14188        let mut driver = Driver::new(
14189            TransportConfig {
14190                transport_enabled: false,
14191                identity_hash: None,
14192                prefer_shorter_path: false,
14193                max_paths_per_destination: 1,
14194                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14195                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14196                max_path_destinations: usize::MAX,
14197                max_tunnel_destinations_total: usize::MAX,
14198                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14199                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14200                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14201                announce_sig_cache_enabled: true,
14202                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14203                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14204                announce_queue_max_entries: 256,
14205                announce_queue_max_interfaces: 1024,
14206            },
14207            rx,
14208            tx.clone(),
14209            Box::new(cbs),
14210        );
14211        let info = make_interface_info(1);
14212        driver.engine.register_interface(info);
14213        let (writer, sent) = MockWriter::new();
14214        driver
14215            .interfaces
14216            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
14217
14218        // Register a destination with ProveAll
14219        let dest = [0xDD; 16];
14220        let identity = Identity::new(&mut OsRng);
14221        let prv_key = identity.get_private_key().unwrap();
14222        driver
14223            .engine
14224            .register_destination(dest, constants::DESTINATION_SINGLE);
14225        driver.proof_strategies.insert(
14226            dest,
14227            (
14228                rns_core::types::ProofStrategy::ProveAll,
14229                Some(Identity::from_private_key(&prv_key)),
14230            ),
14231        );
14232
14233        // Send a DATA packet to that destination
14234        let flags = PacketFlags {
14235            header_type: constants::HEADER_1,
14236            context_flag: constants::FLAG_UNSET,
14237            transport_type: constants::TRANSPORT_BROADCAST,
14238            destination_type: constants::DESTINATION_SINGLE,
14239            packet_type: constants::PACKET_TYPE_DATA,
14240        };
14241        let packet =
14242            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"hello").unwrap();
14243
14244        tx.send(Event::Frame {
14245            interface_id: InterfaceId(1),
14246            data: packet.raw,
14247        })
14248        .unwrap();
14249        tx.send(Event::Shutdown).unwrap();
14250        driver.run();
14251
14252        // Should have delivered the packet
14253        assert_eq!(deliveries.lock().unwrap().len(), 1);
14254
14255        // Should have sent at least one proof packet on the wire
14256        let sent_packets = sent.lock().unwrap();
14257        // The original DATA is not sent out (it was delivered locally), but a PROOF should be
14258        let has_proof = sent_packets.iter().any(|raw| {
14259            let flags = PacketFlags::unpack(raw[0] & 0x7F);
14260            flags.packet_type == constants::PACKET_TYPE_PROOF
14261        });
14262        assert!(
14263            has_proof,
14264            "ProveAll should generate a proof packet: sent {} packets",
14265            sent_packets.len()
14266        );
14267    }
14268
14269    #[test]
14270    fn prove_none_does_not_generate_proof() {
14271        let (tx, rx) = event::channel();
14272        let (cbs, _, _, deliveries, _, _) = MockCallbacks::new();
14273        let mut driver = Driver::new(
14274            TransportConfig {
14275                transport_enabled: false,
14276                identity_hash: None,
14277                prefer_shorter_path: false,
14278                max_paths_per_destination: 1,
14279                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14280                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14281                max_path_destinations: usize::MAX,
14282                max_tunnel_destinations_total: usize::MAX,
14283                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14284                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14285                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14286                announce_sig_cache_enabled: true,
14287                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14288                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14289                announce_queue_max_entries: 256,
14290                announce_queue_max_interfaces: 1024,
14291            },
14292            rx,
14293            tx.clone(),
14294            Box::new(cbs),
14295        );
14296        let info = make_interface_info(1);
14297        driver.engine.register_interface(info);
14298        let (writer, sent) = MockWriter::new();
14299        driver
14300            .interfaces
14301            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
14302
14303        // Register a destination with ProveNone
14304        let dest = [0xDD; 16];
14305        driver
14306            .engine
14307            .register_destination(dest, constants::DESTINATION_SINGLE);
14308        driver
14309            .proof_strategies
14310            .insert(dest, (rns_core::types::ProofStrategy::ProveNone, None));
14311
14312        // Send a DATA packet to that destination
14313        let flags = PacketFlags {
14314            header_type: constants::HEADER_1,
14315            context_flag: constants::FLAG_UNSET,
14316            transport_type: constants::TRANSPORT_BROADCAST,
14317            destination_type: constants::DESTINATION_SINGLE,
14318            packet_type: constants::PACKET_TYPE_DATA,
14319        };
14320        let packet =
14321            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"hello").unwrap();
14322
14323        tx.send(Event::Frame {
14324            interface_id: InterfaceId(1),
14325            data: packet.raw,
14326        })
14327        .unwrap();
14328        tx.send(Event::Shutdown).unwrap();
14329        driver.run();
14330
14331        // Should have delivered the packet
14332        assert_eq!(deliveries.lock().unwrap().len(), 1);
14333
14334        // Should NOT have sent any proof
14335        let sent_packets = sent.lock().unwrap();
14336        let has_proof = sent_packets.iter().any(|raw| {
14337            let flags = PacketFlags::unpack(raw[0] & 0x7F);
14338            flags.packet_type == constants::PACKET_TYPE_PROOF
14339        });
14340        assert!(!has_proof, "ProveNone should not generate a proof packet");
14341    }
14342
14343    #[test]
14344    fn no_proof_strategy_does_not_generate_proof() {
14345        let (tx, rx) = event::channel();
14346        let (cbs, _, _, deliveries, _, _) = MockCallbacks::new();
14347        let mut driver = Driver::new(
14348            TransportConfig {
14349                transport_enabled: false,
14350                identity_hash: None,
14351                prefer_shorter_path: false,
14352                max_paths_per_destination: 1,
14353                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14354                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14355                max_path_destinations: usize::MAX,
14356                max_tunnel_destinations_total: usize::MAX,
14357                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14358                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14359                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14360                announce_sig_cache_enabled: true,
14361                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14362                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14363                announce_queue_max_entries: 256,
14364                announce_queue_max_interfaces: 1024,
14365            },
14366            rx,
14367            tx.clone(),
14368            Box::new(cbs),
14369        );
14370        let info = make_interface_info(1);
14371        driver.engine.register_interface(info);
14372        let (writer, sent) = MockWriter::new();
14373        driver
14374            .interfaces
14375            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
14376
14377        // Register destination but NO proof strategy
14378        let dest = [0xDD; 16];
14379        driver
14380            .engine
14381            .register_destination(dest, constants::DESTINATION_SINGLE);
14382
14383        let flags = PacketFlags {
14384            header_type: constants::HEADER_1,
14385            context_flag: constants::FLAG_UNSET,
14386            transport_type: constants::TRANSPORT_BROADCAST,
14387            destination_type: constants::DESTINATION_SINGLE,
14388            packet_type: constants::PACKET_TYPE_DATA,
14389        };
14390        let packet =
14391            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"hello").unwrap();
14392
14393        tx.send(Event::Frame {
14394            interface_id: InterfaceId(1),
14395            data: packet.raw,
14396        })
14397        .unwrap();
14398        tx.send(Event::Shutdown).unwrap();
14399        driver.run();
14400
14401        assert_eq!(deliveries.lock().unwrap().len(), 1);
14402
14403        let sent_packets = sent.lock().unwrap();
14404        let has_proof = sent_packets.iter().any(|raw| {
14405            let flags = PacketFlags::unpack(raw[0] & 0x7F);
14406            flags.packet_type == constants::PACKET_TYPE_PROOF
14407        });
14408        assert!(!has_proof, "No proof strategy means no proof generated");
14409    }
14410
14411    #[test]
14412    fn prove_app_calls_callback() {
14413        let (tx, rx) = event::channel();
14414        let proof_requested = Arc::new(Mutex::new(Vec::new()));
14415        let deliveries = Arc::new(Mutex::new(Vec::new()));
14416        let cbs = MockCallbacks {
14417            announces: Arc::new(Mutex::new(Vec::new())),
14418            paths: Arc::new(Mutex::new(Vec::new())),
14419            deliveries: deliveries.clone(),
14420            iface_ups: Arc::new(Mutex::new(Vec::new())),
14421            iface_downs: Arc::new(Mutex::new(Vec::new())),
14422            link_established: Arc::new(Mutex::new(Vec::new())),
14423            link_closed: Arc::new(Mutex::new(Vec::new())),
14424            remote_identified: Arc::new(Mutex::new(Vec::new())),
14425            resources_received: Arc::new(Mutex::new(Vec::new())),
14426            resource_completed: Arc::new(Mutex::new(Vec::new())),
14427            resource_failed: Arc::new(Mutex::new(Vec::new())),
14428            channel_messages: Arc::new(Mutex::new(Vec::new())),
14429            link_data: Arc::new(Mutex::new(Vec::new())),
14430            responses: Arc::new(Mutex::new(Vec::new())),
14431            proofs: Arc::new(Mutex::new(Vec::new())),
14432            proof_requested: proof_requested.clone(),
14433        };
14434
14435        let mut driver = Driver::new(
14436            TransportConfig {
14437                transport_enabled: false,
14438                identity_hash: None,
14439                prefer_shorter_path: false,
14440                max_paths_per_destination: 1,
14441                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14442                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14443                max_path_destinations: usize::MAX,
14444                max_tunnel_destinations_total: usize::MAX,
14445                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14446                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14447                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14448                announce_sig_cache_enabled: true,
14449                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14450                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14451                announce_queue_max_entries: 256,
14452                announce_queue_max_interfaces: 1024,
14453            },
14454            rx,
14455            tx.clone(),
14456            Box::new(cbs),
14457        );
14458        let info = make_interface_info(1);
14459        driver.engine.register_interface(info);
14460        let (writer, sent) = MockWriter::new();
14461        driver
14462            .interfaces
14463            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
14464
14465        // Register dest with ProveApp
14466        let dest = [0xDD; 16];
14467        let identity = Identity::new(&mut OsRng);
14468        let prv_key = identity.get_private_key().unwrap();
14469        driver
14470            .engine
14471            .register_destination(dest, constants::DESTINATION_SINGLE);
14472        driver.proof_strategies.insert(
14473            dest,
14474            (
14475                rns_core::types::ProofStrategy::ProveApp,
14476                Some(Identity::from_private_key(&prv_key)),
14477            ),
14478        );
14479
14480        let flags = PacketFlags {
14481            header_type: constants::HEADER_1,
14482            context_flag: constants::FLAG_UNSET,
14483            transport_type: constants::TRANSPORT_BROADCAST,
14484            destination_type: constants::DESTINATION_SINGLE,
14485            packet_type: constants::PACKET_TYPE_DATA,
14486        };
14487        let packet =
14488            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"app test").unwrap();
14489
14490        tx.send(Event::Frame {
14491            interface_id: InterfaceId(1),
14492            data: packet.raw,
14493        })
14494        .unwrap();
14495        tx.send(Event::Shutdown).unwrap();
14496        driver.run();
14497
14498        // on_proof_requested should have been called
14499        let prs = proof_requested.lock().unwrap();
14500        assert_eq!(prs.len(), 1);
14501        assert_eq!(prs[0].0, DestHash(dest));
14502
14503        // Since our mock returns true, a proof should also have been sent
14504        let sent_packets = sent.lock().unwrap();
14505        let has_proof = sent_packets.iter().any(|raw| {
14506            let flags = PacketFlags::unpack(raw[0] & 0x7F);
14507            flags.packet_type == constants::PACKET_TYPE_PROOF
14508        });
14509        assert!(
14510            has_proof,
14511            "ProveApp (callback returns true) should generate a proof"
14512        );
14513    }
14514
14515    #[test]
14516    fn inbound_proof_fires_callback() {
14517        let (tx, rx) = event::channel();
14518        let proofs = Arc::new(Mutex::new(Vec::new()));
14519        let cbs = MockCallbacks {
14520            announces: Arc::new(Mutex::new(Vec::new())),
14521            paths: Arc::new(Mutex::new(Vec::new())),
14522            deliveries: Arc::new(Mutex::new(Vec::new())),
14523            iface_ups: Arc::new(Mutex::new(Vec::new())),
14524            iface_downs: Arc::new(Mutex::new(Vec::new())),
14525            link_established: Arc::new(Mutex::new(Vec::new())),
14526            link_closed: Arc::new(Mutex::new(Vec::new())),
14527            remote_identified: Arc::new(Mutex::new(Vec::new())),
14528            resources_received: Arc::new(Mutex::new(Vec::new())),
14529            resource_completed: Arc::new(Mutex::new(Vec::new())),
14530            resource_failed: Arc::new(Mutex::new(Vec::new())),
14531            channel_messages: Arc::new(Mutex::new(Vec::new())),
14532            link_data: Arc::new(Mutex::new(Vec::new())),
14533            responses: Arc::new(Mutex::new(Vec::new())),
14534            proofs: proofs.clone(),
14535            proof_requested: Arc::new(Mutex::new(Vec::new())),
14536        };
14537
14538        let mut driver = Driver::new(
14539            TransportConfig {
14540                transport_enabled: false,
14541                identity_hash: None,
14542                prefer_shorter_path: false,
14543                max_paths_per_destination: 1,
14544                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14545                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14546                max_path_destinations: usize::MAX,
14547                max_tunnel_destinations_total: usize::MAX,
14548                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14549                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14550                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14551                announce_sig_cache_enabled: true,
14552                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14553                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14554                announce_queue_max_entries: 256,
14555                announce_queue_max_interfaces: 1024,
14556            },
14557            rx,
14558            tx.clone(),
14559            Box::new(cbs),
14560        );
14561        let info = make_interface_info(1);
14562        driver.engine.register_interface(info);
14563        let (writer, _sent) = MockWriter::new();
14564        driver
14565            .interfaces
14566            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
14567
14568        // Register a destination so proof packets can be delivered locally
14569        let dest = [0xEE; 16];
14570        driver
14571            .engine
14572            .register_destination(dest, constants::DESTINATION_SINGLE);
14573
14574        // Simulate a sent packet that we're tracking
14575        let tracked_hash = [0x42u8; 32];
14576        let sent_time = time::now() - 0.5; // 500ms ago
14577        driver.sent_packets.insert(tracked_hash, (dest, sent_time));
14578
14579        // Build a PROOF packet with the tracked hash + dummy signature
14580        let mut proof_data = Vec::new();
14581        proof_data.extend_from_slice(&tracked_hash);
14582        proof_data.extend_from_slice(&[0xAA; 64]); // dummy signature
14583
14584        let flags = PacketFlags {
14585            header_type: constants::HEADER_1,
14586            context_flag: constants::FLAG_UNSET,
14587            transport_type: constants::TRANSPORT_BROADCAST,
14588            destination_type: constants::DESTINATION_SINGLE,
14589            packet_type: constants::PACKET_TYPE_PROOF,
14590        };
14591        let packet =
14592            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, &proof_data).unwrap();
14593
14594        tx.send(Event::Frame {
14595            interface_id: InterfaceId(1),
14596            data: packet.raw,
14597        })
14598        .unwrap();
14599        tx.send(Event::Shutdown).unwrap();
14600        driver.run();
14601
14602        // on_proof callback should have been fired
14603        let proof_list = proofs.lock().unwrap();
14604        assert_eq!(proof_list.len(), 1);
14605        assert_eq!(proof_list[0].0, DestHash(dest));
14606        assert_eq!(proof_list[0].1, PacketHash(tracked_hash));
14607        assert!(
14608            proof_list[0].2 >= 0.4,
14609            "RTT should be approximately 0.5s, got {}",
14610            proof_list[0].2
14611        );
14612
14613        // Tracked packet should be removed
14614        assert!(!driver.sent_packets.contains_key(&tracked_hash));
14615    }
14616
14617    #[test]
14618    fn inbound_proof_for_unknown_packet_is_ignored() {
14619        let (tx, rx) = event::channel();
14620        let proofs = Arc::new(Mutex::new(Vec::new()));
14621        let cbs = MockCallbacks {
14622            announces: Arc::new(Mutex::new(Vec::new())),
14623            paths: Arc::new(Mutex::new(Vec::new())),
14624            deliveries: Arc::new(Mutex::new(Vec::new())),
14625            iface_ups: Arc::new(Mutex::new(Vec::new())),
14626            iface_downs: Arc::new(Mutex::new(Vec::new())),
14627            link_established: Arc::new(Mutex::new(Vec::new())),
14628            link_closed: Arc::new(Mutex::new(Vec::new())),
14629            remote_identified: Arc::new(Mutex::new(Vec::new())),
14630            resources_received: Arc::new(Mutex::new(Vec::new())),
14631            resource_completed: Arc::new(Mutex::new(Vec::new())),
14632            resource_failed: Arc::new(Mutex::new(Vec::new())),
14633            channel_messages: Arc::new(Mutex::new(Vec::new())),
14634            link_data: Arc::new(Mutex::new(Vec::new())),
14635            responses: Arc::new(Mutex::new(Vec::new())),
14636            proofs: proofs.clone(),
14637            proof_requested: Arc::new(Mutex::new(Vec::new())),
14638        };
14639
14640        let mut driver = Driver::new(
14641            TransportConfig {
14642                transport_enabled: false,
14643                identity_hash: None,
14644                prefer_shorter_path: false,
14645                max_paths_per_destination: 1,
14646                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14647                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14648                max_path_destinations: usize::MAX,
14649                max_tunnel_destinations_total: usize::MAX,
14650                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14651                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14652                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14653                announce_sig_cache_enabled: true,
14654                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14655                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14656                announce_queue_max_entries: 256,
14657                announce_queue_max_interfaces: 1024,
14658            },
14659            rx,
14660            tx.clone(),
14661            Box::new(cbs),
14662        );
14663        let info = make_interface_info(1);
14664        driver.engine.register_interface(info);
14665        let (writer, _sent) = MockWriter::new();
14666        driver
14667            .interfaces
14668            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
14669
14670        let dest = [0xEE; 16];
14671        driver
14672            .engine
14673            .register_destination(dest, constants::DESTINATION_SINGLE);
14674
14675        // Build a PROOF packet for an untracked hash
14676        let unknown_hash = [0xFF; 32];
14677        let mut proof_data = Vec::new();
14678        proof_data.extend_from_slice(&unknown_hash);
14679        proof_data.extend_from_slice(&[0xAA; 64]);
14680
14681        let flags = PacketFlags {
14682            header_type: constants::HEADER_1,
14683            context_flag: constants::FLAG_UNSET,
14684            transport_type: constants::TRANSPORT_BROADCAST,
14685            destination_type: constants::DESTINATION_SINGLE,
14686            packet_type: constants::PACKET_TYPE_PROOF,
14687        };
14688        let packet =
14689            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, &proof_data).unwrap();
14690
14691        tx.send(Event::Frame {
14692            interface_id: InterfaceId(1),
14693            data: packet.raw,
14694        })
14695        .unwrap();
14696        tx.send(Event::Shutdown).unwrap();
14697        driver.run();
14698
14699        // on_proof should NOT have been called
14700        assert!(proofs.lock().unwrap().is_empty());
14701    }
14702
14703    #[test]
14704    fn inbound_implicit_proof_matches_truncated_destination() {
14705        let (tx, rx) = event::channel();
14706        let proofs = Arc::new(Mutex::new(Vec::new()));
14707        let cbs = MockCallbacks {
14708            announces: Arc::new(Mutex::new(Vec::new())),
14709            paths: Arc::new(Mutex::new(Vec::new())),
14710            deliveries: Arc::new(Mutex::new(Vec::new())),
14711            iface_ups: Arc::new(Mutex::new(Vec::new())),
14712            iface_downs: Arc::new(Mutex::new(Vec::new())),
14713            link_established: Arc::new(Mutex::new(Vec::new())),
14714            link_closed: Arc::new(Mutex::new(Vec::new())),
14715            remote_identified: Arc::new(Mutex::new(Vec::new())),
14716            resources_received: Arc::new(Mutex::new(Vec::new())),
14717            resource_completed: Arc::new(Mutex::new(Vec::new())),
14718            resource_failed: Arc::new(Mutex::new(Vec::new())),
14719            channel_messages: Arc::new(Mutex::new(Vec::new())),
14720            link_data: Arc::new(Mutex::new(Vec::new())),
14721            responses: Arc::new(Mutex::new(Vec::new())),
14722            proofs: proofs.clone(),
14723            proof_requested: Arc::new(Mutex::new(Vec::new())),
14724        };
14725
14726        let mut driver = Driver::new(
14727            TransportConfig {
14728                transport_enabled: false,
14729                identity_hash: None,
14730                prefer_shorter_path: false,
14731                max_paths_per_destination: 1,
14732                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14733                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14734                max_path_destinations: usize::MAX,
14735                max_tunnel_destinations_total: usize::MAX,
14736                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14737                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14738                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14739                announce_sig_cache_enabled: true,
14740                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14741                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14742                announce_queue_max_entries: 256,
14743                announce_queue_max_interfaces: 1024,
14744            },
14745            rx,
14746            tx.clone(),
14747            Box::new(cbs),
14748        );
14749        let info = make_interface_info(1);
14750        driver.engine.register_interface(info);
14751        let (writer, _sent) = MockWriter::new();
14752        driver
14753            .interfaces
14754            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
14755
14756        let tracked_hash = [0x3Cu8; 32];
14757        let sent_time = time::now() - 0.25;
14758        driver
14759            .sent_packets
14760            .insert(tracked_hash, ([0xEE; 16], sent_time));
14761
14762        let mut proof_dest = [0u8; 16];
14763        proof_dest.copy_from_slice(&tracked_hash[..16]);
14764        driver
14765            .engine
14766            .register_destination(proof_dest, constants::DESTINATION_SINGLE);
14767
14768        // Implicit proof is signature-only (64 bytes)
14769        let proof_data = vec![0xAA; 64];
14770        let flags = PacketFlags {
14771            header_type: constants::HEADER_1,
14772            context_flag: constants::FLAG_UNSET,
14773            transport_type: constants::TRANSPORT_BROADCAST,
14774            destination_type: constants::DESTINATION_SINGLE,
14775            packet_type: constants::PACKET_TYPE_PROOF,
14776        };
14777        let packet = RawPacket::pack(
14778            flags,
14779            0,
14780            &proof_dest,
14781            None,
14782            constants::CONTEXT_NONE,
14783            &proof_data,
14784        )
14785        .unwrap();
14786
14787        tx.send(Event::Frame {
14788            interface_id: InterfaceId(1),
14789            data: packet.raw,
14790        })
14791        .unwrap();
14792        tx.send(Event::Shutdown).unwrap();
14793        driver.run();
14794
14795        let proof_list = proofs.lock().unwrap();
14796        assert_eq!(proof_list.len(), 1);
14797        assert_eq!(proof_list[0].0, DestHash([0xEE; 16]));
14798        assert_eq!(proof_list[0].1, PacketHash(tracked_hash));
14799        assert!(!driver.sent_packets.contains_key(&tracked_hash));
14800    }
14801
14802    #[test]
14803    fn link_manager_data_send_is_tracked_for_proofs() {
14804        let mut driver = new_test_driver();
14805        let (writer, _sent) = MockWriter::new();
14806        driver
14807            .interfaces
14808            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
14809
14810        let flags = PacketFlags {
14811            header_type: constants::HEADER_1,
14812            context_flag: constants::FLAG_UNSET,
14813            transport_type: constants::TRANSPORT_BROADCAST,
14814            destination_type: constants::DESTINATION_LINK,
14815            packet_type: constants::PACKET_TYPE_DATA,
14816        };
14817        let packet = RawPacket::pack(
14818            flags,
14819            0,
14820            &[0x77; 16],
14821            None,
14822            constants::CONTEXT_NONE,
14823            b"track me",
14824        )
14825        .unwrap();
14826        let packet_hash = packet.packet_hash;
14827        let destination_hash = packet.destination_hash;
14828
14829        driver.dispatch_link_actions(vec![LinkManagerAction::SendPacket {
14830            raw: packet.raw,
14831            dest_type: constants::DESTINATION_LINK,
14832            attached_interface: Some(InterfaceId(1)),
14833        }]);
14834
14835        assert_eq!(
14836            driver.sent_packets.get(&packet_hash).map(|(dest, _)| *dest),
14837            Some(destination_hash)
14838        );
14839    }
14840
14841    #[test]
14842    fn inbound_proof_with_valid_signature_fires_callback() {
14843        // When the destination IS in known_destinations, the proof signature is verified
14844        let (tx, rx) = event::channel();
14845        let proofs = Arc::new(Mutex::new(Vec::new()));
14846        let cbs = MockCallbacks {
14847            announces: Arc::new(Mutex::new(Vec::new())),
14848            paths: Arc::new(Mutex::new(Vec::new())),
14849            deliveries: Arc::new(Mutex::new(Vec::new())),
14850            iface_ups: Arc::new(Mutex::new(Vec::new())),
14851            iface_downs: Arc::new(Mutex::new(Vec::new())),
14852            link_established: Arc::new(Mutex::new(Vec::new())),
14853            link_closed: Arc::new(Mutex::new(Vec::new())),
14854            remote_identified: Arc::new(Mutex::new(Vec::new())),
14855            resources_received: Arc::new(Mutex::new(Vec::new())),
14856            resource_completed: Arc::new(Mutex::new(Vec::new())),
14857            resource_failed: Arc::new(Mutex::new(Vec::new())),
14858            channel_messages: Arc::new(Mutex::new(Vec::new())),
14859            link_data: Arc::new(Mutex::new(Vec::new())),
14860            responses: Arc::new(Mutex::new(Vec::new())),
14861            proofs: proofs.clone(),
14862            proof_requested: Arc::new(Mutex::new(Vec::new())),
14863        };
14864
14865        let mut driver = Driver::new(
14866            TransportConfig {
14867                transport_enabled: false,
14868                identity_hash: None,
14869                prefer_shorter_path: false,
14870                max_paths_per_destination: 1,
14871                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14872                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14873                max_path_destinations: usize::MAX,
14874                max_tunnel_destinations_total: usize::MAX,
14875                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14876                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14877                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14878                announce_sig_cache_enabled: true,
14879                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14880                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14881                announce_queue_max_entries: 256,
14882                announce_queue_max_interfaces: 1024,
14883            },
14884            rx,
14885            tx.clone(),
14886            Box::new(cbs),
14887        );
14888        let info = make_interface_info(1);
14889        driver.engine.register_interface(info);
14890        let (writer, _sent) = MockWriter::new();
14891        driver
14892            .interfaces
14893            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
14894
14895        let dest = [0xEE; 16];
14896        driver
14897            .engine
14898            .register_destination(dest, constants::DESTINATION_SINGLE);
14899
14900        // Create real identity and add to known_destinations
14901        let identity = Identity::new(&mut OsRng);
14902        let pub_key = identity.get_public_key();
14903        driver.known_destinations.insert(
14904            dest,
14905            KnownDestinationState {
14906                announced: crate::destination::AnnouncedIdentity {
14907                    dest_hash: DestHash(dest),
14908                    identity_hash: IdentityHash(*identity.hash()),
14909                    public_key: pub_key.unwrap(),
14910                    app_data: None,
14911                    hops: 0,
14912                    received_at: time::now(),
14913                    receiving_interface: InterfaceId(0),
14914                },
14915                was_used: false,
14916                last_used_at: None,
14917                retained: false,
14918            },
14919        );
14920
14921        // Sign a packet hash with the identity
14922        let tracked_hash = [0x42u8; 32];
14923        let sent_time = time::now() - 0.5;
14924        driver.sent_packets.insert(tracked_hash, (dest, sent_time));
14925
14926        let signature = identity.sign(&tracked_hash).unwrap();
14927        let mut proof_data = Vec::new();
14928        proof_data.extend_from_slice(&tracked_hash);
14929        proof_data.extend_from_slice(&signature);
14930
14931        let flags = PacketFlags {
14932            header_type: constants::HEADER_1,
14933            context_flag: constants::FLAG_UNSET,
14934            transport_type: constants::TRANSPORT_BROADCAST,
14935            destination_type: constants::DESTINATION_SINGLE,
14936            packet_type: constants::PACKET_TYPE_PROOF,
14937        };
14938        let packet =
14939            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, &proof_data).unwrap();
14940
14941        tx.send(Event::Frame {
14942            interface_id: InterfaceId(1),
14943            data: packet.raw,
14944        })
14945        .unwrap();
14946        tx.send(Event::Shutdown).unwrap();
14947        driver.run();
14948
14949        // Valid signature: on_proof should fire
14950        let proof_list = proofs.lock().unwrap();
14951        assert_eq!(proof_list.len(), 1);
14952        assert_eq!(proof_list[0].0, DestHash(dest));
14953        assert_eq!(proof_list[0].1, PacketHash(tracked_hash));
14954    }
14955
14956    #[test]
14957    fn inbound_proof_with_invalid_signature_rejected() {
14958        // When known_destinations has the public key, bad signatures are rejected
14959        let (tx, rx) = event::channel();
14960        let proofs = Arc::new(Mutex::new(Vec::new()));
14961        let cbs = MockCallbacks {
14962            announces: Arc::new(Mutex::new(Vec::new())),
14963            paths: Arc::new(Mutex::new(Vec::new())),
14964            deliveries: Arc::new(Mutex::new(Vec::new())),
14965            iface_ups: Arc::new(Mutex::new(Vec::new())),
14966            iface_downs: Arc::new(Mutex::new(Vec::new())),
14967            link_established: Arc::new(Mutex::new(Vec::new())),
14968            link_closed: Arc::new(Mutex::new(Vec::new())),
14969            remote_identified: Arc::new(Mutex::new(Vec::new())),
14970            resources_received: Arc::new(Mutex::new(Vec::new())),
14971            resource_completed: Arc::new(Mutex::new(Vec::new())),
14972            resource_failed: Arc::new(Mutex::new(Vec::new())),
14973            channel_messages: Arc::new(Mutex::new(Vec::new())),
14974            link_data: Arc::new(Mutex::new(Vec::new())),
14975            responses: Arc::new(Mutex::new(Vec::new())),
14976            proofs: proofs.clone(),
14977            proof_requested: Arc::new(Mutex::new(Vec::new())),
14978        };
14979
14980        let mut driver = Driver::new(
14981            TransportConfig {
14982                transport_enabled: false,
14983                identity_hash: None,
14984                prefer_shorter_path: false,
14985                max_paths_per_destination: 1,
14986                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14987                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14988                max_path_destinations: usize::MAX,
14989                max_tunnel_destinations_total: usize::MAX,
14990                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14991                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14992                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14993                announce_sig_cache_enabled: true,
14994                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14995                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14996                announce_queue_max_entries: 256,
14997                announce_queue_max_interfaces: 1024,
14998            },
14999            rx,
15000            tx.clone(),
15001            Box::new(cbs),
15002        );
15003        let info = make_interface_info(1);
15004        driver.engine.register_interface(info);
15005        let (writer, _sent) = MockWriter::new();
15006        driver
15007            .interfaces
15008            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
15009
15010        let dest = [0xEE; 16];
15011        driver
15012            .engine
15013            .register_destination(dest, constants::DESTINATION_SINGLE);
15014
15015        // Create identity and add to known_destinations
15016        let identity = Identity::new(&mut OsRng);
15017        let pub_key = identity.get_public_key();
15018        driver.known_destinations.insert(
15019            dest,
15020            KnownDestinationState {
15021                announced: crate::destination::AnnouncedIdentity {
15022                    dest_hash: DestHash(dest),
15023                    identity_hash: IdentityHash(*identity.hash()),
15024                    public_key: pub_key.unwrap(),
15025                    app_data: None,
15026                    hops: 0,
15027                    received_at: time::now(),
15028                    receiving_interface: InterfaceId(0),
15029                },
15030                was_used: false,
15031                last_used_at: None,
15032                retained: false,
15033            },
15034        );
15035
15036        // Track a sent packet
15037        let tracked_hash = [0x42u8; 32];
15038        let sent_time = time::now() - 0.5;
15039        driver.sent_packets.insert(tracked_hash, (dest, sent_time));
15040
15041        // Use WRONG signature (all 0xAA — invalid for this identity)
15042        let mut proof_data = Vec::new();
15043        proof_data.extend_from_slice(&tracked_hash);
15044        proof_data.extend_from_slice(&[0xAA; 64]);
15045
15046        let flags = PacketFlags {
15047            header_type: constants::HEADER_1,
15048            context_flag: constants::FLAG_UNSET,
15049            transport_type: constants::TRANSPORT_BROADCAST,
15050            destination_type: constants::DESTINATION_SINGLE,
15051            packet_type: constants::PACKET_TYPE_PROOF,
15052        };
15053        let packet =
15054            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, &proof_data).unwrap();
15055
15056        tx.send(Event::Frame {
15057            interface_id: InterfaceId(1),
15058            data: packet.raw,
15059        })
15060        .unwrap();
15061        tx.send(Event::Shutdown).unwrap();
15062        driver.run();
15063
15064        // Invalid signature: on_proof should NOT fire
15065        assert!(proofs.lock().unwrap().is_empty());
15066    }
15067
15068    #[test]
15069    fn proof_data_is_valid_explicit_proof() {
15070        // Verify that the proof generated by ProveAll is a valid explicit proof
15071        let (tx, rx) = event::channel();
15072        let (cbs, _, _, _, _, _) = MockCallbacks::new();
15073        let mut driver = Driver::new(
15074            TransportConfig {
15075                transport_enabled: false,
15076                identity_hash: None,
15077                prefer_shorter_path: false,
15078                max_paths_per_destination: 1,
15079                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
15080                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
15081                max_path_destinations: usize::MAX,
15082                max_tunnel_destinations_total: usize::MAX,
15083                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
15084                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
15085                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
15086                announce_sig_cache_enabled: true,
15087                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
15088                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
15089                announce_queue_max_entries: 256,
15090                announce_queue_max_interfaces: 1024,
15091            },
15092            rx,
15093            tx.clone(),
15094            Box::new(cbs),
15095        );
15096        let info = make_interface_info(1);
15097        driver.engine.register_interface(info);
15098        let (writer, sent) = MockWriter::new();
15099        driver
15100            .interfaces
15101            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
15102
15103        let dest = [0xDD; 16];
15104        let identity = Identity::new(&mut OsRng);
15105        let prv_key = identity.get_private_key().unwrap();
15106        driver
15107            .engine
15108            .register_destination(dest, constants::DESTINATION_SINGLE);
15109        driver.proof_strategies.insert(
15110            dest,
15111            (
15112                rns_core::types::ProofStrategy::ProveAll,
15113                Some(Identity::from_private_key(&prv_key)),
15114            ),
15115        );
15116
15117        let flags = PacketFlags {
15118            header_type: constants::HEADER_1,
15119            context_flag: constants::FLAG_UNSET,
15120            transport_type: constants::TRANSPORT_BROADCAST,
15121            destination_type: constants::DESTINATION_SINGLE,
15122            packet_type: constants::PACKET_TYPE_DATA,
15123        };
15124        let data_packet =
15125            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"verify me").unwrap();
15126        let data_packet_hash = data_packet.packet_hash;
15127
15128        tx.send(Event::Frame {
15129            interface_id: InterfaceId(1),
15130            data: data_packet.raw,
15131        })
15132        .unwrap();
15133        tx.send(Event::Shutdown).unwrap();
15134        driver.run();
15135
15136        // Find the proof packet in sent
15137        let sent_packets = sent.lock().unwrap();
15138        let proof_raw = sent_packets.iter().find(|raw| {
15139            let f = PacketFlags::unpack(raw[0] & 0x7F);
15140            f.packet_type == constants::PACKET_TYPE_PROOF
15141        });
15142        assert!(proof_raw.is_some(), "Should have sent a proof");
15143
15144        let proof_packet = RawPacket::unpack(proof_raw.unwrap()).unwrap();
15145        // Proof data should be 96 bytes: packet_hash(32) + signature(64)
15146        assert_eq!(
15147            proof_packet.data.len(),
15148            96,
15149            "Explicit proof should be 96 bytes"
15150        );
15151
15152        // Validate using rns-core's receipt module
15153        let result = rns_core::receipt::validate_proof(
15154            &proof_packet.data,
15155            &data_packet_hash,
15156            &Identity::from_private_key(&prv_key), // same identity
15157        );
15158        assert_eq!(result, rns_core::receipt::ProofResult::Valid);
15159    }
15160
15161    #[test]
15162    fn query_local_destinations_empty() {
15163        let (tx, rx) = event::channel();
15164        let (cbs, _, _, _, _, _) = MockCallbacks::new();
15165        let driver_config = TransportConfig {
15166            transport_enabled: false,
15167            identity_hash: None,
15168            prefer_shorter_path: false,
15169            max_paths_per_destination: 1,
15170            packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
15171            max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
15172            max_path_destinations: usize::MAX,
15173            max_tunnel_destinations_total: usize::MAX,
15174            destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
15175            announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
15176            announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
15177            announce_sig_cache_enabled: true,
15178            announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
15179            announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
15180            announce_queue_max_entries: 256,
15181            announce_queue_max_interfaces: 1024,
15182        };
15183        let mut driver = Driver::new(driver_config, rx, tx.clone(), Box::new(cbs));
15184
15185        let (resp_tx, resp_rx) = mpsc::channel();
15186        tx.send(Event::Query(QueryRequest::LocalDestinations, resp_tx))
15187            .unwrap();
15188        tx.send(Event::Shutdown).unwrap();
15189        driver.run();
15190
15191        match resp_rx.recv().unwrap() {
15192            QueryResponse::LocalDestinations(entries) => {
15193                // Should contain the two internal destinations (tunnel_synth + path_request)
15194                assert_eq!(entries.len(), 2);
15195                for entry in &entries {
15196                    assert_eq!(entry.dest_type, rns_core::constants::DESTINATION_PLAIN);
15197                }
15198            }
15199            other => panic!("expected LocalDestinations, got {:?}", other),
15200        }
15201    }
15202
15203    #[test]
15204    fn query_local_destinations_with_registered() {
15205        let (tx, rx) = event::channel();
15206        let (cbs, _, _, _, _, _) = MockCallbacks::new();
15207        let driver_config = TransportConfig {
15208            transport_enabled: false,
15209            identity_hash: None,
15210            prefer_shorter_path: false,
15211            max_paths_per_destination: 1,
15212            packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
15213            max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
15214            max_path_destinations: usize::MAX,
15215            max_tunnel_destinations_total: usize::MAX,
15216            destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
15217            announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
15218            announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
15219            announce_sig_cache_enabled: true,
15220            announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
15221            announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
15222            announce_queue_max_entries: 256,
15223            announce_queue_max_interfaces: 1024,
15224        };
15225        let mut driver = Driver::new(driver_config, rx, tx.clone(), Box::new(cbs));
15226
15227        let dest_hash = [0xAA; 16];
15228        tx.send(Event::RegisterDestination {
15229            dest_hash,
15230            dest_type: rns_core::constants::DESTINATION_SINGLE,
15231        })
15232        .unwrap();
15233
15234        let (resp_tx, resp_rx) = mpsc::channel();
15235        tx.send(Event::Query(QueryRequest::LocalDestinations, resp_tx))
15236            .unwrap();
15237        tx.send(Event::Shutdown).unwrap();
15238        driver.run();
15239
15240        match resp_rx.recv().unwrap() {
15241            QueryResponse::LocalDestinations(entries) => {
15242                // 2 internal + 1 registered
15243                assert_eq!(entries.len(), 3);
15244                assert!(entries.iter().any(|e| e.hash == dest_hash
15245                    && e.dest_type == rns_core::constants::DESTINATION_SINGLE));
15246            }
15247            other => panic!("expected LocalDestinations, got {:?}", other),
15248        }
15249    }
15250
15251    #[test]
15252    fn query_local_destinations_tracks_link_dest() {
15253        let (tx, rx) = event::channel();
15254        let (cbs, _, _, _, _, _) = MockCallbacks::new();
15255        let driver_config = TransportConfig {
15256            transport_enabled: false,
15257            identity_hash: None,
15258            prefer_shorter_path: false,
15259            max_paths_per_destination: 1,
15260            packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
15261            max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
15262            max_path_destinations: usize::MAX,
15263            max_tunnel_destinations_total: usize::MAX,
15264            destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
15265            announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
15266            announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
15267            announce_sig_cache_enabled: true,
15268            announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
15269            announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
15270            announce_queue_max_entries: 256,
15271            announce_queue_max_interfaces: 1024,
15272        };
15273        let mut driver = Driver::new(driver_config, rx, tx.clone(), Box::new(cbs));
15274
15275        let dest_hash = [0xBB; 16];
15276        tx.send(Event::RegisterLinkDestination {
15277            dest_hash,
15278            sig_prv_bytes: [0x11; 32],
15279            sig_pub_bytes: [0x22; 32],
15280            resource_strategy: 0,
15281        })
15282        .unwrap();
15283
15284        let (resp_tx, resp_rx) = mpsc::channel();
15285        tx.send(Event::Query(QueryRequest::LocalDestinations, resp_tx))
15286            .unwrap();
15287        tx.send(Event::Shutdown).unwrap();
15288        driver.run();
15289
15290        match resp_rx.recv().unwrap() {
15291            QueryResponse::LocalDestinations(entries) => {
15292                // 2 internal + 1 link destination
15293                assert_eq!(entries.len(), 3);
15294                assert!(entries.iter().any(|e| e.hash == dest_hash
15295                    && e.dest_type == rns_core::constants::DESTINATION_SINGLE));
15296            }
15297            other => panic!("expected LocalDestinations, got {:?}", other),
15298        }
15299    }
15300
15301    #[test]
15302    fn query_links_empty() {
15303        let (tx, rx) = event::channel();
15304        let (cbs, _, _, _, _, _) = MockCallbacks::new();
15305        let driver_config = TransportConfig {
15306            transport_enabled: false,
15307            identity_hash: None,
15308            prefer_shorter_path: false,
15309            max_paths_per_destination: 1,
15310            packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
15311            max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
15312            max_path_destinations: usize::MAX,
15313            max_tunnel_destinations_total: usize::MAX,
15314            destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
15315            announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
15316            announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
15317            announce_sig_cache_enabled: true,
15318            announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
15319            announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
15320            announce_queue_max_entries: 256,
15321            announce_queue_max_interfaces: 1024,
15322        };
15323        let mut driver = Driver::new(driver_config, rx, tx.clone(), Box::new(cbs));
15324
15325        let (resp_tx, resp_rx) = mpsc::channel();
15326        tx.send(Event::Query(QueryRequest::Links, resp_tx)).unwrap();
15327        tx.send(Event::Shutdown).unwrap();
15328        driver.run();
15329
15330        match resp_rx.recv().unwrap() {
15331            QueryResponse::Links(entries) => {
15332                assert!(entries.is_empty());
15333            }
15334            other => panic!("expected Links, got {:?}", other),
15335        }
15336    }
15337
15338    #[test]
15339    fn query_resources_empty() {
15340        let (tx, rx) = event::channel();
15341        let (cbs, _, _, _, _, _) = MockCallbacks::new();
15342        let driver_config = TransportConfig {
15343            transport_enabled: false,
15344            identity_hash: None,
15345            prefer_shorter_path: false,
15346            max_paths_per_destination: 1,
15347            packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
15348            max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
15349            max_path_destinations: usize::MAX,
15350            max_tunnel_destinations_total: usize::MAX,
15351            destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
15352            announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
15353            announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
15354            announce_sig_cache_enabled: true,
15355            announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
15356            announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
15357            announce_queue_max_entries: 256,
15358            announce_queue_max_interfaces: 1024,
15359        };
15360        let mut driver = Driver::new(driver_config, rx, tx.clone(), Box::new(cbs));
15361
15362        let (resp_tx, resp_rx) = mpsc::channel();
15363        tx.send(Event::Query(QueryRequest::Resources, resp_tx))
15364            .unwrap();
15365        tx.send(Event::Shutdown).unwrap();
15366        driver.run();
15367
15368        match resp_rx.recv().unwrap() {
15369            QueryResponse::Resources(entries) => {
15370                assert!(entries.is_empty());
15371            }
15372            other => panic!("expected Resources, got {:?}", other),
15373        }
15374    }
15375
15376    #[test]
15377    fn infer_interface_type_from_name() {
15378        assert_eq!(
15379            super::infer_interface_type("TCPServerInterface/Client-1234"),
15380            "TCPServerClientInterface"
15381        );
15382        assert_eq!(
15383            super::infer_interface_type("BackboneInterface/5"),
15384            "BackboneInterface"
15385        );
15386        assert_eq!(
15387            super::infer_interface_type("LocalInterface"),
15388            "LocalServerClientInterface"
15389        );
15390        assert_eq!(
15391            super::infer_interface_type("MyAutoGroup:fe80::1"),
15392            "AutoInterface"
15393        );
15394    }
15395
15396    // ---- extract_dest_hash tests ----
15397
15398    #[test]
15399    fn test_extract_dest_hash_empty() {
15400        assert_eq!(super::extract_dest_hash(&[]), [0u8; 16]);
15401    }
15402
15403    // =========================================================================
15404    // Probe tests: SendProbe, CheckProof, completed_proofs, probe_responder
15405    // =========================================================================
15406
15407    #[test]
15408    fn send_probe_unknown_dest_returns_none() {
15409        let (tx, rx) = event::channel();
15410        let (cbs, _, _, _, _, _) = MockCallbacks::new();
15411        let mut driver = Driver::new(
15412            TransportConfig {
15413                transport_enabled: false,
15414                identity_hash: None,
15415                prefer_shorter_path: false,
15416                max_paths_per_destination: 1,
15417                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
15418                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
15419                max_path_destinations: usize::MAX,
15420                max_tunnel_destinations_total: usize::MAX,
15421                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
15422                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
15423                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
15424                announce_sig_cache_enabled: true,
15425                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
15426                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
15427                announce_queue_max_entries: 256,
15428                announce_queue_max_interfaces: 1024,
15429            },
15430            rx,
15431            tx.clone(),
15432            Box::new(cbs),
15433        );
15434        let info = make_interface_info(1);
15435        driver.engine.register_interface(info);
15436        let (writer, _sent) = MockWriter::new();
15437        driver
15438            .interfaces
15439            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
15440
15441        // SendProbe for a dest_hash with no known identity should return None
15442        let (resp_tx, resp_rx) = mpsc::channel();
15443        tx.send(Event::Query(
15444            QueryRequest::SendProbe {
15445                dest_hash: [0xAA; 16],
15446                payload_size: 16,
15447            },
15448            resp_tx,
15449        ))
15450        .unwrap();
15451        tx.send(Event::Shutdown).unwrap();
15452        driver.run();
15453
15454        match resp_rx.recv().unwrap() {
15455            QueryResponse::SendProbe(None) => {}
15456            other => panic!("expected SendProbe(None), got {:?}", other),
15457        }
15458    }
15459
15460    #[test]
15461    fn send_probe_known_dest_returns_packet_hash() {
15462        let (tx, rx) = event::channel();
15463        let (cbs, _, _, _, _, _) = MockCallbacks::new();
15464        let mut driver = Driver::new(
15465            TransportConfig {
15466                transport_enabled: false,
15467                identity_hash: None,
15468                prefer_shorter_path: false,
15469                max_paths_per_destination: 1,
15470                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
15471                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
15472                max_path_destinations: usize::MAX,
15473                max_tunnel_destinations_total: usize::MAX,
15474                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
15475                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
15476                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
15477                announce_sig_cache_enabled: true,
15478                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
15479                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
15480                announce_queue_max_entries: 256,
15481                announce_queue_max_interfaces: 1024,
15482            },
15483            rx,
15484            tx.clone(),
15485            Box::new(cbs),
15486        );
15487        let info = make_interface_info(1);
15488        driver.engine.register_interface(info);
15489        let (writer, sent) = MockWriter::new();
15490        driver
15491            .interfaces
15492            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
15493
15494        // Inject a known identity so SendProbe can encrypt to it
15495        let remote_identity = Identity::new(&mut OsRng);
15496        let dest_hash = rns_core::destination::destination_hash(
15497            "rnstransport",
15498            &["probe"],
15499            Some(remote_identity.hash()),
15500        );
15501
15502        // First inject the identity via announce
15503        let (inject_tx, inject_rx) = mpsc::channel();
15504        tx.send(Event::Query(
15505            QueryRequest::InjectIdentity {
15506                dest_hash,
15507                identity_hash: *remote_identity.hash(),
15508                public_key: remote_identity.get_public_key().unwrap(),
15509                app_data: None,
15510                hops: 1,
15511                received_at: 0.0,
15512            },
15513            inject_tx,
15514        ))
15515        .unwrap();
15516
15517        // Now send the probe
15518        let (resp_tx, resp_rx) = mpsc::channel();
15519        tx.send(Event::Query(
15520            QueryRequest::SendProbe {
15521                dest_hash,
15522                payload_size: 16,
15523            },
15524            resp_tx,
15525        ))
15526        .unwrap();
15527        tx.send(Event::Shutdown).unwrap();
15528        driver.run();
15529
15530        // Verify injection succeeded
15531        match inject_rx.recv().unwrap() {
15532            QueryResponse::InjectIdentity(true) => {}
15533            other => panic!("expected InjectIdentity(true), got {:?}", other),
15534        }
15535
15536        // Verify probe sent
15537        match resp_rx.recv().unwrap() {
15538            QueryResponse::SendProbe(Some((packet_hash, _hops))) => {
15539                // Packet hash should be non-zero
15540                assert_ne!(packet_hash, [0u8; 32]);
15541                // Should be tracked in sent_packets
15542                assert!(driver.sent_packets.contains_key(&packet_hash));
15543                // Should have sent a DATA packet on the wire
15544                let sent_data = sent.lock().unwrap();
15545                assert!(!sent_data.is_empty(), "Probe packet should be sent on wire");
15546                // Verify it's a DATA SINGLE packet
15547                let raw = &sent_data[0];
15548                let flags = PacketFlags::unpack(raw[0] & 0x7F);
15549                assert_eq!(flags.packet_type, constants::PACKET_TYPE_DATA);
15550                assert_eq!(flags.destination_type, constants::DESTINATION_SINGLE);
15551            }
15552            other => panic!("expected SendProbe(Some(..)), got {:?}", other),
15553        }
15554    }
15555
15556    #[test]
15557    fn check_proof_not_found_returns_none() {
15558        let (tx, rx) = event::channel();
15559        let (cbs, _, _, _, _, _) = MockCallbacks::new();
15560        let mut driver = Driver::new(
15561            TransportConfig {
15562                transport_enabled: false,
15563                identity_hash: None,
15564                prefer_shorter_path: false,
15565                max_paths_per_destination: 1,
15566                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
15567                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
15568                max_path_destinations: usize::MAX,
15569                max_tunnel_destinations_total: usize::MAX,
15570                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
15571                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
15572                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
15573                announce_sig_cache_enabled: true,
15574                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
15575                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
15576                announce_queue_max_entries: 256,
15577                announce_queue_max_interfaces: 1024,
15578            },
15579            rx,
15580            tx.clone(),
15581            Box::new(cbs),
15582        );
15583
15584        let (resp_tx, resp_rx) = mpsc::channel();
15585        tx.send(Event::Query(
15586            QueryRequest::CheckProof {
15587                packet_hash: [0xBB; 32],
15588            },
15589            resp_tx,
15590        ))
15591        .unwrap();
15592        tx.send(Event::Shutdown).unwrap();
15593        driver.run();
15594
15595        match resp_rx.recv().unwrap() {
15596            QueryResponse::CheckProof(None) => {}
15597            other => panic!("expected CheckProof(None), got {:?}", other),
15598        }
15599    }
15600
15601    #[test]
15602    fn check_proof_found_returns_rtt() {
15603        let (tx, rx) = event::channel();
15604        let (cbs, _, _, _, _, _) = MockCallbacks::new();
15605        let mut driver = Driver::new(
15606            TransportConfig {
15607                transport_enabled: false,
15608                identity_hash: None,
15609                prefer_shorter_path: false,
15610                max_paths_per_destination: 1,
15611                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
15612                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
15613                max_path_destinations: usize::MAX,
15614                max_tunnel_destinations_total: usize::MAX,
15615                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
15616                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
15617                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
15618                announce_sig_cache_enabled: true,
15619                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
15620                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
15621                announce_queue_max_entries: 256,
15622                announce_queue_max_interfaces: 1024,
15623            },
15624            rx,
15625            tx.clone(),
15626            Box::new(cbs),
15627        );
15628
15629        // Pre-populate completed_proofs
15630        let packet_hash = [0xCC; 32];
15631        driver
15632            .completed_proofs
15633            .insert(packet_hash, (0.123, time::now()));
15634
15635        let (resp_tx, resp_rx) = mpsc::channel();
15636        tx.send(Event::Query(
15637            QueryRequest::CheckProof { packet_hash },
15638            resp_tx,
15639        ))
15640        .unwrap();
15641        tx.send(Event::Shutdown).unwrap();
15642        driver.run();
15643
15644        match resp_rx.recv().unwrap() {
15645            QueryResponse::CheckProof(Some(rtt)) => {
15646                assert!(
15647                    (rtt - 0.123).abs() < 0.001,
15648                    "RTT should be ~0.123, got {}",
15649                    rtt
15650                );
15651            }
15652            other => panic!("expected CheckProof(Some(..)), got {:?}", other),
15653        }
15654        // Should be consumed (removed) after checking
15655        assert!(!driver.completed_proofs.contains_key(&packet_hash));
15656    }
15657
15658    #[test]
15659    fn inbound_proof_populates_completed_proofs() {
15660        let (tx, rx) = event::channel();
15661        let proofs = Arc::new(Mutex::new(Vec::new()));
15662        let cbs = MockCallbacks {
15663            announces: Arc::new(Mutex::new(Vec::new())),
15664            paths: Arc::new(Mutex::new(Vec::new())),
15665            deliveries: Arc::new(Mutex::new(Vec::new())),
15666            iface_ups: Arc::new(Mutex::new(Vec::new())),
15667            iface_downs: Arc::new(Mutex::new(Vec::new())),
15668            link_established: Arc::new(Mutex::new(Vec::new())),
15669            link_closed: Arc::new(Mutex::new(Vec::new())),
15670            remote_identified: Arc::new(Mutex::new(Vec::new())),
15671            resources_received: Arc::new(Mutex::new(Vec::new())),
15672            resource_completed: Arc::new(Mutex::new(Vec::new())),
15673            resource_failed: Arc::new(Mutex::new(Vec::new())),
15674            channel_messages: Arc::new(Mutex::new(Vec::new())),
15675            link_data: Arc::new(Mutex::new(Vec::new())),
15676            responses: Arc::new(Mutex::new(Vec::new())),
15677            proofs: proofs.clone(),
15678            proof_requested: Arc::new(Mutex::new(Vec::new())),
15679        };
15680
15681        let mut driver = Driver::new(
15682            TransportConfig {
15683                transport_enabled: false,
15684                identity_hash: None,
15685                prefer_shorter_path: false,
15686                max_paths_per_destination: 1,
15687                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
15688                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
15689                max_path_destinations: usize::MAX,
15690                max_tunnel_destinations_total: usize::MAX,
15691                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
15692                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
15693                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
15694                announce_sig_cache_enabled: true,
15695                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
15696                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
15697                announce_queue_max_entries: 256,
15698                announce_queue_max_interfaces: 1024,
15699            },
15700            rx,
15701            tx.clone(),
15702            Box::new(cbs),
15703        );
15704        let info = make_interface_info(1);
15705        driver.engine.register_interface(info);
15706        let (writer, sent) = MockWriter::new();
15707        driver
15708            .interfaces
15709            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
15710
15711        // Register a destination with ProveAll so we can get a proof back
15712        let dest = [0xDD; 16];
15713        let identity = Identity::new(&mut OsRng);
15714        let prv_key = identity.get_private_key().unwrap();
15715        driver
15716            .engine
15717            .register_destination(dest, constants::DESTINATION_SINGLE);
15718        driver.proof_strategies.insert(
15719            dest,
15720            (
15721                rns_core::types::ProofStrategy::ProveAll,
15722                Some(Identity::from_private_key(&prv_key)),
15723            ),
15724        );
15725
15726        // Build and send a DATA packet to the dest (this creates a sent_packet + proof)
15727        let flags = PacketFlags {
15728            header_type: constants::HEADER_1,
15729            context_flag: constants::FLAG_UNSET,
15730            transport_type: constants::TRANSPORT_BROADCAST,
15731            destination_type: constants::DESTINATION_SINGLE,
15732            packet_type: constants::PACKET_TYPE_DATA,
15733        };
15734        let data_packet = RawPacket::pack(
15735            flags,
15736            0,
15737            &dest,
15738            None,
15739            constants::CONTEXT_NONE,
15740            b"probe data",
15741        )
15742        .unwrap();
15743        let data_packet_hash = data_packet.packet_hash;
15744
15745        // Track it as a sent packet so the proof handler recognizes it
15746        driver
15747            .sent_packets
15748            .insert(data_packet_hash, (dest, time::now()));
15749
15750        // Deliver the frame — this generates a proof which gets sent on wire
15751        tx.send(Event::Frame {
15752            interface_id: InterfaceId(1),
15753            data: data_packet.raw,
15754        })
15755        .unwrap();
15756        tx.send(Event::Shutdown).unwrap();
15757        driver.run();
15758
15759        // The proof was generated and sent on the wire
15760        let sent_packets = sent.lock().unwrap();
15761        let proof_packets: Vec<_> = sent_packets
15762            .iter()
15763            .filter(|raw| {
15764                let flags = PacketFlags::unpack(raw[0] & 0x7F);
15765                flags.packet_type == constants::PACKET_TYPE_PROOF
15766            })
15767            .collect();
15768        assert!(!proof_packets.is_empty(), "Should have sent a proof packet");
15769
15770        // Now feed the proof packet back to the driver so handle_inbound_proof fires.
15771        // We need a fresh driver run since the previous one shut down.
15772        // Instead, verify the data flow: the proof was sent on wire, and when
15773        // handle_inbound_proof processes a matching proof, completed_proofs gets populated.
15774        // Since our DATA packet was both delivered locally AND tracked in sent_packets,
15775        // the proof was generated on delivery. But the proof is for the *sender* to verify --
15776        // the proof gets sent back to the sender. So in this test (same driver = both sides),
15777        // the proof was sent on wire but not yet received back.
15778        //
15779        // Let's verify handle_inbound_proof directly by feeding the proof frame back.
15780        let proof_raw = proof_packets[0].clone();
15781        drop(sent_packets); // release lock
15782
15783        // Create a new event loop to handle the proof frame
15784        let (tx2, rx2) = event::channel();
15785        let proofs2 = Arc::new(Mutex::new(Vec::new()));
15786        let cbs2 = MockCallbacks {
15787            announces: Arc::new(Mutex::new(Vec::new())),
15788            paths: Arc::new(Mutex::new(Vec::new())),
15789            deliveries: Arc::new(Mutex::new(Vec::new())),
15790            iface_ups: Arc::new(Mutex::new(Vec::new())),
15791            iface_downs: Arc::new(Mutex::new(Vec::new())),
15792            link_established: Arc::new(Mutex::new(Vec::new())),
15793            link_closed: Arc::new(Mutex::new(Vec::new())),
15794            remote_identified: Arc::new(Mutex::new(Vec::new())),
15795            resources_received: Arc::new(Mutex::new(Vec::new())),
15796            resource_completed: Arc::new(Mutex::new(Vec::new())),
15797            resource_failed: Arc::new(Mutex::new(Vec::new())),
15798            channel_messages: Arc::new(Mutex::new(Vec::new())),
15799            link_data: Arc::new(Mutex::new(Vec::new())),
15800            responses: Arc::new(Mutex::new(Vec::new())),
15801            proofs: proofs2.clone(),
15802            proof_requested: Arc::new(Mutex::new(Vec::new())),
15803        };
15804        let mut driver2 = Driver::new(
15805            TransportConfig {
15806                transport_enabled: false,
15807                identity_hash: None,
15808                prefer_shorter_path: false,
15809                max_paths_per_destination: 1,
15810                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
15811                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
15812                max_path_destinations: usize::MAX,
15813                max_tunnel_destinations_total: usize::MAX,
15814                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
15815                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
15816                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
15817                announce_sig_cache_enabled: true,
15818                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
15819                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
15820                announce_queue_max_entries: 256,
15821                announce_queue_max_interfaces: 1024,
15822            },
15823            rx2,
15824            tx2.clone(),
15825            Box::new(cbs2),
15826        );
15827        let info2 = make_interface_info(1);
15828        driver2.engine.register_interface(info2);
15829        let (writer2, _sent2) = MockWriter::new();
15830        driver2
15831            .interfaces
15832            .insert(InterfaceId(1), make_entry(1, Box::new(writer2), true));
15833
15834        // Track the original sent packet in driver2 so it recognizes the proof
15835        driver2
15836            .sent_packets
15837            .insert(data_packet_hash, (dest, time::now()));
15838
15839        // Feed the proof frame
15840        tx2.send(Event::Frame {
15841            interface_id: InterfaceId(1),
15842            data: proof_raw,
15843        })
15844        .unwrap();
15845        tx2.send(Event::Shutdown).unwrap();
15846        driver2.run();
15847
15848        // The on_proof callback should have fired
15849        let proof_events = proofs2.lock().unwrap();
15850        assert_eq!(proof_events.len(), 1, "on_proof callback should fire once");
15851        assert_eq!(
15852            proof_events[0].1 .0, data_packet_hash,
15853            "proof should match original packet hash"
15854        );
15855        assert!(proof_events[0].2 >= 0.0, "RTT should be non-negative");
15856
15857        // completed_proofs should contain the entry
15858        assert!(
15859            driver2.completed_proofs.contains_key(&data_packet_hash),
15860            "completed_proofs should contain the packet hash"
15861        );
15862        let (rtt, _received) = driver2.completed_proofs[&data_packet_hash];
15863        assert!(rtt >= 0.0, "RTT should be non-negative");
15864    }
15865
15866    #[test]
15867    fn interface_stats_includes_probe_responder() {
15868        let (tx, rx) = event::channel();
15869        let (cbs, _, _, _, _, _) = MockCallbacks::new();
15870        let mut driver = Driver::new(
15871            TransportConfig {
15872                transport_enabled: true,
15873                identity_hash: Some([0x42; 16]),
15874                prefer_shorter_path: false,
15875                max_paths_per_destination: 1,
15876                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
15877                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
15878                max_path_destinations: usize::MAX,
15879                max_tunnel_destinations_total: usize::MAX,
15880                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
15881                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
15882                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
15883                announce_sig_cache_enabled: true,
15884                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
15885                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
15886                announce_queue_max_entries: 256,
15887                announce_queue_max_interfaces: 1024,
15888            },
15889            rx,
15890            tx.clone(),
15891            Box::new(cbs),
15892        );
15893        let (writer, _sent) = MockWriter::new();
15894        driver
15895            .interfaces
15896            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
15897
15898        // Set probe_responder_hash
15899        driver.probe_responder_hash = Some([0xEE; 16]);
15900
15901        let (resp_tx, resp_rx) = mpsc::channel();
15902        tx.send(Event::Query(QueryRequest::InterfaceStats, resp_tx))
15903            .unwrap();
15904        tx.send(Event::Shutdown).unwrap();
15905        driver.run();
15906
15907        match resp_rx.recv().unwrap() {
15908            QueryResponse::InterfaceStats(stats) => {
15909                assert_eq!(stats.probe_responder, Some([0xEE; 16]));
15910            }
15911            other => panic!("expected InterfaceStats, got {:?}", other),
15912        }
15913    }
15914
15915    #[test]
15916    fn interface_stats_probe_responder_none_when_disabled() {
15917        let (tx, rx) = event::channel();
15918        let (cbs, _, _, _, _, _) = MockCallbacks::new();
15919        let mut driver = Driver::new(
15920            TransportConfig {
15921                transport_enabled: false,
15922                identity_hash: None,
15923                prefer_shorter_path: false,
15924                max_paths_per_destination: 1,
15925                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
15926                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
15927                max_path_destinations: usize::MAX,
15928                max_tunnel_destinations_total: usize::MAX,
15929                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
15930                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
15931                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
15932                announce_sig_cache_enabled: true,
15933                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
15934                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
15935                announce_queue_max_entries: 256,
15936                announce_queue_max_interfaces: 1024,
15937            },
15938            rx,
15939            tx.clone(),
15940            Box::new(cbs),
15941        );
15942        let (writer, _sent) = MockWriter::new();
15943        driver
15944            .interfaces
15945            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
15946
15947        let (resp_tx, resp_rx) = mpsc::channel();
15948        tx.send(Event::Query(QueryRequest::InterfaceStats, resp_tx))
15949            .unwrap();
15950        tx.send(Event::Shutdown).unwrap();
15951        driver.run();
15952
15953        match resp_rx.recv().unwrap() {
15954            QueryResponse::InterfaceStats(stats) => {
15955                assert_eq!(stats.probe_responder, None);
15956            }
15957            other => panic!("expected InterfaceStats, got {:?}", other),
15958        }
15959    }
15960
15961    #[test]
15962    fn test_extract_dest_hash_too_short() {
15963        // Packet too short to contain a full dest hash
15964        assert_eq!(super::extract_dest_hash(&[0x00, 0x00, 0xAA]), [0u8; 16]);
15965    }
15966
15967    #[test]
15968    fn test_extract_dest_hash_header1() {
15969        // HEADER_1: bit 6 = 0, dest at bytes 2..18
15970        let mut raw = vec![0x00, 0x00]; // flags (header_type=0), hops
15971        let dest = [0x11; 16];
15972        raw.extend_from_slice(&dest);
15973        raw.extend_from_slice(&[0xFF; 10]); // trailing data
15974        assert_eq!(super::extract_dest_hash(&raw), dest);
15975    }
15976
15977    #[test]
15978    fn test_extract_dest_hash_header2() {
15979        // HEADER_2: bit 6 = 1, transport_id at 2..18, dest at 18..34
15980        let mut raw = vec![0x40, 0x00]; // flags (header_type=1), hops
15981        raw.extend_from_slice(&[0xAA; 16]); // transport_id (bytes 2..18)
15982        let dest = [0x22; 16];
15983        raw.extend_from_slice(&dest); // dest (bytes 18..34)
15984        raw.extend_from_slice(&[0xFF; 10]); // trailing data
15985        assert_eq!(super::extract_dest_hash(&raw), dest);
15986    }
15987
15988    #[test]
15989    fn test_extract_dest_hash_header2_too_short() {
15990        // HEADER_2 packet that's too short for the dest portion
15991        let mut raw = vec![0x40, 0x00];
15992        raw.extend_from_slice(&[0xAA; 16]); // transport_id only, no dest
15993        assert_eq!(super::extract_dest_hash(&raw), [0u8; 16]);
15994    }
15995
15996    #[test]
15997    fn announce_stores_receiving_interface_in_known_destinations() {
15998        // When an announce arrives on interface 1, the AnnouncedIdentity
15999        // stored in known_destinations must have receiving_interface == InterfaceId(1).
16000        let (tx, rx) = event::channel();
16001        let (cbs, _, _, _, _, _) = MockCallbacks::new();
16002        let mut driver = Driver::new(
16003            TransportConfig {
16004                transport_enabled: false,
16005                identity_hash: None,
16006                prefer_shorter_path: false,
16007                max_paths_per_destination: 1,
16008                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
16009                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
16010                max_path_destinations: usize::MAX,
16011                max_tunnel_destinations_total: usize::MAX,
16012                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
16013                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
16014                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
16015                announce_sig_cache_enabled: true,
16016                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
16017                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
16018                announce_queue_max_entries: 256,
16019                announce_queue_max_interfaces: 1024,
16020            },
16021            rx,
16022            tx.clone(),
16023            Box::new(cbs),
16024        );
16025        let info = make_interface_info(1);
16026        driver.engine.register_interface(info);
16027        let (writer, _sent) = MockWriter::new();
16028        driver
16029            .interfaces
16030            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
16031
16032        let identity = Identity::new(&mut OsRng);
16033        let announce_raw = build_announce_packet(&identity);
16034
16035        tx.send(Event::Frame {
16036            interface_id: InterfaceId(1),
16037            data: announce_raw,
16038        })
16039        .unwrap();
16040        tx.send(Event::Shutdown).unwrap();
16041        driver.run();
16042
16043        // The identity should be cached with the correct receiving interface
16044        assert_eq!(driver.known_destinations.len(), 1);
16045        let (_, announced) = driver.known_destinations.iter().next().unwrap();
16046        assert_eq!(
16047            announced.announced.receiving_interface,
16048            InterfaceId(1),
16049            "receiving_interface should match the interface the announce arrived on"
16050        );
16051    }
16052
16053    #[test]
16054    fn announce_on_different_interfaces_stores_correct_id() {
16055        // Announces arriving on interface 2 should store InterfaceId(2).
16056        let (tx, rx) = event::channel();
16057        let (cbs, _, _, _, _, _) = MockCallbacks::new();
16058        let mut driver = Driver::new(
16059            TransportConfig {
16060                transport_enabled: false,
16061                identity_hash: None,
16062                prefer_shorter_path: false,
16063                max_paths_per_destination: 1,
16064                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
16065                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
16066                max_path_destinations: usize::MAX,
16067                max_tunnel_destinations_total: usize::MAX,
16068                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
16069                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
16070                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
16071                announce_sig_cache_enabled: true,
16072                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
16073                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
16074                announce_queue_max_entries: 256,
16075                announce_queue_max_interfaces: 1024,
16076            },
16077            rx,
16078            tx.clone(),
16079            Box::new(cbs),
16080        );
16081        // Register two interfaces
16082        for id in [1, 2] {
16083            driver.engine.register_interface(make_interface_info(id));
16084            let (writer, _) = MockWriter::new();
16085            driver
16086                .interfaces
16087                .insert(InterfaceId(id), make_entry(id, Box::new(writer), true));
16088        }
16089
16090        let identity = Identity::new(&mut OsRng);
16091        let announce_raw = build_announce_packet(&identity);
16092
16093        // Send on interface 2
16094        tx.send(Event::Frame {
16095            interface_id: InterfaceId(2),
16096            data: announce_raw,
16097        })
16098        .unwrap();
16099        tx.send(Event::Shutdown).unwrap();
16100        driver.run();
16101
16102        assert_eq!(driver.known_destinations.len(), 1);
16103        let (_, announced) = driver.known_destinations.iter().next().unwrap();
16104        assert_eq!(announced.announced.receiving_interface, InterfaceId(2));
16105    }
16106
16107    #[test]
16108    fn inject_identity_stores_sentinel_interface() {
16109        // InjectIdentity (used for persistence restore) should store InterfaceId(0)
16110        // because the identity wasn't received from a real interface.
16111        let (tx, rx) = event::channel();
16112        let (cbs, _, _, _, _, _) = MockCallbacks::new();
16113        let mut driver = Driver::new(
16114            TransportConfig {
16115                transport_enabled: false,
16116                identity_hash: None,
16117                prefer_shorter_path: false,
16118                max_paths_per_destination: 1,
16119                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
16120                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
16121                max_path_destinations: usize::MAX,
16122                max_tunnel_destinations_total: usize::MAX,
16123                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
16124                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
16125                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
16126                announce_sig_cache_enabled: true,
16127                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
16128                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
16129                announce_queue_max_entries: 256,
16130                announce_queue_max_interfaces: 1024,
16131            },
16132            rx,
16133            tx.clone(),
16134            Box::new(cbs),
16135        );
16136
16137        let identity = Identity::new(&mut OsRng);
16138        let dest_hash =
16139            rns_core::destination::destination_hash("test", &["app"], Some(identity.hash()));
16140
16141        let (resp_tx, resp_rx) = mpsc::channel();
16142        tx.send(Event::Query(
16143            QueryRequest::InjectIdentity {
16144                dest_hash,
16145                identity_hash: *identity.hash(),
16146                public_key: identity.get_public_key().unwrap(),
16147                app_data: Some(b"restored".to_vec()),
16148                hops: 2,
16149                received_at: 99.0,
16150            },
16151            resp_tx,
16152        ))
16153        .unwrap();
16154        tx.send(Event::Shutdown).unwrap();
16155        driver.run();
16156
16157        match resp_rx.recv().unwrap() {
16158            QueryResponse::InjectIdentity(true) => {}
16159            other => panic!("expected InjectIdentity(true), got {:?}", other),
16160        }
16161
16162        let announced = driver
16163            .known_destinations
16164            .get(&dest_hash)
16165            .expect("identity should be cached");
16166        assert_eq!(
16167            announced.announced.receiving_interface,
16168            InterfaceId(0),
16169            "injected identity should have sentinel InterfaceId(0)"
16170        );
16171        assert_eq!(announced.announced.dest_hash.0, dest_hash);
16172        assert_eq!(announced.announced.identity_hash.0, *identity.hash());
16173        assert_eq!(
16174            announced.announced.public_key,
16175            identity.get_public_key().unwrap()
16176        );
16177        assert_eq!(announced.announced.app_data, Some(b"restored".to_vec()));
16178        assert_eq!(announced.announced.hops, 2);
16179        assert_eq!(announced.announced.received_at, 99.0);
16180    }
16181
16182    #[test]
16183    fn inject_identity_overwrites_previous_entry() {
16184        // A second InjectIdentity for the same dest_hash should overwrite the first.
16185        let (tx, rx) = event::channel();
16186        let (cbs, _, _, _, _, _) = MockCallbacks::new();
16187        let mut driver = Driver::new(
16188            TransportConfig {
16189                transport_enabled: false,
16190                identity_hash: None,
16191                prefer_shorter_path: false,
16192                max_paths_per_destination: 1,
16193                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
16194                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
16195                max_path_destinations: usize::MAX,
16196                max_tunnel_destinations_total: usize::MAX,
16197                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
16198                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
16199                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
16200                announce_sig_cache_enabled: true,
16201                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
16202                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
16203                announce_queue_max_entries: 256,
16204                announce_queue_max_interfaces: 1024,
16205            },
16206            rx,
16207            tx.clone(),
16208            Box::new(cbs),
16209        );
16210
16211        let identity = Identity::new(&mut OsRng);
16212        let dest_hash =
16213            rns_core::destination::destination_hash("test", &["app"], Some(identity.hash()));
16214
16215        // First injection
16216        let (resp_tx1, resp_rx1) = mpsc::channel();
16217        tx.send(Event::Query(
16218            QueryRequest::InjectIdentity {
16219                dest_hash,
16220                identity_hash: *identity.hash(),
16221                public_key: identity.get_public_key().unwrap(),
16222                app_data: Some(b"first".to_vec()),
16223                hops: 1,
16224                received_at: 10.0,
16225            },
16226            resp_tx1,
16227        ))
16228        .unwrap();
16229
16230        // Second injection with different app_data
16231        let (resp_tx2, resp_rx2) = mpsc::channel();
16232        tx.send(Event::Query(
16233            QueryRequest::InjectIdentity {
16234                dest_hash,
16235                identity_hash: *identity.hash(),
16236                public_key: identity.get_public_key().unwrap(),
16237                app_data: Some(b"second".to_vec()),
16238                hops: 3,
16239                received_at: 20.0,
16240            },
16241            resp_tx2,
16242        ))
16243        .unwrap();
16244
16245        tx.send(Event::Shutdown).unwrap();
16246        driver.run();
16247
16248        assert!(matches!(
16249            resp_rx1.recv().unwrap(),
16250            QueryResponse::InjectIdentity(true)
16251        ));
16252        assert!(matches!(
16253            resp_rx2.recv().unwrap(),
16254            QueryResponse::InjectIdentity(true)
16255        ));
16256
16257        // Should have the second injection's data
16258        let announced = driver.known_destinations.get(&dest_hash).unwrap();
16259        assert_eq!(announced.announced.app_data, Some(b"second".to_vec()));
16260        assert_eq!(announced.announced.hops, 3);
16261        assert_eq!(announced.announced.received_at, 20.0);
16262    }
16263
16264    #[test]
16265    fn re_announce_updates_receiving_interface() {
16266        // If we get two announces for the same dest from different interfaces,
16267        // the latest should win (known_destinations is a HashMap keyed by dest_hash).
16268        let (tx, rx) = event::channel();
16269        let (cbs, _, _, _, _, _) = MockCallbacks::new();
16270        let mut driver = Driver::new(
16271            TransportConfig {
16272                transport_enabled: false,
16273                identity_hash: None,
16274                prefer_shorter_path: false,
16275                max_paths_per_destination: 1,
16276                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
16277                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
16278                max_path_destinations: usize::MAX,
16279                max_tunnel_destinations_total: usize::MAX,
16280                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
16281                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
16282                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
16283                announce_sig_cache_enabled: true,
16284                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
16285                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
16286                announce_queue_max_entries: 256,
16287                announce_queue_max_interfaces: 1024,
16288            },
16289            rx,
16290            tx.clone(),
16291            Box::new(cbs),
16292        );
16293        for id in [1, 2] {
16294            driver.engine.register_interface(make_interface_info(id));
16295            let (writer, _) = MockWriter::new();
16296            driver
16297                .interfaces
16298                .insert(InterfaceId(id), make_entry(id, Box::new(writer), true));
16299        }
16300
16301        let identity = Identity::new(&mut OsRng);
16302        let announce_raw = build_announce_packet(&identity);
16303
16304        // Same announce on interface 1, then interface 2
16305        tx.send(Event::Frame {
16306            interface_id: InterfaceId(1),
16307            data: announce_raw.clone(),
16308        })
16309        .unwrap();
16310        // The second announce of the same identity will be dropped by the transport
16311        // engine's deduplication (same random_hash). Build a second identity instead
16312        // to verify the field is correctly set per-announce.
16313        let identity2 = Identity::new(&mut OsRng);
16314        let announce_raw2 = build_announce_packet(&identity2);
16315        tx.send(Event::Frame {
16316            interface_id: InterfaceId(2),
16317            data: announce_raw2,
16318        })
16319        .unwrap();
16320        tx.send(Event::Shutdown).unwrap();
16321        driver.run();
16322
16323        // Both should be cached with their respective interface IDs
16324        assert_eq!(driver.known_destinations.len(), 2);
16325        for (_, announced) in &driver.known_destinations {
16326            // We can't predict ordering, but each should have a valid non-zero interface
16327            assert!(
16328                announced.announced.receiving_interface == InterfaceId(1)
16329                    || announced.announced.receiving_interface == InterfaceId(2)
16330            );
16331        }
16332        // Verify we actually got both interfaces represented
16333        let ifaces: Vec<_> = driver
16334            .known_destinations
16335            .values()
16336            .map(|a| a.announced.receiving_interface)
16337            .collect();
16338        assert!(ifaces.contains(&InterfaceId(1)));
16339        assert!(ifaces.contains(&InterfaceId(2)));
16340    }
16341
16342    #[test]
16343    fn test_extract_dest_hash_other_flags_preserved() {
16344        // Ensure other flag bits don't affect header type detection
16345        // 0x3F = all bits set except bit 6 -> still HEADER_1
16346        let mut raw = vec![0x3F, 0x00];
16347        let dest = [0x33; 16];
16348        raw.extend_from_slice(&dest);
16349        raw.extend_from_slice(&[0xFF; 10]);
16350        assert_eq!(super::extract_dest_hash(&raw), dest);
16351
16352        // 0xFF = all bits set including bit 6 -> HEADER_2
16353        let mut raw2 = vec![0xFF, 0x00];
16354        raw2.extend_from_slice(&[0xBB; 16]); // transport_id
16355        let dest2 = [0x44; 16];
16356        raw2.extend_from_slice(&dest2);
16357        raw2.extend_from_slice(&[0xFF; 10]);
16358        assert_eq!(super::extract_dest_hash(&raw2), dest2);
16359    }
16360}