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