Skip to main content

rns_net/
node.rs

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