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.
65    /// Connected UDP stays default-on for Linux and default-off for macOS,
66    /// where live mobile/NAT testing has shown the `SO_REUSEPORT` listener
67    /// group can destabilize the wildcard receive path. The old macOS-specific
68    /// `FIPS_MACOS_CONNECTED_UDP=1` is still accepted as an enable-only legacy
69    /// override.
70    #[serde(default = "ConnectedUdpConfig::default_enabled")]
71    pub enabled: bool,
72
73    /// Maximum peers that may have connected UDP sockets installed
74    /// (`node.connected_udp.max_peers`).
75    ///
76    /// This is an explicit escape hatch for large meshes while connected UDP
77    /// still uses one receive-drain thread per installed peer. Set to `0` to
78    /// disable the explicit cap and rely only on the fd budget and
79    /// `node.limits.max_peers`.
80    #[serde(default = "ConnectedUdpConfig::default_max_peers")]
81    pub max_peers: usize,
82
83    /// Number of process file descriptors to leave for non-connected-UDP use
84    /// (`node.connected_udp.fd_reserve`).
85    ///
86    /// This is headroom, not a peer cap. Connected UDP uses three FDs per
87    /// installed peer, so the effective fast-path peer budget is roughly
88    /// `(RLIMIT_NOFILE - fd_reserve) / 3`, also bounded by `max_peers` when
89    /// non-zero and by `node.limits.max_peers`.
90    #[serde(default = "ConnectedUdpConfig::default_fd_reserve")]
91    pub fd_reserve: usize,
92}
93
94impl Default for ConnectedUdpConfig {
95    fn default() -> Self {
96        Self {
97            enabled: Self::default_enabled(),
98            max_peers: Self::default_max_peers(),
99            fd_reserve: Self::default_fd_reserve(),
100        }
101    }
102}
103
104impl ConnectedUdpConfig {
105    fn default_enabled() -> bool {
106        #[cfg(target_os = "macos")]
107        {
108            return false;
109        }
110
111        #[cfg(not(target_os = "macos"))]
112        true
113    }
114
115    fn default_max_peers() -> usize {
116        0
117    }
118
119    fn default_fd_reserve() -> usize {
120        128
121    }
122}
123
124/// Rate limiting (`node.rate_limit.*`).
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct RateLimitConfig {
127    /// Token bucket burst capacity (`node.rate_limit.handshake_burst`).
128    #[serde(default = "RateLimitConfig::default_handshake_burst")]
129    pub handshake_burst: u32,
130    /// Tokens/sec refill rate (`node.rate_limit.handshake_rate`).
131    #[serde(default = "RateLimitConfig::default_handshake_rate")]
132    pub handshake_rate: f64,
133    /// Stale handshake cleanup timeout in seconds (`node.rate_limit.handshake_timeout_secs`).
134    #[serde(default = "RateLimitConfig::default_handshake_timeout_secs")]
135    pub handshake_timeout_secs: u64,
136    /// Initial handshake resend interval in ms (`node.rate_limit.handshake_resend_interval_ms`).
137    /// Handshake messages are resent with exponential backoff within the timeout window.
138    #[serde(default = "RateLimitConfig::default_handshake_resend_interval_ms")]
139    pub handshake_resend_interval_ms: u64,
140    /// Handshake resend backoff multiplier (`node.rate_limit.handshake_resend_backoff`).
141    #[serde(default = "RateLimitConfig::default_handshake_resend_backoff")]
142    pub handshake_resend_backoff: f64,
143    /// Max handshake resends per attempt (`node.rate_limit.handshake_max_resends`).
144    #[serde(default = "RateLimitConfig::default_handshake_max_resends")]
145    pub handshake_max_resends: u32,
146}
147
148impl Default for RateLimitConfig {
149    fn default() -> Self {
150        Self {
151            handshake_burst: 100,
152            handshake_rate: 10.0,
153            handshake_timeout_secs: 30,
154            handshake_resend_interval_ms: 1000,
155            handshake_resend_backoff: 2.0,
156            handshake_max_resends: 5,
157        }
158    }
159}
160
161impl RateLimitConfig {
162    fn default_handshake_burst() -> u32 {
163        100
164    }
165    fn default_handshake_rate() -> f64 {
166        10.0
167    }
168    fn default_handshake_timeout_secs() -> u64 {
169        30
170    }
171    fn default_handshake_resend_interval_ms() -> u64 {
172        1000
173    }
174    fn default_handshake_resend_backoff() -> f64 {
175        2.0
176    }
177    fn default_handshake_max_resends() -> u32 {
178        5
179    }
180}
181
182/// Retry/backoff configuration (`node.retry.*`).
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct RetryConfig {
185    /// Max connection retry attempts (`node.retry.max_retries`).
186    #[serde(default = "RetryConfig::default_max_retries")]
187    pub max_retries: u32,
188    /// Base backoff interval in seconds (`node.retry.base_interval_secs`).
189    #[serde(default = "RetryConfig::default_base_interval_secs")]
190    pub base_interval_secs: u64,
191    /// Cap on exponential backoff in seconds (`node.retry.max_backoff_secs`).
192    #[serde(default = "RetryConfig::default_max_backoff_secs")]
193    pub max_backoff_secs: u64,
194}
195
196impl Default for RetryConfig {
197    fn default() -> Self {
198        Self {
199            max_retries: 5,
200            base_interval_secs: 5,
201            max_backoff_secs: 300,
202        }
203    }
204}
205
206impl RetryConfig {
207    fn default_max_retries() -> u32 {
208        5
209    }
210    fn default_base_interval_secs() -> u64 {
211        5
212    }
213    fn default_max_backoff_secs() -> u64 {
214        300
215    }
216}
217
218/// Cache parameters (`node.cache.*`).
219#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct CacheConfig {
221    /// Max entries in coord cache (`node.cache.coord_size`).
222    #[serde(default = "CacheConfig::default_coord_size")]
223    pub coord_size: usize,
224    /// Coord cache entry TTL in seconds (`node.cache.coord_ttl_secs`).
225    #[serde(default = "CacheConfig::default_coord_ttl_secs")]
226    pub coord_ttl_secs: u64,
227    /// Max entries in identity cache (`node.cache.identity_size`).
228    #[serde(default = "CacheConfig::default_identity_size")]
229    pub identity_size: usize,
230}
231
232impl Default for CacheConfig {
233    fn default() -> Self {
234        Self {
235            coord_size: 50_000,
236            coord_ttl_secs: 300,
237            identity_size: 10_000,
238        }
239    }
240}
241
242impl CacheConfig {
243    fn default_coord_size() -> usize {
244        50_000
245    }
246    fn default_coord_ttl_secs() -> u64 {
247        300
248    }
249    fn default_identity_size() -> usize {
250        10_000
251    }
252}
253
254mod discovery;
255
256pub use discovery::{DiscoveryConfig, NostrDiscoveryConfig, NostrDiscoveryPolicy};
257
258/// Spanning tree (`node.tree.*`).
259#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct TreeConfig {
261    /// Per-peer TreeAnnounce rate limit in ms (`node.tree.announce_min_interval_ms`).
262    #[serde(default = "TreeConfig::default_announce_min_interval_ms")]
263    pub announce_min_interval_ms: u64,
264    /// Hysteresis factor for cost-based parent re-selection (`node.tree.parent_hysteresis`).
265    ///
266    /// Only switch parents when the candidate's effective_depth is better than
267    /// `current_effective_depth * (1.0 - parent_hysteresis)`. Range: 0.0-1.0.
268    /// Set to 0.0 to disable hysteresis (switch on any improvement).
269    #[serde(default = "TreeConfig::default_parent_hysteresis")]
270    pub parent_hysteresis: f64,
271    /// Hold-down period after parent switch in seconds (`node.tree.hold_down_secs`).
272    ///
273    /// After switching parents, suppress re-evaluation for this duration to allow
274    /// MMP metrics to stabilize on the new link. Set to 0 to disable.
275    #[serde(default = "TreeConfig::default_hold_down_secs")]
276    pub hold_down_secs: u64,
277    /// Periodic parent re-evaluation interval in seconds (`node.tree.reeval_interval_secs`).
278    ///
279    /// How often to re-evaluate parent selection based on current MMP link costs,
280    /// independent of TreeAnnounce traffic. Catches link degradation after the
281    /// tree has stabilized. Set to 0 to disable.
282    #[serde(default = "TreeConfig::default_reeval_interval_secs")]
283    pub reeval_interval_secs: u64,
284    /// Flap dampening: max parent switches before extended hold-down (`node.tree.flap_threshold`).
285    #[serde(default = "TreeConfig::default_flap_threshold")]
286    pub flap_threshold: u32,
287    /// Flap dampening: window in seconds for counting switches (`node.tree.flap_window_secs`).
288    #[serde(default = "TreeConfig::default_flap_window_secs")]
289    pub flap_window_secs: u64,
290    /// Flap dampening: extended hold-down duration in seconds (`node.tree.flap_dampening_secs`).
291    #[serde(default = "TreeConfig::default_flap_dampening_secs")]
292    pub flap_dampening_secs: u64,
293}
294
295impl Default for TreeConfig {
296    fn default() -> Self {
297        Self {
298            announce_min_interval_ms: 500,
299            parent_hysteresis: 0.2,
300            hold_down_secs: 30,
301            reeval_interval_secs: 60,
302            flap_threshold: 4,
303            flap_window_secs: 60,
304            flap_dampening_secs: 120,
305        }
306    }
307}
308
309impl TreeConfig {
310    fn default_announce_min_interval_ms() -> u64 {
311        500
312    }
313    fn default_parent_hysteresis() -> f64 {
314        0.2
315    }
316    fn default_hold_down_secs() -> u64 {
317        30
318    }
319    fn default_reeval_interval_secs() -> u64 {
320        60
321    }
322    fn default_flap_threshold() -> u32 {
323        4
324    }
325    fn default_flap_window_secs() -> u64 {
326        60
327    }
328    fn default_flap_dampening_secs() -> u64 {
329        120
330    }
331}
332
333/// Routing strategy selection (`node.routing.*`).
334#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct RoutingConfig {
336    /// Next-hop selection mode (`node.routing.mode`).
337    #[serde(default)]
338    pub mode: RoutingMode,
339    /// TTL for learned reverse-path routes in seconds (`node.routing.learned_ttl_secs`).
340    #[serde(default = "RoutingConfig::default_learned_ttl_secs")]
341    pub learned_ttl_secs: u64,
342    /// Maximum locally observed next-hop candidates kept per destination for
343    /// reply-learned multipath/exploration
344    /// (`node.routing.max_learned_routes_per_dest`).
345    #[serde(default = "RoutingConfig::default_max_learned_routes_per_dest")]
346    pub max_learned_routes_per_dest: usize,
347    /// Every N learned-route selections, try the coordinate/bloom/tree route
348    /// instead so new paths can be discovered (`0` disables fallback exploration).
349    #[serde(default = "RoutingConfig::default_learned_fallback_explore_interval")]
350    pub learned_fallback_explore_interval: u64,
351}
352
353impl Default for RoutingConfig {
354    fn default() -> Self {
355        Self {
356            mode: RoutingMode::default(),
357            learned_ttl_secs: 300,
358            max_learned_routes_per_dest: 4,
359            learned_fallback_explore_interval: 16,
360        }
361    }
362}
363
364impl RoutingConfig {
365    fn default_learned_ttl_secs() -> u64 {
366        300
367    }
368
369    fn default_max_learned_routes_per_dest() -> usize {
370        4
371    }
372
373    fn default_learned_fallback_explore_interval() -> u64 {
374        16
375    }
376}
377
378/// Daemon routing mode.
379#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
380#[serde(rename_all = "snake_case")]
381pub enum RoutingMode {
382    /// Current FIPS behavior: bloom-assisted greedy tree routing.
383    #[default]
384    Tree,
385    /// Prefer locally learned reverse paths before falling back to tree routing.
386    ///
387    /// Learned routes are populated only from local evidence: inbound
388    /// SessionDatagrams and verified LookupResponses.
389    ReplyLearned,
390}
391
392impl std::fmt::Display for RoutingMode {
393    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
394        match self {
395            RoutingMode::Tree => write!(f, "tree"),
396            RoutingMode::ReplyLearned => write!(f, "reply_learned"),
397        }
398    }
399}
400
401/// Bloom filter (`node.bloom.*`).
402#[derive(Debug, Clone, Serialize, Deserialize)]
403pub struct BloomConfig {
404    /// Debounce interval for filter updates in ms (`node.bloom.update_debounce_ms`).
405    #[serde(default = "BloomConfig::default_update_debounce_ms")]
406    pub update_debounce_ms: u64,
407    /// Antipoison cap: reject inbound FilterAnnounce whose FPR exceeds
408    /// this value (`node.bloom.max_inbound_fpr`). Valid range `(0.0, 1.0)`.
409    /// Default `0.10` ≈ fill 0.631 at k=5 ≈ ~1,630 entries on the 1 KB
410    /// filter. This leaves headroom for legitimate aggregates near the
411    /// fixed-filter operating ceiling while still rejecting saturated or
412    /// poisoned filters. Conceptually distinct from future autoscaling
413    /// hysteresis setpoints — same unit, different knobs.
414    #[serde(default = "BloomConfig::default_max_inbound_fpr")]
415    pub max_inbound_fpr: f64,
416}
417
418impl Default for BloomConfig {
419    fn default() -> Self {
420        Self {
421            update_debounce_ms: 500,
422            max_inbound_fpr: 0.10,
423        }
424    }
425}
426
427impl BloomConfig {
428    fn default_update_debounce_ms() -> u64 {
429        500
430    }
431    fn default_max_inbound_fpr() -> f64 {
432        0.10
433    }
434}
435
436/// Session/data plane (`node.session.*`).
437#[derive(Debug, Clone, Serialize, Deserialize)]
438pub struct SessionConfig {
439    /// Default SessionDatagram TTL (`node.session.default_ttl`).
440    #[serde(default = "SessionConfig::default_ttl")]
441    pub default_ttl: u8,
442    /// Queue depth per dest during session establishment (`node.session.pending_packets_per_dest`).
443    #[serde(default = "SessionConfig::default_pending_packets_per_dest")]
444    pub pending_packets_per_dest: usize,
445    /// Max destinations with pending packets (`node.session.pending_max_destinations`).
446    #[serde(default = "SessionConfig::default_pending_max_destinations")]
447    pub pending_max_destinations: usize,
448    /// Idle session timeout in seconds (`node.session.idle_timeout_secs`).
449    /// Established sessions with no application data for this duration are
450    /// removed. MMP reports do not count as activity for this timer.
451    #[serde(default = "SessionConfig::default_idle_timeout_secs")]
452    pub idle_timeout_secs: u64,
453    /// Number of initial data packets per session that include COORDS_PRESENT
454    /// for transit cache warmup (`node.session.coords_warmup_packets`).
455    /// Also used as the reset count on CoordsRequired receipt.
456    #[serde(default = "SessionConfig::default_coords_warmup_packets")]
457    pub coords_warmup_packets: u8,
458    /// Minimum interval (ms) between standalone CoordsWarmup responses to
459    /// CoordsRequired/PathBroken signals, per destination
460    /// (`node.session.coords_response_interval_ms`).
461    #[serde(default = "SessionConfig::default_coords_response_interval_ms")]
462    pub coords_response_interval_ms: u64,
463}
464
465impl Default for SessionConfig {
466    fn default() -> Self {
467        Self {
468            default_ttl: 64,
469            pending_packets_per_dest: 16,
470            pending_max_destinations: 256,
471            idle_timeout_secs: 90,
472            coords_warmup_packets: 5,
473            coords_response_interval_ms: 2000,
474        }
475    }
476}
477
478impl SessionConfig {
479    fn default_ttl() -> u8 {
480        64
481    }
482    fn default_pending_packets_per_dest() -> usize {
483        16
484    }
485    fn default_pending_max_destinations() -> usize {
486        256
487    }
488    fn default_idle_timeout_secs() -> u64 {
489        90
490    }
491    fn default_coords_warmup_packets() -> u8 {
492        5
493    }
494    fn default_coords_response_interval_ms() -> u64 {
495        2000
496    }
497}
498
499/// Session-layer Metrics Measurement Protocol (`node.session_mmp.*`).
500///
501/// Separate from link-layer `node.mmp.*` to allow independent mode/interval
502/// configuration per layer. Session reports consume bandwidth on every transit
503/// link, so operators may want a lighter mode (e.g., Lightweight) for sessions
504/// while running Full mode on links.
505#[derive(Debug, Clone, Serialize, Deserialize)]
506pub struct SessionMmpConfig {
507    /// Operating mode (`node.session_mmp.mode`).
508    #[serde(default)]
509    pub mode: MmpMode,
510
511    /// Periodic operator log interval in seconds (`node.session_mmp.log_interval_secs`).
512    #[serde(default = "SessionMmpConfig::default_log_interval_secs")]
513    pub log_interval_secs: u64,
514
515    /// OWD trend ring buffer size (`node.session_mmp.owd_window_size`).
516    #[serde(default = "SessionMmpConfig::default_owd_window_size")]
517    pub owd_window_size: usize,
518}
519
520impl Default for SessionMmpConfig {
521    fn default() -> Self {
522        Self {
523            mode: MmpMode::default(),
524            log_interval_secs: DEFAULT_LOG_INTERVAL_SECS,
525            owd_window_size: DEFAULT_OWD_WINDOW_SIZE,
526        }
527    }
528}
529
530impl SessionMmpConfig {
531    fn default_log_interval_secs() -> u64 {
532        DEFAULT_LOG_INTERVAL_SECS
533    }
534    fn default_owd_window_size() -> usize {
535        DEFAULT_OWD_WINDOW_SIZE
536    }
537}
538
539/// Control socket configuration (`node.control.*`).
540#[derive(Debug, Clone, Serialize, Deserialize)]
541pub struct ControlConfig {
542    /// Enable the control socket (`node.control.enabled`).
543    #[serde(default = "ControlConfig::default_enabled")]
544    pub enabled: bool,
545    /// Unix socket path (`node.control.socket_path`).
546    #[serde(default = "ControlConfig::default_socket_path")]
547    pub socket_path: String,
548}
549
550impl Default for ControlConfig {
551    fn default() -> Self {
552        Self {
553            enabled: true,
554            socket_path: Self::default_socket_path(),
555        }
556    }
557}
558
559impl ControlConfig {
560    fn default_enabled() -> bool {
561        true
562    }
563
564    /// Default control socket path.
565    ///
566    /// On Unix, returns the shared `/run/fips`, `XDG_RUNTIME_DIR`, then `/tmp`
567    /// fallback used by fipsctl and fipstop. On Windows, returns a TCP port
568    /// number as a string since Windows does not support Unix domain sockets;
569    /// the control socket listens on localhost at this port.
570    fn default_socket_path() -> String {
571        #[cfg(unix)]
572        {
573            super::resolve_default_socket("control.sock")
574        }
575        #[cfg(windows)]
576        {
577            "21210".to_string()
578        }
579    }
580}
581
582const DEFAULT_PACKET_CHANNEL_CAPACITY: usize = 4096;
583
584/// Internal buffers (`node.buffers.*`).
585#[derive(Debug, Clone, Serialize, Deserialize)]
586pub struct BuffersConfig {
587    /// Transport→Node bulk packet capacity (`node.buffers.packet_channel`).
588    ///
589    /// Priority/control packets use a reserved lane. This bounds bulk packet
590    /// backlog in packet units rather than receive-batch channel items.
591    #[serde(default = "BuffersConfig::default_packet_channel")]
592    pub packet_channel: usize,
593    /// TUN→Node outbound channel capacity (`node.buffers.tun_channel`).
594    #[serde(default = "BuffersConfig::default_tun_channel")]
595    pub tun_channel: usize,
596    /// DNS→Node identity channel capacity (`node.buffers.dns_channel`).
597    #[serde(default = "BuffersConfig::default_dns_channel")]
598    pub dns_channel: usize,
599}
600
601impl Default for BuffersConfig {
602    fn default() -> Self {
603        Self {
604            packet_channel: DEFAULT_PACKET_CHANNEL_CAPACITY,
605            tun_channel: 1024,
606            dns_channel: 64,
607        }
608    }
609}
610
611impl BuffersConfig {
612    fn default_packet_channel() -> usize {
613        DEFAULT_PACKET_CHANNEL_CAPACITY
614    }
615    fn default_tun_channel() -> usize {
616        1024
617    }
618    fn default_dns_channel() -> usize {
619        64
620    }
621}
622
623// ============================================================================
624// ECN Congestion Signaling
625// ============================================================================
626
627/// Rekey / session rekeying configuration (`node.rekey.*`).
628///
629/// Controls periodic full rekey for both FMP (link layer) and FSP
630/// (session layer) Noise sessions. Rekeying provides true forward secrecy
631/// with fresh DH randomness, nonce reset, and session index rotation.
632///
633/// Keep the packet-count default high for packet-tunnel workloads. A low value
634/// such as 65k packets can force multi-hundred-Mbit tunnels to rekey every few
635/// seconds, which creates avoidable cutover churn and can dominate throughput.
636/// Operators can still lower `node.rekey.after_messages` for CI stress tests or
637/// very conservative deployments; the time-based `after_secs` default remains
638/// the normal production rekey cadence.
639const DEFAULT_REKEY_AFTER_MESSAGES: u64 = 1 << 48;
640
641#[derive(Debug, Clone, Serialize, Deserialize)]
642pub struct RekeyConfig {
643    /// Enable periodic rekey (`node.rekey.enabled`).
644    #[serde(default = "RekeyConfig::default_enabled")]
645    pub enabled: bool,
646
647    /// Initiate rekey after this many seconds (`node.rekey.after_secs`).
648    #[serde(default = "RekeyConfig::default_after_secs")]
649    pub after_secs: u64,
650
651    /// Initiate rekey after this many messages sent (`node.rekey.after_messages`).
652    #[serde(default = "RekeyConfig::default_after_messages")]
653    pub after_messages: u64,
654}
655
656impl Default for RekeyConfig {
657    fn default() -> Self {
658        Self {
659            enabled: true,
660            after_secs: 120,
661            after_messages: DEFAULT_REKEY_AFTER_MESSAGES,
662        }
663    }
664}
665
666impl RekeyConfig {
667    fn default_enabled() -> bool {
668        true
669    }
670    fn default_after_secs() -> u64 {
671        120
672    }
673    fn default_after_messages() -> u64 {
674        DEFAULT_REKEY_AFTER_MESSAGES
675    }
676}
677
678/// ECN congestion signaling configuration (`node.ecn.*`).
679///
680/// Controls the FMP CE relay chain: transit nodes detect congestion on outgoing
681/// links and set the CE flag in forwarded datagrams. The destination marks
682/// IPv6 ECN-CE on ECN-capable packets before TUN delivery.
683#[derive(Debug, Clone, Serialize, Deserialize)]
684pub struct EcnConfig {
685    /// Enable ECN congestion signaling (`node.ecn.enabled`).
686    #[serde(default = "EcnConfig::default_enabled")]
687    pub enabled: bool,
688
689    /// Loss rate threshold for marking CE (`node.ecn.loss_threshold`).
690    /// When the outgoing link's loss rate meets or exceeds this value,
691    /// the transit node sets CE on forwarded datagrams.
692    #[serde(default = "EcnConfig::default_loss_threshold")]
693    pub loss_threshold: f64,
694
695    /// ETX threshold for marking CE (`node.ecn.etx_threshold`).
696    /// When the outgoing link's ETX meets or exceeds this value,
697    /// the transit node sets CE on forwarded datagrams.
698    #[serde(default = "EcnConfig::default_etx_threshold")]
699    pub etx_threshold: f64,
700}
701
702impl Default for EcnConfig {
703    fn default() -> Self {
704        Self {
705            enabled: true,
706            loss_threshold: 0.05,
707            etx_threshold: 3.0,
708        }
709    }
710}
711
712impl EcnConfig {
713    fn default_enabled() -> bool {
714        true
715    }
716    fn default_loss_threshold() -> f64 {
717        0.05
718    }
719    fn default_etx_threshold() -> f64 {
720        3.0
721    }
722}
723
724// ============================================================================
725// Node Configuration (Root)
726// ============================================================================
727
728/// Node configuration (`node.*`).
729#[derive(Debug, Clone, Serialize, Deserialize)]
730pub struct NodeConfig {
731    /// Identity configuration (`node.identity.*`).
732    #[serde(default)]
733    pub identity: IdentityConfig,
734
735    /// Leaf-only mode (`node.leaf_only`).
736    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
737    pub leaf_only: bool,
738
739    /// RX loop maintenance tick period in seconds (`node.tick_interval_secs`).
740    #[serde(default = "NodeConfig::default_tick_interval_secs")]
741    pub tick_interval_secs: u64,
742
743    /// Initial RTT estimate for new links in ms (`node.base_rtt_ms`).
744    #[serde(default = "NodeConfig::default_base_rtt_ms")]
745    pub base_rtt_ms: u64,
746
747    /// Link heartbeat send interval in seconds (`node.heartbeat_interval_secs`).
748    #[serde(default = "NodeConfig::default_heartbeat_interval_secs")]
749    pub heartbeat_interval_secs: u64,
750
751    /// Link dead timeout in seconds (`node.link_dead_timeout_secs`).
752    /// Peers silent for this duration are removed.
753    #[serde(default = "NodeConfig::default_link_dead_timeout_secs")]
754    pub link_dead_timeout_secs: u64,
755
756    /// Accelerated link dead timeout in seconds, used in place of
757    /// `link_dead_timeout_secs` while a recent `transport.send` returned
758    /// a local-side errno (`NetworkUnreachable` / `HostUnreachable` /
759    /// `AddrNotAvailable`) — direct evidence our outbound path is broken
760    /// right now (interface vanished, default route flapped, etc.). No
761    /// reason to wait the full receive-silence window when the kernel
762    /// already told us we can't send. Steady-state behavior is unchanged
763    /// because the signal is cleared on the next successful send.
764    /// (`node.fast_link_dead_timeout_secs`)
765    #[serde(default = "NodeConfig::default_fast_link_dead_timeout_secs")]
766    pub fast_link_dead_timeout_secs: u64,
767
768    /// Resource limits (`node.limits.*`).
769    #[serde(default)]
770    pub limits: LimitsConfig,
771
772    /// Connected UDP fast path (`node.connected_udp.*`).
773    #[serde(default)]
774    pub connected_udp: ConnectedUdpConfig,
775
776    /// Rate limiting (`node.rate_limit.*`).
777    #[serde(default)]
778    pub rate_limit: RateLimitConfig,
779
780    /// Retry/backoff (`node.retry.*`).
781    #[serde(default)]
782    pub retry: RetryConfig,
783
784    /// Cache parameters (`node.cache.*`).
785    #[serde(default)]
786    pub cache: CacheConfig,
787
788    /// Discovery protocol (`node.discovery.*`).
789    #[serde(default)]
790    pub discovery: DiscoveryConfig,
791
792    /// Spanning tree (`node.tree.*`).
793    #[serde(default)]
794    pub tree: TreeConfig,
795
796    /// Routing strategy (`node.routing.*`).
797    #[serde(default)]
798    pub routing: RoutingConfig,
799
800    /// Bloom filter (`node.bloom.*`).
801    #[serde(default)]
802    pub bloom: BloomConfig,
803
804    /// Session/data plane (`node.session.*`).
805    #[serde(default)]
806    pub session: SessionConfig,
807
808    /// Internal buffers (`node.buffers.*`).
809    #[serde(default)]
810    pub buffers: BuffersConfig,
811
812    /// Control socket (`node.control.*`).
813    #[serde(default)]
814    pub control: ControlConfig,
815
816    /// Metrics Measurement Protocol — link layer (`node.mmp.*`).
817    #[serde(default)]
818    pub mmp: MmpConfig,
819
820    /// Metrics Measurement Protocol — session layer (`node.session_mmp.*`).
821    #[serde(default)]
822    pub session_mmp: SessionMmpConfig,
823
824    /// ECN congestion signaling (`node.ecn.*`).
825    #[serde(default)]
826    pub ecn: EcnConfig,
827
828    /// Rekey / session rekeying (`node.rekey.*`).
829    #[serde(default)]
830    pub rekey: RekeyConfig,
831
832    /// Enable daemon-oriented system files such as `/etc/fips/hosts` and
833    /// `/etc/fips/peers.{allow,deny}`. Embedded endpoints disable this.
834    #[serde(default = "NodeConfig::default_system_files_enabled")]
835    pub system_files_enabled: bool,
836
837    /// Enable off-task Unix encrypt/decrypt worker pools (`node.worker_pools_enabled`).
838    /// Embedded/mobile endpoints can disable this to keep crypto/send work inline
839    /// with the rx loop when platform extension sandboxes make OS-thread pools
840    /// unsuitable.
841    #[serde(default = "NodeConfig::default_worker_pools_enabled")]
842    pub worker_pools_enabled: bool,
843
844    /// Log level (`node.log_level`). Case-insensitive.
845    /// Valid values: trace, debug, info, warn, error. Default: info.
846    #[serde(default)]
847    pub log_level: Option<String>,
848}
849
850impl Default for NodeConfig {
851    fn default() -> Self {
852        Self {
853            identity: IdentityConfig::default(),
854            leaf_only: false,
855            tick_interval_secs: 1,
856            base_rtt_ms: 100,
857            heartbeat_interval_secs: 10,
858            link_dead_timeout_secs: 30,
859            fast_link_dead_timeout_secs: 5,
860            limits: LimitsConfig::default(),
861            connected_udp: ConnectedUdpConfig::default(),
862            rate_limit: RateLimitConfig::default(),
863            retry: RetryConfig::default(),
864            cache: CacheConfig::default(),
865            discovery: DiscoveryConfig::default(),
866            tree: TreeConfig::default(),
867            routing: RoutingConfig::default(),
868            bloom: BloomConfig::default(),
869            session: SessionConfig::default(),
870            buffers: BuffersConfig::default(),
871            control: ControlConfig::default(),
872            mmp: MmpConfig::default(),
873            session_mmp: SessionMmpConfig::default(),
874            ecn: EcnConfig::default(),
875            rekey: RekeyConfig::default(),
876            system_files_enabled: true,
877            worker_pools_enabled: true,
878            log_level: None,
879        }
880    }
881}
882
883impl NodeConfig {
884    /// Get the log level as a tracing Level. Default: INFO.
885    pub fn log_level(&self) -> tracing::Level {
886        match self
887            .log_level
888            .as_deref()
889            .map(|s| s.to_lowercase())
890            .as_deref()
891        {
892            Some("trace") => tracing::Level::TRACE,
893            Some("debug") => tracing::Level::DEBUG,
894            Some("warn") | Some("warning") => tracing::Level::WARN,
895            Some("error") => tracing::Level::ERROR,
896            _ => tracing::Level::INFO,
897        }
898    }
899
900    fn default_tick_interval_secs() -> u64 {
901        1
902    }
903    fn default_base_rtt_ms() -> u64 {
904        100
905    }
906    fn default_heartbeat_interval_secs() -> u64 {
907        10
908    }
909    fn default_link_dead_timeout_secs() -> u64 {
910        30
911    }
912    fn default_fast_link_dead_timeout_secs() -> u64 {
913        5
914    }
915    fn default_system_files_enabled() -> bool {
916        true
917    }
918    fn default_worker_pools_enabled() -> bool {
919        true
920    }
921}
922
923#[cfg(test)]
924mod tests;