use std::net::Ipv4Addr;
use std::sync::Arc;
use std::time::{Duration, Instant};
use parking_lot::Mutex;
#[derive(Debug, Clone)]
pub struct IfaceInfo {
pub index: u32,
pub name: String,
pub ip: Ipv4Addr,
pub netmask: Ipv4Addr,
pub broadcast: Option<Ipv4Addr>,
pub up_non_loopback: bool,
}
#[derive(Clone)]
pub struct IfaceMap {
inner: Arc<Mutex<Inner>>,
}
struct Inner {
ifaces: Vec<IfaceInfo>,
last_refresh: Instant,
}
impl IfaceMap {
pub fn new() -> Self {
let me = Self {
inner: Arc::new(Mutex::new(Inner {
ifaces: Vec::new(),
last_refresh: Instant::now() - Duration::from_secs(3600),
})),
};
me.refresh();
me
}
pub fn refresh(&self) {
let new = enumerate_v4();
let mut g = self.inner.lock();
g.ifaces = new;
g.last_refresh = Instant::now();
}
pub fn refresh_if_stale(&self, max_age: Duration) -> Duration {
let age = self.inner.lock().last_refresh.elapsed();
if age > max_age {
self.refresh();
}
age
}
pub fn spawn_refresh(&self, period: Duration) -> tokio::task::JoinHandle<()> {
let me = self.clone();
tokio::spawn(async move {
let mut tick = tokio::time::interval(period);
tick.tick().await;
loop {
tick.tick().await;
me.refresh();
}
})
}
pub fn all(&self) -> Vec<IfaceInfo> {
self.inner.lock().ifaces.clone()
}
pub fn up_non_loopback(&self) -> Vec<IfaceInfo> {
self.inner
.lock()
.ifaces
.iter()
.filter(|i| i.up_non_loopback)
.cloned()
.collect()
}
pub fn by_index(&self, index: u32) -> Option<IfaceInfo> {
self.inner
.lock()
.ifaces
.iter()
.find(|i| i.index == index)
.cloned()
}
pub fn route_to(&self, dest: Ipv4Addr) -> Option<IfaceInfo> {
let g = self.inner.lock();
for i in &g.ifaces {
if subnet_contains(i.ip, i.netmask, dest) {
return Some(i.clone());
}
}
for i in &g.ifaces {
if Some(dest) == i.broadcast {
return Some(i.clone());
}
}
if dest.is_loopback() {
return g.ifaces.iter().find(|i| i.ip.is_loopback()).cloned();
}
None
}
}
impl Default for IfaceMap {
fn default() -> Self {
Self::new()
}
}
fn subnet_contains(ip: Ipv4Addr, mask: Ipv4Addr, candidate: Ipv4Addr) -> bool {
let net = u32::from(ip) & u32::from(mask);
let cnet = u32::from(candidate) & u32::from(mask);
net == cnet && u32::from(mask) != 0
}
fn enumerate_v4() -> Vec<IfaceInfo> {
let Ok(list) = if_addrs::get_if_addrs() else {
return Vec::new();
};
let mut out = Vec::with_capacity(list.len());
for iface in list {
let if_addrs::IfAddr::V4(v4) = &iface.addr else {
continue;
};
let index = iface.index.unwrap_or(0);
out.push(IfaceInfo {
index,
name: iface.name.clone(),
ip: v4.ip,
netmask: v4.netmask,
broadcast: v4.broadcast,
up_non_loopback: !iface.is_loopback(),
});
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn enumerate_returns_loopback_at_minimum() {
let map = IfaceMap::new();
let all = map.all();
assert!(
all.iter().any(|i| i.ip.is_loopback()),
"loopback IPv4 interface should be present (got {all:?})"
);
}
#[test]
fn loopback_routing_lands_on_loopback() {
let map = IfaceMap::new();
let r = map.route_to(Ipv4Addr::LOCALHOST);
assert!(r.is_some(), "127.0.0.1 must route to a known interface");
assert!(r.unwrap().ip.is_loopback());
}
#[test]
fn refresh_updates_timestamp() {
let map = IfaceMap::new();
std::thread::sleep(Duration::from_millis(20));
let age = map.refresh_if_stale(Duration::from_millis(10));
assert!(
age >= Duration::from_millis(20),
"refresh_if_stale should report the pre-refresh age (got {age:?})"
);
}
#[test]
fn subnet_contains_basic() {
let ip = Ipv4Addr::new(10, 0, 0, 5);
let mask = Ipv4Addr::new(255, 255, 255, 0);
assert!(subnet_contains(ip, mask, Ipv4Addr::new(10, 0, 0, 99)));
assert!(!subnet_contains(ip, mask, Ipv4Addr::new(10, 0, 1, 1)));
}
#[test]
fn subnet_contains_zero_mask_rejects() {
assert!(!subnet_contains(
Ipv4Addr::UNSPECIFIED,
Ipv4Addr::UNSPECIFIED,
Ipv4Addr::new(8, 8, 8, 8)
));
}
#[tokio::test(flavor = "current_thread")]
async fn spawn_refresh_advances_last_refresh() {
let map = IfaceMap::new();
let initial = map.inner.lock().last_refresh;
let handle = map.spawn_refresh(Duration::from_millis(50));
tokio::time::sleep(Duration::from_millis(200)).await;
let after = map.inner.lock().last_refresh;
assert!(
after > initial,
"background refresh must update last_refresh"
);
handle.abort();
}
}