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