config.rs
213 lignes · 7579 octets
//! Runtime configuration for the fediverse guard. //! //! Parsed once from `FEDIVERSE_GUARD_*` env vars at boot via //! [`GuardConfig::from_env`]. Production deployments override defaults //! through env vars; tests can build a `GuardConfig` literal directly. use std::time::Duration; use thiserror::Error; /// Errors raised while loading [`GuardConfig`] from the environment. #[derive(Debug, Error)] pub enum GuardConfigError { /// An env var was set to a value that could not be parsed. #[error("invalid value for {name}: {source}")] InvalidValue { /// Name of the env var that failed to parse. name: &'static str, /// Underlying parser error. #[source] source: Box<dyn std::error::Error + Send + Sync>, }, } /// Per-detector tunable threshold. A source (domain or actor) is /// auto-banned when more than `count` qualifying events are observed /// within `window`. #[derive(Debug, Clone, Copy)] pub struct DetectorThreshold { /// Maximum events tolerated within the sliding window. pub count: u32, /// Sliding-window duration over which `count` is enforced. pub window: Duration, } impl DetectorThreshold { /// Build a threshold from explicit values. #[must_use] pub const fn new(count: u32, window_secs: u64) -> Self { Self { count, window: Duration::from_secs(window_secs), } } } /// Top-level runtime configuration for the fediverse guard. #[derive(Debug, Clone)] pub struct GuardConfig { /// Master kill-switch. When `false`, no detector runs and /// [`crate::BanManager::is_banned`] always returns `false`. pub enabled: bool, /// When `true`, detectors emit events and tracing logs but no ban /// is ever persisted. Use this to validate thresholds in /// production before flipping the switch. pub dry_run: bool, /// HTTP signatures that fail verification, per signing actor's /// domain. pub signature_failures: DetectorThreshold, /// Detected replays (Date drift / duplicate nonce / signature /// already seen). pub replay_attempts: DetectorThreshold, /// Attempts by the local outbound fetcher to talk to a private/ /// loopback/link-local IP. Surfaces SSRF probing campaigns. pub fetch_ssrf: DetectorThreshold, /// Inbox payloads that fail size, schema, or activity-type /// validation. pub inbox_abuse: DetectorThreshold, /// AP-native auth challenges initiated without subsequent consume. pub auth_spam: DetectorThreshold, /// TTL applied to auto-bans. `None` means permanent. pub auto_ban_duration: Option<Duration>, /// Retention for raw events in `fediverse_guard_events`. pub events_retention: Duration, } impl GuardConfig { /// Load configuration from `FEDIVERSE_GUARD_*` environment /// variables, falling back to documented defaults for any var /// that is unset. /// /// # Errors /// /// Returns [`GuardConfigError::InvalidValue`] when an env var /// is set to a value that cannot be parsed as the expected /// type. pub fn from_env() -> Result<Self, GuardConfigError> { Ok(Self { enabled: parse_bool_env("FEDIVERSE_GUARD_ENABLED", true)?, dry_run: parse_bool_env("FEDIVERSE_GUARD_DRY_RUN", false)?, signature_failures: DetectorThreshold { count: parse_u32_env("FEDIVERSE_GUARD_SIG_FAILURE_THRESHOLD", 10)?, window: Duration::from_secs(parse_u64_env( "FEDIVERSE_GUARD_SIG_FAILURE_WINDOW_SECS", 300, )?), }, replay_attempts: DetectorThreshold { count: parse_u32_env("FEDIVERSE_GUARD_REPLAY_THRESHOLD", 3)?, window: Duration::from_secs(parse_u64_env( "FEDIVERSE_GUARD_REPLAY_WINDOW_SECS", 600, )?), }, fetch_ssrf: DetectorThreshold { count: parse_u32_env("FEDIVERSE_GUARD_SSRF_THRESHOLD", 5)?, window: Duration::from_secs(parse_u64_env( "FEDIVERSE_GUARD_SSRF_WINDOW_SECS", 600, )?), }, inbox_abuse: DetectorThreshold { count: parse_u32_env("FEDIVERSE_GUARD_INBOX_ABUSE_THRESHOLD", 20)?, window: Duration::from_secs(parse_u64_env( "FEDIVERSE_GUARD_INBOX_ABUSE_WINDOW_SECS", 300, )?), }, auth_spam: DetectorThreshold { count: parse_u32_env("FEDIVERSE_GUARD_AUTH_SPAM_THRESHOLD", 30)?, window: Duration::from_secs(parse_u64_env( "FEDIVERSE_GUARD_AUTH_SPAM_WINDOW_SECS", 300, )?), }, auto_ban_duration: { let secs = parse_u64_env("FEDIVERSE_GUARD_AUTO_BAN_DURATION_SECS", 3600)?; if secs == 0 { None } else { Some(Duration::from_secs(secs)) } }, events_retention: Duration::from_secs( parse_u64_env("FEDIVERSE_GUARD_EVENTS_RETENTION_DAYS", 90)? * 24 * 3600, ), }) } /// Build a permissive `GuardConfig` for tests. #[must_use] pub fn for_tests() -> Self { Self { enabled: true, dry_run: false, signature_failures: DetectorThreshold::new(3, 60), replay_attempts: DetectorThreshold::new(2, 60), fetch_ssrf: DetectorThreshold::new(3, 60), inbox_abuse: DetectorThreshold::new(5, 60), auth_spam: DetectorThreshold::new(5, 60), auto_ban_duration: Some(Duration::from_secs(60)), events_retention: Duration::from_secs(3600), } } } fn parse_bool_env(name: &'static str, default: bool) -> Result<bool, GuardConfigError> { match std::env::var(name) { Ok(v) => match v.to_ascii_lowercase().as_str() { "true" | "1" | "yes" => Ok(true), "false" | "0" | "no" => Ok(false), other => Err(GuardConfigError::InvalidValue { name, source: format!("expected true/false, got {other:?}").into(), }), }, Err(_) => Ok(default), } } fn parse_u32_env(name: &'static str, default: u32) -> Result<u32, GuardConfigError> { match std::env::var(name) { Ok(v) => v .parse::<u32>() .map_err(|e| GuardConfigError::InvalidValue { name, source: Box::new(e), }), Err(_) => Ok(default), } } fn parse_u64_env(name: &'static str, default: u64) -> Result<u64, GuardConfigError> { match std::env::var(name) { Ok(v) => v .parse::<u64>() .map_err(|e| GuardConfigError::InvalidValue { name, source: Box::new(e), }), Err(_) => Ok(default), } } #[cfg(test)] mod tests { use super::*; #[test] fn for_tests_yields_sane_defaults() { let cfg = GuardConfig::for_tests(); assert!(cfg.enabled); assert!(!cfg.dry_run); assert_eq!(cfg.signature_failures.count, 3); } #[test] fn detector_threshold_new_converts_secs_to_duration() { let t = DetectorThreshold::new(7, 120); assert_eq!(t.count, 7); assert_eq!(t.window, Duration::from_secs(120)); } }
GitRust