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