android_packages_modules_Co.../staticlibs/testutils/host/python/apf_utils.py
Junyu Lai 4a1baf042b Add support for sending raw packets
This commit includes several new functions to support sending custom
raw Ethernet packets. This includes methods for retrieving
MAC addresses, sending empty EtherCAT packets, and sending
arbitrary raw packets with specified types and destinations.
These functions are primarily intended for testing purposes
and rely on the `send-raw-packet-downstream` command provided
by NetworkStack.

Test: atest NetworkStaticLibHostPythonTests
Bug: 335368434
Change-Id: Iae7879bf92e1dda8fbd32b92ff166e82c87593c7
2024-07-01 16:53:50 +08:00

199 lines
6.5 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.
import re
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
# Constants.
ETHER_BROADCAST = "FFFFFFFFFFFF"
ETH_P_ETHERCAT = "88A4"
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 send_broadcast_empty_ethercat_packet(
ad: android_device.AndroidDevice, iface_name: str
) -> None:
"""Transmits a broadcast empty EtherCat packet on the specified interface."""
# Get the interface's MAC address.
mac_address = get_hardware_address(ad, iface_name)
# TODO: Build packet by using scapy library.
# Ethernet header (14 bytes).
packet = ETHER_BROADCAST # Destination MAC (broadcast)
packet += mac_address.replace(":", "") # Source MAC
packet += ETH_P_ETHERCAT # EtherType (EtherCAT)
# EtherCAT header (2 bytes) + 44 bytes of zero padding.
packet += "00" * 46
# Send the packet using a raw socket.
send_raw_packet_downstream(
ad, ETH_P_ETHERCAT, iface_name, ETHER_BROADCAST, packet
)
def send_raw_packet_downstream(
ad: android_device.AndroidDevice,
packetType: str,
iface_name: str,
dst_mac: 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.
packetType: The type of packet to send (e.g., "88A4" for EtherCAT).
iface_name: The name of the network interface to use (e.g., "wlan0",
"eth0").
dst_mac: The destination MAC address of the packet (e.g., "FFFFFFFFFFFF"
for broadcast).
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" {packetType} {iface_name} {dst_mac} {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}."
)