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();
}
if let Some(i) = g
.ifaces
.iter()
.find(|i| !i.ip.is_loopback() && u32::from(i.netmask) == 0)
{
return Some(i.clone());
}
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
}
#[cfg(target_os = "linux")]
fn interface_up_flags() -> std::collections::HashMap<String, bool> {
use std::collections::HashMap;
use std::ffi::CStr;
let mut map: HashMap<String, bool> = HashMap::new();
unsafe {
let mut head: *mut libc::ifaddrs = std::ptr::null_mut();
if libc::getifaddrs(&mut head) != 0 || head.is_null() {
return map;
}
let mut cur = head;
while !cur.is_null() {
let ifa = &*cur;
if !ifa.ifa_name.is_null() {
if let Ok(name) = CStr::from_ptr(ifa.ifa_name).to_str() {
let flags = ifa.ifa_flags as libc::c_int;
let up = (flags & libc::IFF_UP) != 0;
let loopback = (flags & libc::IFF_LOOPBACK) != 0;
let eligible = up && !loopback;
map.entry(name.to_string())
.and_modify(|e| *e |= eligible)
.or_insert(eligible);
}
}
cur = ifa.ifa_next;
}
libc::freeifaddrs(head);
}
map
}
fn enumerate_v4() -> Vec<IfaceInfo> {
let Ok(list) = if_addrs::get_if_addrs() else {
return Vec::new();
};
#[cfg(target_os = "linux")]
let up_flags = interface_up_flags();
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);
#[cfg(target_os = "linux")]
let up_non_loopback = match up_flags.get(&iface.name) {
Some(&eligible) => eligible && !iface.is_loopback(),
None => !iface.is_loopback(),
};
#[cfg(not(target_os = "linux"))]
let up_non_loopback = !iface.is_loopback();
out.push(IfaceInfo {
index,
name: iface.name.clone(),
ip: v4.ip,
netmask: v4.netmask,
broadcast: v4.broadcast,
up_non_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 up_non_loopback_excludes_loopback() {
let map = IfaceMap::new();
for iface in map.all() {
if iface.ip.is_loopback() {
assert!(
!iface.up_non_loopback,
"loopback {iface:?} must not be a fanout target"
);
}
}
for iface in map.up_non_loopback() {
assert!(
!iface.ip.is_loopback(),
"up_non_loopback() must not surface loopback: {iface:?}"
);
}
}
#[cfg(target_os = "linux")]
#[test]
fn interface_up_flags_marks_loopback_ineligible() {
let flags = interface_up_flags();
let lo_ineligible = flags
.iter()
.any(|(name, &eligible)| (name == "lo" || name == "lo0") && !eligible);
assert!(
lo_ineligible || flags.is_empty(),
"loopback must be flagged ineligible in the IFF_UP map: {flags:?}"
);
}
#[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();
}
}