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