android_packages_modules_Co.../staticlibs/testutils/host/python/apf_utils.py
Junyu Lai 73711f7b41 Check APF version, VSR API level, and Android version
This commit introduces checks for APF version, VSR API level,
and Android version to decide whether an APFv4 test should be
run, skipped, or fail:
* If Android version is less than V, skip the test
* If VSR API level is greater than or equal to 14 and APF
  version is less than 4, fail the test
* Otherwise, if APF version is less than 4, skip the test

Test: m connectivity_multi_devices_snippet && \
      atest CtsConnectivityMultiDevicesTestCases:ConnectivityMultiDevicesTest
Fix: 352132428
Change-Id: I023164fb59ad40f489abea2e0c45761efc39699b
2024-09-10 18:06:28 +08:00

243 lines
7.7 KiB
Python

# Copyright (C) 2024 The Android Open Source Project
#
# 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.
from dataclasses import dataclass
import re
from mobly import asserts
from mobly.controllers import android_device
from mobly.controllers.android_device_lib.adb import AdbError
from net_tests_utils.host.python import adb_utils, assert_utils
class PatternNotFoundException(Exception):
"""Raised when the given pattern cannot be found."""
class UnsupportedOperationException(Exception):
pass
def get_apf_counter(
ad: android_device.AndroidDevice, iface: str, counter_name: str
) -> int:
counters = get_apf_counters_from_dumpsys(ad, iface)
return counters.get(counter_name, 0)
def get_apf_counters_from_dumpsys(
ad: android_device.AndroidDevice, iface_name: str
) -> dict:
dumpsys = adb_utils.get_dumpsys_for_service(ad, "network_stack")
# Extract IpClient section of the specified interface.
# This takes inputs like:
# IpClient.wlan0
# ...
# IpClient.wlan1
# ...
iface_pattern = re.compile(
r"^IpClient\." + iface_name + r"\n" + r"((^\s.*\n)+)", re.MULTILINE
)
iface_result = iface_pattern.search(dumpsys)
if iface_result is None:
raise PatternNotFoundException("Cannot find IpClient for " + iface_name)
# Extract APF counters section from IpClient section, which looks like:
# APF packet counters:
# COUNTER_NAME: VALUE
# ....
apf_pattern = re.compile(
r"APF packet counters:.*\n.(\s+[A-Z_0-9]+: \d+\n)+", re.MULTILINE
)
apf_result = apf_pattern.search(iface_result.group(0))
if apf_result is None:
raise PatternNotFoundException(
"Cannot find APF counters in text: " + iface_result.group(0)
)
# Extract key-value pairs from APF counters section into a list of tuples,
# e.g. [('COUNTER1', '1'), ('COUNTER2', '2')].
counter_pattern = re.compile(r"(?P<name>[A-Z_0-9]+): (?P<value>\d+)")
counter_result = counter_pattern.findall(apf_result.group(0))
if counter_result is None:
raise PatternNotFoundException(
"Cannot extract APF counters in text: " + apf_result.group(0)
)
# Convert into a dict.
result = {}
for key, value_str in counter_result:
result[key] = int(value_str)
ad.log.debug("Getting apf counters: " + str(result))
return result
def get_hardware_address(
ad: android_device.AndroidDevice, iface_name: str
) -> str:
"""Retrieves the hardware (MAC) address for a given network interface.
Returns:
The hex representative of the MAC address in uppercase.
E.g. 12:34:56:78:90:AB
Raises:
PatternNotFoundException: If the MAC address is not found in the command
output.
"""
# Run the "ip link" command and get its output.
ip_link_output = adb_utils.adb_shell(ad, f"ip link show {iface_name}")
# Regular expression to extract the MAC address.
# Parse hardware address from ip link output like below:
# 46: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq ...
# link/ether 72:05:77:82:21:e0 brd ff:ff:ff:ff:ff:ff
pattern = r"link/ether (([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2})"
match = re.search(pattern, ip_link_output)
if match:
return match.group(1).upper() # Extract the MAC address string.
else:
raise PatternNotFoundException(
"Cannot get hardware address for " + iface_name
)
def is_send_raw_packet_downstream_supported(
ad: android_device.AndroidDevice,
) -> bool:
try:
# Invoke the shell command with empty argument and see how NetworkStack respond.
# If supported, an IllegalArgumentException with help page will be printed.
send_raw_packet_downstream(ad, "", "")
except assert_utils.UnexpectedBehaviorError:
return True
except UnsupportedOperationException:
return False
def send_raw_packet_downstream(
ad: android_device.AndroidDevice,
iface_name: str,
packet_in_hex: str,
) -> None:
"""Sends a raw packet over the specified downstream interface.
This function constructs and sends a raw packet using the
`send-raw-packet-downstream`
command provided by NetworkStack process. It's primarily intended for testing
purposes.
Args:
ad: The AndroidDevice object representing the connected device.
iface_name: The name of the network interface to use (e.g., "wlan0",
"eth0").
packet_in_hex: The raw packet data starting from L2 header encoded in
hexadecimal string format.
Raises:
UnsupportedOperationException: If the NetworkStack doesn't support
the `send-raw-packet` command.
UnexpectedBehaviorException: If the command execution produces unexpected
output other than an empty response or "Unknown command".
Important Considerations:
Security: This method only works on tethering downstream interfaces due
to security restrictions.
Packet Format: The `packet_in_hex` must be a valid hexadecimal
representation of a packet starting from L2 header.
"""
cmd = (
"cmd network_stack send-raw-packet-downstream"
f" {iface_name} {packet_in_hex}"
)
# Expect no output or Unknown command if NetworkStack is too old. Throw otherwise.
try:
output = adb_utils.adb_shell(ad, cmd)
except AdbError as e:
output = str(e.stdout)
if output:
if "Unknown command" in output:
raise UnsupportedOperationException(
"send-raw-packet-downstream command is not supported."
)
raise assert_utils.UnexpectedBehaviorError(
f"Got unexpected output: {output} for command: {cmd}."
)
@dataclass
class ApfCapabilities:
"""APF program support capabilities.
See android.net.apf.ApfCapabilities.
Attributes:
apf_version_supported (int): Version of APF instruction set supported for
packet filtering. 0 indicates no support for packet filtering using APF
programs.
apf_ram_size (int): Size of APF ram.
apf_packet_format (int): Format of packets passed to APF filter. Should be
one of ARPHRD_*
"""
apf_version_supported: int
apf_ram_size: int
apf_packet_format: int
def __init__(
self,
apf_version_supported: int,
apf_ram_size: int,
apf_packet_format: int,
):
self.apf_version_supported = apf_version_supported
self.apf_ram_size = apf_ram_size
self.apf_packet_format = apf_packet_format
def __str__(self):
"""Returns a user-friendly string representation of the APF capabilities."""
return (
f"APF Version: {self.apf_version_supported}\n"
f"Ram Size: {self.apf_ram_size} bytes\n"
f"Packet Format: {self.apf_packet_format}"
)
def get_apf_capabilities(
ad: android_device.AndroidDevice, iface_name: str
) -> ApfCapabilities:
output = adb_utils.adb_shell(
ad, f"cmd network_stack apf {iface_name} capabilities"
)
try:
values = [int(value_str) for value_str in output.split(",")]
except ValueError:
return ApfCapabilities(0, 0, 0) # Conversion to integer failed
return ApfCapabilities(values[0], values[1], values[2])
def assume_apf_version_support_at_least(
ad: android_device.AndroidDevice, iface_name: str, expected_version: int
) -> None:
caps = get_apf_capabilities(ad, iface_name)
asserts.abort_class_if(
caps.apf_version_supported < expected_version,
f"Supported apf version {caps.apf_version_supported} < expected version"
f" {expected_version}",
)