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::{InterfaceInfo, 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;
20use crate::interface::tcp::TcpClientConfig;
21use crate::interface::tcp_server::TcpServerConfig;
22use crate::interface::udp::UdpConfig;
23use crate::interface::local::{LocalServerConfig, LocalClientConfig};
24use crate::interface::serial_iface::SerialIfaceConfig;
25use crate::interface::kiss_iface::KissIfaceConfig;
26use crate::interface::pipe::PipeConfig;
27use crate::interface::rnode::{RNodeConfig, RNodeSubConfig};
28use crate::interface::backbone::{BackboneConfig, BackboneClientConfig};
29use crate::interface::auto::AutoConfig;
30use crate::interface::i2p::I2pConfig;
31use crate::interface::{InterfaceEntry, InterfaceStats};
32use crate::time;
33use crate::serial::Parity;
34use crate::storage;
35
36/// Parse an interface mode string to the corresponding constant.
37/// Matches Python's `_synthesize_interface()` in `RNS/Reticulum.py`.
38fn parse_interface_mode(mode: &str) -> u8 {
39    match mode.to_lowercase().as_str() {
40        "full" => rns_core::constants::MODE_FULL,
41        "access_point" | "accesspoint" | "ap" => rns_core::constants::MODE_ACCESS_POINT,
42        "pointtopoint" | "ptp" => rns_core::constants::MODE_POINT_TO_POINT,
43        "roaming" => rns_core::constants::MODE_ROAMING,
44        "boundary" => rns_core::constants::MODE_BOUNDARY,
45        "gateway" | "gw" => rns_core::constants::MODE_GATEWAY,
46        _ => rns_core::constants::MODE_FULL,
47    }
48}
49
50/// Parse a parity string from config. Matches Python's serial.PARITY_*.
51fn parse_parity(s: &str) -> Parity {
52    match s.to_lowercase().as_str() {
53        "e" | "even" => Parity::Even,
54        "o" | "odd" => Parity::Odd,
55        _ => Parity::None,
56    }
57}
58
59/// Extract IFAC configuration from interface params, if present.
60/// Returns None if neither networkname/network_name nor passphrase/pass_phrase is set.
61fn extract_ifac_config(params: &std::collections::HashMap<String, String>, default_size: usize) -> Option<IfacConfig> {
62    let netname = params.get("networkname")
63        .or_else(|| params.get("network_name"))
64        .cloned();
65    let netkey = params.get("passphrase")
66        .or_else(|| params.get("pass_phrase"))
67        .cloned();
68
69    if netname.is_none() && netkey.is_none() {
70        return None;
71    }
72
73    // ifac_size is specified in bits in config, divide by 8 for bytes
74    let size = params.get("ifac_size")
75        .and_then(|v| v.parse::<usize>().ok())
76        .map(|bits| (bits / 8).max(1))
77        .unwrap_or(default_size);
78
79    Some(IfacConfig { netname, netkey, size })
80}
81
82/// Extract discovery configuration from interface params, if `discoverable` is set.
83fn extract_discovery_config(
84    iface_name: &str,
85    iface_type: &str,
86    params: &std::collections::HashMap<String, String>,
87) -> Option<crate::discovery::DiscoveryConfig> {
88    let discoverable = params.get("discoverable")
89        .and_then(|v| config::parse_bool_pub(v))
90        .unwrap_or(false);
91    if !discoverable {
92        return None;
93    }
94
95    let discovery_name = params.get("discovery_name")
96        .cloned()
97        .unwrap_or_else(|| iface_name.to_string());
98
99    // Config value is in seconds. Min 300s (5min), default 21600s (6h).
100    let announce_interval = params.get("announce_interval")
101        .and_then(|v| v.parse::<u64>().ok())
102        .map(|secs| secs.max(300))
103        .unwrap_or(21600);
104
105    let stamp_value = params.get("discovery_stamp_value")
106        .and_then(|v| v.parse::<u8>().ok())
107        .unwrap_or(crate::discovery::DEFAULT_STAMP_VALUE);
108
109    let reachable_on = params.get("reachable_on").cloned();
110
111    let listen_port = params.get("listen_port")
112        .or_else(|| params.get("port"))
113        .and_then(|v| v.parse().ok());
114
115    let latitude = params.get("latitude")
116        .or_else(|| params.get("lat"))
117        .and_then(|v| v.parse().ok());
118    let longitude = params.get("longitude")
119        .or_else(|| params.get("lon"))
120        .and_then(|v| v.parse().ok());
121    let height = params.get("height")
122        .and_then(|v| v.parse().ok());
123
124    Some(crate::discovery::DiscoveryConfig {
125        discovery_name,
126        announce_interval,
127        stamp_value,
128        reachable_on,
129        interface_type: iface_type.to_string(),
130        listen_port,
131        latitude,
132        longitude,
133        height,
134    })
135}
136
137/// Top-level node configuration.
138pub struct NodeConfig {
139    pub transport_enabled: bool,
140    pub identity: Option<Identity>,
141    pub interfaces: Vec<InterfaceConfig>,
142    /// Enable shared instance server for local clients (rns-ctl, etc.)
143    pub share_instance: bool,
144    /// Instance name for Unix socket namespace (default: "default").
145    pub instance_name: String,
146    /// Shared instance port for local client connections (default 37428).
147    pub shared_instance_port: u16,
148    /// RPC control port (default 37429). Only used when share_instance is true.
149    pub rpc_port: u16,
150    /// Cache directory for announce cache. If None, announce caching is disabled.
151    pub cache_dir: Option<std::path::PathBuf>,
152    /// Remote management configuration.
153    pub management: crate::management::ManagementConfig,
154    /// Port to run the STUN probe server on (for facilitator nodes).
155    pub probe_port: Option<u16>,
156    /// Address of the STUN probe server (for client nodes behind NAT).
157    pub probe_addr: Option<std::net::SocketAddr>,
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}
167
168/// Interface configuration variant with its mode.
169pub struct InterfaceConfig {
170    pub variant: InterfaceVariant,
171    /// Interface mode (MODE_FULL, MODE_ACCESS_POINT, etc.)
172    pub mode: u8,
173    /// IFAC (Interface Access Code) configuration, if enabled.
174    pub ifac: Option<IfacConfig>,
175    /// Discovery configuration, if this interface is discoverable.
176    pub discovery: Option<crate::discovery::DiscoveryConfig>,
177}
178
179/// IFAC configuration for an interface.
180pub struct IfacConfig {
181    pub netname: Option<String>,
182    pub netkey: Option<String>,
183    pub size: usize,
184}
185
186/// The specific interface type and its parameters.
187pub enum InterfaceVariant {
188    TcpClient(TcpClientConfig),
189    TcpServer(TcpServerConfig),
190    Udp(UdpConfig),
191    LocalServer(LocalServerConfig),
192    LocalClient(LocalClientConfig),
193    Serial(SerialIfaceConfig),
194    Kiss(KissIfaceConfig),
195    Pipe(PipeConfig),
196    RNode(RNodeConfig),
197    Backbone(BackboneConfig),
198    BackboneClient(BackboneClientConfig),
199    Auto(AutoConfig),
200    I2p(I2pConfig),
201}
202
203use crate::event::{QueryRequest, QueryResponse};
204
205/// Error returned when the driver thread has shut down.
206#[derive(Debug)]
207pub struct SendError;
208
209impl std::fmt::Display for SendError {
210    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211        write!(f, "driver shut down")
212    }
213}
214
215impl std::error::Error for SendError {}
216
217/// A running RNS node.
218pub struct RnsNode {
219    tx: EventSender,
220    driver_handle: Option<JoinHandle<()>>,
221    rpc_server: Option<crate::rpc::RpcServer>,
222    tick_interval_ms: Arc<AtomicU64>,
223    #[allow(dead_code)]
224    probe_server: Option<crate::holepunch::probe::ProbeServerHandle>,
225}
226
227impl RnsNode {
228    /// Start the node from a config file path.
229    /// If `config_path` is None, uses `~/.reticulum/`.
230    pub fn from_config(
231        config_path: Option<&Path>,
232        callbacks: Box<dyn Callbacks>,
233    ) -> io::Result<Self> {
234        let config_dir = storage::resolve_config_dir(config_path);
235        let paths = storage::ensure_storage_dirs(&config_dir)?;
236
237        // Parse config file
238        let config_file = config_dir.join("config");
239        let rns_config = if config_file.exists() {
240            config::parse_file(&config_file).map_err(|e| {
241                io::Error::new(io::ErrorKind::InvalidData, format!("{}", e))
242            })?
243        } else {
244            // No config file, use defaults
245            config::parse("").map_err(|e| {
246                io::Error::new(io::ErrorKind::InvalidData, format!("{}", e))
247            })?
248        };
249
250        // Load or create identity
251        let identity = if let Some(ref id_path_str) = rns_config.reticulum.network_identity {
252            let id_path = std::path::PathBuf::from(id_path_str);
253            if id_path.exists() {
254                storage::load_identity(&id_path)?
255            } else {
256                let id = Identity::new(&mut OsRng);
257                storage::save_identity(&id, &id_path)?;
258                id
259            }
260        } else {
261            storage::load_or_create_identity(&paths.identities)?
262        };
263
264        // Build interface configs from parsed config
265        let mut interface_configs = Vec::new();
266        let mut next_id_val = 1u64;
267
268        for iface in &rns_config.interfaces {
269            if !iface.enabled {
270                continue;
271            }
272
273            let iface_id = rns_core::transport::types::InterfaceId(next_id_val);
274            next_id_val += 1;
275
276            let mut iface_mode = parse_interface_mode(&iface.mode);
277
278            // Auto-configure mode when discovery is enabled (Python Reticulum.py).
279            // AutoInterface inherently uses discovery; RNodeInterface may have discoverable=true.
280            let has_discovery = match iface.interface_type.as_str() {
281                "AutoInterface" => true,
282                "RNodeInterface" => iface.params.get("discoverable")
283                    .and_then(|v| config::parse_bool_pub(v))
284                    .unwrap_or(false),
285                _ => false,
286            };
287            if has_discovery
288                && iface_mode != rns_core::constants::MODE_ACCESS_POINT
289                && iface_mode != rns_core::constants::MODE_GATEWAY
290            {
291                let new_mode = if iface.interface_type == "RNodeInterface" {
292                    rns_core::constants::MODE_ACCESS_POINT
293                } else {
294                    rns_core::constants::MODE_GATEWAY
295                };
296                log::info!(
297                    "Interface '{}' has discovery enabled, auto-configuring mode to {}",
298                    iface.name,
299                    if new_mode == rns_core::constants::MODE_ACCESS_POINT {
300                        "ACCESS_POINT"
301                    } else {
302                        "GATEWAY"
303                    }
304                );
305                iface_mode = new_mode;
306            }
307
308            // Default IFAC size depends on interface type:
309            // 8 bytes for Serial/KISS/RNode, 16 for TCP/UDP/Auto/Local
310            let default_ifac_size = match iface.interface_type.as_str() {
311                "SerialInterface" | "KISSInterface" | "RNodeInterface" => 8,
312                _ => 16,
313            };
314            let ifac_config = extract_ifac_config(&iface.params, default_ifac_size);
315            let discovery_config = extract_discovery_config(
316                &iface.name, &iface.interface_type, &iface.params,
317            );
318
319            match iface.interface_type.as_str() {
320                "TCPClientInterface" => {
321                    let target_host = iface
322                        .params
323                        .get("target_host")
324                        .cloned()
325                        .unwrap_or_else(|| "127.0.0.1".into());
326                    let target_port = iface
327                        .params
328                        .get("target_port")
329                        .and_then(|v| v.parse().ok())
330                        .unwrap_or(4242);
331
332                    interface_configs.push(InterfaceConfig {
333                        variant: InterfaceVariant::TcpClient(TcpClientConfig {
334                            name: iface.name.clone(),
335                            target_host,
336                            target_port,
337                            interface_id: iface_id,
338                            device: rns_config.reticulum.device.clone(),
339                            ..TcpClientConfig::default()
340                        }),
341                        mode: iface_mode,
342                        ifac: ifac_config,
343                        discovery: discovery_config.clone(),
344                    });
345                }
346                "TCPServerInterface" => {
347                    let listen_ip = iface
348                        .params
349                        .get("listen_ip")
350                        .cloned()
351                        .unwrap_or_else(|| "0.0.0.0".into());
352                    let listen_port = iface
353                        .params
354                        .get("listen_port")
355                        .and_then(|v| v.parse().ok())
356                        .unwrap_or(4242);
357
358                    interface_configs.push(InterfaceConfig {
359                        variant: InterfaceVariant::TcpServer(TcpServerConfig {
360                            name: iface.name.clone(),
361                            listen_ip,
362                            listen_port,
363                            interface_id: iface_id,
364                        }),
365                        mode: iface_mode,
366                        ifac: ifac_config,
367                        discovery: discovery_config.clone(),
368                    });
369                }
370                "UDPInterface" => {
371                    let listen_ip = iface.params.get("listen_ip").cloned();
372                    let listen_port = iface
373                        .params
374                        .get("listen_port")
375                        .and_then(|v| v.parse().ok());
376                    let forward_ip = iface.params.get("forward_ip").cloned();
377                    let forward_port = iface
378                        .params
379                        .get("forward_port")
380                        .and_then(|v| v.parse().ok());
381
382                    // Handle 'port' shorthand (sets both listen_port and forward_port)
383                    let port = iface.params.get("port").and_then(|v| v.parse::<u16>().ok());
384                    let listen_port = listen_port.or(port);
385                    let forward_port = forward_port.or(port);
386
387                    interface_configs.push(InterfaceConfig {
388                        variant: InterfaceVariant::Udp(UdpConfig {
389                            name: iface.name.clone(),
390                            listen_ip,
391                            listen_port,
392                            forward_ip,
393                            forward_port,
394                            interface_id: iface_id,
395                        }),
396                        mode: iface_mode,
397                        ifac: ifac_config,
398                        discovery: discovery_config.clone(),
399                    });
400                }
401                "SerialInterface" => {
402                    let port = match iface.params.get("port") {
403                        Some(p) => p.clone(),
404                        None => {
405                            log::warn!("No port specified for SerialInterface '{}'", iface.name);
406                            continue;
407                        }
408                    };
409                    let speed = iface.params.get("speed")
410                        .and_then(|v| v.parse().ok())
411                        .unwrap_or(9600);
412                    let databits = iface.params.get("databits")
413                        .and_then(|v| v.parse().ok())
414                        .unwrap_or(8);
415                    let parity = iface.params.get("parity")
416                        .map(|v| parse_parity(v))
417                        .unwrap_or(Parity::None);
418                    let stopbits = iface.params.get("stopbits")
419                        .and_then(|v| v.parse().ok())
420                        .unwrap_or(1);
421
422                    interface_configs.push(InterfaceConfig {
423                        variant: InterfaceVariant::Serial(SerialIfaceConfig {
424                            name: iface.name.clone(),
425                            port,
426                            speed,
427                            data_bits: databits,
428                            parity,
429                            stop_bits: stopbits,
430                            interface_id: iface_id,
431                        }),
432                        mode: iface_mode,
433                        ifac: ifac_config,
434                        discovery: discovery_config.clone(),
435                    });
436                }
437                "KISSInterface" => {
438                    let port = match iface.params.get("port") {
439                        Some(p) => p.clone(),
440                        None => {
441                            log::warn!("No port specified for KISSInterface '{}'", iface.name);
442                            continue;
443                        }
444                    };
445                    let speed = iface.params.get("speed")
446                        .and_then(|v| v.parse().ok())
447                        .unwrap_or(9600);
448                    let databits = iface.params.get("databits")
449                        .and_then(|v| v.parse().ok())
450                        .unwrap_or(8);
451                    let parity = iface.params.get("parity")
452                        .map(|v| parse_parity(v))
453                        .unwrap_or(Parity::None);
454                    let stopbits = iface.params.get("stopbits")
455                        .and_then(|v| v.parse().ok())
456                        .unwrap_or(1);
457                    let preamble = iface.params.get("preamble")
458                        .and_then(|v| v.parse().ok())
459                        .unwrap_or(350);
460                    let txtail = iface.params.get("txtail")
461                        .and_then(|v| v.parse().ok())
462                        .unwrap_or(20);
463                    let persistence = iface.params.get("persistence")
464                        .and_then(|v| v.parse().ok())
465                        .unwrap_or(64);
466                    let slottime = iface.params.get("slottime")
467                        .and_then(|v| v.parse().ok())
468                        .unwrap_or(20);
469                    let flow_control = iface.params.get("flow_control")
470                        .and_then(|v| config::parse_bool_pub(v))
471                        .unwrap_or(false);
472                    let beacon_interval = iface.params.get("id_interval")
473                        .and_then(|v| v.parse().ok());
474                    let beacon_data = iface.params.get("id_callsign")
475                        .map(|v| v.as_bytes().to_vec());
476
477                    interface_configs.push(InterfaceConfig {
478                        variant: InterfaceVariant::Kiss(KissIfaceConfig {
479                            name: iface.name.clone(),
480                            port,
481                            speed,
482                            data_bits: databits,
483                            parity,
484                            stop_bits: stopbits,
485                            preamble,
486                            txtail,
487                            persistence,
488                            slottime,
489                            flow_control,
490                            beacon_interval,
491                            beacon_data,
492                            interface_id: iface_id,
493                        }),
494                        mode: iface_mode,
495                        ifac: ifac_config,
496                        discovery: discovery_config.clone(),
497                    });
498                }
499                "RNodeInterface" => {
500                    let port = match iface.params.get("port") {
501                        Some(p) => p.clone(),
502                        None => {
503                            log::warn!("No port specified for RNodeInterface '{}'", iface.name);
504                            continue;
505                        }
506                    };
507                    let speed = iface.params.get("speed")
508                        .and_then(|v| v.parse().ok())
509                        .unwrap_or(115200);
510                    let frequency = iface.params.get("frequency")
511                        .and_then(|v| v.parse().ok())
512                        .unwrap_or(868_000_000);
513                    let bandwidth = iface.params.get("bandwidth")
514                        .and_then(|v| v.parse().ok())
515                        .unwrap_or(125_000);
516                    let txpower = iface.params.get("txpower")
517                        .and_then(|v| v.parse().ok())
518                        .unwrap_or(7);
519                    let spreading_factor = iface.params.get("spreadingfactor")
520                        .or_else(|| iface.params.get("spreading_factor"))
521                        .and_then(|v| v.parse().ok())
522                        .unwrap_or(8);
523                    let coding_rate = iface.params.get("codingrate")
524                        .or_else(|| iface.params.get("coding_rate"))
525                        .and_then(|v| v.parse().ok())
526                        .unwrap_or(5);
527                    let flow_control = iface.params.get("flow_control")
528                        .and_then(|v| config::parse_bool_pub(v))
529                        .unwrap_or(false);
530                    let st_alock = iface.params.get("st_alock")
531                        .and_then(|v| v.parse().ok());
532                    let lt_alock = iface.params.get("lt_alock")
533                        .and_then(|v| v.parse().ok());
534                    let id_interval = iface.params.get("id_interval")
535                        .and_then(|v| v.parse().ok());
536                    let id_callsign = iface.params.get("id_callsign")
537                        .map(|v| v.as_bytes().to_vec());
538
539                    let sub = RNodeSubConfig {
540                        name: iface.name.clone(),
541                        frequency,
542                        bandwidth,
543                        txpower,
544                        spreading_factor,
545                        coding_rate,
546                        flow_control,
547                        st_alock,
548                        lt_alock,
549                    };
550
551                    interface_configs.push(InterfaceConfig {
552                        variant: InterfaceVariant::RNode(RNodeConfig {
553                            name: iface.name.clone(),
554                            port,
555                            speed,
556                            subinterfaces: vec![sub],
557                            id_interval,
558                            id_callsign,
559                            base_interface_id: iface_id,
560                        }),
561                        mode: iface_mode,
562                        ifac: ifac_config,
563                        discovery: discovery_config.clone(),
564                    });
565                }
566                "PipeInterface" => {
567                    let command = match iface.params.get("command") {
568                        Some(c) => c.clone(),
569                        None => {
570                            log::warn!("No command specified for PipeInterface '{}'", iface.name);
571                            continue;
572                        }
573                    };
574                    let respawn_delay = iface.params.get("respawn_delay")
575                        .and_then(|v| v.parse::<u64>().ok())
576                        .map(Duration::from_millis)
577                        .unwrap_or(Duration::from_secs(5));
578
579                    interface_configs.push(InterfaceConfig {
580                        variant: InterfaceVariant::Pipe(PipeConfig {
581                            name: iface.name.clone(),
582                            command,
583                            respawn_delay,
584                            interface_id: iface_id,
585                        }),
586                        mode: iface_mode,
587                        ifac: ifac_config,
588                        discovery: discovery_config.clone(),
589                    });
590                }
591                "BackboneInterface" => {
592                    if let Some(target_host) = iface.params.get("remote")
593                        .or_else(|| iface.params.get("target_host"))
594                    {
595                        // Client mode
596                        let target_host = target_host.clone();
597                        let target_port = iface.params.get("target_port")
598                            .or_else(|| iface.params.get("port"))
599                            .and_then(|v| v.parse().ok())
600                            .unwrap_or(4242);
601                        let transport_identity = iface.params.get("transport_identity").cloned();
602
603                        interface_configs.push(InterfaceConfig {
604                            variant: InterfaceVariant::BackboneClient(BackboneClientConfig {
605                                name: iface.name.clone(),
606                                target_host,
607                                target_port,
608                                interface_id: iface_id,
609                                transport_identity,
610                                ..BackboneClientConfig::default()
611                            }),
612                            mode: iface_mode,
613                            ifac: ifac_config,
614                            discovery: discovery_config.clone(),
615                        });
616                    } else {
617                        // Server mode
618                        let listen_ip = iface.params.get("listen_ip")
619                            .or_else(|| iface.params.get("device"))
620                            .cloned()
621                            .unwrap_or_else(|| "0.0.0.0".into());
622                        let listen_port = iface.params.get("listen_port")
623                            .or_else(|| iface.params.get("port"))
624                            .and_then(|v| v.parse().ok())
625                            .unwrap_or(4242);
626
627                        interface_configs.push(InterfaceConfig {
628                            variant: InterfaceVariant::Backbone(BackboneConfig {
629                                name: iface.name.clone(),
630                                listen_ip,
631                                listen_port,
632                                interface_id: iface_id,
633                            }),
634                            mode: iface_mode,
635                            ifac: ifac_config,
636                            discovery: discovery_config.clone(),
637                        });
638                    }
639                }
640                "AutoInterface" => {
641                    let group_id = iface
642                        .params
643                        .get("group_id")
644                        .map(|s| s.as_bytes().to_vec())
645                        .unwrap_or_else(|| crate::interface::auto::DEFAULT_GROUP_ID.to_vec());
646
647                    let discovery_scope = iface
648                        .params
649                        .get("discovery_scope")
650                        .map(|s| match s.to_lowercase().as_str() {
651                            "link" => crate::interface::auto::SCOPE_LINK.to_string(),
652                            "admin" => crate::interface::auto::SCOPE_ADMIN.to_string(),
653                            "site" => crate::interface::auto::SCOPE_SITE.to_string(),
654                            "organisation" | "organization" => crate::interface::auto::SCOPE_ORGANISATION.to_string(),
655                            "global" => crate::interface::auto::SCOPE_GLOBAL.to_string(),
656                            other => other.to_string(),
657                        })
658                        .unwrap_or_else(|| crate::interface::auto::SCOPE_LINK.to_string());
659
660                    let discovery_port = iface
661                        .params
662                        .get("discovery_port")
663                        .and_then(|v| v.parse().ok())
664                        .unwrap_or(crate::interface::auto::DEFAULT_DISCOVERY_PORT);
665
666                    let data_port = iface
667                        .params
668                        .get("data_port")
669                        .and_then(|v| v.parse().ok())
670                        .unwrap_or(crate::interface::auto::DEFAULT_DATA_PORT);
671
672                    let multicast_address_type = iface
673                        .params
674                        .get("multicast_address_type")
675                        .map(|s| match s.to_lowercase().as_str() {
676                            "permanent" => crate::interface::auto::MULTICAST_PERMANENT_ADDRESS_TYPE.to_string(),
677                            "temporary" => crate::interface::auto::MULTICAST_TEMPORARY_ADDRESS_TYPE.to_string(),
678                            other => other.to_string(),
679                        })
680                        .unwrap_or_else(|| crate::interface::auto::MULTICAST_TEMPORARY_ADDRESS_TYPE.to_string());
681
682                    let configured_bitrate = iface
683                        .params
684                        .get("configured_bitrate")
685                        .or_else(|| iface.params.get("bitrate"))
686                        .and_then(|v| v.parse().ok())
687                        .unwrap_or(crate::interface::auto::BITRATE_GUESS);
688
689                    // Parse device lists (comma-separated)
690                    let allowed_interfaces = iface
691                        .params
692                        .get("devices")
693                        .or_else(|| iface.params.get("allowed_interfaces"))
694                        .map(|s| s.split(',').map(|d| d.trim().to_string()).filter(|d| !d.is_empty()).collect())
695                        .unwrap_or_default();
696
697                    let ignored_interfaces = iface
698                        .params
699                        .get("ignored_devices")
700                        .or_else(|| iface.params.get("ignored_interfaces"))
701                        .map(|s| s.split(',').map(|d| d.trim().to_string()).filter(|d| !d.is_empty()).collect())
702                        .unwrap_or_default();
703
704                    interface_configs.push(InterfaceConfig {
705                        variant: InterfaceVariant::Auto(AutoConfig {
706                            name: iface.name.clone(),
707                            group_id,
708                            discovery_scope,
709                            discovery_port,
710                            data_port,
711                            multicast_address_type,
712                            allowed_interfaces,
713                            ignored_interfaces,
714                            configured_bitrate,
715                            interface_id: iface_id,
716                        }),
717                        mode: iface_mode,
718                        ifac: ifac_config,
719                        discovery: discovery_config.clone(),
720                    });
721                }
722                "I2PInterface" => {
723                    let sam_host = iface
724                        .params
725                        .get("sam_host")
726                        .cloned()
727                        .unwrap_or_else(|| "127.0.0.1".into());
728                    let sam_port = iface
729                        .params
730                        .get("sam_port")
731                        .and_then(|v| v.parse().ok())
732                        .unwrap_or(7656);
733                    let connectable = iface
734                        .params
735                        .get("connectable")
736                        .and_then(|v| config::parse_bool_pub(v))
737                        .unwrap_or(false);
738                    let peers: Vec<String> = iface
739                        .params
740                        .get("peers")
741                        .map(|s| {
742                            s.split(',')
743                                .map(|p| p.trim().to_string())
744                                .filter(|p| !p.is_empty())
745                                .collect()
746                        })
747                        .unwrap_or_default();
748
749                    interface_configs.push(InterfaceConfig {
750                        variant: InterfaceVariant::I2p(I2pConfig {
751                            name: iface.name.clone(),
752                            interface_id: iface_id,
753                            sam_host,
754                            sam_port,
755                            peers,
756                            connectable,
757                            storage_dir: paths.storage.clone(),
758                        }),
759                        mode: iface_mode,
760                        ifac: ifac_config,
761                        discovery: discovery_config.clone(),
762                    });
763                }
764                _ => {
765                    log::warn!(
766                        "Unsupported interface type '{}' for '{}'",
767                        iface.interface_type,
768                        iface.name
769                    );
770                }
771            }
772        }
773
774        // Parse management config
775        let mut mgmt_allowed = Vec::new();
776        for hex_hash in &rns_config.reticulum.remote_management_allowed {
777            if hex_hash.len() == 32 {
778                if let Ok(bytes) = (0..hex_hash.len())
779                    .step_by(2)
780                    .map(|i| u8::from_str_radix(&hex_hash[i..i+2], 16))
781                    .collect::<Result<Vec<u8>, _>>()
782                {
783                    if bytes.len() == 16 {
784                        let mut h = [0u8; 16];
785                        h.copy_from_slice(&bytes);
786                        mgmt_allowed.push(h);
787                    }
788                } else {
789                    log::warn!("Invalid hex in remote_management_allowed: {}", hex_hash);
790                }
791            } else {
792                log::warn!(
793                    "Invalid entry in remote_management_allowed (expected 32 hex chars, got {}): {}",
794                    hex_hash.len(), hex_hash,
795                );
796            }
797        }
798
799        // Parse probe_addr string to SocketAddr
800        let probe_addr = rns_config.reticulum.probe_addr.as_ref().and_then(|s| {
801            s.parse::<std::net::SocketAddr>().map_err(|e| {
802                log::warn!("Invalid probe_addr '{}': {}", s, e);
803                e
804            }).ok()
805        });
806
807        let node_config = NodeConfig {
808            transport_enabled: rns_config.reticulum.enable_transport,
809            identity: Some(identity),
810            interfaces: interface_configs,
811            share_instance: rns_config.reticulum.share_instance,
812            instance_name: rns_config.reticulum.instance_name.clone(),
813            shared_instance_port: rns_config.reticulum.shared_instance_port,
814            rpc_port: rns_config.reticulum.instance_control_port,
815            cache_dir: Some(paths.cache),
816            management: crate::management::ManagementConfig {
817                enable_remote_management: rns_config.reticulum.enable_remote_management,
818                remote_management_allowed: mgmt_allowed,
819                publish_blackhole: rns_config.reticulum.publish_blackhole,
820            },
821            probe_port: rns_config.reticulum.probe_port,
822            probe_addr,
823            device: rns_config.reticulum.device.clone(),
824            hooks: rns_config.hooks.clone(),
825            discover_interfaces: rns_config.reticulum.discover_interfaces,
826            discovery_required_value: rns_config.reticulum.required_discovery_value,
827        };
828
829        Self::start(node_config, callbacks)
830    }
831
832    /// Start the node. Connects all interfaces, starts driver and timer threads.
833    pub fn start(config: NodeConfig, callbacks: Box<dyn Callbacks>) -> io::Result<Self> {
834        let identity = config
835            .identity
836            .unwrap_or_else(|| Identity::new(&mut OsRng));
837
838        let transport_config = TransportConfig {
839            transport_enabled: config.transport_enabled,
840            identity_hash: Some(*identity.hash()),
841        };
842
843        let (tx, rx) = event::channel();
844        let mut driver = Driver::new(transport_config, rx, tx.clone(), callbacks);
845
846        // Set up announce cache if cache directory is configured
847        if let Some(ref cache_dir) = config.cache_dir {
848            let announces_dir = cache_dir.join("announces");
849            let _ = std::fs::create_dir_all(&announces_dir);
850            driver.announce_cache = Some(crate::announce_cache::AnnounceCache::new(announces_dir));
851        }
852
853        // Configure probe address and device for hole punching
854        if config.probe_addr.is_some() || config.device.is_some() {
855            driver.set_probe_config(config.probe_addr, config.device.clone());
856        }
857
858        // Start probe server if configured
859        let probe_server = if let Some(port) = config.probe_port {
860            let listen_addr: std::net::SocketAddr = ([0, 0, 0, 0], port).into();
861            match crate::holepunch::probe::start_probe_server(listen_addr) {
862                Ok(handle) => {
863                    log::info!("Probe server started on 0.0.0.0:{}", port);
864                    Some(handle)
865                }
866                Err(e) => {
867                    log::error!("Failed to start probe server on port {}: {}", port, e);
868                    None
869                }
870            }
871        } else {
872            None
873        };
874
875        // Store management config on driver for ACL enforcement
876        driver.management_config = config.management.clone();
877
878        // Store transport identity for tunnel synthesis
879        if let Some(prv_key) = identity.get_private_key() {
880            driver.transport_identity = Some(Identity::from_private_key(&prv_key));
881        }
882
883        // Load hooks from config
884        #[cfg(feature = "rns-hooks")]
885        {
886            for hook_cfg in &config.hooks {
887                if !hook_cfg.enabled {
888                    continue;
889                }
890                let point_idx = match config::parse_hook_point(&hook_cfg.attach_point) {
891                    Some(idx) => idx,
892                    None => {
893                        log::warn!(
894                            "Unknown hook point '{}' for hook '{}'",
895                            hook_cfg.attach_point,
896                            hook_cfg.name,
897                        );
898                        continue;
899                    }
900                };
901                let mgr = match driver.hook_manager.as_ref() {
902                    Some(m) => m,
903                    None => {
904                        log::warn!("Hook manager not available, skipping hook '{}'", hook_cfg.name);
905                        continue;
906                    }
907                };
908                match mgr.load_file(
909                    hook_cfg.name.clone(),
910                    std::path::Path::new(&hook_cfg.path),
911                    hook_cfg.priority,
912                ) {
913                    Ok(program) => {
914                        driver.hook_slots[point_idx].attach(program);
915                        log::info!(
916                            "Loaded hook '{}' at point {} (priority {})",
917                            hook_cfg.name,
918                            hook_cfg.attach_point,
919                            hook_cfg.priority,
920                        );
921                    }
922                    Err(e) => {
923                        log::error!(
924                            "Failed to load hook '{}' from '{}': {}",
925                            hook_cfg.name,
926                            hook_cfg.path,
927                            e,
928                        );
929                    }
930                }
931            }
932        }
933
934        // Configure discovery
935        driver.discover_interfaces = config.discover_interfaces;
936        if let Some(val) = config.discovery_required_value {
937            driver.discovery_required_value = val;
938        }
939
940        // Shared counter for dynamic interface IDs
941        let next_dynamic_id = Arc::new(AtomicU64::new(10000));
942
943        // Collect discoverable interface configs for the announcer
944        let mut discoverable_interfaces = Vec::new();
945
946        // Start each interface
947        for iface_config in config.interfaces {
948            let iface_mode = iface_config.mode;
949            let ifac_cfg = iface_config.ifac;
950
951            // Collect discovery config before consuming ifac_cfg
952            if let Some(ref disc) = iface_config.discovery {
953                discoverable_interfaces.push(crate::discovery::DiscoverableInterface {
954                    config: disc.clone(),
955                    transport_enabled: config.transport_enabled,
956                    ifac_netname: ifac_cfg.as_ref().and_then(|ic| ic.netname.clone()),
957                    ifac_netkey: ifac_cfg.as_ref().and_then(|ic| ic.netkey.clone()),
958                });
959            }
960
961            // Derive IFAC state if configured
962            let mut ifac_state = ifac_cfg.as_ref().and_then(|ic| {
963                if ic.netname.is_some() || ic.netkey.is_some() {
964                    Some(ifac::derive_ifac(
965                        ic.netname.as_deref(),
966                        ic.netkey.as_deref(),
967                        ic.size,
968                    ))
969                } else {
970                    None
971                }
972            });
973
974            match iface_config.variant {
975                InterfaceVariant::TcpClient(tcp_config) => {
976                    let id = tcp_config.interface_id;
977                    let name = tcp_config.name.clone();
978                    let info = InterfaceInfo {
979                        id,
980                        name,
981                        mode: iface_mode,
982                        out_capable: true,
983                        in_capable: true,
984                        bitrate: None,
985                        announce_rate_target: None,
986                        announce_rate_grace: 0,
987                        announce_rate_penalty: 0.0,
988                        announce_cap: rns_core::constants::ANNOUNCE_CAP,
989                        is_local_client: false,
990                        wants_tunnel: false,
991                        tunnel_id: None,
992                        mtu: 65535,
993                        ingress_control: true,
994                        ia_freq: 0.0,
995                        started: time::now(),
996                    };
997
998                    let writer =
999                        crate::interface::tcp::start(tcp_config, tx.clone())?;
1000
1001                    driver.engine.register_interface(info.clone());
1002                    driver.interfaces.insert(
1003                        id,
1004                        InterfaceEntry {
1005                            id,
1006                            info,
1007                            writer,
1008                            online: false,
1009                            dynamic: false,
1010                            ifac: ifac_state,
1011                            stats: InterfaceStats {
1012                                started: time::now(),
1013                                ..Default::default()
1014                            },
1015                            interface_type: "TCPClientInterface".to_string(),
1016                        },
1017                    );
1018                }
1019                InterfaceVariant::TcpServer(server_config) => {
1020                    crate::interface::tcp_server::start(
1021                        server_config,
1022                        tx.clone(),
1023                        next_dynamic_id.clone(),
1024                    )?;
1025                    // Server itself doesn't register as an interface;
1026                    // per-client interfaces are registered dynamically via InterfaceUp
1027                }
1028                InterfaceVariant::Udp(udp_config) => {
1029                    let id = udp_config.interface_id;
1030                    let name = udp_config.name.clone();
1031                    let out_capable = udp_config.forward_ip.is_some();
1032                    let in_capable = udp_config.listen_ip.is_some();
1033
1034                    let writer = crate::interface::udp::start(udp_config, tx.clone())?;
1035
1036                    let info = InterfaceInfo {
1037                        id,
1038                        name,
1039                        mode: iface_mode,
1040                        out_capable,
1041                        in_capable,
1042                        bitrate: Some(10_000_000), // 10 Mbps guess (matches Python)
1043                        announce_rate_target: None,
1044                        announce_rate_grace: 0,
1045                        announce_rate_penalty: 0.0,
1046                        announce_cap: rns_core::constants::ANNOUNCE_CAP,
1047                        is_local_client: false,
1048                        wants_tunnel: false,
1049                        tunnel_id: None,
1050                        mtu: 1400,
1051                        ingress_control: true,
1052                        ia_freq: 0.0,
1053                        started: time::now(),
1054                    };
1055
1056                    driver.engine.register_interface(info.clone());
1057
1058                    if let Some(w) = writer {
1059                        driver.interfaces.insert(
1060                            id,
1061                            InterfaceEntry {
1062                                id,
1063                                info,
1064                                writer: w,
1065                                online: in_capable || out_capable,
1066                                dynamic: false,
1067                                ifac: ifac_state,
1068                                stats: InterfaceStats {
1069                                    started: time::now(),
1070                                    ..Default::default()
1071                                },
1072                                interface_type: "UDPInterface".to_string(),
1073                            },
1074                        );
1075                    }
1076                }
1077                InterfaceVariant::LocalServer(local_config) => {
1078                    crate::interface::local::start_server(
1079                        local_config,
1080                        tx.clone(),
1081                        next_dynamic_id.clone(),
1082                    )?;
1083                }
1084                InterfaceVariant::LocalClient(local_config) => {
1085                    let id = local_config.interface_id;
1086                    let name = local_config.name.clone();
1087                    let info = InterfaceInfo {
1088                        id,
1089                        name,
1090                        mode: iface_mode,
1091                        out_capable: true,
1092                        in_capable: true,
1093                        bitrate: Some(1_000_000_000),
1094                        announce_rate_target: None,
1095                        announce_rate_grace: 0,
1096                        announce_rate_penalty: 0.0,
1097                        announce_cap: rns_core::constants::ANNOUNCE_CAP,
1098                        is_local_client: false,
1099                        wants_tunnel: false,
1100                        tunnel_id: None,
1101                        mtu: 65535,
1102                        ingress_control: false,
1103                        ia_freq: 0.0,
1104                        started: time::now(),
1105                    };
1106
1107                    let writer =
1108                        crate::interface::local::start_client(local_config, tx.clone())?;
1109
1110                    driver.engine.register_interface(info.clone());
1111                    driver.interfaces.insert(
1112                        id,
1113                        InterfaceEntry {
1114                            id,
1115                            info,
1116                            writer,
1117                            online: false,
1118                            dynamic: false,
1119                            ifac: ifac_state,
1120                            stats: InterfaceStats {
1121                                started: time::now(),
1122                                ..Default::default()
1123                            },
1124                            interface_type: "LocalInterface".to_string(),
1125                        },
1126                    );
1127                }
1128                InterfaceVariant::Serial(serial_config) => {
1129                    let id = serial_config.interface_id;
1130                    let name = serial_config.name.clone();
1131                    let bitrate = serial_config.speed;
1132                    let info = InterfaceInfo {
1133                        id,
1134                        name,
1135                        mode: iface_mode,
1136                        out_capable: true,
1137                        in_capable: true,
1138                        bitrate: Some(bitrate as u64),
1139                        announce_rate_target: None,
1140                        announce_rate_grace: 0,
1141                        announce_rate_penalty: 0.0,
1142                        announce_cap: rns_core::constants::ANNOUNCE_CAP,
1143                        is_local_client: false,
1144                        wants_tunnel: false,
1145                        tunnel_id: None,
1146                        mtu: rns_core::constants::MTU as u32,
1147                        ingress_control: false,
1148                        ia_freq: 0.0,
1149                        started: time::now(),
1150                    };
1151
1152                    let writer =
1153                        crate::interface::serial_iface::start(serial_config, tx.clone())?;
1154
1155                    driver.engine.register_interface(info.clone());
1156                    driver.interfaces.insert(
1157                        id,
1158                        InterfaceEntry {
1159                            id,
1160                            info,
1161                            writer,
1162                            online: false,
1163                            dynamic: false,
1164                            ifac: ifac_state,
1165                            stats: InterfaceStats {
1166                                started: time::now(),
1167                                ..Default::default()
1168                            },
1169                            interface_type: "SerialInterface".to_string(),
1170                        },
1171                    );
1172                }
1173                InterfaceVariant::Kiss(kiss_config) => {
1174                    let id = kiss_config.interface_id;
1175                    let name = kiss_config.name.clone();
1176                    let info = InterfaceInfo {
1177                        id,
1178                        name,
1179                        mode: iface_mode,
1180                        out_capable: true,
1181                        in_capable: true,
1182                        bitrate: Some(1200), // BITRATE_GUESS from Python
1183                        announce_rate_target: None,
1184                        announce_rate_grace: 0,
1185                        announce_rate_penalty: 0.0,
1186                        announce_cap: rns_core::constants::ANNOUNCE_CAP,
1187                        is_local_client: false,
1188                        wants_tunnel: false,
1189                        tunnel_id: None,
1190                        mtu: rns_core::constants::MTU as u32,
1191                        ingress_control: false,
1192                        ia_freq: 0.0,
1193                        started: time::now(),
1194                    };
1195
1196                    let writer =
1197                        crate::interface::kiss_iface::start(kiss_config, tx.clone())?;
1198
1199                    driver.engine.register_interface(info.clone());
1200                    driver.interfaces.insert(
1201                        id,
1202                        InterfaceEntry {
1203                            id,
1204                            info,
1205                            writer,
1206                            online: false,
1207                            dynamic: false,
1208                            ifac: ifac_state,
1209                            stats: InterfaceStats {
1210                                started: time::now(),
1211                                ..Default::default()
1212                            },
1213                            interface_type: "KISSInterface".to_string(),
1214                        },
1215                    );
1216                }
1217                InterfaceVariant::Pipe(pipe_config) => {
1218                    let id = pipe_config.interface_id;
1219                    let name = pipe_config.name.clone();
1220                    let info = InterfaceInfo {
1221                        id,
1222                        name,
1223                        mode: iface_mode,
1224                        out_capable: true,
1225                        in_capable: true,
1226                        bitrate: Some(1_000_000), // 1 Mbps guess
1227                        announce_rate_target: None,
1228                        announce_rate_grace: 0,
1229                        announce_rate_penalty: 0.0,
1230                        announce_cap: rns_core::constants::ANNOUNCE_CAP,
1231                        is_local_client: false,
1232                        wants_tunnel: false,
1233                        tunnel_id: None,
1234                        mtu: rns_core::constants::MTU as u32,
1235                        ingress_control: false,
1236                        ia_freq: 0.0,
1237                        started: time::now(),
1238                    };
1239
1240                    let writer =
1241                        crate::interface::pipe::start(pipe_config, tx.clone())?;
1242
1243                    driver.engine.register_interface(info.clone());
1244                    driver.interfaces.insert(
1245                        id,
1246                        InterfaceEntry {
1247                            id,
1248                            info,
1249                            writer,
1250                            online: false,
1251                            dynamic: false,
1252                            ifac: ifac_state,
1253                            stats: InterfaceStats {
1254                                started: time::now(),
1255                                ..Default::default()
1256                            },
1257                            interface_type: "PipeInterface".to_string(),
1258                        },
1259                    );
1260                }
1261                InterfaceVariant::RNode(rnode_config) => {
1262                    let name = rnode_config.name.clone();
1263                    let sub_writers =
1264                        crate::interface::rnode::start(rnode_config, tx.clone())?;
1265
1266                    // For multi-subinterface RNodes, we need an IfacState per sub.
1267                    // Re-derive from the original config for each beyond the first.
1268                    let mut first = true;
1269                    let mut sub_index = 0u32;
1270                    for (sub_id, writer) in sub_writers {
1271                        let sub_name = if sub_index == 0 {
1272                            name.clone()
1273                        } else {
1274                            format!("{}/{}", name, sub_index)
1275                        };
1276                        sub_index += 1;
1277
1278                        let info = InterfaceInfo {
1279                            id: sub_id,
1280                            name: sub_name,
1281                            mode: iface_mode,
1282                            out_capable: true,
1283                            in_capable: true,
1284                            bitrate: None, // LoRa bitrate depends on SF/BW, set dynamically
1285                            announce_rate_target: None,
1286                            announce_rate_grace: 0,
1287                            announce_rate_penalty: 0.0,
1288                        announce_cap: rns_core::constants::ANNOUNCE_CAP,
1289                        is_local_client: false,
1290                        wants_tunnel: false,
1291                        tunnel_id: None,
1292                        mtu: rns_core::constants::MTU as u32,
1293                        ingress_control: false,
1294                        ia_freq: 0.0,
1295                        started: time::now(),
1296                        };
1297
1298                        let sub_ifac = if first {
1299                            first = false;
1300                            ifac_state.take()
1301                        } else if let Some(ref ic) = ifac_cfg {
1302                            Some(ifac::derive_ifac(
1303                                ic.netname.as_deref(),
1304                                ic.netkey.as_deref(),
1305                                ic.size,
1306                            ))
1307                        } else {
1308                            None
1309                        };
1310
1311                        driver.engine.register_interface(info.clone());
1312                        driver.interfaces.insert(
1313                            sub_id,
1314                            InterfaceEntry {
1315                                id: sub_id,
1316                                info,
1317                                writer,
1318                                online: false,
1319                                dynamic: false,
1320                                ifac: sub_ifac,
1321                                stats: InterfaceStats {
1322                                    started: time::now(),
1323                                    ..Default::default()
1324                                },
1325                                interface_type: "RNodeInterface".to_string(),
1326                            },
1327                        );
1328                    }
1329
1330                }
1331                InterfaceVariant::Backbone(backbone_config) => {
1332                    crate::interface::backbone::start(
1333                        backbone_config,
1334                        tx.clone(),
1335                        next_dynamic_id.clone(),
1336                    )?;
1337                    // Like TcpServer/LocalServer, backbone itself doesn't register
1338                    // as an interface; per-client interfaces are registered via InterfaceUp
1339                }
1340                InterfaceVariant::BackboneClient(config) => {
1341                    let id = config.interface_id;
1342                    let name = config.name.clone();
1343                    let info = InterfaceInfo {
1344                        id,
1345                        name,
1346                        mode: iface_mode,
1347                        out_capable: true,
1348                        in_capable: true,
1349                        bitrate: Some(1_000_000_000),
1350                        announce_rate_target: None,
1351                        announce_rate_grace: 0,
1352                        announce_rate_penalty: 0.0,
1353                        announce_cap: rns_core::constants::ANNOUNCE_CAP,
1354                        is_local_client: false,
1355                        wants_tunnel: false,
1356                        tunnel_id: None,
1357                        mtu: 65535,
1358                        ingress_control: true,
1359                        ia_freq: 0.0,
1360                        started: time::now(),
1361                    };
1362
1363                    let writer =
1364                        crate::interface::backbone::start_client(config, tx.clone())?;
1365
1366                    driver.engine.register_interface(info.clone());
1367                    driver.interfaces.insert(
1368                        id,
1369                        InterfaceEntry {
1370                            id,
1371                            info,
1372                            writer,
1373                            online: false,
1374                            dynamic: false,
1375                            ifac: ifac_state,
1376                            stats: InterfaceStats {
1377                                started: time::now(),
1378                                ..Default::default()
1379                            },
1380                            interface_type: "BackboneInterface".to_string(),
1381                        },
1382                    );
1383                }
1384                InterfaceVariant::Auto(auto_config) => {
1385                    crate::interface::auto::start(
1386                        auto_config,
1387                        tx.clone(),
1388                        next_dynamic_id.clone(),
1389                    )?;
1390                    // Like TcpServer, AutoInterface doesn't register itself;
1391                    // per-peer interfaces are registered dynamically via InterfaceUp
1392                }
1393                InterfaceVariant::I2p(i2p_config) => {
1394                    crate::interface::i2p::start(
1395                        i2p_config,
1396                        tx.clone(),
1397                        next_dynamic_id.clone(),
1398                    )?;
1399                    // Like TcpServer, I2P doesn't register itself;
1400                    // per-peer interfaces are registered dynamically via InterfaceUp
1401                }
1402            }
1403        }
1404
1405        // Set up interface announcer if we have discoverable interfaces
1406        if !discoverable_interfaces.is_empty() {
1407            let transport_id = *identity.hash();
1408            let announcer = crate::discovery::InterfaceAnnouncer::new(
1409                transport_id,
1410                discoverable_interfaces,
1411            );
1412            log::info!("Interface discovery announcer initialized");
1413            driver.interface_announcer = Some(announcer);
1414        }
1415
1416        // Set up discovered interfaces storage path
1417        if let Some(ref cache_dir) = config.cache_dir {
1418            let disc_path = std::path::PathBuf::from(cache_dir)
1419                .parent()
1420                .unwrap_or(std::path::Path::new("."))
1421                .join("storage")
1422                .join("discovery")
1423                .join("interfaces");
1424            let _ = std::fs::create_dir_all(&disc_path);
1425            driver.discovered_interfaces = crate::discovery::DiscoveredInterfaceStorage::new(disc_path);
1426        }
1427
1428        // Set up management destinations if enabled
1429        if config.management.enable_remote_management {
1430            if let Some(prv_key) = identity.get_private_key() {
1431                let identity_hash = *identity.hash();
1432                let mgmt_dest = crate::management::management_dest_hash(&identity_hash);
1433
1434                // Extract Ed25519 signing keys from the identity
1435                let sig_prv = rns_crypto::ed25519::Ed25519PrivateKey::from_bytes(
1436                    &prv_key[32..64].try_into().unwrap(),
1437                );
1438                let sig_pub_bytes: [u8; 32] = identity
1439                    .get_public_key()
1440                    .unwrap()[32..64]
1441                    .try_into()
1442                    .unwrap();
1443
1444                // Register as SINGLE destination in transport engine
1445                driver.engine.register_destination(
1446                    mgmt_dest,
1447                    rns_core::constants::DESTINATION_SINGLE,
1448                );
1449                driver.local_destinations.insert(
1450                    mgmt_dest,
1451                    rns_core::constants::DESTINATION_SINGLE,
1452                );
1453
1454                // Register as link destination in link manager
1455                driver.link_manager.register_link_destination(
1456                    mgmt_dest,
1457                    sig_prv,
1458                    sig_pub_bytes,
1459                );
1460
1461                // Register management path hashes
1462                driver.link_manager.register_management_path(
1463                    crate::management::status_path_hash(),
1464                );
1465                driver.link_manager.register_management_path(
1466                    crate::management::path_path_hash(),
1467                );
1468
1469                log::info!(
1470                    "Remote management enabled on {:02x?}",
1471                    &mgmt_dest[..4],
1472                );
1473
1474                // Set up allowed list
1475                if !config.management.remote_management_allowed.is_empty() {
1476                    log::info!(
1477                        "Remote management allowed for {} identities",
1478                        config.management.remote_management_allowed.len(),
1479                    );
1480                }
1481            }
1482        }
1483
1484        if config.management.publish_blackhole {
1485            if let Some(prv_key) = identity.get_private_key() {
1486                let identity_hash = *identity.hash();
1487                let bh_dest = crate::management::blackhole_dest_hash(&identity_hash);
1488
1489                let sig_prv = rns_crypto::ed25519::Ed25519PrivateKey::from_bytes(
1490                    &prv_key[32..64].try_into().unwrap(),
1491                );
1492                let sig_pub_bytes: [u8; 32] = identity
1493                    .get_public_key()
1494                    .unwrap()[32..64]
1495                    .try_into()
1496                    .unwrap();
1497
1498                driver.engine.register_destination(
1499                    bh_dest,
1500                    rns_core::constants::DESTINATION_SINGLE,
1501                );
1502                driver.link_manager.register_link_destination(
1503                    bh_dest,
1504                    sig_prv,
1505                    sig_pub_bytes,
1506                );
1507                driver.link_manager.register_management_path(
1508                    crate::management::list_path_hash(),
1509                );
1510
1511                log::info!(
1512                    "Blackhole list publishing enabled on {:02x?}",
1513                    &bh_dest[..4],
1514                );
1515            }
1516        }
1517
1518        // Spawn timer thread with configurable tick interval
1519        let tick_interval_ms = Arc::new(AtomicU64::new(1000));
1520        let timer_tx = tx.clone();
1521        let timer_interval = Arc::clone(&tick_interval_ms);
1522        thread::Builder::new()
1523            .name("rns-timer".into())
1524            .spawn(move || {
1525                loop {
1526                    let ms = timer_interval.load(Ordering::Relaxed);
1527                    thread::sleep(Duration::from_millis(ms));
1528                    if timer_tx.send(Event::Tick).is_err() {
1529                        break; // receiver dropped
1530                    }
1531                }
1532            })?;
1533
1534        // Start LocalServer for shared instance clients if share_instance is enabled
1535        if config.share_instance {
1536            let local_server_config = LocalServerConfig {
1537                instance_name: config.instance_name.clone(),
1538                port: config.shared_instance_port,
1539                interface_id: rns_core::transport::types::InterfaceId(0), // Not used for server
1540            };
1541            match crate::interface::local::start_server(
1542                local_server_config,
1543                tx.clone(),
1544                next_dynamic_id.clone(),
1545            ) {
1546                Ok(()) => {
1547                    log::info!(
1548                        "Local shared instance server started (instance={}, port={})",
1549                        config.instance_name,
1550                        config.shared_instance_port
1551                    );
1552                }
1553                Err(e) => {
1554                    log::error!("Failed to start local shared instance server: {}", e);
1555                }
1556            }
1557        }
1558
1559        // Start RPC server if share_instance is enabled
1560        let rpc_server = if config.share_instance {
1561            let auth_key = crate::rpc::derive_auth_key(
1562                &identity.get_private_key().unwrap_or([0u8; 64]),
1563            );
1564            let rpc_addr = crate::rpc::RpcAddr::Tcp("127.0.0.1".into(), config.rpc_port);
1565            match crate::rpc::RpcServer::start(&rpc_addr, auth_key, tx.clone()) {
1566                Ok(server) => {
1567                    log::info!("RPC server started on 127.0.0.1:{}", config.rpc_port);
1568                    Some(server)
1569                }
1570                Err(e) => {
1571                    log::error!("Failed to start RPC server: {}", e);
1572                    None
1573                }
1574            }
1575        } else {
1576            None
1577        };
1578
1579        // Spawn driver thread
1580        let driver_handle = thread::Builder::new()
1581            .name("rns-driver".into())
1582            .spawn(move || {
1583                driver.run();
1584            })?;
1585
1586        Ok(RnsNode {
1587            tx,
1588            driver_handle: Some(driver_handle),
1589            rpc_server,
1590            tick_interval_ms,
1591            probe_server,
1592        })
1593    }
1594
1595    /// Query the driver for state information.
1596    pub fn query(&self, request: QueryRequest) -> Result<QueryResponse, SendError> {
1597        let (resp_tx, resp_rx) = std::sync::mpsc::channel();
1598        self.tx
1599            .send(Event::Query(request, resp_tx))
1600            .map_err(|_| SendError)?;
1601        resp_rx.recv().map_err(|_| SendError)
1602    }
1603
1604    /// Send a raw outbound packet.
1605    pub fn send_raw(
1606        &self,
1607        raw: Vec<u8>,
1608        dest_type: u8,
1609        attached_interface: Option<rns_core::transport::types::InterfaceId>,
1610    ) -> Result<(), SendError> {
1611        self.tx
1612            .send(Event::SendOutbound {
1613                raw,
1614                dest_type,
1615                attached_interface,
1616            })
1617            .map_err(|_| SendError)
1618    }
1619
1620    /// Register a local destination with the transport engine.
1621    pub fn register_destination(
1622        &self,
1623        dest_hash: [u8; 16],
1624        dest_type: u8,
1625    ) -> Result<(), SendError> {
1626        self.tx
1627            .send(Event::RegisterDestination { dest_hash, dest_type })
1628            .map_err(|_| SendError)
1629    }
1630
1631    /// Deregister a local destination.
1632    pub fn deregister_destination(&self, dest_hash: [u8; 16]) -> Result<(), SendError> {
1633        self.tx
1634            .send(Event::DeregisterDestination { dest_hash })
1635            .map_err(|_| SendError)
1636    }
1637
1638    /// Deregister a link destination (stop accepting incoming links).
1639    pub fn deregister_link_destination(&self, dest_hash: [u8; 16]) -> Result<(), SendError> {
1640        self.tx
1641            .send(Event::DeregisterLinkDestination { dest_hash })
1642            .map_err(|_| SendError)
1643    }
1644
1645    /// Register a link destination that can accept incoming links.
1646    ///
1647    /// `dest_hash`: the destination hash
1648    /// `sig_prv_bytes`: Ed25519 private signing key (32 bytes)
1649    /// `sig_pub_bytes`: Ed25519 public signing key (32 bytes)
1650    pub fn register_link_destination(
1651        &self,
1652        dest_hash: [u8; 16],
1653        sig_prv_bytes: [u8; 32],
1654        sig_pub_bytes: [u8; 32],
1655    ) -> Result<(), SendError> {
1656        self.tx
1657            .send(Event::RegisterLinkDestination {
1658                dest_hash,
1659                sig_prv_bytes,
1660                sig_pub_bytes,
1661            })
1662            .map_err(|_| SendError)
1663    }
1664
1665    /// Register a request handler for a given path on established links.
1666    pub fn register_request_handler<F>(
1667        &self,
1668        path: &str,
1669        allowed_list: Option<Vec<[u8; 16]>>,
1670        handler: F,
1671    ) -> Result<(), SendError>
1672    where
1673        F: Fn([u8; 16], &str, &[u8], Option<&([u8; 16], [u8; 64])>) -> Option<Vec<u8>> + Send + 'static,
1674    {
1675        self.tx
1676            .send(Event::RegisterRequestHandler {
1677                path: path.to_string(),
1678                allowed_list,
1679                handler: Box::new(handler),
1680            })
1681            .map_err(|_| SendError)
1682    }
1683
1684    /// Create an outbound link to a destination.
1685    ///
1686    /// Returns the link_id on success.
1687    pub fn create_link(
1688        &self,
1689        dest_hash: [u8; 16],
1690        dest_sig_pub_bytes: [u8; 32],
1691    ) -> Result<[u8; 16], SendError> {
1692        let (response_tx, response_rx) = std::sync::mpsc::channel();
1693        self.tx
1694            .send(Event::CreateLink {
1695                dest_hash,
1696                dest_sig_pub_bytes,
1697                response_tx,
1698            })
1699            .map_err(|_| SendError)?;
1700        response_rx.recv().map_err(|_| SendError)
1701    }
1702
1703    /// Send a request on an established link.
1704    pub fn send_request(
1705        &self,
1706        link_id: [u8; 16],
1707        path: &str,
1708        data: &[u8],
1709    ) -> Result<(), SendError> {
1710        self.tx
1711            .send(Event::SendRequest {
1712                link_id,
1713                path: path.to_string(),
1714                data: data.to_vec(),
1715            })
1716            .map_err(|_| SendError)
1717    }
1718
1719    /// Identify on a link (reveal identity to remote peer).
1720    pub fn identify_on_link(
1721        &self,
1722        link_id: [u8; 16],
1723        identity_prv_key: [u8; 64],
1724    ) -> Result<(), SendError> {
1725        self.tx
1726            .send(Event::IdentifyOnLink {
1727                link_id,
1728                identity_prv_key,
1729            })
1730            .map_err(|_| SendError)
1731    }
1732
1733    /// Tear down a link.
1734    pub fn teardown_link(&self, link_id: [u8; 16]) -> Result<(), SendError> {
1735        self.tx
1736            .send(Event::TeardownLink { link_id })
1737            .map_err(|_| SendError)
1738    }
1739
1740    /// Send a resource on an established link.
1741    pub fn send_resource(
1742        &self,
1743        link_id: [u8; 16],
1744        data: Vec<u8>,
1745        metadata: Option<Vec<u8>>,
1746    ) -> Result<(), SendError> {
1747        self.tx
1748            .send(Event::SendResource { link_id, data, metadata })
1749            .map_err(|_| SendError)
1750    }
1751
1752    /// Set the resource acceptance strategy for a link.
1753    ///
1754    /// 0 = AcceptNone, 1 = AcceptAll, 2 = AcceptApp
1755    pub fn set_resource_strategy(
1756        &self,
1757        link_id: [u8; 16],
1758        strategy: u8,
1759    ) -> Result<(), SendError> {
1760        self.tx
1761            .send(Event::SetResourceStrategy { link_id, strategy })
1762            .map_err(|_| SendError)
1763    }
1764
1765    /// Accept or reject a pending resource (for AcceptApp strategy).
1766    pub fn accept_resource(
1767        &self,
1768        link_id: [u8; 16],
1769        resource_hash: Vec<u8>,
1770        accept: bool,
1771    ) -> Result<(), SendError> {
1772        self.tx
1773            .send(Event::AcceptResource { link_id, resource_hash, accept })
1774            .map_err(|_| SendError)
1775    }
1776
1777    /// Send a channel message on a link.
1778    pub fn send_channel_message(
1779        &self,
1780        link_id: [u8; 16],
1781        msgtype: u16,
1782        payload: Vec<u8>,
1783    ) -> Result<(), SendError> {
1784        self.tx
1785            .send(Event::SendChannelMessage { link_id, msgtype, payload })
1786            .map_err(|_| SendError)
1787    }
1788
1789    /// Propose a direct P2P connection to a peer via NAT hole punching.
1790    ///
1791    /// The link must be active and connected through a backbone node.
1792    /// If successful, a direct UDP connection will be established, bypassing the backbone.
1793    pub fn propose_direct_connect(&self, link_id: [u8; 16]) -> Result<(), SendError> {
1794        self.tx
1795            .send(Event::ProposeDirectConnect { link_id })
1796            .map_err(|_| SendError)
1797    }
1798
1799    /// Set the policy for handling incoming direct-connect proposals.
1800    pub fn set_direct_connect_policy(
1801        &self,
1802        policy: crate::holepunch::orchestrator::HolePunchPolicy,
1803    ) -> Result<(), SendError> {
1804        self.tx
1805            .send(Event::SetDirectConnectPolicy { policy })
1806            .map_err(|_| SendError)
1807    }
1808
1809    /// Send data on a link with a given context.
1810    pub fn send_on_link(
1811        &self,
1812        link_id: [u8; 16],
1813        data: Vec<u8>,
1814        context: u8,
1815    ) -> Result<(), SendError> {
1816        self.tx
1817            .send(Event::SendOnLink { link_id, data, context })
1818            .map_err(|_| SendError)
1819    }
1820
1821    /// Build and broadcast an announce for a destination.
1822    ///
1823    /// The identity is used to sign the announce. Must be the identity that
1824    /// owns the destination (i.e. `identity.hash()` matches `dest.identity_hash`).
1825    pub fn announce(
1826        &self,
1827        dest: &crate::destination::Destination,
1828        identity: &Identity,
1829        app_data: Option<&[u8]>,
1830    ) -> Result<(), SendError> {
1831        let name_hash = rns_core::destination::name_hash(
1832            &dest.app_name,
1833            &dest.aspects.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
1834        );
1835
1836        let mut random_hash = [0u8; 10];
1837        OsRng.fill_bytes(&mut random_hash);
1838
1839        let (announce_data, _has_ratchet) = rns_core::announce::AnnounceData::pack(
1840            identity,
1841            &dest.hash.0,
1842            &name_hash,
1843            &random_hash,
1844            None, // no ratchet
1845            app_data,
1846        ).map_err(|_| SendError)?;
1847
1848        let context_flag = rns_core::constants::FLAG_UNSET;
1849
1850        let flags = rns_core::packet::PacketFlags {
1851            header_type: rns_core::constants::HEADER_1,
1852            context_flag,
1853            transport_type: rns_core::constants::TRANSPORT_BROADCAST,
1854            destination_type: rns_core::constants::DESTINATION_SINGLE,
1855            packet_type: rns_core::constants::PACKET_TYPE_ANNOUNCE,
1856        };
1857
1858        let packet = rns_core::packet::RawPacket::pack(
1859            flags, 0, &dest.hash.0, None,
1860            rns_core::constants::CONTEXT_NONE, &announce_data,
1861        ).map_err(|_| SendError)?;
1862
1863        self.send_raw(
1864            packet.raw,
1865            dest.dest_type.to_wire_constant(),
1866            None,
1867        )
1868    }
1869
1870    /// Send an encrypted (SINGLE) or plaintext (PLAIN) packet to a destination.
1871    ///
1872    /// For SINGLE destinations, `dest.public_key` must be set (OUT direction).
1873    /// Returns the packet hash for proof tracking.
1874    pub fn send_packet(
1875        &self,
1876        dest: &crate::destination::Destination,
1877        data: &[u8],
1878    ) -> Result<rns_core::types::PacketHash, SendError> {
1879        use rns_core::types::DestinationType;
1880
1881        let payload = match dest.dest_type {
1882            DestinationType::Single => {
1883                let pub_key = dest.public_key.ok_or(SendError)?;
1884                let remote_id = rns_crypto::identity::Identity::from_public_key(&pub_key);
1885                remote_id.encrypt(data, &mut OsRng).map_err(|_| SendError)?
1886            }
1887            DestinationType::Plain => data.to_vec(),
1888            DestinationType::Group => {
1889                dest.encrypt(data).map_err(|_| SendError)?
1890            }
1891        };
1892
1893        let flags = rns_core::packet::PacketFlags {
1894            header_type: rns_core::constants::HEADER_1,
1895            context_flag: rns_core::constants::FLAG_UNSET,
1896            transport_type: rns_core::constants::TRANSPORT_BROADCAST,
1897            destination_type: dest.dest_type.to_wire_constant(),
1898            packet_type: rns_core::constants::PACKET_TYPE_DATA,
1899        };
1900
1901        let packet = rns_core::packet::RawPacket::pack(
1902            flags, 0, &dest.hash.0, None,
1903            rns_core::constants::CONTEXT_NONE, &payload,
1904        ).map_err(|_| SendError)?;
1905
1906        let packet_hash = rns_core::types::PacketHash(packet.packet_hash);
1907
1908        self.tx
1909            .send(Event::SendOutbound {
1910                raw: packet.raw,
1911                dest_type: dest.dest_type.to_wire_constant(),
1912                attached_interface: None,
1913            })
1914            .map_err(|_| SendError)?;
1915
1916        Ok(packet_hash)
1917    }
1918
1919    /// Register a destination with the transport engine and set its proof strategy.
1920    ///
1921    /// `signing_key` is the full 64-byte identity private key (X25519 32 bytes +
1922    /// Ed25519 32 bytes), needed for ProveAll/ProveApp to sign proof packets.
1923    pub fn register_destination_with_proof(
1924        &self,
1925        dest: &crate::destination::Destination,
1926        signing_key: Option<[u8; 64]>,
1927    ) -> Result<(), SendError> {
1928        // Register with transport engine
1929        self.register_destination(dest.hash.0, dest.dest_type.to_wire_constant())?;
1930
1931        // Register proof strategy if not ProveNone
1932        if dest.proof_strategy != rns_core::types::ProofStrategy::ProveNone {
1933            self.tx
1934                .send(Event::RegisterProofStrategy {
1935                    dest_hash: dest.hash.0,
1936                    strategy: dest.proof_strategy,
1937                    signing_key,
1938                })
1939                .map_err(|_| SendError)?;
1940        }
1941
1942        Ok(())
1943    }
1944
1945    /// Request a path to a destination from the network.
1946    pub fn request_path(&self, dest_hash: &rns_core::types::DestHash) -> Result<(), SendError> {
1947        self.tx
1948            .send(Event::RequestPath { dest_hash: dest_hash.0 })
1949            .map_err(|_| SendError)
1950    }
1951
1952    /// Check if a path exists to a destination (synchronous query).
1953    pub fn has_path(&self, dest_hash: &rns_core::types::DestHash) -> Result<bool, SendError> {
1954        match self.query(QueryRequest::HasPath { dest_hash: dest_hash.0 })? {
1955            QueryResponse::HasPath(v) => Ok(v),
1956            _ => Ok(false),
1957        }
1958    }
1959
1960    /// Get hop count to a destination (synchronous query).
1961    pub fn hops_to(&self, dest_hash: &rns_core::types::DestHash) -> Result<Option<u8>, SendError> {
1962        match self.query(QueryRequest::HopsTo { dest_hash: dest_hash.0 })? {
1963            QueryResponse::HopsTo(v) => Ok(v),
1964            _ => Ok(None),
1965        }
1966    }
1967
1968    /// Recall the identity information for a previously announced destination.
1969    pub fn recall_identity(
1970        &self,
1971        dest_hash: &rns_core::types::DestHash,
1972    ) -> Result<Option<crate::destination::AnnouncedIdentity>, SendError> {
1973        match self.query(QueryRequest::RecallIdentity { dest_hash: dest_hash.0 })? {
1974            QueryResponse::RecallIdentity(v) => Ok(v),
1975            _ => Ok(None),
1976        }
1977    }
1978
1979    /// Load a WASM hook at runtime.
1980    pub fn load_hook(
1981        &self,
1982        name: String,
1983        wasm_bytes: Vec<u8>,
1984        attach_point: String,
1985        priority: i32,
1986    ) -> Result<Result<(), String>, SendError> {
1987        let (response_tx, response_rx) = std::sync::mpsc::channel();
1988        self.tx
1989            .send(Event::LoadHook {
1990                name,
1991                wasm_bytes,
1992                attach_point,
1993                priority,
1994                response_tx,
1995            })
1996            .map_err(|_| SendError)?;
1997        response_rx.recv().map_err(|_| SendError)
1998    }
1999
2000    /// Unload a WASM hook at runtime.
2001    pub fn unload_hook(
2002        &self,
2003        name: String,
2004        attach_point: String,
2005    ) -> Result<Result<(), String>, SendError> {
2006        let (response_tx, response_rx) = std::sync::mpsc::channel();
2007        self.tx
2008            .send(Event::UnloadHook {
2009                name,
2010                attach_point,
2011                response_tx,
2012            })
2013            .map_err(|_| SendError)?;
2014        response_rx.recv().map_err(|_| SendError)
2015    }
2016
2017    /// Reload a WASM hook at runtime (detach + recompile + reattach with same priority).
2018    pub fn reload_hook(
2019        &self,
2020        name: String,
2021        attach_point: String,
2022        wasm_bytes: Vec<u8>,
2023    ) -> Result<Result<(), String>, SendError> {
2024        let (response_tx, response_rx) = std::sync::mpsc::channel();
2025        self.tx
2026            .send(Event::ReloadHook {
2027                name,
2028                attach_point,
2029                wasm_bytes,
2030                response_tx,
2031            })
2032            .map_err(|_| SendError)?;
2033        response_rx.recv().map_err(|_| SendError)
2034    }
2035
2036    /// List all loaded hooks.
2037    pub fn list_hooks(&self) -> Result<Vec<crate::event::HookInfo>, SendError> {
2038        let (response_tx, response_rx) = std::sync::mpsc::channel();
2039        self.tx
2040            .send(Event::ListHooks { response_tx })
2041            .map_err(|_| SendError)?;
2042        response_rx.recv().map_err(|_| SendError)
2043    }
2044
2045    /// Construct an RnsNode from its constituent parts.
2046    /// Used by `shared_client` to build a client-mode node.
2047    pub(crate) fn from_parts(
2048        tx: EventSender,
2049        driver_handle: thread::JoinHandle<()>,
2050        rpc_server: Option<crate::rpc::RpcServer>,
2051        tick_interval_ms: Arc<AtomicU64>,
2052    ) -> Self {
2053        RnsNode {
2054            tx,
2055            driver_handle: Some(driver_handle),
2056            rpc_server,
2057            tick_interval_ms,
2058            probe_server: None,
2059        }
2060    }
2061
2062    /// Get the event sender for direct event injection.
2063    pub fn event_sender(&self) -> &EventSender {
2064        &self.tx
2065    }
2066
2067    /// Set the tick interval in milliseconds.
2068    /// Default is 1000 (1 second). Changes take effect on the next tick cycle.
2069    /// Values are clamped to the range 100..=10000.
2070    /// Returns the actual stored value (which may differ from `ms` if clamped).
2071    pub fn set_tick_interval(&self, ms: u64) -> u64 {
2072        let clamped = ms.clamp(100, 10_000);
2073        if clamped != ms {
2074            log::warn!(
2075                "tick interval {}ms out of range, clamped to {}ms",
2076                ms,
2077                clamped
2078            );
2079        }
2080        self.tick_interval_ms.store(clamped, Ordering::Relaxed);
2081        clamped
2082    }
2083
2084    /// Get the current tick interval in milliseconds.
2085    pub fn tick_interval(&self) -> u64 {
2086        self.tick_interval_ms.load(Ordering::Relaxed)
2087    }
2088
2089    /// Shut down the node. Blocks until the driver thread exits.
2090    pub fn shutdown(mut self) {
2091        // Stop RPC server first
2092        if let Some(mut rpc) = self.rpc_server.take() {
2093            rpc.stop();
2094        }
2095        let _ = self.tx.send(Event::Shutdown);
2096        if let Some(handle) = self.driver_handle.take() {
2097            let _ = handle.join();
2098        }
2099    }
2100}
2101
2102#[cfg(test)]
2103mod tests {
2104    use super::*;
2105    use std::fs;
2106
2107    struct NoopCallbacks;
2108
2109    impl Callbacks for NoopCallbacks {
2110        fn on_announce(&mut self, _: crate::destination::AnnouncedIdentity) {}
2111        fn on_path_updated(&mut self, _: rns_core::types::DestHash, _: u8) {}
2112        fn on_local_delivery(&mut self, _: rns_core::types::DestHash, _: Vec<u8>, _: rns_core::types::PacketHash) {}
2113    }
2114
2115    #[test]
2116    fn start_and_shutdown() {
2117        let node = RnsNode::start(
2118            NodeConfig {
2119                transport_enabled: false,
2120                identity: None,
2121                interfaces: vec![],
2122                share_instance: false,
2123                instance_name: "default".into(),
2124                shared_instance_port: 37428,
2125                rpc_port: 0,
2126                cache_dir: None,
2127                management: Default::default(),
2128                probe_port: None,
2129                probe_addr: None,
2130                device: None,
2131                hooks: Vec::new(),
2132                discover_interfaces: false,
2133                discovery_required_value: None,
2134            },
2135            Box::new(NoopCallbacks),
2136        )
2137        .unwrap();
2138        node.shutdown();
2139    }
2140
2141    #[test]
2142    fn start_with_identity() {
2143        let identity = Identity::new(&mut OsRng);
2144        let hash = *identity.hash();
2145        let node = RnsNode::start(
2146            NodeConfig {
2147                transport_enabled: true,
2148                identity: Some(identity),
2149                interfaces: vec![],
2150                share_instance: false,
2151                instance_name: "default".into(),
2152                shared_instance_port: 37428,
2153                rpc_port: 0,
2154                cache_dir: None,
2155                management: Default::default(),
2156                probe_port: None,
2157                probe_addr: None,
2158                device: None,
2159                hooks: Vec::new(),
2160                discover_interfaces: false,
2161                discovery_required_value: None,
2162            },
2163            Box::new(NoopCallbacks),
2164        )
2165        .unwrap();
2166        // The identity hash should have been used
2167        let _ = hash;
2168        node.shutdown();
2169    }
2170
2171    #[test]
2172    fn start_generates_identity() {
2173        let node = RnsNode::start(
2174            NodeConfig {
2175                transport_enabled: false,
2176                identity: None,
2177                interfaces: vec![],
2178                share_instance: false,
2179                instance_name: "default".into(),
2180                shared_instance_port: 37428,
2181                rpc_port: 0,
2182                cache_dir: None,
2183                management: Default::default(),
2184                probe_port: None,
2185                probe_addr: None,
2186                device: None,
2187                hooks: Vec::new(),
2188                discover_interfaces: false,
2189                discovery_required_value: None,
2190            },
2191            Box::new(NoopCallbacks),
2192        )
2193        .unwrap();
2194        // Should not panic - identity was auto-generated
2195        node.shutdown();
2196    }
2197
2198    #[test]
2199    fn from_config_creates_identity() {
2200        let dir = std::env::temp_dir().join(format!("rns-test-fc-{}", std::process::id()));
2201        let _ = fs::remove_dir_all(&dir);
2202        fs::create_dir_all(&dir).unwrap();
2203
2204        // Write a minimal config file
2205        fs::write(
2206            dir.join("config"),
2207            "[reticulum]\nenable_transport = False\n",
2208        )
2209        .unwrap();
2210
2211        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks)).unwrap();
2212
2213        // Identity file should have been created
2214        assert!(dir.join("storage/identities/identity").exists());
2215
2216        node.shutdown();
2217        let _ = fs::remove_dir_all(&dir);
2218    }
2219
2220    #[test]
2221    fn from_config_loads_identity() {
2222        let dir = std::env::temp_dir().join(format!("rns-test-fl-{}", std::process::id()));
2223        let _ = fs::remove_dir_all(&dir);
2224        fs::create_dir_all(dir.join("storage/identities")).unwrap();
2225
2226        // Pre-create an identity
2227        let identity = Identity::new(&mut OsRng);
2228        let hash = *identity.hash();
2229        storage::save_identity(&identity, &dir.join("storage/identities/identity")).unwrap();
2230
2231        fs::write(
2232            dir.join("config"),
2233            "[reticulum]\nenable_transport = False\n",
2234        )
2235        .unwrap();
2236
2237        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks)).unwrap();
2238
2239        // Verify the same identity was loaded (hash matches)
2240        let loaded = storage::load_identity(&dir.join("storage/identities/identity")).unwrap();
2241        assert_eq!(*loaded.hash(), hash);
2242
2243        node.shutdown();
2244        let _ = fs::remove_dir_all(&dir);
2245    }
2246
2247    #[test]
2248    fn from_config_tcp_server() {
2249        let dir = std::env::temp_dir().join(format!("rns-test-fts-{}", std::process::id()));
2250        let _ = fs::remove_dir_all(&dir);
2251        fs::create_dir_all(&dir).unwrap();
2252
2253        // Find a free port
2254        let port = std::net::TcpListener::bind("127.0.0.1:0")
2255            .unwrap()
2256            .local_addr()
2257            .unwrap()
2258            .port();
2259
2260        let config = format!(
2261            r#"
2262[reticulum]
2263enable_transport = False
2264
2265[interfaces]
2266  [[Test TCP Server]]
2267    type = TCPServerInterface
2268    listen_ip = 127.0.0.1
2269    listen_port = {}
2270"#,
2271            port
2272        );
2273
2274        fs::write(dir.join("config"), config).unwrap();
2275
2276        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks)).unwrap();
2277
2278        // Give server time to start
2279        thread::sleep(Duration::from_millis(100));
2280
2281        // Should be able to connect
2282        let _client = std::net::TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
2283
2284        node.shutdown();
2285        let _ = fs::remove_dir_all(&dir);
2286    }
2287
2288    #[test]
2289    fn test_parse_interface_mode() {
2290        use rns_core::constants::*;
2291
2292        assert_eq!(parse_interface_mode("full"), MODE_FULL);
2293        assert_eq!(parse_interface_mode("Full"), MODE_FULL);
2294        assert_eq!(parse_interface_mode("access_point"), MODE_ACCESS_POINT);
2295        assert_eq!(parse_interface_mode("accesspoint"), MODE_ACCESS_POINT);
2296        assert_eq!(parse_interface_mode("ap"), MODE_ACCESS_POINT);
2297        assert_eq!(parse_interface_mode("AP"), MODE_ACCESS_POINT);
2298        assert_eq!(parse_interface_mode("pointtopoint"), MODE_POINT_TO_POINT);
2299        assert_eq!(parse_interface_mode("ptp"), MODE_POINT_TO_POINT);
2300        assert_eq!(parse_interface_mode("roaming"), MODE_ROAMING);
2301        assert_eq!(parse_interface_mode("boundary"), MODE_BOUNDARY);
2302        assert_eq!(parse_interface_mode("gateway"), MODE_GATEWAY);
2303        assert_eq!(parse_interface_mode("gw"), MODE_GATEWAY);
2304        // Unknown defaults to FULL
2305        assert_eq!(parse_interface_mode("invalid"), MODE_FULL);
2306    }
2307
2308    #[test]
2309    fn to_node_config_serial() {
2310        // Verify from_config parses SerialInterface correctly.
2311        // The serial port won't exist, so start() will fail, but the config
2312        // parsing path is exercised. We verify via the error (not a config error).
2313        let dir = std::env::temp_dir().join(format!("rns-test-serial-{}", std::process::id()));
2314        let _ = fs::remove_dir_all(&dir);
2315        fs::create_dir_all(&dir).unwrap();
2316
2317        let config = r#"
2318[reticulum]
2319enable_transport = False
2320
2321[interfaces]
2322  [[Test Serial Port]]
2323    type = SerialInterface
2324    port = /dev/nonexistent_rns_test_serial
2325    speed = 115200
2326    databits = 8
2327    parity = E
2328    stopbits = 1
2329    interface_mode = ptp
2330    networkname = testnet
2331"#;
2332        fs::write(dir.join("config"), config).unwrap();
2333
2334        let result = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks));
2335        // Should fail because the serial port doesn't exist, not because of config parsing
2336        match result {
2337            Ok(node) => {
2338                node.shutdown();
2339                panic!("Expected error from non-existent serial port");
2340            }
2341            Err(err) => {
2342                let msg = format!("{}", err);
2343                assert!(
2344                    !msg.contains("Unsupported") && !msg.contains("parse"),
2345                    "Error should be from serial open, got: {}",
2346                    msg
2347                );
2348            }
2349        }
2350
2351        let _ = fs::remove_dir_all(&dir);
2352    }
2353
2354    #[test]
2355    fn to_node_config_kiss() {
2356        // Verify from_config parses KISSInterface correctly.
2357        let dir = std::env::temp_dir().join(format!("rns-test-kiss-{}", std::process::id()));
2358        let _ = fs::remove_dir_all(&dir);
2359        fs::create_dir_all(&dir).unwrap();
2360
2361        let config = r#"
2362[reticulum]
2363enable_transport = False
2364
2365[interfaces]
2366  [[Test KISS TNC]]
2367    type = KISSInterface
2368    port = /dev/nonexistent_rns_test_kiss
2369    speed = 9600
2370    preamble = 500
2371    txtail = 30
2372    persistence = 128
2373    slottime = 40
2374    flow_control = True
2375    id_interval = 600
2376    id_callsign = TEST0
2377    interface_mode = full
2378    passphrase = secretkey
2379"#;
2380        fs::write(dir.join("config"), config).unwrap();
2381
2382        let result = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks));
2383        // Should fail because the serial port doesn't exist
2384        match result {
2385            Ok(node) => {
2386                node.shutdown();
2387                panic!("Expected error from non-existent serial port");
2388            }
2389            Err(err) => {
2390                let msg = format!("{}", err);
2391                assert!(
2392                    !msg.contains("Unsupported") && !msg.contains("parse"),
2393                    "Error should be from serial open, got: {}",
2394                    msg
2395                );
2396            }
2397        }
2398
2399        let _ = fs::remove_dir_all(&dir);
2400    }
2401
2402    #[test]
2403    fn test_extract_ifac_config() {
2404        use std::collections::HashMap;
2405
2406        // No IFAC params → None
2407        let params: HashMap<String, String> = HashMap::new();
2408        assert!(extract_ifac_config(&params, 16).is_none());
2409
2410        // networkname only
2411        let mut params = HashMap::new();
2412        params.insert("networkname".into(), "testnet".into());
2413        let ifac = extract_ifac_config(&params, 16).unwrap();
2414        assert_eq!(ifac.netname.as_deref(), Some("testnet"));
2415        assert!(ifac.netkey.is_none());
2416        assert_eq!(ifac.size, 16);
2417
2418        // passphrase only with custom size (in bits)
2419        let mut params = HashMap::new();
2420        params.insert("passphrase".into(), "secret".into());
2421        params.insert("ifac_size".into(), "64".into()); // 64 bits = 8 bytes
2422        let ifac = extract_ifac_config(&params, 16).unwrap();
2423        assert!(ifac.netname.is_none());
2424        assert_eq!(ifac.netkey.as_deref(), Some("secret"));
2425        assert_eq!(ifac.size, 8);
2426
2427        // Both with alternate key names
2428        let mut params = HashMap::new();
2429        params.insert("network_name".into(), "mynet".into());
2430        params.insert("pass_phrase".into(), "mykey".into());
2431        let ifac = extract_ifac_config(&params, 8).unwrap();
2432        assert_eq!(ifac.netname.as_deref(), Some("mynet"));
2433        assert_eq!(ifac.netkey.as_deref(), Some("mykey"));
2434        assert_eq!(ifac.size, 8);
2435    }
2436
2437    #[test]
2438    fn test_parse_parity() {
2439        assert_eq!(parse_parity("E"), Parity::Even);
2440        assert_eq!(parse_parity("even"), Parity::Even);
2441        assert_eq!(parse_parity("O"), Parity::Odd);
2442        assert_eq!(parse_parity("odd"), Parity::Odd);
2443        assert_eq!(parse_parity("N"), Parity::None);
2444        assert_eq!(parse_parity("none"), Parity::None);
2445        assert_eq!(parse_parity("unknown"), Parity::None);
2446    }
2447
2448    #[test]
2449    fn to_node_config_rnode() {
2450        // Verify from_config parses RNodeInterface correctly.
2451        // The serial port won't exist, so start() will fail at open time.
2452        let dir = std::env::temp_dir().join(format!("rns-test-rnode-{}", std::process::id()));
2453        let _ = fs::remove_dir_all(&dir);
2454        fs::create_dir_all(&dir).unwrap();
2455
2456        let config = r#"
2457[reticulum]
2458enable_transport = False
2459
2460[interfaces]
2461  [[Test RNode]]
2462    type = RNodeInterface
2463    port = /dev/nonexistent_rns_test_rnode
2464    frequency = 867200000
2465    bandwidth = 125000
2466    txpower = 7
2467    spreadingfactor = 8
2468    codingrate = 5
2469    flow_control = True
2470    st_alock = 5.0
2471    lt_alock = 2.5
2472    interface_mode = full
2473    networkname = testnet
2474"#;
2475        fs::write(dir.join("config"), config).unwrap();
2476
2477        let result = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks));
2478        // Should fail because the serial port doesn't exist, not because of config parsing
2479        match result {
2480            Ok(node) => {
2481                node.shutdown();
2482                panic!("Expected error from non-existent serial port");
2483            }
2484            Err(err) => {
2485                let msg = format!("{}", err);
2486                assert!(
2487                    !msg.contains("Unsupported") && !msg.contains("parse"),
2488                    "Error should be from serial open, got: {}",
2489                    msg
2490                );
2491            }
2492        }
2493
2494        let _ = fs::remove_dir_all(&dir);
2495    }
2496
2497    #[test]
2498    fn to_node_config_pipe() {
2499        // Verify from_config parses PipeInterface correctly.
2500        // Use `cat` as a real command so it actually starts.
2501        let dir = std::env::temp_dir().join(format!("rns-test-pipe-{}", std::process::id()));
2502        let _ = fs::remove_dir_all(&dir);
2503        fs::create_dir_all(&dir).unwrap();
2504
2505        let config = r#"
2506[reticulum]
2507enable_transport = False
2508
2509[interfaces]
2510  [[Test Pipe]]
2511    type = PipeInterface
2512    command = cat
2513    respawn_delay = 5000
2514    interface_mode = full
2515"#;
2516        fs::write(dir.join("config"), config).unwrap();
2517
2518        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks)).unwrap();
2519        // If we got here, config parsing and start() succeeded
2520        node.shutdown();
2521
2522        let _ = fs::remove_dir_all(&dir);
2523    }
2524
2525    #[test]
2526    fn to_node_config_backbone() {
2527        // Verify from_config parses BackboneInterface correctly.
2528        let dir = std::env::temp_dir().join(format!("rns-test-backbone-{}", std::process::id()));
2529        let _ = fs::remove_dir_all(&dir);
2530        fs::create_dir_all(&dir).unwrap();
2531
2532        let port = std::net::TcpListener::bind("127.0.0.1:0")
2533            .unwrap()
2534            .local_addr()
2535            .unwrap()
2536            .port();
2537
2538        let config = format!(
2539            r#"
2540[reticulum]
2541enable_transport = False
2542
2543[interfaces]
2544  [[Test Backbone]]
2545    type = BackboneInterface
2546    listen_ip = 127.0.0.1
2547    listen_port = {}
2548    interface_mode = full
2549"#,
2550            port
2551        );
2552
2553        fs::write(dir.join("config"), config).unwrap();
2554
2555        let node = RnsNode::from_config(Some(&dir), Box::new(NoopCallbacks)).unwrap();
2556
2557        // Give server time to start
2558        thread::sleep(Duration::from_millis(100));
2559
2560        // Should be able to connect
2561        {
2562            let _client = std::net::TcpStream::connect(format!("127.0.0.1:{}", port)).unwrap();
2563            // client drops here, closing the connection cleanly
2564        }
2565
2566        // Small delay to let epoll process the disconnect
2567        thread::sleep(Duration::from_millis(50));
2568
2569        node.shutdown();
2570        let _ = fs::remove_dir_all(&dir);
2571    }
2572
2573    #[test]
2574    fn rnode_config_defaults() {
2575        use crate::interface::rnode::{RNodeConfig, RNodeSubConfig};
2576
2577        let config = RNodeConfig::default();
2578        assert_eq!(config.speed, 115200);
2579        assert!(config.subinterfaces.is_empty());
2580        assert!(config.id_interval.is_none());
2581        assert!(config.id_callsign.is_none());
2582
2583        let sub = RNodeSubConfig {
2584            name: "test".into(),
2585            frequency: 868_000_000,
2586            bandwidth: 125_000,
2587            txpower: 7,
2588            spreading_factor: 8,
2589            coding_rate: 5,
2590            flow_control: false,
2591            st_alock: None,
2592            lt_alock: None,
2593        };
2594        assert_eq!(sub.frequency, 868_000_000);
2595        assert_eq!(sub.bandwidth, 125_000);
2596        assert!(!sub.flow_control);
2597    }
2598
2599    // =========================================================================
2600    // Phase 9c: Announce + Discovery node-level tests
2601    // =========================================================================
2602
2603    #[test]
2604    fn announce_builds_valid_packet() {
2605        let identity = Identity::new(&mut OsRng);
2606        let identity_hash = rns_core::types::IdentityHash(*identity.hash());
2607
2608        let node = RnsNode::start(
2609            NodeConfig {
2610                transport_enabled: false,
2611                identity: None,
2612                interfaces: vec![],
2613                share_instance: false,
2614                instance_name: "default".into(),
2615                shared_instance_port: 37428,
2616                rpc_port: 0,
2617                cache_dir: None,
2618                management: Default::default(),
2619                probe_port: None,
2620                probe_addr: None,
2621                device: None,
2622                hooks: Vec::new(),
2623                discover_interfaces: false,
2624                discovery_required_value: None,
2625            },
2626            Box::new(NoopCallbacks),
2627        ).unwrap();
2628
2629        let dest = crate::destination::Destination::single_in(
2630            "test", &["echo"], identity_hash,
2631        );
2632
2633        // Register destination first
2634        node.register_destination(dest.hash.0, dest.dest_type.to_wire_constant()).unwrap();
2635
2636        // Announce should succeed (though no interfaces to send on)
2637        let result = node.announce(&dest, &identity, Some(b"hello"));
2638        assert!(result.is_ok());
2639
2640        node.shutdown();
2641    }
2642
2643    #[test]
2644    fn has_path_and_hops_to() {
2645        let node = RnsNode::start(
2646            NodeConfig {
2647                transport_enabled: false,
2648                identity: None,
2649                interfaces: vec![],
2650                share_instance: false,
2651                instance_name: "default".into(),
2652                shared_instance_port: 37428,
2653                rpc_port: 0,
2654                cache_dir: None,
2655                management: Default::default(),
2656                probe_port: None,
2657                probe_addr: None,
2658                device: None,
2659                hooks: Vec::new(),
2660                discover_interfaces: false,
2661                discovery_required_value: None,
2662            },
2663            Box::new(NoopCallbacks),
2664        ).unwrap();
2665
2666        let dh = rns_core::types::DestHash([0xAA; 16]);
2667
2668        // No path should exist
2669        assert_eq!(node.has_path(&dh).unwrap(), false);
2670        assert_eq!(node.hops_to(&dh).unwrap(), None);
2671
2672        node.shutdown();
2673    }
2674
2675    #[test]
2676    fn recall_identity_none_when_unknown() {
2677        let node = RnsNode::start(
2678            NodeConfig {
2679                transport_enabled: false,
2680                identity: None,
2681                interfaces: vec![],
2682                share_instance: false,
2683                instance_name: "default".into(),
2684                shared_instance_port: 37428,
2685                rpc_port: 0,
2686                cache_dir: None,
2687                management: Default::default(),
2688                probe_port: None,
2689                probe_addr: None,
2690                device: None,
2691                hooks: Vec::new(),
2692                discover_interfaces: false,
2693                discovery_required_value: None,
2694            },
2695            Box::new(NoopCallbacks),
2696        ).unwrap();
2697
2698        let dh = rns_core::types::DestHash([0xBB; 16]);
2699        assert!(node.recall_identity(&dh).unwrap().is_none());
2700
2701        node.shutdown();
2702    }
2703
2704    #[test]
2705    fn request_path_does_not_crash() {
2706        let node = RnsNode::start(
2707            NodeConfig {
2708                transport_enabled: false,
2709                identity: None,
2710                interfaces: vec![],
2711                share_instance: false,
2712                instance_name: "default".into(),
2713                shared_instance_port: 37428,
2714                rpc_port: 0,
2715                cache_dir: None,
2716                management: Default::default(),
2717                probe_port: None,
2718                probe_addr: None,
2719                device: None,
2720                hooks: Vec::new(),
2721                discover_interfaces: false,
2722                discovery_required_value: None,
2723            },
2724            Box::new(NoopCallbacks),
2725        ).unwrap();
2726
2727        let dh = rns_core::types::DestHash([0xCC; 16]);
2728        assert!(node.request_path(&dh).is_ok());
2729
2730        // Small wait for the event to be processed
2731        thread::sleep(Duration::from_millis(50));
2732
2733        node.shutdown();
2734    }
2735
2736    // =========================================================================
2737    // Phase 9d: send_packet + register_destination_with_proof tests
2738    // =========================================================================
2739
2740    #[test]
2741    fn send_packet_plain() {
2742        let node = RnsNode::start(
2743            NodeConfig {
2744                transport_enabled: false,
2745                identity: None,
2746                interfaces: vec![],
2747                share_instance: false,
2748                instance_name: "default".into(),
2749                shared_instance_port: 37428,
2750                rpc_port: 0,
2751                cache_dir: None,
2752                management: Default::default(),
2753                probe_port: None,
2754                probe_addr: None,
2755                device: None,
2756                hooks: Vec::new(),
2757                discover_interfaces: false,
2758                discovery_required_value: None,
2759            },
2760            Box::new(NoopCallbacks),
2761        ).unwrap();
2762
2763        let dest = crate::destination::Destination::plain("test", &["echo"]);
2764        let result = node.send_packet(&dest, b"hello world");
2765        assert!(result.is_ok());
2766
2767        let packet_hash = result.unwrap();
2768        // Packet hash should be non-zero
2769        assert_ne!(packet_hash.0, [0u8; 32]);
2770
2771        // Small wait for the event to be processed
2772        thread::sleep(Duration::from_millis(50));
2773
2774        node.shutdown();
2775    }
2776
2777    #[test]
2778    fn send_packet_single_requires_public_key() {
2779        let node = RnsNode::start(
2780            NodeConfig {
2781                transport_enabled: false,
2782                identity: None,
2783                interfaces: vec![],
2784                share_instance: false,
2785                instance_name: "default".into(),
2786                shared_instance_port: 37428,
2787                rpc_port: 0,
2788                cache_dir: None,
2789                management: Default::default(),
2790                probe_port: None,
2791                probe_addr: None,
2792                device: None,
2793                hooks: Vec::new(),
2794                discover_interfaces: false,
2795                discovery_required_value: None,
2796            },
2797            Box::new(NoopCallbacks),
2798        ).unwrap();
2799
2800        // single_in has no public_key — sending should fail
2801        let dest = crate::destination::Destination::single_in(
2802            "test", &["echo"],
2803            rns_core::types::IdentityHash([0x42; 16]),
2804        );
2805        let result = node.send_packet(&dest, b"hello");
2806        assert!(result.is_err(), "single_in has no public_key, should fail");
2807
2808        node.shutdown();
2809    }
2810
2811    #[test]
2812    fn send_packet_single_encrypts() {
2813        let node = RnsNode::start(
2814            NodeConfig {
2815                transport_enabled: false,
2816                identity: None,
2817                interfaces: vec![],
2818                share_instance: false,
2819                instance_name: "default".into(),
2820                shared_instance_port: 37428,
2821                rpc_port: 0,
2822                cache_dir: None,
2823                management: Default::default(),
2824                probe_port: None,
2825                probe_addr: None,
2826                device: None,
2827                hooks: Vec::new(),
2828                discover_interfaces: false,
2829                discovery_required_value: None,
2830            },
2831            Box::new(NoopCallbacks),
2832        ).unwrap();
2833
2834        // Create a proper OUT SINGLE destination with a real identity's public key
2835        let remote_identity = Identity::new(&mut OsRng);
2836        let recalled = crate::destination::AnnouncedIdentity {
2837            dest_hash: rns_core::types::DestHash([0xAA; 16]),
2838            identity_hash: rns_core::types::IdentityHash(*remote_identity.hash()),
2839            public_key: remote_identity.get_public_key().unwrap(),
2840            app_data: None,
2841            hops: 1,
2842            received_at: 0.0,
2843        };
2844        let dest = crate::destination::Destination::single_out("test", &["echo"], &recalled);
2845
2846        let result = node.send_packet(&dest, b"secret message");
2847        assert!(result.is_ok());
2848
2849        let packet_hash = result.unwrap();
2850        assert_ne!(packet_hash.0, [0u8; 32]);
2851
2852        thread::sleep(Duration::from_millis(50));
2853        node.shutdown();
2854    }
2855
2856    #[test]
2857    fn register_destination_with_proof_prove_all() {
2858        let node = RnsNode::start(
2859            NodeConfig {
2860                transport_enabled: false,
2861                identity: None,
2862                interfaces: vec![],
2863                share_instance: false,
2864                instance_name: "default".into(),
2865                shared_instance_port: 37428,
2866                rpc_port: 0,
2867                cache_dir: None,
2868                management: Default::default(),
2869                probe_port: None,
2870                probe_addr: None,
2871                device: None,
2872                hooks: Vec::new(),
2873                discover_interfaces: false,
2874                discovery_required_value: None,
2875            },
2876            Box::new(NoopCallbacks),
2877        ).unwrap();
2878
2879        let identity = Identity::new(&mut OsRng);
2880        let ih = rns_core::types::IdentityHash(*identity.hash());
2881        let dest = crate::destination::Destination::single_in("echo", &["request"], ih)
2882            .set_proof_strategy(rns_core::types::ProofStrategy::ProveAll);
2883        let prv_key = identity.get_private_key().unwrap();
2884
2885        let result = node.register_destination_with_proof(&dest, Some(prv_key));
2886        assert!(result.is_ok());
2887
2888        // Small wait for the events to be processed
2889        thread::sleep(Duration::from_millis(50));
2890
2891        node.shutdown();
2892    }
2893
2894    #[test]
2895    fn register_destination_with_proof_prove_none() {
2896        let node = RnsNode::start(
2897            NodeConfig {
2898                transport_enabled: false,
2899                identity: None,
2900                interfaces: vec![],
2901                share_instance: false,
2902                instance_name: "default".into(),
2903                shared_instance_port: 37428,
2904                rpc_port: 0,
2905                cache_dir: None,
2906                management: Default::default(),
2907                probe_port: None,
2908                probe_addr: None,
2909                device: None,
2910                hooks: Vec::new(),
2911                discover_interfaces: false,
2912                discovery_required_value: None,
2913            },
2914            Box::new(NoopCallbacks),
2915        ).unwrap();
2916
2917        // ProveNone should not send RegisterProofStrategy event
2918        let dest = crate::destination::Destination::plain("test", &["data"])
2919            .set_proof_strategy(rns_core::types::ProofStrategy::ProveNone);
2920
2921        let result = node.register_destination_with_proof(&dest, None);
2922        assert!(result.is_ok());
2923
2924        thread::sleep(Duration::from_millis(50));
2925        node.shutdown();
2926    }
2927}