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
+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 {