[PATCH 08/15] AP: Generate the Neighbor Report ANQP-element
Andrei-Alexandru Bleortu
me at andrei-z.com
Sun Oct 4 03:42:01 PDT 2026
Wi-Fi Agile Multiband 3.5.1.1 and 4.3.3 require an AP that receives an
ANQP request for the Neighbor Report ANQP-element to answer with the
BSSs it considers candidates, its own BSS included, each with a BSS
Transition Candidate Preference subelement, the most preferred at 255.
hostapd could only return a static anqp_elem=272 value, which cannot
follow the neighbor report database.
Move the candidate list built for BSS Transition Management Query
responses to neighbor_db.c and, unless anqp_elem=272 is configured,
answer the Neighbor Report ANQP-element from it when the neighbor
report database has the own BSS (rrm_neighbor_report=1). With no STA
associated, the preferences come from the database alone. The element
is also listed in the ANQP Capability List then.
Assisted-by: LLM
Signed-off-by: Andrei-Alexandru Bleortu <me at andrei-z.com>
---
src/ap/gas_serv.c | 49 +++++++++++++-
src/ap/neighbor_db.c | 140 ++++++++++++++++++++++++++++++++++++++++
src/ap/neighbor_db.h | 3 +
src/ap/wnm_ap.c | 139 +--------------------------------------
tests/hwsim/test_mbo.py | 47 ++++++++++++++
5 files changed, 240 insertions(+), 138 deletions(-)
diff --git a/src/ap/gas_serv.c b/src/ap/gas_serv.c
index 9c2c7fe2f..061849261 100644
--- a/src/ap/gas_serv.c
+++ b/src/ap/gas_serv.c
@@ -19,6 +19,7 @@
#include "dpp_hostapd.h"
#include "sta_info.h"
#include "gas_serv.h"
+#include "neighbor_db.h"
static void gas_serv_peer_timeout(void *eloop_ctx, void *timeout_ctx);
@@ -288,6 +289,17 @@ static int anqp_add_override(struct hostapd_data *hapd, struct wpabuf **buf,
}
+/* The Neighbor Report ANQP-element from the neighbor report database, unless
+ * configured with anqp_elem */
+static bool anqp_local_neighbor_report(struct hostapd_data *hapd)
+{
+ return !get_anqp_elem(hapd, ANQP_NEIGHBOR_REPORT) &&
+ (hapd->conf->radio_measurements[0] &
+ WLAN_RRM_CAPS_NEIGHBOR_REPORT) &&
+ !dl_list_empty(&hapd->nr_db);
+}
+
+
static void anqp_add_capab_list(struct hostapd_data *hapd,
struct wpabuf **_buf)
{
@@ -334,7 +346,8 @@ static void anqp_add_capab_list(struct hostapd_data *hapd,
wpabuf_put_le16(buf, ANQP_TDLS_CAPABILITY);
if (get_anqp_elem(hapd, ANQP_EMERGENCY_NAI))
wpabuf_put_le16(buf, ANQP_EMERGENCY_NAI);
- if (get_anqp_elem(hapd, ANQP_NEIGHBOR_REPORT))
+ if (get_anqp_elem(hapd, ANQP_NEIGHBOR_REPORT) ||
+ anqp_local_neighbor_report(hapd))
wpabuf_put_le16(buf, ANQP_NEIGHBOR_REPORT);
#ifdef CONFIG_FILS
if (!dl_list_empty(&hapd->conf->fils_realms) ||
@@ -401,6 +414,26 @@ static void anqp_add_venue_name(struct hostapd_data *hapd, struct wpabuf **buf)
}
+static void anqp_add_neighbor_report(struct hostapd_data *hapd,
+ struct wpabuf **buf)
+{
+ u8 nr[1400];
+ size_t nr_len;
+ u8 *len;
+
+ /* The candidates with preferences, the own BSS included (Wi-Fi Agile
+ * Multiband 3.5.1.1, 4.3.3); no STA is associated yet */
+ nr_len = hostapd_neighbor_candidates(hapd, NULL, nr, sizeof(nr));
+ if (!nr_len)
+ return;
+ if (wpabuf_resize(buf, 4 + nr_len) < 0)
+ return;
+ len = gas_anqp_add_element(*buf, ANQP_NEIGHBOR_REPORT);
+ wpabuf_put_data(*buf, nr, nr_len);
+ gas_anqp_set_element_len(*buf, len);
+}
+
+
static void anqp_add_venue_url(struct hostapd_data *hapd, struct wpabuf **buf)
{
if (anqp_add_override(hapd, buf, ANQP_VENUE_URL))
@@ -1114,6 +1147,16 @@ gas_serv_build_gas_resp_payload(struct hostapd_data *hapd,
continue;
}
#endif /* CONFIG_FILS */
+ if (extra_req[i] == ANQP_NEIGHBOR_REPORT &&
+ anqp_local_neighbor_report(hapd)) {
+ anqp_add_neighbor_report(hapd, &buf);
+ if (wpabuf_len(buf) > max_len) {
+ wpa_printf(MSG_INFO,
+ "ANQP: ANQP-element would go beyond maximum GAS response length - skip");
+ buf->used = prev_len;
+ }
+ continue;
+ }
if (extra_req[i] == ANQP_VENUE_URL) {
anqp_add_venue_url(hapd, &buf);
if (wpabuf_len(buf) > max_len) {
@@ -1307,6 +1350,10 @@ static void rx_anqp_query_list_id(struct hostapd_data *hapd, u16 info_id,
if (info_id == ANQP_VENUE_URL && hapd->conf->venue_url) {
wpa_printf(MSG_DEBUG,
"ANQP: Venue URL (local)");
+ } else if (info_id == ANQP_NEIGHBOR_REPORT &&
+ anqp_local_neighbor_report(hapd)) {
+ wpa_printf(MSG_DEBUG,
+ "ANQP: Neighbor Report (local)");
} else if (!get_anqp_elem(hapd, info_id)) {
wpa_printf(MSG_DEBUG, "ANQP: Unsupported Info Id %u",
info_id);
diff --git a/src/ap/neighbor_db.c b/src/ap/neighbor_db.c
index 93203c5d2..ce29c1f04 100644
--- a/src/ap/neighbor_db.c
+++ b/src/ap/neighbor_db.c
@@ -11,8 +11,10 @@
#include "utils/common.h"
#include "utils/crc32.h"
+#include "common/ieee802_11_common.h"
#include "hostapd.h"
#include "ieee802_11.h"
+#include "sta_info.h"
#include "neighbor_db.h"
@@ -374,3 +376,141 @@ int hostapd_neighbor_sync_own_report(struct hostapd_data *hapd)
return 0;
}
+
+
+/*
+ * 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 neighbor_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;
+}
+
+
+/*
+ * Candidate BSSs for a STA to transition to, as Neighbor Report elements: for
+ * a BSS Transition Management Request that answers a query (IEEE Std
+ * 802.11-2024, 11.21.7.3; Wi-Fi Agile Multiband 3.5.1.2) or for the Neighbor
+ * Report ANQP-element (Wi-Fi Agile Multiband 3.5.1.1, 4.3.3), sta then being
+ * NULL. Every BSS of this ESS in the neighbor report database, the own BSS
+ * included, gets an element with a BSS Transition Candidate Preference
+ * subelement. A preference configured in the database is used, else one from
+ * neighbor_cand_pref(); the values are then renumbered so that the most
+ * preferred is 255 and each lower level is one less, 0 staying 0. Without the
+ * own BSS in the database (rrm_neighbor_report=0), there is no list.
+ */
+size_t hostapd_neighbor_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 = neighbor_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;
+}
diff --git a/src/ap/neighbor_db.h b/src/ap/neighbor_db.h
index 53f714203..050b9cd8c 100644
--- a/src/ap/neighbor_db.h
+++ b/src/ap/neighbor_db.h
@@ -24,5 +24,8 @@ int hostapd_neighbor_sync_own_report(struct hostapd_data *hapd);
int hostapd_neighbor_remove(struct hostapd_data *hapd, const u8 *bssid,
const struct wpa_ssid_value *ssid);
void hostapd_free_neighbor_db(struct hostapd_data *hapd);
+size_t hostapd_neighbor_candidates(struct hostapd_data *hapd,
+ struct sta_info *sta, u8 *pos,
+ size_t max_len);
#endif /* NEIGHBOR_DB_H */
diff --git a/src/ap/wnm_ap.c b/src/ap/wnm_ap.c
index 09fcc6584..90f6b1a53 100644
--- a/src/ap/wnm_ap.c
+++ b/src/ap/wnm_ap.c
@@ -21,6 +21,7 @@
#include "ap/wpa_auth.h"
#include "mbo_ap.h"
#include "wnm_ap.h"
+#include "neighbor_db.h"
#define MAX_TFS_IE_LEN 1024
@@ -380,142 +381,6 @@ 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)
@@ -547,7 +412,7 @@ 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,
+ cand_len = hostapd_neighbor_candidates(hapd, sta, pos,
(u8 *) mgmt + IEEE80211_MAX_MMPDU_SIZE -
pos);
if (cand_len) {
diff --git a/tests/hwsim/test_mbo.py b/tests/hwsim/test_mbo.py
index a530e0f35..c814bc860 100644
--- a/tests/hwsim/test_mbo.py
+++ b/tests/hwsim/test_mbo.py
@@ -623,6 +623,53 @@ def check_mbo_anqp(dev, bssid, cell_data_conn_pref):
dev.dump_monitor()
+def test_mbo_anqp_neighbor_report(dev, apdev):
+ """MBO ANQP Neighbor Report from the neighbor report database"""
+ params = {'ssid': "test-wnm-mbo",
+ 'mbo': '1',
+ 'interworking': '1',
+ 'rrm_neighbor_report': '1'}
+ hapd = hostapd.add_ap(apdev[0], params)
+ bssid = hapd.own_addr()
+ # A neighbor with a configured preference that is not 255
+ nr = "020000009900" + "0f000000" + "51" + "01" + "07" + "030164"
+ if "OK" not in hapd.request("SET_NEIGHBOR 02:00:00:00:99:00 ssid=\"test-wnm-mbo\" nr=" + nr):
+ raise Exception("SET_NEIGHBOR failed")
+
+ dev[0].scan_for_bss(bssid, freq="2412", force_scan=True)
+ if "OK" not in dev[0].request("ANQP_GET " + bssid + " 257,272"):
+ raise Exception("ANQP_GET command failed")
+ ev = dev[0].wait_event(["GAS-QUERY-DONE"], timeout=10)
+ if ev is None:
+ raise Exception("GAS query timed out")
+
+ bss = dev[0].get_bss(bssid)
+ capab = bytes.fromhex(bss['anqp_capability_list'])
+ if b'\x10\x01' not in capab:
+ raise Exception("Neighbor Report not in the ANQP Capability List")
+ if 'anqp[272]' not in bss:
+ raise Exception("No Neighbor Report ANQP-element received")
+ buf = bytes.fromhex(bss['anqp[272]'])
+ prefs = {}
+ while len(buf) >= 2:
+ eid, elen = buf[0], buf[1]
+ elem = buf[2:2 + elen]
+ buf = buf[2 + elen:]
+ if eid != 52 or elen < 13:
+ raise Exception("Unexpected element %d in the Neighbor Report" % eid)
+ sub = elem[13:]
+ pref = None
+ while len(sub) >= 2:
+ if sub[0] == 3 and sub[1] >= 1:
+ pref = sub[2]
+ sub = sub[2 + sub[1]:]
+ prefs[elem[0:6].hex(':')] = pref
+ logger.info("Candidate preferences: " + str(prefs))
+ if prefs.get(bssid) != 255:
+ raise Exception("Own BSS not the most preferred candidate")
+ if prefs.get("02:00:00:00:99:00") != 254:
+ raise Exception("Neighbor preference not renumbered to 254")
+
def test_mbo_anqp(dev, apdev):
"""MBO ANQP"""
params = {'ssid': "test-wnm-mbo",
--
2.47.3
More information about the Hostap
mailing list