[PATCH 03/15] WNM: Include a candidate list when answering a BSS TM Query

Andrei-Alexandru Bleortu me at andrei-z.com
Sun Oct 4 03:41:56 PDT 2026


IEEE Std 802.11-2024, 11.21.7.3 has an AP include the BSS Transition
Candidate List Entries field in the BSS Transition Management Request
that answers a BSS Transition Management Query when it has information
about candidates, and Wi-Fi Agile Multiband 3.5.1.2 requires it: one
Neighbor Report element per candidate in a global operating class, the
AP's own BSS included, each with a BSS Transition Candidate Preference
subelement, the most preferred at 255 and each lower level one less.
hostapd answered with an empty request.

List the BSSs of the same SSID from the neighbor report database, which
holds the own BSS when rrm_neighbor_report=1 (without it, the answer
stays empty), skipping operating classes that are not global or that
the STA did not list as supported. A preference configured in the
database is kept; otherwise channels the STA reported as non-operable
(MBO) get 0 and non-preferred ones a lower level. The values are then
renumbered from 255. With a list, the request stays valid for 255 TBTTs
instead of one, so that the STA has time to scan the candidates.

Assisted-by: LLM
Signed-off-by: Andrei-Alexandru Bleortu <me at andrei-z.com>
---
 src/ap/wnm_ap.c         | 152 +++++++++++++++++++++++++++++++++++++++-
 tests/hwsim/test_wnm.py |  33 +++++++++
 2 files changed, 183 insertions(+), 2 deletions(-)

diff --git a/src/ap/wnm_ap.c b/src/ap/wnm_ap.c
index 37c85f851..09fcc6584 100644
--- a/src/ap/wnm_ap.c
+++ b/src/ap/wnm_ap.c
@@ -11,6 +11,7 @@
 #include "utils/common.h"
 #include "utils/eloop.h"
 #include "common/ieee802_11_defs.h"
+#include "common/ieee802_11_common.h"
 #include "common/wpa_ctrl.h"
 #include "common/ocv.h"
 #include "ap/hostapd.h"
@@ -379,6 +380,142 @@ static void ieee802_11_rx_wnmsleep_req(struct hostapd_data *hapd,
 }
 
 
+/*
+ * Preference for a candidate before normalization: 0 excludes it, -1 skips it.
+ * The STA's operating classes and, with MBO, its non-preferred channel
+ * reports (Wi-Fi Agile Multiband 3.2) decide.
+ */
+static int wnm_bss_tm_cand_pref(struct hostapd_data *hapd, struct sta_info *sta,
+				const u8 *nr)
+{
+	u8 op_class = nr[10], chan = nr[11];
+#ifdef CONFIG_MBO
+	struct mbo_non_pref_chan_info *info;
+#endif /* CONFIG_MBO */
+
+	/* Neighbor Report elements in the list use global operating classes */
+	if (!get_oper_class(NULL, op_class))
+		return -1;
+
+	if (sta && sta->supp_op_classes) {
+		const u8 *pos = &sta->supp_op_classes[1];
+		const u8 *end = pos + sta->supp_op_classes[0];
+
+		/* Current Operating Class and the Operating Classes list, up
+		 * to the first delimiter */
+		while (pos < end && *pos != 130 && *pos != 0 &&
+		       *pos != op_class)
+			pos++;
+		if (pos == end || *pos != op_class)
+			return -1;
+	}
+
+#ifdef CONFIG_MBO
+	for (info = sta ? sta->non_pref_chan : NULL; info; info = info->next) {
+		unsigned int i;
+
+		if (info->op_class != op_class)
+			continue;
+		for (i = 0; i < info->num_channels; i++) {
+			if (info->channels[i] == chan)
+				return info->pref == 0 ? 0 : 254;
+		}
+	}
+#endif /* CONFIG_MBO */
+
+	return 255;
+}
+
+
+/*
+ * BSS Transition Candidate List Entries for a BSS Transition Management
+ * Request that answers a BSS Transition Management Query (IEEE Std
+ * 802.11-2024, 11.21.7.3; Wi-Fi Agile Multiband 3.5.1.2): a Neighbor Report
+ * element for every BSS of this ESS in the neighbor report database, the own
+ * BSS included, each with a BSS Transition Candidate Preference subelement. A
+ * preference configured in the database is used, else one from
+ * wnm_bss_tm_cand_pref(); the values are then renumbered so that the most
+ * preferred is 255 and each lower level is one less, 0 staying 0.
+ */
+static size_t wnm_bss_tm_candidates(struct hostapd_data *hapd,
+				    struct sta_info *sta, u8 *pos,
+				    size_t max_len)
+{
+	struct hostapd_neighbor_entry *nr;
+	u8 used[256 / 8];
+	bool own = false;
+	u8 *start = pos;
+	int pass;
+
+	os_memset(used, 0, sizeof(used));
+	for (pass = 0; pass < 2; pass++) {
+		dl_list_for_each(nr, &hapd->nr_db,
+				 struct hostapd_neighbor_entry, list) {
+			const u8 *nr_pos, *sub;
+			size_t len;
+			int pref, v, higher;
+
+			if (!nr->nr || wpabuf_len(nr->nr) < 13 ||
+			    nr->ssid.ssid_len != hapd->conf->ssid.ssid_len ||
+			    os_memcmp(nr->ssid.ssid, hapd->conf->ssid.ssid,
+				      nr->ssid.ssid_len) != 0)
+				continue;
+
+			nr_pos = wpabuf_head_u8(nr->nr);
+			len = wpabuf_len(nr->nr);
+			pref = wnm_bss_tm_cand_pref(hapd, sta, nr_pos);
+			if (pref < 0)
+				continue;
+			sub = get_ie(nr_pos + 13, len - 13,
+				     WNM_NEIGHBOR_BSS_TRANSITION_CANDIDATE);
+			if (sub && sub[1] >= 1)
+				pref = sub[2];
+
+			if (pass == 0) {
+				used[pref / 8] |= BIT(pref % 8);
+				if (ether_addr_equal(nr->bssid,
+						     hapd->own_addr))
+					own = true;
+				continue;
+			}
+
+			/* Renumber: one level below 255 per higher value */
+			higher = 0;
+			for (v = pref + 1; pref && v < 256; v++)
+				if (used[v / 8] & BIT(v % 8))
+					higher++;
+			if (pref)
+				pref = 255 - higher;
+
+			if (!sub)
+				len += 3;
+			if (len > 255 ||
+			    (size_t) (pos - start) + 2 + len > max_len)
+				continue;
+
+			*pos++ = WLAN_EID_NEIGHBOR_REPORT;
+			*pos++ = len;
+			os_memcpy(pos, nr_pos, wpabuf_len(nr->nr));
+			if (sub)
+				pos[sub - nr_pos + 2] = pref;
+			pos += wpabuf_len(nr->nr);
+			if (!sub) {
+				*pos++ = WNM_NEIGHBOR_BSS_TRANSITION_CANDIDATE;
+				*pos++ = 1;
+				*pos++ = pref;
+			}
+		}
+
+		/* The list has to include the own BSS (Wi-Fi Agile Multiband
+		 * 3.5.1.2); the database has it when rrm_neighbor_report=1 */
+		if (pass == 0 && !own)
+			return 0;
+	}
+
+	return pos - start;
+}
+
+
 static int ieee802_11_send_bss_trans_mgmt_request(struct hostapd_data *hapd,
 						  const u8 *addr,
 						  u8 dialog_token)
@@ -386,11 +523,11 @@ static int ieee802_11_send_bss_trans_mgmt_request(struct hostapd_data *hapd,
 	struct ieee80211_mgmt *mgmt;
 	const u8 *own_addr;
 	struct sta_info *sta;
-	size_t len;
+	size_t len, cand_len;
 	u8 *pos;
 	int res;
 
-	mgmt = os_zalloc(sizeof(*mgmt));
+	mgmt = os_zalloc(IEEE80211_MAX_MMPDU_SIZE);
 	if (mgmt == NULL)
 		return -1;
 
@@ -410,6 +547,17 @@ static int ieee802_11_send_bss_trans_mgmt_request(struct hostapd_data *hapd,
 	mgmt->u.action.u.bss_tm_req.validity_interval = 1;
 	pos = mgmt->u.action.u.bss_tm_req.variable;
 
+	cand_len = wnm_bss_tm_candidates(hapd, sta, pos,
+					 (u8 *) mgmt + IEEE80211_MAX_MMPDU_SIZE -
+					 pos);
+	if (cand_len) {
+		mgmt->u.action.u.bss_tm_req.req_mode =
+			WNM_BSS_TM_REQ_PREF_CAND_LIST_INCLUDED;
+		/* Long enough for the STA to scan the candidates */
+		mgmt->u.action.u.bss_tm_req.validity_interval = 255;
+		pos += cand_len;
+	}
+
 	wpa_printf(MSG_DEBUG, "WNM: Send BSS Transition Management Request to "
 		   MACSTR " dialog_token=%u req_mode=0x%x disassoc_timer=%u "
 		   "validity_interval=%u",
diff --git a/tests/hwsim/test_wnm.py b/tests/hwsim/test_wnm.py
index 1591719ac..bbc876e46 100644
--- a/tests/hwsim/test_wnm.py
+++ b/tests/hwsim/test_wnm.py
@@ -1969,6 +1969,39 @@ def test_wnm_bss_transition_mgmt_query_disabled_on_ap(dev, apdev):
     if ev is not None:
         raise Exception("Unexpected BSS TM Response reported")
 
+def test_wnm_bss_transition_mgmt_query_candidates(dev, apdev):
+    """WNM BSS Transition Management query answered with candidates"""
+    params = {"ssid": "test-wnm", "bss_transition": "1",
+              "rrm_neighbor_report": "1"}
+    hapd0 = hostapd.add_ap(apdev[0], params)
+    hapd1 = hostapd.add_ap(apdev[1], params)
+    bssid0 = hapd0.own_addr()
+    bssid1 = hapd1.own_addr()
+
+    # Exclude the own BSS (preference 0); 100 is renumbered to 255
+    for bssid, pref in ((bssid0, "00"), (bssid1, "64")):
+        nr = bssid.replace(':', '') + "0f000000" + "51" + "01" + "07" + \
+            "0301" + pref
+        if "OK" not in hapd0.request("SET_NEIGHBOR " + bssid +
+                                     " ssid=\"test-wnm\" nr=" + nr):
+            raise Exception("SET_NEIGHBOR failed")
+
+    dev[0].scan_for_bss(bssid1, 2412)
+    dev[0].scan_for_bss(bssid0, 2412)
+    dev[0].connect("test-wnm", key_mgmt="NONE", bssid=bssid0,
+                   scan_freq="2412")
+    dev[0].set_network(0, "bssid", "")
+    dev[0].request("WNM_BSS_QUERY 0")
+
+    ev = hapd0.wait_event(["BSS-TM-RESP"], timeout=5)
+    if ev is None:
+        raise Exception("No BSS Transition Management Response")
+    if "status_code=0" not in ev or "target_bssid=" + bssid1 not in ev:
+        raise Exception("Unexpected BSS TM Response: " + ev)
+    dev[0].wait_connected(timeout=10)
+    if dev[0].get_status_field("bssid") != bssid1:
+        raise Exception("STA did not move to the preferred candidate")
+
 def test_wnm_bss_transition_mgmt_query_mbo(dev, apdev):
     """WNM BSS Transition Management query - TM only due to MBO on AP"""
     hapd = start_wnm_ap(apdev[0], bss_transition=False, mbo=True)
-- 
2.47.3





More information about the Hostap mailing list