Skip to main content

fips_core/config/
node.rs

1//! Node configuration subsections.
2//!
3//! All the `node.*` configuration parameters: resource limits, rate limiting,
4//! retry/backoff, cache sizing, discovery, spanning tree, bloom filters,
5//! session management, and internal buffers.
6
7use serde::{Deserialize, Serialize};
8
9use super::IdentityConfig;
10use crate::mmp::{DEFAULT_LOG_INTERVAL_SECS, DEFAULT_OWD_WINDOW_SIZE, MmpConfig, MmpMode};
11
12// ============================================================================
13// Node Configuration Subsections
14// ============================================================================
15
16/// Resource limits (`node.limits.*`).
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct LimitsConfig {
19    /// Max handshake-phase connections (`node.limits.max_connections`).
20    #[serde(default = "LimitsConfig::default_max_connections")]
21    pub max_connections: usize,
22    /// Max authenticated peers (`node.limits.max_peers`).
23    #[serde(default = "LimitsConfig::default_max_peers")]
24    pub max_peers: usize,
25    /// Max active links (`node.limits.max_links`).
26    #[serde(default = "LimitsConfig::default_max_links")]
27    pub max_links: usize,
28    /// Max pending inbound handshakes (`node.limits.max_pending_inbound`).
29    #[serde(default = "LimitsConfig::default_max_pending_inbound")]
30    pub max_pending_inbound: usize,
31}
32
33impl Default for LimitsConfig {
34    fn default() -> Self {
35        Self {
36            max_connections: 256,
37            max_peers: 128,
38            max_links: 256,
39            max_pending_inbound: 1000,
40        }
41    }
42}
43
44impl LimitsConfig {
45    fn default_max_connections() -> usize {
46        256
47    }
48    fn default_max_peers() -> usize {
49        128
50    }
51    fn default_max_links() -> usize {
52        256
53    }
54    fn default_max_pending_inbound() -> usize {
55        1000
56    }
57}
58
59/// Connected UDP fast-path configuration (`node.connected_udp.*`).
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct ConnectedUdpConfig {
62    /// Enable per-peer connected UDP sockets (`node.connected_udp.enabled`).
63    ///
64    /// Environment overrides are still honored for operational A/B tests:
65    /// `FIPS_CONNECTED_UDP`, and on macOS `FIPS_MACOS_CONNECTED_UDP`.
66    #[serde(default = "ConnectedUdpConfig::default_enabled")]
67    pub enabled: bool,
68
69    /// Number of process file descriptors to leave for non-connected-UDP use
70    /// (`node.connected_udp.fd_reserve`).
71    ///
72    /// This is headroom, not a peer cap. Connected UDP uses three FDs per
73    /// installed peer, so the effective fast-path peer budget is roughly
74    /// `(RLIMIT_NOFILE - fd_reserve) / 3`, also bounded by
75    /// `node.limits.max_peers`.
76    #[serde(default = "ConnectedUdpConfig::default_fd_reserve")]
77    pub fd_reserve: usize,
78}
79
80impl Default for ConnectedUdpConfig {
81    fn default() -> Self {
82        Self {
83            enabled: Self::default_enabled(),
84            fd_reserve: Self::default_fd_reserve(),
85        }
86    }
87}
88
89impl ConnectedUdpConfig {
90    fn default_enabled() -> bool {
91        true
92    }
93
94    fn default_fd_reserve() -> usize {
95        128
96    }
97}
98
99/// Rate limiting (`node.rate_limit.*`).
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct RateLimitConfig {
102    /// Token bucket burst capacity (`node.rate_limit.handshake_burst`).
103    #[serde(default = "RateLimitConfig::default_handshake_burst")]
104    pub handshake_burst: u32,
105    /// Tokens/sec refill rate (`node.rate_limit.handshake_rate`).
106    #[serde(default = "RateLimitConfig::default_handshake_rate")]
107    pub handshake_rate: f64,
108    /// Stale handshake cleanup timeout in seconds (`node.rate_limit.handshake_timeout_secs`).
109    #[serde(default = "RateLimitConfig::default_handshake_timeout_secs")]
110    pub handshake_timeout_secs: u64,
111    /// Initial handshake resend interval in ms (`node.rate_limit.handshake_resend_interval_ms`).
112    /// Handshake messages are resent with exponential backoff within the timeout window.
113    #[serde(default = "RateLimitConfig::default_handshake_resend_interval_ms")]
114    pub handshake_resend_interval_ms: u64,
115    /// Handshake resend backoff multiplier (`node.rate_limit.handshake_resend_backoff`).
116    #[serde(default = "RateLimitConfig::default_handshake_resend_backoff")]
117    pub handshake_resend_backoff: f64,
118    /// Max handshake resends per attempt (`node.rate_limit.handshake_max_resends`).
119    #[serde(default = "RateLimitConfig::default_handshake_max_resends")]
120    pub handshake_max_resends: u32,
121}
122
123impl Default for RateLimitConfig {
124    fn default() -> Self {
125        Self {
126            handshake_burst: 100,
127            handshake_rate: 10.0,
128            handshake_timeout_secs: 30,
129            handshake_resend_interval_ms: 1000,
130            handshake_resend_backoff: 2.0,
131            handshake_max_resends: 5,
132        }
133    }
134}
135
136impl RateLimitConfig {
137    fn default_handshake_burst() -> u32 {
138        100
139    }
140    fn default_handshake_rate() -> f64 {
141        10.0
142    }
143    fn default_handshake_timeout_secs() -> u64 {
144        30
145    }
146    fn default_handshake_resend_interval_ms() -> u64 {
147        1000
148    }
149    fn default_handshake_resend_backoff() -> f64 {
150        2.0
151    }
152    fn default_handshake_max_resends() -> u32 {
153        5
154    }
155}
156
157/// Retry/backoff configuration (`node.retry.*`).
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct RetryConfig {
160    /// Max connection retry attempts (`node.retry.max_retries`).
161    #[serde(default = "RetryConfig::default_max_retries")]
162    pub max_retries: u32,
163    /// Base backoff interval in seconds (`node.retry.base_interval_secs`).
164    #[serde(default = "RetryConfig::default_base_interval_secs")]
165    pub base_interval_secs: u64,
166    /// Cap on exponential backoff in seconds (`node.retry.max_backoff_secs`).
167    #[serde(default = "RetryConfig::default_max_backoff_secs")]
168    pub max_backoff_secs: u64,
169}
170
171impl Default for RetryConfig {
172    fn default() -> Self {
173        Self {
174            max_retries: 5,
175            base_interval_secs: 5,
176            max_backoff_secs: 300,
177        }
178    }
179}
180
181impl RetryConfig {
182    fn default_max_retries() -> u32 {
183        5
184    }
185    fn default_base_interval_secs() -> u64 {
186        5
187    }
188    fn default_max_backoff_secs() -> u64 {
189        300
190    }
191}
192
193/// Cache parameters (`node.cache.*`).
194#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct CacheConfig {
196    /// Max entries in coord cache (`node.cache.coord_size`).
197    #[serde(default = "CacheConfig::default_coord_size")]
198    pub coord_size: usize,
199    /// Coord cache entry TTL in seconds (`node.cache.coord_ttl_secs`).
200    #[serde(default = "CacheConfig::default_coord_ttl_secs")]
201    pub coord_ttl_secs: u64,
202    /// Max entries in identity cache (`node.cache.identity_size`).
203    #[serde(default = "CacheConfig::default_identity_size")]
204    pub identity_size: usize,
205}
206
207impl Default for CacheConfig {
208    fn default() -> Self {
209        Self {
210            coord_size: 50_000,
211            coord_ttl_secs: 300,
212            identity_size: 10_000,
213        }
214    }
215}
216
217impl CacheConfig {
218    fn default_coord_size() -> usize {
219        50_000
220    }
221    fn default_coord_ttl_secs() -> u64 {
222        300
223    }
224    fn default_identity_size() -> usize {
225        10_000
226    }
227}
228
229/// Discovery protocol (`node.discovery.*`).
230#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct DiscoveryConfig {
232    /// Hop limit for LookupRequest flood (`node.discovery.ttl`).
233    #[serde(default = "DiscoveryConfig::default_ttl")]
234    pub ttl: u8,
235    /// Per-attempt timeouts in seconds (`node.discovery.attempt_timeouts_secs`).
236    /// Each entry is the time to wait for a response before sending the next
237    /// LookupRequest (with a fresh request_id). Sequence length determines the
238    /// total number of attempts before declaring the destination unreachable.
239    /// Default `[1, 2, 4, 8]` gives 4 attempts and a 15s total budget.
240    #[serde(default = "DiscoveryConfig::default_attempt_timeouts_secs")]
241    pub attempt_timeouts_secs: Vec<u64>,
242    /// Dedup cache expiry in seconds (`node.discovery.recent_expiry_secs`).
243    #[serde(default = "DiscoveryConfig::default_recent_expiry_secs")]
244    pub recent_expiry_secs: u64,
245    /// Base backoff after lookup failure in seconds (`node.discovery.backoff_base_secs`).
246    /// Doubles per consecutive failure up to `backoff_max_secs`. Set both to
247    /// 0 to disable post-failure suppression.
248    #[serde(default = "DiscoveryConfig::default_backoff_base_secs")]
249    pub backoff_base_secs: u64,
250    /// Maximum backoff cap in seconds (`node.discovery.backoff_max_secs`).
251    #[serde(default = "DiscoveryConfig::default_backoff_max_secs")]
252    pub backoff_max_secs: u64,
253    /// Minimum interval between forwarded lookups for the same target in seconds
254    /// (`node.discovery.forward_min_interval_secs`).
255    /// Defense-in-depth against misbehaving nodes.
256    #[serde(default = "DiscoveryConfig::default_forward_min_interval_secs")]
257    pub forward_min_interval_secs: u64,
258    /// Nostr-mediated overlay endpoint discovery.
259    #[serde(default = "DiscoveryConfig::default_nostr")]
260    pub nostr: NostrDiscoveryConfig,
261    /// mDNS / DNS-SD peer discovery on the local link. Identity surface
262    /// is a strict subset of what `nostr.advertise` already publishes
263    /// publicly, so there's no marginal privacy cost; the latency win
264    /// for same-LAN peers is large (sub-second pairing, no relay).
265    #[serde(default = "DiscoveryConfig::default_lan")]
266    pub lan: crate::discovery::lan::LanDiscoveryConfig,
267    /// Same-host process discovery through `~/.fips/instances/*.json`.
268    /// Embedded endpoints with a discovery scope enable this so local VMs,
269    /// browser helpers, and native app processes can find loopback-reachable
270    /// FIPS sockets without polling relays or relying on mDNS loopback.
271    #[serde(default = "DiscoveryConfig::default_local")]
272    pub local: crate::discovery::local::LocalInstanceDiscoveryConfig,
273}
274
275impl Default for DiscoveryConfig {
276    fn default() -> Self {
277        Self {
278            ttl: 64,
279            attempt_timeouts_secs: vec![1, 2, 4, 8],
280            recent_expiry_secs: 10,
281            backoff_base_secs: 30,
282            backoff_max_secs: 300,
283            forward_min_interval_secs: 2,
284            nostr: NostrDiscoveryConfig::default(),
285            lan: crate::discovery::lan::LanDiscoveryConfig::default(),
286            local: crate::discovery::local::LocalInstanceDiscoveryConfig::default(),
287        }
288    }
289}
290
291impl DiscoveryConfig {
292    fn default_ttl() -> u8 {
293        64
294    }
295    fn default_attempt_timeouts_secs() -> Vec<u64> {
296        vec![1, 2, 4, 8]
297    }
298    fn default_recent_expiry_secs() -> u64 {
299        10
300    }
301    fn default_backoff_base_secs() -> u64 {
302        30
303    }
304    fn default_backoff_max_secs() -> u64 {
305        300
306    }
307    fn default_forward_min_interval_secs() -> u64 {
308        2
309    }
310    fn default_nostr() -> NostrDiscoveryConfig {
311        NostrDiscoveryConfig::default()
312    }
313    fn default_lan() -> crate::discovery::lan::LanDiscoveryConfig {
314        crate::discovery::lan::LanDiscoveryConfig::default()
315    }
316    fn default_local() -> crate::discovery::local::LocalInstanceDiscoveryConfig {
317        crate::discovery::local::LocalInstanceDiscoveryConfig::default()
318    }
319}
320
321/// Nostr advert discovery policy.
322///
323/// Controls how overlay endpoint adverts are consumed:
324/// - `disabled`: ignore advert-derived endpoints for all peers
325/// - `configured_only`: allow advert fallback for configured peers
326/// - `open`: also consider adverts for non-configured peers
327#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
328#[serde(rename_all = "snake_case")]
329pub enum NostrDiscoveryPolicy {
330    Disabled,
331    #[default]
332    ConfiguredOnly,
333    Open,
334}
335
336/// Nostr-mediated overlay endpoint discovery (`node.discovery.nostr.*`).
337#[derive(Debug, Clone, Serialize, Deserialize)]
338#[serde(deny_unknown_fields)]
339pub struct NostrDiscoveryConfig {
340    /// Enable Nostr-signaled traversal bootstrap.
341    #[serde(default)]
342    pub enabled: bool,
343    /// Publish service advertisements so remote peers can bootstrap inbound.
344    #[serde(default = "NostrDiscoveryConfig::default_advertise")]
345    pub advertise: bool,
346    /// Relay URLs used for service advertisements.
347    #[serde(default = "NostrDiscoveryConfig::default_advert_relays")]
348    pub advert_relays: Vec<String>,
349    /// Relay URLs used for encrypted signaling events.
350    #[serde(default = "NostrDiscoveryConfig::default_dm_relays")]
351    pub dm_relays: Vec<String>,
352    /// STUN servers used for local reflexive address discovery.
353    /// Outbound observation uses only this local list; peer-advertised STUN
354    /// values are informational and are not treated as egress targets.
355    #[serde(default = "NostrDiscoveryConfig::default_stun_servers")]
356    pub stun_servers: Vec<String>,
357    /// Whether to advertise local (RFC 1918 / ULA) interface addresses as
358    /// host candidates in the traversal offer.
359    ///
360    /// Off by default: in most deployments the relevant peers are not on the
361    /// same broadcast domain, and sharing private host candidates causes
362    /// misleading punch successes when an asymmetric L3 path (corporate VPN,
363    /// Tailscale subnet route, overlapping address space, etc.) makes a
364    /// peer's private IP one-way reachable from this node. Enable only when
365    /// peers are on the same physical LAN and same-LAN punching is wanted.
366    #[serde(default)]
367    pub share_local_candidates: bool,
368    /// Traversal application namespace advertised in the Nostr protocol tag.
369    #[serde(default = "NostrDiscoveryConfig::default_app")]
370    pub app: String,
371    /// Signaling TTL in seconds.
372    #[serde(default = "NostrDiscoveryConfig::default_signal_ttl_secs")]
373    pub signal_ttl_secs: u64,
374    /// Policy for advert-derived endpoint discovery.
375    #[serde(default)]
376    pub policy: NostrDiscoveryPolicy,
377    /// Max number of open-discovery peers queued for outbound retry/connection
378    /// at once. Prevents unbounded queue growth from ambient advert traffic.
379    #[serde(default = "NostrDiscoveryConfig::default_open_discovery_max_pending")]
380    pub open_discovery_max_pending: usize,
381    /// Max concurrent inbound traversal offers processed at once.
382    /// Acts as a rate limit against offer spam from relays.
383    #[serde(default = "NostrDiscoveryConfig::default_max_concurrent_incoming_offers")]
384    pub max_concurrent_incoming_offers: usize,
385    /// Max cached overlay adverts retained from relay traffic.
386    /// Bounds memory under ambient advert volume.
387    #[serde(default = "NostrDiscoveryConfig::default_advert_cache_max_entries")]
388    pub advert_cache_max_entries: usize,
389    /// Max seen-session IDs retained for replay detection.
390    /// Oldest entries are evicted when the cap is exceeded.
391    #[serde(default = "NostrDiscoveryConfig::default_seen_sessions_max_entries")]
392    pub seen_sessions_max_entries: usize,
393    /// Overall punch attempt timeout in seconds.
394    #[serde(default = "NostrDiscoveryConfig::default_attempt_timeout_secs")]
395    pub attempt_timeout_secs: u64,
396    /// Replay tracking retention window in seconds.
397    #[serde(default = "NostrDiscoveryConfig::default_replay_window_secs")]
398    pub replay_window_secs: u64,
399    /// Delay before punch traffic starts.
400    #[serde(default = "NostrDiscoveryConfig::default_punch_start_delay_ms")]
401    pub punch_start_delay_ms: u64,
402    /// Interval between punch packets.
403    #[serde(default = "NostrDiscoveryConfig::default_punch_interval_ms")]
404    pub punch_interval_ms: u64,
405    /// How long to keep punching before failure.
406    #[serde(default = "NostrDiscoveryConfig::default_punch_duration_ms")]
407    pub punch_duration_ms: u64,
408    /// Advert TTL in seconds.
409    #[serde(default = "NostrDiscoveryConfig::default_advert_ttl_secs")]
410    pub advert_ttl_secs: u64,
411    /// How often adverts are refreshed in seconds.
412    #[serde(default = "NostrDiscoveryConfig::default_advert_refresh_secs")]
413    pub advert_refresh_secs: u64,
414    /// Settle delay in seconds after Nostr discovery starts before the
415    /// one-shot startup sweep of cached adverts runs. Allows the relay
416    /// subscription backlog to populate the in-memory advert cache.
417    /// Only used under `policy: open`. Default: 5.
418    #[serde(default = "NostrDiscoveryConfig::default_startup_sweep_delay_secs")]
419    pub startup_sweep_delay_secs: u64,
420    /// Maximum age in seconds for cached adverts considered by the
421    /// one-shot startup sweep. Adverts whose `created_at` is older than
422    /// `now - startup_sweep_max_age_secs` are skipped. Only used under
423    /// `policy: open`. Default: 3600 (1 hour).
424    #[serde(default = "NostrDiscoveryConfig::default_startup_sweep_max_age_secs")]
425    pub startup_sweep_max_age_secs: u64,
426    /// Number of consecutive NAT-traversal failures against a peer before
427    /// an extended cooldown is applied to throttle further offer publishes.
428    /// At this threshold the daemon also actively re-fetches the peer's
429    /// advert from `advert_relays` to evict cache entries for peers that
430    /// have gone away. Default: 5.
431    #[serde(default = "NostrDiscoveryConfig::default_failure_streak_threshold")]
432    pub failure_streak_threshold: u32,
433    /// Cooldown applied to a peer once `failure_streak_threshold` is hit.
434    /// Suppresses both open-discovery sweep enqueues and per-attempt
435    /// retry firings until elapsed. Default: 1800 (30 minutes).
436    #[serde(default = "NostrDiscoveryConfig::default_extended_cooldown_secs")]
437    pub extended_cooldown_secs: u64,
438    /// Minimum interval between `NAT traversal failed` WARN log lines for
439    /// the same peer. Subsequent failures inside the window log at DEBUG.
440    /// Reduces log spam on public-test nodes with many cache-learned
441    /// peers. Default: 300 (5 minutes).
442    #[serde(default = "NostrDiscoveryConfig::default_warn_log_interval_secs")]
443    pub warn_log_interval_secs: u64,
444    /// Maximum entries retained in the per-npub failure-state map.
445    /// Bounds memory under high cache turnover. Oldest entries (by last
446    /// failure time) evicted when the cap is exceeded. Default: 4096.
447    #[serde(default = "NostrDiscoveryConfig::default_failure_state_max_entries")]
448    pub failure_state_max_entries: usize,
449    /// Cooldown applied after observing a fatal protocol mismatch on a
450    /// Nostr-adopted bootstrap transport (e.g. `Unknown FMP version`
451    /// from a peer running a different FMP-protocol version). Independent
452    /// of `extended_cooldown_secs` and much longer because the mismatch
453    /// is structural — re-traversing the peer is wasted effort until one
454    /// side upgrades. Default: 86400 (24 hours).
455    #[serde(default = "NostrDiscoveryConfig::default_protocol_mismatch_cooldown_secs")]
456    pub protocol_mismatch_cooldown_secs: u64,
457}
458
459impl Default for NostrDiscoveryConfig {
460    fn default() -> Self {
461        Self {
462            enabled: false,
463            advertise: Self::default_advertise(),
464            advert_relays: Self::default_advert_relays(),
465            dm_relays: Self::default_dm_relays(),
466            stun_servers: Self::default_stun_servers(),
467            share_local_candidates: false,
468            app: Self::default_app(),
469            signal_ttl_secs: Self::default_signal_ttl_secs(),
470            policy: NostrDiscoveryPolicy::default(),
471            open_discovery_max_pending: Self::default_open_discovery_max_pending(),
472            max_concurrent_incoming_offers: Self::default_max_concurrent_incoming_offers(),
473            advert_cache_max_entries: Self::default_advert_cache_max_entries(),
474            seen_sessions_max_entries: Self::default_seen_sessions_max_entries(),
475            attempt_timeout_secs: Self::default_attempt_timeout_secs(),
476            replay_window_secs: Self::default_replay_window_secs(),
477            punch_start_delay_ms: Self::default_punch_start_delay_ms(),
478            punch_interval_ms: Self::default_punch_interval_ms(),
479            punch_duration_ms: Self::default_punch_duration_ms(),
480            advert_ttl_secs: Self::default_advert_ttl_secs(),
481            advert_refresh_secs: Self::default_advert_refresh_secs(),
482            startup_sweep_delay_secs: Self::default_startup_sweep_delay_secs(),
483            startup_sweep_max_age_secs: Self::default_startup_sweep_max_age_secs(),
484            failure_streak_threshold: Self::default_failure_streak_threshold(),
485            extended_cooldown_secs: Self::default_extended_cooldown_secs(),
486            warn_log_interval_secs: Self::default_warn_log_interval_secs(),
487            failure_state_max_entries: Self::default_failure_state_max_entries(),
488            protocol_mismatch_cooldown_secs: Self::default_protocol_mismatch_cooldown_secs(),
489        }
490    }
491}
492
493impl NostrDiscoveryConfig {
494    fn default_advertise() -> bool {
495        true
496    }
497
498    fn default_advert_relays() -> Vec<String> {
499        vec![
500            "wss://relay.damus.io".to_string(),
501            "wss://nos.lol".to_string(),
502            "wss://offchain.pub".to_string(),
503            "wss://temp.iris.to".to_string(),
504        ]
505    }
506
507    fn default_dm_relays() -> Vec<String> {
508        vec![
509            "wss://relay.damus.io".to_string(),
510            "wss://nos.lol".to_string(),
511            "wss://offchain.pub".to_string(),
512            "wss://temp.iris.to".to_string(),
513        ]
514    }
515
516    fn default_stun_servers() -> Vec<String> {
517        vec![
518            "stun:stun.l.google.com:19302".to_string(),
519            "stun:stun.cloudflare.com:3478".to_string(),
520            "stun:global.stun.twilio.com:3478".to_string(),
521        ]
522    }
523
524    fn default_app() -> String {
525        "fips-overlay-v1".to_string()
526    }
527
528    fn default_signal_ttl_secs() -> u64 {
529        120
530    }
531
532    fn default_open_discovery_max_pending() -> usize {
533        64
534    }
535
536    fn default_max_concurrent_incoming_offers() -> usize {
537        16
538    }
539
540    fn default_advert_cache_max_entries() -> usize {
541        2048
542    }
543
544    fn default_seen_sessions_max_entries() -> usize {
545        2048
546    }
547
548    fn default_attempt_timeout_secs() -> u64 {
549        10
550    }
551
552    fn default_replay_window_secs() -> u64 {
553        300
554    }
555
556    fn default_punch_start_delay_ms() -> u64 {
557        2_000
558    }
559
560    fn default_punch_interval_ms() -> u64 {
561        200
562    }
563
564    fn default_punch_duration_ms() -> u64 {
565        10_000
566    }
567
568    fn default_advert_ttl_secs() -> u64 {
569        3_600
570    }
571
572    fn default_advert_refresh_secs() -> u64 {
573        1_800
574    }
575
576    fn default_startup_sweep_delay_secs() -> u64 {
577        5
578    }
579
580    fn default_startup_sweep_max_age_secs() -> u64 {
581        3_600
582    }
583
584    fn default_failure_streak_threshold() -> u32 {
585        5
586    }
587
588    fn default_extended_cooldown_secs() -> u64 {
589        1_800
590    }
591
592    fn default_warn_log_interval_secs() -> u64 {
593        300
594    }
595
596    fn default_failure_state_max_entries() -> usize {
597        4_096
598    }
599
600    fn default_protocol_mismatch_cooldown_secs() -> u64 {
601        86_400
602    }
603}
604
605/// Spanning tree (`node.tree.*`).
606#[derive(Debug, Clone, Serialize, Deserialize)]
607pub struct TreeConfig {
608    /// Per-peer TreeAnnounce rate limit in ms (`node.tree.announce_min_interval_ms`).
609    #[serde(default = "TreeConfig::default_announce_min_interval_ms")]
610    pub announce_min_interval_ms: u64,
611    /// Hysteresis factor for cost-based parent re-selection (`node.tree.parent_hysteresis`).
612    ///
613    /// Only switch parents when the candidate's effective_depth is better than
614    /// `current_effective_depth * (1.0 - parent_hysteresis)`. Range: 0.0-1.0.
615    /// Set to 0.0 to disable hysteresis (switch on any improvement).
616    #[serde(default = "TreeConfig::default_parent_hysteresis")]
617    pub parent_hysteresis: f64,
618    /// Hold-down period after parent switch in seconds (`node.tree.hold_down_secs`).
619    ///
620    /// After switching parents, suppress re-evaluation for this duration to allow
621    /// MMP metrics to stabilize on the new link. Set to 0 to disable.
622    #[serde(default = "TreeConfig::default_hold_down_secs")]
623    pub hold_down_secs: u64,
624    /// Periodic parent re-evaluation interval in seconds (`node.tree.reeval_interval_secs`).
625    ///
626    /// How often to re-evaluate parent selection based on current MMP link costs,
627    /// independent of TreeAnnounce traffic. Catches link degradation after the
628    /// tree has stabilized. Set to 0 to disable.
629    #[serde(default = "TreeConfig::default_reeval_interval_secs")]
630    pub reeval_interval_secs: u64,
631    /// Flap dampening: max parent switches before extended hold-down (`node.tree.flap_threshold`).
632    #[serde(default = "TreeConfig::default_flap_threshold")]
633    pub flap_threshold: u32,
634    /// Flap dampening: window in seconds for counting switches (`node.tree.flap_window_secs`).
635    #[serde(default = "TreeConfig::default_flap_window_secs")]
636    pub flap_window_secs: u64,
637    /// Flap dampening: extended hold-down duration in seconds (`node.tree.flap_dampening_secs`).
638    #[serde(default = "TreeConfig::default_flap_dampening_secs")]
639    pub flap_dampening_secs: u64,
640}
641
642impl Default for TreeConfig {
643    fn default() -> Self {
644        Self {
645            announce_min_interval_ms: 500,
646            parent_hysteresis: 0.2,
647            hold_down_secs: 30,
648            reeval_interval_secs: 60,
649            flap_threshold: 4,
650            flap_window_secs: 60,
651            flap_dampening_secs: 120,
652        }
653    }
654}
655
656impl TreeConfig {
657    fn default_announce_min_interval_ms() -> u64 {
658        500
659    }
660    fn default_parent_hysteresis() -> f64 {
661        0.2
662    }
663    fn default_hold_down_secs() -> u64 {
664        30
665    }
666    fn default_reeval_interval_secs() -> u64 {
667        60
668    }
669    fn default_flap_threshold() -> u32 {
670        4
671    }
672    fn default_flap_window_secs() -> u64 {
673        60
674    }
675    fn default_flap_dampening_secs() -> u64 {
676        120
677    }
678}
679
680/// Routing strategy selection (`node.routing.*`).
681#[derive(Debug, Clone, Serialize, Deserialize)]
682pub struct RoutingConfig {
683    /// Next-hop selection mode (`node.routing.mode`).
684    #[serde(default)]
685    pub mode: RoutingMode,
686    /// TTL for learned reverse-path routes in seconds (`node.routing.learned_ttl_secs`).
687    #[serde(default = "RoutingConfig::default_learned_ttl_secs")]
688    pub learned_ttl_secs: u64,
689    /// Maximum locally observed next-hop candidates kept per destination for
690    /// reply-learned multipath/exploration
691    /// (`node.routing.max_learned_routes_per_dest`).
692    #[serde(default = "RoutingConfig::default_max_learned_routes_per_dest")]
693    pub max_learned_routes_per_dest: usize,
694    /// Every N learned-route selections, try the coordinate/bloom/tree route
695    /// instead so new paths can be discovered (`0` disables fallback exploration).
696    #[serde(default = "RoutingConfig::default_learned_fallback_explore_interval")]
697    pub learned_fallback_explore_interval: u64,
698}
699
700impl Default for RoutingConfig {
701    fn default() -> Self {
702        Self {
703            mode: RoutingMode::default(),
704            learned_ttl_secs: 300,
705            max_learned_routes_per_dest: 4,
706            learned_fallback_explore_interval: 16,
707        }
708    }
709}
710
711impl RoutingConfig {
712    fn default_learned_ttl_secs() -> u64 {
713        300
714    }
715
716    fn default_max_learned_routes_per_dest() -> usize {
717        4
718    }
719
720    fn default_learned_fallback_explore_interval() -> u64 {
721        16
722    }
723}
724
725/// Daemon routing mode.
726#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
727#[serde(rename_all = "snake_case")]
728pub enum RoutingMode {
729    /// Current FIPS behavior: bloom-assisted greedy tree routing.
730    #[default]
731    Tree,
732    /// Prefer locally learned reverse paths before falling back to tree routing.
733    ///
734    /// Learned routes are populated only from local evidence: inbound
735    /// SessionDatagrams and verified LookupResponses.
736    ReplyLearned,
737}
738
739impl std::fmt::Display for RoutingMode {
740    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
741        match self {
742            RoutingMode::Tree => write!(f, "tree"),
743            RoutingMode::ReplyLearned => write!(f, "reply_learned"),
744        }
745    }
746}
747
748/// Bloom filter (`node.bloom.*`).
749#[derive(Debug, Clone, Serialize, Deserialize)]
750pub struct BloomConfig {
751    /// Debounce interval for filter updates in ms (`node.bloom.update_debounce_ms`).
752    #[serde(default = "BloomConfig::default_update_debounce_ms")]
753    pub update_debounce_ms: u64,
754    /// Antipoison cap: reject inbound FilterAnnounce whose FPR exceeds
755    /// this value (`node.bloom.max_inbound_fpr`). Valid range `(0.0, 1.0)`.
756    /// Default `0.05` ≈ fill 0.549 at k=5 ≈ ~3,200 entries on the 1KB
757    /// filter. Conceptually distinct from future autoscaling hysteresis
758    /// setpoints — same unit, different knobs.
759    #[serde(default = "BloomConfig::default_max_inbound_fpr")]
760    pub max_inbound_fpr: f64,
761}
762
763impl Default for BloomConfig {
764    fn default() -> Self {
765        Self {
766            update_debounce_ms: 500,
767            max_inbound_fpr: 0.05,
768        }
769    }
770}
771
772impl BloomConfig {
773    fn default_update_debounce_ms() -> u64 {
774        500
775    }
776    fn default_max_inbound_fpr() -> f64 {
777        0.05
778    }
779}
780
781/// Session/data plane (`node.session.*`).
782#[derive(Debug, Clone, Serialize, Deserialize)]
783pub struct SessionConfig {
784    /// Default SessionDatagram TTL (`node.session.default_ttl`).
785    #[serde(default = "SessionConfig::default_ttl")]
786    pub default_ttl: u8,
787    /// Queue depth per dest during session establishment (`node.session.pending_packets_per_dest`).
788    #[serde(default = "SessionConfig::default_pending_packets_per_dest")]
789    pub pending_packets_per_dest: usize,
790    /// Max destinations with pending packets (`node.session.pending_max_destinations`).
791    #[serde(default = "SessionConfig::default_pending_max_destinations")]
792    pub pending_max_destinations: usize,
793    /// Idle session timeout in seconds (`node.session.idle_timeout_secs`).
794    /// Established sessions with no application data for this duration are
795    /// removed. MMP reports do not count as activity for this timer.
796    #[serde(default = "SessionConfig::default_idle_timeout_secs")]
797    pub idle_timeout_secs: u64,
798    /// Number of initial data packets per session that include COORDS_PRESENT
799    /// for transit cache warmup (`node.session.coords_warmup_packets`).
800    /// Also used as the reset count on CoordsRequired receipt.
801    #[serde(default = "SessionConfig::default_coords_warmup_packets")]
802    pub coords_warmup_packets: u8,
803    /// Minimum interval (ms) between standalone CoordsWarmup responses to
804    /// CoordsRequired/PathBroken signals, per destination
805    /// (`node.session.coords_response_interval_ms`).
806    #[serde(default = "SessionConfig::default_coords_response_interval_ms")]
807    pub coords_response_interval_ms: u64,
808}
809
810impl Default for SessionConfig {
811    fn default() -> Self {
812        Self {
813            default_ttl: 64,
814            pending_packets_per_dest: 16,
815            pending_max_destinations: 256,
816            idle_timeout_secs: 90,
817            coords_warmup_packets: 5,
818            coords_response_interval_ms: 2000,
819        }
820    }
821}
822
823impl SessionConfig {
824    fn default_ttl() -> u8 {
825        64
826    }
827    fn default_pending_packets_per_dest() -> usize {
828        16
829    }
830    fn default_pending_max_destinations() -> usize {
831        256
832    }
833    fn default_idle_timeout_secs() -> u64 {
834        90
835    }
836    fn default_coords_warmup_packets() -> u8 {
837        5
838    }
839    fn default_coords_response_interval_ms() -> u64 {
840        2000
841    }
842}
843
844/// Session-layer Metrics Measurement Protocol (`node.session_mmp.*`).
845///
846/// Separate from link-layer `node.mmp.*` to allow independent mode/interval
847/// configuration per layer. Session reports consume bandwidth on every transit
848/// link, so operators may want a lighter mode (e.g., Lightweight) for sessions
849/// while running Full mode on links.
850#[derive(Debug, Clone, Serialize, Deserialize)]
851pub struct SessionMmpConfig {
852    /// Operating mode (`node.session_mmp.mode`).
853    #[serde(default)]
854    pub mode: MmpMode,
855
856    /// Periodic operator log interval in seconds (`node.session_mmp.log_interval_secs`).
857    #[serde(default = "SessionMmpConfig::default_log_interval_secs")]
858    pub log_interval_secs: u64,
859
860    /// OWD trend ring buffer size (`node.session_mmp.owd_window_size`).
861    #[serde(default = "SessionMmpConfig::default_owd_window_size")]
862    pub owd_window_size: usize,
863}
864
865impl Default for SessionMmpConfig {
866    fn default() -> Self {
867        Self {
868            mode: MmpMode::default(),
869            log_interval_secs: DEFAULT_LOG_INTERVAL_SECS,
870            owd_window_size: DEFAULT_OWD_WINDOW_SIZE,
871        }
872    }
873}
874
875impl SessionMmpConfig {
876    fn default_log_interval_secs() -> u64 {
877        DEFAULT_LOG_INTERVAL_SECS
878    }
879    fn default_owd_window_size() -> usize {
880        DEFAULT_OWD_WINDOW_SIZE
881    }
882}
883
884/// Control socket configuration (`node.control.*`).
885#[derive(Debug, Clone, Serialize, Deserialize)]
886pub struct ControlConfig {
887    /// Enable the control socket (`node.control.enabled`).
888    #[serde(default = "ControlConfig::default_enabled")]
889    pub enabled: bool,
890    /// Unix socket path (`node.control.socket_path`).
891    #[serde(default = "ControlConfig::default_socket_path")]
892    pub socket_path: String,
893}
894
895impl Default for ControlConfig {
896    fn default() -> Self {
897        Self {
898            enabled: true,
899            socket_path: Self::default_socket_path(),
900        }
901    }
902}
903
904impl ControlConfig {
905    fn default_enabled() -> bool {
906        true
907    }
908
909    /// Default control socket path.
910    ///
911    /// On Unix, returns the shared `/run/fips`, `XDG_RUNTIME_DIR`, then `/tmp`
912    /// fallback used by fipsctl and fipstop. On Windows, returns a TCP port
913    /// number as a string since Windows does not support Unix domain sockets;
914    /// the control socket listens on localhost at this port.
915    fn default_socket_path() -> String {
916        #[cfg(unix)]
917        {
918            super::resolve_default_socket("control.sock")
919        }
920        #[cfg(windows)]
921        {
922            "21210".to_string()
923        }
924    }
925}
926
927/// Internal buffers (`node.buffers.*`).
928#[derive(Debug, Clone, Serialize, Deserialize)]
929pub struct BuffersConfig {
930    /// Transport→Node packet channel capacity (`node.buffers.packet_channel`).
931    #[serde(default = "BuffersConfig::default_packet_channel")]
932    pub packet_channel: usize,
933    /// TUN→Node outbound channel capacity (`node.buffers.tun_channel`).
934    #[serde(default = "BuffersConfig::default_tun_channel")]
935    pub tun_channel: usize,
936    /// DNS→Node identity channel capacity (`node.buffers.dns_channel`).
937    #[serde(default = "BuffersConfig::default_dns_channel")]
938    pub dns_channel: usize,
939}
940
941impl Default for BuffersConfig {
942    fn default() -> Self {
943        Self {
944            packet_channel: 1024,
945            tun_channel: 1024,
946            dns_channel: 64,
947        }
948    }
949}
950
951impl BuffersConfig {
952    fn default_packet_channel() -> usize {
953        1024
954    }
955    fn default_tun_channel() -> usize {
956        1024
957    }
958    fn default_dns_channel() -> usize {
959        64
960    }
961}
962
963// ============================================================================
964// ECN Congestion Signaling
965// ============================================================================
966
967/// Rekey / session rekeying configuration (`node.rekey.*`).
968///
969/// Controls periodic full rekey for both FMP (link layer) and FSP
970/// (session layer) Noise sessions. Rekeying provides true forward secrecy
971/// with fresh DH randomness, nonce reset, and session index rotation.
972///
973/// Keep the packet-count default high for packet-tunnel workloads. A low value
974/// such as 65k packets can force multi-hundred-Mbit tunnels to rekey every few
975/// seconds, which creates avoidable cutover churn and can dominate throughput.
976/// Operators can still lower `node.rekey.after_messages` for CI stress tests or
977/// very conservative deployments; the time-based `after_secs` default remains
978/// the normal production rekey cadence.
979const DEFAULT_REKEY_AFTER_MESSAGES: u64 = 1 << 48;
980
981#[derive(Debug, Clone, Serialize, Deserialize)]
982pub struct RekeyConfig {
983    /// Enable periodic rekey (`node.rekey.enabled`).
984    #[serde(default = "RekeyConfig::default_enabled")]
985    pub enabled: bool,
986
987    /// Initiate rekey after this many seconds (`node.rekey.after_secs`).
988    #[serde(default = "RekeyConfig::default_after_secs")]
989    pub after_secs: u64,
990
991    /// Initiate rekey after this many messages sent (`node.rekey.after_messages`).
992    #[serde(default = "RekeyConfig::default_after_messages")]
993    pub after_messages: u64,
994}
995
996impl Default for RekeyConfig {
997    fn default() -> Self {
998        Self {
999            enabled: true,
1000            after_secs: 120,
1001            after_messages: DEFAULT_REKEY_AFTER_MESSAGES,
1002        }
1003    }
1004}
1005
1006impl RekeyConfig {
1007    fn default_enabled() -> bool {
1008        true
1009    }
1010    fn default_after_secs() -> u64 {
1011        120
1012    }
1013    fn default_after_messages() -> u64 {
1014        DEFAULT_REKEY_AFTER_MESSAGES
1015    }
1016}
1017
1018/// ECN congestion signaling configuration (`node.ecn.*`).
1019///
1020/// Controls the FMP CE relay chain: transit nodes detect congestion on outgoing
1021/// links and set the CE flag in forwarded datagrams. The destination marks
1022/// IPv6 ECN-CE on ECN-capable packets before TUN delivery.
1023#[derive(Debug, Clone, Serialize, Deserialize)]
1024pub struct EcnConfig {
1025    /// Enable ECN congestion signaling (`node.ecn.enabled`).
1026    #[serde(default = "EcnConfig::default_enabled")]
1027    pub enabled: bool,
1028
1029    /// Loss rate threshold for marking CE (`node.ecn.loss_threshold`).
1030    /// When the outgoing link's loss rate meets or exceeds this value,
1031    /// the transit node sets CE on forwarded datagrams.
1032    #[serde(default = "EcnConfig::default_loss_threshold")]
1033    pub loss_threshold: f64,
1034
1035    /// ETX threshold for marking CE (`node.ecn.etx_threshold`).
1036    /// When the outgoing link's ETX meets or exceeds this value,
1037    /// the transit node sets CE on forwarded datagrams.
1038    #[serde(default = "EcnConfig::default_etx_threshold")]
1039    pub etx_threshold: f64,
1040}
1041
1042impl Default for EcnConfig {
1043    fn default() -> Self {
1044        Self {
1045            enabled: true,
1046            loss_threshold: 0.05,
1047            etx_threshold: 3.0,
1048        }
1049    }
1050}
1051
1052impl EcnConfig {
1053    fn default_enabled() -> bool {
1054        true
1055    }
1056    fn default_loss_threshold() -> f64 {
1057        0.05
1058    }
1059    fn default_etx_threshold() -> f64 {
1060        3.0
1061    }
1062}
1063
1064// ============================================================================
1065// Node Configuration (Root)
1066// ============================================================================
1067
1068/// Node configuration (`node.*`).
1069#[derive(Debug, Clone, Serialize, Deserialize)]
1070pub struct NodeConfig {
1071    /// Identity configuration (`node.identity.*`).
1072    #[serde(default)]
1073    pub identity: IdentityConfig,
1074
1075    /// Leaf-only mode (`node.leaf_only`).
1076    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1077    pub leaf_only: bool,
1078
1079    /// RX loop maintenance tick period in seconds (`node.tick_interval_secs`).
1080    #[serde(default = "NodeConfig::default_tick_interval_secs")]
1081    pub tick_interval_secs: u64,
1082
1083    /// Initial RTT estimate for new links in ms (`node.base_rtt_ms`).
1084    #[serde(default = "NodeConfig::default_base_rtt_ms")]
1085    pub base_rtt_ms: u64,
1086
1087    /// Link heartbeat send interval in seconds (`node.heartbeat_interval_secs`).
1088    #[serde(default = "NodeConfig::default_heartbeat_interval_secs")]
1089    pub heartbeat_interval_secs: u64,
1090
1091    /// Link dead timeout in seconds (`node.link_dead_timeout_secs`).
1092    /// Peers silent for this duration are removed.
1093    #[serde(default = "NodeConfig::default_link_dead_timeout_secs")]
1094    pub link_dead_timeout_secs: u64,
1095
1096    /// Accelerated link dead timeout in seconds, used in place of
1097    /// `link_dead_timeout_secs` while a recent `transport.send` returned
1098    /// a local-side errno (`NetworkUnreachable` / `HostUnreachable` /
1099    /// `AddrNotAvailable`) — direct evidence our outbound path is broken
1100    /// right now (interface vanished, default route flapped, etc.). No
1101    /// reason to wait the full receive-silence window when the kernel
1102    /// already told us we can't send. Steady-state behavior is unchanged
1103    /// because the signal is cleared on the next successful send.
1104    /// (`node.fast_link_dead_timeout_secs`)
1105    #[serde(default = "NodeConfig::default_fast_link_dead_timeout_secs")]
1106    pub fast_link_dead_timeout_secs: u64,
1107
1108    /// Resource limits (`node.limits.*`).
1109    #[serde(default)]
1110    pub limits: LimitsConfig,
1111
1112    /// Connected UDP fast path (`node.connected_udp.*`).
1113    #[serde(default)]
1114    pub connected_udp: ConnectedUdpConfig,
1115
1116    /// Rate limiting (`node.rate_limit.*`).
1117    #[serde(default)]
1118    pub rate_limit: RateLimitConfig,
1119
1120    /// Retry/backoff (`node.retry.*`).
1121    #[serde(default)]
1122    pub retry: RetryConfig,
1123
1124    /// Cache parameters (`node.cache.*`).
1125    #[serde(default)]
1126    pub cache: CacheConfig,
1127
1128    /// Discovery protocol (`node.discovery.*`).
1129    #[serde(default)]
1130    pub discovery: DiscoveryConfig,
1131
1132    /// Spanning tree (`node.tree.*`).
1133    #[serde(default)]
1134    pub tree: TreeConfig,
1135
1136    /// Routing strategy (`node.routing.*`).
1137    #[serde(default)]
1138    pub routing: RoutingConfig,
1139
1140    /// Bloom filter (`node.bloom.*`).
1141    #[serde(default)]
1142    pub bloom: BloomConfig,
1143
1144    /// Session/data plane (`node.session.*`).
1145    #[serde(default)]
1146    pub session: SessionConfig,
1147
1148    /// Internal buffers (`node.buffers.*`).
1149    #[serde(default)]
1150    pub buffers: BuffersConfig,
1151
1152    /// Control socket (`node.control.*`).
1153    #[serde(default)]
1154    pub control: ControlConfig,
1155
1156    /// Metrics Measurement Protocol — link layer (`node.mmp.*`).
1157    #[serde(default)]
1158    pub mmp: MmpConfig,
1159
1160    /// Metrics Measurement Protocol — session layer (`node.session_mmp.*`).
1161    #[serde(default)]
1162    pub session_mmp: SessionMmpConfig,
1163
1164    /// ECN congestion signaling (`node.ecn.*`).
1165    #[serde(default)]
1166    pub ecn: EcnConfig,
1167
1168    /// Rekey / session rekeying (`node.rekey.*`).
1169    #[serde(default)]
1170    pub rekey: RekeyConfig,
1171
1172    /// Enable daemon-oriented system files such as `/etc/fips/hosts` and
1173    /// `/etc/fips/peers.{allow,deny}`. Embedded endpoints disable this.
1174    #[serde(default = "NodeConfig::default_system_files_enabled")]
1175    pub system_files_enabled: bool,
1176
1177    /// Enable off-task Unix encrypt/decrypt worker pools (`node.worker_pools_enabled`).
1178    /// Embedded/mobile endpoints can disable this to keep crypto/send work inline
1179    /// with the rx loop when platform extension sandboxes make OS-thread pools
1180    /// unsuitable.
1181    #[serde(default = "NodeConfig::default_worker_pools_enabled")]
1182    pub worker_pools_enabled: bool,
1183
1184    /// Log level (`node.log_level`). Case-insensitive.
1185    /// Valid values: trace, debug, info, warn, error. Default: info.
1186    #[serde(default)]
1187    pub log_level: Option<String>,
1188}
1189
1190impl Default for NodeConfig {
1191    fn default() -> Self {
1192        Self {
1193            identity: IdentityConfig::default(),
1194            leaf_only: false,
1195            tick_interval_secs: 1,
1196            base_rtt_ms: 100,
1197            heartbeat_interval_secs: 10,
1198            link_dead_timeout_secs: 30,
1199            fast_link_dead_timeout_secs: 5,
1200            limits: LimitsConfig::default(),
1201            connected_udp: ConnectedUdpConfig::default(),
1202            rate_limit: RateLimitConfig::default(),
1203            retry: RetryConfig::default(),
1204            cache: CacheConfig::default(),
1205            discovery: DiscoveryConfig::default(),
1206            tree: TreeConfig::default(),
1207            routing: RoutingConfig::default(),
1208            bloom: BloomConfig::default(),
1209            session: SessionConfig::default(),
1210            buffers: BuffersConfig::default(),
1211            control: ControlConfig::default(),
1212            mmp: MmpConfig::default(),
1213            session_mmp: SessionMmpConfig::default(),
1214            ecn: EcnConfig::default(),
1215            rekey: RekeyConfig::default(),
1216            system_files_enabled: true,
1217            worker_pools_enabled: true,
1218            log_level: None,
1219        }
1220    }
1221}
1222
1223impl NodeConfig {
1224    /// Get the log level as a tracing Level. Default: INFO.
1225    pub fn log_level(&self) -> tracing::Level {
1226        match self
1227            .log_level
1228            .as_deref()
1229            .map(|s| s.to_lowercase())
1230            .as_deref()
1231        {
1232            Some("trace") => tracing::Level::TRACE,
1233            Some("debug") => tracing::Level::DEBUG,
1234            Some("warn") | Some("warning") => tracing::Level::WARN,
1235            Some("error") => tracing::Level::ERROR,
1236            _ => tracing::Level::INFO,
1237        }
1238    }
1239
1240    fn default_tick_interval_secs() -> u64 {
1241        1
1242    }
1243    fn default_base_rtt_ms() -> u64 {
1244        100
1245    }
1246    fn default_heartbeat_interval_secs() -> u64 {
1247        10
1248    }
1249    fn default_link_dead_timeout_secs() -> u64 {
1250        30
1251    }
1252    fn default_fast_link_dead_timeout_secs() -> u64 {
1253        5
1254    }
1255    fn default_system_files_enabled() -> bool {
1256        true
1257    }
1258    fn default_worker_pools_enabled() -> bool {
1259        true
1260    }
1261}
1262
1263#[cfg(test)]
1264mod tests {
1265    use super::*;
1266
1267    #[test]
1268    fn test_ecn_config_defaults() {
1269        let c = EcnConfig::default();
1270        assert!(c.enabled);
1271        assert!((c.loss_threshold - 0.05).abs() < 1e-9);
1272        assert!((c.etx_threshold - 3.0).abs() < 1e-9);
1273    }
1274
1275    #[test]
1276    fn test_rekey_config_defaults() {
1277        let c = RekeyConfig::default();
1278        assert!(c.enabled);
1279        assert_eq!(c.after_secs, 120);
1280        assert_eq!(c.after_messages, 1 << 48);
1281    }
1282
1283    #[test]
1284    fn test_rekey_config_partial_yaml_uses_defaults() {
1285        let yaml = "after_secs: 30\n";
1286        let c: RekeyConfig = serde_yaml::from_str(yaml).unwrap();
1287        assert!(c.enabled);
1288        assert_eq!(c.after_secs, 30);
1289        assert_eq!(c.after_messages, 1 << 48);
1290    }
1291
1292    #[test]
1293    fn test_connected_udp_config_defaults() {
1294        let c = ConnectedUdpConfig::default();
1295        assert!(c.enabled);
1296        assert_eq!(c.fd_reserve, 128);
1297    }
1298
1299    #[test]
1300    fn test_connected_udp_config_yaml() {
1301        let yaml = "enabled: false\nfd_reserve: 4096\n";
1302        let c: ConnectedUdpConfig = serde_yaml::from_str(yaml).unwrap();
1303        assert!(!c.enabled);
1304        assert_eq!(c.fd_reserve, 4096);
1305    }
1306
1307    #[test]
1308    fn test_routing_config_defaults() {
1309        let c = RoutingConfig::default();
1310        assert_eq!(c.mode, RoutingMode::Tree);
1311        assert_eq!(c.learned_ttl_secs, 300);
1312        assert_eq!(c.max_learned_routes_per_dest, 4);
1313        assert_eq!(c.learned_fallback_explore_interval, 16);
1314    }
1315
1316    #[test]
1317    fn test_routing_config_yaml() {
1318        let yaml = "mode: reply_learned\nlearned_ttl_secs: 120\nmax_learned_routes_per_dest: 2\nlearned_fallback_explore_interval: 8\n";
1319        let c: RoutingConfig = serde_yaml::from_str(yaml).unwrap();
1320        assert_eq!(c.mode, RoutingMode::ReplyLearned);
1321        assert_eq!(c.learned_ttl_secs, 120);
1322        assert_eq!(c.max_learned_routes_per_dest, 2);
1323        assert_eq!(c.learned_fallback_explore_interval, 8);
1324    }
1325
1326    #[test]
1327    fn test_ecn_config_yaml_roundtrip() {
1328        let yaml = "loss_threshold: 0.10\netx_threshold: 2.5\nenabled: false\n";
1329        let c: EcnConfig = serde_yaml::from_str(yaml).unwrap();
1330        assert!(!c.enabled);
1331        assert!((c.loss_threshold - 0.10).abs() < 1e-9);
1332        assert!((c.etx_threshold - 2.5).abs() < 1e-9);
1333    }
1334
1335    #[test]
1336    fn test_ecn_config_partial_yaml() {
1337        // Only specify loss_threshold — others should get defaults
1338        let yaml = "loss_threshold: 0.02\n";
1339        let c: EcnConfig = serde_yaml::from_str(yaml).unwrap();
1340        assert!(c.enabled); // default
1341        assert!((c.loss_threshold - 0.02).abs() < 1e-9);
1342        assert!((c.etx_threshold - 3.0).abs() < 1e-9); // default
1343    }
1344
1345    #[test]
1346    fn test_nostr_discovery_startup_sweep_defaults() {
1347        let c = NostrDiscoveryConfig::default();
1348        assert_eq!(c.startup_sweep_delay_secs, 5);
1349        assert_eq!(c.startup_sweep_max_age_secs, 3_600);
1350    }
1351
1352    #[test]
1353    fn test_nostr_discovery_startup_sweep_yaml_override() {
1354        let yaml = "enabled: true\npolicy: open\nstartup_sweep_delay_secs: 10\nstartup_sweep_max_age_secs: 1800\n";
1355        let c: NostrDiscoveryConfig = serde_yaml::from_str(yaml).unwrap();
1356        assert!(c.enabled);
1357        assert_eq!(c.policy, NostrDiscoveryPolicy::Open);
1358        assert_eq!(c.startup_sweep_delay_secs, 10);
1359        assert_eq!(c.startup_sweep_max_age_secs, 1_800);
1360    }
1361
1362    #[test]
1363    fn test_nostr_discovery_startup_sweep_partial_yaml_uses_defaults() {
1364        // Only override delay; max_age should fall back to default.
1365        let yaml = "enabled: true\nstartup_sweep_delay_secs: 30\n";
1366        let c: NostrDiscoveryConfig = serde_yaml::from_str(yaml).unwrap();
1367        assert_eq!(c.startup_sweep_delay_secs, 30);
1368        assert_eq!(c.startup_sweep_max_age_secs, 3_600);
1369    }
1370
1371    #[test]
1372    fn test_log_level_parser() {
1373        // Pin the observed behavior of NodeConfig::log_level():
1374        // - 5 explicit lowercased match arms (trace/debug/warn|warning/error)
1375        // - INFO is the default (no explicit "info" arm; falls through default)
1376        // - Case-insensitive via .to_lowercase()
1377        // - Unknown strings and None both fall through to INFO
1378        let cases: &[(Option<&str>, tracing::Level)] = &[
1379            // Explicit arms (lowercase canonical form)
1380            (Some("trace"), tracing::Level::TRACE),
1381            (Some("debug"), tracing::Level::DEBUG),
1382            (Some("warn"), tracing::Level::WARN),
1383            (Some("warning"), tracing::Level::WARN),
1384            (Some("error"), tracing::Level::ERROR),
1385            // "info" has no explicit arm — falls through default
1386            (Some("info"), tracing::Level::INFO),
1387            // None → default INFO
1388            (None, tracing::Level::INFO),
1389            // Case-insensitivity (parser lowercases via .to_lowercase())
1390            (Some("TRACE"), tracing::Level::TRACE),
1391            (Some("Debug"), tracing::Level::DEBUG),
1392            (Some("Warning"), tracing::Level::WARN),
1393            (Some("WARN"), tracing::Level::WARN),
1394            (Some("ERROR"), tracing::Level::ERROR),
1395            (Some("INFO"), tracing::Level::INFO),
1396            // Unknown strings → INFO default (no error path)
1397            (Some("verbose"), tracing::Level::INFO),
1398            (Some("nonsense"), tracing::Level::INFO),
1399            (Some(""), tracing::Level::INFO),
1400        ];
1401
1402        for (input, expected) in cases {
1403            let cfg = NodeConfig {
1404                log_level: input.map(|s| s.to_string()),
1405                ..NodeConfig::default()
1406            };
1407            assert_eq!(
1408                cfg.log_level(),
1409                *expected,
1410                "input {:?} should map to {:?}",
1411                input,
1412                expected
1413            );
1414        }
1415    }
1416
1417    #[cfg(windows)]
1418    #[test]
1419    fn test_default_socket_path_windows() {
1420        let config = ControlConfig::default();
1421        // On Windows, socket_path is a TCP port number
1422        let port: u16 = config
1423            .socket_path
1424            .parse()
1425            .expect("should be a valid port number");
1426        assert_eq!(port, 21210);
1427    }
1428}