Skip to main content

zond_engine/system/interface/
os.rs

1// Copyright (c) 2026 Erik Lening (hollowpointer) and Contributors
2//
3// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
4// If a copy of the MPL was not distributed with this file, You can obtain one at
5// https://mozilla.org/MPL/2.0/.
6
7//! Hardware detection heuristics specialized for target Operating Systems.
8//!
9//! Bridges platform-specific techniques (sysfs, CLI tools, Windows API)
10//! to classify network adapters seamlessly.
11
12use pnet::datalink::NetworkInterface;
13
14#[cfg(target_os = "linux")]
15#[doc(inline)]
16pub use linux_impl::{is_physical, is_wireless};
17#[cfg(target_os = "macos")]
18#[doc(inline)]
19pub use macos_impl::{is_physical, is_wireless};
20#[cfg(target_os = "windows")]
21#[doc(inline)]
22pub use windows_impl::{is_physical, is_wireless};
23
24/// Determines if the interface corresponds to a physical adapter (not virtual).
25#[cfg(target_os = "linux")]
26pub mod linux_impl {
27    use super::*;
28    use std::path::Path;
29
30    pub fn is_physical(interface: &NetworkInterface) -> bool {
31        Path::new(&format!("/sys/class/net/{}/device", interface.name)).exists()
32    }
33
34    pub fn is_wireless(interface: &NetworkInterface) -> bool {
35        Path::new(&format!("sys/class/net/{}/wireless", interface.name)).exists()
36    }
37}
38
39#[cfg(target_os = "macos")]
40pub mod macos_impl {
41    use super::*;
42    use std::collections::HashSet;
43    use std::process::Command;
44    use std::sync::OnceLock;
45
46    /// A struct to hold the cached hardware information
47    struct HardwareInfo {
48        physical_devices: HashSet<String>,
49        wireless_devices: HashSet<String>,
50    }
51
52    /// Singleton that runs the shell commands only once on first access.
53    fn get_hardware_info() -> &'static HardwareInfo {
54        static HARDWARE_INFO: OnceLock<HardwareInfo> = OnceLock::new();
55
56        HARDWARE_INFO.get_or_init(|| {
57            let mut physical = HashSet::new();
58            let mut wireless = HashSet::new();
59
60            // Get Physical Ports (Wired & Wireless hardware)
61            if let Ok(output) = Command::new("networksetup")
62                .arg("-listallhardwareports")
63                .output()
64            {
65                let stdout = String::from_utf8_lossy(&output.stdout);
66                for line in stdout.lines() {
67                    if let Some(device) = line.strip_prefix("Device: ") {
68                        physical.insert(device.trim().to_string());
69                    }
70                }
71            }
72
73            // Identify which of those are specifically Wireless
74            for device in &physical {
75                let is_wifi = Command::new("networksetup")
76                    .arg("-getairportnetwork")
77                    .arg(device)
78                    .output()
79                    .map(|out| out.status.success())
80                    .unwrap_or(false);
81
82                if is_wifi {
83                    wireless.insert(device.clone());
84                }
85            }
86
87            HardwareInfo {
88                physical_devices: physical,
89                wireless_devices: wireless,
90            }
91        })
92    }
93
94    pub fn is_physical(interface: &NetworkInterface) -> bool {
95        get_hardware_info()
96            .physical_devices
97            .contains(&interface.name)
98    }
99
100    pub fn is_wireless(interface: &NetworkInterface) -> bool {
101        get_hardware_info()
102            .wireless_devices
103            .contains(&interface.name)
104    }
105}
106
107#[cfg(target_os = "windows")]
108pub mod windows_impl {
109    use super::*;
110    use std::cell::RefCell;
111    use std::collections::HashSet;
112    use std::time::{Duration, Instant};
113
114    use windows_sys::Win32::NetworkManagement::IpHelper::{
115        FreeMibTable, GetIfTable2, MIB_IF_ROW2, MIB_IF_TABLE2,
116    };
117
118    const IF_TYPE_SOFTWARE_LOOPBACK: u32 = 24;
119    const IF_TYPE_TUNNEL: u32 = 131;
120    const IF_TYPE_IEEE80211: u32 = 71;
121    const IF_TYPE_PPP: u32 = 23;
122
123    struct WindowsInterfaceInfo {
124        physical_devices: HashSet<String>,
125        wireless_devices: HashSet<String>,
126    }
127
128    // Thread-local cache prevents the N+1 FFI performance trap when iterating,
129    // but the 2-second TTL ensures we still support hot-plugged devices across scans.
130    thread_local! {
131        static INTERFACE_CACHE: RefCell<Option<(Instant, WindowsInterfaceInfo)>> = RefCell::new(None);
132    }
133
134    fn fetch_windows_interface_info() -> WindowsInterfaceInfo {
135        let mut physical = HashSet::new();
136        let mut wireless = HashSet::new();
137
138        unsafe {
139            let mut table_ptr: *mut MIB_IF_TABLE2 = std::ptr::null_mut();
140            if GetIfTable2(&mut table_ptr) == 0 && !table_ptr.is_null() {
141                let table = &*table_ptr;
142                let rows = std::slice::from_raw_parts(
143                    &table.Table[0] as *const MIB_IF_ROW2,
144                    table.NumEntries as usize,
145                );
146
147                for row in rows {
148                    let is_software = row.Type == IF_TYPE_SOFTWARE_LOOPBACK
149                        || row.Type == IF_TYPE_TUNNEL
150                        || row.Type == IF_TYPE_PPP;
151
152                    let bitfield = row.InterfaceAndOperStatusFlags._bitfield;
153                    let is_hardware_bit = (bitfield & 1) != 0;
154                    let has_connector_bit = (bitfield & (1 << 2)) != 0;
155
156                    let guid = row.InterfaceGuid;
157                    let guid_str = format!(
158                        "{{{:08X}-{:04X}-{:04X}-{:02X}{:02X}-{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}}}",
159                        guid.data1,
160                        guid.data2,
161                        guid.data3,
162                        guid.data4[0],
163                        guid.data4[1],
164                        guid.data4[2],
165                        guid.data4[3],
166                        guid.data4[4],
167                        guid.data4[5],
168                        guid.data4[6],
169                        guid.data4[7]
170                    );
171
172                    // Must check !is_software first to prevent spoofed connector bits
173                    if !is_software && (is_hardware_bit || has_connector_bit) {
174                        physical.insert(guid_str.clone());
175                    }
176
177                    if row.Type == IF_TYPE_IEEE80211 {
178                        wireless.insert(guid_str);
179                    }
180                }
181                FreeMibTable(table_ptr as *mut std::ffi::c_void);
182            }
183        }
184
185        WindowsInterfaceInfo {
186            physical_devices: physical,
187            wireless_devices: wireless,
188        }
189    }
190
191    /// Helper to access the cache, updating it if necessary (older than 2 seconds)
192    fn with_cache<F, R>(f: F) -> R
193    where
194        F: FnOnce(&WindowsInterfaceInfo) -> R,
195    {
196        INTERFACE_CACHE.with(|cache| {
197            let mut cache_ref = cache.borrow_mut();
198            let now = Instant::now();
199
200            let needs_update = match cache_ref.as_ref() {
201                Some((timestamp, _)) => now.duration_since(*timestamp) > Duration::from_secs(2),
202                None => true,
203            };
204
205            if needs_update {
206                *cache_ref = Some((now, fetch_windows_interface_info()));
207            }
208
209            // Safe to unwrap because we just guaranteed it's populated above
210            f(&cache_ref.as_ref().unwrap().1)
211        })
212    }
213
214    pub fn is_physical(interface: &NetworkInterface) -> bool {
215        with_cache(|info| {
216            let name = interface
217                .name
218                .strip_prefix(r"\Device\NPF_")
219                .unwrap_or(&interface.name);
220            info.physical_devices.contains(name)
221        })
222    }
223
224    pub fn is_wireless(interface: &NetworkInterface) -> bool {
225        with_cache(|info| {
226            let name = interface
227                .name
228                .strip_prefix(r"\Device\NPF_")
229                .unwrap_or(&interface.name);
230            info.wireless_devices.contains(name)
231        })
232    }
233}