Resguardo inicial

This commit is contained in:
lastdragon
2026-09-06 12:53:21 -06:00
parent 662efe3c2a
commit ad2088d178
94853 changed files with 43200404 additions and 0 deletions
@@ -0,0 +1,4 @@
# SPDX-License-Identifier: GPL-2.0-only
iou-zcrx
ncdevmem
toeplitz
@@ -0,0 +1,91 @@
# SPDX-License-Identifier: GPL-2.0+ OR MIT
# Check if io_uring supports zero-copy receive
HAS_IOURING_ZCRX := $(shell \
echo -e '#include <liburing.h>\n' \
'void *func = (void *)io_uring_register_ifq;\n' \
'int main() {return 0;}' | \
$(CC) -luring -x c - -o /dev/null 2>&1 && echo y)
ifeq ($(HAS_IOURING_ZCRX),y)
COND_GEN_FILES += iou-zcrx
else
$(warning excluding iouring tests, liburing not installed or too old)
endif
TEST_GEN_FILES := \
$(COND_GEN_FILES) \
# end of TEST_GEN_FILES
TEST_PROGS = \
csum.py \
devlink_port_split.py \
devlink_rate_tc_bw.py \
devmem.py \
ethtool.sh \
ethtool_extended_state.sh \
ethtool_mm.sh \
ethtool_rmon.sh \
ethtool_std_stats.sh \
gro_hw.py \
hw_stats_l3.sh \
hw_stats_l3_gre.sh \
iou-zcrx.py \
ipsec_vxlan.py \
irq.py \
loopback.sh \
nic_timestamp.py \
nk_devmem.py \
nk_netns.py \
nk_qlease.py \
ntuple.py \
pp_alloc_fail.py \
rss_api.py \
rss_ctx.py \
rss_drv.py \
rss_flow_label.py \
rss_input_xfrm.py \
toeplitz.py \
tso.py \
userns_devmem.py \
uso.py \
xdp_metadata.py \
xsk_reconfig.py \
#
TEST_FILES := \
devmem_lib.py \
ethtool_lib.sh \
#
TEST_INCLUDES := \
$(wildcard lib/py/*.py ../lib/py/*.py) \
../../../net/lib.sh \
../../../net/forwarding/ipip_lib.sh \
../../../net/forwarding/lib.sh \
../../../net/forwarding/tc_common.sh \
#
# YNL files, must be before "include ..lib.mk"
YNL_GEN_FILES := \
ncdevmem \
toeplitz \
# end of YNL_GEN_FILES
TEST_GEN_FILES += $(YNL_GEN_FILES)
TEST_GEN_FILES += $(patsubst %.c,%.o,$(wildcard *.bpf.c))
include ../../../lib.mk
# YNL build
YNL_GENS := \
ethtool \
netdev \
# end of YNL_GENS
include ../../../net/ynl.mk
include ../../../net/bpf.mk
ifeq ($(HAS_IOURING_ZCRX),y)
$(OUTPUT)/iou-zcrx: LDLIBS += -luring
endif
@@ -0,0 +1,26 @@
CONFIG_BPF_SYSCALL=y
CONFIG_FAIL_FUNCTION=y
CONFIG_FAULT_INJECTION=y
CONFIG_FAULT_INJECTION_DEBUG_FS=y
CONFIG_FUNCTION_ERROR_INJECTION=y
CONFIG_HUGETLBFS=y
CONFIG_INET6_ESP=y
CONFIG_INET6_ESP_OFFLOAD=y
CONFIG_INET_ESP=y
CONFIG_INET_ESP_OFFLOAD=y
CONFIG_IO_URING=y
CONFIG_IPV6=y
CONFIG_IPV6_GRE=y
CONFIG_IPV6_SIT=y
CONFIG_IPV6_TUNNEL=y
CONFIG_NET_CLS_ACT=y
CONFIG_NET_CLS_BPF=y
CONFIG_NET_IPGRE=y
CONFIG_NET_IPGRE_DEMUX=y
CONFIG_NET_IPIP=y
CONFIG_NETKIT=y
CONFIG_NET_SCH_INGRESS=y
CONFIG_UDMABUF=y
CONFIG_USER_NS=y
CONFIG_VXLAN=y
CONFIG_XFRM_USER=y
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""Run the tools/testing/selftests/net/csum testsuite."""
from os import path
from lib.py import ksft_run, ksft_exit, KsftSkipEx
from lib.py import EthtoolFamily, NetDrvEpEnv
from lib.py import bkg, cmd, wait_port_listen
def test_receive(cfg, ipver="6", extra_args=None):
"""Test local nic checksum receive. Remote host sends crafted packets."""
if not cfg.have_rx_csum:
raise KsftSkipEx(f"Test requires rx checksum offload on {cfg.ifname}")
ip_args = f"-{ipver} -S {cfg.remote_addr_v[ipver]} -D {cfg.addr_v[ipver]}"
rx_cmd = f"{cfg.bin_local} -i {cfg.ifname} -n 100 {ip_args} -r 1 -R {extra_args}"
tx_cmd = f"{cfg.bin_remote} -i {cfg.remote_ifname} -n 100 {ip_args} -r 1 -T {extra_args}"
with bkg(rx_cmd, exit_wait=True):
wait_port_listen(34000, proto="udp")
cmd(tx_cmd, host=cfg.remote)
def test_transmit(cfg, ipver="6", extra_args=None):
"""Test local nic checksum transmit. Remote host verifies packets."""
if (not cfg.have_tx_csum_generic and
not (cfg.have_tx_csum_ipv4 and ipver == "4") and
not (cfg.have_tx_csum_ipv6 and ipver == "6")):
raise KsftSkipEx(f"Test requires tx checksum offload on {cfg.ifname}")
ip_args = f"-{ipver} -S {cfg.addr_v[ipver]} -D {cfg.remote_addr_v[ipver]}"
# Cannot randomize input when calculating zero checksum
if extra_args != "-U -Z":
extra_args += " -r 1"
rx_cmd = f"{cfg.bin_remote} -i {cfg.remote_ifname} -L 1 -n 100 {ip_args} -R {extra_args}"
tx_cmd = f"{cfg.bin_local} -i {cfg.ifname} -L 1 -n 100 {ip_args} -T {extra_args}"
with bkg(rx_cmd, host=cfg.remote, exit_wait=True):
wait_port_listen(34000, proto="udp", host=cfg.remote)
cmd(tx_cmd)
def test_builder(name, cfg, ipver="6", tx=False, extra_args=""):
"""Construct specific tests from the common template.
Most tests follow the same basic pattern, differing only in
Direction of the test and optional flags passed to csum."""
def f(cfg):
cfg.require_ipver(ipver)
if tx:
test_transmit(cfg, ipver, extra_args)
else:
test_receive(cfg, ipver, extra_args)
f.__name__ = f"ipv{ipver}_" + name
return f
def check_nic_features(cfg) -> None:
"""Test whether Tx and Rx checksum offload are enabled.
If the device under test has either off, then skip the relevant tests."""
cfg.have_tx_csum_generic = False
cfg.have_tx_csum_ipv4 = False
cfg.have_tx_csum_ipv6 = False
cfg.have_rx_csum = False
ethnl = EthtoolFamily()
features = ethnl.features_get({"header": {"dev-index": cfg.ifindex}})
for f in features["active"]["bits"]["bit"]:
if f["name"] == "tx-checksum-ip-generic":
cfg.have_tx_csum_generic = True
elif f["name"] == "tx-checksum-ipv4":
cfg.have_tx_csum_ipv4 = True
elif f["name"] == "tx-checksum-ipv6":
cfg.have_tx_csum_ipv6 = True
elif f["name"] == "rx-checksum":
cfg.have_rx_csum = True
def main() -> None:
with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
check_nic_features(cfg)
cfg.bin_local = cfg.net_lib_dir / "csum"
cfg.bin_remote = cfg.remote.deploy(cfg.bin_local)
cases = []
for ipver in ["4", "6"]:
cases.append(test_builder("rx_tcp", cfg, ipver, False, "-t"))
cases.append(test_builder("rx_tcp_invalid", cfg, ipver, False, "-t -E"))
cases.append(test_builder("rx_udp", cfg, ipver, False, ""))
cases.append(test_builder("rx_udp_invalid", cfg, ipver, False, "-E"))
cases.append(test_builder("tx_udp_csum_offload", cfg, ipver, True, "-U"))
cases.append(test_builder("tx_udp_zero_checksum", cfg, ipver, True, "-U -Z"))
ksft_run(cases=cases, args=(cfg, ))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,309 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
from subprocess import PIPE, Popen
import json
import time
import argparse
import collections
import sys
#
# Test port split configuration using devlink-port lanes attribute.
# The test is skipped in case the attribute is not available.
#
# First, check that all the ports with 1 lane fail to split.
# Second, check that all the ports with more than 1 lane can be split
# to all valid configurations (e.g., split to 2, split to 4 etc.)
#
# Kselftest framework requirement - SKIP code is 4
KSFT_SKIP=4
Port = collections.namedtuple('Port', 'bus_info name')
def run_command(cmd, should_fail=False):
"""
Run a command in subprocess.
Return: Tuple of (stdout, stderr).
"""
p = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True)
stdout, stderr = p.communicate()
stdout, stderr = stdout.decode(), stderr.decode()
if stderr != "" and not should_fail:
print("Error sending command: %s" % cmd)
print(stdout)
print(stderr)
return stdout, stderr
class devlink_ports(object):
"""
Class that holds information on the devlink ports, required to the tests;
if_names: A list of interfaces in the devlink ports.
"""
def get_if_names(dev):
"""
Get a list of physical devlink ports.
Return: Array of tuples (bus_info/port, if_name).
"""
arr = []
cmd = "devlink -j port show"
stdout, stderr = run_command(cmd)
assert stderr == ""
ports = json.loads(stdout)['port']
validate_devlink_output(ports, 'flavour')
for port in ports:
if dev in port:
if ports[port]['flavour'] == 'physical':
arr.append(Port(bus_info=port, name=ports[port]['netdev']))
return arr
def __init__(self, dev):
self.if_names = devlink_ports.get_if_names(dev)
def get_max_lanes(port):
"""
Get the $port's maximum number of lanes.
Return: number of lanes, e.g. 1, 2, 4 and 8.
"""
cmd = "devlink -j port show %s" % port
stdout, stderr = run_command(cmd)
assert stderr == ""
values = list(json.loads(stdout)['port'].values())[0]
if 'lanes' in values:
lanes = values['lanes']
else:
lanes = 0
return lanes
def get_split_ability(port):
"""
Get the $port split ability.
Return: split ability, true or false.
"""
cmd = "devlink -j port show %s" % port.name
stdout, stderr = run_command(cmd)
assert stderr == ""
values = list(json.loads(stdout)['port'].values())[0]
return values['splittable']
def split(k, port, should_fail=False):
"""
Split $port into $k ports.
If should_fail == True, the split should fail. Otherwise, should pass.
Return: Array of sub ports after splitting.
If the $port wasn't split, the array will be empty.
"""
cmd = "devlink port split %s count %s" % (port.bus_info, k)
stdout, stderr = run_command(cmd, should_fail=should_fail)
if should_fail:
if not test(stderr != "", "%s is unsplittable" % port.name):
print("split an unsplittable port %s" % port.name)
return create_split_group(port, k)
else:
if stderr == "":
return create_split_group(port, k)
print("didn't split a splittable port %s" % port.name)
return []
def unsplit(port):
"""
Unsplit $port.
"""
cmd = "devlink port unsplit %s" % port
stdout, stderr = run_command(cmd)
test(stderr == "", "Unsplit port %s" % port)
def exists(port, dev):
"""
Check if $port exists in the devlink ports.
Return: True is so, False otherwise.
"""
return any(dev_port.name == port
for dev_port in devlink_ports.get_if_names(dev))
def exists_and_lanes(ports, lanes, dev):
"""
Check if every port in the list $ports exists in the devlink ports and has
$lanes number of lanes after splitting.
Return: True if both are True, False otherwise.
"""
for port in ports:
max_lanes = get_max_lanes(port)
if not exists(port, dev):
print("port %s doesn't exist in devlink ports" % port)
return False
if max_lanes != lanes:
print("port %s has %d lanes, but %s were expected"
% (port, lanes, max_lanes))
return False
return True
def test(cond, msg):
"""
Check $cond and print a message accordingly.
Return: True is pass, False otherwise.
"""
if cond:
print("TEST: %-60s [ OK ]" % msg)
else:
print("TEST: %-60s [FAIL]" % msg)
return cond
def create_split_group(port, k):
"""
Create the split group for $port.
Return: Array with $k elements, which are the split port group.
"""
return list(port.name + "s" + str(i) for i in range(k))
def split_unsplittable_port(port, k):
"""
Test that splitting of unsplittable port fails.
"""
# split to max
new_split_group = split(k, port, should_fail=True)
if new_split_group != []:
unsplit(port.bus_info)
def split_splittable_port(port, k, lanes, dev):
"""
Test that splitting of splittable port passes correctly.
"""
new_split_group = split(k, port)
# Once the split command ends, it takes some time to the sub ifaces'
# to get their names. Use udevadm to continue only when all current udev
# events are handled.
cmd = "udevadm settle"
stdout, stderr = run_command(cmd)
assert stderr == ""
if new_split_group != []:
test(exists_and_lanes(new_split_group, lanes/k, dev),
"split port %s into %s" % (port.name, k))
unsplit(port.bus_info)
def validate_devlink_output(devlink_data, target_property=None):
"""
Determine if test should be skipped by checking:
1. devlink_data contains values
2. The target_property exist in devlink_data
"""
skip_reason = None
if any(devlink_data.values()):
if target_property:
skip_reason = "{} not found in devlink output, test skipped".format(target_property)
for key in devlink_data:
if target_property in devlink_data[key]:
skip_reason = None
else:
skip_reason = 'devlink output is empty, test skipped'
if skip_reason:
print(skip_reason)
sys.exit(KSFT_SKIP)
def make_parser():
parser = argparse.ArgumentParser(description='A test for port splitting.')
parser.add_argument('--dev',
help='The devlink handle of the device under test. ' +
'The default is the first registered devlink ' +
'handle.')
return parser
def main(cmdline=None):
parser = make_parser()
args = parser.parse_args(cmdline)
dev = args.dev
if not dev:
cmd = "devlink -j dev show"
stdout, stderr = run_command(cmd)
assert stderr == ""
validate_devlink_output(json.loads(stdout))
devs = json.loads(stdout)['dev']
dev = list(devs.keys())[0]
cmd = "devlink dev show %s" % dev
stdout, stderr = run_command(cmd)
if stderr != "":
print("devlink device %s can not be found" % dev)
sys.exit(1)
ports = devlink_ports(dev)
found_max_lanes = False
for port in ports.if_names:
max_lanes = get_max_lanes(port.name)
# If max lanes is 0, do not test port splitting at all
if max_lanes == 0:
continue
# If 1 lane, shouldn't be able to split
elif max_lanes == 1:
test(not get_split_ability(port),
"%s should not be able to split" % port.name)
split_unsplittable_port(port, max_lanes)
# Else, splitting should pass and all the split ports should exist.
else:
lane = max_lanes
test(get_split_ability(port),
"%s should be able to split" % port.name)
while lane > 1:
split_splittable_port(port, lane, max_lanes, dev)
lane //= 2
found_max_lanes = True
if not found_max_lanes:
print(f"Test not started, no port of device {dev} reports max_lanes")
sys.exit(KSFT_SKIP)
if __name__ == "__main__":
main()
@@ -0,0 +1,439 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""
Devlink Rate TC Bandwidth Test Suite
===================================
This test suite verifies the functionality of devlink-rate traffic class (TC)
bandwidth distribution in a virtualized environment. The tests validate that
bandwidth can be properly allocated between different traffic classes and
that TC mapping works as expected.
Test Environment:
----------------
- Creates 1 VF
- Establishes a bridge connecting the VF representor and the uplink representor
- Sets up 2 VLAN interfaces on the VF with different VLAN IDs (101, 102)
- Configures different traffic classes (TC3 and TC4) for each VLAN
Test Cases:
----------
1. test_no_tc_mapping_bandwidth:
- Verifies that without TC mapping, bandwidth is NOT distributed according to
the configured 20/80 split between TC3 and TC4
- This test should fail if bandwidth matches the 20/80 split without TC
mapping
- Expected: Bandwidth should NOT be distributed as 20/80
2. test_tc_mapping_bandwidth:
- Configures TC mapping using mqprio qdisc
- Verifies that with TC mapping, bandwidth IS distributed according to the
configured 20/80 split between TC3 and TC4
- Expected: Bandwidth should be distributed as 20/80
Bandwidth Distribution:
----------------------
- TC3 (VLAN 101): Configured for 20% of total bandwidth
- TC4 (VLAN 102): Configured for 80% of total bandwidth
- Total bandwidth: 1Gbps
- Tolerance: +-12%
Hardware-Specific Behavior (mlx5):
--------------------------
mlx5 hardware enforces traffic class separation by ensuring that each transmit
queue (SQ) is associated with a single TC. If a packet is sent on a queue that
doesn't match the expected TC (based on DSCP or VLAN priority and hypervisor-set
mapping), the hardware moves the queue to the correct TC scheduler to preserve
traffic isolation.
This behavior means that even without explicit TC-to-queue mapping, bandwidth
enforcement may still appear to work—because the hardware dynamically adjusts
the scheduling context. However, this can lead to performance issues in high
rates and HOL blocking if traffic from different TCs is mixed on the same queue.
"""
import json
import os
import subprocess
import threading
import time
from lib.py import ksft_pr, ksft_run, ksft_exit
from lib.py import KsftSkipEx, KsftFailEx, KsftXfailEx
from lib.py import NetDrvEpEnv, DevlinkFamily
from lib.py import NlError
from lib.py import cmd, defer, ethtool, ip
from lib.py import Iperf3Runner
class BandwidthValidator:
"""
Validates total bandwidth and individual shares with tolerance
relative to the overall total.
"""
def __init__(self, shares):
self.tolerance_percent = 12
self.expected_total = sum(shares.values())
self.bounds = {}
for name, exp in shares.items():
self.bounds[name] = (self.min_expected(exp), self.max_expected(exp))
def min_expected(self, value):
"""Calculates the minimum acceptable value based on tolerance."""
return value - (self.expected_total * self.tolerance_percent / 100)
def max_expected(self, value):
"""Calculates the maximum acceptable value based on tolerance."""
return value + (self.expected_total * self.tolerance_percent / 100)
def bound(self, values):
"""
Return True if all given values fall within tolerance.
"""
for name, value in values.items():
low, high = self.bounds[name]
if not low <= value <= high:
return False
return True
def setup_vf(cfg, set_tc_mapping=True):
"""
Sets up a VF on the given network interface.
Enables SR-IOV and switchdev mode, brings the VF interface up,
and optionally configures TC mapping using mqprio.
"""
try:
cmd(f"devlink dev eswitch set pci/{cfg.pci} mode switchdev")
defer(cmd, f"devlink dev eswitch set pci/{cfg.pci} mode legacy")
except Exception as exc:
raise KsftSkipEx(f"Failed to enable switchdev mode on {cfg.pci}") from exc
try:
cmd(f"echo 1 > /sys/class/net/{cfg.ifname}/device/sriov_numvfs", shell=True)
defer(cmd, f"echo 0 > /sys/class/net/{cfg.ifname}/device/sriov_numvfs", shell=True)
except Exception as exc:
raise KsftSkipEx(f"Failed to enable SR-IOV on {cfg.ifname}") from exc
time.sleep(2)
vf_ifc = (os.listdir(
f"/sys/class/net/{cfg.ifname}/device/virtfn0/net") or [None])[0]
if vf_ifc:
ip(f"link set dev {vf_ifc} up")
else:
raise KsftSkipEx("VF interface not found")
if set_tc_mapping:
cmd(f"tc qdisc add dev {vf_ifc} root handle 5 mqprio mode dcb hw 1 num_tc 8")
return vf_ifc
def setup_vlans_on_vf(vf_ifc):
"""
Sets up two VLAN interfaces on the given VF, each mapped to a different TC.
"""
vlan_configs = [
{"vlan_id": 101, "tc": 3, "ip": "198.51.100.1"},
{"vlan_id": 102, "tc": 4, "ip": "198.51.100.9"},
]
for config in vlan_configs:
vlan_dev = f"{vf_ifc}.{config['vlan_id']}"
ip(f"link add link {vf_ifc} name {vlan_dev} type vlan id {config['vlan_id']}")
ip(f"addr add {config['ip']}/29 dev {vlan_dev}")
ip(f"link set dev {vlan_dev} up")
ip(f"link set dev {vlan_dev} type vlan egress-qos-map 0:{config['tc']}")
ksft_pr(f"Created VLAN {vlan_dev} on {vf_ifc} with tc {config['tc']} and IP {config['ip']}")
def get_vf_info(cfg):
"""
Finds the VF representor interface and devlink port index
for the given PCI device used in the test environment.
"""
cfg.vf_representor = None
cfg.vf_port_index = None
out = subprocess.check_output(["devlink", "-j", "port", "show"], encoding="utf-8")
ports = json.loads(out)["port"]
for port_name, props in ports.items():
netdev = props.get("netdev")
if (port_name.startswith(f"pci/{cfg.pci}/") and
props.get("vfnum") == 0):
cfg.vf_representor = netdev
cfg.vf_port_index = int(port_name.split("/")[-1])
break
def setup_bridge(cfg):
"""
Creates and configures a Linux bridge, with both the uplink
and VF representor interfaces attached to it.
"""
bridge_name = f"br_{os.getpid()}"
ip(f"link add name {bridge_name} type bridge")
defer(cmd, f"ip link del name {bridge_name} type bridge")
ip(f"link set dev {cfg.ifname} master {bridge_name}")
rep_name = cfg.vf_representor
if rep_name:
ip(f"link set dev {rep_name} master {bridge_name}")
ip(f"link set dev {rep_name} up")
ksft_pr(f"Set representor {rep_name} up and added to bridge")
else:
raise KsftSkipEx("Could not find representor for the VF")
ip(f"link set dev {bridge_name} up")
def setup_devlink_rate(cfg):
"""
Configures devlink rate tx_max and traffic class bandwidth for the VF.
"""
port_index = cfg.vf_port_index
if port_index is None:
raise KsftSkipEx("Could not find VF port index")
try:
cfg.devnl.rate_set({
"bus-name": "pci",
"dev-name": cfg.pci,
"port-index": port_index,
"rate-tx-max": 125000000,
"rate-tc-bws": [
{"index": 0, "bw": 0},
{"index": 1, "bw": 0},
{"index": 2, "bw": 0},
{"index": 3, "bw": 20},
{"index": 4, "bw": 80},
{"index": 5, "bw": 0},
{"index": 6, "bw": 0},
{"index": 7, "bw": 0},
]
})
except NlError as exc:
if exc.error == 95: # EOPNOTSUPP
raise KsftSkipEx("devlink rate configuration is not supported on the VF") from exc
raise KsftFailEx(f"rate_set failed on VF port {port_index}") from exc
def setup_remote_vlans(cfg):
"""
Sets up VLAN interfaces on the remote side.
"""
remote_dev = cfg.remote_ifname
vlan_ids = [101, 102]
remote_ips = ["198.51.100.2", "198.51.100.10"]
for vlan_id, ip_addr in zip(vlan_ids, remote_ips):
vlan_dev = f"{remote_dev}.{vlan_id}"
cmd(f"ip link add link {remote_dev} name {vlan_dev} "
f"type vlan id {vlan_id}", host=cfg.remote)
cmd(f"ip addr add {ip_addr}/29 dev {vlan_dev}", host=cfg.remote)
cmd(f"ip link set dev {vlan_dev} up", host=cfg.remote)
defer(cmd, f"ip link del {vlan_dev}", host=cfg.remote)
def setup_test_environment(cfg, set_tc_mapping=True):
"""
Sets up the complete test environment including VF creation, VLANs,
bridge configuration and devlink rate setup.
"""
vf_ifc = setup_vf(cfg, set_tc_mapping)
ksft_pr(f"Created VF interface: {vf_ifc}")
setup_vlans_on_vf(vf_ifc)
get_vf_info(cfg)
setup_bridge(cfg)
setup_devlink_rate(cfg)
setup_remote_vlans(cfg)
def measure_bandwidth(cfg, server_ip, client_ip, barrier):
"""
Synchronizes with peers and runs an iperf3-based bandwidth measurement
between the given endpoints. Returns average Gbps.
"""
runner = Iperf3Runner(cfg, server_ip=server_ip, client_ip=client_ip)
try:
barrier.wait(timeout=10)
except Exception as exc:
raise KsftFailEx("iperf3 barrier wait timed") from exc
try:
bw_gbps = runner.measure_bandwidth(reverse=True)
except Exception as exc:
raise KsftFailEx("iperf3 bandwidth measurement failed") from exc
return bw_gbps
def run_bandwidth_test(cfg):
"""
Runs parallel bandwidth measurements for each VLAN/TC pair and collects results.
"""
def _run_measure_bandwidth_thread(local_ip, remote_ip, results, barrier, tc_ix):
results[tc_ix] = measure_bandwidth(cfg, local_ip, remote_ip, barrier)
vf_vlan_data = [
# (local_ip, remote_ip, TC)
("198.51.100.1", "198.51.100.2", 3),
("198.51.100.9", "198.51.100.10", 4),
]
results = {}
threads = []
start_barrier = threading.Barrier(len(vf_vlan_data))
for local_ip, remote_ip, tc_ix in vf_vlan_data:
thread = threading.Thread(
target=_run_measure_bandwidth_thread,
args=(local_ip, remote_ip, results, start_barrier, tc_ix)
)
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
for tc_ix, tc_bw in results.items():
if tc_bw is None:
raise KsftFailEx("iperf3 failed; cannot evaluate bandwidth")
return results
def calculate_bandwidth_percentages(results):
"""
Calculates the percentage of total bandwidth received by TC3 and TC4.
"""
if 3 not in results or 4 not in results:
raise KsftFailEx(f"Missing expected TC results in {results}")
tc3_bw = results[3]
tc4_bw = results[4]
total_bw = tc3_bw + tc4_bw
tc3_percentage = (tc3_bw / total_bw) * 100
tc4_percentage = (tc4_bw / total_bw) * 100
return {
'tc3_bw': tc3_bw,
'tc4_bw': tc4_bw,
'tc3_percentage': tc3_percentage,
'tc4_percentage': tc4_percentage,
'total_bw': total_bw
}
def print_bandwidth_results(bw_data, test_name):
"""
Prints bandwidth measurements and TC usage summary for a given test.
"""
ksft_pr(f"Bandwidth check results {test_name}:")
ksft_pr(f"TC 3: {bw_data['tc3_bw']:.2f} Gbits/sec")
ksft_pr(f"TC 4: {bw_data['tc4_bw']:.2f} Gbits/sec")
ksft_pr(f"Total bandwidth: {bw_data['total_bw']:.2f} Gbits/sec")
ksft_pr(f"TC 3 percentage: {bw_data['tc3_percentage']:.1f}%")
ksft_pr(f"TC 4 percentage: {bw_data['tc4_percentage']:.1f}%")
def verify_total_bandwidth(bw_data, validator):
"""
Ensures the total measured bandwidth falls within the acceptable tolerance.
"""
total = bw_data['total_bw']
if validator.bound({"total": total}):
return
low, high = validator.bounds["total"]
if total < low:
raise KsftSkipEx(
f"Total bandwidth {total:.2f} Gbps < minimum "
f"{low:.2f} Gbps; "
f"parent tx_max ({validator.expected_total:.1f} G) "
f"not reached, cannot validate share"
)
raise KsftFailEx(
f"Total bandwidth {total:.2f} Gbps exceeds allowed ceiling "
f"{high:.2f} Gbps "
f"(VF tx_max set to {validator.expected_total:.1f} G)"
)
def run_bandwidth_distribution_test(cfg, set_tc_mapping):
"""
Runs parallel bandwidth measurements for both TCs and collects results.
"""
setup_test_environment(cfg, set_tc_mapping)
bandwidths = run_bandwidth_test(cfg)
bw_data = calculate_bandwidth_percentages(bandwidths)
test_name = "with TC mapping" if set_tc_mapping else "without TC mapping"
print_bandwidth_results(bw_data, test_name)
verify_total_bandwidth(bw_data, cfg.traffic_bw_validator)
return cfg.tc_bw_validator.bound({"tc3": bw_data['tc3_percentage'],
"tc4": bw_data['tc4_percentage']})
def test_no_tc_mapping_bandwidth(cfg):
"""
Verifies that bandwidth is not split 20/80 without traffic class mapping.
"""
pass_bw_msg = "Bandwidth is NOT distributed as 20/80 without TC mapping"
fail_bw_msg = "Bandwidth matched 20/80 split without TC mapping"
is_mlx5 = "driver: mlx5" in ethtool(f"-i {cfg.ifname}").stdout
if run_bandwidth_distribution_test(cfg, set_tc_mapping=False):
if is_mlx5:
raise KsftXfailEx(fail_bw_msg)
raise KsftFailEx(fail_bw_msg)
if is_mlx5:
raise KsftFailEx("mlx5 behavior changed:" + pass_bw_msg)
ksft_pr(pass_bw_msg)
def test_tc_mapping_bandwidth(cfg):
"""
Verifies that bandwidth is correctly split 20/80 between TC3 and TC4
when traffic class mapping is set.
"""
if run_bandwidth_distribution_test(cfg, set_tc_mapping=True):
ksft_pr("Bandwidth is distributed as 20/80 with TC mapping")
else:
raise KsftFailEx("Bandwidth did not match 20/80 split with TC mapping")
def main() -> None:
"""
Main entry point for running the test cases.
"""
with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
cfg.devnl = DevlinkFamily()
cfg.pci = os.path.basename(
os.path.realpath(f"/sys/class/net/{cfg.ifname}/device")
)
if not cfg.pci:
raise KsftSkipEx("Could not get PCI address of the interface")
cfg.traffic_bw_validator = BandwidthValidator({"total": 1})
cfg.tc_bw_validator = BandwidthValidator({"tc3": 20, "tc4": 80})
cases = [test_no_tc_mapping_bandwidth, test_tc_mapping_bandwidth]
ksft_run(cases=cases, args=(cfg,))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
from os import path
from devmem_lib import setup_test, run_rx, run_tx, run_tx_chunks, run_rx_hds
from lib.py import ksft_run, ksft_exit, ksft_disruptive
from lib.py import NetDrvEpEnv
@ksft_disruptive
def check_rx(cfg) -> None:
"""Run the devmem RX test."""
run_rx(cfg)
@ksft_disruptive
def check_tx(cfg) -> None:
"""Run the devmem TX test."""
run_tx(cfg)
@ksft_disruptive
def check_tx_chunks(cfg) -> None:
"""Run the devmem TX chunking test."""
run_tx_chunks(cfg)
def check_rx_hds(cfg) -> None:
"""Run the HDS test."""
run_rx_hds(cfg)
def main() -> None:
"""Run the devmem test cases."""
with NetDrvEpEnv(__file__) as cfg:
setup_test(cfg, path.abspath(path.dirname(__file__) + "/ncdevmem"))
ksft_run([check_rx, check_tx, check_tx_chunks, check_rx_hds],
args=(cfg,))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,222 @@
# SPDX-License-Identifier: GPL-2.0
"""Shared helpers for devmem TCP selftests."""
import re
from lib.py import (bkg, cmd, defer, ethtool, rand_port, wait_port_listen,
ksft_eq, KsftSkipEx, NetNSEnter, EthtoolFamily,
NetdevFamily)
def require_devmem(cfg):
"""Probe ncdevmem on cfg.ifname and SKIP the test if devmem isn't supported."""
if not hasattr(cfg, "devmem_probed"):
probe_command = f"{cfg.bin_local} -f {cfg.ifname}"
cfg.devmem_supported = cmd(probe_command, fail=False, shell=True).ret == 0
cfg.devmem_probed = True
if not cfg.devmem_supported:
raise KsftSkipEx("Test requires devmem support")
def configure_nic(cfg):
"""Channels, rings, RSS, queue lease for netkit devmem."""
if not hasattr(cfg, 'netns'):
return
cfg.require_ipver('6')
ethnl = EthtoolFamily()
channels = ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
channels = channels['combined-count']
if channels < 2:
raise KsftSkipEx(
'Test requires NETIF with at least 2 combined channels'
)
rings = ethnl.rings_get({'header': {'dev-index': cfg.ifindex}})
orig_rx_rings = rings['rx']
orig_hds_thresh = rings.get('hds-thresh', 0)
orig_data_split = rings.get('tcp-data-split', 'unknown')
ethnl.rings_set({'header': {'dev-index': cfg.ifindex},
'tcp-data-split': 'enabled',
'hds-thresh': 0,
'rx': min(64, orig_rx_rings)})
defer(ethnl.rings_set, {'header': {'dev-index': cfg.ifindex},
'tcp-data-split': orig_data_split,
'hds-thresh': orig_hds_thresh,
'rx': orig_rx_rings})
cfg.src_queue = channels - 1
ethtool(f"-X {cfg.ifname} equal {cfg.src_queue}")
defer(ethtool, f"-X {cfg.ifname} default")
if not hasattr(cfg, 'nk_queue'):
with NetNSEnter(str(cfg.netns)):
netdevnl = NetdevFamily()
lease_result = netdevnl.queue_create({
"ifindex": cfg.nk_guest_ifindex,
"type": "rx",
"lease": {
"ifindex": cfg.ifindex,
"queue": {"id": cfg.src_queue, "type": "rx"},
"netns-id": 0,
},
})
cfg.nk_queue = lease_result['id']
def set_flow_rule(cfg, port):
"""Install a flow rule steering to src_queue and return the flow rule ID."""
output = ethtool(
f"-N {cfg.ifname} flow-type tcp6 dst-port {port}"
f" action {cfg.src_queue}"
).stdout
return int(re.search(r'ID (\d+)', output).group(1))
def ncdevmem_rx(cfg, port, verify=True, fail_on_linear=False, flow_steer=False):
"""Build the ncdevmem RX listener command."""
if hasattr(cfg, 'netns'):
flow_rule_id = set_flow_rule(cfg, port)
defer(ethtool, f"-N {cfg.ifname} delete {flow_rule_id}")
ifname = cfg.nk_guest_ifname
addr = cfg.nk_guest_ipv6
extras = [f"-t {cfg.nk_queue}", "-q 1", "-n"]
else:
ifname = cfg.ifname
addr = cfg.addr
extras = []
if flow_steer:
extras.append(f"-c {cfg.remote_addr}")
if verify:
extras.append("-v 7")
if fail_on_linear:
extras.append("-L")
parts = [cfg.bin_local, "-l", f"-f {ifname}", f"-s {addr}",
f"-p {port}", *extras]
return " ".join(parts)
def ncdevmem_tx(cfg, port, chunk_size=0):
"""Build the ncdevmem TX send command."""
if hasattr(cfg, 'netns'):
ifname = cfg.nk_guest_ifname
addr = cfg.remote_addr_v['6']
extras = ["-t 0", "-q 1", "-n"]
else:
ifname = cfg.ifname
addr = cfg.remote_addr
extras = []
if chunk_size:
extras.append(f"-z {chunk_size}")
parts = [cfg.bin_local, f"-f {ifname}", f"-s {addr}",
f"-p {port}", *extras]
return " ".join(parts)
def socat_send(cfg, port, buf_size=0):
"""Socat command for sending to the devmem listener.
When buf_size > 0, force one TCP segment per write of exactly that size by
setting socat's buffer (-b) and disabling Nagle (TCP_NODELAY).
"""
proto = f"TCP{cfg.addr_ipver}"
if hasattr(cfg, 'netns'):
addr = f"[{cfg.nk_guest_ipv6}]"
else:
addr = cfg.baddr
suffix = f",bind={cfg.remote_baddr}:{port}"
buf = ""
if buf_size:
buf = f"-b {buf_size}"
suffix += ",nodelay"
return f"socat {buf} -u - {proto}:{addr}:{port}{suffix}"
def socat_listen(cfg, port):
"""Socat listen command for TX tests."""
return f"socat -U - TCP{cfg.addr_ipver}-LISTEN:{port}"
def setup_test(cfg, bin_local):
"""Stash the local ncdevmem path on cfg and deploy it to the remote."""
cfg.bin_local = bin_local
cfg.bin_remote = cfg.remote.deploy(cfg.bin_local)
def run_rx(cfg):
"""Run the devmem RX test."""
require_devmem(cfg)
configure_nic(cfg)
port = rand_port()
socat = socat_send(cfg, port)
data_pipe = (f"yes $(echo -e \x01\x02\x03\x04\x05\x06) | head -c 1K"
f" | {socat}")
netns = getattr(cfg, "netns", None)
listen_cmd = ncdevmem_rx(cfg, port, flow_steer=not hasattr(cfg, 'netns'))
with bkg(listen_cmd, exit_wait=True, ns=netns) as ncdevmem:
wait_port_listen(port, proto="tcp", ns=netns)
cmd(data_pipe, host=cfg.remote, shell=True)
ksft_eq(ncdevmem.ret, 0)
def run_tx(cfg):
"""Run the devmem TX test."""
require_devmem(cfg)
configure_nic(cfg)
netns = getattr(cfg, "netns", None)
port = rand_port()
tx_cmd = ncdevmem_tx(cfg, port)
listen_cmd = socat_listen(cfg, port)
with bkg(listen_cmd, host=cfg.remote, exit_wait=True) as socat:
wait_port_listen(port, host=cfg.remote)
cmd(f"bash -c 'echo -e \"hello\\nworld\" | {tx_cmd}'", ns=netns, shell=True)
ksft_eq(socat.stdout.strip(), "hello\nworld")
def run_tx_chunks(cfg):
"""Run the devmem TX chunking test."""
require_devmem(cfg)
configure_nic(cfg)
netns = getattr(cfg, "netns", None)
port = rand_port()
tx_cmd = ncdevmem_tx(cfg, port, chunk_size=3)
listen_cmd = socat_listen(cfg, port)
with bkg(listen_cmd, host=cfg.remote, exit_wait=True) as socat:
wait_port_listen(port, host=cfg.remote)
cmd(f"bash -c 'echo -e \"hello\\nworld\" | {tx_cmd}'", ns=netns, shell=True)
ksft_eq(socat.stdout.strip(), "hello\nworld")
def run_rx_hds(cfg):
"""Run the HDS test by running devmem RX across a segment size sweep."""
require_devmem(cfg)
configure_nic(cfg)
netns = getattr(cfg, "netns", None)
for size in [1, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192]:
port = rand_port()
listen_cmd = ncdevmem_rx(cfg, port, verify=False,
fail_on_linear=True)
socat = socat_send(cfg, port, buf_size=size)
with bkg(listen_cmd, exit_wait=True, ns=netns) as ncdevmem:
wait_port_listen(port, proto="tcp", ns=netns)
cmd(f"dd if=/dev/zero bs={size} count=1 2>/dev/null | "
f"{socat}", host=cfg.remote, shell=True)
ksft_eq(ncdevmem.ret, 0, f"HDS failed for payload size {size}")
@@ -0,0 +1,297 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
ALL_TESTS="
same_speeds_autoneg_off
different_speeds_autoneg_off
combination_of_neg_on_and_off
advertise_subset_of_speeds
check_highest_speed_is_chosen
different_speeds_autoneg_on
"
NUM_NETIFS=2
lib_dir=$(dirname "$0")
source "$lib_dir"/../../../net/forwarding/lib.sh
source ethtool_lib.sh
h1_create()
{
simple_if_init $h1 192.0.2.1/24
}
h1_destroy()
{
simple_if_fini $h1 192.0.2.1/24
}
h2_create()
{
simple_if_init $h2 192.0.2.2/24
}
h2_destroy()
{
simple_if_fini $h2 192.0.2.2/24
}
setup_prepare()
{
h1=${NETIFS[p1]}
h2=${NETIFS[p2]}
h1_create
h2_create
}
cleanup()
{
pre_cleanup
h2_destroy
h1_destroy
}
same_speeds_autoneg_off()
{
# Check that when each of the reported speeds is forced, the links come
# up and are operational.
local -a speeds_arr=($(common_speeds_get $h1 $h2 0 0))
for speed in "${speeds_arr[@]}"; do
RET=0
ethtool_set $h1 speed $speed autoneg off
ethtool_set $h2 speed $speed autoneg off
setup_wait_dev_with_timeout $h1
setup_wait_dev_with_timeout $h2
ping_do $h1 192.0.2.2
check_err $? "ping with speed $speed autoneg off"
log_test "force speed $speed on both ends"
done
ethtool -s $h2 autoneg on
ethtool -s $h1 autoneg on
}
different_speeds_autoneg_off()
{
# Test that when we force different speeds, links are not up and ping
# fails.
RET=0
local -a speeds_arr=($(different_speeds_get $h1 $h2 0 0))
local speed1=${speeds_arr[0]}
local speed2=${speeds_arr[1]}
ethtool_set $h1 speed $speed1 autoneg off
ethtool_set $h2 speed $speed2 autoneg off
setup_wait_dev_with_timeout $h1
setup_wait_dev_with_timeout $h2
ping_do $h1 192.0.2.2
check_fail $? "ping with different speeds"
log_test "force of different speeds autoneg off"
ethtool -s $h2 autoneg on
ethtool -s $h1 autoneg on
}
combination_of_neg_on_and_off()
{
# Test that when one device is forced to a speed supported by both
# endpoints and the other device is configured to autoneg on, the links
# are up and ping passes.
local -a speeds_arr=($(common_speeds_get $h1 $h2 0 1))
for speed in "${speeds_arr[@]}"; do
RET=0
ethtool_set $h1 speed $speed autoneg off
setup_wait_dev_with_timeout $h1
setup_wait_dev_with_timeout $h2
ping_do $h1 192.0.2.2
check_err $? "ping with h1-speed=$speed autoneg off, h2 autoneg on"
log_test "force speed $speed vs. autoneg"
done
ethtool -s $h1 autoneg on
}
hex_speed_value_get()
{
local speed=$1; shift
local shift_size=${speed_values[$speed]}
speed=$((0x1 << $"shift_size"))
printf "%#x" "$speed"
}
subset_of_common_speeds_get()
{
local dev1=$1; shift
local dev2=$1; shift
local adver=$1; shift
local -a speeds_arr=($(common_speeds_get $dev1 $dev2 0 $adver))
local speed_to_advertise=0
local speed_to_remove=${speeds_arr[0]}
speed_to_remove+='base'
local -a speeds_mode_arr=($(common_speeds_get $dev1 $dev2 1 $adver))
for speed in ${speeds_mode_arr[@]}; do
if [[ $speed != $speed_to_remove* ]]; then
speed=$(hex_speed_value_get $speed)
speed_to_advertise=$(($speed_to_advertise | \
$speed))
fi
done
# Convert to hex.
printf "%#x" "$speed_to_advertise"
}
speed_to_advertise_get()
{
# The function returns the hex number that is composed by OR-ing all
# the modes corresponding to the provided speed.
local speed_without_mode=$1; shift
local supported_speeds=("$@"); shift
local speed_to_advertise=0
speed_without_mode+='base'
for speed in ${supported_speeds[@]}; do
if [[ $speed == $speed_without_mode* ]]; then
speed=$(hex_speed_value_get $speed)
speed_to_advertise=$(($speed_to_advertise | \
$speed))
fi
done
# Convert to hex.
printf "%#x" "$speed_to_advertise"
}
advertise_subset_of_speeds()
{
# Test that when one device advertises a subset of speeds and another
# advertises a specific speed (but all modes of this speed), the links
# are up and ping passes.
RET=0
local speed_1_to_advertise=$(subset_of_common_speeds_get $h1 $h2 1)
ethtool_set $h1 advertise $speed_1_to_advertise
if [ $RET != 0 ]; then
log_test "advertise subset of speeds"
return
fi
local -a speeds_arr_without_mode=($(common_speeds_get $h1 $h2 0 1))
# Check only speeds that h1 advertised. Remove the first speed.
unset speeds_arr_without_mode[0]
local -a speeds_arr_with_mode=($(common_speeds_get $h1 $h2 1 1))
for speed_value in ${speeds_arr_without_mode[@]}; do
RET=0
local speed_2_to_advertise=$(speed_to_advertise_get $speed_value \
"${speeds_arr_with_mode[@]}")
ethtool_set $h2 advertise $speed_2_to_advertise
setup_wait_dev_with_timeout $h1
setup_wait_dev_with_timeout $h2
ping_do $h1 192.0.2.2
check_err $? "ping with h1=$speed_1_to_advertise, h2=$speed_2_to_advertise ($speed_value)"
log_test "advertise $speed_1_to_advertise vs. $speed_2_to_advertise"
done
ethtool -s $h2 autoneg on
ethtool -s $h1 autoneg on
}
check_highest_speed_is_chosen()
{
# Test that when one device advertises a subset of speeds, the other
# chooses the highest speed. This test checks configuration without
# traffic.
RET=0
local max_speed
local chosen_speed
local speed_to_advertise=$(subset_of_common_speeds_get $h1 $h2 1)
ethtool_set $h1 advertise $speed_to_advertise
if [ $RET != 0 ]; then
log_test "check highest speed"
return
fi
local -a speeds_arr=($(common_speeds_get $h1 $h2 0 1))
max_speed=${speeds_arr[0]}
for current in ${speeds_arr[@]}; do
if [[ $current -gt $max_speed ]]; then
max_speed=$current
fi
done
setup_wait_dev_with_timeout $h1
setup_wait_dev_with_timeout $h2
chosen_speed=$(ethtool $h1 | grep 'Speed:')
chosen_speed=${chosen_speed%"Mb/s"*}
chosen_speed=${chosen_speed#*"Speed: "}
((chosen_speed == max_speed))
check_err $? "h1 advertise $speed_to_advertise, h2 sync to speed $chosen_speed"
log_test "check highest speed"
ethtool -s $h2 autoneg on
ethtool -s $h1 autoneg on
}
different_speeds_autoneg_on()
{
# Test that when we configure links to advertise different speeds,
# links are not up and ping fails.
RET=0
local -a speeds=($(different_speeds_get $h1 $h2 1 1))
local speed1=${speeds[0]}
local speed2=${speeds[1]}
speed1=$(hex_speed_value_get $speed1)
speed2=$(hex_speed_value_get $speed2)
ethtool_set $h1 advertise $speed1
ethtool_set $h2 advertise $speed2
if (($RET)); then
setup_wait_dev_with_timeout $h1
setup_wait_dev_with_timeout $h2
ping_do $h1 192.0.2.2
check_fail $? "ping with different speeds autoneg on"
fi
log_test "advertise different speeds autoneg on"
ethtool -s $h2 autoneg on
ethtool -s $h1 autoneg on
}
trap cleanup EXIT
setup_prepare
setup_wait
declare -gA speed_values
eval "speed_values=($(speeds_arr_get))"
tests_run
exit $EXIT_STATUS
@@ -0,0 +1,116 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
ALL_TESTS="
autoneg
autoneg_force_mode
no_cable
"
NUM_NETIFS=2
lib_dir=$(dirname "$0")
source "$lib_dir"/../../../net/forwarding/lib.sh
source ethtool_lib.sh
TIMEOUT=$((WAIT_TIMEOUT * 1000)) # ms
setup_prepare()
{
swp1=${NETIFS[p1]}
swp2=${NETIFS[p2]}
swp3=$NETIF_NO_CABLE
}
ethtool_ext_state()
{
local dev=$1; shift
local expected_ext_state=$1; shift
local expected_ext_substate=${1:-""}; shift
local ext_state=$(ethtool $dev | grep "Link detected" \
| cut -d "(" -f2 | cut -d ")" -f1)
local ext_substate=$(echo $ext_state | cut -sd "," -f2 \
| sed -e 's/^[[:space:]]*//')
ext_state=$(echo $ext_state | cut -d "," -f1)
if [[ $ext_state != $expected_ext_state ]]; then
echo "Expected \"$expected_ext_state\", got \"$ext_state\""
return 1
fi
if [[ $ext_substate != $expected_ext_substate ]]; then
echo "Expected \"$expected_ext_substate\", got \"$ext_substate\""
return 1
fi
}
autoneg()
{
local msg
RET=0
ip link set dev $swp1 up
msg=$(busywait $TIMEOUT ethtool_ext_state $swp1 \
"Autoneg" "No partner detected")
check_err $? "$msg"
log_test "Autoneg, No partner detected"
ip link set dev $swp1 down
}
autoneg_force_mode()
{
local msg
RET=0
ip link set dev $swp1 up
ip link set dev $swp2 up
local -a speeds_arr=($(different_speeds_get $swp1 $swp2 0 0))
local speed1=${speeds_arr[0]}
local speed2=${speeds_arr[1]}
ethtool_set $swp1 speed $speed1 autoneg off
ethtool_set $swp2 speed $speed2 autoneg off
msg=$(busywait $TIMEOUT ethtool_ext_state $swp1 \
"Autoneg" "No partner detected during force mode")
check_err $? "$msg"
msg=$(busywait $TIMEOUT ethtool_ext_state $swp2 \
"Autoneg" "No partner detected during force mode")
check_err $? "$msg"
log_test "Autoneg, No partner detected during force mode"
ethtool -s $swp2 autoneg on
ethtool -s $swp1 autoneg on
ip link set dev $swp2 down
ip link set dev $swp1 down
}
no_cable()
{
local msg
RET=0
ip link set dev $swp3 up
msg=$(busywait $TIMEOUT ethtool_ext_state $swp3 "No cable")
check_err $? "$msg"
log_test "No cable"
ip link set dev $swp3 down
}
setup_prepare
tests_run
exit $EXIT_STATUS
@@ -0,0 +1,120 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
speeds_arr_get()
{
cmd='/ETHTOOL_LINK_MODE_[^[:space:]]*_BIT[[:space:]]+=[[:space:]]+/ \
{sub(/,$/, "") \
sub(/ETHTOOL_LINK_MODE_/,"") \
sub(/_BIT/,"") \
sub(/_Full/,"/Full") \
sub(/_Half/,"/Half");\
print "["$1"]="$3}'
awk "${cmd}" /usr/include/linux/ethtool.h
}
ethtool_set()
{
local cmd="$@"
local out=$(ethtool -s $cmd 2>&1 | wc -l)
check_err $out "error in configuration. $cmd"
}
dev_linkmodes_params_get()
{
local dev=$1; shift
local adver=$1; shift
local -a linkmodes_params
local param_count
local arr
if (($adver)); then
mode="Advertised link modes"
else
mode="Supported link modes"
fi
local -a dev_linkmodes=($(dev_speeds_get $dev 1 $adver))
for ((i=0; i<${#dev_linkmodes[@]}; i++)); do
linkmodes_params[$i]=$(echo -e "${dev_linkmodes[$i]}" | \
# Replaces all non numbers with spaces
sed -e 's/[^0-9]/ /g' | \
# Squeeze spaces in sequence to 1 space
tr -s ' ')
# Count how many numbers were found in the linkmode
param_count=$(echo "${linkmodes_params[$i]}" | wc -w)
if [[ $param_count -eq 1 ]]; then
linkmodes_params[$i]="${linkmodes_params[$i]} 1"
elif [[ $param_count -ge 3 ]]; then
arr=(${linkmodes_params[$i]})
# Take only first two params
linkmodes_params[$i]=$(echo "${arr[@]:0:2}")
fi
done
echo ${linkmodes_params[@]}
}
dev_speeds_get()
{
local dev=$1; shift
local with_mode=$1; shift
local adver=$1; shift
local speeds_str
if (($adver)); then
mode="Advertised link modes"
else
mode="Supported link modes"
fi
speeds_str=$(ethtool "$dev" | \
# Snip everything before the link modes section.
sed -n '/'"$mode"':/,$p' | \
# Quit processing the rest at the start of the next section.
# When checking, skip the header of this section (hence the 2,).
sed -n '2,${/^[\t][^ \t]/q};p' | \
# Drop the section header of the current section.
cut -d':' -f2)
local -a speeds_arr=($speeds_str)
if [[ $with_mode -eq 0 ]]; then
for ((i=0; i<${#speeds_arr[@]}; i++)); do
speeds_arr[$i]=${speeds_arr[$i]%base*}
done
fi
echo ${speeds_arr[@]}
}
common_speeds_get()
{
dev1=$1; shift
dev2=$1; shift
with_mode=$1; shift
adver=$1; shift
local -a dev1_speeds=($(dev_speeds_get $dev1 $with_mode $adver))
local -a dev2_speeds=($(dev_speeds_get $dev2 $with_mode $adver))
comm -12 \
<(printf '%s\n' "${dev1_speeds[@]}" | sort -u) \
<(printf '%s\n' "${dev2_speeds[@]}" | sort -u)
}
different_speeds_get()
{
local dev1=$1; shift
local dev2=$1; shift
local with_mode=$1; shift
local adver=$1; shift
local -a speeds_arr
speeds_arr=($(common_speeds_get $dev1 $dev2 $with_mode $adver))
if [[ ${#speeds_arr[@]} < 2 ]]; then
check_err 1 "cannot check different speeds. There are not enough speeds"
fi
echo ${speeds_arr[0]} ${speeds_arr[1]}
}
@@ -0,0 +1,341 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
ALL_TESTS="
manual_with_verification_h1_to_h2
manual_with_verification_h2_to_h1
manual_without_verification_h1_to_h2
manual_without_verification_h2_to_h1
manual_failed_verification_h1_to_h2
manual_failed_verification_h2_to_h1
lldp
"
NUM_NETIFS=2
REQUIRE_MZ=no
PREEMPTIBLE_PRIO=0
lib_dir=$(dirname "$0")
source "$lib_dir"/../../../net/forwarding/lib.sh
traffic_test()
{
local if=$1; shift
local src=$1; shift
local num_pkts=10000
local before=
local after=
local delta=
if [ ${has_pmac_stats[$if]} = false ]; then
src="aggregate"
fi
before=$(ethtool_std_stats_get $if "eth-mac" "FramesTransmittedOK" $src)
$MZ $if -q -c $num_pkts -p 64 -b bcast -t ip -R $PREEMPTIBLE_PRIO
after=$(ethtool_std_stats_get $if "eth-mac" "FramesTransmittedOK" $src)
delta=$((after - before))
# Allow an extra 1% tolerance for random packets sent by the stack
[ $delta -ge $num_pkts ] && [ $delta -le $((num_pkts + 100)) ]
}
manual_with_verification()
{
local tx=$1; shift
local rx=$1; shift
RET=0
# It isn't completely clear from IEEE 802.3-2018 Figure 99-5: Transmit
# Processing state diagram whether the "send_r" variable (send response
# to verification frame) should be taken into consideration while the
# MAC Merge TX direction is disabled. That being said, at least the
# NXP ENETC does not, and requires tx-enabled on in order to respond to
# the link partner's verification frames.
ethtool --set-mm $rx tx-enabled on
ethtool --set-mm $tx verify-enabled on tx-enabled on
# Wait for verification to finish
sleep 1
ethtool --json --show-mm $tx | jq -r '.[]."verify-status"' | \
grep -q 'SUCCEEDED'
check_err "$?" "Verification did not succeed"
ethtool --json --show-mm $tx | jq -r '.[]."tx-active"' | grep -q 'true'
check_err "$?" "pMAC TX is not active"
traffic_test $tx "pmac"
check_err "$?" "Traffic did not get sent through $tx's pMAC"
ethtool --set-mm $tx verify-enabled off tx-enabled off
ethtool --set-mm $rx tx-enabled off
log_test "Manual configuration with verification: $tx to $rx"
}
manual_with_verification_h1_to_h2()
{
manual_with_verification $h1 $h2
}
manual_with_verification_h2_to_h1()
{
manual_with_verification $h2 $h1
}
manual_without_verification()
{
local tx=$1; shift
local rx=$1; shift
RET=0
ethtool --set-mm $tx verify-enabled off tx-enabled on
ethtool --json --show-mm $tx | jq -r '.[]."verify-status"' | \
grep -q 'DISABLED'
check_err "$?" "Verification is not disabled"
ethtool --json --show-mm $tx | jq -r '.[]."tx-active"' | grep -q 'true'
check_err "$?" "pMAC TX is not active"
traffic_test $tx "pmac"
check_err "$?" "Traffic did not get sent through $tx's pMAC"
ethtool --set-mm $tx verify-enabled off tx-enabled off
log_test "Manual configuration without verification: $tx to $rx"
}
manual_without_verification_h1_to_h2()
{
manual_without_verification $h1 $h2
}
manual_without_verification_h2_to_h1()
{
manual_without_verification $h2 $h1
}
manual_failed_verification()
{
local tx=$1; shift
local rx=$1; shift
RET=0
ethtool --set-mm $rx pmac-enabled off
ethtool --set-mm $tx verify-enabled on tx-enabled on
# Wait for verification to time out
sleep 1
ethtool --json --show-mm $tx | jq -r '.[]."verify-status"' | \
grep -q 'SUCCEEDED'
check_fail "$?" "Verification succeeded when it shouldn't have"
ethtool --json --show-mm $tx | jq -r '.[]."tx-active"' | grep -q 'true'
check_fail "$?" "pMAC TX is active when it shouldn't have"
traffic_test $tx "emac"
check_err "$?" "Traffic did not get sent through $tx's eMAC"
ethtool --set-mm $tx verify-enabled off tx-enabled off
ethtool --set-mm $rx pmac-enabled on
log_test "Manual configuration with failed verification: $tx to $rx"
}
manual_failed_verification_h1_to_h2()
{
manual_failed_verification $h1 $h2
}
manual_failed_verification_h2_to_h1()
{
manual_failed_verification $h2 $h1
}
smallest_supported_add_frag_size()
{
local iface=$1
local rx_min_frag_size=
rx_min_frag_size=$(ethtool --json --show-mm $iface | \
jq '.[]."rx-min-frag-size"')
if [ $rx_min_frag_size -le 60 ]; then
echo 0
elif [ $rx_min_frag_size -le 124 ]; then
echo 1
elif [ $rx_min_frag_size -le 188 ]; then
echo 2
elif [ $rx_min_frag_size -le 252 ]; then
echo 3
else
echo "$iface: RX min frag size $rx_min_frag_size cannot be advertised over LLDP"
exit 1
fi
}
expected_add_frag_size()
{
local iface=$1
local requested=$2
local min=$(smallest_supported_add_frag_size $iface)
[ $requested -le $min ] && echo $min || echo $requested
}
lldp_change_add_frag_size()
{
local add_frag_size=$1
local pattern=
lldptool -T -i $h1 -V addEthCaps addFragSize=$add_frag_size >/dev/null
# Wait for TLVs to be received
sleep 2
pattern=$(printf "Additional fragment size: %d" \
$(expected_add_frag_size $h1 $add_frag_size))
lldptool -i $h2 -t -n -V addEthCaps | grep -q "$pattern"
}
lldp()
{
RET=0
systemctl start lldpad
# Configure the interfaces to receive and transmit LLDPDUs
lldptool -L -i $h1 adminStatus=rxtx >/dev/null
lldptool -L -i $h2 adminStatus=rxtx >/dev/null
# Enable the transmission of Additional Ethernet Capabilities TLV
lldptool -T -i $h1 -V addEthCaps enableTx=yes >/dev/null
lldptool -T -i $h2 -V addEthCaps enableTx=yes >/dev/null
# Wait for TLVs to be received
sleep 2
lldptool -i $h1 -t -n -V addEthCaps | \
grep -q "Preemption capability active"
check_err "$?" "$h1 pMAC TX is not active"
lldptool -i $h2 -t -n -V addEthCaps | \
grep -q "Preemption capability active"
check_err "$?" "$h2 pMAC TX is not active"
lldp_change_add_frag_size 3
check_err "$?" "addFragSize 3"
lldp_change_add_frag_size 2
check_err "$?" "addFragSize 2"
lldp_change_add_frag_size 1
check_err "$?" "addFragSize 1"
lldp_change_add_frag_size 0
check_err "$?" "addFragSize 0"
traffic_test $h1 "pmac"
check_err "$?" "Traffic did not get sent through $h1's pMAC"
traffic_test $h2 "pmac"
check_err "$?" "Traffic did not get sent through $h2's pMAC"
systemctl stop lldpad
log_test "LLDP"
}
h1_create()
{
ip link set dev $h1 up
tc qdisc add dev $h1 root mqprio num_tc 4 map 0 1 2 3 \
queues 1@0 1@1 1@2 1@3 \
fp P E E E \
hw 1
ethtool --set-mm $h1 pmac-enabled on tx-enabled off verify-enabled off
}
h2_create()
{
ip link set dev $h2 up
ethtool --set-mm $h2 pmac-enabled on tx-enabled off verify-enabled off
tc qdisc add dev $h2 root mqprio num_tc 4 map 0 1 2 3 \
queues 1@0 1@1 1@2 1@3 \
fp P E E E \
hw 1
}
h1_destroy()
{
ethtool --set-mm $h1 pmac-enabled off tx-enabled off verify-enabled off
tc qdisc del dev $h1 root
ip link set dev $h1 down
}
h2_destroy()
{
tc qdisc del dev $h2 root
ethtool --set-mm $h2 pmac-enabled off tx-enabled off verify-enabled off
ip link set dev $h2 down
}
setup_prepare()
{
h1=${NETIFS[p1]}
h2=${NETIFS[p2]}
h1_create
h2_create
}
cleanup()
{
pre_cleanup
h2_destroy
h1_destroy
}
check_ethtool_mm_support
check_tc_fp_support
require_command lldptool
bail_on_lldpad "autoconfigure the MAC Merge layer" "configure it manually"
for netif in ${NETIFS[@]}; do
ethtool --show-mm $netif 2>&1 &> /dev/null
if [[ $? -ne 0 ]]; then
echo "SKIP: $netif does not support MAC Merge"
exit $ksft_skip
fi
if check_ethtool_pmac_std_stats_support $netif eth-mac; then
has_pmac_stats[$netif]=true
else
has_pmac_stats[$netif]=false
echo "$netif does not report pMAC statistics, falling back to aggregate"
fi
done
trap cleanup EXIT
setup_prepare
setup_wait
tests_run
exit $EXIT_STATUS
@@ -0,0 +1,159 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#shellcheck disable=SC2034 # SC does not see the global variables
#shellcheck disable=SC2317,SC2329 # unused functions
ALL_TESTS="
rmon_rx_histogram
rmon_tx_histogram
"
: "${DRIVER_TEST_CONFORMANT:=yes}"
NUM_NETIFS=2
lib_dir=$(dirname "$0")
source "$lib_dir"/../../../net/forwarding/lib.sh
source "$lib_dir"/../../../kselftest/ktap_helpers.sh
UINT32_MAX=$((2**32 - 1))
ETH_FCS_LEN=4
ETH_HLEN=$((6+6+2))
TEST_NAME=$(basename "$0" .sh)
declare -A netif_mtu
ensure_mtu()
{
local iface=$1; shift
local len=$1; shift
local required=$((len - ETH_HLEN - ETH_FCS_LEN))
local current
current=$(run_on "$iface" \
ip -j link show dev "$iface" | jq -r '.[0].mtu')
if [ "$current" -lt "$required" ]; then
run_on "$iface" ip link set dev "$iface" mtu "$required" \
|| return 1
fi
}
bucket_test()
{
local iface=$1; shift
local neigh=$1; shift
local set=$1; shift
local bucket=$1; shift
local len=$1; shift
local num_rx=10000
local num_tx=20000
local expected=
local before=
local after=
local delta=
# Mausezahn does not include FCS bytes in its length - but the
# histogram counters do
len=$((len - ETH_FCS_LEN))
len=$((len > 0 ? len : 0))
before=$(run_on "$iface" ethtool --json -S "$iface" --groups rmon | \
jq -r ".[0].rmon[\"${set}-pktsNtoM\"][$bucket].val")
# Send 10k one way and 20k in the other, to detect counters
# mapped to the wrong direction
run_on "$neigh" \
"$MZ" "$neigh" -q -c "$num_rx" -p "$len" -a own -b bcast -d 10us
run_on "$iface" \
"$MZ" "$iface" -q -c "$num_tx" -p "$len" -a own -b bcast -d 10us
after=$(run_on "$iface" ethtool --json -S "$iface" --groups rmon | \
jq -r ".[0].rmon[\"${set}-pktsNtoM\"][$bucket].val")
delta=$((after - before))
expected=$([ "$set" = rx ] && echo "$num_rx" || echo "$num_tx")
[ "$delta" -ge "$expected" ] && [ "$delta" -le "$UINT32_MAX" ]
}
rmon_histogram()
{
local iface=$1; shift
local neigh=$1; shift
local set=$1; shift
local nbuckets=0
local step=
while read -r -a bucket; do
step="$set-pkts${bucket[0]}to${bucket[1]}"
for if in "$iface" "$neigh"; do
if ! ensure_mtu "$if" "${bucket[0]}"; then
ktap_print_msg "$if does not support the required MTU for $step"
ktap_test_xfail "$TEST_NAME.$step"
return
fi
done
if ! bucket_test "$iface" "$neigh" "$set" "$nbuckets" "${bucket[0]}"; then
ktap_test_fail "$TEST_NAME.$step"
return 1
fi
ktap_test_pass "$TEST_NAME.$step"
nbuckets=$((nbuckets + 1))
done < <(run_on "$iface" ethtool --json -S "$iface" --groups rmon | \
jq -r ".[0].rmon[\"${set}-pktsNtoM\"][]|[.low, .high]|@tsv" 2>/dev/null)
if [ "$nbuckets" -eq 0 ]; then
ktap_print_msg "$iface does not support $set histogram counters"
return
fi
}
rmon_rx_histogram()
{
rmon_histogram "$h1" "$h2" rx
}
rmon_tx_histogram()
{
rmon_histogram "$h1" "$h2" tx
}
setup_prepare()
{
h1=${NETIFS[p1]}
h2=${NETIFS[p2]}
for iface in "$h1" "$h2"; do
netif_mtu["$iface"]=$(run_on "$iface" \
ip -j link show dev "$iface" | jq -r '.[0].mtu')
done
}
cleanup()
{
pre_cleanup
# Do not bring down the interfaces, just configure the initial MTU
for iface in "$h2" "$h1"; do
run_on "$iface" ip link set dev "$iface" \
mtu "${netif_mtu[$iface]}"
done
}
check_ethtool_counter_group_support
trap cleanup EXIT
bucket_count=$(ethtool --json -S "${NETIFS[p1]}" --groups rmon | \
jq -r '.[0].rmon |
"\((."rx-pktsNtoM" | length) +
(."tx-pktsNtoM" | length))"')
ktap_print_header
ktap_set_plan "$bucket_count"
setup_prepare
setup_wait
tests_run
ktap_finished
@@ -0,0 +1,206 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#shellcheck disable=SC2034 # SC does not see the global variables
#shellcheck disable=SC2317,SC2329 # unused functions
ALL_TESTS="
test_eth_ctrl_stats
test_eth_mac_stats
test_pause_stats
"
: "${DRIVER_TEST_CONFORMANT:=yes}"
STABLE_MAC_ADDRS=yes
NUM_NETIFS=2
lib_dir=$(dirname "$0")
# shellcheck source=./../../../net/forwarding/lib.sh
source "$lib_dir"/../../../net/forwarding/lib.sh
# shellcheck source=./../../../kselftest/ktap_helpers.sh
source "$lib_dir"/../../../kselftest/ktap_helpers.sh
UINT32_MAX=$((2**32 - 1))
SUBTESTS=0
TEST_NAME=$(basename "$0" .sh)
traffic_test()
{
local iface=$1; shift
local neigh=$1; shift
local num_tx=$1; shift
local pkt_format="$1"; shift
local -a counters=("$@")
local int grp cnt target exact_check
local before after delta
local num_rx=$((num_tx * 2))
local xfail_message
local src="aggregate"
local i
for i in "${!counters[@]}"; do
read -r int grp cnt target exact_check xfail_message \
<<< "${counters[$i]}"
before[i]=$(ethtool_std_stats_get "$int" "$grp" "$cnt" "$src")
done
# shellcheck disable=SC2086 # needs split options
run_on "$iface" "$MZ" "$iface" -q -c "$num_tx" $pkt_format
# shellcheck disable=SC2086 # needs split options
run_on "$neigh" "$MZ" "$neigh" -q -c "$num_rx" $pkt_format
for i in "${!counters[@]}"; do
read -r int grp cnt target exact_check xfail_message \
<<< "${counters[$i]}"
after[i]=$(ethtool_std_stats_get "$int" "$grp" "$cnt" "$src")
if [[ "${after[$i]}" == "null" ]]; then
ktap_test_skip "$TEST_NAME.$grp-$cnt"
continue;
fi
delta=$((after[i] - before[i]))
if [ "$exact_check" -ne 0 ]; then
[ "$delta" -eq "$target" ]
else
[ "$delta" -ge "$target" ] && \
[ "$delta" -le "$UINT32_MAX" ]
fi
err="$?"
if [[ $err != 0 ]] && [[ -n $xfail_message ]]; then
ktap_print_msg "$xfail_message"
ktap_test_xfail "$TEST_NAME.$grp-$cnt"
continue;
fi
if [[ $err != 0 ]]; then
ktap_print_msg "$grp-$cnt is not valid on $int (expected $target, got $delta)"
ktap_test_fail "$TEST_NAME.$grp-$cnt"
else
ktap_test_pass "$TEST_NAME.$grp-$cnt"
fi
done
}
test_eth_ctrl_stats()
{
local pkt_format="-a own -b bcast 88:08 -p 64"
local num_pkts=1000
local -a counters
counters=("$h1 eth-ctrl MACControlFramesTransmitted $num_pkts 0")
traffic_test "$h1" "$h2" "$num_pkts" "$pkt_format" \
"${counters[@]}"
counters=("$h1 eth-ctrl MACControlFramesReceived $num_pkts 0")
traffic_test "$h2" "$h1" "$num_pkts" "$pkt_format" \
"${counters[@]}"
}
SUBTESTS=$((SUBTESTS + 2))
test_eth_mac_stats()
{
local pkt_size=100
local pkt_size_fcs=$((pkt_size + 4))
local bcast_pkt_format="-a own -b bcast -p $pkt_size"
local mcast_pkt_format="-a own -b 01:00:5E:00:00:01 -p $pkt_size"
local num_pkts=2000
local octets=$((pkt_size_fcs * num_pkts))
local -a counters error_cnt collision_cnt
# Error counters should be exactly zero
counters=("$h1 eth-mac FrameCheckSequenceErrors 0 1"
"$h1 eth-mac AlignmentErrors 0 1"
"$h1 eth-mac FramesLostDueToIntMACXmitError 0 1"
"$h1 eth-mac CarrierSenseErrors 0 1"
"$h1 eth-mac FramesLostDueToIntMACRcvError 0 1"
"$h1 eth-mac InRangeLengthErrors 0 1"
"$h1 eth-mac OutOfRangeLengthField 0 1"
"$h1 eth-mac FrameTooLongErrors 0 1"
"$h1 eth-mac FramesAbortedDueToXSColls 0 1")
traffic_test "$h1" "$h2" "$num_pkts" "$bcast_pkt_format" \
"${counters[@]}"
# Collision related counters should also be zero
counters=("$h1 eth-mac SingleCollisionFrames 0 1"
"$h1 eth-mac MultipleCollisionFrames 0 1"
"$h1 eth-mac FramesWithDeferredXmissions 0 1"
"$h1 eth-mac LateCollisions 0 1"
"$h1 eth-mac FramesWithExcessiveDeferral 0 1")
traffic_test "$h1" "$h2" "$num_pkts" "$bcast_pkt_format" \
"${counters[@]}"
counters=("$h1 eth-mac BroadcastFramesXmittedOK $num_pkts 0"
"$h1 eth-mac OctetsTransmittedOK $octets 0")
traffic_test "$h1" "$h2" "$num_pkts" "$bcast_pkt_format" \
"${counters[@]}"
counters=("$h1 eth-mac BroadcastFramesReceivedOK $num_pkts 0"
"$h1 eth-mac OctetsReceivedOK $octets 0")
traffic_test "$h2" "$h1" "$num_pkts" "$bcast_pkt_format" \
"${counters[@]}"
counters=("$h1 eth-mac FramesTransmittedOK $num_pkts 0"
"$h1 eth-mac MulticastFramesXmittedOK $num_pkts 0")
traffic_test "$h1" "$h2" "$num_pkts" "$mcast_pkt_format" \
"${counters[@]}"
counters=("$h1 eth-mac FramesReceivedOK $num_pkts 0"
"$h1 eth-mac MulticastFramesReceivedOK $num_pkts 0")
traffic_test "$h2" "$h1" "$num_pkts" "$mcast_pkt_format" \
"${counters[@]}"
}
SUBTESTS=$((SUBTESTS + 22))
test_pause_stats()
{
local pkt_format="-a own -b 01:80:c2:00:00:01 88:08:00:01:00:01"
local xfail_message="software sent pause frames not detected"
local num_pkts=2000
local -a counters
local int
local i
# Check that there is pause frame support
for ((i = 1; i <= NUM_NETIFS; ++i)); do
int="${NETIFS[p$i]}"
if ! run_on "$int" ethtool -I --json -a "$int" > /dev/null 2>&1; then
ktap_test_skip "$TEST_NAME.tx_pause_frames"
ktap_test_skip "$TEST_NAME.rx_pause_frames"
return
fi
done
counters=("$h1 pause tx_pause_frames $num_pkts 0 $xfail_message")
traffic_test "$h1" "$h2" "$num_pkts" "$pkt_format" \
"${counters[@]}"
counters=("$h1 pause rx_pause_frames $num_pkts 0")
traffic_test "$h2" "$h1" "$num_pkts" "$pkt_format" \
"${counters[@]}"
}
SUBTESTS=$((SUBTESTS + 2))
setup_prepare()
{
local iface
h1=${NETIFS[p1]}
h2=${NETIFS[p2]}
h2_mac=$(mac_get "$h2")
}
ktap_print_header
ktap_set_plan $SUBTESTS
check_ethtool_counter_group_support
trap cleanup EXIT
setup_prepare
setup_wait
tests_run
ktap_finished
@@ -0,0 +1,304 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""
HW GRO tests focusing on device machinery like stats, rather than protocol
processing.
"""
import glob
import re
from lib.py import ksft_run, ksft_exit, ksft_pr
from lib.py import ksft_eq, ksft_ge, ksft_variants
from lib.py import NetDrvEpEnv, NetdevFamily
from lib.py import KsftSkipEx
from lib.py import bkg, cmd, defer, ethtool, ip
# gro.c uses hardcoded DPORT=8000
GRO_DPORT = 8000
def _get_queue_stats(cfg, queue_id):
"""Get stats for a specific Rx queue."""
cfg.wait_hw_stats_settle()
data = cfg.netnl.qstats_get({"ifindex": cfg.ifindex, "scope": ["queue"]},
dump=True)
for q in data:
if q.get('queue-type') == 'rx' and q.get('queue-id') == queue_id:
return q
return {}
def _resolve_dmac(cfg, ipver):
"""Find the destination MAC address for sending packets."""
attr = "dmac" + ipver
if hasattr(cfg, attr):
return getattr(cfg, attr)
route = ip(f"-{ipver} route get {cfg.addr_v[ipver]}",
json=True, host=cfg.remote)[0]
gw = route.get("gateway")
if not gw:
setattr(cfg, attr, cfg.dev['address'])
return getattr(cfg, attr)
cmd(f"ping -c1 -W0 -I{cfg.remote_ifname} {gw}", host=cfg.remote)
neigh = ip(f"neigh get {gw} dev {cfg.remote_ifname}",
json=True, host=cfg.remote)[0]
setattr(cfg, attr, neigh['lladdr'])
return getattr(cfg, attr)
def _require_ntuple(cfg):
features = ethtool(f"-k {cfg.ifname}", json=True)[0]
if not features["ntuple-filters"]["active"]:
if features["ntuple-filters"]["fixed"]:
raise KsftSkipEx("Device does not support ntuple-filters")
ethtool(f"-K {cfg.ifname} ntuple-filters on")
defer(ethtool, f"-K {cfg.ifname} ntuple-filters off")
def _setup_isolated_queue(cfg):
"""Set up an isolated queue for testing using ntuple filter.
Remove queue 1 from the default RSS context and steer test traffic to it.
"""
_require_ntuple(cfg)
test_queue = 1
qcnt = len(glob.glob(f"/sys/class/net/{cfg.ifname}/queues/rx-*"))
if qcnt < 2:
raise KsftSkipEx(f"Need at least 2 queues, have {qcnt}")
# Remove queue 1 from default RSS context by setting its weight to 0
weights = ["1"] * qcnt
weights[test_queue] = "0"
ethtool(f"-X {cfg.ifname} weight " + " ".join(weights))
defer(ethtool, f"-X {cfg.ifname} default")
# Set up ntuple filter to steer our test traffic to the isolated queue
flow = f"flow-type tcp{cfg.addr_ipver} "
flow += f"dst-ip {cfg.addr} dst-port {GRO_DPORT} action {test_queue}"
output = ethtool(f"-N {cfg.ifname} {flow}").stdout
ntuple_id = int(output.split()[-1])
defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
return test_queue
def _run_gro_test(cfg, test_name, num_flows=None, ignore_fail=False,
order_check=False):
"""Run gro binary with given test and return output."""
if not hasattr(cfg, "bin_remote"):
cfg.bin_local = cfg.net_lib_dir / "gro"
cfg.bin_remote = cfg.remote.deploy(cfg.bin_local)
ipver = cfg.addr_ipver
protocol = f"--ipv{ipver}"
dmac = _resolve_dmac(cfg, ipver)
base_args = [
protocol,
f"--dmac {dmac}",
f"--smac {cfg.remote_dev['address']}",
f"--daddr {cfg.addr}",
f"--saddr {cfg.remote_addr_v[ipver]}",
f"--test {test_name}",
]
if num_flows:
base_args.append(f"--num-flows {num_flows}")
if order_check:
base_args.append("--order-check")
args = " ".join(base_args)
rx_cmd = f"{cfg.bin_local} {args} --rx --iface {cfg.ifname}"
tx_cmd = f"{cfg.bin_remote} {args} --iface {cfg.remote_ifname}"
with bkg(rx_cmd, ksft_ready=True, exit_wait=True, fail=False) as rx_proc:
cmd(tx_cmd, host=cfg.remote)
if not ignore_fail:
ksft_eq(rx_proc.ret, 0)
if rx_proc.ret != 0:
ksft_pr(rx_proc)
return rx_proc.stdout
def _require_hw_gro_stats(cfg, queue_id):
"""Check if device reports HW GRO stats for the queue."""
stats = _get_queue_stats(cfg, queue_id)
required = ['rx-packets', 'rx-hw-gro-packets', 'rx-hw-gro-wire-packets']
for stat in required:
if stat not in stats:
raise KsftSkipEx(f"Driver does not report '{stat}' via qstats")
def _set_ethtool_feat(cfg, current, feats):
"""Set ethtool features with defer to restore original state."""
s2n = {True: "on", False: "off"}
new = ["-K", cfg.ifname]
old = ["-K", cfg.ifname]
no_change = True
for name, state in feats.items():
new += [name, s2n[state]]
old += [name, s2n[current[name]["active"]]]
if current[name]["active"] != state:
no_change = False
if current[name]["fixed"]:
raise KsftSkipEx(f"Device does not support {name}")
if no_change:
return
eth_cmd = ethtool(" ".join(new))
defer(ethtool, " ".join(old))
# If ethtool printed something kernel must have modified some features
if eth_cmd.stdout:
ksft_pr(eth_cmd)
def _setup_hw_gro(cfg):
"""Enable HW GRO on the device, disabling SW GRO."""
feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
# Try to disable SW GRO and enable HW GRO
_set_ethtool_feat(cfg, feat,
{"generic-receive-offload": False,
"rx-gro-hw": True,
"large-receive-offload": False})
# Some NICs treat HW GRO as a GRO sub-feature so disabling GRO
# will also clear HW GRO. Use a hack of installing XDP generic
# to skip SW GRO, even when enabled.
feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
if not feat["rx-gro-hw"]["active"]:
ksft_pr("Driver clears HW GRO when SW GRO is cleared, using generic XDP workaround")
prog = cfg.net_lib_dir / "xdp_dummy.bpf.o"
ip(f"link set dev {cfg.ifname} xdpgeneric obj {prog} sec xdp")
defer(ip, f"link set dev {cfg.ifname} xdpgeneric off")
# Attaching XDP may change features, fetch the latest state
feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
_set_ethtool_feat(cfg, feat,
{"generic-receive-offload": True,
"rx-gro-hw": True,
"large-receive-offload": False})
def _check_gro_stats(cfg, test_queue, stats_before,
expect_rx, expect_gro, expect_wire):
"""Validate GRO stats against expected values."""
stats_after = _get_queue_stats(cfg, test_queue)
rx_delta = (stats_after.get('rx-packets', 0) -
stats_before.get('rx-packets', 0))
gro_delta = (stats_after.get('rx-hw-gro-packets', 0) -
stats_before.get('rx-hw-gro-packets', 0))
wire_delta = (stats_after.get('rx-hw-gro-wire-packets', 0) -
stats_before.get('rx-hw-gro-wire-packets', 0))
ksft_eq(rx_delta, expect_rx, comment="rx-packets")
ksft_eq(gro_delta, expect_gro, comment="rx-hw-gro-packets")
ksft_eq(wire_delta, expect_wire, comment="rx-hw-gro-wire-packets")
def test_gro_stats_single(cfg):
"""
Test that a single packet doesn't affect GRO stats.
Send a single packet that cannot be coalesced (nothing to coalesce with).
GRO stats should not increase since no coalescing occurred.
rx-packets should increase by 2 (1 data + 1 FIN).
"""
_setup_hw_gro(cfg)
test_queue = _setup_isolated_queue(cfg)
_require_hw_gro_stats(cfg, test_queue)
stats_before = _get_queue_stats(cfg, test_queue)
_run_gro_test(cfg, "single")
# 1 data + 1 FIN = 2 rx-packets, no coalescing
_check_gro_stats(cfg, test_queue, stats_before,
expect_rx=2, expect_gro=0, expect_wire=0)
def test_gro_stats_full(cfg):
"""
Test GRO stats when overwhelming HW GRO capacity.
Send 500 flows to exceed HW GRO flow capacity on a single queue.
This should result in some packets not being coalesced.
Validate that qstats match what gro.c observed.
"""
_setup_hw_gro(cfg)
test_queue = _setup_isolated_queue(cfg)
_require_hw_gro_stats(cfg, test_queue)
num_flows = 500
stats_before = _get_queue_stats(cfg, test_queue)
# Run capacity test - will likely fail because not all packets coalesce
output = _run_gro_test(cfg, "capacity", num_flows=num_flows,
ignore_fail=True)
# Parse gro.c output: "STATS: received=X wire=Y coalesced=Z"
match = re.search(r'STATS: received=(\d+) wire=(\d+) coalesced=(\d+)',
output)
if not match:
raise KsftSkipEx(f"Could not parse gro.c output: {output}")
rx_frames = int(match.group(2))
gro_coalesced = int(match.group(3))
ksft_ge(gro_coalesced, 1,
comment="At least some packets should coalesce")
# received + 1 FIN, coalesced super-packets, coalesced * 2 wire packets
_check_gro_stats(cfg, test_queue, stats_before,
expect_rx=rx_frames + 1,
expect_gro=gro_coalesced,
expect_wire=gro_coalesced * 2)
@ksft_variants([4, 32, 512])
def test_gro_order(cfg, num_flows):
"""
Test that HW GRO preserves packet ordering between flows.
Packets may get delayed until the aggregate is released,
but reordering between aggregates and packet terminating
the aggregate and normal packets should not happen.
Note that this test is stricter than truly required.
Reordering packets between flows should not cause issues.
This test will also fail if traffic is run over an ECMP fabric.
"""
_setup_hw_gro(cfg)
_setup_isolated_queue(cfg)
_run_gro_test(cfg, "capacity", num_flows=num_flows, order_check=True)
def main() -> None:
""" Ksft boiler plate main """
with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
cfg.netnl = NetdevFamily()
ksft_run([test_gro_stats_single,
test_gro_stats_full,
test_gro_order], args=(cfg,))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,334 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
# +--------------------+ +----------------------+
# | H1 | | H2 |
# | | | |
# | $h1.200 + | | + $h2.200 |
# | 192.0.2.1/28 | | | | 192.0.2.18/28 |
# | 2001:db8:1::1/64 | | | | 2001:db8:2::1/64 |
# | | | | | |
# | $h1 + | | + $h2 |
# | | | | | |
# +------------------|-+ +-|--------------------+
# | |
# +------------------|-------------------------|--------------------+
# | SW | | |
# | | | |
# | $rp1 + + $rp2 |
# | | | |
# | $rp1.200 + + $rp2.200 |
# | 192.0.2.2/28 192.0.2.17/28 |
# | 2001:db8:1::2/64 2001:db8:2::2/64 |
# | |
# +-----------------------------------------------------------------+
ALL_TESTS="
ping_ipv4
ping_ipv6
test_stats_rx_ipv4
test_stats_tx_ipv4
test_stats_rx_ipv6
test_stats_tx_ipv6
respin_enablement
test_stats_rx_ipv4
test_stats_tx_ipv4
test_stats_rx_ipv6
test_stats_tx_ipv6
reapply_config
ping_ipv4
ping_ipv6
test_stats_rx_ipv4
test_stats_tx_ipv4
test_stats_rx_ipv6
test_stats_tx_ipv6
test_stats_report_rx
test_stats_report_tx
test_destroy_enabled
test_double_enable
"
NUM_NETIFS=4
lib_dir=$(dirname "$0")
source "$lib_dir"/../../../net/forwarding/lib.sh
source "$lib_dir"/../../../net/forwarding/tc_common.sh
h1_create()
{
simple_if_init $h1
vlan_create $h1 200 v$h1 192.0.2.1/28 2001:db8:1::1/64
ip route add 192.0.2.16/28 vrf v$h1 nexthop via 192.0.2.2
ip -6 route add 2001:db8:2::/64 vrf v$h1 nexthop via 2001:db8:1::2
}
h1_destroy()
{
ip -6 route del 2001:db8:2::/64 vrf v$h1 nexthop via 2001:db8:1::2
ip route del 192.0.2.16/28 vrf v$h1 nexthop via 192.0.2.2
vlan_destroy $h1 200
simple_if_fini $h1
}
h2_create()
{
simple_if_init $h2
vlan_create $h2 200 v$h2 192.0.2.18/28 2001:db8:2::1/64
ip route add 192.0.2.0/28 vrf v$h2 nexthop via 192.0.2.17
ip -6 route add 2001:db8:1::/64 vrf v$h2 nexthop via 2001:db8:2::2
}
h2_destroy()
{
ip -6 route del 2001:db8:1::/64 vrf v$h2 nexthop via 2001:db8:2::2
ip route del 192.0.2.0/28 vrf v$h2 nexthop via 192.0.2.17
vlan_destroy $h2 200
simple_if_fini $h2
}
router_rp1_200_create()
{
ip link add name $rp1.200 link $rp1 type vlan id 200
ip link set dev $rp1.200 addrgenmode eui64
ip link set dev $rp1.200 up
ip address add dev $rp1.200 192.0.2.2/28
ip address add dev $rp1.200 2001:db8:1::2/64
ip stats set dev $rp1.200 l3_stats on
}
router_rp1_200_destroy()
{
ip stats set dev $rp1.200 l3_stats off
ip address del dev $rp1.200 2001:db8:1::2/64
ip address del dev $rp1.200 192.0.2.2/28
ip link del dev $rp1.200
}
router_create()
{
ip link set dev $rp1 up
router_rp1_200_create
ip link set dev $rp2 up
vlan_create $rp2 200 "" 192.0.2.17/28 2001:db8:2::2/64
}
router_destroy()
{
vlan_destroy $rp2 200
ip link set dev $rp2 down
router_rp1_200_destroy
ip link set dev $rp1 down
}
setup_prepare()
{
h1=${NETIFS[p1]}
rp1=${NETIFS[p2]}
rp2=${NETIFS[p3]}
h2=${NETIFS[p4]}
rp1mac=$(mac_get $rp1)
rp2mac=$(mac_get $rp2)
vrf_prepare
h1_create
h2_create
router_create
forwarding_enable
}
cleanup()
{
pre_cleanup
forwarding_restore
router_destroy
h2_destroy
h1_destroy
vrf_cleanup
}
ping_ipv4()
{
ping_test $h1.200 192.0.2.18 " IPv4"
}
ping_ipv6()
{
ping_test $h1.200 2001:db8:2::1 " IPv6"
}
send_packets_rx_ipv4()
{
# Send 21 packets instead of 20, because the first one might trap and go
# through the SW datapath, which might not bump the HW counter.
$MZ $h1.200 -c 21 -d 20msec -p 100 \
-a own -b $rp1mac -A 192.0.2.1 -B 192.0.2.18 \
-q -t udp sp=54321,dp=12345
}
send_packets_rx_ipv6()
{
$MZ $h1.200 -6 -c 21 -d 20msec -p 100 \
-a own -b $rp1mac -A 2001:db8:1::1 -B 2001:db8:2::1 \
-q -t udp sp=54321,dp=12345
}
send_packets_tx_ipv4()
{
$MZ $h2.200 -c 21 -d 20msec -p 100 \
-a own -b $rp2mac -A 192.0.2.18 -B 192.0.2.1 \
-q -t udp sp=54321,dp=12345
}
send_packets_tx_ipv6()
{
$MZ $h2.200 -6 -c 21 -d 20msec -p 100 \
-a own -b $rp2mac -A 2001:db8:2::1 -B 2001:db8:1::1 \
-q -t udp sp=54321,dp=12345
}
___test_stats()
{
local dir=$1; shift
local prot=$1; shift
local a
local b
a=$(hw_stats_get l3_stats $rp1.200 ${dir} packets)
send_packets_${dir}_${prot}
"$@"
b=$(busywait "$TC_HIT_TIMEOUT" until_counter_is ">= $a + 20" \
hw_stats_get l3_stats $rp1.200 ${dir} packets)
check_err $? "Traffic not reflected in the counter: $a -> $b"
}
__test_stats()
{
local dir=$1; shift
local prot=$1; shift
RET=0
___test_stats "$dir" "$prot"
log_test "Test $dir packets: $prot"
}
test_stats_rx_ipv4()
{
__test_stats rx ipv4
}
test_stats_tx_ipv4()
{
__test_stats tx ipv4
}
test_stats_rx_ipv6()
{
__test_stats rx ipv6
}
test_stats_tx_ipv6()
{
__test_stats tx ipv6
}
# Make sure everything works well even after stats have been disabled and
# reenabled on the same device without touching the L3 configuration.
respin_enablement()
{
log_info "Turning stats off and on again"
ip stats set dev $rp1.200 l3_stats off
ip stats set dev $rp1.200 l3_stats on
}
# For the initial run, l3_stats is enabled on a completely set up netdevice. Now
# do it the other way around: enabling the L3 stats on an L2 netdevice, and only
# then apply the L3 configuration.
reapply_config()
{
log_info "Reapplying configuration"
router_rp1_200_destroy
ip link add name $rp1.200 link $rp1 type vlan id 200
ip link set dev $rp1.200 addrgenmode none
ip stats set dev $rp1.200 l3_stats on
ip link set dev $rp1.200 addrgenmode eui64
ip link set dev $rp1.200 up
ip address add dev $rp1.200 192.0.2.2/28
ip address add dev $rp1.200 2001:db8:1::2/64
}
__test_stats_report()
{
local dir=$1; shift
local prot=$1; shift
local a
local b
RET=0
a=$(hw_stats_get l3_stats $rp1.200 ${dir} packets)
send_packets_${dir}_${prot}
ip address flush dev $rp1.200
b=$(busywait "$TC_HIT_TIMEOUT" until_counter_is ">= $a + 20" \
hw_stats_get l3_stats $rp1.200 ${dir} packets)
check_err $? "Traffic not reflected in the counter: $a -> $b"
log_test "Test ${dir} packets: stats pushed on loss of L3"
ip stats set dev $rp1.200 l3_stats off
ip link del dev $rp1.200
router_rp1_200_create
}
test_stats_report_rx()
{
__test_stats_report rx ipv4
}
test_stats_report_tx()
{
__test_stats_report tx ipv4
}
test_destroy_enabled()
{
RET=0
ip link del dev $rp1.200
router_rp1_200_create
log_test "Destroy l3_stats-enabled netdev"
}
test_double_enable()
{
RET=0
___test_stats rx ipv4 \
ip stats set dev $rp1.200 l3_stats on
log_test "Test stat retention across a spurious enablement"
}
trap cleanup EXIT
setup_prepare
setup_wait
used=$(ip -j stats show dev $rp1.200 group offload subgroup hw_stats_info |
jq '.[].info.l3_stats.used')
[[ $used = true ]]
check_err $? "hw_stats_info.used=$used"
log_test "l3_stats offloaded"
tests_run
exit $EXIT_STATUS
@@ -0,0 +1,111 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
# Test L3 stats on IP-in-IP GRE tunnel without key.
# This test uses flat topology for IP tunneling tests. See ipip_lib.sh for more
# details.
ALL_TESTS="
ping_ipv4
test_stats_rx
test_stats_tx
"
NUM_NETIFS=6
lib_dir=$(dirname "$0")
source "$lib_dir"/../../../net/forwarding/lib.sh
source "$lib_dir"/../../../net/forwarding/ipip_lib.sh
source "$lib_dir"/../../../net/forwarding/tc_common.sh
setup_prepare()
{
h1=${NETIFS[p1]}
ol1=${NETIFS[p2]}
ul1=${NETIFS[p3]}
ul2=${NETIFS[p4]}
ol2=${NETIFS[p5]}
h2=${NETIFS[p6]}
ol1mac=$(mac_get $ol1)
forwarding_enable
vrf_prepare
h1_create
h2_create
sw1_flat_create gre $ol1 $ul1
sw2_flat_create gre $ol2 $ul2
ip stats set dev g1a l3_stats on
ip stats set dev g2a l3_stats on
}
cleanup()
{
pre_cleanup
ip stats set dev g1a l3_stats off
ip stats set dev g2a l3_stats off
sw2_flat_destroy $ol2 $ul2
sw1_flat_destroy $ol1 $ul1
h2_destroy
h1_destroy
vrf_cleanup
forwarding_restore
}
ping_ipv4()
{
RET=0
ping_test $h1 192.0.2.18 " gre flat"
}
send_packets_ipv4()
{
# Send 21 packets instead of 20, because the first one might trap and go
# through the SW datapath, which might not bump the HW counter.
$MZ $h1 -c 21 -d 20msec -p 100 \
-a own -b $ol1mac -A 192.0.2.1 -B 192.0.2.18 \
-q -t udp sp=54321,dp=12345
}
test_stats()
{
local dev=$1; shift
local dir=$1; shift
local a
local b
RET=0
a=$(hw_stats_get l3_stats $dev $dir packets)
send_packets_ipv4
b=$(busywait "$TC_HIT_TIMEOUT" until_counter_is ">= $a + 20" \
hw_stats_get l3_stats $dev $dir packets)
check_err $? "Traffic not reflected in the counter: $a -> $b"
log_test "Test $dir packets: $prot"
}
test_stats_tx()
{
test_stats g1a tx
}
test_stats_rx()
{
test_stats g2a rx
}
trap cleanup EXIT
setup_prepare
setup_wait
tests_run
exit $EXIT_STATUS
@@ -0,0 +1,512 @@
// SPDX-License-Identifier: GPL-2.0
#include <assert.h>
#include <errno.h>
#include <error.h>
#include <fcntl.h>
#include <limits.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <linux/mman.h>
#include <linux/errqueue.h>
#include <linux/if_packet.h>
#include <linux/ipv6.h>
#include <linux/socket.h>
#include <linux/sockios.h>
#include <net/ethernet.h>
#include <net/if.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip6.h>
#include <netinet/tcp.h>
#include <netinet/udp.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <liburing.h>
#define SKIP_CODE 42
struct t_io_uring_zcrx_ifq_reg {
__u32 if_idx;
__u32 if_rxq;
__u32 rq_entries;
__u32 flags;
__u64 area_ptr; /* pointer to struct io_uring_zcrx_area_reg */
__u64 region_ptr; /* struct io_uring_region_desc * */
struct io_uring_zcrx_offsets offsets;
__u32 zcrx_id;
__u32 rx_buf_len;
__u64 __resv[3];
};
static long page_size;
#define AREA_SIZE (8192 * page_size)
#define SEND_SIZE (512 * 4096)
#define min(a, b) \
({ \
typeof(a) _a = (a); \
typeof(b) _b = (b); \
_a < _b ? _a : _b; \
})
#define min_t(t, a, b) \
({ \
t _ta = (a); \
t _tb = (b); \
min(_ta, _tb); \
})
#define ALIGN_UP(v, align) (((v) + (align) - 1) & ~((align) - 1))
static int cfg_server;
static int cfg_client;
static int cfg_port = 8000;
static int cfg_payload_len;
static const char *cfg_ifname;
static int cfg_queue_id = -1;
static bool cfg_oneshot;
static int cfg_oneshot_recvs;
static int cfg_send_size = SEND_SIZE;
static struct sockaddr_in6 cfg_addr;
static unsigned int cfg_rx_buf_len;
static bool cfg_dry_run;
static char *payload;
static void *area_ptr;
static void *ring_ptr;
static size_t ring_size;
static struct io_uring_zcrx_rq rq_ring;
static unsigned long area_token;
static int connfd;
static bool stop;
static size_t received;
static unsigned long gettimeofday_ms(void)
{
struct timeval tv;
gettimeofday(&tv, NULL);
return (tv.tv_sec * 1000) + (tv.tv_usec / 1000);
}
static int parse_address(const char *str, int port, struct sockaddr_in6 *sin6)
{
int ret;
sin6->sin6_family = AF_INET6;
sin6->sin6_port = htons(port);
ret = inet_pton(sin6->sin6_family, str, &sin6->sin6_addr);
if (ret != 1) {
/* fallback to plain IPv4 */
ret = inet_pton(AF_INET, str, &sin6->sin6_addr.s6_addr32[3]);
if (ret != 1)
return -1;
/* add ::ffff prefix */
sin6->sin6_addr.s6_addr32[0] = 0;
sin6->sin6_addr.s6_addr32[1] = 0;
sin6->sin6_addr.s6_addr16[4] = 0;
sin6->sin6_addr.s6_addr16[5] = 0xffff;
}
return 0;
}
static inline size_t get_refill_ring_size(unsigned int rq_entries)
{
size_t size;
ring_size = rq_entries * sizeof(struct io_uring_zcrx_rqe);
/* add space for the header (head/tail/etc.) */
ring_size += page_size;
return ALIGN_UP(ring_size, page_size);
}
static void setup_zcrx(struct io_uring *ring)
{
unsigned int ifindex;
unsigned int rq_entries = 4096;
int ret;
ifindex = if_nametoindex(cfg_ifname);
if (!ifindex)
error(1, 0, "bad interface name: %s", cfg_ifname);
if (cfg_rx_buf_len && cfg_rx_buf_len != page_size) {
area_ptr = mmap(NULL,
AREA_SIZE,
PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE |
MAP_HUGETLB | MAP_HUGE_2MB,
-1,
0);
if (area_ptr == MAP_FAILED) {
printf("Can't allocate huge pages\n");
exit(SKIP_CODE);
}
} else {
area_ptr = mmap(NULL,
AREA_SIZE,
PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE,
0,
0);
if (area_ptr == MAP_FAILED)
error(1, 0, "mmap(): zero copy area");
}
ring_size = get_refill_ring_size(rq_entries);
ring_ptr = mmap(NULL,
ring_size,
PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE,
0,
0);
struct io_uring_region_desc region_reg = {
.size = ring_size,
.user_addr = (__u64)(unsigned long)ring_ptr,
.flags = IORING_MEM_REGION_TYPE_USER,
};
struct io_uring_zcrx_area_reg area_reg = {
.addr = (__u64)(unsigned long)area_ptr,
.len = AREA_SIZE,
.flags = 0,
};
struct t_io_uring_zcrx_ifq_reg reg = {
.if_idx = ifindex,
.if_rxq = cfg_queue_id,
.rq_entries = rq_entries,
.area_ptr = (__u64)(unsigned long)&area_reg,
.region_ptr = (__u64)(unsigned long)&region_reg,
.rx_buf_len = cfg_rx_buf_len,
};
ret = io_uring_register_ifq(ring, (void *)&reg);
if (cfg_rx_buf_len && (ret == -EINVAL || ret == -EOPNOTSUPP ||
ret == -ERANGE)) {
printf("Large chunks are not supported %i\n", ret);
exit(SKIP_CODE);
} else if (ret) {
error(1, 0, "io_uring_register_ifq(): %d", ret);
}
rq_ring.khead = (unsigned int *)((char *)ring_ptr + reg.offsets.head);
rq_ring.ktail = (unsigned int *)((char *)ring_ptr + reg.offsets.tail);
rq_ring.rqes = (struct io_uring_zcrx_rqe *)((char *)ring_ptr + reg.offsets.rqes);
rq_ring.rq_tail = 0;
rq_ring.ring_entries = reg.rq_entries;
area_token = area_reg.rq_area_token;
}
static void add_accept(struct io_uring *ring, int sockfd)
{
struct io_uring_sqe *sqe;
sqe = io_uring_get_sqe(ring);
io_uring_prep_accept(sqe, sockfd, NULL, NULL, 0);
sqe->user_data = 1;
}
static void add_recvzc(struct io_uring *ring, int sockfd)
{
struct io_uring_sqe *sqe;
sqe = io_uring_get_sqe(ring);
io_uring_prep_rw(IORING_OP_RECV_ZC, sqe, sockfd, NULL, 0, 0);
sqe->ioprio |= IORING_RECV_MULTISHOT;
sqe->user_data = 2;
}
static void add_recvzc_oneshot(struct io_uring *ring, int sockfd, size_t len)
{
struct io_uring_sqe *sqe;
sqe = io_uring_get_sqe(ring);
io_uring_prep_rw(IORING_OP_RECV_ZC, sqe, sockfd, NULL, len, 0);
sqe->ioprio |= IORING_RECV_MULTISHOT;
sqe->user_data = 2;
}
static void process_accept(struct io_uring *ring, struct io_uring_cqe *cqe)
{
if (cqe->res < 0)
error(1, 0, "accept()");
if (connfd)
error(1, 0, "Unexpected second connection");
connfd = cqe->res;
if (cfg_oneshot)
add_recvzc_oneshot(ring, connfd, page_size);
else
add_recvzc(ring, connfd);
}
static void process_recvzc(struct io_uring *ring, struct io_uring_cqe *cqe)
{
unsigned rq_mask = rq_ring.ring_entries - 1;
struct io_uring_zcrx_cqe *rcqe;
struct io_uring_zcrx_rqe *rqe;
struct io_uring_sqe *sqe;
uint64_t mask;
char *data;
ssize_t n;
int i;
if (cqe->res == 0 && cqe->flags == 0 && cfg_oneshot_recvs == 0) {
stop = true;
return;
}
if (cqe->res < 0)
error(1, 0, "recvzc(): %d", cqe->res);
if (cfg_oneshot) {
if (cqe->res == 0 && cqe->flags == 0 && cfg_oneshot_recvs) {
add_recvzc_oneshot(ring, connfd, page_size);
cfg_oneshot_recvs--;
}
} else if (!(cqe->flags & IORING_CQE_F_MORE)) {
add_recvzc(ring, connfd);
}
rcqe = (struct io_uring_zcrx_cqe *)(cqe + 1);
n = cqe->res;
mask = (1ULL << IORING_ZCRX_AREA_SHIFT) - 1;
data = (char *)area_ptr + (rcqe->off & mask);
for (i = 0; i < n; i++) {
if (*(data + i) != payload[(received + i)])
error(1, 0, "payload mismatch at %d", i);
}
received += n;
rqe = &rq_ring.rqes[(rq_ring.rq_tail & rq_mask)];
rqe->off = (rcqe->off & ~IORING_ZCRX_AREA_MASK) | area_token;
rqe->len = cqe->res;
io_uring_smp_store_release(rq_ring.ktail, ++rq_ring.rq_tail);
}
static void server_loop(struct io_uring *ring)
{
struct io_uring_cqe *cqe;
unsigned int count = 0;
unsigned int head;
int i, ret;
io_uring_submit_and_wait(ring, 1);
io_uring_for_each_cqe(ring, head, cqe) {
if (cqe->user_data == 1)
process_accept(ring, cqe);
else if (cqe->user_data == 2)
process_recvzc(ring, cqe);
else
error(1, 0, "unknown cqe");
count++;
}
io_uring_cq_advance(ring, count);
}
static void run_server(void)
{
unsigned int flags = 0;
struct io_uring ring;
int fd, enable, ret;
uint64_t tstop;
fd = socket(AF_INET6, SOCK_STREAM, 0);
if (fd == -1)
error(1, 0, "socket()");
enable = 1;
ret = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
if (ret < 0)
error(1, 0, "setsockopt(SO_REUSEADDR)");
ret = bind(fd, (struct sockaddr *)&cfg_addr, sizeof(cfg_addr));
if (ret < 0)
error(1, 0, "bind()");
flags |= IORING_SETUP_COOP_TASKRUN;
flags |= IORING_SETUP_SINGLE_ISSUER;
flags |= IORING_SETUP_DEFER_TASKRUN;
flags |= IORING_SETUP_SUBMIT_ALL;
flags |= IORING_SETUP_CQE32;
io_uring_queue_init(512, &ring, flags);
setup_zcrx(&ring);
if (cfg_dry_run)
return;
if (listen(fd, 1024) < 0)
error(1, 0, "listen()");
add_accept(&ring, fd);
tstop = gettimeofday_ms() + 5000;
while (!stop && gettimeofday_ms() < tstop)
server_loop(&ring);
if (!stop)
error(1, 0, "test failed\n");
}
static void run_client(void)
{
ssize_t to_send = cfg_send_size;
ssize_t sent = 0;
ssize_t chunk, res;
int fd;
fd = socket(AF_INET6, SOCK_STREAM, 0);
if (fd == -1)
error(1, 0, "socket()");
if (connect(fd, (struct sockaddr *)&cfg_addr, sizeof(cfg_addr)))
error(1, 0, "connect()");
while (to_send) {
void *src = &payload[sent];
chunk = min_t(ssize_t, cfg_payload_len, to_send);
res = send(fd, src, chunk, 0);
if (res < 0)
error(1, 0, "send(): %zd", sent);
sent += res;
to_send -= res;
}
close(fd);
}
static void usage(const char *filepath)
{
error(1, 0, "Usage: %s (-4|-6) (-s|-c) -h<server_ip> -p<port> "
"-l<payload_size> -i<ifname> -q<rxq_id>", filepath);
}
static void parse_opts(int argc, char **argv)
{
const int max_payload_len = SEND_SIZE -
sizeof(struct ipv6hdr) -
sizeof(struct tcphdr) -
40 /* max tcp options */;
struct sockaddr_in6 *addr6 = (void *) &cfg_addr;
char *addr = NULL;
int ret;
int c;
if (argc <= 1)
usage(argv[0]);
cfg_payload_len = max_payload_len;
while ((c = getopt(argc, argv, "sch:p:l:i:q:o:z:x:d")) != -1) {
switch (c) {
case 's':
if (cfg_client)
error(1, 0, "Pass one of -s or -c");
cfg_server = 1;
break;
case 'c':
if (cfg_server)
error(1, 0, "Pass one of -s or -c");
cfg_client = 1;
break;
case 'h':
addr = optarg;
break;
case 'p':
cfg_port = strtoul(optarg, NULL, 0);
break;
case 'l':
cfg_payload_len = strtoul(optarg, NULL, 0);
break;
case 'i':
cfg_ifname = optarg;
break;
case 'q':
cfg_queue_id = strtoul(optarg, NULL, 0);
break;
case 'o': {
cfg_oneshot = true;
cfg_oneshot_recvs = strtoul(optarg, NULL, 0);
break;
}
case 'z':
cfg_send_size = strtoul(optarg, NULL, 0);
break;
case 'x':
cfg_rx_buf_len = page_size * strtoul(optarg, NULL, 0);
break;
case 'd':
cfg_dry_run = true;
break;
}
}
if (cfg_server && addr)
error(1, 0, "Receiver cannot have -h specified");
memset(addr6, 0, sizeof(*addr6));
addr6->sin6_family = AF_INET6;
addr6->sin6_port = htons(cfg_port);
addr6->sin6_addr = in6addr_any;
if (addr) {
ret = parse_address(addr, cfg_port, addr6);
if (ret)
error(1, 0, "receiver address parse error: %s", addr);
}
if (cfg_payload_len > max_payload_len)
error(1, 0, "-l: payload exceeds max (%d)", max_payload_len);
}
int main(int argc, char **argv)
{
const char *cfg_test = argv[argc - 1];
int i;
page_size = sysconf(_SC_PAGESIZE);
if (page_size < 0)
return 1;
if (posix_memalign((void **)&payload, page_size, SEND_SIZE))
return 1;
parse_opts(argc, argv);
for (i = 0; i < SEND_SIZE; i++)
payload[i] = 'a' + (i % 26);
if (cfg_server)
run_server();
else if (cfg_client)
run_client();
return 0;
}
@@ -0,0 +1,210 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
import re
import resource
import time
from os import path
from lib.py import ksft_run, ksft_exit, KsftSkipEx, ksft_variants, KsftNamedVariant
from lib.py import NetDrvEpEnv
from lib.py import bkg, cmd, defer, ethtool, rand_port, wait_port_listen
from lib.py import EthtoolFamily, NetdevFamily
SKIP_CODE = 42
def mp_clear_wait(cfg):
"""Wait for io_uring memory providers to clear from all device queues."""
deadline = time.time() + 5
while time.time() < deadline:
queues = cfg.netnl.queue_get({'ifindex': cfg.ifindex}, dump=True)
if not any('io-uring' in q for q in queues):
return
time.sleep(0.1)
raise TimeoutError("Timed out waiting for memory provider to clear")
def create_rss_ctx(cfg):
output = ethtool(f"-X {cfg.ifname} context new start {cfg.target} equal 1").stdout
values = re.search(r'New RSS context is (\d+)', output).group(1)
return int(values)
def set_flow_rule(cfg):
output = ethtool(f"-N {cfg.ifname} flow-type tcp6 dst-port {cfg.port} action {cfg.target}").stdout
values = re.search(r'ID (\d+)', output).group(1)
return int(values)
def set_flow_rule_rss(cfg, rss_ctx_id):
output = ethtool(f"-N {cfg.ifname} flow-type tcp6 dst-port {cfg.port} context {rss_ctx_id}").stdout
values = re.search(r'ID (\d+)', output).group(1)
return int(values)
def check_iou_rx_buf_len(cfg, expected_rx_buf_len):
"""Check the io-uring memory provider exposes the expected rx_buf_len."""
q = cfg.netnl.queue_get({'ifindex': cfg.ifindex, 'type': 'rx', 'id': cfg.target})
napi_id = q['napi-id']
pools = cfg.netnl.page_pool_get({}, dump=True)
pools = [p for p in pools if p.get('napi-id') == napi_id
and 'io-uring' in p]
if len(pools) != 1:
raise Exception(f"Expected 1 io-uring page pool, found {len(pools)}")
rx_buf_len = pools[0]['io-uring'].get('rx-buf-len')
if rx_buf_len is None:
raise KsftSkipEx("io-uring 'rx-buf-len' attribute not supported")
if rx_buf_len != expected_rx_buf_len:
raise Exception(f'Expected io-uring rx-buf-len {expected_rx_buf_len}, '
f'got {rx_buf_len}')
def single(cfg):
channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
channels = channels['combined-count']
if channels < 2:
raise KsftSkipEx('Test requires NETIF with at least 2 combined channels')
rings = cfg.ethnl.rings_get({'header': {'dev-index': cfg.ifindex}})
rx_rings = rings['rx']
hds_thresh = rings.get('hds-thresh', 0)
cfg.ethnl.rings_set({'header': {'dev-index': cfg.ifindex},
'tcp-data-split': 'enabled',
'hds-thresh': 0,
'rx': 64})
defer(cfg.ethnl.rings_set, {'header': {'dev-index': cfg.ifindex},
'tcp-data-split': 'unknown',
'hds-thresh': hds_thresh,
'rx': rx_rings})
defer(mp_clear_wait, cfg)
cfg.target = channels - 1
ethtool(f"-X {cfg.ifname} equal {cfg.target}")
defer(ethtool, f"-X {cfg.ifname} default")
flow_rule_id = set_flow_rule(cfg)
defer(ethtool, f"-N {cfg.ifname} delete {flow_rule_id}")
def rss(cfg):
channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
channels = channels['combined-count']
if channels < 2:
raise KsftSkipEx('Test requires NETIF with at least 2 combined channels')
rings = cfg.ethnl.rings_get({'header': {'dev-index': cfg.ifindex}})
rx_rings = rings['rx']
hds_thresh = rings.get('hds-thresh', 0)
cfg.ethnl.rings_set({'header': {'dev-index': cfg.ifindex},
'tcp-data-split': 'enabled',
'hds-thresh': 0,
'rx': 64})
defer(cfg.ethnl.rings_set, {'header': {'dev-index': cfg.ifindex},
'tcp-data-split': 'unknown',
'hds-thresh': hds_thresh,
'rx': rx_rings})
defer(mp_clear_wait, cfg)
cfg.target = channels - 1
ethtool(f"-X {cfg.ifname} equal {cfg.target}")
defer(ethtool, f"-X {cfg.ifname} default")
rss_ctx_id = create_rss_ctx(cfg)
defer(ethtool, f"-X {cfg.ifname} delete context {rss_ctx_id}")
flow_rule_id = set_flow_rule_rss(cfg, rss_ctx_id)
defer(ethtool, f"-N {cfg.ifname} delete {flow_rule_id}")
def _require_ntuple(cfg):
features = ethtool(f"-k {cfg.ifname}", json=True)[0]
if not features["ntuple-filters"]["active"]:
if features["ntuple-filters"]["fixed"]:
raise KsftSkipEx("Device does not support ntuple-filters")
ethtool(f"-K {cfg.ifname} ntuple-filters on")
defer(ethtool, f"-K {cfg.ifname} ntuple-filters off")
@ksft_variants([
KsftNamedVariant("single", single),
KsftNamedVariant("rss", rss),
])
def test_zcrx(cfg, setup) -> None:
cfg.require_ipver('6')
_require_ntuple(cfg)
setup(cfg)
rx_cmd = f"{cfg.bin_local} -s -p {cfg.port} -i {cfg.ifname} -q {cfg.target}"
tx_cmd = f"{cfg.bin_remote} -c -h {cfg.addr_v['6']} -p {cfg.port} -l 12840"
with bkg(rx_cmd, exit_wait=True):
wait_port_listen(cfg.port, proto="tcp")
cmd(tx_cmd, host=cfg.remote)
@ksft_variants([
KsftNamedVariant("single", single),
KsftNamedVariant("rss", rss),
])
def test_zcrx_oneshot(cfg, setup) -> None:
cfg.require_ipver('6')
_require_ntuple(cfg)
setup(cfg)
rx_cmd = f"{cfg.bin_local} -s -p {cfg.port} -i {cfg.ifname} -q {cfg.target} -o 4"
tx_cmd = f"{cfg.bin_remote} -c -h {cfg.addr_v['6']} -p {cfg.port} -l 4096 -z 16384"
with bkg(rx_cmd, exit_wait=True):
wait_port_listen(cfg.port, proto="tcp")
cmd(tx_cmd, host=cfg.remote)
def test_zcrx_large_chunks(cfg) -> None:
"""Test zcrx with large buffer chunks."""
cfg.require_ipver('6')
_require_ntuple(cfg)
hp_file = "/proc/sys/vm/nr_hugepages"
with open(hp_file, 'r+', encoding='utf-8') as f:
nr_hugepages = int(f.read().strip())
if nr_hugepages < 64:
f.seek(0)
f.write("64")
defer(lambda: open(hp_file, 'w', encoding='utf-8').write(str(nr_hugepages)))
single(cfg)
page_size = resource.getpagesize()
nr_pages = 2
rx_buf_len = nr_pages * page_size
rx_cmd = f"{cfg.bin_local} -s -p {cfg.port} -i {cfg.ifname} -q {cfg.target} -x {nr_pages}"
tx_cmd = f"{cfg.bin_remote} -c -h {cfg.addr_v['6']} -p {cfg.port} -l 12840"
probe = cmd(rx_cmd + " -d", fail=False)
if probe.ret == SKIP_CODE:
raise KsftSkipEx(probe.stdout.strip())
mp_clear_wait(cfg)
with bkg(rx_cmd, exit_wait=True):
wait_port_listen(cfg.port, proto="tcp")
check_iou_rx_buf_len(cfg, rx_buf_len)
cmd(tx_cmd, host=cfg.remote)
def main() -> None:
with NetDrvEpEnv(__file__) as cfg:
cfg.bin_local = path.abspath(path.dirname(__file__) + "/../../../drivers/net/hw/iou-zcrx")
cfg.bin_remote = cfg.remote.deploy(cfg.bin_local)
cfg.ethnl = EthtoolFamily()
cfg.netnl = NetdevFamily()
cfg.port = rand_port()
ksft_run(globs=globals(), cases=[test_zcrx, test_zcrx_oneshot,
test_zcrx_large_chunks], args=(cfg, ))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,204 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""Traffic test for VXLAN + IPsec crypto-offload."""
import os
from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_ge
from lib.py import ksft_variants, KsftNamedVariant, KsftSkipEx
from lib.py import CmdExitFailure, NetDrvEpEnv, cmd, defer, ethtool, ip
from lib.py import Iperf3Runner
# Inner tunnel addresses - TEST-NET-2 (RFC 5737) / doc prefix (RFC 3849)
INNER_V4_LOCAL = "198.51.100.1"
INNER_V4_REMOTE = "198.51.100.2"
INNER_V6_LOCAL = "2001:db8:100::1"
INNER_V6_REMOTE = "2001:db8:100::2"
# ESP parameters
SPI_OUT = "0x1000"
SPI_IN = "0x1001"
# 128-bit key + 32-bit salt = 20 bytes hex, 128-bit ICV
ESP_AEAD = "aead 'rfc4106(gcm(aes))' 0x" + "01" * 20 + " 128"
def xfrm(args, host=None):
"""Runs 'ip xfrm' via shell to preserve parentheses in algo names."""
cmd(f"ip xfrm {args}", shell=True, host=host)
def check_xfrm_offload_support():
"""Skips if iproute2 lacks xfrm offload support."""
out = cmd("ip xfrm state help", fail=False)
if "offload" not in out.stdout + out.stderr:
raise KsftSkipEx("iproute2 too old, missing xfrm offload")
def check_esp_hw_offload(cfg):
"""Skips if device lacks esp-hw-offload support."""
check_xfrm_offload_support()
try:
feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
except (CmdExitFailure, IndexError) as e:
raise KsftSkipEx(f"can't query features: {e}") from e
if not feat.get("esp-hw-offload", {}).get("active"):
raise KsftSkipEx("Device does not support esp-hw-offload")
def get_tx_drops(cfg):
"""Returns TX dropped counter from the physical device."""
stats = ip("-s -s link show dev " + cfg.ifname, json=True)[0]
return stats["stats64"]["tx"]["dropped"]
def setup_vxlan_ipsec(cfg, outer_ipver, inner_ipver):
"""Sets up VXLAN tunnel with IPsec transport-mode crypto-offload."""
vxlan_name = f"vx{os.getpid()}"
local_addr = cfg.addr_v[outer_ipver]
remote_addr = cfg.remote_addr_v[outer_ipver]
if inner_ipver == "4":
inner_local = f"{INNER_V4_LOCAL}/24"
inner_remote = f"{INNER_V4_REMOTE}/24"
addr_extra = ""
else:
inner_local = f"{INNER_V6_LOCAL}/64"
inner_remote = f"{INNER_V6_REMOTE}/64"
addr_extra = " nodad"
if outer_ipver == "6":
vxlan_opts = "udp6zerocsumtx udp6zerocsumrx"
else:
vxlan_opts = "noudpcsum"
# VXLAN tunnel - local side
ip(f"link add {vxlan_name} type vxlan id 100 dstport 4789 {vxlan_opts} "
f"local {local_addr} remote {remote_addr} dev {cfg.ifname}")
defer(ip, f"link del {vxlan_name}")
ip(f"addr add {inner_local} dev {vxlan_name}{addr_extra}")
ip(f"link set {vxlan_name} up")
# VXLAN tunnel - remote side
ip(f"link add {vxlan_name} type vxlan id 100 dstport 4789 {vxlan_opts} "
f"local {remote_addr} remote {local_addr} dev {cfg.remote_ifname}",
host=cfg.remote)
defer(ip, f"link del {vxlan_name}", host=cfg.remote)
ip(f"addr add {inner_remote} dev {vxlan_name}{addr_extra}",
host=cfg.remote)
ip(f"link set {vxlan_name} up", host=cfg.remote)
# xfrm state - local outbound SA
xfrm(f"state add src {local_addr} dst {remote_addr} "
f"proto esp spi {SPI_OUT} "
f"{ESP_AEAD} "
f"mode transport offload crypto dev {cfg.ifname} dir out")
defer(xfrm, f"state del src {local_addr} dst {remote_addr} "
f"proto esp spi {SPI_OUT}")
# xfrm state - local inbound SA
xfrm(f"state add src {remote_addr} dst {local_addr} "
f"proto esp spi {SPI_IN} "
f"{ESP_AEAD} "
f"mode transport offload crypto dev {cfg.ifname} dir in")
defer(xfrm, f"state del src {remote_addr} dst {local_addr} "
f"proto esp spi {SPI_IN}")
# xfrm state - remote outbound SA (mirror, software crypto)
xfrm(f"state add src {remote_addr} dst {local_addr} "
f"proto esp spi {SPI_IN} "
f"{ESP_AEAD} "
f"mode transport",
host=cfg.remote)
defer(xfrm, f"state del src {remote_addr} dst {local_addr} "
f"proto esp spi {SPI_IN}", host=cfg.remote)
# xfrm state - remote inbound SA (mirror, software crypto)
xfrm(f"state add src {local_addr} dst {remote_addr} "
f"proto esp spi {SPI_OUT} "
f"{ESP_AEAD} "
f"mode transport",
host=cfg.remote)
defer(xfrm, f"state del src {local_addr} dst {remote_addr} "
f"proto esp spi {SPI_OUT}", host=cfg.remote)
# xfrm policy - local out
xfrm(f"policy add src {local_addr} dst {remote_addr} "
f"proto udp dport 4789 dir out "
f"tmpl src {local_addr} dst {remote_addr} proto esp mode transport")
defer(xfrm, f"policy del src {local_addr} dst {remote_addr} "
f"proto udp dport 4789 dir out")
# xfrm policy - local in
xfrm(f"policy add src {remote_addr} dst {local_addr} "
f"proto udp dport 4789 dir in "
f"tmpl src {remote_addr} dst {local_addr} proto esp mode transport")
defer(xfrm, f"policy del src {remote_addr} dst {local_addr} "
f"proto udp dport 4789 dir in")
# xfrm policy - remote out
xfrm(f"policy add src {remote_addr} dst {local_addr} "
f"proto udp dport 4789 dir out "
f"tmpl src {remote_addr} dst {local_addr} proto esp mode transport",
host=cfg.remote)
defer(xfrm, f"policy del src {remote_addr} dst {local_addr} "
f"proto udp dport 4789 dir out", host=cfg.remote)
# xfrm policy - remote in
xfrm(f"policy add src {local_addr} dst {remote_addr} "
f"proto udp dport 4789 dir in "
f"tmpl src {local_addr} dst {remote_addr} proto esp mode transport",
host=cfg.remote)
defer(xfrm, f"policy del src {local_addr} dst {remote_addr} "
f"proto udp dport 4789 dir in", host=cfg.remote)
def _vxlan_ipsec_variants():
"""Generates outer/inner IP version variants."""
for outer in ["4", "6"]:
for inner in ["4", "6"]:
yield KsftNamedVariant(f"outer_v{outer}_inner_v{inner}", outer, inner)
@ksft_variants(_vxlan_ipsec_variants())
def test_vxlan_ipsec_crypto_offload(cfg, outer_ipver, inner_ipver):
"""Tests VXLAN+IPsec crypto-offload has no TX drops."""
cfg.require_ipver(outer_ipver)
check_esp_hw_offload(cfg)
setup_vxlan_ipsec(cfg, outer_ipver, inner_ipver)
if inner_ipver == "4":
inner_local = INNER_V4_LOCAL
inner_remote = INNER_V4_REMOTE
ping = "ping"
else:
inner_local = INNER_V6_LOCAL
inner_remote = INNER_V6_REMOTE
ping = "ping -6"
cmd(f"{ping} -c 1 -W 2 {inner_remote}")
drops_before = get_tx_drops(cfg)
runner = Iperf3Runner(cfg, server_ip=inner_local,
client_ip=inner_remote)
bw_gbps = runner.measure_bandwidth(reverse=True)
cfg.wait_hw_stats_settle()
drops_after = get_tx_drops(cfg)
ksft_eq(drops_after - drops_before, 0,
comment="TX drops during VXLAN+IPsec")
ksft_ge(bw_gbps, 0.1,
comment="Minimum 100Mbps over VXLAN+IPsec")
def main():
"""Runs VXLAN+IPsec crypto-offload GSO selftest."""
with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
ksft_run([test_vxlan_ipsec_crypto_offload], args=(cfg,))
ksft_exit()
if __name__ == "__main__":
main()
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
from lib.py import ksft_run, ksft_exit
from lib.py import ksft_ge, ksft_eq
from lib.py import KsftSkipEx
from lib.py import ksft_disruptive
from lib.py import EthtoolFamily, NetdevFamily
from lib.py import NetDrvEnv
from lib.py import cmd, ip, defer
def read_affinity(irq) -> str:
with open(f'/proc/irq/{irq}/smp_affinity', 'r') as fp:
return fp.read().lstrip("0,").strip()
def write_affinity(irq, what) -> str:
if what != read_affinity(irq):
with open(f'/proc/irq/{irq}/smp_affinity', 'w') as fp:
fp.write(what)
def check_irqs_reported(cfg) -> None:
""" Check that device reports IRQs for NAPI instances """
napis = cfg.netnl.napi_get({"ifindex": cfg.ifindex}, dump=True)
irqs = sum(['irq' in x for x in napis])
ksft_ge(irqs, 1)
ksft_eq(irqs, len(napis))
def _check_reconfig(cfg, reconfig_cb) -> None:
napis = cfg.netnl.napi_get({"ifindex": cfg.ifindex}, dump=True)
for n in reversed(napis):
if 'irq' in n:
break
else:
raise KsftSkipEx(f"Device has no NAPI with IRQ attribute (#napis: {len(napis)}")
old = read_affinity(n['irq'])
# pick an affinity that's not the current one
new = "3" if old != "3" else "5"
write_affinity(n['irq'], new)
defer(write_affinity, n['irq'], old)
reconfig_cb(cfg)
ksft_eq(read_affinity(n['irq']), new, comment="IRQ affinity changed after reconfig")
def check_reconfig_queues(cfg) -> None:
def reconfig(cfg) -> None:
channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
if channels['combined-count'] == 0:
rx_type = 'rx'
else:
rx_type = 'combined'
cur_queue_cnt = channels[f'{rx_type}-count']
max_queue_cnt = channels[f'{rx_type}-max']
cmd(f"ethtool -L {cfg.ifname} {rx_type} 1")
cmd(f"ethtool -L {cfg.ifname} {rx_type} {max_queue_cnt}")
cmd(f"ethtool -L {cfg.ifname} {rx_type} {cur_queue_cnt}")
_check_reconfig(cfg, reconfig)
def check_reconfig_xdp(cfg) -> None:
def reconfig(cfg) -> None:
ip(f"link set dev %s xdp obj %s sec xdp" %
(cfg.ifname, cfg.net_lib_dir / "xdp_dummy.bpf.o"))
ip(f"link set dev %s xdp off" % cfg.ifname)
_check_reconfig(cfg, reconfig)
@ksft_disruptive
def check_down(cfg) -> None:
def reconfig(cfg) -> None:
ip("link set dev %s down" % cfg.ifname)
ip("link set dev %s up" % cfg.ifname)
_check_reconfig(cfg, reconfig)
def main() -> None:
with NetDrvEnv(__file__, nsim_test=False) as cfg:
cfg.ethnl = EthtoolFamily()
cfg.netnl = NetdevFamily()
ksft_run([check_irqs_reported, check_reconfig_queues,
check_reconfig_xdp, check_down],
args=(cfg, ))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,56 @@
# SPDX-License-Identifier: GPL-2.0
"""
Driver test environment (hardware-only tests).
NetDrvEnv and NetDrvEpEnv are the main environment classes.
NetDrvContEnv extends NetDrvEpEnv with netkit container support.
Former is for local host only tests, latter creates / connects
to a remote endpoint. See NIPA wiki for more information about
running and writing driver tests.
"""
import sys
from pathlib import Path
KSFT_DIR = (Path(__file__).parent / "../../../../..").resolve()
try:
sys.path.append(KSFT_DIR.as_posix())
# Import one by one to avoid pylint false positives
from net.lib.py import NetNS, NetNSEnter, NetdevSimDev, UserNetNS
from net.lib.py import EthtoolFamily, NetdevFamily, NetshaperFamily, \
NlError, RtnlFamily, DevlinkFamily, PSPFamily, Netlink
from net.lib.py import CmdExitFailure
from net.lib.py import bkg, cmd, bpftool, bpftrace, defer, ethtool, \
fd_read_timeout, ip, rand_port, rand_ports, wait_port_listen, \
wait_file, tool
from net.lib.py import bpf_map_set, bpf_map_dump, bpf_prog_map_ids
from net.lib.py import KsftSkipEx, KsftFailEx, KsftXfailEx
from net.lib.py import ksft_disruptive, ksft_exit, ksft_pr, ksft_run, \
ksft_setup, ksft_variants, KsftNamedVariant
from net.lib.py import ksft_eq, ksft_ge, ksft_in, ksft_is, ksft_lt, \
ksft_ne, ksft_not_in, ksft_raises, ksft_true, ksft_gt, ksft_not_none
from drivers.net.lib.py import GenerateTraffic, Remote, Iperf3Runner
from drivers.net.lib.py import NetDrvEnv, NetDrvEpEnv, NetDrvContEnv
__all__ = ["NetNS", "NetNSEnter", "NetdevSimDev", "UserNetNS",
"EthtoolFamily", "NetdevFamily", "NetshaperFamily",
"NlError", "RtnlFamily", "DevlinkFamily", "PSPFamily", "Netlink",
"CmdExitFailure",
"bkg", "cmd", "bpftool", "bpftrace", "defer", "ethtool",
"fd_read_timeout", "ip", "rand_port", "rand_ports",
"wait_port_listen", "wait_file", "tool",
"bpf_map_set", "bpf_map_dump", "bpf_prog_map_ids",
"KsftSkipEx", "KsftFailEx", "KsftXfailEx",
"ksft_disruptive", "ksft_exit", "ksft_pr", "ksft_run",
"ksft_setup", "ksft_variants", "KsftNamedVariant",
"ksft_eq", "ksft_ge", "ksft_in", "ksft_is", "ksft_lt",
"ksft_ne", "ksft_not_in", "ksft_raises", "ksft_true", "ksft_gt",
"ksft_not_none", "ksft_not_none",
"NetDrvEnv", "NetDrvEpEnv", "NetDrvContEnv", "GenerateTraffic",
"Remote", "Iperf3Runner"]
except ModuleNotFoundError as e:
print("Failed importing `net` library from kernel sources")
print(str(e))
sys.exit(4)
@@ -0,0 +1,103 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
# Kselftest framework requirement - SKIP code is 4.
ksft_skip=4
ALL_TESTS="loopback_test"
NUM_NETIFS=2
lib_dir=$(dirname "$0")
source "$lib_dir"/../../../net/forwarding/tc_common.sh
source "$lib_dir"/../../../net/forwarding/lib.sh
h1_create()
{
simple_if_init $h1 192.0.2.1/24
tc qdisc add dev $h1 clsact
}
h1_destroy()
{
tc qdisc del dev $h1 clsact
simple_if_fini $h1 192.0.2.1/24
}
h2_create()
{
simple_if_init $h2
}
h2_destroy()
{
simple_if_fini $h2
}
loopback_test()
{
RET=0
tc filter add dev $h1 ingress protocol arp pref 1 handle 101 flower \
skip_hw arp_op reply arp_tip 192.0.2.1 action drop
$MZ $h1 -c 1 -t arp -q
tc_check_packets "dev $h1 ingress" 101 1
check_fail $? "Matched on a filter without loopback setup"
ethtool -K $h1 loopback on
check_err $? "Failed to enable loopback"
setup_wait_dev $h1
$MZ $h1 -c 1 -t arp -q
tc_check_packets "dev $h1 ingress" 101 1
check_err $? "Did not match on filter with loopback"
ethtool -K $h1 loopback off
check_err $? "Failed to disable loopback"
$MZ $h1 -c 1 -t arp -q
tc_check_packets "dev $h1 ingress" 101 2
check_fail $? "Matched on a filter after loopback was removed"
tc filter del dev $h1 ingress protocol arp pref 1 handle 101 flower
log_test "loopback"
}
setup_prepare()
{
h1=${NETIFS[p1]}
h2=${NETIFS[p2]}
vrf_prepare
h1_create
h2_create
if ethtool -k $h1 | grep loopback | grep -q fixed; then
log_test "SKIP: dev $h1 does not support loopback feature"
exit $ksft_skip
fi
}
cleanup()
{
pre_cleanup
h2_destroy
h1_destroy
vrf_cleanup
}
trap cleanup EXIT
setup_prepare
setup_wait
tests_run
exit $EXIT_STATUS
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,225 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
# pylint: disable=locally-disabled, invalid-name, attribute-defined-outside-init, too-few-public-methods
"""
Tests related to configuration of HW timestamping
"""
import errno
import ctypes
import fcntl
import socket
from lib.py import ksft_run, ksft_exit, ksft_ge, ksft_eq, KsftSkipEx
from lib.py import NetDrvEnv, EthtoolFamily, NlError
SIOCSHWTSTAMP = 0x89b0
SIOCGHWTSTAMP = 0x89b1
class hwtstamp_config(ctypes.Structure):
""" Python copy of struct hwtstamp_config """
_fields_ = [
("flags", ctypes.c_int),
("tx_type", ctypes.c_int),
("rx_filter", ctypes.c_int),
]
class ifreq(ctypes.Structure):
""" Python copy of struct ifreq """
_fields_ = [
("ifr_name", ctypes.c_char * 16),
("ifr_data", ctypes.POINTER(hwtstamp_config)),
]
def __get_hwtimestamp_support(cfg):
""" Retrieve supported configuration information """
try:
tsinfo = cfg.ethnl.tsinfo_get({'header': {'dev-name': cfg.ifname}})
except NlError as e:
if e.error == errno.EOPNOTSUPP:
raise KsftSkipEx("timestamping configuration is not supported") from e
raise
ctx = {}
tx = tsinfo.get('tx-types', {})
rx = tsinfo.get('rx-filters', {})
bits = tx.get('bits', {})
ctx['tx'] = bits.get('bit', [])
bits = rx.get('bits', {})
ctx['rx'] = bits.get('bit', [])
return ctx
def __get_hwtimestamp_config_ioctl(cfg):
""" Retrieve current TS configuration information (via ioctl) """
config = hwtstamp_config()
req = ifreq()
req.ifr_name = cfg.ifname.encode()
req.ifr_data = ctypes.pointer(config)
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
fcntl.ioctl(sock.fileno(), SIOCGHWTSTAMP, req)
sock.close()
except OSError as e:
if e.errno == errno.EOPNOTSUPP:
raise KsftSkipEx("timestamping configuration is not supported via ioctl") from e
raise
return config
def __get_hwtimestamp_config(cfg):
""" Retrieve current TS configuration information (via netLink) """
try:
tscfg = cfg.ethnl.tsconfig_get({'header': {'dev-name': cfg.ifname}})
except NlError as e:
if e.error == errno.EOPNOTSUPP:
raise KsftSkipEx("timestamping configuration is not supported via netlink") from e
raise
return tscfg
def __set_hwtimestamp_config_ioctl(cfg, ts):
""" Setup new TS configuration information (via ioctl) """
config = hwtstamp_config()
config.rx_filter = ts['rx-filters']['bits']['bit'][0]['index']
config.tx_type = ts['tx-types']['bits']['bit'][0]['index']
req = ifreq()
req.ifr_name = cfg.ifname.encode()
req.ifr_data = ctypes.pointer(config)
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
fcntl.ioctl(sock.fileno(), SIOCSHWTSTAMP, req)
sock.close()
except OSError as e:
if e.errno == errno.EOPNOTSUPP:
raise KsftSkipEx("timestamping configuration is not supported via ioctl") from e
raise
def __set_hwtimestamp_config(cfg, ts):
""" Setup new TS configuration information (via netlink) """
ts['header'] = {'dev-name': cfg.ifname}
try:
res = cfg.ethnl.tsconfig_set(ts)
except NlError as e:
if e.error == errno.EOPNOTSUPP:
raise KsftSkipEx("timestamping configuration is not supported via netlink") from e
raise
return res
def __perform_hwtstamp_tx(cfg, is_ioctl):
"""
Test TX timestamp configuration via either netlink or ioctl.
The driver should apply provided config and report back proper state.
"""
orig_tscfg = __get_hwtimestamp_config(cfg)
ts = __get_hwtimestamp_support(cfg)
tx = ts['tx']
for t in tx:
res = None
tscfg = orig_tscfg
tscfg['tx-types']['bits']['bit'] = [t]
if is_ioctl:
__set_hwtimestamp_config_ioctl(cfg, tscfg)
else:
res = __set_hwtimestamp_config(cfg, tscfg)
if res is None:
res = __get_hwtimestamp_config(cfg)
resioctl = __get_hwtimestamp_config_ioctl(cfg)
ksft_eq(res['tx-types']['bits']['bit'], [t])
ksft_eq(resioctl.tx_type, t['index'])
__set_hwtimestamp_config(cfg, orig_tscfg)
def test_hwtstamp_tx_netlink(cfg):
"""
Test TX timestamp configuration setup via netlink.
The driver should apply provided config and report back proper state.
"""
__perform_hwtstamp_tx(cfg, False)
def test_hwtstamp_tx_ioctl(cfg):
"""
Test TX timestamp configuration setup via ioctl.
The driver should apply provided config and report back proper state.
"""
__perform_hwtstamp_tx(cfg, True)
def __perform_hwtstamp_rx(cfg, is_ioctl):
"""
Test RX timestamp configuration.
The filter configuration is taken from the list of supported filters.
The driver should apply the config without error and report back proper state.
Some extension of the timestamping scope is allowed for PTP filters.
"""
orig_tscfg = __get_hwtimestamp_config(cfg)
ts = __get_hwtimestamp_support(cfg)
rx = ts['rx']
for r in rx:
res = None
tscfg = orig_tscfg
tscfg['rx-filters']['bits']['bit'] = [r]
if is_ioctl:
__set_hwtimestamp_config_ioctl(cfg, tscfg)
else:
res = __set_hwtimestamp_config(cfg, tscfg)
if res is None:
res = __get_hwtimestamp_config(cfg)
resioctl = __get_hwtimestamp_config_ioctl(cfg)
ksft_eq(resioctl.rx_filter, res['rx-filters']['bits']['bit'][0]['index'])
if r['index'] == 0 or r['index'] == 1:
ksft_eq(res['rx-filters']['bits']['bit'][0]['index'], r['index'])
else:
# the driver can fallback to some value which has higher coverage for timestamping
ksft_ge(res['rx-filters']['bits']['bit'][0]['index'], r['index'])
__set_hwtimestamp_config(cfg, orig_tscfg)
def test_hwtstamp_rx_netlink(cfg):
"""
Test RX timestamp configuration via netlink.
The filter configuration is taken from the list of supported filters.
The driver should apply the config without error and report back proper state.
Some extension of the timestamping scope is allowed for PTP filters.
"""
__perform_hwtstamp_rx(cfg, False)
def test_hwtstamp_rx_ioctl(cfg):
"""
Test RX timestamp configuration via ioctl.
The filter configuration is taken from the list of supported filters.
The driver should apply the config without error and report back proper state.
Some extension of the timestamping scope is allowed for PTP filters.
"""
__perform_hwtstamp_rx(cfg, True)
def main() -> None:
""" Ksft boiler plate main """
with NetDrvEnv(__file__, nsim_test=False) as cfg:
cfg.ethnl = EthtoolFamily()
ksft_run([test_hwtstamp_tx_ioctl, test_hwtstamp_tx_netlink,
test_hwtstamp_rx_ioctl, test_hwtstamp_rx_netlink],
args=(cfg,))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""Test devmem TCP with netkit."""
import os
from devmem_lib import setup_test, run_rx, run_tx, run_tx_chunks, run_rx_hds
from lib.py import ksft_run, ksft_exit, ksft_disruptive
from lib.py import NetDrvContEnv
@ksft_disruptive
def check_nk_rx(cfg) -> None:
"""Run the devmem RX test through netkit."""
run_rx(cfg)
@ksft_disruptive
def check_nk_tx(cfg) -> None:
"""Run the devmem TX test through netkit."""
run_tx(cfg)
@ksft_disruptive
def check_nk_tx_chunks(cfg) -> None:
"""Run the devmem TX chunking test through netkit."""
run_tx_chunks(cfg)
def check_nk_rx_hds(cfg) -> None:
"""Run the HDS test through netkit."""
run_rx_hds(cfg)
def main() -> None:
"""Run the netkit devmem test cases."""
with NetDrvContEnv(__file__, rxqueues=2, primary_rx_redirect=True) as cfg:
setup_test(cfg,
os.path.join(os.path.dirname(os.path.abspath(__file__)),
"ncdevmem"))
ksft_run([check_nk_rx, check_nk_tx, check_nk_tx_chunks,
check_nk_rx_hds], args=(cfg,))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: GPL-2.0
#include <linux/bpf.h>
#include <linux/pkt_cls.h>
#include <linux/if_ether.h>
#include <linux/ipv6.h>
#include <linux/in6.h>
#include <bpf/bpf_endian.h>
#include <bpf/bpf_helpers.h>
#define TC_ACT_OK 0
#define ETH_P_IPV6 0x86DD
#define ctx_ptr(field) ((void *)(long)(field))
#define v6_p64_equal(a, b) (a.s6_addr32[0] == b.s6_addr32[0] && \
a.s6_addr32[1] == b.s6_addr32[1])
volatile __u32 netkit_ifindex;
volatile __u8 ipv6_prefix[16];
SEC("tc/ingress")
int tc_redirect_peer(struct __sk_buff *skb)
{
void *data_end = ctx_ptr(skb->data_end);
void *data = ctx_ptr(skb->data);
struct in6_addr *peer_addr;
struct ipv6hdr *ip6h;
struct ethhdr *eth;
peer_addr = (struct in6_addr *)ipv6_prefix;
if (skb->protocol != bpf_htons(ETH_P_IPV6))
return TC_ACT_OK;
eth = data;
if ((void *)(eth + 1) > data_end)
return TC_ACT_OK;
ip6h = data + sizeof(struct ethhdr);
if ((void *)(ip6h + 1) > data_end)
return TC_ACT_OK;
if (!v6_p64_equal(ip6h->daddr, (*peer_addr)))
return TC_ACT_OK;
return bpf_redirect_peer(netkit_ifindex, 0);
}
char __license[] SEC("license") = "GPL";
@@ -0,0 +1,29 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""
Test exercising NetDrvContEnv() itself, a NetDrvContEnv() selftest.
"""
from lib.py import ksft_run, ksft_exit
from lib.py import NetDrvContEnv
from lib.py import cmd
def test_ping(cfg) -> None:
""" Run ping between the container and the remote system. """
cfg.require_ipver("6")
cmd(f"ping -c 1 -W5 {cfg.nk_guest_ipv6}", host=cfg.remote)
cmd(f"ping -c 1 -W5 {cfg.remote_addr_v['6']}", ns=cfg.netns)
def main() -> None:
""" Ksft boiler plate main """
with NetDrvContEnv(__file__) as cfg:
ksft_run([test_ping], args=(cfg,))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: GPL-2.0
#include <linux/bpf.h>
#include <linux/pkt_cls.h>
#include <linux/if_ether.h>
#include <linux/in.h>
#include <linux/ipv6.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#define ctx_ptr(field) ((void *)(long)(field))
volatile __u32 phys_ifindex;
SEC("tc/ingress")
int nk_primary_rx_redirect(struct __sk_buff *skb)
{
void *data_end = ctx_ptr(skb->data_end);
void *data = ctx_ptr(skb->data);
struct ethhdr *eth;
struct ipv6hdr *ip6h;
eth = data;
if ((void *)(eth + 1) > data_end)
return TC_ACT_OK;
if (eth->h_proto != bpf_htons(ETH_P_IPV6))
return TC_ACT_OK;
ip6h = data + sizeof(struct ethhdr);
if ((void *)(ip6h + 1) > data_end)
return TC_ACT_OK;
if (ip6h->nexthdr == IPPROTO_ICMPV6)
return TC_ACT_OK;
return bpf_redirect_neigh(phys_ifindex, NULL, 0, 0);
}
char __license[] SEC("license") = "GPL";
@@ -0,0 +1,465 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
import re
import time
import threading
from os import path
from lib.py import (
ksft_run,
ksft_exit,
ksft_eq,
ksft_in,
ksft_not_in,
ksft_raises,
)
from lib.py import (
NetDrvContEnv,
NetNSEnter,
EthtoolFamily,
NetdevFamily,
RtnlFamily,
)
from lib.py import (
Netlink,
bkg,
cmd,
defer,
ethtool,
ip,
rand_port,
wait_port_listen,
)
from lib.py import KsftSkipEx, CmdExitFailure
# iou-zcrx exits with 42 from setup_zcrx() when the NIC does not advertise
# QCFG_RX_PAGE_SIZE (or otherwise rejects the requested rx_buf_len).
SKIP_CODE = 42
def _restore_hugepages(count):
with open("/proc/sys/vm/nr_hugepages", "w", encoding="utf-8") as f:
f.write(str(count))
def _mp_clear_wait(cfg, src_queue):
"""Wait for the io_uring memory provider to clear from the leased
physical queue; io_uring tears it down asynchronously after the
process holding the ifq exits."""
netdevnl = NetdevFamily()
deadline = time.time() + 5
while time.time() < deadline:
queue_info = netdevnl.queue_get(
{"ifindex": cfg.ifindex, "id": src_queue, "type": "rx"}
)
if "io-uring" not in queue_info:
return
time.sleep(0.1)
raise TimeoutError("Timed out waiting for memory provider to clear")
def _create_netkit_pair(cfg, rxqueues=2):
if cfg.nk_host_ifname:
cmd(f"ip link del dev {cfg.nk_host_ifname}", fail=False)
cfg.nk_host_ifname = None
cfg.nk_guest_ifname = None
cfg.detach_bpf()
all_links = ip("-d link show", json=True)
old_idxs = {
link["ifindex"]
for link in all_links
if link.get("linkinfo", {}).get("info_kind") == "netkit"
}
rtnl = RtnlFamily()
rtnl.newlink(
{
"linkinfo": {
"kind": "netkit",
"data": {
"mode": "l2",
"policy": "forward",
"peer-policy": "forward",
},
},
"num-rx-queues": rxqueues,
},
flags=[Netlink.NLM_F_CREATE, Netlink.NLM_F_EXCL],
)
all_links = ip("-d link show", json=True)
nk_links = [
link
for link in all_links
if link.get("linkinfo", {}).get("info_kind") == "netkit"
and link["ifindex"] not in old_idxs
]
if len(nk_links) != 2:
raise KsftSkipEx("Failed to create netkit pair")
nk_links.sort(key=lambda x: x["ifindex"])
cfg.nk_host_ifname = nk_links[1]["ifname"]
cfg.nk_guest_ifname = nk_links[0]["ifname"]
cfg.nk_host_ifindex = nk_links[1]["ifindex"]
cfg.nk_guest_ifindex = nk_links[0]["ifindex"]
ip(f"link set dev {cfg.nk_guest_ifname} netns {cfg.netns.name}")
ip(f"link set dev {cfg.nk_host_ifname} up")
ip(f"-6 addr add fe80::1/64 dev {cfg.nk_host_ifname} nodad")
ip(
f"-6 route add {cfg.nk_guest_ipv6}/128 via fe80::2 "
f"dev {cfg.nk_host_ifname}"
)
ip(f"link set dev {cfg.nk_guest_ifname} up", ns=cfg.netns)
ip(f"-6 addr add fe80::2/64 dev {cfg.nk_guest_ifname}", ns=cfg.netns)
ip(
f"-6 addr add {cfg.nk_guest_ipv6}/64 dev {cfg.nk_guest_ifname} nodad",
ns=cfg.netns,
)
ip(
f"-6 route add default via fe80::1 dev {cfg.nk_guest_ifname}",
ns=cfg.netns,
)
cfg.attach_bpf()
def _setup_lease(cfg, rxqueues=2):
_create_netkit_pair(cfg, rxqueues=rxqueues)
ethnl = EthtoolFamily()
channels = ethnl.channels_get({"header": {"dev-index": cfg.ifindex}})[
"combined-count"
]
if channels < 2:
raise KsftSkipEx(
"Test requires NETIF with at least 2 combined channels"
)
src_queue = channels - 1
with NetNSEnter(str(cfg.netns)):
netdevnl = NetdevFamily()
bind_result = netdevnl.queue_create(
{
"ifindex": cfg.nk_guest_ifindex,
"type": "rx",
"lease": {
"ifindex": cfg.ifindex,
"queue": {"id": src_queue, "type": "rx"},
"netns-id": 0,
},
}
)
return src_queue, bind_result["id"]
def _teardown_netkit(cfg):
if cfg.nk_host_ifname:
cmd(f"ip link del dev {cfg.nk_host_ifname}", fail=False)
cfg.nk_host_ifname = None
cfg.nk_guest_ifname = None
def set_flow_rule(cfg, src_queue):
output = ethtool(
f"-N {cfg.ifname} flow-type tcp6 dst-port {cfg.port} action {src_queue}"
).stdout
values = re.search(r"ID (\d+)", output).group(1)
return int(values)
def test_iou_zcrx(cfg) -> None:
cfg.require_ipver("6")
src_queue, nk_queue = _setup_lease(cfg)
defer(_teardown_netkit, cfg)
ethnl = EthtoolFamily()
rings = ethnl.rings_get({"header": {"dev-index": cfg.ifindex}})
rx_rings = rings["rx"]
hds_thresh = rings.get("hds-thresh", 0)
ethnl.rings_set(
{
"header": {"dev-index": cfg.ifindex},
"tcp-data-split": "enabled",
"hds-thresh": 0,
"rx": 64,
}
)
defer(
ethnl.rings_set,
{
"header": {"dev-index": cfg.ifindex},
"tcp-data-split": "unknown",
"hds-thresh": hds_thresh,
"rx": rx_rings,
},
)
ethtool(f"-X {cfg.ifname} equal {src_queue}")
defer(ethtool, f"-X {cfg.ifname} default")
flow_rule_id = set_flow_rule(cfg, src_queue)
defer(ethtool, f"-N {cfg.ifname} delete {flow_rule_id}")
rx_cmd = (
f"{cfg.bin_local} -s -p {cfg.port} "
f"-i {cfg.nk_guest_ifname} -q {nk_queue}"
)
tx_cmd = f"{cfg.bin_remote} -c -h {cfg.nk_guest_ipv6} -p {cfg.port} -l 12840"
with bkg(rx_cmd, exit_wait=True, ns=cfg.netns):
wait_port_listen(cfg.port, proto="tcp", ns=cfg.netns)
cmd(tx_cmd, host=cfg.remote)
def test_iou_zcrx_large_buf(cfg) -> None:
"""iou-zcrx with rx_buf_len > page size, going through a netkit-leased
queue. Exercises the queue rx-buf-len path via netif_mp_open_rxq()'s
lease redirect: the netkit ifindex is opaque to io_uring, but
rx_page_size is honoured by the *physical* qops because the lease
pointer rewrites the request from netkit onto the leased physical
rxq before supported_params/validate_qcfg are consulted.
"""
cfg.require_ipver("6")
src_queue, nk_queue = _setup_lease(cfg)
defer(_teardown_netkit, cfg)
ethnl = EthtoolFamily()
with open("/proc/sys/vm/nr_hugepages", "r+", encoding="utf-8") as f:
nr_hugepages = int(f.read().strip())
if nr_hugepages < 64:
f.seek(0)
f.write("64")
defer(_restore_hugepages, nr_hugepages)
rings = ethnl.rings_get({"header": {"dev-index": cfg.ifindex}})
rx_rings = rings["rx"]
hds_thresh = rings.get("hds-thresh", 0)
ethnl.rings_set(
{
"header": {"dev-index": cfg.ifindex},
"tcp-data-split": "enabled",
"hds-thresh": 0,
"rx": 64,
}
)
defer(
ethnl.rings_set,
{
"header": {"dev-index": cfg.ifindex},
"tcp-data-split": "unknown",
"hds-thresh": hds_thresh,
"rx": rx_rings,
},
)
ethtool(f"-X {cfg.ifname} equal {src_queue}")
defer(ethtool, f"-X {cfg.ifname} default")
flow_rule_id = set_flow_rule(cfg, src_queue)
defer(ethtool, f"-N {cfg.ifname} delete {flow_rule_id}")
# -x 2 asks iou-zcrx for rx_buf_len = 2 * page_size (8 KiB on x86_64),
# backed by a 2 MiB hugepage area so the chunks are physically
# contiguous, which is what zcrx requires for non-default rx_buf_len.
rx_cmd = (
f"{cfg.bin_local} -s -p {cfg.port} "
f"-i {cfg.nk_guest_ifname} -q {nk_queue} -x 2"
)
tx_cmd = f"{cfg.bin_remote} -c -h {cfg.nk_guest_ipv6} -p {cfg.port} -l 12840"
# Probe via -d (dry run): exits with SKIP_CODE if the leased physical
# qops doesn't advertise QCFG_RX_PAGE_SIZE (e.g. older bnxt FW/HW).
probe = cmd(rx_cmd + " -d", fail=False, ns=cfg.netns)
if probe.ret == SKIP_CODE:
msg = probe.stdout.strip() or "rx_buf_len not supported by leased NIC"
raise KsftSkipEx(msg)
# A successful dry run still registered the zcrx ifq on the leased
# physical queue; wait for its async teardown before the real server
# binds the same queue.
_mp_clear_wait(cfg, src_queue)
with bkg(rx_cmd, exit_wait=True, ns=cfg.netns):
wait_port_listen(cfg.port, proto="tcp", ns=cfg.netns)
cmd(tx_cmd, host=cfg.remote)
def test_attrs(cfg) -> None:
cfg.require_ipver("6")
src_queue, nk_queue = _setup_lease(cfg)
defer(_teardown_netkit, cfg)
netdevnl = NetdevFamily()
queue_info = netdevnl.queue_get(
{"ifindex": cfg.ifindex, "id": src_queue, "type": "rx"}
)
ksft_eq(queue_info["id"], src_queue)
ksft_eq(queue_info["type"], "rx")
ksft_eq(queue_info["ifindex"], cfg.ifindex)
ksft_in("lease", queue_info)
lease = queue_info["lease"]
ksft_eq(lease["ifindex"], cfg.nk_guest_ifindex)
ksft_eq(lease["queue"]["id"], nk_queue)
ksft_eq(lease["queue"]["type"], "rx")
ksft_in("netns-id", lease)
def test_attach_xdp_with_mp(cfg) -> None:
cfg.require_ipver("6")
src_queue, nk_queue = _setup_lease(cfg)
defer(_teardown_netkit, cfg)
ethnl = EthtoolFamily()
rings = ethnl.rings_get({"header": {"dev-index": cfg.ifindex}})
rx_rings = rings["rx"]
hds_thresh = rings.get("hds-thresh", 0)
ethnl.rings_set(
{
"header": {"dev-index": cfg.ifindex},
"tcp-data-split": "enabled",
"hds-thresh": 0,
"rx": 64,
}
)
defer(
ethnl.rings_set,
{
"header": {"dev-index": cfg.ifindex},
"tcp-data-split": "unknown",
"hds-thresh": hds_thresh,
"rx": rx_rings,
},
)
ethtool(f"-X {cfg.ifname} equal {src_queue}")
defer(ethtool, f"-X {cfg.ifname} default")
netdevnl = NetdevFamily()
rx_cmd = (
f"{cfg.bin_local} -s -p {cfg.port} "
f"-i {cfg.nk_guest_ifname} -q {nk_queue}"
)
with bkg(rx_cmd, ns=cfg.netns):
wait_port_listen(cfg.port, proto="tcp", ns=cfg.netns)
time.sleep(0.1)
queue_info = netdevnl.queue_get(
{"ifindex": cfg.ifindex, "id": src_queue, "type": "rx"}
)
ksft_in("io-uring", queue_info)
prog = cfg.net_lib_dir / "xdp_dummy.bpf.o"
with ksft_raises(CmdExitFailure):
ip(f"link set dev {cfg.ifname} xdp obj {prog} sec xdp.frags")
time.sleep(0.1)
queue_info = netdevnl.queue_get(
{"ifindex": cfg.ifindex, "id": src_queue, "type": "rx"}
)
ksft_not_in("io-uring", queue_info)
def test_destroy(cfg) -> None:
cfg.require_ipver("6")
src_queue, nk_queue = _setup_lease(cfg)
defer(_teardown_netkit, cfg)
ethnl = EthtoolFamily()
rings = ethnl.rings_get({"header": {"dev-index": cfg.ifindex}})
rx_rings = rings["rx"]
hds_thresh = rings.get("hds-thresh", 0)
ethnl.rings_set(
{
"header": {"dev-index": cfg.ifindex},
"tcp-data-split": "enabled",
"hds-thresh": 0,
"rx": 64,
}
)
defer(
ethnl.rings_set,
{
"header": {"dev-index": cfg.ifindex},
"tcp-data-split": "unknown",
"hds-thresh": hds_thresh,
"rx": rx_rings,
},
)
ethtool(f"-X {cfg.ifname} equal {src_queue}")
defer(ethtool, f"-X {cfg.ifname} default")
rx_cmd = (
f"{cfg.bin_local} -s -p {cfg.port} "
f"-i {cfg.nk_guest_ifname} -q {nk_queue}"
)
rx_proc = cmd(rx_cmd, background=True, ns=cfg.netns)
wait_port_listen(cfg.port, proto="tcp", ns=cfg.netns)
netdevnl = NetdevFamily()
queue_info = netdevnl.queue_get(
{"ifindex": cfg.ifindex, "id": src_queue, "type": "rx"}
)
ksft_in("io-uring", queue_info)
# ip link del will wait for all refs to drop first, but iou-zcrx is holding
# onto a ref. Terminate iou-zcrx async via a thread after a delay.
kill_timer = threading.Timer(1, rx_proc.proc.terminate)
kill_timer.start()
ip(f"link del dev {cfg.nk_host_ifname}")
kill_timer.join()
cfg.nk_host_ifname = None
cfg.nk_guest_ifname = None
queue_info = netdevnl.queue_get(
{"ifindex": cfg.ifindex, "id": src_queue, "type": "rx"}
)
ksft_not_in("io-uring", queue_info)
flow_rule_id = set_flow_rule(cfg, src_queue)
defer(ethtool, f"-N {cfg.ifname} delete {flow_rule_id}")
rx_cmd = f"{cfg.bin_local} -s -p {cfg.port} -i {cfg.ifname} -q {src_queue}"
tx_cmd = f"{cfg.bin_remote} -c -h {cfg.addr_v['6']} -p {cfg.port} -l 12840"
with bkg(rx_cmd, exit_wait=True):
wait_port_listen(cfg.port, proto="tcp")
cmd(tx_cmd, host=cfg.remote)
# Short delay since iou cleanup is async and takes a bit of time.
time.sleep(0.1)
queue_info = netdevnl.queue_get(
{"ifindex": cfg.ifindex, "id": src_queue, "type": "rx"}
)
ksft_not_in("io-uring", queue_info)
def main() -> None:
with NetDrvContEnv(__file__, rxqueues=2) as cfg:
cfg.bin_local = path.abspath(
path.dirname(__file__) + "/../../../drivers/net/hw/iou-zcrx"
)
cfg.bin_remote = cfg.remote.deploy(cfg.bin_local)
cfg.port = rand_port()
ksft_run(
[
test_iou_zcrx,
test_iou_zcrx_large_buf,
test_attrs,
test_attach_xdp_with_mp,
test_destroy,
],
args=(cfg,),
)
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,165 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""Test ethtool NFC (ntuple) flow steering rules."""
import random
from enum import Enum, auto
from lib.py import ksft_run, ksft_exit
from lib.py import ksft_eq, ksft_ge
from lib.py import ksft_variants, KsftNamedVariant
from lib.py import EthtoolFamily, NetDrvEpEnv, NetdevFamily
from lib.py import KsftSkipEx
from lib.py import cmd, ethtool, defer, rand_ports, bkg, wait_port_listen
class NtupleField(Enum):
SRC_IP = auto()
DST_IP = auto()
SRC_PORT = auto()
DST_PORT = auto()
def _require_ntuple(cfg):
features = ethtool(f"-k {cfg.ifname}", json=True)[0]
if not features["ntuple-filters"]["active"]:
if features["ntuple-filters"]["fixed"]:
raise KsftSkipEx("Device does not support ntuple-filters")
ethtool(f"-K {cfg.ifname} ntuple-filters on")
defer(ethtool, f"-K {cfg.ifname} ntuple-filters off")
def _get_rx_cnts(cfg, prev=None):
"""Get Rx packet counts for all queues, as a simple list of integers
if @prev is specified the prev counts will be subtracted"""
cfg.wait_hw_stats_settle()
data = cfg.netdevnl.qstats_get({"ifindex": cfg.ifindex, "scope": ["queue"]}, dump=True)
data = [x for x in data if x['queue-type'] == "rx"]
max_q = max([x["queue-id"] for x in data])
queue_stats = [0] * (max_q + 1)
for q in data:
queue_stats[q["queue-id"]] = q["rx-packets"]
if prev and q["queue-id"] < len(prev):
queue_stats[q["queue-id"]] -= prev[q["queue-id"]]
return queue_stats
def _ntuple_rule_add(cfg, flow_spec):
"""Install an NFC rule via ethtool."""
output = ethtool(f"-N {cfg.ifname} {flow_spec}").stdout
rule_id = int(output.split()[-1])
defer(ethtool, f"-N {cfg.ifname} delete {rule_id}")
def _setup_isolated_queue(cfg):
"""Default all traffic to queue 0, and pick a random queue to
steer NFC traffic to."""
channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
ch_max = channels['combined-max']
qcnt = channels['combined-count']
if ch_max < 2:
raise KsftSkipEx(f"Need at least 2 combined channels, max is {ch_max}")
desired_queues = min(ch_max, 4)
if qcnt >= desired_queues:
desired_queues = qcnt
else:
ethtool(f"-L {cfg.ifname} combined {desired_queues}")
defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
ethtool(f"-X {cfg.ifname} equal 1")
defer(ethtool, f"-X {cfg.ifname} default")
return random.randint(1, desired_queues - 1)
def _send_traffic(cfg, ipver, proto, dst_port, src_port, pkt_cnt=40):
"""Generate traffic with the desired flow signature."""
cfg.require_cmd("socat", remote=True)
socat_proto = proto.upper()
dst_addr = f"[{cfg.addr_v['6']}]" if ipver == '6' else cfg.addr_v['4']
extra_opts = ",nodelay" if proto == "tcp" else ",shut-null"
listen_cmd = (f"socat -{ipver} -t 2 -u "
f"{socat_proto}-LISTEN:{dst_port},reuseport /dev/null")
with bkg(listen_cmd, exit_wait=True):
wait_port_listen(dst_port, proto=proto)
send_cmd = f"""
bash -c 'for i in $(seq {pkt_cnt}); do echo msg; sleep 0.02; done' |
socat -{ipver} -u - \
{socat_proto}:{dst_addr}:{dst_port},sourceport={src_port},reuseaddr{extra_opts}
"""
cmd(send_cmd, shell=True, host=cfg.remote)
def _add_ntuple_rule_and_send_traffic(cfg, ipver, proto, fields, test_queue):
ports = rand_ports(2)
src_port = ports[0]
dst_port = ports[1]
flow_parts = [f"flow-type {proto}{ipver}"]
for field in fields:
if field == NtupleField.SRC_IP:
flow_parts.append(f"src-ip {cfg.remote_addr_v[ipver]}")
elif field == NtupleField.DST_IP:
flow_parts.append(f"dst-ip {cfg.addr_v[ipver]}")
elif field == NtupleField.SRC_PORT:
flow_parts.append(f"src-port {src_port}")
elif field == NtupleField.DST_PORT:
flow_parts.append(f"dst-port {dst_port}")
flow_parts.append(f"action {test_queue}")
_ntuple_rule_add(cfg, " ".join(flow_parts))
_send_traffic(cfg, ipver, proto, dst_port=dst_port, src_port=src_port)
def _ntuple_variants():
for ipver in ["4", "6"]:
for proto in ["tcp", "udp"]:
for fields in [[NtupleField.SRC_IP],
[NtupleField.DST_IP],
[NtupleField.SRC_PORT],
[NtupleField.DST_PORT],
[NtupleField.SRC_IP, NtupleField.DST_IP],
[NtupleField.SRC_IP, NtupleField.DST_IP,
NtupleField.SRC_PORT, NtupleField.DST_PORT]]:
name = ".".join(f.name.lower() for f in fields)
yield KsftNamedVariant(f"{proto}{ipver}.{name}",
ipver, proto, fields)
@ksft_variants(_ntuple_variants())
def queue(cfg, ipver, proto, fields):
"""Test that an NFC rule steers traffic to the correct queue."""
cfg.require_ipver(ipver)
_require_ntuple(cfg)
test_queue = _setup_isolated_queue(cfg)
cnts = _get_rx_cnts(cfg)
_add_ntuple_rule_and_send_traffic(cfg, ipver, proto, fields, test_queue)
cnts = _get_rx_cnts(cfg, prev=cnts)
ksft_ge(cnts[test_queue], 40, f"Traffic on test queue {test_queue}: {cnts}")
sum_idle = sum(cnts) - cnts[0] - cnts[test_queue]
ksft_eq(sum_idle, 0, f"Traffic on idle queues: {cnts}")
def main() -> None:
"""Ksft boilerplate main."""
with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
cfg.ethnl = EthtoolFamily()
cfg.netdevnl = NetdevFamily()
ksft_run([queue], args=(cfg,))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""
Test driver resilience vs page pool allocation failures.
"""
import errno
import time
import math
import os
from lib.py import ksft_run, ksft_exit, ksft_pr
from lib.py import KsftSkipEx, KsftFailEx
from lib.py import NetdevFamily, NlError
from lib.py import NetDrvEpEnv
from lib.py import cmd, tool, GenerateTraffic
def _write_fail_config(config):
for key, value in config.items():
path = "/sys/kernel/debug/fail_function/"
with open(path + key, "w", encoding='ascii') as fp:
fp.write(str(value) + "\n")
def _enable_pp_allocation_fail():
if not os.path.exists("/sys/kernel/debug/fail_function"):
raise KsftSkipEx("Kernel built without function error injection (or DebugFS)")
if not os.path.exists("/sys/kernel/debug/fail_function/page_pool_alloc_netmems"):
_write_fail_config({"inject": "page_pool_alloc_netmems"})
_write_fail_config({
"verbose": 0,
"interval": 511,
"probability": 100,
"times": -1,
})
def _disable_pp_allocation_fail():
if not os.path.exists("/sys/kernel/debug/fail_function"):
return
if os.path.exists("/sys/kernel/debug/fail_function/page_pool_alloc_netmems"):
_write_fail_config({"inject": ""})
_write_fail_config({
"probability": 0,
"times": 0,
})
def test_pp_alloc(cfg, netdevnl):
"""
Configure page pool allocation fail injection while traffic is running.
"""
def get_stats():
return netdevnl.qstats_get({"ifindex": cfg.ifindex}, dump=True)[0]
def check_traffic_flowing():
stat1 = get_stats()
time.sleep(1)
stat2 = get_stats()
if stat2['rx-packets'] - stat1['rx-packets'] < 4000:
raise KsftFailEx("Traffic seems low:", stat2['rx-packets'] - stat1['rx-packets'])
try:
stats = get_stats()
except NlError as e:
if e.nl_msg.error == -errno.EOPNOTSUPP:
stats = {}
else:
raise
if 'rx-alloc-fail' not in stats:
raise KsftSkipEx("Driver does not report 'rx-alloc-fail' via qstats")
set_g = False
traffic = None
try:
traffic = GenerateTraffic(cfg)
check_traffic_flowing()
_enable_pp_allocation_fail()
s1 = get_stats()
time.sleep(3)
s2 = get_stats()
seen_fails = s2['rx-alloc-fail'] - s1['rx-alloc-fail']
if seen_fails < 1:
raise KsftSkipEx("Allocation failures not increasing")
pkts = s2['rx-packets'] - s1['rx-packets']
# Expecting one failure per 512 buffers, 3.1x safety margin
want_fails = math.floor(pkts / 512 / 3.1)
if seen_fails < want_fails:
raise KsftSkipEx("Allocation increasing too slowly", seen_fails,
"packets:", pkts)
ksft_pr(f"Seen: pkts:{pkts} fails:{seen_fails} (pass thrs:{want_fails})")
# Basic failures are fine, try to wobble some settings to catch extra failures
check_traffic_flowing()
g = tool("ethtool", "-g " + cfg.ifname, json=True)[0]
if 'rx' in g and g["rx"] * 2 <= g["rx-max"]:
new_g = g['rx'] * 2
elif 'rx' in g:
new_g = g['rx'] // 2
else:
new_g = None
if new_g:
set_g = cmd(f"ethtool -G {cfg.ifname} rx {new_g}", fail=False).ret == 0
if set_g:
ksft_pr("ethtool -G change retval: success")
else:
ksft_pr("ethtool -G change retval: did not succeed", new_g)
else:
ksft_pr("ethtool -G change retval: did not try")
time.sleep(0.1)
check_traffic_flowing()
finally:
_disable_pp_allocation_fail()
if traffic:
traffic.stop()
time.sleep(0.1)
if set_g:
cmd(f"ethtool -G {cfg.ifname} rx {g['rx']}")
def main() -> None:
""" Ksft boiler plate main """
netdevnl = NetdevFamily()
with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
ksft_run([test_pp_alloc], args=(cfg, netdevnl, ))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,476 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""
API level tests for RSS (mostly Netlink vs IOCTL).
"""
import errno
import glob
import random
from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_is, ksft_ne, ksft_raises
from lib.py import KsftSkipEx, KsftFailEx
from lib.py import defer, ethtool, CmdExitFailure
from lib.py import EthtoolFamily, NlError
from lib.py import NetDrvEnv
def _require_2qs(cfg):
qcnt = len(glob.glob(f"/sys/class/net/{cfg.ifname}/queues/rx-*"))
if qcnt < 2:
raise KsftSkipEx(f"Local has only {qcnt} queues")
return qcnt
def _ethtool_create(cfg, act, opts):
output = ethtool(f"{act} {cfg.ifname} {opts}").stdout
# Output will be something like: "New RSS context is 1" or
# "Added rule with ID 7", we want the integer from the end
return int(output.split()[-1])
def _ethtool_get_cfg(cfg, fl_type, to_nl=False):
descr = ethtool(f"-n {cfg.ifname} rx-flow-hash {fl_type}").stdout
if to_nl:
converter = {
"IP SA": "ip-src",
"IP DA": "ip-dst",
"L4 bytes 0 & 1 [TCP/UDP src port]": "l4-b-0-1",
"L4 bytes 2 & 3 [TCP/UDP dst port]": "l4-b-2-3",
}
ret = set()
else:
converter = {
"IP SA": "s",
"IP DA": "d",
"L3 proto": "t",
"L4 bytes 0 & 1 [TCP/UDP src port]": "f",
"L4 bytes 2 & 3 [TCP/UDP dst port]": "n",
}
ret = ""
for line in descr.split("\n")[1:-2]:
# if this raises we probably need to add more keys to converter above
if to_nl:
ret.add(converter[line])
else:
ret += converter[line]
return ret
def test_rxfh_nl_set_fail(cfg):
"""
Test error path of Netlink SET.
"""
_require_2qs(cfg)
ethnl = EthtoolFamily()
ethnl.ntf_subscribe("monitor")
with ksft_raises(NlError):
ethnl.rss_set({"header": {"dev-name": "lo"},
"indir": None})
with ksft_raises(NlError):
ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
"indir": [100000]})
ntf = next(ethnl.poll_ntf(duration=0.2), None)
ksft_is(ntf, None)
def test_rxfh_nl_set_indir(cfg):
"""
Test setting indirection table via Netlink.
"""
qcnt = _require_2qs(cfg)
# Test some SETs with a value
reset = defer(cfg.ethnl.rss_set,
{"header": {"dev-index": cfg.ifindex}, "indir": None})
cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
"indir": [1]})
rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
ksft_eq(set(rss.get("indir", [-1])), {1})
cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
"indir": [0, 1]})
rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
ksft_eq(set(rss.get("indir", [-1])), {0, 1})
# Make sure we can't set the queue count below max queue used
with ksft_raises(CmdExitFailure):
ethtool(f"-L {cfg.ifname} combined 0 rx 1")
with ksft_raises(CmdExitFailure):
ethtool(f"-L {cfg.ifname} combined 1 rx 0")
# Test reset back to default
reset.exec()
rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
ksft_eq(set(rss.get("indir", [-1])), set(range(qcnt)))
def test_rxfh_nl_set_indir_ctx(cfg):
"""
Test setting indirection table for a custom context via Netlink.
"""
_require_2qs(cfg)
# Get setting for ctx 0, we'll make sure they don't get clobbered
dflt = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
# Create context
ctx_id = _ethtool_create(cfg, "-X", "context new")
defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
"context": ctx_id, "indir": [1]})
rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex},
"context": ctx_id})
ksft_eq(set(rss.get("indir", [-1])), {1})
ctx0 = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
ksft_eq(ctx0, dflt)
cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
"context": ctx_id, "indir": [0, 1]})
rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex},
"context": ctx_id})
ksft_eq(set(rss.get("indir", [-1])), {0, 1})
ctx0 = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
ksft_eq(ctx0, dflt)
# Make sure we can't set the queue count below max queue used
with ksft_raises(CmdExitFailure):
ethtool(f"-L {cfg.ifname} combined 0 rx 1")
with ksft_raises(CmdExitFailure):
ethtool(f"-L {cfg.ifname} combined 1 rx 0")
def test_rxfh_indir_ntf(cfg):
"""
Check that Netlink notifications are generated when RSS indirection
table was modified.
"""
_require_2qs(cfg)
ethnl = EthtoolFamily()
ethnl.ntf_subscribe("monitor")
ethtool(f"--disable-netlink -X {cfg.ifname} weight 0 1")
reset = defer(ethtool, f"-X {cfg.ifname} default")
ntf = next(ethnl.poll_ntf(duration=0.2), None)
if ntf is None:
raise KsftFailEx("No notification received")
ksft_eq(ntf["name"], "rss-ntf")
ksft_eq(set(ntf["msg"]["indir"]), {1})
reset.exec()
ntf = next(ethnl.poll_ntf(duration=0.2), None)
if ntf is None:
raise KsftFailEx("No notification received after reset")
ksft_eq(ntf["name"], "rss-ntf")
ksft_is(ntf["msg"].get("context"), None)
ksft_ne(set(ntf["msg"]["indir"]), {1})
def test_rxfh_indir_ctx_ntf(cfg):
"""
Check that Netlink notifications are generated when RSS indirection
table was modified on an additional RSS context.
"""
_require_2qs(cfg)
ctx_id = _ethtool_create(cfg, "-X", "context new")
defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
ethnl = EthtoolFamily()
ethnl.ntf_subscribe("monitor")
ethtool(f"--disable-netlink -X {cfg.ifname} context {ctx_id} weight 0 1")
ntf = next(ethnl.poll_ntf(duration=0.2), None)
if ntf is None:
raise KsftFailEx("No notification received")
ksft_eq(ntf["name"], "rss-ntf")
ksft_eq(ntf["msg"].get("context"), ctx_id)
ksft_eq(set(ntf["msg"]["indir"]), {1})
def test_rxfh_nl_set_key(cfg):
"""
Test setting hashing key via Netlink.
"""
dflt = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
defer(cfg.ethnl.rss_set,
{"header": {"dev-index": cfg.ifindex},
"hkey": dflt["hkey"], "indir": None})
# Empty key should error out
with ksft_raises(NlError) as cm:
cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
"hkey": None})
ksft_eq(cm.exception.nl_msg.extack['bad-attr'], '.hkey')
# Set key to random
mod = random.randbytes(len(dflt["hkey"]))
cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
"hkey": mod})
rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
ksft_eq(rss.get("hkey", [-1]), mod)
# Set key to random and indir tbl to something at once
mod = random.randbytes(len(dflt["hkey"]))
cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
"indir": [0, 1], "hkey": mod})
rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
ksft_eq(rss.get("hkey", [-1]), mod)
ksft_eq(set(rss.get("indir", [-1])), {0, 1})
def test_rxfh_fields(cfg):
"""
Test reading Rx Flow Hash over Netlink.
"""
flow_types = ["tcp4", "tcp6", "udp4", "udp6"]
ethnl = EthtoolFamily()
cfg_nl = ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
for fl_type in flow_types:
one = _ethtool_get_cfg(cfg, fl_type, to_nl=True)
ksft_eq(one, cfg_nl["flow-hash"][fl_type],
comment="Config for " + fl_type)
def test_rxfh_fields_set(cfg):
""" Test configuring Rx Flow Hash over Netlink. """
flow_types = ["tcp4", "tcp6", "udp4", "udp6"]
# Collect current settings
cfg_old = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
# symmetric hashing is config-order-sensitive make sure we leave
# symmetric mode, or make the flow-hash sym-compatible first
changes = [{"flow-hash": cfg_old["flow-hash"],},
{"input-xfrm": cfg_old.get("input-xfrm", {}),}]
if cfg_old.get("input-xfrm"):
changes = list(reversed(changes))
for old in changes:
defer(cfg.ethnl.rss_set, {"header": {"dev-index": cfg.ifindex},} | old)
# symmetric hashing prevents some of the configs below
if cfg_old.get("input-xfrm"):
cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
"input-xfrm": {}})
for fl_type in flow_types:
cur = _ethtool_get_cfg(cfg, fl_type)
if cur == "sdfn":
change_nl = {"ip-src", "ip-dst"}
change_ic = "sd"
else:
change_nl = {"l4-b-0-1", "l4-b-2-3", "ip-src", "ip-dst"}
change_ic = "sdfn"
cfg.ethnl.rss_set({
"header": {"dev-index": cfg.ifindex},
"flow-hash": {fl_type: change_nl}
})
reset = defer(ethtool, f"--disable-netlink -N {cfg.ifname} "
f"rx-flow-hash {fl_type} {cur}")
cfg_nl = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
ksft_eq(change_nl, cfg_nl["flow-hash"][fl_type],
comment=f"Config for {fl_type} over Netlink")
cfg_ic = _ethtool_get_cfg(cfg, fl_type)
ksft_eq(change_ic, cfg_ic,
comment=f"Config for {fl_type} over IOCTL")
reset.exec()
cfg_nl = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
ksft_eq(cfg_old["flow-hash"][fl_type], cfg_nl["flow-hash"][fl_type],
comment=f"Un-config for {fl_type} over Netlink")
cfg_ic = _ethtool_get_cfg(cfg, fl_type)
ksft_eq(cur, cfg_ic, comment=f"Un-config for {fl_type} over IOCTL")
# Try to set multiple at once, the defer was already installed at the start
change = {"ip-src"}
if change == cfg_old["flow-hash"]["tcp4"]:
change = {"ip-dst"}
cfg.ethnl.rss_set({
"header": {"dev-index": cfg.ifindex},
"flow-hash": {x: change for x in flow_types}
})
cfg_nl = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
for fl_type in flow_types:
ksft_eq(change, cfg_nl["flow-hash"][fl_type],
comment=f"multi-config for {fl_type} over Netlink")
def test_rxfh_fields_set_xfrm(cfg):
""" Test changing Rx Flow Hash vs xfrm_input at once. """
def set_rss(cfg, xfrm, fh):
cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
"input-xfrm": xfrm, "flow-hash": fh})
# Install the reset handler
cfg_old = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
# symmetric hashing is config-order-sensitive make sure we leave
# symmetric mode, or make the flow-hash sym-compatible first
changes = [{"flow-hash": cfg_old["flow-hash"],},
{"input-xfrm": cfg_old.get("input-xfrm", {}),}]
if cfg_old.get("input-xfrm"):
changes = list(reversed(changes))
for old in changes:
defer(cfg.ethnl.rss_set, {"header": {"dev-index": cfg.ifindex},} | old)
# Make sure we start with input-xfrm off, and tcp4 config non-sym
set_rss(cfg, {}, {})
set_rss(cfg, {}, {"tcp4": {"ip-src"}})
# Setting sym and fixing tcp4 config not expected to pass right now
with ksft_raises(NlError):
set_rss(cfg, {"sym-xor"}, {"tcp4": {"ip-src", "ip-dst"}})
# One at a time should work, hopefully
set_rss(cfg, 0, {"tcp4": {"ip-src", "ip-dst"}})
no_support = False
try:
set_rss(cfg, {"sym-xor"}, {})
except NlError:
try:
set_rss(cfg, {"sym-or-xor"}, {})
except NlError:
no_support = True
if no_support:
raise KsftSkipEx("no input-xfrm supported")
# Disabling two at once should not work either without kernel changes
with ksft_raises(NlError):
set_rss(cfg, {}, {"tcp4": {"ip-src"}})
def test_rxfh_fields_ntf(cfg):
""" Test Rx Flow Hash notifications. """
cur = _ethtool_get_cfg(cfg, "tcp4")
if cur == "sdfn":
change = {"ip-src", "ip-dst"}
else:
change = {"l4-b-0-1", "l4-b-2-3", "ip-src", "ip-dst"}
ethnl = EthtoolFamily()
ethnl.ntf_subscribe("monitor")
ethnl.rss_set({
"header": {"dev-index": cfg.ifindex},
"flow-hash": {"tcp4": change}
})
reset = defer(ethtool,
f"--disable-netlink -N {cfg.ifname} rx-flow-hash tcp4 {cur}")
ntf = next(ethnl.poll_ntf(duration=0.2), None)
if ntf is None:
raise KsftFailEx("No notification received after IOCTL change")
ksft_eq(ntf["name"], "rss-ntf")
ksft_eq(ntf["msg"]["flow-hash"]["tcp4"], change)
ksft_eq(next(ethnl.poll_ntf(duration=0.01), None), None)
reset.exec()
ntf = next(ethnl.poll_ntf(duration=0.2), None)
if ntf is None:
raise KsftFailEx("No notification received after Netlink change")
ksft_eq(ntf["name"], "rss-ntf")
ksft_ne(ntf["msg"]["flow-hash"]["tcp4"], change)
ksft_eq(next(ethnl.poll_ntf(duration=0.01), None), None)
def test_rss_ctx_add(cfg):
""" Test creating an additional RSS context via Netlink """
_require_2qs(cfg)
# Test basic creation
ctx = cfg.ethnl.rss_create_act({"header": {"dev-index": cfg.ifindex}})
d = defer(ethtool, f"-X {cfg.ifname} context {ctx.get('context')} delete")
ksft_ne(ctx.get("context", 0), 0)
ksft_ne(set(ctx.get("indir", [0])), {0},
comment="Driver should init the indirection table")
# Try requesting the ID we just got allocated
with ksft_raises(NlError) as cm:
ctx = cfg.ethnl.rss_create_act({
"header": {"dev-index": cfg.ifindex},
"context": ctx.get("context"),
})
ethtool(f"-X {cfg.ifname} context {ctx.get('context')} delete")
d.exec()
ksft_eq(cm.exception.nl_msg.error, -errno.EBUSY)
# Test creating with a specified RSS table, and context ID
ctx_id = ctx.get("context")
ctx = cfg.ethnl.rss_create_act({
"header": {"dev-index": cfg.ifindex},
"context": ctx_id,
"indir": [1],
})
ethtool(f"-X {cfg.ifname} context {ctx.get('context')} delete")
ksft_eq(ctx.get("context"), ctx_id)
ksft_eq(set(ctx.get("indir", [0])), {1})
def test_rss_ctx_ntf(cfg):
""" Test notifications for creating additional RSS contexts """
ethnl = EthtoolFamily()
ethnl.ntf_subscribe("monitor")
# Create / delete via Netlink
ctx = cfg.ethnl.rss_create_act({"header": {"dev-index": cfg.ifindex}})
cfg.ethnl.rss_delete_act({
"header": {"dev-index": cfg.ifindex},
"context": ctx["context"],
})
ntf = next(ethnl.poll_ntf(duration=0.2), None)
if ntf is None:
raise KsftFailEx("[NL] No notification after context creation")
ksft_eq(ntf["name"], "rss-create-ntf")
ksft_eq(ctx, ntf["msg"])
ntf = next(ethnl.poll_ntf(duration=0.2), None)
if ntf is None:
raise KsftFailEx("[NL] No notification after context deletion")
ksft_eq(ntf["name"], "rss-delete-ntf")
# Create / deleve via IOCTL
ctx_id = _ethtool_create(cfg, "--disable-netlink -X", "context new")
ethtool(f"--disable-netlink -X {cfg.ifname} context {ctx_id} delete")
ntf = next(ethnl.poll_ntf(duration=0.2), None)
if ntf is None:
raise KsftFailEx("[IOCTL] No notification after context creation")
ksft_eq(ntf["name"], "rss-create-ntf")
ntf = next(ethnl.poll_ntf(duration=0.2), None)
if ntf is None:
raise KsftFailEx("[IOCTL] No notification after context deletion")
ksft_eq(ntf["name"], "rss-delete-ntf")
def main() -> None:
""" Ksft boiler plate main """
with NetDrvEnv(__file__, nsim_test=False) as cfg:
cfg.ethnl = EthtoolFamily()
ksft_run(globs=globals(), case_pfx={"test_"}, args=(cfg, ))
ksft_exit()
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,313 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""
Driver-related behavior tests for RSS.
"""
from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_ge
from lib.py import ksft_variants, KsftNamedVariant, KsftSkipEx, ksft_raises
from lib.py import defer, ethtool, CmdExitFailure
from lib.py import EthtoolFamily, NlError
from lib.py import NetDrvEnv
def _is_power_of_two(n):
return n > 0 and (n & (n - 1)) == 0
def _get_rss(cfg, context=0):
return ethtool(f"-x {cfg.ifname} context {context}", json=True)[0]
def _test_rss_indir_size(cfg, qcnt, context=0):
"""Test that indirection table size is at least 4x queue count."""
ethtool(f"-L {cfg.ifname} combined {qcnt}")
rss = _get_rss(cfg, context=context)
indir = rss['rss-indirection-table']
ksft_ge(len(indir), 4 * qcnt, "Table smaller than 4x")
return len(indir)
def _maybe_create_context(cfg, create_context):
""" Either create a context and return its ID or return 0 for main ctx """
if not create_context:
return 0
try:
ctx = cfg.ethnl.rss_create_act({'header': {'dev-index': cfg.ifindex}})
ctx_id = ctx['context']
defer(cfg.ethnl.rss_delete_act,
{'header': {'dev-index': cfg.ifindex}, 'context': ctx_id})
except NlError:
raise KsftSkipEx("Device does not support additional RSS contexts")
return ctx_id
def _require_dynamic_indir_size(cfg, ch_max):
"""Skip if the device does not dynamically size its indirection table."""
ethtool(f"-X {cfg.ifname} default")
ethtool(f"-L {cfg.ifname} combined 2")
small = len(_get_rss(cfg)['rss-indirection-table'])
ethtool(f"-L {cfg.ifname} combined {ch_max}")
large = len(_get_rss(cfg)['rss-indirection-table'])
if small == large:
raise KsftSkipEx("Device does not dynamically size indirection table")
@ksft_variants([
KsftNamedVariant("main", False),
KsftNamedVariant("ctx", True),
])
def indir_size_4x(cfg, create_context):
"""
Test that the indirection table has at least 4 entries per queue.
Empirically network-heavy workloads like memcache suffer with the 33%
imbalance of a 2x indirection table size.
4x table translates to a 16% imbalance.
"""
channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
ch_max = channels.get('combined-max', 0)
qcnt = channels['combined-count']
if ch_max < 3:
raise KsftSkipEx(f"Not enough queues for the test: max={ch_max}")
defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
ethtool(f"-L {cfg.ifname} combined 3")
ctx_id = _maybe_create_context(cfg, create_context)
indir_sz = _test_rss_indir_size(cfg, 3, context=ctx_id)
# Test with max queue count (max - 1 if max is a power of two)
test_max = ch_max - 1 if _is_power_of_two(ch_max) else ch_max
if test_max > 3 and indir_sz < test_max * 4:
_test_rss_indir_size(cfg, test_max, context=ctx_id)
@ksft_variants([
KsftNamedVariant("main", False),
KsftNamedVariant("ctx", True),
])
def resize_periodic(cfg, create_context):
"""Test that a periodic indirection table survives channel changes.
Set a non-default periodic table ([3, 2, 1, 0] x N) via netlink,
reduce channels to trigger a fold, then increase to trigger an
unfold. Using a reversed pattern (instead of [0, 1, 2, 3]) ensures
the test can distinguish a correct fold from a driver that silently
resets the table to defaults. Verify the exact pattern is preserved
and the size tracks the channel count.
"""
channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
ch_max = channels.get('combined-max', 0)
qcnt = channels['combined-count']
if ch_max < 4:
raise KsftSkipEx(f"Not enough queues for the test: max={ch_max}")
defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
_require_dynamic_indir_size(cfg, ch_max)
ctx_id = _maybe_create_context(cfg, create_context)
# Set a non-default periodic pattern via netlink.
# Send only 4 entries (user_size=4) so the kernel replicates it
# to fill the device table. This allows folding down to 4 entries.
rss = _get_rss(cfg, context=ctx_id)
orig_size = len(rss['rss-indirection-table'])
pattern = [3, 2, 1, 0]
req = {'header': {'dev-index': cfg.ifindex}, 'indir': pattern}
if ctx_id:
req['context'] = ctx_id
else:
defer(ethtool, f"-X {cfg.ifname} default")
cfg.ethnl.rss_set(req)
# Shrink — should fold
ethtool(f"-L {cfg.ifname} combined 4")
rss = _get_rss(cfg, context=ctx_id)
indir = rss['rss-indirection-table']
ksft_ge(orig_size, len(indir), "Table did not shrink")
ksft_eq(indir, [3, 2, 1, 0] * (len(indir) // 4),
"Folded table has wrong pattern")
# Grow back — should unfold
ethtool(f"-L {cfg.ifname} combined {ch_max}")
rss = _get_rss(cfg, context=ctx_id)
indir = rss['rss-indirection-table']
ksft_eq(len(indir), orig_size, "Table size not restored")
ksft_eq(indir, [3, 2, 1, 0] * (len(indir) // 4),
"Unfolded table has wrong pattern")
@ksft_variants([
KsftNamedVariant("main", False),
KsftNamedVariant("ctx", True),
])
def resize_below_user_size_reject(cfg, create_context):
"""Test that shrinking below user_size is rejected.
Send a table via netlink whose size (user_size) sits between
the small and large device table sizes. The table is periodic,
so folding would normally succeed, but the user_size floor must
prevent it.
"""
channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
ch_max = channels.get('combined-max', 0)
qcnt = channels['combined-count']
if ch_max < 4:
raise KsftSkipEx(f"Not enough queues for the test: max={ch_max}")
defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
_require_dynamic_indir_size(cfg, ch_max)
ctx_id = _maybe_create_context(cfg, create_context)
# Measure the table size at max channels
rss = _get_rss(cfg, context=ctx_id)
big_size = len(rss['rss-indirection-table'])
# Measure the table size at reduced channels
ethtool(f"-L {cfg.ifname} combined 4")
rss = _get_rss(cfg, context=ctx_id)
small_size = len(rss['rss-indirection-table'])
ethtool(f"-L {cfg.ifname} combined {ch_max}")
if small_size >= big_size:
raise KsftSkipEx("Table did not shrink at reduced channels")
# Find a user_size
user_size = None
for div in [2, 4]:
candidate = big_size // div
if candidate > small_size and big_size % candidate == 0:
user_size = candidate
break
if user_size is None:
raise KsftSkipEx("No suitable user_size between small and big table")
# Send a periodic sub-table of exactly user_size entries.
# Pattern safe for 4 channels.
pattern = [0, 1, 2, 3] * (user_size // 4)
if len(pattern) != user_size:
raise KsftSkipEx(f"user_size ({user_size}) not divisible by 4")
req = {'header': {'dev-index': cfg.ifindex}, 'indir': pattern}
if ctx_id:
req['context'] = ctx_id
else:
defer(ethtool, f"-X {cfg.ifname} default")
cfg.ethnl.rss_set(req)
# Shrink channels — table would go to small_size < user_size.
# The table is periodic so folding would work, but user_size
# floor must reject it.
with ksft_raises(CmdExitFailure):
ethtool(f"-L {cfg.ifname} combined 4")
@ksft_variants([
KsftNamedVariant("main", False),
KsftNamedVariant("ctx", True),
])
def resize_nonperiodic_reject(cfg, create_context):
"""Test that a non-periodic table blocks channel reduction.
Set equal weight across all queues so the table is not periodic
at any smaller size, then verify channel reduction is rejected.
An additional context with a periodic table is created to verify
that validation catches the non-periodic one even when others
are fine.
"""
channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
ch_max = channels.get('combined-max', 0)
qcnt = channels['combined-count']
if ch_max < 4:
raise KsftSkipEx(f"Not enough queues for the test: max={ch_max}")
defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
_require_dynamic_indir_size(cfg, ch_max)
ctx_id = _maybe_create_context(cfg, create_context)
ctx_ref = f"context {ctx_id}" if ctx_id else ""
# Create an extra context with a periodic (foldable) table so that
# the validation must iterate all contexts to find the bad one.
extra_ctx = _maybe_create_context(cfg, True)
ethtool(f"-X {cfg.ifname} context {extra_ctx} equal 2")
ethtool(f"-X {cfg.ifname} {ctx_ref} equal {ch_max}")
if not create_context:
defer(ethtool, f"-X {cfg.ifname} default")
with ksft_raises(CmdExitFailure):
ethtool(f"-L {cfg.ifname} combined 2")
@ksft_variants([
KsftNamedVariant("main", False),
KsftNamedVariant("ctx", True),
])
def resize_nonperiodic_no_corruption(cfg, create_context):
"""Test that a failed resize does not corrupt table or channel count.
Set a non-periodic table, attempt a channel reduction (which must
fail), then verify both the indirection table contents and the
channel count are unchanged.
"""
channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
ch_max = channels.get('combined-max', 0)
qcnt = channels['combined-count']
if ch_max < 4:
raise KsftSkipEx(f"Not enough queues for the test: max={ch_max}")
defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
_require_dynamic_indir_size(cfg, ch_max)
ctx_id = _maybe_create_context(cfg, create_context)
ctx_ref = f"context {ctx_id}" if ctx_id else ""
ethtool(f"-X {cfg.ifname} {ctx_ref} equal {ch_max}")
if not create_context:
defer(ethtool, f"-X {cfg.ifname} default")
rss_before = _get_rss(cfg, context=ctx_id)
with ksft_raises(CmdExitFailure):
ethtool(f"-L {cfg.ifname} combined 2")
rss_after = _get_rss(cfg, context=ctx_id)
ksft_eq(rss_after['rss-indirection-table'],
rss_before['rss-indirection-table'],
"Indirection table corrupted after failed resize")
channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
ksft_eq(channels['combined-count'], ch_max,
"Channel count changed after failed resize")
def main() -> None:
""" Ksft boiler plate main """
with NetDrvEnv(__file__) as cfg:
cfg.ethnl = EthtoolFamily()
ksft_run([indir_size_4x, resize_periodic,
resize_below_user_size_reject,
resize_nonperiodic_reject,
resize_nonperiodic_no_corruption], args=(cfg, ))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,172 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""
Tests for RSS hashing on IPv6 Flow Label.
"""
import glob
import os
import socket
from lib.py import CmdExitFailure
from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_ge, ksft_in, \
ksft_not_in, ksft_raises, KsftSkipEx
from lib.py import bkg, cmd, defer, fd_read_timeout, rand_port
from lib.py import NetDrvEpEnv
def _check_system(cfg):
if not hasattr(socket, "SO_INCOMING_CPU"):
raise KsftSkipEx("socket.SO_INCOMING_CPU was added in Python 3.11")
qcnt = len(glob.glob(f"/sys/class/net/{cfg.ifname}/queues/rx-*"))
if qcnt < 2:
raise KsftSkipEx(f"Local has only {qcnt} queues")
for f in [f"/sys/class/net/{cfg.ifname}/queues/rx-0/rps_flow_cnt",
f"/sys/class/net/{cfg.ifname}/queues/rx-0/rps_cpus"]:
try:
with open(f, 'r') as fp:
setting = fp.read().strip()
# CPU mask will be zeros and commas
if setting.replace("0", "").replace(",", ""):
raise KsftSkipEx(f"RPS/RFS is configured: {f}: {setting}")
except FileNotFoundError:
pass
# 1 is the default, if someone changed it we probably shouldn"t mess with it
af = cmd("cat /proc/sys/net/ipv6/auto_flowlabels", host=cfg.remote).stdout
if af.strip() != "1":
raise KsftSkipEx("Remote does not have auto_flowlabels enabled")
def _ethtool_get_cfg(cfg, fl_type):
descr = cmd(f"ethtool -n {cfg.ifname} rx-flow-hash {fl_type}").stdout
converter = {
"IP SA": "s",
"IP DA": "d",
"L3 proto": "t",
"L4 bytes 0 & 1 [TCP/UDP src port]": "f",
"L4 bytes 2 & 3 [TCP/UDP dst port]": "n",
"IPv6 Flow Label": "l",
}
ret = ""
for line in descr.split("\n")[1:-2]:
# if this raises we probably need to add more keys to converter above
ret += converter[line]
return ret
def _traffic(cfg, one_sock, one_cpu):
local_port = rand_port(socket.SOCK_DGRAM)
remote_port = rand_port(socket.SOCK_DGRAM)
sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
sock.bind(("", local_port))
sock.connect((cfg.remote_addr_v["6"], 0))
if one_sock:
send = f"exec 5<>/dev/udp/{cfg.addr_v['6']}/{local_port}; " \
"for i in `seq 20`; do echo a >&5; sleep 0.02; done; exec 5>&-"
else:
send = "for i in `seq 20`; do echo a | socat -t0.02 - UDP6:" \
f"[{cfg.addr_v['6']}]:{local_port},sourceport={remote_port}; done"
cpus = set()
with bkg(send, shell=True, host=cfg.remote, exit_wait=True):
for _ in range(20):
fd_read_timeout(sock.fileno(), 1)
cpu = sock.getsockopt(socket.SOL_SOCKET, socket.SO_INCOMING_CPU)
cpus.add(cpu)
if one_cpu:
ksft_eq(len(cpus), 1,
f"{one_sock=} - expected one CPU, got traffic on: {cpus=}")
else:
ksft_ge(len(cpus), 2,
f"{one_sock=} - expected many CPUs, got traffic on: {cpus=}")
def test_rss_flow_label(cfg):
"""
Test hashing on IPv6 flow label. Send traffic over a single socket
and over multiple sockets. Depend on the remote having auto-label
enabled so that it randomizes the label per socket.
"""
cfg.require_ipver("6")
cfg.require_cmd("socat", remote=True)
_check_system(cfg)
# Enable flow label hashing for UDP6
initial = _ethtool_get_cfg(cfg, "udp6")
no_lbl = initial.replace("l", "")
if "l" not in initial:
try:
cmd(f"ethtool -N {cfg.ifname} rx-flow-hash udp6 l{no_lbl}")
except CmdExitFailure as exc:
raise KsftSkipEx("Device doesn't support Flow Label for UDP6") from exc
defer(cmd, f"ethtool -N {cfg.ifname} rx-flow-hash udp6 {initial}")
_traffic(cfg, one_sock=True, one_cpu=True)
_traffic(cfg, one_sock=False, one_cpu=False)
# Disable it, we should see no hashing (reset was already defer()ed)
cmd(f"ethtool -N {cfg.ifname} rx-flow-hash udp6 {no_lbl}")
_traffic(cfg, one_sock=False, one_cpu=True)
def _check_v4_flow_types(cfg):
for fl_type in ["tcp4", "udp4", "ah4", "esp4", "sctp4"]:
try:
cur = cmd(f"ethtool -n {cfg.ifname} rx-flow-hash {fl_type}").stdout
ksft_not_in("Flow Label", cur,
comment=f"{fl_type=} has Flow Label:" + cur)
except CmdExitFailure:
# Probably does not support this flow type
pass
def test_rss_flow_label_6only(cfg):
"""
Test interactions with IPv4 flow types. It should not be possible to set
IPv6 Flow Label hashing for an IPv4 flow type. The Flow Label should also
not appear in the IPv4 "current config".
"""
with ksft_raises(CmdExitFailure) as cm:
cmd(f"ethtool -N {cfg.ifname} rx-flow-hash tcp4 sdfnl")
ksft_in("Invalid argument", cm.exception.cmd.stderr)
_check_v4_flow_types(cfg)
# Try to enable Flow Labels and check again, in case it leaks thru
initial = _ethtool_get_cfg(cfg, "udp6")
no_lbl = initial.replace("l", "")
if "l" not in initial:
try:
cmd(f"ethtool -N {cfg.ifname} rx-flow-hash udp6 l{no_lbl}")
except CmdExitFailure as exc:
raise KsftSkipEx("Device doesn't support Flow Label for UDP6") from exc
else:
cmd(f"ethtool -N {cfg.ifname} rx-flow-hash udp6 {no_lbl}")
restore = defer(cmd, f"ethtool -N {cfg.ifname} rx-flow-hash udp6 {initial}")
_check_v4_flow_types(cfg)
restore.exec()
_check_v4_flow_types(cfg)
def main() -> None:
with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
ksft_run([test_rss_flow_label,
test_rss_flow_label_6only],
args=(cfg, ))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
import multiprocessing
import socket
from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_ge, cmd, fd_read_timeout
from lib.py import NetDrvEpEnv
from lib.py import EthtoolFamily, NetdevFamily, NlError
from lib.py import KsftSkipEx, KsftFailEx
from lib.py import defer, ksft_pr, rand_port
def traffic(cfg, local_port, remote_port, ipver):
af_inet = socket.AF_INET if ipver == "4" else socket.AF_INET6
sock = socket.socket(af_inet, socket.SOCK_DGRAM)
sock.bind(("", local_port))
sock.connect((cfg.remote_addr_v[ipver], remote_port))
tgt = f"{ipver}:[{cfg.addr_v[ipver]}]:{local_port},sourceport={remote_port}"
cmd("echo a | socat - UDP" + tgt, host=cfg.remote)
fd_read_timeout(sock.fileno(), 5)
return sock.getsockopt(socket.SOL_SOCKET, socket.SO_INCOMING_CPU)
def _rss_input_xfrm_try_enable(cfg):
"""
Check if symmetric input-xfrm is already enabled, if not try to enable it
and register a cleanup.
"""
rss = cfg.ethnl.rss_get({'header': {'dev-name': cfg.ifname}})
orig_xfrm = rss.get('input-xfrm', set())
sym_xfrm = set(filter(lambda x: 'sym' in x, orig_xfrm))
if sym_xfrm:
ksft_pr("Sym input xfrm already enabled:", sym_xfrm)
return sym_xfrm
for xfrm in cfg.ethnl.consts["input-xfrm"].entries:
# Skip non-symmetric transforms
if "sym" not in xfrm:
continue
try_xfrm = {xfrm} | orig_xfrm
try:
cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
"input-xfrm": try_xfrm})
except NlError:
continue
ksft_pr("Sym input xfrm configured:", try_xfrm)
defer(cfg.ethnl.rss_set,
{"header": {"dev-index": cfg.ifindex},
"input-xfrm": orig_xfrm})
return {xfrm}
return set()
def test_rss_input_xfrm(cfg, ipver):
"""
Test symmetric input_xfrm.
If symmetric RSS hash is configured, send traffic twice, swapping the
src/dst UDP ports, and verify that the same queue is receiving the traffic
in both cases (IPs are constant).
"""
if multiprocessing.cpu_count() < 2:
raise KsftSkipEx("Need at least two CPUs to test symmetric RSS hash")
cfg.require_cmd("socat", local=False, remote=True)
if not hasattr(socket, "SO_INCOMING_CPU"):
raise KsftSkipEx("socket.SO_INCOMING_CPU was added in Python 3.11")
# Check for symmetric xor/or-xor
input_xfrm = _rss_input_xfrm_try_enable(cfg)
if not input_xfrm:
raise KsftSkipEx("Symmetric RSS hash not supported by device")
cpus = set()
successful = 0
for _ in range(100):
try:
port1 = rand_port(socket.SOCK_DGRAM)
port2 = rand_port(socket.SOCK_DGRAM)
cpu1 = traffic(cfg, port1, port2, ipver)
cpu2 = traffic(cfg, port2, port1, ipver)
cpus.update([cpu1, cpu2])
ksft_eq(
cpu1, cpu2, comment=f"Received traffic on different cpus with ports ({port1 = }, {port2 = }) while symmetric hash is configured")
successful += 1
if successful == 10:
break
except:
continue
else:
raise KsftFailEx("Failed to run traffic")
ksft_ge(len(cpus), 2,
comment=f"Received traffic on less than two cpus {cpus = }")
def test_rss_input_xfrm_ipv4(cfg):
cfg.require_ipver("4")
test_rss_input_xfrm(cfg, "4")
def test_rss_input_xfrm_ipv6(cfg):
cfg.require_ipver("6")
test_rss_input_xfrm(cfg, "6")
def main() -> None:
with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
cfg.ethnl = EthtoolFamily()
cfg.netdevnl = NetdevFamily()
ksft_run([test_rss_input_xfrm_ipv4, test_rss_input_xfrm_ipv6],
args=(cfg, ))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1 @@
timeout=0
@@ -0,0 +1,673 @@
// SPDX-License-Identifier: GPL-2.0
/* Toeplitz test
*
* 1. Read packets and their rx_hash using PF_PACKET/TPACKET_V3
* 2. Compute the rx_hash in software based on the packet contents
* 3. Compare the two
*
* Optionally, either '-C $rx_irq_cpu_list' or '-r $rps_bitmap' may be given.
*
* If '-C $rx_irq_cpu_list' is given, also
*
* 4. Identify the cpu on which the packet arrived with PACKET_FANOUT_CPU
* 5. Compute the rxqueue that RSS would select based on this rx_hash
* 6. Using the $rx_irq_cpu_list map, identify the arriving cpu based on rxq irq
* 7. Compare the cpus from 4 and 6
*
* Else if '-r $rps_bitmap' is given, also
*
* 4. Identify the cpu on which the packet arrived with PACKET_FANOUT_CPU
* 5. Compute the cpu that RPS should select based on rx_hash and $rps_bitmap
* 6. Compare the cpus from 4 and 5
*/
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <error.h>
#include <fcntl.h>
#include <getopt.h>
#include <linux/filter.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <net/if.h>
#include <netdb.h>
#include <netinet/ip.h>
#include <netinet/ip6.h>
#include <netinet/tcp.h>
#include <netinet/udp.h>
#include <poll.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/sysinfo.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <ynl.h>
#include "ethtool-user.h"
#include "kselftest.h"
#include "../../../net/lib/ksft.h"
#define TOEPLITZ_KEY_MIN_LEN 40
#define TOEPLITZ_KEY_MAX_LEN 256
#define TOEPLITZ_STR_LEN(K) (((K) * 3) - 1) /* hex encoded: AA:BB:CC:...:ZZ */
#define TOEPLITZ_STR_MIN_LEN TOEPLITZ_STR_LEN(TOEPLITZ_KEY_MIN_LEN)
#define TOEPLITZ_STR_MAX_LEN TOEPLITZ_STR_LEN(TOEPLITZ_KEY_MAX_LEN)
#define FOUR_TUPLE_MAX_LEN ((sizeof(struct in6_addr) * 2) + (sizeof(uint16_t) * 2))
#define RSS_MAX_CPUS (1 << 16) /* real constraint is PACKET_FANOUT_MAX */
#define RSS_MAX_INDIR (1 << 16)
#define RPS_MAX_CPUS 16UL /* must be a power of 2 */
#define MIN_PKT_SAMPLES 40 /* minimum number of packets to receive */
/* configuration options (cmdline arguments) */
static uint16_t cfg_dport = 8000;
static int cfg_family = AF_INET6;
static char *cfg_ifname = "eth0";
static int cfg_num_queues;
static int cfg_num_rps_cpus;
static bool cfg_sink;
static int cfg_type = SOCK_STREAM;
static int cfg_timeout_msec = 1000;
static bool cfg_verbose;
/* global vars */
static int num_cpus;
static int ring_block_nr;
static int ring_block_sz;
/* stats */
static int frames_received;
static int frames_nohash;
static int frames_error;
#define log_verbose(args...) do { if (cfg_verbose) fprintf(stderr, args); } while (0)
/* tpacket ring */
struct ring_state {
int fd;
char *mmap;
int idx;
int cpu;
};
static unsigned int rx_irq_cpus[RSS_MAX_CPUS]; /* map from rxq to cpu */
static int rps_silo_to_cpu[RPS_MAX_CPUS];
static unsigned char toeplitz_key[TOEPLITZ_KEY_MAX_LEN];
static unsigned int rss_indir_tbl[RSS_MAX_INDIR];
static unsigned int rss_indir_tbl_size;
static struct ring_state rings[RSS_MAX_CPUS];
static inline uint32_t toeplitz(const unsigned char *four_tuple,
const unsigned char *key)
{
int i, bit, ret = 0;
uint32_t key32;
key32 = ntohl(*((uint32_t *)key));
key += 4;
for (i = 0; i < FOUR_TUPLE_MAX_LEN; i++) {
for (bit = 7; bit >= 0; bit--) {
if (four_tuple[i] & (1 << bit))
ret ^= key32;
key32 <<= 1;
key32 |= !!(key[0] & (1 << bit));
}
key++;
}
return ret;
}
/* Compare computed cpu with arrival cpu from packet_fanout_cpu */
static void verify_rss(uint32_t rx_hash, int cpu)
{
int queue;
if (rss_indir_tbl_size)
queue = rss_indir_tbl[rx_hash % rss_indir_tbl_size];
else
queue = rx_hash % cfg_num_queues;
log_verbose(" rxq %d (cpu %d)", queue, rx_irq_cpus[queue]);
if (rx_irq_cpus[queue] != cpu) {
log_verbose(". error: rss cpu mismatch (%d)", cpu);
frames_error++;
}
}
static void verify_rps(uint64_t rx_hash, int cpu)
{
int silo = (rx_hash * cfg_num_rps_cpus) >> 32;
log_verbose(" silo %d (cpu %d)", silo, rps_silo_to_cpu[silo]);
if (rps_silo_to_cpu[silo] != cpu) {
log_verbose(". error: rps cpu mismatch (%d)", cpu);
frames_error++;
}
}
static void log_rxhash(int cpu, uint32_t rx_hash,
const char *addrs, int addr_len)
{
char saddr[INET6_ADDRSTRLEN], daddr[INET6_ADDRSTRLEN];
uint16_t *ports;
if (!inet_ntop(cfg_family, addrs, saddr, sizeof(saddr)) ||
!inet_ntop(cfg_family, addrs + addr_len, daddr, sizeof(daddr)))
error(1, 0, "address parse error");
ports = (void *)addrs + (addr_len * 2);
log_verbose("cpu %d: rx_hash 0x%08x [saddr %s daddr %s sport %02hu dport %02hu]",
cpu, rx_hash, saddr, daddr,
ntohs(ports[0]), ntohs(ports[1]));
}
/* Compare computed rxhash with rxhash received from tpacket_v3 */
static void verify_rxhash(const char *pkt, uint32_t rx_hash, int cpu)
{
unsigned char four_tuple[FOUR_TUPLE_MAX_LEN] = {0};
uint32_t rx_hash_sw;
const char *addrs;
int addr_len;
if (cfg_family == AF_INET) {
addr_len = sizeof(struct in_addr);
addrs = pkt + offsetof(struct iphdr, saddr);
} else {
addr_len = sizeof(struct in6_addr);
addrs = pkt + offsetof(struct ip6_hdr, ip6_src);
}
memcpy(four_tuple, addrs, (addr_len * 2) + (sizeof(uint16_t) * 2));
rx_hash_sw = toeplitz(four_tuple, toeplitz_key);
if (cfg_verbose)
log_rxhash(cpu, rx_hash, addrs, addr_len);
if (rx_hash != rx_hash_sw) {
log_verbose(" != expected 0x%x\n", rx_hash_sw);
frames_error++;
return;
}
log_verbose(" OK");
if (cfg_num_queues)
verify_rss(rx_hash, cpu);
else if (cfg_num_rps_cpus)
verify_rps(rx_hash, cpu);
log_verbose("\n");
}
static char *recv_frame(const struct ring_state *ring, char *frame)
{
struct tpacket3_hdr *hdr = (void *)frame;
if (hdr->hv1.tp_rxhash)
verify_rxhash(frame + hdr->tp_net, hdr->hv1.tp_rxhash,
ring->cpu);
else
frames_nohash++;
return frame + hdr->tp_next_offset;
}
/* A single TPACKET_V3 block can hold multiple frames */
static bool recv_block(struct ring_state *ring)
{
struct tpacket_block_desc *block;
char *frame;
int i;
block = (void *)(ring->mmap + ring->idx * ring_block_sz);
if (!(block->hdr.bh1.block_status & TP_STATUS_USER))
return false;
frame = (char *)block;
frame += block->hdr.bh1.offset_to_first_pkt;
for (i = 0; i < block->hdr.bh1.num_pkts; i++) {
frame = recv_frame(ring, frame);
frames_received++;
}
block->hdr.bh1.block_status = TP_STATUS_KERNEL;
ring->idx = (ring->idx + 1) % ring_block_nr;
return true;
}
/* simple test: process all rings until MIN_PKT_SAMPLES packets are received,
* or the test times out.
*/
static void process_rings(void)
{
struct timeval start, now;
bool pkts_found = true;
long elapsed_usec;
int i;
gettimeofday(&start, NULL);
do {
if (!pkts_found)
usleep(100);
pkts_found = false;
for (i = 0; i < num_cpus; i++)
pkts_found |= recv_block(&rings[i]);
gettimeofday(&now, NULL);
elapsed_usec = (now.tv_sec - start.tv_sec) * 1000000 +
(now.tv_usec - start.tv_usec);
} while (frames_received - frames_nohash < MIN_PKT_SAMPLES &&
elapsed_usec < cfg_timeout_msec * 1000);
fprintf(stderr, "count: pass=%u nohash=%u fail=%u\n",
frames_received - frames_nohash - frames_error,
frames_nohash, frames_error);
}
static char *setup_ring(int fd)
{
struct tpacket_req3 req3 = {0};
void *ring;
req3.tp_retire_blk_tov = cfg_timeout_msec / 8;
req3.tp_feature_req_word = TP_FT_REQ_FILL_RXHASH;
req3.tp_frame_size = 2048;
req3.tp_frame_nr = 1 << 10;
req3.tp_block_nr = 16;
req3.tp_block_size = req3.tp_frame_size * req3.tp_frame_nr;
req3.tp_block_size /= req3.tp_block_nr;
if (setsockopt(fd, SOL_PACKET, PACKET_RX_RING, &req3, sizeof(req3)))
error(1, errno, "setsockopt PACKET_RX_RING");
ring_block_sz = req3.tp_block_size;
ring_block_nr = req3.tp_block_nr;
ring = mmap(0, req3.tp_block_size * req3.tp_block_nr,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_LOCKED | MAP_POPULATE, fd, 0);
if (ring == MAP_FAILED)
error(1, 0, "mmap failed");
return ring;
}
static void __set_filter(int fd, int off_proto, uint8_t proto, int off_dport)
{
struct sock_filter filter[] = {
BPF_STMT(BPF_LD + BPF_B + BPF_ABS, SKF_AD_OFF + SKF_AD_PKTTYPE),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, PACKET_HOST, 0, 4),
BPF_STMT(BPF_LD + BPF_B + BPF_ABS, off_proto),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, proto, 0, 2),
BPF_STMT(BPF_LD + BPF_H + BPF_ABS, off_dport),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, cfg_dport, 1, 0),
BPF_STMT(BPF_RET + BPF_K, 0),
BPF_STMT(BPF_RET + BPF_K, 0xFFFF),
};
struct sock_fprog prog = {};
prog.filter = filter;
prog.len = ARRAY_SIZE(filter);
if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &prog, sizeof(prog)))
error(1, errno, "setsockopt filter");
}
/* filter on transport protocol and destination port */
static void set_filter(int fd)
{
const int off_dport = offsetof(struct tcphdr, dest); /* same for udp */
uint8_t proto;
proto = cfg_type == SOCK_STREAM ? IPPROTO_TCP : IPPROTO_UDP;
if (cfg_family == AF_INET)
__set_filter(fd, offsetof(struct iphdr, protocol), proto,
sizeof(struct iphdr) + off_dport);
else
__set_filter(fd, offsetof(struct ip6_hdr, ip6_nxt), proto,
sizeof(struct ip6_hdr) + off_dport);
}
/* drop everything: used temporarily during setup */
static void set_filter_null(int fd)
{
struct sock_filter filter[] = {
BPF_STMT(BPF_RET + BPF_K, 0),
};
struct sock_fprog prog = {};
prog.filter = filter;
prog.len = ARRAY_SIZE(filter);
if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &prog, sizeof(prog)))
error(1, errno, "setsockopt filter");
}
static int create_ring(char **ring)
{
struct fanout_args args = {
.id = 1,
.type_flags = PACKET_FANOUT_CPU,
.max_num_members = RSS_MAX_CPUS
};
struct sockaddr_ll ll = { 0 };
int fd, val;
fd = socket(PF_PACKET, SOCK_DGRAM, 0);
if (fd == -1)
error(1, errno, "socket creation failed");
val = TPACKET_V3;
if (setsockopt(fd, SOL_PACKET, PACKET_VERSION, &val, sizeof(val)))
error(1, errno, "setsockopt PACKET_VERSION");
*ring = setup_ring(fd);
/* block packets until all rings are added to the fanout group:
* else packets can arrive during setup and get misclassified
*/
set_filter_null(fd);
ll.sll_family = AF_PACKET;
ll.sll_ifindex = if_nametoindex(cfg_ifname);
ll.sll_protocol = cfg_family == AF_INET ? htons(ETH_P_IP) :
htons(ETH_P_IPV6);
if (bind(fd, (void *)&ll, sizeof(ll)))
error(1, errno, "bind");
/* must come after bind: verifies all programs in group match */
if (setsockopt(fd, SOL_PACKET, PACKET_FANOUT, &args, sizeof(args))) {
/* on failure, retry using old API if that is sufficient:
* it has a hard limit of 256 sockets, so only try if
* (a) only testing rxhash, not RSS or (b) <= 256 cpus.
* in this API, the third argument is left implicit.
*/
if (cfg_num_queues || num_cpus > 256 ||
setsockopt(fd, SOL_PACKET, PACKET_FANOUT,
&args, sizeof(uint32_t)))
error(1, errno, "setsockopt PACKET_FANOUT cpu");
}
return fd;
}
/* setup inet(6) socket to blackhole the test traffic, if arg '-s' */
static int setup_sink(void)
{
int fd, val;
fd = socket(cfg_family, cfg_type, 0);
if (fd == -1)
error(1, errno, "socket %d.%d", cfg_family, cfg_type);
val = 1 << 20;
if (setsockopt(fd, SOL_SOCKET, SO_RCVBUFFORCE, &val, sizeof(val)))
error(1, errno, "setsockopt rcvbuf");
return fd;
}
static void setup_rings(void)
{
int i;
for (i = 0; i < num_cpus; i++) {
rings[i].cpu = i;
rings[i].fd = create_ring(&rings[i].mmap);
}
/* accept packets once all rings in the fanout group are up */
for (i = 0; i < num_cpus; i++)
set_filter(rings[i].fd);
}
static void cleanup_rings(void)
{
int i;
for (i = 0; i < num_cpus; i++) {
if (munmap(rings[i].mmap, ring_block_nr * ring_block_sz))
error(1, errno, "munmap");
if (close(rings[i].fd))
error(1, errno, "close");
}
}
static void parse_cpulist(const char *arg)
{
do {
rx_irq_cpus[cfg_num_queues++] = strtol(arg, NULL, 10);
arg = strchr(arg, ',');
if (!arg)
break;
arg++; // skip ','
} while (1);
}
static void show_cpulist(void)
{
int i;
for (i = 0; i < cfg_num_queues; i++)
fprintf(stderr, "rxq %d: cpu %d\n", i, rx_irq_cpus[i]);
}
static void show_silos(void)
{
int i;
for (i = 0; i < cfg_num_rps_cpus; i++)
fprintf(stderr, "silo %d: cpu %d\n", i, rps_silo_to_cpu[i]);
}
static void parse_toeplitz_key(const char *str, int slen, unsigned char *key)
{
int i, ret, off;
if (slen < TOEPLITZ_STR_MIN_LEN ||
slen > TOEPLITZ_STR_MAX_LEN + 1)
error(1, 0, "invalid toeplitz key");
for (i = 0, off = 0; off < slen; i++, off += 3) {
ret = sscanf(str + off, "%hhx", &key[i]);
if (ret != 1)
error(1, 0, "key parse error at %d off %d len %d",
i, off, slen);
}
}
static void parse_rps_bitmap(const char *arg)
{
unsigned long bitmap;
int i;
bitmap = strtoul(arg, NULL, 0);
if (bitmap & ~((1UL << RPS_MAX_CPUS) - 1))
error(1, 0, "rps bitmap 0x%lx out of bounds, max cpu %lu",
bitmap, RPS_MAX_CPUS - 1);
for (i = 0; i < RPS_MAX_CPUS; i++)
if (bitmap & 1UL << i)
rps_silo_to_cpu[cfg_num_rps_cpus++] = i;
}
static void read_rss_dev_info_ynl(void)
{
struct ethtool_rss_get_req *req;
struct ethtool_rss_get_rsp *rsp;
struct ynl_sock *ys;
ys = ynl_sock_create(&ynl_ethtool_family, NULL);
if (!ys)
error(1, errno, "ynl_sock_create failed");
req = ethtool_rss_get_req_alloc();
if (!req)
error(1, errno, "ethtool_rss_get_req_alloc failed");
ethtool_rss_get_req_set_header_dev_name(req, cfg_ifname);
rsp = ethtool_rss_get(ys, req);
if (!rsp)
error(1, ys->err.code, "YNL: %s", ys->err.msg);
if (!rsp->_len.hkey)
error(1, 0, "RSS key not available for %s", cfg_ifname);
if (rsp->_len.hkey < TOEPLITZ_KEY_MIN_LEN ||
rsp->_len.hkey > TOEPLITZ_KEY_MAX_LEN)
error(1, 0, "RSS key length %u out of bounds [%u, %u]",
rsp->_len.hkey, TOEPLITZ_KEY_MIN_LEN,
TOEPLITZ_KEY_MAX_LEN);
memcpy(toeplitz_key, rsp->hkey, rsp->_len.hkey);
if (rsp->_count.indir > RSS_MAX_INDIR)
error(1, 0, "RSS indirection table too large (%u > %u)",
rsp->_count.indir, RSS_MAX_INDIR);
/* If indir table not available we'll fallback to simple modulo math */
if (rsp->_count.indir) {
memcpy(rss_indir_tbl, rsp->indir,
rsp->_count.indir * sizeof(rss_indir_tbl[0]));
rss_indir_tbl_size = rsp->_count.indir;
log_verbose("RSS indirection table size: %u\n",
rss_indir_tbl_size);
}
ethtool_rss_get_rsp_free(rsp);
ethtool_rss_get_req_free(req);
ynl_sock_destroy(ys);
}
static void parse_opts(int argc, char **argv)
{
static struct option long_options[] = {
{"dport", required_argument, 0, 'd'},
{"cpus", required_argument, 0, 'C'},
{"key", required_argument, 0, 'k'},
{"iface", required_argument, 0, 'i'},
{"ipv4", no_argument, 0, '4'},
{"ipv6", no_argument, 0, '6'},
{"sink", no_argument, 0, 's'},
{"tcp", no_argument, 0, 't'},
{"timeout", required_argument, 0, 'T'},
{"udp", no_argument, 0, 'u'},
{"verbose", no_argument, 0, 'v'},
{"rps", required_argument, 0, 'r'},
{0, 0, 0, 0}
};
bool have_toeplitz = false;
int index, c;
while ((c = getopt_long(argc, argv, "46C:d:i:k:r:stT:uv", long_options, &index)) != -1) {
switch (c) {
case '4':
cfg_family = AF_INET;
break;
case '6':
cfg_family = AF_INET6;
break;
case 'C':
parse_cpulist(optarg);
break;
case 'd':
cfg_dport = strtol(optarg, NULL, 0);
break;
case 'i':
cfg_ifname = optarg;
break;
case 'k':
parse_toeplitz_key(optarg, strlen(optarg),
toeplitz_key);
have_toeplitz = true;
break;
case 'r':
parse_rps_bitmap(optarg);
break;
case 's':
cfg_sink = true;
break;
case 't':
cfg_type = SOCK_STREAM;
break;
case 'T':
cfg_timeout_msec = strtol(optarg, NULL, 0);
break;
case 'u':
cfg_type = SOCK_DGRAM;
break;
case 'v':
cfg_verbose = true;
break;
default:
error(1, 0, "unknown option %c", optopt);
break;
}
}
if (!have_toeplitz)
read_rss_dev_info_ynl();
num_cpus = get_nprocs();
if (num_cpus > RSS_MAX_CPUS)
error(1, 0, "increase RSS_MAX_CPUS");
if (cfg_num_queues && cfg_num_rps_cpus)
error(1, 0,
"Can't supply both RSS cpus ('-C') and RPS map ('-r')");
if (cfg_verbose) {
show_cpulist();
show_silos();
}
}
int main(int argc, char **argv)
{
const int min_tests = 10;
int fd_sink = -1;
parse_opts(argc, argv);
if (cfg_sink)
fd_sink = setup_sink();
setup_rings();
/* Signal to test framework that we're ready to receive */
ksft_ready();
process_rings();
cleanup_rings();
if (cfg_sink && close(fd_sink))
error(1, errno, "close sink");
if (frames_received - frames_nohash < min_tests)
error(1, 0, "too few frames for verification");
return frames_error;
}
@@ -0,0 +1,216 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""
Toeplitz Rx hashing test:
- rxhash (the hash value calculation itself);
- RSS mapping from rxhash to rx queue;
- RPS mapping from rxhash to cpu.
"""
import glob
import os
import socket
from lib.py import ksft_run, ksft_exit, ksft_pr
from lib.py import NetDrvEpEnv, EthtoolFamily, NetdevFamily
from lib.py import cmd, bkg, rand_port, defer
from lib.py import ksft_in
from lib.py import ksft_variants, KsftNamedVariant, KsftSkipEx, KsftFailEx
# "define" for the ID of the Toeplitz hash function
ETH_RSS_HASH_TOP = 1
# Must match RPS_MAX_CPUS in toeplitz.c
RPS_MAX_CPUS = 16
def _check_rps_and_rfs_not_configured(cfg):
"""Verify that RPS is not already configured."""
for rps_file in glob.glob(f"/sys/class/net/{cfg.ifname}/queues/rx-*/rps_cpus"):
with open(rps_file, "r", encoding="utf-8") as fp:
val = fp.read().strip()
if set(val) - {"0", ","}:
raise KsftSkipEx(f"RPS already configured on {rps_file}: {val}")
rfs_file = "/proc/sys/net/core/rps_sock_flow_entries"
with open(rfs_file, "r", encoding="utf-8") as fp:
val = fp.read().strip()
if val != "0":
raise KsftSkipEx(f"RFS already configured {rfs_file}: {val}")
def _get_cpu_for_irq(irq):
with open(f"/proc/irq/{irq}/smp_affinity_list", "r",
encoding="utf-8") as fp:
data = fp.read().strip()
if "," in data or "-" in data:
raise KsftFailEx(f"IRQ{irq} is not mapped to a single core: {data}")
return int(data)
def _get_irq_cpus(cfg):
"""
Read the list of IRQs for the device Rx queues.
"""
queues = cfg.netnl.queue_get({"ifindex": cfg.ifindex}, dump=True)
napis = cfg.netnl.napi_get({"ifindex": cfg.ifindex}, dump=True)
# Remap into ID-based dicts
napis = {n["id"]: n for n in napis}
queues = {f"{q['type']}{q['id']}": q for q in queues}
cpus = []
for rx in range(9999):
name = f"rx{rx}"
if name not in queues:
break
cpus.append(_get_cpu_for_irq(napis[queues[name]["napi-id"]]["irq"]))
return cpus
def _get_unused_rps_cpus(cfg, count=2):
"""
Get CPUs that are not used by Rx queues for RPS.
Returns a list of at least 'count' CPU numbers within
the RPS_MAX_CPUS supported range.
"""
# Get CPUs used by Rx queues
rx_cpus = set(_get_irq_cpus(cfg))
# Get total number of CPUs, capped by RPS_MAX_CPUS
num_cpus = min(os.cpu_count(), RPS_MAX_CPUS)
# Find unused CPUs
unused_cpus = [cpu for cpu in range(num_cpus) if cpu not in rx_cpus]
if len(unused_cpus) < count:
raise KsftSkipEx(f"Need at least {count} CPUs in range 0..{num_cpus - 1} not used by Rx queues, found {len(unused_cpus)}")
return unused_cpus[:count]
def _configure_rps(cfg, rps_cpus):
"""Configure RPS for all Rx queues."""
mask = 0
for cpu in rps_cpus:
mask |= (1 << cpu)
mask = hex(mask)
# Set RPS bitmap for all rx queues
for rps_file in glob.glob(f"/sys/class/net/{cfg.ifname}/queues/rx-*/rps_cpus"):
with open(rps_file, "w", encoding="utf-8") as fp:
# sysfs expects hex without '0x' prefix, toeplitz.c needs the prefix
fp.write(mask[2:])
return mask
def _send_traffic(cfg, proto_flag, ipver, port):
"""Send 20 packets of requested type."""
# Determine protocol and IP version for socat
if proto_flag == "-u":
proto = "UDP"
else:
proto = "TCP"
baddr = f"[{cfg.addr_v['6']}]" if ipver == "6" else cfg.addr_v["4"]
# Run socat in a loop to send traffic periodically
# Use sh -c with a loop similar to toeplitz_client.sh
socat_cmd = f"""
for i in `seq 20`; do
echo "msg $i" | socat -{ipver} -t 0.1 - {proto}:{baddr}:{port};
sleep 0.001;
done
"""
cmd(socat_cmd, shell=True, host=cfg.remote)
def _test_variants():
for grp in ["", "rss", "rps"]:
for l4 in ["tcp", "udp"]:
for l3 in ["4", "6"]:
name = f"{l4}_ipv{l3}"
if grp:
name = f"{grp}_{name}"
yield KsftNamedVariant(name, "-" + l4[0], l3, grp)
@ksft_variants(_test_variants())
def test(cfg, proto_flag, ipver, grp):
"""Run a single toeplitz test."""
cfg.require_ipver(ipver)
# Check that rxhash is enabled
ksft_in("receive-hashing: on", cmd(f"ethtool -k {cfg.ifname}").stdout)
rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
# Make sure NIC is configured to use Toeplitz hash, and no key xfrm.
if rss.get('hfunc') != ETH_RSS_HASH_TOP or rss.get('input-xfrm'):
cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
"hfunc": ETH_RSS_HASH_TOP,
"input-xfrm": {}})
defer(cfg.ethnl.rss_set, {"header": {"dev-index": cfg.ifindex},
"hfunc": rss.get('hfunc'),
"input-xfrm": rss.get('input-xfrm', {})
})
port = rand_port(socket.SOCK_DGRAM)
toeplitz_path = cfg.test_dir / "toeplitz"
rx_cmd = [
str(toeplitz_path),
"-" + ipver,
proto_flag,
"-d", str(port),
"-i", cfg.ifname,
"-T", "4000",
"-s",
"-v"
]
if grp:
_check_rps_and_rfs_not_configured(cfg)
if grp == "rss":
irq_cpus = ",".join([str(x) for x in _get_irq_cpus(cfg)])
rx_cmd += ["-C", irq_cpus]
ksft_pr(f"RSS using CPUs: {irq_cpus}")
elif grp == "rps":
# Get CPUs not used by Rx queues and configure them for RPS
rps_cpus = _get_unused_rps_cpus(cfg, count=2)
rps_mask = _configure_rps(cfg, rps_cpus)
defer(_configure_rps, cfg, [])
rx_cmd += ["-r", rps_mask]
ksft_pr(f"RPS using CPUs: {rps_cpus}, mask: {rps_mask}")
# Run rx in background, it will exit once it has seen enough packets
with bkg(" ".join(rx_cmd), ksft_ready=True, exit_wait=True) as rx_proc:
while rx_proc.proc.poll() is None:
_send_traffic(cfg, proto_flag, ipver, port)
# Check rx result
ksft_pr("Receiver output:")
ksft_pr(rx_proc.stdout.strip().replace('\n', '\n# '))
if rx_proc.stderr:
ksft_pr(rx_proc.stderr.strip().replace('\n', '\n# '))
def main() -> None:
"""Ksft boilerplate main."""
with NetDrvEpEnv(__file__) as cfg:
cfg.ethnl = EthtoolFamily()
cfg.netnl = NetdevFamily()
ksft_run(cases=[test], args=(cfg,))
ksft_exit()
if __name__ == "__main__":
main()
+263
View File
@@ -0,0 +1,263 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""Run the tools/testing/selftests/net/csum testsuite."""
import fcntl
import socket
import struct
import termios
import time
from lib.py import ksft_pr, ksft_run, ksft_exit, KsftSkipEx, KsftXfailEx
from lib.py import ksft_eq, ksft_ge, ksft_lt
from lib.py import EthtoolFamily, NetdevFamily, NetDrvEpEnv
from lib.py import bkg, cmd, defer, ethtool, ip, rand_port, wait_port_listen
def sock_wait_drain(sock, max_wait=1000):
"""Wait for all pending write data on the socket to get ACKed."""
for _ in range(max_wait):
one = b'\0' * 4
outq = fcntl.ioctl(sock.fileno(), termios.TIOCOUTQ, one)
outq = struct.unpack("I", outq)[0]
if outq == 0:
break
time.sleep(0.01)
ksft_eq(outq, 0)
def tcp_sock_get_retrans(sock):
"""Get the number of retransmissions for the TCP socket."""
info = sock.getsockopt(socket.SOL_TCP, socket.TCP_INFO, 512)
return struct.unpack("I", info[100:104])[0]
def run_one_stream(cfg, ipver, remote_v4, remote_v6, should_lso):
cfg.require_cmd("socat", local=False, remote=True)
# Set recv window clamp to avoid overwhelming receiver on debug kernels
# the 200k clamp should still let use reach > 15Gbps on real HW
port = rand_port()
listen_opts = f"{port},reuseport,tcp-window-clamp=200000"
listen_cmd = f"socat -{ipver} -t 2 -u TCP-LISTEN:{listen_opts} /dev/null,ignoreeof"
with bkg(listen_cmd, host=cfg.remote, exit_wait=True) as nc:
wait_port_listen(port, host=cfg.remote)
if ipver == "4":
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((remote_v4, port))
else:
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
sock.connect((remote_v6, port))
# Small send to make sure the connection is working.
sock.send("ping".encode())
sock_wait_drain(sock)
# Send 4MB of data, record the LSO packet count.
qstat_old = cfg.netnl.qstats_get({"ifindex": cfg.ifindex}, dump=True)[0]
buf = b"0" * 1024 * 1024 * 4
sock.send(buf)
sock_wait_drain(sock)
qstat_new = cfg.netnl.qstats_get({"ifindex": cfg.ifindex}, dump=True)[0]
# Check that at least 90% of the data was sent as LSO packets.
# System noise may cause false negatives. Also header overheads
# will add up to 5% of extra packes... The check is best effort.
total_lso_wire = len(buf) * 0.90 // cfg.dev["mtu"]
total_lso_super = len(buf) * 0.90 // cfg.dev["tso_max_size"]
# Make sure we have order of magnitude more LSO packets than
# retransmits, in case TCP retransmitted all the LSO packets.
ksft_lt(tcp_sock_get_retrans(sock), total_lso_wire / 16)
sock.close()
if should_lso:
if cfg.have_stat_super_count:
ksft_ge(qstat_new['tx-hw-gso-packets'] -
qstat_old['tx-hw-gso-packets'],
total_lso_super,
comment="Number of LSO super-packets with LSO enabled")
if cfg.have_stat_wire_count:
ksft_ge(qstat_new['tx-hw-gso-wire-packets'] -
qstat_old['tx-hw-gso-wire-packets'],
total_lso_wire,
comment="Number of LSO wire-packets with LSO enabled")
else:
if cfg.have_stat_super_count:
ksft_lt(qstat_new['tx-hw-gso-packets'] -
qstat_old['tx-hw-gso-packets'],
15, comment="Number of LSO super-packets with LSO disabled")
if cfg.have_stat_wire_count:
ksft_lt(qstat_new['tx-hw-gso-wire-packets'] -
qstat_old['tx-hw-gso-wire-packets'],
500, comment="Number of LSO wire-packets with LSO disabled")
def build_tunnel(cfg, outer_ipver, tun_info):
local_v4 = NetDrvEpEnv.nsim_v4_pfx + "1"
local_v6 = NetDrvEpEnv.nsim_v6_pfx + "1"
remote_v4 = NetDrvEpEnv.nsim_v4_pfx + "2"
remote_v6 = NetDrvEpEnv.nsim_v6_pfx + "2"
local_addr = cfg.addr_v[outer_ipver]
remote_addr = cfg.remote_addr_v[outer_ipver]
tun_type = tun_info[0]
tun_arg = tun_info[1]
ip(f"link add {tun_type}-ksft type {tun_type} {tun_arg} local {local_addr} remote {remote_addr} dev {cfg.ifname}")
defer(ip, f"link del {tun_type}-ksft")
ip(f"link set dev {tun_type}-ksft up")
ip(f"addr add {local_v4}/24 dev {tun_type}-ksft")
ip(f"addr add {local_v6}/64 dev {tun_type}-ksft")
ip(f"link add {tun_type}-ksft type {tun_type} {tun_arg} local {remote_addr} remote {local_addr} dev {cfg.remote_ifname}",
host=cfg.remote)
defer(ip, f"link del {tun_type}-ksft", host=cfg.remote)
ip(f"link set dev {tun_type}-ksft up", host=cfg.remote)
ip(f"addr add {remote_v4}/24 dev {tun_type}-ksft", host=cfg.remote)
ip(f"addr add {remote_v6}/64 dev {tun_type}-ksft", host=cfg.remote)
return remote_v4, remote_v6
def restore_wanted_features(cfg):
features_cmd = ""
for feature in cfg.hw_features:
setting = "on" if feature in cfg.wanted_features else "off"
features_cmd += f" {feature} {setting}"
try:
ethtool(f"-K {cfg.ifname} {features_cmd}")
except Exception as e:
ksft_pr(f"WARNING: failure restoring wanted features: {e}")
def test_builder(name, cfg, outer_ipver, feature, tun=None, inner_ipver=None):
"""Construct specific tests from the common template."""
def f(cfg):
cfg.require_ipver(outer_ipver)
defer(restore_wanted_features, cfg)
if not cfg.have_stat_super_count and \
not cfg.have_stat_wire_count:
raise KsftSkipEx(f"Device does not support LSO queue stats")
if feature not in cfg.hw_features:
raise KsftSkipEx(f"Device does not support {feature}")
ipver = outer_ipver
if tun:
remote_v4, remote_v6 = build_tunnel(cfg, ipver, tun)
ipver = inner_ipver
else:
remote_v4 = cfg.remote_addr_v["4"]
remote_v6 = cfg.remote_addr_v["6"]
# First test without the feature enabled.
ethtool(f"-K {cfg.ifname} {feature} off")
run_one_stream(cfg, ipver, remote_v4, remote_v6, should_lso=False)
ethtool(f"-K {cfg.ifname} tx-gso-partial off")
ethtool(f"-K {cfg.ifname} tx-tcp-mangleid-segmentation off")
if feature in cfg.partial_features:
ethtool(f"-K {cfg.ifname} tx-gso-partial on")
if ipver == "4":
ksft_pr("Testing with mangleid enabled")
ethtool(f"-K {cfg.ifname} tx-tcp-mangleid-segmentation on")
# Full feature enabled.
ethtool(f"-K {cfg.ifname} {feature} on")
run_one_stream(cfg, ipver, remote_v4, remote_v6, should_lso=True)
f.__name__ = name + ((outer_ipver + "_") if tun else "") + "ipv" + inner_ipver
return f
def query_nic_features(cfg) -> None:
"""Query and cache the NIC features."""
cfg.have_stat_super_count = False
cfg.have_stat_wire_count = False
features = cfg.ethnl.features_get({"header": {"dev-index": cfg.ifindex}})
cfg.wanted_features = set()
for f in features["wanted"]["bits"]["bit"]:
cfg.wanted_features.add(f["name"])
cfg.hw_features = set()
for f in features["hw"]["bits"]["bit"]:
if f.get("value", False):
cfg.hw_features.add(f["name"])
# Check which features are supported via GSO partial
cfg.partial_features = set()
if 'tx-gso-partial' in cfg.hw_features:
seg_features = {f for f in cfg.hw_features if "segmentation" in f}
ethtool(f"-K {cfg.ifname} " +
" ".join(f"{f} on" for f in seg_features))
ethtool(f"-K {cfg.ifname} tx-gso-partial off")
no_partial = set()
features = cfg.ethnl.features_get({"header": {"dev-index": cfg.ifindex}})
for f in features["active"]["bits"]["bit"]:
no_partial.add(f["name"])
cfg.partial_features = seg_features - no_partial
ethtool(f"-K {cfg.ifname} tx-gso-partial on")
restore_wanted_features(cfg)
stats = cfg.netnl.qstats_get({"ifindex": cfg.ifindex}, dump=True)
if stats:
if 'tx-hw-gso-packets' in stats[0]:
ksft_pr("Detected qstat for LSO super-packets")
cfg.have_stat_super_count = True
if 'tx-hw-gso-wire-packets' in stats[0]:
ksft_pr("Detected qstat for LSO wire-packets")
cfg.have_stat_wire_count = True
def main() -> None:
with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
cfg.ethnl = EthtoolFamily()
cfg.netnl = NetdevFamily()
query_nic_features(cfg)
test_info = (
# name, v4/v6 ethtool_feature tun:(type, args, inner ip versions)
("", "4", "tx-tcp-segmentation", None),
("", "6", "tx-tcp6-segmentation", None),
("vxlan", "4", "tx-udp_tnl-segmentation", ("vxlan", "id 100 dstport 4789 noudpcsum", ("4", "6"))),
("vxlan", "6", "tx-udp_tnl-segmentation", ("vxlan", "id 100 dstport 4789 udp6zerocsumtx udp6zerocsumrx", ("4", "6"))),
("vxlan_csum", "", "tx-udp_tnl-csum-segmentation", ("vxlan", "id 100 dstport 4789 udpcsum", ("4", "6"))),
("gre", "4", "tx-gre-segmentation", ("gre", "", ("4", "6"))),
("gre", "6", "tx-gre-segmentation", ("ip6gre","", ("4", "6"))),
("ip", "6", "tx-ipxip6-segmentation", ("ip6tnl","mode any", ("4", "6"))),
("ip", "4", "tx-ipxip4-segmentation", ("sit","", ("6", ))),
("ip", "4", "tx-ipxip4-segmentation", ("ipip","", ("4", ))),
)
cases = []
for outer_ipver in ["4", "6"]:
for info in test_info:
# Skip if test which only works for a specific IP version
if info[1] and outer_ipver != info[1]:
continue
if info[3]:
cases += [
test_builder(info[0], cfg, outer_ipver, info[2], info[3], inner_ipver)
for inner_ipver in info[3][2]
]
else:
cases.append(test_builder(info[0], cfg, outer_ipver, info[2], None, outer_ipver))
ksft_run(cases=cases, args=(cfg, ))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""
Devmem tests for non-init userns.
"""
import os
from devmem_lib import run_rx, run_rx_hds, run_tx, run_tx_chunks, setup_test
from lib.py import NetDrvContEnv, ksft_disruptive, ksft_exit, ksft_run
@ksft_disruptive
def check_userns_rx(cfg) -> None:
"""Run the devmem RX test through non-init userns netkit."""
run_rx(cfg)
@ksft_disruptive
def check_userns_tx(cfg) -> None:
"""Run the devmem TX test through non-init userns netkit."""
run_tx(cfg)
@ksft_disruptive
def check_userns_tx_chunks(cfg) -> None:
"""Run the devmem TX chunking test through non-init userns netkit."""
run_tx_chunks(cfg)
def check_userns_rx_hds(cfg) -> None:
"""Run the HDS test through non-init userns netkit."""
run_rx_hds(cfg)
def main() -> None:
"""Run userns devmem RX selftests against the test environment."""
with NetDrvContEnv(__file__, userns=True, rxqueues=2,
primary_rx_redirect=True) as cfg:
setup_test(cfg,
os.path.join(os.path.dirname(os.path.abspath(__file__)),
"ncdevmem"))
ksft_run([check_userns_rx, check_userns_tx, check_userns_tx_chunks,
check_userns_rx_hds], args=(cfg,))
ksft_exit()
if __name__ == "__main__":
main()
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""Test USO
Sends large UDP datagrams with UDP_SEGMENT and verifies that the peer
receives the expected total payload and that the NIC transmitted at least
the expected number of segments.
"""
import random
import socket
import string
from lib.py import ksft_run, ksft_exit, KsftSkipEx
from lib.py import ksft_eq, ksft_ge, ksft_variants, KsftNamedVariant
from lib.py import NetDrvEpEnv
from lib.py import bkg, defer, ethtool, ip, rand_port, wait_port_listen
# python doesn't expose this constant, so we need to hardcode it to enable UDP
# segmentation for large payloads
UDP_SEGMENT = 103
def _send_uso(cfg, ipver, mss, total_payload, port):
if ipver == "4":
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
dst = (cfg.remote_addr_v["4"], port)
else:
sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
dst = (cfg.remote_addr_v["6"], port)
sock.setsockopt(socket.IPPROTO_UDP, UDP_SEGMENT, mss)
payload = ''.join(random.choice(string.ascii_lowercase)
for _ in range(total_payload))
sock.sendto(payload.encode(), dst)
sock.close()
def _get_tx_packets(cfg):
stats = ip(f"-s link show dev {cfg.ifname}", json=True)[0]
return stats['stats64']['tx']['packets']
def _test_uso(cfg, ipver, mss, total_payload):
cfg.require_ipver(ipver)
cfg.require_cmd("socat", remote=True)
features = ethtool(f"-k {cfg.ifname}", json=True)
uso_was_on = features[0]["tx-udp-segmentation"]["active"]
try:
ethtool(f"-K {cfg.ifname} tx-udp-segmentation on")
except Exception as exc:
raise KsftSkipEx(
"Device does not support tx-udp-segmentation") from exc
if not uso_was_on:
defer(ethtool, f"-K {cfg.ifname} tx-udp-segmentation off")
expected_segs = (total_payload + mss - 1) // mss
port = rand_port(stype=socket.SOCK_DGRAM)
rx_cmd = f"socat -{ipver} -T 2 -u UDP-LISTEN:{port},reuseport STDOUT"
tx_before = _get_tx_packets(cfg)
with bkg(rx_cmd, host=cfg.remote, exit_wait=True) as rx:
wait_port_listen(port, proto="udp", host=cfg.remote)
_send_uso(cfg, ipver, mss, total_payload, port)
ksft_eq(len(rx.stdout), total_payload,
comment=f"Received {len(rx.stdout)}B, expected {total_payload}B")
cfg.wait_hw_stats_settle()
tx_after = _get_tx_packets(cfg)
tx_delta = tx_after - tx_before
ksft_ge(tx_delta, expected_segs,
comment=f"Expected >= {expected_segs} tx packets, got {tx_delta}")
def _uso_variants():
for ipver in ["4", "6"]:
yield KsftNamedVariant(f"v{ipver}_partial", ipver, 1400, 1400 * 10 + 500)
yield KsftNamedVariant(f"v{ipver}_exact", ipver, 1400, 1400 * 5)
@ksft_variants(_uso_variants())
def test_uso(cfg, ipver, mss, total_payload):
"""Send a USO datagram and verify the peer receives the expected segments."""
_test_uso(cfg, ipver, mss, total_payload)
def main() -> None:
"""Run USO tests."""
with NetDrvEpEnv(__file__) as cfg:
ksft_run([test_uso],
args=(cfg, ))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,146 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""
Tests for XDP metadata kfuncs (e.g. bpf_xdp_metadata_rx_hash).
These tests load device-bound XDP programs from xdp_metadata.bpf.o
that call metadata kfuncs, send traffic, and verify the extracted
metadata via BPF maps.
"""
from lib.py import ksft_run, ksft_eq, ksft_exit, ksft_ge, ksft_ne, ksft_pr
from lib.py import KsftNamedVariant, ksft_variants
from lib.py import CmdExitFailure, KsftSkipEx, NetDrvEpEnv
from lib.py import NetdevFamily
from lib.py import bkg, cmd, rand_port, wait_port_listen
from lib.py import ip, bpftool, defer
from lib.py import bpf_map_set, bpf_map_dump, bpf_prog_map_ids
def _load_xdp_metadata_prog(cfg, prog_name, bpf_file="xdp_metadata.bpf.o"):
"""Load a device-bound XDP metadata program and return prog/map info.
Returns:
dict with 'id', 'name', and 'maps' (name -> map_id).
"""
abs_path = cfg.net_lib_dir / bpf_file
pin_dir = "/sys/fs/bpf/xdp_metadata_test"
cmd(f"rm -rf {pin_dir}", shell=True, fail=False)
cmd(f"mkdir -p {pin_dir}", shell=True)
try:
bpftool(f"prog loadall {abs_path} {pin_dir} type xdp "
f"xdpmeta_dev {cfg.ifname}")
except CmdExitFailure as e:
cmd(f"rm -rf {pin_dir}", shell=True, fail=False)
raise KsftSkipEx(
f"Failed to load device-bound XDP program '{prog_name}'"
) from e
defer(cmd, f"rm -rf {pin_dir}", shell=True, fail=False)
pin_path = f"{pin_dir}/{prog_name}"
ip(f"link set dev {cfg.ifname} xdpdrv pinned {pin_path}")
defer(ip, f"link set dev {cfg.ifname} xdpdrv off")
xdp_info = ip(f"-d link show dev {cfg.ifname}", json=True)[0]
prog_id = xdp_info["xdp"]["prog"]["id"]
return {"id": prog_id,
"name": xdp_info["xdp"]["prog"]["name"],
"maps": bpf_prog_map_ids(prog_id)}
def _send_probe(cfg, port, proto="tcp"):
"""Send a single payload from the remote end using socat.
Args:
cfg: Configuration object containing network settings.
port: Port number for the exchange.
proto: Protocol to use, either "tcp" or "udp".
"""
cfg.require_cmd("socat", remote=True)
if proto == "tcp":
rx_cmd = f"socat -{cfg.addr_ipver} -T 2 TCP-LISTEN:{port},reuseport STDOUT"
tx_cmd = f"echo -n rss_hash_test | socat -t 2 -u STDIN TCP:{cfg.baddr}:{port}"
else:
rx_cmd = f"socat -{cfg.addr_ipver} -T 2 -u UDP-RECV:{port},reuseport STDOUT"
tx_cmd = f"echo -n rss_hash_test | socat -t 2 -u STDIN UDP:{cfg.baddr}:{port}"
with bkg(rx_cmd, exit_wait=True):
wait_port_listen(port, proto=proto)
cmd(tx_cmd, host=cfg.remote, shell=True)
# BPF map keys matching the enums in xdp_metadata.bpf.c
_SETUP_KEY_PORT = 1
_RSS_KEY_HASH = 0
_RSS_KEY_TYPE = 1
_RSS_KEY_PKT_CNT = 2
_RSS_KEY_ERR_CNT = 3
XDP_RSS_L4 = 0x8 # BIT(3) from enum xdp_rss_hash_type
@ksft_variants([
KsftNamedVariant("tcp", "tcp"),
KsftNamedVariant("udp", "udp"),
])
def test_xdp_rss_hash(cfg, proto):
"""Test RSS hash metadata extraction via bpf_xdp_metadata_rx_hash().
This test will only run on devices that support xdp-rx-metadata-features.
Loads the xdp_rss_hash program from xdp_metadata, sends a packet using
the specified protocol, and verifies that the program extracted a non-zero
hash with an L4 hash type.
"""
dev_info = cfg.netnl.dev_get({"ifindex": cfg.ifindex})
rx_meta = dev_info.get("xdp-rx-metadata-features", [])
if "hash" not in rx_meta:
raise KsftSkipEx("device does not support XDP rx hash metadata")
prog_info = _load_xdp_metadata_prog(cfg, "xdp_rss_hash")
port = rand_port()
bpf_map_set("map_xdp_setup", _SETUP_KEY_PORT, port)
rss_map_id = prog_info["maps"]["map_rss"]
_send_probe(cfg, port, proto=proto)
rss = bpf_map_dump(rss_map_id)
pkt_cnt = rss.get(_RSS_KEY_PKT_CNT, 0)
err_cnt = rss.get(_RSS_KEY_ERR_CNT, 0)
hash_val = rss.get(_RSS_KEY_HASH, 0)
hash_type = rss.get(_RSS_KEY_TYPE, 0)
ksft_ge(pkt_cnt, 1, comment="should have received at least one packet")
ksft_eq(err_cnt, 0, comment=f"RSS hash error count: {err_cnt}")
ksft_ne(hash_val, 0,
f"RSS hash should be non-zero for {proto.upper()} traffic")
ksft_pr(f" RSS hash: {hash_val:#010x}")
ksft_pr(f" RSS hash type: {hash_type:#06x}")
ksft_ne(hash_type & XDP_RSS_L4, 0,
f"RSS hash type should include L4 for {proto.upper()} traffic")
def main():
"""Run XDP metadata kfunc tests against a real device."""
with NetDrvEpEnv(__file__) as cfg:
cfg.netnl = NetdevFamily()
ksft_run(
[
test_xdp_rss_hash,
],
args=(cfg,))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
# This is intended to be run on a virtio-net guest interface.
# The test binds the XDP socket to the interface without setting
# the fill ring to trigger delayed refill_work. This helps to
# make it easier to reproduce the deadlock when XDP program,
# XDP socket bind/unbind, rx ring resize race with refill_work on
# the buggy kernel.
#
# The Qemu command to setup virtio-net
# -netdev tap,id=hostnet1,vhost=on,script=no,downscript=no
# -device virtio-net-pci,netdev=hostnet1,iommu_platform=on,disable-legacy=on
from lib.py import ksft_exit, ksft_run
from lib.py import KsftSkipEx, KsftFailEx
from lib.py import NetDrvEnv
from lib.py import bkg, ip, cmd, ethtool
import time
def _get_rx_ring_entries(cfg):
output = ethtool(f"-g {cfg.ifname}", json=True)
return output[0]["rx"]
def setup_xsk(cfg, xdp_queue_id = 0) -> bkg:
# Probe for support
xdp = cmd(f'{cfg.net_lib_dir / "xdp_helper"} - -', fail=False)
if xdp.ret == 255:
raise KsftSkipEx('AF_XDP unsupported')
elif xdp.ret > 0:
raise KsftFailEx('unable to create AF_XDP socket')
try:
return bkg(f'{cfg.net_lib_dir / "xdp_helper"} {cfg.ifindex} ' \
'{xdp_queue_id} -z', ksft_wait=3)
except:
raise KsftSkipEx('Failed to bind XDP socket in zerocopy.\n' \
'Please consider adding iommu_platform=on ' \
'when setting up virtio-net-pci')
def check_xdp_bind(cfg):
with setup_xsk(cfg):
ip(f"link set dev %s xdp obj %s sec xdp" %
(cfg.ifname, cfg.net_lib_dir / "xdp_dummy.bpf.o"))
ip(f"link set dev %s xdp off" % cfg.ifname)
def check_rx_resize(cfg):
with setup_xsk(cfg):
rx_ring = _get_rx_ring_entries(cfg)
ethtool(f"-G %s rx %d" % (cfg.ifname, rx_ring // 2))
ethtool(f"-G %s rx %d" % (cfg.ifname, rx_ring))
def main():
with NetDrvEnv(__file__, nsim_test=False) as cfg:
ksft_run([check_xdp_bind, check_rx_resize],
args=(cfg, ))
ksft_exit()
if __name__ == "__main__":
main()