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