Skip to content

Commit dc6c119

Browse files
committed
Add simulated band steering post
1 parent 367e257 commit dc6c119

File tree

3 files changed

+309
-0
lines changed

3 files changed

+309
-0
lines changed
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
+++
2+
title = "Simulated WiFi Band Steering"
3+
date = "2025-11-01"
4+
description = ""
5+
path = "blog/simulated-band-steering"
6+
7+
#[taxonomies]
8+
#tags = ["linux", "wifi"]
9+
+++
10+
11+
## Overview
12+
13+
In this post, I'll provide demonstrate how to simulate WiFi band steering without any physical WiFi radios. We'll use
14+
open source Linux tools that are used in commercial WiFi products and hopefully learn a thing or two on the way!
15+
16+
If you'd like to replicate this yourself, you'll need a Linux system with `sudo` access, kernel support for the `mac80211_hwsim` driver
17+
(which is generally the case for most Linux distributions), and `hostapd` and `wpa_supplicant` installed. These tools will permit
18+
complete WiFi simulation in software. However, the same setup will also work with real WiFi radios. This can also be run within
19+
a container or virtual machine.
20+
21+
This setup assumes some experience using Linux, Wireshark, and WiFi. There are plenty of great resources out there.
22+
I maintain a reference of common Linux commands [here](@/blog/2023-12-linux-cmds.md). Additionally, if you'd like to
23+
(responsibly) do real WiFi packet capture with Wireshark, I wrote a guide which is available [here](@/blog/2023-10-wifi-packet-capture.md).
24+
25+
## Background
26+
27+
Before we dive in, I'd like to first cover some background WiFi information, especially as it relates to band steering. If you're familiar already, skip to the next section.
28+
29+
In WiFi, clients select and connect to the WiFi access point (AP) BSSID they want. Each client operates slightly differently, but metrics like channel utilization and estimated throughput strongly influence this decision. As a client operates and possibly moves around in its environment, conditions will change. The client may decide to shift to another channel on the same AP (different BSSID), it may roam to another AP, or it may do something else entirely! These decisions are ultimately left up to the client, not the AP.
30+
31+
Given this, an AP is left with limited options to influence the client decisions. An AP can reject the client's authentication/association request or send a deauthentication/disassociation. However, these methods are heavy handed and may result in poor user experience.
32+
33+
Several amendments to the WiFi 802.11 standard serve to address this and to improve user experience generally. Amendments 802.11k (neighbor reports), 802.11r (fast transition), and 802.11v (BSS transition management) are a couple widely known examples. For the purposes of this post, we'll focus on 802.11k and 802.11v, which introduce neighbor reports and BSS transition management (BTM), respectively. Both are essential for band steering, whether it be simulated or real-world.
34+
35+
If you'd like to brush up on band steering or the amendments I mentioned, check out my colleague Isaac Konikoff's talk on band steering fails [here](https://www.youtube.com/watch?v=X5ffNbd5Duw) and this Cisco 802.11v explainer available [here](https://web.archive.org/web/20230815035221/https://www.cisco.com/c/en/us/td/docs/wireless/controller/9800/config-guide/b_wl_16_10_cg/802-11v.pdf). For more in-depth coverage, check out my colleague Sitarama Penumetsa's lecture [here](https://youtu.be/BiktVCtMGnk?si=c6LDYNyg1qJ_DqQ8&t=2002) from his WiFi fundamentals course.
36+
37+
With this all in mind, we're ready to setup for testing!
38+
39+
## Test Setup
40+
41+
In order to simulate band steering, we must first configure our test environment. While this same configuration is possible with
42+
real radios, we'll instead use the `mac80211_hwsim` Linux driver to simulate this testing entirely in software, hopefully making
43+
it more accessible.
44+
45+
### Recommendations
46+
47+
To make your life easier, I recommend the following:
48+
49+
- **Use three terminals**
50+
51+
- One terminal each for AP, client, and configuration CLI
52+
53+
- This will permit reading logs while we setup and run the test
54+
55+
- Now may be a good time to learn how to use `tmux` or `screen`
56+
57+
- **Disconnect from your WiFi (if present) while running this**
58+
59+
- When running with verbose logging, `wpa_supplicant` and `hostapd` will output information on any changes to any interface
60+
on the system. Verbose logs can become especially busy on a normal system with other WiFi active.
61+
62+
- **Do not disable WiFi in your networking daemon.** This will enable rfkill which soft blocks WiFi on all interfaces
63+
(including the `mac80211_hwsim` ones)
64+
65+
### Setup Instructions
66+
67+
**NOTE:** Many commands in this post will require root permissions (e.g. run with `sudo`), including `hostapd`, `wpa_supplicant`, `modprobe`.
68+
69+
1. **Load the `mac80211_hwsim` kernel module (driver)**
70+
71+
```Bash
72+
# Three simulated radios are needed
73+
# 1. Client (STA)
74+
# 2. AP 2.4 GHz BSSID
75+
# 3. AP 5 GHz BSSID
76+
modprobe mac80211_hwsim radios=3 support_p2p_device=0
77+
```
78+
79+
1. **Identify simulated WiFi interfaces and their MAC addresses**
80+
81+
```Bash
82+
# Show interface link-layer information and names ('-br' forces terse output)
83+
ip -br link show
84+
```
85+
86+
**Most likely the `mac80211_hwsim` interfaces will be named `wlan0`, `wlan1`, and `wlan2`**. The driver will additionally
87+
create another interface named `hwsim0`, which permits packet capture of simulated WiFi traffic.
88+
89+
To know for sure, first check the MAC address. The driver creates the interfaces using the format `02:00:00:00:XX:00`,
90+
where the `XX` matches the simulated radio number.
91+
92+
If you're encountering issues or want to know more, check out [this section](#identifying-interfaces-and-macs-cont).
93+
94+
1. **Configure your network daemon to ignore the interfaces**
95+
96+
Practically all modern workstation Linux distributions ship with NetworkManager. If you're using something else, I assume you know
97+
what you're doing or can figure it out...
98+
99+
When using NetworkManager, we must configure it to ignore the new `mac80211_hwsim` interfaces, which you can do with the
100+
following command.
101+
102+
```Bash
103+
# Run this for each simulated WiFi interface, substituting in
104+
# your system's interface names for this and subsequent commands
105+
nmcli device set wlan0 managed false
106+
```
107+
108+
When complete, the interfaces should show as `unmanaged` in the output of `nmcli device status` (or `nmcli device` for short),
109+
as shown below. You may still see some unexpected traffic (e.g. ARP, ICMPv6, DHCP) and the interfaces may even autoconfigure
110+
with a link-local IPv4 address. However, we configured NetworkManager to not control the WiFi interfaces, which will allow us
111+
to run our test.
112+
113+
1. **Download example `hostapd` and `wpa_supplicant` configs from [here](https://codeberg.org/a-gavin/hostap-confs)**
114+
115+
I recommend using the open (no security) configuration, as this will make packet capture much more simple.
116+
With configurations supporting encryption, packet decryption is possible, but security improvements like 802.11w
117+
and WPA3 make this increasingly difficult (which is good for security!).
118+
119+
```Bash
120+
git clone https://codeberg.org/a-gavin/hostap-confs.git
121+
```
122+
123+
1. **Start the AP interfaces**
124+
125+
```Bash
126+
# Options used are:
127+
# -t: Print timestamps
128+
# -i: Specify interface to run AP with. Can be specified multiple times.
129+
#
130+
# For verbose or very verbose logging, add the '-d' or '-dd' options
131+
hostapd -t -i wlan0 -i wlan1 hostapd_2.4GHz-open.conf hostapd_5GHz-open.conf
132+
```
133+
134+
Assuming no errors appear in the logs, you can verify the AP interface channel/frequency, SSID, and MAC
135+
with the `iw wlan0 info` command.
136+
137+
1. **Enable `mac80211_hwsim` packet capture interface**
138+
139+
This interface is likely named `hwsim0`.
140+
141+
```Bash
142+
ip link set up dev hwsim0
143+
```
144+
145+
1. **Run Wireshark using the `mac80211_hwsim` packet capture interface**
146+
147+
Packet capture requires root permissions. See the Wireshark documentation [here](https://wiki.wireshark.org/capturesetup/captureprivileges#gnulinux-distributions-wireshark-is-installed-using-a-package-manager) for more information on how to enable
148+
this.
149+
150+
1. **Start the client interface**
151+
152+
```Bash
153+
# Similar options and requirements as 'hostapd'
154+
# The '-c' option required to specify the config file
155+
wpa_supplicant -t -i wlan2 -c supplicant_open.conf
156+
```
157+
158+
1. **Wait for client to connect**
159+
160+
As the client connects, you should start to see the simulated WiFi traffic in Wireshark. The simulated client will
161+
connect through the normal WiFi client connection process. Connection is complete once the AP responds with a
162+
successful association response frame. I recommend using the `wlan.fc.type_subtype != 8` filter to not display beacons.
163+
164+
Once the client connects, you should also see the message `CTRL-EVENT-CONNECTED` in the station logs and `AP-STA-CONNECTED` in
165+
the AP logs. Take note of the AP interface that the client connected to. As with the AP interfaces, you can run the
166+
`iw wlan2 info` command to verify connection, channel, SSID, BSSID, etc.
167+
168+
1. **Connect to the `hostapd` CLI interface**
169+
170+
In another terminal, run the following command. This will enter a the `hostapd` CLI interface which may
171+
or may not permit line editing and/or command history, depending on your system.
172+
173+
```Bash
174+
# Example configs here use default control interface directory.
175+
# If you use something else specify it using the '-p' option
176+
hostapd_cli
177+
```
178+
179+
1. **Select the AP interface to which the client connected**
180+
181+
Using the `interface` command, select the desired interface. Then, check the list of connected clients
182+
with the `list_sta` command, as shown below.
183+
184+
```Bash
185+
$ hostapd_cli -p /var/run/hostapd/
186+
...
187+
> interface wlan0
188+
> list_sta <---- No clients associated, check the other AP interface
189+
> interface wlan1
190+
> list_sta
191+
02:00:00:00:02:00
192+
```
193+
194+
With the simulated client connected, Wireshark running, and the `hostapd` CLI up, we're ready to run some tests!
195+
196+
## Simulated Band Steering Test
197+
198+
### Test Overview and Goals
199+
200+
For this test, we'll attempt to band steer the client from its currently associated BSSID to the AP's other BSSID.
201+
This test aims to simulate a client receiving a transition request from an AP, which is often seen in real world scenarios,
202+
for example a client as it walks away from the AP.
203+
204+
A successful test here should result in the client disconnecting from the initial BSSID and then reconnecting to the
205+
other BSSID. This should occur after the client receives a BTM request from the initial AP. The request should
206+
include the other BSSID as a preferred neighbor candidate, which should trigger client reconnection to the other BSSID.
207+
All of this should be visible in the Wireshark capture we started earlier.
208+
209+
### Test Instructions
210+
211+
Using the `hostapd` CLI interface we configured earlier, we'll use the `bss_tm_req` command to trigger the client steer.
212+
This command instructs the specified AP to transmit a BTM request frame to the client, hopefully resulting in a successful
213+
band steer.
214+
215+
With the suggested example configs, the client should respond and steer successfully, assuming the command used is correct.
216+
It's possible to simulate a client which doesn't support BTM requests by setting the `disable_btm=1` config option
217+
(outside the network block) and reconnecting, but that's left as an exercise to the reader.
218+
219+
The following commands will generate BTM requests to steer the client from one BSSID to the other and vice versa.
220+
Adjust as needed. More information on this command can be found in [this section](#bss-tm-req-command-format).
221+
222+
```Bash
223+
# From 5 GHz to 2.4 GHz
224+
# First MAC is client MAC. Second MAC (neighbor) is 2.4 GHz BSSID.
225+
bss_tm_req 02:00:00:00:02:00 pref=1 neighbor=02:00:00:00:00:00,0x0000008f,81,1,14
226+
227+
# From 2.4 GHz to 5 GHz
228+
# First MAC is client MAC. Second MAC (neighbor) is 5 GHz BSSID.
229+
# Unlike the 5 GHz to 2.4 GHz config above, the client may take a bit to scan.
230+
bss_tm_req 02:00:00:00:02:00 pref=1 neighbor=02:00:00:00:01:00,0x0000008f,81,36,14
231+
```
232+
233+
Assuming the command ran successfully, you should see 'OK' in the CLI and a successful BTM request and response exchange in Wireshark as shown below.
234+
235+
{{ image(src="/blog/2025-11-simulated-band-steering/btm_request_success_5ghz_to_2.4ghz.png", caption="BTM Request to downsteer from 5 GHz to 2.4 GHz BSSID", alt="BTM Request to downsteer from 5 GHz to 2.4 GHz BSSID") }}
236+
237+
In the case that the command failed to parse, you'll see a 'FAIL' in the CLI. However, if the command succeeded
238+
but the client did not steer, there are other issues present.
239+
240+
The following image shows another BTM request exchange. Here the client rejects the AP's request with
241+
status code 7, no suitable candidates. If you see similar, possibly you misconfigured the neighbor BSSID, or
242+
the client was unable to scan the neighbor.
243+
244+
{{ image(src="/blog/2025-11-simulated-band-steering/btm_request_reject_no_suitable_candidates.png", caption="BTM Request rejected, no suitable candidates", alt="BTM Request rejected, no suitable candidates") }}
245+
246+
## Closing Thoughts
247+
248+
In this post, we detailed how to run a simple band steering test using a single client and two AP BSSIDs triggered by the AP.
249+
This test was simulated entirely in software using the `mac80211_hwsim` virtual WiFi driver along with standard Linux WiFi tooling.
250+
251+
Similar testing using the client as the trigger is also possible (using the `wpa_cli` `wnm_bss_query` command).
252+
However, upstream `hostapd` does not support BTM query responses that will trigger band steering (does not
253+
include preferred neighbor report). For that, your best bet will be to test with real APs. Have fun!
254+
255+
## Addendum
256+
257+
### Identifying Interfaces and MACs Cont
258+
259+
In the case that interface names and MAC addresses are non-default or difficult to identify, there may be several
260+
things getting in the way. This section covers a couple reasons why this may be.
261+
262+
The most likely culprit is your networking daemon. If you're using Network Manager, it's possible your system
263+
configuration may randomize MAC addresses, which will happen before you can instruct Network Manager to ignore them.
264+
I run NixOS nowadays and that's exactly what it did for me.
265+
266+
As mentioned in the [setup instructions](#setup-instructions) above, the `mac80211_hwsim` driver will create the simulated
267+
radios and WiFi interfaces with MAC addresses matching the pattern `02:00:00:00:XX:00`. Assuming the driver loaded properly,
268+
if you don't see this in the output of `ip -br link show`, then networking daemon MAC randomization is almost certainly the cause.
269+
270+
<!-- TODO: How to disable MAC changing on NetworkManager -->
271+
272+
Another possible source of issues could be `systemd-udev`. On modern Linux distributions which use `systemd`,
273+
predictable network interface naming is enabled by default (see [here](https://systemd.io/PREDICTABLE_INTERFACE_NAMES/) for more info).
274+
However, either `udev` '.rules' files in `/etc/udev/rules.d/` or `systemd` '.link' files in `/etc/systemd/network/`
275+
(which are used even when running Network Manager) could rename the network interfaces. Unless you've been adjusting
276+
this in your system already, your issue is almost certainly the networking daemon.
277+
278+
### `bss_tm_req` Command Format
279+
280+
In general, `hostapd` and `wpa_supplicant` CLI commands are straightforward but not well documented. In the `hostapd` CLI,
281+
you can run the `help` to display more information on the desired command (e.g. `help status`). However, often times the output
282+
is terse, and I find myself referencing the source instead. If you also need to do that, start [here](https://git.w1.fi/cgit/hostap/tree/wpa_supplicant/ctrl_iface.c)
283+
for `wpa_supplicant` CLI and [here](https://git.w1.fi/cgit/hostap/tree/src/ap/ctrl_iface_ap.c) for `hostapd` CLI.
284+
285+
Unfortunately, the `bss_tm_req` command is one of such not well documented commands. If you used the provided example configs
286+
and your simulated WiFi interface MAC addresses match, the provided `bss_tm_req` command should just work. If that's not the
287+
case, though, the following details some information on the format of the command.
288+
289+
```Bash
290+
bss_tm_req CLIENT_MAC pref=1 neighbor=BSSID,BSSID_INFO,OP_CLASS,CHANNEL,PHY_TYPE
291+
```
292+
293+
- Client MAC address always required. However, just this won't get you far.
294+
295+
- Setting `pref=1` includes the `neighbor=...` section in the BTM request.
296+
297+
- A client almost certainly won't steer without this.
298+
299+
- In the `neighbor=...` section, all information is for the target AP (AP to steer the client to)
300+
301+
- Neighbor information is verbatim set in the BTM request neighbor report with basically no error checking
302+
303+
- `OP_CLASS` defines the operating class of the neighbor AP's channel
304+
305+
- `PHY_TYPE` defines the neighbor AP's PHY capabilities, as defined `dot11PHYType` in IEEE Std 802.11-2020, Annex C
306+
- HT: 7
307+
- VHT: 9
308+
- HE: 14
309+
- EHT: Not in 802.11-2024, so unsure
137 KB
Loading
226 KB
Loading

0 commit comments

Comments
 (0)