Skip to main content

rns_net/
node.rs

1//! RnsNode: high-level lifecycle management.
2//!
3//! Wires together the driver, interfaces, and timer thread.
4
5use std::collections::HashSet;
6use std::io::{self, Read};
7use std::path::Path;
8use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
9use std::sync::Arc;
10use std::thread::{self, JoinHandle};
11use std::time::Duration;
12
13use rns_core::transport::announce_verify_queue::OverflowPolicy as AnnounceQueueOverflowPolicy;
14use rns_core::transport::types::TransportConfig;
15use rns_crypto::identity::Identity;
16use rns_crypto::{OsRng, Rng};
17
18use crate::config;
19use crate::driver::{AnnounceRateDefaults, Callbacks, Driver};
20#[cfg(feature = "iface-backbone")]
21use crate::driver::{BackbonePeerPoolCandidateConfig, BackbonePeerPoolSettings};
22use crate::event::{self, Event, EventSender};
23use crate::ifac;
24#[cfg(feature = "iface-auto")]
25use crate::interface::auto::{auto_runtime_handle_from_config, AutoConfig};
26#[cfg(feature = "iface-backbone")]
27use crate::interface::backbone::{
28    client_config_from_mode, client_priority_from_mode, client_runtime_handle_from_mode,
29    peer_state_handle_from_mode, runtime_handle_from_mode, BackboneMode,
30};
31#[cfg(feature = "iface-i2p")]
32use crate::interface::i2p::{i2p_runtime_handle_from_config, I2pConfig};
33#[cfg(feature = "iface-local")]
34use crate::interface::local::LocalServerConfig;
35#[cfg(feature = "iface-pipe")]
36use crate::interface::pipe::{pipe_runtime_handle_from_config, PipeConfig};
37#[cfg(feature = "iface-rnode")]
38use crate::interface::rnode::{rnode_runtime_handle_from_config, RNodeConfig};
39#[cfg(feature = "iface-tcp")]
40use crate::interface::tcp::{tcp_client_runtime_handle_from_config, TcpClientConfig};
41#[cfg(feature = "iface-tcp")]
42use crate::interface::tcp_server::{
43    runtime_handle_from_config as tcp_runtime_handle_from_config, TcpServerConfig,
44};
45#[cfg(feature = "iface-udp")]
46use crate::interface::udp::{udp_runtime_handle_from_config, UdpConfig};
47use crate::interface::{InterfaceEntry, InterfaceStats};
48use crate::storage;
49use crate::time;
50
51static NEXT_RESOURCE_TRANSFER_ID: AtomicU64 = AtomicU64::new(1);
52
53#[cfg(test)]
54const DEFAULT_KNOWN_DESTINATIONS_TTL: Duration = Duration::from_secs(48 * 60 * 60);
55const DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES: usize = 8192;
56
57/// Parse an interface mode string to the corresponding constant.
58/// Matches Python's `_synthesize_interface()` in `RNS/Reticulum.py`.
59fn parse_interface_mode(mode: &str) -> u8 {
60    match mode.to_lowercase().as_str() {
61        "full" => rns_core::constants::MODE_FULL,
62        "access_point" | "accesspoint" | "ap" => rns_core::constants::MODE_ACCESS_POINT,
63        "pointtopoint" | "ptp" => rns_core::constants::MODE_POINT_TO_POINT,
64        "roaming" => rns_core::constants::MODE_ROAMING,
65        "boundary" => rns_core::constants::MODE_BOUNDARY,
66        "gateway" | "gw" => rns_core::constants::MODE_GATEWAY,
67        "internal" => rns_core::constants::MODE_INTERNAL,
68        _ => rns_core::constants::MODE_FULL,
69    }
70}
71
72fn parse_optional_interface_mode(mode: &str) -> Option<u8> {
73    match mode.to_lowercase().as_str() {
74        "full" => Some(rns_core::constants::MODE_FULL),
75        "access_point" | "accesspoint" | "ap" => Some(rns_core::constants::MODE_ACCESS_POINT),
76        "pointtopoint" | "ptp" => Some(rns_core::constants::MODE_POINT_TO_POINT),
77        "roaming" => Some(rns_core::constants::MODE_ROAMING),
78        "boundary" => Some(rns_core::constants::MODE_BOUNDARY),
79        "gateway" | "gw" => Some(rns_core::constants::MODE_GATEWAY),
80        "internal" => Some(rns_core::constants::MODE_INTERNAL),
81        _ => None,
82    }
83}
84
85fn parse_interface_gravity(value: Option<&str>, default_gravity: i64) -> i64 {
86    value
87        .and_then(|value| value.parse().ok())
88        .unwrap_or(default_gravity)
89}
90
91/// Apply Reticulum's mode normalization for discoverable interfaces.
92fn normalize_discovery_mode(
93    interface_type: &str,
94    configured_mode: u8,
95    discoverable: bool,
96    ignore_config_warnings: bool,
97) -> u8 {
98    if !discoverable
99        || ignore_config_warnings
100        || matches!(
101            configured_mode,
102            rns_core::constants::MODE_ACCESS_POINT
103                | rns_core::constants::MODE_GATEWAY
104                | rns_core::constants::MODE_INTERNAL
105        )
106    {
107        return configured_mode;
108    }
109
110    if matches!(interface_type, "RNodeInterface" | "RNodeMultiInterface") {
111        rns_core::constants::MODE_ACCESS_POINT
112    } else {
113        rns_core::constants::MODE_GATEWAY
114    }
115}
116
117fn default_ingress_control_for_type(
118    iface_type: &str,
119) -> rns_core::transport::types::IngressControlConfig {
120    match iface_type {
121        "AutoInterface" | "BackboneInterface" | "TCPClientInterface" | "TCPServerInterface"
122        | "UDPInterface" | "I2PInterface" => {
123            rns_core::transport::types::IngressControlConfig::enabled()
124        }
125        _ => rns_core::transport::types::IngressControlConfig::disabled(),
126    }
127}
128
129fn parse_ingress_control_config(
130    iface_type: &str,
131    params: &std::collections::HashMap<String, String>,
132) -> Result<rns_core::transport::types::IngressControlConfig, String> {
133    let mut config = default_ingress_control_for_type(iface_type);
134
135    if let Some(v) = params.get("ingress_control") {
136        config.enabled = config::parse_bool_pub(v)
137            .ok_or_else(|| format!("ingress_control must be a boolean, got '{}'", v))?;
138    }
139    if let Some(v) = params.get("ic_max_held_announces") {
140        config.max_held_announces = v
141            .parse::<usize>()
142            .map_err(|_| format!("ic_max_held_announces must be an integer, got '{}'", v))?;
143    }
144    if let Some(v) = params.get("ic_burst_hold") {
145        config.burst_hold = parse_nonnegative_f64("ic_burst_hold", v)?;
146    }
147    if let Some(v) = params.get("ic_burst_freq_new") {
148        config.burst_freq_new = parse_nonnegative_f64("ic_burst_freq_new", v)?;
149    }
150    if let Some(v) = params.get("ic_burst_freq") {
151        config.burst_freq = parse_nonnegative_f64("ic_burst_freq", v)?;
152    }
153    if let Some(v) = params.get("ic_pr_burst_freq_new") {
154        config.pr_burst_freq_new = parse_nonnegative_f64("ic_pr_burst_freq_new", v)?;
155    }
156    if let Some(v) = params.get("ic_pr_burst_freq") {
157        config.pr_burst_freq = parse_nonnegative_f64("ic_pr_burst_freq", v)?;
158    }
159    if let Some(v) = params.get("egress_control") {
160        config.egress_enabled = config::parse_bool_pub(v)
161            .ok_or_else(|| format!("egress_control must be a boolean, got '{}'", v))?;
162    }
163    if let Some(v) = params.get("ec_pr_freq") {
164        config.egress_pr_freq = parse_nonnegative_f64("ec_pr_freq", v)?;
165    }
166    if let Some(v) = params.get("ic_new_time") {
167        config.new_time = parse_nonnegative_f64("ic_new_time", v)?;
168    }
169    if let Some(v) = params.get("ic_burst_penalty") {
170        config.burst_penalty = parse_nonnegative_f64("ic_burst_penalty", v)?;
171    }
172    if let Some(v) = params.get("ic_held_release_interval") {
173        config.held_release_interval = parse_nonnegative_f64("ic_held_release_interval", v)?;
174    }
175
176    Ok(config)
177}
178
179fn parse_nonnegative_f64(key: &str, value: &str) -> Result<f64, String> {
180    let parsed = value
181        .parse::<f64>()
182        .map_err(|_| format!("{} must be numeric, got '{}'", key, value))?;
183    if parsed < 0.0 {
184        return Err(format!("{} must be >= 0, got '{}'", key, value));
185    }
186    Ok(parsed)
187}
188
189/// Extract IFAC configuration from interface params, if present.
190/// Returns None if neither networkname/network_name nor passphrase/pass_phrase is set.
191fn extract_ifac_config(
192    params: &std::collections::HashMap<String, String>,
193    default_size: usize,
194) -> Option<IfacConfig> {
195    let netname = params
196        .get("networkname")
197        .or_else(|| params.get("network_name"))
198        .cloned();
199    let netkey = params
200        .get("passphrase")
201        .or_else(|| params.get("pass_phrase"))
202        .cloned();
203
204    if netname.is_none() && netkey.is_none() {
205        return None;
206    }
207
208    // ifac_size is specified in bits in config, divide by 8 for bytes
209    let size = params
210        .get("ifac_size")
211        .and_then(|v| v.parse::<usize>().ok())
212        .map(|bits| (bits / 8).max(1))
213        .unwrap_or(default_size);
214
215    Some(IfacConfig {
216        netname,
217        netkey,
218        size,
219    })
220}
221
222/// Extract discovery configuration from interface params, if `discoverable` is set.
223fn extract_discovery_config(
224    iface_name: &str,
225    iface_type: &str,
226    params: &std::collections::HashMap<String, String>,
227) -> Option<crate::discovery::DiscoveryConfig> {
228    let discoverable = params
229        .get("discoverable")
230        .and_then(|v| config::parse_bool_pub(v))
231        .unwrap_or(false);
232    if !discoverable {
233        return None;
234    }
235
236    if iface_type == "TCPClientInterface" {
237        log::error!(
238            "Invalid interface discovery configuration for {}, aborting discovery announce",
239            iface_name
240        );
241        return None;
242    }
243
244    let discovery_name = params
245        .get("discovery_name")
246        .cloned()
247        .unwrap_or_else(|| iface_name.to_string());
248
249    // Config value is in seconds. Min 300s (5min), default 21600s (6h).
250    let announce_interval = params
251        .get("announce_interval")
252        .and_then(|v| v.parse::<u64>().ok())
253        .map(|secs| secs.max(300))
254        .unwrap_or(21600);
255
256    let stamp_value = params
257        .get("discovery_stamp_value")
258        .and_then(|v| v.parse::<u8>().ok())
259        .unwrap_or(crate::discovery::DEFAULT_STAMP_VALUE);
260
261    let reachable_on = params.get("reachable_on").cloned();
262
263    let listen_port = params
264        .get("listen_port")
265        .or_else(|| params.get("port"))
266        .and_then(|v| v.parse().ok());
267
268    let location_cmd = params.get("location_cmd").cloned();
269
270    let latitude = params
271        .get("latitude")
272        .or_else(|| params.get("lat"))
273        .and_then(|v| v.parse().ok());
274    let longitude = params
275        .get("longitude")
276        .or_else(|| params.get("lon"))
277        .and_then(|v| v.parse().ok());
278    let height = params.get("height").and_then(|v| v.parse().ok());
279    let operator_lxmf_address = params.get("discovery_lxmf_address").and_then(|value| {
280        let parsed = parse_discovery_lxmf_address(value);
281        if parsed.is_none() {
282            log::error!(
283                "Invalid interface discovery LXMF address for '{}': {}",
284                iface_name,
285                value
286            );
287        }
288        parsed
289    });
290
291    Some(crate::discovery::DiscoveryConfig {
292        discovery_name,
293        announce_interval,
294        stamp_value,
295        reachable_on,
296        interface_type: iface_type.to_string(),
297        listen_port,
298        location_cmd,
299        latitude,
300        longitude,
301        height,
302        operator_lxmf_address,
303    })
304}
305
306fn default_discovery_runtime_config(
307    interface_name: &str,
308    interface_type: &str,
309    listen_port: Option<u16>,
310) -> crate::discovery::DiscoveryConfig {
311    crate::discovery::DiscoveryConfig {
312        discovery_name: interface_name.to_string(),
313        announce_interval: 21600,
314        stamp_value: crate::discovery::DEFAULT_STAMP_VALUE,
315        reachable_on: None,
316        interface_type: interface_type.to_string(),
317        listen_port,
318        location_cmd: None,
319        latitude: None,
320        longitude: None,
321        height: None,
322        operator_lxmf_address: None,
323    }
324}
325
326fn parse_discovery_lxmf_address(value: &str) -> Option<[u8; 16]> {
327    if value.len() != 32 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
328        return None;
329    }
330    let mut address = [0u8; 16];
331    for (index, byte) in address.iter_mut().enumerate() {
332        *byte = u8::from_str_radix(&value[index * 2..index * 2 + 2], 16).ok()?;
333    }
334    Some(address)
335}
336
337fn discovery_runtime_ifac_fields(ifac: Option<&IfacConfig>) -> (Option<String>, Option<String>) {
338    (
339        ifac.and_then(|cfg| cfg.netname.clone()),
340        ifac.and_then(|cfg| cfg.netkey.clone()),
341    )
342}
343
344#[cfg(feature = "iface-backbone")]
345fn backbone_discovery_runtime_from_interface(
346    interface_name: &str,
347    mode: &BackboneMode,
348    discovery: Option<&crate::discovery::DiscoveryConfig>,
349    transport_enabled: bool,
350    ifac: Option<&IfacConfig>,
351) -> Option<crate::driver::BackboneDiscoveryRuntimeHandle> {
352    let config = match mode {
353        BackboneMode::Server(config) => config,
354        BackboneMode::Client(_, _) => return None,
355    };
356
357    let startup_config = discovery.cloned().unwrap_or_else(|| {
358        default_discovery_runtime_config(
359            interface_name,
360            "BackboneInterface",
361            Some(config.listen_port),
362        )
363    });
364    let (ifac_netname, ifac_netkey) = discovery_runtime_ifac_fields(ifac);
365
366    Some(crate::driver::BackboneDiscoveryRuntimeHandle::from_parts(
367        config.name.clone(),
368        startup_config,
369        transport_enabled,
370        ifac_netname,
371        ifac_netkey,
372        discovery.is_some(),
373    ))
374}
375
376#[cfg(feature = "iface-tcp")]
377fn tcp_server_discovery_runtime_from_interface(
378    interface_name: &str,
379    config: &crate::interface::tcp_server::TcpServerConfig,
380    discovery: Option<&crate::discovery::DiscoveryConfig>,
381    transport_enabled: bool,
382    ifac: Option<&IfacConfig>,
383) -> crate::driver::TcpServerDiscoveryRuntimeHandle {
384    let startup_config = discovery.cloned().unwrap_or_else(|| {
385        default_discovery_runtime_config(
386            interface_name,
387            "TCPServerInterface",
388            Some(config.listen_port),
389        )
390    });
391    let (ifac_netname, ifac_netkey) = discovery_runtime_ifac_fields(ifac);
392
393    crate::driver::TcpServerDiscoveryRuntimeHandle::from_parts(
394        config.name.clone(),
395        startup_config,
396        transport_enabled,
397        ifac_netname,
398        ifac_netkey,
399        discovery.is_some(),
400    )
401}
402
403fn ifac_runtime_from_config(
404    ifac: Option<&IfacConfig>,
405    default_size: usize,
406) -> crate::driver::IfacRuntimeConfig {
407    crate::driver::IfacRuntimeConfig::from_parts(
408        ifac.and_then(|cfg| cfg.netname.clone()),
409        ifac.and_then(|cfg| cfg.netkey.clone()),
410        ifac.map(|cfg| cfg.size).unwrap_or(default_size),
411    )
412}
413
414fn discoverable_interface_from_config(
415    interface_name: &str,
416    discovery: &crate::discovery::DiscoveryConfig,
417    transport_enabled: bool,
418    ifac: Option<&IfacConfig>,
419) -> crate::discovery::DiscoverableInterface {
420    crate::discovery::DiscoverableInterface {
421        interface_name: interface_name.to_string(),
422        config: discovery.clone(),
423        transport_enabled,
424        ifac_netname: ifac.and_then(|cfg| cfg.netname.clone()),
425        ifac_netkey: ifac.and_then(|cfg| cfg.netkey.clone()),
426    }
427}
428
429fn derive_ifac_state(
430    ifac: Option<&IfacConfig>,
431    interface_name: &str,
432) -> io::Result<Option<crate::ifac::IfacState>> {
433    let Some(ifac) = ifac else {
434        return Ok(None);
435    };
436    if ifac.netname.is_none() && ifac.netkey.is_none() {
437        return Ok(None);
438    }
439
440    ifac::derive_ifac(ifac.netname.as_deref(), ifac.netkey.as_deref(), ifac.size)
441        .map(Some)
442        .map_err(|err| {
443            io::Error::other(format!(
444                "failed to derive IFAC for {}: {}",
445                interface_name, err
446            ))
447        })
448}
449
450struct StartedInterface<'a> {
451    driver: &'a mut Driver,
452    tx: &'a EventSender,
453    queue_capacity: usize,
454    id: rns_core::transport::types::InterfaceId,
455    info: rns_core::transport::types::InterfaceInfo,
456    writer: Box<dyn crate::interface::Writer>,
457    interface_type_name: String,
458    ifac_state: Option<crate::ifac::IfacState>,
459    ifac_runtime: &'a crate::driver::IfacRuntimeConfig,
460}
461
462fn register_started_interface(params: StartedInterface<'_>) {
463    let StartedInterface {
464        driver,
465        tx,
466        queue_capacity,
467        id,
468        mut info,
469        writer,
470        interface_type_name,
471        ifac_state,
472        ifac_runtime,
473    } = params;
474    driver.apply_announce_rate_defaults(&mut info);
475    driver.apply_ingress_control_defaults(&mut info);
476    let (writer, async_writer_metrics) =
477        crate::interface::wrap_async_writer(writer, id, &info.name, tx.clone(), queue_capacity);
478    driver.register_interface_runtime_defaults(&info);
479    driver.register_interface_ifac_runtime(&info.name, ifac_runtime.clone());
480    driver.engine.register_interface(info.clone());
481    driver.interfaces.insert(
482        id,
483        InterfaceEntry {
484            id,
485            info,
486            writer,
487            async_writer_metrics: Some(async_writer_metrics),
488            enabled: true,
489            online: false,
490            dynamic: false,
491            ifac: ifac_state,
492            stats: InterfaceStats {
493                started: time::now(),
494                ..Default::default()
495            },
496            interface_type: interface_type_name,
497            send_retry_at: None,
498            send_retry_backoff: Duration::ZERO,
499        },
500    );
501}
502
503/// Top-level node configuration.
504pub struct NodeConfig {
505    pub transport_enabled: bool,
506    pub static_transport_identity: bool,
507    pub local_hops_delta: bool,
508    pub identity: Option<Identity>,
509    /// Interface configurations (parsed via registry factories).
510    pub interfaces: Vec<InterfaceConfig>,
511    /// Enable shared instance server for local clients (rns-ctl, etc.)
512    pub share_instance: bool,
513    /// Instance name for Unix socket namespace (default: "default").
514    pub instance_name: String,
515    /// Shared instance port for local client connections (default 37428).
516    pub shared_instance_port: u16,
517    /// RPC control port (default 37429). Only used when share_instance is true.
518    pub rpc_port: u16,
519    /// Cache directory for announce cache. If None, announce caching is disabled.
520    pub cache_dir: Option<std::path::PathBuf>,
521    /// Store for received ratchets. If None, received ratchets are not persisted or used.
522    pub ratchet_store: Option<Arc<dyn storage::RatchetStore>>,
523    /// TTL for received ratchets.
524    pub ratchet_expiry: Duration,
525    /// Remote management configuration.
526    pub management: crate::management::ManagementConfig,
527    /// Port to run the STUN probe server on (for facilitator nodes).
528    pub probe_port: Option<u16>,
529    /// Addresses of STUN/RNSP probe servers (tried sequentially with failover).
530    pub probe_addrs: Vec<std::net::SocketAddr>,
531    /// Protocol for endpoint discovery: "rnsp" (default) or "stun".
532    pub probe_protocol: rns_core::holepunch::ProbeProtocol,
533    /// Policy for incoming direct-connect proposals.
534    pub direct_connect_policy: crate::event::HolePunchPolicy,
535    /// Network interface to bind outbound sockets to (e.g. "usb0").
536    pub device: Option<String>,
537    /// Linux `SO_MARK` value applied to Reticulum IP underlay sockets.
538    pub underlay_mark: Option<u32>,
539    /// Hook configurations loaded from the config file.
540    pub hooks: Vec<config::ParsedHook>,
541    /// Enable interface discovery.
542    pub discover_interfaces: bool,
543    /// Mode assigned to auto-connected discovered interfaces, when configured.
544    pub autoconnect_interface_mode: Option<u8>,
545    /// Gravity assigned to auto-connected discovered interfaces.
546    pub autoconnect_interface_gravity: i64,
547    /// Allow auto-connected interfaces to propagate announces to internal interfaces.
548    pub autoconnect_announces_to_internal: bool,
549    /// Minimum stamp value for accepting discovered interfaces (default: 16).
550    pub discovery_required_value: Option<u8>,
551    /// Respond to probe packets with automatic proof (like Python's respond_to_probes).
552    pub respond_to_probes: bool,
553    /// Accept an announce with strictly fewer hops even when the random_blob
554    /// is a duplicate of the existing path entry.  Default `false` preserves
555    /// Python-compatible anti-replay behaviour.
556    pub prefer_shorter_path: bool,
557    /// Maximum number of alternative paths stored per destination.
558    /// Default 1 (single path, backward-compatible).
559    pub max_paths_per_destination: usize,
560    /// Maximum number of packet hashes retained for duplicate suppression.
561    pub packet_hashlist_max_entries: usize,
562    /// Startup allocation policy for the packet hashlist payload pages.
563    pub packet_hashlist_allocation: rns_core::transport::types::PacketHashlistAllocation,
564    /// Maximum number of discovery path-request tags remembered.
565    pub max_discovery_pr_tags: usize,
566    /// Maximum number of destination hashes retained in the live path table.
567    pub max_path_destinations: usize,
568    /// Maximum number of retained tunnel-known destinations.
569    pub max_tunnel_destinations_total: usize,
570    /// TTL for recalled known destinations without an active path.
571    pub known_destinations_ttl: Duration,
572    /// Maximum number of recalled known destinations retained.
573    pub known_destinations_max_entries: usize,
574    /// TTL for announce retransmission state.
575    pub announce_table_ttl: Duration,
576    /// Maximum retained bytes for announce retransmission state.
577    pub announce_table_max_bytes: usize,
578    /// Maximum queued events awaiting driver processing.
579    pub driver_event_queue_capacity: usize,
580    /// Maximum queued outbound frames per interface writer worker.
581    pub interface_writer_queue_capacity: usize,
582    /// Default announce-rate controls applied to interfaces when transport is enabled.
583    pub announce_rate_defaults: AnnounceRateDefaults,
584    /// Default ingress/egress controls applied to interfaces when transport is enabled.
585    pub ingress_control_defaults: rns_core::transport::types::IngressControlConfig,
586    /// Outbound Backbone peer-pool settings. Disabled when `None`.
587    #[cfg(feature = "iface-backbone")]
588    pub backbone_peer_pool: Option<BackbonePeerPoolSettings>,
589    /// Whether the announce signature verification cache is enabled.
590    pub announce_sig_cache_enabled: bool,
591    /// Maximum entries in the announce signature verification cache.
592    pub announce_sig_cache_max_entries: usize,
593    /// TTL for announce signature cache entries.
594    pub announce_sig_cache_ttl: Duration,
595    /// Custom interface registry. If `None`, uses `InterfaceRegistry::with_builtins()`.
596    pub registry: Option<crate::interface::registry::InterfaceRegistry>,
597    /// If true, a single interface failing to start will abort the entire node.
598    /// If false (default), the error is logged and remaining interfaces continue.
599    pub panic_on_interface_error: bool,
600    /// External provider bridge for hook-emitted events.
601    #[cfg(feature = "hooks")]
602    pub provider_bridge: Option<crate::provider_bridge::ProviderBridgeConfig>,
603}
604
605struct NodeQueueConfig {
606    announce_max_entries: usize,
607    announce_max_interfaces: usize,
608    announce_max_bytes: usize,
609    announce_ttl_secs: f64,
610    announce_overflow_policy: AnnounceQueueOverflowPolicy,
611    inbound_capacities: crate::event::InboundQueueCapacities,
612    link_mtu_discovery: bool,
613}
614
615impl Default for NodeQueueConfig {
616    fn default() -> Self {
617        Self {
618            announce_max_entries: 256,
619            announce_max_interfaces: 1024,
620            announce_max_bytes: 256 * 1024,
621            announce_ttl_secs: 30.0,
622            announce_overflow_policy: AnnounceQueueOverflowPolicy::DropWorst,
623            inbound_capacities: crate::event::InboundQueueCapacities::default(),
624            link_mtu_discovery: true,
625        }
626    }
627}
628
629impl Default for NodeConfig {
630    fn default() -> Self {
631        Self {
632            transport_enabled: false,
633            static_transport_identity: false,
634            local_hops_delta: false,
635            identity: None,
636            interfaces: Vec::new(),
637            share_instance: false,
638            instance_name: "default".into(),
639            shared_instance_port: 37428,
640            rpc_port: 0,
641            cache_dir: None,
642            ratchet_store: None,
643            ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
644            management: Default::default(),
645            probe_port: None,
646            probe_addrs: vec![],
647            probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
648            direct_connect_policy: crate::event::HolePunchPolicy::AcceptAll,
649            device: None,
650            underlay_mark: None,
651            hooks: Vec::new(),
652            discover_interfaces: false,
653            autoconnect_interface_mode: None,
654            autoconnect_interface_gravity: 0,
655            autoconnect_announces_to_internal: false,
656            discovery_required_value: None,
657            respond_to_probes: false,
658            prefer_shorter_path: false,
659            max_paths_per_destination: 1,
660            packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
661            packet_hashlist_allocation: rns_core::transport::types::PacketHashlistAllocation::Eager,
662            max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
663            max_path_destinations: rns_core::transport::types::DEFAULT_MAX_PATH_DESTINATIONS,
664            max_tunnel_destinations_total: usize::MAX,
665            known_destinations_ttl: Duration::from_secs(48 * 60 * 60),
666            known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
667            announce_table_ttl: Duration::from_secs(rns_core::constants::ANNOUNCE_TABLE_TTL as u64),
668            announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
669            driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
670            interface_writer_queue_capacity: crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
671            announce_rate_defaults: AnnounceRateDefaults::default(),
672            ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(),
673            #[cfg(feature = "iface-backbone")]
674            backbone_peer_pool: None,
675            announce_sig_cache_enabled: true,
676            announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
677            announce_sig_cache_ttl: Duration::from_secs(
678                rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
679            ),
680            registry: None,
681            panic_on_interface_error: false,
682            #[cfg(feature = "hooks")]
683            provider_bridge: None,
684        }
685    }
686}
687
688/// IFAC configuration for an interface.
689pub struct IfacConfig {
690    pub netname: Option<String>,
691    pub netkey: Option<String>,
692    pub size: usize,
693}
694
695/// Interface configuration, parsed via an [`InterfaceFactory`] from the registry.
696pub struct InterfaceConfig {
697    pub name: String,
698    pub type_name: String,
699    pub config_data: Box<dyn crate::interface::InterfaceConfigData>,
700    pub mode: u8,
701    pub gravity: i64,
702    pub recursive_prs: bool,
703    pub announces_from_internal: bool,
704    pub announces_to_internal: Option<bool>,
705    pub ingress_control: rns_core::transport::types::IngressControlConfig,
706    pub ifac: Option<IfacConfig>,
707    pub discovery: Option<crate::discovery::DiscoveryConfig>,
708}
709
710use crate::event::{QueryRequest, QueryResponse};
711
712/// Error returned when the driver thread has shut down.
713#[derive(Debug)]
714pub struct SendError;
715
716impl std::fmt::Display for SendError {
717    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
718        write!(f, "driver shut down")
719    }
720}
721
722impl std::error::Error for SendError {}
723
724/// A running RNS node.
725pub struct RnsNode {
726    tx: EventSender,
727    driver_handle: Option<JoinHandle<()>>,
728    verify_handle: Option<JoinHandle<()>>,
729    verify_shutdown: Arc<AtomicBool>,
730    persist_handle: Option<JoinHandle<()>>,
731    persist_stop: Option<std::sync::mpsc::Sender<()>>,
732    rpc_server: Option<crate::rpc::RpcServer>,
733    tick_interval_ms: Arc<AtomicU64>,
734    #[allow(dead_code)]
735    probe_server: Option<crate::holepunch::probe::ProbeServerHandle>,
736    known_destinations_path: Option<std::path::PathBuf>,
737    transport_state_dir: Option<std::path::PathBuf>,
738    ratchet_store: Option<Arc<dyn storage::RatchetStore>>,
739    ratchet_expiry: Duration,
740}
741
742impl RnsNode {
743    /// Start the node from a config file path.
744    /// If `config_path` is None, uses `~/.reticulum/`.
745    pub fn from_config(
746        config_path: Option<&Path>,
747        callbacks: Box<dyn Callbacks>,
748    ) -> io::Result<Self> {
749        let config_dir = storage::resolve_config_dir(config_path);
750        Self::from_config_dirs(&config_dir, &config_dir, None, false, callbacks)
751    }
752
753    /// Start an application-private node from a dedicated configuration and
754    /// state root.
755    ///
756    /// Private nodes never expose the shared-instance or management servers,
757    /// never load hooks, fail on unsupported/malformed enabled interfaces, and
758    /// force runtime interface failures to stop startup. `underlay_mark`
759    /// overrides the value in the Reticulum configuration.
760    pub fn from_private_config(
761        config_dir: &Path,
762        state_dir: &Path,
763        underlay_mark: Option<u32>,
764        callbacks: Box<dyn Callbacks>,
765    ) -> io::Result<Self> {
766        Self::from_config_dirs(config_dir, state_dir, underlay_mark, true, callbacks)
767    }
768
769    fn from_config_dirs(
770        config_dir: &Path,
771        state_dir: &Path,
772        underlay_mark: Option<u32>,
773        private: bool,
774        callbacks: Box<dyn Callbacks>,
775    ) -> io::Result<Self> {
776        let paths = storage::ensure_storage_dirs(state_dir)?;
777        let known_destinations_path = paths.storage.join("known_destinations");
778        let ratchet_store: Arc<dyn storage::RatchetStore> =
779            Arc::new(storage::FsRatchetStore::new(paths.ratchets.clone()));
780
781        // Parse config file
782        let config_file = config_dir.join("config");
783        let config_content = if config_file.exists() {
784            std::fs::read_to_string(&config_file)?
785        } else {
786            String::new()
787        };
788        let rns_config = config::parse(&config_content)
789            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("{}", e)))?;
790        let link_mtu_discovery = config::parse_link_mtu_discovery(&config_content)
791            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("{}", e)))?;
792
793        // Load or create identity
794        let identity = if let Some(ref id_path_str) = rns_config.reticulum.network_identity {
795            let id_path = std::path::PathBuf::from(id_path_str);
796            if id_path.exists() {
797                storage::load_identity(&id_path)?
798            } else {
799                let id = Identity::new(&mut OsRng);
800                storage::save_identity(&id, &id_path)?;
801                id
802            }
803        } else {
804            storage::load_or_create_identity(&paths.identities)?
805        };
806
807        // Build interface configs from parsed config using registry
808        let registry = crate::interface::registry::InterfaceRegistry::with_builtins();
809        let mut interface_configs = Vec::new();
810        let mut next_id_val = 1u64;
811
812        for iface in &rns_config.interfaces {
813            if !iface.enabled {
814                continue;
815            }
816
817            let iface_id = rns_core::transport::types::InterfaceId(next_id_val);
818            next_id_val += 1;
819
820            let factory = match registry.get(&iface.interface_type) {
821                Some(f) => f,
822                None => {
823                    if private {
824                        return Err(io::Error::new(
825                            io::ErrorKind::InvalidData,
826                            format!(
827                                "unsupported enabled interface type '{}' for '{}'",
828                                iface.interface_type, iface.name
829                            ),
830                        ));
831                    }
832                    log::warn!(
833                        "Unsupported interface type '{}' for '{}'",
834                        iface.interface_type,
835                        iface.name,
836                    );
837                    continue;
838                }
839            };
840
841            let mut iface_mode = parse_interface_mode(&iface.mode);
842
843            let discoverable = iface
844                .params
845                .get("discoverable")
846                .and_then(|value| config::parse_bool_pub(value))
847                .unwrap_or(false);
848            let ignore_config_warnings = iface
849                .params
850                .get("ignore_config_warnings")
851                .and_then(|value| config::parse_bool_pub(value))
852                .unwrap_or(false);
853            let normalized_mode = normalize_discovery_mode(
854                &iface.interface_type,
855                iface_mode,
856                discoverable,
857                ignore_config_warnings,
858            );
859            if normalized_mode != iface_mode {
860                log::info!(
861                    "Interface '{}' has discovery enabled, auto-configuring mode to {}",
862                    iface.name,
863                    if normalized_mode == rns_core::constants::MODE_ACCESS_POINT {
864                        "ACCESS_POINT"
865                    } else {
866                        "GATEWAY"
867                    }
868                );
869                iface_mode = normalized_mode;
870            }
871
872            let default_ifac_size = factory.default_ifac_size();
873            let ifac_config = extract_ifac_config(&iface.params, default_ifac_size);
874            let discovery_config =
875                extract_discovery_config(&iface.name, &iface.interface_type, &iface.params);
876            let ingress_control =
877                match parse_ingress_control_config(&iface.interface_type, &iface.params) {
878                    Ok(config) => config,
879                    Err(e) => {
880                        if private {
881                            return Err(io::Error::new(
882                                io::ErrorKind::InvalidData,
883                                format!("invalid ingress control for '{}': {e}", iface.name),
884                            ));
885                        }
886                        log::warn!(
887                            "Failed to parse ingress control config for '{}': {}",
888                            iface.name,
889                            e
890                        );
891                        continue;
892                    }
893                };
894            let recursive_prs = iface
895                .params
896                .get("recursive_prs")
897                .and_then(|v| config::parse_bool_pub(v))
898                .unwrap_or(false);
899            let announces_from_internal = iface
900                .params
901                .get("announces_from_internal")
902                .and_then(|v| config::parse_bool_pub(v))
903                .unwrap_or(true);
904            let gravity = parse_interface_gravity(
905                iface.params.get("gravity").map(String::as_str),
906                rns_config.reticulum.default_gravity,
907            );
908            let announces_to_internal = iface
909                .params
910                .get("announces_to_internal")
911                .and_then(|v| config::parse_bool_pub(v));
912
913            // Inject storage_dir for I2P (and any future factories that need it)
914            let mut params = iface.params.clone();
915            if !params.contains_key("storage_dir") {
916                params.insert(
917                    "storage_dir".to_string(),
918                    paths.storage.to_string_lossy().to_string(),
919                );
920            }
921            // Inject device for TCP client
922            if let Some(ref device) = rns_config.reticulum.device {
923                if !params.contains_key("device") {
924                    params.insert("device".to_string(), device.clone());
925                }
926            }
927
928            let config_data = match factory.parse_config_section(
929                &iface.name,
930                iface_id,
931                crate::interface::ConfigSection {
932                    params: &params,
933                    children: &iface.subinterfaces,
934                },
935            ) {
936                Ok(data) => data,
937                Err(e) => {
938                    if private {
939                        return Err(io::Error::new(
940                            io::ErrorKind::InvalidData,
941                            format!("invalid interface '{}': {e}", iface.name),
942                        ));
943                    }
944                    log::warn!("Failed to parse config for '{}': {}", iface.name, e);
945                    continue;
946                }
947            };
948
949            interface_configs.push(InterfaceConfig {
950                name: iface.name.clone(),
951                type_name: iface.interface_type.clone(),
952                config_data,
953                mode: iface_mode,
954                gravity,
955                recursive_prs,
956                announces_from_internal,
957                announces_to_internal,
958                ingress_control,
959                ifac: ifac_config,
960                discovery: discovery_config,
961            });
962        }
963
964        // Parse management config
965        let mut mgmt_allowed = Vec::new();
966        for hex_hash in &rns_config.reticulum.remote_management_allowed {
967            if hex_hash.len() == 32 {
968                if let Ok(bytes) = (0..hex_hash.len())
969                    .step_by(2)
970                    .map(|i| u8::from_str_radix(&hex_hash[i..i + 2], 16))
971                    .collect::<Result<Vec<u8>, _>>()
972                {
973                    if bytes.len() == 16 {
974                        let mut h = [0u8; 16];
975                        h.copy_from_slice(&bytes);
976                        mgmt_allowed.push(h);
977                    }
978                } else {
979                    log::warn!("Invalid hex in remote_management_allowed: {}", hex_hash);
980                }
981            } else {
982                log::warn!(
983                    "Invalid entry in remote_management_allowed (expected 32 hex chars, got {}): {}",
984                    hex_hash.len(), hex_hash,
985                );
986            }
987        }
988
989        // Parse probe_addr (comma-separated list of SocketAddr)
990        let probe_addrs: Vec<std::net::SocketAddr> = rns_config
991            .reticulum
992            .probe_addr
993            .as_ref()
994            .map(|s| {
995                s.split(',')
996                    .filter_map(|entry| {
997                        let trimmed = entry.trim();
998                        if trimmed.is_empty() {
999                            return None;
1000                        }
1001                        trimmed
1002                            .parse::<std::net::SocketAddr>()
1003                            .map_err(|e| {
1004                                log::warn!("Invalid probe_addr entry '{}': {}", trimmed, e);
1005                                e
1006                            })
1007                            .ok()
1008                    })
1009                    .collect()
1010            })
1011            .unwrap_or_default();
1012
1013        // Parse probe_protocol (default: rnsp)
1014        let probe_protocol = match rns_config
1015            .reticulum
1016            .probe_protocol
1017            .as_deref()
1018            .map(|s| s.to_lowercase())
1019        {
1020            Some(ref s) if s == "stun" => rns_core::holepunch::ProbeProtocol::Stun,
1021            _ => rns_core::holepunch::ProbeProtocol::Rnsp,
1022        };
1023
1024        let known_destinations = match storage::load_known_destinations(&known_destinations_path) {
1025            Ok(destinations) => destinations,
1026            Err(err) if err.kind() == io::ErrorKind::NotFound => Default::default(),
1027            Err(err) => {
1028                log::warn!("failed to load known destinations: {}", err);
1029                Default::default()
1030            }
1031        };
1032        let known_destination_hashes: HashSet<[u8; 16]> =
1033            known_destinations.keys().copied().collect();
1034        match ratchet_store.cleanup(
1035            &known_destination_hashes,
1036            time::now(),
1037            rns_config.reticulum.ratchet_expiry as f64,
1038        ) {
1039            Ok(stats) if stats.processed > 0 => log::debug!(
1040                "Processed {} ratchets, not in use {}, removed {}",
1041                stats.processed,
1042                stats.not_known,
1043                stats.removed
1044            ),
1045            Ok(_) => {}
1046            Err(err) => log::warn!("failed to clean ratchets: {}", err),
1047        }
1048
1049        let node_config = NodeConfig {
1050            transport_enabled: rns_config.reticulum.enable_transport,
1051            static_transport_identity: rns_config.reticulum.static_transport_identity,
1052            local_hops_delta: rns_config.reticulum.local_hops_delta,
1053            identity: Some(identity),
1054            share_instance: !private && rns_config.reticulum.share_instance,
1055            instance_name: rns_config.reticulum.instance_name.clone(),
1056            shared_instance_port: rns_config.reticulum.shared_instance_port,
1057            rpc_port: rns_config.reticulum.instance_control_port,
1058            cache_dir: Some(paths.cache),
1059            ratchet_store: Some(Arc::clone(&ratchet_store)),
1060            ratchet_expiry: Duration::from_secs(rns_config.reticulum.ratchet_expiry),
1061            management: if private {
1062                crate::management::ManagementConfig::default()
1063            } else {
1064                crate::management::ManagementConfig {
1065                    enable_remote_management: rns_config.reticulum.enable_remote_management,
1066                    remote_management_allowed: mgmt_allowed,
1067                    publish_blackhole: rns_config.reticulum.publish_blackhole,
1068                }
1069            },
1070            probe_port: rns_config.reticulum.probe_port,
1071            probe_addrs,
1072            probe_protocol,
1073            direct_connect_policy: rns_config.reticulum.direct_connect_policy,
1074            device: rns_config.reticulum.device.clone(),
1075            underlay_mark: underlay_mark.or(rns_config.reticulum.underlay_mark),
1076            hooks: if private {
1077                Vec::new()
1078            } else {
1079                rns_config.hooks.clone()
1080            },
1081            discover_interfaces: rns_config.reticulum.discover_interfaces,
1082            autoconnect_interface_mode: rns_config
1083                .reticulum
1084                .autoconnect_interface_mode
1085                .as_deref()
1086                .and_then(parse_optional_interface_mode),
1087            autoconnect_interface_gravity: rns_config.reticulum.autoconnect_interface_gravity,
1088            autoconnect_announces_to_internal: rns_config
1089                .reticulum
1090                .autoconnect_announces_to_internal,
1091            discovery_required_value: rns_config.reticulum.required_discovery_value,
1092            respond_to_probes: rns_config.reticulum.respond_to_probes,
1093            prefer_shorter_path: rns_config.reticulum.prefer_shorter_path,
1094            max_paths_per_destination: rns_config.reticulum.max_paths_per_destination,
1095            packet_hashlist_max_entries: rns_config.reticulum.packet_hashlist_max_entries,
1096            packet_hashlist_allocation: rns_config.reticulum.packet_hashlist_allocation,
1097            max_discovery_pr_tags: rns_config.reticulum.max_discovery_pr_tags,
1098            max_path_destinations: rns_config.reticulum.max_path_destinations,
1099            max_tunnel_destinations_total: rns_config.reticulum.max_tunnel_destinations_total,
1100            known_destinations_ttl: Duration::from_secs(
1101                rns_config.reticulum.known_destinations_ttl,
1102            ),
1103            known_destinations_max_entries: rns_config.reticulum.known_destinations_max_entries,
1104            announce_table_ttl: Duration::from_secs(rns_config.reticulum.announce_table_ttl),
1105            announce_table_max_bytes: rns_config.reticulum.announce_table_max_bytes,
1106            driver_event_queue_capacity: rns_config.reticulum.driver_event_queue_capacity,
1107            interface_writer_queue_capacity: rns_config.reticulum.interface_writer_queue_capacity,
1108            announce_rate_defaults: AnnounceRateDefaults {
1109                target: rns_config.reticulum.default_ar_target,
1110                penalty: rns_config.reticulum.default_ar_penalty,
1111                grace: rns_config.reticulum.default_ar_grace,
1112            },
1113            ingress_control_defaults: rns_core::transport::types::IngressControlConfig {
1114                enabled: true,
1115                egress_enabled: rns_config.reticulum.default_egress_control,
1116                max_held_announces: rns_config.reticulum.default_ic_max_held_announces,
1117                burst_freq_new: rns_config.reticulum.default_ic_burst_freq_new,
1118                burst_freq: rns_config.reticulum.default_ic_burst_freq,
1119                pr_burst_freq_new: rns_config.reticulum.default_ic_pr_burst_freq_new,
1120                pr_burst_freq: rns_config.reticulum.default_ic_pr_burst_freq,
1121                egress_pr_freq: rns_config.reticulum.default_ec_pr_freq,
1122                new_time: rns_config.reticulum.default_ic_new_time,
1123                burst_hold: rns_config.reticulum.default_ic_burst_hold,
1124                burst_penalty: rns_config.reticulum.default_ic_burst_penalty,
1125                held_release_interval: rns_config.reticulum.default_ic_held_release_interval,
1126            },
1127            #[cfg(feature = "iface-backbone")]
1128            backbone_peer_pool: if rns_config.reticulum.backbone_peer_pool_max_connected > 0 {
1129                Some(BackbonePeerPoolSettings {
1130                    max_connected: rns_config.reticulum.backbone_peer_pool_max_connected,
1131                    failure_threshold: rns_config.reticulum.backbone_peer_pool_failure_threshold,
1132                    failure_window: Duration::from_secs(
1133                        rns_config.reticulum.backbone_peer_pool_failure_window,
1134                    ),
1135                    cooldown: Duration::from_secs(rns_config.reticulum.backbone_peer_pool_cooldown),
1136                })
1137            } else {
1138                None
1139            },
1140            announce_sig_cache_enabled: rns_config.reticulum.announce_sig_cache_enabled,
1141            announce_sig_cache_max_entries: rns_config.reticulum.announce_sig_cache_max_entries,
1142            announce_sig_cache_ttl: Duration::from_secs(
1143                rns_config.reticulum.announce_sig_cache_ttl,
1144            ),
1145            interfaces: interface_configs,
1146            registry: None,
1147            panic_on_interface_error: private || rns_config.reticulum.panic_on_interface_error,
1148            #[cfg(feature = "hooks")]
1149            provider_bridge: if !private && rns_config.reticulum.provider_bridge {
1150                Some(crate::provider_bridge::ProviderBridgeConfig {
1151                    enabled: true,
1152                    socket_path: rns_config
1153                        .reticulum
1154                        .provider_socket_path
1155                        .as_ref()
1156                        .map(std::path::PathBuf::from)
1157                        .unwrap_or_else(|| config_dir.join("provider.sock")),
1158                    queue_max_events: rns_config.reticulum.provider_queue_max_events,
1159                    queue_max_bytes: rns_config.reticulum.provider_queue_max_bytes,
1160                    overflow_policy: match rns_config.reticulum.provider_overflow_policy.as_str() {
1161                        "drop_oldest" => crate::provider_bridge::OverflowPolicy::DropOldest,
1162                        _ => crate::provider_bridge::OverflowPolicy::DropNewest,
1163                    },
1164                    node_instance: rns_config.reticulum.instance_name.clone(),
1165                })
1166            } else {
1167                None
1168            },
1169        };
1170
1171        let mut node = Self::start_with_queue_config(
1172            node_config,
1173            callbacks,
1174            NodeQueueConfig {
1175                announce_max_entries: rns_config.reticulum.announce_queue_max_entries,
1176                announce_max_interfaces: rns_config.reticulum.announce_queue_max_interfaces,
1177                announce_max_bytes: rns_config.reticulum.announce_queue_max_bytes,
1178                announce_ttl_secs: rns_config.reticulum.announce_queue_ttl as f64,
1179                announce_overflow_policy: match rns_config
1180                    .reticulum
1181                    .announce_queue_overflow_policy
1182                    .as_str()
1183                {
1184                    "drop_newest" => AnnounceQueueOverflowPolicy::DropNewest,
1185                    "drop_oldest" => AnnounceQueueOverflowPolicy::DropOldest,
1186                    _ => AnnounceQueueOverflowPolicy::DropWorst,
1187                },
1188                inbound_capacities: rns_config.reticulum.inbound_queue_capacities,
1189                link_mtu_discovery,
1190            },
1191        )?;
1192
1193        node.known_destinations_path = Some(known_destinations_path.clone());
1194        for (dest_hash, known) in known_destinations {
1195            let _ = node.query(QueryRequest::RestoreKnownDestination(
1196                crate::event::KnownDestinationEntry {
1197                    dest_hash,
1198                    identity_hash: known.identity_hash,
1199                    public_key: known.public_key,
1200                    app_data: known.app_data,
1201                    hops: known.hops,
1202                    received_at: known.received_at,
1203                    receiving_interface: rns_core::transport::types::InterfaceId(
1204                        known.receiving_interface,
1205                    ),
1206                    was_used: known.was_used,
1207                    last_used_at: known.last_used_at,
1208                    retained: known.retained,
1209                },
1210            ));
1211        }
1212
1213        Ok(node)
1214    }
1215
1216    /// Start the node. Connects all interfaces, starts driver and timer threads.
1217    pub fn start(config: NodeConfig, callbacks: Box<dyn Callbacks>) -> io::Result<Self> {
1218        crate::interface::validate_underlay_mark(config.underlay_mark)?;
1219        let inbound_queue_capacities = crate::event::InboundQueueCapacities::from_shared_capacity(
1220            config.driver_event_queue_capacity,
1221        );
1222        Self::start_with_queue_config(
1223            config,
1224            callbacks,
1225            NodeQueueConfig {
1226                inbound_capacities: inbound_queue_capacities,
1227                ..NodeQueueConfig::default()
1228            },
1229        )
1230    }
1231
1232    fn start_with_queue_config(
1233        config: NodeConfig,
1234        callbacks: Box<dyn Callbacks>,
1235        queue_config: NodeQueueConfig,
1236    ) -> io::Result<Self> {
1237        let transport_state_dir = if config.transport_enabled {
1238            config.cache_dir.as_ref().map(|cache_dir| {
1239                cache_dir
1240                    .parent()
1241                    .unwrap_or_else(|| std::path::Path::new("."))
1242                    .join("storage")
1243            })
1244        } else {
1245            None
1246        };
1247        let identity = config.identity.unwrap_or_else(|| Identity::new(&mut OsRng));
1248        let transport_identity = if config.transport_enabled || config.static_transport_identity {
1249            Identity::from_private_key(
1250                &identity
1251                    .get_private_key()
1252                    .expect("node identity must expose a private key"),
1253            )
1254        } else {
1255            Identity::new(&mut OsRng)
1256        };
1257
1258        let local_hops_delta = if config.local_hops_delta {
1259            let mut byte = [0u8; 1];
1260            OsRng.fill_bytes(&mut byte);
1261            byte[0] % 6 + 2
1262        } else {
1263            0
1264        };
1265
1266        let transport_config = TransportConfig {
1267            transport_enabled: config.transport_enabled,
1268            identity_hash: Some(*transport_identity.hash()),
1269            local_hops_delta,
1270            prefer_shorter_path: config.prefer_shorter_path,
1271            max_paths_per_destination: config.max_paths_per_destination,
1272            packet_hashlist_max_entries: config.packet_hashlist_max_entries,
1273            packet_hashlist_allocation: config.packet_hashlist_allocation,
1274            max_discovery_pr_tags: config.max_discovery_pr_tags,
1275            max_path_destinations: config.max_path_destinations,
1276            max_tunnel_destinations_total: config.max_tunnel_destinations_total,
1277            destination_timeout_secs: config.known_destinations_ttl.as_secs_f64(),
1278            announce_table_ttl_secs: config.announce_table_ttl.as_secs_f64(),
1279            announce_table_max_bytes: config.announce_table_max_bytes,
1280            announce_sig_cache_enabled: config.announce_sig_cache_enabled,
1281            announce_sig_cache_max_entries: config.announce_sig_cache_max_entries,
1282            announce_sig_cache_ttl_secs: config.announce_sig_cache_ttl.as_secs_f64(),
1283            announce_queue_max_entries: queue_config.announce_max_entries,
1284            announce_queue_max_interfaces: queue_config.announce_max_interfaces,
1285        };
1286
1287        let (tx, rx) = event::channel_with_queue_capacities(
1288            config.driver_event_queue_capacity,
1289            queue_config.inbound_capacities,
1290        );
1291        let tick_interval_ms = Arc::new(AtomicU64::new(1000));
1292        let mut driver = Driver::new(transport_config, rx, tx.clone(), callbacks);
1293        driver.underlay_mark = config.underlay_mark;
1294        driver
1295            .link_manager
1296            .set_link_mtu_discovery(queue_config.link_mtu_discovery);
1297        driver.set_announce_verify_queue_config(
1298            queue_config.announce_max_entries,
1299            queue_config.announce_max_bytes,
1300            queue_config.announce_ttl_secs,
1301            queue_config.announce_overflow_policy,
1302        );
1303        driver.async_announce_verification = true;
1304        driver.set_tick_interval_handle(Arc::clone(&tick_interval_ms));
1305        driver.known_destinations_ttl = config.known_destinations_ttl.as_secs_f64();
1306        driver.known_destinations_max_entries = config.known_destinations_max_entries;
1307        driver.ratchet_store = config.ratchet_store.clone();
1308        driver.ratchet_expiry_secs = config.ratchet_expiry.as_secs_f64();
1309        driver.interface_writer_queue_capacity = config.interface_writer_queue_capacity;
1310        driver.set_announce_rate_defaults(config.announce_rate_defaults);
1311        driver.set_ingress_control_defaults(config.ingress_control_defaults);
1312        driver.runtime_config_defaults.known_destinations_ttl =
1313            config.known_destinations_ttl.as_secs_f64();
1314        driver
1315            .holepunch_manager
1316            .set_policy(config.direct_connect_policy);
1317        driver.runtime_config_defaults.direct_connect_policy = config.direct_connect_policy;
1318        #[cfg(feature = "hooks")]
1319        if let Some(provider_config) = config.provider_bridge.clone() {
1320            driver.runtime_config_defaults.provider_queue_max_events =
1321                provider_config.queue_max_events;
1322            driver.runtime_config_defaults.provider_queue_max_bytes =
1323                provider_config.queue_max_bytes;
1324            if provider_config.enabled {
1325                match crate::provider_bridge::ProviderBridge::start(provider_config) {
1326                    Ok(bridge) => driver.provider_bridge = Some(bridge),
1327                    Err(err) => log::warn!("failed to start provider bridge: {}", err),
1328                }
1329            }
1330        }
1331
1332        // Set up announce cache if cache directory is configured
1333        if let Some(ref cache_dir) = config.cache_dir {
1334            let announces_dir = cache_dir.join("announces");
1335            let _ = std::fs::create_dir_all(&announces_dir);
1336            driver.announce_cache = Some(crate::announce_cache::AnnounceCache::new(announces_dir));
1337        }
1338
1339        // Configure probe addresses and device for hole punching
1340        if !config.probe_addrs.is_empty()
1341            || config.device.is_some()
1342            || config.underlay_mark.is_some()
1343        {
1344            driver.set_probe_config(
1345                config.probe_addrs.clone(),
1346                config.probe_protocol,
1347                config.device.clone(),
1348            );
1349        }
1350
1351        // Start probe server if configured
1352        let probe_server = if let Some(port) = config.probe_port {
1353            let listen_addr: std::net::SocketAddr = ([0, 0, 0, 0], port).into();
1354            match crate::holepunch::probe::start_probe_server_with_mark(
1355                listen_addr,
1356                config.underlay_mark,
1357            ) {
1358                Ok(handle) => {
1359                    log::info!("Probe server started on 0.0.0.0:{}", port);
1360                    Some(handle)
1361                }
1362                Err(e) => {
1363                    log::error!("Failed to start probe server on port {}: {}", port, e);
1364                    None
1365                }
1366            }
1367        } else {
1368            None
1369        };
1370
1371        // Store management config on driver for ACL enforcement
1372        driver.management_config = config.management.clone();
1373
1374        // Store transport identity for tunnel synthesis and management announces.
1375        driver.transport_identity = Some(transport_identity);
1376
1377        // Load hooks from config
1378        #[cfg(feature = "hooks")]
1379        {
1380            for hook_cfg in &config.hooks {
1381                if !hook_cfg.enabled {
1382                    continue;
1383                }
1384                let point_idx = match config::parse_hook_point(&hook_cfg.attach_point) {
1385                    Some(idx) => idx,
1386                    None => {
1387                        log::warn!(
1388                            "Unknown hook point '{}' for hook '{}'",
1389                            hook_cfg.attach_point,
1390                            hook_cfg.name,
1391                        );
1392                        continue;
1393                    }
1394                };
1395                let mgr = match driver.hook_manager.as_ref() {
1396                    Some(m) => m,
1397                    None => {
1398                        log::warn!(
1399                            "Hook manager not available, skipping hook '{}'",
1400                            hook_cfg.name
1401                        );
1402                        continue;
1403                    }
1404                };
1405                let hook_backend = match config::parse_hook_backend(&hook_cfg.hook_type) {
1406                    Ok(backend) => backend,
1407                    Err(e) => {
1408                        log::warn!(
1409                            "Invalid hook type '{}' for hook '{}': {}",
1410                            hook_cfg.hook_type,
1411                            hook_cfg.name,
1412                            e,
1413                        );
1414                        continue;
1415                    }
1416                };
1417                let load_result = if hook_backend == rns_hooks::HookBackend::Builtin {
1418                    let builtin_id = hook_cfg
1419                        .builtin_id
1420                        .as_deref()
1421                        .filter(|id| !id.is_empty())
1422                        .or_else(|| (!hook_cfg.path.is_empty()).then_some(hook_cfg.path.as_str()));
1423                    match builtin_id {
1424                        Some(id) => mgr.load_builtin(hook_cfg.name.clone(), id, hook_cfg.priority),
1425                        None => Err(rns_hooks::HookError::CompileError(
1426                            "built-in hook requires builtin/id or path".to_string(),
1427                        )),
1428                    }
1429                } else {
1430                    mgr.load_file_backend(
1431                        hook_cfg.name.clone(),
1432                        std::path::Path::new(&hook_cfg.path),
1433                        hook_cfg.priority,
1434                        hook_backend,
1435                    )
1436                };
1437                match load_result {
1438                    Ok(program) => {
1439                        driver.hook_slots[point_idx].attach(program);
1440                        log::info!(
1441                            "Loaded hook '{}' at point {} (priority {})",
1442                            hook_cfg.name,
1443                            hook_cfg.attach_point,
1444                            hook_cfg.priority,
1445                        );
1446                    }
1447                    Err(e) => {
1448                        log::error!(
1449                            "Failed to load hook '{}' from '{}': {}",
1450                            hook_cfg.name,
1451                            hook_cfg.path,
1452                            e,
1453                        );
1454                    }
1455                }
1456            }
1457        }
1458
1459        // Configure discovery
1460        driver.discover_interfaces = config.discover_interfaces;
1461        driver.autoconnect_interface_mode = config.autoconnect_interface_mode;
1462        driver.autoconnect_interface_gravity = config.autoconnect_interface_gravity;
1463        driver.autoconnect_announces_to_internal =
1464            config.autoconnect_announces_to_internal.then_some(true);
1465        if let Some(val) = config.discovery_required_value {
1466            driver.discovery_required_value = val;
1467        }
1468
1469        // Shared counter for dynamic interface IDs
1470        let next_dynamic_id = Arc::new(AtomicU64::new(10000));
1471        #[cfg(feature = "iface-backbone")]
1472        {
1473            driver.next_dynamic_interface_id = Arc::clone(&next_dynamic_id);
1474        }
1475
1476        // Collect discoverable interface configs for the announcer
1477        let mut discoverable_interfaces = Vec::new();
1478        #[cfg(feature = "iface-backbone")]
1479        let mut backbone_peer_pool_candidates = Vec::new();
1480
1481        // --- Registry-based startup for interfaces ---
1482        let registry = config
1483            .registry
1484            .unwrap_or_else(crate::interface::registry::InterfaceRegistry::with_builtins);
1485        for iface_config in config.interfaces {
1486            #[cfg(feature = "iface-backbone")]
1487            if iface_config.type_name == "BackboneInterface" {
1488                if let Some(mode) = iface_config
1489                    .config_data
1490                    .as_any()
1491                    .downcast_ref::<BackboneMode>()
1492                {
1493                    if let Some(handle) = runtime_handle_from_mode(mode) {
1494                        driver.register_backbone_runtime(handle);
1495                    }
1496                    if let Some(handle) = peer_state_handle_from_mode(mode) {
1497                        driver.register_backbone_peer_state(handle);
1498                    }
1499                    if let Some(handle) = client_runtime_handle_from_mode(mode) {
1500                        driver.register_backbone_client_runtime(handle);
1501                    }
1502                    if let Some(handle) = backbone_discovery_runtime_from_interface(
1503                        &iface_config.name,
1504                        mode,
1505                        iface_config.discovery.as_ref(),
1506                        config.transport_enabled,
1507                        iface_config.ifac.as_ref(),
1508                    ) {
1509                        driver.register_backbone_discovery_runtime(handle);
1510                    }
1511                }
1512            }
1513            #[cfg(feature = "iface-tcp")]
1514            if iface_config.type_name == "TCPClientInterface" {
1515                if let Some(tcp_config) = iface_config
1516                    .config_data
1517                    .as_any()
1518                    .downcast_ref::<TcpClientConfig>()
1519                {
1520                    driver.register_tcp_client_runtime(tcp_client_runtime_handle_from_config(
1521                        tcp_config,
1522                    ));
1523                }
1524            }
1525            #[cfg(feature = "iface-tcp")]
1526            if iface_config.type_name == "TCPServerInterface" {
1527                if let Some(tcp_config) = iface_config
1528                    .config_data
1529                    .as_any()
1530                    .downcast_ref::<TcpServerConfig>()
1531                {
1532                    driver.register_tcp_server_runtime(tcp_runtime_handle_from_config(tcp_config));
1533                    driver.register_tcp_server_discovery_runtime(
1534                        tcp_server_discovery_runtime_from_interface(
1535                            &iface_config.name,
1536                            tcp_config,
1537                            iface_config.discovery.as_ref(),
1538                            config.transport_enabled,
1539                            iface_config.ifac.as_ref(),
1540                        ),
1541                    );
1542                }
1543            }
1544            #[cfg(feature = "iface-udp")]
1545            if iface_config.type_name == "UDPInterface" {
1546                if let Some(udp_config) = iface_config
1547                    .config_data
1548                    .as_any()
1549                    .downcast_ref::<UdpConfig>()
1550                {
1551                    driver.register_udp_runtime(udp_runtime_handle_from_config(udp_config));
1552                }
1553            }
1554            #[cfg(feature = "iface-auto")]
1555            if iface_config.type_name == "AutoInterface" {
1556                if let Some(auto_config) = iface_config
1557                    .config_data
1558                    .as_any()
1559                    .downcast_ref::<AutoConfig>()
1560                {
1561                    driver.register_auto_runtime(auto_runtime_handle_from_config(auto_config));
1562                }
1563            }
1564            #[cfg(feature = "iface-i2p")]
1565            if iface_config.type_name == "I2PInterface" {
1566                if let Some(i2p_config) = iface_config
1567                    .config_data
1568                    .as_any()
1569                    .downcast_ref::<I2pConfig>()
1570                {
1571                    driver.register_i2p_runtime(i2p_runtime_handle_from_config(i2p_config));
1572                }
1573            }
1574            #[cfg(feature = "iface-pipe")]
1575            if iface_config.type_name == "PipeInterface" {
1576                if let Some(pipe_config) = iface_config
1577                    .config_data
1578                    .as_any()
1579                    .downcast_ref::<PipeConfig>()
1580                {
1581                    driver.register_pipe_runtime(pipe_runtime_handle_from_config(pipe_config));
1582                }
1583            }
1584            #[cfg(feature = "iface-rnode")]
1585            if iface_config.type_name == "RNodeInterface" {
1586                if let Some(rnode_config) = iface_config
1587                    .config_data
1588                    .as_any()
1589                    .downcast_ref::<RNodeConfig>()
1590                {
1591                    driver.register_rnode_runtime(rnode_runtime_handle_from_config(rnode_config));
1592                }
1593            }
1594
1595            let factory = match registry.get(&iface_config.type_name) {
1596                Some(f) => f,
1597                None => {
1598                    log::warn!(
1599                        "No factory registered for interface type '{}'",
1600                        iface_config.type_name
1601                    );
1602                    continue;
1603                }
1604            };
1605
1606            let mut ifac_state = derive_ifac_state(iface_config.ifac.as_ref(), &iface_config.name)?;
1607            let ifac_runtime =
1608                ifac_runtime_from_config(iface_config.ifac.as_ref(), factory.default_ifac_size());
1609
1610            #[cfg(feature = "iface-backbone")]
1611            if config.backbone_peer_pool.is_some() && iface_config.type_name == "BackboneInterface"
1612            {
1613                if let Some(mode) = iface_config
1614                    .config_data
1615                    .as_any()
1616                    .downcast_ref::<BackboneMode>()
1617                {
1618                    if let Some(client) = client_config_from_mode(mode) {
1619                        let priority = client_priority_from_mode(mode).unwrap_or(
1620                            crate::driver::BACKBONE_PEER_POOL_CONFIGURED_DEFAULT_PRIORITY,
1621                        );
1622                        backbone_peer_pool_candidates.push(BackbonePeerPoolCandidateConfig {
1623                            client,
1624                            mode: iface_config.mode,
1625                            gravity: iface_config.gravity,
1626                            recursive_prs: iface_config.recursive_prs,
1627                            announces_from_internal: iface_config.announces_from_internal,
1628                            announces_to_internal: iface_config.announces_to_internal,
1629                            ingress_control: iface_config.ingress_control,
1630                            ifac_runtime: ifac_runtime.clone(),
1631                            ifac_enabled: ifac_state.is_some(),
1632                            interface_type_name: iface_config.type_name.clone(),
1633                            source: crate::driver::BackbonePeerPoolCandidateSource::Configured,
1634                            priority,
1635                            discovery: None,
1636                        });
1637                        if let Some(ref disc) = iface_config.discovery {
1638                            discoverable_interfaces.push(discoverable_interface_from_config(
1639                                &iface_config.name,
1640                                disc,
1641                                config.transport_enabled,
1642                                iface_config.ifac.as_ref(),
1643                            ));
1644                        }
1645                        continue;
1646                    }
1647                }
1648            }
1649
1650            let ctx = crate::interface::StartContext {
1651                tx: tx.clone(),
1652                next_dynamic_id: next_dynamic_id.clone(),
1653                mode: iface_config.mode,
1654                gravity: iface_config.gravity,
1655                recursive_prs: iface_config.recursive_prs,
1656                announces_from_internal: iface_config.announces_from_internal,
1657                announces_to_internal: iface_config.announces_to_internal,
1658                ingress_control: iface_config.ingress_control,
1659                ifac: ifac_state.clone(),
1660                underlay_mark: config.underlay_mark,
1661            };
1662
1663            let result = match factory.start(iface_config.config_data, ctx) {
1664                Ok(r) => r,
1665                Err(e) => {
1666                    if config.panic_on_interface_error {
1667                        return Err(e);
1668                    }
1669                    log::error!(
1670                        "Interface '{}' ({}) failed to start: {}",
1671                        iface_config.name,
1672                        iface_config.type_name,
1673                        e
1674                    );
1675                    continue;
1676                }
1677            };
1678
1679            if let Some(ref disc) = iface_config.discovery {
1680                discoverable_interfaces.push(discoverable_interface_from_config(
1681                    &iface_config.name,
1682                    disc,
1683                    config.transport_enabled,
1684                    iface_config.ifac.as_ref(),
1685                ));
1686            }
1687
1688            match result {
1689                crate::interface::StartResult::Simple {
1690                    id,
1691                    info,
1692                    writer,
1693                    interface_type_name,
1694                } => {
1695                    register_started_interface(StartedInterface {
1696                        driver: &mut driver,
1697                        tx: &tx,
1698                        queue_capacity: config.interface_writer_queue_capacity,
1699                        id,
1700                        info,
1701                        writer,
1702                        interface_type_name,
1703                        ifac_state,
1704                        ifac_runtime: &ifac_runtime,
1705                    });
1706                }
1707                crate::interface::StartResult::Listener { control } => {
1708                    // Listener-type interface (TcpServer, Auto, I2P, etc.)
1709                    // registers dynamic interfaces via InterfaceUp events.
1710                    if let Some(control) = control {
1711                        driver.register_listener_control(control);
1712                    }
1713                }
1714                crate::interface::StartResult::Multi(subs) => {
1715                    let ifac_cfg = &iface_config.ifac;
1716                    let mut first = true;
1717                    for sub in subs {
1718                        let sub_ifac = if first {
1719                            first = false;
1720                            ifac_state.take()
1721                        } else {
1722                            derive_ifac_state(ifac_cfg.as_ref(), &sub.info.name)?
1723                        };
1724                        register_started_interface(StartedInterface {
1725                            driver: &mut driver,
1726                            tx: &tx,
1727                            queue_capacity: config.interface_writer_queue_capacity,
1728                            id: sub.id,
1729                            info: sub.info,
1730                            writer: sub.writer,
1731                            interface_type_name: sub.interface_type_name,
1732                            ifac_state: sub_ifac,
1733                            ifac_runtime: &ifac_runtime,
1734                        });
1735                    }
1736                }
1737            }
1738        }
1739
1740        // Set up interface announcer if we have discoverable interfaces
1741        if !discoverable_interfaces.is_empty() {
1742            let transport_id = *identity.hash();
1743            let announcer =
1744                crate::discovery::InterfaceAnnouncer::new(transport_id, discoverable_interfaces);
1745            log::info!("Interface discovery announcer initialized");
1746            driver.interface_announcer = Some(announcer);
1747        }
1748
1749        // Set up discovered interfaces storage path
1750        if let Some(ref cache_dir) = config.cache_dir {
1751            let disc_path = std::path::PathBuf::from(cache_dir)
1752                .parent()
1753                .unwrap_or(std::path::Path::new("."))
1754                .join("storage")
1755                .join("discovery")
1756                .join("interfaces");
1757            let _ = std::fs::create_dir_all(&disc_path);
1758            driver.discovered_interfaces =
1759                crate::discovery::DiscoveredInterfaceStorage::new(disc_path);
1760        }
1761
1762        // Restore after static interfaces and the announce cache are available,
1763        // but before the driver can process inbound traffic.
1764        if let Some(ref storage_dir) = transport_state_dir {
1765            match storage::load_transport_state(storage_dir) {
1766                Ok(snapshot) => {
1767                    let announce_cache = driver.announce_cache.as_ref();
1768                    let stats = driver.engine.restore_persistence_snapshot(
1769                        snapshot,
1770                        time::now(),
1771                        |packet_hash| {
1772                            announce_cache
1773                                .and_then(|cache| cache.get(packet_hash).ok().flatten())
1774                                .map(|(raw, _)| raw)
1775                        },
1776                    );
1777                    log::debug!(
1778                        "restored transport state: {} packet hashes, {} paths, {} tunnels ({} paths skipped)",
1779                        stats.packet_hashes,
1780                        stats.paths,
1781                        stats.tunnels,
1782                        stats.skipped_paths,
1783                    );
1784                }
1785                Err(err) => log::warn!("failed to restore transport state: {}", err),
1786            }
1787        }
1788
1789        #[cfg(feature = "iface-backbone")]
1790        if let Some(settings) = config.backbone_peer_pool.clone() {
1791            driver.configure_backbone_peer_pool(settings, backbone_peer_pool_candidates);
1792            driver.seed_backbone_peer_pool_from_discovery_cache();
1793        }
1794
1795        // Set up management destinations if enabled
1796        if config.management.enable_remote_management {
1797            if let Some(prv_key) = identity.get_private_key() {
1798                let identity_hash = *identity.hash();
1799                let mgmt_dest = crate::management::management_dest_hash(&identity_hash);
1800
1801                // Extract Ed25519 signing keys from the identity
1802                let sig_prv = rns_crypto::ed25519::Ed25519PrivateKey::from_bytes(
1803                    &prv_key[32..64].try_into().unwrap(),
1804                );
1805                let sig_pub_bytes: [u8; 32] = identity.get_public_key().unwrap()[32..64]
1806                    .try_into()
1807                    .unwrap();
1808
1809                // Register as SINGLE destination in transport engine
1810                driver
1811                    .engine
1812                    .register_destination(mgmt_dest, rns_core::constants::DESTINATION_SINGLE);
1813                driver
1814                    .local_destinations
1815                    .insert(mgmt_dest, rns_core::constants::DESTINATION_SINGLE);
1816
1817                // Register as link destination in link manager
1818                driver.link_manager.register_link_destination(
1819                    mgmt_dest,
1820                    sig_prv,
1821                    sig_pub_bytes,
1822                    crate::link_manager::ResourceStrategy::AcceptNone,
1823                );
1824
1825                // Register management path hashes
1826                driver
1827                    .link_manager
1828                    .register_management_path(crate::management::status_path_hash());
1829                driver
1830                    .link_manager
1831                    .register_management_path(crate::management::path_path_hash());
1832
1833                log::info!("Remote management enabled on {:02x?}", &mgmt_dest[..4],);
1834
1835                // Set up allowed list
1836                if !config.management.remote_management_allowed.is_empty() {
1837                    log::info!(
1838                        "Remote management allowed for {} identities",
1839                        config.management.remote_management_allowed.len(),
1840                    );
1841                }
1842            }
1843        }
1844
1845        if config.management.publish_blackhole {
1846            if let Some(prv_key) = identity.get_private_key() {
1847                let identity_hash = *identity.hash();
1848                let bh_dest = crate::management::blackhole_dest_hash(&identity_hash);
1849
1850                let sig_prv = rns_crypto::ed25519::Ed25519PrivateKey::from_bytes(
1851                    &prv_key[32..64].try_into().unwrap(),
1852                );
1853                let sig_pub_bytes: [u8; 32] = identity.get_public_key().unwrap()[32..64]
1854                    .try_into()
1855                    .unwrap();
1856
1857                driver
1858                    .engine
1859                    .register_destination(bh_dest, rns_core::constants::DESTINATION_SINGLE);
1860                driver.link_manager.register_link_destination(
1861                    bh_dest,
1862                    sig_prv,
1863                    sig_pub_bytes,
1864                    crate::link_manager::ResourceStrategy::AcceptNone,
1865                );
1866                driver
1867                    .link_manager
1868                    .register_management_path(crate::management::list_path_hash());
1869
1870                log::info!(
1871                    "Blackhole list publishing enabled on {:02x?}",
1872                    &bh_dest[..4],
1873                );
1874            }
1875        }
1876
1877        // Set up probe responder if enabled
1878        if config.respond_to_probes && config.transport_enabled {
1879            let identity_hash = *identity.hash();
1880            let probe_dest = crate::management::probe_dest_hash(&identity_hash);
1881
1882            // Register as SINGLE destination in transport engine
1883            driver
1884                .engine
1885                .register_destination(probe_dest, rns_core::constants::DESTINATION_SINGLE);
1886            driver
1887                .local_destinations
1888                .insert(probe_dest, rns_core::constants::DESTINATION_SINGLE);
1889
1890            // Register PROVE_ALL proof strategy with transport identity
1891            let probe_identity = rns_crypto::identity::Identity::from_private_key(
1892                &identity.get_private_key().unwrap(),
1893            );
1894            driver.proof_strategies.insert(
1895                probe_dest,
1896                (
1897                    rns_core::types::ProofStrategy::ProveAll,
1898                    Some(probe_identity),
1899                ),
1900            );
1901
1902            driver.probe_responder_hash = Some(probe_dest);
1903
1904            log::info!("Probe responder enabled on {:02x?}", &probe_dest[..4],);
1905        }
1906
1907        // Spawn timer thread with configurable tick interval
1908        let timer_tx = tx.clone();
1909        let timer_interval = Arc::clone(&tick_interval_ms);
1910        thread::Builder::new()
1911            .name("rns-timer".into())
1912            .spawn(move || {
1913                loop {
1914                    let ms = timer_interval.load(Ordering::Relaxed);
1915                    thread::sleep(Duration::from_millis(ms));
1916                    if timer_tx.send(Event::Tick).is_err() {
1917                        break; // receiver dropped
1918                    }
1919                }
1920            })?;
1921
1922        // Start LocalServer for shared instance clients if share_instance is enabled
1923        #[cfg(feature = "iface-local")]
1924        if config.share_instance {
1925            let local_server_config = LocalServerConfig {
1926                instance_name: config.instance_name.clone(),
1927                port: config.shared_instance_port,
1928                interface_id: rns_core::transport::types::InterfaceId(0), // Not used for server
1929            };
1930            match crate::interface::local::start_server(
1931                local_server_config,
1932                tx.clone(),
1933                next_dynamic_id.clone(),
1934            ) {
1935                Ok(control) => {
1936                    driver.register_listener_control(control);
1937                    log::info!(
1938                        "Local shared instance server started (instance={}, port={})",
1939                        config.instance_name,
1940                        config.shared_instance_port
1941                    );
1942                }
1943                Err(e) => {
1944                    log::error!("Failed to start local shared instance server: {}", e);
1945                }
1946            }
1947        }
1948
1949        // Start RPC server if share_instance is enabled
1950        let rpc_server = if config.share_instance {
1951            let auth_key =
1952                crate::rpc::derive_auth_key(&identity.get_private_key().unwrap_or([0u8; 64]));
1953            let rpc_addr = crate::rpc::RpcAddr::Tcp("127.0.0.1".into(), config.rpc_port);
1954            match crate::rpc::RpcServer::start(&rpc_addr, auth_key, tx.clone()) {
1955                Ok(server) => {
1956                    log::info!("RPC server started on 127.0.0.1:{}", config.rpc_port);
1957                    Some(server)
1958                }
1959                Err(e) => {
1960                    log::error!("Failed to start RPC server: {}", e);
1961                    None
1962                }
1963            }
1964        } else {
1965            None
1966        };
1967
1968        let announce_verify_queue = Arc::clone(&driver.announce_verify_queue);
1969        let verify_shutdown = Arc::new(AtomicBool::new(false));
1970        let verify_shutdown_thread = Arc::clone(&verify_shutdown);
1971        let verify_tx = tx.clone();
1972        let verify_handle = thread::Builder::new()
1973            .name("rns-verify".into())
1974            .spawn(move || {
1975                #[cfg(target_family = "unix")]
1976                {
1977                    unsafe {
1978                        libc::nice(5);
1979                    }
1980                }
1981
1982                while !verify_shutdown_thread.load(Ordering::Relaxed) {
1983                    let batch = {
1984                        let mut queue = announce_verify_queue
1985                            .lock()
1986                            .unwrap_or_else(|poisoned| poisoned.into_inner());
1987                        queue.take_pending(time::now())
1988                    };
1989
1990                    if batch.is_empty() {
1991                        thread::sleep(Duration::from_millis(50));
1992                        continue;
1993                    }
1994
1995                    for (key, pending) in batch {
1996                        if verify_shutdown_thread.load(Ordering::Relaxed) {
1997                            break;
1998                        }
1999                        let has_ratchet =
2000                            pending.packet.flags.context_flag == rns_core::constants::FLAG_SET;
2001                        let announce = match rns_core::announce::AnnounceData::unpack(
2002                            &pending.packet.data,
2003                            has_ratchet,
2004                        ) {
2005                            Ok(announce) => announce,
2006                            Err(_) => {
2007                                let signature = [0u8; 64];
2008                                let sig_cache_key = {
2009                                    let mut material = [0u8; 80];
2010                                    material[..16]
2011                                        .copy_from_slice(&pending.packet.destination_hash);
2012                                    material[16..].copy_from_slice(&signature);
2013                                    rns_core::hash::full_hash(&material)
2014                                };
2015                                if verify_tx
2016                                    .send(Event::AnnounceVerifyFailed { key, sig_cache_key })
2017                                    .is_err()
2018                                {
2019                                    return;
2020                                }
2021                                continue;
2022                            }
2023                        };
2024                        let mut material = [0u8; 80];
2025                        material[..16].copy_from_slice(&pending.packet.destination_hash);
2026                        material[16..].copy_from_slice(&announce.signature);
2027                        let sig_cache_key = rns_core::hash::full_hash(&material);
2028                        match announce.validate(&pending.packet.destination_hash) {
2029                            Ok(validated) => {
2030                                if verify_tx
2031                                    .send(Event::AnnounceVerified {
2032                                        key,
2033                                        validated,
2034                                        sig_cache_key,
2035                                    })
2036                                    .is_err()
2037                                {
2038                                    return;
2039                                }
2040                            }
2041                            Err(_) => {
2042                                if verify_tx
2043                                    .send(Event::AnnounceVerifyFailed { key, sig_cache_key })
2044                                    .is_err()
2045                                {
2046                                    return;
2047                                }
2048                            }
2049                        }
2050                    }
2051                }
2052            })?;
2053
2054        // Spawn the driver after startup has registered local destinations and static interfaces.
2055        // Interface readers can enqueue frames before this point; the bounded event queue preserves
2056        // ordering and backpressures instead of dropping startup traffic.
2057        let driver_handle = thread::Builder::new()
2058            .name("rns-driver".into())
2059            .spawn(move || {
2060                driver.run();
2061            })?;
2062
2063        // Match upstream's 12-hour background persistence cadence. The worker
2064        // performs disk serialization at low priority and never touches engine
2065        // state directly; it obtains a consistent snapshot through the driver.
2066        let (persist_handle, persist_stop) = if let Some(storage_dir) = transport_state_dir.clone()
2067        {
2068            let persist_tx = tx.clone();
2069            let (stop_tx, stop_rx) = std::sync::mpsc::channel();
2070            let handle = thread::Builder::new()
2071                .name("rns-persist".into())
2072                .spawn(move || {
2073                    #[cfg(target_family = "unix")]
2074                    unsafe {
2075                        libc::nice(5);
2076                    }
2077
2078                    loop {
2079                        match stop_rx.recv_timeout(Duration::from_secs(12 * 60 * 60)) {
2080                            Ok(()) | Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
2081                            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}
2082                        }
2083                        let (response_tx, response_rx) = std::sync::mpsc::channel();
2084                        if persist_tx
2085                            .send(Event::Query(
2086                                QueryRequest::TransportStateSnapshot,
2087                                response_tx,
2088                            ))
2089                            .is_err()
2090                        {
2091                            break;
2092                        }
2093                        let Ok(QueryResponse::TransportStateSnapshot(snapshot)) =
2094                            response_rx.recv()
2095                        else {
2096                            continue;
2097                        };
2098                        if let Err(err) =
2099                            storage::save_transport_state(&snapshot, &storage_dir, true)
2100                        {
2101                            log::warn!("failed to persist transport state: {}", err);
2102                        }
2103                    }
2104                })?;
2105            (Some(handle), Some(stop_tx))
2106        } else {
2107            (None, None)
2108        };
2109
2110        Ok(RnsNode {
2111            tx,
2112            driver_handle: Some(driver_handle),
2113            verify_handle: Some(verify_handle),
2114            verify_shutdown,
2115            persist_handle,
2116            persist_stop,
2117            rpc_server,
2118            tick_interval_ms,
2119            probe_server,
2120            known_destinations_path: None,
2121            transport_state_dir,
2122            ratchet_store: config.ratchet_store,
2123            ratchet_expiry: config.ratchet_expiry,
2124        })
2125    }
2126
2127    /// Query the driver for state information.
2128    pub fn query(&self, request: QueryRequest) -> Result<QueryResponse, SendError> {
2129        let (resp_tx, resp_rx) = std::sync::mpsc::channel();
2130        self.tx
2131            .send(Event::Query(request, resp_tx))
2132            .map_err(|_| SendError)?;
2133        resp_rx.recv().map_err(|_| SendError)
2134    }
2135
2136    /// Snapshot active Link metadata, including the negotiated direct-data MDU.
2137    pub fn links(&self) -> Result<Vec<crate::event::LinkInfoEntry>, SendError> {
2138        match self.query(QueryRequest::Links)? {
2139            QueryResponse::Links(links) => Ok(links),
2140            _ => Err(SendError),
2141        }
2142    }
2143
2144    /// Return the slowest positive bitrate among currently registered interfaces.
2145    ///
2146    /// Returns `Ok(None)` when no registered interface advertises a usable
2147    /// bitrate.
2148    pub fn lowest_interface_bitrate(&self) -> Result<Option<u64>, SendError> {
2149        match self.query(QueryRequest::LowestInterfaceBitrate)? {
2150            QueryResponse::LowestInterfaceBitrate(bitrate) => Ok(bitrate),
2151            _ => Err(SendError),
2152        }
2153    }
2154
2155    /// Return the adaptive medium path timeout advertised by the transport engine.
2156    ///
2157    /// The estimate covers a full MTU round trip on the slowest registered
2158    /// positive-bitrate interface plus the per-hop allowance, or is zero when
2159    /// no usable bitrate is known.
2160    pub fn medium_path_timeout(&self) -> Result<f64, SendError> {
2161        match self.query(QueryRequest::MediumPathTimeout)? {
2162            QueryResponse::MediumPathTimeout(timeout) => Ok(timeout),
2163            _ => Err(SendError),
2164        }
2165    }
2166
2167    /// Enter drain mode and stop admitting new work.
2168    pub fn begin_drain(&self, timeout: Duration) -> Result<(), SendError> {
2169        self.tx
2170            .send(Event::BeginDrain { timeout })
2171            .map_err(|_| SendError)
2172    }
2173
2174    /// Query current drain/lifecycle status.
2175    pub fn drain_status(&self) -> Result<crate::event::DrainStatus, SendError> {
2176        match self.query(QueryRequest::DrainStatus)? {
2177            QueryResponse::DrainStatus(status) => Ok(status),
2178            _ => Err(SendError),
2179        }
2180    }
2181
2182    fn reject_new_work_if_draining(&self) -> Result<(), SendError> {
2183        let status = self.drain_status()?;
2184        if matches!(status.state, crate::event::LifecycleState::Active) {
2185            Ok(())
2186        } else {
2187            Err(SendError)
2188        }
2189    }
2190
2191    /// Send a raw outbound packet.
2192    pub fn send_raw(
2193        &self,
2194        raw: Vec<u8>,
2195        dest_type: u8,
2196        attached_interface: Option<rns_core::transport::types::InterfaceId>,
2197    ) -> Result<(), SendError> {
2198        self.tx
2199            .send(Event::SendOutbound {
2200                raw,
2201                dest_type,
2202                attached_interface,
2203            })
2204            .map_err(|_| SendError)
2205    }
2206
2207    /// Register a local destination with the transport engine.
2208    pub fn register_destination(
2209        &self,
2210        dest_hash: [u8; 16],
2211        dest_type: u8,
2212    ) -> Result<(), SendError> {
2213        self.tx
2214            .send(Event::RegisterDestination {
2215                dest_hash,
2216                dest_type,
2217            })
2218            .map_err(|_| SendError)
2219    }
2220
2221    /// Deregister a local destination.
2222    pub fn deregister_destination(&self, dest_hash: [u8; 16]) -> Result<(), SendError> {
2223        self.tx
2224            .send(Event::DeregisterDestination { dest_hash })
2225            .map_err(|_| SendError)
2226    }
2227
2228    /// Deregister a link destination (stop accepting incoming links).
2229    pub fn deregister_link_destination(&self, dest_hash: [u8; 16]) -> Result<(), SendError> {
2230        self.tx
2231            .send(Event::DeregisterLinkDestination { dest_hash })
2232            .map_err(|_| SendError)
2233    }
2234
2235    /// Register a link destination that can accept incoming links.
2236    ///
2237    /// `dest_hash`: the destination hash
2238    /// `sig_prv_bytes`: Ed25519 private signing key (32 bytes)
2239    /// `sig_pub_bytes`: Ed25519 public signing key (32 bytes)
2240    pub fn register_link_destination(
2241        &self,
2242        dest_hash: [u8; 16],
2243        sig_prv_bytes: [u8; 32],
2244        sig_pub_bytes: [u8; 32],
2245        resource_strategy: u8,
2246    ) -> Result<(), SendError> {
2247        self.register_link_destination_with_max_request_size(
2248            dest_hash,
2249            sig_prv_bytes,
2250            sig_pub_bytes,
2251            resource_strategy,
2252            None,
2253        )
2254    }
2255
2256    /// Register a link destination with an optional maximum accepted request size.
2257    pub fn register_link_destination_with_max_request_size(
2258        &self,
2259        dest_hash: [u8; 16],
2260        sig_prv_bytes: [u8; 32],
2261        sig_pub_bytes: [u8; 32],
2262        resource_strategy: u8,
2263        max_request_size: Option<usize>,
2264    ) -> Result<(), SendError> {
2265        self.tx
2266            .send(Event::RegisterLinkDestination {
2267                dest_hash,
2268                sig_prv_bytes,
2269                sig_pub_bytes,
2270                resource_strategy,
2271                max_request_size,
2272            })
2273            .map_err(|_| SendError)
2274    }
2275
2276    /// Register a request handler for a given path on established links.
2277    pub fn register_request_handler<F>(
2278        &self,
2279        path: &str,
2280        allowed_list: Option<Vec<[u8; 16]>>,
2281        handler: F,
2282    ) -> Result<(), SendError>
2283    where
2284        F: Fn([u8; 16], &str, &[u8], Option<&([u8; 16], [u8; 64])>) -> Option<Vec<u8>>
2285            + Send
2286            + 'static,
2287    {
2288        self.tx
2289            .send(Event::RegisterRequestHandler {
2290                path: path.to_string(),
2291                allowed_list,
2292                handler: Box::new(handler),
2293            })
2294            .map_err(|_| SendError)
2295    }
2296
2297    /// Register a request handler that can return resource responses with metadata.
2298    pub fn register_request_handler_response<F>(
2299        &self,
2300        path: &str,
2301        allowed_list: Option<Vec<[u8; 16]>>,
2302        handler: F,
2303    ) -> Result<(), SendError>
2304    where
2305        F: Fn(
2306                [u8; 16],
2307                &str,
2308                &[u8],
2309                Option<&([u8; 16], [u8; 64])>,
2310            ) -> Option<crate::link_manager::RequestResponse>
2311            + Send
2312            + 'static,
2313    {
2314        self.tx
2315            .send(Event::RegisterRequestHandlerResponse {
2316                path: path.to_string(),
2317                allowed_list,
2318                handler: Box::new(handler),
2319            })
2320            .map_err(|_| SendError)
2321    }
2322
2323    /// Register a request handler whose response is completed asynchronously
2324    /// with [`RnsNode::send_deferred_response`].
2325    pub fn register_deferred_request_handler<F>(
2326        &self,
2327        path: &str,
2328        allowed_list: Option<Vec<[u8; 16]>>,
2329        handler: F,
2330    ) -> Result<(), SendError>
2331    where
2332        F: Fn([u8; 16], &str, [u8; 16], &[u8], Option<&([u8; 16], [u8; 64])>) + Send + 'static,
2333    {
2334        self.tx
2335            .send(Event::RegisterDeferredRequestHandler {
2336                path: path.to_string(),
2337                allowed_list,
2338                handler: Box::new(handler),
2339            })
2340            .map_err(|_| SendError)
2341    }
2342
2343    /// Complete a request accepted by a deferred request handler.
2344    pub fn send_deferred_response(
2345        &self,
2346        link_id: [u8; 16],
2347        request_id: [u8; 16],
2348        data: Vec<u8>,
2349    ) -> Result<(), SendError> {
2350        self.tx
2351            .send(Event::SendDeferredResponse {
2352                link_id,
2353                request_id,
2354                data,
2355            })
2356            .map_err(|_| SendError)
2357    }
2358
2359    /// Create an outbound link to a destination.
2360    ///
2361    /// Returns the link_id on success.
2362    pub fn create_link(
2363        &self,
2364        dest_hash: [u8; 16],
2365        dest_sig_pub_bytes: [u8; 32],
2366    ) -> Result<[u8; 16], SendError> {
2367        self.reject_new_work_if_draining()?;
2368        let (response_tx, response_rx) = std::sync::mpsc::channel();
2369        self.tx
2370            .send(Event::CreateLink {
2371                dest_hash,
2372                dest_sig_pub_bytes,
2373                response_tx,
2374            })
2375            .map_err(|_| SendError)?;
2376        let link_id = response_rx.recv().map_err(|_| SendError)?;
2377        if link_id == [0u8; 16] {
2378            Err(SendError)
2379        } else {
2380            Ok(link_id)
2381        }
2382    }
2383
2384    /// Send a request on an established link.
2385    pub fn send_request(
2386        &self,
2387        link_id: [u8; 16],
2388        path: &str,
2389        data: &[u8],
2390    ) -> Result<(), SendError> {
2391        self.send_request_with_max_response_size(link_id, path, data, None)
2392    }
2393
2394    /// Send a request with an optional maximum accepted response size.
2395    pub fn send_request_with_max_response_size(
2396        &self,
2397        link_id: [u8; 16],
2398        path: &str,
2399        data: &[u8],
2400        max_response_size: Option<usize>,
2401    ) -> Result<(), SendError> {
2402        self.reject_new_work_if_draining()?;
2403        self.tx
2404            .send(Event::SendRequest {
2405                link_id,
2406                path: path.to_string(),
2407                data: data.to_vec(),
2408                max_response_size,
2409            })
2410            .map_err(|_| SendError)
2411    }
2412
2413    /// Identify on a link (reveal identity to remote peer).
2414    pub fn identify_on_link(
2415        &self,
2416        link_id: [u8; 16],
2417        identity_prv_key: [u8; 64],
2418    ) -> Result<(), SendError> {
2419        self.reject_new_work_if_draining()?;
2420        self.tx
2421            .send(Event::IdentifyOnLink {
2422                link_id,
2423                identity_prv_key,
2424            })
2425            .map_err(|_| SendError)
2426    }
2427
2428    /// Tear down a link.
2429    pub fn teardown_link(&self, link_id: [u8; 16]) -> Result<(), SendError> {
2430        self.tx
2431            .send(Event::TeardownLink { link_id })
2432            .map_err(|_| SendError)
2433    }
2434
2435    /// Send a resource on an established link.
2436    pub fn send_resource(
2437        &self,
2438        link_id: [u8; 16],
2439        data: Vec<u8>,
2440        metadata: Option<Vec<u8>>,
2441    ) -> Result<(), SendError> {
2442        self.send_resource_with_auto_compress(link_id, data, metadata, true)
2443    }
2444
2445    /// Send a resource on an established link, controlling automatic compression.
2446    pub fn send_resource_with_auto_compress(
2447        &self,
2448        link_id: [u8; 16],
2449        data: Vec<u8>,
2450        metadata: Option<Vec<u8>>,
2451        auto_compress: bool,
2452    ) -> Result<(), SendError> {
2453        self.reject_new_work_if_draining()?;
2454        self.tx
2455            .send(Event::SendResource {
2456                link_id,
2457                data,
2458                metadata,
2459                auto_compress,
2460            })
2461            .map_err(|_| SendError)
2462    }
2463
2464    /// Send a Resource from a sequential reader without retaining the full
2465    /// logical payload in memory.
2466    pub fn send_resource_reader<R: Read + Send + 'static>(
2467        &self,
2468        link_id: [u8; 16],
2469        reader: R,
2470        declared_length: u64,
2471        metadata: Option<Vec<u8>>,
2472        auto_compress: bool,
2473    ) -> Result<crate::resource::ResourceTransferId, SendError> {
2474        self.reject_new_work_if_draining()?;
2475        let transfer_id = crate::resource::ResourceTransferId(
2476            NEXT_RESOURCE_TRANSFER_ID.fetch_add(1, Ordering::Relaxed),
2477        );
2478        self.tx
2479            .send(Event::SendResourceStream {
2480                link_id,
2481                transfer_id,
2482                reader: Box::new(reader),
2483                declared_length,
2484                metadata,
2485                auto_compress,
2486            })
2487            .map_err(|_| SendError)?;
2488        Ok(transfer_id)
2489    }
2490
2491    /// Send a file as a bounded-memory streaming Resource.
2492    pub fn send_resource_file(
2493        &self,
2494        link_id: [u8; 16],
2495        path: impl AsRef<Path>,
2496        metadata: Option<Vec<u8>>,
2497        auto_compress: bool,
2498    ) -> io::Result<crate::resource::ResourceTransferId> {
2499        let file = std::fs::File::open(path.as_ref())?;
2500        let length = file.metadata()?.len();
2501        self.send_resource_reader(link_id, file, length, metadata, auto_compress)
2502            .map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "RNS driver shut down"))
2503    }
2504
2505    /// Select delivery policy for independent incoming Resources on a link.
2506    pub fn set_resource_receive_mode(
2507        &self,
2508        link_id: [u8; 16],
2509        mode: crate::resource::ResourceReceiveMode,
2510    ) -> Result<(), SendError> {
2511        self.tx
2512            .send(Event::SetResourceReceiveMode { link_id, mode })
2513            .map_err(|_| SendError)
2514    }
2515
2516    /// Set the resource acceptance strategy for a link.
2517    ///
2518    /// 0 = AcceptNone, 1 = AcceptAll, 2 = AcceptApp
2519    pub fn set_resource_strategy(&self, link_id: [u8; 16], strategy: u8) -> Result<(), SendError> {
2520        self.tx
2521            .send(Event::SetResourceStrategy { link_id, strategy })
2522            .map_err(|_| SendError)
2523    }
2524
2525    /// Accept or reject a pending resource (for AcceptApp strategy).
2526    pub fn accept_resource(
2527        &self,
2528        link_id: [u8; 16],
2529        resource_hash: Vec<u8>,
2530        accept: bool,
2531    ) -> Result<(), SendError> {
2532        if accept {
2533            self.reject_new_work_if_draining()?;
2534        }
2535        self.tx
2536            .send(Event::AcceptResource {
2537                link_id,
2538                resource_hash,
2539                accept,
2540            })
2541            .map_err(|_| SendError)
2542    }
2543
2544    /// Send a channel message on a link.
2545    pub fn send_channel_message(
2546        &self,
2547        link_id: [u8; 16],
2548        msgtype: u16,
2549        payload: Vec<u8>,
2550    ) -> Result<(), SendError> {
2551        self.reject_new_work_if_draining()?;
2552        let (response_tx, response_rx) = std::sync::mpsc::channel();
2553        self.tx
2554            .send(Event::SendChannelMessage {
2555                link_id,
2556                msgtype,
2557                payload,
2558                response_tx,
2559            })
2560            .map_err(|_| SendError)?;
2561        response_rx
2562            .recv()
2563            .map_err(|_| SendError)?
2564            .map_err(|_| SendError)
2565    }
2566
2567    /// Propose a direct P2P connection to a peer via NAT hole punching.
2568    ///
2569    /// The link must be active and connected through a backbone node.
2570    /// If successful, a direct UDP connection will be established, bypassing the backbone.
2571    pub fn propose_direct_connect(&self, link_id: [u8; 16]) -> Result<(), SendError> {
2572        self.reject_new_work_if_draining()?;
2573        self.tx
2574            .send(Event::ProposeDirectConnect { link_id })
2575            .map_err(|_| SendError)
2576    }
2577
2578    /// Set the policy for handling incoming direct-connect proposals.
2579    pub fn set_direct_connect_policy(
2580        &self,
2581        policy: crate::holepunch::orchestrator::HolePunchPolicy,
2582    ) -> Result<(), SendError> {
2583        self.tx
2584            .send(Event::SetDirectConnectPolicy { policy })
2585            .map_err(|_| SendError)
2586    }
2587
2588    /// Send data on a link with a given context.
2589    pub fn send_on_link(
2590        &self,
2591        link_id: [u8; 16],
2592        data: Vec<u8>,
2593        context: u8,
2594    ) -> Result<(), SendError> {
2595        self.reject_new_work_if_draining()?;
2596        self.tx
2597            .send(Event::SendOnLink {
2598                link_id,
2599                data,
2600                context,
2601                response_tx: None,
2602            })
2603            .map_err(|_| SendError)
2604    }
2605
2606    /// Admit a best-effort direct Link datagram without blocking on queue space.
2607    ///
2608    /// Success means the driver accepted and encoded the packet. It does not
2609    /// imply remote delivery, acknowledgement, or interface transmission.
2610    pub fn try_send_link_datagram(
2611        &self,
2612        link_id: [u8; 16],
2613        data: Vec<u8>,
2614        context: u8,
2615        timeout: Duration,
2616    ) -> Result<(), crate::event::LinkDatagramError> {
2617        use crate::event::LinkDatagramError;
2618        if self.reject_new_work_if_draining().is_err() {
2619            return Err(LinkDatagramError::DriverStopped);
2620        }
2621        let (response_tx, response_rx) = std::sync::mpsc::channel();
2622        self.tx
2623            .try_send(Event::SendOnLink {
2624                link_id,
2625                data,
2626                context,
2627                response_tx: Some(response_tx),
2628            })
2629            .map_err(|error| match error {
2630                std::sync::mpsc::TrySendError::Full(_) => LinkDatagramError::DriverQueueFull,
2631                std::sync::mpsc::TrySendError::Disconnected(_) => LinkDatagramError::DriverStopped,
2632            })?;
2633        response_rx
2634            .recv_timeout(timeout)
2635            .map_err(|error| match error {
2636                std::sync::mpsc::RecvTimeoutError::Timeout => LinkDatagramError::AdmissionTimedOut,
2637                std::sync::mpsc::RecvTimeoutError::Disconnected => LinkDatagramError::DriverStopped,
2638            })?
2639    }
2640
2641    /// Build and broadcast an announce for a destination.
2642    ///
2643    /// The identity is used to sign the announce. Must be the identity that
2644    /// owns the destination (i.e. `identity.hash()` matches `dest.identity_hash`).
2645    pub fn announce(
2646        &self,
2647        dest: &crate::destination::Destination,
2648        identity: &Identity,
2649        app_data: Option<&[u8]>,
2650    ) -> Result<(), SendError> {
2651        self.reject_new_work_if_draining()?;
2652        let name_hash = rns_core::destination::name_hash(
2653            &dest.app_name,
2654            &dest.aspects.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
2655        );
2656
2657        let mut random_hash = [0u8; 10];
2658        OsRng.fill_bytes(&mut random_hash[..5]);
2659        // Bytes [5:10] must be the emission timestamp (seconds since epoch,
2660        // big-endian, truncated to 5 bytes) so that path table dedup can
2661        // compare announce freshness.  Matches Python: int(time.time()).to_bytes(5, "big")
2662        let now_secs = std::time::SystemTime::now()
2663            .duration_since(std::time::UNIX_EPOCH)
2664            .unwrap_or_default()
2665            .as_secs();
2666        random_hash[5..10].copy_from_slice(&now_secs.to_be_bytes()[3..8]);
2667
2668        let (announce_data, _has_ratchet) = rns_core::announce::AnnounceData::pack(
2669            identity,
2670            &dest.hash.0,
2671            &name_hash,
2672            &random_hash,
2673            None, // no ratchet
2674            app_data,
2675        )
2676        .map_err(|_| SendError)?;
2677
2678        let context_flag = rns_core::constants::FLAG_UNSET;
2679
2680        let flags = rns_core::packet::PacketFlags {
2681            header_type: rns_core::constants::HEADER_1,
2682            context_flag,
2683            transport_type: rns_core::constants::TRANSPORT_BROADCAST,
2684            destination_type: rns_core::constants::DESTINATION_SINGLE,
2685            packet_type: rns_core::constants::PACKET_TYPE_ANNOUNCE,
2686        };
2687
2688        let packet = rns_core::packet::RawPacket::pack(
2689            flags,
2690            0,
2691            &dest.hash.0,
2692            None,
2693            rns_core::constants::CONTEXT_NONE,
2694            &announce_data,
2695        )
2696        .map_err(|_| SendError)?;
2697
2698        if dest.dest_type == rns_core::types::DestinationType::Single {
2699            if let Some(identity_prv_key) = identity.get_private_key() {
2700                self.tx
2701                    .send(Event::StoreSharedAnnounce {
2702                        dest_hash: dest.hash.0,
2703                        name_hash,
2704                        identity_prv_key,
2705                        app_data: app_data.map(|d| d.to_vec()),
2706                    })
2707                    .map_err(|_| SendError)?;
2708            }
2709        }
2710
2711        self.send_raw(packet.raw, dest.dest_type.to_wire_constant(), None)
2712    }
2713
2714    /// Send an encrypted (SINGLE) or plaintext (PLAIN) packet to a destination.
2715    ///
2716    /// For SINGLE destinations, `dest.public_key` must be set (OUT direction).
2717    /// Returns the packet hash for proof tracking.
2718    pub fn send_packet(
2719        &self,
2720        dest: &crate::destination::Destination,
2721        data: &[u8],
2722    ) -> Result<rns_core::types::PacketHash, SendError> {
2723        self.reject_new_work_if_draining()?;
2724        use rns_core::types::DestinationType;
2725
2726        let payload = match dest.dest_type {
2727            DestinationType::Single => self.encrypt_single_payload(dest, data)?,
2728            DestinationType::Plain => data.to_vec(),
2729            DestinationType::Group => dest.encrypt(data).map_err(|_| SendError)?,
2730        };
2731
2732        let flags = rns_core::packet::PacketFlags {
2733            header_type: rns_core::constants::HEADER_1,
2734            context_flag: rns_core::constants::FLAG_UNSET,
2735            transport_type: rns_core::constants::TRANSPORT_BROADCAST,
2736            destination_type: dest.dest_type.to_wire_constant(),
2737            packet_type: rns_core::constants::PACKET_TYPE_DATA,
2738        };
2739
2740        let packet = rns_core::packet::RawPacket::pack(
2741            flags,
2742            0,
2743            &dest.hash.0,
2744            None,
2745            rns_core::constants::CONTEXT_NONE,
2746            &payload,
2747        )
2748        .map_err(|_| SendError)?;
2749
2750        let packet_hash = rns_core::types::PacketHash(packet.packet_hash);
2751
2752        self.tx
2753            .send(Event::SendOutbound {
2754                raw: packet.raw,
2755                dest_type: dest.dest_type.to_wire_constant(),
2756                attached_interface: None,
2757            })
2758            .map_err(|_| SendError)?;
2759
2760        Ok(packet_hash)
2761    }
2762
2763    fn encrypt_single_payload(
2764        &self,
2765        dest: &crate::destination::Destination,
2766        data: &[u8],
2767    ) -> Result<Vec<u8>, SendError> {
2768        let pub_key = dest.public_key.ok_or(SendError)?;
2769        let remote_id = rns_crypto::identity::Identity::from_public_key(&pub_key);
2770        let ratchet = self.ratchet_store.as_ref().and_then(|store| {
2771            match store.current(&dest.hash.0, time::now(), self.ratchet_expiry.as_secs_f64()) {
2772                Ok(entry) => entry.map(|entry| entry.ratchet),
2773                Err(err) => {
2774                    log::warn!(
2775                        "failed to load ratchet for {:02x}{:02x}{:02x}{:02x}..: {}",
2776                        dest.hash.0[0],
2777                        dest.hash.0[1],
2778                        dest.hash.0[2],
2779                        dest.hash.0[3],
2780                        err
2781                    );
2782                    None
2783                }
2784            }
2785        });
2786        remote_id
2787            .encrypt_with_ratchet(data, ratchet.as_ref(), &mut OsRng)
2788            .map_err(|_| SendError)
2789    }
2790
2791    /// Register a destination with the transport engine and set its proof strategy.
2792    ///
2793    /// `signing_key` is the full 64-byte identity private key (X25519 32 bytes +
2794    /// Ed25519 32 bytes), needed for ProveAll/ProveApp to sign proof packets.
2795    pub fn register_destination_with_proof(
2796        &self,
2797        dest: &crate::destination::Destination,
2798        signing_key: Option<[u8; 64]>,
2799    ) -> Result<(), SendError> {
2800        // Register with transport engine
2801        self.register_destination(dest.hash.0, dest.dest_type.to_wire_constant())?;
2802
2803        // Register proof strategy if not ProveNone
2804        if dest.proof_strategy != rns_core::types::ProofStrategy::ProveNone {
2805            self.tx
2806                .send(Event::RegisterProofStrategy {
2807                    dest_hash: dest.hash.0,
2808                    strategy: dest.proof_strategy,
2809                    signing_key,
2810                })
2811                .map_err(|_| SendError)?;
2812        }
2813
2814        Ok(())
2815    }
2816
2817    /// Request a path to a destination from the network.
2818    pub fn request_path(&self, dest_hash: &rns_core::types::DestHash) -> Result<(), SendError> {
2819        self.reject_new_work_if_draining()?;
2820        self.tx
2821            .send(Event::RequestPath {
2822                dest_hash: dest_hash.0,
2823            })
2824            .map_err(|_| SendError)
2825    }
2826
2827    /// Check if a path exists to a destination (synchronous query).
2828    pub fn has_path(&self, dest_hash: &rns_core::types::DestHash) -> Result<bool, SendError> {
2829        match self.query(QueryRequest::HasPath {
2830            dest_hash: dest_hash.0,
2831        })? {
2832            QueryResponse::HasPath(v) => Ok(v),
2833            _ => Ok(false),
2834        }
2835    }
2836
2837    /// Get hop count to a destination (synchronous query).
2838    pub fn hops_to(&self, dest_hash: &rns_core::types::DestHash) -> Result<Option<u8>, SendError> {
2839        match self.query(QueryRequest::HopsTo {
2840            dest_hash: dest_hash.0,
2841        })? {
2842            QueryResponse::HopsTo(v) => Ok(v),
2843            _ => Ok(None),
2844        }
2845    }
2846
2847    /// Recall the identity information for a previously announced destination.
2848    pub fn recall_identity(
2849        &self,
2850        dest_hash: &rns_core::types::DestHash,
2851    ) -> Result<Option<crate::destination::AnnouncedIdentity>, SendError> {
2852        match self.query(QueryRequest::RecallIdentity {
2853            dest_hash: dest_hash.0,
2854        })? {
2855            QueryResponse::RecallIdentity(v) => Ok(v),
2856            _ => Ok(None),
2857        }
2858    }
2859
2860    /// List known destinations and their lifecycle state.
2861    pub fn known_destinations(
2862        &self,
2863    ) -> Result<Vec<crate::event::KnownDestinationEntry>, SendError> {
2864        match self.query(QueryRequest::KnownDestinations)? {
2865            QueryResponse::KnownDestinations(entries) => Ok(entries),
2866            _ => Ok(Vec::new()),
2867        }
2868    }
2869
2870    /// Mark a known destination as retained.
2871    pub fn retain_known_destination(
2872        &self,
2873        dest_hash: &rns_core::types::DestHash,
2874    ) -> Result<bool, SendError> {
2875        match self.query(QueryRequest::RetainKnownDestination {
2876            dest_hash: dest_hash.0,
2877        })? {
2878            QueryResponse::RetainKnownDestination(ok) => Ok(ok),
2879            _ => Ok(false),
2880        }
2881    }
2882
2883    /// Mark all known destinations announced by an identity as retained.
2884    pub fn retain_identity(
2885        &self,
2886        identity_hash: &rns_core::types::IdentityHash,
2887    ) -> Result<bool, SendError> {
2888        match self.query(QueryRequest::RetainIdentity {
2889            identity_hash: identity_hash.0,
2890        })? {
2891            QueryResponse::RetainIdentity(ok) => Ok(ok),
2892            _ => Ok(false),
2893        }
2894    }
2895
2896    /// Clear the retained flag on a known destination.
2897    pub fn unretain_known_destination(
2898        &self,
2899        dest_hash: &rns_core::types::DestHash,
2900    ) -> Result<bool, SendError> {
2901        match self.query(QueryRequest::UnretainKnownDestination {
2902            dest_hash: dest_hash.0,
2903        })? {
2904            QueryResponse::UnretainKnownDestination(ok) => Ok(ok),
2905            _ => Ok(false),
2906        }
2907    }
2908
2909    /// Mark a known destination as used.
2910    pub fn mark_known_destination_used(
2911        &self,
2912        dest_hash: &rns_core::types::DestHash,
2913    ) -> Result<bool, SendError> {
2914        match self.query(QueryRequest::MarkKnownDestinationUsed {
2915            dest_hash: dest_hash.0,
2916        })? {
2917            QueryResponse::MarkKnownDestinationUsed(ok) => Ok(ok),
2918            _ => Ok(false),
2919        }
2920    }
2921
2922    fn persist_known_destinations(&self) {
2923        let Some(path) = self.known_destinations_path.as_ref() else {
2924            return;
2925        };
2926
2927        let Ok(entries) = self.known_destinations() else {
2928            return;
2929        };
2930
2931        let destinations: std::collections::HashMap<[u8; 16], storage::KnownDestination> = entries
2932            .into_iter()
2933            .map(|entry| {
2934                (
2935                    entry.dest_hash,
2936                    storage::KnownDestination {
2937                        identity_hash: entry.identity_hash,
2938                        public_key: entry.public_key,
2939                        app_data: entry.app_data,
2940                        hops: entry.hops,
2941                        received_at: entry.received_at,
2942                        receiving_interface: entry.receiving_interface.0,
2943                        was_used: entry.was_used,
2944                        last_used_at: entry.last_used_at,
2945                        retained: entry.retained,
2946                    },
2947                )
2948            })
2949            .collect();
2950
2951        if let Err(err) = storage::save_known_destinations(&destinations, path) {
2952            log::warn!("failed to persist known destinations: {}", err);
2953        }
2954    }
2955
2956    fn persist_transport_state(&self) {
2957        let Some(storage_dir) = self.transport_state_dir.as_ref() else {
2958            return;
2959        };
2960        let Ok(QueryResponse::TransportStateSnapshot(snapshot)) =
2961            self.query(QueryRequest::TransportStateSnapshot)
2962        else {
2963            return;
2964        };
2965        if let Err(err) = storage::save_transport_state(&snapshot, storage_dir, true) {
2966            log::warn!("failed to persist transport state: {}", err);
2967        }
2968    }
2969
2970    /// Load an in-memory WASM hook at runtime.
2971    pub fn load_hook(
2972        &self,
2973        name: String,
2974        wasm_bytes: Vec<u8>,
2975        attach_point: String,
2976        priority: i32,
2977    ) -> Result<Result<(), String>, SendError> {
2978        let (response_tx, response_rx) = std::sync::mpsc::channel();
2979        self.tx
2980            .send(Event::LoadHook {
2981                name,
2982                wasm_bytes,
2983                attach_point,
2984                priority,
2985                response_tx,
2986            })
2987            .map_err(|_| SendError)?;
2988        response_rx.recv().map_err(|_| SendError)
2989    }
2990
2991    /// Load a hook from a server-local filesystem path at runtime.
2992    pub fn load_hook_file(
2993        &self,
2994        name: String,
2995        path: String,
2996        hook_type: String,
2997        attach_point: String,
2998        priority: i32,
2999    ) -> Result<Result<(), String>, SendError> {
3000        let (response_tx, response_rx) = std::sync::mpsc::channel();
3001        self.tx
3002            .send(Event::LoadHookFile {
3003                name,
3004                path,
3005                hook_type,
3006                attach_point,
3007                priority,
3008                response_tx,
3009            })
3010            .map_err(|_| SendError)?;
3011        response_rx.recv().map_err(|_| SendError)
3012    }
3013
3014    /// Load a registered built-in hook at runtime.
3015    pub fn load_builtin_hook(
3016        &self,
3017        name: String,
3018        builtin_id: String,
3019        attach_point: String,
3020        priority: i32,
3021    ) -> Result<Result<(), String>, SendError> {
3022        let (response_tx, response_rx) = std::sync::mpsc::channel();
3023        self.tx
3024            .send(Event::LoadBuiltinHook {
3025                name,
3026                builtin_id,
3027                attach_point,
3028                priority,
3029                response_tx,
3030            })
3031            .map_err(|_| SendError)?;
3032        response_rx.recv().map_err(|_| SendError)
3033    }
3034
3035    /// Unload a hook at runtime.
3036    pub fn unload_hook(
3037        &self,
3038        name: String,
3039        attach_point: String,
3040    ) -> Result<Result<(), String>, SendError> {
3041        let (response_tx, response_rx) = std::sync::mpsc::channel();
3042        self.tx
3043            .send(Event::UnloadHook {
3044                name,
3045                attach_point,
3046                response_tx,
3047            })
3048            .map_err(|_| SendError)?;
3049        response_rx.recv().map_err(|_| SendError)
3050    }
3051
3052    /// Reload an in-memory WASM hook at runtime (detach + recompile + reattach with same priority).
3053    pub fn reload_hook(
3054        &self,
3055        name: String,
3056        attach_point: String,
3057        wasm_bytes: Vec<u8>,
3058    ) -> Result<Result<(), String>, SendError> {
3059        let (response_tx, response_rx) = std::sync::mpsc::channel();
3060        self.tx
3061            .send(Event::ReloadHook {
3062                name,
3063                attach_point,
3064                wasm_bytes,
3065                response_tx,
3066            })
3067            .map_err(|_| SendError)?;
3068        response_rx.recv().map_err(|_| SendError)
3069    }
3070
3071    /// Reload a hook from a server-local filesystem path at runtime.
3072    pub fn reload_hook_file(
3073        &self,
3074        name: String,
3075        attach_point: String,
3076        path: String,
3077        hook_type: String,
3078    ) -> Result<Result<(), String>, SendError> {
3079        let (response_tx, response_rx) = std::sync::mpsc::channel();
3080        self.tx
3081            .send(Event::ReloadHookFile {
3082                name,
3083                attach_point,
3084                path,
3085                hook_type,
3086                response_tx,
3087            })
3088            .map_err(|_| SendError)?;
3089        response_rx.recv().map_err(|_| SendError)
3090    }
3091
3092    /// Reload a registered built-in hook at runtime.
3093    pub fn reload_builtin_hook(
3094        &self,
3095        name: String,
3096        attach_point: String,
3097        builtin_id: String,
3098    ) -> Result<Result<(), String>, SendError> {
3099        let (response_tx, response_rx) = std::sync::mpsc::channel();
3100        self.tx
3101            .send(Event::ReloadBuiltinHook {
3102                name,
3103                attach_point,
3104                builtin_id,
3105                response_tx,
3106            })
3107            .map_err(|_| SendError)?;
3108        response_rx.recv().map_err(|_| SendError)
3109    }
3110
3111    /// Enable or disable a loaded hook at runtime.
3112    pub fn set_hook_enabled(
3113        &self,
3114        name: String,
3115        attach_point: String,
3116        enabled: bool,
3117    ) -> Result<Result<(), String>, SendError> {
3118        let (response_tx, response_rx) = std::sync::mpsc::channel();
3119        self.tx
3120            .send(Event::SetHookEnabled {
3121                name,
3122                attach_point,
3123                enabled,
3124                response_tx,
3125            })
3126            .map_err(|_| SendError)?;
3127        response_rx.recv().map_err(|_| SendError)
3128    }
3129
3130    /// Update the priority of a loaded hook at runtime.
3131    pub fn set_hook_priority(
3132        &self,
3133        name: String,
3134        attach_point: String,
3135        priority: i32,
3136    ) -> Result<Result<(), String>, SendError> {
3137        let (response_tx, response_rx) = std::sync::mpsc::channel();
3138        self.tx
3139            .send(Event::SetHookPriority {
3140                name,
3141                attach_point,
3142                priority,
3143                response_tx,
3144            })
3145            .map_err(|_| SendError)?;
3146        response_rx.recv().map_err(|_| SendError)
3147    }
3148
3149    /// List all loaded hooks.
3150    pub fn list_hooks(&self) -> Result<Vec<crate::event::HookInfo>, SendError> {
3151        let (response_tx, response_rx) = std::sync::mpsc::channel();
3152        self.tx
3153            .send(Event::ListHooks { response_tx })
3154            .map_err(|_| SendError)?;
3155        response_rx.recv().map_err(|_| SendError)
3156    }
3157
3158    /// Construct an RnsNode from its constituent parts.
3159    /// Used by `shared_client` to build a client-mode node.
3160    pub(crate) fn from_parts(
3161        tx: EventSender,
3162        driver_handle: thread::JoinHandle<()>,
3163        rpc_server: Option<crate::rpc::RpcServer>,
3164        tick_interval_ms: Arc<AtomicU64>,
3165    ) -> Self {
3166        RnsNode {
3167            tx,
3168            driver_handle: Some(driver_handle),
3169            verify_handle: None,
3170            verify_shutdown: Arc::new(AtomicBool::new(false)),
3171            persist_handle: None,
3172            persist_stop: None,
3173            rpc_server,
3174            tick_interval_ms,
3175            probe_server: None,
3176            known_destinations_path: None,
3177            transport_state_dir: None,
3178            ratchet_store: None,
3179            ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
3180        }
3181    }
3182
3183    /// Get the event sender for direct event injection.
3184    pub fn event_sender(&self) -> &EventSender {
3185        &self.tx
3186    }
3187
3188    /// Set the tick interval in milliseconds.
3189    /// Default is 1000 (1 second). Changes take effect on the next tick cycle.
3190    /// Values are clamped to the range 100..=10000.
3191    /// Returns the actual stored value (which may differ from `ms` if clamped).
3192    pub fn set_tick_interval(&self, ms: u64) -> u64 {
3193        let clamped = ms.clamp(100, 10_000);
3194        if clamped != ms {
3195            log::warn!(
3196                "tick interval {}ms out of range, clamped to {}ms",
3197                ms,
3198                clamped
3199            );
3200        }
3201        self.tick_interval_ms.store(clamped, Ordering::Relaxed);
3202        clamped
3203    }
3204
3205    /// Get the current tick interval in milliseconds.
3206    pub fn tick_interval(&self) -> u64 {
3207        self.tick_interval_ms.load(Ordering::Relaxed)
3208    }
3209
3210    /// Shut down the node. Blocks until the driver thread exits.
3211    pub fn shutdown(mut self) {
3212        self.shutdown_inner();
3213    }
3214
3215    fn shutdown_inner(&mut self) {
3216        if self.driver_handle.is_none()
3217            && self.verify_handle.is_none()
3218            && self.persist_handle.is_none()
3219            && self.rpc_server.is_none()
3220        {
3221            return;
3222        }
3223        // Stop RPC server first
3224        if let Some(mut rpc) = self.rpc_server.take() {
3225            rpc.stop();
3226        }
3227        self.persist_known_destinations();
3228        self.persist_transport_state();
3229        if let Some(stop) = self.persist_stop.take() {
3230            let _ = stop.send(());
3231        }
3232        if let Some(handle) = self.persist_handle.take() {
3233            let _ = handle.join();
3234        }
3235        self.verify_shutdown.store(true, Ordering::Relaxed);
3236        let _ = self.tx.send(Event::Shutdown);
3237        if let Some(handle) = self.driver_handle.take() {
3238            let _ = handle.join();
3239        }
3240        if let Some(handle) = self.verify_handle.take() {
3241            let _ = handle.join();
3242        }
3243    }
3244}
3245
3246impl Drop for RnsNode {
3247    fn drop(&mut self) {
3248        self.shutdown_inner();
3249    }
3250}
3251
3252#[cfg(test)]
3253mod tests {
3254    use super::*;
3255    use crate::driver::IfacRuntimeConfig;
3256    use crate::storage::RatchetStore;
3257    use std::fs;
3258    use tempfile::tempdir;
3259
3260    struct NoopCallbacks;
3261
3262    impl Callbacks for NoopCallbacks {
3263        fn on_announce(&mut self, _: crate::destination::AnnouncedIdentity) {}
3264        fn on_path_updated(&mut self, _: rns_core::types::DestHash, _: u8) {}
3265        fn on_local_delivery(
3266            &mut self,
3267            _: rns_core::types::DestHash,
3268            _: Vec<u8>,
3269            _: rns_core::types::PacketHash,
3270        ) {
3271        }
3272    }
3273
3274    struct TestWriter;
3275
3276    impl crate::interface::Writer for TestWriter {
3277        fn send_frame(&mut self, _data: &[u8]) -> io::Result<()> {
3278            Ok(())
3279        }
3280    }
3281
3282    fn test_transport_config(transport_enabled: bool) -> TransportConfig {
3283        TransportConfig {
3284            transport_enabled,
3285            identity_hash: None,
3286            local_hops_delta: 0,
3287            prefer_shorter_path: false,
3288            max_paths_per_destination: 1,
3289            packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
3290            packet_hashlist_allocation: rns_core::transport::types::PacketHashlistAllocation::Eager,
3291            max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
3292            max_path_destinations: usize::MAX,
3293            max_tunnel_destinations_total: usize::MAX,
3294            destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
3295            announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
3296            announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
3297            announce_sig_cache_enabled: true,
3298            announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
3299            announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
3300            announce_queue_max_entries: 256,
3301            announce_queue_max_interfaces: 1024,
3302        }
3303    }
3304
3305    fn test_interface_info(id: u64) -> rns_core::transport::types::InterfaceInfo {
3306        rns_core::transport::types::InterfaceInfo {
3307            id: rns_core::transport::types::InterfaceId(id),
3308            name: format!("test-{id}"),
3309            mode: rns_core::constants::MODE_FULL,
3310            gravity: 0,
3311            recursive_prs: false,
3312            announces_from_internal: true,
3313            announces_to_internal: None,
3314            out_capable: true,
3315            in_capable: true,
3316            bitrate: None,
3317            airtime_profile: None,
3318            announce_rate_target: None,
3319            announce_rate_grace: 0,
3320            announce_rate_penalty: 0.0,
3321            announce_cap: rns_core::constants::ANNOUNCE_CAP,
3322            is_local_client: false,
3323            wants_tunnel: false,
3324            tunnel_id: None,
3325            mtu: rns_core::constants::MTU as u32,
3326            ingress_control: rns_core::transport::types::IngressControlConfig::disabled(),
3327            ia_freq: 0.0,
3328            ip_freq: 0.0,
3329            op_freq: 0.0,
3330            op_samples: 0,
3331            started: 0.0,
3332        }
3333    }
3334
3335    #[test]
3336    fn static_interface_registration_applies_transport_announce_rate_defaults() {
3337        let (tx, rx) = crate::event::channel();
3338        let mut driver = Driver::new(
3339            test_transport_config(true),
3340            rx,
3341            tx.clone(),
3342            Box::new(NoopCallbacks),
3343        );
3344        driver.set_announce_rate_defaults(AnnounceRateDefaults {
3345            target: Some(7200.0),
3346            penalty: 15.0,
3347            grace: 7,
3348        });
3349
3350        let id = rns_core::transport::types::InterfaceId(7);
3351        register_started_interface(StartedInterface {
3352            driver: &mut driver,
3353            tx: &tx,
3354            queue_capacity: crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
3355            id,
3356            info: test_interface_info(id.0),
3357            writer: Box::new(TestWriter),
3358            interface_type_name: "TestInterface".to_string(),
3359            ifac_state: None,
3360            ifac_runtime: &IfacRuntimeConfig {
3361                netname: None,
3362                netkey: None,
3363                size: 16,
3364            },
3365        });
3366
3367        let info = &driver.interfaces[&id].info;
3368        assert_eq!(info.announce_rate_target, Some(7200.0));
3369        assert_eq!(info.announce_rate_penalty, 15.0);
3370        assert_eq!(info.announce_rate_grace, 7);
3371    }
3372
3373    #[test]
3374    fn static_interface_registration_applies_transport_ingress_control_defaults() {
3375        let (tx, rx) = crate::event::channel();
3376        let mut driver = Driver::new(
3377            test_transport_config(true),
3378            rx,
3379            tx.clone(),
3380            Box::new(NoopCallbacks),
3381        );
3382        let mut defaults = rns_core::transport::types::IngressControlConfig::enabled();
3383        defaults.burst_hold = 1.5;
3384        defaults.pr_burst_freq = 5.5;
3385        defaults.egress_enabled = true;
3386        defaults.egress_pr_freq = 9.5;
3387        driver.set_ingress_control_defaults(defaults);
3388
3389        let id = rns_core::transport::types::InterfaceId(8);
3390        register_started_interface(StartedInterface {
3391            driver: &mut driver,
3392            tx: &tx,
3393            queue_capacity: crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
3394            id,
3395            info: test_interface_info(id.0),
3396            writer: Box::new(TestWriter),
3397            interface_type_name: "TestInterface".to_string(),
3398            ifac_state: None,
3399            ifac_runtime: &IfacRuntimeConfig {
3400                netname: None,
3401                netkey: None,
3402                size: 16,
3403            },
3404        });
3405
3406        let ic = driver.interfaces[&id].info.ingress_control;
3407        assert_eq!(ic.burst_hold, 1.5);
3408        assert_eq!(ic.pr_burst_freq, 5.5);
3409        assert!(ic.egress_enabled);
3410        assert_eq!(ic.egress_pr_freq, 9.5);
3411    }
3412
3413    struct TestNodeRatchetStore {
3414        entry: storage::RatchetEntry,
3415        current_calls: std::sync::Mutex<Vec<[u8; 16]>>,
3416    }
3417
3418    impl storage::RatchetStore for TestNodeRatchetStore {
3419        fn remember(&self, _dest_hash: [u8; 16], _entry: storage::RatchetEntry) -> io::Result<()> {
3420            Ok(())
3421        }
3422
3423        fn current(
3424            &self,
3425            dest_hash: &[u8; 16],
3426            _now: f64,
3427            _expiry_secs: f64,
3428        ) -> io::Result<Option<storage::RatchetEntry>> {
3429            self.current_calls.lock().unwrap().push(*dest_hash);
3430            Ok(Some(self.entry))
3431        }
3432
3433        fn cleanup(
3434            &self,
3435            _known_destinations: &HashSet<[u8; 16]>,
3436            _now: f64,
3437            _expiry_secs: f64,
3438        ) -> io::Result<storage::RatchetCleanupStats> {
3439            Ok(Default::default())
3440        }
3441    }
3442
3443    #[test]
3444    fn send_packet_checks_ratchet_store_for_single_destinations() {
3445        let store = Arc::new(TestNodeRatchetStore {
3446            entry: storage::RatchetEntry {
3447                ratchet: [0x55; 32],
3448                received_at: time::now(),
3449            },
3450            current_calls: std::sync::Mutex::new(Vec::new()),
3451        });
3452        let ratchet_store: Arc<dyn storage::RatchetStore> = store.clone();
3453        let node = RnsNode::start(
3454            NodeConfig {
3455                ratchet_store: Some(ratchet_store),
3456                ..Default::default()
3457            },
3458            Box::new(NoopCallbacks),
3459        )
3460        .unwrap();
3461
3462        let mut rng = OsRng;
3463        let remote_identity = Identity::new(&mut rng);
3464        let public_key = remote_identity.get_public_key().unwrap();
3465        let announced = crate::destination::AnnouncedIdentity {
3466            dest_hash: rns_core::types::DestHash([0x22; 16]),
3467            identity_hash: rns_core::types::IdentityHash(*remote_identity.hash()),
3468            public_key,
3469            app_data: None,
3470            hops: 1,
3471            received_at: time::now(),
3472            receiving_interface: rns_core::transport::types::InterfaceId(0),
3473            rssi: Some(-100),
3474            snr: Some(10.5),
3475        };
3476        let dest = crate::destination::Destination::single_out("test", &["ratchet"], &announced);
3477
3478        node.send_packet(&dest, b"hello").unwrap();
3479        assert_eq!(
3480            store.current_calls.lock().unwrap().as_slice(),
3481            &[dest.hash.0]
3482        );
3483
3484        node.shutdown();
3485    }
3486
3487    #[test]
3488    fn single_payload_uses_stored_ratchet_key_material() {
3489        let ratchet_prv = rns_crypto::x25519::X25519PrivateKey::from_bytes(&[0x42; 32]);
3490        let ratchet_pub = ratchet_prv.public_key().public_bytes();
3491        let store = Arc::new(TestNodeRatchetStore {
3492            entry: storage::RatchetEntry {
3493                ratchet: ratchet_pub,
3494                received_at: time::now(),
3495            },
3496            current_calls: std::sync::Mutex::new(Vec::new()),
3497        });
3498        let ratchet_store: Arc<dyn storage::RatchetStore> = store.clone();
3499        let (tx, _rx) = crate::event::channel();
3500        let node = RnsNode {
3501            tx,
3502            driver_handle: None,
3503            verify_handle: None,
3504            verify_shutdown: Arc::new(AtomicBool::new(false)),
3505            persist_handle: None,
3506            persist_stop: None,
3507            rpc_server: None,
3508            tick_interval_ms: Arc::new(AtomicU64::new(1000)),
3509            probe_server: None,
3510            known_destinations_path: None,
3511            transport_state_dir: None,
3512            ratchet_store: Some(ratchet_store),
3513            ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
3514        };
3515
3516        let mut rng = OsRng;
3517        let remote_identity = Identity::new(&mut rng);
3518        let public_key = remote_identity.get_public_key().unwrap();
3519        let announced = crate::destination::AnnouncedIdentity {
3520            dest_hash: rns_core::types::DestHash([0x33; 16]),
3521            identity_hash: rns_core::types::IdentityHash(*remote_identity.hash()),
3522            public_key,
3523            app_data: None,
3524            hops: 1,
3525            received_at: time::now(),
3526            receiving_interface: rns_core::transport::types::InterfaceId(0),
3527            rssi: Some(-100),
3528            snr: Some(10.5),
3529        };
3530        let dest = crate::destination::Destination::single_out("test", &["ratchet"], &announced);
3531        let payload = node
3532            .encrypt_single_payload(&dest, b"ratchet message")
3533            .unwrap();
3534
3535        assert_eq!(
3536            store.current_calls.lock().unwrap().as_slice(),
3537            &[dest.hash.0]
3538        );
3539        assert!(remote_identity.decrypt(&payload).is_err());
3540
3541        let peer_pub_bytes: [u8; 32] = payload[..32].try_into().unwrap();
3542        let peer_pub = rns_crypto::x25519::X25519PublicKey::from_bytes(&peer_pub_bytes);
3543        let shared_key = ratchet_prv.exchange(&peer_pub);
3544        let derived_key = rns_crypto::hkdf::hkdf(
3545            rns_crypto::identity::DERIVED_KEY_LENGTH,
3546            &shared_key,
3547            Some(remote_identity.hash()),
3548            None,
3549        )
3550        .unwrap();
3551        let token = rns_crypto::token::Token::new(&derived_key).unwrap();
3552        let plaintext = token.decrypt(&payload[32..]).unwrap();
3553        assert_eq!(plaintext, b"ratchet message");
3554    }
3555
3556    #[test]
3557    fn tcp_client_interface_is_not_discoverable_without_kiss_framing() {
3558        let mut params = std::collections::HashMap::new();
3559        params.insert("discoverable".to_string(), "yes".to_string());
3560        params.insert(
3561            "discovery_name".to_string(),
3562            "invalid-tcp-client".to_string(),
3563        );
3564        params.insert("reachable_on".to_string(), "example.com".to_string());
3565        params.insert("target_port".to_string(), "4242".to_string());
3566
3567        let discovery =
3568            super::extract_discovery_config("tcp-client", "TCPClientInterface", &params);
3569
3570        assert!(
3571            discovery.is_none(),
3572            "TCPClientInterface discovery must be rejected unless KISS framing is supported"
3573        );
3574    }
3575
3576    #[test]
3577    fn discovery_config_parses_location_command() {
3578        let mut params = std::collections::HashMap::new();
3579        params.insert("discoverable".into(), "yes".into());
3580        params.insert("location_cmd".into(), "~/bin/reticulum-location".into());
3581
3582        let discovery =
3583            super::extract_discovery_config("public", "BackboneInterface", &params).unwrap();
3584        assert_eq!(
3585            discovery.location_cmd.as_deref(),
3586            Some("~/bin/reticulum-location")
3587        );
3588    }
3589
3590    #[test]
3591    fn discovery_config_parses_valid_operator_lxmf_address() {
3592        let mut params = std::collections::HashMap::new();
3593        params.insert("discoverable".into(), "yes".into());
3594        params.insert("discovery_lxmf_address".into(), "ab".repeat(16));
3595
3596        let discovery =
3597            super::extract_discovery_config("public", "BackboneInterface", &params).unwrap();
3598
3599        assert_eq!(discovery.operator_lxmf_address, Some([0xab; 16]));
3600    }
3601
3602    #[test]
3603    fn discovery_config_ignores_invalid_operator_lxmf_addresses() {
3604        for address in ["ab".repeat(15), "zz".repeat(16)] {
3605            let mut params = std::collections::HashMap::new();
3606            params.insert("discoverable".into(), "yes".into());
3607            params.insert("discovery_lxmf_address".into(), address);
3608
3609            let discovery =
3610                super::extract_discovery_config("public", "BackboneInterface", &params).unwrap();
3611
3612            assert_eq!(discovery.operator_lxmf_address, None);
3613        }
3614    }
3615
3616    #[test]
3617    fn ingress_control_config_defaults_by_interface_type() {
3618        let params = std::collections::HashMap::new();
3619
3620        let tcp = super::parse_ingress_control_config("TCPServerInterface", &params).unwrap();
3621        assert!(tcp.enabled);
3622        assert_eq!(
3623            tcp.max_held_announces,
3624            rns_core::constants::IC_MAX_HELD_ANNOUNCES
3625        );
3626        assert_eq!(tcp.burst_hold, rns_core::constants::IC_BURST_HOLD);
3627
3628        let pipe = super::parse_ingress_control_config("PipeInterface", &params).unwrap();
3629        assert!(!pipe.enabled);
3630        assert_eq!(
3631            pipe.held_release_interval,
3632            rns_core::constants::IC_HELD_RELEASE_INTERVAL
3633        );
3634    }
3635
3636    #[test]
3637    fn ingress_control_config_parses_python_ic_keys() {
3638        let mut params = std::collections::HashMap::new();
3639        params.insert("ingress_control".to_string(), "No".to_string());
3640        params.insert("ic_max_held_announces".to_string(), "17".to_string());
3641        params.insert("ic_burst_hold".to_string(), "1.5".to_string());
3642        params.insert("ic_burst_freq_new".to_string(), "2.5".to_string());
3643        params.insert("ic_burst_freq".to_string(), "3.5".to_string());
3644        params.insert("ic_pr_burst_freq_new".to_string(), "3.25".to_string());
3645        params.insert("ic_pr_burst_freq".to_string(), "8.25".to_string());
3646        params.insert("egress_control".to_string(), "Yes".to_string());
3647        params.insert("ec_pr_freq".to_string(), "5.25".to_string());
3648        params.insert("ic_new_time".to_string(), "4.5".to_string());
3649        params.insert("ic_burst_penalty".to_string(), "5.5".to_string());
3650        params.insert("ic_held_release_interval".to_string(), "6.5".to_string());
3651
3652        let config = super::parse_ingress_control_config("TCPServerInterface", &params).unwrap();
3653
3654        assert!(!config.enabled);
3655        assert_eq!(config.max_held_announces, 17);
3656        assert_eq!(config.burst_hold, 1.5);
3657        assert_eq!(config.burst_freq_new, 2.5);
3658        assert_eq!(config.burst_freq, 3.5);
3659        assert_eq!(config.pr_burst_freq_new, 3.25);
3660        assert_eq!(config.pr_burst_freq, 8.25);
3661        assert!(config.egress_enabled);
3662        assert_eq!(config.egress_pr_freq, 5.25);
3663        assert_eq!(config.new_time, 4.5);
3664        assert_eq!(config.burst_penalty, 5.5);
3665        assert_eq!(config.held_release_interval, 6.5);
3666    }
3667
3668    #[test]
3669    fn ingress_control_config_rejects_invalid_values() {
3670        let mut params = std::collections::HashMap::new();
3671        params.insert("ic_burst_hold".to_string(), "-1".to_string());
3672
3673        let err = super::parse_ingress_control_config("TCPServerInterface", &params).unwrap_err();
3674
3675        assert!(err.contains("ic_burst_hold"));
3676    }
3677
3678    #[test]
3679    fn discovery_stamp_default_is_16_but_explicit_override_is_preserved() {
3680        let mut params = std::collections::HashMap::new();
3681        params.insert("discoverable".to_string(), "yes".to_string());
3682        let defaults =
3683            super::extract_discovery_config("Discovery Test", "BackboneInterface", &params)
3684                .unwrap();
3685        assert_eq!(defaults.stamp_value, 16);
3686
3687        params.insert("discovery_stamp_value".to_string(), "14".to_string());
3688        let overridden =
3689            super::extract_discovery_config("Discovery Test", "BackboneInterface", &params)
3690                .unwrap();
3691        assert_eq!(overridden.stamp_value, 14);
3692    }
3693
3694    #[test]
3695    fn start_and_shutdown() {
3696        let node = RnsNode::start(
3697            NodeConfig {
3698                panic_on_interface_error: false,
3699                transport_enabled: false,
3700                static_transport_identity: false,
3701                local_hops_delta: false,
3702                identity: None,
3703                interfaces: vec![],
3704                share_instance: false,
3705                instance_name: "default".into(),
3706                shared_instance_port: 37428,
3707                rpc_port: 0,
3708                cache_dir: None,
3709                ratchet_store: None,
3710                ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
3711                management: Default::default(),
3712                probe_port: None,
3713                probe_addrs: vec![],
3714                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
3715                direct_connect_policy: Default::default(),
3716                device: None,
3717                underlay_mark: None,
3718                hooks: Vec::new(),
3719                discover_interfaces: false,
3720                autoconnect_interface_mode: None,
3721                autoconnect_interface_gravity: 0,
3722                autoconnect_announces_to_internal: false,
3723                discovery_required_value: None,
3724                respond_to_probes: false,
3725                prefer_shorter_path: false,
3726                max_paths_per_destination: 1,
3727                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
3728                packet_hashlist_allocation:
3729                    rns_core::transport::types::PacketHashlistAllocation::Eager,
3730                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
3731                max_path_destinations: usize::MAX,
3732                max_tunnel_destinations_total: usize::MAX,
3733                known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
3734                known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
3735                announce_table_ttl: Duration::from_secs(
3736                    rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
3737                ),
3738                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
3739                driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
3740                interface_writer_queue_capacity:
3741                    crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
3742                announce_rate_defaults: AnnounceRateDefaults::default(),
3743                ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(
3744                ),
3745                #[cfg(feature = "iface-backbone")]
3746                backbone_peer_pool: None,
3747                announce_sig_cache_enabled: true,
3748                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
3749                announce_sig_cache_ttl: Duration::from_secs(
3750                    rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
3751                ),
3752                registry: None,
3753                #[cfg(feature = "hooks")]
3754                provider_bridge: None,
3755            },
3756            Box::new(NoopCallbacks),
3757        )
3758        .unwrap();
3759        node.shutdown();
3760    }
3761
3762    #[test]
3763    fn known_destinations_persist_across_restart() {
3764        let dir = tempdir().unwrap();
3765        let dest_hash = [0x91; 16];
3766        let identity = Identity::new(&mut OsRng);
3767        let last_used_at = 77.0;
3768        let receiving_interface = rns_core::transport::types::InterfaceId(42);
3769
3770        let node = RnsNode::from_config(Some(dir.path()), Box::new(NoopCallbacks)).unwrap();
3771        let (response_tx, response_rx) = std::sync::mpsc::channel();
3772        node.event_sender()
3773            .send(crate::event::Event::Query(
3774                QueryRequest::RestoreKnownDestination(crate::event::KnownDestinationEntry {
3775                    dest_hash,
3776                    identity_hash: *identity.hash(),
3777                    public_key: identity.get_public_key().unwrap(),
3778                    app_data: Some(b"persisted".to_vec()),
3779                    hops: 2,
3780                    received_at: 55.0,
3781                    receiving_interface,
3782                    was_used: true,
3783                    last_used_at: Some(last_used_at),
3784                    retained: true,
3785                }),
3786                response_tx,
3787            ))
3788            .unwrap();
3789        assert!(matches!(
3790            response_rx.recv().unwrap(),
3791            QueryResponse::RestoreKnownDestination(true)
3792        ));
3793        node.shutdown();
3794
3795        let restarted = RnsNode::from_config(Some(dir.path()), Box::new(NoopCallbacks)).unwrap();
3796        let entries = restarted.known_destinations().unwrap();
3797        let entry = entries
3798            .iter()
3799            .find(|entry| entry.dest_hash == dest_hash)
3800            .expect("reloaded destination should appear in lifecycle listing");
3801        assert!(entry.retained);
3802        assert!(entry.was_used);
3803        assert_eq!(entry.hops, 2);
3804        assert_eq!(entry.receiving_interface, receiving_interface);
3805        assert_eq!(entry.last_used_at, Some(last_used_at));
3806
3807        let recalled = restarted
3808            .recall_identity(&rns_core::types::DestHash(dest_hash))
3809            .unwrap()
3810            .expect("known destination should reload from storage");
3811        assert_eq!(recalled.identity_hash.0, *identity.hash());
3812        assert_eq!(recalled.app_data, Some(b"persisted".to_vec()));
3813        restarted.shutdown();
3814    }
3815
3816    #[test]
3817    fn transport_paths_hashes_and_detached_tunnels_restore_from_upstream_files() {
3818        use rns_core::packet::{PacketFlags, RawPacket};
3819        use rns_core::transport::persistence::{
3820            PersistedPath, PersistedTunnel, TransportStateSnapshot,
3821        };
3822
3823        let dir = tempdir().unwrap();
3824        fs::write(
3825            dir.path().join("config"),
3826            r#"[reticulum]
3827enable_transport = True
3828share_instance = False
3829
3830[interfaces]
3831  [[Persistence Test]]
3832    type = UDPInterface
3833    forward_ip = 127.0.0.1
3834    forward_port = 9
3835    interface_mode = full
3836"#,
3837        )
3838        .unwrap();
3839        let paths = storage::ensure_storage_dirs(dir.path()).unwrap();
3840        let destination_hash = [0x31; 16];
3841        let packet = RawPacket::pack(
3842            PacketFlags {
3843                header_type: rns_core::constants::HEADER_1,
3844                context_flag: rns_core::constants::FLAG_UNSET,
3845                transport_type: rns_core::constants::TRANSPORT_BROADCAST,
3846                destination_type: rns_core::constants::DESTINATION_SINGLE,
3847                packet_type: rns_core::constants::PACKET_TYPE_ANNOUNCE,
3848            },
3849            0,
3850            &destination_hash,
3851            None,
3852            rns_core::constants::CONTEXT_NONE,
3853            &[0xA5; 32],
3854        )
3855        .unwrap();
3856        crate::announce_cache::AnnounceCache::new(paths.cache.join("announces"))
3857            .store(&packet.packet_hash, &packet.raw, None)
3858            .unwrap();
3859        let now = time::now();
3860        let route = PersistedPath {
3861            destination_hash,
3862            timestamp: now,
3863            received_from: [0x41; 16],
3864            hops: 2,
3865            expires: now + 600.0,
3866            random_blobs: vec![[0x51; 10]],
3867            interface_hash: Some(rns_core::hash::full_hash(b"Persistence Test")),
3868            packet_hash: packet.packet_hash,
3869        };
3870        storage::save_transport_state(
3871            &TransportStateSnapshot {
3872                packet_hashes: vec![[0x61; 32], [0x62; 32]],
3873                paths: vec![route.clone()],
3874                tunnels: vec![PersistedTunnel {
3875                    tunnel_id: [0x71; 32],
3876                    interface_hash: None,
3877                    paths: vec![PersistedPath {
3878                        interface_hash: None,
3879                        ..route
3880                    }],
3881                    expires: now + 1_200.0,
3882                }],
3883            },
3884            &paths.storage,
3885            false,
3886        )
3887        .unwrap();
3888
3889        let node = RnsNode::from_config(Some(dir.path()), Box::new(NoopCallbacks)).unwrap();
3890        let QueryResponse::TransportStateSnapshot(restored) =
3891            node.query(QueryRequest::TransportStateSnapshot).unwrap()
3892        else {
3893            panic!("unexpected transport snapshot response");
3894        };
3895        assert_eq!(restored.packet_hashes, vec![[0x61; 32], [0x62; 32]]);
3896        assert_eq!(restored.paths.len(), 1);
3897        assert_eq!(restored.paths[0].destination_hash, destination_hash);
3898        assert_eq!(restored.paths[0].received_from, [0x41; 16]);
3899        assert_eq!(restored.tunnels.len(), 1);
3900        assert_eq!(restored.tunnels[0].tunnel_id, [0x71; 32]);
3901        assert_eq!(restored.tunnels[0].interface_hash, None);
3902        node.shutdown();
3903    }
3904
3905    #[test]
3906    fn from_config_cleans_persistent_ratchets_after_loading_known_destinations() {
3907        fn hex(bytes: &[u8]) -> String {
3908            bytes.iter().map(|byte| format!("{:02x}", byte)).collect()
3909        }
3910
3911        let dir = tempdir().unwrap();
3912        fs::write(
3913            dir.path().join("config"),
3914            "[reticulum]\nenable_transport = False\nshare_instance = False\nratchet_expiry = 300\n",
3915        )
3916        .unwrap();
3917        let paths = storage::ensure_storage_dirs(dir.path()).unwrap();
3918        let known_live = [0x01; 16];
3919        let known_expired = [0x02; 16];
3920        let unknown = [0x03; 16];
3921        let known_corrupt = [0x04; 16];
3922        let now = time::now();
3923
3924        let identity = Identity::new(&mut OsRng);
3925        let known = std::collections::HashMap::from([
3926            (
3927                known_live,
3928                storage::KnownDestination {
3929                    identity_hash: *identity.hash(),
3930                    public_key: identity.get_public_key().unwrap(),
3931                    app_data: None,
3932                    hops: 1,
3933                    received_at: now,
3934                    receiving_interface: 0,
3935                    was_used: false,
3936                    last_used_at: None,
3937                    retained: false,
3938                },
3939            ),
3940            (
3941                known_expired,
3942                storage::KnownDestination {
3943                    identity_hash: *identity.hash(),
3944                    public_key: identity.get_public_key().unwrap(),
3945                    app_data: None,
3946                    hops: 1,
3947                    received_at: now,
3948                    receiving_interface: 0,
3949                    was_used: false,
3950                    last_used_at: None,
3951                    retained: false,
3952                },
3953            ),
3954            (
3955                known_corrupt,
3956                storage::KnownDestination {
3957                    identity_hash: *identity.hash(),
3958                    public_key: identity.get_public_key().unwrap(),
3959                    app_data: None,
3960                    hops: 1,
3961                    received_at: now,
3962                    receiving_interface: 0,
3963                    was_used: false,
3964                    last_used_at: None,
3965                    retained: false,
3966                },
3967            ),
3968        ]);
3969        storage::save_known_destinations(&known, &paths.storage.join("known_destinations"))
3970            .unwrap();
3971
3972        let store = storage::FsRatchetStore::new(paths.ratchets.clone());
3973        store
3974            .remember(
3975                known_live,
3976                storage::RatchetEntry {
3977                    ratchet: [0x11; 32],
3978                    received_at: now,
3979                },
3980            )
3981            .unwrap();
3982        store
3983            .remember(
3984                known_expired,
3985                storage::RatchetEntry {
3986                    ratchet: [0x22; 32],
3987                    received_at: now - 1000.0,
3988                },
3989            )
3990            .unwrap();
3991        store
3992            .remember(
3993                unknown,
3994                storage::RatchetEntry {
3995                    ratchet: [0x33; 32],
3996                    received_at: now,
3997                },
3998            )
3999            .unwrap();
4000        fs::write(paths.ratchets.join(hex(&known_corrupt)), b"not msgpack").unwrap();
4001        fs::write(paths.ratchets.join("0102.out"), b"temp").unwrap();
4002
4003        let node = RnsNode::from_config(Some(dir.path()), Box::new(NoopCallbacks)).unwrap();
4004
4005        assert!(paths.ratchets.join(hex(&known_live)).exists());
4006        assert!(!paths.ratchets.join(hex(&known_expired)).exists());
4007        assert!(!paths.ratchets.join(hex(&unknown)).exists());
4008        assert!(!paths.ratchets.join(hex(&known_corrupt)).exists());
4009        assert!(!paths.ratchets.join("0102.out").exists());
4010
4011        node.shutdown();
4012    }
4013
4014    #[test]
4015    fn start_with_identity() {
4016        let identity = Identity::new(&mut OsRng);
4017        let hash = *identity.hash();
4018        let node = RnsNode::start(
4019            NodeConfig {
4020                panic_on_interface_error: false,
4021                transport_enabled: true,
4022                static_transport_identity: false,
4023                local_hops_delta: false,
4024                identity: Some(identity),
4025                interfaces: vec![],
4026                share_instance: false,
4027                instance_name: "default".into(),
4028                shared_instance_port: 37428,
4029                rpc_port: 0,
4030                cache_dir: None,
4031                ratchet_store: None,
4032                ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
4033                management: Default::default(),
4034                probe_port: None,
4035                probe_addrs: vec![],
4036                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
4037                direct_connect_policy: Default::default(),
4038                device: None,
4039                underlay_mark: None,
4040                hooks: Vec::new(),
4041                discover_interfaces: false,
4042                autoconnect_interface_mode: None,
4043                autoconnect_interface_gravity: 0,
4044                autoconnect_announces_to_internal: false,
4045                discovery_required_value: None,
4046                respond_to_probes: false,
4047                prefer_shorter_path: false,
4048                max_paths_per_destination: 1,
4049                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
4050                packet_hashlist_allocation:
4051                    rns_core::transport::types::PacketHashlistAllocation::Eager,
4052                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
4053                max_path_destinations: usize::MAX,
4054                max_tunnel_destinations_total: usize::MAX,
4055                known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
4056                known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
4057                announce_table_ttl: Duration::from_secs(
4058                    rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
4059                ),
4060                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
4061                driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
4062                interface_writer_queue_capacity:
4063                    crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
4064                announce_rate_defaults: AnnounceRateDefaults::default(),
4065                ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(
4066                ),
4067                #[cfg(feature = "iface-backbone")]
4068                backbone_peer_pool: None,
4069                announce_sig_cache_enabled: true,
4070                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
4071                announce_sig_cache_ttl: Duration::from_secs(
4072                    rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
4073                ),
4074                registry: None,
4075                #[cfg(feature = "hooks")]
4076                provider_bridge: None,
4077            },
4078            Box::new(NoopCallbacks),
4079        )
4080        .unwrap();
4081        // The identity hash should have been used
4082        let _ = hash;
4083        node.shutdown();
4084    }
4085
4086    #[test]
4087    fn start_generates_identity() {
4088        let node = RnsNode::start(
4089            NodeConfig {
4090                panic_on_interface_error: false,
4091                transport_enabled: false,
4092                static_transport_identity: false,
4093                local_hops_delta: false,
4094                identity: None,
4095                interfaces: vec![],
4096                share_instance: false,
4097                instance_name: "default".into(),
4098                shared_instance_port: 37428,
4099                rpc_port: 0,
4100                cache_dir: None,
4101                ratchet_store: None,
4102                ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
4103                management: Default::default(),
4104                probe_port: None,
4105                probe_addrs: vec![],
4106                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
4107                direct_connect_policy: Default::default(),
4108                device: None,
4109                underlay_mark: None,
4110                hooks: Vec::new(),
4111                discover_interfaces: false,
4112                autoconnect_interface_mode: None,
4113                autoconnect_interface_gravity: 0,
4114                autoconnect_announces_to_internal: false,
4115                discovery_required_value: None,
4116                respond_to_probes: false,
4117                prefer_shorter_path: false,
4118                max_paths_per_destination: 1,
4119                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
4120                packet_hashlist_allocation:
4121                    rns_core::transport::types::PacketHashlistAllocation::Eager,
4122                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
4123                max_path_destinations: usize::MAX,
4124                max_tunnel_destinations_total: usize::MAX,
4125                known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
4126                known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
4127                announce_table_ttl: Duration::from_secs(
4128                    rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
4129                ),
4130                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
4131                driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
4132                interface_writer_queue_capacity:
4133                    crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
4134                announce_rate_defaults: AnnounceRateDefaults::default(),
4135                ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(
4136                ),
4137                #[cfg(feature = "iface-backbone")]
4138                backbone_peer_pool: None,
4139                announce_sig_cache_enabled: true,
4140                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
4141                announce_sig_cache_ttl: Duration::from_secs(
4142                    rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
4143                ),
4144                registry: None,
4145                #[cfg(feature = "hooks")]
4146                provider_bridge: None,
4147            },
4148            Box::new(NoopCallbacks),
4149        )
4150        .unwrap();
4151        // Should not panic - identity was auto-generated
4152        node.shutdown();
4153    }
4154
4155    #[test]
4156    fn from_config_creates_identity() {
4157        let dir = std::env::temp_dir().join(format!("rns-test-fc-{}", std::process::id()));
4158        let _ = fs::remove_dir_all(&dir);
4159        fs::create_dir_all(&dir).unwrap();
4160
4161        // Write a minimal config file
4162        fs::write(
4163            dir.join("config"),
4164            "[reticulum]\nenable_transport = False\n",
4165        )
4166        .unwrap();
4167
4168        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks)).unwrap();
4169
4170        // Identity file should have been created
4171        assert!(dir.join("storage/identities/identity").exists());
4172
4173        node.shutdown();
4174        let _ = fs::remove_dir_all(&dir);
4175    }
4176
4177    #[test]
4178    fn private_config_keeps_state_out_of_config_directory() {
4179        let root = tempfile::tempdir().unwrap();
4180        let config_dir = root.path().join("config");
4181        let state_dir = root.path().join("state");
4182        fs::create_dir_all(&config_dir).unwrap();
4183        fs::write(
4184            config_dir.join("config"),
4185            "[reticulum]\nenable_transport = False\nshare_instance = Yes\n",
4186        )
4187        .unwrap();
4188        let node =
4189            RnsNode::from_private_config(&config_dir, &state_dir, None, Box::new(NoopCallbacks))
4190                .unwrap();
4191        assert!(state_dir.join("storage/identities/identity").exists());
4192        assert!(!config_dir.join("storage").exists());
4193        node.shutdown();
4194    }
4195
4196    #[test]
4197    fn from_config_loads_identity() {
4198        let dir = std::env::temp_dir().join(format!("rns-test-fl-{}", std::process::id()));
4199        let _ = fs::remove_dir_all(&dir);
4200        fs::create_dir_all(dir.join("storage/identities")).unwrap();
4201
4202        // Pre-create an identity
4203        let identity = Identity::new(&mut OsRng);
4204        let hash = *identity.hash();
4205        storage::save_identity(&identity, &dir.join("storage/identities/identity")).unwrap();
4206
4207        fs::write(
4208            dir.join("config"),
4209            "[reticulum]\nenable_transport = False\n",
4210        )
4211        .unwrap();
4212
4213        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks)).unwrap();
4214
4215        // Verify the same identity was loaded (hash matches)
4216        let loaded = storage::load_identity(&dir.join("storage/identities/identity")).unwrap();
4217        assert_eq!(*loaded.hash(), hash);
4218
4219        node.shutdown();
4220        let _ = fs::remove_dir_all(&dir);
4221    }
4222
4223    #[test]
4224    fn from_config_tcp_server() {
4225        let dir = std::env::temp_dir().join(format!("rns-test-fts-{}", std::process::id()));
4226        let _ = fs::remove_dir_all(&dir);
4227        fs::create_dir_all(&dir).unwrap();
4228
4229        let port = crate::test_support::port();
4230
4231        let config = format!(
4232            r#"
4233[reticulum]
4234enable_transport = False
4235
4236[interfaces]
4237  [[Test TCP Server]]
4238    type = TCPServerInterface
4239    listen_ip = 127.0.0.1
4240    listen_port = {}
4241"#,
4242            port
4243        );
4244
4245        fs::write(dir.join("config"), config).unwrap();
4246
4247        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks)).unwrap();
4248
4249        // Give server time to start
4250        thread::sleep(Duration::from_millis(100));
4251
4252        // Should be able to connect
4253        let _client = std::net::TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
4254
4255        node.shutdown();
4256        let _ = fs::remove_dir_all(&dir);
4257    }
4258
4259    #[test]
4260    fn from_config_starts_rpc_when_share_instance_enabled() {
4261        let dir = std::env::temp_dir().join(format!("rns-test-rpc-{}", std::process::id()));
4262        let _ = fs::remove_dir_all(&dir);
4263        fs::create_dir_all(&dir).unwrap();
4264
4265        let rpc_port = crate::test_support::port();
4266
4267        let config = format!(
4268            r#"
4269[reticulum]
4270enable_transport = False
4271share_instance = Yes
4272instance_control_port = {}
4273
4274[interfaces]
4275"#,
4276            rpc_port
4277        );
4278
4279        fs::write(dir.join("config"), config).unwrap();
4280
4281        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks)).unwrap();
4282
4283        thread::sleep(Duration::from_millis(100));
4284
4285        let _client = std::net::TcpStream::connect(format!("127.0.0.1:{}", rpc_port)).unwrap();
4286
4287        node.shutdown();
4288        let _ = fs::remove_dir_all(&dir);
4289    }
4290
4291    #[test]
4292    fn from_config_starts_rpc_when_transport_enabled() {
4293        let dir =
4294            std::env::temp_dir().join(format!("rns-test-rpc-transport-{}", std::process::id()));
4295        let _ = fs::remove_dir_all(&dir);
4296        fs::create_dir_all(&dir).unwrap();
4297
4298        let rpc_port = crate::test_support::port();
4299
4300        let config = format!(
4301            r#"
4302[reticulum]
4303enable_transport = True
4304share_instance = Yes
4305instance_control_port = {}
4306
4307[interfaces]
4308"#,
4309            rpc_port
4310        );
4311
4312        fs::write(dir.join("config"), config).unwrap();
4313
4314        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks)).unwrap();
4315
4316        thread::sleep(Duration::from_millis(100));
4317
4318        let _client = std::net::TcpStream::connect(format!("127.0.0.1:{}", rpc_port)).unwrap();
4319
4320        node.shutdown();
4321        let _ = fs::remove_dir_all(&dir);
4322    }
4323
4324    #[test]
4325    fn from_config_starts_rpc_when_tcp_client_is_unreachable() {
4326        let dir =
4327            std::env::temp_dir().join(format!("rns-test-rpc-unreachable-{}", std::process::id()));
4328        let _ = fs::remove_dir_all(&dir);
4329        fs::create_dir_all(&dir).unwrap();
4330
4331        let rpc_port = crate::test_support::port();
4332        let unreachable_port = crate::test_support::port();
4333
4334        let config = format!(
4335            r#"
4336[reticulum]
4337enable_transport = True
4338share_instance = Yes
4339instance_control_port = {}
4340
4341[interfaces]
4342  [[Unreachable Upstream]]
4343    type = TCPClientInterface
4344    target_host = 127.0.0.1
4345    target_port = {}
4346"#,
4347            rpc_port, unreachable_port
4348        );
4349
4350        fs::write(dir.join("config"), config).unwrap();
4351
4352        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks)).unwrap();
4353
4354        thread::sleep(Duration::from_millis(100));
4355
4356        let _client = std::net::TcpStream::connect(format!("127.0.0.1:{}", rpc_port)).unwrap();
4357
4358        node.shutdown();
4359        let _ = fs::remove_dir_all(&dir);
4360    }
4361
4362    #[test]
4363    fn test_parse_interface_mode() {
4364        use rns_core::constants::*;
4365
4366        assert_eq!(parse_interface_mode("full"), MODE_FULL);
4367        assert_eq!(parse_interface_mode("Full"), MODE_FULL);
4368        assert_eq!(parse_interface_mode("access_point"), MODE_ACCESS_POINT);
4369        assert_eq!(parse_interface_mode("accesspoint"), MODE_ACCESS_POINT);
4370        assert_eq!(parse_interface_mode("ap"), MODE_ACCESS_POINT);
4371        assert_eq!(parse_interface_mode("AP"), MODE_ACCESS_POINT);
4372        assert_eq!(parse_interface_mode("pointtopoint"), MODE_POINT_TO_POINT);
4373        assert_eq!(parse_interface_mode("ptp"), MODE_POINT_TO_POINT);
4374        assert_eq!(parse_interface_mode("roaming"), MODE_ROAMING);
4375        assert_eq!(parse_interface_mode("boundary"), MODE_BOUNDARY);
4376        assert_eq!(parse_interface_mode("gateway"), MODE_GATEWAY);
4377        assert_eq!(parse_interface_mode("gw"), MODE_GATEWAY);
4378        assert_eq!(parse_interface_mode("internal"), MODE_INTERNAL);
4379        assert_eq!(parse_interface_mode("Internal"), MODE_INTERNAL);
4380        // Unknown defaults to FULL
4381        assert_eq!(parse_interface_mode("invalid"), MODE_FULL);
4382    }
4383
4384    #[test]
4385    fn discovery_mode_normalization_matrix() {
4386        use rns_core::constants::*;
4387
4388        let interface_types = [
4389            "RNodeInterface",
4390            "RNodeMultiInterface",
4391            "BackboneInterface",
4392            "TCPServerInterface",
4393            "AutoInterface",
4394        ];
4395        let modes = [
4396            MODE_FULL,
4397            MODE_ACCESS_POINT,
4398            MODE_POINT_TO_POINT,
4399            MODE_ROAMING,
4400            MODE_BOUNDARY,
4401            MODE_GATEWAY,
4402            MODE_INTERNAL,
4403        ];
4404
4405        for interface_type in interface_types {
4406            for configured_mode in modes {
4407                for discoverable in [false, true] {
4408                    for ignore_warnings in [false, true] {
4409                        let expected = if !discoverable
4410                            || ignore_warnings
4411                            || matches!(
4412                                configured_mode,
4413                                MODE_ACCESS_POINT | MODE_GATEWAY | MODE_INTERNAL
4414                            ) {
4415                            configured_mode
4416                        } else if matches!(interface_type, "RNodeInterface" | "RNodeMultiInterface")
4417                        {
4418                            MODE_ACCESS_POINT
4419                        } else {
4420                            MODE_GATEWAY
4421                        };
4422                        assert_eq!(
4423                            normalize_discovery_mode(
4424                                interface_type,
4425                                configured_mode,
4426                                discoverable,
4427                                ignore_warnings,
4428                            ),
4429                            expected,
4430                            "type={interface_type}, mode={configured_mode}, discoverable={discoverable}, ignore={ignore_warnings}",
4431                        );
4432                    }
4433                }
4434            }
4435        }
4436    }
4437
4438    #[test]
4439    fn to_node_config_serial() {
4440        // Verify from_config parses SerialInterface correctly.
4441        // The serial port won't exist, so start() will fail, but the config
4442        // parsing path is exercised. We verify via the error (not a config error).
4443        let dir = std::env::temp_dir().join(format!("rns-test-serial-{}", std::process::id()));
4444        let _ = fs::remove_dir_all(&dir);
4445        fs::create_dir_all(&dir).unwrap();
4446
4447        let config = r#"
4448[reticulum]
4449enable_transport = False
4450
4451[interfaces]
4452  [[Test Serial Port]]
4453    type = SerialInterface
4454    port = /dev/nonexistent_rns_test_serial
4455    speed = 115200
4456    databits = 8
4457    parity = E
4458    stopbits = 1
4459    interface_mode = ptp
4460    networkname = testnet
4461"#;
4462        fs::write(dir.join("config"), config).unwrap();
4463
4464        // Interface error is non-fatal: the node starts but logs the error.
4465        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks))
4466            .expect("Config should parse; interface failure is non-fatal");
4467        node.shutdown();
4468
4469        let _ = fs::remove_dir_all(&dir);
4470    }
4471
4472    #[test]
4473    fn to_node_config_kiss() {
4474        // Verify from_config parses KISSInterface correctly.
4475        let dir = std::env::temp_dir().join(format!("rns-test-kiss-{}", std::process::id()));
4476        let _ = fs::remove_dir_all(&dir);
4477        fs::create_dir_all(&dir).unwrap();
4478
4479        let config = r#"
4480[reticulum]
4481enable_transport = False
4482
4483[interfaces]
4484  [[Test KISS TNC]]
4485    type = KISSInterface
4486    port = /dev/nonexistent_rns_test_kiss
4487    speed = 9600
4488    preamble = 500
4489    txtail = 30
4490    persistence = 128
4491    slottime = 40
4492    flow_control = True
4493    id_interval = 600
4494    id_callsign = TEST0
4495    interface_mode = full
4496    passphrase = secretkey
4497"#;
4498        fs::write(dir.join("config"), config).unwrap();
4499
4500        // Interface error is non-fatal: the node starts but logs the error.
4501        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks))
4502            .expect("Config should parse; interface failure is non-fatal");
4503        node.shutdown();
4504
4505        let _ = fs::remove_dir_all(&dir);
4506    }
4507
4508    #[test]
4509    fn config_parser_accepts_recursive_prs_interface_option() {
4510        let config = r#"
4511[reticulum]
4512enable_transport = True
4513
4514[interfaces]
4515  [[Recursive Client]]
4516    type = TCPClientInterface
4517    target_host = 127.0.0.1
4518    target_port = 4242
4519    interface_mode = full
4520    recursive_prs = yes
4521
4522  [[Default Client]]
4523    type = TCPClientInterface
4524    target_host = 127.0.0.1
4525    target_port = 4243
4526    interface_mode = gateway
4527"#;
4528
4529        let parsed = config::parse(config).unwrap();
4530
4531        assert_eq!(parsed.interfaces.len(), 2);
4532        assert_eq!(parsed.interfaces[0].name, "Recursive Client");
4533        assert_eq!(parsed.interfaces[0].mode, "full");
4534        assert_eq!(
4535            parsed.interfaces[0]
4536                .params
4537                .get("recursive_prs")
4538                .map(String::as_str),
4539            Some("yes")
4540        );
4541        assert_eq!(parsed.interfaces[1].name, "Default Client");
4542        assert_eq!(parsed.interfaces[1].mode, "gateway");
4543        assert!(!parsed.interfaces[1].params.contains_key("recursive_prs"));
4544    }
4545
4546    #[test]
4547    fn config_parser_accepts_announces_from_internal_interface_option() {
4548        let config = r#"
4549[reticulum]
4550enable_transport = True
4551
4552[interfaces]
4553  [[Filtered Client]]
4554    type = TCPClientInterface
4555    target_host = 127.0.0.1
4556    target_port = 4242
4557    interface_mode = gateway
4558    announces_from_internal = no
4559
4560  [[Default Client]]
4561    type = TCPClientInterface
4562    target_host = 127.0.0.1
4563    target_port = 4243
4564    interface_mode = full
4565"#;
4566
4567        let parsed = config::parse(config).unwrap();
4568
4569        assert_eq!(parsed.interfaces.len(), 2);
4570        assert_eq!(parsed.interfaces[0].name, "Filtered Client");
4571        assert_eq!(parsed.interfaces[0].mode, "gateway");
4572        assert_eq!(
4573            parsed.interfaces[0]
4574                .params
4575                .get("announces_from_internal")
4576                .map(String::as_str),
4577            Some("no")
4578        );
4579        assert_eq!(parsed.interfaces[1].name, "Default Client");
4580        assert_eq!(parsed.interfaces[1].mode, "full");
4581        assert!(!parsed.interfaces[1]
4582            .params
4583            .contains_key("announces_from_internal"));
4584    }
4585
4586    #[test]
4587    fn config_parser_preserves_announces_to_internal_tristate() {
4588        let config = r#"
4589[interfaces]
4590  [[Allowed Boundary]]
4591    type = TCPClientInterface
4592    target_host = 127.0.0.1
4593    target_port = 4242
4594    announces_to_internal = yes
4595
4596  [[Blocked Boundary]]
4597    type = TCPClientInterface
4598    target_host = 127.0.0.1
4599    target_port = 4243
4600    announces_to_internal = no
4601
4602  [[Default Boundary]]
4603    type = TCPClientInterface
4604    target_host = 127.0.0.1
4605    target_port = 4244
4606"#;
4607
4608        let parsed = config::parse(config).unwrap();
4609        let parsed_value = |index: usize| {
4610            parsed.interfaces[index]
4611                .params
4612                .get("announces_to_internal")
4613                .and_then(|value| config::parse_bool_pub(value))
4614        };
4615
4616        assert_eq!(parsed_value(0), Some(true));
4617        assert_eq!(parsed_value(1), Some(false));
4618        assert_eq!(parsed_value(2), None);
4619    }
4620
4621    #[test]
4622    fn interface_gravity_parser_accepts_signed_values_and_uses_global_default() {
4623        assert_eq!(parse_interface_gravity(Some("7"), -2), 7);
4624        assert_eq!(parse_interface_gravity(Some("-4"), 2), -4);
4625        assert_eq!(parse_interface_gravity(None, -6), -6);
4626    }
4627
4628    #[test]
4629    fn config_parser_accepts_local_hops_delta_option() {
4630        let cfg = r#"
4631[reticulum]
4632local_hops_delta = yes
4633"#;
4634        let parsed = config::parse(cfg).unwrap();
4635        assert!(parsed.reticulum.local_hops_delta);
4636    }
4637
4638    #[test]
4639    fn config_parser_accepts_static_transport_identity_option() {
4640        let config = r#"
4641[reticulum]
4642enable_transport = false
4643static_transport_identity = yes
4644"#;
4645
4646        let parsed = config::parse(config).unwrap();
4647
4648        assert!(!parsed.reticulum.enable_transport);
4649        assert!(parsed.reticulum.static_transport_identity);
4650    }
4651
4652    #[test]
4653    fn non_transport_node_uses_ephemeral_transport_identity_by_default() {
4654        let identity = Identity::new(&mut rns_crypto::FixedRng::new(&[0x41; 32]));
4655        let stored_hash = *identity.hash();
4656        let node = RnsNode::start(
4657            NodeConfig {
4658                transport_enabled: false,
4659                static_transport_identity: false,
4660                local_hops_delta: false,
4661                identity: Some(identity),
4662                share_instance: false,
4663                ..Default::default()
4664            },
4665            Box::new(NoopCallbacks),
4666        )
4667        .unwrap();
4668
4669        let response = node.query(QueryRequest::TransportIdentity).unwrap();
4670        let QueryResponse::TransportIdentity(Some(hash)) = response else {
4671            panic!("transport identity query returned {response:?}");
4672        };
4673        assert_ne!(hash, stored_hash);
4674
4675        node.shutdown();
4676    }
4677
4678    #[test]
4679    fn non_transport_node_can_use_static_transport_identity() {
4680        let identity = Identity::new(&mut rns_crypto::FixedRng::new(&[0x42; 32]));
4681        let stored_hash = *identity.hash();
4682        let node = RnsNode::start(
4683            NodeConfig {
4684                transport_enabled: false,
4685                static_transport_identity: true,
4686                local_hops_delta: false,
4687                identity: Some(identity),
4688                share_instance: false,
4689                ..Default::default()
4690            },
4691            Box::new(NoopCallbacks),
4692        )
4693        .unwrap();
4694
4695        let response = node.query(QueryRequest::TransportIdentity).unwrap();
4696        let QueryResponse::TransportIdentity(Some(hash)) = response else {
4697            panic!("transport identity query returned {response:?}");
4698        };
4699        assert_eq!(hash, stored_hash);
4700
4701        node.shutdown();
4702    }
4703
4704    #[test]
4705    fn test_extract_ifac_config() {
4706        use std::collections::HashMap;
4707
4708        // No IFAC params → None
4709        let params: HashMap<String, String> = HashMap::new();
4710        assert!(extract_ifac_config(&params, 16).is_none());
4711
4712        // networkname only
4713        let mut params = HashMap::new();
4714        params.insert("networkname".into(), "testnet".into());
4715        let ifac = extract_ifac_config(&params, 16).unwrap();
4716        assert_eq!(ifac.netname.as_deref(), Some("testnet"));
4717        assert!(ifac.netkey.is_none());
4718        assert_eq!(ifac.size, 16);
4719
4720        // passphrase only with custom size (in bits)
4721        let mut params = HashMap::new();
4722        params.insert("passphrase".into(), "secret".into());
4723        params.insert("ifac_size".into(), "64".into()); // 64 bits = 8 bytes
4724        let ifac = extract_ifac_config(&params, 16).unwrap();
4725        assert!(ifac.netname.is_none());
4726        assert_eq!(ifac.netkey.as_deref(), Some("secret"));
4727        assert_eq!(ifac.size, 8);
4728
4729        // Both with alternate key names
4730        let mut params = HashMap::new();
4731        params.insert("network_name".into(), "mynet".into());
4732        params.insert("pass_phrase".into(), "mykey".into());
4733        let ifac = extract_ifac_config(&params, 8).unwrap();
4734        assert_eq!(ifac.netname.as_deref(), Some("mynet"));
4735        assert_eq!(ifac.netkey.as_deref(), Some("mykey"));
4736        assert_eq!(ifac.size, 8);
4737    }
4738
4739    #[test]
4740    fn to_node_config_rnode() {
4741        // Verify from_config parses RNodeInterface correctly.
4742        // The serial port won't exist, so start() will fail at open time.
4743        let dir = std::env::temp_dir().join(format!("rns-test-rnode-{}", std::process::id()));
4744        let _ = fs::remove_dir_all(&dir);
4745        fs::create_dir_all(&dir).unwrap();
4746
4747        let config = r#"
4748[reticulum]
4749enable_transport = False
4750
4751[interfaces]
4752  [[Test RNode]]
4753    type = RNodeInterface
4754    port = /dev/nonexistent_rns_test_rnode
4755    frequency = 867200000
4756    bandwidth = 125000
4757    txpower = 7
4758    spreadingfactor = 8
4759    codingrate = 5
4760    flow_control = True
4761    st_alock = 5.0
4762    lt_alock = 2.5
4763    interface_mode = full
4764    networkname = testnet
4765"#;
4766        fs::write(dir.join("config"), config).unwrap();
4767
4768        // Interface error is non-fatal: the node starts but logs the error.
4769        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks))
4770            .expect("Config should parse; interface failure is non-fatal");
4771        node.shutdown();
4772
4773        let _ = fs::remove_dir_all(&dir);
4774    }
4775
4776    #[test]
4777    fn to_node_config_pipe() {
4778        // Verify from_config parses PipeInterface correctly.
4779        // Use `cat` as a real command so it actually starts.
4780        let dir = std::env::temp_dir().join(format!("rns-test-pipe-{}", std::process::id()));
4781        let _ = fs::remove_dir_all(&dir);
4782        fs::create_dir_all(&dir).unwrap();
4783
4784        let config = r#"
4785[reticulum]
4786enable_transport = False
4787
4788[interfaces]
4789  [[Test Pipe]]
4790    type = PipeInterface
4791    command = cat
4792    respawn_delay = 5000
4793    interface_mode = full
4794"#;
4795        fs::write(dir.join("config"), config).unwrap();
4796
4797        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks)).unwrap();
4798        // If we got here, config parsing and start() succeeded
4799        node.shutdown();
4800
4801        let _ = fs::remove_dir_all(&dir);
4802    }
4803
4804    #[test]
4805    fn to_node_config_backbone() {
4806        // Verify from_config parses BackboneInterface correctly.
4807        let dir = std::env::temp_dir().join(format!("rns-test-backbone-{}", std::process::id()));
4808        let _ = fs::remove_dir_all(&dir);
4809        fs::create_dir_all(&dir).unwrap();
4810
4811        let port = crate::test_support::port();
4812
4813        let config = format!(
4814            r#"
4815[reticulum]
4816enable_transport = False
4817
4818[interfaces]
4819  [[Test Backbone]]
4820    type = BackboneInterface
4821    listen_ip = 127.0.0.1
4822    listen_port = {}
4823    interface_mode = full
4824"#,
4825            port
4826        );
4827
4828        fs::write(dir.join("config"), config).unwrap();
4829
4830        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks)).unwrap();
4831
4832        // Give server time to start
4833        thread::sleep(Duration::from_millis(100));
4834
4835        // Should be able to connect
4836        {
4837            let _client = std::net::TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
4838            // client drops here, closing the connection cleanly
4839        }
4840
4841        // Small delay to let epoll process the disconnect
4842        thread::sleep(Duration::from_millis(50));
4843
4844        node.shutdown();
4845        let _ = fs::remove_dir_all(&dir);
4846    }
4847
4848    #[test]
4849    fn rnode_config_defaults() {
4850        use crate::interface::rnode::{RNodeConfig, RNodeSubConfig};
4851
4852        let config = RNodeConfig::default();
4853        assert_eq!(config.speed, 115200);
4854        assert!(config.subinterfaces.is_empty());
4855        assert!(config.id_interval.is_none());
4856        assert!(config.id_callsign.is_none());
4857
4858        let sub = RNodeSubConfig {
4859            name: "test".into(),
4860            vport: 0,
4861            outgoing: true,
4862            frequency: 868_000_000,
4863            bandwidth: 125_000,
4864            txpower: 7,
4865            spreading_factor: 8,
4866            coding_rate: 5,
4867            flow_control: false,
4868            st_alock: None,
4869            lt_alock: None,
4870        };
4871        assert_eq!(sub.frequency, 868_000_000);
4872        assert_eq!(sub.bandwidth, 125_000);
4873        assert!(!sub.flow_control);
4874    }
4875
4876    // =========================================================================
4877    // Phase 9c: Announce + Discovery node-level tests
4878    // =========================================================================
4879
4880    #[test]
4881    fn announce_builds_valid_packet() {
4882        let identity = Identity::new(&mut OsRng);
4883        let identity_hash = rns_core::types::IdentityHash(*identity.hash());
4884
4885        let node = RnsNode::start(
4886            NodeConfig {
4887                panic_on_interface_error: false,
4888                transport_enabled: false,
4889                static_transport_identity: false,
4890                local_hops_delta: false,
4891                identity: None,
4892                interfaces: vec![],
4893                share_instance: false,
4894                instance_name: "default".into(),
4895                shared_instance_port: 37428,
4896                rpc_port: 0,
4897                cache_dir: None,
4898                ratchet_store: None,
4899                ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
4900                management: Default::default(),
4901                probe_port: None,
4902                probe_addrs: vec![],
4903                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
4904                direct_connect_policy: Default::default(),
4905                device: None,
4906                underlay_mark: None,
4907                hooks: Vec::new(),
4908                discover_interfaces: false,
4909                autoconnect_interface_mode: None,
4910                autoconnect_interface_gravity: 0,
4911                autoconnect_announces_to_internal: false,
4912                discovery_required_value: None,
4913                respond_to_probes: false,
4914                prefer_shorter_path: false,
4915                max_paths_per_destination: 1,
4916                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
4917                packet_hashlist_allocation:
4918                    rns_core::transport::types::PacketHashlistAllocation::Eager,
4919                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
4920                max_path_destinations: usize::MAX,
4921                max_tunnel_destinations_total: usize::MAX,
4922                known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
4923                known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
4924                announce_table_ttl: Duration::from_secs(
4925                    rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
4926                ),
4927                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
4928                driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
4929                interface_writer_queue_capacity:
4930                    crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
4931                announce_rate_defaults: AnnounceRateDefaults::default(),
4932                ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(
4933                ),
4934                #[cfg(feature = "iface-backbone")]
4935                backbone_peer_pool: None,
4936                announce_sig_cache_enabled: true,
4937                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
4938                announce_sig_cache_ttl: Duration::from_secs(
4939                    rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
4940                ),
4941                registry: None,
4942                #[cfg(feature = "hooks")]
4943                provider_bridge: None,
4944            },
4945            Box::new(NoopCallbacks),
4946        )
4947        .unwrap();
4948
4949        let dest = crate::destination::Destination::single_in("test", &["echo"], identity_hash);
4950
4951        // Register destination first
4952        node.register_destination(dest.hash.0, dest.dest_type.to_wire_constant())
4953            .unwrap();
4954
4955        // Announce should succeed (though no interfaces to send on)
4956        let result = node.announce(&dest, &identity, Some(b"hello"));
4957        assert!(result.is_ok());
4958
4959        node.shutdown();
4960    }
4961
4962    #[test]
4963    fn has_path_and_hops_to() {
4964        let node = RnsNode::start(
4965            NodeConfig {
4966                panic_on_interface_error: false,
4967                transport_enabled: false,
4968                static_transport_identity: false,
4969                local_hops_delta: false,
4970                identity: None,
4971                interfaces: vec![],
4972                share_instance: false,
4973                instance_name: "default".into(),
4974                shared_instance_port: 37428,
4975                rpc_port: 0,
4976                cache_dir: None,
4977                ratchet_store: None,
4978                ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
4979                management: Default::default(),
4980                probe_port: None,
4981                probe_addrs: vec![],
4982                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
4983                direct_connect_policy: Default::default(),
4984                device: None,
4985                underlay_mark: None,
4986                hooks: Vec::new(),
4987                discover_interfaces: false,
4988                autoconnect_interface_mode: None,
4989                autoconnect_interface_gravity: 0,
4990                autoconnect_announces_to_internal: false,
4991                discovery_required_value: None,
4992                respond_to_probes: false,
4993                prefer_shorter_path: false,
4994                max_paths_per_destination: 1,
4995                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
4996                packet_hashlist_allocation:
4997                    rns_core::transport::types::PacketHashlistAllocation::Eager,
4998                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
4999                max_path_destinations: usize::MAX,
5000                max_tunnel_destinations_total: usize::MAX,
5001                known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
5002                known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
5003                announce_table_ttl: Duration::from_secs(
5004                    rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
5005                ),
5006                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
5007                driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
5008                interface_writer_queue_capacity:
5009                    crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
5010                announce_rate_defaults: AnnounceRateDefaults::default(),
5011                ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(
5012                ),
5013                #[cfg(feature = "iface-backbone")]
5014                backbone_peer_pool: None,
5015                announce_sig_cache_enabled: true,
5016                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
5017                announce_sig_cache_ttl: Duration::from_secs(
5018                    rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
5019                ),
5020                registry: None,
5021                #[cfg(feature = "hooks")]
5022                provider_bridge: None,
5023            },
5024            Box::new(NoopCallbacks),
5025        )
5026        .unwrap();
5027
5028        let dh = rns_core::types::DestHash([0xAA; 16]);
5029
5030        // No path should exist
5031        assert!(!node.has_path(&dh).unwrap());
5032        assert_eq!(node.hops_to(&dh).unwrap(), None);
5033
5034        node.shutdown();
5035    }
5036
5037    #[test]
5038    fn recall_identity_none_when_unknown() {
5039        let node = RnsNode::start(
5040            NodeConfig {
5041                panic_on_interface_error: false,
5042                transport_enabled: false,
5043                static_transport_identity: false,
5044                local_hops_delta: false,
5045                identity: None,
5046                interfaces: vec![],
5047                share_instance: false,
5048                instance_name: "default".into(),
5049                shared_instance_port: 37428,
5050                rpc_port: 0,
5051                cache_dir: None,
5052                ratchet_store: None,
5053                ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
5054                management: Default::default(),
5055                probe_port: None,
5056                probe_addrs: vec![],
5057                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
5058                direct_connect_policy: Default::default(),
5059                device: None,
5060                underlay_mark: None,
5061                hooks: Vec::new(),
5062                discover_interfaces: false,
5063                autoconnect_interface_mode: None,
5064                autoconnect_interface_gravity: 0,
5065                autoconnect_announces_to_internal: false,
5066                discovery_required_value: None,
5067                respond_to_probes: false,
5068                prefer_shorter_path: false,
5069                max_paths_per_destination: 1,
5070                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
5071                packet_hashlist_allocation:
5072                    rns_core::transport::types::PacketHashlistAllocation::Eager,
5073                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
5074                max_path_destinations: usize::MAX,
5075                max_tunnel_destinations_total: usize::MAX,
5076                known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
5077                known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
5078                announce_table_ttl: Duration::from_secs(
5079                    rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
5080                ),
5081                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
5082                driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
5083                interface_writer_queue_capacity:
5084                    crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
5085                announce_rate_defaults: AnnounceRateDefaults::default(),
5086                ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(
5087                ),
5088                #[cfg(feature = "iface-backbone")]
5089                backbone_peer_pool: None,
5090                announce_sig_cache_enabled: true,
5091                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
5092                announce_sig_cache_ttl: Duration::from_secs(
5093                    rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
5094                ),
5095                registry: None,
5096                #[cfg(feature = "hooks")]
5097                provider_bridge: None,
5098            },
5099            Box::new(NoopCallbacks),
5100        )
5101        .unwrap();
5102
5103        let dh = rns_core::types::DestHash([0xBB; 16]);
5104        assert!(node.recall_identity(&dh).unwrap().is_none());
5105
5106        node.shutdown();
5107    }
5108
5109    #[test]
5110    fn request_path_does_not_crash() {
5111        let node = RnsNode::start(
5112            NodeConfig {
5113                panic_on_interface_error: false,
5114                transport_enabled: false,
5115                static_transport_identity: false,
5116                local_hops_delta: false,
5117                identity: None,
5118                interfaces: vec![],
5119                share_instance: false,
5120                instance_name: "default".into(),
5121                shared_instance_port: 37428,
5122                rpc_port: 0,
5123                cache_dir: None,
5124                ratchet_store: None,
5125                ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
5126                management: Default::default(),
5127                probe_port: None,
5128                probe_addrs: vec![],
5129                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
5130                direct_connect_policy: Default::default(),
5131                device: None,
5132                underlay_mark: None,
5133                hooks: Vec::new(),
5134                discover_interfaces: false,
5135                autoconnect_interface_mode: None,
5136                autoconnect_interface_gravity: 0,
5137                autoconnect_announces_to_internal: false,
5138                discovery_required_value: None,
5139                respond_to_probes: false,
5140                prefer_shorter_path: false,
5141                max_paths_per_destination: 1,
5142                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
5143                packet_hashlist_allocation:
5144                    rns_core::transport::types::PacketHashlistAllocation::Eager,
5145                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
5146                max_path_destinations: usize::MAX,
5147                max_tunnel_destinations_total: usize::MAX,
5148                known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
5149                known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
5150                announce_table_ttl: Duration::from_secs(
5151                    rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
5152                ),
5153                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
5154                driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
5155                interface_writer_queue_capacity:
5156                    crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
5157                announce_rate_defaults: AnnounceRateDefaults::default(),
5158                ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(
5159                ),
5160                #[cfg(feature = "iface-backbone")]
5161                backbone_peer_pool: None,
5162                announce_sig_cache_enabled: true,
5163                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
5164                announce_sig_cache_ttl: Duration::from_secs(
5165                    rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
5166                ),
5167                registry: None,
5168                #[cfg(feature = "hooks")]
5169                provider_bridge: None,
5170            },
5171            Box::new(NoopCallbacks),
5172        )
5173        .unwrap();
5174
5175        let dh = rns_core::types::DestHash([0xCC; 16]);
5176        assert!(node.request_path(&dh).is_ok());
5177
5178        // Small wait for the event to be processed
5179        thread::sleep(Duration::from_millis(50));
5180
5181        node.shutdown();
5182    }
5183
5184    #[test]
5185    fn create_link_returns_error_while_draining() {
5186        let node = RnsNode::start(NodeConfig::default(), Box::new(NoopCallbacks)).unwrap();
5187
5188        node.begin_drain(Duration::from_secs(1)).unwrap();
5189        assert!(node.create_link([0xAB; 16], [0xCD; 32]).is_err());
5190
5191        node.shutdown();
5192    }
5193
5194    #[test]
5195    fn request_path_returns_error_while_draining() {
5196        let node = RnsNode::start(NodeConfig::default(), Box::new(NoopCallbacks)).unwrap();
5197
5198        node.begin_drain(Duration::from_secs(1)).unwrap();
5199        assert!(node
5200            .request_path(&rns_core::types::DestHash([0xAB; 16]))
5201            .is_err());
5202
5203        node.shutdown();
5204    }
5205
5206    // =========================================================================
5207    // Phase 9d: send_packet + register_destination_with_proof tests
5208    // =========================================================================
5209
5210    #[test]
5211    fn send_packet_returns_error_while_draining() {
5212        let node = RnsNode::start(NodeConfig::default(), Box::new(NoopCallbacks)).unwrap();
5213        let dest = crate::destination::Destination::plain("drain-test", &["send"]);
5214
5215        node.begin_drain(Duration::from_secs(1)).unwrap();
5216        assert!(node.send_packet(&dest, b"hello").is_err());
5217
5218        node.shutdown();
5219    }
5220
5221    #[test]
5222    fn send_packet_plain() {
5223        let node = RnsNode::start(
5224            NodeConfig {
5225                panic_on_interface_error: false,
5226                transport_enabled: false,
5227                static_transport_identity: false,
5228                local_hops_delta: false,
5229                identity: None,
5230                interfaces: vec![],
5231                share_instance: false,
5232                instance_name: "default".into(),
5233                shared_instance_port: 37428,
5234                rpc_port: 0,
5235                cache_dir: None,
5236                ratchet_store: None,
5237                ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
5238                management: Default::default(),
5239                probe_port: None,
5240                probe_addrs: vec![],
5241                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
5242                direct_connect_policy: Default::default(),
5243                device: None,
5244                underlay_mark: None,
5245                hooks: Vec::new(),
5246                discover_interfaces: false,
5247                autoconnect_interface_mode: None,
5248                autoconnect_interface_gravity: 0,
5249                autoconnect_announces_to_internal: false,
5250                discovery_required_value: None,
5251                respond_to_probes: false,
5252                prefer_shorter_path: false,
5253                max_paths_per_destination: 1,
5254                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
5255                packet_hashlist_allocation:
5256                    rns_core::transport::types::PacketHashlistAllocation::Eager,
5257                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
5258                max_path_destinations: usize::MAX,
5259                max_tunnel_destinations_total: usize::MAX,
5260                known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
5261                known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
5262                announce_table_ttl: Duration::from_secs(
5263                    rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
5264                ),
5265                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
5266                driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
5267                interface_writer_queue_capacity:
5268                    crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
5269                announce_rate_defaults: AnnounceRateDefaults::default(),
5270                ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(
5271                ),
5272                #[cfg(feature = "iface-backbone")]
5273                backbone_peer_pool: None,
5274                announce_sig_cache_enabled: true,
5275                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
5276                announce_sig_cache_ttl: Duration::from_secs(
5277                    rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
5278                ),
5279                registry: None,
5280                #[cfg(feature = "hooks")]
5281                provider_bridge: None,
5282            },
5283            Box::new(NoopCallbacks),
5284        )
5285        .unwrap();
5286
5287        let dest = crate::destination::Destination::plain("test", &["echo"]);
5288        let result = node.send_packet(&dest, b"hello world");
5289        assert!(result.is_ok());
5290
5291        let packet_hash = result.unwrap();
5292        // Packet hash should be non-zero
5293        assert_ne!(packet_hash.0, [0u8; 32]);
5294
5295        // Small wait for the event to be processed
5296        thread::sleep(Duration::from_millis(50));
5297
5298        node.shutdown();
5299    }
5300
5301    #[test]
5302    fn send_packet_single_requires_public_key() {
5303        let node = RnsNode::start(
5304            NodeConfig {
5305                panic_on_interface_error: false,
5306                transport_enabled: false,
5307                static_transport_identity: false,
5308                local_hops_delta: false,
5309                identity: None,
5310                interfaces: vec![],
5311                share_instance: false,
5312                instance_name: "default".into(),
5313                shared_instance_port: 37428,
5314                rpc_port: 0,
5315                cache_dir: None,
5316                ratchet_store: None,
5317                ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
5318                management: Default::default(),
5319                probe_port: None,
5320                probe_addrs: vec![],
5321                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
5322                direct_connect_policy: Default::default(),
5323                device: None,
5324                underlay_mark: None,
5325                hooks: Vec::new(),
5326                discover_interfaces: false,
5327                autoconnect_interface_mode: None,
5328                autoconnect_interface_gravity: 0,
5329                autoconnect_announces_to_internal: false,
5330                discovery_required_value: None,
5331                respond_to_probes: false,
5332                prefer_shorter_path: false,
5333                max_paths_per_destination: 1,
5334                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
5335                packet_hashlist_allocation:
5336                    rns_core::transport::types::PacketHashlistAllocation::Eager,
5337                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
5338                max_path_destinations: usize::MAX,
5339                max_tunnel_destinations_total: usize::MAX,
5340                known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
5341                known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
5342                announce_table_ttl: Duration::from_secs(
5343                    rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
5344                ),
5345                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
5346                driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
5347                interface_writer_queue_capacity:
5348                    crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
5349                announce_rate_defaults: AnnounceRateDefaults::default(),
5350                ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(
5351                ),
5352                #[cfg(feature = "iface-backbone")]
5353                backbone_peer_pool: None,
5354                announce_sig_cache_enabled: true,
5355                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
5356                announce_sig_cache_ttl: Duration::from_secs(
5357                    rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
5358                ),
5359                registry: None,
5360                #[cfg(feature = "hooks")]
5361                provider_bridge: None,
5362            },
5363            Box::new(NoopCallbacks),
5364        )
5365        .unwrap();
5366
5367        // single_in has no public_key — sending should fail
5368        let dest = crate::destination::Destination::single_in(
5369            "test",
5370            &["echo"],
5371            rns_core::types::IdentityHash([0x42; 16]),
5372        );
5373        let result = node.send_packet(&dest, b"hello");
5374        assert!(result.is_err(), "single_in has no public_key, should fail");
5375
5376        node.shutdown();
5377    }
5378
5379    #[test]
5380    fn send_packet_single_encrypts() {
5381        let node = RnsNode::start(
5382            NodeConfig {
5383                panic_on_interface_error: false,
5384                transport_enabled: false,
5385                static_transport_identity: false,
5386                local_hops_delta: false,
5387                identity: None,
5388                interfaces: vec![],
5389                share_instance: false,
5390                instance_name: "default".into(),
5391                shared_instance_port: 37428,
5392                rpc_port: 0,
5393                cache_dir: None,
5394                ratchet_store: None,
5395                ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
5396                management: Default::default(),
5397                probe_port: None,
5398                probe_addrs: vec![],
5399                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
5400                direct_connect_policy: Default::default(),
5401                device: None,
5402                underlay_mark: None,
5403                hooks: Vec::new(),
5404                discover_interfaces: false,
5405                autoconnect_interface_mode: None,
5406                autoconnect_interface_gravity: 0,
5407                autoconnect_announces_to_internal: false,
5408                discovery_required_value: None,
5409                respond_to_probes: false,
5410                prefer_shorter_path: false,
5411                max_paths_per_destination: 1,
5412                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
5413                packet_hashlist_allocation:
5414                    rns_core::transport::types::PacketHashlistAllocation::Eager,
5415                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
5416                max_path_destinations: usize::MAX,
5417                max_tunnel_destinations_total: usize::MAX,
5418                known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
5419                known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
5420                announce_table_ttl: Duration::from_secs(
5421                    rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
5422                ),
5423                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
5424                driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
5425                interface_writer_queue_capacity:
5426                    crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
5427                announce_rate_defaults: AnnounceRateDefaults::default(),
5428                ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(
5429                ),
5430                #[cfg(feature = "iface-backbone")]
5431                backbone_peer_pool: None,
5432                announce_sig_cache_enabled: true,
5433                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
5434                announce_sig_cache_ttl: Duration::from_secs(
5435                    rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
5436                ),
5437                registry: None,
5438                #[cfg(feature = "hooks")]
5439                provider_bridge: None,
5440            },
5441            Box::new(NoopCallbacks),
5442        )
5443        .unwrap();
5444
5445        // Create a proper OUT SINGLE destination with a real identity's public key
5446        let remote_identity = Identity::new(&mut OsRng);
5447        let recalled = crate::destination::AnnouncedIdentity {
5448            dest_hash: rns_core::types::DestHash([0xAA; 16]),
5449            identity_hash: rns_core::types::IdentityHash(*remote_identity.hash()),
5450            public_key: remote_identity.get_public_key().unwrap(),
5451            app_data: None,
5452            hops: 1,
5453            received_at: 0.0,
5454            receiving_interface: rns_core::transport::types::InterfaceId(0),
5455            rssi: Some(-100),
5456            snr: Some(10.5),
5457        };
5458        let dest = crate::destination::Destination::single_out("test", &["echo"], &recalled);
5459
5460        let result = node.send_packet(&dest, b"secret message");
5461        assert!(result.is_ok());
5462
5463        let packet_hash = result.unwrap();
5464        assert_ne!(packet_hash.0, [0u8; 32]);
5465
5466        thread::sleep(Duration::from_millis(50));
5467        node.shutdown();
5468    }
5469
5470    #[test]
5471    fn register_destination_with_proof_prove_all() {
5472        let node = RnsNode::start(
5473            NodeConfig {
5474                panic_on_interface_error: false,
5475                transport_enabled: false,
5476                static_transport_identity: false,
5477                local_hops_delta: false,
5478                identity: None,
5479                interfaces: vec![],
5480                share_instance: false,
5481                instance_name: "default".into(),
5482                shared_instance_port: 37428,
5483                rpc_port: 0,
5484                cache_dir: None,
5485                ratchet_store: None,
5486                ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
5487                management: Default::default(),
5488                probe_port: None,
5489                probe_addrs: vec![],
5490                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
5491                direct_connect_policy: Default::default(),
5492                device: None,
5493                underlay_mark: None,
5494                hooks: Vec::new(),
5495                discover_interfaces: false,
5496                autoconnect_interface_mode: None,
5497                autoconnect_interface_gravity: 0,
5498                autoconnect_announces_to_internal: false,
5499                discovery_required_value: None,
5500                respond_to_probes: false,
5501                prefer_shorter_path: false,
5502                max_paths_per_destination: 1,
5503                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
5504                packet_hashlist_allocation:
5505                    rns_core::transport::types::PacketHashlistAllocation::Eager,
5506                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
5507                max_path_destinations: usize::MAX,
5508                max_tunnel_destinations_total: usize::MAX,
5509                known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
5510                known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
5511                announce_table_ttl: Duration::from_secs(
5512                    rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
5513                ),
5514                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
5515                driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
5516                interface_writer_queue_capacity:
5517                    crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
5518                announce_rate_defaults: AnnounceRateDefaults::default(),
5519                ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(
5520                ),
5521                #[cfg(feature = "iface-backbone")]
5522                backbone_peer_pool: None,
5523                announce_sig_cache_enabled: true,
5524                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
5525                announce_sig_cache_ttl: Duration::from_secs(
5526                    rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
5527                ),
5528                registry: None,
5529                #[cfg(feature = "hooks")]
5530                provider_bridge: None,
5531            },
5532            Box::new(NoopCallbacks),
5533        )
5534        .unwrap();
5535
5536        let identity = Identity::new(&mut OsRng);
5537        let ih = rns_core::types::IdentityHash(*identity.hash());
5538        let dest = crate::destination::Destination::single_in("echo", &["request"], ih)
5539            .set_proof_strategy(rns_core::types::ProofStrategy::ProveAll);
5540        let prv_key = identity.get_private_key().unwrap();
5541
5542        let result = node.register_destination_with_proof(&dest, Some(prv_key));
5543        assert!(result.is_ok());
5544
5545        // Small wait for the events to be processed
5546        thread::sleep(Duration::from_millis(50));
5547
5548        node.shutdown();
5549    }
5550
5551    #[test]
5552    fn register_destination_with_proof_prove_none() {
5553        let node = RnsNode::start(
5554            NodeConfig {
5555                panic_on_interface_error: false,
5556                transport_enabled: false,
5557                static_transport_identity: false,
5558                local_hops_delta: false,
5559                identity: None,
5560                interfaces: vec![],
5561                share_instance: false,
5562                instance_name: "default".into(),
5563                shared_instance_port: 37428,
5564                rpc_port: 0,
5565                cache_dir: None,
5566                ratchet_store: None,
5567                ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
5568                management: Default::default(),
5569                probe_port: None,
5570                probe_addrs: vec![],
5571                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
5572                direct_connect_policy: Default::default(),
5573                device: None,
5574                underlay_mark: None,
5575                hooks: Vec::new(),
5576                discover_interfaces: false,
5577                autoconnect_interface_mode: None,
5578                autoconnect_interface_gravity: 0,
5579                autoconnect_announces_to_internal: false,
5580                discovery_required_value: None,
5581                respond_to_probes: false,
5582                prefer_shorter_path: false,
5583                max_paths_per_destination: 1,
5584                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
5585                packet_hashlist_allocation:
5586                    rns_core::transport::types::PacketHashlistAllocation::Eager,
5587                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
5588                max_path_destinations: usize::MAX,
5589                max_tunnel_destinations_total: usize::MAX,
5590                known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
5591                known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
5592                announce_table_ttl: Duration::from_secs(
5593                    rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
5594                ),
5595                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
5596                driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
5597                interface_writer_queue_capacity:
5598                    crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
5599                announce_rate_defaults: AnnounceRateDefaults::default(),
5600                ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(
5601                ),
5602                #[cfg(feature = "iface-backbone")]
5603                backbone_peer_pool: None,
5604                announce_sig_cache_enabled: true,
5605                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
5606                announce_sig_cache_ttl: Duration::from_secs(
5607                    rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
5608                ),
5609                registry: None,
5610                #[cfg(feature = "hooks")]
5611                provider_bridge: None,
5612            },
5613            Box::new(NoopCallbacks),
5614        )
5615        .unwrap();
5616
5617        // ProveNone should not send RegisterProofStrategy event
5618        let dest = crate::destination::Destination::plain("test", &["data"])
5619            .set_proof_strategy(rns_core::types::ProofStrategy::ProveNone);
5620
5621        let result = node.register_destination_with_proof(&dest, None);
5622        assert!(result.is_ok());
5623
5624        thread::sleep(Duration::from_millis(50));
5625        node.shutdown();
5626    }
5627}