Esqueleto inicial: sabia-core + sabia-cli + UI local, OpenWolf

- sabia-core: descoberta de interfaces, controle de modo monitor/canal
  via iw (ponte até ter nl80211 nativo), captura crua 802.11 via
  AF_PACKET (sem libpcap), parsing de radiotap e quadros de
  gerenciamento (beacon/probe/EAPOL), agregação em tabela de APs/
  clientes, escrita de .pcap, trilha de auditoria encadeada por hash,
  auto-atualização via GitHub releases (curl, sem dependências de
  rede extras)
- sabia-cli: `sabia iface`, `sabia scan`, `sabia update`,
  `sabia audit-verify`, `sabia ui` (servidor local + navegador,
  protegido por token, mesma filosofia do Guará)
- OpenWolf inicializado para memória de projeto entre agentes
This commit is contained in:
Lucas committed 2026-10-23 08:22:04 -03:00
commit e989413018
76 files changed
+8386

No files matched your search

+110
View File
@@ -0,0 +1,110 @@
//! Descoberta de interfaces de rede e identificação das que são Wi-Fi.
//!
//! Não depende de `iw`: lê direto de `/sys/class/net`, do mesmo jeito que o
//! próprio `iw`/`ip` fazem por baixo.
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,
/// Caminho do phy (ex.: `phy0`), presente só em interfaces 802.11.
pub phy: Option<String>,
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")
}
/// Lista todas as interfaces de rede, Wi-Fi ou não (chamador filtra por `phy`).
pub fn list_all() -> Result<Vec<Interface>> {
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)
}
/// Lista apenas interfaces com um `phy80211` (ou seja, rádios Wi-Fi).
pub fn list_wireless() -> Result<Vec<Interface>> {
Ok(list_all()?.into_iter().filter(|i| i.phy.is_some()).collect())
}
pub fn get(name: &str) -> Result<Interface> {
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<Interface> {
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(|| {
// em alguns kernels phy80211 é um diretório real, não symlink;
// o nome do phy então vem do subdiretório "phyNN" dentro dele.
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 })
}
/// Lê o modo (`managed`/`monitor`) via `/sys/class/net/<if>/type` + `uevent`
/// não é suficiente (type é sempre ARPHRD_ETHER para Wi-Fi); a forma estável
/// sem libnl é delegar a leitura real ao `iw dev <if> info` quando disponível,
/// mantendo aqui um melhor-esforço local para não depender de root.
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,
}
}