Linux Bluetooth Stack Explained: How BlueZ Connects Applications to the Controller
Published on crazydaks.hashnode.dev | Series: BlueZ Internals
Introduction
One of the most common misconceptions among developers new to Linux Bluetooth is that BlueZ is the entire Bluetooth stack.
It is not.
Others believe Bluetooth starts and ends with bluetoothctl or maybe btmgmt.
It does not.
When a Bluetooth keyboard connects to your Raspberry Pi, when a BLE sensor starts advertising, or when a headset streams audio through A2DP — multiple software layers cooperate to make that happen.
Applications never talk directly to the Bluetooth controller.
The controller never talks directly to applications.
BlueZ sits in the middle, translating application requests into kernel operations and Bluetooth protocol exchanges.
Understanding this architecture is the difference between randomly trying commands and systematically debugging Bluetooth problems.
In this article we will walk through every layer of the Linux Bluetooth stack — then follow two real operations from application all the way down to the controller and back.
Who this Series is For
This series is written for embedded Linux and systems engineers who work with Bluetooth on Linux — not for application developers looking for a quick bluetoothctl tutorial. If you have ever wondered what actually happens inside bluetoothd, why btmon output looks the way it does, or how a pairing failure at the SMP layer differs from one at the D-Bus layer — this series is for you.
The Complete Linux Bluetooth Stack
Every Bluetooth operation travels through some variation of this path. The exact path depends on whether you are dealing with Classic Bluetooth, BLE, audio, HID, or LE Audio — but the overall layering remains the same.
What Exactly Is BlueZ?
Many developers conotate BlueZ with bluetoothd or bluetooth daemon. In reality BlueZ is a larger project.
The BlueZ project contains:
bluetoothd— the central daemonbluetoothctl— interactive management toolbtmgmt— management API clientbtmon— HCI monitorobexd— OBEX daemon for file transferD-Bus API definitions
Profile implementations (A2DP, HFP, HID, GATT)
Testing tools and scripts
The most important component is bluetoothd. This daemon acts as the central coordinator for all Bluetooth activity on a Linux system.
But here is the nuance most articles miss: the Linux kernel Bluetooth subsystem (net/bluetooth) is not part of BlueZ. It lives separately in the kernel tree, maintained by the same core team (Marcel Holtmann, Johan Hedberg, Luiz Augusto von Dentz) but released independently. The two co-evolved — BlueZ engineers contributed net/bluetooth to the mainline kernel — but today they are separate codebases.
So "the Linux Bluetooth stack" is really:
net/bluetooth → Linux kernel tree (kernel.org)
BlueZ → bluez.org / github.com/bluez/bluez
Same maintainers. Two repos. One stack.
Why Does BlueZ Exist?
A reasonable question: why not let applications talk directly to the controller?
The answer becomes obvious when multiple applications need Bluetooth simultaneously. Imagine three applications all trying to scan, pair, connect, and register GATT services directly with the controller at the same time. Chaos follows.
BlueZ provides the following — and each one is more nuanced than it first appears:
Centralized Userspace Coordination
bluetoothd serializes all application access through the kernel's Management Interface — but "one process owns everything" is an oversimplification. State is actually split across three layers:
bluetoothdowns userspace Bluetooth state — paired devices, profiles, GATT database, bonding keysThe kernel owns the actual controller state —
hci_dev, connection table (hci_conn), command queue, power stateThe controller firmware owns the radio state — link layer, encryption keys, connection handles
These three state machines must stay synchronized. The Management Interface exists precisely to enforce that synchronization.
Security Coordination
bluetoothd enforces pairing policies and persists bonding keys to disk, while the kernel SMP layer executes the pairing protocol and the controller handles actual AES-CCM encryption. Security responsibility is split across all three layers:
Pairing policies — split responsibility:
bluetoothddecides whether to pair (trusted devices, agent confirmation)The actual SMP pairing protocol (feature exchange, key generation, authentication) runs in kernel
smp.cfor BLEFor Classic BT, HCI pairing commands go to the controller which executes the pairing state machine in firmware
Here is the full distribution of security responsibilities — it may look overwhelming now but will become clear as we proceed through the series:
Bonding Storage
Bonding storage sounds like a simple file write — but is also split across three layers.
bluetoothd writes and reads bonding files from /var/lib/bluetooth/. The directory structure looks like this:
/var/lib/bluetooth/
└── AA:BB:CC:DD:EE:FF/ (adapter BD_ADDR)
└── 11:22:33:44:55:66/ (bonded device BD_ADDR)
└── info (LTK, IRK, link key stored here)
The kernel then loads those keys into the controller via HCI commands on every reconnection. The controller actually uses the key for encryption — bluetoothd never touches raw cryptographic operations.
State Tracking
State tracking is the most misunderstood aspect of bluetoothd. Its view of device state is a derived mirror — not the source of truth.
Critical point: if bluetoothd crashes mid-connection, the kernel and controller stay connected. The link is real regardless of what bluetoothd thinks. On restart, bluetoothd rebuilds its state by querying the kernel via the mgmt socket.
The disconnect event flow illustrates this clearly:
Controller LL state
↓ HCI Disconnection Complete event
hci_core destroys hci_conn struct
↓ mgmt event to bluetoothd
bluetoothd updates Device1.Connected = false
↓ D-Bus PropertiesChanged signal
Application sees device disconnected
Profile Management
bluetoothd handles profile registration and connection negotiation — but once a profile connection is established, the actual data transport often bypasses bluetoothd entirely.
The data path after profile setup is direct — A2DP audio flows kernel → PipeWire, HID reports flow kernel → /dev/input/eventX, bypassing bluetoothd entirely:
bluetoothd (profile setup + SDP registration)
↓ L2CAP channel established by kernel
kernel (data transport — AVDTP / RFCOMM / ATT)
↓ data flows directly
PipeWire / /dev/input/eventX / application
Shared Access
Multiple applications sharing one adapter without conflicts — this is entirely bluetoothd's responsibility through D-Bus serialization.
The last row is important — when your application crashes or disconnects from D-Bus, bluetoothd automatically cleans up any registrations it made (GATT services, profiles, scan requests). This is one of the strongest arguments for going through D-Bus rather than raw sockets.
Layer 1 — The D-Bus Interface
Applications communicate with BlueZ through D-Bus. This is the first layer most developers encounter.
bluetoothctl does not talk to the controller directly. It sends D-Bus method calls to bluetoothd. BlueZ exposes Bluetooth resources as a hierarchy of D-Bus objects:
/org/bluez ← root
/org/bluez/hci0 ← Adapter1
/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF ← Device1
/org/bluez/hci0/dev_.../service0001 ← GattService1
/org/bluez/hci0/dev_.../service0001/char0002 ← GattCharacteristic1
The adapter, each device, each GATT service and characteristic — all are D-Bus objects with a consistent introspectable API.
# Explore the full BlueZ object tree
busctl tree org.bluez
# Introspect a specific adapter
busctl introspect org.bluez /org/bluez/hci0
# Monitor all BlueZ D-Bus signals live
dbus-monitor --system "type='signal',sender='org.bluez'"
Layer 2 — Bluetooth daemon Internals
bluetoothd is the brain of the BlueZ stack — a single long-running process that manages all Bluetooth state in userspace.
Key internal components:
adapter.c— manages each HCI controller (hci0, hci1), powers up, sets LE paramsdevice.c— manages paired and discovered devices, connection state machineprofile.c— registers and manages Classic BT profiles (A2DP, HFP, HID)gatt-database.c— hosts the local GATT server for BLE peripheralsagent.c— delegates pairing UI to your application via Agent1 D-Bus interfaceplugins/— extend bluetoothd without modifying core
Configuration in /etc/bluetooth/main.conf:
[General]
ControllerMode = dual # le, bredr, or dual
Privacy = device # network or device mode RPA
Experimental = true # required for LE Audio, ISO sockets
[Policy]
AutoEnable = true # auto power-on adapters at startup
Run bluetoothd manually to see its full debug output:
sudo systemctl stop bluetooth
sudo bluetoothd -n -d 2>&1 | head -60
Layer 3 — The Management Interface
Between bluetoothd and the kernel sits the Management Interface (MGMT) — a layer most developers never notice but critical to understand.
Why It Was Introduced
Historically, tools such as hciconfig, hcitool, and hcidump interacted with controllers using raw HCI sockets. While this provided direct access to HCI commands and events, it also created a coordination problem. Multiple userspace applications could modify controller state independently, leaving bluetoothd and the kernel with an incomplete or inconsistent view of the adapter's actual state.
The Management Interface (MGMT) was introduced to provide a kernel-managed control plane for Bluetooth adapters. Instead of exposing low-level HCI operations, MGMT exposes higher-level operations such as powering adapters, starting discovery, pairing devices, and configuring privacy. This allows the kernel Bluetooth subsystem and bluetoothd to maintain a consistent view of adapter state while providing a stable management API to userspace.
How It Works
The mgmt socket exposes higher-level opcodes that map to HCI command sequences:
MGMT_OP_POWER_ON (0x0005) → HCI Reset + controller init
MGMT_OP_SET_LE (0x000D) → LE Set Event Mask + feature negotiation
MGMT_OP_START_DISCOVERY (0x0023) → LE Set Scan Params + LE Set Scan Enable
MGMT_OP_PAIR_DEVICE (0x0019) → full SMP pairing sequence
bluetoothd never constructs raw HCI packets — it speaks management opcodes and lets hci_core handle the HCI translation. btmgmt talks to this socket directly, bypassing bluetoothd:
btmgmt info # controller status and feature flags
btmgmt power on # power via mgmt socket, not bluetoothd
btmgmt le on # enable LE
btmgmt privacy on # enable RPA
Layer 4 — The Linux Bluetooth Kernel Subsystem
Below the management interface is net/bluetooth in the kernel. Key components:
hci_core (net/bluetooth/hci_core.c) Central component. Manages HCI device registration, command queuing, connection tracking, and event routing. Every controller is represented as an hci_dev struct.
L2CAP (net/bluetooth/l2cap_core.c) Logical Link Control and Adaptation Protocol. Transport layer for both Classic BT and BLE — ATT, RFCOMM, and SMP all run over L2CAP channels.
SMP (net/bluetooth/smp.c) Security Manager Protocol. BLE pairing, key exchange, and bonding handled entirely in the kernel — not in bluetoothd.
RFCOMM (net/bluetooth/rfcomm/) Serial-port emulation. Used by Classic profiles like HFP and SPP.
mgmt (net/bluetooth/mgmt.c) Kernel-side Management Interface — receives opcodes from bluetoothd and translates them to HCI operations.
# list adapters
ls /sys/class/bluetooth/
# correct way to get adapter details on RPi
btmgmt info
hciconfig -a
Layer 5 — Transport Drivers
The transport driver is the glue between hci_core and physical hardware.
btusb — for USB Bluetooth adapters and most built-in Intel/Qualcomm controllers:
Bulk USB endpoints for HCI commands and ACL data
Isochronous USB endpoints for SCO and ISO audio
Firmware download on startup for chips that require it (Intel, MediaTek, Qualcomm)
hci_uart — for UART-attached controllers (embedded systems, RPi built-in):
Multiple line disciplines: H4, BCSP, Three-wire (LL)
Manages flow control at the UART layer
Standard on mobile and embedded platforms
On RPi Zero the built-in BCM43438 is UART-attached and uses hci_uart:
dmesg | grep -i bluetooth
dmesg | grep -i bcm
Layer 6 — The Bluetooth Controller
At the bottom sits the radio hardware. On RPi Zero this is the BCM43438, which handles:
RF transmission and reception
Link layer advertising, scanning, connection state machines
AES-CCM encryption acceleration for BLE
Its own firmware (downloaded by the driver at startup)
The controller speaks HCI — a standardized protocol defined in the Bluetooth spec. This is why the same hci_core works with controllers from every vendor.
HCI has four packet types:
HCI Command (host → controller) 0x01
HCI ACL Data (bidirectional) 0x02
HCI SCO Data (bidirectional) 0x03
HCI Event (controller → host) 0x04
Every Bluetooth operation reduces to sequences of these four packet types. btmon makes them visible in real time.
Multi-Adapter Systems
Many production systems have more than one controller. On RPi this is easy to demonstrate — plug in a USB Bluetooth dongle alongside the built-in BCM43438:
hci0 ← built-in BCM43438 (UART)
hci1 ← USB Bluetooth dongle
Each adapter has its own hci_dev in the kernel and its own path in BlueZ (/org/bluez/hci0, /org/bluez/hci1). You can run independent operations on each — for example hci0 for BLE scanning while hci1 handles a Classic A2DP connection.
btmgmt info # shows all adapters with capabilities
Tracing a Real Operation: bluetoothctl scan on
Tracing a Real Operation: bluetoothctl connect
Where Debugging Tools Fit
One of the biggest mistakes engineers make is using the wrong tool at the wrong layer:
When a problem occurs, identify the layer first. Then pick the right tool.
What BlueZ Does Not Do
BlueZ is not:
Controller firmware — that lives on the chip
The kernel transport driver — that is
btusb/hci_uartThe radio hardware
Understanding these boundaries tells you immediately which project owns a bug:
Summary
The Linux Bluetooth stack is six layers:
Application → D-Bus → bluetoothd → Management Interface
→ hci_core → Transport Driver → Controller
Each layer has a single responsibility. Each can be observed independently with the right tool. BlueZ sits at the center — translating between the application world above and the kernel/hardware world below.
Once you understand these layers, Bluetooth debugging becomes a structured exercise in layer isolation rather than guesswork.
Next in this series: BlueZ Debugging Tools: Management API Based vs Raw HCI Sockets Based — A Practitioner's Guide
Part of the BlueZ Internals series on crazydaks.hashnode.dev
I am building a hands-on paid course series covering BlueZ from userspace tools to kernel driver internals — with live RPi Zero demos throughout.
What's Coming Next in This Series
BlueZ Debugging Tools: Management API vs Raw HCI Sockets — every tool explained, when old deprecated tools still win, real debugging workflows with btmon traces
btmon Masterclass — reading HCI traces, filtering, timing analysis
Inside bluetoothd — source code walkthrough of adapter.c, device.c, and the connection state machine
BLE Privacy and RPA in BlueZ — IRK, resolving lists, per-device privacy modes
I am building a hands-on paid course series covering BlueZ from userspace tools to kernel driver internals — with live Raspberry Pi Zero demos for every concept covered in this series.
To get notified when new articles and courses drop: → Follow this blog on Hashnode → Connect on LinkedIn: https://www.linkedin.com/in/aks-connectivity
If this article saved you debugging time, share it with your team.