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
10 changes: 8 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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"
Expand Down
28 changes: 23 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand All @@ -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
Expand All @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -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");
}
}
2 changes: 1 addition & 1 deletion src/interface/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u64>,
Expand Down
3 changes: 1 addition & 2 deletions src/interface/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,7 @@ pub(crate) fn interfaces() -> Vec<Interface> {
{
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()
}
Expand Down
98 changes: 98 additions & 0 deletions src/os/ios/dns.rs
Original file line number Diff line number Diff line change
@@ -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<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)
}

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 })
}

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)
}

pub fn get_system_dns_conf() -> Vec<IpAddr> {
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)
}
78 changes: 73 additions & 5 deletions src/os/ios/interface.rs
Original file line number Diff line number Diff line change
@@ -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<Interface> {
let mut ifaces: Vec<Interface> = 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 {
Expand All @@ -27,20 +65,50 @@ pub fn interfaces() -> Vec<Interface> {
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();
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/os/ios/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Loading
Loading