use std::collections::{HashMap, VecDeque}; use std::net::{IpAddr, SocketAddr}; use std::sync::{Mutex, OnceLock}; use std::time::{SystemTime, UNIX_EPOCH}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum BndAddrStatus { Ok, Bogon, Error, } impl BndAddrStatus { pub(crate) fn as_str(self) -> &'static str { match self { Self::Ok => "ok", Self::Bogon => "bogon", Self::Error => "error", } } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum BndPortStatus { Ok, Zero, Error, } impl BndPortStatus { pub(crate) fn as_str(self) -> &'static str { match self { Self::Ok => "ok", Self::Zero => "zero", Self::Error => "error", } } } #[derive(Clone, Debug)] pub(crate) struct MeBndSnapshot { pub addr_status: &'static str, pub port_status: &'static str, pub last_addr: Option, pub last_seen_age_secs: Option, } #[derive(Clone, Debug)] pub(crate) struct MeUpstreamBndSnapshot { pub upstream_id: usize, pub addr_status: &'static str, pub port_status: &'static str, pub last_addr: Option, pub last_ip: Option, pub last_seen_age_secs: Option, } #[derive(Clone, Debug, Default)] pub(crate) struct MeTimeskewSnapshot { pub max_skew_secs_15m: Option, pub samples_15m: usize, pub last_skew_secs: Option, pub last_source: Option<&'static str>, pub last_seen_age_secs: Option, } #[derive(Clone, Copy, Debug)] struct MeTimeskewSample { ts_epoch_secs: u64, skew_secs: u64, source: &'static str, } #[derive(Debug)] struct MeSelftestState { bnd_addr_status: BndAddrStatus, bnd_port_status: BndPortStatus, bnd_last_addr: Option, bnd_last_seen_epoch_secs: Option, upstream_bnd: HashMap, timeskew_samples: VecDeque, } #[derive(Clone, Copy, Debug)] struct UpstreamBndState { addr_status: BndAddrStatus, port_status: BndPortStatus, last_addr: Option, last_ip: Option, last_seen_epoch_secs: Option, } impl Default for MeSelftestState { fn default() -> Self { Self { bnd_addr_status: BndAddrStatus::Error, bnd_port_status: BndPortStatus::Error, bnd_last_addr: None, bnd_last_seen_epoch_secs: None, upstream_bnd: HashMap::new(), timeskew_samples: VecDeque::new(), } } } const MAX_TIMESKEW_SAMPLES: usize = 512; const TIMESKEW_WINDOW_SECS: u64 = 15 * 60; static ME_SELFTEST_STATE: OnceLock> = OnceLock::new(); fn state() -> &'static Mutex { ME_SELFTEST_STATE.get_or_init(|| Mutex::new(MeSelftestState::default())) } pub(crate) fn record_bnd_status( addr_status: BndAddrStatus, port_status: BndPortStatus, last_addr: Option, ) { let now_epoch_secs = now_epoch_secs(); let Ok(mut guard) = state().lock() else { return; }; guard.bnd_addr_status = addr_status; guard.bnd_port_status = port_status; guard.bnd_last_addr = last_addr; guard.bnd_last_seen_epoch_secs = Some(now_epoch_secs); } pub(crate) fn bnd_snapshot() -> MeBndSnapshot { let now_epoch_secs = now_epoch_secs(); let Ok(guard) = state().lock() else { return MeBndSnapshot { addr_status: BndAddrStatus::Error.as_str(), port_status: BndPortStatus::Error.as_str(), last_addr: None, last_seen_age_secs: None, }; }; MeBndSnapshot { addr_status: guard.bnd_addr_status.as_str(), port_status: guard.bnd_port_status.as_str(), last_addr: guard.bnd_last_addr, last_seen_age_secs: guard .bnd_last_seen_epoch_secs .map(|value| now_epoch_secs.saturating_sub(value)), } } pub(crate) fn record_upstream_bnd_status( upstream_id: usize, addr_status: BndAddrStatus, port_status: BndPortStatus, last_addr: Option, last_ip: Option, ) { let now_epoch_secs = now_epoch_secs(); let Ok(mut guard) = state().lock() else { return; }; guard.upstream_bnd.insert( upstream_id, UpstreamBndState { addr_status, port_status, last_addr, last_ip, last_seen_epoch_secs: Some(now_epoch_secs), }, ); } pub(crate) fn upstream_bnd_snapshots() -> Vec { let now_epoch_secs = now_epoch_secs(); let Ok(guard) = state().lock() else { return Vec::new(); }; let mut out = Vec::with_capacity(guard.upstream_bnd.len()); for (upstream_id, entry) in &guard.upstream_bnd { out.push(MeUpstreamBndSnapshot { upstream_id: *upstream_id, addr_status: entry.addr_status.as_str(), port_status: entry.port_status.as_str(), last_addr: entry.last_addr, last_ip: entry.last_ip, last_seen_age_secs: entry .last_seen_epoch_secs .map(|value| now_epoch_secs.saturating_sub(value)), }); } out.sort_by_key(|entry| entry.upstream_id); out } pub(crate) fn record_timeskew_sample(source: &'static str, skew_secs: u64) { let now_epoch_secs = now_epoch_secs(); let Ok(mut guard) = state().lock() else { return; }; guard.timeskew_samples.push_back(MeTimeskewSample { ts_epoch_secs: now_epoch_secs, skew_secs, source, }); while guard.timeskew_samples.len() > MAX_TIMESKEW_SAMPLES { guard.timeskew_samples.pop_front(); } let cutoff = now_epoch_secs.saturating_sub(TIMESKEW_WINDOW_SECS * 2); while guard .timeskew_samples .front() .is_some_and(|sample| sample.ts_epoch_secs < cutoff) { guard.timeskew_samples.pop_front(); } } pub(crate) fn timeskew_snapshot() -> MeTimeskewSnapshot { let now_epoch_secs = now_epoch_secs(); let Ok(guard) = state().lock() else { return MeTimeskewSnapshot::default(); }; let mut max_skew_secs_15m = None; let mut samples_15m = 0usize; let window_start = now_epoch_secs.saturating_sub(TIMESKEW_WINDOW_SECS); for sample in &guard.timeskew_samples { if sample.ts_epoch_secs < window_start { continue; } samples_15m = samples_15m.saturating_add(1); max_skew_secs_15m = Some(max_skew_secs_15m.unwrap_or(0).max(sample.skew_secs)); } let (last_skew_secs, last_source, last_seen_age_secs) = if let Some(last) = guard.timeskew_samples.back() { ( Some(last.skew_secs), Some(last.source), Some(now_epoch_secs.saturating_sub(last.ts_epoch_secs)), ) } else { (None, None, None) }; MeTimeskewSnapshot { max_skew_secs_15m, samples_15m, last_skew_secs, last_source, last_seen_age_secs, } } fn now_epoch_secs() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs() }