diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca29d78..2e69a5c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: name: Rustfmt runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install Rust Toolchain run: rustup toolchain install stable --profile minimal --no-self-update @@ -36,8 +36,14 @@ jobs: - uses: swatinem/rust-cache@v2 - - name: Run cargo check - run: cargo check + - name: "Display network interfaces on machine (for test failure debugging)" + run: cargo run --example list_interfaces + + - name: Run cargo build + run: cargo build + + - name: Run cargo test + run: cargo test cross: name: Cross compile @@ -64,5 +70,7 @@ jobs: - name: Install cross run: cargo install cross - - name: Rust check - run: cross check --target ${{ matrix.target }} + - name: Cross Compile Check + run: | + export CARGO_TARGET_DIR=target/build/${{ matrix.target }} + cross check --target ${{ matrix.target }} diff --git a/examples/list_interfaces.rs b/examples/list_interfaces.rs index ee91d06..fad1bd1 100644 --- a/examples/list_interfaces.rs +++ b/examples/list_interfaces.rs @@ -24,7 +24,16 @@ fn main() { println!("\tMAC Address: (Failed to get mac address)"); } println!("\tIPv4: {:?}", interface.ipv4); - println!("\tIPv6: {:?}", interface.ipv6); + + // Print the IPv6 addresses with the scope ID after them as a suffix + let ipv6_strs: Vec = interface + .ipv6 + .iter() + .zip(interface.ipv6_scope_ids) + .map(|(ipv6, scope_id)| format!("{:?}%{}", ipv6, scope_id)) + .collect(); + println!("\tIPv6: [{}]", ipv6_strs.join(", ")); + println!("\tTransmit Speed: {:?}", interface.transmit_speed); println!("\tReceive Speed: {:?}", interface.receive_speed); if let Some(gateway) = interface.gateway { diff --git a/src/interface/android.rs b/src/interface/android.rs index 8e1a4c4..b439471 100644 --- a/src/interface/android.rs +++ b/src/interface/android.rs @@ -101,6 +101,7 @@ pub mod netlink { mac_addr: None, ipv4: Vec::new(), ipv6: Vec::new(), + ipv6_scope_ids: Vec::new(), flags: link_msg.header.flags.bits(), transmit_speed: None, receive_speed: None, @@ -152,7 +153,14 @@ pub mod netlink { Err(_) => {} }, IpAddr::V6(ip) => match Ipv6Net::new(ip, addr_msg.header.prefix_len) { - Ok(ipv6) => interface.ipv6.push(ipv6), + Ok(ipv6) => { + interface.ipv6.push(ipv6); + + // Note: On Unix platforms the scope ID is equal to the interface index, or at least + // that's what the glibc devs seemed to think when implementing getifaddrs! + // https://github.com/lattera/glibc/blob/895ef79e04a953cac1493863bcae29ad85657ee1/sysdeps/unix/sysv/linux/ifaddrs.c#L621 + interface.ipv6_scope_ids.push(interface.index); + } Err(_) => {} }, } diff --git a/src/interface/mod.rs b/src/interface/mod.rs index ee799e3..39982ca 100644 --- a/src/interface/mod.rs +++ b/src/interface/mod.rs @@ -52,33 +52,50 @@ use std::net::IpAddr; #[derive(Clone, Eq, PartialEq, Hash, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Interface { - /// Index of network interface + /// Index of network interface. This is an integer which uniquely identifies the interface + /// on this machine. pub index: u32, - /// Name of network interface + /// Machine-readable name of the network interface. On unix-like OSs, this is the interface + /// name, like 'eth0' or 'eno1'. On Windows, this is the interface's GUID as a string. pub name: String, - /// Friendly Name of network interface + /// Friendly name of network interface. On Windows, this is the network adapter configured + /// name, e.g. "Ethernet 5" or "Wi-Fi". On Mac, this is the interface display name, + /// such as "Ethernet" or "FireWire". If no friendly name is available, this is left as None. pub friendly_name: Option, - /// Description of the network interface + /// Description of the network interface. On Windows, this is the network adapter model, such + /// as "Realtek USB GbE Family Controller #4" or "Software Loopback Interface 1". Currently + /// this is not available on platforms other than Windows. pub description: Option, /// Interface Type pub if_type: InterfaceType, /// MAC address of network interface pub mac_addr: Option, - /// List of Ipv4Net for the network interface + /// List of Ipv4Nets (IPv4 address + netmask) for the network interface pub ipv4: Vec, - /// List of Ipv6Net for the network interface + /// List of Ipv6Nets (IPv6 address + netmask) for the network interface pub ipv6: Vec, + /// List of IPv6 Scope IDs for each of the corresponding elements in the ipv6 address vector. + /// The Scope ID is an integer which uniquely identifies this interface address on the system, + /// and must be provided when using link-local addressing to specify which interface + /// you wish to use. The scope ID can be the same as the interface index, but is not + /// required to be by the standard. + /// The scope ID can also be referred to as the zone index. + pub ipv6_scope_ids: Vec, /// Flags for the network interface (OS Specific) pub flags: u32, - /// Speed in bits per second of the transmit for the network interface + /// Speed in bits per second of the transmit for the network interface, if known. + /// Currently only supported on Linux, Android, and Windows. pub transmit_speed: Option, - /// Speed in bits per second of the receive for the network interface + /// Speed in bits per second of the receive for the network interface. + /// Currently only supported on Linux, Android, and Windows. pub receive_speed: Option, - /// Default gateway for the network interface + /// Default gateway for the network interface. This is the address of the router to which + /// IP packets are forwarded when they need to be sent to a device outside + /// of the local network. pub gateway: Option, - /// DNS servers for the network interface + /// DNS server addresses for the network interface pub dns_servers: Vec, - /// is default interface + /// Whether this is the default interface for accessing the Internet. pub default: bool, } @@ -122,6 +139,7 @@ impl Interface { mac_addr: None, ipv4: Vec::new(), ipv6: Vec::new(), + ipv6_scope_ids: Vec::new(), flags: 0, transmit_speed: None, receive_speed: None, @@ -214,4 +232,73 @@ mod tests { fn test_default_interface() { println!("{:#?}", get_default_interface()); } + + #[test] + fn sanity_check_loopback() { + let interfaces = get_interfaces(); + + assert!(interfaces.len() >= 2, "There should be at least 2 network interfaces on any machine, the loopback and one other one"); + + // Try and find the loopback interface + let loopback_interfaces: Vec<&Interface> = interfaces + .iter() + .filter(|iface| iface.if_type == InterfaceType::Loopback) + .collect(); + assert_eq!( + loopback_interfaces.len(), + 1, + "There should be exactly one loopback interface on the machine" + ); + let loopback = loopback_interfaces[0]; + + // Make sure that 127.0.0.1 is one of loopback's IPv4 addresses + let loopback_expected_ipv4: std::net::Ipv4Addr = "127.0.0.1".parse().unwrap(); + let matching_ipv4s: Vec<&Ipv4Net> = loopback + .ipv4 + .iter() + .filter(|&ipv4_net| ipv4_net.addr() == loopback_expected_ipv4) + .collect(); + assert_eq!( + matching_ipv4s.len(), + 1, + "The loopback interface should have IP 127.0.0.1" + ); + println!("Found IP {:?} on the loopback interface", matching_ipv4s[0]); + + // Make sure that ::1 is one of loopback's IPv6 addresses + let loopback_expected_ipv6: std::net::Ipv6Addr = "::1".parse().unwrap(); + let matching_ipv6s: Vec<&Ipv6Net> = loopback + .ipv6 + .iter() + .filter(|&ipv6_net| ipv6_net.addr() == loopback_expected_ipv6) + .collect(); + assert_eq!( + matching_ipv6s.len(), + 1, + "The loopback interface should have IP ::1" + ); + println!("Found IP {:?} on the loopback interface", matching_ipv6s[0]); + + // Make sure that the loopback interface has the same number of scope IDs as it does IPv6 addresses + assert_eq!(loopback.ipv6.len(), loopback.ipv6_scope_ids.len()); + + // Check flags + assert!( + loopback.is_running(), + "Loopback interface should be running!" + ); + assert!( + !loopback.is_physical(), + "Loopback interface should not be physical!" + ); + + // Make sure that, if the loopback interface has a MAC, it has a known loopback MAC + match loopback.mac_addr { + Some(mac) => assert!( + crate::db::oui::is_known_loopback_mac(&mac), + "Loopback interface MAC not a known loopback MAC" + ), + None => {} + } + } } diff --git a/src/interface/unix.rs b/src/interface/unix.rs index 2301f47..dce5269 100644 --- a/src/interface/unix.rs +++ b/src/interface/unix.rs @@ -141,15 +141,17 @@ pub fn interfaces() -> Vec { interfaces } +// Convert a socket address struct into a Rust IP address or MAC address struct. +// If the socket address is an IPv6 address, also returns the scope ID. #[cfg(any(target_os = "linux", target_os = "android"))] pub(super) fn sockaddr_to_network_addr( sa: *mut libc::sockaddr, -) -> (Option, Option) { +) -> (Option, Option, Option) { use std::net::SocketAddr; unsafe { if sa.is_null() { - (None, None) + (None, None, None) } else if (*sa).sa_family as libc::c_int == libc::AF_PACKET { let sll: *const libc::sockaddr_ll = mem::transmute(sa); let mac = MacAddr( @@ -161,15 +163,15 @@ pub(super) fn sockaddr_to_network_addr( (*sll).sll_addr[5], ); - (Some(mac), None) + (Some(mac), None, None) } else { let addr = sys::sockaddr_to_addr(mem::transmute(sa), mem::size_of::()); match addr { - Ok(SocketAddr::V4(sa)) => (None, Some(IpAddr::V4(*sa.ip()))), - Ok(SocketAddr::V6(sa)) => (None, Some(IpAddr::V6(*sa.ip()))), - Err(_) => (None, None), + Ok(SocketAddr::V4(sa)) => (None, Some(IpAddr::V4(*sa.ip())), None), + Ok(SocketAddr::V6(sa)) => (None, Some(IpAddr::V6(*sa.ip())), Some(sa.scope_id())), + Err(_) => (None, None, None), } } } @@ -182,12 +184,14 @@ pub(super) fn sockaddr_to_network_addr( target_os = "macos", target_os = "ios" ))] -fn sockaddr_to_network_addr(sa: *mut libc::sockaddr) -> (Option, Option) { +fn sockaddr_to_network_addr( + sa: *mut libc::sockaddr, +) -> (Option, Option, Option) { use std::net::SocketAddr; unsafe { if sa.is_null() { - (None, None) + (None, None, None) } else if (*sa).sa_family as libc::c_int == libc::AF_LINK { let nlen: i8 = (*sa).sa_data[3]; let alen: i8 = (*sa).sa_data[4]; @@ -204,17 +208,17 @@ fn sockaddr_to_network_addr(sa: *mut libc::sockaddr) -> (Option, Option extended[6 + nlen as usize + 4] as u8, extended[6 + nlen as usize + 5] as u8, ); - return (Some(mac), None); + return (Some(mac), None, None); } - (None, None) + (None, None, None) } else { let addr = sys::sockaddr_to_addr(mem::transmute(sa), mem::size_of::()); match addr { - Ok(SocketAddr::V4(sa)) => (None, Some(IpAddr::V4(*sa.ip()))), - Ok(SocketAddr::V6(sa)) => (None, Some(IpAddr::V6(*sa.ip()))), - Err(_) => (None, None), + Ok(SocketAddr::V4(sa)) => (None, Some(IpAddr::V4(*sa.ip())), None), + Ok(SocketAddr::V6(sa)) => (None, Some(IpAddr::V6(*sa.ip())), Some(sa.scope_id())), + Err(_) => (None, None, None), } } } @@ -296,10 +300,11 @@ fn unix_interfaces_inner( let c_str = addr_ref.ifa_name as *const c_char; let bytes = unsafe { CStr::from_ptr(c_str).to_bytes() }; let name = unsafe { from_utf8_unchecked(bytes).to_owned() }; - let (mac, ip) = sockaddr_to_network_addr(addr_ref.ifa_addr as *mut libc::sockaddr); - let (_, netmask) = sockaddr_to_network_addr(addr_ref.ifa_netmask as *mut libc::sockaddr); - let mut ini_ipv4: Vec = vec![]; - let mut ini_ipv6: Vec = vec![]; + let (mac, ip, ipv6_scope_id) = + sockaddr_to_network_addr(addr_ref.ifa_addr as *mut libc::sockaddr); + let (_, netmask, _) = sockaddr_to_network_addr(addr_ref.ifa_netmask as *mut libc::sockaddr); + let mut ini_ipv4: Option = None; + let mut ini_ipv6: Option = None; if let Some(ip) = ip { match ip { IpAddr::V4(ipv4) => { @@ -311,7 +316,7 @@ fn unix_interfaces_inner( None => Ipv4Addr::UNSPECIFIED, }; match Ipv4Net::with_netmask(ipv4, netmask) { - Ok(ipv4_net) => ini_ipv4.push(ipv4_net), + Ok(ipv4_net) => ini_ipv4 = Some(ipv4_net), Err(_) => {} } } @@ -324,68 +329,66 @@ fn unix_interfaces_inner( None => Ipv6Addr::UNSPECIFIED, }; match Ipv6Net::with_netmask(ipv6, netmask) { - Ok(ipv6_net) => ini_ipv6.push(ipv6_net), + Ok(ipv6_net) => { + ini_ipv6 = Some(ipv6_net); + if ipv6_scope_id.is_none() { + panic!("IPv6 address without scope ID!") + } + } Err(_) => {} - } + }; } } } - let interface: Interface = Interface { - index: 0, - name: name.clone(), - friendly_name: None, - description: None, - if_type: if_type, - mac_addr: mac.clone(), - ipv4: ini_ipv4, - ipv6: ini_ipv6, - flags: addr_ref.ifa_flags, - transmit_speed: None, - receive_speed: None, - gateway: None, - dns_servers: Vec::new(), - default: false, - }; + + // Check if there is already an interface with this name (since getifaddrs returns one + // entry per address, so if the interface has multiple addresses, it returns multiple entries). + // If so, add the IP addresses from the current entry into the existing interface. Otherwise, add a new interface. let mut found: bool = false; for iface in &mut ifaces { if name == iface.name { if let Some(mac) = mac.clone() { iface.mac_addr = Some(mac); } - if let Some(ip) = ip { - match ip { - IpAddr::V4(ipv4) => { - let netmask: Ipv4Addr = match netmask { - Some(netmask) => match netmask { - IpAddr::V4(netmask) => netmask, - IpAddr::V6(_) => Ipv4Addr::UNSPECIFIED, - }, - None => Ipv4Addr::UNSPECIFIED, - }; - match Ipv4Net::with_netmask(ipv4, netmask) { - Ok(ipv4_net) => iface.ipv4.push(ipv4_net), - Err(_) => {} - } - } - IpAddr::V6(ipv6) => { - let netmask: Ipv6Addr = match netmask { - Some(netmask) => match netmask { - IpAddr::V4(_) => Ipv6Addr::UNSPECIFIED, - IpAddr::V6(netmask) => netmask, - }, - None => Ipv6Addr::UNSPECIFIED, - }; - match Ipv6Net::with_netmask(ipv6, netmask) { - Ok(ipv6_net) => iface.ipv6.push(ipv6_net), - Err(_) => {} - } - } - } + + if ini_ipv4.is_some() { + iface.ipv4.push(ini_ipv4.unwrap()); + } + + if ini_ipv6.is_some() { + iface.ipv6.push(ini_ipv6.unwrap()); + iface.ipv6_scope_ids.push(ipv6_scope_id.unwrap()); } found = true; } } if !found { + let interface: Interface = Interface { + index: 0, // We will set these below + name: name.clone(), + friendly_name: None, + description: None, + if_type: if_type, + mac_addr: mac.clone(), + ipv4: match ini_ipv4 { + Some(ipv4_addr) => vec![ipv4_addr], + None => vec![], + }, + ipv6: match ini_ipv6 { + Some(ipv6_addr) => vec![ipv6_addr], + None => vec![], + }, + ipv6_scope_ids: match ini_ipv6 { + Some(_) => vec![ipv6_scope_id.unwrap()], + None => vec![], + }, + flags: addr_ref.ifa_flags, + transmit_speed: None, + receive_speed: None, + gateway: None, + dns_servers: Vec::new(), + default: false, + }; ifaces.push(interface); } addr = addr_ref.ifa_next; @@ -401,15 +404,3 @@ fn unix_interfaces_inner( } ifaces } - -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn test_unix_interfaces() { - let interfaces = interfaces(); - for interface in interfaces { - println!("{:#?}", interface); - } - } -} diff --git a/src/interface/windows.rs b/src/interface/windows.rs index 258d065..ccd7815 100644 --- a/src/interface/windows.rs +++ b/src/interface/windows.rs @@ -48,16 +48,26 @@ fn get_mac_through_arp(src_ip: Ipv4Addr, dst_ip: Ipv4Addr) -> MacAddr { } } -unsafe fn socket_address_to_ipaddr(addr: &SOCKET_ADDRESS) -> Option { - let sockaddr = addr.lpSockaddr.cast::().as_ref()?; - - Some(match sockaddr.si_family { - AF_INET => unsafe { sockaddr.Ipv4.sin_addr.S_un.S_addr } - .to_ne_bytes() - .into(), - AF_INET6 => unsafe { sockaddr.Ipv6.sin6_addr.u.Byte }.into(), - _ => return None, - }) +// Convert a socket address into a Rust IpAddr object and also a scope ID if it's an +// IPv6 address +unsafe fn socket_address_to_ipaddr(addr: &SOCKET_ADDRESS) -> (Option, Option) { + match addr.lpSockaddr.cast::().as_ref() { + None => (None, None), + Some(sockaddr) => match sockaddr.si_family { + AF_INET => { + let addr: IpAddr = unsafe { sockaddr.Ipv4.sin_addr.S_un.S_addr } + .to_ne_bytes() + .into(); + (Some(addr), None) + } + AF_INET6 => { + let addr: IpAddr = unsafe { sockaddr.Ipv6.sin6_addr.u.Byte }.into(); + let scope_id = sockaddr.Ipv6.Anonymous.sin6_scope_id; + (Some(addr), Some(scope_id)) + } + _ => (None, None), + }, + } } pub fn is_running(interface: &Interface) -> bool { @@ -198,26 +208,30 @@ pub fn interfaces() -> Vec { let mac_addr: MacAddr = MacAddr::from_octets(mac_addr_arr); let mut ipv4_vec: Vec = vec![]; let mut ipv6_vec: Vec = vec![]; + let mut ipv6_scope_id_vec: Vec = vec![]; // Enumerate all IPs for cur_a in unsafe { linked_list_iter!(&cur.FirstUnicastAddress) } { - let Some(ip_addr) = (unsafe { socket_address_to_ipaddr(&cur_a.Address) }) else { - continue; - }; + let (ip_addr, ipv6_scope_id) = unsafe { socket_address_to_ipaddr(&cur_a.Address) }; + let prefix_len = cur_a.OnLinkPrefixLength; match ip_addr { - IpAddr::V4(ipv4) => match Ipv4Net::new(ipv4, prefix_len) { + Some(IpAddr::V4(ipv4)) => match Ipv4Net::new(ipv4, prefix_len) { Ok(ipv4_net) => ipv4_vec.push(ipv4_net), Err(_) => {} }, - IpAddr::V6(ipv6) => match Ipv6Net::new(ipv6, prefix_len) { - Ok(ipv6_net) => ipv6_vec.push(ipv6_net), + Some(IpAddr::V6(ipv6)) => match Ipv6Net::new(ipv6, prefix_len) { + Ok(ipv6_net) => { + ipv6_vec.push(ipv6_net); + ipv6_scope_id_vec.push(ipv6_scope_id.unwrap()); + } Err(_) => {} }, + None => {} } } // Gateway let gateway_ips: Vec = unsafe { linked_list_iter!(&cur.FirstGatewayAddress) } - .filter_map(|cur_g| unsafe { socket_address_to_ipaddr(&cur_g.Address) }) + .filter_map(|cur_g| unsafe { socket_address_to_ipaddr(&cur_g.Address).0 }) .collect(); let mut default_gateway: NetworkDevice = NetworkDevice::new(); if flags & sys::IFF_UP != 0 { @@ -240,7 +254,7 @@ pub fn interfaces() -> Vec { } // DNS Servers let dns_servers: Vec = unsafe { linked_list_iter!(&cur.FirstDnsServerAddress) } - .filter_map(|cur_d| unsafe { socket_address_to_ipaddr(&cur_d.Address) }) + .filter_map(|cur_d| unsafe { socket_address_to_ipaddr(&cur_d.Address).0 }) .collect(); let default: bool = match local_ip { IpAddr::V4(local_ipv4) => ipv4_vec.iter().any(|x| x.addr() == local_ipv4), @@ -255,6 +269,7 @@ pub fn interfaces() -> Vec { mac_addr: Some(mac_addr), ipv4: ipv4_vec, ipv6: ipv6_vec, + ipv6_scope_ids: ipv6_scope_id_vec, flags, transmit_speed: Some(cur.TransmitLinkSpeed), receive_speed: Some(cur.ReceiveLinkSpeed), diff --git a/src/sys/unix.rs b/src/sys/unix.rs index 749a75b..bb30709 100644 --- a/src/sys/unix.rs +++ b/src/sys/unix.rs @@ -16,6 +16,7 @@ fn ntohs(u: u16) -> u16 { u16::from_be(u) } +// Converts libc socket address type to Rust SocketAddr struct pub fn sockaddr_to_addr(storage: &SockAddrStorage, len: usize) -> io::Result { match storage.ss_family as libc::c_int { AF_INET => {