//! Network interface enumeration and 802.11 wireless phy identification. //! //! Reads directly from `/sys/class/net` on Linux, falling back to `iw` for //! detailed interface mode inspection when available. use crate::error::{Result, SabiaError}; use std::fs; use std::path::{Path, PathBuf}; #[derive(Debug, Clone, serde::Serialize)] pub struct Interface { pub name: String, pub mac: String, /// Wireless `wiphy` name (e.g. `"phy0"`), present only on 802.11 interfaces. pub phy: Option, pub mode: Mode, pub up: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)] pub enum Mode { Managed, Monitor, Unknown, } fn net_root() -> &'static Path { Path::new("/sys/class/net") } /// Lists all network interfaces on the system (wireless interfaces have `phy.is_some()`). pub fn list_all() -> Result> { let mut out = Vec::new(); for entry in fs::read_dir(net_root())? { let entry = entry?; let name = entry.file_name().to_string_lossy().into_owned(); if name == "lo" { continue; } out.push(read_interface(&name)?); } out.sort_by(|a, b| a.name.cmp(&b.name)); Ok(out) } /// Lists only 802.11 wireless interfaces (`phy80211` present). pub fn list_wireless() -> Result> { Ok(list_all()? .into_iter() .filter(|i| i.phy.is_some()) .collect()) } pub fn get(name: &str) -> Result { let iface = read_interface(name)?; if iface.phy.is_none() { return Err(SabiaError::NotWireless(name.to_string())); } Ok(iface) } fn read_interface(name: &str) -> Result { let base: PathBuf = net_root().join(name); if !base.exists() { return Err(SabiaError::InterfaceNotFound(name.to_string())); } let mac = fs::read_to_string(base.join("address")) .map(|s| s.trim().to_string()) .unwrap_or_default(); let up = fs::read_to_string(base.join("operstate")) .map(|s| s.trim() == "up") .unwrap_or(false); let phy80211 = base.join("phy80211"); let phy = if phy80211.is_dir() { fs::read_link(&phy80211) .ok() .and_then(|p| p.file_name().map(|f| f.to_string_lossy().into_owned())) .or_else(|| { // On some kernels `phy80211` is a directory rather than a symlink; // read the `"phyNN"` subdirectory inside it. fs::read_dir(&phy80211) .ok() .and_then(|mut rd| rd.next()) .and_then(|e| e.ok()) .map(|e| e.file_name().to_string_lossy().into_owned()) }) } else { None }; let mode = if phy.is_some() { read_wireless_mode(name) } else { Mode::Unknown }; Ok(Interface { name: name.to_string(), mac, phy, mode, up, }) } /// Reads the interface operating mode (`managed`/`monitor`) via `/sys/class/net//type` /// and `iw dev info` when available. /// /// fn read_wireless_mode(name: &str) -> Mode { match crate::netlink::iw_dev_type(name) { Some(Mode::Monitor) => Mode::Monitor, Some(Mode::Managed) => Mode::Managed, _ => Mode::Unknown, } }