mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
A malformed FilterAnnounce whose fill ratio produces an implausibly high false-positive rate is mostly useless for routing and, once merged into our outgoing filter via bitwise OR, propagates the saturated state to tree peers one hop per announce tick. A saturated filter also made estimated_count() return f64::INFINITY, which compute_mesh_size summed into its cached estimate. handle_filter_announce now rejects inbound FilterAnnounce whose derived FPR exceeds `node.bloom.max_inbound_fpr` (new config field, default 0.05 ≈ fill 0.549 at k=5). Rejection is silent on the wire, logs at WARN, and increments a new `bloom.fill_exceeded` counter. The peer's prior stored filter and filter_sequence are left unchanged so a single rejected announce does not wipe the peer's existing contribution to aggregation. After a successful outgoing FilterAnnounce send, a rate-limited WARN fires if our own filter's FPR exceeds the same cap, surfacing aggregation drift. Limited to once per 60 seconds via a new Node.last_self_warn field. BloomFilter::estimated_count() now takes max_fpr and returns Option<f64>. Returns None for saturated filters (regardless of cap) or when the filter's FPR exceeds max_fpr. Callers updated: debug logs render None as "—", the Debug impl uses f64::INFINITY as "no cap" and prints "saturated" instead of inf, control-socket JSON emits null, and compute_mesh_size propagates None into the already- Option<u64> estimated_mesh_size field.
803 lines
26 KiB
Rust
803 lines
26 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,
|
|
/// Lookup completion timeout in seconds (`node.discovery.timeout_secs`).
|
|
#[serde(default = "DiscoveryConfig::default_timeout_secs")]
|
|
pub timeout_secs: 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 first lookup failure in seconds (`node.discovery.backoff_base_secs`).
|
|
/// Doubles per consecutive failure up to `backoff_max_secs`.
|
|
#[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,
|
|
/// Retry interval within the timeout window in seconds
|
|
/// (`node.discovery.retry_interval_secs`).
|
|
/// After this interval without a response, resend the lookup.
|
|
#[serde(default = "DiscoveryConfig::default_retry_interval_secs")]
|
|
pub retry_interval_secs: u64,
|
|
/// Maximum attempts per lookup (`node.discovery.max_attempts`).
|
|
/// 1 = no retry, 2 = one retry, etc.
|
|
#[serde(default = "DiscoveryConfig::default_max_attempts")]
|
|
pub max_attempts: u8,
|
|
}
|
|
|
|
impl Default for DiscoveryConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
ttl: 64,
|
|
timeout_secs: 10,
|
|
recent_expiry_secs: 10,
|
|
backoff_base_secs: 30,
|
|
backoff_max_secs: 300,
|
|
forward_min_interval_secs: 2,
|
|
retry_interval_secs: 5,
|
|
max_attempts: 2,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl DiscoveryConfig {
|
|
fn default_ttl() -> u8 {
|
|
64
|
|
}
|
|
fn default_timeout_secs() -> u64 {
|
|
10
|
|
}
|
|
fn default_recent_expiry_secs() -> u64 {
|
|
10
|
|
}
|
|
fn default_backoff_base_secs() -> u64 {
|
|
30
|
|
}
|
|
fn default_backoff_max_secs() -> u64 {
|
|
300
|
|
}
|
|
fn default_forward_min_interval_secs() -> u64 {
|
|
2
|
|
}
|
|
fn default_retry_interval_secs() -> u64 {
|
|
5
|
|
}
|
|
fn default_max_attempts() -> u8 {
|
|
2
|
|
}
|
|
}
|
|
|
|
/// 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.05` ≈ fill 0.549 at k=5 ≈ ~3,200 entries on the 1KB
|
|
/// filter. 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.05,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl BloomConfig {
|
|
fn default_update_debounce_ms() -> u64 {
|
|
500
|
|
}
|
|
fn default_max_inbound_fpr() -> f64 {
|
|
0.05
|
|
}
|
|
}
|
|
|
|
/// 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
|
|
}
|
|
|
|
fn default_socket_path() -> String {
|
|
if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
|
|
format!("{runtime_dir}/fips/control.sock")
|
|
} else if std::fs::create_dir_all("/run/fips").is_ok() {
|
|
"/run/fips/control.sock".to_string()
|
|
} else {
|
|
"/tmp/fips-control.sock".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,
|
|
}
|
|
|
|
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(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl NodeConfig {
|
|
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
|
|
}
|
|
}
|