From 40034ee3884bd6be076999880f7dc7645932baac Mon Sep 17 00:00:00 2001 From: Thomaz de Torres Bandeira Date: Fri, 14 Aug 2026 01:31:24 -0300 Subject: [PATCH 1/4] feat: add ESP-NOW capture callback API --- src/Politician.h | 19 +++++++++++++++---- src/PoliticianTypes.h | 22 ++++++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/Politician.h b/src/Politician.h index 0ab466a..5c1a78f 100644 --- a/src/Politician.h +++ b/src/Politician.h @@ -72,6 +72,7 @@ typedef struct { #define MGMT_SUB_AUTH 0xB0 #define MGMT_SUB_DISASSOC 0xA0 #define MGMT_SUB_DEAUTH 0xC0 +#define MGMT_SUB_ACTION 0xD0 // ─── EAPOL ──────────────────────────────────────────────────────────────────── #define EAPOL_LLC_OFFSET 0 @@ -456,13 +457,20 @@ class Politician { void setClientFoundCallback(ClientFoundCb cb) { _clientFoundCb = cb; } /** - * @brief Sets the callback fired when a WPS Enrollee's M1 message is captured. - * The M1 message is the first EAP-WSC frame sent by the Enrollee and is unencrypted, - * revealing device attributes (name, manufacturer, model, auth/config capabilities). - * Only fired if the callback is set — has zero overhead otherwise. + * @brief Set callback for Wi-Fi Protected Setup (WPS) M1 messages. + * Captured from unassociated Probe Requests. + * @param cb Callback function */ void setWpsCallback(WpsCb cb) { _wpsCb = cb; } +#ifndef POLITICIAN_NO_ESPNOW + /** + * @brief Set callback for captured ESP-NOW action frames. + * @param cb Callback function + */ + void setEspNowCallback(EspNowCb cb) { _espNowCb = cb; } +#endif + #ifndef POLITICIAN_NO_MSCHAPV2 /** * @brief Sets the callback fired on a bare EAP-MSCHAPv2 challenge/response exchange. @@ -627,6 +635,9 @@ class Politician { RogueApCb _rogueApCb = nullptr; _FpHookCb _fpHook = nullptr; WpsCb _wpsCb = nullptr; +#ifndef POLITICIAN_NO_ESPNOW + EspNowCb _espNowCb = nullptr; +#endif #ifndef POLITICIAN_NO_KARMA KarmaCb _karmaCb = nullptr; bool _karmaEnabled = false; diff --git a/src/PoliticianTypes.h b/src/PoliticianTypes.h index afb373b..02f8dc4 100644 --- a/src/PoliticianTypes.h +++ b/src/PoliticianTypes.h @@ -365,6 +365,28 @@ typedef void (*KarmaCb)(const KarmaRecord &rec); #endif #endif // POLITICIAN_NO_KARMA +// ─── ESP-NOW Record ─────────────────────────────────────────────────────────── +#ifndef POLITICIAN_NO_ESPNOW +/** + * @brief Delivered to the EspNowCb callback when an ESP-NOW vendor-specific action frame is captured. + */ +struct EspNowRecord { + uint8_t src[6]; ///< Source MAC address (transmitter) + uint8_t dst[6]; ///< Destination MAC address (receiver or broadcast) + uint8_t channel; ///< Channel on which the frame was captured + int8_t rssi; ///< Signal strength of the frame (dBm) + uint32_t ts_usec; ///< RX timestamp in microseconds + const uint8_t* payload; ///< Pointer to the ESP-NOW payload (internal buffer, do not free) + uint16_t length; ///< Length of the ESP-NOW payload +}; + +#ifndef POLITICIAN_NO_STD_FUNCTION +using EspNowCb = std::function; +#else +typedef void (*EspNowCb)(const EspNowRecord &rec); +#endif +#endif // POLITICIAN_NO_ESPNOW + // ─── 802.1X Enterprise Identity Record ───────────────────────────────────────── // EAP method constants (RFC 3748 / RFC 5281) static const uint8_t EAP_METHOD_IDENTITY = 0x01; ///< EAP Identity (always 0x01 for harvested records) From 88f0cbe9159de7568f1e6421a50e0f8251c90e71 Mon Sep 17 00:00:00 2001 From: Thomaz de Torres Bandeira Date: Fri, 14 Aug 2026 01:31:46 -0300 Subject: [PATCH 2/4] feat: parse ESP-NOW action frames --- src/Politician.cpp | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/Politician.cpp b/src/Politician.cpp index 3e7d23a..6b22c83 100644 --- a/src/Politician.cpp +++ b/src/Politician.cpp @@ -1058,6 +1058,35 @@ void Politician::_handleMgmt(const ieee80211_hdr_t *hdr, const uint8_t *payload, return; } +#ifndef POLITICIAN_NO_ESPNOW + if (subtype == MGMT_SUB_ACTION) { + if (_espNowCb != nullptr && len >= 15) { + if (payload[0] == 0x7F && + payload[1] == 0x18 && payload[2] == 0xFE && payload[3] == 0x34 && + payload[8] == 0xDD && + payload[10] == 0x18 && payload[11] == 0xFE && payload[12] == 0x34 && + payload[13] == 0x04 && payload[14] == 0x01) { + const uint16_t bodyOffset = 15; + const uint16_t elementBodyLength = payload[9] >= 5 ? payload[9] - 5 : 0; + const uint16_t availableBodyLength = len - bodyOffset; + const uint16_t bodyLength = elementBodyLength < availableBodyLength + ? elementBodyLength + : availableBodyLength; + EspNowRecord rec; + memcpy(rec.src, hdr->addr2, 6); + memcpy(rec.dst, hdr->addr1, 6); + rec.channel = _rxChannel; + rec.rssi = rssi; + rec.ts_usec = (uint32_t)esp_timer_get_time(); + rec.payload = payload + bodyOffset; + rec.length = bodyLength; + _espNowCb(rec); + } + } + return; + } +#endif + // Parse both Beacons and Probe Responses. // Sniffing Probe Responses automatically enables Active Decloaking // of Hidden Networks when clients reconnect following a CSA/Deauth attack. From 4dbeece1cca5dd5ac2955956748320b88c0aef25 Mon Sep 17 00:00:00 2001 From: Thomaz de Torres Bandeira Date: Fri, 14 Aug 2026 01:31:49 -0300 Subject: [PATCH 3/4] example: add ESP-NOW capture example --- examples/EspNowCapture/platformio.ini | 11 ++++++ examples/EspNowCapture/src/main.cpp | 48 +++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 examples/EspNowCapture/platformio.ini create mode 100644 examples/EspNowCapture/src/main.cpp diff --git a/examples/EspNowCapture/platformio.ini b/examples/EspNowCapture/platformio.ini new file mode 100644 index 0000000..0252138 --- /dev/null +++ b/examples/EspNowCapture/platformio.ini @@ -0,0 +1,11 @@ +[platformio] +default_envs = esp32 + +[env:esp32] +platform = espressif32 +board = esp32dev +framework = arduino +monitor_speed = 115200 +lib_extra_dirs = ../../ +build_flags = + -D CORE_DEBUG_LEVEL=5 diff --git a/examples/EspNowCapture/src/main.cpp b/examples/EspNowCapture/src/main.cpp new file mode 100644 index 0000000..5dedee7 --- /dev/null +++ b/examples/EspNowCapture/src/main.cpp @@ -0,0 +1,48 @@ +#include +#include + +using namespace politician; + +Politician engine; + +void setup() { + Serial.begin(115200); + delay(2000); + Serial.println("\n\n--- Politician ESP-NOW Capture Example ---"); + + Config cfg; + // We don't necessarily need to capture beacons for ESP-NOW, but it helps populate the AP cache + cfg.capture_filter = LOG_FILTER_BEACONS | LOG_FILTER_PROBES; + + // Initialize the engine + if (engine.begin(cfg) != politician::OK) { + Serial.println("Failed to start Politician engine"); + while (1) delay(100); + } + + // Set the callback to process ESP-NOW payloads + engine.setEspNowCallback([](const EspNowRecord &rec) { + Serial.printf("[ESP-NOW] ch=%d rssi=%d src=%02x:%02x:%02x:%02x:%02x:%02x dst=%02x:%02x:%02x:%02x:%02x:%02x len=%d payload=[", + rec.channel, rec.rssi, + rec.src[0], rec.src[1], rec.src[2], rec.src[3], rec.src[4], rec.src[5], + rec.dst[0], rec.dst[1], rec.dst[2], rec.dst[3], rec.dst[4], rec.dst[5], + rec.length); + + // Print the first few bytes of the payload in hex + int printLen = rec.length > 16 ? 16 : rec.length; + for (int i = 0; i < printLen; i++) { + Serial.printf("%02x ", rec.payload[i]); + } + if (rec.length > 16) Serial.print("..."); + Serial.println("]"); + }); + + // Start hopping to discover ESP-NOW devices on any channel, + // or use engine.lockChannel(1) if you know their specific channel. + engine.startHopping(); + Serial.println("Hopping started. Waiting for ESP-NOW traffic..."); +} + +void loop() { + engine.tick(); +} From 4ffa9297bbff06f2362166d69e2ce8d0d01992ca Mon Sep 17 00:00:00 2001 From: Albert Katri Date: Wed, 28 Oct 2026 13:03:41 -0300 Subject: [PATCH 4/4] fix: address review feedback on esp-now capture and add to ci matrix - Include esp_timer.h in politician_compat.h under ARDUINO - Restore accurate WPS M1 callback docstring in Politician.h - Clarify buffer lifetime and memcpy recommendation in EspNowRecord - Update EspNowCapture platformio.ini to use symlink lib_deps - Add EspNowCapture to build-examples CI matrix and pin action SHAs - Document EspNowCapture in README examples table --- .github/workflows/build-examples.yml | 7 ++++--- README.md | 1 + examples/EspNowCapture/platformio.ini | 5 +++-- src/Politician.h | 7 +++++-- src/PoliticianTypes.h | 2 +- src/politician_compat.h | 1 + 6 files changed, 15 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-examples.yml b/.github/workflows/build-examples.yml index 3deb901..0aad4a7 100644 --- a/.github/workflows/build-examples.yml +++ b/.github/workflows/build-examples.yml @@ -30,13 +30,14 @@ jobs: - MsChapCapture - WpsCapture - KarmaResponder + - EspNowCapture steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: '3.x' @@ -44,7 +45,7 @@ jobs: run: pip install platformio intelhex - name: Cache PlatformIO packages - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: ~/.platformio key: pio-${{ runner.os }}-${{ hashFiles('**/platformio.ini') }} diff --git a/README.md b/README.md index efde051..e31aab3 100644 --- a/README.md +++ b/README.md @@ -1127,6 +1127,7 @@ beaconFlood(ssids, ssidCount, 6, 5000); | `MsChapCapture` | Bare EAP-MSCHAPv2 credential capture (hashcat -m 5500) | | `KarmaResponder` | KARMA rogue AP responder with runtime Serial toggle | | `PassiveSensing` | RSSI-based human presence and motion detection | +| `EspNowCapture` | ESP-NOW action frame capture and payload parsing | ## Legal & Ethical Use diff --git a/examples/EspNowCapture/platformio.ini b/examples/EspNowCapture/platformio.ini index 0252138..628399e 100644 --- a/examples/EspNowCapture/platformio.ini +++ b/examples/EspNowCapture/platformio.ini @@ -2,10 +2,11 @@ default_envs = esp32 [env:esp32] -platform = espressif32 +platform = espressif32 @ ~6.13.0 board = esp32dev framework = arduino monitor_speed = 115200 -lib_extra_dirs = ../../ +lib_deps = + symlink://../.. build_flags = -D CORE_DEBUG_LEVEL=5 diff --git a/src/Politician.h b/src/Politician.h index 5c1a78f..771682e 100644 --- a/src/Politician.h +++ b/src/Politician.h @@ -457,8 +457,10 @@ class Politician { void setClientFoundCallback(ClientFoundCb cb) { _clientFoundCb = cb; } /** - * @brief Set callback for Wi-Fi Protected Setup (WPS) M1 messages. - * Captured from unassociated Probe Requests. + * @brief Sets the callback fired when a WPS Enrollee's M1 message is captured. + * The M1 message is the first EAP-WSC frame sent by the Enrollee and is unencrypted, + * revealing device attributes (name, manufacturer, model, auth/config capabilities). + * Only fired if the callback is set — has zero overhead otherwise. * @param cb Callback function */ void setWpsCallback(WpsCb cb) { _wpsCb = cb; } @@ -466,6 +468,7 @@ class Politician { #ifndef POLITICIAN_NO_ESPNOW /** * @brief Set callback for captured ESP-NOW action frames. + * Only fired if the callback is set — has zero overhead otherwise. * @param cb Callback function */ void setEspNowCallback(EspNowCb cb) { _espNowCb = cb; } diff --git a/src/PoliticianTypes.h b/src/PoliticianTypes.h index 02f8dc4..9415769 100644 --- a/src/PoliticianTypes.h +++ b/src/PoliticianTypes.h @@ -376,7 +376,7 @@ struct EspNowRecord { uint8_t channel; ///< Channel on which the frame was captured int8_t rssi; ///< Signal strength of the frame (dBm) uint32_t ts_usec; ///< RX timestamp in microseconds - const uint8_t* payload; ///< Pointer to the ESP-NOW payload (internal buffer, do not free) + const uint8_t* payload; ///< Pointer to internal ring-buffer memory; only valid for callback duration. Do not free, retain, or queue past callback return; memcpy if needed. uint16_t length; ///< Length of the ESP-NOW payload }; diff --git a/src/politician_compat.h b/src/politician_compat.h index b366e67..922bc9e 100644 --- a/src/politician_compat.h +++ b/src/politician_compat.h @@ -2,6 +2,7 @@ #ifdef ARDUINO #include + #include #else #include #include