1use std::collections::HashMap;
8use std::fmt;
9use std::io;
10use std::path::Path;
11
12use rns_core::stamp::StampCost;
13
14#[derive(Debug, Clone)]
16pub struct RnsConfig {
17 pub reticulum: ReticulumSection,
18 pub logging: LoggingSection,
19 pub stamps: StampsSection,
20 pub filter: FilterSection,
21 pub interfaces: Vec<ParsedInterface>,
22 pub hooks: Vec<ParsedHook>,
23}
24
25#[derive(Debug, Clone)]
27pub struct ParsedHook {
28 pub name: String,
29 pub path: String,
30 pub hook_type: String,
31 pub builtin_id: Option<String>,
32 pub attach_point: String,
33 pub priority: i32,
34 pub enabled: bool,
35}
36
37#[derive(Debug, Clone)]
39pub struct ReticulumSection {
40 pub enable_transport: bool,
41 pub hop_limiter: u8,
45 pub static_transport_identity: bool,
46 pub local_hops_delta: bool,
47 pub share_instance: bool,
48 pub enable_interface_management: bool,
50 pub instance_name: String,
51 pub shared_instance_port: u16,
52 pub instance_control_port: u16,
53 pub panic_on_interface_error: bool,
54 pub use_implicit_proof: bool,
55 pub network_identity: Option<String>,
56 pub respond_to_probes: bool,
57 pub enable_remote_management: bool,
58 pub remote_management_allowed: Vec<String>,
59 pub publish_blackhole: bool,
60 pub probe_port: Option<u16>,
61 pub probe_addr: Option<String>,
62 pub probe_protocol: Option<String>,
64 pub direct_connect_policy: crate::event::HolePunchPolicy,
66 pub direct_connect_allow_private_endpoints: bool,
69 pub device: Option<String>,
71 pub underlay_mark: Option<u32>,
73 pub discover_interfaces: bool,
76 pub default_gravity: i64,
78 pub autoconnect_interface_mode: Option<String>,
80 pub autoconnect_interface_gravity: i64,
82 pub autoconnect_announces_to_internal: bool,
84 pub autoconnect_unverified_implementations: bool,
86 pub required_discovery_value: Option<StampCost>,
89 pub prefer_shorter_path: bool,
92 pub max_paths_per_destination: usize,
95 pub packet_hashlist_max_entries: usize,
97 pub packet_hashlist_allocation: rns_core::transport::types::PacketHashlistAllocation,
99 pub max_discovery_pr_tags: usize,
101 pub max_path_destinations: usize,
103 pub max_tunnel_destinations_total: usize,
105 pub known_destinations_ttl: u64,
107 pub known_destinations_max_entries: usize,
109 pub ratchet_expiry: u64,
111 pub announce_table_ttl: u64,
113 pub announce_table_max_bytes: usize,
115 pub announce_sig_cache_enabled: bool,
117 pub announce_sig_cache_max_entries: usize,
119 pub announce_sig_cache_ttl: u64,
121 pub announce_queue_max_entries: usize,
123 pub announce_queue_max_interfaces: usize,
125 pub default_ar_target: Option<f64>,
127 pub default_ar_penalty: f64,
129 pub default_ar_grace: u32,
131 pub default_ic_max_held_announces: usize,
133 pub default_ic_burst_hold: f64,
135 pub default_ic_burst_freq_new: f64,
137 pub default_ic_burst_freq: f64,
139 pub default_ic_pr_burst_freq_new: f64,
141 pub default_ic_pr_burst_freq: f64,
143 pub default_ic_new_time: f64,
145 pub default_ic_burst_penalty: f64,
147 pub default_ic_held_release_interval: f64,
149 pub default_egress_control: bool,
151 pub default_ec_pr_freq: f64,
153 pub announce_queue_max_bytes: usize,
155 pub announce_queue_ttl: u64,
157 pub announce_queue_overflow_policy: String,
159 pub driver_event_queue_capacity: usize,
161 pub inbound_queue_capacities: crate::event::InboundQueueCapacities,
163 pub interface_writer_queue_capacity: usize,
165 pub backbone_peer_pool_max_connected: usize,
167 pub backbone_peer_pool_failure_threshold: usize,
172 pub backbone_peer_pool_failure_window: u64,
174 pub backbone_peer_pool_cooldown: u64,
176 #[cfg(feature = "hooks")]
177 pub provider_bridge: bool,
178 #[cfg(feature = "hooks")]
179 pub provider_socket_path: Option<String>,
180 #[cfg(feature = "hooks")]
181 pub provider_queue_max_events: usize,
182 #[cfg(feature = "hooks")]
183 pub provider_queue_max_bytes: usize,
184 #[cfg(feature = "hooks")]
185 pub provider_overflow_policy: String,
186}
187
188impl Default for ReticulumSection {
189 fn default() -> Self {
190 ReticulumSection {
191 enable_transport: false,
192 hop_limiter: rns_core::constants::HOP_LIMITER,
193 static_transport_identity: false,
194 local_hops_delta: false,
195 share_instance: true,
196 enable_interface_management: true,
197 instance_name: "default".into(),
198 shared_instance_port: 37428,
199 instance_control_port: 37429,
200 panic_on_interface_error: false,
201 use_implicit_proof: true,
202 network_identity: None,
203 respond_to_probes: false,
204 enable_remote_management: false,
205 remote_management_allowed: Vec::new(),
206 publish_blackhole: false,
207 probe_port: None,
208 probe_addr: None,
209 probe_protocol: None,
210 direct_connect_policy: crate::event::HolePunchPolicy::IdentifiedOnly,
211 direct_connect_allow_private_endpoints: false,
212 device: None,
213 underlay_mark: None,
214 discover_interfaces: false,
215 default_gravity: 0,
216 autoconnect_interface_mode: None,
217 autoconnect_interface_gravity: 0,
218 autoconnect_announces_to_internal: false,
219 autoconnect_unverified_implementations: false,
220 required_discovery_value: None,
221 prefer_shorter_path: false,
222 max_paths_per_destination: 1,
223 packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
224 packet_hashlist_allocation: rns_core::transport::types::PacketHashlistAllocation::Lazy,
225 max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
226 max_path_destinations: rns_core::transport::types::DEFAULT_MAX_PATH_DESTINATIONS,
227 max_tunnel_destinations_total: usize::MAX,
228 known_destinations_ttl: 48 * 60 * 60,
229 known_destinations_max_entries: 8192,
230 ratchet_expiry: rns_core::constants::RATCHET_EXPIRY,
231 announce_table_ttl: rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
232 announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
233 announce_sig_cache_enabled: true,
234 announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
235 announce_sig_cache_ttl: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
236 announce_queue_max_entries: 256,
237 announce_queue_max_interfaces: 1024,
238 default_ar_target: Some(3600.0),
239 default_ar_penalty: 0.0,
240 default_ar_grace: 5,
241 default_ic_max_held_announces: rns_core::constants::IC_MAX_HELD_ANNOUNCES,
242 default_ic_burst_hold: rns_core::constants::IC_BURST_HOLD,
243 default_ic_burst_freq_new: rns_core::constants::IC_BURST_FREQ_NEW,
244 default_ic_burst_freq: rns_core::constants::IC_BURST_FREQ,
245 default_ic_pr_burst_freq_new: rns_core::constants::IC_PR_BURST_FREQ_NEW,
246 default_ic_pr_burst_freq: rns_core::constants::IC_PR_BURST_FREQ,
247 default_ic_new_time: rns_core::constants::IC_NEW_TIME,
248 default_ic_burst_penalty: rns_core::constants::IC_BURST_PENALTY,
249 default_ic_held_release_interval: rns_core::constants::IC_HELD_RELEASE_INTERVAL,
250 default_egress_control: false,
251 default_ec_pr_freq: rns_core::constants::EC_PR_FREQ,
252 announce_queue_max_bytes: 256 * 1024,
253 announce_queue_ttl: 30,
254 announce_queue_overflow_policy: "drop_worst".into(),
255 driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
256 inbound_queue_capacities: crate::event::InboundQueueCapacities::default(),
257 interface_writer_queue_capacity: crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
258 backbone_peer_pool_max_connected: 0,
259 backbone_peer_pool_failure_threshold: 3,
260 backbone_peer_pool_failure_window: 600,
261 backbone_peer_pool_cooldown: 900,
262 #[cfg(feature = "hooks")]
263 provider_bridge: false,
264 #[cfg(feature = "hooks")]
265 provider_socket_path: None,
266 #[cfg(feature = "hooks")]
267 provider_queue_max_events: 16384,
268 #[cfg(feature = "hooks")]
269 provider_queue_max_bytes: 8 * 1024 * 1024,
270 #[cfg(feature = "hooks")]
271 provider_overflow_policy: "drop_newest".into(),
272 }
273 }
274}
275
276#[derive(Debug, Clone, PartialEq)]
284pub struct StampsSection {
285 pub stamps_enforced: bool,
287 pub lr_stamps_enforced: bool,
289 pub stamp_destinations: bool,
291 pub stamp_prs: bool,
293 pub stamp_lrs: bool,
297 pub always_stamp_lrs: bool,
299 pub destination_cost: StampCost,
301 pub destination_value: StampCost,
303 pub pr_cost: StampCost,
305 pub pr_value: StampCost,
307 pub lr_cost: StampCost,
309 pub lr_stamp_timeout: f64,
311 pub pr_stamp_timeout: f64,
313}
314
315#[derive(Debug, Clone, Default, PartialEq, Eq)]
320pub struct FilterSection {
321 pub required_only: bool,
323 pub ignored_types: Vec<String>,
326 pub allowed_types: Vec<String>,
329}
330
331impl FilterSection {
332 pub fn destination_filter(
334 &self,
335 ) -> rns_core::transport::destination_filter::DestinationTypeFilter {
336 use rns_core::transport::destination_filter::{destination_type, DestinationTypeFilter};
337 let mut filter = DestinationTypeFilter::default();
338 filter.set_required_only(self.required_only);
339 for name in &self.ignored_types {
340 filter.block(destination_type(name));
341 }
342 for name in &self.allowed_types {
343 filter.allow(destination_type(name));
344 }
345 filter
346 }
347}
348
349impl Default for StampsSection {
350 fn default() -> Self {
351 use rns_core::constants;
352 StampsSection {
353 stamps_enforced: constants::STAMPS_ENFORCED,
354 lr_stamps_enforced: constants::LR_STAMPS_ENFORCED,
355 stamp_destinations: constants::STAMP_DESTINATIONS,
356 stamp_prs: constants::STAMP_PRS,
357 stamp_lrs: constants::STAMP_LRS,
358 always_stamp_lrs: constants::ALWAYS_STAMP_LRS,
359 destination_cost: constants::DESTINATION_STAMP_COST,
360 destination_value: constants::DESTINATION_STAMP_VALUE,
361 pr_cost: constants::PATH_REQUEST_STAMP_COST,
362 pr_value: constants::PATH_REQUEST_STAMP_VALUE,
363 lr_cost: constants::LINK_REQUEST_STAMP_COST,
364 lr_stamp_timeout: constants::LINK_STAMP_TIMEOUT as f64,
365 pr_stamp_timeout: constants::PATH_REQUEST_STAMP_TIMEOUT as f64,
366 }
367 }
368}
369
370impl StampsSection {
371 pub fn requirements(&self) -> rns_core::transport::types::StampRequirements {
373 rns_core::transport::types::StampRequirements {
374 enforced: self.stamps_enforced,
375 destination_cost: self.destination_cost,
376 path_request_cost: self.pr_cost,
377 }
378 }
379
380 pub fn link_stamp_timeout(&self) -> std::time::Duration {
382 stamp_timeout_duration(self.lr_stamp_timeout)
383 }
384
385 pub fn path_request_stamp_timeout(&self) -> std::time::Duration {
387 stamp_timeout_duration(self.pr_stamp_timeout)
388 }
389}
390
391fn stamp_timeout_duration(seconds: f64) -> std::time::Duration {
393 std::time::Duration::try_from_secs_f64(seconds).unwrap_or(std::time::Duration::MAX)
394}
395
396#[derive(Debug, Clone)]
398pub struct LoggingSection {
399 pub loglevel: u8,
400 pub logtimestamps: bool,
401}
402
403impl Default for LoggingSection {
404 fn default() -> Self {
405 LoggingSection {
406 loglevel: 4,
407 logtimestamps: true,
408 }
409 }
410}
411
412#[derive(Debug, Clone)]
414pub struct ParsedInterface {
415 pub name: String,
416 pub interface_type: String,
417 pub enabled: bool,
418 pub mode: String,
419 pub params: HashMap<String, String>,
420 pub subinterfaces: Vec<ParsedSubinterface>,
421}
422
423#[derive(Debug, Clone, PartialEq, Eq)]
425pub struct ParsedSubinterface {
426 pub name: String,
427 pub params: HashMap<String, String>,
428}
429
430#[derive(Debug, Clone)]
432pub enum ConfigError {
433 Io(String),
434 Parse(String),
435 InvalidValue { key: String, value: String },
436}
437
438impl fmt::Display for ConfigError {
439 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
440 match self {
441 ConfigError::Io(msg) => write!(f, "Config I/O error: {}", msg),
442 ConfigError::Parse(msg) => write!(f, "Config parse error: {}", msg),
443 ConfigError::InvalidValue { key, value } => {
444 write!(f, "Invalid value for '{}': '{}'", key, value)
445 }
446 }
447 }
448}
449
450impl From<io::Error> for ConfigError {
451 fn from(e: io::Error) -> Self {
452 ConfigError::Io(e.to_string())
453 }
454}
455
456pub fn parse(input: &str) -> Result<RnsConfig, ConfigError> {
458 let mut current_section: Option<String> = None;
459 let mut current_subsection: Option<String> = None;
460
461 let mut reticulum_kvs: HashMap<String, String> = HashMap::new();
462 let mut logging_kvs: HashMap<String, String> = HashMap::new();
463 let mut stamps_kvs: HashMap<String, String> = HashMap::new();
464 let mut filter_kvs: HashMap<String, String> = HashMap::new();
465 let mut interfaces: Vec<ParsedInterface> = Vec::new();
466 let mut current_iface_kvs: Option<HashMap<String, String>> = None;
467 let mut current_iface_name: Option<String> = None;
468 let mut current_iface_subinterfaces: Vec<ParsedSubinterface> = Vec::new();
469 let mut current_subiface_name: Option<String> = None;
470 let mut current_subiface_kvs: Option<HashMap<String, String>> = None;
471 let mut hooks: Vec<ParsedHook> = Vec::new();
472 let mut current_hook_kvs: Option<HashMap<String, String>> = None;
473 let mut current_hook_name: Option<String> = None;
474
475 for line in input.lines() {
476 let line = strip_comment(line);
478 let trimmed = line.trim();
479
480 if trimmed.is_empty() {
482 continue;
483 }
484
485 if trimmed.starts_with("[[[") && trimmed.ends_with("]]]") {
487 if current_iface_kvs.is_none() {
488 return Err(ConfigError::Parse(
489 "third-level section outside an interface".into(),
490 ));
491 }
492 if let (Some(name), Some(params)) =
493 (current_subiface_name.take(), current_subiface_kvs.take())
494 {
495 current_iface_subinterfaces.push(ParsedSubinterface { name, params });
496 }
497 current_subiface_name = Some(trimmed[3..trimmed.len() - 3].trim().to_string());
498 current_subiface_kvs = Some(HashMap::new());
499 continue;
500 }
501
502 if trimmed.starts_with("[[") && trimmed.ends_with("]]") {
504 let name = trimmed[2..trimmed.len() - 2].trim().to_string();
505 if let (Some(name), Some(params)) =
506 (current_subiface_name.take(), current_subiface_kvs.take())
507 {
508 current_iface_subinterfaces.push(ParsedSubinterface { name, params });
509 }
510 if let (Some(iface_name), Some(kvs)) =
512 (current_iface_name.take(), current_iface_kvs.take())
513 {
514 interfaces.push(build_parsed_interface(
515 iface_name,
516 kvs,
517 std::mem::take(&mut current_iface_subinterfaces),
518 ));
519 }
520 if let (Some(hook_name), Some(kvs)) =
522 (current_hook_name.take(), current_hook_kvs.take())
523 {
524 hooks.push(build_parsed_hook(hook_name, kvs));
525 }
526 current_subsection = Some(name.clone());
527 if current_section.as_deref() == Some("hooks") {
529 current_hook_name = Some(name);
530 current_hook_kvs = Some(HashMap::new());
531 } else {
532 current_iface_name = Some(name);
533 current_iface_kvs = Some(HashMap::new());
534 }
535 continue;
536 }
537
538 if trimmed.starts_with('[') && trimmed.ends_with(']') {
540 if let (Some(name), Some(params)) =
541 (current_subiface_name.take(), current_subiface_kvs.take())
542 {
543 current_iface_subinterfaces.push(ParsedSubinterface { name, params });
544 }
545 if let (Some(iface_name), Some(kvs)) =
547 (current_iface_name.take(), current_iface_kvs.take())
548 {
549 interfaces.push(build_parsed_interface(
550 iface_name,
551 kvs,
552 std::mem::take(&mut current_iface_subinterfaces),
553 ));
554 }
555 if let (Some(hook_name), Some(kvs)) =
557 (current_hook_name.take(), current_hook_kvs.take())
558 {
559 hooks.push(build_parsed_hook(hook_name, kvs));
560 }
561 current_subsection = None;
562
563 let name = trimmed[1..trimmed.len() - 1].trim().to_lowercase();
564 current_section = Some(name);
565 continue;
566 }
567
568 if let Some(eq_pos) = trimmed.find('=') {
570 let key = trimmed[..eq_pos].trim().to_string();
571 let value = trimmed[eq_pos + 1..].trim().to_string();
572
573 if current_subsection.is_some() {
574 debug_assert!(
576 !(current_hook_kvs.is_some() && current_iface_kvs.is_some()),
577 "hook and interface subsections should never be active simultaneously"
578 );
579 if let Some(ref mut kvs) = current_subiface_kvs {
580 kvs.insert(key, value);
581 } else if let Some(ref mut kvs) = current_hook_kvs {
582 kvs.insert(key, value);
583 } else if let Some(ref mut kvs) = current_iface_kvs {
584 kvs.insert(key, value);
585 }
586 } else if let Some(ref section) = current_section {
587 match section.as_str() {
588 "reticulum" => {
589 reticulum_kvs.insert(key, value);
590 }
591 "logging" => {
592 logging_kvs.insert(key, value);
593 }
594 "stamps" => {
595 stamps_kvs.insert(key, value);
596 }
597 "filter" => {
598 filter_kvs.insert(key, value);
599 }
600 _ => {} }
602 }
603 }
604 }
605
606 if let (Some(name), Some(params)) = (current_subiface_name.take(), current_subiface_kvs.take())
608 {
609 current_iface_subinterfaces.push(ParsedSubinterface { name, params });
610 }
611 if let (Some(iface_name), Some(kvs)) = (current_iface_name.take(), current_iface_kvs.take()) {
612 interfaces.push(build_parsed_interface(
613 iface_name,
614 kvs,
615 current_iface_subinterfaces,
616 ));
617 }
618 if let (Some(hook_name), Some(kvs)) = (current_hook_name.take(), current_hook_kvs.take()) {
619 hooks.push(build_parsed_hook(hook_name, kvs));
620 }
621
622 let reticulum = build_reticulum_section(&reticulum_kvs)?;
624 let logging = build_logging_section(&logging_kvs)?;
625 let stamps = build_stamps_section(&stamps_kvs)?;
626 let filter = build_filter_section(&filter_kvs)?;
627
628 Ok(RnsConfig {
629 reticulum,
630 logging,
631 stamps,
632 filter,
633 interfaces,
634 hooks,
635 })
636}
637
638pub fn parse_file(path: &Path) -> Result<RnsConfig, ConfigError> {
640 let content = std::fs::read_to_string(path)?;
641 parse(&content)
642}
643
644pub(crate) fn parse_link_mtu_discovery(input: &str) -> Result<bool, ConfigError> {
645 let mut in_reticulum = false;
646 for line in input.lines() {
647 let trimmed = strip_comment(line).trim();
648 if trimmed.starts_with('[') && trimmed.ends_with(']') {
649 in_reticulum = trimmed.eq_ignore_ascii_case("[reticulum]");
650 continue;
651 }
652 if !in_reticulum {
653 continue;
654 }
655 let Some((key, value)) = trimmed.split_once('=') else {
656 continue;
657 };
658 if key.trim() == "link_mtu_discovery" {
659 let value = value.trim();
660 return parse_bool(value).ok_or_else(|| ConfigError::InvalidValue {
661 key: "link_mtu_discovery".into(),
662 value: value.into(),
663 });
664 }
665 }
666 Ok(true)
667}
668
669fn strip_comment(line: &str) -> &str {
671 let mut in_quote = false;
673 let mut quote_char = '"';
674 for (i, ch) in line.char_indices() {
675 if !in_quote && (ch == '"' || ch == '\'') {
676 in_quote = true;
677 quote_char = ch;
678 } else if in_quote && ch == quote_char {
679 in_quote = false;
680 } else if !in_quote && ch == '#' {
681 return &line[..i];
682 }
683 }
684 line
685}
686
687pub fn parse_bool_pub(value: &str) -> Option<bool> {
689 parse_bool(value)
690}
691
692fn parse_bool(value: &str) -> Option<bool> {
694 match value.to_lowercase().as_str() {
695 "yes" | "true" | "1" | "on" => Some(true),
696 "no" | "false" | "0" | "off" => Some(false),
697 _ => None,
698 }
699}
700
701fn parse_nonnegative_f64_option(
702 kvs: &HashMap<String, String>,
703 key: &str,
704) -> Result<Option<f64>, ConfigError> {
705 let Some(v) = kvs.get(key) else {
706 return Ok(None);
707 };
708 let parsed = v.parse::<f64>().map_err(|_| ConfigError::InvalidValue {
709 key: key.into(),
710 value: v.clone(),
711 })?;
712 if !parsed.is_finite() || parsed < 0.0 {
713 return Err(ConfigError::InvalidValue {
714 key: key.into(),
715 value: v.clone(),
716 });
717 }
718 Ok(Some(parsed))
719}
720
721fn parse_usize_option(
722 kvs: &HashMap<String, String>,
723 key: &str,
724) -> Result<Option<usize>, ConfigError> {
725 let Some(v) = kvs.get(key) else {
726 return Ok(None);
727 };
728 let parsed = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
729 key: key.into(),
730 value: v.clone(),
731 })?;
732 Ok(Some(parsed))
733}
734
735fn build_parsed_interface(
736 name: String,
737 mut kvs: HashMap<String, String>,
738 subinterfaces: Vec<ParsedSubinterface>,
739) -> ParsedInterface {
740 let interface_type = kvs.remove("type").unwrap_or_default();
741 let enabled = kvs
742 .remove("enabled")
743 .and_then(|v| parse_bool(&v))
744 .unwrap_or(true);
745 let mode = kvs
747 .remove("interface_mode")
748 .or_else(|| kvs.remove("mode"))
749 .unwrap_or_else(|| "full".into());
750
751 ParsedInterface {
752 name,
753 interface_type,
754 enabled,
755 mode,
756 params: kvs,
757 subinterfaces,
758 }
759}
760
761fn build_parsed_hook(name: String, mut kvs: HashMap<String, String>) -> ParsedHook {
762 let path = kvs.remove("path").unwrap_or_default();
763 let hook_type = kvs
764 .remove("type")
765 .or_else(|| kvs.remove("backend"))
766 .unwrap_or_else(|| default_hook_type().into());
767 let builtin_id = kvs
768 .remove("builtin")
769 .or_else(|| kvs.remove("builtin_id"))
770 .or_else(|| kvs.remove("id"));
771 let attach_point = kvs.remove("attach_point").unwrap_or_default();
772 let priority = kvs
773 .remove("priority")
774 .and_then(|v| v.parse::<i32>().ok())
775 .unwrap_or(0);
776 let enabled = kvs
777 .remove("enabled")
778 .and_then(|v| parse_bool(&v))
779 .unwrap_or(true);
780
781 ParsedHook {
782 name,
783 path,
784 hook_type,
785 builtin_id,
786 attach_point,
787 priority,
788 enabled,
789 }
790}
791
792fn default_hook_type() -> &'static str {
793 #[cfg(feature = "rns-hooks-native")]
794 {
795 "native"
796 }
797 #[cfg(all(not(feature = "rns-hooks-native"), feature = "rns-hooks-wasm"))]
798 {
799 return "wasm";
800 }
801 #[cfg(all(not(feature = "rns-hooks-native"), not(feature = "rns-hooks-wasm")))]
802 {
803 "wasm"
804 }
805}
806
807pub fn parse_hook_point(s: &str) -> Option<usize> {
809 match s {
810 "PreIngress" => Some(0),
811 "PreDispatch" => Some(1),
812 "AnnounceReceived" => Some(2),
813 "PathUpdated" => Some(3),
814 "AnnounceRetransmit" => Some(4),
815 "LinkRequestReceived" => Some(5),
816 "LinkEstablished" => Some(6),
817 "LinkClosed" => Some(7),
818 "InterfaceUp" => Some(8),
819 "InterfaceDown" => Some(9),
820 "InterfaceConfigChanged" => Some(10),
821 "BackbonePeerConnected" => Some(11),
822 "BackbonePeerDisconnected" => Some(12),
823 "BackbonePeerIdleTimeout" => Some(13),
824 "BackbonePeerWriteStall" => Some(14),
825 "BackbonePeerPenalty" => Some(15),
826 "SendOnInterface" => Some(16),
827 "BroadcastOnAllInterfaces" => Some(17),
828 "DeliverLocal" => Some(18),
829 "TunnelSynthesize" => Some(19),
830 "Tick" => Some(20),
831 _ => None,
832 }
833}
834
835#[cfg(feature = "hooks")]
836pub fn parse_hook_backend(s: &str) -> Result<rns_hooks::HookBackend, String> {
837 s.parse()
838}
839
840fn build_reticulum_section(kvs: &HashMap<String, String>) -> Result<ReticulumSection, ConfigError> {
841 let mut section = ReticulumSection::default();
842
843 if let Some(v) = kvs.get("enable_transport") {
844 section.enable_transport = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
845 key: "enable_transport".into(),
846 value: v.clone(),
847 })?;
848 }
849 if let Some(v) = kvs.get("static_transport_identity") {
850 section.static_transport_identity =
851 parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
852 key: "static_transport_identity".into(),
853 value: v.clone(),
854 })?;
855 }
856 if let Some(v) = kvs.get("local_hops_delta") {
857 section.local_hops_delta = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
858 key: "local_hops_delta".into(),
859 value: v.clone(),
860 })?;
861 }
862 if let Some(v) = kvs.get("share_instance") {
863 section.share_instance = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
864 key: "share_instance".into(),
865 value: v.clone(),
866 })?;
867 }
868 if let Some(v) = kvs.get("enable_interface_management") {
869 section.enable_interface_management =
870 parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
871 key: "enable_interface_management".into(),
872 value: v.clone(),
873 })?;
874 }
875 if let Some(v) = kvs.get("instance_name") {
876 section.instance_name = v.clone();
877 }
878 if let Some(v) = kvs.get("shared_instance_port") {
879 section.shared_instance_port = v.parse::<u16>().map_err(|_| ConfigError::InvalidValue {
880 key: "shared_instance_port".into(),
881 value: v.clone(),
882 })?;
883 }
884 if let Some(v) = kvs.get("instance_control_port") {
885 section.instance_control_port =
886 v.parse::<u16>().map_err(|_| ConfigError::InvalidValue {
887 key: "instance_control_port".into(),
888 value: v.clone(),
889 })?;
890 }
891 if let Some(v) = kvs.get("panic_on_interface_error") {
892 section.panic_on_interface_error =
893 parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
894 key: "panic_on_interface_error".into(),
895 value: v.clone(),
896 })?;
897 }
898 if let Some(v) = kvs.get("use_implicit_proof") {
899 section.use_implicit_proof = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
900 key: "use_implicit_proof".into(),
901 value: v.clone(),
902 })?;
903 }
904 if let Some(v) = kvs.get("network_identity") {
905 section.network_identity = Some(v.clone());
906 }
907 if let Some(v) = kvs.get("respond_to_probes") {
908 section.respond_to_probes = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
909 key: "respond_to_probes".into(),
910 value: v.clone(),
911 })?;
912 }
913 if let Some(v) = kvs.get("enable_remote_management") {
914 section.enable_remote_management =
915 parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
916 key: "enable_remote_management".into(),
917 value: v.clone(),
918 })?;
919 }
920 if let Some(v) = kvs.get("remote_management_allowed") {
921 for item in v.split(',') {
923 let trimmed = item.trim();
924 if !trimmed.is_empty() {
925 section.remote_management_allowed.push(trimmed.to_string());
926 }
927 }
928 }
929 if let Some(v) = kvs.get("publish_blackhole") {
930 section.publish_blackhole = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
931 key: "publish_blackhole".into(),
932 value: v.clone(),
933 })?;
934 }
935 if let Some(v) = kvs.get("probe_port") {
936 section.probe_port = Some(v.parse::<u16>().map_err(|_| ConfigError::InvalidValue {
937 key: "probe_port".into(),
938 value: v.clone(),
939 })?);
940 }
941 if let Some(v) = kvs.get("probe_addr") {
942 section.probe_addr = Some(v.clone());
943 }
944 if let Some(v) = kvs.get("probe_protocol") {
945 section.probe_protocol = Some(v.clone());
946 }
947 if let Some(v) = kvs.get("direct_connect_policy") {
948 section.direct_connect_policy = match v.to_ascii_lowercase().as_str() {
949 "reject" => crate::event::HolePunchPolicy::Reject,
950 "accept_all" => crate::event::HolePunchPolicy::AcceptAll,
951 "identified_only" => crate::event::HolePunchPolicy::IdentifiedOnly,
952 "ask_app" => crate::event::HolePunchPolicy::AskApp,
953 _ => {
954 return Err(ConfigError::InvalidValue {
955 key: "direct_connect_policy".into(),
956 value: v.clone(),
957 })
958 }
959 };
960 }
961 if let Some(v) = kvs.get("direct_connect_allow_private_endpoints") {
962 section.direct_connect_allow_private_endpoints =
963 parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
964 key: "direct_connect_allow_private_endpoints".into(),
965 value: v.clone(),
966 })?;
967 }
968 if let Some(v) = kvs.get("device") {
969 section.device = Some(v.clone());
970 }
971 if let Some(v) = kvs.get("underlay_mark") {
972 let mark = v.parse::<u32>().map_err(|_| ConfigError::InvalidValue {
973 key: "underlay_mark".into(),
974 value: v.clone(),
975 })?;
976 if mark == 0 {
977 return Err(ConfigError::InvalidValue {
978 key: "underlay_mark".into(),
979 value: v.clone(),
980 });
981 }
982 section.underlay_mark = Some(mark);
983 }
984 if let Some(v) = kvs.get("discover_interfaces") {
985 section.discover_interfaces = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
986 key: "discover_interfaces".into(),
987 value: v.clone(),
988 })?;
989 }
990 if let Some(v) = kvs.get("default_gravity") {
991 section.default_gravity = v.parse::<i64>().map_err(|_| ConfigError::InvalidValue {
992 key: "default_gravity".into(),
993 value: v.clone(),
994 })?;
995 }
996 if let Some(v) = kvs.get("autoconnect_discovered_mode") {
999 section.autoconnect_interface_mode = Some(v.to_lowercase());
1000 }
1001 if let Some(v) = kvs.get("autoconnect_interface_mode") {
1002 section.autoconnect_interface_mode = Some(v.to_lowercase());
1003 }
1004 if let Some(v) = kvs.get("autoconnect_interface_gravity") {
1005 section.autoconnect_interface_gravity =
1006 v.parse::<i64>().map_err(|_| ConfigError::InvalidValue {
1007 key: "autoconnect_interface_gravity".into(),
1008 value: v.clone(),
1009 })?;
1010 }
1011 if let Some(v) = kvs.get("autoconnect_announces_to_internal") {
1012 section.autoconnect_announces_to_internal =
1013 parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
1014 key: "autoconnect_announces_to_internal".into(),
1015 value: v.clone(),
1016 })?;
1017 }
1018 if let Some(v) = kvs.get("autoconnect_unverified_implementations") {
1019 section.autoconnect_unverified_implementations =
1020 parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
1021 key: "autoconnect_unverified_implementations".into(),
1022 value: v.clone(),
1023 })?;
1024 }
1025 if let Some(v) = kvs.get("required_discovery_value") {
1026 let v = parse_config_float(v).ok_or_else(|| ConfigError::InvalidValue {
1028 key: "required_discovery_value".into(),
1029 value: v.clone(),
1030 })?;
1031 section.required_discovery_value =
1032 (v > 0.0).then(|| rns_core::stamp::clamp_cost(v, rns_core::constants::STAMP_COST_CAP));
1033 }
1034 if let Some(v) = kvs.get("hop_limiter") {
1035 let n = parse_config_int(v).ok_or_else(|| ConfigError::InvalidValue {
1037 key: "hop_limiter".into(),
1038 value: v.clone(),
1039 })?;
1040 section.hop_limiter = n.clamp(1, i64::from(rns_core::constants::PATHFINDER_M)) as u8;
1041 }
1042 if let Some(v) = kvs.get("prefer_shorter_path") {
1043 section.prefer_shorter_path = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
1044 key: "prefer_shorter_path".into(),
1045 value: v.clone(),
1046 })?;
1047 }
1048 if let Some(v) = kvs.get("max_paths_per_destination") {
1049 let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
1050 key: "max_paths_per_destination".into(),
1051 value: v.clone(),
1052 })?;
1053 section.max_paths_per_destination = n.max(1);
1054 }
1055 if let Some(v) = kvs.get("packet_hashlist_max_entries") {
1056 let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
1057 key: "packet_hashlist_max_entries".into(),
1058 value: v.clone(),
1059 })?;
1060 section.packet_hashlist_max_entries = n.max(1);
1061 }
1062 if let Some(v) = kvs.get("packet_hashlist_allocation") {
1063 section.packet_hashlist_allocation = match v.to_ascii_lowercase().as_str() {
1064 "eager" => rns_core::transport::types::PacketHashlistAllocation::Eager,
1065 "lazy" => rns_core::transport::types::PacketHashlistAllocation::Lazy,
1066 _ => {
1067 return Err(ConfigError::InvalidValue {
1068 key: "packet_hashlist_allocation".into(),
1069 value: v.clone(),
1070 });
1071 }
1072 };
1073 }
1074 if let Some(v) = kvs.get("max_discovery_pr_tags") {
1075 let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
1076 key: "max_discovery_pr_tags".into(),
1077 value: v.clone(),
1078 })?;
1079 section.max_discovery_pr_tags = n.max(1);
1080 }
1081 if let Some(v) = kvs.get("max_path_destinations") {
1082 let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
1083 key: "max_path_destinations".into(),
1084 value: v.clone(),
1085 })?;
1086 section.max_path_destinations = n.max(1);
1087 }
1088 if let Some(v) = kvs.get("max_tunnel_destinations_total") {
1089 let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
1090 key: "max_tunnel_destinations_total".into(),
1091 value: v.clone(),
1092 })?;
1093 section.max_tunnel_destinations_total = n.max(1);
1094 }
1095 if let Some(v) = kvs.get("known_destinations_ttl") {
1096 section.known_destinations_ttl =
1097 v.parse::<u64>().map_err(|_| ConfigError::InvalidValue {
1098 key: "known_destinations_ttl".into(),
1099 value: v.clone(),
1100 })?;
1101 }
1102 if let Some(v) = kvs.get("known_destinations_max_entries") {
1103 let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
1104 key: "known_destinations_max_entries".into(),
1105 value: v.clone(),
1106 })?;
1107 if n == 0 {
1108 return Err(ConfigError::InvalidValue {
1109 key: "known_destinations_max_entries".into(),
1110 value: v.clone(),
1111 });
1112 }
1113 section.known_destinations_max_entries = n;
1114 }
1115 if let Some(v) = kvs.get("ratchet_expiry") {
1116 let expiry = v.parse::<u64>().map_err(|_| ConfigError::InvalidValue {
1117 key: "ratchet_expiry".into(),
1118 value: v.clone(),
1119 })?;
1120 if expiry == 0 {
1121 return Err(ConfigError::InvalidValue {
1122 key: "ratchet_expiry".into(),
1123 value: v.clone(),
1124 });
1125 }
1126 section.ratchet_expiry = expiry;
1127 }
1128 if let Some(v) = kvs.get("destination_timeout_secs") {
1129 section.known_destinations_ttl =
1130 v.parse::<u64>().map_err(|_| ConfigError::InvalidValue {
1131 key: "destination_timeout_secs".into(),
1132 value: v.clone(),
1133 })?;
1134 }
1135 if let Some(v) = kvs.get("announce_table_ttl") {
1136 let ttl = v.parse::<u64>().map_err(|_| ConfigError::InvalidValue {
1137 key: "announce_table_ttl".into(),
1138 value: v.clone(),
1139 })?;
1140 if ttl == 0 {
1141 return Err(ConfigError::InvalidValue {
1142 key: "announce_table_ttl".into(),
1143 value: v.clone(),
1144 });
1145 }
1146 section.announce_table_ttl = ttl;
1147 }
1148 if let Some(v) = kvs.get("announce_table_max_bytes") {
1149 let max_bytes = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
1150 key: "announce_table_max_bytes".into(),
1151 value: v.clone(),
1152 })?;
1153 if max_bytes == 0 {
1154 return Err(ConfigError::InvalidValue {
1155 key: "announce_table_max_bytes".into(),
1156 value: v.clone(),
1157 });
1158 }
1159 section.announce_table_max_bytes = max_bytes;
1160 }
1161 if let Some(v) = kvs.get("announce_signature_cache_enabled") {
1162 section.announce_sig_cache_enabled = match v.as_str() {
1163 "true" | "yes" | "True" | "Yes" => true,
1164 "false" | "no" | "False" | "No" => false,
1165 _ => {
1166 return Err(ConfigError::InvalidValue {
1167 key: "announce_signature_cache_enabled".into(),
1168 value: v.clone(),
1169 })
1170 }
1171 };
1172 }
1173 if let Some(v) = kvs.get("announce_signature_cache_max_entries") {
1174 let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
1175 key: "announce_signature_cache_max_entries".into(),
1176 value: v.clone(),
1177 })?;
1178 section.announce_sig_cache_max_entries = n;
1179 }
1180 if let Some(v) = kvs.get("announce_signature_cache_ttl") {
1181 let ttl = v.parse::<u64>().map_err(|_| ConfigError::InvalidValue {
1182 key: "announce_signature_cache_ttl".into(),
1183 value: v.clone(),
1184 })?;
1185 section.announce_sig_cache_ttl = ttl;
1186 }
1187 if let Some(v) = kvs.get("announce_queue_max_entries") {
1188 let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
1189 key: "announce_queue_max_entries".into(),
1190 value: v.clone(),
1191 })?;
1192 if n == 0 {
1193 return Err(ConfigError::InvalidValue {
1194 key: "announce_queue_max_entries".into(),
1195 value: v.clone(),
1196 });
1197 }
1198 section.announce_queue_max_entries = n;
1199 }
1200 if let Some(v) = kvs.get("announce_queue_max_interfaces") {
1201 let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
1202 key: "announce_queue_max_interfaces".into(),
1203 value: v.clone(),
1204 })?;
1205 if n == 0 {
1206 return Err(ConfigError::InvalidValue {
1207 key: "announce_queue_max_interfaces".into(),
1208 value: v.clone(),
1209 });
1210 }
1211 section.announce_queue_max_interfaces = n;
1212 }
1213 if let Some(v) = kvs.get("default_ar_target") {
1214 let target = v.parse::<f64>().map_err(|_| ConfigError::InvalidValue {
1215 key: "default_ar_target".into(),
1216 value: v.clone(),
1217 })?;
1218 if !target.is_finite() || target < 0.0 {
1219 return Err(ConfigError::InvalidValue {
1220 key: "default_ar_target".into(),
1221 value: v.clone(),
1222 });
1223 }
1224 section.default_ar_target = if target == 0.0 { None } else { Some(target) };
1225 }
1226 if let Some(v) = kvs.get("default_ar_penalty") {
1227 let penalty = v.parse::<f64>().map_err(|_| ConfigError::InvalidValue {
1228 key: "default_ar_penalty".into(),
1229 value: v.clone(),
1230 })?;
1231 if !penalty.is_finite() || penalty < 0.0 {
1232 return Err(ConfigError::InvalidValue {
1233 key: "default_ar_penalty".into(),
1234 value: v.clone(),
1235 });
1236 }
1237 section.default_ar_penalty = penalty;
1238 }
1239 if let Some(v) = kvs.get("default_ar_grace") {
1240 let grace = v.parse::<u32>().map_err(|_| ConfigError::InvalidValue {
1241 key: "default_ar_grace".into(),
1242 value: v.clone(),
1243 })?;
1244 section.default_ar_grace = grace;
1245 }
1246 if let Some(v) = parse_usize_option(kvs, "ic_max_held_announces")? {
1247 section.default_ic_max_held_announces = v;
1248 }
1249 if let Some(v) = parse_nonnegative_f64_option(kvs, "ic_burst_hold")? {
1250 section.default_ic_burst_hold = v;
1251 }
1252 if let Some(v) = parse_nonnegative_f64_option(kvs, "ic_burst_freq_new")? {
1253 section.default_ic_burst_freq_new = v;
1254 }
1255 if let Some(v) = parse_nonnegative_f64_option(kvs, "ic_burst_freq")? {
1256 section.default_ic_burst_freq = v;
1257 }
1258 if let Some(v) = parse_nonnegative_f64_option(kvs, "ic_pr_burst_freq_new")? {
1259 section.default_ic_pr_burst_freq_new = v;
1260 }
1261 if let Some(v) = parse_nonnegative_f64_option(kvs, "ic_pr_burst_freq")? {
1262 section.default_ic_pr_burst_freq = v;
1263 }
1264 if let Some(v) = parse_nonnegative_f64_option(kvs, "ic_new_time")? {
1265 section.default_ic_new_time = v;
1266 }
1267 if let Some(v) = parse_nonnegative_f64_option(kvs, "ic_burst_penalty")? {
1268 section.default_ic_burst_penalty = v;
1269 }
1270 if let Some(v) = parse_nonnegative_f64_option(kvs, "ic_held_release_interval")? {
1271 section.default_ic_held_release_interval = v;
1272 }
1273 if let Some(v) = kvs.get("egress_control") {
1274 section.default_egress_control =
1275 parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
1276 key: "egress_control".into(),
1277 value: v.clone(),
1278 })?;
1279 }
1280 if let Some(v) = parse_nonnegative_f64_option(kvs, "ec_pr_freq")? {
1281 section.default_ec_pr_freq = v;
1282 }
1283 if let Some(v) = kvs.get("announce_queue_max_bytes") {
1284 let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
1285 key: "announce_queue_max_bytes".into(),
1286 value: v.clone(),
1287 })?;
1288 if n == 0 {
1289 return Err(ConfigError::InvalidValue {
1290 key: "announce_queue_max_bytes".into(),
1291 value: v.clone(),
1292 });
1293 }
1294 section.announce_queue_max_bytes = n;
1295 }
1296 if let Some(v) = kvs.get("announce_queue_ttl") {
1297 let ttl = v.parse::<u64>().map_err(|_| ConfigError::InvalidValue {
1298 key: "announce_queue_ttl".into(),
1299 value: v.clone(),
1300 })?;
1301 if ttl == 0 {
1302 return Err(ConfigError::InvalidValue {
1303 key: "announce_queue_ttl".into(),
1304 value: v.clone(),
1305 });
1306 }
1307 section.announce_queue_ttl = ttl;
1308 }
1309 if let Some(v) = kvs.get("announce_queue_overflow_policy") {
1310 let normalized = v.to_lowercase();
1311 if normalized != "drop_newest" && normalized != "drop_oldest" && normalized != "drop_worst"
1312 {
1313 return Err(ConfigError::InvalidValue {
1314 key: "announce_queue_overflow_policy".into(),
1315 value: v.clone(),
1316 });
1317 }
1318 section.announce_queue_overflow_policy = normalized;
1319 }
1320 if let Some(v) = kvs.get("driver_event_queue_capacity") {
1321 let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
1322 key: "driver_event_queue_capacity".into(),
1323 value: v.clone(),
1324 })?;
1325 if n == 0 {
1326 return Err(ConfigError::InvalidValue {
1327 key: "driver_event_queue_capacity".into(),
1328 value: v.clone(),
1329 });
1330 }
1331 section.driver_event_queue_capacity = n;
1332 section.inbound_queue_capacities =
1333 crate::event::InboundQueueCapacities::from_shared_capacity(n);
1334 }
1335 for (key, queue) in [
1336 ("qlen_in_data", 0),
1337 ("qlen_in_announce", 1),
1338 ("qlen_in_pr", 2),
1339 ("qlen_in_il", 3),
1340 ] {
1341 let Some(v) = kvs.get(key) else {
1342 continue;
1343 };
1344 let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
1345 key: key.into(),
1346 value: v.clone(),
1347 })?;
1348 if n == 0 {
1349 return Err(ConfigError::InvalidValue {
1350 key: key.into(),
1351 value: v.clone(),
1352 });
1353 }
1354 match queue {
1355 0 => section.inbound_queue_capacities.data = n,
1356 1 => section.inbound_queue_capacities.announce = n,
1357 2 => section.inbound_queue_capacities.path_request = n,
1358 _ => section.inbound_queue_capacities.ingress_limited = n,
1359 }
1360 }
1361 if let Some(v) = kvs.get("interface_writer_queue_capacity") {
1362 let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
1363 key: "interface_writer_queue_capacity".into(),
1364 value: v.clone(),
1365 })?;
1366 if n == 0 {
1367 return Err(ConfigError::InvalidValue {
1368 key: "interface_writer_queue_capacity".into(),
1369 value: v.clone(),
1370 });
1371 }
1372 section.interface_writer_queue_capacity = n;
1373 }
1374 if let Some(v) = kvs.get("backbone_peer_pool_max_connected") {
1375 section.backbone_peer_pool_max_connected =
1376 v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
1377 key: "backbone_peer_pool_max_connected".into(),
1378 value: v.clone(),
1379 })?;
1380 }
1381 if let Some(v) = kvs.get("backbone_peer_pool_failure_threshold") {
1382 let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
1383 key: "backbone_peer_pool_failure_threshold".into(),
1384 value: v.clone(),
1385 })?;
1386 if n == 0 {
1387 return Err(ConfigError::InvalidValue {
1388 key: "backbone_peer_pool_failure_threshold".into(),
1389 value: v.clone(),
1390 });
1391 }
1392 section.backbone_peer_pool_failure_threshold = n;
1393 }
1394 if let Some(v) = kvs.get("backbone_peer_pool_failure_window") {
1395 section.backbone_peer_pool_failure_window =
1396 v.parse::<u64>().map_err(|_| ConfigError::InvalidValue {
1397 key: "backbone_peer_pool_failure_window".into(),
1398 value: v.clone(),
1399 })?;
1400 }
1401 if let Some(v) = kvs.get("backbone_peer_pool_cooldown") {
1402 section.backbone_peer_pool_cooldown =
1403 v.parse::<u64>().map_err(|_| ConfigError::InvalidValue {
1404 key: "backbone_peer_pool_cooldown".into(),
1405 value: v.clone(),
1406 })?;
1407 }
1408 #[cfg(feature = "hooks")]
1409 if let Some(v) = kvs.get("provider_bridge") {
1410 section.provider_bridge = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
1411 key: "provider_bridge".into(),
1412 value: v.clone(),
1413 })?;
1414 }
1415 #[cfg(feature = "hooks")]
1416 if let Some(v) = kvs.get("provider_socket_path") {
1417 section.provider_socket_path = Some(v.clone());
1418 }
1419 #[cfg(feature = "hooks")]
1420 if let Some(v) = kvs.get("provider_queue_max_events") {
1421 section.provider_queue_max_events =
1422 v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
1423 key: "provider_queue_max_events".into(),
1424 value: v.clone(),
1425 })?;
1426 }
1427 #[cfg(feature = "hooks")]
1428 if let Some(v) = kvs.get("provider_queue_max_bytes") {
1429 section.provider_queue_max_bytes =
1430 v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
1431 key: "provider_queue_max_bytes".into(),
1432 value: v.clone(),
1433 })?;
1434 }
1435 #[cfg(feature = "hooks")]
1436 if let Some(v) = kvs.get("provider_overflow_policy") {
1437 let normalized = v.to_lowercase();
1438 if normalized != "drop_newest" && normalized != "drop_oldest" {
1439 return Err(ConfigError::InvalidValue {
1440 key: "provider_overflow_policy".into(),
1441 value: v.clone(),
1442 });
1443 }
1444 section.provider_overflow_policy = normalized;
1445 }
1446
1447 Ok(section)
1448}
1449
1450fn parse_config_int(value: &str) -> Option<i64> {
1453 let value = value.trim();
1454 if let Ok(n) = value.parse::<i64>() {
1455 return Some(n);
1456 }
1457 let digits = value.strip_prefix(['+', '-']).unwrap_or(value);
1458 if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
1459 return None;
1460 }
1461 Some(if value.starts_with('-') {
1462 i64::MIN
1463 } else {
1464 i64::MAX
1465 })
1466}
1467
1468pub(crate) fn parse_config_float(value: &str) -> Option<f64> {
1471 value.trim().parse::<f64>().ok()
1472}
1473
1474fn build_filter_section(kvs: &HashMap<String, String>) -> Result<FilterSection, ConfigError> {
1475 let mut section = FilterSection::default();
1476 if let Some(v) = kvs.get("required_only") {
1477 section.required_only = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
1478 key: "required_only".into(),
1479 value: v.clone(),
1480 })?;
1481 }
1482 let list = |key: &str| -> Vec<String> {
1483 kvs.get(key)
1484 .map(|v| {
1485 v.split(',')
1486 .map(str::trim)
1487 .filter(|item| !item.is_empty())
1488 .map(str::to_string)
1489 .collect()
1490 })
1491 .unwrap_or_default()
1492 };
1493 section.ignored_types = list("ignored_types");
1494 section.allowed_types = list("allowed_types");
1495 Ok(section)
1496}
1497
1498fn build_stamps_section(kvs: &HashMap<String, String>) -> Result<StampsSection, ConfigError> {
1499 use rns_core::constants::{LINK_COST_CAP, STAMP_COST_CAP, STAMP_TIMEOUT_MIN};
1500
1501 let mut section = StampsSection::default();
1502 let flag = |key: &str| -> Result<bool, ConfigError> {
1503 match kvs.get(key) {
1504 None => Ok(false),
1505 Some(v) => parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
1506 key: key.into(),
1507 value: v.clone(),
1508 }),
1509 }
1510 };
1511 let cost = |key: &str, cap: u8| -> Result<Option<StampCost>, ConfigError> {
1512 let Some(v) = kvs.get(key) else {
1513 return Ok(None);
1514 };
1515 let n = parse_config_float(v).ok_or_else(|| ConfigError::InvalidValue {
1516 key: key.into(),
1517 value: v.clone(),
1518 })?;
1519 Ok(Some(rns_core::stamp::clamp_cost(n, cap)))
1520 };
1521 let timeout = |key: &str| -> Result<Option<f64>, ConfigError> {
1522 let Some(v) = kvs.get(key) else {
1523 return Ok(None);
1524 };
1525 let n = v
1526 .trim()
1527 .parse::<f64>()
1528 .map_err(|_| ConfigError::InvalidValue {
1529 key: key.into(),
1530 value: v.clone(),
1531 })?;
1532 Ok(Some(if n.is_nan() {
1534 STAMP_TIMEOUT_MIN
1535 } else {
1536 n.max(STAMP_TIMEOUT_MIN)
1537 }))
1538 };
1539
1540 if flag("enforce_stamps")? {
1542 section.stamps_enforced = true;
1543 section.lr_stamps_enforced = true;
1544 }
1545 if flag("stamp_destinations")? {
1546 section.stamp_destinations = true;
1547 }
1548 if flag("stamp_prs")? {
1549 section.stamp_prs = true;
1550 }
1551 if flag("always_stamp_lrs")? {
1554 section.always_stamp_lrs = true;
1555 }
1556 if let Some(v) = cost("destination_cost", STAMP_COST_CAP)? {
1557 section.destination_cost = v;
1558 }
1559 if let Some(v) = cost("destination_value", STAMP_COST_CAP)? {
1560 section.destination_value = v;
1561 }
1562 if let Some(v) = cost("pr_cost", STAMP_COST_CAP)? {
1563 section.pr_cost = v;
1564 }
1565 if let Some(v) = cost("pr_value", STAMP_COST_CAP)? {
1566 section.pr_value = v;
1567 }
1568 if let Some(v) = cost("lr_cost", LINK_COST_CAP)? {
1569 section.lr_cost = v;
1570 }
1571 if let Some(v) = timeout("lr_stamp_timeout")? {
1572 section.lr_stamp_timeout = v;
1573 }
1574 if let Some(v) = timeout("pr_stamp_timeout")? {
1575 section.pr_stamp_timeout = v;
1576 }
1577 Ok(section)
1578}
1579
1580fn build_logging_section(kvs: &HashMap<String, String>) -> Result<LoggingSection, ConfigError> {
1581 let mut section = LoggingSection::default();
1582
1583 if let Some(v) = kvs.get("loglevel") {
1584 section.loglevel = v.parse::<u8>().map_err(|_| ConfigError::InvalidValue {
1585 key: "loglevel".into(),
1586 value: v.clone(),
1587 })?;
1588 if section.loglevel > crate::logging::LOG_EXTREME {
1589 return Err(ConfigError::InvalidValue {
1590 key: "loglevel".into(),
1591 value: v.clone(),
1592 });
1593 }
1594 }
1595 if let Some(v) = kvs.get("logtimestamps") {
1596 section.logtimestamps = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
1597 key: "logtimestamps".into(),
1598 value: v.clone(),
1599 })?;
1600 }
1601
1602 Ok(section)
1603}
1604
1605#[cfg(test)]
1606mod tests {
1607 use super::*;
1608
1609 #[test]
1610 fn parse_empty() {
1611 let config = parse("").unwrap();
1612 assert!(!config.reticulum.enable_transport);
1613 assert!(!config.reticulum.static_transport_identity);
1614 assert!(!config.reticulum.local_hops_delta);
1615 assert!(config.reticulum.share_instance);
1616 assert_eq!(config.reticulum.instance_name, "default");
1617 assert_eq!(config.logging.loglevel, 4);
1618 assert!(config.logging.logtimestamps);
1619 assert!(config.interfaces.is_empty());
1620 assert_eq!(
1621 config.reticulum.packet_hashlist_max_entries,
1622 rns_core::constants::HASHLIST_MAXSIZE
1623 );
1624 assert_eq!(
1625 config.reticulum.packet_hashlist_allocation,
1626 rns_core::transport::types::PacketHashlistAllocation::Lazy
1627 );
1628 assert_eq!(
1629 config.reticulum.announce_table_ttl,
1630 rns_core::constants::ANNOUNCE_TABLE_TTL as u64
1631 );
1632 assert_eq!(
1633 config.reticulum.announce_table_max_bytes,
1634 rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES
1635 );
1636 assert_eq!(
1637 config.reticulum.ratchet_expiry,
1638 rns_core::constants::RATCHET_EXPIRY
1639 );
1640 assert_eq!(
1641 config.reticulum.inbound_queue_capacities,
1642 crate::event::InboundQueueCapacities::default()
1643 );
1644 }
1645
1646 #[test]
1647 fn parse_reticulum_inbound_queue_lengths() {
1648 let config = parse(
1649 r#"
1650[reticulum]
1651qlen_in_data = 101
1652qlen_in_announce = 102
1653qlen_in_pr = 103
1654qlen_in_il = 104
1655"#,
1656 )
1657 .unwrap();
1658
1659 assert_eq!(
1660 config.reticulum.inbound_queue_capacities,
1661 crate::event::InboundQueueCapacities {
1662 data: 101,
1663 announce: 102,
1664 path_request: 103,
1665 ingress_limited: 104,
1666 }
1667 );
1668 }
1669
1670 #[test]
1671 fn inbound_queue_lengths_must_be_positive_integers() {
1672 for key in [
1673 "qlen_in_data",
1674 "qlen_in_announce",
1675 "qlen_in_pr",
1676 "qlen_in_il",
1677 ] {
1678 for value in ["0", "-1", "many"] {
1679 let input = format!("[reticulum]\n{key} = {value}\n");
1680 assert!(matches!(
1681 parse(&input),
1682 Err(ConfigError::InvalidValue { key: invalid_key, .. })
1683 if invalid_key == key
1684 ));
1685 }
1686 }
1687 }
1688
1689 #[test]
1690 fn legacy_event_capacity_still_scales_class_defaults() {
1691 let config = parse("[reticulum]\ndriver_event_queue_capacity = 64\n").unwrap();
1692 assert_eq!(
1693 config.reticulum.inbound_queue_capacities,
1694 crate::event::InboundQueueCapacities {
1695 data: 64,
1696 announce: 64,
1697 path_request: 64,
1698 ingress_limited: 8,
1699 }
1700 );
1701 }
1702
1703 #[cfg(feature = "hooks")]
1704 #[test]
1705 fn parse_provider_bridge_config() {
1706 let config = parse(
1707 r#"
1708[reticulum]
1709provider_bridge = yes
1710provider_socket_path = /tmp/rns-provider.sock
1711provider_queue_max_events = 42
1712provider_queue_max_bytes = 8192
1713provider_overflow_policy = drop_oldest
1714"#,
1715 )
1716 .unwrap();
1717
1718 assert!(config.reticulum.provider_bridge);
1719 assert_eq!(
1720 config.reticulum.provider_socket_path.as_deref(),
1721 Some("/tmp/rns-provider.sock")
1722 );
1723 assert_eq!(config.reticulum.provider_queue_max_events, 42);
1724 assert_eq!(config.reticulum.provider_queue_max_bytes, 8192);
1725 assert_eq!(config.reticulum.provider_overflow_policy, "drop_oldest");
1726 }
1727
1728 #[test]
1729 fn parse_default_config() {
1730 let input = r#"
1733[reticulum]
1734enable_transport = False
1735share_instance = Yes
1736instance_name = default
1737
1738[stamps]
1739
1740# destination_cost = 23.5
1741# destination_value = 25.0
1742# pr_cost = 15.8
1743# pr_value = 15.8
1744# lr_cost = 12.5
1745# lr_stamp_timeout = 15
1746# pr_stamp_timeout = 15
1747
1748[logging]
1749loglevel = 4
1750
1751[interfaces]
1752
1753 [[Default Interface]]
1754 type = AutoInterface
1755 enabled = Yes
1756"#;
1757 let config = parse(input).unwrap();
1758 assert!(!config.reticulum.enable_transport);
1759 assert!(config.reticulum.share_instance);
1760 assert_eq!(config.reticulum.instance_name, "default");
1761 assert_eq!(config.logging.loglevel, 4);
1762 assert_eq!(config.interfaces.len(), 1);
1763 assert_eq!(config.interfaces[0].name, "Default Interface");
1764 assert_eq!(config.interfaces[0].interface_type, "AutoInterface");
1765 assert!(config.interfaces[0].enabled);
1766 assert_eq!(config.stamps, StampsSection::default());
1767 }
1768
1769 #[test]
1770 fn parse_reticulum_section() {
1771 let input = r#"
1772[reticulum]
1773enable_transport = True
1774share_instance = No
1775instance_name = mynode
1776shared_instance_port = 12345
1777instance_control_port = 12346
1778panic_on_interface_error = Yes
1779use_implicit_proof = False
1780respond_to_probes = True
1781network_identity = /home/user/.reticulum/identity
1782known_destinations_ttl = 1234
1783known_destinations_max_entries = 4321
1784ratchet_expiry = 9876
1785announce_table_ttl = 45
1786announce_table_max_bytes = 65536
1787packet_hashlist_max_entries = 321
1788max_discovery_pr_tags = 222
1789max_path_destinations = 111
1790max_tunnel_destinations_total = 99
1791announce_signature_cache_enabled = false
1792announce_signature_cache_max_entries = 500
1793announce_signature_cache_ttl = 300
1794announce_queue_max_entries = 123
1795announce_queue_max_interfaces = 321
1796announce_queue_max_bytes = 4567
1797announce_queue_ttl = 89
1798announce_queue_overflow_policy = drop_oldest
1799driver_event_queue_capacity = 6543
1800interface_writer_queue_capacity = 210
1801underlay_mark = 51820
1802backbone_peer_pool_max_connected = 6
1803backbone_peer_pool_failure_threshold = 4
1804backbone_peer_pool_failure_window = 120
1805backbone_peer_pool_cooldown = 300
1806"#;
1807 let config = parse(input).unwrap();
1808 assert!(config.reticulum.enable_transport);
1809 assert!(!config.reticulum.share_instance);
1810 assert_eq!(config.reticulum.instance_name, "mynode");
1811 assert_eq!(config.reticulum.shared_instance_port, 12345);
1812 assert_eq!(config.reticulum.instance_control_port, 12346);
1813 assert!(config.reticulum.panic_on_interface_error);
1814 assert!(!config.reticulum.use_implicit_proof);
1815 assert!(config.reticulum.respond_to_probes);
1816 assert_eq!(config.reticulum.underlay_mark, Some(51820));
1817 assert_eq!(
1818 config.reticulum.network_identity.as_deref(),
1819 Some("/home/user/.reticulum/identity")
1820 );
1821 assert_eq!(config.reticulum.known_destinations_ttl, 1234);
1822 assert_eq!(config.reticulum.known_destinations_max_entries, 4321);
1823 assert_eq!(config.reticulum.ratchet_expiry, 9876);
1824 assert_eq!(config.reticulum.announce_table_ttl, 45);
1825 assert_eq!(config.reticulum.announce_table_max_bytes, 65536);
1826 assert_eq!(config.reticulum.packet_hashlist_max_entries, 321);
1827 assert_eq!(config.reticulum.max_discovery_pr_tags, 222);
1828 assert_eq!(config.reticulum.max_path_destinations, 111);
1829 assert_eq!(config.reticulum.max_tunnel_destinations_total, 99);
1830 assert!(!config.reticulum.announce_sig_cache_enabled);
1831 assert_eq!(config.reticulum.announce_sig_cache_max_entries, 500);
1832 assert_eq!(config.reticulum.announce_sig_cache_ttl, 300);
1833 assert_eq!(config.reticulum.announce_queue_max_entries, 123);
1834 assert_eq!(config.reticulum.announce_queue_max_interfaces, 321);
1835 assert_eq!(config.reticulum.announce_queue_max_bytes, 4567);
1836 assert_eq!(config.reticulum.announce_queue_ttl, 89);
1837 assert_eq!(
1838 config.reticulum.announce_queue_overflow_policy,
1839 "drop_oldest"
1840 );
1841 assert_eq!(config.reticulum.driver_event_queue_capacity, 6543);
1842 assert_eq!(config.reticulum.interface_writer_queue_capacity, 210);
1843 assert_eq!(config.reticulum.backbone_peer_pool_max_connected, 6);
1844 assert_eq!(config.reticulum.backbone_peer_pool_failure_threshold, 4);
1845 assert_eq!(config.reticulum.backbone_peer_pool_failure_window, 120);
1846 assert_eq!(config.reticulum.backbone_peer_pool_cooldown, 300);
1847 }
1848
1849 #[test]
1850 fn underlay_mark_defaults_to_disabled() {
1851 let config = parse("[reticulum]\n").unwrap();
1852 assert_eq!(config.reticulum.underlay_mark, None);
1853 }
1854
1855 #[test]
1856 fn underlay_mark_rejects_zero_and_non_numeric_values() {
1857 for value in ["0", "-1", "not-a-mark", "4294967296"] {
1858 let error = parse(&format!("[reticulum]\nunderlay_mark = {value}\n"))
1859 .expect_err("invalid underlay mark must be rejected");
1860 assert!(matches!(
1861 error,
1862 ConfigError::InvalidValue { ref key, .. } if key == "underlay_mark"
1863 ));
1864 }
1865 }
1866
1867 #[test]
1868 fn parse_reticulum_announce_rate_defaults() {
1869 let input = r#"
1870[reticulum]
1871default_ar_target = 7200
1872default_ar_penalty = 15
1873default_ar_grace = 7
1874"#;
1875 let config = parse(input).unwrap();
1876
1877 assert_eq!(config.reticulum.default_ar_target, Some(7200.0));
1878 assert_eq!(config.reticulum.default_ar_penalty, 15.0);
1879 assert_eq!(config.reticulum.default_ar_grace, 7);
1880 }
1881
1882 #[test]
1883 fn parse_interface_gravity_defaults_and_autoconnect_options() {
1884 let config = parse(
1885 r#"
1886[reticulum]
1887default_gravity = -3
1888autoconnect_discovered_mode = full
1889autoconnect_interface_mode = boundary
1890autoconnect_interface_gravity = 7
1891autoconnect_announces_to_internal = yes
1892autoconnect_unverified_implementations = yes
1893"#,
1894 )
1895 .unwrap();
1896
1897 assert_eq!(
1898 config.reticulum.autoconnect_interface_mode.as_deref(),
1899 Some("boundary")
1900 );
1901 assert_eq!(config.reticulum.default_gravity, -3);
1902 assert_eq!(config.reticulum.autoconnect_interface_gravity, 7);
1903 assert!(config.reticulum.autoconnect_announces_to_internal);
1904 assert!(config.reticulum.autoconnect_unverified_implementations);
1905 }
1906
1907 #[test]
1908 fn autoconnect_unverified_implementations_defaults_to_false() {
1909 let config = parse("[reticulum]\n").unwrap();
1910 assert!(!config.reticulum.autoconnect_unverified_implementations);
1911 }
1912
1913 #[test]
1914 fn interface_gravity_options_default_to_zero() {
1915 let config = parse("[reticulum]\n").unwrap();
1916 assert_eq!(config.reticulum.default_gravity, 0);
1917 assert_eq!(config.reticulum.autoconnect_interface_gravity, 0);
1918 }
1919
1920 #[test]
1921 fn parses_packet_hashlist_allocation_case_insensitively() {
1922 for (value, expected) in [
1923 (
1924 "eager",
1925 rns_core::transport::types::PacketHashlistAllocation::Eager,
1926 ),
1927 (
1928 "LAZY",
1929 rns_core::transport::types::PacketHashlistAllocation::Lazy,
1930 ),
1931 (
1932 "LaZy",
1933 rns_core::transport::types::PacketHashlistAllocation::Lazy,
1934 ),
1935 ] {
1936 let input = format!("[reticulum]\npacket_hashlist_allocation = {value}\n");
1937 assert_eq!(
1938 parse(&input).unwrap().reticulum.packet_hashlist_allocation,
1939 expected
1940 );
1941 }
1942 }
1943
1944 #[test]
1945 fn rejects_invalid_packet_hashlist_allocation() {
1946 assert!(parse("[reticulum]\npacket_hashlist_allocation = dynamic\n").is_err());
1947 }
1948
1949 #[test]
1950 fn rejects_non_integer_global_gravity_options() {
1951 assert!(parse("[reticulum]\ndefault_gravity = heavy\n").is_err());
1952 assert!(parse("[reticulum]\nautoconnect_interface_gravity = light\n").is_err());
1953 }
1954
1955 #[test]
1956 fn parse_reticulum_announce_rate_target_zero_disables_default() {
1957 let input = r#"
1958[reticulum]
1959default_ar_target = 0
1960default_ar_penalty = 0
1961default_ar_grace = 0
1962"#;
1963 let config = parse(input).unwrap();
1964
1965 assert_eq!(config.reticulum.default_ar_target, None);
1966 assert_eq!(config.reticulum.default_ar_penalty, 0.0);
1967 assert_eq!(config.reticulum.default_ar_grace, 0);
1968 }
1969
1970 #[test]
1971 fn parse_reticulum_announce_rate_defaults_reject_negative_values() {
1972 for (key, value) in [
1973 ("default_ar_target", "-1"),
1974 ("default_ar_target", "NaN"),
1975 ("default_ar_target", "inf"),
1976 ("default_ar_penalty", "-1"),
1977 ("default_ar_penalty", "NaN"),
1978 ("default_ar_penalty", "inf"),
1979 ("default_ar_grace", "-1"),
1980 ] {
1981 let input = format!("[reticulum]\n{key} = {value}\n");
1982 let err = parse(&input).unwrap_err();
1983 assert!(
1984 err.to_string().contains(key),
1985 "error {err:?} should mention {key}"
1986 );
1987 }
1988 }
1989
1990 #[test]
1991 fn parse_reticulum_ingress_and_egress_control_defaults() {
1992 let input = r#"[reticulum]
1993ic_max_held_announces = 17
1994ic_burst_hold = 1.5
1995ic_burst_freq_new = 2.5
1996ic_burst_freq = 3.5
1997ic_pr_burst_freq_new = 4.5
1998ic_pr_burst_freq = 5.5
1999ic_new_time = 6.5
2000ic_burst_penalty = 7.5
2001ic_held_release_interval = 8.5
2002egress_control = Yes
2003ec_pr_freq = 9.5
2004"#;
2005
2006 let config = parse(input).unwrap();
2007
2008 assert_eq!(config.reticulum.default_ic_max_held_announces, 17);
2009 assert_eq!(config.reticulum.default_ic_burst_hold, 1.5);
2010 assert_eq!(config.reticulum.default_ic_burst_freq_new, 2.5);
2011 assert_eq!(config.reticulum.default_ic_burst_freq, 3.5);
2012 assert_eq!(config.reticulum.default_ic_pr_burst_freq_new, 4.5);
2013 assert_eq!(config.reticulum.default_ic_pr_burst_freq, 5.5);
2014 assert_eq!(config.reticulum.default_ic_new_time, 6.5);
2015 assert_eq!(config.reticulum.default_ic_burst_penalty, 7.5);
2016 assert_eq!(config.reticulum.default_ic_held_release_interval, 8.5);
2017 assert!(config.reticulum.default_egress_control);
2018 assert_eq!(config.reticulum.default_ec_pr_freq, 9.5);
2019 }
2020
2021 #[test]
2022 fn parse_reticulum_ingress_and_egress_defaults_reject_invalid_values() {
2023 for (key, value) in [
2024 ("ic_max_held_announces", "-1"),
2025 ("ic_burst_hold", "-1"),
2026 ("ic_burst_hold", "NaN"),
2027 ("ic_burst_freq_new", "-1"),
2028 ("ic_burst_freq", "inf"),
2029 ("ic_pr_burst_freq_new", "-1"),
2030 ("ic_pr_burst_freq", "NaN"),
2031 ("ic_new_time", "-1"),
2032 ("ic_burst_penalty", "-1"),
2033 ("ic_held_release_interval", "-1"),
2034 ("ec_pr_freq", "-1"),
2035 ("ec_pr_freq", "inf"),
2036 ] {
2037 let input = format!("[reticulum]\n{key} = {value}\n");
2038 let err = parse(&input).unwrap_err();
2039 assert!(
2040 format!("{err}").contains(key),
2041 "error {err} should mention {key}"
2042 );
2043 }
2044 }
2045
2046 #[test]
2047 fn parse_backbone_peer_pool_defaults_disabled() {
2048 let config = parse("[reticulum]\n").unwrap();
2049 assert_eq!(config.reticulum.backbone_peer_pool_max_connected, 0);
2050 assert_eq!(config.reticulum.backbone_peer_pool_failure_threshold, 3);
2051 assert_eq!(config.reticulum.backbone_peer_pool_failure_window, 600);
2052 assert_eq!(config.reticulum.backbone_peer_pool_cooldown, 900);
2053 }
2054
2055 #[test]
2056 fn parse_announce_table_limits_reject_zero() {
2057 let err = parse(
2058 r#"
2059[reticulum]
2060announce_table_ttl = 0
2061"#,
2062 )
2063 .unwrap_err();
2064 assert!(matches!(
2065 err,
2066 ConfigError::InvalidValue { key, .. } if key == "announce_table_ttl"
2067 ));
2068
2069 let err = parse(
2070 r#"
2071[reticulum]
2072known_destinations_max_entries = 0
2073"#,
2074 )
2075 .unwrap_err();
2076 assert!(matches!(
2077 err,
2078 ConfigError::InvalidValue { key, .. } if key == "known_destinations_max_entries"
2079 ));
2080
2081 let err = parse(
2082 r#"
2083[reticulum]
2084ratchet_expiry = 0
2085"#,
2086 )
2087 .unwrap_err();
2088 assert!(matches!(
2089 err,
2090 ConfigError::InvalidValue { key, .. } if key == "ratchet_expiry"
2091 ));
2092
2093 let err = parse(
2094 r#"
2095[reticulum]
2096announce_table_max_bytes = 0
2097"#,
2098 )
2099 .unwrap_err();
2100 assert!(matches!(
2101 err,
2102 ConfigError::InvalidValue { key, .. } if key == "announce_table_max_bytes"
2103 ));
2104
2105 let err = parse(
2106 r#"
2107[reticulum]
2108announce_queue_max_entries = 0
2109"#,
2110 )
2111 .unwrap_err();
2112 assert!(matches!(
2113 err,
2114 ConfigError::InvalidValue { key, .. } if key == "announce_queue_max_entries"
2115 ));
2116
2117 let err = parse(
2118 r#"
2119[reticulum]
2120announce_queue_max_interfaces = 0
2121"#,
2122 )
2123 .unwrap_err();
2124 assert!(matches!(
2125 err,
2126 ConfigError::InvalidValue { key, .. } if key == "announce_queue_max_interfaces"
2127 ));
2128
2129 let err = parse(
2130 r#"
2131[reticulum]
2132announce_queue_max_bytes = 0
2133"#,
2134 )
2135 .unwrap_err();
2136 assert!(matches!(
2137 err,
2138 ConfigError::InvalidValue { key, .. } if key == "announce_queue_max_bytes"
2139 ));
2140
2141 let err = parse(
2142 r#"
2143[reticulum]
2144driver_event_queue_capacity = 0
2145"#,
2146 )
2147 .unwrap_err();
2148 assert!(matches!(
2149 err,
2150 ConfigError::InvalidValue { key, .. } if key == "driver_event_queue_capacity"
2151 ));
2152
2153 let err = parse(
2154 r#"
2155[reticulum]
2156interface_writer_queue_capacity = 0
2157"#,
2158 )
2159 .unwrap_err();
2160 assert!(matches!(
2161 err,
2162 ConfigError::InvalidValue { key, .. } if key == "interface_writer_queue_capacity"
2163 ));
2164
2165 let err = parse(
2166 r#"
2167[reticulum]
2168announce_queue_ttl = 0
2169"#,
2170 )
2171 .unwrap_err();
2172 assert!(matches!(
2173 err,
2174 ConfigError::InvalidValue { key, .. } if key == "announce_queue_ttl"
2175 ));
2176 }
2177
2178 #[test]
2179 fn parse_announce_queue_overflow_policy_rejects_invalid() {
2180 let err = parse(
2181 r#"
2182[reticulum]
2183announce_queue_overflow_policy = keep_everything
2184"#,
2185 )
2186 .unwrap_err();
2187 assert!(matches!(
2188 err,
2189 ConfigError::InvalidValue { key, .. } if key == "announce_queue_overflow_policy"
2190 ));
2191 }
2192
2193 #[test]
2194 fn parse_destination_timeout_secs_alias() {
2195 let config = parse(
2196 r#"
2197[reticulum]
2198destination_timeout_secs = 777
2199"#,
2200 )
2201 .unwrap();
2202
2203 assert_eq!(config.reticulum.known_destinations_ttl, 777);
2204 }
2205
2206 #[test]
2207 fn parse_logging_section() {
2208 let input = "[logging]\nloglevel = 6\nlogtimestamps = no\n";
2209 let config = parse(input).unwrap();
2210 assert_eq!(config.logging.loglevel, 6);
2211 assert!(!config.logging.logtimestamps);
2212 }
2213
2214 #[test]
2215 fn parse_logging_accepts_pathing_and_extreme_levels() {
2216 for level in [7, 8] {
2217 let config = parse(&format!("[logging]\nloglevel = {level}\n")).unwrap();
2218 assert_eq!(config.logging.loglevel, level);
2219 }
2220 }
2221
2222 #[test]
2223 fn parse_logging_rejects_levels_above_extreme() {
2224 for value in ["9", "255", "256", "-1"] {
2225 let err = parse(&format!("[logging]\nloglevel = {value}\n")).unwrap_err();
2226 assert!(matches!(
2227 err,
2228 ConfigError::InvalidValue { key, .. } if key == "loglevel"
2229 ));
2230 }
2231 }
2232
2233 #[test]
2234 fn parse_logging_rejects_invalid_logtimestamps() {
2235 let err = parse("[logging]\nlogtimestamps = maybe\n").unwrap_err();
2236 assert!(matches!(
2237 err,
2238 ConfigError::InvalidValue { key, .. } if key == "logtimestamps"
2239 ));
2240 }
2241
2242 #[test]
2243 fn parse_interface_tcp_client() {
2244 let input = r#"
2245[interfaces]
2246 [[TCP Client]]
2247 type = TCPClientInterface
2248 enabled = Yes
2249 target_host = 87.106.8.245
2250 target_port = 4242
2251"#;
2252 let config = parse(input).unwrap();
2253 assert_eq!(config.interfaces.len(), 1);
2254 let iface = &config.interfaces[0];
2255 assert_eq!(iface.name, "TCP Client");
2256 assert_eq!(iface.interface_type, "TCPClientInterface");
2257 assert!(iface.enabled);
2258 assert_eq!(iface.params.get("target_host").unwrap(), "87.106.8.245");
2259 assert_eq!(iface.params.get("target_port").unwrap(), "4242");
2260 }
2261
2262 #[test]
2263 fn parse_interface_tcp_server() {
2264 let input = r#"
2265[interfaces]
2266 [[TCP Server]]
2267 type = TCPServerInterface
2268 enabled = Yes
2269 listen_ip = 0.0.0.0
2270 listen_port = 4242
2271"#;
2272 let config = parse(input).unwrap();
2273 assert_eq!(config.interfaces.len(), 1);
2274 let iface = &config.interfaces[0];
2275 assert_eq!(iface.name, "TCP Server");
2276 assert_eq!(iface.interface_type, "TCPServerInterface");
2277 assert_eq!(iface.params.get("listen_ip").unwrap(), "0.0.0.0");
2278 assert_eq!(iface.params.get("listen_port").unwrap(), "4242");
2279 }
2280
2281 #[test]
2282 fn parse_interface_udp() {
2283 let input = r#"
2284[interfaces]
2285 [[UDP Interface]]
2286 type = UDPInterface
2287 enabled = Yes
2288 listen_ip = 0.0.0.0
2289 listen_port = 4242
2290 forward_ip = 255.255.255.255
2291 forward_port = 4242
2292"#;
2293 let config = parse(input).unwrap();
2294 assert_eq!(config.interfaces.len(), 1);
2295 let iface = &config.interfaces[0];
2296 assert_eq!(iface.name, "UDP Interface");
2297 assert_eq!(iface.interface_type, "UDPInterface");
2298 assert_eq!(iface.params.get("listen_ip").unwrap(), "0.0.0.0");
2299 assert_eq!(iface.params.get("forward_ip").unwrap(), "255.255.255.255");
2300 }
2301
2302 #[test]
2303 fn parse_multiple_interfaces() {
2304 let input = r#"
2305[interfaces]
2306 [[TCP Client]]
2307 type = TCPClientInterface
2308 target_host = 10.0.0.1
2309 target_port = 4242
2310
2311 [[UDP Broadcast]]
2312 type = UDPInterface
2313 listen_ip = 0.0.0.0
2314 listen_port = 5555
2315 forward_ip = 255.255.255.255
2316 forward_port = 5555
2317"#;
2318 let config = parse(input).unwrap();
2319 assert_eq!(config.interfaces.len(), 2);
2320 assert_eq!(config.interfaces[0].name, "TCP Client");
2321 assert_eq!(config.interfaces[0].interface_type, "TCPClientInterface");
2322 assert_eq!(config.interfaces[1].name, "UDP Broadcast");
2323 assert_eq!(config.interfaces[1].interface_type, "UDPInterface");
2324 }
2325
2326 #[test]
2327 fn parse_third_level_interface_sections() {
2328 let config = parse(
2329 "[interfaces]\n[[radio]]\n type = RNodeMultiInterface\n port = /dev/ttyUSB0\n [[[fast]]]\n enabled = yes\n vport = 7\n frequency = 915000000\n [[[disabled]]]\n enabled = no\n vport = 2\n",
2330 )
2331 .unwrap();
2332 assert_eq!(config.interfaces.len(), 1);
2333 let interface = &config.interfaces[0];
2334 assert_eq!(interface.params["port"], "/dev/ttyUSB0");
2335 assert_eq!(interface.subinterfaces.len(), 2);
2336 assert_eq!(interface.subinterfaces[0].name, "fast");
2337 assert_eq!(interface.subinterfaces[0].params["vport"], "7");
2338 }
2339
2340 #[test]
2341 fn parse_booleans() {
2342 for (input, expected) in &[
2344 ("Yes", true),
2345 ("No", false),
2346 ("True", true),
2347 ("False", false),
2348 ("true", true),
2349 ("false", false),
2350 ("1", true),
2351 ("0", false),
2352 ("on", true),
2353 ("off", false),
2354 ] {
2355 let result = parse_bool(input);
2356 assert_eq!(result, Some(*expected), "parse_bool({}) failed", input);
2357 }
2358 }
2359
2360 #[test]
2361 fn parse_comments() {
2362 let input = r#"
2363# This is a comment
2364[reticulum]
2365enable_transport = True # inline comment
2366# share_instance = No
2367instance_name = test
2368"#;
2369 let config = parse(input).unwrap();
2370 assert!(config.reticulum.enable_transport);
2371 assert!(config.reticulum.share_instance); assert_eq!(config.reticulum.instance_name, "test");
2373 }
2374
2375 #[test]
2376 fn parse_interface_mode_field() {
2377 let input = r#"
2378[interfaces]
2379 [[TCP Client]]
2380 type = TCPClientInterface
2381 interface_mode = access_point
2382 target_host = 10.0.0.1
2383 target_port = 4242
2384"#;
2385 let config = parse(input).unwrap();
2386 assert_eq!(config.interfaces[0].mode, "access_point");
2387 }
2388
2389 #[test]
2390 fn parse_mode_fallback() {
2391 let input = r#"
2393[interfaces]
2394 [[TCP Client]]
2395 type = TCPClientInterface
2396 mode = gateway
2397 target_host = 10.0.0.1
2398 target_port = 4242
2399"#;
2400 let config = parse(input).unwrap();
2401 assert_eq!(config.interfaces[0].mode, "gateway");
2402 }
2403
2404 #[test]
2405 fn parse_interface_mode_takes_precedence() {
2406 let input = r#"
2408[interfaces]
2409 [[TCP Client]]
2410 type = TCPClientInterface
2411 interface_mode = roaming
2412 mode = boundary
2413 target_host = 10.0.0.1
2414 target_port = 4242
2415"#;
2416 let config = parse(input).unwrap();
2417 assert_eq!(config.interfaces[0].mode, "roaming");
2418 }
2419
2420 #[test]
2421 fn parse_disabled_interface() {
2422 let input = r#"
2423[interfaces]
2424 [[Disabled TCP]]
2425 type = TCPClientInterface
2426 enabled = No
2427 target_host = 10.0.0.1
2428 target_port = 4242
2429"#;
2430 let config = parse(input).unwrap();
2431 assert_eq!(config.interfaces.len(), 1);
2432 assert!(!config.interfaces[0].enabled);
2433 }
2434
2435 #[test]
2436 fn parse_serial_interface() {
2437 let input = r#"
2438[interfaces]
2439 [[Serial Port]]
2440 type = SerialInterface
2441 enabled = Yes
2442 port = /dev/ttyUSB0
2443 speed = 115200
2444 databits = 8
2445 parity = N
2446 stopbits = 1
2447"#;
2448 let config = parse(input).unwrap();
2449 assert_eq!(config.interfaces.len(), 1);
2450 let iface = &config.interfaces[0];
2451 assert_eq!(iface.name, "Serial Port");
2452 assert_eq!(iface.interface_type, "SerialInterface");
2453 assert!(iface.enabled);
2454 assert_eq!(iface.params.get("port").unwrap(), "/dev/ttyUSB0");
2455 assert_eq!(iface.params.get("speed").unwrap(), "115200");
2456 assert_eq!(iface.params.get("databits").unwrap(), "8");
2457 assert_eq!(iface.params.get("parity").unwrap(), "N");
2458 assert_eq!(iface.params.get("stopbits").unwrap(), "1");
2459 }
2460
2461 #[test]
2462 fn parse_kiss_interface() {
2463 let input = r#"
2464[interfaces]
2465 [[KISS TNC]]
2466 type = KISSInterface
2467 enabled = Yes
2468 port = /dev/ttyUSB1
2469 speed = 9600
2470 preamble = 350
2471 txtail = 20
2472 persistence = 64
2473 slottime = 20
2474 flow_control = True
2475 id_interval = 600
2476 id_callsign = MYCALL
2477"#;
2478 let config = parse(input).unwrap();
2479 assert_eq!(config.interfaces.len(), 1);
2480 let iface = &config.interfaces[0];
2481 assert_eq!(iface.name, "KISS TNC");
2482 assert_eq!(iface.interface_type, "KISSInterface");
2483 assert_eq!(iface.params.get("port").unwrap(), "/dev/ttyUSB1");
2484 assert_eq!(iface.params.get("speed").unwrap(), "9600");
2485 assert_eq!(iface.params.get("preamble").unwrap(), "350");
2486 assert_eq!(iface.params.get("txtail").unwrap(), "20");
2487 assert_eq!(iface.params.get("persistence").unwrap(), "64");
2488 assert_eq!(iface.params.get("slottime").unwrap(), "20");
2489 assert_eq!(iface.params.get("flow_control").unwrap(), "True");
2490 assert_eq!(iface.params.get("id_interval").unwrap(), "600");
2491 assert_eq!(iface.params.get("id_callsign").unwrap(), "MYCALL");
2492 }
2493
2494 #[test]
2495 fn parse_ifac_networkname() {
2496 let input = r#"
2497[interfaces]
2498 [[TCP Client]]
2499 type = TCPClientInterface
2500 target_host = 10.0.0.1
2501 target_port = 4242
2502 networkname = testnet
2503"#;
2504 let config = parse(input).unwrap();
2505 assert_eq!(
2506 config.interfaces[0].params.get("networkname").unwrap(),
2507 "testnet"
2508 );
2509 }
2510
2511 #[test]
2512 fn parse_ifac_passphrase() {
2513 let input = r#"
2514[interfaces]
2515 [[TCP Client]]
2516 type = TCPClientInterface
2517 target_host = 10.0.0.1
2518 target_port = 4242
2519 passphrase = secret123
2520 ifac_size = 64
2521"#;
2522 let config = parse(input).unwrap();
2523 assert_eq!(
2524 config.interfaces[0].params.get("passphrase").unwrap(),
2525 "secret123"
2526 );
2527 assert_eq!(config.interfaces[0].params.get("ifac_size").unwrap(), "64");
2528 }
2529
2530 #[test]
2531 fn parse_remote_management_config() {
2532 let input = r#"
2533[reticulum]
2534enable_transport = True
2535enable_remote_management = Yes
2536remote_management_allowed = aabbccdd00112233aabbccdd00112233, 11223344556677881122334455667788
2537publish_blackhole = Yes
2538"#;
2539 let config = parse(input).unwrap();
2540 assert!(config.reticulum.enable_remote_management);
2541 assert!(config.reticulum.publish_blackhole);
2542 assert_eq!(config.reticulum.remote_management_allowed.len(), 2);
2543 assert_eq!(
2544 config.reticulum.remote_management_allowed[0],
2545 "aabbccdd00112233aabbccdd00112233"
2546 );
2547 assert_eq!(
2548 config.reticulum.remote_management_allowed[1],
2549 "11223344556677881122334455667788"
2550 );
2551 }
2552
2553 #[test]
2554 fn parse_remote_management_defaults() {
2555 let input = "[reticulum]\n";
2556 let config = parse(input).unwrap();
2557 assert!(!config.reticulum.enable_remote_management);
2558 assert!(!config.reticulum.publish_blackhole);
2559 assert!(config.reticulum.remote_management_allowed.is_empty());
2560 }
2561
2562 #[test]
2563 fn parse_hooks_section() {
2564 let input = r#"
2565[hooks]
2566 [[drop_tick]]
2567 path = /tmp/drop_tick.wasm
2568 attach_point = Tick
2569 priority = 10
2570 enabled = Yes
2571
2572 [[log_announce]]
2573 path = /tmp/log_announce.wasm
2574 type = native
2575 attach_point = AnnounceReceived
2576 priority = 5
2577 enabled = No
2578
2579 [[builtin_tick]]
2580 builtin = example.tick
2581 type = builtin
2582 attach_point = Tick
2583"#;
2584 let config = parse(input).unwrap();
2585 assert_eq!(config.hooks.len(), 3);
2586 assert_eq!(config.hooks[0].name, "drop_tick");
2587 assert_eq!(config.hooks[0].path, "/tmp/drop_tick.wasm");
2588 assert_eq!(config.hooks[0].hook_type, default_hook_type());
2589 assert_eq!(config.hooks[0].attach_point, "Tick");
2590 assert_eq!(config.hooks[0].priority, 10);
2591 assert!(config.hooks[0].enabled);
2592 assert_eq!(config.hooks[1].name, "log_announce");
2593 assert_eq!(config.hooks[1].hook_type, "native");
2594 assert_eq!(config.hooks[1].attach_point, "AnnounceReceived");
2595 assert!(!config.hooks[1].enabled);
2596 assert_eq!(config.hooks[2].hook_type, "builtin");
2597 assert_eq!(config.hooks[2].builtin_id.as_deref(), Some("example.tick"));
2598 }
2599
2600 #[test]
2601 fn parse_empty_hooks() {
2602 let input = "[hooks]\n";
2603 let config = parse(input).unwrap();
2604 assert!(config.hooks.is_empty());
2605 }
2606
2607 #[test]
2608 fn parse_hook_point_names() {
2609 assert_eq!(parse_hook_point("PreIngress"), Some(0));
2610 assert_eq!(parse_hook_point("PreDispatch"), Some(1));
2611 assert_eq!(parse_hook_point("AnnounceReceived"), Some(2));
2612 assert_eq!(parse_hook_point("PathUpdated"), Some(3));
2613 assert_eq!(parse_hook_point("AnnounceRetransmit"), Some(4));
2614 assert_eq!(parse_hook_point("LinkRequestReceived"), Some(5));
2615 assert_eq!(parse_hook_point("LinkEstablished"), Some(6));
2616 assert_eq!(parse_hook_point("LinkClosed"), Some(7));
2617 assert_eq!(parse_hook_point("InterfaceUp"), Some(8));
2618 assert_eq!(parse_hook_point("InterfaceDown"), Some(9));
2619 assert_eq!(parse_hook_point("InterfaceConfigChanged"), Some(10));
2620 assert_eq!(parse_hook_point("BackbonePeerConnected"), Some(11));
2621 assert_eq!(parse_hook_point("BackbonePeerDisconnected"), Some(12));
2622 assert_eq!(parse_hook_point("BackbonePeerIdleTimeout"), Some(13));
2623 assert_eq!(parse_hook_point("BackbonePeerWriteStall"), Some(14));
2624 assert_eq!(parse_hook_point("BackbonePeerPenalty"), Some(15));
2625 assert_eq!(parse_hook_point("SendOnInterface"), Some(16));
2626 assert_eq!(parse_hook_point("BroadcastOnAllInterfaces"), Some(17));
2627 assert_eq!(parse_hook_point("DeliverLocal"), Some(18));
2628 assert_eq!(parse_hook_point("TunnelSynthesize"), Some(19));
2629 assert_eq!(parse_hook_point("Tick"), Some(20));
2630 assert_eq!(parse_hook_point("Unknown"), None);
2631 }
2632
2633 #[test]
2634 fn backbone_extra_params_preserved() {
2635 let config = r#"
2636[reticulum]
2637enable_transport = True
2638
2639[interfaces]
2640 [[Public Entrypoint]]
2641 type = BackboneInterface
2642 enabled = yes
2643 listen_ip = 0.0.0.0
2644 listen_port = 4242
2645 interface_mode = gateway
2646 discoverable = Yes
2647 discovery_name = PizzaSpaghettiMandolino
2648 announce_interval = 600
2649 discovery_stamp_value = 24
2650 reachable_on = 87.106.8.245
2651"#;
2652 let parsed = parse(config).unwrap();
2653 assert_eq!(parsed.interfaces.len(), 1);
2654 let iface = &parsed.interfaces[0];
2655 assert_eq!(iface.name, "Public Entrypoint");
2656 assert_eq!(iface.interface_type, "BackboneInterface");
2657 assert_eq!(
2659 iface.params.get("discoverable").map(|s| s.as_str()),
2660 Some("Yes")
2661 );
2662 assert_eq!(
2663 iface.params.get("discovery_name").map(|s| s.as_str()),
2664 Some("PizzaSpaghettiMandolino")
2665 );
2666 assert_eq!(
2667 iface.params.get("announce_interval").map(|s| s.as_str()),
2668 Some("600")
2669 );
2670 assert_eq!(
2671 iface
2672 .params
2673 .get("discovery_stamp_value")
2674 .map(|s| s.as_str()),
2675 Some("24")
2676 );
2677 assert_eq!(
2678 iface.params.get("reachable_on").map(|s| s.as_str()),
2679 Some("87.106.8.245")
2680 );
2681 assert_eq!(
2682 iface.params.get("listen_ip").map(|s| s.as_str()),
2683 Some("0.0.0.0")
2684 );
2685 assert_eq!(
2686 iface.params.get("listen_port").map(|s| s.as_str()),
2687 Some("4242")
2688 );
2689 }
2690
2691 #[test]
2692 fn parse_probe_protocol() {
2693 let input = r#"
2694[reticulum]
2695probe_addr = 1.2.3.4:19302
2696probe_protocol = stun
2697"#;
2698 let config = parse(input).unwrap();
2699 assert_eq!(
2700 config.reticulum.probe_addr.as_deref(),
2701 Some("1.2.3.4:19302")
2702 );
2703 assert_eq!(config.reticulum.probe_protocol.as_deref(), Some("stun"));
2704 }
2705
2706 #[test]
2707 fn parse_probe_protocol_defaults_to_none() {
2708 let input = r#"
2709[reticulum]
2710probe_addr = 1.2.3.4:4343
2711"#;
2712 let config = parse(input).unwrap();
2713 assert_eq!(config.reticulum.probe_addr.as_deref(), Some("1.2.3.4:4343"));
2714 assert!(config.reticulum.probe_protocol.is_none());
2715 }
2716
2717 #[test]
2718 fn parse_direct_connect_policy() {
2719 let config = parse(
2720 r#"
2721[reticulum]
2722direct_connect_policy = identified_only
2723"#,
2724 )
2725 .unwrap();
2726 assert_eq!(
2727 config.reticulum.direct_connect_policy,
2728 crate::event::HolePunchPolicy::IdentifiedOnly
2729 );
2730 }
2731
2732 #[test]
2733 fn direct_connect_policy_defaults_to_identified_only_and_rejects_invalid() {
2734 let config = parse("[reticulum]\n").unwrap();
2735 assert_eq!(
2736 config.reticulum.direct_connect_policy,
2737 crate::event::HolePunchPolicy::IdentifiedOnly
2738 );
2739 let config = parse("[reticulum]\ndirect_connect_policy = accept_all\n").unwrap();
2740 assert_eq!(
2741 config.reticulum.direct_connect_policy,
2742 crate::event::HolePunchPolicy::AcceptAll
2743 );
2744 assert!(parse("[reticulum]\ndirect_connect_policy = sometimes\n").is_err());
2745 }
2746
2747 #[test]
2748 fn direct_connect_allow_private_endpoints_parses_and_defaults_false() {
2749 let config = parse("[reticulum]\n").unwrap();
2750 assert!(!config.reticulum.direct_connect_allow_private_endpoints);
2751 let config = parse("[reticulum]\ndirect_connect_allow_private_endpoints = yes\n").unwrap();
2752 assert!(config.reticulum.direct_connect_allow_private_endpoints);
2753 let config =
2754 parse("[reticulum]\ndirect_connect_allow_private_endpoints = False\n").unwrap();
2755 assert!(!config.reticulum.direct_connect_allow_private_endpoints);
2756 assert!(parse("[reticulum]\ndirect_connect_allow_private_endpoints = maybe\n").is_err());
2757 }
2758
2759 #[test]
2760 fn link_mtu_discovery_defaults_true_and_honors_both_boolean_values() {
2761 assert!(parse_link_mtu_discovery("").unwrap());
2762 assert!(parse_link_mtu_discovery("[reticulum]\nlink_mtu_discovery = Yes\n").unwrap());
2763 assert!(!parse_link_mtu_discovery("[reticulum]\nlink_mtu_discovery = False\n").unwrap());
2764 assert!(parse_link_mtu_discovery("[logging]\nlink_mtu_discovery = No\n").unwrap());
2765 assert!(parse_link_mtu_discovery("[reticulum]\nlink_mtu_discovery = maybe\n").is_err());
2766 }
2767
2768 #[test]
2769 fn filter_section_defaults_to_no_filtering() {
2770 let config = parse("").unwrap();
2771 assert_eq!(config.filter, FilterSection::default());
2772 assert!(!config.filter.required_only);
2773 }
2774
2775 #[test]
2776 fn filter_section_parses_every_option() {
2777 let config = parse(
2778 "[filter]\n\
2779 required_only = Yes\n\
2780 ignored_types = nomadnetwork.node, lxmf.propagation\n\
2781 allowed_types = lxmf.delivery\n",
2782 )
2783 .unwrap();
2784 assert_eq!(
2785 config.filter,
2786 FilterSection {
2787 required_only: true,
2788 ignored_types: vec!["nomadnetwork.node".into(), "lxmf.propagation".into()],
2789 allowed_types: vec!["lxmf.delivery".into()],
2790 }
2791 );
2792
2793 use rns_core::transport::destination_filter::destination_type;
2794 let filter = config.filter.destination_filter();
2795 assert!(filter.required_only());
2796 assert!(!filter.allows(&destination_type("nomadnetwork.node"), true));
2797 assert!(filter.allows(&destination_type("lxmf.delivery"), false));
2798 assert!(!filter.allows(&destination_type("rncp.receive"), false));
2799 }
2800
2801 #[test]
2802 fn filter_section_rejects_an_invalid_boolean() {
2803 assert!(parse("[filter]\nrequired_only = maybe\n").is_err());
2804 }
2805
2806 #[test]
2807 fn stamps_section_defaults_to_the_protocol_constants() {
2808 use rns_core::constants;
2809 let config = parse("").unwrap();
2810 assert_eq!(config.stamps, StampsSection::default());
2811 let stamps = config.stamps;
2812 let flags = [
2813 stamps.stamps_enforced,
2814 stamps.lr_stamps_enforced,
2815 stamps.stamp_destinations,
2816 stamps.stamp_prs,
2817 stamps.always_stamp_lrs,
2818 ];
2819 assert_eq!(flags, [false; 5]);
2820 assert!(stamps.stamp_lrs);
2822 assert_eq!(stamps.destination_cost, constants::DESTINATION_STAMP_COST);
2823 assert_eq!(stamps.destination_value, StampCost::from_whole(25));
2824 assert_eq!(stamps.pr_cost, constants::PATH_REQUEST_STAMP_COST);
2825 assert_eq!(stamps.pr_value, constants::PATH_REQUEST_STAMP_VALUE);
2826 assert_eq!(stamps.lr_cost, constants::LINK_REQUEST_STAMP_COST);
2827 assert_eq!(
2828 stamps.link_stamp_timeout(),
2829 std::time::Duration::from_secs(15)
2830 );
2831 assert_eq!(
2832 stamps.path_request_stamp_timeout(),
2833 std::time::Duration::from_secs(15)
2834 );
2835 assert_eq!(config.reticulum.hop_limiter, constants::PATHFINDER_M);
2836 }
2837
2838 #[test]
2839 fn stamps_section_parses_every_option() {
2840 let config = parse(
2841 "[stamps]\n\
2842 enforce_stamps = yes\n\
2843 stamp_destinations = Yes\n\
2844 stamp_prs = true\n\
2845 always_stamp_lrs = on\n\
2846 destination_cost = 20\n\
2847 destination_value = 30\n\
2848 pr_cost = 12\n\
2849 pr_value = 14\n\
2850 lr_cost = 9\n\
2851 lr_stamp_timeout = 2.5\n\
2852 pr_stamp_timeout = 7\n",
2853 )
2854 .unwrap();
2855 let stamps = config.stamps;
2856 assert!(stamps.stamps_enforced);
2858 assert!(stamps.lr_stamps_enforced);
2859 assert!(stamps.stamp_destinations);
2860 assert!(stamps.stamp_prs);
2861 assert!(stamps.always_stamp_lrs);
2862 assert_eq!(stamps.destination_cost, StampCost::from_whole(20));
2863 assert_eq!(stamps.destination_value, StampCost::from_whole(30));
2864 assert_eq!(stamps.pr_cost, StampCost::from_whole(12));
2865 assert_eq!(stamps.pr_value, StampCost::from_whole(14));
2866 assert_eq!(stamps.lr_cost, StampCost::from_whole(9));
2867 assert_eq!(
2868 stamps.link_stamp_timeout(),
2869 std::time::Duration::from_millis(2500)
2870 );
2871 assert_eq!(
2872 stamps.path_request_stamp_timeout(),
2873 std::time::Duration::from_secs(7)
2874 );
2875 }
2876
2877 #[test]
2878 fn stamps_section_booleans_can_only_enable() {
2879 let config = parse(
2882 "[stamps]\n\
2883 enforce_stamps = no\n\
2884 stamp_destinations = False\n\
2885 stamp_prs = off\n\
2886 always_stamp_lrs = 0\n",
2887 )
2888 .unwrap();
2889 assert_eq!(config.stamps, StampsSection::default());
2890 assert!(parse("[stamps]\nstamp_prs = maybe\n").is_err());
2891 assert!(parse("[stamps]\nenforce_stamps = 2\n").is_err());
2892 }
2893
2894 #[test]
2895 fn stamps_section_ignores_the_removed_stamp_lrs_option() {
2896 let config = parse("[stamps]\nstamp_lrs = no\n").unwrap();
2898 assert_eq!(config.stamps, StampsSection::default());
2899 assert!(config.stamps.stamp_lrs);
2900 }
2901
2902 #[test]
2903 fn stamps_section_costs_are_fractional() {
2904 let config = parse(
2906 "[stamps]\n\
2907 destination_cost = 23.5\n\
2908 pr_value = 15.8\n\
2909 lr_cost = 12\n\
2910 pr_cost = inf\n\
2911 destination_value = nan\n",
2912 )
2913 .unwrap();
2914 let stamps = config.stamps;
2915 assert_eq!(stamps.destination_cost, StampCost::from_fp(6016));
2916 assert_eq!(stamps.pr_value, StampCost::from_f64(15.8));
2917 assert_eq!(stamps.lr_cost, StampCost::from_whole(12));
2918 assert_eq!(stamps.pr_cost, StampCost::from_whole(100));
2920 assert_eq!(stamps.destination_value, StampCost::ZERO);
2921 assert!(parse("[stamps]\npr_cost = high\n").is_err());
2922 }
2923
2924 #[test]
2925 fn stamps_section_clamps_costs_and_timeouts() {
2926 use rns_core::constants;
2927 let config = parse(
2928 "[stamps]\n\
2929 destination_cost = 500\n\
2930 destination_value = -3\n\
2931 pr_cost = 101\n\
2932 pr_value = 99999999999999999999999\n\
2933 lr_cost = 41\n\
2934 lr_stamp_timeout = 0\n\
2935 pr_stamp_timeout = -10\n",
2936 )
2937 .unwrap();
2938 let stamps = config.stamps;
2939 let cap = StampCost::from_whole(u32::from(constants::STAMP_COST_CAP));
2940 assert_eq!(stamps.destination_cost, cap);
2941 assert_eq!(stamps.destination_value, StampCost::ZERO);
2942 assert_eq!(stamps.pr_cost, cap);
2943 assert_eq!(stamps.pr_value, cap);
2944 assert_eq!(
2946 stamps.lr_cost,
2947 StampCost::from_whole(u32::from(constants::LINK_COST_CAP))
2948 );
2949 assert_eq!(
2950 stamps.link_stamp_timeout(),
2951 std::time::Duration::from_millis(20)
2952 );
2953 assert_eq!(
2954 stamps.path_request_stamp_timeout(),
2955 std::time::Duration::from_millis(20)
2956 );
2957
2958 let config = parse("[stamps]\nlr_cost = -1\npr_stamp_timeout = nan\n").unwrap();
2959 assert_eq!(config.stamps.lr_cost, StampCost::ZERO);
2960 assert_eq!(
2961 config.stamps.path_request_stamp_timeout(),
2962 std::time::Duration::from_millis(20)
2963 );
2964 let config = parse("[stamps]\nlr_stamp_timeout = inf\n").unwrap();
2965 assert_eq!(config.stamps.link_stamp_timeout(), std::time::Duration::MAX);
2966
2967 assert!(parse("[stamps]\npr_cost = lots\n").is_err());
2969 assert!(parse("[stamps]\npr_stamp_timeout = soon\n").is_err());
2970 }
2971
2972 #[test]
2973 fn stamps_options_outside_the_stamps_section_are_ignored() {
2974 let config = parse("[reticulum]\nstamp_prs = yes\ndestination_cost = 5\n").unwrap();
2975 assert_eq!(config.stamps, StampsSection::default());
2976 }
2977
2978 #[test]
2979 fn hop_limiter_parses_and_clamps_to_pathfinder_range() {
2980 use rns_core::constants::PATHFINDER_M;
2981 let limiter = |value: &str| {
2982 parse(&format!("[reticulum]\nhop_limiter = {value}\n"))
2983 .unwrap()
2984 .reticulum
2985 .hop_limiter
2986 };
2987 assert_eq!(limiter("16"), 16);
2988 assert_eq!(limiter("1"), 1);
2989 assert_eq!(limiter("0"), 1);
2990 assert_eq!(limiter("-5"), 1);
2991 assert_eq!(limiter("128"), PATHFINDER_M);
2992 assert_eq!(limiter("1000"), PATHFINDER_M);
2993 assert!(parse("[reticulum]\nhop_limiter = many\n").is_err());
2994 assert!(parse("[reticulum]\nhop_limiter = 4.5\n").is_err());
2995 }
2996}