//! Defense module: decoy IP rotation, anti-bot suspicion scoring, and simple fallbacks. use rand::seq::SliceRandom; use serde::Serialize; use std::net::IpAddr; #[derive(Debug, Clone, Serialize)] pub struct DecoySet { pub current: String, pub pool: Vec, } pub fn rotate_decoy_ip(pool: &[String]) -> DecoySet { let mut rng = rand::thread_rng(); let mut p = pool.to_vec(); p.shuffle(&mut rng); let current = p.first().cloned().unwrap_or_else(|| "127.0.0.1".into()); DecoySet { current, pool: p } } #[derive(Debug, Clone, Serialize)] pub struct BotCheck { pub score: u8, // 0 (good) .. 100 (bot) pub reason: String, } pub fn suspicion_score(user_agent: Option<&str>, ip: Option) -> BotCheck { let mut score = 0u8; let mut reasons = Vec::new(); if let Some(ua) = user_agent { let ua_l = ua.to_ascii_lowercase(); if ua_l.contains("curl/") || ua_l.contains("wget/") { score = score.saturating_add(30); reasons.push("cli-agent"); } if ua_l.contains("bot") || ua_l.contains("crawler") { score = score.saturating_add(50); reasons.push("bot-keyword"); } } else { score = score.saturating_add(20); reasons.push("missing-ua"); } if let Some(ip) = ip { match ip { IpAddr::V4(v4) if v4.is_private() => { score = score.saturating_add(5); reasons.push("private-ip"); } _ => {} } } if score == 0 { reasons.push("ok"); } BotCheck { score, reason: reasons.join(",") } } #[derive(Debug, Clone, Serialize)] pub struct DnsHealing { pub primary: String, pub fallbacks: Vec, } pub fn dns_self_heal(primary: &str, fallbacks: &[&str]) -> DnsHealing { DnsHealing { primary: primary.to_string(), fallbacks: fallbacks.iter().map(|s| s.to_string()).collect() } } #[derive(Debug, Clone, Serialize, Default)] pub struct DefenseStatus { pub decoy_current: String, pub decoy_pool_size: usize, pub last_domain_ok: Option, pub last_domain_checked: Option, pub last_ip_rotation_ts: Option, pub ua_hint: String, } /// Return a plausible randomized desktop/mobile User-Agent string. pub fn random_user_agent() -> String { let pool = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_6_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Mozilla/5.0 (iPhone; CPU iPhone OS 16_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.7 Mobile/15E148 Safari/604.1", "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36", ]; let mut rng = rand::thread_rng(); pool.choose(&mut rng).unwrap().to_string() }