blob: 39c702abd31b322b2d75878f5ef01bd206ef7a47 [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 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()