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