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