Setting up usteer and band-steering in OpenWrt
usteer is a daemon for sharing wifi client information amongst APs on the same network, and can be used for band steering.
This can be useful for improved WiFi performance when you have a network with multiple APs. Especially on EAP networks, it is highly recommended to set up 802.11r also.
Unfortunately documentation on usteer is sparse, so this wiki stub is for the community to add to.
WiFi Roaming
An summary of WiFi roaming technologies can be found on the roaming page.
Prerequisites
The following items are prerequisite to set up usteer on OpenWrt:
- One or more OpenWrt based APs (and routers)
- install the full version of wpad
- usteer package installed on each
Setting up usteer
To set up usteer:
- configure 802.11k and 802.11v on all WiFi AP-nodes.
- install and configure required packages for usteer
- reboot all nodes (or just restart the network if no wpad packages have been changed).
Configure 802.11k and 802.11v on all AP-nodes
SSH into each of your wifi/AP nodes and add the following config-lines in /etc/config/wireless
to each of your SSIDs:
option bss_transition '1' option ieee80211k '1'
Note: From OpenWrt 18.06.0 to 21.02, the config option ieee80211v
was also required. If migrating from an older version, you could remove this option.
Install and configure required packages
We need to install usteer. This is as simple as:
opkg update && opkg install usteer
At this point you have all the packages you need, but they may be in need of additional configuration.
Configuring usteer
usteer's config-file /etc/config/usteer specifies which interface it will listen on for other nodes. By default this is lan.
Config options
Name | Type | Required | Default | Description | ||
---|---|---|---|---|---|---|
network | string | yes | lan | The network interface for inter-AP communication | ||
syslog | boolean | yes | 1 | Log messages to syslog (0/1) | ||
ipv6 | boolean | yes | 0 | Use IPv6 for remote exchange | ||
debug_level | integer | yes | 2 | Minimum level of logged messages. 0 = Fatal, 1 = Info, 2 = Verbose, 3 = some debug messages, 4 = network packet information, 5 = all debug messages | ||
max_neighbour_reports | integer | no | 8 | Maximum number of neighbor reports set for a node | ||
sta_block_timeout | integer | no | 30000 | Maximum amount of time (ms) a station may be blocked due to policy decisions | ||
local_sta_timeout | integer | no | 120000 | Maximum amount of time (ms) a local unconnected station is tracked | ||
measurement_report_timeout | integer | no | 120000 | Maximum amount of time (ms) a measurement report is stored | ||
local_sta_update | integer | no | 1000 | Local station information update interval (ms) | ||
max_retry_band | integer | no | 5 | Maximum number of consecutive times a station may be blocked by policy | ||
seen_policy_timeout | integer | no | 30000 | Maximum idle time of a station entry (ms) to be considered for policy decisions | ||
load_balancing_threshold | integer | no | 0 | Minimum number of stations delta between APs before load balancing policy is active | ||
band_steering_threshold | integer | no | 5 | Minimum number of stations delta between bands before band steering policy is active. Only used if load_balancing_threshold is set. | ||
remote_update_interval | integer | no | 1000 | Interval (ms) between sending state updates to other APs | ||
remote_node_timeout | integer | no | 10 | Number of remote update intervals after which a remote-node is deleted | ||
assoc_steering | boolean | no | 0 | Allow rejecting assoc requests for steering purposes (0/1) | ||
probe_steering | boolean | no | 0 | Allow ignoring probe requests for steering purposes (0/1) | ||
min_connect_snr | integer | no | 0 | Minimum signal-to-noise ratio or signal level (dBm) to allow connections | ||
min_snr | integer | no | 0 | Minimum signal-to-noise ratio or signal level (dBm) to remain connected | ||
min_snr_kick_delay | integer | no | 5000 | Timeout after which a station with snr < min_snr will be kicked | ||
roam_process_timeout | integer | no | 5000 | Timeout (in ms) after which a association following a disassociation is not seen as a roam | ||
roam_scan_snr | integer | no | 0 | Minimum signal-to-noise ratio or signal level (dBm) before attempting to trigger client scans for roam | ||
roam_scan_tries | integer | no | 3 | Maximum number of client roaming scan trigger attempts | ||
roam_scan_timeout | integer | no | 0 | Retry scanning when roam_scan_tries is exceeded after this timeout (in ms). In case this option is set to 0, the client is kicked instead | ||
roam_scan_interval | integer | no | 10000 | Minimum time (ms) between client roaming scan trigger attempts | ||
roam_trigger_snr | integer | no | 0 | Minimum signal-to-noise ratio or signal level (dBm) before attempting to trigger forced client roaming | ||
roam_trigger_interval | integer | no | 60000 | Minimum time (ms) between client roaming trigger attempts | ||
roam_kick_delay | integer | no | 100 | Timeout (in 100ms beacon intervals) for client roam requests | ||
signal_diff_threshold | integer | no | 0 | Minimum signal strength difference until AP steering policy is active | ||
initial_connect_delay | integer | no | 0 | Initial delay (ms) before responding to probe requests (to allow other APs to see packets as well) | ||
load_kick_enabled | boolean | no | 0 | Enable kicking client on excessive channel load (0/1) | ||
load_kick_threshold | integer | no | 75 | Minimum channel load (%) before kicking clients | ||
load_kick_delay | integer | no | 10000 | Minimum amount of time (ms) that channel load is above threshold before starting to kick clients | ||
load_kick_min_clients | integer | no | 10 | Minimum number of connected clients before kicking based on channel load | ||
load_kick_reason_code | integer | no | 5 | Reason code on client kick based on channel load (default: WLAN_REASON_DISASSOC_AP_BUSY) | ||
band_steering_interval | integer | no | 120000 | Attempting to steer clients to a higher frequency-band every n ms. A value of 0 disables band-steering. | ||
band_steering_min_snr | integer | no | -60 | Minimal SNR or absolute signal a device has to maintain over band_steering_interval to be steered to a higher frequency band. | ||
link_measurement_interval | integer | no | 30000 | Interval (ms) the device is sent a link-measurement request to help assess the bi-directional link quality. Setting the interval to 0 disables link-measurements. | ||
node_up_script | string | no | blank | Script to run after bringing up a node | ||
event_log_types | list | no | blank | Message types to include in log. Available types: probe_req_accept probe_req_deny, auth_req_accept, auth_req_deny, assoc_req_accept, assoc_req_deny, load_kick_trigger, load_kick_reset, load_kick_min_clients, load_kick_no_client, load_kick_client, signal_kick | ||
ssid_list | list | no | blank | List of SSIDs to enable steering on |
After making any change, reload usteer:
/etc/init.d/usteer reload
Again, do this on all nodes in your network.
Config gotchas
- For AP steering to take effect, signal_diff_threshold needs to be set to a value greater than 0.
- band_steering_threshold does not take effect unless load_balancing_threshold is also set to a value greater than 0.
- roam_scan_snr needs to be set to trigger client scans for roaming.
- If roam_scan_snr is unset and roam_trigger_snr is set, then roam_scan_snr will take the value of roam_trigger_snr.
Luci
If you prefer to configure usteer using Luci, there is one app available:
opkg update & opkg install luci-app-usteer
The app shows too information about nodes, clients, signal, etc. that can help to adjust correctly the configuration of usteer and verify that is working correctly.
Issues with Intel Wifi cards and potentially other vendors
According to the OpenWrt development mailing list, Intel Wifi cards do not seem to understand the current implementation of usteer because of missing disassociation_timer and disassociation_imminent = true. While a patch is in progress, you might be better of using dawn if you experience your computer constantly switching access points and/or frequencies.
Ubus interface
usteer has a ubus interface that can be accessed by:
ubus call usteer <command> [args]
Ubus commands
Name | Arguments | Description |
---|---|---|
local_info | none | Prints local wifi network information |
remote_hosts | none | Prints information about remote usteer instances |
remote_info | none | Prints remote wifi network information (same as local_info, but for remote APs) |
connected_clients | none | Prints clients that are connected to local wifi interfaces |
get_clients | none | Prints all clients from all APs (local and remote) |
get_client_info | MAC address (json) | Prints information about a specific client, including capabilities and information about roaming attempts |
get_config | none | Prints the local configuration |
set_config | yes (json) | Details required |
update_config | yes (json) | Details required |
set_node_data | yes (json) | Details required |
delete_node_data | yes (json) | Details required |
local_info
root@AccessPoint:~# ubus call usteer local_info
{
"hostapd.wlan1-1": {
"bssid": "11:22:33:44:55:66",
"freq": 2412,
"n_assoc": 0,
"noise": -95,
"load": 5,
"max_assoc": 0,
"roam_events": {
"source": 0,
"target": 0
},
"rrm_nr": [
"11:22:33:44:55:66",
"OpenWrtSSID",
"<hex string>"
]
}
}
remote_hosts
root@AccessPoint:~# ubus call usteer remote_hosts
{
"192.168.1.1": {
"id": -525792052
}
}
remote_info
root@AccessPoint:~# ubus call usteer remote_info
{
"192.168.1.1#hostapd.wlan0": {
"bssid": "11:22:33:44:55:66",
"freq": 5180,
"n_assoc": 6,
"noise": -107,
"load": 3,
"max_assoc": 0,
"roam_events": {
"source": 0,
"target": 0
},
"rrm_nr": [
"11:22:33:44:55:66",
"OpenWrtSSID 5GHz",
"<hex string>"
]
},
"192.168.1.1#hostapd.wlan1": {
"bssid": "11:22:33:44:55:67",
"freq": 2412,
"n_assoc": 7,
"noise": -90,
"load": 13,
"max_assoc": 0,
"roam_events": {
"source": 0,
"target": 0
},
"rrm_nr": [
"11:22:33:44:55:67",
"OpenWrtSSID",
"<hex string>"
]
}
}
connected_clients
root@AccessPoint:~# ubus call usteer connected_clients
{
"hostapd.wlan0": {
"aa:bb:cc:dd:ee:ff": {
"signal": -91,
"created": 604990289,
"seen": 610827895,
"last_connected": 610827895,
"snr-kick": {
"seen-below": 0
},
"load-kick": {
"count": 0
},
"roam-state-machine": {
"tries": 0,
"event": 0,
"kick": 0,
"scan_start": 0,
"scan_timeout_start": 0
},
"bss-transition-response": {
"status-code": 0,
"timestamp": 0
},
"beacon-measurement-modes": [
],
"bss-transition-management": false,
"measurements": [
]
},
...
"aa:bb:cc:dd:ee:fe": {
"signal": -81,
"created": 600282172,
"seen": 610827895,
"last_connected": 610827895,
"snr-kick": {
"seen-below": 0
},
"load-kick": {
"count": 0
},
"roam-state-machine": {
"tries": 0,
"event": 0,
"kick": 0,
"scan_start": 0,
"scan_timeout_start": 0
},
"bss-transition-response": {
"status-code": 0,
"timestamp": 0
},
"beacon-measurement-modes": [
],
"bss-transition-management": false,
"measurements": [
]
}
},
"hostapd.wlan1": {
"aa:bb:cc:dd:ee:fd": {
"signal": -58,
"created": 600282173,
"seen": 610827901,
"last_connected": 610827896,
"snr-kick": {
"seen-below": 0
},
"load-kick": {
"count": 0
},
"roam-state-machine": {
"tries": 0,
"event": 0,
"kick": 0,
"scan_start": 0,
"scan_timeout_start": 0
},
"bss-transition-response": {
"status-code": 0,
"timestamp": 0
},
"beacon-measurement-modes": [
],
"bss-transition-management": false,
"measurements": [
]
}
}
}
get_clients
root@AccessPoint:~# ubus call usteer get_clients
{
"aa:bb:cc:dd:ee:ff": {
"192.168.1.1#hostapd.wlan0": {
"connected": true,
"signal": -77
}
},
...
"aa:bb:cc:dd:ee:fe": {
"192.168.1.1#hostapd.wlan1": {
"connected": false,
"signal": -74
},
"hostapd.wlan1-1": {
"connected": false,
"signal": -86
}
},
"aa:bb:cc:dd:ee:fd": {
"192.168.1.1#hostapd.wlan0": {
"connected": false,
"signal": -70
},
"192.168.1.1#hostapd.wlan1": {
"connected": false,
"signal": -81
},
"hostapd.wlan1-1": {
"connected": false,
"signal": -76
}
},
...
"aa:bb:cc:dd:ee:fc": {
"hostapd.wlan0": {
"connected": true,
"signal": -74
}
}
}
get_client_info
A host that is connected locally
root@AccessPoint:~# ubus call usteer get_client_info "{'address':'aa:bb:cc:dd:ee:ff'}"
{
"2ghz": true,
"5ghz": true,
"nodes": {
"hostapd.wlan0": {
"connected": true,
"signal": -54,
"stats": {
"probe": {
"requests": 5,
"blocked_cur": 0,
"blocked_total": 0
},
"assoc": {
"requests": 5,
"blocked_cur": 0,
"blocked_total": 0
},
"auth": {
"requests": 5,
"blocked_cur": 0,
"blocked_total": 0
}
}
}
}
}
A host that is connected remotely
root@AccessPoint:~# ubus call usteer get_client_info "{'address':'aa:bb:cc:dd:ee:fe'}"
{
"2ghz": true,
"5ghz": false,
"nodes": {
"192.168.1.32#hostapd.wlan1": {
"connected": true,
"signal": -38,
"stats": {
"probe": {
"requests": 0,
"blocked_cur": 0,
"blocked_total": 0
},
"assoc": {
"requests": 0,
"blocked_cur": 0,
"blocked_total": 0
},
"auth": {
"requests": 0,
"blocked_cur": 0,
"blocked_total": 0
}
}
}
}
}
get_config
root@AccessPoint:~# ubus call usteer get_config
{
"syslog": true,
"debug_level": 2,
"ipv6": true,
"local_mode": false,
"sta_block_timeout": 30000,
"local_sta_timeout": 120000,
"local_sta_update": 1000,
"max_neighbor_reports": 8,
"max_retry_band": 5,
"seen_policy_timeout": 30000,
"measurement_report_timeout": 120000,
"load_balancing_threshold": 0,
"band_steering_threshold": 5,
"remote_update_interval": 5000,
"remote_node_timeout": 12,
"assoc_steering": false,
"min_connect_snr": 0,
"min_snr": 0,
"min_snr_kick_delay": 5000,
"steer_reject_timeout": 60000,
"roam_process_timeout": 5000,
"roam_scan_snr": 0,
"roam_scan_tries": 3,
"roam_scan_timeout": 0,
"roam_scan_interval": 10000,
"roam_trigger_snr": 0,
"roam_trigger_interval": 60000,
"roam_kick_delay": 10000,
"signal_diff_threshold": 0,
"initial_connect_delay": 0,
"load_kick_enabled": false,
"load_kick_threshold": 75,
"load_kick_delay": 10000,
"load_kick_min_clients": 10,
"load_kick_reason_code": 5,
"band_steering_interval": 20000,
"band_steering_min_snr": -60,
"link_measurement_interval": 30000,
"interfaces": [
"mesh0",
"mesh1"
],
"event_log_types": [
],
"ssid_list": [
"OpenWrtSSID",
"OpenWrtSSID 5GHz"
]
}
set_config
Details required
update_config
Details required
set_node_data
Details required
delete_node_data
Details required