From 5440aa2d95d61fe4a9cb3cc0b986e58b80b72e86 Mon Sep 17 00:00:00 2001 From: DeFlock Joplin Date: Sat, 20 Jun 2026 14:16:01 -0500 Subject: [PATCH 1/8] update main.cpp for LilyGO T-Dongle S3 support and enhance detection methods - Added configuration for LilyGO T-Dongle S3, including display and APA102 LED support. - Adjusted channel hopping sequence to descending order and reduced dwell time to 250 ms. - Updated detection methods to focus on IE fingerprinting, disabling other methods to reduce false positives. - Enhanced README to reflect changes in detection strategy and camera behavior observations. -Fixed Mario boot sounds. This commit improves the firmware's ability to detect Flock cameras by refining the detection logic and adapting to new hardware configurations. Corrected Mario sounds result in improved UX and satisfaction. --- README.md | 81 ++++++---- display_dongle.cpp | 135 ++++++++++++++++ main.cpp | 392 ++++++++++++++++++++++++++++++++++++++------- 3 files changed, 515 insertions(+), 93 deletions(-) create mode 100644 display_dongle.cpp diff --git a/README.md b/README.md index 288b459..3a22851 100644 --- a/README.md +++ b/README.md @@ -5,22 +5,19 @@ **Passive 2.4 GHz promiscuous-mode detector for Flock Safety surveillance infrastructure. Runs standalone or feeds the Flask dashboard over USB for live GPS-tagged wardriving.** > **Dev note:** This is the `promiscious-dev` branch — adds the -> DeFlockJoplin wildcard-probe tightening and a 31st OUI on top of the +> DeFlockJoplin Information Element research + wildcard probe on top of the > `promiscious` baseline. See "Further research" below. --- ## Credit - -All WiFi promiscuous detection research — the **30-OUI target list**, the **promiscuous-mode strategy**, and the **addr1-receiver detection technique** — is the work of **ØяĐöØцяöЪöяцฐ / @NitekryDPaul**. The firmware here is a mod of his original firmware with added SPIFFS persistence and Flask-dashboard integration. Full research writeup: [`datasets/NitekryDPaul_wifi_ouis.md`](datasets/NitekryDPaul_wifi_ouis.md). - -Additional research credit to **Michael / DeFlockJoplin** for the **wildcard-probe-request signature** and the 31st OUI (`82:6b:f2`). Field-tested to 11/12 cameras caught with only 2 false positives in Joplin. Source: [DeflockJoplin/flock-you](https://github.com/DeflockJoplin/flock-you). +Full credit to @NitekryDPaul for the initial research that sparked my research. His OUI list helps make a strong method rock solid. None of this would be here without his original submission. --- ## What this branch does -Turns a Seeed XIAO ESP32-S3 into a passive WiFi receiver that watches 2.4 GHz management and data frames for Flock Safety MAC OUIs. No AP, no transmit — the radio stays dedicated to sniffing while the device hops channels 1 / 6 / 11 at 350 ms dwell. +Turns a Seeed XIAO ESP32-S3 into a passive WiFi receiver that watches 2.4 GHz management and data frames for Flock Safety MAC OUIs. No AP, no transmit — the radio stays dedicated to sniffing while the device hops channels 11 / 6 / 1 (descending) at 350 ms dwell. Every detection is: @@ -32,38 +29,37 @@ The device works standalone (no USB host needed) and plugged in (live dashboard) --- -## Why promiscuous mode, and why `addr1` +## DeFlock Joplin Research Continues - Behavior and IE Fingerprint -Most WiFi sniffers only check the transmitter address (`addr2`). Flock infrastructure spends most of its duty cycle **asleep** — it wakes briefly in bursts, uploads, then sleeps again. During the silence it may never transmit a single frame in your capture window. +Earlier descriptions of Flock behavior are different than what I have observed. This firmware is based on my observations. I do not make claims that others are incorrect or that this behavior has always existed. -But it may still appear on the air as the **destination** (`addr1`) of probe responses or data frames from nearby APs. +**Past Flock Camera Behavior** +In the past, Flock cameras were detectable by the AP they were broadcasting for management. Some time around December 2025, this AP was deactivated. The community began using other detection methods like BLE. Those stopped working some time in the spring. -Checking `addr1` in addition to `addr2` picks those silent stations up. It requires two guards to avoid false positives: +**Current Camera Behavior** +These observations are not meant to cast doubt on anyone else's. These cameras are not managed well and are updated OTA. -- `addr1` is broadcast (`ff:ff:ff:ff:ff:ff`) in beacons and broadcasts — **multicast filter** -- Modern devices use randomised (locally-administered) MACs that can't be fingerprinted by OUI — **randomised-MAC filter** on byte 0 bit 1 +Observations: +Flock cameras transmit wildcard probe requests on wi-fi channels in ascending order. These probes are emitted at around .125 second intervals. These probes are essentially a WiFi client asking any AP to respond with the SSID of the AP. Hidden SSIDs generally require you send the exact SSID in the probe to generate a response. See [here](https://goodwi.fi/posts/2023/12/hunt-for-hidden-probe/). Flock cameras are also known to use cellular LTE modems to upload to the Flock cloud. -Both are applied before the OUI match. This whole approach, including the 30-OUI list, is **@NitekryDPaul's research**. +Given this, I have disabled all other detection methods in this branch. While they are associated with Flock cameras, they are essentially all echoes of the camera itself, which is only gated by OUI match. The other methods can fire when any OUI matched device is sending wildcard probes because nearby APs can respond and generate a false positive match on addr1 or addr3 methods. ---- +The new method relies on IE fingerprinting research done by others in the past such [here](https://www.researchgate.net/publication/367065691_Analysis_of_Wi-Fi_Probe_Requests_Towards_Information_Element_Fingerprinting). According to the linked paper, IE field detection can already be a high confidence identifier, but we combine it with the OUI list collected by @NitekryDPaul for an extremely certain signature. To the point that I have not experience a false postive after hundreds of miles of driving. Other detection methods have trended towards more active methods, but this is entirely passive. -## Further research — the wildcard-probe signature (DeFlockJoplin) +I have also enabled descending channel hopping and reduced dwell time to 250 ms (2x observed hop time) to assist with faster intercept with a Flock signal. -Michael / DeFlockJoplin used the OUI + addr1/addr2/addr3 work above as a starting point and characterised what Flock cameras actually do on the air. His finding: - -> The cameras are hopping channels and sending out a wildcard WiFi probe request on every channel. This specific type of request combined with OUI matching has created what seems to be a fairly unique signature. - -His drive-test in Joplin caught **11 of 12 cameras** with only **2 false positives**. The 12th camera was doing the same wildcard-probe behaviour but with an OUI (`82:6b:f2`) that wasn't in @NitekryDPaul's original 30 — it's now the 31st entry in our list, credited to him. +Hypothesis on probe behavior: +When the wifi management AP was disabled around December, the devices moved from AP mode to STA mode. The probes likely began at this time. The devices now appear to enumerating networks, perhaps as a default behavior and not something Flock has explicitly set. The tightened signature that's active on this branch: 1. Frame is 802.11 Management, type=0 subtype=4 (**Probe Request**) 2. SSID Information Element (tag 0) is present with **length 0** (wildcard) 3. `addr2` (transmitter) matches the known-OUI list +4. IE fields match signature collected by Michael / DeFlock Joplin -When all three hit, we emit `detection_method: wifi_wildcard_probe` — the high-precision class. Non-probe frames from the same OUIs still emit `wifi_oui_addr2`, and the `addr1` receiver-side sleeper-catch still runs independently. +When we get a hit, we emit `detection_method: wifi_wildcard_probe_ie_sig`. Broad OUI paths (`wifi_wildcard_probe`, `wifi_oui_addr2`, `wifi_oui_addr1`, `wifi_oui_addr3`) are disabled in firmware — IE fingerprint on uplink probe requests is the active detection path. -His proof-of-concept firmware (different enough we're not just pulling it in wholesale, but the core idea carried over cleanly): [DeflockJoplin/flock-you](https://github.com/DeflockJoplin/flock-you). The wildcard-probe analysis is his; we ported the detection into this firmware and kept our SPIFFS persistence, Flask JSON emission, and audio/LED feedback on top. --- @@ -160,10 +156,10 @@ The firmware emits one JSON line per detection in the same schema the BLE detect `detection_method` values: -- `wifi_wildcard_probe` — **Probe Request + wildcard SSID from a known OUI** (the DeFlockJoplin high-precision signature). When this fires, the `addr2` broad alert is suppressed for the same frame to avoid double-counting. -- `wifi_oui_addr2` — transmitter-side OUI match on any non-probe frame -- `wifi_oui_addr1` — **receiver-side OUI match** (the @NitekryDPaul technique) -- `wifi_oui_addr3` — BSSID OUI match (mgmt frames only; disabled by default) +- `wifi_wildcard_probe_ie_sig` — **Probe Request + wildcard SSID + primary Flock IE signature** from a known OUI (PACK method 2 PoC; no rolling-window gates). Supersedes the removed `wifi_wildcard_probe` tier (same wildcard/OUI gates plus IE-field verification). +- `wifi_oui_addr2` — *(disabled in firmware)* transmitter-side OUI match on any frame +- `wifi_oui_addr1` — *(disabled in firmware)* receiver-side OUI match (the @NitekryDPaul technique) +- `wifi_oui_addr3` — *(disabled in firmware)* BSSID OUI on mgmt frames; broad OUI-only filter, false-positive prone - `wifi_ssid` — SSID keyword match (disabled by default) ### GPS wardriving @@ -189,7 +185,7 @@ Open `http://localhost:5000`, pick your serial port from the UI, detections star ## Hardware -**Board:** Seeed Studio XIAO ESP32-S3 +### Seeed XIAO ESP32-S3 (default: `xiao_esp32s3`) | Pin | Function | |-----|----------| @@ -197,7 +193,19 @@ Open `http://localhost:5000`, pick your serial port from the UI, detections star | GPIO 21 | Onboard user LED (active low) | | GPIO 43 | Serial1 TX mirror (115200 baud) | -Boot sound: first 6 notes of Super Mario Bros. World 1-2 (underground). +### LilyGO T-Dongle S3 (`lilygo_t_dongle_s3`). Added because Michael lost his OUI-Spy for testing. + +| Pin | Function | +|-----|----------| +| GPIO 1–5, 38 | ST7735 display (RST, DC, MOSI, CS, SCLK, backlight) | +| GPIO 39 / 40 | APA102 RGB LED (clock / data) — red flash on detection | +| USB CDC | Serial JSON for Flask dashboard | + +**Display:** idle screen shows `SCANNING`, current WiFi channel, and unique hit count. On each emitted detection, shows `DETECT`, method, MAC, RSSI, and channel for **5 s**, then returns to idle. Backlight is driven on init (GPIO 38 active-low). + +No buzzer on this env. + +Boot sound (XIAO only): first 6 notes of Super Mario Bros. World 1-2 (underground). --- @@ -206,12 +214,15 @@ Boot sound: first 6 notes of Super Mario Bros. World 1-2 (underground). Requires [PlatformIO](https://platformio.org/). ```bash -pio run # build -pio run -t upload # flash -pio device monitor # serial output +pio run -e xiao_esp32s3 # Seeed XIAO (default) +pio run -e lilygo_t_dongle_s3 # LilyGO T-Dongle S3 + +pio run -e xiao_esp32s3 -t upload # flash XIAO +pio run -e lilygo_t_dongle_s3 -t upload # flash T-Dongle (hold BOOT if port missing) +pio device monitor ``` -`platformio.ini` and `partitions.csv` are at the root (1.9 MB SPIFFS partition, 6 MB app). No extra libraries needed beyond the Arduino-ESP32 core that ships with the espressif32 platform. +`platformio.ini` and `partitions.csv` are at the root (1.9 MB SPIFFS partition, 6 MB app). The T-Dongle env adds **TFT_eSPI** for the onboard display; XIAO needs no extra libraries. --- @@ -219,12 +230,12 @@ pio device monitor # serial output | Define | Default | Notes | |---|---|---| -| `CHANNEL_MODE` | `CHANNEL_MODE_CUSTOM` | `CUSTOM` (1/6/11), `FULL_HOP` (1-11), or `SINGLE` | +| `CHANNEL_MODE` | `CHANNEL_MODE_CUSTOM` | `CUSTOM` (11/6/1 desc), `FULL_HOP` (11-1 desc), or `SINGLE` | | `CHANNEL_DWELL_MS` | 350 | Time on each channel before hop | | `RSSI_MIN` | -95 | Drop frames weaker than this | | `ALERT_COOLDOWN_MS` | 5000 | Per-MAC serial-emit rate limit | -| `CHECK_ADDR1` | 1 | The @NitekryDPaul receiver-side technique | -| `CHECK_ADDR3` | 0 | BSSID fallback (mgmt frames only) | +| `CHECK_ADDR1` | 0 | Receiver-side OUI (disabled — see main.cpp) | +| `CHECK_ADDR3` | 0 | BSSID OUI fallback (disabled — see main.cpp) | | `ENABLE_SSID_MATCH` | 0 | Substring match against `target_ssid_keywords[]` | | `PROCESS_MGMT_FRAMES` | 1 | Beacons, probe req/resp, etc. | | `PROCESS_DATA_FRAMES` | 1 | Data frames (where addr1 catch shines) | diff --git a/display_dongle.cpp b/display_dongle.cpp new file mode 100644 index 0000000..2fd66f9 --- /dev/null +++ b/display_dongle.cpp @@ -0,0 +1,135 @@ +#ifdef USE_T_DONGLE_DISPLAY + +#include "display_dongle.h" +#include +#include +#include + +// LilyGO T-Dongle S3 ST7735 80x160 IPS — backlight active-low on GPIO38. +#define DONGLE_TFT_BL_PIN 38 +#define DONGLE_TFT_RST_PIN 1 + +// 160x80 landscape — matches LilyGO examples/TFT_eSPI/TFT_eSPI.ino +#define DONGLE_TFT_ROTATION 1 + +static TFT_eSPI tft = TFT_eSPI(); +static unsigned long alertUntilMs = 0; +static uint8_t idleCh = 1; +static int idleDetCount = 0; +static bool inAlert = false; +static uint8_t lastDrawnCh = 0xFF; +static int lastDrawnHits = -1; + +static void backlightOn() { + pinMode(DONGLE_TFT_BL_PIN, OUTPUT); + digitalWrite(DONGLE_TFT_BL_PIN, LOW); +} + +static void tftHardwareReset() { + pinMode(DONGLE_TFT_RST_PIN, OUTPUT); + digitalWrite(DONGLE_TFT_RST_PIN, HIGH); + delay(10); + digitalWrite(DONGLE_TFT_RST_PIN, LOW); + delay(10); + digitalWrite(DONGLE_TFT_RST_PIN, HIGH); + delay(120); +} + +static void drawMethodLines(const char* method, int y) { + tft.setTextColor(TFT_WHITE, TFT_BLACK); + tft.setTextSize(1); + char line[32]; + const char* p = method; + while (*p) { + size_t n = 0; + const char* seg = p; + while (p[n] && p[n] != '_' && n < 16) n++; + if (p[n] == '_') n++; + size_t copy = n; + if (copy >= sizeof(line)) copy = sizeof(line) - 1; + memcpy(line, seg, copy); + line[copy] = '\0'; + tft.setCursor(2, y); + tft.print(line); + y += 10; + p += n; + if (y > 58) break; + } +} + +void dongleDisplayInit() { + tftHardwareReset(); + tft.init(); + tft.setRotation(DONGLE_TFT_ROTATION); + tft.fillScreen(TFT_BLACK); + backlightOn(); + dongleDisplayShowIdle(1, 0); +} + +void dongleDisplayShowIdle(uint8_t ch, int detCount) { + idleCh = ch; + idleDetCount = detCount; + inAlert = false; + alertUntilMs = 0; + + tft.fillScreen(TFT_BLACK); + tft.setTextColor(TFT_GREEN, TFT_BLACK); + tft.setTextSize(2); + tft.setCursor(4, 4); + tft.print("SCAN"); + tft.setCursor(4, 24); + tft.print("NING"); + + tft.setTextColor(TFT_WHITE, TFT_BLACK); + tft.setTextSize(1); + tft.setCursor(2, 50); + tft.printf("Ch: %u", (unsigned)ch); + tft.setCursor(2, 64); + tft.printf("Hits: %d", detCount); + + lastDrawnCh = ch; + lastDrawnHits = detCount; +} + +void dongleDisplayShowAlert(const char* method, const char* mac, int8_t rssi, + uint8_t ch, unsigned long alertMs) { + idleCh = ch; + inAlert = true; + if (alertMs == 0) alertMs = 1; + alertUntilMs = millis() + alertMs; + + tft.fillScreen(TFT_BLACK); + tft.setTextColor(TFT_RED, TFT_BLACK); + tft.setTextSize(2); + tft.setCursor(4, 2); + tft.print("DETECT"); + + drawMethodLines(method ? method : "?", 22); + + tft.setTextColor(TFT_WHITE, TFT_BLACK); + tft.setTextSize(1); + tft.setCursor(2, 44); + tft.print(mac ? mac : ""); + tft.setCursor(2, 56); + tft.printf("RSSI %d", (int)rssi); + tft.setCursor(2, 68); + tft.printf("CH %u", (unsigned)ch); +} + +bool dongleDisplayInAlert(unsigned long now) { + return inAlert && alertUntilMs != 0 && (long)(now - alertUntilMs) < 0; +} + +void dongleDisplayTick(unsigned long now, uint8_t ch, int detCount) { + idleCh = ch; + idleDetCount = detCount; + if (inAlert && alertUntilMs != 0 && (long)(now - alertUntilMs) >= 0) { + dongleDisplayShowIdle(idleCh, idleDetCount); + return; + } + if (!inAlert && (ch != lastDrawnCh || detCount != lastDrawnHits)) { + dongleDisplayShowIdle(ch, detCount); + } +} + +#endif diff --git a/main.cpp b/main.cpp index 4621777..e908c7a 100644 --- a/main.cpp +++ b/main.cpp @@ -4,37 +4,49 @@ #include #include #include +#include "display_dongle.h" // ============================================================ -// CONFIG +// CONFIG (board defaults; override via platformio build_flags) // ============================================================ -#define BUZZER_PIN 3 -#define USE_BUZZER 1 +#ifdef BOARD_LILYGO_T_DONGLE_S3 +// LilyGO T-Dongle S3: ST7735 display + APA102 RGB (no buzzer). +#define USE_BUZZER 0 +#define USE_LED 1 +#define USE_APA102_LED 1 +#define APA102_DATA_PIN 40 +#define APA102_CLK_PIN 39 +#define APA102_FLASH_R 255 +#define APA102_FLASH_G 0 +#define APA102_FLASH_B 0 +#define MIRROR_SERIAL 0 // GPIO43 is UART TX on this board +#else +// Seeed XIAO ESP32-S3 +#define BUZZER_PIN 3 +#define USE_BUZZER 1 +#define LED_PIN 21 +#define USE_LED 1 +#define LED_ACTIVE_HIGH 0 +#define MIRROR_SERIAL 1 +#define MIRROR_TX_PIN 43 +#endif -// Onboard user LED on Seeed XIAO ESP32-S3 is GPIO21 and is ACTIVE LOW -// (driving the pin LOW lights the LED). -#define LED_PIN 21 -#define USE_LED 1 -#define LED_ACTIVE_HIGH 0 -#define LED_FLASH_MS 120 - -#define MIRROR_SERIAL 1 -#define MIRROR_TX_PIN 43 -#define MIRROR_BAUD 115200 +#define LED_FLASH_MS 120 +#define MIRROR_BAUD 115200 #define CHANNEL_MODE_FULL_HOP 0 #define CHANNEL_MODE_CUSTOM 1 #define CHANNEL_MODE_SINGLE 2 #define CHANNEL_MODE CHANNEL_MODE_CUSTOM -#define CHANNEL_DWELL_MS 350 +#define CHANNEL_DWELL_MS 250 #define SINGLE_CHANNEL 1 -static const uint8_t customChannels[] = {1, 6, 11}; +static const uint8_t customChannels[] = {11, 6, 1}; static const size_t customChannelCount = sizeof(customChannels) / sizeof(customChannels[0]); -static const uint8_t fullHopChannels[] = {1,2,3,4,5,6,7,8,9,10,11}; +static const uint8_t fullHopChannels[] = {11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}; static const size_t fullHopChannelCount = sizeof(fullHopChannels) / sizeof(fullHopChannels[0]); #define HEARTBEAT_MS 30000 @@ -60,8 +72,8 @@ static const size_t fullHopChannelCount = sizeof(fullHopChannels) / sizeof(full #define HB_BEEP_GAP_MS 70 #define ENABLE_SSID_MATCH 0 -#define CHECK_ADDR1 1 // dst/rx — catches Flock STAs receiving probe responses -#define CHECK_ADDR3 0 // bssid fallback for randomised addr2 +#define CHECK_ADDR1 0 // disabled — see wifiSniffer() comment block +#define CHECK_ADDR3 0 // disabled — see wifiSniffer() comment block static const char* target_ssid_keywords[] = { "flock" }; static const size_t SSID_KEYWORD_COUNT = sizeof(target_ssid_keywords) / sizeof(target_ssid_keywords[0]); @@ -87,11 +99,8 @@ static const char* target_ouis[] = { "94:08:53", "e4:aa:ea", "f4:6a:dd", "f8:a2:d6", "24:b2:b9", "00:f4:8d", "d0:39:57", "e8:d0:fc", "e0:4f:43", "b8:1e:a4", "70:08:94", "58:8e:81", "ec:1b:bd", "3c:71:bf", "58:00:e3", - "90:35:ea", "5c:93:a2", "64:6e:69", "48:27:ea", "a4:cf:12", - // Contributed by Michael / DeFlockJoplin — discovered via wildcard-probe - // + OUI signature during field testing. The 12th camera in his drive-test - // used this prefix and wasn't in @NitekryDPaul's original 30. - "82:6b:f2" + "90:35:ea", "5c:93:a2", "64:6e:69", "48:27:ea", "a4:cf:12" + }; static const size_t OUI_COUNT = sizeof(target_ouis) / sizeof(target_ouis[0]); @@ -111,10 +120,10 @@ typedef enum : uint8_t { ALERT_OUI_ADDR1 = 1, ALERT_OUI_ADDR3 = 2, ALERT_SSID = 3, - // Probe Request + wildcard SSID (tag 0, length 0) from a known-OUI addr2. - // Tight signature from Michael / DeFlockJoplin field research: - // https://github.com/DeflockJoplin/flock-you - ALERT_WILDCARD_PROBE = 4, + // Wildcard probe + OUI + primary IE signature (wifi_wildcard_probe_ie_sig). + // wifi_wildcard_probe (OUI + wildcard only) was removed — superseded by this + // path: same wildcard/OUI gates plus IE-field verification. + ALERT_WILDCARD_PROBE_IE_SIG = 4, } AlertType; typedef struct { @@ -166,7 +175,7 @@ static void IRAM_ATTR enqueueAlert(AlertType type, const uint8_t* mac, int8_t rs typedef struct { char mac[18]; - char method[16]; // "oui_addr2" / "oui_addr1" / "oui_addr3" / "ssid" + char method[24]; // alertTypeToMethod strings (incl. wildcard_probe_ie_sig) int8_t rssi; uint8_t channel; uint32_t firstSeen; // millis() at first hit @@ -207,6 +216,33 @@ static size_t dedupeIdx = 0; // LED one-shot pulse timer static volatile unsigned long ledOffAt = 0; +#if USE_LED && defined(USE_APA102_LED) +static void apa102WriteByte(uint8_t b) { + for (int bit = 7; bit >= 0; bit--) { + digitalWrite(APA102_DATA_PIN, (b >> bit) & 1); + digitalWrite(APA102_CLK_PIN, HIGH); + digitalWrite(APA102_CLK_PIN, LOW); + } +} + +static void apa102SetColor(uint8_t r, uint8_t g, uint8_t b) { + for (int i = 0; i < 4; i++) apa102WriteByte(0x00); + apa102WriteByte(0xFF); // global brightness + apa102WriteByte(b); + apa102WriteByte(g); + apa102WriteByte(r); + for (int i = 0; i < 4; i++) apa102WriteByte(0xFF); +} + +static void apa102Init() { + pinMode(APA102_DATA_PIN, OUTPUT); + pinMode(APA102_CLK_PIN, OUTPUT); + digitalWrite(APA102_CLK_PIN, LOW); + digitalWrite(APA102_DATA_PIN, LOW); + apa102SetColor(0, 0, 0); +} +#endif + // Heartbeat audio state: last time any target was seen, last time the // heartbeat beep-pair was played. When nothing has been seen for // HB_DEVICE_ACTIVE_MS the heartbeat stops until the next new detection. @@ -256,12 +292,17 @@ static void dualPrintln(const char* str) { static inline void ledSet(bool on) { #if USE_LED +#if defined(USE_APA102_LED) + if (on) apa102SetColor(APA102_FLASH_R, APA102_FLASH_G, APA102_FLASH_B); + else apa102SetColor(0, 0, 0); +#else #if LED_ACTIVE_HIGH digitalWrite(LED_PIN, on ? HIGH : LOW); #else digitalWrite(LED_PIN, on ? LOW : HIGH); #endif #endif +#endif } static void ledFlash(unsigned ms) { @@ -308,8 +349,9 @@ static void heartbeatBeep() { static void startupBeep() { #if USE_BUZZER // First 6 notes of SMB World 1-2 (underground). Koji Kondo's descending - // pattern: C5 → C4 → A4 → A3 → G#4 → G#3 (alternating-octave pairs). - static const uint16_t notes[6] = { 523, 262, 440, 220, 415, 208 }; + // pattern: C4, C5, A3, A4, B♭3, B♭4). (alternating-octave pairs). + static const uint16_t notes[6] = { 262, 523, 220, 440, 233, 466 }; + for (int i = 0; i < 6; i++) { tone(BUZZER_PIN, notes[i]); delay((i == 5) ? 160 : 95); @@ -445,6 +487,9 @@ static void printHeartbeat() { dualPrintf("[flockyou] scanning (ch=%u mode=%s det=%d)\n", currentChannel, channelModeName(), fyDetCount); lastHeartbeat = millis(); + if (!dongleDisplayInAlert(millis())) { + dongleDisplayShowIdle(currentChannel, fyDetCount); + } } } @@ -457,9 +502,9 @@ static const char* alertTypeToMethod(AlertType t) { case ALERT_OUI_ADDR2: return "oui_addr2"; case ALERT_OUI_ADDR1: return "oui_addr1"; case ALERT_OUI_ADDR3: return "oui_addr3"; - case ALERT_SSID: return "ssid"; - case ALERT_WILDCARD_PROBE: return "wildcard_probe"; - default: return "unknown"; + case ALERT_SSID: return "ssid"; + case ALERT_WILDCARD_PROBE_IE_SIG: return "wildcard_probe_ie_sig"; + default: return "unknown"; } } @@ -824,6 +869,206 @@ static int IRAM_ATTR isWildcardProbeIE(const uint8_t* body, int len) { return -1; } +// --- PACK method 2 PoC: Flock probe IE signature (primary allowlist only) --- + +static const char FLOCK_PROBE_IE_SIG_PRIMARY[] = + "2,12,127,221:506f9a16030103,45,191,221:0050f208000000"; +static const char FLOCK_LITEON_IE_SIG_PREFIX[] = "221:506f9a16030103"; + +#define FY_IE_SSID 0 +#define FY_IE_VENDOR 221 +#define FY_PHANTOM_SKIP_CAP 16 +#define FY_TLV_RESYNC_MAX 64 + +static void IRAM_ATTR fyHexNibbles(char* dst, const uint8_t* b, int n) { + static const char hd[] = "0123456789abcdef"; + for (int i = 0; i < n; i++) { + dst[i * 2] = hd[b[i] >> 4]; + dst[i * 2 + 1] = hd[b[i] & 0x0f]; + } +} + +static bool IRAM_ATTR fyLiteonVendorAt(const uint8_t* ies, int len, int pos) { + return pos + 9 <= len && ies[pos] == FY_IE_VENDOR && ies[pos + 1] == 7 + && ies[pos + 2] == 0x50 && ies[pos + 3] == 0x6f && ies[pos + 4] == 0x9a; +} + +static bool IRAM_ATTR fyPhantomLiteonAhead(const uint8_t* ies, int len, int pos) { + int end = pos + 2 + 32; + if (end > len - 1) end = len - 1; + for (int j = pos + 2; j < end; j++) { + if (fyLiteonVendorAt(ies, len, j)) return true; + } + return false; +} + +static bool IRAM_ATTR fyIsPhantomOverflow(const uint8_t* ies, int len, + uint8_t id, int elen, int i) { + if (i + 2 + elen <= len) return false; + if (elen > 200) return true; + return id == 64 && elen == 128 && fyPhantomLiteonAhead(ies, len, i); +} + +static int IRAM_ATTR fyTlvResync(const uint8_t* ies, int len, int start) { + int end = start + FY_TLV_RESYNC_MAX; + if (end > len - 1) end = len - 1; + for (int j = start; j < end; j++) { + int elen = (int)ies[j + 1]; + if (elen <= 200 && j + 2 + elen <= len) return j; + } + return -1; +} + +static bool IRAM_ATTR fySigAppend(char* out, size_t cap, size_t* pos, const char* part) { + size_t plen = strlen(part); + if (*pos != 0) { + if (*pos + 1 >= cap) return false; + out[(*pos)++] = ','; + } + if (*pos + plen >= cap) return false; + memcpy(out + *pos, part, plen); + *pos += plen; + out[*pos] = '\0'; + return true; +} + +static bool IRAM_ATTR fySigAppendTag(char* out, size_t cap, size_t* pos, uint8_t id) { + char buf[8]; + snprintf(buf, sizeof(buf), "%u", (unsigned)id); + return fySigAppend(out, cap, pos, buf); +} + +static bool IRAM_ATTR fySigAppendVendor(char* out, size_t cap, size_t* pos, + const uint8_t* body, int elen) { + char buf[24]; + int take = elen < 8 ? elen : 8; + buf[0] = '2'; buf[1] = '2'; buf[2] = '1'; buf[3] = ':'; + fyHexNibbles(buf + 4, body, take); + buf[4 + take * 2] = '\0'; + return fySigAppend(out, cap, pos, buf); +} + +// Returns false if parsing fails irrecoverably. +static bool IRAM_ATTR fyBuildFlockIeSigFromIes(const uint8_t* ies, int len, + char* out, size_t cap, bool* complete) { + if (!ies || len < 2 || !out || cap < 2) return false; + size_t pos = 0; + out[0] = '\0'; + int i = 0; + uint8_t phantomSkips = 0; + while (i + 2 <= len) { + uint8_t id = ies[i]; + int elen = (int)ies[i + 1]; + if (i + 2 + elen > len) { + if (phantomSkips < FY_PHANTOM_SKIP_CAP + && fyIsPhantomOverflow(ies, len, id, elen, i)) { + phantomSkips++; + i += 2; + continue; + } + int j = fyTlvResync(ies, len, i); + if (j > i) { + i = j; + continue; + } + return false; + } + i += 2; + if (id == FY_IE_SSID) { + if (elen == 0) { + while (i + 2 <= len && ies[i] == 0 && ies[i + 1] == 0) i += 2; + } else { + i += elen; + } + continue; + } + if (id == FY_IE_VENDOR && elen >= 4) { + if (!fySigAppendVendor(out, cap, &pos, ies + i, elen)) return false; + } else { + if (!fySigAppendTag(out, cap, &pos, id)) return false; + } + i += elen; + } + if (complete) *complete = (i == len); + return pos > 0; +} + +static void IRAM_ATTR fyCanonicalizeFlockIeSig(char* sig, size_t cap) { + if (!sig || cap < 8) return; + if (strncmp(sig, "2,12,127,", 9) == 0 + && strstr(sig, FLOCK_LITEON_IE_SIG_PREFIX) != nullptr) { + return; + } + const char* anchor = strstr(sig, FLOCK_LITEON_IE_SIG_PREFIX); + if (!anchor) return; + char tmp[128]; + int n = snprintf(tmp, sizeof(tmp), "2,12,127,%s", anchor); + if (n > 0 && (size_t)n < cap) memcpy(sig, tmp, (size_t)n + 1); +} + +static bool IRAM_ATTR fyPickBetterSig(const char* a, bool aComplete, + const char* b, bool bComplete, + char* out, size_t cap) { + if (!a[0] && !b[0]) return false; + if (a[0] && !b[0]) { + strncpy(out, a, cap - 1); + out[cap - 1] = '\0'; + return true; + } + if (!a[0] && b[0]) { + strncpy(out, b, cap - 1); + out[cap - 1] = '\0'; + return true; + } + const char* pick = a; + if (aComplete && !bComplete) pick = a; + else if (!aComplete && bComplete) pick = b; + else if (strlen(b) > strlen(a)) pick = b; + strncpy(out, pick, cap - 1); + out[cap - 1] = '\0'; + return true; +} + +static bool IRAM_ATTR fyBuildFlockIeSigFromProbeBody(const uint8_t* body, int bodyLen, + char* out, size_t cap) { + if (!body || bodyLen < 2 || !out || cap < 16) return false; + char sigA[128] = {0}; + char sigB[128] = {0}; + bool completeA = false, completeB = false; + bool okA = fyBuildFlockIeSigFromIes(body, bodyLen, sigA, sizeof(sigA), &completeA); + bool okB = false; + if (bodyLen >= 2 && body[0] == 0 && body[1] == 0) { + okB = fyBuildFlockIeSigFromIes(body + 2, bodyLen - 2, sigB, sizeof(sigB), &completeB); + } + char merged[128] = {0}; + if (!fyPickBetterSig(okA ? sigA : "", completeA, okB ? sigB : "", completeB, + merged, sizeof(merged))) { + return false; + } + fyCanonicalizeFlockIeSig(merged, sizeof(merged)); + strncpy(out, merged, cap - 1); + out[cap - 1] = '\0'; + return out[0] != '\0'; +} + +static bool IRAM_ATTR fyFlockIeSigIsPrimary(const char* sig) { + return sig && strcmp(sig, FLOCK_PROBE_IE_SIG_PRIMARY) == 0; +} + +static bool IRAM_ATTR fyProbeBodyFlockIeSigPrimary(const uint8_t* body, int bodyLen) { + char ieSig[128]; + int len = bodyLen; + if (fyBuildFlockIeSigFromProbeBody(body, len, ieSig, sizeof(ieSig)) + && fyFlockIeSigIsPrimary(ieSig)) { + return true; + } + if (len > 4 && fyBuildFlockIeSigFromProbeBody(body, len - 4, ieSig, sizeof(ieSig)) + && fyFlockIeSigIsPrimary(ieSig)) { + return true; + } + return false; +} + static void IRAM_ATTR wifiSniffer(void* buf, wifi_promiscuous_pkt_type_t type) { if (!buf || sniffingStopped) return; @@ -848,15 +1093,13 @@ static void IRAM_ATTR wifiSniffer(void* buf, wifi_promiscuous_pkt_type_t type) { // --- OUI check: addr2 (transmitter/source) --- // - // For mgmt Probe Requests (type=0 subtype=4) from a matched OUI, tighten - // to the DeFlockJoplin wildcard-probe signature: SSID IE (tag 0) length - // must be zero. This reduces false positives dramatically (Michael's field - // test: 11/12 true-positive with only 2 false-positives in Joplin). + // Probe requests (type=0 subtype=4) from a matched OUI: wildcard SSID IE + // (tag 0, length 0) plus primary Flock IE signature → wifi_wildcard_probe_ie_sig. // - // Non-probe frames from the same OUI still emit the broad ADDR2 alert. - // See: https://github.com/DeflockJoplin/flock-you + // wifi_wildcard_probe (OUI + wildcard only, no IE check) was removed here. + // Suggest superseded by the IE fingerprint path: it uses the same wildcard + // and OUI gates and adds verification on probe IE fields. if (matchOuiRaw(hdr->addr2)) { - bool emitted = false; if (type == WIFI_PKT_MGMT) { uint8_t fc0 = hdr->frame_ctrl & 0xFF; uint8_t ftype = (fc0 >> 2) & 0x03; @@ -870,34 +1113,50 @@ static void IRAM_ATTR wifiSniffer(void* buf, wifi_promiscuous_pkt_type_t type) { // ALL (-1). A found-but-nonzero (0) means legit directed probe; do // not retry — it would mis-classify. if (r == -1 && bodyLen > 4) r = isWildcardProbeIE(body, bodyLen - 4); - if (r == 1) { - enqueueAlert(ALERT_WILDCARD_PROBE, hdr->addr2, rssi, ch, + if (r == 1 && fyProbeBodyFlockIeSigPrimary(body, bodyLen)) { + enqueueAlert(ALERT_WILDCARD_PROBE_IE_SIG, hdr->addr2, rssi, ch, nullptr, "probe_req"); - emitted = true; } } } - if (!emitted) { - enqueueAlert(ALERT_OUI_ADDR2, hdr->addr2, rssi, ch, nullptr, "addr2"); - } + // wifi_oui_addr2 — broad transmitter OUI on any non-fingerprint frame: + // if (!emitted) { + // enqueueAlert(ALERT_OUI_ADDR2, hdr->addr2, rssi, ch, nullptr, "addr2"); + // } } + // --- Disabled: wifi_oui_addr1 (receiver / addr1) --- + // + // Suggest leaving this disabled. Flock cameras are known to channel-hop and + // send wildcard probe requests (addr2 = camera). Nearby APs that hear those + // probes reply with probe responses where addr1 = camera MAC and addr2 = AP. + // This path is the same OUI list again, but matching addr1 (destination) — + // i.e. "is anyone sending *to* a Flock OUI?" — not the camera transmitting. + // + // 802.11 MAC header roles (infrastructure / mgmt): + // addr1 = receiver (DA) addr2 = transmitter (SA) addr3 = BSSID + // On a camera probe request: addr2=camera, addr1 often broadcast. + // On an AP probe response: addr1=camera, addr2=AP, addr3=AP BSSID. + // + // addr1 hits are therefore mostly second-hand fallout from the same probe + // behavior (AP replies), redundant with wildcard+IE detection on the uplink + // probe request itself. #if CHECK_ADDR1 - // addr1 (receiver/destination): catches Flock STAs that appear only as the - // dst of probe responses and data frames — never transmitting in the capture - // window due to their burst-sleep duty cycle. Multicast guard is mandatory - // here since addr1 is broadcast (ff:ff:ff:ff:ff:ff) in beacons/broadcasts. - if (!isMulticast(hdr->addr1) && matchOuiRaw(hdr->addr1)) { - enqueueAlert(ALERT_OUI_ADDR1, hdr->addr1, rssi, ch, nullptr, "addr1"); - } + // if (!isMulticast(hdr->addr1) && matchOuiRaw(hdr->addr1)) { + // enqueueAlert(ALERT_OUI_ADDR1, hdr->addr1, rssi, ch, nullptr, "addr1"); + // } #endif + // --- Disabled: wifi_oui_addr3 (BSSID / addr3) --- + // + // Suggest leaving this disabled. Another broad OUI filter on addr3 (BSSID) + // on management frames — intended for randomised addr2 with real OUI in + // addr3, but still OUI-only with no probe/IE behavioral check, so it can + // generate false positives on unrelated mgmt traffic. #if CHECK_ADDR3 - // addr3 fallback: catches cases where addr2 is randomised but addr3 - // carries the real BSSID OUI (management frames only). - if (type == WIFI_PKT_MGMT && matchOuiRaw(hdr->addr3)) { - enqueueAlert(ALERT_OUI_ADDR3, hdr->addr3, rssi, ch, nullptr, "addr3"); - } + // if (type == WIFI_PKT_MGMT && matchOuiRaw(hdr->addr3)) { + // enqueueAlert(ALERT_OUI_ADDR3, hdr->addr3, rssi, ch, nullptr, "addr3"); + // } #endif #if ENABLE_SSID_MATCH @@ -1008,6 +1267,10 @@ static void drainAlertQueue() { } ledFlash(LED_FLASH_MS); + char methodLine[40]; + snprintf(methodLine, sizeof(methodLine), "wifi_%s", method); + dongleDisplayShowAlert(methodLine, macStr, e.rssi, e.channel, ALERT_COOLDOWN_MS); + #if STOP_ON_OUI_HIT if (e.type != ALERT_SSID) stopSniffing("OUI hit"); #endif @@ -1049,6 +1312,10 @@ void setup() { Serial.setTxTimeoutMs(0); delay(300); +#ifdef BOARD_LILYGO_T_DONGLE_S3 + dongleDisplayInit(); +#endif + #if MIRROR_SERIAL Serial1.begin(MIRROR_BAUD, SERIAL_8N1, -1, MIRROR_TX_PIN); // TX-only on GPIO43 #endif @@ -1059,8 +1326,12 @@ void setup() { #endif #if USE_LED +#if defined(USE_APA102_LED) + apa102Init(); +#else pinMode(LED_PIN, OUTPUT); ledSet(false); +#endif #endif startupBeep(); @@ -1109,6 +1380,10 @@ void setup() { lastHeartbeat = millis(); fyLastSaveAt = millis(); + +#ifdef BOARD_LILYGO_T_DONGLE_S3 + dongleDisplayShowIdle(currentChannel, fyDetCount); +#endif } void loop() { @@ -1117,6 +1392,7 @@ void loop() { autosaveTick(); // periodic SPIFFS write if dirty heartbeatTick(); // audible beep-pair while a target is still in range ledTick(); // turn off LED after LED_FLASH_MS + dongleDisplayTick(millis(), currentChannel, fyDetCount); printHeartbeat(); delay(1); } From 69696497d6c6204ee23f627439bb0ac2fefe2e8e Mon Sep 17 00:00:00 2001 From: DeFlock Joplin Date: Sat, 20 Jun 2026 14:39:15 -0500 Subject: [PATCH 2/8] add comments to fingerprint section --- main.cpp | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/main.cpp b/main.cpp index e908c7a..1f72520 100644 --- a/main.cpp +++ b/main.cpp @@ -880,6 +880,7 @@ static const char FLOCK_LITEON_IE_SIG_PREFIX[] = "221:506f9a16030103"; #define FY_PHANTOM_SKIP_CAP 16 #define FY_TLV_RESYNC_MAX 64 +// Encode n raw bytes as lowercase hex pairs (no separator) for vendor IE tokens. static void IRAM_ATTR fyHexNibbles(char* dst, const uint8_t* b, int n) { static const char hd[] = "0123456789abcdef"; for (int i = 0; i < n; i++) { @@ -887,12 +888,14 @@ static void IRAM_ATTR fyHexNibbles(char* dst, const uint8_t* b, int n) { dst[i * 2 + 1] = hd[b[i] & 0x0f]; } } - +// True when ies[pos] starts vendor IE 221 with OUI 50:6f:9a (LiteON / Flock stack). +// Used to spot real IE boundaries inside corrupted/overflow TLV runs. static bool IRAM_ATTR fyLiteonVendorAt(const uint8_t* ies, int len, int pos) { return pos + 9 <= len && ies[pos] == FY_IE_VENDOR && ies[pos + 1] == 7 && ies[pos + 2] == 0x50 && ies[pos + 3] == 0x6f && ies[pos + 4] == 0x9a; } - +// Scan up to 32 bytes past a bogus TLV header for a real LiteON vendor IE — +// signals a phantom overflow (driver length/FCS skew) rather than end of frame. static bool IRAM_ATTR fyPhantomLiteonAhead(const uint8_t* ies, int len, int pos) { int end = pos + 2 + 32; if (end > len - 1) end = len - 1; @@ -901,14 +904,16 @@ static bool IRAM_ATTR fyPhantomLiteonAhead(const uint8_t* ies, int len, int pos) } return false; } - +// True when declared IE length extends past the buffer but looks like a phantom +// tag-64/len-128 overflow with LiteON payload still present ahead in the buffer. static bool IRAM_ATTR fyIsPhantomOverflow(const uint8_t* ies, int len, uint8_t id, int elen, int i) { if (i + 2 + elen <= len) return false; if (elen > 200) return true; return id == 64 && elen == 128 && fyPhantomLiteonAhead(ies, len, i); } - +// After a TLV parse failure, slide forward up to FY_TLV_RESYNC_MAX bytes to find +// the next plausible IE header (id + len that fits in the buffer). static int IRAM_ATTR fyTlvResync(const uint8_t* ies, int len, int start) { int end = start + FY_TLV_RESYNC_MAX; if (end > len - 1) end = len - 1; @@ -918,7 +923,7 @@ static int IRAM_ATTR fyTlvResync(const uint8_t* ies, int len, int start) { } return -1; } - +// Append a comma-separated fragment to the growing IE signature string; fails if cap exceeded. static bool IRAM_ATTR fySigAppend(char* out, size_t cap, size_t* pos, const char* part) { size_t plen = strlen(part); if (*pos != 0) { @@ -931,13 +936,13 @@ static bool IRAM_ATTR fySigAppend(char* out, size_t cap, size_t* pos, const char out[*pos] = '\0'; return true; } - +// Append a non-vendor IE as its decimal tag id (e.g. "12", "127", "45"). static bool IRAM_ATTR fySigAppendTag(char* out, size_t cap, size_t* pos, uint8_t id) { char buf[8]; snprintf(buf, sizeof(buf), "%u", (unsigned)id); return fySigAppend(out, cap, pos, buf); } - +// Append vendor IE as "221:" + up to 8 payload bytes hex (matches PACK sig format). static bool IRAM_ATTR fySigAppendVendor(char* out, size_t cap, size_t* pos, const uint8_t* body, int elen) { char buf[24]; @@ -948,7 +953,9 @@ static bool IRAM_ATTR fySigAppendVendor(char* out, size_t cap, size_t* pos, return fySigAppend(out, cap, pos, buf); } -// Returns false if parsing fails irrecoverably. +// Walk 802.11 IE TLVs and build comma-separated fingerprint: skip SSID (tag 0), +// encode vendor 221 payloads, otherwise record tag numbers. Handles phantom +// overflows and resync. Sets *complete when every byte was consumed. static bool IRAM_ATTR fyBuildFlockIeSigFromIes(const uint8_t* ies, int len, char* out, size_t cap, bool* complete) { if (!ies || len < 2 || !out || cap < 2) return false; @@ -992,7 +999,8 @@ static bool IRAM_ATTR fyBuildFlockIeSigFromIes(const uint8_t* ies, int len, if (complete) *complete = (i == len); return pos > 0; } - +// Normalize signature to "2,12,127," when the LiteON +// vendor prefix is present but leading tags were truncated by parse skew. static void IRAM_ATTR fyCanonicalizeFlockIeSig(char* sig, size_t cap) { if (!sig || cap < 8) return; if (strncmp(sig, "2,12,127,", 9) == 0 @@ -1005,7 +1013,8 @@ static void IRAM_ATTR fyCanonicalizeFlockIeSig(char* sig, size_t cap) { int n = snprintf(tmp, sizeof(tmp), "2,12,127,%s", anchor); if (n > 0 && (size_t)n < cap) memcpy(sig, tmp, (size_t)n + 1); } - +// Normalize signature to "2,12,127," when the LiteON +// vendor prefix is present but leading tags were truncated by parse skew. static bool IRAM_ATTR fyPickBetterSig(const char* a, bool aComplete, const char* b, bool bComplete, char* out, size_t cap) { @@ -1028,7 +1037,8 @@ static bool IRAM_ATTR fyPickBetterSig(const char* a, bool aComplete, out[cap - 1] = '\0'; return true; } - +// Build fingerprint from full body and from body+2 (skip leading empty SSID IE pair); +// merge, canonicalize, write to out. static bool IRAM_ATTR fyBuildFlockIeSigFromProbeBody(const uint8_t* body, int bodyLen, char* out, size_t cap) { if (!body || bodyLen < 2 || !out || cap < 16) return false; @@ -1050,7 +1060,7 @@ static bool IRAM_ATTR fyBuildFlockIeSigFromProbeBody(const uint8_t* body, int bo out[cap - 1] = '\0'; return out[0] != '\0'; } - +// True when sig exactly matches FLOCK_PROBE_IE_SIG_PRIMARY (drive-tested allowlist entry). static bool IRAM_ATTR fyFlockIeSigIsPrimary(const char* sig) { return sig && strcmp(sig, FLOCK_PROBE_IE_SIG_PRIMARY) == 0; } From 2e694abf7f95be0ea3ba0cb5b334bb5c37818919 Mon Sep 17 00:00:00 2001 From: DeFlock Joplin Date: Sat, 20 Jun 2026 19:03:20 -0500 Subject: [PATCH 3/8] Add credit for nsm_barii's original observations. --- README.md | 2 +- main.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3a22851..ddf470f 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ In the past, Flock cameras were detectable by the AP they were broadcasting for These observations are not meant to cast doubt on anyone else's. These cameras are not managed well and are updated OTA. Observations: -Flock cameras transmit wildcard probe requests on wi-fi channels in ascending order. These probes are emitted at around .125 second intervals. These probes are essentially a WiFi client asking any AP to respond with the SSID of the AP. Hidden SSIDs generally require you send the exact SSID in the probe to generate a response. See [here](https://goodwi.fi/posts/2023/12/hunt-for-hidden-probe/). Flock cameras are also known to use cellular LTE modems to upload to the Flock cloud. +Flock cameras transmit wildcard probe requests on wi-fi channels in ascending order. These probes are emitted at around .125 second intervals (credit to nsm_barii for orginal observations of both of these). These probes are essentially a WiFi client asking any AP to respond with the SSID of the AP. Hidden SSIDs generally require you send the exact SSID in the probe to generate a response. See [here](https://goodwi.fi/posts/2023/12/hunt-for-hidden-probe/). Flock cameras are also known to use cellular LTE modems to upload to the Flock cloud. Given this, I have disabled all other detection methods in this branch. While they are associated with Flock cameras, they are essentially all echoes of the camera itself, which is only gated by OUI match. The other methods can fire when any OUI matched device is sending wildcard probes because nearby APs can respond and generate a false positive match on addr1 or addr3 methods. diff --git a/main.cpp b/main.cpp index 1f72520..5b50a5e 100644 --- a/main.cpp +++ b/main.cpp @@ -40,9 +40,9 @@ #define CHANNEL_MODE_SINGLE 2 #define CHANNEL_MODE CHANNEL_MODE_CUSTOM -#define CHANNEL_DWELL_MS 250 +#define CHANNEL_DWELL_MS 250 // Changed to 2 x 125ms to aid in faster detection. 125ms is the observed hop time of the cameras (credit to nsm_barri for the observation). #define SINGLE_CHANNEL 1 - +// Channel order reversed to aid in faster detection. Credit to nsm_barri for the observation on the ascending hop order of the cameras. static const uint8_t customChannels[] = {11, 6, 1}; static const size_t customChannelCount = sizeof(customChannels) / sizeof(customChannels[0]); From b869495456805230448d718368295564671ad16a Mon Sep 17 00:00:00 2001 From: DeFlock Joplin Date: Sat, 20 Jun 2026 22:29:25 -0500 Subject: [PATCH 4/8] Add credit and proper citation for information element research paper. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index ddf470f..47df06c 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,9 @@ ## Credit Full credit to @NitekryDPaul for the initial research that sparked my research. His OUI list helps make a strong method rock solid. None of this would be here without his original submission. +Additional credit goes to the researchers that found that information element fields are unique to devices: Lucia Pintor & Luigi Atzori. + +Pintor, Lucia & Atzori, Luigi. (2022). Analysis of Wi-Fi Probe Requests Towards Information Element Fingerprinting. 3857-3862. 10.1109/GLOBECOM48099.2022.10001618. --- ## What this branch does From 39012a771ea0d9994fd8455d88d10bf3d7079a2d Mon Sep 17 00:00:00 2001 From: DeFlock Joplin Date: Sat, 20 Jun 2026 23:31:18 -0500 Subject: [PATCH 5/8] Remove possible erroneous MAC for further validation. --- datasets/NitekryDPaul_wifi_ouis.md | 1 - 1 file changed, 1 deletion(-) diff --git a/datasets/NitekryDPaul_wifi_ouis.md b/datasets/NitekryDPaul_wifi_ouis.md index fc4261d..b826fab 100644 --- a/datasets/NitekryDPaul_wifi_ouis.md +++ b/datasets/NitekryDPaul_wifi_ouis.md @@ -85,7 +85,6 @@ a4:cf:12 | 64:6e:69 | Flock Safety infrastructure | WiFi 2.4 GHz | @NitekryDPaul | | 48:27:ea | Flock Safety infrastructure | WiFi 2.4 GHz | @NitekryDPaul | | a4:cf:12 | Flock Safety infrastructure | WiFi 2.4 GHz | @NitekryDPaul | -| 82:6b:f2 | Flock Safety infrastructure | WiFi 2.4 GHz (wildcard probe) | Michael / DeFlockJoplin | ## Detection strategy From f185b9720f7f22b1d89f2dc1f982e1cc254d0c48 Mon Sep 17 00:00:00 2001 From: DeFlock Joplin Date: Sat, 20 Jun 2026 23:32:23 -0500 Subject: [PATCH 6/8] fix remaining text. --- datasets/NitekryDPaul_wifi_ouis.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/datasets/NitekryDPaul_wifi_ouis.md b/datasets/NitekryDPaul_wifi_ouis.md index b826fab..caccc5a 100644 --- a/datasets/NitekryDPaul_wifi_ouis.md +++ b/datasets/NitekryDPaul_wifi_ouis.md @@ -10,12 +10,9 @@ Flock stations spend most of their duty cycle asleep, waking briefly to upload a This addr1 technique is @NitekryDPaul's discovery and is the basis of the `promiscuis-flock-you` firmware. -## OUI list (31 prefixes, lowercase, colon-separated) +## OUI list (30 prefixes, lowercase, colon-separated) -@NitekryDPaul contributed the first 30. The 31st (`82:6b:f2`) was contributed -by **Michael / DeFlockJoplin** during follow-up drive-testing in Joplin — it's -the OUI of the 12th camera in his field test, which the original list didn't -catch. See [DeflockJoplin/flock-you](https://github.com/DeflockJoplin/flock-you). +@NitekryDPaul contributed all 30. ``` 70:c9:4e From c19c1f8637acb761cce6330642d09b3695c6e912 Mon Sep 17 00:00:00 2001 From: DeFlock Joplin Date: Sun, 21 Jun 2026 12:38:13 -0500 Subject: [PATCH 7/8] fix formatting issue introduced with new credits. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 47df06c..7b87473 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Full credit to @NitekryDPaul for the initial research that sparked my research. Additional credit goes to the researchers that found that information element fields are unique to devices: Lucia Pintor & Luigi Atzori. Pintor, Lucia & Atzori, Luigi. (2022). Analysis of Wi-Fi Probe Requests Towards Information Element Fingerprinting. 3857-3862. 10.1109/GLOBECOM48099.2022.10001618. + --- ## What this branch does From 25280bc6ae16dbcf99cf9b16c1f0859cf8a456ba Mon Sep 17 00:00:00 2001 From: DeFlock Joplin Date: Mon, 22 Jun 2026 18:30:29 -0500 Subject: [PATCH 8/8] add display dongle.h that was removed mistakenly. --- display_dongle.h | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 display_dongle.h diff --git a/display_dongle.h b/display_dongle.h new file mode 100644 index 0000000..c5cb1a2 --- /dev/null +++ b/display_dongle.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +#ifdef USE_T_DONGLE_DISPLAY + +void dongleDisplayInit(); +void dongleDisplayShowIdle(uint8_t ch, int detCount); +void dongleDisplayShowAlert(const char* method, const char* mac, int8_t rssi, + uint8_t ch, unsigned long alertMs); +void dongleDisplayTick(unsigned long now, uint8_t ch, int detCount); +bool dongleDisplayInAlert(unsigned long now); + +#else + +static inline void dongleDisplayInit() {} +static inline void dongleDisplayShowIdle(uint8_t, int) {} +static inline void dongleDisplayShowAlert(const char*, const char*, int8_t, uint8_t, + unsigned long) {} +static inline void dongleDisplayTick(unsigned long, uint8_t, int) {} +static inline bool dongleDisplayInAlert(unsigned long) { return false; } + +#endif