mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 16:24:45 +00:00
1209 lines
42 KiB
Rust
1209 lines
42 KiB
Rust
//! Node configuration subsections.
|
||
//!
|
||
//! All the `node.*` configuration parameters: resource limits, rate limiting,
|
||
//! retry/backoff, cache sizing, discovery, spanning tree, bloom filters,
|
||
//! session management, and internal buffers.
|
||
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
use super::IdentityConfig;
|
||
use crate::mmp::{DEFAULT_LOG_INTERVAL_SECS, DEFAULT_OWD_WINDOW_SIZE, MmpConfig, MmpMode};
|
||
|
||
// ============================================================================
|
||
// Node Configuration Subsections
|
||
// ============================================================================
|
||
|
||
/// Resource limits (`node.limits.*`).
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct LimitsConfig {
|
||
/// Max handshake-phase connections (`node.limits.max_connections`).
|
||
#[serde(default = "LimitsConfig::default_max_connections")]
|
||
pub max_connections: usize,
|
||
/// Max authenticated peers (`node.limits.max_peers`).
|
||
#[serde(default = "LimitsConfig::default_max_peers")]
|
||
pub max_peers: usize,
|
||
/// Max active links (`node.limits.max_links`).
|
||
#[serde(default = "LimitsConfig::default_max_links")]
|
||
pub max_links: usize,
|
||
/// Max pending inbound handshakes (`node.limits.max_pending_inbound`).
|
||
#[serde(default = "LimitsConfig::default_max_pending_inbound")]
|
||
pub max_pending_inbound: usize,
|
||
}
|
||
|
||
impl Default for LimitsConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
max_connections: 256,
|
||
max_peers: 128,
|
||
max_links: 256,
|
||
max_pending_inbound: 1000,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl LimitsConfig {
|
||
fn default_max_connections() -> usize {
|
||
256
|
||
}
|
||
fn default_max_peers() -> usize {
|
||
128
|
||
}
|
||
fn default_max_links() -> usize {
|
||
256
|
||
}
|
||
fn default_max_pending_inbound() -> usize {
|
||
1000
|
||
}
|
||
}
|
||
|
||
/// Rate limiting (`node.rate_limit.*`).
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct RateLimitConfig {
|
||
/// Token bucket burst capacity (`node.rate_limit.handshake_burst`).
|
||
#[serde(default = "RateLimitConfig::default_handshake_burst")]
|
||
pub handshake_burst: u32,
|
||
/// Tokens/sec refill rate (`node.rate_limit.handshake_rate`).
|
||
#[serde(default = "RateLimitConfig::default_handshake_rate")]
|
||
pub handshake_rate: f64,
|
||
/// Stale handshake cleanup timeout in seconds (`node.rate_limit.handshake_timeout_secs`).
|
||
#[serde(default = "RateLimitConfig::default_handshake_timeout_secs")]
|
||
pub handshake_timeout_secs: u64,
|
||
/// Initial handshake resend interval in ms (`node.rate_limit.handshake_resend_interval_ms`).
|
||
/// Handshake messages are resent with exponential backoff within the timeout window.
|
||
#[serde(default = "RateLimitConfig::default_handshake_resend_interval_ms")]
|
||
pub handshake_resend_interval_ms: u64,
|
||
/// Handshake resend backoff multiplier (`node.rate_limit.handshake_resend_backoff`).
|
||
#[serde(default = "RateLimitConfig::default_handshake_resend_backoff")]
|
||
pub handshake_resend_backoff: f64,
|
||
/// Max handshake resends per attempt (`node.rate_limit.handshake_max_resends`).
|
||
#[serde(default = "RateLimitConfig::default_handshake_max_resends")]
|
||
pub handshake_max_resends: u32,
|
||
}
|
||
|
||
impl Default for RateLimitConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
handshake_burst: 100,
|
||
handshake_rate: 10.0,
|
||
handshake_timeout_secs: 30,
|
||
handshake_resend_interval_ms: 1000,
|
||
handshake_resend_backoff: 2.0,
|
||
handshake_max_resends: 5,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl RateLimitConfig {
|
||
fn default_handshake_burst() -> u32 {
|
||
100
|
||
}
|
||
fn default_handshake_rate() -> f64 {
|
||
10.0
|
||
}
|
||
fn default_handshake_timeout_secs() -> u64 {
|
||
30
|
||
}
|
||
fn default_handshake_resend_interval_ms() -> u64 {
|
||
1000
|
||
}
|
||
fn default_handshake_resend_backoff() -> f64 {
|
||
2.0
|
||
}
|
||
fn default_handshake_max_resends() -> u32 {
|
||
5
|
||
}
|
||
}
|
||
|
||
/// Retry/backoff configuration (`node.retry.*`).
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct RetryConfig {
|
||
/// Max connection retry attempts (`node.retry.max_retries`).
|
||
#[serde(default = "RetryConfig::default_max_retries")]
|
||
pub max_retries: u32,
|
||
/// Base backoff interval in seconds (`node.retry.base_interval_secs`).
|
||
#[serde(default = "RetryConfig::default_base_interval_secs")]
|
||
pub base_interval_secs: u64,
|
||
/// Cap on exponential backoff in seconds (`node.retry.max_backoff_secs`).
|
||
#[serde(default = "RetryConfig::default_max_backoff_secs")]
|
||
pub max_backoff_secs: u64,
|
||
}
|
||
|
||
impl Default for RetryConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
max_retries: 5,
|
||
base_interval_secs: 5,
|
||
max_backoff_secs: 300,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl RetryConfig {
|
||
fn default_max_retries() -> u32 {
|
||
5
|
||
}
|
||
fn default_base_interval_secs() -> u64 {
|
||
5
|
||
}
|
||
fn default_max_backoff_secs() -> u64 {
|
||
300
|
||
}
|
||
}
|
||
|
||
/// Cache parameters (`node.cache.*`).
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct CacheConfig {
|
||
/// Max entries in coord cache (`node.cache.coord_size`).
|
||
#[serde(default = "CacheConfig::default_coord_size")]
|
||
pub coord_size: usize,
|
||
/// Coord cache entry TTL in seconds (`node.cache.coord_ttl_secs`).
|
||
#[serde(default = "CacheConfig::default_coord_ttl_secs")]
|
||
pub coord_ttl_secs: u64,
|
||
/// Max entries in identity cache (`node.cache.identity_size`).
|
||
#[serde(default = "CacheConfig::default_identity_size")]
|
||
pub identity_size: usize,
|
||
}
|
||
|
||
impl Default for CacheConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
coord_size: 50_000,
|
||
coord_ttl_secs: 300,
|
||
identity_size: 10_000,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl CacheConfig {
|
||
fn default_coord_size() -> usize {
|
||
50_000
|
||
}
|
||
fn default_coord_ttl_secs() -> u64 {
|
||
300
|
||
}
|
||
fn default_identity_size() -> usize {
|
||
10_000
|
||
}
|
||
}
|
||
|
||
/// Discovery protocol (`node.discovery.*`).
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct DiscoveryConfig {
|
||
/// Hop limit for LookupRequest flood (`node.discovery.ttl`).
|
||
#[serde(default = "DiscoveryConfig::default_ttl")]
|
||
pub ttl: u8,
|
||
/// Per-attempt timeouts in seconds (`node.discovery.attempt_timeouts_secs`).
|
||
/// Each entry is the time to wait for a response before sending the next
|
||
/// LookupRequest (with a fresh request_id). Sequence length determines the
|
||
/// total number of attempts before declaring the destination unreachable.
|
||
/// Default `[1, 2, 4, 8]` gives 4 attempts and a 15s total budget.
|
||
#[serde(default = "DiscoveryConfig::default_attempt_timeouts_secs")]
|
||
pub attempt_timeouts_secs: Vec<u64>,
|
||
/// Dedup cache expiry in seconds (`node.discovery.recent_expiry_secs`).
|
||
#[serde(default = "DiscoveryConfig::default_recent_expiry_secs")]
|
||
pub recent_expiry_secs: u64,
|
||
/// Base backoff after lookup failure in seconds (`node.discovery.backoff_base_secs`).
|
||
/// Doubles per consecutive failure up to `backoff_max_secs`. Defaults to 0
|
||
/// (no post-failure suppression); the per-attempt sequence in
|
||
/// `attempt_timeouts_secs` provides the only retry pacing.
|
||
#[serde(default = "DiscoveryConfig::default_backoff_base_secs")]
|
||
pub backoff_base_secs: u64,
|
||
/// Maximum backoff cap in seconds (`node.discovery.backoff_max_secs`).
|
||
#[serde(default = "DiscoveryConfig::default_backoff_max_secs")]
|
||
pub backoff_max_secs: u64,
|
||
/// Minimum interval between forwarded lookups for the same target in seconds
|
||
/// (`node.discovery.forward_min_interval_secs`).
|
||
/// Defense-in-depth against misbehaving nodes.
|
||
#[serde(default = "DiscoveryConfig::default_forward_min_interval_secs")]
|
||
pub forward_min_interval_secs: u64,
|
||
/// Nostr-mediated overlay endpoint discovery.
|
||
#[serde(default = "DiscoveryConfig::default_nostr")]
|
||
pub nostr: NostrDiscoveryConfig,
|
||
/// mDNS / DNS-SD peer discovery on the local link. Identity surface
|
||
/// is a strict subset of what `nostr.advertise` already publishes
|
||
/// publicly, so there's no marginal privacy cost; the latency win
|
||
/// for same-LAN peers is large (sub-second pairing, no relay).
|
||
#[serde(default = "DiscoveryConfig::default_lan")]
|
||
pub lan: crate::discovery::lan::LanDiscoveryConfig,
|
||
}
|
||
|
||
impl Default for DiscoveryConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
ttl: 64,
|
||
attempt_timeouts_secs: vec![1, 2, 4, 8],
|
||
recent_expiry_secs: 10,
|
||
backoff_base_secs: 0,
|
||
backoff_max_secs: 0,
|
||
forward_min_interval_secs: 2,
|
||
nostr: NostrDiscoveryConfig::default(),
|
||
lan: crate::discovery::lan::LanDiscoveryConfig::default(),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl DiscoveryConfig {
|
||
fn default_ttl() -> u8 {
|
||
64
|
||
}
|
||
fn default_attempt_timeouts_secs() -> Vec<u64> {
|
||
vec![1, 2, 4, 8]
|
||
}
|
||
fn default_recent_expiry_secs() -> u64 {
|
||
10
|
||
}
|
||
fn default_backoff_base_secs() -> u64 {
|
||
0
|
||
}
|
||
fn default_backoff_max_secs() -> u64 {
|
||
0
|
||
}
|
||
fn default_forward_min_interval_secs() -> u64 {
|
||
2
|
||
}
|
||
fn default_nostr() -> NostrDiscoveryConfig {
|
||
NostrDiscoveryConfig::default()
|
||
}
|
||
fn default_lan() -> crate::discovery::lan::LanDiscoveryConfig {
|
||
crate::discovery::lan::LanDiscoveryConfig::default()
|
||
}
|
||
}
|
||
|
||
/// Nostr advert discovery policy.
|
||
///
|
||
/// Controls how overlay endpoint adverts are consumed:
|
||
/// - `disabled`: ignore advert-derived endpoints for all peers
|
||
/// - `configured_only`: allow advert fallback only for configured peers with
|
||
/// `peers[].via_nostr = true`
|
||
/// - `open`: also consider adverts for non-configured peers
|
||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||
#[serde(rename_all = "snake_case")]
|
||
pub enum NostrDiscoveryPolicy {
|
||
Disabled,
|
||
#[default]
|
||
ConfiguredOnly,
|
||
Open,
|
||
}
|
||
|
||
/// Nostr-mediated overlay endpoint discovery (`node.discovery.nostr.*`).
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
#[serde(deny_unknown_fields)]
|
||
pub struct NostrDiscoveryConfig {
|
||
/// Enable Nostr-signaled traversal bootstrap.
|
||
#[serde(default)]
|
||
pub enabled: bool,
|
||
/// Publish service advertisements so remote peers can bootstrap inbound.
|
||
#[serde(default = "NostrDiscoveryConfig::default_advertise")]
|
||
pub advertise: bool,
|
||
/// Relay URLs used for service advertisements.
|
||
#[serde(default = "NostrDiscoveryConfig::default_advert_relays")]
|
||
pub advert_relays: Vec<String>,
|
||
/// Relay URLs used for encrypted signaling events.
|
||
#[serde(default = "NostrDiscoveryConfig::default_dm_relays")]
|
||
pub dm_relays: Vec<String>,
|
||
/// STUN servers used for local reflexive address discovery.
|
||
/// Outbound observation uses only this local list; peer-advertised STUN
|
||
/// values are informational and are not treated as egress targets.
|
||
#[serde(default = "NostrDiscoveryConfig::default_stun_servers")]
|
||
pub stun_servers: Vec<String>,
|
||
/// Whether to advertise local (RFC 1918 / ULA) interface addresses as
|
||
/// host candidates in the traversal offer.
|
||
///
|
||
/// Off by default: in most deployments the relevant peers are not on the
|
||
/// same broadcast domain, and sharing private host candidates causes
|
||
/// misleading punch successes when an asymmetric L3 path (corporate VPN,
|
||
/// Tailscale subnet route, overlapping address space, etc.) makes a
|
||
/// peer's private IP one-way reachable from this node. Enable only when
|
||
/// peers are on the same physical LAN and same-LAN punching is wanted.
|
||
#[serde(default)]
|
||
pub share_local_candidates: bool,
|
||
/// Traversal application namespace and advert identifier suffix.
|
||
#[serde(default = "NostrDiscoveryConfig::default_app")]
|
||
pub app: String,
|
||
/// Signaling TTL in seconds.
|
||
#[serde(default = "NostrDiscoveryConfig::default_signal_ttl_secs")]
|
||
pub signal_ttl_secs: u64,
|
||
/// Policy for advert-derived endpoint discovery.
|
||
#[serde(default)]
|
||
pub policy: NostrDiscoveryPolicy,
|
||
/// Max number of open-discovery peers queued for outbound retry/connection
|
||
/// at once. Prevents unbounded queue growth from ambient advert traffic.
|
||
#[serde(default = "NostrDiscoveryConfig::default_open_discovery_max_pending")]
|
||
pub open_discovery_max_pending: usize,
|
||
/// Max concurrent inbound traversal offers processed at once.
|
||
/// Acts as a rate limit against offer spam from relays.
|
||
#[serde(default = "NostrDiscoveryConfig::default_max_concurrent_incoming_offers")]
|
||
pub max_concurrent_incoming_offers: usize,
|
||
/// Max cached overlay adverts retained from relay traffic.
|
||
/// Bounds memory under ambient advert volume.
|
||
#[serde(default = "NostrDiscoveryConfig::default_advert_cache_max_entries")]
|
||
pub advert_cache_max_entries: usize,
|
||
/// Max seen-session IDs retained for replay detection.
|
||
/// Oldest entries are evicted when the cap is exceeded.
|
||
#[serde(default = "NostrDiscoveryConfig::default_seen_sessions_max_entries")]
|
||
pub seen_sessions_max_entries: usize,
|
||
/// Overall punch attempt timeout in seconds.
|
||
#[serde(default = "NostrDiscoveryConfig::default_attempt_timeout_secs")]
|
||
pub attempt_timeout_secs: u64,
|
||
/// Replay tracking retention window in seconds.
|
||
#[serde(default = "NostrDiscoveryConfig::default_replay_window_secs")]
|
||
pub replay_window_secs: u64,
|
||
/// Delay before punch traffic starts.
|
||
#[serde(default = "NostrDiscoveryConfig::default_punch_start_delay_ms")]
|
||
pub punch_start_delay_ms: u64,
|
||
/// Interval between punch packets.
|
||
#[serde(default = "NostrDiscoveryConfig::default_punch_interval_ms")]
|
||
pub punch_interval_ms: u64,
|
||
/// How long to keep punching before failure.
|
||
#[serde(default = "NostrDiscoveryConfig::default_punch_duration_ms")]
|
||
pub punch_duration_ms: u64,
|
||
/// Advert TTL in seconds.
|
||
#[serde(default = "NostrDiscoveryConfig::default_advert_ttl_secs")]
|
||
pub advert_ttl_secs: u64,
|
||
/// How often adverts are refreshed in seconds.
|
||
#[serde(default = "NostrDiscoveryConfig::default_advert_refresh_secs")]
|
||
pub advert_refresh_secs: u64,
|
||
/// Settle delay in seconds after Nostr discovery starts before the
|
||
/// one-shot startup sweep of cached adverts runs. Allows the relay
|
||
/// subscription backlog to populate the in-memory advert cache.
|
||
/// Only used under `policy: open`. Default: 5.
|
||
#[serde(default = "NostrDiscoveryConfig::default_startup_sweep_delay_secs")]
|
||
pub startup_sweep_delay_secs: u64,
|
||
/// Maximum age in seconds for cached adverts considered by the
|
||
/// one-shot startup sweep. Adverts whose `created_at` is older than
|
||
/// `now - startup_sweep_max_age_secs` are skipped. Only used under
|
||
/// `policy: open`. Default: 3600 (1 hour).
|
||
#[serde(default = "NostrDiscoveryConfig::default_startup_sweep_max_age_secs")]
|
||
pub startup_sweep_max_age_secs: u64,
|
||
/// Number of consecutive NAT-traversal failures against a peer before
|
||
/// an extended cooldown is applied to throttle further offer publishes.
|
||
/// At this threshold the daemon also actively re-fetches the peer's
|
||
/// advert from `advert_relays` to evict cache entries for peers that
|
||
/// have gone away. Default: 5.
|
||
#[serde(default = "NostrDiscoveryConfig::default_failure_streak_threshold")]
|
||
pub failure_streak_threshold: u32,
|
||
/// Cooldown applied to a peer once `failure_streak_threshold` is hit.
|
||
/// Suppresses both open-discovery sweep enqueues and per-attempt
|
||
/// retry firings until elapsed. Default: 1800 (30 minutes).
|
||
#[serde(default = "NostrDiscoveryConfig::default_extended_cooldown_secs")]
|
||
pub extended_cooldown_secs: u64,
|
||
/// Minimum interval between `NAT traversal failed` WARN log lines for
|
||
/// the same peer. Subsequent failures inside the window log at DEBUG.
|
||
/// Reduces log spam on public-test nodes with many cache-learned
|
||
/// peers. Default: 300 (5 minutes).
|
||
#[serde(default = "NostrDiscoveryConfig::default_warn_log_interval_secs")]
|
||
pub warn_log_interval_secs: u64,
|
||
/// Maximum entries retained in the per-npub failure-state map.
|
||
/// Bounds memory under high cache turnover. Oldest entries (by last
|
||
/// failure time) evicted when the cap is exceeded. Default: 4096.
|
||
#[serde(default = "NostrDiscoveryConfig::default_failure_state_max_entries")]
|
||
pub failure_state_max_entries: usize,
|
||
/// Cooldown applied after observing a fatal protocol mismatch on a
|
||
/// Nostr-adopted bootstrap transport (e.g. `Unknown FMP version`
|
||
/// from a peer running a different FMP-protocol version). Independent
|
||
/// of `extended_cooldown_secs` and much longer because the mismatch
|
||
/// is structural — re-traversing the peer is wasted effort until one
|
||
/// side upgrades. Default: 86400 (24 hours).
|
||
#[serde(default = "NostrDiscoveryConfig::default_protocol_mismatch_cooldown_secs")]
|
||
pub protocol_mismatch_cooldown_secs: u64,
|
||
}
|
||
|
||
impl Default for NostrDiscoveryConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
enabled: false,
|
||
advertise: Self::default_advertise(),
|
||
advert_relays: Self::default_advert_relays(),
|
||
dm_relays: Self::default_dm_relays(),
|
||
stun_servers: Self::default_stun_servers(),
|
||
share_local_candidates: false,
|
||
app: Self::default_app(),
|
||
signal_ttl_secs: Self::default_signal_ttl_secs(),
|
||
policy: NostrDiscoveryPolicy::default(),
|
||
open_discovery_max_pending: Self::default_open_discovery_max_pending(),
|
||
max_concurrent_incoming_offers: Self::default_max_concurrent_incoming_offers(),
|
||
advert_cache_max_entries: Self::default_advert_cache_max_entries(),
|
||
seen_sessions_max_entries: Self::default_seen_sessions_max_entries(),
|
||
attempt_timeout_secs: Self::default_attempt_timeout_secs(),
|
||
replay_window_secs: Self::default_replay_window_secs(),
|
||
punch_start_delay_ms: Self::default_punch_start_delay_ms(),
|
||
punch_interval_ms: Self::default_punch_interval_ms(),
|
||
punch_duration_ms: Self::default_punch_duration_ms(),
|
||
advert_ttl_secs: Self::default_advert_ttl_secs(),
|
||
advert_refresh_secs: Self::default_advert_refresh_secs(),
|
||
startup_sweep_delay_secs: Self::default_startup_sweep_delay_secs(),
|
||
startup_sweep_max_age_secs: Self::default_startup_sweep_max_age_secs(),
|
||
failure_streak_threshold: Self::default_failure_streak_threshold(),
|
||
extended_cooldown_secs: Self::default_extended_cooldown_secs(),
|
||
warn_log_interval_secs: Self::default_warn_log_interval_secs(),
|
||
failure_state_max_entries: Self::default_failure_state_max_entries(),
|
||
protocol_mismatch_cooldown_secs: Self::default_protocol_mismatch_cooldown_secs(),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl NostrDiscoveryConfig {
|
||
fn default_advertise() -> bool {
|
||
true
|
||
}
|
||
|
||
fn default_advert_relays() -> Vec<String> {
|
||
vec![
|
||
"wss://relay.damus.io".to_string(),
|
||
"wss://nos.lol".to_string(),
|
||
"wss://offchain.pub".to_string(),
|
||
]
|
||
}
|
||
|
||
fn default_dm_relays() -> Vec<String> {
|
||
vec![
|
||
"wss://relay.damus.io".to_string(),
|
||
"wss://nos.lol".to_string(),
|
||
"wss://offchain.pub".to_string(),
|
||
]
|
||
}
|
||
|
||
fn default_stun_servers() -> Vec<String> {
|
||
vec![
|
||
"stun:stun.l.google.com:19302".to_string(),
|
||
"stun:stun.cloudflare.com:3478".to_string(),
|
||
"stun:global.stun.twilio.com:3478".to_string(),
|
||
]
|
||
}
|
||
|
||
fn default_app() -> String {
|
||
"fips-overlay-v1".to_string()
|
||
}
|
||
|
||
fn default_signal_ttl_secs() -> u64 {
|
||
120
|
||
}
|
||
|
||
fn default_open_discovery_max_pending() -> usize {
|
||
64
|
||
}
|
||
|
||
fn default_max_concurrent_incoming_offers() -> usize {
|
||
16
|
||
}
|
||
|
||
fn default_advert_cache_max_entries() -> usize {
|
||
2048
|
||
}
|
||
|
||
fn default_seen_sessions_max_entries() -> usize {
|
||
2048
|
||
}
|
||
|
||
fn default_attempt_timeout_secs() -> u64 {
|
||
10
|
||
}
|
||
|
||
fn default_replay_window_secs() -> u64 {
|
||
300
|
||
}
|
||
|
||
fn default_punch_start_delay_ms() -> u64 {
|
||
2_000
|
||
}
|
||
|
||
fn default_punch_interval_ms() -> u64 {
|
||
200
|
||
}
|
||
|
||
fn default_punch_duration_ms() -> u64 {
|
||
10_000
|
||
}
|
||
|
||
fn default_advert_ttl_secs() -> u64 {
|
||
3_600
|
||
}
|
||
|
||
fn default_advert_refresh_secs() -> u64 {
|
||
1_800
|
||
}
|
||
|
||
fn default_startup_sweep_delay_secs() -> u64 {
|
||
5
|
||
}
|
||
|
||
fn default_startup_sweep_max_age_secs() -> u64 {
|
||
3_600
|
||
}
|
||
|
||
fn default_failure_streak_threshold() -> u32 {
|
||
5
|
||
}
|
||
|
||
fn default_extended_cooldown_secs() -> u64 {
|
||
1_800
|
||
}
|
||
|
||
fn default_warn_log_interval_secs() -> u64 {
|
||
300
|
||
}
|
||
|
||
fn default_failure_state_max_entries() -> usize {
|
||
4_096
|
||
}
|
||
|
||
fn default_protocol_mismatch_cooldown_secs() -> u64 {
|
||
86_400
|
||
}
|
||
}
|
||
|
||
/// Spanning tree (`node.tree.*`).
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct TreeConfig {
|
||
/// Per-peer TreeAnnounce rate limit in ms (`node.tree.announce_min_interval_ms`).
|
||
#[serde(default = "TreeConfig::default_announce_min_interval_ms")]
|
||
pub announce_min_interval_ms: u64,
|
||
/// Hysteresis factor for cost-based parent re-selection (`node.tree.parent_hysteresis`).
|
||
///
|
||
/// Only switch parents when the candidate's effective_depth is better than
|
||
/// `current_effective_depth * (1.0 - parent_hysteresis)`. Range: 0.0-1.0.
|
||
/// Set to 0.0 to disable hysteresis (switch on any improvement).
|
||
#[serde(default = "TreeConfig::default_parent_hysteresis")]
|
||
pub parent_hysteresis: f64,
|
||
/// Hold-down period after parent switch in seconds (`node.tree.hold_down_secs`).
|
||
///
|
||
/// After switching parents, suppress re-evaluation for this duration to allow
|
||
/// MMP metrics to stabilize on the new link. Set to 0 to disable.
|
||
#[serde(default = "TreeConfig::default_hold_down_secs")]
|
||
pub hold_down_secs: u64,
|
||
/// Periodic parent re-evaluation interval in seconds (`node.tree.reeval_interval_secs`).
|
||
///
|
||
/// How often to re-evaluate parent selection based on current MMP link costs,
|
||
/// independent of TreeAnnounce traffic. Catches link degradation after the
|
||
/// tree has stabilized. Set to 0 to disable.
|
||
#[serde(default = "TreeConfig::default_reeval_interval_secs")]
|
||
pub reeval_interval_secs: u64,
|
||
/// Flap dampening: max parent switches before extended hold-down (`node.tree.flap_threshold`).
|
||
#[serde(default = "TreeConfig::default_flap_threshold")]
|
||
pub flap_threshold: u32,
|
||
/// Flap dampening: window in seconds for counting switches (`node.tree.flap_window_secs`).
|
||
#[serde(default = "TreeConfig::default_flap_window_secs")]
|
||
pub flap_window_secs: u64,
|
||
/// Flap dampening: extended hold-down duration in seconds (`node.tree.flap_dampening_secs`).
|
||
#[serde(default = "TreeConfig::default_flap_dampening_secs")]
|
||
pub flap_dampening_secs: u64,
|
||
}
|
||
|
||
impl Default for TreeConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
announce_min_interval_ms: 500,
|
||
parent_hysteresis: 0.2,
|
||
hold_down_secs: 30,
|
||
reeval_interval_secs: 60,
|
||
flap_threshold: 4,
|
||
flap_window_secs: 60,
|
||
flap_dampening_secs: 120,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl TreeConfig {
|
||
fn default_announce_min_interval_ms() -> u64 {
|
||
500
|
||
}
|
||
fn default_parent_hysteresis() -> f64 {
|
||
0.2
|
||
}
|
||
fn default_hold_down_secs() -> u64 {
|
||
30
|
||
}
|
||
fn default_reeval_interval_secs() -> u64 {
|
||
60
|
||
}
|
||
fn default_flap_threshold() -> u32 {
|
||
4
|
||
}
|
||
fn default_flap_window_secs() -> u64 {
|
||
60
|
||
}
|
||
fn default_flap_dampening_secs() -> u64 {
|
||
120
|
||
}
|
||
}
|
||
|
||
/// Bloom filter (`node.bloom.*`).
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct BloomConfig {
|
||
/// Debounce interval for filter updates in ms (`node.bloom.update_debounce_ms`).
|
||
#[serde(default = "BloomConfig::default_update_debounce_ms")]
|
||
pub update_debounce_ms: u64,
|
||
/// Antipoison cap: reject inbound FilterAnnounce whose FPR exceeds
|
||
/// this value (`node.bloom.max_inbound_fpr`). Valid range `(0.0, 1.0)`.
|
||
/// Default `0.10` ≈ fill 0.631 at k=5 ≈ ~1,630 entries on the 1 KB
|
||
/// filter (Swamidass–Baldi). Raised from 0.05 so aggregates that are
|
||
/// legitimately near their operating ceiling are not rejected before
|
||
/// the network reaches the fixed-filter capacity limit; conceptually
|
||
/// distinct from future autoscaling hysteresis setpoints — same unit,
|
||
/// different knobs.
|
||
#[serde(default = "BloomConfig::default_max_inbound_fpr")]
|
||
pub max_inbound_fpr: f64,
|
||
}
|
||
|
||
impl Default for BloomConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
update_debounce_ms: 500,
|
||
max_inbound_fpr: 0.10,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl BloomConfig {
|
||
fn default_update_debounce_ms() -> u64 {
|
||
500
|
||
}
|
||
fn default_max_inbound_fpr() -> f64 {
|
||
0.10
|
||
}
|
||
}
|
||
|
||
/// Session/data plane (`node.session.*`).
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct SessionConfig {
|
||
/// Default SessionDatagram TTL (`node.session.default_ttl`).
|
||
#[serde(default = "SessionConfig::default_ttl")]
|
||
pub default_ttl: u8,
|
||
/// Queue depth per dest during session establishment (`node.session.pending_packets_per_dest`).
|
||
#[serde(default = "SessionConfig::default_pending_packets_per_dest")]
|
||
pub pending_packets_per_dest: usize,
|
||
/// Max destinations with pending packets (`node.session.pending_max_destinations`).
|
||
#[serde(default = "SessionConfig::default_pending_max_destinations")]
|
||
pub pending_max_destinations: usize,
|
||
/// Idle session timeout in seconds (`node.session.idle_timeout_secs`).
|
||
/// Established sessions with no application data for this duration are
|
||
/// removed. MMP reports do not count as activity for this timer.
|
||
#[serde(default = "SessionConfig::default_idle_timeout_secs")]
|
||
pub idle_timeout_secs: u64,
|
||
/// Number of initial data packets per session that include COORDS_PRESENT
|
||
/// for transit cache warmup (`node.session.coords_warmup_packets`).
|
||
/// Also used as the reset count on CoordsRequired receipt.
|
||
#[serde(default = "SessionConfig::default_coords_warmup_packets")]
|
||
pub coords_warmup_packets: u8,
|
||
/// Minimum interval (ms) between standalone CoordsWarmup responses to
|
||
/// CoordsRequired/PathBroken signals, per destination
|
||
/// (`node.session.coords_response_interval_ms`).
|
||
#[serde(default = "SessionConfig::default_coords_response_interval_ms")]
|
||
pub coords_response_interval_ms: u64,
|
||
}
|
||
|
||
impl Default for SessionConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
default_ttl: 64,
|
||
pending_packets_per_dest: 16,
|
||
pending_max_destinations: 256,
|
||
idle_timeout_secs: 90,
|
||
coords_warmup_packets: 5,
|
||
coords_response_interval_ms: 2000,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl SessionConfig {
|
||
fn default_ttl() -> u8 {
|
||
64
|
||
}
|
||
fn default_pending_packets_per_dest() -> usize {
|
||
16
|
||
}
|
||
fn default_pending_max_destinations() -> usize {
|
||
256
|
||
}
|
||
fn default_idle_timeout_secs() -> u64 {
|
||
90
|
||
}
|
||
fn default_coords_warmup_packets() -> u8 {
|
||
5
|
||
}
|
||
fn default_coords_response_interval_ms() -> u64 {
|
||
2000
|
||
}
|
||
}
|
||
|
||
/// Session-layer Metrics Measurement Protocol (`node.session_mmp.*`).
|
||
///
|
||
/// Separate from link-layer `node.mmp.*` to allow independent mode/interval
|
||
/// configuration per layer. Session reports consume bandwidth on every transit
|
||
/// link, so operators may want a lighter mode (e.g., Lightweight) for sessions
|
||
/// while running Full mode on links.
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct SessionMmpConfig {
|
||
/// Operating mode (`node.session_mmp.mode`).
|
||
#[serde(default)]
|
||
pub mode: MmpMode,
|
||
|
||
/// Periodic operator log interval in seconds (`node.session_mmp.log_interval_secs`).
|
||
#[serde(default = "SessionMmpConfig::default_log_interval_secs")]
|
||
pub log_interval_secs: u64,
|
||
|
||
/// OWD trend ring buffer size (`node.session_mmp.owd_window_size`).
|
||
#[serde(default = "SessionMmpConfig::default_owd_window_size")]
|
||
pub owd_window_size: usize,
|
||
}
|
||
|
||
impl Default for SessionMmpConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
mode: MmpMode::default(),
|
||
log_interval_secs: DEFAULT_LOG_INTERVAL_SECS,
|
||
owd_window_size: DEFAULT_OWD_WINDOW_SIZE,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl SessionMmpConfig {
|
||
fn default_log_interval_secs() -> u64 {
|
||
DEFAULT_LOG_INTERVAL_SECS
|
||
}
|
||
fn default_owd_window_size() -> usize {
|
||
DEFAULT_OWD_WINDOW_SIZE
|
||
}
|
||
}
|
||
|
||
/// Control socket configuration (`node.control.*`).
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct ControlConfig {
|
||
/// Enable the control socket (`node.control.enabled`).
|
||
#[serde(default = "ControlConfig::default_enabled")]
|
||
pub enabled: bool,
|
||
/// Unix socket path (`node.control.socket_path`).
|
||
#[serde(default = "ControlConfig::default_socket_path")]
|
||
pub socket_path: String,
|
||
}
|
||
|
||
impl Default for ControlConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
enabled: true,
|
||
socket_path: Self::default_socket_path(),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl ControlConfig {
|
||
fn default_enabled() -> bool {
|
||
true
|
||
}
|
||
|
||
/// Default control socket path.
|
||
///
|
||
/// On Unix, delegates to [`super::resolve_default_socket`] for the
|
||
/// canonical `/run/fips` → `XDG_RUNTIME_DIR` → `/tmp` order shared with
|
||
/// the client-side `default_control_path`. On Windows, returns a TCP
|
||
/// port number as a string since Windows does not support Unix domain
|
||
/// sockets; the control socket listens on localhost at this port.
|
||
fn default_socket_path() -> String {
|
||
#[cfg(unix)]
|
||
{
|
||
super::resolve_default_socket("control.sock")
|
||
}
|
||
#[cfg(windows)]
|
||
{
|
||
"21210".to_string()
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Internal buffers (`node.buffers.*`).
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct BuffersConfig {
|
||
/// Transport→Node packet channel capacity (`node.buffers.packet_channel`).
|
||
#[serde(default = "BuffersConfig::default_packet_channel")]
|
||
pub packet_channel: usize,
|
||
/// TUN→Node outbound channel capacity (`node.buffers.tun_channel`).
|
||
#[serde(default = "BuffersConfig::default_tun_channel")]
|
||
pub tun_channel: usize,
|
||
/// DNS→Node identity channel capacity (`node.buffers.dns_channel`).
|
||
#[serde(default = "BuffersConfig::default_dns_channel")]
|
||
pub dns_channel: usize,
|
||
}
|
||
|
||
impl Default for BuffersConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
packet_channel: 1024,
|
||
tun_channel: 1024,
|
||
dns_channel: 64,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl BuffersConfig {
|
||
fn default_packet_channel() -> usize {
|
||
1024
|
||
}
|
||
fn default_tun_channel() -> usize {
|
||
1024
|
||
}
|
||
fn default_dns_channel() -> usize {
|
||
64
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// ECN Congestion Signaling
|
||
// ============================================================================
|
||
|
||
/// Rekey / session rekeying configuration (`node.rekey.*`).
|
||
///
|
||
/// Controls periodic full rekey for both FMP (link layer) and FSP
|
||
/// (session layer) Noise sessions. Rekeying provides true forward secrecy
|
||
/// with fresh DH randomness, nonce reset, and session index rotation.
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct RekeyConfig {
|
||
/// Enable periodic rekey (`node.rekey.enabled`).
|
||
#[serde(default = "RekeyConfig::default_enabled")]
|
||
pub enabled: bool,
|
||
|
||
/// Initiate rekey after this many seconds (`node.rekey.after_secs`).
|
||
#[serde(default = "RekeyConfig::default_after_secs")]
|
||
pub after_secs: u64,
|
||
|
||
/// Initiate rekey after this many messages sent (`node.rekey.after_messages`).
|
||
#[serde(default = "RekeyConfig::default_after_messages")]
|
||
pub after_messages: u64,
|
||
}
|
||
|
||
impl Default for RekeyConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
enabled: true,
|
||
after_secs: 120,
|
||
after_messages: 1 << 16, // 65536
|
||
}
|
||
}
|
||
}
|
||
|
||
impl RekeyConfig {
|
||
fn default_enabled() -> bool {
|
||
true
|
||
}
|
||
fn default_after_secs() -> u64 {
|
||
120
|
||
}
|
||
fn default_after_messages() -> u64 {
|
||
1 << 16
|
||
}
|
||
}
|
||
|
||
/// ECN congestion signaling configuration (`node.ecn.*`).
|
||
///
|
||
/// Controls the FMP CE relay chain: transit nodes detect congestion on outgoing
|
||
/// links and set the CE flag in forwarded datagrams. The destination marks
|
||
/// IPv6 ECN-CE on ECN-capable packets before TUN delivery.
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct EcnConfig {
|
||
/// Enable ECN congestion signaling (`node.ecn.enabled`).
|
||
#[serde(default = "EcnConfig::default_enabled")]
|
||
pub enabled: bool,
|
||
|
||
/// Loss rate threshold for marking CE (`node.ecn.loss_threshold`).
|
||
/// When the outgoing link's loss rate meets or exceeds this value,
|
||
/// the transit node sets CE on forwarded datagrams.
|
||
#[serde(default = "EcnConfig::default_loss_threshold")]
|
||
pub loss_threshold: f64,
|
||
|
||
/// ETX threshold for marking CE (`node.ecn.etx_threshold`).
|
||
/// When the outgoing link's ETX meets or exceeds this value,
|
||
/// the transit node sets CE on forwarded datagrams.
|
||
#[serde(default = "EcnConfig::default_etx_threshold")]
|
||
pub etx_threshold: f64,
|
||
}
|
||
|
||
impl Default for EcnConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
enabled: true,
|
||
loss_threshold: 0.05,
|
||
etx_threshold: 3.0,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl EcnConfig {
|
||
fn default_enabled() -> bool {
|
||
true
|
||
}
|
||
fn default_loss_threshold() -> f64 {
|
||
0.05
|
||
}
|
||
fn default_etx_threshold() -> f64 {
|
||
3.0
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// Node Configuration (Root)
|
||
// ============================================================================
|
||
|
||
/// Node configuration (`node.*`).
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct NodeConfig {
|
||
/// Identity configuration (`node.identity.*`).
|
||
#[serde(default)]
|
||
pub identity: IdentityConfig,
|
||
|
||
/// Leaf-only mode (`node.leaf_only`).
|
||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||
pub leaf_only: bool,
|
||
|
||
/// RX loop maintenance tick period in seconds (`node.tick_interval_secs`).
|
||
#[serde(default = "NodeConfig::default_tick_interval_secs")]
|
||
pub tick_interval_secs: u64,
|
||
|
||
/// Initial RTT estimate for new links in ms (`node.base_rtt_ms`).
|
||
#[serde(default = "NodeConfig::default_base_rtt_ms")]
|
||
pub base_rtt_ms: u64,
|
||
|
||
/// Link heartbeat send interval in seconds (`node.heartbeat_interval_secs`).
|
||
#[serde(default = "NodeConfig::default_heartbeat_interval_secs")]
|
||
pub heartbeat_interval_secs: u64,
|
||
|
||
/// Link dead timeout in seconds (`node.link_dead_timeout_secs`).
|
||
/// Peers silent for this duration are removed.
|
||
#[serde(default = "NodeConfig::default_link_dead_timeout_secs")]
|
||
pub link_dead_timeout_secs: u64,
|
||
|
||
/// Resource limits (`node.limits.*`).
|
||
#[serde(default)]
|
||
pub limits: LimitsConfig,
|
||
|
||
/// Rate limiting (`node.rate_limit.*`).
|
||
#[serde(default)]
|
||
pub rate_limit: RateLimitConfig,
|
||
|
||
/// Retry/backoff (`node.retry.*`).
|
||
#[serde(default)]
|
||
pub retry: RetryConfig,
|
||
|
||
/// Cache parameters (`node.cache.*`).
|
||
#[serde(default)]
|
||
pub cache: CacheConfig,
|
||
|
||
/// Discovery protocol (`node.discovery.*`).
|
||
#[serde(default)]
|
||
pub discovery: DiscoveryConfig,
|
||
|
||
/// Spanning tree (`node.tree.*`).
|
||
#[serde(default)]
|
||
pub tree: TreeConfig,
|
||
|
||
/// Bloom filter (`node.bloom.*`).
|
||
#[serde(default)]
|
||
pub bloom: BloomConfig,
|
||
|
||
/// Session/data plane (`node.session.*`).
|
||
#[serde(default)]
|
||
pub session: SessionConfig,
|
||
|
||
/// Internal buffers (`node.buffers.*`).
|
||
#[serde(default)]
|
||
pub buffers: BuffersConfig,
|
||
|
||
/// Control socket (`node.control.*`).
|
||
#[serde(default)]
|
||
pub control: ControlConfig,
|
||
|
||
/// Metrics Measurement Protocol — link layer (`node.mmp.*`).
|
||
#[serde(default)]
|
||
pub mmp: MmpConfig,
|
||
|
||
/// Metrics Measurement Protocol — session layer (`node.session_mmp.*`).
|
||
#[serde(default)]
|
||
pub session_mmp: SessionMmpConfig,
|
||
|
||
/// ECN congestion signaling (`node.ecn.*`).
|
||
#[serde(default)]
|
||
pub ecn: EcnConfig,
|
||
|
||
/// Rekey / session rekeying (`node.rekey.*`).
|
||
#[serde(default)]
|
||
pub rekey: RekeyConfig,
|
||
|
||
/// Log level (`node.log_level`). Case-insensitive.
|
||
/// Valid values: trace, debug, info, warn, error. Default: info.
|
||
#[serde(default)]
|
||
pub log_level: Option<String>,
|
||
}
|
||
|
||
impl Default for NodeConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
identity: IdentityConfig::default(),
|
||
leaf_only: false,
|
||
tick_interval_secs: 1,
|
||
base_rtt_ms: 100,
|
||
heartbeat_interval_secs: 10,
|
||
link_dead_timeout_secs: 30,
|
||
limits: LimitsConfig::default(),
|
||
rate_limit: RateLimitConfig::default(),
|
||
retry: RetryConfig::default(),
|
||
cache: CacheConfig::default(),
|
||
discovery: DiscoveryConfig::default(),
|
||
tree: TreeConfig::default(),
|
||
bloom: BloomConfig::default(),
|
||
session: SessionConfig::default(),
|
||
buffers: BuffersConfig::default(),
|
||
control: ControlConfig::default(),
|
||
mmp: MmpConfig::default(),
|
||
session_mmp: SessionMmpConfig::default(),
|
||
ecn: EcnConfig::default(),
|
||
rekey: RekeyConfig::default(),
|
||
log_level: None,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl NodeConfig {
|
||
/// Get the log level as a tracing Level. Default: INFO.
|
||
pub fn log_level(&self) -> tracing::Level {
|
||
match self
|
||
.log_level
|
||
.as_deref()
|
||
.map(|s| s.to_lowercase())
|
||
.as_deref()
|
||
{
|
||
Some("trace") => tracing::Level::TRACE,
|
||
Some("debug") => tracing::Level::DEBUG,
|
||
Some("warn") | Some("warning") => tracing::Level::WARN,
|
||
Some("error") => tracing::Level::ERROR,
|
||
_ => tracing::Level::INFO,
|
||
}
|
||
}
|
||
|
||
fn default_tick_interval_secs() -> u64 {
|
||
1
|
||
}
|
||
fn default_base_rtt_ms() -> u64 {
|
||
100
|
||
}
|
||
fn default_heartbeat_interval_secs() -> u64 {
|
||
10
|
||
}
|
||
fn default_link_dead_timeout_secs() -> u64 {
|
||
30
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn test_ecn_config_defaults() {
|
||
let c = EcnConfig::default();
|
||
assert!(c.enabled);
|
||
assert!((c.loss_threshold - 0.05).abs() < 1e-9);
|
||
assert!((c.etx_threshold - 3.0).abs() < 1e-9);
|
||
}
|
||
|
||
#[test]
|
||
fn test_ecn_config_yaml_roundtrip() {
|
||
let yaml = "loss_threshold: 0.10\netx_threshold: 2.5\nenabled: false\n";
|
||
let c: EcnConfig = serde_yaml::from_str(yaml).unwrap();
|
||
assert!(!c.enabled);
|
||
assert!((c.loss_threshold - 0.10).abs() < 1e-9);
|
||
assert!((c.etx_threshold - 2.5).abs() < 1e-9);
|
||
}
|
||
|
||
#[test]
|
||
fn test_ecn_config_partial_yaml() {
|
||
// Only specify loss_threshold — others should get defaults
|
||
let yaml = "loss_threshold: 0.02\n";
|
||
let c: EcnConfig = serde_yaml::from_str(yaml).unwrap();
|
||
assert!(c.enabled); // default
|
||
assert!((c.loss_threshold - 0.02).abs() < 1e-9);
|
||
assert!((c.etx_threshold - 3.0).abs() < 1e-9); // default
|
||
}
|
||
|
||
#[test]
|
||
fn test_nostr_discovery_startup_sweep_defaults() {
|
||
let c = NostrDiscoveryConfig::default();
|
||
assert_eq!(c.startup_sweep_delay_secs, 5);
|
||
assert_eq!(c.startup_sweep_max_age_secs, 3_600);
|
||
}
|
||
|
||
#[test]
|
||
fn test_nostr_discovery_startup_sweep_yaml_override() {
|
||
let yaml = "enabled: true\npolicy: open\nstartup_sweep_delay_secs: 10\nstartup_sweep_max_age_secs: 1800\n";
|
||
let c: NostrDiscoveryConfig = serde_yaml::from_str(yaml).unwrap();
|
||
assert!(c.enabled);
|
||
assert_eq!(c.policy, NostrDiscoveryPolicy::Open);
|
||
assert_eq!(c.startup_sweep_delay_secs, 10);
|
||
assert_eq!(c.startup_sweep_max_age_secs, 1_800);
|
||
}
|
||
|
||
#[test]
|
||
fn test_nostr_discovery_startup_sweep_partial_yaml_uses_defaults() {
|
||
// Only override delay; max_age should fall back to default.
|
||
let yaml = "enabled: true\nstartup_sweep_delay_secs: 30\n";
|
||
let c: NostrDiscoveryConfig = serde_yaml::from_str(yaml).unwrap();
|
||
assert_eq!(c.startup_sweep_delay_secs, 30);
|
||
assert_eq!(c.startup_sweep_max_age_secs, 3_600);
|
||
}
|
||
|
||
#[test]
|
||
fn test_log_level_parser() {
|
||
// Pin the observed behavior of NodeConfig::log_level():
|
||
// - 5 explicit lowercased match arms (trace/debug/warn|warning/error)
|
||
// - INFO is the default (no explicit "info" arm; falls through default)
|
||
// - Case-insensitive via .to_lowercase()
|
||
// - Unknown strings and None both fall through to INFO
|
||
let cases: &[(Option<&str>, tracing::Level)] = &[
|
||
// Explicit arms (lowercase canonical form)
|
||
(Some("trace"), tracing::Level::TRACE),
|
||
(Some("debug"), tracing::Level::DEBUG),
|
||
(Some("warn"), tracing::Level::WARN),
|
||
(Some("warning"), tracing::Level::WARN),
|
||
(Some("error"), tracing::Level::ERROR),
|
||
// "info" has no explicit arm — falls through default
|
||
(Some("info"), tracing::Level::INFO),
|
||
// None → default INFO
|
||
(None, tracing::Level::INFO),
|
||
// Case-insensitivity (parser lowercases via .to_lowercase())
|
||
(Some("TRACE"), tracing::Level::TRACE),
|
||
(Some("Debug"), tracing::Level::DEBUG),
|
||
(Some("Warning"), tracing::Level::WARN),
|
||
(Some("WARN"), tracing::Level::WARN),
|
||
(Some("ERROR"), tracing::Level::ERROR),
|
||
(Some("INFO"), tracing::Level::INFO),
|
||
// Unknown strings → INFO default (no error path)
|
||
(Some("verbose"), tracing::Level::INFO),
|
||
(Some("nonsense"), tracing::Level::INFO),
|
||
(Some(""), tracing::Level::INFO),
|
||
];
|
||
|
||
for (input, expected) in cases {
|
||
let cfg = NodeConfig {
|
||
log_level: input.map(|s| s.to_string()),
|
||
..NodeConfig::default()
|
||
};
|
||
assert_eq!(
|
||
cfg.log_level(),
|
||
*expected,
|
||
"input {:?} should map to {:?}",
|
||
input,
|
||
expected
|
||
);
|
||
}
|
||
}
|
||
|
||
#[cfg(windows)]
|
||
#[test]
|
||
fn test_default_socket_path_windows() {
|
||
let config = ControlConfig::default();
|
||
// On Windows, socket_path is a TCP port number
|
||
let port: u16 = config
|
||
.socket_path
|
||
.parse()
|
||
.expect("should be a valid port number");
|
||
assert_eq!(port, 21210);
|
||
}
|
||
}
|