| #!/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 datetime import datetime, timedelta, timezone |
| from typing import FrozenSet, Optional |
| |
| 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 |
| from antlion.controllers.ap_lib.radio_measurement import ( |
| BssidInformation, |
| BssidInformationCapabilities, |
| NeighborReportElement, |
| PhyType, |
| ) |
| from antlion.controllers.ap_lib.wireless_network_management import ( |
| BssTransitionManagementRequest, |
| ) |
| from antlion.test_utils.abstract_devices.wlan_device import create_wlan_device |
| from antlion.test_utils.wifi import base_test |
| |
| |
| # 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 these |
| tests check that WNM features are not enabled. |
| """ |
| |
| 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] |
| if self.fuchsia_device.association_mechanism != "policy": |
| raise AttributeError("Must use WLAN policy layer to test WNM.") |
| |
| self.dut = create_wlan_device(self.fuchsia_device) |
| 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, |
| additional_ap_parameters: Optional[dict] = 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. |
| additional_ap_parameters: A dictionary of parameters that can sent |
| directly into 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(security_mode=None), |
| 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. |
| ConnectionError if the DUT interface query fails. |
| """ |
| for wlan_iface in self.dut.get_wlan_interface_id_list(): |
| iface_info = self.fuchsia_device.sl4f.wlan_lib.wlanQueryInterface( |
| wlan_iface |
| ) |
| if iface_info.get("error"): |
| raise ConnectionError( |
| f"Failed to query wlan iface: {iface_info['error']}" |
| ) |
| |
| if iface_info["result"]["role"] == "Client": |
| return utils.mac_address_list_to_str(iface_info["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() |
| |
| 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() |
| |
| 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): |
| 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() |
| |
| 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", |
| ) |
| |
| def test_roam_on_btm_req(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( |
| client_mac in self.access_point.get_stas(self.access_point.wlan_2g), |
| "Client MAC not included in list of associated STAs 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( |
| 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/117517) 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 reassociated. |
| REASSOC_DEADLINE = datetime.now(timezone.utc) + timedelta(seconds=2) |
| while datetime.now(timezone.utc) < REASSOC_DEADLINE: |
| if client_mac in self.access_point.get_stas(self.access_point.wlan_5g): |
| break |
| else: |
| time.sleep(0.25) |
| |
| # Verify that DUT roamed (as seen from AP). |
| asserts.assert_true( |
| client_mac in self.access_point.get_stas(self.access_point.wlan_5g), |
| "Client MAC not included in list of associated STAs 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( |
| client_mac in self.access_point.get_stas(self.access_point.wlan_2g), |
| "Client MAC not included in list of associated STAs 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 reassociated. |
| REASSOC_DEADLINE = datetime.now(timezone.utc) + timedelta(seconds=2) |
| while datetime.now(timezone.utc) < REASSOC_DEADLINE: |
| # Fail if DUT has reassociated to 5 GHz AP (as seen from AP). |
| if client_mac in self.access_point.get_stas(self.access_point.wlan_5g): |
| 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( |
| client_mac in self.access_point.get_stas(self.access_point.wlan_2g), |
| "DUT 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( |
| client_mac in self.access_point.get_stas(self.access_point.wlan_2g), |
| "Client MAC not included in list of associated STAs 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=126, |
| 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/117517) 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. |
| REASSOC_DEADLINE = datetime.now(timezone.utc) + timedelta(seconds=2) |
| while datetime.now(timezone.utc) < REASSOC_DEADLINE: |
| # Fail if DUT has reassociated to 5 GHz AP (as seen from AP). |
| if client_mac in self.access_point.get_stas(self.access_point.wlan_5g): |
| 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( |
| client_mac in self.access_point.get_stas(self.access_point.wlan_2g), |
| "DUT lost association on the 2.4GHz band after BTM request", |
| ) |
| |
| |
| if __name__ == "__main__": |
| test_runner.main() |