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