use std::collections::HashMap;
use std::io::stdout;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use clap::Parser;
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use pcap::Device;
use ratatui::prelude::*;
use wifiscan::beacon::{spawn_beacon_flood, BeaconFloodProgress};
use wifiscan::deauth::{spawn_deauth, DeauthProgress, DEAUTH_REASONS};
use wifiscan::debug::{dbg_log, init_debug, init_pcap_dump};
use wifiscan::export::{
export_aps_csv, export_aps_json, export_pmkid_hashcat, export_probes_csv, export_wigle_csv,
};
use wifiscan::monitor::*;
use wifiscan::oui::init_oui;
use wifiscan::capture::start_capture;
use wifiscan::channel::start_channel_hopper;
use wifiscan::types::*;
use wifiscan::ui::{ui, App};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
if !is_root() {
eprintln!("Error: wifiscan requires root privileges.");
eprintln!(" sudo ./target/release/wifiscan -i {}", args.interface);
std::process::exit(1);
}
init_debug(args.debug, "./wifiscan_debug.log");
init_pcap_dump(args.dump_pcap.as_ref());
init_oui();
if args.debug {
dbg_log("=== wifiscan debug session started ===");
dbg_log(&format!(
"Interface: {} | Channel: {} | HS timeout: {}s | Band: {} | Auto-expire: {}s",
args.interface, args.channel, args.hs_timeout, args.band, args.auto_expire
));
}
let devices = Device::list()?;
if !devices.iter().any(|d| d.name == args.interface) {
eprintln!("Interface '{}' not found. Available:", args.interface);
for d in &devices {
eprintln!(
" {}{}",
d.name,
d.desc.as_ref().map(|d| format!(" ({})", d)).unwrap_or_default()
);
}
std::process::exit(1);
}
let (active_iface, in_monitor, we_enabled_monitor) = if args.skip_monitor {
let mon = is_monitor_mode(&args.interface);
if !mon {
let itype = get_iface_type(&args.interface);
eprintln!(
"Warning: {} is in '{}' mode, not monitor. Deauth disabled.",
args.interface, itype
);
}
(args.interface.clone(), mon, false)
} else if is_monitor_mode(&args.interface) {
eprintln!("[+] {} is already in monitor mode.", args.interface);
(args.interface.clone(), true, false)
} else {
eprintln!(
"[*] {} is in '{}' mode. Enabling monitor mode...",
args.interface,
get_iface_type(&args.interface)
);
match enable_monitor_mode(&args.interface) {
Ok(mon_iface) => {
eprintln!("[+] Monitor mode enabled on '{}'.", mon_iface);
(mon_iface, true, true)
}
Err(e) => {
eprintln!("[-] Failed to enable monitor mode: {}", e);
eprintln!("[-] Continuing in managed mode.");
(args.interface.clone(), false, false)
}
}
};
if !args.no_handshakes && in_monitor {
let _ = std::fs::create_dir_all(&args.handshake_dir);
}
let ap_map: ApMap = Arc::new(Mutex::new(HashMap::new()));
let handshake_map: HandshakeMap = Arc::new(Mutex::new(HashMap::new()));
let handshake_count: HandshakeCount = Arc::new(Mutex::new(0));
let current_channel: CurrentChannel = Arc::new(Mutex::new(0));
let channel_stats: ChannelStats = Arc::new(Mutex::new(HashMap::new()));
let probe_map: ProbeMap = Arc::new(Mutex::new(HashMap::new()));
let shutdown = Arc::new(AtomicBool::new(false));
let hop_pause = Arc::new(AtomicBool::new(false));
let shutdown_sig = shutdown.clone();
let active_iface_sig = active_iface.clone();
let we_enabled_sig = we_enabled_monitor;
ctrlc::set_handler(move || {
shutdown_sig.store(true, Ordering::SeqCst);
let _ = disable_raw_mode();
let _ = stdout().execute(LeaveAlternateScreen);
if we_enabled_sig {
eprintln!("\n[*] Restoring managed mode...");
disable_monitor_mode(&active_iface_sig);
}
std::process::exit(0);
})
.expect("Failed to set CTRL+C handler");
start_channel_hopper(
&active_iface,
args.channel,
current_channel.clone(),
args.dwell_2g,
args.dwell_5g,
args.dwell_6g,
shutdown.clone(),
hop_pause.clone(),
);
let capture_hs = !args.no_handshakes && in_monitor;
if let Err(e) = start_capture(
&active_iface,
ap_map.clone(),
handshake_map.clone(),
handshake_count.clone(),
args.handshake_dir.clone(),
capture_hs,
probe_map.clone(),
channel_stats.clone(),
shutdown.clone(),
args.hs_timeout,
) {
eprintln!("[-] Failed to start capture on '{}': {}", active_iface, e);
if we_enabled_monitor {
disable_monitor_mode(&active_iface);
}
std::process::exit(1);
}
eprintln!("[+] Capture started on '{}'. Launching TUI...", active_iface);
std::thread::sleep(Duration::from_millis(200));
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
let mut terminal = Terminal::new(backend)?;
while event::poll(Duration::from_millis(100)).unwrap_or(false) {
let _ = event::read();
}
let band_filter = BandFilter::parse_band(&args.band);
let mut app = App::new(
ap_map.clone(),
args.sort,
active_iface.clone(),
args.interface.clone(),
in_monitor,
handshake_count.clone(),
current_channel.clone(),
args.deauth_burst,
hop_pause.clone(),
channel_stats.clone(),
probe_map.clone(),
args.deauth_dwell,
args.hs_timeout,
band_filter,
args.auto_expire,
args.min_signal,
handshake_map.clone(),
);
let refresh_dur = Duration::from_millis(args.refresh);
let mut filtering = false;
loop {
if shutdown.load(Ordering::Relaxed) {
break;
}
if !app.paused {
app.refresh_cache();
}
let deauth_finished = if let DeauthState::Running { ref progress, .. } = app.deauth_state {
if progress.done.load(Ordering::Relaxed) {
let sent = progress.sent.load(Ordering::Relaxed);
let failed = progress.failed.load(Ordering::Relaxed);
let captured = progress.hs_captured.load(Ordering::Relaxed);
Some((sent, failed, captured))
} else {
None
}
} else {
None
};
if let Some((sent, failed, captured)) = deauth_finished {
app.deauth_state = DeauthState::Idle;
if failed > 0 && sent == 0 {
app.set_status(format!("Deauth FAILED: injection not working ({} errors). Check adapter supports injection.", failed));
} else if captured {
app.set_status(format!("Deauth done: {} frames sent, handshake captured!", sent));
} else if failed > 0 {
app.set_status(format!("Deauth done: {} sent, {} failed", sent, failed));
} else {
app.set_status(format!("Deauth done: {} frames sent", sent));
}
}
terminal.draw(|f| ui(f, &mut app))?;
if event::poll(refresh_dur)? {
if let Event::Key(key) = event::read()? {
if key.kind != KeyEventKind::Press {
continue;
}
match &app.deauth_state {
DeauthState::Confirm { ap, target } => match key.code {
KeyCode::Char('y') | KeyCode::Char('Y') => {
let ap_clone = ap.clone();
let target_clone = target.clone();
let target_count = match &target_clone {
DeauthTarget::All => 1 + ap_clone.clients.len() as u32,
DeauthTarget::SingleClient(_) => 1,
};
let burst = app.deauth_burst;
let total = DEAUTH_REASONS.len() as u32 * 2 * target_count * 2 * burst;
let dwell = app.deauth_dwell;
let progress = Arc::new(DeauthProgress::new(total, dwell as u32));
spawn_deauth(
app.interface.clone(),
ap_clone.clone(),
target_clone,
burst,
dwell,
progress.clone(),
app.hop_pause.clone(),
app.current_channel.clone(),
app.handshake_count.clone(),
app.ap_map.clone(),
);
app.deauth_state = DeauthState::Running {
ap: ap_clone,
progress,
};
}
_ => {
app.deauth_state = DeauthState::Idle;
}
},
DeauthState::Running { ref progress, .. } => {
if key.code == KeyCode::Esc {
progress.stop.store(true, Ordering::Relaxed);
}
continue;
}
DeauthState::Idle => {}
}
match &app.beacon_flood_state {
BeaconFloodState::Confirm { ap } => match key.code {
KeyCode::Char('y') | KeyCode::Char('Y') => {
let ap_clone = ap.clone();
let progress = Arc::new(BeaconFloodProgress::new());
let rsn_ie = if ap_clone.encryption.has_rsn {
None
} else {
None
};
let rogue_bssid = None; spawn_beacon_flood(
app.interface.clone(),
ap_clone.essid.clone(),
ap_clone.channel,
rogue_bssid,
rsn_ie,
progress.clone(),
);
app.beacon_flood_state = BeaconFloodState::Running {
ssid: ap_clone.essid.clone(),
bssid: "random".to_string(),
channel: ap_clone.channel,
progress,
};
}
_ => {
app.beacon_flood_state = BeaconFloodState::Idle;
}
},
BeaconFloodState::Running { ref progress, .. } => {
match key.code {
KeyCode::Esc | KeyCode::Char('t') => {
progress.stop.store(true, Ordering::Relaxed);
let sent = progress.beacons_sent.load(Ordering::Relaxed);
app.beacon_flood_state = BeaconFloodState::Idle;
app.set_status(format!("Beacon flood stopped: {} beacons sent", sent));
}
_ => {}
}
continue;
}
BeaconFloodState::Idle => {}
}
if filtering {
match key.code {
KeyCode::Enter => filtering = false,
KeyCode::Esc => {
app.filter.clear();
filtering = false;
}
KeyCode::Backspace => {
app.filter.pop();
}
KeyCode::Char(c) => app.filter.push(c),
_ => {}
}
continue;
}
if app.detail_view.is_some() {
match key.code {
KeyCode::Esc | KeyCode::Backspace => {
app.detail_view = None;
app.detail_client_scroll = 0;
app.detail_client_selected = None;
}
KeyCode::Char('q') => break,
KeyCode::Down | KeyCode::Char('j') => {
app.detail_client_scroll =
app.detail_client_scroll.saturating_add(1);
}
KeyCode::Up | KeyCode::Char('k') => {
app.detail_client_scroll =
app.detail_client_scroll.saturating_sub(1);
}
KeyCode::Tab => {
if let Some(ap) = app.get_detail_ap() {
let count = ap.clients.len();
if count > 0 {
app.detail_client_selected = Some(
app.detail_client_selected
.map(|i| (i + 1) % count)
.unwrap_or(0),
);
}
}
}
KeyCode::Char('d') => {
if app.monitor_mode {
if let Some(ap) = app.get_detail_ap() {
app.detail_view = None;
app.detail_client_scroll = 0;
app.detail_client_selected = None;
app.deauth_state = DeauthState::Confirm {
ap,
target: DeauthTarget::All,
};
}
}
}
KeyCode::Char('D') => {
if app.monitor_mode {
if let Some(ap) = app.get_detail_ap() {
if let Some(idx) = app.detail_client_selected {
let mut clients: Vec<_> =
ap.clients.values().cloned().collect();
clients
.sort_by(|a, b| b.data_count.cmp(&a.data_count));
if let Some(client) = clients.get(idx) {
let mac = client.mac.clone();
app.detail_view = None;
app.detail_client_scroll = 0;
app.detail_client_selected = None;
app.deauth_state = DeauthState::Confirm {
ap,
target: DeauthTarget::SingleClient(mac),
};
}
} else {
app.set_status(
"Select a client with Tab first".to_string(),
);
}
}
}
}
_ => {}
}
continue;
}
match key.code {
KeyCode::Char('q') => break,
KeyCode::Enter => {
let idx = app.table_state.selected().unwrap_or(0);
if let Some(sel) = app.cached_aps.get(idx) {
app.detail_view = Some(sel.bssid.clone());
app.detail_client_scroll = 0;
app.detail_client_selected = None;
}
}
KeyCode::Esc => {
if app.show_help {
app.show_help = false;
} else if app.show_alerts {
app.show_alerts = false;
} else if !app.filter.is_empty() {
app.filter.clear();
}
}
KeyCode::Down | KeyCode::Char('j') => {
let i = app.table_state.selected().unwrap_or(0);
let count = app.cached_aps.len();
if count > 0 {
app.table_state.select(Some((i + 1).min(count - 1)));
app.scroll_state = app.scroll_state.position(i + 1);
}
}
KeyCode::Up | KeyCode::Char('k') => {
let i = app.table_state.selected().unwrap_or(0);
app.table_state.select(Some(i.saturating_sub(1)));
app.scroll_state = app.scroll_state.position(i.saturating_sub(1));
}
KeyCode::Char('s') => app.next_sort(),
KeyCode::Char('r') => app.sort_ascending = !app.sort_ascending,
KeyCode::Char('/') => filtering = true,
KeyCode::Char('p') => app.paused = !app.paused,
KeyCode::Char('b') => {
app.band_filter = app.band_filter.next();
app.set_status(format!("Band: {}", app.band_filter.label()));
}
KeyCode::Tab => {
app.view_mode = app.view_mode.next();
app.set_status(format!("View: {}", app.view_mode.label()));
}
KeyCode::Char('c') => {
let mut map = app.ap_map.lock().unwrap();
let before = map.len();
map.retain(|_, ap| ap.age_secs() < 60);
let removed = before - map.len();
drop(map);
app.set_status(format!("Cleared {} stale APs", removed));
}
KeyCode::Char('e') => {
let path = "./wifiscan_export.csv";
match export_aps_csv(&app.ap_map, path) {
Ok(n) => app.set_status(format!("Exported {} APs to {}", n, path)),
Err(e) => app.set_status(format!("Export failed: {}", e)),
}
}
KeyCode::Char('E') => {
let path = "./wifiscan_export.json";
match export_aps_json(&app.ap_map, path) {
Ok(n) => app.set_status(format!("Exported {} APs to {}", n, path)),
Err(e) => app.set_status(format!("Export failed: {}", e)),
}
}
KeyCode::Char('P') => {
let path = "./wifiscan_probes.csv";
match export_probes_csv(&app.probe_map, path) {
Ok(n) => app.set_status(format!("Exported {} clients to {}", n, path)),
Err(e) => app.set_status(format!("Probe export failed: {}", e)),
}
}
KeyCode::Char('H') => {
let path = "./wifiscan_pmkids.22000";
match export_pmkid_hashcat(&handshake_map, path) {
Ok(n) if n > 0 => {
app.set_status(format!("Exported {} PMKIDs to {}", n, path))
}
Ok(_) => app.set_status("No PMKIDs captured yet".to_string()),
Err(e) => app.set_status(format!("PMKID export failed: {}", e)),
}
}
KeyCode::Char('W') => {
let path = "./wifiscan_wigle.csv";
match export_wigle_csv(&app.ap_map, path) {
Ok(n) => {
app.set_status(format!("Exported {} APs WiGLE format", n))
}
Err(e) => app.set_status(format!("WiGLE export failed: {}", e)),
}
}
KeyCode::Char('a') => {
app.show_alerts = !app.show_alerts;
}
KeyCode::Char('d') => {
if app.monitor_mode {
let idx = app.table_state.selected().unwrap_or(0);
if let Some(sel) = app.cached_aps.get(idx) {
app.deauth_state = DeauthState::Confirm {
ap: sel.clone(),
target: DeauthTarget::All,
};
}
}
}
KeyCode::Char('t') => {
if app.monitor_mode {
let idx = app.table_state.selected().unwrap_or(0);
if let Some(sel) = app.cached_aps.get(idx) {
if !sel.essid.is_empty() && sel.essid != "<hidden>" {
app.beacon_flood_state = BeaconFloodState::Confirm {
ap: Box::new(sel.clone()),
};
} else {
app.set_status("Cannot clone hidden SSID".to_string());
}
}
}
}
KeyCode::Char('?') | KeyCode::Char('h') => {
app.show_help = !app.show_help;
}
_ => {}
}
}
}
}
if let BeaconFloodState::Running { ref progress, .. } = app.beacon_flood_state {
progress.stop.store(true, Ordering::Relaxed);
}
shutdown.store(true, Ordering::SeqCst);
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
if we_enabled_monitor {
eprintln!("[*] Restoring {} to managed mode...", app.original_interface);
disable_monitor_mode(&active_iface);
eprintln!("[+] Done. NetworkManager restarted.");
}
let total = FRAMES_TOTAL.load(Ordering::Relaxed);
let data = FRAMES_DATA.load(Ordering::Relaxed);
let prot_skip = FRAMES_PROTECTED_SKIP.load(Ordering::Relaxed);
let null_skip = FRAMES_NULL_SKIP.load(Ordering::Relaxed);
let checked = FRAMES_EAPOL_CHECKED.load(Ordering::Relaxed);
let found = FRAMES_EAPOL_FOUND.load(Ordering::Relaxed);
let m1 = FRAMES_EAPOL_MSG[0].load(Ordering::Relaxed);
let m2 = FRAMES_EAPOL_MSG[1].load(Ordering::Relaxed);
let m3 = FRAMES_EAPOL_MSG[2].load(Ordering::Relaxed);
let m4 = FRAMES_EAPOL_MSG[3].load(Ordering::Relaxed);
let probes = FRAMES_PROBE_REQ.load(Ordering::Relaxed);
eprintln!();
eprintln!("[*] Session diagnostics:");
eprintln!(" Frames total: {}", total);
eprintln!(" Data frames: {}", data);
eprintln!(" Probe requests: {}", probes);
eprintln!(" Protected skip: {} (encrypted)", prot_skip);
eprintln!(" Null Data skip: {} (no payload)", null_skip);
eprintln!(" EAPOL inspected: {}", checked);
eprintln!(" EAPOL found: {}", found);
eprintln!(" M1: {} M2: {} M3: {} M4: {}", m1, m2, m3, m4);
let cs = channel_stats.lock().unwrap();
if !cs.is_empty() {
let mut channels: Vec<_> = cs.iter().collect();
channels.sort_by(|a, b| b.1.cmp(a.1));
let top: Vec<String> = channels
.iter()
.take(10)
.map(|(ch, cnt)| format!("CH{}:{}", ch, cnt))
.collect();
eprintln!(" Top channels: {}", top.join(" "));
}
let pm = probe_map.lock().unwrap();
if !pm.is_empty() {
eprintln!(" Unique probing clients: {}", pm.len());
}
if found == 0 && data > 0 {
eprintln!();
eprintln!(" No EAPOL frames detected. Possible causes:");
eprintln!(" - No clients reconnecting after deauth");
eprintln!(" - 802.11w (PMF) enabled (deauth ignored by clients)");
eprintln!(" - Channel hopping moved off-channel during handshake");
eprintln!(" - Try: --debug --dump-pcap dump.pcap");
eprintln!(" - Try locking channel: -c <channel_number>");
}
Ok(())
}