diff --git a/examples/default_interface.rs b/examples/default_interface.rs index 58242e3..c8fd959 100644 --- a/examples/default_interface.rs +++ b/examples/default_interface.rs @@ -29,6 +29,8 @@ fn main() { println!("\tTransmit Speed: {:?}", interface.transmit_speed); println!("\tReceive Speed: {:?}", interface.receive_speed); println!("\tAuto-negotiate: {:?}", interface.auto_negotiate); + println!("\tDHCPv4 enabled: {:?}", interface.dhcp_v4_enabled); + println!("\tDHCPv6 enabled: {:?}", interface.dhcp_v6_enabled); println!("\tStats: {:?}", interface.stats); if let Some(gateway) = interface.gateway { println!("Default Gateway"); diff --git a/examples/list_interfaces.rs b/examples/list_interfaces.rs index feda458..bdc4ca3 100644 --- a/examples/list_interfaces.rs +++ b/examples/list_interfaces.rs @@ -61,6 +61,8 @@ fn main() { println!("\tTransmit Speed: {:?}", interface.transmit_speed); println!("\tReceive Speed: {:?}", interface.receive_speed); println!("\tAuto-negotiate: {:?}", interface.auto_negotiate); + println!("\tDHCPv4 enabled: {:?}", interface.dhcp_v4_enabled); + println!("\tDHCPv6 enabled: {:?}", interface.dhcp_v6_enabled); println!("\tStats: {:?}", interface.stats); #[cfg(feature = "gateway")] if let Some(gateway) = interface.gateway { diff --git a/src/interface/interface.rs b/src/interface/interface.rs index 9c6cda8..9644752 100644 --- a/src/interface/interface.rs +++ b/src/interface/interface.rs @@ -83,6 +83,16 @@ pub struct Interface { /// /// It may `None` if reading this information has not been implemented for a specific OS. pub auto_negotiate: Option, + /// Whether this interface is configured to use DHCP for IPv4. + /// + /// This may be `None` if reading this information is not available on the + /// current platform, not implemented, or not applicable to the interface. + pub dhcp_v4_enabled: Option, + /// Whether this interface is configured to use DHCP for IPv6. + /// + /// This may be `None` if reading this information is not available on the + /// current platform, not implemented, or not applicable to the interface. + pub dhcp_v6_enabled: Option, /// Traffic counters captured when the interface snapshot was collected. /// /// The counters are cumulative totals reported by the OS, typically since boot. @@ -138,6 +148,8 @@ impl Interface { transmit_speed: None, receive_speed: None, auto_negotiate: None, + dhcp_v4_enabled: None, + dhcp_v6_enabled: None, stats: None, #[cfg(feature = "gateway")] gateway: None, @@ -275,6 +287,12 @@ mod tests { use ipnet::{Ipv4Net, Ipv6Net}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + #[test] + fn dummy_initializes_dhcp_state_as_unknown() { + assert_eq!(Interface::dummy().dhcp_v4_enabled, None); + assert_eq!(Interface::dummy().dhcp_v6_enabled, None); + } + #[test] fn global_helpers_filter() { let mut itf = Interface::dummy(); diff --git a/src/os/android/interface.rs b/src/os/android/interface.rs index 00abf41..c6c46e5 100644 --- a/src/os/android/interface.rs +++ b/src/os/android/interface.rs @@ -95,6 +95,8 @@ pub fn interfaces() -> Vec { transmit_speed: None, receive_speed: None, auto_negotiate: None, + dhcp_v4_enabled: None, + dhcp_v6_enabled: None, stats: r.stats.clone(), #[cfg(feature = "gateway")] gateway: None, diff --git a/src/os/linux/dhcp.rs b/src/os/linux/dhcp.rs new file mode 100644 index 0000000..1c19708 --- /dev/null +++ b/src/os/linux/dhcp.rs @@ -0,0 +1,307 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub(crate) struct DhcpState { + pub v4: Option, + pub v6: Option, +} + +impl DhcpState { + fn merge_missing(&mut self, other: DhcpState) { + if self.v4.is_none() { + self.v4 = other.v4; + } + if self.v6.is_none() { + self.v6 = other.v6; + } + } +} + +pub(crate) fn dhcp_state(iface_name: &str, ifindex: u32) -> DhcpState { + let mut state = systemd_networkd_dhcp_state(ifindex); + state.merge_missing(systemd_lease_dhcp_state(ifindex)); + state.merge_missing(network_manager_dhcp_state(iface_name)); + state +} + +fn systemd_networkd_dhcp_state(ifindex: u32) -> DhcpState { + if ifindex == 0 { + return DhcpState::default(); + } + let path = PathBuf::from(format!("/run/systemd/netif/links/{ifindex}")); + let Ok(content) = fs::read_to_string(path) else { + return DhcpState::default(); + }; + parse_systemd_networkd_link(&content) +} + +fn systemd_lease_dhcp_state(ifindex: u32) -> DhcpState { + if ifindex == 0 { + return DhcpState::default(); + } + let path = PathBuf::from(format!("/run/systemd/netif/leases/{ifindex}")); + match fs::read_to_string(path) { + Ok(_) => DhcpState { + v4: Some(true), + v6: None, + }, + Err(_) => DhcpState::default(), + } +} + +fn network_manager_dhcp_state(iface_name: &str) -> DhcpState { + for dir in [ + "/run/NetworkManager/system-connections", + "/etc/NetworkManager/system-connections", + ] { + let state = network_manager_dir_dhcp_state(Path::new(dir), iface_name); + if state.v4.is_some() || state.v6.is_some() { + return state; + } + } + DhcpState::default() +} + +fn network_manager_dir_dhcp_state(dir: &Path, iface_name: &str) -> DhcpState { + let Ok(entries) = fs::read_dir(dir) else { + return DhcpState::default(); + }; + for entry in entries.flatten() { + let Ok(file_type) = entry.file_type() else { + continue; + }; + if !file_type.is_file() { + continue; + } + let Ok(content) = fs::read_to_string(entry.path()) else { + continue; + }; + let state = parse_network_manager_connection(&content, iface_name); + if state.v4.is_some() || state.v6.is_some() { + return state; + } + } + DhcpState::default() +} + +fn parse_systemd_networkd_link(content: &str) -> DhcpState { + let mut inferred = DhcpState::default(); + let mut explicit = DhcpState::default(); + + for line in content.lines() { + let Some((key, value)) = split_key_value(line) else { + continue; + }; + if key.starts_with("DHCP4_CLIENT_") { + inferred.v4 = Some(true); + continue; + } + if key.starts_with("DHCP6_CLIENT_") { + inferred.v6 = Some(true); + continue; + } + if key == "DHCP" { + explicit = parse_systemd_dhcp_value(value); + } + } + + let mut result = inferred; + if explicit.v4.is_some() { + result.v4 = explicit.v4; + } + if explicit.v6.is_some() { + result.v6 = explicit.v6; + } + result +} + +fn parse_systemd_dhcp_value(value: &str) -> DhcpState { + match value.trim().to_ascii_lowercase().as_str() { + "yes" | "true" | "both" => DhcpState { + v4: Some(true), + v6: Some(true), + }, + "ipv4" => DhcpState { + v4: Some(true), + v6: Some(false), + }, + "ipv6" => DhcpState { + v4: Some(false), + v6: Some(true), + }, + "no" | "false" | "none" => DhcpState { + v4: Some(false), + v6: Some(false), + }, + _ => DhcpState::default(), + } +} + +fn parse_network_manager_connection(content: &str, iface_name: &str) -> DhcpState { + let mut section = ""; + let mut connection_matches = false; + let mut ipv4_method = None; + let mut ipv6_method = None; + + for raw_line in content.lines() { + let line = raw_line.trim(); + if line.is_empty() || line.starts_with('#') || line.starts_with(';') { + continue; + } + if let Some(name) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) { + section = name.trim(); + continue; + } + let Some((key, value)) = split_key_value(line) else { + continue; + }; + match (section, key) { + ("connection", "interface-name") => { + connection_matches = value == iface_name; + } + ("ipv4", "method") => { + ipv4_method = Some(value); + } + ("ipv6", "method") => { + ipv6_method = Some(value); + } + _ => {} + } + } + + if !connection_matches { + return DhcpState::default(); + } + + DhcpState { + v4: ipv4_method.and_then(parse_network_manager_ipv4_method), + v6: ipv6_method.and_then(parse_network_manager_ipv6_method), + } +} + +fn parse_network_manager_ipv4_method(method: &str) -> Option { + match method.trim().to_ascii_lowercase().as_str() { + "auto" => Some(true), + "manual" | "disabled" | "link-local" | "shared" => Some(false), + _ => None, + } +} + +fn parse_network_manager_ipv6_method(method: &str) -> Option { + match method.trim().to_ascii_lowercase().as_str() { + "dhcp" => Some(true), + "manual" | "disabled" | "link-local" => Some(false), + _ => None, + } +} + +fn split_key_value(line: &str) -> Option<(&str, &str)> { + let (key, value) = line.split_once('=')?; + Some((key.trim(), value.trim())) +} + +#[cfg(test)] +mod tests { + use super::{ + DhcpState, parse_network_manager_connection, parse_network_manager_ipv4_method, + parse_network_manager_ipv6_method, parse_systemd_networkd_link, + }; + + #[test] + fn parses_systemd_networkd_dhcp_values() { + assert_eq!( + parse_systemd_networkd_link("ADMIN_STATE=configured\nDHCP=yes\n"), + DhcpState { + v4: Some(true), + v6: Some(true), + } + ); + assert_eq!( + parse_systemd_networkd_link("DHCP=ipv4\n"), + DhcpState { + v4: Some(true), + v6: Some(false), + } + ); + assert_eq!( + parse_systemd_networkd_link("DHCP=ipv6\n"), + DhcpState { + v4: Some(false), + v6: Some(true), + } + ); + } + + #[test] + fn detects_systemd_runtime_client_keys() { + assert_eq!( + parse_systemd_networkd_link("DHCP4_CLIENT_ADDRESS=192.0.2.10\n"), + DhcpState { + v4: Some(true), + v6: None, + } + ); + assert_eq!( + parse_systemd_networkd_link("DHCP6_CLIENT_DUID=00:01:00:01\n"), + DhcpState { + v4: None, + v6: Some(true), + } + ); + } + + #[test] + fn explicit_systemd_dhcp_value_overrides_runtime_keys() { + assert_eq!( + parse_systemd_networkd_link("DHCP4_CLIENT_ADDRESS=192.0.2.10\nDHCP=no\n"), + DhcpState { + v4: Some(false), + v6: Some(false), + } + ); + } + + #[test] + fn parses_network_manager_connection_for_matching_interface() { + let content = "\ +[connection] +id=Wired +interface-name=eth0 + +[ipv4] +method=auto + +[ipv6] +method=dhcp +"; + assert_eq!( + parse_network_manager_connection(content, "eth0"), + DhcpState { + v4: Some(true), + v6: Some(true), + } + ); + assert_eq!( + parse_network_manager_connection(content, "wlan0"), + DhcpState::default() + ); + } + + #[test] + fn parses_network_manager_ipv4_methods() { + assert_eq!(parse_network_manager_ipv4_method("auto"), Some(true)); + assert_eq!(parse_network_manager_ipv4_method("manual"), Some(false)); + assert_eq!(parse_network_manager_ipv4_method("disabled"), Some(false)); + assert_eq!(parse_network_manager_ipv4_method("unknown"), None); + } + + #[test] + fn parses_network_manager_ipv6_methods() { + assert_eq!(parse_network_manager_ipv6_method("dhcp"), Some(true)); + assert_eq!(parse_network_manager_ipv6_method("auto"), None); + assert_eq!(parse_network_manager_ipv6_method("manual"), Some(false)); + assert_eq!(parse_network_manager_ipv6_method("disabled"), Some(false)); + assert_eq!(parse_network_manager_ipv6_method("link-local"), Some(false)); + } +} diff --git a/src/os/linux/interface.rs b/src/os/linux/interface.rs index ce2ab44..72830ba 100644 --- a/src/os/linux/interface.rs +++ b/src/os/linux/interface.rs @@ -71,6 +71,8 @@ pub fn interfaces() -> Vec { transmit_speed: None, receive_speed: None, auto_negotiate: None, + dhcp_v4_enabled: None, + dhcp_v6_enabled: None, stats: None, #[cfg(feature = "gateway")] gateway: None, @@ -137,6 +139,9 @@ pub fn interfaces() -> Vec { let if_speed = super::sysfs::get_interface_speed(&iface.name); iface.transmit_speed = if_speed; iface.receive_speed = if_speed; + let dhcp = super::dhcp::dhcp_state(&iface.name, iface.index); + iface.dhcp_v4_enabled = dhcp.v4; + iface.dhcp_v6_enabled = dhcp.v6; iface.oper_state = super::sysfs::operstate(&iface.name); if iface.stats.is_none() { diff --git a/src/os/linux/mod.rs b/src/os/linux/mod.rs index 516663e..2d3d677 100644 --- a/src/os/linux/mod.rs +++ b/src/os/linux/mod.rs @@ -1,5 +1,7 @@ pub mod arp; #[cfg(not(target_os = "android"))] +mod dhcp; +#[cfg(not(target_os = "android"))] pub mod flags; #[cfg(not(target_os = "android"))] pub mod interface; diff --git a/src/os/macos/interface.rs b/src/os/macos/interface.rs index 757917b..bb89d30 100644 --- a/src/os/macos/interface.rs +++ b/src/os/macos/interface.rs @@ -1,5 +1,5 @@ use crate::os::darwin::types::{get_functional_type, interface_type_by_name}; -use crate::os::macos::sc::{get_sc_interface_map, read_sc_interfaces_plist_map}; +use crate::os::macos::sc::{get_sc_interface_map, read_sc_plist_interface_map}; use crate::{ interface::interface::Interface, os::{ @@ -15,7 +15,7 @@ pub fn interfaces() -> Vec { let mut ifaces: Vec = unix_interfaces(); let if_extra_map: HashMap = - read_sc_interfaces_plist_map().unwrap_or_else(|_| { + read_sc_plist_interface_map().unwrap_or_else(|_| { // Fallback to SCNetworkInterfaceCopyAll ... get_sc_interface_map() }); @@ -40,6 +40,8 @@ pub fn interfaces() -> Vec { iface.if_type = sc_type; } iface.friendly_name = sc_inface.friendly_name.clone(); + iface.dhcp_v4_enabled = sc_inface.dhcp_v4_enabled; + iface.dhcp_v6_enabled = sc_inface.dhcp_v6_enabled; } if iface.if_type == InterfaceType::Wireless80211 { diff --git a/src/os/macos/sc.rs b/src/os/macos/sc.rs index 0d40790..ee39e9c 100644 --- a/src/os/macos/sc.rs +++ b/src/os/macos/sc.rs @@ -8,6 +8,7 @@ use std::collections::HashMap; use std::io::Cursor; const SC_NWIF_PATH: &str = "/Library/Preferences/SystemConfiguration/NetworkInterfaces.plist"; +const SC_PREFS_PATH: &str = "/Library/Preferences/SystemConfiguration/preferences.plist"; #[derive(Clone, Debug, Default)] pub(crate) struct SCInterface { @@ -19,21 +20,22 @@ pub(crate) struct SCInterface { pub sc_type: Option, #[allow(dead_code)] pub active: Option, + pub dhcp_v4_enabled: Option, + pub dhcp_v6_enabled: Option, } impl SCInterface { pub fn if_type(&self) -> Option { - if let Some(sc_type) = &self.sc_type { - Some(map_sc_interface_type(sc_type)) - } else { - None - } + self.sc_type + .as_ref() + .map(|sc_type| map_sc_interface_type(sc_type)) } } fn map_sc_interface_type(type_id: &str) -> InterfaceType { match type_id { "Bridge" => InterfaceType::Bridge, + "AirPort" => InterfaceType::Wireless80211, "Ethernet" => InterfaceType::Ethernet, "IEEE80211" => InterfaceType::Wireless80211, "Loopback" => InterfaceType::Loopback, @@ -73,7 +75,7 @@ pub(crate) fn get_sc_interface_map() -> HashMap { let mac: Option = sc_iface .hardware_address_string() - .and_then(|mac_str| Some(MacAddr::from_hex_format(&mac_str.to_string()))); + .map(|mac_str| MacAddr::from_hex_format(&mac_str.to_string())); if_map.insert( name.clone(), @@ -83,6 +85,8 @@ pub(crate) fn get_sc_interface_map() -> HashMap { sc_type: sc_if_type, mac, active: None, + dhcp_v4_enabled: None, + dhcp_v6_enabled: None, }, ); } @@ -153,6 +157,8 @@ fn load_sc_interfaces_plist_map(bytes: &[u8]) -> HashMap { sc_type, mac, active, + dhcp_v4_enabled: None, + dhcp_v6_enabled: None, }, ); } @@ -160,8 +166,265 @@ fn load_sc_interfaces_plist_map(bytes: &[u8]) -> HashMap { map } +fn load_sc_preferences_plist_map(bytes: &[u8]) -> HashMap { + let mut map = HashMap::new(); + + let v = match plist::Value::from_reader(Cursor::new(bytes)) { + Ok(v) => v, + Err(_) => return map, + }; + let dict = match v.as_dictionary() { + Some(d) => d, + None => return map, + }; + let services = match dict.get("NetworkServices").and_then(|v| v.as_dictionary()) { + Some(d) => d, + None => return map, + }; + + for service_id in current_set_service_order(dict) { + if let Some(service) = services.get(&service_id).and_then(|v| v.as_dictionary()) { + insert_service_metadata(&mut map, service); + } + } + + for service in services.values().filter_map(|v| v.as_dictionary()) { + insert_service_metadata(&mut map, service); + } + + map +} + +fn current_set_service_order(dict: &plist::Dictionary) -> Vec { + let Some(current_set) = dict.get("CurrentSet").and_then(|v| v.as_string()) else { + return Vec::new(); + }; + let Some(set_id) = current_set.strip_prefix("/Sets/") else { + return Vec::new(); + }; + + dict.get("Sets") + .and_then(|v| v.as_dictionary()) + .and_then(|sets| sets.get(set_id)) + .and_then(|v| v.as_dictionary()) + .and_then(|set| set.get("Network")) + .and_then(|v| v.as_dictionary()) + .and_then(|network| network.get("Global")) + .and_then(|v| v.as_dictionary()) + .and_then(|global| global.get("IPv4")) + .and_then(|v| v.as_dictionary()) + .and_then(|ipv4| ipv4.get("ServiceOrder")) + .and_then(|v| v.as_array()) + .map(|order| { + order + .iter() + .filter_map(|v| v.as_string().map(ToOwned::to_owned)) + .collect() + }) + .unwrap_or_default() +} + +fn insert_service_metadata(map: &mut HashMap, service: &plist::Dictionary) { + let Some(interface) = service.get("Interface").and_then(|v| v.as_dictionary()) else { + return; + }; + let Some(bsd_name) = interface.get("DeviceName").and_then(|v| v.as_string()) else { + return; + }; + if bsd_name.is_empty() || map.contains_key(bsd_name) { + return; + } + + let friendly_name = service + .get("UserDefinedName") + .and_then(|v| v.as_string()) + .or_else(|| interface.get("UserDefinedName").and_then(|v| v.as_string())) + .map(ToOwned::to_owned); + let sc_type = interface + .get("Hardware") + .and_then(|v| v.as_string()) + .or_else(|| interface.get("Type").and_then(|v| v.as_string())) + .map(ToOwned::to_owned); + let dhcp_v4_enabled = service + .get("IPv4") + .and_then(|v| v.as_dictionary()) + .and_then(|ipv4| ipv4.get("ConfigMethod")) + .and_then(|v| v.as_string()) + .and_then(map_ipv4_config_method_to_dhcp_v4); + let dhcp_v6_enabled = service + .get("IPv6") + .and_then(|v| v.as_dictionary()) + .and_then(|ipv6| ipv6.get("ConfigMethod")) + .and_then(|v| v.as_string()) + .and_then(map_ipv6_config_method_to_dhcp_v6); + + map.insert( + bsd_name.to_string(), + SCInterface { + bsd_name: bsd_name.to_string(), + mac: None, + friendly_name, + sc_type, + active: None, + dhcp_v4_enabled, + dhcp_v6_enabled, + }, + ); +} + +fn map_ipv4_config_method_to_dhcp_v4(method: &str) -> Option { + match method { + "DHCP" => Some(true), + "Manual" | "Off" => Some(false), + _ => None, + } +} + +fn map_ipv6_config_method_to_dhcp_v6(method: &str) -> Option { + match method { + "Manual" | "LinkLocal" | "Off" => Some(false), + _ => None, + } +} + +fn merge_sc_interface_maps( + mut base: HashMap, + overlay: HashMap, +) -> HashMap { + for (name, overlay_iface) in overlay { + let entry = base.entry(name).or_insert_with(|| SCInterface { + bsd_name: overlay_iface.bsd_name.clone(), + ..SCInterface::default() + }); + + if entry.mac.is_none() { + entry.mac = overlay_iface.mac; + } + if overlay_iface.friendly_name.is_some() { + entry.friendly_name = overlay_iface.friendly_name; + } + if overlay_iface.sc_type.is_some() { + entry.sc_type = overlay_iface.sc_type; + } + if entry.active.is_none() { + entry.active = overlay_iface.active; + } + if overlay_iface.dhcp_v4_enabled.is_some() { + entry.dhcp_v4_enabled = overlay_iface.dhcp_v4_enabled; + } + if overlay_iface.dhcp_v6_enabled.is_some() { + entry.dhcp_v6_enabled = overlay_iface.dhcp_v6_enabled; + } + } + base +} + /// Read and parse the NetworkInterfaces.plist file into a map of `BSD name -> SCInterface`. pub(crate) fn read_sc_interfaces_plist_map() -> std::io::Result> { let bytes = std::fs::read(SC_NWIF_PATH)?; Ok(load_sc_interfaces_plist_map(&bytes)) } + +/// Read macOS SystemConfiguration plist files into a map of `BSD name -> SCInterface`. +pub(crate) fn read_sc_plist_interface_map() -> std::io::Result> { + let preferences = std::fs::read(SC_PREFS_PATH) + .map(|bytes| load_sc_preferences_plist_map(&bytes)) + .unwrap_or_default(); + + match read_sc_interfaces_plist_map() { + Ok(interfaces) => Ok(merge_sc_interface_maps(interfaces, preferences)), + Err(_err) if !preferences.is_empty() => Ok(preferences), + Err(err) => Err(err), + } +} + +#[cfg(test)] +mod tests { + use super::{ + load_sc_preferences_plist_map, map_ipv4_config_method_to_dhcp_v4, + map_ipv6_config_method_to_dhcp_v6, + }; + + #[test] + fn maps_ipv4_config_methods_to_dhcp_v4_state() { + assert_eq!(map_ipv4_config_method_to_dhcp_v4("DHCP"), Some(true)); + assert_eq!(map_ipv4_config_method_to_dhcp_v4("Manual"), Some(false)); + assert_eq!(map_ipv4_config_method_to_dhcp_v4("Off"), Some(false)); + assert_eq!(map_ipv4_config_method_to_dhcp_v4("Automatic"), None); + assert_eq!(map_ipv4_config_method_to_dhcp_v4("Unknown"), None); + } + + #[test] + fn maps_ipv6_config_methods_to_dhcp_v6_state() { + assert_eq!(map_ipv6_config_method_to_dhcp_v6("Automatic"), None); + assert_eq!(map_ipv6_config_method_to_dhcp_v6("Manual"), Some(false)); + assert_eq!(map_ipv6_config_method_to_dhcp_v6("LinkLocal"), Some(false)); + assert_eq!(map_ipv6_config_method_to_dhcp_v6("Off"), Some(false)); + assert_eq!(map_ipv6_config_method_to_dhcp_v6("Unknown"), None); + } + + #[test] + fn parses_preferences_network_service_metadata() { + let plist = br#" + + + + CurrentSet + /Sets/SET + NetworkServices + + SERVICE + + IPv4 + + ConfigMethod + DHCP + + Interface + + DeviceName + en1 + Hardware + AirPort + UserDefinedName + Wi-Fi + + UserDefinedName + Wi-Fi + IPv6 + + ConfigMethod + Automatic + + + + Sets + + SET + + Network + + Global + + IPv4 + + ServiceOrder + + SERVICE + + + + + + + +"#; + + let map = load_sc_preferences_plist_map(plist); + let iface = map.get("en1").unwrap(); + assert_eq!(iface.friendly_name.as_deref(), Some("Wi-Fi")); + assert_eq!(iface.sc_type.as_deref(), Some("AirPort")); + assert_eq!(iface.dhcp_v4_enabled, Some(true)); + assert_eq!(iface.dhcp_v6_enabled, None); + } +} diff --git a/src/os/unix/interface.rs b/src/os/unix/interface.rs index 6054a40..fd4f805 100644 --- a/src/os/unix/interface.rs +++ b/src/os/unix/interface.rs @@ -165,6 +165,8 @@ fn unix_interfaces_inner( transmit_speed: None, receive_speed: None, auto_negotiate: None, + dhcp_v4_enabled: None, + dhcp_v6_enabled: None, stats, #[cfg(feature = "gateway")] gateway: None, diff --git a/src/os/windows/interface.rs b/src/os/windows/interface.rs index 9cc8254..b93397c 100644 --- a/src/os/windows/interface.rs +++ b/src/os/windows/interface.rs @@ -2,6 +2,7 @@ use std::net::IpAddr; use windows_sys::Win32::Foundation::{ERROR_BUFFER_OVERFLOW, NO_ERROR}; use windows_sys::Win32::NetworkManagement::IpHelper::{ GAA_FLAG_INCLUDE_GATEWAYS, GetAdaptersAddresses, IP_ADAPTER_ADDRESSES_LH, + IP_ADAPTER_DHCP_ENABLED, }; use windows_sys::Win32::NetworkManagement::Ndis::NET_IF_OPER_STATUS_UP; use windows_sys::Win32::Networking::WinSock::{ @@ -269,6 +270,10 @@ pub fn interfaces() -> Vec { transmit_speed: sanitize_u64(cur.TransmitLinkSpeed), receive_speed: sanitize_u64(cur.ReceiveLinkSpeed), auto_negotiate: None, + dhcp_v4_enabled: Some( + unsafe { cur.Anonymous2.Flags } & IP_ADAPTER_DHCP_ENABLED != 0, + ), + dhcp_v6_enabled: None, stats, #[cfg(feature = "gateway")] gateway: if default_gateway.mac_addr == MacAddr::zero() {