EmbeddedPathashala · BLE Mesh Series
How an unprovisioned device joins a mesh network — and how to do it yourself using meshctl and mesh-cfgclient.
Key Concepts in This Post
From Unprovisioned Device to Mesh Node
In Bluetooth Mesh, joining a network is called provisioning — not pairing or bonding. It is a fundamentally different process: instead of two devices linking directly, a Provisioner (typically a smartphone or a Linux gateway running meshctl) invites a device into the network, assigns it a unicast address, and hands it the keys it needs to participate.
Once provisioned, the device is a node and can send/receive mesh messages. If a node is later removed from the network, it returns to being an unprovisioned device.
🔌 Devices vs Nodes
- Cannot send or receive mesh messages
- Broadcasts Unprovisioned Device Beacon
- Has no unicast address yet
- Has no network or application keys
- Waiting to be invited by a Provisioner
- Full mesh participant
- Has a unique 16-bit unicast address
- Holds NetKey(s), AppKey(s), DevKey
- Can send, receive, and relay messages
- Managed by a Configuration Client
Shared by all nodes in a subnet. Used to encrypt and authenticate at the Network Layer. A relay node only needs the NetKey to forward messages — it never sees the payload.
Shared by nodes that belong to the same application group (e.g., all lights). Used to encrypt payload at the Upper Transport Layer. Bound to a NetKey.
Unique per node. Assigned during provisioning. Used by the Configuration Client to securely communicate with each individual node — e.g., to push new NetKeys or AppKeys.
🚀 Provisioning Flow — Step by Step
The unprovisioned device broadcasts a Mesh Beacon on BLE advertising channels. The beacon contains the device UUID (128-bit) and optional OOB info flags.
The Provisioner (e.g., meshctl) scans, finds the UUID, and sends a Provisioning Invite PDU. It also sets the attention timer — the device flashes, beeps, or vibrates to confirm identity.
Device and Provisioner exchange ECDH public keys to establish a shared secret. This prevents eavesdroppers from obtaining the provisioning data exchanged in the next steps.
Device is authenticated using an OOB method: No OOB (skip), Static OOB (fixed code), Output OOB (device blinks a number), or Input OOB (user types a number into device). This prevents rogue devices joining.
Provisioner sends the device: NetKey, NetKey Index, IV Index, a Unicast Address, and IV Update flags. Device derives its DevKey from the shared secret. Device is now a node.
During provisioning, the Provisioner sets a non-zero attention timer (in seconds) on the device. While this timer is running, the device identifies itself using any capability it has — flashing an LED, making a sound, or vibrating. When the timer expires, the device stops. This allows a Provisioner to confirm it is talking to the right physical device in a room full of identical sensors.
🔧 BlueZ: Full Provisioning Walkthrough
# Stop bluetoothd (cannot run alongside bluetooth-meshd)
sudo systemctl stop bluetooth
# Start the BlueZ mesh daemon
sudo bluetooth-meshd --nodetach --debug
meshctl
# Inside meshctl interactive prompt:
[meshctl]# create
# Creates a new mesh network.
# BlueZ generates a primary NetKey and assigns
# this machine the Provisioner role with unicast addr 0x0001.
# Network config stored in: ~/.config/meshctl/
# Alternatively, load an existing network:
[meshctl]# import /tmp/mesh-network.json
# Start scanning for unprovisioned device beacons
[meshctl]# discover-unprovisioned on
# Expected output (when a device advertises its beacon):
# Unprovisioned device:
# UUID = aabbccddeeff00112233445566778899
# OOB = 0x0000
# Stop scanning
[meshctl]# discover-unprovisioned off
# Provision the device using its UUID
# BlueZ will run ECDH key exchange + No-OOB auth by default
[meshctl]# provision aabbccddeeff00112233445566778899
# Expected output on success:
# Provisioning node aabbccddeeff00112233445566778899
# Provisioning complete
# Node 0x0002 added
# List all nodes now in the network
[meshctl]# node-list
# Unicast | UUID | DevKey
# 0x0001 | <provisioner uuid> | ...
# 0x0002 | aabbccddeeff00112233445566778899 | ...
After provisioning, the node only has the NetKey. You must push an AppKey and bind it to the node’s models before it can process application messages.
# mesh-cfgclient is the BlueZ configuration client tool
# It communicates with nodes over Foundation Model messages
mesh-cfgclient
# Inside mesh-cfgclient interactive prompt:
# Target node 0x0002 (the newly provisioned node)
[mesh-cfgclient]# target 0002
# Get the node composition data (element count, models)
[mesh-cfgclient]# get-composition 0
# Returns: CID, PID, VID, element list, model IDs per element
# Add AppKey index 0 bound to NetKey index 0
[mesh-cfgclient]# appkey-add 0 0
# Arguments: <net_key_index> <app_key_index>
# Bind AppKey 0 to the Generic OnOff Server model (0x1000)
# on element index 0 of node 0x0002
[mesh-cfgclient]# bind 0002 0 0 1000
# Arguments: <node_addr> <elem_idx> <app_key_idx> <model_id>
# Set publish address: node 0x0002 publishes to group 0xC000
[mesh-cfgclient]# pub-set 0002 C000 0 0 0 1000
# Arguments: <node> <pub_addr> <app_key_idx> <period> <retransmit> <model_id>
# Add subscription: node 0x0002 subscribes to group 0xC000
[mesh-cfgclient]# sub-add 0002 0 C000 1000
# Arguments: <node> <elem_idx> <sub_addr> <model_id>
[mesh-cfgclient]# quit
A Provisioner adds unprovisioned devices to the network (meshctl). A Configuration Client configures already-provisioned nodes — distributing AppKeys, binding models, and setting publish/subscribe addresses (mesh-cfgclient). In practice, the same device (your Linux machine or smartphone) acts as both. They communicate using different key types: provisioning uses ECDH, while configuration uses the node’s unique DevKey.
A single physical device can be provisioned into more than one mesh network. Each provisioning creates a separate instance with its own unicast address and device key. This is useful for a device that bridges two separate mesh networks, or a contractor’s phone that needs to manage multiple buildings. Each instance is completely isolated by its own set of keys and addresses.
🏆 Series Recap
8-layer stack from BLE Core to Model Layer. Two separate encryption keys (NetKey, AppKey). Works on any BLE chip.
PDU encapsulation, AES-CCM encryption at two layers, SAR in lower transport, Bearer types, BlueZ D-Bus API.
Managed-flood relay, TTL countdown, message cache (SRC+SEQ), subnets with NetKey isolation, IV Index, BlueZ meshd setup.
ECDH key exchange, OOB authentication, attention timer, meshctl provisioning, mesh-cfgclient AppKey distribution and model binding.
You Now Understand Bluetooth Mesh!
You have covered the full BLE Mesh stack — from radio to application model — and seen how BlueZ implements it on Linux.
Next up on EmbeddedPathashala: Writing a Custom BlueZ Mesh Application from Scratch — implementing a Generic OnOff Server node in Python using the D-Bus API.
