Skip to content

Instantly share code, notes, and snippets.

@JayGoldberg
Last active October 11, 2025 18:26
Show Gist options
  • Save JayGoldberg/5c68e5044bcda8f5875f3baa69e22f75 to your computer and use it in GitHub Desktop.
Save JayGoldberg/5c68e5044bcda8f5875f3baa69e22f75 to your computer and use it in GitHub Desktop.
different attempts to get Wi-Fi probe request observation working on ESP32

ESP32 probe request sniffer

At first I started with the flock-you project to get a basic ESP32 monitor (promiscuous) mode dump of Wi-Fi SSID (directed) probe requests. But I could not get them this way.

  1. removed the MAC and SSID filtering to show all packets
  2. It seemed to only classify all packets as wild card probes. I saw no directed probes.
  3. I tried to remove logic to find the offset of SSID in the probe requests to see the error and got lost

Tried to use a method that didn't use wifi_ieee80211_mac_hdr_t struct to somehow dump hex or strings of the contents, got confused, made little progress.

Building

mkdir probescan
cd $_
git clone https://gist.github.com/JayGoldberg/5c68e5044bcda8f5875f3baa69e22f75
mkdir src
mv main_rawhex.cpp src # or whatever version you want
# plug in your esp32
pio run -e esp32-s3-devkit --target upload

This will flash the ESP32 with the code, then you can listen on the serial port.

pio device monitor # optional: --port /dev/ttyUSB0 --baud 115200

The environment -e argument to select a board is using values from platformio.ini. Board defns are located at https://docs.platformio.org/en/latest/boards/index.html.

// this appears to be an ESP-IDF implementation not Arduino? Using esp_event vs loop() fn
#include "freertos/FreeRTOS.h"
#include "nvs_flash.h"
#include "lwip/err.h"
#include "esp_wifi.h"
#include "esp_wifi_internal.h"
#include "esp_system.h"
#include "esp_event.h"
#include "esp_event_loop.h"
uint16_t offset = 0;
esp_err_t event_handler(void* ctx, system_event_t* event)
{
return ESP_OK;
}
void promiscuousmode(void* buffer, wifi_promiscuous_pkt_type_t type)
{
wifi_promiscuous_pkt_t* p = (wifi_promiscuous_pkt_t*)buffer;
//remove last 4 bytes of mgmt packets
//if (type == WIFI_PKT_MGMT) (packet->rx_ctrl.sig_len) -= 4;
for (int i = 0; i < p->rx_ctrl.sig_len; i++) {
if (i % 8 == 0) {
printf("%06X ", offset);
offset += 8;
}
printf("%02X ", p->payload[i]);
if (i % 8 == 7) {
printf("\n");
}
else if ((i + 1) == p->rx_ctrl.sig_len) {
printf("\n");
}
}
offset = 0;
}
void app_main(void)
{
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_interface_t wifi_if;
nvs_flash_init();
tcpip_adapter_init();
ESP_ERROR_CHECK(esp_event_loop_init(event_handler, NULL));
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_start());
esp_wifi_set_promiscuous(true);
esp_wifi_set_promiscuous_rx_cb(&promiscuousmode);
}
#include <WiFi.h>
#include <esp_wifi.h>
#include <esp_wifi_types.h>
// A global offset counter for the hex dump, reset inside the handler
uint16_t offset = 0;
static uint8_t current_channel = 1;
static unsigned long last_channel_hop = 0;
// WiFi Promiscuous Mode Configuration
#define MAX_CHANNEL 11
#define CHANNEL_HOP_INTERVAL 500 // milliseconds
void hop_channel() {
unsigned long now = millis();
// Check if the CHANNEL_HOP_INTERVAL (300ms) has passed since the last hop
if (now - last_channel_hop > CHANNEL_HOP_INTERVAL) {
current_channel++;
if (current_channel > MAX_CHANNEL) {
current_channel = 1; // Loop back to channel 1
}
esp_wifi_set_channel(current_channel, WIFI_SECOND_CHAN_NONE);
last_channel_hop = now;
printf("Hopped to channel %d\n", current_channel);
}
}
void sniffer(void *buf, wifi_promiscuous_pkt_type_t type) {
wifi_promiscuous_pkt_t *pkt = (wifi_promiscuous_pkt_t *)buf;
wifi_pkt_rx_ctrl_t ctrl = (wifi_pkt_rx_ctrl_t)pkt->rx_ctrl;
Serial.print("Received packet with RSSI: ");
Serial.println(ctrl.rssi);
//process packet data here
}
void promiscuous_rx_cb(void* buffer, wifi_promiscuous_pkt_type_t type) {
// Cast the raw buffer to the ESP-IDF promiscuous packet structure
wifi_promiscuous_pkt_t* p = (wifi_promiscuous_pkt_t*)buffer;
// Use the signal length from the control structure
uint16_t packet_length = p->rx_ctrl.sig_len;
// Reset offset for a new packet
offset = 0;
// Hex dump logic
for (int i = 0; i < packet_length; i++) {
// Print the offset marker every 8 bytes
if (i % 8 == 0) {
// Use Serial.printf for standard Arduino environment
Serial.printf("%06X ", offset);
offset += 8;
}
// Print the byte in hex
Serial.printf("%02X ", p->payload[i]);
// Print a newline every 8 bytes, or if it's the last byte
if (i % 8 == 7) {
Serial.printf("\n");
} else if ((i + 1) == packet_length) {
Serial.printf("\n");
}
}
}
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_MODE_STA);
esp_wifi_set_channel(current_channel, WIFI_SECOND_CHAN_NONE);
esp_wifi_set_promiscuous(true);
// WIFI_PROMIS_FILTER_* defines what raw packet types to receive,
// while WIFI_PROMIS_CTRL_* defines what control fields within those
// packets to apply a filter based on.
// AP probe requests are included in WIFI_PROMIS_FILTER_MASK_MGMT
wifi_promiscuous_filter_t filter = {
// include the desired main Control Frame types
.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT
// .filter_mask = 0,
// .filter_mask |= WIFI_PROMIS_FILTER_MASK_MGMT,
// set the frame subtype control_mask, not applicable to WIFI_PROMIS_FILTER_MASK_MGMT
// .control_mask = WIFI_PROMIS_CTRL_MASK_ALL,
};
ESP_ERROR_CHECK(esp_wifi_set_promiscuous_rx_cb(&promiscuous_rx_cb)); // callback function to handle promiscuous packet receptions
ESP_ERROR_CHECK(esp_wifi_set_promiscuous_filter(&filter)); // optional, can be used to filter results
//~ ESP_ERROR_CHECK(esp_wifi_set_promiscuous_ctrl_filter(&ctrl_filter)); // default is no
}
void loop() {
hop_channel();
delay(10);
}
#include <Arduino.h>
#include <WiFi.h>
#include <esp_wifi.h>
#include <esp_wifi_types.h>
#include <string.h>
// ============================================================================
// GLOBAL VARIABLES
// ============================================================================
// A global offset counter for the hex dump, reset inside the handler
uint16_t offset = 0;
static uint8_t current_channel = 1;
static unsigned long last_channel_hop = 0;
#define MAX_CHANNEL 11
#define CHANNEL_HOP_INTERVAL 500 // milliseconds
// ============================================================================
// GLOBAL VARIABLES
void hop_channel() {
unsigned long now = millis();
// Check if the CHANNEL_HOP_INTERVAL (300ms) has passed since the last hop
if (now - last_channel_hop > CHANNEL_HOP_INTERVAL) {
current_channel++;
if (current_channel > MAX_CHANNEL) {
current_channel = 1; // Loop back to channel 1
}
esp_wifi_set_channel(current_channel, WIFI_SECOND_CHAN_NONE);
last_channel_hop = now;
printf("Hopped to channel %d\n", current_channel);
}
}
// ============================================================================
// STRUCT DEFINITIONS FOR 802.11 PARSING
// ============================================================================
// Standard 802.11 MAC header (24 bytes)
typedef struct {
uint16_t frame_ctrl;
uint16_t duration_id;
uint8_t addr1[6]; // Destination MAC (DA)
uint8_t addr2[6]; // Source MAC (SA) / Transmitter Address (TA)
uint8_t addr3[6]; // BSS ID (BSSID) / Receiver Address (RA)
uint16_t seq_ctrl;
// Note: addr4 is present in WDS/Mesh frames, but typically not here.
} wifi_ieee80211_mac_hdr_t;
// Full packet structure with the header and payload
typedef struct {
wifi_ieee80211_mac_hdr_t hdr;
uint8_t payload[0]; // Start of the fixed parameters/payload
} wifi_ieee80211_packet_t;
//~ /**
//~ * @brief Cycles through Wi-Fi channels to capture traffic across the 2.4 GHz band.
//~ */
//~ void promiscuous_rx_cb(void* buffer, wifi_promiscuous_pkt_type_t type) {
//~ // Cast the raw buffer to the ESP-IDF promiscuous packet structure
//~ wifi_promiscuous_pkt_t* p = (wifi_promiscuous_pkt_t*)buffer;
//~ // Use the signal length from the control structure
//~ uint16_t packet_length = p->rx_ctrl.sig_len;
//~ // Reset offset for a new packet
//~ offset = 0;
//~ // Hex dump logic
//~ for (int i = 0; i < packet_length; i++) {
//~ // Print the offset marker every 8 bytes
//~ if (i % 8 == 0) {
//~ // Use Serial.printf for standard Arduino environment
//~ Serial.printf("%06X ", offset);
//~ offset += 8;
//~ }
//~ // Print the byte in hex
//~ Serial.printf("%02X ", p->payload[i]);
//~ // Print a newline every 8 bytes, or if it's the last byte
//~ if (i % 8 == 7) {
//~ Serial.printf("\n");
//~ } else if ((i + 1) == packet_length) {
//~ Serial.printf("\n");
//~ }
//~ }
//~ }
// ============================================================================
// WIFI PROMISCUOUS MODE HANDLER
// ============================================================================
/**
* @brief Formats a MAC address array into a string.
*/
String mac_to_string(const uint8_t* mac) {
char buf[18];
sprintf(buf, "%02X:%02X:%02X:%02X:%02X:%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
return String(buf);
}
/**
* @brief Extracts and prints human-readable data from the Wi-Fi packet.
*/
void promiscuous_rx_cb(void* buffer, wifi_promiscuous_pkt_type_t type) {
// Cast the raw buffer to the promiscuous packet structure
const wifi_promiscuous_pkt_t* ppkt = (wifi_promiscuous_pkt_t*)buffer;
// Cast the payload to the 802.11 packet structure for header access
const wifi_ieee80211_packet_t *ipkt = (wifi_ieee80211_packet_t *)ppkt->payload;
const wifi_ieee80211_mac_hdr_t *hdr = &ipkt->hdr;
// Frame Control: Byte 0 of the MAC header contains Type (bits 2-3) and Subtype (bits 4-7)
uint8_t frame_subtype = (hdr->frame_ctrl & 0xFC) >> 2; // Type and Subtype bits
// Filter for Management Frames: Probe Request (Subtype 4) or Beacon (Subtype 8)
//~ if (frame_subtype != 0x04 && frame_subtype != 0x08) {
//~ return; // Ignore all other frame types
//~ }
// Determine the packet type string and where the IEs start
const char *pkt_type_str = "UNKNOWN";
uint16_t ie_start_offset = 0;
if (frame_subtype == 0x40) {
// Probe Request (0x40): MAC Header (24) + Fixed Params (2) = 26
pkt_type_str = "PROBE REQUEST";
ie_start_offset = 26;
} else if (frame_subtype == 0x80) {
// Beacon (0x80): MAC Header (24) + Fixed Params (12) = 36
pkt_type_str = "BEACON";
ie_start_offset = 36;
} else {
return; // Should be caught by the first check, but safe guard.
}
// Pointer to the start of the Information Elements (IEs)
uint8_t *ie_payload = (uint8_t *)ppkt->payload + ie_start_offset;
uint16_t ie_payload_len = ppkt->rx_ctrl.sig_len - ie_start_offset;
// Prepare for SSID extraction
char ssid[33] = {0};
uint8_t ssid_length = 0;
// Iterate through IEs to find the SSID (Tag 0)
// The structure of an IE is: [Tag (1 byte)][Length (1 byte)][Data (Length bytes)]
for (uint16_t i = 0; i < ie_payload_len; ) {
uint8_t tag = ie_payload[i];
uint8_t len = ie_payload[i + 1];
// Check if this is the SSID element (Tag 0)
if (tag == 0 && len <= 32) {
ssid_length = len;
// Copy data (which starts at i + 2)
memcpy(ssid, &ie_payload[i + 2], ssid_length);
ssid[ssid_length] = '\0'; // Null termination
break; // Found SSID, stop searching IEs
}
// Move to the next IE: Current IE size (Tag + Length + Data)
i += (2 + len);
}
// Apply the filter: Skip logging if SSID is a wildcard (length 0)
// BUT only for PROBE REQUESTS. Beacons should always have a length > 0.
if (frame_subtype == 0x04 && ssid_length == 0) {
// This is a filtered wildcard probe request (SSID is NULL)
// If you wanted to log that it happened:
// Serial.printf("[CH%02d | %4d dBm] Wildcard Probe from %s\n",
// current_channel, ppkt->rx_ctrl.rssi, mac_to_string(hdr->addr2).c_str());
return;
}
// ========================================================================
// PRINT HUMAN-READABLE OUTPUT
// ========================================================================
Serial.printf("----------------------------------------------------------\n");
Serial.printf("| %-13s | CH %02d | RSSI %4d dBm |\n",
pkt_type_str, current_channel, ppkt->rx_ctrl.rssi);
Serial.printf("----------------------------------------------------------\n");
// Print MAC addresses (Source MAC is addr2)
Serial.printf("SOURCE MAC: %s\n", mac_to_string(hdr->addr2).c_str());
Serial.printf("DESTINATION MAC: %s\n", mac_to_string(hdr->addr1).c_str());
Serial.printf("BSSID: %s\n", mac_to_string(hdr->addr3).c_str());
// Print the SSID
if (ssid_length > 0) {
Serial.printf("SSID: %s\n", ssid);
} else {
// This should only happen for certain types of management frames
// that still need logging, or if IE parsing failed unexpectedly.
Serial.printf("SSID: [Unknown/Empty]\n");
}
Serial.printf("Packet Length: %u bytes\n", ppkt->rx_ctrl.sig_len);
Serial.printf("----------------------------------------------------------\n\n");
}
void setup() {
Serial.begin(115200);
delay(100);
Serial.println("Starting Wi-Fi Promiscuous Sniffer...");
WiFi.mode(WIFI_MODE_STA); // WIFI_STA?
WiFi.disconnect();
esp_wifi_set_channel(current_channel, WIFI_SECOND_CHAN_NONE);
esp_wifi_set_promiscuous(true);
esp_wifi_set_promiscuous_rx_cb(&promiscuous_rx_cb);
Serial.println("Promiscuous mode enabled. Monitoring packets...");
last_channel_hop = millis();
}
void loop() {
hop_channel();
delay(10);
}
[env:xiao_esp32s3]
platform = espressif32
board = seeed_xiao_esp32s3
framework = arduino
monitor_speed = 115200
board_build.partitions = huge_app.csv
board_build.flash_mode = qio
board_build.flash_size = 8MB
board_build.psram_type = opi
lib_deps =
h2zero/NimBLE-Arduino@^1.4.0
bblanchon/ArduinoJson@^6.21.0
build_flags =
-DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1
-DCONFIG_BT_NIMBLE_ENABLED=1
[env:xiao_esp32c3]
platform = espressif32
board = seeed_xiao_esp32c3
framework = arduino
monitor_speed = 115200
board_build.partitions = huge_app.csv
board_build.flash_mode = qio
board_build.flash_size = 4MB
board_build.psram_type = opi
lib_deps =
h2zero/NimBLE-Arduino@^1.4.0
bblanchon/ArduinoJson@^6.21.0
build_flags =
-DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1
-DCONFIG_BT_NIMBLE_ENABLED=1
[env:esp32-s3-devkit]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
monitor_speed = 115200
board_build.partitions = huge_app.csv
board_build.flash_mode = qio
board_build.flash_size = 32MB
board_build.psram_type = opi
lib_deps =
h2zero/NimBLE-Arduino@^1.4.0
bblanchon/ArduinoJson@^6.21.0
build_flags =
-DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1
-DCONFIG_BT_NIMBLE_ENABLED=1
[env:esp32-c3-devkitm-1]
platform = espressif32
board = esp32-c3-devkitm-1
framework = arduino
monitor_speed = 115200
board_build.partitions = huge_app.csv
board_build.flash_mode = qio
lib_deps =
h2zero/NimBLE-Arduino@^1.4.0
bblanchon/ArduinoJson@^6.21.0
build_flags =
-DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1
-DCONFIG_BT_NIMBLE_ENABLED=1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment