use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket};
use std::sync::Mutex;
use std::time::{Duration, Instant};
use crate::family::Family;
const CACHE_TTL: Duration = Duration::from_secs(300);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LocalStack {
has_v6: bool,
has_v4: bool,
}
impl LocalStack {
pub fn detect() -> LocalStack {
LocalStack {
has_v6: probe_v6(),
has_v4: probe_v4(),
}
}
pub fn cached() -> LocalStack {
static CACHE: Mutex<Option<(LocalStack, Instant)>> = Mutex::new(None);
let mut guard = CACHE.lock().unwrap_or_else(|e| e.into_inner());
if let Some((stack, at)) = *guard {
if at.elapsed() < CACHE_TTL {
return stack;
}
}
let fresh = LocalStack::detect();
*guard = Some((fresh, Instant::now()));
fresh
}
pub const fn from_flags(has_v6: bool, has_v4: bool) -> LocalStack {
LocalStack { has_v6, has_v4 }
}
pub fn has(&self, family: Family) -> bool {
match family {
Family::V6 => self.has_v6,
Family::V4 => self.has_v4,
}
}
pub fn families(&self) -> Vec<Family> {
Family::PREFERENCE
.into_iter()
.filter(|f| self.has(*f))
.collect()
}
}
fn probe_v6() -> bool {
let Ok(socket) = UdpSocket::bind((Ipv6Addr::UNSPECIFIED, 0)) else {
return false;
};
let probe = SocketAddr::new(IpAddr::V6("2001:db8::1".parse().unwrap()), 9);
socket.connect(probe).is_ok()
}
fn probe_v4() -> bool {
let Ok(socket) = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)) else {
return false;
};
let probe = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 1)), 9);
socket.connect(probe).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_flags_reports_capability() {
let dual = LocalStack::from_flags(true, true);
assert!(dual.has(Family::V6));
assert!(dual.has(Family::V4));
let v4_only = LocalStack::from_flags(false, true);
assert!(!v4_only.has(Family::V6));
assert!(v4_only.has(Family::V4));
}
#[test]
fn families_are_preference_ordered_and_present_only() {
assert_eq!(
LocalStack::from_flags(true, true).families(),
vec![Family::V6, Family::V4]
);
assert_eq!(
LocalStack::from_flags(false, true).families(),
vec![Family::V4]
);
assert_eq!(
LocalStack::from_flags(true, false).families(),
vec![Family::V6]
);
assert!(LocalStack::from_flags(false, false).families().is_empty());
}
#[test]
fn detect_and_cached_run_without_panicking() {
let _ = LocalStack::detect();
let a = LocalStack::cached();
let b = LocalStack::cached();
assert_eq!(a, b, "cached detection is stable within the TTL");
}
}