netdev 0.47.0

Cross-platform library for enumerating network interfaces with metadata.
Documentation
use crate::net::device::NetworkDevice;
use crate::net::mac::MacAddr;
use std::collections::HashMap;
use std::fs::read_to_string;
use std::net::{Ipv4Addr, Ipv6Addr};

const PATH_PROC_NET_ROUTE: &str = "/proc/net/route";
const PATH_PROC_NET_IPV6_ROUTE: &str = "/proc/net/ipv6_route";
const PATH_PROC_NET_ARP: &str = "/proc/net/arp";

fn convert_hex_ipv4(hex_ip: &str) -> Option<Ipv4Addr> {
    if hex_ip.len() != 8 {
        return None;
    }
    // /proc/net/route prints the native-endian integer backing the IPv4 address.
    u32::from_str_radix(hex_ip, 16)
        .ok()
        .map(|value| Ipv4Addr::from(value.to_ne_bytes()))
}

fn convert_hex_ipv6(hex_ip: &str) -> Option<Ipv6Addr> {
    if hex_ip.len() != 32 {
        return None;
    }
    u128::from_str_radix(hex_ip, 16).ok().map(Ipv6Addr::from)
}

fn get_arp_map() -> HashMap<(String, Ipv4Addr), MacAddr> {
    read_to_string(PATH_PROC_NET_ARP)
        .map(|text| parse_arp_map(&text))
        .unwrap_or_default()
}

fn parse_arp_map(text: &str) -> HashMap<(String, Ipv4Addr), MacAddr> {
    let mut map = HashMap::new();
    for row in text.lines() {
        let fields: Vec<_> = row.split_whitespace().collect();
        if fields.len() < 6 {
            continue;
        }
        let Ok(flags) = u32::from_str_radix(fields[2].trim_start_matches("0x"), 16) else {
            continue;
        };
        if flags & 2 == 0 {
            continue;
        }
        if let Ok(ip) = fields[0].parse() {
            map.insert(
                (fields[5].to_owned(), ip),
                MacAddr::from_hex_format(fields[3]),
            );
        }
    }
    map
}

fn get_ipv4_gateway_map() -> HashMap<String, Ipv4Addr> {
    read_to_string(PATH_PROC_NET_ROUTE)
        .map(|text| parse_ipv4_gateway_map(&text))
        .unwrap_or_default()
}

fn parse_ipv4_gateway_map(text: &str) -> HashMap<String, Ipv4Addr> {
    let mut gateways = HashMap::new();
    for row in text.lines() {
        let fields: Vec<_> = row.split_whitespace().collect();
        if fields.len() < 8 || fields[1] != "00000000" || fields[7] != "00000000" {
            continue;
        }
        let Ok(flags) = u32::from_str_radix(fields[3], 16) else {
            continue;
        };
        // Require an active gateway route, excluding rejected routes.
        if flags & 3 != 3 || flags & 0x200 != 0 {
            continue;
        }
        if let Some(gateway) = convert_hex_ipv4(fields[2]).filter(|ip| !ip.is_unspecified()) {
            gateways.insert(fields[0].to_owned(), gateway);
        }
    }
    gateways
}

fn get_ipv6_gateway_map() -> HashMap<String, Ipv6Addr> {
    read_to_string(PATH_PROC_NET_IPV6_ROUTE)
        .map(|text| parse_ipv6_gateway_map(&text))
        .unwrap_or_default()
}

fn parse_ipv6_gateway_map(text: &str) -> HashMap<String, Ipv6Addr> {
    let mut gateways = HashMap::new();
    for row in text.lines() {
        let fields: Vec<_> = row.split_whitespace().collect();
        if fields.len() < 10 || fields[0] != "00000000000000000000000000000000" || fields[1] != "00"
        {
            continue;
        }
        let Ok(flags) = u32::from_str_radix(fields[8], 16) else {
            continue;
        };
        // Apply the same active gateway and reject checks as the IPv4 table.
        if flags & 3 != 3 || flags & 0x200 != 0 {
            continue;
        }
        if let Some(gateway) = convert_hex_ipv6(fields[4]).filter(|ip| !ip.is_unspecified()) {
            gateways.insert(fields[9].to_owned(), gateway);
        }
    }
    gateways
}

pub fn get_gateway_map() -> HashMap<String, NetworkDevice> {
    let mut gateway_map: HashMap<String, NetworkDevice> = HashMap::new();
    let mut arp_map = get_arp_map();
    for (if_name, ipv4_addr) in get_ipv4_gateway_map() {
        let mac = arp_map.remove(&(if_name.clone(), ipv4_addr));
        let gateway = gateway_map.entry(if_name).or_insert(NetworkDevice::new());
        if let Some(mac_addr) = mac {
            gateway.mac_addr = mac_addr;
        }
        gateway.ipv4.push(ipv4_addr);
    }
    for (if_name, ipv6_addr) in get_ipv6_gateway_map() {
        let gateway = gateway_map.entry(if_name).or_insert(NetworkDevice::new());
        gateway.ipv6.push(ipv6_addr);
    }
    gateway_map
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn arp_entries_are_scoped_to_their_interface() {
        let map = parse_arp_map(
            "192.168.1.1 0x1 0x2 00:01:02:03:04:05 * eth0\n192.168.1.1 0x1 0x2 06:07:08:09:0a:0b * eth1\n192.168.1.1 0x1 0x0 00:00:00:00:00:00 * eth2",
        );
        let ip = "192.168.1.1".parse().unwrap();
        assert_eq!(map.len(), 2);
        assert_eq!(
            map[&("eth0".into(), ip)],
            MacAddr::from_octets([0, 1, 2, 3, 4, 5])
        );
        assert_eq!(
            map[&("eth1".into(), ip)],
            MacAddr::from_octets([6, 7, 8, 9, 10, 11])
        );
    }

    #[test]
    fn ipv6_only_accepts_active_default_gateway_routes() {
        let zero = "00000000000000000000000000000000";
        let gateway = "fe800000000000000000000000000001";
        let rejected_gateway = "fe800000000000000000000000000002";
        let text = format!(
            "{zero} 00 {zero} 00 {gateway} 00000064 00000000 00000000 00000003 eth0\n             {zero} 00 {zero} 00 {rejected_gateway} 00000064 00000000 00000000 00000203 eth0\n             {zero} 00 {zero} 00 {gateway} 00000064 00000000 00000000 00000002 eth1\n             {zero} 40 {zero} 00 {gateway} 00000064 00000000 00000000 00000003 eth2\n             {zero} 00 {zero} 00 {zero} 00000064 00000000 00000000 00000003 eth3\n             {zero} 00 {zero} 00 invalid 00000064 00000000 00000000 00000003 eth4"
        );
        assert_eq!(
            parse_ipv6_gateway_map(&text),
            HashMap::from([("eth0".into(), "fe80::1".parse().unwrap())])
        );
    }

    #[test]
    fn ipv6_hex_parser_rejects_invalid_input() {
        assert_eq!(
            convert_hex_ipv6("fe800000000000000000000000000001"),
            Some("fe80::1".parse().unwrap())
        );
        assert_eq!(convert_hex_ipv6(&"z".repeat(32)), None);
        assert_eq!(convert_hex_ipv6(&"\u{3042}".repeat(10)), None);
    }

    #[test]
    fn only_accepts_active_default_gateway_routes() {
        let gateway = Ipv4Addr::new(192, 0, 2, 1);
        let hex = format!("{:08X}", u32::from_ne_bytes(gateway.octets()));
        let text = format!(
            "Iface Destination Gateway Flags RefCnt Use Metric Mask
eth0 00000000 {hex} 0003 0 0 100 00000000
eth0 0000000A 0100000A 0003 0 0 0 000000FF
eth1 00000000 {hex} 0002 0 0 0 00000000
eth2 00000000 {hex} 0203 0 0 0 00000000
eth3 00000000 00000000 0003 0 0 0 00000000
eth4 00000000 invalid! 0003 0 0 0 00000000
"
        );
        assert_eq!(
            parse_ipv4_gateway_map(&text),
            HashMap::from([("eth0".into(), gateway)])
        );
    }
}