Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion build.rs
Original file line number Diff line number Diff line change
@@ -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");
}
}
7 changes: 4 additions & 3 deletions src/interface/ipv6_addr_flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"))]
Expand All @@ -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()
}
1 change: 1 addition & 0 deletions src/os/darwin/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod flags;
#[cfg(target_os = "macos")]
pub mod ipv6_addr_flags;
pub mod mtu;
#[cfg(feature = "gateway")]
Expand Down
146 changes: 60 additions & 86 deletions src/os/ios/dns.rs
Original file line number Diff line number Diff line change
@@ -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 `<resolv.h>`, kept opaque: 552 bytes, 8-aligned
/// on 64-bit iOS.
#[repr(C, align(8))]
struct ResState([u8; 552]);

fn new_dynamic_store() -> Option<CFRetained<SCDynamicStore>> {
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<T: Type>(value: CFRetained<CFPropertyList>) -> CFRetained<T> {
// 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<CFDictionary<CFString, CFArray<CFString>>>,
) -> Vec<IpAddr> {
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::<IpAddr>() {
if !out.contains(&ip) {
out.push(ip);
}
}
}
out
}

fn dns_servers_for_key(store: &SCDynamicStore, key: &CFString) -> Vec<IpAddr> {
let Some(value) = SCDynamicStore::value(Some(store), key) else {
return Vec::new();
};
let dict: CFRetained<CFDictionary<CFString, CFArray<CFString>>> = cast_property_list(value);
parse_dns_server_addresses(dict)
}

fn global_dns_servers(store: &SCDynamicStore) -> Vec<IpAddr> {
let key = SCDynamicStore::key_create_network_global_entity(
None,
unsafe { kSCDynamicStoreDomainState },
unsafe { kSCEntNetDNS },
);
dns_servers_for_key(store, &key)
}
/// `union res_sockaddr_union` from `<resolv.h>`: 128 bytes, 4-aligned.
#[repr(C, align(4))]
#[derive(Clone, Copy)]
struct ResSockaddrUnion([u8; 128]);

fn primary_service_id(store: &SCDynamicStore) -> Option<CFRetained<CFString>> {
let key = SCDynamicStore::key_create_network_global_entity(
None,
unsafe { kSCDynamicStoreDomainState },
unsafe { kSCEntNetIPv4 },
);
let value = SCDynamicStore::value(Some(store), &key)?;
let dict: CFRetained<CFDictionary<CFString, CFString>> = cast_property_list(value);
dict.get(unsafe { kSCDynamicStorePropNetPrimaryService })
}
/// `MAXNS` from `<resolv.h>`.
const MAXNS: usize = 3;

fn primary_service_dns_servers(store: &SCDynamicStore) -> Vec<IpAddr> {
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<IpAddr> {
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<IpAddr> {
// 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)
}
24 changes: 1 addition & 23 deletions src/os/ios/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ pub fn interfaces() -> Vec<Interface> {
.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();
Expand Down Expand Up @@ -67,18 +65,6 @@ pub fn interfaces() -> Vec<Interface> {
}
}

#[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) {
Expand Down Expand Up @@ -106,15 +92,7 @@ pub fn interfaces() -> Vec<Interface> {
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();
}
}
}
Expand Down
4 changes: 1 addition & 3 deletions src/os/ios/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Loading
Loading