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