Skip to main content

rns_net/
node.rs

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