Skip to main content

rns_net/
node.rs

1//! RnsNode: high-level lifecycle management.
2//!
3//! Wires together the driver, interfaces, and timer thread.
4
5use std::io;
6use std::path::Path;
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::Arc;
9use std::thread::{self, JoinHandle};
10use std::time::Duration;
11
12use rns_core::transport::types::TransportConfig;
13use rns_crypto::identity::Identity;
14use rns_crypto::{OsRng, Rng};
15
16use crate::config;
17use crate::driver::{Callbacks, Driver};
18use crate::event::{self, Event, EventSender};
19use crate::ifac;
20#[cfg(feature = "iface-local")]
21use crate::interface::local::LocalServerConfig;
22use crate::interface::{InterfaceEntry, InterfaceStats};
23use crate::storage;
24use crate::time;
25
26/// Parse an interface mode string to the corresponding constant.
27/// Matches Python's `_synthesize_interface()` in `RNS/Reticulum.py`.
28fn parse_interface_mode(mode: &str) -> u8 {
29    match mode.to_lowercase().as_str() {
30        "full" => rns_core::constants::MODE_FULL,
31        "access_point" | "accesspoint" | "ap" => rns_core::constants::MODE_ACCESS_POINT,
32        "pointtopoint" | "ptp" => rns_core::constants::MODE_POINT_TO_POINT,
33        "roaming" => rns_core::constants::MODE_ROAMING,
34        "boundary" => rns_core::constants::MODE_BOUNDARY,
35        "gateway" | "gw" => rns_core::constants::MODE_GATEWAY,
36        _ => rns_core::constants::MODE_FULL,
37    }
38}
39
40/// Extract IFAC configuration from interface params, if present.
41/// Returns None if neither networkname/network_name nor passphrase/pass_phrase is set.
42fn extract_ifac_config(
43    params: &std::collections::HashMap<String, String>,
44    default_size: usize,
45) -> Option<IfacConfig> {
46    let netname = params
47        .get("networkname")
48        .or_else(|| params.get("network_name"))
49        .cloned();
50    let netkey = params
51        .get("passphrase")
52        .or_else(|| params.get("pass_phrase"))
53        .cloned();
54
55    if netname.is_none() && netkey.is_none() {
56        return None;
57    }
58
59    // ifac_size is specified in bits in config, divide by 8 for bytes
60    let size = params
61        .get("ifac_size")
62        .and_then(|v| v.parse::<usize>().ok())
63        .map(|bits| (bits / 8).max(1))
64        .unwrap_or(default_size);
65
66    Some(IfacConfig {
67        netname,
68        netkey,
69        size,
70    })
71}
72
73/// Extract discovery configuration from interface params, if `discoverable` is set.
74fn extract_discovery_config(
75    iface_name: &str,
76    iface_type: &str,
77    params: &std::collections::HashMap<String, String>,
78) -> Option<crate::discovery::DiscoveryConfig> {
79    let discoverable = params
80        .get("discoverable")
81        .and_then(|v| config::parse_bool_pub(v))
82        .unwrap_or(false);
83    if !discoverable {
84        return None;
85    }
86
87    let discovery_name = params
88        .get("discovery_name")
89        .cloned()
90        .unwrap_or_else(|| iface_name.to_string());
91
92    // Config value is in seconds. Min 300s (5min), default 21600s (6h).
93    let announce_interval = params
94        .get("announce_interval")
95        .and_then(|v| v.parse::<u64>().ok())
96        .map(|secs| secs.max(300))
97        .unwrap_or(21600);
98
99    let stamp_value = params
100        .get("discovery_stamp_value")
101        .and_then(|v| v.parse::<u8>().ok())
102        .unwrap_or(crate::discovery::DEFAULT_STAMP_VALUE);
103
104    let reachable_on = params.get("reachable_on").cloned();
105
106    let listen_port = params
107        .get("listen_port")
108        .or_else(|| params.get("port"))
109        .and_then(|v| v.parse().ok());
110
111    let latitude = params
112        .get("latitude")
113        .or_else(|| params.get("lat"))
114        .and_then(|v| v.parse().ok());
115    let longitude = params
116        .get("longitude")
117        .or_else(|| params.get("lon"))
118        .and_then(|v| v.parse().ok());
119    let height = params.get("height").and_then(|v| v.parse().ok());
120
121    Some(crate::discovery::DiscoveryConfig {
122        discovery_name,
123        announce_interval,
124        stamp_value,
125        reachable_on,
126        interface_type: iface_type.to_string(),
127        listen_port,
128        latitude,
129        longitude,
130        height,
131    })
132}
133
134/// Top-level node configuration.
135pub struct NodeConfig {
136    pub transport_enabled: bool,
137    pub identity: Option<Identity>,
138    /// Interface configurations (parsed via registry factories).
139    pub interfaces: Vec<InterfaceConfig>,
140    /// Enable shared instance server for local clients (rns-ctl, etc.)
141    pub share_instance: bool,
142    /// Instance name for Unix socket namespace (default: "default").
143    pub instance_name: String,
144    /// Shared instance port for local client connections (default 37428).
145    pub shared_instance_port: u16,
146    /// RPC control port (default 37429). Only used when share_instance is true.
147    pub rpc_port: u16,
148    /// Cache directory for announce cache. If None, announce caching is disabled.
149    pub cache_dir: Option<std::path::PathBuf>,
150    /// Remote management configuration.
151    pub management: crate::management::ManagementConfig,
152    /// Port to run the STUN probe server on (for facilitator nodes).
153    pub probe_port: Option<u16>,
154    /// Addresses of STUN/RNSP probe servers (tried sequentially with failover).
155    pub probe_addrs: Vec<std::net::SocketAddr>,
156    /// Protocol for endpoint discovery: "rnsp" (default) or "stun".
157    pub probe_protocol: rns_core::holepunch::ProbeProtocol,
158    /// Network interface to bind outbound sockets to (e.g. "usb0").
159    pub device: Option<String>,
160    /// Hook configurations loaded from the config file.
161    pub hooks: Vec<config::ParsedHook>,
162    /// Enable interface discovery.
163    pub discover_interfaces: bool,
164    /// Minimum stamp value for accepting discovered interfaces (default: 14).
165    pub discovery_required_value: Option<u8>,
166    /// Respond to probe packets with automatic proof (like Python's respond_to_probes).
167    pub respond_to_probes: bool,
168    /// Accept an announce with strictly fewer hops even when the random_blob
169    /// is a duplicate of the existing path entry.  Default `false` preserves
170    /// Python-compatible anti-replay behaviour.
171    pub prefer_shorter_path: bool,
172    /// Maximum number of alternative paths stored per destination.
173    /// Default 1 (single path, backward-compatible).
174    pub max_paths_per_destination: usize,
175    /// Custom interface registry. If `None`, uses `InterfaceRegistry::with_builtins()`.
176    pub registry: Option<crate::interface::registry::InterfaceRegistry>,
177    /// If true, a single interface failing to start will abort the entire node.
178    /// If false (default), the error is logged and remaining interfaces continue.
179    pub panic_on_interface_error: bool,
180}
181
182/// IFAC configuration for an interface.
183pub struct IfacConfig {
184    pub netname: Option<String>,
185    pub netkey: Option<String>,
186    pub size: usize,
187}
188
189/// Interface configuration, parsed via an [`InterfaceFactory`] from the registry.
190pub struct InterfaceConfig {
191    pub name: String,
192    pub type_name: String,
193    pub config_data: Box<dyn crate::interface::InterfaceConfigData>,
194    pub mode: u8,
195    pub ifac: Option<IfacConfig>,
196    pub discovery: Option<crate::discovery::DiscoveryConfig>,
197}
198
199use crate::event::{QueryRequest, QueryResponse};
200
201/// Error returned when the driver thread has shut down.
202#[derive(Debug)]
203pub struct SendError;
204
205impl std::fmt::Display for SendError {
206    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207        write!(f, "driver shut down")
208    }
209}
210
211impl std::error::Error for SendError {}
212
213/// A running RNS node.
214pub struct RnsNode {
215    tx: EventSender,
216    driver_handle: Option<JoinHandle<()>>,
217    rpc_server: Option<crate::rpc::RpcServer>,
218    tick_interval_ms: Arc<AtomicU64>,
219    #[allow(dead_code)]
220    probe_server: Option<crate::holepunch::probe::ProbeServerHandle>,
221}
222
223impl RnsNode {
224    /// Start the node from a config file path.
225    /// If `config_path` is None, uses `~/.reticulum/`.
226    pub fn from_config(
227        config_path: Option<&Path>,
228        callbacks: Box<dyn Callbacks>,
229    ) -> io::Result<Self> {
230        let config_dir = storage::resolve_config_dir(config_path);
231        let paths = storage::ensure_storage_dirs(&config_dir)?;
232
233        // Parse config file
234        let config_file = config_dir.join("config");
235        let rns_config = if config_file.exists() {
236            config::parse_file(&config_file)
237                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("{}", e)))?
238        } else {
239            // No config file, use defaults
240            config::parse("")
241                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("{}", e)))?
242        };
243
244        // Load or create identity
245        let identity = if let Some(ref id_path_str) = rns_config.reticulum.network_identity {
246            let id_path = std::path::PathBuf::from(id_path_str);
247            if id_path.exists() {
248                storage::load_identity(&id_path)?
249            } else {
250                let id = Identity::new(&mut OsRng);
251                storage::save_identity(&id, &id_path)?;
252                id
253            }
254        } else {
255            storage::load_or_create_identity(&paths.identities)?
256        };
257
258        // Build interface configs from parsed config using registry
259        let registry = crate::interface::registry::InterfaceRegistry::with_builtins();
260        let mut interface_configs = Vec::new();
261        let mut next_id_val = 1u64;
262
263        for iface in &rns_config.interfaces {
264            if !iface.enabled {
265                continue;
266            }
267
268            let iface_id = rns_core::transport::types::InterfaceId(next_id_val);
269            next_id_val += 1;
270
271            let factory = match registry.get(&iface.interface_type) {
272                Some(f) => f,
273                None => {
274                    log::warn!(
275                        "Unsupported interface type '{}' for '{}'",
276                        iface.interface_type,
277                        iface.name,
278                    );
279                    continue;
280                }
281            };
282
283            let mut iface_mode = parse_interface_mode(&iface.mode);
284
285            // Auto-configure mode when discovery is enabled (Python Reticulum.py).
286            let has_discovery = match iface.interface_type.as_str() {
287                "AutoInterface" => true,
288                "RNodeInterface" => iface
289                    .params
290                    .get("discoverable")
291                    .and_then(|v| config::parse_bool_pub(v))
292                    .unwrap_or(false),
293                _ => false,
294            };
295            if has_discovery
296                && iface_mode != rns_core::constants::MODE_ACCESS_POINT
297                && iface_mode != rns_core::constants::MODE_GATEWAY
298            {
299                let new_mode = if iface.interface_type == "RNodeInterface" {
300                    rns_core::constants::MODE_ACCESS_POINT
301                } else {
302                    rns_core::constants::MODE_GATEWAY
303                };
304                log::info!(
305                    "Interface '{}' has discovery enabled, auto-configuring mode to {}",
306                    iface.name,
307                    if new_mode == rns_core::constants::MODE_ACCESS_POINT {
308                        "ACCESS_POINT"
309                    } else {
310                        "GATEWAY"
311                    }
312                );
313                iface_mode = new_mode;
314            }
315
316            let default_ifac_size = factory.default_ifac_size();
317            let ifac_config = extract_ifac_config(&iface.params, default_ifac_size);
318            let discovery_config =
319                extract_discovery_config(&iface.name, &iface.interface_type, &iface.params);
320
321            // Inject storage_dir for I2P (and any future factories that need it)
322            let mut params = iface.params.clone();
323            if !params.contains_key("storage_dir") {
324                params.insert(
325                    "storage_dir".to_string(),
326                    paths.storage.to_string_lossy().to_string(),
327                );
328            }
329            // Inject device for TCP client
330            if let Some(ref device) = rns_config.reticulum.device {
331                if !params.contains_key("device") {
332                    params.insert("device".to_string(), device.clone());
333                }
334            }
335
336            let config_data = match factory.parse_config(&iface.name, iface_id, &params) {
337                Ok(data) => data,
338                Err(e) => {
339                    log::warn!("Failed to parse config for '{}': {}", iface.name, e);
340                    continue;
341                }
342            };
343
344            interface_configs.push(InterfaceConfig {
345                name: iface.name.clone(),
346                type_name: iface.interface_type.clone(),
347                config_data,
348                mode: iface_mode,
349                ifac: ifac_config,
350                discovery: discovery_config,
351            });
352        }
353
354        // Parse management config
355        let mut mgmt_allowed = Vec::new();
356        for hex_hash in &rns_config.reticulum.remote_management_allowed {
357            if hex_hash.len() == 32 {
358                if let Ok(bytes) = (0..hex_hash.len())
359                    .step_by(2)
360                    .map(|i| u8::from_str_radix(&hex_hash[i..i + 2], 16))
361                    .collect::<Result<Vec<u8>, _>>()
362                {
363                    if bytes.len() == 16 {
364                        let mut h = [0u8; 16];
365                        h.copy_from_slice(&bytes);
366                        mgmt_allowed.push(h);
367                    }
368                } else {
369                    log::warn!("Invalid hex in remote_management_allowed: {}", hex_hash);
370                }
371            } else {
372                log::warn!(
373                    "Invalid entry in remote_management_allowed (expected 32 hex chars, got {}): {}",
374                    hex_hash.len(), hex_hash,
375                );
376            }
377        }
378
379        // Parse probe_addr (comma-separated list of SocketAddr)
380        let probe_addrs: Vec<std::net::SocketAddr> = rns_config
381            .reticulum
382            .probe_addr
383            .as_ref()
384            .map(|s| {
385                s.split(',')
386                    .filter_map(|entry| {
387                        let trimmed = entry.trim();
388                        if trimmed.is_empty() {
389                            return None;
390                        }
391                        trimmed
392                            .parse::<std::net::SocketAddr>()
393                            .map_err(|e| {
394                                log::warn!("Invalid probe_addr entry '{}': {}", trimmed, e);
395                                e
396                            })
397                            .ok()
398                    })
399                    .collect()
400            })
401            .unwrap_or_default();
402
403        // Parse probe_protocol (default: rnsp)
404        let probe_protocol = match rns_config
405            .reticulum
406            .probe_protocol
407            .as_deref()
408            .map(|s| s.to_lowercase())
409        {
410            Some(ref s) if s == "stun" => rns_core::holepunch::ProbeProtocol::Stun,
411            _ => rns_core::holepunch::ProbeProtocol::Rnsp,
412        };
413
414        let node_config = NodeConfig {
415            transport_enabled: rns_config.reticulum.enable_transport,
416            identity: Some(identity),
417            share_instance: rns_config.reticulum.share_instance,
418            instance_name: rns_config.reticulum.instance_name.clone(),
419            shared_instance_port: rns_config.reticulum.shared_instance_port,
420            rpc_port: rns_config.reticulum.instance_control_port,
421            cache_dir: Some(paths.cache),
422            management: crate::management::ManagementConfig {
423                enable_remote_management: rns_config.reticulum.enable_remote_management,
424                remote_management_allowed: mgmt_allowed,
425                publish_blackhole: rns_config.reticulum.publish_blackhole,
426            },
427            probe_port: rns_config.reticulum.probe_port,
428            probe_addrs,
429            probe_protocol,
430            device: rns_config.reticulum.device.clone(),
431            hooks: rns_config.hooks.clone(),
432            discover_interfaces: rns_config.reticulum.discover_interfaces,
433            discovery_required_value: rns_config.reticulum.required_discovery_value,
434            respond_to_probes: rns_config.reticulum.respond_to_probes,
435            prefer_shorter_path: rns_config.reticulum.prefer_shorter_path,
436            max_paths_per_destination: rns_config.reticulum.max_paths_per_destination,
437            interfaces: interface_configs,
438            registry: None,
439            panic_on_interface_error: rns_config.reticulum.panic_on_interface_error,
440        };
441
442        Self::start(node_config, callbacks)
443    }
444
445    /// Start the node. Connects all interfaces, starts driver and timer threads.
446    pub fn start(config: NodeConfig, callbacks: Box<dyn Callbacks>) -> io::Result<Self> {
447        let identity = config.identity.unwrap_or_else(|| Identity::new(&mut OsRng));
448
449        let transport_config = TransportConfig {
450            transport_enabled: config.transport_enabled,
451            identity_hash: Some(*identity.hash()),
452            prefer_shorter_path: config.prefer_shorter_path,
453            max_paths_per_destination: config.max_paths_per_destination,
454        };
455
456        let (tx, rx) = event::channel();
457        let mut driver = Driver::new(transport_config, rx, tx.clone(), callbacks);
458
459        // Set up announce cache if cache directory is configured
460        if let Some(ref cache_dir) = config.cache_dir {
461            let announces_dir = cache_dir.join("announces");
462            let _ = std::fs::create_dir_all(&announces_dir);
463            driver.announce_cache = Some(crate::announce_cache::AnnounceCache::new(announces_dir));
464        }
465
466        // Configure probe addresses and device for hole punching
467        if !config.probe_addrs.is_empty() || config.device.is_some() {
468            driver.set_probe_config(
469                config.probe_addrs.clone(),
470                config.probe_protocol,
471                config.device.clone(),
472            );
473        }
474
475        // Start probe server if configured
476        let probe_server = if let Some(port) = config.probe_port {
477            let listen_addr: std::net::SocketAddr = ([0, 0, 0, 0], port).into();
478            match crate::holepunch::probe::start_probe_server(listen_addr) {
479                Ok(handle) => {
480                    log::info!("Probe server started on 0.0.0.0:{}", port);
481                    Some(handle)
482                }
483                Err(e) => {
484                    log::error!("Failed to start probe server on port {}: {}", port, e);
485                    None
486                }
487            }
488        } else {
489            None
490        };
491
492        // Store management config on driver for ACL enforcement
493        driver.management_config = config.management.clone();
494
495        // Store transport identity for tunnel synthesis
496        if let Some(prv_key) = identity.get_private_key() {
497            driver.transport_identity = Some(Identity::from_private_key(&prv_key));
498        }
499
500        // Load hooks from config
501        #[cfg(feature = "rns-hooks")]
502        {
503            for hook_cfg in &config.hooks {
504                if !hook_cfg.enabled {
505                    continue;
506                }
507                let point_idx = match config::parse_hook_point(&hook_cfg.attach_point) {
508                    Some(idx) => idx,
509                    None => {
510                        log::warn!(
511                            "Unknown hook point '{}' for hook '{}'",
512                            hook_cfg.attach_point,
513                            hook_cfg.name,
514                        );
515                        continue;
516                    }
517                };
518                let mgr = match driver.hook_manager.as_ref() {
519                    Some(m) => m,
520                    None => {
521                        log::warn!(
522                            "Hook manager not available, skipping hook '{}'",
523                            hook_cfg.name
524                        );
525                        continue;
526                    }
527                };
528                match mgr.load_file(
529                    hook_cfg.name.clone(),
530                    std::path::Path::new(&hook_cfg.path),
531                    hook_cfg.priority,
532                ) {
533                    Ok(program) => {
534                        driver.hook_slots[point_idx].attach(program);
535                        log::info!(
536                            "Loaded hook '{}' at point {} (priority {})",
537                            hook_cfg.name,
538                            hook_cfg.attach_point,
539                            hook_cfg.priority,
540                        );
541                    }
542                    Err(e) => {
543                        log::error!(
544                            "Failed to load hook '{}' from '{}': {}",
545                            hook_cfg.name,
546                            hook_cfg.path,
547                            e,
548                        );
549                    }
550                }
551            }
552        }
553
554        // Configure discovery
555        driver.discover_interfaces = config.discover_interfaces;
556        if let Some(val) = config.discovery_required_value {
557            driver.discovery_required_value = val;
558        }
559
560        // Shared counter for dynamic interface IDs
561        let next_dynamic_id = Arc::new(AtomicU64::new(10000));
562
563        // Collect discoverable interface configs for the announcer
564        let mut discoverable_interfaces = Vec::new();
565
566        // --- Registry-based startup for interfaces ---
567        let registry = config
568            .registry
569            .unwrap_or_else(crate::interface::registry::InterfaceRegistry::with_builtins);
570        for iface_config in config.interfaces {
571            let factory = match registry.get(&iface_config.type_name) {
572                Some(f) => f,
573                None => {
574                    log::warn!(
575                        "No factory registered for interface type '{}'",
576                        iface_config.type_name
577                    );
578                    continue;
579                }
580            };
581
582            let mut ifac_state = iface_config.ifac.as_ref().and_then(|ic| {
583                if ic.netname.is_some() || ic.netkey.is_some() {
584                    Some(ifac::derive_ifac(
585                        ic.netname.as_deref(),
586                        ic.netkey.as_deref(),
587                        ic.size,
588                    ))
589                } else {
590                    None
591                }
592            });
593
594            let ctx = crate::interface::StartContext {
595                tx: tx.clone(),
596                next_dynamic_id: next_dynamic_id.clone(),
597                mode: iface_config.mode,
598            };
599
600            let result = match factory.start(iface_config.config_data, ctx) {
601                Ok(r) => r,
602                Err(e) => {
603                    if config.panic_on_interface_error {
604                        return Err(e);
605                    }
606                    log::error!(
607                        "Interface '{}' ({}) failed to start: {}",
608                        iface_config.name,
609                        iface_config.type_name,
610                        e
611                    );
612                    continue;
613                }
614            };
615
616            if let Some(ref disc) = iface_config.discovery {
617                discoverable_interfaces.push(crate::discovery::DiscoverableInterface {
618                    config: disc.clone(),
619                    transport_enabled: config.transport_enabled,
620                    ifac_netname: iface_config.ifac.as_ref().and_then(|ic| ic.netname.clone()),
621                    ifac_netkey: iface_config.ifac.as_ref().and_then(|ic| ic.netkey.clone()),
622                });
623            }
624
625            match result {
626                crate::interface::StartResult::Simple {
627                    id,
628                    info,
629                    writer,
630                    interface_type_name,
631                } => {
632                    driver.engine.register_interface(info.clone());
633                    driver.interfaces.insert(
634                        id,
635                        InterfaceEntry {
636                            id,
637                            info,
638                            writer,
639                            online: false,
640                            dynamic: false,
641                            ifac: ifac_state,
642                            stats: InterfaceStats {
643                                started: time::now(),
644                                ..Default::default()
645                            },
646                            interface_type: interface_type_name,
647                        },
648                    );
649                }
650                crate::interface::StartResult::Listener => {
651                    // Listener-type interface (TcpServer, Auto, I2P, etc.)
652                    // registers dynamic interfaces via InterfaceUp events.
653                }
654                crate::interface::StartResult::Multi(subs) => {
655                    let ifac_cfg = &iface_config.ifac;
656                    let mut first = true;
657                    for sub in subs {
658                        let sub_ifac = if first {
659                            first = false;
660                            ifac_state.take()
661                        } else if let Some(ref ic) = ifac_cfg {
662                            Some(ifac::derive_ifac(
663                                ic.netname.as_deref(),
664                                ic.netkey.as_deref(),
665                                ic.size,
666                            ))
667                        } else {
668                            None
669                        };
670
671                        driver.engine.register_interface(sub.info.clone());
672                        driver.interfaces.insert(
673                            sub.id,
674                            InterfaceEntry {
675                                id: sub.id,
676                                info: sub.info,
677                                writer: sub.writer,
678                                online: false,
679                                dynamic: false,
680                                ifac: sub_ifac,
681                                stats: InterfaceStats {
682                                    started: time::now(),
683                                    ..Default::default()
684                                },
685                                interface_type: sub.interface_type_name,
686                            },
687                        );
688                    }
689                }
690            }
691        }
692
693        // Set up interface announcer if we have discoverable interfaces
694        if !discoverable_interfaces.is_empty() {
695            let transport_id = *identity.hash();
696            let announcer =
697                crate::discovery::InterfaceAnnouncer::new(transport_id, discoverable_interfaces);
698            log::info!("Interface discovery announcer initialized");
699            driver.interface_announcer = Some(announcer);
700        }
701
702        // Set up discovered interfaces storage path
703        if let Some(ref cache_dir) = config.cache_dir {
704            let disc_path = std::path::PathBuf::from(cache_dir)
705                .parent()
706                .unwrap_or(std::path::Path::new("."))
707                .join("storage")
708                .join("discovery")
709                .join("interfaces");
710            let _ = std::fs::create_dir_all(&disc_path);
711            driver.discovered_interfaces =
712                crate::discovery::DiscoveredInterfaceStorage::new(disc_path);
713        }
714
715        // Set up management destinations if enabled
716        if config.management.enable_remote_management {
717            if let Some(prv_key) = identity.get_private_key() {
718                let identity_hash = *identity.hash();
719                let mgmt_dest = crate::management::management_dest_hash(&identity_hash);
720
721                // Extract Ed25519 signing keys from the identity
722                let sig_prv = rns_crypto::ed25519::Ed25519PrivateKey::from_bytes(
723                    &prv_key[32..64].try_into().unwrap(),
724                );
725                let sig_pub_bytes: [u8; 32] = identity.get_public_key().unwrap()[32..64]
726                    .try_into()
727                    .unwrap();
728
729                // Register as SINGLE destination in transport engine
730                driver
731                    .engine
732                    .register_destination(mgmt_dest, rns_core::constants::DESTINATION_SINGLE);
733                driver
734                    .local_destinations
735                    .insert(mgmt_dest, rns_core::constants::DESTINATION_SINGLE);
736
737                // Register as link destination in link manager
738                driver.link_manager.register_link_destination(
739                    mgmt_dest,
740                    sig_prv,
741                    sig_pub_bytes,
742                    crate::link_manager::ResourceStrategy::AcceptNone,
743                );
744
745                // Register management path hashes
746                driver
747                    .link_manager
748                    .register_management_path(crate::management::status_path_hash());
749                driver
750                    .link_manager
751                    .register_management_path(crate::management::path_path_hash());
752
753                log::info!("Remote management enabled on {:02x?}", &mgmt_dest[..4],);
754
755                // Set up allowed list
756                if !config.management.remote_management_allowed.is_empty() {
757                    log::info!(
758                        "Remote management allowed for {} identities",
759                        config.management.remote_management_allowed.len(),
760                    );
761                }
762            }
763        }
764
765        if config.management.publish_blackhole {
766            if let Some(prv_key) = identity.get_private_key() {
767                let identity_hash = *identity.hash();
768                let bh_dest = crate::management::blackhole_dest_hash(&identity_hash);
769
770                let sig_prv = rns_crypto::ed25519::Ed25519PrivateKey::from_bytes(
771                    &prv_key[32..64].try_into().unwrap(),
772                );
773                let sig_pub_bytes: [u8; 32] = identity.get_public_key().unwrap()[32..64]
774                    .try_into()
775                    .unwrap();
776
777                driver
778                    .engine
779                    .register_destination(bh_dest, rns_core::constants::DESTINATION_SINGLE);
780                driver.link_manager.register_link_destination(
781                    bh_dest,
782                    sig_prv,
783                    sig_pub_bytes,
784                    crate::link_manager::ResourceStrategy::AcceptNone,
785                );
786                driver
787                    .link_manager
788                    .register_management_path(crate::management::list_path_hash());
789
790                log::info!(
791                    "Blackhole list publishing enabled on {:02x?}",
792                    &bh_dest[..4],
793                );
794            }
795        }
796
797        // Set up probe responder if enabled
798        if config.respond_to_probes && config.transport_enabled {
799            let identity_hash = *identity.hash();
800            let probe_dest = crate::management::probe_dest_hash(&identity_hash);
801
802            // Register as SINGLE destination in transport engine
803            driver
804                .engine
805                .register_destination(probe_dest, rns_core::constants::DESTINATION_SINGLE);
806            driver
807                .local_destinations
808                .insert(probe_dest, rns_core::constants::DESTINATION_SINGLE);
809
810            // Register PROVE_ALL proof strategy with transport identity
811            let probe_identity = rns_crypto::identity::Identity::from_private_key(
812                &identity.get_private_key().unwrap(),
813            );
814            driver.proof_strategies.insert(
815                probe_dest,
816                (
817                    rns_core::types::ProofStrategy::ProveAll,
818                    Some(probe_identity),
819                ),
820            );
821
822            driver.probe_responder_hash = Some(probe_dest);
823
824            log::info!("Probe responder enabled on {:02x?}", &probe_dest[..4],);
825        }
826
827        // Spawn timer thread with configurable tick interval
828        let tick_interval_ms = Arc::new(AtomicU64::new(1000));
829        let timer_tx = tx.clone();
830        let timer_interval = Arc::clone(&tick_interval_ms);
831        thread::Builder::new()
832            .name("rns-timer".into())
833            .spawn(move || {
834                loop {
835                    let ms = timer_interval.load(Ordering::Relaxed);
836                    thread::sleep(Duration::from_millis(ms));
837                    if timer_tx.send(Event::Tick).is_err() {
838                        break; // receiver dropped
839                    }
840                }
841            })?;
842
843        // Start LocalServer for shared instance clients if share_instance is enabled
844        #[cfg(feature = "iface-local")]
845        if config.share_instance {
846            let local_server_config = LocalServerConfig {
847                instance_name: config.instance_name.clone(),
848                port: config.shared_instance_port,
849                interface_id: rns_core::transport::types::InterfaceId(0), // Not used for server
850            };
851            match crate::interface::local::start_server(
852                local_server_config,
853                tx.clone(),
854                next_dynamic_id.clone(),
855            ) {
856                Ok(()) => {
857                    log::info!(
858                        "Local shared instance server started (instance={}, port={})",
859                        config.instance_name,
860                        config.shared_instance_port
861                    );
862                }
863                Err(e) => {
864                    log::error!("Failed to start local shared instance server: {}", e);
865                }
866            }
867        }
868
869        // Start RPC server if share_instance is enabled
870        let rpc_server = if config.share_instance {
871            let auth_key =
872                crate::rpc::derive_auth_key(&identity.get_private_key().unwrap_or([0u8; 64]));
873            let rpc_addr = crate::rpc::RpcAddr::Tcp("127.0.0.1".into(), config.rpc_port);
874            match crate::rpc::RpcServer::start(&rpc_addr, auth_key, tx.clone()) {
875                Ok(server) => {
876                    log::info!("RPC server started on 127.0.0.1:{}", config.rpc_port);
877                    Some(server)
878                }
879                Err(e) => {
880                    log::error!("Failed to start RPC server: {}", e);
881                    None
882                }
883            }
884        } else {
885            None
886        };
887
888        // Spawn driver thread
889        let driver_handle = thread::Builder::new()
890            .name("rns-driver".into())
891            .spawn(move || {
892                driver.run();
893            })?;
894
895        Ok(RnsNode {
896            tx,
897            driver_handle: Some(driver_handle),
898            rpc_server,
899            tick_interval_ms,
900            probe_server,
901        })
902    }
903
904    /// Query the driver for state information.
905    pub fn query(&self, request: QueryRequest) -> Result<QueryResponse, SendError> {
906        let (resp_tx, resp_rx) = std::sync::mpsc::channel();
907        self.tx
908            .send(Event::Query(request, resp_tx))
909            .map_err(|_| SendError)?;
910        resp_rx.recv().map_err(|_| SendError)
911    }
912
913    /// Send a raw outbound packet.
914    pub fn send_raw(
915        &self,
916        raw: Vec<u8>,
917        dest_type: u8,
918        attached_interface: Option<rns_core::transport::types::InterfaceId>,
919    ) -> Result<(), SendError> {
920        self.tx
921            .send(Event::SendOutbound {
922                raw,
923                dest_type,
924                attached_interface,
925            })
926            .map_err(|_| SendError)
927    }
928
929    /// Register a local destination with the transport engine.
930    pub fn register_destination(
931        &self,
932        dest_hash: [u8; 16],
933        dest_type: u8,
934    ) -> Result<(), SendError> {
935        self.tx
936            .send(Event::RegisterDestination {
937                dest_hash,
938                dest_type,
939            })
940            .map_err(|_| SendError)
941    }
942
943    /// Deregister a local destination.
944    pub fn deregister_destination(&self, dest_hash: [u8; 16]) -> Result<(), SendError> {
945        self.tx
946            .send(Event::DeregisterDestination { dest_hash })
947            .map_err(|_| SendError)
948    }
949
950    /// Deregister a link destination (stop accepting incoming links).
951    pub fn deregister_link_destination(&self, dest_hash: [u8; 16]) -> Result<(), SendError> {
952        self.tx
953            .send(Event::DeregisterLinkDestination { dest_hash })
954            .map_err(|_| SendError)
955    }
956
957    /// Register a link destination that can accept incoming links.
958    ///
959    /// `dest_hash`: the destination hash
960    /// `sig_prv_bytes`: Ed25519 private signing key (32 bytes)
961    /// `sig_pub_bytes`: Ed25519 public signing key (32 bytes)
962    pub fn register_link_destination(
963        &self,
964        dest_hash: [u8; 16],
965        sig_prv_bytes: [u8; 32],
966        sig_pub_bytes: [u8; 32],
967        resource_strategy: u8,
968    ) -> Result<(), SendError> {
969        self.tx
970            .send(Event::RegisterLinkDestination {
971                dest_hash,
972                sig_prv_bytes,
973                sig_pub_bytes,
974                resource_strategy,
975            })
976            .map_err(|_| SendError)
977    }
978
979    /// Register a request handler for a given path on established links.
980    pub fn register_request_handler<F>(
981        &self,
982        path: &str,
983        allowed_list: Option<Vec<[u8; 16]>>,
984        handler: F,
985    ) -> Result<(), SendError>
986    where
987        F: Fn([u8; 16], &str, &[u8], Option<&([u8; 16], [u8; 64])>) -> Option<Vec<u8>>
988            + Send
989            + 'static,
990    {
991        self.tx
992            .send(Event::RegisterRequestHandler {
993                path: path.to_string(),
994                allowed_list,
995                handler: Box::new(handler),
996            })
997            .map_err(|_| SendError)
998    }
999
1000    /// Create an outbound link to a destination.
1001    ///
1002    /// Returns the link_id on success.
1003    pub fn create_link(
1004        &self,
1005        dest_hash: [u8; 16],
1006        dest_sig_pub_bytes: [u8; 32],
1007    ) -> Result<[u8; 16], SendError> {
1008        let (response_tx, response_rx) = std::sync::mpsc::channel();
1009        self.tx
1010            .send(Event::CreateLink {
1011                dest_hash,
1012                dest_sig_pub_bytes,
1013                response_tx,
1014            })
1015            .map_err(|_| SendError)?;
1016        response_rx.recv().map_err(|_| SendError)
1017    }
1018
1019    /// Send a request on an established link.
1020    pub fn send_request(
1021        &self,
1022        link_id: [u8; 16],
1023        path: &str,
1024        data: &[u8],
1025    ) -> Result<(), SendError> {
1026        self.tx
1027            .send(Event::SendRequest {
1028                link_id,
1029                path: path.to_string(),
1030                data: data.to_vec(),
1031            })
1032            .map_err(|_| SendError)
1033    }
1034
1035    /// Identify on a link (reveal identity to remote peer).
1036    pub fn identify_on_link(
1037        &self,
1038        link_id: [u8; 16],
1039        identity_prv_key: [u8; 64],
1040    ) -> Result<(), SendError> {
1041        self.tx
1042            .send(Event::IdentifyOnLink {
1043                link_id,
1044                identity_prv_key,
1045            })
1046            .map_err(|_| SendError)
1047    }
1048
1049    /// Tear down a link.
1050    pub fn teardown_link(&self, link_id: [u8; 16]) -> Result<(), SendError> {
1051        self.tx
1052            .send(Event::TeardownLink { link_id })
1053            .map_err(|_| SendError)
1054    }
1055
1056    /// Send a resource on an established link.
1057    pub fn send_resource(
1058        &self,
1059        link_id: [u8; 16],
1060        data: Vec<u8>,
1061        metadata: Option<Vec<u8>>,
1062    ) -> Result<(), SendError> {
1063        self.tx
1064            .send(Event::SendResource {
1065                link_id,
1066                data,
1067                metadata,
1068            })
1069            .map_err(|_| SendError)
1070    }
1071
1072    /// Set the resource acceptance strategy for a link.
1073    ///
1074    /// 0 = AcceptNone, 1 = AcceptAll, 2 = AcceptApp
1075    pub fn set_resource_strategy(&self, link_id: [u8; 16], strategy: u8) -> Result<(), SendError> {
1076        self.tx
1077            .send(Event::SetResourceStrategy { link_id, strategy })
1078            .map_err(|_| SendError)
1079    }
1080
1081    /// Accept or reject a pending resource (for AcceptApp strategy).
1082    pub fn accept_resource(
1083        &self,
1084        link_id: [u8; 16],
1085        resource_hash: Vec<u8>,
1086        accept: bool,
1087    ) -> Result<(), SendError> {
1088        self.tx
1089            .send(Event::AcceptResource {
1090                link_id,
1091                resource_hash,
1092                accept,
1093            })
1094            .map_err(|_| SendError)
1095    }
1096
1097    /// Send a channel message on a link.
1098    pub fn send_channel_message(
1099        &self,
1100        link_id: [u8; 16],
1101        msgtype: u16,
1102        payload: Vec<u8>,
1103    ) -> Result<(), SendError> {
1104        self.tx
1105            .send(Event::SendChannelMessage {
1106                link_id,
1107                msgtype,
1108                payload,
1109            })
1110            .map_err(|_| SendError)
1111    }
1112
1113    /// Propose a direct P2P connection to a peer via NAT hole punching.
1114    ///
1115    /// The link must be active and connected through a backbone node.
1116    /// If successful, a direct UDP connection will be established, bypassing the backbone.
1117    pub fn propose_direct_connect(&self, link_id: [u8; 16]) -> Result<(), SendError> {
1118        self.tx
1119            .send(Event::ProposeDirectConnect { link_id })
1120            .map_err(|_| SendError)
1121    }
1122
1123    /// Set the policy for handling incoming direct-connect proposals.
1124    pub fn set_direct_connect_policy(
1125        &self,
1126        policy: crate::holepunch::orchestrator::HolePunchPolicy,
1127    ) -> Result<(), SendError> {
1128        self.tx
1129            .send(Event::SetDirectConnectPolicy { policy })
1130            .map_err(|_| SendError)
1131    }
1132
1133    /// Send data on a link with a given context.
1134    pub fn send_on_link(
1135        &self,
1136        link_id: [u8; 16],
1137        data: Vec<u8>,
1138        context: u8,
1139    ) -> Result<(), SendError> {
1140        self.tx
1141            .send(Event::SendOnLink {
1142                link_id,
1143                data,
1144                context,
1145            })
1146            .map_err(|_| SendError)
1147    }
1148
1149    /// Build and broadcast an announce for a destination.
1150    ///
1151    /// The identity is used to sign the announce. Must be the identity that
1152    /// owns the destination (i.e. `identity.hash()` matches `dest.identity_hash`).
1153    pub fn announce(
1154        &self,
1155        dest: &crate::destination::Destination,
1156        identity: &Identity,
1157        app_data: Option<&[u8]>,
1158    ) -> Result<(), SendError> {
1159        let name_hash = rns_core::destination::name_hash(
1160            &dest.app_name,
1161            &dest.aspects.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
1162        );
1163
1164        let mut random_hash = [0u8; 10];
1165        OsRng.fill_bytes(&mut random_hash[..5]);
1166        // Bytes [5:10] must be the emission timestamp (seconds since epoch,
1167        // big-endian, truncated to 5 bytes) so that path table dedup can
1168        // compare announce freshness.  Matches Python: int(time.time()).to_bytes(5, "big")
1169        let now_secs = std::time::SystemTime::now()
1170            .duration_since(std::time::UNIX_EPOCH)
1171            .unwrap_or_default()
1172            .as_secs();
1173        random_hash[5..10].copy_from_slice(&now_secs.to_be_bytes()[3..8]);
1174
1175        let (announce_data, _has_ratchet) = rns_core::announce::AnnounceData::pack(
1176            identity,
1177            &dest.hash.0,
1178            &name_hash,
1179            &random_hash,
1180            None, // no ratchet
1181            app_data,
1182        )
1183        .map_err(|_| SendError)?;
1184
1185        let context_flag = rns_core::constants::FLAG_UNSET;
1186
1187        let flags = rns_core::packet::PacketFlags {
1188            header_type: rns_core::constants::HEADER_1,
1189            context_flag,
1190            transport_type: rns_core::constants::TRANSPORT_BROADCAST,
1191            destination_type: rns_core::constants::DESTINATION_SINGLE,
1192            packet_type: rns_core::constants::PACKET_TYPE_ANNOUNCE,
1193        };
1194
1195        let packet = rns_core::packet::RawPacket::pack(
1196            flags,
1197            0,
1198            &dest.hash.0,
1199            None,
1200            rns_core::constants::CONTEXT_NONE,
1201            &announce_data,
1202        )
1203        .map_err(|_| SendError)?;
1204
1205        self.send_raw(packet.raw, dest.dest_type.to_wire_constant(), None)
1206    }
1207
1208    /// Send an encrypted (SINGLE) or plaintext (PLAIN) packet to a destination.
1209    ///
1210    /// For SINGLE destinations, `dest.public_key` must be set (OUT direction).
1211    /// Returns the packet hash for proof tracking.
1212    pub fn send_packet(
1213        &self,
1214        dest: &crate::destination::Destination,
1215        data: &[u8],
1216    ) -> Result<rns_core::types::PacketHash, SendError> {
1217        use rns_core::types::DestinationType;
1218
1219        let payload = match dest.dest_type {
1220            DestinationType::Single => {
1221                let pub_key = dest.public_key.ok_or(SendError)?;
1222                let remote_id = rns_crypto::identity::Identity::from_public_key(&pub_key);
1223                remote_id.encrypt(data, &mut OsRng).map_err(|_| SendError)?
1224            }
1225            DestinationType::Plain => data.to_vec(),
1226            DestinationType::Group => dest.encrypt(data).map_err(|_| SendError)?,
1227        };
1228
1229        let flags = rns_core::packet::PacketFlags {
1230            header_type: rns_core::constants::HEADER_1,
1231            context_flag: rns_core::constants::FLAG_UNSET,
1232            transport_type: rns_core::constants::TRANSPORT_BROADCAST,
1233            destination_type: dest.dest_type.to_wire_constant(),
1234            packet_type: rns_core::constants::PACKET_TYPE_DATA,
1235        };
1236
1237        let packet = rns_core::packet::RawPacket::pack(
1238            flags,
1239            0,
1240            &dest.hash.0,
1241            None,
1242            rns_core::constants::CONTEXT_NONE,
1243            &payload,
1244        )
1245        .map_err(|_| SendError)?;
1246
1247        let packet_hash = rns_core::types::PacketHash(packet.packet_hash);
1248
1249        self.tx
1250            .send(Event::SendOutbound {
1251                raw: packet.raw,
1252                dest_type: dest.dest_type.to_wire_constant(),
1253                attached_interface: None,
1254            })
1255            .map_err(|_| SendError)?;
1256
1257        Ok(packet_hash)
1258    }
1259
1260    /// Register a destination with the transport engine and set its proof strategy.
1261    ///
1262    /// `signing_key` is the full 64-byte identity private key (X25519 32 bytes +
1263    /// Ed25519 32 bytes), needed for ProveAll/ProveApp to sign proof packets.
1264    pub fn register_destination_with_proof(
1265        &self,
1266        dest: &crate::destination::Destination,
1267        signing_key: Option<[u8; 64]>,
1268    ) -> Result<(), SendError> {
1269        // Register with transport engine
1270        self.register_destination(dest.hash.0, dest.dest_type.to_wire_constant())?;
1271
1272        // Register proof strategy if not ProveNone
1273        if dest.proof_strategy != rns_core::types::ProofStrategy::ProveNone {
1274            self.tx
1275                .send(Event::RegisterProofStrategy {
1276                    dest_hash: dest.hash.0,
1277                    strategy: dest.proof_strategy,
1278                    signing_key,
1279                })
1280                .map_err(|_| SendError)?;
1281        }
1282
1283        Ok(())
1284    }
1285
1286    /// Request a path to a destination from the network.
1287    pub fn request_path(&self, dest_hash: &rns_core::types::DestHash) -> Result<(), SendError> {
1288        self.tx
1289            .send(Event::RequestPath {
1290                dest_hash: dest_hash.0,
1291            })
1292            .map_err(|_| SendError)
1293    }
1294
1295    /// Check if a path exists to a destination (synchronous query).
1296    pub fn has_path(&self, dest_hash: &rns_core::types::DestHash) -> Result<bool, SendError> {
1297        match self.query(QueryRequest::HasPath {
1298            dest_hash: dest_hash.0,
1299        })? {
1300            QueryResponse::HasPath(v) => Ok(v),
1301            _ => Ok(false),
1302        }
1303    }
1304
1305    /// Get hop count to a destination (synchronous query).
1306    pub fn hops_to(&self, dest_hash: &rns_core::types::DestHash) -> Result<Option<u8>, SendError> {
1307        match self.query(QueryRequest::HopsTo {
1308            dest_hash: dest_hash.0,
1309        })? {
1310            QueryResponse::HopsTo(v) => Ok(v),
1311            _ => Ok(None),
1312        }
1313    }
1314
1315    /// Recall the identity information for a previously announced destination.
1316    pub fn recall_identity(
1317        &self,
1318        dest_hash: &rns_core::types::DestHash,
1319    ) -> Result<Option<crate::destination::AnnouncedIdentity>, SendError> {
1320        match self.query(QueryRequest::RecallIdentity {
1321            dest_hash: dest_hash.0,
1322        })? {
1323            QueryResponse::RecallIdentity(v) => Ok(v),
1324            _ => Ok(None),
1325        }
1326    }
1327
1328    /// Load a WASM hook at runtime.
1329    pub fn load_hook(
1330        &self,
1331        name: String,
1332        wasm_bytes: Vec<u8>,
1333        attach_point: String,
1334        priority: i32,
1335    ) -> Result<Result<(), String>, SendError> {
1336        let (response_tx, response_rx) = std::sync::mpsc::channel();
1337        self.tx
1338            .send(Event::LoadHook {
1339                name,
1340                wasm_bytes,
1341                attach_point,
1342                priority,
1343                response_tx,
1344            })
1345            .map_err(|_| SendError)?;
1346        response_rx.recv().map_err(|_| SendError)
1347    }
1348
1349    /// Unload a WASM hook at runtime.
1350    pub fn unload_hook(
1351        &self,
1352        name: String,
1353        attach_point: String,
1354    ) -> Result<Result<(), String>, SendError> {
1355        let (response_tx, response_rx) = std::sync::mpsc::channel();
1356        self.tx
1357            .send(Event::UnloadHook {
1358                name,
1359                attach_point,
1360                response_tx,
1361            })
1362            .map_err(|_| SendError)?;
1363        response_rx.recv().map_err(|_| SendError)
1364    }
1365
1366    /// Reload a WASM hook at runtime (detach + recompile + reattach with same priority).
1367    pub fn reload_hook(
1368        &self,
1369        name: String,
1370        attach_point: String,
1371        wasm_bytes: Vec<u8>,
1372    ) -> Result<Result<(), String>, SendError> {
1373        let (response_tx, response_rx) = std::sync::mpsc::channel();
1374        self.tx
1375            .send(Event::ReloadHook {
1376                name,
1377                attach_point,
1378                wasm_bytes,
1379                response_tx,
1380            })
1381            .map_err(|_| SendError)?;
1382        response_rx.recv().map_err(|_| SendError)
1383    }
1384
1385    /// List all loaded hooks.
1386    pub fn list_hooks(&self) -> Result<Vec<crate::event::HookInfo>, SendError> {
1387        let (response_tx, response_rx) = std::sync::mpsc::channel();
1388        self.tx
1389            .send(Event::ListHooks { response_tx })
1390            .map_err(|_| SendError)?;
1391        response_rx.recv().map_err(|_| SendError)
1392    }
1393
1394    /// Construct an RnsNode from its constituent parts.
1395    /// Used by `shared_client` to build a client-mode node.
1396    pub(crate) fn from_parts(
1397        tx: EventSender,
1398        driver_handle: thread::JoinHandle<()>,
1399        rpc_server: Option<crate::rpc::RpcServer>,
1400        tick_interval_ms: Arc<AtomicU64>,
1401    ) -> Self {
1402        RnsNode {
1403            tx,
1404            driver_handle: Some(driver_handle),
1405            rpc_server,
1406            tick_interval_ms,
1407            probe_server: None,
1408        }
1409    }
1410
1411    /// Get the event sender for direct event injection.
1412    pub fn event_sender(&self) -> &EventSender {
1413        &self.tx
1414    }
1415
1416    /// Set the tick interval in milliseconds.
1417    /// Default is 1000 (1 second). Changes take effect on the next tick cycle.
1418    /// Values are clamped to the range 100..=10000.
1419    /// Returns the actual stored value (which may differ from `ms` if clamped).
1420    pub fn set_tick_interval(&self, ms: u64) -> u64 {
1421        let clamped = ms.clamp(100, 10_000);
1422        if clamped != ms {
1423            log::warn!(
1424                "tick interval {}ms out of range, clamped to {}ms",
1425                ms,
1426                clamped
1427            );
1428        }
1429        self.tick_interval_ms.store(clamped, Ordering::Relaxed);
1430        clamped
1431    }
1432
1433    /// Get the current tick interval in milliseconds.
1434    pub fn tick_interval(&self) -> u64 {
1435        self.tick_interval_ms.load(Ordering::Relaxed)
1436    }
1437
1438    /// Shut down the node. Blocks until the driver thread exits.
1439    pub fn shutdown(mut self) {
1440        // Stop RPC server first
1441        if let Some(mut rpc) = self.rpc_server.take() {
1442            rpc.stop();
1443        }
1444        let _ = self.tx.send(Event::Shutdown);
1445        if let Some(handle) = self.driver_handle.take() {
1446            let _ = handle.join();
1447        }
1448    }
1449}
1450
1451#[cfg(test)]
1452mod tests {
1453    use super::*;
1454    use std::fs;
1455
1456    struct NoopCallbacks;
1457
1458    impl Callbacks for NoopCallbacks {
1459        fn on_announce(&mut self, _: crate::destination::AnnouncedIdentity) {}
1460        fn on_path_updated(&mut self, _: rns_core::types::DestHash, _: u8) {}
1461        fn on_local_delivery(
1462            &mut self,
1463            _: rns_core::types::DestHash,
1464            _: Vec<u8>,
1465            _: rns_core::types::PacketHash,
1466        ) {
1467        }
1468    }
1469
1470    #[test]
1471    fn start_and_shutdown() {
1472        let node = RnsNode::start(
1473            NodeConfig { panic_on_interface_error: false,
1474                transport_enabled: false,
1475                identity: None,
1476                interfaces: vec![],
1477                share_instance: false,
1478                instance_name: "default".into(),
1479                shared_instance_port: 37428,
1480                rpc_port: 0,
1481                cache_dir: None,
1482                management: Default::default(),
1483                probe_port: None,
1484                probe_addrs: vec![],
1485                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
1486                device: None,
1487                hooks: Vec::new(),
1488                discover_interfaces: false,
1489                discovery_required_value: None,
1490                respond_to_probes: false,
1491                prefer_shorter_path: false,
1492                max_paths_per_destination: 1,
1493                registry: None,
1494            },
1495            Box::new(NoopCallbacks),
1496        )
1497        .unwrap();
1498        node.shutdown();
1499    }
1500
1501    #[test]
1502    fn start_with_identity() {
1503        let identity = Identity::new(&mut OsRng);
1504        let hash = *identity.hash();
1505        let node = RnsNode::start(
1506            NodeConfig { panic_on_interface_error: false,
1507                transport_enabled: true,
1508                identity: Some(identity),
1509                interfaces: vec![],
1510                share_instance: false,
1511                instance_name: "default".into(),
1512                shared_instance_port: 37428,
1513                rpc_port: 0,
1514                cache_dir: None,
1515                management: Default::default(),
1516                probe_port: None,
1517                probe_addrs: vec![],
1518                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
1519                device: None,
1520                hooks: Vec::new(),
1521                discover_interfaces: false,
1522                discovery_required_value: None,
1523                respond_to_probes: false,
1524                prefer_shorter_path: false,
1525                max_paths_per_destination: 1,
1526                registry: None,
1527            },
1528            Box::new(NoopCallbacks),
1529        )
1530        .unwrap();
1531        // The identity hash should have been used
1532        let _ = hash;
1533        node.shutdown();
1534    }
1535
1536    #[test]
1537    fn start_generates_identity() {
1538        let node = RnsNode::start(
1539            NodeConfig { panic_on_interface_error: false,
1540                transport_enabled: false,
1541                identity: None,
1542                interfaces: vec![],
1543                share_instance: false,
1544                instance_name: "default".into(),
1545                shared_instance_port: 37428,
1546                rpc_port: 0,
1547                cache_dir: None,
1548                management: Default::default(),
1549                probe_port: None,
1550                probe_addrs: vec![],
1551                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
1552                device: None,
1553                hooks: Vec::new(),
1554                discover_interfaces: false,
1555                discovery_required_value: None,
1556                respond_to_probes: false,
1557                prefer_shorter_path: false,
1558                max_paths_per_destination: 1,
1559                registry: None,
1560            },
1561            Box::new(NoopCallbacks),
1562        )
1563        .unwrap();
1564        // Should not panic - identity was auto-generated
1565        node.shutdown();
1566    }
1567
1568    #[test]
1569    fn from_config_creates_identity() {
1570        let dir = std::env::temp_dir().join(format!("rns-test-fc-{}", std::process::id()));
1571        let _ = fs::remove_dir_all(&dir);
1572        fs::create_dir_all(&dir).unwrap();
1573
1574        // Write a minimal config file
1575        fs::write(
1576            dir.join("config"),
1577            "[reticulum]\nenable_transport = False\n",
1578        )
1579        .unwrap();
1580
1581        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks)).unwrap();
1582
1583        // Identity file should have been created
1584        assert!(dir.join("storage/identities/identity").exists());
1585
1586        node.shutdown();
1587        let _ = fs::remove_dir_all(&dir);
1588    }
1589
1590    #[test]
1591    fn from_config_loads_identity() {
1592        let dir = std::env::temp_dir().join(format!("rns-test-fl-{}", std::process::id()));
1593        let _ = fs::remove_dir_all(&dir);
1594        fs::create_dir_all(dir.join("storage/identities")).unwrap();
1595
1596        // Pre-create an identity
1597        let identity = Identity::new(&mut OsRng);
1598        let hash = *identity.hash();
1599        storage::save_identity(&identity, &dir.join("storage/identities/identity")).unwrap();
1600
1601        fs::write(
1602            dir.join("config"),
1603            "[reticulum]\nenable_transport = False\n",
1604        )
1605        .unwrap();
1606
1607        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks)).unwrap();
1608
1609        // Verify the same identity was loaded (hash matches)
1610        let loaded = storage::load_identity(&dir.join("storage/identities/identity")).unwrap();
1611        assert_eq!(*loaded.hash(), hash);
1612
1613        node.shutdown();
1614        let _ = fs::remove_dir_all(&dir);
1615    }
1616
1617    #[test]
1618    fn from_config_tcp_server() {
1619        let dir = std::env::temp_dir().join(format!("rns-test-fts-{}", std::process::id()));
1620        let _ = fs::remove_dir_all(&dir);
1621        fs::create_dir_all(&dir).unwrap();
1622
1623        // Find a free port
1624        let port = std::net::TcpListener::bind("127.0.0.1:0")
1625            .unwrap()
1626            .local_addr()
1627            .unwrap()
1628            .port();
1629
1630        let config = format!(
1631            r#"
1632[reticulum]
1633enable_transport = False
1634
1635[interfaces]
1636  [[Test TCP Server]]
1637    type = TCPServerInterface
1638    listen_ip = 127.0.0.1
1639    listen_port = {}
1640"#,
1641            port
1642        );
1643
1644        fs::write(dir.join("config"), config).unwrap();
1645
1646        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks)).unwrap();
1647
1648        // Give server time to start
1649        thread::sleep(Duration::from_millis(100));
1650
1651        // Should be able to connect
1652        let _client = std::net::TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
1653
1654        node.shutdown();
1655        let _ = fs::remove_dir_all(&dir);
1656    }
1657
1658    #[test]
1659    fn test_parse_interface_mode() {
1660        use rns_core::constants::*;
1661
1662        assert_eq!(parse_interface_mode("full"), MODE_FULL);
1663        assert_eq!(parse_interface_mode("Full"), MODE_FULL);
1664        assert_eq!(parse_interface_mode("access_point"), MODE_ACCESS_POINT);
1665        assert_eq!(parse_interface_mode("accesspoint"), MODE_ACCESS_POINT);
1666        assert_eq!(parse_interface_mode("ap"), MODE_ACCESS_POINT);
1667        assert_eq!(parse_interface_mode("AP"), MODE_ACCESS_POINT);
1668        assert_eq!(parse_interface_mode("pointtopoint"), MODE_POINT_TO_POINT);
1669        assert_eq!(parse_interface_mode("ptp"), MODE_POINT_TO_POINT);
1670        assert_eq!(parse_interface_mode("roaming"), MODE_ROAMING);
1671        assert_eq!(parse_interface_mode("boundary"), MODE_BOUNDARY);
1672        assert_eq!(parse_interface_mode("gateway"), MODE_GATEWAY);
1673        assert_eq!(parse_interface_mode("gw"), MODE_GATEWAY);
1674        // Unknown defaults to FULL
1675        assert_eq!(parse_interface_mode("invalid"), MODE_FULL);
1676    }
1677
1678    #[test]
1679    fn to_node_config_serial() {
1680        // Verify from_config parses SerialInterface correctly.
1681        // The serial port won't exist, so start() will fail, but the config
1682        // parsing path is exercised. We verify via the error (not a config error).
1683        let dir = std::env::temp_dir().join(format!("rns-test-serial-{}", std::process::id()));
1684        let _ = fs::remove_dir_all(&dir);
1685        fs::create_dir_all(&dir).unwrap();
1686
1687        let config = r#"
1688[reticulum]
1689enable_transport = False
1690
1691[interfaces]
1692  [[Test Serial Port]]
1693    type = SerialInterface
1694    port = /dev/nonexistent_rns_test_serial
1695    speed = 115200
1696    databits = 8
1697    parity = E
1698    stopbits = 1
1699    interface_mode = ptp
1700    networkname = testnet
1701"#;
1702        fs::write(dir.join("config"), config).unwrap();
1703
1704        // Interface error is non-fatal: the node starts but logs the error.
1705        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks))
1706            .expect("Config should parse; interface failure is non-fatal");
1707        node.shutdown();
1708
1709        let _ = fs::remove_dir_all(&dir);
1710    }
1711
1712    #[test]
1713    fn to_node_config_kiss() {
1714        // Verify from_config parses KISSInterface correctly.
1715        let dir = std::env::temp_dir().join(format!("rns-test-kiss-{}", std::process::id()));
1716        let _ = fs::remove_dir_all(&dir);
1717        fs::create_dir_all(&dir).unwrap();
1718
1719        let config = r#"
1720[reticulum]
1721enable_transport = False
1722
1723[interfaces]
1724  [[Test KISS TNC]]
1725    type = KISSInterface
1726    port = /dev/nonexistent_rns_test_kiss
1727    speed = 9600
1728    preamble = 500
1729    txtail = 30
1730    persistence = 128
1731    slottime = 40
1732    flow_control = True
1733    id_interval = 600
1734    id_callsign = TEST0
1735    interface_mode = full
1736    passphrase = secretkey
1737"#;
1738        fs::write(dir.join("config"), config).unwrap();
1739
1740        // Interface error is non-fatal: the node starts but logs the error.
1741        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks))
1742            .expect("Config should parse; interface failure is non-fatal");
1743        node.shutdown();
1744
1745        let _ = fs::remove_dir_all(&dir);
1746    }
1747
1748    #[test]
1749    fn test_extract_ifac_config() {
1750        use std::collections::HashMap;
1751
1752        // No IFAC params → None
1753        let params: HashMap<String, String> = HashMap::new();
1754        assert!(extract_ifac_config(&params, 16).is_none());
1755
1756        // networkname only
1757        let mut params = HashMap::new();
1758        params.insert("networkname".into(), "testnet".into());
1759        let ifac = extract_ifac_config(&params, 16).unwrap();
1760        assert_eq!(ifac.netname.as_deref(), Some("testnet"));
1761        assert!(ifac.netkey.is_none());
1762        assert_eq!(ifac.size, 16);
1763
1764        // passphrase only with custom size (in bits)
1765        let mut params = HashMap::new();
1766        params.insert("passphrase".into(), "secret".into());
1767        params.insert("ifac_size".into(), "64".into()); // 64 bits = 8 bytes
1768        let ifac = extract_ifac_config(&params, 16).unwrap();
1769        assert!(ifac.netname.is_none());
1770        assert_eq!(ifac.netkey.as_deref(), Some("secret"));
1771        assert_eq!(ifac.size, 8);
1772
1773        // Both with alternate key names
1774        let mut params = HashMap::new();
1775        params.insert("network_name".into(), "mynet".into());
1776        params.insert("pass_phrase".into(), "mykey".into());
1777        let ifac = extract_ifac_config(&params, 8).unwrap();
1778        assert_eq!(ifac.netname.as_deref(), Some("mynet"));
1779        assert_eq!(ifac.netkey.as_deref(), Some("mykey"));
1780        assert_eq!(ifac.size, 8);
1781    }
1782
1783    #[test]
1784    fn to_node_config_rnode() {
1785        // Verify from_config parses RNodeInterface correctly.
1786        // The serial port won't exist, so start() will fail at open time.
1787        let dir = std::env::temp_dir().join(format!("rns-test-rnode-{}", std::process::id()));
1788        let _ = fs::remove_dir_all(&dir);
1789        fs::create_dir_all(&dir).unwrap();
1790
1791        let config = r#"
1792[reticulum]
1793enable_transport = False
1794
1795[interfaces]
1796  [[Test RNode]]
1797    type = RNodeInterface
1798    port = /dev/nonexistent_rns_test_rnode
1799    frequency = 867200000
1800    bandwidth = 125000
1801    txpower = 7
1802    spreadingfactor = 8
1803    codingrate = 5
1804    flow_control = True
1805    st_alock = 5.0
1806    lt_alock = 2.5
1807    interface_mode = full
1808    networkname = testnet
1809"#;
1810        fs::write(dir.join("config"), config).unwrap();
1811
1812        // Interface error is non-fatal: the node starts but logs the error.
1813        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks))
1814            .expect("Config should parse; interface failure is non-fatal");
1815        node.shutdown();
1816
1817        let _ = fs::remove_dir_all(&dir);
1818    }
1819
1820    #[test]
1821    fn to_node_config_pipe() {
1822        // Verify from_config parses PipeInterface correctly.
1823        // Use `cat` as a real command so it actually starts.
1824        let dir = std::env::temp_dir().join(format!("rns-test-pipe-{}", std::process::id()));
1825        let _ = fs::remove_dir_all(&dir);
1826        fs::create_dir_all(&dir).unwrap();
1827
1828        let config = r#"
1829[reticulum]
1830enable_transport = False
1831
1832[interfaces]
1833  [[Test Pipe]]
1834    type = PipeInterface
1835    command = cat
1836    respawn_delay = 5000
1837    interface_mode = full
1838"#;
1839        fs::write(dir.join("config"), config).unwrap();
1840
1841        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks)).unwrap();
1842        // If we got here, config parsing and start() succeeded
1843        node.shutdown();
1844
1845        let _ = fs::remove_dir_all(&dir);
1846    }
1847
1848    #[test]
1849    fn to_node_config_backbone() {
1850        // Verify from_config parses BackboneInterface correctly.
1851        let dir = std::env::temp_dir().join(format!("rns-test-backbone-{}", std::process::id()));
1852        let _ = fs::remove_dir_all(&dir);
1853        fs::create_dir_all(&dir).unwrap();
1854
1855        let port = std::net::TcpListener::bind("127.0.0.1:0")
1856            .unwrap()
1857            .local_addr()
1858            .unwrap()
1859            .port();
1860
1861        let config = format!(
1862            r#"
1863[reticulum]
1864enable_transport = False
1865
1866[interfaces]
1867  [[Test Backbone]]
1868    type = BackboneInterface
1869    listen_ip = 127.0.0.1
1870    listen_port = {}
1871    interface_mode = full
1872"#,
1873            port
1874        );
1875
1876        fs::write(dir.join("config"), config).unwrap();
1877
1878        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks)).unwrap();
1879
1880        // Give server time to start
1881        thread::sleep(Duration::from_millis(100));
1882
1883        // Should be able to connect
1884        {
1885            let _client = std::net::TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
1886            // client drops here, closing the connection cleanly
1887        }
1888
1889        // Small delay to let epoll process the disconnect
1890        thread::sleep(Duration::from_millis(50));
1891
1892        node.shutdown();
1893        let _ = fs::remove_dir_all(&dir);
1894    }
1895
1896    #[test]
1897    fn rnode_config_defaults() {
1898        use crate::interface::rnode::{RNodeConfig, RNodeSubConfig};
1899
1900        let config = RNodeConfig::default();
1901        assert_eq!(config.speed, 115200);
1902        assert!(config.subinterfaces.is_empty());
1903        assert!(config.id_interval.is_none());
1904        assert!(config.id_callsign.is_none());
1905
1906        let sub = RNodeSubConfig {
1907            name: "test".into(),
1908            frequency: 868_000_000,
1909            bandwidth: 125_000,
1910            txpower: 7,
1911            spreading_factor: 8,
1912            coding_rate: 5,
1913            flow_control: false,
1914            st_alock: None,
1915            lt_alock: None,
1916        };
1917        assert_eq!(sub.frequency, 868_000_000);
1918        assert_eq!(sub.bandwidth, 125_000);
1919        assert!(!sub.flow_control);
1920    }
1921
1922    // =========================================================================
1923    // Phase 9c: Announce + Discovery node-level tests
1924    // =========================================================================
1925
1926    #[test]
1927    fn announce_builds_valid_packet() {
1928        let identity = Identity::new(&mut OsRng);
1929        let identity_hash = rns_core::types::IdentityHash(*identity.hash());
1930
1931        let node = RnsNode::start(
1932            NodeConfig { panic_on_interface_error: false,
1933                transport_enabled: false,
1934                identity: None,
1935                interfaces: vec![],
1936                share_instance: false,
1937                instance_name: "default".into(),
1938                shared_instance_port: 37428,
1939                rpc_port: 0,
1940                cache_dir: None,
1941                management: Default::default(),
1942                probe_port: None,
1943                probe_addrs: vec![],
1944                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
1945                device: None,
1946                hooks: Vec::new(),
1947                discover_interfaces: false,
1948                discovery_required_value: None,
1949                respond_to_probes: false,
1950                prefer_shorter_path: false,
1951                max_paths_per_destination: 1,
1952                registry: None,
1953            },
1954            Box::new(NoopCallbacks),
1955        )
1956        .unwrap();
1957
1958        let dest = crate::destination::Destination::single_in("test", &["echo"], identity_hash);
1959
1960        // Register destination first
1961        node.register_destination(dest.hash.0, dest.dest_type.to_wire_constant())
1962            .unwrap();
1963
1964        // Announce should succeed (though no interfaces to send on)
1965        let result = node.announce(&dest, &identity, Some(b"hello"));
1966        assert!(result.is_ok());
1967
1968        node.shutdown();
1969    }
1970
1971    #[test]
1972    fn has_path_and_hops_to() {
1973        let node = RnsNode::start(
1974            NodeConfig { panic_on_interface_error: false,
1975                transport_enabled: false,
1976                identity: None,
1977                interfaces: vec![],
1978                share_instance: false,
1979                instance_name: "default".into(),
1980                shared_instance_port: 37428,
1981                rpc_port: 0,
1982                cache_dir: None,
1983                management: Default::default(),
1984                probe_port: None,
1985                probe_addrs: vec![],
1986                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
1987                device: None,
1988                hooks: Vec::new(),
1989                discover_interfaces: false,
1990                discovery_required_value: None,
1991                respond_to_probes: false,
1992                prefer_shorter_path: false,
1993                max_paths_per_destination: 1,
1994                registry: None,
1995            },
1996            Box::new(NoopCallbacks),
1997        )
1998        .unwrap();
1999
2000        let dh = rns_core::types::DestHash([0xAA; 16]);
2001
2002        // No path should exist
2003        assert_eq!(node.has_path(&dh).unwrap(), false);
2004        assert_eq!(node.hops_to(&dh).unwrap(), None);
2005
2006        node.shutdown();
2007    }
2008
2009    #[test]
2010    fn recall_identity_none_when_unknown() {
2011        let node = RnsNode::start(
2012            NodeConfig { panic_on_interface_error: false,
2013                transport_enabled: false,
2014                identity: None,
2015                interfaces: vec![],
2016                share_instance: false,
2017                instance_name: "default".into(),
2018                shared_instance_port: 37428,
2019                rpc_port: 0,
2020                cache_dir: None,
2021                management: Default::default(),
2022                probe_port: None,
2023                probe_addrs: vec![],
2024                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
2025                device: None,
2026                hooks: Vec::new(),
2027                discover_interfaces: false,
2028                discovery_required_value: None,
2029                respond_to_probes: false,
2030                prefer_shorter_path: false,
2031                max_paths_per_destination: 1,
2032                registry: None,
2033            },
2034            Box::new(NoopCallbacks),
2035        )
2036        .unwrap();
2037
2038        let dh = rns_core::types::DestHash([0xBB; 16]);
2039        assert!(node.recall_identity(&dh).unwrap().is_none());
2040
2041        node.shutdown();
2042    }
2043
2044    #[test]
2045    fn request_path_does_not_crash() {
2046        let node = RnsNode::start(
2047            NodeConfig { panic_on_interface_error: false,
2048                transport_enabled: false,
2049                identity: None,
2050                interfaces: vec![],
2051                share_instance: false,
2052                instance_name: "default".into(),
2053                shared_instance_port: 37428,
2054                rpc_port: 0,
2055                cache_dir: None,
2056                management: Default::default(),
2057                probe_port: None,
2058                probe_addrs: vec![],
2059                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
2060                device: None,
2061                hooks: Vec::new(),
2062                discover_interfaces: false,
2063                discovery_required_value: None,
2064                respond_to_probes: false,
2065                prefer_shorter_path: false,
2066                max_paths_per_destination: 1,
2067                registry: None,
2068            },
2069            Box::new(NoopCallbacks),
2070        )
2071        .unwrap();
2072
2073        let dh = rns_core::types::DestHash([0xCC; 16]);
2074        assert!(node.request_path(&dh).is_ok());
2075
2076        // Small wait for the event to be processed
2077        thread::sleep(Duration::from_millis(50));
2078
2079        node.shutdown();
2080    }
2081
2082    // =========================================================================
2083    // Phase 9d: send_packet + register_destination_with_proof tests
2084    // =========================================================================
2085
2086    #[test]
2087    fn send_packet_plain() {
2088        let node = RnsNode::start(
2089            NodeConfig { panic_on_interface_error: false,
2090                transport_enabled: false,
2091                identity: None,
2092                interfaces: vec![],
2093                share_instance: false,
2094                instance_name: "default".into(),
2095                shared_instance_port: 37428,
2096                rpc_port: 0,
2097                cache_dir: None,
2098                management: Default::default(),
2099                probe_port: None,
2100                probe_addrs: vec![],
2101                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
2102                device: None,
2103                hooks: Vec::new(),
2104                discover_interfaces: false,
2105                discovery_required_value: None,
2106                respond_to_probes: false,
2107                prefer_shorter_path: false,
2108                max_paths_per_destination: 1,
2109                registry: None,
2110            },
2111            Box::new(NoopCallbacks),
2112        )
2113        .unwrap();
2114
2115        let dest = crate::destination::Destination::plain("test", &["echo"]);
2116        let result = node.send_packet(&dest, b"hello world");
2117        assert!(result.is_ok());
2118
2119        let packet_hash = result.unwrap();
2120        // Packet hash should be non-zero
2121        assert_ne!(packet_hash.0, [0u8; 32]);
2122
2123        // Small wait for the event to be processed
2124        thread::sleep(Duration::from_millis(50));
2125
2126        node.shutdown();
2127    }
2128
2129    #[test]
2130    fn send_packet_single_requires_public_key() {
2131        let node = RnsNode::start(
2132            NodeConfig { panic_on_interface_error: false,
2133                transport_enabled: false,
2134                identity: None,
2135                interfaces: vec![],
2136                share_instance: false,
2137                instance_name: "default".into(),
2138                shared_instance_port: 37428,
2139                rpc_port: 0,
2140                cache_dir: None,
2141                management: Default::default(),
2142                probe_port: None,
2143                probe_addrs: vec![],
2144                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
2145                device: None,
2146                hooks: Vec::new(),
2147                discover_interfaces: false,
2148                discovery_required_value: None,
2149                respond_to_probes: false,
2150                prefer_shorter_path: false,
2151                max_paths_per_destination: 1,
2152                registry: None,
2153            },
2154            Box::new(NoopCallbacks),
2155        )
2156        .unwrap();
2157
2158        // single_in has no public_key — sending should fail
2159        let dest = crate::destination::Destination::single_in(
2160            "test",
2161            &["echo"],
2162            rns_core::types::IdentityHash([0x42; 16]),
2163        );
2164        let result = node.send_packet(&dest, b"hello");
2165        assert!(result.is_err(), "single_in has no public_key, should fail");
2166
2167        node.shutdown();
2168    }
2169
2170    #[test]
2171    fn send_packet_single_encrypts() {
2172        let node = RnsNode::start(
2173            NodeConfig { panic_on_interface_error: false,
2174                transport_enabled: false,
2175                identity: None,
2176                interfaces: vec![],
2177                share_instance: false,
2178                instance_name: "default".into(),
2179                shared_instance_port: 37428,
2180                rpc_port: 0,
2181                cache_dir: None,
2182                management: Default::default(),
2183                probe_port: None,
2184                probe_addrs: vec![],
2185                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
2186                device: None,
2187                hooks: Vec::new(),
2188                discover_interfaces: false,
2189                discovery_required_value: None,
2190                respond_to_probes: false,
2191                prefer_shorter_path: false,
2192                max_paths_per_destination: 1,
2193                registry: None,
2194            },
2195            Box::new(NoopCallbacks),
2196        )
2197        .unwrap();
2198
2199        // Create a proper OUT SINGLE destination with a real identity's public key
2200        let remote_identity = Identity::new(&mut OsRng);
2201        let recalled = crate::destination::AnnouncedIdentity {
2202            dest_hash: rns_core::types::DestHash([0xAA; 16]),
2203            identity_hash: rns_core::types::IdentityHash(*remote_identity.hash()),
2204            public_key: remote_identity.get_public_key().unwrap(),
2205            app_data: None,
2206            hops: 1,
2207            received_at: 0.0,
2208            receiving_interface: rns_core::transport::types::InterfaceId(0),
2209        };
2210        let dest = crate::destination::Destination::single_out("test", &["echo"], &recalled);
2211
2212        let result = node.send_packet(&dest, b"secret message");
2213        assert!(result.is_ok());
2214
2215        let packet_hash = result.unwrap();
2216        assert_ne!(packet_hash.0, [0u8; 32]);
2217
2218        thread::sleep(Duration::from_millis(50));
2219        node.shutdown();
2220    }
2221
2222    #[test]
2223    fn register_destination_with_proof_prove_all() {
2224        let node = RnsNode::start(
2225            NodeConfig { panic_on_interface_error: false,
2226                transport_enabled: false,
2227                identity: None,
2228                interfaces: vec![],
2229                share_instance: false,
2230                instance_name: "default".into(),
2231                shared_instance_port: 37428,
2232                rpc_port: 0,
2233                cache_dir: None,
2234                management: Default::default(),
2235                probe_port: None,
2236                probe_addrs: vec![],
2237                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
2238                device: None,
2239                hooks: Vec::new(),
2240                discover_interfaces: false,
2241                discovery_required_value: None,
2242                respond_to_probes: false,
2243                prefer_shorter_path: false,
2244                max_paths_per_destination: 1,
2245                registry: None,
2246            },
2247            Box::new(NoopCallbacks),
2248        )
2249        .unwrap();
2250
2251        let identity = Identity::new(&mut OsRng);
2252        let ih = rns_core::types::IdentityHash(*identity.hash());
2253        let dest = crate::destination::Destination::single_in("echo", &["request"], ih)
2254            .set_proof_strategy(rns_core::types::ProofStrategy::ProveAll);
2255        let prv_key = identity.get_private_key().unwrap();
2256
2257        let result = node.register_destination_with_proof(&dest, Some(prv_key));
2258        assert!(result.is_ok());
2259
2260        // Small wait for the events to be processed
2261        thread::sleep(Duration::from_millis(50));
2262
2263        node.shutdown();
2264    }
2265
2266    #[test]
2267    fn register_destination_with_proof_prove_none() {
2268        let node = RnsNode::start(
2269            NodeConfig { panic_on_interface_error: false,
2270                transport_enabled: false,
2271                identity: None,
2272                interfaces: vec![],
2273                share_instance: false,
2274                instance_name: "default".into(),
2275                shared_instance_port: 37428,
2276                rpc_port: 0,
2277                cache_dir: None,
2278                management: Default::default(),
2279                probe_port: None,
2280                probe_addrs: vec![],
2281                probe_protocol: rns_core::holepunch::ProbeProtocol::Rnsp,
2282                device: None,
2283                hooks: Vec::new(),
2284                discover_interfaces: false,
2285                discovery_required_value: None,
2286                respond_to_probes: false,
2287                prefer_shorter_path: false,
2288                max_paths_per_destination: 1,
2289                registry: None,
2290            },
2291            Box::new(NoopCallbacks),
2292        )
2293        .unwrap();
2294
2295        // ProveNone should not send RegisterProofStrategy event
2296        let dest = crate::destination::Destination::plain("test", &["data"])
2297            .set_proof_strategy(rns_core::types::ProofStrategy::ProveNone);
2298
2299        let result = node.register_destination_with_proof(&dest, None);
2300        assert!(result.is_ok());
2301
2302        thread::sleep(Duration::from_millis(50));
2303        node.shutdown();
2304    }
2305}