Skip to main content

Command Palette

Search for a command to run...

Linux Bluetooth Stack Explained: How BlueZ Connects Applications to the Controller

Updated

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 daemon

  • bluetoothctl — interactive management tool

  • btmgmt — management API client

  • btmon — HCI monitor

  • obexd — OBEX daemon for file transfer

  • D-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:

  • bluetoothd owns userspace Bluetooth state — paired devices, profiles, GATT database, bonding keys

  • The kernel owns the actual controller state — hci_dev, connection table (hci_conn), command queue, power state

  • The 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:

  • bluetoothd decides whether to pair (trusted devices, agent confirmation)

  • The actual SMP pairing protocol (feature exchange, key generation, authentication) runs in kernel smp.c for BLE

  • For 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 params

  • device.c — manages paired and discovered devices, connection state machine

  • profile.c — registers and manages Classic BT profiles (A2DP, HFP, HID)

  • gatt-database.c — hosts the local GATT server for BLE peripherals

  • agent.c — delegates pairing UI to your application via Agent1 D-Bus interface

  • plugins/ — 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_uart

  • The 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.

295 views

Bluez Internals

Part 1 of 1

A practitioner's deep dive into the Linux Bluetooth stack — BlueZ architecture, kernel drivers, debugging tools, profile implementation, and real-world engineering from 15+ years of embedded Linux and Bluetooth development. Written for systems engineers, not beginners.