diff --git a/src/core/devices/wifi/nm-device-wifi.c b/src/core/devices/wifi/nm-device-wifi.c index 6a9e5655c8..af9f237d6e 100644 --- a/src/core/devices/wifi/nm-device-wifi.c +++ b/src/core/devices/wifi/nm-device-wifi.c @@ -3100,12 +3100,19 @@ build_supplicant_config(NMDeviceWifi *self, goto error; } - if (!nm_supplicant_config_add_bgscan(config, - connection, - nm_settings_connection_get_num_seen_bssids(sett_conn), - error)) { - g_prefix_error(error, "bgscan: "); - goto error; + { + const char *seen_bssids[NM_SETTINGS_CONNECTION_SEEN_BSSIDS_MAX + 1]; + guint num_seen_bssids; + + num_seen_bssids = nm_settings_connection_get_seen_bssids(sett_conn, seen_bssids); + if (!nm_supplicant_config_add_bgscan(config, + connection, + num_seen_bssids, + seen_bssids, + error)) { + g_prefix_error(error, "bgscan: "); + goto error; + } } ap_isolation = nm_setting_wireless_get_ap_isolation(s_wireless); diff --git a/src/core/settings/nm-settings-connection.c b/src/core/settings/nm-settings-connection.c index 7ed3712b47..beb51a1bc6 100644 --- a/src/core/settings/nm-settings-connection.c +++ b/src/core/settings/nm-settings-connection.c @@ -28,8 +28,6 @@ #include "nm-dbus-manager.h" #include "settings/plugins/keyfile/nms-keyfile-storage.h" -#define SEEN_BSSIDS_MAX 30 - #define _NM_SETTINGS_UPDATE2_FLAG_ALL_PERSIST_MODES \ ((NMSettingsUpdate2Flags) (NM_SETTINGS_UPDATE2_FLAG_TO_DISK \ | NM_SETTINGS_UPDATE2_FLAG_IN_MEMORY \ @@ -220,9 +218,7 @@ static const GDBusSignalInfo signal_info_updated; static const GDBusSignalInfo signal_info_removed; static const NMDBusInterfaceInfoExtended interface_info_settings_connection; -static void update_agent_secrets_cache(NMSettingsConnection *self, NMConnection *new); -static guint _get_seen_bssids(NMSettingsConnection *self, - const char *strv_buf[static(SEEN_BSSIDS_MAX + 1)]); +static void update_agent_secrets_cache(NMSettingsConnection *self, NMConnection *new); /*****************************************************************************/ @@ -1405,7 +1401,7 @@ get_settings_auth_cb(NMSettingsConnection *self, GError *error, gpointer data) { - const char *seen_bssids_strv[SEEN_BSSIDS_MAX + 1]; + const char *seen_bssids_strv[NM_SETTINGS_CONNECTION_SEEN_BSSIDS_MAX + 1]; NMConnectionSerializationOptions options = {}; if (error) { @@ -1426,7 +1422,7 @@ get_settings_auth_cb(NMSettingsConnection *self, * from the same reason as timestamp. Thus we put it here to GetSettings() * return settings too. */ - _get_seen_bssids(self, seen_bssids_strv); + nm_settings_connection_get_seen_bssids(self, seen_bssids_strv); options.seen_bssids = seen_bssids_strv; /* Secrets should *never* be returned by the GetSettings method, they @@ -2434,7 +2430,7 @@ _nm_settings_connection_register_kf_dbs(NMSettingsConnection *self, SeenBssidEntry *entry; nm_assert(result_len == nm_g_hash_table_size(priv->seen_bssids_hash)); - if (result_len >= SEEN_BSSIDS_MAX) + if (result_len >= NM_SETTINGS_CONNECTION_SEEN_BSSIDS_MAX) break; if (!_nm_utils_hwaddr_aton_exact(tmp_strv[i], &addr_bin, sizeof(addr_bin))) @@ -2459,12 +2455,14 @@ _nm_settings_connection_register_kf_dbs(NMSettingsConnection *self, nm_clear_pointer(&priv->seen_bssids_hash, g_hash_table_destroy); nm_assert(nm_g_hash_table_size(priv->seen_bssids_hash) == result_len); - nm_assert(result_len <= SEEN_BSSIDS_MAX); + nm_assert(result_len <= NM_SETTINGS_CONNECTION_SEEN_BSSIDS_MAX); } } -static guint -_get_seen_bssids(NMSettingsConnection *self, const char *strv_buf[static(SEEN_BSSIDS_MAX + 1)]) +guint +nm_settings_connection_get_seen_bssids( + NMSettingsConnection *self, + const char *strv_buf[static(NM_SETTINGS_CONNECTION_SEEN_BSSIDS_MAX + 1)]) { NMSettingsConnectionPrivate *priv = NM_SETTINGS_CONNECTION_GET_PRIVATE(self); SeenBssidEntry *entry; @@ -2472,7 +2470,7 @@ _get_seen_bssids(NMSettingsConnection *self, const char *strv_buf[static(SEEN_BS i = 0; c_list_for_each_entry (entry, &priv->seen_bssids_lst_head, seen_bssids_lst) { - nm_assert(i <= SEEN_BSSIDS_MAX); + nm_assert(i <= NM_SETTINGS_CONNECTION_SEEN_BSSIDS_MAX); strv_buf[i++] = entry->bssid; } strv_buf[i] = NULL; @@ -2515,7 +2513,7 @@ void nm_settings_connection_add_seen_bssid(NMSettingsConnection *self, const char *seen_bssid) { NMSettingsConnectionPrivate *priv = NM_SETTINGS_CONNECTION_GET_PRIVATE(self); - const char *seen_bssids_strv[SEEN_BSSIDS_MAX + 1]; + const char *seen_bssids_strv[NM_SETTINGS_CONNECTION_SEEN_BSSIDS_MAX + 1]; NMEtherAddr addr_bin; const char *connection_uuid; SeenBssidEntry entry_stack; @@ -2546,14 +2544,14 @@ nm_settings_connection_add_seen_bssid(NMSettingsConnection *self, const char *se if (!g_hash_table_add(priv->seen_bssids_hash, entry)) nm_assert_not_reached(); - if (g_hash_table_size(priv->seen_bssids_hash) > SEEN_BSSIDS_MAX) { + if (g_hash_table_size(priv->seen_bssids_hash) > NM_SETTINGS_CONNECTION_SEEN_BSSIDS_MAX) { g_hash_table_remove( priv->seen_bssids_hash, c_list_last_entry(&priv->seen_bssids_lst_head, SeenBssidEntry, seen_bssids_lst)); } } - nm_assert(g_hash_table_size(priv->seen_bssids_hash) <= SEEN_BSSIDS_MAX); + nm_assert(g_hash_table_size(priv->seen_bssids_hash) <= NM_SETTINGS_CONNECTION_SEEN_BSSIDS_MAX); nm_assert(g_hash_table_size(priv->seen_bssids_hash) == c_list_length(&priv->seen_bssids_lst_head)); @@ -2564,7 +2562,7 @@ nm_settings_connection_add_seen_bssid(NMSettingsConnection *self, const char *se if (!connection_uuid) return; - i = _get_seen_bssids(self, seen_bssids_strv); + i = nm_settings_connection_get_seen_bssids(self, seen_bssids_strv); nm_key_file_db_set_string_list(priv->kf_db_seen_bssids, connection_uuid, seen_bssids_strv, i); } diff --git a/src/core/settings/nm-settings-connection.h b/src/core/settings/nm-settings-connection.h index d15a75b749..c2da97314b 100644 --- a/src/core/settings/nm-settings-connection.h +++ b/src/core/settings/nm-settings-connection.h @@ -348,6 +348,15 @@ void nm_settings_connection_add_seen_bssid(NMSettingsConnection *self, const cha guint nm_settings_connection_get_num_seen_bssids(NMSettingsConnection *self); +/* Maximum number of BSSIDs tracked per connection. Public so that callers + * needing the full list can stack-allocate a fixed-size strv buffer for + * nm_settings_connection_get_seen_bssids(). */ +#define NM_SETTINGS_CONNECTION_SEEN_BSSIDS_MAX 30u + +guint nm_settings_connection_get_seen_bssids( + NMSettingsConnection *self, + const char *strv_buf[static(NM_SETTINGS_CONNECTION_SEEN_BSSIDS_MAX + 1)]); + gboolean nm_settings_connection_autoconnect_is_blocked(NMSettingsConnection *self); NMSettingsAutoconnectBlockedReason diff --git a/src/core/supplicant/nm-supplicant-config.c b/src/core/supplicant/nm-supplicant-config.c index ced4d66866..c3ada5ccbb 100644 --- a/src/core/supplicant/nm-supplicant-config.c +++ b/src/core/supplicant/nm-supplicant-config.c @@ -752,10 +752,66 @@ nm_supplicant_config_add_setting_wireless(NMSupplicantConfig *self, return TRUE; } +/* Maximum number of seen BSSIDs we treat as evidence of a single Wi-Fi 7 + * MLO AP. 802.11be defines tri-link (2.4 + 5 + 6 GHz) as the maximum; + * beyond 3 the heuristic does not apply and the multi-AP path runs. + */ +#define _BGSCAN_MLO_HEURISTIC_MAX_BSSIDS 3u + +/* _seen_bssids_look_like_mlo_per_link: + * + * Heuristic returning TRUE when @seen_bssids appears to be the per-link + * BSSIDs of one Wi-Fi 7 MLO-capable AP rather than several physical APs. + * + * In MLO, one physical AP advertises one BSSID per link (2 for 5+6 GHz, + * 3 for tri-link). Vendors observed in the wild derive the per-link + * BSSIDs as locally-administered virtual MAC addresses sharing octets + * 1-4 with each other. The MLD address itself may or may not be in the + * seen-bssids list. + * + * Real multi-AP ESSes (mesh, enterprise) typically use vendor-assigned + * MACs (LAA bit unset) and unrelated address blocks per AP, so the + * heuristic should not false-positive on them. + */ +static gboolean +_seen_bssids_look_like_mlo_per_link(const char *const *seen_bssids, guint num_seen_bssids) +{ + guint8 common_octets_1_to_4[4]; + guint i; + + if (num_seen_bssids < 2u || num_seen_bssids > _BGSCAN_MLO_HEURISTIC_MAX_BSSIDS) + return FALSE; + if (!seen_bssids) + return FALSE; + + for (i = 0; i < num_seen_bssids; i++) { + const char *bssid = seen_bssids[i]; + guint8 addr[6]; + + if (!bssid || !nm_utils_hwaddr_aton(bssid, addr, sizeof(addr))) + return FALSE; + + /* Locally-administered bit is bit 1 (value 0x02) of the first octet. */ + if (!(addr[0] & 0x02u)) + return FALSE; + + /* Octets 1-4 must be identical across all seen BSSIDs (only octets + * 0 and 5 vary across per-link MAC addresses for the same MLD). + */ + if (i == 0) + memcpy(common_octets_1_to_4, &addr[1], 4); + else if (memcmp(common_octets_1_to_4, &addr[1], 4) != 0) + return FALSE; + } + + return TRUE; +} + gboolean nm_supplicant_config_add_bgscan(NMSupplicantConfig *self, NMConnection *connection, guint num_seen_bssids, + const char *const *seen_bssids, GError **error) { NMSettingWireless *s_wifi; @@ -792,8 +848,12 @@ nm_supplicant_config_add_bgscan(NMSupplicantConfig *self, * in which we want more reliable roaming between APs. Thus trigger scans * when the signal is still somewhat OK so we have an up-to-date roam * candidate list when the signal gets bad. + * + * Exception: when @seen_bssids look like the per-link addresses of a + * single WiFi-7 MLO AP (locally-administered bit set + shared octets + * 1-4), keep the long interval. See _seen_bssids_look_like_mlo_per_link(). */ - if (num_seen_bssids > 1u + if ((num_seen_bssids > 1u && !_seen_bssids_look_like_mlo_per_link(seen_bssids, num_seen_bssids)) || ((s_wsec = nm_connection_get_setting_wireless_security(connection)) && NM_IN_STRSET(nm_setting_wireless_security_get_key_mgmt(s_wsec), "ieee8021x", diff --git a/src/core/supplicant/nm-supplicant-config.h b/src/core/supplicant/nm-supplicant-config.h index 96460b86c7..d58fa91e8d 100644 --- a/src/core/supplicant/nm-supplicant-config.h +++ b/src/core/supplicant/nm-supplicant-config.h @@ -47,6 +47,7 @@ gboolean nm_supplicant_config_add_setting_wireless(NMSupplicantConfig *self, gboolean nm_supplicant_config_add_bgscan(NMSupplicantConfig *self, NMConnection *connection, guint num_seen_bssids, + const char *const *seen_bssids, GError **error); gboolean nm_supplicant_config_add_setting_wireless_security(NMSupplicantConfig *self, diff --git a/src/core/supplicant/tests/test-supplicant-config.c b/src/core/supplicant/tests/test-supplicant-config.c index 416fe0054f..9d3bccf31b 100644 --- a/src/core/supplicant/tests/test-supplicant-config.c +++ b/src/core/supplicant/tests/test-supplicant-config.c @@ -129,7 +129,7 @@ build_supplicant_config(NMConnection *connection, g_assert_no_error(error); g_assert(success); - success = nm_supplicant_config_add_bgscan(config, connection, 0, &error); + success = nm_supplicant_config_add_bgscan(config, connection, 0, NULL, &error); g_assert_no_error(error); g_assert(success); @@ -914,6 +914,124 @@ test_suppl_cap_mask(void) /*****************************************************************************/ +static void +test_wifi_bgscan_mlo_dedup(void) +{ + /* The bgscan multi-AP heuristic should treat per-link BSSIDs of one + * Wi-Fi 7 MLO AP as a single AP (long-interval bgscan) and treat + * vendor-assigned multi-AP BSSIDs as multi-AP (short-interval bgscan). + */ + gs_unref_object NMConnection *connection = NULL; + const unsigned char ssid_data[] = {'M', 'L', 'O', '-', 't', 'e', 's', 't'}; + gs_unref_bytes GBytes *ssid = g_bytes_new(ssid_data, sizeof(ssid_data)); + + /* Per-link addresses for one MLO AP: LAA bit set on first octet, + * octets 1-4 shared, only octet 0 and 5 vary across links. + * Captured from a TP-Link Deco BE63 mesh. + */ + const char *mlo_seen[] = { + "f6:75:0c:74:4b:75", + "ca:75:0c:74:4b:70", + NULL, + }; + + /* Real multi-AP setup: vendor-assigned MACs (UAA bit unset) with + * unrelated address blocks per AP. + */ + const char *multi_ap_seen[] = { + "00:11:22:33:44:55", + "aa:bb:cc:dd:ee:ff", + NULL, + }; + + /* All-LAA but octets 1-4 do not match: must NOT be treated as MLO. */ + const char *laa_unrelated_seen[] = { + "f6:75:0c:74:4b:75", + "f2:99:88:77:66:55", + NULL, + }; + + /* Tri-link MLO: 3 BSSIDs all matching the per-link MAC pattern. + * 802.11be defines tri-link (2.4 + 5 + 6 GHz) as the maximum; + * the heuristic's upper bound is 3. + */ + const char *mlo_seen_3link[] = { + "f6:75:0c:74:4b:75", + "ca:75:0c:74:4b:70", + "e2:75:0c:74:4b:80", + NULL, + }; + + connection = new_basic_connection("MLO Wi-Fi", ssid, NULL); + g_object_set(nm_connection_get_setting_wireless(connection), + NM_SETTING_WIRELESS_BAND, + "a", + NULL); + + { + gs_unref_object NMSupplicantConfig *config = NULL; + GError *error = NULL; + gboolean success; + + config = + nm_supplicant_config_new(NM_SUPPL_CAP_MASK_NONE, + nm_utils_get_connection_first_permissions_user(connection)); + NMTST_EXPECT_NM_INFO("Config: added 'bgscan' value 'simple:30:-70:86400'*"); + success = nm_supplicant_config_add_bgscan(config, connection, 2, mlo_seen, &error); + g_assert_no_error(error); + g_assert(success); + g_test_assert_expected_messages(); + } + + { + gs_unref_object NMSupplicantConfig *config = NULL; + GError *error = NULL; + gboolean success; + + config = + nm_supplicant_config_new(NM_SUPPL_CAP_MASK_NONE, + nm_utils_get_connection_first_permissions_user(connection)); + NMTST_EXPECT_NM_INFO("Config: added 'bgscan' value 'simple:30:-65:300'*"); + success = nm_supplicant_config_add_bgscan(config, connection, 2, multi_ap_seen, &error); + g_assert_no_error(error); + g_assert(success); + g_test_assert_expected_messages(); + } + + { + gs_unref_object NMSupplicantConfig *config = NULL; + GError *error = NULL; + gboolean success; + + config = + nm_supplicant_config_new(NM_SUPPL_CAP_MASK_NONE, + nm_utils_get_connection_first_permissions_user(connection)); + NMTST_EXPECT_NM_INFO("Config: added 'bgscan' value 'simple:30:-65:300'*"); + success = + nm_supplicant_config_add_bgscan(config, connection, 2, laa_unrelated_seen, &error); + g_assert_no_error(error); + g_assert(success); + g_test_assert_expected_messages(); + } + + { + gs_unref_object NMSupplicantConfig *config = NULL; + GError *error = NULL; + gboolean success; + + config = + nm_supplicant_config_new(NM_SUPPL_CAP_MASK_NONE, + nm_utils_get_connection_first_permissions_user(connection)); + NMTST_EXPECT_NM_INFO("Config: added 'bgscan' value 'simple:30:-70:86400'*"); + success = nm_supplicant_config_add_bgscan(config, connection, 3, mlo_seen_3link, &error); + g_assert_no_error(error); + g_assert(success); + g_test_assert_expected_messages(); + } +} + +/*****************************************************************************/ + NMTST_DEFINE(); int @@ -930,6 +1048,7 @@ main(int argc, char **argv) g_test_add_func("/supplicant-config/wifi-sae", test_wifi_sae); g_test_add_func("/supplicant-config/test_suppl_cap_mask", test_suppl_cap_mask); g_test_add_func("/supplicant-config/wifi-eap-suite-b-192", test_wifi_eap_suite_b_generation); + g_test_add_func("/supplicant-config/wifi-bgscan-mlo-dedup", test_wifi_bgscan_mlo_dedup); return g_test_run(); }