Open-source physical verification of cooperative-broadcast claims (FAA Remote ID for drones, AIS for ships) using passive radar.
Built at the 3rd Annual National Security Hackathon (Shack15, San Francisco, May 2-3, 2026) on $300 of commercial SDR hardware (Pi 5 + Pluto+ + RTL-SDR), using the open-source blah2 passive-radar pipeline. From scratch this weekend.
Operator view at http://localhost:8090/combined — single page combining the verdicts dashboard (left) with the live track map (right). Left pane shows live PHANTOM verdicts from the spoofer demo, with sub-millisecond latency badges, TAK/Delta-ready CoT XML output, and the raw verdict payload. Right pane shows 14 live blah2 tracks geolocated via bistatic ellipse + Doppler — yellow markers with translucent CE rings around the Shack15 receiver, receiver shown as the blue dot. Same architecture serves both airborne (drone Remote ID spoof defense) and maritime (AIS spoof defense) variants.
| URL | Purpose |
|---|---|
http://localhost:8090/combined |
Operator view — dashboard + map side-by-side (recommended for the demo) |
http://localhost:7879/ |
Verdicts dashboard alone (fusion's primary UI) |
http://localhost:8090/ |
Map alone (mapserver's primary UI) |
Russia launches three thousand Shahed drones a month at Ukraine. The best detection layer they have — Sky Fortress, a 14,000-microphone acoustic mesh — gets alerts to fire teams in 12 seconds. But it's signature-dependent and breaks on jet variants like Geran-3. Active radars cost $10M per unit and get hit by anti-radiation missiles within a week of deployment.
We wanted to see how much of a passive-radar capability you could build from scratch in a weekend, on commercial hardware, against three real defense problems at once:
- Drone detection — using a TV broadcast tower as transmitter (no emissions of our own)
- Indoor presence sensing — using ambient Wi-Fi reflections (live volunteer demo)
- Drone-ID spoof defense — physical verification of FAA Remote ID via radar truth
Same box, same code, three constituencies addressed by one platform.
Remote ID is the FAA's drone identification standard. It's broadcast over Wi-Fi NAN beacons or BLE 5 extended advertisements and tells anyone listening "drone X is at lat/lon Y, altitude Z, going N m/s heading H." It's how law enforcement, airports, and other operators identify drones in the airspace.
It can be spoofed in 10 seconds with $5 of hardware. A simple ESP32 sketch broadcasts arbitrary Remote ID frames, and any receiver believes them. There's no cryptographic signing on the protocol.
A defender can't tell a real drone from a phantom one — unless they have an independent physical sensor confirming the broadcast. Fantom Finder uses passive radar (FM, DTV, or Wi-Fi as illuminator of opportunity) to do exactly that. If a Remote ID claim comes in and there's no radar track at the claimed position, we flag PHANTOM. If a track exists but its rotor micro-Doppler signature contradicts the claimed UA type, we flag DECEPTION (cf. Kozlov et al., Nature Sci Reports 2025, on radar deception).
- Defense — drone spoof defense in contested airspace; counter-c-UAS deception.
- Aviation safety — uncorrelated tracks visible alongside ADS-B, OpenDroneID feeds.
- Anomaly research — physics-based filters (kinematic-impossibility, supersonic-Doppler) that the passive-radar research community (Greneker, Georgia Tech) has flagged as the unsolved problem in long-range anomaly detection.
┌─────────────────────────────────────────────────────────────────────┐
│ Nemesis (Pi 5 + Pluto+ SDR, 2 coherent RX channels) │
│ Reference antenna ──> Pluto Ch0 ──┐ │
│ Surveillance ant ──> Pluto Ch1 ──┴──> blah2 (existing C++) │
│ Wiener-Hopf clutter → │
│ Cross-Ambiguity Function → │
│ CFAR → Tracker │
│ │ │
│ ▼ │
│ /api/tracks/live │
│ │ │
│ ESP32-S3 / Linux libopendroneid ──> JSON ──┐ │ │
│ ▼ ▼ │
│ ┌──────── fusion.py (this repo) ───────────────────┐ │
│ │ Match Remote ID → radar tracks (haversine + class) │
│ │ Verdict: CONFIRMED / PHANTOM / DECEPTION + provenance │
│ │ HTTP/SSE to dashboard.html on :7879 │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
Software-only demo (no hardware required):
git clone https://github.com/<yours>/phantom-proof
cd phantom-proof
sudo apt-get install -y python3-aiohttp python3-numpy python3-scipy
# Terminal 1: synthetic radar tracks (stand-in for blah2)
python3 fake_tracker.py --port 49256
# Terminal 2: fusion service + dashboard
python3 fusion.py --tracker http://localhost:49256 --dashboard-port 7879
# Browser: http://localhost:7879/
# Press SPACE → 6 verdicts cascade in 5.5s (CONFIRMED + PHANTOM × 3 + DECEPTION + CONFIRMED)# On the Pi, install Docker + build blah2
bash scripts/01-install-blah2-on-pi.sh
# Swap to indoor Wi-Fi config (5 GHz, 20 MHz BW)
bash scripts/03-switch-to-wifi-config.sh
# Start fusion against the real radar tracker
bash scripts/04-start-demo.sh
# Browser: http://<pi-ip>:7879/Walk past the antennas — you'll see your own body Doppler track. Run the spoofer (or fire the on-stage adversary ESP32):
python3 spoofer.py --target localhost:7878 --count 3PHANTOM verdicts appear immediately because the spoofed positions don't match any radar track.
| File | Purpose |
|---|---|
fusion.py |
OpenDroneID UDP listener + verdict engine + HTTP/SSE dashboard server (:7879) |
dashboard.html |
Single-page operator UI with verdict feed, latency HUD, and CoT panel |
spoofer.py |
Software adversary — emits fake Remote ID claims to fusion's UDP :7878 |
firmware/esp32_droneid_spoofer.ino |
Real-RF Remote ID spoofer (ESP32-S3 sketch) |
fake_tracker.py |
Stand-in for nemesis-tracker /api/tracks/live; demos without radar |
demo_show.py |
38 s scripted CONFIRMED → PHANTOM × 3 → DECEPTION → CONFIRMED sequence |
aural.py |
Convert rotor micro-Doppler spectrogram to audible WAV |
| File | Purpose |
|---|---|
ais_fusion.py |
AIS UDP listener + verdict engine + HTTP/SSE on :7876 (UDP :7877) |
ais_spoofer.py |
Software adversary — fake AIS broadcasts (zombie tankers, identity-spoofers) |
| File | Purpose |
|---|---|
geolocate.py |
Bistatic-ellipse + Doppler target geolocation; honest CE estimates |
blah2_config.py |
Live fetch of rx/tx/fc from blah2's /api/config; hardcoded fallback |
blah2_to_cot.py |
One-shot CLI: pull a single track from blah2/nemesis-tracker, emit CoT 2.0 XML; supports --with-rationale, --multicast, --unicast |
cot_emitter.py |
Verdicts → CoT XML; UDP multicast to ATAK SA channel + HTTP/SSE on :7880 |
cot_tcp_bridge.py |
TCP server iTAK / WinTAK / TAK clients connect to (port 8087) |
cot_verify.py |
CoT multicast listener for testing without a real TAK client |
mapserver.py |
Web map server (proxies tracks + applies geolocation) on :8090 |
map.html |
Leaflet-based live map; renders position + CE ring per track |
| File | Purpose |
|---|---|
classifier/rationale.py |
Operator-grade rationale per track or verdict; rule-based default, optional Anthropic Claude path when ANTHROPIC_API_KEY is set |
| File | Purpose |
|---|---|
test_blah2_cot.py |
Programmatic validator: pulls live tracks, runs conversion, asserts every CoT field (16 checks/track) |
blah2-config/config-wifi-indoor.yml |
blah2 config for indoor 5 GHz Wi-Fi illuminator |
scripts/0[0-9]-*.sh |
Bring-up: install blah2, deploy, switch config, start fusion, start CoT, software-only demo |
pitch.md / pitch_maritime.md |
3:00 pitch scripts (airborne / maritime variants) with stage cues + Q&A |
docs/map_screenshot.png |
Reference screenshot (above) |
Remote ID claim arrives (UDP JSON, port 7878).
If claim has lat/lon AND any radar track has lat/lon:
Find the geographically nearest track within MATCH_RADIUS_M (default 80 m).
If none found: PHANTOM.
Otherwise: candidate is the nearest track.
If claim has no lat/lon:
Find any active track within MATCH_TIMEOUT_S (default 3 s).
If none found: PHANTOM.
For the candidate track:
If (claim.ua_type, observed_class) is a known contradiction
(e.g. claim=Aeroplane but rotor harmonics show quadcopter):
DECEPTION.
Otherwise: CONFIRMED.
Every verdict carries a human-readable reason string (the "because") so the operator dashboard can surface provenance directly.
| Endpoint | Method | Purpose |
|---|---|---|
/ |
GET | Dashboard HTML |
/events |
GET | SSE feed of verdicts |
/verdicts |
GET | JSON history (last 200 verdicts) |
/tracks |
GET | Last-polled radar tracks |
/health |
GET | Service health, uptime, counts |
/demo |
POST | Fire scripted CONFIRMED → PHANTOM × 3 → DECEPTION → CONFIRMED sequence |
/clear |
POST | Clear verdict history |
UDP port 7878 receives Remote ID JSON envelopes:
{
"ts": 1777755472.123,
"basic_id": {"uas_id": "MAVIC-2A", "ua_type": "Helicopter_Multirotor"},
"location": {"latitude": 37.79504, "longitude": -122.39349, "altitude_m": 30}
}MIT. Built on top of 30hours/blah2 (BSD), with credit to the OpenDroneID community and cyber-defence-campus/droneRemoteIDSpoofer for the receiver/spoofer reference.
- Gene Greneker (Georgia Tech) — kinematic-impossibility filter framing, supersonic-Doppler insight
- Victor Chen, The Micro-Doppler Effect in Radar (Artech House, 2nd ed.) — bistatic micro-Doppler equations, rotor signature analysis
- ELDAEON — Nemesis hardware platform
- US Army xTech, Cerebral Valley, Shield Capital — hosting the hackathon that produced this work
