A sophisticated WiFi auditing library for ESP32 microcontrollers

Politician is an embedded C++ library designed for WiFi security auditing on ESP32 platforms. It provides a clean, modern API for capturing WPA/WPA2/WPA3 handshakes and harvesting enterprise credentials using advanced 802.11 protocol techniques.
Migration Notes
ClientFoundCb signature change (develop)
ClientFoundCb was changed from (const uint8_t *bssid, const uint8_t *sta, int8_t rssi) to (const ClientRecord &rec). Update existing callbacks:
engine.setClientFoundCallback([](
const uint8_t *bssid,
const uint8_t *sta, int8_t rssi) { … });
engine.setClientFoundCallback([](
const ClientRecord &rec) {
});
Key Capabilities
- PMKID Capture — Extract PMKIDs via fake association without disconnecting any client
- CSA Injection — Channel Switch Announcement; modern PMF-bypassing alternative to deauth
- 802.11v BTM Injection — BSS Transition Management Request; politely steers clients to reconnect
- Classic Deauth — Reason-7 deauthentication for legacy networks without PMF
- Client Stimulation — Wake sleeping devices with QoS Null Data frames
- Enterprise Credential Harvesting — Passively capture EAP-Identity and bare EAP-MSCHAPv2 exchanges
- WPS M1 Capture — Harvest device fingerprint attributes from unencrypted WPS Enrollee M1 messages
- Hidden Network Discovery — SSID decloaking via directed probe requests; optional wordlist cycling
- VHT/HE Detection — Parse 802.11ac/ax IEs to expose channel width and Wi-Fi generation per AP
- Device Fingerprinting — Identify 150+ IoT/consumer brands via MAC OUI and IE signatures
- Export Formats — Streaming PCAPNG (Wireshark-compatible); auxiliary HC22000 for Hashcat; Wigle CSV
- Passive Motion Sensing — RSSI variance analysis detects human presence and movement via a fixed anchor AP; device-free, no mode switching, runs alongside the audit engine
Architecture
The library is built around a non-blocking state machine managing channel hopping, target selection, attack execution, and capture processing. All operations are contained within the politician namespace.
Core Headers
Attack Modes
| Mode | Bit | Description |
ATTACK_PMKID | 0x01 | PMKID fishing via fake association |
ATTACK_CSA | 0x02 | Channel Switch Announcement injection |
ATTACK_PASSIVE | 0x04 | Listen-only — zero transmission |
ATTACK_DEAUTH | 0x08 | Classic Reason-7 deauthentication |
ATTACK_STIMULATE | 0x10 | QoS Null Data client stimulation |
ATTACK_BTM | 0x20 | 802.11v BSS Transition Management Request |
ATTACK_ALL | 0x3F | All active attack vectors |
Compile-Time Feature Gates
Define any of these before including Politician.h or via your build system (-DNAME):
; platformio.ini
build_flags =
-DPOLITICIAN_NO_DB ; Strip 14KB OUI database (no vendor lookups)
-DPOLITICIAN_NO_PCAPNG ; Strip PCAPNG serialization
-DPOLITICIAN_NO_HC22000 ; Strip Hashcat HC22000 formatter
-DPOLITICIAN_NO_LOGGING ; Strip all internal Serial log output
-DPOLITICIAN_NO_STD_FUNCTION ; Use raw fn pointers instead of std::function
-DPOLITICIAN_NO_MSCHAPV2 ; Strip bare EAP-MSCHAPv2 capture
-DPOLITICIAN_NO_KARMA ; Strip KARMA rogue AP responder
-DPOLITICIAN_MAX_INSTANCES=1 ; Limit to one instance (saves a pointer array slot)
POLITICIAN_NO_STD_FUNCTION reverts all callback types from std::function<> to raw function pointers — saving ~2KB flash and enabling use in environments without <functional>. Lambda captures are unavailable when this flag is set.
POLITICIAN_MAX_INSTANCES (default 2) controls the size of the static instance registry used by the promiscuous ISR dispatcher. Set to 1 for single-instance deployments to eliminate the loop overhead in the ISR.
Installation
PlatformIO
[env:myboard]
platform = espressif32
board = esp32dev
framework = arduino
lib_deps =
Politician
Or clone directly into your project's lib/ directory:
cd lib/
git clone https://github.com/0ldev/Politician.git
Arduino IDE
- Download the library as a ZIP file
- Sketch → Include Library → Add .ZIP Library
ESP-IDF
Clone into your project's components/ directory and create a CMakeLists.txt:
idf_component_register(
SRCS "src/Politician.cpp" "src/PoliticianFormat.cpp" "src/PoliticianStress.cpp"
INCLUDE_DIRS "src"
)
PoliticianStorage.h is Arduino-only and emits a #error under ESP-IDF. Use ESP-IDF's VFS and nvs_flash APIs directly.
Quick Start
Basic Handshake Capture
#include <Arduino.h>
#include <SD.h>
Serial.printf("[✓] %s ch%d rssi=%d type=%d\n",
}
Serial.begin(115200);
SD.begin();
pcap.
open(SD,
"/captures.pcapng", 4 * 1024 * 1024);
}
}
The core WiFi handshake capturing engine.
Helper for writing HandshakeRecords and raw packets to a standard PCAPNG file.
bool open(fs::FS &fs, const char *path, uint32_t maxBytes=0)
Opens the PCAPNG file for streaming writes.
bool write(const HandshakeRecord &rec)
Writes a HandshakeRecord to the open file.
void onHandshake(const HandshakeRecord &rec)
A captured handshake or PMKID record delivered to the EapolCb callback.
Bare ESP-IDF Quick Start
#include <nvs_flash.h>
#include <esp_event.h>
static void audit_task(void *) {
});
for (;;) {
engine.tick(); vTaskDelay(pdMS_TO_TICKS(1)); }
}
extern "C" void app_main(void) {
nvs_flash_init();
esp_event_loop_create_default();
xTaskCreate(audit_task, "politician", 8192, nullptr, 5, nullptr);
}
void setEapolCallback(EapolCb cb)
Sets the callback for when a handshake (EAPOL or PMKID) is captured.
API Reference
Initialization
Configuration for the Politician engine.
Initializes the WiFi driver in promiscuous mode. Must be called before any other method. Clamps and warns on invalid Config values at startup. Use validateConfig() beforehand to surface misconfigurations before deploying in the field:
const char *warnings[8];
for (int i = 0; i < n; i++) Serial.println(warnings[i]);
uint16_t hop_max_dwell_ms
int validateConfig(const Config &cfg, const char **out, uint8_t maxOut)
Validates a Config struct and returns human-readable warning strings for values that will be silently...
uint16_t hop_min_dwell_ms
validateConfig() is a free inline function in the politician namespace — zero allocations, zero dependencies.
Configuration
uint16_t hop_dwell_ms = 200;
bool smart_hopping = true;
uint16_t hop_min_dwell_ms = 50;
uint16_t hop_max_dwell_ms = 400;
uint32_t m1_lock_ms = 800;
uint32_t fish_timeout_ms = 2000;
uint8_t fish_max_retries = 2;
uint32_t csa_wait_ms = 4000;
uint8_t csa_beacon_count = 8;
uint8_t csa_deauth_count = 15;
uint8_t deauth_burst_count = 16;
uint8_t deauth_reason = 7;
bool deauth_reason_cycling = true;
uint8_t btm_burst_count = 8;
uint16_t btm_disassoc_timer = 3;
uint16_t probe_aggr_interval_s = 30;
uint32_t session_timeout_ms = 60000;
uint32_t ap_expiry_ms = 300000;
uint32_t probe_hidden_interval_ms = 0;
bool capture_half_handshakes = false;
bool capture_group_keys = false;
int8_t min_rssi = -100;
uint8_t min_beacon_count = 0;
uint8_t max_total_attempts = 0;
bool skip_immune_networks = true;
bool require_active_clients = false;
bool unicast_deauth = true;
uint8_t sta_filter[6] = {};
char ssid_filter[33] = {};
bool ssid_filter_exact = true;
uint8_t enc_filter_mask = 0xFF;
const char* soft_ap_ssid = nullptr;
};
#define LOG_FILTER_HANDSHAKES
#define LOG_FILTER_PROBES
Callbacks
Register any subset — unregistered callbacks have zero overhead.
void setEapolCallback(EapolCb cb);
void setApFoundCallback(ApFoundCb cb);
void setIdentityCallback(IdentityCb cb);
void setWpsCallback(WpsCb cb);
void setMsChapCallback(MsChapCb cb);
void setClientFoundCallback(ClientFoundCb cb);
void setAttackResultCallback(AttackResultCb cb);
void setProbeRequestCallback(ProbeRequestCb cb);
void setDisruptCallback(DisruptCb cb);
void setTargetFilter(TargetFilterCb cb);
void setTargetScoreCallback(TargetScoreCb cb);
void setPacketLogger(PacketCb cb);
void setLogger(
LogCb cb);
std::function< void(const char *msg)> LogCb
std::function< void(const RogueApRecord &rec)> RogueApCb
Engine Control
void tick();
void setActive(bool active);
void stop();
void startHopping(uint16_t dwellMs=0);
void stopHopping();
Error lockChannel(uint8_t ch);
Error setChannel(uint8_t ch);
void setChannelList(const uint8_t *channels, uint8_t count);
uint8_t getChannelsSortedByActivity(uint8_t *out, uint8_t count) const;
uint8_t setAutoChannelList(uint8_t topN);
void setChannelBands(bool ghz24, bool ghz5);
Target Control
Error setTarget(
const uint8_t *bssid, uint8_t channel);
Error setTargetBySsid(
const char *ssid);
void clearTarget();
bool hasTarget() const;
bool isAttacking() const;
void setAutoTarget(bool enable);
void setAttackMask(uint8_t mask);
void setAttackMaskForBssid(const uint8_t *bssid, uint8_t mask);
void setAttackMaskForSsid(const char *ssid, uint8_t mask, bool substring=false);
void clearAttackMaskOverrides();
AP Cache & Stats
int getApCount() const;
bool getAp(
int idx,
ApRecord &out)
const;
bool getApByBssid(
const uint8_t *bssid,
ApRecord &out)
const;
void forEachAp(
void (*cb)(
const ApRecord &ap,
void *ctx),
void *ctx)
const;
int getClientCount(const uint8_t *bssid) const;
bool getClient(const uint8_t *bssid, int idx, uint8_t out_sta[6]) const;
void resetStats();
Snapshot of a discovered Access Point from the internal cache.
Cumulative frame and capture counters for the engine session.
Probe Wordlist
void setProbeWordlist(const char * const *wordlist, uint8_t count);
When cfg.probe_hidden_interval_ms > 0, the engine probes each hidden AP with one wordlist entry per interval, cycling independently per AP. Use with PoliticianProbe.h:
engine.setProbeWordlist(WORDLIST, WORDLIST_COUNT);
Opt-in SSID wordlist for hidden network discovery via directed probe requests.
uint32_t probe_hidden_interval_ms
Pass nullptr to revert to wildcard-only probing. The wordlist must remain in scope for the engine's lifetime (PROGMEM or static storage).
Frame Injection
Error injectCustomFrame(
const uint8_t *payload,
size_t len, uint8_t channel,
uint32_t lock_ms = 0, bool wait_for_channel = false);
lock_ms > 0 — disable hopping and hold channel for this duration after injection
wait_for_channel = true — queue frame for stealthy injection when hopper naturally lands on the channel
Data Structures
Config Constants
#define ENC_OPEN 0
#define ENC_WEP 1
#define ENC_WPA 2
#define ENC_WPA2 3
#define ENC_ENT 4
#define ENC_OWE 5
#define ATTACK_PMKID 0x01
#define ATTACK_CSA 0x02
#define ATTACK_PASSIVE 0x04
#define ATTACK_DEAUTH 0x08
#define ATTACK_STIMULATE 0x10
#define ATTACK_BTM 0x20
#define ATTACK_ALL 0x3F
#define CAP_PMKID 0x01
#define CAP_EAPOL 0x02
#define CAP_EAPOL_CSA 0x03
#define CAP_EAPOL_HALF 0x04
#define CAP_EAPOL_GROUP 0x05
#define CAP_SAE 0x06
#define CIPHER_UNKNOWN 0
#define CIPHER_TKIP 1
#define CIPHER_CCMP 2
#define LOG_FILTER_HANDSHAKES 0x01
#define LOG_FILTER_PROBES 0x02
#define LOG_FILTER_BEACONS 0x04
#define LOG_FILTER_PROBE_REQ 0x08
#define LOG_FILTER_MGMT_DISRUPT 0x10
#define LOG_FILTER_ALL 0xFF
ApRecord
uint8_t bssid[6];
char ssid[33];
uint8_t ssid_len;
uint8_t channel;
int8_t rssi;
uint8_t enc;
bool wps_enabled;
bool pmf_capable;
bool pmf_required;
bool ft_capable;
bool is_hidden;
bool is_vht;
bool is_he;
uint8_t chan_width;
uint8_t total_attempts;
bool captured;
uint32_t first_seen_ms;
uint32_t last_seen_ms;
char country[3];
uint16_t beacon_interval;
uint8_t max_rate_mbps;
uint16_t sta_count;
uint8_t chan_util;
uint8_t venue_group;
uint8_t venue_type;
uint8_t network_type;
uint32_t beacon_count;
uint8_t capture_count;
uint32_t last_attack_ms;
};
Stats
uint32_t total;
uint32_t mgmt;
uint32_t ctrl;
uint32_t data;
uint32_t eapol;
uint32_t pmkid_found;
uint32_t sae_found;
uint32_t beacons;
uint32_t captures;
uint32_t failed_pmkid;
uint32_t failed_csa;
volatile uint32_t dropped;
uint32_t rb_max;
uint16_t channel_frames[200];
};
HandshakeRecord
uint8_t type;
uint8_t channel;
int8_t rssi;
uint8_t bssid[6];
uint8_t sta[6];
char ssid[33];
uint8_t ssid_len;
uint8_t enc;
uint8_t cipher;
uint8_t pmkid[16];
uint8_t anonce[32];
uint8_t snonce[32];
uint8_t mic[16];
uint8_t eapol_m2[256];
uint8_t eapol_m3[256];
uint8_t eapol_m4[256];
uint16_t eapol_m2_len;
uint16_t eapol_m3_len;
uint16_t eapol_m4_len;
bool has_mic;
bool has_anonce;
bool has_snonce;
bool has_m3;
bool has_m4;
bool is_full;
uint8_t sae_seq;
};
EapIdentityRecord
uint8_t bssid[6];
uint8_t client[6];
char identity[65];
uint8_t channel;
int8_t rssi;
uint8_t eap_method;
};
A harvested 802.1X Enterprise plaintext identity, delivered to the IdentityCb callback.
WpsRecord
Captured from WPS M1 (Enrollee → AP). M1 is always unencrypted — subsequent messages are inside a TLS tunnel and cannot be read passively.
uint8_t bssid[6];
uint8_t sta[6];
uint8_t channel;
int8_t rssi;
char device_name[33];
char manufacturer[65];
char model_name[33];
char model_number[33];
char serial_number[33];
uint16_t auth_type_flags;
uint16_t config_methods;
uint8_t rf_bands;
uint16_t primary_dev_type_cat;
};
WPS M1 device attributes harvested from an EAP-WSC exchange.
Use PoliticianWPS::print(Serial, rec) for formatted output. See PoliticianWPS.h for bitmask constants and helpers.
MsChapRecord *(requires bare MSCHAPv2 — no PEAP/TTLS tunnel)*
uint8_t bssid[6];
uint8_t sta[6];
uint8_t channel;
int8_t rssi;
char username[65];
uint8_t server_challenge[16];
uint8_t peer_challenge[16];
uint8_t nt_response[24];
};
Bare EAP-MSCHAPv2 challenge/response pair harvested passively.
Note: This only fires against networks delivering bare EAP-MSCHAPv2 without a TLS tunnel — a misconfiguration. Most enterprise networks wrap MSCHAPv2 inside PEAP or TTLS, making it opaque. EapIdentityRecord (username only) works against all 802.1X deployments regardless of inner method.
Other Records
struct ClientRecord { uint8_t bssid[6]; uint8_t sta[6]; int8_t rssi; uint32_t first_seen_ms; uint32_t last_seen_ms;
bool rand_mac;
char vendor[32]; };
struct RogueApRecord { uint8_t known_bssid[6]; uint8_t rogue_bssid[6];
char ssid[33]; uint8_t ssid_len; uint8_t channel; int8_t rssi; };
struct ProbeRequestRecord { uint8_t client[6]; uint8_t channel; int8_t rssi;
char ssid[33]; uint8_t ssid_len;
bool rand_mac; };
struct DisruptRecord { uint8_t src[6]; uint8_t dst[6]; uint8_t bssid[6]; uint16_t reason; uint8_t subtype; uint8_t channel; int8_t rssi;
bool rand_mac; };
Identifies the AP and failure reason for a failed attack, delivered to the AttackResultCb callback.
Snapshot of a client station observed associated with an AP.
A deauthentication or disassociation frame observed on the air, delivered to the DisruptCb callback.
A probe request frame observed on the air, delivered to the ProbeRequestCb callback.
Fired when a second BSSID advertising the same SSID is observed on the same channel.
Storage Utilities
Requires #include <PoliticianStorage.h>. Arduino only.
Streaming PCAPNG (Recommended)
Keep the file handle open across the entire capture session — avoids repeated SD open/close overhead and reduces wear:
SD.begin();
storage::setTimestampProvider([]() -> const char * { return "2025-01-01 00:00:00"; });
pcap.
open(SD,
"/captures.pcapng", 8 * 1024 * 1024);
}
}
void onPacket(const uint8_t *payload, uint16_t len, int8_t rssi, uint8_t ch, uint32_t ts) {
}
void teardown() {
}
void close()
Closes the underlying file handle.
bool writePacket(const uint8_t *payload, uint16_t len, int8_t rssi, uint8_t channel, uint32_t ts_usec)
Writes a raw 802.11 sniffer frame to the open file.
One-Shot PCAPNG (Legacy)
Opens and closes the file on every call — suitable for infrequent writes:
PcapngFileLogger::append(SD, "/captures.pcapng", rec);
PcapngFileLogger::appendPacket(SD, "/intel.pcapng", payload, len, rssi, ch, ts);
HC22000
hc.
open(SD,
"/captures.hc22000");
Hc22000FileLogger::append(SD, "/captures.hc22000", rec);
Helper for writing HandshakeRecords to an HC22000 text file.
bool open(fs::FS &fs, const char *path)
Opens the HC22000 file for streaming writes.
void close()
Closes the underlying file handle.
bool write(const HandshakeRecord &rec)
Writes a HandshakeRecord to the open file.
Wigle CSV
WigleCsvLogger::appendAp(SD, "/wardrive.csv", ap, lat, lon);
WigleCsvLogger::append(SD, "/wardrive.csv", rec, lat, lon);
Enterprise CSV
EnterpriseCsvLogger::append(SD, "/identities.csv", rec);
NVS BSSID Cache (Deduplication)
if (!nvsCache.contains(rec.bssid)) {
nvsCache.add(rec.bssid);
}
}
void onTeardown() {
if (nvsCache.isDirty()) {
nvsCache.flush();
}
}
Helper for persistently storing captured BSSIDs in NVS memory.
The dirty-flag pattern batches NVS writes: add() only updates RAM; flush() performs the single NVS putBytes() call. Call flush() at natural checkpoints (e.g., channel hop, button press, end()).
Network Stream Loggers
TcpStreamLogger tcp(collectorIp, 9000);
UdpStreamLogger udp(collectorIp, 9001);
tcp.write(rec);
udp.writePacket(payload, len, rssi, ch, ts);
Define POLITICIAN_NO_NETWORK_LOGGER to strip these Arduino/WiFi-backed streamers.
Advanced Features
KARMA Rogue AP Responder
When a device sends a named probe request (looking for a remembered network), the KARMA responder replies with a matching probe response and beacon advertising that SSID as an open AP, enticing the device to auto-associate.
cfg.karma_enabled = true;
cfg.karma_open_only = true;
Serial.printf("[KARMA] echoed '%s' to %02X:%02X:%02X:%02X:%02X:%02X ch%d\n",
});
Delivered to the KarmaCb callback when the KARMA responder replies to a named probe request.
Config fields:
| Field | Default | Description |
karma_enabled | false | Enable KARMA at begin() |
karma_open_only | true | Skip probes for SSIDs already cached as WPA APs |
karma_max_ssids | 16 | Dedup table size (circular eviction, 10s suppression window) |
Strip the entire feature at compile time with -DPOLITICIAN_NO_KARMA.
Note: KARMA requires the engine to be in active (transmitting) mode. The spoofed AP uses an open (no RSN) configuration; clients that require WPA will not associate.
BTM (BSS Transition Management) is a Wi-Fi 802.11v mechanism that politely asks clients to roam. When a client respects it, it disconnects and reassociates — triggering a new EAPOL handshake. Combines with or replaces CSA/Deauth:
cfg.btm_burst_count = 8;
cfg.btm_disassoc_timer = 3;
BTM fires independently for every known client on the target AP. Most modern phones and laptops honour it; legacy 802.11a/b/g devices ignore it.
WPS M1 Device Fingerprinting
PoliticianWPS::print(Serial, rec);
});
Opt-in WPS M1 capture utilities for the Politician engine.
Bare EAP-MSCHAPv2 Capture
Serial.printf("[MSCHAPv2] user=%s\n", rec.username);
});
Hidden Network Discovery with Wordlist
cfg.probe_hidden_interval_ms = 5000;
engine.setProbeWordlist(WORDLIST, WORDLIST_COUNT);
static const char * const myList[] = { "CorpNet", "HQ-Internal", "IoT-Devices" };
engine.setProbeWordlist(myList, 3);
Each hidden AP in the cache maintains its own position in the wordlist — APs cycle independently so no word is ever skipped. If the AP responds, its SSID is revealed and stored in the cache automatically.
VHT/HE Detection
const char *gen = ap.is_he ? "Wi-Fi 6 (ax)" : ap.is_vht ? "Wi-Fi 5 (ac)" : "Wi-Fi 4 (n)";
const char *width[] = { "20MHz", "40MHz", "80MHz", "160MHz", "80+80MHz" };
Serial.printf("%s %s %s\n", ap.ssid, gen, width[ap.chan_width]);
});
HE/VHT-Aware Attack Path
Wi-Fi 5 (VHT) and Wi-Fi 6 (HE) networks that mandate PMF (pmf_required = true) cryptographically sign every management frame. Deauthentication and CSA beacon injections will be silently dropped by these clients.
The engine automatically detects this condition and suppresses ATTACK_DEAUTH and ATTACK_CSA for those networks, redirecting the attack cycle to ATTACK_PMKID and ATTACK_BTM — both of which work regardless of PMF:
is_he && pmf_required → skip DEAUTH + CSA → PMKID fish → BTM steer
is_vht && pmf_required → skip DEAUTH + CSA → PMKID fish → BTM steer
No configuration required — suppression is automatic. Use ATTACK_ALL and the engine selects the correct strategy per AP.
Exponential Backoff for Failing Targets
Each ApCacheEntry tracks total_attempts — the number of failed attack cycles against that BSSID. The PMKID throttle window doubles per failure, capped at 8 minutes:
throttle = base_ms << min(total_attempts, 4) // max 16× base
cap = 480 000 ms (8 minutes)
| Attempts | Base 30s | Effective window |
| 0 | 30s | 30s |
| 1 | 30s | 60s |
| 2 | 30s | 2 min |
| 3 | 30s | 4 min |
| ≥4 | 30s | 8 min (capped) |
This prevents the engine wasting its attack window on chronic failures. When has_target = true (manual target pinned) backoff is bypassed entirely so targeted sessions always attack immediately.
Multi-Instance Support
Up to POLITICIAN_MAX_INSTANCES (default 2) independent Politician objects can be active simultaneously. Each instance has its own ring buffer, worker task, AP cache, and callback set. The single promiscuous ISR dispatches every captured frame to all registered active instances.
#define POLITICIAN_MAX_INSTANCES 2
if (scanner.
begin(scanCfg) != OK) { }
if (auditor.
begin(auditCfg) != OK) { }
}
}
Error lockChannel(uint8_t ch)
Stops hopping and locks the radio to a specific channel.
void tick()
Main worker method.
void setActive(bool active)
Enables or disables frame processing.
Error begin(const Config &cfg=Config())
Initializes the WiFi driver in promiscuous mode.
void startHopping(uint16_t dwellMs=0)
Starts autonomous channel hopping.
Constraints:
- All instances share the single Wi-Fi radio.
esp_wifi_set_channel() affects every instance — coordinate channel access explicitly or use lockChannel() on the instance that should own the channel.
- The Wi-Fi driver is initialised once by whichever instance calls
begin() first. Subsequent instances skip driver init and inherit the promiscuous mode.
- Exceeding
POLITICIAN_MAX_INSTANCES returns ERR_MAX_INSTANCES (6) from begin().
stop() deregisters the instance so its slot is immediately reusable.
Passive Motion & Presence Sensing
PoliticianSense.h hooks into the engine's promiscuous-mode packet stream and measures RSSI variance from a fixed anchor AP to detect human presence and motion — no additional hardware, no mode switching, no conflict with the audit engine.
A human body walking between the anchor AP and the ESP32 absorbs and scatters 2.4 GHz radio waves, causing measurable fluctuations in the received beacon signal strength. PoliticianSense tracks variance over a configurable sliding window and fires a callback when the space transitions between quiet and active.
Scope: With a single ESP32 you get presence / motion detection — not localization. Knowing where in the space someone is requires multiple observation points.
Serial.printf("[SENSE] %s var=%.2f dBm²\n",
ev == SENSE_MOTION ? "MOTION" : "STILL", var);
});
uint8_t anchor[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF};
}
}
Opt-in passive RSSI-based presence and motion sensing for the Politician engine.
#define LOG_FILTER_BEACONS
Passive RSSI-based motion and presence detector.
void setSenseCallback(SenseCb cb)
Sets the state-change callback.
void setThreshold(float dBm2)
Sets the variance threshold (dBm²) that triggers SENSE_MOTION.
void setDebounce(uint32_t ms)
Sets the debounce hold-time (ms).
void begin(Politician &engine, const uint8_t *anchorBssid=nullptr)
Attaches the sensor to a Politician engine.
void setWindowSize(uint8_t n)
Sets the sliding window size in samples (clamped to [4, POLITICIAN_SENSE_MAX_WINDOW]).
void tick()
Main worker — call from loop() alongside engine.tick().
SenseEvent
State transition delivered to the SenseCb callback.
Anchor modes:
| Mode | Method | Notes |
| BSSID (recommended) | sense.begin(engine, bssid) | Most stable; survives SSID name changes |
| SSID lookup | sense.beginBySSID(engine, "MyRouter") | Picks strongest BSSID if multiple match |
| Any AP | sense.begin(engine, nullptr) | Aggregates all visible APs; noisier baseline |
API:
| Method | Description |
begin(engine, bssid) | Attach to engine; start sampling from anchor BSSID |
beginBySSID(engine, ssid) | Resolve BSSID by SSID from engine cache, then attach |
end() | Detach from engine and clear its packet logger slot |
tick() | Worker — call from loop() alongside engine.tick() |
setSenseCallback(cb) | Fired once per SENSE_STILL ↔ SENSE_MOTION transition |
setPacketLogger(cb) | Pass-through for raw-frame access alongside sensing |
setThreshold(dBm²) | Variance above which SENSE_MOTION fires. Range: 3–15. Default: 6.0 |
setWindowSize(n) | Sliding window depth in samples [4–64]. Default: 32 |
setDebounce(ms) | Hold SENSE_MOTION for this long after last spike. Default: 2000 |
setStaleTimeout(ms) | Zero variance and let debounce expire when no samples arrive for this long. Default: 10000 |
getVariance() | Current RSSI variance across the window (dBm²) |
getMeanRssi() | Mean RSSI across the window (dBm) |
getState() | SENSE_STILL or SENSE_MOTION |
getTotalSamples() | Total samples collected since begin() |
reset() | Clear sample window without detaching from engine |
Compile-time tuning:
#define POLITICIAN_SENSE_MAX_WINDOW 128
Tuning guide:
| Symptom | Fix |
| False triggers in an empty room | Raise setThreshold() |
| Real motion not detected | Lower setThreshold() or widen setWindowSize() |
| MOTION held too long after person leaves | Lower setDebounce() |
| Sparse samples / choppy data | Call engine.lockChannel(anchorCh) to stop hopping |
| Flat variance regardless of movement | Move anchor AP closer or choose a less-obstructed path |
PoliticianSense requires std::function support. Do not combine with POLITICIAN_NO_STD_FUNCTION.
cfg.capture_filter |= LOG_FILTER_BEACONS must be set before engine.begin(). PoliticianSense only samples beacon frames; without this flag no data is collected. The "SDMMC ONLY!" warning in PoliticianTypes.h applies to high-volume SD logging — in-memory callbacks are unaffected.
sense.setPacketLogger() must be called before sense.begin() if you need raw frame access alongside sensing. Setting it after begin() is a data race with the engine worker task.
AP Iteration and Rich Client Discovery
Serial.printf("%s beacons=%lu captures=%u\n", ap.ssid, (unsigned long)ap.beacon_count, ap.capture_count);
}, nullptr);
Serial.printf("STA %02X:%02X:%02X:%02X:%02X:%02X rand=%d vendor=%s\n",
rec.sta[0], rec.sta[1], rec.sta[2], rec.sta[3], rec.sta[4], rec.sta[5],
rec.rand_mac, rec.vendor);
});
engine.setTargetScoreCallback([](
const ApRecord &ap,
const char *vendor) ->
int {
int score = ap.rssi;
if (strstr(vendor, "Apple")) score += 50;
if (strstr(vendor, "Hikvision")) score += 80;
if (ap.is_hidden) score -= 100;
return score;
});
Per-BSSID / Per-SSID Attack Overrides
uint8_t sensitive_ap[6] = { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF };
Custom Disconnection Strategy
engine.setDisconnectionStrategy(STRATEGY_AUTO_FALLBACK);
engine.setDisconnectionStrategy(STRATEGY_SIMULTANEOUS);
Half-Handshakes and Smart Pivot
When cfg.capture_half_handshakes = true, M2-only captures fire the EAPOL callback with type = CAP_EAPOL_HALF, then the engine immediately launches CSA/Deauth to force a fresh 4-way handshake. HandshakeRecord.cipher is also populated from the cached RSN pairwise suite so offline tooling can distinguish TKIP vs CCMP captures.
Smart Channel Lists
uint8_t hottest[8];
uint8_t n =
engine.getChannelsSortedByActivity(hottest, 8);
if (n)
engine.setAutoChannelList(n);
This is useful after a warm-up scan when you want hopping restricted to the busiest channels only.
802.11r Fast Transition Detection
ApRecord.ft_capable is set when FT-PSK (suite type 4) or FT-EAP (suite type 3) AKMs are detected. For FT-only APs, save the PCAPNG capture and use hcxpcapngtool --enable_ft for offline cracking.
Usage Examples
Enterprise Identity Harvesting
Serial.printf("[802.1X] %s method=%u\n", rec.identity, rec.eap_method);
EnterpriseCsvLogger::append(SD, "/identities.csv", rec);
});
cfg.hop_dwell_ms = 800;
GPS Wardriving
#include <TinyGPS++.h>
TinyGPSPlus gps;
if (gps.location.isValid())
WigleCsvLogger::appendAp(SD, "/wardrive.csv", ap,
gps.location.lat(), gps.location.lng());
});
Selective Attack Filtering
if (ap.rssi < -70) return false;
if (ap.enc < 2) return false;
if (ap.pmf_required) return false;
return true;
});
Streaming Packet Capture
raw.
open(SD,
"/intel.pcapng");
engine.setPacketLogger([&](
const uint8_t *data, uint16_t len, int8_t rssi, uint8_t ch, uint32_t ts) {
});
Performance Considerations
| Concern | Guidance |
| SD writes | Use streaming open/write/close — never one-shot in a tight loop |
| Beacon logging | LOG_FILTER_BEACONS generates 500+ writes/s — requires SDMMC (4-bit DMA) |
| Enterprise capture | Set hop_dwell_ms = 800–1200 to not cut off EAP exchanges mid-flight |
| Flash size | Use feature gates (POLITICIAN_NO_DB, etc.) to trim binary on tight builds |
| RAM | Core engine ~45KB. Storage helpers and callbacks are opt-in |
| 5GHz | channel_frames[200] covers all channels; use setChannelBands(false, true) to hop 5GHz only |
Hardware Requirements
- Platform: ESP32, ESP32-S2, ESP32-S3, ESP32-C3
- Framework: Arduino or ESP-IDF (storage helpers are Arduino-only)
- Flash: 4MB minimum recommended
- Optional: SD card module for persistent logging; GPS module for Wigle integration
Troubleshooting
No handshakes captured
- Try
ATTACK_ALL for maximum aggression
- Increase
hop_dwell_ms for slow-reconnecting devices
- Check
skip_immune_networks — pure WPA3 APs are auto-skipped by default
Enterprise identities not captured
- Increase
hop_dwell_ms to 800-1200ms
- Use
ATTACK_PASSIVE or ATTACK_STIMULATE only — aggressive attacks disrupt EAP exchanges
WPS / MSCHAPv2 callbacks never fire
- WPS requires a WPS Enrollee actively associating during capture
- MSCHAPv2 only fires against bare (no-tunnel) EAP-MSCHAPv2 — rare in modern deployments
- Verify the callback is registered before
begin()
SD card writes fail
- Confirm
SD.begin() succeeds before any logger call
- Disable
LOG_FILTER_BEACONS if using SPI SD — use SDMMC for high-volume logging
Stress Helpers
beaconFlood(ssids, ssidCount, 6, 5000);
PoliticianStress: Decoupled DoS / Disruption Payload Delivery System.
PoliticianStress.h now exposes beacon flood generation alongside SAE commit flooding and probe flood helpers.
Examples
| Example | Description |
AutonomousHunter | Score-based auto-targeting with fingerprinting |
AutoEnterpriseHunter | Automatic enterprise network targeting |
DeviceFingerprinting | Passive IoT/consumer brand identification |
TargetedAuditing | Network filtering and callbacks |
EnterpriseAuditing | 802.1X identity harvesting |
StorageAndNVS | Streaming PCAPNG logging and NVS persistence |
WigleIntegration | GPS wardriving with Wigle CSV export |
ExportFormats | PCAPNG capture and HC22000 text export |
DynamicControl | Runtime attack mode switching |
FuzzingAndInjection | Custom frame injection and fuzzing |
SerialStreaming | Real-time packet streaming |
StressTest | Performance and memory testing |
BtmSteering | 802.11v BTM Request injection + PMKID combination |
WpsCapture | Passive WPS M1 device fingerprint harvesting |
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 |
Legal & Ethical Use
This library is intended for:
- ✅ Authorized penetration testing
- ✅ Security research in controlled environments
- ✅ Educational purposes with permission
- ✅ Auditing your own networks
Unauthorized access to networks you do not own or have permission to test is illegal under laws such as the Computer Fraud and Abuse Act (CFAA) in the United States and similar legislation worldwide.
The authors and contributors assume no liability for misuse of this software.
Contributing
Contributions are welcome! Fork the repository, create a feature branch, add tests/examples for new features, and submit a pull request.
License
MIT License — see [LICENSE](LICENSE) for details.
Acknowledgments
Special thanks to justcallmekoko for inspiring this project and the broader hardware hacking community through the ESP32 Marauder project.