[PATCH 5/5] initial UHR support

Johannes Berg johannes at sipsolutions.net
Tue Mar 17 06:05:04 PDT 2026


From: Johannes Berg <johannes.berg at intel.com>

Add initial UHR support, based on a very "superficial"
reading of D1.3 (it's incomplete and not well-specified
in quite a few places.)

Reviewed-by: Benjamin Berg <benjamin.berg at intel.com>
Signed-off-by: Johannes Berg <johannes.berg at intel.com>
---
 hostapd/Android.mk                        |   6 +
 hostapd/Makefile                          |   6 +
 hostapd/config_file.c                     |   8 ++
 hostapd/hostapd.conf                      |  13 ++
 src/ap/ap_config.h                        |   6 +
 src/ap/ap_drv_ops.c                       |   4 +
 src/ap/ap_drv_ops.h                       |   2 +
 src/ap/beacon.c                           |  42 +++++++
 src/ap/ctrl_iface_ap.c                    |  16 +++
 src/ap/hostapd.h                          |   6 +
 src/ap/ieee802_11.c                       |  53 +++++++-
 src/ap/ieee802_11.h                       |  13 ++
 src/ap/ieee802_11_uhr.c                   | 145 ++++++++++++++++++++++
 src/ap/sta_info.c                         |  10 +-
 src/ap/sta_info.h                         |  65 +++++-----
 src/common/ieee802_11_common.c            |  16 +++
 src/common/ieee802_11_common.h            |   4 +
 src/common/ieee802_11_defs.h              |  36 ++++++
 src/drivers/driver.h                      |  28 +++++
 src/drivers/driver_common.c               |   6 +
 src/drivers/driver_nl80211.c              |  23 ++++
 src/drivers/driver_nl80211_capa.c         |  19 +++
 tests/hwsim/example-hostapd.config        |   2 +
 tests/hwsim/example-wpa_supplicant.config |   2 +
 tests/hwsim/test_uhr.py                   | 145 ++++++++++++++++++++++
 wpa_supplicant/Android.mk                 |   3 +
 wpa_supplicant/Makefile                   |   7 ++
 wpa_supplicant/config.c                   |   1 +
 wpa_supplicant/config_file.c              |   1 +
 wpa_supplicant/config_ssid.h              |   8 ++
 wpa_supplicant/ctrl_iface.c               |   8 +-
 wpa_supplicant/events.c                   |  13 ++
 wpa_supplicant/sme.c                      |   1 +
 wpa_supplicant/wpa_cli.c                  |   1 +
 wpa_supplicant/wpa_supplicant.c           |  17 ++-
 wpa_supplicant/wpa_supplicant_i.h         |   4 +
 36 files changed, 699 insertions(+), 41 deletions(-)
 create mode 100644 src/ap/ieee802_11_uhr.c
 create mode 100644 tests/hwsim/test_uhr.py

diff --git a/hostapd/Android.mk b/hostapd/Android.mk
index 042f51280427..69c1edbf9bc4 100644
--- a/hostapd/Android.mk
+++ b/hostapd/Android.mk
@@ -299,6 +299,12 @@ ifdef CONFIG_IEEE80211AC
 L_CFLAGS += -DCONFIG_IEEE80211AC
 endif
 
+ifdef CONFIG_IEEE80211BN
+CONFIG_IEEE80211BE=y
+L_CFLAGS += -DCONFIG_IEEE80211BN
+OBJS += src/ap/ieee802_11_uhr.c
+endif
+
 ifdef CONFIG_IEEE80211BE
 CONFIG_IEEE80211AX=y
 L_CFLAGS += -DCONFIG_IEEE80211BE
diff --git a/hostapd/Makefile b/hostapd/Makefile
index c67f874dac4b..a5bdb28a5f62 100644
--- a/hostapd/Makefile
+++ b/hostapd/Makefile
@@ -344,6 +344,12 @@ ifdef CONFIG_IEEE80211AC
 CFLAGS += -DCONFIG_IEEE80211AC
 endif
 
+ifdef CONFIG_IEEE80211BN
+CONFIG_IEEE80211BE=y
+CFLAGS += -DCONFIG_IEEE80211BN
+OBJS += ../src/ap/ieee802_11_uhr.o
+endif
+
 ifdef CONFIG_IEEE80211BE
 CONFIG_IEEE80211AX=y
 CFLAGS += -DCONFIG_IEEE80211BE
diff --git a/hostapd/config_file.c b/hostapd/config_file.c
index 545683871a15..1d5fa9ee7a8d 100644
--- a/hostapd/config_file.c
+++ b/hostapd/config_file.c
@@ -4876,6 +4876,8 @@ static int hostapd_config_fill(struct hostapd_config *conf,
 		bss->disable_11ax = !!atoi(pos);
 	} else if (os_strcmp(buf, "disable_11be") == 0) {
 		bss->disable_11be = !!atoi(pos);
+	} else if (os_strcmp(buf, "disable_11bn") == 0) {
+		bss->disable_11bn = !!atoi(pos);
 #ifdef CONFIG_PASN
 #ifdef CONFIG_TESTING_OPTIONS
 	} else if (os_strcmp(buf, "force_kdk_derivation") == 0) {
@@ -4973,6 +4975,12 @@ static int hostapd_config_fill(struct hostapd_config *conf,
 		bss->mld_indicate_disabled = atoi(pos);
 #endif /* CONFIG_TESTING_OPTIONS */
 #endif /* CONFIG_IEEE80211BE */
+#ifdef CONFIG_IEEE80211BN
+	} else if (os_strcmp(buf, "ieee80211bn") == 0) {
+		conf->ieee80211bn = atoi(pos);
+	} else if (os_strcmp(buf, "require_uhr") == 0) {
+		conf->require_uhr = atoi(pos);
+#endif /* CONFIG_IEEE80211BN */
 	} else if (os_strcmp(buf, "i2r_lmr_policy") == 0) {
 		conf->i2r_lmr_policy = atoi(pos);
 	} else {
diff --git a/hostapd/hostapd.conf b/hostapd/hostapd.conf
index 9e3f9d4d524c..27d5a5a39053 100644
--- a/hostapd/hostapd.conf
+++ b/hostapd/hostapd.conf
@@ -1125,6 +1125,19 @@ wmm_ac_vo_acm=0
 # will be used as the AP MLD MAC address.
 #mld_addr=02:03:04:05:06:07
 
+##### IEEE 802.11bn related configuration #####################################
+
+#ieee80211bn: Whether IEEE 802.11bn (UHR) is enabled
+# 0 = disabled (default)
+# 1 = enabled
+#ieee80211bn=1
+
+#disable_11bn: Boolean (0/1) to disable UHR for a specific BSS
+#disable_11bn=0
+
+# Require stations to support UHR PHY (reject association if they do not)
+#require_uhr=0
+
 ##### IEEE 802.1X-2004 related configuration ##################################
 
 # Require IEEE 802.1X authorization
diff --git a/src/ap/ap_config.h b/src/ap/ap_config.h
index 15bee01b5ca2..c3ed1893f8af 100644
--- a/src/ap/ap_config.h
+++ b/src/ap/ap_config.h
@@ -569,6 +569,7 @@ struct hostapd_bss_config {
 	bool disable_11ac;
 	bool disable_11ax;
 	bool disable_11be;
+	bool disable_11bn;
 
 	/* IEEE 802.11v */
 	int time_advertisement;
@@ -1233,6 +1234,11 @@ struct hostapd_config {
 	int require_eht;
 #endif /* CONFIG_IEEE80211BE */
 
+	int ieee80211bn;
+#ifdef CONFIG_IEEE80211BN
+	bool require_uhr;
+#endif
+
 	/* EHT enable/disable config from CHAN_SWITCH */
 #define CH_SWITCH_EHT_ENABLED BIT(0)
 #define CH_SWITCH_EHT_DISABLED BIT(1)
diff --git a/src/ap/ap_drv_ops.c b/src/ap/ap_drv_ops.c
index 61b823abe138..7234eb316f98 100644
--- a/src/ap/ap_drv_ops.c
+++ b/src/ap/ap_drv_ops.c
@@ -477,6 +477,8 @@ int hostapd_sta_add(struct hostapd_data *hapd,
 		    size_t he_capab_len,
 		    const struct ieee80211_eht_capabilities *eht_capab,
 		    size_t eht_capab_len,
+		    const struct ieee80211_uhr_capabilities *uhr_capab,
+		    size_t uhr_capab_len,
 		    const struct ieee80211_he_6ghz_band_cap *he_6ghz_capab,
 		    u32 flags, u8 qosinfo, u8 vht_opmode, int supp_p2p_ps,
 		    int set, const u8 *link_addr, bool mld_link_sta,
@@ -502,6 +504,8 @@ int hostapd_sta_add(struct hostapd_data *hapd,
 	params.he_capab_len = he_capab_len;
 	params.eht_capab = eht_capab;
 	params.eht_capab_len = eht_capab_len;
+	params.uhr_capab = uhr_capab;
+	params.uhr_capab_len = uhr_capab_len;
 	params.he_6ghz_capab = he_6ghz_capab;
 	params.vht_opmode_enabled = !!(flags & WLAN_STA_VHT_OPMODE_ENABLED);
 	params.vht_opmode = vht_opmode;
diff --git a/src/ap/ap_drv_ops.h b/src/ap/ap_drv_ops.h
index 66913ed0fca7..f09b3ad9a306 100644
--- a/src/ap/ap_drv_ops.h
+++ b/src/ap/ap_drv_ops.h
@@ -47,6 +47,8 @@ int hostapd_sta_add(struct hostapd_data *hapd,
 		    size_t he_capab_len,
 		    const struct ieee80211_eht_capabilities *eht_capab,
 		    size_t eht_capab_len,
+		    const struct ieee80211_uhr_capabilities *uhr_capab,
+		    size_t uhr_capab_len,
 		    const struct ieee80211_he_6ghz_band_cap *he_6ghz_capab,
 		    u32 flags, u8 qosinfo, u8 vht_opmode, int supp_p2p_ps,
 		    int set, const u8 *link_addr, bool mld_link_sta,
diff --git a/src/ap/beacon.c b/src/ap/beacon.c
index f16ff1fdcb06..f6ac012787e2 100644
--- a/src/ap/beacon.c
+++ b/src/ap/beacon.c
@@ -805,6 +805,13 @@ static size_t hostapd_probe_resp_elems_len(struct hostapd_data *hapd,
 	}
 #endif /* CONFIG_IEEE80211BE */
 
+#ifdef CONFIG_IEEE80211BN
+	if (hostapd_is_uhr_enabled(hapd)) {
+		buflen += hostapd_eid_uhr_capab_len(hapd, IEEE80211_MODE_AP);
+		buflen += 3 + IEEE80211_UHR_OPER_MAX_SIZE;
+	}
+#endif /* CONFIG_IEEE80211BN */
+
 	buflen += hostapd_eid_mbssid_len(hapd_probed, WLAN_FC_STYPE_PROBE_RESP,
 					 NULL,
 					 params->known_bss,
@@ -977,6 +984,13 @@ static u8 * hostapd_probe_resp_fill_elems(struct hostapd_data *hapd,
 	}
 #endif /* CONFIG_IEEE80211BE */
 
+#ifdef CONFIG_IEEE80211BN
+	if (hostapd_is_uhr_enabled(hapd)) {
+		pos = hostapd_eid_uhr_capab(hapd, pos, IEEE80211_MODE_AP);
+		pos = hostapd_eid_uhr_operation(hapd, pos, false);
+	}
+#endif /* CONFIG_IEEE80211BN */
+
 #ifdef CONFIG_IEEE80211AC
 	if (hapd->conf->vendor_vht)
 		pos = hostapd_eid_vendor_vht(hapd, pos);
@@ -2295,6 +2309,11 @@ int ieee802_11_build_ap_params(struct hostapd_data *hapd,
 	}
 #endif /* CONFIG_IEEE80211BE */
 
+#ifdef CONFIG_IEEE80211BN
+	if (hostapd_is_uhr_enabled(hapd))
+		tail_len += 3 + sizeof(struct ieee80211_uhr_operation);
+#endif /* CONFIG_IEEE80211BN */
+
 	if (hapd->iconf->mbssid == ENHANCED_MBSSID_ENABLED &&
 	    hapd == hostapd_mbssid_get_tx_bss(hapd))
 		tail_len += 5; /* Multiple BSSID Configuration element */
@@ -2477,6 +2496,25 @@ int ieee802_11_build_ap_params(struct hostapd_data *hapd,
 	}
 #endif /* CONFIG_IEEE80211BE */
 
+#ifdef CONFIG_IEEE80211BN
+	if (hostapd_is_uhr_enabled(hapd)) {
+		u8 *uhr_oper;
+
+		tailpos = hostapd_eid_uhr_operation(hapd, tailpos, true);
+		params->uhr_oper = os_zalloc(3 + IEEE80211_UHR_OPER_MAX_SIZE);
+		if (!params->uhr_oper)
+			goto error;
+
+		uhr_oper = hostapd_eid_uhr_operation(hapd, params->uhr_oper,
+						     false);
+		/* check that it was filled */
+		if (uhr_oper == params->uhr_oper) {
+			os_free(params->uhr_oper);
+			params->uhr_oper = NULL;
+		}
+	}
+#endif /* CONFIG_IEEE80211BN */
+
 #ifdef CONFIG_IEEE80211AC
 	if (hapd->conf->vendor_vht)
 		tailpos = hostapd_eid_vendor_vht(hapd, tailpos);
@@ -2683,6 +2721,8 @@ error:
 	os_free(head);
 	os_free(tail);
 	os_free(resp);
+	os_free(params->uhr_oper);
+	params->uhr_oper = NULL;
 	return -1;
 #endif /* CONFIG_SAE || NEED_AP_MLME */
 }
@@ -2714,6 +2754,8 @@ void ieee802_11_free_ap_params(struct wpa_driver_ap_params *params)
 #endif /* CONFIG_IEEE80211AX */
 	os_free(params->allowed_freqs);
 	params->allowed_freqs = NULL;
+	os_free(params->uhr_oper);
+	params->uhr_oper = NULL;
 }
 
 
diff --git a/src/ap/ctrl_iface_ap.c b/src/ap/ctrl_iface_ap.c
index f2093018b939..57aa05e1dccc 100644
--- a/src/ap/ctrl_iface_ap.c
+++ b/src/ap/ctrl_iface_ap.c
@@ -405,6 +405,20 @@ static int hostapd_ctrl_iface_sta_mib(struct hostapd_data *hapd,
 	}
 #endif /* CONFIG_IEEE80211BE */
 
+#ifdef CONFIG_IEEE80211BN
+	if ((sta->flags & WLAN_STA_UHR) && sta->uhr_capab) {
+		res = os_snprintf(buf + len, buflen - len, "uhr_capab=");
+		if (!os_snprintf_error(buflen - len, res))
+			len += res;
+		len += wpa_snprintf_hex(buf + len, buflen - len,
+					(const u8 *) sta->uhr_capab,
+					sta->uhr_capab_len);
+		res = os_snprintf(buf + len, buflen - len, "\n");
+		if (!os_snprintf_error(buflen - len, res))
+			len += res;
+	}
+#endif /* CONFIG_IEEE80211BN */
+
 #ifdef CONFIG_IEEE80211AC
 	if ((sta->flags & WLAN_STA_VHT) && sta->vht_capabilities) {
 		res = os_snprintf(buf + len, buflen - len,
@@ -885,6 +899,7 @@ int hostapd_ctrl_iface_status(struct hostapd_data *hapd, char *buf,
 			  "ieee80211ac=%d\n"
 			  "ieee80211ax=%d\n"
 			  "ieee80211be=%d\n"
+			  "ieee80211bn=%d\n"
 			  "beacon_int=%u\n"
 			  "dtim_period=%d\n",
 			  iface->conf->channel,
@@ -896,6 +911,7 @@ int hostapd_ctrl_iface_status(struct hostapd_data *hapd, char *buf,
 			  hostapd_is_vht_enabled(hapd),
 			  hostapd_is_he_enabled(hapd),
 			  hostapd_is_eht_enabled(hapd),
+			  hostapd_is_uhr_enabled(hapd),
 			  iface->conf->beacon_int,
 			  hapd->conf->dtim_period);
 	if (os_snprintf_error(buflen - len, ret))
diff --git a/src/ap/hostapd.h b/src/ap/hostapd.h
index be09bb36c93b..11ef777bcbd8 100644
--- a/src/ap/hostapd.h
+++ b/src/ap/hostapd.h
@@ -944,4 +944,10 @@ hostapd_is_eht_enabled(struct hostapd_data *hapd)
 	return hapd->iconf->ieee80211be && !hapd->conf->disable_11be;
 }
 
+static inline bool
+hostapd_is_uhr_enabled(struct hostapd_data *hapd)
+{
+	return hapd->iconf->ieee80211bn && !hapd->conf->disable_11bn;
+}
+
 #endif /* HOSTAPD_H */
diff --git a/src/ap/ieee802_11.c b/src/ap/ieee802_11.c
index 8a6fa084a7fc..65d220681ac9 100644
--- a/src/ap/ieee802_11.c
+++ b/src/ap/ieee802_11.c
@@ -151,6 +151,11 @@ static size_t hostapd_supp_rates(struct hostapd_data *hapd, u8 *buf)
 		*pos++ = 0x80 | BSS_MEMBERSHIP_SELECTOR_EHT_PHY;
 #endif /* CONFIG_IEEE80211AX */
 
+#ifdef CONFIG_IEEE80211BN
+	if (hapd->iconf->ieee80211bn && hapd->iconf->require_uhr)
+		*pos++ = 0x80 | BSS_MEMBERSHIP_SELECTOR_UHR_PHY;
+#endif /* CONFIG_IEEE80211BN */
+
 #ifdef CONFIG_SAE
 	if ((hapd->conf->sae_pwe == SAE_PWE_HASH_TO_ELEMENT ||
 	     hostapd_sae_pw_id_in_use(hapd->conf) == 2) &&
@@ -4667,6 +4672,24 @@ static int __check_assoc_ies(struct hostapd_data *hapd, struct sta_info *sta,
 		}
 	}
 #endif /* CONFIG_IEEE80211BE */
+#ifdef CONFIG_IEEE80211BN
+	if (hostapd_is_uhr_enabled(hapd)) {
+		resp = copy_sta_uhr_capab(hapd, sta, IEEE80211_MODE_AP,
+					  elems->uhr_capabilities,
+					  elems->uhr_capabilities_len);
+		if (resp != WLAN_STATUS_SUCCESS)
+			return resp;
+
+		if (hapd->iconf->require_uhr && !(sta->flags & WLAN_STA_UHR)) {
+			hostapd_logger(hapd, sta->addr,
+				       HOSTAPD_MODULE_IEEE80211,
+				       HOSTAPD_LEVEL_INFO,
+				       "Station does not support mandatory UHR PHY - reject association");
+			/* FIXME - need assignment from spec */
+			return WLAN_STATUS_DENIED_EHT_NOT_SUPPORTED;
+		}
+	}
+#endif /* CONFIG_IEEE80211BN */
 
 #ifdef CONFIG_P2P
 	if (elems->p2p && ies && ies_len) {
@@ -5254,6 +5277,13 @@ void ieee80211_ml_build_assoc_resp(struct hostapd_data *hapd,
 		}
 	}
 
+#ifdef CONFIG_IEEE80211BN
+	if (hostapd_is_uhr_enabled(hapd)) {
+		p = hostapd_eid_uhr_capab(hapd, p, IEEE80211_MODE_AP);
+		p = hostapd_eid_uhr_operation(hapd, p, false);
+	}
+#endif /* CONFIG_IEEE80211BN */
+
 	p = hostapd_eid_ext_capab(hapd, p, false);
 	p = hostapd_eid_mbo(hapd, p, buf + buflen - p);
 	p = hostapd_eid_wmm(hapd, p);
@@ -5525,6 +5555,7 @@ static int add_associated_sta(struct hostapd_data *hapd,
 	struct ieee80211_vht_capabilities vht_cap;
 	struct ieee80211_he_capabilities he_cap;
 	struct ieee80211_eht_capabilities eht_cap;
+	struct ieee80211_uhr_capabilities uhr_cap;
 	int set = 1;
 	const u8 *mld_link_addr = NULL;
 	bool mld_link_sta = false, epp_sta = false;
@@ -5606,6 +5637,11 @@ static int add_associated_sta(struct hostapd_data *hapd,
 		hostapd_get_eht_capab(hapd, sta->eht_capab, &eht_cap,
 				      sta->eht_capab_len);
 #endif /* CONFIG_IEEE80211BE */
+#ifdef CONFIG_IEEE80211BN
+	if (sta->flags & WLAN_STA_UHR)
+		hostapd_get_uhr_capab(hapd, sta->uhr_capab, &uhr_cap,
+				      sta->uhr_capab_len);
+#endif /* CONFIG_IEEE80211BN */
 
 	/*
 	 * Add the station with forced WLAN_STA_ASSOC flag. The sta->flags
@@ -5621,6 +5657,8 @@ static int add_associated_sta(struct hostapd_data *hapd,
 			    sta->flags & WLAN_STA_HE ? sta->he_capab_len : 0,
 			    sta->flags & WLAN_STA_EHT ? &eht_cap : NULL,
 			    sta->flags & WLAN_STA_EHT ? sta->eht_capab_len : 0,
+			    sta->flags & WLAN_STA_UHR ? &uhr_cap : NULL,
+			    sta->flags & WLAN_STA_UHR ? sta->uhr_capab_len : 0,
 			    sta->he_6ghz_capab,
 			    sta->flags | WLAN_STA_ASSOC, sta->qosinfo,
 			    sta->vht_opmode, sta->p2p_ie ? 1 : 0,
@@ -5682,6 +5720,12 @@ static u16 send_assoc_resp(struct hostapd_data *hapd, struct sta_info *sta,
 			buflen += hostapd_eid_eht_ml_tid_to_link_map_len(hapd);
 	}
 #endif /* CONFIG_IEEE80211BE */
+#ifdef CONFIG_IEEE80211BN
+	if (hostapd_is_uhr_enabled(hapd)) {
+		buflen += hostapd_eid_uhr_capab_len(hapd, IEEE80211_MODE_AP);
+		buflen += 3 + IEEE80211_UHR_OPER_MAX_SIZE;
+	}
+#endif /* CONFIG_IEEE80211BN */
 
 	buf = os_zalloc(buflen);
 	if (!buf) {
@@ -5844,6 +5888,13 @@ rsnxe_done:
 	}
 #endif /* CONFIG_IEEE80211BE */
 
+#ifdef CONFIG_IEEE80211BN
+	if (hostapd_is_uhr_enabled(hapd)) {
+		p = hostapd_eid_uhr_capab(hapd, p, IEEE80211_MODE_AP);
+		p = hostapd_eid_uhr_operation(hapd, p, false);
+	}
+#endif /* CONFIG_IEEE80211BN */
+
 #ifdef CONFIG_OWE
 	if (((hapd->conf->wpa_key_mgmt | hapd->conf->rsn_override_key_mgmt |
 	      hapd->conf->rsn_override_key_mgmt_2) & WPA_KEY_MGMT_OWE) &&
@@ -6345,7 +6396,7 @@ static void handle_assoc(struct hostapd_data *hapd,
 			hostapd_logger(hapd, mgmt->sa,
 				       HOSTAPD_MODULE_IEEE80211,
 				       HOSTAPD_LEVEL_INFO,
-				       "Station tried to associate before authentication (aid=%d flags=0x%x)",
+				       "Station tried to associate before authentication (aid=%d flags=0x%llx)",
 				       sta ? sta->aid : -1,
 				       sta ? sta->flags : 0);
 			send_deauth(hapd, mgmt->sa,
diff --git a/src/ap/ieee802_11.h b/src/ap/ieee802_11.h
index 991377c1c483..82c471e92725 100644
--- a/src/ap/ieee802_11.h
+++ b/src/ap/ieee802_11.h
@@ -328,4 +328,17 @@ void hostapd_link_reconf_resp_tx_status(struct hostapd_data *hapd,
 size_t hostapd_eid_eht_ml_tid_to_link_map_len(struct hostapd_data *hapd);
 u8 * hostapd_eid_eht_ml_tid_to_link_map(struct hostapd_data *hapd, u8 *eid);
 
+size_t hostapd_eid_uhr_capab_len(struct hostapd_data *hapd,
+				 enum ieee80211_op_mode opmode);
+u8 * hostapd_eid_uhr_capab(struct hostapd_data *hapd, u8 *eid,
+			   enum ieee80211_op_mode opmode);
+u8 * hostapd_eid_uhr_operation(struct hostapd_data *hapd, u8 *eid, bool beacon);
+u16 copy_sta_uhr_capab(struct hostapd_data *hapd, struct sta_info *sta,
+		       enum ieee80211_op_mode opmode,
+		       const u8 *uhr_capab, size_t uhr_capab_len);
+void hostapd_get_uhr_capab(struct hostapd_data *hapd,
+			   const struct ieee80211_uhr_capabilities *src,
+			   struct ieee80211_uhr_capabilities *dest,
+			   size_t len);
+
 #endif /* IEEE802_11_H */
diff --git a/src/ap/ieee802_11_uhr.c b/src/ap/ieee802_11_uhr.c
new file mode 100644
index 000000000000..d7b00746bd93
--- /dev/null
+++ b/src/ap/ieee802_11_uhr.c
@@ -0,0 +1,145 @@
+/*
+ * hostapd / IEEE 802.11bn UHR
+ * Copyright (C) 2025 Intel Corporation
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#include "utils/includes.h"
+#include "utils/common.h"
+#include "common/ocv.h"
+#include "common/wpa_ctrl.h"
+#include "crypto/crypto.h"
+#include "crypto/dh_groups.h"
+#include "hostapd.h"
+#include "sta_info.h"
+#include "ap_drv_ops.h"
+#include "wpa_auth.h"
+#include "ieee802_11.h"
+
+
+size_t hostapd_eid_uhr_capab_len(struct hostapd_data *hapd,
+				 enum ieee80211_op_mode opmode)
+{
+	struct hostapd_hw_modes *mode;
+	struct uhr_capabilities *uhr_cap;
+
+	mode = hapd->iface->current_mode;
+	if (!mode)
+		return 0;
+
+	uhr_cap = &mode->uhr_capab[opmode];
+	if (!uhr_cap->uhr_supported)
+		return 0;
+
+	return 6;
+}
+
+
+u8 * hostapd_eid_uhr_capab(struct hostapd_data *hapd, u8 *eid,
+			   enum ieee80211_op_mode opmode)
+{
+	struct hostapd_hw_modes *mode;
+	struct uhr_capabilities *uhr_cap;
+	struct ieee80211_uhr_capabilities *cap;
+	u8 *pos = eid, *length_pos;
+
+	mode = hapd->iface->current_mode;
+	if (!mode)
+		return eid;
+
+	uhr_cap = &mode->uhr_capab[opmode];
+	if (!uhr_cap->uhr_supported)
+		return eid;
+
+	*pos++ = WLAN_EID_EXTENSION;
+	length_pos = pos++;
+	*pos++ = WLAN_EID_EXT_UHR_CAPABILITIES;
+
+	cap = (struct ieee80211_uhr_capabilities *) pos;
+	os_memcpy(cap->mac, uhr_cap->mac, sizeof(cap->mac));
+	os_memcpy(cap->phy, uhr_cap->phy, sizeof(cap->phy));
+	pos += sizeof(*cap);
+
+	*length_pos = pos - (eid + 2);
+	return pos;
+}
+
+
+u8 * hostapd_eid_uhr_operation(struct hostapd_data *hapd, u8 *eid, bool beacon)
+{
+	struct ieee80211_uhr_operation *oper;
+	u8 *pos = eid;
+
+	if (!hapd->iface->current_mode)
+		return eid;
+
+	*pos++ = WLAN_EID_EXTENSION;
+	*pos++ = 1 + sizeof(*oper);
+	*pos++ = WLAN_EID_EXT_UHR_OPERATION;
+
+	oper = (void *) pos;
+	oper->oper_params = 0;
+
+	/* TODO: Fill in appropriate UHR-MCS max Nss information */
+	oper->basic_uhr_mcs_nss_set[0] = 0x11;
+	oper->basic_uhr_mcs_nss_set[1] = 0x00;
+	oper->basic_uhr_mcs_nss_set[2] = 0x00;
+	oper->basic_uhr_mcs_nss_set[3] = 0x00;
+
+	return pos + sizeof(*oper);
+}
+
+
+static bool ieee80211_invalid_uhr_cap_size(enum hostapd_hw_mode mode,
+					   const u8 *uhr_cap, size_t len)
+{
+	return len < sizeof(struct ieee80211_uhr_capabilities);
+}
+
+
+u16 copy_sta_uhr_capab(struct hostapd_data *hapd, struct sta_info *sta,
+		       enum ieee80211_op_mode opmode,
+		       const u8 *uhr_capab, size_t uhr_capab_len)
+{
+	struct hostapd_hw_modes *c_mode = hapd->iface->current_mode;
+	enum hostapd_hw_mode mode = c_mode ? c_mode->mode : NUM_HOSTAPD_MODES;
+
+	if (!hostapd_is_uhr_enabled(hapd) || !uhr_capab ||
+	    ieee80211_invalid_uhr_cap_size(mode, uhr_capab, uhr_capab_len)) {
+		sta->flags &= ~WLAN_STA_UHR;
+		os_free(sta->uhr_capab);
+		sta->uhr_capab = NULL;
+		return WLAN_STATUS_SUCCESS;
+	}
+
+	os_free(sta->uhr_capab);
+	sta->uhr_capab = os_memdup(uhr_capab, uhr_capab_len);
+	if (!sta->uhr_capab) {
+		sta->uhr_capab_len = 0;
+		return WLAN_STATUS_UNSPECIFIED_FAILURE;
+	}
+
+	sta->flags |= WLAN_STA_UHR;
+	sta->uhr_capab_len = uhr_capab_len;
+
+	return WLAN_STATUS_SUCCESS;
+}
+
+
+void hostapd_get_uhr_capab(struct hostapd_data *hapd,
+			   const struct ieee80211_uhr_capabilities *src,
+			   struct ieee80211_uhr_capabilities *dest,
+			   size_t len)
+{
+	if (!src || !dest)
+		return;
+
+	if (len > sizeof(*dest))
+		len = sizeof(*dest);
+	/* TODO: mask out unsupported features */
+
+	os_memset(dest, 0, sizeof(*dest));
+	os_memcpy(dest, src, len);
+}
diff --git a/src/ap/sta_info.c b/src/ap/sta_info.c
index fc16b1a426bd..386cd24b21c6 100644
--- a/src/ap/sta_info.c
+++ b/src/ap/sta_info.c
@@ -474,6 +474,7 @@ void ap_free_sta(struct hostapd_data *hapd, struct sta_info *sta)
 	os_free(sta->he_capab);
 	os_free(sta->he_6ghz_capab);
 	os_free(sta->eht_capab);
+	os_free(sta->uhr_capab);
 	hostapd_free_psk_list(sta->psk);
 	os_free(sta->identity);
 	os_free(sta->radius_cui);
@@ -596,7 +597,7 @@ void ap_handle_timer(void *eloop_ctx, void *timeout_ctx)
 	int reason;
 	int max_inactivity = hapd->conf->ap_max_inactivity;
 
-	wpa_printf(MSG_DEBUG, "%s: %s: " MACSTR " flags=0x%x timeout_next=%d",
+	wpa_printf(MSG_DEBUG, "%s: %s: " MACSTR " flags=0x%llx timeout_next=%d",
 		   hapd->conf->iface, __func__, MAC2STR(sta->addr), sta->flags,
 		   sta->timeout_next);
 	if (sta->timeout_next == STA_REMOVE) {
@@ -1958,13 +1959,13 @@ void ap_sta_clear_assoc_timeout(struct hostapd_data *hapd,
 }
 
 
-int ap_sta_flags_txt(u32 flags, char *buf, size_t buflen)
+int ap_sta_flags_txt(unsigned long long flags, char *buf, size_t buflen)
 {
 	int res;
 
 	buf[0] = '\0';
 	res = os_snprintf(buf, buflen,
-			  "%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s",
+			  "%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s",
 			  (flags & WLAN_STA_AUTH ? "[AUTH]" : ""),
 			  (flags & WLAN_STA_ASSOC ? "[ASSOC]" : ""),
 			  (flags & WLAN_STA_AUTHORIZED ? "[AUTHORIZED]" : ""),
@@ -1985,6 +1986,7 @@ int ap_sta_flags_txt(u32 flags, char *buf, size_t buflen)
 			  (flags & WLAN_STA_VHT ? "[VHT]" : ""),
 			  (flags & WLAN_STA_HE ? "[HE]" : ""),
 			  (flags & WLAN_STA_EHT ? "[EHT]" : ""),
+			  (flags & WLAN_STA_UHR ? "[UHR]" : ""),
 			  (flags & WLAN_STA_6GHZ ? "[6GHZ]" : ""),
 			  (flags & WLAN_STA_VENDOR_VHT ? "[VENDOR_VHT]" : ""),
 			  (flags & WLAN_STA_SPP_AMSDU ? "[SPP-A-MSDU]" : ""),
@@ -2109,7 +2111,7 @@ int ap_sta_re_add(struct hostapd_data *hapd, struct sta_info *sta)
 	if (hostapd_sta_add(hapd, sta->addr, 0, 0,
 			    sta->supported_rates,
 			    sta->supported_rates_len,
-			    0, NULL, NULL, NULL, 0, NULL, 0, NULL,
+			    0, NULL, NULL, NULL, 0, NULL, 0, NULL, 0, NULL,
 			    sta->flags, 0, 0, 0, 0,
 			    mld_link_addr, mld_link_sta, eml_cap, epp_sta)) {
 		hostapd_logger(hapd, sta->addr,
diff --git a/src/ap/sta_info.h b/src/ap/sta_info.h
index a7d6c280167d..4154739bb005 100644
--- a/src/ap/sta_info.h
+++ b/src/ap/sta_info.h
@@ -20,35 +20,36 @@
 #include "hostapd.h"
 
 /* STA flags */
-#define WLAN_STA_AUTH BIT(0)
-#define WLAN_STA_ASSOC BIT(1)
-#define WLAN_STA_SPP_AMSDU BIT(2)
-#define WLAN_STA_AUTHORIZED BIT(5)
-#define WLAN_STA_PENDING_POLL BIT(6) /* pending activity poll not ACKed */
-#define WLAN_STA_SHORT_PREAMBLE BIT(7)
-#define WLAN_STA_PREAUTH BIT(8)
-#define WLAN_STA_WMM BIT(9)
-#define WLAN_STA_MFP BIT(10)
-#define WLAN_STA_HT BIT(11)
-#define WLAN_STA_WPS BIT(12)
-#define WLAN_STA_MAYBE_WPS BIT(13)
-#define WLAN_STA_WDS BIT(14)
-#define WLAN_STA_ASSOC_REQ_OK BIT(15)
-#define WLAN_STA_WPS2 BIT(16)
-#define WLAN_STA_GAS BIT(17)
-#define WLAN_STA_VHT BIT(18)
-#define WLAN_STA_WNM_SLEEP_MODE BIT(19)
-#define WLAN_STA_VHT_OPMODE_ENABLED BIT(20)
-#define WLAN_STA_VENDOR_VHT BIT(21)
-#define WLAN_STA_PENDING_FILS_ERP BIT(22)
-#define WLAN_STA_MULTI_AP BIT(23)
-#define WLAN_STA_HE BIT(24)
-#define WLAN_STA_6GHZ BIT(25)
-#define WLAN_STA_PENDING_PASN_FILS_ERP BIT(26)
-#define WLAN_STA_EHT BIT(27)
-#define WLAN_STA_PENDING_DISASSOC_CB BIT(29)
-#define WLAN_STA_PENDING_DEAUTH_CB BIT(30)
-#define WLAN_STA_NONERP BIT(31)
+#define WLAN_STA_AUTH BIT_ULL(0)
+#define WLAN_STA_ASSOC BIT_ULL(1)
+#define WLAN_STA_SPP_AMSDU BIT_ULL(2)
+#define WLAN_STA_AUTHORIZED BIT_ULL(5)
+#define WLAN_STA_PENDING_POLL BIT_ULL(6) /* pending activity poll not ACKed */
+#define WLAN_STA_SHORT_PREAMBLE BIT_ULL(7)
+#define WLAN_STA_PREAUTH BIT_ULL(8)
+#define WLAN_STA_WMM BIT_ULL(9)
+#define WLAN_STA_MFP BIT_ULL(10)
+#define WLAN_STA_HT BIT_ULL(11)
+#define WLAN_STA_WPS BIT_ULL(12)
+#define WLAN_STA_MAYBE_WPS BIT_ULL(13)
+#define WLAN_STA_WDS BIT_ULL(14)
+#define WLAN_STA_ASSOC_REQ_OK BIT_ULL(15)
+#define WLAN_STA_WPS2 BIT_ULL(16)
+#define WLAN_STA_GAS BIT_ULL(17)
+#define WLAN_STA_VHT BIT_ULL(18)
+#define WLAN_STA_WNM_SLEEP_MODE BIT_ULL(19)
+#define WLAN_STA_VHT_OPMODE_ENABLED BIT_ULL(20)
+#define WLAN_STA_VENDOR_VHT BIT_ULL(21)
+#define WLAN_STA_PENDING_FILS_ERP BIT_ULL(22)
+#define WLAN_STA_MULTI_AP BIT_ULL(23)
+#define WLAN_STA_HE BIT_ULL(24)
+#define WLAN_STA_6GHZ BIT_ULL(25)
+#define WLAN_STA_PENDING_PASN_FILS_ERP BIT_ULL(26)
+#define WLAN_STA_EHT BIT_ULL(27)
+#define WLAN_STA_PENDING_DISASSOC_CB BIT_ULL(29)
+#define WLAN_STA_PENDING_DEAUTH_CB BIT_ULL(30)
+#define WLAN_STA_NONERP BIT_ULL(31)
+#define WLAN_STA_UHR BIT_ULL(32)
 
 /* Maximum number of supported rates (from both Supported Rates and Extended
  * Supported Rates IEs). */
@@ -93,7 +94,7 @@ struct sta_info {
 	struct dl_list ip6addr; /* list head for struct ip6addr */
 	u16 aid; /* STA's unique AID (1 .. 2007) or 0 if not yet assigned */
 	u16 disconnect_reason_code; /* RADIUS server override */
-	u32 flags; /* Bitfield of WLAN_STA_* */
+	unsigned long long flags; /* Bitfield of WLAN_STA_* */
 	u16 capability;
 	u16 listen_interval; /* or beacon_int for APs */
 	u8 supported_rates[WLAN_SUPP_RATES_MAX];
@@ -207,6 +208,8 @@ struct sta_info {
 	struct ieee80211_he_6ghz_band_cap *he_6ghz_capab;
 	struct ieee80211_eht_capabilities *eht_capab;
 	size_t eht_capab_len;
+	struct ieee80211_uhr_capabilities *uhr_capab;
+	size_t uhr_capab_len;
 
 	int sa_query_count; /* number of pending SA Query requests;
 			     * 0 = no SA Query in progress */
@@ -418,7 +421,7 @@ void ap_sta_clear_disconnect_timeouts(struct hostapd_data *hapd,
 void ap_sta_clear_assoc_timeout(struct hostapd_data *hapd,
 				struct sta_info *sta);
 
-int ap_sta_flags_txt(u32 flags, char *buf, size_t buflen);
+int ap_sta_flags_txt(unsigned long long flags, char *buf, size_t buflen);
 void ap_sta_delayed_1x_auth_fail_disconnect(struct hostapd_data *hapd,
 					    struct sta_info *sta,
 					    unsigned timeout);
diff --git a/src/common/ieee802_11_common.c b/src/common/ieee802_11_common.c
index 0fd15603c7b7..9b2e8823a7b8 100644
--- a/src/common/ieee802_11_common.c
+++ b/src/common/ieee802_11_common.c
@@ -434,6 +434,14 @@ static int ieee802_11_parse_extension(const u8 *pos, size_t elen,
 		elems->pasn_encrypted_data = pos;
 		elems->pasn_encrypted_data_len = elen;
 		break;
+	case WLAN_EID_EXT_UHR_CAPABILITIES:
+		elems->uhr_capabilities = pos;
+		elems->uhr_capabilities_len = elen;
+		break;
+	case WLAN_EID_EXT_UHR_OPERATION:
+		elems->uhr_operation = pos;
+		elems->uhr_operation_len = elen;
+		break;
 	default:
 		if (show_errors) {
 			wpa_printf(MSG_MSGDUMP,
@@ -1008,6 +1016,14 @@ void ieee802_11_elems_clear_ext_ids(struct ieee802_11_elems *elems,
 			elems->eht_operation = NULL;
 			elems->eht_operation_len = 0;
 			break;
+		case WLAN_EID_EXT_UHR_CAPABILITIES:
+			elems->uhr_capabilities = NULL;
+			elems->uhr_capabilities_len = 0;
+			break;
+		case WLAN_EID_EXT_UHR_OPERATION:
+			elems->uhr_operation = NULL;
+			elems->uhr_operation_len = 0;
+			break;
 		}
 	}
 }
diff --git a/src/common/ieee802_11_common.h b/src/common/ieee802_11_common.h
index 438f70dd57b5..a2cdaf41898e 100644
--- a/src/common/ieee802_11_common.h
+++ b/src/common/ieee802_11_common.h
@@ -128,6 +128,8 @@ struct ieee802_11_elems {
 	const u8 *rsn_selection;
 	const u8 *wfa_capab;
 	const u8 *proximity_ranging;
+	const u8 *uhr_capabilities;
+	const u8 *uhr_operation;
 
 	u8 ssid_len;
 	u8 supp_rates_len;
@@ -198,6 +200,8 @@ struct ieee802_11_elems {
 	size_t rsn_selection_len;
 	u8 wfa_capab_len;
 	size_t proximity_ranging_len;
+	u8 uhr_capabilities_len;
+	u8 uhr_operation_len;
 
 	struct mb_ies_info mb_ies;
 
diff --git a/src/common/ieee802_11_defs.h b/src/common/ieee802_11_defs.h
index 55e74f93ac08..17bd3d8d2946 100644
--- a/src/common/ieee802_11_defs.h
+++ b/src/common/ieee802_11_defs.h
@@ -541,6 +541,8 @@
 #define WLAN_EID_EXT_BANDWIDTH_INDICATION 135
 #define WLAN_EID_EXT_KNOWN_STA_IDENTIFICATION 136
 #define WLAN_EID_EXT_PASN_ENCRYPTED_DATA 140
+#define WLAN_EID_EXT_UHR_OPERATION 151
+#define WLAN_EID_EXT_UHR_CAPABILITIES 152
 
 /* Extended Capabilities field */
 #define WLAN_EXT_CAPAB_20_40_COEX 0
@@ -1412,6 +1414,7 @@ struct ieee80211_ampe_ie {
 #define HT_OPER_PARAM_PCO_PHASE				((u16) BIT(11))
 /* B36..B39 - Reserved */
 
+#define BSS_MEMBERSHIP_SELECTOR_UHR_PHY 120
 #define BSS_MEMBERSHIP_SELECTOR_EHT_PHY 121
 #define BSS_MEMBERSHIP_SELECTOR_HE_PHY 122
 #define BSS_MEMBERSHIP_SELECTOR_SAE_H2E_ONLY 123
@@ -3292,6 +3295,39 @@ struct ieee80211_s1g_beacon_compat {
 	le32 tsf_completion;
 } STRUCT_PACKED;
 
+/* UHR Capabilities element format */
+struct ieee80211_uhr_capabilities {
+	/* UHR MAC Capabilities Information */
+	u8 mac[5];
+	/* UHR PHY Capabilities Information */
+	u8 phy[1];
+} STRUCT_PACKED;
+
+#define UHR_OPER_PARAMS_DPS_ENA               0x0001
+#define UHR_OPER_PARAMS_NPCA_ENA              0x0002
+#define UHR_OPER_PARAMS_PEDCA_ENA             0x0004
+#define UHR_OPER_PARAMS_DBE_ENA               0x0008
+
+/* UHR Operation element format */
+struct ieee80211_uhr_operation {
+	le16 oper_params; /* UHR Operation Parameters: UHR_OPER_* bits */
+	u8 basic_uhr_mcs_nss_set[4];
+	/* FIXME: DPS, P-EDCA, DBE */
+} STRUCT_PACKED;
+
+/* Max size in Draft P802.11bn D1.3 with DPS, NPCA, P-EDCA, DBE */
+#define IEEE80211_UHR_OPER_MAX_SIZE	\
+	(sizeof(struct ieee80211_uhr_operation) + 4 + 6 + 3 + 3)
+
+/* IEEE P802.11bn/D1.3, Figure 9-aa4 */
+#define UHR_OPER_PARAMS_NPCA_PRIM_CHAN_OFFS		0x0000000F
+#define UHR_OPER_PARAMS_NPCA_NPCA_MIN_DUR_THRESH	0x000000F0
+#define UHR_OPER_PARAMS_NPCA_NPCA_SWITCH_DELAY		0x00003F00
+#define UHR_OPER_PARAMS_NPCA_NPCA_SWITCH_BACK_DELAY	0x000FC000
+#define UHR_OPER_PARAMS_NPCA_INIT_NPCA_QRSC		0x00300000
+#define UHR_OPER_PARAMS_NPCA_MOPLEN_NPCA		0x00400000
+#define UHR_OPER_PARAMS_NPCA_DIS_SUBCH_BITMAP_PRES	0x00800000
+
 #ifdef _MSC_VER
 #pragma pack(pop)
 #endif /* _MSC_VER */
diff --git a/src/drivers/driver.h b/src/drivers/driver.h
index cefc73942ea6..d437dc7d58c5 100644
--- a/src/drivers/driver.h
+++ b/src/drivers/driver.h
@@ -216,6 +216,13 @@ struct eht_capabilities {
 	u8 ppet[EHT_PPE_THRESH_CAPAB_LEN];
 };
 
+/* struct uhr_capabilities - IEEE 802.11bn UHR capabilities */
+struct uhr_capabilities {
+	bool uhr_supported;
+	u8 mac[5];
+	u8 phy[1];
+};
+
 #define HOSTAPD_MODE_FLAG_HT_INFO_KNOWN BIT(0)
 #define HOSTAPD_MODE_FLAG_VHT_INFO_KNOWN BIT(1)
 #define HOSTAPD_MODE_FLAG_HE_INFO_KNOWN BIT(2)
@@ -326,6 +333,11 @@ struct hostapd_hw_modes {
 	 * eht_capab - EHT (IEEE 802.11be) capabilities
 	 */
 	struct eht_capabilities eht_capab[IEEE80211_MODE_NUM];
+
+	/**
+	 * uhr_capab - UHR (IEEE 802.11bb) capabilities
+	 */
+	struct uhr_capabilities uhr_capab[IEEE80211_MODE_NUM];
 };
 
 
@@ -1403,6 +1415,11 @@ struct wpa_driver_associate_params {
 	 */
 	int disable_eht;
 
+	/**
+	 * disable_uhr - Disable UHR for this connection
+	 */
+	int disable_uhr;
+
 	/*
 	 * mld_params - MLD association parameters
 	 */
@@ -1928,6 +1945,12 @@ struct wpa_driver_ap_params {
 	 * sae_password - SAE password for SAE offload
 	 */
 	const char *sae_password;
+
+	/**
+	 * uhr_oper - Full UHR operation (beacon only has abridged data),
+	 * includes the extended element header
+	 */
+	u8 *uhr_oper;
 };
 
 struct wpa_driver_mesh_bss_params {
@@ -2702,6 +2725,9 @@ struct hostapd_sta_add_params {
 	s8 mld_link_id;
 	const u8 *mld_link_addr;
 	u16 eml_cap;
+
+	const struct ieee80211_uhr_capabilities *uhr_capab;
+	size_t uhr_capab_len;
 };
 
 struct mac_address {
@@ -7380,6 +7406,8 @@ bool he_supported(const struct hostapd_hw_modes *hw_mode,
 		  enum ieee80211_op_mode op_mode);
 bool eht_supported(const struct hostapd_hw_modes *hw_mode,
 		   enum ieee80211_op_mode op_mode);
+bool uhr_supported(const struct hostapd_hw_modes *hw_mode,
+		   enum ieee80211_op_mode op_mode);
 
 struct wowlan_triggers *
 wpa_get_wowlan_triggers(const char *wowlan_triggers,
diff --git a/src/drivers/driver_common.c b/src/drivers/driver_common.c
index 3653c639e843..97ddaa1670f8 100644
--- a/src/drivers/driver_common.c
+++ b/src/drivers/driver_common.c
@@ -220,6 +220,12 @@ bool eht_supported(const struct hostapd_hw_modes *hw_mode,
 	return hw_mode->eht_capab[op_mode].eht_supported;
 }
 
+bool uhr_supported(const struct hostapd_hw_modes *hw_mode,
+		   enum ieee80211_op_mode op_mode)
+{
+	return hw_mode->uhr_capab[op_mode].uhr_supported;
+}
+
 
 static int wpa_check_wowlan_trigger(const char *start, const char *trigger,
 				    int capa_trigger, u8 *param_trigger)
diff --git a/src/drivers/driver_nl80211.c b/src/drivers/driver_nl80211.c
index 031e68704dc8..1f56bbf8f963 100644
--- a/src/drivers/driver_nl80211.c
+++ b/src/drivers/driver_nl80211.c
@@ -5866,6 +5866,15 @@ static int wpa_driver_nl80211_set_ap(void *priv,
 		goto fail;
 #endif /* CONFIG_IEEE80211AX */
 
+#ifdef CONFIG_IEEE80211BN
+	if (params->uhr_oper &&
+	    nla_put(msg, NL80211_ATTR_UHR_OPERATION,
+		    /* nl80211 wants it without the extended element header */
+		    params->uhr_oper[1] - 1,
+		    params->uhr_oper + 3))
+		goto fail;
+#endif /* CONFIG_IEEE80211BN */
+
 #ifdef CONFIG_SAE
 	if (wpa_key_mgmt_sae(params->key_mgmt_suites) &&
 	    nl80211_put_sae_pwe(msg, params->sae_pwe) < 0)
@@ -6138,6 +6147,14 @@ static int wpa_driver_nl80211_sta_add(void *priv,
 				goto fail;
 		}
 
+		if (params->uhr_capab) {
+			wpa_hexdump(MSG_DEBUG, "  * uhr_capab",
+				    params->uhr_capab, params->uhr_capab_len);
+			if (nla_put(msg, NL80211_ATTR_UHR_CAPABILITY,
+				    params->uhr_capab_len, params->uhr_capab))
+				goto fail;
+		}
+
 		if (params->ext_capab) {
 			wpa_hexdump(MSG_DEBUG, "  * ext_capab",
 				    params->ext_capab, params->ext_capab_len);
@@ -7027,6 +7044,12 @@ static int nl80211_ht_vht_overrides(struct nl_msg *msg,
 			return -1;
 	}
 
+	if (params->disable_uhr) {
+		wpa_printf(MSG_DEBUG, "  * UHR disabled");
+		if (nla_put_flag(msg, NL80211_ATTR_DISABLE_UHR))
+			return -1;
+	}
+
 	return 0;
 }
 
diff --git a/src/drivers/driver_nl80211_capa.c b/src/drivers/driver_nl80211_capa.c
index 18154e2445a4..5b36982dea92 100644
--- a/src/drivers/driver_nl80211_capa.c
+++ b/src/drivers/driver_nl80211_capa.c
@@ -2012,6 +2012,7 @@ static void phy_info_iftype_copy(struct hostapd_hw_modes *mode,
 	size_t len;
 	struct he_capabilities *he_capab = &mode->he_capab[opmode];
 	struct eht_capabilities *eht_capab = &mode->eht_capab[opmode];
+	struct uhr_capabilities *uhr_capab = &mode->uhr_capab[opmode];
 
 	switch (opmode) {
 	case IEEE80211_MODE_INFRA:
@@ -2119,6 +2120,24 @@ static void phy_info_iftype_copy(struct hostapd_hw_modes *mode,
 			  nla_data(tb[NL80211_BAND_IFTYPE_ATTR_EHT_CAP_PPE]),
 			  len);
 	}
+
+	if (!tb[NL80211_BAND_IFTYPE_ATTR_UHR_CAP_MAC] ||
+	    !tb[NL80211_BAND_IFTYPE_ATTR_UHR_CAP_PHY])
+		return;
+
+	uhr_capab->uhr_supported = true;
+
+	if (tb[NL80211_BAND_IFTYPE_ATTR_UHR_CAP_MAC] &&
+	    nla_len(tb[NL80211_BAND_IFTYPE_ATTR_UHR_CAP_MAC]) >= (int)sizeof(uhr_capab->mac))
+		os_memcpy(uhr_capab->mac,
+			  nla_data(tb[NL80211_BAND_IFTYPE_ATTR_UHR_CAP_MAC]),
+			  sizeof(uhr_capab->mac));
+
+	if (tb[NL80211_BAND_IFTYPE_ATTR_UHR_CAP_PHY] &&
+	    nla_len(tb[NL80211_BAND_IFTYPE_ATTR_UHR_CAP_PHY]) >= (int)sizeof(uhr_capab->phy))
+		os_memcpy(uhr_capab->phy,
+			  nla_data(tb[NL80211_BAND_IFTYPE_ATTR_UHR_CAP_PHY]),
+			  sizeof(uhr_capab->phy));
 }
 
 
diff --git a/tests/hwsim/example-hostapd.config b/tests/hwsim/example-hostapd.config
index 21e44017fb62..e17285240d75 100644
--- a/tests/hwsim/example-hostapd.config
+++ b/tests/hwsim/example-hostapd.config
@@ -53,6 +53,8 @@ CONFIG_LIBNL3_ROUTE=y
 CONFIG_IEEE80211R=y
 CONFIG_IEEE80211AC=y
 CONFIG_IEEE80211AX=y
+CONFIG_IEEE80211BE=y
+CONFIG_IEEE80211BN=y
 
 CONFIG_OCV=y
 
diff --git a/tests/hwsim/example-wpa_supplicant.config b/tests/hwsim/example-wpa_supplicant.config
index 20f3936294ce..aae5f7aff242 100644
--- a/tests/hwsim/example-wpa_supplicant.config
+++ b/tests/hwsim/example-wpa_supplicant.config
@@ -175,3 +175,5 @@ CONFIG_NAN=y
 
 CONFIG_ENC_ASSOC=y
 CONFIG_PMKSA_PRIVACY=y
+
+CONFIG_IEEE80211BN=y
diff --git a/tests/hwsim/test_uhr.py b/tests/hwsim/test_uhr.py
new file mode 100644
index 000000000000..a5dfea0aee35
--- /dev/null
+++ b/tests/hwsim/test_uhr.py
@@ -0,0 +1,145 @@
+# UHR tests
+# Copyright (c) 2022-2024, Qualcomm Innovation Center, Inc.
+# Copyright (C) 2025 Intel Corporation
+#
+# This software may be distributed under the terms of the BSD license.
+# See README for more details.
+
+import binascii
+import subprocess
+import tempfile
+import time
+
+import hostapd
+from utils import *
+from hwsim import HWSimRadio
+import hwsim_utils
+from wpasupplicant import WpaSupplicant
+from test_eht import eht_verify_status, traffic_test
+
+def uhr_verify_wifi_version(dev):
+    status = dev.get_status()
+    logger.info("station status: " + str(status))
+
+    if 'wifi_generation' not in status:
+        raise Exception("Missing wifi_generation information")
+    if status['wifi_generation'] != "8":
+        raise Exception("Unexpected wifi_generation value: " + status['wifi_generation'])
+
+def uhr_verify_status(wpas, hapd, is_ht=False, is_vht=False):
+    status = hapd.get_status()
+
+    logger.info("hostapd STATUS: " + str(status))
+    if is_ht and status["ieee80211n"] != "1":
+        raise Exception("Unexpected STATUS ieee80211n value")
+    if is_vht and status["ieee80211ac"] != "1":
+        raise Exception("Unexpected STATUS ieee80211ac value")
+    if status["ieee80211ax"] != "1":
+        raise Exception("Unexpected STATUS ieee80211ax value")
+    if status["ieee80211be"] != "1":
+        raise Exception("Unexpected STATUS ieee80211be value")
+    if status["ieee80211bn"] != "1":
+        raise Exception("Unexpected STATUS ieee80211bn value")
+
+    sta = hapd.get_sta(wpas.own_addr())
+    logger.info("hostapd STA: " + str(sta))
+    if sta['addr'] == 'FAIL':
+        raise Exception("hostapd " + hapd.ifname + " did not have a STA entry for the STA " + wpas.own_addr())
+    if is_ht and "[HT]" not in sta['flags']:
+        raise Exception("Missing STA flag: HT")
+    if is_vht and "[VHT]" not in sta['flags']:
+        raise Exception("Missing STA flag: VHT")
+    if "[HE]" not in sta['flags']:
+        raise Exception("Missing STA flag: HE")
+    if "[EHT]" not in sta['flags']:
+        raise Exception("Missing STA flag: EHT")
+    if "[UHR]" not in sta['flags']:
+        raise Exception("Missing STA flag: UHR")
+
+def test_uhr_open(dev, apdev):
+    """UHR AP with open mode configuration"""
+    params = {
+        "ssid": "uhr",
+        "ieee80211ax": "1",
+        "ieee80211be": "1",
+        "ieee80211bn": "1",
+        "require_uhr": "1",
+    }
+    try:
+        hapd = hostapd.add_ap(apdev[0], params)
+    except Exception as e:
+        if isinstance(e, Exception) and \
+           str(e) == "Failed to set hostapd parameter ieee80211bn":
+            raise HwsimSkip("UHR not supported")
+        raise
+    if hapd.get_status_field("ieee80211bn") != "1":
+        raise Exception("AP STATUS did not indicate ieee80211bn=1")
+    dev[0].connect("uhr", key_mgmt="NONE", scan_freq="2412")
+    time.sleep(1)
+    sta = hapd.get_sta(dev[0].own_addr())
+    uhr_verify_status(dev[0], hapd, is_ht=True)
+    status = dev[0].request("STATUS")
+    if "wifi_generation=8" not in status:
+        raise Exception("STA STATUS did not indicate wifi_generation=7")
+
+def uhr_mld_ap_wpa2_params(ssid, passphrase=None, key_mgmt="WPA-PSK-SHA256",
+                           mfp="2", pwe=None, beacon_prot="1", bridge=False):
+    params = hostapd.wpa2_params(ssid=ssid, passphrase=passphrase,
+                                 wpa_key_mgmt=key_mgmt, ieee80211w=mfp)
+    params['ieee80211n'] = '1'
+    params['ieee80211ax'] = '1'
+    params['ieee80211be'] = '1'
+    params['ieee80211bn'] = '1'
+    params['channel'] = '1'
+    params['hw_mode'] = 'g'
+    params['group_mgmt_cipher'] = "AES-128-CMAC"
+    params['beacon_prot'] = beacon_prot
+    if bridge:
+        params['bridge'] = 'ap-br0'
+
+    if pwe is not None:
+        params['sae_pwe'] = pwe
+
+    return params
+
+def uhr_mld_enable_ap(iface, link_id, params):
+    hapd = hostapd.add_mld_link(iface, link_id, params)
+    hapd.enable()
+
+    ev = hapd.wait_event(["AP-ENABLED", "AP-DISABLED"], timeout=1)
+    if ev is None:
+        raise Exception("AP startup timed out")
+    if "AP-ENABLED" not in ev:
+        raise Exception("AP startup failed")
+
+    return hapd
+
+def run_uhr_mld_sae_single_link(dev, apdev, anti_clogging_token=False):
+    with HWSimRadio(use_mlo=True) as (hapd_radio, hapd_iface), \
+            HWSimRadio(use_mlo=True) as (wpas_radio, wpas_iface):
+        wpas = WpaSupplicant(global_iface='/tmp/wpas-wlan5')
+        wpas.interface_add(wpas_iface)
+        check_sae_capab(wpas)
+
+        passphrase = 'qwertyuiop'
+        ssid = "mld_ap_sae_single_link"
+        params = uhr_mld_ap_wpa2_params(ssid, passphrase, key_mgmt="SAE",
+                                        mfp="2", pwe='2')
+        if anti_clogging_token:
+            params['sae_anti_clogging_threshold'] = '0'
+
+        hapd0 = uhr_mld_enable_ap(hapd_iface, 0, params)
+
+        wpas.set("sae_pwe", "1")
+        wpas.connect(ssid, sae_password=passphrase, scan_freq="2412",
+                     key_mgmt="SAE", ieee80211w="2")
+
+        eht_verify_status(wpas, hapd0, 2412, 20, is_ht=True, mld=True,
+                          valid_links=1, active_links=1)
+        uhr_verify_status(wpas, hapd0, is_ht=True)
+        uhr_verify_wifi_version(wpas)
+        traffic_test(wpas, hapd0)
+
+def test_uhr_mld_sae_single_link(dev, apdev):
+    """UHR MLD AP with MLD client SAE H2E connection using single link"""
+    run_uhr_mld_sae_single_link(dev, apdev)
diff --git a/wpa_supplicant/Android.mk b/wpa_supplicant/Android.mk
index b8c7152c5b54..b63ab7a45cf8 100644
--- a/wpa_supplicant/Android.mk
+++ b/wpa_supplicant/Android.mk
@@ -975,6 +975,9 @@ endif
 ifdef CONFIG_IEEE80211BE
 OBJS += src/ap/ieee802_11_eht.c
 endif
+ifdef CONFIG_IEEE80211BN
+OBJS += src/ap/ieee802_11_uhr.c
+endif
 ifdef CONFIG_WNM_AP
 L_CFLAGS += -DCONFIG_WNM_AP
 OBJS += src/ap/wnm_ap.c
diff --git a/wpa_supplicant/Makefile b/wpa_supplicant/Makefile
index fbfc7307f93a..c6f3378ca9b5 100644
--- a/wpa_supplicant/Makefile
+++ b/wpa_supplicant/Makefile
@@ -1049,6 +1049,9 @@ endif
 ifdef CONFIG_IEEE80211BE
 OBJS += ../src/ap/ieee802_11_eht.o
 endif
+ifdef CONFIG_IEEE80211BN
+OBJS += ../src/ap/ieee802_11_uhr.o
+endif
 ifdef CONFIG_WNM_AP
 CFLAGS += -DCONFIG_WNM_AP
 OBJS += ../src/ap/wnm_ap.o
@@ -1071,6 +1074,10 @@ OBJS += ../src/eap_server/eap_server_methods.o
 ifdef CONFIG_IEEE80211AC
 CFLAGS += -DCONFIG_IEEE80211AC
 endif
+ifdef CONFIG_IEEE80211BN
+CONFIG_IEEE80211BE=y
+CFLAGS += -DCONFIG_IEEE80211BN
+endif
 ifdef CONFIG_IEEE80211BE
 CONFIG_IEEE80211AX=y
 CFLAGS += -DCONFIG_IEEE80211BE
diff --git a/wpa_supplicant/config.c b/wpa_supplicant/config.c
index 20dd4eaf3c9e..65a26bc4b719 100644
--- a/wpa_supplicant/config.c
+++ b/wpa_supplicant/config.c
@@ -2835,6 +2835,7 @@ static const struct parse_data ssid_fields[] = {
 	{ INT_RANGE(transition_disable, 0, 255) },
 	{ INT_RANGE(sae_pk, 0, 2) },
 	{ INT_RANGE(disable_eht, 0, 1)},
+	{ INT_RANGE(disable_uhr, 0, 1)},
 	{ INT_RANGE(enable_4addr_mode, 0, 1)},
 	{ INT_RANGE(max_idle, 0, 65535)},
 	{ INT_RANGE(ssid_protection, 0, 1)},
diff --git a/wpa_supplicant/config_file.c b/wpa_supplicant/config_file.c
index 2f62e6cee167..e54b2dd4b598 100644
--- a/wpa_supplicant/config_file.c
+++ b/wpa_supplicant/config_file.c
@@ -1019,6 +1019,7 @@ static void wpa_config_write_network(FILE *f, struct wpa_ssid *ssid,
 	INT(disable_he);
 #endif /* CONFIG_HE_OVERRIDES */
 	INT(disable_eht);
+	INT(disable_uhr);
 	INT(enable_4addr_mode);
 	INT(max_idle);
 	INT(ssid_protection);
diff --git a/wpa_supplicant/config_ssid.h b/wpa_supplicant/config_ssid.h
index 43a13053090d..b1083fbacf75 100644
--- a/wpa_supplicant/config_ssid.h
+++ b/wpa_supplicant/config_ssid.h
@@ -1300,6 +1300,14 @@ struct wpa_ssid {
 	 */
 	int disable_eht;
 
+	/**
+	 * disable_uhr - Disable UHR (IEEE 802.11bn) for this network
+	 *
+	 * By default, use it if it is available, but this can be configured
+	 * to 1 to have it disabled.
+	 */
+	int disable_uhr;
+
 	/**
 	 * enable_4addr_mode - Set 4addr mode after association
 	 * 0 = Do not attempt to set 4addr mode
diff --git a/wpa_supplicant/ctrl_iface.c b/wpa_supplicant/ctrl_iface.c
index f2125b0b44fc..727d2573a053 100644
--- a/wpa_supplicant/ctrl_iface.c
+++ b/wpa_supplicant/ctrl_iface.c
@@ -2379,12 +2379,14 @@ static int wpa_supplicant_ctrl_iface_status(struct wpa_supplicant *wpa_s,
 
 		if (wpa_s->connection_set &&
 		    (wpa_s->connection_ht || wpa_s->connection_vht ||
-		     wpa_s->connection_he || wpa_s->connection_eht)) {
+		     wpa_s->connection_he || wpa_s->connection_eht ||
+		     wpa_s->connection_uhr)) {
 			ret = os_snprintf(pos, end - pos,
 					  "wifi_generation=%u\n",
+					  wpa_s->connection_uhr ? 8 :
 					  wpa_s->connection_eht ? 7 :
-					  (wpa_s->connection_he ? 6 :
-					   (wpa_s->connection_vht ? 5 : 4)));
+					  wpa_s->connection_he ? 6 :
+					  wpa_s->connection_vht ? 5 : 4);
 			if (os_snprintf_error(end - pos, ret))
 				return pos - buf;
 			pos += ret;
diff --git a/wpa_supplicant/events.c b/wpa_supplicant/events.c
index c241ea2cf827..75fca81f3e91 100644
--- a/wpa_supplicant/events.c
+++ b/wpa_supplicant/events.c
@@ -1063,6 +1063,17 @@ static int rate_match(struct wpa_supplicant *wpa_s, struct wpa_ssid *ssid,
 				continue;
 			}
 
+			if (flagged && ((rate_ie[j] & 0x7f) ==
+					BSS_MEMBERSHIP_SELECTOR_UHR_PHY)) {
+				if (!uhr_supported(mode, IEEE80211_MODE_INFRA)) {
+					if (debug_print)
+						wpa_dbg(wpa_s, MSG_DEBUG,
+							"   hardware does not support UHR PHY");
+					return 0;
+				}
+				continue;
+			}
+
 #ifdef CONFIG_SAE
 			if (flagged && ((rate_ie[j] & 0x7f) ==
 					BSS_MEMBERSHIP_SELECTOR_SAE_H2E_ONLY)) {
@@ -3555,6 +3566,8 @@ static void wpas_parse_connection_info(struct wpa_supplicant *wpa_s,
 		resp_elems.eht_capabilities;
 	if (req_elems.rrm_enabled)
 		wpa_s->rrm.rrm_used = 1;
+	wpa_s->connection_uhr = req_elems.uhr_capabilities &&
+		resp_elems.uhr_capabilities;
 
 #ifdef CONFIG_PMKSA_PRIVACY
 	if (wpa_s->assoc_resp_encrypted && resp_elems.nonce) {
diff --git a/wpa_supplicant/sme.c b/wpa_supplicant/sme.c
index 8c6bc23e644d..005b498eebc4 100644
--- a/wpa_supplicant/sme.c
+++ b/wpa_supplicant/sme.c
@@ -2984,6 +2984,7 @@ mscs_fail:
 	wpa_supplicant_apply_he_overrides(wpa_s, ssid, &params);
 #endif /* CONFIG_HE_OVERRIDES */
 	wpa_supplicant_apply_eht_overrides(wpa_s, ssid, &params);
+	wpa_supplicant_apply_uhr_overrides(wpa_s, ssid, &params);
 #ifdef CONFIG_IEEE80211R
 	if (auth_type == WLAN_AUTH_FT && wpa_s->sme.ft_ies &&
 	    get_ie(wpa_s->sme.ft_ies, wpa_s->sme.ft_ies_len,
diff --git a/wpa_supplicant/wpa_cli.c b/wpa_supplicant/wpa_cli.c
index 098ada6ceb6f..7542586e3caf 100644
--- a/wpa_supplicant/wpa_cli.c
+++ b/wpa_supplicant/wpa_cli.c
@@ -1497,6 +1497,7 @@ static const char *network_fields[] = {
 	"disable_he",
 #endif /* CONFIG_HE_OVERRIDES */
 	"disable_eht",
+	"disable_uhr",
 	"ap_max_inactivity", "dtim_period", "beacon_int",
 #ifdef CONFIG_MACSEC
 	"macsec_policy",
diff --git a/wpa_supplicant/wpa_supplicant.c b/wpa_supplicant/wpa_supplicant.c
index 937a26265e5b..d0756ade8e64 100644
--- a/wpa_supplicant/wpa_supplicant.c
+++ b/wpa_supplicant/wpa_supplicant.c
@@ -2258,7 +2258,7 @@ int wpa_supplicant_set_suites(struct wpa_supplicant *wpa_s,
 		wpa_sm_set_param(wpa_s->wpa, WPA_PARAM_USE_EXT_KEY_ID, 0);
 	}
 
-	/* Mark WMM enabled for any HT/VHT/HE/EHT association to get more
+	/* Mark WMM enabled for any HT/VHT/HE/EHT/UHR association to get more
 	 * appropriate advertisement of the supported number of PTKSA receive
 	 * counters. In theory, this could be based on a driver capability, but
 	 * in practice all cases using WMM support at least eight replay
@@ -2269,7 +2269,8 @@ int wpa_supplicant_set_suites(struct wpa_supplicant *wpa_s,
 	 * is far more likely for any current device to support WMM. */
 	wmm = wpa_s->connection_set &&
 		(wpa_s->connection_ht || wpa_s->connection_vht ||
-		 wpa_s->connection_he || wpa_s->connection_eht);
+		 wpa_s->connection_he || wpa_s->connection_eht ||
+		 wpa_s->connection_uhr);
 	if (!wmm && bss)
 		wmm = !!wpa_bss_get_vendor_ie(bss, WMM_IE_VENDOR_TYPE);
 	wpa_sm_set_param(wpa_s->wpa, WPA_PARAM_WMM_ENABLED, wmm);
@@ -4930,6 +4931,7 @@ static void wpas_start_assoc_cb(struct wpa_radio_work *work, int deinit)
 	wpa_supplicant_apply_he_overrides(wpa_s, ssid, &params);
 #endif /* CONFIG_HE_OVERRIDES */
 	wpa_supplicant_apply_eht_overrides(wpa_s, ssid, &params);
+	wpa_supplicant_apply_uhr_overrides(wpa_s, ssid, &params);
 
 #ifdef CONFIG_P2P
 	/*
@@ -6885,6 +6887,17 @@ void wpa_supplicant_apply_eht_overrides(
 }
 
 
+void wpa_supplicant_apply_uhr_overrides(
+	struct wpa_supplicant *wpa_s, struct wpa_ssid *ssid,
+	struct wpa_driver_associate_params *params)
+{
+	if (!ssid)
+		return;
+
+	params->disable_uhr = ssid->disable_uhr;
+}
+
+
 static int pcsc_reader_init(struct wpa_supplicant *wpa_s)
 {
 #ifdef PCSC_FUNCS
diff --git a/wpa_supplicant/wpa_supplicant_i.h b/wpa_supplicant/wpa_supplicant_i.h
index 4148e94ea06d..175a043b0b3a 100644
--- a/wpa_supplicant/wpa_supplicant_i.h
+++ b/wpa_supplicant/wpa_supplicant_i.h
@@ -1011,6 +1011,7 @@ struct wpa_supplicant {
 	unsigned int connection_vht:1;
 	unsigned int connection_he:1;
 	unsigned int connection_eht:1;
+	unsigned int connection_uhr:1;
 	unsigned int disable_mbo_oce:1;
 	u8 connection_max_nss_rx;
 	u8 connection_max_nss_tx;
@@ -1710,6 +1711,9 @@ void wpa_supplicant_apply_he_overrides(
 void wpa_supplicant_apply_eht_overrides(
 	struct wpa_supplicant *wpa_s, struct wpa_ssid *ssid,
 	struct wpa_driver_associate_params *params);
+void wpa_supplicant_apply_uhr_overrides(
+	struct wpa_supplicant *wpa_s, struct wpa_ssid *ssid,
+	struct wpa_driver_associate_params *params);
 
 int wpa_set_wep_keys(struct wpa_supplicant *wpa_s, struct wpa_ssid *ssid);
 int wpa_supplicant_set_wpa_none_key(struct wpa_supplicant *wpa_s,
-- 
2.53.0




More information about the Hostap mailing list