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
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,22 @@ jobs:
- name: Run cargo test
run: cargo test

- name: Test with all features
run: cargo test --all-features

- name: Test without default features
run: cargo test --no-default-features

- name: Check documentation links with all features
env:
RUSTDOCFLAGS: "-D warnings"
run: cargo doc --no-deps --all-features

- name: Check documentation links without default features
env:
RUSTDOCFLAGS: "-D warnings"
run: cargo doc --no-deps --no-default-features

- name: Run cargo check with all features
run: cargo check --all-features

Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "netdev"
version = "0.46.3"
version = "0.47.0"
authors = ["shellrow <shellrow@foctal.com>"]
edition = "2024"
description = "Cross-platform library for enumerating network interfaces with metadata."
Expand Down
34 changes: 17 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ See the [Interface][doc-interface-url] struct documentation for more details.
Add `netdev` to your `Cargo.toml`:
```toml
[dependencies]
netdev = "0.46"
netdev = "0.47"
```

For more details, see [examples][examples-url] or [doc][doc-url].
Expand All @@ -48,20 +48,20 @@ For more details, see [examples][examples-url] or [doc][doc-url].
- `gateway` (default)
- Enables default interface and default gateway helpers.
- `apple-system-configuration-extra` (default)
- Enables deeper Apple metadata enrichment using `SystemConfiguration` APIs.
- 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.
- Enables additional `objc2-system-configuration` dependency bindings on macOS.
- Currently, this flag does not change metadata collection: macOS reads interface names and DHCP hints from SystemConfiguration plist files, with a basic SystemConfiguration API fallback, regardless of this flag. It has no effect on iOS.
- `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.
- `apple-wifi-extra`
- Enables macOS Wi-Fi transmit-speed collection through CoreWLAN.
- This feature is disabled by default because CoreWLAN metadata collection may perform synchronous IPC during interface enumeration.

To opt out of the additional Apple metadata enrichment while keeping gateway helpers:
To disable default feature flags while keeping gateway helpers (macOS metadata collection remains enabled):

```toml
[dependencies]
netdev = { version = "0.46", default-features = false, features = ["gateway"] }
netdev = { version = "0.47", default-features = false, features = ["gateway"] }
```

## Apple behavior
Expand All @@ -74,7 +74,7 @@ macOS Wi-Fi transmit-speed collection is disabled by default. To enable it, opt

```toml
[dependencies]
netdev = { version = "0.46", features = ["apple-wifi-extra"] }
netdev = { version = "0.47", features = ["apple-wifi-extra"] }
```

This uses CoreWLAN, whose metadata lookup may perform synchronous IPC while interfaces are
Expand All @@ -90,7 +90,7 @@ and was later rebranded to `netdev` by the author for future expansion, clearer
## Tested on
- Linux
- Ubuntu
- 24.04
- 24.04.5
- 22.04
- 21.10
- 20.04
Expand All @@ -107,20 +107,20 @@ and was later rebranded to `netdev` by the author for future expansion, clearer
- 14.7.6
- macOS (Intel)
- 13.4.1
- 11.6
- Windows
- 11 25H2 26200.8037
- 11 25H2 26200.9711
- 11 24H2 26100.6584
- 11 23H2 22631.4602
- 11 Pro 22H2 22621.3155
- 11 22H2 22621.3155
- 10 21H2 19044.1586
- FreeBSD
- 14
- 14.4
- OpenBSD
- 7.9
- NetBSD
- 10.1
- Android (arm64)
- 16.0
- 17
- 16
- Android (x86_64)
- 16.0
- 16
- iOS
- 26.6.2
- 18.6.2
- 18.1.1
29 changes: 20 additions & 9 deletions scripts/build-all.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,25 @@ $targets = @(
"x86_64-linux-android"
)

# cross build
foreach ($target in $targets) {
Write-Host "==> Building for $target..."
$result = & cross build --target $target
Write-Host "✅ Success: $target"
if ($LASTEXITCODE -ne 0) {
Write-Error "❌ Build failed for $target"
exit 1
# Build scripts and proc macros run inside the container. Keep their artifacts
# separate because cross images can have incompatible host glibc versions.
$originalTargetDir = $env:CARGO_TARGET_DIR
$targetDir = if ([string]::IsNullOrEmpty($originalTargetDir)) { "target" } else { $originalTargetDir }
$buildRoot = Join-Path $targetDir "build"

try {
foreach ($target in $targets) {
$env:CARGO_TARGET_DIR = Join-Path $buildRoot $target
Write-Host "==> Building for $target..."
& cross build --target $target
if ($LASTEXITCODE -ne 0) {
Write-Error "❌ Build failed for $target"
exit 1
}
Write-Host "✅ Success: $target"
}
Write-Host "✅ All builds succeeded."
} finally {
# Restore the caller's environment even when a build fails.
$env:CARGO_TARGET_DIR = $originalTargetDir
}
Write-Host "✅ All builds succeeded."
6 changes: 5 additions & 1 deletion scripts/build-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ TARGETS=(
x86_64-linux-android
)

# Build scripts and proc macros run inside the container. Keep their artifacts
# separate because cross images can have incompatible host glibc versions.
BUILD_ROOT="${CARGO_TARGET_DIR:-target}/build"

# cross build
for target in "${TARGETS[@]}"; do
echo "==> Building for $target..."
if cross build --target "$target"; then
if CARGO_TARGET_DIR="$BUILD_ROOT/$target" cross build --target "$target"; then
echo "✅ Success: $target"
else
echo "❌ Failed: $target"
Expand Down
9 changes: 6 additions & 3 deletions src/interface/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,12 @@ pub struct Interface {
pub transmit_speed: Option<u64>,
/// Reported receive link speed in bits per second.
///
/// This field follows the same availability rules as `Interface::transmit_speed`.
/// Availability is platform- and driver-dependent. On macOS, CoreWLAN supplies
/// only transmit speed, so enabling `apple-wifi-extra` does not add receive speed.
pub receive_speed: Option<u64>,
/// Whether this interface was configured to auto-negotiate the link speed.
///
/// It may `None` if reading this information has not been implemented for a specific OS.
/// It may be `None` if reading this information has not been implemented for a specific OS.
pub auto_negotiate: Option<bool>,
/// Whether this interface is configured to use DHCP for IPv4.
///
Expand Down Expand Up @@ -113,7 +114,9 @@ pub struct Interface {
pub gateway: Option<NetworkDevice>,
/// DNS resolver addresses associated with this interface.
///
/// This field is available only with the `gateway` feature.
/// This field is available only with the `gateway` feature. On platforms without
/// per-interface resolver information, system resolvers are attached to the default
/// interface and may include local stub resolver addresses.
#[cfg(feature = "gateway")]
pub dns_servers: Vec<IpAddr>,
/// Maximum transmission unit in bytes, when available.
Expand Down
8 changes: 0 additions & 8 deletions src/interface/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,6 @@ pub(crate) fn pick_default_iface_index(ifaces: &[Interface], local_ip: IpAddr) -
subnet_candidate
}

#[cfg(feature = "gateway")]
pub(crate) fn iface_has_ip(iface: &Interface, local_ip: IpAddr) -> bool {
match local_ip {
IpAddr::V4(ipv4) => iface.ipv4.iter().any(|x| x.addr() == ipv4),
IpAddr::V6(ipv6) => iface.ipv6.iter().any(|x| x.addr() == ipv6),
}
}

#[cfg(feature = "gateway")]
pub(crate) fn resolve_default_interface(mut ifaces: Vec<Interface>) -> Result<Interface, String> {
use crate::net::ip::get_local_ipaddr;
Expand Down
9 changes: 8 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@
//! Main entry points:
//! - [`get_interfaces`] returns a snapshot of all visible interfaces.
//! - [`Interface`] represents one interface and its collected metadata.
//! - [`get_default_interface`] and [`get_default_gateway`] are available with the `gateway` feature (default).
#![cfg_attr(
feature = "gateway",
doc = "- [`get_default_interface`] and [`get_default_gateway`] are available with the `gateway` feature (default)."
)]
#![cfg_attr(
not(feature = "gateway"),
doc = "- `get_default_interface` and `get_default_gateway` are available with the `gateway` feature (default)."
)]
//!
pub mod interface;
pub mod net;
Expand Down
34 changes: 31 additions & 3 deletions src/net/ip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ fn try_ipv6() -> Option<IpAddr> {
/// that would be used to reach a non-routable destination. No application data is sent.
///
/// The returned address is useful for identifying the interface currently preferred for the
/// default route.
/// default route. More-specific routes to the probe destinations (10.254.254.254 or
/// fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff) can select a different interface.
///
/// Returns `None` when socket creation, address selection, or local address inspection fails.
#[cfg(feature = "gateway")]
Expand Down Expand Up @@ -105,15 +106,17 @@ pub(crate) fn is_global_ipv6(ipv6_addr: &Ipv6Addr) -> bool {
|| matches!(ipv6_addr.segments(), [0, 0, 0, 0, 0, 0xffff, _, _])
// IPv4-IPv6 Translat. (`64:ff9b:1::/48`)
|| matches!(ipv6_addr.segments(), [0x64, 0xff9b, 1, _, _, _, _, _])
// Discard-Only Address Block (`100::/64`)
|| matches!(ipv6_addr.segments(), [0x100, 0, 0, 0, _, _, _, _])
// Discard-Only Address Block (`100::/64`) and Dummy IPv6 Prefix (`100:0:0:1::/64`)
|| matches!(ipv6_addr.segments(), [0x100, 0, 0, 0..=1, _, _, _, _])
// IETF Protocol Assignments (`2001::/23`)
|| (matches!(ipv6_addr.segments(), [0x2001, b, _, _, _, _, _, _] if b < 0x200)
&& !(
// Port Control Protocol Anycast (`2001:1::1`)
u128::from_be_bytes(ipv6_addr.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0001
// Traversal Using Relays around NAT Anycast (`2001:1::2`)
|| u128::from_be_bytes(ipv6_addr.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0002
// DNS-SD Service Registration Protocol Anycast (`2001:1::3`)
|| u128::from_be_bytes(ipv6_addr.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0003
// AMT (`2001:3::/32`)
|| matches!(ipv6_addr.segments(), [0x2001, 3, _, _, _, _, _, _])
// AS112-v6 (`2001:4:112::/48`)
Expand All @@ -125,6 +128,8 @@ pub(crate) fn is_global_ipv6(ipv6_addr: &Ipv6Addr) -> bool {
// 6to4 (`2002::/16`) - it's not explicitly documented as globally reachable,
// IANA says N/A.
|| matches!(ipv6_addr.segments(), [0x2002, _, _, _, _, _, _, _])
// Segment Routing (SRv6) SIDs (`5f00::/16`)
|| ipv6_addr.segments()[0] == 0x5f00
|| is_documentation_ipv6(ipv6_addr)
|| ipv6_addr.is_unique_local()
|| ipv6_addr.is_unicast_link_local())
Expand Down Expand Up @@ -193,6 +198,29 @@ mod tests {
assert!(!is_global_ipv6(&doc));
}

#[test]
fn special_purpose_ipv6_boundaries() {
for address in [
"100:0:0:1::",
"100:0:0:1:ffff:ffff:ffff:ffff",
"5f00::",
"5f00:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
"2001:1::4",
] {
assert!(!is_global_ipv6(&address.parse().unwrap()), "{address}");
}
for address in [
"100:0:0:2::",
"5eff:ffff::",
"5f01::",
"2001:1::1",
"2001:1::2",
"2001:1::3",
] {
assert!(is_global_ipv6(&address.parse().unwrap()), "{address}");
}
}

#[test]
fn test_is_global_ip() {
let ip_v4 = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1));
Expand Down
49 changes: 3 additions & 46 deletions src/os/android/netlink.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
use crate::interface::types::InterfaceType;
use crate::os::linux::netlink_data::ip_from_addr;
use crate::os::linux::netlink_data::stats_from_link;
use crate::stats::counters::InterfaceStats;
use netlink_packet_core::NetlinkPayload;
use netlink_packet_route::{
RouteNetlinkMessage,
address::{AddressAttribute, AddressFlags, AddressMessage},
address::AddressMessage,
link::{LinkAttribute, LinkMessage},
};
use netlink_sys::{Socket, protocols::NETLINK_ROUTE};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::time::SystemTime;
use std::{collections::HashMap, io};

#[cfg(feature = "gateway")]
Expand Down Expand Up @@ -123,27 +124,6 @@ fn name_from_link(link: &LinkMessage) -> Option<String> {
None
}

fn ip_from_addr(addr: &AddressMessage) -> Option<(IpAddr, u8, u32)> {
let pfx = addr.header.prefix_len;
let mut ip_out = None;
let mut flags: Option<AddressFlags> = None;
for nla in &addr.attributes {
match nla {
AddressAttribute::Local(ip) | AddressAttribute::Address(ip) => {
ip_out = Some(*ip);
}
AddressAttribute::Flags(f) => {
flags = Some(*f);
}
_ => {}
}
}
let addr_flags = flags
.map(|f| f.bits())
.unwrap_or(addr.header.flags.bits() as u32);
ip_out.map(|ip| (ip, pfx, addr_flags))
}

#[cfg(feature = "gateway")]
fn route_addr_to_ip(a: &RouteAddress) -> Option<IpAddr> {
match a {
Expand Down Expand Up @@ -230,29 +210,6 @@ fn if_type_from_link(link: &LinkMessage, _name: &str) -> InterfaceType {
InterfaceType::try_from(arphrd).unwrap_or(InterfaceType::UnknownWithValue(arphrd))
}

fn stats_from_link(link: &LinkMessage) -> Option<InterfaceStats> {
for nla in &link.attributes {
match nla {
LinkAttribute::Stats64(s) => {
return Some(InterfaceStats {
rx_bytes: s.rx_bytes,
tx_bytes: s.tx_bytes,
timestamp: Some(SystemTime::now()),
});
}
LinkAttribute::Stats(s) => {
return Some(InterfaceStats {
rx_bytes: s.rx_bytes as u64,
tx_bytes: s.tx_bytes as u64,
timestamp: Some(SystemTime::now()),
});
}
_ => {}
}
}
None
}

pub fn get_flags_by_name(name: &str) -> io::Result<Option<u32>> {
let links = dump_links()?;
for l in links {
Expand Down
Loading
Loading