Implement WiFi promiscuous mode scanner module

This commit is contained in:
C F committed 2026-04-24 19:09:46 -07:00
1 parent 1c9bf0d113
commit 10e2f921ed
2 files changed
+314

No files matched your search

+251
View File
@@ -0,0 +1,251 @@
#include "wifi_scanner.h"
#include <nvs_flash.h>
#include <esp_netif.h>
#include <esp_event.h>
#include <esp_wifi.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <string.h>
#include <Arduino.h> // 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<const wifi_promiscuous_pkt_t*>(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<uint16_t>(frame[0]) | (static_cast<uint16_t>(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<uint8_t>(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<int8_t>(rx.rssi), chan, auth);
}
}
// ---- Enqueue raw packet for Core 1 → PCAP writer ----
WifiPacket wpkt;
wpkt.timestamp_ms = static_cast<uint32_t>(millis());
wpkt.channel = static_cast<uint8_t>(rx.channel);
wpkt.rssi = static_cast<int8_t>(rx.rssi);
wpkt.len = static_cast<uint16_t>(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;
}
}
+63
View File
@@ -0,0 +1,63 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <esp_wifi_types.h>
#include <stdint.h>
// 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);