From 24592cd84c6325c0ac78cff0bce4e34705fb1a17 Mon Sep 17 00:00:00 2001 From: Jamie Smith Date: Wed, 5 Mar 2025 15:00:40 -0800 Subject: [PATCH 1/9] Initial Unix implementation --- src/interface/mod.rs | 33 ++++++++++++ src/interface/unix.rs | 123 ++++++++++++++++++------------------------ src/sys/unix.rs | 1 + 3 files changed, 86 insertions(+), 71 deletions(-) diff --git a/src/interface/mod.rs b/src/interface/mod.rs index ee799e3..1d886d1 100644 --- a/src/interface/mod.rs +++ b/src/interface/mod.rs @@ -68,6 +68,11 @@ pub struct Interface { pub ipv4: Vec, /// List of Ipv6Net 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 generally has to be provided when opening a IPv6 socket on this interface for link- + /// local communications. + 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 @@ -122,6 +127,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 +220,31 @@ 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| match iface.mac_addr { Some(mac) => crate::db::oui::is_known_loopback_mac(&mac), None => false}).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 has the same number of scope IDs as it does IPv6 addresses + assert_eq!(loopback.ipv6.len(), loopback.ipv6_scope_ids.len()); + } } diff --git a/src/interface/unix.rs b/src/interface/unix.rs index 2301f47..d3d74d9 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), } } } @@ -296,10 +298,10 @@ 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 +313,9 @@ 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 +328,57 @@ 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; @@ -400,16 +393,4 @@ 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); - } - } -} +} \ No newline at end of file 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 => { From 10fad3f3783f6d53267b2aa7fc49a7bae06f7811 Mon Sep 17 00:00:00 2001 From: Jamie Smith Date: Wed, 5 Mar 2025 16:38:59 -0800 Subject: [PATCH 2/9] Update example, update some docs --- examples/list_interfaces.rs | 6 ++++- src/interface/mod.rs | 32 +++++++++++++++-------- src/interface/windows.rs | 52 +++++++++++++++++++++++++------------ 3 files changed, 61 insertions(+), 29 deletions(-) diff --git a/examples/list_interfaces.rs b/examples/list_interfaces.rs index ee91d06..2bb6cbd 100644 --- a/examples/list_interfaces.rs +++ b/examples/list_interfaces.rs @@ -24,7 +24,11 @@ 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/mod.rs b/src/interface/mod.rs index 1d886d1..2be78c7 100644 --- a/src/interface/mod.rs +++ b/src/interface/mod.rs @@ -52,21 +52,27 @@ 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, @@ -75,15 +81,19 @@ pub struct Interface { 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, } diff --git a/src/interface/windows.rs b/src/interface/windows.rs index 258d065..75a4ef1 100644 --- a/src/interface/windows.rs +++ b/src/interface/windows.rs @@ -48,16 +48,29 @@ 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()?; +// 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) { - 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, - }) + 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 +211,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 +257,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 +272,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), From d3bd54c836f7473d3a0e87d2e08201402d0f4c9a Mon Sep 17 00:00:00 2001 From: Jamie Smith Date: Wed, 5 Mar 2025 17:21:07 -0800 Subject: [PATCH 3/9] Also test flags --- src/interface/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/interface/mod.rs b/src/interface/mod.rs index 2be78c7..f903a09 100644 --- a/src/interface/mod.rs +++ b/src/interface/mod.rs @@ -256,5 +256,8 @@ mod tests { // Make sure that the loopback has the same number of scope IDs as it does IPv6 addresses assert_eq!(loopback.ipv6.len(), loopback.ipv6_scope_ids.len()); + + assert!(loopback.is_running(), "Loopback interface should be running!"); + assert!(!loopback.is_physical(), "Loopback interface should not be physical!"); } } From c5cb6e16cb48a4eaafe0e2df37286934ecad4cfc Mon Sep 17 00:00:00 2001 From: Jamie Smith Date: Wed, 5 Mar 2025 17:24:32 -0800 Subject: [PATCH 4/9] Rev Checkout action --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca29d78..2670f01 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 From 1b2ad0d5fc7e2241b111bdca4d098240ed194e61 Mon Sep 17 00:00:00 2001 From: Jamie Smith Date: Wed, 5 Mar 2025 17:27:05 -0800 Subject: [PATCH 5/9] Run formatter --- examples/list_interfaces.rs | 7 +++++- src/interface/mod.rs | 48 ++++++++++++++++++++++++++++++------- src/interface/unix.rs | 26 +++++++++++++------- src/interface/windows.rs | 33 ++++++++++++------------- 4 files changed, 78 insertions(+), 36 deletions(-) diff --git a/examples/list_interfaces.rs b/examples/list_interfaces.rs index 2bb6cbd..fad1bd1 100644 --- a/examples/list_interfaces.rs +++ b/examples/list_interfaces.rs @@ -26,7 +26,12 @@ fn main() { println!("\tIPv4: {:?}", interface.ipv4); // 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(); + 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); diff --git a/src/interface/mod.rs b/src/interface/mod.rs index f903a09..83f1dbc 100644 --- a/src/interface/mod.rs +++ b/src/interface/mod.rs @@ -238,26 +238,58 @@ mod tests { 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| match iface.mac_addr { Some(mac) => crate::db::oui::is_known_loopback_mac(&mac), None => false}).collect(); - assert_eq!(loopback_interfaces.len(), 1, "There should be exactly one loopback interface on the machine"); + let loopback_interfaces: Vec<&Interface> = interfaces + .iter() + .filter(|iface| match iface.mac_addr { + Some(mac) => crate::db::oui::is_known_loopback_mac(&mac), + None => false, + }) + .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"); + 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"); + 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 has the same number of scope IDs as it does IPv6 addresses assert_eq!(loopback.ipv6.len(), loopback.ipv6_scope_ids.len()); - assert!(loopback.is_running(), "Loopback interface should be running!"); - assert!(!loopback.is_physical(), "Loopback interface should not be physical!"); + assert!( + loopback.is_running(), + "Loopback interface should be running!" + ); + assert!( + !loopback.is_physical(), + "Loopback interface should not be physical!" + ); } } diff --git a/src/interface/unix.rs b/src/interface/unix.rs index d3d74d9..a9f4848 100644 --- a/src/interface/unix.rs +++ b/src/interface/unix.rs @@ -298,7 +298,8 @@ 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, ipv6_scope_id) = sockaddr_to_network_addr(addr_ref.ifa_addr as *mut libc::sockaddr); + 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; @@ -313,9 +314,7 @@ fn unix_interfaces_inner( None => Ipv4Addr::UNSPECIFIED, }; match Ipv4Net::with_netmask(ipv4, netmask) { - Ok(ipv4_net) => { - ini_ipv4 = Some(ipv4_net) - }, + Ok(ipv4_net) => ini_ipv4 = Some(ipv4_net), Err(_) => {} } } @@ -333,7 +332,7 @@ fn unix_interfaces_inner( if ipv6_scope_id.is_none() { panic!("IPv6 address without scope ID!") } - }, + } Err(_) => {} }; } @@ -369,9 +368,18 @@ fn unix_interfaces_inner( 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![]}, + 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, @@ -393,4 +401,4 @@ fn unix_interfaces_inner( } } ifaces -} \ No newline at end of file +} diff --git a/src/interface/windows.rs b/src/interface/windows.rs index 75a4ef1..ccd7815 100644 --- a/src/interface/windows.rs +++ b/src/interface/windows.rs @@ -51,25 +51,22 @@ fn get_mac_through_arp(src_ip: Ipv4Addr, dst_ip: Ipv4Addr) -> MacAddr { // 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), + 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), + }, } } @@ -214,7 +211,7 @@ pub fn interfaces() -> Vec { let mut ipv6_scope_id_vec: Vec = vec![]; // Enumerate all IPs for cur_a in unsafe { linked_list_iter!(&cur.FirstUnicastAddress) } { - let (ip_addr, ipv6_scope_id) = unsafe { socket_address_to_ipaddr(&cur_a.Address)}; + let (ip_addr, ipv6_scope_id) = unsafe { socket_address_to_ipaddr(&cur_a.Address) }; let prefix_len = cur_a.OnLinkPrefixLength; match ip_addr { @@ -226,7 +223,7 @@ pub fn interfaces() -> Vec { Ok(ipv6_net) => { ipv6_vec.push(ipv6_net); ipv6_scope_id_vec.push(ipv6_scope_id.unwrap()); - }, + } Err(_) => {} }, None => {} From 5233377986db821f1b4b6eb93ca64081b49d80d3 Mon Sep 17 00:00:00 2001 From: Jamie Smith Date: Wed, 5 Mar 2025 17:30:20 -0800 Subject: [PATCH 6/9] Add test, try and fix mac build --- .github/workflows/ci.yml | 7 +++++-- src/interface/unix.rs | 14 +++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2670f01..c085f27 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,8 +36,11 @@ jobs: - uses: swatinem/rust-cache@v2 - - name: Run cargo check - run: cargo check + - name: Run cargo build + run: cargo build + + - name: Run cargo test + run: cargo test cross: name: Cross compile diff --git a/src/interface/unix.rs b/src/interface/unix.rs index a9f4848..8cd2345 100644 --- a/src/interface/unix.rs +++ b/src/interface/unix.rs @@ -184,12 +184,12 @@ 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]; @@ -206,17 +206,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), } } } From d8d82108511d5602bda56cc2460335fb808c2e37 Mon Sep 17 00:00:00 2001 From: Jamie Smith Date: Wed, 5 Mar 2025 17:59:07 -0800 Subject: [PATCH 7/9] Try and fix Android build --- .github/workflows/ci.yml | 3 +++ src/interface/android.rs | 10 +++++++++- src/interface/mod.rs | 8 +++++--- src/interface/unix.rs | 4 +++- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c085f27..a1acedb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,9 @@ jobs: - uses: swatinem/rust-cache@v2 + - name: "Display network interfaces on machine (for test failure debugging)" + run: cargo run --example list_interfaces + - name: Run cargo build run: cargo build 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 83f1dbc..608edf7 100644 --- a/src/interface/mod.rs +++ b/src/interface/mod.rs @@ -74,10 +74,12 @@ pub struct Interface { pub ipv4: Vec, /// 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. + /// 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 generally has to be provided when opening a IPv6 socket on this interface for link- - /// local communications. + /// 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, diff --git a/src/interface/unix.rs b/src/interface/unix.rs index 8cd2345..dce5269 100644 --- a/src/interface/unix.rs +++ b/src/interface/unix.rs @@ -184,7 +184,9 @@ pub(super) fn sockaddr_to_network_addr( target_os = "macos", target_os = "ios" ))] -fn sockaddr_to_network_addr(sa: *mut libc::sockaddr) -> (Option, Option, Option) { +fn sockaddr_to_network_addr( + sa: *mut libc::sockaddr, +) -> (Option, Option, Option) { use std::net::SocketAddr; unsafe { From 7fec8580adef99f969050f9120b9995175367975 Mon Sep 17 00:00:00 2001 From: Jamie Smith Date: Wed, 5 Mar 2025 18:16:28 -0800 Subject: [PATCH 8/9] What if we don't use the cache? --- .github/workflows/ci.yml | 2 -- src/interface/mod.rs | 14 +++++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1acedb..1fe6781 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,8 +65,6 @@ jobs: - name: Install Rust Toolchain run: rustup toolchain install stable --profile minimal --no-self-update - - uses: swatinem/rust-cache@v2 - - name: Install cross run: cargo install cross diff --git a/src/interface/mod.rs b/src/interface/mod.rs index 608edf7..bdf775c 100644 --- a/src/interface/mod.rs +++ b/src/interface/mod.rs @@ -242,10 +242,7 @@ mod tests { // Try and find the loopback interface let loopback_interfaces: Vec<&Interface> = interfaces .iter() - .filter(|iface| match iface.mac_addr { - Some(mac) => crate::db::oui::is_known_loopback_mac(&mac), - None => false, - }) + .filter(|iface| iface.if_type == InterfaceType::Loopback) .collect(); assert_eq!( loopback_interfaces.len(), @@ -282,9 +279,10 @@ mod tests { ); println!("Found IP {:?} on the loopback interface", matching_ipv6s[0]); - // Make sure that the loopback has the same number of scope IDs as it does IPv6 addresses + // 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!" @@ -293,5 +291,11 @@ mod tests { !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 => {} + } } } From 46d97a78414154b0c9c495b3ac33a6a1add7c3cc Mon Sep 17 00:00:00 2001 From: Jamie Smith Date: Wed, 5 Mar 2025 18:19:36 -0800 Subject: [PATCH 9/9] Try workaround --- .github/workflows/ci.yml | 8 ++++++-- src/interface/mod.rs | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1fe6781..2e69a5c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,8 +65,12 @@ jobs: - name: Install Rust Toolchain run: rustup toolchain install stable --profile minimal --no-self-update + - uses: swatinem/rust-cache@v2 + - 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/src/interface/mod.rs b/src/interface/mod.rs index bdf775c..39982ca 100644 --- a/src/interface/mod.rs +++ b/src/interface/mod.rs @@ -294,7 +294,10 @@ mod tests { // 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"), + Some(mac) => assert!( + crate::db::oui::is_known_loopback_mac(&mac), + "Loopback interface MAC not a known loopback MAC" + ), None => {} } }