Files
sabia/crates/sabia-core/src/netlink.rs
T
Lucas RayanandClaude Opus 5.5 791538c9d9 v0.1.0: logo, i18n pt/en, CI com caches, testes e documentação
- Logo oficial (sabia.png) e conjunto de ícones do app desktop.
- Interface em português e inglês: tradutor por observação do DOM, seletor
  de idioma e checagem de cobertura de tradução no CI.
- CI/CD com caches (rust-cache, .deb do Tauri em cache) e pipeline de release
  multiplataforma (CLI em 5 alvos + app desktop via tauri-action), templates
  de issue/PR.
- Suíte de testes de integração do sabia-core (scan e2e, handshake/crack
  4-way e PMKID, pcap, trilha de auditoria) — 46 testes verdes.
- Documentação completa pt/en: README, ROADMAP, SECURITY, CONTRIBUTING,
  CHANGELOG e docs/ (cli, interface, arquitetura), com enquadramento de uso
  autorizado.
- Correções de clippy (-D warnings) e formatação em toda a árvore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:46:19 -03:00

253 lines
7.4 KiB
Rust

//! 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<f32>,
}
fn run(cmd: &str, args: &[&str]) -> Result<String> {
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<Mode> {
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<String> {
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<Vec<ChannelInfo>> {
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<Vec<u32>> {
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<Vec<u32>> {
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<u32> = 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<String> {
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<ChannelInfo> {
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::<f32>() 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::<u32>() 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::<f32>().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);
}
}