From 08c222af42926adb53249d59c361a7afe9b156d2 Mon Sep 17 00:00:00 2001 From: shellrow Date: Sun, 14 Dec 2025 21:58:39 +0900 Subject: [PATCH 1/3] perf(macos): update interface extras lookup --- Cargo.toml | 6 +- src/interface/types.rs | 3 + src/os/darwin/types.rs | 103 +++++++++++++++++++++++ src/os/macos/interface.rs | 49 +++++++---- src/os/macos/mod.rs | 2 +- src/os/macos/sc.rs | 168 ++++++++++++++++++++++++++++++++++++++ src/os/macos/types.rs | 44 ---------- 7 files changed, 311 insertions(+), 64 deletions(-) create mode 100644 src/os/macos/sc.rs delete mode 100644 src/os/macos/types.rs diff --git a/Cargo.toml b/Cargo.toml index 507816d..10afe76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,8 +36,10 @@ features = [ "Win32_NetworkManagement_Ndis", ] -[target.'cfg(target_os = "macos")'.dependencies] -system-configuration = "0.6" +[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] +objc2-core-foundation = "0.3" +objc2-system-configuration = { version = "0.3", features = ["SCNetworkConfiguration"] } +plist = "1.8" [dev-dependencies] serde_json = "1.0" diff --git a/src/interface/types.rs b/src/interface/types.rs index 5512def..6492850 100644 --- a/src/interface/types.rs +++ b/src/interface/types.rs @@ -69,6 +69,8 @@ pub enum InterfaceType { Bridge, /// Controller Area Network Can, + /// Peer-to-Peer Wireless (Wi-Fi Direct / AWDL) + PeerToPeerWireless, /// Unknown interface type with a specific value UnknownWithValue(u32), } @@ -198,6 +200,7 @@ impl InterfaceType { InterfaceType::Wwanpp => String::from("WWANPP"), InterfaceType::Wwanpp2 => String::from("WWANPP2"), InterfaceType::Can => String::from("CAN"), + InterfaceType::PeerToPeerWireless => String::from("Peer-to-Peer Wireless"), InterfaceType::UnknownWithValue(v) => format!("Unknown ({})", v), } } diff --git a/src/os/darwin/types.rs b/src/os/darwin/types.rs index fc32a49..d048acc 100644 --- a/src/os/darwin/types.rs +++ b/src/os/darwin/types.rs @@ -1,5 +1,7 @@ +use std::{ffi::CString, mem}; use crate::interface::types::InterfaceType; +/// Get the interface type from ifaddrs if_data.ifi_type field. pub fn get_interface_type(addr_ref: &libc::ifaddrs) -> InterfaceType { if !addr_ref.ifa_data.is_null() { let if_data = unsafe { &*(addr_ref.ifa_data as *const libc::if_data) }; @@ -8,3 +10,104 @@ pub fn get_interface_type(addr_ref: &libc::ifaddrs) -> InterfaceType { InterfaceType::Unknown } } + +// BSD ioctl encoding (ioccom.h) +const IOC_INOUT: u32 = 0xC000_0000; +const IOCPARM_MASK: u32 = 0x1fff; + +const fn ioc(inout: u32, group: u8, num: u8, len: u32) -> u32 { + inout | ((len & IOCPARM_MASK) << 16) | ((group as u32) << 8) | (num as u32) +} + +// #define SIOCGIFFUNCTIONALTYPE _IOWR('i', 173, struct ifreq) +fn siocgiffunctionaltype() -> u64 { + let len = mem::size_of::() as u32; + ioc(IOC_INOUT, b'i', 173, len) as u64 +} + +// Values from (IFRTYPE_FUNCTIONAL_*) +const IFRTYPE_FUNCTIONAL_UNKNOWN: u32 = 0; +const IFRTYPE_FUNCTIONAL_LOOPBACK: u32 = 1; +const IFRTYPE_FUNCTIONAL_WIRED: u32 = 2; +const IFRTYPE_FUNCTIONAL_WIFI_INFRA: u32 = 3; +const IFRTYPE_FUNCTIONAL_WIFI_AWDL: u32 = 4; +const IFRTYPE_FUNCTIONAL_CELLULAR: u32 = 5; +const IFRTYPE_FUNCTIONAL_INTCOPROC: u32 = 6; +const IFRTYPE_FUNCTIONAL_COMPANIONLINK: u32 = 7; + +fn map_to_interface_type(ft: u32) -> InterfaceType { + match ft { + IFRTYPE_FUNCTIONAL_UNKNOWN => InterfaceType::Unknown, + IFRTYPE_FUNCTIONAL_LOOPBACK => InterfaceType::Loopback, + IFRTYPE_FUNCTIONAL_WIRED => InterfaceType::Ethernet, + IFRTYPE_FUNCTIONAL_WIFI_INFRA => InterfaceType::Wireless80211, + IFRTYPE_FUNCTIONAL_WIFI_AWDL => InterfaceType::PeerToPeerWireless, + IFRTYPE_FUNCTIONAL_CELLULAR => InterfaceType::Wwanpp, + IFRTYPE_FUNCTIONAL_INTCOPROC => InterfaceType::Unknown, + IFRTYPE_FUNCTIONAL_COMPANIONLINK => InterfaceType::Unknown, + _ => InterfaceType::Unknown, + } +} + +/// Get the functional interface type using SIOCGIFFUNCTIONALTYPE ioctl. +pub(crate) fn get_functional_type(name: &str) -> InterfaceType { + // socket(AF_INET, SOCK_DGRAM, 0) + ioctl(SIOCGIFFUNCTIONALTYPE) + let fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0) }; + if fd < 0 { + return InterfaceType::Unknown; + } + + let c_name = match CString::new(name) { + Ok(v) => v, + Err(_) => { + unsafe { libc::close(fd) }; + return InterfaceType::Unknown; + } + }; + + // ifreq layout is platform-specific; libc::ifreq exists on macOS/iOS targets. + let mut ifr: libc::ifreq = unsafe { mem::zeroed() }; + + // ifr_name is a fixed-size [c_char; IFNAMSIZ] + // Copy with truncation; ensure NUL terminated + unsafe { + let dst = ifr.ifr_name.as_mut_ptr() as *mut u8; + let src = c_name.as_bytes_with_nul(); + let copy_len = std::cmp::min(ifr.ifr_name.len() - 1, src.len() - 1); + + std::ptr::copy_nonoverlapping(src.as_ptr(), dst, copy_len); + *dst.add(copy_len) = 0; + } + + let req = siocgiffunctionaltype(); + let ok = unsafe { libc::ioctl(fd, req, &mut ifr) } >= 0; + unsafe { libc::close(fd) }; + + if !ok { + return InterfaceType::Unknown; + } + + #[allow(clippy::unnecessary_cast)] + let type_id = unsafe { + ifr.ifr_ifru.ifru_functional_type as u32 + }; + + map_to_interface_type(type_id) + +} + +pub(crate) fn interface_type_by_name(name: &str) -> Option { + let n = name.as_bytes(); + + if n.starts_with(b"awdl") { + return Some(InterfaceType::PeerToPeerWireless); + } + if n.starts_with(b"utun") || n.starts_with(b"gif") || n.starts_with(b"stf") { + return Some(InterfaceType::Tunnel); + } + if n.starts_with(b"bridge") { + return Some(InterfaceType::Bridge); + } + + None +} diff --git a/src/os/macos/interface.rs b/src/os/macos/interface.rs index f2e28bd..b789024 100644 --- a/src/os/macos/interface.rs +++ b/src/os/macos/interface.rs @@ -1,27 +1,43 @@ use crate::{ - interface::{interface::Interface, types::InterfaceType}, - os::unix::interface::unix_interfaces, + interface::interface::Interface, + os::{macos::sc::SCInterface, unix::interface::unix_interfaces}, + prelude::InterfaceType, }; +use crate::os::macos::sc::{read_sc_interfaces_plist_map, get_sc_interface_map}; +use crate::os::darwin::types::{get_functional_type, interface_type_by_name}; +use std::collections::HashMap; -#[derive(Debug)] -pub struct SCInterface { - #[allow(dead_code)] - pub name: String, - pub friendly_name: Option, - pub interface_type: InterfaceType, -} - -pub fn interfaces() -> Vec { - let type_map = super::types::get_if_type_map(); +pub fn interfaces() -> Vec { let mut ifaces: Vec = unix_interfaces(); + let if_extra_map: HashMap = match read_sc_interfaces_plist_map() { + Ok(m) => m, + Err(_) => { + // Fallback to SCNetworkInterfaceCopyAll ... + get_sc_interface_map() + }, + }; + #[cfg(feature = "gateway")] let gateway_map = crate::os::darwin::route::get_gateway_map(); for iface in &mut ifaces { - if let Some(sc_interface) = type_map.get(&iface.name) { - iface.if_type = sc_interface.interface_type; - iface.friendly_name = sc_interface.friendly_name.clone(); + // If interface type is Ethernet, try to get a more accurate type + if iface.if_type == InterfaceType::Ethernet { + let ft: InterfaceType = get_functional_type(&iface.name); + if ft != InterfaceType::Unknown { + iface.if_type = ft; + } + } + if let Some(name_type) = interface_type_by_name(&iface.name) { + iface.if_type = name_type; + } + + if let Some(sc_inface) = if_extra_map.get(&iface.name) { + if let Some(sc_type) = sc_inface.if_type() { + iface.if_type = sc_type; + } + iface.friendly_name = sc_inface.friendly_name.clone(); } #[cfg(feature = "gateway")] @@ -34,12 +50,11 @@ pub fn interfaces() -> Vec { #[cfg(feature = "gateway")] { - use crate::os::unix::dns::get_system_dns_conf; if let Some(local_ip) = crate::net::ip::get_local_ipaddr() { if let Some(idx) = crate::interface::pick_default_iface_index(&ifaces, local_ip) { if let Some(iface) = ifaces.iter_mut().find(|it| it.index == idx) { iface.default = true; - iface.dns_servers = get_system_dns_conf(); + iface.dns_servers = crate::os::unix::dns::get_system_dns_conf(); } } } diff --git a/src/os/macos/mod.rs b/src/os/macos/mod.rs index 09a6797..8721393 100644 --- a/src/os/macos/mod.rs +++ b/src/os/macos/mod.rs @@ -1,2 +1,2 @@ pub mod interface; -pub mod types; +pub mod sc; diff --git a/src/os/macos/sc.rs b/src/os/macos/sc.rs new file mode 100644 index 0000000..37973c0 --- /dev/null +++ b/src/os/macos/sc.rs @@ -0,0 +1,168 @@ +//! SystemConfiguration::NetworkInterface-related types and functions for macOS. + +use std::collections::HashMap; +use std::io::Cursor; +use mac_addr::MacAddr; +use objc2_core_foundation::{CFArray, CFRetained}; +use objc2_system_configuration::SCNetworkInterface; +use crate::interface::types::InterfaceType; + +const SC_NWIF_PATH : &str = "/Library/Preferences/SystemConfiguration/NetworkInterfaces.plist"; + +#[derive(Clone, Debug, Default)] +pub(crate) struct SCInterface { + #[allow(dead_code)] + pub bsd_name: String, + #[allow(dead_code)] + pub mac: Option, + pub friendly_name: Option, + pub sc_type: Option, + #[allow(dead_code)] + pub active: Option, +} + +impl SCInterface { + pub fn if_type(&self) -> Option { + if let Some(sc_type) = &self.sc_type { + Some(map_sc_interface_type(sc_type)) + } else { + None + } + } +} + +fn map_sc_interface_type(type_id: &str) -> InterfaceType { + match type_id { + "Bridge" => InterfaceType::Bridge, + "Ethernet" => InterfaceType::Ethernet, + "IEEE80211" => InterfaceType::Wireless80211, + "Loopback" => InterfaceType::Loopback, + "Modem" => InterfaceType::GenericModem, + "PPP" => InterfaceType::Ppp, + "WWAN" => InterfaceType::Wwanpp, + _ => InterfaceType::Unknown, + } +} + +fn sc_network_interfaces_all() -> CFRetained> { + let untyped_ifaces: CFRetained = SCNetworkInterface::all(); + + // SAFETY: + // SCNetworkInterfaceCopyAll() returns a CFArray whose elements are SCNetworkInterfaceRef. + unsafe { + let raw = CFRetained::into_raw(untyped_ifaces); + CFRetained::from_raw(raw.cast()) + } +} + +/// Build a map of `BSD name -> SCInterface` using SystemConfiguration. +pub(crate) fn get_sc_interface_map() -> HashMap { + let mut if_map = HashMap::new(); + let sc_interfaces: CFRetained> = sc_network_interfaces_all(); + for sc_iface in sc_interfaces.iter() { + // Key by BSD interface name (e.g. "en0", "bridge0"). + let Some(bsd_name) = sc_iface.bsd_name() else { + continue; + }; + + let name = bsd_name.to_string(); + + let sc_if_type: Option = sc_iface + .interface_type() + .map(|s| s.to_string()); + + let friendly_name = sc_iface.localized_display_name().map(|s| s.to_string()); + + let mac: Option = sc_iface.hardware_address_string().and_then(|mac_str| { + Some(MacAddr::from_hex_format(&mac_str.to_string())) + }); + + if_map.insert( + name.clone(), + SCInterface { + bsd_name: name, + friendly_name, + sc_type: sc_if_type, + mac, + active: None, + }, + ); + } + if_map +} + +fn load_sc_interfaces_plist_map(bytes: &[u8]) -> HashMap { + let mut map = HashMap::new(); + + let v = match plist::Value::from_reader(Cursor::new(bytes)) { + Ok(v) => v, + Err(_) => return map, + }; + + let dict = match v.into_dictionary() { + Some(d) => d, + None => return map, + }; + + let interfaces = match dict.get("Interfaces").and_then(|v| v.as_array()) { + Some(a) => a, + None => return map, + }; + + for it in interfaces { + let d = match it.as_dictionary() { + Some(d) => d, + None => continue, + }; + + let bsd = match d.get("BSD Name").and_then(|v| v.as_string()) { + Some(s) if !s.is_empty() => s.to_string(), + _ => continue, + }; + + let friendly_name = d + .get("SCNetworkInterfaceInfo") + .and_then(|v| v.as_dictionary()) + .and_then(|info| info.get("UserDefinedName")) + .and_then(|v| v.as_string()) + .map(|s| s.to_string()); + + let sc_type = d + .get("SCNetworkInterfaceType") + .and_then(|v| v.as_string()) + .map(|s| s.to_string()); + + let active = d.get("Active").and_then(|v| v.as_boolean()); + + let mac = d.get("IOMACAddress") + .and_then(|v| v.as_data()) + .and_then(|data| { + if data.len() == 6 { + let mut a = [0u8; 6]; + a.copy_from_slice(data); + Some(MacAddr::from_octets(a)) + } else { + None + } + }); + + map.insert( + bsd.clone(), + SCInterface { + bsd_name: bsd, + friendly_name, + sc_type, + mac, + active, + }, + ); + } + + map +} + +/// Read and parse the NetworkInterfaces.plist file into a map of `BSD name -> SCInterface`. +pub(crate) fn read_sc_interfaces_plist_map() -> std::io::Result> { + let bytes = std::fs::read(SC_NWIF_PATH)?; + Ok(load_sc_interfaces_plist_map(&bytes)) +} diff --git a/src/os/macos/types.rs b/src/os/macos/types.rs deleted file mode 100644 index 1ffa56c..0000000 --- a/src/os/macos/types.rs +++ /dev/null @@ -1,44 +0,0 @@ -use crate::{interface::types::InterfaceType, os::macos::interface::SCInterface}; -use std::collections::HashMap; -use system_configuration::network_configuration; - -fn get_if_type_from_id(type_id: String) -> InterfaceType { - match type_id.as_str() { - "Bridge" => InterfaceType::Bridge, - "Ethernet" => InterfaceType::Ethernet, - "IEEE80211" => InterfaceType::Wireless80211, - "Loopback" => InterfaceType::Loopback, - "Modem" => InterfaceType::GenericModem, - "PPP" => InterfaceType::Ppp, - _ => InterfaceType::Unknown, - } -} - -pub fn get_if_type_map() -> HashMap { - let mut map: HashMap = HashMap::new(); - let interfaces = network_configuration::get_interfaces(); - for interface in &interfaces { - let if_name: String = if let Some(bsd_name) = interface.bsd_name() { - bsd_name.to_string() - } else { - continue; - }; - let type_id: String = if let Some(type_string) = interface.interface_type_string() { - type_string.to_string() - } else { - continue; - }; - let friendly_name: Option = if let Some(name) = interface.display_name() { - Some(name.to_string()) - } else { - None - }; - let sc_if = SCInterface { - name: if_name.clone(), - friendly_name: friendly_name, - interface_type: get_if_type_from_id(type_id), - }; - map.insert(if_name, sc_if); - } - return map; -} From 1a3c863a68a63a0a3ba1a2de415a3736cc5a993a Mon Sep 17 00:00:00 2001 From: shellrow Date: Sun, 14 Dec 2025 22:14:52 +0900 Subject: [PATCH 2/3] Format code with cargo fmt --- src/os/darwin/types.rs | 9 +++------ src/os/macos/interface.rs | 10 +++++----- src/os/macos/sc.rs | 23 +++++++++++------------ 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/os/darwin/types.rs b/src/os/darwin/types.rs index d048acc..d4b0985 100644 --- a/src/os/darwin/types.rs +++ b/src/os/darwin/types.rs @@ -1,5 +1,5 @@ -use std::{ffi::CString, mem}; use crate::interface::types::InterfaceType; +use std::{ffi::CString, mem}; /// Get the interface type from ifaddrs if_data.ifi_type field. pub fn get_interface_type(addr_ref: &libc::ifaddrs) -> InterfaceType { @@ -86,14 +86,11 @@ pub(crate) fn get_functional_type(name: &str) -> InterfaceType { if !ok { return InterfaceType::Unknown; } - + #[allow(clippy::unnecessary_cast)] - let type_id = unsafe { - ifr.ifr_ifru.ifru_functional_type as u32 - }; + let type_id = unsafe { ifr.ifr_ifru.ifru_functional_type as u32 }; map_to_interface_type(type_id) - } pub(crate) fn interface_type_by_name(name: &str) -> Option { diff --git a/src/os/macos/interface.rs b/src/os/macos/interface.rs index b789024..1bbeb5b 100644 --- a/src/os/macos/interface.rs +++ b/src/os/macos/interface.rs @@ -1,13 +1,13 @@ +use crate::os::darwin::types::{get_functional_type, interface_type_by_name}; +use crate::os::macos::sc::{get_sc_interface_map, read_sc_interfaces_plist_map}; use crate::{ interface::interface::Interface, os::{macos::sc::SCInterface, unix::interface::unix_interfaces}, prelude::InterfaceType, }; -use crate::os::macos::sc::{read_sc_interfaces_plist_map, get_sc_interface_map}; -use crate::os::darwin::types::{get_functional_type, interface_type_by_name}; use std::collections::HashMap; -pub fn interfaces() -> Vec { +pub fn interfaces() -> Vec { let mut ifaces: Vec = unix_interfaces(); let if_extra_map: HashMap = match read_sc_interfaces_plist_map() { @@ -15,7 +15,7 @@ pub fn interfaces() -> Vec { Err(_) => { // Fallback to SCNetworkInterfaceCopyAll ... get_sc_interface_map() - }, + } }; #[cfg(feature = "gateway")] @@ -32,7 +32,7 @@ pub fn interfaces() -> Vec { if let Some(name_type) = interface_type_by_name(&iface.name) { iface.if_type = name_type; } - + if let Some(sc_inface) = if_extra_map.get(&iface.name) { if let Some(sc_type) = sc_inface.if_type() { iface.if_type = sc_type; diff --git a/src/os/macos/sc.rs b/src/os/macos/sc.rs index 37973c0..0d40790 100644 --- a/src/os/macos/sc.rs +++ b/src/os/macos/sc.rs @@ -1,13 +1,13 @@ //! SystemConfiguration::NetworkInterface-related types and functions for macOS. -use std::collections::HashMap; -use std::io::Cursor; +use crate::interface::types::InterfaceType; use mac_addr::MacAddr; use objc2_core_foundation::{CFArray, CFRetained}; use objc2_system_configuration::SCNetworkInterface; -use crate::interface::types::InterfaceType; +use std::collections::HashMap; +use std::io::Cursor; -const SC_NWIF_PATH : &str = "/Library/Preferences/SystemConfiguration/NetworkInterfaces.plist"; +const SC_NWIF_PATH: &str = "/Library/Preferences/SystemConfiguration/NetworkInterfaces.plist"; #[derive(Clone, Debug, Default)] pub(crate) struct SCInterface { @@ -67,16 +67,14 @@ pub(crate) fn get_sc_interface_map() -> HashMap { let name = bsd_name.to_string(); - let sc_if_type: Option = sc_iface - .interface_type() - .map(|s| s.to_string()); + let sc_if_type: Option = sc_iface.interface_type().map(|s| s.to_string()); let friendly_name = sc_iface.localized_display_name().map(|s| s.to_string()); - let mac: Option = sc_iface.hardware_address_string().and_then(|mac_str| { - Some(MacAddr::from_hex_format(&mac_str.to_string())) - }); - + let mac: Option = sc_iface + .hardware_address_string() + .and_then(|mac_str| Some(MacAddr::from_hex_format(&mac_str.to_string()))); + if_map.insert( name.clone(), SCInterface { @@ -134,7 +132,8 @@ fn load_sc_interfaces_plist_map(bytes: &[u8]) -> HashMap { let active = d.get("Active").and_then(|v| v.as_boolean()); - let mac = d.get("IOMACAddress") + let mac = d + .get("IOMACAddress") .and_then(|v| v.as_data()) .and_then(|data| { if data.len() == 6 { From 37a3d0cb282d685d1c58e21d67de5ac4599a5311 Mon Sep 17 00:00:00 2001 From: shellrow Date: Sun, 14 Dec 2025 22:35:18 +0900 Subject: [PATCH 3/3] ios: update interface extras lookup --- src/os/ios/interface.rs | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/os/ios/interface.rs b/src/os/ios/interface.rs index 2b1265d..72ca9e1 100644 --- a/src/os/ios/interface.rs +++ b/src/os/ios/interface.rs @@ -1,29 +1,45 @@ +use crate::interface::types::InterfaceType; +use crate::os::darwin::types::{get_functional_type, interface_type_by_name}; use crate::{interface::interface::Interface, os::unix::interface::unix_interfaces}; pub fn interfaces() -> Vec { - #[cfg(not(feature = "gateway"))] - { - unix_interfaces() - } + let mut ifaces: Vec = unix_interfaces(); + #[cfg(feature = "gateway")] - { - use crate::os::unix::dns::get_system_dns_conf; + let gateway_map = crate::os::darwin::route::get_gateway_map(); + + for iface in &mut ifaces { + // If interface type is Ethernet, try to get a more accurate type + if iface.if_type == InterfaceType::Ethernet { + let ft: InterfaceType = get_functional_type(&iface.name); + if ft != InterfaceType::Unknown { + iface.if_type = ft; + } + } - let mut ifaces: Vec = unix_interfaces(); - let gateway_map = crate::os::darwin::route::get_gateway_map(); - for iface in &mut ifaces { + if let Some(name_type) = interface_type_by_name(&iface.name) { + iface.if_type = name_type; + } + + #[cfg(feature = "gateway")] + { if let Some(gateway) = gateway_map.get(&iface.index) { iface.gateway = Some(gateway.clone()); } } + } + + #[cfg(feature = "gateway")] + { if let Some(local_ip) = crate::net::ip::get_local_ipaddr() { if let Some(idx) = crate::interface::pick_default_iface_index(&ifaces, local_ip) { if let Some(iface) = ifaces.iter_mut().find(|it| it.index == idx) { iface.default = true; - iface.dns_servers = get_system_dns_conf(); + iface.dns_servers = crate::os::unix::dns::get_system_dns_conf(); } } } - ifaces } + + ifaces }