Skip to main content

rns_net/
node.rs

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