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