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::{HashMap, HashSet};
6use std::io::{self, Read};
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
51static NEXT_RESOURCE_TRANSFER_ID: AtomicU64 = AtomicU64::new(1);
52
53#[cfg(test)]
54const DEFAULT_KNOWN_DESTINATIONS_TTL: Duration = Duration::from_secs(48 * 60 * 60);
55const DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES: usize = 8192;
56
57/// Parse an interface mode string to the corresponding constant.
58/// Matches Python's `_synthesize_interface()` in `RNS/Reticulum.py`.
59fn parse_interface_mode(mode: &str) -> u8 {
60    match mode.to_lowercase().as_str() {
61        "full" => rns_core::constants::MODE_FULL,
62        "access_point" | "accesspoint" | "ap" => rns_core::constants::MODE_ACCESS_POINT,
63        "pointtopoint" | "ptp" => rns_core::constants::MODE_POINT_TO_POINT,
64        "roaming" => rns_core::constants::MODE_ROAMING,
65        "boundary" => rns_core::constants::MODE_BOUNDARY,
66        "gateway" | "gw" => rns_core::constants::MODE_GATEWAY,
67        "internal" => rns_core::constants::MODE_INTERNAL,
68        _ => rns_core::constants::MODE_FULL,
69    }
70}
71
72fn parse_optional_interface_mode(mode: &str) -> Option<u8> {
73    match mode.to_lowercase().as_str() {
74        "full" => Some(rns_core::constants::MODE_FULL),
75        "access_point" | "accesspoint" | "ap" => Some(rns_core::constants::MODE_ACCESS_POINT),
76        "pointtopoint" | "ptp" => Some(rns_core::constants::MODE_POINT_TO_POINT),
77        "roaming" => Some(rns_core::constants::MODE_ROAMING),
78        "boundary" => Some(rns_core::constants::MODE_BOUNDARY),
79        "gateway" | "gw" => Some(rns_core::constants::MODE_GATEWAY),
80        "internal" => Some(rns_core::constants::MODE_INTERNAL),
81        _ => None,
82    }
83}
84
85fn parse_interface_gravity(value: Option<&str>, default_gravity: i64) -> i64 {
86    value
87        .and_then(|value| value.parse().ok())
88        .unwrap_or(default_gravity)
89}
90
91/// Apply Reticulum's mode normalization for discoverable interfaces.
92fn normalize_discovery_mode(
93    interface_type: &str,
94    configured_mode: u8,
95    discoverable: bool,
96    ignore_config_warnings: bool,
97) -> u8 {
98    if !discoverable
99        || ignore_config_warnings
100        || matches!(
101            configured_mode,
102            rns_core::constants::MODE_ACCESS_POINT
103                | rns_core::constants::MODE_GATEWAY
104                | rns_core::constants::MODE_INTERNAL
105        )
106    {
107        return configured_mode;
108    }
109
110    if matches!(interface_type, "RNodeInterface" | "RNodeMultiInterface") {
111        rns_core::constants::MODE_ACCESS_POINT
112    } else {
113        rns_core::constants::MODE_GATEWAY
114    }
115}
116
117fn default_ingress_control_for_type(
118    iface_type: &str,
119) -> rns_core::transport::types::IngressControlConfig {
120    match iface_type {
121        "AutoInterface" | "BackboneInterface" | "TCPClientInterface" | "TCPServerInterface"
122        | "UDPInterface" | "I2PInterface" => {
123            rns_core::transport::types::IngressControlConfig::enabled()
124        }
125        _ => rns_core::transport::types::IngressControlConfig::disabled(),
126    }
127}
128
129fn parse_ingress_control_config(
130    iface_type: &str,
131    params: &std::collections::HashMap<String, String>,
132) -> Result<rns_core::transport::types::IngressControlConfig, String> {
133    let mut config = default_ingress_control_for_type(iface_type);
134
135    if let Some(v) = params.get("ingress_control") {
136        config.enabled = config::parse_bool_pub(v)
137            .ok_or_else(|| format!("ingress_control must be a boolean, got '{}'", v))?;
138    }
139    if let Some(v) = params.get("ic_max_held_announces") {
140        config.max_held_announces = v
141            .parse::<usize>()
142            .map_err(|_| format!("ic_max_held_announces must be an integer, got '{}'", v))?;
143    }
144    if let Some(v) = params.get("ic_burst_hold") {
145        config.burst_hold = parse_nonnegative_f64("ic_burst_hold", v)?;
146    }
147    if let Some(v) = params.get("ic_burst_freq_new") {
148        config.burst_freq_new = parse_nonnegative_f64("ic_burst_freq_new", v)?;
149    }
150    if let Some(v) = params.get("ic_burst_freq") {
151        config.burst_freq = parse_nonnegative_f64("ic_burst_freq", v)?;
152    }
153    if let Some(v) = params.get("ic_pr_burst_freq_new") {
154        config.pr_burst_freq_new = parse_nonnegative_f64("ic_pr_burst_freq_new", v)?;
155    }
156    if let Some(v) = params.get("ic_pr_burst_freq") {
157        config.pr_burst_freq = parse_nonnegative_f64("ic_pr_burst_freq", v)?;
158    }
159    if let Some(v) = params.get("egress_control") {
160        config.egress_enabled = config::parse_bool_pub(v)
161            .ok_or_else(|| format!("egress_control must be a boolean, got '{}'", v))?;
162    }
163    if let Some(v) = params.get("ec_pr_freq") {
164        config.egress_pr_freq = parse_nonnegative_f64("ec_pr_freq", v)?;
165    }
166    if let Some(v) = params.get("ic_new_time") {
167        config.new_time = parse_nonnegative_f64("ic_new_time", v)?;
168    }
169    if let Some(v) = params.get("ic_burst_penalty") {
170        config.burst_penalty = parse_nonnegative_f64("ic_burst_penalty", v)?;
171    }
172    if let Some(v) = params.get("ic_held_release_interval") {
173        config.held_release_interval = parse_nonnegative_f64("ic_held_release_interval", v)?;
174    }
175
176    Ok(config)
177}
178
179fn parse_nonnegative_f64(key: &str, value: &str) -> Result<f64, String> {
180    let parsed = value
181        .parse::<f64>()
182        .map_err(|_| format!("{} must be numeric, got '{}'", key, value))?;
183    if parsed < 0.0 {
184        return Err(format!("{} must be >= 0, got '{}'", key, value));
185    }
186    Ok(parsed)
187}
188
189/// Reject empty and the nonsensical literal `"None"` IFAC config values.
190///
191/// Upstream treats `"None"` as an unset value and logs a warning, because a
192/// quoted TOML/JSON `None` is commonly mistaken for a real network name or
193/// passphrase. Empty strings are also ignored.
194fn sanitize_ifac_config_string(kind: &str, value: &str) -> Option<String> {
195    if value == "None" {
196        log::warn!(
197            "Ambiguous IFAC {} \"None\", this value is ignored and an IFAC {} has NOT been set",
198            kind,
199            kind
200        );
201        return None;
202    }
203    if value.is_empty() {
204        return None;
205    }
206    Some(value.to_string())
207}
208
209/// Extract IFAC configuration from interface params, if present.
210/// Returns None if neither networkname/network_name nor passphrase/pass_phrase is set.
211fn extract_ifac_config(
212    params: &std::collections::HashMap<String, String>,
213    default_size: usize,
214) -> Option<IfacConfig> {
215    let netname = params
216        .get("networkname")
217        .or_else(|| params.get("network_name"))
218        .cloned()
219        .and_then(|value| sanitize_ifac_config_string("network name", &value));
220    let netkey = params
221        .get("passphrase")
222        .or_else(|| params.get("pass_phrase"))
223        .cloned()
224        .and_then(|value| sanitize_ifac_config_string("passphrase", &value));
225
226    if netname.is_none() && netkey.is_none() {
227        return None;
228    }
229
230    // ifac_size is specified in bits in config, divide by 8 for bytes
231    let size = params
232        .get("ifac_size")
233        .and_then(|v| v.parse::<usize>().ok())
234        .map(|bits| (bits / 8).max(1))
235        .unwrap_or(default_size);
236
237    Some(IfacConfig {
238        netname,
239        netkey,
240        size,
241    })
242}
243
244/// Upstream enables the static transport identity when a discoverable interface
245/// is configured on a non-transport instance, so the announced transport
246/// identity is stable across restarts instead of ephemeral.
247fn effective_static_transport_identity(
248    configured: bool,
249    transport_enabled: bool,
250    has_discoverable_interface: bool,
251) -> bool {
252    configured || (!transport_enabled && has_discoverable_interface)
253}
254
255/// Extract discovery configuration from interface params, if `discoverable` is set.
256fn extract_discovery_config(
257    iface_name: &str,
258    iface_type: &str,
259    params: &std::collections::HashMap<String, String>,
260) -> Option<crate::discovery::DiscoveryConfig> {
261    let discoverable = params
262        .get("discoverable")
263        .and_then(|v| config::parse_bool_pub(v))
264        .unwrap_or(false);
265    if !discoverable {
266        return None;
267    }
268
269    if iface_type == "TCPClientInterface" {
270        log::error!(
271            "Invalid interface discovery configuration for {}, aborting discovery announce",
272            iface_name
273        );
274        return None;
275    }
276
277    let discovery_name = params
278        .get("discovery_name")
279        .cloned()
280        .unwrap_or_else(|| iface_name.to_string());
281
282    // Config value is in seconds. Min 300s (5min), default 21600s (6h).
283    let announce_interval = params
284        .get("announce_interval")
285        .and_then(|v| v.parse::<u64>().ok())
286        .map(|secs| secs.max(300))
287        .unwrap_or(21600);
288
289    // `as_float` since upstream 796ef2d4.
290    let stamp_value = params
291        .get("discovery_stamp_value")
292        .and_then(|v| config::parse_config_float(v))
293        .map(|v| rns_core::stamp::clamp_cost(v, rns_core::constants::STAMP_COST_CAP))
294        .unwrap_or(crate::discovery::DEFAULT_STAMP_VALUE);
295
296    let reachable_on = params.get("reachable_on").cloned();
297
298    let listen_port = params
299        .get("listen_port")
300        .or_else(|| params.get("port"))
301        .and_then(|v| v.parse().ok());
302
303    let location_cmd = params.get("location_cmd").cloned();
304
305    let latitude = params
306        .get("latitude")
307        .or_else(|| params.get("lat"))
308        .and_then(|v| v.parse().ok());
309    let longitude = params
310        .get("longitude")
311        .or_else(|| params.get("lon"))
312        .and_then(|v| v.parse().ok());
313    let height = params.get("height").and_then(|v| v.parse().ok());
314    let operator_lxmf_address = params.get("discovery_lxmf_address").and_then(|value| {
315        let parsed = parse_discovery_lxmf_address(value);
316        if parsed.is_none() {
317            log::error!(
318                "Invalid interface discovery LXMF address for '{}': {}",
319                iface_name,
320                value
321            );
322        }
323        parsed
324    });
325
326    Some(crate::discovery::DiscoveryConfig {
327        discovery_name,
328        announce_interval,
329        stamp_value,
330        reachable_on,
331        interface_type: iface_type.to_string(),
332        listen_port,
333        location_cmd,
334        latitude,
335        longitude,
336        height,
337        operator_lxmf_address,
338        // Invalid values were already rejected with the interface itself.
339        stamp_costs: parse_interface_stamp_costs(params).unwrap_or_default(),
340    })
341}
342
343fn default_discovery_runtime_config(
344    interface_name: &str,
345    interface_type: &str,
346    listen_port: Option<u16>,
347) -> crate::discovery::DiscoveryConfig {
348    crate::discovery::DiscoveryConfig {
349        stamp_costs: Default::default(),
350        discovery_name: interface_name.to_string(),
351        announce_interval: 21600,
352        stamp_value: crate::discovery::DEFAULT_STAMP_VALUE,
353        reachable_on: None,
354        interface_type: interface_type.to_string(),
355        listen_port,
356        location_cmd: None,
357        latitude: None,
358        longitude: None,
359        height: None,
360        operator_lxmf_address: None,
361    }
362}
363
364fn parse_discovery_lxmf_address(value: &str) -> Option<[u8; 16]> {
365    if value.len() != 32 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
366        return None;
367    }
368    let mut address = [0u8; 16];
369    for (index, byte) in address.iter_mut().enumerate() {
370        *byte = u8::from_str_radix(&value[index * 2..index * 2 + 2], 16).ok()?;
371    }
372    Some(address)
373}
374
375fn discovery_runtime_ifac_fields(ifac: Option<&IfacConfig>) -> (Option<String>, Option<String>) {
376    (
377        ifac.and_then(|cfg| cfg.netname.clone()),
378        ifac.and_then(|cfg| cfg.netkey.clone()),
379    )
380}
381
382#[cfg(feature = "iface-backbone")]
383pub(crate) fn backbone_discovery_runtime_from_interface(
384    interface_name: &str,
385    mode: &BackboneMode,
386    discovery: Option<&crate::discovery::DiscoveryConfig>,
387    transport_enabled: bool,
388    ifac: Option<&IfacConfig>,
389) -> Option<crate::driver::BackboneDiscoveryRuntimeHandle> {
390    let config = match mode {
391        BackboneMode::Server(config) => config,
392        BackboneMode::Client(_, _) => return None,
393    };
394
395    let startup_config = discovery.cloned().unwrap_or_else(|| {
396        default_discovery_runtime_config(
397            interface_name,
398            "BackboneInterface",
399            Some(config.listen_port),
400        )
401    });
402    let (ifac_netname, ifac_netkey) = discovery_runtime_ifac_fields(ifac);
403
404    Some(crate::driver::BackboneDiscoveryRuntimeHandle::from_parts(
405        config.name.clone(),
406        startup_config,
407        transport_enabled,
408        ifac_netname,
409        ifac_netkey,
410        discovery.is_some(),
411    ))
412}
413
414#[cfg(feature = "iface-tcp")]
415pub(crate) fn tcp_server_discovery_runtime_from_interface(
416    interface_name: &str,
417    config: &crate::interface::tcp_server::TcpServerConfig,
418    discovery: Option<&crate::discovery::DiscoveryConfig>,
419    transport_enabled: bool,
420    ifac: Option<&IfacConfig>,
421) -> crate::driver::TcpServerDiscoveryRuntimeHandle {
422    let startup_config = discovery.cloned().unwrap_or_else(|| {
423        default_discovery_runtime_config(
424            interface_name,
425            "TCPServerInterface",
426            Some(config.listen_port),
427        )
428    });
429    let (ifac_netname, ifac_netkey) = discovery_runtime_ifac_fields(ifac);
430
431    crate::driver::TcpServerDiscoveryRuntimeHandle::from_parts(
432        config.name.clone(),
433        startup_config,
434        transport_enabled,
435        ifac_netname,
436        ifac_netkey,
437        discovery.is_some(),
438    )
439}
440
441pub(crate) fn ifac_runtime_from_config(
442    ifac: Option<&IfacConfig>,
443    default_size: usize,
444) -> crate::driver::IfacRuntimeConfig {
445    crate::driver::IfacRuntimeConfig::from_parts(
446        ifac.and_then(|cfg| cfg.netname.clone()),
447        ifac.and_then(|cfg| cfg.netkey.clone()),
448        ifac.map(|cfg| cfg.size).unwrap_or(default_size),
449    )
450}
451
452pub(crate) fn discoverable_interface_from_config(
453    interface_name: &str,
454    discovery: &crate::discovery::DiscoveryConfig,
455    transport_enabled: bool,
456    ifac: Option<&IfacConfig>,
457) -> crate::discovery::DiscoverableInterface {
458    crate::discovery::DiscoverableInterface {
459        interface_name: interface_name.to_string(),
460        config: discovery.clone(),
461        transport_enabled,
462        ifac_netname: ifac.and_then(|cfg| cfg.netname.clone()),
463        ifac_netkey: ifac.and_then(|cfg| cfg.netkey.clone()),
464    }
465}
466
467pub(crate) fn derive_ifac_state(
468    ifac: Option<&IfacConfig>,
469    interface_name: &str,
470) -> io::Result<Option<crate::ifac::IfacState>> {
471    let Some(ifac) = ifac else {
472        return Ok(None);
473    };
474    if ifac.netname.is_none() && ifac.netkey.is_none() {
475        return Ok(None);
476    }
477
478    ifac::derive_ifac(ifac.netname.as_deref(), ifac.netkey.as_deref(), ifac.size)
479        .map(Some)
480        .map_err(|err| {
481            io::Error::other(format!(
482                "failed to derive IFAC for {}: {}",
483                interface_name, err
484            ))
485        })
486}
487
488pub(crate) struct StartedInterface<'a> {
489    pub(crate) driver: &'a mut Driver,
490    pub(crate) tx: &'a EventSender,
491    pub(crate) queue_capacity: usize,
492    pub(crate) id: rns_core::transport::types::InterfaceId,
493    pub(crate) info: rns_core::transport::types::InterfaceInfo,
494    pub(crate) writer: Box<dyn crate::interface::Writer>,
495    pub(crate) interface_type_name: String,
496    pub(crate) ifac_state: Option<crate::ifac::IfacState>,
497    pub(crate) ifac_runtime: &'a crate::driver::IfacRuntimeConfig,
498    pub(crate) stamp_costs: rns_core::transport::types::InterfaceStampCosts,
499}
500
501pub(crate) fn register_started_interface(params: StartedInterface<'_>) {
502    let StartedInterface {
503        driver,
504        tx,
505        queue_capacity,
506        id,
507        mut info,
508        writer,
509        interface_type_name,
510        ifac_state,
511        ifac_runtime,
512        stamp_costs,
513    } = params;
514    info.stamp_costs = stamp_costs;
515    driver.apply_announce_rate_defaults(&mut info);
516    driver.apply_ingress_control_defaults(&mut info);
517    let (writer, async_writer_metrics) =
518        crate::interface::wrap_async_writer(writer, id, &info.name, tx.clone(), queue_capacity);
519    driver.register_interface_runtime_defaults(&info);
520    driver.register_interface_ifac_runtime(&info.name, ifac_runtime.clone());
521    driver.engine.register_interface(info.clone());
522    driver.interfaces.insert(
523        id,
524        InterfaceEntry {
525            id,
526            info,
527            writer,
528            async_writer_metrics: Some(async_writer_metrics),
529            enabled: true,
530            online: false,
531            dynamic: false,
532            ifac: ifac_state,
533            stats: InterfaceStats {
534                started: time::now(),
535                ..Default::default()
536            },
537            interface_type: interface_type_name,
538            send_retry_at: None,
539            send_retry_backoff: Duration::ZERO,
540        },
541    );
542}
543
544/// Sources and startup defaults needed to reread a named interface from disk.
545pub(crate) struct InterfaceManagementConfig {
546    pub(crate) config_file: std::path::PathBuf,
547    pub(crate) storage_dir: std::path::PathBuf,
548    pub(crate) initial_ids: HashMap<String, rns_core::transport::types::InterfaceId>,
549    pub(crate) queue_capacity: usize,
550    pub(crate) transport_enabled: bool,
551    pub(crate) underlay_mark: Option<u32>,
552}
553
554/// Top-level node configuration.
555pub struct NodeConfig {
556    pub transport_enabled: bool,
557    pub static_transport_identity: bool,
558    pub local_hops_delta: bool,
559    pub identity: Option<Identity>,
560    /// Interface configurations (parsed via registry factories).
561    pub interfaces: Vec<InterfaceConfig>,
562    /// Enable shared instance server for local clients (rns-ctl, etc.)
563    pub share_instance: bool,
564    /// Instance name for Unix socket namespace (default: "default").
565    pub instance_name: String,
566    /// Shared instance port for local client connections (default 37428).
567    pub shared_instance_port: u16,
568    /// RPC control port (default 37429). Only used when share_instance is true.
569    pub rpc_port: u16,
570    /// Cache directory for announce cache. If None, announce caching is disabled.
571    pub cache_dir: Option<std::path::PathBuf>,
572    /// Store for received ratchets. If None, received ratchets are not persisted or used.
573    pub ratchet_store: Option<Arc<dyn storage::RatchetStore>>,
574    /// TTL for received ratchets.
575    pub ratchet_expiry: Duration,
576    /// Remote management configuration.
577    pub management: crate::management::ManagementConfig,
578    /// Port to run the STUN probe server on (for facilitator nodes).
579    pub probe_port: Option<u16>,
580    /// Addresses of STUN/RNSP probe servers (tried sequentially with failover).
581    pub probe_addrs: Vec<std::net::SocketAddr>,
582    /// Protocol for endpoint discovery: "rnsp" (default) or "stun".
583    pub probe_protocol: rns_core::holepunch::ProbeProtocol,
584    /// Policy for incoming direct-connect proposals.
585    pub direct_connect_policy: crate::event::HolePunchPolicy,
586    /// Accept direct-connect endpoints in private, shared, link-local,
587    /// and unique-local ranges (LAN-only deployments).
588    pub direct_connect_allow_private_endpoints: bool,
589    /// Network interface to bind outbound sockets to (e.g. "usb0").
590    pub device: Option<String>,
591    /// Linux `SO_MARK` value applied to Reticulum IP underlay sockets.
592    pub underlay_mark: Option<u32>,
593    /// Hook configurations loaded from the config file.
594    pub hooks: Vec<config::ParsedHook>,
595    /// Enable interface discovery.
596    pub discover_interfaces: bool,
597    /// Mode assigned to auto-connected discovered interfaces, when configured.
598    pub autoconnect_interface_mode: Option<u8>,
599    /// Gravity assigned to auto-connected discovered interfaces.
600    pub autoconnect_interface_gravity: i64,
601    /// Allow auto-connected interfaces to propagate announces to internal interfaces.
602    pub autoconnect_announces_to_internal: bool,
603    /// Allow auto-connecting discovered interfaces from unverified implementations.
604    pub autoconnect_unverified_implementations: bool,
605    /// Minimum stamp value for accepting discovered interfaces (default: 16).
606    pub discovery_required_value: Option<rns_core::stamp::StampCost>,
607    /// Respond to probe packets with automatic proof (like Python's respond_to_probes).
608    pub respond_to_probes: bool,
609    /// Accept an announce with strictly fewer hops even when the random_blob
610    /// is a duplicate of the existing path entry.  Default `false` preserves
611    /// Python-compatible anti-replay behaviour.
612    pub prefer_shorter_path: bool,
613    /// Maximum number of alternative paths stored per destination.
614    /// Default 1 (single path, backward-compatible).
615    pub max_paths_per_destination: usize,
616    /// Maximum number of packet hashes retained for duplicate suppression.
617    pub packet_hashlist_max_entries: usize,
618    /// Startup allocation policy for the packet hashlist payload pages.
619    pub packet_hashlist_allocation: rns_core::transport::types::PacketHashlistAllocation,
620    /// Maximum number of discovery path-request tags remembered.
621    pub max_discovery_pr_tags: usize,
622    /// Maximum number of destination hashes retained in the live path table.
623    pub max_path_destinations: usize,
624    /// Maximum number of retained tunnel-known destinations.
625    pub max_tunnel_destinations_total: usize,
626    /// TTL for recalled known destinations without an active path.
627    pub known_destinations_ttl: Duration,
628    /// Maximum number of recalled known destinations retained.
629    pub known_destinations_max_entries: usize,
630    /// TTL for announce retransmission state.
631    pub announce_table_ttl: Duration,
632    /// Maximum retained bytes for announce retransmission state.
633    pub announce_table_max_bytes: usize,
634    /// Maximum queued events awaiting driver processing.
635    pub driver_event_queue_capacity: usize,
636    /// Maximum queued outbound frames per interface writer worker.
637    pub interface_writer_queue_capacity: usize,
638    /// Default announce-rate controls applied to interfaces when transport is enabled.
639    pub announce_rate_defaults: AnnounceRateDefaults,
640    /// Default ingress/egress controls applied to interfaces when transport is enabled.
641    pub ingress_control_defaults: rns_core::transport::types::IngressControlConfig,
642    /// Outbound Backbone peer-pool settings. Disabled when `None`.
643    #[cfg(feature = "iface-backbone")]
644    pub backbone_peer_pool: Option<BackbonePeerPoolSettings>,
645    /// Whether the announce signature verification cache is enabled.
646    pub announce_sig_cache_enabled: bool,
647    /// Maximum entries in the announce signature verification cache.
648    pub announce_sig_cache_max_entries: usize,
649    /// TTL for announce signature cache entries.
650    pub announce_sig_cache_ttl: Duration,
651    /// Custom interface registry. If `None`, uses `InterfaceRegistry::with_builtins()`.
652    pub registry: Option<crate::interface::registry::InterfaceRegistry>,
653    /// If true, a single interface failing to start will abort the entire node.
654    /// If false (default), the error is logged and remaining interfaces continue.
655    pub panic_on_interface_error: bool,
656    /// Threads that verify and sign link packet proofs off the driver thread
657    /// (`Auto`: half the CPUs the process may use, from one to four).
658    pub link_proof_verify_threads: crate::ProofVerifyThreads,
659    /// Stamp generation, enforcement, costs and timeouts: the `[stamps]`
660    /// section, which [`RnsNode::from_config`] reads from the config file.
661    /// Upstream's `Reticulum(enable_stamping=True)` corresponds to setting
662    /// `stamp_destinations`, `stamp_prs` and `stamp_lrs`, and
663    /// `enforce_stamps=True` to `stamps_enforced` and `lr_stamps_enforced`.
664    pub stamps: config::StampsSection,
665    /// External provider bridge for hook-emitted events.
666    #[cfg(feature = "hooks")]
667    pub provider_bridge: Option<crate::provider_bridge::ProviderBridgeConfig>,
668}
669
670struct NodeQueueConfig {
671    rpc_auth_key: Option<[u8; 32]>,
672    announce_max_entries: usize,
673    announce_max_interfaces: usize,
674    announce_max_bytes: usize,
675    announce_ttl_secs: f64,
676    announce_overflow_policy: AnnounceQueueOverflowPolicy,
677    inbound_capacities: crate::event::InboundQueueCapacities,
678    link_mtu_discovery: bool,
679    /// The `[filter]` config section.
680    filter: config::FilterSection,
681    /// `[reticulum] hop_limiter`.
682    hop_limiter: u8,
683}
684
685impl Default for NodeQueueConfig {
686    fn default() -> Self {
687        Self {
688            rpc_auth_key: None,
689            announce_max_entries: 256,
690            announce_max_interfaces: 1024,
691            announce_max_bytes: 256 * 1024,
692            announce_ttl_secs: 30.0,
693            announce_overflow_policy: AnnounceQueueOverflowPolicy::DropWorst,
694            inbound_capacities: crate::event::InboundQueueCapacities::default(),
695            link_mtu_discovery: true,
696            filter: config::FilterSection::default(),
697            hop_limiter: rns_core::constants::HOP_LIMITER,
698        }
699    }
700}
701
702impl Default for NodeConfig {
703    fn default() -> Self {
704        Self {
705            transport_enabled: false,
706            static_transport_identity: false,
707            local_hops_delta: false,
708            identity: None,
709            interfaces: Vec::new(),
710            share_instance: false,
711            instance_name: "default".into(),
712            shared_instance_port: 37428,
713            rpc_port: 0,
714            cache_dir: None,
715            ratchet_store: None,
716            ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
717            management: Default::default(),
718            probe_port: None,
719            probe_addrs: vec![],
720            probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
721            direct_connect_policy: crate::event::HolePunchPolicy::IdentifiedOnly,
722            direct_connect_allow_private_endpoints: false,
723            device: None,
724            underlay_mark: None,
725            hooks: Vec::new(),
726            discover_interfaces: false,
727            autoconnect_interface_mode: None,
728            autoconnect_interface_gravity: 0,
729            autoconnect_announces_to_internal: false,
730            autoconnect_unverified_implementations: false,
731            discovery_required_value: None,
732            respond_to_probes: false,
733            prefer_shorter_path: false,
734            max_paths_per_destination: 1,
735            packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
736            packet_hashlist_allocation: rns_core::transport::types::PacketHashlistAllocation::Lazy,
737            max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
738            max_path_destinations: rns_core::transport::types::DEFAULT_MAX_PATH_DESTINATIONS,
739            max_tunnel_destinations_total: usize::MAX,
740            known_destinations_ttl: Duration::from_secs(48 * 60 * 60),
741            known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
742            announce_table_ttl: Duration::from_secs(rns_core::constants::ANNOUNCE_TABLE_TTL as u64),
743            announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
744            driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
745            interface_writer_queue_capacity: crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
746            announce_rate_defaults: AnnounceRateDefaults::default(),
747            ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(),
748            #[cfg(feature = "iface-backbone")]
749            backbone_peer_pool: None,
750            announce_sig_cache_enabled: true,
751            announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
752            announce_sig_cache_ttl: Duration::from_secs(
753                rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
754            ),
755            registry: None,
756            panic_on_interface_error: false,
757            link_proof_verify_threads: Default::default(),
758            stamps: config::StampsSection::default(),
759            #[cfg(feature = "hooks")]
760            provider_bridge: None,
761        }
762    }
763}
764
765/// IFAC configuration for an interface.
766pub struct IfacConfig {
767    pub netname: Option<String>,
768    pub netkey: Option<String>,
769    pub size: usize,
770}
771
772/// Interface configuration, parsed via an [`InterfaceFactory`] from the registry.
773pub struct InterfaceConfig {
774    pub name: String,
775    pub type_name: String,
776    pub config_data: Box<dyn crate::interface::InterfaceConfigData>,
777    pub mode: u8,
778    pub gravity: i64,
779    pub recursive_prs: bool,
780    pub announces_from_internal: bool,
781    pub announces_to_internal: Option<bool>,
782    pub ingress_control: rns_core::transport::types::IngressControlConfig,
783    /// Per-interface `destination_cost` and `pr_cost` (upstream 9b464382).
784    pub stamp_costs: rns_core::transport::types::InterfaceStampCosts,
785    pub ifac: Option<IfacConfig>,
786    pub discovery: Option<crate::discovery::DiscoveryConfig>,
787}
788
789/// Parse the per-interface `destination_cost` and `pr_cost` options
790/// (upstream 9b464382): `as_float`, clamped to `0..=STAMP_COST_CAP`.
791pub(crate) fn parse_interface_stamp_costs(
792    params: &HashMap<String, String>,
793) -> Result<rns_core::transport::types::InterfaceStampCosts, String> {
794    let cost = |key: &str| -> Result<Option<rns_core::stamp::StampCost>, String> {
795        let Some(value) = params.get(key) else {
796            return Ok(None);
797        };
798        let value = config::parse_config_float(value)
799            .ok_or_else(|| format!("{key} must be a number, got '{value}'"))?;
800        Ok(Some(rns_core::stamp::clamp_cost(
801            value,
802            rns_core::constants::STAMP_COST_CAP,
803        )))
804    };
805    Ok(rns_core::transport::types::InterfaceStampCosts {
806        destination_cost: cost("destination_cost")?,
807        pr_cost: cost("pr_cost")?,
808    })
809}
810
811pub(crate) fn parse_managed_interface(
812    iface: &config::ParsedInterface,
813    parsed: &config::RnsConfig,
814    storage_dir: &Path,
815    id: rns_core::transport::types::InterfaceId,
816    registry: &crate::interface::registry::InterfaceRegistry,
817) -> io::Result<InterfaceConfig> {
818    let factory = registry.get(&iface.interface_type).ok_or_else(|| {
819        io::Error::new(
820            io::ErrorKind::InvalidInput,
821            format!("unsupported interface type '{}'", iface.interface_type),
822        )
823    })?;
824    let discoverable = iface
825        .params
826        .get("discoverable")
827        .and_then(|value| config::parse_bool_pub(value))
828        .unwrap_or(false);
829    let ignore_warnings = iface
830        .params
831        .get("ignore_config_warnings")
832        .and_then(|value| config::parse_bool_pub(value))
833        .unwrap_or(false);
834    let mode = normalize_discovery_mode(
835        &iface.interface_type,
836        parse_interface_mode(&iface.mode),
837        discoverable,
838        ignore_warnings,
839    );
840    let ifac = extract_ifac_config(&iface.params, factory.default_ifac_size());
841    let discovery = extract_discovery_config(&iface.name, &iface.interface_type, &iface.params);
842    let ingress_control = parse_ingress_control_config(&iface.interface_type, &iface.params)
843        .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
844    let stamp_costs = parse_interface_stamp_costs(&iface.params)
845        .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
846    let recursive_prs = iface
847        .params
848        .get("recursive_prs")
849        .and_then(|value| config::parse_bool_pub(value))
850        .unwrap_or(false);
851    let announces_from_internal = iface
852        .params
853        .get("announces_from_internal")
854        .and_then(|value| config::parse_bool_pub(value))
855        .unwrap_or(true);
856    let announces_to_internal = iface
857        .params
858        .get("announces_to_internal")
859        .and_then(|value| config::parse_bool_pub(value));
860    let gravity = parse_interface_gravity(
861        iface.params.get("gravity").map(String::as_str),
862        parsed.reticulum.default_gravity,
863    );
864    let mut params = iface.params.clone();
865    params
866        .entry("storage_dir".to_string())
867        .or_insert_with(|| storage_dir.to_string_lossy().to_string());
868    if let Some(device) = &parsed.reticulum.device {
869        params
870            .entry("device".to_string())
871            .or_insert_with(|| device.clone());
872    }
873    let config_data = factory
874        .parse_config_section(
875            &iface.name,
876            id,
877            crate::interface::ConfigSection {
878                params: &params,
879                children: &iface.subinterfaces,
880            },
881        )
882        .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
883    Ok(InterfaceConfig {
884        name: iface.name.clone(),
885        type_name: iface.interface_type.clone(),
886        config_data,
887        mode,
888        gravity,
889        recursive_prs,
890        announces_from_internal,
891        announces_to_internal,
892        ingress_control,
893        stamp_costs,
894        ifac,
895        discovery,
896    })
897}
898
899use crate::event::{QueryRequest, QueryResponse};
900
901/// Error returned when the driver thread has shut down.
902#[derive(Debug)]
903pub struct SendError;
904
905impl std::fmt::Display for SendError {
906    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
907        write!(f, "driver shut down")
908    }
909}
910
911impl std::error::Error for SendError {}
912
913/// A channel send can be temporarily blocked without the link having failed.
914#[derive(Debug, Clone, PartialEq, Eq)]
915pub enum ChannelSendError {
916    NotReady,
917    Rejected(String),
918    Unavailable,
919}
920
921impl From<String> for ChannelSendError {
922    fn from(error: String) -> Self {
923        if error == rns_core::channel::ChannelError::NotReady.to_string() {
924            Self::NotReady
925        } else {
926            Self::Rejected(error)
927        }
928    }
929}
930
931impl std::fmt::Display for ChannelSendError {
932    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
933        match self {
934            Self::NotReady => write!(f, "channel is not ready to send"),
935            Self::Rejected(error) => write!(f, "{error}"),
936            Self::Unavailable => write!(f, "driver is stopped or draining"),
937        }
938    }
939}
940
941impl std::error::Error for ChannelSendError {}
942
943/// A running RNS node.
944pub struct RnsNode {
945    tx: EventSender,
946    driver_handle: Option<JoinHandle<()>>,
947    verify_handle: Option<JoinHandle<()>>,
948    verify_shutdown: Arc<crate::driver::WorkSignal>,
949    persist_handle: Option<JoinHandle<()>>,
950    persist_stop: Option<std::sync::mpsc::Sender<()>>,
951    rpc_server: Option<crate::rpc::RpcServer>,
952    #[cfg(target_os = "linux")]
953    rpc_unix_server: Option<crate::rpc::RpcServer>,
954    tick_interval_ms: Arc<AtomicU64>,
955    /// The next [`SendTicket`](crate::SendTicket) number.
956    next_send_ticket: AtomicU64,
957    /// Whether deferred sends are encrypted on the caller's thread: when
958    /// this process may run on one CPU only, another thread would share it.
959    encrypt_deferred_inline: bool,
960    /// Cleared by the driver when it stops accepting new work.
961    accepting_work: Arc<AtomicBool>,
962    #[allow(dead_code)]
963    probe_server: Option<crate::holepunch::probe::ProbeServerHandle>,
964    known_destinations_path: Option<std::path::PathBuf>,
965    known_destinations_store: Option<Arc<crate::storage_sqlite::KnownDestinationsStore>>,
966    stamp_store: Option<crate::stamps::DestinationStampStore>,
967    /// `Transport.STAMP_DESTINATIONS`: whether announces from this node carry
968    /// a destination stamp. Defaults to the compile-time constant; shared
969    /// with the driver, which applies the `global.stamp_destinations`
970    /// runtime config key.
971    stamp_destinations: Arc<AtomicBool>,
972    stamp_targets: crate::stamper::StampTargets,
973    /// Shared with the driver; grinds stamps off the calling thread.
974    stamper: Option<Arc<crate::stamper::Stamper>>,
975    transport_state_dir: Option<std::path::PathBuf>,
976    ratchet_store: Option<Arc<dyn storage::RatchetStore>>,
977    ratchet_expiry: Duration,
978    /// Destination types this application needs (upstream 825c2e19).
979    #[cfg(feature = "iface-local")]
980    pub(crate) destination_types: crate::shared_client::DestinationTypeRequester,
981}
982
983impl RnsNode {
984    /// Start the node from a config file path.
985    /// If `config_path` is None, uses `~/.reticulum/`.
986    pub fn from_config(
987        config_path: Option<&Path>,
988        callbacks: Box<dyn Callbacks>,
989    ) -> io::Result<Self> {
990        let config_dir = storage::resolve_config_dir(config_path);
991        Self::from_config_dirs(&config_dir, &config_dir, None, false, callbacks)
992    }
993
994    /// Start an application-private node from a dedicated configuration and
995    /// state root.
996    ///
997    /// Private nodes never expose the shared-instance or management servers,
998    /// never load hooks, fail on unsupported/malformed enabled interfaces, and
999    /// force runtime interface failures to stop startup. `underlay_mark`
1000    /// overrides the value in the Reticulum configuration.
1001    pub fn from_private_config(
1002        config_dir: &Path,
1003        state_dir: &Path,
1004        underlay_mark: Option<u32>,
1005        callbacks: Box<dyn Callbacks>,
1006    ) -> io::Result<Self> {
1007        Self::from_config_dirs(config_dir, state_dir, underlay_mark, true, callbacks)
1008    }
1009
1010    fn from_config_dirs(
1011        config_dir: &Path,
1012        state_dir: &Path,
1013        underlay_mark: Option<u32>,
1014        private: bool,
1015        callbacks: Box<dyn Callbacks>,
1016    ) -> io::Result<Self> {
1017        let paths = storage::ensure_storage_dirs(state_dir)?;
1018        let known_destinations_path = paths.storage.join("known_destinations");
1019
1020        // Parse config file
1021        let config_file = config_dir.join("config");
1022        let config_content = if config_file.exists() {
1023            std::fs::read_to_string(&config_file)?
1024        } else {
1025            String::new()
1026        };
1027        let rns_config = config::parse(&config_content)
1028            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("{}", e)))?;
1029        let link_mtu_discovery = config::parse_link_mtu_discovery(&config_content)
1030            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("{}", e)))?;
1031
1032        // Load or create identity
1033        let identity = if let Some(ref id_path_str) = rns_config.reticulum.network_identity {
1034            let id_path = std::path::PathBuf::from(id_path_str);
1035            if id_path.exists() {
1036                storage::load_identity(&id_path)?
1037            } else {
1038                let id = Identity::new(&mut OsRng);
1039                storage::save_identity(&id, &id_path)?;
1040                id
1041            }
1042        } else {
1043            storage::load_or_create_identity(&paths.identities)?
1044        };
1045        // Python clients derive their RPC key from this persisted identity, even
1046        // when their non-transport instance uses an ephemeral network identity.
1047        let rpc_auth_key = if !private && rns_config.reticulum.share_instance {
1048            let transport_identity_path = storage::transport_identity_path(&paths);
1049            let rpc_identity = if transport_identity_path.exists() {
1050                storage::load_identity(&transport_identity_path)?
1051            } else {
1052                storage::save_identity(&identity, &transport_identity_path)?;
1053                Identity::from_private_key(
1054                    &identity
1055                        .get_private_key()
1056                        .expect("node identity has private key"),
1057                )
1058            };
1059            Some(crate::rpc::derive_auth_key(
1060                &rpc_identity
1061                    .get_private_key()
1062                    .expect("RPC identity has private key"),
1063            ))
1064        } else {
1065            None
1066        };
1067
1068        // Build interface configs from parsed config using registry
1069        let registry = crate::interface::registry::InterfaceRegistry::with_builtins();
1070        let mut interface_configs = Vec::new();
1071        let mut initial_ids = HashMap::new();
1072        let mut next_id_val = 1u64;
1073
1074        for iface in &rns_config.interfaces {
1075            if !iface.enabled {
1076                continue;
1077            }
1078
1079            let iface_id = rns_core::transport::types::InterfaceId(next_id_val);
1080            next_id_val += 1;
1081
1082            let factory = match registry.get(&iface.interface_type) {
1083                Some(f) => f,
1084                None => {
1085                    if private {
1086                        return Err(io::Error::new(
1087                            io::ErrorKind::InvalidData,
1088                            format!(
1089                                "unsupported enabled interface type '{}' for '{}'",
1090                                iface.interface_type, iface.name
1091                            ),
1092                        ));
1093                    }
1094                    log::warn!(
1095                        "Unsupported interface type '{}' for '{}'",
1096                        iface.interface_type,
1097                        iface.name,
1098                    );
1099                    continue;
1100                }
1101            };
1102
1103            let mut iface_mode = parse_interface_mode(&iface.mode);
1104
1105            let discoverable = iface
1106                .params
1107                .get("discoverable")
1108                .and_then(|value| config::parse_bool_pub(value))
1109                .unwrap_or(false);
1110            let ignore_config_warnings = iface
1111                .params
1112                .get("ignore_config_warnings")
1113                .and_then(|value| config::parse_bool_pub(value))
1114                .unwrap_or(false);
1115            let normalized_mode = normalize_discovery_mode(
1116                &iface.interface_type,
1117                iface_mode,
1118                discoverable,
1119                ignore_config_warnings,
1120            );
1121            if normalized_mode != iface_mode {
1122                log::info!(
1123                    "Interface '{}' has discovery enabled, auto-configuring mode to {}",
1124                    iface.name,
1125                    if normalized_mode == rns_core::constants::MODE_ACCESS_POINT {
1126                        "ACCESS_POINT"
1127                    } else {
1128                        "GATEWAY"
1129                    }
1130                );
1131                iface_mode = normalized_mode;
1132            }
1133
1134            let default_ifac_size = factory.default_ifac_size();
1135            let ifac_config = extract_ifac_config(&iface.params, default_ifac_size);
1136            let discovery_config =
1137                extract_discovery_config(&iface.name, &iface.interface_type, &iface.params);
1138            let ingress_control =
1139                match parse_ingress_control_config(&iface.interface_type, &iface.params) {
1140                    Ok(config) => config,
1141                    Err(e) => {
1142                        if private {
1143                            return Err(io::Error::new(
1144                                io::ErrorKind::InvalidData,
1145                                format!("invalid ingress control for '{}': {e}", iface.name),
1146                            ));
1147                        }
1148                        log::warn!(
1149                            "Failed to parse ingress control config for '{}': {}",
1150                            iface.name,
1151                            e
1152                        );
1153                        continue;
1154                    }
1155                };
1156            let stamp_costs = match parse_interface_stamp_costs(&iface.params) {
1157                Ok(costs) => costs,
1158                Err(e) => {
1159                    if private {
1160                        return Err(io::Error::new(
1161                            io::ErrorKind::InvalidData,
1162                            format!("invalid stamp cost for '{}': {e}", iface.name),
1163                        ));
1164                    }
1165                    log::warn!("Failed to parse stamp costs for '{}': {}", iface.name, e);
1166                    continue;
1167                }
1168            };
1169            let recursive_prs = iface
1170                .params
1171                .get("recursive_prs")
1172                .and_then(|v| config::parse_bool_pub(v))
1173                .unwrap_or(false);
1174            let announces_from_internal = iface
1175                .params
1176                .get("announces_from_internal")
1177                .and_then(|v| config::parse_bool_pub(v))
1178                .unwrap_or(true);
1179            let gravity = parse_interface_gravity(
1180                iface.params.get("gravity").map(String::as_str),
1181                rns_config.reticulum.default_gravity,
1182            );
1183            let announces_to_internal = iface
1184                .params
1185                .get("announces_to_internal")
1186                .and_then(|v| config::parse_bool_pub(v));
1187
1188            // Inject storage_dir for I2P (and any future factories that need it)
1189            let mut params = iface.params.clone();
1190            if !params.contains_key("storage_dir") {
1191                params.insert(
1192                    "storage_dir".to_string(),
1193                    paths.storage.to_string_lossy().to_string(),
1194                );
1195            }
1196            // Inject device for TCP client
1197            if let Some(ref device) = rns_config.reticulum.device {
1198                if !params.contains_key("device") {
1199                    params.insert("device".to_string(), device.clone());
1200                }
1201            }
1202
1203            let config_data = match factory.parse_config_section(
1204                &iface.name,
1205                iface_id,
1206                crate::interface::ConfigSection {
1207                    params: &params,
1208                    children: &iface.subinterfaces,
1209                },
1210            ) {
1211                Ok(data) => data,
1212                Err(e) => {
1213                    if private {
1214                        return Err(io::Error::new(
1215                            io::ErrorKind::InvalidData,
1216                            format!("invalid interface '{}': {e}", iface.name),
1217                        ));
1218                    }
1219                    log::warn!("Failed to parse config for '{}': {}", iface.name, e);
1220                    continue;
1221                }
1222            };
1223
1224            interface_configs.push(InterfaceConfig {
1225                name: iface.name.clone(),
1226                type_name: iface.interface_type.clone(),
1227                config_data,
1228                mode: iface_mode,
1229                gravity,
1230                recursive_prs,
1231                announces_from_internal,
1232                announces_to_internal,
1233                ingress_control,
1234                stamp_costs,
1235                ifac: ifac_config,
1236                discovery: discovery_config,
1237            });
1238            initial_ids.insert(iface.name.clone(), iface_id);
1239        }
1240
1241        // Parse management config
1242        let mut mgmt_allowed = Vec::new();
1243        for hex_hash in &rns_config.reticulum.remote_management_allowed {
1244            if hex_hash.len() == 32 {
1245                if let Ok(bytes) = (0..hex_hash.len())
1246                    .step_by(2)
1247                    .map(|i| u8::from_str_radix(&hex_hash[i..i + 2], 16))
1248                    .collect::<Result<Vec<u8>, _>>()
1249                {
1250                    if bytes.len() == 16 {
1251                        let mut h = [0u8; 16];
1252                        h.copy_from_slice(&bytes);
1253                        mgmt_allowed.push(h);
1254                    }
1255                } else {
1256                    log::warn!("Invalid hex in remote_management_allowed: {}", hex_hash);
1257                }
1258            } else {
1259                log::warn!(
1260                    "Invalid entry in remote_management_allowed (expected 32 hex chars, got {}): {}",
1261                    hex_hash.len(), hex_hash,
1262                );
1263            }
1264        }
1265
1266        // Parse probe_addr (comma-separated list of SocketAddr)
1267        let probe_addrs: Vec<std::net::SocketAddr> = rns_config
1268            .reticulum
1269            .probe_addr
1270            .as_ref()
1271            .map(|s| {
1272                s.split(',')
1273                    .filter_map(|entry| {
1274                        let trimmed = entry.trim();
1275                        if trimmed.is_empty() {
1276                            return None;
1277                        }
1278                        trimmed
1279                            .parse::<std::net::SocketAddr>()
1280                            .map_err(|e| {
1281                                log::warn!("Invalid probe_addr entry '{}': {}", trimmed, e);
1282                                e
1283                            })
1284                            .ok()
1285                    })
1286                    .collect()
1287            })
1288            .unwrap_or_default();
1289
1290        // Parse probe_protocol (default: rnsp)
1291        let probe_protocol = match rns_config
1292            .reticulum
1293            .probe_protocol
1294            .as_deref()
1295            .map(|s| s.to_lowercase())
1296        {
1297            Some(ref s) if s == "stun" => rns_core::holepunch::ProbeProtocol::Stun,
1298            _ => rns_core::holepunch::ProbeProtocol::Rnsp,
1299        };
1300
1301        let known_destinations_db = crate::storage_sqlite::default_path(&paths.storage);
1302        let mut known_destinations_store: Option<
1303            Arc<crate::storage_sqlite::KnownDestinationsStore>,
1304        > = None;
1305        let (known_destinations, ratchet_store): (
1306            std::collections::HashMap<[u8; 16], storage::KnownDestination>,
1307            Arc<dyn storage::RatchetStore>,
1308        ) = match crate::storage_sqlite::KnownDestinationsStore::open(&known_destinations_db, 16) {
1309            Ok(store) => {
1310                let store = Arc::new(store);
1311                let mut destinations: std::collections::HashMap<
1312                    [u8; 16],
1313                    storage::KnownDestination,
1314                > = store
1315                    .load_all()
1316                    .map(|entries| entries.into_iter().collect())
1317                    .unwrap_or_else(|err| {
1318                        log::warn!("failed to load known destinations from database: {}", err);
1319                        Default::default()
1320                    });
1321                if destinations.is_empty() && known_destinations_path.is_file() {
1322                    match store.import_legacy(&known_destinations_path) {
1323                        Ok(imported) if imported > 0 => {
1324                            log::info!(
1325                                "imported {} known destinations from legacy storage",
1326                                imported
1327                            );
1328                            let _ = std::fs::rename(
1329                                &known_destinations_path,
1330                                known_destinations_path.with_extension("imported"),
1331                            );
1332                            destinations = store
1333                                .load_all()
1334                                .map(|entries| entries.into_iter().collect())
1335                                .unwrap_or_default();
1336                        }
1337                        Ok(_) => {}
1338                        Err(err) => {
1339                            log::warn!("could not import legacy known destinations: {}", err)
1340                        }
1341                    }
1342                }
1343                let known_destination_hashes: HashSet<[u8; 16]> =
1344                    destinations.keys().copied().collect();
1345                if let Err(err) = store.import_legacy_ratchets(
1346                    &paths.ratchets,
1347                    time::now() - rns_config.reticulum.ratchet_expiry as f64,
1348                    &known_destination_hashes,
1349                ) {
1350                    log::warn!("could not import legacy ratchets: {}", err);
1351                }
1352                known_destinations_store = Some(Arc::clone(&store));
1353                (destinations, store)
1354            }
1355            Err(err) => {
1356                log::warn!(
1357                        "could not initialise known destinations database, falling back to legacy storage: {}",
1358                        err
1359                    );
1360                let destinations = match storage::load_known_destinations(&known_destinations_path)
1361                {
1362                    Ok(destinations) => destinations,
1363                    Err(err) if err.kind() == io::ErrorKind::NotFound => Default::default(),
1364                    Err(err) => {
1365                        log::warn!("failed to load known destinations: {}", err);
1366                        Default::default()
1367                    }
1368                };
1369                (
1370                    destinations,
1371                    Arc::new(storage::FsRatchetStore::new(paths.ratchets.clone())),
1372                )
1373            }
1374        };
1375        let known_destination_hashes: HashSet<[u8; 16]> =
1376            known_destinations.keys().copied().collect();
1377        match ratchet_store.cleanup(
1378            &known_destination_hashes,
1379            time::now(),
1380            rns_config.reticulum.ratchet_expiry as f64,
1381        ) {
1382            Ok(stats) if stats.processed > 0 => log::debug!(
1383                "Processed {} ratchets, not in use {}, removed {}",
1384                stats.processed,
1385                stats.not_known,
1386                stats.removed
1387            ),
1388            Ok(_) => {}
1389            Err(err) => log::warn!("failed to clean ratchets: {}", err),
1390        }
1391
1392        let interface_management = if private || !rns_config.reticulum.enable_interface_management {
1393            None
1394        } else {
1395            Some(InterfaceManagementConfig {
1396                config_file: config_file.clone(),
1397                storage_dir: paths.storage.clone(),
1398                initial_ids,
1399                queue_capacity: rns_config.reticulum.interface_writer_queue_capacity,
1400                transport_enabled: rns_config.reticulum.enable_transport,
1401                underlay_mark: underlay_mark.or(rns_config.reticulum.underlay_mark),
1402            })
1403        };
1404        let node_config = NodeConfig {
1405            transport_enabled: rns_config.reticulum.enable_transport,
1406            static_transport_identity: rns_config.reticulum.static_transport_identity,
1407            local_hops_delta: rns_config.reticulum.local_hops_delta,
1408            identity: Some(identity),
1409            share_instance: !private && rns_config.reticulum.share_instance,
1410            instance_name: rns_config.reticulum.instance_name.clone(),
1411            shared_instance_port: rns_config.reticulum.shared_instance_port,
1412            rpc_port: rns_config.reticulum.instance_control_port,
1413            cache_dir: Some(paths.cache),
1414            ratchet_store: Some(Arc::clone(&ratchet_store)),
1415            ratchet_expiry: Duration::from_secs(rns_config.reticulum.ratchet_expiry),
1416            management: if private {
1417                crate::management::ManagementConfig::default()
1418            } else {
1419                crate::management::ManagementConfig {
1420                    enable_remote_management: rns_config.reticulum.enable_remote_management,
1421                    remote_management_allowed: mgmt_allowed,
1422                    publish_blackhole: rns_config.reticulum.publish_blackhole,
1423                }
1424            },
1425            probe_port: rns_config.reticulum.probe_port,
1426            probe_addrs,
1427            probe_protocol,
1428            direct_connect_policy: rns_config.reticulum.direct_connect_policy,
1429            direct_connect_allow_private_endpoints: rns_config
1430                .reticulum
1431                .direct_connect_allow_private_endpoints,
1432            device: rns_config.reticulum.device.clone(),
1433            underlay_mark: underlay_mark.or(rns_config.reticulum.underlay_mark),
1434            hooks: if private {
1435                Vec::new()
1436            } else {
1437                rns_config.hooks.clone()
1438            },
1439            discover_interfaces: rns_config.reticulum.discover_interfaces,
1440            autoconnect_interface_mode: rns_config
1441                .reticulum
1442                .autoconnect_interface_mode
1443                .as_deref()
1444                .and_then(parse_optional_interface_mode),
1445            autoconnect_interface_gravity: rns_config.reticulum.autoconnect_interface_gravity,
1446            autoconnect_announces_to_internal: rns_config
1447                .reticulum
1448                .autoconnect_announces_to_internal,
1449            autoconnect_unverified_implementations: rns_config
1450                .reticulum
1451                .autoconnect_unverified_implementations,
1452            discovery_required_value: rns_config.reticulum.required_discovery_value,
1453            respond_to_probes: rns_config.reticulum.respond_to_probes,
1454            prefer_shorter_path: rns_config.reticulum.prefer_shorter_path,
1455            max_paths_per_destination: rns_config.reticulum.max_paths_per_destination,
1456            packet_hashlist_max_entries: rns_config.reticulum.packet_hashlist_max_entries,
1457            packet_hashlist_allocation: rns_config.reticulum.packet_hashlist_allocation,
1458            max_discovery_pr_tags: rns_config.reticulum.max_discovery_pr_tags,
1459            max_path_destinations: rns_config.reticulum.max_path_destinations,
1460            max_tunnel_destinations_total: rns_config.reticulum.max_tunnel_destinations_total,
1461            known_destinations_ttl: Duration::from_secs(
1462                rns_config.reticulum.known_destinations_ttl,
1463            ),
1464            known_destinations_max_entries: rns_config.reticulum.known_destinations_max_entries,
1465            announce_table_ttl: Duration::from_secs(rns_config.reticulum.announce_table_ttl),
1466            announce_table_max_bytes: rns_config.reticulum.announce_table_max_bytes,
1467            driver_event_queue_capacity: rns_config.reticulum.driver_event_queue_capacity,
1468            interface_writer_queue_capacity: rns_config.reticulum.interface_writer_queue_capacity,
1469            announce_rate_defaults: AnnounceRateDefaults {
1470                target: rns_config.reticulum.default_ar_target,
1471                penalty: rns_config.reticulum.default_ar_penalty,
1472                grace: rns_config.reticulum.default_ar_grace,
1473            },
1474            ingress_control_defaults: rns_core::transport::types::IngressControlConfig {
1475                enabled: true,
1476                egress_enabled: rns_config.reticulum.default_egress_control,
1477                max_held_announces: rns_config.reticulum.default_ic_max_held_announces,
1478                burst_freq_new: rns_config.reticulum.default_ic_burst_freq_new,
1479                burst_freq: rns_config.reticulum.default_ic_burst_freq,
1480                pr_burst_freq_new: rns_config.reticulum.default_ic_pr_burst_freq_new,
1481                pr_burst_freq: rns_config.reticulum.default_ic_pr_burst_freq,
1482                egress_pr_freq: rns_config.reticulum.default_ec_pr_freq,
1483                new_time: rns_config.reticulum.default_ic_new_time,
1484                burst_hold: rns_config.reticulum.default_ic_burst_hold,
1485                burst_penalty: rns_config.reticulum.default_ic_burst_penalty,
1486                held_release_interval: rns_config.reticulum.default_ic_held_release_interval,
1487            },
1488            #[cfg(feature = "iface-backbone")]
1489            backbone_peer_pool: if rns_config.reticulum.backbone_peer_pool_max_connected > 0 {
1490                Some(BackbonePeerPoolSettings {
1491                    max_connected: rns_config.reticulum.backbone_peer_pool_max_connected,
1492                    failure_threshold: rns_config.reticulum.backbone_peer_pool_failure_threshold,
1493                    failure_window: Duration::from_secs(
1494                        rns_config.reticulum.backbone_peer_pool_failure_window,
1495                    ),
1496                    cooldown: Duration::from_secs(rns_config.reticulum.backbone_peer_pool_cooldown),
1497                })
1498            } else {
1499                None
1500            },
1501            announce_sig_cache_enabled: rns_config.reticulum.announce_sig_cache_enabled,
1502            announce_sig_cache_max_entries: rns_config.reticulum.announce_sig_cache_max_entries,
1503            announce_sig_cache_ttl: Duration::from_secs(
1504                rns_config.reticulum.announce_sig_cache_ttl,
1505            ),
1506            interfaces: interface_configs,
1507            registry: None,
1508            panic_on_interface_error: private || rns_config.reticulum.panic_on_interface_error,
1509            link_proof_verify_threads: Default::default(),
1510            stamps: rns_config.stamps.clone(),
1511            #[cfg(feature = "hooks")]
1512            provider_bridge: if !private && rns_config.reticulum.provider_bridge {
1513                Some(crate::provider_bridge::ProviderBridgeConfig {
1514                    enabled: true,
1515                    socket_path: rns_config
1516                        .reticulum
1517                        .provider_socket_path
1518                        .as_ref()
1519                        .map(std::path::PathBuf::from)
1520                        .unwrap_or_else(|| config_dir.join("provider.sock")),
1521                    queue_max_events: rns_config.reticulum.provider_queue_max_events,
1522                    queue_max_bytes: rns_config.reticulum.provider_queue_max_bytes,
1523                    overflow_policy: match rns_config.reticulum.provider_overflow_policy.as_str() {
1524                        "drop_oldest" => crate::provider_bridge::OverflowPolicy::DropOldest,
1525                        _ => crate::provider_bridge::OverflowPolicy::DropNewest,
1526                    },
1527                    node_instance: rns_config.reticulum.instance_name.clone(),
1528                })
1529            } else {
1530                None
1531            },
1532        };
1533
1534        let mut node = Self::start_with_queue_config(
1535            node_config,
1536            callbacks,
1537            NodeQueueConfig {
1538                rpc_auth_key,
1539                announce_max_entries: rns_config.reticulum.announce_queue_max_entries,
1540                announce_max_interfaces: rns_config.reticulum.announce_queue_max_interfaces,
1541                announce_max_bytes: rns_config.reticulum.announce_queue_max_bytes,
1542                announce_ttl_secs: rns_config.reticulum.announce_queue_ttl as f64,
1543                announce_overflow_policy: match rns_config
1544                    .reticulum
1545                    .announce_queue_overflow_policy
1546                    .as_str()
1547                {
1548                    "drop_newest" => AnnounceQueueOverflowPolicy::DropNewest,
1549                    "drop_oldest" => AnnounceQueueOverflowPolicy::DropOldest,
1550                    _ => AnnounceQueueOverflowPolicy::DropWorst,
1551                },
1552                inbound_capacities: rns_config.reticulum.inbound_queue_capacities,
1553                link_mtu_discovery,
1554                filter: rns_config.filter.clone(),
1555                hop_limiter: rns_config.reticulum.hop_limiter,
1556            },
1557            interface_management,
1558        )?;
1559
1560        node.known_destinations_path = Some(known_destinations_path.clone());
1561        node.known_destinations_store = known_destinations_store;
1562        node.set_stamp_store(crate::stamps::DestinationStampStore::new(&paths.storage));
1563        for (dest_hash, known) in known_destinations {
1564            let _ = node.query(QueryRequest::RestoreKnownDestination(
1565                crate::event::KnownDestinationEntry {
1566                    dest_hash,
1567                    identity_hash: known.identity_hash,
1568                    public_key: known.public_key,
1569                    app_data: known.app_data,
1570                    hops: known.hops,
1571                    received_at: known.received_at,
1572                    receiving_interface: rns_core::transport::types::InterfaceId(
1573                        known.receiving_interface,
1574                    ),
1575                    was_used: known.was_used,
1576                    last_used_at: known.last_used_at,
1577                    retained: known.retained,
1578                    stamp: known
1579                        .stamp
1580                        .as_deref()
1581                        .and_then(|stamp| stamp.try_into().ok()),
1582                    stamp_value: known.stamp_value,
1583                    lr_cost: known.lr_cost,
1584                },
1585            ));
1586        }
1587
1588        Ok(node)
1589    }
1590
1591    /// Start the node. Connects all interfaces, starts driver and timer threads.
1592    pub fn start(config: NodeConfig, callbacks: Box<dyn Callbacks>) -> io::Result<Self> {
1593        crate::interface::validate_underlay_mark(config.underlay_mark)?;
1594        let inbound_queue_capacities = crate::event::InboundQueueCapacities::from_shared_capacity(
1595            config.driver_event_queue_capacity,
1596        );
1597        Self::start_with_queue_config(
1598            config,
1599            callbacks,
1600            NodeQueueConfig {
1601                inbound_capacities: inbound_queue_capacities,
1602                ..NodeQueueConfig::default()
1603            },
1604            None,
1605        )
1606    }
1607
1608    fn start_with_queue_config(
1609        config: NodeConfig,
1610        callbacks: Box<dyn Callbacks>,
1611        queue_config: NodeQueueConfig,
1612        interface_management: Option<InterfaceManagementConfig>,
1613    ) -> io::Result<Self> {
1614        let storage_dir = config.cache_dir.as_ref().map(|cache_dir| {
1615            cache_dir
1616                .parent()
1617                .unwrap_or_else(|| std::path::Path::new("."))
1618                .join("storage")
1619        });
1620        let transport_state_dir = if config.transport_enabled {
1621            storage_dir.clone()
1622        } else {
1623            None
1624        };
1625        let identity = config.identity.unwrap_or_else(|| Identity::new(&mut OsRng));
1626        let rpc_auth_key = queue_config.rpc_auth_key.unwrap_or_else(|| {
1627            crate::rpc::derive_auth_key(
1628                &identity
1629                    .get_private_key()
1630                    .expect("node identity has private key"),
1631            )
1632        });
1633        let has_discoverable_interface = config
1634            .interfaces
1635            .iter()
1636            .any(|iface| iface.discovery.is_some());
1637        let static_transport_identity = effective_static_transport_identity(
1638            config.static_transport_identity,
1639            config.transport_enabled,
1640            has_discoverable_interface,
1641        );
1642        if static_transport_identity && !config.static_transport_identity {
1643            log::warn!(
1644                "Discoverable interface was configured, enabling static transport identity on non-transport instance"
1645            );
1646        }
1647        let transport_identity = if config.transport_enabled || static_transport_identity {
1648            Identity::from_private_key(
1649                &identity
1650                    .get_private_key()
1651                    .expect("node identity must expose a private key"),
1652            )
1653        } else {
1654            Identity::new(&mut OsRng)
1655        };
1656
1657        let local_hops_delta = if config.local_hops_delta {
1658            let mut byte = [0u8; 1];
1659            OsRng.fill_bytes(&mut byte);
1660            byte[0] % 6 + 2
1661        } else {
1662            0
1663        };
1664
1665        let transport_config = TransportConfig {
1666            transport_enabled: config.transport_enabled,
1667            identity_hash: Some(*transport_identity.hash()),
1668            local_hops_delta,
1669            prefer_shorter_path: config.prefer_shorter_path,
1670            max_paths_per_destination: config.max_paths_per_destination,
1671            packet_hashlist_max_entries: config.packet_hashlist_max_entries,
1672            packet_hashlist_allocation: config.packet_hashlist_allocation,
1673            max_discovery_pr_tags: config.max_discovery_pr_tags,
1674            max_path_destinations: config.max_path_destinations,
1675            max_tunnel_destinations_total: config.max_tunnel_destinations_total,
1676            destination_timeout_secs: config.known_destinations_ttl.as_secs_f64(),
1677            announce_table_ttl_secs: config.announce_table_ttl.as_secs_f64(),
1678            announce_table_max_bytes: config.announce_table_max_bytes,
1679            announce_sig_cache_enabled: config.announce_sig_cache_enabled,
1680            announce_sig_cache_max_entries: config.announce_sig_cache_max_entries,
1681            announce_sig_cache_ttl_secs: config.announce_sig_cache_ttl.as_secs_f64(),
1682            announce_queue_max_entries: queue_config.announce_max_entries,
1683            announce_queue_max_interfaces: queue_config.announce_max_interfaces,
1684        };
1685
1686        let (tx, rx) = event::channel_with_queue_capacities(
1687            config.driver_event_queue_capacity,
1688            queue_config.inbound_capacities,
1689        );
1690        let tick_interval_ms = Arc::new(AtomicU64::new(1000));
1691        let mut driver = Driver::new(transport_config, rx, tx.clone(), callbacks);
1692        let stamper = Arc::new(crate::stamper::Stamper::new(tx.clone(), None));
1693        driver.set_stamper(Arc::clone(&stamper));
1694        let stamp_destinations = Arc::new(AtomicBool::new(rns_core::constants::STAMP_DESTINATIONS));
1695        driver.set_stamp_destinations_flag(Arc::clone(&stamp_destinations));
1696        driver.apply_stamp_config(&config.stamps, queue_config.hop_limiter);
1697        *driver.engine.destination_filter_mut() = queue_config.filter.destination_filter();
1698        let stamp_targets = crate::stamper::StampTargets::from(&config.stamps);
1699        driver.underlay_mark = config.underlay_mark;
1700        driver
1701            .link_manager
1702            .set_link_mtu_discovery(queue_config.link_mtu_discovery);
1703        driver
1704            .link_manager
1705            .set_proof_verify_threads(config.link_proof_verify_threads);
1706        driver.set_announce_verify_queue_config(
1707            queue_config.announce_max_entries,
1708            queue_config.announce_max_bytes,
1709            queue_config.announce_ttl_secs,
1710            queue_config.announce_overflow_policy,
1711        );
1712        driver.async_announce_verification = true;
1713        driver.set_tick_interval_handle(Arc::clone(&tick_interval_ms));
1714        let accepting_work = driver.accepting_work_handle();
1715        driver.known_destinations_ttl = config.known_destinations_ttl.as_secs_f64();
1716        driver.known_destinations_max_entries = config.known_destinations_max_entries;
1717        driver.ratchet_store = config.ratchet_store.clone();
1718        driver.ratchet_expiry_secs = config.ratchet_expiry.as_secs_f64();
1719        driver.interface_writer_queue_capacity = config.interface_writer_queue_capacity;
1720        driver.set_announce_rate_defaults(config.announce_rate_defaults);
1721        driver.set_ingress_control_defaults(config.ingress_control_defaults);
1722        driver.runtime_config_defaults.known_destinations_ttl =
1723            config.known_destinations_ttl.as_secs_f64();
1724        driver
1725            .holepunch_manager
1726            .set_policy(config.direct_connect_policy);
1727        driver
1728            .holepunch_manager
1729            .set_allow_private_endpoints(config.direct_connect_allow_private_endpoints);
1730        driver.runtime_config_defaults.direct_connect_policy = config.direct_connect_policy;
1731        #[cfg(feature = "hooks")]
1732        if let Some(provider_config) = config.provider_bridge.clone() {
1733            driver.runtime_config_defaults.provider_queue_max_events =
1734                provider_config.queue_max_events;
1735            driver.runtime_config_defaults.provider_queue_max_bytes =
1736                provider_config.queue_max_bytes;
1737            if provider_config.enabled {
1738                let started = if crate::engine::FOLD_SERVICES {
1739                    crate::provider_bridge::ProviderBridge::start_folded(
1740                        provider_config,
1741                        Arc::clone(tx.poller()),
1742                    )
1743                } else {
1744                    crate::provider_bridge::ProviderBridge::start(provider_config)
1745                };
1746                match started {
1747                    Ok(bridge) => driver.provider_bridge = Some(bridge),
1748                    Err(err) => log::warn!("failed to start provider bridge: {}", err),
1749                }
1750            }
1751        }
1752
1753        // Set up announce cache if cache directory is configured
1754        if let Some(ref cache_dir) = config.cache_dir {
1755            let announces_dir = cache_dir.join("announces");
1756            let _ = std::fs::create_dir_all(&announces_dir);
1757            driver.announce_cache = Some(crate::announce_cache::AnnounceCache::new(announces_dir));
1758        }
1759
1760        // Configure probe addresses and device for hole punching
1761        if !config.probe_addrs.is_empty()
1762            || config.device.is_some()
1763            || config.underlay_mark.is_some()
1764        {
1765            driver.set_probe_config(
1766                config.probe_addrs.clone(),
1767                config.probe_protocol,
1768                config.device.clone(),
1769            );
1770        }
1771
1772        // Start probe server if configured
1773        let probe_server = if let Some(port) = config.probe_port {
1774            let listen_addr: std::net::SocketAddr = ([0, 0, 0, 0], port).into();
1775            if crate::engine::FOLD_SERVICES {
1776                match crate::holepunch::probe::FoldedProbeServer::bind(
1777                    listen_addr,
1778                    config.underlay_mark,
1779                    Arc::clone(tx.poller()),
1780                ) {
1781                    Ok(server) => {
1782                        log::info!("Probe server started on 0.0.0.0:{}", port);
1783                        driver.probe_server = Some(server);
1784                    }
1785                    Err(e) => log::error!("Failed to start probe server on port {}: {}", port, e),
1786                }
1787                None
1788            } else {
1789                match crate::holepunch::probe::start_probe_server_with_mark(
1790                    listen_addr,
1791                    config.underlay_mark,
1792                ) {
1793                    Ok(handle) => {
1794                        log::info!("Probe server started on 0.0.0.0:{}", port);
1795                        Some(handle)
1796                    }
1797                    Err(e) => {
1798                        log::error!("Failed to start probe server on port {}: {}", port, e);
1799                        None
1800                    }
1801                }
1802            }
1803        } else {
1804            None
1805        };
1806
1807        // Store management config on driver for ACL enforcement
1808        driver.management_config = config.management.clone();
1809
1810        // Store transport identity for tunnel synthesis and management announces.
1811        driver.transport_identity = Some(transport_identity);
1812
1813        // Load hooks from config
1814        #[cfg(feature = "hooks")]
1815        {
1816            for hook_cfg in &config.hooks {
1817                if !hook_cfg.enabled {
1818                    continue;
1819                }
1820                let point_idx = match config::parse_hook_point(&hook_cfg.attach_point) {
1821                    Some(idx) => idx,
1822                    None => {
1823                        log::warn!(
1824                            "Unknown hook point '{}' for hook '{}'",
1825                            hook_cfg.attach_point,
1826                            hook_cfg.name,
1827                        );
1828                        continue;
1829                    }
1830                };
1831                let mgr = match driver.hook_manager.as_ref() {
1832                    Some(m) => m,
1833                    None => {
1834                        log::warn!(
1835                            "Hook manager not available, skipping hook '{}'",
1836                            hook_cfg.name
1837                        );
1838                        continue;
1839                    }
1840                };
1841                let hook_backend = match config::parse_hook_backend(&hook_cfg.hook_type) {
1842                    Ok(backend) => backend,
1843                    Err(e) => {
1844                        log::warn!(
1845                            "Invalid hook type '{}' for hook '{}': {}",
1846                            hook_cfg.hook_type,
1847                            hook_cfg.name,
1848                            e,
1849                        );
1850                        continue;
1851                    }
1852                };
1853                let load_result = if hook_backend == rns_hooks::HookBackend::Builtin {
1854                    let builtin_id = hook_cfg
1855                        .builtin_id
1856                        .as_deref()
1857                        .filter(|id| !id.is_empty())
1858                        .or_else(|| (!hook_cfg.path.is_empty()).then_some(hook_cfg.path.as_str()));
1859                    match builtin_id {
1860                        Some(id) => mgr.load_builtin(hook_cfg.name.clone(), id, hook_cfg.priority),
1861                        None => Err(rns_hooks::HookError::CompileError(
1862                            "built-in hook requires builtin/id or path".to_string(),
1863                        )),
1864                    }
1865                } else {
1866                    mgr.load_file_backend(
1867                        hook_cfg.name.clone(),
1868                        std::path::Path::new(&hook_cfg.path),
1869                        hook_cfg.priority,
1870                        hook_backend,
1871                    )
1872                };
1873                match load_result {
1874                    Ok(program) => {
1875                        driver.hook_slots[point_idx].attach(program);
1876                        log::info!(
1877                            "Loaded hook '{}' at point {} (priority {})",
1878                            hook_cfg.name,
1879                            hook_cfg.attach_point,
1880                            hook_cfg.priority,
1881                        );
1882                    }
1883                    Err(e) => {
1884                        log::error!(
1885                            "Failed to load hook '{}' from '{}': {}",
1886                            hook_cfg.name,
1887                            hook_cfg.path,
1888                            e,
1889                        );
1890                    }
1891                }
1892            }
1893        }
1894
1895        // Configure discovery
1896        driver.discover_interfaces = config.discover_interfaces;
1897        driver.autoconnect_interface_mode = config.autoconnect_interface_mode;
1898        driver.autoconnect_interface_gravity = config.autoconnect_interface_gravity;
1899        driver.autoconnect_announces_to_internal =
1900            config.autoconnect_announces_to_internal.then_some(true);
1901        driver.autoconnect_unverified_implementations =
1902            config.autoconnect_unverified_implementations;
1903        if let Some(val) = config.discovery_required_value {
1904            driver.discovery_required_value = val;
1905        }
1906
1907        // Shared counter for dynamic interface IDs
1908        let next_dynamic_id = Arc::new(AtomicU64::new(10000));
1909        driver.next_dynamic_interface_id = Arc::clone(&next_dynamic_id);
1910
1911        // Collect discoverable interface configs for the announcer
1912        let mut discoverable_interfaces = Vec::new();
1913        #[cfg(feature = "iface-backbone")]
1914        let mut backbone_peer_pool_candidates = Vec::new();
1915
1916        let initial_managed_ids = interface_management
1917            .as_ref()
1918            .map(|management| management.initial_ids.clone())
1919            .unwrap_or_default();
1920        driver.interface_management = interface_management;
1921
1922        // --- Registry-based startup for interfaces ---
1923        let registry = config
1924            .registry
1925            .unwrap_or_else(crate::interface::registry::InterfaceRegistry::with_builtins);
1926        for iface_config in config.interfaces {
1927            let managed_id = initial_managed_ids.get(&iface_config.name).copied();
1928            #[cfg(feature = "iface-backbone")]
1929            if iface_config.type_name == "BackboneInterface" {
1930                if let Some(mode) = iface_config
1931                    .config_data
1932                    .as_any()
1933                    .downcast_ref::<BackboneMode>()
1934                {
1935                    if let Some(handle) = runtime_handle_from_mode(mode) {
1936                        driver.register_backbone_runtime(handle);
1937                    }
1938                    if let Some(handle) = peer_state_handle_from_mode(mode) {
1939                        driver.register_backbone_peer_state(handle);
1940                    }
1941                    if let Some(handle) = client_runtime_handle_from_mode(mode) {
1942                        driver.register_backbone_client_runtime(handle);
1943                    }
1944                    if let Some(handle) = backbone_discovery_runtime_from_interface(
1945                        &iface_config.name,
1946                        mode,
1947                        iface_config.discovery.as_ref(),
1948                        config.transport_enabled,
1949                        iface_config.ifac.as_ref(),
1950                    ) {
1951                        driver.register_backbone_discovery_runtime(handle);
1952                    }
1953                }
1954            }
1955            #[cfg(feature = "iface-tcp")]
1956            if iface_config.type_name == "TCPClientInterface" {
1957                if let Some(tcp_config) = iface_config
1958                    .config_data
1959                    .as_any()
1960                    .downcast_ref::<TcpClientConfig>()
1961                {
1962                    driver.register_tcp_client_runtime(tcp_client_runtime_handle_from_config(
1963                        tcp_config,
1964                    ));
1965                }
1966            }
1967            #[cfg(feature = "iface-tcp")]
1968            if iface_config.type_name == "TCPServerInterface" {
1969                if let Some(tcp_config) = iface_config
1970                    .config_data
1971                    .as_any()
1972                    .downcast_ref::<TcpServerConfig>()
1973                {
1974                    driver.register_tcp_server_runtime(tcp_runtime_handle_from_config(tcp_config));
1975                    driver.register_tcp_server_discovery_runtime(
1976                        tcp_server_discovery_runtime_from_interface(
1977                            &iface_config.name,
1978                            tcp_config,
1979                            iface_config.discovery.as_ref(),
1980                            config.transport_enabled,
1981                            iface_config.ifac.as_ref(),
1982                        ),
1983                    );
1984                }
1985            }
1986            #[cfg(feature = "iface-udp")]
1987            if iface_config.type_name == "UDPInterface" {
1988                if let Some(udp_config) = iface_config
1989                    .config_data
1990                    .as_any()
1991                    .downcast_ref::<UdpConfig>()
1992                {
1993                    driver.register_udp_runtime(udp_runtime_handle_from_config(udp_config));
1994                }
1995            }
1996            #[cfg(feature = "iface-auto")]
1997            if iface_config.type_name == "AutoInterface" {
1998                if let Some(auto_config) = iface_config
1999                    .config_data
2000                    .as_any()
2001                    .downcast_ref::<AutoConfig>()
2002                {
2003                    driver.register_auto_runtime(auto_runtime_handle_from_config(auto_config));
2004                }
2005            }
2006            #[cfg(feature = "iface-i2p")]
2007            if iface_config.type_name == "I2PInterface" {
2008                if let Some(i2p_config) = iface_config
2009                    .config_data
2010                    .as_any()
2011                    .downcast_ref::<I2pConfig>()
2012                {
2013                    driver.register_i2p_runtime(i2p_runtime_handle_from_config(i2p_config));
2014                }
2015            }
2016            #[cfg(feature = "iface-pipe")]
2017            if iface_config.type_name == "PipeInterface" {
2018                if let Some(pipe_config) = iface_config
2019                    .config_data
2020                    .as_any()
2021                    .downcast_ref::<PipeConfig>()
2022                {
2023                    driver.register_pipe_runtime(pipe_runtime_handle_from_config(pipe_config));
2024                }
2025            }
2026            #[cfg(feature = "iface-rnode")]
2027            if iface_config.type_name == "RNodeInterface" {
2028                if let Some(rnode_config) = iface_config
2029                    .config_data
2030                    .as_any()
2031                    .downcast_ref::<RNodeConfig>()
2032                {
2033                    driver.register_rnode_runtime(rnode_runtime_handle_from_config(rnode_config));
2034                }
2035            }
2036
2037            let factory = match registry.get(&iface_config.type_name) {
2038                Some(f) => f,
2039                None => {
2040                    log::warn!(
2041                        "No factory registered for interface type '{}'",
2042                        iface_config.type_name
2043                    );
2044                    continue;
2045                }
2046            };
2047
2048            let mut ifac_state = derive_ifac_state(iface_config.ifac.as_ref(), &iface_config.name)?;
2049            let ifac_runtime =
2050                ifac_runtime_from_config(iface_config.ifac.as_ref(), factory.default_ifac_size());
2051
2052            #[cfg(feature = "iface-backbone")]
2053            if config.backbone_peer_pool.is_some() && iface_config.type_name == "BackboneInterface"
2054            {
2055                if let Some(mode) = iface_config
2056                    .config_data
2057                    .as_any()
2058                    .downcast_ref::<BackboneMode>()
2059                {
2060                    if let Some(client) = client_config_from_mode(mode) {
2061                        let priority = client_priority_from_mode(mode).unwrap_or(
2062                            crate::driver::BACKBONE_PEER_POOL_CONFIGURED_DEFAULT_PRIORITY,
2063                        );
2064                        backbone_peer_pool_candidates.push(BackbonePeerPoolCandidateConfig {
2065                            client,
2066                            mode: iface_config.mode,
2067                            gravity: iface_config.gravity,
2068                            recursive_prs: iface_config.recursive_prs,
2069                            announces_from_internal: iface_config.announces_from_internal,
2070                            announces_to_internal: iface_config.announces_to_internal,
2071                            ingress_control: iface_config.ingress_control,
2072                            stamp_costs: iface_config.stamp_costs,
2073                            ifac_runtime: ifac_runtime.clone(),
2074                            ifac_enabled: ifac_state.is_some(),
2075                            interface_type_name: iface_config.type_name.clone(),
2076                            source: crate::driver::BackbonePeerPoolCandidateSource::Configured,
2077                            priority,
2078                            discovery: None,
2079                        });
2080                        if let Some(ref disc) = iface_config.discovery {
2081                            discoverable_interfaces.push(discoverable_interface_from_config(
2082                                &iface_config.name,
2083                                disc,
2084                                config.transport_enabled,
2085                                iface_config.ifac.as_ref(),
2086                            ));
2087                        }
2088                        continue;
2089                    }
2090                }
2091            }
2092
2093            let ctx = crate::interface::StartContext {
2094                tx: tx.clone(),
2095                next_dynamic_id: next_dynamic_id.clone(),
2096                mode: iface_config.mode,
2097                gravity: iface_config.gravity,
2098                recursive_prs: iface_config.recursive_prs,
2099                announces_from_internal: iface_config.announces_from_internal,
2100                announces_to_internal: iface_config.announces_to_internal,
2101                ingress_control: iface_config.ingress_control,
2102                stamp_costs: iface_config.stamp_costs,
2103                ifac: ifac_state.clone(),
2104                underlay_mark: config.underlay_mark,
2105            };
2106
2107            let result = match factory.start(iface_config.config_data, ctx) {
2108                Ok(r) => r,
2109                Err(e) => {
2110                    if config.panic_on_interface_error {
2111                        return Err(e);
2112                    }
2113                    log::error!(
2114                        "Interface '{}' ({}) failed to start: {}",
2115                        iface_config.name,
2116                        iface_config.type_name,
2117                        e
2118                    );
2119                    continue;
2120                }
2121            };
2122
2123            if let Some(ref disc) = iface_config.discovery {
2124                discoverable_interfaces.push(discoverable_interface_from_config(
2125                    &iface_config.name,
2126                    disc,
2127                    config.transport_enabled,
2128                    iface_config.ifac.as_ref(),
2129                ));
2130            }
2131
2132            match result {
2133                crate::interface::StartResult::Simple {
2134                    id,
2135                    info,
2136                    writer,
2137                    interface_type_name,
2138                    control,
2139                } => {
2140                    let managed_control = control.clone();
2141                    if let Some(control) = control {
2142                        driver.register_listener_control(control);
2143                    }
2144                    if let Some(parent_id) = managed_id {
2145                        driver.managed_interfaces.insert(
2146                            iface_config.name.clone(),
2147                            crate::driver::ManagedInterface {
2148                                parent_id,
2149                                type_name: iface_config.type_name.clone(),
2150                                control: managed_control,
2151                                static_ids: vec![id],
2152                            },
2153                        );
2154                    }
2155                    register_started_interface(StartedInterface {
2156                        driver: &mut driver,
2157                        tx: &tx,
2158                        queue_capacity: config.interface_writer_queue_capacity,
2159                        id,
2160                        info,
2161                        writer,
2162                        interface_type_name,
2163                        ifac_state,
2164                        ifac_runtime: &ifac_runtime,
2165                        stamp_costs: iface_config.stamp_costs,
2166                    });
2167                }
2168                crate::interface::StartResult::Listener { control } => {
2169                    // Listener-type interface (TcpServer, Auto, I2P, etc.)
2170                    // registers dynamic interfaces via InterfaceUp events.
2171                    let managed_control = control.clone();
2172                    if let Some(control) = control {
2173                        driver.register_listener_control(control);
2174                    }
2175                    if let Some(parent_id) = managed_id {
2176                        driver.managed_interfaces.insert(
2177                            iface_config.name.clone(),
2178                            crate::driver::ManagedInterface {
2179                                parent_id,
2180                                type_name: iface_config.type_name.clone(),
2181                                control: managed_control,
2182                                static_ids: Vec::new(),
2183                            },
2184                        );
2185                    }
2186                }
2187                crate::interface::StartResult::Multi {
2188                    subinterfaces: subs,
2189                    control,
2190                } => {
2191                    let managed_control = control.clone();
2192                    if let Some(control) = control {
2193                        driver.register_listener_control(control);
2194                    }
2195                    let static_ids = subs.iter().map(|sub| sub.id).collect();
2196                    if let Some(parent_id) = managed_id {
2197                        driver.managed_interfaces.insert(
2198                            iface_config.name.clone(),
2199                            crate::driver::ManagedInterface {
2200                                parent_id,
2201                                type_name: iface_config.type_name.clone(),
2202                                control: managed_control,
2203                                static_ids,
2204                            },
2205                        );
2206                    }
2207                    let ifac_cfg = &iface_config.ifac;
2208                    let mut first = true;
2209                    for sub in subs {
2210                        let sub_ifac = if first {
2211                            first = false;
2212                            ifac_state.take()
2213                        } else {
2214                            derive_ifac_state(ifac_cfg.as_ref(), &sub.info.name)?
2215                        };
2216                        register_started_interface(StartedInterface {
2217                            driver: &mut driver,
2218                            tx: &tx,
2219                            queue_capacity: config.interface_writer_queue_capacity,
2220                            id: sub.id,
2221                            info: sub.info,
2222                            writer: sub.writer,
2223                            interface_type_name: sub.interface_type_name,
2224                            ifac_state: sub_ifac,
2225                            ifac_runtime: &ifac_runtime,
2226                            stamp_costs: iface_config.stamp_costs,
2227                        });
2228                    }
2229                }
2230            }
2231        }
2232
2233        // Set up interface announcer if we have discoverable interfaces
2234        if !discoverable_interfaces.is_empty() {
2235            let transport_id = *identity.hash();
2236            let announcer =
2237                crate::discovery::InterfaceAnnouncer::new(transport_id, discoverable_interfaces);
2238            log::info!("Interface discovery announcer initialized");
2239            driver.interface_announcer = Some(announcer);
2240        }
2241
2242        // Set up discovered interfaces storage path
2243        if let Some(ref cache_dir) = config.cache_dir {
2244            let disc_path = std::path::PathBuf::from(cache_dir)
2245                .parent()
2246                .unwrap_or(std::path::Path::new("."))
2247                .join("storage")
2248                .join("discovery")
2249                .join("interfaces");
2250            let _ = std::fs::create_dir_all(&disc_path);
2251            driver.discovered_interfaces =
2252                crate::discovery::DiscoveredInterfaceStorage::new(disc_path);
2253        }
2254
2255        // Restore after static interfaces and the announce cache are available,
2256        // but before the driver can process inbound traffic.
2257        if let Some(ref storage_dir) = transport_state_dir {
2258            match storage::load_transport_state(storage_dir) {
2259                Ok(snapshot) => {
2260                    let announce_cache = driver.announce_cache.as_ref();
2261                    let stats = driver.engine.restore_persistence_snapshot(
2262                        snapshot,
2263                        time::now(),
2264                        |packet_hash| {
2265                            announce_cache
2266                                .and_then(|cache| cache.get(packet_hash).ok().flatten())
2267                                .map(|(raw, _)| raw)
2268                        },
2269                    );
2270                    log::debug!(
2271                        "restored transport state: {} packet hashes, {} paths, {} tunnels ({} paths skipped)",
2272                        stats.packet_hashes,
2273                        stats.paths,
2274                        stats.tunnels,
2275                        stats.skipped_paths,
2276                    );
2277                }
2278                Err(err) => log::warn!("failed to restore transport state: {}", err),
2279            }
2280        }
2281
2282        #[cfg(feature = "iface-backbone")]
2283        if let Some(settings) = config.backbone_peer_pool.clone() {
2284            driver.configure_backbone_peer_pool(settings, backbone_peer_pool_candidates);
2285            driver.seed_backbone_peer_pool_from_discovery_cache();
2286        }
2287
2288        // Set up management destinations if enabled
2289        if config.management.enable_remote_management {
2290            if let Some(prv_key) = identity.get_private_key() {
2291                let identity_hash = *identity.hash();
2292                let mgmt_dest = crate::management::management_dest_hash(&identity_hash);
2293
2294                // Extract Ed25519 signing keys from the identity
2295                let sig_prv = rns_crypto::ed25519::Ed25519PrivateKey::from_bytes(
2296                    &prv_key[32..64].try_into().unwrap(),
2297                );
2298                let sig_pub_bytes: [u8; 32] = identity.get_public_key().unwrap()[32..64]
2299                    .try_into()
2300                    .unwrap();
2301
2302                // Register as SINGLE destination in transport engine
2303                driver
2304                    .engine
2305                    .register_destination(mgmt_dest, rns_core::constants::DESTINATION_SINGLE);
2306                driver
2307                    .local_destinations
2308                    .insert(mgmt_dest, rns_core::constants::DESTINATION_SINGLE);
2309
2310                // Register as link destination in link manager
2311                driver.link_manager.register_link_destination(
2312                    mgmt_dest,
2313                    sig_prv,
2314                    sig_pub_bytes,
2315                    crate::link_manager::ResourceStrategy::AcceptNone,
2316                );
2317
2318                // Register management path hashes
2319                driver
2320                    .link_manager
2321                    .register_management_path(crate::management::status_path_hash());
2322                driver
2323                    .link_manager
2324                    .register_management_path(crate::management::path_path_hash());
2325
2326                log::info!("Remote management enabled on {:02x?}", &mgmt_dest[..4],);
2327
2328                // Set up allowed list
2329                if !config.management.remote_management_allowed.is_empty() {
2330                    log::info!(
2331                        "Remote management allowed for {} identities",
2332                        config.management.remote_management_allowed.len(),
2333                    );
2334                }
2335            }
2336        }
2337
2338        if config.management.publish_blackhole {
2339            if let Some(prv_key) = identity.get_private_key() {
2340                let identity_hash = *identity.hash();
2341                let bh_dest = crate::management::blackhole_dest_hash(&identity_hash);
2342
2343                let sig_prv = rns_crypto::ed25519::Ed25519PrivateKey::from_bytes(
2344                    &prv_key[32..64].try_into().unwrap(),
2345                );
2346                let sig_pub_bytes: [u8; 32] = identity.get_public_key().unwrap()[32..64]
2347                    .try_into()
2348                    .unwrap();
2349
2350                driver
2351                    .engine
2352                    .register_destination(bh_dest, rns_core::constants::DESTINATION_SINGLE);
2353                driver.link_manager.register_link_destination(
2354                    bh_dest,
2355                    sig_prv,
2356                    sig_pub_bytes,
2357                    crate::link_manager::ResourceStrategy::AcceptNone,
2358                );
2359                driver
2360                    .link_manager
2361                    .register_management_path(crate::management::list_path_hash());
2362                // Upstream 426673e9: requesting the list costs a 5.25 stamp,
2363                // below the `LR_COST` default.
2364                driver
2365                    .link_manager
2366                    .link_request_costs()
2367                    .set(bh_dest, rns_core::constants::BLACKHOLE_LINK_COST.as_f64());
2368
2369                log::info!(
2370                    "Blackhole list publishing enabled on {:02x?}",
2371                    &bh_dest[..4],
2372                );
2373            }
2374        }
2375
2376        // Set up probe responder if enabled
2377        if config.respond_to_probes && config.transport_enabled {
2378            let identity_hash = *identity.hash();
2379            let probe_dest = crate::management::probe_dest_hash(&identity_hash);
2380
2381            // Register as SINGLE destination in transport engine
2382            driver
2383                .engine
2384                .register_destination(probe_dest, rns_core::constants::DESTINATION_SINGLE);
2385            driver
2386                .local_destinations
2387                .insert(probe_dest, rns_core::constants::DESTINATION_SINGLE);
2388
2389            // Register PROVE_ALL proof strategy with transport identity
2390            let probe_identity = rns_crypto::identity::Identity::from_private_key(
2391                &identity.get_private_key().unwrap(),
2392            );
2393            driver.proof_strategies.insert(
2394                probe_dest,
2395                (
2396                    rns_core::types::ProofStrategy::ProveAll,
2397                    Some(probe_identity),
2398                ),
2399            );
2400
2401            driver.probe_responder_hash = Some(probe_dest);
2402
2403            log::info!("Probe responder enabled on {:02x?}", &probe_dest[..4],);
2404        }
2405
2406        // Spawn timer thread with configurable tick interval, unless the
2407        // driver keeps the tick itself (compact engine).
2408        if !crate::engine::FOLD_TIMERS {
2409            let timer_tx = tx.clone();
2410            let timer_interval = Arc::clone(&tick_interval_ms);
2411            thread::Builder::new()
2412                .name("rns-timer".into())
2413                .spawn(move || loop {
2414                    let ms = timer_interval.load(Ordering::Relaxed);
2415                    thread::sleep(Duration::from_millis(ms));
2416                    if timer_tx.send(Event::Tick).is_err() {
2417                        break; // receiver dropped
2418                    }
2419                })?;
2420        }
2421
2422        // Start LocalServer for shared instance clients if share_instance is enabled
2423        #[cfg(feature = "iface-local")]
2424        if config.share_instance {
2425            let local_server_config = LocalServerConfig {
2426                instance_name: config.instance_name.clone(),
2427                port: config.shared_instance_port,
2428                // Keys the listener on the driver loop when interface I/O is
2429                // folded; clients get ids of their own.
2430                interface_id: rns_core::transport::types::InterfaceId(
2431                    next_dynamic_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
2432                ),
2433            };
2434            match crate::interface::local::start_server(
2435                local_server_config,
2436                tx.clone(),
2437                next_dynamic_id.clone(),
2438            ) {
2439                Ok(control) => {
2440                    driver.register_listener_control(control);
2441                    log::info!(
2442                        "Local shared instance server started (instance={}, port={})",
2443                        config.instance_name,
2444                        config.shared_instance_port
2445                    );
2446                }
2447                Err(e) => {
2448                    log::error!("Failed to start local shared instance server: {}", e);
2449                }
2450            }
2451        }
2452
2453        // Start the RPC servers if share_instance is enabled: on threads, or
2454        // on the driver loop when the compact engine folds services.
2455        let serve_rpc_threads = config.share_instance && !crate::engine::FOLD_SERVICES;
2456        if config.share_instance && crate::engine::FOLD_SERVICES {
2457            let mut service = crate::driver::RpcService::new(rpc_auth_key, Arc::clone(tx.poller()));
2458            match std::net::TcpListener::bind(("127.0.0.1", config.rpc_port)).and_then(|listener| {
2459                service.add_listener(crate::driver::RpcListener::Tcp(listener))
2460            }) {
2461                Ok(()) => log::info!("RPC server started on 127.0.0.1:{}", config.rpc_port),
2462                Err(e) => log::error!("Failed to start RPC server: {}", e),
2463            }
2464            #[cfg(target_os = "linux")]
2465            match crate::rpc::bind_abstract_unix(&config.instance_name).and_then(|listener| {
2466                service.add_listener(crate::driver::RpcListener::Unix(listener))
2467            }) {
2468                Ok(()) => log::info!(
2469                    "RPC server started on abstract Unix socket for {}",
2470                    config.instance_name
2471                ),
2472                Err(e) => log::error!("Failed to start abstract Unix RPC server: {e}"),
2473            }
2474            driver.rpc_service = Some(service);
2475        }
2476        let rpc_server = if serve_rpc_threads {
2477            let rpc_addr = crate::rpc::RpcAddr::Tcp("127.0.0.1".into(), config.rpc_port);
2478            match crate::rpc::RpcServer::start(&rpc_addr, rpc_auth_key, tx.clone()) {
2479                Ok(server) => {
2480                    log::info!("RPC server started on 127.0.0.1:{}", config.rpc_port);
2481                    Some(server)
2482                }
2483                Err(e) => {
2484                    log::error!("Failed to start RPC server: {}", e);
2485                    None
2486                }
2487            }
2488        } else {
2489            None
2490        };
2491        #[cfg(target_os = "linux")]
2492        let rpc_unix_server = if serve_rpc_threads {
2493            match crate::rpc::RpcServer::start_abstract_unix(
2494                &config.instance_name,
2495                rpc_auth_key,
2496                tx.clone(),
2497            ) {
2498                Ok(server) => {
2499                    log::info!(
2500                        "RPC server started on abstract Unix socket for {}",
2501                        config.instance_name
2502                    );
2503                    Some(server)
2504                }
2505                Err(e) => {
2506                    log::error!("Failed to start abstract Unix RPC server: {e}");
2507                    None
2508                }
2509            }
2510        } else {
2511            None
2512        };
2513
2514        let announce_verify_queue = Arc::clone(&driver.announce_verify_queue);
2515        let verify_shutdown = Arc::clone(&driver.announce_verify_signal);
2516        let verify_shutdown_thread = Arc::clone(&verify_shutdown);
2517        let verify_tx = tx.clone();
2518        // The verification thread, unless the compact engine verifies
2519        // announces on its driver loop.
2520        let verify_handle = if crate::engine::FOLD_WORK {
2521            None
2522        } else {
2523            Some(
2524                thread::Builder::new()
2525                    .name("rns-verify".into())
2526                    .spawn(move || {
2527                        #[cfg(target_family = "unix")]
2528                        {
2529                            unsafe {
2530                                libc::nice(5);
2531                            }
2532                        }
2533
2534                        while !verify_shutdown_thread.stopped() {
2535                            let batch = {
2536                                let mut queue = announce_verify_queue
2537                                    .lock()
2538                                    .unwrap_or_else(|poisoned| poisoned.into_inner());
2539                                queue.take_pending(time::now())
2540                            };
2541
2542                            if batch.is_empty() {
2543                                // Until the driver queues an announce.
2544                                verify_shutdown_thread.wait();
2545                                continue;
2546                            }
2547
2548                            for (key, pending) in batch {
2549                                if verify_shutdown_thread.stopped() {
2550                                    break;
2551                                }
2552                                let event = match crate::driver::check_pending_announce(&pending) {
2553                                    crate::driver::AnnounceCheck::Valid {
2554                                        validated,
2555                                        sig_cache_key,
2556                                    } => Event::AnnounceVerified {
2557                                        key,
2558                                        validated,
2559                                        sig_cache_key,
2560                                    },
2561                                    crate::driver::AnnounceCheck::Invalid { sig_cache_key } => {
2562                                        Event::AnnounceVerifyFailed { key, sig_cache_key }
2563                                    }
2564                                };
2565                                if verify_tx.send(event).is_err() {
2566                                    return;
2567                                }
2568                            }
2569                        }
2570                    })?,
2571            )
2572        };
2573
2574        // Spawn the driver after startup has registered local destinations and static interfaces.
2575        // Interface readers can enqueue frames before this point; the bounded event queue preserves
2576        // ordering and backpressures instead of dropping startup traffic.
2577        log::info!("rns-net {} engine", crate::engine::ENGINE);
2578        // The compact engine persists transport state on the driver loop.
2579        if crate::engine::FOLD_TIMERS {
2580            driver.folded.persist_dir = transport_state_dir.clone();
2581        }
2582        let driver_handle = thread::Builder::new()
2583            .name("rns-driver".into())
2584            .spawn(move || {
2585                driver.run();
2586            })?;
2587
2588        // Match upstream's 12-hour background persistence cadence. The worker
2589        // performs disk serialization at low priority and never touches engine
2590        // state directly; it obtains a consistent snapshot through the driver.
2591        let (persist_handle, persist_stop) = if let Some(storage_dir) = transport_state_dir
2592            .clone()
2593            .filter(|_| !crate::engine::FOLD_TIMERS)
2594        {
2595            let persist_tx = tx.clone();
2596            let (stop_tx, stop_rx) = std::sync::mpsc::channel();
2597            let handle = thread::Builder::new()
2598                .name("rns-persist".into())
2599                .spawn(move || {
2600                    #[cfg(target_family = "unix")]
2601                    unsafe {
2602                        libc::nice(5);
2603                    }
2604
2605                    loop {
2606                        match stop_rx.recv_timeout(Duration::from_secs(12 * 60 * 60)) {
2607                            Ok(()) | Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
2608                            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}
2609                        }
2610                        let (response_tx, response_rx) = std::sync::mpsc::channel();
2611                        if persist_tx
2612                            .send(Event::query(
2613                                QueryRequest::TransportStateSnapshot,
2614                                response_tx,
2615                            ))
2616                            .is_err()
2617                        {
2618                            break;
2619                        }
2620                        let Ok(QueryResponse::TransportStateSnapshot(snapshot)) =
2621                            response_rx.recv()
2622                        else {
2623                            continue;
2624                        };
2625                        if let Err(err) =
2626                            storage::save_transport_state(&snapshot, &storage_dir, true)
2627                        {
2628                            log::warn!("failed to persist transport state: {}", err);
2629                        }
2630                    }
2631                })?;
2632            (Some(handle), Some(stop_tx))
2633        } else {
2634            (None, None)
2635        };
2636
2637        let mut node = RnsNode {
2638            tx,
2639            driver_handle: Some(driver_handle),
2640            verify_handle,
2641            verify_shutdown,
2642            persist_handle,
2643            persist_stop,
2644            rpc_server,
2645            #[cfg(target_os = "linux")]
2646            rpc_unix_server,
2647            tick_interval_ms,
2648            next_send_ticket: AtomicU64::new(0),
2649            encrypt_deferred_inline: std::thread::available_parallelism()
2650                .map_or(true, |cpus| cpus.get() == 1),
2651            accepting_work,
2652            probe_server,
2653            known_destinations_path: None,
2654            known_destinations_store: None,
2655            stamp_store: None,
2656            stamp_destinations,
2657            stamp_targets,
2658            stamper: Some(stamper),
2659            transport_state_dir,
2660            ratchet_store: config.ratchet_store,
2661            ratchet_expiry: config.ratchet_expiry,
2662            #[cfg(feature = "iface-local")]
2663            destination_types: Default::default(),
2664        };
2665        // Destination stamps live under `storage/stamps` next to the cache
2666        // directory, the layout `from_config_dirs` uses; a node without a
2667        // cache directory cannot stamp its announces.
2668        if let Some(storage_dir) = storage_dir {
2669            node.set_stamp_store(crate::stamps::DestinationStampStore::new(&storage_dir));
2670        }
2671        Ok(node)
2672    }
2673
2674    /// Query the driver for state information.
2675    pub fn query(&self, request: QueryRequest) -> Result<QueryResponse, SendError> {
2676        let (resp_tx, resp_rx) = std::sync::mpsc::channel();
2677        self.tx
2678            .send(Event::query(request, resp_tx))
2679            .map_err(|_| SendError)?;
2680        resp_rx.recv().map_err(|_| SendError)
2681    }
2682
2683    /// Attach a named interface from the current on-disk configuration.
2684    pub fn attach_interface(&self, name: &str) -> Result<Option<bool>, SendError> {
2685        self.manage_interface(crate::event::InterfaceManagementOperation::Attach, name)
2686    }
2687
2688    /// Detach a named interface and its spawned children.
2689    pub fn detach_interface(&self, name: &str) -> Result<Option<bool>, SendError> {
2690        self.manage_interface(crate::event::InterfaceManagementOperation::Detach, name)
2691    }
2692
2693    /// Detach and attach a named interface using the current on-disk configuration.
2694    pub fn reload_interface(&self, name: &str) -> Result<Option<bool>, SendError> {
2695        self.manage_interface(crate::event::InterfaceManagementOperation::Reload, name)
2696    }
2697
2698    fn manage_interface(
2699        &self,
2700        operation: crate::event::InterfaceManagementOperation,
2701        name: &str,
2702    ) -> Result<Option<bool>, SendError> {
2703        match self.query(QueryRequest::ManageInterface {
2704            operation,
2705            name: name.to_string(),
2706        })? {
2707            QueryResponse::InterfaceManagement(result) => Ok(result),
2708            _ => Err(SendError),
2709        }
2710    }
2711
2712    /// Snapshot active Link metadata, including the negotiated direct-data MDU.
2713    pub fn links(&self) -> Result<Vec<crate::event::LinkInfoEntry>, SendError> {
2714        match self.query(QueryRequest::Links)? {
2715            QueryResponse::Links(links) => Ok(links),
2716            _ => Err(SendError),
2717        }
2718    }
2719
2720    /// Snapshot one Link, including local send backlog, or None if unknown.
2721    pub fn query_link(
2722        &self,
2723        link_id: [u8; 16],
2724    ) -> Result<Option<crate::event::LinkInfoEntry>, SendError> {
2725        Ok(self
2726            .links()?
2727            .into_iter()
2728            .find(|link| link.link_id == link_id))
2729    }
2730
2731    /// The link and interface statistics the driver last published, read
2732    /// without a driver round trip.
2733    ///
2734    /// Unlike [`query`](Self::query), [`links`](Self::links) and
2735    /// [`query_link`](Self::query_link), which queue behind every earlier
2736    /// control event and answer exactly at that point, this returns at once
2737    /// even while the driver works through a long backlog or is blocked, for
2738    /// example in a callback. The price is freshness and ordering: the driver
2739    /// republishes on every tick (`global.tick_interval_ms`, 1 s by default)
2740    /// and as soon as a link or interface changes state, so the snapshot may
2741    /// lag by up to one tick, plus however long the driver has been blocked,
2742    /// and it is not a barrier: events sent before the call, sends included,
2743    /// may not be reflected yet. Before the first publish the snapshot is
2744    /// empty with `sequence == 0`.
2745    pub fn state_snapshot(&self) -> Arc<crate::state_snapshot::StateSnapshot> {
2746        self.tx.state_snapshot().load()
2747    }
2748
2749    /// Links as of the last published [`state_snapshot`](Self::state_snapshot).
2750    ///
2751    /// May lag by up to one tick and is not ordered after earlier events; use
2752    /// [`links`](Self::links) for an exact, ordered answer.
2753    pub fn snapshot_links(&self) -> Vec<crate::event::LinkInfoEntry> {
2754        self.state_snapshot().links.clone()
2755    }
2756
2757    /// One link as of the last published
2758    /// [`state_snapshot`](Self::state_snapshot), or None if it was not known
2759    /// then.
2760    ///
2761    /// May lag by up to one tick and is not ordered after earlier events; use
2762    /// [`query_link`](Self::query_link) for an exact, ordered answer.
2763    pub fn snapshot_link(&self, link_id: [u8; 16]) -> Option<crate::event::LinkInfoEntry> {
2764        self.state_snapshot().link(&link_id).cloned()
2765    }
2766
2767    /// Interface statistics as of the last published
2768    /// [`state_snapshot`](Self::state_snapshot).
2769    ///
2770    /// May lag by up to one tick and is not ordered after earlier events; use
2771    /// `query(QueryRequest::InterfaceStats)` for an exact, ordered answer.
2772    pub fn snapshot_interfaces(&self) -> crate::event::InterfaceStatsResponse {
2773        self.state_snapshot().interfaces.clone()
2774    }
2775
2776    /// Return the slowest positive bitrate among currently registered interfaces.
2777    ///
2778    /// Returns `Ok(None)` when no registered interface advertises a usable
2779    /// bitrate.
2780    pub fn lowest_interface_bitrate(&self) -> Result<Option<u64>, SendError> {
2781        match self.query(QueryRequest::LowestInterfaceBitrate)? {
2782            QueryResponse::LowestInterfaceBitrate(bitrate) => Ok(bitrate),
2783            _ => Err(SendError),
2784        }
2785    }
2786
2787    /// Return the adaptive medium path timeout advertised by the transport engine.
2788    ///
2789    /// The estimate covers a full MTU round trip on the slowest registered
2790    /// positive-bitrate interface plus the per-hop allowance, or is zero when
2791    /// no usable bitrate is known.
2792    pub fn medium_path_timeout(&self) -> Result<f64, SendError> {
2793        match self.query(QueryRequest::MediumPathTimeout)? {
2794            QueryResponse::MediumPathTimeout(timeout) => Ok(timeout),
2795            _ => Err(SendError),
2796        }
2797    }
2798
2799    /// Enter drain mode and stop admitting new work.
2800    pub fn begin_drain(&self, timeout: Duration) -> Result<(), SendError> {
2801        // Draining is one-way. Refuse new work from this call on, before the
2802        // driver has processed the event.
2803        self.accepting_work.store(false, Ordering::Release);
2804        self.tx
2805            .send(Event::BeginDrain { timeout })
2806            .map_err(|_| SendError)
2807    }
2808
2809    /// Query current drain/lifecycle status.
2810    pub fn drain_status(&self) -> Result<crate::event::DrainStatus, SendError> {
2811        match self.query(QueryRequest::DrainStatus)? {
2812            QueryResponse::DrainStatus(status) => Ok(status),
2813            _ => Err(SendError),
2814        }
2815    }
2816
2817    fn reject_new_work_if_draining(&self) -> Result<(), SendError> {
2818        // A flag the driver clears when draining starts, not a driver query:
2819        // this runs on every send.
2820        if self.accepting_work.load(Ordering::Acquire) {
2821            Ok(())
2822        } else {
2823            Err(SendError)
2824        }
2825    }
2826
2827    /// Send a raw outbound packet.
2828    pub fn send_raw(
2829        &self,
2830        raw: Vec<u8>,
2831        dest_type: u8,
2832        attached_interface: Option<rns_core::transport::types::InterfaceId>,
2833    ) -> Result<(), SendError> {
2834        self.tx
2835            .send(Event::SendOutbound {
2836                raw,
2837                dest_type,
2838                attached_interface,
2839            })
2840            .map_err(|_| SendError)
2841    }
2842
2843    /// Register a local destination with the transport engine.
2844    pub fn register_destination(
2845        &self,
2846        dest_hash: [u8; 16],
2847        dest_type: u8,
2848    ) -> Result<(), SendError> {
2849        self.tx
2850            .send(Event::RegisterDestination {
2851                dest_hash,
2852                dest_type,
2853            })
2854            .map_err(|_| SendError)
2855    }
2856
2857    /// Deregister a local destination.
2858    pub fn deregister_destination(&self, dest_hash: [u8; 16]) -> Result<(), SendError> {
2859        self.tx
2860            .send(Event::DeregisterDestination { dest_hash })
2861            .map_err(|_| SendError)
2862    }
2863
2864    /// Deregister a link destination (stop accepting incoming links).
2865    pub fn deregister_link_destination(&self, dest_hash: [u8; 16]) -> Result<(), SendError> {
2866        self.tx
2867            .send(Event::DeregisterLinkDestination { dest_hash })
2868            .map_err(|_| SendError)
2869    }
2870
2871    /// Register a link destination that can accept incoming links.
2872    ///
2873    /// `dest_hash`: the destination hash
2874    /// `sig_prv_bytes`: Ed25519 private signing key (32 bytes)
2875    /// `sig_pub_bytes`: Ed25519 public signing key (32 bytes)
2876    pub fn register_link_destination(
2877        &self,
2878        dest_hash: [u8; 16],
2879        sig_prv_bytes: [u8; 32],
2880        sig_pub_bytes: [u8; 32],
2881        resource_strategy: u8,
2882    ) -> Result<(), SendError> {
2883        self.register_link_destination_with_max_request_size(
2884            dest_hash,
2885            sig_prv_bytes,
2886            sig_pub_bytes,
2887            resource_strategy,
2888            None,
2889        )
2890    }
2891
2892    /// Register a link destination with an optional maximum accepted request size.
2893    pub fn register_link_destination_with_max_request_size(
2894        &self,
2895        dest_hash: [u8; 16],
2896        sig_prv_bytes: [u8; 32],
2897        sig_pub_bytes: [u8; 32],
2898        resource_strategy: u8,
2899        max_request_size: Option<usize>,
2900    ) -> Result<(), SendError> {
2901        self.tx
2902            .send(Event::RegisterLinkDestination {
2903                dest_hash,
2904                sig_prv_bytes,
2905                sig_pub_bytes,
2906                resource_strategy,
2907                max_request_size,
2908            })
2909            .map_err(|_| SendError)
2910    }
2911
2912    /// Set the required link establishment stamp cost for a destination.
2913    ///
2914    /// Mirrors `Destination.set_link_cost`: the cost is clamped to
2915    /// `0..=LINK_COST_CAP` and the clamped value is returned. Set it to 0 to
2916    /// disable. The cost is carried in the destination's announce extension
2917    /// (emitted when destination stamping is on) so initiators grind their
2918    /// link request stamps to it, and, when link request stamps are enforced,
2919    /// incoming link requests whose stamp value is below it are dropped.
2920    pub fn set_link_cost(&self, dest_hash: [u8; 16], cost: f64) -> f64 {
2921        self.tx.link_request_costs().set(dest_hash, cost).as_f64()
2922    }
2923
2924    /// The link request stamp cost announced and required for a destination.
2925    ///
2926    /// Mirrors `Destination.get_link_cost`: the configured cost, or the
2927    /// default `LINK_REQUEST_STAMP_COST` (`Transport.LR_COST`) when none was
2928    /// set. Costs are fractional since upstream 796ef2d4 and held on the
2929    /// 1/256 grid they are announced and checked on, so 14.2 reads back as
2930    /// 14.19921875.
2931    pub fn link_cost(&self, dest_hash: &[u8; 16]) -> f64 {
2932        self.tx.link_request_costs().get(dest_hash).as_f64()
2933    }
2934
2935    /// Override `Transport.LR_STAMPS_ENFORCED` for this node.
2936    ///
2937    /// When enabled, incoming link requests whose stamp value is below the
2938    /// destination's [`link_cost`](Self::link_cost) are dropped. Defaults to
2939    /// `rns_core::constants::LR_STAMPS_ENFORCED` (off, as upstream).
2940    pub fn set_link_request_stamps_enforced(&self, enforced: bool) {
2941        self.tx.link_request_costs().set_enforced(enforced);
2942    }
2943
2944    /// Whether incoming link request stamps are enforced.
2945    pub fn link_request_stamps_enforced(&self) -> bool {
2946        self.tx.link_request_costs().enforced()
2947    }
2948
2949    /// Register a request handler for a given path on established links.
2950    pub fn register_request_handler<F>(
2951        &self,
2952        path: &str,
2953        allowed_list: Option<Vec<[u8; 16]>>,
2954        handler: F,
2955    ) -> Result<(), SendError>
2956    where
2957        F: Fn([u8; 16], &str, &[u8], Option<&([u8; 16], [u8; 64])>) -> Option<Vec<u8>>
2958            + Send
2959            + 'static,
2960    {
2961        self.tx
2962            .send(Event::RegisterRequestHandler {
2963                path: path.to_string(),
2964                allowed_list,
2965                handler: Box::new(handler),
2966            })
2967            .map_err(|_| SendError)
2968    }
2969
2970    /// Register a request handler that can return resource responses with metadata.
2971    pub fn register_request_handler_response<F>(
2972        &self,
2973        path: &str,
2974        allowed_list: Option<Vec<[u8; 16]>>,
2975        handler: F,
2976    ) -> Result<(), SendError>
2977    where
2978        F: Fn(
2979                [u8; 16],
2980                &str,
2981                &[u8],
2982                Option<&([u8; 16], [u8; 64])>,
2983            ) -> Option<crate::link_manager::RequestResponse>
2984            + Send
2985            + 'static,
2986    {
2987        self.tx
2988            .send(Event::RegisterRequestHandlerResponse {
2989                path: path.to_string(),
2990                allowed_list,
2991                handler: Box::new(handler),
2992            })
2993            .map_err(|_| SendError)
2994    }
2995
2996    /// Register a request handler whose response is completed asynchronously
2997    /// with [`RnsNode::send_deferred_response`].
2998    pub fn register_deferred_request_handler<F>(
2999        &self,
3000        path: &str,
3001        allowed_list: Option<Vec<[u8; 16]>>,
3002        handler: F,
3003    ) -> Result<(), SendError>
3004    where
3005        F: Fn([u8; 16], &str, [u8; 16], &[u8], Option<&([u8; 16], [u8; 64])>) + Send + 'static,
3006    {
3007        self.tx
3008            .send(Event::RegisterDeferredRequestHandler {
3009                path: path.to_string(),
3010                allowed_list,
3011                handler: Box::new(handler),
3012            })
3013            .map_err(|_| SendError)
3014    }
3015
3016    /// Complete a request accepted by a deferred request handler.
3017    pub fn send_deferred_response(
3018        &self,
3019        link_id: [u8; 16],
3020        request_id: [u8; 16],
3021        data: Vec<u8>,
3022    ) -> Result<(), SendError> {
3023        self.tx
3024            .send(Event::SendDeferredResponse {
3025                link_id,
3026                request_id,
3027                data,
3028            })
3029            .map_err(|_| SendError)
3030    }
3031
3032    /// Create an outbound link to a destination.
3033    ///
3034    /// Returns the link_id on success.
3035    pub fn create_link(
3036        &self,
3037        dest_hash: [u8; 16],
3038        dest_sig_pub_bytes: [u8; 32],
3039    ) -> Result<[u8; 16], SendError> {
3040        self.reject_new_work_if_draining()?;
3041        let (response_tx, response_rx) = std::sync::mpsc::channel();
3042        self.tx
3043            .send(Event::CreateLink {
3044                dest_hash,
3045                dest_sig_pub_bytes,
3046                response_tx,
3047            })
3048            .map_err(|_| SendError)?;
3049        let link_id = response_rx.recv().map_err(|_| SendError)?;
3050        if link_id == [0u8; 16] {
3051            Err(SendError)
3052        } else {
3053            Ok(link_id)
3054        }
3055    }
3056
3057    /// Send a request on an established link.
3058    pub fn send_request(
3059        &self,
3060        link_id: [u8; 16],
3061        path: &str,
3062        data: &[u8],
3063    ) -> Result<(), SendError> {
3064        self.send_request_with_max_response_size(link_id, path, data, None)
3065    }
3066
3067    /// Send a request with an optional maximum accepted response size.
3068    pub fn send_request_with_max_response_size(
3069        &self,
3070        link_id: [u8; 16],
3071        path: &str,
3072        data: &[u8],
3073        max_response_size: Option<usize>,
3074    ) -> Result<(), SendError> {
3075        self.reject_new_work_if_draining()?;
3076        self.tx
3077            .send(Event::SendRequest {
3078                link_id,
3079                path: path.to_string(),
3080                data: data.to_vec(),
3081                max_response_size,
3082            })
3083            .map_err(|_| SendError)
3084    }
3085
3086    /// Identify on a link (reveal identity to remote peer).
3087    pub fn identify_on_link(
3088        &self,
3089        link_id: [u8; 16],
3090        identity_prv_key: [u8; 64],
3091    ) -> Result<(), SendError> {
3092        self.reject_new_work_if_draining()?;
3093        self.tx
3094            .send(Event::IdentifyOnLink {
3095                link_id,
3096                identity_prv_key,
3097            })
3098            .map_err(|_| SendError)
3099    }
3100
3101    /// Tear down a link.
3102    pub fn teardown_link(&self, link_id: [u8; 16]) -> Result<(), SendError> {
3103        self.tx
3104            .send(Event::TeardownLink { link_id })
3105            .map_err(|_| SendError)
3106    }
3107
3108    /// Send a resource on an established link.
3109    pub fn send_resource(
3110        &self,
3111        link_id: [u8; 16],
3112        data: Vec<u8>,
3113        metadata: Option<Vec<u8>>,
3114    ) -> Result<(), SendError> {
3115        self.send_resource_with_auto_compress(link_id, data, metadata, true)
3116    }
3117
3118    /// Send a resource on an established link, controlling automatic compression.
3119    ///
3120    /// Set `auto_compress` to `false` when the application knows its payload is
3121    /// already compressed or encrypted. This skips the compression attempt;
3122    /// link encryption and Resource integrity checks still apply. With `true`,
3123    /// eligible payloads are fully compressed and the result is used only when
3124    /// smaller. Even a rejected attempt costs CPU, while disabling compression
3125    /// for compressible content can substantially increase transmitted bytes.
3126    ///
3127    /// Buffered sends of at least 16 KiB that fit one Resource segment can
3128    /// prepare compression on a node-owned worker. Its admission budget is eight
3129    /// jobs and 8 MiB of owned input capacity plus reserved compressed output.
3130    /// Codec workspace and the running job's temporary input are additional.
3131    /// Saturation, small/split Resources and worker startup failure use synchronous
3132    /// preparation, preserving earlier buffered sends on the same link. This
3133    /// improves responsiveness for eligible sends; it does not provide a latency
3134    /// bound under overload. Reader transfers and request responses keep their
3135    /// existing synchronous preparation paths.
3136    pub fn send_resource_with_auto_compress(
3137        &self,
3138        link_id: [u8; 16],
3139        data: Vec<u8>,
3140        metadata: Option<Vec<u8>>,
3141        auto_compress: bool,
3142    ) -> Result<(), SendError> {
3143        self.send_resource_with_options(
3144            link_id,
3145            data,
3146            metadata,
3147            crate::resource::ResourceSendOptions {
3148                auto_compress,
3149                ..Default::default()
3150            },
3151        )
3152    }
3153
3154    /// Send a resource on an established link with explicit send options,
3155    /// including the application flag the receiver sees with the data.
3156    pub fn send_resource_with_options(
3157        &self,
3158        link_id: [u8; 16],
3159        data: Vec<u8>,
3160        metadata: Option<Vec<u8>>,
3161        options: crate::resource::ResourceSendOptions,
3162    ) -> Result<(), SendError> {
3163        self.reject_new_work_if_draining()?;
3164        self.tx
3165            .send(Event::SendResource {
3166                link_id,
3167                data,
3168                metadata,
3169                options,
3170            })
3171            .map_err(|_| SendError)
3172    }
3173
3174    /// Send a Resource from a sequential reader without retaining the full
3175    /// logical payload in memory.
3176    pub fn send_resource_reader<R: Read + Send + 'static>(
3177        &self,
3178        link_id: [u8; 16],
3179        reader: R,
3180        declared_length: u64,
3181        metadata: Option<Vec<u8>>,
3182        auto_compress: bool,
3183    ) -> Result<crate::resource::ResourceTransferId, SendError> {
3184        self.send_resource_reader_with_options(
3185            link_id,
3186            reader,
3187            declared_length,
3188            metadata,
3189            crate::resource::ResourceSendOptions {
3190                auto_compress,
3191                ..Default::default()
3192            },
3193        )
3194    }
3195
3196    /// Send a Resource from a sequential reader with explicit send options.
3197    pub fn send_resource_reader_with_options<R: Read + Send + 'static>(
3198        &self,
3199        link_id: [u8; 16],
3200        reader: R,
3201        declared_length: u64,
3202        metadata: Option<Vec<u8>>,
3203        options: crate::resource::ResourceSendOptions,
3204    ) -> Result<crate::resource::ResourceTransferId, SendError> {
3205        self.reject_new_work_if_draining()?;
3206        let transfer_id = crate::resource::ResourceTransferId(
3207            NEXT_RESOURCE_TRANSFER_ID.fetch_add(1, Ordering::Relaxed),
3208        );
3209        self.tx
3210            .send(Event::SendResourceStream {
3211                link_id,
3212                transfer_id,
3213                reader: Box::new(reader),
3214                declared_length,
3215                metadata,
3216                options,
3217            })
3218            .map_err(|_| SendError)?;
3219        Ok(transfer_id)
3220    }
3221
3222    /// Send a file as a bounded-memory streaming Resource.
3223    pub fn send_resource_file(
3224        &self,
3225        link_id: [u8; 16],
3226        path: impl AsRef<Path>,
3227        metadata: Option<Vec<u8>>,
3228        auto_compress: bool,
3229    ) -> io::Result<crate::resource::ResourceTransferId> {
3230        let file = std::fs::File::open(path.as_ref())?;
3231        let length = file.metadata()?.len();
3232        self.send_resource_reader(link_id, file, length, metadata, auto_compress)
3233            .map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "RNS driver shut down"))
3234    }
3235
3236    /// Select delivery policy for independent incoming Resources on a link.
3237    pub fn set_resource_receive_mode(
3238        &self,
3239        link_id: [u8; 16],
3240        mode: crate::resource::ResourceReceiveMode,
3241    ) -> Result<(), SendError> {
3242        self.tx
3243            .send(Event::SetResourceReceiveMode { link_id, mode })
3244            .map_err(|_| SendError)
3245    }
3246
3247    /// Set the resource acceptance strategy for a link.
3248    ///
3249    /// 0 = AcceptNone, 1 = AcceptAll, 2 = AcceptApp
3250    pub fn set_resource_strategy(&self, link_id: [u8; 16], strategy: u8) -> Result<(), SendError> {
3251        self.tx
3252            .send(Event::SetResourceStrategy { link_id, strategy })
3253            .map_err(|_| SendError)
3254    }
3255
3256    /// Accept or reject a pending resource (for AcceptApp strategy).
3257    pub fn accept_resource(
3258        &self,
3259        link_id: [u8; 16],
3260        resource_hash: Vec<u8>,
3261        accept: bool,
3262    ) -> Result<(), SendError> {
3263        if accept {
3264            self.reject_new_work_if_draining()?;
3265        }
3266        self.tx
3267            .send(Event::AcceptResource {
3268                link_id,
3269                resource_hash,
3270                accept,
3271            })
3272            .map_err(|_| SendError)
3273    }
3274
3275    /// Send a channel message on a link (compatibility API).
3276    pub fn send_channel_message(
3277        &self,
3278        link_id: [u8; 16],
3279        msgtype: u16,
3280        payload: Vec<u8>,
3281    ) -> Result<(), SendError> {
3282        self.try_send_channel_message(link_id, msgtype, payload)
3283            .map_err(|_| SendError)
3284    }
3285
3286    /// Attempt to enqueue a channel message, preserving temporary backpressure.
3287    /// A NotReady result means no sequence was reserved; callers can retry the
3288    /// same payload when capacity is available. Other failures are terminal.
3289    pub fn try_send_channel_message(
3290        &self,
3291        link_id: [u8; 16],
3292        msgtype: u16,
3293        payload: Vec<u8>,
3294    ) -> Result<(), ChannelSendError> {
3295        self.reject_new_work_if_draining()
3296            .map_err(|_| ChannelSendError::Unavailable)?;
3297        let (response_tx, response_rx) = std::sync::mpsc::channel();
3298        self.tx
3299            .send(Event::SendChannelMessage {
3300                link_id,
3301                msgtype,
3302                payload,
3303                response_tx,
3304            })
3305            .map_err(|_| ChannelSendError::Unavailable)?;
3306        response_rx
3307            .recv()
3308            .map_err(|_| ChannelSendError::Unavailable)?
3309            .map_err(ChannelSendError::from)
3310    }
3311
3312    /// Propose a direct P2P connection to a peer via NAT hole punching.
3313    ///
3314    /// The link must be active and connected through a backbone node.
3315    /// If successful, a direct UDP connection will be established, bypassing the backbone.
3316    pub fn propose_direct_connect(&self, link_id: [u8; 16]) -> Result<(), SendError> {
3317        self.reject_new_work_if_draining()?;
3318        self.tx
3319            .send(Event::ProposeDirectConnect { link_id })
3320            .map_err(|_| SendError)
3321    }
3322
3323    /// Set the policy for handling incoming direct-connect proposals.
3324    pub fn set_direct_connect_policy(
3325        &self,
3326        policy: crate::holepunch::orchestrator::HolePunchPolicy,
3327    ) -> Result<(), SendError> {
3328        self.tx
3329            .send(Event::SetDirectConnectPolicy { policy })
3330            .map_err(|_| SendError)
3331    }
3332
3333    /// Wait for capacity and transmit a Link packet through its interface writer.
3334    ///
3335    /// Success means the underlying transport accepted the complete packet,
3336    /// not that the peer received or acknowledged it. Cancellation before
3337    /// admission sends nothing; after admission, dropping this future does not
3338    /// cancel transmission. This future is independent of any async runtime.
3339    /// The driver's configured event capacity also bounds the total number of
3340    /// admitted confirmed sends (Links, packets and announcements), including
3341    /// frames waiting for an interface or write.
3342    /// A transmission error can follow a partial write; only QueueFull from the
3343    /// try API guarantees that retrying cannot duplicate an admitted packet.
3344    pub async fn send_on_link(
3345        &self,
3346        link_id: [u8; 16],
3347        data: Vec<u8>,
3348        context: u8,
3349    ) -> Result<(), crate::link_send::LinkSendError> {
3350        use crate::link_send::Completion;
3351        let tracking = self.tx.link_send_pool().track(link_id, true);
3352        let permit = self.tx.link_send_pool().acquire().await?;
3353        let (completion, receipt) = Completion::new_tracked(permit, tracking);
3354        self.tx
3355            .send_async(Event::SendLinkTracked {
3356                link_id,
3357                data,
3358                context,
3359                completion,
3360            })
3361            .await?;
3362        receipt.await
3363    }
3364
3365    /// Try to admit a Link send without waiting for capacity.
3366    ///
3367    /// A successful return is a receipt, not a transmission result. Await the
3368    /// receipt for exactly the same completion contract as `send_on_link`.
3369    /// QueueFull means nothing was admitted and the caller may retry.
3370    pub fn try_send_on_link(
3371        &self,
3372        link_id: [u8; 16],
3373        data: Vec<u8>,
3374        context: u8,
3375    ) -> Result<crate::link_send::LinkSendReceipt, crate::link_send::LinkSendError> {
3376        use crate::link_send::{Completion, LinkSendError};
3377        let permit = self.tx.link_send_pool().try_acquire()?;
3378        let tracking = self.tx.link_send_pool().track(link_id, false);
3379        let (completion, receipt) = Completion::new_tracked(permit, tracking);
3380        self.tx
3381            .try_send(Event::SendLinkTracked {
3382                link_id,
3383                data,
3384                context,
3385                completion,
3386            })
3387            .map_err(|error| match error {
3388                std::sync::mpsc::TrySendError::Full(_) => LinkSendError::QueueFull,
3389                std::sync::mpsc::TrySendError::Disconnected(_) => LinkSendError::DriverStopped,
3390            })?;
3391        Ok(receipt)
3392    }
3393
3394    /// Admit a best-effort direct Link datagram without blocking on queue space.
3395    ///
3396    /// Success means the driver accepted and encoded the packet. It does not
3397    /// imply remote delivery, acknowledgement, or interface transmission.
3398    pub fn try_send_link_datagram(
3399        &self,
3400        link_id: [u8; 16],
3401        data: Vec<u8>,
3402        context: u8,
3403        timeout: Duration,
3404    ) -> Result<(), crate::event::LinkDatagramError> {
3405        use crate::event::LinkDatagramError;
3406        if self.reject_new_work_if_draining().is_err() {
3407            return Err(LinkDatagramError::DriverStopped);
3408        }
3409        let (response_tx, response_rx) = std::sync::mpsc::channel();
3410        self.tx
3411            .try_send(Event::SendOnLink {
3412                link_id,
3413                data,
3414                context,
3415                response_tx: Some(response_tx),
3416            })
3417            .map_err(|error| match error {
3418                std::sync::mpsc::TrySendError::Full(_) => LinkDatagramError::DriverQueueFull,
3419                std::sync::mpsc::TrySendError::Disconnected(_) => LinkDatagramError::DriverStopped,
3420            })?;
3421        response_rx
3422            .recv_timeout(timeout)
3423            .map_err(|error| match error {
3424                std::sync::mpsc::RecvTimeoutError::Timeout => LinkDatagramError::AdmissionTimedOut,
3425                std::sync::mpsc::RecvTimeoutError::Disconnected => LinkDatagramError::DriverStopped,
3426            })?
3427    }
3428
3429    /// Attach the same private ratchet owner used by the application's receiver.
3430    /// Reattaching that Arc is idempotent; conflicting owners are rejected. The
3431    /// owner remains attached across destination deregistration/re-registration.
3432    pub fn attach_local_ratchets(&self, owner: Arc<crate::LocalRatchets>) -> io::Result<()> {
3433        let mut owners = self.tx.local_ratchets().lock().unwrap();
3434        if let Some(existing) = owners.get(&owner.destination_hash()) {
3435            if !Arc::ptr_eq(existing, &owner) {
3436                return Err(io::Error::new(
3437                    io::ErrorKind::AlreadyExists,
3438                    "destination already has a ratchet owner",
3439                ));
3440            }
3441        }
3442        owners.insert(owner.destination_hash(), owner);
3443        Ok(())
3444    }
3445
3446    /// Wait for capacity, then transmit an announcement on all selected local interfaces.
3447    ///
3448    /// Success means all selected writers accepted the complete packet, not
3449    /// remote delivery or subsequent relay. An error can follow partial delivery
3450    /// on one or more interfaces. No eligible interface returns `NoRoute`.
3451    /// Cancelling before admission sends nothing; after admission, transmission
3452    /// continues. Shared-client replay metadata travels in the same admitted
3453    /// event as the packet, never in a separate event from a rejected send.
3454    pub async fn announce(
3455        &self,
3456        dest: &crate::destination::Destination,
3457        identity: &Identity,
3458        app_data: Option<&[u8]>,
3459    ) -> Result<(), crate::TransmissionError> {
3460        let permit = self.tx.link_send_pool().acquire().await?;
3461        if self.announce_needs_stamp(dest, identity) {
3462            // Destination.announce with STAMP_DESTINATIONS and no stamp yet:
3463            // the stamper grinds it and submits the announce; the receipt
3464            // resolves when that announce has been written.
3465            let (completion, receipt) = crate::link_send::Completion::new(permit);
3466            self.enqueue_stamped_announce(dest, identity, app_data, Some(completion))
3467                .map_err(|_| crate::TransmissionError::QueueFull)?;
3468            return receipt.await;
3469        }
3470        let (packet, replay) = self
3471            .build_announce(dest, identity, app_data)
3472            .map_err(|_| crate::TransmissionError::PacketBuildFailed)?;
3473        let (completion, receipt) = crate::link_send::Completion::new(permit);
3474        self.tx
3475            .send_async(Event::SendOutboundTracked {
3476                raw: packet.raw,
3477                dest_type: dest.dest_type.to_wire_constant(),
3478                replay,
3479                completion,
3480            })
3481            .await?;
3482        receipt.await
3483    }
3484
3485    /// Try admission immediately; await the receipt for the exact same result
3486    /// as `announce`. `QueueFull` guarantees nothing was admitted.
3487    pub fn try_announce(
3488        &self,
3489        dest: &crate::destination::Destination,
3490        identity: &Identity,
3491        app_data: Option<&[u8]>,
3492    ) -> Result<crate::TransmissionReceipt, crate::TransmissionError> {
3493        let permit = self.tx.link_send_pool().try_acquire()?;
3494        if self.announce_needs_stamp(dest, identity) {
3495            // Deferred to the stamper; the receipt resolves once the stamped
3496            // announce has been written, or with `Interrupted` if a newer
3497            // announce for the same destination replaces this one in the
3498            // queue.
3499            let (completion, receipt) = crate::link_send::Completion::new(permit);
3500            self.enqueue_stamped_announce(dest, identity, app_data, Some(completion))
3501                .map_err(|_| crate::TransmissionError::QueueFull)?;
3502            return Ok(receipt);
3503        }
3504        let (packet, replay) = self
3505            .build_announce(dest, identity, app_data)
3506            .map_err(|_| crate::TransmissionError::PacketBuildFailed)?;
3507        let (completion, receipt) = crate::link_send::Completion::new(permit);
3508        self.try_confirmed_event(Event::SendOutboundTracked {
3509            raw: packet.raw,
3510            dest_type: dest.dest_type.to_wire_constant(),
3511            replay,
3512            completion,
3513        })?;
3514        Ok(receipt)
3515    }
3516
3517    /// Wait for capacity and local transmission, returning the hash for proof tracking.
3518    ///
3519    /// Uses the same all-selected-writers contract as `announce` and the
3520    /// same admission/completion capacity as `send_on_link`. No remote delivery
3521    /// is promised; errors may follow partial transmission. Dropping an admitted
3522    /// future does not cancel its send. No async runtime is required.
3523    pub async fn send_packet(
3524        &self,
3525        dest: &crate::destination::Destination,
3526        data: &[u8],
3527    ) -> Result<rns_core::types::PacketHash, crate::TransmissionError> {
3528        let permit = self.tx.link_send_pool().acquire().await?;
3529        let packet = self
3530            .build_packet(dest, data)
3531            .map_err(|_| crate::TransmissionError::PacketBuildFailed)?;
3532        let hash = rns_core::types::PacketHash(packet.packet_hash);
3533        let (completion, receipt) = crate::link_send::Completion::new(permit);
3534        self.tx
3535            .send_async(Event::SendOutboundTracked {
3536                raw: packet.raw,
3537                dest_type: dest.dest_type.to_wire_constant(),
3538                replay: None,
3539                completion,
3540            })
3541            .await?;
3542        receipt.await?;
3543        Ok(hash)
3544    }
3545
3546    /// Try admission immediately. The receipt exposes the packet hash and
3547    /// resolves to the same result as `send_packet`, after local writes.
3548    /// `QueueFull` guarantees nothing was admitted and is safe to retry.
3549    pub fn try_send_packet(
3550        &self,
3551        dest: &crate::destination::Destination,
3552        data: &[u8],
3553    ) -> Result<crate::PacketSendReceipt, crate::TransmissionError> {
3554        let permit = self.tx.link_send_pool().try_acquire()?;
3555        let packet = self
3556            .build_packet(dest, data)
3557            .map_err(|_| crate::TransmissionError::PacketBuildFailed)?;
3558        let hash = rns_core::types::PacketHash(packet.packet_hash);
3559        let (completion, receipt) = crate::link_send::Completion::new(permit);
3560        self.try_confirmed_event(Event::SendOutboundTracked {
3561            raw: packet.raw,
3562            dest_type: dest.dest_type.to_wire_constant(),
3563            replay: None,
3564            completion,
3565        })?;
3566        Ok(crate::PacketSendReceipt {
3567            completion: receipt,
3568            hash,
3569        })
3570    }
3571
3572    fn try_confirmed_event(&self, event: Event) -> Result<(), crate::TransmissionError> {
3573        self.tx.try_send(event).map_err(|error| match error {
3574            std::sync::mpsc::TrySendError::Full(_) => crate::TransmissionError::QueueFull,
3575            std::sync::mpsc::TrySendError::Disconnected(_) => {
3576                crate::TransmissionError::DriverStopped
3577            }
3578        })
3579    }
3580
3581    /// Build and broadcast an announce for a destination (queue-only compatibility API).
3582    ///
3583    /// Success means driver submission, not transmission. Use `announce`
3584    /// or `try_announce` for confirmed local transmission.
3585    ///
3586    /// The identity is used to sign the announce. Must be the identity that
3587    /// owns the destination (i.e. `identity.hash()` matches `dest.identity_hash`).
3588    pub fn announce_queued(
3589        &self,
3590        dest: &crate::destination::Destination,
3591        identity: &Identity,
3592        app_data: Option<&[u8]>,
3593    ) -> Result<(), SendError> {
3594        self.reject_new_work_if_draining()?;
3595        if self.announce_needs_stamp(dest, identity) {
3596            // Destination.announce with STAMP_DESTINATIONS and no stamp yet
3597            // queues the announce for background stamping and emission.
3598            return self
3599                .enqueue_stamped_announce(dest, identity, app_data, None)
3600                .map_err(|_| SendError);
3601        }
3602        let (packet, replay) = self.build_announce(dest, identity, app_data)?;
3603        if let Some(replay) = replay {
3604            self.tx
3605                .send(Event::StoreSharedAnnounce {
3606                    dest_hash: replay.dest_hash,
3607                    name_hash: replay.name_hash,
3608                    identity_prv_key: replay.identity_prv_key,
3609                    app_data: replay.app_data,
3610                })
3611                .map_err(|_| SendError)?;
3612        }
3613        self.send_raw(packet.raw, dest.dest_type.to_wire_constant(), None)
3614    }
3615
3616    fn build_announce(
3617        &self,
3618        dest: &crate::destination::Destination,
3619        identity: &Identity,
3620        app_data: Option<&[u8]>,
3621    ) -> Result<
3622        (
3623            rns_core::packet::RawPacket,
3624            Option<Box<crate::common::event::AnnounceReplay>>,
3625        ),
3626        SendError,
3627    > {
3628        build_announce_packet(
3629            &self.tx,
3630            self.stamp_store.as_ref(),
3631            self.stamp_destinations.load(Ordering::Relaxed),
3632            self.stamp_targets,
3633            dest,
3634            identity,
3635            app_data,
3636        )
3637    }
3638
3639    /// Whether `Destination.announce` would hand this announce to the
3640    /// stamper: destination stamping is on and no persisted stamp reaches
3641    /// the destination's target value (Destination.py `_announce`).
3642    fn announce_needs_stamp(
3643        &self,
3644        dest: &crate::destination::Destination,
3645        identity: &Identity,
3646    ) -> bool {
3647        if !self.stamp_destinations.load(Ordering::Relaxed) || self.stamper.is_none() {
3648            return false;
3649        }
3650        let (Some(store), Some(public_key)) =
3651            (self.stamp_store.as_ref(), identity.get_public_key())
3652        else {
3653            return false;
3654        };
3655        let name_hash = rns_core::destination::name_hash(
3656            &dest.app_name,
3657            &dest.aspects.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
3658        );
3659        let material =
3660            crate::stamps::destination_stamp_material(&dest.hash.0, &public_key, &name_hash);
3661        store
3662            .load(
3663                &dest.hash.0,
3664                &material,
3665                self.stamp_targets.destination_target(dest),
3666            )
3667            .is_none()
3668    }
3669
3670    /// `Transport.enqueue_stamped_announce`: the worker grinds the stamp and
3671    /// emits the announce; a full queue drops it.
3672    fn enqueue_stamped_announce(
3673        &self,
3674        dest: &crate::destination::Destination,
3675        identity: &Identity,
3676        app_data: Option<&[u8]>,
3677        completion: Option<crate::link_send::Completion>,
3678    ) -> Result<(), crate::stamper::QueueFull> {
3679        let stamper = self.stamper.as_ref().ok_or(crate::stamper::QueueFull)?;
3680        let identity_prv_key = identity
3681            .get_private_key()
3682            .ok_or(crate::stamper::QueueFull)?;
3683        let result = stamper.enqueue_announce(crate::stamper::AnnounceStampJob {
3684            dest: dest.clone(),
3685            identity_prv_key,
3686            app_data: app_data.map(|data| data.to_vec()),
3687            completion,
3688        });
3689        if result.is_err() {
3690            log::debug!(
3691                "Stamp queue full, dropping announce for {:02x?}",
3692                &dest.hash.0[..4]
3693            );
3694        }
3695        result
3696    }
3697
3698    /// Use `store` for destination stamps, here and in the stamper.
3699    pub(crate) fn set_stamp_store(&mut self, store: crate::stamps::DestinationStampStore) {
3700        if let Some(stamper) = self.stamper.as_ref() {
3701            stamper.set_stamp_store(Some(store.clone()));
3702        }
3703        self.stamp_store = Some(store);
3704    }
3705
3706    /// Override `STAMP_DESTINATIONS` and the stamp targets for this node and
3707    /// its stamper.
3708    #[doc(hidden)]
3709    pub fn set_destination_stamping(
3710        &mut self,
3711        enabled: bool,
3712        targets: crate::stamper::StampTargets,
3713    ) {
3714        self.stamp_destinations.store(enabled, Ordering::Relaxed);
3715        self.stamp_targets = targets;
3716        if let Some(stamper) = self.stamper.as_ref() {
3717            stamper.set_targets(targets);
3718        }
3719    }
3720
3721    /// The stamper shared with the driver.
3722    #[doc(hidden)]
3723    pub fn stamper(&self) -> Option<&Arc<crate::stamper::Stamper>> {
3724        self.stamper.as_ref()
3725    }
3726
3727    /// Queue an encrypted (SINGLE) or plaintext (PLAIN) packet to a destination.
3728    ///
3729    /// Queue-only compatibility API: use `send_packet` or `try_send_packet`
3730    /// for confirmed local transmission. The returned hash does not mean sent.
3731    ///
3732    /// For SINGLE destinations, `dest.public_key` must be set (OUT direction).
3733    /// Returns the packet hash for proof tracking.
3734    pub fn send_packet_queued(
3735        &self,
3736        dest: &crate::destination::Destination,
3737        data: &[u8],
3738    ) -> Result<rns_core::types::PacketHash, SendError> {
3739        self.reject_new_work_if_draining()?;
3740        let packet = self.build_packet(dest, data)?;
3741        let packet_hash = rns_core::types::PacketHash(packet.packet_hash);
3742        self.tx
3743            .send(Event::SendOutbound {
3744                raw: packet.raw,
3745                dest_type: dest.dest_type.to_wire_constant(),
3746                attached_interface: None,
3747            })
3748            .map_err(|_| SendError)?;
3749        Ok(packet_hash)
3750    }
3751
3752    /// Deliver single packets addressed to `dest`, a local destination,
3753    /// decrypted: through [`Callbacks::on_packet_received`](crate::Callbacks::on_packet_received)
3754    /// in place of `on_local_delivery`, decrypted on the node's proof pool
3755    /// when it has one, so a receiver is not bound by decrypting on its own
3756    /// thread. They are decrypted with the destination's attached ratchets
3757    /// ([`attach_local_ratchets`](Self::attach_local_ratchets)) when it has
3758    /// them, else with `identity`, which must hold its private keys. Packets
3759    /// that do not decrypt are dropped and not proved. Deregistering the
3760    /// destination ends it.
3761    pub fn deliver_decrypted(
3762        &self,
3763        dest: &crate::destination::Destination,
3764        identity: &rns_crypto::identity::Identity,
3765    ) -> Result<(), SendError> {
3766        let private = identity.get_private_key().ok_or(SendError)?;
3767        self.tx
3768            .send(Event::DeliverDecrypted {
3769                dest_hash: dest.hash.0,
3770                identity: Box::new(rns_crypto::identity::Identity::from_private_key(&private)),
3771            })
3772            .map_err(|_| SendError)
3773    }
3774
3775    /// Queue a packet for a SINGLE destination without encrypting it on this
3776    /// thread, unlike [`send_packet_queued`](Self::send_packet_queued): the
3777    /// node encrypts it (on its proof pool when it has one; on this thread
3778    /// when the process may use one CPU only) and reports the
3779    /// packet hash through [`Callbacks::on_packet_sent`](crate::Callbacks::on_packet_sent)
3780    /// with the returned ticket, before sending it. A sender is then not
3781    /// bound by its own thread's encryption, one X25519 key generation and
3782    /// exchange per packet.
3783    ///
3784    /// `dest.public_key` must be set (OUT direction). Like
3785    /// `send_packet_queued`, queueing does not mean sent.
3786    pub fn send_packet_deferred(
3787        &self,
3788        dest: &crate::destination::Destination,
3789        data: &[u8],
3790    ) -> Result<crate::SendTicket, SendError> {
3791        self.reject_new_work_if_draining()?;
3792        if dest.dest_type != rns_core::types::DestinationType::Single {
3793            return Err(SendError);
3794        }
3795        let public_key = dest.public_key.ok_or(SendError)?;
3796        let ticket = self.next_send_ticket.fetch_add(1, Ordering::Relaxed);
3797        let ratchet = self.single_ratchet(dest);
3798        let (data, encrypted) = if self.encrypt_deferred_inline {
3799            let payload = rns_crypto::identity::Identity::encrypt_for_public_key(
3800                &public_key,
3801                data,
3802                ratchet.as_ref(),
3803                &mut OsRng,
3804            )
3805            .map_err(|_| SendError)?;
3806            (payload, true)
3807        } else {
3808            (data.to_vec(), false)
3809        };
3810        self.tx
3811            .send(Event::SendDeferred(Box::new(
3812                crate::common::event::DeferredSend {
3813                    ticket,
3814                    destination: dest.hash.0,
3815                    public_key,
3816                    ratchet,
3817                    data,
3818                    encrypted,
3819                },
3820            )))
3821            .map_err(|_| SendError)?;
3822        Ok(crate::SendTicket(ticket))
3823    }
3824
3825    fn build_packet(
3826        &self,
3827        dest: &crate::destination::Destination,
3828        data: &[u8],
3829    ) -> Result<rns_core::packet::RawPacket, SendError> {
3830        use rns_core::types::DestinationType;
3831
3832        let payload = match dest.dest_type {
3833            DestinationType::Single => self.encrypt_single_payload(dest, data)?,
3834            DestinationType::Plain => data.to_vec(),
3835            DestinationType::Group => dest.encrypt(data).map_err(|_| SendError)?,
3836        };
3837
3838        let flags = rns_core::packet::PacketFlags {
3839            header_type: rns_core::constants::HEADER_1,
3840            context_flag: rns_core::constants::FLAG_UNSET,
3841            transport_type: rns_core::constants::TRANSPORT_BROADCAST,
3842            destination_type: dest.dest_type.to_wire_constant(),
3843            packet_type: rns_core::constants::PACKET_TYPE_DATA,
3844        };
3845
3846        let packet = rns_core::packet::RawPacket::pack(
3847            flags,
3848            0,
3849            &dest.hash.0,
3850            None,
3851            rns_core::constants::CONTEXT_NONE,
3852            &payload,
3853        )
3854        .map_err(|_| SendError)?;
3855
3856        Ok(packet)
3857    }
3858
3859    fn encrypt_single_payload(
3860        &self,
3861        dest: &crate::destination::Destination,
3862        data: &[u8],
3863    ) -> Result<Vec<u8>, SendError> {
3864        let pub_key = dest.public_key.ok_or(SendError)?;
3865        let ratchet = self.single_ratchet(dest);
3866        // Encrypted straight from the key bytes: building the identity would
3867        // decompress its signing key on every send, and encryption does not
3868        // use it.
3869        rns_crypto::identity::Identity::encrypt_for_public_key(
3870            &pub_key,
3871            data,
3872            ratchet.as_ref(),
3873            &mut OsRng,
3874        )
3875        .map_err(|_| SendError)
3876    }
3877
3878    /// The ratchet to encrypt a packet for `dest` with: this node's own for
3879    /// its destinations, else the latest one `dest` announced.
3880    fn single_ratchet(&self, dest: &crate::destination::Destination) -> Option<[u8; 32]> {
3881        let local_ratchet = self
3882            .tx
3883            .local_ratchets()
3884            .lock()
3885            .unwrap()
3886            .get(&dest.hash.0)
3887            .and_then(|owner| owner.current_public());
3888        local_ratchet.or_else(|| {
3889            self.ratchet_store.as_ref().and_then(|store| {
3890                match store.current(&dest.hash.0, time::now(), self.ratchet_expiry.as_secs_f64()) {
3891                    Ok(entry) => entry.map(|entry| entry.ratchet),
3892                    Err(err) => {
3893                        log::warn!(
3894                            "failed to load ratchet for {:02x}{:02x}{:02x}{:02x}..: {}",
3895                            dest.hash.0[0],
3896                            dest.hash.0[1],
3897                            dest.hash.0[2],
3898                            dest.hash.0[3],
3899                            err
3900                        );
3901                        None
3902                    }
3903                }
3904            })
3905        })
3906    }
3907
3908    /// Register a destination with the transport engine and set its proof strategy.
3909    ///
3910    /// `signing_key` is the full 64-byte identity private key (X25519 32 bytes +
3911    /// Ed25519 32 bytes), needed for ProveAll/ProveApp to sign proof packets.
3912    pub fn register_destination_with_proof(
3913        &self,
3914        dest: &crate::destination::Destination,
3915        signing_key: Option<[u8; 64]>,
3916    ) -> Result<(), SendError> {
3917        // Register with transport engine
3918        self.register_destination(dest.hash.0, dest.dest_type.to_wire_constant())?;
3919
3920        // Register proof strategy if not ProveNone
3921        if dest.proof_strategy != rns_core::types::ProofStrategy::ProveNone {
3922            self.tx
3923                .send(Event::RegisterProofStrategy {
3924                    dest_hash: dest.hash.0,
3925                    strategy: dest.proof_strategy,
3926                    signing_key,
3927                })
3928                .map_err(|_| SendError)?;
3929        }
3930
3931        Ok(())
3932    }
3933
3934    /// Request a path to a destination from the network.
3935    pub fn request_path(&self, dest_hash: &rns_core::types::DestHash) -> Result<(), SendError> {
3936        self.reject_new_work_if_draining()?;
3937        self.tx
3938            .send(Event::RequestPath {
3939                dest_hash: dest_hash.0,
3940            })
3941            .map_err(|_| SendError)
3942    }
3943
3944    /// Check if a path exists to a destination (synchronous query).
3945    pub fn has_path(&self, dest_hash: &rns_core::types::DestHash) -> Result<bool, SendError> {
3946        match self.query(QueryRequest::HasPath {
3947            dest_hash: dest_hash.0,
3948        })? {
3949            QueryResponse::HasPath(v) => Ok(v),
3950            _ => Ok(false),
3951        }
3952    }
3953
3954    /// Get hop count to a destination (synchronous query).
3955    pub fn hops_to(&self, dest_hash: &rns_core::types::DestHash) -> Result<Option<u8>, SendError> {
3956        match self.query(QueryRequest::HopsTo {
3957            dest_hash: dest_hash.0,
3958        })? {
3959            QueryResponse::HopsTo(v) => Ok(v),
3960            _ => Ok(None),
3961        }
3962    }
3963
3964    /// Recall the identity information for a previously announced destination.
3965    pub fn recall_identity(
3966        &self,
3967        dest_hash: &rns_core::types::DestHash,
3968    ) -> Result<Option<crate::destination::AnnouncedIdentity>, SendError> {
3969        match self.query(QueryRequest::RecallIdentity {
3970            dest_hash: dest_hash.0,
3971        })? {
3972            QueryResponse::RecallIdentity(v) => Ok(v),
3973            _ => Ok(None),
3974        }
3975    }
3976
3977    /// Stamp value and announced link request cost recorded from a known
3978    /// destination's last announce.
3979    pub fn recall_destination_stamp(
3980        &self,
3981        dest_hash: &rns_core::types::DestHash,
3982    ) -> Result<Option<crate::event::DestinationStampInfo>, SendError> {
3983        match self.query(QueryRequest::RecallDestinationStamp {
3984            dest_hash: dest_hash.0,
3985        })? {
3986            QueryResponse::RecallDestinationStamp(v) => Ok(v),
3987            _ => Ok(None),
3988        }
3989    }
3990
3991    /// List known destinations and their lifecycle state.
3992    pub fn known_destinations(
3993        &self,
3994    ) -> Result<Vec<crate::event::KnownDestinationEntry>, SendError> {
3995        match self.query(QueryRequest::KnownDestinations)? {
3996            QueryResponse::KnownDestinations(entries) => Ok(entries),
3997            _ => Ok(Vec::new()),
3998        }
3999    }
4000
4001    /// Mark a known destination as retained.
4002    pub fn retain_known_destination(
4003        &self,
4004        dest_hash: &rns_core::types::DestHash,
4005    ) -> Result<bool, SendError> {
4006        match self.query(QueryRequest::RetainKnownDestination {
4007            dest_hash: dest_hash.0,
4008        })? {
4009            QueryResponse::RetainKnownDestination(ok) => Ok(ok),
4010            _ => Ok(false),
4011        }
4012    }
4013
4014    /// Mark all known destinations announced by an identity as retained.
4015    pub fn retain_identity(
4016        &self,
4017        identity_hash: &rns_core::types::IdentityHash,
4018    ) -> Result<bool, SendError> {
4019        match self.query(QueryRequest::RetainIdentity {
4020            identity_hash: identity_hash.0,
4021        })? {
4022            QueryResponse::RetainIdentity(ok) => Ok(ok),
4023            _ => Ok(false),
4024        }
4025    }
4026
4027    /// Clear the retained flag on a known destination.
4028    pub fn unretain_known_destination(
4029        &self,
4030        dest_hash: &rns_core::types::DestHash,
4031    ) -> Result<bool, SendError> {
4032        match self.query(QueryRequest::UnretainKnownDestination {
4033            dest_hash: dest_hash.0,
4034        })? {
4035            QueryResponse::UnretainKnownDestination(ok) => Ok(ok),
4036            _ => Ok(false),
4037        }
4038    }
4039
4040    /// Mark a known destination as used.
4041    pub fn mark_known_destination_used(
4042        &self,
4043        dest_hash: &rns_core::types::DestHash,
4044    ) -> Result<bool, SendError> {
4045        match self.query(QueryRequest::MarkKnownDestinationUsed {
4046            dest_hash: dest_hash.0,
4047        })? {
4048            QueryResponse::MarkKnownDestinationUsed(ok) => Ok(ok),
4049            _ => Ok(false),
4050        }
4051    }
4052
4053    fn persist_known_destinations(&self) {
4054        if self.known_destinations_store.is_none() && self.known_destinations_path.is_none() {
4055            return;
4056        }
4057
4058        let Ok(entries) = self.known_destinations() else {
4059            return;
4060        };
4061
4062        let destinations: std::collections::HashMap<[u8; 16], storage::KnownDestination> = entries
4063            .into_iter()
4064            .map(|entry| {
4065                (
4066                    entry.dest_hash,
4067                    storage::KnownDestination {
4068                        identity_hash: entry.identity_hash,
4069                        public_key: entry.public_key,
4070                        app_data: entry.app_data,
4071                        hops: entry.hops,
4072                        received_at: entry.received_at,
4073                        receiving_interface: entry.receiving_interface.0,
4074                        was_used: entry.was_used,
4075                        last_used_at: entry.last_used_at,
4076                        retained: entry.retained,
4077                        stamp: entry.stamp.map(|stamp| stamp.to_vec()),
4078                        stamp_value: entry.stamp_value,
4079                        lr_cost: entry.lr_cost,
4080                    },
4081                )
4082            })
4083            .collect();
4084
4085        if let Some(store) = self.known_destinations_store.as_ref() {
4086            if let Err(err) = store.save_all(&destinations) {
4087                log::warn!("failed to persist known destinations: {}", err);
4088            }
4089        } else if let Some(path) = self.known_destinations_path.as_ref() {
4090            if let Err(err) = storage::save_known_destinations(&destinations, path) {
4091                log::warn!("failed to persist known destinations: {}", err);
4092            }
4093        }
4094    }
4095
4096    fn persist_transport_state(&self) {
4097        let Some(storage_dir) = self.transport_state_dir.as_ref() else {
4098            return;
4099        };
4100        let Ok(QueryResponse::TransportStateSnapshot(snapshot)) =
4101            self.query(QueryRequest::TransportStateSnapshot)
4102        else {
4103            return;
4104        };
4105        if let Err(err) = storage::save_transport_state(&snapshot, storage_dir, true) {
4106            log::warn!("failed to persist transport state: {}", err);
4107        }
4108    }
4109
4110    /// Load an in-memory WASM hook at runtime.
4111    pub fn load_hook(
4112        &self,
4113        name: String,
4114        wasm_bytes: Vec<u8>,
4115        attach_point: String,
4116        priority: i32,
4117    ) -> Result<Result<(), String>, SendError> {
4118        let (response_tx, response_rx) = std::sync::mpsc::channel();
4119        self.tx
4120            .send(Event::LoadHook {
4121                name,
4122                wasm_bytes,
4123                attach_point,
4124                priority,
4125                response_tx,
4126            })
4127            .map_err(|_| SendError)?;
4128        response_rx.recv().map_err(|_| SendError)
4129    }
4130
4131    /// Load a hook from a server-local filesystem path at runtime.
4132    pub fn load_hook_file(
4133        &self,
4134        name: String,
4135        path: String,
4136        hook_type: String,
4137        attach_point: String,
4138        priority: i32,
4139    ) -> Result<Result<(), String>, SendError> {
4140        let (response_tx, response_rx) = std::sync::mpsc::channel();
4141        self.tx
4142            .send(Event::LoadHookFile {
4143                name,
4144                path,
4145                hook_type,
4146                attach_point,
4147                priority,
4148                response_tx,
4149            })
4150            .map_err(|_| SendError)?;
4151        response_rx.recv().map_err(|_| SendError)
4152    }
4153
4154    /// Load a registered built-in hook at runtime.
4155    pub fn load_builtin_hook(
4156        &self,
4157        name: String,
4158        builtin_id: String,
4159        attach_point: String,
4160        priority: i32,
4161    ) -> Result<Result<(), String>, SendError> {
4162        let (response_tx, response_rx) = std::sync::mpsc::channel();
4163        self.tx
4164            .send(Event::LoadBuiltinHook {
4165                name,
4166                builtin_id,
4167                attach_point,
4168                priority,
4169                response_tx,
4170            })
4171            .map_err(|_| SendError)?;
4172        response_rx.recv().map_err(|_| SendError)
4173    }
4174
4175    /// Unload a hook at runtime.
4176    pub fn unload_hook(
4177        &self,
4178        name: String,
4179        attach_point: String,
4180    ) -> Result<Result<(), String>, SendError> {
4181        let (response_tx, response_rx) = std::sync::mpsc::channel();
4182        self.tx
4183            .send(Event::UnloadHook {
4184                name,
4185                attach_point,
4186                response_tx,
4187            })
4188            .map_err(|_| SendError)?;
4189        response_rx.recv().map_err(|_| SendError)
4190    }
4191
4192    /// Reload an in-memory WASM hook at runtime (detach + recompile + reattach with same priority).
4193    pub fn reload_hook(
4194        &self,
4195        name: String,
4196        attach_point: String,
4197        wasm_bytes: Vec<u8>,
4198    ) -> Result<Result<(), String>, SendError> {
4199        let (response_tx, response_rx) = std::sync::mpsc::channel();
4200        self.tx
4201            .send(Event::ReloadHook {
4202                name,
4203                attach_point,
4204                wasm_bytes,
4205                response_tx,
4206            })
4207            .map_err(|_| SendError)?;
4208        response_rx.recv().map_err(|_| SendError)
4209    }
4210
4211    /// Reload a hook from a server-local filesystem path at runtime.
4212    pub fn reload_hook_file(
4213        &self,
4214        name: String,
4215        attach_point: String,
4216        path: String,
4217        hook_type: String,
4218    ) -> Result<Result<(), String>, SendError> {
4219        let (response_tx, response_rx) = std::sync::mpsc::channel();
4220        self.tx
4221            .send(Event::ReloadHookFile {
4222                name,
4223                attach_point,
4224                path,
4225                hook_type,
4226                response_tx,
4227            })
4228            .map_err(|_| SendError)?;
4229        response_rx.recv().map_err(|_| SendError)
4230    }
4231
4232    /// Reload a registered built-in hook at runtime.
4233    pub fn reload_builtin_hook(
4234        &self,
4235        name: String,
4236        attach_point: String,
4237        builtin_id: String,
4238    ) -> Result<Result<(), String>, SendError> {
4239        let (response_tx, response_rx) = std::sync::mpsc::channel();
4240        self.tx
4241            .send(Event::ReloadBuiltinHook {
4242                name,
4243                attach_point,
4244                builtin_id,
4245                response_tx,
4246            })
4247            .map_err(|_| SendError)?;
4248        response_rx.recv().map_err(|_| SendError)
4249    }
4250
4251    /// Enable or disable a loaded hook at runtime.
4252    pub fn set_hook_enabled(
4253        &self,
4254        name: String,
4255        attach_point: String,
4256        enabled: bool,
4257    ) -> Result<Result<(), String>, SendError> {
4258        let (response_tx, response_rx) = std::sync::mpsc::channel();
4259        self.tx
4260            .send(Event::SetHookEnabled {
4261                name,
4262                attach_point,
4263                enabled,
4264                response_tx,
4265            })
4266            .map_err(|_| SendError)?;
4267        response_rx.recv().map_err(|_| SendError)
4268    }
4269
4270    /// Update the priority of a loaded hook at runtime.
4271    pub fn set_hook_priority(
4272        &self,
4273        name: String,
4274        attach_point: String,
4275        priority: i32,
4276    ) -> Result<Result<(), String>, SendError> {
4277        let (response_tx, response_rx) = std::sync::mpsc::channel();
4278        self.tx
4279            .send(Event::SetHookPriority {
4280                name,
4281                attach_point,
4282                priority,
4283                response_tx,
4284            })
4285            .map_err(|_| SendError)?;
4286        response_rx.recv().map_err(|_| SendError)
4287    }
4288
4289    /// List all loaded hooks.
4290    pub fn list_hooks(&self) -> Result<Vec<crate::event::HookInfo>, SendError> {
4291        let (response_tx, response_rx) = std::sync::mpsc::channel();
4292        self.tx
4293            .send(Event::ListHooks { response_tx })
4294            .map_err(|_| SendError)?;
4295        response_rx.recv().map_err(|_| SendError)
4296    }
4297
4298    /// Construct an RnsNode from its constituent parts.
4299    /// Used by `shared_client` to build a client-mode node.
4300    #[cfg(any(feature = "iface-local", test))]
4301    pub(crate) fn from_parts(
4302        tx: EventSender,
4303        driver_handle: thread::JoinHandle<()>,
4304        rpc_server: Option<crate::rpc::RpcServer>,
4305        tick_interval_ms: Arc<AtomicU64>,
4306        accepting_work: Arc<AtomicBool>,
4307    ) -> Self {
4308        RnsNode {
4309            tx,
4310            driver_handle: Some(driver_handle),
4311            verify_handle: None,
4312            verify_shutdown: Arc::default(),
4313            persist_handle: None,
4314            persist_stop: None,
4315            rpc_server,
4316            #[cfg(target_os = "linux")]
4317            rpc_unix_server: None,
4318            tick_interval_ms,
4319            next_send_ticket: AtomicU64::new(0),
4320            encrypt_deferred_inline: std::thread::available_parallelism()
4321                .map_or(true, |cpus| cpus.get() == 1),
4322            accepting_work,
4323            probe_server: None,
4324            known_destinations_path: None,
4325            known_destinations_store: None,
4326            stamp_store: None,
4327            stamp_destinations: Arc::new(AtomicBool::new(rns_core::constants::STAMP_DESTINATIONS)),
4328            stamp_targets: crate::stamper::StampTargets::default(),
4329            stamper: None,
4330            transport_state_dir: None,
4331            ratchet_store: None,
4332            ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
4333            #[cfg(feature = "iface-local")]
4334            destination_types: Default::default(),
4335        }
4336    }
4337
4338    /// Get the event sender for direct event injection.
4339    pub fn event_sender(&self) -> &EventSender {
4340        &self.tx
4341    }
4342
4343    /// Set the tick interval in milliseconds.
4344    /// Default is 1000 (1 second). Changes take effect on the next tick cycle.
4345    /// Values are clamped to the range 100..=10000.
4346    /// Returns the actual stored value (which may differ from `ms` if clamped).
4347    pub fn set_tick_interval(&self, ms: u64) -> u64 {
4348        let clamped = ms.clamp(100, 10_000);
4349        if clamped != ms {
4350            log::warn!(
4351                "tick interval {}ms out of range, clamped to {}ms",
4352                ms,
4353                clamped
4354            );
4355        }
4356        self.tick_interval_ms.store(clamped, Ordering::Relaxed);
4357        clamped
4358    }
4359
4360    /// Get the current tick interval in milliseconds.
4361    pub fn tick_interval(&self) -> u64 {
4362        self.tick_interval_ms.load(Ordering::Relaxed)
4363    }
4364
4365    /// Shut down the node. Blocks until the driver thread exits.
4366    pub fn shutdown(mut self) {
4367        self.shutdown_inner();
4368    }
4369
4370    fn shutdown_inner(&mut self) {
4371        #[cfg(feature = "iface-local")]
4372        self.destination_types.withdraw_from_shared_instance();
4373        if self.driver_handle.is_none()
4374            && self.verify_handle.is_none()
4375            && self.persist_handle.is_none()
4376            && self.rpc_server.is_none()
4377            && {
4378                #[cfg(target_os = "linux")]
4379                {
4380                    self.rpc_unix_server.is_none()
4381                }
4382                #[cfg(not(target_os = "linux"))]
4383                {
4384                    true
4385                }
4386            }
4387        {
4388            return;
4389        }
4390        // Stop RPC server first
4391        if let Some(mut rpc) = self.rpc_server.take() {
4392            rpc.stop();
4393        }
4394        #[cfg(target_os = "linux")]
4395        if let Some(mut rpc) = self.rpc_unix_server.take() {
4396            rpc.stop();
4397        }
4398        self.persist_known_destinations();
4399        self.persist_transport_state();
4400        if let Some(stop) = self.persist_stop.take() {
4401            let _ = stop.send(());
4402        }
4403        if let Some(handle) = self.persist_handle.take() {
4404            let _ = handle.join();
4405        }
4406        self.verify_shutdown.stop();
4407        let _ = self.tx.send(Event::Shutdown);
4408        if let Some(handle) = self.driver_handle.take() {
4409            let _ = handle.join();
4410        }
4411        // Reader/ticker EventSender clones may outlive the driver. They must not
4412        // retain private owners (and their file locks) after node shutdown.
4413        self.tx.local_ratchets().lock().unwrap().clear();
4414        if let Some(handle) = self.verify_handle.take() {
4415            let _ = handle.join();
4416        }
4417    }
4418}
4419
4420/// Make sure a destination stamp reaching the destination's target value is
4421/// persisted for `dest`, grinding one at that target when none is. This is
4422/// the stamp half of `Destination._announce`, run by the stamper off the
4423/// calling thread.
4424///
4425/// Since d7749b0b upstream reloads or regenerates the held stamp whenever
4426/// its value is below `target_value`, and both the store lookup and the
4427/// grind use `target_value` rather than `DESTINATION_COST` /
4428/// `DESTINATION_VALUE` (Destination.py:319-323). A persisted stamp that
4429/// still meets the announce cost but not the target is therefore replaced.
4430pub(crate) fn ensure_destination_stamp(
4431    store: &crate::stamps::DestinationStampStore,
4432    targets: crate::stamper::StampTargets,
4433    dest: &crate::destination::Destination,
4434    identity: &Identity,
4435) -> Result<(), String> {
4436    let public_key = identity
4437        .get_public_key()
4438        .ok_or_else(|| "identity has no public key".to_string())?;
4439    let name_hash = rns_core::destination::name_hash(
4440        &dest.app_name,
4441        &dest.aspects.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
4442    );
4443    let material = crate::stamps::destination_stamp_material(&dest.hash.0, &public_key, &name_hash);
4444    let target = targets.destination_target(dest);
4445    if store.load(&dest.hash.0, &material, target).is_some() {
4446        return Ok(());
4447    }
4448    let started = std::time::Instant::now();
4449    let (stamp, value) =
4450        crate::stamps::generate_stamp(&material, target, rns_core::constants::STAMP_LENGTH, 0)
4451            .ok_or_else(|| "stamp cost is above the protocol cap".to_string())?;
4452    if let Err(error) = store.store(&dest.hash.0, &stamp, value) {
4453        log::warn!("failed to persist destination stamp: {error}");
4454    }
4455    log::debug!(
4456        "Generated and persisted stamp with value {value} for {:02x?} in {:?}",
4457        &dest.hash.0[..4],
4458        started.elapsed()
4459    );
4460    Ok(())
4461}
4462
4463/// Build an announce packet for `dest`, signed by `identity`.
4464///
4465/// With `stamp_destinations` the app data is extended with the destination
4466/// stamp from `stamp_store`, grinding one inline when none is persisted, as
4467/// `Destination._announce` does. The node API checks for a missing stamp
4468/// first and defers to the stamper, so the inline grind only runs when a
4469/// stamp disappeared between that check and this call.
4470pub(crate) fn build_announce_packet(
4471    tx: &EventSender,
4472    stamp_store: Option<&crate::stamps::DestinationStampStore>,
4473    stamp_destinations: bool,
4474    targets: crate::stamper::StampTargets,
4475    dest: &crate::destination::Destination,
4476    identity: &Identity,
4477    app_data: Option<&[u8]>,
4478) -> Result<
4479    (
4480        rns_core::packet::RawPacket,
4481        Option<Box<crate::common::event::AnnounceReplay>>,
4482    ),
4483    SendError,
4484> {
4485    let name_hash = rns_core::destination::name_hash(
4486        &dest.app_name,
4487        &dest.aspects.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
4488    );
4489
4490    let mut random_hash = [0u8; 10];
4491    OsRng.fill_bytes(&mut random_hash[..5]);
4492    // Bytes [5:10] must be the emission timestamp (seconds since epoch,
4493    // big-endian, truncated to 5 bytes) so that path table dedup can
4494    // compare announce freshness.  Matches Python: int(time.time()).to_bytes(5, "big")
4495    let now_secs = std::time::SystemTime::now()
4496        .duration_since(std::time::UNIX_EPOCH)
4497        .unwrap_or_default()
4498        .as_secs();
4499    random_hash[5..10].copy_from_slice(&now_secs.to_be_bytes()[3..8]);
4500
4501    let owner = tx
4502        .local_ratchets()
4503        .lock()
4504        .unwrap()
4505        .get(&dest.hash.0)
4506        .cloned();
4507    let ratchet = if let Some(owner) = owner {
4508        if dest.direction != rns_core::types::Direction::In
4509            || owner.identity_hash() != *identity.hash()
4510        {
4511            return Err(SendError);
4512        }
4513        Some(owner.public_for_announce(now_secs).map_err(|error| {
4514            log::error!("local ratchet announcement failed: {error}");
4515            SendError
4516        })?)
4517    } else {
4518        None
4519    };
4520    let wire_app_data: Option<Vec<u8>> = if stamp_destinations {
4521        let public_key = identity.get_public_key().ok_or(SendError)?;
4522        let material =
4523            crate::stamps::destination_stamp_material(&dest.hash.0, &public_key, &name_hash);
4524        let store = stamp_store.ok_or(SendError)?;
4525        let target = targets.destination_target(dest);
4526        let (stamp, _value) = match store.load(&dest.hash.0, &material, target) {
4527            Some(entry) => entry,
4528            None => {
4529                let (stamp, value) = crate::stamps::generate_stamp(
4530                    &material,
4531                    target,
4532                    rns_core::constants::STAMP_LENGTH,
4533                    0,
4534                )
4535                .ok_or(SendError)?;
4536                if let Err(error) = store.store(&dest.hash.0, &stamp, value) {
4537                    log::warn!("failed to persist destination stamp: {error}");
4538                }
4539                (stamp, value)
4540            }
4541        };
4542        let mut wire = app_data.map(|data| data.to_vec()).unwrap_or_default();
4543        // `cost_to_bytes(self.get_link_cost())`: the destination's own cost,
4544        // or the default `LR_COST` when none was set, fraction included.
4545        wire.extend_from_slice(&crate::stamps::announce_ext_payload(
4546            &stamp,
4547            0,
4548            tx.link_request_costs().get(&dest.hash.0),
4549        ));
4550        Some(wire)
4551    } else {
4552        app_data.map(|data| data.to_vec())
4553    };
4554
4555    let (announce_data, has_ratchet) = rns_core::announce::AnnounceData::pack(
4556        identity,
4557        &dest.hash.0,
4558        &name_hash,
4559        &random_hash,
4560        ratchet.as_ref(),
4561        wire_app_data.as_deref(),
4562    )
4563    .map_err(|_| SendError)?;
4564
4565    let context_flag = if has_ratchet {
4566        rns_core::constants::FLAG_SET
4567    } else {
4568        rns_core::constants::FLAG_UNSET
4569    };
4570
4571    let flags = rns_core::packet::PacketFlags {
4572        header_type: rns_core::constants::HEADER_1,
4573        context_flag,
4574        transport_type: rns_core::constants::TRANSPORT_BROADCAST,
4575        destination_type: rns_core::constants::DESTINATION_SINGLE,
4576        packet_type: rns_core::constants::PACKET_TYPE_ANNOUNCE,
4577    };
4578
4579    let packet = rns_core::packet::RawPacket::pack(
4580        flags,
4581        0,
4582        &dest.hash.0,
4583        None,
4584        rns_core::constants::CONTEXT_NONE,
4585        &announce_data,
4586    )
4587    .map_err(|_| SendError)?;
4588
4589    let replay = if dest.dest_type == rns_core::types::DestinationType::Single {
4590        identity.get_private_key().map(|identity_prv_key| {
4591            Box::new(crate::common::event::AnnounceReplay {
4592                dest_hash: dest.hash.0,
4593                name_hash,
4594                identity_prv_key,
4595                app_data: wire_app_data,
4596            })
4597        })
4598    } else {
4599        None
4600    };
4601    Ok((packet, replay))
4602}
4603
4604impl Drop for RnsNode {
4605    fn drop(&mut self) {
4606        self.shutdown_inner();
4607    }
4608}
4609
4610#[cfg(test)]
4611mod tests {
4612    use super::*;
4613    use crate::driver::IfacRuntimeConfig;
4614    use crate::storage::RatchetStore;
4615    use rns_core::stamp::StampCost;
4616    use std::fs;
4617    use tempfile::tempdir;
4618
4619    #[test]
4620    fn local_ratchet_announcements_and_self_send() {
4621        let node = RnsNode::start(NodeConfig::default(), Box::new(NoopCallbacks)).unwrap();
4622        let identity = Identity::from_private_key(&[42; 64]);
4623        let dest = crate::Destination::single_in(
4624            "test",
4625            &["ratchet"],
4626            rns_core::types::IdentityHash(*identity.hash()),
4627        );
4628        let owner = crate::LocalRatchets::with_store(
4629            &dest,
4630            Identity::from_private_key(&[42; 64]),
4631            Arc::new(crate::MemoryRatchetStore::default()),
4632        )
4633        .unwrap();
4634        owner.enforce();
4635        node.attach_local_ratchets(owner.clone()).unwrap();
4636        node.attach_local_ratchets(owner.clone()).unwrap();
4637        let conflicting = crate::LocalRatchets::with_store(
4638            &dest,
4639            Identity::from_private_key(&[42; 64]),
4640            Arc::new(crate::MemoryRatchetStore::default()),
4641        )
4642        .unwrap();
4643        assert!(node.attach_local_ratchets(conflicting).is_err());
4644        let (packet, _) = node.build_announce(&dest, &identity, Some(b"app")).unwrap();
4645        assert_eq!(packet.flags.context_flag, rns_core::constants::FLAG_SET);
4646        let announce = rns_core::announce::AnnounceData::unpack(packet.data(), true).unwrap();
4647        announce.validate(&dest.hash.0).unwrap();
4648        assert_eq!(announce.ratchet, owner.current_public());
4649        assert!(node
4650            .build_announce(&dest, &Identity::from_private_key(&[43; 64]), None)
4651            .is_err());
4652        assert!(node
4653            .build_announce(&dest, &identity, Some(&vec![0; 500]))
4654            .is_err());
4655        let mut outgoing = dest.clone();
4656        outgoing.direction = rns_core::types::Direction::Out;
4657        outgoing.public_key = identity.get_public_key();
4658        let encrypted = node.encrypt_single_payload(&outgoing, b"self").unwrap();
4659        assert_eq!(owner.decrypt(&encrypted).unwrap().plaintext, b"self");
4660        assert!(identity.decrypt(&encrypted).is_err());
4661    }
4662
4663    struct NoopCallbacks;
4664
4665    impl Callbacks for NoopCallbacks {
4666        fn on_announce(&mut self, _: crate::destination::AnnouncedIdentity) {}
4667        fn on_path_updated(&mut self, _: rns_core::types::DestHash, _: u8) {}
4668        fn on_local_delivery(
4669            &mut self,
4670            _: rns_core::types::DestHash,
4671            _: Vec<u8>,
4672            _: rns_core::types::PacketHash,
4673        ) {
4674        }
4675    }
4676
4677    #[test]
4678    fn managed_interface_attaches_disabled_section_and_reloads_from_disk() {
4679        use crate::event::InterfaceManagementOperation as Operation;
4680
4681        let dir = tempdir().unwrap();
4682        let config_file = dir.path().join("config");
4683        let write_config = |port| {
4684            fs::write(&config_file, format!("[reticulum]\nshare_instance = no\n\n[interfaces]\n  [[Managed UDP]]\n    type = UDPInterface\n    enabled = no\n    forward_ip = 127.0.0.1\n    forward_port = {port}\n"))
4685            .unwrap();
4686        };
4687        write_config(9);
4688        let node = RnsNode::from_config(Some(dir.path()), Box::new(NoopCallbacks)).unwrap();
4689        let manage = |operation| {
4690            node.query(QueryRequest::ManageInterface {
4691                operation,
4692                name: "Managed UDP".into(),
4693            })
4694            .unwrap()
4695        };
4696        assert!(matches!(
4697            manage(Operation::Detach),
4698            QueryResponse::InterfaceManagement(None)
4699        ));
4700        assert!(matches!(
4701            manage(Operation::Attach),
4702            QueryResponse::InterfaceManagement(Some(true))
4703        ));
4704        assert!(matches!(
4705            manage(Operation::Attach),
4706            QueryResponse::InterfaceManagement(Some(false))
4707        ));
4708        write_config(10);
4709        assert!(matches!(
4710            manage(Operation::Reload),
4711            QueryResponse::InterfaceManagement(Some(true))
4712        ));
4713        assert!(matches!(
4714            manage(Operation::Detach),
4715            QueryResponse::InterfaceManagement(Some(true))
4716        ));
4717        assert!(matches!(
4718            manage(Operation::Detach),
4719            QueryResponse::InterfaceManagement(None)
4720        ));
4721        node.shutdown();
4722    }
4723
4724    #[test]
4725    fn interface_management_can_be_disabled() {
4726        use crate::event::InterfaceManagementOperation as Operation;
4727
4728        let dir = tempdir().unwrap();
4729        fs::write(
4730            dir.path().join("config"),
4731            "[reticulum]\nshare_instance = no\nenable_interface_management = no\n",
4732        )
4733        .unwrap();
4734        let node = RnsNode::from_config(Some(dir.path()), Box::new(NoopCallbacks)).unwrap();
4735        assert!(matches!(
4736            node.query(QueryRequest::ManageInterface {
4737                operation: Operation::Attach,
4738                name: "missing".into(),
4739            })
4740            .unwrap(),
4741            QueryResponse::InterfaceManagement(Some(false))
4742        ));
4743        node.shutdown();
4744    }
4745
4746    #[cfg(feature = "iface-tcp")]
4747    #[test]
4748    fn managed_tcp_listener_releases_port_on_detach() {
4749        use crate::event::InterfaceManagementOperation as Operation;
4750        use std::io::Read;
4751
4752        let dir = tempdir().unwrap();
4753        let port = crate::test_support::port();
4754        fs::write(dir.path().join("config"), format!(
4755            "[reticulum]\nshare_instance = no\n\n[interfaces]\n  [[Managed TCP]]\n    type = TCPServerInterface\n    enabled = no\n    listen_ip = 127.0.0.1\n    listen_port = {port}\n"
4756        )).unwrap();
4757        let node = RnsNode::from_config(Some(dir.path()), Box::new(NoopCallbacks)).unwrap();
4758        let manage = |operation| {
4759            node.query(QueryRequest::ManageInterface {
4760                operation,
4761                name: "Managed TCP".into(),
4762            })
4763            .unwrap()
4764        };
4765        assert!(matches!(
4766            manage(Operation::Attach),
4767            QueryResponse::InterfaceManagement(Some(true))
4768        ));
4769        let mut client = std::net::TcpStream::connect(("127.0.0.1", port)).unwrap();
4770        client
4771            .set_read_timeout(Some(Duration::from_secs(2)))
4772            .unwrap();
4773        thread::sleep(Duration::from_millis(100));
4774        assert!(matches!(
4775            manage(Operation::Detach),
4776            QueryResponse::InterfaceManagement(Some(true))
4777        ));
4778        let deadline = std::time::Instant::now() + Duration::from_secs(2);
4779        while std::net::TcpStream::connect(("127.0.0.1", port)).is_ok()
4780            && std::time::Instant::now() < deadline
4781        {
4782            thread::sleep(Duration::from_millis(25));
4783        }
4784        assert!(std::net::TcpStream::connect(("127.0.0.1", port)).is_err());
4785        assert_eq!(client.read(&mut [0u8; 1]).unwrap(), 0);
4786        assert!(matches!(
4787            manage(Operation::Attach),
4788            QueryResponse::InterfaceManagement(Some(true))
4789        ));
4790        assert!(std::net::TcpStream::connect(("127.0.0.1", port)).is_ok());
4791        node.shutdown();
4792    }
4793
4794    #[test]
4795    fn shared_instance_rpc_manages_interface_by_name() {
4796        use crate::pickle::PickleValue;
4797        use crate::rpc::{derive_auth_key, RpcAddr, RpcClient};
4798
4799        let dir = tempdir().unwrap();
4800        let rpc_port = crate::test_support::port();
4801        let shared_port = crate::test_support::port();
4802        fs::write(dir.path().join("config"), format!(
4803            "[reticulum]\nshare_instance = yes\ninstance_control_port = {rpc_port}\nshared_instance_port = {shared_port}\n\n[interfaces]\n  [[Managed UDP]]\n    type = UDPInterface\n    enabled = no\n    forward_ip = 127.0.0.1\n    forward_port = 9\n"
4804        )).unwrap();
4805        let node = RnsNode::from_config(Some(dir.path()), Box::new(NoopCallbacks)).unwrap();
4806        let paths = storage::ensure_storage_dirs(dir.path()).unwrap();
4807        let identity = storage::load_or_create_identity(&paths.identities).unwrap();
4808        let key = derive_auth_key(&identity.get_private_key().unwrap());
4809        let call = |action: &str| {
4810            let mut client =
4811                RpcClient::connect(&RpcAddr::Tcp("127.0.0.1".into(), rpc_port), &key).unwrap();
4812            client
4813                .call(&PickleValue::Dict(vec![
4814                    (
4815                        PickleValue::String("manage".into()),
4816                        PickleValue::String(action.into()),
4817                    ),
4818                    (
4819                        PickleValue::String("name".into()),
4820                        PickleValue::String("Managed UDP".into()),
4821                    ),
4822                ]))
4823                .unwrap()
4824        };
4825        assert_eq!(call("attach_interface"), PickleValue::Bool(true));
4826        assert_eq!(call("reload_interface"), PickleValue::Bool(true));
4827        assert_eq!(call("detach_interface"), PickleValue::Bool(true));
4828        assert_eq!(call("detach_interface"), PickleValue::None);
4829        node.shutdown();
4830    }
4831
4832    struct TestWriter;
4833
4834    impl crate::interface::Writer for TestWriter {
4835        fn send_frame(&mut self, _data: &[u8]) -> io::Result<()> {
4836            Ok(())
4837        }
4838    }
4839
4840    fn test_transport_config(transport_enabled: bool) -> TransportConfig {
4841        TransportConfig {
4842            transport_enabled,
4843            identity_hash: None,
4844            local_hops_delta: 0,
4845            prefer_shorter_path: false,
4846            max_paths_per_destination: 1,
4847            packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
4848            packet_hashlist_allocation: rns_core::transport::types::PacketHashlistAllocation::Eager,
4849            max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
4850            max_path_destinations: usize::MAX,
4851            max_tunnel_destinations_total: usize::MAX,
4852            destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
4853            announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
4854            announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
4855            announce_sig_cache_enabled: true,
4856            announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
4857            announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
4858            announce_queue_max_entries: 256,
4859            announce_queue_max_interfaces: 1024,
4860        }
4861    }
4862
4863    fn test_interface_info(id: u64) -> rns_core::transport::types::InterfaceInfo {
4864        rns_core::transport::types::InterfaceInfo {
4865            stamp_costs: Default::default(),
4866            id: rns_core::transport::types::InterfaceId(id),
4867            name: format!("test-{id}"),
4868            mode: rns_core::constants::MODE_FULL,
4869            gravity: 0,
4870            recursive_prs: false,
4871            announces_from_internal: true,
4872            announces_to_internal: None,
4873            out_capable: true,
4874            in_capable: true,
4875            bitrate: None,
4876            airtime_profile: None,
4877            announce_rate_target: None,
4878            announce_rate_grace: 0,
4879            announce_rate_penalty: 0.0,
4880            announce_cap: rns_core::constants::ANNOUNCE_CAP,
4881            is_local_client: false,
4882            wants_tunnel: false,
4883            tunnel_id: None,
4884            mtu: rns_core::constants::MTU as u32,
4885            ingress_control: rns_core::transport::types::IngressControlConfig::disabled(),
4886            ia_freq: 0.0,
4887            ip_freq: 0.0,
4888            op_freq: 0.0,
4889            op_samples: 0,
4890            started: 0.0,
4891        }
4892    }
4893
4894    #[test]
4895    fn static_interface_registration_applies_transport_announce_rate_defaults() {
4896        let (tx, rx) = crate::event::channel();
4897        let mut driver = Driver::new(
4898            test_transport_config(true),
4899            rx,
4900            tx.clone(),
4901            Box::new(NoopCallbacks),
4902        );
4903        driver.set_announce_rate_defaults(AnnounceRateDefaults {
4904            target: Some(7200.0),
4905            penalty: 15.0,
4906            grace: 7,
4907        });
4908
4909        let id = rns_core::transport::types::InterfaceId(7);
4910        register_started_interface(StartedInterface {
4911            driver: &mut driver,
4912            tx: &tx,
4913            queue_capacity: crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
4914            id,
4915            info: test_interface_info(id.0),
4916            writer: Box::new(TestWriter),
4917            interface_type_name: "TestInterface".to_string(),
4918            ifac_state: None,
4919            ifac_runtime: &IfacRuntimeConfig {
4920                netname: None,
4921                netkey: None,
4922                size: 16,
4923            },
4924            stamp_costs: Default::default(),
4925        });
4926
4927        let info = &driver.interfaces[&id].info;
4928        assert_eq!(info.announce_rate_target, Some(7200.0));
4929        assert_eq!(info.announce_rate_penalty, 15.0);
4930        assert_eq!(info.announce_rate_grace, 7);
4931    }
4932
4933    #[test]
4934    fn static_interface_registration_applies_transport_ingress_control_defaults() {
4935        let (tx, rx) = crate::event::channel();
4936        let mut driver = Driver::new(
4937            test_transport_config(true),
4938            rx,
4939            tx.clone(),
4940            Box::new(NoopCallbacks),
4941        );
4942        let mut defaults = rns_core::transport::types::IngressControlConfig::enabled();
4943        defaults.burst_hold = 1.5;
4944        defaults.pr_burst_freq = 5.5;
4945        defaults.egress_enabled = true;
4946        defaults.egress_pr_freq = 9.5;
4947        driver.set_ingress_control_defaults(defaults);
4948
4949        let id = rns_core::transport::types::InterfaceId(8);
4950        register_started_interface(StartedInterface {
4951            driver: &mut driver,
4952            tx: &tx,
4953            queue_capacity: crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
4954            id,
4955            info: test_interface_info(id.0),
4956            writer: Box::new(TestWriter),
4957            interface_type_name: "TestInterface".to_string(),
4958            ifac_state: None,
4959            ifac_runtime: &IfacRuntimeConfig {
4960                netname: None,
4961                netkey: None,
4962                size: 16,
4963            },
4964            stamp_costs: rns_core::transport::types::InterfaceStampCosts {
4965                destination_cost: None,
4966                pr_cost: Some(rns_core::stamp::StampCost::from_f64(12.25)),
4967            },
4968        });
4969
4970        let ic = driver.interfaces[&id].info.ingress_control;
4971        assert_eq!(ic.burst_hold, 1.5);
4972        assert_eq!(ic.pr_burst_freq, 5.5);
4973        assert!(ic.egress_enabled);
4974        assert_eq!(ic.egress_pr_freq, 9.5);
4975        // The configured per-interface costs reach the engine (9b464382).
4976        assert_eq!(
4977            driver
4978                .engine
4979                .interface_info(&id)
4980                .unwrap()
4981                .stamp_costs
4982                .pr_cost,
4983            Some(rns_core::stamp::StampCost::from_f64(12.25))
4984        );
4985    }
4986
4987    struct TestNodeRatchetStore {
4988        entry: storage::RatchetEntry,
4989        current_calls: std::sync::Mutex<Vec<[u8; 16]>>,
4990    }
4991
4992    impl storage::RatchetStore for TestNodeRatchetStore {
4993        fn remember(&self, _dest_hash: [u8; 16], _entry: storage::RatchetEntry) -> io::Result<()> {
4994            Ok(())
4995        }
4996
4997        fn current(
4998            &self,
4999            dest_hash: &[u8; 16],
5000            _now: f64,
5001            _expiry_secs: f64,
5002        ) -> io::Result<Option<storage::RatchetEntry>> {
5003            self.current_calls.lock().unwrap().push(*dest_hash);
5004            Ok(Some(self.entry))
5005        }
5006
5007        fn cleanup(
5008            &self,
5009            _known_destinations: &HashSet<[u8; 16]>,
5010            _now: f64,
5011            _expiry_secs: f64,
5012        ) -> io::Result<storage::RatchetCleanupStats> {
5013            Ok(Default::default())
5014        }
5015    }
5016
5017    #[test]
5018    fn send_packet_checks_ratchet_store_for_single_destinations() {
5019        let store = Arc::new(TestNodeRatchetStore {
5020            entry: storage::RatchetEntry {
5021                ratchet: [0x55; 32],
5022                received_at: time::now(),
5023            },
5024            current_calls: std::sync::Mutex::new(Vec::new()),
5025        });
5026        let ratchet_store: Arc<dyn storage::RatchetStore> = store.clone();
5027        let node = RnsNode::start(
5028            NodeConfig {
5029                ratchet_store: Some(ratchet_store),
5030                ..Default::default()
5031            },
5032            Box::new(NoopCallbacks),
5033        )
5034        .unwrap();
5035
5036        let mut rng = OsRng;
5037        let remote_identity = Identity::new(&mut rng);
5038        let public_key = remote_identity.get_public_key().unwrap();
5039        let announced = crate::destination::AnnouncedIdentity {
5040            dest_hash: rns_core::types::DestHash([0x22; 16]),
5041            identity_hash: rns_core::types::IdentityHash(*remote_identity.hash()),
5042            public_key,
5043            app_data: None,
5044            hops: 1,
5045            received_at: time::now(),
5046            receiving_interface: rns_core::transport::types::InterfaceId(0),
5047            rssi: Some(-100),
5048            snr: Some(10.5),
5049        };
5050        let dest = crate::destination::Destination::single_out("test", &["ratchet"], &announced);
5051
5052        node.send_packet_queued(&dest, b"hello").unwrap();
5053        assert_eq!(
5054            store.current_calls.lock().unwrap().as_slice(),
5055            &[dest.hash.0]
5056        );
5057
5058        node.shutdown();
5059    }
5060
5061    #[test]
5062    fn single_payload_uses_stored_ratchet_key_material() {
5063        let ratchet_prv = rns_crypto::x25519::X25519PrivateKey::from_bytes(&[0x42; 32]);
5064        let ratchet_pub = ratchet_prv.public_key().public_bytes();
5065        let store = Arc::new(TestNodeRatchetStore {
5066            entry: storage::RatchetEntry {
5067                ratchet: ratchet_pub,
5068                received_at: time::now(),
5069            },
5070            current_calls: std::sync::Mutex::new(Vec::new()),
5071        });
5072        let ratchet_store: Arc<dyn storage::RatchetStore> = store.clone();
5073        let (tx, _rx) = crate::event::channel();
5074        let node = RnsNode {
5075            tx,
5076            driver_handle: None,
5077            verify_handle: None,
5078            verify_shutdown: Arc::default(),
5079            persist_handle: None,
5080            persist_stop: None,
5081            rpc_server: None,
5082            #[cfg(target_os = "linux")]
5083            rpc_unix_server: None,
5084            tick_interval_ms: Arc::new(AtomicU64::new(1000)),
5085            next_send_ticket: AtomicU64::new(0),
5086            encrypt_deferred_inline: std::thread::available_parallelism()
5087                .map_or(true, |cpus| cpus.get() == 1),
5088            accepting_work: Arc::new(AtomicBool::new(true)),
5089            probe_server: None,
5090            known_destinations_path: None,
5091            known_destinations_store: None,
5092            stamp_store: None,
5093            stamp_destinations: Arc::new(AtomicBool::new(rns_core::constants::STAMP_DESTINATIONS)),
5094            stamp_targets: crate::stamper::StampTargets::default(),
5095            stamper: None,
5096            transport_state_dir: None,
5097            ratchet_store: Some(ratchet_store),
5098            ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
5099            #[cfg(feature = "iface-local")]
5100            destination_types: Default::default(),
5101        };
5102
5103        let mut rng = OsRng;
5104        let remote_identity = Identity::new(&mut rng);
5105        let public_key = remote_identity.get_public_key().unwrap();
5106        let announced = crate::destination::AnnouncedIdentity {
5107            dest_hash: rns_core::types::DestHash([0x33; 16]),
5108            identity_hash: rns_core::types::IdentityHash(*remote_identity.hash()),
5109            public_key,
5110            app_data: None,
5111            hops: 1,
5112            received_at: time::now(),
5113            receiving_interface: rns_core::transport::types::InterfaceId(0),
5114            rssi: Some(-100),
5115            snr: Some(10.5),
5116        };
5117        let dest = crate::destination::Destination::single_out("test", &["ratchet"], &announced);
5118        let payload = node
5119            .encrypt_single_payload(&dest, b"ratchet message")
5120            .unwrap();
5121
5122        assert_eq!(
5123            store.current_calls.lock().unwrap().as_slice(),
5124            &[dest.hash.0]
5125        );
5126        assert!(remote_identity.decrypt(&payload).is_err());
5127
5128        let peer_pub_bytes: [u8; 32] = payload[..32].try_into().unwrap();
5129        let peer_pub = rns_crypto::x25519::X25519PublicKey::from_bytes(&peer_pub_bytes);
5130        let shared_key = ratchet_prv.exchange(&peer_pub);
5131        let derived_key = rns_crypto::hkdf::hkdf(
5132            rns_crypto::identity::DERIVED_KEY_LENGTH,
5133            &shared_key,
5134            Some(remote_identity.hash()),
5135            None,
5136        )
5137        .unwrap();
5138        let token = rns_crypto::token::Token::new(&derived_key).unwrap();
5139        let plaintext = token.decrypt(&payload[32..]).unwrap();
5140        assert_eq!(plaintext, b"ratchet message");
5141    }
5142
5143    #[test]
5144    fn tcp_client_interface_is_not_discoverable_without_kiss_framing() {
5145        let mut params = std::collections::HashMap::new();
5146        params.insert("discoverable".to_string(), "yes".to_string());
5147        params.insert(
5148            "discovery_name".to_string(),
5149            "invalid-tcp-client".to_string(),
5150        );
5151        params.insert("reachable_on".to_string(), "example.com".to_string());
5152        params.insert("target_port".to_string(), "4242".to_string());
5153
5154        let discovery =
5155            super::extract_discovery_config("tcp-client", "TCPClientInterface", &params);
5156
5157        assert!(
5158            discovery.is_none(),
5159            "TCPClientInterface discovery must be rejected unless KISS framing is supported"
5160        );
5161    }
5162
5163    #[test]
5164    fn discovery_config_parses_location_command() {
5165        let mut params = std::collections::HashMap::new();
5166        params.insert("discoverable".into(), "yes".into());
5167        params.insert("location_cmd".into(), "~/bin/reticulum-location".into());
5168
5169        let discovery =
5170            super::extract_discovery_config("public", "BackboneInterface", &params).unwrap();
5171        assert_eq!(
5172            discovery.location_cmd.as_deref(),
5173            Some("~/bin/reticulum-location")
5174        );
5175    }
5176
5177    #[test]
5178    fn discovery_config_parses_valid_operator_lxmf_address() {
5179        let mut params = std::collections::HashMap::new();
5180        params.insert("discoverable".into(), "yes".into());
5181        params.insert("discovery_lxmf_address".into(), "ab".repeat(16));
5182
5183        let discovery =
5184            super::extract_discovery_config("public", "BackboneInterface", &params).unwrap();
5185
5186        assert_eq!(discovery.operator_lxmf_address, Some([0xab; 16]));
5187    }
5188
5189    #[test]
5190    fn discovery_config_ignores_invalid_operator_lxmf_addresses() {
5191        for address in ["ab".repeat(15), "zz".repeat(16)] {
5192            let mut params = std::collections::HashMap::new();
5193            params.insert("discoverable".into(), "yes".into());
5194            params.insert("discovery_lxmf_address".into(), address);
5195
5196            let discovery =
5197                super::extract_discovery_config("public", "BackboneInterface", &params).unwrap();
5198
5199            assert_eq!(discovery.operator_lxmf_address, None);
5200        }
5201    }
5202
5203    #[test]
5204    fn ingress_control_config_defaults_by_interface_type() {
5205        let params = std::collections::HashMap::new();
5206
5207        let tcp = super::parse_ingress_control_config("TCPServerInterface", &params).unwrap();
5208        assert!(tcp.enabled);
5209        assert_eq!(
5210            tcp.max_held_announces,
5211            rns_core::constants::IC_MAX_HELD_ANNOUNCES
5212        );
5213        assert_eq!(tcp.burst_hold, rns_core::constants::IC_BURST_HOLD);
5214
5215        let pipe = super::parse_ingress_control_config("PipeInterface", &params).unwrap();
5216        assert!(!pipe.enabled);
5217        assert_eq!(
5218            pipe.held_release_interval,
5219            rns_core::constants::IC_HELD_RELEASE_INTERVAL
5220        );
5221    }
5222
5223    #[test]
5224    fn ingress_control_config_parses_python_ic_keys() {
5225        let mut params = std::collections::HashMap::new();
5226        params.insert("ingress_control".to_string(), "No".to_string());
5227        params.insert("ic_max_held_announces".to_string(), "17".to_string());
5228        params.insert("ic_burst_hold".to_string(), "1.5".to_string());
5229        params.insert("ic_burst_freq_new".to_string(), "2.5".to_string());
5230        params.insert("ic_burst_freq".to_string(), "3.5".to_string());
5231        params.insert("ic_pr_burst_freq_new".to_string(), "3.25".to_string());
5232        params.insert("ic_pr_burst_freq".to_string(), "8.25".to_string());
5233        params.insert("egress_control".to_string(), "Yes".to_string());
5234        params.insert("ec_pr_freq".to_string(), "5.25".to_string());
5235        params.insert("ic_new_time".to_string(), "4.5".to_string());
5236        params.insert("ic_burst_penalty".to_string(), "5.5".to_string());
5237        params.insert("ic_held_release_interval".to_string(), "6.5".to_string());
5238
5239        let config = super::parse_ingress_control_config("TCPServerInterface", &params).unwrap();
5240
5241        assert!(!config.enabled);
5242        assert_eq!(config.max_held_announces, 17);
5243        assert_eq!(config.burst_hold, 1.5);
5244        assert_eq!(config.burst_freq_new, 2.5);
5245        assert_eq!(config.burst_freq, 3.5);
5246        assert_eq!(config.pr_burst_freq_new, 3.25);
5247        assert_eq!(config.pr_burst_freq, 8.25);
5248        assert!(config.egress_enabled);
5249        assert_eq!(config.egress_pr_freq, 5.25);
5250        assert_eq!(config.new_time, 4.5);
5251        assert_eq!(config.burst_penalty, 5.5);
5252        assert_eq!(config.held_release_interval, 6.5);
5253    }
5254
5255    #[test]
5256    fn ingress_control_config_rejects_invalid_values() {
5257        let mut params = std::collections::HashMap::new();
5258        params.insert("ic_burst_hold".to_string(), "-1".to_string());
5259
5260        let err = super::parse_ingress_control_config("TCPServerInterface", &params).unwrap_err();
5261
5262        assert!(err.contains("ic_burst_hold"));
5263    }
5264
5265    #[test]
5266    fn interface_stamp_costs_are_fractional_and_clamped() {
5267        // Reticulum.py since 9b464382: `as_float`, `max(0, min(v, cap))`.
5268        use rns_core::stamp::StampCost;
5269        let mut params = std::collections::HashMap::new();
5270        let none = super::parse_interface_stamp_costs(&params).unwrap();
5271        assert_eq!(none, Default::default());
5272
5273        params.insert("destination_cost".to_string(), "17.5".to_string());
5274        params.insert("pr_cost".to_string(), "250".to_string());
5275        let costs = super::parse_interface_stamp_costs(&params).unwrap();
5276        assert_eq!(costs.destination_cost, Some(StampCost::from_f64(17.5)));
5277        assert_eq!(costs.pr_cost, Some(StampCost::from_whole(100)));
5278
5279        params.insert("pr_cost".to_string(), "-3".to_string());
5280        let costs = super::parse_interface_stamp_costs(&params).unwrap();
5281        assert_eq!(costs.pr_cost, Some(StampCost::ZERO));
5282
5283        params.insert("destination_cost".to_string(), "steep".to_string());
5284        let err = super::parse_interface_stamp_costs(&params).unwrap_err();
5285        assert!(err.contains("destination_cost"));
5286    }
5287
5288    #[test]
5289    fn discovery_stamp_default_is_16_but_explicit_override_is_preserved() {
5290        let mut params = std::collections::HashMap::new();
5291        params.insert("discoverable".to_string(), "yes".to_string());
5292        let defaults =
5293            super::extract_discovery_config("Discovery Test", "BackboneInterface", &params)
5294                .unwrap();
5295        assert_eq!(
5296            defaults.stamp_value,
5297            rns_core::stamp::StampCost::from_whole(16)
5298        );
5299
5300        params.insert("discovery_stamp_value".to_string(), "14".to_string());
5301        let overridden =
5302            super::extract_discovery_config("Discovery Test", "BackboneInterface", &params)
5303                .unwrap();
5304        assert_eq!(
5305            overridden.stamp_value,
5306            rns_core::stamp::StampCost::from_whole(14)
5307        );
5308    }
5309
5310    #[test]
5311    fn start_and_shutdown() {
5312        let node = RnsNode::start(
5313            NodeConfig {
5314                panic_on_interface_error: false,
5315                link_proof_verify_threads: Default::default(),
5316                stamps: Default::default(),
5317                transport_enabled: false,
5318                static_transport_identity: false,
5319                local_hops_delta: false,
5320                identity: None,
5321                interfaces: vec![],
5322                share_instance: false,
5323                instance_name: "default".into(),
5324                shared_instance_port: 37428,
5325                rpc_port: 0,
5326                cache_dir: None,
5327                ratchet_store: None,
5328                ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
5329                management: Default::default(),
5330                probe_port: None,
5331                probe_addrs: vec![],
5332                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
5333                direct_connect_policy: Default::default(),
5334                direct_connect_allow_private_endpoints: false,
5335                device: None,
5336                underlay_mark: None,
5337                hooks: Vec::new(),
5338                discover_interfaces: false,
5339                autoconnect_interface_mode: None,
5340                autoconnect_interface_gravity: 0,
5341                autoconnect_announces_to_internal: false,
5342                autoconnect_unverified_implementations: false,
5343                discovery_required_value: None,
5344                respond_to_probes: false,
5345                prefer_shorter_path: false,
5346                max_paths_per_destination: 1,
5347                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
5348                packet_hashlist_allocation:
5349                    rns_core::transport::types::PacketHashlistAllocation::Eager,
5350                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
5351                max_path_destinations: usize::MAX,
5352                max_tunnel_destinations_total: usize::MAX,
5353                known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
5354                known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
5355                announce_table_ttl: Duration::from_secs(
5356                    rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
5357                ),
5358                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
5359                driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
5360                interface_writer_queue_capacity:
5361                    crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
5362                announce_rate_defaults: AnnounceRateDefaults::default(),
5363                ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(
5364                ),
5365                #[cfg(feature = "iface-backbone")]
5366                backbone_peer_pool: None,
5367                announce_sig_cache_enabled: true,
5368                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
5369                announce_sig_cache_ttl: Duration::from_secs(
5370                    rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
5371                ),
5372                registry: None,
5373                #[cfg(feature = "hooks")]
5374                provider_bridge: None,
5375            },
5376            Box::new(NoopCallbacks),
5377        )
5378        .unwrap();
5379        node.shutdown();
5380    }
5381
5382    #[test]
5383    fn known_destinations_persist_across_restart() {
5384        let dir = tempdir().unwrap();
5385        let dest_hash = [0x91; 16];
5386        let identity = Identity::new(&mut OsRng);
5387        let last_used_at = 77.0;
5388        let receiving_interface = rns_core::transport::types::InterfaceId(42);
5389
5390        let node = RnsNode::from_config(Some(dir.path()), Box::new(NoopCallbacks)).unwrap();
5391        let (response_tx, response_rx) = std::sync::mpsc::channel();
5392        node.event_sender()
5393            .send(crate::event::Event::query(
5394                QueryRequest::RestoreKnownDestination(crate::event::KnownDestinationEntry {
5395                    dest_hash,
5396                    identity_hash: *identity.hash(),
5397                    public_key: identity.get_public_key().unwrap(),
5398                    app_data: Some(b"persisted".to_vec()),
5399                    hops: 2,
5400                    received_at: 55.0,
5401                    receiving_interface,
5402                    was_used: true,
5403                    last_used_at: Some(last_used_at),
5404                    retained: true,
5405                    stamp: None,
5406                    stamp_value: rns_core::stamp::StampValue::ZERO,
5407                    lr_cost: None,
5408                }),
5409                response_tx,
5410            ))
5411            .unwrap();
5412        assert!(matches!(
5413            response_rx.recv().unwrap(),
5414            QueryResponse::RestoreKnownDestination(true)
5415        ));
5416        node.shutdown();
5417
5418        let restarted = RnsNode::from_config(Some(dir.path()), Box::new(NoopCallbacks)).unwrap();
5419        let entries = restarted.known_destinations().unwrap();
5420        let entry = entries
5421            .iter()
5422            .find(|entry| entry.dest_hash == dest_hash)
5423            .expect("reloaded destination should appear in lifecycle listing");
5424        assert!(entry.retained);
5425        assert!(entry.was_used);
5426        assert_eq!(entry.hops, 2);
5427        assert_eq!(entry.receiving_interface, receiving_interface);
5428        assert_eq!(entry.last_used_at, Some(last_used_at));
5429
5430        let recalled = restarted
5431            .recall_identity(&rns_core::types::DestHash(dest_hash))
5432            .unwrap()
5433            .expect("known destination should reload from storage");
5434        assert_eq!(recalled.identity_hash.0, *identity.hash());
5435        assert_eq!(recalled.app_data, Some(b"persisted".to_vec()));
5436        restarted.shutdown();
5437    }
5438
5439    #[test]
5440    fn transport_paths_hashes_and_detached_tunnels_restore_from_upstream_files() {
5441        use rns_core::packet::{PacketFlags, RawPacket};
5442        use rns_core::transport::persistence::{
5443            PersistedPath, PersistedTunnel, TransportStateSnapshot,
5444        };
5445
5446        let dir = tempdir().unwrap();
5447        fs::write(
5448            dir.path().join("config"),
5449            r#"[reticulum]
5450enable_transport = True
5451share_instance = False
5452
5453[interfaces]
5454  [[Persistence Test]]
5455    type = UDPInterface
5456    forward_ip = 127.0.0.1
5457    forward_port = 9
5458    interface_mode = full
5459"#,
5460        )
5461        .unwrap();
5462        let paths = storage::ensure_storage_dirs(dir.path()).unwrap();
5463        let destination_hash = [0x31; 16];
5464        let packet = RawPacket::pack(
5465            PacketFlags {
5466                header_type: rns_core::constants::HEADER_1,
5467                context_flag: rns_core::constants::FLAG_UNSET,
5468                transport_type: rns_core::constants::TRANSPORT_BROADCAST,
5469                destination_type: rns_core::constants::DESTINATION_SINGLE,
5470                packet_type: rns_core::constants::PACKET_TYPE_ANNOUNCE,
5471            },
5472            0,
5473            &destination_hash,
5474            None,
5475            rns_core::constants::CONTEXT_NONE,
5476            &[0xA5; 32],
5477        )
5478        .unwrap();
5479        crate::announce_cache::AnnounceCache::new(paths.cache.join("announces"))
5480            .store(&packet.packet_hash, &packet.raw, None)
5481            .unwrap();
5482        let now = time::now();
5483        let route = PersistedPath {
5484            destination_hash,
5485            timestamp: now,
5486            received_from: [0x41; 16],
5487            hops: 2,
5488            expires: now + 600.0,
5489            random_blobs: vec![[0x51; 10]],
5490            interface_hash: Some(rns_core::hash::full_hash(b"Persistence Test")),
5491            packet_hash: packet.packet_hash,
5492            used: 0.0,
5493        };
5494        storage::save_transport_state(
5495            &TransportStateSnapshot {
5496                packet_hashes: vec![[0x61; 32], [0x62; 32]],
5497                paths: vec![route.clone()],
5498                tunnels: vec![PersistedTunnel {
5499                    tunnel_id: [0x71; 32],
5500                    interface_hash: None,
5501                    paths: vec![PersistedPath {
5502                        interface_hash: None,
5503                        ..route
5504                    }],
5505                    expires: now + 1_200.0,
5506                }],
5507            },
5508            &paths.storage,
5509            false,
5510        )
5511        .unwrap();
5512
5513        let node = RnsNode::from_config(Some(dir.path()), Box::new(NoopCallbacks)).unwrap();
5514        let QueryResponse::TransportStateSnapshot(restored) =
5515            node.query(QueryRequest::TransportStateSnapshot).unwrap()
5516        else {
5517            panic!("unexpected transport snapshot response");
5518        };
5519        // Upstream's full hashes load; the table keeps their first 16 bytes.
5520        let stored = |byte: u8| {
5521            let mut hash = [0u8; 32];
5522            hash[..16].fill(byte);
5523            hash
5524        };
5525        assert_eq!(restored.packet_hashes, vec![stored(0x61), stored(0x62)]);
5526        assert_eq!(restored.paths.len(), 1);
5527        assert_eq!(restored.paths[0].destination_hash, destination_hash);
5528        assert_eq!(restored.paths[0].received_from, [0x41; 16]);
5529        assert_eq!(restored.tunnels.len(), 1);
5530        assert_eq!(restored.tunnels[0].tunnel_id, [0x71; 32]);
5531        assert_eq!(restored.tunnels[0].interface_hash, None);
5532        node.shutdown();
5533    }
5534
5535    #[test]
5536    fn from_config_cleans_persistent_ratchets_after_loading_known_destinations() {
5537        fn hex(bytes: &[u8]) -> String {
5538            bytes.iter().map(|byte| format!("{:02x}", byte)).collect()
5539        }
5540
5541        let dir = tempdir().unwrap();
5542        fs::write(
5543            dir.path().join("config"),
5544            "[reticulum]\nenable_transport = False\nshare_instance = False\nratchet_expiry = 300\n",
5545        )
5546        .unwrap();
5547        let paths = storage::ensure_storage_dirs(dir.path()).unwrap();
5548        let known_live = [0x01; 16];
5549        let known_expired = [0x02; 16];
5550        let unknown = [0x03; 16];
5551        let known_corrupt = [0x04; 16];
5552        let now = time::now();
5553
5554        let identity = Identity::new(&mut OsRng);
5555        let known = std::collections::HashMap::from([
5556            (
5557                known_live,
5558                storage::KnownDestination {
5559                    identity_hash: *identity.hash(),
5560                    public_key: identity.get_public_key().unwrap(),
5561                    app_data: None,
5562                    hops: 1,
5563                    received_at: now,
5564                    receiving_interface: 0,
5565                    was_used: false,
5566                    last_used_at: None,
5567                    retained: false,
5568                    stamp: None,
5569                    stamp_value: rns_core::stamp::StampValue::ZERO,
5570                    lr_cost: None,
5571                },
5572            ),
5573            (
5574                known_expired,
5575                storage::KnownDestination {
5576                    identity_hash: *identity.hash(),
5577                    public_key: identity.get_public_key().unwrap(),
5578                    app_data: None,
5579                    hops: 1,
5580                    received_at: now,
5581                    receiving_interface: 0,
5582                    was_used: false,
5583                    last_used_at: None,
5584                    retained: false,
5585                    stamp: None,
5586                    stamp_value: rns_core::stamp::StampValue::ZERO,
5587                    lr_cost: None,
5588                },
5589            ),
5590            (
5591                known_corrupt,
5592                storage::KnownDestination {
5593                    identity_hash: *identity.hash(),
5594                    public_key: identity.get_public_key().unwrap(),
5595                    app_data: None,
5596                    hops: 1,
5597                    received_at: now,
5598                    receiving_interface: 0,
5599                    was_used: false,
5600                    last_used_at: None,
5601                    retained: false,
5602                    stamp: None,
5603                    stamp_value: rns_core::stamp::StampValue::ZERO,
5604                    lr_cost: None,
5605                },
5606            ),
5607        ]);
5608        storage::save_known_destinations(&known, &paths.storage.join("known_destinations"))
5609            .unwrap();
5610
5611        let store = storage::FsRatchetStore::new(paths.ratchets.clone());
5612        store
5613            .remember(
5614                known_live,
5615                storage::RatchetEntry {
5616                    ratchet: [0x11; 32],
5617                    received_at: now,
5618                },
5619            )
5620            .unwrap();
5621        store
5622            .remember(
5623                known_expired,
5624                storage::RatchetEntry {
5625                    ratchet: [0x22; 32],
5626                    received_at: now - 1000.0,
5627                },
5628            )
5629            .unwrap();
5630        store
5631            .remember(
5632                unknown,
5633                storage::RatchetEntry {
5634                    ratchet: [0x33; 32],
5635                    received_at: now,
5636                },
5637            )
5638            .unwrap();
5639        fs::write(paths.ratchets.join(hex(&known_corrupt)), b"not msgpack").unwrap();
5640        fs::write(paths.ratchets.join("0102.out"), b"temp").unwrap();
5641
5642        let node = RnsNode::from_config(Some(dir.path()), Box::new(NoopCallbacks)).unwrap();
5643
5644        assert!(!paths.ratchets.join(hex(&known_live)).exists());
5645        assert!(!paths.ratchets.join(hex(&known_expired)).exists());
5646        assert!(!paths.ratchets.join(hex(&unknown)).exists());
5647        assert!(!paths.ratchets.join(hex(&known_corrupt)).exists());
5648        assert!(!paths.ratchets.join("0102.out").exists());
5649        assert!(paths.storage.join("known_destinations.imported").exists());
5650
5651        let store = crate::storage_sqlite::KnownDestinationsStore::open_read_only(
5652            &crate::storage_sqlite::default_path(&paths.storage),
5653            16,
5654        )
5655        .unwrap();
5656        assert_eq!(store.load_all().unwrap().len(), 3);
5657        assert!(store.current(&known_live, now, 300.0).unwrap().is_some());
5658        assert!(store.current(&known_expired, now, 300.0).unwrap().is_none());
5659        assert!(store.current(&unknown, now, 300.0).unwrap().is_none());
5660        assert!(store.current(&known_corrupt, now, 300.0).unwrap().is_none());
5661
5662        node.shutdown();
5663    }
5664
5665    #[test]
5666    fn start_with_identity() {
5667        let identity = Identity::new(&mut OsRng);
5668        let hash = *identity.hash();
5669        let node = RnsNode::start(
5670            NodeConfig {
5671                panic_on_interface_error: false,
5672                link_proof_verify_threads: Default::default(),
5673                stamps: Default::default(),
5674                transport_enabled: true,
5675                static_transport_identity: false,
5676                local_hops_delta: false,
5677                identity: Some(identity),
5678                interfaces: vec![],
5679                share_instance: false,
5680                instance_name: "default".into(),
5681                shared_instance_port: 37428,
5682                rpc_port: 0,
5683                cache_dir: None,
5684                ratchet_store: None,
5685                ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
5686                management: Default::default(),
5687                probe_port: None,
5688                probe_addrs: vec![],
5689                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
5690                direct_connect_policy: Default::default(),
5691                direct_connect_allow_private_endpoints: false,
5692                device: None,
5693                underlay_mark: None,
5694                hooks: Vec::new(),
5695                discover_interfaces: false,
5696                autoconnect_interface_mode: None,
5697                autoconnect_interface_gravity: 0,
5698                autoconnect_announces_to_internal: false,
5699                autoconnect_unverified_implementations: false,
5700                discovery_required_value: None,
5701                respond_to_probes: false,
5702                prefer_shorter_path: false,
5703                max_paths_per_destination: 1,
5704                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
5705                packet_hashlist_allocation:
5706                    rns_core::transport::types::PacketHashlistAllocation::Eager,
5707                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
5708                max_path_destinations: usize::MAX,
5709                max_tunnel_destinations_total: usize::MAX,
5710                known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
5711                known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
5712                announce_table_ttl: Duration::from_secs(
5713                    rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
5714                ),
5715                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
5716                driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
5717                interface_writer_queue_capacity:
5718                    crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
5719                announce_rate_defaults: AnnounceRateDefaults::default(),
5720                ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(
5721                ),
5722                #[cfg(feature = "iface-backbone")]
5723                backbone_peer_pool: None,
5724                announce_sig_cache_enabled: true,
5725                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
5726                announce_sig_cache_ttl: Duration::from_secs(
5727                    rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
5728                ),
5729                registry: None,
5730                #[cfg(feature = "hooks")]
5731                provider_bridge: None,
5732            },
5733            Box::new(NoopCallbacks),
5734        )
5735        .unwrap();
5736        // The identity hash should have been used
5737        let _ = hash;
5738        node.shutdown();
5739    }
5740
5741    #[test]
5742    fn start_generates_identity() {
5743        let node = RnsNode::start(
5744            NodeConfig {
5745                panic_on_interface_error: false,
5746                link_proof_verify_threads: Default::default(),
5747                stamps: Default::default(),
5748                transport_enabled: false,
5749                static_transport_identity: false,
5750                local_hops_delta: false,
5751                identity: None,
5752                interfaces: vec![],
5753                share_instance: false,
5754                instance_name: "default".into(),
5755                shared_instance_port: 37428,
5756                rpc_port: 0,
5757                cache_dir: None,
5758                ratchet_store: None,
5759                ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
5760                management: Default::default(),
5761                probe_port: None,
5762                probe_addrs: vec![],
5763                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
5764                direct_connect_policy: Default::default(),
5765                direct_connect_allow_private_endpoints: false,
5766                device: None,
5767                underlay_mark: None,
5768                hooks: Vec::new(),
5769                discover_interfaces: false,
5770                autoconnect_interface_mode: None,
5771                autoconnect_interface_gravity: 0,
5772                autoconnect_announces_to_internal: false,
5773                autoconnect_unverified_implementations: false,
5774                discovery_required_value: None,
5775                respond_to_probes: false,
5776                prefer_shorter_path: false,
5777                max_paths_per_destination: 1,
5778                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
5779                packet_hashlist_allocation:
5780                    rns_core::transport::types::PacketHashlistAllocation::Eager,
5781                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
5782                max_path_destinations: usize::MAX,
5783                max_tunnel_destinations_total: usize::MAX,
5784                known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
5785                known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
5786                announce_table_ttl: Duration::from_secs(
5787                    rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
5788                ),
5789                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
5790                driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
5791                interface_writer_queue_capacity:
5792                    crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
5793                announce_rate_defaults: AnnounceRateDefaults::default(),
5794                ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(
5795                ),
5796                #[cfg(feature = "iface-backbone")]
5797                backbone_peer_pool: None,
5798                announce_sig_cache_enabled: true,
5799                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
5800                announce_sig_cache_ttl: Duration::from_secs(
5801                    rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
5802                ),
5803                registry: None,
5804                #[cfg(feature = "hooks")]
5805                provider_bridge: None,
5806            },
5807            Box::new(NoopCallbacks),
5808        )
5809        .unwrap();
5810        // Should not panic - identity was auto-generated
5811        node.shutdown();
5812    }
5813
5814    #[test]
5815    fn from_config_creates_identity() {
5816        let dir = std::env::temp_dir().join(format!("rns-test-fc-{}", std::process::id()));
5817        let _ = fs::remove_dir_all(&dir);
5818        fs::create_dir_all(&dir).unwrap();
5819
5820        // Write a minimal config file
5821        fs::write(
5822            dir.join("config"),
5823            "[reticulum]\nenable_transport = False\n",
5824        )
5825        .unwrap();
5826
5827        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks)).unwrap();
5828
5829        // Identity file should have been created
5830        assert!(dir.join("storage/identities/identity").exists());
5831
5832        node.shutdown();
5833        let _ = fs::remove_dir_all(&dir);
5834    }
5835
5836    #[test]
5837    fn private_config_keeps_state_out_of_config_directory() {
5838        let root = tempfile::tempdir().unwrap();
5839        let config_dir = root.path().join("config");
5840        let state_dir = root.path().join("state");
5841        fs::create_dir_all(&config_dir).unwrap();
5842        fs::write(
5843            config_dir.join("config"),
5844            "[reticulum]\nenable_transport = False\nshare_instance = Yes\n",
5845        )
5846        .unwrap();
5847        let node =
5848            RnsNode::from_private_config(&config_dir, &state_dir, None, Box::new(NoopCallbacks))
5849                .unwrap();
5850        assert!(state_dir.join("storage/identities/identity").exists());
5851        assert!(!config_dir.join("storage").exists());
5852        node.shutdown();
5853    }
5854
5855    #[test]
5856    fn from_config_loads_identity() {
5857        let dir = std::env::temp_dir().join(format!("rns-test-fl-{}", std::process::id()));
5858        let _ = fs::remove_dir_all(&dir);
5859        fs::create_dir_all(dir.join("storage/identities")).unwrap();
5860
5861        // Pre-create an identity
5862        let identity = Identity::new(&mut OsRng);
5863        let hash = *identity.hash();
5864        storage::save_identity(&identity, &dir.join("storage/identities/identity")).unwrap();
5865
5866        fs::write(
5867            dir.join("config"),
5868            "[reticulum]\nenable_transport = False\n",
5869        )
5870        .unwrap();
5871
5872        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks)).unwrap();
5873
5874        // Verify the same identity was loaded (hash matches)
5875        let loaded = storage::load_identity(&dir.join("storage/identities/identity")).unwrap();
5876        assert_eq!(*loaded.hash(), hash);
5877
5878        node.shutdown();
5879        let _ = fs::remove_dir_all(&dir);
5880    }
5881
5882    #[test]
5883    fn from_config_tcp_server() {
5884        let dir = std::env::temp_dir().join(format!("rns-test-fts-{}", std::process::id()));
5885        let _ = fs::remove_dir_all(&dir);
5886        fs::create_dir_all(&dir).unwrap();
5887
5888        let port = crate::test_support::port();
5889
5890        let config = format!(
5891            r#"
5892[reticulum]
5893enable_transport = False
5894
5895[interfaces]
5896  [[Test TCP Server]]
5897    type = TCPServerInterface
5898    listen_ip = 127.0.0.1
5899    listen_port = {}
5900"#,
5901            port
5902        );
5903
5904        fs::write(dir.join("config"), config).unwrap();
5905
5906        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks)).unwrap();
5907
5908        // Give server time to start
5909        thread::sleep(Duration::from_millis(100));
5910
5911        // Should be able to connect
5912        let _client = std::net::TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
5913
5914        node.shutdown();
5915        let _ = fs::remove_dir_all(&dir);
5916    }
5917
5918    #[test]
5919    fn from_config_starts_rpc_when_share_instance_enabled() {
5920        let dir = std::env::temp_dir().join(format!("rns-test-rpc-{}", std::process::id()));
5921        let _ = fs::remove_dir_all(&dir);
5922        fs::create_dir_all(&dir).unwrap();
5923
5924        let rpc_port = crate::test_support::port();
5925
5926        let config = format!(
5927            r#"
5928[reticulum]
5929enable_transport = False
5930share_instance = Yes
5931instance_control_port = {}
5932
5933[interfaces]
5934"#,
5935            rpc_port
5936        );
5937
5938        fs::write(dir.join("config"), config).unwrap();
5939
5940        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks)).unwrap();
5941
5942        thread::sleep(Duration::from_millis(100));
5943
5944        let _client = std::net::TcpStream::connect(format!("127.0.0.1:{}", rpc_port)).unwrap();
5945
5946        node.shutdown();
5947        let _ = fs::remove_dir_all(&dir);
5948    }
5949
5950    #[test]
5951    fn from_config_starts_rpc_when_transport_enabled() {
5952        let dir =
5953            std::env::temp_dir().join(format!("rns-test-rpc-transport-{}", std::process::id()));
5954        let _ = fs::remove_dir_all(&dir);
5955        fs::create_dir_all(&dir).unwrap();
5956
5957        let rpc_port = crate::test_support::port();
5958
5959        let config = format!(
5960            r#"
5961[reticulum]
5962enable_transport = True
5963share_instance = Yes
5964instance_control_port = {}
5965
5966[interfaces]
5967"#,
5968            rpc_port
5969        );
5970
5971        fs::write(dir.join("config"), config).unwrap();
5972
5973        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks)).unwrap();
5974
5975        thread::sleep(Duration::from_millis(100));
5976
5977        let _client = std::net::TcpStream::connect(format!("127.0.0.1:{}", rpc_port)).unwrap();
5978
5979        node.shutdown();
5980        let _ = fs::remove_dir_all(&dir);
5981    }
5982
5983    #[test]
5984    fn from_config_starts_rpc_when_tcp_client_is_unreachable() {
5985        let dir =
5986            std::env::temp_dir().join(format!("rns-test-rpc-unreachable-{}", std::process::id()));
5987        let _ = fs::remove_dir_all(&dir);
5988        fs::create_dir_all(&dir).unwrap();
5989
5990        let rpc_port = crate::test_support::port();
5991        let unreachable_port = crate::test_support::port();
5992
5993        let config = format!(
5994            r#"
5995[reticulum]
5996enable_transport = True
5997share_instance = Yes
5998instance_control_port = {}
5999
6000[interfaces]
6001  [[Unreachable Upstream]]
6002    type = TCPClientInterface
6003    target_host = 127.0.0.1
6004    target_port = {}
6005"#,
6006            rpc_port, unreachable_port
6007        );
6008
6009        fs::write(dir.join("config"), config).unwrap();
6010
6011        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks)).unwrap();
6012
6013        thread::sleep(Duration::from_millis(100));
6014
6015        let _client = std::net::TcpStream::connect(format!("127.0.0.1:{}", rpc_port)).unwrap();
6016
6017        node.shutdown();
6018        let _ = fs::remove_dir_all(&dir);
6019    }
6020
6021    #[test]
6022    fn test_parse_interface_mode() {
6023        use rns_core::constants::*;
6024
6025        assert_eq!(parse_interface_mode("full"), MODE_FULL);
6026        assert_eq!(parse_interface_mode("Full"), MODE_FULL);
6027        assert_eq!(parse_interface_mode("access_point"), MODE_ACCESS_POINT);
6028        assert_eq!(parse_interface_mode("accesspoint"), MODE_ACCESS_POINT);
6029        assert_eq!(parse_interface_mode("ap"), MODE_ACCESS_POINT);
6030        assert_eq!(parse_interface_mode("AP"), MODE_ACCESS_POINT);
6031        assert_eq!(parse_interface_mode("pointtopoint"), MODE_POINT_TO_POINT);
6032        assert_eq!(parse_interface_mode("ptp"), MODE_POINT_TO_POINT);
6033        assert_eq!(parse_interface_mode("roaming"), MODE_ROAMING);
6034        assert_eq!(parse_interface_mode("boundary"), MODE_BOUNDARY);
6035        assert_eq!(parse_interface_mode("gateway"), MODE_GATEWAY);
6036        assert_eq!(parse_interface_mode("gw"), MODE_GATEWAY);
6037        assert_eq!(parse_interface_mode("internal"), MODE_INTERNAL);
6038        assert_eq!(parse_interface_mode("Internal"), MODE_INTERNAL);
6039        // Unknown defaults to FULL
6040        assert_eq!(parse_interface_mode("invalid"), MODE_FULL);
6041    }
6042
6043    #[test]
6044    fn discovery_mode_normalization_matrix() {
6045        use rns_core::constants::*;
6046
6047        let interface_types = [
6048            "RNodeInterface",
6049            "RNodeMultiInterface",
6050            "BackboneInterface",
6051            "TCPServerInterface",
6052            "AutoInterface",
6053        ];
6054        let modes = [
6055            MODE_FULL,
6056            MODE_ACCESS_POINT,
6057            MODE_POINT_TO_POINT,
6058            MODE_ROAMING,
6059            MODE_BOUNDARY,
6060            MODE_GATEWAY,
6061            MODE_INTERNAL,
6062        ];
6063
6064        for interface_type in interface_types {
6065            for configured_mode in modes {
6066                for discoverable in [false, true] {
6067                    for ignore_warnings in [false, true] {
6068                        let expected = if !discoverable
6069                            || ignore_warnings
6070                            || matches!(
6071                                configured_mode,
6072                                MODE_ACCESS_POINT | MODE_GATEWAY | MODE_INTERNAL
6073                            ) {
6074                            configured_mode
6075                        } else if matches!(interface_type, "RNodeInterface" | "RNodeMultiInterface")
6076                        {
6077                            MODE_ACCESS_POINT
6078                        } else {
6079                            MODE_GATEWAY
6080                        };
6081                        assert_eq!(
6082                            normalize_discovery_mode(
6083                                interface_type,
6084                                configured_mode,
6085                                discoverable,
6086                                ignore_warnings,
6087                            ),
6088                            expected,
6089                            "type={interface_type}, mode={configured_mode}, discoverable={discoverable}, ignore={ignore_warnings}",
6090                        );
6091                    }
6092                }
6093            }
6094        }
6095    }
6096
6097    #[test]
6098    fn to_node_config_serial() {
6099        // Verify from_config parses SerialInterface correctly.
6100        // The serial port won't exist, so start() will fail, but the config
6101        // parsing path is exercised. We verify via the error (not a config error).
6102        let dir = std::env::temp_dir().join(format!("rns-test-serial-{}", std::process::id()));
6103        let _ = fs::remove_dir_all(&dir);
6104        fs::create_dir_all(&dir).unwrap();
6105
6106        let config = r#"
6107[reticulum]
6108enable_transport = False
6109
6110[interfaces]
6111  [[Test Serial Port]]
6112    type = SerialInterface
6113    port = /dev/nonexistent_rns_test_serial
6114    speed = 115200
6115    databits = 8
6116    parity = E
6117    stopbits = 1
6118    interface_mode = ptp
6119    networkname = testnet
6120"#;
6121        fs::write(dir.join("config"), config).unwrap();
6122
6123        // Interface error is non-fatal: the node starts but logs the error.
6124        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks))
6125            .expect("Config should parse; interface failure is non-fatal");
6126        node.shutdown();
6127
6128        let _ = fs::remove_dir_all(&dir);
6129    }
6130
6131    #[test]
6132    fn to_node_config_kiss() {
6133        // Verify from_config parses KISSInterface correctly.
6134        let dir = std::env::temp_dir().join(format!("rns-test-kiss-{}", std::process::id()));
6135        let _ = fs::remove_dir_all(&dir);
6136        fs::create_dir_all(&dir).unwrap();
6137
6138        let config = r#"
6139[reticulum]
6140enable_transport = False
6141
6142[interfaces]
6143  [[Test KISS TNC]]
6144    type = KISSInterface
6145    port = /dev/nonexistent_rns_test_kiss
6146    speed = 9600
6147    preamble = 500
6148    txtail = 30
6149    persistence = 128
6150    slottime = 40
6151    flow_control = True
6152    id_interval = 600
6153    id_callsign = TEST0
6154    interface_mode = full
6155    passphrase = secretkey
6156"#;
6157        fs::write(dir.join("config"), config).unwrap();
6158
6159        // Interface error is non-fatal: the node starts but logs the error.
6160        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks))
6161            .expect("Config should parse; interface failure is non-fatal");
6162        node.shutdown();
6163
6164        let _ = fs::remove_dir_all(&dir);
6165    }
6166
6167    #[test]
6168    fn config_parser_accepts_recursive_prs_interface_option() {
6169        let config = r#"
6170[reticulum]
6171enable_transport = True
6172
6173[interfaces]
6174  [[Recursive Client]]
6175    type = TCPClientInterface
6176    target_host = 127.0.0.1
6177    target_port = 4242
6178    interface_mode = full
6179    recursive_prs = yes
6180
6181  [[Default Client]]
6182    type = TCPClientInterface
6183    target_host = 127.0.0.1
6184    target_port = 4243
6185    interface_mode = gateway
6186"#;
6187
6188        let parsed = config::parse(config).unwrap();
6189
6190        assert_eq!(parsed.interfaces.len(), 2);
6191        assert_eq!(parsed.interfaces[0].name, "Recursive Client");
6192        assert_eq!(parsed.interfaces[0].mode, "full");
6193        assert_eq!(
6194            parsed.interfaces[0]
6195                .params
6196                .get("recursive_prs")
6197                .map(String::as_str),
6198            Some("yes")
6199        );
6200        assert_eq!(parsed.interfaces[1].name, "Default Client");
6201        assert_eq!(parsed.interfaces[1].mode, "gateway");
6202        assert!(!parsed.interfaces[1].params.contains_key("recursive_prs"));
6203    }
6204
6205    #[test]
6206    fn config_parser_accepts_announces_from_internal_interface_option() {
6207        let config = r#"
6208[reticulum]
6209enable_transport = True
6210
6211[interfaces]
6212  [[Filtered Client]]
6213    type = TCPClientInterface
6214    target_host = 127.0.0.1
6215    target_port = 4242
6216    interface_mode = gateway
6217    announces_from_internal = no
6218
6219  [[Default Client]]
6220    type = TCPClientInterface
6221    target_host = 127.0.0.1
6222    target_port = 4243
6223    interface_mode = full
6224"#;
6225
6226        let parsed = config::parse(config).unwrap();
6227
6228        assert_eq!(parsed.interfaces.len(), 2);
6229        assert_eq!(parsed.interfaces[0].name, "Filtered Client");
6230        assert_eq!(parsed.interfaces[0].mode, "gateway");
6231        assert_eq!(
6232            parsed.interfaces[0]
6233                .params
6234                .get("announces_from_internal")
6235                .map(String::as_str),
6236            Some("no")
6237        );
6238        assert_eq!(parsed.interfaces[1].name, "Default Client");
6239        assert_eq!(parsed.interfaces[1].mode, "full");
6240        assert!(!parsed.interfaces[1]
6241            .params
6242            .contains_key("announces_from_internal"));
6243    }
6244
6245    #[test]
6246    fn config_parser_preserves_announces_to_internal_tristate() {
6247        let config = r#"
6248[interfaces]
6249  [[Allowed Boundary]]
6250    type = TCPClientInterface
6251    target_host = 127.0.0.1
6252    target_port = 4242
6253    announces_to_internal = yes
6254
6255  [[Blocked Boundary]]
6256    type = TCPClientInterface
6257    target_host = 127.0.0.1
6258    target_port = 4243
6259    announces_to_internal = no
6260
6261  [[Default Boundary]]
6262    type = TCPClientInterface
6263    target_host = 127.0.0.1
6264    target_port = 4244
6265"#;
6266
6267        let parsed = config::parse(config).unwrap();
6268        let parsed_value = |index: usize| {
6269            parsed.interfaces[index]
6270                .params
6271                .get("announces_to_internal")
6272                .and_then(|value| config::parse_bool_pub(value))
6273        };
6274
6275        assert_eq!(parsed_value(0), Some(true));
6276        assert_eq!(parsed_value(1), Some(false));
6277        assert_eq!(parsed_value(2), None);
6278    }
6279
6280    #[test]
6281    fn interface_gravity_parser_accepts_signed_values_and_uses_global_default() {
6282        assert_eq!(parse_interface_gravity(Some("7"), -2), 7);
6283        assert_eq!(parse_interface_gravity(Some("-4"), 2), -4);
6284        assert_eq!(parse_interface_gravity(None, -6), -6);
6285    }
6286
6287    #[test]
6288    fn config_parser_accepts_local_hops_delta_option() {
6289        let cfg = r#"
6290[reticulum]
6291local_hops_delta = yes
6292"#;
6293        let parsed = config::parse(cfg).unwrap();
6294        assert!(parsed.reticulum.local_hops_delta);
6295    }
6296
6297    #[test]
6298    fn config_parser_accepts_static_transport_identity_option() {
6299        let config = r#"
6300[reticulum]
6301enable_transport = false
6302static_transport_identity = yes
6303"#;
6304
6305        let parsed = config::parse(config).unwrap();
6306
6307        assert!(!parsed.reticulum.enable_transport);
6308        assert!(parsed.reticulum.static_transport_identity);
6309    }
6310
6311    #[test]
6312    fn non_transport_node_uses_ephemeral_transport_identity_by_default() {
6313        let identity = Identity::new(&mut rns_crypto::FixedRng::new(&[0x41; 32]));
6314        let stored_hash = *identity.hash();
6315        let node = RnsNode::start(
6316            NodeConfig {
6317                transport_enabled: false,
6318                static_transport_identity: false,
6319                local_hops_delta: false,
6320                identity: Some(identity),
6321                share_instance: false,
6322                ..Default::default()
6323            },
6324            Box::new(NoopCallbacks),
6325        )
6326        .unwrap();
6327
6328        let response = node.query(QueryRequest::TransportIdentity).unwrap();
6329        let QueryResponse::TransportIdentity(Some(hash)) = response else {
6330            panic!("transport identity query returned {response:?}");
6331        };
6332        assert_ne!(hash, stored_hash);
6333
6334        node.shutdown();
6335    }
6336
6337    #[test]
6338    fn non_transport_node_can_use_static_transport_identity() {
6339        let identity = Identity::new(&mut rns_crypto::FixedRng::new(&[0x42; 32]));
6340        let stored_hash = *identity.hash();
6341        let node = RnsNode::start(
6342            NodeConfig {
6343                transport_enabled: false,
6344                static_transport_identity: true,
6345                local_hops_delta: false,
6346                identity: Some(identity),
6347                share_instance: false,
6348                ..Default::default()
6349            },
6350            Box::new(NoopCallbacks),
6351        )
6352        .unwrap();
6353
6354        let response = node.query(QueryRequest::TransportIdentity).unwrap();
6355        let QueryResponse::TransportIdentity(Some(hash)) = response else {
6356            panic!("transport identity query returned {response:?}");
6357        };
6358        assert_eq!(hash, stored_hash);
6359
6360        node.shutdown();
6361    }
6362
6363    #[test]
6364    fn discoverable_interface_forces_static_transport_identity_on_non_transport() {
6365        assert!(!effective_static_transport_identity(false, false, false));
6366        assert!(effective_static_transport_identity(true, false, false));
6367        assert!(!effective_static_transport_identity(false, true, false));
6368        assert!(effective_static_transport_identity(false, false, true));
6369        assert!(effective_static_transport_identity(true, false, true));
6370    }
6371
6372    #[test]
6373    fn test_extract_ifac_config() {
6374        use std::collections::HashMap;
6375
6376        // No IFAC params → None
6377        let params: HashMap<String, String> = HashMap::new();
6378        assert!(extract_ifac_config(&params, 16).is_none());
6379
6380        // networkname only
6381        let mut params = HashMap::new();
6382        params.insert("networkname".into(), "testnet".into());
6383        let ifac = extract_ifac_config(&params, 16).unwrap();
6384        assert_eq!(ifac.netname.as_deref(), Some("testnet"));
6385        assert!(ifac.netkey.is_none());
6386        assert_eq!(ifac.size, 16);
6387
6388        // passphrase only with custom size (in bits)
6389        let mut params = HashMap::new();
6390        params.insert("passphrase".into(), "secret".into());
6391        params.insert("ifac_size".into(), "64".into()); // 64 bits = 8 bytes
6392        let ifac = extract_ifac_config(&params, 16).unwrap();
6393        assert!(ifac.netname.is_none());
6394        assert_eq!(ifac.netkey.as_deref(), Some("secret"));
6395        assert_eq!(ifac.size, 8);
6396
6397        // Both with alternate key names
6398        let mut params = HashMap::new();
6399        params.insert("network_name".into(), "mynet".into());
6400        params.insert("pass_phrase".into(), "mykey".into());
6401        let ifac = extract_ifac_config(&params, 8).unwrap();
6402        assert_eq!(ifac.netname.as_deref(), Some("mynet"));
6403        assert_eq!(ifac.netkey.as_deref(), Some("mykey"));
6404        assert_eq!(ifac.size, 8);
6405
6406        // Nonsensical "None" network name and passphrase are ignored.
6407        let mut params = HashMap::new();
6408        params.insert("networkname".into(), "None".into());
6409        params.insert("passphrase".into(), "None".into());
6410        assert!(extract_ifac_config(&params, 16).is_none());
6411
6412        // Empty values are ignored, but a valid sibling value still applies.
6413        let mut params = HashMap::new();
6414        params.insert("networkname".into(), String::new());
6415        params.insert("passphrase".into(), "secret".into());
6416        let ifac = extract_ifac_config(&params, 16).unwrap();
6417        assert!(ifac.netname.is_none());
6418        assert_eq!(ifac.netkey.as_deref(), Some("secret"));
6419    }
6420
6421    #[test]
6422    fn to_node_config_rnode() {
6423        // Verify from_config parses RNodeInterface correctly.
6424        // The serial port won't exist, so start() will fail at open time.
6425        let dir = std::env::temp_dir().join(format!("rns-test-rnode-{}", std::process::id()));
6426        let _ = fs::remove_dir_all(&dir);
6427        fs::create_dir_all(&dir).unwrap();
6428
6429        let config = r#"
6430[reticulum]
6431enable_transport = False
6432
6433[interfaces]
6434  [[Test RNode]]
6435    type = RNodeInterface
6436    port = /dev/nonexistent_rns_test_rnode
6437    frequency = 867200000
6438    bandwidth = 125000
6439    txpower = 7
6440    spreadingfactor = 8
6441    codingrate = 5
6442    flow_control = True
6443    st_alock = 5.0
6444    lt_alock = 2.5
6445    interface_mode = full
6446    networkname = testnet
6447"#;
6448        fs::write(dir.join("config"), config).unwrap();
6449
6450        // Interface error is non-fatal: the node starts but logs the error.
6451        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks))
6452            .expect("Config should parse; interface failure is non-fatal");
6453        node.shutdown();
6454
6455        let _ = fs::remove_dir_all(&dir);
6456    }
6457
6458    #[test]
6459    fn to_node_config_pipe() {
6460        // Verify from_config parses PipeInterface correctly.
6461        // Use `cat` as a real command so it actually starts.
6462        let dir = std::env::temp_dir().join(format!("rns-test-pipe-{}", std::process::id()));
6463        let _ = fs::remove_dir_all(&dir);
6464        fs::create_dir_all(&dir).unwrap();
6465
6466        let config = r#"
6467[reticulum]
6468enable_transport = False
6469
6470[interfaces]
6471  [[Test Pipe]]
6472    type = PipeInterface
6473    command = cat
6474    respawn_delay = 5000
6475    interface_mode = full
6476"#;
6477        fs::write(dir.join("config"), config).unwrap();
6478
6479        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks)).unwrap();
6480        // If we got here, config parsing and start() succeeded
6481        node.shutdown();
6482
6483        let _ = fs::remove_dir_all(&dir);
6484    }
6485
6486    #[test]
6487    fn to_node_config_backbone() {
6488        // Verify from_config parses BackboneInterface correctly.
6489        let dir = std::env::temp_dir().join(format!("rns-test-backbone-{}", std::process::id()));
6490        let _ = fs::remove_dir_all(&dir);
6491        fs::create_dir_all(&dir).unwrap();
6492
6493        let port = crate::test_support::port();
6494
6495        let config = format!(
6496            r#"
6497[reticulum]
6498enable_transport = False
6499
6500[interfaces]
6501  [[Test Backbone]]
6502    type = BackboneInterface
6503    listen_ip = 127.0.0.1
6504    listen_port = {}
6505    interface_mode = full
6506"#,
6507            port
6508        );
6509
6510        fs::write(dir.join("config"), config).unwrap();
6511
6512        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks)).unwrap();
6513
6514        // Give server time to start
6515        thread::sleep(Duration::from_millis(100));
6516
6517        // Should be able to connect
6518        {
6519            let _client = std::net::TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
6520            // client drops here, closing the connection cleanly
6521        }
6522
6523        // Small delay to let epoll process the disconnect
6524        thread::sleep(Duration::from_millis(50));
6525
6526        node.shutdown();
6527        let _ = fs::remove_dir_all(&dir);
6528    }
6529
6530    #[test]
6531    fn rnode_config_defaults() {
6532        use crate::interface::rnode::{RNodeConfig, RNodeSubConfig};
6533
6534        let config = RNodeConfig::default();
6535        assert_eq!(config.speed, 115200);
6536        assert!(config.subinterfaces.is_empty());
6537        assert!(config.id_interval.is_none());
6538        assert!(config.id_callsign.is_none());
6539
6540        let sub = RNodeSubConfig {
6541            name: "test".into(),
6542            vport: 0,
6543            outgoing: true,
6544            frequency: 868_000_000,
6545            bandwidth: 125_000,
6546            txpower: 7,
6547            spreading_factor: 8,
6548            coding_rate: 5,
6549            flow_control: false,
6550            st_alock: None,
6551            lt_alock: None,
6552        };
6553        assert_eq!(sub.frequency, 868_000_000);
6554        assert_eq!(sub.bandwidth, 125_000);
6555        assert!(!sub.flow_control);
6556    }
6557
6558    // =========================================================================
6559    // Phase 9c: Announce + Discovery node-level tests
6560    // =========================================================================
6561
6562    #[test]
6563    fn announce_without_persisted_stamp_is_deferred_to_the_stamper() {
6564        let dir = tempdir().unwrap();
6565        let store = crate::stamps::DestinationStampStore::new(dir.path());
6566        let targets = crate::stamper::StampTargets {
6567            destination_value: StampCost::from_whole(6),
6568            path_request_value: StampCost::from_whole(6),
6569        };
6570        let mut node = RnsNode::start(NodeConfig::default(), Box::new(NoopCallbacks)).unwrap();
6571        node.set_stamp_store(store.clone());
6572        node.set_destination_stamping(true, targets);
6573        let stamper = Arc::clone(node.stamper().expect("started nodes share a stamper"));
6574        assert_eq!(
6575            stamper.targets(),
6576            targets,
6577            "the stamper grinds at the node's targets"
6578        );
6579
6580        let identity = Identity::from_private_key(&[0x51; 64]);
6581        let dest = crate::Destination::single_in(
6582            "stamper",
6583            &["node"],
6584            rns_core::types::IdentityHash(*identity.hash()),
6585        );
6586        node.register_destination(dest.hash.0, dest.dest_type.to_wire_constant())
6587            .unwrap();
6588        assert!(node.announce_needs_stamp(&dest, &identity));
6589
6590        // Destination.announce with STAMP_DESTINATIONS and no stamp: the call
6591        // returns at once and the worker grinds, persists and emits.
6592        let started = std::time::Instant::now();
6593        node.announce_queued(&dest, &identity, Some(b"first"))
6594            .unwrap();
6595        assert!(started.elapsed() < Duration::from_secs(1));
6596
6597        let name_hash = rns_core::destination::name_hash("stamper", &["node"]);
6598        let material = crate::stamps::destination_stamp_material(
6599            &dest.hash.0,
6600            &identity.get_public_key().unwrap(),
6601            &name_hash,
6602        );
6603        let deadline = std::time::Instant::now() + Duration::from_secs(30);
6604        while store
6605            .load(&dest.hash.0, &material, targets.destination_value)
6606            .is_none()
6607        {
6608            assert!(
6609                std::time::Instant::now() < deadline,
6610                "stamp was never persisted"
6611            );
6612            thread::sleep(Duration::from_millis(20));
6613        }
6614
6615        // From now on announces are synchronous: the persisted stamp is reused.
6616        assert!(!node.announce_needs_stamp(&dest, &identity));
6617        node.announce_queued(&dest, &identity, Some(b"second"))
6618            .unwrap();
6619        assert_eq!(stamper.queued(), 0);
6620        let receipt = node
6621            .try_announce(&dest, &identity, None)
6622            .expect("synchronous stamped announce is admitted");
6623        // No interfaces: the receipt settles either way, it must not hang.
6624        let _ = futures::executor::block_on(receipt);
6625
6626        // A confirmed-transmission announce for a destination still without a
6627        // stamp also defers, and its receipt settles once the worker emitted.
6628        let other_identity = Identity::from_private_key(&[0x52; 64]);
6629        let other = crate::Destination::single_in(
6630            "stamper",
6631            &["node", "other"],
6632            rns_core::types::IdentityHash(*other_identity.hash()),
6633        );
6634        node.register_destination(other.hash.0, other.dest_type.to_wire_constant())
6635            .unwrap();
6636        let started = std::time::Instant::now();
6637        let receipt = node
6638            .try_announce(&other, &other_identity, Some(b"deferred"))
6639            .expect("deferred announce is admitted");
6640        assert!(started.elapsed() < Duration::from_secs(1));
6641        let _ = futures::executor::block_on(receipt);
6642        assert!(!node.announce_needs_stamp(&other, &other_identity));
6643
6644        node.shutdown();
6645    }
6646
6647    #[test]
6648    fn announce_builds_valid_packet() {
6649        let identity = Identity::new(&mut OsRng);
6650        let identity_hash = rns_core::types::IdentityHash(*identity.hash());
6651
6652        let node = RnsNode::start(
6653            NodeConfig {
6654                panic_on_interface_error: false,
6655                link_proof_verify_threads: Default::default(),
6656                stamps: Default::default(),
6657                transport_enabled: false,
6658                static_transport_identity: false,
6659                local_hops_delta: false,
6660                identity: None,
6661                interfaces: vec![],
6662                share_instance: false,
6663                instance_name: "default".into(),
6664                shared_instance_port: 37428,
6665                rpc_port: 0,
6666                cache_dir: None,
6667                ratchet_store: None,
6668                ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
6669                management: Default::default(),
6670                probe_port: None,
6671                probe_addrs: vec![],
6672                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
6673                direct_connect_policy: Default::default(),
6674                direct_connect_allow_private_endpoints: false,
6675                device: None,
6676                underlay_mark: None,
6677                hooks: Vec::new(),
6678                discover_interfaces: false,
6679                autoconnect_interface_mode: None,
6680                autoconnect_interface_gravity: 0,
6681                autoconnect_announces_to_internal: false,
6682                autoconnect_unverified_implementations: false,
6683                discovery_required_value: None,
6684                respond_to_probes: false,
6685                prefer_shorter_path: false,
6686                max_paths_per_destination: 1,
6687                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
6688                packet_hashlist_allocation:
6689                    rns_core::transport::types::PacketHashlistAllocation::Eager,
6690                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
6691                max_path_destinations: usize::MAX,
6692                max_tunnel_destinations_total: usize::MAX,
6693                known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
6694                known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
6695                announce_table_ttl: Duration::from_secs(
6696                    rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
6697                ),
6698                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
6699                driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
6700                interface_writer_queue_capacity:
6701                    crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
6702                announce_rate_defaults: AnnounceRateDefaults::default(),
6703                ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(
6704                ),
6705                #[cfg(feature = "iface-backbone")]
6706                backbone_peer_pool: None,
6707                announce_sig_cache_enabled: true,
6708                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
6709                announce_sig_cache_ttl: Duration::from_secs(
6710                    rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
6711                ),
6712                registry: None,
6713                #[cfg(feature = "hooks")]
6714                provider_bridge: None,
6715            },
6716            Box::new(NoopCallbacks),
6717        )
6718        .unwrap();
6719
6720        let dest = crate::destination::Destination::single_in("test", &["echo"], identity_hash);
6721
6722        // Register destination first
6723        node.register_destination(dest.hash.0, dest.dest_type.to_wire_constant())
6724            .unwrap();
6725
6726        // Announce should succeed (though no interfaces to send on)
6727        let result = node.announce_queued(&dest, &identity, Some(b"hello"));
6728        assert!(result.is_ok());
6729
6730        node.shutdown();
6731    }
6732
6733    #[test]
6734    fn has_path_and_hops_to() {
6735        let node = RnsNode::start(
6736            NodeConfig {
6737                panic_on_interface_error: false,
6738                link_proof_verify_threads: Default::default(),
6739                stamps: Default::default(),
6740                transport_enabled: false,
6741                static_transport_identity: false,
6742                local_hops_delta: false,
6743                identity: None,
6744                interfaces: vec![],
6745                share_instance: false,
6746                instance_name: "default".into(),
6747                shared_instance_port: 37428,
6748                rpc_port: 0,
6749                cache_dir: None,
6750                ratchet_store: None,
6751                ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
6752                management: Default::default(),
6753                probe_port: None,
6754                probe_addrs: vec![],
6755                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
6756                direct_connect_policy: Default::default(),
6757                direct_connect_allow_private_endpoints: false,
6758                device: None,
6759                underlay_mark: None,
6760                hooks: Vec::new(),
6761                discover_interfaces: false,
6762                autoconnect_interface_mode: None,
6763                autoconnect_interface_gravity: 0,
6764                autoconnect_announces_to_internal: false,
6765                autoconnect_unverified_implementations: false,
6766                discovery_required_value: None,
6767                respond_to_probes: false,
6768                prefer_shorter_path: false,
6769                max_paths_per_destination: 1,
6770                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
6771                packet_hashlist_allocation:
6772                    rns_core::transport::types::PacketHashlistAllocation::Eager,
6773                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
6774                max_path_destinations: usize::MAX,
6775                max_tunnel_destinations_total: usize::MAX,
6776                known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
6777                known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
6778                announce_table_ttl: Duration::from_secs(
6779                    rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
6780                ),
6781                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
6782                driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
6783                interface_writer_queue_capacity:
6784                    crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
6785                announce_rate_defaults: AnnounceRateDefaults::default(),
6786                ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(
6787                ),
6788                #[cfg(feature = "iface-backbone")]
6789                backbone_peer_pool: None,
6790                announce_sig_cache_enabled: true,
6791                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
6792                announce_sig_cache_ttl: Duration::from_secs(
6793                    rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
6794                ),
6795                registry: None,
6796                #[cfg(feature = "hooks")]
6797                provider_bridge: None,
6798            },
6799            Box::new(NoopCallbacks),
6800        )
6801        .unwrap();
6802
6803        let dh = rns_core::types::DestHash([0xAA; 16]);
6804
6805        // No path should exist
6806        assert!(!node.has_path(&dh).unwrap());
6807        assert_eq!(node.hops_to(&dh).unwrap(), None);
6808
6809        node.shutdown();
6810    }
6811
6812    #[test]
6813    fn recall_identity_none_when_unknown() {
6814        let node = RnsNode::start(
6815            NodeConfig {
6816                panic_on_interface_error: false,
6817                link_proof_verify_threads: Default::default(),
6818                stamps: Default::default(),
6819                transport_enabled: false,
6820                static_transport_identity: false,
6821                local_hops_delta: false,
6822                identity: None,
6823                interfaces: vec![],
6824                share_instance: false,
6825                instance_name: "default".into(),
6826                shared_instance_port: 37428,
6827                rpc_port: 0,
6828                cache_dir: None,
6829                ratchet_store: None,
6830                ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
6831                management: Default::default(),
6832                probe_port: None,
6833                probe_addrs: vec![],
6834                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
6835                direct_connect_policy: Default::default(),
6836                direct_connect_allow_private_endpoints: false,
6837                device: None,
6838                underlay_mark: None,
6839                hooks: Vec::new(),
6840                discover_interfaces: false,
6841                autoconnect_interface_mode: None,
6842                autoconnect_interface_gravity: 0,
6843                autoconnect_announces_to_internal: false,
6844                autoconnect_unverified_implementations: false,
6845                discovery_required_value: None,
6846                respond_to_probes: false,
6847                prefer_shorter_path: false,
6848                max_paths_per_destination: 1,
6849                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
6850                packet_hashlist_allocation:
6851                    rns_core::transport::types::PacketHashlistAllocation::Eager,
6852                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
6853                max_path_destinations: usize::MAX,
6854                max_tunnel_destinations_total: usize::MAX,
6855                known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
6856                known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
6857                announce_table_ttl: Duration::from_secs(
6858                    rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
6859                ),
6860                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
6861                driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
6862                interface_writer_queue_capacity:
6863                    crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
6864                announce_rate_defaults: AnnounceRateDefaults::default(),
6865                ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(
6866                ),
6867                #[cfg(feature = "iface-backbone")]
6868                backbone_peer_pool: None,
6869                announce_sig_cache_enabled: true,
6870                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
6871                announce_sig_cache_ttl: Duration::from_secs(
6872                    rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
6873                ),
6874                registry: None,
6875                #[cfg(feature = "hooks")]
6876                provider_bridge: None,
6877            },
6878            Box::new(NoopCallbacks),
6879        )
6880        .unwrap();
6881
6882        let dh = rns_core::types::DestHash([0xBB; 16]);
6883        assert!(node.recall_identity(&dh).unwrap().is_none());
6884
6885        node.shutdown();
6886    }
6887
6888    #[test]
6889    fn request_path_does_not_crash() {
6890        let node = RnsNode::start(
6891            NodeConfig {
6892                panic_on_interface_error: false,
6893                link_proof_verify_threads: Default::default(),
6894                stamps: Default::default(),
6895                transport_enabled: false,
6896                static_transport_identity: false,
6897                local_hops_delta: false,
6898                identity: None,
6899                interfaces: vec![],
6900                share_instance: false,
6901                instance_name: "default".into(),
6902                shared_instance_port: 37428,
6903                rpc_port: 0,
6904                cache_dir: None,
6905                ratchet_store: None,
6906                ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
6907                management: Default::default(),
6908                probe_port: None,
6909                probe_addrs: vec![],
6910                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
6911                direct_connect_policy: Default::default(),
6912                direct_connect_allow_private_endpoints: false,
6913                device: None,
6914                underlay_mark: None,
6915                hooks: Vec::new(),
6916                discover_interfaces: false,
6917                autoconnect_interface_mode: None,
6918                autoconnect_interface_gravity: 0,
6919                autoconnect_announces_to_internal: false,
6920                autoconnect_unverified_implementations: false,
6921                discovery_required_value: None,
6922                respond_to_probes: false,
6923                prefer_shorter_path: false,
6924                max_paths_per_destination: 1,
6925                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
6926                packet_hashlist_allocation:
6927                    rns_core::transport::types::PacketHashlistAllocation::Eager,
6928                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
6929                max_path_destinations: usize::MAX,
6930                max_tunnel_destinations_total: usize::MAX,
6931                known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
6932                known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
6933                announce_table_ttl: Duration::from_secs(
6934                    rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
6935                ),
6936                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
6937                driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
6938                interface_writer_queue_capacity:
6939                    crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
6940                announce_rate_defaults: AnnounceRateDefaults::default(),
6941                ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(
6942                ),
6943                #[cfg(feature = "iface-backbone")]
6944                backbone_peer_pool: None,
6945                announce_sig_cache_enabled: true,
6946                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
6947                announce_sig_cache_ttl: Duration::from_secs(
6948                    rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
6949                ),
6950                registry: None,
6951                #[cfg(feature = "hooks")]
6952                provider_bridge: None,
6953            },
6954            Box::new(NoopCallbacks),
6955        )
6956        .unwrap();
6957
6958        let dh = rns_core::types::DestHash([0xCC; 16]);
6959        assert!(node.request_path(&dh).is_ok());
6960
6961        // Small wait for the event to be processed
6962        thread::sleep(Duration::from_millis(50));
6963
6964        node.shutdown();
6965    }
6966
6967    #[test]
6968    fn create_link_returns_error_while_draining() {
6969        let node = RnsNode::start(NodeConfig::default(), Box::new(NoopCallbacks)).unwrap();
6970
6971        node.begin_drain(Duration::from_secs(1)).unwrap();
6972        assert!(node.create_link([0xAB; 16], [0xCD; 32]).is_err());
6973
6974        node.shutdown();
6975    }
6976
6977    #[test]
6978    fn request_path_returns_error_while_draining() {
6979        let node = RnsNode::start(NodeConfig::default(), Box::new(NoopCallbacks)).unwrap();
6980
6981        node.begin_drain(Duration::from_secs(1)).unwrap();
6982        assert!(node
6983            .request_path(&rns_core::types::DestHash([0xAB; 16]))
6984            .is_err());
6985
6986        node.shutdown();
6987    }
6988
6989    // =========================================================================
6990    // Phase 9d: send_packet + register_destination_with_proof tests
6991    // =========================================================================
6992
6993    #[test]
6994    fn send_packet_returns_error_while_draining() {
6995        let node = RnsNode::start(NodeConfig::default(), Box::new(NoopCallbacks)).unwrap();
6996        let dest = crate::destination::Destination::plain("drain-test", &["send"]);
6997
6998        node.begin_drain(Duration::from_secs(1)).unwrap();
6999        assert!(node.send_packet_queued(&dest, b"hello").is_err());
7000
7001        node.shutdown();
7002    }
7003
7004    #[test]
7005    fn send_packet_plain() {
7006        let node = RnsNode::start(
7007            NodeConfig {
7008                panic_on_interface_error: false,
7009                link_proof_verify_threads: Default::default(),
7010                stamps: Default::default(),
7011                transport_enabled: false,
7012                static_transport_identity: false,
7013                local_hops_delta: false,
7014                identity: None,
7015                interfaces: vec![],
7016                share_instance: false,
7017                instance_name: "default".into(),
7018                shared_instance_port: 37428,
7019                rpc_port: 0,
7020                cache_dir: None,
7021                ratchet_store: None,
7022                ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
7023                management: Default::default(),
7024                probe_port: None,
7025                probe_addrs: vec![],
7026                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
7027                direct_connect_policy: Default::default(),
7028                direct_connect_allow_private_endpoints: false,
7029                device: None,
7030                underlay_mark: None,
7031                hooks: Vec::new(),
7032                discover_interfaces: false,
7033                autoconnect_interface_mode: None,
7034                autoconnect_interface_gravity: 0,
7035                autoconnect_announces_to_internal: false,
7036                autoconnect_unverified_implementations: false,
7037                discovery_required_value: None,
7038                respond_to_probes: false,
7039                prefer_shorter_path: false,
7040                max_paths_per_destination: 1,
7041                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
7042                packet_hashlist_allocation:
7043                    rns_core::transport::types::PacketHashlistAllocation::Eager,
7044                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
7045                max_path_destinations: usize::MAX,
7046                max_tunnel_destinations_total: usize::MAX,
7047                known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
7048                known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
7049                announce_table_ttl: Duration::from_secs(
7050                    rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
7051                ),
7052                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
7053                driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
7054                interface_writer_queue_capacity:
7055                    crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
7056                announce_rate_defaults: AnnounceRateDefaults::default(),
7057                ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(
7058                ),
7059                #[cfg(feature = "iface-backbone")]
7060                backbone_peer_pool: None,
7061                announce_sig_cache_enabled: true,
7062                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
7063                announce_sig_cache_ttl: Duration::from_secs(
7064                    rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
7065                ),
7066                registry: None,
7067                #[cfg(feature = "hooks")]
7068                provider_bridge: None,
7069            },
7070            Box::new(NoopCallbacks),
7071        )
7072        .unwrap();
7073
7074        let dest = crate::destination::Destination::plain("test", &["echo"]);
7075        let result = node.send_packet_queued(&dest, b"hello world");
7076        assert!(result.is_ok());
7077
7078        let packet_hash = result.unwrap();
7079        // Packet hash should be non-zero
7080        assert_ne!(packet_hash.0, [0u8; 32]);
7081
7082        // Small wait for the event to be processed
7083        thread::sleep(Duration::from_millis(50));
7084
7085        node.shutdown();
7086    }
7087
7088    #[test]
7089    fn send_packet_single_requires_public_key() {
7090        let node = RnsNode::start(
7091            NodeConfig {
7092                panic_on_interface_error: false,
7093                link_proof_verify_threads: Default::default(),
7094                stamps: Default::default(),
7095                transport_enabled: false,
7096                static_transport_identity: false,
7097                local_hops_delta: false,
7098                identity: None,
7099                interfaces: vec![],
7100                share_instance: false,
7101                instance_name: "default".into(),
7102                shared_instance_port: 37428,
7103                rpc_port: 0,
7104                cache_dir: None,
7105                ratchet_store: None,
7106                ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
7107                management: Default::default(),
7108                probe_port: None,
7109                probe_addrs: vec![],
7110                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
7111                direct_connect_policy: Default::default(),
7112                direct_connect_allow_private_endpoints: false,
7113                device: None,
7114                underlay_mark: None,
7115                hooks: Vec::new(),
7116                discover_interfaces: false,
7117                autoconnect_interface_mode: None,
7118                autoconnect_interface_gravity: 0,
7119                autoconnect_announces_to_internal: false,
7120                autoconnect_unverified_implementations: false,
7121                discovery_required_value: None,
7122                respond_to_probes: false,
7123                prefer_shorter_path: false,
7124                max_paths_per_destination: 1,
7125                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
7126                packet_hashlist_allocation:
7127                    rns_core::transport::types::PacketHashlistAllocation::Eager,
7128                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
7129                max_path_destinations: usize::MAX,
7130                max_tunnel_destinations_total: usize::MAX,
7131                known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
7132                known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
7133                announce_table_ttl: Duration::from_secs(
7134                    rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
7135                ),
7136                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
7137                driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
7138                interface_writer_queue_capacity:
7139                    crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
7140                announce_rate_defaults: AnnounceRateDefaults::default(),
7141                ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(
7142                ),
7143                #[cfg(feature = "iface-backbone")]
7144                backbone_peer_pool: None,
7145                announce_sig_cache_enabled: true,
7146                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
7147                announce_sig_cache_ttl: Duration::from_secs(
7148                    rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
7149                ),
7150                registry: None,
7151                #[cfg(feature = "hooks")]
7152                provider_bridge: None,
7153            },
7154            Box::new(NoopCallbacks),
7155        )
7156        .unwrap();
7157
7158        // single_in has no public_key — sending should fail
7159        let dest = crate::destination::Destination::single_in(
7160            "test",
7161            &["echo"],
7162            rns_core::types::IdentityHash([0x42; 16]),
7163        );
7164        let result = node.send_packet_queued(&dest, b"hello");
7165        assert!(result.is_err(), "single_in has no public_key, should fail");
7166
7167        node.shutdown();
7168    }
7169
7170    #[test]
7171    fn send_packet_single_encrypts() {
7172        let node = RnsNode::start(
7173            NodeConfig {
7174                panic_on_interface_error: false,
7175                link_proof_verify_threads: Default::default(),
7176                stamps: Default::default(),
7177                transport_enabled: false,
7178                static_transport_identity: false,
7179                local_hops_delta: false,
7180                identity: None,
7181                interfaces: vec![],
7182                share_instance: false,
7183                instance_name: "default".into(),
7184                shared_instance_port: 37428,
7185                rpc_port: 0,
7186                cache_dir: None,
7187                ratchet_store: None,
7188                ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
7189                management: Default::default(),
7190                probe_port: None,
7191                probe_addrs: vec![],
7192                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
7193                direct_connect_policy: Default::default(),
7194                direct_connect_allow_private_endpoints: false,
7195                device: None,
7196                underlay_mark: None,
7197                hooks: Vec::new(),
7198                discover_interfaces: false,
7199                autoconnect_interface_mode: None,
7200                autoconnect_interface_gravity: 0,
7201                autoconnect_announces_to_internal: false,
7202                autoconnect_unverified_implementations: false,
7203                discovery_required_value: None,
7204                respond_to_probes: false,
7205                prefer_shorter_path: false,
7206                max_paths_per_destination: 1,
7207                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
7208                packet_hashlist_allocation:
7209                    rns_core::transport::types::PacketHashlistAllocation::Eager,
7210                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
7211                max_path_destinations: usize::MAX,
7212                max_tunnel_destinations_total: usize::MAX,
7213                known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
7214                known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
7215                announce_table_ttl: Duration::from_secs(
7216                    rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
7217                ),
7218                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
7219                driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
7220                interface_writer_queue_capacity:
7221                    crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
7222                announce_rate_defaults: AnnounceRateDefaults::default(),
7223                ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(
7224                ),
7225                #[cfg(feature = "iface-backbone")]
7226                backbone_peer_pool: None,
7227                announce_sig_cache_enabled: true,
7228                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
7229                announce_sig_cache_ttl: Duration::from_secs(
7230                    rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
7231                ),
7232                registry: None,
7233                #[cfg(feature = "hooks")]
7234                provider_bridge: None,
7235            },
7236            Box::new(NoopCallbacks),
7237        )
7238        .unwrap();
7239
7240        // Create a proper OUT SINGLE destination with a real identity's public key
7241        let remote_identity = Identity::new(&mut OsRng);
7242        let recalled = crate::destination::AnnouncedIdentity {
7243            dest_hash: rns_core::types::DestHash([0xAA; 16]),
7244            identity_hash: rns_core::types::IdentityHash(*remote_identity.hash()),
7245            public_key: remote_identity.get_public_key().unwrap(),
7246            app_data: None,
7247            hops: 1,
7248            received_at: 0.0,
7249            receiving_interface: rns_core::transport::types::InterfaceId(0),
7250            rssi: Some(-100),
7251            snr: Some(10.5),
7252        };
7253        let dest = crate::destination::Destination::single_out("test", &["echo"], &recalled);
7254
7255        let result = node.send_packet_queued(&dest, b"secret message");
7256        assert!(result.is_ok());
7257
7258        let packet_hash = result.unwrap();
7259        assert_ne!(packet_hash.0, [0u8; 32]);
7260
7261        thread::sleep(Duration::from_millis(50));
7262        node.shutdown();
7263    }
7264
7265    #[test]
7266    fn register_destination_with_proof_prove_all() {
7267        let node = RnsNode::start(
7268            NodeConfig {
7269                panic_on_interface_error: false,
7270                link_proof_verify_threads: Default::default(),
7271                stamps: Default::default(),
7272                transport_enabled: false,
7273                static_transport_identity: false,
7274                local_hops_delta: false,
7275                identity: None,
7276                interfaces: vec![],
7277                share_instance: false,
7278                instance_name: "default".into(),
7279                shared_instance_port: 37428,
7280                rpc_port: 0,
7281                cache_dir: None,
7282                ratchet_store: None,
7283                ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
7284                management: Default::default(),
7285                probe_port: None,
7286                probe_addrs: vec![],
7287                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
7288                direct_connect_policy: Default::default(),
7289                direct_connect_allow_private_endpoints: false,
7290                device: None,
7291                underlay_mark: None,
7292                hooks: Vec::new(),
7293                discover_interfaces: false,
7294                autoconnect_interface_mode: None,
7295                autoconnect_interface_gravity: 0,
7296                autoconnect_announces_to_internal: false,
7297                autoconnect_unverified_implementations: false,
7298                discovery_required_value: None,
7299                respond_to_probes: false,
7300                prefer_shorter_path: false,
7301                max_paths_per_destination: 1,
7302                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
7303                packet_hashlist_allocation:
7304                    rns_core::transport::types::PacketHashlistAllocation::Eager,
7305                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
7306                max_path_destinations: usize::MAX,
7307                max_tunnel_destinations_total: usize::MAX,
7308                known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
7309                known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
7310                announce_table_ttl: Duration::from_secs(
7311                    rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
7312                ),
7313                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
7314                driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
7315                interface_writer_queue_capacity:
7316                    crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
7317                announce_rate_defaults: AnnounceRateDefaults::default(),
7318                ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(
7319                ),
7320                #[cfg(feature = "iface-backbone")]
7321                backbone_peer_pool: None,
7322                announce_sig_cache_enabled: true,
7323                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
7324                announce_sig_cache_ttl: Duration::from_secs(
7325                    rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
7326                ),
7327                registry: None,
7328                #[cfg(feature = "hooks")]
7329                provider_bridge: None,
7330            },
7331            Box::new(NoopCallbacks),
7332        )
7333        .unwrap();
7334
7335        let identity = Identity::new(&mut OsRng);
7336        let ih = rns_core::types::IdentityHash(*identity.hash());
7337        let dest = crate::destination::Destination::single_in("echo", &["request"], ih)
7338            .set_proof_strategy(rns_core::types::ProofStrategy::ProveAll);
7339        let prv_key = identity.get_private_key().unwrap();
7340
7341        let result = node.register_destination_with_proof(&dest, Some(prv_key));
7342        assert!(result.is_ok());
7343
7344        // Small wait for the events to be processed
7345        thread::sleep(Duration::from_millis(50));
7346
7347        node.shutdown();
7348    }
7349
7350    #[test]
7351    fn register_destination_with_proof_prove_none() {
7352        let node = RnsNode::start(
7353            NodeConfig {
7354                panic_on_interface_error: false,
7355                link_proof_verify_threads: Default::default(),
7356                stamps: Default::default(),
7357                transport_enabled: false,
7358                static_transport_identity: false,
7359                local_hops_delta: false,
7360                identity: None,
7361                interfaces: vec![],
7362                share_instance: false,
7363                instance_name: "default".into(),
7364                shared_instance_port: 37428,
7365                rpc_port: 0,
7366                cache_dir: None,
7367                ratchet_store: None,
7368                ratchet_expiry: Duration::from_secs(rns_core::constants::RATCHET_EXPIRY),
7369                management: Default::default(),
7370                probe_port: None,
7371                probe_addrs: vec![],
7372                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
7373                direct_connect_policy: Default::default(),
7374                direct_connect_allow_private_endpoints: false,
7375                device: None,
7376                underlay_mark: None,
7377                hooks: Vec::new(),
7378                discover_interfaces: false,
7379                autoconnect_interface_mode: None,
7380                autoconnect_interface_gravity: 0,
7381                autoconnect_announces_to_internal: false,
7382                autoconnect_unverified_implementations: false,
7383                discovery_required_value: None,
7384                respond_to_probes: false,
7385                prefer_shorter_path: false,
7386                max_paths_per_destination: 1,
7387                packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
7388                packet_hashlist_allocation:
7389                    rns_core::transport::types::PacketHashlistAllocation::Eager,
7390                max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
7391                max_path_destinations: usize::MAX,
7392                max_tunnel_destinations_total: usize::MAX,
7393                known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
7394                known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
7395                announce_table_ttl: Duration::from_secs(
7396                    rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
7397                ),
7398                announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
7399                driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
7400                interface_writer_queue_capacity:
7401                    crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
7402                announce_rate_defaults: AnnounceRateDefaults::default(),
7403                ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(
7404                ),
7405                #[cfg(feature = "iface-backbone")]
7406                backbone_peer_pool: None,
7407                announce_sig_cache_enabled: true,
7408                announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
7409                announce_sig_cache_ttl: Duration::from_secs(
7410                    rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
7411                ),
7412                registry: None,
7413                #[cfg(feature = "hooks")]
7414                provider_bridge: None,
7415            },
7416            Box::new(NoopCallbacks),
7417        )
7418        .unwrap();
7419
7420        // ProveNone should not send RegisterProofStrategy event
7421        let dest = crate::destination::Destination::plain("test", &["data"])
7422            .set_proof_strategy(rns_core::types::ProofStrategy::ProveNone);
7423
7424        let result = node.register_destination_with_proof(&dest, None);
7425        assert!(result.is_ok());
7426
7427        thread::sleep(Duration::from_millis(50));
7428        node.shutdown();
7429    }
7430
7431    #[test]
7432    fn destination_stamp_below_target_value_is_regenerated() {
7433        // Destination._announce (d7749b0b): the held stamp is reloaded or
7434        // regenerated when its value is below `target_value`, and both the
7435        // lookup and the grind use the target (Destination.py:319-323).
7436        let dir = tempdir().unwrap();
7437        let store = crate::stamps::DestinationStampStore::new(dir.path());
7438        let targets = crate::stamper::StampTargets {
7439            destination_value: StampCost::from_whole(4),
7440            path_request_value: StampCost::from_whole(4),
7441        };
7442        let identity = Identity::from_private_key(&[0x52; 64]);
7443        let mut dest = crate::Destination::single_in(
7444            "stamper",
7445            &["target"],
7446            rns_core::types::IdentityHash(*identity.hash()),
7447        );
7448        let name_hash = rns_core::destination::name_hash("stamper", &["target"]);
7449        let material = crate::stamps::destination_stamp_material(
7450            &dest.hash.0,
7451            &identity.get_public_key().unwrap(),
7452            &name_hash,
7453        );
7454
7455        ensure_destination_stamp(&store, targets, &dest, &identity).unwrap();
7456        let (first, first_value) = store
7457            .load(&dest.hash.0, &material, StampCost::ZERO)
7458            .expect("stamp persisted at the default target");
7459        assert!(first_value.meets(StampCost::from_whole(4)));
7460
7461        // A target the held stamp already meets reuses it.
7462        dest.set_value(f64::from(first_value.whole()));
7463        ensure_destination_stamp(&store, targets, &dest, &identity).unwrap();
7464        assert_eq!(
7465            store
7466                .load(&dest.hash.0, &material, StampCost::ZERO)
7467                .unwrap()
7468                .0,
7469            first
7470        );
7471
7472        // A target above the held value regenerates and persists a stamp
7473        // that reaches it.
7474        let raised = first_value.whole() as u8 + 1;
7475        dest.set_value(f64::from(raised));
7476        ensure_destination_stamp(&store, targets, &dest, &identity).unwrap();
7477        let (second, second_value) = store
7478            .load(&dest.hash.0, &material, StampCost::ZERO)
7479            .unwrap();
7480        assert_ne!(second, first);
7481        assert!(second_value.meets(StampCost::from_whole(u32::from(raised))));
7482
7483        // The announce builder reloads at the same target and carries the
7484        // regenerated stamp and the configured default link cost.
7485        let (tx, _rx) = crate::event::channel();
7486        tx.link_request_costs()
7487            .set_default_cost(StampCost::from_whole(7));
7488        let (packet, _) =
7489            build_announce_packet(&tx, Some(&store), true, targets, &dest, &identity, None)
7490                .unwrap();
7491        let announce = rns_core::announce::AnnounceData::unpack(packet.data(), false).unwrap();
7492        let app_data = announce
7493            .app_data
7494            .expect("stamped announce carries app data");
7495        let ext = &app_data[app_data.len() - rns_core::constants::EXT_PAYLOAD_LENGTH..];
7496        assert_eq!(ext[0], rns_core::constants::EXT_PAYLOAD_MARKER);
7497        assert_eq!(
7498            &ext[rns_core::constants::EXT_HEADER_LENGTH
7499                ..rns_core::constants::EXT_HEADER_LENGTH + rns_core::constants::STAMP_LENGTH],
7500            &second[..]
7501        );
7502        assert_eq!(ext[rns_core::constants::EXT_PAYLOAD_LENGTH - 1], 7);
7503    }
7504
7505    #[test]
7506    fn from_config_applies_the_stamps_section_and_hop_limiter() {
7507        let dir = tempdir().unwrap();
7508        std::fs::write(
7509            dir.path().join("config"),
7510            "[reticulum]\n\
7511             hop_limiter = 12\n\
7512             [stamps]\n\
7513             enforce_stamps = yes\n\
7514             stamp_destinations = yes\n\
7515             stamp_prs = yes\n\
7516             always_stamp_lrs = yes\n\
7517             destination_cost = 18\n\
7518             destination_value = 27\n\
7519             pr_cost = 11\n\
7520             pr_value = 13\n\
7521             lr_cost = 9\n\
7522             pr_stamp_timeout = 3\n",
7523        )
7524        .unwrap();
7525        let node =
7526            RnsNode::from_private_config(dir.path(), dir.path(), None, Box::new(NoopCallbacks))
7527                .unwrap();
7528
7529        assert!(node.stamp_destinations.load(Ordering::Relaxed));
7530        assert_eq!(
7531            node.stamp_targets,
7532            crate::stamper::StampTargets {
7533                destination_value: StampCost::from_whole(27),
7534                path_request_value: StampCost::from_whole(13),
7535            }
7536        );
7537        // `lr_cost` and `enforce_stamps` reach the shared link cost table.
7538        assert_eq!(node.link_cost(&[0u8; 16]), 9.0);
7539        assert!(node.link_request_stamps_enforced());
7540        let stamper = node.stamper().unwrap();
7541        assert_eq!(stamper.targets(), node.stamp_targets);
7542        assert_eq!(stamper.path_request_timeout(), Duration::from_secs(3));
7543
7544        // The runtime keys report the configured values as their startup
7545        // defaults, so a reset returns to the config file and not to the
7546        // compile-time constants.
7547        for key in [
7548            "global.stamp_destinations",
7549            "global.stamp_path_requests",
7550            "global.stamps_enforced",
7551            "global.stamp_link_requests",
7552            "global.always_stamp_link_requests",
7553        ] {
7554            let QueryResponse::RuntimeConfigEntry(Some(entry)) = node
7555                .query(QueryRequest::GetRuntimeConfig { key: key.into() })
7556                .unwrap()
7557            else {
7558                panic!("expected runtime config entry for {key}");
7559            };
7560            assert_eq!(
7561                entry.value,
7562                crate::event::RuntimeConfigValue::Bool(true),
7563                "{key}"
7564            );
7565            assert_eq!(
7566                entry.default,
7567                crate::event::RuntimeConfigValue::Bool(true),
7568                "{key}"
7569            );
7570        }
7571        node.shutdown();
7572    }
7573
7574    #[test]
7575    fn start_applies_the_node_config_stamps() {
7576        // Upstream eb826cde: `Reticulum(enable_stamping=True,
7577        // enforce_stamps=True)` for a node configured in code.
7578        let node = RnsNode::start(
7579            NodeConfig {
7580                stamps: config::StampsSection {
7581                    stamp_destinations: true,
7582                    stamp_prs: true,
7583                    stamps_enforced: true,
7584                    lr_stamps_enforced: true,
7585                    lr_cost: StampCost::from_whole(9),
7586                    ..Default::default()
7587                },
7588                ..NodeConfig::default()
7589            },
7590            Box::new(NoopCallbacks),
7591        )
7592        .unwrap();
7593
7594        assert!(node.stamp_destinations.load(Ordering::Relaxed));
7595        assert_eq!(node.link_cost(&[0u8; 16]), 9.0);
7596        assert!(node.link_request_stamps_enforced());
7597        for key in [
7598            "global.stamp_destinations",
7599            "global.stamp_path_requests",
7600            "global.stamps_enforced",
7601            "global.lr_stamps_enforced",
7602        ] {
7603            let QueryResponse::RuntimeConfigEntry(Some(entry)) = node
7604                .query(QueryRequest::GetRuntimeConfig { key: key.into() })
7605                .unwrap()
7606            else {
7607                panic!("expected runtime config entry for {key}");
7608            };
7609            assert_eq!(
7610                entry.value,
7611                crate::event::RuntimeConfigValue::Bool(true),
7612                "{key}"
7613            );
7614            assert_eq!(
7615                entry.default,
7616                crate::event::RuntimeConfigValue::Bool(true),
7617                "{key}"
7618            );
7619        }
7620        node.shutdown();
7621    }
7622
7623    #[test]
7624    fn published_blackhole_destination_requires_its_link_cost() {
7625        let identity = Identity::new(&mut OsRng);
7626        let blackhole = crate::management::blackhole_dest_hash(identity.hash());
7627        let node = RnsNode::start(
7628            NodeConfig {
7629                identity: Some(Identity::from_private_key(
7630                    &identity.get_private_key().unwrap(),
7631                )),
7632                management: crate::ManagementConfig {
7633                    publish_blackhole: true,
7634                    ..Default::default()
7635                },
7636                ..NodeConfig::default()
7637            },
7638            Box::new(NoopCallbacks),
7639        )
7640        .unwrap();
7641
7642        assert_eq!(node.link_cost(&blackhole), 5.25);
7643        // Other destinations keep the `LR_COST` default.
7644        assert_eq!(node.link_cost(&[0u8; 16]), 12.5);
7645        node.shutdown();
7646    }
7647}