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