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