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