From 9e62af9eeb141d3a1ac05f55ee212bebb79d7e89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= Date: Mon, 19 Jan 2026 16:18:18 +0100 Subject: [PATCH] Wi-Fi: Add roaming support (802.11k,v,r) Useful feature if running multiple radios with the same SSIDs or multiple SSIDs to get good coverage. --- doc/wifi.md | 52 +++++++++-- package/feature-wifi/Config.in | 1 + src/confd/configure.ac | 1 + src/confd/src/Makefile.am | 2 + src/confd/src/hardware.c | 109 +++++++++++++++++++++++- src/confd/src/if-wifi.c | 2 +- src/confd/yang/confd/infix-if-wifi.yang | 99 +++++++++++++++++---- 7 files changed, 236 insertions(+), 30 deletions(-) diff --git a/doc/wifi.md b/doc/wifi.md index 6db907b1..af3ca8fc 100644 --- a/doc/wifi.md +++ b/doc/wifi.md @@ -497,8 +497,8 @@ your requirements. Enable 802.11r for fast handoff (<50ms) between APs with the same SSID: ``` -admin@example:/config/interface/wifi0/> set wifi access-point roaming enable-80211r true -admin@example:/config/interface/wifi0/> set wifi access-point roaming mobility-domain 4f57 +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:** @@ -506,14 +506,39 @@ admin@example:/config/interface/wifi0/> set wifi access-point roaming mobility-d - All APs must have **identical** passphrase (same keystore secret) - All APs must use the **same mobility-domain** identifier -The mobility-domain defaults to `4f57` if not specified. +**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: + +`-.` + +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 enable-80211k true +admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11k ``` Enables neighbor reports and beacon reports, allowing clients to discover @@ -524,7 +549,7 @@ nearby APs before roaming. Enable 802.11v for network-assisted roaming: ``` -admin@example:/config/interface/wifi0/> set wifi access-point roaming enable-80211v true +admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11v ``` Allows APs to suggest better APs to clients, improving roaming decisions. @@ -534,10 +559,19 @@ Allows APs to suggest better APs to clients, improving roaming decisions. For optimal roaming experience, enable all three features: ``` -admin@example:/config/interface/wifi0/> set wifi access-point roaming enable-80211k true -admin@example:/config/interface/wifi0/> set wifi access-point roaming enable-80211r true -admin@example:/config/interface/wifi0/> set wifi access-point roaming enable-80211v true -admin@example:/config/interface/wifi0/> set wifi access-point roaming mobility-domain 4f57 +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. diff --git a/package/feature-wifi/Config.in b/package/feature-wifi/Config.in index 1c802abe..ecd4c933 100644 --- a/package/feature-wifi/Config.in +++ b/package/feature-wifi/Config.in @@ -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. diff --git a/src/confd/configure.ac b/src/confd/configure.ac index 7fdb4744..2306d586 100644 --- a/src/confd/configure.ac +++ b/src/confd/configure.ac @@ -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" diff --git a/src/confd/src/Makefile.am b/src/confd/src/Makefile.am index 44711799..fc67edbb 100644 --- a/src/confd/src/Makefile.am +++ b/src/confd/src/Makefile.am @@ -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) diff --git a/src/confd/src/hardware.c b/src/confd/src/hardware.c index 0a18394f..78efd7e5 100644 --- a/src/confd/src/hardware.c +++ b/src/confd/src/hardware.c @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include @@ -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); } diff --git a/src/confd/src/if-wifi.c b/src/confd/src/if-wifi.c index 95ecae28..b2e3beda 100644 --- a/src/confd/src/if-wifi.c +++ b/src/confd/src/if-wifi.c @@ -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, diff --git a/src/confd/yang/confd/infix-if-wifi.yang b/src/confd/yang/confd/infix-if-wifi.yang index 9f97120e..0c2adfe2 100644 --- a/src/confd/yang/confd/infix-if-wifi.yang +++ b/src/confd/yang/confd/infix-if-wifi.yang @@ -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 @@ -429,8 +456,8 @@ submodule infix-if-wifi { 802.11r: Fast BSS Transition (fast handoff) 802.11v: BSS Transition Management (network-assisted roaming)"; - leaf enable-80211k { - type boolean; + container dot11k { + presence "Enable 802.11k Radio Resource Management"; description "Enable 802.11k Radio Resource Management. @@ -440,17 +467,57 @@ submodule infix-if-wifi { - Beacon reports"; } - leaf enable-80211r { - type boolean; + 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: + -.."; + } + } + type string { + length "1..253"; + } + } + default auto; + description + "Override hostapd NAS-Identifier used for 802.11r. + + Set to 'auto' to derive: + -.. + + This value should be unique per AP BSS and stable."; + } } - leaf enable-80211v { - type boolean; + container dot11v { + presence "Enable 802.11v BSS Transition Management"; description "Enable 802.11v BSS Transition Management. @@ -458,19 +525,19 @@ submodule infix-if-wifi { network-assisted roaming decisions."; } - leaf mobility-domain { - when "../enable-80211r = 'true'"; - type string { - pattern '[0-9a-fA-F]{4}'; - } - default "4f57"; + leaf okc { + type boolean; + default true; description - "802.11r Mobility Domain identifier (4 hex digits). + "Opportunistic Key Caching (OKC). - All APs that clients should roam between MUST use the - same mobility domain ID. + 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. - Only applicable when 802.11r is enabled."; + Safe to leave enabled (default) - only activates when + both AP and client support it."; } }