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    BackbonePeerStateEntry, BlackholeInfo, DrainStatus, Event, EventReceiver,
24    InterfaceStatsResponse, LifecycleState, LocalDestinationEntry, NextHopResponse, PathTableEntry,
25    QueryRequest, QueryResponse, RateTableEntry, RuntimeConfigApplyMode, RuntimeConfigEntry,
26    RuntimeConfigError, RuntimeConfigErrorCode, RuntimeConfigSource, RuntimeConfigValue,
27    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(all(feature = "iface-backbone", target_os = "linux", test))]
36use crate::interface::backbone::{
37    BackboneAbuseConfig, BackboneClientRuntime, BackboneServerRuntime,
38};
39#[cfg(feature = "iface-backbone")]
40use crate::interface::backbone::{
41    BackboneClientRuntimeConfigHandle, BackbonePeerStateHandle, BackboneRuntimeConfigHandle,
42};
43#[cfg(all(feature = "iface-i2p", test))]
44use crate::interface::i2p::I2pRuntime;
45#[cfg(feature = "iface-i2p")]
46use crate::interface::i2p::I2pRuntimeConfigHandle;
47#[cfg(all(feature = "iface-pipe", test))]
48use crate::interface::pipe::PipeRuntime;
49#[cfg(feature = "iface-pipe")]
50use crate::interface::pipe::PipeRuntimeConfigHandle;
51#[cfg(all(feature = "iface-rnode", test))]
52use crate::interface::rnode::RNodeSubConfig;
53#[cfg(feature = "iface-rnode")]
54use crate::interface::rnode::{validate_sub_config, RNodeRuntime, RNodeRuntimeConfigHandle};
55#[cfg(feature = "iface-tcp")]
56use crate::interface::tcp::TcpClientRuntimeConfigHandle;
57#[cfg(all(feature = "iface-tcp", test))]
58use crate::interface::tcp_server::TcpServerRuntime;
59#[cfg(feature = "iface-tcp")]
60use crate::interface::tcp_server::TcpServerRuntimeConfigHandle;
61#[cfg(all(feature = "iface-udp", test))]
62use crate::interface::udp::UdpRuntime;
63#[cfg(feature = "iface-udp")]
64use crate::interface::udp::UdpRuntimeConfigHandle;
65use crate::interface::{InterfaceEntry, InterfaceStats};
66use crate::link_manager::{LinkManager, LinkManagerAction};
67use crate::time;
68
69const DEFAULT_KNOWN_DESTINATIONS_TTL: f64 = 48.0 * 60.0 * 60.0;
70const DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES: usize = 8192;
71const DEFAULT_RATE_LIMITER_TTL_SECS: f64 = 48.0 * 60.0 * 60.0;
72const DEFAULT_TICK_INTERVAL_MS: u64 = 1000;
73const DEFAULT_KNOWN_DESTINATIONS_CLEANUP_INTERVAL_TICKS: u32 = 3600;
74const DEFAULT_ANNOUNCE_CACHE_CLEANUP_INTERVAL_TICKS: u32 = 3600;
75const DEFAULT_ANNOUNCE_CACHE_CLEANUP_BATCH_SIZE: usize = 10_000;
76const DEFAULT_DISCOVERY_CLEANUP_INTERVAL_TICKS: u32 = 3600;
77const DEFAULT_MANAGEMENT_ANNOUNCE_INTERVAL_SECS: f64 = 300.0;
78const SEND_RETRY_BACKOFF_MIN: Duration = Duration::from_millis(25);
79const SEND_RETRY_BACKOFF_MAX: Duration = Duration::from_millis(1000);
80
81fn inject_transport_header(raw: &[u8], next_hop: &[u8; 16]) -> Vec<u8> {
82    if raw.len() < 18 {
83        return raw.to_vec();
84    }
85
86    let new_flags = (rns_core::constants::HEADER_2 << 6)
87        | (rns_core::constants::TRANSPORT_TRANSPORT << 4)
88        | (raw[0] & 0x0F);
89
90    let mut new_raw = Vec::with_capacity(raw.len() + 16);
91    new_raw.push(new_flags);
92    new_raw.push(raw[1]);
93    new_raw.extend_from_slice(next_hop);
94    new_raw.extend_from_slice(&raw[2..]);
95    new_raw
96}
97
98#[derive(Debug, Clone, Copy)]
99pub(crate) struct RuntimeConfigDefaults {
100    pub(crate) tick_interval_ms: u64,
101    pub(crate) known_destinations_ttl: f64,
102    pub(crate) rate_limiter_ttl_secs: f64,
103    pub(crate) known_destinations_cleanup_interval_ticks: u32,
104    pub(crate) announce_cache_cleanup_interval_ticks: u32,
105    pub(crate) announce_cache_cleanup_batch_size: usize,
106    pub(crate) discovery_cleanup_interval_ticks: u32,
107    pub(crate) management_announce_interval_secs: f64,
108    pub(crate) direct_connect_policy: crate::event::HolePunchPolicy,
109    #[cfg(feature = "rns-hooks")]
110    pub(crate) provider_queue_max_events: usize,
111    #[cfg(feature = "rns-hooks")]
112    pub(crate) provider_queue_max_bytes: usize,
113}
114
115#[cfg(feature = "iface-backbone")]
116#[derive(Debug, Clone)]
117pub(crate) struct BackboneDiscoveryRuntime {
118    pub(crate) discoverable: bool,
119    pub(crate) config: crate::discovery::DiscoveryConfig,
120    pub(crate) transport_enabled: bool,
121    pub(crate) ifac_netname: Option<String>,
122    pub(crate) ifac_netkey: Option<String>,
123}
124
125#[cfg(feature = "iface-backbone")]
126#[derive(Debug, Clone)]
127pub(crate) struct BackboneDiscoveryRuntimeHandle {
128    pub(crate) interface_name: String,
129    pub(crate) current: BackboneDiscoveryRuntime,
130    pub(crate) startup: BackboneDiscoveryRuntime,
131}
132
133#[cfg(feature = "iface-tcp")]
134#[derive(Debug, Clone)]
135pub(crate) struct TcpServerDiscoveryRuntime {
136    pub(crate) discoverable: bool,
137    pub(crate) config: crate::discovery::DiscoveryConfig,
138    pub(crate) transport_enabled: bool,
139    pub(crate) ifac_netname: Option<String>,
140    pub(crate) ifac_netkey: Option<String>,
141}
142
143#[cfg(feature = "iface-tcp")]
144#[derive(Debug, Clone)]
145pub(crate) struct TcpServerDiscoveryRuntimeHandle {
146    pub(crate) interface_name: String,
147    pub(crate) current: TcpServerDiscoveryRuntime,
148    pub(crate) startup: TcpServerDiscoveryRuntime,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub(crate) struct IfacRuntimeConfig {
153    pub(crate) netname: Option<String>,
154    pub(crate) netkey: Option<String>,
155    pub(crate) size: usize,
156}
157
158/// Thin wrapper providing `EngineAccess` for a `TransportEngine` + Driver interfaces.
159#[cfg(feature = "rns-hooks")]
160struct EngineRef<'a> {
161    engine: &'a TransportEngine,
162    interfaces: &'a HashMap<InterfaceId, InterfaceEntry>,
163    link_manager: &'a LinkManager,
164    now: f64,
165}
166
167#[cfg(feature = "rns-hooks")]
168impl<'a> EngineAccess for EngineRef<'a> {
169    fn has_path(&self, dest: &[u8; 16]) -> bool {
170        self.engine.has_path(dest)
171    }
172    fn hops_to(&self, dest: &[u8; 16]) -> Option<u8> {
173        self.engine.hops_to(dest)
174    }
175    fn next_hop(&self, dest: &[u8; 16]) -> Option<[u8; 16]> {
176        self.engine.next_hop(dest)
177    }
178    fn is_blackholed(&self, identity: &[u8; 16]) -> bool {
179        self.engine.is_blackholed(identity, self.now)
180    }
181    fn interface_name(&self, id: u64) -> Option<String> {
182        self.interfaces
183            .get(&InterfaceId(id))
184            .map(|e| e.info.name.clone())
185    }
186    fn interface_mode(&self, id: u64) -> Option<u8> {
187        self.interfaces.get(&InterfaceId(id)).map(|e| e.info.mode)
188    }
189    fn identity_hash(&self) -> Option<[u8; 16]> {
190        self.engine.identity_hash().copied()
191    }
192    fn announce_rate(&self, id: u64) -> Option<i32> {
193        self.interfaces
194            .get(&InterfaceId(id))
195            .map(|e| (e.stats.outgoing_announce_freq() * 1000.0) as i32)
196    }
197    fn link_state(&self, link_hash: &[u8; 16]) -> Option<u8> {
198        use rns_core::link::types::LinkState;
199        self.link_manager.link_state(link_hash).map(|s| match s {
200            LinkState::Pending => 0,
201            LinkState::Handshake => 1,
202            LinkState::Active => 2,
203            LinkState::Stale => 3,
204            LinkState::Closed => 4,
205        })
206    }
207}
208
209/// Extract the 16-byte destination hash from a raw packet header.
210///
211/// HEADER_1 (raw[0] & 0x40 == 0): dest at bytes 2..18
212/// HEADER_2 (raw[0] & 0x40 != 0): dest at bytes 18..34 (after transport ID)
213#[cfg(any(test, feature = "rns-hooks"))]
214fn extract_dest_hash(raw: &[u8]) -> [u8; 16] {
215    let mut dest = [0u8; 16];
216    if raw.is_empty() {
217        return dest;
218    }
219    let is_header2 = raw[0] & 0x40 != 0;
220    let start = if is_header2 { 18 } else { 2 };
221    let end = start + 16;
222    if raw.len() >= end {
223        dest.copy_from_slice(&raw[start..end]);
224    }
225    dest
226}
227
228/// Execute a hook chain on disjoint Driver fields (avoids &mut self borrow conflict).
229#[cfg(feature = "rns-hooks")]
230fn run_hook_inner(
231    programs: &mut [rns_hooks::LoadedProgram],
232    hook_manager: &Option<HookManager>,
233    engine_access: &dyn EngineAccess,
234    ctx: &HookContext,
235    now: f64,
236    provider_events_enabled: bool,
237) -> Option<rns_hooks::ExecuteResult> {
238    if programs.is_empty() {
239        return None;
240    }
241    let mgr = hook_manager.as_ref()?;
242    mgr.run_chain_with_provider_events(programs, ctx, engine_access, now, provider_events_enabled)
243}
244
245#[cfg(feature = "rns-hooks")]
246fn backbone_peer_hook_context(event: &BackbonePeerHookEvent) -> HookContext<'_> {
247    HookContext::BackbonePeer {
248        server_interface_id: event.server_interface_id.0,
249        peer_interface_id: event.peer_interface_id.map(|id| id.0),
250        peer_ip: event.peer_ip,
251        peer_port: event.peer_port,
252        connected_for: event.connected_for,
253        had_received_data: event.had_received_data,
254        penalty_level: event.penalty_level,
255        blacklist_for: event.blacklist_for,
256    }
257}
258
259/// Convert a Vec of ActionWire into TransportActions for dispatch.
260#[cfg(feature = "rns-hooks")]
261fn convert_injected_actions(actions: Vec<rns_hooks::ActionWire>) -> Vec<TransportAction> {
262    actions
263        .into_iter()
264        .map(|a| {
265            use rns_hooks::ActionWire;
266            match a {
267                ActionWire::SendOnInterface { interface, raw } => {
268                    TransportAction::SendOnInterface {
269                        interface: InterfaceId(interface),
270                        raw,
271                    }
272                }
273                ActionWire::BroadcastOnAllInterfaces {
274                    raw,
275                    exclude,
276                    has_exclude,
277                } => TransportAction::BroadcastOnAllInterfaces {
278                    raw,
279                    exclude: if has_exclude != 0 {
280                        Some(InterfaceId(exclude))
281                    } else {
282                        None
283                    },
284                },
285                ActionWire::DeliverLocal {
286                    destination_hash,
287                    raw,
288                    packet_hash,
289                    receiving_interface,
290                } => TransportAction::DeliverLocal {
291                    destination_hash,
292                    raw,
293                    packet_hash,
294                    receiving_interface: InterfaceId(receiving_interface),
295                },
296                ActionWire::PathUpdated {
297                    destination_hash,
298                    hops,
299                    next_hop,
300                    interface,
301                } => TransportAction::PathUpdated {
302                    destination_hash,
303                    hops,
304                    next_hop,
305                    interface: InterfaceId(interface),
306                },
307                ActionWire::CacheAnnounce { packet_hash, raw } => {
308                    TransportAction::CacheAnnounce { packet_hash, raw }
309                }
310                ActionWire::TunnelEstablished {
311                    tunnel_id,
312                    interface,
313                } => TransportAction::TunnelEstablished {
314                    tunnel_id,
315                    interface: InterfaceId(interface),
316                },
317                ActionWire::TunnelSynthesize {
318                    interface,
319                    data,
320                    dest_hash,
321                } => TransportAction::TunnelSynthesize {
322                    interface: InterfaceId(interface),
323                    data,
324                    dest_hash,
325                },
326                ActionWire::ForwardToLocalClients {
327                    raw,
328                    exclude,
329                    has_exclude,
330                } => TransportAction::ForwardToLocalClients {
331                    raw,
332                    exclude: if has_exclude != 0 {
333                        Some(InterfaceId(exclude))
334                    } else {
335                        None
336                    },
337                },
338                ActionWire::ForwardPlainBroadcast {
339                    raw,
340                    to_local,
341                    exclude,
342                    has_exclude,
343                } => TransportAction::ForwardPlainBroadcast {
344                    raw,
345                    to_local: to_local != 0,
346                    exclude: if has_exclude != 0 {
347                        Some(InterfaceId(exclude))
348                    } else {
349                        None
350                    },
351                },
352                ActionWire::AnnounceReceived {
353                    destination_hash,
354                    identity_hash,
355                    public_key,
356                    name_hash,
357                    random_hash,
358                    app_data,
359                    hops,
360                    receiving_interface,
361                } => TransportAction::AnnounceReceived {
362                    destination_hash,
363                    identity_hash,
364                    public_key,
365                    name_hash,
366                    random_hash,
367                    app_data,
368                    hops,
369                    receiving_interface: InterfaceId(receiving_interface),
370                },
371            }
372        })
373        .collect()
374}
375
376/// Infer the interface type string from a dynamic interface's name.
377/// Dynamic interfaces (TCP server clients, backbone peers, auto peers, local server clients)
378/// include their type in the name prefix set at construction.
379fn infer_interface_type(name: &str) -> String {
380    if name.starts_with("TCPServerInterface") {
381        "TCPServerClientInterface".to_string()
382    } else if name.starts_with("BackboneInterface") {
383        "BackboneInterface".to_string()
384    } else if name.starts_with("LocalInterface") {
385        "LocalServerClientInterface".to_string()
386    } else {
387        // AutoInterface peers use "{group_name}:{peer_addr}" format where
388        // group_name is the config section name (typically "AutoInterface" or similar).
389        "AutoInterface".to_string()
390    }
391}
392
393pub use crate::common::callbacks::Callbacks;
394
395#[derive(Clone)]
396struct SharedAnnounceRecord {
397    name_hash: [u8; 10],
398    identity_prv_key: [u8; 64],
399    app_data: Option<Vec<u8>>,
400}
401
402/// The driver loop. Owns the engine and all interface entries.
403pub struct Driver {
404    pub(crate) engine: TransportEngine,
405    pub(crate) interfaces: HashMap<InterfaceId, InterfaceEntry>,
406    pub(crate) rng: OsRng,
407    pub(crate) rx: EventReceiver,
408    pub(crate) callbacks: Box<dyn Callbacks>,
409    pub(crate) started: f64,
410    pub(crate) lifecycle_state: LifecycleState,
411    pub(crate) drain_started_at: Option<Instant>,
412    pub(crate) drain_deadline: Option<Instant>,
413    pub(crate) listener_controls: Vec<crate::interface::ListenerControl>,
414    pub(crate) announce_cache: Option<crate::announce_cache::AnnounceCache>,
415    /// Destination hash for rnstransport.tunnel.synthesize (PLAIN).
416    pub(crate) tunnel_synth_dest: [u8; 16],
417    /// Transport identity (optional, needed for tunnel synthesis).
418    pub(crate) transport_identity: Option<rns_crypto::identity::Identity>,
419    /// Link manager: handles link lifecycle, request/response.
420    pub(crate) link_manager: LinkManager,
421    /// Management configuration for ACL checks.
422    pub(crate) management_config: crate::management::ManagementConfig,
423    /// Last time management announces were emitted.
424    pub(crate) last_management_announce: f64,
425    /// Whether initial management announce has been sent (delayed 5s after start).
426    pub(crate) initial_announce_sent: bool,
427    /// Cache of known announced identities, keyed by destination hash.
428    pub(crate) known_destinations: HashMap<[u8; 16], crate::destination::AnnouncedIdentity>,
429    /// TTL for known destinations without an active path, in seconds.
430    pub(crate) known_destinations_ttl: f64,
431    /// Maximum number of retained known destinations.
432    pub(crate) known_destinations_max_entries: usize,
433    /// TTL for announce rate-limiter entries without an active path, in seconds.
434    pub(crate) rate_limiter_ttl_secs: f64,
435    /// Destination hash for rnstransport.path.request (PLAIN).
436    pub(crate) path_request_dest: [u8; 16],
437    /// Proof strategies per destination hash.
438    /// Maps dest_hash → (strategy, optional signing identity for generating proofs).
439    pub(crate) proof_strategies: HashMap<
440        [u8; 16],
441        (
442            rns_core::types::ProofStrategy,
443            Option<rns_crypto::identity::Identity>,
444        ),
445    >,
446    /// Tracked sent packets for proof matching: packet_hash → (dest_hash, sent_time).
447    pub(crate) sent_packets: HashMap<[u8; 32], ([u8; 16], f64)>,
448    /// Completed proofs for probe polling: packet_hash → (rtt_seconds, received_time).
449    pub(crate) completed_proofs: HashMap<[u8; 32], (f64, f64)>,
450    /// Locally registered destinations: hash → dest_type.
451    pub(crate) local_destinations: HashMap<[u8; 16], u8>,
452    /// Latest explicit SINGLE announces to replay after shared-client reconnect.
453    shared_announces: HashMap<[u8; 16], SharedAnnounceRecord>,
454    /// Shared local interfaces that went down and should replay announces on reconnect.
455    shared_reconnect_pending: HashMap<InterfaceId, bool>,
456    /// Hole-punch manager for direct P2P connections.
457    pub(crate) holepunch_manager: HolePunchManager,
458    /// Event sender for worker threads to send results back to the driver loop.
459    pub(crate) event_tx: crate::event::EventSender,
460    /// Maximum queued outbound frames per interface writer worker.
461    pub(crate) interface_writer_queue_capacity: usize,
462    /// Shared timer interval used by the node timer thread.
463    pub(crate) tick_interval_ms: Arc<AtomicU64>,
464    /// Runtime-config handles for backbone server interfaces, keyed by config name.
465    #[cfg(feature = "iface-backbone")]
466    pub(crate) backbone_runtime: HashMap<String, BackboneRuntimeConfigHandle>,
467    /// Live peer-state handles for backbone server interfaces, keyed by config name.
468    #[cfg(feature = "iface-backbone")]
469    pub(crate) backbone_peer_state: HashMap<String, BackbonePeerStateHandle>,
470    /// Runtime-config handles for backbone client interfaces, keyed by config name.
471    #[cfg(feature = "iface-backbone")]
472    pub(crate) backbone_client_runtime: HashMap<String, BackboneClientRuntimeConfigHandle>,
473    /// Runtime-config state for backbone discovery metadata, keyed by config name.
474    #[cfg(feature = "iface-backbone")]
475    pub(crate) backbone_discovery_runtime: HashMap<String, BackboneDiscoveryRuntimeHandle>,
476    /// Runtime-config handles for TCP server interfaces, keyed by config name.
477    #[cfg(feature = "iface-tcp")]
478    pub(crate) tcp_server_runtime: HashMap<String, TcpServerRuntimeConfigHandle>,
479    /// Runtime-config handles for TCP client interfaces, keyed by config name.
480    #[cfg(feature = "iface-tcp")]
481    pub(crate) tcp_client_runtime: HashMap<String, TcpClientRuntimeConfigHandle>,
482    /// Runtime-config state for TCP server discovery metadata, keyed by config name.
483    #[cfg(feature = "iface-tcp")]
484    pub(crate) tcp_server_discovery_runtime: HashMap<String, TcpServerDiscoveryRuntimeHandle>,
485    /// Runtime-config handles for UDP interfaces, keyed by config name.
486    #[cfg(feature = "iface-udp")]
487    pub(crate) udp_runtime: HashMap<String, UdpRuntimeConfigHandle>,
488    /// Runtime-config handles for Auto interfaces, keyed by config name.
489    #[cfg(feature = "iface-auto")]
490    pub(crate) auto_runtime: HashMap<String, AutoRuntimeConfigHandle>,
491    /// Runtime-config handles for I2P interfaces, keyed by config name.
492    #[cfg(feature = "iface-i2p")]
493    pub(crate) i2p_runtime: HashMap<String, I2pRuntimeConfigHandle>,
494    /// Runtime-config handles for Pipe interfaces, keyed by config name.
495    #[cfg(feature = "iface-pipe")]
496    pub(crate) pipe_runtime: HashMap<String, PipeRuntimeConfigHandle>,
497    /// Runtime-config handles for RNode interfaces, keyed by config name.
498    #[cfg(feature = "iface-rnode")]
499    pub(crate) rnode_runtime: HashMap<String, RNodeRuntimeConfigHandle>,
500    /// Startup/default interface metadata for generic cross-cutting runtime config.
501    pub(crate) interface_runtime_defaults:
502        HashMap<String, rns_core::transport::types::InterfaceInfo>,
503    /// Current IFAC runtime config for static interfaces that support IFAC mutation.
504    pub(crate) interface_ifac_runtime: HashMap<String, IfacRuntimeConfig>,
505    /// Startup/default IFAC runtime config for static interfaces.
506    pub(crate) interface_ifac_runtime_defaults: HashMap<String, IfacRuntimeConfig>,
507    /// Storage for discovered interfaces.
508    pub(crate) discovered_interfaces: crate::discovery::DiscoveredInterfaceStorage,
509    /// Required stamp value for accepting discovered interfaces.
510    pub(crate) discovery_required_value: u8,
511    /// Name hash for interface discovery announces ("rnstransport.discovery.interface").
512    pub(crate) discovery_name_hash: [u8; 10],
513    /// Destination hash for the probe responder (if respond_to_probes is enabled).
514    pub(crate) probe_responder_hash: Option<[u8; 16]>,
515    /// Whether interface discovery is enabled.
516    pub(crate) discover_interfaces: bool,
517    /// Announcer for discoverable interfaces (None if nothing to announce).
518    pub(crate) interface_announcer: Option<crate::discovery::InterfaceAnnouncer>,
519    /// Shared async announce verification queue.
520    pub(crate) announce_verify_queue: Arc<Mutex<AnnounceVerifyQueue>>,
521    /// Whether inbound announces should be verified off the driver thread.
522    pub(crate) async_announce_verification: bool,
523    /// Tick counter for periodic discovery cleanup (every ~3600 ticks = ~1 hour).
524    pub(crate) discovery_cleanup_counter: u32,
525    /// Runtime-configurable discovery cleanup interval.
526    pub(crate) discovery_cleanup_interval_ticks: u32,
527    /// Tick counter for periodic MEMSTATS logging (every 300 ticks = ~5 min).
528    pub(crate) memory_stats_counter: u32,
529    /// Tick counter for periodic memory/cache cleanup (every ~3600 ticks = ~1 hour).
530    pub(crate) cache_cleanup_counter: u32,
531    /// Tick counter for incremental announce-cache cleanup scheduling.
532    pub(crate) announce_cache_cleanup_counter: u32,
533    /// Runtime-configurable cleanup interval for known destinations.
534    pub(crate) known_destinations_cleanup_interval_ticks: u32,
535    /// Count of known-destination cap evictions since start.
536    pub(crate) known_destinations_cap_evict_count: usize,
537    /// Runtime-configurable interval for starting announce cache cleanup.
538    pub(crate) announce_cache_cleanup_interval_ticks: u32,
539    /// When set, announce cache cleanup is in progress (contains active packet hashes).
540    pub(crate) cache_cleanup_active_hashes: Option<Vec<[u8; 32]>>,
541    /// Directory iterator for incremental announce cache cleanup.
542    pub(crate) cache_cleanup_entries: Option<std::fs::ReadDir>,
543    /// Running total of files removed during current cache cleanup cycle.
544    pub(crate) cache_cleanup_removed: usize,
545    /// Runtime-configurable announce cache cleanup batch size.
546    pub(crate) announce_cache_cleanup_batch_size: usize,
547    /// Runtime-configurable management announce interval.
548    pub(crate) management_announce_interval_secs: f64,
549    /// Startup/default runtime-config values.
550    pub(crate) runtime_config_defaults: RuntimeConfigDefaults,
551    /// Hook slots for the WASM hook system (one per HookPoint).
552    #[cfg(feature = "rns-hooks")]
553    pub(crate) hook_slots: [HookSlot; HookPoint::COUNT],
554    /// WASM hook manager (runtime + linker). None if initialization failed.
555    #[cfg(feature = "rns-hooks")]
556    pub(crate) hook_manager: Option<HookManager>,
557    #[cfg(feature = "rns-hooks")]
558    pub(crate) provider_bridge: Option<ProviderBridge>,
559}
560
561impl Driver {
562    /// Create a new driver.
563    pub fn new(
564        config: TransportConfig,
565        rx: EventReceiver,
566        tx: crate::event::EventSender,
567        callbacks: Box<dyn Callbacks>,
568    ) -> Self {
569        let announce_queue_max_entries = config.announce_queue_max_entries;
570        let tunnel_synth_dest = rns_core::destination::destination_hash(
571            "rnstransport",
572            &["tunnel", "synthesize"],
573            None,
574        );
575        let path_request_dest =
576            rns_core::destination::destination_hash("rnstransport", &["path", "request"], None);
577        let discovery_name_hash = crate::discovery::discovery_name_hash();
578        let mut engine = TransportEngine::new(config);
579        engine.register_destination(tunnel_synth_dest, rns_core::constants::DESTINATION_PLAIN);
580        // Register path request destination so inbound path requests are delivered locally
581        engine.register_destination(path_request_dest, rns_core::constants::DESTINATION_PLAIN);
582        // Note: discovery destination is NOT registered as local — it's a SINGLE destination
583        // whose hash depends on the sender's identity. We match it by name_hash instead.
584        let mut local_destinations = HashMap::new();
585        local_destinations.insert(tunnel_synth_dest, rns_core::constants::DESTINATION_PLAIN);
586        local_destinations.insert(path_request_dest, rns_core::constants::DESTINATION_PLAIN);
587        let runtime_config_defaults = RuntimeConfigDefaults {
588            tick_interval_ms: DEFAULT_TICK_INTERVAL_MS,
589            known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
590            rate_limiter_ttl_secs: DEFAULT_RATE_LIMITER_TTL_SECS,
591            known_destinations_cleanup_interval_ticks:
592                DEFAULT_KNOWN_DESTINATIONS_CLEANUP_INTERVAL_TICKS,
593            announce_cache_cleanup_interval_ticks: DEFAULT_ANNOUNCE_CACHE_CLEANUP_INTERVAL_TICKS,
594            announce_cache_cleanup_batch_size: DEFAULT_ANNOUNCE_CACHE_CLEANUP_BATCH_SIZE,
595            discovery_cleanup_interval_ticks: DEFAULT_DISCOVERY_CLEANUP_INTERVAL_TICKS,
596            management_announce_interval_secs: DEFAULT_MANAGEMENT_ANNOUNCE_INTERVAL_SECS,
597            direct_connect_policy: crate::event::HolePunchPolicy::default(),
598            #[cfg(feature = "rns-hooks")]
599            provider_queue_max_events: crate::provider_bridge::ProviderBridgeConfig::default()
600                .queue_max_events,
601            #[cfg(feature = "rns-hooks")]
602            provider_queue_max_bytes: crate::provider_bridge::ProviderBridgeConfig::default()
603                .queue_max_bytes,
604        };
605        Driver {
606            engine,
607            interfaces: HashMap::new(),
608            rng: OsRng,
609            rx,
610            callbacks,
611            started: time::now(),
612            lifecycle_state: LifecycleState::Active,
613            drain_started_at: None,
614            drain_deadline: None,
615            listener_controls: Vec::new(),
616            announce_cache: None,
617            tunnel_synth_dest,
618            transport_identity: None,
619            link_manager: LinkManager::new(),
620            management_config: Default::default(),
621            last_management_announce: 0.0,
622            initial_announce_sent: false,
623            known_destinations: HashMap::new(),
624            known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
625            known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
626            rate_limiter_ttl_secs: DEFAULT_RATE_LIMITER_TTL_SECS,
627            path_request_dest,
628            proof_strategies: HashMap::new(),
629            sent_packets: HashMap::new(),
630            completed_proofs: HashMap::new(),
631            local_destinations,
632            shared_announces: HashMap::new(),
633            shared_reconnect_pending: HashMap::new(),
634            holepunch_manager: HolePunchManager::new(
635                vec![],
636                rns_core::holepunch::ProbeProtocol::Rnsp,
637                None,
638            ),
639            event_tx: tx,
640            interface_writer_queue_capacity: crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
641            tick_interval_ms: Arc::new(AtomicU64::new(DEFAULT_TICK_INTERVAL_MS)),
642            #[cfg(feature = "iface-backbone")]
643            backbone_runtime: HashMap::new(),
644            #[cfg(feature = "iface-backbone")]
645            backbone_peer_state: HashMap::new(),
646            #[cfg(feature = "iface-backbone")]
647            backbone_client_runtime: HashMap::new(),
648            #[cfg(feature = "iface-backbone")]
649            backbone_discovery_runtime: HashMap::new(),
650            #[cfg(feature = "iface-tcp")]
651            tcp_server_runtime: HashMap::new(),
652            #[cfg(feature = "iface-tcp")]
653            tcp_client_runtime: HashMap::new(),
654            #[cfg(feature = "iface-tcp")]
655            tcp_server_discovery_runtime: HashMap::new(),
656            #[cfg(feature = "iface-udp")]
657            udp_runtime: HashMap::new(),
658            #[cfg(feature = "iface-auto")]
659            auto_runtime: HashMap::new(),
660            #[cfg(feature = "iface-i2p")]
661            i2p_runtime: HashMap::new(),
662            #[cfg(feature = "iface-pipe")]
663            pipe_runtime: HashMap::new(),
664            #[cfg(feature = "iface-rnode")]
665            rnode_runtime: HashMap::new(),
666            interface_runtime_defaults: HashMap::new(),
667            interface_ifac_runtime: HashMap::new(),
668            interface_ifac_runtime_defaults: HashMap::new(),
669            discovered_interfaces: crate::discovery::DiscoveredInterfaceStorage::new(
670                std::env::temp_dir().join("rns-discovered-interfaces"),
671            ),
672            discovery_required_value: crate::discovery::DEFAULT_STAMP_VALUE,
673            discovery_name_hash,
674            probe_responder_hash: None,
675            discover_interfaces: false,
676            interface_announcer: None,
677            announce_verify_queue: Arc::new(Mutex::new(AnnounceVerifyQueue::new(
678                announce_queue_max_entries,
679            ))),
680            async_announce_verification: false,
681            discovery_cleanup_counter: 0,
682            discovery_cleanup_interval_ticks: runtime_config_defaults
683                .discovery_cleanup_interval_ticks,
684            memory_stats_counter: 0,
685            cache_cleanup_counter: 0,
686            announce_cache_cleanup_counter: 0,
687            known_destinations_cleanup_interval_ticks: runtime_config_defaults
688                .known_destinations_cleanup_interval_ticks,
689            known_destinations_cap_evict_count: 0,
690            announce_cache_cleanup_interval_ticks: runtime_config_defaults
691                .announce_cache_cleanup_interval_ticks,
692            cache_cleanup_active_hashes: None,
693            cache_cleanup_entries: None,
694            cache_cleanup_removed: 0,
695            announce_cache_cleanup_batch_size: runtime_config_defaults
696                .announce_cache_cleanup_batch_size,
697            management_announce_interval_secs: runtime_config_defaults
698                .management_announce_interval_secs,
699            runtime_config_defaults,
700            #[cfg(feature = "rns-hooks")]
701            hook_slots: create_hook_slots(),
702            #[cfg(feature = "rns-hooks")]
703            hook_manager: HookManager::new().ok(),
704            #[cfg(feature = "rns-hooks")]
705            provider_bridge: None,
706        }
707    }
708
709    pub fn set_announce_verify_queue_config(
710        &mut self,
711        max_entries: usize,
712        max_bytes: usize,
713        max_stale_secs: f64,
714        overflow_policy: OverflowPolicy,
715    ) {
716        self.announce_verify_queue = Arc::new(Mutex::new(AnnounceVerifyQueue::with_limits(
717            max_entries,
718            max_bytes,
719            max_stale_secs,
720            overflow_policy,
721        )));
722    }
723
724    fn wrap_interface_writer(
725        &self,
726        interface_id: InterfaceId,
727        interface_name: &str,
728        writer: Box<dyn crate::interface::Writer>,
729    ) -> (
730        Box<dyn crate::interface::Writer>,
731        crate::interface::AsyncWriterMetrics,
732    ) {
733        crate::interface::wrap_async_writer(
734            writer,
735            interface_id,
736            interface_name,
737            self.event_tx.clone(),
738            self.interface_writer_queue_capacity,
739        )
740    }
741
742    fn upsert_known_destination(
743        &mut self,
744        dest_hash: [u8; 16],
745        announced: crate::destination::AnnouncedIdentity,
746    ) {
747        if let Some(existing) = self.known_destinations.get_mut(&dest_hash) {
748            *existing = announced;
749            return;
750        }
751
752        self.enforce_known_destination_cap(true);
753        self.known_destinations.insert(dest_hash, announced);
754    }
755
756    fn begin_drain(&mut self, timeout: Duration) {
757        let now = Instant::now();
758        let deadline = now + timeout;
759        match self.lifecycle_state {
760            LifecycleState::Active => {
761                self.lifecycle_state = LifecycleState::Draining;
762                self.drain_started_at = Some(now);
763                self.drain_deadline = Some(deadline);
764                log::info!(
765                    "driver entering drain mode with {:.3}s timeout",
766                    timeout.as_secs_f64()
767                );
768                self.stop_listener_accepts();
769            }
770            LifecycleState::Draining => {
771                self.drain_deadline = Some(deadline);
772                log::info!(
773                    "driver drain deadline updated to {:.3}s from now",
774                    timeout.as_secs_f64()
775                );
776                self.stop_listener_accepts();
777            }
778            LifecycleState::Stopping | LifecycleState::Stopped => {
779                log::debug!(
780                    "ignoring BeginDrain while lifecycle state is {:?}",
781                    self.lifecycle_state
782                );
783            }
784        }
785    }
786
787    fn is_draining(&self) -> bool {
788        matches!(self.lifecycle_state, LifecycleState::Draining)
789    }
790
791    pub fn register_listener_control(&mut self, control: crate::interface::ListenerControl) {
792        self.listener_controls.push(control);
793    }
794
795    fn stop_listener_accepts(&mut self) {
796        for control in &self.listener_controls {
797            control.request_stop();
798        }
799        #[cfg(feature = "rns-hooks")]
800        if let Some(bridge) = self.provider_bridge.as_ref() {
801            bridge.stop_accepting();
802        }
803    }
804
805    fn reject_new_work(&self, op: &str) {
806        log::info!("rejecting {} while node is draining", op);
807    }
808
809    fn drain_error(&self, op: &str) -> String {
810        format!("cannot {} while node is draining", op)
811    }
812
813    fn drain_status(&self) -> DrainStatus {
814        let now = Instant::now();
815        let active_links = self.link_manager.link_count();
816        let active_resource_transfers = self.link_manager.resource_transfer_count();
817        let active_holepunch_sessions = self.holepunch_manager.session_count();
818        let interface_writer_queued_frames = self
819            .interfaces
820            .values()
821            .map(|entry| {
822                entry
823                    .async_writer_metrics
824                    .as_ref()
825                    .map(|metrics| metrics.queued_frames())
826                    .unwrap_or(0)
827            })
828            .sum();
829        #[cfg(feature = "rns-hooks")]
830        let (provider_backlog_events, provider_consumer_queued_events) = self
831            .provider_bridge
832            .as_ref()
833            .map(|bridge| {
834                let stats = bridge.stats();
835                (
836                    stats.backlog_len,
837                    stats
838                        .consumers
839                        .iter()
840                        .map(|consumer| consumer.queue_len)
841                        .sum(),
842                )
843            })
844            .unwrap_or((0, 0));
845        #[cfg(not(feature = "rns-hooks"))]
846        let (provider_backlog_events, provider_consumer_queued_events) = (0, 0);
847        let drain_age_seconds = self
848            .drain_started_at
849            .map(|started| started.elapsed().as_secs_f64());
850        let deadline_remaining_seconds = self.drain_deadline.map(|deadline| {
851            deadline
852                .checked_duration_since(now)
853                .map(|remaining| remaining.as_secs_f64())
854                .unwrap_or(0.0)
855        });
856        let detail = match self.lifecycle_state {
857            LifecycleState::Active => Some("node is accepting normal work".into()),
858            LifecycleState::Draining => {
859                let mut remaining = Vec::new();
860                if active_links > 0 {
861                    remaining.push(format!("{active_links} link(s)"));
862                }
863                if active_resource_transfers > 0 {
864                    remaining.push(format!("{active_resource_transfers} resource transfer(s)"));
865                }
866                if active_holepunch_sessions > 0 {
867                    remaining.push(format!("{active_holepunch_sessions} hole-punch session(s)"));
868                }
869                if interface_writer_queued_frames > 0 {
870                    remaining.push(format!(
871                        "{interface_writer_queued_frames} queued interface writer frame(s)"
872                    ));
873                }
874                if provider_backlog_events > 0 {
875                    remaining.push(format!(
876                        "{provider_backlog_events} provider backlog event(s)"
877                    ));
878                }
879                if provider_consumer_queued_events > 0 {
880                    remaining.push(format!(
881                        "{provider_consumer_queued_events} queued provider consumer event(s)"
882                    ));
883                }
884                Some(if remaining.is_empty() {
885                    "node is draining existing work; no active links, resource transfers, hole-punch sessions, or queued writer/provider work remain".into()
886                } else {
887                    format!(
888                        "node is draining existing work; {} still active",
889                        remaining.join(", ")
890                    )
891                })
892            }
893            LifecycleState::Stopping => Some("node is tearing down remaining work".into()),
894            LifecycleState::Stopped => Some("node is stopped".into()),
895        };
896
897        DrainStatus {
898            state: self.lifecycle_state,
899            drain_age_seconds,
900            deadline_remaining_seconds,
901            drain_complete: !matches!(self.lifecycle_state, LifecycleState::Draining)
902                || (active_links == 0
903                    && active_resource_transfers == 0
904                    && active_holepunch_sessions == 0
905                    && interface_writer_queued_frames == 0
906                    && provider_backlog_events == 0
907                    && provider_consumer_queued_events == 0),
908            interface_writer_queued_frames,
909            provider_backlog_events,
910            provider_consumer_queued_events,
911            detail,
912        }
913    }
914
915    fn enforce_drain_deadline(&mut self) {
916        if !matches!(self.lifecycle_state, LifecycleState::Draining) {
917            return;
918        }
919        let Some(deadline) = self.drain_deadline else {
920            return;
921        };
922        if Instant::now() < deadline {
923            return;
924        }
925
926        log::info!("driver drain deadline reached; tearing down remaining links");
927        self.lifecycle_state = LifecycleState::Stopping;
928        let resource_actions = self.link_manager.cancel_all_resources(&mut self.rng);
929        self.dispatch_link_actions(resource_actions);
930        let link_actions = self.link_manager.teardown_all_links();
931        self.dispatch_link_actions(link_actions);
932        let cleanup_actions = self.link_manager.tick(&mut self.rng);
933        self.dispatch_link_actions(cleanup_actions);
934        self.holepunch_manager.abort_all_sessions();
935    }
936
937    fn enforce_known_destination_cap(&mut self, for_insert: bool) -> usize {
938        if self.known_destinations_max_entries == usize::MAX {
939            return 0;
940        }
941
942        let mut evicted = 0usize;
943        while if for_insert {
944            self.known_destinations.len() >= self.known_destinations_max_entries
945        } else {
946            self.known_destinations.len() > self.known_destinations_max_entries
947        } {
948            let active_dests = self.engine.active_destination_hashes();
949            let candidate = self
950                .oldest_known_destination(false, &active_dests)
951                .or_else(|| self.oldest_known_destination(true, &active_dests));
952            let Some(dest_hash) = candidate else {
953                break;
954            };
955            if self.known_destinations.remove(&dest_hash).is_some() {
956                evicted += 1;
957                self.known_destinations_cap_evict_count += 1;
958            } else {
959                break;
960            }
961        }
962        evicted
963    }
964
965    fn oldest_known_destination(
966        &self,
967        include_protected: bool,
968        active_dests: &std::collections::BTreeSet<[u8; 16]>,
969    ) -> Option<[u8; 16]> {
970        self.known_destinations
971            .iter()
972            .filter(|(dest_hash, _)| {
973                include_protected
974                    || (!active_dests.contains(*dest_hash)
975                        && !self.local_destinations.contains_key(*dest_hash))
976            })
977            .min_by(|a, b| {
978                a.1.received_at
979                    .partial_cmp(&b.1.received_at)
980                    .unwrap_or(std::cmp::Ordering::Equal)
981                    .then_with(|| a.0.cmp(b.0))
982            })
983            .map(|(dest_hash, _)| *dest_hash)
984    }
985
986    #[cfg(feature = "rns-hooks")]
987    fn provider_events_enabled(&self) -> bool {
988        self.provider_bridge.is_some()
989    }
990
991    #[cfg(feature = "rns-hooks")]
992    fn run_backbone_peer_hook(
993        &mut self,
994        attach_point: &str,
995        point: HookPoint,
996        event: &BackbonePeerHookEvent,
997    ) {
998        let ctx = backbone_peer_hook_context(event);
999        let now = time::now();
1000        let engine_ref = EngineRef {
1001            engine: &self.engine,
1002            interfaces: &self.interfaces,
1003            link_manager: &self.link_manager,
1004            now,
1005        };
1006        let provider_events_enabled = self.provider_events_enabled();
1007        if let Some(ref e) = run_hook_inner(
1008            &mut self.hook_slots[point as usize].programs,
1009            &self.hook_manager,
1010            &engine_ref,
1011            &ctx,
1012            now,
1013            provider_events_enabled,
1014        ) {
1015            self.forward_hook_side_effects(attach_point, e);
1016        }
1017    }
1018
1019    #[cfg(feature = "iface-backbone")]
1020    fn make_discoverable_interface(
1021        runtime: &BackboneDiscoveryRuntimeHandle,
1022    ) -> crate::discovery::DiscoverableInterface {
1023        crate::discovery::DiscoverableInterface {
1024            interface_name: runtime.interface_name.clone(),
1025            config: runtime.current.config.clone(),
1026            transport_enabled: runtime.current.transport_enabled,
1027            ifac_netname: runtime.current.ifac_netname.clone(),
1028            ifac_netkey: runtime.current.ifac_netkey.clone(),
1029        }
1030    }
1031
1032    #[cfg(feature = "iface-backbone")]
1033    fn sync_backbone_discovery_runtime(
1034        &mut self,
1035        interface_name: &str,
1036    ) -> Result<(), RuntimeConfigError> {
1037        let handle = self
1038            .backbone_discovery_runtime
1039            .get(interface_name)
1040            .ok_or(RuntimeConfigError {
1041                code: RuntimeConfigErrorCode::NotFound,
1042                message: format!("backbone interface '{}' not found", interface_name),
1043            })?
1044            .clone();
1045
1046        if handle.current.discoverable {
1047            let iface = Self::make_discoverable_interface(&handle);
1048            if let Some(announcer) = self.interface_announcer.as_mut() {
1049                announcer.upsert_interface(iface);
1050            } else if let Some(identity) = self.transport_identity.as_ref() {
1051                self.interface_announcer = Some(crate::discovery::InterfaceAnnouncer::new(
1052                    *identity.hash(),
1053                    vec![iface],
1054                ));
1055            }
1056        } else if let Some(announcer) = self.interface_announcer.as_mut() {
1057            announcer.remove_interface(interface_name);
1058            if announcer.is_empty() {
1059                self.interface_announcer = None;
1060            }
1061        }
1062
1063        Ok(())
1064    }
1065
1066    #[cfg(feature = "iface-tcp")]
1067    fn make_tcp_server_discoverable_interface(
1068        runtime: &TcpServerDiscoveryRuntimeHandle,
1069    ) -> crate::discovery::DiscoverableInterface {
1070        crate::discovery::DiscoverableInterface {
1071            interface_name: runtime.interface_name.clone(),
1072            config: runtime.current.config.clone(),
1073            transport_enabled: runtime.current.transport_enabled,
1074            ifac_netname: runtime.current.ifac_netname.clone(),
1075            ifac_netkey: runtime.current.ifac_netkey.clone(),
1076        }
1077    }
1078
1079    #[cfg(feature = "iface-tcp")]
1080    fn sync_tcp_server_discovery_runtime(
1081        &mut self,
1082        interface_name: &str,
1083    ) -> Result<(), RuntimeConfigError> {
1084        let handle = self
1085            .tcp_server_discovery_runtime
1086            .get(interface_name)
1087            .ok_or(RuntimeConfigError {
1088                code: RuntimeConfigErrorCode::NotFound,
1089                message: format!("tcp server interface '{}' not found", interface_name),
1090            })?
1091            .clone();
1092
1093        if handle.current.discoverable {
1094            let iface = Self::make_tcp_server_discoverable_interface(&handle);
1095            if let Some(announcer) = self.interface_announcer.as_mut() {
1096                announcer.upsert_interface(iface);
1097            } else if let Some(identity) = self.transport_identity.as_ref() {
1098                self.interface_announcer = Some(crate::discovery::InterfaceAnnouncer::new(
1099                    *identity.hash(),
1100                    vec![iface],
1101                ));
1102            }
1103        } else if let Some(announcer) = self.interface_announcer.as_mut() {
1104            announcer.remove_interface(interface_name);
1105            if announcer.is_empty() {
1106                self.interface_announcer = None;
1107            }
1108        }
1109
1110        Ok(())
1111    }
1112
1113    #[cfg(feature = "rns-hooks")]
1114    fn update_hook_program<F>(
1115        &mut self,
1116        name: &str,
1117        attach_point: &str,
1118        mut update: F,
1119    ) -> Result<(), String>
1120    where
1121        F: FnMut(&mut rns_hooks::LoadedProgram),
1122    {
1123        let point_idx = crate::config::parse_hook_point(attach_point)
1124            .ok_or_else(|| format!("unknown hook point '{}'", attach_point))?;
1125        let program = self.hook_slots[point_idx]
1126            .programs
1127            .iter_mut()
1128            .find(|program| program.name == name)
1129            .ok_or_else(|| format!("hook '{}' not found at point '{}'", name, attach_point))?;
1130        update(program);
1131        Ok(())
1132    }
1133
1134    pub(crate) fn set_tick_interval_handle(&mut self, tick_interval_ms: Arc<AtomicU64>) {
1135        self.tick_interval_ms = tick_interval_ms;
1136    }
1137
1138    pub(crate) fn set_packet_hashlist_max_entries(&mut self, max_entries: usize) {
1139        self.engine.set_packet_hashlist_max_entries(max_entries);
1140    }
1141
1142    fn build_shared_announce_raw(
1143        &mut self,
1144        dest_hash: &[u8; 16],
1145        record: &SharedAnnounceRecord,
1146        path_response: bool,
1147    ) -> Option<Vec<u8>> {
1148        let identity = rns_crypto::identity::Identity::from_private_key(&record.identity_prv_key);
1149
1150        let mut random_hash = [0u8; 10];
1151        self.rng.fill_bytes(&mut random_hash[..5]);
1152        let now_secs = std::time::SystemTime::now()
1153            .duration_since(std::time::UNIX_EPOCH)
1154            .ok()?
1155            .as_secs();
1156        random_hash[5..10].copy_from_slice(&now_secs.to_be_bytes()[3..8]);
1157
1158        let (announce_data, _has_ratchet) = rns_core::announce::AnnounceData::pack(
1159            &identity,
1160            dest_hash,
1161            &record.name_hash,
1162            &random_hash,
1163            None,
1164            record.app_data.as_deref(),
1165        )
1166        .ok()?;
1167
1168        let flags = rns_core::packet::PacketFlags {
1169            header_type: rns_core::constants::HEADER_1,
1170            context_flag: rns_core::constants::FLAG_UNSET,
1171            transport_type: rns_core::constants::TRANSPORT_BROADCAST,
1172            destination_type: rns_core::constants::DESTINATION_SINGLE,
1173            packet_type: rns_core::constants::PACKET_TYPE_ANNOUNCE,
1174        };
1175        let context = if path_response {
1176            rns_core::constants::CONTEXT_PATH_RESPONSE
1177        } else {
1178            rns_core::constants::CONTEXT_NONE
1179        };
1180
1181        rns_core::packet::RawPacket::pack(flags, 0, dest_hash, None, context, &announce_data)
1182            .ok()
1183            .map(|packet| packet.raw)
1184    }
1185
1186    fn replay_shared_announces(&mut self) {
1187        let records: Vec<([u8; 16], SharedAnnounceRecord)> = self
1188            .shared_announces
1189            .iter()
1190            .map(|(dest_hash, record)| (*dest_hash, record.clone()))
1191            .collect();
1192        for (dest_hash, record) in records {
1193            if let Some(raw) = self.build_shared_announce_raw(&dest_hash, &record, true) {
1194                let event = Event::SendOutbound {
1195                    raw,
1196                    dest_type: rns_core::constants::DESTINATION_SINGLE,
1197                    attached_interface: None,
1198                };
1199                match event {
1200                    Event::SendOutbound {
1201                        raw,
1202                        dest_type,
1203                        attached_interface,
1204                    } => match RawPacket::unpack(&raw) {
1205                        Ok(packet) => {
1206                            let actions = self.engine.handle_outbound(
1207                                &packet,
1208                                dest_type,
1209                                attached_interface,
1210                                time::now(),
1211                            );
1212                            self.dispatch_all(actions);
1213                        }
1214                        Err(e) => {
1215                            log::warn!(
1216                                "Shared announce replay failed for {:02x?}: {:?}",
1217                                &dest_hash[..4],
1218                                e
1219                            );
1220                        }
1221                    },
1222                    _ => unreachable!(),
1223                }
1224            }
1225        }
1226    }
1227
1228    fn handle_shared_interface_down(&mut self, id: InterfaceId) {
1229        let dropped_paths = self.engine.drop_paths_for_interface(id);
1230        let dropped_reverse = self.engine.drop_reverse_for_interface(id);
1231        let dropped_links = self.engine.drop_links_for_interface(id);
1232        self.engine.drop_announce_queues();
1233        let link_actions = self.link_manager.teardown_all_links();
1234        self.dispatch_link_actions(link_actions);
1235        self.shared_reconnect_pending.insert(id, true);
1236        log::info!(
1237            "[{}] cleared shared state: {} paths, {} reverse entries, {} transport links",
1238            id.0,
1239            dropped_paths,
1240            dropped_reverse,
1241            dropped_links
1242        );
1243    }
1244
1245    #[cfg(feature = "iface-backbone")]
1246    pub(crate) fn register_backbone_runtime(&mut self, handle: BackboneRuntimeConfigHandle) {
1247        self.backbone_runtime
1248            .insert(handle.interface_name.clone(), handle);
1249    }
1250
1251    #[cfg(feature = "iface-backbone")]
1252    pub(crate) fn register_backbone_peer_state(&mut self, handle: BackbonePeerStateHandle) {
1253        self.backbone_peer_state
1254            .insert(handle.interface_name.clone(), handle);
1255    }
1256
1257    #[cfg(feature = "iface-backbone")]
1258    pub(crate) fn register_backbone_client_runtime(
1259        &mut self,
1260        handle: BackboneClientRuntimeConfigHandle,
1261    ) {
1262        self.backbone_client_runtime
1263            .insert(handle.interface_name.clone(), handle);
1264    }
1265
1266    #[cfg(feature = "iface-backbone")]
1267    pub(crate) fn register_backbone_discovery_runtime(
1268        &mut self,
1269        handle: BackboneDiscoveryRuntimeHandle,
1270    ) {
1271        self.backbone_discovery_runtime
1272            .insert(handle.interface_name.clone(), handle);
1273    }
1274
1275    #[cfg(feature = "iface-backbone")]
1276    fn list_backbone_peer_state(
1277        &self,
1278        interface_name: Option<&str>,
1279    ) -> Vec<BackbonePeerStateEntry> {
1280        let mut names: Vec<&String> = match interface_name {
1281            Some(name) => self
1282                .backbone_peer_state
1283                .keys()
1284                .filter(|candidate| candidate.as_str() == name)
1285                .collect(),
1286            None => self.backbone_peer_state.keys().collect(),
1287        };
1288        names.sort();
1289
1290        let mut entries = Vec::new();
1291        for name in names {
1292            if let Some(handle) = self.backbone_peer_state.get(name) {
1293                entries.extend(handle.peer_state.lock().unwrap().list(name));
1294            }
1295        }
1296        entries.sort_by(|a, b| {
1297            a.interface_name
1298                .cmp(&b.interface_name)
1299                .then_with(|| a.peer_ip.cmp(&b.peer_ip))
1300        });
1301        entries
1302    }
1303
1304    #[cfg(feature = "iface-backbone")]
1305    fn list_backbone_interfaces(&self) -> Vec<crate::event::BackboneInterfaceEntry> {
1306        let mut entries: Vec<_> = self
1307            .backbone_peer_state
1308            .values()
1309            .map(|handle| crate::event::BackboneInterfaceEntry {
1310                interface_id: handle.interface_id,
1311                interface_name: handle.interface_name.clone(),
1312            })
1313            .collect();
1314        entries.sort_by(|a, b| a.interface_name.cmp(&b.interface_name));
1315        entries
1316    }
1317
1318    #[cfg(feature = "iface-backbone")]
1319    fn clear_backbone_peer_state(
1320        &mut self,
1321        interface_name: &str,
1322        peer_ip: std::net::IpAddr,
1323    ) -> bool {
1324        self.backbone_peer_state
1325            .get(interface_name)
1326            .map(|handle| handle.peer_state.lock().unwrap().clear(peer_ip))
1327            .unwrap_or(false)
1328    }
1329
1330    fn blacklist_backbone_peer(
1331        &mut self,
1332        interface_name: &str,
1333        peer_ip: std::net::IpAddr,
1334        duration: std::time::Duration,
1335        reason: String,
1336        penalty_level: u8,
1337    ) -> bool {
1338        let capped_duration = self
1339            .backbone_runtime
1340            .get(interface_name)
1341            .and_then(|handle| {
1342                handle
1343                    .runtime
1344                    .lock()
1345                    .ok()
1346                    .map(|runtime| runtime.abuse.max_penalty_duration)
1347            })
1348            .flatten()
1349            .map(|max| duration.min(max))
1350            .unwrap_or(duration);
1351        let Some(handle) = self.backbone_peer_state.get(interface_name) else {
1352            return false;
1353        };
1354        let ok = handle
1355            .peer_state
1356            .lock()
1357            .unwrap()
1358            .blacklist(peer_ip, capped_duration, reason);
1359        if ok {
1360            #[cfg(feature = "rns-hooks")]
1361            self.run_backbone_peer_hook(
1362                "BackbonePeerPenalty",
1363                HookPoint::BackbonePeerPenalty,
1364                &BackbonePeerHookEvent {
1365                    server_interface_id: self
1366                        .interfaces
1367                        .iter()
1368                        .find(|(_, entry)| entry.info.name == interface_name)
1369                        .map(|(id, _)| *id)
1370                        .unwrap_or(InterfaceId(0)),
1371                    peer_interface_id: None,
1372                    peer_ip,
1373                    peer_port: 0,
1374                    connected_for: Duration::ZERO,
1375                    had_received_data: false,
1376                    penalty_level,
1377                    blacklist_for: capped_duration,
1378                },
1379            );
1380            #[cfg(not(feature = "rns-hooks"))]
1381            let _ = (peer_ip, capped_duration, penalty_level);
1382        }
1383        ok
1384    }
1385
1386    #[cfg(feature = "iface-tcp")]
1387    pub(crate) fn register_tcp_server_runtime(&mut self, handle: TcpServerRuntimeConfigHandle) {
1388        self.tcp_server_runtime
1389            .insert(handle.interface_name.clone(), handle);
1390    }
1391
1392    #[cfg(feature = "iface-tcp")]
1393    pub(crate) fn register_tcp_client_runtime(&mut self, handle: TcpClientRuntimeConfigHandle) {
1394        self.tcp_client_runtime
1395            .insert(handle.interface_name.clone(), handle);
1396    }
1397
1398    #[cfg(feature = "iface-tcp")]
1399    pub(crate) fn register_tcp_server_discovery_runtime(
1400        &mut self,
1401        handle: TcpServerDiscoveryRuntimeHandle,
1402    ) {
1403        self.tcp_server_discovery_runtime
1404            .insert(handle.interface_name.clone(), handle);
1405    }
1406
1407    #[cfg(feature = "iface-udp")]
1408    pub(crate) fn register_udp_runtime(&mut self, handle: UdpRuntimeConfigHandle) {
1409        self.udp_runtime
1410            .insert(handle.interface_name.clone(), handle);
1411    }
1412
1413    #[cfg(feature = "iface-auto")]
1414    pub(crate) fn register_auto_runtime(&mut self, handle: AutoRuntimeConfigHandle) {
1415        self.auto_runtime
1416            .insert(handle.interface_name.clone(), handle);
1417    }
1418
1419    #[cfg(feature = "iface-i2p")]
1420    pub(crate) fn register_i2p_runtime(&mut self, handle: I2pRuntimeConfigHandle) {
1421        self.i2p_runtime
1422            .insert(handle.interface_name.clone(), handle);
1423    }
1424
1425    #[cfg(feature = "iface-pipe")]
1426    pub(crate) fn register_pipe_runtime(&mut self, handle: PipeRuntimeConfigHandle) {
1427        self.pipe_runtime
1428            .insert(handle.interface_name.clone(), handle);
1429    }
1430
1431    #[cfg(feature = "iface-rnode")]
1432    pub(crate) fn register_rnode_runtime(&mut self, handle: RNodeRuntimeConfigHandle) {
1433        self.rnode_runtime
1434            .insert(handle.interface_name.clone(), handle);
1435    }
1436
1437    pub(crate) fn register_interface_runtime_defaults(
1438        &mut self,
1439        info: &rns_core::transport::types::InterfaceInfo,
1440    ) {
1441        self.interface_runtime_defaults
1442            .entry(info.name.clone())
1443            .or_insert_with(|| info.clone());
1444    }
1445
1446    pub(crate) fn register_interface_ifac_runtime(
1447        &mut self,
1448        interface_name: &str,
1449        startup: IfacRuntimeConfig,
1450    ) {
1451        self.interface_ifac_runtime_defaults
1452            .entry(interface_name.to_string())
1453            .or_insert_with(|| startup.clone());
1454        self.interface_ifac_runtime
1455            .entry(interface_name.to_string())
1456            .or_insert(startup);
1457    }
1458
1459    fn runtime_config_entry(&self, key: &str) -> Option<RuntimeConfigEntry> {
1460        let defaults = self.runtime_config_defaults;
1461        let make_entry = |key: &str,
1462                          value: RuntimeConfigValue,
1463                          default: RuntimeConfigValue,
1464                          apply_mode: RuntimeConfigApplyMode,
1465                          description: &str| RuntimeConfigEntry {
1466            key: key.to_string(),
1467            source: if value == default {
1468                RuntimeConfigSource::Startup
1469            } else {
1470                RuntimeConfigSource::RuntimeOverride
1471            },
1472            value,
1473            default,
1474            apply_mode,
1475            description: Some(description.to_string()),
1476        };
1477
1478        match key {
1479            "global.tick_interval_ms" => Some(make_entry(
1480                key,
1481                RuntimeConfigValue::Int(self.tick_interval_ms.load(Ordering::Relaxed) as i64),
1482                RuntimeConfigValue::Int(defaults.tick_interval_ms as i64),
1483                RuntimeConfigApplyMode::Immediate,
1484                "Driver tick interval in milliseconds.",
1485            )),
1486            "global.known_destinations_ttl_secs" => Some(make_entry(
1487                key,
1488                RuntimeConfigValue::Float(self.known_destinations_ttl),
1489                RuntimeConfigValue::Float(defaults.known_destinations_ttl),
1490                RuntimeConfigApplyMode::Immediate,
1491                "TTL for known destinations without an active path.",
1492            )),
1493            "global.rate_limiter_ttl_secs" => Some(make_entry(
1494                key,
1495                RuntimeConfigValue::Float(self.rate_limiter_ttl_secs),
1496                RuntimeConfigValue::Float(defaults.rate_limiter_ttl_secs),
1497                RuntimeConfigApplyMode::Immediate,
1498                "TTL for announce rate-limiter entries without an active path.",
1499            )),
1500            "global.known_destinations_cleanup_interval_ticks" => Some(make_entry(
1501                key,
1502                RuntimeConfigValue::Int(self.known_destinations_cleanup_interval_ticks as i64),
1503                RuntimeConfigValue::Int(defaults.known_destinations_cleanup_interval_ticks as i64),
1504                RuntimeConfigApplyMode::Immediate,
1505                "Tick interval between known-destinations cleanup passes.",
1506            )),
1507            "global.announce_cache_cleanup_interval_ticks" => Some(make_entry(
1508                key,
1509                RuntimeConfigValue::Int(self.announce_cache_cleanup_interval_ticks as i64),
1510                RuntimeConfigValue::Int(defaults.announce_cache_cleanup_interval_ticks as i64),
1511                RuntimeConfigApplyMode::Immediate,
1512                "Tick interval between announce-cache cleanup cycles.",
1513            )),
1514            "global.announce_cache_cleanup_batch_size" => Some(make_entry(
1515                key,
1516                RuntimeConfigValue::Int(self.announce_cache_cleanup_batch_size as i64),
1517                RuntimeConfigValue::Int(defaults.announce_cache_cleanup_batch_size as i64),
1518                RuntimeConfigApplyMode::Immediate,
1519                "Number of announce-cache entries processed per cleanup tick.",
1520            )),
1521            "global.discovery_cleanup_interval_ticks" => Some(make_entry(
1522                key,
1523                RuntimeConfigValue::Int(self.discovery_cleanup_interval_ticks as i64),
1524                RuntimeConfigValue::Int(defaults.discovery_cleanup_interval_ticks as i64),
1525                RuntimeConfigApplyMode::Immediate,
1526                "Tick interval between discovered-interface cleanup passes.",
1527            )),
1528            "global.management_announce_interval_secs" => Some(make_entry(
1529                key,
1530                RuntimeConfigValue::Float(self.management_announce_interval_secs),
1531                RuntimeConfigValue::Float(defaults.management_announce_interval_secs),
1532                RuntimeConfigApplyMode::Immediate,
1533                "Interval between management announces in seconds.",
1534            )),
1535            "global.direct_connect_policy" => Some(make_entry(
1536                key,
1537                RuntimeConfigValue::String(Self::holepunch_policy_name(
1538                    self.holepunch_manager.policy(),
1539                )),
1540                RuntimeConfigValue::String(Self::holepunch_policy_name(
1541                    defaults.direct_connect_policy,
1542                )),
1543                RuntimeConfigApplyMode::Immediate,
1544                "Policy for incoming direct-connect proposals.",
1545            )),
1546            #[cfg(feature = "rns-hooks")]
1547            "provider.queue_max_events" => {
1548                let value = self
1549                    .provider_bridge
1550                    .as_ref()
1551                    .map(|b| b.queue_max_events())
1552                    .unwrap_or(defaults.provider_queue_max_events);
1553                Some(make_entry(
1554                    key,
1555                    RuntimeConfigValue::Int(value as i64),
1556                    RuntimeConfigValue::Int(defaults.provider_queue_max_events as i64),
1557                    RuntimeConfigApplyMode::Immediate,
1558                    "Max queued events in the provider bridge.",
1559                ))
1560            }
1561            #[cfg(feature = "rns-hooks")]
1562            "provider.queue_max_bytes" => {
1563                let value = self
1564                    .provider_bridge
1565                    .as_ref()
1566                    .map(|b| b.queue_max_bytes())
1567                    .unwrap_or(defaults.provider_queue_max_bytes);
1568                Some(make_entry(
1569                    key,
1570                    RuntimeConfigValue::Int(value as i64),
1571                    RuntimeConfigValue::Int(defaults.provider_queue_max_bytes as i64),
1572                    RuntimeConfigApplyMode::Immediate,
1573                    "Max queued bytes in the provider bridge.",
1574                ))
1575            }
1576            _ => {
1577                #[cfg(feature = "iface-backbone")]
1578                if let Some(entry) = self.backbone_runtime_entry(key) {
1579                    return Some(entry);
1580                }
1581                #[cfg(feature = "iface-backbone")]
1582                if let Some(entry) = self.backbone_client_runtime_entry(key) {
1583                    return Some(entry);
1584                }
1585                #[cfg(feature = "iface-tcp")]
1586                if let Some(entry) = self.tcp_server_runtime_entry(key) {
1587                    return Some(entry);
1588                }
1589                #[cfg(feature = "iface-tcp")]
1590                if let Some(entry) = self.tcp_client_runtime_entry(key) {
1591                    return Some(entry);
1592                }
1593                #[cfg(feature = "iface-udp")]
1594                if let Some(entry) = self.udp_runtime_entry(key) {
1595                    return Some(entry);
1596                }
1597                #[cfg(feature = "iface-auto")]
1598                if let Some(entry) = self.auto_runtime_entry(key) {
1599                    return Some(entry);
1600                }
1601                #[cfg(feature = "iface-i2p")]
1602                if let Some(entry) = self.i2p_runtime_entry(key) {
1603                    return Some(entry);
1604                }
1605                #[cfg(feature = "iface-pipe")]
1606                if let Some(entry) = self.pipe_runtime_entry(key) {
1607                    return Some(entry);
1608                }
1609                #[cfg(feature = "iface-rnode")]
1610                if let Some(entry) = self.rnode_runtime_entry(key) {
1611                    return Some(entry);
1612                }
1613                if let Some(entry) = self.generic_interface_runtime_entry(key) {
1614                    return Some(entry);
1615                }
1616                None
1617            }
1618        }
1619    }
1620
1621    fn list_runtime_config(&self) -> Vec<RuntimeConfigEntry> {
1622        let mut entries: Vec<RuntimeConfigEntry> = [
1623            "global.tick_interval_ms",
1624            "global.known_destinations_ttl_secs",
1625            "global.rate_limiter_ttl_secs",
1626            "global.known_destinations_cleanup_interval_ticks",
1627            "global.announce_cache_cleanup_interval_ticks",
1628            "global.announce_cache_cleanup_batch_size",
1629            "global.discovery_cleanup_interval_ticks",
1630            "global.management_announce_interval_secs",
1631            "global.direct_connect_policy",
1632        ]
1633        .into_iter()
1634        .filter_map(|key| self.runtime_config_entry(key))
1635        .collect();
1636
1637        #[cfg(feature = "rns-hooks")]
1638        {
1639            entries.extend(
1640                ["provider.queue_max_events", "provider.queue_max_bytes"]
1641                    .into_iter()
1642                    .filter_map(|key| self.runtime_config_entry(key)),
1643            );
1644        }
1645        #[cfg(feature = "iface-backbone")]
1646        {
1647            entries.extend(self.list_backbone_runtime_config());
1648            entries.extend(self.list_backbone_client_runtime_config());
1649        }
1650        #[cfg(feature = "iface-tcp")]
1651        {
1652            entries.extend(self.list_tcp_server_runtime_config());
1653            entries.extend(self.list_tcp_client_runtime_config());
1654        }
1655        #[cfg(feature = "iface-udp")]
1656        {
1657            entries.extend(self.list_udp_runtime_config());
1658        }
1659        #[cfg(feature = "iface-auto")]
1660        {
1661            entries.extend(self.list_auto_runtime_config());
1662        }
1663        #[cfg(feature = "iface-i2p")]
1664        {
1665            entries.extend(self.list_i2p_runtime_config());
1666        }
1667        #[cfg(feature = "iface-pipe")]
1668        {
1669            entries.extend(self.list_pipe_runtime_config());
1670        }
1671        #[cfg(feature = "iface-rnode")]
1672        {
1673            entries.extend(self.list_rnode_runtime_config());
1674        }
1675        entries.extend(self.list_generic_interface_runtime_config());
1676
1677        entries
1678    }
1679
1680    fn holepunch_policy_name(policy: crate::event::HolePunchPolicy) -> String {
1681        match policy {
1682            crate::event::HolePunchPolicy::Reject => "reject".to_string(),
1683            crate::event::HolePunchPolicy::AcceptAll => "accept_all".to_string(),
1684            crate::event::HolePunchPolicy::AskApp => "ask_app".to_string(),
1685        }
1686    }
1687
1688    fn parse_holepunch_policy(value: &RuntimeConfigValue) -> Option<crate::event::HolePunchPolicy> {
1689        match value {
1690            RuntimeConfigValue::String(s) => match s.to_ascii_lowercase().as_str() {
1691                "reject" => Some(crate::event::HolePunchPolicy::Reject),
1692                "accept_all" | "acceptall" => Some(crate::event::HolePunchPolicy::AcceptAll),
1693                "ask_app" | "askapp" => Some(crate::event::HolePunchPolicy::AskApp),
1694                _ => None,
1695            },
1696            _ => None,
1697        }
1698    }
1699
1700    fn expect_u64(value: RuntimeConfigValue, key: &str) -> Result<u64, RuntimeConfigError> {
1701        match value {
1702            RuntimeConfigValue::Int(v) if v >= 0 => Ok(v as u64),
1703            RuntimeConfigValue::Int(_) => Err(RuntimeConfigError {
1704                code: RuntimeConfigErrorCode::InvalidValue,
1705                message: format!("{} must be >= 0", key),
1706            }),
1707            _ => Err(RuntimeConfigError {
1708                code: RuntimeConfigErrorCode::InvalidType,
1709                message: format!("{} expects an integer", key),
1710            }),
1711        }
1712    }
1713
1714    fn expect_f64(value: RuntimeConfigValue, key: &str) -> Result<f64, RuntimeConfigError> {
1715        match value {
1716            RuntimeConfigValue::Float(v) if v >= 0.0 => Ok(v),
1717            RuntimeConfigValue::Int(v) if v >= 0 => Ok(v as f64),
1718            RuntimeConfigValue::Float(_) | RuntimeConfigValue::Int(_) => Err(RuntimeConfigError {
1719                code: RuntimeConfigErrorCode::InvalidValue,
1720                message: format!("{} must be >= 0", key),
1721            }),
1722            _ => Err(RuntimeConfigError {
1723                code: RuntimeConfigErrorCode::InvalidType,
1724                message: format!("{} expects a numeric value", key),
1725            }),
1726        }
1727    }
1728
1729    fn expect_i64(value: RuntimeConfigValue, key: &str) -> Result<i64, RuntimeConfigError> {
1730        match value {
1731            RuntimeConfigValue::Int(v) => Ok(v),
1732            _ => Err(RuntimeConfigError {
1733                code: RuntimeConfigErrorCode::InvalidType,
1734                message: format!("{} expects an integer", key),
1735            }),
1736        }
1737    }
1738
1739    fn expect_bool(value: RuntimeConfigValue, key: &str) -> Result<bool, RuntimeConfigError> {
1740        match value {
1741            RuntimeConfigValue::Bool(v) => Ok(v),
1742            _ => Err(RuntimeConfigError {
1743                code: RuntimeConfigErrorCode::InvalidType,
1744                message: format!("{} expects a boolean", key),
1745            }),
1746        }
1747    }
1748
1749    fn expect_string(value: RuntimeConfigValue, key: &str) -> Result<String, RuntimeConfigError> {
1750        match value {
1751            RuntimeConfigValue::String(v) => Ok(v),
1752            _ => Err(RuntimeConfigError {
1753                code: RuntimeConfigErrorCode::InvalidType,
1754                message: format!("{} expects a string", key),
1755            }),
1756        }
1757    }
1758
1759    fn expect_optional_f64(
1760        value: RuntimeConfigValue,
1761        key: &str,
1762    ) -> Result<Option<f64>, RuntimeConfigError> {
1763        match value {
1764            RuntimeConfigValue::Null => Ok(None),
1765            RuntimeConfigValue::Float(v) => Ok(Some(v)),
1766            RuntimeConfigValue::Int(v) => Ok(Some(v as f64)),
1767            _ => Err(RuntimeConfigError {
1768                code: RuntimeConfigErrorCode::InvalidType,
1769                message: format!("{} expects a numeric value or null", key),
1770            }),
1771        }
1772    }
1773
1774    fn expect_optional_string(
1775        value: RuntimeConfigValue,
1776        key: &str,
1777    ) -> Result<Option<String>, RuntimeConfigError> {
1778        match value {
1779            RuntimeConfigValue::Null => Ok(None),
1780            RuntimeConfigValue::String(v) => Ok(Some(v)),
1781            _ => Err(RuntimeConfigError {
1782                code: RuntimeConfigErrorCode::InvalidType,
1783                message: format!("{} expects a string or null", key),
1784            }),
1785        }
1786    }
1787
1788    #[cfg(feature = "iface-backbone")]
1789    fn split_backbone_runtime_key<'a>(
1790        &self,
1791        key: &'a str,
1792    ) -> Result<(&'a str, &'a str), RuntimeConfigError> {
1793        let rest = key.strip_prefix("backbone.").ok_or(RuntimeConfigError {
1794            code: RuntimeConfigErrorCode::UnknownKey,
1795            message: format!("unknown runtime-config key '{}'", key),
1796        })?;
1797        rest.split_once('.').ok_or(RuntimeConfigError {
1798            code: RuntimeConfigErrorCode::UnknownKey,
1799            message: format!("unknown runtime-config key '{}'", key),
1800        })
1801    }
1802
1803    #[cfg(feature = "iface-backbone")]
1804    fn set_optional_duration(
1805        value: RuntimeConfigValue,
1806        key: &str,
1807    ) -> Result<Option<Duration>, RuntimeConfigError> {
1808        let secs = Self::expect_f64(value, key)?;
1809        if secs == 0.0 {
1810            Ok(None)
1811        } else {
1812            Ok(Some(Duration::from_secs_f64(secs)))
1813        }
1814    }
1815
1816    #[cfg(feature = "iface-backbone")]
1817    fn set_optional_usize(
1818        value: RuntimeConfigValue,
1819        key: &str,
1820    ) -> Result<Option<usize>, RuntimeConfigError> {
1821        let raw = Self::expect_u64(value, key)?;
1822        if raw == 0 {
1823            Ok(None)
1824        } else {
1825            Ok(Some(raw as usize))
1826        }
1827    }
1828
1829    #[cfg(feature = "iface-backbone")]
1830    fn set_backbone_runtime_config(
1831        &mut self,
1832        key: &str,
1833        value: RuntimeConfigValue,
1834    ) -> Result<(), RuntimeConfigError> {
1835        let (name, setting) = self.split_backbone_runtime_key(key)?;
1836        if matches!(
1837            setting,
1838            "discoverable"
1839                | "discovery_name"
1840                | "announce_interval_secs"
1841                | "reachable_on"
1842                | "stamp_value"
1843                | "latitude"
1844                | "longitude"
1845                | "height"
1846        ) {
1847            return self.set_backbone_discovery_runtime_config(key, value);
1848        }
1849        let handle = self.backbone_runtime.get(name).ok_or(RuntimeConfigError {
1850            code: RuntimeConfigErrorCode::NotFound,
1851            message: format!("backbone interface '{}' not found", name),
1852        })?;
1853        let mut runtime = handle.runtime.lock().unwrap();
1854        match setting {
1855            "idle_timeout_secs" => {
1856                runtime.idle_timeout = Self::set_optional_duration(value, key)?;
1857                Ok(())
1858            }
1859            "write_stall_timeout_secs" => {
1860                runtime.write_stall_timeout = Self::set_optional_duration(value, key)?;
1861                Ok(())
1862            }
1863            "max_penalty_duration_secs" => {
1864                runtime.abuse.max_penalty_duration = Self::set_optional_duration(value, key)?;
1865                Ok(())
1866            }
1867            "max_connections" => {
1868                runtime.max_connections = Self::set_optional_usize(value, key)?;
1869                Ok(())
1870            }
1871            _ => Err(RuntimeConfigError {
1872                code: RuntimeConfigErrorCode::UnknownKey,
1873                message: format!("unknown runtime-config key '{}'", key),
1874            }),
1875        }
1876    }
1877
1878    #[cfg(feature = "iface-backbone")]
1879    fn split_backbone_discovery_runtime_key<'a>(
1880        &self,
1881        key: &'a str,
1882    ) -> Result<(&'a str, &'a str), RuntimeConfigError> {
1883        let rest = key.strip_prefix("backbone.").ok_or(RuntimeConfigError {
1884            code: RuntimeConfigErrorCode::UnknownKey,
1885            message: format!("unknown runtime-config key '{}'", key),
1886        })?;
1887        rest.split_once('.').ok_or(RuntimeConfigError {
1888            code: RuntimeConfigErrorCode::UnknownKey,
1889            message: format!("unknown runtime-config key '{}'", key),
1890        })
1891    }
1892
1893    #[cfg(feature = "iface-backbone")]
1894    fn set_backbone_discovery_runtime_config(
1895        &mut self,
1896        key: &str,
1897        value: RuntimeConfigValue,
1898    ) -> Result<(), RuntimeConfigError> {
1899        let (name, setting) = self.split_backbone_discovery_runtime_key(key)?;
1900        let handle = self
1901            .backbone_discovery_runtime
1902            .get_mut(name)
1903            .ok_or(RuntimeConfigError {
1904                code: RuntimeConfigErrorCode::NotFound,
1905                message: format!("backbone interface '{}' not found", name),
1906            })?;
1907        match setting {
1908            "discoverable" => {
1909                handle.current.discoverable = Self::expect_bool(value, key)?;
1910            }
1911            "discovery_name" => {
1912                handle.current.config.discovery_name = Self::expect_string(value, key)?;
1913            }
1914            "announce_interval_secs" => {
1915                let secs = Self::expect_u64(value, key)?;
1916                if secs < 300 {
1917                    return Err(RuntimeConfigError {
1918                        code: RuntimeConfigErrorCode::InvalidValue,
1919                        message: format!("{} must be >= 300", key),
1920                    });
1921                }
1922                handle.current.config.announce_interval = secs;
1923            }
1924            "reachable_on" => {
1925                handle.current.config.reachable_on = Self::expect_optional_string(value, key)?;
1926            }
1927            "stamp_value" => {
1928                let raw = Self::expect_u64(value, key)?;
1929                if raw > u8::MAX as u64 {
1930                    return Err(RuntimeConfigError {
1931                        code: RuntimeConfigErrorCode::InvalidValue,
1932                        message: format!("{} must be <= {}", key, u8::MAX),
1933                    });
1934                }
1935                handle.current.config.stamp_value = raw as u8;
1936            }
1937            "latitude" => {
1938                handle.current.config.latitude = Self::expect_optional_f64(value, key)?;
1939            }
1940            "longitude" => {
1941                handle.current.config.longitude = Self::expect_optional_f64(value, key)?;
1942            }
1943            "height" => {
1944                handle.current.config.height = Self::expect_optional_f64(value, key)?;
1945            }
1946            _ => {
1947                return Err(RuntimeConfigError {
1948                    code: RuntimeConfigErrorCode::UnknownKey,
1949                    message: format!("unknown runtime-config key '{}'", key),
1950                });
1951            }
1952        }
1953        self.sync_backbone_discovery_runtime(name)
1954    }
1955
1956    #[cfg(feature = "iface-backbone")]
1957    fn reset_backbone_discovery_runtime_config(
1958        &mut self,
1959        key: &str,
1960    ) -> Result<(), RuntimeConfigError> {
1961        let (name, setting) = self.split_backbone_discovery_runtime_key(key)?;
1962        let handle = self
1963            .backbone_discovery_runtime
1964            .get_mut(name)
1965            .ok_or(RuntimeConfigError {
1966                code: RuntimeConfigErrorCode::NotFound,
1967                message: format!("backbone interface '{}' not found", name),
1968            })?;
1969        match setting {
1970            "discoverable" => handle.current.discoverable = handle.startup.discoverable,
1971            "discovery_name" => {
1972                handle.current.config.discovery_name = handle.startup.config.discovery_name.clone()
1973            }
1974            "announce_interval_secs" => {
1975                handle.current.config.announce_interval = handle.startup.config.announce_interval
1976            }
1977            "reachable_on" => {
1978                handle.current.config.reachable_on = handle.startup.config.reachable_on.clone()
1979            }
1980            "stamp_value" => handle.current.config.stamp_value = handle.startup.config.stamp_value,
1981            "latitude" => handle.current.config.latitude = handle.startup.config.latitude,
1982            "longitude" => handle.current.config.longitude = handle.startup.config.longitude,
1983            "height" => handle.current.config.height = handle.startup.config.height,
1984            _ => {
1985                return Err(RuntimeConfigError {
1986                    code: RuntimeConfigErrorCode::UnknownKey,
1987                    message: format!("unknown runtime-config key '{}'", key),
1988                });
1989            }
1990        }
1991        self.sync_backbone_discovery_runtime(name)
1992    }
1993
1994    #[cfg(feature = "iface-backbone")]
1995    fn reset_backbone_runtime_config(&mut self, key: &str) -> Result<(), RuntimeConfigError> {
1996        let (name, setting) = self.split_backbone_runtime_key(key)?;
1997        if matches!(
1998            setting,
1999            "discoverable"
2000                | "discovery_name"
2001                | "announce_interval_secs"
2002                | "reachable_on"
2003                | "stamp_value"
2004                | "latitude"
2005                | "longitude"
2006                | "height"
2007        ) {
2008            return self.reset_backbone_discovery_runtime_config(key);
2009        }
2010        let handle = self.backbone_runtime.get(name).ok_or(RuntimeConfigError {
2011            code: RuntimeConfigErrorCode::NotFound,
2012            message: format!("backbone interface '{}' not found", name),
2013        })?;
2014        let mut runtime = handle.runtime.lock().unwrap();
2015        let startup = handle.startup.clone();
2016        match setting {
2017            "idle_timeout_secs" => runtime.idle_timeout = startup.idle_timeout,
2018            "write_stall_timeout_secs" => runtime.write_stall_timeout = startup.write_stall_timeout,
2019            "max_penalty_duration_secs" => {
2020                runtime.abuse.max_penalty_duration = startup.abuse.max_penalty_duration
2021            }
2022            "max_connections" => runtime.max_connections = startup.max_connections,
2023            _ => {
2024                return Err(RuntimeConfigError {
2025                    code: RuntimeConfigErrorCode::UnknownKey,
2026                    message: format!("unknown runtime-config key '{}'", key),
2027                })
2028            }
2029        }
2030        Ok(())
2031    }
2032
2033    #[cfg(feature = "iface-backbone")]
2034    fn list_backbone_client_runtime_config(&self) -> Vec<RuntimeConfigEntry> {
2035        let mut entries = Vec::new();
2036        let mut names: Vec<&String> = self.backbone_client_runtime.keys().collect();
2037        names.sort();
2038        for name in names {
2039            for suffix in [
2040                "connect_timeout_secs",
2041                "reconnect_wait_secs",
2042                "max_reconnect_tries",
2043            ] {
2044                let key = format!("backbone_client.{}.{}", name, suffix);
2045                if let Some(entry) = self.backbone_client_runtime_entry(&key) {
2046                    entries.push(entry);
2047                }
2048            }
2049        }
2050        entries
2051    }
2052
2053    #[cfg(feature = "iface-backbone")]
2054    fn backbone_client_runtime_entry(&self, key: &str) -> Option<RuntimeConfigEntry> {
2055        let rest = key.strip_prefix("backbone_client.")?;
2056        let (name, setting) = rest.split_once('.')?;
2057        let handle = self.backbone_client_runtime.get(name)?;
2058        let current = handle.runtime.lock().unwrap().clone();
2059        let startup = handle.startup.clone();
2060        let make_entry = |value: RuntimeConfigValue,
2061                          default: RuntimeConfigValue,
2062                          description: &str|
2063         -> RuntimeConfigEntry {
2064            RuntimeConfigEntry {
2065                key: key.to_string(),
2066                source: if value == default {
2067                    RuntimeConfigSource::Startup
2068                } else {
2069                    RuntimeConfigSource::RuntimeOverride
2070                },
2071                value,
2072                default,
2073                apply_mode: RuntimeConfigApplyMode::NextReconnect,
2074                description: Some(description.to_string()),
2075            }
2076        };
2077        match setting {
2078            "connect_timeout_secs" => Some(make_entry(
2079                RuntimeConfigValue::Float(current.connect_timeout.as_secs_f64()),
2080                RuntimeConfigValue::Float(startup.connect_timeout.as_secs_f64()),
2081                "Backbone client connect timeout in seconds; applies on the next reconnect.",
2082            )),
2083            "reconnect_wait_secs" => Some(make_entry(
2084                RuntimeConfigValue::Float(current.reconnect_wait.as_secs_f64()),
2085                RuntimeConfigValue::Float(startup.reconnect_wait.as_secs_f64()),
2086                "Delay between backbone client reconnect attempts in seconds.",
2087            )),
2088            "max_reconnect_tries" => Some(make_entry(
2089                RuntimeConfigValue::Int(current.max_reconnect_tries.unwrap_or(0) as i64),
2090                RuntimeConfigValue::Int(startup.max_reconnect_tries.unwrap_or(0) as i64),
2091                "Maximum backbone client reconnect attempts; 0 disables the cap.",
2092            )),
2093            _ => None,
2094        }
2095    }
2096
2097    #[cfg(feature = "iface-backbone")]
2098    fn split_backbone_client_runtime_key<'a>(
2099        &self,
2100        key: &'a str,
2101    ) -> Result<(&'a str, &'a str), RuntimeConfigError> {
2102        let rest = key
2103            .strip_prefix("backbone_client.")
2104            .ok_or(RuntimeConfigError {
2105                code: RuntimeConfigErrorCode::UnknownKey,
2106                message: format!("unknown runtime-config key '{}'", key),
2107            })?;
2108        rest.split_once('.').ok_or(RuntimeConfigError {
2109            code: RuntimeConfigErrorCode::UnknownKey,
2110            message: format!("unknown runtime-config key '{}'", key),
2111        })
2112    }
2113
2114    #[cfg(feature = "iface-backbone")]
2115    fn set_backbone_client_runtime_config(
2116        &mut self,
2117        key: &str,
2118        value: RuntimeConfigValue,
2119    ) -> Result<(), RuntimeConfigError> {
2120        let (name, setting) = self.split_backbone_client_runtime_key(key)?;
2121        let handle = self
2122            .backbone_client_runtime
2123            .get(name)
2124            .ok_or(RuntimeConfigError {
2125                code: RuntimeConfigErrorCode::NotFound,
2126                message: format!("backbone client interface '{}' not found", name),
2127            })?;
2128        let mut runtime = handle.runtime.lock().unwrap();
2129        match setting {
2130            "connect_timeout_secs" => {
2131                runtime.connect_timeout = Duration::from_secs_f64(Self::expect_f64(value, key)?);
2132                Ok(())
2133            }
2134            "reconnect_wait_secs" => {
2135                runtime.reconnect_wait = Duration::from_secs_f64(Self::expect_f64(value, key)?);
2136                Ok(())
2137            }
2138            "max_reconnect_tries" => {
2139                runtime.max_reconnect_tries = match Self::expect_u64(value, key)? {
2140                    0 => None,
2141                    raw => Some(raw as u32),
2142                };
2143                Ok(())
2144            }
2145            _ => Err(RuntimeConfigError {
2146                code: RuntimeConfigErrorCode::UnknownKey,
2147                message: format!("unknown runtime-config key '{}'", key),
2148            }),
2149        }
2150    }
2151
2152    #[cfg(feature = "iface-backbone")]
2153    fn reset_backbone_client_runtime_config(
2154        &mut self,
2155        key: &str,
2156    ) -> Result<(), RuntimeConfigError> {
2157        let (name, setting) = self.split_backbone_client_runtime_key(key)?;
2158        let handle = self
2159            .backbone_client_runtime
2160            .get(name)
2161            .ok_or(RuntimeConfigError {
2162                code: RuntimeConfigErrorCode::NotFound,
2163                message: format!("backbone client interface '{}' not found", name),
2164            })?;
2165        let mut runtime = handle.runtime.lock().unwrap();
2166        let startup = handle.startup.clone();
2167        match setting {
2168            "connect_timeout_secs" => runtime.connect_timeout = startup.connect_timeout,
2169            "reconnect_wait_secs" => runtime.reconnect_wait = startup.reconnect_wait,
2170            "max_reconnect_tries" => runtime.max_reconnect_tries = startup.max_reconnect_tries,
2171            _ => {
2172                return Err(RuntimeConfigError {
2173                    code: RuntimeConfigErrorCode::UnknownKey,
2174                    message: format!("unknown runtime-config key '{}'", key),
2175                })
2176            }
2177        }
2178        Ok(())
2179    }
2180
2181    #[cfg(feature = "iface-tcp")]
2182    fn list_tcp_server_runtime_config(&self) -> Vec<RuntimeConfigEntry> {
2183        let mut entries = Vec::new();
2184        let mut names: Vec<&String> = self.tcp_server_runtime.keys().collect();
2185        names.sort();
2186        for name in names {
2187            for suffix in [
2188                "max_connections",
2189                "discoverable",
2190                "discovery_name",
2191                "announce_interval_secs",
2192                "reachable_on",
2193                "stamp_value",
2194                "latitude",
2195                "longitude",
2196                "height",
2197            ] {
2198                let key = format!("tcp_server.{}.{}", name, suffix);
2199                if let Some(entry) = self.tcp_server_runtime_entry(&key) {
2200                    entries.push(entry);
2201                }
2202            }
2203        }
2204        entries
2205    }
2206
2207    #[cfg(feature = "iface-tcp")]
2208    fn list_tcp_client_runtime_config(&self) -> Vec<RuntimeConfigEntry> {
2209        let mut entries = Vec::new();
2210        let mut names: Vec<&String> = self.tcp_client_runtime.keys().collect();
2211        names.sort();
2212        for name in names {
2213            for suffix in [
2214                "connect_timeout_secs",
2215                "reconnect_wait_secs",
2216                "max_reconnect_tries",
2217            ] {
2218                let key = format!("tcp_client.{}.{}", name, suffix);
2219                if let Some(entry) = self.tcp_client_runtime_entry(&key) {
2220                    entries.push(entry);
2221                }
2222            }
2223        }
2224        entries
2225    }
2226
2227    #[cfg(feature = "iface-tcp")]
2228    fn tcp_client_runtime_entry(&self, key: &str) -> Option<RuntimeConfigEntry> {
2229        let rest = key.strip_prefix("tcp_client.")?;
2230        let (name, setting) = rest.split_once('.')?;
2231        let handle = self.tcp_client_runtime.get(name)?;
2232        let current = handle.runtime.lock().unwrap().clone();
2233        let startup = handle.startup.clone();
2234        let make_entry = |value: RuntimeConfigValue,
2235                          default: RuntimeConfigValue,
2236                          description: &str|
2237         -> RuntimeConfigEntry {
2238            RuntimeConfigEntry {
2239                key: key.to_string(),
2240                source: if value == default {
2241                    RuntimeConfigSource::Startup
2242                } else {
2243                    RuntimeConfigSource::RuntimeOverride
2244                },
2245                value,
2246                default,
2247                apply_mode: RuntimeConfigApplyMode::NextReconnect,
2248                description: Some(description.to_string()),
2249            }
2250        };
2251        match setting {
2252            "connect_timeout_secs" => Some(make_entry(
2253                RuntimeConfigValue::Float(current.connect_timeout.as_secs_f64()),
2254                RuntimeConfigValue::Float(startup.connect_timeout.as_secs_f64()),
2255                "TCP client connect timeout in seconds; applies on the next reconnect.",
2256            )),
2257            "reconnect_wait_secs" => Some(make_entry(
2258                RuntimeConfigValue::Float(current.reconnect_wait.as_secs_f64()),
2259                RuntimeConfigValue::Float(startup.reconnect_wait.as_secs_f64()),
2260                "Delay between TCP client reconnect attempts in seconds.",
2261            )),
2262            "max_reconnect_tries" => Some(make_entry(
2263                RuntimeConfigValue::Int(current.max_reconnect_tries.unwrap_or(0) as i64),
2264                RuntimeConfigValue::Int(startup.max_reconnect_tries.unwrap_or(0) as i64),
2265                "Maximum TCP client reconnect attempts; 0 disables the cap.",
2266            )),
2267            _ => None,
2268        }
2269    }
2270
2271    #[cfg(feature = "iface-tcp")]
2272    fn split_tcp_client_runtime_key<'a>(
2273        &self,
2274        key: &'a str,
2275    ) -> Result<(&'a str, &'a str), RuntimeConfigError> {
2276        let rest = key.strip_prefix("tcp_client.").ok_or(RuntimeConfigError {
2277            code: RuntimeConfigErrorCode::UnknownKey,
2278            message: format!("unknown runtime-config key '{}'", key),
2279        })?;
2280        rest.split_once('.').ok_or(RuntimeConfigError {
2281            code: RuntimeConfigErrorCode::UnknownKey,
2282            message: format!("unknown runtime-config key '{}'", key),
2283        })
2284    }
2285
2286    #[cfg(feature = "iface-tcp")]
2287    fn set_tcp_client_runtime_config(
2288        &mut self,
2289        key: &str,
2290        value: RuntimeConfigValue,
2291    ) -> Result<(), RuntimeConfigError> {
2292        let (name, setting) = self.split_tcp_client_runtime_key(key)?;
2293        let handle = self
2294            .tcp_client_runtime
2295            .get(name)
2296            .ok_or(RuntimeConfigError {
2297                code: RuntimeConfigErrorCode::NotFound,
2298                message: format!("tcp client interface '{}' not found", name),
2299            })?;
2300        let mut runtime = handle.runtime.lock().unwrap();
2301        match setting {
2302            "connect_timeout_secs" => {
2303                runtime.connect_timeout = Duration::from_secs_f64(Self::expect_f64(value, key)?);
2304                Ok(())
2305            }
2306            "reconnect_wait_secs" => {
2307                runtime.reconnect_wait = Duration::from_secs_f64(Self::expect_f64(value, key)?);
2308                Ok(())
2309            }
2310            "max_reconnect_tries" => {
2311                runtime.max_reconnect_tries = match Self::expect_u64(value, key)? {
2312                    0 => None,
2313                    raw => Some(raw as u32),
2314                };
2315                Ok(())
2316            }
2317            _ => Err(RuntimeConfigError {
2318                code: RuntimeConfigErrorCode::UnknownKey,
2319                message: format!("unknown runtime-config key '{}'", key),
2320            }),
2321        }
2322    }
2323
2324    #[cfg(feature = "iface-tcp")]
2325    fn reset_tcp_client_runtime_config(&mut self, key: &str) -> Result<(), RuntimeConfigError> {
2326        let (name, setting) = self.split_tcp_client_runtime_key(key)?;
2327        let handle = self
2328            .tcp_client_runtime
2329            .get(name)
2330            .ok_or(RuntimeConfigError {
2331                code: RuntimeConfigErrorCode::NotFound,
2332                message: format!("tcp client interface '{}' not found", name),
2333            })?;
2334        let mut runtime = handle.runtime.lock().unwrap();
2335        let startup = handle.startup.clone();
2336        match setting {
2337            "connect_timeout_secs" => runtime.connect_timeout = startup.connect_timeout,
2338            "reconnect_wait_secs" => runtime.reconnect_wait = startup.reconnect_wait,
2339            "max_reconnect_tries" => runtime.max_reconnect_tries = startup.max_reconnect_tries,
2340            _ => {
2341                return Err(RuntimeConfigError {
2342                    code: RuntimeConfigErrorCode::UnknownKey,
2343                    message: format!("unknown runtime-config key '{}'", key),
2344                })
2345            }
2346        }
2347        Ok(())
2348    }
2349
2350    #[cfg(feature = "iface-udp")]
2351    fn list_udp_runtime_config(&self) -> Vec<RuntimeConfigEntry> {
2352        let mut entries = Vec::new();
2353        let mut names: Vec<&String> = self.udp_runtime.keys().collect();
2354        names.sort();
2355        for name in names {
2356            for suffix in ["forward_ip", "forward_port"] {
2357                let key = format!("udp.{}.{}", name, suffix);
2358                if let Some(entry) = self.udp_runtime_entry(&key) {
2359                    entries.push(entry);
2360                }
2361            }
2362        }
2363        entries
2364    }
2365
2366    #[cfg(feature = "iface-udp")]
2367    fn udp_runtime_entry(&self, key: &str) -> Option<RuntimeConfigEntry> {
2368        let rest = key.strip_prefix("udp.")?;
2369        let (name, setting) = rest.split_once('.')?;
2370        let handle = self.udp_runtime.get(name)?;
2371        let current = handle.runtime.lock().unwrap().clone();
2372        let startup = handle.startup.clone();
2373        let make_entry = |value: RuntimeConfigValue,
2374                          default: RuntimeConfigValue,
2375                          description: &str|
2376         -> RuntimeConfigEntry {
2377            RuntimeConfigEntry {
2378                key: key.to_string(),
2379                source: if value == default {
2380                    RuntimeConfigSource::Startup
2381                } else {
2382                    RuntimeConfigSource::RuntimeOverride
2383                },
2384                value,
2385                default,
2386                apply_mode: RuntimeConfigApplyMode::Immediate,
2387                description: Some(description.to_string()),
2388            }
2389        };
2390        match setting {
2391            "forward_ip" => Some(make_entry(
2392                current
2393                    .forward_ip
2394                    .clone()
2395                    .map(RuntimeConfigValue::String)
2396                    .unwrap_or(RuntimeConfigValue::Null),
2397                startup
2398                    .forward_ip
2399                    .clone()
2400                    .map(RuntimeConfigValue::String)
2401                    .unwrap_or(RuntimeConfigValue::Null),
2402                "Outbound UDP destination IP or hostname; null clears it.",
2403            )),
2404            "forward_port" => Some(make_entry(
2405                current
2406                    .forward_port
2407                    .map(|value| RuntimeConfigValue::Int(value as i64))
2408                    .unwrap_or(RuntimeConfigValue::Null),
2409                startup
2410                    .forward_port
2411                    .map(|value| RuntimeConfigValue::Int(value as i64))
2412                    .unwrap_or(RuntimeConfigValue::Null),
2413                "Outbound UDP destination port; null clears it.",
2414            )),
2415            _ => None,
2416        }
2417    }
2418
2419    #[cfg(feature = "iface-udp")]
2420    fn split_udp_runtime_key<'a>(
2421        &self,
2422        key: &'a str,
2423    ) -> Result<(&'a str, &'a str), RuntimeConfigError> {
2424        let rest = key.strip_prefix("udp.").ok_or(RuntimeConfigError {
2425            code: RuntimeConfigErrorCode::UnknownKey,
2426            message: format!("unknown runtime-config key '{}'", key),
2427        })?;
2428        rest.split_once('.').ok_or(RuntimeConfigError {
2429            code: RuntimeConfigErrorCode::UnknownKey,
2430            message: format!("unknown runtime-config key '{}'", key),
2431        })
2432    }
2433
2434    #[cfg(feature = "iface-udp")]
2435    fn set_udp_runtime_config(
2436        &mut self,
2437        key: &str,
2438        value: RuntimeConfigValue,
2439    ) -> Result<(), RuntimeConfigError> {
2440        let (name, setting) = self.split_udp_runtime_key(key)?;
2441        let handle = self.udp_runtime.get(name).ok_or(RuntimeConfigError {
2442            code: RuntimeConfigErrorCode::NotFound,
2443            message: format!("udp interface '{}' not found", name),
2444        })?;
2445        let mut runtime = handle.runtime.lock().unwrap();
2446        match setting {
2447            "forward_ip" => {
2448                runtime.forward_ip = Self::expect_optional_string(value, key)?;
2449                Ok(())
2450            }
2451            "forward_port" => {
2452                runtime.forward_port = match value {
2453                    RuntimeConfigValue::Null => None,
2454                    other => Some(Self::expect_u64(other, key)? as u16),
2455                };
2456                Ok(())
2457            }
2458            _ => Err(RuntimeConfigError {
2459                code: RuntimeConfigErrorCode::UnknownKey,
2460                message: format!("unknown runtime-config key '{}'", key),
2461            }),
2462        }
2463    }
2464
2465    #[cfg(feature = "iface-udp")]
2466    fn reset_udp_runtime_config(&mut self, key: &str) -> Result<(), RuntimeConfigError> {
2467        let (name, setting) = self.split_udp_runtime_key(key)?;
2468        let handle = self.udp_runtime.get(name).ok_or(RuntimeConfigError {
2469            code: RuntimeConfigErrorCode::NotFound,
2470            message: format!("udp interface '{}' not found", name),
2471        })?;
2472        let mut runtime = handle.runtime.lock().unwrap();
2473        let startup = handle.startup.clone();
2474        match setting {
2475            "forward_ip" => runtime.forward_ip = startup.forward_ip,
2476            "forward_port" => runtime.forward_port = startup.forward_port,
2477            _ => {
2478                return Err(RuntimeConfigError {
2479                    code: RuntimeConfigErrorCode::UnknownKey,
2480                    message: format!("unknown runtime-config key '{}'", key),
2481                })
2482            }
2483        }
2484        Ok(())
2485    }
2486
2487    #[cfg(feature = "iface-auto")]
2488    fn list_auto_runtime_config(&self) -> Vec<RuntimeConfigEntry> {
2489        let mut entries = Vec::new();
2490        let mut names: Vec<&String> = self.auto_runtime.keys().collect();
2491        names.sort();
2492        for name in names {
2493            for suffix in [
2494                "announce_interval_secs",
2495                "peer_timeout_secs",
2496                "peer_job_interval_secs",
2497            ] {
2498                let key = format!("auto.{}.{}", name, suffix);
2499                if let Some(entry) = self.auto_runtime_entry(&key) {
2500                    entries.push(entry);
2501                }
2502            }
2503        }
2504        entries
2505    }
2506
2507    #[cfg(feature = "iface-auto")]
2508    fn auto_runtime_entry(&self, key: &str) -> Option<RuntimeConfigEntry> {
2509        let rest = key.strip_prefix("auto.")?;
2510        let (name, setting) = rest.split_once('.')?;
2511        let handle = self.auto_runtime.get(name)?;
2512        let current = handle.runtime.lock().unwrap().clone();
2513        let startup = handle.startup.clone();
2514        let make_entry = |value: RuntimeConfigValue,
2515                          default: RuntimeConfigValue,
2516                          description: &str|
2517         -> RuntimeConfigEntry {
2518            RuntimeConfigEntry {
2519                key: key.to_string(),
2520                source: if value == default {
2521                    RuntimeConfigSource::Startup
2522                } else {
2523                    RuntimeConfigSource::RuntimeOverride
2524                },
2525                value,
2526                default,
2527                apply_mode: RuntimeConfigApplyMode::Immediate,
2528                description: Some(description.to_string()),
2529            }
2530        };
2531        match setting {
2532            "announce_interval_secs" => Some(make_entry(
2533                RuntimeConfigValue::Float(current.announce_interval_secs),
2534                RuntimeConfigValue::Float(startup.announce_interval_secs),
2535                "Interval between multicast discovery announces in seconds.",
2536            )),
2537            "peer_timeout_secs" => Some(make_entry(
2538                RuntimeConfigValue::Float(current.peer_timeout_secs),
2539                RuntimeConfigValue::Float(startup.peer_timeout_secs),
2540                "How long an Auto peer may stay quiet before being culled.",
2541            )),
2542            "peer_job_interval_secs" => Some(make_entry(
2543                RuntimeConfigValue::Float(current.peer_job_interval_secs),
2544                RuntimeConfigValue::Float(startup.peer_job_interval_secs),
2545                "Interval between Auto peer maintenance passes.",
2546            )),
2547            _ => None,
2548        }
2549    }
2550
2551    #[cfg(feature = "iface-auto")]
2552    fn split_auto_runtime_key<'a>(
2553        &self,
2554        key: &'a str,
2555    ) -> Result<(&'a str, &'a str), RuntimeConfigError> {
2556        let rest = key.strip_prefix("auto.").ok_or(RuntimeConfigError {
2557            code: RuntimeConfigErrorCode::UnknownKey,
2558            message: format!("unknown runtime-config key '{}'", key),
2559        })?;
2560        rest.split_once('.').ok_or(RuntimeConfigError {
2561            code: RuntimeConfigErrorCode::UnknownKey,
2562            message: format!("unknown runtime-config key '{}'", key),
2563        })
2564    }
2565
2566    #[cfg(feature = "iface-auto")]
2567    fn set_auto_runtime_config(
2568        &mut self,
2569        key: &str,
2570        value: RuntimeConfigValue,
2571    ) -> Result<(), RuntimeConfigError> {
2572        let (name, setting) = self.split_auto_runtime_key(key)?;
2573        let handle = self.auto_runtime.get(name).ok_or(RuntimeConfigError {
2574            code: RuntimeConfigErrorCode::NotFound,
2575            message: format!("auto interface '{}' not found", name),
2576        })?;
2577        let mut runtime = handle.runtime.lock().unwrap();
2578        match setting {
2579            "announce_interval_secs" => {
2580                runtime.announce_interval_secs = Self::expect_f64(value, key)?.max(0.1)
2581            }
2582            "peer_timeout_secs" => {
2583                runtime.peer_timeout_secs = Self::expect_f64(value, key)?.max(0.1)
2584            }
2585            "peer_job_interval_secs" => {
2586                runtime.peer_job_interval_secs = Self::expect_f64(value, key)?.max(0.1)
2587            }
2588            _ => {
2589                return Err(RuntimeConfigError {
2590                    code: RuntimeConfigErrorCode::UnknownKey,
2591                    message: format!("unknown runtime-config key '{}'", key),
2592                });
2593            }
2594        }
2595        Ok(())
2596    }
2597
2598    #[cfg(feature = "iface-auto")]
2599    fn reset_auto_runtime_config(&mut self, key: &str) -> Result<(), RuntimeConfigError> {
2600        let (name, setting) = self.split_auto_runtime_key(key)?;
2601        let handle = self.auto_runtime.get(name).ok_or(RuntimeConfigError {
2602            code: RuntimeConfigErrorCode::NotFound,
2603            message: format!("auto interface '{}' not found", name),
2604        })?;
2605        let mut runtime = handle.runtime.lock().unwrap();
2606        let startup = handle.startup.clone();
2607        match setting {
2608            "announce_interval_secs" => {
2609                runtime.announce_interval_secs = startup.announce_interval_secs
2610            }
2611            "peer_timeout_secs" => runtime.peer_timeout_secs = startup.peer_timeout_secs,
2612            "peer_job_interval_secs" => {
2613                runtime.peer_job_interval_secs = startup.peer_job_interval_secs
2614            }
2615            _ => {
2616                return Err(RuntimeConfigError {
2617                    code: RuntimeConfigErrorCode::UnknownKey,
2618                    message: format!("unknown runtime-config key '{}'", key),
2619                });
2620            }
2621        }
2622        Ok(())
2623    }
2624
2625    #[cfg(feature = "iface-i2p")]
2626    fn list_i2p_runtime_config(&self) -> Vec<RuntimeConfigEntry> {
2627        let mut entries = Vec::new();
2628        let mut names: Vec<&String> = self.i2p_runtime.keys().collect();
2629        names.sort();
2630        for name in names {
2631            let key = format!("i2p.{}.reconnect_wait_secs", name);
2632            if let Some(entry) = self.i2p_runtime_entry(&key) {
2633                entries.push(entry);
2634            }
2635        }
2636        entries
2637    }
2638
2639    #[cfg(feature = "iface-i2p")]
2640    fn i2p_runtime_entry(&self, key: &str) -> Option<RuntimeConfigEntry> {
2641        let rest = key.strip_prefix("i2p.")?;
2642        let (name, setting) = rest.split_once('.')?;
2643        let handle = self.i2p_runtime.get(name)?;
2644        let current = handle.runtime.lock().unwrap().clone();
2645        let startup = handle.startup.clone();
2646        match setting {
2647            "reconnect_wait_secs" => Some(RuntimeConfigEntry {
2648                key: key.to_string(),
2649                source: if current.reconnect_wait == startup.reconnect_wait {
2650                    RuntimeConfigSource::Startup
2651                } else {
2652                    RuntimeConfigSource::RuntimeOverride
2653                },
2654                value: RuntimeConfigValue::Float(current.reconnect_wait.as_secs_f64()),
2655                default: RuntimeConfigValue::Float(startup.reconnect_wait.as_secs_f64()),
2656                apply_mode: RuntimeConfigApplyMode::NextReconnect,
2657                description: Some(
2658                    "Delay before retrying outbound I2P peer connections.".to_string(),
2659                ),
2660            }),
2661            _ => None,
2662        }
2663    }
2664
2665    #[cfg(feature = "iface-i2p")]
2666    fn split_i2p_runtime_key<'a>(
2667        &self,
2668        key: &'a str,
2669    ) -> Result<(&'a str, &'a str), RuntimeConfigError> {
2670        let rest = key.strip_prefix("i2p.").ok_or(RuntimeConfigError {
2671            code: RuntimeConfigErrorCode::UnknownKey,
2672            message: format!("unknown runtime-config key '{}'", key),
2673        })?;
2674        rest.split_once('.').ok_or(RuntimeConfigError {
2675            code: RuntimeConfigErrorCode::UnknownKey,
2676            message: format!("unknown runtime-config key '{}'", key),
2677        })
2678    }
2679
2680    #[cfg(feature = "iface-i2p")]
2681    fn set_i2p_runtime_config(
2682        &mut self,
2683        key: &str,
2684        value: RuntimeConfigValue,
2685    ) -> Result<(), RuntimeConfigError> {
2686        let (name, setting) = self.split_i2p_runtime_key(key)?;
2687        let handle = self.i2p_runtime.get(name).ok_or(RuntimeConfigError {
2688            code: RuntimeConfigErrorCode::NotFound,
2689            message: format!("i2p interface '{}' not found", name),
2690        })?;
2691        let mut runtime = handle.runtime.lock().unwrap();
2692        match setting {
2693            "reconnect_wait_secs" => {
2694                runtime.reconnect_wait =
2695                    Duration::from_secs_f64(Self::expect_f64(value, key)?.max(0.1));
2696                Ok(())
2697            }
2698            _ => Err(RuntimeConfigError {
2699                code: RuntimeConfigErrorCode::UnknownKey,
2700                message: format!("unknown runtime-config key '{}'", key),
2701            }),
2702        }
2703    }
2704
2705    #[cfg(feature = "iface-i2p")]
2706    fn reset_i2p_runtime_config(&mut self, key: &str) -> Result<(), RuntimeConfigError> {
2707        let (name, setting) = self.split_i2p_runtime_key(key)?;
2708        let handle = self.i2p_runtime.get(name).ok_or(RuntimeConfigError {
2709            code: RuntimeConfigErrorCode::NotFound,
2710            message: format!("i2p interface '{}' not found", name),
2711        })?;
2712        let mut runtime = handle.runtime.lock().unwrap();
2713        let startup = handle.startup.clone();
2714        match setting {
2715            "reconnect_wait_secs" => runtime.reconnect_wait = startup.reconnect_wait,
2716            _ => {
2717                return Err(RuntimeConfigError {
2718                    code: RuntimeConfigErrorCode::UnknownKey,
2719                    message: format!("unknown runtime-config key '{}'", key),
2720                });
2721            }
2722        }
2723        Ok(())
2724    }
2725
2726    #[cfg(feature = "iface-pipe")]
2727    fn list_pipe_runtime_config(&self) -> Vec<RuntimeConfigEntry> {
2728        let mut entries = Vec::new();
2729        let mut names: Vec<&String> = self.pipe_runtime.keys().collect();
2730        names.sort();
2731        for name in names {
2732            let key = format!("pipe.{}.respawn_delay_secs", name);
2733            if let Some(entry) = self.pipe_runtime_entry(&key) {
2734                entries.push(entry);
2735            }
2736        }
2737        entries
2738    }
2739
2740    #[cfg(feature = "iface-pipe")]
2741    fn pipe_runtime_entry(&self, key: &str) -> Option<RuntimeConfigEntry> {
2742        let rest = key.strip_prefix("pipe.")?;
2743        let (name, setting) = rest.split_once('.')?;
2744        let handle = self.pipe_runtime.get(name)?;
2745        let current = handle.runtime.lock().unwrap().clone();
2746        let startup = handle.startup.clone();
2747        match setting {
2748            "respawn_delay_secs" => Some(RuntimeConfigEntry {
2749                key: key.to_string(),
2750                source: if current.respawn_delay == startup.respawn_delay {
2751                    RuntimeConfigSource::Startup
2752                } else {
2753                    RuntimeConfigSource::RuntimeOverride
2754                },
2755                value: RuntimeConfigValue::Float(current.respawn_delay.as_secs_f64()),
2756                default: RuntimeConfigValue::Float(startup.respawn_delay.as_secs_f64()),
2757                apply_mode: RuntimeConfigApplyMode::NextReconnect,
2758                description: Some(
2759                    "Delay before respawning the pipe subprocess after exit.".to_string(),
2760                ),
2761            }),
2762            _ => None,
2763        }
2764    }
2765
2766    #[cfg(feature = "iface-pipe")]
2767    fn split_pipe_runtime_key<'a>(
2768        &self,
2769        key: &'a str,
2770    ) -> Result<(&'a str, &'a str), RuntimeConfigError> {
2771        let rest = key.strip_prefix("pipe.").ok_or(RuntimeConfigError {
2772            code: RuntimeConfigErrorCode::UnknownKey,
2773            message: format!("unknown runtime-config key '{}'", key),
2774        })?;
2775        rest.split_once('.').ok_or(RuntimeConfigError {
2776            code: RuntimeConfigErrorCode::UnknownKey,
2777            message: format!("unknown runtime-config key '{}'", key),
2778        })
2779    }
2780
2781    #[cfg(feature = "iface-pipe")]
2782    fn set_pipe_runtime_config(
2783        &mut self,
2784        key: &str,
2785        value: RuntimeConfigValue,
2786    ) -> Result<(), RuntimeConfigError> {
2787        let (name, setting) = self.split_pipe_runtime_key(key)?;
2788        let handle = self.pipe_runtime.get(name).ok_or(RuntimeConfigError {
2789            code: RuntimeConfigErrorCode::NotFound,
2790            message: format!("pipe interface '{}' not found", name),
2791        })?;
2792        let mut runtime = handle.runtime.lock().unwrap();
2793        match setting {
2794            "respawn_delay_secs" => {
2795                runtime.respawn_delay =
2796                    Duration::from_secs_f64(Self::expect_f64(value, key)?.max(0.1));
2797                Ok(())
2798            }
2799            _ => Err(RuntimeConfigError {
2800                code: RuntimeConfigErrorCode::UnknownKey,
2801                message: format!("unknown runtime-config key '{}'", key),
2802            }),
2803        }
2804    }
2805
2806    #[cfg(feature = "iface-pipe")]
2807    fn reset_pipe_runtime_config(&mut self, key: &str) -> Result<(), RuntimeConfigError> {
2808        let (name, setting) = self.split_pipe_runtime_key(key)?;
2809        let handle = self.pipe_runtime.get(name).ok_or(RuntimeConfigError {
2810            code: RuntimeConfigErrorCode::NotFound,
2811            message: format!("pipe interface '{}' not found", name),
2812        })?;
2813        let mut runtime = handle.runtime.lock().unwrap();
2814        let startup = handle.startup.clone();
2815        match setting {
2816            "respawn_delay_secs" => runtime.respawn_delay = startup.respawn_delay,
2817            _ => {
2818                return Err(RuntimeConfigError {
2819                    code: RuntimeConfigErrorCode::UnknownKey,
2820                    message: format!("unknown runtime-config key '{}'", key),
2821                });
2822            }
2823        }
2824        Ok(())
2825    }
2826
2827    #[cfg(feature = "iface-rnode")]
2828    fn list_rnode_runtime_config(&self) -> Vec<RuntimeConfigEntry> {
2829        let mut entries = Vec::new();
2830        let mut names: Vec<&String> = self.rnode_runtime.keys().collect();
2831        names.sort();
2832        for name in names {
2833            for suffix in [
2834                "frequency_hz",
2835                "bandwidth_hz",
2836                "txpower_dbm",
2837                "spreading_factor",
2838                "coding_rate",
2839                "st_alock_pct",
2840                "lt_alock_pct",
2841            ] {
2842                let key = format!("rnode.{}.{}", name, suffix);
2843                if let Some(entry) = self.rnode_runtime_entry(&key) {
2844                    entries.push(entry);
2845                }
2846            }
2847        }
2848        entries
2849    }
2850
2851    #[cfg(feature = "iface-rnode")]
2852    fn rnode_runtime_entry(&self, key: &str) -> Option<RuntimeConfigEntry> {
2853        let rest = key.strip_prefix("rnode.")?;
2854        let (name, setting) = rest.split_once('.')?;
2855        let handle = self.rnode_runtime.get(name)?;
2856        let current = handle.runtime.lock().unwrap().clone();
2857        let startup = handle.startup.clone();
2858        let make_entry = |value: RuntimeConfigValue,
2859                          default: RuntimeConfigValue,
2860                          description: &str|
2861         -> RuntimeConfigEntry {
2862            RuntimeConfigEntry {
2863                key: key.to_string(),
2864                source: if value == default {
2865                    RuntimeConfigSource::Startup
2866                } else {
2867                    RuntimeConfigSource::RuntimeOverride
2868                },
2869                value,
2870                default,
2871                apply_mode: RuntimeConfigApplyMode::Immediate,
2872                description: Some(description.to_string()),
2873            }
2874        };
2875        match setting {
2876            "frequency_hz" => Some(make_entry(
2877                RuntimeConfigValue::Int(current.sub.frequency as i64),
2878                RuntimeConfigValue::Int(startup.sub.frequency as i64),
2879                "RNode radio frequency in Hz.",
2880            )),
2881            "bandwidth_hz" => Some(make_entry(
2882                RuntimeConfigValue::Int(current.sub.bandwidth as i64),
2883                RuntimeConfigValue::Int(startup.sub.bandwidth as i64),
2884                "RNode radio bandwidth in Hz.",
2885            )),
2886            "txpower_dbm" => Some(make_entry(
2887                RuntimeConfigValue::Int(current.sub.txpower as i64),
2888                RuntimeConfigValue::Int(startup.sub.txpower as i64),
2889                "RNode transmit power in dBm.",
2890            )),
2891            "spreading_factor" => Some(make_entry(
2892                RuntimeConfigValue::Int(current.sub.spreading_factor as i64),
2893                RuntimeConfigValue::Int(startup.sub.spreading_factor as i64),
2894                "RNode LoRa spreading factor.",
2895            )),
2896            "coding_rate" => Some(make_entry(
2897                RuntimeConfigValue::Int(current.sub.coding_rate as i64),
2898                RuntimeConfigValue::Int(startup.sub.coding_rate as i64),
2899                "RNode LoRa coding rate.",
2900            )),
2901            "st_alock_pct" => Some(make_entry(
2902                current
2903                    .sub
2904                    .st_alock
2905                    .map(|value| RuntimeConfigValue::Float(value as f64))
2906                    .unwrap_or(RuntimeConfigValue::Null),
2907                startup
2908                    .sub
2909                    .st_alock
2910                    .map(|value| RuntimeConfigValue::Float(value as f64))
2911                    .unwrap_or(RuntimeConfigValue::Null),
2912                "RNode short-term airtime lock percent; null clears it.",
2913            )),
2914            "lt_alock_pct" => Some(make_entry(
2915                current
2916                    .sub
2917                    .lt_alock
2918                    .map(|value| RuntimeConfigValue::Float(value as f64))
2919                    .unwrap_or(RuntimeConfigValue::Null),
2920                startup
2921                    .sub
2922                    .lt_alock
2923                    .map(|value| RuntimeConfigValue::Float(value as f64))
2924                    .unwrap_or(RuntimeConfigValue::Null),
2925                "RNode long-term airtime lock percent; null clears it.",
2926            )),
2927            _ => None,
2928        }
2929    }
2930
2931    #[cfg(feature = "iface-rnode")]
2932    fn split_rnode_runtime_key<'a>(
2933        &self,
2934        key: &'a str,
2935    ) -> Result<(&'a str, &'a str), RuntimeConfigError> {
2936        let rest = key.strip_prefix("rnode.").ok_or(RuntimeConfigError {
2937            code: RuntimeConfigErrorCode::UnknownKey,
2938            message: format!("unknown runtime-config key '{}'", key),
2939        })?;
2940        rest.split_once('.').ok_or(RuntimeConfigError {
2941            code: RuntimeConfigErrorCode::UnknownKey,
2942            message: format!("unknown runtime-config key '{}'", key),
2943        })
2944    }
2945
2946    #[cfg(feature = "iface-rnode")]
2947    fn apply_rnode_runtime(runtime: &mut RNodeRuntime) -> Result<(), RuntimeConfigError> {
2948        if let Some(err) = validate_sub_config(&runtime.sub) {
2949            return Err(RuntimeConfigError {
2950                code: RuntimeConfigErrorCode::InvalidValue,
2951                message: err,
2952            });
2953        }
2954        if let Some(writer) = runtime.writer.clone() {
2955            crate::interface::rnode::configure_subinterface(&writer, 0, &runtime.sub, false)
2956                .map_err(|e| RuntimeConfigError {
2957                    code: RuntimeConfigErrorCode::ApplyFailed,
2958                    message: format!("failed to apply RNode config: {}", e),
2959                })?;
2960        }
2961        Ok(())
2962    }
2963
2964    #[cfg(feature = "iface-rnode")]
2965    fn set_rnode_runtime_config(
2966        &mut self,
2967        key: &str,
2968        value: RuntimeConfigValue,
2969    ) -> Result<(), RuntimeConfigError> {
2970        let (name, setting) = self.split_rnode_runtime_key(key)?;
2971        let handle = self.rnode_runtime.get(name).ok_or(RuntimeConfigError {
2972            code: RuntimeConfigErrorCode::NotFound,
2973            message: format!("rnode interface '{}' not found", name),
2974        })?;
2975        let mut runtime = handle.runtime.lock().unwrap();
2976        let old = runtime.sub.clone();
2977        match setting {
2978            "frequency_hz" => runtime.sub.frequency = Self::expect_u64(value, key)? as u32,
2979            "bandwidth_hz" => runtime.sub.bandwidth = Self::expect_u64(value, key)? as u32,
2980            "txpower_dbm" => runtime.sub.txpower = Self::expect_i64(value, key)? as i8,
2981            "spreading_factor" => {
2982                runtime.sub.spreading_factor = Self::expect_u64(value, key)? as u8
2983            }
2984            "coding_rate" => runtime.sub.coding_rate = Self::expect_u64(value, key)? as u8,
2985            "st_alock_pct" => {
2986                runtime.sub.st_alock = match value {
2987                    RuntimeConfigValue::Null => None,
2988                    other => Some(Self::expect_f64(other, key)? as f32),
2989                };
2990            }
2991            "lt_alock_pct" => {
2992                runtime.sub.lt_alock = match value {
2993                    RuntimeConfigValue::Null => None,
2994                    other => Some(Self::expect_f64(other, key)? as f32),
2995                };
2996            }
2997            _ => {
2998                return Err(RuntimeConfigError {
2999                    code: RuntimeConfigErrorCode::UnknownKey,
3000                    message: format!("unknown runtime-config key '{}'", key),
3001                });
3002            }
3003        }
3004        if let Err(err) = Self::apply_rnode_runtime(&mut runtime) {
3005            runtime.sub = old;
3006            return Err(err);
3007        }
3008        Ok(())
3009    }
3010
3011    #[cfg(feature = "iface-rnode")]
3012    fn reset_rnode_runtime_config(&mut self, key: &str) -> Result<(), RuntimeConfigError> {
3013        let (name, setting) = self.split_rnode_runtime_key(key)?;
3014        let handle = self.rnode_runtime.get(name).ok_or(RuntimeConfigError {
3015            code: RuntimeConfigErrorCode::NotFound,
3016            message: format!("rnode interface '{}' not found", name),
3017        })?;
3018        let mut runtime = handle.runtime.lock().unwrap();
3019        let old = runtime.sub.clone();
3020        let startup = handle.startup.clone();
3021        match setting {
3022            "frequency_hz" => runtime.sub.frequency = startup.sub.frequency,
3023            "bandwidth_hz" => runtime.sub.bandwidth = startup.sub.bandwidth,
3024            "txpower_dbm" => runtime.sub.txpower = startup.sub.txpower,
3025            "spreading_factor" => runtime.sub.spreading_factor = startup.sub.spreading_factor,
3026            "coding_rate" => runtime.sub.coding_rate = startup.sub.coding_rate,
3027            "st_alock_pct" => runtime.sub.st_alock = startup.sub.st_alock,
3028            "lt_alock_pct" => runtime.sub.lt_alock = startup.sub.lt_alock,
3029            _ => {
3030                return Err(RuntimeConfigError {
3031                    code: RuntimeConfigErrorCode::UnknownKey,
3032                    message: format!("unknown runtime-config key '{}'", key),
3033                });
3034            }
3035        }
3036        if let Err(err) = Self::apply_rnode_runtime(&mut runtime) {
3037            runtime.sub = old;
3038            return Err(err);
3039        }
3040        Ok(())
3041    }
3042
3043    fn list_generic_interface_runtime_config(&self) -> Vec<RuntimeConfigEntry> {
3044        let mut entries = Vec::new();
3045        let mut names: Vec<String> = self
3046            .interfaces
3047            .values()
3048            .map(|entry| entry.info.name.clone())
3049            .collect();
3050        names.sort();
3051        names.dedup();
3052        for name in names {
3053            for suffix in [
3054                "enabled",
3055                "mode",
3056                "announce_rate_target",
3057                "announce_rate_grace",
3058                "announce_rate_penalty",
3059                "announce_cap",
3060                "ingress_control",
3061                "ic_max_held_announces",
3062                "ic_burst_hold",
3063                "ic_burst_freq_new",
3064                "ic_burst_freq",
3065                "ic_new_time",
3066                "ic_burst_penalty",
3067                "ic_held_release_interval",
3068            ] {
3069                let key = format!("interface.{}.{}", name, suffix);
3070                if let Some(entry) = self.generic_interface_runtime_entry(&key) {
3071                    entries.push(entry);
3072                }
3073            }
3074            if self.interface_ifac_runtime.contains_key(&name) {
3075                for suffix in ["ifac_netname", "ifac_passphrase", "ifac_size_bytes"] {
3076                    let key = format!("interface.{}.{}", name, suffix);
3077                    if let Some(entry) = self.generic_interface_runtime_entry(&key) {
3078                        entries.push(entry);
3079                    }
3080                }
3081            }
3082        }
3083        entries
3084    }
3085
3086    fn generic_interface_runtime_entry(&self, key: &str) -> Option<RuntimeConfigEntry> {
3087        let rest = key.strip_prefix("interface.")?;
3088        let (name, setting) = rest.rsplit_once('.')?;
3089        let make_entry = |value: RuntimeConfigValue,
3090                          default: RuntimeConfigValue,
3091                          apply_mode: RuntimeConfigApplyMode,
3092                          description: &str|
3093         -> RuntimeConfigEntry {
3094            RuntimeConfigEntry {
3095                key: key.to_string(),
3096                source: if value == default {
3097                    RuntimeConfigSource::Startup
3098                } else {
3099                    RuntimeConfigSource::RuntimeOverride
3100                },
3101                value,
3102                default,
3103                apply_mode,
3104                description: Some(description.to_string()),
3105            }
3106        };
3107        match setting {
3108            "enabled" => {
3109                let entry = self
3110                    .interfaces
3111                    .values()
3112                    .find(|entry| entry.info.name == name)?;
3113                Some(make_entry(
3114                    RuntimeConfigValue::Bool(entry.enabled),
3115                    RuntimeConfigValue::Bool(true),
3116                    RuntimeConfigApplyMode::Immediate,
3117                    "Administrative enable/disable state for this interface.",
3118                ))
3119            }
3120            "ifac_netname" => {
3121                let current = self.interface_ifac_runtime.get(name)?;
3122                let startup = self.interface_ifac_runtime_defaults.get(name)?;
3123                Some(make_entry(
3124                    current
3125                        .netname
3126                        .clone()
3127                        .map(RuntimeConfigValue::String)
3128                        .unwrap_or(RuntimeConfigValue::Null),
3129                    startup
3130                        .netname
3131                        .clone()
3132                        .map(RuntimeConfigValue::String)
3133                        .unwrap_or(RuntimeConfigValue::Null),
3134                    RuntimeConfigApplyMode::Immediate,
3135                    "IFAC network name for this interface; null clears it.",
3136                ))
3137            }
3138            "ifac_passphrase" => {
3139                let current = self.interface_ifac_runtime.get(name)?;
3140                let startup = self.interface_ifac_runtime_defaults.get(name)?;
3141                let current_value = current
3142                    .netkey
3143                    .as_ref()
3144                    .map(|_| RuntimeConfigValue::String("<redacted>".to_string()))
3145                    .unwrap_or(RuntimeConfigValue::Null);
3146                let default_value = startup
3147                    .netkey
3148                    .as_ref()
3149                    .map(|_| RuntimeConfigValue::String("<redacted>".to_string()))
3150                    .unwrap_or(RuntimeConfigValue::Null);
3151                Some(RuntimeConfigEntry {
3152                    key: key.to_string(),
3153                    source: if current.netkey == startup.netkey {
3154                        RuntimeConfigSource::Startup
3155                    } else {
3156                        RuntimeConfigSource::RuntimeOverride
3157                    },
3158                    value: current_value,
3159                    default: default_value,
3160                    apply_mode: RuntimeConfigApplyMode::Immediate,
3161                    description: Some(
3162                        "IFAC passphrase for this interface; write-only, set a string to change it or null to clear it."
3163                            .to_string(),
3164                    ),
3165                })
3166            }
3167            "ifac_size_bytes" => {
3168                let current = self.interface_ifac_runtime.get(name)?;
3169                let startup = self.interface_ifac_runtime_defaults.get(name)?;
3170                Some(make_entry(
3171                    RuntimeConfigValue::Int(current.size as i64),
3172                    RuntimeConfigValue::Int(startup.size as i64),
3173                    RuntimeConfigApplyMode::Immediate,
3174                    "IFAC size in bytes; applies when IFAC is enabled.",
3175                ))
3176            }
3177            "mode" => {
3178                let (_, current, startup) = self.interface_runtime_infos_by_name(name)?;
3179                Some(make_entry(
3180                    RuntimeConfigValue::String(Self::interface_mode_name(current.mode)),
3181                    RuntimeConfigValue::String(Self::interface_mode_name(startup.mode)),
3182                    RuntimeConfigApplyMode::Immediate,
3183                    "Routing mode for this interface.",
3184                ))
3185            }
3186            "announce_rate_target" => {
3187                let (_, current, startup) = self.interface_runtime_infos_by_name(name)?;
3188                Some(make_entry(
3189                    current
3190                        .announce_rate_target
3191                        .map(RuntimeConfigValue::Float)
3192                        .unwrap_or(RuntimeConfigValue::Null),
3193                    startup
3194                        .announce_rate_target
3195                        .map(RuntimeConfigValue::Float)
3196                        .unwrap_or(RuntimeConfigValue::Null),
3197                    RuntimeConfigApplyMode::Immediate,
3198                    "Optional announce rate target in announces/sec; null disables it.",
3199                ))
3200            }
3201            "announce_rate_grace" => {
3202                let (_, current, startup) = self.interface_runtime_infos_by_name(name)?;
3203                Some(make_entry(
3204                    RuntimeConfigValue::Int(current.announce_rate_grace as i64),
3205                    RuntimeConfigValue::Int(startup.announce_rate_grace as i64),
3206                    RuntimeConfigApplyMode::Immediate,
3207                    "Announce rate grace period in announces.",
3208                ))
3209            }
3210            "announce_rate_penalty" => {
3211                let (_, current, startup) = self.interface_runtime_infos_by_name(name)?;
3212                Some(make_entry(
3213                    RuntimeConfigValue::Float(current.announce_rate_penalty),
3214                    RuntimeConfigValue::Float(startup.announce_rate_penalty),
3215                    RuntimeConfigApplyMode::Immediate,
3216                    "Announce rate penalty multiplier.",
3217                ))
3218            }
3219            "announce_cap" => {
3220                let (_, current, startup) = self.interface_runtime_infos_by_name(name)?;
3221                Some(make_entry(
3222                    RuntimeConfigValue::Float(current.announce_cap),
3223                    RuntimeConfigValue::Float(startup.announce_cap),
3224                    RuntimeConfigApplyMode::Immediate,
3225                    "Fraction of bitrate reserved for announces.",
3226                ))
3227            }
3228            "ingress_control" => {
3229                let (_, current, startup) = self.interface_runtime_infos_by_name(name)?;
3230                Some(make_entry(
3231                    RuntimeConfigValue::Bool(current.ingress_control.enabled),
3232                    RuntimeConfigValue::Bool(startup.ingress_control.enabled),
3233                    RuntimeConfigApplyMode::Immediate,
3234                    "Whether ingress control is enabled for this interface.",
3235                ))
3236            }
3237            "ic_max_held_announces" => {
3238                let (_, current, startup) = self.interface_runtime_infos_by_name(name)?;
3239                Some(make_entry(
3240                    RuntimeConfigValue::Int(current.ingress_control.max_held_announces as i64),
3241                    RuntimeConfigValue::Int(startup.ingress_control.max_held_announces as i64),
3242                    RuntimeConfigApplyMode::Immediate,
3243                    "Maximum held announces retained while ingress control is limiting this interface.",
3244                ))
3245            }
3246            "ic_burst_hold" => {
3247                let (_, current, startup) = self.interface_runtime_infos_by_name(name)?;
3248                Some(make_entry(
3249                    RuntimeConfigValue::Float(current.ingress_control.burst_hold),
3250                    RuntimeConfigValue::Float(startup.ingress_control.burst_hold),
3251                    RuntimeConfigApplyMode::Immediate,
3252                    "Seconds to keep ingress-control burst state active before releasing held announces.",
3253                ))
3254            }
3255            "ic_burst_freq_new" => {
3256                let (_, current, startup) = self.interface_runtime_infos_by_name(name)?;
3257                Some(make_entry(
3258                    RuntimeConfigValue::Float(current.ingress_control.burst_freq_new),
3259                    RuntimeConfigValue::Float(startup.ingress_control.burst_freq_new),
3260                    RuntimeConfigApplyMode::Immediate,
3261                    "Announce frequency threshold for new interfaces.",
3262                ))
3263            }
3264            "ic_burst_freq" => {
3265                let (_, current, startup) = self.interface_runtime_infos_by_name(name)?;
3266                Some(make_entry(
3267                    RuntimeConfigValue::Float(current.ingress_control.burst_freq),
3268                    RuntimeConfigValue::Float(startup.ingress_control.burst_freq),
3269                    RuntimeConfigApplyMode::Immediate,
3270                    "Announce frequency threshold for established interfaces.",
3271                ))
3272            }
3273            "ic_new_time" => {
3274                let (_, current, startup) = self.interface_runtime_infos_by_name(name)?;
3275                Some(make_entry(
3276                    RuntimeConfigValue::Float(current.ingress_control.new_time),
3277                    RuntimeConfigValue::Float(startup.ingress_control.new_time),
3278                    RuntimeConfigApplyMode::Immediate,
3279                    "Seconds after interface start that ingress control uses the new-interface burst threshold.",
3280                ))
3281            }
3282            "ic_burst_penalty" => {
3283                let (_, current, startup) = self.interface_runtime_infos_by_name(name)?;
3284                Some(make_entry(
3285                    RuntimeConfigValue::Float(current.ingress_control.burst_penalty),
3286                    RuntimeConfigValue::Float(startup.ingress_control.burst_penalty),
3287                    RuntimeConfigApplyMode::Immediate,
3288                    "Seconds to wait after a burst before releasing held announces.",
3289                ))
3290            }
3291            "ic_held_release_interval" => {
3292                let (_, current, startup) = self.interface_runtime_infos_by_name(name)?;
3293                Some(make_entry(
3294                    RuntimeConfigValue::Float(current.ingress_control.held_release_interval),
3295                    RuntimeConfigValue::Float(startup.ingress_control.held_release_interval),
3296                    RuntimeConfigApplyMode::Immediate,
3297                    "Seconds between held announce releases.",
3298                ))
3299            }
3300            _ => None,
3301        }
3302    }
3303
3304    fn split_generic_interface_runtime_key<'a>(
3305        &self,
3306        key: &'a str,
3307    ) -> Result<(&'a str, &'a str), RuntimeConfigError> {
3308        let rest = key.strip_prefix("interface.").ok_or(RuntimeConfigError {
3309            code: RuntimeConfigErrorCode::UnknownKey,
3310            message: format!("unknown runtime-config key '{}'", key),
3311        })?;
3312        rest.rsplit_once('.').ok_or(RuntimeConfigError {
3313            code: RuntimeConfigErrorCode::UnknownKey,
3314            message: format!("unknown runtime-config key '{}'", key),
3315        })
3316    }
3317
3318    fn interface_runtime_infos_by_name(
3319        &self,
3320        name: &str,
3321    ) -> Option<(
3322        rns_core::transport::types::InterfaceId,
3323        &rns_core::transport::types::InterfaceInfo,
3324        &rns_core::transport::types::InterfaceInfo,
3325    )> {
3326        let (id, entry) = self
3327            .interfaces
3328            .iter()
3329            .find(|(_, entry)| entry.info.name == name)?;
3330        let startup = self.interface_runtime_defaults.get(name)?;
3331        Some((*id, &entry.info, startup))
3332    }
3333
3334    fn interface_mode_name(mode: u8) -> String {
3335        match mode {
3336            rns_core::constants::MODE_FULL => "full".to_string(),
3337            rns_core::constants::MODE_ACCESS_POINT => "access_point".to_string(),
3338            rns_core::constants::MODE_POINT_TO_POINT => "point_to_point".to_string(),
3339            rns_core::constants::MODE_ROAMING => "roaming".to_string(),
3340            rns_core::constants::MODE_BOUNDARY => "boundary".to_string(),
3341            rns_core::constants::MODE_GATEWAY => "gateway".to_string(),
3342            _ => mode.to_string(),
3343        }
3344    }
3345
3346    fn parse_interface_mode(value: &RuntimeConfigValue) -> Option<u8> {
3347        match value {
3348            RuntimeConfigValue::Int(v) if *v >= 0 && *v <= u8::MAX as i64 => Some(*v as u8),
3349            RuntimeConfigValue::String(s) => match s.to_ascii_lowercase().as_str() {
3350                "full" => Some(rns_core::constants::MODE_FULL),
3351                "access_point" | "accesspoint" | "ap" => {
3352                    Some(rns_core::constants::MODE_ACCESS_POINT)
3353                }
3354                "point_to_point" | "pointtopoint" | "ptp" => {
3355                    Some(rns_core::constants::MODE_POINT_TO_POINT)
3356                }
3357                "roaming" => Some(rns_core::constants::MODE_ROAMING),
3358                "boundary" => Some(rns_core::constants::MODE_BOUNDARY),
3359                "gateway" | "gw" => Some(rns_core::constants::MODE_GATEWAY),
3360                _ => None,
3361            },
3362            _ => None,
3363        }
3364    }
3365
3366    fn apply_interface_ifac_runtime(entry: &mut InterfaceEntry, config: &IfacRuntimeConfig) {
3367        entry.ifac = if config.netname.is_some() || config.netkey.is_some() {
3368            Some(ifac::derive_ifac(
3369                config.netname.as_deref(),
3370                config.netkey.as_deref(),
3371                config.size,
3372            ))
3373        } else {
3374            None
3375        };
3376    }
3377
3378    fn set_generic_interface_runtime_config(
3379        &mut self,
3380        key: &str,
3381        value: RuntimeConfigValue,
3382    ) -> Result<(), RuntimeConfigError> {
3383        let (name, setting) = self.split_generic_interface_runtime_key(key)?;
3384        let (id, _) = self
3385            .interfaces
3386            .iter()
3387            .find(|(_, entry)| entry.info.name == name)
3388            .map(|(id, entry)| (*id, entry))
3389            .ok_or(RuntimeConfigError {
3390                code: RuntimeConfigErrorCode::NotFound,
3391                message: format!("interface '{}' not found", name),
3392            })?;
3393        let entry = self.interfaces.get_mut(&id).unwrap();
3394        match setting {
3395            "enabled" => {
3396                entry.enabled = Self::expect_bool(value, key)?;
3397            }
3398            "ifac_netname" => {
3399                let runtime =
3400                    self.interface_ifac_runtime
3401                        .get_mut(name)
3402                        .ok_or(RuntimeConfigError {
3403                            code: RuntimeConfigErrorCode::UnknownKey,
3404                            message: format!("unknown runtime-config key '{}'", key),
3405                        })?;
3406                runtime.netname = match value {
3407                    RuntimeConfigValue::Null => None,
3408                    RuntimeConfigValue::String(value) => Some(value),
3409                    _ => {
3410                        return Err(RuntimeConfigError {
3411                            code: RuntimeConfigErrorCode::InvalidType,
3412                            message: format!("{} expects a string or null", key),
3413                        })
3414                    }
3415                };
3416                Self::apply_interface_ifac_runtime(entry, runtime);
3417            }
3418            "ifac_passphrase" => {
3419                let runtime =
3420                    self.interface_ifac_runtime
3421                        .get_mut(name)
3422                        .ok_or(RuntimeConfigError {
3423                            code: RuntimeConfigErrorCode::UnknownKey,
3424                            message: format!("unknown runtime-config key '{}'", key),
3425                        })?;
3426                runtime.netkey = match value {
3427                    RuntimeConfigValue::Null => None,
3428                    RuntimeConfigValue::String(value) => Some(value),
3429                    _ => {
3430                        return Err(RuntimeConfigError {
3431                            code: RuntimeConfigErrorCode::InvalidType,
3432                            message: format!("{} expects a string or null", key),
3433                        })
3434                    }
3435                };
3436                Self::apply_interface_ifac_runtime(entry, runtime);
3437            }
3438            "ifac_size_bytes" => {
3439                let runtime =
3440                    self.interface_ifac_runtime
3441                        .get_mut(name)
3442                        .ok_or(RuntimeConfigError {
3443                            code: RuntimeConfigErrorCode::UnknownKey,
3444                            message: format!("unknown runtime-config key '{}'", key),
3445                        })?;
3446                runtime.size =
3447                    (Self::expect_u64(value, key)? as usize).max(crate::ifac::IFAC_MIN_SIZE);
3448                Self::apply_interface_ifac_runtime(entry, runtime);
3449            }
3450            "mode" => {
3451                entry.info.mode = Self::parse_interface_mode(&value).ok_or(RuntimeConfigError {
3452                    code: RuntimeConfigErrorCode::InvalidValue,
3453                    message: format!("{} must be a valid interface mode", key),
3454                })?;
3455            }
3456            "announce_rate_target" => {
3457                entry.info.announce_rate_target = match value {
3458                    RuntimeConfigValue::Null => None,
3459                    RuntimeConfigValue::Float(v) if v >= 0.0 => Some(v),
3460                    RuntimeConfigValue::Int(v) if v >= 0 => Some(v as f64),
3461                    RuntimeConfigValue::Float(_) | RuntimeConfigValue::Int(_) => {
3462                        return Err(RuntimeConfigError {
3463                            code: RuntimeConfigErrorCode::InvalidValue,
3464                            message: format!("{} must be >= 0", key),
3465                        })
3466                    }
3467                    _ => {
3468                        return Err(RuntimeConfigError {
3469                            code: RuntimeConfigErrorCode::InvalidType,
3470                            message: format!("{} expects a numeric value or null", key),
3471                        })
3472                    }
3473                };
3474            }
3475            "announce_rate_grace" => {
3476                entry.info.announce_rate_grace = Self::expect_u64(value, key)? as u32
3477            }
3478            "announce_rate_penalty" => {
3479                entry.info.announce_rate_penalty = Self::expect_f64(value, key)?
3480            }
3481            "announce_cap" => entry.info.announce_cap = Self::expect_f64(value, key)?,
3482            "ingress_control" => {
3483                entry.info.ingress_control.enabled = Self::expect_bool(value, key)?
3484            }
3485            "ic_max_held_announces" => {
3486                entry.info.ingress_control.max_held_announces =
3487                    Self::expect_u64(value, key)? as usize
3488            }
3489            "ic_burst_hold" => {
3490                entry.info.ingress_control.burst_hold = Self::expect_f64(value, key)?
3491            }
3492            "ic_burst_freq_new" => {
3493                entry.info.ingress_control.burst_freq_new = Self::expect_f64(value, key)?
3494            }
3495            "ic_burst_freq" => {
3496                entry.info.ingress_control.burst_freq = Self::expect_f64(value, key)?
3497            }
3498            "ic_new_time" => entry.info.ingress_control.new_time = Self::expect_f64(value, key)?,
3499            "ic_burst_penalty" => {
3500                entry.info.ingress_control.burst_penalty = Self::expect_f64(value, key)?
3501            }
3502            "ic_held_release_interval" => {
3503                entry.info.ingress_control.held_release_interval = Self::expect_f64(value, key)?
3504            }
3505            _ => {
3506                return Err(RuntimeConfigError {
3507                    code: RuntimeConfigErrorCode::UnknownKey,
3508                    message: format!("unknown runtime-config key '{}'", key),
3509                })
3510            }
3511        }
3512        let info = entry.info.clone();
3513        self.engine.register_interface(info);
3514        Ok(())
3515    }
3516
3517    fn reset_generic_interface_runtime_config(
3518        &mut self,
3519        key: &str,
3520    ) -> Result<(), RuntimeConfigError> {
3521        let (name, setting) = self.split_generic_interface_runtime_key(key)?;
3522        let startup =
3523            self.interface_runtime_defaults
3524                .get(name)
3525                .cloned()
3526                .ok_or(RuntimeConfigError {
3527                    code: RuntimeConfigErrorCode::NotFound,
3528                    message: format!("interface '{}' not found", name),
3529                })?;
3530        let entry = self
3531            .interfaces
3532            .values_mut()
3533            .find(|entry| entry.info.name == name)
3534            .ok_or(RuntimeConfigError {
3535                code: RuntimeConfigErrorCode::NotFound,
3536                message: format!("interface '{}' not found", name),
3537            })?;
3538        match setting {
3539            "enabled" => entry.enabled = true,
3540            "ifac_netname" => {
3541                let startup_ifac =
3542                    self.interface_ifac_runtime_defaults
3543                        .get(name)
3544                        .ok_or(RuntimeConfigError {
3545                            code: RuntimeConfigErrorCode::UnknownKey,
3546                            message: format!("unknown runtime-config key '{}'", key),
3547                        })?;
3548                let runtime =
3549                    self.interface_ifac_runtime
3550                        .get_mut(name)
3551                        .ok_or(RuntimeConfigError {
3552                            code: RuntimeConfigErrorCode::UnknownKey,
3553                            message: format!("unknown runtime-config key '{}'", key),
3554                        })?;
3555                runtime.netname = startup_ifac.netname.clone();
3556                Self::apply_interface_ifac_runtime(entry, runtime);
3557            }
3558            "ifac_passphrase" => {
3559                let startup_ifac =
3560                    self.interface_ifac_runtime_defaults
3561                        .get(name)
3562                        .ok_or(RuntimeConfigError {
3563                            code: RuntimeConfigErrorCode::UnknownKey,
3564                            message: format!("unknown runtime-config key '{}'", key),
3565                        })?;
3566                let runtime =
3567                    self.interface_ifac_runtime
3568                        .get_mut(name)
3569                        .ok_or(RuntimeConfigError {
3570                            code: RuntimeConfigErrorCode::UnknownKey,
3571                            message: format!("unknown runtime-config key '{}'", key),
3572                        })?;
3573                runtime.netkey = startup_ifac.netkey.clone();
3574                Self::apply_interface_ifac_runtime(entry, runtime);
3575            }
3576            "ifac_size_bytes" => {
3577                let startup_ifac =
3578                    self.interface_ifac_runtime_defaults
3579                        .get(name)
3580                        .ok_or(RuntimeConfigError {
3581                            code: RuntimeConfigErrorCode::UnknownKey,
3582                            message: format!("unknown runtime-config key '{}'", key),
3583                        })?;
3584                let runtime =
3585                    self.interface_ifac_runtime
3586                        .get_mut(name)
3587                        .ok_or(RuntimeConfigError {
3588                            code: RuntimeConfigErrorCode::UnknownKey,
3589                            message: format!("unknown runtime-config key '{}'", key),
3590                        })?;
3591                runtime.size = startup_ifac.size;
3592                Self::apply_interface_ifac_runtime(entry, runtime);
3593            }
3594            "mode" => entry.info.mode = startup.mode,
3595            "announce_rate_target" => {
3596                entry.info.announce_rate_target = startup.announce_rate_target
3597            }
3598            "announce_rate_grace" => entry.info.announce_rate_grace = startup.announce_rate_grace,
3599            "announce_rate_penalty" => {
3600                entry.info.announce_rate_penalty = startup.announce_rate_penalty
3601            }
3602            "announce_cap" => entry.info.announce_cap = startup.announce_cap,
3603            "ingress_control" => {
3604                entry.info.ingress_control.enabled = startup.ingress_control.enabled
3605            }
3606            "ic_max_held_announces" => {
3607                entry.info.ingress_control.max_held_announces =
3608                    startup.ingress_control.max_held_announces
3609            }
3610            "ic_burst_hold" => {
3611                entry.info.ingress_control.burst_hold = startup.ingress_control.burst_hold
3612            }
3613            "ic_burst_freq_new" => {
3614                entry.info.ingress_control.burst_freq_new = startup.ingress_control.burst_freq_new
3615            }
3616            "ic_burst_freq" => {
3617                entry.info.ingress_control.burst_freq = startup.ingress_control.burst_freq
3618            }
3619            "ic_new_time" => entry.info.ingress_control.new_time = startup.ingress_control.new_time,
3620            "ic_burst_penalty" => {
3621                entry.info.ingress_control.burst_penalty = startup.ingress_control.burst_penalty
3622            }
3623            "ic_held_release_interval" => {
3624                entry.info.ingress_control.held_release_interval =
3625                    startup.ingress_control.held_release_interval
3626            }
3627            _ => {
3628                return Err(RuntimeConfigError {
3629                    code: RuntimeConfigErrorCode::UnknownKey,
3630                    message: format!("unknown runtime-config key '{}'", key),
3631                })
3632            }
3633        }
3634        let info = entry.info.clone();
3635        self.engine.register_interface(info);
3636        Ok(())
3637    }
3638
3639    #[cfg(feature = "iface-tcp")]
3640    fn tcp_server_runtime_entry(&self, key: &str) -> Option<RuntimeConfigEntry> {
3641        let rest = key.strip_prefix("tcp_server.")?;
3642        let (name, setting) = rest.split_once('.')?;
3643        if matches!(
3644            setting,
3645            "discoverable"
3646                | "discovery_name"
3647                | "announce_interval_secs"
3648                | "reachable_on"
3649                | "stamp_value"
3650                | "latitude"
3651                | "longitude"
3652                | "height"
3653        ) {
3654            let handle = self.tcp_server_discovery_runtime.get(name)?;
3655            let current = &handle.current;
3656            let startup = &handle.startup;
3657            let make_entry = |value: RuntimeConfigValue,
3658                              default: RuntimeConfigValue,
3659                              apply_mode: RuntimeConfigApplyMode,
3660                              description: &str|
3661             -> RuntimeConfigEntry {
3662                RuntimeConfigEntry {
3663                    key: key.to_string(),
3664                    source: if value == default {
3665                        RuntimeConfigSource::Startup
3666                    } else {
3667                        RuntimeConfigSource::RuntimeOverride
3668                    },
3669                    value,
3670                    default,
3671                    apply_mode,
3672                    description: Some(description.to_string()),
3673                }
3674            };
3675            return match setting {
3676                "discoverable" => Some(make_entry(
3677                    RuntimeConfigValue::Bool(current.discoverable),
3678                    RuntimeConfigValue::Bool(startup.discoverable),
3679                    RuntimeConfigApplyMode::Immediate,
3680                    "Whether this TCP server interface is advertised through interface discovery.",
3681                )),
3682                "discovery_name" => Some(make_entry(
3683                    RuntimeConfigValue::String(current.config.discovery_name.clone()),
3684                    RuntimeConfigValue::String(startup.config.discovery_name.clone()),
3685                    RuntimeConfigApplyMode::Immediate,
3686                    "Human-readable discovery name advertised for this TCP server interface.",
3687                )),
3688                "announce_interval_secs" => Some(make_entry(
3689                    RuntimeConfigValue::Int(current.config.announce_interval as i64),
3690                    RuntimeConfigValue::Int(startup.config.announce_interval as i64),
3691                    RuntimeConfigApplyMode::Immediate,
3692                    "Discovery announce interval for this TCP server interface in seconds.",
3693                )),
3694                "reachable_on" => Some(make_entry(
3695                    current
3696                        .config
3697                        .reachable_on
3698                        .clone()
3699                        .map(RuntimeConfigValue::String)
3700                        .unwrap_or(RuntimeConfigValue::Null),
3701                    startup
3702                        .config
3703                        .reachable_on
3704                        .clone()
3705                        .map(RuntimeConfigValue::String)
3706                        .unwrap_or(RuntimeConfigValue::Null),
3707                    RuntimeConfigApplyMode::Immediate,
3708                    "Reachable hostname or IP advertised for this TCP server interface; null clears it.",
3709                )),
3710                "stamp_value" => Some(make_entry(
3711                    RuntimeConfigValue::Int(current.config.stamp_value as i64),
3712                    RuntimeConfigValue::Int(startup.config.stamp_value as i64),
3713                    RuntimeConfigApplyMode::Immediate,
3714                    "Discovery proof-of-work stamp cost for this TCP server interface.",
3715                )),
3716                "latitude" => Some(make_entry(
3717                    current
3718                        .config
3719                        .latitude
3720                        .map(RuntimeConfigValue::Float)
3721                        .unwrap_or(RuntimeConfigValue::Null),
3722                    startup
3723                        .config
3724                        .latitude
3725                        .map(RuntimeConfigValue::Float)
3726                        .unwrap_or(RuntimeConfigValue::Null),
3727                    RuntimeConfigApplyMode::Immediate,
3728                    "Latitude advertised for this TCP server interface; null clears it.",
3729                )),
3730                "longitude" => Some(make_entry(
3731                    current
3732                        .config
3733                        .longitude
3734                        .map(RuntimeConfigValue::Float)
3735                        .unwrap_or(RuntimeConfigValue::Null),
3736                    startup
3737                        .config
3738                        .longitude
3739                        .map(RuntimeConfigValue::Float)
3740                        .unwrap_or(RuntimeConfigValue::Null),
3741                    RuntimeConfigApplyMode::Immediate,
3742                    "Longitude advertised for this TCP server interface; null clears it.",
3743                )),
3744                "height" => Some(make_entry(
3745                    current
3746                        .config
3747                        .height
3748                        .map(RuntimeConfigValue::Float)
3749                        .unwrap_or(RuntimeConfigValue::Null),
3750                    startup
3751                        .config
3752                        .height
3753                        .map(RuntimeConfigValue::Float)
3754                        .unwrap_or(RuntimeConfigValue::Null),
3755                    RuntimeConfigApplyMode::Immediate,
3756                    "Height advertised for this TCP server interface; null clears it.",
3757                )),
3758                _ => None,
3759            };
3760        }
3761
3762        let handle = self.tcp_server_runtime.get(name)?;
3763        let current = handle.runtime.lock().unwrap().clone();
3764        let startup = handle.startup.clone();
3765        match setting {
3766            "max_connections" => Some(RuntimeConfigEntry {
3767                key: key.to_string(),
3768                value: RuntimeConfigValue::Int(current.max_connections.unwrap_or(0) as i64),
3769                default: RuntimeConfigValue::Int(startup.max_connections.unwrap_or(0) as i64),
3770                source: if current.max_connections == startup.max_connections {
3771                    RuntimeConfigSource::Startup
3772                } else {
3773                    RuntimeConfigSource::RuntimeOverride
3774                },
3775                apply_mode: RuntimeConfigApplyMode::NewConnectionsOnly,
3776                description: Some(
3777                    "Maximum simultaneous inbound TCP server connections; 0 disables the cap."
3778                        .to_string(),
3779                ),
3780            }),
3781            _ => None,
3782        }
3783    }
3784
3785    #[cfg(feature = "iface-tcp")]
3786    fn split_tcp_server_runtime_key<'a>(
3787        &self,
3788        key: &'a str,
3789    ) -> Result<(&'a str, &'a str), RuntimeConfigError> {
3790        let rest = key.strip_prefix("tcp_server.").ok_or(RuntimeConfigError {
3791            code: RuntimeConfigErrorCode::UnknownKey,
3792            message: format!("unknown runtime-config key '{}'", key),
3793        })?;
3794        rest.split_once('.').ok_or(RuntimeConfigError {
3795            code: RuntimeConfigErrorCode::UnknownKey,
3796            message: format!("unknown runtime-config key '{}'", key),
3797        })
3798    }
3799
3800    #[cfg(feature = "iface-tcp")]
3801    fn set_tcp_server_runtime_config(
3802        &mut self,
3803        key: &str,
3804        value: RuntimeConfigValue,
3805    ) -> Result<(), RuntimeConfigError> {
3806        let (name, setting) = self.split_tcp_server_runtime_key(key)?;
3807        if matches!(
3808            setting,
3809            "discoverable"
3810                | "discovery_name"
3811                | "announce_interval_secs"
3812                | "reachable_on"
3813                | "stamp_value"
3814                | "latitude"
3815                | "longitude"
3816                | "height"
3817        ) {
3818            return self.set_tcp_server_discovery_runtime_config(key, value);
3819        }
3820        let handle = self
3821            .tcp_server_runtime
3822            .get(name)
3823            .ok_or(RuntimeConfigError {
3824                code: RuntimeConfigErrorCode::NotFound,
3825                message: format!("tcp server interface '{}' not found", name),
3826            })?;
3827        let mut runtime = handle.runtime.lock().unwrap();
3828        match setting {
3829            "max_connections" => {
3830                runtime.max_connections = Self::set_optional_usize(value, key)?;
3831                Ok(())
3832            }
3833            _ => Err(RuntimeConfigError {
3834                code: RuntimeConfigErrorCode::UnknownKey,
3835                message: format!("unknown runtime-config key '{}'", key),
3836            }),
3837        }
3838    }
3839
3840    #[cfg(feature = "iface-tcp")]
3841    fn reset_tcp_server_runtime_config(&mut self, key: &str) -> Result<(), RuntimeConfigError> {
3842        let (name, setting) = self.split_tcp_server_runtime_key(key)?;
3843        if matches!(
3844            setting,
3845            "discoverable"
3846                | "discovery_name"
3847                | "announce_interval_secs"
3848                | "reachable_on"
3849                | "stamp_value"
3850                | "latitude"
3851                | "longitude"
3852                | "height"
3853        ) {
3854            return self.reset_tcp_server_discovery_runtime_config(key);
3855        }
3856        let handle = self
3857            .tcp_server_runtime
3858            .get(name)
3859            .ok_or(RuntimeConfigError {
3860                code: RuntimeConfigErrorCode::NotFound,
3861                message: format!("tcp server interface '{}' not found", name),
3862            })?;
3863        let mut runtime = handle.runtime.lock().unwrap();
3864        let startup = handle.startup.clone();
3865        match setting {
3866            "max_connections" => runtime.max_connections = startup.max_connections,
3867            _ => {
3868                return Err(RuntimeConfigError {
3869                    code: RuntimeConfigErrorCode::UnknownKey,
3870                    message: format!("unknown runtime-config key '{}'", key),
3871                })
3872            }
3873        }
3874        Ok(())
3875    }
3876
3877    #[cfg(feature = "iface-tcp")]
3878    fn set_tcp_server_discovery_runtime_config(
3879        &mut self,
3880        key: &str,
3881        value: RuntimeConfigValue,
3882    ) -> Result<(), RuntimeConfigError> {
3883        let (name, setting) = self.split_tcp_server_runtime_key(key)?;
3884        let handle = self
3885            .tcp_server_discovery_runtime
3886            .get_mut(name)
3887            .ok_or(RuntimeConfigError {
3888                code: RuntimeConfigErrorCode::NotFound,
3889                message: format!("tcp server interface '{}' not found", name),
3890            })?;
3891        match setting {
3892            "discoverable" => handle.current.discoverable = Self::expect_bool(value, key)?,
3893            "discovery_name" => {
3894                handle.current.config.discovery_name = Self::expect_string(value, key)?
3895            }
3896            "announce_interval_secs" => {
3897                let secs = Self::expect_u64(value, key)?;
3898                if secs < 300 {
3899                    return Err(RuntimeConfigError {
3900                        code: RuntimeConfigErrorCode::InvalidValue,
3901                        message: format!("{} must be >= 300", key),
3902                    });
3903                }
3904                handle.current.config.announce_interval = secs;
3905            }
3906            "reachable_on" => {
3907                handle.current.config.reachable_on = Self::expect_optional_string(value, key)?
3908            }
3909            "stamp_value" => {
3910                let raw = Self::expect_u64(value, key)?;
3911                if raw > u8::MAX as u64 {
3912                    return Err(RuntimeConfigError {
3913                        code: RuntimeConfigErrorCode::InvalidValue,
3914                        message: format!("{} must be <= {}", key, u8::MAX),
3915                    });
3916                }
3917                handle.current.config.stamp_value = raw as u8;
3918            }
3919            "latitude" => handle.current.config.latitude = Self::expect_optional_f64(value, key)?,
3920            "longitude" => handle.current.config.longitude = Self::expect_optional_f64(value, key)?,
3921            "height" => handle.current.config.height = Self::expect_optional_f64(value, key)?,
3922            _ => {
3923                return Err(RuntimeConfigError {
3924                    code: RuntimeConfigErrorCode::UnknownKey,
3925                    message: format!("unknown runtime-config key '{}'", key),
3926                })
3927            }
3928        }
3929        self.sync_tcp_server_discovery_runtime(name)
3930    }
3931
3932    #[cfg(feature = "iface-tcp")]
3933    fn reset_tcp_server_discovery_runtime_config(
3934        &mut self,
3935        key: &str,
3936    ) -> Result<(), RuntimeConfigError> {
3937        let (name, setting) = self.split_tcp_server_runtime_key(key)?;
3938        let handle = self
3939            .tcp_server_discovery_runtime
3940            .get_mut(name)
3941            .ok_or(RuntimeConfigError {
3942                code: RuntimeConfigErrorCode::NotFound,
3943                message: format!("tcp server interface '{}' not found", name),
3944            })?;
3945        match setting {
3946            "discoverable" => handle.current.discoverable = handle.startup.discoverable,
3947            "discovery_name" => {
3948                handle.current.config.discovery_name = handle.startup.config.discovery_name.clone()
3949            }
3950            "announce_interval_secs" => {
3951                handle.current.config.announce_interval = handle.startup.config.announce_interval
3952            }
3953            "reachable_on" => {
3954                handle.current.config.reachable_on = handle.startup.config.reachable_on.clone()
3955            }
3956            "stamp_value" => handle.current.config.stamp_value = handle.startup.config.stamp_value,
3957            "latitude" => handle.current.config.latitude = handle.startup.config.latitude,
3958            "longitude" => handle.current.config.longitude = handle.startup.config.longitude,
3959            "height" => handle.current.config.height = handle.startup.config.height,
3960            _ => {
3961                return Err(RuntimeConfigError {
3962                    code: RuntimeConfigErrorCode::UnknownKey,
3963                    message: format!("unknown runtime-config key '{}'", key),
3964                })
3965            }
3966        }
3967        self.sync_tcp_server_discovery_runtime(name)
3968    }
3969
3970    #[cfg(feature = "iface-backbone")]
3971    fn list_backbone_runtime_config(&self) -> Vec<RuntimeConfigEntry> {
3972        let mut entries = Vec::new();
3973        let mut names: Vec<&String> = self.backbone_runtime.keys().collect();
3974        names.sort();
3975        for name in names {
3976            for suffix in [
3977                "idle_timeout_secs",
3978                "write_stall_timeout_secs",
3979                "max_penalty_duration_secs",
3980                "max_connections",
3981            ] {
3982                let key = format!("backbone.{}.{}", name, suffix);
3983                if let Some(entry) = self.backbone_runtime_entry(&key) {
3984                    entries.push(entry);
3985                }
3986            }
3987            for suffix in [
3988                "discoverable",
3989                "discovery_name",
3990                "announce_interval_secs",
3991                "reachable_on",
3992                "stamp_value",
3993                "latitude",
3994                "longitude",
3995                "height",
3996            ] {
3997                let key = format!("backbone.{}.{}", name, suffix);
3998                if let Some(entry) = self.backbone_runtime_entry(&key) {
3999                    entries.push(entry);
4000                }
4001            }
4002        }
4003        entries
4004    }
4005
4006    #[cfg(feature = "iface-backbone")]
4007    fn backbone_runtime_entry(&self, key: &str) -> Option<RuntimeConfigEntry> {
4008        let rest = key.strip_prefix("backbone.")?;
4009        let (name, setting) = rest.split_once('.')?;
4010
4011        let make_entry = |value: RuntimeConfigValue,
4012                          default: RuntimeConfigValue,
4013                          apply_mode: RuntimeConfigApplyMode,
4014                          description: &str| RuntimeConfigEntry {
4015            key: key.to_string(),
4016            source: if value == default {
4017                RuntimeConfigSource::Startup
4018            } else {
4019                RuntimeConfigSource::RuntimeOverride
4020            },
4021            value,
4022            default,
4023            apply_mode,
4024            description: Some(description.to_string()),
4025        };
4026
4027        if matches!(
4028            setting,
4029            "discoverable"
4030                | "discovery_name"
4031                | "announce_interval_secs"
4032                | "reachable_on"
4033                | "stamp_value"
4034                | "latitude"
4035                | "longitude"
4036                | "height"
4037        ) {
4038            let handle = self.backbone_discovery_runtime.get(name)?;
4039            let current = &handle.current;
4040            let startup = &handle.startup;
4041            return match setting {
4042                "discoverable" => Some(make_entry(
4043                    RuntimeConfigValue::Bool(current.discoverable),
4044                    RuntimeConfigValue::Bool(startup.discoverable),
4045                    RuntimeConfigApplyMode::Immediate,
4046                    "Whether this backbone interface is advertised through interface discovery.",
4047                )),
4048                "discovery_name" => Some(make_entry(
4049                    RuntimeConfigValue::String(current.config.discovery_name.clone()),
4050                    RuntimeConfigValue::String(startup.config.discovery_name.clone()),
4051                    RuntimeConfigApplyMode::Immediate,
4052                    "Human-readable discovery name advertised for this backbone interface.",
4053                )),
4054                "announce_interval_secs" => Some(make_entry(
4055                    RuntimeConfigValue::Int(current.config.announce_interval as i64),
4056                    RuntimeConfigValue::Int(startup.config.announce_interval as i64),
4057                    RuntimeConfigApplyMode::Immediate,
4058                    "Discovery announce interval for this backbone interface in seconds.",
4059                )),
4060                "reachable_on" => Some(make_entry(
4061                    current
4062                        .config
4063                        .reachable_on
4064                        .clone()
4065                        .map(RuntimeConfigValue::String)
4066                        .unwrap_or(RuntimeConfigValue::Null),
4067                    startup
4068                        .config
4069                        .reachable_on
4070                        .clone()
4071                        .map(RuntimeConfigValue::String)
4072                        .unwrap_or(RuntimeConfigValue::Null),
4073                    RuntimeConfigApplyMode::Immediate,
4074                    "Reachable hostname or IP advertised for this backbone interface; null clears it.",
4075                )),
4076                "stamp_value" => Some(make_entry(
4077                    RuntimeConfigValue::Int(current.config.stamp_value as i64),
4078                    RuntimeConfigValue::Int(startup.config.stamp_value as i64),
4079                    RuntimeConfigApplyMode::Immediate,
4080                    "Discovery proof-of-work stamp cost for this backbone interface.",
4081                )),
4082                "latitude" => Some(make_entry(
4083                    current
4084                        .config
4085                        .latitude
4086                        .map(RuntimeConfigValue::Float)
4087                        .unwrap_or(RuntimeConfigValue::Null),
4088                    startup
4089                        .config
4090                        .latitude
4091                        .map(RuntimeConfigValue::Float)
4092                        .unwrap_or(RuntimeConfigValue::Null),
4093                    RuntimeConfigApplyMode::Immediate,
4094                    "Latitude advertised for this backbone interface; null clears it.",
4095                )),
4096                "longitude" => Some(make_entry(
4097                    current
4098                        .config
4099                        .longitude
4100                        .map(RuntimeConfigValue::Float)
4101                        .unwrap_or(RuntimeConfigValue::Null),
4102                    startup
4103                        .config
4104                        .longitude
4105                        .map(RuntimeConfigValue::Float)
4106                        .unwrap_or(RuntimeConfigValue::Null),
4107                    RuntimeConfigApplyMode::Immediate,
4108                    "Longitude advertised for this backbone interface; null clears it.",
4109                )),
4110                "height" => Some(make_entry(
4111                    current
4112                        .config
4113                        .height
4114                        .map(RuntimeConfigValue::Float)
4115                        .unwrap_or(RuntimeConfigValue::Null),
4116                    startup
4117                        .config
4118                        .height
4119                        .map(RuntimeConfigValue::Float)
4120                        .unwrap_or(RuntimeConfigValue::Null),
4121                    RuntimeConfigApplyMode::Immediate,
4122                    "Height advertised for this backbone interface; null clears it.",
4123                )),
4124                _ => None,
4125            };
4126        }
4127
4128        if let Some(handle) = self.backbone_runtime.get(name) {
4129            let current = handle.runtime.lock().unwrap().clone();
4130            let startup = handle.startup.clone();
4131            return match setting {
4132                "idle_timeout_secs" => Some(make_entry(
4133                    RuntimeConfigValue::Float(current.idle_timeout.map(|d| d.as_secs_f64()).unwrap_or(0.0)),
4134                    RuntimeConfigValue::Float(startup.idle_timeout.map(|d| d.as_secs_f64()).unwrap_or(0.0)),
4135                    RuntimeConfigApplyMode::Immediate,
4136                    "Disconnect silent inbound peers after this many seconds; 0 disables the timeout.",
4137                )),
4138                "write_stall_timeout_secs" => Some(make_entry(
4139                    RuntimeConfigValue::Float(current.write_stall_timeout.map(|d| d.as_secs_f64()).unwrap_or(0.0)),
4140                    RuntimeConfigValue::Float(startup.write_stall_timeout.map(|d| d.as_secs_f64()).unwrap_or(0.0)),
4141                    RuntimeConfigApplyMode::Immediate,
4142                    "Disconnect peers whose send buffer remains unwritable for this many seconds; 0 disables the timeout.",
4143                )),
4144                "max_penalty_duration_secs" => Some(make_entry(
4145                    RuntimeConfigValue::Float(current.abuse.max_penalty_duration.map(|d| d.as_secs_f64()).unwrap_or(0.0)),
4146                    RuntimeConfigValue::Float(startup.abuse.max_penalty_duration.map(|d| d.as_secs_f64()).unwrap_or(0.0)),
4147                    RuntimeConfigApplyMode::Immediate,
4148                    "Maximum accepted backbone blacklist duration; 0 means no cap.",
4149                )),
4150                "max_connections" => Some(make_entry(
4151                    RuntimeConfigValue::Int(current.max_connections.unwrap_or(0) as i64),
4152                    RuntimeConfigValue::Int(startup.max_connections.unwrap_or(0) as i64),
4153                    RuntimeConfigApplyMode::NewConnectionsOnly,
4154                    "Maximum simultaneous inbound backbone connections; 0 disables the cap.",
4155                )),
4156                _ => None,
4157            };
4158        }
4159
4160        None
4161    }
4162
4163    #[cfg(feature = "rns-hooks")]
4164    fn forward_hook_side_effects(&mut self, attach_point: &str, exec: &rns_hooks::ExecuteResult) {
4165        if !exec.injected_actions.is_empty() {
4166            self.dispatch_all(convert_injected_actions(exec.injected_actions.clone()));
4167        }
4168        if let Some(ref bridge) = self.provider_bridge {
4169            for event in &exec.provider_events {
4170                bridge.emit_event(
4171                    attach_point,
4172                    event.hook_name.clone(),
4173                    event.payload_type.clone(),
4174                    event.payload.clone(),
4175                );
4176            }
4177        }
4178    }
4179
4180    #[cfg(feature = "rns-hooks")]
4181    fn collect_hook_side_effects(
4182        &mut self,
4183        attach_point: &str,
4184        exec: &rns_hooks::ExecuteResult,
4185        out: &mut Vec<TransportAction>,
4186    ) {
4187        if !exec.injected_actions.is_empty() {
4188            out.extend(convert_injected_actions(exec.injected_actions.clone()));
4189        }
4190        if let Some(ref bridge) = self.provider_bridge {
4191            for event in &exec.provider_events {
4192                bridge.emit_event(
4193                    attach_point,
4194                    event.hook_name.clone(),
4195                    event.payload_type.clone(),
4196                    event.payload.clone(),
4197                );
4198            }
4199        }
4200    }
4201
4202    /// Set the probe addresses, protocol, and optional device for hole punching.
4203    pub fn set_probe_config(
4204        &mut self,
4205        addrs: Vec<std::net::SocketAddr>,
4206        protocol: rns_core::holepunch::ProbeProtocol,
4207        device: Option<String>,
4208    ) {
4209        self.holepunch_manager = HolePunchManager::new(addrs, protocol, device);
4210    }
4211
4212    /// Run the event loop. Blocks until Shutdown or all senders are dropped.
4213    pub fn run(&mut self) {
4214        loop {
4215            let event = match self.rx.recv() {
4216                Ok(e) => e,
4217                Err(_) => break, // all senders dropped
4218            };
4219
4220            match event {
4221                Event::Frame { interface_id, data } => {
4222                    // Log incoming announces
4223                    if data.len() > 2 && (data[0] & 0x03) == 0x01 {
4224                        log::debug!(
4225                            "Announce:frame from iface {} (len={}, flags=0x{:02x})",
4226                            interface_id.0,
4227                            data.len(),
4228                            data[0]
4229                        );
4230                    }
4231                    if let Some(entry) = self.interfaces.get(&interface_id) {
4232                        if !entry.enabled || !entry.online {
4233                            continue;
4234                        }
4235                    }
4236                    // Update rx stats
4237                    if let Some(entry) = self.interfaces.get_mut(&interface_id) {
4238                        entry.stats.rxb += data.len() as u64;
4239                        entry.stats.rx_packets += 1;
4240                    }
4241
4242                    // IFAC inbound processing
4243                    let packet = if let Some(entry) = self.interfaces.get(&interface_id) {
4244                        if let Some(ref ifac_state) = entry.ifac {
4245                            // Interface has IFAC enabled — unmask
4246                            match ifac::unmask_inbound(&data, ifac_state) {
4247                                Some(unmasked) => unmasked,
4248                                None => {
4249                                    log::debug!("[{}] IFAC rejected packet", interface_id.0);
4250                                    continue;
4251                                }
4252                            }
4253                        } else {
4254                            // No IFAC — drop if IFAC flag is set
4255                            if data.len() > 2 && data[0] & 0x80 == 0x80 {
4256                                log::debug!(
4257                                    "[{}] dropping packet with IFAC flag on non-IFAC interface",
4258                                    interface_id.0
4259                                );
4260                                continue;
4261                            }
4262                            data
4263                        }
4264                    } else {
4265                        data
4266                    };
4267
4268                    // PreIngress hook: after IFAC, before engine processing
4269                    #[cfg(feature = "rns-hooks")]
4270                    {
4271                        let pkt_ctx = rns_hooks::PacketContext {
4272                            flags: if packet.is_empty() { 0 } else { packet[0] },
4273                            hops: if packet.len() > 1 { packet[1] } else { 0 },
4274                            destination_hash: extract_dest_hash(&packet),
4275                            context: 0,
4276                            packet_hash: [0; 32],
4277                            interface_id: interface_id.0,
4278                            data_offset: 0,
4279                            data_len: packet.len() as u32,
4280                        };
4281                        let ctx = HookContext::Packet {
4282                            ctx: &pkt_ctx,
4283                            raw: &packet,
4284                        };
4285                        let now = time::now();
4286                        let engine_ref = EngineRef {
4287                            engine: &self.engine,
4288                            interfaces: &self.interfaces,
4289                            link_manager: &self.link_manager,
4290                            now,
4291                        };
4292                        let provider_events_enabled = self.provider_events_enabled();
4293                        {
4294                            let exec = run_hook_inner(
4295                                &mut self.hook_slots[HookPoint::PreIngress as usize].programs,
4296                                &self.hook_manager,
4297                                &engine_ref,
4298                                &ctx,
4299                                now,
4300                                provider_events_enabled,
4301                            );
4302                            if let Some(ref e) = exec {
4303                                self.forward_hook_side_effects("PreIngress", e);
4304                                if e.hook_result.as_ref().map_or(false, |r| r.is_drop()) {
4305                                    continue;
4306                                }
4307                            }
4308                        }
4309                    }
4310
4311                    // Record incoming announce for frequency tracking (before engine processing)
4312                    if packet.len() > 2 && (packet[0] & 0x03) == 0x01 {
4313                        let now = time::now();
4314                        if let Some(entry) = self.interfaces.get_mut(&interface_id) {
4315                            entry.stats.record_incoming_announce(now);
4316                        }
4317                    }
4318
4319                    // Sync announce frequency to engine before processing
4320                    if let Some(entry) = self.interfaces.get(&interface_id) {
4321                        self.engine.update_interface_freq(
4322                            interface_id,
4323                            entry.stats.incoming_announce_freq(),
4324                        );
4325                    }
4326
4327                    let actions = if self.async_announce_verification {
4328                        let mut announce_queue = self
4329                            .announce_verify_queue
4330                            .lock()
4331                            .unwrap_or_else(|poisoned| poisoned.into_inner());
4332                        self.engine.handle_inbound_with_announce_queue(
4333                            &packet,
4334                            interface_id,
4335                            time::now(),
4336                            &mut self.rng,
4337                            Some(&mut announce_queue),
4338                        )
4339                    } else {
4340                        self.engine.handle_inbound(
4341                            &packet,
4342                            interface_id,
4343                            time::now(),
4344                            &mut self.rng,
4345                        )
4346                    };
4347
4348                    // PreDispatch hook: after engine, before action dispatch
4349                    #[cfg(feature = "rns-hooks")]
4350                    {
4351                        let pkt_ctx2 = rns_hooks::PacketContext {
4352                            flags: if packet.is_empty() { 0 } else { packet[0] },
4353                            hops: if packet.len() > 1 { packet[1] } else { 0 },
4354                            destination_hash: extract_dest_hash(&packet),
4355                            context: 0,
4356                            packet_hash: [0; 32],
4357                            interface_id: interface_id.0,
4358                            data_offset: 0,
4359                            data_len: packet.len() as u32,
4360                        };
4361                        let ctx = HookContext::Packet {
4362                            ctx: &pkt_ctx2,
4363                            raw: &packet,
4364                        };
4365                        let now = time::now();
4366                        let engine_ref = EngineRef {
4367                            engine: &self.engine,
4368                            interfaces: &self.interfaces,
4369                            link_manager: &self.link_manager,
4370                            now,
4371                        };
4372                        let provider_events_enabled = self.provider_events_enabled();
4373                        if let Some(ref e) = run_hook_inner(
4374                            &mut self.hook_slots[HookPoint::PreDispatch as usize].programs,
4375                            &self.hook_manager,
4376                            &engine_ref,
4377                            &ctx,
4378                            now,
4379                            provider_events_enabled,
4380                        ) {
4381                            self.forward_hook_side_effects("PreDispatch", e);
4382                        }
4383                    }
4384
4385                    self.dispatch_all(actions);
4386                }
4387                Event::AnnounceVerified {
4388                    key,
4389                    validated,
4390                    sig_cache_key,
4391                } => {
4392                    let pending = {
4393                        let mut announce_queue = self
4394                            .announce_verify_queue
4395                            .lock()
4396                            .unwrap_or_else(|poisoned| poisoned.into_inner());
4397                        announce_queue.complete_success(&key)
4398                    };
4399                    if let Some(pending) = pending {
4400                        let actions = self.engine.complete_verified_announce(
4401                            pending,
4402                            validated,
4403                            sig_cache_key,
4404                            time::now(),
4405                            &mut self.rng,
4406                        );
4407                        self.dispatch_all(actions);
4408                    }
4409                }
4410                Event::AnnounceVerifyFailed { key, .. } => {
4411                    let mut announce_queue = self
4412                        .announce_verify_queue
4413                        .lock()
4414                        .unwrap_or_else(|poisoned| poisoned.into_inner());
4415                    let _ = announce_queue.complete_failure(&key);
4416                }
4417                Event::Tick => {
4418                    // Tick hook
4419                    #[cfg(feature = "rns-hooks")]
4420                    {
4421                        let ctx = HookContext::Tick;
4422                        let now = time::now();
4423                        let engine_ref = EngineRef {
4424                            engine: &self.engine,
4425                            interfaces: &self.interfaces,
4426                            link_manager: &self.link_manager,
4427                            now,
4428                        };
4429                        let provider_events_enabled = self.provider_events_enabled();
4430                        if let Some(ref e) = run_hook_inner(
4431                            &mut self.hook_slots[HookPoint::Tick as usize].programs,
4432                            &self.hook_manager,
4433                            &engine_ref,
4434                            &ctx,
4435                            now,
4436                            provider_events_enabled,
4437                        ) {
4438                            self.forward_hook_side_effects("Tick", e);
4439                        }
4440                    }
4441
4442                    let now = time::now();
4443                    // Sync announce frequency to engine for all interfaces before tick
4444                    for (id, entry) in &self.interfaces {
4445                        self.engine
4446                            .update_interface_freq(*id, entry.stats.incoming_announce_freq());
4447                    }
4448                    let actions = self.engine.tick(now, &mut self.rng);
4449                    self.dispatch_all(actions);
4450                    // Tick link manager (keepalive, stale, timeout)
4451                    let link_actions = self.link_manager.tick(&mut self.rng);
4452                    self.dispatch_link_actions(link_actions);
4453                    self.enforce_drain_deadline();
4454                    // Tick hole-punch manager
4455                    {
4456                        let tx = self.get_event_sender();
4457                        let hp_actions = self.holepunch_manager.tick(&tx);
4458                        self.dispatch_holepunch_actions(hp_actions);
4459                    }
4460                    // Emit management announces
4461                    self.tick_management_announces(now);
4462                    // Cull expired sent packet tracking entries (no proof received within 60s)
4463                    self.sent_packets
4464                        .retain(|_, (_, sent_time)| now - *sent_time < 60.0);
4465                    // Cull old completed proof entries (older than 120s)
4466                    self.completed_proofs
4467                        .retain(|_, (_, received)| now - *received < 120.0);
4468
4469                    self.tick_discovery_announcer(now);
4470
4471                    // Periodic MEMSTATS logging (~every 5 min / 300 ticks)
4472                    self.memory_stats_counter += 1;
4473                    if self.memory_stats_counter >= 300 {
4474                        self.memory_stats_counter = 0;
4475                        self.log_memory_stats();
4476                    }
4477
4478                    // Periodic discovery cleanup
4479                    if self.discover_interfaces {
4480                        self.discovery_cleanup_counter += 1;
4481                        if self.discovery_cleanup_counter >= self.discovery_cleanup_interval_ticks {
4482                            self.discovery_cleanup_counter = 0;
4483                            if let Ok(removed) = self.discovered_interfaces.cleanup() {
4484                                if removed > 0 {
4485                                    log::info!(
4486                                        "Discovery cleanup: removed {} stale entries",
4487                                        removed
4488                                    );
4489                                }
4490                            }
4491                        }
4492                    }
4493
4494                    // Periodic known-destinations cleanup
4495                    self.cache_cleanup_counter += 1;
4496                    if self.cache_cleanup_counter >= self.known_destinations_cleanup_interval_ticks
4497                    {
4498                        self.cache_cleanup_counter = 0;
4499
4500                        let active_dests = self.engine.active_destination_hashes();
4501
4502                        // Retain known destinations while their path is active, while they are
4503                        // locally registered, or until their configured TTL expires.
4504                        let now = time::now();
4505                        let ttl = self.known_destinations_ttl;
4506                        let kd_before = self.known_destinations.len();
4507                        self.known_destinations.retain(|k, announced| {
4508                            active_dests.contains(k)
4509                                || self.local_destinations.contains_key(k)
4510                                || now - announced.received_at < ttl
4511                        });
4512                        let kd_removed = kd_before - self.known_destinations.len();
4513                        let kd_evicted = self.enforce_known_destination_cap(false);
4514
4515                        // Cull rate limiter entries while keeping active or recently used ones.
4516                        let rl_removed = self.engine.cull_rate_limiter(
4517                            &active_dests,
4518                            now,
4519                            self.rate_limiter_ttl_secs,
4520                        );
4521
4522                        if kd_removed > 0 || kd_evicted > 0 || rl_removed > 0 {
4523                            log::info!(
4524                                "Memory cleanup: removed {} known_destinations, evicted {} known_destinations, {} rate_limiter entries",
4525                                kd_removed, kd_evicted, rl_removed
4526                            );
4527                        }
4528                    }
4529
4530                    // Periodic announce-cache cleanup scheduling
4531                    self.announce_cache_cleanup_counter += 1;
4532                    if self.announce_cache_cleanup_counter
4533                        >= self.announce_cache_cleanup_interval_ticks
4534                    {
4535                        self.announce_cache_cleanup_counter = 0;
4536                        if self.announce_cache.is_some()
4537                            && self.cache_cleanup_active_hashes.is_none()
4538                        {
4539                            self.cache_cleanup_active_hashes =
4540                                Some(self.engine.active_packet_hashes());
4541                            self.cache_cleanup_entries = None;
4542                            self.cache_cleanup_removed = 0;
4543                        }
4544                    }
4545
4546                    // Incremental announce cache cleanup
4547                    if self.cache_cleanup_active_hashes.is_some() {
4548                        if let Some(ref cache) = self.announce_cache {
4549                            if self.cache_cleanup_entries.is_none() {
4550                                match cache.entries() {
4551                                    Ok(entries) => self.cache_cleanup_entries = Some(entries),
4552                                    Err(e) => {
4553                                        log::warn!(
4554                                            "Announce cache cleanup failed to open directory: {}",
4555                                            e
4556                                        );
4557                                        self.cache_cleanup_active_hashes = None;
4558                                        self.cache_cleanup_entries = None;
4559                                    }
4560                                }
4561                            }
4562                        }
4563
4564                        if let Some(ref cache) = self.announce_cache {
4565                            let active_hashes = self.cache_cleanup_active_hashes.as_ref().unwrap();
4566                            let entries = match self.cache_cleanup_entries.as_mut() {
4567                                Some(entries) => entries,
4568                                None => continue,
4569                            };
4570                            match cache.clean_batch(
4571                                active_hashes,
4572                                entries,
4573                                self.announce_cache_cleanup_batch_size,
4574                            ) {
4575                                Ok((removed, finished)) => {
4576                                    self.cache_cleanup_removed += removed;
4577                                    if finished {
4578                                        if self.cache_cleanup_removed > 0 {
4579                                            log::info!(
4580                                                "Announce cache cleanup complete: removed {} stale files",
4581                                                self.cache_cleanup_removed
4582                                            );
4583                                        }
4584                                        self.cache_cleanup_active_hashes = None;
4585                                        self.cache_cleanup_entries = None;
4586                                    }
4587                                }
4588                                Err(e) => {
4589                                    log::warn!("Announce cache cleanup failed: {}", e);
4590                                    self.cache_cleanup_active_hashes = None;
4591                                    self.cache_cleanup_entries = None;
4592                                }
4593                            }
4594                        } else {
4595                            self.cache_cleanup_active_hashes = None;
4596                            self.cache_cleanup_entries = None;
4597                        }
4598                    }
4599                }
4600                Event::BeginDrain { timeout } => {
4601                    self.begin_drain(timeout);
4602                }
4603                Event::InterfaceUp(id, new_writer, info) => {
4604                    let wants_tunnel;
4605                    let mut replay_shared_announces = false;
4606                    if let Some(mut info) = info {
4607                        // New dynamic interface (e.g., TCP server client connection)
4608                        log::info!("[{}] dynamic interface registered", id.0);
4609                        wants_tunnel = info.wants_tunnel;
4610                        let iface_type = infer_interface_type(&info.name);
4611                        // Set started time for ingress control age tracking
4612                        info.started = time::now();
4613                        self.register_interface_runtime_defaults(&info);
4614                        self.engine.register_interface(info.clone());
4615                        if let Some(writer) = new_writer {
4616                            let (writer, async_writer_metrics) =
4617                                self.wrap_interface_writer(id, &info.name, writer);
4618                            self.interfaces.insert(
4619                                id,
4620                                InterfaceEntry {
4621                                    id,
4622                                    info,
4623                                    writer,
4624                                    async_writer_metrics: Some(async_writer_metrics),
4625                                    enabled: true,
4626                                    online: true,
4627                                    dynamic: true,
4628                                    ifac: None,
4629                                    stats: InterfaceStats {
4630                                        started: time::now(),
4631                                        ..Default::default()
4632                                    },
4633                                    interface_type: iface_type,
4634                                    send_retry_at: None,
4635                                    send_retry_backoff: Duration::ZERO,
4636                                },
4637                            );
4638                        }
4639                        self.callbacks.on_interface_up(id);
4640                        #[cfg(feature = "rns-hooks")]
4641                        {
4642                            let ctx = HookContext::Interface { interface_id: id.0 };
4643                            let now = time::now();
4644                            let engine_ref = EngineRef {
4645                                engine: &self.engine,
4646                                interfaces: &self.interfaces,
4647                                link_manager: &self.link_manager,
4648                                now,
4649                            };
4650                            let provider_events_enabled = self.provider_events_enabled();
4651                            if let Some(ref e) = run_hook_inner(
4652                                &mut self.hook_slots[HookPoint::InterfaceUp as usize].programs,
4653                                &self.hook_manager,
4654                                &engine_ref,
4655                                &ctx,
4656                                now,
4657                                provider_events_enabled,
4658                            ) {
4659                                self.forward_hook_side_effects("InterfaceUp", e);
4660                            }
4661                        }
4662                    } else {
4663                        // Existing interface reconnected
4664                        let is_local_client = self
4665                            .interfaces
4666                            .get(&id)
4667                            .map(|entry| entry.info.is_local_client)
4668                            .unwrap_or(false);
4669                        replay_shared_announces = is_local_client
4670                            && self.shared_reconnect_pending.remove(&id).unwrap_or(false);
4671                        let interface_name = self
4672                            .interfaces
4673                            .get(&id)
4674                            .map(|entry| entry.info.name.clone())
4675                            .unwrap_or_else(|| format!("iface-{}", id.0));
4676                        let wrapped_writer = if let Some(writer) = new_writer {
4677                            Some(self.wrap_interface_writer(id, &interface_name, writer))
4678                        } else {
4679                            None
4680                        };
4681                        if let Some(entry) = self.interfaces.get_mut(&id) {
4682                            log::info!("[{}] interface online", id.0);
4683                            wants_tunnel = entry.info.wants_tunnel;
4684                            entry.online = true;
4685                            if let Some((writer, async_writer_metrics)) = wrapped_writer {
4686                                log::info!("[{}] writer refreshed after reconnect", id.0);
4687                                entry.writer = writer;
4688                                entry.async_writer_metrics = Some(async_writer_metrics);
4689                            }
4690                            self.callbacks.on_interface_up(id);
4691                            #[cfg(feature = "rns-hooks")]
4692                            {
4693                                let ctx = HookContext::Interface { interface_id: id.0 };
4694                                let now = time::now();
4695                                let engine_ref = EngineRef {
4696                                    engine: &self.engine,
4697                                    interfaces: &self.interfaces,
4698                                    link_manager: &self.link_manager,
4699                                    now,
4700                                };
4701                                let provider_events_enabled = self.provider_events_enabled();
4702                                if let Some(ref e) = run_hook_inner(
4703                                    &mut self.hook_slots[HookPoint::InterfaceUp as usize].programs,
4704                                    &self.hook_manager,
4705                                    &engine_ref,
4706                                    &ctx,
4707                                    now,
4708                                    provider_events_enabled,
4709                                ) {
4710                                    self.forward_hook_side_effects("InterfaceUp", e);
4711                                }
4712                            }
4713                        } else {
4714                            wants_tunnel = false;
4715                        }
4716                    }
4717
4718                    // Trigger tunnel synthesis if the interface wants it
4719                    if wants_tunnel {
4720                        self.synthesize_tunnel_for_interface(id);
4721                    }
4722                    if replay_shared_announces {
4723                        self.replay_shared_announces();
4724                    }
4725                }
4726                Event::InterfaceDown(id) => {
4727                    // Void tunnel if interface had one
4728                    if let Some(entry) = self.interfaces.get(&id) {
4729                        if let Some(tunnel_id) = entry.info.tunnel_id {
4730                            self.engine.void_tunnel_interface(&tunnel_id);
4731                        }
4732                    }
4733
4734                    if let Some(entry) = self.interfaces.get(&id) {
4735                        let is_dynamic = entry.dynamic;
4736                        let is_local_client = entry.info.is_local_client;
4737                        let interface_name = entry.info.name.clone();
4738                        if is_dynamic {
4739                            // Dynamic interfaces are removed entirely
4740                            log::info!("[{}] dynamic interface removed", id.0);
4741                            self.interface_runtime_defaults.remove(&interface_name);
4742                            self.engine.deregister_interface(id);
4743                            self.interfaces.remove(&id);
4744                        } else {
4745                            // Static interfaces are just marked offline
4746                            log::info!("[{}] interface offline", id.0);
4747                            self.interfaces.get_mut(&id).unwrap().online = false;
4748                            if is_local_client {
4749                                self.handle_shared_interface_down(id);
4750                            }
4751                        }
4752                        self.callbacks.on_interface_down(id);
4753                        #[cfg(feature = "rns-hooks")]
4754                        {
4755                            let ctx = HookContext::Interface { interface_id: id.0 };
4756                            let now = time::now();
4757                            let engine_ref = EngineRef {
4758                                engine: &self.engine,
4759                                interfaces: &self.interfaces,
4760                                link_manager: &self.link_manager,
4761                                now,
4762                            };
4763                            let provider_events_enabled = self.provider_events_enabled();
4764                            if let Some(ref e) = run_hook_inner(
4765                                &mut self.hook_slots[HookPoint::InterfaceDown as usize].programs,
4766                                &self.hook_manager,
4767                                &engine_ref,
4768                                &ctx,
4769                                now,
4770                                provider_events_enabled,
4771                            ) {
4772                                self.forward_hook_side_effects("InterfaceDown", e);
4773                            }
4774                        }
4775                    }
4776                }
4777                Event::SendOutbound {
4778                    raw,
4779                    dest_type,
4780                    attached_interface,
4781                } => {
4782                    if self.is_draining() {
4783                        self.reject_new_work("send outbound packet");
4784                        continue;
4785                    }
4786                    match RawPacket::unpack(&raw) {
4787                        Ok(packet) => {
4788                            let is_announce = packet.flags.packet_type
4789                                == rns_core::constants::PACKET_TYPE_ANNOUNCE;
4790                            if is_announce {
4791                                log::debug!("SendOutbound: ANNOUNCE for {:02x?} (len={}, dest_type={}, attached={:?})",
4792                                    &packet.destination_hash[..4], raw.len(), dest_type, attached_interface);
4793                            }
4794                            // Track sent DATA packets for proof matching
4795                            if packet.flags.packet_type == rns_core::constants::PACKET_TYPE_DATA {
4796                                self.sent_packets.insert(
4797                                    packet.packet_hash,
4798                                    (packet.destination_hash, time::now()),
4799                                );
4800                            }
4801                            let actions = self.engine.handle_outbound(
4802                                &packet,
4803                                dest_type,
4804                                attached_interface,
4805                                time::now(),
4806                            );
4807                            if is_announce {
4808                                log::debug!(
4809                                    "SendOutbound: announce routed to {} actions: {:?}",
4810                                    actions.len(),
4811                                    actions
4812                                        .iter()
4813                                        .map(|a| match a {
4814                                            TransportAction::SendOnInterface {
4815                                                interface, ..
4816                                            } => format!("SendOn({})", interface.0),
4817                                            TransportAction::BroadcastOnAllInterfaces {
4818                                                ..
4819                                            } => "BroadcastAll".to_string(),
4820                                            _ => "other".to_string(),
4821                                        })
4822                                        .collect::<Vec<_>>()
4823                                );
4824                            }
4825                            self.dispatch_all(actions);
4826                        }
4827                        Err(e) => {
4828                            log::warn!("SendOutbound: failed to unpack packet: {:?}", e);
4829                        }
4830                    }
4831                }
4832                Event::RegisterDestination {
4833                    dest_hash,
4834                    dest_type,
4835                } => {
4836                    self.engine.register_destination(dest_hash, dest_type);
4837                    self.local_destinations.insert(dest_hash, dest_type);
4838                }
4839                Event::StoreSharedAnnounce {
4840                    dest_hash,
4841                    name_hash,
4842                    identity_prv_key,
4843                    app_data,
4844                } => {
4845                    self.shared_announces.insert(
4846                        dest_hash,
4847                        SharedAnnounceRecord {
4848                            name_hash,
4849                            identity_prv_key,
4850                            app_data,
4851                        },
4852                    );
4853                }
4854                Event::DeregisterDestination { dest_hash } => {
4855                    self.engine.deregister_destination(&dest_hash);
4856                    self.local_destinations.remove(&dest_hash);
4857                    self.shared_announces.remove(&dest_hash);
4858                }
4859                Event::Query(request, response_tx) => {
4860                    let response = self.handle_query_mut(request);
4861                    let _ = response_tx.send(response);
4862                }
4863                Event::DeregisterLinkDestination { dest_hash } => {
4864                    self.link_manager.deregister_link_destination(&dest_hash);
4865                }
4866                Event::RegisterLinkDestination {
4867                    dest_hash,
4868                    sig_prv_bytes,
4869                    sig_pub_bytes,
4870                    resource_strategy,
4871                } => {
4872                    let sig_prv =
4873                        rns_crypto::ed25519::Ed25519PrivateKey::from_bytes(&sig_prv_bytes);
4874                    let strat = match resource_strategy {
4875                        1 => crate::link_manager::ResourceStrategy::AcceptAll,
4876                        2 => crate::link_manager::ResourceStrategy::AcceptApp,
4877                        _ => crate::link_manager::ResourceStrategy::AcceptNone,
4878                    };
4879                    self.link_manager.register_link_destination(
4880                        dest_hash,
4881                        sig_prv,
4882                        sig_pub_bytes,
4883                        strat,
4884                    );
4885                    // Also register in transport engine so inbound packets are delivered locally
4886                    self.engine
4887                        .register_destination(dest_hash, rns_core::constants::DESTINATION_SINGLE);
4888                    self.local_destinations
4889                        .insert(dest_hash, rns_core::constants::DESTINATION_SINGLE);
4890                }
4891                Event::RegisterRequestHandler {
4892                    path,
4893                    allowed_list,
4894                    handler,
4895                } => {
4896                    self.link_manager.register_request_handler(
4897                        &path,
4898                        allowed_list,
4899                        move |link_id, p, data, remote| handler(link_id, p, data, remote),
4900                    );
4901                }
4902                Event::CreateLink {
4903                    dest_hash,
4904                    dest_sig_pub_bytes,
4905                    response_tx,
4906                } => {
4907                    if self.is_draining() {
4908                        self.reject_new_work("create link");
4909                        let _ = (dest_hash, dest_sig_pub_bytes);
4910                        let _ = response_tx.send([0u8; 16]);
4911                        continue;
4912                    }
4913                    let hops = self.engine.hops_to(&dest_hash).unwrap_or(0);
4914                    let mtu = self
4915                        .engine
4916                        .next_hop_interface(&dest_hash)
4917                        .and_then(|iface_id| self.interfaces.get(&iface_id))
4918                        .map(|entry| entry.info.mtu)
4919                        .unwrap_or(rns_core::constants::MTU as u32);
4920                    let (link_id, link_actions) = self.link_manager.create_link(
4921                        &dest_hash,
4922                        &dest_sig_pub_bytes,
4923                        hops,
4924                        mtu,
4925                        &mut self.rng,
4926                    );
4927                    let _ = response_tx.send(link_id);
4928                    self.dispatch_link_actions(link_actions);
4929                }
4930                Event::SendRequest {
4931                    link_id,
4932                    path,
4933                    data,
4934                } => {
4935                    if self.is_draining() {
4936                        self.reject_new_work("send link request");
4937                        let _ = (link_id, path, data);
4938                        continue;
4939                    }
4940                    let link_actions =
4941                        self.link_manager
4942                            .send_request(&link_id, &path, &data, &mut self.rng);
4943                    self.dispatch_link_actions(link_actions);
4944                }
4945                Event::IdentifyOnLink {
4946                    link_id,
4947                    identity_prv_key,
4948                } => {
4949                    if self.is_draining() {
4950                        self.reject_new_work("identify on link");
4951                        let _ = (link_id, identity_prv_key);
4952                        continue;
4953                    }
4954                    let identity =
4955                        rns_crypto::identity::Identity::from_private_key(&identity_prv_key);
4956                    let link_actions =
4957                        self.link_manager
4958                            .identify(&link_id, &identity, &mut self.rng);
4959                    self.dispatch_link_actions(link_actions);
4960                }
4961                Event::TeardownLink { link_id } => {
4962                    let link_actions = self.link_manager.teardown_link(&link_id);
4963                    self.dispatch_link_actions(link_actions);
4964                }
4965                Event::SendResource {
4966                    link_id,
4967                    data,
4968                    metadata,
4969                } => {
4970                    if self.is_draining() {
4971                        self.reject_new_work("send resource");
4972                        let _ = (link_id, data, metadata);
4973                        continue;
4974                    }
4975                    let link_actions = self.link_manager.send_resource(
4976                        &link_id,
4977                        &data,
4978                        metadata.as_deref(),
4979                        &mut self.rng,
4980                    );
4981                    self.dispatch_link_actions(link_actions);
4982                }
4983                Event::SetResourceStrategy { link_id, strategy } => {
4984                    use crate::link_manager::ResourceStrategy;
4985                    let strat = match strategy {
4986                        0 => ResourceStrategy::AcceptNone,
4987                        1 => ResourceStrategy::AcceptAll,
4988                        2 => ResourceStrategy::AcceptApp,
4989                        _ => ResourceStrategy::AcceptNone,
4990                    };
4991                    self.link_manager.set_resource_strategy(&link_id, strat);
4992                }
4993                Event::AcceptResource {
4994                    link_id,
4995                    resource_hash,
4996                    accept,
4997                } => {
4998                    if self.is_draining() && accept {
4999                        self.reject_new_work("accept resource");
5000                        let _ = (link_id, resource_hash, accept);
5001                        continue;
5002                    }
5003                    let link_actions = self.link_manager.accept_resource(
5004                        &link_id,
5005                        &resource_hash,
5006                        accept,
5007                        &mut self.rng,
5008                    );
5009                    self.dispatch_link_actions(link_actions);
5010                }
5011                Event::SendChannelMessage {
5012                    link_id,
5013                    msgtype,
5014                    payload,
5015                    response_tx,
5016                } => {
5017                    if self.is_draining() {
5018                        self.reject_new_work("send channel message");
5019                        let _ = response_tx.send(Err(self.drain_error("send channel message")));
5020                        continue;
5021                    }
5022                    match self.link_manager.send_channel_message(
5023                        &link_id,
5024                        msgtype,
5025                        &payload,
5026                        &mut self.rng,
5027                    ) {
5028                        Ok(link_actions) => {
5029                            self.dispatch_link_actions(link_actions);
5030                            let _ = response_tx.send(Ok(()));
5031                        }
5032                        Err(err) => {
5033                            let _ = response_tx.send(Err(err));
5034                        }
5035                    }
5036                }
5037                Event::SendOnLink {
5038                    link_id,
5039                    data,
5040                    context,
5041                } => {
5042                    if self.is_draining() {
5043                        self.reject_new_work("send link payload");
5044                        let _ = (link_id, data, context);
5045                        continue;
5046                    }
5047                    let link_actions =
5048                        self.link_manager
5049                            .send_on_link(&link_id, &data, context, &mut self.rng);
5050                    self.dispatch_link_actions(link_actions);
5051                }
5052                Event::RequestPath { dest_hash } => {
5053                    if self.is_draining() {
5054                        self.reject_new_work("request path");
5055                        let _ = dest_hash;
5056                        continue;
5057                    }
5058                    self.handle_request_path(dest_hash);
5059                }
5060                Event::RegisterProofStrategy {
5061                    dest_hash,
5062                    strategy,
5063                    signing_key,
5064                } => {
5065                    let identity = signing_key
5066                        .map(|key| rns_crypto::identity::Identity::from_private_key(&key));
5067                    self.proof_strategies
5068                        .insert(dest_hash, (strategy, identity));
5069                }
5070                Event::ProposeDirectConnect { link_id } => {
5071                    if self.is_draining() {
5072                        self.reject_new_work("propose direct connect");
5073                        let _ = link_id;
5074                        continue;
5075                    }
5076                    let derived_key = self.link_manager.get_derived_key(&link_id);
5077                    if let Some(dk) = derived_key {
5078                        let tx = self.get_event_sender();
5079                        let hp_actions =
5080                            self.holepunch_manager
5081                                .propose(link_id, &dk, &mut self.rng, &tx);
5082                        self.dispatch_holepunch_actions(hp_actions);
5083                    } else {
5084                        log::warn!(
5085                            "Cannot propose direct connect: no derived key for link {:02x?}",
5086                            &link_id[..4]
5087                        );
5088                    }
5089                }
5090                Event::SetDirectConnectPolicy { policy } => {
5091                    self.holepunch_manager.set_policy(policy);
5092                }
5093                Event::HolePunchProbeResult {
5094                    link_id,
5095                    session_id,
5096                    observed_addr,
5097                    socket,
5098                    probe_server,
5099                } => {
5100                    let hp_actions = self.holepunch_manager.handle_probe_result(
5101                        link_id,
5102                        session_id,
5103                        observed_addr,
5104                        socket,
5105                        probe_server,
5106                    );
5107                    self.dispatch_holepunch_actions(hp_actions);
5108                }
5109                Event::HolePunchProbeFailed {
5110                    link_id,
5111                    session_id,
5112                } => {
5113                    let hp_actions = self
5114                        .holepunch_manager
5115                        .handle_probe_failed(link_id, session_id);
5116                    self.dispatch_holepunch_actions(hp_actions);
5117                }
5118                Event::LoadHook {
5119                    name,
5120                    wasm_bytes,
5121                    attach_point,
5122                    priority,
5123                    response_tx,
5124                } => {
5125                    #[cfg(feature = "rns-hooks")]
5126                    {
5127                        let result = (|| -> Result<(), String> {
5128                            let point_idx = crate::config::parse_hook_point(&attach_point)
5129                                .ok_or_else(|| format!("unknown hook point '{}'", attach_point))?;
5130                            let mgr = self
5131                                .hook_manager
5132                                .as_ref()
5133                                .ok_or_else(|| "hook manager not available".to_string())?;
5134                            let program = mgr
5135                                .compile(name.clone(), &wasm_bytes, priority)
5136                                .map_err(|e| format!("compile error: {}", e))?;
5137                            self.hook_slots[point_idx].attach(program);
5138                            log::info!(
5139                                "Loaded hook '{}' at point {} (priority {})",
5140                                name,
5141                                attach_point,
5142                                priority
5143                            );
5144                            Ok(())
5145                        })();
5146                        let _ = response_tx.send(result);
5147                    }
5148                    #[cfg(not(feature = "rns-hooks"))]
5149                    {
5150                        let _ = (name, wasm_bytes, attach_point, priority);
5151                        let _ = response_tx.send(Err("hooks not enabled".to_string()));
5152                    }
5153                }
5154                Event::UnloadHook {
5155                    name,
5156                    attach_point,
5157                    response_tx,
5158                } => {
5159                    #[cfg(feature = "rns-hooks")]
5160                    {
5161                        let result = (|| -> Result<(), String> {
5162                            let point_idx = crate::config::parse_hook_point(&attach_point)
5163                                .ok_or_else(|| format!("unknown hook point '{}'", attach_point))?;
5164                            match self.hook_slots[point_idx].detach(&name) {
5165                                Some(_) => {
5166                                    log::info!(
5167                                        "Unloaded hook '{}' from point {}",
5168                                        name,
5169                                        attach_point
5170                                    );
5171                                    Ok(())
5172                                }
5173                                None => Err(format!(
5174                                    "hook '{}' not found at point '{}'",
5175                                    name, attach_point
5176                                )),
5177                            }
5178                        })();
5179                        let _ = response_tx.send(result);
5180                    }
5181                    #[cfg(not(feature = "rns-hooks"))]
5182                    {
5183                        let _ = (name, attach_point);
5184                        let _ = response_tx.send(Err("hooks not enabled".to_string()));
5185                    }
5186                }
5187                Event::ReloadHook {
5188                    name,
5189                    attach_point,
5190                    wasm_bytes,
5191                    response_tx,
5192                } => {
5193                    #[cfg(feature = "rns-hooks")]
5194                    {
5195                        let result = (|| -> Result<(), String> {
5196                            let point_idx = crate::config::parse_hook_point(&attach_point)
5197                                .ok_or_else(|| format!("unknown hook point '{}'", attach_point))?;
5198                            let old =
5199                                self.hook_slots[point_idx].detach(&name).ok_or_else(|| {
5200                                    format!("hook '{}' not found at point '{}'", name, attach_point)
5201                                })?;
5202                            let priority = old.priority;
5203                            let mgr = match self.hook_manager.as_ref() {
5204                                Some(m) => m,
5205                                None => {
5206                                    self.hook_slots[point_idx].attach(old);
5207                                    return Err("hook manager not available".to_string());
5208                                }
5209                            };
5210                            match mgr.compile(name.clone(), &wasm_bytes, priority) {
5211                                Ok(program) => {
5212                                    self.hook_slots[point_idx].attach(program);
5213                                    log::info!(
5214                                        "Reloaded hook '{}' at point {} (priority {})",
5215                                        name,
5216                                        attach_point,
5217                                        priority
5218                                    );
5219                                    Ok(())
5220                                }
5221                                Err(e) => {
5222                                    self.hook_slots[point_idx].attach(old);
5223                                    Err(format!("compile error: {}", e))
5224                                }
5225                            }
5226                        })();
5227                        let _ = response_tx.send(result);
5228                    }
5229                    #[cfg(not(feature = "rns-hooks"))]
5230                    {
5231                        let _ = (name, attach_point, wasm_bytes);
5232                        let _ = response_tx.send(Err("hooks not enabled".to_string()));
5233                    }
5234                }
5235                Event::SetHookEnabled {
5236                    name,
5237                    attach_point,
5238                    enabled,
5239                    response_tx,
5240                } => {
5241                    #[cfg(feature = "rns-hooks")]
5242                    {
5243                        let result = self.update_hook_program(&name, &attach_point, |program| {
5244                            program.enabled = enabled;
5245                        });
5246                        if result.is_ok() {
5247                            log::info!(
5248                                "{} hook '{}' at point {}",
5249                                if enabled { "Enabled" } else { "Disabled" },
5250                                name,
5251                                attach_point,
5252                            );
5253                        }
5254                        let _ = response_tx.send(result);
5255                    }
5256                    #[cfg(not(feature = "rns-hooks"))]
5257                    {
5258                        let _ = (name, attach_point, enabled);
5259                        let _ = response_tx.send(Err("hooks not enabled".to_string()));
5260                    }
5261                }
5262                Event::SetHookPriority {
5263                    name,
5264                    attach_point,
5265                    priority,
5266                    response_tx,
5267                } => {
5268                    #[cfg(feature = "rns-hooks")]
5269                    {
5270                        let result = self.update_hook_program(&name, &attach_point, |program| {
5271                            program.priority = priority;
5272                        });
5273                        if result.is_ok() {
5274                            let point_idx = crate::config::parse_hook_point(&attach_point)
5275                                .expect("validated hook point");
5276                            self.hook_slots[point_idx]
5277                                .programs
5278                                .sort_by(|a, b| b.priority.cmp(&a.priority));
5279                            log::info!(
5280                                "Updated hook '{}' at point {} to priority {}",
5281                                name,
5282                                attach_point,
5283                                priority,
5284                            );
5285                        }
5286                        let _ = response_tx.send(result);
5287                    }
5288                    #[cfg(not(feature = "rns-hooks"))]
5289                    {
5290                        let _ = (name, attach_point, priority);
5291                        let _ = response_tx.send(Err("hooks not enabled".to_string()));
5292                    }
5293                }
5294                Event::ListHooks { response_tx } => {
5295                    #[cfg(feature = "rns-hooks")]
5296                    {
5297                        let hook_point_names = [
5298                            "PreIngress",
5299                            "PreDispatch",
5300                            "AnnounceReceived",
5301                            "PathUpdated",
5302                            "AnnounceRetransmit",
5303                            "LinkRequestReceived",
5304                            "LinkEstablished",
5305                            "LinkClosed",
5306                            "InterfaceUp",
5307                            "InterfaceDown",
5308                            "InterfaceConfigChanged",
5309                            "BackbonePeerConnected",
5310                            "BackbonePeerDisconnected",
5311                            "BackbonePeerIdleTimeout",
5312                            "BackbonePeerWriteStall",
5313                            "BackbonePeerPenalty",
5314                            "SendOnInterface",
5315                            "BroadcastOnAllInterfaces",
5316                            "DeliverLocal",
5317                            "TunnelSynthesize",
5318                            "Tick",
5319                        ];
5320                        let mut infos = Vec::new();
5321                        for (idx, slot) in self.hook_slots.iter().enumerate() {
5322                            let point_name = hook_point_names.get(idx).unwrap_or(&"Unknown");
5323                            for prog in &slot.programs {
5324                                infos.push(crate::event::HookInfo {
5325                                    name: prog.name.clone(),
5326                                    attach_point: point_name.to_string(),
5327                                    priority: prog.priority,
5328                                    enabled: prog.enabled,
5329                                    consecutive_traps: prog.consecutive_traps,
5330                                });
5331                            }
5332                        }
5333                        let _ = response_tx.send(infos);
5334                    }
5335                    #[cfg(not(feature = "rns-hooks"))]
5336                    {
5337                        let _ = response_tx.send(Vec::new());
5338                    }
5339                }
5340                Event::InterfaceConfigChanged(id) => {
5341                    #[cfg(feature = "rns-hooks")]
5342                    {
5343                        let ctx = HookContext::Interface { interface_id: id.0 };
5344                        let now = time::now();
5345                        let engine_ref = EngineRef {
5346                            engine: &self.engine,
5347                            interfaces: &self.interfaces,
5348                            link_manager: &self.link_manager,
5349                            now,
5350                        };
5351                        let provider_events_enabled = self.provider_events_enabled();
5352                        if let Some(ref e) = run_hook_inner(
5353                            &mut self.hook_slots[HookPoint::InterfaceConfigChanged as usize]
5354                                .programs,
5355                            &self.hook_manager,
5356                            &engine_ref,
5357                            &ctx,
5358                            now,
5359                            provider_events_enabled,
5360                        ) {
5361                            self.forward_hook_side_effects("InterfaceConfigChanged", e);
5362                        }
5363                    }
5364                    #[cfg(not(feature = "rns-hooks"))]
5365                    let _ = id;
5366                }
5367                Event::BackbonePeerConnected {
5368                    server_interface_id,
5369                    peer_interface_id,
5370                    peer_ip,
5371                    peer_port,
5372                } => {
5373                    #[cfg(feature = "rns-hooks")]
5374                    {
5375                        self.run_backbone_peer_hook(
5376                            "BackbonePeerConnected",
5377                            HookPoint::BackbonePeerConnected,
5378                            &BackbonePeerHookEvent {
5379                                server_interface_id,
5380                                peer_interface_id: Some(peer_interface_id),
5381                                peer_ip,
5382                                peer_port,
5383                                connected_for: Duration::ZERO,
5384                                had_received_data: false,
5385                                penalty_level: 0,
5386                                blacklist_for: Duration::ZERO,
5387                            },
5388                        );
5389                    }
5390                    #[cfg(not(feature = "rns-hooks"))]
5391                    let _ = (server_interface_id, peer_interface_id, peer_ip, peer_port);
5392                }
5393                Event::BackbonePeerDisconnected {
5394                    server_interface_id,
5395                    peer_interface_id,
5396                    peer_ip,
5397                    peer_port,
5398                    connected_for,
5399                    had_received_data,
5400                } => {
5401                    #[cfg(feature = "rns-hooks")]
5402                    {
5403                        self.run_backbone_peer_hook(
5404                            "BackbonePeerDisconnected",
5405                            HookPoint::BackbonePeerDisconnected,
5406                            &BackbonePeerHookEvent {
5407                                server_interface_id,
5408                                peer_interface_id: Some(peer_interface_id),
5409                                peer_ip,
5410                                peer_port,
5411                                connected_for,
5412                                had_received_data,
5413                                penalty_level: 0,
5414                                blacklist_for: Duration::ZERO,
5415                            },
5416                        );
5417                    }
5418                    #[cfg(not(feature = "rns-hooks"))]
5419                    let _ = (
5420                        server_interface_id,
5421                        peer_interface_id,
5422                        peer_ip,
5423                        peer_port,
5424                        connected_for,
5425                        had_received_data,
5426                    );
5427                }
5428                Event::BackbonePeerIdleTimeout {
5429                    server_interface_id,
5430                    peer_interface_id,
5431                    peer_ip,
5432                    peer_port,
5433                    connected_for,
5434                } => {
5435                    #[cfg(feature = "rns-hooks")]
5436                    {
5437                        self.run_backbone_peer_hook(
5438                            "BackbonePeerIdleTimeout",
5439                            HookPoint::BackbonePeerIdleTimeout,
5440                            &BackbonePeerHookEvent {
5441                                server_interface_id,
5442                                peer_interface_id: Some(peer_interface_id),
5443                                peer_ip,
5444                                peer_port,
5445                                connected_for,
5446                                had_received_data: false,
5447                                penalty_level: 0,
5448                                blacklist_for: Duration::ZERO,
5449                            },
5450                        );
5451                    }
5452                    #[cfg(not(feature = "rns-hooks"))]
5453                    let _ = (
5454                        server_interface_id,
5455                        peer_interface_id,
5456                        peer_ip,
5457                        peer_port,
5458                        connected_for,
5459                    );
5460                }
5461                Event::BackbonePeerWriteStall {
5462                    server_interface_id,
5463                    peer_interface_id,
5464                    peer_ip,
5465                    peer_port,
5466                    connected_for,
5467                } => {
5468                    #[cfg(feature = "rns-hooks")]
5469                    {
5470                        self.run_backbone_peer_hook(
5471                            "BackbonePeerWriteStall",
5472                            HookPoint::BackbonePeerWriteStall,
5473                            &BackbonePeerHookEvent {
5474                                server_interface_id,
5475                                peer_interface_id: Some(peer_interface_id),
5476                                peer_ip,
5477                                peer_port,
5478                                connected_for,
5479                                had_received_data: false,
5480                                penalty_level: 0,
5481                                blacklist_for: Duration::ZERO,
5482                            },
5483                        );
5484                    }
5485                    #[cfg(not(feature = "rns-hooks"))]
5486                    let _ = (
5487                        server_interface_id,
5488                        peer_interface_id,
5489                        peer_ip,
5490                        peer_port,
5491                        connected_for,
5492                    );
5493                }
5494                Event::BackbonePeerPenalty {
5495                    server_interface_id,
5496                    peer_ip,
5497                    penalty_level,
5498                    blacklist_for,
5499                } => {
5500                    #[cfg(feature = "rns-hooks")]
5501                    {
5502                        self.run_backbone_peer_hook(
5503                            "BackbonePeerPenalty",
5504                            HookPoint::BackbonePeerPenalty,
5505                            &BackbonePeerHookEvent {
5506                                server_interface_id,
5507                                peer_interface_id: None,
5508                                peer_ip,
5509                                peer_port: 0,
5510                                connected_for: Duration::ZERO,
5511                                had_received_data: false,
5512                                penalty_level,
5513                                blacklist_for,
5514                            },
5515                        );
5516                    }
5517                    #[cfg(not(feature = "rns-hooks"))]
5518                    let _ = (server_interface_id, peer_ip, penalty_level, blacklist_for);
5519                }
5520                Event::Shutdown => {
5521                    self.lifecycle_state = LifecycleState::Stopped;
5522                    break;
5523                }
5524            }
5525        }
5526    }
5527
5528    /// Handle a query request and produce a response.
5529    fn handle_query(&self, request: QueryRequest) -> QueryResponse {
5530        match request {
5531            QueryRequest::InterfaceStats => {
5532                let mut interfaces = Vec::new();
5533                let mut total_rxb: u64 = 0;
5534                let mut total_txb: u64 = 0;
5535                for entry in self.interfaces.values() {
5536                    total_rxb += entry.stats.rxb;
5537                    total_txb += entry.stats.txb;
5538                    interfaces.push(SingleInterfaceStat {
5539                        id: entry.info.id.0,
5540                        name: entry.info.name.clone(),
5541                        status: entry.online && entry.enabled,
5542                        mode: entry.info.mode,
5543                        rxb: entry.stats.rxb,
5544                        txb: entry.stats.txb,
5545                        rx_packets: entry.stats.rx_packets,
5546                        tx_packets: entry.stats.tx_packets,
5547                        bitrate: entry.info.bitrate,
5548                        ifac_size: entry.ifac.as_ref().map(|s| s.size),
5549                        started: entry.stats.started,
5550                        ia_freq: entry.stats.incoming_announce_freq(),
5551                        oa_freq: entry.stats.outgoing_announce_freq(),
5552                        interface_type: entry.interface_type.clone(),
5553                    });
5554                }
5555                // Sort by name for consistent output
5556                interfaces.sort_by(|a, b| a.name.cmp(&b.name));
5557                QueryResponse::InterfaceStats(InterfaceStatsResponse {
5558                    interfaces,
5559                    transport_id: self.engine.identity_hash().copied(),
5560                    transport_enabled: self.engine.transport_enabled(),
5561                    transport_uptime: time::now() - self.started,
5562                    total_rxb,
5563                    total_txb,
5564                    probe_responder: self.probe_responder_hash,
5565                })
5566            }
5567            QueryRequest::BackboneInterfaces => {
5568                QueryResponse::BackboneInterfaces(self.list_backbone_interfaces())
5569            }
5570            QueryRequest::ProviderBridgeStats => {
5571                #[cfg(feature = "rns-hooks")]
5572                {
5573                    QueryResponse::ProviderBridgeStats(
5574                        self.provider_bridge.as_ref().map(|bridge| bridge.stats()),
5575                    )
5576                }
5577                #[cfg(not(feature = "rns-hooks"))]
5578                {
5579                    QueryResponse::ProviderBridgeStats(None::<crate::event::ProviderBridgeStats>)
5580                }
5581            }
5582            QueryRequest::DrainStatus => QueryResponse::DrainStatus(self.drain_status()),
5583            QueryRequest::PathTable { max_hops } => {
5584                let entries: Vec<PathTableEntry> = self
5585                    .engine
5586                    .path_table_entries()
5587                    .filter(|(_, entry)| max_hops.map_or(true, |max| entry.hops <= max))
5588                    .map(|(hash, entry)| {
5589                        let iface_name = self
5590                            .interfaces
5591                            .get(&entry.receiving_interface)
5592                            .map(|e| e.info.name.clone())
5593                            .or_else(|| {
5594                                self.engine
5595                                    .interface_info(&entry.receiving_interface)
5596                                    .map(|i| i.name.clone())
5597                            })
5598                            .unwrap_or_default();
5599                        PathTableEntry {
5600                            hash: *hash,
5601                            timestamp: entry.timestamp,
5602                            via: entry.next_hop,
5603                            hops: entry.hops,
5604                            expires: entry.expires,
5605                            interface: entry.receiving_interface,
5606                            interface_name: iface_name,
5607                        }
5608                    })
5609                    .collect();
5610                QueryResponse::PathTable(entries)
5611            }
5612            QueryRequest::RateTable => {
5613                let entries: Vec<RateTableEntry> = self
5614                    .engine
5615                    .rate_limiter()
5616                    .entries()
5617                    .map(|(hash, entry)| RateTableEntry {
5618                        hash: *hash,
5619                        last: entry.last,
5620                        rate_violations: entry.rate_violations,
5621                        blocked_until: entry.blocked_until,
5622                        timestamps: entry.timestamps.clone(),
5623                    })
5624                    .collect();
5625                QueryResponse::RateTable(entries)
5626            }
5627            QueryRequest::NextHop { dest_hash } => {
5628                let resp = self
5629                    .engine
5630                    .next_hop(&dest_hash)
5631                    .map(|next_hop| NextHopResponse {
5632                        next_hop,
5633                        hops: self.engine.hops_to(&dest_hash).unwrap_or(0),
5634                        interface: self
5635                            .engine
5636                            .next_hop_interface(&dest_hash)
5637                            .unwrap_or(InterfaceId(0)),
5638                    });
5639                QueryResponse::NextHop(resp)
5640            }
5641            QueryRequest::NextHopIfName { dest_hash } => {
5642                let name = self
5643                    .engine
5644                    .next_hop_interface(&dest_hash)
5645                    .and_then(|id| self.interfaces.get(&id))
5646                    .map(|entry| entry.info.name.clone());
5647                QueryResponse::NextHopIfName(name)
5648            }
5649            QueryRequest::LinkCount => QueryResponse::LinkCount(
5650                self.engine.link_table_count() + self.link_manager.link_count(),
5651            ),
5652            QueryRequest::DropPath { .. } => {
5653                // Mutating queries are handled by handle_query_mut
5654                QueryResponse::DropPath(false)
5655            }
5656            QueryRequest::DropAllVia { .. } => QueryResponse::DropAllVia(0),
5657            QueryRequest::DropAnnounceQueues => QueryResponse::DropAnnounceQueues,
5658            QueryRequest::TransportIdentity => {
5659                QueryResponse::TransportIdentity(self.engine.identity_hash().copied())
5660            }
5661            QueryRequest::GetBlackholed => {
5662                let now = time::now();
5663                let entries: Vec<BlackholeInfo> = self
5664                    .engine
5665                    .blackholed_entries()
5666                    .filter(|(_, e)| e.expires == 0.0 || e.expires > now)
5667                    .map(|(hash, entry)| BlackholeInfo {
5668                        identity_hash: *hash,
5669                        created: entry.created,
5670                        expires: entry.expires,
5671                        reason: entry.reason.clone(),
5672                    })
5673                    .collect();
5674                QueryResponse::Blackholed(entries)
5675            }
5676            QueryRequest::BlackholeIdentity { .. } | QueryRequest::UnblackholeIdentity { .. } => {
5677                // Mutating queries handled by handle_query_mut
5678                QueryResponse::BlackholeResult(false)
5679            }
5680            QueryRequest::InjectPath { .. } => {
5681                // Mutating queries handled by handle_query_mut
5682                QueryResponse::InjectPath(false)
5683            }
5684            QueryRequest::InjectIdentity { .. } => {
5685                // Mutating queries handled by handle_query_mut
5686                QueryResponse::InjectIdentity(false)
5687            }
5688            QueryRequest::HasPath { dest_hash } => {
5689                QueryResponse::HasPath(self.engine.has_path(&dest_hash))
5690            }
5691            QueryRequest::HopsTo { dest_hash } => {
5692                QueryResponse::HopsTo(self.engine.hops_to(&dest_hash))
5693            }
5694            QueryRequest::RecallIdentity { dest_hash } => {
5695                QueryResponse::RecallIdentity(self.known_destinations.get(&dest_hash).cloned())
5696            }
5697            QueryRequest::LocalDestinations => {
5698                let entries: Vec<LocalDestinationEntry> = self
5699                    .local_destinations
5700                    .iter()
5701                    .map(|(hash, dest_type)| LocalDestinationEntry {
5702                        hash: *hash,
5703                        dest_type: *dest_type,
5704                    })
5705                    .collect();
5706                QueryResponse::LocalDestinations(entries)
5707            }
5708            QueryRequest::Links => QueryResponse::Links(self.link_manager.link_entries()),
5709            QueryRequest::Resources => {
5710                QueryResponse::Resources(self.link_manager.resource_entries())
5711            }
5712            QueryRequest::DiscoveredInterfaces {
5713                only_available,
5714                only_transport,
5715            } => {
5716                let mut interfaces = self.discovered_interfaces.list().unwrap_or_default();
5717                crate::discovery::filter_and_sort_interfaces(
5718                    &mut interfaces,
5719                    only_available,
5720                    only_transport,
5721                );
5722                QueryResponse::DiscoveredInterfaces(interfaces)
5723            }
5724            QueryRequest::ListRuntimeConfig => {
5725                QueryResponse::RuntimeConfigList(self.list_runtime_config())
5726            }
5727            QueryRequest::GetRuntimeConfig { key } => {
5728                QueryResponse::RuntimeConfigEntry(self.runtime_config_entry(&key))
5729            }
5730            QueryRequest::BackbonePeerState { interface_name } => QueryResponse::BackbonePeerState(
5731                self.list_backbone_peer_state(interface_name.as_deref()),
5732            ),
5733            // Mutating queries handled by handle_query_mut
5734            QueryRequest::SendProbe { .. } => QueryResponse::SendProbe(None),
5735            QueryRequest::CheckProof { .. } => QueryResponse::CheckProof(None),
5736            QueryRequest::SetRuntimeConfig { .. } => {
5737                QueryResponse::RuntimeConfigSet(Err(RuntimeConfigError {
5738                    code: RuntimeConfigErrorCode::Unsupported,
5739                    message: "mutating runtime config is handled separately".to_string(),
5740                }))
5741            }
5742            QueryRequest::ResetRuntimeConfig { .. } => {
5743                QueryResponse::RuntimeConfigReset(Err(RuntimeConfigError {
5744                    code: RuntimeConfigErrorCode::Unsupported,
5745                    message: "mutating runtime config is handled separately".to_string(),
5746                }))
5747            }
5748            QueryRequest::ClearBackbonePeerState { .. } => {
5749                QueryResponse::ClearBackbonePeerState(false)
5750            }
5751            QueryRequest::BlacklistBackbonePeer { .. } => {
5752                QueryResponse::BlacklistBackbonePeer(false)
5753            }
5754        }
5755    }
5756
5757    /// Handle a mutating query request.
5758    fn handle_query_mut(&mut self, request: QueryRequest) -> QueryResponse {
5759        match request {
5760            QueryRequest::BlackholeIdentity {
5761                identity_hash,
5762                duration_hours,
5763                reason,
5764            } => {
5765                let now = time::now();
5766                self.engine
5767                    .blackhole_identity(identity_hash, now, duration_hours, reason);
5768                QueryResponse::BlackholeResult(true)
5769            }
5770            QueryRequest::UnblackholeIdentity { identity_hash } => {
5771                let result = self.engine.unblackhole_identity(&identity_hash);
5772                QueryResponse::UnblackholeResult(result)
5773            }
5774            QueryRequest::DropPath { dest_hash } => {
5775                QueryResponse::DropPath(self.engine.drop_path(&dest_hash))
5776            }
5777            QueryRequest::DropAllVia { transport_hash } => {
5778                QueryResponse::DropAllVia(self.engine.drop_all_via(&transport_hash))
5779            }
5780            QueryRequest::DropAnnounceQueues => {
5781                self.engine.drop_announce_queues();
5782                QueryResponse::DropAnnounceQueues
5783            }
5784            QueryRequest::ClearBackbonePeerState {
5785                interface_name,
5786                peer_ip,
5787            } => QueryResponse::ClearBackbonePeerState(
5788                self.clear_backbone_peer_state(&interface_name, peer_ip),
5789            ),
5790            QueryRequest::BlacklistBackbonePeer {
5791                interface_name,
5792                peer_ip,
5793                duration,
5794                reason,
5795                penalty_level,
5796            } => QueryResponse::BlacklistBackbonePeer(self.blacklist_backbone_peer(
5797                &interface_name,
5798                peer_ip,
5799                duration,
5800                reason,
5801                penalty_level,
5802            )),
5803            QueryRequest::DrainStatus => QueryResponse::DrainStatus(self.drain_status()),
5804            QueryRequest::InjectPath {
5805                dest_hash,
5806                next_hop,
5807                hops,
5808                expires,
5809                interface_name,
5810                packet_hash,
5811            } => {
5812                // Resolve interface_name → InterfaceId
5813                let iface_id = self
5814                    .interfaces
5815                    .iter()
5816                    .find(|(_, entry)| entry.info.name == interface_name)
5817                    .map(|(id, _)| *id);
5818                match iface_id {
5819                    Some(id) => {
5820                        let entry = PathEntry {
5821                            timestamp: time::now(),
5822                            next_hop,
5823                            hops,
5824                            expires,
5825                            random_blobs: Vec::new(),
5826                            receiving_interface: id,
5827                            packet_hash,
5828                            announce_raw: None,
5829                        };
5830                        self.engine.inject_path(dest_hash, entry);
5831                        QueryResponse::InjectPath(true)
5832                    }
5833                    None => QueryResponse::InjectPath(false),
5834                }
5835            }
5836            QueryRequest::InjectIdentity {
5837                dest_hash,
5838                identity_hash,
5839                public_key,
5840                app_data,
5841                hops,
5842                received_at,
5843            } => {
5844                self.upsert_known_destination(
5845                    dest_hash,
5846                    crate::destination::AnnouncedIdentity {
5847                        dest_hash: rns_core::types::DestHash(dest_hash),
5848                        identity_hash: rns_core::types::IdentityHash(identity_hash),
5849                        public_key,
5850                        app_data,
5851                        hops,
5852                        received_at,
5853                        receiving_interface: rns_core::transport::types::InterfaceId(0),
5854                    },
5855                );
5856                QueryResponse::InjectIdentity(true)
5857            }
5858            QueryRequest::SendProbe {
5859                dest_hash,
5860                payload_size,
5861            } => {
5862                // Look up the identity for this destination hash
5863                let announced = self.known_destinations.get(&dest_hash).cloned();
5864                match announced {
5865                    Some(recalled) => {
5866                        // Encrypt random payload with remote public key
5867                        let remote_id =
5868                            rns_crypto::identity::Identity::from_public_key(&recalled.public_key);
5869                        let mut payload = vec![0u8; payload_size];
5870                        self.rng.fill_bytes(&mut payload);
5871                        match remote_id.encrypt(&payload, &mut self.rng) {
5872                            Ok(ciphertext) => {
5873                                // Build DATA SINGLE BROADCAST packet to dest_hash
5874                                let flags = rns_core::packet::PacketFlags {
5875                                    header_type: rns_core::constants::HEADER_1,
5876                                    context_flag: rns_core::constants::FLAG_UNSET,
5877                                    transport_type: rns_core::constants::TRANSPORT_BROADCAST,
5878                                    destination_type: rns_core::constants::DESTINATION_SINGLE,
5879                                    packet_type: rns_core::constants::PACKET_TYPE_DATA,
5880                                };
5881                                match RawPacket::pack(
5882                                    flags,
5883                                    0,
5884                                    &dest_hash,
5885                                    None,
5886                                    rns_core::constants::CONTEXT_NONE,
5887                                    &ciphertext,
5888                                ) {
5889                                    Ok(packet) => {
5890                                        let packet_hash = packet.packet_hash;
5891                                        let hops = self.engine.hops_to(&dest_hash).unwrap_or(0);
5892                                        // Track for proof matching
5893                                        self.sent_packets
5894                                            .insert(packet_hash, (dest_hash, time::now()));
5895                                        // Send via engine
5896                                        let actions = self.engine.handle_outbound(
5897                                            &packet,
5898                                            rns_core::constants::DESTINATION_SINGLE,
5899                                            None,
5900                                            time::now(),
5901                                        );
5902                                        self.dispatch_all(actions);
5903                                        log::debug!(
5904                                            "Sent probe ({} bytes) to {:02x?}",
5905                                            payload_size,
5906                                            &dest_hash[..4],
5907                                        );
5908                                        QueryResponse::SendProbe(Some((packet_hash, hops)))
5909                                    }
5910                                    Err(_) => {
5911                                        log::warn!("Failed to pack probe packet");
5912                                        QueryResponse::SendProbe(None)
5913                                    }
5914                                }
5915                            }
5916                            Err(_) => {
5917                                log::warn!("Failed to encrypt probe payload");
5918                                QueryResponse::SendProbe(None)
5919                            }
5920                        }
5921                    }
5922                    None => {
5923                        log::debug!("No known identity for probe dest {:02x?}", &dest_hash[..4]);
5924                        QueryResponse::SendProbe(None)
5925                    }
5926                }
5927            }
5928            QueryRequest::CheckProof { packet_hash } => {
5929                match self.completed_proofs.remove(&packet_hash) {
5930                    Some((rtt, _received)) => QueryResponse::CheckProof(Some(rtt)),
5931                    None => QueryResponse::CheckProof(None),
5932                }
5933            }
5934            QueryRequest::SetRuntimeConfig { key, value } => {
5935                let result = match key.as_str() {
5936                    "global.tick_interval_ms" => match Self::expect_u64(value, &key) {
5937                        Ok(value) => {
5938                            let clamped = value.clamp(100, 10_000);
5939                            self.tick_interval_ms.store(clamped, Ordering::Relaxed);
5940                            Ok(())
5941                        }
5942                        Err(err) => Err(err),
5943                    },
5944                    "global.known_destinations_ttl_secs" => match Self::expect_f64(value, &key) {
5945                        Ok(value) => {
5946                            self.known_destinations_ttl = value;
5947                            Ok(())
5948                        }
5949                        Err(err) => Err(err),
5950                    },
5951                    "global.rate_limiter_ttl_secs" => match Self::expect_f64(value, &key) {
5952                        Ok(value) if value >= 0.0 => {
5953                            self.rate_limiter_ttl_secs = value;
5954                            Ok(())
5955                        }
5956                        Ok(_) => Err(RuntimeConfigError {
5957                            code: RuntimeConfigErrorCode::InvalidValue,
5958                            message: format!("{} must be >= 0", key),
5959                        }),
5960                        Err(err) => Err(err),
5961                    },
5962                    "global.known_destinations_cleanup_interval_ticks" => {
5963                        match Self::expect_u64(value, &key) {
5964                            Ok(value) if value > 0 => {
5965                                self.known_destinations_cleanup_interval_ticks = value as u32;
5966                                Ok(())
5967                            }
5968                            Ok(_) => Err(RuntimeConfigError {
5969                                code: RuntimeConfigErrorCode::InvalidValue,
5970                                message: format!("{} must be >= 1", key),
5971                            }),
5972                            Err(err) => Err(err),
5973                        }
5974                    }
5975                    "global.announce_cache_cleanup_interval_ticks" => {
5976                        match Self::expect_u64(value, &key) {
5977                            Ok(value) if value > 0 => {
5978                                self.announce_cache_cleanup_interval_ticks = value as u32;
5979                                Ok(())
5980                            }
5981                            Ok(_) => Err(RuntimeConfigError {
5982                                code: RuntimeConfigErrorCode::InvalidValue,
5983                                message: format!("{} must be >= 1", key),
5984                            }),
5985                            Err(err) => Err(err),
5986                        }
5987                    }
5988                    "global.announce_cache_cleanup_batch_size" => {
5989                        match Self::expect_u64(value, &key) {
5990                            Ok(value) if value > 0 => {
5991                                self.announce_cache_cleanup_batch_size = value as usize;
5992                                Ok(())
5993                            }
5994                            Ok(_) => Err(RuntimeConfigError {
5995                                code: RuntimeConfigErrorCode::InvalidValue,
5996                                message: format!("{} must be >= 1", key),
5997                            }),
5998                            Err(err) => Err(err),
5999                        }
6000                    }
6001                    "global.discovery_cleanup_interval_ticks" => {
6002                        match Self::expect_u64(value, &key) {
6003                            Ok(value) if value > 0 => {
6004                                self.discovery_cleanup_interval_ticks = value as u32;
6005                                Ok(())
6006                            }
6007                            Ok(_) => Err(RuntimeConfigError {
6008                                code: RuntimeConfigErrorCode::InvalidValue,
6009                                message: format!("{} must be >= 1", key),
6010                            }),
6011                            Err(err) => Err(err),
6012                        }
6013                    }
6014                    "global.management_announce_interval_secs" => {
6015                        match Self::expect_f64(value, &key) {
6016                            Ok(value) => {
6017                                self.management_announce_interval_secs = value;
6018                                Ok(())
6019                            }
6020                            Err(err) => Err(err),
6021                        }
6022                    }
6023                    "global.direct_connect_policy" => {
6024                        let policy = match Self::parse_holepunch_policy(&value) {
6025                            Some(policy) => policy,
6026                            None => {
6027                                return QueryResponse::RuntimeConfigSet(Err(RuntimeConfigError {
6028                                    code: RuntimeConfigErrorCode::InvalidValue,
6029                                    message: format!(
6030                                        "{} must be one of: reject, accept_all, ask_app",
6031                                        key
6032                                    ),
6033                                }))
6034                            }
6035                        };
6036                        self.holepunch_manager.set_policy(policy);
6037                        Ok(())
6038                    }
6039                    #[cfg(feature = "rns-hooks")]
6040                    "provider.queue_max_events" => match Self::expect_u64(value, &key) {
6041                        Ok(v) if v > 0 => {
6042                            if let Some(ref bridge) = self.provider_bridge {
6043                                bridge.set_queue_max_events(v as usize);
6044                            }
6045                            Ok(())
6046                        }
6047                        Ok(_) => Err(RuntimeConfigError {
6048                            code: RuntimeConfigErrorCode::InvalidValue,
6049                            message: format!("{} must be >= 1", key),
6050                        }),
6051                        Err(err) => Err(err),
6052                    },
6053                    #[cfg(feature = "rns-hooks")]
6054                    "provider.queue_max_bytes" => match Self::expect_u64(value, &key) {
6055                        Ok(v) if v > 0 => {
6056                            if let Some(ref bridge) = self.provider_bridge {
6057                                bridge.set_queue_max_bytes(v as usize);
6058                            }
6059                            Ok(())
6060                        }
6061                        Ok(_) => Err(RuntimeConfigError {
6062                            code: RuntimeConfigErrorCode::InvalidValue,
6063                            message: format!("{} must be >= 1", key),
6064                        }),
6065                        Err(err) => Err(err),
6066                    },
6067                    #[cfg(feature = "iface-backbone")]
6068                    _ if key.starts_with("backbone.") => {
6069                        self.set_backbone_runtime_config(&key, value)
6070                    }
6071                    #[cfg(feature = "iface-backbone")]
6072                    _ if key.starts_with("backbone_client.") => {
6073                        self.set_backbone_client_runtime_config(&key, value)
6074                    }
6075                    #[cfg(feature = "iface-tcp")]
6076                    _ if key.starts_with("tcp_server.") => {
6077                        self.set_tcp_server_runtime_config(&key, value)
6078                    }
6079                    #[cfg(feature = "iface-tcp")]
6080                    _ if key.starts_with("tcp_client.") => {
6081                        self.set_tcp_client_runtime_config(&key, value)
6082                    }
6083                    #[cfg(feature = "iface-udp")]
6084                    _ if key.starts_with("udp.") => self.set_udp_runtime_config(&key, value),
6085                    #[cfg(feature = "iface-auto")]
6086                    _ if key.starts_with("auto.") => self.set_auto_runtime_config(&key, value),
6087                    #[cfg(feature = "iface-i2p")]
6088                    _ if key.starts_with("i2p.") => self.set_i2p_runtime_config(&key, value),
6089                    #[cfg(feature = "iface-pipe")]
6090                    _ if key.starts_with("pipe.") => self.set_pipe_runtime_config(&key, value),
6091                    #[cfg(feature = "iface-rnode")]
6092                    _ if key.starts_with("rnode.") => self.set_rnode_runtime_config(&key, value),
6093                    _ if key.starts_with("interface.") => {
6094                        self.set_generic_interface_runtime_config(&key, value)
6095                    }
6096                    _ => {
6097                        return QueryResponse::RuntimeConfigSet(Err(RuntimeConfigError {
6098                            code: RuntimeConfigErrorCode::UnknownKey,
6099                            message: format!("unknown runtime-config key '{}'", key),
6100                        }))
6101                    }
6102                };
6103
6104                QueryResponse::RuntimeConfigSet(match result {
6105                    Ok(()) => self.runtime_config_entry(&key).ok_or(RuntimeConfigError {
6106                        code: RuntimeConfigErrorCode::ApplyFailed,
6107                        message: format!("failed to read back runtime-config key '{}'", key),
6108                    }),
6109                    Err(err) => Err(err),
6110                })
6111            }
6112            QueryRequest::ResetRuntimeConfig { key } => {
6113                let defaults = self.runtime_config_defaults;
6114                let result = match key.as_str() {
6115                    "global.tick_interval_ms" => {
6116                        self.tick_interval_ms
6117                            .store(defaults.tick_interval_ms, Ordering::Relaxed);
6118                        Ok(())
6119                    }
6120                    "global.known_destinations_ttl_secs" => {
6121                        self.known_destinations_ttl = defaults.known_destinations_ttl;
6122                        Ok(())
6123                    }
6124                    "global.rate_limiter_ttl_secs" => {
6125                        self.rate_limiter_ttl_secs = defaults.rate_limiter_ttl_secs;
6126                        Ok(())
6127                    }
6128                    "global.known_destinations_cleanup_interval_ticks" => {
6129                        self.known_destinations_cleanup_interval_ticks =
6130                            defaults.known_destinations_cleanup_interval_ticks;
6131                        Ok(())
6132                    }
6133                    "global.announce_cache_cleanup_interval_ticks" => {
6134                        self.announce_cache_cleanup_interval_ticks =
6135                            defaults.announce_cache_cleanup_interval_ticks;
6136                        Ok(())
6137                    }
6138                    "global.announce_cache_cleanup_batch_size" => {
6139                        self.announce_cache_cleanup_batch_size =
6140                            defaults.announce_cache_cleanup_batch_size;
6141                        Ok(())
6142                    }
6143                    "global.discovery_cleanup_interval_ticks" => {
6144                        self.discovery_cleanup_interval_ticks =
6145                            defaults.discovery_cleanup_interval_ticks;
6146                        Ok(())
6147                    }
6148                    "global.management_announce_interval_secs" => {
6149                        self.management_announce_interval_secs =
6150                            defaults.management_announce_interval_secs;
6151                        Ok(())
6152                    }
6153                    "global.direct_connect_policy" => {
6154                        self.holepunch_manager
6155                            .set_policy(defaults.direct_connect_policy);
6156                        Ok(())
6157                    }
6158                    #[cfg(feature = "rns-hooks")]
6159                    "provider.queue_max_events" => {
6160                        if let Some(ref bridge) = self.provider_bridge {
6161                            bridge.set_queue_max_events(defaults.provider_queue_max_events);
6162                        }
6163                        Ok(())
6164                    }
6165                    #[cfg(feature = "rns-hooks")]
6166                    "provider.queue_max_bytes" => {
6167                        if let Some(ref bridge) = self.provider_bridge {
6168                            bridge.set_queue_max_bytes(defaults.provider_queue_max_bytes);
6169                        }
6170                        Ok(())
6171                    }
6172                    #[cfg(feature = "iface-backbone")]
6173                    _ if key.starts_with("backbone.") => self.reset_backbone_runtime_config(&key),
6174                    #[cfg(feature = "iface-backbone")]
6175                    _ if key.starts_with("backbone_client.") => {
6176                        self.reset_backbone_client_runtime_config(&key)
6177                    }
6178                    #[cfg(feature = "iface-tcp")]
6179                    _ if key.starts_with("tcp_server.") => {
6180                        self.reset_tcp_server_runtime_config(&key)
6181                    }
6182                    #[cfg(feature = "iface-tcp")]
6183                    _ if key.starts_with("tcp_client.") => {
6184                        self.reset_tcp_client_runtime_config(&key)
6185                    }
6186                    #[cfg(feature = "iface-udp")]
6187                    _ if key.starts_with("udp.") => self.reset_udp_runtime_config(&key),
6188                    #[cfg(feature = "iface-auto")]
6189                    _ if key.starts_with("auto.") => self.reset_auto_runtime_config(&key),
6190                    #[cfg(feature = "iface-i2p")]
6191                    _ if key.starts_with("i2p.") => self.reset_i2p_runtime_config(&key),
6192                    #[cfg(feature = "iface-pipe")]
6193                    _ if key.starts_with("pipe.") => self.reset_pipe_runtime_config(&key),
6194                    #[cfg(feature = "iface-rnode")]
6195                    _ if key.starts_with("rnode.") => self.reset_rnode_runtime_config(&key),
6196                    _ if key.starts_with("interface.") => {
6197                        self.reset_generic_interface_runtime_config(&key)
6198                    }
6199                    _ => {
6200                        return QueryResponse::RuntimeConfigReset(Err(RuntimeConfigError {
6201                            code: RuntimeConfigErrorCode::UnknownKey,
6202                            message: format!("unknown runtime-config key '{}'", key),
6203                        }))
6204                    }
6205                };
6206
6207                QueryResponse::RuntimeConfigReset(match result {
6208                    Ok(()) => self.runtime_config_entry(&key).ok_or(RuntimeConfigError {
6209                        code: RuntimeConfigErrorCode::ApplyFailed,
6210                        message: format!("failed to read back runtime-config key '{}'", key),
6211                    }),
6212                    Err(err) => Err(err),
6213                })
6214            }
6215            other => self.handle_query(other),
6216        }
6217    }
6218
6219    /// Handle a tunnel synthesis packet delivered locally.
6220    fn handle_tunnel_synth_delivery(&mut self, raw: &[u8]) {
6221        // Extract the data payload from the raw packet
6222        let packet = match RawPacket::unpack(raw) {
6223            Ok(p) => p,
6224            Err(_) => return,
6225        };
6226
6227        match rns_core::transport::tunnel::validate_tunnel_synthesize_data(&packet.data) {
6228            Ok(validated) => {
6229                // Find the interface this tunnel belongs to by computing the expected
6230                // tunnel_id for each interface with wants_tunnel
6231                let iface_id = self
6232                    .interfaces
6233                    .iter()
6234                    .find(|(_, entry)| entry.info.wants_tunnel && entry.online && entry.enabled)
6235                    .map(|(id, _)| *id);
6236
6237                if let Some(iface) = iface_id {
6238                    let now = time::now();
6239                    let tunnel_actions = self.engine.handle_tunnel(validated.tunnel_id, iface, now);
6240                    self.dispatch_all(tunnel_actions);
6241                }
6242            }
6243            Err(e) => {
6244                log::debug!("Tunnel synthesis validation failed: {}", e);
6245            }
6246        }
6247    }
6248
6249    /// Synthesize a tunnel on an interface that wants it.
6250    ///
6251    /// Called when an interface with `wants_tunnel` comes up.
6252    fn synthesize_tunnel_for_interface(&mut self, interface: InterfaceId) {
6253        if let Some(ref identity) = self.transport_identity {
6254            let actions = self
6255                .engine
6256                .synthesize_tunnel(identity, interface, &mut self.rng);
6257            self.dispatch_all(actions);
6258        }
6259    }
6260
6261    /// Build and send a path request packet for a destination.
6262    fn handle_request_path(&mut self, dest_hash: [u8; 16]) {
6263        // Build path request data: dest_hash(16) || [transport_id(16)] || random_tag(16)
6264        let mut data = Vec::with_capacity(48);
6265        data.extend_from_slice(&dest_hash);
6266
6267        if self.engine.transport_enabled() {
6268            if let Some(id_hash) = self.engine.identity_hash() {
6269                data.extend_from_slice(id_hash);
6270            }
6271        }
6272
6273        // Random tag (16 bytes)
6274        let mut tag = [0u8; 16];
6275        self.rng.fill_bytes(&mut tag);
6276        data.extend_from_slice(&tag);
6277
6278        // Build as BROADCAST DATA PLAIN packet to rnstransport.path.request
6279        let flags = rns_core::packet::PacketFlags {
6280            header_type: rns_core::constants::HEADER_1,
6281            context_flag: rns_core::constants::FLAG_UNSET,
6282            transport_type: rns_core::constants::TRANSPORT_BROADCAST,
6283            destination_type: rns_core::constants::DESTINATION_PLAIN,
6284            packet_type: rns_core::constants::PACKET_TYPE_DATA,
6285        };
6286
6287        if let Ok(packet) = RawPacket::pack(
6288            flags,
6289            0,
6290            &self.path_request_dest,
6291            None,
6292            rns_core::constants::CONTEXT_NONE,
6293            &data,
6294        ) {
6295            let actions = self.engine.handle_outbound(
6296                &packet,
6297                rns_core::constants::DESTINATION_PLAIN,
6298                None,
6299                time::now(),
6300            );
6301            self.dispatch_all(actions);
6302        }
6303    }
6304
6305    /// Check if we should generate a proof for a delivered packet,
6306    /// and if so, sign and send it.
6307    fn maybe_generate_proof(&mut self, dest_hash: [u8; 16], packet_hash: &[u8; 32]) {
6308        use rns_core::types::ProofStrategy;
6309
6310        let (strategy, identity) = match self.proof_strategies.get(&dest_hash) {
6311            Some((s, id)) => (*s, id.as_ref()),
6312            None => return,
6313        };
6314
6315        let should_prove = match strategy {
6316            ProofStrategy::ProveAll => true,
6317            ProofStrategy::ProveApp => self.callbacks.on_proof_requested(
6318                rns_core::types::DestHash(dest_hash),
6319                rns_core::types::PacketHash(*packet_hash),
6320            ),
6321            ProofStrategy::ProveNone => false,
6322        };
6323
6324        if !should_prove {
6325            return;
6326        }
6327
6328        let identity = match identity {
6329            Some(id) => id,
6330            None => {
6331                log::warn!(
6332                    "Cannot generate proof for {:02x?}: no signing key",
6333                    &dest_hash[..4]
6334                );
6335                return;
6336            }
6337        };
6338
6339        // Sign the packet hash to create the proof
6340        let signature = match identity.sign(packet_hash) {
6341            Ok(sig) => sig,
6342            Err(e) => {
6343                log::warn!("Failed to sign proof for {:02x?}: {:?}", &dest_hash[..4], e);
6344                return;
6345            }
6346        };
6347
6348        // Build explicit proof: [packet_hash:32][signature:64]
6349        let mut proof_data = Vec::with_capacity(96);
6350        proof_data.extend_from_slice(packet_hash);
6351        proof_data.extend_from_slice(&signature);
6352
6353        // Address the proof to the truncated packet hash (first 16 bytes),
6354        // matching Python's ProofDestination (Packet.py:390-394).
6355        // Transport nodes create reverse_table entries keyed by truncated
6356        // packet hash when forwarding data, so this allows proofs to be
6357        // routed back to the sender via the reverse path.
6358        let mut proof_dest = [0u8; 16];
6359        proof_dest.copy_from_slice(&packet_hash[..16]);
6360
6361        let flags = rns_core::packet::PacketFlags {
6362            header_type: rns_core::constants::HEADER_1,
6363            context_flag: rns_core::constants::FLAG_UNSET,
6364            transport_type: rns_core::constants::TRANSPORT_BROADCAST,
6365            destination_type: rns_core::constants::DESTINATION_SINGLE,
6366            packet_type: rns_core::constants::PACKET_TYPE_PROOF,
6367        };
6368
6369        if let Ok(packet) = RawPacket::pack(
6370            flags,
6371            0,
6372            &proof_dest,
6373            None,
6374            rns_core::constants::CONTEXT_NONE,
6375            &proof_data,
6376        ) {
6377            let actions = self.engine.handle_outbound(
6378                &packet,
6379                rns_core::constants::DESTINATION_SINGLE,
6380                None,
6381                time::now(),
6382            );
6383            self.dispatch_all(actions);
6384            log::debug!(
6385                "Generated proof for packet on dest {:02x?}",
6386                &dest_hash[..4]
6387            );
6388        }
6389    }
6390
6391    /// Handle an inbound proof packet: validate and fire on_proof callback.
6392    fn handle_inbound_proof(
6393        &mut self,
6394        dest_hash: [u8; 16],
6395        proof_data: &[u8],
6396        _raw_packet_hash: &[u8; 32],
6397    ) {
6398        // Reticulum supports both proof formats:
6399        // - explicit: [packet_hash:32][signature:64]
6400        // - implicit: [signature:64], keyed by proof destination hash
6401        let (tracked_hash, signature): ([u8; 32], &[u8]) = if proof_data.len() >= 96 {
6402            let mut tracked_hash = [0u8; 32];
6403            tracked_hash.copy_from_slice(&proof_data[..32]);
6404            (tracked_hash, &proof_data[32..96])
6405        } else if proof_data.len() == 64 {
6406            let mut candidates = self
6407                .sent_packets
6408                .iter()
6409                .filter_map(|(packet_hash, _)| {
6410                    if packet_hash[..16] == dest_hash {
6411                        Some(*packet_hash)
6412                    } else {
6413                        None
6414                    }
6415                })
6416                .collect::<Vec<_>>();
6417
6418            if candidates.is_empty() {
6419                log::debug!(
6420                    "Implicit proof for unknown packet prefix {:02x?} on dest {:02x?}",
6421                    &dest_hash[..4],
6422                    &dest_hash[..4]
6423                );
6424                return;
6425            }
6426
6427            // Multiple matches are extremely unlikely (16-byte truncated hash).
6428            // Use the newest tracked packet for deterministic behavior.
6429            if candidates.len() > 1 {
6430                candidates.sort_by(|a, b| {
6431                    let ta = self.sent_packets.get(a).map(|(_, t)| *t).unwrap_or_default();
6432                    let tb = self.sent_packets.get(b).map(|(_, t)| *t).unwrap_or_default();
6433                    tb.partial_cmp(&ta).unwrap_or(core::cmp::Ordering::Equal)
6434                });
6435                log::debug!(
6436                    "Implicit proof matched {} candidates for prefix {:02x?}; using newest",
6437                    candidates.len(),
6438                    &dest_hash[..4]
6439                );
6440            }
6441
6442            (candidates[0], &proof_data[..64])
6443        } else {
6444            log::debug!("Unsupported proof length: {} bytes", proof_data.len());
6445            return;
6446        };
6447
6448        // Look up the tracked sent packet
6449        if let Some((tracked_dest, sent_time)) = self.sent_packets.remove(&tracked_hash) {
6450            // Validate the proof signature using the destination's public key
6451            // (matches Python's PacketReceipt.validate_proof behavior)
6452            if let Some(announced) = self.known_destinations.get(&tracked_dest) {
6453                let identity =
6454                    rns_crypto::identity::Identity::from_public_key(&announced.public_key);
6455                let mut sig = [0u8; 64];
6456                sig.copy_from_slice(signature);
6457                if !identity.verify(&sig, &tracked_hash) {
6458                    log::debug!("Proof signature invalid for {:02x?}", &tracked_hash[..4],);
6459                    return;
6460                }
6461            } else {
6462                log::debug!(
6463                    "No known identity for dest {:02x?}, accepting proof without signature check",
6464                    &tracked_dest[..4],
6465                );
6466            }
6467
6468            let now = time::now();
6469            let rtt = now - sent_time;
6470            log::debug!(
6471                "Proof received for {:02x?} rtt={:.3}s",
6472                &tracked_hash[..4],
6473                rtt,
6474            );
6475            self.completed_proofs.insert(tracked_hash, (rtt, now));
6476            self.callbacks.on_proof(
6477                rns_core::types::DestHash(tracked_dest),
6478                rns_core::types::PacketHash(tracked_hash),
6479                rtt,
6480            );
6481        } else {
6482            log::debug!(
6483                "Proof for unknown packet {:02x?} on dest {:02x?}",
6484                &tracked_hash[..4],
6485                &dest_hash[..4],
6486            );
6487        }
6488    }
6489
6490    fn interface_send_deferred(entry: &InterfaceEntry, now: Instant) -> bool {
6491        matches!(entry.send_retry_at, Some(retry_at) if now < retry_at)
6492    }
6493
6494    fn record_send_result(
6495        entry: &mut InterfaceEntry,
6496        result: &std::io::Result<()>,
6497        context: &str,
6498        interface_id: InterfaceId,
6499    ) {
6500        match result {
6501            Ok(()) => {
6502                entry.send_retry_at = None;
6503                entry.send_retry_backoff = Duration::ZERO;
6504            }
6505            Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
6506                let next_backoff = if entry.send_retry_backoff.is_zero() {
6507                    SEND_RETRY_BACKOFF_MIN
6508                } else {
6509                    (entry.send_retry_backoff * 2).min(SEND_RETRY_BACKOFF_MAX)
6510                };
6511                entry.send_retry_backoff = next_backoff;
6512                entry.send_retry_at = Some(Instant::now() + next_backoff);
6513                log::debug!(
6514                    "[{}] {} deferred after WouldBlock; retry in {:?}",
6515                    interface_id.0,
6516                    context,
6517                    next_backoff
6518                );
6519            }
6520            Err(e) => {
6521                entry.send_retry_at = None;
6522                entry.send_retry_backoff = Duration::ZERO;
6523                log::warn!("[{}] {} failed: {}", interface_id.0, context, e);
6524            }
6525        }
6526    }
6527
6528    /// Dispatch a list of transport actions.
6529    fn dispatch_all(&mut self, actions: Vec<TransportAction>) {
6530        #[cfg(feature = "rns-hooks")]
6531        let mut hook_injected: Vec<TransportAction> = Vec::new();
6532
6533        for action in actions {
6534            match action {
6535                TransportAction::SendOnInterface { interface, raw } => {
6536                    #[cfg(feature = "rns-hooks")]
6537                    {
6538                        let pkt_ctx = rns_hooks::PacketContext {
6539                            flags: if raw.is_empty() { 0 } else { raw[0] },
6540                            hops: if raw.len() > 1 { raw[1] } else { 0 },
6541                            destination_hash: extract_dest_hash(&raw),
6542                            context: 0,
6543                            packet_hash: [0; 32],
6544                            interface_id: interface.0,
6545                            data_offset: 0,
6546                            data_len: raw.len() as u32,
6547                        };
6548                        let ctx = HookContext::Packet {
6549                            ctx: &pkt_ctx,
6550                            raw: &raw,
6551                        };
6552                        let now = time::now();
6553                        let engine_ref = EngineRef {
6554                            engine: &self.engine,
6555                            interfaces: &self.interfaces,
6556                            link_manager: &self.link_manager,
6557                            now,
6558                        };
6559                        let provider_events_enabled = self.provider_events_enabled();
6560                        {
6561                            let exec = run_hook_inner(
6562                                &mut self.hook_slots[HookPoint::SendOnInterface as usize].programs,
6563                                &self.hook_manager,
6564                                &engine_ref,
6565                                &ctx,
6566                                now,
6567                                provider_events_enabled,
6568                            );
6569                            if let Some(ref e) = exec {
6570                                self.collect_hook_side_effects(
6571                                    "SendOnInterface",
6572                                    e,
6573                                    &mut hook_injected,
6574                                );
6575                                if e.hook_result.as_ref().map_or(false, |r| r.is_drop()) {
6576                                    continue;
6577                                }
6578                            }
6579                        }
6580                    }
6581                    let is_announce = raw.len() > 2 && (raw[0] & 0x03) == 0x01;
6582                    if is_announce {
6583                        log::debug!(
6584                            "Announce:dispatching to iface {} (len={}, online={})",
6585                            interface.0,
6586                            raw.len(),
6587                            self.interfaces
6588                                .get(&interface)
6589                                .map(|e| e.online && e.enabled)
6590                                .unwrap_or(false)
6591                        );
6592                    }
6593                    if let Some(entry) = self.interfaces.get_mut(&interface) {
6594                        if entry.online && entry.enabled {
6595                            if Self::interface_send_deferred(entry, Instant::now()) {
6596                                continue;
6597                            }
6598                            let data = if let Some(ref ifac_state) = entry.ifac {
6599                                ifac::mask_outbound(&raw, ifac_state)
6600                            } else {
6601                                raw
6602                            };
6603                            // Update tx stats
6604                            entry.stats.txb += data.len() as u64;
6605                            entry.stats.tx_packets += 1;
6606                            if is_announce {
6607                                entry.stats.record_outgoing_announce(time::now());
6608                            }
6609                            let send_result = entry.writer.send_frame(&data);
6610                            let sent_ok = send_result.is_ok();
6611                            Self::record_send_result(entry, &send_result, "send", interface);
6612                            if sent_ok && is_announce {
6613                                // For HEADER_2 (transported), dest hash is at bytes 18-33
6614                                // For HEADER_1 (original), dest hash is at bytes 2-17
6615                                let header_type = (data[0] >> 6) & 0x03;
6616                                let dest_start = if header_type == 1 { 18usize } else { 2usize };
6617                                let dest_preview = if data.len() >= dest_start + 4 {
6618                                    format!("{:02x?}", &data[dest_start..dest_start + 4])
6619                                } else {
6620                                    "??".into()
6621                                };
6622                                log::debug!(
6623                                    "Announce:SENT on iface {} (len={}, h={}, dest=[{}])",
6624                                    interface.0,
6625                                    data.len(),
6626                                    header_type,
6627                                    dest_preview
6628                                );
6629                            }
6630                        }
6631                    }
6632                }
6633                TransportAction::BroadcastOnAllInterfaces { raw, exclude } => {
6634                    #[cfg(feature = "rns-hooks")]
6635                    {
6636                        let pkt_ctx = rns_hooks::PacketContext {
6637                            flags: if raw.is_empty() { 0 } else { raw[0] },
6638                            hops: if raw.len() > 1 { raw[1] } else { 0 },
6639                            destination_hash: extract_dest_hash(&raw),
6640                            context: 0,
6641                            packet_hash: [0; 32],
6642                            interface_id: 0,
6643                            data_offset: 0,
6644                            data_len: raw.len() as u32,
6645                        };
6646                        let ctx = HookContext::Packet {
6647                            ctx: &pkt_ctx,
6648                            raw: &raw,
6649                        };
6650                        let now = time::now();
6651                        let engine_ref = EngineRef {
6652                            engine: &self.engine,
6653                            interfaces: &self.interfaces,
6654                            link_manager: &self.link_manager,
6655                            now,
6656                        };
6657                        let provider_events_enabled = self.provider_events_enabled();
6658                        {
6659                            let exec = run_hook_inner(
6660                                &mut self.hook_slots[HookPoint::BroadcastOnAllInterfaces as usize]
6661                                    .programs,
6662                                &self.hook_manager,
6663                                &engine_ref,
6664                                &ctx,
6665                                now,
6666                                provider_events_enabled,
6667                            );
6668                            if let Some(ref e) = exec {
6669                                self.collect_hook_side_effects(
6670                                    "BroadcastOnAllInterfaces",
6671                                    e,
6672                                    &mut hook_injected,
6673                                );
6674                                if e.hook_result.as_ref().map_or(false, |r| r.is_drop()) {
6675                                    continue;
6676                                }
6677                            }
6678                        }
6679                    }
6680                    let is_announce = raw.len() > 2 && (raw[0] & 0x03) == 0x01;
6681                    for entry in self.interfaces.values_mut() {
6682                        if entry.online && entry.enabled && Some(entry.id) != exclude {
6683                            if Self::interface_send_deferred(entry, Instant::now()) {
6684                                continue;
6685                            }
6686                            let data = if let Some(ref ifac_state) = entry.ifac {
6687                                ifac::mask_outbound(&raw, ifac_state)
6688                            } else {
6689                                raw.clone()
6690                            };
6691                            // Update tx stats
6692                            entry.stats.txb += data.len() as u64;
6693                            entry.stats.tx_packets += 1;
6694                            if is_announce {
6695                                entry.stats.record_outgoing_announce(time::now());
6696                            }
6697                            let send_result = entry.writer.send_frame(&data);
6698                            Self::record_send_result(entry, &send_result, "broadcast", entry.id);
6699                        }
6700                    }
6701                }
6702                TransportAction::DeliverLocal {
6703                    destination_hash,
6704                    raw,
6705                    packet_hash,
6706                    receiving_interface,
6707                } => {
6708                    #[cfg(feature = "rns-hooks")]
6709                    {
6710                        let pkt_ctx = rns_hooks::PacketContext {
6711                            flags: 0,
6712                            hops: 0,
6713                            destination_hash,
6714                            context: 0,
6715                            packet_hash,
6716                            interface_id: receiving_interface.0,
6717                            data_offset: 0,
6718                            data_len: raw.len() as u32,
6719                        };
6720                        let ctx = HookContext::Packet {
6721                            ctx: &pkt_ctx,
6722                            raw: &raw,
6723                        };
6724                        let now = time::now();
6725                        let engine_ref = EngineRef {
6726                            engine: &self.engine,
6727                            interfaces: &self.interfaces,
6728                            link_manager: &self.link_manager,
6729                            now,
6730                        };
6731                        let provider_events_enabled = self.provider_events_enabled();
6732                        {
6733                            let exec = run_hook_inner(
6734                                &mut self.hook_slots[HookPoint::DeliverLocal as usize].programs,
6735                                &self.hook_manager,
6736                                &engine_ref,
6737                                &ctx,
6738                                now,
6739                                provider_events_enabled,
6740                            );
6741                            if let Some(ref e) = exec {
6742                                self.collect_hook_side_effects(
6743                                    "DeliverLocal",
6744                                    e,
6745                                    &mut hook_injected,
6746                                );
6747                                if e.hook_result.as_ref().map_or(false, |r| r.is_drop()) {
6748                                    continue;
6749                                }
6750                            }
6751                        }
6752                    }
6753                    if destination_hash == self.tunnel_synth_dest {
6754                        // Tunnel synthesis packet — validate and handle
6755                        self.handle_tunnel_synth_delivery(&raw);
6756                    } else if destination_hash == self.path_request_dest {
6757                        // Path request packet — extract data and handle
6758                        if let Ok(packet) = RawPacket::unpack(&raw) {
6759                            let actions = self.engine.handle_path_request(
6760                                &packet.data,
6761                                receiving_interface,
6762                                time::now(),
6763                            );
6764                            self.dispatch_all(actions);
6765                        }
6766                    } else if self.link_manager.is_link_destination(&destination_hash) {
6767                        // Link-related packet — route to link manager
6768                        let link_actions = self.link_manager.handle_local_delivery(
6769                            destination_hash,
6770                            &raw,
6771                            packet_hash,
6772                            receiving_interface,
6773                            &mut self.rng,
6774                        );
6775                        if link_actions.is_empty() {
6776                            // Link manager couldn't handle (e.g. opportunistic DATA
6777                            // for a registered link destination). Fall back to
6778                            // regular delivery.
6779                            if let Ok(packet) = RawPacket::unpack(&raw) {
6780                                if packet.flags.packet_type
6781                                    == rns_core::constants::PACKET_TYPE_PROOF
6782                                {
6783                                    self.handle_inbound_proof(
6784                                        destination_hash,
6785                                        &packet.data,
6786                                        &packet_hash,
6787                                    );
6788                                    continue;
6789                                }
6790                            }
6791                            self.maybe_generate_proof(destination_hash, &packet_hash);
6792                            self.callbacks.on_local_delivery(
6793                                rns_core::types::DestHash(destination_hash),
6794                                raw,
6795                                rns_core::types::PacketHash(packet_hash),
6796                            );
6797                        } else {
6798                            self.dispatch_link_actions(link_actions);
6799                        }
6800                    } else {
6801                        // Check if this is a PROOF packet for a packet we sent
6802                        if let Ok(packet) = RawPacket::unpack(&raw) {
6803                            if packet.flags.packet_type == rns_core::constants::PACKET_TYPE_PROOF {
6804                                self.handle_inbound_proof(
6805                                    destination_hash,
6806                                    &packet.data,
6807                                    &packet_hash,
6808                                );
6809                                continue;
6810                            }
6811                        }
6812
6813                        // Check if destination has a proof strategy — generate proof if needed
6814                        self.maybe_generate_proof(destination_hash, &packet_hash);
6815
6816                        self.callbacks.on_local_delivery(
6817                            rns_core::types::DestHash(destination_hash),
6818                            raw,
6819                            rns_core::types::PacketHash(packet_hash),
6820                        );
6821                    }
6822                }
6823                TransportAction::AnnounceReceived {
6824                    destination_hash,
6825                    identity_hash,
6826                    public_key,
6827                    name_hash,
6828                    app_data,
6829                    hops,
6830                    receiving_interface,
6831                    ..
6832                } => {
6833                    #[cfg(feature = "rns-hooks")]
6834                    {
6835                        let ctx = HookContext::Announce {
6836                            destination_hash,
6837                            hops,
6838                            interface_id: receiving_interface.0,
6839                        };
6840                        let now = time::now();
6841                        let engine_ref = EngineRef {
6842                            engine: &self.engine,
6843                            interfaces: &self.interfaces,
6844                            link_manager: &self.link_manager,
6845                            now,
6846                        };
6847                        let provider_events_enabled = self.provider_events_enabled();
6848                        {
6849                            let exec = run_hook_inner(
6850                                &mut self.hook_slots[HookPoint::AnnounceReceived as usize].programs,
6851                                &self.hook_manager,
6852                                &engine_ref,
6853                                &ctx,
6854                                now,
6855                                provider_events_enabled,
6856                            );
6857                            if let Some(ref e) = exec {
6858                                self.collect_hook_side_effects(
6859                                    "AnnounceReceived",
6860                                    e,
6861                                    &mut hook_injected,
6862                                );
6863                                if e.hook_result.as_ref().map_or(false, |r| r.is_drop()) {
6864                                    continue;
6865                                }
6866                            }
6867                        }
6868                    }
6869
6870                    // Check if this is a discovery announce (matched by name_hash
6871                    // since discovery is a SINGLE destination — its dest hash varies
6872                    // with the sender's identity).
6873                    if name_hash == self.discovery_name_hash {
6874                        if self.discover_interfaces {
6875                            if let Some(ref app_data) = app_data {
6876                                if let Some(mut discovered) =
6877                                    crate::discovery::parse_interface_announce(
6878                                        app_data,
6879                                        &identity_hash,
6880                                        hops,
6881                                        self.discovery_required_value,
6882                                    )
6883                                {
6884                                    // Check if we already have this interface
6885                                    if let Ok(Some(existing)) =
6886                                        self.discovered_interfaces.load(&discovered.discovery_hash)
6887                                    {
6888                                        discovered.discovered = existing.discovered;
6889                                        discovered.heard_count = existing.heard_count + 1;
6890                                    }
6891                                    if let Err(e) = self.discovered_interfaces.store(&discovered) {
6892                                        log::warn!("Failed to store discovered interface: {}", e);
6893                                    } else {
6894                                        log::debug!(
6895                                            "Discovered interface '{}' ({}) at {}:{} [stamp={}]",
6896                                            discovered.name,
6897                                            discovered.interface_type,
6898                                            discovered.reachable_on.as_deref().unwrap_or("?"),
6899                                            discovered
6900                                                .port
6901                                                .map(|p| p.to_string())
6902                                                .unwrap_or_else(|| "?".into()),
6903                                            discovered.stamp_value,
6904                                        );
6905                                    }
6906                                }
6907                            }
6908                        }
6909                        // Still cache the identity and notify callbacks
6910                    }
6911
6912                    // Cache the announced identity
6913                    let announced = crate::destination::AnnouncedIdentity {
6914                        dest_hash: rns_core::types::DestHash(destination_hash),
6915                        identity_hash: rns_core::types::IdentityHash(identity_hash),
6916                        public_key,
6917                        app_data: app_data.clone(),
6918                        hops,
6919                        received_at: time::now(),
6920                        receiving_interface,
6921                    };
6922                    self.upsert_known_destination(destination_hash, announced.clone());
6923                    log::info!(
6924                        "Announce:validated dest={:02x}{:02x}{:02x}{:02x}.. hops={}",
6925                        destination_hash[0],
6926                        destination_hash[1],
6927                        destination_hash[2],
6928                        destination_hash[3],
6929                        hops,
6930                    );
6931                    self.callbacks.on_announce(announced);
6932                }
6933                TransportAction::PathUpdated {
6934                    destination_hash,
6935                    hops,
6936                    interface,
6937                    ..
6938                } => {
6939                    #[cfg(feature = "rns-hooks")]
6940                    {
6941                        let ctx = HookContext::Announce {
6942                            destination_hash,
6943                            hops,
6944                            interface_id: interface.0,
6945                        };
6946                        let now = time::now();
6947                        let engine_ref = EngineRef {
6948                            engine: &self.engine,
6949                            interfaces: &self.interfaces,
6950                            link_manager: &self.link_manager,
6951                            now,
6952                        };
6953                        let provider_events_enabled = self.provider_events_enabled();
6954                        if let Some(ref e) = run_hook_inner(
6955                            &mut self.hook_slots[HookPoint::PathUpdated as usize].programs,
6956                            &self.hook_manager,
6957                            &engine_ref,
6958                            &ctx,
6959                            now,
6960                            provider_events_enabled,
6961                        ) {
6962                            self.collect_hook_side_effects("PathUpdated", e, &mut hook_injected);
6963                        }
6964                    }
6965                    #[cfg(not(feature = "rns-hooks"))]
6966                    let _ = interface;
6967
6968                    self.callbacks
6969                        .on_path_updated(rns_core::types::DestHash(destination_hash), hops);
6970                }
6971                TransportAction::ForwardToLocalClients { raw, exclude } => {
6972                    for entry in self.interfaces.values_mut() {
6973                        if entry.online
6974                            && entry.enabled
6975                            && entry.info.is_local_client
6976                            && Some(entry.id) != exclude
6977                        {
6978                            if Self::interface_send_deferred(entry, Instant::now()) {
6979                                continue;
6980                            }
6981                            let data = if let Some(ref ifac_state) = entry.ifac {
6982                                ifac::mask_outbound(&raw, ifac_state)
6983                            } else {
6984                                raw.clone()
6985                            };
6986                            entry.stats.txb += data.len() as u64;
6987                            entry.stats.tx_packets += 1;
6988                            let send_result = entry.writer.send_frame(&data);
6989                            Self::record_send_result(
6990                                entry,
6991                                &send_result,
6992                                "forward to local client",
6993                                entry.id,
6994                            );
6995                        }
6996                    }
6997                }
6998                TransportAction::ForwardPlainBroadcast {
6999                    raw,
7000                    to_local,
7001                    exclude,
7002                } => {
7003                    for entry in self.interfaces.values_mut() {
7004                        if entry.online
7005                            && entry.enabled
7006                            && entry.info.is_local_client == to_local
7007                            && Some(entry.id) != exclude
7008                        {
7009                            if Self::interface_send_deferred(entry, Instant::now()) {
7010                                continue;
7011                            }
7012                            let data = if let Some(ref ifac_state) = entry.ifac {
7013                                ifac::mask_outbound(&raw, ifac_state)
7014                            } else {
7015                                raw.clone()
7016                            };
7017                            entry.stats.txb += data.len() as u64;
7018                            entry.stats.tx_packets += 1;
7019                            let send_result = entry.writer.send_frame(&data);
7020                            Self::record_send_result(
7021                                entry,
7022                                &send_result,
7023                                "forward plain broadcast",
7024                                entry.id,
7025                            );
7026                        }
7027                    }
7028                }
7029                TransportAction::CacheAnnounce { packet_hash, raw } => {
7030                    if let Some(ref cache) = self.announce_cache {
7031                        if let Err(e) = cache.store(&packet_hash, &raw, None) {
7032                            log::warn!("Failed to cache announce: {}", e);
7033                        }
7034                    }
7035                }
7036                TransportAction::TunnelSynthesize {
7037                    interface,
7038                    data,
7039                    dest_hash,
7040                } => {
7041                    #[cfg(feature = "rns-hooks")]
7042                    {
7043                        let pkt_ctx = rns_hooks::PacketContext {
7044                            flags: 0,
7045                            hops: 0,
7046                            destination_hash: dest_hash,
7047                            context: 0,
7048                            packet_hash: [0; 32],
7049                            interface_id: interface.0,
7050                            data_offset: 0,
7051                            data_len: data.len() as u32,
7052                        };
7053                        let ctx = HookContext::Packet {
7054                            ctx: &pkt_ctx,
7055                            raw: &data,
7056                        };
7057                        let now = time::now();
7058                        let engine_ref = EngineRef {
7059                            engine: &self.engine,
7060                            interfaces: &self.interfaces,
7061                            link_manager: &self.link_manager,
7062                            now,
7063                        };
7064                        let provider_events_enabled = self.provider_events_enabled();
7065                        {
7066                            let exec = run_hook_inner(
7067                                &mut self.hook_slots[HookPoint::TunnelSynthesize as usize].programs,
7068                                &self.hook_manager,
7069                                &engine_ref,
7070                                &ctx,
7071                                now,
7072                                provider_events_enabled,
7073                            );
7074                            if let Some(ref e) = exec {
7075                                self.collect_hook_side_effects(
7076                                    "TunnelSynthesize",
7077                                    e,
7078                                    &mut hook_injected,
7079                                );
7080                                if e.hook_result.as_ref().map_or(false, |r| r.is_drop()) {
7081                                    continue;
7082                                }
7083                            }
7084                        }
7085                    }
7086                    // Pack as BROADCAST DATA PLAIN packet and send on interface
7087                    let flags = rns_core::packet::PacketFlags {
7088                        header_type: rns_core::constants::HEADER_1,
7089                        context_flag: rns_core::constants::FLAG_UNSET,
7090                        transport_type: rns_core::constants::TRANSPORT_BROADCAST,
7091                        destination_type: rns_core::constants::DESTINATION_PLAIN,
7092                        packet_type: rns_core::constants::PACKET_TYPE_DATA,
7093                    };
7094                    if let Ok(packet) = rns_core::packet::RawPacket::pack(
7095                        flags,
7096                        0,
7097                        &dest_hash,
7098                        None,
7099                        rns_core::constants::CONTEXT_NONE,
7100                        &data,
7101                    ) {
7102                        if let Some(entry) = self.interfaces.get_mut(&interface) {
7103                            if entry.online && entry.enabled {
7104                                let raw = if let Some(ref ifac_state) = entry.ifac {
7105                                    ifac::mask_outbound(&packet.raw, ifac_state)
7106                                } else {
7107                                    packet.raw
7108                                };
7109                                entry.stats.txb += raw.len() as u64;
7110                                entry.stats.tx_packets += 1;
7111                                if let Err(e) = entry.writer.send_frame(&raw) {
7112                                    log::warn!(
7113                                        "[{}] tunnel synthesize send failed: {}",
7114                                        entry.info.id.0,
7115                                        e
7116                                    );
7117                                }
7118                            }
7119                        }
7120                    }
7121                }
7122                TransportAction::TunnelEstablished {
7123                    tunnel_id,
7124                    interface,
7125                } => {
7126                    log::info!(
7127                        "Tunnel established: {:02x?} on interface {}",
7128                        &tunnel_id[..4],
7129                        interface.0
7130                    );
7131                }
7132                TransportAction::AnnounceRetransmit {
7133                    destination_hash,
7134                    hops,
7135                    interface,
7136                } => {
7137                    #[cfg(feature = "rns-hooks")]
7138                    {
7139                        let ctx = HookContext::Announce {
7140                            destination_hash,
7141                            hops,
7142                            interface_id: interface.map(|i| i.0).unwrap_or(0),
7143                        };
7144                        let now = time::now();
7145                        let engine_ref = EngineRef {
7146                            engine: &self.engine,
7147                            interfaces: &self.interfaces,
7148                            link_manager: &self.link_manager,
7149                            now,
7150                        };
7151                        let provider_events_enabled = self.provider_events_enabled();
7152                        if let Some(ref e) = run_hook_inner(
7153                            &mut self.hook_slots[HookPoint::AnnounceRetransmit as usize].programs,
7154                            &self.hook_manager,
7155                            &engine_ref,
7156                            &ctx,
7157                            now,
7158                            provider_events_enabled,
7159                        ) {
7160                            self.collect_hook_side_effects(
7161                                "AnnounceRetransmit",
7162                                e,
7163                                &mut hook_injected,
7164                            );
7165                        }
7166                    }
7167                    #[cfg(not(feature = "rns-hooks"))]
7168                    {
7169                        let _ = (destination_hash, hops, interface);
7170                    }
7171                }
7172                TransportAction::LinkRequestReceived {
7173                    link_id,
7174                    destination_hash: _,
7175                    receiving_interface,
7176                } => {
7177                    #[cfg(feature = "rns-hooks")]
7178                    {
7179                        let ctx = HookContext::Link {
7180                            link_id,
7181                            interface_id: receiving_interface.0,
7182                        };
7183                        let now = time::now();
7184                        let engine_ref = EngineRef {
7185                            engine: &self.engine,
7186                            interfaces: &self.interfaces,
7187                            link_manager: &self.link_manager,
7188                            now,
7189                        };
7190                        let provider_events_enabled = self.provider_events_enabled();
7191                        if let Some(ref e) = run_hook_inner(
7192                            &mut self.hook_slots[HookPoint::LinkRequestReceived as usize].programs,
7193                            &self.hook_manager,
7194                            &engine_ref,
7195                            &ctx,
7196                            now,
7197                            provider_events_enabled,
7198                        ) {
7199                            self.collect_hook_side_effects(
7200                                "LinkRequestReceived",
7201                                e,
7202                                &mut hook_injected,
7203                            );
7204                        }
7205                    }
7206                    #[cfg(not(feature = "rns-hooks"))]
7207                    {
7208                        let _ = (link_id, receiving_interface);
7209                    }
7210                }
7211                TransportAction::LinkEstablished { link_id, interface } => {
7212                    #[cfg(feature = "rns-hooks")]
7213                    {
7214                        let ctx = HookContext::Link {
7215                            link_id,
7216                            interface_id: interface.0,
7217                        };
7218                        let now = time::now();
7219                        let engine_ref = EngineRef {
7220                            engine: &self.engine,
7221                            interfaces: &self.interfaces,
7222                            link_manager: &self.link_manager,
7223                            now,
7224                        };
7225                        let provider_events_enabled = self.provider_events_enabled();
7226                        if let Some(ref e) = run_hook_inner(
7227                            &mut self.hook_slots[HookPoint::LinkEstablished as usize].programs,
7228                            &self.hook_manager,
7229                            &engine_ref,
7230                            &ctx,
7231                            now,
7232                            provider_events_enabled,
7233                        ) {
7234                            self.collect_hook_side_effects(
7235                                "LinkEstablished",
7236                                e,
7237                                &mut hook_injected,
7238                            );
7239                        }
7240                    }
7241                    #[cfg(not(feature = "rns-hooks"))]
7242                    {
7243                        let _ = (link_id, interface);
7244                    }
7245                }
7246                TransportAction::LinkClosed { link_id } => {
7247                    #[cfg(feature = "rns-hooks")]
7248                    {
7249                        let ctx = HookContext::Link {
7250                            link_id,
7251                            interface_id: 0,
7252                        };
7253                        let now = time::now();
7254                        let engine_ref = EngineRef {
7255                            engine: &self.engine,
7256                            interfaces: &self.interfaces,
7257                            link_manager: &self.link_manager,
7258                            now,
7259                        };
7260                        let provider_events_enabled = self.provider_events_enabled();
7261                        if let Some(ref e) = run_hook_inner(
7262                            &mut self.hook_slots[HookPoint::LinkClosed as usize].programs,
7263                            &self.hook_manager,
7264                            &engine_ref,
7265                            &ctx,
7266                            now,
7267                            provider_events_enabled,
7268                        ) {
7269                            self.collect_hook_side_effects("LinkClosed", e, &mut hook_injected);
7270                        }
7271                    }
7272                    #[cfg(not(feature = "rns-hooks"))]
7273                    {
7274                        let _ = link_id;
7275                    }
7276                }
7277            }
7278        }
7279
7280        // Dispatch any actions injected by hooks during action processing
7281        #[cfg(feature = "rns-hooks")]
7282        if !hook_injected.is_empty() {
7283            self.dispatch_all(hook_injected);
7284        }
7285    }
7286
7287    /// Dispatch link manager actions.
7288    fn dispatch_link_actions(&mut self, actions: Vec<LinkManagerAction>) {
7289        #[cfg(feature = "rns-hooks")]
7290        let mut hook_injected: Vec<TransportAction> = Vec::new();
7291
7292        for action in actions {
7293            match action {
7294                LinkManagerAction::SendPacket {
7295                    mut raw,
7296                    dest_type,
7297                    mut attached_interface,
7298                } => {
7299                    if dest_type == rns_core::constants::DESTINATION_LINK
7300                        && attached_interface.is_none()
7301                    {
7302                        if let Ok(packet) = RawPacket::unpack(&raw) {
7303                            let link_id = packet.destination_hash;
7304                            if let Some((iface, transport_id)) =
7305                                self.link_manager.get_link_route_hint(&link_id)
7306                            {
7307                                attached_interface = Some(iface);
7308                                if packet.flags.header_type == rns_core::constants::HEADER_1 {
7309                                    if let Some(next_hop) = transport_id {
7310                                        raw = inject_transport_header(&packet.raw, &next_hop);
7311                                        log::debug!(
7312                                            "Link SendPacket rewrite: link={:02x?} iface={} header=1->2 tid={:02x?}",
7313                                            &link_id[..4],
7314                                            iface.0,
7315                                            &next_hop[..4]
7316                                        );
7317                                    } else {
7318                                        log::debug!(
7319                                            "Link SendPacket route: link={:02x?} iface={} header=1 (no transport_id)",
7320                                            &link_id[..4],
7321                                            iface.0
7322                                        );
7323                                    }
7324                                }
7325                            } else {
7326                                log::debug!(
7327                                    "Link SendPacket no route hint: link={:02x?}",
7328                                    &link_id[..4]
7329                                );
7330                            }
7331                        }
7332                    }
7333
7334                    // Route through the transport engine's outbound path
7335                    match RawPacket::unpack(&raw) {
7336                        Ok(packet) => {
7337                            if packet.flags.packet_type == rns_core::constants::PACKET_TYPE_DATA {
7338                                self.sent_packets.insert(
7339                                    packet.packet_hash,
7340                                    (packet.destination_hash, time::now()),
7341                                );
7342                            }
7343                            let transport_actions = self.engine.handle_outbound(
7344                                &packet,
7345                                dest_type,
7346                                attached_interface,
7347                                time::now(),
7348                            );
7349                            self.dispatch_all(transport_actions);
7350                        }
7351                        Err(e) => {
7352                            log::warn!("LinkManager SendPacket: failed to unpack: {:?}", e);
7353                        }
7354                    }
7355                }
7356                LinkManagerAction::LinkEstablished {
7357                    link_id,
7358                    dest_hash,
7359                    rtt,
7360                    is_initiator,
7361                } => {
7362                    #[cfg(feature = "rns-hooks")]
7363                    {
7364                        let ctx = HookContext::Link {
7365                            link_id,
7366                            interface_id: 0,
7367                        };
7368                        let now = time::now();
7369                        let engine_ref = EngineRef {
7370                            engine: &self.engine,
7371                            interfaces: &self.interfaces,
7372                            link_manager: &self.link_manager,
7373                            now,
7374                        };
7375                        let provider_events_enabled = self.provider_events_enabled();
7376                        if let Some(ref e) = run_hook_inner(
7377                            &mut self.hook_slots[HookPoint::LinkEstablished as usize].programs,
7378                            &self.hook_manager,
7379                            &engine_ref,
7380                            &ctx,
7381                            now,
7382                            provider_events_enabled,
7383                        ) {
7384                            self.collect_hook_side_effects(
7385                                "LinkEstablished",
7386                                e,
7387                                &mut hook_injected,
7388                            );
7389                        }
7390                    }
7391                    log::info!(
7392                        "Link established: {:02x?} rtt={:.3}s initiator={}",
7393                        &link_id[..4],
7394                        rtt,
7395                        is_initiator,
7396                    );
7397                    self.callbacks.on_link_established(
7398                        rns_core::types::LinkId(link_id),
7399                        rns_core::types::DestHash(dest_hash),
7400                        rtt,
7401                        is_initiator,
7402                    );
7403                }
7404                LinkManagerAction::LinkClosed { link_id, reason } => {
7405                    #[cfg(feature = "rns-hooks")]
7406                    {
7407                        let ctx = HookContext::Link {
7408                            link_id,
7409                            interface_id: 0,
7410                        };
7411                        let now = time::now();
7412                        let engine_ref = EngineRef {
7413                            engine: &self.engine,
7414                            interfaces: &self.interfaces,
7415                            link_manager: &self.link_manager,
7416                            now,
7417                        };
7418                        let provider_events_enabled = self.provider_events_enabled();
7419                        if let Some(ref e) = run_hook_inner(
7420                            &mut self.hook_slots[HookPoint::LinkClosed as usize].programs,
7421                            &self.hook_manager,
7422                            &engine_ref,
7423                            &ctx,
7424                            now,
7425                            provider_events_enabled,
7426                        ) {
7427                            self.collect_hook_side_effects("LinkClosed", e, &mut hook_injected);
7428                        }
7429                    }
7430                    log::info!("Link closed: {:02x?} reason={:?}", &link_id[..4], reason);
7431                    self.holepunch_manager.link_closed(&link_id);
7432                    self.callbacks
7433                        .on_link_closed(rns_core::types::LinkId(link_id), reason);
7434                }
7435                LinkManagerAction::RemoteIdentified {
7436                    link_id,
7437                    identity_hash,
7438                    public_key,
7439                } => {
7440                    log::debug!(
7441                        "Remote identified on link {:02x?}: {:02x?}",
7442                        &link_id[..4],
7443                        &identity_hash[..4],
7444                    );
7445                    self.callbacks.on_remote_identified(
7446                        rns_core::types::LinkId(link_id),
7447                        rns_core::types::IdentityHash(identity_hash),
7448                        public_key,
7449                    );
7450                }
7451                LinkManagerAction::RegisterLinkDest { link_id } => {
7452                    // Register the link_id as a LINK destination in the transport engine
7453                    self.engine
7454                        .register_destination(link_id, rns_core::constants::DESTINATION_LINK);
7455                }
7456                LinkManagerAction::DeregisterLinkDest { link_id } => {
7457                    self.engine.deregister_destination(&link_id);
7458                }
7459                LinkManagerAction::ManagementRequest {
7460                    link_id,
7461                    path_hash,
7462                    data,
7463                    request_id,
7464                    remote_identity,
7465                } => {
7466                    self.handle_management_request(
7467                        link_id,
7468                        path_hash,
7469                        data,
7470                        request_id,
7471                        remote_identity,
7472                    );
7473                }
7474                LinkManagerAction::ResourceReceived {
7475                    link_id,
7476                    data,
7477                    metadata,
7478                } => {
7479                    self.callbacks.on_resource_received(
7480                        rns_core::types::LinkId(link_id),
7481                        data,
7482                        metadata,
7483                    );
7484                }
7485                LinkManagerAction::ResourceCompleted { link_id } => {
7486                    self.callbacks
7487                        .on_resource_completed(rns_core::types::LinkId(link_id));
7488                }
7489                LinkManagerAction::ResourceFailed { link_id, error } => {
7490                    log::debug!("Resource failed on link {:02x?}: {}", &link_id[..4], error);
7491                    self.callbacks
7492                        .on_resource_failed(rns_core::types::LinkId(link_id), error);
7493                }
7494                LinkManagerAction::ResourceProgress {
7495                    link_id,
7496                    received,
7497                    total,
7498                } => {
7499                    self.callbacks.on_resource_progress(
7500                        rns_core::types::LinkId(link_id),
7501                        received,
7502                        total,
7503                    );
7504                }
7505                LinkManagerAction::ResourceAcceptQuery {
7506                    link_id,
7507                    resource_hash,
7508                    transfer_size,
7509                    has_metadata,
7510                } => {
7511                    let accept = self.callbacks.on_resource_accept_query(
7512                        rns_core::types::LinkId(link_id),
7513                        resource_hash.clone(),
7514                        transfer_size,
7515                        has_metadata,
7516                    );
7517                    let accept_actions = self.link_manager.accept_resource(
7518                        &link_id,
7519                        &resource_hash,
7520                        accept,
7521                        &mut self.rng,
7522                    );
7523                    // Re-dispatch (recursive but bounded: accept_resource won't produce more AcceptQuery)
7524                    self.dispatch_link_actions(accept_actions);
7525                }
7526                LinkManagerAction::ChannelMessageReceived {
7527                    link_id,
7528                    msgtype,
7529                    payload,
7530                } => {
7531                    // Intercept hole-punch signaling messages (0xFE00..=0xFE04)
7532                    if HolePunchManager::is_holepunch_message(msgtype) {
7533                        let derived_key = self.link_manager.get_derived_key(&link_id);
7534                        let tx = self.get_event_sender();
7535                        let (handled, hp_actions) = self.holepunch_manager.handle_signal(
7536                            link_id,
7537                            msgtype,
7538                            payload,
7539                            derived_key.as_deref(),
7540                            &tx,
7541                        );
7542                        if handled {
7543                            self.dispatch_holepunch_actions(hp_actions);
7544                        }
7545                    } else {
7546                        self.callbacks.on_channel_message(
7547                            rns_core::types::LinkId(link_id),
7548                            msgtype,
7549                            payload,
7550                        );
7551                    }
7552                }
7553                LinkManagerAction::LinkDataReceived {
7554                    link_id,
7555                    context,
7556                    data,
7557                } => {
7558                    self.callbacks
7559                        .on_link_data(rns_core::types::LinkId(link_id), context, data);
7560                }
7561                LinkManagerAction::ResponseReceived {
7562                    link_id,
7563                    request_id,
7564                    data,
7565                } => {
7566                    self.callbacks
7567                        .on_response(rns_core::types::LinkId(link_id), request_id, data);
7568                }
7569                LinkManagerAction::LinkRequestReceived {
7570                    link_id,
7571                    receiving_interface,
7572                } => {
7573                    #[cfg(feature = "rns-hooks")]
7574                    {
7575                        let ctx = HookContext::Link {
7576                            link_id,
7577                            interface_id: receiving_interface.0,
7578                        };
7579                        let now = time::now();
7580                        let engine_ref = EngineRef {
7581                            engine: &self.engine,
7582                            interfaces: &self.interfaces,
7583                            link_manager: &self.link_manager,
7584                            now,
7585                        };
7586                        let provider_events_enabled = self.provider_events_enabled();
7587                        if let Some(ref e) = run_hook_inner(
7588                            &mut self.hook_slots[HookPoint::LinkRequestReceived as usize].programs,
7589                            &self.hook_manager,
7590                            &engine_ref,
7591                            &ctx,
7592                            now,
7593                            provider_events_enabled,
7594                        ) {
7595                            self.collect_hook_side_effects(
7596                                "LinkRequestReceived",
7597                                e,
7598                                &mut hook_injected,
7599                            );
7600                        }
7601                    }
7602                    #[cfg(not(feature = "rns-hooks"))]
7603                    {
7604                        let _ = (link_id, receiving_interface);
7605                    }
7606                }
7607            }
7608        }
7609
7610        // Dispatch any actions injected by hooks during action processing
7611        #[cfg(feature = "rns-hooks")]
7612        if !hook_injected.is_empty() {
7613            self.dispatch_all(hook_injected);
7614        }
7615    }
7616
7617    /// Dispatch hole-punch manager actions.
7618    fn dispatch_holepunch_actions(&mut self, actions: Vec<HolePunchManagerAction>) {
7619        for action in actions {
7620            match action {
7621                HolePunchManagerAction::SendChannelMessage {
7622                    link_id,
7623                    msgtype,
7624                    payload,
7625                } => {
7626                    if let Ok(link_actions) = self.link_manager.send_channel_message(
7627                        &link_id,
7628                        msgtype,
7629                        &payload,
7630                        &mut self.rng,
7631                    ) {
7632                        self.dispatch_link_actions(link_actions);
7633                    }
7634                }
7635                HolePunchManagerAction::DirectConnectEstablished {
7636                    link_id,
7637                    session_id,
7638                    interface_id,
7639                    rtt,
7640                    mtu,
7641                } => {
7642                    log::info!(
7643                        "Direct connection established for link {:02x?} session {:02x?} iface {} rtt={:.1}ms mtu={}",
7644                        &link_id[..4], &session_id[..4], interface_id.0, rtt * 1000.0, mtu
7645                    );
7646                    // Redirect the link's path to use the direct interface
7647                    self.engine
7648                        .redirect_path(&link_id, interface_id, time::now());
7649                    // Update the link's RTT and MTU to reflect the direct path
7650                    self.link_manager.set_link_rtt(&link_id, rtt);
7651                    self.link_manager.set_link_mtu(&link_id, mtu);
7652                    // Reset inbound timer — set_rtt shortens the keepalive/stale
7653                    // intervals, so without this the link goes stale immediately
7654                    self.link_manager.record_link_inbound(&link_id);
7655                    // Flush holepunch signaling messages from the channel window
7656                    self.link_manager.flush_channel_tx(&link_id);
7657                    self.callbacks.on_direct_connect_established(
7658                        rns_core::types::LinkId(link_id),
7659                        interface_id,
7660                    );
7661                }
7662                HolePunchManagerAction::DirectConnectFailed {
7663                    link_id,
7664                    session_id,
7665                    reason,
7666                } => {
7667                    log::debug!(
7668                        "Direct connection failed for link {:02x?} session {:02x?} reason={}",
7669                        &link_id[..4],
7670                        &session_id[..4],
7671                        reason
7672                    );
7673                    self.callbacks
7674                        .on_direct_connect_failed(rns_core::types::LinkId(link_id), reason);
7675                }
7676            }
7677        }
7678    }
7679
7680    /// Get an event sender for worker threads to send results back to the driver.
7681    ///
7682    /// This is a bit of a workaround since the driver owns the receiver.
7683    /// We store a clone of the sender when the driver is created.
7684    fn get_event_sender(&self) -> crate::event::EventSender {
7685        // The driver doesn't directly have a sender, but node.rs creates the channel
7686        // and passes rx to the driver. We need to store a sender clone.
7687        // For now we use an internal sender that was set during construction.
7688        self.event_tx.clone()
7689    }
7690
7691    /// Delay before first management announce after startup.
7692    const MANAGEMENT_ANNOUNCE_DELAY: f64 = 5.0;
7693
7694    /// Tick the discovery announcer: start stamp generation if due, send announce if ready.
7695    fn tick_discovery_announcer(&mut self, now: f64) {
7696        let announcer = match self.interface_announcer.as_mut() {
7697            Some(a) => a,
7698            None => return,
7699        };
7700
7701        announcer.maybe_start(now);
7702
7703        let stamp_result = match announcer.poll_ready() {
7704            Some(r) => r,
7705            None => return,
7706        };
7707
7708        if !announcer.contains_interface(&stamp_result.interface_name) {
7709            log::debug!(
7710                "Discovery: dropping completed stamp for removed interface '{}'",
7711                stamp_result.interface_name
7712            );
7713            return;
7714        }
7715
7716        let identity = match self.transport_identity.as_ref() {
7717            Some(id) => id,
7718            None => {
7719                log::warn!("Discovery: stamp ready but no transport identity");
7720                return;
7721            }
7722        };
7723
7724        // Discovery is a SINGLE destination — the dest hash includes the transport identity
7725        let identity_hash = identity.hash();
7726        let disc_dest = rns_core::destination::destination_hash(
7727            crate::discovery::APP_NAME,
7728            &["discovery", "interface"],
7729            Some(&identity_hash),
7730        );
7731        let name_hash = self.discovery_name_hash;
7732        let mut random_hash = [0u8; 10];
7733        self.rng.fill_bytes(&mut random_hash);
7734
7735        let (announce_data, _) = match rns_core::announce::AnnounceData::pack(
7736            identity,
7737            &disc_dest,
7738            &name_hash,
7739            &random_hash,
7740            None,
7741            Some(&stamp_result.app_data),
7742        ) {
7743            Ok(v) => v,
7744            Err(e) => {
7745                log::warn!("Discovery: failed to pack announce: {}", e);
7746                return;
7747            }
7748        };
7749
7750        let flags = rns_core::packet::PacketFlags {
7751            header_type: rns_core::constants::HEADER_1,
7752            context_flag: rns_core::constants::FLAG_UNSET,
7753            transport_type: rns_core::constants::TRANSPORT_BROADCAST,
7754            destination_type: rns_core::constants::DESTINATION_SINGLE,
7755            packet_type: rns_core::constants::PACKET_TYPE_ANNOUNCE,
7756        };
7757
7758        let packet = match RawPacket::pack(
7759            flags,
7760            0,
7761            &disc_dest,
7762            None,
7763            rns_core::constants::CONTEXT_NONE,
7764            &announce_data,
7765        ) {
7766            Ok(p) => p,
7767            Err(e) => {
7768                log::warn!("Discovery: failed to pack packet: {}", e);
7769                return;
7770            }
7771        };
7772
7773        let outbound_actions = self.engine.handle_outbound(
7774            &packet,
7775            rns_core::constants::DESTINATION_SINGLE,
7776            None,
7777            now,
7778        );
7779        log::debug!(
7780            "Discovery announce sent for interface '{}' ({} actions, dest={:02x?})",
7781            stamp_result.interface_name,
7782            outbound_actions.len(),
7783            &disc_dest[..4],
7784        );
7785        self.dispatch_all(outbound_actions);
7786    }
7787
7788    /// Read RSS from /proc/self/statm (Linux only).
7789    fn rss_mb() -> Option<f64> {
7790        let statm = std::fs::read_to_string("/proc/self/statm").ok()?;
7791        let rss_pages: u64 = statm.split_whitespace().nth(1)?.parse().ok()?;
7792        Some(rss_pages as f64 * 4096.0 / (1024.0 * 1024.0))
7793    }
7794
7795    fn parse_proc_kib(contents: &str, key: &str) -> Option<u64> {
7796        contents.lines().find_map(|line| {
7797            let value = line.strip_prefix(key)?;
7798            value.split_whitespace().next()?.parse().ok()
7799        })
7800    }
7801
7802    fn proc_status_mb() -> Option<(f64, f64, f64, f64)> {
7803        let status = std::fs::read_to_string("/proc/self/status").ok()?;
7804        let vm_rss = Self::parse_proc_kib(&status, "VmRSS:")? as f64 / 1024.0;
7805        let vm_hwm = Self::parse_proc_kib(&status, "VmHWM:")? as f64 / 1024.0;
7806        let vm_data = Self::parse_proc_kib(&status, "VmData:")? as f64 / 1024.0;
7807        let vm_swap = Self::parse_proc_kib(&status, "VmSwap:").unwrap_or(0) as f64 / 1024.0;
7808        Some((vm_rss, vm_hwm, vm_data, vm_swap))
7809    }
7810
7811    fn smaps_rollup_mb() -> Option<(f64, f64, f64, f64, f64, f64, f64, f64)> {
7812        let smaps = std::fs::read_to_string("/proc/self/smaps_rollup").ok()?;
7813        let rss_kib = Self::parse_proc_kib(&smaps, "Rss:")?;
7814        let anon_kib = Self::parse_proc_kib(&smaps, "Anonymous:")?;
7815        let shared_clean_kib = Self::parse_proc_kib(&smaps, "Shared_Clean:").unwrap_or(0);
7816        let shared_dirty_kib = Self::parse_proc_kib(&smaps, "Shared_Dirty:").unwrap_or(0);
7817        let private_clean_kib = Self::parse_proc_kib(&smaps, "Private_Clean:").unwrap_or(0);
7818        let private_dirty_kib = Self::parse_proc_kib(&smaps, "Private_Dirty:").unwrap_or(0);
7819        let swap_kib = Self::parse_proc_kib(&smaps, "Swap:").unwrap_or(0);
7820        let file_est_kib = rss_kib.saturating_sub(anon_kib);
7821        Some((
7822            rss_kib as f64 / 1024.0,
7823            anon_kib as f64 / 1024.0,
7824            file_est_kib as f64 / 1024.0,
7825            shared_clean_kib as f64 / 1024.0,
7826            shared_dirty_kib as f64 / 1024.0,
7827            private_clean_kib as f64 / 1024.0,
7828            private_dirty_kib as f64 / 1024.0,
7829            swap_kib as f64 / 1024.0,
7830        ))
7831    }
7832
7833    /// Log sizes of all major collections for memory growth diagnostics.
7834    fn log_memory_stats(&self) {
7835        let rss = Self::rss_mb()
7836            .map(|v| format!("{:.1}", v))
7837            .unwrap_or_else(|| "N/A".into());
7838        let (vm_rss, vm_hwm, vm_data, vm_swap) = Self::proc_status_mb()
7839            .map(|(rss, hwm, data, swap)| {
7840                (
7841                    format!("{rss:.1}"),
7842                    format!("{hwm:.1}"),
7843                    format!("{data:.1}"),
7844                    format!("{swap:.1}"),
7845                )
7846            })
7847            .unwrap_or_else(|| ("N/A".into(), "N/A".into(), "N/A".into(), "N/A".into()));
7848        let (
7849            smaps_rss,
7850            smaps_anon,
7851            smaps_file_est,
7852            smaps_shared_clean,
7853            smaps_shared_dirty,
7854            smaps_private_clean,
7855            smaps_private_dirty,
7856            smaps_swap,
7857        ) = Self::smaps_rollup_mb()
7858            .map(
7859                |(
7860                    rss,
7861                    anon,
7862                    file_est,
7863                    shared_clean,
7864                    shared_dirty,
7865                    private_clean,
7866                    private_dirty,
7867                    swap,
7868                )| {
7869                    (
7870                        format!("{rss:.1}"),
7871                        format!("{anon:.1}"),
7872                        format!("{file_est:.1}"),
7873                        format!("{shared_clean:.1}"),
7874                        format!("{shared_dirty:.1}"),
7875                        format!("{private_clean:.1}"),
7876                        format!("{private_dirty:.1}"),
7877                        format!("{swap:.1}"),
7878                    )
7879                },
7880            )
7881            .unwrap_or_else(|| {
7882                (
7883                    "N/A".into(),
7884                    "N/A".into(),
7885                    "N/A".into(),
7886                    "N/A".into(),
7887                    "N/A".into(),
7888                    "N/A".into(),
7889                    "N/A".into(),
7890                    "N/A".into(),
7891                )
7892            });
7893        log::info!(
7894            "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={}",
7895            rss,
7896            vm_rss,
7897            vm_hwm,
7898            vm_data,
7899            vm_swap,
7900            smaps_rss,
7901            smaps_anon,
7902            smaps_file_est,
7903            smaps_shared_clean,
7904            smaps_shared_dirty,
7905            smaps_private_clean,
7906            smaps_private_dirty,
7907            smaps_swap,
7908            self.known_destinations.len(),
7909            self.known_destinations_cap_evict_count,
7910            self.engine.path_table_count(),
7911            self.engine.path_destination_cap_evict_count(),
7912            self.engine.announce_table_count(),
7913            self.engine.reverse_table_count(),
7914            self.engine.link_table_count(),
7915            self.engine.held_announces_count(),
7916            self.engine.packet_hashlist_len(),
7917            self.engine.announce_sig_cache_len(),
7918            self.announce_verify_queue
7919                .lock()
7920                .map(|queue| queue.len())
7921                .unwrap_or(0),
7922            self.engine.rate_limiter_count(),
7923            self.engine.blackholed_count(),
7924            self.engine.tunnel_count(),
7925            self.engine.announce_queue_count(),
7926            self.engine.nonempty_announce_queue_count(),
7927            self.engine.queued_announce_count(),
7928            self.engine.queued_announce_bytes(),
7929            self.engine.announce_queue_interface_cap_drop_count(),
7930            self.engine.discovery_pr_tags_count(),
7931            self.engine.discovery_path_requests_count(),
7932            self.sent_packets.len(),
7933            self.completed_proofs.len(),
7934            self.local_destinations.len(),
7935            self.shared_announces.len(),
7936            self.link_manager.link_count(),
7937            self.holepunch_manager.session_count(),
7938            self.proof_strategies.len(),
7939        );
7940    }
7941
7942    /// Emit management and/or blackhole announces if enabled and due.
7943    fn tick_management_announces(&mut self, now: f64) {
7944        if self.transport_identity.is_none() {
7945            return;
7946        }
7947
7948        let uptime = now - self.started;
7949
7950        // Wait for initial delay
7951        if !self.initial_announce_sent {
7952            if uptime < Self::MANAGEMENT_ANNOUNCE_DELAY {
7953                return;
7954            }
7955            self.initial_announce_sent = true;
7956            self.emit_management_announces(now);
7957            return;
7958        }
7959
7960        // Periodic re-announce
7961        if now - self.last_management_announce >= self.management_announce_interval_secs {
7962            self.emit_management_announces(now);
7963        }
7964    }
7965
7966    /// Emit management/blackhole announce packets through the engine outbound path.
7967    fn emit_management_announces(&mut self, now: f64) {
7968        use crate::management;
7969
7970        self.last_management_announce = now;
7971
7972        let identity = match self.transport_identity {
7973            Some(ref id) => id,
7974            None => return,
7975        };
7976
7977        // Build announce packets first (immutable borrow of identity), then dispatch
7978        let mgmt_raw = if self.management_config.enable_remote_management {
7979            management::build_management_announce(identity, &mut self.rng)
7980        } else {
7981            None
7982        };
7983
7984        let bh_raw = if self.management_config.publish_blackhole {
7985            management::build_blackhole_announce(identity, &mut self.rng)
7986        } else {
7987            None
7988        };
7989
7990        let probe_raw = if self.probe_responder_hash.is_some() {
7991            management::build_probe_announce(identity, &mut self.rng)
7992        } else {
7993            None
7994        };
7995
7996        if let Some(raw) = mgmt_raw {
7997            if let Ok(packet) = RawPacket::unpack(&raw) {
7998                let actions = self.engine.handle_outbound(
7999                    &packet,
8000                    rns_core::constants::DESTINATION_SINGLE,
8001                    None,
8002                    now,
8003                );
8004                self.dispatch_all(actions);
8005                log::debug!("Emitted management destination announce");
8006            }
8007        }
8008
8009        if let Some(raw) = bh_raw {
8010            if let Ok(packet) = RawPacket::unpack(&raw) {
8011                let actions = self.engine.handle_outbound(
8012                    &packet,
8013                    rns_core::constants::DESTINATION_SINGLE,
8014                    None,
8015                    now,
8016                );
8017                self.dispatch_all(actions);
8018                log::debug!("Emitted blackhole info announce");
8019            }
8020        }
8021
8022        if let Some(raw) = probe_raw {
8023            if let Ok(packet) = RawPacket::unpack(&raw) {
8024                let actions = self.engine.handle_outbound(
8025                    &packet,
8026                    rns_core::constants::DESTINATION_SINGLE,
8027                    None,
8028                    now,
8029                );
8030                self.dispatch_all(actions);
8031                log::debug!("Emitted probe responder announce");
8032            }
8033        }
8034    }
8035
8036    /// Handle a management request by querying engine state and sending a response.
8037    fn handle_management_request(
8038        &mut self,
8039        link_id: [u8; 16],
8040        path_hash: [u8; 16],
8041        data: Vec<u8>,
8042        request_id: [u8; 16],
8043        remote_identity: Option<([u8; 16], [u8; 64])>,
8044    ) {
8045        use crate::management;
8046
8047        // ACL check for /status and /path (ALLOW_LIST), /list is ALLOW_ALL
8048        let is_restricted = path_hash == management::status_path_hash()
8049            || path_hash == management::path_path_hash();
8050
8051        if is_restricted && !self.management_config.remote_management_allowed.is_empty() {
8052            match remote_identity {
8053                Some((identity_hash, _)) => {
8054                    if !self
8055                        .management_config
8056                        .remote_management_allowed
8057                        .contains(&identity_hash)
8058                    {
8059                        log::debug!("Management request denied: identity not in allowed list");
8060                        return;
8061                    }
8062                }
8063                None => {
8064                    log::debug!("Management request denied: peer not identified");
8065                    return;
8066                }
8067            }
8068        }
8069
8070        let response_data = if path_hash == management::status_path_hash() {
8071            {
8072                let views: Vec<&dyn management::InterfaceStatusView> = self
8073                    .interfaces
8074                    .values()
8075                    .map(|e| e as &dyn management::InterfaceStatusView)
8076                    .collect();
8077                management::handle_status_request(
8078                    &data,
8079                    &self.engine,
8080                    &views,
8081                    self.started,
8082                    self.probe_responder_hash,
8083                )
8084            }
8085        } else if path_hash == management::path_path_hash() {
8086            management::handle_path_request(&data, &self.engine)
8087        } else if path_hash == management::list_path_hash() {
8088            management::handle_blackhole_list_request(&self.engine)
8089        } else {
8090            log::warn!("Unknown management path_hash: {:02x?}", &path_hash[..4]);
8091            None
8092        };
8093
8094        if let Some(response) = response_data {
8095            let actions = self.link_manager.send_management_response(
8096                &link_id,
8097                &request_id,
8098                &response,
8099                &mut self.rng,
8100            );
8101            self.dispatch_link_actions(actions);
8102        }
8103    }
8104}
8105
8106#[cfg(test)]
8107mod tests {
8108    use super::*;
8109    use crate::event;
8110    use crate::interface::Writer;
8111    use rns_core::announce::AnnounceData;
8112    use rns_core::constants;
8113    use rns_core::packet::PacketFlags;
8114    use rns_core::transport::types::InterfaceInfo;
8115    use rns_crypto::identity::Identity;
8116    use std::io;
8117    use std::sync::mpsc;
8118    use std::sync::{Arc, Mutex};
8119    use std::thread;
8120    use std::time::{Duration, Instant};
8121
8122    struct MockWriter {
8123        sent: Arc<Mutex<Vec<Vec<u8>>>>,
8124    }
8125
8126    impl MockWriter {
8127        fn new() -> (Self, Arc<Mutex<Vec<Vec<u8>>>>) {
8128            let sent = Arc::new(Mutex::new(Vec::new()));
8129            (MockWriter { sent: sent.clone() }, sent)
8130        }
8131    }
8132
8133    impl Writer for MockWriter {
8134        fn send_frame(&mut self, data: &[u8]) -> io::Result<()> {
8135            self.sent.lock().unwrap().push(data.to_vec());
8136            Ok(())
8137        }
8138    }
8139
8140    struct BlockingWriter {
8141        entered_tx: std::sync::mpsc::Sender<()>,
8142        release_rx: std::sync::mpsc::Receiver<()>,
8143    }
8144
8145    impl Writer for BlockingWriter {
8146        fn send_frame(&mut self, _data: &[u8]) -> io::Result<()> {
8147            let _ = self.entered_tx.send(());
8148            let _ = self.release_rx.recv();
8149            Ok(())
8150        }
8151    }
8152
8153    struct WouldBlockWriter {
8154        attempts: Arc<Mutex<usize>>,
8155    }
8156
8157    impl WouldBlockWriter {
8158        fn new() -> (Self, Arc<Mutex<usize>>) {
8159            let attempts = Arc::new(Mutex::new(0));
8160            (
8161                WouldBlockWriter {
8162                    attempts: attempts.clone(),
8163                },
8164                attempts,
8165            )
8166        }
8167    }
8168
8169    impl Writer for WouldBlockWriter {
8170        fn send_frame(&mut self, _data: &[u8]) -> io::Result<()> {
8171            *self.attempts.lock().unwrap() += 1;
8172            Err(io::Error::new(
8173                io::ErrorKind::WouldBlock,
8174                "intentional stall",
8175            ))
8176        }
8177    }
8178
8179    fn wait_for_sent_len(sent: &Arc<Mutex<Vec<Vec<u8>>>>, expected: usize) {
8180        let deadline = Instant::now() + Duration::from_millis(200);
8181        while Instant::now() < deadline {
8182            if sent.lock().unwrap().len() == expected {
8183                return;
8184            }
8185            thread::sleep(Duration::from_millis(5));
8186        }
8187        assert_eq!(sent.lock().unwrap().len(), expected);
8188    }
8189
8190    use rns_core::types::{DestHash, IdentityHash, LinkId as TypedLinkId, PacketHash};
8191
8192    struct MockCallbacks {
8193        announces: Arc<Mutex<Vec<(DestHash, u8)>>>,
8194        paths: Arc<Mutex<Vec<(DestHash, u8)>>>,
8195        deliveries: Arc<Mutex<Vec<DestHash>>>,
8196        iface_ups: Arc<Mutex<Vec<InterfaceId>>>,
8197        iface_downs: Arc<Mutex<Vec<InterfaceId>>>,
8198        link_established: Arc<Mutex<Vec<(TypedLinkId, f64, bool)>>>,
8199        link_closed: Arc<Mutex<Vec<TypedLinkId>>>,
8200        remote_identified: Arc<Mutex<Vec<(TypedLinkId, IdentityHash)>>>,
8201        resources_received: Arc<Mutex<Vec<(TypedLinkId, Vec<u8>)>>>,
8202        resource_completed: Arc<Mutex<Vec<TypedLinkId>>>,
8203        resource_failed: Arc<Mutex<Vec<(TypedLinkId, String)>>>,
8204        channel_messages: Arc<Mutex<Vec<(TypedLinkId, u16, Vec<u8>)>>>,
8205        link_data: Arc<Mutex<Vec<(TypedLinkId, u8, Vec<u8>)>>>,
8206        responses: Arc<Mutex<Vec<(TypedLinkId, [u8; 16], Vec<u8>)>>>,
8207        proofs: Arc<Mutex<Vec<(DestHash, PacketHash, f64)>>>,
8208        proof_requested: Arc<Mutex<Vec<(DestHash, PacketHash)>>>,
8209    }
8210
8211    impl MockCallbacks {
8212        fn new() -> (
8213            Self,
8214            Arc<Mutex<Vec<(DestHash, u8)>>>,
8215            Arc<Mutex<Vec<(DestHash, u8)>>>,
8216            Arc<Mutex<Vec<DestHash>>>,
8217            Arc<Mutex<Vec<InterfaceId>>>,
8218            Arc<Mutex<Vec<InterfaceId>>>,
8219        ) {
8220            let announces = Arc::new(Mutex::new(Vec::new()));
8221            let paths = Arc::new(Mutex::new(Vec::new()));
8222            let deliveries = Arc::new(Mutex::new(Vec::new()));
8223            let iface_ups = Arc::new(Mutex::new(Vec::new()));
8224            let iface_downs = Arc::new(Mutex::new(Vec::new()));
8225            (
8226                MockCallbacks {
8227                    announces: announces.clone(),
8228                    paths: paths.clone(),
8229                    deliveries: deliveries.clone(),
8230                    iface_ups: iface_ups.clone(),
8231                    iface_downs: iface_downs.clone(),
8232                    link_established: Arc::new(Mutex::new(Vec::new())),
8233                    link_closed: Arc::new(Mutex::new(Vec::new())),
8234                    remote_identified: Arc::new(Mutex::new(Vec::new())),
8235                    resources_received: Arc::new(Mutex::new(Vec::new())),
8236                    resource_completed: Arc::new(Mutex::new(Vec::new())),
8237                    resource_failed: Arc::new(Mutex::new(Vec::new())),
8238                    channel_messages: Arc::new(Mutex::new(Vec::new())),
8239                    link_data: Arc::new(Mutex::new(Vec::new())),
8240                    responses: Arc::new(Mutex::new(Vec::new())),
8241                    proofs: Arc::new(Mutex::new(Vec::new())),
8242                    proof_requested: Arc::new(Mutex::new(Vec::new())),
8243                },
8244                announces,
8245                paths,
8246                deliveries,
8247                iface_ups,
8248                iface_downs,
8249            )
8250        }
8251
8252        fn with_link_tracking() -> (
8253            Self,
8254            Arc<Mutex<Vec<(TypedLinkId, f64, bool)>>>,
8255            Arc<Mutex<Vec<TypedLinkId>>>,
8256            Arc<Mutex<Vec<(TypedLinkId, IdentityHash)>>>,
8257        ) {
8258            let link_established = Arc::new(Mutex::new(Vec::new()));
8259            let link_closed = Arc::new(Mutex::new(Vec::new()));
8260            let remote_identified = Arc::new(Mutex::new(Vec::new()));
8261            (
8262                MockCallbacks {
8263                    announces: Arc::new(Mutex::new(Vec::new())),
8264                    paths: Arc::new(Mutex::new(Vec::new())),
8265                    deliveries: Arc::new(Mutex::new(Vec::new())),
8266                    iface_ups: Arc::new(Mutex::new(Vec::new())),
8267                    iface_downs: Arc::new(Mutex::new(Vec::new())),
8268                    link_established: link_established.clone(),
8269                    link_closed: link_closed.clone(),
8270                    remote_identified: remote_identified.clone(),
8271                    resources_received: Arc::new(Mutex::new(Vec::new())),
8272                    resource_completed: Arc::new(Mutex::new(Vec::new())),
8273                    resource_failed: Arc::new(Mutex::new(Vec::new())),
8274                    channel_messages: Arc::new(Mutex::new(Vec::new())),
8275                    link_data: Arc::new(Mutex::new(Vec::new())),
8276                    responses: Arc::new(Mutex::new(Vec::new())),
8277                    proofs: Arc::new(Mutex::new(Vec::new())),
8278                    proof_requested: Arc::new(Mutex::new(Vec::new())),
8279                },
8280                link_established,
8281                link_closed,
8282                remote_identified,
8283            )
8284        }
8285    }
8286
8287    fn new_test_driver() -> Driver {
8288        let transport_config = TransportConfig {
8289            transport_enabled: false,
8290            identity_hash: None,
8291            prefer_shorter_path: false,
8292            max_paths_per_destination: 1,
8293            packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
8294            max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
8295            max_path_destinations: usize::MAX,
8296            max_tunnel_destinations_total: usize::MAX,
8297            destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
8298            announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
8299            announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
8300            announce_sig_cache_enabled: true,
8301            announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
8302            announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
8303            announce_queue_max_entries: 256,
8304            announce_queue_max_interfaces: 1024,
8305        };
8306        let (callbacks, _, _, _, _, _) = MockCallbacks::new();
8307        let (tx, rx) = event::channel();
8308        let mut driver = Driver::new(transport_config, rx, tx, Box::new(callbacks));
8309        driver.set_tick_interval_handle(Arc::new(AtomicU64::new(1000)));
8310        driver
8311    }
8312
8313    fn make_announced_identity(
8314        dest_hash: [u8; 16],
8315        received_at: f64,
8316        receiving_interface: InterfaceId,
8317    ) -> crate::destination::AnnouncedIdentity {
8318        crate::destination::AnnouncedIdentity {
8319            dest_hash: rns_core::types::DestHash(dest_hash),
8320            identity_hash: rns_core::types::IdentityHash([dest_hash[0]; 16]),
8321            public_key: [dest_hash[0]; 64],
8322            app_data: None,
8323            hops: 1,
8324            received_at,
8325            receiving_interface,
8326        }
8327    }
8328
8329    #[cfg(feature = "iface-backbone")]
8330    fn register_test_backbone(driver: &mut Driver, name: &str) {
8331        let startup = BackboneServerRuntime {
8332            max_connections: Some(8),
8333            idle_timeout: Some(Duration::from_secs(10)),
8334            write_stall_timeout: Some(Duration::from_secs(30)),
8335            abuse: BackboneAbuseConfig {
8336                max_penalty_duration: Some(Duration::from_secs(3600)),
8337            },
8338        };
8339        let peer_state = Arc::new(std::sync::Mutex::new(
8340            crate::interface::backbone::BackbonePeerMonitor::new(),
8341        ));
8342        driver.register_backbone_runtime(BackboneRuntimeConfigHandle {
8343            interface_name: name.to_string(),
8344            runtime: Arc::new(std::sync::Mutex::new(startup.clone())),
8345            startup,
8346        });
8347        driver.register_backbone_peer_state(BackbonePeerStateHandle {
8348            interface_id: InterfaceId(1),
8349            interface_name: name.to_string(),
8350            peer_state,
8351        });
8352    }
8353
8354    #[cfg(feature = "iface-backbone")]
8355    fn register_test_backbone_client(driver: &mut Driver, name: &str) {
8356        let startup = BackboneClientRuntime {
8357            reconnect_wait: Duration::from_secs(5),
8358            max_reconnect_tries: Some(3),
8359            connect_timeout: Duration::from_secs(5),
8360        };
8361        driver.register_backbone_client_runtime(BackboneClientRuntimeConfigHandle {
8362            interface_name: name.to_string(),
8363            runtime: Arc::new(std::sync::Mutex::new(startup.clone())),
8364            startup,
8365        });
8366    }
8367
8368    #[cfg(feature = "iface-backbone")]
8369    fn register_test_backbone_discovery(driver: &mut Driver, name: &str, discoverable: bool) {
8370        let startup = BackboneDiscoveryRuntime {
8371            discoverable,
8372            config: crate::discovery::DiscoveryConfig {
8373                discovery_name: name.to_string(),
8374                announce_interval: 3600,
8375                stamp_value: crate::discovery::DEFAULT_STAMP_VALUE,
8376                reachable_on: None,
8377                interface_type: "BackboneInterface".to_string(),
8378                listen_port: Some(4242),
8379                latitude: None,
8380                longitude: None,
8381                height: None,
8382            },
8383            transport_enabled: true,
8384            ifac_netname: None,
8385            ifac_netkey: None,
8386        };
8387        driver.register_backbone_discovery_runtime(BackboneDiscoveryRuntimeHandle {
8388            interface_name: name.to_string(),
8389            current: startup.clone(),
8390            startup,
8391        });
8392    }
8393
8394    #[cfg(feature = "iface-tcp")]
8395    fn register_test_tcp_server(driver: &mut Driver, name: &str) {
8396        let startup = TcpServerRuntime {
8397            max_connections: Some(4),
8398        };
8399        driver.register_tcp_server_runtime(TcpServerRuntimeConfigHandle {
8400            interface_name: name.to_string(),
8401            runtime: Arc::new(std::sync::Mutex::new(startup.clone())),
8402            startup,
8403        });
8404    }
8405
8406    #[cfg(feature = "iface-tcp")]
8407    fn register_test_tcp_server_discovery(driver: &mut Driver, name: &str, discoverable: bool) {
8408        let startup = TcpServerDiscoveryRuntime {
8409            discoverable,
8410            config: crate::discovery::DiscoveryConfig {
8411                discovery_name: name.to_string(),
8412                announce_interval: 3600,
8413                stamp_value: crate::discovery::DEFAULT_STAMP_VALUE,
8414                reachable_on: None,
8415                interface_type: "TCPServerInterface".to_string(),
8416                listen_port: Some(4242),
8417                latitude: None,
8418                longitude: None,
8419                height: None,
8420            },
8421            transport_enabled: true,
8422            ifac_netname: None,
8423            ifac_netkey: None,
8424        };
8425        driver.register_tcp_server_discovery_runtime(TcpServerDiscoveryRuntimeHandle {
8426            interface_name: name.to_string(),
8427            current: startup.clone(),
8428            startup,
8429        });
8430    }
8431
8432    #[cfg(feature = "iface-tcp")]
8433    fn register_test_tcp_client(driver: &mut Driver, name: &str) {
8434        let startup = crate::interface::tcp::TcpClientRuntime {
8435            target_host: "127.0.0.1".into(),
8436            target_port: 4242,
8437            reconnect_wait: Duration::from_secs(5),
8438            max_reconnect_tries: Some(3),
8439            connect_timeout: Duration::from_secs(5),
8440        };
8441        driver.register_tcp_client_runtime(crate::interface::tcp::TcpClientRuntimeConfigHandle {
8442            interface_name: name.to_string(),
8443            runtime: Arc::new(std::sync::Mutex::new(startup.clone())),
8444            startup,
8445        });
8446    }
8447
8448    #[cfg(feature = "iface-udp")]
8449    fn register_test_udp(driver: &mut Driver, name: &str) {
8450        let startup = UdpRuntime {
8451            forward_ip: Some("127.0.0.1".into()),
8452            forward_port: Some(4242),
8453        };
8454        driver.register_udp_runtime(UdpRuntimeConfigHandle {
8455            interface_name: name.to_string(),
8456            runtime: Arc::new(std::sync::Mutex::new(startup.clone())),
8457            startup,
8458        });
8459    }
8460
8461    fn register_test_generic_interface(driver: &mut Driver, id: u64, name: &str) {
8462        let mut info = make_interface_info(id);
8463        info.name = name.to_string();
8464        info.mode = rns_core::constants::MODE_FULL;
8465        info.announce_rate_target = Some(1.5);
8466        info.announce_rate_grace = 2;
8467        info.announce_rate_penalty = 0.25;
8468        info.announce_cap = 0.05;
8469        info.ingress_control.enabled = true;
8470        driver.register_interface_runtime_defaults(&info);
8471        driver.register_interface_ifac_runtime(
8472            &info.name,
8473            IfacRuntimeConfig {
8474                netname: None,
8475                netkey: None,
8476                size: 16,
8477            },
8478        );
8479        driver.engine.register_interface(info.clone());
8480        let (writer, _) = MockWriter::new();
8481        driver.interfaces.insert(
8482            InterfaceId(id),
8483            InterfaceEntry {
8484                id: InterfaceId(id),
8485                info,
8486                writer: Box::new(writer),
8487                async_writer_metrics: None,
8488                enabled: true,
8489                online: true,
8490                dynamic: false,
8491                ifac: None,
8492                stats: InterfaceStats {
8493                    started: time::now(),
8494                    ..Default::default()
8495                },
8496                interface_type: "TestInterface".to_string(),
8497                send_retry_at: None,
8498                send_retry_backoff: Duration::ZERO,
8499            },
8500        );
8501    }
8502
8503    #[cfg(feature = "iface-auto")]
8504    fn register_test_auto(driver: &mut Driver, name: &str) {
8505        let startup = AutoRuntime {
8506            announce_interval_secs: 1.6,
8507            peer_timeout_secs: 22.0,
8508            peer_job_interval_secs: 4.0,
8509        };
8510        driver.register_auto_runtime(AutoRuntimeConfigHandle {
8511            interface_name: name.to_string(),
8512            runtime: Arc::new(std::sync::Mutex::new(startup.clone())),
8513            startup,
8514        });
8515    }
8516
8517    #[cfg(feature = "iface-i2p")]
8518    fn register_test_i2p(driver: &mut Driver, name: &str) {
8519        let startup = I2pRuntime {
8520            reconnect_wait: Duration::from_secs(15),
8521        };
8522        driver.register_i2p_runtime(I2pRuntimeConfigHandle {
8523            interface_name: name.to_string(),
8524            runtime: Arc::new(std::sync::Mutex::new(startup.clone())),
8525            startup,
8526        });
8527    }
8528
8529    #[cfg(feature = "iface-pipe")]
8530    fn register_test_pipe(driver: &mut Driver, name: &str) {
8531        let startup = PipeRuntime {
8532            respawn_delay: Duration::from_secs(5),
8533        };
8534        driver.register_pipe_runtime(PipeRuntimeConfigHandle {
8535            interface_name: name.to_string(),
8536            runtime: Arc::new(std::sync::Mutex::new(startup.clone())),
8537            startup,
8538        });
8539    }
8540
8541    #[cfg(feature = "iface-rnode")]
8542    fn register_test_rnode(driver: &mut Driver, name: &str) {
8543        let startup = RNodeRuntime {
8544            sub: RNodeSubConfig {
8545                name: name.to_string(),
8546                frequency: 868_000_000,
8547                bandwidth: 125_000,
8548                txpower: 7,
8549                spreading_factor: 8,
8550                coding_rate: 5,
8551                flow_control: false,
8552                st_alock: None,
8553                lt_alock: None,
8554            },
8555            writer: None,
8556        };
8557        driver.register_rnode_runtime(RNodeRuntimeConfigHandle {
8558            interface_name: name.to_string(),
8559            runtime: Arc::new(std::sync::Mutex::new(startup.clone())),
8560            startup,
8561        });
8562    }
8563
8564    impl Callbacks for MockCallbacks {
8565        fn on_announce(&mut self, announced: crate::destination::AnnouncedIdentity) {
8566            self.announces
8567                .lock()
8568                .unwrap()
8569                .push((announced.dest_hash, announced.hops));
8570        }
8571
8572        fn on_path_updated(&mut self, dest_hash: DestHash, hops: u8) {
8573            self.paths.lock().unwrap().push((dest_hash, hops));
8574        }
8575
8576        fn on_local_delivery(
8577            &mut self,
8578            dest_hash: DestHash,
8579            _raw: Vec<u8>,
8580            _packet_hash: PacketHash,
8581        ) {
8582            self.deliveries.lock().unwrap().push(dest_hash);
8583        }
8584
8585        fn on_interface_up(&mut self, id: InterfaceId) {
8586            self.iface_ups.lock().unwrap().push(id);
8587        }
8588
8589        fn on_interface_down(&mut self, id: InterfaceId) {
8590            self.iface_downs.lock().unwrap().push(id);
8591        }
8592
8593        fn on_link_established(
8594            &mut self,
8595            link_id: TypedLinkId,
8596            _dest_hash: DestHash,
8597            rtt: f64,
8598            is_initiator: bool,
8599        ) {
8600            self.link_established
8601                .lock()
8602                .unwrap()
8603                .push((link_id, rtt, is_initiator));
8604        }
8605
8606        fn on_link_closed(
8607            &mut self,
8608            link_id: TypedLinkId,
8609            _reason: Option<rns_core::link::TeardownReason>,
8610        ) {
8611            self.link_closed.lock().unwrap().push(link_id);
8612        }
8613
8614        fn on_remote_identified(
8615            &mut self,
8616            link_id: TypedLinkId,
8617            identity_hash: IdentityHash,
8618            _public_key: [u8; 64],
8619        ) {
8620            self.remote_identified
8621                .lock()
8622                .unwrap()
8623                .push((link_id, identity_hash));
8624        }
8625
8626        fn on_resource_received(
8627            &mut self,
8628            link_id: TypedLinkId,
8629            data: Vec<u8>,
8630            _metadata: Option<Vec<u8>>,
8631        ) {
8632            self.resources_received
8633                .lock()
8634                .unwrap()
8635                .push((link_id, data));
8636        }
8637
8638        fn on_resource_completed(&mut self, link_id: TypedLinkId) {
8639            self.resource_completed.lock().unwrap().push(link_id);
8640        }
8641
8642        fn on_resource_failed(&mut self, link_id: TypedLinkId, error: String) {
8643            self.resource_failed.lock().unwrap().push((link_id, error));
8644        }
8645
8646        fn on_channel_message(&mut self, link_id: TypedLinkId, msgtype: u16, payload: Vec<u8>) {
8647            self.channel_messages
8648                .lock()
8649                .unwrap()
8650                .push((link_id, msgtype, payload));
8651        }
8652
8653        fn on_link_data(&mut self, link_id: TypedLinkId, context: u8, data: Vec<u8>) {
8654            self.link_data
8655                .lock()
8656                .unwrap()
8657                .push((link_id, context, data));
8658        }
8659
8660        fn on_response(&mut self, link_id: TypedLinkId, request_id: [u8; 16], data: Vec<u8>) {
8661            self.responses
8662                .lock()
8663                .unwrap()
8664                .push((link_id, request_id, data));
8665        }
8666
8667        fn on_proof(&mut self, dest_hash: DestHash, packet_hash: PacketHash, rtt: f64) {
8668            self.proofs
8669                .lock()
8670                .unwrap()
8671                .push((dest_hash, packet_hash, rtt));
8672        }
8673
8674        fn on_proof_requested(&mut self, dest_hash: DestHash, packet_hash: PacketHash) -> bool {
8675            self.proof_requested
8676                .lock()
8677                .unwrap()
8678                .push((dest_hash, packet_hash));
8679            true
8680        }
8681    }
8682
8683    fn make_interface_info(id: u64) -> InterfaceInfo {
8684        InterfaceInfo {
8685            id: InterfaceId(id),
8686            name: format!("test-{}", id),
8687            mode: constants::MODE_FULL,
8688            out_capable: true,
8689            in_capable: true,
8690            bitrate: None,
8691            announce_rate_target: None,
8692            announce_rate_grace: 0,
8693            announce_rate_penalty: 0.0,
8694            announce_cap: rns_core::constants::ANNOUNCE_CAP,
8695            is_local_client: false,
8696            wants_tunnel: false,
8697            tunnel_id: None,
8698            mtu: constants::MTU as u32,
8699            ia_freq: 0.0,
8700            started: 0.0,
8701            ingress_control: rns_core::transport::types::IngressControlConfig::disabled(),
8702        }
8703    }
8704
8705    fn make_entry(id: u64, writer: Box<dyn Writer>, online: bool) -> InterfaceEntry {
8706        InterfaceEntry {
8707            id: InterfaceId(id),
8708            info: make_interface_info(id),
8709            writer,
8710            async_writer_metrics: None,
8711            enabled: true,
8712            online,
8713            dynamic: false,
8714            ifac: None,
8715            stats: InterfaceStats::default(),
8716            interface_type: String::new(),
8717            send_retry_at: None,
8718            send_retry_backoff: Duration::ZERO,
8719        }
8720    }
8721
8722    /// Build a valid announce packet that the engine will accept.
8723    fn build_announce_packet(identity: &Identity) -> Vec<u8> {
8724        let dest_hash =
8725            rns_core::destination::destination_hash("test", &["app"], Some(identity.hash()));
8726        let name_hash = rns_core::destination::name_hash("test", &["app"]);
8727        let random_hash = [0x42u8; 10];
8728
8729        let (announce_data, _has_ratchet) =
8730            AnnounceData::pack(identity, &dest_hash, &name_hash, &random_hash, None, None).unwrap();
8731
8732        let flags = PacketFlags {
8733            header_type: constants::HEADER_1,
8734            context_flag: constants::FLAG_UNSET,
8735            transport_type: constants::TRANSPORT_BROADCAST,
8736            destination_type: constants::DESTINATION_SINGLE,
8737            packet_type: constants::PACKET_TYPE_ANNOUNCE,
8738        };
8739
8740        let packet = RawPacket::pack(
8741            flags,
8742            0,
8743            &dest_hash,
8744            None,
8745            constants::CONTEXT_NONE,
8746            &announce_data,
8747        )
8748        .unwrap();
8749        packet.raw
8750    }
8751
8752    #[test]
8753    fn process_inbound_frame() {
8754        let (tx, rx) = event::channel();
8755        let (cbs, announces, _, _, _, _) = MockCallbacks::new();
8756        let mut driver = Driver::new(
8757            TransportConfig {
8758                transport_enabled: false,
8759                identity_hash: None,
8760                prefer_shorter_path: false,
8761                max_paths_per_destination: 1,
8762                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
8763                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
8764                max_path_destinations: usize::MAX,
8765                max_tunnel_destinations_total: usize::MAX,
8766                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
8767                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
8768                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
8769                announce_sig_cache_enabled: true,
8770                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
8771                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
8772                announce_queue_max_entries: 256,
8773                announce_queue_max_interfaces: 1024,
8774            },
8775            rx,
8776            tx.clone(),
8777            Box::new(cbs),
8778        );
8779        let info = make_interface_info(1);
8780        driver.engine.register_interface(info.clone());
8781        let (writer, _sent) = MockWriter::new();
8782        driver
8783            .interfaces
8784            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
8785
8786        let identity = Identity::new(&mut OsRng);
8787        let announce_raw = build_announce_packet(&identity);
8788
8789        // Send frame then shutdown
8790        tx.send(Event::Frame {
8791            interface_id: InterfaceId(1),
8792            data: announce_raw,
8793        })
8794        .unwrap();
8795        tx.send(Event::Shutdown).unwrap();
8796        driver.run();
8797
8798        assert_eq!(announces.lock().unwrap().len(), 1);
8799    }
8800
8801    #[test]
8802    fn dispatch_send() {
8803        let (tx, rx) = event::channel();
8804        let (cbs, _, _, _, _, _) = MockCallbacks::new();
8805        let mut driver = Driver::new(
8806            TransportConfig {
8807                transport_enabled: false,
8808                identity_hash: None,
8809                prefer_shorter_path: false,
8810                max_paths_per_destination: 1,
8811                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
8812                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
8813                max_path_destinations: usize::MAX,
8814                max_tunnel_destinations_total: usize::MAX,
8815                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
8816                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
8817                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
8818                announce_sig_cache_enabled: true,
8819                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
8820                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
8821                announce_queue_max_entries: 256,
8822                announce_queue_max_interfaces: 1024,
8823            },
8824            rx,
8825            tx.clone(),
8826            Box::new(cbs),
8827        );
8828        let (writer, sent) = MockWriter::new();
8829        driver
8830            .interfaces
8831            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
8832
8833        driver.dispatch_all(vec![TransportAction::SendOnInterface {
8834            interface: InterfaceId(1),
8835            raw: vec![0x01, 0x02, 0x03],
8836        }]);
8837
8838        assert_eq!(sent.lock().unwrap().len(), 1);
8839        assert_eq!(sent.lock().unwrap()[0], vec![0x01, 0x02, 0x03]);
8840
8841        drop(tx);
8842    }
8843
8844    #[test]
8845    fn dispatch_broadcast() {
8846        let (tx, rx) = event::channel();
8847        let (cbs, _, _, _, _, _) = MockCallbacks::new();
8848        let mut driver = Driver::new(
8849            TransportConfig {
8850                transport_enabled: false,
8851                identity_hash: None,
8852                prefer_shorter_path: false,
8853                max_paths_per_destination: 1,
8854                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
8855                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
8856                max_path_destinations: usize::MAX,
8857                max_tunnel_destinations_total: usize::MAX,
8858                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
8859                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
8860                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
8861                announce_sig_cache_enabled: true,
8862                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
8863                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
8864                announce_queue_max_entries: 256,
8865                announce_queue_max_interfaces: 1024,
8866            },
8867            rx,
8868            tx.clone(),
8869            Box::new(cbs),
8870        );
8871
8872        let (w1, sent1) = MockWriter::new();
8873        let (w2, sent2) = MockWriter::new();
8874        driver
8875            .interfaces
8876            .insert(InterfaceId(1), make_entry(1, Box::new(w1), true));
8877        driver
8878            .interfaces
8879            .insert(InterfaceId(2), make_entry(2, Box::new(w2), true));
8880
8881        driver.dispatch_all(vec![TransportAction::BroadcastOnAllInterfaces {
8882            raw: vec![0xAA],
8883            exclude: None,
8884        }]);
8885
8886        assert_eq!(sent1.lock().unwrap().len(), 1);
8887        assert_eq!(sent2.lock().unwrap().len(), 1);
8888
8889        drop(tx);
8890    }
8891
8892    #[test]
8893    fn dispatch_broadcast_exclude() {
8894        let (tx, rx) = event::channel();
8895        let (cbs, _, _, _, _, _) = MockCallbacks::new();
8896        let mut driver = Driver::new(
8897            TransportConfig {
8898                transport_enabled: false,
8899                identity_hash: None,
8900                prefer_shorter_path: false,
8901                max_paths_per_destination: 1,
8902                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
8903                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
8904                max_path_destinations: usize::MAX,
8905                max_tunnel_destinations_total: usize::MAX,
8906                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
8907                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
8908                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
8909                announce_sig_cache_enabled: true,
8910                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
8911                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
8912                announce_queue_max_entries: 256,
8913                announce_queue_max_interfaces: 1024,
8914            },
8915            rx,
8916            tx.clone(),
8917            Box::new(cbs),
8918        );
8919
8920        let (w1, sent1) = MockWriter::new();
8921        let (w2, sent2) = MockWriter::new();
8922        driver
8923            .interfaces
8924            .insert(InterfaceId(1), make_entry(1, Box::new(w1), true));
8925        driver
8926            .interfaces
8927            .insert(InterfaceId(2), make_entry(2, Box::new(w2), true));
8928
8929        driver.dispatch_all(vec![TransportAction::BroadcastOnAllInterfaces {
8930            raw: vec![0xBB],
8931            exclude: Some(InterfaceId(1)),
8932        }]);
8933
8934        assert_eq!(sent1.lock().unwrap().len(), 0); // excluded
8935        assert_eq!(sent2.lock().unwrap().len(), 1);
8936
8937        drop(tx);
8938    }
8939
8940    #[test]
8941    fn tick_event() {
8942        let (tx, rx) = event::channel();
8943        let (cbs, _, _, _, _, _) = MockCallbacks::new();
8944        let mut driver = Driver::new(
8945            TransportConfig {
8946                transport_enabled: true,
8947                identity_hash: Some([0x42; 16]),
8948                prefer_shorter_path: false,
8949                max_paths_per_destination: 1,
8950                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
8951                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
8952                max_path_destinations: usize::MAX,
8953                max_tunnel_destinations_total: usize::MAX,
8954                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
8955                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
8956                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
8957                announce_sig_cache_enabled: true,
8958                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
8959                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
8960                announce_queue_max_entries: 256,
8961                announce_queue_max_interfaces: 1024,
8962            },
8963            rx,
8964            tx.clone(),
8965            Box::new(cbs),
8966        );
8967        let info = make_interface_info(1);
8968        driver.engine.register_interface(info.clone());
8969        let (writer, _sent) = MockWriter::new();
8970        driver
8971            .interfaces
8972            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
8973
8974        // Send Tick then Shutdown
8975        tx.send(Event::Tick).unwrap();
8976        tx.send(Event::Shutdown).unwrap();
8977        driver.run();
8978        // No crash = tick was processed successfully
8979    }
8980
8981    #[test]
8982    fn shutdown_event() {
8983        let (tx, rx) = event::channel();
8984        let (cbs, _, _, _, _, _) = MockCallbacks::new();
8985        let mut driver = Driver::new(
8986            TransportConfig {
8987                transport_enabled: false,
8988                identity_hash: None,
8989                prefer_shorter_path: false,
8990                max_paths_per_destination: 1,
8991                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
8992                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
8993                max_path_destinations: usize::MAX,
8994                max_tunnel_destinations_total: usize::MAX,
8995                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
8996                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
8997                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
8998                announce_sig_cache_enabled: true,
8999                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9000                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9001                announce_queue_max_entries: 256,
9002                announce_queue_max_interfaces: 1024,
9003            },
9004            rx,
9005            tx.clone(),
9006            Box::new(cbs),
9007        );
9008
9009        tx.send(Event::Shutdown).unwrap();
9010        driver.run(); // Should return immediately
9011    }
9012
9013    #[test]
9014    fn begin_drain_updates_driver_status() {
9015        let (tx, rx) = event::channel();
9016        let (cbs, _, _, _, _, _) = MockCallbacks::new();
9017        let mut driver = Driver::new(
9018            TransportConfig {
9019                transport_enabled: false,
9020                identity_hash: None,
9021                prefer_shorter_path: false,
9022                max_paths_per_destination: 1,
9023                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9024                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9025                max_path_destinations: usize::MAX,
9026                max_tunnel_destinations_total: usize::MAX,
9027                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9028                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9029                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9030                announce_sig_cache_enabled: true,
9031                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9032                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9033                announce_queue_max_entries: 256,
9034                announce_queue_max_interfaces: 1024,
9035            },
9036            rx,
9037            tx,
9038            Box::new(cbs),
9039        );
9040
9041        driver.begin_drain(Duration::from_secs(3));
9042
9043        let QueryResponse::DrainStatus(status) = driver.handle_query(QueryRequest::DrainStatus)
9044        else {
9045            panic!("expected drain status response");
9046        };
9047        assert_eq!(status.state, LifecycleState::Draining);
9048        assert!(status.drain_complete);
9049        assert!(status.drain_age_seconds.is_some());
9050        assert!(status.deadline_remaining_seconds.is_some());
9051        assert_eq!(
9052            status.detail.as_deref(),
9053            Some("node is draining existing work; no active links, resource transfers, hole-punch sessions, or queued writer/provider work remain")
9054        );
9055    }
9056
9057    #[test]
9058    fn begin_drain_with_pending_link_reports_incomplete_status() {
9059        let (tx, rx) = event::channel();
9060        let (cbs, _, _, _, _, _) = MockCallbacks::new();
9061        let mut driver = Driver::new(
9062            TransportConfig {
9063                transport_enabled: false,
9064                identity_hash: None,
9065                prefer_shorter_path: false,
9066                max_paths_per_destination: 1,
9067                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9068                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9069                max_path_destinations: usize::MAX,
9070                max_tunnel_destinations_total: usize::MAX,
9071                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9072                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9073                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9074                announce_sig_cache_enabled: true,
9075                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9076                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9077                announce_queue_max_entries: 256,
9078                announce_queue_max_interfaces: 1024,
9079            },
9080            rx,
9081            tx,
9082            Box::new(cbs),
9083        );
9084
9085        let _ = driver.link_manager.create_link(
9086            &[0xDD; 16],
9087            &[0x11; 32],
9088            1,
9089            rns_core::constants::MTU as u32,
9090            &mut OsRng,
9091        );
9092
9093        driver.begin_drain(Duration::from_secs(3));
9094
9095        let QueryResponse::DrainStatus(status) = driver.handle_query(QueryRequest::DrainStatus)
9096        else {
9097            panic!("expected drain status response");
9098        };
9099        assert_eq!(status.state, LifecycleState::Draining);
9100        assert!(!status.drain_complete);
9101        assert!(status
9102            .detail
9103            .unwrap_or_default()
9104            .contains("1 link(s) still active"));
9105    }
9106
9107    #[test]
9108    fn begin_drain_with_queued_writer_frames_reports_incomplete_status() {
9109        let (tx, rx) = event::channel();
9110        let (cbs, _, _, _, _, _) = MockCallbacks::new();
9111        let mut driver = Driver::new(
9112            TransportConfig {
9113                transport_enabled: false,
9114                identity_hash: None,
9115                prefer_shorter_path: false,
9116                max_paths_per_destination: 1,
9117                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9118                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9119                max_path_destinations: usize::MAX,
9120                max_tunnel_destinations_total: usize::MAX,
9121                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9122                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9123                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9124                announce_sig_cache_enabled: true,
9125                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9126                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9127                announce_queue_max_entries: 256,
9128                announce_queue_max_interfaces: 1024,
9129            },
9130            rx,
9131            tx,
9132            Box::new(cbs),
9133        );
9134
9135        let info = make_interface_info(77);
9136        let (entered_tx, entered_rx) = std::sync::mpsc::channel();
9137        let (release_tx, release_rx) = std::sync::mpsc::channel();
9138        let (writer, async_writer_metrics) = crate::interface::wrap_async_writer(
9139            Box::new(BlockingWriter {
9140                entered_tx,
9141                release_rx,
9142            }),
9143            InterfaceId(77),
9144            &info.name,
9145            driver.event_tx.clone(),
9146            1,
9147        );
9148
9149        driver.interfaces.insert(
9150            InterfaceId(77),
9151            InterfaceEntry {
9152                id: InterfaceId(77),
9153                info,
9154                writer,
9155                async_writer_metrics: Some(async_writer_metrics),
9156                enabled: true,
9157                online: true,
9158                dynamic: false,
9159                ifac: None,
9160                stats: InterfaceStats::default(),
9161                interface_type: "TestInterface".to_string(),
9162                send_retry_at: None,
9163                send_retry_backoff: Duration::ZERO,
9164            },
9165        );
9166
9167        driver.dispatch_all(vec![TransportAction::SendOnInterface {
9168            interface: InterfaceId(77),
9169            raw: vec![0x01],
9170        }]);
9171        entered_rx.recv_timeout(Duration::from_secs(1)).unwrap();
9172        driver.dispatch_all(vec![TransportAction::SendOnInterface {
9173            interface: InterfaceId(77),
9174            raw: vec![0x02],
9175        }]);
9176
9177        driver.begin_drain(Duration::from_secs(3));
9178
9179        let QueryResponse::DrainStatus(status) = driver.handle_query(QueryRequest::DrainStatus)
9180        else {
9181            panic!("expected drain status response");
9182        };
9183        assert_eq!(status.state, LifecycleState::Draining);
9184        assert!(!status.drain_complete);
9185        assert_eq!(status.interface_writer_queued_frames, 1);
9186        assert!(status
9187            .detail
9188            .unwrap_or_default()
9189            .contains("queued interface writer frame"));
9190
9191        let _ = release_tx.send(());
9192    }
9193
9194    #[test]
9195    fn enforce_drain_deadline_tears_down_remaining_links() {
9196        let (tx, rx) = event::channel();
9197        let (cbs, _, _, _, _, _) = MockCallbacks::new();
9198        let mut driver = Driver::new(
9199            TransportConfig {
9200                transport_enabled: false,
9201                identity_hash: None,
9202                prefer_shorter_path: false,
9203                max_paths_per_destination: 1,
9204                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9205                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9206                max_path_destinations: usize::MAX,
9207                max_tunnel_destinations_total: usize::MAX,
9208                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9209                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9210                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9211                announce_sig_cache_enabled: true,
9212                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9213                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9214                announce_queue_max_entries: 256,
9215                announce_queue_max_interfaces: 1024,
9216            },
9217            rx,
9218            tx,
9219            Box::new(cbs),
9220        );
9221
9222        let _ = driver.link_manager.create_link(
9223            &[0xDD; 16],
9224            &[0x11; 32],
9225            1,
9226            rns_core::constants::MTU as u32,
9227            &mut OsRng,
9228        );
9229        driver.begin_drain(Duration::ZERO);
9230
9231        driver.enforce_drain_deadline();
9232
9233        assert_eq!(driver.lifecycle_state, LifecycleState::Stopping);
9234        assert_eq!(driver.link_manager.link_count(), 0);
9235        let QueryResponse::DrainStatus(status) = driver.handle_query(QueryRequest::DrainStatus)
9236        else {
9237            panic!("expected drain status response");
9238        };
9239        assert!(status.drain_complete);
9240        assert_eq!(status.state, LifecycleState::Stopping);
9241    }
9242
9243    #[test]
9244    fn begin_drain_with_holepunch_session_reports_incomplete_status_and_deadline_aborts_it() {
9245        let (tx, rx) = event::channel();
9246        let (cbs, _, _, _, _, _) = MockCallbacks::new();
9247        let mut driver = Driver::new(
9248            TransportConfig {
9249                transport_enabled: false,
9250                identity_hash: None,
9251                prefer_shorter_path: false,
9252                max_paths_per_destination: 1,
9253                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9254                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9255                max_path_destinations: usize::MAX,
9256                max_tunnel_destinations_total: usize::MAX,
9257                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9258                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9259                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9260                announce_sig_cache_enabled: true,
9261                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9262                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9263                announce_queue_max_entries: 256,
9264                announce_queue_max_interfaces: 1024,
9265            },
9266            rx,
9267            tx,
9268            Box::new(cbs),
9269        );
9270        driver.holepunch_manager = crate::holepunch::orchestrator::HolePunchManager::new(
9271            vec!["127.0.0.1:4343".parse().unwrap()],
9272            rns_core::holepunch::ProbeProtocol::Rnsp,
9273            None,
9274        );
9275
9276        let _ = driver.holepunch_manager.propose(
9277            [0x44; 16],
9278            &[0xAA; 32],
9279            &mut OsRng,
9280            &driver.get_event_sender(),
9281        );
9282        assert_eq!(driver.holepunch_manager.session_count(), 1);
9283
9284        driver.begin_drain(Duration::from_secs(3));
9285
9286        let QueryResponse::DrainStatus(status) = driver.handle_query(QueryRequest::DrainStatus)
9287        else {
9288            panic!("expected drain status response");
9289        };
9290        assert_eq!(status.state, LifecycleState::Draining);
9291        assert!(!status.drain_complete);
9292        assert!(status
9293            .detail
9294            .unwrap_or_default()
9295            .contains("1 hole-punch session(s) still active"));
9296
9297        driver.begin_drain(Duration::ZERO);
9298        driver.enforce_drain_deadline();
9299
9300        assert_eq!(driver.holepunch_manager.session_count(), 0);
9301        let QueryResponse::DrainStatus(status) = driver.handle_query(QueryRequest::DrainStatus)
9302        else {
9303            panic!("expected drain status response");
9304        };
9305        assert!(status.drain_complete);
9306        assert_eq!(status.state, LifecycleState::Stopping);
9307    }
9308
9309    #[test]
9310    fn begin_drain_event_is_processed_by_run_loop() {
9311        let (tx, rx) = event::channel();
9312        let tx_query = tx.clone();
9313        let (cbs, _, _, _, _, _) = MockCallbacks::new();
9314        let mut driver = Driver::new(
9315            TransportConfig {
9316                transport_enabled: false,
9317                identity_hash: None,
9318                prefer_shorter_path: false,
9319                max_paths_per_destination: 1,
9320                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9321                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9322                max_path_destinations: usize::MAX,
9323                max_tunnel_destinations_total: usize::MAX,
9324                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9325                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9326                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9327                announce_sig_cache_enabled: true,
9328                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9329                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9330                announce_queue_max_entries: 256,
9331                announce_queue_max_interfaces: 1024,
9332            },
9333            rx,
9334            tx.clone(),
9335            Box::new(cbs),
9336        );
9337
9338        let handle = std::thread::spawn(move || driver.run());
9339        tx.send(Event::BeginDrain {
9340            timeout: Duration::from_secs(2),
9341        })
9342        .unwrap();
9343        let (resp_tx, resp_rx) = std::sync::mpsc::channel();
9344        tx_query
9345            .send(Event::Query(QueryRequest::DrainStatus, resp_tx))
9346            .unwrap();
9347        let status = match resp_rx.recv().unwrap() {
9348            QueryResponse::DrainStatus(status) => status,
9349            other => panic!("expected drain status response, got {:?}", other),
9350        };
9351        assert_eq!(status.state, LifecycleState::Draining);
9352        tx_query.send(Event::Shutdown).unwrap();
9353        handle.join().unwrap();
9354    }
9355
9356    #[test]
9357    fn send_channel_message_returns_error_while_draining() {
9358        let (tx, rx) = event::channel();
9359        let (cbs, _, _, _, _, _) = MockCallbacks::new();
9360        let mut driver = Driver::new(
9361            TransportConfig {
9362                transport_enabled: false,
9363                identity_hash: None,
9364                prefer_shorter_path: false,
9365                max_paths_per_destination: 1,
9366                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9367                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9368                max_path_destinations: usize::MAX,
9369                max_tunnel_destinations_total: usize::MAX,
9370                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9371                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9372                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9373                announce_sig_cache_enabled: true,
9374                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9375                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9376                announce_queue_max_entries: 256,
9377                announce_queue_max_interfaces: 1024,
9378            },
9379            rx,
9380            tx.clone(),
9381            Box::new(cbs),
9382        );
9383
9384        tx.send(Event::BeginDrain {
9385            timeout: Duration::from_secs(2),
9386        })
9387        .unwrap();
9388        let (resp_tx, resp_rx) = mpsc::channel();
9389        tx.send(Event::SendChannelMessage {
9390            link_id: [0xAA; 16],
9391            msgtype: 7,
9392            payload: b"drain".to_vec(),
9393            response_tx: resp_tx,
9394        })
9395        .unwrap();
9396        tx.send(Event::Shutdown).unwrap();
9397        driver.run();
9398
9399        let response = resp_rx.recv().unwrap();
9400        assert_eq!(
9401            response,
9402            Err("cannot send channel message while node is draining".into())
9403        );
9404    }
9405
9406    #[test]
9407    fn send_outbound_is_ignored_while_draining() {
9408        let (tx, rx) = event::channel();
9409        let (cbs, _, _, _, _, _) = MockCallbacks::new();
9410        let mut driver = Driver::new(
9411            TransportConfig {
9412                transport_enabled: false,
9413                identity_hash: None,
9414                prefer_shorter_path: false,
9415                max_paths_per_destination: 1,
9416                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9417                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9418                max_path_destinations: usize::MAX,
9419                max_tunnel_destinations_total: usize::MAX,
9420                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9421                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9422                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9423                announce_sig_cache_enabled: true,
9424                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9425                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9426                announce_queue_max_entries: 256,
9427                announce_queue_max_interfaces: 1024,
9428            },
9429            rx,
9430            tx.clone(),
9431            Box::new(cbs),
9432        );
9433        let identity = Identity::new(&mut OsRng);
9434        let info = make_interface_info(1);
9435        driver.engine.register_interface(info);
9436        let (writer, sent) = MockWriter::new();
9437        driver
9438            .interfaces
9439            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
9440
9441        tx.send(Event::BeginDrain {
9442            timeout: Duration::from_secs(2),
9443        })
9444        .unwrap();
9445        tx.send(Event::SendOutbound {
9446            raw: build_announce_packet(&identity),
9447            dest_type: constants::DESTINATION_SINGLE,
9448            attached_interface: None,
9449        })
9450        .unwrap();
9451        tx.send(Event::Shutdown).unwrap();
9452        driver.run();
9453
9454        assert!(sent.lock().unwrap().is_empty());
9455        assert!(driver.sent_packets.is_empty());
9456    }
9457
9458    #[test]
9459    fn request_path_is_ignored_while_draining() {
9460        let (tx, rx) = event::channel();
9461        let (cbs, _, _, _, _, _) = MockCallbacks::new();
9462        let mut driver = Driver::new(
9463            TransportConfig {
9464                transport_enabled: false,
9465                identity_hash: None,
9466                prefer_shorter_path: false,
9467                max_paths_per_destination: 1,
9468                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9469                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9470                max_path_destinations: usize::MAX,
9471                max_tunnel_destinations_total: usize::MAX,
9472                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9473                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9474                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9475                announce_sig_cache_enabled: true,
9476                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9477                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9478                announce_queue_max_entries: 256,
9479                announce_queue_max_interfaces: 1024,
9480            },
9481            rx,
9482            tx.clone(),
9483            Box::new(cbs),
9484        );
9485        let info = make_interface_info(1);
9486        driver.engine.register_interface(info);
9487        let (writer, sent) = MockWriter::new();
9488        driver
9489            .interfaces
9490            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
9491
9492        tx.send(Event::BeginDrain {
9493            timeout: Duration::from_secs(2),
9494        })
9495        .unwrap();
9496        tx.send(Event::RequestPath {
9497            dest_hash: [0xAA; 16],
9498        })
9499        .unwrap();
9500        tx.send(Event::Shutdown).unwrap();
9501        driver.run();
9502
9503        assert!(sent.lock().unwrap().is_empty());
9504    }
9505
9506    #[test]
9507    fn create_link_returns_zero_link_id_while_draining() {
9508        let (tx, rx) = event::channel();
9509        let (cbs, _, _, _, _, _) = MockCallbacks::new();
9510        let mut driver = Driver::new(
9511            TransportConfig {
9512                transport_enabled: false,
9513                identity_hash: None,
9514                prefer_shorter_path: false,
9515                max_paths_per_destination: 1,
9516                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9517                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9518                max_path_destinations: usize::MAX,
9519                max_tunnel_destinations_total: usize::MAX,
9520                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9521                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9522                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9523                announce_sig_cache_enabled: true,
9524                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9525                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9526                announce_queue_max_entries: 256,
9527                announce_queue_max_interfaces: 1024,
9528            },
9529            rx,
9530            tx.clone(),
9531            Box::new(cbs),
9532        );
9533
9534        tx.send(Event::BeginDrain {
9535            timeout: Duration::from_secs(2),
9536        })
9537        .unwrap();
9538        let (resp_tx, resp_rx) = mpsc::channel();
9539        tx.send(Event::CreateLink {
9540            dest_hash: [0xAB; 16],
9541            dest_sig_pub_bytes: [0xCD; 32],
9542            response_tx: resp_tx,
9543        })
9544        .unwrap();
9545        tx.send(Event::Shutdown).unwrap();
9546        driver.run();
9547
9548        assert_eq!(resp_rx.recv().unwrap(), [0u8; 16]);
9549    }
9550
9551    #[test]
9552    fn announce_callback() {
9553        let (tx, rx) = event::channel();
9554        let (cbs, announces, paths, _, _, _) = MockCallbacks::new();
9555        let mut driver = Driver::new(
9556            TransportConfig {
9557                transport_enabled: false,
9558                identity_hash: None,
9559                prefer_shorter_path: false,
9560                max_paths_per_destination: 1,
9561                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9562                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9563                max_path_destinations: usize::MAX,
9564                max_tunnel_destinations_total: usize::MAX,
9565                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9566                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9567                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9568                announce_sig_cache_enabled: true,
9569                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9570                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9571                announce_queue_max_entries: 256,
9572                announce_queue_max_interfaces: 1024,
9573            },
9574            rx,
9575            tx.clone(),
9576            Box::new(cbs),
9577        );
9578        let info = make_interface_info(1);
9579        driver.engine.register_interface(info.clone());
9580        let (writer, _sent) = MockWriter::new();
9581        driver
9582            .interfaces
9583            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
9584
9585        let identity = Identity::new(&mut OsRng);
9586        let announce_raw = build_announce_packet(&identity);
9587
9588        tx.send(Event::Frame {
9589            interface_id: InterfaceId(1),
9590            data: announce_raw,
9591        })
9592        .unwrap();
9593        tx.send(Event::Shutdown).unwrap();
9594        driver.run();
9595
9596        let ann = announces.lock().unwrap();
9597        assert_eq!(ann.len(), 1);
9598        // Hops should be 1 (incremented from 0 by handle_inbound)
9599        assert_eq!(ann[0].1, 1);
9600
9601        let p = paths.lock().unwrap();
9602        assert_eq!(p.len(), 1);
9603    }
9604
9605    #[test]
9606    fn dispatch_skips_offline_interface() {
9607        let (tx, rx) = event::channel();
9608        let (cbs, _, _, _, _, _) = MockCallbacks::new();
9609        let mut driver = Driver::new(
9610            TransportConfig {
9611                transport_enabled: false,
9612                identity_hash: None,
9613                prefer_shorter_path: false,
9614                max_paths_per_destination: 1,
9615                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9616                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9617                max_path_destinations: usize::MAX,
9618                max_tunnel_destinations_total: usize::MAX,
9619                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9620                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9621                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9622                announce_sig_cache_enabled: true,
9623                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9624                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9625                announce_queue_max_entries: 256,
9626                announce_queue_max_interfaces: 1024,
9627            },
9628            rx,
9629            tx.clone(),
9630            Box::new(cbs),
9631        );
9632
9633        let (w1, sent1) = MockWriter::new();
9634        let (w2, sent2) = MockWriter::new();
9635        driver
9636            .interfaces
9637            .insert(InterfaceId(1), make_entry(1, Box::new(w1), false)); // offline
9638        driver
9639            .interfaces
9640            .insert(InterfaceId(2), make_entry(2, Box::new(w2), true));
9641
9642        // Direct send to offline interface: should be skipped
9643        driver.dispatch_all(vec![TransportAction::SendOnInterface {
9644            interface: InterfaceId(1),
9645            raw: vec![0x01],
9646        }]);
9647        assert_eq!(sent1.lock().unwrap().len(), 0);
9648
9649        // Broadcast: only online interface should receive
9650        driver.dispatch_all(vec![TransportAction::BroadcastOnAllInterfaces {
9651            raw: vec![0x02],
9652            exclude: None,
9653        }]);
9654        assert_eq!(sent1.lock().unwrap().len(), 0); // still offline
9655        assert_eq!(sent2.lock().unwrap().len(), 1);
9656
9657        drop(tx);
9658    }
9659
9660    #[test]
9661    fn interface_up_refreshes_writer() {
9662        let (tx, rx) = event::channel();
9663        let (cbs, _, _, _, _, _) = MockCallbacks::new();
9664        let mut driver = Driver::new(
9665            TransportConfig {
9666                transport_enabled: false,
9667                identity_hash: None,
9668                prefer_shorter_path: false,
9669                max_paths_per_destination: 1,
9670                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9671                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9672                max_path_destinations: usize::MAX,
9673                max_tunnel_destinations_total: usize::MAX,
9674                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9675                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9676                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9677                announce_sig_cache_enabled: true,
9678                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9679                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9680                announce_queue_max_entries: 256,
9681                announce_queue_max_interfaces: 1024,
9682            },
9683            rx,
9684            tx.clone(),
9685            Box::new(cbs),
9686        );
9687
9688        let (w_old, sent_old) = MockWriter::new();
9689        driver
9690            .interfaces
9691            .insert(InterfaceId(1), make_entry(1, Box::new(w_old), false));
9692
9693        // Simulate reconnect: InterfaceUp with new writer
9694        let (w_new, sent_new) = MockWriter::new();
9695        tx.send(Event::InterfaceUp(
9696            InterfaceId(1),
9697            Some(Box::new(w_new)),
9698            None,
9699        ))
9700        .unwrap();
9701        tx.send(Event::Shutdown).unwrap();
9702        driver.run();
9703
9704        // Interface should be online now
9705        assert!(driver.interfaces[&InterfaceId(1)].online);
9706
9707        // Send via the (now-refreshed) interface
9708        driver.dispatch_all(vec![TransportAction::SendOnInterface {
9709            interface: InterfaceId(1),
9710            raw: vec![0xFF],
9711        }]);
9712
9713        // Old writer should not have received anything
9714        assert_eq!(sent_old.lock().unwrap().len(), 0);
9715        // New writer should have received the data
9716        wait_for_sent_len(&sent_new, 1);
9717        assert_eq!(sent_new.lock().unwrap()[0], vec![0xFF]);
9718
9719        drop(tx);
9720    }
9721
9722    #[test]
9723    fn dynamic_interface_register() {
9724        let (tx, rx) = event::channel();
9725        let (cbs, _, _, _, iface_ups, _) = MockCallbacks::new();
9726        let mut driver = Driver::new(
9727            TransportConfig {
9728                transport_enabled: false,
9729                identity_hash: None,
9730                prefer_shorter_path: false,
9731                max_paths_per_destination: 1,
9732                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9733                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9734                max_path_destinations: usize::MAX,
9735                max_tunnel_destinations_total: usize::MAX,
9736                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9737                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9738                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9739                announce_sig_cache_enabled: true,
9740                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9741                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9742                announce_queue_max_entries: 256,
9743                announce_queue_max_interfaces: 1024,
9744            },
9745            rx,
9746            tx.clone(),
9747            Box::new(cbs),
9748        );
9749
9750        let info = make_interface_info(100);
9751        let (writer, sent) = MockWriter::new();
9752
9753        // InterfaceUp with InterfaceInfo = new dynamic interface
9754        tx.send(Event::InterfaceUp(
9755            InterfaceId(100),
9756            Some(Box::new(writer)),
9757            Some(info),
9758        ))
9759        .unwrap();
9760        tx.send(Event::Shutdown).unwrap();
9761        driver.run();
9762
9763        // Should be registered and online
9764        assert!(driver.interfaces.contains_key(&InterfaceId(100)));
9765        assert!(driver.interfaces[&InterfaceId(100)].online);
9766        assert!(driver.interfaces[&InterfaceId(100)].dynamic);
9767
9768        // Callback should have fired
9769        assert_eq!(iface_ups.lock().unwrap().len(), 1);
9770        assert_eq!(iface_ups.lock().unwrap()[0], InterfaceId(100));
9771
9772        // Can send to it
9773        driver.dispatch_all(vec![TransportAction::SendOnInterface {
9774            interface: InterfaceId(100),
9775            raw: vec![0x42],
9776        }]);
9777        wait_for_sent_len(&sent, 1);
9778
9779        drop(tx);
9780    }
9781
9782    #[test]
9783    fn dynamic_interface_deregister() {
9784        let (tx, rx) = event::channel();
9785        let (cbs, _, _, _, _, iface_downs) = MockCallbacks::new();
9786        let mut driver = Driver::new(
9787            TransportConfig {
9788                transport_enabled: false,
9789                identity_hash: None,
9790                prefer_shorter_path: false,
9791                max_paths_per_destination: 1,
9792                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9793                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9794                max_path_destinations: usize::MAX,
9795                max_tunnel_destinations_total: usize::MAX,
9796                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9797                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9798                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9799                announce_sig_cache_enabled: true,
9800                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9801                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9802                announce_queue_max_entries: 256,
9803                announce_queue_max_interfaces: 1024,
9804            },
9805            rx,
9806            tx.clone(),
9807            Box::new(cbs),
9808        );
9809
9810        // Register a dynamic interface
9811        let info = make_interface_info(200);
9812        driver.engine.register_interface(info.clone());
9813        let (writer, _sent) = MockWriter::new();
9814        driver.interfaces.insert(
9815            InterfaceId(200),
9816            InterfaceEntry {
9817                id: InterfaceId(200),
9818                info,
9819                writer: Box::new(writer),
9820                async_writer_metrics: None,
9821                enabled: true,
9822                online: true,
9823                dynamic: true,
9824                ifac: None,
9825                stats: InterfaceStats::default(),
9826                interface_type: String::new(),
9827                send_retry_at: None,
9828                send_retry_backoff: Duration::ZERO,
9829            },
9830        );
9831
9832        // InterfaceDown for dynamic → should be removed entirely
9833        tx.send(Event::InterfaceDown(InterfaceId(200))).unwrap();
9834        tx.send(Event::Shutdown).unwrap();
9835        driver.run();
9836
9837        assert!(!driver.interfaces.contains_key(&InterfaceId(200)));
9838        assert_eq!(iface_downs.lock().unwrap().len(), 1);
9839        assert_eq!(iface_downs.lock().unwrap()[0], InterfaceId(200));
9840    }
9841
9842    #[test]
9843    fn send_wouldblock_is_backed_off_between_dispatches() {
9844        let (tx, rx) = event::channel();
9845        let (cbs, ..) = MockCallbacks::new();
9846        let mut driver = Driver::new(
9847            TransportConfig {
9848                transport_enabled: false,
9849                identity_hash: None,
9850                prefer_shorter_path: false,
9851                max_paths_per_destination: 1,
9852                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9853                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9854                max_path_destinations: usize::MAX,
9855                max_tunnel_destinations_total: usize::MAX,
9856                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9857                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9858                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9859                announce_sig_cache_enabled: true,
9860                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9861                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9862                announce_queue_max_entries: 256,
9863                announce_queue_max_interfaces: 1024,
9864            },
9865            rx,
9866            tx,
9867            Box::new(cbs),
9868        );
9869        let (writer, attempts) = WouldBlockWriter::new();
9870        driver
9871            .interfaces
9872            .insert(InterfaceId(7), make_entry(7, Box::new(writer), true));
9873
9874        let action = TransportAction::SendOnInterface {
9875            interface: InterfaceId(7),
9876            raw: vec![0x01, 0x00, 0x42],
9877        };
9878        driver.dispatch_all(vec![action.clone()]);
9879        assert_eq!(*attempts.lock().unwrap(), 1);
9880
9881        driver.dispatch_all(vec![action.clone()]);
9882        assert_eq!(
9883            *attempts.lock().unwrap(),
9884            1,
9885            "second dispatch should be deferred during backoff"
9886        );
9887
9888        let entry = driver.interfaces.get_mut(&InterfaceId(7)).unwrap();
9889        entry.send_retry_at = Some(Instant::now() - Duration::from_millis(1));
9890        driver.dispatch_all(vec![action]);
9891        assert_eq!(*attempts.lock().unwrap(), 2);
9892    }
9893
9894    #[test]
9895    fn interface_callbacks_fire() {
9896        let (tx, rx) = event::channel();
9897        let (cbs, _, _, _, iface_ups, iface_downs) = MockCallbacks::new();
9898        let mut driver = Driver::new(
9899            TransportConfig {
9900                transport_enabled: false,
9901                identity_hash: None,
9902                prefer_shorter_path: false,
9903                max_paths_per_destination: 1,
9904                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9905                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9906                max_path_destinations: usize::MAX,
9907                max_tunnel_destinations_total: usize::MAX,
9908                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9909                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9910                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9911                announce_sig_cache_enabled: true,
9912                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9913                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9914                announce_queue_max_entries: 256,
9915                announce_queue_max_interfaces: 1024,
9916            },
9917            rx,
9918            tx.clone(),
9919            Box::new(cbs),
9920        );
9921
9922        // Static interface
9923        let (writer, _) = MockWriter::new();
9924        driver
9925            .interfaces
9926            .insert(InterfaceId(1), make_entry(1, Box::new(writer), false));
9927
9928        tx.send(Event::InterfaceUp(InterfaceId(1), None, None))
9929            .unwrap();
9930        tx.send(Event::InterfaceDown(InterfaceId(1))).unwrap();
9931        tx.send(Event::Shutdown).unwrap();
9932        driver.run();
9933
9934        assert_eq!(iface_ups.lock().unwrap().len(), 1);
9935        assert_eq!(iface_downs.lock().unwrap().len(), 1);
9936        // Static interface should still exist but be offline
9937        assert!(driver.interfaces.contains_key(&InterfaceId(1)));
9938        assert!(!driver.interfaces[&InterfaceId(1)].online);
9939    }
9940
9941    // =========================================================================
9942    // New tests for Phase 6a
9943    // =========================================================================
9944
9945    #[test]
9946    fn frame_updates_rx_stats() {
9947        let (tx, rx) = event::channel();
9948        let (cbs, _, _, _, _, _) = MockCallbacks::new();
9949        let mut driver = Driver::new(
9950            TransportConfig {
9951                transport_enabled: false,
9952                identity_hash: None,
9953                prefer_shorter_path: false,
9954                max_paths_per_destination: 1,
9955                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
9956                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
9957                max_path_destinations: usize::MAX,
9958                max_tunnel_destinations_total: usize::MAX,
9959                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
9960                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
9961                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
9962                announce_sig_cache_enabled: true,
9963                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
9964                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
9965                announce_queue_max_entries: 256,
9966                announce_queue_max_interfaces: 1024,
9967            },
9968            rx,
9969            tx.clone(),
9970            Box::new(cbs),
9971        );
9972        let info = make_interface_info(1);
9973        driver.engine.register_interface(info.clone());
9974        let (writer, _sent) = MockWriter::new();
9975        driver
9976            .interfaces
9977            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
9978
9979        let identity = Identity::new(&mut OsRng);
9980        let announce_raw = build_announce_packet(&identity);
9981        let announce_len = announce_raw.len() as u64;
9982
9983        tx.send(Event::Frame {
9984            interface_id: InterfaceId(1),
9985            data: announce_raw,
9986        })
9987        .unwrap();
9988        tx.send(Event::Shutdown).unwrap();
9989        driver.run();
9990
9991        let stats = &driver.interfaces[&InterfaceId(1)].stats;
9992        assert_eq!(stats.rxb, announce_len);
9993        assert_eq!(stats.rx_packets, 1);
9994    }
9995
9996    #[test]
9997    fn send_updates_tx_stats() {
9998        let (tx, rx) = event::channel();
9999        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10000        let mut driver = Driver::new(
10001            TransportConfig {
10002                transport_enabled: false,
10003                identity_hash: None,
10004                prefer_shorter_path: false,
10005                max_paths_per_destination: 1,
10006                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10007                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10008                max_path_destinations: usize::MAX,
10009                max_tunnel_destinations_total: usize::MAX,
10010                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10011                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10012                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10013                announce_sig_cache_enabled: true,
10014                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10015                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10016                announce_queue_max_entries: 256,
10017                announce_queue_max_interfaces: 1024,
10018            },
10019            rx,
10020            tx.clone(),
10021            Box::new(cbs),
10022        );
10023        let (writer, _sent) = MockWriter::new();
10024        driver
10025            .interfaces
10026            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
10027
10028        driver.dispatch_all(vec![TransportAction::SendOnInterface {
10029            interface: InterfaceId(1),
10030            raw: vec![0x01, 0x02, 0x03],
10031        }]);
10032
10033        let stats = &driver.interfaces[&InterfaceId(1)].stats;
10034        assert_eq!(stats.txb, 3);
10035        assert_eq!(stats.tx_packets, 1);
10036
10037        drop(tx);
10038    }
10039
10040    #[test]
10041    fn broadcast_updates_tx_stats() {
10042        let (tx, rx) = event::channel();
10043        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10044        let mut driver = Driver::new(
10045            TransportConfig {
10046                transport_enabled: false,
10047                identity_hash: None,
10048                prefer_shorter_path: false,
10049                max_paths_per_destination: 1,
10050                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10051                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10052                max_path_destinations: usize::MAX,
10053                max_tunnel_destinations_total: usize::MAX,
10054                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10055                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10056                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10057                announce_sig_cache_enabled: true,
10058                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10059                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10060                announce_queue_max_entries: 256,
10061                announce_queue_max_interfaces: 1024,
10062            },
10063            rx,
10064            tx.clone(),
10065            Box::new(cbs),
10066        );
10067        let (w1, _s1) = MockWriter::new();
10068        let (w2, _s2) = MockWriter::new();
10069        driver
10070            .interfaces
10071            .insert(InterfaceId(1), make_entry(1, Box::new(w1), true));
10072        driver
10073            .interfaces
10074            .insert(InterfaceId(2), make_entry(2, Box::new(w2), true));
10075
10076        driver.dispatch_all(vec![TransportAction::BroadcastOnAllInterfaces {
10077            raw: vec![0xAA, 0xBB],
10078            exclude: None,
10079        }]);
10080
10081        // Both interfaces should have tx stats updated
10082        assert_eq!(driver.interfaces[&InterfaceId(1)].stats.txb, 2);
10083        assert_eq!(driver.interfaces[&InterfaceId(1)].stats.tx_packets, 1);
10084        assert_eq!(driver.interfaces[&InterfaceId(2)].stats.txb, 2);
10085        assert_eq!(driver.interfaces[&InterfaceId(2)].stats.tx_packets, 1);
10086
10087        drop(tx);
10088    }
10089
10090    #[test]
10091    fn query_interface_stats() {
10092        let (tx, rx) = event::channel();
10093        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10094        let mut driver = Driver::new(
10095            TransportConfig {
10096                transport_enabled: true,
10097                identity_hash: Some([0x42; 16]),
10098                prefer_shorter_path: false,
10099                max_paths_per_destination: 1,
10100                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10101                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10102                max_path_destinations: usize::MAX,
10103                max_tunnel_destinations_total: usize::MAX,
10104                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10105                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10106                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10107                announce_sig_cache_enabled: true,
10108                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10109                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10110                announce_queue_max_entries: 256,
10111                announce_queue_max_interfaces: 1024,
10112            },
10113            rx,
10114            tx.clone(),
10115            Box::new(cbs),
10116        );
10117        let (writer, _sent) = MockWriter::new();
10118        driver
10119            .interfaces
10120            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
10121
10122        let (resp_tx, resp_rx) = mpsc::channel();
10123        tx.send(Event::Query(QueryRequest::InterfaceStats, resp_tx))
10124            .unwrap();
10125        tx.send(Event::Shutdown).unwrap();
10126        driver.run();
10127
10128        let resp = resp_rx.recv().unwrap();
10129        match resp {
10130            QueryResponse::InterfaceStats(stats) => {
10131                assert_eq!(stats.interfaces.len(), 1);
10132                assert_eq!(stats.interfaces[0].name, "test-1");
10133                assert!(stats.interfaces[0].status);
10134                assert_eq!(stats.transport_id, Some([0x42; 16]));
10135                assert!(stats.transport_enabled);
10136            }
10137            _ => panic!("unexpected response"),
10138        }
10139    }
10140
10141    #[test]
10142    fn query_path_table() {
10143        let (tx, rx) = event::channel();
10144        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10145        let mut driver = Driver::new(
10146            TransportConfig {
10147                transport_enabled: false,
10148                identity_hash: None,
10149                prefer_shorter_path: false,
10150                max_paths_per_destination: 1,
10151                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10152                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10153                max_path_destinations: usize::MAX,
10154                max_tunnel_destinations_total: usize::MAX,
10155                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10156                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10157                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10158                announce_sig_cache_enabled: true,
10159                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10160                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10161                announce_queue_max_entries: 256,
10162                announce_queue_max_interfaces: 1024,
10163            },
10164            rx,
10165            tx.clone(),
10166            Box::new(cbs),
10167        );
10168        let info = make_interface_info(1);
10169        driver.engine.register_interface(info);
10170        let (writer, _sent) = MockWriter::new();
10171        driver
10172            .interfaces
10173            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
10174
10175        // Feed an announce to create a path entry
10176        let identity = Identity::new(&mut OsRng);
10177        let announce_raw = build_announce_packet(&identity);
10178        tx.send(Event::Frame {
10179            interface_id: InterfaceId(1),
10180            data: announce_raw,
10181        })
10182        .unwrap();
10183
10184        let (resp_tx, resp_rx) = mpsc::channel();
10185        tx.send(Event::Query(
10186            QueryRequest::PathTable { max_hops: None },
10187            resp_tx,
10188        ))
10189        .unwrap();
10190        tx.send(Event::Shutdown).unwrap();
10191        driver.run();
10192
10193        let resp = resp_rx.recv().unwrap();
10194        match resp {
10195            QueryResponse::PathTable(entries) => {
10196                assert_eq!(entries.len(), 1);
10197                assert_eq!(entries[0].hops, 1);
10198            }
10199            _ => panic!("unexpected response"),
10200        }
10201    }
10202
10203    #[test]
10204    fn query_drop_path() {
10205        let (tx, rx) = event::channel();
10206        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10207        let mut driver = Driver::new(
10208            TransportConfig {
10209                transport_enabled: false,
10210                identity_hash: None,
10211                prefer_shorter_path: false,
10212                max_paths_per_destination: 1,
10213                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10214                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10215                max_path_destinations: usize::MAX,
10216                max_tunnel_destinations_total: usize::MAX,
10217                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10218                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10219                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10220                announce_sig_cache_enabled: true,
10221                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10222                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10223                announce_queue_max_entries: 256,
10224                announce_queue_max_interfaces: 1024,
10225            },
10226            rx,
10227            tx.clone(),
10228            Box::new(cbs),
10229        );
10230        let info = make_interface_info(1);
10231        driver.engine.register_interface(info);
10232        let (writer, _sent) = MockWriter::new();
10233        driver
10234            .interfaces
10235            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
10236
10237        // Feed an announce to create a path entry
10238        let identity = Identity::new(&mut OsRng);
10239        let announce_raw = build_announce_packet(&identity);
10240        let dest_hash =
10241            rns_core::destination::destination_hash("test", &["app"], Some(identity.hash()));
10242
10243        tx.send(Event::Frame {
10244            interface_id: InterfaceId(1),
10245            data: announce_raw,
10246        })
10247        .unwrap();
10248
10249        let (resp_tx, resp_rx) = mpsc::channel();
10250        tx.send(Event::Query(QueryRequest::DropPath { dest_hash }, resp_tx))
10251            .unwrap();
10252        tx.send(Event::Shutdown).unwrap();
10253        driver.run();
10254
10255        let resp = resp_rx.recv().unwrap();
10256        match resp {
10257            QueryResponse::DropPath(dropped) => {
10258                assert!(dropped);
10259            }
10260            _ => panic!("unexpected response"),
10261        }
10262    }
10263
10264    #[test]
10265    fn send_outbound_event() {
10266        let (tx, rx) = event::channel();
10267        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10268        let mut driver = Driver::new(
10269            TransportConfig {
10270                transport_enabled: false,
10271                identity_hash: None,
10272                prefer_shorter_path: false,
10273                max_paths_per_destination: 1,
10274                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10275                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10276                max_path_destinations: usize::MAX,
10277                max_tunnel_destinations_total: usize::MAX,
10278                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10279                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10280                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10281                announce_sig_cache_enabled: true,
10282                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10283                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10284                announce_queue_max_entries: 256,
10285                announce_queue_max_interfaces: 1024,
10286            },
10287            rx,
10288            tx.clone(),
10289            Box::new(cbs),
10290        );
10291        let (writer, sent) = MockWriter::new();
10292        let info = make_interface_info(1);
10293        driver.engine.register_interface(info);
10294        driver
10295            .interfaces
10296            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
10297
10298        // Build a DATA packet to a destination
10299        let dest = [0xAA; 16];
10300        let flags = PacketFlags {
10301            header_type: constants::HEADER_1,
10302            context_flag: constants::FLAG_UNSET,
10303            transport_type: constants::TRANSPORT_BROADCAST,
10304            destination_type: constants::DESTINATION_PLAIN,
10305            packet_type: constants::PACKET_TYPE_DATA,
10306        };
10307        let packet =
10308            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"hello").unwrap();
10309
10310        tx.send(Event::SendOutbound {
10311            raw: packet.raw,
10312            dest_type: constants::DESTINATION_PLAIN,
10313            attached_interface: None,
10314        })
10315        .unwrap();
10316        tx.send(Event::Shutdown).unwrap();
10317        driver.run();
10318
10319        // PLAIN packet should be broadcast on all interfaces
10320        assert_eq!(sent.lock().unwrap().len(), 1);
10321    }
10322
10323    #[test]
10324    fn register_destination_and_deliver() {
10325        let (tx, rx) = event::channel();
10326        let (cbs, _, _, deliveries, _, _) = MockCallbacks::new();
10327        let mut driver = Driver::new(
10328            TransportConfig {
10329                transport_enabled: false,
10330                identity_hash: None,
10331                prefer_shorter_path: false,
10332                max_paths_per_destination: 1,
10333                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10334                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10335                max_path_destinations: usize::MAX,
10336                max_tunnel_destinations_total: usize::MAX,
10337                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10338                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10339                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10340                announce_sig_cache_enabled: true,
10341                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10342                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10343                announce_queue_max_entries: 256,
10344                announce_queue_max_interfaces: 1024,
10345            },
10346            rx,
10347            tx.clone(),
10348            Box::new(cbs),
10349        );
10350        let info = make_interface_info(1);
10351        driver.engine.register_interface(info);
10352        let (writer, _sent) = MockWriter::new();
10353        driver
10354            .interfaces
10355            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
10356
10357        let dest = [0xBB; 16];
10358
10359        // Register destination then send a data packet to it
10360        tx.send(Event::RegisterDestination {
10361            dest_hash: dest,
10362            dest_type: constants::DESTINATION_SINGLE,
10363        })
10364        .unwrap();
10365
10366        let flags = PacketFlags {
10367            header_type: constants::HEADER_1,
10368            context_flag: constants::FLAG_UNSET,
10369            transport_type: constants::TRANSPORT_BROADCAST,
10370            destination_type: constants::DESTINATION_SINGLE,
10371            packet_type: constants::PACKET_TYPE_DATA,
10372        };
10373        let packet =
10374            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"data").unwrap();
10375        tx.send(Event::Frame {
10376            interface_id: InterfaceId(1),
10377            data: packet.raw,
10378        })
10379        .unwrap();
10380        tx.send(Event::Shutdown).unwrap();
10381        driver.run();
10382
10383        assert_eq!(deliveries.lock().unwrap().len(), 1);
10384        assert_eq!(deliveries.lock().unwrap()[0], DestHash(dest));
10385    }
10386
10387    #[test]
10388    fn query_transport_identity() {
10389        let (tx, rx) = event::channel();
10390        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10391        let mut driver = Driver::new(
10392            TransportConfig {
10393                transport_enabled: true,
10394                identity_hash: Some([0xAA; 16]),
10395                prefer_shorter_path: false,
10396                max_paths_per_destination: 1,
10397                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10398                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10399                max_path_destinations: usize::MAX,
10400                max_tunnel_destinations_total: usize::MAX,
10401                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10402                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10403                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10404                announce_sig_cache_enabled: true,
10405                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10406                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10407                announce_queue_max_entries: 256,
10408                announce_queue_max_interfaces: 1024,
10409            },
10410            rx,
10411            tx.clone(),
10412            Box::new(cbs),
10413        );
10414
10415        let (resp_tx, resp_rx) = mpsc::channel();
10416        tx.send(Event::Query(QueryRequest::TransportIdentity, resp_tx))
10417            .unwrap();
10418        tx.send(Event::Shutdown).unwrap();
10419        driver.run();
10420
10421        match resp_rx.recv().unwrap() {
10422            QueryResponse::TransportIdentity(Some(hash)) => {
10423                assert_eq!(hash, [0xAA; 16]);
10424            }
10425            _ => panic!("unexpected response"),
10426        }
10427    }
10428
10429    #[test]
10430    fn query_link_count() {
10431        let (tx, rx) = event::channel();
10432        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10433        let mut driver = Driver::new(
10434            TransportConfig {
10435                transport_enabled: false,
10436                identity_hash: None,
10437                prefer_shorter_path: false,
10438                max_paths_per_destination: 1,
10439                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10440                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10441                max_path_destinations: usize::MAX,
10442                max_tunnel_destinations_total: usize::MAX,
10443                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10444                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10445                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10446                announce_sig_cache_enabled: true,
10447                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10448                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10449                announce_queue_max_entries: 256,
10450                announce_queue_max_interfaces: 1024,
10451            },
10452            rx,
10453            tx.clone(),
10454            Box::new(cbs),
10455        );
10456
10457        let (resp_tx, resp_rx) = mpsc::channel();
10458        tx.send(Event::Query(QueryRequest::LinkCount, resp_tx))
10459            .unwrap();
10460        tx.send(Event::Shutdown).unwrap();
10461        driver.run();
10462
10463        match resp_rx.recv().unwrap() {
10464            QueryResponse::LinkCount(count) => assert_eq!(count, 0),
10465            _ => panic!("unexpected response"),
10466        }
10467    }
10468
10469    #[test]
10470    fn query_rate_table() {
10471        let (tx, rx) = event::channel();
10472        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10473        let mut driver = Driver::new(
10474            TransportConfig {
10475                transport_enabled: false,
10476                identity_hash: None,
10477                prefer_shorter_path: false,
10478                max_paths_per_destination: 1,
10479                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10480                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10481                max_path_destinations: usize::MAX,
10482                max_tunnel_destinations_total: usize::MAX,
10483                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10484                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10485                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10486                announce_sig_cache_enabled: true,
10487                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10488                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10489                announce_queue_max_entries: 256,
10490                announce_queue_max_interfaces: 1024,
10491            },
10492            rx,
10493            tx.clone(),
10494            Box::new(cbs),
10495        );
10496
10497        let (resp_tx, resp_rx) = mpsc::channel();
10498        tx.send(Event::Query(QueryRequest::RateTable, resp_tx))
10499            .unwrap();
10500        tx.send(Event::Shutdown).unwrap();
10501        driver.run();
10502
10503        match resp_rx.recv().unwrap() {
10504            QueryResponse::RateTable(entries) => assert!(entries.is_empty()),
10505            _ => panic!("unexpected response"),
10506        }
10507    }
10508
10509    #[test]
10510    fn query_next_hop() {
10511        let (tx, rx) = event::channel();
10512        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10513        let mut driver = Driver::new(
10514            TransportConfig {
10515                transport_enabled: false,
10516                identity_hash: None,
10517                prefer_shorter_path: false,
10518                max_paths_per_destination: 1,
10519                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10520                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10521                max_path_destinations: usize::MAX,
10522                max_tunnel_destinations_total: usize::MAX,
10523                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10524                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10525                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10526                announce_sig_cache_enabled: true,
10527                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10528                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10529                announce_queue_max_entries: 256,
10530                announce_queue_max_interfaces: 1024,
10531            },
10532            rx,
10533            tx.clone(),
10534            Box::new(cbs),
10535        );
10536
10537        let dest = [0xBB; 16];
10538        let (resp_tx, resp_rx) = mpsc::channel();
10539        tx.send(Event::Query(
10540            QueryRequest::NextHop { dest_hash: dest },
10541            resp_tx,
10542        ))
10543        .unwrap();
10544        tx.send(Event::Shutdown).unwrap();
10545        driver.run();
10546
10547        match resp_rx.recv().unwrap() {
10548            QueryResponse::NextHop(None) => {}
10549            _ => panic!("unexpected response"),
10550        }
10551    }
10552
10553    #[test]
10554    fn query_next_hop_if_name() {
10555        let (tx, rx) = event::channel();
10556        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10557        let mut driver = Driver::new(
10558            TransportConfig {
10559                transport_enabled: false,
10560                identity_hash: None,
10561                prefer_shorter_path: false,
10562                max_paths_per_destination: 1,
10563                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10564                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10565                max_path_destinations: usize::MAX,
10566                max_tunnel_destinations_total: usize::MAX,
10567                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10568                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10569                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10570                announce_sig_cache_enabled: true,
10571                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10572                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10573                announce_queue_max_entries: 256,
10574                announce_queue_max_interfaces: 1024,
10575            },
10576            rx,
10577            tx.clone(),
10578            Box::new(cbs),
10579        );
10580
10581        let dest = [0xCC; 16];
10582        let (resp_tx, resp_rx) = mpsc::channel();
10583        tx.send(Event::Query(
10584            QueryRequest::NextHopIfName { dest_hash: dest },
10585            resp_tx,
10586        ))
10587        .unwrap();
10588        tx.send(Event::Shutdown).unwrap();
10589        driver.run();
10590
10591        match resp_rx.recv().unwrap() {
10592            QueryResponse::NextHopIfName(None) => {}
10593            _ => panic!("unexpected response"),
10594        }
10595    }
10596
10597    #[test]
10598    fn query_drop_all_via() {
10599        let (tx, rx) = event::channel();
10600        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10601        let mut driver = Driver::new(
10602            TransportConfig {
10603                transport_enabled: false,
10604                identity_hash: None,
10605                prefer_shorter_path: false,
10606                max_paths_per_destination: 1,
10607                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10608                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10609                max_path_destinations: usize::MAX,
10610                max_tunnel_destinations_total: usize::MAX,
10611                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10612                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10613                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10614                announce_sig_cache_enabled: true,
10615                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10616                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10617                announce_queue_max_entries: 256,
10618                announce_queue_max_interfaces: 1024,
10619            },
10620            rx,
10621            tx.clone(),
10622            Box::new(cbs),
10623        );
10624
10625        let transport = [0xDD; 16];
10626        let (resp_tx, resp_rx) = mpsc::channel();
10627        tx.send(Event::Query(
10628            QueryRequest::DropAllVia {
10629                transport_hash: transport,
10630            },
10631            resp_tx,
10632        ))
10633        .unwrap();
10634        tx.send(Event::Shutdown).unwrap();
10635        driver.run();
10636
10637        match resp_rx.recv().unwrap() {
10638            QueryResponse::DropAllVia(count) => assert_eq!(count, 0),
10639            _ => panic!("unexpected response"),
10640        }
10641    }
10642
10643    #[test]
10644    fn query_drop_announce_queues() {
10645        let (tx, rx) = event::channel();
10646        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10647        let mut driver = Driver::new(
10648            TransportConfig {
10649                transport_enabled: false,
10650                identity_hash: None,
10651                prefer_shorter_path: false,
10652                max_paths_per_destination: 1,
10653                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10654                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10655                max_path_destinations: usize::MAX,
10656                max_tunnel_destinations_total: usize::MAX,
10657                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10658                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10659                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10660                announce_sig_cache_enabled: true,
10661                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10662                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10663                announce_queue_max_entries: 256,
10664                announce_queue_max_interfaces: 1024,
10665            },
10666            rx,
10667            tx.clone(),
10668            Box::new(cbs),
10669        );
10670
10671        let (resp_tx, resp_rx) = mpsc::channel();
10672        tx.send(Event::Query(QueryRequest::DropAnnounceQueues, resp_tx))
10673            .unwrap();
10674        tx.send(Event::Shutdown).unwrap();
10675        driver.run();
10676
10677        match resp_rx.recv().unwrap() {
10678            QueryResponse::DropAnnounceQueues => {}
10679            _ => panic!("unexpected response"),
10680        }
10681    }
10682
10683    // =========================================================================
10684    // Phase 7e: Link wiring integration tests
10685    // =========================================================================
10686
10687    #[test]
10688    fn register_link_dest_event() {
10689        let (tx, rx) = event::channel();
10690        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10691        let mut driver = Driver::new(
10692            TransportConfig {
10693                transport_enabled: false,
10694                identity_hash: None,
10695                prefer_shorter_path: false,
10696                max_paths_per_destination: 1,
10697                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10698                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10699                max_path_destinations: usize::MAX,
10700                max_tunnel_destinations_total: usize::MAX,
10701                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10702                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10703                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10704                announce_sig_cache_enabled: true,
10705                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10706                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10707                announce_queue_max_entries: 256,
10708                announce_queue_max_interfaces: 1024,
10709            },
10710            rx,
10711            tx.clone(),
10712            Box::new(cbs),
10713        );
10714        let info = make_interface_info(1);
10715        driver.engine.register_interface(info);
10716        let (writer, _sent) = MockWriter::new();
10717        driver
10718            .interfaces
10719            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
10720
10721        let mut rng = OsRng;
10722        let sig_prv = rns_crypto::ed25519::Ed25519PrivateKey::generate(&mut rng);
10723        let sig_pub_bytes = sig_prv.public_key().public_bytes();
10724        let sig_prv_bytes = sig_prv.private_bytes();
10725        let dest_hash = [0xDD; 16];
10726
10727        tx.send(Event::RegisterLinkDestination {
10728            dest_hash,
10729            sig_prv_bytes,
10730            sig_pub_bytes,
10731            resource_strategy: 0,
10732        })
10733        .unwrap();
10734        tx.send(Event::Shutdown).unwrap();
10735        driver.run();
10736
10737        // Link manager should know about the destination
10738        assert!(driver.link_manager.is_link_destination(&dest_hash));
10739    }
10740
10741    #[test]
10742    fn create_link_event() {
10743        let (tx, rx) = event::channel();
10744        let (cbs, _link_established, _, _) = MockCallbacks::with_link_tracking();
10745        let mut driver = Driver::new(
10746            TransportConfig {
10747                transport_enabled: false,
10748                identity_hash: None,
10749                prefer_shorter_path: false,
10750                max_paths_per_destination: 1,
10751                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10752                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10753                max_path_destinations: usize::MAX,
10754                max_tunnel_destinations_total: usize::MAX,
10755                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10756                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10757                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10758                announce_sig_cache_enabled: true,
10759                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10760                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10761                announce_queue_max_entries: 256,
10762                announce_queue_max_interfaces: 1024,
10763            },
10764            rx,
10765            tx.clone(),
10766            Box::new(cbs),
10767        );
10768        let info = make_interface_info(1);
10769        driver.engine.register_interface(info);
10770        let (writer, _sent) = MockWriter::new();
10771        driver
10772            .interfaces
10773            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
10774
10775        let dest_hash = [0xDD; 16];
10776        let dummy_sig_pub = [0xAA; 32];
10777
10778        let (resp_tx, resp_rx) = mpsc::channel();
10779        tx.send(Event::CreateLink {
10780            dest_hash,
10781            dest_sig_pub_bytes: dummy_sig_pub,
10782            response_tx: resp_tx,
10783        })
10784        .unwrap();
10785        tx.send(Event::Shutdown).unwrap();
10786        driver.run();
10787
10788        // Should have received a link_id
10789        let link_id = resp_rx.recv().unwrap();
10790        assert_ne!(link_id, [0u8; 16]);
10791
10792        // Link should be in pending state in the manager
10793        assert_eq!(driver.link_manager.link_count(), 1);
10794
10795        // The LINKREQUEST packet won't be sent on the wire without a path
10796        // to the destination (DESTINATION_LINK requires a known path or
10797        // attached_interface). In a real scenario, the path would exist from
10798        // an announce received earlier.
10799    }
10800
10801    #[test]
10802    fn deliver_local_routes_to_link_manager() {
10803        // Verify that DeliverLocal for a registered link destination goes to
10804        // the link manager instead of the callbacks.
10805        let (tx, rx) = event::channel();
10806        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10807        let mut driver = Driver::new(
10808            TransportConfig {
10809                transport_enabled: false,
10810                identity_hash: None,
10811                prefer_shorter_path: false,
10812                max_paths_per_destination: 1,
10813                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10814                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10815                max_path_destinations: usize::MAX,
10816                max_tunnel_destinations_total: usize::MAX,
10817                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10818                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10819                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10820                announce_sig_cache_enabled: true,
10821                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10822                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10823                announce_queue_max_entries: 256,
10824                announce_queue_max_interfaces: 1024,
10825            },
10826            rx,
10827            tx.clone(),
10828            Box::new(cbs),
10829        );
10830        let info = make_interface_info(1);
10831        driver.engine.register_interface(info);
10832        let (writer, _sent) = MockWriter::new();
10833        driver
10834            .interfaces
10835            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
10836
10837        // Register a link destination
10838        let mut rng = OsRng;
10839        let sig_prv = rns_crypto::ed25519::Ed25519PrivateKey::generate(&mut rng);
10840        let sig_pub_bytes = sig_prv.public_key().public_bytes();
10841        let dest_hash = [0xEE; 16];
10842        driver.link_manager.register_link_destination(
10843            dest_hash,
10844            sig_prv,
10845            sig_pub_bytes,
10846            crate::link_manager::ResourceStrategy::AcceptNone,
10847        );
10848
10849        // dispatch_all with a DeliverLocal for that dest should route to link_manager
10850        // (not to callbacks). We can't easily test this via run() since we need
10851        // a valid LINKREQUEST, but we can check is_link_destination works.
10852        assert!(driver.link_manager.is_link_destination(&dest_hash));
10853
10854        // Non-link destination should go to callbacks
10855        assert!(!driver.link_manager.is_link_destination(&[0xFF; 16]));
10856
10857        drop(tx);
10858    }
10859
10860    #[test]
10861    fn teardown_link_event() {
10862        let (tx, rx) = event::channel();
10863        let (cbs, _, link_closed, _) = MockCallbacks::with_link_tracking();
10864        let mut driver = Driver::new(
10865            TransportConfig {
10866                transport_enabled: false,
10867                identity_hash: None,
10868                prefer_shorter_path: false,
10869                max_paths_per_destination: 1,
10870                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10871                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10872                max_path_destinations: usize::MAX,
10873                max_tunnel_destinations_total: usize::MAX,
10874                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10875                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10876                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10877                announce_sig_cache_enabled: true,
10878                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10879                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10880                announce_queue_max_entries: 256,
10881                announce_queue_max_interfaces: 1024,
10882            },
10883            rx,
10884            tx.clone(),
10885            Box::new(cbs),
10886        );
10887        let info = make_interface_info(1);
10888        driver.engine.register_interface(info);
10889        let (writer, _sent) = MockWriter::new();
10890        driver
10891            .interfaces
10892            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
10893
10894        // Create a link first
10895        let (resp_tx, resp_rx) = mpsc::channel();
10896        tx.send(Event::CreateLink {
10897            dest_hash: [0xDD; 16],
10898            dest_sig_pub_bytes: [0xAA; 32],
10899            response_tx: resp_tx,
10900        })
10901        .unwrap();
10902        // Then tear it down
10903        // We can't receive resp_rx yet since driver.run() hasn't started,
10904        // but we know the link_id will be created. Send teardown after CreateLink.
10905        // Actually, we need to get the link_id first. Let's use a two-phase approach.
10906        tx.send(Event::Shutdown).unwrap();
10907        driver.run();
10908
10909        let link_id = resp_rx.recv().unwrap();
10910        assert_ne!(link_id, [0u8; 16]);
10911        assert_eq!(driver.link_manager.link_count(), 1);
10912
10913        // Now restart with same driver (just use events directly since driver loop exited)
10914        let teardown_actions = driver.link_manager.teardown_link(&link_id);
10915        driver.dispatch_link_actions(teardown_actions);
10916
10917        // Callback should have been called
10918        assert_eq!(link_closed.lock().unwrap().len(), 1);
10919        assert_eq!(link_closed.lock().unwrap()[0], TypedLinkId(link_id));
10920    }
10921
10922    #[test]
10923    fn link_count_includes_link_manager() {
10924        let (tx, rx) = event::channel();
10925        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10926        let mut driver = Driver::new(
10927            TransportConfig {
10928                transport_enabled: false,
10929                identity_hash: None,
10930                prefer_shorter_path: false,
10931                max_paths_per_destination: 1,
10932                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10933                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10934                max_path_destinations: usize::MAX,
10935                max_tunnel_destinations_total: usize::MAX,
10936                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10937                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10938                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10939                announce_sig_cache_enabled: true,
10940                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10941                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
10942                announce_queue_max_entries: 256,
10943                announce_queue_max_interfaces: 1024,
10944            },
10945            rx,
10946            tx.clone(),
10947            Box::new(cbs),
10948        );
10949        let info = make_interface_info(1);
10950        driver.engine.register_interface(info);
10951        let (writer, _sent) = MockWriter::new();
10952        driver
10953            .interfaces
10954            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
10955
10956        // Create a link via link_manager directly
10957        let mut rng = OsRng;
10958        let dummy_sig = [0xAA; 32];
10959        driver.link_manager.create_link(
10960            &[0xDD; 16],
10961            &dummy_sig,
10962            1,
10963            constants::MTU as u32,
10964            &mut rng,
10965        );
10966
10967        // Query link count — should include link_manager links
10968        let (resp_tx, resp_rx) = mpsc::channel();
10969        tx.send(Event::Query(QueryRequest::LinkCount, resp_tx))
10970            .unwrap();
10971        tx.send(Event::Shutdown).unwrap();
10972        driver.run();
10973
10974        match resp_rx.recv().unwrap() {
10975            QueryResponse::LinkCount(count) => assert_eq!(count, 1),
10976            _ => panic!("unexpected response"),
10977        }
10978    }
10979
10980    #[test]
10981    fn register_request_handler_event() {
10982        let (tx, rx) = event::channel();
10983        let (cbs, _, _, _, _, _) = MockCallbacks::new();
10984        let mut driver = Driver::new(
10985            TransportConfig {
10986                transport_enabled: false,
10987                identity_hash: None,
10988                prefer_shorter_path: false,
10989                max_paths_per_destination: 1,
10990                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
10991                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
10992                max_path_destinations: usize::MAX,
10993                max_tunnel_destinations_total: usize::MAX,
10994                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
10995                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
10996                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
10997                announce_sig_cache_enabled: true,
10998                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
10999                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
11000                announce_queue_max_entries: 256,
11001                announce_queue_max_interfaces: 1024,
11002            },
11003            rx,
11004            tx.clone(),
11005            Box::new(cbs),
11006        );
11007
11008        tx.send(Event::RegisterRequestHandler {
11009            path: "/status".to_string(),
11010            allowed_list: None,
11011            handler: Box::new(|_link_id, _path, _data, _remote| Some(b"OK".to_vec())),
11012        })
11013        .unwrap();
11014        tx.send(Event::Shutdown).unwrap();
11015        driver.run();
11016
11017        // Handler should be registered (we can't directly query the count,
11018        // but at least verify no crash)
11019    }
11020
11021    // Phase 8c: Management announce timing tests
11022
11023    #[test]
11024    fn management_announces_emitted_after_delay() {
11025        let (tx, rx) = event::channel();
11026        let (cbs, _announces, _, _, _, _) = MockCallbacks::new();
11027        let identity = Identity::new(&mut OsRng);
11028        let identity_hash = *identity.hash();
11029        let mut driver = Driver::new(
11030            TransportConfig {
11031                transport_enabled: true,
11032                identity_hash: Some(identity_hash),
11033                prefer_shorter_path: false,
11034                max_paths_per_destination: 1,
11035                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
11036                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
11037                max_path_destinations: usize::MAX,
11038                max_tunnel_destinations_total: usize::MAX,
11039                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
11040                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
11041                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
11042                announce_sig_cache_enabled: true,
11043                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
11044                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
11045                announce_queue_max_entries: 256,
11046                announce_queue_max_interfaces: 1024,
11047            },
11048            rx,
11049            tx.clone(),
11050            Box::new(cbs),
11051        );
11052
11053        // Register interface so announces can be sent
11054        let info = make_interface_info(1);
11055        driver.engine.register_interface(info.clone());
11056        let (writer, sent) = MockWriter::new();
11057        driver
11058            .interfaces
11059            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
11060
11061        // Enable management announces
11062        driver.management_config.enable_remote_management = true;
11063        driver.transport_identity = Some(identity);
11064
11065        // Set started time to 10 seconds ago so the 5s delay has passed
11066        driver.started = time::now() - 10.0;
11067
11068        // Send Tick then Shutdown
11069        tx.send(Event::Tick).unwrap();
11070        tx.send(Event::Shutdown).unwrap();
11071        driver.run();
11072
11073        // Should have sent at least one packet (the management announce)
11074        let sent_packets = sent.lock().unwrap();
11075        assert!(
11076            !sent_packets.is_empty(),
11077            "Management announce should be sent after startup delay"
11078        );
11079    }
11080
11081    #[test]
11082    fn runtime_config_list_contains_global_keys() {
11083        let driver = new_test_driver();
11084        let response = driver.handle_query(QueryRequest::ListRuntimeConfig);
11085        let QueryResponse::RuntimeConfigList(entries) = response else {
11086            panic!("expected runtime config list");
11087        };
11088        let keys: Vec<String> = entries.into_iter().map(|entry| entry.key).collect();
11089        assert!(keys.contains(&"global.tick_interval_ms".to_string()));
11090        assert!(keys.contains(&"global.known_destinations_ttl_secs".to_string()));
11091        assert!(keys.contains(&"global.rate_limiter_ttl_secs".to_string()));
11092        assert!(keys.contains(&"global.direct_connect_policy".to_string()));
11093    }
11094
11095    #[test]
11096    fn runtime_config_set_and_reset_tick_interval() {
11097        let mut driver = new_test_driver();
11098
11099        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11100            key: "global.tick_interval_ms".into(),
11101            value: RuntimeConfigValue::Int(250),
11102        });
11103        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11104            panic!("expected runtime config set success");
11105        };
11106        assert_eq!(entry.key, "global.tick_interval_ms");
11107        assert_eq!(entry.value, RuntimeConfigValue::Int(250));
11108        assert_eq!(driver.tick_interval_ms.load(Ordering::Relaxed), 250);
11109
11110        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
11111            key: "global.tick_interval_ms".into(),
11112        });
11113        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
11114            panic!("expected runtime config reset success");
11115        };
11116        assert_eq!(entry.value, RuntimeConfigValue::Int(1000));
11117        assert_eq!(driver.tick_interval_ms.load(Ordering::Relaxed), 1000);
11118    }
11119
11120    #[test]
11121    fn runtime_config_rejects_invalid_policy() {
11122        let mut driver = new_test_driver();
11123        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11124            key: "global.direct_connect_policy".into(),
11125            value: RuntimeConfigValue::String("bogus".into()),
11126        });
11127        let QueryResponse::RuntimeConfigSet(Err(err)) = response else {
11128            panic!("expected runtime config set failure");
11129        };
11130        assert_eq!(err.code, RuntimeConfigErrorCode::InvalidValue);
11131    }
11132
11133    #[test]
11134    fn runtime_config_set_and_reset_rate_limiter_ttl() {
11135        let mut driver = new_test_driver();
11136
11137        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11138            key: "global.rate_limiter_ttl_secs".into(),
11139            value: RuntimeConfigValue::Float(600.0),
11140        });
11141        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11142            panic!("expected runtime config set success");
11143        };
11144        assert_eq!(entry.value, RuntimeConfigValue::Float(600.0));
11145        assert_eq!(driver.rate_limiter_ttl_secs, 600.0);
11146
11147        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
11148            key: "global.rate_limiter_ttl_secs".into(),
11149        });
11150        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
11151            panic!("expected runtime config reset success");
11152        };
11153        assert_eq!(
11154            entry.value,
11155            RuntimeConfigValue::Float(DEFAULT_RATE_LIMITER_TTL_SECS)
11156        );
11157        assert_eq!(driver.rate_limiter_ttl_secs, DEFAULT_RATE_LIMITER_TTL_SECS);
11158    }
11159
11160    #[cfg(feature = "iface-backbone")]
11161    #[test]
11162    fn runtime_config_lists_backbone_keys() {
11163        let mut driver = new_test_driver();
11164        register_test_backbone(&mut driver, "public");
11165        register_test_backbone_client(&mut driver, "uplink");
11166        register_test_backbone_discovery(&mut driver, "public", false);
11167        let response = driver.handle_query(QueryRequest::ListRuntimeConfig);
11168        let QueryResponse::RuntimeConfigList(entries) = response else {
11169            panic!("expected runtime config list");
11170        };
11171        let keys: Vec<String> = entries.into_iter().map(|entry| entry.key).collect();
11172        assert!(keys.contains(&"backbone.public.idle_timeout_secs".to_string()));
11173        assert!(keys.contains(&"backbone.public.write_stall_timeout_secs".to_string()));
11174        assert!(keys.contains(&"backbone.public.max_connections".to_string()));
11175        assert!(keys.contains(&"backbone.public.discoverable".to_string()));
11176        assert!(keys.contains(&"backbone.public.discovery_name".to_string()));
11177        assert!(keys.contains(&"backbone.public.latitude".to_string()));
11178        assert!(keys.contains(&"backbone.public.longitude".to_string()));
11179        assert!(keys.contains(&"backbone.public.height".to_string()));
11180        assert!(keys.contains(&"backbone_client.uplink.connect_timeout_secs".to_string()));
11181        assert!(keys.contains(&"backbone_client.uplink.reconnect_wait_secs".to_string()));
11182        assert!(keys.contains(&"backbone_client.uplink.max_reconnect_tries".to_string()));
11183    }
11184
11185    #[cfg(feature = "iface-backbone")]
11186    #[test]
11187    fn runtime_config_sets_backbone_values() {
11188        let mut driver = new_test_driver();
11189        register_test_backbone(&mut driver, "public");
11190        register_test_backbone_discovery(&mut driver, "public", false);
11191        driver.transport_identity = Some(rns_crypto::identity::Identity::new(&mut OsRng));
11192
11193        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11194            key: "backbone.public.idle_timeout_secs".into(),
11195            value: RuntimeConfigValue::Float(2.5),
11196        });
11197        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11198            panic!("expected runtime config set success");
11199        };
11200        assert_eq!(entry.value, RuntimeConfigValue::Float(2.5));
11201
11202        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11203            key: "backbone.public.write_stall_timeout_secs".into(),
11204            value: RuntimeConfigValue::Float(15.0),
11205        });
11206        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11207            panic!("expected runtime config set success");
11208        };
11209        assert_eq!(entry.value, RuntimeConfigValue::Float(15.0));
11210
11211        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11212            key: "backbone.public.max_connections".into(),
11213            value: RuntimeConfigValue::Int(0),
11214        });
11215        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11216            panic!("expected runtime config set success");
11217        };
11218        assert_eq!(entry.value, RuntimeConfigValue::Int(0));
11219
11220        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
11221            key: "backbone.public.max_connections".into(),
11222        });
11223        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
11224            panic!("expected runtime config reset success");
11225        };
11226        assert_eq!(entry.value, RuntimeConfigValue::Int(8));
11227
11228        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
11229            key: "backbone.public.write_stall_timeout_secs".into(),
11230        });
11231        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
11232            panic!("expected runtime config reset success");
11233        };
11234        assert_eq!(entry.value, RuntimeConfigValue::Float(30.0));
11235
11236        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11237            key: "backbone.public.discoverable".into(),
11238            value: RuntimeConfigValue::Bool(true),
11239        });
11240        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11241            panic!("expected runtime config set success");
11242        };
11243        assert_eq!(entry.value, RuntimeConfigValue::Bool(true));
11244        assert!(driver
11245            .interface_announcer
11246            .as_ref()
11247            .map(|announcer| announcer.contains_interface("public"))
11248            .unwrap_or(false));
11249
11250        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11251            key: "backbone.public.discovery_name".into(),
11252            value: RuntimeConfigValue::String("Public Backbone".into()),
11253        });
11254        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11255            panic!("expected runtime config set success");
11256        };
11257        assert_eq!(
11258            entry.value,
11259            RuntimeConfigValue::String("Public Backbone".into())
11260        );
11261
11262        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11263            key: "backbone.public.latitude".into(),
11264            value: RuntimeConfigValue::Float(45.4642),
11265        });
11266        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11267            panic!("expected runtime config set success");
11268        };
11269        assert_eq!(entry.value, RuntimeConfigValue::Float(45.4642));
11270
11271        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11272            key: "backbone.public.longitude".into(),
11273            value: RuntimeConfigValue::Float(9.19),
11274        });
11275        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11276            panic!("expected runtime config set success");
11277        };
11278        assert_eq!(entry.value, RuntimeConfigValue::Float(9.19));
11279
11280        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11281            key: "backbone.public.height".into(),
11282            value: RuntimeConfigValue::Int(120),
11283        });
11284        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11285            panic!("expected runtime config set success");
11286        };
11287        assert_eq!(entry.value, RuntimeConfigValue::Float(120.0));
11288
11289        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
11290            key: "backbone.public.discoverable".into(),
11291        });
11292        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
11293            panic!("expected runtime config reset success");
11294        };
11295        assert_eq!(entry.value, RuntimeConfigValue::Bool(false));
11296        assert!(driver.interface_announcer.is_none());
11297
11298        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
11299            key: "backbone.public.latitude".into(),
11300        });
11301        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
11302            panic!("expected runtime config reset success");
11303        };
11304        assert_eq!(entry.value, RuntimeConfigValue::Null);
11305    }
11306
11307    #[cfg(feature = "iface-backbone")]
11308    #[test]
11309    fn runtime_config_sets_backbone_client_values() {
11310        let mut driver = new_test_driver();
11311        register_test_backbone_client(&mut driver, "uplink");
11312
11313        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11314            key: "backbone_client.uplink.connect_timeout_secs".into(),
11315            value: RuntimeConfigValue::Float(2.5),
11316        });
11317        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11318            panic!("expected runtime config set success");
11319        };
11320        assert_eq!(entry.value, RuntimeConfigValue::Float(2.5));
11321
11322        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11323            key: "backbone_client.uplink.max_reconnect_tries".into(),
11324            value: RuntimeConfigValue::Int(0),
11325        });
11326        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11327            panic!("expected runtime config set success");
11328        };
11329        assert_eq!(entry.value, RuntimeConfigValue::Int(0));
11330
11331        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
11332            key: "backbone_client.uplink.connect_timeout_secs".into(),
11333        });
11334        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
11335            panic!("expected runtime config reset success");
11336        };
11337        assert_eq!(entry.value, RuntimeConfigValue::Float(5.0));
11338    }
11339
11340    #[cfg(feature = "iface-backbone")]
11341    #[test]
11342    fn backbone_peer_state_query_lists_entries() {
11343        let mut driver = new_test_driver();
11344        register_test_backbone(&mut driver, "public");
11345        driver
11346            .backbone_peer_state
11347            .get("public")
11348            .unwrap()
11349            .peer_state
11350            .lock()
11351            .unwrap()
11352            .seed_entry(BackbonePeerStateEntry {
11353                interface_name: "public".into(),
11354                peer_ip: "203.0.113.10".parse().unwrap(),
11355                connected_count: 1,
11356                blacklisted_remaining_secs: Some(120.0),
11357                blacklist_reason: Some("repeated idle timeouts".into()),
11358                reject_count: 7,
11359            });
11360
11361        let response = driver.handle_query(QueryRequest::BackbonePeerState {
11362            interface_name: Some("public".into()),
11363        });
11364        let QueryResponse::BackbonePeerState(entries) = response else {
11365            panic!("expected backbone peer state list");
11366        };
11367        assert_eq!(entries.len(), 1);
11368        assert_eq!(entries[0].peer_ip.to_string(), "203.0.113.10");
11369        assert_eq!(entries[0].connected_count, 1);
11370        assert_eq!(entries[0].reject_count, 7);
11371        assert_eq!(
11372            entries[0].blacklist_reason.as_deref(),
11373            Some("repeated idle timeouts")
11374        );
11375        assert!(entries[0].blacklisted_remaining_secs.unwrap() > 0.0);
11376    }
11377
11378    #[cfg(feature = "iface-backbone")]
11379    #[test]
11380    fn backbone_peer_state_clear_removes_entry() {
11381        let mut driver = new_test_driver();
11382        register_test_backbone(&mut driver, "public");
11383        driver
11384            .backbone_peer_state
11385            .get("public")
11386            .unwrap()
11387            .peer_state
11388            .lock()
11389            .unwrap()
11390            .seed_entry(BackbonePeerStateEntry {
11391                interface_name: "public".into(),
11392                peer_ip: "203.0.113.11".parse().unwrap(),
11393                connected_count: 0,
11394                blacklisted_remaining_secs: None,
11395                blacklist_reason: None,
11396                reject_count: 0,
11397            });
11398
11399        let response = driver.handle_query_mut(QueryRequest::ClearBackbonePeerState {
11400            interface_name: "public".into(),
11401            peer_ip: "203.0.113.11".parse().unwrap(),
11402        });
11403        let QueryResponse::ClearBackbonePeerState(true) = response else {
11404            panic!("expected successful peer-state clear");
11405        };
11406
11407        let response = driver.handle_query(QueryRequest::BackbonePeerState {
11408            interface_name: Some("public".into()),
11409        });
11410        let QueryResponse::BackbonePeerState(entries) = response else {
11411            panic!("expected backbone peer state list");
11412        };
11413        assert!(entries.is_empty());
11414    }
11415
11416    #[cfg(feature = "iface-backbone")]
11417    #[test]
11418    fn backbone_peer_blacklist_sets_blacklist() {
11419        let mut driver = new_test_driver();
11420        register_test_backbone(&mut driver, "public");
11421        driver
11422            .backbone_peer_state
11423            .get("public")
11424            .unwrap()
11425            .peer_state
11426            .lock()
11427            .unwrap()
11428            .seed_entry(BackbonePeerStateEntry {
11429                interface_name: "public".into(),
11430                peer_ip: "203.0.113.50".parse().unwrap(),
11431                connected_count: 1,
11432                blacklisted_remaining_secs: None,
11433                blacklist_reason: None,
11434                reject_count: 0,
11435            });
11436
11437        let response = driver.handle_query_mut(QueryRequest::BlacklistBackbonePeer {
11438            interface_name: "public".into(),
11439            peer_ip: "203.0.113.50".parse().unwrap(),
11440            duration: Duration::from_secs(300),
11441            reason: "sentinel blacklist".into(),
11442            penalty_level: 2,
11443        });
11444        let QueryResponse::BlacklistBackbonePeer(true) = response else {
11445            panic!("expected successful blacklist");
11446        };
11447
11448        // Verify the peer is now blacklisted
11449        let response = driver.handle_query(QueryRequest::BackbonePeerState {
11450            interface_name: Some("public".into()),
11451        });
11452        let QueryResponse::BackbonePeerState(entries) = response else {
11453            panic!("expected backbone peer state list");
11454        };
11455        let entry = entries
11456            .iter()
11457            .find(|e| e.peer_ip == "203.0.113.50".parse::<std::net::IpAddr>().unwrap())
11458            .expect("expected entry for blacklisted peer");
11459        assert!(entry.blacklisted_remaining_secs.is_some());
11460        let remaining = entry.blacklisted_remaining_secs.unwrap();
11461        assert!(remaining > 290.0 && remaining <= 300.0);
11462        assert_eq!(
11463            entry.blacklist_reason.as_deref(),
11464            Some("sentinel blacklist")
11465        );
11466    }
11467
11468    #[cfg(feature = "iface-backbone")]
11469    #[test]
11470    fn backbone_peer_blacklist_unknown_interface_returns_false() {
11471        let mut driver = new_test_driver();
11472        let response = driver.handle_query_mut(QueryRequest::BlacklistBackbonePeer {
11473            interface_name: "nonexistent".into(),
11474            peer_ip: "203.0.113.50".parse().unwrap(),
11475            duration: Duration::from_secs(60),
11476            reason: "sentinel blacklist".into(),
11477            penalty_level: 1,
11478        });
11479        let QueryResponse::BlacklistBackbonePeer(false) = response else {
11480            panic!("expected false for unknown interface");
11481        };
11482    }
11483
11484    #[cfg(feature = "iface-backbone")]
11485    #[test]
11486    fn backbone_peer_blacklist_creates_entry_for_unknown_ip() {
11487        let mut driver = new_test_driver();
11488        register_test_backbone(&mut driver, "public");
11489
11490        // Blacklist an IP that has no existing peer state
11491        let response = driver.handle_query_mut(QueryRequest::BlacklistBackbonePeer {
11492            interface_name: "public".into(),
11493            peer_ip: "198.51.100.1".parse().unwrap(),
11494            duration: Duration::from_secs(120),
11495            reason: "sentinel blacklist".into(),
11496            penalty_level: 1,
11497        });
11498        let QueryResponse::BlacklistBackbonePeer(true) = response else {
11499            panic!("expected successful blacklist for new IP");
11500        };
11501
11502        let response = driver.handle_query(QueryRequest::BackbonePeerState {
11503            interface_name: Some("public".into()),
11504        });
11505        let QueryResponse::BackbonePeerState(entries) = response else {
11506            panic!("expected backbone peer state list");
11507        };
11508        let entry = entries
11509            .iter()
11510            .find(|e| e.peer_ip == "198.51.100.1".parse::<std::net::IpAddr>().unwrap())
11511            .expect("expected entry for newly blacklisted IP");
11512        assert!(entry.blacklisted_remaining_secs.is_some());
11513    }
11514
11515    #[cfg(feature = "iface-tcp")]
11516    #[test]
11517    fn runtime_config_lists_tcp_server_keys() {
11518        let mut driver = new_test_driver();
11519        register_test_tcp_server(&mut driver, "public");
11520        register_test_tcp_server_discovery(&mut driver, "public", false);
11521        let response = driver.handle_query(QueryRequest::ListRuntimeConfig);
11522        let QueryResponse::RuntimeConfigList(entries) = response else {
11523            panic!("expected runtime config list");
11524        };
11525        let keys: Vec<String> = entries.into_iter().map(|entry| entry.key).collect();
11526        assert!(keys.contains(&"tcp_server.public.max_connections".to_string()));
11527        assert!(keys.contains(&"tcp_server.public.discoverable".to_string()));
11528        assert!(keys.contains(&"tcp_server.public.discovery_name".to_string()));
11529    }
11530
11531    #[cfg(feature = "iface-tcp")]
11532    #[test]
11533    fn runtime_config_sets_tcp_server_values() {
11534        let mut driver = new_test_driver();
11535        register_test_tcp_server(&mut driver, "public");
11536        register_test_tcp_server_discovery(&mut driver, "public", false);
11537        driver.transport_identity = Some(rns_crypto::identity::Identity::new(&mut OsRng));
11538
11539        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11540            key: "tcp_server.public.max_connections".into(),
11541            value: RuntimeConfigValue::Int(0),
11542        });
11543        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11544            panic!("expected runtime config set success");
11545        };
11546        assert_eq!(entry.value, RuntimeConfigValue::Int(0));
11547
11548        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
11549            key: "tcp_server.public.max_connections".into(),
11550        });
11551        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
11552            panic!("expected runtime config reset success");
11553        };
11554        assert_eq!(entry.value, RuntimeConfigValue::Int(4));
11555
11556        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11557            key: "tcp_server.public.discoverable".into(),
11558            value: RuntimeConfigValue::Bool(true),
11559        });
11560        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11561            panic!("expected runtime config set success");
11562        };
11563        assert_eq!(entry.value, RuntimeConfigValue::Bool(true));
11564
11565        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11566            key: "tcp_server.public.latitude".into(),
11567            value: RuntimeConfigValue::Float(41.9028),
11568        });
11569        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11570            panic!("expected runtime config set success");
11571        };
11572        assert_eq!(entry.value, RuntimeConfigValue::Float(41.9028));
11573
11574        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
11575            key: "tcp_server.public.latitude".into(),
11576        });
11577        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
11578            panic!("expected runtime config reset success");
11579        };
11580        assert_eq!(entry.value, RuntimeConfigValue::Null);
11581    }
11582
11583    #[cfg(feature = "iface-tcp")]
11584    #[test]
11585    fn runtime_config_lists_tcp_client_keys() {
11586        let mut driver = new_test_driver();
11587        register_test_tcp_client(&mut driver, "uplink");
11588        let response = driver.handle_query(QueryRequest::ListRuntimeConfig);
11589        let QueryResponse::RuntimeConfigList(entries) = response else {
11590            panic!("expected runtime config list");
11591        };
11592        let keys: Vec<String> = entries.into_iter().map(|entry| entry.key).collect();
11593        assert!(keys.contains(&"tcp_client.uplink.connect_timeout_secs".to_string()));
11594        assert!(keys.contains(&"tcp_client.uplink.reconnect_wait_secs".to_string()));
11595        assert!(keys.contains(&"tcp_client.uplink.max_reconnect_tries".to_string()));
11596    }
11597
11598    #[cfg(feature = "iface-tcp")]
11599    #[test]
11600    fn runtime_config_sets_tcp_client_values() {
11601        let mut driver = new_test_driver();
11602        register_test_tcp_client(&mut driver, "uplink");
11603
11604        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11605            key: "tcp_client.uplink.connect_timeout_secs".into(),
11606            value: RuntimeConfigValue::Float(2.5),
11607        });
11608        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11609            panic!("expected runtime config set success");
11610        };
11611        assert_eq!(entry.value, RuntimeConfigValue::Float(2.5));
11612
11613        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11614            key: "tcp_client.uplink.max_reconnect_tries".into(),
11615            value: RuntimeConfigValue::Int(0),
11616        });
11617        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11618            panic!("expected runtime config set success");
11619        };
11620        assert_eq!(entry.value, RuntimeConfigValue::Int(0));
11621
11622        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
11623            key: "tcp_client.uplink.connect_timeout_secs".into(),
11624        });
11625        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
11626            panic!("expected runtime config reset success");
11627        };
11628        assert_eq!(entry.value, RuntimeConfigValue::Float(5.0));
11629    }
11630
11631    #[cfg(feature = "iface-udp")]
11632    #[test]
11633    fn runtime_config_lists_udp_keys() {
11634        let mut driver = new_test_driver();
11635        register_test_udp(&mut driver, "lan");
11636        let response = driver.handle_query(QueryRequest::ListRuntimeConfig);
11637        let QueryResponse::RuntimeConfigList(entries) = response else {
11638            panic!("expected runtime config list");
11639        };
11640        let keys: Vec<String> = entries.into_iter().map(|entry| entry.key).collect();
11641        assert!(keys.contains(&"udp.lan.forward_ip".to_string()));
11642        assert!(keys.contains(&"udp.lan.forward_port".to_string()));
11643    }
11644
11645    #[cfg(feature = "iface-udp")]
11646    #[test]
11647    fn runtime_config_sets_udp_values() {
11648        let mut driver = new_test_driver();
11649        register_test_udp(&mut driver, "lan");
11650
11651        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11652            key: "udp.lan.forward_ip".into(),
11653            value: RuntimeConfigValue::String("192.168.1.10".into()),
11654        });
11655        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11656            panic!("expected set ok");
11657        };
11658        assert_eq!(
11659            entry.value,
11660            RuntimeConfigValue::String("192.168.1.10".into())
11661        );
11662
11663        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11664            key: "udp.lan.forward_port".into(),
11665            value: RuntimeConfigValue::Null,
11666        });
11667        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11668            panic!("expected set ok");
11669        };
11670        assert_eq!(entry.value, RuntimeConfigValue::Null);
11671
11672        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
11673            key: "udp.lan.forward_port".into(),
11674        });
11675        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
11676            panic!("expected reset ok");
11677        };
11678        assert_eq!(entry.value, RuntimeConfigValue::Int(4242));
11679    }
11680
11681    #[cfg(feature = "iface-auto")]
11682    #[test]
11683    fn runtime_config_lists_auto_keys() {
11684        let mut driver = new_test_driver();
11685        register_test_auto(&mut driver, "lan");
11686        let response = driver.handle_query(QueryRequest::ListRuntimeConfig);
11687        let QueryResponse::RuntimeConfigList(entries) = response else {
11688            panic!("expected runtime config list");
11689        };
11690        let keys: Vec<String> = entries.into_iter().map(|entry| entry.key).collect();
11691        assert!(keys.contains(&"auto.lan.announce_interval_secs".to_string()));
11692        assert!(keys.contains(&"auto.lan.peer_timeout_secs".to_string()));
11693        assert!(keys.contains(&"auto.lan.peer_job_interval_secs".to_string()));
11694    }
11695
11696    #[cfg(feature = "iface-auto")]
11697    #[test]
11698    fn runtime_config_sets_auto_values() {
11699        let mut driver = new_test_driver();
11700        register_test_auto(&mut driver, "lan");
11701
11702        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11703            key: "auto.lan.announce_interval_secs".into(),
11704            value: RuntimeConfigValue::Float(2.5),
11705        });
11706        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11707            panic!("expected set ok");
11708        };
11709        assert_eq!(entry.value, RuntimeConfigValue::Float(2.5));
11710
11711        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11712            key: "auto.lan.peer_timeout_secs".into(),
11713            value: RuntimeConfigValue::Float(30.0),
11714        });
11715        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11716            panic!("expected set ok");
11717        };
11718        assert_eq!(entry.value, RuntimeConfigValue::Float(30.0));
11719
11720        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
11721            key: "auto.lan.peer_job_interval_secs".into(),
11722        });
11723        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
11724            panic!("expected reset ok");
11725        };
11726        assert_eq!(entry.value, RuntimeConfigValue::Float(4.0));
11727    }
11728
11729    #[cfg(feature = "iface-i2p")]
11730    #[test]
11731    fn runtime_config_lists_i2p_keys() {
11732        let mut driver = new_test_driver();
11733        register_test_i2p(&mut driver, "anon");
11734        let response = driver.handle_query(QueryRequest::ListRuntimeConfig);
11735        let QueryResponse::RuntimeConfigList(entries) = response else {
11736            panic!("expected runtime config list");
11737        };
11738        let keys: Vec<String> = entries.into_iter().map(|entry| entry.key).collect();
11739        assert!(keys.contains(&"i2p.anon.reconnect_wait_secs".to_string()));
11740    }
11741
11742    #[cfg(feature = "iface-i2p")]
11743    #[test]
11744    fn runtime_config_sets_i2p_values() {
11745        let mut driver = new_test_driver();
11746        register_test_i2p(&mut driver, "anon");
11747
11748        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11749            key: "i2p.anon.reconnect_wait_secs".into(),
11750            value: RuntimeConfigValue::Float(3.5),
11751        });
11752        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11753            panic!("expected set ok");
11754        };
11755        assert_eq!(entry.value, RuntimeConfigValue::Float(3.5));
11756
11757        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
11758            key: "i2p.anon.reconnect_wait_secs".into(),
11759        });
11760        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
11761            panic!("expected reset ok");
11762        };
11763        assert_eq!(entry.value, RuntimeConfigValue::Float(15.0));
11764    }
11765
11766    #[cfg(feature = "iface-pipe")]
11767    #[test]
11768    fn runtime_config_lists_pipe_keys() {
11769        let mut driver = new_test_driver();
11770        register_test_pipe(&mut driver, "worker");
11771        let response = driver.handle_query(QueryRequest::ListRuntimeConfig);
11772        let QueryResponse::RuntimeConfigList(entries) = response else {
11773            panic!("expected runtime config list");
11774        };
11775        let keys: Vec<String> = entries.into_iter().map(|entry| entry.key).collect();
11776        assert!(keys.contains(&"pipe.worker.respawn_delay_secs".to_string()));
11777    }
11778
11779    #[cfg(feature = "iface-pipe")]
11780    #[test]
11781    fn runtime_config_sets_pipe_values() {
11782        let mut driver = new_test_driver();
11783        register_test_pipe(&mut driver, "worker");
11784
11785        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11786            key: "pipe.worker.respawn_delay_secs".into(),
11787            value: RuntimeConfigValue::Float(2.0),
11788        });
11789        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11790            panic!("expected set ok");
11791        };
11792        assert_eq!(entry.value, RuntimeConfigValue::Float(2.0));
11793
11794        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
11795            key: "pipe.worker.respawn_delay_secs".into(),
11796        });
11797        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
11798            panic!("expected reset ok");
11799        };
11800        assert_eq!(entry.value, RuntimeConfigValue::Float(5.0));
11801    }
11802
11803    #[cfg(feature = "iface-rnode")]
11804    #[test]
11805    fn runtime_config_lists_rnode_keys() {
11806        let mut driver = new_test_driver();
11807        register_test_rnode(&mut driver, "radio");
11808        let response = driver.handle_query(QueryRequest::ListRuntimeConfig);
11809        let QueryResponse::RuntimeConfigList(entries) = response else {
11810            panic!("expected runtime config list");
11811        };
11812        let keys: Vec<String> = entries.into_iter().map(|entry| entry.key).collect();
11813        assert!(keys.contains(&"rnode.radio.frequency_hz".to_string()));
11814        assert!(keys.contains(&"rnode.radio.bandwidth_hz".to_string()));
11815        assert!(keys.contains(&"rnode.radio.txpower_dbm".to_string()));
11816        assert!(keys.contains(&"rnode.radio.spreading_factor".to_string()));
11817        assert!(keys.contains(&"rnode.radio.coding_rate".to_string()));
11818        assert!(keys.contains(&"rnode.radio.st_alock_pct".to_string()));
11819        assert!(keys.contains(&"rnode.radio.lt_alock_pct".to_string()));
11820    }
11821
11822    #[cfg(feature = "iface-rnode")]
11823    #[test]
11824    fn runtime_config_sets_rnode_values() {
11825        let mut driver = new_test_driver();
11826        register_test_rnode(&mut driver, "radio");
11827
11828        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11829            key: "rnode.radio.frequency_hz".into(),
11830            value: RuntimeConfigValue::Int(915_000_000),
11831        });
11832        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11833            panic!("expected set ok");
11834        };
11835        assert_eq!(entry.value, RuntimeConfigValue::Int(915_000_000));
11836
11837        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11838            key: "rnode.radio.st_alock_pct".into(),
11839            value: RuntimeConfigValue::Float(12.5),
11840        });
11841        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11842            panic!("expected set ok");
11843        };
11844        assert_eq!(entry.value, RuntimeConfigValue::Float(12.5));
11845
11846        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
11847            key: "rnode.radio.frequency_hz".into(),
11848        });
11849        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
11850            panic!("expected reset ok");
11851        };
11852        assert_eq!(entry.value, RuntimeConfigValue::Int(868_000_000));
11853    }
11854
11855    #[test]
11856    fn runtime_config_lists_generic_interface_keys() {
11857        let mut driver = new_test_driver();
11858        register_test_generic_interface(&mut driver, 1, "public");
11859        let response = driver.handle_query(QueryRequest::ListRuntimeConfig);
11860        let QueryResponse::RuntimeConfigList(entries) = response else {
11861            panic!("expected runtime config list");
11862        };
11863        let keys: Vec<String> = entries.into_iter().map(|entry| entry.key).collect();
11864        assert!(keys.contains(&"interface.public.enabled".to_string()));
11865        assert!(keys.contains(&"interface.public.mode".to_string()));
11866        assert!(keys.contains(&"interface.public.announce_rate_target".to_string()));
11867        assert!(keys.contains(&"interface.public.announce_rate_grace".to_string()));
11868        assert!(keys.contains(&"interface.public.announce_rate_penalty".to_string()));
11869        assert!(keys.contains(&"interface.public.announce_cap".to_string()));
11870        assert!(keys.contains(&"interface.public.ingress_control".to_string()));
11871        assert!(keys.contains(&"interface.public.ic_max_held_announces".to_string()));
11872        assert!(keys.contains(&"interface.public.ic_burst_hold".to_string()));
11873        assert!(keys.contains(&"interface.public.ic_burst_freq_new".to_string()));
11874        assert!(keys.contains(&"interface.public.ic_burst_freq".to_string()));
11875        assert!(keys.contains(&"interface.public.ic_new_time".to_string()));
11876        assert!(keys.contains(&"interface.public.ic_burst_penalty".to_string()));
11877        assert!(keys.contains(&"interface.public.ic_held_release_interval".to_string()));
11878        assert!(keys.contains(&"interface.public.ifac_netname".to_string()));
11879        assert!(keys.contains(&"interface.public.ifac_passphrase".to_string()));
11880        assert!(keys.contains(&"interface.public.ifac_size_bytes".to_string()));
11881    }
11882
11883    #[test]
11884    fn runtime_config_sets_generic_interface_values() {
11885        let mut driver = new_test_driver();
11886        register_test_generic_interface(&mut driver, 1, "public");
11887
11888        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11889            key: "interface.public.enabled".into(),
11890            value: RuntimeConfigValue::Bool(false),
11891        });
11892        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11893            panic!("expected set ok");
11894        };
11895        assert_eq!(entry.value, RuntimeConfigValue::Bool(false));
11896        assert!(!driver.interfaces.get(&InterfaceId(1)).unwrap().enabled);
11897
11898        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11899            key: "interface.public.announce_cap".into(),
11900            value: RuntimeConfigValue::Float(0.15),
11901        });
11902        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11903            panic!("expected set ok");
11904        };
11905        assert_eq!(entry.value, RuntimeConfigValue::Float(0.15));
11906        assert_eq!(
11907            driver
11908                .engine
11909                .interface_info(&InterfaceId(1))
11910                .unwrap()
11911                .announce_cap,
11912            0.15
11913        );
11914
11915        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11916            key: "interface.public.mode".into(),
11917            value: RuntimeConfigValue::String("gateway".into()),
11918        });
11919        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11920            panic!("expected set ok");
11921        };
11922        assert_eq!(entry.value, RuntimeConfigValue::String("gateway".into()));
11923
11924        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
11925            key: "interface.public.mode".into(),
11926        });
11927        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
11928            panic!("expected reset ok");
11929        };
11930        assert_eq!(entry.value, RuntimeConfigValue::String("full".into()));
11931
11932        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11933            key: "interface.public.ic_max_held_announces".into(),
11934            value: RuntimeConfigValue::Int(17),
11935        });
11936        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11937            panic!("expected set ok");
11938        };
11939        assert_eq!(entry.value, RuntimeConfigValue::Int(17));
11940        assert_eq!(
11941            driver
11942                .engine
11943                .interface_info(&InterfaceId(1))
11944                .unwrap()
11945                .ingress_control
11946                .max_held_announces,
11947            17
11948        );
11949
11950        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11951            key: "interface.public.ic_burst_hold".into(),
11952            value: RuntimeConfigValue::Float(1.5),
11953        });
11954        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11955            panic!("expected set ok");
11956        };
11957        assert_eq!(entry.value, RuntimeConfigValue::Float(1.5));
11958
11959        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11960            key: "interface.public.ic_burst_freq_new".into(),
11961            value: RuntimeConfigValue::Float(2.5),
11962        });
11963        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11964            panic!("expected set ok");
11965        };
11966        assert_eq!(entry.value, RuntimeConfigValue::Float(2.5));
11967
11968        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11969            key: "interface.public.ic_burst_freq".into(),
11970            value: RuntimeConfigValue::Float(3.5),
11971        });
11972        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11973            panic!("expected set ok");
11974        };
11975        assert_eq!(entry.value, RuntimeConfigValue::Float(3.5));
11976
11977        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11978            key: "interface.public.ic_new_time".into(),
11979            value: RuntimeConfigValue::Float(4.5),
11980        });
11981        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11982            panic!("expected set ok");
11983        };
11984        assert_eq!(entry.value, RuntimeConfigValue::Float(4.5));
11985
11986        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11987            key: "interface.public.ic_burst_penalty".into(),
11988            value: RuntimeConfigValue::Float(5.5),
11989        });
11990        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
11991            panic!("expected set ok");
11992        };
11993        assert_eq!(entry.value, RuntimeConfigValue::Float(5.5));
11994
11995        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
11996            key: "interface.public.ic_held_release_interval".into(),
11997            value: RuntimeConfigValue::Float(6.5),
11998        });
11999        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12000            panic!("expected set ok");
12001        };
12002        assert_eq!(entry.value, RuntimeConfigValue::Float(6.5));
12003
12004        let ingress_control = driver
12005            .engine
12006            .interface_info(&InterfaceId(1))
12007            .unwrap()
12008            .ingress_control;
12009        assert_eq!(ingress_control.burst_hold, 1.5);
12010        assert_eq!(ingress_control.burst_freq_new, 2.5);
12011        assert_eq!(ingress_control.burst_freq, 3.5);
12012        assert_eq!(ingress_control.new_time, 4.5);
12013        assert_eq!(ingress_control.burst_penalty, 5.5);
12014        assert_eq!(ingress_control.held_release_interval, 6.5);
12015
12016        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
12017            key: "interface.public.ic_max_held_announces".into(),
12018        });
12019        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
12020            panic!("expected reset ok");
12021        };
12022        assert_eq!(
12023            entry.value,
12024            RuntimeConfigValue::Int(rns_core::constants::IC_MAX_HELD_ANNOUNCES as i64)
12025        );
12026
12027        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
12028            key: "interface.public.enabled".into(),
12029        });
12030        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
12031            panic!("expected reset ok");
12032        };
12033        assert_eq!(entry.value, RuntimeConfigValue::Bool(true));
12034        assert!(driver.interfaces.get(&InterfaceId(1)).unwrap().enabled);
12035
12036        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12037            key: "interface.public.ifac_netname".into(),
12038            value: RuntimeConfigValue::String("mesh".into()),
12039        });
12040        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12041            panic!("expected set ok");
12042        };
12043        assert_eq!(entry.value, RuntimeConfigValue::String("mesh".into()));
12044        assert_eq!(
12045            driver
12046                .interfaces
12047                .get(&InterfaceId(1))
12048                .unwrap()
12049                .ifac
12050                .as_ref()
12051                .unwrap()
12052                .size,
12053            16
12054        );
12055
12056        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12057            key: "interface.public.ifac_passphrase".into(),
12058            value: RuntimeConfigValue::String("secret".into()),
12059        });
12060        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12061            panic!("expected set ok");
12062        };
12063        assert_eq!(entry.value, RuntimeConfigValue::String("<redacted>".into()));
12064
12065        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12066            key: "interface.public.ifac_size_bytes".into(),
12067            value: RuntimeConfigValue::Int(24),
12068        });
12069        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12070            panic!("expected set ok");
12071        };
12072        assert_eq!(entry.value, RuntimeConfigValue::Int(24));
12073        let ifac = driver
12074            .interfaces
12075            .get(&InterfaceId(1))
12076            .unwrap()
12077            .ifac
12078            .as_ref()
12079            .unwrap();
12080        assert_eq!(ifac.size, 24);
12081
12082        let response = driver.handle_query(QueryRequest::GetRuntimeConfig {
12083            key: "interface.public.ifac_passphrase".into(),
12084        });
12085        let QueryResponse::RuntimeConfigEntry(Some(entry)) = response else {
12086            panic!("expected runtime config entry");
12087        };
12088        assert_eq!(entry.value, RuntimeConfigValue::String("<redacted>".into()));
12089
12090        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
12091            key: "interface.public.ifac_netname".into(),
12092        });
12093        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
12094            panic!("expected reset ok");
12095        };
12096        assert_eq!(entry.value, RuntimeConfigValue::Null);
12097        assert!(driver
12098            .interfaces
12099            .get(&InterfaceId(1))
12100            .unwrap()
12101            .ifac
12102            .is_some());
12103
12104        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
12105            key: "interface.public.ifac_passphrase".into(),
12106        });
12107        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
12108            panic!("expected reset ok");
12109        };
12110        assert_eq!(entry.value, RuntimeConfigValue::Null);
12111        assert!(driver
12112            .interfaces
12113            .get(&InterfaceId(1))
12114            .unwrap()
12115            .ifac
12116            .is_none());
12117    }
12118
12119    #[cfg(feature = "rns-hooks")]
12120    #[test]
12121    fn runtime_config_sets_provider_bridge_values() {
12122        let mut driver = new_test_driver();
12123
12124        let dir = tempfile::tempdir().unwrap();
12125        let socket_path = dir.path().join("provider.sock");
12126        let bridge = crate::provider_bridge::ProviderBridge::start(
12127            crate::provider_bridge::ProviderBridgeConfig {
12128                enabled: true,
12129                socket_path,
12130                queue_max_events: 1024,
12131                queue_max_bytes: 1024 * 1024,
12132                ..Default::default()
12133            },
12134        )
12135        .unwrap();
12136        driver.runtime_config_defaults.provider_queue_max_events = 1024;
12137        driver.runtime_config_defaults.provider_queue_max_bytes = 1024 * 1024;
12138        driver.provider_bridge = Some(bridge);
12139
12140        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12141            key: "provider.queue_max_events".into(),
12142            value: RuntimeConfigValue::Int(4096),
12143        });
12144        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12145            panic!("expected set ok");
12146        };
12147        assert_eq!(entry.value, RuntimeConfigValue::Int(4096));
12148        assert_eq!(entry.source, RuntimeConfigSource::RuntimeOverride,);
12149
12150        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12151            key: "provider.queue_max_bytes".into(),
12152            value: RuntimeConfigValue::Int(2 * 1024 * 1024),
12153        });
12154        let QueryResponse::RuntimeConfigSet(Ok(entry)) = response else {
12155            panic!("expected set ok");
12156        };
12157        assert_eq!(entry.value, RuntimeConfigValue::Int(2 * 1024 * 1024));
12158
12159        // Reject zero values
12160        let response = driver.handle_query_mut(QueryRequest::SetRuntimeConfig {
12161            key: "provider.queue_max_events".into(),
12162            value: RuntimeConfigValue::Int(0),
12163        });
12164        assert!(matches!(response, QueryResponse::RuntimeConfigSet(Err(_))));
12165
12166        // Reset
12167        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
12168            key: "provider.queue_max_events".into(),
12169        });
12170        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
12171            panic!("expected reset ok");
12172        };
12173        assert_eq!(entry.value, RuntimeConfigValue::Int(1024));
12174        assert_eq!(entry.source, RuntimeConfigSource::Startup);
12175
12176        let response = driver.handle_query_mut(QueryRequest::ResetRuntimeConfig {
12177            key: "provider.queue_max_bytes".into(),
12178        });
12179        let QueryResponse::RuntimeConfigReset(Ok(entry)) = response else {
12180            panic!("expected reset ok");
12181        };
12182        assert_eq!(entry.value, RuntimeConfigValue::Int(1024 * 1024));
12183    }
12184
12185    #[test]
12186    fn disabled_interface_drops_ingress_and_egress() {
12187        let (tx, rx) = event::channel();
12188        let (cbs, _, _, _, _, _) = MockCallbacks::new();
12189        let mut driver = Driver::new(
12190            TransportConfig {
12191                transport_enabled: false,
12192                identity_hash: None,
12193                prefer_shorter_path: false,
12194                max_paths_per_destination: 1,
12195                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
12196                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
12197                max_path_destinations: usize::MAX,
12198                max_tunnel_destinations_total: usize::MAX,
12199                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
12200                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
12201                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
12202                announce_sig_cache_enabled: true,
12203                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
12204                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
12205                announce_queue_max_entries: 256,
12206                announce_queue_max_interfaces: 1024,
12207            },
12208            rx,
12209            tx.clone(),
12210            Box::new(cbs),
12211        );
12212        let info = make_interface_info(1);
12213        driver.register_interface_runtime_defaults(&info);
12214        driver.engine.register_interface(info.clone());
12215        let (writer, sent) = MockWriter::new();
12216        driver.interfaces.insert(
12217            InterfaceId(1),
12218            InterfaceEntry {
12219                id: InterfaceId(1),
12220                info,
12221                writer: Box::new(writer),
12222                async_writer_metrics: None,
12223                enabled: false,
12224                online: true,
12225                dynamic: false,
12226                ifac: None,
12227                stats: InterfaceStats::default(),
12228                interface_type: String::new(),
12229                send_retry_at: None,
12230                send_retry_backoff: Duration::ZERO,
12231            },
12232        );
12233
12234        driver.dispatch_all(vec![TransportAction::SendOnInterface {
12235            interface: InterfaceId(1),
12236            raw: vec![0x00, 0x01, 0x42],
12237        }]);
12238        assert!(sent.lock().unwrap().is_empty());
12239
12240        tx.send(Event::Frame {
12241            interface_id: InterfaceId(1),
12242            data: vec![0x00, 0x01, 0x42],
12243        })
12244        .unwrap();
12245        tx.send(Event::Shutdown).unwrap();
12246        driver.run();
12247
12248        let entry = driver.interfaces.get(&InterfaceId(1)).unwrap();
12249        assert_eq!(entry.stats.rxb, 0);
12250        assert_eq!(entry.stats.rx_packets, 0);
12251    }
12252
12253    #[test]
12254    fn management_announces_not_emitted_when_disabled() {
12255        let (tx, rx) = event::channel();
12256        let (cbs, _, _, _, _, _) = MockCallbacks::new();
12257        let identity = Identity::new(&mut OsRng);
12258        let identity_hash = *identity.hash();
12259        let mut driver = Driver::new(
12260            TransportConfig {
12261                transport_enabled: true,
12262                identity_hash: Some(identity_hash),
12263                prefer_shorter_path: false,
12264                max_paths_per_destination: 1,
12265                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
12266                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
12267                max_path_destinations: usize::MAX,
12268                max_tunnel_destinations_total: usize::MAX,
12269                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
12270                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
12271                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
12272                announce_sig_cache_enabled: true,
12273                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
12274                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
12275                announce_queue_max_entries: 256,
12276                announce_queue_max_interfaces: 1024,
12277            },
12278            rx,
12279            tx.clone(),
12280            Box::new(cbs),
12281        );
12282
12283        let info = make_interface_info(1);
12284        driver.engine.register_interface(info.clone());
12285        let (writer, sent) = MockWriter::new();
12286        driver
12287            .interfaces
12288            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
12289
12290        // Management announces disabled (default)
12291        driver.transport_identity = Some(identity);
12292        driver.started = time::now() - 10.0;
12293
12294        tx.send(Event::Tick).unwrap();
12295        tx.send(Event::Shutdown).unwrap();
12296        driver.run();
12297
12298        // Should NOT have sent any packets
12299        let sent_packets = sent.lock().unwrap();
12300        assert!(
12301            sent_packets.is_empty(),
12302            "No announces should be sent when management is disabled"
12303        );
12304    }
12305
12306    #[test]
12307    fn management_announces_not_emitted_before_delay() {
12308        let (tx, rx) = event::channel();
12309        let (cbs, _, _, _, _, _) = MockCallbacks::new();
12310        let identity = Identity::new(&mut OsRng);
12311        let identity_hash = *identity.hash();
12312        let mut driver = Driver::new(
12313            TransportConfig {
12314                transport_enabled: true,
12315                identity_hash: Some(identity_hash),
12316                prefer_shorter_path: false,
12317                max_paths_per_destination: 1,
12318                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
12319                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
12320                max_path_destinations: usize::MAX,
12321                max_tunnel_destinations_total: usize::MAX,
12322                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
12323                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
12324                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
12325                announce_sig_cache_enabled: true,
12326                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
12327                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
12328                announce_queue_max_entries: 256,
12329                announce_queue_max_interfaces: 1024,
12330            },
12331            rx,
12332            tx.clone(),
12333            Box::new(cbs),
12334        );
12335
12336        let info = make_interface_info(1);
12337        driver.engine.register_interface(info.clone());
12338        let (writer, sent) = MockWriter::new();
12339        driver
12340            .interfaces
12341            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
12342
12343        driver.management_config.enable_remote_management = true;
12344        driver.transport_identity = Some(identity);
12345        // Started just now - delay hasn't passed
12346        driver.started = time::now();
12347
12348        tx.send(Event::Tick).unwrap();
12349        tx.send(Event::Shutdown).unwrap();
12350        driver.run();
12351
12352        let sent_packets = sent.lock().unwrap();
12353        assert!(sent_packets.is_empty(), "No announces before startup delay");
12354    }
12355
12356    // =========================================================================
12357    // Phase 9c: Announce + Discovery tests
12358    // =========================================================================
12359
12360    #[test]
12361    fn announce_received_populates_known_destinations() {
12362        let (tx, rx) = event::channel();
12363        let (cbs, _, _, _, _, _) = MockCallbacks::new();
12364        let mut driver = Driver::new(
12365            TransportConfig {
12366                transport_enabled: false,
12367                identity_hash: None,
12368                prefer_shorter_path: false,
12369                max_paths_per_destination: 1,
12370                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
12371                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
12372                max_path_destinations: usize::MAX,
12373                max_tunnel_destinations_total: usize::MAX,
12374                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
12375                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
12376                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
12377                announce_sig_cache_enabled: true,
12378                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
12379                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
12380                announce_queue_max_entries: 256,
12381                announce_queue_max_interfaces: 1024,
12382            },
12383            rx,
12384            tx.clone(),
12385            Box::new(cbs),
12386        );
12387        let info = make_interface_info(1);
12388        driver.engine.register_interface(info);
12389        let (writer, _sent) = MockWriter::new();
12390        driver
12391            .interfaces
12392            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
12393
12394        let identity = Identity::new(&mut OsRng);
12395        let announce_raw = build_announce_packet(&identity);
12396
12397        let dest_hash =
12398            rns_core::destination::destination_hash("test", &["app"], Some(identity.hash()));
12399
12400        tx.send(Event::Frame {
12401            interface_id: InterfaceId(1),
12402            data: announce_raw,
12403        })
12404        .unwrap();
12405        tx.send(Event::Shutdown).unwrap();
12406        driver.run();
12407
12408        // known_destinations should be populated
12409        assert!(driver.known_destinations.contains_key(&dest_hash));
12410        let recalled = &driver.known_destinations[&dest_hash];
12411        assert_eq!(recalled.dest_hash.0, dest_hash);
12412        assert_eq!(recalled.identity_hash.0, *identity.hash());
12413        assert_eq!(&recalled.public_key, &identity.get_public_key().unwrap());
12414        assert_eq!(recalled.hops, 1);
12415    }
12416
12417    #[test]
12418    fn known_destinations_cleanup_respects_ttl() {
12419        let (tx, rx) = event::channel();
12420        let (cbs, _, _, _, _, _) = MockCallbacks::new();
12421        let mut driver = Driver::new(
12422            TransportConfig {
12423                transport_enabled: false,
12424                identity_hash: None,
12425                prefer_shorter_path: false,
12426                max_paths_per_destination: 1,
12427                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
12428                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
12429                max_path_destinations: usize::MAX,
12430                max_tunnel_destinations_total: usize::MAX,
12431                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
12432                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
12433                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
12434                announce_sig_cache_enabled: true,
12435                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
12436                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
12437                announce_queue_max_entries: 256,
12438                announce_queue_max_interfaces: 1024,
12439            },
12440            rx,
12441            tx.clone(),
12442            Box::new(cbs),
12443        );
12444
12445        driver.known_destinations_ttl = 10.0;
12446        driver.cache_cleanup_counter = 3599;
12447
12448        let stale_dest = [0x11; 16];
12449        let fresh_dest = [0x22; 16];
12450        driver.known_destinations.insert(
12451            stale_dest,
12452            crate::destination::AnnouncedIdentity {
12453                dest_hash: rns_core::types::DestHash(stale_dest),
12454                identity_hash: rns_core::types::IdentityHash([0x33; 16]),
12455                public_key: [0x44; 64],
12456                app_data: None,
12457                hops: 1,
12458                received_at: time::now() - 20.0,
12459                receiving_interface: InterfaceId(1),
12460            },
12461        );
12462        driver.known_destinations.insert(
12463            fresh_dest,
12464            crate::destination::AnnouncedIdentity {
12465                dest_hash: rns_core::types::DestHash(fresh_dest),
12466                identity_hash: rns_core::types::IdentityHash([0x55; 16]),
12467                public_key: [0x66; 64],
12468                app_data: None,
12469                hops: 1,
12470                received_at: time::now() - 5.0,
12471                receiving_interface: InterfaceId(1),
12472            },
12473        );
12474
12475        tx.send(Event::Tick).unwrap();
12476        tx.send(Event::Shutdown).unwrap();
12477        driver.run();
12478
12479        assert!(!driver.known_destinations.contains_key(&stale_dest));
12480        assert!(driver.known_destinations.contains_key(&fresh_dest));
12481    }
12482
12483    #[test]
12484    fn known_destinations_cap_prefers_evicting_oldest_non_active_non_local() {
12485        let mut driver = new_test_driver();
12486        driver.known_destinations_max_entries = 2;
12487        driver.engine.register_interface(make_interface_info(1));
12488
12489        let active_dest = [0x11; 16];
12490        let evictable_dest = [0x22; 16];
12491        let new_dest = [0x33; 16];
12492
12493        driver.engine.inject_path(
12494            active_dest,
12495            PathEntry {
12496                timestamp: 100.0,
12497                next_hop: [0x44; 16],
12498                hops: 1,
12499                expires: 1000.0,
12500                random_blobs: Vec::new(),
12501                receiving_interface: InterfaceId(1),
12502                packet_hash: [0x55; 32],
12503                announce_raw: None,
12504            },
12505        );
12506
12507        driver.upsert_known_destination(
12508            active_dest,
12509            make_announced_identity(active_dest, 10.0, InterfaceId(1)),
12510        );
12511        driver.upsert_known_destination(
12512            evictable_dest,
12513            make_announced_identity(evictable_dest, 20.0, InterfaceId(1)),
12514        );
12515        driver.upsert_known_destination(
12516            new_dest,
12517            make_announced_identity(new_dest, 30.0, InterfaceId(1)),
12518        );
12519
12520        assert!(driver.known_destinations.contains_key(&active_dest));
12521        assert!(!driver.known_destinations.contains_key(&evictable_dest));
12522        assert!(driver.known_destinations.contains_key(&new_dest));
12523        assert_eq!(driver.known_destinations_cap_evict_count, 1);
12524    }
12525
12526    #[test]
12527    fn known_destinations_cap_falls_back_to_oldest_overall_when_all_protected() {
12528        let mut driver = new_test_driver();
12529        driver.known_destinations_max_entries = 2;
12530
12531        let local_oldest = [0x41; 16];
12532        let local_newer = [0x42; 16];
12533        let new_dest = [0x43; 16];
12534        driver
12535            .local_destinations
12536            .insert(local_oldest, rns_core::constants::DESTINATION_SINGLE);
12537        driver
12538            .local_destinations
12539            .insert(local_newer, rns_core::constants::DESTINATION_SINGLE);
12540
12541        driver.upsert_known_destination(
12542            local_oldest,
12543            make_announced_identity(local_oldest, 10.0, InterfaceId(1)),
12544        );
12545        driver.upsert_known_destination(
12546            local_newer,
12547            make_announced_identity(local_newer, 20.0, InterfaceId(1)),
12548        );
12549        driver.upsert_known_destination(
12550            new_dest,
12551            make_announced_identity(new_dest, 30.0, InterfaceId(1)),
12552        );
12553
12554        assert!(!driver.known_destinations.contains_key(&local_oldest));
12555        assert!(driver.known_destinations.contains_key(&local_newer));
12556        assert!(driver.known_destinations.contains_key(&new_dest));
12557        assert_eq!(driver.known_destinations_cap_evict_count, 1);
12558    }
12559
12560    #[test]
12561    fn known_destinations_cap_update_existing_entry_does_not_evict() {
12562        let mut driver = new_test_driver();
12563        driver.known_destinations_max_entries = 1;
12564
12565        let dest = [0x61; 16];
12566        driver.upsert_known_destination(dest, make_announced_identity(dest, 10.0, InterfaceId(1)));
12567        driver.upsert_known_destination(dest, make_announced_identity(dest, 20.0, InterfaceId(2)));
12568
12569        assert_eq!(driver.known_destinations.len(), 1);
12570        assert_eq!(
12571            driver.known_destinations[&dest].receiving_interface,
12572            InterfaceId(2)
12573        );
12574        assert_eq!(driver.known_destinations_cap_evict_count, 0);
12575    }
12576
12577    #[test]
12578    fn known_destinations_cleanup_enforces_cap() {
12579        let (tx, rx) = event::channel();
12580        let (cbs, _, _, _, _, _) = MockCallbacks::new();
12581        let mut driver = Driver::new(
12582            TransportConfig {
12583                transport_enabled: false,
12584                identity_hash: None,
12585                prefer_shorter_path: false,
12586                max_paths_per_destination: 1,
12587                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
12588                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
12589                max_path_destinations: usize::MAX,
12590                max_tunnel_destinations_total: usize::MAX,
12591                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
12592                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
12593                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
12594                announce_sig_cache_enabled: true,
12595                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
12596                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
12597                announce_queue_max_entries: 256,
12598                announce_queue_max_interfaces: 1024,
12599            },
12600            rx,
12601            tx.clone(),
12602            Box::new(cbs),
12603        );
12604
12605        driver.known_destinations_ttl = 1000.0;
12606        driver.known_destinations_max_entries = 2;
12607        driver.cache_cleanup_counter = 3599;
12608        let now = time::now();
12609        driver.known_destinations.insert(
12610            [0x71; 16],
12611            make_announced_identity([0x71; 16], now - 30.0, InterfaceId(1)),
12612        );
12613        driver.known_destinations.insert(
12614            [0x72; 16],
12615            make_announced_identity([0x72; 16], now - 20.0, InterfaceId(1)),
12616        );
12617        driver.known_destinations.insert(
12618            [0x73; 16],
12619            make_announced_identity([0x73; 16], now - 10.0, InterfaceId(1)),
12620        );
12621
12622        tx.send(Event::Tick).unwrap();
12623        tx.send(Event::Shutdown).unwrap();
12624        driver.run();
12625
12626        assert_eq!(driver.known_destinations.len(), 2);
12627        assert!(!driver.known_destinations.contains_key(&[0x71; 16]));
12628        assert_eq!(driver.known_destinations_cap_evict_count, 1);
12629    }
12630
12631    #[test]
12632    fn query_has_path() {
12633        let (tx, rx) = event::channel();
12634        let (cbs, _, _, _, _, _) = MockCallbacks::new();
12635        let mut driver = Driver::new(
12636            TransportConfig {
12637                transport_enabled: false,
12638                identity_hash: None,
12639                prefer_shorter_path: false,
12640                max_paths_per_destination: 1,
12641                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
12642                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
12643                max_path_destinations: usize::MAX,
12644                max_tunnel_destinations_total: usize::MAX,
12645                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
12646                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
12647                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
12648                announce_sig_cache_enabled: true,
12649                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
12650                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
12651                announce_queue_max_entries: 256,
12652                announce_queue_max_interfaces: 1024,
12653            },
12654            rx,
12655            tx.clone(),
12656            Box::new(cbs),
12657        );
12658        let info = make_interface_info(1);
12659        driver.engine.register_interface(info);
12660        let (writer, _sent) = MockWriter::new();
12661        driver
12662            .interfaces
12663            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
12664
12665        // No path yet
12666        let (resp_tx, resp_rx) = mpsc::channel();
12667        tx.send(Event::Query(
12668            QueryRequest::HasPath {
12669                dest_hash: [0xAA; 16],
12670            },
12671            resp_tx,
12672        ))
12673        .unwrap();
12674
12675        // Feed an announce to create a path
12676        let identity = Identity::new(&mut OsRng);
12677        let announce_raw = build_announce_packet(&identity);
12678        let dest_hash =
12679            rns_core::destination::destination_hash("test", &["app"], Some(identity.hash()));
12680        tx.send(Event::Frame {
12681            interface_id: InterfaceId(1),
12682            data: announce_raw,
12683        })
12684        .unwrap();
12685
12686        let (resp_tx2, resp_rx2) = mpsc::channel();
12687        tx.send(Event::Query(QueryRequest::HasPath { dest_hash }, resp_tx2))
12688            .unwrap();
12689
12690        tx.send(Event::Shutdown).unwrap();
12691        driver.run();
12692
12693        // First query — no path
12694        match resp_rx.recv().unwrap() {
12695            QueryResponse::HasPath(false) => {}
12696            other => panic!("expected HasPath(false), got {:?}", other),
12697        }
12698
12699        // Second query — path exists
12700        match resp_rx2.recv().unwrap() {
12701            QueryResponse::HasPath(true) => {}
12702            other => panic!("expected HasPath(true), got {:?}", other),
12703        }
12704    }
12705
12706    #[test]
12707    fn query_hops_to() {
12708        let (tx, rx) = event::channel();
12709        let (cbs, _, _, _, _, _) = MockCallbacks::new();
12710        let mut driver = Driver::new(
12711            TransportConfig {
12712                transport_enabled: false,
12713                identity_hash: None,
12714                prefer_shorter_path: false,
12715                max_paths_per_destination: 1,
12716                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
12717                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
12718                max_path_destinations: usize::MAX,
12719                max_tunnel_destinations_total: usize::MAX,
12720                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
12721                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
12722                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
12723                announce_sig_cache_enabled: true,
12724                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
12725                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
12726                announce_queue_max_entries: 256,
12727                announce_queue_max_interfaces: 1024,
12728            },
12729            rx,
12730            tx.clone(),
12731            Box::new(cbs),
12732        );
12733        let info = make_interface_info(1);
12734        driver.engine.register_interface(info);
12735        let (writer, _sent) = MockWriter::new();
12736        driver
12737            .interfaces
12738            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
12739
12740        // Feed an announce
12741        let identity = Identity::new(&mut OsRng);
12742        let announce_raw = build_announce_packet(&identity);
12743        let dest_hash =
12744            rns_core::destination::destination_hash("test", &["app"], Some(identity.hash()));
12745
12746        tx.send(Event::Frame {
12747            interface_id: InterfaceId(1),
12748            data: announce_raw,
12749        })
12750        .unwrap();
12751
12752        let (resp_tx, resp_rx) = mpsc::channel();
12753        tx.send(Event::Query(QueryRequest::HopsTo { dest_hash }, resp_tx))
12754            .unwrap();
12755        tx.send(Event::Shutdown).unwrap();
12756        driver.run();
12757
12758        match resp_rx.recv().unwrap() {
12759            QueryResponse::HopsTo(Some(1)) => {}
12760            other => panic!("expected HopsTo(Some(1)), got {:?}", other),
12761        }
12762    }
12763
12764    #[test]
12765    fn query_recall_identity() {
12766        let (tx, rx) = event::channel();
12767        let (cbs, _, _, _, _, _) = MockCallbacks::new();
12768        let mut driver = Driver::new(
12769            TransportConfig {
12770                transport_enabled: false,
12771                identity_hash: None,
12772                prefer_shorter_path: false,
12773                max_paths_per_destination: 1,
12774                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
12775                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
12776                max_path_destinations: usize::MAX,
12777                max_tunnel_destinations_total: usize::MAX,
12778                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
12779                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
12780                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
12781                announce_sig_cache_enabled: true,
12782                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
12783                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
12784                announce_queue_max_entries: 256,
12785                announce_queue_max_interfaces: 1024,
12786            },
12787            rx,
12788            tx.clone(),
12789            Box::new(cbs),
12790        );
12791        let info = make_interface_info(1);
12792        driver.engine.register_interface(info);
12793        let (writer, _sent) = MockWriter::new();
12794        driver
12795            .interfaces
12796            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
12797
12798        let identity = Identity::new(&mut OsRng);
12799        let announce_raw = build_announce_packet(&identity);
12800        let dest_hash =
12801            rns_core::destination::destination_hash("test", &["app"], Some(identity.hash()));
12802
12803        tx.send(Event::Frame {
12804            interface_id: InterfaceId(1),
12805            data: announce_raw,
12806        })
12807        .unwrap();
12808
12809        // Recall identity
12810        let (resp_tx, resp_rx) = mpsc::channel();
12811        tx.send(Event::Query(
12812            QueryRequest::RecallIdentity { dest_hash },
12813            resp_tx,
12814        ))
12815        .unwrap();
12816
12817        // Also recall unknown destination
12818        let (resp_tx2, resp_rx2) = mpsc::channel();
12819        tx.send(Event::Query(
12820            QueryRequest::RecallIdentity {
12821                dest_hash: [0xFF; 16],
12822            },
12823            resp_tx2,
12824        ))
12825        .unwrap();
12826
12827        tx.send(Event::Shutdown).unwrap();
12828        driver.run();
12829
12830        match resp_rx.recv().unwrap() {
12831            QueryResponse::RecallIdentity(Some(recalled)) => {
12832                assert_eq!(recalled.dest_hash.0, dest_hash);
12833                assert_eq!(recalled.identity_hash.0, *identity.hash());
12834                assert_eq!(recalled.public_key, identity.get_public_key().unwrap());
12835                assert_eq!(recalled.hops, 1);
12836            }
12837            other => panic!("expected RecallIdentity(Some(..)), got {:?}", other),
12838        }
12839
12840        match resp_rx2.recv().unwrap() {
12841            QueryResponse::RecallIdentity(None) => {}
12842            other => panic!("expected RecallIdentity(None), got {:?}", other),
12843        }
12844    }
12845
12846    #[test]
12847    fn request_path_sends_packet() {
12848        let (tx, rx) = event::channel();
12849        let (cbs, _, _, _, _, _) = MockCallbacks::new();
12850        let mut driver = Driver::new(
12851            TransportConfig {
12852                transport_enabled: false,
12853                identity_hash: None,
12854                prefer_shorter_path: false,
12855                max_paths_per_destination: 1,
12856                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
12857                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
12858                max_path_destinations: usize::MAX,
12859                max_tunnel_destinations_total: usize::MAX,
12860                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
12861                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
12862                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
12863                announce_sig_cache_enabled: true,
12864                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
12865                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
12866                announce_queue_max_entries: 256,
12867                announce_queue_max_interfaces: 1024,
12868            },
12869            rx,
12870            tx.clone(),
12871            Box::new(cbs),
12872        );
12873        let info = make_interface_info(1);
12874        driver.engine.register_interface(info);
12875        let (writer, sent) = MockWriter::new();
12876        driver
12877            .interfaces
12878            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
12879
12880        // Send path request
12881        tx.send(Event::RequestPath {
12882            dest_hash: [0xAA; 16],
12883        })
12884        .unwrap();
12885        tx.send(Event::Shutdown).unwrap();
12886        driver.run();
12887
12888        // Should have sent a packet on the wire (broadcast)
12889        let sent_packets = sent.lock().unwrap();
12890        assert!(
12891            !sent_packets.is_empty(),
12892            "Path request should be sent on wire"
12893        );
12894
12895        // Verify the sent packet is a DATA PLAIN BROADCAST packet
12896        let raw = &sent_packets[0];
12897        let flags = rns_core::packet::PacketFlags::unpack(raw[0] & 0x7F);
12898        assert_eq!(flags.packet_type, constants::PACKET_TYPE_DATA);
12899        assert_eq!(flags.destination_type, constants::DESTINATION_PLAIN);
12900        assert_eq!(flags.transport_type, constants::TRANSPORT_BROADCAST);
12901    }
12902
12903    #[test]
12904    fn request_path_includes_transport_id() {
12905        let (tx, rx) = event::channel();
12906        let (cbs, _, _, _, _, _) = MockCallbacks::new();
12907        let mut driver = Driver::new(
12908            TransportConfig {
12909                transport_enabled: true,
12910                identity_hash: Some([0xBB; 16]),
12911                prefer_shorter_path: false,
12912                max_paths_per_destination: 1,
12913                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
12914                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
12915                max_path_destinations: usize::MAX,
12916                max_tunnel_destinations_total: usize::MAX,
12917                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
12918                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
12919                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
12920                announce_sig_cache_enabled: true,
12921                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
12922                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
12923                announce_queue_max_entries: 256,
12924                announce_queue_max_interfaces: 1024,
12925            },
12926            rx,
12927            tx.clone(),
12928            Box::new(cbs),
12929        );
12930        let info = make_interface_info(1);
12931        driver.engine.register_interface(info);
12932        let (writer, sent) = MockWriter::new();
12933        driver
12934            .interfaces
12935            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
12936
12937        tx.send(Event::RequestPath {
12938            dest_hash: [0xAA; 16],
12939        })
12940        .unwrap();
12941        tx.send(Event::Shutdown).unwrap();
12942        driver.run();
12943
12944        let sent_packets = sent.lock().unwrap();
12945        assert!(!sent_packets.is_empty());
12946
12947        // Unpack the packet to check data length includes transport_id
12948        let raw = &sent_packets[0];
12949        if let Ok(packet) = RawPacket::unpack(raw) {
12950            // Data: dest_hash(16) + transport_id(16) + random_tag(16) = 48 bytes
12951            assert_eq!(
12952                packet.data.len(),
12953                48,
12954                "Path request data should be 48 bytes with transport_id"
12955            );
12956            assert_eq!(
12957                &packet.data[..16],
12958                &[0xAA; 16],
12959                "First 16 bytes should be dest_hash"
12960            );
12961            assert_eq!(
12962                &packet.data[16..32],
12963                &[0xBB; 16],
12964                "Next 16 bytes should be transport_id"
12965            );
12966        } else {
12967            panic!("Could not unpack sent packet");
12968        }
12969    }
12970
12971    #[test]
12972    fn path_request_dest_registered() {
12973        let (tx, rx) = event::channel();
12974        let (cbs, _, _, _, _, _) = MockCallbacks::new();
12975        let driver = Driver::new(
12976            TransportConfig {
12977                transport_enabled: false,
12978                identity_hash: None,
12979                prefer_shorter_path: false,
12980                max_paths_per_destination: 1,
12981                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
12982                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
12983                max_path_destinations: usize::MAX,
12984                max_tunnel_destinations_total: usize::MAX,
12985                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
12986                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
12987                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
12988                announce_sig_cache_enabled: true,
12989                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
12990                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
12991                announce_queue_max_entries: 256,
12992                announce_queue_max_interfaces: 1024,
12993            },
12994            rx,
12995            tx.clone(),
12996            Box::new(cbs),
12997        );
12998
12999        // The path request dest should be registered as a local PLAIN destination
13000        let expected_dest =
13001            rns_core::destination::destination_hash("rnstransport", &["path", "request"], None);
13002        assert_eq!(driver.path_request_dest, expected_dest);
13003
13004        drop(tx);
13005    }
13006
13007    // =========================================================================
13008    // Phase 9d: send_packet + proofs tests
13009    // =========================================================================
13010
13011    #[test]
13012    fn register_proof_strategy_event() {
13013        let (tx, rx) = event::channel();
13014        let (cbs, _, _, _, _, _) = MockCallbacks::new();
13015        let mut driver = Driver::new(
13016            TransportConfig {
13017                transport_enabled: false,
13018                identity_hash: None,
13019                prefer_shorter_path: false,
13020                max_paths_per_destination: 1,
13021                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
13022                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
13023                max_path_destinations: usize::MAX,
13024                max_tunnel_destinations_total: usize::MAX,
13025                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
13026                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
13027                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
13028                announce_sig_cache_enabled: true,
13029                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
13030                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
13031                announce_queue_max_entries: 256,
13032                announce_queue_max_interfaces: 1024,
13033            },
13034            rx,
13035            tx.clone(),
13036            Box::new(cbs),
13037        );
13038
13039        let dest = [0xAA; 16];
13040        let identity = Identity::new(&mut OsRng);
13041        let prv_key = identity.get_private_key().unwrap();
13042
13043        tx.send(Event::RegisterProofStrategy {
13044            dest_hash: dest,
13045            strategy: rns_core::types::ProofStrategy::ProveAll,
13046            signing_key: Some(prv_key),
13047        })
13048        .unwrap();
13049        tx.send(Event::Shutdown).unwrap();
13050        driver.run();
13051
13052        assert!(driver.proof_strategies.contains_key(&dest));
13053        let (strategy, ref id_opt) = driver.proof_strategies[&dest];
13054        assert_eq!(strategy, rns_core::types::ProofStrategy::ProveAll);
13055        assert!(id_opt.is_some());
13056    }
13057
13058    #[test]
13059    fn register_proof_strategy_prove_none_no_identity() {
13060        let (tx, rx) = event::channel();
13061        let (cbs, _, _, _, _, _) = MockCallbacks::new();
13062        let mut driver = Driver::new(
13063            TransportConfig {
13064                transport_enabled: false,
13065                identity_hash: None,
13066                prefer_shorter_path: false,
13067                max_paths_per_destination: 1,
13068                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
13069                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
13070                max_path_destinations: usize::MAX,
13071                max_tunnel_destinations_total: usize::MAX,
13072                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
13073                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
13074                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
13075                announce_sig_cache_enabled: true,
13076                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
13077                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
13078                announce_queue_max_entries: 256,
13079                announce_queue_max_interfaces: 1024,
13080            },
13081            rx,
13082            tx.clone(),
13083            Box::new(cbs),
13084        );
13085
13086        let dest = [0xBB; 16];
13087        tx.send(Event::RegisterProofStrategy {
13088            dest_hash: dest,
13089            strategy: rns_core::types::ProofStrategy::ProveNone,
13090            signing_key: None,
13091        })
13092        .unwrap();
13093        tx.send(Event::Shutdown).unwrap();
13094        driver.run();
13095
13096        assert!(driver.proof_strategies.contains_key(&dest));
13097        let (strategy, ref id_opt) = driver.proof_strategies[&dest];
13098        assert_eq!(strategy, rns_core::types::ProofStrategy::ProveNone);
13099        assert!(id_opt.is_none());
13100    }
13101
13102    #[test]
13103    fn send_outbound_tracks_sent_packets() {
13104        let (tx, rx) = event::channel();
13105        let (cbs, _, _, _, _, _) = MockCallbacks::new();
13106        let mut driver = Driver::new(
13107            TransportConfig {
13108                transport_enabled: false,
13109                identity_hash: None,
13110                prefer_shorter_path: false,
13111                max_paths_per_destination: 1,
13112                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
13113                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
13114                max_path_destinations: usize::MAX,
13115                max_tunnel_destinations_total: usize::MAX,
13116                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
13117                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
13118                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
13119                announce_sig_cache_enabled: true,
13120                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
13121                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
13122                announce_queue_max_entries: 256,
13123                announce_queue_max_interfaces: 1024,
13124            },
13125            rx,
13126            tx.clone(),
13127            Box::new(cbs),
13128        );
13129        let info = make_interface_info(1);
13130        driver.engine.register_interface(info);
13131        let (writer, _sent) = MockWriter::new();
13132        driver
13133            .interfaces
13134            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
13135
13136        // Build a DATA packet
13137        let dest = [0xCC; 16];
13138        let flags = PacketFlags {
13139            header_type: constants::HEADER_1,
13140            context_flag: constants::FLAG_UNSET,
13141            transport_type: constants::TRANSPORT_BROADCAST,
13142            destination_type: constants::DESTINATION_PLAIN,
13143            packet_type: constants::PACKET_TYPE_DATA,
13144        };
13145        let packet =
13146            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"test data").unwrap();
13147        let expected_hash = packet.packet_hash;
13148
13149        tx.send(Event::SendOutbound {
13150            raw: packet.raw,
13151            dest_type: constants::DESTINATION_PLAIN,
13152            attached_interface: None,
13153        })
13154        .unwrap();
13155        tx.send(Event::Shutdown).unwrap();
13156        driver.run();
13157
13158        // Should be tracking the sent packet
13159        assert!(driver.sent_packets.contains_key(&expected_hash));
13160        let (tracked_dest, _sent_time) = &driver.sent_packets[&expected_hash];
13161        assert_eq!(tracked_dest, &dest);
13162    }
13163
13164    #[test]
13165    fn prove_all_generates_proof_on_delivery() {
13166        let (tx, rx) = event::channel();
13167        let (cbs, _, _, deliveries, _, _) = MockCallbacks::new();
13168        let mut driver = Driver::new(
13169            TransportConfig {
13170                transport_enabled: false,
13171                identity_hash: None,
13172                prefer_shorter_path: false,
13173                max_paths_per_destination: 1,
13174                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
13175                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
13176                max_path_destinations: usize::MAX,
13177                max_tunnel_destinations_total: usize::MAX,
13178                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
13179                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
13180                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
13181                announce_sig_cache_enabled: true,
13182                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
13183                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
13184                announce_queue_max_entries: 256,
13185                announce_queue_max_interfaces: 1024,
13186            },
13187            rx,
13188            tx.clone(),
13189            Box::new(cbs),
13190        );
13191        let info = make_interface_info(1);
13192        driver.engine.register_interface(info);
13193        let (writer, sent) = MockWriter::new();
13194        driver
13195            .interfaces
13196            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
13197
13198        // Register a destination with ProveAll
13199        let dest = [0xDD; 16];
13200        let identity = Identity::new(&mut OsRng);
13201        let prv_key = identity.get_private_key().unwrap();
13202        driver
13203            .engine
13204            .register_destination(dest, constants::DESTINATION_SINGLE);
13205        driver.proof_strategies.insert(
13206            dest,
13207            (
13208                rns_core::types::ProofStrategy::ProveAll,
13209                Some(Identity::from_private_key(&prv_key)),
13210            ),
13211        );
13212
13213        // Send a DATA packet to that destination
13214        let flags = PacketFlags {
13215            header_type: constants::HEADER_1,
13216            context_flag: constants::FLAG_UNSET,
13217            transport_type: constants::TRANSPORT_BROADCAST,
13218            destination_type: constants::DESTINATION_SINGLE,
13219            packet_type: constants::PACKET_TYPE_DATA,
13220        };
13221        let packet =
13222            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"hello").unwrap();
13223
13224        tx.send(Event::Frame {
13225            interface_id: InterfaceId(1),
13226            data: packet.raw,
13227        })
13228        .unwrap();
13229        tx.send(Event::Shutdown).unwrap();
13230        driver.run();
13231
13232        // Should have delivered the packet
13233        assert_eq!(deliveries.lock().unwrap().len(), 1);
13234
13235        // Should have sent at least one proof packet on the wire
13236        let sent_packets = sent.lock().unwrap();
13237        // The original DATA is not sent out (it was delivered locally), but a PROOF should be
13238        let has_proof = sent_packets.iter().any(|raw| {
13239            let flags = PacketFlags::unpack(raw[0] & 0x7F);
13240            flags.packet_type == constants::PACKET_TYPE_PROOF
13241        });
13242        assert!(
13243            has_proof,
13244            "ProveAll should generate a proof packet: sent {} packets",
13245            sent_packets.len()
13246        );
13247    }
13248
13249    #[test]
13250    fn prove_none_does_not_generate_proof() {
13251        let (tx, rx) = event::channel();
13252        let (cbs, _, _, deliveries, _, _) = MockCallbacks::new();
13253        let mut driver = Driver::new(
13254            TransportConfig {
13255                transport_enabled: false,
13256                identity_hash: None,
13257                prefer_shorter_path: false,
13258                max_paths_per_destination: 1,
13259                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
13260                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
13261                max_path_destinations: usize::MAX,
13262                max_tunnel_destinations_total: usize::MAX,
13263                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
13264                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
13265                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
13266                announce_sig_cache_enabled: true,
13267                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
13268                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
13269                announce_queue_max_entries: 256,
13270                announce_queue_max_interfaces: 1024,
13271            },
13272            rx,
13273            tx.clone(),
13274            Box::new(cbs),
13275        );
13276        let info = make_interface_info(1);
13277        driver.engine.register_interface(info);
13278        let (writer, sent) = MockWriter::new();
13279        driver
13280            .interfaces
13281            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
13282
13283        // Register a destination with ProveNone
13284        let dest = [0xDD; 16];
13285        driver
13286            .engine
13287            .register_destination(dest, constants::DESTINATION_SINGLE);
13288        driver
13289            .proof_strategies
13290            .insert(dest, (rns_core::types::ProofStrategy::ProveNone, None));
13291
13292        // Send a DATA packet to that destination
13293        let flags = PacketFlags {
13294            header_type: constants::HEADER_1,
13295            context_flag: constants::FLAG_UNSET,
13296            transport_type: constants::TRANSPORT_BROADCAST,
13297            destination_type: constants::DESTINATION_SINGLE,
13298            packet_type: constants::PACKET_TYPE_DATA,
13299        };
13300        let packet =
13301            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"hello").unwrap();
13302
13303        tx.send(Event::Frame {
13304            interface_id: InterfaceId(1),
13305            data: packet.raw,
13306        })
13307        .unwrap();
13308        tx.send(Event::Shutdown).unwrap();
13309        driver.run();
13310
13311        // Should have delivered the packet
13312        assert_eq!(deliveries.lock().unwrap().len(), 1);
13313
13314        // Should NOT have sent any proof
13315        let sent_packets = sent.lock().unwrap();
13316        let has_proof = sent_packets.iter().any(|raw| {
13317            let flags = PacketFlags::unpack(raw[0] & 0x7F);
13318            flags.packet_type == constants::PACKET_TYPE_PROOF
13319        });
13320        assert!(!has_proof, "ProveNone should not generate a proof packet");
13321    }
13322
13323    #[test]
13324    fn no_proof_strategy_does_not_generate_proof() {
13325        let (tx, rx) = event::channel();
13326        let (cbs, _, _, deliveries, _, _) = MockCallbacks::new();
13327        let mut driver = Driver::new(
13328            TransportConfig {
13329                transport_enabled: false,
13330                identity_hash: None,
13331                prefer_shorter_path: false,
13332                max_paths_per_destination: 1,
13333                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
13334                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
13335                max_path_destinations: usize::MAX,
13336                max_tunnel_destinations_total: usize::MAX,
13337                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
13338                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
13339                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
13340                announce_sig_cache_enabled: true,
13341                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
13342                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
13343                announce_queue_max_entries: 256,
13344                announce_queue_max_interfaces: 1024,
13345            },
13346            rx,
13347            tx.clone(),
13348            Box::new(cbs),
13349        );
13350        let info = make_interface_info(1);
13351        driver.engine.register_interface(info);
13352        let (writer, sent) = MockWriter::new();
13353        driver
13354            .interfaces
13355            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
13356
13357        // Register destination but NO proof strategy
13358        let dest = [0xDD; 16];
13359        driver
13360            .engine
13361            .register_destination(dest, constants::DESTINATION_SINGLE);
13362
13363        let flags = PacketFlags {
13364            header_type: constants::HEADER_1,
13365            context_flag: constants::FLAG_UNSET,
13366            transport_type: constants::TRANSPORT_BROADCAST,
13367            destination_type: constants::DESTINATION_SINGLE,
13368            packet_type: constants::PACKET_TYPE_DATA,
13369        };
13370        let packet =
13371            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"hello").unwrap();
13372
13373        tx.send(Event::Frame {
13374            interface_id: InterfaceId(1),
13375            data: packet.raw,
13376        })
13377        .unwrap();
13378        tx.send(Event::Shutdown).unwrap();
13379        driver.run();
13380
13381        assert_eq!(deliveries.lock().unwrap().len(), 1);
13382
13383        let sent_packets = sent.lock().unwrap();
13384        let has_proof = sent_packets.iter().any(|raw| {
13385            let flags = PacketFlags::unpack(raw[0] & 0x7F);
13386            flags.packet_type == constants::PACKET_TYPE_PROOF
13387        });
13388        assert!(!has_proof, "No proof strategy means no proof generated");
13389    }
13390
13391    #[test]
13392    fn prove_app_calls_callback() {
13393        let (tx, rx) = event::channel();
13394        let proof_requested = Arc::new(Mutex::new(Vec::new()));
13395        let deliveries = Arc::new(Mutex::new(Vec::new()));
13396        let cbs = MockCallbacks {
13397            announces: Arc::new(Mutex::new(Vec::new())),
13398            paths: Arc::new(Mutex::new(Vec::new())),
13399            deliveries: deliveries.clone(),
13400            iface_ups: Arc::new(Mutex::new(Vec::new())),
13401            iface_downs: Arc::new(Mutex::new(Vec::new())),
13402            link_established: Arc::new(Mutex::new(Vec::new())),
13403            link_closed: Arc::new(Mutex::new(Vec::new())),
13404            remote_identified: Arc::new(Mutex::new(Vec::new())),
13405            resources_received: Arc::new(Mutex::new(Vec::new())),
13406            resource_completed: Arc::new(Mutex::new(Vec::new())),
13407            resource_failed: Arc::new(Mutex::new(Vec::new())),
13408            channel_messages: Arc::new(Mutex::new(Vec::new())),
13409            link_data: Arc::new(Mutex::new(Vec::new())),
13410            responses: Arc::new(Mutex::new(Vec::new())),
13411            proofs: Arc::new(Mutex::new(Vec::new())),
13412            proof_requested: proof_requested.clone(),
13413        };
13414
13415        let mut driver = Driver::new(
13416            TransportConfig {
13417                transport_enabled: false,
13418                identity_hash: None,
13419                prefer_shorter_path: false,
13420                max_paths_per_destination: 1,
13421                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
13422                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
13423                max_path_destinations: usize::MAX,
13424                max_tunnel_destinations_total: usize::MAX,
13425                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
13426                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
13427                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
13428                announce_sig_cache_enabled: true,
13429                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
13430                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
13431                announce_queue_max_entries: 256,
13432                announce_queue_max_interfaces: 1024,
13433            },
13434            rx,
13435            tx.clone(),
13436            Box::new(cbs),
13437        );
13438        let info = make_interface_info(1);
13439        driver.engine.register_interface(info);
13440        let (writer, sent) = MockWriter::new();
13441        driver
13442            .interfaces
13443            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
13444
13445        // Register dest with ProveApp
13446        let dest = [0xDD; 16];
13447        let identity = Identity::new(&mut OsRng);
13448        let prv_key = identity.get_private_key().unwrap();
13449        driver
13450            .engine
13451            .register_destination(dest, constants::DESTINATION_SINGLE);
13452        driver.proof_strategies.insert(
13453            dest,
13454            (
13455                rns_core::types::ProofStrategy::ProveApp,
13456                Some(Identity::from_private_key(&prv_key)),
13457            ),
13458        );
13459
13460        let flags = PacketFlags {
13461            header_type: constants::HEADER_1,
13462            context_flag: constants::FLAG_UNSET,
13463            transport_type: constants::TRANSPORT_BROADCAST,
13464            destination_type: constants::DESTINATION_SINGLE,
13465            packet_type: constants::PACKET_TYPE_DATA,
13466        };
13467        let packet =
13468            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"app test").unwrap();
13469
13470        tx.send(Event::Frame {
13471            interface_id: InterfaceId(1),
13472            data: packet.raw,
13473        })
13474        .unwrap();
13475        tx.send(Event::Shutdown).unwrap();
13476        driver.run();
13477
13478        // on_proof_requested should have been called
13479        let prs = proof_requested.lock().unwrap();
13480        assert_eq!(prs.len(), 1);
13481        assert_eq!(prs[0].0, DestHash(dest));
13482
13483        // Since our mock returns true, a proof should also have been sent
13484        let sent_packets = sent.lock().unwrap();
13485        let has_proof = sent_packets.iter().any(|raw| {
13486            let flags = PacketFlags::unpack(raw[0] & 0x7F);
13487            flags.packet_type == constants::PACKET_TYPE_PROOF
13488        });
13489        assert!(
13490            has_proof,
13491            "ProveApp (callback returns true) should generate a proof"
13492        );
13493    }
13494
13495    #[test]
13496    fn inbound_proof_fires_callback() {
13497        let (tx, rx) = event::channel();
13498        let proofs = Arc::new(Mutex::new(Vec::new()));
13499        let cbs = MockCallbacks {
13500            announces: Arc::new(Mutex::new(Vec::new())),
13501            paths: Arc::new(Mutex::new(Vec::new())),
13502            deliveries: Arc::new(Mutex::new(Vec::new())),
13503            iface_ups: Arc::new(Mutex::new(Vec::new())),
13504            iface_downs: Arc::new(Mutex::new(Vec::new())),
13505            link_established: Arc::new(Mutex::new(Vec::new())),
13506            link_closed: Arc::new(Mutex::new(Vec::new())),
13507            remote_identified: Arc::new(Mutex::new(Vec::new())),
13508            resources_received: Arc::new(Mutex::new(Vec::new())),
13509            resource_completed: Arc::new(Mutex::new(Vec::new())),
13510            resource_failed: Arc::new(Mutex::new(Vec::new())),
13511            channel_messages: Arc::new(Mutex::new(Vec::new())),
13512            link_data: Arc::new(Mutex::new(Vec::new())),
13513            responses: Arc::new(Mutex::new(Vec::new())),
13514            proofs: proofs.clone(),
13515            proof_requested: Arc::new(Mutex::new(Vec::new())),
13516        };
13517
13518        let mut driver = Driver::new(
13519            TransportConfig {
13520                transport_enabled: false,
13521                identity_hash: None,
13522                prefer_shorter_path: false,
13523                max_paths_per_destination: 1,
13524                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
13525                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
13526                max_path_destinations: usize::MAX,
13527                max_tunnel_destinations_total: usize::MAX,
13528                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
13529                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
13530                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
13531                announce_sig_cache_enabled: true,
13532                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
13533                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
13534                announce_queue_max_entries: 256,
13535                announce_queue_max_interfaces: 1024,
13536            },
13537            rx,
13538            tx.clone(),
13539            Box::new(cbs),
13540        );
13541        let info = make_interface_info(1);
13542        driver.engine.register_interface(info);
13543        let (writer, _sent) = MockWriter::new();
13544        driver
13545            .interfaces
13546            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
13547
13548        // Register a destination so proof packets can be delivered locally
13549        let dest = [0xEE; 16];
13550        driver
13551            .engine
13552            .register_destination(dest, constants::DESTINATION_SINGLE);
13553
13554        // Simulate a sent packet that we're tracking
13555        let tracked_hash = [0x42u8; 32];
13556        let sent_time = time::now() - 0.5; // 500ms ago
13557        driver.sent_packets.insert(tracked_hash, (dest, sent_time));
13558
13559        // Build a PROOF packet with the tracked hash + dummy signature
13560        let mut proof_data = Vec::new();
13561        proof_data.extend_from_slice(&tracked_hash);
13562        proof_data.extend_from_slice(&[0xAA; 64]); // dummy signature
13563
13564        let flags = PacketFlags {
13565            header_type: constants::HEADER_1,
13566            context_flag: constants::FLAG_UNSET,
13567            transport_type: constants::TRANSPORT_BROADCAST,
13568            destination_type: constants::DESTINATION_SINGLE,
13569            packet_type: constants::PACKET_TYPE_PROOF,
13570        };
13571        let packet =
13572            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, &proof_data).unwrap();
13573
13574        tx.send(Event::Frame {
13575            interface_id: InterfaceId(1),
13576            data: packet.raw,
13577        })
13578        .unwrap();
13579        tx.send(Event::Shutdown).unwrap();
13580        driver.run();
13581
13582        // on_proof callback should have been fired
13583        let proof_list = proofs.lock().unwrap();
13584        assert_eq!(proof_list.len(), 1);
13585        assert_eq!(proof_list[0].0, DestHash(dest));
13586        assert_eq!(proof_list[0].1, PacketHash(tracked_hash));
13587        assert!(
13588            proof_list[0].2 >= 0.4,
13589            "RTT should be approximately 0.5s, got {}",
13590            proof_list[0].2
13591        );
13592
13593        // Tracked packet should be removed
13594        assert!(!driver.sent_packets.contains_key(&tracked_hash));
13595    }
13596
13597    #[test]
13598    fn inbound_proof_for_unknown_packet_is_ignored() {
13599        let (tx, rx) = event::channel();
13600        let proofs = Arc::new(Mutex::new(Vec::new()));
13601        let cbs = MockCallbacks {
13602            announces: Arc::new(Mutex::new(Vec::new())),
13603            paths: Arc::new(Mutex::new(Vec::new())),
13604            deliveries: Arc::new(Mutex::new(Vec::new())),
13605            iface_ups: Arc::new(Mutex::new(Vec::new())),
13606            iface_downs: Arc::new(Mutex::new(Vec::new())),
13607            link_established: Arc::new(Mutex::new(Vec::new())),
13608            link_closed: Arc::new(Mutex::new(Vec::new())),
13609            remote_identified: Arc::new(Mutex::new(Vec::new())),
13610            resources_received: Arc::new(Mutex::new(Vec::new())),
13611            resource_completed: Arc::new(Mutex::new(Vec::new())),
13612            resource_failed: Arc::new(Mutex::new(Vec::new())),
13613            channel_messages: Arc::new(Mutex::new(Vec::new())),
13614            link_data: Arc::new(Mutex::new(Vec::new())),
13615            responses: Arc::new(Mutex::new(Vec::new())),
13616            proofs: proofs.clone(),
13617            proof_requested: Arc::new(Mutex::new(Vec::new())),
13618        };
13619
13620        let mut driver = Driver::new(
13621            TransportConfig {
13622                transport_enabled: false,
13623                identity_hash: None,
13624                prefer_shorter_path: false,
13625                max_paths_per_destination: 1,
13626                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
13627                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
13628                max_path_destinations: usize::MAX,
13629                max_tunnel_destinations_total: usize::MAX,
13630                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
13631                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
13632                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
13633                announce_sig_cache_enabled: true,
13634                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
13635                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
13636                announce_queue_max_entries: 256,
13637                announce_queue_max_interfaces: 1024,
13638            },
13639            rx,
13640            tx.clone(),
13641            Box::new(cbs),
13642        );
13643        let info = make_interface_info(1);
13644        driver.engine.register_interface(info);
13645        let (writer, _sent) = MockWriter::new();
13646        driver
13647            .interfaces
13648            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
13649
13650        let dest = [0xEE; 16];
13651        driver
13652            .engine
13653            .register_destination(dest, constants::DESTINATION_SINGLE);
13654
13655        // Build a PROOF packet for an untracked hash
13656        let unknown_hash = [0xFF; 32];
13657        let mut proof_data = Vec::new();
13658        proof_data.extend_from_slice(&unknown_hash);
13659        proof_data.extend_from_slice(&[0xAA; 64]);
13660
13661        let flags = PacketFlags {
13662            header_type: constants::HEADER_1,
13663            context_flag: constants::FLAG_UNSET,
13664            transport_type: constants::TRANSPORT_BROADCAST,
13665            destination_type: constants::DESTINATION_SINGLE,
13666            packet_type: constants::PACKET_TYPE_PROOF,
13667        };
13668        let packet =
13669            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, &proof_data).unwrap();
13670
13671        tx.send(Event::Frame {
13672            interface_id: InterfaceId(1),
13673            data: packet.raw,
13674        })
13675        .unwrap();
13676        tx.send(Event::Shutdown).unwrap();
13677        driver.run();
13678
13679        // on_proof should NOT have been called
13680        assert!(proofs.lock().unwrap().is_empty());
13681    }
13682
13683    #[test]
13684    fn inbound_implicit_proof_matches_truncated_destination() {
13685        let (tx, rx) = event::channel();
13686        let proofs = Arc::new(Mutex::new(Vec::new()));
13687        let cbs = MockCallbacks {
13688            announces: Arc::new(Mutex::new(Vec::new())),
13689            paths: Arc::new(Mutex::new(Vec::new())),
13690            deliveries: Arc::new(Mutex::new(Vec::new())),
13691            iface_ups: Arc::new(Mutex::new(Vec::new())),
13692            iface_downs: Arc::new(Mutex::new(Vec::new())),
13693            link_established: Arc::new(Mutex::new(Vec::new())),
13694            link_closed: Arc::new(Mutex::new(Vec::new())),
13695            remote_identified: Arc::new(Mutex::new(Vec::new())),
13696            resources_received: Arc::new(Mutex::new(Vec::new())),
13697            resource_completed: Arc::new(Mutex::new(Vec::new())),
13698            resource_failed: Arc::new(Mutex::new(Vec::new())),
13699            channel_messages: Arc::new(Mutex::new(Vec::new())),
13700            link_data: Arc::new(Mutex::new(Vec::new())),
13701            responses: Arc::new(Mutex::new(Vec::new())),
13702            proofs: proofs.clone(),
13703            proof_requested: Arc::new(Mutex::new(Vec::new())),
13704        };
13705
13706        let mut driver = Driver::new(
13707            TransportConfig {
13708                transport_enabled: false,
13709                identity_hash: None,
13710                prefer_shorter_path: false,
13711                max_paths_per_destination: 1,
13712                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
13713                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
13714                max_path_destinations: usize::MAX,
13715                max_tunnel_destinations_total: usize::MAX,
13716                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
13717                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
13718                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
13719                announce_sig_cache_enabled: true,
13720                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
13721                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
13722                announce_queue_max_entries: 256,
13723                announce_queue_max_interfaces: 1024,
13724            },
13725            rx,
13726            tx.clone(),
13727            Box::new(cbs),
13728        );
13729        let info = make_interface_info(1);
13730        driver.engine.register_interface(info);
13731        let (writer, _sent) = MockWriter::new();
13732        driver
13733            .interfaces
13734            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
13735
13736        let tracked_hash = [0x3Cu8; 32];
13737        let sent_time = time::now() - 0.25;
13738        driver
13739            .sent_packets
13740            .insert(tracked_hash, ([0xEE; 16], sent_time));
13741
13742        let mut proof_dest = [0u8; 16];
13743        proof_dest.copy_from_slice(&tracked_hash[..16]);
13744        driver
13745            .engine
13746            .register_destination(proof_dest, constants::DESTINATION_SINGLE);
13747
13748        // Implicit proof is signature-only (64 bytes)
13749        let proof_data = vec![0xAA; 64];
13750        let flags = PacketFlags {
13751            header_type: constants::HEADER_1,
13752            context_flag: constants::FLAG_UNSET,
13753            transport_type: constants::TRANSPORT_BROADCAST,
13754            destination_type: constants::DESTINATION_SINGLE,
13755            packet_type: constants::PACKET_TYPE_PROOF,
13756        };
13757        let packet =
13758            RawPacket::pack(flags, 0, &proof_dest, None, constants::CONTEXT_NONE, &proof_data)
13759                .unwrap();
13760
13761        tx.send(Event::Frame {
13762            interface_id: InterfaceId(1),
13763            data: packet.raw,
13764        })
13765        .unwrap();
13766        tx.send(Event::Shutdown).unwrap();
13767        driver.run();
13768
13769        let proof_list = proofs.lock().unwrap();
13770        assert_eq!(proof_list.len(), 1);
13771        assert_eq!(proof_list[0].0, DestHash([0xEE; 16]));
13772        assert_eq!(proof_list[0].1, PacketHash(tracked_hash));
13773        assert!(!driver.sent_packets.contains_key(&tracked_hash));
13774    }
13775
13776    #[test]
13777    fn link_manager_data_send_is_tracked_for_proofs() {
13778        let mut driver = new_test_driver();
13779        let (writer, _sent) = MockWriter::new();
13780        driver
13781            .interfaces
13782            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
13783
13784        let flags = PacketFlags {
13785            header_type: constants::HEADER_1,
13786            context_flag: constants::FLAG_UNSET,
13787            transport_type: constants::TRANSPORT_BROADCAST,
13788            destination_type: constants::DESTINATION_LINK,
13789            packet_type: constants::PACKET_TYPE_DATA,
13790        };
13791        let packet = RawPacket::pack(
13792            flags,
13793            0,
13794            &[0x77; 16],
13795            None,
13796            constants::CONTEXT_NONE,
13797            b"track me",
13798        )
13799        .unwrap();
13800        let packet_hash = packet.packet_hash;
13801        let destination_hash = packet.destination_hash;
13802
13803        driver.dispatch_link_actions(vec![LinkManagerAction::SendPacket {
13804            raw: packet.raw,
13805            dest_type: constants::DESTINATION_LINK,
13806            attached_interface: Some(InterfaceId(1)),
13807        }]);
13808
13809        assert_eq!(
13810            driver
13811                .sent_packets
13812                .get(&packet_hash)
13813                .map(|(dest, _)| *dest),
13814            Some(destination_hash)
13815        );
13816    }
13817
13818    #[test]
13819    fn inbound_proof_with_valid_signature_fires_callback() {
13820        // When the destination IS in known_destinations, the proof signature is verified
13821        let (tx, rx) = event::channel();
13822        let proofs = Arc::new(Mutex::new(Vec::new()));
13823        let cbs = MockCallbacks {
13824            announces: Arc::new(Mutex::new(Vec::new())),
13825            paths: Arc::new(Mutex::new(Vec::new())),
13826            deliveries: Arc::new(Mutex::new(Vec::new())),
13827            iface_ups: Arc::new(Mutex::new(Vec::new())),
13828            iface_downs: Arc::new(Mutex::new(Vec::new())),
13829            link_established: Arc::new(Mutex::new(Vec::new())),
13830            link_closed: Arc::new(Mutex::new(Vec::new())),
13831            remote_identified: Arc::new(Mutex::new(Vec::new())),
13832            resources_received: Arc::new(Mutex::new(Vec::new())),
13833            resource_completed: Arc::new(Mutex::new(Vec::new())),
13834            resource_failed: Arc::new(Mutex::new(Vec::new())),
13835            channel_messages: Arc::new(Mutex::new(Vec::new())),
13836            link_data: Arc::new(Mutex::new(Vec::new())),
13837            responses: Arc::new(Mutex::new(Vec::new())),
13838            proofs: proofs.clone(),
13839            proof_requested: Arc::new(Mutex::new(Vec::new())),
13840        };
13841
13842        let mut driver = Driver::new(
13843            TransportConfig {
13844                transport_enabled: false,
13845                identity_hash: None,
13846                prefer_shorter_path: false,
13847                max_paths_per_destination: 1,
13848                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
13849                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
13850                max_path_destinations: usize::MAX,
13851                max_tunnel_destinations_total: usize::MAX,
13852                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
13853                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
13854                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
13855                announce_sig_cache_enabled: true,
13856                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
13857                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
13858                announce_queue_max_entries: 256,
13859                announce_queue_max_interfaces: 1024,
13860            },
13861            rx,
13862            tx.clone(),
13863            Box::new(cbs),
13864        );
13865        let info = make_interface_info(1);
13866        driver.engine.register_interface(info);
13867        let (writer, _sent) = MockWriter::new();
13868        driver
13869            .interfaces
13870            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
13871
13872        let dest = [0xEE; 16];
13873        driver
13874            .engine
13875            .register_destination(dest, constants::DESTINATION_SINGLE);
13876
13877        // Create real identity and add to known_destinations
13878        let identity = Identity::new(&mut OsRng);
13879        let pub_key = identity.get_public_key();
13880        driver.known_destinations.insert(
13881            dest,
13882            crate::destination::AnnouncedIdentity {
13883                dest_hash: DestHash(dest),
13884                identity_hash: IdentityHash(*identity.hash()),
13885                public_key: pub_key.unwrap(),
13886                app_data: None,
13887                hops: 0,
13888                received_at: time::now(),
13889                receiving_interface: InterfaceId(0),
13890            },
13891        );
13892
13893        // Sign a packet hash with the identity
13894        let tracked_hash = [0x42u8; 32];
13895        let sent_time = time::now() - 0.5;
13896        driver.sent_packets.insert(tracked_hash, (dest, sent_time));
13897
13898        let signature = identity.sign(&tracked_hash).unwrap();
13899        let mut proof_data = Vec::new();
13900        proof_data.extend_from_slice(&tracked_hash);
13901        proof_data.extend_from_slice(&signature);
13902
13903        let flags = PacketFlags {
13904            header_type: constants::HEADER_1,
13905            context_flag: constants::FLAG_UNSET,
13906            transport_type: constants::TRANSPORT_BROADCAST,
13907            destination_type: constants::DESTINATION_SINGLE,
13908            packet_type: constants::PACKET_TYPE_PROOF,
13909        };
13910        let packet =
13911            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, &proof_data).unwrap();
13912
13913        tx.send(Event::Frame {
13914            interface_id: InterfaceId(1),
13915            data: packet.raw,
13916        })
13917        .unwrap();
13918        tx.send(Event::Shutdown).unwrap();
13919        driver.run();
13920
13921        // Valid signature: on_proof should fire
13922        let proof_list = proofs.lock().unwrap();
13923        assert_eq!(proof_list.len(), 1);
13924        assert_eq!(proof_list[0].0, DestHash(dest));
13925        assert_eq!(proof_list[0].1, PacketHash(tracked_hash));
13926    }
13927
13928    #[test]
13929    fn inbound_proof_with_invalid_signature_rejected() {
13930        // When known_destinations has the public key, bad signatures are rejected
13931        let (tx, rx) = event::channel();
13932        let proofs = Arc::new(Mutex::new(Vec::new()));
13933        let cbs = MockCallbacks {
13934            announces: Arc::new(Mutex::new(Vec::new())),
13935            paths: Arc::new(Mutex::new(Vec::new())),
13936            deliveries: Arc::new(Mutex::new(Vec::new())),
13937            iface_ups: Arc::new(Mutex::new(Vec::new())),
13938            iface_downs: Arc::new(Mutex::new(Vec::new())),
13939            link_established: Arc::new(Mutex::new(Vec::new())),
13940            link_closed: Arc::new(Mutex::new(Vec::new())),
13941            remote_identified: Arc::new(Mutex::new(Vec::new())),
13942            resources_received: Arc::new(Mutex::new(Vec::new())),
13943            resource_completed: Arc::new(Mutex::new(Vec::new())),
13944            resource_failed: Arc::new(Mutex::new(Vec::new())),
13945            channel_messages: Arc::new(Mutex::new(Vec::new())),
13946            link_data: Arc::new(Mutex::new(Vec::new())),
13947            responses: Arc::new(Mutex::new(Vec::new())),
13948            proofs: proofs.clone(),
13949            proof_requested: Arc::new(Mutex::new(Vec::new())),
13950        };
13951
13952        let mut driver = Driver::new(
13953            TransportConfig {
13954                transport_enabled: false,
13955                identity_hash: None,
13956                prefer_shorter_path: false,
13957                max_paths_per_destination: 1,
13958                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
13959                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
13960                max_path_destinations: usize::MAX,
13961                max_tunnel_destinations_total: usize::MAX,
13962                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
13963                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
13964                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
13965                announce_sig_cache_enabled: true,
13966                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
13967                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
13968                announce_queue_max_entries: 256,
13969                announce_queue_max_interfaces: 1024,
13970            },
13971            rx,
13972            tx.clone(),
13973            Box::new(cbs),
13974        );
13975        let info = make_interface_info(1);
13976        driver.engine.register_interface(info);
13977        let (writer, _sent) = MockWriter::new();
13978        driver
13979            .interfaces
13980            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
13981
13982        let dest = [0xEE; 16];
13983        driver
13984            .engine
13985            .register_destination(dest, constants::DESTINATION_SINGLE);
13986
13987        // Create identity and add to known_destinations
13988        let identity = Identity::new(&mut OsRng);
13989        let pub_key = identity.get_public_key();
13990        driver.known_destinations.insert(
13991            dest,
13992            crate::destination::AnnouncedIdentity {
13993                dest_hash: DestHash(dest),
13994                identity_hash: IdentityHash(*identity.hash()),
13995                public_key: pub_key.unwrap(),
13996                app_data: None,
13997                hops: 0,
13998                received_at: time::now(),
13999                receiving_interface: InterfaceId(0),
14000            },
14001        );
14002
14003        // Track a sent packet
14004        let tracked_hash = [0x42u8; 32];
14005        let sent_time = time::now() - 0.5;
14006        driver.sent_packets.insert(tracked_hash, (dest, sent_time));
14007
14008        // Use WRONG signature (all 0xAA — invalid for this identity)
14009        let mut proof_data = Vec::new();
14010        proof_data.extend_from_slice(&tracked_hash);
14011        proof_data.extend_from_slice(&[0xAA; 64]);
14012
14013        let flags = PacketFlags {
14014            header_type: constants::HEADER_1,
14015            context_flag: constants::FLAG_UNSET,
14016            transport_type: constants::TRANSPORT_BROADCAST,
14017            destination_type: constants::DESTINATION_SINGLE,
14018            packet_type: constants::PACKET_TYPE_PROOF,
14019        };
14020        let packet =
14021            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, &proof_data).unwrap();
14022
14023        tx.send(Event::Frame {
14024            interface_id: InterfaceId(1),
14025            data: packet.raw,
14026        })
14027        .unwrap();
14028        tx.send(Event::Shutdown).unwrap();
14029        driver.run();
14030
14031        // Invalid signature: on_proof should NOT fire
14032        assert!(proofs.lock().unwrap().is_empty());
14033    }
14034
14035    #[test]
14036    fn proof_data_is_valid_explicit_proof() {
14037        // Verify that the proof generated by ProveAll is a valid explicit proof
14038        let (tx, rx) = event::channel();
14039        let (cbs, _, _, _, _, _) = MockCallbacks::new();
14040        let mut driver = Driver::new(
14041            TransportConfig {
14042                transport_enabled: false,
14043                identity_hash: None,
14044                prefer_shorter_path: false,
14045                max_paths_per_destination: 1,
14046                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14047                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14048                max_path_destinations: usize::MAX,
14049                max_tunnel_destinations_total: usize::MAX,
14050                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14051                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14052                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14053                announce_sig_cache_enabled: true,
14054                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14055                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14056                announce_queue_max_entries: 256,
14057                announce_queue_max_interfaces: 1024,
14058            },
14059            rx,
14060            tx.clone(),
14061            Box::new(cbs),
14062        );
14063        let info = make_interface_info(1);
14064        driver.engine.register_interface(info);
14065        let (writer, sent) = MockWriter::new();
14066        driver
14067            .interfaces
14068            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
14069
14070        let dest = [0xDD; 16];
14071        let identity = Identity::new(&mut OsRng);
14072        let prv_key = identity.get_private_key().unwrap();
14073        driver
14074            .engine
14075            .register_destination(dest, constants::DESTINATION_SINGLE);
14076        driver.proof_strategies.insert(
14077            dest,
14078            (
14079                rns_core::types::ProofStrategy::ProveAll,
14080                Some(Identity::from_private_key(&prv_key)),
14081            ),
14082        );
14083
14084        let flags = PacketFlags {
14085            header_type: constants::HEADER_1,
14086            context_flag: constants::FLAG_UNSET,
14087            transport_type: constants::TRANSPORT_BROADCAST,
14088            destination_type: constants::DESTINATION_SINGLE,
14089            packet_type: constants::PACKET_TYPE_DATA,
14090        };
14091        let data_packet =
14092            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"verify me").unwrap();
14093        let data_packet_hash = data_packet.packet_hash;
14094
14095        tx.send(Event::Frame {
14096            interface_id: InterfaceId(1),
14097            data: data_packet.raw,
14098        })
14099        .unwrap();
14100        tx.send(Event::Shutdown).unwrap();
14101        driver.run();
14102
14103        // Find the proof packet in sent
14104        let sent_packets = sent.lock().unwrap();
14105        let proof_raw = sent_packets.iter().find(|raw| {
14106            let f = PacketFlags::unpack(raw[0] & 0x7F);
14107            f.packet_type == constants::PACKET_TYPE_PROOF
14108        });
14109        assert!(proof_raw.is_some(), "Should have sent a proof");
14110
14111        let proof_packet = RawPacket::unpack(proof_raw.unwrap()).unwrap();
14112        // Proof data should be 96 bytes: packet_hash(32) + signature(64)
14113        assert_eq!(
14114            proof_packet.data.len(),
14115            96,
14116            "Explicit proof should be 96 bytes"
14117        );
14118
14119        // Validate using rns-core's receipt module
14120        let result = rns_core::receipt::validate_proof(
14121            &proof_packet.data,
14122            &data_packet_hash,
14123            &Identity::from_private_key(&prv_key), // same identity
14124        );
14125        assert_eq!(result, rns_core::receipt::ProofResult::Valid);
14126    }
14127
14128    #[test]
14129    fn query_local_destinations_empty() {
14130        let (tx, rx) = event::channel();
14131        let (cbs, _, _, _, _, _) = MockCallbacks::new();
14132        let driver_config = TransportConfig {
14133            transport_enabled: false,
14134            identity_hash: None,
14135            prefer_shorter_path: false,
14136            max_paths_per_destination: 1,
14137            packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14138            max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14139            max_path_destinations: usize::MAX,
14140            max_tunnel_destinations_total: usize::MAX,
14141            destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14142            announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14143            announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14144            announce_sig_cache_enabled: true,
14145            announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14146            announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14147            announce_queue_max_entries: 256,
14148            announce_queue_max_interfaces: 1024,
14149        };
14150        let mut driver = Driver::new(driver_config, rx, tx.clone(), Box::new(cbs));
14151
14152        let (resp_tx, resp_rx) = mpsc::channel();
14153        tx.send(Event::Query(QueryRequest::LocalDestinations, resp_tx))
14154            .unwrap();
14155        tx.send(Event::Shutdown).unwrap();
14156        driver.run();
14157
14158        match resp_rx.recv().unwrap() {
14159            QueryResponse::LocalDestinations(entries) => {
14160                // Should contain the two internal destinations (tunnel_synth + path_request)
14161                assert_eq!(entries.len(), 2);
14162                for entry in &entries {
14163                    assert_eq!(entry.dest_type, rns_core::constants::DESTINATION_PLAIN);
14164                }
14165            }
14166            other => panic!("expected LocalDestinations, got {:?}", other),
14167        }
14168    }
14169
14170    #[test]
14171    fn query_local_destinations_with_registered() {
14172        let (tx, rx) = event::channel();
14173        let (cbs, _, _, _, _, _) = MockCallbacks::new();
14174        let driver_config = TransportConfig {
14175            transport_enabled: false,
14176            identity_hash: None,
14177            prefer_shorter_path: false,
14178            max_paths_per_destination: 1,
14179            packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14180            max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14181            max_path_destinations: usize::MAX,
14182            max_tunnel_destinations_total: usize::MAX,
14183            destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14184            announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14185            announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14186            announce_sig_cache_enabled: true,
14187            announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14188            announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14189            announce_queue_max_entries: 256,
14190            announce_queue_max_interfaces: 1024,
14191        };
14192        let mut driver = Driver::new(driver_config, rx, tx.clone(), Box::new(cbs));
14193
14194        let dest_hash = [0xAA; 16];
14195        tx.send(Event::RegisterDestination {
14196            dest_hash,
14197            dest_type: rns_core::constants::DESTINATION_SINGLE,
14198        })
14199        .unwrap();
14200
14201        let (resp_tx, resp_rx) = mpsc::channel();
14202        tx.send(Event::Query(QueryRequest::LocalDestinations, resp_tx))
14203            .unwrap();
14204        tx.send(Event::Shutdown).unwrap();
14205        driver.run();
14206
14207        match resp_rx.recv().unwrap() {
14208            QueryResponse::LocalDestinations(entries) => {
14209                // 2 internal + 1 registered
14210                assert_eq!(entries.len(), 3);
14211                assert!(entries.iter().any(|e| e.hash == dest_hash
14212                    && e.dest_type == rns_core::constants::DESTINATION_SINGLE));
14213            }
14214            other => panic!("expected LocalDestinations, got {:?}", other),
14215        }
14216    }
14217
14218    #[test]
14219    fn query_local_destinations_tracks_link_dest() {
14220        let (tx, rx) = event::channel();
14221        let (cbs, _, _, _, _, _) = MockCallbacks::new();
14222        let driver_config = TransportConfig {
14223            transport_enabled: false,
14224            identity_hash: None,
14225            prefer_shorter_path: false,
14226            max_paths_per_destination: 1,
14227            packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14228            max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14229            max_path_destinations: usize::MAX,
14230            max_tunnel_destinations_total: usize::MAX,
14231            destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14232            announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14233            announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14234            announce_sig_cache_enabled: true,
14235            announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14236            announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14237            announce_queue_max_entries: 256,
14238            announce_queue_max_interfaces: 1024,
14239        };
14240        let mut driver = Driver::new(driver_config, rx, tx.clone(), Box::new(cbs));
14241
14242        let dest_hash = [0xBB; 16];
14243        tx.send(Event::RegisterLinkDestination {
14244            dest_hash,
14245            sig_prv_bytes: [0x11; 32],
14246            sig_pub_bytes: [0x22; 32],
14247            resource_strategy: 0,
14248        })
14249        .unwrap();
14250
14251        let (resp_tx, resp_rx) = mpsc::channel();
14252        tx.send(Event::Query(QueryRequest::LocalDestinations, resp_tx))
14253            .unwrap();
14254        tx.send(Event::Shutdown).unwrap();
14255        driver.run();
14256
14257        match resp_rx.recv().unwrap() {
14258            QueryResponse::LocalDestinations(entries) => {
14259                // 2 internal + 1 link destination
14260                assert_eq!(entries.len(), 3);
14261                assert!(entries.iter().any(|e| e.hash == dest_hash
14262                    && e.dest_type == rns_core::constants::DESTINATION_SINGLE));
14263            }
14264            other => panic!("expected LocalDestinations, got {:?}", other),
14265        }
14266    }
14267
14268    #[test]
14269    fn query_links_empty() {
14270        let (tx, rx) = event::channel();
14271        let (cbs, _, _, _, _, _) = MockCallbacks::new();
14272        let driver_config = TransportConfig {
14273            transport_enabled: false,
14274            identity_hash: None,
14275            prefer_shorter_path: false,
14276            max_paths_per_destination: 1,
14277            packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14278            max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14279            max_path_destinations: usize::MAX,
14280            max_tunnel_destinations_total: usize::MAX,
14281            destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14282            announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14283            announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14284            announce_sig_cache_enabled: true,
14285            announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14286            announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14287            announce_queue_max_entries: 256,
14288            announce_queue_max_interfaces: 1024,
14289        };
14290        let mut driver = Driver::new(driver_config, rx, tx.clone(), Box::new(cbs));
14291
14292        let (resp_tx, resp_rx) = mpsc::channel();
14293        tx.send(Event::Query(QueryRequest::Links, resp_tx)).unwrap();
14294        tx.send(Event::Shutdown).unwrap();
14295        driver.run();
14296
14297        match resp_rx.recv().unwrap() {
14298            QueryResponse::Links(entries) => {
14299                assert!(entries.is_empty());
14300            }
14301            other => panic!("expected Links, got {:?}", other),
14302        }
14303    }
14304
14305    #[test]
14306    fn query_resources_empty() {
14307        let (tx, rx) = event::channel();
14308        let (cbs, _, _, _, _, _) = MockCallbacks::new();
14309        let driver_config = TransportConfig {
14310            transport_enabled: false,
14311            identity_hash: None,
14312            prefer_shorter_path: false,
14313            max_paths_per_destination: 1,
14314            packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14315            max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14316            max_path_destinations: usize::MAX,
14317            max_tunnel_destinations_total: usize::MAX,
14318            destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14319            announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14320            announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14321            announce_sig_cache_enabled: true,
14322            announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14323            announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14324            announce_queue_max_entries: 256,
14325            announce_queue_max_interfaces: 1024,
14326        };
14327        let mut driver = Driver::new(driver_config, rx, tx.clone(), Box::new(cbs));
14328
14329        let (resp_tx, resp_rx) = mpsc::channel();
14330        tx.send(Event::Query(QueryRequest::Resources, resp_tx))
14331            .unwrap();
14332        tx.send(Event::Shutdown).unwrap();
14333        driver.run();
14334
14335        match resp_rx.recv().unwrap() {
14336            QueryResponse::Resources(entries) => {
14337                assert!(entries.is_empty());
14338            }
14339            other => panic!("expected Resources, got {:?}", other),
14340        }
14341    }
14342
14343    #[test]
14344    fn infer_interface_type_from_name() {
14345        assert_eq!(
14346            super::infer_interface_type("TCPServerInterface/Client-1234"),
14347            "TCPServerClientInterface"
14348        );
14349        assert_eq!(
14350            super::infer_interface_type("BackboneInterface/5"),
14351            "BackboneInterface"
14352        );
14353        assert_eq!(
14354            super::infer_interface_type("LocalInterface"),
14355            "LocalServerClientInterface"
14356        );
14357        assert_eq!(
14358            super::infer_interface_type("MyAutoGroup:fe80::1"),
14359            "AutoInterface"
14360        );
14361    }
14362
14363    // ---- extract_dest_hash tests ----
14364
14365    #[test]
14366    fn test_extract_dest_hash_empty() {
14367        assert_eq!(super::extract_dest_hash(&[]), [0u8; 16]);
14368    }
14369
14370    // =========================================================================
14371    // Probe tests: SendProbe, CheckProof, completed_proofs, probe_responder
14372    // =========================================================================
14373
14374    #[test]
14375    fn send_probe_unknown_dest_returns_none() {
14376        let (tx, rx) = event::channel();
14377        let (cbs, _, _, _, _, _) = MockCallbacks::new();
14378        let mut driver = Driver::new(
14379            TransportConfig {
14380                transport_enabled: false,
14381                identity_hash: None,
14382                prefer_shorter_path: false,
14383                max_paths_per_destination: 1,
14384                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14385                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14386                max_path_destinations: usize::MAX,
14387                max_tunnel_destinations_total: usize::MAX,
14388                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14389                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14390                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14391                announce_sig_cache_enabled: true,
14392                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14393                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14394                announce_queue_max_entries: 256,
14395                announce_queue_max_interfaces: 1024,
14396            },
14397            rx,
14398            tx.clone(),
14399            Box::new(cbs),
14400        );
14401        let info = make_interface_info(1);
14402        driver.engine.register_interface(info);
14403        let (writer, _sent) = MockWriter::new();
14404        driver
14405            .interfaces
14406            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
14407
14408        // SendProbe for a dest_hash with no known identity should return None
14409        let (resp_tx, resp_rx) = mpsc::channel();
14410        tx.send(Event::Query(
14411            QueryRequest::SendProbe {
14412                dest_hash: [0xAA; 16],
14413                payload_size: 16,
14414            },
14415            resp_tx,
14416        ))
14417        .unwrap();
14418        tx.send(Event::Shutdown).unwrap();
14419        driver.run();
14420
14421        match resp_rx.recv().unwrap() {
14422            QueryResponse::SendProbe(None) => {}
14423            other => panic!("expected SendProbe(None), got {:?}", other),
14424        }
14425    }
14426
14427    #[test]
14428    fn send_probe_known_dest_returns_packet_hash() {
14429        let (tx, rx) = event::channel();
14430        let (cbs, _, _, _, _, _) = MockCallbacks::new();
14431        let mut driver = Driver::new(
14432            TransportConfig {
14433                transport_enabled: false,
14434                identity_hash: None,
14435                prefer_shorter_path: false,
14436                max_paths_per_destination: 1,
14437                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14438                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14439                max_path_destinations: usize::MAX,
14440                max_tunnel_destinations_total: usize::MAX,
14441                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14442                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14443                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14444                announce_sig_cache_enabled: true,
14445                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14446                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14447                announce_queue_max_entries: 256,
14448                announce_queue_max_interfaces: 1024,
14449            },
14450            rx,
14451            tx.clone(),
14452            Box::new(cbs),
14453        );
14454        let info = make_interface_info(1);
14455        driver.engine.register_interface(info);
14456        let (writer, sent) = MockWriter::new();
14457        driver
14458            .interfaces
14459            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
14460
14461        // Inject a known identity so SendProbe can encrypt to it
14462        let remote_identity = Identity::new(&mut OsRng);
14463        let dest_hash = rns_core::destination::destination_hash(
14464            "rnstransport",
14465            &["probe"],
14466            Some(remote_identity.hash()),
14467        );
14468
14469        // First inject the identity via announce
14470        let (inject_tx, inject_rx) = mpsc::channel();
14471        tx.send(Event::Query(
14472            QueryRequest::InjectIdentity {
14473                dest_hash,
14474                identity_hash: *remote_identity.hash(),
14475                public_key: remote_identity.get_public_key().unwrap(),
14476                app_data: None,
14477                hops: 1,
14478                received_at: 0.0,
14479            },
14480            inject_tx,
14481        ))
14482        .unwrap();
14483
14484        // Now send the probe
14485        let (resp_tx, resp_rx) = mpsc::channel();
14486        tx.send(Event::Query(
14487            QueryRequest::SendProbe {
14488                dest_hash,
14489                payload_size: 16,
14490            },
14491            resp_tx,
14492        ))
14493        .unwrap();
14494        tx.send(Event::Shutdown).unwrap();
14495        driver.run();
14496
14497        // Verify injection succeeded
14498        match inject_rx.recv().unwrap() {
14499            QueryResponse::InjectIdentity(true) => {}
14500            other => panic!("expected InjectIdentity(true), got {:?}", other),
14501        }
14502
14503        // Verify probe sent
14504        match resp_rx.recv().unwrap() {
14505            QueryResponse::SendProbe(Some((packet_hash, _hops))) => {
14506                // Packet hash should be non-zero
14507                assert_ne!(packet_hash, [0u8; 32]);
14508                // Should be tracked in sent_packets
14509                assert!(driver.sent_packets.contains_key(&packet_hash));
14510                // Should have sent a DATA packet on the wire
14511                let sent_data = sent.lock().unwrap();
14512                assert!(!sent_data.is_empty(), "Probe packet should be sent on wire");
14513                // Verify it's a DATA SINGLE packet
14514                let raw = &sent_data[0];
14515                let flags = PacketFlags::unpack(raw[0] & 0x7F);
14516                assert_eq!(flags.packet_type, constants::PACKET_TYPE_DATA);
14517                assert_eq!(flags.destination_type, constants::DESTINATION_SINGLE);
14518            }
14519            other => panic!("expected SendProbe(Some(..)), got {:?}", other),
14520        }
14521    }
14522
14523    #[test]
14524    fn check_proof_not_found_returns_none() {
14525        let (tx, rx) = event::channel();
14526        let (cbs, _, _, _, _, _) = MockCallbacks::new();
14527        let mut driver = Driver::new(
14528            TransportConfig {
14529                transport_enabled: false,
14530                identity_hash: None,
14531                prefer_shorter_path: false,
14532                max_paths_per_destination: 1,
14533                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14534                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14535                max_path_destinations: usize::MAX,
14536                max_tunnel_destinations_total: usize::MAX,
14537                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14538                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14539                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14540                announce_sig_cache_enabled: true,
14541                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14542                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14543                announce_queue_max_entries: 256,
14544                announce_queue_max_interfaces: 1024,
14545            },
14546            rx,
14547            tx.clone(),
14548            Box::new(cbs),
14549        );
14550
14551        let (resp_tx, resp_rx) = mpsc::channel();
14552        tx.send(Event::Query(
14553            QueryRequest::CheckProof {
14554                packet_hash: [0xBB; 32],
14555            },
14556            resp_tx,
14557        ))
14558        .unwrap();
14559        tx.send(Event::Shutdown).unwrap();
14560        driver.run();
14561
14562        match resp_rx.recv().unwrap() {
14563            QueryResponse::CheckProof(None) => {}
14564            other => panic!("expected CheckProof(None), got {:?}", other),
14565        }
14566    }
14567
14568    #[test]
14569    fn check_proof_found_returns_rtt() {
14570        let (tx, rx) = event::channel();
14571        let (cbs, _, _, _, _, _) = MockCallbacks::new();
14572        let mut driver = Driver::new(
14573            TransportConfig {
14574                transport_enabled: false,
14575                identity_hash: None,
14576                prefer_shorter_path: false,
14577                max_paths_per_destination: 1,
14578                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14579                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14580                max_path_destinations: usize::MAX,
14581                max_tunnel_destinations_total: usize::MAX,
14582                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14583                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14584                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14585                announce_sig_cache_enabled: true,
14586                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14587                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14588                announce_queue_max_entries: 256,
14589                announce_queue_max_interfaces: 1024,
14590            },
14591            rx,
14592            tx.clone(),
14593            Box::new(cbs),
14594        );
14595
14596        // Pre-populate completed_proofs
14597        let packet_hash = [0xCC; 32];
14598        driver
14599            .completed_proofs
14600            .insert(packet_hash, (0.123, time::now()));
14601
14602        let (resp_tx, resp_rx) = mpsc::channel();
14603        tx.send(Event::Query(
14604            QueryRequest::CheckProof { packet_hash },
14605            resp_tx,
14606        ))
14607        .unwrap();
14608        tx.send(Event::Shutdown).unwrap();
14609        driver.run();
14610
14611        match resp_rx.recv().unwrap() {
14612            QueryResponse::CheckProof(Some(rtt)) => {
14613                assert!(
14614                    (rtt - 0.123).abs() < 0.001,
14615                    "RTT should be ~0.123, got {}",
14616                    rtt
14617                );
14618            }
14619            other => panic!("expected CheckProof(Some(..)), got {:?}", other),
14620        }
14621        // Should be consumed (removed) after checking
14622        assert!(!driver.completed_proofs.contains_key(&packet_hash));
14623    }
14624
14625    #[test]
14626    fn inbound_proof_populates_completed_proofs() {
14627        let (tx, rx) = event::channel();
14628        let proofs = Arc::new(Mutex::new(Vec::new()));
14629        let cbs = MockCallbacks {
14630            announces: Arc::new(Mutex::new(Vec::new())),
14631            paths: Arc::new(Mutex::new(Vec::new())),
14632            deliveries: Arc::new(Mutex::new(Vec::new())),
14633            iface_ups: Arc::new(Mutex::new(Vec::new())),
14634            iface_downs: Arc::new(Mutex::new(Vec::new())),
14635            link_established: Arc::new(Mutex::new(Vec::new())),
14636            link_closed: Arc::new(Mutex::new(Vec::new())),
14637            remote_identified: Arc::new(Mutex::new(Vec::new())),
14638            resources_received: Arc::new(Mutex::new(Vec::new())),
14639            resource_completed: Arc::new(Mutex::new(Vec::new())),
14640            resource_failed: Arc::new(Mutex::new(Vec::new())),
14641            channel_messages: Arc::new(Mutex::new(Vec::new())),
14642            link_data: Arc::new(Mutex::new(Vec::new())),
14643            responses: Arc::new(Mutex::new(Vec::new())),
14644            proofs: proofs.clone(),
14645            proof_requested: Arc::new(Mutex::new(Vec::new())),
14646        };
14647
14648        let mut driver = Driver::new(
14649            TransportConfig {
14650                transport_enabled: false,
14651                identity_hash: None,
14652                prefer_shorter_path: false,
14653                max_paths_per_destination: 1,
14654                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14655                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14656                max_path_destinations: usize::MAX,
14657                max_tunnel_destinations_total: usize::MAX,
14658                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14659                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14660                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14661                announce_sig_cache_enabled: true,
14662                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14663                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14664                announce_queue_max_entries: 256,
14665                announce_queue_max_interfaces: 1024,
14666            },
14667            rx,
14668            tx.clone(),
14669            Box::new(cbs),
14670        );
14671        let info = make_interface_info(1);
14672        driver.engine.register_interface(info);
14673        let (writer, sent) = MockWriter::new();
14674        driver
14675            .interfaces
14676            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
14677
14678        // Register a destination with ProveAll so we can get a proof back
14679        let dest = [0xDD; 16];
14680        let identity = Identity::new(&mut OsRng);
14681        let prv_key = identity.get_private_key().unwrap();
14682        driver
14683            .engine
14684            .register_destination(dest, constants::DESTINATION_SINGLE);
14685        driver.proof_strategies.insert(
14686            dest,
14687            (
14688                rns_core::types::ProofStrategy::ProveAll,
14689                Some(Identity::from_private_key(&prv_key)),
14690            ),
14691        );
14692
14693        // Build and send a DATA packet to the dest (this creates a sent_packet + proof)
14694        let flags = PacketFlags {
14695            header_type: constants::HEADER_1,
14696            context_flag: constants::FLAG_UNSET,
14697            transport_type: constants::TRANSPORT_BROADCAST,
14698            destination_type: constants::DESTINATION_SINGLE,
14699            packet_type: constants::PACKET_TYPE_DATA,
14700        };
14701        let data_packet = RawPacket::pack(
14702            flags,
14703            0,
14704            &dest,
14705            None,
14706            constants::CONTEXT_NONE,
14707            b"probe data",
14708        )
14709        .unwrap();
14710        let data_packet_hash = data_packet.packet_hash;
14711
14712        // Track it as a sent packet so the proof handler recognizes it
14713        driver
14714            .sent_packets
14715            .insert(data_packet_hash, (dest, time::now()));
14716
14717        // Deliver the frame — this generates a proof which gets sent on wire
14718        tx.send(Event::Frame {
14719            interface_id: InterfaceId(1),
14720            data: data_packet.raw,
14721        })
14722        .unwrap();
14723        tx.send(Event::Shutdown).unwrap();
14724        driver.run();
14725
14726        // The proof was generated and sent on the wire
14727        let sent_packets = sent.lock().unwrap();
14728        let proof_packets: Vec<_> = sent_packets
14729            .iter()
14730            .filter(|raw| {
14731                let flags = PacketFlags::unpack(raw[0] & 0x7F);
14732                flags.packet_type == constants::PACKET_TYPE_PROOF
14733            })
14734            .collect();
14735        assert!(!proof_packets.is_empty(), "Should have sent a proof packet");
14736
14737        // Now feed the proof packet back to the driver so handle_inbound_proof fires.
14738        // We need a fresh driver run since the previous one shut down.
14739        // Instead, verify the data flow: the proof was sent on wire, and when
14740        // handle_inbound_proof processes a matching proof, completed_proofs gets populated.
14741        // Since our DATA packet was both delivered locally AND tracked in sent_packets,
14742        // the proof was generated on delivery. But the proof is for the *sender* to verify --
14743        // the proof gets sent back to the sender. So in this test (same driver = both sides),
14744        // the proof was sent on wire but not yet received back.
14745        //
14746        // Let's verify handle_inbound_proof directly by feeding the proof frame back.
14747        let proof_raw = proof_packets[0].clone();
14748        drop(sent_packets); // release lock
14749
14750        // Create a new event loop to handle the proof frame
14751        let (tx2, rx2) = event::channel();
14752        let proofs2 = Arc::new(Mutex::new(Vec::new()));
14753        let cbs2 = MockCallbacks {
14754            announces: Arc::new(Mutex::new(Vec::new())),
14755            paths: Arc::new(Mutex::new(Vec::new())),
14756            deliveries: Arc::new(Mutex::new(Vec::new())),
14757            iface_ups: Arc::new(Mutex::new(Vec::new())),
14758            iface_downs: Arc::new(Mutex::new(Vec::new())),
14759            link_established: Arc::new(Mutex::new(Vec::new())),
14760            link_closed: Arc::new(Mutex::new(Vec::new())),
14761            remote_identified: Arc::new(Mutex::new(Vec::new())),
14762            resources_received: Arc::new(Mutex::new(Vec::new())),
14763            resource_completed: Arc::new(Mutex::new(Vec::new())),
14764            resource_failed: Arc::new(Mutex::new(Vec::new())),
14765            channel_messages: Arc::new(Mutex::new(Vec::new())),
14766            link_data: Arc::new(Mutex::new(Vec::new())),
14767            responses: Arc::new(Mutex::new(Vec::new())),
14768            proofs: proofs2.clone(),
14769            proof_requested: Arc::new(Mutex::new(Vec::new())),
14770        };
14771        let mut driver2 = Driver::new(
14772            TransportConfig {
14773                transport_enabled: false,
14774                identity_hash: None,
14775                prefer_shorter_path: false,
14776                max_paths_per_destination: 1,
14777                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14778                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14779                max_path_destinations: usize::MAX,
14780                max_tunnel_destinations_total: usize::MAX,
14781                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14782                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14783                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14784                announce_sig_cache_enabled: true,
14785                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14786                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14787                announce_queue_max_entries: 256,
14788                announce_queue_max_interfaces: 1024,
14789            },
14790            rx2,
14791            tx2.clone(),
14792            Box::new(cbs2),
14793        );
14794        let info2 = make_interface_info(1);
14795        driver2.engine.register_interface(info2);
14796        let (writer2, _sent2) = MockWriter::new();
14797        driver2
14798            .interfaces
14799            .insert(InterfaceId(1), make_entry(1, Box::new(writer2), true));
14800
14801        // Track the original sent packet in driver2 so it recognizes the proof
14802        driver2
14803            .sent_packets
14804            .insert(data_packet_hash, (dest, time::now()));
14805
14806        // Feed the proof frame
14807        tx2.send(Event::Frame {
14808            interface_id: InterfaceId(1),
14809            data: proof_raw,
14810        })
14811        .unwrap();
14812        tx2.send(Event::Shutdown).unwrap();
14813        driver2.run();
14814
14815        // The on_proof callback should have fired
14816        let proof_events = proofs2.lock().unwrap();
14817        assert_eq!(proof_events.len(), 1, "on_proof callback should fire once");
14818        assert_eq!(
14819            proof_events[0].1 .0, data_packet_hash,
14820            "proof should match original packet hash"
14821        );
14822        assert!(proof_events[0].2 >= 0.0, "RTT should be non-negative");
14823
14824        // completed_proofs should contain the entry
14825        assert!(
14826            driver2.completed_proofs.contains_key(&data_packet_hash),
14827            "completed_proofs should contain the packet hash"
14828        );
14829        let (rtt, _received) = driver2.completed_proofs[&data_packet_hash];
14830        assert!(rtt >= 0.0, "RTT should be non-negative");
14831    }
14832
14833    #[test]
14834    fn interface_stats_includes_probe_responder() {
14835        let (tx, rx) = event::channel();
14836        let (cbs, _, _, _, _, _) = MockCallbacks::new();
14837        let mut driver = Driver::new(
14838            TransportConfig {
14839                transport_enabled: true,
14840                identity_hash: Some([0x42; 16]),
14841                prefer_shorter_path: false,
14842                max_paths_per_destination: 1,
14843                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14844                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14845                max_path_destinations: usize::MAX,
14846                max_tunnel_destinations_total: usize::MAX,
14847                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14848                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14849                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14850                announce_sig_cache_enabled: true,
14851                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14852                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14853                announce_queue_max_entries: 256,
14854                announce_queue_max_interfaces: 1024,
14855            },
14856            rx,
14857            tx.clone(),
14858            Box::new(cbs),
14859        );
14860        let (writer, _sent) = MockWriter::new();
14861        driver
14862            .interfaces
14863            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
14864
14865        // Set probe_responder_hash
14866        driver.probe_responder_hash = Some([0xEE; 16]);
14867
14868        let (resp_tx, resp_rx) = mpsc::channel();
14869        tx.send(Event::Query(QueryRequest::InterfaceStats, resp_tx))
14870            .unwrap();
14871        tx.send(Event::Shutdown).unwrap();
14872        driver.run();
14873
14874        match resp_rx.recv().unwrap() {
14875            QueryResponse::InterfaceStats(stats) => {
14876                assert_eq!(stats.probe_responder, Some([0xEE; 16]));
14877            }
14878            other => panic!("expected InterfaceStats, got {:?}", other),
14879        }
14880    }
14881
14882    #[test]
14883    fn interface_stats_probe_responder_none_when_disabled() {
14884        let (tx, rx) = event::channel();
14885        let (cbs, _, _, _, _, _) = MockCallbacks::new();
14886        let mut driver = Driver::new(
14887            TransportConfig {
14888                transport_enabled: false,
14889                identity_hash: None,
14890                prefer_shorter_path: false,
14891                max_paths_per_destination: 1,
14892                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14893                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14894                max_path_destinations: usize::MAX,
14895                max_tunnel_destinations_total: usize::MAX,
14896                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14897                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14898                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14899                announce_sig_cache_enabled: true,
14900                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14901                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14902                announce_queue_max_entries: 256,
14903                announce_queue_max_interfaces: 1024,
14904            },
14905            rx,
14906            tx.clone(),
14907            Box::new(cbs),
14908        );
14909        let (writer, _sent) = MockWriter::new();
14910        driver
14911            .interfaces
14912            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
14913
14914        let (resp_tx, resp_rx) = mpsc::channel();
14915        tx.send(Event::Query(QueryRequest::InterfaceStats, resp_tx))
14916            .unwrap();
14917        tx.send(Event::Shutdown).unwrap();
14918        driver.run();
14919
14920        match resp_rx.recv().unwrap() {
14921            QueryResponse::InterfaceStats(stats) => {
14922                assert_eq!(stats.probe_responder, None);
14923            }
14924            other => panic!("expected InterfaceStats, got {:?}", other),
14925        }
14926    }
14927
14928    #[test]
14929    fn test_extract_dest_hash_too_short() {
14930        // Packet too short to contain a full dest hash
14931        assert_eq!(super::extract_dest_hash(&[0x00, 0x00, 0xAA]), [0u8; 16]);
14932    }
14933
14934    #[test]
14935    fn test_extract_dest_hash_header1() {
14936        // HEADER_1: bit 6 = 0, dest at bytes 2..18
14937        let mut raw = vec![0x00, 0x00]; // flags (header_type=0), hops
14938        let dest = [0x11; 16];
14939        raw.extend_from_slice(&dest);
14940        raw.extend_from_slice(&[0xFF; 10]); // trailing data
14941        assert_eq!(super::extract_dest_hash(&raw), dest);
14942    }
14943
14944    #[test]
14945    fn test_extract_dest_hash_header2() {
14946        // HEADER_2: bit 6 = 1, transport_id at 2..18, dest at 18..34
14947        let mut raw = vec![0x40, 0x00]; // flags (header_type=1), hops
14948        raw.extend_from_slice(&[0xAA; 16]); // transport_id (bytes 2..18)
14949        let dest = [0x22; 16];
14950        raw.extend_from_slice(&dest); // dest (bytes 18..34)
14951        raw.extend_from_slice(&[0xFF; 10]); // trailing data
14952        assert_eq!(super::extract_dest_hash(&raw), dest);
14953    }
14954
14955    #[test]
14956    fn test_extract_dest_hash_header2_too_short() {
14957        // HEADER_2 packet that's too short for the dest portion
14958        let mut raw = vec![0x40, 0x00];
14959        raw.extend_from_slice(&[0xAA; 16]); // transport_id only, no dest
14960        assert_eq!(super::extract_dest_hash(&raw), [0u8; 16]);
14961    }
14962
14963    #[test]
14964    fn announce_stores_receiving_interface_in_known_destinations() {
14965        // When an announce arrives on interface 1, the AnnouncedIdentity
14966        // stored in known_destinations must have receiving_interface == InterfaceId(1).
14967        let (tx, rx) = event::channel();
14968        let (cbs, _, _, _, _, _) = MockCallbacks::new();
14969        let mut driver = Driver::new(
14970            TransportConfig {
14971                transport_enabled: false,
14972                identity_hash: None,
14973                prefer_shorter_path: false,
14974                max_paths_per_destination: 1,
14975                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
14976                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
14977                max_path_destinations: usize::MAX,
14978                max_tunnel_destinations_total: usize::MAX,
14979                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
14980                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
14981                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
14982                announce_sig_cache_enabled: true,
14983                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
14984                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
14985                announce_queue_max_entries: 256,
14986                announce_queue_max_interfaces: 1024,
14987            },
14988            rx,
14989            tx.clone(),
14990            Box::new(cbs),
14991        );
14992        let info = make_interface_info(1);
14993        driver.engine.register_interface(info);
14994        let (writer, _sent) = MockWriter::new();
14995        driver
14996            .interfaces
14997            .insert(InterfaceId(1), make_entry(1, Box::new(writer), true));
14998
14999        let identity = Identity::new(&mut OsRng);
15000        let announce_raw = build_announce_packet(&identity);
15001
15002        tx.send(Event::Frame {
15003            interface_id: InterfaceId(1),
15004            data: announce_raw,
15005        })
15006        .unwrap();
15007        tx.send(Event::Shutdown).unwrap();
15008        driver.run();
15009
15010        // The identity should be cached with the correct receiving interface
15011        assert_eq!(driver.known_destinations.len(), 1);
15012        let (_, announced) = driver.known_destinations.iter().next().unwrap();
15013        assert_eq!(
15014            announced.receiving_interface,
15015            InterfaceId(1),
15016            "receiving_interface should match the interface the announce arrived on"
15017        );
15018    }
15019
15020    #[test]
15021    fn announce_on_different_interfaces_stores_correct_id() {
15022        // Announces arriving on interface 2 should store InterfaceId(2).
15023        let (tx, rx) = event::channel();
15024        let (cbs, _, _, _, _, _) = MockCallbacks::new();
15025        let mut driver = Driver::new(
15026            TransportConfig {
15027                transport_enabled: false,
15028                identity_hash: None,
15029                prefer_shorter_path: false,
15030                max_paths_per_destination: 1,
15031                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
15032                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
15033                max_path_destinations: usize::MAX,
15034                max_tunnel_destinations_total: usize::MAX,
15035                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
15036                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
15037                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
15038                announce_sig_cache_enabled: true,
15039                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
15040                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
15041                announce_queue_max_entries: 256,
15042                announce_queue_max_interfaces: 1024,
15043            },
15044            rx,
15045            tx.clone(),
15046            Box::new(cbs),
15047        );
15048        // Register two interfaces
15049        for id in [1, 2] {
15050            driver.engine.register_interface(make_interface_info(id));
15051            let (writer, _) = MockWriter::new();
15052            driver
15053                .interfaces
15054                .insert(InterfaceId(id), make_entry(id, Box::new(writer), true));
15055        }
15056
15057        let identity = Identity::new(&mut OsRng);
15058        let announce_raw = build_announce_packet(&identity);
15059
15060        // Send on interface 2
15061        tx.send(Event::Frame {
15062            interface_id: InterfaceId(2),
15063            data: announce_raw,
15064        })
15065        .unwrap();
15066        tx.send(Event::Shutdown).unwrap();
15067        driver.run();
15068
15069        assert_eq!(driver.known_destinations.len(), 1);
15070        let (_, announced) = driver.known_destinations.iter().next().unwrap();
15071        assert_eq!(announced.receiving_interface, InterfaceId(2));
15072    }
15073
15074    #[test]
15075    fn inject_identity_stores_sentinel_interface() {
15076        // InjectIdentity (used for persistence restore) should store InterfaceId(0)
15077        // because the identity wasn't received from a real interface.
15078        let (tx, rx) = event::channel();
15079        let (cbs, _, _, _, _, _) = MockCallbacks::new();
15080        let mut driver = Driver::new(
15081            TransportConfig {
15082                transport_enabled: false,
15083                identity_hash: None,
15084                prefer_shorter_path: false,
15085                max_paths_per_destination: 1,
15086                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
15087                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
15088                max_path_destinations: usize::MAX,
15089                max_tunnel_destinations_total: usize::MAX,
15090                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
15091                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
15092                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
15093                announce_sig_cache_enabled: true,
15094                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
15095                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
15096                announce_queue_max_entries: 256,
15097                announce_queue_max_interfaces: 1024,
15098            },
15099            rx,
15100            tx.clone(),
15101            Box::new(cbs),
15102        );
15103
15104        let identity = Identity::new(&mut OsRng);
15105        let dest_hash =
15106            rns_core::destination::destination_hash("test", &["app"], Some(identity.hash()));
15107
15108        let (resp_tx, resp_rx) = mpsc::channel();
15109        tx.send(Event::Query(
15110            QueryRequest::InjectIdentity {
15111                dest_hash,
15112                identity_hash: *identity.hash(),
15113                public_key: identity.get_public_key().unwrap(),
15114                app_data: Some(b"restored".to_vec()),
15115                hops: 2,
15116                received_at: 99.0,
15117            },
15118            resp_tx,
15119        ))
15120        .unwrap();
15121        tx.send(Event::Shutdown).unwrap();
15122        driver.run();
15123
15124        match resp_rx.recv().unwrap() {
15125            QueryResponse::InjectIdentity(true) => {}
15126            other => panic!("expected InjectIdentity(true), got {:?}", other),
15127        }
15128
15129        let announced = driver
15130            .known_destinations
15131            .get(&dest_hash)
15132            .expect("identity should be cached");
15133        assert_eq!(
15134            announced.receiving_interface,
15135            InterfaceId(0),
15136            "injected identity should have sentinel InterfaceId(0)"
15137        );
15138        assert_eq!(announced.dest_hash.0, dest_hash);
15139        assert_eq!(announced.identity_hash.0, *identity.hash());
15140        assert_eq!(announced.public_key, identity.get_public_key().unwrap());
15141        assert_eq!(announced.app_data, Some(b"restored".to_vec()));
15142        assert_eq!(announced.hops, 2);
15143        assert_eq!(announced.received_at, 99.0);
15144    }
15145
15146    #[test]
15147    fn inject_identity_overwrites_previous_entry() {
15148        // A second InjectIdentity for the same dest_hash should overwrite the first.
15149        let (tx, rx) = event::channel();
15150        let (cbs, _, _, _, _, _) = MockCallbacks::new();
15151        let mut driver = Driver::new(
15152            TransportConfig {
15153                transport_enabled: false,
15154                identity_hash: None,
15155                prefer_shorter_path: false,
15156                max_paths_per_destination: 1,
15157                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
15158                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
15159                max_path_destinations: usize::MAX,
15160                max_tunnel_destinations_total: usize::MAX,
15161                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
15162                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
15163                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
15164                announce_sig_cache_enabled: true,
15165                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
15166                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
15167                announce_queue_max_entries: 256,
15168                announce_queue_max_interfaces: 1024,
15169            },
15170            rx,
15171            tx.clone(),
15172            Box::new(cbs),
15173        );
15174
15175        let identity = Identity::new(&mut OsRng);
15176        let dest_hash =
15177            rns_core::destination::destination_hash("test", &["app"], Some(identity.hash()));
15178
15179        // First injection
15180        let (resp_tx1, resp_rx1) = mpsc::channel();
15181        tx.send(Event::Query(
15182            QueryRequest::InjectIdentity {
15183                dest_hash,
15184                identity_hash: *identity.hash(),
15185                public_key: identity.get_public_key().unwrap(),
15186                app_data: Some(b"first".to_vec()),
15187                hops: 1,
15188                received_at: 10.0,
15189            },
15190            resp_tx1,
15191        ))
15192        .unwrap();
15193
15194        // Second injection with different app_data
15195        let (resp_tx2, resp_rx2) = mpsc::channel();
15196        tx.send(Event::Query(
15197            QueryRequest::InjectIdentity {
15198                dest_hash,
15199                identity_hash: *identity.hash(),
15200                public_key: identity.get_public_key().unwrap(),
15201                app_data: Some(b"second".to_vec()),
15202                hops: 3,
15203                received_at: 20.0,
15204            },
15205            resp_tx2,
15206        ))
15207        .unwrap();
15208
15209        tx.send(Event::Shutdown).unwrap();
15210        driver.run();
15211
15212        assert!(matches!(
15213            resp_rx1.recv().unwrap(),
15214            QueryResponse::InjectIdentity(true)
15215        ));
15216        assert!(matches!(
15217            resp_rx2.recv().unwrap(),
15218            QueryResponse::InjectIdentity(true)
15219        ));
15220
15221        // Should have the second injection's data
15222        let announced = driver.known_destinations.get(&dest_hash).unwrap();
15223        assert_eq!(announced.app_data, Some(b"second".to_vec()));
15224        assert_eq!(announced.hops, 3);
15225        assert_eq!(announced.received_at, 20.0);
15226    }
15227
15228    #[test]
15229    fn re_announce_updates_receiving_interface() {
15230        // If we get two announces for the same dest from different interfaces,
15231        // the latest should win (known_destinations is a HashMap keyed by dest_hash).
15232        let (tx, rx) = event::channel();
15233        let (cbs, _, _, _, _, _) = MockCallbacks::new();
15234        let mut driver = Driver::new(
15235            TransportConfig {
15236                transport_enabled: false,
15237                identity_hash: None,
15238                prefer_shorter_path: false,
15239                max_paths_per_destination: 1,
15240                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
15241                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
15242                max_path_destinations: usize::MAX,
15243                max_tunnel_destinations_total: usize::MAX,
15244                destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
15245                announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
15246                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
15247                announce_sig_cache_enabled: true,
15248                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
15249                announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
15250                announce_queue_max_entries: 256,
15251                announce_queue_max_interfaces: 1024,
15252            },
15253            rx,
15254            tx.clone(),
15255            Box::new(cbs),
15256        );
15257        for id in [1, 2] {
15258            driver.engine.register_interface(make_interface_info(id));
15259            let (writer, _) = MockWriter::new();
15260            driver
15261                .interfaces
15262                .insert(InterfaceId(id), make_entry(id, Box::new(writer), true));
15263        }
15264
15265        let identity = Identity::new(&mut OsRng);
15266        let announce_raw = build_announce_packet(&identity);
15267
15268        // Same announce on interface 1, then interface 2
15269        tx.send(Event::Frame {
15270            interface_id: InterfaceId(1),
15271            data: announce_raw.clone(),
15272        })
15273        .unwrap();
15274        // The second announce of the same identity will be dropped by the transport
15275        // engine's deduplication (same random_hash). Build a second identity instead
15276        // to verify the field is correctly set per-announce.
15277        let identity2 = Identity::new(&mut OsRng);
15278        let announce_raw2 = build_announce_packet(&identity2);
15279        tx.send(Event::Frame {
15280            interface_id: InterfaceId(2),
15281            data: announce_raw2,
15282        })
15283        .unwrap();
15284        tx.send(Event::Shutdown).unwrap();
15285        driver.run();
15286
15287        // Both should be cached with their respective interface IDs
15288        assert_eq!(driver.known_destinations.len(), 2);
15289        for (_, announced) in &driver.known_destinations {
15290            // We can't predict ordering, but each should have a valid non-zero interface
15291            assert!(
15292                announced.receiving_interface == InterfaceId(1)
15293                    || announced.receiving_interface == InterfaceId(2)
15294            );
15295        }
15296        // Verify we actually got both interfaces represented
15297        let ifaces: Vec<_> = driver
15298            .known_destinations
15299            .values()
15300            .map(|a| a.receiving_interface)
15301            .collect();
15302        assert!(ifaces.contains(&InterfaceId(1)));
15303        assert!(ifaces.contains(&InterfaceId(2)));
15304    }
15305
15306    #[test]
15307    fn test_extract_dest_hash_other_flags_preserved() {
15308        // Ensure other flag bits don't affect header type detection
15309        // 0x3F = all bits set except bit 6 -> still HEADER_1
15310        let mut raw = vec![0x3F, 0x00];
15311        let dest = [0x33; 16];
15312        raw.extend_from_slice(&dest);
15313        raw.extend_from_slice(&[0xFF; 10]);
15314        assert_eq!(super::extract_dest_hash(&raw), dest);
15315
15316        // 0xFF = all bits set including bit 6 -> HEADER_2
15317        let mut raw2 = vec![0xFF, 0x00];
15318        raw2.extend_from_slice(&[0xBB; 16]); // transport_id
15319        let dest2 = [0x44; 16];
15320        raw2.extend_from_slice(&dest2);
15321        raw2.extend_from_slice(&[0xFF; 10]);
15322        assert_eq!(super::extract_dest_hash(&raw2), dest2);
15323    }
15324}