pub mod bfd; pub mod config; pub mod control; pub mod events; use std::{ fmt::{self, Display}, net::IpAddr, sync::atomic::{AtomicU64, Ordering}, time::{Duration, Instant}, }; use log::debug; use nix::sys::socket::{getsockopt, setsockopt, sockopt}; use tokio::net::UdpSocket; use crate::bfd::*; /// A snapshot of the packet count and inter-packet interval statistics for one direction of a session. #[derive(Debug, Clone, Copy, Default)] pub struct IntervalStats { count: u64, min: Option, max: Option, mean: Option, } impl IntervalStats { pub fn count(&self) -> u64 { self.count } pub fn min(&self) -> Option { self.min } pub fn max(&self) -> Option { self.max } pub fn mean(&self) -> Option { self.mean } } impl Display for IntervalStats { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let ms = |d: Option| { d.map_or("-".to_string(), |d| { format!("{:.0}", d.as_secs_f64() * 1000.0) }) }; write!( f, "{} pkts, interval min/avg/max {}/{}/{} ms", self.count, ms(self.min), ms(self.mean), ms(self.max) ) } } /// An optional `Instant` that can be shared between threads, stored as nanoseconds since its construction. /// Instants earlier than the construction time are clamped to it. #[derive(Debug)] pub struct AtomicInstant { epoch: Instant, nanos: AtomicU64, } impl AtomicInstant { const NONE: u64 = u64::MAX; pub fn new() -> Self { Self { epoch: Instant::now(), nanos: AtomicU64::new(Self::NONE), } } pub fn load(&self) -> Option { match self.nanos.load(Ordering::Relaxed) { Self::NONE => None, n => Some(self.epoch + Duration::from_nanos(n)), } } pub fn store(&self, value: Instant) { self.swap(value); } pub fn swap(&self, value: Instant) -> Option { let n = u64::try_from(value.saturating_duration_since(self.epoch).as_nanos()) .unwrap_or(Self::NONE - 1) .min(Self::NONE - 1); match self.nanos.swap(n, Ordering::Relaxed) { Self::NONE => None, prev => Some(self.epoch + Duration::from_nanos(prev)), } } pub fn clear(&self) { self.nanos.store(Self::NONE, Ordering::Relaxed); } } impl Default for AtomicInstant { fn default() -> Self { Self::new() } } /// Lock-free accumulator for packet counts and inter-packet intervals. Updates are not atomic as a group, so it /// is intended for a single writer; a concurrent reader may observe a mix of old and new values. #[derive(Debug)] pub struct AtomicIntervalStats { count: AtomicU64, last: AtomicInstant, min_nanos: AtomicU64, max_nanos: AtomicU64, total_nanos: AtomicU64, } impl AtomicIntervalStats { pub fn new() -> Self { Self { count: AtomicU64::new(0), last: AtomicInstant::new(), min_nanos: AtomicU64::new(u64::MAX), max_nanos: AtomicU64::new(0), total_nanos: AtomicU64::new(0), } } pub fn record(&self, now: Instant) { self.count.fetch_add(1, Ordering::Relaxed); if let Some(prev) = self.last.swap(now) { let nanos = u64::try_from(now.saturating_duration_since(prev).as_nanos()).unwrap_or(u64::MAX); self.min_nanos.fetch_min(nanos, Ordering::Relaxed); self.max_nanos.fetch_max(nanos, Ordering::Relaxed); self.total_nanos.fetch_add(nanos, Ordering::Relaxed); } } pub fn reset(&self) { self.count.store(0, Ordering::Relaxed); self.last.clear(); self.min_nanos.store(u64::MAX, Ordering::Relaxed); self.max_nanos.store(0, Ordering::Relaxed); self.total_nanos.store(0, Ordering::Relaxed); } pub fn snapshot(&self) -> IntervalStats { let count = self.count.load(Ordering::Relaxed); let intervals = count.saturating_sub(1); if intervals == 0 { return IntervalStats { count, ..Default::default() }; } let total = self.total_nanos.load(Ordering::Relaxed); IntervalStats { count, min: Some(Duration::from_nanos(self.min_nanos.load(Ordering::Relaxed))), max: Some(Duration::from_nanos(self.max_nanos.load(Ordering::Relaxed))), mean: Some(Duration::from_nanos(total / intervals)), } } } impl Default for AtomicIntervalStats { fn default() -> Self { Self::new() } } fn fmt_ago(t: Option) -> String { t.map_or("never".to_string(), |t| { format!("{}s ago", t.elapsed().as_secs()) }) } #[derive(Debug)] pub struct BfdSessionStats { pub local_ip: IpAddr, pub remote_ip: IpAddr, pub local_discr: BfdDiscriminator, pub remote_discr: BfdDiscriminator, pub state: BfdState, pub last_diag: BfdDiagnostic, /// Packet statistics since the session last entered Up pub rx: IntervalStats, pub tx: IntervalStats, pub last_up: Option, pub last_down: Option, pub last_change: Instant, pub detect_time: BfdInterval, pub base_interval: BfdInterval, } impl BfdSessionStats { pub fn with_base_interval(mut self, base_interval: BfdInterval) -> Self { self.base_interval = base_interval; self } } impl Display for BfdSessionStats { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!( f, "{:<20} {:<9} {:>6} {:>6}", self.remote_ip, self.state, self.rx.count(), self.tx.count() )?; writeln!( f, " LocalDiscr:{} RemoteDiscr:{} DetectTime: {} TxTime: {} Diag:{} Last: {}s", self.local_discr, self.remote_discr, self.detect_time, self.base_interval, self.last_diag, Instant::now().duration_since(self.last_change).as_secs() )?; writeln!(f, " Rx: {}", self.rx)?; writeln!(f, " Tx: {}", self.tx)?; write!( f, " Last Up: {} Last Down: {}", fmt_ago(self.last_up), fmt_ago(self.last_down) ) } } pub fn set_ttl_or_hop_limit(sock: &UdpSocket, hop_limit: u8) -> Result<(), std::io::Error> { let addr = sock.local_addr()?; if addr.is_ipv4() { debug!("IPv4 socket, setting ttl {}", hop_limit as u32); sock.set_ttl(hop_limit as u32) } else if addr.is_ipv6() { debug!("IPv6 socket, setting hop limit {}", hop_limit as i32); let i32_hop_limit = hop_limit as i32; if let Err(e) = setsockopt(sock, sockopt::Ipv6Ttl, &i32_hop_limit) { Err(std::io::Error::from_raw_os_error(e as i32)) } else { let res = getsockopt(sock, sockopt::Ipv6Ttl)?; debug!("Got {} after setting {}", res, i32_hop_limit); Ok(()) } } else { panic!("UdpSocket with no address family ?!") } } #[cfg(test)] mod tests { use super::*; #[test] fn interval_stats_empty() { let s = AtomicIntervalStats::new().snapshot(); assert_eq!(s.count(), 0); assert_eq!((s.min(), s.max(), s.mean()), (None, None, None)); } #[test] fn interval_stats_single_packet_has_no_intervals() { let stats = AtomicIntervalStats::new(); stats.record(Instant::now()); let s = stats.snapshot(); assert_eq!(s.count(), 1); assert_eq!((s.min(), s.max(), s.mean()), (None, None, None)); } #[test] fn interval_stats_min_max_mean() { let stats = AtomicIntervalStats::new(); let t0 = Instant::now(); for ms in [0, 100, 400, 500] { stats.record(t0 + Duration::from_millis(ms)); } let s = stats.snapshot(); assert_eq!(s.count(), 4); assert_eq!(s.min(), Some(Duration::from_millis(100))); assert_eq!(s.max(), Some(Duration::from_millis(300))); assert_eq!(s.mean().unwrap().as_millis(), 166); } #[test] fn interval_stats_reset() { let stats = AtomicIntervalStats::new(); let t0 = Instant::now(); stats.record(t0); stats.record(t0 + Duration::from_millis(100)); stats.reset(); let s = stats.snapshot(); assert_eq!(s.count(), 0); assert_eq!((s.min(), s.max(), s.mean()), (None, None, None)); stats.record(t0 + Duration::from_millis(900)); stats.record(t0 + Duration::from_millis(1000)); assert_eq!(stats.snapshot().max(), Some(Duration::from_millis(100))); } #[test] fn atomic_instant_roundtrip() { let a = AtomicInstant::new(); assert_eq!(a.load(), None); let t = Instant::now() + Duration::from_secs(5); assert_eq!(a.swap(t), None); assert_eq!(a.load(), Some(t)); a.clear(); assert_eq!(a.load(), None); } }