Compare commits
3 commits
5099581fc3
...
9c05879451
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c05879451 | |||
| 2685a6d71b | |||
| 967d0db853 |
2 changed files with 74 additions and 33 deletions
35
README.md
35
README.md
|
|
@ -43,32 +43,37 @@ Dantir uses **seven detection methods** across BLE and WiFi:
|
|||
|
||||
| Method | Radio | How it matches |
|
||||
|--------|-------|----------------|
|
||||
| `mac_prefix` | BLE | MAC OUI against ~20 Flock Safety prefixes |
|
||||
| `mac_prefix` | BLE | MAC OUI against the Flock Safety prefix set (lowest confidence — see note) |
|
||||
| `device_name` | BLE | Advertised name patterns (Flock, Penguin, Pigvision, Ring, Ray-Ban…) |
|
||||
| `ble_mfr_id` | BLE | Bluetooth manufacturer company IDs |
|
||||
| `raven_uuid` | BLE | Raven gunshot-detector service UUIDs (+ firmware-version estimate) |
|
||||
| `wifi_probe` | WiFi | Probe-request source MAC OUI (promiscuous mode) |
|
||||
| `wifi_beacon` | WiFi | Beacon-frame source MAC OUI (promiscuous mode) |
|
||||
|
||||
Detections are grouped into **eight categories**, each with its own Morse-code
|
||||
Detections are grouped into **ten categories**, each with its own Morse-code
|
||||
audio signature on the buzzer:
|
||||
|
||||
| Category | Morse | What it covers |
|
||||
|----------|:-----:|----------------|
|
||||
| `flock` | ··-· (F) | Flock Safety / ALPR cameras, Penguin, Pigvision |
|
||||
| `glasses` | --· (G) | Recording smart glasses (Ray-Ban Meta, Oakley, Snap) |
|
||||
| `tracker` | - (T) | Bluetooth trackers (Tile, etc.) |
|
||||
| `lawenf` | ·-·· (L) | Law-enforcement gear (TASER/Axon, Motorola Solutions) |
|
||||
| `ring` | ·-· (R) | Ring / Blink / Amazon cameras |
|
||||
| `camera` | ··· (S) | Other surveillance cameras (Hikvision, Arlo, Wyze) |
|
||||
| `raven` | ···- (V) | Raven gunshot-detector nodes |
|
||||
| `wifi` | ·-- (W) | Generic WiFi-side detections |
|
||||
| `flock` | ··-· (F) | Flock Safety / ALPR cameras, Penguin, Pigvision |
|
||||
| `axon` | ·- (A) | Axon ALPR cameras (the vendor replacing Flock in some cities) |
|
||||
| `glasses` | --· (G) | Camera-equipped smart glasses (Ray-Ban Meta, Oakley, Snap) |
|
||||
| `vr_headset` | · (E) | VR headsets (Quest/Oculus) — benign; logged with a quiet blip, not a threat alert |
|
||||
| `tracker` | - (T) | Bluetooth trackers (Tile, etc.) |
|
||||
| `lawenf` | ·-·· (L) | Law-enforcement gear (TASER/Axon body-cams, Motorola Solutions) |
|
||||
| `ring` | ·-· (R) | Ring / Blink / Amazon cameras |
|
||||
| `camera` | ··· (S) | Other surveillance cameras (Hikvision, Arlo, Wyze) |
|
||||
| `raven` | ···- (V) | Raven gunshot-detector nodes |
|
||||
| `wifi` | ·-- (W) | Generic WiFi-side detections |
|
||||
|
||||
> **Detection is signature-based, not proof.** OUI/manufacturer matches catch
|
||||
> consumer hardware that shares the same chipset vendors (e.g. some Wyze, Hue,
|
||||
> or OBD-II dongles can trip a low-confidence match). Treat `device_name` and
|
||||
> `ble_mfr_id` hits as higher confidence than a bare `mac_prefix` match, and
|
||||
> confirm visually before drawing conclusions.
|
||||
> **Detection is signature-based, not proof.** Many OUIs are shared across a
|
||||
> chipset vendor's entire catalog, so a bare `mac_prefix` match is the
|
||||
> lowest-confidence signal. Treat `device_name`, `ble_mfr_id`, and `raven_uuid`
|
||||
> hits as higher confidence, and confirm visually before drawing conclusions.
|
||||
> Known shared-OUI false-positive sources — e.g. a Silicon Labs prefix that also
|
||||
> covers Wyze locks and OBD-II dongles, and the Raspberry Pi prefix — have been
|
||||
> pulled from the Flock list; real Flock-on-RPi now relies on name (Penguin /
|
||||
> Pigvision) and manufacturer ID, not a bare OUI.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
72
src/main.cpp
72
src/main.cpp
|
|
@ -96,11 +96,16 @@
|
|||
// Known Flock Safety MAC address prefixes (OUIs) — matched during BLE scan
|
||||
static const char* ble_mac_prefixes[] = {
|
||||
// Flock Safety — FS Ext Battery devices
|
||||
"58:8e:81", "cc:cc:cc", "ec:1b:bd", "90:35:ea", "04:0d:84",
|
||||
"58:8e:81", "cc:cc:cc", "ec:1b:bd", "90:35:ea",
|
||||
"f0:82:c0", "1c:34:f1", "38:5b:44", "94:34:69", "b4:e3:f9",
|
||||
// Flock Safety — WiFi-enabled devices
|
||||
"70:c9:4e", "3c:91:80", "d8:f3:bc", "80:30:49", "14:5a:fc",
|
||||
"74:4c:a1", "08:3a:88", "9c:2f:9d", "94:08:53", "e4:aa:ea"
|
||||
"74:4c:a1", "9c:2f:9d", "94:08:53", "e4:aa:ea"
|
||||
// REMOVED 2026-06-23 (false-positive sources — see signatures/ audit):
|
||||
// "04:0d:84" — Silicon Labs OUI (Wyze Lock / BlueDriver / Philips Hue), NOT Flock
|
||||
// "08:3a:88" — Raspberry Pi Foundation OUI (millions of hobby RPis), too ambiguous
|
||||
// Both produced false "flock" tags. Real Flock-on-RPi now relies on name
|
||||
// (Penguin/Pigvision) + mfr ID 0x09C8, not these shared OUIs.
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -166,14 +171,19 @@ static const MfrEntry ble_manufacturer_entries[] = {
|
|||
// These OUIs are checked against source MACs in WiFi management frames
|
||||
// (probe requests, beacon frames) captured via promiscuous callback.
|
||||
|
||||
// Ring LLC OUI prefixes (IEEE registered, verified Feb 2026)
|
||||
static const char* wifi_mac_prefixes[] = {
|
||||
// WiFi-side surveillance OUI prefixes (matched in promiscuous mode). Each carries its
|
||||
// own category so matches aren't all assumed Ring (mirrors the shared signatures/ library).
|
||||
struct WifiPrefixEntry { const char* prefix; const char* category; };
|
||||
static const WifiPrefixEntry wifi_mac_prefixes[] = {
|
||||
// Ring LLC
|
||||
"00:b4:63", "18:7f:88", "24:2b:d6", "34:3e:a4",
|
||||
"54:e0:19", "5c:47:5e", "64:9a:63", "90:48:6c",
|
||||
"9c:76:13", "ac:9f:c3", "c4:db:ad", "cc:3b:fb",
|
||||
{"00:b4:63","ring"}, {"18:7f:88","ring"}, {"24:2b:d6","ring"}, {"34:3e:a4","ring"},
|
||||
{"54:e0:19","ring"}, {"5c:47:5e","ring"}, {"64:9a:63","ring"}, {"90:48:6c","ring"},
|
||||
{"9c:76:13","ring"}, {"ac:9f:c3","ring"}, {"c4:db:ad","ring"}, {"cc:3b:fb","ring"},
|
||||
// Blink by Amazon (Ring's sister brand)
|
||||
"70:ad:43"
|
||||
{"70:ad:43","ring"},
|
||||
// Axon ALPR — catches Lightpost's 2.4GHz radio / Outpost in setup mode.
|
||||
// (Deployed Outpost is LTE-only/RF-silent; Lightpost's 5GHz is invisible to ESP32.)
|
||||
{"00:25:df","axon"},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -440,6 +450,12 @@ static void fyMorseCategory(const char* category) {
|
|||
} else if (strcmp(category, "raven") == 0) {
|
||||
// V: ···-
|
||||
fyMorseDit(); fyMorseDit(); fyMorseDit(); fyMorseDah();
|
||||
} else if (strcmp(category, "axon") == 0) {
|
||||
// A: ·- (Axon ALPR — Flock's municipal replacement)
|
||||
fyMorseDit(); fyMorseDah();
|
||||
} else if (strcmp(category, "vr_headset") == 0) {
|
||||
// E: · (single dit — VR headset, benign; quiet acknowledgement, not a threat)
|
||||
fyMorseDit();
|
||||
} else {
|
||||
// Unknown: single long beep
|
||||
fyMorseDah(); fyMorseDah();
|
||||
|
|
@ -562,13 +578,14 @@ static bool checkBLEMACPrefix(const uint8_t* mac) {
|
|||
return false;
|
||||
}
|
||||
|
||||
static bool checkWiFiMACPrefix(const uint8_t* mac) {
|
||||
// Returns the category for a matching WiFi OUI, or nullptr if no match.
|
||||
static const char* checkWiFiMACPrefix(const uint8_t* mac) {
|
||||
char mac_str[9];
|
||||
snprintf(mac_str, sizeof(mac_str), "%02x:%02x:%02x", mac[0], mac[1], mac[2]);
|
||||
for (size_t i = 0; i < sizeof(wifi_mac_prefixes)/sizeof(wifi_mac_prefixes[0]); i++) {
|
||||
if (strncasecmp(mac_str, wifi_mac_prefixes[i], 8) == 0) return true;
|
||||
if (strncasecmp(mac_str, wifi_mac_prefixes[i].prefix, 8) == 0) return wifi_mac_prefixes[i].category;
|
||||
}
|
||||
return false;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -608,7 +625,8 @@ static void fyWifiPromiscuousCB(void *buf, wifi_promiscuous_pkt_type_t type) {
|
|||
// Address 2 (source/transmitter MAC) at offset 10
|
||||
const uint8_t *src_mac = &frame[10];
|
||||
|
||||
if (!checkWiFiMACPrefix(src_mac)) return;
|
||||
const char *wcat = checkWiFiMACPrefix(src_mac);
|
||||
if (!wcat) return;
|
||||
|
||||
// Match! Build MAC string and register detection
|
||||
char mac_str[18];
|
||||
|
|
@ -619,12 +637,12 @@ static void fyWifiPromiscuousCB(void *buf, wifi_promiscuous_pkt_type_t type) {
|
|||
const char *method = (subtype == WIFI_MGMT_PROBE_REQ) ? "wifi_probe" : "wifi_beacon";
|
||||
int rssi = pkt->rx_ctrl.rssi;
|
||||
|
||||
int idx = fyAddDetection(mac_str, "", rssi, method, "ring"); // WiFi OUIs are all Ring/Blink
|
||||
int idx = fyAddDetection(mac_str, "", rssi, method, wcat);
|
||||
if (idx >= 0) {
|
||||
if (fyDet[idx].count == 1) {
|
||||
// First sighting — new WiFi surveillance device
|
||||
fyWifiDetCount++;
|
||||
printf("[DANTIR] WiFi DETECTED [ring]: %s RSSI:%d [%s]\n", mac_str, rssi, method);
|
||||
printf("[DANTIR] WiFi DETECTED [%s]: %s RSSI:%d [%s]\n", wcat, mac_str, rssi, method);
|
||||
|
||||
// JSON serial output
|
||||
char gpsBuf[80] = "";
|
||||
|
|
@ -633,9 +651,9 @@ static void fyWifiPromiscuousCB(void *buf, wifi_promiscuous_pkt_type_t type) {
|
|||
",\"gps\":{\"latitude\":%.8f,\"longitude\":%.8f,\"accuracy\":%.1f}",
|
||||
fyGPSLat, fyGPSLon, fyGPSAcc);
|
||||
}
|
||||
printf("{\"detection_method\":\"%s\",\"category\":\"ring\",\"protocol\":\"wifi\","
|
||||
printf("{\"detection_method\":\"%s\",\"category\":\"%s\",\"protocol\":\"wifi\","
|
||||
"\"mac_address\":\"%s\",\"rssi\":%d%s}\n",
|
||||
method, mac_str, rssi, gpsBuf);
|
||||
method, wcat, mac_str, rssi, gpsBuf);
|
||||
}
|
||||
|
||||
// Defer buzzer/LED alert to main loop (can't call delay() here)
|
||||
|
|
@ -913,6 +931,18 @@ class FYBLECallbacks : public NimBLEAdvertisedDeviceCallbacks {
|
|||
}
|
||||
}
|
||||
|
||||
// 5. Refine glasses: Meta's mfr ID covers BOTH Ray-Ban Meta (camera/surveillance
|
||||
// glasses) and Quest/Oculus (VR headsets) — split by device name (different
|
||||
// threat models). Mirrors firmware-todo #6 / shared signatures library.
|
||||
if (cat && strcmp(cat, "glasses") == 0 && !name.empty()) {
|
||||
std::string lname = name;
|
||||
for (auto& c : lname) c = (char)tolower((unsigned char)c);
|
||||
if (lname.find("quest") != std::string::npos ||
|
||||
lname.find("oculus") != std::string::npos) {
|
||||
cat = "vr_headset";
|
||||
}
|
||||
}
|
||||
|
||||
if (detected) {
|
||||
int idx = fyAddDetection(addrStr.c_str(), name.c_str(), rssi,
|
||||
method, cat ? cat : "unknown",
|
||||
|
|
@ -1496,6 +1526,12 @@ h+='<div class="pg"><h3>Raven UUIDs ('+p.raven.length+')</h3><div class="it">'+p
|
|||
document.getElementById('pC').innerHTML=h;window._pL=1;}).catch(()=>{});}
|
||||
// === GPS ===
|
||||
let _gW=null,_gOk=false,_gTried=false;
|
||||
// Screen Wake Lock — keep the screen on during GPS capture. Phone screen-sleep suspends the
|
||||
// page and stops geolocation (the #1 cause of GPS-less detections while walking). Re-acquire
|
||||
// when the page returns to the foreground. Needs a secure context (same flag GPS already needs).
|
||||
let _wl=null;
|
||||
async function acquireWake(){try{if('wakeLock' in navigator){_wl=await navigator.wakeLock.request('screen');_wl.addEventListener('release',function(){_wl=null;});}}catch(e){}}
|
||||
document.addEventListener('visibilitychange',function(){if(document.visibilityState==='visible'&&_gW!==null&&_wl===null){acquireWake();}});
|
||||
function sendGPS(p){_gOk=true;let g=document.getElementById('sG');g.textContent='OK';g.style.color='#22c55e';
|
||||
fetch('/api/gps?lat='+p.coords.latitude+'&lon='+p.coords.longitude+'&acc='+(p.coords.accuracy||0)).catch(()=>{});}
|
||||
function gpsErr(e){_gOk=false;let g=document.getElementById('sG');
|
||||
|
|
@ -1506,7 +1542,7 @@ g.textContent=msg;}
|
|||
function startGPS(){if(!navigator.geolocation){return false;}
|
||||
if(_gW!==null){navigator.geolocation.clearWatch(_gW);_gW=null;}
|
||||
let g=document.getElementById('sG');g.textContent='...';g.style.color='#facc15';
|
||||
_gW=navigator.geolocation.watchPosition(sendGPS,gpsErr,{enableHighAccuracy:true,maximumAge:5000,timeout:15000});return true;}
|
||||
_gW=navigator.geolocation.watchPosition(sendGPS,gpsErr,{enableHighAccuracy:true,maximumAge:5000,timeout:15000});acquireWake();return true;}
|
||||
function reqGPS(){if(!navigator.geolocation){alert('GPS not available in this browser.');return;}
|
||||
if(_gOk){return;}
|
||||
if(!window.isSecureContext){alert('GPS requires a secure context (HTTPS). This HTTP page may not get GPS permission.\\n\\nAndroid Chrome: try chrome://flags and enable "Insecure origins treated as secure", add http://192.168.4.1\\n\\niPhone: GPS will not work over HTTP.');}
|
||||
|
|
@ -1602,7 +1638,7 @@ static void fySetupServer() {
|
|||
resp->print("],\"wifi_macs\":[");
|
||||
for (size_t i = 0; i < sizeof(wifi_mac_prefixes)/sizeof(wifi_mac_prefixes[0]); i++) {
|
||||
if (i > 0) resp->print(",");
|
||||
resp->printf("\"%s\"", wifi_mac_prefixes[i]);
|
||||
resp->printf("{\"p\":\"%s\",\"c\":\"%s\"}", wifi_mac_prefixes[i].prefix, wifi_mac_prefixes[i].category);
|
||||
}
|
||||
resp->print("],\"names\":[");
|
||||
for (size_t i = 0; i < sizeof(device_name_entries)/sizeof(device_name_entries[0]); i++) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue