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>
This commit is contained in:
1 parent
df27974359
commit
791538c9d9
112 files changed
+15620
-494
No files matched your search
@@ -1,15 +1,26 @@
|
||||
//! Controle de modo (managed/monitor) e canal via `iw`.
|
||||
//! Controle de modo (managed/monitor), canal e leitura das capacidades do
|
||||
//! rádio via `iw`.
|
||||
//!
|
||||
//! Isso é uma ponte deliberada: o protocolo real é nl80211 por netlink, e
|
||||
//! trocar este módulo por uma implementação nativa (sem depender do binário
|
||||
//! `iw` instalado) é o próximo passo natural. Por ora, `iw` é amplamente
|
||||
//! disponível, testado e evita reimplementar genl/nl80211 antes de validar
|
||||
//! o resto do pipeline com o adaptador real.
|
||||
//! É 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)
|
||||
@@ -25,7 +36,10 @@ fn run(cmd: &str, args: &[&str]) -> Result<String> {
|
||||
} else {
|
||||
detail
|
||||
};
|
||||
if detail.contains("Operation not permitted") || detail.contains("nl80211 not found") {
|
||||
if detail.contains("Operation not permitted")
|
||||
|| detail.contains("Insufficient privileges")
|
||||
|| detail.contains("nl80211 not found")
|
||||
{
|
||||
return Err(SabiaError::PermissionDenied);
|
||||
}
|
||||
return Err(SabiaError::CommandFailed {
|
||||
@@ -36,6 +50,28 @@ fn run(cmd: &str, args: &[&str]) -> Result<String> {
|
||||
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() {
|
||||
@@ -51,35 +87,76 @@ pub fn iw_dev_type(iface: &str) -> Option<Mode> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Lista (canal, frequência) suportados pelo phy da interface.
|
||||
pub fn supported_channels(iface: &str) -> Result<Vec<u32>> {
|
||||
pub fn phy_for_iface(iface: &str) -> Result<String> {
|
||||
let out = run("iw", &["dev", iface, "info"])?;
|
||||
let phy = out
|
||||
.lines()
|
||||
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"])?;
|
||||
let mut channels = Vec::new();
|
||||
for line in info.lines() {
|
||||
let line = line.trim();
|
||||
// formato: "* 2412 MHz [1] (20.0 dBm)"
|
||||
if let Some(rest) = line.strip_prefix("* ") {
|
||||
if let Some(start) = rest.find('[') {
|
||||
if let Some(end) = rest[start..].find(']') {
|
||||
if let Ok(ch) = rest[start + 1..start + end].parse::<u32>() {
|
||||
channels.push(ch);
|
||||
}
|
||||
}
|
||||
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)"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
channels.sort_unstable();
|
||||
channels.dedup();
|
||||
Ok(channels)
|
||||
}),
|
||||
};
|
||||
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).
|
||||
@@ -99,3 +176,77 @@ pub fn set_channel(iface: &str, channel: u32) -> Result<()> {
|
||||
)?;
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user