Files
sabia/crates/sabia-core/src/radiotap.rs
T
Lucas e989413018 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
2026-10-23 08:22:04 -03:00

104 lines
3.0 KiB
Rust

//! Parser mínimo de cabeçalho Radiotap — só os campos que usamos
//! (canal/frequência e força de sinal). Especificação:
//! <https://www.radiotap.org/>
#[derive(Debug, Clone, Copy, Default)]
pub struct RadiotapInfo {
pub freq_mhz: Option<u16>,
pub signal_dbm: Option<i8>,
pub header_len: usize,
}
// Índices de bit no bitmap "present" que nos interessam.
const BIT_FLAGS: u32 = 1 << 1;
const BIT_RATE: u32 = 1 << 2;
const BIT_CHANNEL: u32 = 1 << 3;
const BIT_ANTSIGNAL: u32 = 1 << 5;
/// Retorna as infos extraídas e quantos bytes o cabeçalho ocupa (para o
/// chamador saber onde o frame 802.11 propriamente começa).
pub fn parse(buf: &[u8]) -> Option<RadiotapInfo> {
if buf.len() < 8 || buf[0] != 0 {
return None; // version deve ser 0
}
let header_len = u16::from_le_bytes([buf[2], buf[3]]) as usize;
if header_len > buf.len() || header_len < 8 {
return None;
}
// Pode haver múltiplas palavras de "present" (bit 31 = continua).
let mut present_words = Vec::new();
let mut offset = 4;
loop {
if offset + 4 > buf.len() {
return None;
}
let word = u32::from_le_bytes([buf[offset], buf[offset + 1], buf[offset + 2], buf[offset + 3]]);
present_words.push(word);
offset += 4;
if word & (1 << 31) == 0 {
break;
}
}
let present = present_words[0]; // só o primeiro conjunto de campos padrão nos interessa
let mut info = RadiotapInfo { header_len, ..Default::default() };
let mut cursor = offset;
// TSFT (u64, align 8) viria antes de Flags; para o nosso subconjunto de
// campos simplificamos assumindo captura via mac80211 (ordem padrão:
// TSFT, Flags, Rate, Channel, ..., AntSignal). Avançamos campo a campo
// respeitando alinhamento, sem travar se algum bit não estiver setado.
macro_rules! align {
($a:expr) => {{
let rem = cursor % $a;
if rem != 0 {
cursor += $a - rem;
}
}};
}
if present & (1 << 0) != 0 {
align!(8);
cursor += 8; // TSFT
}
if present & BIT_FLAGS != 0 {
align!(1);
cursor += 1;
}
if present & BIT_RATE != 0 {
align!(1);
cursor += 1;
}
if present & BIT_CHANNEL != 0 {
align!(2);
if cursor + 4 <= buf.len() {
let freq = u16::from_le_bytes([buf[cursor], buf[cursor + 1]]);
info.freq_mhz = Some(freq);
}
cursor += 4;
}
// FHSS (bit 4) — raro, ignorado.
if present & (1 << 4) != 0 {
align!(2);
cursor += 2;
}
if present & BIT_ANTSIGNAL != 0 {
align!(1);
if cursor < buf.len() {
info.signal_dbm = Some(buf[cursor] as i8);
}
}
Some(info)
}
pub fn freq_to_channel(freq_mhz: u16) -> Option<u32> {
match freq_mhz {
2412..=2472 => Some(((freq_mhz - 2412) / 5 + 1) as u32),
2484 => Some(14),
5000..=5900 => Some(((freq_mhz - 5000) / 5) as u32),
_ => None,
}
}