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