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