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