Skip to content

Add passive Flock/Raven surveillance detection for WARHOG - #56

Open
jebjarrell1974-debug wants to merge 15 commits into
0ct0sec:mainfrom
jebjarrell1974-debug:feature/flock-detection
Open

Add passive Flock/Raven surveillance detection for WARHOG#56
jebjarrell1974-debug wants to merge 15 commits into
0ct0sec:mainfrom
jebjarrell1974-debug:feature/flock-detection

Conversation

@jebjarrell1974-debug

@jebjarrell1974-debug jebjarrell1974-debug commented Aug 4, 2026

Copy link
Copy Markdown

PORKCHOP goes on defense: a passive surveillance & threat-detection suite

What started as "add Flock camera detection" grew into a full passive counter-surveillance layer for the Cardputer (ADV-tested). Everything here is receive-only — no TX, no attacks — in the spirit of DO NO HAM. Detectors are framework-agnostic and host-testable. Built, flashed, and run on real Cardputer ADV hardware (flash ~59.6%, RAM ~42.7%).

It's a large set — happy to split into smaller PRs if you'd rather take it piecemeal.

Flock / Raven surveillance detection

  • Passive WiFi (promiscuous: OUI match on addr1 and addr2 + wildcard-probe corroboration, broadcast/multicast guard) and BLE (mfr 0x09C8, Raven UUIDs), confidence-scored.
  • Field-tested @NitekryDPaul 31-OUI list (+ DeFlockJoplin's 31st).
  • Runs mode-independently via NetworkRecon's shared promiscuous path + a global drain, so it fires in DO NO HAM / OINK / SPECTRUM / WARHOG.
  • GPS proximity map: ~2,736 ALPR points baked in from OpenStreetMap, with a distance-based audio ramp as you approach a mapped camera.
  • Logs to a deflock.me-ready flock.csv while the AP still flows to the normal WiGLE export.
  • WARHOG time-slice: alternates active-scan and promiscuous so you wardrive and catch live Flock gear.

The Defensive Suite (src/defense/) + GUARD HOG

  • TICK CHECK — BLE tracker detection (AirTag / Tile / Samsung SmartTag / Google Find My)
  • TAIL WAGGER — GPS anti-stalking: unknown device persisting across waypoints → !FOLLOWING
  • SQUEAL ALERT — deauth/disassoc + beacon/probe flood detection
  • FAKE BACON — evil-twin / rogue-AP detection
  • SKY HOGS — drone Remote ID (OpenDroneID, BLE 0xFFFA/0x0D)
  • FLIPPER FINDER — Flipper Zero tally
  • GUARD HOG — always-on watch face with a fused CALM/SNIFFY/SPOOKED score that only escalates when two independent radios agree; Eye-Spy-style BLE↔WiFi rotation using passive promiscuous on the WiFi slice (not active scan, to honor no-TX)

Piglet integration

  • Pig-squeal/grunt warning audio for detections and the proximity ramp.
  • Defensive catches feed the piglet (XP + mood); idle GUARD HOG is ~break-even (dash-mounted pig won't starve); SPOOKED is the best meal. LIFE/XP readout on the watch face.

Docs

  • New README section documenting the suite, including an honest "what the pig CANNOT do" (no IMSI catchers, no analog cameras, 2.4GHz/BLE/LoRa only).

Attribution & licensing (please read)

  • Detection signatures are facts (MAC OUIs, mfr IDs, service UUIDs), attributed in-source: deflock.me / @NitekryDPaul / DeFlockJoplin (Flock); public standards for the rest (ASTM F3411; Apple/Samsung/Tile mfr IDs; Flipper prefixes).
  • The embedded proximity map is derived from OpenStreetMap (ODbL), attribution retained in flock_map_data.h — flagging in case you'd prefer ODbL-derived data kept out of the MIT tree or handled separately.
  • New code is original/clean-room; no GPL/AGPL source copied in.

Notes

  • Passive / no-TX throughout. Adds flash footprint (~59.6%); no partition-table changes needed for direct flash.
  • Known limits: AirTag MAC rotation (~15 min) weakens TAIL WAGGER persistence for AirTags specifically (type detection still fires); WiFi-side drone RID not implemented (BLE covers consumer drones).

Happy to split, rename, or gate any of this behind a build flag — your call.

Confidence-scored WiFi (OUI + addr1 receiver + wildcard probe) and BLE (mfr 0x09C8, name hints, Raven service UUIDs) detection. Dual logging to WiGLE export and a separate flock.csv for deflock.me. Passive only, no TX. Signature data attributed to deflock.me / NitekryDPaul / DeFlockJoplin.

Adds passive Flock Safety / Raven surveillance-camera detection to WARHOG.
Passive only — no new radio mode, no TX.

  • Confidence-scored detector: matches an AP's BSSID OUI against the Flock table
    and its SSID against name hints. Generic Espressif OUIs are scored weak so they
    don't false-alarm (default threshold Medium).
  • Wired into WARHOG's processScanResults(): hits log to a session flock.csv
    (deflock.me-ready) while the AP still flows to the existing WiGLE export.
  • Audible alerts via the SFX module: loud SIREN on a camera hit, distinct tone
    for a Raven gunshot-detector.
  • Namespace renamed flock -> flockdet to avoid colliding with the POSIX flock()
    in the ESP32 toolchain headers.

Builds clean for env:m5cardputer and flashed/run on Cardputer ADV hardware.
Signature table ships the publicly-documented OUIs with the full deflock.me set
stubbed pending a license check; expanding it plus a map-based proximity alert
is the planned follow-up. Signature data attributed to deflock.me / @NitekryDPaul
/ DeFlockJoplin.

Confidence-scored WiFi (OUI + addr1 receiver + wildcard probe) and BLE (mfr 0x09C8, name hints, Raven service UUIDs) detection. Dual logging to WiGLE export and a separate flock.csv for deflock.me. Passive only, no TX. Signature data attributed to deflock.me / NitekryDPaul / DeFlockJoplin.
Runs detection on WARHOG active-scan results via new inspectScanResult() (BSSID OUI + SSID name hints, Medium threshold). Hits log to a session flock.csv for deflock.me while the AP still flows to the WiGLE export. Loud SIREN on a camera hit, distinct tone for Raven, via the SFX module. Renames namespace flock->flockdet to avoid colliding with the POSIX flock() declared in the ESP32 toolchain headers. Builds clean for env:m5cardputer and flashed/tested on Cardputer ADV.
@kaicherry

Copy link
Copy Markdown

This is great man! Note that the code in the 'janus hog' branch is newer, though. I've pulled your code into my fork off that, and it merges cleanly.
-K

…cal OUIs

Wires flockdet::inspectWifiFrame into the promiscuous callback so sleeping
cameras are detected in DO NO HAM / OINK / SPECTRUM, not just WARHOG's active
scan. Hits go through a lock-free ring buffer and are drained in update() for
the siren, toast and GPS-tagged flock CSV on SD.

Replaces the OUI table with the canonical 31-prefix field-tested list
(@NitekryDPaul + DeFlockJoplin 82:6b:f2), all FLOCK_LINKED. Drops the seven
generic Espressif vendor blocks, which matched any ESP32 including the
Cardputer itself. Adds an I/G group-bit guard so broadcast/multicast addr1
values can never match.
flockprox scans a flash-resident table of 2736 reported ALPR locations
(OpenStreetMap surveillance:type=ALPR, ODbL) and ramps an audible tick as the
nearest one closes: silent beyond 1000 ft, a tick that tightens from 1500 ms to
350 ms across 1000-300 ft, then a faster, higher tick inside 300 ft, plus a
one-shot ALPR AHEAD toast on entering the zone.

This is the map half of the feature and is independent of the radio, so it
keeps warning while WiFi is mid-scan. An integer bounding-box prefilter drops
all but a handful of points before any trig runs, and the scan is rate-limited
to 2 Hz since GPS only moves at 1 Hz.

fetch_flockmap.py (Overpass query) and scripts/gen_flock_map.py (CSV to header)
are committed for provenance; the intermediate CSV stays gitignored.
An active scan only sees a camera that is currently beaconing. A sleeping one
shows up as a receiver address in another device frames, which needs
promiscuous mode -- and WARHOG previously stopped NetworkRecon outright, so it
could never see one. WARHOG now runs an 8s scan slice and a 4s promiscuous
slice in a loop, with live Flock detection and the siren running during the
latter.

NetworkRecon is parked with pause() instead of stop() for the duration of the
mode, so each handover costs ~50ms rather than a full BLE-deinit and WiFi
re-init. Handovers only run with no scan in flight, so the radio never changes
mode mid-scan. Both slice durations are tunable constants.
DO NO HAM was silent near cameras because drainFlockHits() lived inside
NetworkRecon::update(), which early-returns whenever recon is paused and is
coupled to recon's lifecycle. WARHOG only ever alarmed from its own active-scan
path, so the promiscuous alarm path had never actually been exercised.

Move the drain into a public NetworkRecon::serviceFlockAlerts() called every
frame from the global app loop, so the alarm + flock.csv log fire in every
sniffing mode (DNH / OINK / SPECTRUM / WARHOG) regardless of pause state.
Enqueue stays in the shared core promiscuous callback, which already covers
every mode. Pipeline counters (match/enqueue/drain/alert) stay live behind
FLOCK_DEBUG_COUNTERS (default off) for future bring-up.

Replace the siren/beeps with pig-themed SFX: PIG_ALARM (squeal x2, camera
found), PIG_RAVEN (low angry squeal, Raven), PIG_SQUEAL (close-range proximity
loop) and PIG_GRUNT (gentle proximity tick). Wire detection and the proximity
ramp to them.
Adds the defensive-suite container mode GUARD HOG (menu RECON > GRDHOG, or key
G from idle). Passive, RX-only: it stops NetworkRecon, powers WiFi down and runs
a NimBLE passive scan.

TICK CHECK (defense/tracker_detect): classifies BLE personal-item trackers from
raw advertisement AD structures -- Apple AirTag / Find My (mfr 0x004C, type 0x12,
lost-broadcast 0x1E), Tile (0xFEED/0xFEEC), Samsung SmartTag (0xFD5A / mfr 0x0075)
and best-effort Google Find My (0xFEAA, low confidence, Eddystone-shared).
Framework-agnostic like flock_detect so it host-tests. Signatures attributed to
the reference projects (Eye Spy / CYD AirTag scanner) as facts.

TAIL WAGGER: bounded cross-waypoint persistence engine. A BLE MAC seen >=3x
across >=3 distinct GPS waypoints (>=120m apart) over >=5 min is flagged
!FOLLOWING with the pig alarm. Trackers also log to guardhog CSV on SD.

The watch face lists nearby trackers (type / RSSI / MAC tail / LOST) and raises
the !FOLLOWING banner. Full CALM/SNIFFY/SPOOKED fusion + WiFi rotation land in P5.
Adds defense/attack_detect, a lock-free per-second monitor fed from the shared
promiscuous callback (so it runs in every WiFi sniffing mode, mode-independent
like the flock hook). Counts deauth (subtype 0x0C), disassoc (0x0A), beacon
floods (0x08 from locally-administered/random BSSIDs, the mdk4 tell) and probe
floods (0x04). Thresholds are tunable.

The per-second roll-up + alert run from serviceFlockAlerts() on the main loop:
crossing a threshold squeals PIG_SQUEAL (rate-limited to once / 5s) and toasts
the attack type + offending BSSID tail. Counters are std::atomic so onFrame()
stays callback-safe. Stats exposed via NetworkRecon::getAttackStats/getLastAttack
for GUARD HOG'\''s fused watch face in P5.
Adds defense/eviltwin_detect, a self-baselining rogue-AP monitor. The first
BSSID seen for an SSID sets its expected security; a later BSSID for that SSID
with a security mismatch (a WPA2 network suddenly beaconing OPEN elsewhere -- the
classic Pineapple/karma clone) is flagged EVIL TWIN. Two BSSIDs with matching
security are ordinary roaming/mesh and stay quiet, keeping false positives off
enterprise and carrier networks.

Fed one observation per newly-seen beacon from processBeacon via a lock-free
ring (callback-safe); the SSID table and alert run on the main loop in
serviceFlockAlerts(), so it works in every WiFi sniffing mode. A flag fires
PIG_ALARM + a toast naming the SSID and both BSSID tails. Count exposed via
NetworkRecon::getEvilTwinCount() for GUARD HOG.
SKY HOGS (defense/drone_detect): parses OpenDroneID / ASTM F3411 BLE frames --
16-bit service data UUID 0xFFFA, application code 0x0D, message counter, then the
25-byte Remote ID message; extracts the UAS ID from a Basic ID message. UUID and
app code verified against the OpenDroneID spec and Eye Spy; attributed as facts.

FLIPPER FINDER (defense/flipper_detect): Flipper Zero by BLE address prefix
(0C:FA:22 / 80:E1:26 / 80:E1:27) or a Flipper advertised name.

Both run in GUARD HOG'\''s existing NimBLE passive scan alongside TICK CHECK. The
callback now un-reverses NimBLE'\''s little-endian address to human order so prefix
matching and display tails are correct. Distinct-MAC tallies drive the watch
face (ticks / skyhogs / flippers); a new drone raises PIG_ALARM, a new Flipper a
grunt. Framework-agnostic detectors, host-testable like the rest.
GUARD HOG now time-shares the one 2.4GHz radio (Eye-Spy rotation): a ~9s BLE
passive-scan slice (trackers / drones / Flippers) alternating with a ~8s WiFi
slice that runs its own PASSIVE channel-hopping promiscuous sniff feeding
NetworkRecon::inspectDefenseFrame (flock cams / deauth+flood / evil-twin).
NimBLE stays initialised throughout -- only the scan starts/stops -- to avoid the
ESP32-S3 re-init trap. No TX in either slice (active scanning dropped to honour
the passive mandate).

Factored inspectDefenseFrame() out of NetworkRecon so both radios feed the same
flock/attack/evil-twin monitors, and exposed getFlockAlertCount().

Fused CALM / SNIFFY / SPOOKED score: SPOOKED only when two independent signals
agree -- a BLE strong signal (follower/drone) AND a WiFi one (flock/attack/twin),
or two distinct WiFi signals (e.g. deauth-flood + evil-twin). Entering SPOOKED
fires PIG_ALARM and turns the piglet angry. The watch face leads with the mood
word, the live radio slice, and both radios'\'' tallies.
Drops the DEFENSIVE SUITE page into README section 2 verbatim (numbered 2.9 to
fit, since 2.5 is PIGGY BLUES). Documents GUARD HOG / TICK CHECK / TAIL WAGGER /
SQUEAL ALERT / FAKE BACON / SKY HOGS / FLIPPER FINDER and, importantly, what the
pig canNOT do (no IMSI catchers, no analog cams, 2.4GHz only). Docs-only.
GUARD HOG rebooted ~4s after entry (only GUARD HOG; the detectors were fine in
other modes). Two GUARD-HOG-specific defects in the BLE scan path:

1. HEAP EXHAUSTION (the ~4s crash). The passive scan set setDuplicateFilter(false)
   but never setMaxResults(), so NimBLE stashed every advertisement in its results
   vector, growing unbounded through the 9s BLE slice. On this no-PSRAM S3 that
   runs the heap out in a few seconds. Fix: setMaxResults(0) -- callback-only, do
   not store adverts. Also window(150) < interval(160) to give the radio room.

2. USE-AFTER-FREE. getAddress() returns a NimBLEAddress BY VALUE and getVal()/
   getBase() point into it, so `const uint8_t* le = device->getAddress().getVal()`
   dangled after the statement. Bind the address to a named local so it outlives
   the read; null-guard it.

Verified on-device: with a temporary auto-enter hook, GUARD HOG ran for 45s
(>2 full BLE<->WiFi rotations) with a single clean boot and zero reboots, where
the old build reboot-looped. Serial slice-transition logs kept for future
diagnosis. Passive/no-TX unchanged.
Coredump (decoded via esp-coredump) showed the real fault: an abort() in the
wifi task, coex_enable() -> coex_core_enable(), during wifi_hw_start(). GUARD
HOG kept the BLE (NimBLE) controller initialized across the BLE->WiFi handoff,
so when the WiFi slice called WiFi.mode(WIFI_STA) the ESP32-S3 coexistence layer
aborted -- two radio controllers can't both be enabled the way it was sequenced.
This is entry-path-independent (it fires at the handoff, ~one slice after entry),
which is why the earlier maxResults/dangling-address fixes did not stop it and
why a USB-CDC survival check gave a false negative (reboots hidden by the
port re-enumerating).

Fix: only ever have ONE radio's controller enabled at a time, using the proven
coex-safe bring-up NetworkRecon already implements. At BLE->WiFi we stop the
scan, NimBLEDevice::deinit(true), then NetworkRecon::start() (which owns WiFi
promiscuous with correct coex and feeds flock/attack/evil-twin -- the global
serviceFlockAlerts drains + alerts). At WiFi->BLE we NetworkRecon::stop(), WiFi
off, then re-init NimBLE + scan. GUARD HOG no longer runs its own promiscuous
callback or channel hop. The maxResults(0)/dangling-address hardening from the
prior commit is retained.

Verified with coredump-to-flash: erased the coredump, ran GUARD HOG through 80s
(>4 full BLE<->WiFi rotations, >8 handoffs); the coredump partition stayed empty
= no panic. Passive/no-TX unchanged.
Jeb's balance: idle guarding holds the pig's life ~flat; catching things fattens
him up. The "life bar" is the piglet mood/happiness (-100..100), which naturally
decays -1 every phraseInterval (5s) = -12/min in quiet modes -- so GUARD HOG left
on the dash slowly starved.

Idle break-even (named constants):
- GH_ONDUTY_DECAY_DIVISOR = 3 (mood.cpp): on guard duty the -1 decay only lands
  every 3rd 5s cycle = -1/15s = -4.0/min.
- GH_VIGILANCE_HAPPINESS = 1 (guardhog.cpp): +1 happiness per completed BLE<->WiFi
  rotation (~17s) = +3.53/min.
- Net idle = -0.47/min -- a hair below zero, never a free gain.

Detections = clear net gain, deduped per device so one camera/tracker can't spam:
- XP_DEFENSE_HIT = 15 XP + GH_CATCH_HAPPINESS = 8 mood per dedup'd catch
  (Mood::onDefensiveCatch: fatten + excitement). Wired at each detector's existing
  dedup point: TICK CHECK new tracker, TAIL WAGGER !FOLLOWING flag, SKY HOGS new
  drone, FLIPPER FINDER new flipper (guardhog.cpp); FLOCK cam, FAKE BACON twin,
  SQUEAL attack (network_recon serviceFlockAlerts, so they also reward in
  DNH/WARHOG). Attack XP is 30s-cooldown-gated (a flood has no per-device key),
  flock is per-MAC, twin is per-SSID.
- XP_DEFENSE_SPOOKED = 30 XP + fatten on entering SPOOKED.

Watch face shows a LIFE + XP readout so the economy is visible: idle holds LIFE
~flat, a catch bumps LIFE and the XP bar. Coredump-verified no crash across 78s of
rotations (mood-decay hot path touched). Passive/no-TX unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants