//! Runtime configuration for mesh networking. //! //! This module provides centralized configuration with sensible defaults //! and validation. Configuration can be loaded from files, environment //! variables, or set programmatically. use std::path::PathBuf; use std::time::Duration; use serde::{Deserialize, Serialize}; use crate::error::{ConfigError, MeshResult}; use crate::transport::CryptoMode; /// Top-level mesh node configuration. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(default)] pub struct MeshConfig { /// Node identity configuration. pub identity: IdentityConfig, /// Announce protocol configuration. pub announce: AnnounceConfig, /// Routing configuration. pub routing: RoutingConfig, /// Store-and-forward configuration. pub store: StoreConfig, /// Transport configuration. pub transport: TransportConfig, /// Crypto configuration. pub crypto: CryptoConfig, /// Rate limiting configuration. pub rate_limit: RateLimitConfig, /// Logging configuration. pub logging: LoggingConfig, } impl Default for MeshConfig { fn default() -> Self { Self { identity: IdentityConfig::default(), announce: AnnounceConfig::default(), routing: RoutingConfig::default(), store: StoreConfig::default(), transport: TransportConfig::default(), crypto: CryptoConfig::default(), rate_limit: RateLimitConfig::default(), logging: LoggingConfig::default(), } } } impl MeshConfig { /// Load configuration from a TOML file. pub fn from_file(path: &PathBuf) -> MeshResult { let content = std::fs::read_to_string(path).map_err(|e| { ConfigError::Parse(format!("failed to read config file: {}", e)) })?; Self::from_toml(&content) } /// Parse configuration from TOML string. pub fn from_toml(toml: &str) -> MeshResult { let config: Self = toml::from_str(toml).map_err(|e| { ConfigError::Parse(format!("TOML parse error: {}", e)) })?; config.validate()?; Ok(config) } /// Serialize to TOML string. pub fn to_toml(&self) -> MeshResult { toml::to_string_pretty(self).map_err(|e| { ConfigError::Parse(format!("TOML serialize error: {}", e)).into() }) } /// Validate configuration values. pub fn validate(&self) -> MeshResult<()> { self.announce.validate()?; self.routing.validate()?; self.store.validate()?; self.rate_limit.validate()?; Ok(()) } /// Create a minimal config for constrained devices. pub fn constrained() -> Self { Self { store: StoreConfig { max_messages: 100, max_keypackages: 50, ..Default::default() }, routing: RoutingConfig { max_entries: 100, ..Default::default() }, announce: AnnounceConfig { interval: Duration::from_secs(1800), // 30 min ..Default::default() }, crypto: CryptoConfig { default_mode: CryptoMode::MlsLiteUnsigned, ..Default::default() }, ..Default::default() } } } /// Identity configuration. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(default)] pub struct IdentityConfig { /// Path to persist identity keypair. pub keypair_path: Option, /// Whether to auto-generate keypair if missing. pub auto_generate: bool, } impl Default for IdentityConfig { fn default() -> Self { Self { keypair_path: None, auto_generate: true, } } } /// Announce protocol configuration. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(default)] pub struct AnnounceConfig { /// Interval between periodic announcements. #[serde(with = "humantime_serde")] pub interval: Duration, /// Maximum age before announce is considered stale. #[serde(with = "humantime_serde")] pub max_age: Duration, /// Maximum propagation hops. pub max_hops: u8, /// Capabilities to advertise. pub capabilities: u16, /// Whether to include KeyPackage hash in announces. pub include_keypackage: bool, } impl Default for AnnounceConfig { fn default() -> Self { Self { interval: Duration::from_secs(600), // 10 min max_age: Duration::from_secs(1800), // 30 min max_hops: 8, capabilities: 0x0003, // CAP_RELAY | CAP_STORE include_keypackage: true, } } } impl AnnounceConfig { fn validate(&self) -> MeshResult<()> { if self.interval < Duration::from_secs(10) { return Err(ConfigError::InvalidValue { key: "announce.interval".to_string(), reason: "must be at least 10 seconds".to_string(), }.into()); } if self.max_hops == 0 || self.max_hops > 32 { return Err(ConfigError::InvalidValue { key: "announce.max_hops".to_string(), reason: "must be between 1 and 32".to_string(), }.into()); } Ok(()) } } /// Routing configuration. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(default)] pub struct RoutingConfig { /// Maximum routing table entries. pub max_entries: usize, /// Default route TTL. #[serde(with = "humantime_serde")] pub default_ttl: Duration, /// How often to garbage collect expired routes. #[serde(with = "humantime_serde")] pub gc_interval: Duration, } impl Default for RoutingConfig { fn default() -> Self { Self { max_entries: 10_000, default_ttl: Duration::from_secs(1800), // 30 min gc_interval: Duration::from_secs(60), } } } impl RoutingConfig { fn validate(&self) -> MeshResult<()> { if self.max_entries == 0 { return Err(ConfigError::InvalidValue { key: "routing.max_entries".to_string(), reason: "must be at least 1".to_string(), }.into()); } Ok(()) } } /// Store-and-forward configuration. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(default)] pub struct StoreConfig { /// Maximum messages in store. pub max_messages: usize, /// Maximum messages per recipient. pub max_per_recipient: usize, /// Maximum cached KeyPackages. pub max_keypackages: usize, /// Maximum KeyPackages per address. pub max_keypackages_per_addr: usize, /// Default message TTL. #[serde(with = "humantime_serde")] pub default_ttl: Duration, /// Path for persistent storage (None = in-memory only). pub persistence_path: Option, } impl Default for StoreConfig { fn default() -> Self { Self { max_messages: 10_000, max_per_recipient: 100, max_keypackages: 1_000, max_keypackages_per_addr: 3, default_ttl: Duration::from_secs(24 * 3600), // 24 hours persistence_path: None, } } } impl StoreConfig { fn validate(&self) -> MeshResult<()> { if self.max_messages == 0 { return Err(ConfigError::InvalidValue { key: "store.max_messages".to_string(), reason: "must be at least 1".to_string(), }.into()); } Ok(()) } } /// Transport configuration. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(default)] pub struct TransportConfig { /// Enable iroh/QUIC transport. pub enable_iroh: bool, /// Enable TCP transport. pub enable_tcp: bool, /// TCP listen address. pub tcp_listen: Option, /// Enable LoRa transport. pub enable_lora: bool, /// LoRa device path (e.g., /dev/ttyUSB0). pub lora_device: Option, /// LoRa spreading factor (7-12). pub lora_sf: u8, /// LoRa bandwidth in kHz. pub lora_bw: u32, /// Connection timeout. #[serde(with = "humantime_serde")] pub connect_timeout: Duration, /// Send timeout. #[serde(with = "humantime_serde")] pub send_timeout: Duration, } impl Default for TransportConfig { fn default() -> Self { Self { enable_iroh: true, enable_tcp: true, tcp_listen: None, enable_lora: false, lora_device: None, lora_sf: 10, lora_bw: 125, connect_timeout: Duration::from_secs(10), send_timeout: Duration::from_secs(30), } } } /// Crypto configuration. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(default)] pub struct CryptoConfig { /// Default crypto mode. pub default_mode: CryptoMode, /// Whether to auto-upgrade to better crypto when available. pub auto_upgrade: bool, /// Whether to sign MLS-Lite messages. pub mls_lite_sign: bool, /// Enable post-quantum hybrid mode. pub enable_pq: bool, } impl Default for CryptoConfig { fn default() -> Self { Self { default_mode: CryptoMode::MlsClassical, auto_upgrade: true, mls_lite_sign: true, enable_pq: false, // PQ is large, opt-in } } } /// Rate limiting configuration. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(default)] pub struct RateLimitConfig { /// Maximum announces per peer per minute. pub announce_per_peer_per_min: u32, /// Maximum messages per peer per minute. pub message_per_peer_per_min: u32, /// Maximum KeyPackage requests per minute. pub keypackage_requests_per_min: u32, /// LoRa duty cycle limit (0.0-1.0, e.g., 0.01 = 1%). pub lora_duty_cycle: f32, } impl Default for RateLimitConfig { fn default() -> Self { Self { announce_per_peer_per_min: 10, message_per_peer_per_min: 60, keypackage_requests_per_min: 20, lora_duty_cycle: 0.01, // EU868 1% default } } } impl RateLimitConfig { fn validate(&self) -> MeshResult<()> { if self.lora_duty_cycle < 0.0 || self.lora_duty_cycle > 1.0 { return Err(ConfigError::InvalidValue { key: "rate_limit.lora_duty_cycle".to_string(), reason: "must be between 0.0 and 1.0".to_string(), }.into()); } Ok(()) } } /// Logging configuration. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(default)] pub struct LoggingConfig { /// Log level (trace, debug, info, warn, error). pub level: String, /// Whether to log to file. pub file: Option, /// Whether to include timestamps. pub timestamps: bool, /// Whether to include span context. pub spans: bool, } impl Default for LoggingConfig { fn default() -> Self { Self { level: "info".to_string(), file: None, timestamps: true, spans: false, } } } // Serde helper for CryptoMode impl Serialize for CryptoMode { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { let s = match self { CryptoMode::MlsHybrid => "mls-hybrid", CryptoMode::MlsClassical => "mls-classical", CryptoMode::MlsLiteSigned => "mls-lite-signed", CryptoMode::MlsLiteUnsigned => "mls-lite-unsigned", }; serializer.serialize_str(s) } } impl<'de> Deserialize<'de> for CryptoMode { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let s = String::deserialize(deserializer)?; match s.as_str() { "mls-hybrid" => Ok(CryptoMode::MlsHybrid), "mls-classical" => Ok(CryptoMode::MlsClassical), "mls-lite-signed" => Ok(CryptoMode::MlsLiteSigned), "mls-lite-unsigned" => Ok(CryptoMode::MlsLiteUnsigned), _ => Err(serde::de::Error::unknown_variant( &s, &["mls-hybrid", "mls-classical", "mls-lite-signed", "mls-lite-unsigned"], )), } } } #[cfg(test)] mod tests { use super::*; #[test] fn default_config_is_valid() { let config = MeshConfig::default(); assert!(config.validate().is_ok()); } #[test] fn constrained_config_is_valid() { let config = MeshConfig::constrained(); assert!(config.validate().is_ok()); assert_eq!(config.store.max_messages, 100); } #[test] fn toml_roundtrip() { let config = MeshConfig::default(); let toml = config.to_toml().expect("serialize"); let restored = MeshConfig::from_toml(&toml).expect("parse"); assert_eq!(config.announce.max_hops, restored.announce.max_hops); } #[test] fn invalid_announce_interval() { let mut config = MeshConfig::default(); config.announce.interval = Duration::from_secs(1); // Too short assert!(config.validate().is_err()); } #[test] fn invalid_duty_cycle() { let mut config = MeshConfig::default(); config.rate_limit.lora_duty_cycle = 2.0; // > 1.0 assert!(config.validate().is_err()); } }