| #!/usr/bin/env python3 |
| # |
| # Copyright 2022 The Fuchsia Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import time |
| from dataclasses import dataclass |
| from datetime import datetime, timedelta, timezone |
| from typing import FrozenSet |
| |
| from mobly import asserts, signals, test_runner |
| from mobly.records import TestResultRecord |
| |
| from antlion import utils |
| from antlion.controllers.access_point import setup_ap |
| from antlion.controllers.ap_lib import hostapd_constants |
| from antlion.controllers.ap_lib.hostapd_security import Security, SecurityMode |
| from antlion.controllers.ap_lib.radio_measurement import ( |
| BssidInformation, |
| BssidInformationCapabilities, |
| NeighborReportElement, |
| PhyType, |
| ) |
| from antlion.controllers.ap_lib.wireless_network_management import ( |
| BssTransitionManagementRequest, |
| ) |
| from antlion.controllers.fuchsia_lib.wlan_lib import WlanMacRole |
| from antlion.test_utils.abstract_devices.wlan_device import ( |
| AssociationMode, |
| create_wlan_device, |
| ) |
| from antlion.test_utils.wifi import base_test |
| |
| |
| @dataclass |
| class TestParams: |
| security_mode: SecurityMode |
| |
| |
| # Antlion can see (via the wlan_features config directive) whether WNM features |
| # are enabled, and runs or skips tests depending on presence of WNM features. |
| class WlanWirelessNetworkManagementTest(base_test.WifiBaseTest): |
| """Tests Fuchsia's Wireless Network Management (AKA 802.11v) support. |
| |
| Testbed Requirements: |
| * One Fuchsia device |
| * One Whirlwind access point |
| |
| Existing Fuchsia drivers do not yet support WNM features out-of-the-box, so this |
| suite skips certain tests depending on whether specific WNM features are enabled. |
| """ |
| |
| def pre_run(self): |
| test_args: list[tuple[TestParams]] = [] |
| |
| SECURITY_MODES = ( |
| SecurityMode.OPEN, |
| SecurityMode.WEP, |
| SecurityMode.WPA, |
| SecurityMode.WPA2, |
| SecurityMode.WPA3, |
| ) |
| for security_mode in SECURITY_MODES: |
| test_args.append( |
| ( |
| TestParams( |
| security_mode=security_mode, |
| ), |
| ) |
| ) |
| |
| def generate_roam_on_btm_req_test_name(test: TestParams): |
| return f"test_roam_on_btm_req_from_{test.security_mode}_2g_to_{test.security_mode}_5g" |
| |
| self.generate_tests( |
| test_logic=self.setup_connect_roam_on_btm_req, |
| name_func=generate_roam_on_btm_req_test_name, |
| arg_sets=test_args, |
| ) |
| |
| def setup_class(self): |
| super().setup_class() |
| |
| if "dut" in self.user_params and self.user_params["dut"] != "fuchsia_devices": |
| raise AttributeError( |
| "WlanWirelessNetworkManagementTest is only relevant for Fuchsia devices." |
| ) |
| |
| if len(self.fuchsia_devices) < 1: |
| raise signals.TestAbortClass("At least one Fuchsia device is required") |
| |
| self.fuchsia_device = self.fuchsia_devices[0] |
| self.dut = create_wlan_device(self.fuchsia_device, AssociationMode.POLICY) |
| self.access_point = self.access_points[0] |
| |
| def teardown_class(self): |
| self.dut.disconnect() |
| self.access_point.stop_all_aps() |
| super().teardown_class() |
| |
| def teardown_test(self): |
| self.dut.disconnect() |
| self.download_ap_logs() |
| self.access_point.stop_all_aps() |
| super().teardown_test() |
| |
| def on_fail(self, record: TestResultRecord): |
| self.dut.disconnect() |
| self.access_point.stop_all_aps() |
| super().on_fail(record) |
| |
| def setup_ap( |
| self, |
| ssid: str, |
| security: Security = None, |
| additional_ap_parameters: dict | None = None, |
| channel: int = hostapd_constants.AP_DEFAULT_CHANNEL_2G, |
| wnm_features: FrozenSet[hostapd_constants.WnmFeature] = frozenset(), |
| ): |
| """Sets up an AP using the provided parameters. |
| |
| Args: |
| ssid: SSID for the AP. |
| security: security config for AP, defaults to None (open network |
| with no password). |
| additional_ap_parameters: A dictionary of parameters that can be set |
| directly in the hostapd config file. |
| channel: which channel number to set the AP to (default is |
| AP_DEFAULT_CHANNEL_2G). |
| wnm_features: Wireless Network Management features to enable |
| (default is no WNM features). |
| """ |
| setup_ap( |
| access_point=self.access_point, |
| profile_name="whirlwind", |
| channel=channel, |
| ssid=ssid, |
| security=security, |
| additional_ap_parameters=additional_ap_parameters, |
| wnm_features=wnm_features, |
| ) |
| |
| def _get_client_mac(self) -> str: |
| """Get the MAC address of the DUT client interface. |
| |
| Returns: |
| str, MAC address of the DUT client interface. |
| Raises: |
| ValueError if there is no DUT client interface. |
| WlanError if the DUT interface query fails. |
| """ |
| for wlan_iface in self.dut.get_wlan_interface_id_list(): |
| result = self.fuchsia_device.sl4f.wlan_lib.query_iface(wlan_iface) |
| if result.role is WlanMacRole.CLIENT: |
| return utils.mac_address_list_to_str(bytes(result.sta_addr)) |
| raise ValueError( |
| "Failed to get client interface mac address. No client interface found." |
| ) |
| |
| def test_bss_transition_is_not_advertised_when_ap_supported_dut_unsupported(self): |
| if self.dut.feature_is_present("BSS_TRANSITION_MANAGEMENT"): |
| raise signals.TestSkip("skipping test because BTM feature is present") |
| |
| ssid = utils.rand_ascii_str(hostapd_constants.AP_SSID_LENGTH_2G) |
| wnm_features = frozenset( |
| [hostapd_constants.WnmFeature.BSS_TRANSITION_MANAGEMENT] |
| ) |
| self.setup_ap(ssid, wnm_features=wnm_features) |
| asserts.assert_true(self.dut.associate(ssid), "Failed to associate.") |
| asserts.assert_true(self.dut.is_connected(), "Failed to connect.") |
| client_mac = self._get_client_mac() |
| # Verify that DUT is actually associated (as seen from AP). |
| asserts.assert_true( |
| self.access_point.sta_associated(self.access_point.wlan_2g, client_mac), |
| "DUT is not associated on the 2.4GHz band", |
| ) |
| |
| ext_capabilities = self.access_point.get_sta_extended_capabilities( |
| self.access_point.wlan_2g, client_mac |
| ) |
| asserts.assert_false( |
| ext_capabilities.bss_transition, |
| "DUT is incorrectly advertising BSS Transition Management support", |
| ) |
| |
| def test_bss_transition_is_advertised_when_ap_supported_dut_supported(self): |
| if not self.dut.feature_is_present("BSS_TRANSITION_MANAGEMENT"): |
| raise signals.TestSkip("skipping test because BTM feature is not present") |
| |
| ssid = utils.rand_ascii_str(hostapd_constants.AP_SSID_LENGTH_2G) |
| wnm_features = frozenset( |
| [hostapd_constants.WnmFeature.BSS_TRANSITION_MANAGEMENT] |
| ) |
| self.setup_ap(ssid, wnm_features=wnm_features) |
| asserts.assert_true(self.dut.associate(ssid), "Failed to associate.") |
| asserts.assert_true(self.dut.is_connected(), "Failed to connect.") |
| client_mac = self._get_client_mac() |
| # Verify that DUT is actually associated (as seen from AP). |
| asserts.assert_true( |
| self.access_point.sta_associated(self.access_point.wlan_2g, client_mac), |
| "DUT is not associated on the 2.4GHz band", |
| ) |
| |
| ext_capabilities = self.access_point.get_sta_extended_capabilities( |
| self.access_point.wlan_2g, client_mac |
| ) |
| asserts.assert_true( |
| ext_capabilities.bss_transition, |
| "DUT is not advertising BSS Transition Management support", |
| ) |
| |
| def test_wnm_sleep_mode_is_not_advertised_when_ap_supported_dut_unsupported(self): |
| if self.dut.feature_is_present("WNM_SLEEP_MODE"): |
| raise signals.TestSkip("skipping test because WNM feature is present") |
| |
| ssid = utils.rand_ascii_str(hostapd_constants.AP_SSID_LENGTH_2G) |
| wnm_features = frozenset([hostapd_constants.WnmFeature.WNM_SLEEP_MODE]) |
| self.setup_ap(ssid, wnm_features=wnm_features) |
| asserts.assert_true(self.dut.associate(ssid), "Failed to associate.") |
| asserts.assert_true(self.dut.is_connected(), "Failed to connect.") |
| client_mac = self._get_client_mac() |
| # Verify that DUT is actually associated (as seen from AP). |
| asserts.assert_true( |
| self.access_point.sta_associated(self.access_point.wlan_2g, client_mac), |
| "DUT is not associated on the 2.4GHz band", |
| ) |
| |
| ext_capabilities = self.access_point.get_sta_extended_capabilities( |
| self.access_point.wlan_2g, client_mac |
| ) |
| asserts.assert_false( |
| ext_capabilities.wnm_sleep_mode, |
| "DUT is incorrectly advertising WNM Sleep Mode support", |
| ) |
| |
| # This is called in generate_tests. |
| def setup_connect_roam_on_btm_req(self, test: TestParams): |
| """Setup the APs, associate a DUT, amd roam when BTM request is received. |
| |
| Args: |
| test: Test parameters |
| """ |
| if not self.dut.feature_is_present("BSS_TRANSITION_MANAGEMENT"): |
| raise signals.TestSkip("skipping test because BTM feature is not present") |
| |
| ssid = utils.rand_ascii_str(hostapd_constants.AP_SSID_LENGTH_2G) |
| password = None |
| if test.security_mode is not SecurityMode.OPEN: |
| # Length 13, so it can be used for WEP or WPA |
| password = utils.rand_ascii_str(13) |
| |
| wnm_features = frozenset( |
| [hostapd_constants.WnmFeature.BSS_TRANSITION_MANAGEMENT] |
| ) |
| |
| # Setup 2.4 GHz AP. |
| security = Security(test.security_mode, password) |
| wnm_features = frozenset( |
| [hostapd_constants.WnmFeature.BSS_TRANSITION_MANAGEMENT] |
| ) |
| # Setup 2.4 GHz AP. |
| self.setup_ap( |
| ssid, |
| security=security, |
| channel=hostapd_constants.AP_DEFAULT_CHANNEL_2G, |
| wnm_features=wnm_features, |
| ) |
| |
| asserts.assert_true( |
| self.dut.associate( |
| ssid, target_pwd=password, target_security=test.security_mode |
| ), |
| "Failed to associate.", |
| ) |
| # Verify that DUT is actually associated (as seen from AP). |
| client_mac = self._get_client_mac() |
| asserts.assert_true( |
| self.access_point.sta_associated(self.access_point.wlan_2g, client_mac), |
| "DUT is not associated on the 2.4GHz band", |
| ) |
| |
| # Setup 5 GHz AP with same SSID. |
| self.setup_ap( |
| ssid, |
| security=security, |
| channel=hostapd_constants.AP_DEFAULT_CHANNEL_5G, |
| wnm_features=wnm_features, |
| ) |
| |
| # Construct a BTM request. |
| dest_bssid = self.access_point.get_bssid_from_ssid( |
| ssid, self.access_point.wlan_5g |
| ) |
| dest_bssid_info = BssidInformation( |
| security=True, capabilities=BssidInformationCapabilities() |
| ) |
| neighbor_5g_ap = NeighborReportElement( |
| dest_bssid, |
| dest_bssid_info, |
| operating_class=116, |
| channel_number=hostapd_constants.AP_DEFAULT_CHANNEL_5G, |
| phy_type=PhyType.VHT, |
| ) |
| btm_req = BssTransitionManagementRequest( |
| preferred_candidate_list_included=True, |
| disassociation_imminent=True, |
| candidate_list=[neighbor_5g_ap], |
| ) |
| |
| # Sleep to avoid concurrent scan during reassociation, necessary due to a firmware bug. |
| # TODO(fxbug.dev/42068735) Remove when fixed, or when non-firmware BTM support is merged. |
| time.sleep(5) |
| |
| # Send BTM request from 2.4 GHz AP to DUT |
| self.access_point.send_bss_transition_management_req( |
| self.access_point.wlan_2g, client_mac, btm_req |
| ) |
| |
| # Give DUT time to roam. |
| ROAM_DEADLINE = datetime.now(timezone.utc) + timedelta(seconds=2) |
| while datetime.now(timezone.utc) < ROAM_DEADLINE: |
| if self.access_point.sta_authorized(self.access_point.wlan_5g, client_mac): |
| break |
| else: |
| time.sleep(0.25) |
| |
| # Verify that DUT roamed (as seen from AP). |
| asserts.assert_true( |
| self.access_point.sta_authenticated(self.access_point.wlan_5g, client_mac), |
| "DUT is not authenticated on the 5GHz band", |
| ) |
| asserts.assert_true( |
| self.access_point.sta_associated(self.access_point.wlan_5g, client_mac), |
| "DUT is not associated on the 5GHz band", |
| ) |
| asserts.assert_true( |
| self.access_point.sta_authorized(self.access_point.wlan_5g, client_mac), |
| "DUT is not 802.1X authorized on the 5GHz band", |
| ) |
| |
| def test_btm_req_ignored_dut_unsupported(self): |
| if self.dut.feature_is_present("BSS_TRANSITION_MANAGEMENT"): |
| raise signals.TestSkip("skipping test because BTM feature is present") |
| |
| ssid = utils.rand_ascii_str(hostapd_constants.AP_SSID_LENGTH_2G) |
| wnm_features = frozenset( |
| [hostapd_constants.WnmFeature.BSS_TRANSITION_MANAGEMENT] |
| ) |
| # Setup 2.4 GHz AP. |
| self.setup_ap( |
| ssid, |
| channel=hostapd_constants.AP_DEFAULT_CHANNEL_2G, |
| wnm_features=wnm_features, |
| ) |
| |
| asserts.assert_true(self.dut.associate(ssid), "Failed to associate.") |
| # Verify that DUT is actually associated (as seen from AP). |
| client_mac = self._get_client_mac() |
| asserts.assert_true( |
| self.access_point.sta_associated(self.access_point.wlan_2g, client_mac), |
| "DUT is not associated on the 2.4GHz band", |
| ) |
| |
| # Setup 5 GHz AP with same SSID. |
| self.setup_ap( |
| ssid, |
| channel=hostapd_constants.AP_DEFAULT_CHANNEL_5G, |
| wnm_features=wnm_features, |
| ) |
| |
| # Construct a BTM request. |
| dest_bssid = self.access_point.get_bssid_from_ssid( |
| ssid, self.access_point.wlan_5g |
| ) |
| dest_bssid_info = BssidInformation( |
| security=True, capabilities=BssidInformationCapabilities() |
| ) |
| neighbor_5g_ap = NeighborReportElement( |
| dest_bssid, |
| dest_bssid_info, |
| operating_class=126, |
| channel_number=hostapd_constants.AP_DEFAULT_CHANNEL_5G, |
| phy_type=PhyType.VHT, |
| ) |
| btm_req = BssTransitionManagementRequest( |
| disassociation_imminent=True, candidate_list=[neighbor_5g_ap] |
| ) |
| |
| # Send BTM request from 2.4 GHz AP to DUT |
| self.access_point.send_bss_transition_management_req( |
| self.access_point.wlan_2g, client_mac, btm_req |
| ) |
| |
| # Check that DUT has not roamed. |
| ROAM_DEADLINE = datetime.now(timezone.utc) + timedelta(seconds=2) |
| while datetime.now(timezone.utc) < ROAM_DEADLINE: |
| # Fail if DUT has reassociated to 5 GHz AP (as seen from AP). |
| if self.access_point.sta_associated(self.access_point.wlan_5g, client_mac): |
| raise signals.TestFailure( |
| "DUT unexpectedly roamed to target BSS after BTM request" |
| ) |
| else: |
| time.sleep(0.25) |
| |
| # DUT should have stayed associated to original AP. |
| asserts.assert_true( |
| self.access_point.sta_associated(self.access_point.wlan_2g, client_mac), |
| "DUT unexpectedly lost association on the 2.4GHz band after BTM request", |
| ) |
| |
| def test_btm_req_target_ap_rejects_reassoc(self): |
| if not self.dut.feature_is_present("BSS_TRANSITION_MANAGEMENT"): |
| raise signals.TestSkip("skipping test because BTM feature is not present") |
| |
| ssid = utils.rand_ascii_str(hostapd_constants.AP_SSID_LENGTH_2G) |
| wnm_features = frozenset( |
| [hostapd_constants.WnmFeature.BSS_TRANSITION_MANAGEMENT] |
| ) |
| # Setup 2.4 GHz AP. |
| self.setup_ap( |
| ssid, |
| channel=hostapd_constants.AP_DEFAULT_CHANNEL_2G, |
| wnm_features=wnm_features, |
| ) |
| |
| asserts.assert_true(self.dut.associate(ssid), "Failed to associate.") |
| # Verify that DUT is actually associated (as seen from AP). |
| client_mac = self._get_client_mac() |
| asserts.assert_true( |
| self.access_point.sta_associated(self.access_point.wlan_2g, client_mac), |
| "DUT is not associated on the 2.4GHz band", |
| ) |
| |
| # Setup 5 GHz AP with same SSID, but reject all STAs. |
| reject_all_sta_param = {"max_num_sta": 0} |
| self.setup_ap( |
| ssid, |
| additional_ap_parameters=reject_all_sta_param, |
| channel=hostapd_constants.AP_DEFAULT_CHANNEL_5G, |
| wnm_features=wnm_features, |
| ) |
| |
| # Construct a BTM request. |
| dest_bssid = self.access_point.get_bssid_from_ssid( |
| ssid, self.access_point.wlan_5g |
| ) |
| dest_bssid_info = BssidInformation( |
| security=True, capabilities=BssidInformationCapabilities() |
| ) |
| neighbor_5g_ap = NeighborReportElement( |
| dest_bssid, |
| dest_bssid_info, |
| operating_class=116, |
| channel_number=hostapd_constants.AP_DEFAULT_CHANNEL_5G, |
| phy_type=PhyType.VHT, |
| ) |
| btm_req = BssTransitionManagementRequest( |
| disassociation_imminent=True, candidate_list=[neighbor_5g_ap] |
| ) |
| |
| # Sleep to avoid concurrent scan during reassociation, necessary due to a firmware bug. |
| # TODO(fxbug.dev/42068735) Remove when fixed, or when non-firmware BTM support is merged. |
| time.sleep(5) |
| |
| # Send BTM request from 2.4 GHz AP to DUT |
| self.access_point.send_bss_transition_management_req( |
| self.access_point.wlan_2g, client_mac, btm_req |
| ) |
| |
| # Check that DUT has not reassociated. |
| ROAM_DEADLINE = datetime.now(timezone.utc) + timedelta(seconds=2) |
| while datetime.now(timezone.utc) < ROAM_DEADLINE: |
| # Check that DUT has not reassociated to 5 GHz AP (as seen from AP). |
| if self.access_point.sta_associated(self.access_point.wlan_5g, client_mac): |
| raise signals.TestFailure("DUT unexpectedly roamed to 5GHz band") |
| else: |
| time.sleep(0.25) |
| |
| |
| if __name__ == "__main__": |
| test_runner.main() |