Every 15 minutes
IRK (128-bit)
4 modes
2 + 2 new (4.2)
8.14 — Link Layer Privacy 1.2
Every time a BLE device broadcasts an advertising packet, it includes its Bluetooth address. If that address never changes, any device nearby — a phone, a laptop, or a dedicated sniffer — can passively log every packet and build a precise picture of where that device (and the person carrying it) has been. This is a real privacy problem for wearable devices.
BLE privacy addresses this by using rotating, pseudo-random addresses instead of the permanent hardware address. This file explains exactly how the mechanism works and introduces the White List — a power-saving filter that limits which devices the link layer will interact with.
The Fitness Band Tracking Problem — A Concrete Example
Imagine a user wearing a fitness band on their wrist. Every time the band needs to sync data with the user’s phone, it starts advertising — broadcasting packets on channels 37, 38, and 39 to announce its presence. These packets contain the band’s fixed Bluetooth address.
The problem is that anyone nearby — not just the paired phone — can receive these advertising packets. A mall, an office building, or any public space could have passive BLE scanners that log every address they hear along with a timestamp. By correlating the same fixed address appearing at different locations and times, a tracker can build a complete movement profile of the device owner.
The solution is to replace the fixed address with a pseudo-random address that changes regularly. A passive observer sees many different addresses and cannot tell which ones belong to the same device. Only the device’s paired partner — who holds a secret key — can resolve the random addresses back to the real identity.
Instead of advertising with its real hardware address, a privacy-enabled device uses a Resolvable Private Address (RPA). An RPA looks like a random Bluetooth address to any observer. However, it is not truly random — it is generated using a mathematical function that takes the device’s Identity Resolution Key (IRK) as input.
The IRK is a 128-bit secret key. It is exchanged between two devices during the pairing process and stored securely. Because the paired device also has the IRK, it can run the same function in reverse to check whether a given random address was generated by the device it knows. This is called address resolution.
Generates RPA using IRK
Advertises with RPA
addr: 7D:A3:F1:22:C8:B0
(RPA)
Resolves 7D:A3:F1… → AA:BB:CC:11:22:33
✅ Recognises fitness band
Sees only 7D:A3:F1…
❌ Cannot link to any person
8.14.1 — Address Resolution in the Controller (BLE 4.2)
In BLE 4.0 the generation and resolution of private addresses was done entirely in the Host — the application processor running the software stack. Every time the controller received an advertising packet with a random address, it had to wake up the Host so the Host could check whether the address belonged to a known bonded device.
In a busy environment with many BLE devices advertising nearby, this creates a lot of unnecessary Host wake-ups. Most of those devices are completely irrelevant to the application — they belong to strangers. All those wake-ups burn CPU time and battery on the Host just to sort through irrelevant devices.
BLE 4.2 moved address resolution down into the Controller (the Bluetooth chip). Now the controller handles the filtering itself. The Host only wakes up when the controller finds a packet from a known, bonded device. This is a significant power saving in environments with many BLE devices around.
For the controller to resolve addresses on its own, the host must first give it the necessary information. This information is called device identity information and is provided for each bonded device. A device identity contains:
The peer device’s real, permanent Bluetooth address. This is the address the host uses when referring to that device (not the RPA).
This device’s own Identity Resolution Key — used to generate this device’s own private addresses.
The paired device’s Identity Resolution Key — used to resolve incoming addresses that the peer generated.
The complete set of device identities for all bonded devices is called the Resolving List. The host maintains this list and pushes it to the controller. The controller uses it to check every incoming random address — if it can be resolved using any IRK in the list, the packet is from a known bonded device. The host can add or remove individual entries using HCI commands at any time.
There is one practical constraint: the controller chip has limited memory. If there are more bonded devices than the controller can store, the host may need to provide only a subset of the resolving list. The number of entries the controller can hold is reported via the HCI command HCI_LE_Read_Resolving_List_Size.
/* Managing the Resolving List via BlueZ HCI commands */
/* Add a bonded device to the controller's resolving list */
/* OGF=0x08, OCF=0x0027 = LE Add Device To Resolving List */
typedef struct {
uint8_t peer_addr_type; /* 0=public, 1=random static */
uint8_t peer_addr[6]; /* peer's identity address */
uint8_t peer_irk[16]; /* peer's IRK (128-bit) */
uint8_t local_irk[16]; /* local IRK (128-bit) */
} __attribute__((packed)) le_add_device_to_resolving_list_cp;
/* Remove a device from the resolving list */
/* OGF=0x08, OCF=0x0028 = LE Remove Device From RL */
/* Enable address resolution in the controller */
/* OGF=0x08, OCF=0x002D = LE Set Address Resolution En. */
/* Read how many entries the controller can hold */
/* OGF=0x08, OCF=0x002A = LE Read Resolving List Size */
/* Returns: uint8_t resolving_list_size */
/* Using bluetoothctl to view bonded devices (have IRKs): */
/* bluetoothctl */
/* [bluetooth]# devices Bonded */
8.14.2 — Better Privacy — Private Address Details
The link layer uses a timer to control how often it generates a new private address. A new address is generated when either of two things happens:
When the device boots up or the link layer is reset, a new private address is immediately generated before any advertising begins.
The timer counts down to zero. A new address is generated, and the timer is immediately restarted. The specification recommends this timer be set to 15 minutes.
The 15-minute interval is a balance. A very short interval (say, every 30 seconds) would mean the paired device has to constantly re-resolve the address in order to keep the connection alive, which wastes battery. A very long interval (say, 1 hour) would give passive trackers a longer window to track the device before the address changes. 15 minutes is the recommended sweet spot.
/* Setting the RPA rotation timeout via BlueZ HCI */
/* OGF=0x08, OCF=0x002E = LE Set Resolvable Private */
/* Address Timeout */
typedef struct {
uint16_t rpa_timeout; /* timeout in seconds */
/* recommended: 900 (15 min) */
/* range: 1 to 65535 seconds */
} __attribute__((packed)) le_set_rpa_timeout_cp;
/* Example: set 15-minute rotation */
le_set_rpa_timeout_cp cp = { .rpa_timeout = htobs(900) };
hci_send_cmd(sock, OGF_LE_CTL,
OCF_LE_SET_RESOLVABLE_PRIVATE_ADDRESS_TIMEOUT,
sizeof(cp), &cp);
The private address is not just used during advertising — it is used during advertising, scanning, and initiating states. This is important because a device is identifiable by its address in all three states, not just when it is advertising.
(Advertising)
AdvA = RPA of fitness band (not real address)
(Scanning)
ScanA = phone’s own RPA (not phone’s real address)
(Initiating)
InitA = phone’s RPA, AdvA = band’s RPA
8.15 — Device Filtering and White List
In a busy environment, the BLE radio receives advertising packets from many devices — headsets, keyboards, other people’s phones, beacons, sensors. Most of these are completely irrelevant to the current application. Without filtering, the controller would have to process every packet and wake up the host for each one. That wastes both radio time and processor cycles.
The White List is a set of device addresses stored in the controller. When a filter policy is active, the controller only responds to or reports devices whose addresses appear in this list. Packets from unlisted devices are silently discarded at the hardware level — the host never sees them.
This has two major benefits:
The controller processes fewer packets. The host is woken up less often. The radio sends fewer responses. All of this directly reduces battery consumption on both the controller chip and the host CPU.
A host can tell the controller “connect to whichever of these three known sensors becomes available first.” The controller handles the race and reports back which device it connected to, via the Peer_Address field of the LE_Connection_Complete event. The host does not need to write retry loops.
The White List starts empty at controller reset. It is populated by the host using HCI commands. HCI_LE_Add_Device_To_White_List adds an address and HCI_LE_Remove_Device_From_White_List removes one. HCI_LE_Clear_White_List empties the whole list at once.
The Advertising Filter Policy controls how the Advertiser’s link layer handles two types of incoming requests: SCAN_REQ (from scanners wanting more data) and CONNECT_REQ (from initiators wanting to connect). The host sets this policy using HCI_LE_Set_Advertising_Parameters.
There are four possible modes. Only one can be active at a time:
Exception for ADV_DIRECT_IND: When the Advertiser is using Connectable Directed advertising (ADV_DIRECT_IND), the filter policy is not used at all. Instead, the Advertiser accepts requests only from the specific device named in the InitA field of the advertising packet. The White List filter is irrelevant in this case.
/* Setting Advertising Filter Policy via BlueZ HCI */
/* Part of HCI_LE_Set_Advertising_Parameters */
/* OGF=0x08, OCF=0x0006 */
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>
le_set_advertising_parameters_cp adv_params;
memset(&adv_params, 0, sizeof(adv_params));
adv_params.min_interval = htobs(0x0320); /* 500ms */
adv_params.max_interval = htobs(0x0320);
adv_params.advtype = 0x00; /* ADV_IND */
adv_params.chan_map = 0x07; /* all channels */
/* filter_policy values: */
/* 0x00 = allow all (default) */
/* 0x01 = white list for both scan and connect */
/* 0x02 = white list for connect only */
/* 0x03 = white list for scan only */
adv_params.filter_policy = 0x01; /* WL for both */
hci_send_cmd(sock, OGF_LE_CTL,
OCF_LE_SET_ADVERTISING_PARAMETERS,
LE_SET_ADVERTISING_PARAMETERS_CP_SIZE,
&adv_params);
The Scanner Filter Policy controls which advertising packets the Scanner’s link layer accepts and passes up to the host. It is set using HCI_LE_Set_Scan_Parameters.
BLE 4.0 defined two basic modes. BLE 4.2 added two extended modes specifically to support link layer privacy:
White List NOT in use
Process all advertising packets from any device regardless of address. This is the default mode after controller reset. The scanner reports every advertising packet to the host.
White List in use
Process advertising packets only from devices in the white list. Packets from all other devices are silently discarded in the controller before they reach the host. Major power saving in a crowded BLE environment.
White List + RPA support
Same as Mode 1 (white list in use), and additionally: do not ignore a connectable directed advertising packet (ADV_DIRECT_IND) if the InitA field contains a resolvable private address. In BLE 4.0, such packets would be dropped because the scanner’s RPA would not match the fixed address the ADV_DIRECT_IND was sent to. BLE 4.2 fixed this by allowing the scanner to resolve the RPA first.
All packets + RPA support
Same as Mode 0 (all packets), and additionally: do not ignore a connectable directed advertising packet if the InitA field is a resolvable private address. The default after reset is still the same as BLE 4.0 Mode 0.
The two 4.2 extensions (Modes 2 and 3) solve a problem that existed with the BLE 4.0 scanner filter policies when both devices were using private addresses. A device sending ADV_DIRECT_IND to a scanner that uses an RPA would include the scanner’s RPA in the InitA field. But the scanner’s RPA changes every 15 minutes. In BLE 4.0, the scanner would not recognise its own old RPA in the InitA and would discard the packet. BLE 4.2 allows the scanner to resolve the InitA address first before deciding to accept or discard.
/* Setting Scanner Filter Policy via BlueZ HCI */
/* Part of LE Set Scan Parameters command */
/* OGF=0x08, OCF=0x000B */
le_set_scan_parameters_cp scan_params;
memset(&scan_params, 0, sizeof(scan_params));
scan_params.type = 0x01; /* active scan */
scan_params.interval = htobs(0x0010); /* 10ms */
scan_params.window = htobs(0x0010); /* 10ms */
scan_params.own_bdaddr_type = 0x02; /* use RPA */
/* filter_policy values: */
/* 0x00 = accept all (default, BLE 4.0) */
/* 0x01 = white list only (BLE 4.0) */
/* 0x02 = white list + allow directed RPA (BLE 4.2) */
/* 0x03 = all + allow directed RPA (BLE 4.2) */
scan_params.filter = 0x02; /* WL + RPA support */
hci_send_cmd(sock, OGF_LE_CTL,
OCF_LE_SET_SCAN_PARAMETERS,
LE_SET_SCAN_PARAMETERS_CP_SIZE,
&scan_params);
Next — Initiator Filter Policy, Practical Examples & Chapter 8 Summary
You now understand Link Layer Privacy and the Advertising/Scanner Filter Policies. The next file covers the Initiator Filter Policy, real sniffer captures showing procedures in action, and the full Chapter 8 summary.
