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