//! Controle de modo (managed/monitor), canal e leitura das capacidades do //! rádio via `iw`. //! //! É uma ponte deliberada: o protocolo real é nl80211 por netlink, e trocar //! este módulo por uma implementação nativa (sem depender dos binários `iw` e //! `ip`) é o próximo passo natural. Por ora `iw`/`ip` são universais em //! Linux, testados, e evitam reimplementar genl/nl80211 antes de validar o //! pipeline com hardware real. use crate::error::{Result, SabiaError}; use crate::iface::Mode; use crate::radiotap::{band_for_freq, Band}; use std::process::Command; #[derive(Debug, Clone, serde::Serialize)] pub struct ChannelInfo { pub channel: u32, pub freq_mhz: u16, pub band: Band, pub disabled: bool, pub max_power_dbm: Option, } fn run(cmd: &str, args: &[&str]) -> Result { let out = Command::new(cmd) .args(args) .output() .map_err(|e| SabiaError::CommandFailed { cmd: format!("{cmd} {}", args.join(" ")), detail: e.to_string(), })?; if !out.status.success() { let detail = String::from_utf8_lossy(&out.stderr).trim().to_string(); let detail = if detail.is_empty() { String::from_utf8_lossy(&out.stdout).trim().to_string() } else { detail }; if detail.contains("Operation not permitted") || detail.contains("Insufficient privileges") || detail.contains("nl80211 not found") { return Err(SabiaError::PermissionDenied); } return Err(SabiaError::CommandFailed { cmd: format!("{cmd} {}", args.join(" ")), detail, }); } Ok(String::from_utf8_lossy(&out.stdout).into_owned()) } pub fn command_exists(name: &str) -> bool { std::env::var_os("PATH") .map(|paths| { std::env::split_paths(&paths).any(|dir| { let p = dir.join(name); p.is_file() }) }) .unwrap_or(false) } pub fn is_root() -> bool { #[cfg(unix)] { unsafe { libc::geteuid() == 0 } } #[cfg(not(unix))] { false } } pub fn iw_dev_type(iface: &str) -> Option { let out = run("iw", &["dev", iface, "info"]).ok()?; for line in out.lines() { let line = line.trim(); if let Some(t) = line.strip_prefix("type ") { return Some(match t.trim() { "monitor" => Mode::Monitor, "managed" => Mode::Managed, _ => Mode::Unknown, }); } } None } pub fn phy_for_iface(iface: &str) -> Result { let out = run("iw", &["dev", iface, "info"])?; out.lines() .find_map(|l| l.trim().strip_prefix("wiphy ")) .map(|n| format!("phy{n}")) .ok_or_else(|| SabiaError::CommandFailed { cmd: "iw dev info".into(), detail: "wiphy não encontrado na saída".into(), }) } /// Lista canais suportados pelo phy da interface. pub fn channels(iface: &str) -> Result> { let phy = phy_for_iface(iface)?; let info = run("iw", &["phy", &phy, "info"])?; Ok(parse_phy_channels(&info)) } pub fn supported_channels(iface: &str) -> Result> { Ok(channels(iface)? .into_iter() .filter(|c| !c.disabled) .map(|c| c.channel) .collect()) } /// Canais a percorrer. `band` aceita `"2.4"`, `"5"`, `"6"`, `"all"`/`None`. pub fn channel_plan(iface: &str, band: Option<&str>) -> Result> { let want = match band.map(|b| b.trim().to_ascii_lowercase()) { None => None, Some(b) if b.is_empty() || b == "all" || b == "auto" => None, Some(b) => Some(match b.as_str() { "2.4" | "2.4ghz" | "2g" => Band::B2G4, "5" | "5ghz" | "5g" => Band::B5, "6" | "6ghz" | "6g" => Band::B6, other => { return Err(SabiaError::InvalidInput(format!( "banda `{other}` inválida (use 2.4, 5, 6 ou all)" ))) } }), }; let mut v: Vec = channels(iface)? .into_iter() .filter(|c| !c.disabled) .filter(|c| want.map_or(true, |w| c.band == w)) .map(|c| c.channel) .collect(); v.sort_unstable(); v.dedup(); Ok(v) } pub fn monitor_supported(iface: &str) -> bool { let Ok(phy) = phy_for_iface(iface) else { return false; }; let Ok(info) = run("iw", &["phy", &phy, "info"]) else { return false; }; info.lines().any(|l| l.trim() == "* monitor") } pub fn driver_name(iface: &str) -> Option { let path = std::path::Path::new("/sys/class/net") .join(iface) .join("device/driver"); std::fs::read_link(path) .ok() .and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned())) } /// Põe a interface em modo monitor. Requer `CAP_NET_ADMIN` (normalmente root). pub fn set_monitor_mode(iface: &str, enable: bool) -> Result<()> { run("ip", &["link", "set", iface, "down"])?; let target = if enable { "monitor" } else { "managed" }; run("iw", &["dev", iface, "set", "type", target])?; run("ip", &["link", "set", iface, "up"])?; Ok(()) } /// Troca o canal atual da interface (precisa já estar em modo monitor). pub fn set_channel(iface: &str, channel: u32) -> Result<()> { run( "iw", &["dev", iface, "set", "channel", &channel.to_string()], )?; Ok(()) } pub fn parse_phy_channels(text: &str) -> Vec { let mut out = Vec::new(); for line in text.lines() { let line = line.trim(); let Some(rest) = line.strip_prefix("* ") else { continue; }; let Some(mhz_at) = rest.find(" MHz") else { continue; }; let Ok(freq) = rest[..mhz_at].trim().parse::() else { continue; }; let Some(open) = rest.find('[') else { continue; }; let Some(close_rel) = rest[open..].find(']') else { continue; }; let Ok(channel) = rest[open + 1..open + close_rel].parse::() else { continue; }; let freq = freq.round() as u16; let disabled = rest.contains("(disabled)") || rest.contains("no IR"); let max_power_dbm = rest .rfind('(') .and_then(|p| rest[p + 1..].strip_suffix(" dBm)")) .and_then(|v| v.trim().parse::().ok()); out.push(ChannelInfo { channel, freq_mhz: freq, band: band_for_freq(freq), disabled, max_power_dbm, }); } out } #[cfg(test)] mod tests { use super::*; const SAMPLE: &str = "\ Wiphy phy0 \tBand 1: \t\tFrequencies: \t\t\t* 2412.0 MHz [1] (20.0 dBm) \t\t\t* 2437.0 MHz [6] (20.0 dBm) \t\t\t* 2484.0 MHz [14] (disabled) \tBand 2: \t\tFrequencies: \t\t\t* 5180.0 MHz [36] (23.0 dBm) \t\t\t* 5200.0 MHz [40] (23.0 dBm) \tBand 4: \t\tFrequencies: \t\t\t* 5955.0 MHz [1] (12.0 dBm) "; #[test] fn parses_iw_phy_output() { let ch = parse_phy_channels(SAMPLE); assert_eq!(ch.len(), 6); assert_eq!(ch[0].channel, 1); assert_eq!(ch[0].freq_mhz, 2412); assert_eq!(ch[0].band, Band::B2G4); assert_eq!(ch[0].max_power_dbm, Some(20.0)); assert!(ch[2].disabled); assert_eq!(ch[3].band, Band::B5); assert_eq!(ch[5].band, Band::B6); assert_eq!(ch[5].channel, 1); } }