Skip to main content

rns_net/
node.rs

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