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