Skip to main content

rns_net/
node.rs

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