Implement WiFi promiscuous mode scanner module
This commit is contained in:
1 parent
1c9bf0d113
commit
10e2f921ed
2 files changed
+314
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
Reference in new issue
Block a user