Wi-Fi: Add roaming support (802.11k,v,r)

Useful feature if running multiple radios on single AP with
the same SSIDs or multiple APs to get good area coverage.
This commit is contained in:
Mattias Walström
2026-06-05 14:54:35 +02:00
parent c0225b7096
commit 4e50c96567
7 changed files with 325 additions and 5 deletions
+95
View File
@@ -486,6 +486,101 @@ radio settings, and `show interface` to see all active AP interfaces.
> AP and Station modes cannot be mixed on the same radio. All virtual interfaces
> on a radio must be the same mode (all APs or all Stations).
## Fast Roaming Between Access Points
Fast roaming enables seamless client handoff between access points through
802.11k/r/v standards. These features can be enabled individually based on
your requirements.
### 802.11r - Fast BSS Transition
Enable 802.11r for fast handoff (<50ms) between APs with the same SSID:
```
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11r
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11r mobility-domain 4f57
```
**Requirements:**
- All APs in roaming group must have **identical** SSID
- All APs must have **identical** passphrase (same keystore secret)
- All APs must use the **same mobility-domain** identifier
**Mobility Domain Options:**
- Explicit 4-character hex value (e.g., `4f57`) - default if not specified
- `hash` - Automatically derive from SSID using MD5 (OpenWrt-compatible)
Using `hash` allows multiple APs with the same SSID to automatically share
the same mobility domain without manual configuration:
```
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11r mobility-domain hash
```
You can optionally set NAS identifier mode:
```
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11r nas-identifier auto
```
`auto` derives:
`<interface-name>-<hostname>.<mobility-domain>`
Or set an explicit string:
```
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11r nas-identifier ap01.wifi0.4f57
```
### 802.11k - Radio Resource Management
Enable 802.11k for client neighbor discovery and better roaming decisions:
```
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11k
```
Enables neighbor reports and beacon reports, allowing clients to discover
nearby APs before roaming.
### 802.11v - BSS Transition Management
Enable 802.11v for network-assisted roaming:
```
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11v
```
Allows APs to suggest better APs to clients, improving roaming decisions.
### Recommended Configuration
For optimal roaming experience, enable all three features:
```
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11k
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11r
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11v
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11r mobility-domain 4f57
```
Or use `hash` for automatic mobility domain derivation from SSID:
```
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11k
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11r
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11v
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11r mobility-domain hash
```
Repeat for all APs that should participate in the roaming group.
> [!NOTE]
> Not all client devices support all roaming features. Modern devices typically
> support 802.11k/r/v, but older devices may only support basic roaming without
> fast transition.
### AP as Bridge Port
WiFi AP interfaces can be added to bridges to integrate wireless devices
+1
View File
@@ -10,6 +10,7 @@ config BR2_PACKAGE_FEATURE_WIFI
select BR2_PACKAGE_HOSTAPD_DRIVER_NL80211
select BR2_PACKAGE_HOSTAPD_WPA3
select BR2_PACKAGE_HOSTAPD_WPS
select BR2_PACKAGE_HOSTAPD_WNM
select BR2_PACKAGE_IW
help
Enables WiFi in Infix. Enables all requried applications.
+1
View File
@@ -89,6 +89,7 @@ PKG_CHECK_MODULES([libite], [libite >= 2.6.1])
PKG_CHECK_MODULES([sysrepo], [sysrepo >= 4.2.10])
PKG_CHECK_MODULES([libyang], [libyang >= 4.2.2])
PKG_CHECK_MODULES([libsrx], [libsrx >= 1.0.0])
PKG_CHECK_MODULES([libcrypto], [libcrypto])
AC_CHECK_HEADER([ev.h],
[saved_LIBS="$LIBS"
+2
View File
@@ -17,6 +17,7 @@ confd_plugin_la_CFLAGS = \
$(glib_CFLAGS) \
$(jansson_CFLAGS) \
$(libite_CFLAGS) \
$(libcrypto_CFLAGS) \
$(sysrepo_CFLAGS) \
$(libsrx_CFLAGS) \
$(CFLAGS)
@@ -26,6 +27,7 @@ confd_plugin_la_LIBADD = \
$(glib_LIBS) \
$(jansson_LIBS) \
$(libite_LIBS) \
$(libcrypto_LIBS) \
$(sysrepo_LIBS) \
$(libsrx_LIBS)
+105 -4
View File
@@ -4,6 +4,8 @@
#include <jansson.h>
#include <libgen.h>
#include <limits.h>
#include <unistd.h>
#include <openssl/evp.h>
#include <srx/common.h>
#include <srx/lyx.h>
@@ -177,6 +179,45 @@ out_free_xpath:
return err;
}
/* Resolve mobility domain - if "hash", derive from SSID using MD5 (OpenWrt-compatible) */
static const char *resolve_mobility_domain(const char *mobility_domain, const char *ssid)
{
static char hash_result[5]; /* 4 hex chars + null */
unsigned char md5_digest[EVP_MAX_MD_SIZE];
unsigned int md5_len;
EVP_MD_CTX *ctx;
if (strcmp(mobility_domain, "hash"))
return mobility_domain;
if (!ssid) {
ERROR("Cannot derive mobility domain from NULL SSID");
return "4f57"; /* Fallback to default */
}
/* Compute MD5 hash using EVP API (OpenSSL 3.0+) */
ctx = EVP_MD_CTX_new();
if (!ctx) {
ERROR("Failed to create EVP context");
return "4f57";
}
if (EVP_DigestInit_ex(ctx, EVP_md5(), NULL) != 1 ||
EVP_DigestUpdate(ctx, ssid, strlen(ssid)) != 1 ||
EVP_DigestFinal_ex(ctx, md5_digest, &md5_len) != 1) {
ERROR("Failed to compute MD5 hash");
EVP_MD_CTX_free(ctx);
return "4f57";
}
EVP_MD_CTX_free(ctx);
/* Extract first 4 hex characters (first 2 bytes) */
snprintf(hash_result, sizeof(hash_result), "%02x%02x", md5_digest[0], md5_digest[1]);
DEBUG("Derived mobility domain '%s' from SSID '%s'", hash_result, ssid);
return hash_result;
}
static int wifi_find_interfaces_on_radio(struct lyd_node *ifs, const char *radio_name,
struct lyd_node ***iface_list, int *count)
@@ -256,10 +297,16 @@ static int wifi_find_radio_aps(struct lyd_node *cifs, const char *radio_name,
/* Helper: Write SSID and security configuration (shared between primary and BSS) */
static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd_node *config, bool is_bss, const char *band)
{
struct lyd_node *wifi, *ap, *security, *secret_node, *roaming;
struct lyd_node *dot11k, *dot11r, *dot11v;
const char *ssid, *hidden, *security_mode, *secret_name;
struct lyd_node *wifi, *ap, *security, *secret_node;
const char *mobility_domain, *mobility_domain_raw;
const char *nas_identifier_cfg;
bool enable_80211k, enable_80211r, enable_80211v;
unsigned char *secret = NULL;
const char *ifname;
char nas_identifier_default[300];
char hostname[64] = "localhost";
char bssid[18];
ifname = lydx_get_cattr(cif, "name");
@@ -271,6 +318,16 @@ static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd
fprintf(hostapd, "bss=%s\n", ifname);
}
/* Check 802.11k/r/v configuration */
roaming = lydx_get_child(ap, "roaming");
dot11k = roaming ? lydx_get_child(roaming, "dot11k") : NULL;
dot11r = roaming ? lydx_get_child(roaming, "dot11r") : NULL;
dot11v = roaming ? lydx_get_child(roaming, "dot11v") : NULL;
enable_80211k = dot11k != NULL;
enable_80211r = dot11r != NULL;
enable_80211v = dot11v != NULL;
/* Set BSSID if custom MAC is configured */
if (!interface_get_phys_addr(cif, bssid))
fprintf(hostapd, "bssid=%s\n", bssid);
@@ -279,6 +336,19 @@ static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd
ssid = lydx_get_cattr(ap, "ssid");
hidden = lydx_get_cattr(ap, "hidden");
if (enable_80211r) {
mobility_domain_raw = lydx_get_cattr(dot11r, "mobility-domain");
mobility_domain = resolve_mobility_domain(mobility_domain_raw, ssid);
nas_identifier_cfg = lydx_get_cattr(dot11r, "nas-identifier");
if (!nas_identifier_cfg || !strcmp(nas_identifier_cfg, "auto")) {
if (gethostname(hostname, sizeof(hostname)) != 0)
snprintf(hostname, sizeof(hostname), "localhost");
hostname[sizeof(hostname) - 1] = '\0';
snprintf(nas_identifier_default, sizeof(nas_identifier_default),
"%s-%s.%s", ifname, hostname, mobility_domain);
nas_identifier_cfg = nas_identifier_default;
}
}
if (ssid)
fprintf(hostapd, "ssid=%s\n", ssid);
if (hidden && !strcmp(hidden, "true"))
@@ -330,7 +400,7 @@ static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd
} else if (!strcmp(security_mode, "wpa2-personal")) {
fprintf(hostapd, "wpa=2\n");
/* WPA-PSK: Pre-shared key authentication */
fprintf(hostapd, "wpa_key_mgmt=WPA-PSK\n");
fprintf(hostapd, "wpa_key_mgmt=%sWPA-PSK\n", enable_80211r ? "FT-PSK " : "");
/* CCMP: AES-based encryption, mandatory for WPA2 */
fprintf(hostapd, "wpa_pairwise=CCMP\n");
if (secret)
@@ -338,7 +408,7 @@ static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd
} else if (!strcmp(security_mode, "wpa3-personal")) {
fprintf(hostapd, "wpa=2\n");
/* SAE: Simultaneous Authentication of Equals, resistant to offline dictionary attacks */
fprintf(hostapd, "wpa_key_mgmt=SAE\n");
fprintf(hostapd, "wpa_key_mgmt=%sSAE\n", enable_80211r ? "FT-SAE " : "");
fprintf(hostapd, "rsn_pairwise=CCMP\n");
if (secret)
fprintf(hostapd, "sae_password=%s\n", secret);
@@ -346,7 +416,8 @@ static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd
} else if (!strcmp(security_mode, "wpa2-wpa3-personal")) {
fprintf(hostapd, "wpa=2\n");
/* Allow both PSK (WPA2) and SAE (WPA3) authentication */
fprintf(hostapd, "wpa_key_mgmt=WPA-PSK SAE\n");
fprintf(hostapd, "wpa_key_mgmt=%sWPA-PSK SAE\n",
enable_80211r ? "FT-PSK FT-SAE " : "");
fprintf(hostapd, "rsn_pairwise=CCMP\n");
if (secret) {
fprintf(hostapd, "wpa_passphrase=%s\n", secret);
@@ -356,6 +427,36 @@ static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd
fprintf(hostapd, "ieee80211w=1\n");
}
/* 802.11r: Fast BSS Transition */
if (enable_80211r) {
fprintf(hostapd, "# Fast BSS Transition (802.11r)\n");
fprintf(hostapd, "mobility_domain=%s\n", mobility_domain);
fprintf(hostapd, "ft_over_ds=1\n");
fprintf(hostapd, "ft_psk_generate_local=1\n");
fprintf(hostapd, "nas_identifier=%s\n", nas_identifier_cfg);
}
/* 802.11k: Radio Resource Management */
if (enable_80211k) {
fprintf(hostapd, "# Radio Resource Management (802.11k)\n");
fprintf(hostapd, "rrm_neighbor_report=1\n");
fprintf(hostapd, "rrm_beacon_report=1\n");
}
/* 802.11v: BSS Transition Management */
if (enable_80211v) {
fprintf(hostapd, "# BSS Transition Management (802.11v)\n");
fprintf(hostapd, "bss_transition=1\n");
}
/* OKC: Opportunistic Key Caching */
if (roaming) {
const char *okc = lydx_get_cattr(roaming, "okc");
if (!okc || !strcmp(okc, "true"))
fprintf(hostapd, "okc=1\n");
}
free(secret);
}
+1 -1
View File
@@ -190,7 +190,7 @@ int wifi_gen_station(struct lyd_node *cif)
asprintf(&security_str, "key_mgmt=NONE");
else if (secret)
asprintf(&security_str,
"key_mgmt=SAE WPA-PSK\n"
"key_mgmt=FT-SAE FT-PSK SAE WPA-PSK\n"
" psk=\"%s\"", secret);
fprintf(wpa_supplicant,
+120
View File
@@ -48,6 +48,12 @@ submodule infix-if-wifi {
- Security: WPA2/WPA3 with keystore integration
- Operational state: Connection status, RSSI, client lists";
revision 2026-02-12 {
description
"Add support for roaming, by adding support for 802.11v/k/r"
reference "internal";
}
revision 2025-12-17 {
description
"Major refactoring for AP mode support (BREAKING CHANGE):
@@ -72,6 +78,27 @@ submodule infix-if-wifi {
description "WiFi support is an optional build-time feature in Infix.";
}
typedef mobility-domain {
type union {
type string {
pattern '[0-9a-fA-F]{4}';
}
type enumeration {
enum hash {
description
"Derive mobility domain by hashing the SSID.
OpenWrt-compatible: md5(ssid), first 4 hex chars.";
}
}
}
default "4f57";
description
"802.11r Mobility Domain identifier (4 hex digits).
Use 'hash' to automatically derive it from the SSID.";
}
augment "/if:interfaces/if:interface" {
when "derived-from-or-self(if:type, 'infixift:wifi')" {
description
@@ -421,6 +448,99 @@ submodule infix-if-wifi {
}
}
container roaming {
description
"Fast roaming and seamless handoff configuration.
802.11k: Radio Resource Management (neighbor discovery)
802.11r: Fast BSS Transition (fast handoff)
802.11v: BSS Transition Management (network-assisted roaming)";
container dot11k {
presence "Enable 802.11k Radio Resource Management";
description
"Enable 802.11k Radio Resource Management.
Allows clients to discover neighboring APs for better
roaming decisions. Enables:
- Neighbor reports
- Beacon reports";
}
container dot11r {
presence "Enable 802.11r Fast BSS Transition";
must "../../security/mode != 'open'" {
error-message "802.11r requires WPA2/WPA3 security and cannot be used with open networks.";
}
description
"Enable 802.11r Fast BSS Transition.
Reduces handoff latency from ~1s to <50ms through
pre-authentication. Required for seamless roaming.";
leaf mobility-domain {
type mobility-domain;
default "4f57";
description
"802.11r Mobility Domain identifier.
All APs that clients should roam between MUST use the
same mobility domain ID.
Can be either:
- A 4-character hex string (e.g., '4f57')
- 'hash' to auto-derive from SSID (OpenWrt-compatible)";
}
leaf nas-identifier {
type union {
type enumeration {
enum auto {
description
"Automatically derive NAS-Identifier as:
<interface-name>-<hostname>.<mobility-domain>.";
}
}
type string {
length "1..253";
}
}
default auto;
description
"Override NAS-Identifier used for 802.11r.
Set to 'auto' to derive:
<interface-name>-<hostname>.<mobility-domain>.
This value should be unique per AP BSS and stable.";
}
}
container dot11v {
presence "Enable 802.11v BSS Transition Management";
description
"Enable 802.11v BSS Transition Management.
Allows APs to suggest better APs to clients, improving
network-assisted roaming decisions.";
}
leaf okc {
type boolean;
default true;
description
"Opportunistic Key Caching (OKC).
Reduces re-authentication time for roaming clients
that don't support 802.11r. The AP caches PMK from
previous associations and shares them with other APs
in the same mobility group.
Safe to leave enabled (default) - only activates when
both AP and client support it.";
}
}
/* Operational state */
container stations {