use super::InterfaceStats;
use anyhow::Result;
use std::collections::HashMap;
pub fn collect_interface_stats() -> Result<HashMap<String, InterfaceStats>> {
Ok(parse_iflist2(&sysctl_iflist2()?))
}
fn sysctl_iflist2() -> Result<Vec<u8>> {
let mut mib: [libc::c_int; 6] = [
libc::CTL_NET,
libc::AF_ROUTE,
0,
0, libc::NET_RT_IFLIST2,
0,
];
unsafe {
let mut len: libc::size_t = 0;
if libc::sysctl(
mib.as_mut_ptr(),
mib.len() as libc::c_uint,
std::ptr::null_mut(),
&mut len,
std::ptr::null_mut(),
0,
) != 0
{
return Err(std::io::Error::last_os_error().into());
}
let mut buf = vec![0u8; len];
if libc::sysctl(
mib.as_mut_ptr(),
mib.len() as libc::c_uint,
buf.as_mut_ptr() as *mut libc::c_void,
&mut len,
std::ptr::null_mut(),
0,
) != 0
{
return Err(std::io::Error::last_os_error().into());
}
buf.truncate(len);
Ok(buf)
}
}
fn parse_iflist2(buf: &[u8]) -> HashMap<String, InterfaceStats> {
let mut stats = HashMap::new();
let hdr_size = std::mem::size_of::<libc::if_msghdr2>();
let mut off = 0usize;
while off + 4 <= buf.len() {
let msglen = u16::from_ne_bytes([buf[off], buf[off + 1]]) as usize;
if msglen < 4 || off + msglen > buf.len() {
break;
}
let mtype = buf[off + 3] as libc::c_int;
if mtype == libc::RTM_IFINFO2 && msglen >= hdr_size {
let hdr: libc::if_msghdr2 = unsafe {
std::ptr::read_unaligned(buf.as_ptr().add(off) as *const libc::if_msghdr2)
};
if let Some(name) = sockaddr_dl_name(&buf[off + hdr_size..off + msglen]) {
if name != "lo0" {
let d = &hdr.ifm_data;
let is_up = (hdr.ifm_flags & libc::IFF_UP) != 0
&& (hdr.ifm_flags & libc::IFF_RUNNING) != 0;
stats.entry(name.clone()).or_insert(InterfaceStats {
name,
rx_bytes: d.ifi_ibytes,
tx_bytes: d.ifi_obytes,
rx_packets: d.ifi_ipackets,
tx_packets: d.ifi_opackets,
rx_errors: d.ifi_ierrors,
tx_errors: d.ifi_oerrors,
rx_drops: d.ifi_iqdrops,
tx_drops: 0, is_up,
});
}
}
}
off += msglen;
}
stats
}
fn sockaddr_dl_name(bytes: &[u8]) -> Option<String> {
const SDL_NLEN_OFF: usize = 5;
const SDL_DATA_OFF: usize = 8;
let nlen = *bytes.get(SDL_NLEN_OFF)? as usize;
let name = bytes.get(SDL_DATA_OFF..SDL_DATA_OFF + nlen)?;
Some(String::from_utf8_lossy(name).into_owned())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sockaddr_dl_name_reads_exactly_nlen_bytes() {
let mut b = vec![0u8; 8];
b[5] = 3; b[6] = 6; b.extend_from_slice(b"en0");
b.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef, 0x00, 0x01]);
assert_eq!(sockaddr_dl_name(&b).as_deref(), Some("en0"));
}
#[test]
fn sockaddr_dl_name_handles_truncated_buffer() {
assert_eq!(sockaddr_dl_name(&[0u8; 4]), None);
let mut b = vec![0u8; 8];
b[5] = 5;
b.extend_from_slice(b"ab");
assert_eq!(sockaddr_dl_name(&b), None);
}
#[test]
fn collect_interface_stats_returns_real_interfaces_no_fork() {
let stats = collect_interface_stats().expect("sysctl NET_RT_IFLIST2 must succeed");
assert!(!stats.is_empty(), "expected at least one interface");
for (name, s) in &stats {
assert_eq!(&s.name, name, "map key and stats.name must agree");
assert_ne!(name, "lo0", "loopback must be filtered");
}
}
}