Files
sabia/apps/gui/src-tauri/src/main.rs
T
Lucas Rayan a3ca73e610 feat: orquestra as novas funções no engine, CLI e app desktop; resiliência de rede
- engine.rs: expõe Evil Twin (PSK/Enterprise), crack com regras/máscara,
  relatório e exportação .hc22000 pros três front-ends (CLI, servidor web
  da UI, Tauri). set_monitor() agora solta a interface do NetworkManager
  antes de ativar modo monitor e devolve ao desativar — sem isso o NM
  reafirma a própria conexão por cima e a captura cai sozinha pouco
  depois ("está em modo managed"). Evil Twin grava um arquivo de estado
  em disco antes de soltar as interfaces; se o processo morrer no meio
  (Ctrl+C sem handler, kill -9, queda de energia), o próximo início do
  engine desfaz tudo sozinho (self_heal_eviltwin_leftovers) — sem isso as
  duas interfaces ficavam presas em managed=no pro NetworkManager pra
  sempre e o Wi-Fi inteiro sumia do painel do sistema. hostapd/hostapd-wpe
  que morrem na hora de subir agora reportam a saída real capturada em
  log, em vez de um processo "<defunct>" mudo. Novo
  ScanSnapshot.warning: avisa quando a sessão roda >12s sem receber
  nenhum quadro — sintoma de placa que aceita ativar modo monitor sem
  entregar captura de verdade (comum em Wi-Fi interno de notebook).
- sabia-cli (main.rs/web/mod.rs) e o app desktop (Tauri main.rs) ganham
  as rotas/comandos equivalentes pra tudo isso.
2026-10-24 14:54:35 -03:00

713 lines
20 KiB
Rust

// Evita a janela de console extra no Windows em release.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use sabia_core::engine::{Engine, ScanOptions};
use std::sync::Arc;
struct State(Arc<Engine>);
#[tauri::command]
fn version(state: tauri::State<'_, State>) -> String {
state.0.version().to_string()
}
#[tauri::command]
fn list_ifaces(state: tauri::State<'_, State>) -> Result<serde_json::Value, String> {
let list = state.0.list_ifaces().map_err(|e| e.to_string())?;
serde_json::to_value(list).map_err(|e| e.to_string())
}
#[tauri::command]
fn set_monitor(state: tauri::State<'_, State>, iface: String, enable: bool) -> Result<(), String> {
state
.0
.set_monitor(&iface, enable)
.map_err(|e| e.to_string())
}
#[tauri::command]
fn start_scan(state: tauri::State<'_, State>, iface: String) -> Result<(), String> {
state.0.start_scan(&iface).map_err(|e| e.to_string())
}
#[tauri::command]
fn stop_scan(state: tauri::State<'_, State>, iface: String) {
state.0.stop_scan(&iface);
}
#[tauri::command]
fn scan_results(
state: tauri::State<'_, State>,
iface: String,
) -> Result<serde_json::Value, String> {
serde_json::to_value(state.0.scan_results(&iface)).map_err(|e| e.to_string())
}
#[tauri::command]
fn scan_snapshot(
state: tauri::State<'_, State>,
iface: String,
) -> Result<serde_json::Value, String> {
let snapshot = state.0.scan_snapshot(&iface).map_err(|e| e.to_string())?;
serde_json::to_value(snapshot).map_err(|e| e.to_string())
}
#[tauri::command]
fn scan_clients(
state: tauri::State<'_, State>,
iface: String,
) -> Result<serde_json::Value, String> {
serde_json::to_value(state.0.scan_clients(&iface)).map_err(|e| e.to_string())
}
#[tauri::command]
fn handshakes(
state: tauri::State<'_, State>,
iface: Option<String>,
) -> Result<serde_json::Value, String> {
serde_json::to_value(state.0.handshakes(iface.as_deref())).map_err(|e| e.to_string())
}
#[tauri::command]
fn export_handshake(
state: tauri::State<'_, State>,
iface: String,
bssid: String,
station: String,
out: String,
) -> Result<serde_json::Value, String> {
let report = state
.0
.export_handshake(&iface, &bssid, &station, &out)
.map_err(|e| e.to_string())?;
serde_json::to_value(report).map_err(|e| e.to_string())
}
#[tauri::command]
fn export_all_handshakes(
state: tauri::State<'_, State>,
iface: String,
out: String,
) -> Result<serde_json::Value, String> {
let report = state
.0
.export_all_handshakes(&iface, &out)
.map_err(|e| e.to_string())?;
serde_json::to_value(report).map_err(|e| e.to_string())
}
#[tauri::command]
fn start_crack(
state: tauri::State<'_, State>,
iface: String,
wordlist: Option<String>,
bssid: Option<String>,
station: Option<String>,
rules: Option<bool>,
) -> Result<String, String> {
let rules = rules.unwrap_or(false);
let result = match wordlist.filter(|w| !w.trim().is_empty()) {
Some(wordlist) => state.0.start_crack(
&iface,
bssid.as_deref(),
station.as_deref(),
&wordlist,
rules,
),
None => state
.0
.start_crack_default(&iface, bssid.as_deref(), station.as_deref(), rules),
};
result.map_err(|e| e.to_string())
}
#[tauri::command]
fn start_crack_mask(
state: tauri::State<'_, State>,
iface: String,
mask: String,
bssid: Option<String>,
station: Option<String>,
max_candidates: Option<u64>,
) -> Result<String, String> {
state
.0
.start_crack_mask(
&iface,
bssid.as_deref(),
station.as_deref(),
&mask,
max_candidates.unwrap_or(50_000_000),
)
.map_err(|e| e.to_string())
}
#[tauri::command]
fn crack_job(state: tauri::State<'_, State>, id: String) -> Result<serde_json::Value, String> {
match state.0.crack_job(&id) {
Some(job) => serde_json::to_value(job).map_err(|e| e.to_string()),
None => Err("job não encontrado".into()),
}
}
#[tauri::command]
fn crack_jobs(state: tauri::State<'_, State>) -> Result<serde_json::Value, String> {
serde_json::to_value(state.0.crack_jobs()).map_err(|e| e.to_string())
}
#[tauri::command]
fn deauth(
state: tauri::State<'_, State>,
iface: String,
bssid: String,
station: Option<String>,
count: Option<u32>,
reason: Option<u16>,
) -> Result<serde_json::Value, String> {
let report = state
.0
.deauth(
&iface,
&bssid,
station.as_deref(),
count.unwrap_or(5),
reason.unwrap_or(7),
)
.map_err(|e| e.to_string())?;
serde_json::to_value(report).map_err(|e| e.to_string())
}
#[tauri::command]
#[allow(clippy::too_many_arguments)]
fn deauth_directional(
state: tauri::State<'_, State>,
iface: String,
bssid: String,
station: Option<String>,
count: Option<u32>,
reason: Option<u16>,
direction: Option<String>,
frame_kind: Option<String>,
) -> Result<serde_json::Value, String> {
let report = state
.0
.deauth_directional(
&iface,
&bssid,
station.as_deref(),
count.unwrap_or(5),
reason.unwrap_or(7),
direction.as_deref().unwrap_or("ap"),
frame_kind.as_deref().unwrap_or("deauth"),
)
.map_err(|e| e.to_string())?;
serde_json::to_value(report).map_err(|e| e.to_string())
}
#[tauri::command]
#[allow(clippy::too_many_arguments)]
fn start_deauth_job(
state: tauri::State<'_, State>,
iface: String,
bssid: String,
station: Option<String>,
reason: Option<u16>,
direction: Option<String>,
frame_kind: Option<String>,
interval_ms: Option<u64>,
max_secs: Option<u64>,
stop_on_handshake: Option<bool>,
) -> Result<String, String> {
state
.0
.start_deauth_job(
&iface,
&bssid,
station.as_deref(),
reason.unwrap_or(7),
direction.as_deref().unwrap_or("both"),
frame_kind.as_deref().unwrap_or("deauth"),
interval_ms.unwrap_or(120),
max_secs,
stop_on_handshake.unwrap_or(false),
)
.map_err(|e| e.to_string())
}
#[tauri::command]
fn stop_deauth_job(state: tauri::State<'_, State>, id: String) -> Result<(), String> {
state.0.stop_deauth_job(&id).map_err(|e| e.to_string())
}
#[tauri::command]
fn deauth_job(state: tauri::State<'_, State>, id: String) -> Result<serde_json::Value, String> {
match state.0.deauth_job(&id) {
Some(job) => serde_json::to_value(job).map_err(|e| e.to_string()),
None => Err("job de deauth não encontrado".into()),
}
}
#[tauri::command]
fn deauth_jobs(
state: tauri::State<'_, State>,
iface: Option<String>,
) -> Result<serde_json::Value, String> {
let jobs = state.0.deauth_jobs();
let jobs: Vec<_> = match iface {
Some(iface) => jobs
.into_iter()
.filter(|j| j.iface.eq_ignore_ascii_case(&iface))
.collect(),
None => jobs,
};
serde_json::to_value(jobs).map_err(|e| e.to_string())
}
#[tauri::command]
fn capture_handshake(
state: tauri::State<'_, State>,
iface: String,
bssid: String,
station: Option<String>,
seconds: Option<u64>,
) -> Result<String, String> {
state
.0
.capture_handshake(&iface, &bssid, station.as_deref(), seconds)
.map_err(|e| e.to_string())
}
#[tauri::command]
fn report(
state: tauri::State<'_, State>,
iface: String,
) -> Result<serde_json::Value, String> {
let model = state.0.report(&iface).map_err(|e| e.to_string())?;
serde_json::to_value(model).map_err(|e| e.to_string())
}
#[tauri::command]
fn report_html(
state: tauri::State<'_, State>,
iface: String,
inline: Option<bool>,
) -> Result<String, String> {
let model = state.0.report(&iface).map_err(|e| e.to_string())?;
let options = sabia_core::report::ReportOptions {
inline_pagedjs: inline.unwrap_or(false),
..Default::default()
};
Ok(sabia_core::report::render(&model, options))
}
#[tauri::command]
fn export_hc22000(
state: tauri::State<'_, State>,
iface: String,
bssid: String,
station: String,
out: String,
) -> Result<serde_json::Value, String> {
let report = state
.0
.export_hc22000(&iface, &bssid, &station, &out)
.map_err(|e| e.to_string())?;
serde_json::to_value(report).map_err(|e| e.to_string())
}
#[tauri::command]
fn export_all_hc22000(
state: tauri::State<'_, State>,
iface: String,
out: String,
) -> Result<serde_json::Value, String> {
let report = state
.0
.export_all_hc22000(&iface, &out)
.map_err(|e| e.to_string())?;
serde_json::to_value(report).map_err(|e| e.to_string())
}
#[tauri::command]
fn pmkid_hunt(
state: tauri::State<'_, State>,
iface: String,
bssid: String,
attempts: Option<u32>,
) -> Result<serde_json::Value, String> {
let report = state
.0
.pmkid_hunt(&iface, &bssid, attempts.unwrap_or(5))
.map_err(|e| e.to_string())?;
serde_json::to_value(report).map_err(|e| e.to_string())
}
#[tauri::command]
fn pmf_test(
state: tauri::State<'_, State>,
iface: String,
bssid: String,
station: String,
reason: Option<u16>,
) -> Result<serde_json::Value, String> {
let report = state
.0
.pmf_test(&iface, &bssid, &station, reason.unwrap_or(7))
.map_err(|e| e.to_string())?;
serde_json::to_value(report).map_err(|e| e.to_string())
}
#[tauri::command]
fn wps_tools(state: tauri::State<'_, State>) -> Result<serde_json::Value, String> {
serde_json::to_value(state.0.wps_tools()).map_err(|e| e.to_string())
}
#[tauri::command]
fn start_wps(
state: tauri::State<'_, State>,
iface: String,
bssid: String,
channel: u32,
tool: String,
pin: Option<String>,
) -> Result<String, String> {
state
.0
.start_wps(&iface, &bssid, channel, &tool, pin.as_deref())
.map_err(|e| e.to_string())
}
#[tauri::command]
fn wps_job(state: tauri::State<'_, State>, id: String) -> Result<serde_json::Value, String> {
match state.0.wps_job(&id) {
Some(job) => serde_json::to_value(job).map_err(|e| e.to_string()),
None => Err("job WPS não encontrado".into()),
}
}
#[tauri::command]
fn wps_jobs(state: tauri::State<'_, State>) -> Result<serde_json::Value, String> {
serde_json::to_value(state.0.wps_jobs()).map_err(|e| e.to_string())
}
#[tauri::command]
fn stop_wps(state: tauri::State<'_, State>, id: String) -> Result<(), String> {
state.0.stop_wps(&id).map_err(|e| e.to_string())
}
#[tauri::command]
fn start_evil_twin(
state: tauri::State<'_, State>,
iface: String,
ap_iface: String,
bssid: String,
portal_port: Option<u16>,
deauth: Option<bool>,
) -> Result<String, String> {
state
.0
.start_evil_twin(
&iface,
&ap_iface,
&bssid,
portal_port.unwrap_or(8080),
deauth.unwrap_or(false),
)
.map_err(|e| e.to_string())
}
#[tauri::command]
fn eviltwin_job(state: tauri::State<'_, State>, id: String) -> Result<serde_json::Value, String> {
match state.0.eviltwin_job(&id) {
Some(job) => serde_json::to_value(job).map_err(|e| e.to_string()),
None => Err("job de Evil Twin não encontrado".into()),
}
}
#[tauri::command]
fn eviltwin_jobs(state: tauri::State<'_, State>) -> Result<serde_json::Value, String> {
serde_json::to_value(state.0.eviltwin_jobs()).map_err(|e| e.to_string())
}
#[tauri::command]
fn stop_evil_twin(state: tauri::State<'_, State>, id: String) -> Result<(), String> {
state.0.stop_evil_twin(&id).map_err(|e| e.to_string())
}
#[tauri::command]
fn start_evil_twin_enterprise(
state: tauri::State<'_, State>,
iface: String,
ap_iface: String,
bssid: String,
) -> Result<String, String> {
state
.0
.start_evil_twin_enterprise(&iface, &ap_iface, &bssid)
.map_err(|e| e.to_string())
}
#[tauri::command]
fn eviltwin_enterprise_job(
state: tauri::State<'_, State>,
id: String,
) -> Result<serde_json::Value, String> {
match state.0.enterprise_twin_job(&id) {
Some(job) => serde_json::to_value(job).map_err(|e| e.to_string()),
None => Err("job de Evil Twin Enterprise não encontrado".into()),
}
}
#[tauri::command]
fn eviltwin_enterprise_jobs(state: tauri::State<'_, State>) -> Result<serde_json::Value, String> {
serde_json::to_value(state.0.enterprise_twin_jobs()).map_err(|e| e.to_string())
}
#[tauri::command]
fn stop_evil_twin_enterprise(state: tauri::State<'_, State>, id: String) -> Result<(), String> {
state
.0
.stop_evil_twin_enterprise(&id)
.map_err(|e| e.to_string())
}
#[tauri::command]
#[allow(clippy::too_many_arguments)]
fn start_scan_opts(
state: tauri::State<'_, State>,
iface: String,
channel: Option<u32>,
band: Option<String>,
hop_ms: Option<u64>,
pcap_out: Option<String>,
seconds: Option<u64>,
) -> Result<(), String> {
let options = ScanOptions {
channel,
band,
hop_ms: hop_ms.unwrap_or(350),
pcap_out: pcap_out.map(Into::into),
duration_secs: seconds,
};
state
.0
.start_scan_with(&iface, options)
.map_err(|e| e.to_string())
}
#[tauri::command]
fn update_check(state: tauri::State<'_, State>) -> Result<serde_json::Value, String> {
let status = state.0.update_check().map_err(|e| e.to_string())?;
serde_json::to_value(status).map_err(|e| e.to_string())
}
#[tauri::command]
fn update_apply(state: tauri::State<'_, State>) -> Result<String, String> {
state.0.update_apply().map_err(|e| e.to_string())
}
/// `true` quando o processo já tem privilégio suficiente pra tudo que o
/// Sabiá faz (modo monitor, captura crua). No Windows o manifesto já
/// exige administrador antes mesmo do app abrir, então aqui é sempre `true`.
#[tauri::command]
fn is_elevated() -> bool {
#[cfg(unix)]
{
unsafe { libc::geteuid() == 0 }
}
#[cfg(windows)]
{
true
}
}
/// Reabre o app com privilégios de administrador (Linux/macOS). O app novo
/// encerra este quando abre; se a autorização falhar ou for cancelada, a
/// janela continua aqui e a interface mostra o motivo.
#[tauri::command]
fn relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> {
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
do_relaunch_elevated(app)
}
#[cfg(windows)]
{
let _ = app;
Err("o Sabiá já roda como administrador no Windows".into())
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn do_relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> {
use tauri::Emitter;
let exe = elevated_exe()?;
let log = std::env::temp_dir().join(format!("sabia-elevate-{}.log", std::process::id()));
let _ = std::fs::write(&log, b"");
let out = std::fs::OpenOptions::new()
.append(true)
.open(&log)
.map_err(|e| e.to_string())?;
let err = out.try_clone().map_err(|e| e.to_string())?;
let pid = std::process::id().to_string();
#[cfg(target_os = "linux")]
let mut cmd = {
let mut c = std::process::Command::new("pkexec");
c.arg("/usr/bin/env");
for k in [
"DISPLAY",
"WAYLAND_DISPLAY",
"XAUTHORITY",
"XDG_RUNTIME_DIR",
"DBUS_SESSION_BUS_ADDRESS",
"GDK_BACKEND",
] {
if let Ok(v) = std::env::var(k) {
c.arg(format!("{k}={v}"));
}
}
c.arg(format!("SABIA_REPLACE_PID={pid}"));
c.arg(&exe);
c
};
#[cfg(target_os = "macos")]
let mut cmd = {
let shell = format!(
"SABIA_REPLACE_PID={} '{}' >/dev/null 2>&1 &",
pid,
exe.display()
);
let script = format!(
"do shell script \"{}\" with administrator privileges",
shell.replace('\\', "\\\\").replace('"', "\\\"")
);
let mut c = std::process::Command::new("osascript");
c.args(["-e", &script]);
c
};
cmd.stdout(out).stderr(err);
let mut child = cmd
.spawn()
.map_err(|e| format!("não foi possível pedir privilégios: {e}"))?;
std::thread::spawn(move || {
let status = child.wait();
if matches!(&status, Ok(s) if s.success()) {
let _ = std::fs::remove_file(&log);
return;
}
let detail = std::fs::read_to_string(&log).unwrap_or_default();
let msg = match status.ok().and_then(|s| s.code()) {
Some(126) => "Autorização cancelada.".to_string(),
Some(127) => "Não foi possível autenticar como administrador.".to_string(),
_ => format!(
"Não foi possível reiniciar como administrador. {}",
detail.lines().last().unwrap_or("").trim()
),
};
let _ = app.emit("elevate-failed", msg);
let _ = std::fs::remove_file(&log);
});
Ok(())
}
/// Caminho que o root consegue executar. No AppImage, `current_exe` aponta
/// pra dentro de um ponto de montagem FUSE do usuário, inacessível ao root:
/// usa o próprio arquivo `.AppImage` (variável `APPIMAGE`).
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn elevated_exe() -> Result<std::path::PathBuf, String> {
if let Some(p) = std::env::var_os("APPIMAGE")
.map(std::path::PathBuf::from)
.filter(|p| p.is_file())
{
return Ok(p);
}
std::env::current_exe().map_err(|e| e.to_string())
}
fn main() {
let engine = Arc::new(Engine::new());
tauri::Builder::default()
.manage(State(engine))
.setup(|app| {
// Quando abrimos elevados a pedido do app normal, ele fecha o app antigo.
#[cfg(unix)]
if let Some(pid) = std::env::var("SABIA_REPLACE_PID")
.ok()
.and_then(|p| p.parse::<i32>().ok())
{
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(1200));
// SAFETY: só envia SIGTERM ao processo anterior do próprio usuário.
unsafe {
if libc::geteuid() == 0 {
libc::kill(pid, libc::SIGTERM);
}
}
});
}
// Captura crua e modo monitor exigem root: pede elevação assim
// que o app abre, sem esperar o usuário procurar um botão. Se
// a autorização falhar, a janela continua aberta e a interface
// mostra o aviso (com um botão pra tentar de novo).
#[cfg(any(target_os = "linux", target_os = "macos"))]
if unsafe { libc::geteuid() != 0 } && std::env::var("SABIA_REPLACE_PID").is_err() {
let handle = app.handle().clone();
std::thread::spawn(move || {
let _ = do_relaunch_elevated(handle);
});
}
Ok(())
})
.invoke_handler(tauri::generate_handler![
version,
list_ifaces,
set_monitor,
start_scan,
stop_scan,
scan_results,
scan_snapshot,
scan_clients,
handshakes,
export_handshake,
export_all_handshakes,
start_crack,
start_crack_mask,
crack_job,
crack_jobs,
deauth,
deauth_directional,
start_deauth_job,
stop_deauth_job,
deauth_job,
deauth_jobs,
capture_handshake,
report,
report_html,
export_hc22000,
export_all_hc22000,
pmkid_hunt,
pmf_test,
wps_tools,
start_wps,
wps_job,
wps_jobs,
stop_wps,
start_evil_twin,
eviltwin_job,
eviltwin_jobs,
stop_evil_twin,
start_evil_twin_enterprise,
eviltwin_enterprise_job,
eviltwin_enterprise_jobs,
stop_evil_twin_enterprise,
start_scan_opts,
update_check,
update_apply,
is_elevated,
relaunch_elevated,
])
.run(tauri::generate_context!())
.expect("erro ao iniciar o Sabiá");
}