Bluetooth Stack Diagnostic Platform — unified timeline correlation
Project description
bluTruth -- Unified Bluetooth Diagnostic Platform
Most Bluetooth debugging tools look at one layer of the stack in isolation. btmon sees HCI frames. bluetoothctl sees D-Bus objects. journalctl sees daemon log lines. None of them talk to each other, which means when a disconnect happens you're manually stitching together three different log streams and hoping the timestamps line up.
bluTruth runs a single collection daemon that captures every observable layer of the Bluetooth stack simultaneously, normalizes all events into a shared schema, writes to SQLite + JSONL, correlates related events across sources, and fires pattern rules that detect known failure modes automatically.
When something breaks, the question isn't "which log do I check?" -- it's "show me everything that touched this device in the last 500ms, across every layer."
Stack Coverage
Your App (Spotify, etc.) <-- PipewireCollector
|
PipeWire / PulseAudio <-- codec negotiation, buffer xruns, routing
|
BlueZ profile plugins <-- GattCollector (service/characteristic discovery)
|
bluetoothd <--> D-Bus <-- DbusCollector (all org.bluez signals)
| <-- DaemonLogCollector (journalctl + managed mode)
mgmt API (netlink) <-- MgmtApiCollector (btmgmt + sysfs debug)
|
core bluetooth.ko <-- KernelDriverCollector (dmesg + ftrace + modules)
| <-- EbpfCollector (kernel tracepoints, ns timestamps)
btusb.ko / hci_uart.ko <-- HciCollector (btmon: frames, RSSI, encryption, features)
| <-- SysfsCollector (adapter state, rfkill, USB power)
hardware (USB hub, dongle) <-- UdevCollector (hotplug: insert/remove/bind/unbind)
|
RF / air <-- UbertoothCollector (Classic BT air -- requires hardware)
<-- BleSnifferCollector (BLE air -- requires hardware)
Active-monitoring collectors: L2pingCollector (L2CAP round-trip time), BatteryCollector (GATT battery level polling).
Everything above the RF line works without specialized hardware. The two air-level collectors (Ubertooth, BLE sniffer) require dedicated radio hardware and are mock-only -- see Not Supported below.
Setup
# Uses uv -- installs it if missing
bash setup.sh
# Or manually
uv venv .venv
uv pip install -e ".[dev]"
source .venv/bin/activate
btmon needs cap_net_admin (or root):
sudo setcap cap_net_admin+eip $(which btmon)
For eBPF kernel tracing (recommended):
sudo apt install python3-bpfcc bpfcc-tools
# or: sudo apt install bpftrace
Dependencies
| Package | Purpose |
|---|---|
pyyaml |
Config file parsing |
dbus-next |
D-Bus monitoring (org.bluez signals, GATT introspection) |
aiohttp |
Web UI server + REST API |
watchfiles |
Config hot-reload (inotify, falls back to polling) |
python3-bpfcc |
eBPF kernel tracing (optional, requires root) |
Python 3.10+ required. Tested on 3.11, 3.12, 3.13.
Commands
# Collect from all sources (foreground)
sudo blutruth collect
sudo blutruth collect -v # verbose: print events to stdout
sudo blutruth collect --no-hci # disable specific collectors
sudo blutruth collect --session "reproduce-bug" # named session
# Collect + web UI
sudo blutruth serve
sudo blutruth serve --host 0.0.0.0 --port 9090
# Live tail (like tail -f)
blutruth tail
blutruth tail -s HCI # filter by source
blutruth tail -d AA:BB:CC:DD:EE:FF # filter by device
# Query stored events
blutruth query --device AA:BB:CC:DD:EE:FF --severity WARN
blutruth query --source HCI --limit 500 --json
# Device history -- disconnect analysis across sessions
blutruth history AA:BB:CC:DD:EE:FF
blutruth history AA:BB:CC:DD:EE:FF --sessions 10 --json
# List sessions, devices
blutruth sessions
blutruth devices # includes OUI manufacturer
# Export
blutruth export --format csv -o events.csv
blutruth export --format jsonl --session-id 12 --source HCI
# Replay a JSONL capture through correlation
blutruth replay capture.jsonl --speed 1.0 --session "replay-test"
# Status / prerequisites
blutruth status
CLI Flags
| Flag | Commands | Effect |
|---|---|---|
-c, --config |
All | Config file path (default: ~/.blutruth/config.yaml) |
-v, --verbose |
collect, serve, tail | Print events to stdout |
--no-hci |
collect, serve | Disable HCI collector |
--no-dbus |
collect, serve | Disable D-Bus collector |
--no-daemon |
collect, serve | Disable daemon log collector |
--no-mgmt |
collect, serve | Disable management API collector |
--no-pipewire |
collect, serve | Disable PipeWire collector |
--no-kernel |
collect, serve | Disable kernel driver collector |
--session |
collect, serve, replay | Named session label |
--host |
serve | Bind address (default: 127.0.0.1) |
--port |
serve | Bind port (default: 8484) |
-s, --source |
tail, query, export | Filter by event source |
-d, --device |
tail, query, export | Filter by device address |
--severity |
query, export | Minimum severity filter |
--json |
query, history | JSON output |
-l, --limit |
query, export | Max events returned |
-o, --output |
export | Output file path |
--format |
export | jsonl or csv |
--session-id |
export | Filter to specific session |
--speed |
replay | Playback speed multiplier |
-n, --sessions |
history | Number of sessions to analyze |
Collectors
HCI Collector
Runs btmon as a subprocess and parses its structured output into events with full field extraction.
| What it extracts | Details |
|---|---|
| Disconnect reasons | reason_code (hex) + reason_name (human-readable), auto-normalized |
| RSSI | rssi_dbm from Read RSSI, Inquiry Result, LE Advertising Report |
| Encryption key size | key_size with KNOB attack detection (knob_risk: HIGH if <7, POSSIBLE if <16) |
| Encryption state | encryption_enabled (bool) from Encryption Change events |
| IO capabilities | io_capability from SSP exchange (downgrade detection) |
| LMP features | Decoded feature bitmap from Read Remote Features (via enrichment module) |
| SMP pairing | smp_io_capability, smp_auth_req, smp_auth_flags, smp_max_key_size |
| CIS/BIS isochronous | cig_id, cis_id, big_handle, sdu_interval_us, iso_interval |
| SCO codec | sco_codec (CVSD, mSBC) from Synchronous Connection events |
| ACL packets | num_completed_packets for bandwidth tracking |
| Handle mapping | Connection handle to device address, maintained across events |
Config:
collectors:
hci:
enabled: true
rssi_warn_dbm: -75 # WARN when active-connection RSSI below this
rssi_error_dbm: -85 # ERROR when active-connection RSSI below this
Requires: btmon (from bluez-utils or bluez package). Exclusive resource: hci_monitor_socket.
Event types: DISCONNECT, CONNECT, CONNECT_FAILED, AUTH_COMPLETE, AUTH_FAILURE, ENCRYPT_CHANGE, LE_ADV_REPORT, IO_CAP, PAIR_COMPLETE, LINK_KEY, SMP_PAIRING, SMP_PAIR_FAILED, HCI_HARDWARE_ERROR, CIS_ESTABLISHED, CIS_REQUEST, BIG_CREATED, BIG_SYNC, BIG_SYNC_LOST, BIG_TERMINATED, SCO_CONNECT, SCO_CHANGED, REMOTE_FEATURES, ACL_COMPLETED, HCI_CMD, HCI_EVT, HCI_ACL, HCI_SCO, HCI_INDEX, HCI_MGMT
D-Bus Collector
Monitors all signals from org.bluez on the system D-Bus using dbus-next.
Watches:
PropertiesChangedon all/org/bluez/*paths (Device1, Adapter1, MediaTransport1, MediaPlayer1)InterfacesAdded/InterfacesRemoved(device appear/disappear)- A2DP codec byte decoding (SBC, MP3, AAC, ATRAC, Vendor)
- Property change severity/stage classification (Connected, ServicesResolved, Paired, RSSI, Powered, etc.)
Event types: DBUS_PROP, DBUS_SIG
No root required. No config options.
GATT Service Discovery Collector
When a BLE device connects and resolves services, introspects the full GATT hierarchy via D-Bus.
Discovers:
- All
org.bluez.GattService1interfaces (UUID, primary/secondary, mapped to known service names) - All
org.bluez.GattCharacteristic1interfaces (UUID, flags, mapped to known names) - All
org.bluez.GattDescriptor1interfaces - Reads safe characteristic values: Device Name, Appearance, Battery Level, Model/Serial/Firmware/Hardware/Software Revision, Manufacturer Name
Config:
collectors:
gatt:
read_characteristics: true # set false to skip characteristic reads
Event types: GATT_DISCOVERY (full tree summary), GATT_SERVICE (per-service), GATT_READ (characteristic values), GATT_ERROR
Triggers on ServicesResolved=true. Deduplicates per device per session. Also scans already-connected devices on startup.
Daemon Log Collector
Captures bluetoothd output with automatic stage classification.
Two modes:
- Journal mode (default):
journalctl -u bluetooth -f -o json - Managed mode (opt-in): Stops the system bluetooth service, runs
bluetoothd -n -dfor full debug output, restores the service on stop
Classifies log lines by keyword matching into stages: DISCOVERY, CONNECTION, HANDSHAKE, AUDIO, TEARDOWN, DATA.
Config:
collectors:
journalctl:
enabled: true
unit: bluetooth
format: json
advanced_bluetoothd:
enabled: false # opt-in: managed debug daemon mode
bluetoothd_path: /usr/lib/bluetooth/bluetoothd
Event type: LOG
Management API Collector
Accesses the kernel Bluetooth management interface and debugfs.
Three strategies:
btmgmt --monitor-- kernel management events (connections, power state)- Sysfs polling -- reads
/sys/kernel/debug/bluetooth/hci*/for controller internals (features, manufacturer, HCI version, connection parameters) - USB power tracking -- monitors adapter USB power state for hub failure detection
Config:
collectors:
mgmt:
enabled: true
sysfs_poll_s: 5.0 # debugfs poll interval
Event types: MGMT_EVT, SYSFS_SNAPSHOT, SYSFS_CHANGE
Requires root for btmgmt socket and debugfs access.
PipeWire / PulseAudio Collector
Monitors the audio routing layer between BlueZ and applications.
PipeWire mode (preferred): Parses pw-dump --monitor --no-colors JSON streams. Detects bluetooth node creation/destruction, codec negotiation, state changes, buffer xruns.
PulseAudio fallback: Parses pactl subscribe for sink/source/card events.
Features:
- Bluetooth node detection via
device.api=bluez5property - Codec quality ranking (enriched via A2DP codec module)
- Buffer xrun (underrun/overrun) detection via
clock.xrun-count - Audio format extraction: sample rate, format, channels
Event types: PW_ADDED, PW_CHANGED, PW_REMOVED, PW_XRUN, PA_NEW, PA_CHANGE, PA_REMOVE
No root required. Auto-detects available audio system.
eBPF Kernel Tracing Collector
Attaches eBPF programs to kernel Bluetooth tracepoints for zero-overhead in-kernel event capture.
Tracepoints:
bluetooth:hci_send_frame-- HCI frame leaving host to controllerbluetooth:hci_recv_frame-- HCI frame arriving from controller
What it adds over btmon:
- Nanosecond kernel timestamps (CLOCK_MONOTONIC)
- Per-process attribution (PID + process name -- which process triggered each BT operation)
- ACL/SCO/ISO bandwidth aggregation (frame counts and byte totals)
- No text parsing overhead -- structured data via perf ring buffer
Two backends:
- BCC (preferred): Python BPF bindings, structured perf buffer output
- bpftrace (fallback): Subprocess with text parsing
High-frequency frames (ACL, SCO, ISO) are aggregated into periodic bandwidth summaries instead of individual events.
Config:
collectors:
ebpf:
enabled: true # enabled by default, gracefully skips if not root
mock_data: false # set true for synthetic events without root
Requires: root (or CAP_BPF + CAP_PERFMON), kernel 5.15+, python3-bpfcc or bpftrace.
Event types: EBPF_HCI_CMD, EBPF_HCI_EVT, EBPF_ACL, EBPF_SCO, EBPF_ISO, EBPF_ACL_STATS
Kernel Driver Collector
Monitors the kernel Bluetooth subsystem via dmesg, ftrace, and module state.
Three strategies:
dmesg --followfiltered for bluetooth-related messages (firmware, USB, resets, errors)- Kernel ftrace tracepoints (opt-in:
bluetooth:hci_send_frame,bluetooth:hci_recv_frame) - Module state polling: monitors load/unload/refcount for bluetooth, btusb, btintel, btrtl, btbcm, btmtk, btmrvl, hci_uart, rfcomm, bnep, hidp
Config:
collectors:
kernel_trace:
enabled: true
ftrace: false # opt-in: enables bluetooth tracepoints
module_poll_s: 10.0 # module state poll interval
Event types: KERNEL_LOG, KERNEL_FW, KERNEL_USB_ENUM, KERNEL_RESET, KERNEL_ERROR, KERNEL_DISCONNECT, KERNEL_FTRACE, KERNEL_MODULE_SNAPSHOT, KERNEL_MODULE_LOAD, KERNEL_MODULE_UNLOAD, KERNEL_MODULE_CHANGE
Requires root for dmesg follow and ftrace. Module polling works without root.
Sysfs Collector
Polls /sys/class/bluetooth/, /sys/class/rfkill/, and USB device sysfs for adapter state.
Monitors:
- Adapter properties: address, type, bus, name, manufacturer
- rfkill state: soft block, hard block (filtered to bluetooth type)
- USB power: runtime_status (active/suspended/error), max power draw, vendor/product IDs
USB power state changes are the distinctive indicator of hub power failure vs. software disconnect.
Config:
collectors:
sysfs:
enabled: true
poll_s: 2.0 # poll interval in seconds
Event types: SYSFS_SNAPSHOT, SYSFS_CHANGE, ADAPTER_REMOVED, RFKILL_CHANGE, USB_POWER_CHANGE
No root required.
Udev Collector
Monitors Bluetooth hotplug events via udevadm monitor.
Actions tracked: add, remove, change, bind, unbind, online, offline
Event types: UDEV_ADD, UDEV_REMOVE, UDEV_CHANGE, UDEV_BIND, UDEV_UNBIND
No root required.
L2ping Collector
Active L2CAP round-trip time measurement for connected Classic BT devices.
Watches the D-Bus event stream for Connected=true events, then periodically pings each connected device. BLE devices are skipped (L2CAP echo not supported on BLE).
Config:
collectors:
l2ping:
enabled: true
poll_interval_s: 30 # seconds between ping rounds
ping_count: 5 # pings per measurement
ping_timeout_s: 2 # per-ping timeout
rtt_warn_ms: 50 # WARN if avg RTT above this
rtt_error_ms: 150 # ERROR if avg RTT above this
Event types: L2PING_RTT, L2PING_TIMEOUT, L2PING_FAILED
Battery Collector
Polls org.bluez.Battery1 for battery percentage on connected devices.
Two modes:
- Polled: reads
Battery1.Percentageevery N seconds (suppresses unchanged values) - Reactive: watches
PropertiesChangedonorg.bluez.Battery1
Config:
collectors:
battery:
enabled: true
poll_interval_s: 60
low_battery_warn: 20 # WARN below this percentage
low_battery_error: 10 # ERROR below this percentage
Event type: BATTERY_LEVEL
Enrichment Modules
Static lookup tables applied during event processing. No network calls, no runtime dependencies beyond the BT Core Spec.
HCI Error Codes
67 codes from Bluetooth Core Spec 5.4, Vol 1, Part F. Each entry includes: name, description, likely cause, suggested action.
from blutruth.enrichment.hci_codes import decode_hci_error
info = decode_hci_error(0x08)
# {"name": "CONNECTION_TIMEOUT", "description": "...", "cause": "...", "action": "..."}
OUI Manufacturer Lookup
391 IEEE OUI prefixes mapped to manufacturer names. Covers >90% of real-world Bluetooth addresses.
from blutruth.enrichment.oui import enrich_oui
enrich_oui("00:17:F2:AA:BB:CC") # "Apple"
LMP Feature Decoder
55 page-0 features, 4 page-1, 11 page-2 bit positions from BT Core Spec Vol 2, Part C. Decodes the 8-byte feature bitmask from Read Remote Features Complete events.
from blutruth.enrichment.lmp_features import decode_lmp_features, summarize_capabilities
features = decode_lmp_features(0x875bffdbfe8fffff)
# ["3-slot packets", "5-slot packets", "encryption", "LE supported (ctrl)", "SSP", ...]
caps = summarize_capabilities(0x875bffdbfe8fffff)
# {"encryption": True, "le_supported": True, "ssp": True, "edr_2mbps": True, ...}
SMP Feature Decoder
Decodes Security Manager Protocol pairing exchanges from BLE traffic.
| Function | Decodes |
|---|---|
decode_io_capability(0x03) |
"NoInputNoOutput" |
decode_auth_req(0x0D) |
["bonding", "MITM", "SC"] |
decode_key_dist(0x07) |
["EncKey", "IdKey", "Sign"] |
predict_pairing_method(init_io, resp_io, sc) |
Pairing method from IO capabilities |
assess_security(io_cap, auth_req, sc) |
Security level assessment (high/medium/low) |
Pairing method prediction covers all 25 IO capability combinations with Secure Connections awareness. Security assessment explains why a configuration is weak.
GATT UUID Mapping
80 service UUIDs, 89 characteristic UUIDs, 12 descriptor UUIDs from Bluetooth SIG Assigned Numbers.
Includes standard services (Generic Access, Device Information, Battery, Heart Rate, HID, Audio Stream Control, etc.) and common vendor UUIDs (Apple, Google Fast Pair, Bose, Samsung, Xiaomi, Fitbit, Sony, etc.).
from blutruth.enrichment.gatt_uuids import service_name, uuid_name, is_vendor_uuid
service_name("180f") # "Battery Service"
service_name("0000180f-0000-1000-8000-00805f9b34fb") # "Battery Service"
uuid_name("2a19") # ("characteristic", "Battery Level")
is_vendor_uuid("fe2c") # True (Google Fast Pair)
USB Adapter Database
21 adapters from Intel, Realtek, Qualcomm/Atheros, Broadcom, MediaTek, CSR, TP-Link, ASUS. Each entry includes: vendor, name, chipset, driver, BT version, notes.
5 known-issue patterns covering: CSR8510 clone detection, Realtek firmware dependency, Intel firmware dependency, MediaTek early driver issues.
from blutruth.enrichment.usb_ids import lookup_adapter, known_issues, adapter_summary
adapter_summary(0x8087, 0x0029) # "Intel AX200 (Intel AX200, BT 5.0)"
known_issues(0x0a12, 0x0001) # [{"issue": "CSR8510 clone detection", ...}, ...]
A2DP Codec Decoder
Decodes codec configuration bytes from AVDTP exchanges and BlueZ MediaTransport1 properties.
| Function | Decodes |
|---|---|
decode_codec_id(type, vid, cid) |
Codec name (SBC, AAC, aptX, aptX HD, LDAC, LC3, Samsung Scalable) |
decode_sbc_config(bytes) |
Sampling freq, channel mode, block length, subbands, allocation, bitpool, estimated bitrate |
decode_aac_config(bytes) |
Object type, sampling freq, channels, VBR flag, bitrate |
decode_ldac_config(bytes) |
Sampling freq, channel mode |
decode_aptx_config(bytes, hd) |
Sampling freq, channel mode, nominal bitrate |
codec_quality_rank(name) |
1-5 quality ranking (SBC=1, LDAC=5) |
is_codec_downgrade(from, to) |
True if switching to a lower-quality codec |
Pattern Rules
33 YAML rules across 4 categories. Rules fire PATTERN_MATCH events when trigger sequences are detected in the live event stream. Each rule includes a description explaining what the pattern means and an action with specific remediation steps.
Rules support: multi-trigger sequences, time windows, same-device constraints, condition matching with dot-notation (changed.State), count expansion (repeat N identical triggers), and negate triggers (fire if event does NOT appear within window).
User rules in ~/.blutruth/rules/*.yaml override built-in rules by ID.
Security Rules (12 rules)
| Rule | Detects | Severity |
|---|---|---|
knob_attack_critical |
Encryption key negotiated below 7 bytes (CVE-2019-9506) | SUSPICIOUS |
knob_attack_possible |
Encryption key below 16 bytes | WARN |
auth_failure_unknown_device |
Auth failure from device not in pairing database | WARN |
controller_throttled_auth |
Controller rate-limiting due to Repeated Attempts (0x17) | SUSPICIOUS |
mic_failure_disconnect |
Message Integrity Check failure -- possible active attack | SUSPICIOUS |
encryption_rejected |
Remote device rejected encryption mode | WARN |
insufficient_security_disconnect |
Disconnect due to insufficient security level | WARN |
ssp_noio_pairing |
NoInputNoOutput SSP pairing (no MITM protection) | WARN |
unexpected_just_works_pairing |
Just Works completed when device has display/keyboard | SUSPICIOUS |
device_impersonation |
Same name from different address in short window | SUSPICIOUS |
scan_flood |
50+ LE advertising reports in 10 seconds from one address | WARN |
bias_indicator |
BIAS attack pattern: role switch after auth (CVE-2020-10135) | SUSPICIOUS |
Connection Rules (8 rules)
| Rule | Detects | Severity |
|---|---|---|
auth_loop |
3+ auth failures for same device in 5s | ERROR |
silent_reconnect |
Connection Timeout disconnect followed by reconnect within 30s | WARN |
lmp_timeout_disconnect |
LMP Response Timeout -- firmware hang or severe RF | ERROR |
repeated_timeouts |
3+ connection timeouts in 2 minutes | ERROR |
reconnect_flood |
Connect/disconnect/connect cycle in 10 seconds | ERROR |
page_timeout_on_connect |
Page Timeout on connection attempt | WARN |
hci_disconnect_plus_dbus |
HCI + D-Bus disconnect correlation (baseline check) | INFO |
usb_hub_power_failure |
USB power state change followed by adapter removal | ERROR |
Audio Rules (5 rules)
| Rule | Detects | Severity |
|---|---|---|
a2dp_codec_downgrade_to_sbc |
A2DP fell back to SBC (lowest quality codec) | WARN |
a2dp_codec_change |
A2DP codec changed mid-session (causes dropout) | INFO |
sco_connection_fail |
SCO/eSCO disconnect -- voice audio lost | ERROR |
a2dp_suspend_resume_flood |
Rapid A2DP transport state cycling (buffer underrun) | WARN |
audio_disconnect_on_rssi_drop |
Audio disconnect following RSSI drop -- confirms RF cause | WARN |
Profile Lifecycle Rules (8 rules)
| Rule | Detects | Severity |
|---|---|---|
a2dp_transport_stuck_pending |
A2DP transport in pending >10s -- audio daemon may have failed | WARN |
a2dp_no_transport_after_connect |
Device connected but no A2DP transport created in 15s | INFO |
a2dp_transport_rapid_cycle |
4+ transport state changes in 5s -- PipeWire/PulseAudio conflict | WARN |
hfp_sco_setup_timeout |
No SCO link after HFP connect -- voice may not work | WARN |
hfp_sco_repeated_failure |
2+ SCO connection failures -- voice calls broken | ERROR |
avrcp_player_not_registered |
A2DP active but no AVRCP player -- media controls won't work | INFO |
profile_connect_without_services |
Profile activity before ServicesResolved -- BlueZ race condition | WARN |
all_profiles_disconnected |
All profiles removed but ACL still up -- zombie connection | INFO |
Correlation Engine
Runs as a background task, periodically scanning recent events and grouping them by (device_addr, time_window). Events from multiple sources that occur within the correlation window get a shared group_id.
Config:
correlation:
time_window_ms: 100 # events within this window are candidates
batch_interval_s: 2.0 # how often the correlation pass runs
rules_path: ~/.blutruth/rules/ # user rule packs (override built-ins by ID)
The rule engine subscribes to the event bus separately and maintains per-device partial match state. Negate triggers fire when an expected event does NOT appear within the time window (e.g., "encryption change did not follow authentication" is suspicious).
Event Schema
Every event from every source is normalized into the same 19-field structure before storage:
| Field | Type | Description |
|---|---|---|
schema_version |
int | Schema version (currently 1) |
source_version |
str | Collector version (e.g., hci-collector-0.2.0) |
parser_version |
str | Parser version |
event_id |
str | UUID (16 hex chars) |
ts_mono_us |
int | Microseconds since process start (primary sort key) |
ts_wall |
str | ISO-8601 wall time (display only) |
source |
str | HCI, DBUS, DAEMON, KERNEL, SYSFS, UDEV, PIPEWIRE, EBPF_KERNEL, RUNTIME |
severity |
str | DEBUG, INFO, WARN, ERROR, SUSPICIOUS |
stage |
str | DISCOVERY, CONNECTION, HANDSHAKE, DATA, AUDIO, TEARDOWN |
event_type |
str | Normalized type (DISCONNECT, DBUS_PROP, GATT_DISCOVERY, ...) |
adapter |
str | hci0, etc. |
device_addr |
str | AA:BB:CC:DD:EE:FF |
device_name |
str | Friendly name |
summary |
str | Human-readable one-liner |
raw_json |
dict | Full structured payload (all extracted fields live here) |
raw |
str | Original unparsed line/bytes |
group_id |
int | Correlation group (null until correlated) |
tags |
list/dict | Free-form tags |
annotations |
dict | User scratch space |
Schema stability is a hard constraint. Don't rename or remove fields. Use annotations, tags, or raw_json for new data.
Notable raw_json fields
| Field | Source | Description |
|---|---|---|
rssi_dbm |
HCI | Signal strength (dBm) |
reason_code / reason_name |
HCI | Disconnect reason with plain-English decode |
handle |
HCI | Connection handle (mapped to device_addr) |
key_size / knob_risk |
HCI | Encryption key size; KNOB attack indicator |
encryption_enabled |
HCI | True/false from Encryption Change |
io_capability |
HCI | SSP IO capability type |
lmp_features / lmp_page |
HCI | Decoded LMP feature list |
smp_io_capability / smp_auth_flags |
HCI | SMP pairing parameters |
smp_max_key_size |
HCI | Maximum SMP encryption key size |
cig_id / cis_id / big_handle |
HCI | LE Audio isochronous parameters |
sdu_interval_us / iso_interval |
HCI | ISO timing parameters |
sco_codec |
HCI | SCO voice codec (CVSD, mSBC) |
num_completed_packets |
HCI | ACL completed packet count |
codec_name |
DBUS | A2DP codec (SBC/AAC/aptX/LDAC) |
interface / changed |
DBUS | D-Bus property change details |
services / characteristics |
DBUS | GATT tree from discovery |
ts_kernel_ns / pid / comm |
EBPF | Kernel timestamp, process attribution |
acl_bytes_tx / acl_bytes_rx |
EBPF | ACL bandwidth counters |
xrun |
PIPEWIRE | Buffer underrun/overrun details |
codec_info |
PIPEWIRE | Codec with quality rank |
rtt_avg_ms / rtt_min_ms / rtt_max_ms |
L2PING | Round-trip time statistics |
Storage
Dual-write to SQLite (indexed, queryable) and JSONL (portable, append-only).
SQLite tables:
events-- all events with indexes on time, device+time, source+time, severity, group_iddevices-- canonical device registry with manufacturer, first/last seenevent_groups-- correlation group membership with role (PRIMARY/CORRELATED)sessions-- collection session metadata
Config:
storage:
sqlite_path: ~/.blutruth/events.db
jsonl_path: ~/.blutruth/events.jsonl
retention_days: 30 # auto-delete old events (0 = disabled)
size_warn_mb: 500 # warn at startup if storage exceeds this
SQLite runs in WAL mode with batched inserts (100 events per batch, 250ms flush interval). Writes run in a thread executor to avoid blocking the asyncio event loop.
JSONL is line-buffered and append-only. Attach it to a bug report.
Both can be rolled (archived to timestamped backup) or deleted via the web API.
Configuration Reference
~/.blutruth/config.yaml -- auto-created on first run. Hot-reloads within ~1 second (inotify via watchfiles, polling fallback). Config changes restart only affected collectors; bus, storage, and correlation continue uninterrupted.
Validated on every load: negative time windows, zero intervals, invalid ports, and other nonsensical values log warnings.
listen:
host: 127.0.0.1
port: 8484
storage:
sqlite_path: ~/.blutruth/events.db
jsonl_path: ~/.blutruth/events.jsonl
retention_days: 30
size_warn_mb: 500
collectors:
hci:
enabled: true
rssi_warn_dbm: -75
rssi_error_dbm: -85
dbus:
enabled: true
journalctl:
enabled: true
unit: bluetooth
format: json
advanced_bluetoothd:
enabled: false
bluetoothd_path: /usr/lib/bluetooth/bluetoothd
mgmt:
enabled: true
sysfs_poll_s: 5.0
pipewire:
enabled: true
kernel_trace:
enabled: true
ftrace: false
module_poll_s: 10.0
sysfs:
enabled: true
poll_s: 2.0
udev:
enabled: true
ebpf:
enabled: true
mock_data: false
l2ping:
enabled: true
poll_interval_s: 30
ping_count: 5
ping_timeout_s: 2
rtt_warn_ms: 50
rtt_error_ms: 150
battery:
enabled: true
poll_interval_s: 60
low_battery_warn: 20
low_battery_error: 10
gatt:
read_characteristics: true
ubertooth:
enabled: true
mock_data: false
ble_sniffer:
enabled: true
mock_data: false
correlation:
time_window_ms: 100
batch_interval_s: 2.0
rules_path: ~/.blutruth/rules/
ui:
live_mode_default: true
fallback_refresh_seconds: 2
max_rows: 500
security:
local_only: true
Diagnosing Common Problems
USB hub power failure
SYSFS INFO USB BT adapter hci0: Realtek [0bda:b00a] power=500mA status=active
SYSFS WARN USB adapter hci0 power: 'active' -> 'suspended'
SYSFS WARN ADAPTER_REMOVED: hci0 [7C:10:C9:75:8D:37]
The suspended before REMOVED is the tell. Software disconnects and rfkill blocks don't produce USB power state changes. This sequence is distinctive of power starvation.
RF / antenna issues
blutruth history <addr> shows disconnect reason patterns across sessions. CONNECTION_TIMEOUT (0x08) and LMP_RESPONSE_TIMEOUT (0x22) repeating across multiple sessions points to RF. L2ping RTT trends confirm latency issues.
Security anomalies
knob_risk: HIGH-- encryption key below 7 bytes, likely KNOB attackio_capability: NoInputNoOutputwhen device previously usedDisplayYesNo-- SSP downgradeSUSPICIOUSseverity events from security rules (BIAS indicator, MIC failure, impersonation)
Audio quality drops
a2dp_codec_downgrade_to_sbc-- codec negotiation fell back to lowest qualityPW_XRUNevents -- PipeWire buffer underruns causing glitchessco_connection_fail-- HFP voice link failed (mSBC/CVSD negotiation issue)a2dp_transport_stuck_pending-- PipeWire didn't start streaming
Not Supported (Without Hardware)
Two capabilities require dedicated radio hardware and are currently mock-only:
Ubertooth One (~$150) -- Classic BT Air-Level Sniffing
Captures raw BR/EDR packets over the air between any two devices (not just your adapter). Sees: LAP/UAP/access codes, piconet hopping sequences, AFH channel maps, timing anomalies, devices rejected at RF level. Requires an Ubertooth One dongle.
The collector stub exists (blutruth/collectors/ubertooth.py) with full capability documentation. Set mock_data: true in config to emit synthetic events for pipeline testing.
nRF BLE Sniffer (~$10-15) -- BLE Air-Level Sniffing
Captures BLE advertising and connection packets between other devices. Sees: connection parameter negotiation from outside, BLE devices your adapter never responded to, advertising/connection interval verification, pairing failures before HCI involvement. Works with any Nordic nRF52840 dongle, Adafruit nRF52840, or Micro:bit v1 with btlejack firmware.
The collector stub exists (blutruth/collectors/ble_sniffer.py). Set mock_data: true for testing.
Both collectors are registered in the runtime and will activate when hardware is present and tools are installed. No code changes needed -- just plug in the hardware.
Architecture
Collectors (async, one per stack layer)
| publish Event objects
v
EventBus (in-process fan-out pub/sub, best-effort, drops on slow subscribers)
|
+---> Runtime._writer_loop (stop-event drain, concurrent writes)
| +---> SqliteSink (batched inserts, WAL mode, thread executor)
| +---> JsonlSink (line-buffered append)
|
+---> CorrelationEngine (background, time-windowed group_id linking)
+---> RuleEngine (YAML pattern rules -> PATTERN_MATCH events)
Design Principles
- Correlation is the differentiator. Individual tools exist. The value is connecting events across layers with a shared
group_id. - Schema stability is a hard constraint. The Python and Rust implementations share the same database and event format. Add with defaults; don't rename or remove fields.
- Annotations over schema changes. Use
annotations,tags, orraw_jsonfor new data during debugging. - Collectors declare capabilities. Each collector exposes its root requirements and exclusive resources. The runtime checks before starting.
- EventBus is best-effort. Slow subscribers drop events (logged every 100 drops). The writer loop uses
max_queue=10000. The daemon stays alive under load. - Graceful degradation. Collectors that can't start (no root, no hardware, missing tool) emit a notice and do nothing. They don't crash the daemon.
Tests
321 tests covering events, bus, config, collectors, storage, correlation, enrichment, and rules.
pytest # run all
pytest tests/test_foo.py # single module
pytest -x # stop on first failure
CI runs on Python 3.11, 3.12, 3.13 via GitHub Actions.
Design Docs
2600/ -- architecture decisions, HCI event taxonomy, collector design notes, session logs.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file blutruth-0.2.0.tar.gz.
File metadata
- Download URL: blutruth-0.2.0.tar.gz
- Upload date:
- Size: 180.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fc22070eddaf7a2ad92a88f04a022e584eae5c58ee3d18354a2ab4e16f98f4f5
|
|
| MD5 |
96a339c39b86429680fcecb342d1dc60
|
|
| BLAKE2b-256 |
bc2224ecb5fd6ffce718d32896d5011efa7d85f12939d2aaaa460634285d0674
|
File details
Details for the file blutruth-0.2.0-py3-none-any.whl.
File metadata
- Download URL: blutruth-0.2.0-py3-none-any.whl
- Upload date:
- Size: 160.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2a45ed1d8dc8d4f3424ede495405f0ede338d771494fa4ce3e338273481d76f7
|
|
| MD5 |
9551f629ad36bad7860cd09d88c95f83
|
|
| BLAKE2b-256 |
079f9b66f1285918f7dcaf3daeb9369f80b624ef74814179c536c2859c8c0b6a
|