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