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