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