diff --git a/doc/wifi.md b/doc/wifi.md index b670803b..af3ca8fc 100644 --- a/doc/wifi.md +++ b/doc/wifi.md @@ -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: + +`-.` + +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 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 1508cd39..95cae206 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 @@ -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: + -.."; + } + } + type string { + length "1..253"; + } + } + default auto; + description + "Override NAS-Identifier used for 802.11r. + + Set to 'auto' to derive: + -.. + + 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 {