diff --git a/Cargo.toml b/Cargo.toml index a3162d5..ca02956 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ features = [ [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] objc2-core-foundation = "0.3" -objc2-system-configuration = { version = "0.3", features = ["SCNetworkConfiguration"] } +objc2-system-configuration = { version = "0.3", default-features = false, features = ["SCNetworkConfiguration"] } plist = "1.8" [target.'cfg(target_os = "macos")'.dependencies] @@ -53,9 +53,15 @@ block2 = "0.6" serde_json = "1.0" [features] -default = ["gateway"] +default = ["gateway", "apple-system-configuration-extra"] serde = ["dep:serde", "mac-addr/serde", "ipnet/serde"] gateway = [] +apple-system-configuration-extra = [ + "objc2-system-configuration/SCPreferences", + "objc2-system-configuration/SCDynamicStore", + "objc2-system-configuration/SCDynamicStoreKey", + "objc2-system-configuration/SCSchemaDefinitions", +] [[example]] name = "list_interfaces" diff --git a/README.md b/README.md index e7c9acf..f5090c8 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,10 @@ and retrieving commonly used metadata across platforms. - Enumerate all available network interfaces - Detect the default network interface - Retrieve interface metadata: - - Interface type - - MAC address - - IPv4 / IPv6 addresses and prefixes - - MTU, flags, operational state, etc... + - Interface type + - MAC address + - IPv4 / IPv6 addresses and prefixes + - MTU, flags, operational state, etc... - Native traffic statistics (RX/TX bytes) for each interface - Designed for **cross-platform** @@ -32,7 +32,7 @@ See the [Interface][doc-interface-url] struct documentation for detail. - macOS - Windows - Android -- iOS (and other Apple targets) +- iOS - BSDs ## Usage @@ -44,6 +44,24 @@ netdev = "0.43" For more details, see [examples][examples-url] or [doc][doc-url]. +## Feature flags +- `gateway` (default) + - Enables default interface and default gateway helpers. +- `apple-system-configuration-extra` (default) + - Enables deeper Apple metadata enrichment using `SystemConfiguration` APIs. + - On Apple targets, this adds metadata such as interface display names, DHCP hints, and iOS DNS resolver lookup when the platform exposes them. + +To opt out of the deeper Apple enrichment while keeping gateway helpers: + +```toml +[dependencies] +netdev = { version = "0.43", default-features = false, features = ["gateway"] } +``` + +## Apple behavior +`netdev` links `SystemConfiguration.framework` automatically on `macOS` and `iOS` through the crate's own build script. +If your final app link is performed by Xcode, you may still need to add `SystemConfiguration.framework` to the app target manually. + ## Project History This crate was originally published as [default-net][default-net-crates-io-url] and later rebranded to `netdev` by the author myself for future expansion, clearer naming, and long-term maintenance. diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..d779e81 --- /dev/null +++ b/build.rs @@ -0,0 +1,7 @@ +fn main() { + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + + if matches!(target_os.as_str(), "macos" | "ios") { + println!("cargo:rustc-link-lib=framework=SystemConfiguration"); + } +} diff --git a/src/interface/interface.rs b/src/interface/interface.rs index 9644752..1671417 100644 --- a/src/interface/interface.rs +++ b/src/interface/interface.rs @@ -71,7 +71,7 @@ pub struct Interface { pub oper_state: OperState, /// Transmit link speed in bits per second. /// - /// This field is usually available on Linux, Android, BSD, macOS and Windows. + /// This field is usually available on Linux, Android, BSD, Apple targets, and Windows. /// It may be `None` for virtual adapters, unsupported drivers, or platforms that do not /// expose link speed. pub transmit_speed: Option, diff --git a/src/interface/mod.rs b/src/interface/mod.rs index ecc0469..e4d1ecd 100644 --- a/src/interface/mod.rs +++ b/src/interface/mod.rs @@ -103,8 +103,7 @@ pub(crate) fn interfaces() -> Vec { { crate::os::macos::interface::interfaces() } - //#[cfg(target_os = "ios")] - #[cfg(all(target_vendor = "apple", not(target_os = "macos")))] + #[cfg(target_os = "ios")] { crate::os::ios::interface::interfaces() } diff --git a/src/os/ios/dns.rs b/src/os/ios/dns.rs new file mode 100644 index 0000000..850389a --- /dev/null +++ b/src/os/ios/dns.rs @@ -0,0 +1,98 @@ +//! DNS resolver lookup for Apple mobile targets. + +use std::net::IpAddr; + +use objc2_core_foundation::{CFArray, CFDictionary, CFPropertyList, CFRetained, CFString, Type}; +use objc2_system_configuration::{ + SCDynamicStore, kSCDynamicStoreDomainState, kSCDynamicStorePropNetPrimaryService, kSCEntNetDNS, + kSCEntNetIPv4, kSCPropNetDNSServerAddresses, +}; + +fn new_dynamic_store() -> Option> { + let name = CFString::from_str("netdev"); + // SAFETY: + // We do not register a callback, and pass a null context pointer. + unsafe { SCDynamicStore::new(None, &name, None, std::ptr::null_mut()) } +} + +fn cast_property_list(value: CFRetained) -> CFRetained { + // SAFETY: + // Callers only use this after determining the expected dynamic-store value shape. + unsafe { + let raw = CFRetained::into_raw(value); + CFRetained::from_raw(raw.cast()) + } +} + +fn parse_dns_server_addresses( + dict: CFRetained>>, +) -> Vec { + let key = unsafe { kSCPropNetDNSServerAddresses }; + let Some(addresses) = dict.get(key) else { + return Vec::new(); + }; + + let mut out = Vec::new(); + for address in addresses.iter() { + if let Ok(ip) = address.to_string().parse::() { + if !out.contains(&ip) { + out.push(ip); + } + } + } + out +} + +fn dns_servers_for_key(store: &SCDynamicStore, key: &CFString) -> Vec { + let Some(value) = SCDynamicStore::value(Some(store), key) else { + return Vec::new(); + }; + let dict: CFRetained>> = cast_property_list(value); + parse_dns_server_addresses(dict) +} + +fn global_dns_servers(store: &SCDynamicStore) -> Vec { + let key = SCDynamicStore::key_create_network_global_entity( + None, + unsafe { kSCDynamicStoreDomainState }, + unsafe { kSCEntNetDNS }, + ); + dns_servers_for_key(store, &key) +} + +fn primary_service_id(store: &SCDynamicStore) -> Option> { + let key = SCDynamicStore::key_create_network_global_entity( + None, + unsafe { kSCDynamicStoreDomainState }, + unsafe { kSCEntNetIPv4 }, + ); + let value = SCDynamicStore::value(Some(store), &key)?; + let dict: CFRetained> = cast_property_list(value); + dict.get(unsafe { kSCDynamicStorePropNetPrimaryService }) +} + +fn primary_service_dns_servers(store: &SCDynamicStore) -> Vec { + let Some(service_id) = primary_service_id(store) else { + return Vec::new(); + }; + let key = SCDynamicStore::key_create_network_service_entity( + None, + unsafe { kSCDynamicStoreDomainState }, + &service_id, + Some(unsafe { kSCEntNetDNS }), + ); + dns_servers_for_key(store, &key) +} + +pub fn get_system_dns_conf() -> Vec { + let Some(store) = new_dynamic_store() else { + return Vec::new(); + }; + + let global = global_dns_servers(&store); + if !global.is_empty() { + return global; + } + + primary_service_dns_servers(&store) +} diff --git a/src/os/ios/interface.rs b/src/os/ios/interface.rs index 352f614..281a4e9 100644 --- a/src/os/ios/interface.rs +++ b/src/os/ios/interface.rs @@ -1,15 +1,53 @@ use crate::interface::types::InterfaceType; use crate::os::darwin::types::{get_functional_type, interface_type_by_name}; +#[cfg(feature = "gateway")] +use crate::os::ios::network::NWPathStatus; use crate::{interface::interface::Interface, os::unix::interface::unix_interfaces}; +#[cfg(feature = "gateway")] +use crate::{net::device::NetworkDevice, net::mac::MacAddr}; + +#[cfg(feature = "gateway")] +fn merge_gateway(base: &mut NetworkDevice, supplement: &NetworkDevice) { + if base.mac_addr == MacAddr::zero() && supplement.mac_addr != MacAddr::zero() { + base.mac_addr = supplement.mac_addr; + } + + for ip in &supplement.ipv4 { + if !base.ipv4.contains(ip) { + base.ipv4.push(*ip); + } + } + + for ip in &supplement.ipv6 { + if !base.ipv6.contains(ip) { + base.ipv6.push(*ip); + } + } +} pub fn interfaces() -> Vec { let mut ifaces: Vec = unix_interfaces(); - let nw_iface_map = super::network::nw_interface_map(); + let nw_path_snapshot = super::network::current_path_snapshot(); + let nw_iface_map = nw_path_snapshot + .as_ref() + .map(|snapshot| snapshot.interface_map()) + .unwrap_or_default(); + #[cfg(feature = "apple-system-configuration-extra")] + let sc_iface_map = super::sc::get_sc_interface_map(); #[cfg(feature = "gateway")] let gateway_map = crate::os::darwin::route::get_gateway_map(); + #[cfg(feature = "gateway")] + let default_idx = crate::net::ip::get_local_ipaddr() + .and_then(|local_ip| crate::interface::pick_default_iface_index(&ifaces, local_ip)) + .or_else(|| { + nw_path_snapshot + .as_ref() + .and_then(|snapshot| snapshot.first_non_loopback_interface_index()) + }); + for iface in &mut ifaces { // If interface type is Ethernet, try to get a more accurate type if iface.if_type == InterfaceType::Ethernet { @@ -27,20 +65,50 @@ pub fn interfaces() -> Vec { iface.if_type = nw_iface.if_type; } + #[cfg(feature = "apple-system-configuration-extra")] + if let Some(sc_iface) = sc_iface_map.get(&iface.name) { + if let Some(sc_type) = sc_iface.if_type() { + iface.if_type = sc_type; + } + iface.friendly_name = sc_iface.friendly_name.clone(); + iface.dhcp_v4_enabled = sc_iface.dhcp_v4_enabled; + iface.dhcp_v6_enabled = sc_iface.dhcp_v6_enabled; + } + #[cfg(feature = "gateway")] { if let Some(gateway) = gateway_map.get(&iface.index) { iface.gateway = Some(gateway.clone()); } + + if Some(iface.index) == default_idx { + iface.default = true; + + if let Some(snapshot) = nw_path_snapshot.as_ref() { + if snapshot.status == NWPathStatus::Satisfied + && (!snapshot.gateways.ipv4.is_empty() + || !snapshot.gateways.ipv6.is_empty()) + { + let gateway = iface.gateway.get_or_insert_with(NetworkDevice::new); + merge_gateway(gateway, &snapshot.gateways); + } + } + } } } #[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; + if let Some(idx) = default_idx { + if let Some(iface) = ifaces.iter_mut().find(|it| it.index == idx) { + iface.default = true; + + #[cfg(feature = "apple-system-configuration-extra")] + { + iface.dns_servers = crate::os::ios::dns::get_system_dns_conf(); + } + #[cfg(not(feature = "apple-system-configuration-extra"))] + { iface.dns_servers = crate::os::unix::dns::get_system_dns_conf(); } } diff --git a/src/os/ios/mod.rs b/src/os/ios/mod.rs index c419d79..7b346fa 100644 --- a/src/os/ios/mod.rs +++ b/src/os/ios/mod.rs @@ -1,2 +1,6 @@ +#[cfg(feature = "apple-system-configuration-extra")] +pub mod dns; pub mod interface; pub mod network; +#[cfg(feature = "apple-system-configuration-extra")] +pub mod sc; diff --git a/src/os/ios/network.rs b/src/os/ios/network.rs index 56fe08e..0c74bab 100644 --- a/src/os/ios/network.rs +++ b/src/os/ios/network.rs @@ -1,9 +1,10 @@ -//! Minimal Network.framework interface enumeration +//! Network.framework helpers for Apple mobile targets. #![allow(non_camel_case_types)] use std::collections::HashMap; use std::ffi::CStr; +use std::net::IpAddr; use std::os::raw::{c_char, c_void}; use std::sync::{Arc, Condvar, Mutex}; use std::time::Duration; @@ -14,23 +15,79 @@ use dispatch2::{ }; use crate::interface::types::InterfaceType; +use crate::net::device::NetworkDevice; #[derive(Debug, Clone)] pub(crate) struct NWInterface { pub bsd_name: String, pub if_type: InterfaceType, - #[allow(dead_code)] + #[cfg(feature = "gateway")] pub index: u32, } -// Link to Network.framework +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum NWPathStatus { + Invalid, + Satisfied, + Unsatisfied, + Satisfiable, +} + +#[allow(dead_code)] +#[derive(Debug, Clone)] +pub(crate) struct NWPathSnapshot { + pub status: NWPathStatus, + pub interfaces: Vec, + pub gateways: NetworkDevice, + pub has_ipv4: bool, + pub has_ipv6: bool, + pub has_dns: bool, + pub is_expensive: bool, + pub is_constrained: bool, + pub is_ultra_constrained: bool, +} + +impl NWPathSnapshot { + pub fn interface_map(&self) -> HashMap { + let mut map = HashMap::with_capacity(self.interfaces.len()); + for iface in &self.interfaces { + map.insert(iface.bsd_name.clone(), iface.clone()); + } + map + } + + #[cfg(feature = "gateway")] + pub fn first_non_loopback_interface_index(&self) -> Option { + self.interfaces + .iter() + .find(|iface| iface.if_type != InterfaceType::Loopback) + .map(|iface| iface.index) + } +} + +impl Default for NWPathSnapshot { + fn default() -> Self { + Self { + status: NWPathStatus::Invalid, + interfaces: Vec::new(), + gateways: NetworkDevice::new(), + has_ipv4: false, + has_ipv6: false, + has_dns: false, + is_expensive: false, + is_constrained: false, + is_ultra_constrained: false, + } + } +} + #[link(name = "Network", kind = "framework")] unsafe extern "C" {} -// Network.framework (minimal) FFI types type nw_path_monitor_t = *mut c_void; type nw_path_t = *mut c_void; type nw_interface_t = *mut c_void; +type nw_endpoint_t = *mut c_void; #[allow(dead_code)] #[repr(C)] @@ -43,6 +100,27 @@ enum nw_interface_type_t { loopback = 4, } +#[allow(dead_code)] +#[repr(C)] +#[derive(Clone, Copy, Debug)] +enum nw_path_status_t { + invalid = 0, + satisfied = 1, + unsatisfied = 2, + satisfiable = 3, +} + +#[allow(dead_code)] +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum nw_endpoint_type_t { + invalid = 0, + address = 1, + host = 2, + bonjour_service = 3, + url = 4, +} + unsafe extern "C" { fn nw_path_monitor_create() -> nw_path_monitor_t; fn nw_path_monitor_set_queue(monitor: nw_path_monitor_t, queue: *mut c_void); @@ -50,16 +128,28 @@ unsafe extern "C" { fn nw_path_monitor_start(monitor: nw_path_monitor_t); fn nw_path_monitor_cancel(monitor: nw_path_monitor_t); + fn nw_path_get_status(path: nw_path_t) -> nw_path_status_t; + fn nw_path_has_ipv4(path: nw_path_t) -> bool; + fn nw_path_has_ipv6(path: nw_path_t) -> bool; + fn nw_path_has_dns(path: nw_path_t) -> bool; + fn nw_path_is_expensive(path: nw_path_t) -> bool; + fn nw_path_is_constrained(path: nw_path_t) -> bool; + fn nw_path_is_ultra_constrained(path: nw_path_t) -> bool; fn nw_path_enumerate_interfaces(path: nw_path_t, enumerate_block: *mut c_void); + fn nw_path_enumerate_gateways(path: nw_path_t, enumerate_block: *mut c_void); fn nw_interface_get_name(interface: nw_interface_t) -> *const c_char; fn nw_interface_get_type(interface: nw_interface_t) -> nw_interface_type_t; + #[cfg(feature = "gateway")] fn nw_interface_get_index(interface: nw_interface_t) -> u32; + fn nw_endpoint_get_type(endpoint: nw_endpoint_t) -> nw_endpoint_type_t; + fn nw_endpoint_copy_address_string(endpoint: nw_endpoint_t) -> *mut c_char; + fn nw_release(obj: *mut c_void); } -fn map_type(t: nw_interface_type_t) -> InterfaceType { +fn map_interface_type(t: nw_interface_type_t) -> InterfaceType { match t { nw_interface_type_t::wifi => InterfaceType::Wireless80211, nw_interface_type_t::cellular => InterfaceType::Wwanpp, @@ -69,102 +159,184 @@ fn map_type(t: nw_interface_type_t) -> InterfaceType { } } -type EnumBlock = dyn Fn(*mut c_void) -> u8 + 'static; +fn map_path_status(status: nw_path_status_t) -> NWPathStatus { + match status { + nw_path_status_t::satisfied => NWPathStatus::Satisfied, + nw_path_status_t::unsatisfied => NWPathStatus::Unsatisfied, + nw_path_status_t::satisfiable => NWPathStatus::Satisfiable, + nw_path_status_t::invalid => NWPathStatus::Invalid, + } +} -/// Enumerate interfaces from Network.framework (NWPath/NWInterface) -pub fn nfw_interfaces() -> Vec { - let shared: Arc<(Mutex>>, Condvar)> = - Arc::new((Mutex::new(None), Condvar::new())); - let shared2 = Arc::clone(&shared); +fn parse_gateway_ip(endpoint: nw_endpoint_t) -> Option { + if endpoint.is_null() { + return None; + } - let queue = DispatchQueue::global_queue(GlobalQueueIdentifier::Priority( - DispatchQueueGlobalPriority::Default, - )); + let ty = unsafe { nw_endpoint_get_type(endpoint) }; + if ty != nw_endpoint_type_t::address { + return None; + } - type UpdateHandler = dyn Fn(*mut c_void) + 'static; + let address = unsafe { nw_endpoint_copy_address_string(endpoint) }; + if address.is_null() { + return None; + } - let blk: RcBlock = RcBlock::new(move |path_ptr: *mut c_void| { - let path = path_ptr as nw_path_t; + let ip = unsafe { CStr::from_ptr(address) } + .to_string_lossy() + .parse::() + .ok(); - let acc: Arc>> = Arc::new(Mutex::new(Vec::new())); - let acc2 = Arc::clone(&acc); + unsafe { + libc::free(address.cast()); + } - let enum_blk: RcBlock = RcBlock::new(move |iface_ptr: *mut c_void| -> u8 { - let iface = iface_ptr as nw_interface_t; - if iface.is_null() { - return 1; - } + ip +} + +fn collect_path_snapshot(path: nw_path_t) -> NWPathSnapshot { + let mut snapshot = NWPathSnapshot { + status: map_path_status(unsafe { nw_path_get_status(path) }), + has_ipv4: unsafe { nw_path_has_ipv4(path) }, + has_ipv6: unsafe { nw_path_has_ipv6(path) }, + has_dns: unsafe { nw_path_has_dns(path) }, + is_expensive: unsafe { nw_path_is_expensive(path) }, + is_constrained: unsafe { nw_path_is_constrained(path) }, + is_ultra_constrained: unsafe { nw_path_is_ultra_constrained(path) }, + ..NWPathSnapshot::default() + }; + + let interfaces: Arc>> = Arc::new(Mutex::new(Vec::new())); + let interfaces_ref = Arc::clone(&interfaces); + type InterfaceEnumBlock = dyn Fn(*mut c_void) -> u8 + 'static; + let interface_block: RcBlock = RcBlock::new(move |iface_ptr| -> u8 { + let iface = iface_ptr as nw_interface_t; + if iface.is_null() { + return 1; + } + + let name_ptr = unsafe { nw_interface_get_name(iface) }; + if name_ptr.is_null() { + return 1; + } + + let name = unsafe { CStr::from_ptr(name_ptr) } + .to_string_lossy() + .into_owned(); + let if_type = map_interface_type(unsafe { nw_interface_get_type(iface) }); + #[cfg(feature = "gateway")] + let index = unsafe { nw_interface_get_index(iface) }; + + let nw_interface = NWInterface { + bsd_name: name, + if_type, + #[cfg(feature = "gateway")] + index, + }; - let name_ptr = unsafe { nw_interface_get_name(iface) }; - if name_ptr.is_null() { - return 1; + interfaces_ref.lock().unwrap().push(nw_interface); + + 1 + }); + + unsafe { + nw_path_enumerate_interfaces( + path, + RcBlock::::as_ptr(&interface_block) as *mut c_void, + ); + } + snapshot.interfaces = std::mem::take(&mut *interfaces.lock().unwrap()); + + let gateways: Arc> = Arc::new(Mutex::new(NetworkDevice::new())); + let gateways_ref = Arc::clone(&gateways); + type GatewayEnumBlock = dyn Fn(*mut c_void) -> u8 + 'static; + let gateway_block: RcBlock = RcBlock::new(move |endpoint_ptr| -> u8 { + if let Some(ip) = parse_gateway_ip(endpoint_ptr as nw_endpoint_t) { + let mut gateway = gateways_ref.lock().unwrap(); + match ip { + IpAddr::V4(ipv4) => { + if !gateway.ipv4.contains(&ipv4) { + gateway.ipv4.push(ipv4); + } + } + IpAddr::V6(ipv6) => { + if !gateway.ipv6.contains(&ipv6) { + gateway.ipv6.push(ipv6); + } + } } + } - let name = unsafe { CStr::from_ptr(name_ptr) } - .to_string_lossy() - .into_owned(); + 1 + }); - let ty = unsafe { nw_interface_get_type(iface) }; - let idx = unsafe { nw_interface_get_index(iface) }; + unsafe { + nw_path_enumerate_gateways( + path, + RcBlock::::as_ptr(&gateway_block) as *mut c_void, + ); + } + snapshot.gateways = gateways.lock().unwrap().clone(); - acc2.lock().unwrap().push(NWInterface { - bsd_name: name, - if_type: map_type(ty), - index: idx, - }); + snapshot +} - 1 - }); +type UpdateHandler = dyn Fn(*mut c_void) + 'static; - let enum_ptr: *mut c_void = RcBlock::::as_ptr(&enum_blk) as *mut c_void; - unsafe { nw_path_enumerate_interfaces(path, enum_ptr) }; +/// Capture a single snapshot of the current default Network.framework path. +pub(crate) fn current_path_snapshot() -> Option { + let shared: Arc<(Mutex>, Condvar)> = + Arc::new((Mutex::new(None), Condvar::new())); + let shared_ref = Arc::clone(&shared); - // Take only the first callback result and notify - let v = std::mem::take(&mut *acc.lock().unwrap()); - let (lock, cv) = &*shared2; - let mut g = lock.lock().unwrap(); - if g.is_none() { - *g = Some(v); - cv.notify_one(); + let queue = DispatchQueue::global_queue(GlobalQueueIdentifier::Priority( + DispatchQueueGlobalPriority::Default, + )); + + let update_block: RcBlock = RcBlock::new(move |path_ptr: *mut c_void| { + if path_ptr.is_null() { + return; } - // note: enum_blk lives until the end of this closure + let snapshot = collect_path_snapshot(path_ptr as nw_path_t); + let (lock, cv) = &*shared_ref; + let mut guard = lock.lock().unwrap(); + if guard.is_none() { + *guard = Some(snapshot); + cv.notify_one(); + } }); unsafe { let monitor = nw_path_monitor_create(); if monitor.is_null() { - return Vec::new(); + return None; } - let q_nn = DispatchRetained::::as_ptr(&queue); - let q_ptr: *mut c_void = q_nn.as_ptr() as *mut c_void; - - let blk_ptr: *mut c_void = RcBlock::::as_ptr(&blk) as *mut c_void; + let queue_ptr = DispatchRetained::::as_ptr(&queue) + .as_ptr() + .cast::(); + let update_ptr = RcBlock::::as_ptr(&update_block) as *mut c_void; - nw_path_monitor_set_queue(monitor, q_ptr); - nw_path_monitor_set_update_handler(monitor, blk_ptr); + nw_path_monitor_set_queue(monitor, queue_ptr); + nw_path_monitor_set_update_handler(monitor, update_ptr); nw_path_monitor_start(monitor); let (lock, cv) = &*shared; - let guard = lock.lock().unwrap(); - if guard.is_none() { - let _ = cv.wait_timeout(guard, Duration::from_millis(500)).unwrap(); - } + let mut guard = if guard.is_none() { + cv.wait_timeout(guard, Duration::from_millis(800)) + .unwrap() + .0 + } else { + guard + }; + let snapshot = guard.take(); nw_path_monitor_cancel(monitor); - nw_release(monitor as *mut c_void); - } - - shared.0.lock().unwrap().take().unwrap_or_default() -} + nw_release(monitor); -pub fn nw_interface_map() -> HashMap { - let mut map = HashMap::new(); - let ifaces = nfw_interfaces(); - for iface in ifaces { - map.insert(iface.bsd_name.clone(), iface); + snapshot } - map } diff --git a/src/os/ios/sc.rs b/src/os/ios/sc.rs new file mode 100644 index 0000000..f498bd2 --- /dev/null +++ b/src/os/ios/sc.rs @@ -0,0 +1,157 @@ +//! SystemConfiguration helpers for Apple mobile targets. + +use crate::interface::types::InterfaceType; +use objc2_core_foundation::{CFArray, CFDictionary, CFRetained, CFString}; +use objc2_system_configuration::{ + SCNetworkInterface, SCNetworkProtocol, SCNetworkService, SCPreferences, + kSCNetworkProtocolTypeIPv4, kSCNetworkProtocolTypeIPv6, +}; +use std::collections::HashMap; + +#[derive(Clone, Debug, Default)] +pub(crate) struct SCInterface { + pub friendly_name: Option, + pub sc_type: Option, + pub dhcp_v4_enabled: Option, + pub dhcp_v6_enabled: Option, +} + +impl SCInterface { + pub fn if_type(&self) -> Option { + self.sc_type.as_deref().map(map_sc_interface_type) + } +} + +fn map_sc_interface_type(type_id: &str) -> InterfaceType { + match type_id { + "Bridge" => InterfaceType::Bridge, + "AirPort" => InterfaceType::Wireless80211, + "Ethernet" => InterfaceType::Ethernet, + "IEEE80211" => InterfaceType::Wireless80211, + "Loopback" => InterfaceType::Loopback, + "Modem" => InterfaceType::GenericModem, + "PPP" => InterfaceType::Ppp, + "WWAN" => InterfaceType::Wwanpp, + _ => InterfaceType::Unknown, + } +} + +fn ipv4_method_to_dhcp(method: &str) -> Option { + match method { + "DHCP" => Some(true), + "Manual" | "Off" => Some(false), + _ => None, + } +} + +fn ipv6_method_to_dhcp(method: &str) -> Option { + match method { + "Manual" | "LinkLocal" | "Off" => Some(false), + _ => None, + } +} + +fn cast_cf_array(array: CFRetained) -> CFRetained> { + // SAFETY: + // The underlying CFArray is documented to contain only the requested type. + unsafe { + let raw = CFRetained::into_raw(array); + CFRetained::from_raw(raw.cast()) + } +} + +fn cast_cf_dictionary(dict: CFRetained) -> CFRetained> { + // SAFETY: + // The callers only use this when the SystemConfiguration API documents + // CFString keys and value types that match `V`. + unsafe { + let raw = CFRetained::into_raw(dict); + CFRetained::from_raw(raw.cast()) + } +} + +fn sc_network_interfaces_all() -> CFRetained> { + cast_cf_array(SCNetworkInterface::all()) +} + +fn sc_network_services_all(prefs: &SCPreferences) -> Option>> { + SCNetworkService::all(prefs).map(cast_cf_array) +} + +fn protocol_config_method( + service: &SCNetworkService, + protocol_type: &CFString, +) -> Option> { + let protocol = service.protocol(protocol_type)?; + let protocol: CFRetained = protocol; + let config = protocol.configuration()?; + let config: CFRetained> = cast_cf_dictionary(config); + let key = CFString::from_str("ConfigMethod"); + config.get(&key) +} + +/// Build a map of `BSD name -> SCInterface` using SystemConfiguration APIs. +pub(crate) fn get_sc_interface_map() -> HashMap { + let mut map = HashMap::new(); + + for sc_iface in sc_network_interfaces_all().iter() { + let Some(bsd_name) = sc_iface.bsd_name() else { + continue; + }; + + map.insert( + bsd_name.to_string(), + SCInterface { + friendly_name: sc_iface.localized_display_name().map(|s| s.to_string()), + sc_type: sc_iface.interface_type().map(|s| s.to_string()), + dhcp_v4_enabled: None, + dhcp_v6_enabled: None, + }, + ); + } + + let prefs_name = CFString::from_str("netdev"); + let Some(prefs) = SCPreferences::new(None, &prefs_name, None) else { + return map; + }; + + let Some(services) = sc_network_services_all(&prefs) else { + return map; + }; + + for service in services.iter() { + let Some(sc_iface) = service.interface() else { + continue; + }; + let Some(bsd_name) = sc_iface.bsd_name() else { + continue; + }; + + let entry = map.entry(bsd_name.to_string()).or_default(); + + if entry.friendly_name.is_none() { + entry.friendly_name = service + .name() + .map(|s| s.to_string()) + .or_else(|| sc_iface.localized_display_name().map(|s| s.to_string())); + } + + if entry.sc_type.is_none() { + entry.sc_type = sc_iface.interface_type().map(|s| s.to_string()); + } + + if entry.dhcp_v4_enabled.is_none() { + entry.dhcp_v4_enabled = + protocol_config_method(&service, unsafe { kSCNetworkProtocolTypeIPv4 }) + .and_then(|method| ipv4_method_to_dhcp(&method.to_string())); + } + + if entry.dhcp_v6_enabled.is_none() { + entry.dhcp_v6_enabled = + protocol_config_method(&service, unsafe { kSCNetworkProtocolTypeIPv6 }) + .and_then(|method| ipv6_method_to_dhcp(&method.to_string())); + } + } + + map +} diff --git a/src/os/mod.rs b/src/os/mod.rs index 78d2fb9..b840c49 100644 --- a/src/os/mod.rs +++ b/src/os/mod.rs @@ -13,8 +13,7 @@ pub mod darwin; #[cfg(target_os = "macos")] pub mod macos; -//#[cfg(target_os = "ios")] -#[cfg(all(target_vendor = "apple", not(target_os = "macos")))] +#[cfg(target_os = "ios")] pub mod ios; #[cfg(target_os = "windows")] diff --git a/src/os/unix/dns.rs b/src/os/unix/dns.rs index 5bf7601..105b7f0 100644 --- a/src/os/unix/dns.rs +++ b/src/os/unix/dns.rs @@ -2,6 +2,7 @@ use std::fs::read_to_string; use std::net::IpAddr; use std::net::ToSocketAddrs; +#[cfg_attr(target_os = "ios", allow(dead_code))] pub fn get_system_dns_conf() -> Vec { const PATH_RESOLV_CONF: &str = "/etc/resolv.conf"; let r = read_to_string(PATH_RESOLV_CONF); diff --git a/src/os/unix/interface.rs b/src/os/unix/interface.rs index fd4f805..f95e757 100644 --- a/src/os/unix/interface.rs +++ b/src/os/unix/interface.rs @@ -190,6 +190,7 @@ fn unix_interfaces_inner( #[cfg(any( target_os = "macos", + target_os = "ios", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd" diff --git a/src/os/unix/link_speed.rs b/src/os/unix/link_speed.rs index 6675398..417a816 100644 --- a/src/os/unix/link_speed.rs +++ b/src/os/unix/link_speed.rs @@ -1,5 +1,6 @@ #![cfg(any( target_os = "macos", + target_os = "ios", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd" @@ -9,7 +10,7 @@ use libc::{AF_INET, IFNAMSIZ, SOCK_DGRAM, close, ioctl, socket}; use std::ffi::CString; -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "ios"))] use macos_subtypes::{ifm_subtype, map_subtype_to_bps}; #[cfg(target_os = "freebsd")] @@ -21,7 +22,7 @@ use openbsd_subtypes::{ifm_subtype, map_subtype_to_bps}; #[cfg(target_os = "netbsd")] use netbsd_subtypes::{ifm_subtype, map_subtype_to_bps}; -/// Returns the unix/BSD/macOS network interface link speed in bps for the given interface name. +/// Returns the unix/BSD/Apple network interface link speed in bps for the given interface name. pub(crate) fn get_link_speed(iface_name: &str) -> std::io::Result { Ok(get_ifmediareq(iface_name)?.into()) } @@ -75,7 +76,7 @@ impl From for LinkSpeed { // OpenBSD uses uint64_t instead of int for their ifmediareq struct, see // https://github.com/openbsd/src/blob/master/sys/net/if.h#L456 . -// Other BSD and macOS use int. Furthermore, OpenBSD uses an u64 TMASK +// Other BSD and Apple targets use int. Furthermore, OpenBSD uses an u64 TMASK // and OpenBSD and NetBSD use different values for the subtypes. #[cfg(target_os = "openbsd")] type ifmediareq_int = u64; @@ -99,7 +100,7 @@ struct ifmediareq { // extended types. However, some BSD variants (OpenBSD and NetBSD) don't define SIOCGIFXMEDIA. // This code uses SIOCGIFXMEDIA where available, otherwise SIOCGIFMEDIA. -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "ios"))] // #define SIOCGIFXMEDIA _IOWR('i', 72, struct ifmediareq) const SIOCGIFXMEDIA: u64 = 0xc02c6948; // #define SIOCGIFMEDIA _IOWR('i', 56, struct ifmediareq) @@ -121,7 +122,7 @@ const SIOCGIFXMEDIA: u32 = 0xc030698b; // #define SIOCGIFMEDIA _IOWR('i', 56, struct ifmediareq) const SIOCGIFXMEDIA: u64 = 0xc02c6938; -// The following constants are the same across all BSD variants and macOS +// The following constants are the same across all BSD variants and Apple targets. const IFM_AUTO: i32 = 0; //const IFM_MANUAL: i32 = 1; @@ -148,7 +149,7 @@ const IFM_HPNA_1: i32 = 17; // HomePNA 1.0 (1Mb/s) // The following constants are different on every BSD variant and macOS -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "ios"))] mod macos_subtypes { // https://github.com/apple/darwin-xnu/blob/main/bsd/net/if_media.h // MacOSX.sdk/usr/include/net/if_media.h