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