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