#[cfg(any(target_os = "windows", test))]
const IF_TYPE_SOFTWARE_LOOPBACK: u32 = 24;
#[cfg(any(target_os = "windows", test))]
const FILTER_INTERFACE_FLAG: u8 = 1 << 1;
#[cfg(target_os = "windows")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct IfRow {
pub name: String,
pub in_octets: u64,
pub out_octets: u64,
}
#[cfg(any(target_os = "windows", test))]
pub(crate) fn is_reportable_interface(if_type: u32, flags: u8) -> bool {
if_type != IF_TYPE_SOFTWARE_LOOPBACK && (flags & FILTER_INTERFACE_FLAG) == 0
}
#[cfg(target_os = "windows")]
pub(crate) fn interfaces() -> Vec<IfRow> {
ffi::enumerate()
}
#[cfg(target_os = "windows")]
pub(crate) fn excluded_interface_names() -> Vec<String> {
ffi::enumerate_excluded()
}
#[cfg(target_os = "windows")]
mod ffi {
use super::{is_reportable_interface, IfRow};
use std::ffi::c_void;
use std::ptr;
#[repr(C)]
pub(super) struct MibIfRow2 {
interface_luid: u64,
interface_index: u32,
interface_guid: [u8; 16],
alias: [u16; 257],
description: [u16; 257],
physical_address_length: u32,
physical_address: [u8; 32],
permanent_physical_address: [u8; 32],
mtu: u32,
if_type: u32,
tunnel_type: u32,
media_type: u32,
physical_medium_type: u32,
access_type: u32,
direction_type: u32,
interface_and_oper_status_flags: u8,
oper_status: u32,
admin_status: u32,
media_connect_state: u32,
network_guid: [u8; 16],
connection_type: u32,
transmit_link_speed: u64,
receive_link_speed: u64,
in_octets: u64,
in_ucast_pkts: u64,
in_nucast_pkts: u64,
in_discards: u64,
in_errors: u64,
in_unknown_protos: u64,
in_ucast_octets: u64,
in_multicast_octets: u64,
in_broadcast_octets: u64,
out_octets: u64,
out_ucast_pkts: u64,
out_nucast_pkts: u64,
out_discards: u64,
out_errors: u64,
out_ucast_octets: u64,
out_multicast_octets: u64,
out_broadcast_octets: u64,
out_qlen: u64,
}
#[repr(C)]
pub(super) struct MibIfTable2 {
num_entries: u32,
table: [MibIfRow2; 1],
}
#[link(name = "iphlpapi")]
extern "system" {
fn GetIfTable2(table: *mut *mut MibIfTable2) -> u32;
fn FreeMibTable(memory: *mut c_void);
}
fn with_rows<T>(mut f: impl FnMut(&MibIfRow2) -> Option<T>) -> Vec<T> {
let mut table: *mut MibIfTable2 = ptr::null_mut();
let rc = unsafe { GetIfTable2(&mut table) };
if rc != 0 || table.is_null() {
return Vec::new();
}
let out = unsafe {
let count = (*table).num_entries as usize;
let rows = ptr::addr_of!((*table).table) as *const MibIfRow2;
(0..count).filter_map(|i| f(&*rows.add(i))).collect()
};
unsafe { FreeMibTable(table as *mut c_void) };
out
}
pub(super) fn enumerate() -> Vec<IfRow> {
let mut out = with_rows(|row| {
if !is_reportable_interface(row.if_type, row.interface_and_oper_status_flags) {
return None;
}
let name = wide_to_string(&row.alias);
if name.is_empty() {
return None;
}
Some(IfRow {
name,
in_octets: row.in_octets,
out_octets: row.out_octets,
})
});
out.sort_by(|a, b| a.name.cmp(&b.name));
out
}
pub(super) fn enumerate_excluded() -> Vec<String> {
with_rows(|row| {
if is_reportable_interface(row.if_type, row.interface_and_oper_status_flags) {
return None;
}
let name = wide_to_string(&row.alias);
if name.is_empty() {
None
} else {
Some(name)
}
})
}
fn wide_to_string(buf: &[u16]) -> String {
let end = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
String::from_utf16_lossy(&buf[..end]).trim().to_string()
}
#[cfg(test)]
mod layout {
use std::mem::{offset_of, size_of};
#[test]
fn ffi_struct_layout() {
assert_eq!(size_of::<super::MibIfRow2>(), 1352);
assert_eq!(offset_of!(super::MibIfRow2, alias), 28);
assert_eq!(offset_of!(super::MibIfRow2, description), 542);
assert_eq!(offset_of!(super::MibIfRow2, if_type), 1128);
assert_eq!(
offset_of!(super::MibIfRow2, interface_and_oper_status_flags),
1152
);
assert_eq!(offset_of!(super::MibIfRow2, in_octets), 1208);
assert_eq!(offset_of!(super::MibIfRow2, out_octets), 1280);
assert_eq!(offset_of!(super::MibIfTable2, table), 8);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const IF_ROWS: &[(&str, u32, u8)] = &[
(
"Local Area Connection* 6-QoS Packet Scheduler-0000",
6,
0b0000_0010,
),
("Bluetooth Network Connection", 6, 0b0001_0000),
("Ethernet", 6, 0b0000_0101),
("Local Area Connection* 6", 6, 0b0000_0000),
("Loopback Pseudo-Interface 1", 24, 0b0000_0000),
("wt0", 53, 0b0000_0000),
(
"Wi-Fi-WFP Native MAC Layer LightWeight Filter-0000",
71,
0b0000_0010,
),
("Wi-Fi-Native WiFi Filter Driver-0000", 71, 0b0000_0010),
("Wi-Fi-QoS Packet Scheduler-0000", 71, 0b0000_0010),
(
"Wi-Fi-WFP 802.3 MAC Layer LightWeight Filter-0000",
71,
0b0000_0010,
),
("Wi-Fi", 71, 0b0000_0101),
("Teredo Tunneling Pseudo-Interface", 131, 0b0000_0000),
];
#[test]
fn test_is_reportable_interface_drops_ndis_filter_duplicates() {
let kept: Vec<&str> = IF_ROWS
.iter()
.filter(|(_, if_type, flags)| is_reportable_interface(*if_type, *flags))
.map(|(alias, _, _)| *alias)
.collect();
assert_eq!(
kept,
vec![
"Bluetooth Network Connection",
"Ethernet",
"Local Area Connection* 6",
"wt0",
"Wi-Fi",
"Teredo Tunneling Pseudo-Interface",
]
);
assert_eq!(kept.iter().filter(|a| a.starts_with("Wi-Fi")).count(), 1);
}
#[test]
fn test_is_reportable_interface_keeps_a_non_hardware_tunnel() {
assert!(is_reportable_interface(53, 0b0000_0000));
assert!(is_reportable_interface(71, 0b0000_0101));
}
#[test]
fn test_is_reportable_interface_drops_loopback() {
assert!(!is_reportable_interface(IF_TYPE_SOFTWARE_LOOPBACK, 0));
}
#[test]
fn test_excluded_names_are_the_complement() {
let excluded: Vec<&str> = IF_ROWS
.iter()
.filter(|(_, if_type, flags)| !is_reportable_interface(*if_type, *flags))
.map(|(alias, _, _)| *alias)
.collect();
assert!(excluded.contains(&"Wi-Fi-Native WiFi Filter Driver-0000"));
assert!(excluded.contains(&"Loopback Pseudo-Interface 1"));
assert!(!excluded.contains(&"Wi-Fi"));
assert!(!excluded.contains(&"wt0"));
}
}