From 10e2f921eddd7e70e2098cdbc17db1c22ad61309 Mon Sep 17 00:00:00 2001 From: C F Date: Fri, 24 Apr 2026 19:08:46 -0700 Subject: [PATCH] Implement WiFi promiscuous mode scanner module --- src/wifi_scanner.cpp | 251 +++++++++++++++++++++++++++++++++++++++++++ src/wifi_scanner.h | 63 +++++++++++ 2 files changed, 314 insertions(+) create mode 100644 src/wifi_scanner.cpp create mode 100644 src/wifi_scanner.h diff --git a/src/wifi_scanner.cpp b/src/wifi_scanner.cpp new file mode 100644 index 0000000..33e45ab --- /dev/null +++ b/src/wifi_scanner.cpp @@ -0,0 +1,251 @@ +#include "wifi_scanner.h" + +#include +#include +#include +#include +#include +#include +#include +#include // millis() + +// ------------------------------------------------------- +// Configuration +// ------------------------------------------------------- +#define QUEUE_DEPTH 32 +#define CHANNEL_DWELL_MS 200 // ms to dwell on each channel before hopping + +// ------------------------------------------------------- +// Module-private state +// ------------------------------------------------------- +QueueHandle_t wifiPacketQueue = nullptr; + +static WifiAP s_apList[WIFI_MAX_APS]; +static int s_apCount = 0; +static SemaphoreHandle_t s_apMutex = nullptr; + +static TaskHandle_t s_taskHandle = nullptr; +static volatile bool s_running = false; +static bool s_initialized = false; + +// ------------------------------------------------------- +// Beacon parsing helpers +// ------------------------------------------------------- + +// Walk an IE block to determine the strongest security mode present. +static wifi_auth_mode_t parseAuthMode(const uint8_t* ies, int ies_len) { + bool has_rsn = false; + bool has_wpa = false; + int pos = 0; + + while (pos + 2 <= ies_len) { + uint8_t tag = ies[pos]; + uint8_t len = ies[pos + 1]; + if (pos + 2 + len > ies_len) break; + const uint8_t* data = ies + pos + 2; + + if (tag == 48) { // RSN → WPA2/WPA3 + has_rsn = true; + } else if (tag == 221 && len >= 4) { // Vendor-specific + // WPA1: OUI 00:50:F2, type 0x01 + if (data[0] == 0x00 && data[1] == 0x50 && + data[2] == 0xF2 && data[3] == 0x01) { + has_wpa = true; + } + } + pos += 2 + len; + } + + if (has_rsn) return WIFI_AUTH_WPA2_PSK; + if (has_wpa) return WIFI_AUTH_WPA_PSK; + return WIFI_AUTH_OPEN; +} + +// Insert or update an entry in the AP list. Non-blocking: drops the update +// if the mutex is held by the reader to avoid stalling the sniffer. +static void updateAPList(const uint8_t* bssid, const char* ssid, + int8_t rssi, uint8_t channel, wifi_auth_mode_t auth) { + if (xSemaphoreTake(s_apMutex, 0) != pdTRUE) return; + + for (int i = 0; i < s_apCount; i++) { + if (memcmp(s_apList[i].bssid, bssid, 6) == 0) { + s_apList[i].rssi = rssi; + s_apList[i].channel = channel; + xSemaphoreGive(s_apMutex); + return; + } + } + + if (s_apCount < WIFI_MAX_APS) { + WifiAP& ap = s_apList[s_apCount++]; + strncpy(ap.ssid, ssid, 32); + ap.ssid[32] = '\0'; + memcpy(ap.bssid, bssid, 6); + ap.rssi = rssi; + ap.channel = channel; + ap.auth_mode = auth; + } + + xSemaphoreGive(s_apMutex); +} + +// ------------------------------------------------------- +// Promiscuous receive callback — runs in WiFi task (Core 0) +// ------------------------------------------------------- +static void promiscuousCallback(void* buf, wifi_promiscuous_pkt_type_t type) { + if (!s_running) return; + if (type != WIFI_PKT_MGMT && type != WIFI_PKT_DATA) return; + + const wifi_promiscuous_pkt_t* pkt = static_cast(buf); + const wifi_pkt_rx_ctrl_t& rx = pkt->rx_ctrl; + const uint8_t* frame = pkt->payload; + const int flen = rx.sig_len; + + // ---- Parse beacon and probe-response frames for AP discovery ---- + if (type == WIFI_PKT_MGMT && flen >= 36) { + // 802.11 frame control: little-endian 16-bit field at offset 0 + const uint16_t fc = static_cast(frame[0]) | (static_cast(frame[1]) << 8); + const uint8_t ftype = (fc >> 2) & 0x03; // 0 = Management + const uint8_t fsubtype = (fc >> 4) & 0x0F; // 8 = Beacon, 5 = Probe Response + + if (ftype == 0 && (fsubtype == 8 || fsubtype == 5)) { + // BSSID field is at offset 16 in the 802.11 management frame header + const uint8_t* bssid = frame + 16; + + // Fixed-length fields: 24-byte MAC header + 8 ts + 2 interval + 2 cap = 36 + // Information Elements start at offset 36 + const uint8_t* ies = frame + 36; + const int ies_len = flen - 36; + + char ssid[33] = {0}; + uint8_t chan = static_cast(rx.channel); + int pos = 0; + + while (pos + 2 <= ies_len) { + const uint8_t tag = ies[pos]; + const uint8_t len = ies[pos + 1]; + if (pos + 2 + len > ies_len) break; + + if (tag == 0) { // SSID IE + const int copy = len < 32 ? len : 32; + memcpy(ssid, ies + pos + 2, copy); + ssid[copy] = '\0'; + } else if (tag == 3 && len == 1) { // DS Parameter Set (channel) + chan = ies[pos + 2]; + } + pos += 2 + len; + } + + const wifi_auth_mode_t auth = parseAuthMode(ies, ies_len); + updateAPList(bssid, ssid, static_cast(rx.rssi), chan, auth); + } + } + + // ---- Enqueue raw packet for Core 1 → PCAP writer ---- + WifiPacket wpkt; + wpkt.timestamp_ms = static_cast(millis()); + wpkt.channel = static_cast(rx.channel); + wpkt.rssi = static_cast(rx.rssi); + wpkt.len = static_cast(flen < WIFI_PKT_BUF_SIZE ? flen : WIFI_PKT_BUF_SIZE); + memcpy(wpkt.buf, frame, wpkt.len); + + // Non-blocking send: drop the packet if the queue is full rather than + // stall the WiFi receive task and risk missing subsequent frames. + xQueueSend(wifiPacketQueue, &wpkt, 0); +} + +// ------------------------------------------------------- +// Channel-hop task — pinned to Core 0 +// ------------------------------------------------------- +static void wifiScannerTask(void*) { + for (;;) { + if (!s_running) { + // Block until wifiScanner_start() sends a task notification. + // Task notifications are "sticky": if the notify arrives before + // we block, ulTaskNotifyTake() returns immediately — no race. + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + continue; + } + + for (uint8_t ch = 1; ch <= 13 && s_running; ch++) { + esp_wifi_set_channel(ch, WIFI_SECOND_CHAN_NONE); + vTaskDelay(pdMS_TO_TICKS(CHANNEL_DWELL_MS)); + } + } +} + +// ------------------------------------------------------- +// Public API +// ------------------------------------------------------- + +void wifiScanner_init() { + if (s_initialized) return; + s_initialized = true; + + // NVS may need erasing after a failed write or version mismatch. + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + nvs_flash_erase(); + nvs_flash_init(); + } + + // The Arduino ESP32 core initialises netif and the event loop in + // initArduino(). Calling them again returns ESP_ERR_INVALID_STATE, + // which is non-fatal — we just continue. + esp_netif_init(); + esp_event_loop_create_default(); + esp_netif_create_default_wifi_sta(); + + wifi_init_config_t wificfg = WIFI_INIT_CONFIG_DEFAULT(); + esp_wifi_init(&wificfg); + esp_wifi_set_mode(WIFI_MODE_STA); + esp_wifi_start(); + + wifiPacketQueue = xQueueCreate(QUEUE_DEPTH, sizeof(WifiPacket)); + s_apMutex = xSemaphoreCreateMutex(); +} + +void wifiScanner_startTask() { + xTaskCreatePinnedToCore( + wifiScannerTask, // task function + "wifi_scan", // name + 4096, // stack words + nullptr, // parameter + 5, // priority + &s_taskHandle, // handle out + 0 // Core 0 + ); +} + +void wifiScanner_start() { + if (s_running) return; + s_running = true; + esp_wifi_set_promiscuous_rx_cb(promiscuousCallback); + esp_wifi_set_promiscuous(true); + if (s_taskHandle) { + xTaskNotifyGive(s_taskHandle); + } +} + +void wifiScanner_stop() { + if (!s_running) return; + s_running = false; + esp_wifi_set_promiscuous(false); + // Channel-hop task checks s_running at each iteration and self-suspends + // via ulTaskNotifyTake() on the next loop pass. +} + +bool wifiScanner_isRunning() { + return s_running; +} + +void wifiScanner_getAPList(const WifiAP** list, int* count) { + if (xSemaphoreTake(s_apMutex, pdMS_TO_TICKS(10)) == pdTRUE) { + *list = s_apList; + *count = s_apCount; + xSemaphoreGive(s_apMutex); + } else { + *list = nullptr; + *count = 0; + } +} diff --git a/src/wifi_scanner.h b/src/wifi_scanner.h new file mode 100644 index 0000000..5ada63c --- /dev/null +++ b/src/wifi_scanner.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include +#include + +// Maximum raw 802.11 frame payload buffered per packet +#define WIFI_PKT_BUF_SIZE 512 + +// Maximum tracked access points +#define WIFI_MAX_APS 64 + +// ------------------------------------------------------- +// Packet pushed onto wifiPacketQueue (Core 0 → Core 1) +// Core 1 drains this queue and hands off to PCAP writer. +// ------------------------------------------------------- +struct WifiPacket { + uint32_t timestamp_ms; + uint8_t channel; + int8_t rssi; + uint16_t len; // actual bytes copied (≤ WIFI_PKT_BUF_SIZE) + uint8_t buf[WIFI_PKT_BUF_SIZE]; +}; + +// ------------------------------------------------------- +// Access point discovered via beacon / probe-response +// ------------------------------------------------------- +struct WifiAP { + char ssid[33]; // null-terminated, empty string for hidden APs + uint8_t bssid[6]; + int8_t rssi; + uint8_t channel; + wifi_auth_mode_t auth_mode; +}; + +// ------------------------------------------------------- +// Shared packet queue — extern so Core 1 can drain it +// ------------------------------------------------------- +extern QueueHandle_t wifiPacketQueue; + +// ------------------------------------------------------- +// Public API +// ------------------------------------------------------- + +// Call once from setup() — initialises WiFi stack, creates queue and AP mutex. +void wifiScanner_init(); + +// Spawn the channel-hop FreeRTOS task pinned to Core 0. +// Call after wifiScanner_init(), before the first button press. +void wifiScanner_startTask(); + +// Enable promiscuous mode and resume the channel-hop task. +void wifiScanner_start(); + +// Disable promiscuous mode; channel-hop task suspends itself. +void wifiScanner_stop(); + +bool wifiScanner_isRunning(); + +// Obtain a read-only snapshot of the current AP list. +// Returns nullptr/0 if the mutex is unavailable. +void wifiScanner_getAPList(const WifiAP** list, int* count);