Skip to content
Open
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ license = "MIT OR BSD-3-Clause"
name = "if-addrs"
readme = "README.md"
repository = "https://github.com/messense/if-addrs"
version = "0.15.0"
version = "0.16.0"
edition = "2021"

[target.'cfg(not(target_os = "windows"))'.dependencies]
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,22 @@ for iface in if_addrs::get_if_addrs().unwrap() {
}
```

Each `Interface` includes its IP address, index, operational status, whether it
is point-to-point, and its hardware (MAC) address when available:

```rust
for iface in if_addrs::get_if_addrs().unwrap() {
if let Some(mac) = iface.mac_addr() {
println!("{}: {}", iface.name, mac); // e.g. "eth0: 02:fc:00:00:00:01"
}
}
```

The MAC address is read from the same OS interface-enumeration call used for the
IP information (`getifaddrs` link-layer entries on POSIX, `GetAdaptersAddresses`
on Windows), so it stays cross-platform without reaching into `/sys` or other
platform-specific locations.

Get notifications for changes in network interfaces:

```rust
Expand Down
140 changes: 137 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,54 @@ impl From<i32> for IfOperStatus {
}
}

/// A 48-bit (6-byte) hardware (MAC) address of an interface.
///
/// This is obtained directly from the operating system's interface
/// enumeration APIs (`getifaddrs` on POSIX systems, `GetAdaptersAddresses`
/// on Windows), so it does not require reading from `/sys` or any other
/// platform-specific location.
///
/// Only 6-byte (Ethernet/EUI-48 style) addresses are represented. Interfaces
/// whose hardware address has a different length (for example InfiniBand) are
/// reported as `None` on the owning [`Interface`].
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, PartialOrd, Ord)]
pub struct MacAddr(pub [u8; 6]);

impl MacAddr {
/// Returns the six octets that make up this address.
#[must_use]
pub const fn octets(&self) -> [u8; 6] {
self.0
}

/// Returns `true` if all octets are zero.
///
/// Loopback and some virtual interfaces commonly report an all-zero
/// hardware address.
#[must_use]
pub const fn is_zero(&self) -> bool {
let o = self.0;
o[0] == 0 && o[1] == 0 && o[2] == 0 && o[3] == 0 && o[4] == 0 && o[5] == 0
}
}

impl From<[u8; 6]> for MacAddr {
fn from(octets: [u8; 6]) -> Self {
MacAddr(octets)
}
}

impl std::fmt::Display for MacAddr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let o = self.0;
write!(
f,
"{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
o[0], o[1], o[2], o[3], o[4], o[5]
)
}
}

/// Details about an interface on this host.
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct Interface {
Expand All @@ -98,6 +146,17 @@ pub struct Interface {
/// On Windows, this is derived from the interface type.
pub is_p2p: bool,

/// The hardware (MAC) address of the interface, if available.
///
/// This is sourced from the same OS interface-enumeration call used for
/// the IP information (`getifaddrs` link-layer entries on POSIX,
/// `GetAdaptersAddresses` on Windows), keeping the crate cross-platform
/// without reaching into `/sys` or other platform-specific locations.
///
/// It is `None` when the OS reports no hardware address for the interface
/// or when the address is not a 6-byte address (see [`MacAddr`]).
pub mac_addr: Option<MacAddr>,

/// (Windows only) A permanent and unique identifier for the interface. It
/// cannot be modified by the user. It is typically a GUID string of the
/// form: "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}", but this is not
Expand Down Expand Up @@ -136,6 +195,12 @@ impl Interface {
pub fn is_p2p(&self) -> bool {
self.is_p2p
}

/// Get the hardware (MAC) address of this interface, if available.
#[must_use]
pub fn mac_addr(&self) -> Option<MacAddr> {
self.mac_addr
}
}

/// Details about the address of an interface on this host.
Expand Down Expand Up @@ -236,10 +301,11 @@ impl Ifv6Addr {
mod getifaddrs_posix {
use libc::if_nametoindex;

use super::{IfAddr, Ifv4Addr, Ifv6Addr, Interface};
use super::{IfAddr, Ifv4Addr, Ifv6Addr, Interface, MacAddr};
use crate::posix::{self as ifaddrs, IfAddrs};
use crate::sockaddr;
use crate::IfOperStatus;
use std::collections::HashMap;
use std::ffi::CStr;
use std::io;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
Expand All @@ -262,6 +328,21 @@ mod getifaddrs_posix {
let mut ret = Vec::<Interface>::new();
let ifaddrs = IfAddrs::new()?;

// `getifaddrs` returns the hardware (MAC) address on a separate
// link-layer entry (`AF_PACKET` on Linux, `AF_LINK` on the BSDs/Apple)
// from the per-address `AF_INET`/`AF_INET6` entries. Collect those
// link-layer addresses first, keyed by interface name, so they can be
// attached to each address entry below.
let mut macs = HashMap::<String, MacAddr>::new();
for ifaddr in ifaddrs.iter() {
if let Some(mac) = sockaddr::to_mac(ifaddr.ifa_addr) {
let name = unsafe { CStr::from_ptr(ifaddr.ifa_name) }
.to_string_lossy()
.into_owned();
macs.insert(name, MacAddr(mac));
}
}

for ifaddr in ifaddrs.iter() {
let addr = match sockaddr::to_ipaddr(ifaddr.ifa_addr) {
None => continue,
Expand Down Expand Up @@ -341,12 +422,15 @@ mod getifaddrs_posix {

let is_p2p = ifaddr.ifa_flags & POSIX_IFF_POINTOPOINT != 0;

let mac_addr = macs.get(&name).copied();

ret.push(Interface {
name,
addr,
index,
oper_status,
is_p2p,
mac_addr,
});
}

Expand All @@ -362,7 +446,7 @@ pub fn get_if_addrs() -> io::Result<Vec<Interface>> {

#[cfg(windows)]
mod getifaddrs_windows {
use super::{IfAddr, Ifv4Addr, Ifv6Addr, Interface};
use super::{IfAddr, Ifv4Addr, Ifv6Addr, Interface, MacAddr};
use crate::sockaddr;
use crate::windows::IfAddrs;
use std::io;
Expand Down Expand Up @@ -490,6 +574,7 @@ mod getifaddrs_windows {
};
let oper_status = ifaddr.oper_status();
let is_p2p = ifaddr.is_p2p();
let mac_addr = ifaddr.mac_addr().map(MacAddr);

ret.push(Interface {
name: ifaddr.name(),
Expand All @@ -498,6 +583,7 @@ mod getifaddrs_windows {
oper_status,
adapter_name: ifaddr.adapter_name(),
is_p2p,
mac_addr,
});
}
}
Expand Down Expand Up @@ -657,6 +743,7 @@ mod tests {
name: String,
is_up: bool,
is_p2p: bool,
mac: Option<String>,
}

fn list_system_interfaces(cmd: &str, args: &[&str]) -> String {
Expand Down Expand Up @@ -775,6 +862,7 @@ mod tests {
name: name.to_string(),
is_up,
is_p2p,
mac: None,
});
}

Expand All @@ -797,7 +885,7 @@ mod tests {
None
})
.collect();
let mut intf_status_vec = Vec::new();
let mut intf_status_vec: Vec<IntfStatus> = Vec::new();
for line in intf_list.lines() {
if !line.starts_with(' ') && !line.is_empty() {
let name_s: Vec<&str> = line.split(':').collect();
Expand All @@ -807,7 +895,15 @@ mod tests {
name: name_s[1].trim().to_string(),
is_up,
is_p2p,
mac: None,
});
} else if let Some(rest) = line.trim().strip_prefix("link/ether ") {
// e.g. "link/ether 02:fc:00:00:00:01 brd ff:ff:ff:ff:ff:ff"
if let Some(mac) = rest.split_whitespace().next() {
if let Some(current) = intf_status_vec.last_mut() {
current.mac = Some(mac.to_lowercase());
}
}
}
}

Expand Down Expand Up @@ -866,12 +962,20 @@ mod tests {
name: name_s[0].to_string(),
is_up: is_admin_up,
is_p2p,
mac: None,
};
intf_status_vec.push(status);
} else if line.contains("status: inactive") {
if let Some(current_intf) = intf_status_vec.last_mut() {
current_intf.is_up = false; // overwrite the admin up
}
} else if let Some(rest) = line.trim().strip_prefix("ether ") {
// e.g. "ether c6:0e:5e:f8:5d:f4"
if let Some(mac) = rest.split_whitespace().next() {
if let Some(current_intf) = intf_status_vec.last_mut() {
current_intf.mac = Some(mac.to_lowercase());
}
}
}
}

Expand Down Expand Up @@ -939,11 +1043,41 @@ mod tests {
);
}
assert_eq!(interface.is_p2p(), intf_status.is_p2p);

// When the system tool reports a MAC for this interface,
// the crate must report the same one. Asserting `Some`
// here (rather than silently skipping when `mac_addr()` is
// `None`) catches a regression where the crate fails to
// retrieve a MAC that the OS does expose.
if let Some(expected_mac) = &intf_status.mac {
let mac = interface.mac_addr().unwrap_or_else(|| {
panic!(
"interface {} has MAC {} per the system tool, \
but the crate reported none",
intf_status.name, expected_mac
)
});
assert_eq!(&mac.to_string(), expected_mac);
}
}
}
}
}

#[test]
fn test_mac_addr_display() {
use super::MacAddr;
let mac = MacAddr([0x02, 0xfc, 0x00, 0x0a, 0xbc, 0xff]);
assert_eq!(mac.to_string(), "02:fc:00:0a:bc:ff");
assert_eq!(mac.octets(), [0x02, 0xfc, 0x00, 0x0a, 0xbc, 0xff]);
assert!(!mac.is_zero());
assert!(MacAddr([0; 6]).is_zero());
assert_eq!(
MacAddr::from([1, 2, 3, 4, 5, 6]),
MacAddr([1, 2, 3, 4, 5, 6])
);
}

#[cfg(not(any(
all(
target_vendor = "apple",
Expand Down
78 changes: 78 additions & 0 deletions src/sockaddr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,84 @@ pub fn to_ipaddr(sockaddr: *const sockaddr) -> Option<IpAddr> {
SockAddr::new(sockaddr)?.as_ipaddr()
}

/// Extract a 6-byte hardware (MAC) address from a link-layer `sockaddr`, if it
/// is one.
///
/// On Linux/Android the link-layer address is carried in a `sockaddr_ll`
/// (`AF_PACKET`); on the BSDs, Apple platforms and illumos it is carried in a
/// `sockaddr_dl` (`AF_LINK`). Returns `None` for non-link-layer addresses or
/// for hardware addresses that are not 6 bytes long.
#[cfg(not(windows))]
#[allow(unsafe_code, clippy::cast_ptr_alignment, clippy::needless_return)]
pub fn to_mac(sockaddr: *const sockaddr) -> Option<[u8; 6]> {
let sa = SockAddr::new(sockaddr)?;

#[cfg(any(target_os = "linux", target_os = "android"))]
{
if sa.sa_family() != libc::AF_PACKET as u32 {
return None;
}
let sll = unsafe { &*(sa.inner.as_ptr() as *const libc::sockaddr_ll) };
if sll.sll_halen != 6 {
return None;
}
let a = sll.sll_addr;
return Some([a[0], a[1], a[2], a[3], a[4], a[5]]);
}

#[cfg(not(any(target_os = "linux", target_os = "android")))]
{
if sa.sa_family() != libc::AF_LINK as u32 {
return None;
}
let sdl = unsafe { &*(sa.inner.as_ptr() as *const libc::sockaddr_dl) };
if sdl.sdl_alen != 6 {
return None;
}
// `sockaddr_dl` is a variable-length structure: the interface name
// (`sdl_nlen` bytes) is followed by the link-layer address
// (`sdl_alen` bytes) starting at `sdl_data`. `sdl_data` is only a
// fixed-size minimum work area in libc (e.g. `[c_char; 12]` on Apple),
// so a name of 6+ bytes (e.g. "bridge0") pushes the MAC past that
// array's compile-time bound. Read it via pointer arithmetic from the
// start of `sdl_data` instead of relying on `sdl_data.len()`.
let start = sdl.sdl_nlen as usize;
let sdl_ptr = sdl as *const libc::sockaddr_dl as *const u8;
let data_offset = unsafe {
std::ptr::addr_of!(sdl.sdl_data)
.cast::<u8>()
.offset_from(sdl_ptr)
} as usize;

// On the BSDs and Apple platforms the structure carries its own byte
// length in `sdl_len`; use it to bound the read. illumos/Solaris
// `sockaddr_dl` has no `sdl_len` field, so it is excluded here and
// relies on `getifaddrs` having populated `sdl_nlen + sdl_alen` bytes.
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "tvos",
target_os = "watchos",
target_os = "visionos",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "dragonfly",
))]
{
if (sdl.sdl_len as usize) < data_offset + start + 6 {
return None;
}
}

let mut mac = [0u8; 6];
unsafe {
std::ptr::copy_nonoverlapping(sdl_ptr.add(data_offset + start), mac.as_mut_ptr(), 6);
}
return Some(mac);
}
}

// Wrapper around a sockaddr pointer. Guaranteed to not be null.
struct SockAddr {
inner: NonNull<sockaddr>,
Expand Down
Loading
Loading