use std::{
collections::HashMap,
ffi::CString,
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
time::Instant,
};
use nix::libc;
const IFT_ETHER: u8 = 6;
const SIOCGIFFLAGS: libc::c_ulong = 0xc0206911;
const SIOCGIFMTU: libc::c_ulong = 0xc0206933;
const SIOCGIFADDR: libc::c_ulong = 0xc0206921;
use tracing::{debug, error, info, warn};
use crate::candidate_discovery::{NetworkInterface, NetworkInterfaceDiscovery};
pub struct MacOSInterfaceDiscovery {
cached_interfaces: HashMap<String, MacOSInterface>,
last_scan_time: Option<Instant>,
cache_ttl: std::time::Duration,
scan_state: ScanState,
pub sc_store: Option<SCDynamicStoreRef>,
run_loop_source: Option<CFRunLoopSourceRef>,
interface_config: InterfaceConfig,
network_changed: bool,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct MacOSInterface {
name: String,
#[allow(dead_code)]
display_name: String,
hardware_type: HardwareType,
state: InterfaceState,
ipv4_addresses: Vec<Ipv4Addr>,
ipv6_addresses: Vec<Ipv6Addr>,
flags: InterfaceFlags,
mtu: u32,
#[allow(dead_code)]
hardware_address: Option<[u8; 6]>,
#[allow(dead_code)]
last_updated: Instant,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
enum HardwareType {
Ethernet,
WiFi,
Bluetooth,
Cellular,
Loopback,
PPP,
VPN,
Bridge,
Thunderbolt,
USB,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InterfaceState {
Active,
Inactive,
Unknown,
}
#[derive(Debug, Clone, Copy, Default)]
struct InterfaceFlags {
is_up: bool,
#[allow(dead_code)]
is_active: bool,
is_wireless: bool,
is_loopback: bool,
#[allow(dead_code)]
supports_ipv4: bool,
#[allow(dead_code)]
supports_ipv6: bool,
is_builtin: bool,
}
#[derive(Debug, Clone, PartialEq)]
enum ScanState {
Idle,
InProgress { started_at: Instant },
Completed { scan_results: Vec<NetworkInterface> },
Failed { error: String },
}
#[derive(Debug, Clone)]
pub(crate) struct InterfaceConfig {
include_inactive: bool,
include_loopback: bool,
include_ipv6: bool,
builtin_only: bool,
min_mtu: u32,
#[allow(dead_code)]
max_interfaces: u32,
}
#[derive(Debug, Clone)]
pub enum MacOSNetworkError {
SystemConfigurationError {
function: &'static str,
message: String,
},
InterfaceNotFound { interface_name: String },
InvalidInterfaceConfig {
interface_name: String,
reason: String,
},
ServiceEnumerationFailed { reason: String },
AddressParsingFailed { address: String, reason: String },
DynamicStoreAccessFailed { reason: String },
RunLoopSourceCreationFailed { reason: String },
DynamicStoreConfigurationFailed {
operation: &'static str,
reason: String,
},
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy)]
pub struct SCDynamicStoreRef(*mut std::ffi::c_void);
unsafe impl Send for SCDynamicStoreRef {}
#[repr(transparent)]
#[derive(Debug, Clone, Copy)]
struct CFRunLoopSourceRef(*mut std::ffi::c_void);
unsafe impl Send for CFRunLoopSourceRef {}
type CFStringRef = *mut std::ffi::c_void;
type CFRunLoopRef = *mut std::ffi::c_void;
type CFArrayRef = *mut std::ffi::c_void;
type CFAllocatorRef = *mut std::ffi::c_void;
#[repr(C)]
struct SCDynamicStoreContext {
version: i64,
info: *mut std::ffi::c_void,
retain: Option<extern "C" fn(*mut std::ffi::c_void) -> *mut std::ffi::c_void>,
release: Option<extern "C" fn(*mut std::ffi::c_void)>,
copyDescription: Option<extern "C" fn(*mut std::ffi::c_void) -> CFStringRef>,
}
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
#[link_name = "kCFRunLoopDefaultMode"]
static kCFRunLoopDefaultMode: CFStringRef;
#[link_name = "kCFAllocatorDefault"]
static kCFAllocatorDefault: CFAllocatorRef;
}
extern "C" fn network_change_callback(
_store: SCDynamicStoreRef,
_changed_keys: CFArrayRef,
info: *mut std::ffi::c_void,
) {
if !info.is_null() {
unsafe {
let discovery = &mut *(info as *mut MacOSInterfaceDiscovery);
discovery.network_changed = true;
debug!("Network change detected via callback");
}
}
}
#[link(name = "SystemConfiguration", kind = "framework")]
unsafe extern "C" {
fn SCDynamicStoreCreate(
allocator: CFAllocatorRef,
name: CFStringRef,
callback: Option<extern "C" fn(SCDynamicStoreRef, CFArrayRef, *mut std::ffi::c_void)>,
context: *mut SCDynamicStoreContext,
) -> SCDynamicStoreRef;
fn SCDynamicStoreCreateRunLoopSource(
allocator: CFAllocatorRef,
store: SCDynamicStoreRef,
order: i32,
) -> CFRunLoopSourceRef;
fn SCDynamicStoreSetNotificationKeys(
store: SCDynamicStoreRef,
keys: CFArrayRef,
patterns: CFArrayRef,
) -> bool;
#[allow(dead_code)]
fn SCDynamicStoreCopyKeyList(store: SCDynamicStoreRef, pattern: CFStringRef) -> CFArrayRef;
#[allow(dead_code)]
fn SCDynamicStoreCopyValue(store: SCDynamicStoreRef, key: CFStringRef)
-> *mut std::ffi::c_void;
fn SCPreferencesCreate(
allocator: CFAllocatorRef,
name: CFStringRef,
prefs_id: CFStringRef,
) -> *mut std::ffi::c_void;
fn SCNetworkServiceCopyAll(prefs: *mut std::ffi::c_void, ) -> CFArrayRef;
fn SCNetworkServiceGetInterface(
service: *mut std::ffi::c_void, ) -> *mut std::ffi::c_void;
fn SCNetworkInterfaceGetBSDName(
interface: *mut std::ffi::c_void, ) -> CFStringRef;
}
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
fn CFRelease(cf: *mut std::ffi::c_void);
#[allow(dead_code)]
fn CFRetain(cf: *mut std::ffi::c_void) -> *mut std::ffi::c_void;
fn CFRunLoopGetCurrent() -> CFRunLoopRef;
fn CFRunLoopAddSource(rl: CFRunLoopRef, source: CFRunLoopSourceRef, mode: CFStringRef);
fn CFRunLoopRemoveSource(rl: CFRunLoopRef, source: CFRunLoopSourceRef, mode: CFStringRef);
fn CFStringCreateWithCString(
allocator: CFAllocatorRef,
cstr: *const std::ffi::c_char,
encoding: u32,
) -> CFStringRef;
fn CFArrayGetCount(array: CFArrayRef) -> i64;
fn CFArrayGetValueAtIndex(array: CFArrayRef, idx: i64) -> *mut std::ffi::c_void;
fn CFArrayCreate(
allocator: CFAllocatorRef,
values: *const *const std::ffi::c_void,
num_values: i64,
callbacks: *const std::ffi::c_void,
) -> CFArrayRef;
#[allow(dead_code)]
fn CFGetTypeID(cf: *mut std::ffi::c_void) -> u64;
#[allow(dead_code)]
fn CFStringGetTypeID() -> u64;
fn CFStringGetCString(
string: CFStringRef,
buffer: *mut std::ffi::c_char,
buffer_size: i64,
encoding: u32,
) -> bool;
fn CFStringGetLength(string: CFStringRef) -> i64;
}
const kCFStringEncodingUTF8: u32 = 0x08000100;
const kCFTypeArrayCallBacks: *const std::ffi::c_void = std::ptr::null();
unsafe fn cf_string_to_rust_string(cf_str: CFStringRef) -> Option<String> {
unsafe {
if cf_str.is_null() {
return None;
}
let length = CFStringGetLength(cf_str);
if length == 0 {
return Some(String::new());
}
let mut buffer = vec![0u8; (length as usize + 1) * 4]; let success = CFStringGetCString(
cf_str,
buffer.as_mut_ptr() as *mut std::ffi::c_char,
buffer.len() as i64,
kCFStringEncodingUTF8,
);
if success {
let null_pos = buffer.iter().position(|&b| b == 0).unwrap_or(buffer.len());
String::from_utf8(buffer[..null_pos].to_vec()).ok()
} else {
None
}
}
}
#[allow(clippy::panic)]
unsafe fn rust_string_to_cf_string(s: &str) -> CFStringRef {
unsafe {
let c_str = CString::new(s).unwrap_or_else(|_| panic!("string should be valid UTF-8"));
CFStringCreateWithCString(kCFAllocatorDefault, c_str.as_ptr(), kCFStringEncodingUTF8)
}
}
impl MacOSInterfaceDiscovery {
pub fn new() -> Self {
Self {
cached_interfaces: HashMap::new(),
last_scan_time: None,
cache_ttl: std::time::Duration::from_secs(30),
scan_state: ScanState::Idle,
sc_store: None,
run_loop_source: None,
interface_config: InterfaceConfig {
include_inactive: false,
include_loopback: false,
include_ipv6: true,
builtin_only: false,
min_mtu: 1280, max_interfaces: 32,
},
network_changed: false,
}
}
#[allow(dead_code)]
pub(crate) fn set_interface_config(&mut self, config: InterfaceConfig) {
self.interface_config = config;
}
#[allow(clippy::panic)]
pub fn initialize_dynamic_store(&mut self) -> Result<(), MacOSNetworkError> {
if self.sc_store.is_some() {
return Ok(());
}
let store_name = CString::new("ant-quic-network-discovery")
.unwrap_or_else(|_| panic!("hardcoded store name should be valid"));
let sc_store = unsafe {
self.create_dynamic_store(store_name.as_ptr())
};
if sc_store.0.is_null() {
return Err(MacOSNetworkError::DynamicStoreAccessFailed {
reason: "Failed to create SCDynamicStore".to_string(),
});
}
self.sc_store = Some(sc_store);
debug!("System Configuration dynamic store initialized");
Ok(())
}
#[allow(clippy::panic)]
pub fn enable_change_monitoring(&mut self) -> Result<(), MacOSNetworkError> {
if self.run_loop_source.is_some() {
return Ok(());
}
self.initialize_dynamic_store()?;
let sc_store = self
.sc_store
.as_ref()
.unwrap_or_else(|| panic!("dynamic store should be initialized"));
unsafe {
let keys = Vec::<CFStringRef>::new();
let mut patterns = Vec::new();
let ipv4_pattern = rust_string_to_cf_string("State:/Network/Interface/.*/IPv4");
let ipv6_pattern = rust_string_to_cf_string("State:/Network/Interface/.*/IPv6");
let link_pattern = rust_string_to_cf_string("State:/Network/Interface/.*/Link");
patterns.push(ipv4_pattern);
patterns.push(ipv6_pattern);
patterns.push(link_pattern);
let keys_array = CFArrayCreate(
kCFAllocatorDefault,
keys.as_ptr() as *const *const std::ffi::c_void,
keys.len() as i64,
kCFTypeArrayCallBacks,
);
let patterns_array = CFArrayCreate(
kCFAllocatorDefault,
patterns.as_ptr() as *const *const std::ffi::c_void,
patterns.len() as i64,
kCFTypeArrayCallBacks,
);
let success = SCDynamicStoreSetNotificationKeys(*sc_store, keys_array, patterns_array);
for pattern in patterns {
CFRelease(pattern);
}
if !keys_array.is_null() {
CFRelease(keys_array);
}
if !patterns_array.is_null() {
CFRelease(patterns_array);
}
if !success {
return Err(MacOSNetworkError::DynamicStoreConfigurationFailed {
operation: "SCDynamicStoreSetNotificationKeys",
reason: "Failed to set notification keys".to_string(),
});
}
}
let run_loop_source = unsafe { self.create_run_loop_source(sc_store) };
if run_loop_source.0.is_null() {
return Err(MacOSNetworkError::RunLoopSourceCreationFailed {
reason: "Failed to create run loop source".to_string(),
});
}
self.run_loop_source = Some(run_loop_source);
debug!("Network change monitoring enabled");
Ok(())
}
pub fn check_network_changes(&mut self) -> bool {
if self.network_changed {
debug!("Network change detected, resetting flag");
self.network_changed = false;
true
} else {
false
}
}
fn enumerate_interfaces(&self) -> Result<Vec<MacOSInterface>, MacOSNetworkError> {
let mut interfaces = Vec::new();
let services = self.get_network_services()?;
for service in services {
match self.process_network_service(&service) {
Ok(interface) => {
if self.should_include_interface(&interface) {
interfaces.push(interface);
}
}
Err(e) => {
warn!("Failed to process network service: {:?}", e);
}
}
}
if self.interface_config.include_loopback {
interfaces.push(self.create_loopback_interface());
}
debug!("Enumerated {} network interfaces", interfaces.len());
Ok(interfaces)
}
fn get_network_services(&self) -> Result<Vec<String>, MacOSNetworkError> {
let mut services = Vec::new();
let common_interfaces = [
"en0", "en1", "en2", "en3", "awdl0", "utun0", "utun1", "utun2", "bridge0", "bridge1", "p2p0", "p2p1", "lo0", ];
for interface in &common_interfaces {
if self.interface_exists(interface) {
services.push(interface.to_string());
}
}
if !services.is_empty() {
debug!(
"Found {} interfaces using simple enumeration",
services.len()
);
return Ok(services);
}
let (tx, rx): (
std::sync::mpsc::Sender<Result<Vec<String>, MacOSNetworkError>>,
std::sync::mpsc::Receiver<Result<Vec<String>, MacOSNetworkError>>,
) = std::sync::mpsc::channel();
let _handle = std::thread::spawn(move || {
let result = unsafe {
let prefs_name = rust_string_to_cf_string("ant-quic-network-discovery");
let prefs = SCPreferencesCreate(
kCFAllocatorDefault,
prefs_name,
std::ptr::null_mut(), );
CFRelease(prefs_name);
if prefs.is_null() {
let _ = tx.send(Ok(Vec::new()));
return;
}
let mut services = Vec::new();
let services_array = SCNetworkServiceCopyAll(prefs);
if !services_array.is_null() {
let count = CFArrayGetCount(services_array);
for i in 0..count {
let service = CFArrayGetValueAtIndex(services_array, i);
if !service.is_null() {
let interface = SCNetworkServiceGetInterface(service);
if !interface.is_null() {
let bsd_name = SCNetworkInterfaceGetBSDName(interface);
if !bsd_name.is_null() {
if let Some(name) = cf_string_to_rust_string(bsd_name) {
services.push(name);
}
}
}
}
}
CFRelease(services_array);
}
CFRelease(prefs);
Ok(services)
};
let _ = tx.send(result);
});
match rx.recv_timeout(std::time::Duration::from_secs(5)) {
Ok(Ok(services_from_sc)) => {
debug!(
"Found {} additional interfaces using System Configuration",
services_from_sc.len()
);
services.extend(services_from_sc);
}
Ok(Err(e)) => {
warn!("System Configuration Framework error: {:?}", e);
}
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
warn!("System Configuration Framework timed out, using simple enumeration results");
}
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
warn!("System Configuration Framework thread disconnected unexpectedly");
}
}
Ok(services)
}
fn interface_exists(&self, interface_name: &str) -> bool {
let c_name = match CString::new(interface_name) {
Ok(name) => name,
Err(_) => return false,
};
let index = unsafe { libc::if_nametoindex(c_name.as_ptr()) };
index != 0
}
fn process_network_service(
&self,
service_name: &str,
) -> Result<MacOSInterface, MacOSNetworkError> {
let hardware_type = self.get_interface_hardware_type(service_name);
let state = self.get_interface_state(service_name);
let ipv4_addresses = self.get_ipv4_addresses(service_name)?;
let ipv6_addresses = if self.interface_config.include_ipv6 {
self.get_ipv6_addresses(service_name)?
} else {
Vec::new()
};
let display_name = self.get_interface_display_name(service_name);
let mtu = self.get_interface_mtu(service_name);
let hardware_address = self.get_hardware_address(service_name);
let flags = InterfaceFlags {
is_up: state == InterfaceState::Active,
is_active: state == InterfaceState::Active,
is_wireless: hardware_type == HardwareType::WiFi,
is_loopback: hardware_type == HardwareType::Loopback,
supports_ipv4: !ipv4_addresses.is_empty(),
supports_ipv6: !ipv6_addresses.is_empty(),
is_builtin: self.is_builtin_interface(service_name),
};
Ok(MacOSInterface {
name: service_name.to_string(),
display_name,
hardware_type,
state,
ipv4_addresses,
ipv6_addresses,
flags,
mtu,
hardware_address,
last_updated: Instant::now(),
})
}
fn get_interface_hardware_type(&self, interface_name: &str) -> HardwareType {
match interface_name {
name if name.starts_with("en") => {
if self.is_wifi_interface(name) {
HardwareType::WiFi
} else {
HardwareType::Ethernet
}
}
name if name.starts_with("lo") => HardwareType::Loopback,
name if name.starts_with("awdl") => HardwareType::WiFi,
name if name.starts_with("utun") => HardwareType::VPN,
name if name.starts_with("bridge") => HardwareType::Bridge,
name if name.starts_with("p2p") => HardwareType::WiFi,
name if name.starts_with("ppp") => HardwareType::PPP,
_ => HardwareType::Unknown,
}
}
fn is_wifi_interface(&self, interface_name: &str) -> bool {
if interface_name.starts_with("en") {
if let Ok(num) = interface_name[2..].parse::<u32>() {
return num <= 2;
}
}
interface_name == "awdl0"
}
fn get_interface_state(&self, interface_name: &str) -> InterfaceState {
let socket_fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0) };
if socket_fd < 0 {
return InterfaceState::Unknown;
}
let mut ifreq: libc::ifreq = unsafe { std::mem::zeroed() };
let name_bytes = interface_name.as_bytes();
let copy_len = std::cmp::min(name_bytes.len(), libc::IFNAMSIZ - 1);
unsafe {
std::ptr::copy_nonoverlapping(
name_bytes.as_ptr(),
ifreq.ifr_name.as_mut_ptr() as *mut u8,
copy_len,
);
}
let result = unsafe { libc::ioctl(socket_fd, SIOCGIFFLAGS, &mut ifreq) };
let state = if result >= 0 {
let flags = unsafe { ifreq.ifr_ifru.ifru_flags };
let is_up = (flags & libc::IFF_UP as i16) != 0;
let is_running = (flags & libc::IFF_RUNNING as i16) != 0;
if is_up && is_running {
InterfaceState::Active
} else if is_up {
InterfaceState::Inactive
} else {
InterfaceState::Inactive
}
} else {
InterfaceState::Unknown
};
unsafe {
libc::close(socket_fd);
}
state
}
fn get_ipv4_addresses(&self, interface_name: &str) -> Result<Vec<Ipv4Addr>, MacOSNetworkError> {
let mut addresses = Vec::new();
let socket_fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0) };
if socket_fd < 0 {
return Err(MacOSNetworkError::SystemConfigurationError {
function: "socket",
message: "Failed to create socket for IPv4 address query".to_string(),
});
}
let mut ifreq: libc::ifreq = unsafe { std::mem::zeroed() };
let name_bytes = interface_name.as_bytes();
let copy_len = std::cmp::min(name_bytes.len(), libc::IFNAMSIZ - 1);
unsafe {
std::ptr::copy_nonoverlapping(
name_bytes.as_ptr(),
ifreq.ifr_name.as_mut_ptr() as *mut u8,
copy_len,
);
}
let result = unsafe { libc::ioctl(socket_fd, SIOCGIFADDR, &mut ifreq) };
if result >= 0 {
let sockaddr_in = unsafe {
&*(&ifreq.ifr_ifru.ifru_addr as *const libc::sockaddr as *const libc::sockaddr_in)
};
if sockaddr_in.sin_family == libc::AF_INET as u8 {
let ip_bytes = sockaddr_in.sin_addr.s_addr.to_ne_bytes();
let ipv4_addr = Ipv4Addr::from(ip_bytes);
if !ipv4_addr.is_unspecified() {
addresses.push(ipv4_addr);
}
}
}
unsafe {
libc::close(socket_fd);
}
Ok(addresses)
}
fn get_ipv6_addresses(&self, interface_name: &str) -> Result<Vec<Ipv6Addr>, MacOSNetworkError> {
let mut addresses = Vec::new();
let mut ifaddrs_ptr: *mut libc::ifaddrs = std::ptr::null_mut();
let result = unsafe { libc::getifaddrs(&mut ifaddrs_ptr) };
if result != 0 {
return Err(MacOSNetworkError::SystemConfigurationError {
function: "getifaddrs",
message: "Failed to get interface addresses".to_string(),
});
}
let mut current = ifaddrs_ptr;
while !current.is_null() {
let ifaddr = unsafe { &*current };
let if_name = unsafe {
let name_ptr = ifaddr.ifa_name;
let name_cstr = std::ffi::CStr::from_ptr(name_ptr);
name_cstr.to_string_lossy().to_string()
};
if if_name == interface_name && !ifaddr.ifa_addr.is_null() {
let sockaddr = unsafe { &*ifaddr.ifa_addr };
if sockaddr.sa_family == libc::AF_INET6 as u8 {
let sockaddr_in6 = unsafe { &*(ifaddr.ifa_addr as *const libc::sockaddr_in6) };
let ipv6_bytes = sockaddr_in6.sin6_addr.s6_addr;
let ipv6_addr = Ipv6Addr::from(ipv6_bytes);
if !ipv6_addr.is_unspecified() {
addresses.push(ipv6_addr);
}
}
}
current = ifaddr.ifa_next;
}
unsafe {
libc::freeifaddrs(ifaddrs_ptr);
}
Ok(addresses)
}
fn get_interface_display_name(&self, interface_name: &str) -> String {
match interface_name {
"en0" => "Wi-Fi".to_string(),
"en1" => "Ethernet".to_string(),
"lo0" => "Loopback".to_string(),
name if name.starts_with("utun") => "VPN".to_string(),
name if name.starts_with("awdl") => "AirDrop".to_string(),
name => format!("Interface {name}"),
}
}
fn get_interface_mtu(&self, interface_name: &str) -> u32 {
let socket_fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0) };
if socket_fd < 0 {
return 1500; }
let mut ifreq: libc::ifreq = unsafe { std::mem::zeroed() };
let name_bytes = interface_name.as_bytes();
let copy_len = std::cmp::min(name_bytes.len(), libc::IFNAMSIZ - 1);
unsafe {
std::ptr::copy_nonoverlapping(
name_bytes.as_ptr(),
ifreq.ifr_name.as_mut_ptr() as *mut u8,
copy_len,
);
}
let result = unsafe { libc::ioctl(socket_fd, SIOCGIFMTU, &mut ifreq) };
let mtu = if result >= 0 {
unsafe { ifreq.ifr_ifru.ifru_mtu as u32 }
} else {
match interface_name {
"lo0" => 16384,
_ => 1500,
}
};
unsafe {
libc::close(socket_fd);
}
mtu
}
fn get_hardware_address(&self, interface_name: &str) -> Option<[u8; 6]> {
let mut ifaddrs_ptr: *mut libc::ifaddrs = std::ptr::null_mut();
let result = unsafe { libc::getifaddrs(&mut ifaddrs_ptr) };
if result != 0 {
return None;
}
let mut hardware_address = None;
let mut current = ifaddrs_ptr;
while !current.is_null() {
let ifaddr = unsafe { &*current };
let if_name = unsafe {
let name_ptr = ifaddr.ifa_name;
let name_cstr = std::ffi::CStr::from_ptr(name_ptr);
name_cstr.to_string_lossy().to_string()
};
if if_name == interface_name && !ifaddr.ifa_addr.is_null() {
let sockaddr = unsafe { &*ifaddr.ifa_addr };
if sockaddr.sa_family == libc::AF_LINK as u8 {
let sockaddr_dl = unsafe { &*(ifaddr.ifa_addr as *const libc::sockaddr_dl) };
if sockaddr_dl.sdl_alen == 6 && sockaddr_dl.sdl_type == IFT_ETHER {
let name_len = sockaddr_dl.sdl_nlen as usize;
let addr_offset = 8 + name_len;
if addr_offset + 6 <= sockaddr_dl.sdl_len as usize {
let addr_data = unsafe {
let base_ptr = ifaddr.ifa_addr as *const u8;
std::slice::from_raw_parts(base_ptr.add(addr_offset), 6)
};
let mut mac = [0u8; 6];
mac.copy_from_slice(addr_data);
hardware_address = Some(mac);
break;
}
}
}
}
current = ifaddr.ifa_next;
}
unsafe {
libc::freeifaddrs(ifaddrs_ptr);
}
hardware_address
}
fn is_builtin_interface(&self, interface_name: &str) -> bool {
matches!(interface_name, "en0" | "en1" | "lo0")
}
fn create_loopback_interface(&self) -> MacOSInterface {
MacOSInterface {
name: "lo0".to_string(),
display_name: "Loopback".to_string(),
hardware_type: HardwareType::Loopback,
state: InterfaceState::Active,
ipv4_addresses: vec![Ipv4Addr::new(127, 0, 0, 1)],
ipv6_addresses: if self.interface_config.include_ipv6 {
vec![Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)]
} else {
Vec::new()
},
flags: InterfaceFlags {
is_up: true,
is_active: true,
is_wireless: false,
is_loopback: true,
supports_ipv4: true,
supports_ipv6: self.interface_config.include_ipv6,
is_builtin: true,
},
mtu: 16384,
hardware_address: None,
last_updated: Instant::now(),
}
}
fn should_include_interface(&self, interface: &MacOSInterface) -> bool {
if interface.flags.is_loopback && !self.interface_config.include_loopback {
return false;
}
if interface.state != InterfaceState::Active && !self.interface_config.include_inactive {
return false;
}
if self.interface_config.builtin_only && !interface.flags.is_builtin {
return false;
}
if interface.mtu < self.interface_config.min_mtu {
return false;
}
if interface.ipv4_addresses.is_empty() && interface.ipv6_addresses.is_empty() {
return false;
}
true
}
fn convert_to_network_interface(&self, macos_interface: &MacOSInterface) -> NetworkInterface {
let mut addresses = Vec::new();
for ipv4 in &macos_interface.ipv4_addresses {
addresses.push(SocketAddr::new(IpAddr::V4(*ipv4), 0));
}
for ipv6 in &macos_interface.ipv6_addresses {
addresses.push(SocketAddr::new(IpAddr::V6(*ipv6), 0));
}
NetworkInterface {
name: macos_interface.name.clone(),
addresses,
is_up: macos_interface.flags.is_up,
is_wireless: macos_interface.flags.is_wireless,
mtu: Some(macos_interface.mtu as u16),
}
}
fn update_cache(&mut self, interfaces: Vec<MacOSInterface>) {
self.cached_interfaces.clear();
for interface in interfaces {
self.cached_interfaces
.insert(interface.name.clone(), interface);
}
self.last_scan_time = Some(Instant::now());
}
fn is_cache_valid(&self) -> bool {
if let Some(last_scan) = self.last_scan_time {
last_scan.elapsed() < self.cache_ttl
} else {
false
}
}
unsafe fn create_dynamic_store(&mut self, name: *const std::ffi::c_char) -> SCDynamicStoreRef {
unsafe {
let cf_name =
CFStringCreateWithCString(kCFAllocatorDefault, name, kCFStringEncodingUTF8);
if cf_name.is_null() {
error!("Failed to create CFString for dynamic store name");
return SCDynamicStoreRef(std::ptr::null_mut());
}
let mut context = SCDynamicStoreContext {
version: 0,
info: self as *mut _ as *mut std::ffi::c_void,
retain: None,
release: None,
copyDescription: None,
};
let store = SCDynamicStoreCreate(
kCFAllocatorDefault,
cf_name,
Some(network_change_callback),
&mut context,
);
CFRelease(cf_name);
store
}
}
unsafe fn create_run_loop_source(&self, store: &SCDynamicStoreRef) -> CFRunLoopSourceRef {
unsafe {
let source = SCDynamicStoreCreateRunLoopSource(
kCFAllocatorDefault,
*store,
0, );
if !source.0.is_null() {
let current_run_loop = CFRunLoopGetCurrent();
CFRunLoopAddSource(current_run_loop, source, kCFRunLoopDefaultMode);
}
source
}
}
}
impl NetworkInterfaceDiscovery for MacOSInterfaceDiscovery {
fn start_scan(&mut self) -> Result<(), String> {
debug!("Starting macOS network interface scan");
if self.is_cache_valid() && !self.check_network_changes() {
debug!("Using cached interface data");
let interfaces: Vec<NetworkInterface> = self
.cached_interfaces
.values()
.map(|mi| self.convert_to_network_interface(mi))
.collect();
self.scan_state = ScanState::Completed {
scan_results: interfaces,
};
return Ok(());
}
self.scan_state = ScanState::InProgress {
started_at: Instant::now(),
};
match self.enumerate_interfaces() {
Ok(interfaces) => {
debug!("Successfully enumerated {} interfaces", interfaces.len());
let network_interfaces: Vec<NetworkInterface> = interfaces
.iter()
.map(|mi| self.convert_to_network_interface(mi))
.collect();
self.update_cache(interfaces);
self.scan_state = ScanState::Completed {
scan_results: network_interfaces,
};
info!("Network interface scan completed successfully");
Ok(())
}
Err(e) => {
let error_msg = format!("macOS interface enumeration failed: {e:?}");
error!("{}", error_msg);
self.scan_state = ScanState::Failed {
error: error_msg.clone(),
};
Err(error_msg)
}
}
}
fn check_scan_complete(&mut self) -> Option<Vec<NetworkInterface>> {
match &self.scan_state {
ScanState::Completed { scan_results } => {
let results = scan_results.clone();
self.scan_state = ScanState::Idle;
Some(results)
}
ScanState::Failed { error } => {
warn!("Scan failed: {}", error);
self.scan_state = ScanState::Idle;
None
}
_ => None,
}
}
}
impl Drop for MacOSInterfaceDiscovery {
fn drop(&mut self) {
unsafe {
if let Some(run_loop_source) = self.run_loop_source.take() {
let current_run_loop = CFRunLoopGetCurrent();
CFRunLoopRemoveSource(current_run_loop, run_loop_source, kCFRunLoopDefaultMode);
CFRelease(run_loop_source.0);
}
if let Some(sc_store) = self.sc_store.take() {
CFRelease(sc_store.0);
}
}
}
}
impl std::fmt::Display for MacOSNetworkError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SystemConfigurationError { function, message } => {
write!(f, "System Configuration error in {function}: {message}")
}
Self::InterfaceNotFound { interface_name } => {
write!(f, "Interface not found: {interface_name}")
}
Self::InvalidInterfaceConfig {
interface_name,
reason,
} => {
write!(f, "Invalid interface config for {interface_name}: {reason}")
}
Self::ServiceEnumerationFailed { reason } => {
write!(f, "Service enumeration failed: {reason}")
}
Self::AddressParsingFailed { address, reason } => {
write!(f, "Address parsing failed for {address}: {reason}")
}
Self::DynamicStoreAccessFailed { reason } => {
write!(f, "Dynamic store access failed: {reason}")
}
Self::RunLoopSourceCreationFailed { reason } => {
write!(f, "Run loop source creation failed: {reason}")
}
Self::DynamicStoreConfigurationFailed { operation, reason } => {
write!(
f,
"Dynamic store configuration failed in {operation}: {reason}"
)
}
}
}
}
impl std::error::Error for MacOSNetworkError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_macos_interface_discovery_creation() {
let discovery = MacOSInterfaceDiscovery::new();
assert!(discovery.cached_interfaces.is_empty());
assert!(discovery.last_scan_time.is_none());
}
#[test]
fn test_interface_config() {
let mut discovery = MacOSInterfaceDiscovery::new();
let config = InterfaceConfig {
include_inactive: true,
include_loopback: true,
include_ipv6: false,
builtin_only: true,
min_mtu: 1000,
max_interfaces: 16,
};
discovery.set_interface_config(config.clone());
assert!(discovery.interface_config.include_loopback);
assert_eq!(discovery.interface_config.min_mtu, 1000);
}
#[test]
fn test_hardware_type_detection() {
let discovery = MacOSInterfaceDiscovery::new();
assert_eq!(
discovery.get_interface_hardware_type("en0"),
HardwareType::WiFi
);
assert_eq!(
discovery.get_interface_hardware_type("en1"),
HardwareType::WiFi
); assert_eq!(
discovery.get_interface_hardware_type("en5"),
HardwareType::Ethernet
); assert_eq!(
discovery.get_interface_hardware_type("lo0"),
HardwareType::Loopback
);
assert_eq!(
discovery.get_interface_hardware_type("utun0"),
HardwareType::VPN
);
assert_eq!(
discovery.get_interface_hardware_type("awdl0"),
HardwareType::WiFi
);
assert_eq!(
discovery.get_interface_hardware_type("bridge0"),
HardwareType::Bridge
);
assert_eq!(
discovery.get_interface_hardware_type("p2p0"),
HardwareType::WiFi
);
assert_eq!(
discovery.get_interface_hardware_type("ppp0"),
HardwareType::PPP
);
assert_eq!(
discovery.get_interface_hardware_type("unknown0"),
HardwareType::Unknown
);
}
#[test]
fn test_cache_validation() {
let mut discovery = MacOSInterfaceDiscovery::new();
assert!(!discovery.is_cache_valid());
discovery.last_scan_time = Some(Instant::now());
assert!(discovery.is_cache_valid());
discovery.last_scan_time = Some(Instant::now() - std::time::Duration::from_secs(60));
assert!(!discovery.is_cache_valid());
}
#[test]
fn test_loopback_interface_creation() {
let discovery = MacOSInterfaceDiscovery::new();
let loopback = discovery.create_loopback_interface();
assert_eq!(loopback.name, "lo0");
assert_eq!(loopback.hardware_type, HardwareType::Loopback);
assert!(loopback.flags.is_loopback);
assert!(loopback.flags.is_up);
assert!(!loopback.ipv4_addresses.is_empty());
}
#[test]
fn test_interface_filtering() {
let mut discovery = MacOSInterfaceDiscovery::new();
let interface = MacOSInterface {
name: "en0".to_string(),
display_name: "Wi-Fi".to_string(),
hardware_type: HardwareType::WiFi,
state: InterfaceState::Active,
ipv4_addresses: vec![Ipv4Addr::new(192, 168, 1, 100)],
ipv6_addresses: Vec::new(),
flags: InterfaceFlags {
is_up: true,
is_active: true,
is_wireless: true,
is_loopback: false,
supports_ipv4: true,
supports_ipv6: false,
is_builtin: true,
},
mtu: 1500,
hardware_address: None,
last_updated: Instant::now(),
};
assert!(discovery.should_include_interface(&interface));
discovery.interface_config.min_mtu = 2000;
assert!(!discovery.should_include_interface(&interface));
}
}