blob: 471e514b255d0005e564405cc0197160e58f0029 [file] [log] [blame]
#!/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()