BlueZ capture backend: Bluetooth Remote ID with no extra hardware - #8
Conversation
Validated against live hardware ✅The "no hardware validation yet" caveat is resolved for the The advertiser MAC What this does and does not proveProven: the BlueZ D-Bus scan driver works and coexists with a running Still unproven:
Given the |
ble_capture requires a Sniffle nRF52 dongle, so a box with no add-on hardware decodes no Bluetooth Remote ID at all. Every Raspberry Pi already has a radio that can hear ASTM F3411 advertisements -- verified by capturing real DroneBeacon DB120 frames with btmon on an AryaOS box. New bluez_capture module, selected by FEED_URL scheme ble+hci://hci0. It reuses ble_parse for ODID extraction and emits the same (pack, meta) contract BleWorker uses, so the whole downstream chain is unchanged. Two cooperating pieces: * Scan driver (always): BlueZ D-Bus SetDiscoveryFilter(Transport=le) + StartDiscovery. An ordinary D-Bus client, so it coexists with bluetoothd and an active Bluetooth PAN instead of seizing the adapter. * Reader, selectable. 'monitor' uses an HCI_CHANNEL_MONITOR socket -- the passive tap btmon uses -- for every advertising report with full AD bytes and per-frame RSSI, nothing coalesced; needs CAP_NET_RAW. 'dbus' reads ServiceData off org.bluez.Device1, needs no capability but BlueZ may coalesce repeats, and for Remote ID a coalesced repeat is a lost message TYPE rather than a duplicate. 'auto' tries monitor then falls back. CPython's socket.bind() for BTPROTO_HCI accepts only (device_id,) and raises "bind(): wrong format" for a channel, so it cannot select the monitor channel at all; bind_hci_monitor() falls back to calling bind(2) via ctypes with a real struct sockaddr_hci. Confirmed correct against a live adapter: the kernel returns EPERM (the CAP_NET_RAW check) rather than EINVAL, so the channel is parsed as MONITOR. Routing note: "ble+hci" contains "ble", so its branch must precede the substring test in create_tasks(). Covered by a test. Captures LEGACY advertising only -- the ASTM long-range profile uses BT5 extended advertising on the Coded PHY, which the Pi's CYW43455 is not known to receive. Documented as complementary to a Sniffle dongle or DroneScout receiver, not a replacement. 18 new tests, decoding real captured advertisement bytes through synthesized HCI monitor frames so they run in CI with no adapter. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0197da7dhvcPoHxYKamrYqyM
Why
ble_capturerequires a Sniffle nRF52 dongle, so a box with no add-on hardware decodes no Bluetooth Remote ID at all. Every Raspberry Pi already has a radio that can hear ASTM F3411 advertisements — verified by capturing real DroneBeacon DB120 frames withbtmonon an AryaOS box:Wi-Fi RID is not an alternative on that radio —
iw liston brcmfmac showsIBSS, managed, AP, P2P-*and no monitor mode. BLE is the only onboard path.Design
New
bluez_capturemodule selected by theble+hci://hci0scheme. It reusesble_parsefor ODID extraction and emits the same(pack, meta)contractBleWorkeralready uses, so the entire downstream chain is untouched.Scan driver (always): BlueZ D-Bus
SetDiscoveryFilter(Transport=le)+StartDiscovery. An ordinary D-Bus client, so it coexists withbluetoothdand an active Bluetooth PAN instead of seizing the adapter.Reader (selectable):
monitorCAP_NET_RAWHCI_CHANNEL_MONITORsocket — the passive tapbtmonuses. Every advertising report, full AD bytes, per-frame RSSI, nothing coalesced.dbusServiceDataofforg.bluez.Device1. BlueZ may coalesce repeats — and since a transmitter rotates message types, a coalesced repeat is a lost message type, not a duplicate.auto(default)monitor, fall back todbus.Two traps worth flagging
CPython cannot bind the monitor channel.
socket.bind()forBTPROTO_HCIaccepts only(device_id,); passing a channel raisesbind(): wrong format. Sobind_hci_monitor()falls back to callingbind(2)via ctypes with a realstruct sockaddr_hci. Confirmed correct against a live adapter: the kernel returns EPERM (theCAP_NET_RAWcheck) rather than EINVAL, which means the channel was parsed asMONITOR."ble+hci"contains"ble", so its branch must precede the substring test increate_tasks(). Covered by a test.Limitation
Captures legacy advertising only. The ASTM long-range profile uses BT5 extended advertising on the Coded PHY, which the Pi's CYW43455 is not known to receive. Documented as complementary to a Sniffle dongle or DroneScout DS101, not a replacement.
Tests
18 new (57 total, zero warnings). Real captured advertisement bytes are replayed through synthesized HCI monitor frames, so they run in CI with no Bluetooth adapter present. Covers legacy + extended reports, multi-report events, wrong-adapter filtering, RSSI thresholding, D-Bus ServiceData re-wrapping, and truncated input at every offset.
Not yet verified
No hardware validation yet. The parser is exercised against real captured bytes, but end-to-end capture on a live adapter needs
CAP_NET_RAWand a transmitting drone. No catch-rate number is claimed anywhere until it is measured — deliberately, since a single radio hopping 3 advertising channels will do worse than a dedicated 3-channel sniffer.🤖 Generated with Claude Code
https://claude.ai/code/session_0197da7dhvcPoHxYKamrYqyM