Skip to main content

rns_net/common/
config.rs

1//! ConfigObj parser for RNS config files.
2//!
3//! Python RNS uses ConfigObj format — NOT TOML, NOT standard INI.
4//! Key differences: nested `[[sections]]`, booleans `Yes`/`No`/`True`/`False`,
5//! comments with `#`, unquoted string values.
6
7use std::collections::HashMap;
8use std::fmt;
9use std::io;
10use std::path::Path;
11
12/// Parsed RNS configuration.
13#[derive(Debug, Clone)]
14pub struct RnsConfig {
15    pub reticulum: ReticulumSection,
16    pub logging: LoggingSection,
17    pub interfaces: Vec<ParsedInterface>,
18    pub hooks: Vec<ParsedHook>,
19}
20
21/// A parsed hook from `[[subsection]]` within `[hooks]`.
22#[derive(Debug, Clone)]
23pub struct ParsedHook {
24    pub name: String,
25    pub path: String,
26    pub hook_type: String,
27    pub builtin_id: Option<String>,
28    pub attach_point: String,
29    pub priority: i32,
30    pub enabled: bool,
31}
32
33/// The `[reticulum]` section.
34#[derive(Debug, Clone)]
35pub struct ReticulumSection {
36    pub enable_transport: bool,
37    pub static_transport_identity: bool,
38    pub local_hops_delta: bool,
39    pub share_instance: bool,
40    pub instance_name: String,
41    pub shared_instance_port: u16,
42    pub instance_control_port: u16,
43    pub panic_on_interface_error: bool,
44    pub use_implicit_proof: bool,
45    pub network_identity: Option<String>,
46    pub respond_to_probes: bool,
47    pub enable_remote_management: bool,
48    pub remote_management_allowed: Vec<String>,
49    pub publish_blackhole: bool,
50    pub probe_port: Option<u16>,
51    pub probe_addr: Option<String>,
52    /// Protocol for endpoint discovery: "rnsp" (default) or "stun".
53    pub probe_protocol: Option<String>,
54    /// Network interface to bind outbound sockets to (e.g. "usb0").
55    pub device: Option<String>,
56    /// Enable interface discovery (advertise discoverable interfaces and
57    /// listen for discovery announces from the network).
58    pub discover_interfaces: bool,
59    /// Global gravity used by interfaces without an explicit override.
60    pub default_gravity: i64,
61    /// Mode assigned to auto-connected interfaces.
62    pub autoconnect_interface_mode: Option<String>,
63    /// Gravity assigned to auto-connected interfaces.
64    pub autoconnect_interface_gravity: i64,
65    /// Allow auto-connected interfaces to propagate announces to internal interfaces.
66    pub autoconnect_announces_to_internal: bool,
67    /// Minimum stamp value for accepting discovered interfaces.
68    pub required_discovery_value: Option<u8>,
69    /// Accept an announce with strictly fewer hops even when the random_blob
70    /// is a duplicate of the existing path entry.
71    pub prefer_shorter_path: bool,
72    /// Maximum number of alternative paths stored per destination.
73    /// Default 1 (single path, backward-compatible).
74    pub max_paths_per_destination: usize,
75    /// Maximum number of packet hashes retained for duplicate suppression.
76    pub packet_hashlist_max_entries: usize,
77    /// Maximum number of discovery path-request tags remembered.
78    pub max_discovery_pr_tags: usize,
79    /// Maximum number of destinations retained in the live path table.
80    pub max_path_destinations: usize,
81    /// Maximum number of destinations retained across tunnel-known paths.
82    pub max_tunnel_destinations_total: usize,
83    /// TTL for recalled known destinations without an active path, in seconds.
84    pub known_destinations_ttl: u64,
85    /// Maximum number of recalled known destinations retained.
86    pub known_destinations_max_entries: usize,
87    /// TTL for received ratchets, in seconds.
88    pub ratchet_expiry: u64,
89    /// TTL for announce retransmission state, in seconds.
90    pub announce_table_ttl: u64,
91    /// Maximum retained bytes for announce retransmission state.
92    pub announce_table_max_bytes: usize,
93    /// Whether the announce signature verification cache is enabled.
94    pub announce_sig_cache_enabled: bool,
95    /// Maximum entries in the announce signature verification cache.
96    pub announce_sig_cache_max_entries: usize,
97    /// TTL for announce signature cache entries, in seconds.
98    pub announce_sig_cache_ttl: u64,
99    /// Maximum entries in the async announce verification queue.
100    pub announce_queue_max_entries: usize,
101    /// Maximum interface-scoped announce queues retained.
102    pub announce_queue_max_interfaces: usize,
103    /// Default announce-rate target for transport-node interfaces, in seconds.
104    pub default_ar_target: Option<f64>,
105    /// Default announce-rate penalty for transport-node interfaces, in seconds.
106    pub default_ar_penalty: f64,
107    /// Default announce-rate grace count for transport-node interfaces.
108    pub default_ar_grace: u32,
109    /// Default maximum held announces for ingress control.
110    pub default_ic_max_held_announces: usize,
111    /// Default announce burst hold time for ingress control.
112    pub default_ic_burst_hold: f64,
113    /// Default new-interface announce burst threshold.
114    pub default_ic_burst_freq_new: f64,
115    /// Default mature-interface announce burst threshold.
116    pub default_ic_burst_freq: f64,
117    /// Default new-interface path request burst threshold.
118    pub default_ic_pr_burst_freq_new: f64,
119    /// Default mature-interface path request burst threshold.
120    pub default_ic_pr_burst_freq: f64,
121    /// Default new-interface age window for ingress control.
122    pub default_ic_new_time: f64,
123    /// Default burst penalty before releasing held announces.
124    pub default_ic_burst_penalty: f64,
125    /// Default interval between released held announces.
126    pub default_ic_held_release_interval: f64,
127    /// Default egress path request limiting state.
128    pub default_egress_control: bool,
129    /// Default egress path request frequency threshold.
130    pub default_ec_pr_freq: f64,
131    /// Maximum retained bytes in the async announce verification queue.
132    pub announce_queue_max_bytes: usize,
133    /// TTL for queued async announce verification entries, in seconds.
134    pub announce_queue_ttl: u64,
135    /// Overflow policy for the async announce verification queue.
136    pub announce_queue_overflow_policy: String,
137    /// Maximum queued events awaiting driver processing.
138    pub driver_event_queue_capacity: usize,
139    /// Maximum queued outbound frames per interface writer worker.
140    pub interface_writer_queue_capacity: usize,
141    /// Maximum active outbound Backbone peer-pool connections. Zero disables pooling.
142    pub backbone_peer_pool_max_connected: usize,
143    /// Failures within the failure window before a pooled Backbone peer enters cooldown.
144    pub backbone_peer_pool_failure_threshold: usize,
145    /// Failure accounting window for pooled Backbone peers, in seconds.
146    pub backbone_peer_pool_failure_window: u64,
147    /// Cooldown duration for failed pooled Backbone peers, in seconds.
148    pub backbone_peer_pool_cooldown: u64,
149    #[cfg(feature = "hooks")]
150    pub provider_bridge: bool,
151    #[cfg(feature = "hooks")]
152    pub provider_socket_path: Option<String>,
153    #[cfg(feature = "hooks")]
154    pub provider_queue_max_events: usize,
155    #[cfg(feature = "hooks")]
156    pub provider_queue_max_bytes: usize,
157    #[cfg(feature = "hooks")]
158    pub provider_overflow_policy: String,
159}
160
161impl Default for ReticulumSection {
162    fn default() -> Self {
163        ReticulumSection {
164            enable_transport: false,
165            static_transport_identity: false,
166            local_hops_delta: false,
167            share_instance: true,
168            instance_name: "default".into(),
169            shared_instance_port: 37428,
170            instance_control_port: 37429,
171            panic_on_interface_error: false,
172            use_implicit_proof: true,
173            network_identity: None,
174            respond_to_probes: false,
175            enable_remote_management: false,
176            remote_management_allowed: Vec::new(),
177            publish_blackhole: false,
178            probe_port: None,
179            probe_addr: None,
180            probe_protocol: None,
181            device: None,
182            discover_interfaces: false,
183            default_gravity: 0,
184            autoconnect_interface_mode: None,
185            autoconnect_interface_gravity: 0,
186            autoconnect_announces_to_internal: false,
187            required_discovery_value: None,
188            prefer_shorter_path: false,
189            max_paths_per_destination: 1,
190            packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
191            max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
192            max_path_destinations: rns_core::transport::types::DEFAULT_MAX_PATH_DESTINATIONS,
193            max_tunnel_destinations_total: usize::MAX,
194            known_destinations_ttl: 48 * 60 * 60,
195            known_destinations_max_entries: 8192,
196            ratchet_expiry: rns_core::constants::RATCHET_EXPIRY,
197            announce_table_ttl: rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
198            announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
199            announce_sig_cache_enabled: true,
200            announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
201            announce_sig_cache_ttl: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
202            announce_queue_max_entries: 256,
203            announce_queue_max_interfaces: 1024,
204            default_ar_target: Some(3600.0),
205            default_ar_penalty: 0.0,
206            default_ar_grace: 5,
207            default_ic_max_held_announces: rns_core::constants::IC_MAX_HELD_ANNOUNCES,
208            default_ic_burst_hold: rns_core::constants::IC_BURST_HOLD,
209            default_ic_burst_freq_new: rns_core::constants::IC_BURST_FREQ_NEW,
210            default_ic_burst_freq: rns_core::constants::IC_BURST_FREQ,
211            default_ic_pr_burst_freq_new: rns_core::constants::IC_PR_BURST_FREQ_NEW,
212            default_ic_pr_burst_freq: rns_core::constants::IC_PR_BURST_FREQ,
213            default_ic_new_time: rns_core::constants::IC_NEW_TIME,
214            default_ic_burst_penalty: rns_core::constants::IC_BURST_PENALTY,
215            default_ic_held_release_interval: rns_core::constants::IC_HELD_RELEASE_INTERVAL,
216            default_egress_control: false,
217            default_ec_pr_freq: rns_core::constants::EC_PR_FREQ,
218            announce_queue_max_bytes: 256 * 1024,
219            announce_queue_ttl: 30,
220            announce_queue_overflow_policy: "drop_worst".into(),
221            driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
222            interface_writer_queue_capacity: crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
223            backbone_peer_pool_max_connected: 0,
224            backbone_peer_pool_failure_threshold: 3,
225            backbone_peer_pool_failure_window: 600,
226            backbone_peer_pool_cooldown: 900,
227            #[cfg(feature = "hooks")]
228            provider_bridge: false,
229            #[cfg(feature = "hooks")]
230            provider_socket_path: None,
231            #[cfg(feature = "hooks")]
232            provider_queue_max_events: 16384,
233            #[cfg(feature = "hooks")]
234            provider_queue_max_bytes: 8 * 1024 * 1024,
235            #[cfg(feature = "hooks")]
236            provider_overflow_policy: "drop_newest".into(),
237        }
238    }
239}
240
241/// The `[logging]` section.
242#[derive(Debug, Clone)]
243pub struct LoggingSection {
244    pub loglevel: u8,
245    pub logtimestamps: bool,
246}
247
248impl Default for LoggingSection {
249    fn default() -> Self {
250        LoggingSection {
251            loglevel: 4,
252            logtimestamps: true,
253        }
254    }
255}
256
257/// A parsed interface from `[[subsection]]` within `[interfaces]`.
258#[derive(Debug, Clone)]
259pub struct ParsedInterface {
260    pub name: String,
261    pub interface_type: String,
262    pub enabled: bool,
263    pub mode: String,
264    pub params: HashMap<String, String>,
265    pub subinterfaces: Vec<ParsedSubinterface>,
266}
267
268/// A `[[[subinterface]]]` nested under an interface section.
269#[derive(Debug, Clone, PartialEq, Eq)]
270pub struct ParsedSubinterface {
271    pub name: String,
272    pub params: HashMap<String, String>,
273}
274
275/// Configuration parse error.
276#[derive(Debug, Clone)]
277pub enum ConfigError {
278    Io(String),
279    Parse(String),
280    InvalidValue { key: String, value: String },
281}
282
283impl fmt::Display for ConfigError {
284    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285        match self {
286            ConfigError::Io(msg) => write!(f, "Config I/O error: {}", msg),
287            ConfigError::Parse(msg) => write!(f, "Config parse error: {}", msg),
288            ConfigError::InvalidValue { key, value } => {
289                write!(f, "Invalid value for '{}': '{}'", key, value)
290            }
291        }
292    }
293}
294
295impl From<io::Error> for ConfigError {
296    fn from(e: io::Error) -> Self {
297        ConfigError::Io(e.to_string())
298    }
299}
300
301/// Parse a config string into an `RnsConfig`.
302pub fn parse(input: &str) -> Result<RnsConfig, ConfigError> {
303    let mut current_section: Option<String> = None;
304    let mut current_subsection: Option<String> = None;
305
306    let mut reticulum_kvs: HashMap<String, String> = HashMap::new();
307    let mut logging_kvs: HashMap<String, String> = HashMap::new();
308    let mut interfaces: Vec<ParsedInterface> = Vec::new();
309    let mut current_iface_kvs: Option<HashMap<String, String>> = None;
310    let mut current_iface_name: Option<String> = None;
311    let mut current_iface_subinterfaces: Vec<ParsedSubinterface> = Vec::new();
312    let mut current_subiface_name: Option<String> = None;
313    let mut current_subiface_kvs: Option<HashMap<String, String>> = None;
314    let mut hooks: Vec<ParsedHook> = Vec::new();
315    let mut current_hook_kvs: Option<HashMap<String, String>> = None;
316    let mut current_hook_name: Option<String> = None;
317
318    for line in input.lines() {
319        // Strip comments (# to end of line, unless inside quotes)
320        let line = strip_comment(line);
321        let trimmed = line.trim();
322
323        // Skip empty lines
324        if trimmed.is_empty() {
325            continue;
326        }
327
328        // Check for third-level subsection [[[name]]] before [[name]].
329        if trimmed.starts_with("[[[") && trimmed.ends_with("]]]") {
330            if current_iface_kvs.is_none() {
331                return Err(ConfigError::Parse(
332                    "third-level section outside an interface".into(),
333                ));
334            }
335            if let (Some(name), Some(params)) =
336                (current_subiface_name.take(), current_subiface_kvs.take())
337            {
338                current_iface_subinterfaces.push(ParsedSubinterface { name, params });
339            }
340            current_subiface_name = Some(trimmed[3..trimmed.len() - 3].trim().to_string());
341            current_subiface_kvs = Some(HashMap::new());
342            continue;
343        }
344
345        // Check for subsection [[name]]
346        if trimmed.starts_with("[[") && trimmed.ends_with("]]") {
347            let name = trimmed[2..trimmed.len() - 2].trim().to_string();
348            if let (Some(name), Some(params)) =
349                (current_subiface_name.take(), current_subiface_kvs.take())
350            {
351                current_iface_subinterfaces.push(ParsedSubinterface { name, params });
352            }
353            // Finalize previous interface subsection if any
354            if let (Some(iface_name), Some(kvs)) =
355                (current_iface_name.take(), current_iface_kvs.take())
356            {
357                interfaces.push(build_parsed_interface(
358                    iface_name,
359                    kvs,
360                    std::mem::take(&mut current_iface_subinterfaces),
361                ));
362            }
363            // Finalize previous hook subsection if any
364            if let (Some(hook_name), Some(kvs)) =
365                (current_hook_name.take(), current_hook_kvs.take())
366            {
367                hooks.push(build_parsed_hook(hook_name, kvs));
368            }
369            current_subsection = Some(name.clone());
370            // Determine which section we're in to know subsection type
371            if current_section.as_deref() == Some("hooks") {
372                current_hook_name = Some(name);
373                current_hook_kvs = Some(HashMap::new());
374            } else {
375                current_iface_name = Some(name);
376                current_iface_kvs = Some(HashMap::new());
377            }
378            continue;
379        }
380
381        // Check for section [name]
382        if trimmed.starts_with('[') && trimmed.ends_with(']') {
383            if let (Some(name), Some(params)) =
384                (current_subiface_name.take(), current_subiface_kvs.take())
385            {
386                current_iface_subinterfaces.push(ParsedSubinterface { name, params });
387            }
388            // Finalize previous interface subsection if any
389            if let (Some(iface_name), Some(kvs)) =
390                (current_iface_name.take(), current_iface_kvs.take())
391            {
392                interfaces.push(build_parsed_interface(
393                    iface_name,
394                    kvs,
395                    std::mem::take(&mut current_iface_subinterfaces),
396                ));
397            }
398            // Finalize previous hook subsection if any
399            if let (Some(hook_name), Some(kvs)) =
400                (current_hook_name.take(), current_hook_kvs.take())
401            {
402                hooks.push(build_parsed_hook(hook_name, kvs));
403            }
404            current_subsection = None;
405
406            let name = trimmed[1..trimmed.len() - 1].trim().to_lowercase();
407            current_section = Some(name);
408            continue;
409        }
410
411        // Parse key = value
412        if let Some(eq_pos) = trimmed.find('=') {
413            let key = trimmed[..eq_pos].trim().to_string();
414            let value = trimmed[eq_pos + 1..].trim().to_string();
415
416            if current_subsection.is_some() {
417                // Inside a [[subsection]] — exactly one of these should be Some
418                debug_assert!(
419                    !(current_hook_kvs.is_some() && current_iface_kvs.is_some()),
420                    "hook and interface subsections should never be active simultaneously"
421                );
422                if let Some(ref mut kvs) = current_subiface_kvs {
423                    kvs.insert(key, value);
424                } else if let Some(ref mut kvs) = current_hook_kvs {
425                    kvs.insert(key, value);
426                } else if let Some(ref mut kvs) = current_iface_kvs {
427                    kvs.insert(key, value);
428                }
429            } else if let Some(ref section) = current_section {
430                match section.as_str() {
431                    "reticulum" => {
432                        reticulum_kvs.insert(key, value);
433                    }
434                    "logging" => {
435                        logging_kvs.insert(key, value);
436                    }
437                    _ => {} // ignore unknown sections
438                }
439            }
440        }
441    }
442
443    // Finalize last subsections
444    if let (Some(name), Some(params)) = (current_subiface_name.take(), current_subiface_kvs.take())
445    {
446        current_iface_subinterfaces.push(ParsedSubinterface { name, params });
447    }
448    if let (Some(iface_name), Some(kvs)) = (current_iface_name.take(), current_iface_kvs.take()) {
449        interfaces.push(build_parsed_interface(
450            iface_name,
451            kvs,
452            current_iface_subinterfaces,
453        ));
454    }
455    if let (Some(hook_name), Some(kvs)) = (current_hook_name.take(), current_hook_kvs.take()) {
456        hooks.push(build_parsed_hook(hook_name, kvs));
457    }
458
459    // Build typed sections
460    let reticulum = build_reticulum_section(&reticulum_kvs)?;
461    let logging = build_logging_section(&logging_kvs)?;
462
463    Ok(RnsConfig {
464        reticulum,
465        logging,
466        interfaces,
467        hooks,
468    })
469}
470
471/// Parse a config file from disk.
472pub fn parse_file(path: &Path) -> Result<RnsConfig, ConfigError> {
473    let content = std::fs::read_to_string(path)?;
474    parse(&content)
475}
476
477/// Strip `#` comments from a line (simple: not inside quotes).
478fn strip_comment(line: &str) -> &str {
479    // Find # that is not inside quotes
480    let mut in_quote = false;
481    let mut quote_char = '"';
482    for (i, ch) in line.char_indices() {
483        if !in_quote && (ch == '"' || ch == '\'') {
484            in_quote = true;
485            quote_char = ch;
486        } else if in_quote && ch == quote_char {
487            in_quote = false;
488        } else if !in_quote && ch == '#' {
489            return &line[..i];
490        }
491    }
492    line
493}
494
495/// Parse a string as a boolean (ConfigObj style). Public API for use by node.rs.
496pub fn parse_bool_pub(value: &str) -> Option<bool> {
497    parse_bool(value)
498}
499
500/// Parse a string as a boolean (ConfigObj style).
501fn parse_bool(value: &str) -> Option<bool> {
502    match value.to_lowercase().as_str() {
503        "yes" | "true" | "1" | "on" => Some(true),
504        "no" | "false" | "0" | "off" => Some(false),
505        _ => None,
506    }
507}
508
509fn parse_nonnegative_f64_option(
510    kvs: &HashMap<String, String>,
511    key: &str,
512) -> Result<Option<f64>, ConfigError> {
513    let Some(v) = kvs.get(key) else {
514        return Ok(None);
515    };
516    let parsed = v.parse::<f64>().map_err(|_| ConfigError::InvalidValue {
517        key: key.into(),
518        value: v.clone(),
519    })?;
520    if !parsed.is_finite() || parsed < 0.0 {
521        return Err(ConfigError::InvalidValue {
522            key: key.into(),
523            value: v.clone(),
524        });
525    }
526    Ok(Some(parsed))
527}
528
529fn parse_usize_option(
530    kvs: &HashMap<String, String>,
531    key: &str,
532) -> Result<Option<usize>, ConfigError> {
533    let Some(v) = kvs.get(key) else {
534        return Ok(None);
535    };
536    let parsed = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
537        key: key.into(),
538        value: v.clone(),
539    })?;
540    Ok(Some(parsed))
541}
542
543fn build_parsed_interface(
544    name: String,
545    mut kvs: HashMap<String, String>,
546    subinterfaces: Vec<ParsedSubinterface>,
547) -> ParsedInterface {
548    let interface_type = kvs.remove("type").unwrap_or_default();
549    let enabled = kvs
550        .remove("enabled")
551        .and_then(|v| parse_bool(&v))
552        .unwrap_or(true);
553    // Python checks `interface_mode` first, then falls back to `mode`
554    let mode = kvs
555        .remove("interface_mode")
556        .or_else(|| kvs.remove("mode"))
557        .unwrap_or_else(|| "full".into());
558
559    ParsedInterface {
560        name,
561        interface_type,
562        enabled,
563        mode,
564        params: kvs,
565        subinterfaces,
566    }
567}
568
569fn build_parsed_hook(name: String, mut kvs: HashMap<String, String>) -> ParsedHook {
570    let path = kvs.remove("path").unwrap_or_default();
571    let hook_type = kvs
572        .remove("type")
573        .or_else(|| kvs.remove("backend"))
574        .unwrap_or_else(|| default_hook_type().into());
575    let builtin_id = kvs
576        .remove("builtin")
577        .or_else(|| kvs.remove("builtin_id"))
578        .or_else(|| kvs.remove("id"));
579    let attach_point = kvs.remove("attach_point").unwrap_or_default();
580    let priority = kvs
581        .remove("priority")
582        .and_then(|v| v.parse::<i32>().ok())
583        .unwrap_or(0);
584    let enabled = kvs
585        .remove("enabled")
586        .and_then(|v| parse_bool(&v))
587        .unwrap_or(true);
588
589    ParsedHook {
590        name,
591        path,
592        hook_type,
593        builtin_id,
594        attach_point,
595        priority,
596        enabled,
597    }
598}
599
600fn default_hook_type() -> &'static str {
601    #[cfg(feature = "rns-hooks-native")]
602    {
603        return "native";
604    }
605    #[cfg(all(not(feature = "rns-hooks-native"), feature = "rns-hooks-wasm"))]
606    {
607        return "wasm";
608    }
609    #[cfg(all(not(feature = "rns-hooks-native"), not(feature = "rns-hooks-wasm")))]
610    {
611        "wasm"
612    }
613}
614
615/// Map a hook point name string to its index. Returns None for unknown names.
616pub fn parse_hook_point(s: &str) -> Option<usize> {
617    match s {
618        "PreIngress" => Some(0),
619        "PreDispatch" => Some(1),
620        "AnnounceReceived" => Some(2),
621        "PathUpdated" => Some(3),
622        "AnnounceRetransmit" => Some(4),
623        "LinkRequestReceived" => Some(5),
624        "LinkEstablished" => Some(6),
625        "LinkClosed" => Some(7),
626        "InterfaceUp" => Some(8),
627        "InterfaceDown" => Some(9),
628        "InterfaceConfigChanged" => Some(10),
629        "BackbonePeerConnected" => Some(11),
630        "BackbonePeerDisconnected" => Some(12),
631        "BackbonePeerIdleTimeout" => Some(13),
632        "BackbonePeerWriteStall" => Some(14),
633        "BackbonePeerPenalty" => Some(15),
634        "SendOnInterface" => Some(16),
635        "BroadcastOnAllInterfaces" => Some(17),
636        "DeliverLocal" => Some(18),
637        "TunnelSynthesize" => Some(19),
638        "Tick" => Some(20),
639        _ => None,
640    }
641}
642
643#[cfg(feature = "hooks")]
644pub fn parse_hook_backend(s: &str) -> Result<rns_hooks::HookBackend, String> {
645    s.parse()
646}
647
648fn build_reticulum_section(kvs: &HashMap<String, String>) -> Result<ReticulumSection, ConfigError> {
649    let mut section = ReticulumSection::default();
650
651    if let Some(v) = kvs.get("enable_transport") {
652        section.enable_transport = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
653            key: "enable_transport".into(),
654            value: v.clone(),
655        })?;
656    }
657    if let Some(v) = kvs.get("static_transport_identity") {
658        section.static_transport_identity =
659            parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
660                key: "static_transport_identity".into(),
661                value: v.clone(),
662            })?;
663    }
664    if let Some(v) = kvs.get("local_hops_delta") {
665        section.local_hops_delta = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
666            key: "local_hops_delta".into(),
667            value: v.clone(),
668        })?;
669    }
670    if let Some(v) = kvs.get("share_instance") {
671        section.share_instance = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
672            key: "share_instance".into(),
673            value: v.clone(),
674        })?;
675    }
676    if let Some(v) = kvs.get("instance_name") {
677        section.instance_name = v.clone();
678    }
679    if let Some(v) = kvs.get("shared_instance_port") {
680        section.shared_instance_port = v.parse::<u16>().map_err(|_| ConfigError::InvalidValue {
681            key: "shared_instance_port".into(),
682            value: v.clone(),
683        })?;
684    }
685    if let Some(v) = kvs.get("instance_control_port") {
686        section.instance_control_port =
687            v.parse::<u16>().map_err(|_| ConfigError::InvalidValue {
688                key: "instance_control_port".into(),
689                value: v.clone(),
690            })?;
691    }
692    if let Some(v) = kvs.get("panic_on_interface_error") {
693        section.panic_on_interface_error =
694            parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
695                key: "panic_on_interface_error".into(),
696                value: v.clone(),
697            })?;
698    }
699    if let Some(v) = kvs.get("use_implicit_proof") {
700        section.use_implicit_proof = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
701            key: "use_implicit_proof".into(),
702            value: v.clone(),
703        })?;
704    }
705    if let Some(v) = kvs.get("network_identity") {
706        section.network_identity = Some(v.clone());
707    }
708    if let Some(v) = kvs.get("respond_to_probes") {
709        section.respond_to_probes = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
710            key: "respond_to_probes".into(),
711            value: v.clone(),
712        })?;
713    }
714    if let Some(v) = kvs.get("enable_remote_management") {
715        section.enable_remote_management =
716            parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
717                key: "enable_remote_management".into(),
718                value: v.clone(),
719            })?;
720    }
721    if let Some(v) = kvs.get("remote_management_allowed") {
722        // Value is a comma-separated list of hex identity hashes
723        for item in v.split(',') {
724            let trimmed = item.trim();
725            if !trimmed.is_empty() {
726                section.remote_management_allowed.push(trimmed.to_string());
727            }
728        }
729    }
730    if let Some(v) = kvs.get("publish_blackhole") {
731        section.publish_blackhole = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
732            key: "publish_blackhole".into(),
733            value: v.clone(),
734        })?;
735    }
736    if let Some(v) = kvs.get("probe_port") {
737        section.probe_port = Some(v.parse::<u16>().map_err(|_| ConfigError::InvalidValue {
738            key: "probe_port".into(),
739            value: v.clone(),
740        })?);
741    }
742    if let Some(v) = kvs.get("probe_addr") {
743        section.probe_addr = Some(v.clone());
744    }
745    if let Some(v) = kvs.get("probe_protocol") {
746        section.probe_protocol = Some(v.clone());
747    }
748    if let Some(v) = kvs.get("device") {
749        section.device = Some(v.clone());
750    }
751    if let Some(v) = kvs.get("discover_interfaces") {
752        section.discover_interfaces = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
753            key: "discover_interfaces".into(),
754            value: v.clone(),
755        })?;
756    }
757    if let Some(v) = kvs.get("default_gravity") {
758        section.default_gravity = v.parse::<i64>().map_err(|_| ConfigError::InvalidValue {
759            key: "default_gravity".into(),
760            value: v.clone(),
761        })?;
762    }
763    // Retain the short-lived pre-release name as a compatibility alias. The
764    // final upstream option takes precedence when both are present.
765    if let Some(v) = kvs.get("autoconnect_discovered_mode") {
766        section.autoconnect_interface_mode = Some(v.to_lowercase());
767    }
768    if let Some(v) = kvs.get("autoconnect_interface_mode") {
769        section.autoconnect_interface_mode = Some(v.to_lowercase());
770    }
771    if let Some(v) = kvs.get("autoconnect_interface_gravity") {
772        section.autoconnect_interface_gravity =
773            v.parse::<i64>().map_err(|_| ConfigError::InvalidValue {
774                key: "autoconnect_interface_gravity".into(),
775                value: v.clone(),
776            })?;
777    }
778    if let Some(v) = kvs.get("autoconnect_announces_to_internal") {
779        section.autoconnect_announces_to_internal =
780            parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
781                key: "autoconnect_announces_to_internal".into(),
782                value: v.clone(),
783            })?;
784    }
785    if let Some(v) = kvs.get("required_discovery_value") {
786        section.required_discovery_value =
787            Some(v.parse::<u8>().map_err(|_| ConfigError::InvalidValue {
788                key: "required_discovery_value".into(),
789                value: v.clone(),
790            })?);
791    }
792    if let Some(v) = kvs.get("prefer_shorter_path") {
793        section.prefer_shorter_path = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
794            key: "prefer_shorter_path".into(),
795            value: v.clone(),
796        })?;
797    }
798    if let Some(v) = kvs.get("max_paths_per_destination") {
799        let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
800            key: "max_paths_per_destination".into(),
801            value: v.clone(),
802        })?;
803        section.max_paths_per_destination = n.max(1);
804    }
805    if let Some(v) = kvs.get("packet_hashlist_max_entries") {
806        let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
807            key: "packet_hashlist_max_entries".into(),
808            value: v.clone(),
809        })?;
810        section.packet_hashlist_max_entries = n.max(1);
811    }
812    if let Some(v) = kvs.get("max_discovery_pr_tags") {
813        let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
814            key: "max_discovery_pr_tags".into(),
815            value: v.clone(),
816        })?;
817        section.max_discovery_pr_tags = n.max(1);
818    }
819    if let Some(v) = kvs.get("max_path_destinations") {
820        let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
821            key: "max_path_destinations".into(),
822            value: v.clone(),
823        })?;
824        section.max_path_destinations = n.max(1);
825    }
826    if let Some(v) = kvs.get("max_tunnel_destinations_total") {
827        let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
828            key: "max_tunnel_destinations_total".into(),
829            value: v.clone(),
830        })?;
831        section.max_tunnel_destinations_total = n.max(1);
832    }
833    if let Some(v) = kvs.get("known_destinations_ttl") {
834        section.known_destinations_ttl =
835            v.parse::<u64>().map_err(|_| ConfigError::InvalidValue {
836                key: "known_destinations_ttl".into(),
837                value: v.clone(),
838            })?;
839    }
840    if let Some(v) = kvs.get("known_destinations_max_entries") {
841        let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
842            key: "known_destinations_max_entries".into(),
843            value: v.clone(),
844        })?;
845        if n == 0 {
846            return Err(ConfigError::InvalidValue {
847                key: "known_destinations_max_entries".into(),
848                value: v.clone(),
849            });
850        }
851        section.known_destinations_max_entries = n;
852    }
853    if let Some(v) = kvs.get("ratchet_expiry") {
854        let expiry = v.parse::<u64>().map_err(|_| ConfigError::InvalidValue {
855            key: "ratchet_expiry".into(),
856            value: v.clone(),
857        })?;
858        if expiry == 0 {
859            return Err(ConfigError::InvalidValue {
860                key: "ratchet_expiry".into(),
861                value: v.clone(),
862            });
863        }
864        section.ratchet_expiry = expiry;
865    }
866    if let Some(v) = kvs.get("destination_timeout_secs") {
867        section.known_destinations_ttl =
868            v.parse::<u64>().map_err(|_| ConfigError::InvalidValue {
869                key: "destination_timeout_secs".into(),
870                value: v.clone(),
871            })?;
872    }
873    if let Some(v) = kvs.get("announce_table_ttl") {
874        let ttl = v.parse::<u64>().map_err(|_| ConfigError::InvalidValue {
875            key: "announce_table_ttl".into(),
876            value: v.clone(),
877        })?;
878        if ttl == 0 {
879            return Err(ConfigError::InvalidValue {
880                key: "announce_table_ttl".into(),
881                value: v.clone(),
882            });
883        }
884        section.announce_table_ttl = ttl;
885    }
886    if let Some(v) = kvs.get("announce_table_max_bytes") {
887        let max_bytes = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
888            key: "announce_table_max_bytes".into(),
889            value: v.clone(),
890        })?;
891        if max_bytes == 0 {
892            return Err(ConfigError::InvalidValue {
893                key: "announce_table_max_bytes".into(),
894                value: v.clone(),
895            });
896        }
897        section.announce_table_max_bytes = max_bytes;
898    }
899    if let Some(v) = kvs.get("announce_signature_cache_enabled") {
900        section.announce_sig_cache_enabled = match v.as_str() {
901            "true" | "yes" | "True" | "Yes" => true,
902            "false" | "no" | "False" | "No" => false,
903            _ => {
904                return Err(ConfigError::InvalidValue {
905                    key: "announce_signature_cache_enabled".into(),
906                    value: v.clone(),
907                })
908            }
909        };
910    }
911    if let Some(v) = kvs.get("announce_signature_cache_max_entries") {
912        let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
913            key: "announce_signature_cache_max_entries".into(),
914            value: v.clone(),
915        })?;
916        section.announce_sig_cache_max_entries = n;
917    }
918    if let Some(v) = kvs.get("announce_signature_cache_ttl") {
919        let ttl = v.parse::<u64>().map_err(|_| ConfigError::InvalidValue {
920            key: "announce_signature_cache_ttl".into(),
921            value: v.clone(),
922        })?;
923        section.announce_sig_cache_ttl = ttl;
924    }
925    if let Some(v) = kvs.get("announce_queue_max_entries") {
926        let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
927            key: "announce_queue_max_entries".into(),
928            value: v.clone(),
929        })?;
930        if n == 0 {
931            return Err(ConfigError::InvalidValue {
932                key: "announce_queue_max_entries".into(),
933                value: v.clone(),
934            });
935        }
936        section.announce_queue_max_entries = n;
937    }
938    if let Some(v) = kvs.get("announce_queue_max_interfaces") {
939        let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
940            key: "announce_queue_max_interfaces".into(),
941            value: v.clone(),
942        })?;
943        if n == 0 {
944            return Err(ConfigError::InvalidValue {
945                key: "announce_queue_max_interfaces".into(),
946                value: v.clone(),
947            });
948        }
949        section.announce_queue_max_interfaces = n;
950    }
951    if let Some(v) = kvs.get("default_ar_target") {
952        let target = v.parse::<f64>().map_err(|_| ConfigError::InvalidValue {
953            key: "default_ar_target".into(),
954            value: v.clone(),
955        })?;
956        if !target.is_finite() || target < 0.0 {
957            return Err(ConfigError::InvalidValue {
958                key: "default_ar_target".into(),
959                value: v.clone(),
960            });
961        }
962        section.default_ar_target = if target == 0.0 { None } else { Some(target) };
963    }
964    if let Some(v) = kvs.get("default_ar_penalty") {
965        let penalty = v.parse::<f64>().map_err(|_| ConfigError::InvalidValue {
966            key: "default_ar_penalty".into(),
967            value: v.clone(),
968        })?;
969        if !penalty.is_finite() || penalty < 0.0 {
970            return Err(ConfigError::InvalidValue {
971                key: "default_ar_penalty".into(),
972                value: v.clone(),
973            });
974        }
975        section.default_ar_penalty = penalty;
976    }
977    if let Some(v) = kvs.get("default_ar_grace") {
978        let grace = v.parse::<u32>().map_err(|_| ConfigError::InvalidValue {
979            key: "default_ar_grace".into(),
980            value: v.clone(),
981        })?;
982        section.default_ar_grace = grace;
983    }
984    if let Some(v) = parse_usize_option(kvs, "ic_max_held_announces")? {
985        section.default_ic_max_held_announces = v;
986    }
987    if let Some(v) = parse_nonnegative_f64_option(kvs, "ic_burst_hold")? {
988        section.default_ic_burst_hold = v;
989    }
990    if let Some(v) = parse_nonnegative_f64_option(kvs, "ic_burst_freq_new")? {
991        section.default_ic_burst_freq_new = v;
992    }
993    if let Some(v) = parse_nonnegative_f64_option(kvs, "ic_burst_freq")? {
994        section.default_ic_burst_freq = v;
995    }
996    if let Some(v) = parse_nonnegative_f64_option(kvs, "ic_pr_burst_freq_new")? {
997        section.default_ic_pr_burst_freq_new = v;
998    }
999    if let Some(v) = parse_nonnegative_f64_option(kvs, "ic_pr_burst_freq")? {
1000        section.default_ic_pr_burst_freq = v;
1001    }
1002    if let Some(v) = parse_nonnegative_f64_option(kvs, "ic_new_time")? {
1003        section.default_ic_new_time = v;
1004    }
1005    if let Some(v) = parse_nonnegative_f64_option(kvs, "ic_burst_penalty")? {
1006        section.default_ic_burst_penalty = v;
1007    }
1008    if let Some(v) = parse_nonnegative_f64_option(kvs, "ic_held_release_interval")? {
1009        section.default_ic_held_release_interval = v;
1010    }
1011    if let Some(v) = kvs.get("egress_control") {
1012        section.default_egress_control =
1013            parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
1014                key: "egress_control".into(),
1015                value: v.clone(),
1016            })?;
1017    }
1018    if let Some(v) = parse_nonnegative_f64_option(kvs, "ec_pr_freq")? {
1019        section.default_ec_pr_freq = v;
1020    }
1021    if let Some(v) = kvs.get("announce_queue_max_bytes") {
1022        let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
1023            key: "announce_queue_max_bytes".into(),
1024            value: v.clone(),
1025        })?;
1026        if n == 0 {
1027            return Err(ConfigError::InvalidValue {
1028                key: "announce_queue_max_bytes".into(),
1029                value: v.clone(),
1030            });
1031        }
1032        section.announce_queue_max_bytes = n;
1033    }
1034    if let Some(v) = kvs.get("announce_queue_ttl") {
1035        let ttl = v.parse::<u64>().map_err(|_| ConfigError::InvalidValue {
1036            key: "announce_queue_ttl".into(),
1037            value: v.clone(),
1038        })?;
1039        if ttl == 0 {
1040            return Err(ConfigError::InvalidValue {
1041                key: "announce_queue_ttl".into(),
1042                value: v.clone(),
1043            });
1044        }
1045        section.announce_queue_ttl = ttl;
1046    }
1047    if let Some(v) = kvs.get("announce_queue_overflow_policy") {
1048        let normalized = v.to_lowercase();
1049        if normalized != "drop_newest" && normalized != "drop_oldest" && normalized != "drop_worst"
1050        {
1051            return Err(ConfigError::InvalidValue {
1052                key: "announce_queue_overflow_policy".into(),
1053                value: v.clone(),
1054            });
1055        }
1056        section.announce_queue_overflow_policy = normalized;
1057    }
1058    if let Some(v) = kvs.get("driver_event_queue_capacity") {
1059        let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
1060            key: "driver_event_queue_capacity".into(),
1061            value: v.clone(),
1062        })?;
1063        if n == 0 {
1064            return Err(ConfigError::InvalidValue {
1065                key: "driver_event_queue_capacity".into(),
1066                value: v.clone(),
1067            });
1068        }
1069        section.driver_event_queue_capacity = n;
1070    }
1071    if let Some(v) = kvs.get("interface_writer_queue_capacity") {
1072        let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
1073            key: "interface_writer_queue_capacity".into(),
1074            value: v.clone(),
1075        })?;
1076        if n == 0 {
1077            return Err(ConfigError::InvalidValue {
1078                key: "interface_writer_queue_capacity".into(),
1079                value: v.clone(),
1080            });
1081        }
1082        section.interface_writer_queue_capacity = n;
1083    }
1084    if let Some(v) = kvs.get("backbone_peer_pool_max_connected") {
1085        section.backbone_peer_pool_max_connected =
1086            v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
1087                key: "backbone_peer_pool_max_connected".into(),
1088                value: v.clone(),
1089            })?;
1090    }
1091    if let Some(v) = kvs.get("backbone_peer_pool_failure_threshold") {
1092        let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
1093            key: "backbone_peer_pool_failure_threshold".into(),
1094            value: v.clone(),
1095        })?;
1096        if n == 0 {
1097            return Err(ConfigError::InvalidValue {
1098                key: "backbone_peer_pool_failure_threshold".into(),
1099                value: v.clone(),
1100            });
1101        }
1102        section.backbone_peer_pool_failure_threshold = n;
1103    }
1104    if let Some(v) = kvs.get("backbone_peer_pool_failure_window") {
1105        section.backbone_peer_pool_failure_window =
1106            v.parse::<u64>().map_err(|_| ConfigError::InvalidValue {
1107                key: "backbone_peer_pool_failure_window".into(),
1108                value: v.clone(),
1109            })?;
1110    }
1111    if let Some(v) = kvs.get("backbone_peer_pool_cooldown") {
1112        section.backbone_peer_pool_cooldown =
1113            v.parse::<u64>().map_err(|_| ConfigError::InvalidValue {
1114                key: "backbone_peer_pool_cooldown".into(),
1115                value: v.clone(),
1116            })?;
1117    }
1118    #[cfg(feature = "hooks")]
1119    if let Some(v) = kvs.get("provider_bridge") {
1120        section.provider_bridge = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
1121            key: "provider_bridge".into(),
1122            value: v.clone(),
1123        })?;
1124    }
1125    #[cfg(feature = "hooks")]
1126    if let Some(v) = kvs.get("provider_socket_path") {
1127        section.provider_socket_path = Some(v.clone());
1128    }
1129    #[cfg(feature = "hooks")]
1130    if let Some(v) = kvs.get("provider_queue_max_events") {
1131        section.provider_queue_max_events =
1132            v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
1133                key: "provider_queue_max_events".into(),
1134                value: v.clone(),
1135            })?;
1136    }
1137    #[cfg(feature = "hooks")]
1138    if let Some(v) = kvs.get("provider_queue_max_bytes") {
1139        section.provider_queue_max_bytes =
1140            v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
1141                key: "provider_queue_max_bytes".into(),
1142                value: v.clone(),
1143            })?;
1144    }
1145    #[cfg(feature = "hooks")]
1146    if let Some(v) = kvs.get("provider_overflow_policy") {
1147        let normalized = v.to_lowercase();
1148        if normalized != "drop_newest" && normalized != "drop_oldest" {
1149            return Err(ConfigError::InvalidValue {
1150                key: "provider_overflow_policy".into(),
1151                value: v.clone(),
1152            });
1153        }
1154        section.provider_overflow_policy = normalized;
1155    }
1156
1157    Ok(section)
1158}
1159
1160fn build_logging_section(kvs: &HashMap<String, String>) -> Result<LoggingSection, ConfigError> {
1161    let mut section = LoggingSection::default();
1162
1163    if let Some(v) = kvs.get("loglevel") {
1164        section.loglevel = v.parse::<u8>().map_err(|_| ConfigError::InvalidValue {
1165            key: "loglevel".into(),
1166            value: v.clone(),
1167        })?;
1168        if section.loglevel > crate::logging::LOG_EXTREME {
1169            return Err(ConfigError::InvalidValue {
1170                key: "loglevel".into(),
1171                value: v.clone(),
1172            });
1173        }
1174    }
1175    if let Some(v) = kvs.get("logtimestamps") {
1176        section.logtimestamps = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
1177            key: "logtimestamps".into(),
1178            value: v.clone(),
1179        })?;
1180    }
1181
1182    Ok(section)
1183}
1184
1185#[cfg(test)]
1186mod tests {
1187    use super::*;
1188
1189    #[test]
1190    fn parse_empty() {
1191        let config = parse("").unwrap();
1192        assert!(!config.reticulum.enable_transport);
1193        assert!(!config.reticulum.static_transport_identity);
1194        assert!(!config.reticulum.local_hops_delta);
1195        assert!(config.reticulum.share_instance);
1196        assert_eq!(config.reticulum.instance_name, "default");
1197        assert_eq!(config.logging.loglevel, 4);
1198        assert!(config.logging.logtimestamps);
1199        assert!(config.interfaces.is_empty());
1200        assert_eq!(
1201            config.reticulum.packet_hashlist_max_entries,
1202            rns_core::constants::HASHLIST_MAXSIZE
1203        );
1204        assert_eq!(
1205            config.reticulum.announce_table_ttl,
1206            rns_core::constants::ANNOUNCE_TABLE_TTL as u64
1207        );
1208        assert_eq!(
1209            config.reticulum.announce_table_max_bytes,
1210            rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES
1211        );
1212        assert_eq!(
1213            config.reticulum.ratchet_expiry,
1214            rns_core::constants::RATCHET_EXPIRY
1215        );
1216    }
1217
1218    #[cfg(feature = "hooks")]
1219    #[test]
1220    fn parse_provider_bridge_config() {
1221        let config = parse(
1222            r#"
1223[reticulum]
1224provider_bridge = yes
1225provider_socket_path = /tmp/rns-provider.sock
1226provider_queue_max_events = 42
1227provider_queue_max_bytes = 8192
1228provider_overflow_policy = drop_oldest
1229"#,
1230        )
1231        .unwrap();
1232
1233        assert!(config.reticulum.provider_bridge);
1234        assert_eq!(
1235            config.reticulum.provider_socket_path.as_deref(),
1236            Some("/tmp/rns-provider.sock")
1237        );
1238        assert_eq!(config.reticulum.provider_queue_max_events, 42);
1239        assert_eq!(config.reticulum.provider_queue_max_bytes, 8192);
1240        assert_eq!(config.reticulum.provider_overflow_policy, "drop_oldest");
1241    }
1242
1243    #[test]
1244    fn parse_default_config() {
1245        // The default config from Python's __default_rns_config__
1246        let input = r#"
1247[reticulum]
1248enable_transport = False
1249share_instance = Yes
1250instance_name = default
1251
1252[logging]
1253loglevel = 4
1254
1255[interfaces]
1256
1257  [[Default Interface]]
1258    type = AutoInterface
1259    enabled = Yes
1260"#;
1261        let config = parse(input).unwrap();
1262        assert!(!config.reticulum.enable_transport);
1263        assert!(config.reticulum.share_instance);
1264        assert_eq!(config.reticulum.instance_name, "default");
1265        assert_eq!(config.logging.loglevel, 4);
1266        assert_eq!(config.interfaces.len(), 1);
1267        assert_eq!(config.interfaces[0].name, "Default Interface");
1268        assert_eq!(config.interfaces[0].interface_type, "AutoInterface");
1269        assert!(config.interfaces[0].enabled);
1270    }
1271
1272    #[test]
1273    fn parse_reticulum_section() {
1274        let input = r#"
1275[reticulum]
1276enable_transport = True
1277share_instance = No
1278instance_name = mynode
1279shared_instance_port = 12345
1280instance_control_port = 12346
1281panic_on_interface_error = Yes
1282use_implicit_proof = False
1283respond_to_probes = True
1284network_identity = /home/user/.reticulum/identity
1285known_destinations_ttl = 1234
1286known_destinations_max_entries = 4321
1287ratchet_expiry = 9876
1288announce_table_ttl = 45
1289announce_table_max_bytes = 65536
1290packet_hashlist_max_entries = 321
1291max_discovery_pr_tags = 222
1292max_path_destinations = 111
1293max_tunnel_destinations_total = 99
1294announce_signature_cache_enabled = false
1295announce_signature_cache_max_entries = 500
1296announce_signature_cache_ttl = 300
1297announce_queue_max_entries = 123
1298announce_queue_max_interfaces = 321
1299announce_queue_max_bytes = 4567
1300announce_queue_ttl = 89
1301announce_queue_overflow_policy = drop_oldest
1302driver_event_queue_capacity = 6543
1303interface_writer_queue_capacity = 210
1304backbone_peer_pool_max_connected = 6
1305backbone_peer_pool_failure_threshold = 4
1306backbone_peer_pool_failure_window = 120
1307backbone_peer_pool_cooldown = 300
1308"#;
1309        let config = parse(input).unwrap();
1310        assert!(config.reticulum.enable_transport);
1311        assert!(!config.reticulum.share_instance);
1312        assert_eq!(config.reticulum.instance_name, "mynode");
1313        assert_eq!(config.reticulum.shared_instance_port, 12345);
1314        assert_eq!(config.reticulum.instance_control_port, 12346);
1315        assert!(config.reticulum.panic_on_interface_error);
1316        assert!(!config.reticulum.use_implicit_proof);
1317        assert!(config.reticulum.respond_to_probes);
1318        assert_eq!(
1319            config.reticulum.network_identity.as_deref(),
1320            Some("/home/user/.reticulum/identity")
1321        );
1322        assert_eq!(config.reticulum.known_destinations_ttl, 1234);
1323        assert_eq!(config.reticulum.known_destinations_max_entries, 4321);
1324        assert_eq!(config.reticulum.ratchet_expiry, 9876);
1325        assert_eq!(config.reticulum.announce_table_ttl, 45);
1326        assert_eq!(config.reticulum.announce_table_max_bytes, 65536);
1327        assert_eq!(config.reticulum.packet_hashlist_max_entries, 321);
1328        assert_eq!(config.reticulum.max_discovery_pr_tags, 222);
1329        assert_eq!(config.reticulum.max_path_destinations, 111);
1330        assert_eq!(config.reticulum.max_tunnel_destinations_total, 99);
1331        assert!(!config.reticulum.announce_sig_cache_enabled);
1332        assert_eq!(config.reticulum.announce_sig_cache_max_entries, 500);
1333        assert_eq!(config.reticulum.announce_sig_cache_ttl, 300);
1334        assert_eq!(config.reticulum.announce_queue_max_entries, 123);
1335        assert_eq!(config.reticulum.announce_queue_max_interfaces, 321);
1336        assert_eq!(config.reticulum.announce_queue_max_bytes, 4567);
1337        assert_eq!(config.reticulum.announce_queue_ttl, 89);
1338        assert_eq!(
1339            config.reticulum.announce_queue_overflow_policy,
1340            "drop_oldest"
1341        );
1342        assert_eq!(config.reticulum.driver_event_queue_capacity, 6543);
1343        assert_eq!(config.reticulum.interface_writer_queue_capacity, 210);
1344        assert_eq!(config.reticulum.backbone_peer_pool_max_connected, 6);
1345        assert_eq!(config.reticulum.backbone_peer_pool_failure_threshold, 4);
1346        assert_eq!(config.reticulum.backbone_peer_pool_failure_window, 120);
1347        assert_eq!(config.reticulum.backbone_peer_pool_cooldown, 300);
1348    }
1349
1350    #[test]
1351    fn parse_reticulum_announce_rate_defaults() {
1352        let input = r#"
1353[reticulum]
1354default_ar_target = 7200
1355default_ar_penalty = 15
1356default_ar_grace = 7
1357"#;
1358        let config = parse(input).unwrap();
1359
1360        assert_eq!(config.reticulum.default_ar_target, Some(7200.0));
1361        assert_eq!(config.reticulum.default_ar_penalty, 15.0);
1362        assert_eq!(config.reticulum.default_ar_grace, 7);
1363    }
1364
1365    #[test]
1366    fn parse_interface_gravity_defaults_and_autoconnect_options() {
1367        let config = parse(
1368            r#"
1369[reticulum]
1370default_gravity = -3
1371autoconnect_discovered_mode = full
1372autoconnect_interface_mode = boundary
1373autoconnect_interface_gravity = 7
1374autoconnect_announces_to_internal = yes
1375"#,
1376        )
1377        .unwrap();
1378
1379        assert_eq!(
1380            config.reticulum.autoconnect_interface_mode.as_deref(),
1381            Some("boundary")
1382        );
1383        assert_eq!(config.reticulum.default_gravity, -3);
1384        assert_eq!(config.reticulum.autoconnect_interface_gravity, 7);
1385        assert!(config.reticulum.autoconnect_announces_to_internal);
1386    }
1387
1388    #[test]
1389    fn interface_gravity_options_default_to_zero() {
1390        let config = parse("[reticulum]\n").unwrap();
1391        assert_eq!(config.reticulum.default_gravity, 0);
1392        assert_eq!(config.reticulum.autoconnect_interface_gravity, 0);
1393    }
1394
1395    #[test]
1396    fn rejects_non_integer_global_gravity_options() {
1397        assert!(parse("[reticulum]\ndefault_gravity = heavy\n").is_err());
1398        assert!(parse("[reticulum]\nautoconnect_interface_gravity = light\n").is_err());
1399    }
1400
1401    #[test]
1402    fn parse_reticulum_announce_rate_target_zero_disables_default() {
1403        let input = r#"
1404[reticulum]
1405default_ar_target = 0
1406default_ar_penalty = 0
1407default_ar_grace = 0
1408"#;
1409        let config = parse(input).unwrap();
1410
1411        assert_eq!(config.reticulum.default_ar_target, None);
1412        assert_eq!(config.reticulum.default_ar_penalty, 0.0);
1413        assert_eq!(config.reticulum.default_ar_grace, 0);
1414    }
1415
1416    #[test]
1417    fn parse_reticulum_announce_rate_defaults_reject_negative_values() {
1418        for (key, value) in [
1419            ("default_ar_target", "-1"),
1420            ("default_ar_target", "NaN"),
1421            ("default_ar_target", "inf"),
1422            ("default_ar_penalty", "-1"),
1423            ("default_ar_penalty", "NaN"),
1424            ("default_ar_penalty", "inf"),
1425            ("default_ar_grace", "-1"),
1426        ] {
1427            let input = format!("[reticulum]\n{key} = {value}\n");
1428            let err = parse(&input).unwrap_err();
1429            assert!(
1430                err.to_string().contains(key),
1431                "error {err:?} should mention {key}"
1432            );
1433        }
1434    }
1435
1436    #[test]
1437    fn parse_reticulum_ingress_and_egress_control_defaults() {
1438        let input = r#"[reticulum]
1439ic_max_held_announces = 17
1440ic_burst_hold = 1.5
1441ic_burst_freq_new = 2.5
1442ic_burst_freq = 3.5
1443ic_pr_burst_freq_new = 4.5
1444ic_pr_burst_freq = 5.5
1445ic_new_time = 6.5
1446ic_burst_penalty = 7.5
1447ic_held_release_interval = 8.5
1448egress_control = Yes
1449ec_pr_freq = 9.5
1450"#;
1451
1452        let config = parse(input).unwrap();
1453
1454        assert_eq!(config.reticulum.default_ic_max_held_announces, 17);
1455        assert_eq!(config.reticulum.default_ic_burst_hold, 1.5);
1456        assert_eq!(config.reticulum.default_ic_burst_freq_new, 2.5);
1457        assert_eq!(config.reticulum.default_ic_burst_freq, 3.5);
1458        assert_eq!(config.reticulum.default_ic_pr_burst_freq_new, 4.5);
1459        assert_eq!(config.reticulum.default_ic_pr_burst_freq, 5.5);
1460        assert_eq!(config.reticulum.default_ic_new_time, 6.5);
1461        assert_eq!(config.reticulum.default_ic_burst_penalty, 7.5);
1462        assert_eq!(config.reticulum.default_ic_held_release_interval, 8.5);
1463        assert!(config.reticulum.default_egress_control);
1464        assert_eq!(config.reticulum.default_ec_pr_freq, 9.5);
1465    }
1466
1467    #[test]
1468    fn parse_reticulum_ingress_and_egress_defaults_reject_invalid_values() {
1469        for (key, value) in [
1470            ("ic_max_held_announces", "-1"),
1471            ("ic_burst_hold", "-1"),
1472            ("ic_burst_hold", "NaN"),
1473            ("ic_burst_freq_new", "-1"),
1474            ("ic_burst_freq", "inf"),
1475            ("ic_pr_burst_freq_new", "-1"),
1476            ("ic_pr_burst_freq", "NaN"),
1477            ("ic_new_time", "-1"),
1478            ("ic_burst_penalty", "-1"),
1479            ("ic_held_release_interval", "-1"),
1480            ("ec_pr_freq", "-1"),
1481            ("ec_pr_freq", "inf"),
1482        ] {
1483            let input = format!("[reticulum]\n{key} = {value}\n");
1484            let err = parse(&input).unwrap_err();
1485            assert!(
1486                format!("{err}").contains(key),
1487                "error {err} should mention {key}"
1488            );
1489        }
1490    }
1491
1492    #[test]
1493    fn parse_backbone_peer_pool_defaults_disabled() {
1494        let config = parse("[reticulum]\n").unwrap();
1495        assert_eq!(config.reticulum.backbone_peer_pool_max_connected, 0);
1496        assert_eq!(config.reticulum.backbone_peer_pool_failure_threshold, 3);
1497        assert_eq!(config.reticulum.backbone_peer_pool_failure_window, 600);
1498        assert_eq!(config.reticulum.backbone_peer_pool_cooldown, 900);
1499    }
1500
1501    #[test]
1502    fn parse_announce_table_limits_reject_zero() {
1503        let err = parse(
1504            r#"
1505[reticulum]
1506announce_table_ttl = 0
1507"#,
1508        )
1509        .unwrap_err();
1510        assert!(matches!(
1511            err,
1512            ConfigError::InvalidValue { key, .. } if key == "announce_table_ttl"
1513        ));
1514
1515        let err = parse(
1516            r#"
1517[reticulum]
1518known_destinations_max_entries = 0
1519"#,
1520        )
1521        .unwrap_err();
1522        assert!(matches!(
1523            err,
1524            ConfigError::InvalidValue { key, .. } if key == "known_destinations_max_entries"
1525        ));
1526
1527        let err = parse(
1528            r#"
1529[reticulum]
1530ratchet_expiry = 0
1531"#,
1532        )
1533        .unwrap_err();
1534        assert!(matches!(
1535            err,
1536            ConfigError::InvalidValue { key, .. } if key == "ratchet_expiry"
1537        ));
1538
1539        let err = parse(
1540            r#"
1541[reticulum]
1542announce_table_max_bytes = 0
1543"#,
1544        )
1545        .unwrap_err();
1546        assert!(matches!(
1547            err,
1548            ConfigError::InvalidValue { key, .. } if key == "announce_table_max_bytes"
1549        ));
1550
1551        let err = parse(
1552            r#"
1553[reticulum]
1554announce_queue_max_entries = 0
1555"#,
1556        )
1557        .unwrap_err();
1558        assert!(matches!(
1559            err,
1560            ConfigError::InvalidValue { key, .. } if key == "announce_queue_max_entries"
1561        ));
1562
1563        let err = parse(
1564            r#"
1565[reticulum]
1566announce_queue_max_interfaces = 0
1567"#,
1568        )
1569        .unwrap_err();
1570        assert!(matches!(
1571            err,
1572            ConfigError::InvalidValue { key, .. } if key == "announce_queue_max_interfaces"
1573        ));
1574
1575        let err = parse(
1576            r#"
1577[reticulum]
1578announce_queue_max_bytes = 0
1579"#,
1580        )
1581        .unwrap_err();
1582        assert!(matches!(
1583            err,
1584            ConfigError::InvalidValue { key, .. } if key == "announce_queue_max_bytes"
1585        ));
1586
1587        let err = parse(
1588            r#"
1589[reticulum]
1590driver_event_queue_capacity = 0
1591"#,
1592        )
1593        .unwrap_err();
1594        assert!(matches!(
1595            err,
1596            ConfigError::InvalidValue { key, .. } if key == "driver_event_queue_capacity"
1597        ));
1598
1599        let err = parse(
1600            r#"
1601[reticulum]
1602interface_writer_queue_capacity = 0
1603"#,
1604        )
1605        .unwrap_err();
1606        assert!(matches!(
1607            err,
1608            ConfigError::InvalidValue { key, .. } if key == "interface_writer_queue_capacity"
1609        ));
1610
1611        let err = parse(
1612            r#"
1613[reticulum]
1614announce_queue_ttl = 0
1615"#,
1616        )
1617        .unwrap_err();
1618        assert!(matches!(
1619            err,
1620            ConfigError::InvalidValue { key, .. } if key == "announce_queue_ttl"
1621        ));
1622    }
1623
1624    #[test]
1625    fn parse_announce_queue_overflow_policy_rejects_invalid() {
1626        let err = parse(
1627            r#"
1628[reticulum]
1629announce_queue_overflow_policy = keep_everything
1630"#,
1631        )
1632        .unwrap_err();
1633        assert!(matches!(
1634            err,
1635            ConfigError::InvalidValue { key, .. } if key == "announce_queue_overflow_policy"
1636        ));
1637    }
1638
1639    #[test]
1640    fn parse_destination_timeout_secs_alias() {
1641        let config = parse(
1642            r#"
1643[reticulum]
1644destination_timeout_secs = 777
1645"#,
1646        )
1647        .unwrap();
1648
1649        assert_eq!(config.reticulum.known_destinations_ttl, 777);
1650    }
1651
1652    #[test]
1653    fn parse_logging_section() {
1654        let input = "[logging]\nloglevel = 6\nlogtimestamps = no\n";
1655        let config = parse(input).unwrap();
1656        assert_eq!(config.logging.loglevel, 6);
1657        assert!(!config.logging.logtimestamps);
1658    }
1659
1660    #[test]
1661    fn parse_logging_accepts_pathing_and_extreme_levels() {
1662        for level in [7, 8] {
1663            let config = parse(&format!("[logging]\nloglevel = {level}\n")).unwrap();
1664            assert_eq!(config.logging.loglevel, level);
1665        }
1666    }
1667
1668    #[test]
1669    fn parse_logging_rejects_levels_above_extreme() {
1670        for value in ["9", "255", "256", "-1"] {
1671            let err = parse(&format!("[logging]\nloglevel = {value}\n")).unwrap_err();
1672            assert!(matches!(
1673                err,
1674                ConfigError::InvalidValue { key, .. } if key == "loglevel"
1675            ));
1676        }
1677    }
1678
1679    #[test]
1680    fn parse_logging_rejects_invalid_logtimestamps() {
1681        let err = parse("[logging]\nlogtimestamps = maybe\n").unwrap_err();
1682        assert!(matches!(
1683            err,
1684            ConfigError::InvalidValue { key, .. } if key == "logtimestamps"
1685        ));
1686    }
1687
1688    #[test]
1689    fn parse_interface_tcp_client() {
1690        let input = r#"
1691[interfaces]
1692  [[TCP Client]]
1693    type = TCPClientInterface
1694    enabled = Yes
1695    target_host = 87.106.8.245
1696    target_port = 4242
1697"#;
1698        let config = parse(input).unwrap();
1699        assert_eq!(config.interfaces.len(), 1);
1700        let iface = &config.interfaces[0];
1701        assert_eq!(iface.name, "TCP Client");
1702        assert_eq!(iface.interface_type, "TCPClientInterface");
1703        assert!(iface.enabled);
1704        assert_eq!(iface.params.get("target_host").unwrap(), "87.106.8.245");
1705        assert_eq!(iface.params.get("target_port").unwrap(), "4242");
1706    }
1707
1708    #[test]
1709    fn parse_interface_tcp_server() {
1710        let input = r#"
1711[interfaces]
1712  [[TCP Server]]
1713    type = TCPServerInterface
1714    enabled = Yes
1715    listen_ip = 0.0.0.0
1716    listen_port = 4242
1717"#;
1718        let config = parse(input).unwrap();
1719        assert_eq!(config.interfaces.len(), 1);
1720        let iface = &config.interfaces[0];
1721        assert_eq!(iface.name, "TCP Server");
1722        assert_eq!(iface.interface_type, "TCPServerInterface");
1723        assert_eq!(iface.params.get("listen_ip").unwrap(), "0.0.0.0");
1724        assert_eq!(iface.params.get("listen_port").unwrap(), "4242");
1725    }
1726
1727    #[test]
1728    fn parse_interface_udp() {
1729        let input = r#"
1730[interfaces]
1731  [[UDP Interface]]
1732    type = UDPInterface
1733    enabled = Yes
1734    listen_ip = 0.0.0.0
1735    listen_port = 4242
1736    forward_ip = 255.255.255.255
1737    forward_port = 4242
1738"#;
1739        let config = parse(input).unwrap();
1740        assert_eq!(config.interfaces.len(), 1);
1741        let iface = &config.interfaces[0];
1742        assert_eq!(iface.name, "UDP Interface");
1743        assert_eq!(iface.interface_type, "UDPInterface");
1744        assert_eq!(iface.params.get("listen_ip").unwrap(), "0.0.0.0");
1745        assert_eq!(iface.params.get("forward_ip").unwrap(), "255.255.255.255");
1746    }
1747
1748    #[test]
1749    fn parse_multiple_interfaces() {
1750        let input = r#"
1751[interfaces]
1752  [[TCP Client]]
1753    type = TCPClientInterface
1754    target_host = 10.0.0.1
1755    target_port = 4242
1756
1757  [[UDP Broadcast]]
1758    type = UDPInterface
1759    listen_ip = 0.0.0.0
1760    listen_port = 5555
1761    forward_ip = 255.255.255.255
1762    forward_port = 5555
1763"#;
1764        let config = parse(input).unwrap();
1765        assert_eq!(config.interfaces.len(), 2);
1766        assert_eq!(config.interfaces[0].name, "TCP Client");
1767        assert_eq!(config.interfaces[0].interface_type, "TCPClientInterface");
1768        assert_eq!(config.interfaces[1].name, "UDP Broadcast");
1769        assert_eq!(config.interfaces[1].interface_type, "UDPInterface");
1770    }
1771
1772    #[test]
1773    fn parse_third_level_interface_sections() {
1774        let config = parse(
1775            "[interfaces]\n[[radio]]\n  type = RNodeMultiInterface\n  port = /dev/ttyUSB0\n  [[[fast]]]\n    enabled = yes\n    vport = 7\n    frequency = 915000000\n  [[[disabled]]]\n    enabled = no\n    vport = 2\n",
1776        )
1777        .unwrap();
1778        assert_eq!(config.interfaces.len(), 1);
1779        let interface = &config.interfaces[0];
1780        assert_eq!(interface.params["port"], "/dev/ttyUSB0");
1781        assert_eq!(interface.subinterfaces.len(), 2);
1782        assert_eq!(interface.subinterfaces[0].name, "fast");
1783        assert_eq!(interface.subinterfaces[0].params["vport"], "7");
1784    }
1785
1786    #[test]
1787    fn parse_booleans() {
1788        // Test all boolean variants
1789        for (input, expected) in &[
1790            ("Yes", true),
1791            ("No", false),
1792            ("True", true),
1793            ("False", false),
1794            ("true", true),
1795            ("false", false),
1796            ("1", true),
1797            ("0", false),
1798            ("on", true),
1799            ("off", false),
1800        ] {
1801            let result = parse_bool(input);
1802            assert_eq!(result, Some(*expected), "parse_bool({}) failed", input);
1803        }
1804    }
1805
1806    #[test]
1807    fn parse_comments() {
1808        let input = r#"
1809# This is a comment
1810[reticulum]
1811enable_transport = True  # inline comment
1812# share_instance = No
1813instance_name = test
1814"#;
1815        let config = parse(input).unwrap();
1816        assert!(config.reticulum.enable_transport);
1817        assert!(config.reticulum.share_instance); // commented out line should be ignored
1818        assert_eq!(config.reticulum.instance_name, "test");
1819    }
1820
1821    #[test]
1822    fn parse_interface_mode_field() {
1823        let input = r#"
1824[interfaces]
1825  [[TCP Client]]
1826    type = TCPClientInterface
1827    interface_mode = access_point
1828    target_host = 10.0.0.1
1829    target_port = 4242
1830"#;
1831        let config = parse(input).unwrap();
1832        assert_eq!(config.interfaces[0].mode, "access_point");
1833    }
1834
1835    #[test]
1836    fn parse_mode_fallback() {
1837        // Python also accepts "mode" as fallback for "interface_mode"
1838        let input = r#"
1839[interfaces]
1840  [[TCP Client]]
1841    type = TCPClientInterface
1842    mode = gateway
1843    target_host = 10.0.0.1
1844    target_port = 4242
1845"#;
1846        let config = parse(input).unwrap();
1847        assert_eq!(config.interfaces[0].mode, "gateway");
1848    }
1849
1850    #[test]
1851    fn parse_interface_mode_takes_precedence() {
1852        // If both interface_mode and mode are set, interface_mode wins
1853        let input = r#"
1854[interfaces]
1855  [[TCP Client]]
1856    type = TCPClientInterface
1857    interface_mode = roaming
1858    mode = boundary
1859    target_host = 10.0.0.1
1860    target_port = 4242
1861"#;
1862        let config = parse(input).unwrap();
1863        assert_eq!(config.interfaces[0].mode, "roaming");
1864    }
1865
1866    #[test]
1867    fn parse_disabled_interface() {
1868        let input = r#"
1869[interfaces]
1870  [[Disabled TCP]]
1871    type = TCPClientInterface
1872    enabled = No
1873    target_host = 10.0.0.1
1874    target_port = 4242
1875"#;
1876        let config = parse(input).unwrap();
1877        assert_eq!(config.interfaces.len(), 1);
1878        assert!(!config.interfaces[0].enabled);
1879    }
1880
1881    #[test]
1882    fn parse_serial_interface() {
1883        let input = r#"
1884[interfaces]
1885  [[Serial Port]]
1886    type = SerialInterface
1887    enabled = Yes
1888    port = /dev/ttyUSB0
1889    speed = 115200
1890    databits = 8
1891    parity = N
1892    stopbits = 1
1893"#;
1894        let config = parse(input).unwrap();
1895        assert_eq!(config.interfaces.len(), 1);
1896        let iface = &config.interfaces[0];
1897        assert_eq!(iface.name, "Serial Port");
1898        assert_eq!(iface.interface_type, "SerialInterface");
1899        assert!(iface.enabled);
1900        assert_eq!(iface.params.get("port").unwrap(), "/dev/ttyUSB0");
1901        assert_eq!(iface.params.get("speed").unwrap(), "115200");
1902        assert_eq!(iface.params.get("databits").unwrap(), "8");
1903        assert_eq!(iface.params.get("parity").unwrap(), "N");
1904        assert_eq!(iface.params.get("stopbits").unwrap(), "1");
1905    }
1906
1907    #[test]
1908    fn parse_kiss_interface() {
1909        let input = r#"
1910[interfaces]
1911  [[KISS TNC]]
1912    type = KISSInterface
1913    enabled = Yes
1914    port = /dev/ttyUSB1
1915    speed = 9600
1916    preamble = 350
1917    txtail = 20
1918    persistence = 64
1919    slottime = 20
1920    flow_control = True
1921    id_interval = 600
1922    id_callsign = MYCALL
1923"#;
1924        let config = parse(input).unwrap();
1925        assert_eq!(config.interfaces.len(), 1);
1926        let iface = &config.interfaces[0];
1927        assert_eq!(iface.name, "KISS TNC");
1928        assert_eq!(iface.interface_type, "KISSInterface");
1929        assert_eq!(iface.params.get("port").unwrap(), "/dev/ttyUSB1");
1930        assert_eq!(iface.params.get("speed").unwrap(), "9600");
1931        assert_eq!(iface.params.get("preamble").unwrap(), "350");
1932        assert_eq!(iface.params.get("txtail").unwrap(), "20");
1933        assert_eq!(iface.params.get("persistence").unwrap(), "64");
1934        assert_eq!(iface.params.get("slottime").unwrap(), "20");
1935        assert_eq!(iface.params.get("flow_control").unwrap(), "True");
1936        assert_eq!(iface.params.get("id_interval").unwrap(), "600");
1937        assert_eq!(iface.params.get("id_callsign").unwrap(), "MYCALL");
1938    }
1939
1940    #[test]
1941    fn parse_ifac_networkname() {
1942        let input = r#"
1943[interfaces]
1944  [[TCP Client]]
1945    type = TCPClientInterface
1946    target_host = 10.0.0.1
1947    target_port = 4242
1948    networkname = testnet
1949"#;
1950        let config = parse(input).unwrap();
1951        assert_eq!(
1952            config.interfaces[0].params.get("networkname").unwrap(),
1953            "testnet"
1954        );
1955    }
1956
1957    #[test]
1958    fn parse_ifac_passphrase() {
1959        let input = r#"
1960[interfaces]
1961  [[TCP Client]]
1962    type = TCPClientInterface
1963    target_host = 10.0.0.1
1964    target_port = 4242
1965    passphrase = secret123
1966    ifac_size = 64
1967"#;
1968        let config = parse(input).unwrap();
1969        assert_eq!(
1970            config.interfaces[0].params.get("passphrase").unwrap(),
1971            "secret123"
1972        );
1973        assert_eq!(config.interfaces[0].params.get("ifac_size").unwrap(), "64");
1974    }
1975
1976    #[test]
1977    fn parse_remote_management_config() {
1978        let input = r#"
1979[reticulum]
1980enable_transport = True
1981enable_remote_management = Yes
1982remote_management_allowed = aabbccdd00112233aabbccdd00112233, 11223344556677881122334455667788
1983publish_blackhole = Yes
1984"#;
1985        let config = parse(input).unwrap();
1986        assert!(config.reticulum.enable_remote_management);
1987        assert!(config.reticulum.publish_blackhole);
1988        assert_eq!(config.reticulum.remote_management_allowed.len(), 2);
1989        assert_eq!(
1990            config.reticulum.remote_management_allowed[0],
1991            "aabbccdd00112233aabbccdd00112233"
1992        );
1993        assert_eq!(
1994            config.reticulum.remote_management_allowed[1],
1995            "11223344556677881122334455667788"
1996        );
1997    }
1998
1999    #[test]
2000    fn parse_remote_management_defaults() {
2001        let input = "[reticulum]\n";
2002        let config = parse(input).unwrap();
2003        assert!(!config.reticulum.enable_remote_management);
2004        assert!(!config.reticulum.publish_blackhole);
2005        assert!(config.reticulum.remote_management_allowed.is_empty());
2006    }
2007
2008    #[test]
2009    fn parse_hooks_section() {
2010        let input = r#"
2011[hooks]
2012  [[drop_tick]]
2013    path = /tmp/drop_tick.wasm
2014    attach_point = Tick
2015    priority = 10
2016    enabled = Yes
2017
2018  [[log_announce]]
2019    path = /tmp/log_announce.wasm
2020    type = native
2021    attach_point = AnnounceReceived
2022    priority = 5
2023    enabled = No
2024
2025  [[builtin_tick]]
2026    builtin = example.tick
2027    type = builtin
2028    attach_point = Tick
2029"#;
2030        let config = parse(input).unwrap();
2031        assert_eq!(config.hooks.len(), 3);
2032        assert_eq!(config.hooks[0].name, "drop_tick");
2033        assert_eq!(config.hooks[0].path, "/tmp/drop_tick.wasm");
2034        assert_eq!(config.hooks[0].hook_type, default_hook_type());
2035        assert_eq!(config.hooks[0].attach_point, "Tick");
2036        assert_eq!(config.hooks[0].priority, 10);
2037        assert!(config.hooks[0].enabled);
2038        assert_eq!(config.hooks[1].name, "log_announce");
2039        assert_eq!(config.hooks[1].hook_type, "native");
2040        assert_eq!(config.hooks[1].attach_point, "AnnounceReceived");
2041        assert!(!config.hooks[1].enabled);
2042        assert_eq!(config.hooks[2].hook_type, "builtin");
2043        assert_eq!(config.hooks[2].builtin_id.as_deref(), Some("example.tick"));
2044    }
2045
2046    #[test]
2047    fn parse_empty_hooks() {
2048        let input = "[hooks]\n";
2049        let config = parse(input).unwrap();
2050        assert!(config.hooks.is_empty());
2051    }
2052
2053    #[test]
2054    fn parse_hook_point_names() {
2055        assert_eq!(parse_hook_point("PreIngress"), Some(0));
2056        assert_eq!(parse_hook_point("PreDispatch"), Some(1));
2057        assert_eq!(parse_hook_point("AnnounceReceived"), Some(2));
2058        assert_eq!(parse_hook_point("PathUpdated"), Some(3));
2059        assert_eq!(parse_hook_point("AnnounceRetransmit"), Some(4));
2060        assert_eq!(parse_hook_point("LinkRequestReceived"), Some(5));
2061        assert_eq!(parse_hook_point("LinkEstablished"), Some(6));
2062        assert_eq!(parse_hook_point("LinkClosed"), Some(7));
2063        assert_eq!(parse_hook_point("InterfaceUp"), Some(8));
2064        assert_eq!(parse_hook_point("InterfaceDown"), Some(9));
2065        assert_eq!(parse_hook_point("InterfaceConfigChanged"), Some(10));
2066        assert_eq!(parse_hook_point("BackbonePeerConnected"), Some(11));
2067        assert_eq!(parse_hook_point("BackbonePeerDisconnected"), Some(12));
2068        assert_eq!(parse_hook_point("BackbonePeerIdleTimeout"), Some(13));
2069        assert_eq!(parse_hook_point("BackbonePeerWriteStall"), Some(14));
2070        assert_eq!(parse_hook_point("BackbonePeerPenalty"), Some(15));
2071        assert_eq!(parse_hook_point("SendOnInterface"), Some(16));
2072        assert_eq!(parse_hook_point("BroadcastOnAllInterfaces"), Some(17));
2073        assert_eq!(parse_hook_point("DeliverLocal"), Some(18));
2074        assert_eq!(parse_hook_point("TunnelSynthesize"), Some(19));
2075        assert_eq!(parse_hook_point("Tick"), Some(20));
2076        assert_eq!(parse_hook_point("Unknown"), None);
2077    }
2078
2079    #[test]
2080    fn backbone_extra_params_preserved() {
2081        let config = r#"
2082[reticulum]
2083enable_transport = True
2084
2085[interfaces]
2086  [[Public Entrypoint]]
2087    type = BackboneInterface
2088    enabled = yes
2089    listen_ip = 0.0.0.0
2090    listen_port = 4242
2091    interface_mode = gateway
2092    discoverable = Yes
2093    discovery_name = PizzaSpaghettiMandolino
2094    announce_interval = 600
2095    discovery_stamp_value = 24
2096    reachable_on = 87.106.8.245
2097"#;
2098        let parsed = parse(config).unwrap();
2099        assert_eq!(parsed.interfaces.len(), 1);
2100        let iface = &parsed.interfaces[0];
2101        assert_eq!(iface.name, "Public Entrypoint");
2102        assert_eq!(iface.interface_type, "BackboneInterface");
2103        // After removing type, enabled, interface_mode, remaining params should include discovery keys
2104        assert_eq!(
2105            iface.params.get("discoverable").map(|s| s.as_str()),
2106            Some("Yes")
2107        );
2108        assert_eq!(
2109            iface.params.get("discovery_name").map(|s| s.as_str()),
2110            Some("PizzaSpaghettiMandolino")
2111        );
2112        assert_eq!(
2113            iface.params.get("announce_interval").map(|s| s.as_str()),
2114            Some("600")
2115        );
2116        assert_eq!(
2117            iface
2118                .params
2119                .get("discovery_stamp_value")
2120                .map(|s| s.as_str()),
2121            Some("24")
2122        );
2123        assert_eq!(
2124            iface.params.get("reachable_on").map(|s| s.as_str()),
2125            Some("87.106.8.245")
2126        );
2127        assert_eq!(
2128            iface.params.get("listen_ip").map(|s| s.as_str()),
2129            Some("0.0.0.0")
2130        );
2131        assert_eq!(
2132            iface.params.get("listen_port").map(|s| s.as_str()),
2133            Some("4242")
2134        );
2135    }
2136
2137    #[test]
2138    fn parse_probe_protocol() {
2139        let input = r#"
2140[reticulum]
2141probe_addr = 1.2.3.4:19302
2142probe_protocol = stun
2143"#;
2144        let config = parse(input).unwrap();
2145        assert_eq!(
2146            config.reticulum.probe_addr.as_deref(),
2147            Some("1.2.3.4:19302")
2148        );
2149        assert_eq!(config.reticulum.probe_protocol.as_deref(), Some("stun"));
2150    }
2151
2152    #[test]
2153    fn parse_probe_protocol_defaults_to_none() {
2154        let input = r#"
2155[reticulum]
2156probe_addr = 1.2.3.4:4343
2157"#;
2158        let config = parse(input).unwrap();
2159        assert_eq!(config.reticulum.probe_addr.as_deref(), Some("1.2.3.4:4343"));
2160        assert!(config.reticulum.probe_protocol.is_none());
2161    }
2162}