From b3bfb0fb5595cd143bfa0b9f07d1ad6e0ef0c313 Mon Sep 17 00:00:00 2001 From: iishabakaev Date: Sat, 24 Oct 2026 20:26:31 +0300 Subject: [PATCH] fix(ios): use only public APIs on iOS SCNetworkInterface, SCNetworkService, SCPreferences and SCDynamicStore are API_UNAVAILABLE(ios) in the iOS SDK; App Store review rejects apps that import them. On iOS: - drop the SystemConfiguration interface metadata (friendly name, DHCP hints); apple-system-configuration-extra is now macOS-only - read DNS resolvers through libresolv (res_ninit / res_getservers) - return default IPv6 address flags: the iOS SDK has no SIOCGIFAFLAG_IN6, and libc >= 0.2.190 defines in6_ifreq for macOS only (E0425 on iOS) - link SystemConfiguration.framework on macOS only --- Cargo.toml | 4 +- README.md | 5 +- build.rs | 2 +- src/interface/ipv6_addr_flags.rs | 7 +- src/os/darwin/mod.rs | 1 + src/os/ios/dns.rs | 146 ++++++++++++---------------- src/os/ios/interface.rs | 24 +---- src/os/ios/mod.rs | 4 +- src/os/ios/sc.rs | 157 ------------------------------- 9 files changed, 73 insertions(+), 277 deletions(-) delete mode 100644 src/os/ios/sc.rs diff --git a/Cargo.toml b/Cargo.toml index 8f49017..a215473 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,11 +40,11 @@ features = [ ] [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] -objc2-core-foundation = "0.3" -objc2-system-configuration = { version = "0.3", default-features = false, features = ["SCNetworkConfiguration"] } plist = "1.10" [target.'cfg(target_os = "macos")'.dependencies] +objc2-core-foundation = "0.3" +objc2-system-configuration = { version = "0.3", default-features = false, features = ["SCNetworkConfiguration"] } objc2 = { version = "0.6", optional = true } objc2-core-wlan = { version = "0.3", optional = true } objc2-foundation = { version = "0.3", optional = true } diff --git a/README.md b/README.md index a51c866..28e1e39 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ For more details, see [examples][examples-url] or [doc][doc-url]. - 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. + - On macOS, this adds metadata such as interface display names and DHCP hints. iOS does not offer these APIs (`API_UNAVAILABLE(ios)`), so the feature has no effect there; iOS reads DNS resolvers through libresolv. - `android-extra` (default) - Enables deeper Android metadata enrichment using Android platform APIs through JNI bindings. - On Android, this can add metadata such as traffic stats, DNS servers, DHCP hints, and Wi-Fi link speed when the app provides the required Android context and permissions. @@ -65,8 +65,9 @@ netdev = { version = "0.46", default-features = false, features = ["gateway"] } ``` ## Apple behavior -`netdev` links `SystemConfiguration.framework` automatically on `macOS` and `iOS` through its build script. +`netdev` links `SystemConfiguration.framework` automatically on `macOS` through its build script. If your app is ultimately linked by Xcode, you may still need to add `SystemConfiguration.framework` to the app target manually. +On `iOS`, `netdev` uses only public APIs: `getifaddrs`, routing sysctls, `Network.framework` and libresolv. macOS Wi-Fi transmit-speed collection is disabled by default. To enable it, opt in to the `apple-wifi-extra` feature: diff --git a/build.rs b/build.rs index d779e81..0a5d998 100644 --- a/build.rs +++ b/build.rs @@ -1,7 +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") { + if target_os == "macos" { println!("cargo:rustc-link-lib=framework=SystemConfiguration"); } } diff --git a/src/interface/ipv6_addr_flags.rs b/src/interface/ipv6_addr_flags.rs index 7a10028..1b26eb7 100644 --- a/src/interface/ipv6_addr_flags.rs +++ b/src/interface/ipv6_addr_flags.rs @@ -48,7 +48,7 @@ pub struct Ipv6AddrFlags { // Platform dispatch for `get_ipv6_addr_flags`, called from `unix_interfaces()`. -#[cfg(target_vendor = "apple")] +#[cfg(target_os = "macos")] pub(crate) use crate::os::darwin::ipv6_addr_flags::*; #[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))] @@ -58,8 +58,9 @@ pub(crate) use crate::os::bsd::ipv6_addr_flags::*; pub(crate) use crate::os::illumos::ipv6_addr_flags::*; // On Linux/Android flags come from netlink; this is only reached via the -// `unix_interfaces()` fallback when netlink is unavailable. -#[cfg(any(target_os = "linux", target_os = "android"))] +// `unix_interfaces()` fallback when netlink is unavailable. The iOS SDK has no +// `SIOCGIFAFLAG_IN6` (`netinet6/in6_var.h` ships for macOS only). +#[cfg(any(target_os = "linux", target_os = "android", target_os = "ios"))] pub(crate) fn get_ipv6_addr_flags(_ifname: &str, _addr: &std::net::Ipv6Addr) -> Ipv6AddrFlags { Ipv6AddrFlags::default() } diff --git a/src/os/darwin/mod.rs b/src/os/darwin/mod.rs index 3be5605..fcd4384 100644 --- a/src/os/darwin/mod.rs +++ b/src/os/darwin/mod.rs @@ -1,4 +1,5 @@ pub mod flags; +#[cfg(target_os = "macos")] pub mod ipv6_addr_flags; pub mod mtu; #[cfg(feature = "gateway")] diff --git a/src/os/ios/dns.rs b/src/os/ios/dns.rs index 850389a..e2a3c01 100644 --- a/src/os/ios/dns.rs +++ b/src/os/ios/dns.rs @@ -1,98 +1,72 @@ -//! DNS resolver lookup for Apple mobile targets. +//! DNS resolver lookup for iOS through libresolv. +//! +//! The SystemConfiguration dynamic store is `API_UNAVAILABLE(ios)`; +//! `res_ninit` / `res_getservers` are the public equivalent. -use std::net::IpAddr; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; -use objc2_core_foundation::{CFArray, CFDictionary, CFPropertyList, CFRetained, CFString, Type}; -use objc2_system_configuration::{ - SCDynamicStore, kSCDynamicStoreDomainState, kSCDynamicStorePropNetPrimaryService, kSCEntNetDNS, - kSCEntNetIPv4, kSCPropNetDNSServerAddresses, -}; +/// `struct __res_state` from ``, kept opaque: 552 bytes, 8-aligned +/// on 64-bit iOS. +#[repr(C, align(8))] +struct ResState([u8; 552]); -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) -} +/// `union res_sockaddr_union` from ``: 128 bytes, 4-aligned. +#[repr(C, align(4))] +#[derive(Clone, Copy)] +struct ResSockaddrUnion([u8; 128]); -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 }) -} +/// `MAXNS` from ``. +const MAXNS: usize = 3; -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) +#[link(name = "resolv")] +unsafe extern "C" { + fn res_9_ninit(state: *mut ResState) -> libc::c_int; + fn res_9_getservers( + state: *mut ResState, + set: *mut ResSockaddrUnion, + cnt: libc::c_int, + ) -> libc::c_int; + fn res_9_ndestroy(state: *mut ResState); } pub fn get_system_dns_conf() -> Vec { - let Some(store) = new_dynamic_store() else { - return Vec::new(); + let mut state = ResState([0; 552]); + let mut servers = [ResSockaddrUnion([0; 128]); MAXNS]; + // SAFETY: `state` is zeroed and as large and aligned as `struct __res_state`, + // as `res_ninit` requires, and destroyed only after a successful init. + // `servers` holds `MAXNS` unions, the count passed. + let count = unsafe { + if res_9_ninit(&mut state) != 0 { + return Vec::new(); + } + let count = res_9_getservers(&mut state, servers.as_mut_ptr(), MAXNS as libc::c_int); + res_9_ndestroy(&mut state); + count }; + servers + .iter() + .take(usize::try_from(count).unwrap_or(0)) + .filter_map(sockaddr_ip) + .collect() +} - let global = global_dns_servers(&store); - if !global.is_empty() { - return global; +fn sockaddr_ip(addr: &ResSockaddrUnion) -> Option { + // BSD `sockaddr`: `sa_len` then `sa_family`. + match i32::from(addr.0[1]) { + libc::AF_INET => { + // SAFETY: the union holds a `sockaddr_in` for `AF_INET` and is larger than it. + let sin: libc::sockaddr_in = + unsafe { std::ptr::read_unaligned(addr.0.as_ptr().cast()) }; + Some(IpAddr::V4(Ipv4Addr::from(u32::from_be( + sin.sin_addr.s_addr, + )))) + } + libc::AF_INET6 => { + // SAFETY: the union holds a `sockaddr_in6` for `AF_INET6` and is larger than it. + let sin6: libc::sockaddr_in6 = + unsafe { std::ptr::read_unaligned(addr.0.as_ptr().cast()) }; + Some(IpAddr::V6(Ipv6Addr::from(sin6.sin6_addr.s6_addr))) + } + _ => None, } - - primary_service_dns_servers(&store) } diff --git a/src/os/ios/interface.rs b/src/os/ios/interface.rs index 246dcfc..dfb27c3 100644 --- a/src/os/ios/interface.rs +++ b/src/os/ios/interface.rs @@ -33,8 +33,6 @@ pub fn interfaces() -> Vec { .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(); @@ -67,18 +65,6 @@ pub fn interfaces() -> Vec { } } - #[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() { - if iface.if_type.should_replace_with(sc_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) { @@ -106,15 +92,7 @@ pub fn interfaces() -> Vec { 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(); - } + iface.dns_servers = super::dns::get_system_dns_conf(); } } } diff --git a/src/os/ios/mod.rs b/src/os/ios/mod.rs index 7b346fa..2505e5f 100644 --- a/src/os/ios/mod.rs +++ b/src/os/ios/mod.rs @@ -1,6 +1,4 @@ -#[cfg(feature = "apple-system-configuration-extra")] +#[cfg(feature = "gateway")] pub mod dns; pub mod interface; pub mod network; -#[cfg(feature = "apple-system-configuration-extra")] -pub mod sc; diff --git a/src/os/ios/sc.rs b/src/os/ios/sc.rs deleted file mode 100644 index f498bd2..0000000 --- a/src/os/ios/sc.rs +++ /dev/null @@ -1,157 +0,0 @@ -//! 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 -}