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