wifiscan 0.4.0

Wireless network scanner TUI with monitor mode, handshake capture, deauth, and evil twin
Documentation
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Instant;

use libwifi::{frame::Frame, Addresses};
use pcap::Capture;
use radiotap::Radiotap;

use crate::debug::{dbg_enabled, dbg_log, pcap_dump};
use crate::eapol::{fallback_data_frame_eapol, handle_data_frame};
use crate::oui::lookup_vendor;
use crate::parse::*;
use crate::types::*;

#[allow(clippy::too_many_arguments)]
pub fn process_packet(
    raw: &[u8],
    ap_map: &ApMap,
    handshake_map: &HandshakeMap,
    handshake_count: &HandshakeCount,
    handshake_dir: &Path,
    capture_handshakes: bool,
    probe_map: &ProbeMap,
    channel_stats: &ChannelStats,
    hs_timeout: u64,
) {
    FRAMES_TOTAL.fetch_add(1, Ordering::Relaxed);
    pcap_dump(raw);

    let rt = match Radiotap::from_bytes(raw) {
        Ok(rt) => rt,
        Err(_) => return,
    };

    let signal_dbm = rt.antenna_signal.map(|s| s.value).unwrap_or(-100);
    let noise_dbm = rt.antenna_noise.map(|n| n.value);
    let freq = rt.channel.map(|c| c.freq).unwrap_or(0);
    let rt_channel = if freq > 0 { frequency_to_channel(freq) } else { 0 };

    // Channel utilization tracking
    if rt_channel > 0 {
        let mut cs = channel_stats.lock().unwrap();
        *cs.entry(rt_channel).or_insert(0) += 1;
    }

    let header_len = rt.header.length;
    if header_len >= raw.len() { return; }
    let frame_bytes = &raw[header_len..];

    // Track data frame counts even before libwifi parsing
    if frame_bytes.len() >= 2 {
        let fc0 = frame_bytes[0];
        let frame_type = (fc0 >> 2) & 0x03;
        let subtype = (fc0 >> 4) & 0x0F;
        if frame_type == 2 {
            FRAMES_DATA.fetch_add(1, Ordering::Relaxed);
        }
        if dbg_enabled() && frame_type == 2 {
            let fc1 = frame_bytes[1];
            let protected = (fc1 & 0x40) != 0;
            if !protected {
                dbg_log(&format!("DATA frame: type={} subtype={} protected=false len={} sig={}dBm ch={}",
                    frame_type, subtype, frame_bytes.len(), signal_dbm, rt_channel));
            }
        }
    }

    let frame = match libwifi::parse_frame(frame_bytes) {
        Ok(f) => f,
        Err(_) => {
            if frame_bytes.len() >= 2 {
                let fc0 = frame_bytes[0];
                let frame_type = (fc0 >> 2) & 0x03;
                if frame_type == 2 && capture_handshakes {
                    dbg_log(&format!("libwifi parse failed on data frame, trying fallback. FC: {:02x} {:02x}",
                        frame_bytes[0], frame_bytes[1]));
                    fallback_data_frame_eapol(raw, frame_bytes, ap_map, handshake_map,
                        handshake_count, handshake_dir, hs_timeout);
                }
            }
            return;
        }
    };

    let now = Instant::now();

    match &frame {
        Frame::Beacon(beacon) => {
            let bssid = match beacon.header.bssid() {
                Some(b) => b.to_string(),
                None => return,
            };
            let base_ie_offset = 24 + 12;
            let base_cap_offset = 24 + 10;
            let ht_adj: usize = if frame_bytes.len() >= 2 && has_ht_control(frame_bytes[1]) { 4 } else { 0 };
            let ie_offset = base_ie_offset + ht_adj;

            let ssid = extract_ssid_from_ies(frame_bytes, ie_offset)
                .or_else(|| beacon.station_info.ssid.clone())
                .unwrap_or_else(|| "<hidden>".to_string());
            let channel = extract_channel_from_ies(frame_bytes, ie_offset).unwrap_or(rt_channel);
            let ie_info = parse_ies_full(frame_bytes, base_cap_offset);

            let mut map = ap_map.lock().unwrap();
            let mac = MacAddr::from_str_hex(&bssid).unwrap_or(MacAddr::ZERO);
            let ap = map.entry(bssid.clone()).or_insert_with(|| {
                let mut a = AccessPoint::new(mac);
                a.vendor = lookup_vendor(&bssid);
                a
            });
            ap.signal_dbm = signal_dbm;
            ap.noise_dbm = noise_dbm;
            ap.last_seen = now;
            ap.beacon_count += 1;
            ap.push_signal(signal_dbm);

            if !ssid.is_empty() && ssid != "<hidden>" {
                ap.essid = ssid;
            }
            if channel > 0 { ap.channel = channel; }
            ap.frequency_mhz = freq;
            ap.encryption = ie_info.encryption;
            ap.wifi_generation = ie_info.wifi_generation;
            ap.channel_width = ie_info.channel_width;
            ap.bss_color = ie_info.bss_color;
            drop(map);

            // Attach beacon to any in-progress handshakes for this BSSID
            if capture_handshakes {
                let mut hs_map = handshake_map.lock().unwrap();
                for (key, hs) in hs_map.iter_mut() {
                    if key.bssid == bssid && hs.beacon_raw.is_none() {
                        hs.beacon_raw = Some(raw.to_vec());
                    }
                }
            }
        }

        Frame::ProbeResponse(probe) => {
            let bssid = match probe.header.bssid() {
                Some(b) => b.to_string(),
                None => return,
            };
            let base_ie_offset = 24 + 12;
            let base_cap_offset = 24 + 10;
            let ht_adj: usize = if frame_bytes.len() >= 2 && has_ht_control(frame_bytes[1]) { 4 } else { 0 };
            let ie_offset = base_ie_offset + ht_adj;

            let ssid = extract_ssid_from_ies(frame_bytes, ie_offset)
                .or_else(|| probe.station_info.ssid.clone())
                .unwrap_or_else(|| "<hidden>".to_string());
            let channel = extract_channel_from_ies(frame_bytes, ie_offset).unwrap_or(rt_channel);
            let ie_info = parse_ies_full(frame_bytes, base_cap_offset);

            let mut map = ap_map.lock().unwrap();
            let mac = MacAddr::from_str_hex(&bssid).unwrap_or(MacAddr::ZERO);
            let ap = map.entry(bssid.clone()).or_insert_with(|| {
                let mut a = AccessPoint::new(mac);
                a.vendor = lookup_vendor(&bssid);
                a
            });
            ap.signal_dbm = signal_dbm;
            ap.noise_dbm = noise_dbm;
            ap.last_seen = now;
            if !ssid.is_empty() && ssid != "<hidden>" { ap.essid = ssid; }
            if channel > 0 { ap.channel = channel; }
            ap.frequency_mhz = freq;
            ap.encryption = ie_info.encryption;
            ap.wifi_generation = ie_info.wifi_generation;
            ap.channel_width = ie_info.channel_width;
            ap.bss_color = ie_info.bss_color;
        }

        Frame::ProbeRequest(probe_req) => {
            FRAMES_PROBE_REQ.fetch_add(1, Ordering::Relaxed);
            // Track client probe requests
            if let Some(src) = probe_req.header.src() {
                let client_mac = src.to_string();
                if client_mac != "ff:ff:ff:ff:ff:ff" {
                    let ssid = probe_req.station_info.ssid.clone()
                        .or_else(|| extract_ssid_from_ies(frame_bytes, 24))
                        .unwrap_or_default();
                    if !ssid.is_empty() && ssid != "<hidden>" {
                        let mut pm = probe_map.lock().unwrap();
                        pm.entry(client_mac.clone())
                            .or_default()
                            .insert(ssid.clone());
                    }
                    // Also update any AP that has this client with probe info
                    let mut map = ap_map.lock().unwrap();
                    for ap in map.values_mut() {
                        if let Some(ci) = ap.clients.get_mut(&client_mac) {
                            let ssid = probe_req.station_info.ssid.clone().unwrap_or_default();
                            if !ssid.is_empty() && ssid != "<hidden>"
                                && !ci.probed_ssids.contains(&ssid)
                            {
                                ci.probed_ssids.push(ssid);
                            }
                        }
                    }
                }
            }
        }

        Frame::Data(data) => {
            if let Some(bssid_addr) = data.bssid() {
                let bssid = bssid_addr.to_string();
                let src = data.src().map(|a| a.to_string());
                let dst = data.dest().to_string();
                handle_data_frame(
                    raw, frame_bytes, &bssid, src, dst, now, signal_dbm,
                    ap_map, handshake_map, handshake_count, handshake_dir,
                    capture_handshakes, hs_timeout,
                );
            }
        }

        Frame::QosData(data) => {
            if let Some(bssid_addr) = data.bssid() {
                let bssid = bssid_addr.to_string();
                let src = data.src().map(|a| a.to_string());
                let dst = data.dest().to_string();
                handle_data_frame(
                    raw, frame_bytes, &bssid, src, dst, now, signal_dbm,
                    ap_map, handshake_map, handshake_count, handshake_dir,
                    capture_handshakes, hs_timeout,
                );
            }
        }

        _ => {
            // Fallback for data frame subtypes libwifi doesn't parse
            if capture_handshakes && frame_bytes.len() >= 26 {
                let fc0 = frame_bytes[0];
                if (fc0 >> 2) & 0x03 == 2 {
                    fallback_data_frame_eapol(
                        raw, frame_bytes, ap_map, handshake_map,
                        handshake_count, handshake_dir, hs_timeout,
                    );
                }
            }
        }
    }
}

// ── Capture Thread ──────────────────────────────────────────────────────────

#[allow(clippy::too_many_arguments)]
pub fn start_capture(
    iface: &str,
    ap_map: ApMap,
    handshake_map: HandshakeMap,
    handshake_count: HandshakeCount,
    handshake_dir: PathBuf,
    capture_handshakes: bool,
    probe_map: ProbeMap,
    channel_stats: ChannelStats,
    shutdown: Arc<AtomicBool>,
    hs_timeout: u64,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut cap = Capture::from_device(iface)?
        .promisc(true)
        .snaplen(65535)
        .timeout(100)
        .open()?;

    std::thread::spawn(move || {
        let mut consecutive_errors: u32 = 0;
        while !shutdown.load(Ordering::Relaxed) {
            match cap.next_packet() {
                Ok(packet) => {
                    consecutive_errors = 0;
                    process_packet(
                        packet.data,
                        &ap_map,
                        &handshake_map,
                        &handshake_count,
                        &handshake_dir,
                        capture_handshakes,
                        &probe_map,
                        &channel_stats,
                        hs_timeout,
                    );
                }
                Err(pcap::Error::TimeoutExpired) => continue,
                Err(e) => {
                    consecutive_errors += 1;
                    if consecutive_errors <= 3 {
                        dbg_log(&format!("Capture error: {} (consecutive: {})", e, consecutive_errors));
                    }
                    if consecutive_errors > 100 {
                        dbg_log("Too many consecutive capture errors, stopping");
                        break;
                    }
                    continue;
                }
            }
        }
    });
    Ok(())
}