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