use std::collections::HashMap;
use std::fmt;
use std::fs;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::str::FromStr;
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct IfIndex(pub u32);
impl fmt::Display for IfIndex {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct MacAddr(pub [u8; 6]);
impl MacAddr {
pub const BROADCAST: MacAddr = MacAddr([0xff; 6]);
pub const ZERO: MacAddr = MacAddr([0; 6]);
#[must_use]
pub const fn octets(&self) -> [u8; 6] {
self.0
}
#[must_use]
pub const fn is_broadcast(&self) -> bool {
matches!(self.0, [0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
}
#[must_use]
pub const fn is_multicast(&self) -> bool {
self.0[0] & 0x01 != 0
}
}
impl fmt::Display for MacAddr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let o = &self.0;
write!(
f,
"{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
o[0], o[1], o[2], o[3], o[4], o[5]
)
}
}
impl fmt::Debug for MacAddr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "MacAddr({self})")
}
}
impl FromStr for MacAddr {
type Err = ParseMacAddrError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let mut octets = [0u8; 6];
let mut parts = s.split([':', '-']);
for slot in &mut octets {
let part = parts.next().ok_or(ParseMacAddrError)?;
*slot = u8::from_str_radix(part, 16).map_err(|_| ParseMacAddrError)?;
}
if parts.next().is_some() {
return Err(ParseMacAddrError);
}
Ok(MacAddr(octets))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParseMacAddrError;
impl fmt::Display for ParseMacAddrError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("invalid MAC address")
}
}
impl std::error::Error for ParseMacAddrError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct IfAddr {
pub addr: IpAddr,
pub netmask: Option<IpAddr>,
pub scope_id: Option<u32>,
}
impl IfAddr {
#[must_use]
pub fn prefix_len(&self) -> Option<u8> {
match self.netmask? {
IpAddr::V4(m) => Some(u32::from(m).count_ones() as u8),
IpAddr::V6(m) => Some(u128::from(m).count_ones() as u8),
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Interface {
pub name: String,
pub index: IfIndex,
pub mac: Option<MacAddr>,
pub ips: Vec<IfAddr>,
pub flags: u32,
pub mtu: u32,
}
impl Interface {
#[must_use]
pub const fn is_up(&self) -> bool {
self.flags & (libc::IFF_UP as u32) != 0
}
#[must_use]
pub const fn is_loopback(&self) -> bool {
self.flags & (libc::IFF_LOOPBACK as u32) != 0
}
pub fn ip_addrs(&self) -> impl Iterator<Item = IpAddr> + '_ {
self.ips.iter().map(|a| a.addr)
}
}
pub fn index_of(name: &str) -> Result<IfIndex> {
let c_name = std::ffi::CString::new(name)
.map_err(|_| Error::InterfaceNotFound(name.to_owned()))?;
let idx = unsafe { libc::if_nametoindex(c_name.as_ptr()) };
if idx == 0 {
let err = std::io::Error::last_os_error();
match err.raw_os_error() {
Some(libc::ENODEV | libc::ENXIO) | None => {
Err(Error::InterfaceNotFound(name.to_owned()))
}
_ => Err(Error::Interfaces(err)),
}
} else {
Ok(IfIndex(idx))
}
}
pub fn interfaces() -> Result<Vec<Interface>> {
let mut addrs = interface_addrs()?;
let mut out = Vec::new();
for entry in fs::read_dir("/sys/class/net").map_err(Error::Interfaces)? {
let entry = entry.map_err(Error::Interfaces)?;
let name = entry.file_name().to_string_lossy().into_owned();
let base = entry.path();
let index = read_sysfs_u32(&base.join("ifindex")).map(IfIndex);
let Ok(index) = index else { continue };
let mac = fs::read_to_string(base.join("address"))
.ok()
.and_then(|s| s.trim().parse::<MacAddr>().ok())
.filter(|m| *m != MacAddr::ZERO);
let flags = read_sysfs_hex_u32(&base.join("flags")).unwrap_or(0);
let mtu = read_sysfs_u32(&base.join("mtu")).unwrap_or(0);
let ips = addrs.remove(&name).unwrap_or_default();
out.push(Interface { name, index, mac, ips, flags, mtu });
}
out.sort_by_key(|i| i.index);
Ok(out)
}
fn interface_addrs() -> Result<HashMap<String, Vec<IfAddr>>> {
let mut map: HashMap<String, Vec<IfAddr>> = HashMap::new();
let mut head: *mut libc::ifaddrs = std::ptr::null_mut();
if unsafe { libc::getifaddrs(&raw mut head) } != 0 {
return Err(Error::Interfaces(std::io::Error::last_os_error()));
}
let mut cur = head;
while !cur.is_null() {
let ifa = unsafe { &*cur };
cur = ifa.ifa_next;
if ifa.ifa_name.is_null() || ifa.ifa_addr.is_null() {
continue;
}
let name = unsafe { std::ffi::CStr::from_ptr(ifa.ifa_name) }.to_string_lossy().into_owned();
if let Some((addr, scope_id)) = sockaddr_to_ip(ifa.ifa_addr) {
let netmask = sockaddr_to_ip(ifa.ifa_netmask).map(|(ip, _)| ip);
map.entry(name).or_default().push(IfAddr { addr, netmask, scope_id });
}
}
unsafe { libc::freeifaddrs(head) };
Ok(map)
}
fn sockaddr_to_ip(sa: *const libc::sockaddr) -> Option<(IpAddr, Option<u32>)> {
if sa.is_null() {
return None;
}
let family = i32::from(unsafe { (*sa).sa_family });
match family {
libc::AF_INET => {
let sin = unsafe { &*sa.cast::<libc::sockaddr_in>() };
Some((IpAddr::V4(Ipv4Addr::from(u32::from_be(sin.sin_addr.s_addr))), None))
}
libc::AF_INET6 => {
let sin6 = unsafe { &*sa.cast::<libc::sockaddr_in6>() };
Some((IpAddr::V6(Ipv6Addr::from(sin6.sin6_addr.s6_addr)), Some(sin6.sin6_scope_id)))
}
_ => None,
}
}
fn read_sysfs_u32(path: &std::path::Path) -> std::io::Result<u32> {
let s = fs::read_to_string(path)?;
s.trim()
.parse::<u32>()
.map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "malformed sysfs integer"))
}
fn read_sysfs_hex_u32(path: &std::path::Path) -> std::io::Result<u32> {
let s = fs::read_to_string(path)?;
let s = s.trim();
let s = s.strip_prefix("0x").unwrap_or(s);
u32::from_str_radix(s, 16).map_err(|_| {
std::io::Error::new(std::io::ErrorKind::InvalidData, "malformed sysfs hex integer")
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mac_parse_roundtrip() {
let m: MacAddr = "01:23:45:67:89:ab".parse().unwrap();
assert_eq!(m.octets(), [0x01, 0x23, 0x45, 0x67, 0x89, 0xab]);
assert_eq!(m.to_string(), "01:23:45:67:89:ab");
}
#[test]
fn mac_parse_accepts_dashes() {
let m: MacAddr = "01-23-45-67-89-AB".parse().unwrap();
assert_eq!(m.octets(), [0x01, 0x23, 0x45, 0x67, 0x89, 0xab]);
}
#[test]
fn mac_parse_rejects_bad_input() {
assert!("01:23:45:67:89".parse::<MacAddr>().is_err());
assert!("01:23:45:67:89:ab:cd".parse::<MacAddr>().is_err());
assert!("zz:23:45:67:89:ab".parse::<MacAddr>().is_err());
}
#[test]
fn mac_flags() {
assert!(MacAddr::BROADCAST.is_broadcast());
assert!(MacAddr::BROADCAST.is_multicast());
assert!(!MacAddr([0x00, 0, 0, 0, 0, 0]).is_multicast());
assert!(MacAddr([0x01, 0, 0, 0, 0, 0]).is_multicast());
}
#[test]
#[cfg_attr(miri, ignore = "uses getifaddrs/sysfs; not interpretable by Miri")]
fn enumerate_has_loopback() {
let ifaces = interfaces().expect("enumerate interfaces");
assert!(ifaces.iter().any(|i| i.name == "lo" && i.is_loopback()));
}
#[test]
#[cfg_attr(miri, ignore = "uses getifaddrs/sysfs; not interpretable by Miri")]
fn ipv6_link_local_addresses_carry_scope_id() {
for iface in interfaces().expect("enumerate interfaces") {
for a in &iface.ips {
if let IpAddr::V6(v6) = a.addr {
if v6.is_unicast_link_local() {
assert_eq!(a.scope_id, Some(iface.index.0), "{}: {v6}", iface.name);
}
}
}
}
}
#[test]
#[cfg_attr(miri, ignore = "uses getifaddrs/sysfs; not interpretable by Miri")]
fn loopback_has_ip_addresses() {
let ifaces = interfaces().expect("enumerate interfaces");
let lo = ifaces.iter().find(|i| i.name == "lo").expect("lo present");
assert!(
lo.ip_addrs().any(|ip| ip == IpAddr::V4(Ipv4Addr::LOCALHOST)),
"lo addresses: {:?}",
lo.ips,
);
}
#[test]
fn prefix_len_from_netmask() {
let a = IfAddr {
addr: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 5)),
netmask: Some(IpAddr::V4(Ipv4Addr::new(255, 255, 255, 0))),
scope_id: None,
};
assert_eq!(a.prefix_len(), Some(24));
let b = IfAddr { addr: IpAddr::V4(Ipv4Addr::LOCALHOST), netmask: None, scope_id: None };
assert_eq!(b.prefix_len(), None);
}
}