diff options
Diffstat (limited to 'tools/testing/selftests/drivers')
75 files changed, 8729 insertions, 2124 deletions
diff --git a/tools/testing/selftests/drivers/net/.gitignore b/tools/testing/selftests/drivers/net/.gitignore index 3633c7a3ed65..e5314ce4bb2d 100644 --- a/tools/testing/selftests/drivers/net/.gitignore +++ b/tools/testing/selftests/drivers/net/.gitignore @@ -1,4 +1,4 @@ # SPDX-License-Identifier: GPL-2.0-only -gro napi_id_helper psp_responder +so_txtime diff --git a/tools/testing/selftests/drivers/net/Makefile b/tools/testing/selftests/drivers/net/Makefile index 8154d6d429d3..d5bf4cb638a8 100644 --- a/tools/testing/selftests/drivers/net/Makefile +++ b/tools/testing/selftests/drivers/net/Makefile @@ -6,13 +6,14 @@ TEST_INCLUDES := $(wildcard lib/py/*.py) \ ../../net/lib.sh \ TEST_GEN_FILES := \ - gro \ napi_id_helper \ + so_txtime \ # end of TEST_GEN_FILES TEST_PROGS := \ gro.py \ hds.py \ + macsec.py \ napi_id.py \ napi_threaded.py \ netpoll_basic.py \ @@ -21,6 +22,7 @@ TEST_PROGS := \ queues.py \ ring_reconfig.py \ shaper.py \ + so_txtime.py \ stats.py \ xdp.py \ # end of TEST_PROGS diff --git a/tools/testing/selftests/drivers/net/README.rst b/tools/testing/selftests/drivers/net/README.rst index eb838ae94844..c6bed9a985bc 100644 --- a/tools/testing/selftests/drivers/net/README.rst +++ b/tools/testing/selftests/drivers/net/README.rst @@ -26,6 +26,10 @@ The netdevice against which tests will be run must exist, be running Refer to list of :ref:`Variables` later in this file to set up running the tests against a real device. +The current support for bash tests restricts the use of the same interface name +on the local system and the remote one and will bail if this case is +encountered. + Both modes required ~~~~~~~~~~~~~~~~~~~ @@ -47,6 +51,10 @@ or:: # Variable set in a file NETIF=eth0 +Please note that the config parser is very simple, if there are +any non-alphanumeric characters in the value it needs to be in +double quotes. + Local test (which don't require endpoint for sending / receiving traffic) need only the ``NETIF`` variable. Remaining variables define the endpoint and communication method. @@ -62,6 +70,44 @@ LOCAL_V4, LOCAL_V6, REMOTE_V4, REMOTE_V6 Local and remote endpoint IP addresses. +LOCAL_PREFIX_V6 +~~~~~~~~~~~~~~~ + +Local IP prefix/subnet which can be used to allocate extra IP addresses (for +network name spaces behind macvlan, veth, netkit devices). DUT must be +reachable using these addresses from the endpoint. + +LOCAL_PREFIX_V6 must NOT match LOCAL_V6. + +Example: + NETIF = "eth0" + LOCAL_V6 = "2001:db8:1::1" + REMOTE_V6 = "2001:db8:1::2" + LOCAL_PREFIX_V6 = "2001:db8:2::0/64" + + +-----------------------------+ +------------------------------+ + dst | INIT NS | | TEST NS | + 2001: | +---------------+ | | | + db8:2::2| | NETIF | | bpf | | + +---|>| 2001:db8:1::1 | |redirect| +-------------------------+ | + | | | |-----------|--------|>| Netkit | | + | | +---------------+ | _peer | | nk_guest | | + | | +-------------+ Netkit pair | | | fe80::2/64 | | + | | | Netkit |.............|........|>| 2001:db8:2::2/64 | | + | | | nk_host | | | +-------------------------+ | + | | | fe80::1/64 | | | | + | | +-------------+ | | route: | + | | | | default | + | | route: | | via fe80::1 dev nk_guest | + | | 2001:db8:2::2/128 | +------------------------------+ + | | via fe80::2 dev nk_host | + | +-----------------------------+ + | + | +---------------+ + | | REMOTE | + +---| 2001:db8:1::2 | + +---------------+ + REMOTE_TYPE ~~~~~~~~~~~ @@ -107,7 +153,7 @@ On the target machine, running the tests will use netdevsim by default:: 1..1 # timeout set to 45 # selftests: drivers/net: ping.py - # TAP version 13 + # KTAP version 1 # 1..3 # ok 1 ping.test_v4 # ok 2 ping.test_v6 @@ -128,9 +174,130 @@ Create a config with remote info:: Run the test:: [/root] # ./ksft-net-drv/drivers/net/ping.py - TAP version 13 + KTAP version 1 1..3 ok 1 ping.test_v4 ok 2 ping.test_v6 # SKIP Test requires IPv6 connectivity ok 3 ping.test_tcp # Totals: pass:2 fail:0 xfail:0 xpass:0 skip:1 error:0 + +Dependencies +~~~~~~~~~~~~ + +The tests have a handful of dependencies. For Fedora / CentOS:: + + dnf -y install netsniff-ng python-yaml socat iperf3 + +Guidance for test authors +========================= + +This section mostly applies to Python tests but some of the guidance +may be more broadly applicable. + +Kernel config +~~~~~~~~~~~~~ + +Each test directory has a ``config`` file listing which kernel +configuration options the tests depend on. This file must be kept +up to date, the CIs build minimal kernels for each test group. + +Adding checks inside the tests to validate that the necessary kernel +configs are enabled is discouraged. The test author may include such +checks, but standalone patches to make tests compatible e.g. with +distro kernel configs are unlikely to be accepted. + +Avoid libraries and frameworks +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Test files should be relatively self contained. The libraries should +only include very core or non-trivial code. +It may be tempting to "factor out" the common code to lib/py/, but fight that +urge. Library code increases the barrier of entry, and complexity in general. + +Avoid mixing test code and boilerplate +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In Python, try to avoid adding code in the ``main()`` function which +instantiates ``NetDrvEnv()`` and calls ``ksft_run()``. It's okay to +set up global resources (e.g. open an RtNetlink socket used by multiple +tests), but any complex logic, test-specific environment configuration +and validation should be done in the tests (even if it means it has to +be repeated). + +Local host is the DUT +~~~~~~~~~~~~~~~~~~~~~ + +Dual-host tests (tests with an endpoint) should be written from the DUT +perspective. IOW the local machine should be the one tested, remote is +just for traffic generation. + +Avoid modifying remote +~~~~~~~~~~~~~~~~~~~~~~ + +Avoid making configuration changes to the remote system as much as possible. +Remote system may be used concurrently by multiple DUTs. + +defer() +~~~~~~~ + +The env must be clean after test exits. Register a ``defer()`` for any +action that needs an "undo" as soon as possible. If you need to run +the cancel action as part of the test - ``defer()`` returns an object +you can ``.exec()``-ute. + +ksft_pr() +~~~~~~~~~ + +Use ``ksft_pr()`` instead of ``print()`` to avoid breaking TAP format. + +ksft_disruptive +~~~~~~~~~~~~~~~ + +By default the tests are expected to be able to run on +single-interface systems. All tests which may disconnect ``NETIF`` +must be annotated with ``@ksft_disruptive``. + +ksft_variants +~~~~~~~~~~~~~ + +Use the ``@ksft_variants`` decorator to run a test with multiple sets +of inputs as separate test cases. This avoids duplicating test functions +that only differ in parameters. + +Parameters can be a single value, a tuple, or a ``KsftNamedVariant`` +(which gives an explicit name to the sub-case). The argument to the +decorator can be a list or a generator. + +Example:: + + @ksft_variants([ + KsftNamedVariant("main", False), + KsftNamedVariant("ctx", True), + ]) + def resize_periodic(cfg, create_context): + # test body receives (cfg, create_context) where create_context + # is False for the "main" variant and True for "ctx" + pass + +or:: + + def _gro_variants(): + for mode in ["sw", "hw"]: + for protocol in ["tcp4", "tcp6"]: + yield (mode, protocol) + + @ksft_variants(_gro_variants()) + def test(cfg, mode, protocol): + pass + +Linters +~~~~~~~ + +We expect clean ``ruff check`` and ``pylint --disable=R``. +The code should be clean, avoid disabling pylint warnings explicitly! + +Running tests CI-style +====================== + +See https://github.com/linux-netdev/nipa/wiki for instructions on how +to easily run the tests using ``virtme-ng``. diff --git a/tools/testing/selftests/drivers/net/bonding/Makefile b/tools/testing/selftests/drivers/net/bonding/Makefile index 6c5c60adb5e8..6364ca02642d 100644 --- a/tools/testing/selftests/drivers/net/bonding/Makefile +++ b/tools/testing/selftests/drivers/net/bonding/Makefile @@ -8,9 +8,12 @@ TEST_PROGS := \ bond-lladdr-target.sh \ bond_ipsec_offload.sh \ bond_lacp_prio.sh \ + bond_lacp_strict.sh \ bond_macvlan_ipvlan.sh \ bond_options.sh \ bond_passive_lacp.sh \ + bond_stacked_header_parse.sh \ + bond_vlan_real_dev.sh \ dev_addr_lists.sh \ mode-1-recovery-updelay.sh \ mode-2-recovery-updelay.sh \ diff --git a/tools/testing/selftests/drivers/net/bonding/bond_lacp_strict.sh b/tools/testing/selftests/drivers/net/bonding/bond_lacp_strict.sh new file mode 100755 index 000000000000..f1a93a1d952f --- /dev/null +++ b/tools/testing/selftests/drivers/net/bonding/bond_lacp_strict.sh @@ -0,0 +1,347 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# Testing if bond lacp_strict works +# +# Partner (p_ns) +# +--------------------------+ +# | bond0 | +# | + | +# | eth0 | eth1 | +# | +---+---+ | +# | | | | +# +--------------------------+ +# | | eth0 | eth1 | +# | | | | +# |(br_ns) | br0 | br1 | +# | | | | +# | | eth2 | eth3 | +# +--------------------------+ +# | | | | +# | +---+---+ | +# | eth0 | eth1 | +# | + | +# | bond0 | +# +--------------------------+ +# Dut (d_ns) + +lib_dir=$(dirname "$0") +# shellcheck disable=SC1090 +source "$lib_dir"/../../../net/lib.sh + +COLLECTING_DISTRIBUTING_MASK=48 +COLLECTING_DISTRIBUTING=48 +FAILED=0 + +setup_links() +{ + # shellcheck disable=SC2154 + ip -n "${p_ns}" link add eth0 type veth peer name eth0 netns "${br_ns}" + ip -n "${p_ns}" link add eth1 type veth peer name eth1 netns "${br_ns}" + ip -n "${d_ns}" link add eth0 type veth peer name eth2 netns "${br_ns}" + ip -n "${d_ns}" link add eth1 type veth peer name eth3 netns "${br_ns}" + + ip -n "${br_ns}" link add br0 type bridge + ip -n "${br_ns}" link add br1 type bridge + + ip -n "${br_ns}" link set dev br0 type bridge stp_state 0 + ip -n "${br_ns}" link set dev br1 type bridge stp_state 0 + + ip -n "${br_ns}" link set eth0 master br0 + ip -n "${br_ns}" link set eth2 master br0 + ip -n "${br_ns}" link set eth1 master br1 + ip -n "${br_ns}" link set eth3 master br1 + + # Allow LACP trames forwarding on bridge ports + ip netns exec "${br_ns}" sh -c "echo 4 > /sys/class/net/br0/brif/eth0/group_fwd_mask" + ip netns exec "${br_ns}" sh -c "echo 4 > /sys/class/net/br1/brif/eth1/group_fwd_mask" + ip netns exec "${br_ns}" sh -c "echo 4 > /sys/class/net/br0/brif/eth2/group_fwd_mask" + ip netns exec "${br_ns}" sh -c "echo 4 > /sys/class/net/br1/brif/eth3/group_fwd_mask" + + ip -n "${br_ns}" link set eth0 up + ip -n "${br_ns}" link set eth2 up + ip -n "${br_ns}" link set eth1 up + ip -n "${br_ns}" link set eth3 up + + ip -n "${br_ns}" link set br0 up + ip -n "${br_ns}" link set br1 up + + ip -n "${d_ns}" link add bond0 type bond mode 802.3ad miimon 100 \ + lacp_rate fast min_links 1 + ip -n "${p_ns}" link add bond0 type bond mode 802.3ad miimon 100 \ + lacp_rate fast min_links 1 + + ip -n "${d_ns}" link set eth0 master bond0 + ip -n "${d_ns}" link set eth1 master bond0 + ip -n "${p_ns}" link set eth0 master bond0 + ip -n "${p_ns}" link set eth1 master bond0 + + ip -n "${d_ns}" link set bond0 up + ip -n "${p_ns}" link set bond0 up +} + +test_master_carrier() { + local expected=$1 + local mode_name=$2 + local carrier + + carrier=$(ip netns exec "${d_ns}" cat /sys/class/net/bond0/carrier) + [ "$carrier" == "1" ] && carrier="up" || carrier="down" + + [ "$carrier" == "$expected" ] && return + + echo "FAIL: Expected carrier $expected in lacp_strict $mode_name mode, got $carrier" + + RET=1 + +} + +compare_state() { + local actual_state=$1 + local expected_state=$2 + local iface=$3 + local last_attempt=$4 + + [ $((actual_state & COLLECTING_DISTRIBUTING_MASK)) -eq "$expected_state" ] \ + && return 0 + + [ "$last_attempt" -ne 1 ] && return 1 + + printf "FAIL: Expected LACP %s actor state to " "$iface" + if [ "$expected_state" -eq $COLLECTING_DISTRIBUTING ]; then + echo "be in Collecting/Distributing state" + else + echo "have neither Collecting nor Distributing set." + fi + + return 1 +} + +_test_lacp_port_state() { + local interface=$1 + local expected=$2 + local last_attempt=$3 + local eth0_actor_state eth1_actor_state + local ret=0 + + # shellcheck disable=SC2016 + while IFS='=' read -r k v; do + printf -v "$k" '%s' "$v" + done < <( + ip netns exec "${d_ns}" awk ' + /^Slave Interface: / { iface=$3 } + /details actor lacp pdu:/ { ctx="actor" } + /details partner lacp pdu:/ { ctx="partner" } + /^[[:space:]]+port state: / { + if (ctx == "actor") { + gsub(":", "", iface) + printf "%s_%s_state=%s\n", iface, ctx, $3 + } + } + ' /proc/net/bonding/bond0 + ) + + if [ "$interface" == "eth0" ] || [ "$interface" == "both" ]; then + compare_state "$eth0_actor_state" "$expected" eth0 "$last_attempt" || ret=1 + fi + + if [ "$interface" == "eth1" ] || [ "$interface" == "both" ]; then + compare_state "$eth1_actor_state" "$expected" eth1 "$last_attempt" || ret=1 + fi + + return $ret +} + +test_lacp_port_state() { + local interface=$1 + local expected=$2 + local retry=$3 + local last_attempt=0 + local attempt=1 + local ret=1 + + while [ $attempt -le $((retry + 1)) ]; do + [ $attempt -eq $((retry + 1)) ] && last_attempt=1 + _test_lacp_port_state "$interface" "$expected" "$last_attempt" && return + ((attempt++)) + sleep 1 + done + + RET=1 +} + + +trap cleanup_all_ns EXIT +setup_ns d_ns p_ns br_ns +setup_links + +# Initial state +RET=0 +mode=off +test_lacp_port_state both $COLLECTING_DISTRIBUTING 3 +test_master_carrier up $mode +log_test "bond LACP" "lacp_strict $mode - eth0 and eth1 up" + +# partner eth0 down, eth1 up +# (replicate eth0 state to dut eth0 by shutting a bridge port) +RET=0 +ip -n "${p_ns}" link set eth0 down +ip -n "${br_ns}" link set eth2 down +test_lacp_port_state eth0 $FAILED 5 +test_lacp_port_state eth1 $COLLECTING_DISTRIBUTING 1 +test_master_carrier up $mode +log_test "bond LACP" "lacp_strict $mode - eth0 down" + +# partner eth0 and eth1 down +RET=0 +ip -n "${p_ns}" link set eth1 down +ip -n "${br_ns}" link set eth3 down +test_lacp_port_state both $FAILED 5 +test_master_carrier down $mode # down because of min_links +log_test "bond LACP" "lacp_strict $mode - eth0 and eth1 down" + +# partner eth0 up, eth1 down +RET=0 +ip -n "${p_ns}" link set eth0 up +ip -n "${br_ns}" link set eth2 up +test_lacp_port_state eth0 $COLLECTING_DISTRIBUTING 60 +test_lacp_port_state eth1 $FAILED 1 +test_master_carrier up $mode +log_test "bond LACP" "lacp_strict $mode - eth0 up, eth1 down" + +# partner eth0 and eth1 up +RET=0 +ip -n "${p_ns}" link set eth1 up +ip -n "${br_ns}" link set eth3 up +test_lacp_port_state both $COLLECTING_DISTRIBUTING 60 +test_master_carrier up $mode +log_test "bond LACP" "lacp_strict $mode - eth0 and eth1 up" + +# partner eth0 stops LACP and eth1 up +RET=0 +ip netns exec "${br_ns}" sh -c "echo 0 > /sys/class/net/br0/brif/eth0/group_fwd_mask" +ip netns exec "${br_ns}" sh -c "echo 0 > /sys/class/net/br0/brif/eth2/group_fwd_mask" +test_lacp_port_state eth0 $FAILED 5 +test_lacp_port_state eth1 $COLLECTING_DISTRIBUTING 1 +test_master_carrier up $mode +log_test "bond LACP" "lacp_strict $mode - eth0 stopped sending LACP" + +# partner eth0 and eth1 stop LACP +RET=0 +ip netns exec "${br_ns}" sh -c "echo 0 > /sys/class/net/br1/brif/eth1/group_fwd_mask" +ip netns exec "${br_ns}" sh -c "echo 0 > /sys/class/net/br1/brif/eth3/group_fwd_mask" +test_lacp_port_state both $FAILED 5 +test_master_carrier up $mode +log_test "bond LACP" "lacp_strict $mode - eth0 and eth1 stopped sending LACP" + +# switch to lacp_strict on +RET=0 +mode=on +ip -n "${d_ns}" link set dev bond0 type bond lacp_strict $mode +test_lacp_port_state both $FAILED 1 +test_master_carrier down $mode +log_test "bond LACP" "lacp_strict $mode - eth0 and eth1 stopped sending LACP" + +# switch back to lacp_strict off mode +RET=0 +mode=off +ip -n "${d_ns}" link set dev bond0 type bond lacp_strict $mode +test_lacp_port_state both $FAILED 1 +test_master_carrier up $mode +log_test "bond LACP" "lacp_strict $mode - eth0 and eth1 stopped sending LACP" + +# eth0 recovers LACP +RET=0 +ip netns exec "${br_ns}" sh -c "echo 4 > /sys/class/net/br0/brif/eth0/group_fwd_mask" +ip netns exec "${br_ns}" sh -c "echo 4 > /sys/class/net/br0/brif/eth2/group_fwd_mask" +test_lacp_port_state eth0 $COLLECTING_DISTRIBUTING 60 +test_lacp_port_state eth1 $FAILED 1 +test_master_carrier up $mode +log_test "bond LACP" "lacp_strict $mode - eth0 recovered and eth1 stopped sending LACP" + +# eth1 recovers LACP +RET=0 +ip netns exec "${br_ns}" sh -c "echo 4 > /sys/class/net/br1/brif/eth1/group_fwd_mask" +ip netns exec "${br_ns}" sh -c "echo 4 > /sys/class/net/br1/brif/eth3/group_fwd_mask" +test_lacp_port_state both $COLLECTING_DISTRIBUTING 60 +test_master_carrier up $mode +log_test "bond LACP" "lacp_strict $mode - eth0 and eth1 recovered LACP" + +# switch to lacp_strict on +RET=0 +mode=on +ip -n "${d_ns}" link set dev bond0 type bond lacp_strict $mode +test_lacp_port_state both $COLLECTING_DISTRIBUTING 1 +test_master_carrier up $mode +log_test "bond LACP" "lacp_strict $mode - eth0 and eth1 up" + +# partner eth0 down, eth1 up +RET=0 +ip -n "${p_ns}" link set eth0 down +ip -n "${br_ns}" link set eth2 down +test_lacp_port_state eth0 $FAILED 5 +test_lacp_port_state eth1 $COLLECTING_DISTRIBUTING 1 +test_master_carrier up $mode +log_test "bond LACP" "lacp_strict $mode - eth0 down" + +# partner eth0 and eth1 down +RET=0 +ip -n "${p_ns}" link set eth1 down +ip -n "${br_ns}" link set eth3 down +test_lacp_port_state both $FAILED 5 +test_master_carrier down $mode # down because of min_links +log_test "bond LACP" "lacp_strict $mode - eth0 and eth1 down" + +# partner eth0 up, eth1 down +RET=0 +ip -n "${p_ns}" link set eth0 up +ip -n "${br_ns}" link set eth2 up +test_lacp_port_state eth0 $COLLECTING_DISTRIBUTING 60 +test_lacp_port_state eth1 $FAILED 1 +test_master_carrier up $mode +log_test "bond LACP" "lacp_strict $mode - eth0 up, eth1 down" + +# partner eth0 and eth1 up +RET=0 +ip -n "${p_ns}" link set eth1 up +ip -n "${br_ns}" link set eth3 up +test_lacp_port_state both $COLLECTING_DISTRIBUTING 60 +test_master_carrier up $mode +log_test "bond LACP" "lacp_strict $mode - eth0 and eth1 up" + +# partner eth0 stops LACP and eth1 up +RET=0 +ip netns exec "${br_ns}" sh -c "echo 0 > /sys/class/net/br0/brif/eth0/group_fwd_mask" +ip netns exec "${br_ns}" sh -c "echo 0 > /sys/class/net/br0/brif/eth2/group_fwd_mask" +test_lacp_port_state eth0 $FAILED 5 +test_lacp_port_state eth1 $COLLECTING_DISTRIBUTING 1 +test_master_carrier up $mode +log_test "bond LACP" "lacp_strict $mode - eth0 stopped sending LACP" + +# partner eth0 and eth1 stop LACP +RET=0 +ip netns exec "${br_ns}" sh -c "echo 0 > /sys/class/net/br1/brif/eth1/group_fwd_mask" +ip netns exec "${br_ns}" sh -c "echo 0 > /sys/class/net/br1/brif/eth3/group_fwd_mask" +test_lacp_port_state both $FAILED 5 +test_master_carrier down $mode +log_test "bond LACP" "lacp_strict $mode - eth0 and eth1 stopped sending LACP" + +# eth0 recovers LACP +RET=0 +ip netns exec "${br_ns}" sh -c "echo 4 > /sys/class/net/br0/brif/eth0/group_fwd_mask" +ip netns exec "${br_ns}" sh -c "echo 4 > /sys/class/net/br0/brif/eth2/group_fwd_mask" +test_lacp_port_state eth0 $COLLECTING_DISTRIBUTING 60 +test_lacp_port_state eth1 $FAILED 1 +test_master_carrier up $mode +log_test "bond LACP" "lacp_strict $mode - eth0 recovered and eth1 stopped sending LACP" + +# eth1 recovers LACP +# shellcheck disable=SC2034 +RET=0 +ip netns exec "${br_ns}" sh -c "echo 4 > /sys/class/net/br1/brif/eth1/group_fwd_mask" +ip netns exec "${br_ns}" sh -c "echo 4 > /sys/class/net/br1/brif/eth3/group_fwd_mask" +test_lacp_port_state both $COLLECTING_DISTRIBUTING 60 +test_master_carrier up $mode +log_test "bond LACP" "lacp_strict $mode - eth0 and eth1 recovered LACP" + +exit "${EXIT_STATUS}" diff --git a/tools/testing/selftests/drivers/net/bonding/bond_stacked_header_parse.sh b/tools/testing/selftests/drivers/net/bonding/bond_stacked_header_parse.sh new file mode 100755 index 000000000000..36bcdef711b0 --- /dev/null +++ b/tools/testing/selftests/drivers/net/bonding/bond_stacked_header_parse.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# Test that bond_header_parse() does not infinitely recurse with stacked bonds. +# +# When a non-Ethernet device (e.g. GRE) is enslaved to a bond that is itself +# enslaved to another bond (bond1 -> bond0 -> gre), receiving a packet via +# AF_PACKET SOCK_DGRAM triggers dev_parse_header() -> bond_header_parse(). +# Since parse() used skb->dev (always the topmost bond) instead of a passed-in +# dev pointer, it would recurse back into itself indefinitely. + +# shellcheck disable=SC2034 +ALL_TESTS=" + bond_test_stacked_header_parse +" +REQUIRE_MZ=no +NUM_NETIFS=0 +lib_dir=$(dirname "$0") +source "$lib_dir"/../../../net/forwarding/lib.sh + +# shellcheck disable=SC2329 +bond_test_stacked_header_parse() +{ + local devdummy="test-dummy0" + local devgre="test-gre0" + local devbond0="test-bond0" + local devbond1="test-bond1" + + # shellcheck disable=SC2034 + RET=0 + + # Setup: dummy -> gre -> bond0 -> bond1 + ip link add name "$devdummy" type dummy + ip addr add 10.0.0.1/24 dev "$devdummy" + ip link set "$devdummy" up + + ip link add name "$devgre" type gre local 10.0.0.1 + + ip link add name "$devbond0" type bond mode active-backup + ip link add name "$devbond1" type bond mode active-backup + + ip link set "$devgre" master "$devbond0" + ip link set "$devbond0" master "$devbond1" + + ip link set "$devgre" up + ip link set "$devbond0" up + ip link set "$devbond1" up + + # tcpdump on a non-Ethernet bond uses AF_PACKET SOCK_DGRAM (cooked + # capture), which triggers dev_parse_header() -> bond_header_parse() + # on receive. With the bug, this recurses infinitely. + timeout 5 tcpdump -c 1 -i "$devbond1" >/dev/null 2>&1 & + local tcpdump_pid=$! + sleep 1 + + # Send a GRE packet to 10.0.0.1 so it arrives via gre -> bond0 -> bond1 + python3 -c "from scapy.all import *; send(IP(src='10.0.0.2', dst='10.0.0.1')/GRE()/IP()/UDP(), verbose=0)" + check_err $? "failed to send GRE packet (scapy installed?)" + + wait "$tcpdump_pid" 2>/dev/null + + ip link del "$devbond1" 2>/dev/null + ip link del "$devbond0" 2>/dev/null + ip link del "$devgre" 2>/dev/null + ip link del "$devdummy" 2>/dev/null + + log_test "Stacked bond header_parse does not recurse" +} + +tests_run + +exit "$EXIT_STATUS" diff --git a/tools/testing/selftests/drivers/net/bonding/bond_topo_2d1c.sh b/tools/testing/selftests/drivers/net/bonding/bond_topo_2d1c.sh index 167aa4a4a12a..903c7a6c7287 100644 --- a/tools/testing/selftests/drivers/net/bonding/bond_topo_2d1c.sh +++ b/tools/testing/selftests/drivers/net/bonding/bond_topo_2d1c.sh @@ -48,7 +48,7 @@ gateway_create() ip -n ${g_ns} link add br0 type bridge ip -n ${g_ns} link set br0 up ip -n ${g_ns} addr add ${g_ip4}/24 dev br0 - ip -n ${g_ns} addr add ${g_ip6}/24 dev br0 + ip -n ${g_ns} addr add ${g_ip6}/24 dev br0 nodad } gateway_destroy() @@ -75,7 +75,7 @@ server_create() ip -n ${s_ns} link set bond0 up ip -n ${s_ns} addr add ${s_ip4}/24 dev bond0 - ip -n ${s_ns} addr add ${s_ip6}/24 dev bond0 + ip -n ${s_ns} addr add ${s_ip6}/24 dev bond0 nodad } # Reset bond with new mode and options @@ -97,9 +97,7 @@ bond_reset() ip -n ${s_ns} link set bond0 up ip -n ${s_ns} addr add ${s_ip4}/24 dev bond0 - ip -n ${s_ns} addr add ${s_ip6}/24 dev bond0 - # Wait for IPv6 address ready as it needs DAD - slowwait 2 ip netns exec ${s_ns} ping6 ${c_ip6} -c 1 -W 0.1 &> /dev/null + ip -n ${s_ns} addr add ${s_ip6}/24 dev bond0 nodad } server_destroy() @@ -124,7 +122,7 @@ client_create() ip -n ${c_ns} link set eth0 up ip -n ${c_ns} addr add ${c_ip4}/24 dev eth0 - ip -n ${c_ns} addr add ${c_ip6}/24 dev eth0 + ip -n ${c_ns} addr add ${c_ip6}/24 dev eth0 nodad } client_destroy() diff --git a/tools/testing/selftests/drivers/net/bonding/bond_vlan_real_dev.sh b/tools/testing/selftests/drivers/net/bonding/bond_vlan_real_dev.sh new file mode 100755 index 000000000000..542d9ffc4819 --- /dev/null +++ b/tools/testing/selftests/drivers/net/bonding/bond_vlan_real_dev.sh @@ -0,0 +1,180 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# Test propagation of a real device's state to the VLANs stacked on top of it +# when the real device is (or becomes) a bond member. +# +# The kernel mirrors a real device's UP/DOWN, MTU and feature changes onto its +# VLANs. This is done asynchronously (netdev_work): doing it synchronously from +# the real device's notifier could deadlock. If the real device is brought up +# while enslaved to a bond - so its instance lock is held across NETDEV_UP - and +# a VLAN on top of it is itself a bond member, the synchronous propagation +# re-entered the stack and tried to take the same instance lock again. +# +# Cover both halves: +# - the deferred UP/DOWN, MTU and feature propagation actually lands on the +# VLAN (link state and MTU use an ops-locked dummy, i.e. the deferral path), +# - the deadlock-prone topology - a VLAN on a dummy, with the VLAN and the +# dummy each enslaved to a different bond - can be built without hanging. + +ALL_TESTS=" + vlan_link_state + vlan_mtu + vlan_features + vlan_real_dev_enslave +" + +REQUIRE_MZ=no +NUM_NETIFS=0 +lib_dir=$(dirname "$0") +source "$lib_dir"/../../../net/forwarding/lib.sh + +# Return 0 if $dev in netns $ns has flag $flag set (e.g. UP) in its <...> flags. +link_has_flag() +{ + local ns=$1 dev=$2 flag=$3 + + ip -n "$ns" link show dev "$dev" 2>/dev/null | grep -q "[<,]${flag}[,>]" +} + +link_lacks_flag() +{ + ! link_has_flag "$@" +} + +link_mtu_is() +{ + local ns=$1 dev=$2 want=$3 cur + + cur=$(ip -n "$ns" link show dev "$dev" 2>/dev/null | \ + sed -n 's/.* mtu \([0-9]\+\).*/\1/p') + [ "$cur" = "$want" ] +} + +vlan_feature_is() +{ + local ns=$1 dev=$2 feature=$3 value=$4 + + ip netns exec "$ns" ethtool -k "$dev" 2>/dev/null | \ + grep -q "^$feature: $value" +} + +link_has_master() +{ + local ns=$1 dev=$2 master=$3 + + ip -n "$ns" -o link show dev "$dev" 2>/dev/null | grep -q "master $master" +} + +vlan_link_state() +{ + RET=0 + + ip -n "$NS" link add ls_dummy type dummy + ip -n "$NS" link add link ls_dummy name ls_vlan type vlan id 100 + + # Bringing the real device up must propagate UP to the VLAN. + ip -n "$NS" link set ls_dummy up + busywait "$BUSYWAIT_TIMEOUT" link_has_flag "$NS" ls_vlan UP + check_err $? "VLAN did not go UP after the real device went UP" + + # ... and likewise for DOWN. + ip -n "$NS" link set ls_dummy down + busywait "$BUSYWAIT_TIMEOUT" link_lacks_flag "$NS" ls_vlan UP + check_err $? "VLAN did not go DOWN after the real device went DOWN" + + ip -n "$NS" link del ls_vlan + ip -n "$NS" link del ls_dummy + + log_test "VLAN link state follows the real device" +} + +vlan_mtu() +{ + RET=0 + + # The VLAN inherits the real device's MTU (2000) at creation time. + ip -n "$NS" link add mtu_dummy mtu 2000 type dummy + ip -n "$NS" link add link mtu_dummy name mtu_vlan type vlan id 100 + + # Shrinking the real device's MTU must clamp the VLAN's MTU. + ip -n "$NS" link set mtu_dummy mtu 1500 + busywait "$BUSYWAIT_TIMEOUT" link_mtu_is "$NS" mtu_vlan 1500 + check_err $? "VLAN MTU not clamped after the real device's MTU shrank" + + ip -n "$NS" link del mtu_vlan + ip -n "$NS" link del mtu_dummy + + log_test "VLAN MTU clamped to the real device" +} + +vlan_features() +{ + RET=0 + + # Use veth as the real device: unlike dummy it exports vlan_features, so + # the VLAN actually inherits a toggleable offload to assert on. + ip -n "$NS" link add ft_veth type veth peer name ft_veth_pr + ip -n "$NS" link add link ft_veth name ft_vlan type vlan id 100 + + vlan_feature_is "$NS" ft_vlan scatter-gather on + check_err $? "VLAN did not inherit scatter-gather from the real device" + + # Toggling the offload on the real device must propagate to the VLAN. + ip netns exec "$NS" ethtool -K ft_veth sg off + busywait "$BUSYWAIT_TIMEOUT" \ + vlan_feature_is "$NS" ft_vlan scatter-gather off + check_err $? "VLAN scatter-gather still on after disabling it on real dev" + + ip netns exec "$NS" ethtool -K ft_veth sg on + busywait "$BUSYWAIT_TIMEOUT" \ + vlan_feature_is "$NS" ft_vlan scatter-gather on + check_err $? "VLAN scatter-gather still off after enabling it on real dev" + + ip -n "$NS" link del ft_vlan + ip -n "$NS" link del ft_veth + + log_test "VLAN features follow the real device" +} + +vlan_real_dev_enslave() +{ + RET=0 + + # dummy <- VLAN -> bond0, then enslave the dummy itself to bond1. The + # last step brings the dummy up under bond1's instance lock, which used + # to deadlock while synchronously propagating UP to the (bond-enslaved) + # VLAN on top. + ip -n "$NS" link add dl_dummy type dummy + ip -n "$NS" link set dl_dummy up + ip -n "$NS" link add link dl_dummy name dl_vlan type vlan id 100 + + ip -n "$NS" link add dl_bond0 type bond mode active-backup + ip -n "$NS" link set dl_vlan down + ip -n "$NS" link set dl_vlan master dl_bond0 + check_err $? "could not enslave the VLAN to bond0" + + ip -n "$NS" link add dl_bond1 type bond mode active-backup + ip -n "$NS" link set dl_dummy down + ip -n "$NS" link set dl_dummy master dl_bond1 + check_err $? "could not enslave the real device to bond1" + + # If we got here the kernel did not deadlock; make sure it is still + # responsive and the enslave really took effect. + link_has_master "$NS" dl_dummy dl_bond1 + check_err $? "real device not enslaved to bond1" + + ip -n "$NS" link del dl_bond1 + ip -n "$NS" link del dl_bond0 + ip -n "$NS" link del dl_vlan + ip -n "$NS" link del dl_dummy + + log_test "VLAN real device enslaved to a second bond" +} + +setup_ns NS +trap 'cleanup_ns $NS' EXIT + +tests_run + +exit "$EXIT_STATUS" diff --git a/tools/testing/selftests/drivers/net/bonding/config b/tools/testing/selftests/drivers/net/bonding/config index 991494376223..b62c70715293 100644 --- a/tools/testing/selftests/drivers/net/bonding/config +++ b/tools/testing/selftests/drivers/net/bonding/config @@ -14,6 +14,7 @@ CONFIG_NETCONSOLE=m CONFIG_NETCONSOLE_DYNAMIC=y CONFIG_NETCONSOLE_EXTENDED_LOG=y CONFIG_NETDEVSIM=m +CONFIG_NET_IPGRE=y CONFIG_NET_SCH_INGRESS=y CONFIG_NLMON=y CONFIG_VETH=y diff --git a/tools/testing/selftests/drivers/net/bonding/lag_lib.sh b/tools/testing/selftests/drivers/net/bonding/lag_lib.sh index bf9bcd1b5ec0..f2e43b6c4c81 100644 --- a/tools/testing/selftests/drivers/net/bonding/lag_lib.sh +++ b/tools/testing/selftests/drivers/net/bonding/lag_lib.sh @@ -23,20 +23,9 @@ test_LAG_cleanup() ip link set dev dummy2 master "$name" elif [ "$driver" = "team" ]; then name="team0" - teamd -d -c ' - { - "device": "'"$name"'", - "runner": { - "name": "'"$mode"'" - }, - "ports": { - "dummy1": - {}, - "dummy2": - {} - } - } - ' + ip link add "$name" type team + ip link set dev dummy1 master "$name" + ip link set dev dummy2 master "$name" ip link set dev "$name" up else check_err 1 diff --git a/tools/testing/selftests/drivers/net/config b/tools/testing/selftests/drivers/net/config index 77ccf83d87e0..4838adf27fa1 100644 --- a/tools/testing/selftests/drivers/net/config +++ b/tools/testing/selftests/drivers/net/config @@ -3,8 +3,24 @@ CONFIG_DEBUG_INFO_BTF=y CONFIG_DEBUG_INFO_BTF_MODULES=n CONFIG_INET_PSP=y CONFIG_IPV6=y +CONFIG_MACSEC=m +CONFIG_NET_ACT_SKBEDIT=m +CONFIG_NET_CLS_ACT=y +CONFIG_NET_CLS_BPF=y +CONFIG_NET_CLS_FLOWER=m +CONFIG_NET_CLS_FW=m +CONFIG_NET_CLS_MATCHALL=m CONFIG_NETCONSOLE=m CONFIG_NETCONSOLE_DYNAMIC=y CONFIG_NETCONSOLE_EXTENDED_LOG=y CONFIG_NETDEVSIM=m +CONFIG_NETKIT=y +CONFIG_NET_SCH_ETF=m +CONFIG_NET_SCH_FQ=m +CONFIG_NET_SCH_INGRESS=y +CONFIG_NET_SCH_PRIO=m +CONFIG_PPP=y +CONFIG_PPPOE=y +CONFIG_TLS=y +CONFIG_VLAN_8021Q=m CONFIG_XDP_SOCKETS=y diff --git a/tools/testing/selftests/drivers/net/gro.c b/tools/testing/selftests/drivers/net/gro.c deleted file mode 100644 index 3c0745b68bfa..000000000000 --- a/tools/testing/selftests/drivers/net/gro.c +++ /dev/null @@ -1,1498 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0 -/* - * This testsuite provides conformance testing for GRO coalescing. - * - * Test cases: - * - * data_*: - * Data packets of the same size and same header setup with correct - * sequence numbers coalesce. The one exception being the last data - * packet coalesced: it can be smaller than the rest and coalesced - * as long as it is in the same flow. - * - data_same: same size packets coalesce - * - data_lrg_sml: large then small coalesces - * - data_sml_lrg: small then large doesn't coalesce - * - * ack: - * Pure ACK does not coalesce. - * - * flags_*: - * No packets with PSH, SYN, URG, RST, CWR set will be coalesced. - * - flags_psh, flags_syn, flags_rst, flags_urg, flags_cwr - * - * tcp_*: - * Packets with incorrect checksum, non-consecutive seqno and - * different TCP header options shouldn't coalesce. Nit: given that - * some extension headers have paddings, such as timestamp, headers - * that are padded differently would not be coalesced. - * - tcp_csum: incorrect checksum - * - tcp_seq: non-consecutive sequence numbers - * - tcp_ts: different timestamps - * - tcp_opt: different TCP options - * - * ip_*: - * Packets with different (ECN, TTL, TOS) header, IP options or - * IP fragments shouldn't coalesce. - * - ip_ecn, ip_tos: shared between IPv4/IPv6 - * - ip_ttl, ip_opt, ip_frag4: IPv4 only - * - ip_id_df*: IPv4 IP ID field coalescing tests - * - ip_frag6, ip_v6ext_*: IPv6 only - * - * large_*: - * Packets larger than GRO_MAX_SIZE packets shouldn't coalesce. - * - large_max: exceeding max size - * - large_rem: remainder handling - * - * MSS is defined as 4096 - header because if it is too small - * (i.e. 1500 MTU - header), it will result in many packets, - * increasing the "large" test case's flakiness. This is because - * due to time sensitivity in the coalescing window, the receiver - * may not coalesce all of the packets. - * - * Note the timing issue applies to all of the test cases, so some - * flakiness is to be expected. - * - */ - -#define _GNU_SOURCE - -#include <arpa/inet.h> -#include <errno.h> -#include <error.h> -#include <getopt.h> -#include <linux/filter.h> -#include <linux/if_packet.h> -#include <linux/ipv6.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 <stdbool.h> -#include <stddef.h> -#include <stdio.h> -#include <stdarg.h> -#include <string.h> -#include <unistd.h> - -#include "kselftest.h" -#include "../../net/lib/ksft.h" - -#define DPORT 8000 -#define SPORT 1500 -#define PAYLOAD_LEN 100 -#define NUM_PACKETS 4 -#define START_SEQ 100 -#define START_ACK 100 -#define ETH_P_NONE 0 -#define TOTAL_HDR_LEN (ETH_HLEN + sizeof(struct ipv6hdr) + sizeof(struct tcphdr)) -#define MSS (4096 - sizeof(struct tcphdr) - sizeof(struct ipv6hdr)) -#define MAX_PAYLOAD (IP_MAXPACKET - sizeof(struct tcphdr) - sizeof(struct ipv6hdr)) -#define NUM_LARGE_PKT (MAX_PAYLOAD / MSS) -#define MAX_HDR_LEN (ETH_HLEN + sizeof(struct ipv6hdr) + sizeof(struct tcphdr)) -#define MIN_EXTHDR_SIZE 8 -#define EXT_PAYLOAD_1 "\x00\x00\x00\x00\x00\x00" -#define EXT_PAYLOAD_2 "\x11\x11\x11\x11\x11\x11" - -#define ipv6_optlen(p) (((p)->hdrlen+1) << 3) /* calculate IPv6 extension header len */ -#define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2*!!(condition)])) - -enum flush_id_case { - FLUSH_ID_DF1_INC, - FLUSH_ID_DF1_FIXED, - FLUSH_ID_DF0_INC, - FLUSH_ID_DF0_FIXED, - FLUSH_ID_DF1_INC_FIXED, - FLUSH_ID_DF1_FIXED_INC, -}; - -static const char *addr6_src = "fdaa::2"; -static const char *addr6_dst = "fdaa::1"; -static const char *addr4_src = "192.168.1.200"; -static const char *addr4_dst = "192.168.1.100"; -static int proto = -1; -static uint8_t src_mac[ETH_ALEN], dst_mac[ETH_ALEN]; -static char *testname = "data"; -static char *ifname = "eth0"; -static char *smac = "aa:00:00:00:00:02"; -static char *dmac = "aa:00:00:00:00:01"; -static bool verbose; -static bool tx_socket = true; -static int tcp_offset = -1; -static int total_hdr_len = -1; -static int ethhdr_proto = -1; -static bool ipip; - -static void vlog(const char *fmt, ...) -{ - va_list args; - - if (verbose) { - va_start(args, fmt); - vfprintf(stderr, fmt, args); - va_end(args); - } -} - -static void setup_sock_filter(int fd) -{ - const int dport_off = tcp_offset + offsetof(struct tcphdr, dest); - const int ethproto_off = offsetof(struct ethhdr, h_proto); - int optlen = 0; - int ipproto_off, opt_ipproto_off; - int next_off; - - if (ipip) - next_off = sizeof(struct iphdr) + offsetof(struct iphdr, protocol); - else if (proto == PF_INET) - next_off = offsetof(struct iphdr, protocol); - else - next_off = offsetof(struct ipv6hdr, nexthdr); - ipproto_off = ETH_HLEN + next_off; - - /* Overridden later if exthdrs are used: */ - opt_ipproto_off = ipproto_off; - - if (strcmp(testname, "ip_opt") == 0) { - optlen = sizeof(struct ip_timestamp); - } else if (strcmp(testname, "ip_frag6") == 0 || - strcmp(testname, "ip_v6ext_same") == 0 || - strcmp(testname, "ip_v6ext_diff") == 0) { - BUILD_BUG_ON(sizeof(struct ip6_hbh) > MIN_EXTHDR_SIZE); - BUILD_BUG_ON(sizeof(struct ip6_dest) > MIN_EXTHDR_SIZE); - BUILD_BUG_ON(sizeof(struct ip6_frag) > MIN_EXTHDR_SIZE); - - /* same size for HBH and Fragment extension header types */ - optlen = MIN_EXTHDR_SIZE; - opt_ipproto_off = ETH_HLEN + sizeof(struct ipv6hdr) - + offsetof(struct ip6_ext, ip6e_nxt); - } - - /* this filter validates the following: - * - packet is IPv4/IPv6 according to the running test. - * - packet is TCP. Also handles the case of one extension header and then TCP. - * - checks the packet tcp dport equals to DPORT. Also handles the case of one - * extension header and then TCP. - */ - struct sock_filter filter[] = { - BPF_STMT(BPF_LD + BPF_H + BPF_ABS, ethproto_off), - BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ntohs(ethhdr_proto), 0, 9), - BPF_STMT(BPF_LD + BPF_B + BPF_ABS, ipproto_off), - BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, IPPROTO_TCP, 2, 0), - BPF_STMT(BPF_LD + BPF_B + BPF_ABS, opt_ipproto_off), - BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, IPPROTO_TCP, 0, 5), - BPF_STMT(BPF_LD + BPF_H + BPF_ABS, dport_off), - BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, DPORT, 2, 0), - BPF_STMT(BPF_LD + BPF_H + BPF_ABS, dport_off + optlen), - BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, DPORT, 0, 1), - BPF_STMT(BPF_RET + BPF_K, 0xFFFFFFFF), - BPF_STMT(BPF_RET + BPF_K, 0), - }; - - struct sock_fprog bpf = { - .len = ARRAY_SIZE(filter), - .filter = filter, - }; - - if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf)) < 0) - error(1, errno, "error setting filter"); -} - -static uint32_t checksum_nofold(void *data, size_t len, uint32_t sum) -{ - uint16_t *words = data; - int i; - - for (i = 0; i < len / 2; i++) - sum += words[i]; - if (len & 1) - sum += ((char *)data)[len - 1]; - return sum; -} - -static uint16_t checksum_fold(void *data, size_t len, uint32_t sum) -{ - sum = checksum_nofold(data, len, sum); - while (sum > 0xFFFF) - sum = (sum & 0xFFFF) + (sum >> 16); - return ~sum; -} - -static uint16_t tcp_checksum(void *buf, int payload_len) -{ - struct pseudo_header6 { - struct in6_addr saddr; - struct in6_addr daddr; - uint16_t protocol; - uint16_t payload_len; - } ph6; - struct pseudo_header4 { - struct in_addr saddr; - struct in_addr daddr; - uint16_t protocol; - uint16_t payload_len; - } ph4; - uint32_t sum = 0; - - if (proto == PF_INET6) { - if (inet_pton(AF_INET6, addr6_src, &ph6.saddr) != 1) - error(1, errno, "inet_pton6 source ip pseudo"); - if (inet_pton(AF_INET6, addr6_dst, &ph6.daddr) != 1) - error(1, errno, "inet_pton6 dest ip pseudo"); - ph6.protocol = htons(IPPROTO_TCP); - ph6.payload_len = htons(sizeof(struct tcphdr) + payload_len); - - sum = checksum_nofold(&ph6, sizeof(ph6), 0); - } else if (proto == PF_INET) { - if (inet_pton(AF_INET, addr4_src, &ph4.saddr) != 1) - error(1, errno, "inet_pton source ip pseudo"); - if (inet_pton(AF_INET, addr4_dst, &ph4.daddr) != 1) - error(1, errno, "inet_pton dest ip pseudo"); - ph4.protocol = htons(IPPROTO_TCP); - ph4.payload_len = htons(sizeof(struct tcphdr) + payload_len); - - sum = checksum_nofold(&ph4, sizeof(ph4), 0); - } - - return checksum_fold(buf, sizeof(struct tcphdr) + payload_len, sum); -} - -static void read_MAC(uint8_t *mac_addr, char *mac) -{ - if (sscanf(mac, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", - &mac_addr[0], &mac_addr[1], &mac_addr[2], - &mac_addr[3], &mac_addr[4], &mac_addr[5]) != 6) - error(1, 0, "sscanf"); -} - -static void fill_datalinklayer(void *buf) -{ - struct ethhdr *eth = buf; - - memcpy(eth->h_dest, dst_mac, ETH_ALEN); - memcpy(eth->h_source, src_mac, ETH_ALEN); - eth->h_proto = ethhdr_proto; -} - -static void fill_networklayer(void *buf, int payload_len, int protocol) -{ - struct ipv6hdr *ip6h = buf; - struct iphdr *iph = buf; - - if (proto == PF_INET6) { - memset(ip6h, 0, sizeof(*ip6h)); - - ip6h->version = 6; - ip6h->payload_len = htons(sizeof(struct tcphdr) + payload_len); - ip6h->nexthdr = protocol; - ip6h->hop_limit = 8; - if (inet_pton(AF_INET6, addr6_src, &ip6h->saddr) != 1) - error(1, errno, "inet_pton source ip6"); - if (inet_pton(AF_INET6, addr6_dst, &ip6h->daddr) != 1) - error(1, errno, "inet_pton dest ip6"); - } else if (proto == PF_INET) { - memset(iph, 0, sizeof(*iph)); - - iph->version = 4; - iph->ihl = 5; - iph->ttl = 8; - iph->protocol = protocol; - iph->tot_len = htons(sizeof(struct tcphdr) + - payload_len + sizeof(struct iphdr)); - iph->frag_off = htons(0x4000); /* DF = 1, MF = 0 */ - if (inet_pton(AF_INET, addr4_src, &iph->saddr) != 1) - error(1, errno, "inet_pton source ip"); - if (inet_pton(AF_INET, addr4_dst, &iph->daddr) != 1) - error(1, errno, "inet_pton dest ip"); - iph->check = checksum_fold(buf, sizeof(struct iphdr), 0); - } -} - -static void fill_transportlayer(void *buf, int seq_offset, int ack_offset, - int payload_len, int fin) -{ - struct tcphdr *tcph = buf; - - memset(tcph, 0, sizeof(*tcph)); - - tcph->source = htons(SPORT); - tcph->dest = htons(DPORT); - tcph->seq = ntohl(START_SEQ + seq_offset); - tcph->ack_seq = ntohl(START_ACK + ack_offset); - tcph->ack = 1; - tcph->fin = fin; - tcph->doff = 5; - tcph->window = htons(TCP_MAXWIN); - tcph->urg_ptr = 0; - tcph->check = tcp_checksum(tcph, payload_len); -} - -static void write_packet(int fd, char *buf, int len, struct sockaddr_ll *daddr) -{ - int ret = -1; - - ret = sendto(fd, buf, len, 0, (struct sockaddr *)daddr, sizeof(*daddr)); - if (ret == -1) - error(1, errno, "sendto failure"); - if (ret != len) - error(1, errno, "sendto wrong length"); -} - -static void create_packet(void *buf, int seq_offset, int ack_offset, - int payload_len, int fin) -{ - memset(buf, 0, total_hdr_len); - memset(buf + total_hdr_len, 'a', payload_len); - - fill_transportlayer(buf + tcp_offset, seq_offset, ack_offset, - payload_len, fin); - - if (ipip) { - fill_networklayer(buf + ETH_HLEN, payload_len + sizeof(struct iphdr), - IPPROTO_IPIP); - fill_networklayer(buf + ETH_HLEN + sizeof(struct iphdr), - payload_len, IPPROTO_TCP); - } else { - fill_networklayer(buf + ETH_HLEN, payload_len, IPPROTO_TCP); - } - - fill_datalinklayer(buf); -} - -#ifndef TH_CWR -#define TH_CWR 0x80 -#endif -static void set_flags(struct tcphdr *tcph, int payload_len, int psh, int syn, - int rst, int urg, int cwr) -{ - tcph->psh = psh; - tcph->syn = syn; - tcph->rst = rst; - tcph->urg = urg; - if (cwr) - tcph->th_flags |= TH_CWR; - else - tcph->th_flags &= ~TH_CWR; - tcph->check = 0; - tcph->check = tcp_checksum(tcph, payload_len); -} - -/* send extra flags of the (NUM_PACKETS / 2) and (NUM_PACKETS / 2 - 1) - * pkts, not first and not last pkt - */ -static void send_flags(int fd, struct sockaddr_ll *daddr, int psh, int syn, - int rst, int urg, int cwr) -{ - static char flag_buf[2][MAX_HDR_LEN + PAYLOAD_LEN]; - static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; - int payload_len, pkt_size, i; - struct tcphdr *tcph; - int flag[2]; - - payload_len = PAYLOAD_LEN * (psh || cwr); - pkt_size = total_hdr_len + payload_len; - flag[0] = NUM_PACKETS / 2; - flag[1] = NUM_PACKETS / 2 - 1; - - /* Create and configure packets with flags - */ - for (i = 0; i < 2; i++) { - if (flag[i] > 0) { - create_packet(flag_buf[i], flag[i] * payload_len, 0, - payload_len, 0); - tcph = (struct tcphdr *)(flag_buf[i] + tcp_offset); - set_flags(tcph, payload_len, psh, syn, rst, urg, cwr); - } - } - - for (i = 0; i < NUM_PACKETS + 1; i++) { - if (i == flag[0]) { - write_packet(fd, flag_buf[0], pkt_size, daddr); - continue; - } else if (i == flag[1] && cwr) { - write_packet(fd, flag_buf[1], pkt_size, daddr); - continue; - } - create_packet(buf, i * PAYLOAD_LEN, 0, PAYLOAD_LEN, 0); - write_packet(fd, buf, total_hdr_len + PAYLOAD_LEN, daddr); - } -} - -/* Test for data of same length, smaller than previous - * and of different lengths - */ -static void send_data_pkts(int fd, struct sockaddr_ll *daddr, - int payload_len1, int payload_len2) -{ - static char buf[ETH_HLEN + IP_MAXPACKET]; - - create_packet(buf, 0, 0, payload_len1, 0); - write_packet(fd, buf, total_hdr_len + payload_len1, daddr); - create_packet(buf, payload_len1, 0, payload_len2, 0); - write_packet(fd, buf, total_hdr_len + payload_len2, daddr); -} - -/* If incoming segments make tracked segment length exceed - * legal IP datagram length, do not coalesce - */ -static void send_large(int fd, struct sockaddr_ll *daddr, int remainder) -{ - static char pkts[NUM_LARGE_PKT][TOTAL_HDR_LEN + MSS]; - static char last[TOTAL_HDR_LEN + MSS]; - static char new_seg[TOTAL_HDR_LEN + MSS]; - int i; - - for (i = 0; i < NUM_LARGE_PKT; i++) - create_packet(pkts[i], i * MSS, 0, MSS, 0); - create_packet(last, NUM_LARGE_PKT * MSS, 0, remainder, 0); - create_packet(new_seg, (NUM_LARGE_PKT + 1) * MSS, 0, remainder, 0); - - for (i = 0; i < NUM_LARGE_PKT; i++) - write_packet(fd, pkts[i], total_hdr_len + MSS, daddr); - write_packet(fd, last, total_hdr_len + remainder, daddr); - write_packet(fd, new_seg, total_hdr_len + remainder, daddr); -} - -/* Pure acks and dup acks don't coalesce */ -static void send_ack(int fd, struct sockaddr_ll *daddr) -{ - static char buf[MAX_HDR_LEN]; - - create_packet(buf, 0, 0, 0, 0); - write_packet(fd, buf, total_hdr_len, daddr); - write_packet(fd, buf, total_hdr_len, daddr); - create_packet(buf, 0, 1, 0, 0); - write_packet(fd, buf, total_hdr_len, daddr); -} - -static void recompute_packet(char *buf, char *no_ext, int extlen) -{ - struct tcphdr *tcphdr = (struct tcphdr *)(buf + tcp_offset); - struct ipv6hdr *ip6h = (struct ipv6hdr *)(buf + ETH_HLEN); - struct iphdr *iph = (struct iphdr *)(buf + ETH_HLEN); - - memmove(buf, no_ext, total_hdr_len); - memmove(buf + total_hdr_len + extlen, - no_ext + total_hdr_len, PAYLOAD_LEN); - - tcphdr->doff = tcphdr->doff + (extlen / 4); - tcphdr->check = 0; - tcphdr->check = tcp_checksum(tcphdr, PAYLOAD_LEN + extlen); - if (proto == PF_INET) { - iph->tot_len = htons(ntohs(iph->tot_len) + extlen); - iph->check = 0; - iph->check = checksum_fold(iph, sizeof(struct iphdr), 0); - - if (ipip) { - iph += 1; - iph->tot_len = htons(ntohs(iph->tot_len) + extlen); - iph->check = 0; - iph->check = checksum_fold(iph, sizeof(struct iphdr), 0); - } - } else { - ip6h->payload_len = htons(ntohs(ip6h->payload_len) + extlen); - } -} - -static void tcp_write_options(char *buf, int kind, int ts) -{ - struct tcp_option_ts { - uint8_t kind; - uint8_t len; - uint32_t tsval; - uint32_t tsecr; - } *opt_ts = (void *)buf; - struct tcp_option_window { - uint8_t kind; - uint8_t len; - uint8_t shift; - } *opt_window = (void *)buf; - - switch (kind) { - case TCPOPT_NOP: - buf[0] = TCPOPT_NOP; - break; - case TCPOPT_WINDOW: - memset(opt_window, 0, sizeof(struct tcp_option_window)); - opt_window->kind = TCPOPT_WINDOW; - opt_window->len = TCPOLEN_WINDOW; - opt_window->shift = 0; - break; - case TCPOPT_TIMESTAMP: - memset(opt_ts, 0, sizeof(struct tcp_option_ts)); - opt_ts->kind = TCPOPT_TIMESTAMP; - opt_ts->len = TCPOLEN_TIMESTAMP; - opt_ts->tsval = ts; - opt_ts->tsecr = 0; - break; - default: - error(1, 0, "unimplemented TCP option"); - break; - } -} - -/* TCP with options is always a permutation of {TS, NOP, NOP}. - * Implement different orders to verify coalescing stops. - */ -static void add_standard_tcp_options(char *buf, char *no_ext, int ts, int order) -{ - switch (order) { - case 0: - tcp_write_options(buf + total_hdr_len, TCPOPT_NOP, 0); - tcp_write_options(buf + total_hdr_len + 1, TCPOPT_NOP, 0); - tcp_write_options(buf + total_hdr_len + 2 /* two NOP opts */, - TCPOPT_TIMESTAMP, ts); - break; - case 1: - tcp_write_options(buf + total_hdr_len, TCPOPT_NOP, 0); - tcp_write_options(buf + total_hdr_len + 1, - TCPOPT_TIMESTAMP, ts); - tcp_write_options(buf + total_hdr_len + 1 + TCPOLEN_TIMESTAMP, - TCPOPT_NOP, 0); - break; - case 2: - tcp_write_options(buf + total_hdr_len, TCPOPT_TIMESTAMP, ts); - tcp_write_options(buf + total_hdr_len + TCPOLEN_TIMESTAMP + 1, - TCPOPT_NOP, 0); - tcp_write_options(buf + total_hdr_len + TCPOLEN_TIMESTAMP + 2, - TCPOPT_NOP, 0); - break; - default: - error(1, 0, "unknown order"); - break; - } - recompute_packet(buf, no_ext, TCPOLEN_TSTAMP_APPA); -} - -/* Packets with invalid checksum don't coalesce. */ -static void send_changed_checksum(int fd, struct sockaddr_ll *daddr) -{ - static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; - struct tcphdr *tcph = (struct tcphdr *)(buf + tcp_offset); - int pkt_size = total_hdr_len + PAYLOAD_LEN; - - create_packet(buf, 0, 0, PAYLOAD_LEN, 0); - write_packet(fd, buf, pkt_size, daddr); - - create_packet(buf, PAYLOAD_LEN, 0, PAYLOAD_LEN, 0); - tcph->check = tcph->check - 1; - write_packet(fd, buf, pkt_size, daddr); -} - - /* Packets with non-consecutive sequence number don't coalesce.*/ -static void send_changed_seq(int fd, struct sockaddr_ll *daddr) -{ - static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; - struct tcphdr *tcph = (struct tcphdr *)(buf + tcp_offset); - int pkt_size = total_hdr_len + PAYLOAD_LEN; - - create_packet(buf, 0, 0, PAYLOAD_LEN, 0); - write_packet(fd, buf, pkt_size, daddr); - - create_packet(buf, PAYLOAD_LEN, 0, PAYLOAD_LEN, 0); - tcph->seq = ntohl(htonl(tcph->seq) + 1); - tcph->check = 0; - tcph->check = tcp_checksum(tcph, PAYLOAD_LEN); - write_packet(fd, buf, pkt_size, daddr); -} - - /* Packet with different timestamp option or different timestamps - * don't coalesce. - */ -static void send_changed_ts(int fd, struct sockaddr_ll *daddr) -{ - static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; - static char extpkt[sizeof(buf) + TCPOLEN_TSTAMP_APPA]; - int pkt_size = total_hdr_len + PAYLOAD_LEN + TCPOLEN_TSTAMP_APPA; - - create_packet(buf, 0, 0, PAYLOAD_LEN, 0); - add_standard_tcp_options(extpkt, buf, 0, 0); - write_packet(fd, extpkt, pkt_size, daddr); - - create_packet(buf, PAYLOAD_LEN, 0, PAYLOAD_LEN, 0); - add_standard_tcp_options(extpkt, buf, 0, 0); - write_packet(fd, extpkt, pkt_size, daddr); - - create_packet(buf, PAYLOAD_LEN * 2, 0, PAYLOAD_LEN, 0); - add_standard_tcp_options(extpkt, buf, 100, 0); - write_packet(fd, extpkt, pkt_size, daddr); - - create_packet(buf, PAYLOAD_LEN * 3, 0, PAYLOAD_LEN, 0); - add_standard_tcp_options(extpkt, buf, 100, 1); - write_packet(fd, extpkt, pkt_size, daddr); - - create_packet(buf, PAYLOAD_LEN * 4, 0, PAYLOAD_LEN, 0); - add_standard_tcp_options(extpkt, buf, 100, 2); - write_packet(fd, extpkt, pkt_size, daddr); -} - -/* Packet with different tcp options don't coalesce. */ -static void send_diff_opt(int fd, struct sockaddr_ll *daddr) -{ - static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; - static char extpkt1[sizeof(buf) + TCPOLEN_TSTAMP_APPA]; - static char extpkt2[sizeof(buf) + TCPOLEN_MAXSEG]; - int extpkt1_size = total_hdr_len + PAYLOAD_LEN + TCPOLEN_TSTAMP_APPA; - int extpkt2_size = total_hdr_len + PAYLOAD_LEN + TCPOLEN_MAXSEG; - - create_packet(buf, 0, 0, PAYLOAD_LEN, 0); - add_standard_tcp_options(extpkt1, buf, 0, 0); - write_packet(fd, extpkt1, extpkt1_size, daddr); - - create_packet(buf, PAYLOAD_LEN, 0, PAYLOAD_LEN, 0); - add_standard_tcp_options(extpkt1, buf, 0, 0); - write_packet(fd, extpkt1, extpkt1_size, daddr); - - create_packet(buf, PAYLOAD_LEN * 2, 0, PAYLOAD_LEN, 0); - tcp_write_options(extpkt2 + MAX_HDR_LEN, TCPOPT_NOP, 0); - tcp_write_options(extpkt2 + MAX_HDR_LEN + 1, TCPOPT_WINDOW, 0); - recompute_packet(extpkt2, buf, TCPOLEN_WINDOW + 1); - write_packet(fd, extpkt2, extpkt2_size, daddr); -} - -static void add_ipv4_ts_option(void *buf, void *optpkt) -{ - struct ip_timestamp *ts = (struct ip_timestamp *)(optpkt + tcp_offset); - int optlen = sizeof(struct ip_timestamp); - struct iphdr *iph; - - if (optlen % 4) - error(1, 0, "ipv4 timestamp length is not a multiple of 4B"); - - ts->ipt_code = IPOPT_TS; - ts->ipt_len = optlen; - ts->ipt_ptr = 5; - ts->ipt_flg = IPOPT_TS_TSONLY; - - memcpy(optpkt, buf, tcp_offset); - memcpy(optpkt + tcp_offset + optlen, buf + tcp_offset, - sizeof(struct tcphdr) + PAYLOAD_LEN); - - iph = (struct iphdr *)(optpkt + ETH_HLEN); - iph->ihl = 5 + (optlen / 4); - iph->tot_len = htons(ntohs(iph->tot_len) + optlen); - iph->check = 0; - iph->check = checksum_fold(iph, sizeof(struct iphdr) + optlen, 0); -} - -static void add_ipv6_exthdr(void *buf, void *optpkt, __u8 exthdr_type, char *ext_payload) -{ - struct ipv6_opt_hdr *exthdr = (struct ipv6_opt_hdr *)(optpkt + tcp_offset); - struct ipv6hdr *iph = (struct ipv6hdr *)(optpkt + ETH_HLEN); - char *exthdr_payload_start = (char *)(exthdr + 1); - - exthdr->hdrlen = 0; - exthdr->nexthdr = IPPROTO_TCP; - - memcpy(exthdr_payload_start, ext_payload, MIN_EXTHDR_SIZE - sizeof(*exthdr)); - - memcpy(optpkt, buf, tcp_offset); - memcpy(optpkt + tcp_offset + MIN_EXTHDR_SIZE, buf + tcp_offset, - sizeof(struct tcphdr) + PAYLOAD_LEN); - - iph->nexthdr = exthdr_type; - iph->payload_len = htons(ntohs(iph->payload_len) + MIN_EXTHDR_SIZE); -} - -static void fix_ip4_checksum(struct iphdr *iph) -{ - iph->check = 0; - iph->check = checksum_fold(iph, sizeof(struct iphdr), 0); -} - -static void send_flush_id_case(int fd, struct sockaddr_ll *daddr, - enum flush_id_case tcase) -{ - static char buf1[MAX_HDR_LEN + PAYLOAD_LEN]; - static char buf2[MAX_HDR_LEN + PAYLOAD_LEN]; - static char buf3[MAX_HDR_LEN + PAYLOAD_LEN]; - bool send_three = false; - struct iphdr *iph1; - struct iphdr *iph2; - struct iphdr *iph3; - - iph1 = (struct iphdr *)(buf1 + ETH_HLEN); - iph2 = (struct iphdr *)(buf2 + ETH_HLEN); - iph3 = (struct iphdr *)(buf3 + ETH_HLEN); - - create_packet(buf1, 0, 0, PAYLOAD_LEN, 0); - create_packet(buf2, PAYLOAD_LEN, 0, PAYLOAD_LEN, 0); - create_packet(buf3, PAYLOAD_LEN * 2, 0, PAYLOAD_LEN, 0); - - switch (tcase) { - case FLUSH_ID_DF1_INC: /* DF=1, Incrementing - should coalesce */ - iph1->frag_off |= htons(IP_DF); - iph1->id = htons(8); - - iph2->frag_off |= htons(IP_DF); - iph2->id = htons(9); - break; - - case FLUSH_ID_DF1_FIXED: /* DF=1, Fixed - should coalesce */ - iph1->frag_off |= htons(IP_DF); - iph1->id = htons(8); - - iph2->frag_off |= htons(IP_DF); - iph2->id = htons(8); - break; - - case FLUSH_ID_DF0_INC: /* DF=0, Incrementing - should coalesce */ - iph1->frag_off &= ~htons(IP_DF); - iph1->id = htons(8); - - iph2->frag_off &= ~htons(IP_DF); - iph2->id = htons(9); - break; - - case FLUSH_ID_DF0_FIXED: /* DF=0, Fixed - should coalesce */ - iph1->frag_off &= ~htons(IP_DF); - iph1->id = htons(8); - - iph2->frag_off &= ~htons(IP_DF); - iph2->id = htons(8); - break; - - case FLUSH_ID_DF1_INC_FIXED: /* DF=1, two packets incrementing, and - * one fixed - should coalesce only the - * first two packets - */ - iph1->frag_off |= htons(IP_DF); - iph1->id = htons(8); - - iph2->frag_off |= htons(IP_DF); - iph2->id = htons(9); - - iph3->frag_off |= htons(IP_DF); - iph3->id = htons(9); - send_three = true; - break; - - case FLUSH_ID_DF1_FIXED_INC: /* DF=1, two packets fixed, and one - * incrementing - should coalesce only - * the first two packets - */ - iph1->frag_off |= htons(IP_DF); - iph1->id = htons(8); - - iph2->frag_off |= htons(IP_DF); - iph2->id = htons(8); - - iph3->frag_off |= htons(IP_DF); - iph3->id = htons(9); - send_three = true; - break; - } - - fix_ip4_checksum(iph1); - fix_ip4_checksum(iph2); - write_packet(fd, buf1, total_hdr_len + PAYLOAD_LEN, daddr); - write_packet(fd, buf2, total_hdr_len + PAYLOAD_LEN, daddr); - - if (send_three) { - fix_ip4_checksum(iph3); - write_packet(fd, buf3, total_hdr_len + PAYLOAD_LEN, daddr); - } -} - -static void send_ipv6_exthdr(int fd, struct sockaddr_ll *daddr, char *ext_data1, char *ext_data2) -{ - static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; - static char exthdr_pck[sizeof(buf) + MIN_EXTHDR_SIZE]; - - create_packet(buf, 0, 0, PAYLOAD_LEN, 0); - add_ipv6_exthdr(buf, exthdr_pck, IPPROTO_DSTOPTS, ext_data1); - write_packet(fd, exthdr_pck, total_hdr_len + PAYLOAD_LEN + MIN_EXTHDR_SIZE, daddr); - - create_packet(buf, PAYLOAD_LEN * 1, 0, PAYLOAD_LEN, 0); - add_ipv6_exthdr(buf, exthdr_pck, IPPROTO_DSTOPTS, ext_data2); - write_packet(fd, exthdr_pck, total_hdr_len + PAYLOAD_LEN + MIN_EXTHDR_SIZE, daddr); -} - -/* IPv4 options shouldn't coalesce */ -static void send_ip_options(int fd, struct sockaddr_ll *daddr) -{ - static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; - static char optpkt[sizeof(buf) + sizeof(struct ip_timestamp)]; - int optlen = sizeof(struct ip_timestamp); - int pkt_size = total_hdr_len + PAYLOAD_LEN + optlen; - - create_packet(buf, 0, 0, PAYLOAD_LEN, 0); - write_packet(fd, buf, total_hdr_len + PAYLOAD_LEN, daddr); - - create_packet(buf, PAYLOAD_LEN * 1, 0, PAYLOAD_LEN, 0); - add_ipv4_ts_option(buf, optpkt); - write_packet(fd, optpkt, pkt_size, daddr); - - create_packet(buf, PAYLOAD_LEN * 2, 0, PAYLOAD_LEN, 0); - write_packet(fd, buf, total_hdr_len + PAYLOAD_LEN, daddr); -} - -/* IPv4 fragments shouldn't coalesce */ -static void send_fragment4(int fd, struct sockaddr_ll *daddr) -{ - static char buf[IP_MAXPACKET]; - struct iphdr *iph = (struct iphdr *)(buf + ETH_HLEN); - int pkt_size = total_hdr_len + PAYLOAD_LEN; - - create_packet(buf, 0, 0, PAYLOAD_LEN, 0); - write_packet(fd, buf, pkt_size, daddr); - - /* Once fragmented, packet would retain the total_len. - * Tcp header is prepared as if rest of data is in follow-up frags, - * but follow up frags aren't actually sent. - */ - memset(buf + total_hdr_len, 'a', PAYLOAD_LEN * 2); - fill_transportlayer(buf + tcp_offset, PAYLOAD_LEN, 0, PAYLOAD_LEN * 2, 0); - fill_networklayer(buf + ETH_HLEN, PAYLOAD_LEN, IPPROTO_TCP); - fill_datalinklayer(buf); - - iph->frag_off = htons(0x6000); // DF = 1, MF = 1 - iph->check = 0; - iph->check = checksum_fold(iph, sizeof(struct iphdr), 0); - write_packet(fd, buf, pkt_size, daddr); -} - -/* IPv4 packets with different ttl don't coalesce.*/ -static void send_changed_ttl(int fd, struct sockaddr_ll *daddr) -{ - int pkt_size = total_hdr_len + PAYLOAD_LEN; - static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; - struct iphdr *iph = (struct iphdr *)(buf + ETH_HLEN); - - create_packet(buf, 0, 0, PAYLOAD_LEN, 0); - write_packet(fd, buf, pkt_size, daddr); - - create_packet(buf, PAYLOAD_LEN, 0, PAYLOAD_LEN, 0); - iph->ttl = 7; - iph->check = 0; - iph->check = checksum_fold(iph, sizeof(struct iphdr), 0); - write_packet(fd, buf, pkt_size, daddr); -} - -/* Packets with different tos don't coalesce.*/ -static void send_changed_tos(int fd, struct sockaddr_ll *daddr) -{ - int pkt_size = total_hdr_len + PAYLOAD_LEN; - static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; - struct iphdr *iph = (struct iphdr *)(buf + ETH_HLEN); - struct ipv6hdr *ip6h = (struct ipv6hdr *)(buf + ETH_HLEN); - - create_packet(buf, 0, 0, PAYLOAD_LEN, 0); - write_packet(fd, buf, pkt_size, daddr); - - create_packet(buf, PAYLOAD_LEN, 0, PAYLOAD_LEN, 0); - if (proto == PF_INET) { - iph->tos = 1; - iph->check = 0; - iph->check = checksum_fold(iph, sizeof(struct iphdr), 0); - } else if (proto == PF_INET6) { - ip6h->priority = 0xf; - } - write_packet(fd, buf, pkt_size, daddr); -} - -/* Packets with different ECN don't coalesce.*/ -static void send_changed_ECN(int fd, struct sockaddr_ll *daddr) -{ - int pkt_size = total_hdr_len + PAYLOAD_LEN; - static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; - struct iphdr *iph = (struct iphdr *)(buf + ETH_HLEN); - - create_packet(buf, 0, 0, PAYLOAD_LEN, 0); - write_packet(fd, buf, pkt_size, daddr); - - create_packet(buf, PAYLOAD_LEN, 0, PAYLOAD_LEN, 0); - if (proto == PF_INET) { - buf[ETH_HLEN + 1] ^= 0x2; // ECN set to 10 - iph->check = 0; - iph->check = checksum_fold(iph, sizeof(struct iphdr), 0); - } else { - buf[ETH_HLEN + 1] ^= 0x20; // ECN set to 10 - } - write_packet(fd, buf, pkt_size, daddr); -} - -/* IPv6 fragments and packets with extensions don't coalesce.*/ -static void send_fragment6(int fd, struct sockaddr_ll *daddr) -{ - static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; - static char extpkt[MAX_HDR_LEN + PAYLOAD_LEN + - sizeof(struct ip6_frag)]; - struct ipv6hdr *ip6h = (struct ipv6hdr *)(buf + ETH_HLEN); - struct ip6_frag *frag = (void *)(extpkt + tcp_offset); - int extlen = sizeof(struct ip6_frag); - int bufpkt_len = total_hdr_len + PAYLOAD_LEN; - int extpkt_len = bufpkt_len + extlen; - int i; - - for (i = 0; i < 2; i++) { - create_packet(buf, PAYLOAD_LEN * i, 0, PAYLOAD_LEN, 0); - write_packet(fd, buf, bufpkt_len, daddr); - } - sleep(1); - create_packet(buf, PAYLOAD_LEN * 2, 0, PAYLOAD_LEN, 0); - memset(extpkt, 0, extpkt_len); - - ip6h->nexthdr = IPPROTO_FRAGMENT; - ip6h->payload_len = htons(ntohs(ip6h->payload_len) + extlen); - frag->ip6f_nxt = IPPROTO_TCP; - - memcpy(extpkt, buf, tcp_offset); - memcpy(extpkt + tcp_offset + extlen, buf + tcp_offset, - sizeof(struct tcphdr) + PAYLOAD_LEN); - write_packet(fd, extpkt, extpkt_len, daddr); - - create_packet(buf, PAYLOAD_LEN * 3, 0, PAYLOAD_LEN, 0); - write_packet(fd, buf, bufpkt_len, daddr); -} - -static void bind_packetsocket(int fd) -{ - struct sockaddr_ll daddr = {}; - - daddr.sll_family = AF_PACKET; - daddr.sll_protocol = ethhdr_proto; - daddr.sll_ifindex = if_nametoindex(ifname); - if (daddr.sll_ifindex == 0) - error(1, errno, "if_nametoindex"); - - if (bind(fd, (void *)&daddr, sizeof(daddr)) < 0) - error(1, errno, "could not bind socket"); -} - -static void set_timeout(int fd) -{ - struct timeval timeout; - - timeout.tv_sec = 3; - timeout.tv_usec = 0; - if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, - sizeof(timeout)) < 0) - error(1, errno, "cannot set timeout, setsockopt failed"); -} - -static void set_rcvbuf(int fd) -{ - int bufsize = 1 * 1024 * 1024; /* 1 MB */ - - if (setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize))) - error(1, errno, "cannot set rcvbuf size, setsockopt failed"); -} - -static void recv_error(int fd, int rcv_errno) -{ - struct tpacket_stats stats; - socklen_t len; - - len = sizeof(stats); - if (getsockopt(fd, SOL_PACKET, PACKET_STATISTICS, &stats, &len)) - error(1, errno, "can't get stats"); - - fprintf(stderr, "Socket stats: packets=%u, drops=%u\n", - stats.tp_packets, stats.tp_drops); - error(1, rcv_errno, "could not receive"); -} - -static void check_recv_pkts(int fd, int *correct_payload, - int correct_num_pkts) -{ - static char buffer[IP_MAXPACKET + ETH_HLEN + 1]; - struct iphdr *iph = (struct iphdr *)(buffer + ETH_HLEN); - struct ipv6hdr *ip6h = (struct ipv6hdr *)(buffer + ETH_HLEN); - struct tcphdr *tcph; - bool bad_packet = false; - int tcp_ext_len = 0; - int ip_ext_len = 0; - int pkt_size = -1; - int data_len = 0; - int num_pkt = 0; - int i; - - vlog("Expected {"); - for (i = 0; i < correct_num_pkts; i++) - vlog("%d ", correct_payload[i]); - vlog("}, Total %d packets\nReceived {", correct_num_pkts); - - while (1) { - ip_ext_len = 0; - pkt_size = recv(fd, buffer, IP_MAXPACKET + ETH_HLEN + 1, 0); - if (pkt_size < 0) - recv_error(fd, errno); - - if (iph->version == 4) - ip_ext_len = (iph->ihl - 5) * 4; - else if (ip6h->version == 6 && ip6h->nexthdr != IPPROTO_TCP) - ip_ext_len = MIN_EXTHDR_SIZE; - - tcph = (struct tcphdr *)(buffer + tcp_offset + ip_ext_len); - - if (tcph->fin) - break; - - tcp_ext_len = (tcph->doff - 5) * 4; - data_len = pkt_size - total_hdr_len - tcp_ext_len - ip_ext_len; - /* Min ethernet frame payload is 46(ETH_ZLEN - ETH_HLEN) by RFC 802.3. - * Ipv4/tcp packets without at least 6 bytes of data will be padded. - * Packet sockets are protocol agnostic, and will not trim the padding. - */ - if (pkt_size == ETH_ZLEN && iph->version == 4) { - data_len = ntohs(iph->tot_len) - - sizeof(struct tcphdr) - sizeof(struct iphdr); - } - vlog("%d ", data_len); - if (data_len != correct_payload[num_pkt]) { - vlog("[!=%d]", correct_payload[num_pkt]); - bad_packet = true; - } - num_pkt++; - } - vlog("}, Total %d packets.\n", num_pkt); - if (num_pkt != correct_num_pkts) - error(1, 0, "incorrect number of packets"); - if (bad_packet) - error(1, 0, "incorrect packet geometry"); - - printf("Test succeeded\n\n"); -} - -static void gro_sender(void) -{ - const int fin_delay_us = 100 * 1000; - static char fin_pkt[MAX_HDR_LEN]; - struct sockaddr_ll daddr = {}; - int txfd = -1; - - txfd = socket(PF_PACKET, SOCK_RAW, IPPROTO_RAW); - if (txfd < 0) - error(1, errno, "socket creation"); - - memset(&daddr, 0, sizeof(daddr)); - daddr.sll_ifindex = if_nametoindex(ifname); - if (daddr.sll_ifindex == 0) - error(1, errno, "if_nametoindex"); - daddr.sll_family = AF_PACKET; - memcpy(daddr.sll_addr, dst_mac, ETH_ALEN); - daddr.sll_halen = ETH_ALEN; - create_packet(fin_pkt, PAYLOAD_LEN * 2, 0, 0, 1); - - /* data sub-tests */ - if (strcmp(testname, "data_same") == 0) { - send_data_pkts(txfd, &daddr, PAYLOAD_LEN, PAYLOAD_LEN); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - } else if (strcmp(testname, "data_lrg_sml") == 0) { - send_data_pkts(txfd, &daddr, PAYLOAD_LEN, PAYLOAD_LEN / 2); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - } else if (strcmp(testname, "data_sml_lrg") == 0) { - send_data_pkts(txfd, &daddr, PAYLOAD_LEN / 2, PAYLOAD_LEN); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - - /* ack test */ - } else if (strcmp(testname, "ack") == 0) { - send_ack(txfd, &daddr); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - - /* flags sub-tests */ - } else if (strcmp(testname, "flags_psh") == 0) { - send_flags(txfd, &daddr, 1, 0, 0, 0, 0); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - } else if (strcmp(testname, "flags_syn") == 0) { - send_flags(txfd, &daddr, 0, 1, 0, 0, 0); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - } else if (strcmp(testname, "flags_rst") == 0) { - send_flags(txfd, &daddr, 0, 0, 1, 0, 0); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - } else if (strcmp(testname, "flags_urg") == 0) { - send_flags(txfd, &daddr, 0, 0, 0, 1, 0); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - } else if (strcmp(testname, "flags_cwr") == 0) { - send_flags(txfd, &daddr, 0, 0, 0, 0, 1); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - - /* tcp sub-tests */ - } else if (strcmp(testname, "tcp_csum") == 0) { - send_changed_checksum(txfd, &daddr); - usleep(fin_delay_us); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - } else if (strcmp(testname, "tcp_seq") == 0) { - send_changed_seq(txfd, &daddr); - usleep(fin_delay_us); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - } else if (strcmp(testname, "tcp_ts") == 0) { - send_changed_ts(txfd, &daddr); - usleep(fin_delay_us); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - } else if (strcmp(testname, "tcp_opt") == 0) { - send_diff_opt(txfd, &daddr); - usleep(fin_delay_us); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - - /* ip sub-tests - shared between IPv4 and IPv6 */ - } else if (strcmp(testname, "ip_ecn") == 0) { - send_changed_ECN(txfd, &daddr); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - } else if (strcmp(testname, "ip_tos") == 0) { - send_changed_tos(txfd, &daddr); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - - /* ip sub-tests - IPv4 only */ - } else if (strcmp(testname, "ip_ttl") == 0) { - send_changed_ttl(txfd, &daddr); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - } else if (strcmp(testname, "ip_opt") == 0) { - send_ip_options(txfd, &daddr); - usleep(fin_delay_us); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - } else if (strcmp(testname, "ip_frag4") == 0) { - send_fragment4(txfd, &daddr); - usleep(fin_delay_us); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - } else if (strcmp(testname, "ip_id_df1_inc") == 0) { - send_flush_id_case(txfd, &daddr, FLUSH_ID_DF1_INC); - usleep(fin_delay_us); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - } else if (strcmp(testname, "ip_id_df1_fixed") == 0) { - send_flush_id_case(txfd, &daddr, FLUSH_ID_DF1_FIXED); - usleep(fin_delay_us); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - } else if (strcmp(testname, "ip_id_df0_inc") == 0) { - send_flush_id_case(txfd, &daddr, FLUSH_ID_DF0_INC); - usleep(fin_delay_us); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - } else if (strcmp(testname, "ip_id_df0_fixed") == 0) { - send_flush_id_case(txfd, &daddr, FLUSH_ID_DF0_FIXED); - usleep(fin_delay_us); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - } else if (strcmp(testname, "ip_id_df1_inc_fixed") == 0) { - send_flush_id_case(txfd, &daddr, FLUSH_ID_DF1_INC_FIXED); - usleep(fin_delay_us); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - } else if (strcmp(testname, "ip_id_df1_fixed_inc") == 0) { - send_flush_id_case(txfd, &daddr, FLUSH_ID_DF1_FIXED_INC); - usleep(fin_delay_us); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - - /* ip sub-tests - IPv6 only */ - } else if (strcmp(testname, "ip_frag6") == 0) { - send_fragment6(txfd, &daddr); - usleep(fin_delay_us); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - } else if (strcmp(testname, "ip_v6ext_same") == 0) { - send_ipv6_exthdr(txfd, &daddr, EXT_PAYLOAD_1, EXT_PAYLOAD_1); - usleep(fin_delay_us); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - } else if (strcmp(testname, "ip_v6ext_diff") == 0) { - send_ipv6_exthdr(txfd, &daddr, EXT_PAYLOAD_1, EXT_PAYLOAD_2); - usleep(fin_delay_us); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - - /* large sub-tests */ - } else if (strcmp(testname, "large_max") == 0) { - int offset = (proto == PF_INET && !ipip) ? 20 : 0; - int remainder = (MAX_PAYLOAD + offset) % MSS; - - send_large(txfd, &daddr, remainder); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - } else if (strcmp(testname, "large_rem") == 0) { - int offset = (proto == PF_INET && !ipip) ? 20 : 0; - int remainder = (MAX_PAYLOAD + offset) % MSS; - - send_large(txfd, &daddr, remainder + 1); - write_packet(txfd, fin_pkt, total_hdr_len, &daddr); - } else { - error(1, 0, "Unknown testcase: %s", testname); - } - - if (close(txfd)) - error(1, errno, "socket close"); -} - -static void gro_receiver(void) -{ - static int correct_payload[NUM_PACKETS]; - int rxfd = -1; - - rxfd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_NONE)); - if (rxfd < 0) - error(1, 0, "socket creation"); - setup_sock_filter(rxfd); - set_timeout(rxfd); - set_rcvbuf(rxfd); - bind_packetsocket(rxfd); - - ksft_ready(); - - memset(correct_payload, 0, sizeof(correct_payload)); - - /* data sub-tests */ - if (strcmp(testname, "data_same") == 0) { - printf("pure data packet of same size: "); - correct_payload[0] = PAYLOAD_LEN * 2; - check_recv_pkts(rxfd, correct_payload, 1); - } else if (strcmp(testname, "data_lrg_sml") == 0) { - printf("large data packets followed by a smaller one: "); - correct_payload[0] = PAYLOAD_LEN * 1.5; - check_recv_pkts(rxfd, correct_payload, 1); - } else if (strcmp(testname, "data_sml_lrg") == 0) { - printf("small data packets followed by a larger one: "); - correct_payload[0] = PAYLOAD_LEN / 2; - correct_payload[1] = PAYLOAD_LEN; - check_recv_pkts(rxfd, correct_payload, 2); - - /* ack test */ - } else if (strcmp(testname, "ack") == 0) { - printf("duplicate ack and pure ack: "); - check_recv_pkts(rxfd, correct_payload, 3); - - /* flags sub-tests */ - } else if (strcmp(testname, "flags_psh") == 0) { - correct_payload[0] = PAYLOAD_LEN * 3; - correct_payload[1] = PAYLOAD_LEN * 2; - printf("psh flag ends coalescing: "); - check_recv_pkts(rxfd, correct_payload, 2); - } else if (strcmp(testname, "flags_syn") == 0) { - correct_payload[0] = PAYLOAD_LEN * 2; - correct_payload[1] = 0; - correct_payload[2] = PAYLOAD_LEN * 2; - printf("syn flag ends coalescing: "); - check_recv_pkts(rxfd, correct_payload, 3); - } else if (strcmp(testname, "flags_rst") == 0) { - correct_payload[0] = PAYLOAD_LEN * 2; - correct_payload[1] = 0; - correct_payload[2] = PAYLOAD_LEN * 2; - printf("rst flag ends coalescing: "); - check_recv_pkts(rxfd, correct_payload, 3); - } else if (strcmp(testname, "flags_urg") == 0) { - correct_payload[0] = PAYLOAD_LEN * 2; - correct_payload[1] = 0; - correct_payload[2] = PAYLOAD_LEN * 2; - printf("urg flag ends coalescing: "); - check_recv_pkts(rxfd, correct_payload, 3); - } else if (strcmp(testname, "flags_cwr") == 0) { - correct_payload[0] = PAYLOAD_LEN; - correct_payload[1] = PAYLOAD_LEN * 2; - correct_payload[2] = PAYLOAD_LEN * 2; - printf("cwr flag ends coalescing: "); - check_recv_pkts(rxfd, correct_payload, 3); - - /* tcp sub-tests */ - } else if (strcmp(testname, "tcp_csum") == 0) { - correct_payload[0] = PAYLOAD_LEN; - correct_payload[1] = PAYLOAD_LEN; - printf("changed checksum does not coalesce: "); - check_recv_pkts(rxfd, correct_payload, 2); - } else if (strcmp(testname, "tcp_seq") == 0) { - correct_payload[0] = PAYLOAD_LEN; - correct_payload[1] = PAYLOAD_LEN; - printf("Wrong Seq number doesn't coalesce: "); - check_recv_pkts(rxfd, correct_payload, 2); - } else if (strcmp(testname, "tcp_ts") == 0) { - correct_payload[0] = PAYLOAD_LEN * 2; - correct_payload[1] = PAYLOAD_LEN; - correct_payload[2] = PAYLOAD_LEN; - correct_payload[3] = PAYLOAD_LEN; - printf("Different timestamp doesn't coalesce: "); - check_recv_pkts(rxfd, correct_payload, 4); - } else if (strcmp(testname, "tcp_opt") == 0) { - correct_payload[0] = PAYLOAD_LEN * 2; - correct_payload[1] = PAYLOAD_LEN; - printf("Different options doesn't coalesce: "); - check_recv_pkts(rxfd, correct_payload, 2); - - /* ip sub-tests - shared between IPv4 and IPv6 */ - } else if (strcmp(testname, "ip_ecn") == 0) { - correct_payload[0] = PAYLOAD_LEN; - correct_payload[1] = PAYLOAD_LEN; - printf("different ECN doesn't coalesce: "); - check_recv_pkts(rxfd, correct_payload, 2); - } else if (strcmp(testname, "ip_tos") == 0) { - correct_payload[0] = PAYLOAD_LEN; - correct_payload[1] = PAYLOAD_LEN; - printf("different tos doesn't coalesce: "); - check_recv_pkts(rxfd, correct_payload, 2); - - /* ip sub-tests - IPv4 only */ - } else if (strcmp(testname, "ip_ttl") == 0) { - correct_payload[0] = PAYLOAD_LEN; - correct_payload[1] = PAYLOAD_LEN; - printf("different ttl doesn't coalesce: "); - check_recv_pkts(rxfd, correct_payload, 2); - } else if (strcmp(testname, "ip_opt") == 0) { - correct_payload[0] = PAYLOAD_LEN; - correct_payload[1] = PAYLOAD_LEN; - correct_payload[2] = PAYLOAD_LEN; - printf("ip options doesn't coalesce: "); - check_recv_pkts(rxfd, correct_payload, 3); - } else if (strcmp(testname, "ip_frag4") == 0) { - correct_payload[0] = PAYLOAD_LEN; - correct_payload[1] = PAYLOAD_LEN; - printf("fragmented ip4 doesn't coalesce: "); - check_recv_pkts(rxfd, correct_payload, 2); - } else if (strcmp(testname, "ip_id_df1_inc") == 0) { - printf("DF=1, Incrementing - should coalesce: "); - correct_payload[0] = PAYLOAD_LEN * 2; - check_recv_pkts(rxfd, correct_payload, 1); - } else if (strcmp(testname, "ip_id_df1_fixed") == 0) { - printf("DF=1, Fixed - should coalesce: "); - correct_payload[0] = PAYLOAD_LEN * 2; - check_recv_pkts(rxfd, correct_payload, 1); - } else if (strcmp(testname, "ip_id_df0_inc") == 0) { - printf("DF=0, Incrementing - should coalesce: "); - correct_payload[0] = PAYLOAD_LEN * 2; - check_recv_pkts(rxfd, correct_payload, 1); - } else if (strcmp(testname, "ip_id_df0_fixed") == 0) { - printf("DF=0, Fixed - should coalesce: "); - correct_payload[0] = PAYLOAD_LEN * 2; - check_recv_pkts(rxfd, correct_payload, 1); - } else if (strcmp(testname, "ip_id_df1_inc_fixed") == 0) { - printf("DF=1, 2 Incrementing and one fixed - should coalesce only first 2 packets: "); - correct_payload[0] = PAYLOAD_LEN * 2; - correct_payload[1] = PAYLOAD_LEN; - check_recv_pkts(rxfd, correct_payload, 2); - } else if (strcmp(testname, "ip_id_df1_fixed_inc") == 0) { - printf("DF=1, 2 Fixed and one incrementing - should coalesce only first 2 packets: "); - correct_payload[0] = PAYLOAD_LEN * 2; - correct_payload[1] = PAYLOAD_LEN; - check_recv_pkts(rxfd, correct_payload, 2); - - /* ip sub-tests - IPv6 only */ - } else if (strcmp(testname, "ip_frag6") == 0) { - /* GRO doesn't check for ipv6 hop limit when flushing. - * Hence no corresponding test to the ipv4 case. - */ - printf("fragmented ip6 doesn't coalesce: "); - correct_payload[0] = PAYLOAD_LEN * 2; - correct_payload[1] = PAYLOAD_LEN; - correct_payload[2] = PAYLOAD_LEN; - check_recv_pkts(rxfd, correct_payload, 3); - } else if (strcmp(testname, "ip_v6ext_same") == 0) { - printf("ipv6 with ext header does coalesce: "); - correct_payload[0] = PAYLOAD_LEN * 2; - check_recv_pkts(rxfd, correct_payload, 1); - } else if (strcmp(testname, "ip_v6ext_diff") == 0) { - printf("ipv6 with ext header with different payloads doesn't coalesce: "); - correct_payload[0] = PAYLOAD_LEN; - correct_payload[1] = PAYLOAD_LEN; - check_recv_pkts(rxfd, correct_payload, 2); - - /* large sub-tests */ - } else if (strcmp(testname, "large_max") == 0) { - int offset = (proto == PF_INET && !ipip) ? 20 : 0; - int remainder = (MAX_PAYLOAD + offset) % MSS; - - correct_payload[0] = (MAX_PAYLOAD + offset); - correct_payload[1] = remainder; - printf("Shouldn't coalesce if exceed IP max pkt size: "); - check_recv_pkts(rxfd, correct_payload, 2); - } else if (strcmp(testname, "large_rem") == 0) { - int offset = (proto == PF_INET && !ipip) ? 20 : 0; - int remainder = (MAX_PAYLOAD + offset) % MSS; - - /* last segment sent individually, doesn't start new segment */ - correct_payload[0] = (MAX_PAYLOAD + offset) - remainder; - correct_payload[1] = remainder + 1; - correct_payload[2] = remainder + 1; - printf("last segment sent individually: "); - check_recv_pkts(rxfd, correct_payload, 3); - } else { - error(1, 0, "Test case error: unknown testname %s", testname); - } - - if (close(rxfd)) - error(1, 0, "socket close"); -} - -static void parse_args(int argc, char **argv) -{ - static const struct option opts[] = { - { "daddr", required_argument, NULL, 'd' }, - { "dmac", required_argument, NULL, 'D' }, - { "iface", required_argument, NULL, 'i' }, - { "ipv4", no_argument, NULL, '4' }, - { "ipv6", no_argument, NULL, '6' }, - { "ipip", no_argument, NULL, 'e' }, - { "rx", no_argument, NULL, 'r' }, - { "saddr", required_argument, NULL, 's' }, - { "smac", required_argument, NULL, 'S' }, - { "test", required_argument, NULL, 't' }, - { "verbose", no_argument, NULL, 'v' }, - { 0, 0, 0, 0 } - }; - int c; - - while ((c = getopt_long(argc, argv, "46d:D:ei:rs:S:t:v", opts, NULL)) != -1) { - switch (c) { - case '4': - proto = PF_INET; - ethhdr_proto = htons(ETH_P_IP); - break; - case '6': - proto = PF_INET6; - ethhdr_proto = htons(ETH_P_IPV6); - break; - case 'e': - ipip = true; - proto = PF_INET; - ethhdr_proto = htons(ETH_P_IP); - break; - case 'd': - addr4_dst = addr6_dst = optarg; - break; - case 'D': - dmac = optarg; - break; - case 'i': - ifname = optarg; - break; - case 'r': - tx_socket = false; - break; - case 's': - addr4_src = addr6_src = optarg; - break; - case 'S': - smac = optarg; - break; - case 't': - testname = optarg; - break; - case 'v': - verbose = true; - break; - default: - error(1, 0, "%s invalid option %c\n", __func__, c); - break; - } - } -} - -int main(int argc, char **argv) -{ - parse_args(argc, argv); - - if (ipip) { - tcp_offset = ETH_HLEN + sizeof(struct iphdr) * 2; - total_hdr_len = tcp_offset + sizeof(struct tcphdr); - } else if (proto == PF_INET) { - tcp_offset = ETH_HLEN + sizeof(struct iphdr); - total_hdr_len = tcp_offset + sizeof(struct tcphdr); - } else if (proto == PF_INET6) { - tcp_offset = ETH_HLEN + sizeof(struct ipv6hdr); - total_hdr_len = MAX_HDR_LEN; - } else { - error(1, 0, "Protocol family is not ipv4 or ipv6"); - } - - read_MAC(src_mac, smac); - read_MAC(dst_mac, dmac); - - if (tx_socket) { - gro_sender(); - } else { - /* Only the receiver exit status determines test success. */ - gro_receiver(); - fprintf(stderr, "Gro::%s test passed.\n", testname); - } - - return 0; -} diff --git a/tools/testing/selftests/drivers/net/gro.py b/tools/testing/selftests/drivers/net/gro.py index cbc1b19dbc91..6ab8c97880d1 100755 --- a/tools/testing/selftests/drivers/net/gro.py +++ b/tools/testing/selftests/drivers/net/gro.py @@ -11,6 +11,7 @@ coalescing behavior. Test cases: - data_same: Same size data packets coalesce - data_lrg_sml: Large packet followed by smaller one coalesces + - data_lrg_1byte: Large packet followed by 1B one coalesces (Ethernet padding) - data_sml_lrg: Small packet followed by larger one doesn't coalesce - ack: Pure ACK packets do not coalesce - flags_psh: Packets with PSH flag don't coalesce @@ -35,11 +36,18 @@ Test cases: - large_rem: Large packet remainder handling """ +import glob import os +import re from lib.py import ksft_run, ksft_exit, ksft_pr -from lib.py import NetDrvEpEnv, KsftXfailEx +from lib.py import NetDrvEpEnv, KsftFailEx, KsftXfailEx +from lib.py import NetdevFamily, EthtoolFamily from lib.py import bkg, cmd, defer, ethtool, ip -from lib.py import ksft_variants +from lib.py import ksft_variants, KsftNamedVariant + + +# gro.c uses hardcoded DPORT=8000 +GRO_DPORT = 8000 def _resolve_dmac(cfg, ipver): @@ -113,11 +121,113 @@ def _set_ethtool_feat(dev, current, feats, host=None): ksft_pr(eth_cmd) +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 _require_ntuple(cfg): + features = ethtool(f"-k {cfg.ifname}", json=True)[0] + if not features["ntuple-filters"]["active"]: + if features["ntuple-filters"]["fixed"]: + raise KsftXfailEx("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 KsftXfailEx(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 _setup_queue_count(cfg, num_queues): + """Configure the NIC to use a specific number of queues.""" + channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}}) + ch_max = channels.get('combined-max', 0) + qcnt = channels['combined-count'] + + if ch_max < num_queues: + raise KsftXfailEx(f"Need at least {num_queues} queues, max={ch_max}") + + defer(ethtool, f"-L {cfg.ifname} combined {qcnt}") + ethtool(f"-L {cfg.ifname} combined {num_queues}") + + +def _run_gro_bin(cfg, test_name, protocol=None, num_flows=None, + order_check=False, verbose=False, fail=False): + """Run gro binary with given test and return the process result.""" + if not hasattr(cfg, "bin_remote"): + cfg.bin_local = cfg.net_lib_dir / "gro" + cfg.bin_remote = cfg.remote.deploy(cfg.bin_local) + + if protocol is None: + ipver = cfg.addr_ipver + protocol = f"ipv{ipver}" + else: + ipver = "6" if protocol[-1] == "6" else "4" + + dmac = _resolve_dmac(cfg, ipver) + + base_args = [ + f"--{protocol}", + f"--dmac {dmac}", + f"--smac {cfg.remote_dev['address']}", + f"--daddr {cfg.addr_v[ipver]}", + 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") + if verbose: + base_args.append("--verbose") + + 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=fail) as rx_proc: + cmd(tx_cmd, host=cfg.remote) + + return rx_proc + + def _setup(cfg, mode, test_name): """ Setup hardware loopback mode for GRO testing. """ if not hasattr(cfg, "bin_remote"): - cfg.bin_local = cfg.test_dir / "gro" + cfg.bin_local = cfg.net_lib_dir / "gro" cfg.bin_remote = cfg.remote.deploy(cfg.bin_local) if not hasattr(cfg, "feat"): @@ -190,7 +300,8 @@ def _gro_variants(): # Tests that work for all protocols common_tests = [ - "data_same", "data_lrg_sml", "data_sml_lrg", + "data_same", "data_lrg_sml", "data_sml_lrg", "data_lrg_1byte", + "data_burst", "ack", "flags_psh", "flags_syn", "flags_rst", "flags_urg", "flags_cwr", "tcp_csum", "tcp_seq", "tcp_ts", "tcp_opt", @@ -200,6 +311,7 @@ def _gro_variants(): # Tests specific to IPv4 ipv4_tests = [ + "ip_csum", "ip_ttl", "ip_opt", "ip_frag4", "ip_id_df1_inc", "ip_id_df1_fixed", "ip_id_df0_inc", "ip_id_df0_fixed", @@ -211,8 +323,14 @@ def _gro_variants(): "ip_frag6", "ip_v6ext_same", "ip_v6ext_diff", ] + # Tests specific to PPPoE + pppoe_tests = [ + "data_same", "data_lrg_sml", "data_sml_lrg", "data_lrg_1byte", + "data_burst", "pppoe_sid", + ] + for mode in ["sw", "hw", "lro"]: - for protocol in ["ipv4", "ipv6", "ipip"]: + for protocol in ["ipv4", "ipv6", "ipip", "ip6ip6"]: for test_name in common_tests: yield mode, protocol, test_name @@ -223,6 +341,11 @@ def _gro_variants(): for test_name in ipv6_tests: yield mode, protocol, test_name + for mode in ["sw"]: + for protocol in ["pppoev4", "pppoev6"]: + for test_name in pppoe_tests: + yield mode, protocol, test_name + @ksft_variants(_gro_variants()) def test(cfg, mode, protocol, test_name): @@ -233,36 +356,26 @@ def test(cfg, mode, protocol, test_name): _setup(cfg, mode, test_name) - base_cmd_args = [ - f"--{protocol}", - f"--dmac {_resolve_dmac(cfg, ipver)}", - f"--smac {cfg.remote_dev['address']}", - f"--daddr {cfg.addr_v[ipver]}", - f"--saddr {cfg.remote_addr_v[ipver]}", - f"--test {test_name}", - "--verbose" - ] - base_args = " ".join(base_cmd_args) - # Each test is run 6 times to deflake, because given the receive timing, # not all packets that should coalesce will be considered in the same flow # on every try. max_retries = 6 for attempt in range(max_retries): - rx_cmd = f"{cfg.bin_local} {base_args} --rx --iface {cfg.ifname}" - tx_cmd = f"{cfg.bin_remote} {base_args} --iface {cfg.remote_ifname}" - fail_now = attempt >= max_retries - 1 - - with bkg(rx_cmd, ksft_ready=True, exit_wait=True, - fail=fail_now) as rx_proc: - cmd(tx_cmd, host=cfg.remote) + rx_proc = _run_gro_bin(cfg, test_name, protocol=protocol, + verbose=True, fail=fail_now) if rx_proc.ret == 0: return ksft_pr(rx_proc) + # ret==42 means the receiver detected over-coalescing. + # This is unambiguous proof of a bug, retries can only cause + # false negatives. + if rx_proc.ret == 42: + raise KsftFailEx(f"GRO over-coalesced in {protocol}/{test_name}") + if test_name.startswith("large_") and os.environ.get("KSFT_MACHINE_SLOW"): ksft_pr(f"Ignoring {protocol}/{test_name} failure due to slow environment") return @@ -270,11 +383,89 @@ def test(cfg, mode, protocol, test_name): ksft_pr(f"Attempt {attempt + 1}/{max_retries} failed, retrying...") +def _capacity_variants(): + """Generate variants for capacity test: mode x queue setup.""" + setups = [ + ("isolated", _setup_isolated_queue), + ("1q", lambda cfg: _setup_queue_count(cfg, 1)), + ("8q", lambda cfg: _setup_queue_count(cfg, 8)), + ] + for mode in ["sw", "hw", "lro"]: + for name, func in setups: + yield KsftNamedVariant(f"{mode}_{name}", mode, func) + + +@ksft_variants(_capacity_variants()) +def test_gro_capacity(cfg, mode, setup_func): + """ + Probe GRO capacity. + + Start with 8 flows and increase by 2x on each successful run. + Retry up to 3 times on failure. + + Variants combine mode (sw, hw, lro) with queue setup: + - isolated: Use a single queue isolated from RSS + - 1q: Configure NIC to use 1 queue + - 8q: Configure NIC to use 8 queues + """ + max_retries = 3 + + _setup(cfg, mode, "capacity") + queue_id = setup_func(cfg) + + num_flows = 8 + while True: + success = False + for attempt in range(max_retries): + if queue_id is not None: + stats_before = _get_queue_stats(cfg, queue_id) + + rx_proc = _run_gro_bin(cfg, "capacity", num_flows=num_flows) + output = rx_proc.stdout + + if queue_id is not None: + stats_after = _get_queue_stats(cfg, queue_id) + qstat_pkts = (stats_after.get('rx-packets', 0) - + stats_before.get('rx-packets', 0)) + gro_pkts = (stats_after.get('rx-hw-gro-packets', 0) - + stats_before.get('rx-hw-gro-packets', 0)) + qstat_str = f" qstat={qstat_pkts} hw-gro={gro_pkts}" + else: + qstat_str = "" + + # Parse and print STATS line + match = re.search( + r'STATS: received=(\d+) wire=(\d+) coalesced=(\d+)', output) + if match: + received = int(match.group(1)) + wire = int(match.group(2)) + coalesced = int(match.group(3)) + status = "PASS" if received == num_flows else "MISS" + ksft_pr(f"flows={num_flows} attempt={attempt + 1} " + f"received={received} wire={wire} " + f"coalesced={coalesced}{qstat_str} [{status}]") + if received == num_flows: + success = True + break + else: + ksft_pr(rx_proc) + ksft_pr(f"flows={num_flows} attempt={attempt + 1}" + f"{qstat_str} [FAIL - can't parse stats]") + + if not success: + ksft_pr(f"Stopped at {num_flows} flows") + break + + num_flows *= 2 + + def main() -> None: """ Ksft boiler plate main """ with NetDrvEpEnv(__file__) as cfg: - ksft_run(cases=[test], args=(cfg,)) + cfg.ethnl = EthtoolFamily() + cfg.netnl = NetdevFamily() + ksft_run(cases=[test, test_gro_capacity], args=(cfg,)) ksft_exit() diff --git a/tools/testing/selftests/drivers/net/hw/Makefile b/tools/testing/selftests/drivers/net/hw/Makefile index a64140333a46..78bb0169350b 100644 --- a/tools/testing/selftests/drivers/net/hw/Makefile +++ b/tools/testing/selftests/drivers/net/hw/Makefile @@ -19,19 +19,26 @@ TEST_GEN_FILES := \ TEST_PROGS = \ csum.py \ - devlink_port_split.py \ + devlink_rate_cross_esw.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 \ @@ -40,10 +47,18 @@ TEST_PROGS = \ rss_input_xfrm.py \ toeplitz.py \ tso.py \ + userns_devmem.py \ + uso.py \ + xdp_metadata.py \ xsk_reconfig.py \ # +TEST_PROGS_EXTENDED := \ + devlink_port_split.py \ +# end of TEST_PROGS_EXTENDED + TEST_FILES := \ + devmem_lib.py \ ethtool_lib.sh \ # diff --git a/tools/testing/selftests/drivers/net/hw/config b/tools/testing/selftests/drivers/net/hw/config index 2307aa001be1..d89a9ba17655 100644 --- a/tools/testing/selftests/drivers/net/hw/config +++ b/tools/testing/selftests/drivers/net/hw/config @@ -1,11 +1,28 @@ +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_DEVMEM=y CONFIG_NET_IPGRE=y CONFIG_NET_IPGRE_DEMUX=y +CONFIG_NET_IPIP=y +CONFIG_NETKIT=y +CONFIG_NET_SCH_INGRESS=y +CONFIG_SYNC_FILE=y CONFIG_UDMABUF=y +CONFIG_USER_NS=y CONFIG_VXLAN=y +CONFIG_XFRM_USER=y diff --git a/tools/testing/selftests/drivers/net/hw/csum.py b/tools/testing/selftests/drivers/net/hw/csum.py index 3e3a89a34afe..0e99198f8d39 100755 --- a/tools/testing/selftests/drivers/net/hw/csum.py +++ b/tools/testing/selftests/drivers/net/hw/csum.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0 -"""Run the tools/testing/selftests/net/csum testsuite.""" +"""Run the tools/testing/selftests/net/lib/csum testsuite.""" from os import path diff --git a/tools/testing/selftests/drivers/net/hw/devlink_rate_cross_esw.py b/tools/testing/selftests/drivers/net/hw/devlink_rate_cross_esw.py new file mode 100755 index 000000000000..4416f024cb76 --- /dev/null +++ b/tools/testing/selftests/drivers/net/hw/devlink_rate_cross_esw.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 + +""" +Devlink Rate Cross-eswitch Scheduling Test Suite +================================================== + +Control-plane tests for cross-eswitch TX scheduling via devlink-rate. +Validates that VFs from different PFs on the same chip can share +rate groups using the cross-device parent-dev attribute. + +Preconditions: +- NETIF points to a bond device with exactly two interfaces. +- the interfaces must be two PFs from different devices sharing the same chip. +- (for mlx5): the two interfaces are in switchdev mode and configured in a LAG: + - devlink dev eswitch set $DEV1 mode switchdev + - devlink dev eswitch set $DEV2 mode switchdev + - devlink dev param set $DEV1 name esw_multiport value 1 cmode runtime + - devlink dev param set $DEV2 name esw_multiport value 1 cmode runtime +- test cases will be skipped if: + - the number of interfaces in the bond device is != 2. + - the kernel doesn't support devlink rates. + - the devlink API doesn't support cross-device parents (ENODEV). + - cross-esw rate scheduling returns EOPNOTSUPP. +""" + +import errno +import glob +import os +import time + +from lib.py import ksft_pr, ksft_eq, ksft_run, ksft_exit +from lib.py import KsftSkipEx, KsftFailEx +from lib.py import NetDrvEnv, DevlinkFamily +from lib.py import NlError +from lib.py import cmd, defer, ip, tool + + +# --- Discovery and setup --- + + +def get_bond_slaves(bond_ifname): + """Returns sorted list of slave netdev names for a bond.""" + pattern = f"/sys/class/net/{bond_ifname}/lower_*" + lowers = glob.glob(pattern) + if not lowers: + raise KsftSkipEx(f"No bond slaves for {bond_ifname}") + slaves = [] + for path in sorted(lowers): + name = os.path.basename(path) + if name.startswith("lower_"): + name = name[len("lower_"):] + slaves.append(name) + return slaves + + +def discover_pfs(cfg): + """Discovers both PFs from bond slaves.""" + slaves = get_bond_slaves(cfg.ifname) + if len(slaves) != 2: + raise KsftSkipEx(f"Need 2 bond slaves, found {len(slaves)}") + + pf0, pf1 = slaves[0], slaves[1] + ksft_pr(f"PF0: {pf0} PF1: {pf1}") + return pf0, pf1 + + +def get_pci_addr(ifname): + """Resolves PCI address for a network interface.""" + return os.path.basename(os.path.realpath(f"/sys/class/net/{ifname}/device")) + + +def get_vf_port_index(pf_pci): + """Finds devlink port-index for vf0 under pf_pci.""" + ports = tool("devlink", "port show", json=True)["port"] + for port_name, props in ports.items(): + if port_name.startswith(f"pci/{pf_pci}/") and props.get("vfnum") == 0: + return int(port_name.split("/")[-1]) + raise KsftSkipEx(f"VF port not found for {pf_pci}") + + +def cleanup_esw(pf): + """Removes VFs if created by tests.""" + cmd(f"echo 0 > /sys/class/net/{pf}/device/sriov_numvfs", shell=True, fail=False) + + +def setup_esw(pf): + """Creates 1 VF on 'pf'.""" + path = f"/sys/class/net/{pf}/device/sriov_numvfs" + cmd(f"echo 0 > {path}", shell=True) + cmd(f"echo 1 > {path}", shell=True) + defer(cleanup_esw, pf) + time.sleep(2) + + vf_dir = f"/sys/class/net/{pf}/device/virtfn0/net" + entries = os.listdir(vf_dir) if os.path.isdir(vf_dir) else [] + if not entries: + raise KsftSkipEx(f"VF not found for {pf}") + ip(f"link set dev {entries[0]} up") + + pf_pci = get_pci_addr(pf) + vf_idx = get_vf_port_index(pf_pci) + ksft_pr(f"Created VF {vf_idx} on PF {pf} ({pf_pci})") + return pf_pci, vf_idx + + +# --- Rate operation helpers --- + + +def rate_new(devnl, dev_pci, node_name, **kwargs): + """Creates rate node.""" + params = { + "bus-name": "pci", + "dev-name": dev_pci, + "rate-node-name": node_name, + } + params.update(kwargs) + try: + devnl.rate_new(params) + except NlError as e: + if e.error == errno.EOPNOTSUPP: + raise KsftSkipEx("rate_new not supported") from e + raise KsftFailEx("rate_new failed") from e + + +def rate_get(devnl, dev_pci, node_name): + """Gets rate node.""" + params = { + "bus-name": "pci", + "dev-name": dev_pci, + "rate-node-name": node_name, + } + return devnl.rate_get(params) + + +def rate_get_leaf(devnl, dev_pci, port_index): + """Gets rate leaf (VF).""" + params = { + "bus-name": "pci", + "dev-name": dev_pci, + "port-index": port_index, + } + return devnl.rate_get(params) + + +def rate_del(devnl, dev_pci, node_name): + """Deletes rate node.""" + devnl.rate_del({ + "bus-name": "pci", + "dev-name": dev_pci, + "rate-node-name": node_name, + }) + + +def rate_set_leaf(devnl, dev_pci, port_index, **kwargs): + """Sets rate attributes on a leaf (VF).""" + params = { + "bus-name": "pci", + "dev-name": dev_pci, + "port-index": port_index, + } + params.update(kwargs) + try: + devnl.rate_set(params) + except NlError as e: + if e.error == errno.EOPNOTSUPP: + raise KsftSkipEx("rate_set not supported") from e + raise KsftFailEx("rate_set failed") from e + + +def rate_set_leaf_parent(devnl, dev_pci, port_index, + parent_name, parent_dev_pci=None): + """Sets a leaf's parent, optionally cross-esw.""" + params = { + "bus-name": "pci", + "dev-name": dev_pci, + "port-index": port_index, + "rate-parent-node-name": parent_name, + } + if parent_dev_pci: + params["parent-dev"] = { + "bus-name": "pci", + "dev-name": parent_dev_pci, + } + try: + devnl.rate_set(params) + except NlError as e: + if e.error == errno.EOPNOTSUPP: + raise KsftSkipEx("rate_set not supported") from e + if parent_dev_pci and e.error == errno.ENODEV: + raise KsftSkipEx("Cross-esw scheduling not supported") from e + raise KsftFailEx("rate_set failed") from e + + +def rate_clear_leaf_parent(devnl, dev_pci, port_index): + """Clears a leaf's parent.""" + rate_set_leaf_parent(devnl, dev_pci, port_index, "") + + +def rate_set_node(devnl, dev_pci, node_name, **kwargs): + """Sets rate attributes on a node.""" + params = { + "bus-name": "pci", + "dev-name": dev_pci, + "rate-node-name": node_name, + } + params.update(kwargs) + devnl.rate_set(params) + + +# --- Test cases --- + + +def test_same_esw_parent(cfg): + """Assigns PF0's VF to PF0's group (same esw baseline).""" + pf0, _ = discover_pfs(cfg) + pf0_pci, vf0_idx = setup_esw(pf0) + + rate_new(cfg.devnl, pf0_pci, "group0") + defer(rate_del, cfg.devnl, pf0_pci, "group0") + ksft_pr("rate-new succeeded") + + rate_set_leaf_parent(cfg.devnl, pf0_pci, vf0_idx, "group0") + defer(rate_clear_leaf_parent, cfg.devnl, pf0_pci, vf0_idx) + + ksft_pr("Same-esw parent assignment succeeded") + + +def test_cross_esw_parent(cfg): + """Sets cross-esw parent, then clear it.""" + pf0, pf1 = discover_pfs(cfg) + pf0_pci, _ = setup_esw(pf0) + pf1_pci, vf1_idx = setup_esw(pf1) + + rate_new(cfg.devnl, pf0_pci, "group1") + defer(rate_del, cfg.devnl, pf0_pci, "group1") + ksft_pr("rate-new succeeded") + + rate_set_leaf_parent(cfg.devnl, pf1_pci, vf1_idx, + "group1", parent_dev_pci=pf0_pci) + defer(rate_clear_leaf_parent, cfg.devnl, pf1_pci, vf1_idx) + + ksft_pr("Cross-esw parent set and clear succeeded") + + +def test_tx_rates_on_cross_esw(cfg): + """Sets tx_max on group and tx_share on leaves in a cross-esw setup.""" + pf0, pf1 = discover_pfs(cfg) + pf0_pci, vf0_idx = setup_esw(pf0) + pf1_pci, vf1_idx = setup_esw(pf1) + + rate_new(cfg.devnl, pf0_pci, "group2", **{"rate-tx-max": 10000000}) + defer(rate_del, cfg.devnl, pf0_pci, "group2") + ksft_pr("rate-new succeeded") + + rate_set_leaf_parent(cfg.devnl, pf1_pci, vf1_idx, + "group2", parent_dev_pci=pf0_pci) + defer(rate_clear_leaf_parent, cfg.devnl, pf1_pci, vf1_idx) + ksft_pr("set parent cross-esw succeeded") + + rate_set_leaf_parent(cfg.devnl, pf0_pci, vf0_idx, "group2") + defer(rate_clear_leaf_parent, cfg.devnl, pf0_pci, vf0_idx) + ksft_pr("set parent same esw succeeded") + + rate_set_leaf(cfg.devnl, pf0_pci, vf0_idx, **{"rate-tx-share": 1000000}) + rate = rate_get_leaf(cfg.devnl, pf0_pci, vf0_idx) + ksft_eq(rate["rate-tx-share"], 1000000) + rate_set_leaf(cfg.devnl, pf1_pci, vf1_idx, **{"rate-tx-share": 2000000}) + rate = rate_get_leaf(cfg.devnl, pf1_pci, vf1_idx) + ksft_eq(rate["rate-tx-share"], 2000000) + rate_set_node(cfg.devnl, pf0_pci, "group2", **{"rate-tx-max": 250000000}) + rate = rate_get(cfg.devnl, pf0_pci, "group2") + ksft_eq(rate["rate-tx-max"], 250000000) + + ksft_pr("tx_max and tx_share set on cross-esw group") + + +def main() -> None: + """Main function.""" + + with NetDrvEnv(__file__, nsim_test=False) as cfg: + cfg.devnl = DevlinkFamily() + + ksft_run( + cases=[ + test_same_esw_parent, + test_cross_esw_parent, + test_tx_rates_on_cross_esw, + ], + args=(cfg,), + ) + ksft_exit() + + +if __name__ == "__main__": + main() diff --git a/tools/testing/selftests/drivers/net/hw/devmem.py b/tools/testing/selftests/drivers/net/hw/devmem.py index ee863e90d1e0..82c11ffc4add 100755 --- a/tools/testing/selftests/drivers/net/hw/devmem.py +++ b/tools/testing/selftests/drivers/net/hw/devmem.py @@ -2,91 +2,47 @@ # SPDX-License-Identifier: GPL-2.0 from os import path -from lib.py import ksft_run, ksft_exit -from lib.py import ksft_eq, KsftSkipEx +from devmem_lib import (setup_test, run_rx, run_tx, run_tx_chunks, run_rx_hds, + run_rx_large_niov) +from lib.py import ksft_run, ksft_exit, ksft_disruptive from lib.py import NetDrvEpEnv -from lib.py import bkg, cmd, rand_port, wait_port_listen -from lib.py import ksft_disruptive - - -def require_devmem(cfg): - 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") @ksft_disruptive def check_rx(cfg) -> None: - require_devmem(cfg) - - port = rand_port() - socat = f"socat -u - TCP{cfg.addr_ipver}:{cfg.baddr}:{port},bind={cfg.remote_baddr}:{port}" - listen_cmd = f"{cfg.bin_local} -l -f {cfg.ifname} -s {cfg.addr} -p {port} -c {cfg.remote_addr} -v 7" - - with bkg(listen_cmd, exit_wait=True) as ncdevmem: - wait_port_listen(port) - cmd(f"yes $(echo -e \x01\x02\x03\x04\x05\x06) | \ - head -c 1K | {socat}", host=cfg.remote, shell=True) - - ksft_eq(ncdevmem.ret, 0) + """Run the devmem RX test.""" + run_rx(cfg) @ksft_disruptive def check_tx(cfg) -> None: - require_devmem(cfg) - - port = rand_port() - listen_cmd = f"socat -U - TCP{cfg.addr_ipver}-LISTEN:{port}" - - with bkg(listen_cmd, host=cfg.remote, exit_wait=True) as socat: - wait_port_listen(port, host=cfg.remote) - cmd(f"echo -e \"hello\\nworld\"| {cfg.bin_local} -f {cfg.ifname} -s {cfg.remote_addr} -p {port}", shell=True) - - ksft_eq(socat.stdout.strip(), "hello\nworld") + """Run the devmem TX test.""" + run_tx(cfg) @ksft_disruptive def check_tx_chunks(cfg) -> None: - require_devmem(cfg) - - port = rand_port() - listen_cmd = f"socat -U - TCP{cfg.addr_ipver}-LISTEN:{port}" - - with bkg(listen_cmd, host=cfg.remote, exit_wait=True) as socat: - wait_port_listen(port, host=cfg.remote) - cmd(f"echo -e \"hello\\nworld\"| {cfg.bin_local} -f {cfg.ifname} -s {cfg.remote_addr} -p {port} -z 3", shell=True) - - ksft_eq(socat.stdout.strip(), "hello\nworld") + """Run the devmem TX chunking test.""" + run_tx_chunks(cfg) def check_rx_hds(cfg) -> None: - """Test HDS splitting across payload sizes.""" - require_devmem(cfg) - - for size in [1, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192]: - port = rand_port() - listen_cmd = f"{cfg.bin_local} -L -l -f {cfg.ifname} -s {cfg.addr} -p {port}" + """Run the HDS test.""" + run_rx_hds(cfg) - with bkg(listen_cmd, exit_wait=True) as ncdevmem: - wait_port_listen(port) - cmd(f"dd if=/dev/zero bs={size} count=1 2>/dev/null | " + - f"socat -b {size} -u - TCP{cfg.addr_ipver}:{cfg.baddr}:{port},nodelay", - host=cfg.remote, shell=True) - ksft_eq(ncdevmem.ret, 0, f"HDS failed for payload size {size}") +def check_rx_large_niov(cfg) -> None: + """Run the devmem RX test with rx-page-size = 16 KiB.""" + run_rx_large_niov(cfg) def main() -> None: + """Run the devmem test cases.""" with NetDrvEpEnv(__file__) as cfg: - cfg.bin_local = path.abspath(path.dirname(__file__) + "/ncdevmem") - cfg.bin_remote = cfg.remote.deploy(cfg.bin_local) - - ksft_run([check_rx, check_tx, check_tx_chunks, check_rx_hds], - args=(cfg, )) + setup_test(cfg, path.abspath(path.dirname(__file__) + "/ncdevmem")) + ksft_run([check_rx, check_tx, check_tx_chunks, check_rx_hds, + check_rx_large_niov], + args=(cfg,)) ksft_exit() diff --git a/tools/testing/selftests/drivers/net/hw/devmem_lib.py b/tools/testing/selftests/drivers/net/hw/devmem_lib.py new file mode 100644 index 000000000000..3554954a6691 --- /dev/null +++ b/tools/testing/selftests/drivers/net/hw/devmem_lib.py @@ -0,0 +1,316 @@ +# SPDX-License-Identifier: GPL-2.0 +# pylint: disable=invalid-name,too-many-arguments +"""Shared helpers for devmem TCP selftests.""" + +import os +import re + +from lib.py import (bkg, cmd, defer, ethtool, rand_port, wait_port_listen, + ksft_eq, KsftSkipEx, NetNSEnter, EthtoolFamily, + NetdevFamily) + + +RX_PAGE_SIZE_DEFAULT = 0 +RX_PAGE_SIZE_16K = 16384 + +PROBE_RX_PAGE_SIZES = (RX_PAGE_SIZE_DEFAULT, RX_PAGE_SIZE_16K) + +NR_HUGEPAGES_FILE = "/proc/sys/vm/nr_hugepages" + + +def _is_aligned(value, alignment): + """Equivalent of the kernel IS_ALIGNED(value, alignment). + + alignment must be a power of two. + """ + return (value & (alignment - 1)) == 0 + + +def _restore_nr_hugepages(nr_hugepages): + with open(NR_HUGEPAGES_FILE, 'w', encoding='utf-8') as f: + f.write(str(nr_hugepages)) + + +def _reserve_hugepages(want=64): + """Raise nr_hugepages to @want and arrange for it to be restored.""" + with open(NR_HUGEPAGES_FILE, 'r+', encoding='utf-8') as f: + nr_hugepages = int(f.read().strip()) + if nr_hugepages >= want: + return + f.seek(0) + f.write(str(want)) + defer(_restore_nr_hugepages, nr_hugepages) + + +def _probe_devmem(cfg, rx_page_size): + """Return True if ncdevmem can bind cfg.ifname at @rx_page_size.""" + probe_command = f"{cfg.bin_local} -f {cfg.ifname}" + if rx_page_size != RX_PAGE_SIZE_DEFAULT: + probe_command += f" -b {rx_page_size}" + return cmd(probe_command, fail=False, shell=True).ret == 0 + + +def require_devmem(cfg, rx_page_size=RX_PAGE_SIZE_DEFAULT): + """Probe ncdevmem on cfg.ifname and SKIP the test if devmem isn't supported.""" + if rx_page_size not in PROBE_RX_PAGE_SIZES: + raise RuntimeError( + f"rx-page-size={rx_page_size} is missing from " + f"PROBE_RX_PAGE_SIZES, so it was never probed.") + + if not hasattr(cfg, "devmem_supported"): + _reserve_hugepages() + # Probe every size upfront: in nk tests a leased queue may land in + # ncdevmem's queue range and cause the probe to fail. + cfg.devmem_supported = {size: _probe_devmem(cfg, size) + for size in PROBE_RX_PAGE_SIZES} + + if not cfg.devmem_supported[RX_PAGE_SIZE_DEFAULT]: + raise KsftSkipEx("Test requires devmem support") + + if rx_page_size != RX_PAGE_SIZE_DEFAULT: + page_size = os.sysconf("SC_PAGE_SIZE") + if not _is_aligned(rx_page_size, page_size): + raise KsftSkipEx( + f"rx-page-size={rx_page_size} is invalid for this platform " + f"(must be a multiple of PAGE_SIZE={page_size})") + + if not cfg.devmem_supported[rx_page_size]: + raise KsftSkipEx( + f"Test requires devmem rx-page-size={rx_page_size} support") + + +def configure_nic(cfg): + """Channels, rings, RSS, queue lease for netkit devmem.""" + if not hasattr(cfg, "devmem_supported"): + raise RuntimeError( + "require_devmem() must be called before configure_nic(), which " + "may lease a queue away and make later probes fail.") + + 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) + + 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': 'unknown', + '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, + rx_page_size=RX_PAGE_SIZE_DEFAULT): + """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") + if rx_page_size != RX_PAGE_SIZE_DEFAULT: + extras.append(f"-b {rx_page_size}") + + 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_large_niov(cfg): + """Run the devmem RX test with a large niov (rx-page-size > PAGE_SIZE). + + Sweep payload sizes that straddle the niov boundary: below, equal to, + and above rx_page_size, to exercise sub-niov, exact-niov, and multi-niov + RX paths. + """ + require_devmem(cfg, rx_page_size=RX_PAGE_SIZE_16K) + _reserve_hugepages() + configure_nic(cfg) + netns = getattr(cfg, "netns", None) + + for size in [1024, 4096, 8192, 16384, 32768, 65536]: + port = rand_port() + socat = socat_send(cfg, port) + listen_cmd = ncdevmem_rx(cfg, port, + flow_steer=not netns, + rx_page_size=RX_PAGE_SIZE_16K) + data_pipe = (f"yes $(echo -e \x01\x02\x03\x04\x05\x06) | " + f"head -c {size} | {socat}") + 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, + f"large-niov failed for payload size {size}") + + +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}") diff --git a/tools/testing/selftests/drivers/net/hw/ethtool_rmon.sh b/tools/testing/selftests/drivers/net/hw/ethtool_rmon.sh index 8f60c1685ad4..a074834cbe59 100755 --- a/tools/testing/selftests/drivers/net/hw/ethtool_rmon.sh +++ b/tools/testing/selftests/drivers/net/hw/ethtool_rmon.sh @@ -1,17 +1,23 @@ #!/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 @@ -19,11 +25,14 @@ ensure_mtu() { local iface=$1; shift local len=$1; shift - local current=$(ip -j link show dev $iface | jq -r '.[0].mtu') local required=$((len - ETH_HLEN - ETH_FCS_LEN)) + local current - if [ $current -lt $required ]; then - ip link set dev $iface mtu $required || return 1 + 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 } @@ -46,23 +55,26 @@ bucket_test() len=$((len - ETH_FCS_LEN)) len=$((len > 0 ? len : 0)) - before=$(ethtool --json -S $iface --groups rmon | \ + 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 - $MZ $neigh -q -c $num_rx -p $len -a own -b bcast -d 10us - $MZ $iface -q -c $num_tx -p $len -a own -b bcast -d 10us + 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=$(ethtool --json -S $iface --groups rmon | \ + hw_stats_settle "$iface" + + 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) + expected=$([ "$set" = rx ] && echo "$num_rx" || echo "$num_tx") - # Allow some extra tolerance for other packets sent by the stack - [ $delta -ge $expected ] && [ $delta -le $((expected + 100)) ] + [ "$delta" -ge "$expected" ] && [ "$delta" -le "$UINT32_MAX" ] } rmon_histogram() @@ -73,43 +85,40 @@ rmon_histogram() local nbuckets=0 local step= - RET=0 - while read -r -a bucket; do - step="$set-pkts${bucket[0]}to${bucket[1]} on $iface" + step="$set-pkts${bucket[0]}to${bucket[1]}" - for if in $iface $neigh; do - if ! ensure_mtu $if ${bucket[0]}; then - log_test_xfail "$if does not support the required MTU for $step" + 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 - check_err 1 "$step failed" + if ! bucket_test "$iface" "$neigh" "$set" "$nbuckets" "${bucket[0]}"; then + ktap_test_fail "$TEST_NAME.$step" return 1 fi - log_test "$step" + ktap_test_pass "$TEST_NAME.$step" nbuckets=$((nbuckets + 1)) - done < <(ethtool --json -S $iface --groups rmon | \ + 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 - log_test_xfail "$iface does not support $set histogram counters" + 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_histogram $h2 $h1 rx + rmon_histogram "$h1" "$h2" rx } rmon_tx_histogram() { - rmon_histogram $h1 $h2 tx - rmon_histogram $h2 $h1 tx + rmon_histogram "$h1" "$h2" tx } setup_prepare() @@ -117,9 +126,9 @@ setup_prepare() h1=${NETIFS[p1]} h2=${NETIFS[p2]} - for iface in $h1 $h2; do - netif_mtu[$iface]=$(ip -j link show dev $iface | jq -r '.[0].mtu') - ip link set dev $iface up + for iface in "$h1" "$h2"; do + netif_mtu["$iface"]=$(run_on "$iface" \ + ip -j link show dev "$iface" | jq -r '.[0].mtu') done } @@ -127,19 +136,26 @@ cleanup() { pre_cleanup - for iface in $h2 $h1; do - ip link set dev $iface \ - mtu ${netif_mtu[$iface]} \ - down + # 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 -exit $EXIT_STATUS +ktap_finished diff --git a/tools/testing/selftests/drivers/net/hw/ethtool_std_stats.sh b/tools/testing/selftests/drivers/net/hw/ethtool_std_stats.sh new file mode 100755 index 000000000000..09f8128c51f3 --- /dev/null +++ b/tools/testing/selftests/drivers/net/hw/ethtool_std_stats.sh @@ -0,0 +1,208 @@ +#!/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 -d 10usec -c "$num_tx" $pkt_format + + # shellcheck disable=SC2086 # needs split options + run_on "$neigh" "$MZ" "$neigh" -q -d 10usec -c "$num_rx" $pkt_format + + hw_stats_settle "$int" + + 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 diff --git a/tools/testing/selftests/drivers/net/hw/gro_hw.py b/tools/testing/selftests/drivers/net/hw/gro_hw.py new file mode 100755 index 000000000000..70e76e3888bd --- /dev/null +++ b/tools/testing/selftests/drivers/net/hw/gro_hw.py @@ -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() diff --git a/tools/testing/selftests/drivers/net/hw/iou-zcrx.c b/tools/testing/selftests/drivers/net/hw/iou-zcrx.c index 240d13dbc54e..f6a8fc5fac24 100644 --- a/tools/testing/selftests/drivers/net/hw/iou-zcrx.c +++ b/tools/testing/selftests/drivers/net/hw/iou-zcrx.c @@ -351,9 +351,6 @@ static void run_server(void) if (ret < 0) error(1, 0, "bind()"); - if (listen(fd, 1024) < 0) - error(1, 0, "listen()"); - flags |= IORING_SETUP_COOP_TASKRUN; flags |= IORING_SETUP_SINGLE_ISSUER; flags |= IORING_SETUP_DEFER_TASKRUN; @@ -366,6 +363,9 @@ static void run_server(void) if (cfg_dry_run) return; + if (listen(fd, 1024) < 0) + error(1, 0, "listen()"); + add_accept(&ring, fd); tstop = gettimeofday_ms() + 5000; diff --git a/tools/testing/selftests/drivers/net/hw/iou-zcrx.py b/tools/testing/selftests/drivers/net/hw/iou-zcrx.py index c63d6d6450d2..b7a225fe4bea 100755 --- a/tools/testing/selftests/drivers/net/hw/iou-zcrx.py +++ b/tools/testing/selftests/drivers/net/hw/iou-zcrx.py @@ -2,14 +2,28 @@ # 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 +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) @@ -28,6 +42,23 @@ def set_flow_rule_rss(cfg, rss_ctx_id): 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'] @@ -46,6 +77,7 @@ def single(cfg): '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}") @@ -73,6 +105,7 @@ def rss(cfg): '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}") @@ -85,12 +118,22 @@ def rss(cfg): 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}" @@ -106,6 +149,7 @@ def test_zcrx(cfg, setup) -> None: ]) 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" @@ -119,37 +163,33 @@ def test_zcrx_large_chunks(cfg) -> None: """Test zcrx with large buffer chunks.""" cfg.require_ipver('6') - - combined_chans = _get_combined_channels(cfg) - if combined_chans < 2: - raise KsftSkipEx('at least 2 combined channels required') - (rx_ring, hds_thresh) = _get_current_settings(cfg) - port = rand_port() - - ethtool(f"-G {cfg.ifname} tcp-data-split on") - defer(ethtool, f"-G {cfg.ifname} tcp-data-split auto") - - ethtool(f"-G {cfg.ifname} hds-thresh 0") - defer(ethtool, f"-G {cfg.ifname} hds-thresh {hds_thresh}") - - ethtool(f"-G {cfg.ifname} rx 64") - defer(ethtool, f"-G {cfg.ifname} rx {rx_ring}") - - ethtool(f"-X {cfg.ifname} equal {combined_chans - 1}") - defer(ethtool, f"-X {cfg.ifname} default") - - flow_rule_id = _set_flow_rule(cfg, port, combined_chans - 1) - defer(ethtool, f"-N {cfg.ifname} delete {flow_rule_id}") - - rx_cmd = f"{cfg.bin_local} -s -p {port} -i {cfg.ifname} -q {combined_chans - 1} -x 2" - tx_cmd = f"{cfg.bin_remote} -c -h {cfg.addr_v['6']} -p {port} -l 12840" + _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) + raise KsftSkipEx(probe.stdout.strip()) + mp_clear_wait(cfg) with bkg(rx_cmd, exit_wait=True): - wait_port_listen(port, proto="tcp") + wait_port_listen(cfg.port, proto="tcp") + + check_iou_rx_buf_len(cfg, rx_buf_len) + cmd(tx_cmd, host=cfg.remote) @@ -159,8 +199,10 @@ def main() -> None: 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], args=(cfg, )) + ksft_run(globs=globals(), cases=[test_zcrx, test_zcrx_oneshot, + test_zcrx_large_chunks], args=(cfg, )) ksft_exit() diff --git a/tools/testing/selftests/drivers/net/hw/ipsec_vxlan.py b/tools/testing/selftests/drivers/net/hw/ipsec_vxlan.py new file mode 100755 index 000000000000..0740a4d85240 --- /dev/null +++ b/tools/testing/selftests/drivers/net/hw/ipsec_vxlan.py @@ -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() diff --git a/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py b/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py index d5d247eca6b7..8a58cb17cc06 100644 --- a/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py +++ b/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py @@ -3,6 +3,7 @@ """ 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. @@ -17,35 +18,38 @@ 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 + from net.lib.py import NetNS, NetNSEnter, NetdevSimDev, UserNetNS from net.lib.py import EthtoolFamily, NetdevFamily, NetshaperFamily, \ - NlError, RtnlFamily, DevlinkFamily, PSPFamily + 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, wait_port_listen, wait_file, tool + 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 + from drivers.net.lib.py import NetDrvEnv, NetDrvEpEnv, NetDrvContEnv - __all__ = ["NetNS", "NetNSEnter", "NetdevSimDev", + __all__ = ["NetNS", "NetNSEnter", "NetdevSimDev", "UserNetNS", "EthtoolFamily", "NetdevFamily", "NetshaperFamily", - "NlError", "RtnlFamily", "DevlinkFamily", "PSPFamily", + "NlError", "RtnlFamily", "DevlinkFamily", "PSPFamily", "Netlink", "CmdExitFailure", "bkg", "cmd", "bpftool", "bpftrace", "defer", "ethtool", - "fd_read_timeout", "ip", "rand_port", + "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", "GenerateTraffic", "Remote", - "Iperf3Runner"] + "NetDrvEnv", "NetDrvEpEnv", "NetDrvContEnv", "GenerateTraffic", + "Remote", "Iperf3Runner"] except ModuleNotFoundError as e: print("Failed importing `net` library from kernel sources") print(str(e)) diff --git a/tools/testing/selftests/drivers/net/hw/ncdevmem.c b/tools/testing/selftests/drivers/net/hw/ncdevmem.c index e098d6534c3c..918e3b51f3b8 100644 --- a/tools/testing/selftests/drivers/net/hw/ncdevmem.c +++ b/tools/testing/selftests/drivers/net/hw/ncdevmem.c @@ -40,6 +40,7 @@ #include <linux/uio.h> #include <stdarg.h> +#include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> @@ -61,6 +62,7 @@ #include <sys/time.h> #include <linux/memfd.h> +#include <sys/param.h> #include <linux/dma-buf.h> #include <linux/errqueue.h> #include <linux/udmabuf.h> @@ -79,6 +81,7 @@ #define PAGE_SHIFT 12 #define TEST_PREFIX "ncdevmem" #define NUM_PAGES 16000 +#define MB(x) ((x) << 20) #ifndef MSG_SOCK_DEVMEM #define MSG_SOCK_DEVMEM 0x2000000 @@ -93,12 +96,14 @@ static char *port; static size_t do_validation; static int start_queue = -1; static int num_queues = -1; +static int skip_config; static char *ifname; static unsigned int ifindex; static unsigned int dmabuf_id; static uint32_t tx_dmabuf_id; static int waittime_ms = 500; static bool fail_on_linear; +static uint32_t rx_page_size; /* System state loaded by current_config_load() */ #define MAX_FLOWS 8 @@ -141,6 +146,7 @@ static struct memory_buffer *udmabuf_alloc(size_t size) { struct udmabuf_create create; struct memory_buffer *ctx; + unsigned int memfd_flags; int ret; ctx = malloc(sizeof(*ctx)); @@ -149,15 +155,20 @@ static struct memory_buffer *udmabuf_alloc(size_t size) ctx->size = size; - ctx->devfd = open("/dev/udmabuf", O_RDWR); + ctx->devfd = open("/dev/udmabuf", O_RDONLY); if (ctx->devfd < 0) { pr_err("[skip,no-udmabuf: Unable to access DMA buffer device file]"); goto err_free_ctx; } - ctx->memfd = memfd_create("udmabuf-test", MFD_ALLOW_SEALING); + memfd_flags = MFD_ALLOW_SEALING; + if (rx_page_size > getpagesize()) + memfd_flags |= MFD_HUGETLB | MFD_HUGE_2MB; + + ctx->memfd = memfd_create("udmabuf-test", memfd_flags); if (ctx->memfd < 0) { - pr_err("[skip,no-memfd]"); + pr_err("[skip,no-memfd%s]", + (memfd_flags & MFD_HUGETLB) ? " (need hugepages)" : ""); goto err_close_dev; } @@ -167,6 +178,11 @@ static struct memory_buffer *udmabuf_alloc(size_t size) goto err_close_memfd; } + if (memfd_flags & MFD_HUGETLB) { + size = roundup(size, MB(2)); + ctx->size = size; + } + ret = ftruncate(ctx->memfd, size); if (ret == -1) { pr_err("[FAIL,memfd-truncate]"); @@ -698,6 +714,8 @@ static int bind_rx_queue(unsigned int ifindex, unsigned int dmabuf_fd, netdev_bind_rx_req_set_ifindex(req, ifindex); netdev_bind_rx_req_set_fd(req, dmabuf_fd); __netdev_bind_rx_req_set_queues(req, queues, n_queue_index); + if (rx_page_size) + netdev_bind_rx_req_set_rx_page_size(req, rx_page_size); rsp = netdev_bind_rx(*ys, req); if (!rsp) { @@ -828,7 +846,7 @@ static struct netdev_queue_id *create_queues(void) static int do_server(struct memory_buffer *mem) { - struct ethtool_rings_get_rsp *ring_config; + struct ethtool_rings_get_rsp *ring_config = NULL; char ctrl_data[sizeof(int) * 20000]; size_t non_page_aligned_frags = 0; struct sockaddr_in6 client_addr; @@ -851,27 +869,29 @@ static int do_server(struct memory_buffer *mem) return -1; } - ring_config = get_ring_config(); - if (!ring_config) { - pr_err("Failed to get current ring configuration"); - return -1; - } + if (!skip_config) { + ring_config = get_ring_config(); + if (!ring_config) { + pr_err("Failed to get current ring configuration"); + return -1; + } - if (configure_headersplit(ring_config, 1)) { - pr_err("Failed to enable TCP header split"); - goto err_free_ring_config; - } + if (configure_headersplit(ring_config, 1)) { + pr_err("Failed to enable TCP header split"); + goto err_free_ring_config; + } - /* Configure RSS to divert all traffic from our devmem queues */ - if (configure_rss()) { - pr_err("Failed to configure rss"); - goto err_reset_headersplit; - } + /* Configure RSS to divert all traffic from our devmem queues */ + if (configure_rss()) { + pr_err("Failed to configure rss"); + goto err_reset_headersplit; + } - /* Flow steer our devmem flows to start_queue */ - if (configure_flow_steering(&server_sin)) { - pr_err("Failed to configure flow steering"); - goto err_reset_rss; + /* Flow steer our devmem flows to start_queue */ + if (configure_flow_steering(&server_sin)) { + pr_err("Failed to configure flow steering"); + goto err_reset_rss; + } } if (bind_rx_queue(ifindex, mem->fd, create_queues(), num_queues, &ys)) { @@ -1052,13 +1072,17 @@ err_free_tmp: err_unbind: ynl_sock_destroy(ys); err_reset_flow_steering: - reset_flow_steering(); + if (!skip_config) + reset_flow_steering(); err_reset_rss: - reset_rss(); + if (!skip_config) + reset_rss(); err_reset_headersplit: - restore_ring_config(ring_config); + if (!skip_config) + restore_ring_config(ring_config); err_free_ring_config: - ethtool_rings_get_rsp_free(ring_config); + if (!skip_config) + ethtool_rings_get_rsp_free(ring_config); return err; } @@ -1404,7 +1428,7 @@ int main(int argc, char *argv[]) int is_server = 0, opt; int ret, err = 1; - while ((opt = getopt(argc, argv, "Lls:c:p:v:q:t:f:z:")) != -1) { + while ((opt = getopt(argc, argv, "Lls:c:p:v:q:t:f:z:nb:")) != -1) { switch (opt) { case 'L': fail_on_linear = true; @@ -1436,6 +1460,22 @@ int main(int argc, char *argv[]) case 'z': max_chunk = atoi(optarg); break; + case 'n': + skip_config = 1; + break; + case 'b': { + unsigned long val; + + errno = 0; + val = strtoul(optarg, NULL, 0); + if ((val == ULONG_MAX && errno == ERANGE) || + val > UINT32_MAX) { + pr_err("invalid rx_page_size: %s", optarg); + return 1; + } + rx_page_size = val; + break; + } case '?': fprintf(stderr, "unknown option: %c\n", optopt); break; diff --git a/tools/testing/selftests/drivers/net/hw/nk_devmem.py b/tools/testing/selftests/drivers/net/hw/nk_devmem.py new file mode 100755 index 000000000000..61c6f31f01e5 --- /dev/null +++ b/tools/testing/selftests/drivers/net/hw/nk_devmem.py @@ -0,0 +1,52 @@ +#!/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, + run_rx_large_niov) +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 check_nk_rx_large_niov(cfg) -> None: + """Run the devmem RX large-niov test through netkit.""" + run_rx_large_niov(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, check_nk_rx_large_niov], args=(cfg,)) + ksft_exit() + + +if __name__ == "__main__": + main() diff --git a/tools/testing/selftests/drivers/net/hw/nk_forward.bpf.c b/tools/testing/selftests/drivers/net/hw/nk_forward.bpf.c new file mode 100644 index 000000000000..86ebfc1445b6 --- /dev/null +++ b/tools/testing/selftests/drivers/net/hw/nk_forward.bpf.c @@ -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"; diff --git a/tools/testing/selftests/drivers/net/hw/nk_netns.py b/tools/testing/selftests/drivers/net/hw/nk_netns.py new file mode 100755 index 000000000000..8b7ab75aa27f --- /dev/null +++ b/tools/testing/selftests/drivers/net/hw/nk_netns.py @@ -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() diff --git a/tools/testing/selftests/drivers/net/hw/nk_primary_rx_redirect.bpf.c b/tools/testing/selftests/drivers/net/hw/nk_primary_rx_redirect.bpf.c new file mode 100644 index 000000000000..46ff494b23de --- /dev/null +++ b/tools/testing/selftests/drivers/net/hw/nk_primary_rx_redirect.bpf.c @@ -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"; diff --git a/tools/testing/selftests/drivers/net/hw/nk_qlease.py b/tools/testing/selftests/drivers/net/hw/nk_qlease.py new file mode 100755 index 000000000000..4f53034c9a50 --- /dev/null +++ b/tools/testing/selftests/drivers/net/hw/nk_qlease.py @@ -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() diff --git a/tools/testing/selftests/drivers/net/hw/ntuple.py b/tools/testing/selftests/drivers/net/hw/ntuple.py new file mode 100755 index 000000000000..ef4604bfa8ef --- /dev/null +++ b/tools/testing/selftests/drivers/net/hw/ntuple.py @@ -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() diff --git a/tools/testing/selftests/drivers/net/hw/rss_ctx.py b/tools/testing/selftests/drivers/net/hw/rss_ctx.py index b9b7527c2c6b..5b25fa89c629 100755 --- a/tools/testing/selftests/drivers/net/hw/rss_ctx.py +++ b/tools/testing/selftests/drivers/net/hw/rss_ctx.py @@ -5,14 +5,15 @@ import datetime import random import re import time +from lib.py import ksft_disruptive from lib.py import ksft_run, ksft_pr, ksft_exit from lib.py import ksft_eq, ksft_ne, ksft_ge, ksft_in, ksft_lt, ksft_true, ksft_raises from lib.py import NetDrvEpEnv -from lib.py import EthtoolFamily, NetdevFamily +from lib.py import EthtoolFamily, NetdevFamily, NlError from lib.py import KsftSkipEx, KsftFailEx -from lib.py import ksft_disruptive -from lib.py import rand_port -from lib.py import cmd, ethtool, ip, defer, GenerateTraffic, CmdExitFailure, wait_file +from lib.py import rand_port, rand_ports +from lib.py import cmd, ethtool, ip, defer, CmdExitFailure, wait_file +from lib.py import GenerateTraffic def _rss_key_str(key): @@ -56,9 +57,10 @@ def ethtool_create(cfg, act, opts): def require_ntuple(cfg): features = ethtool(f"-k {cfg.ifname}", json=True)[0] if not features["ntuple-filters"]["active"]: - # ntuple is more of a capability than a config knob, don't bother - # trying to enable it (until some driver actually needs it). - raise KsftSkipEx("Ntuple filters not enabled on the device: " + str(features["ntuple-filters"])) + 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 require_context_cnt(cfg, need_cnt): @@ -165,9 +167,17 @@ def test_rss_key_indir(cfg): ksft_eq(1, max(data['rss-indirection-table'])) # Check we only get traffic on the first 2 queues - cnts = _get_rx_cnts(cfg) - GenerateTraffic(cfg).wait_pkts_and_stop(20000) - cnts = _get_rx_cnts(cfg, prev=cnts) + + # Retry a few times in case the flows skew to a single queue. + attempts = 3 + for attempt in range(attempts): + cnts = _get_rx_cnts(cfg) + GenerateTraffic(cfg).wait_pkts_and_stop(20000) + cnts = _get_rx_cnts(cfg, prev=cnts) + if cnts[0] >= 5000 and cnts[1] >= 5000: + break + ksft_pr(f"Skewed queue distribution, attempt {attempt + 1}/{attempts}: " + str(cnts)) + # 2 queues, 20k packets, must be at least 5k per queue ksft_ge(cnts[0], 5000, "traffic on main context (1/2): " + str(cnts)) ksft_ge(cnts[1], 5000, "traffic on main context (2/2): " + str(cnts)) @@ -177,9 +187,18 @@ def test_rss_key_indir(cfg): # Restore, and check traffic gets spread again reset_indir.exec() - cnts = _get_rx_cnts(cfg) - GenerateTraffic(cfg).wait_pkts_and_stop(20000) - cnts = _get_rx_cnts(cfg, prev=cnts) + for attempt in range(attempts): + cnts = _get_rx_cnts(cfg) + GenerateTraffic(cfg).wait_pkts_and_stop(20000) + cnts = _get_rx_cnts(cfg, prev=cnts) + if qcnt > 4: + if sum(cnts[:2]) < sum(cnts[2:]): + break + else: + if cnts[2] >= 3500: + break + ksft_pr(f"Skewed queue distribution, attempt {attempt + 1}/{attempts}: " + str(cnts)) + if qcnt > 4: # First two queues get less traffic than all the rest ksft_lt(sum(cnts[:2]), sum(cnts[2:]), @@ -356,7 +375,7 @@ def test_hitless_key_update(cfg): tgen.wait_pkts_and_stop(5000) ksft_lt((t1 - t0).total_seconds(), 0.15) - ksft_eq(errors1 - errors1, 0) + ksft_eq(errors1 - errors0, 0) ksft_eq(carrier1 - carrier0, 0) @@ -454,7 +473,7 @@ def test_rss_context(cfg, ctx_cnt=1, create_with_cfg=None): except: raise KsftSkipEx("Not enough queues for the test") - ports = [] + ports = rand_ports(ctx_cnt) # Use queues 0 and 1 for normal traffic ethtool(f"-X {cfg.ifname} equal 2") @@ -488,7 +507,6 @@ def test_rss_context(cfg, ctx_cnt=1, create_with_cfg=None): ksft_eq(min(data['rss-indirection-table']), 2 + i * 2, "Unexpected context cfg: " + str(data)) ksft_eq(max(data['rss-indirection-table']), 2 + i * 2 + 1, "Unexpected context cfg: " + str(data)) - ports.append(rand_port()) flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {ports[i]} context {ctx_id}" ntuple = ethtool_create(cfg, "-N", flow) defer(ethtool, f"-N {cfg.ifname} delete {ntuple}") @@ -544,7 +562,7 @@ def test_rss_context_out_of_order(cfg, ctx_cnt=4): ntuple = [] ctx = [] - ports = [] + ports = rand_ports(ctx_cnt) def remove_ctx(idx): ntuple[idx].exec() @@ -576,7 +594,6 @@ def test_rss_context_out_of_order(cfg, ctx_cnt=4): ctx_id = ethtool_create(cfg, "-X", f"context new start {2 + i * 2} equal 2") ctx.append(defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")) - ports.append(rand_port()) flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {ports[i]} context {ctx_id}" ntuple_id = ethtool_create(cfg, "-N", flow) ntuple.append(defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")) @@ -634,9 +651,14 @@ def test_rss_context_overlap(cfg, other_ctx=0): ntuple = defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}") # Test the main context - cnts = _get_rx_cnts(cfg) - GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000) - cnts = _get_rx_cnts(cfg, prev=cnts) + attempts = 3 + for attempt in range(attempts): + cnts = _get_rx_cnts(cfg) + GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000) + cnts = _get_rx_cnts(cfg, prev=cnts) + if sum(cnts[:2]) >= 7000 and sum(cnts[2:4]) >= 7000: + break + ksft_pr(f"Skewed queue distribution, attempt {attempt + 1}/{attempts}: " + str(cnts)) ksft_ge(sum(cnts[ :4]), 20000, "traffic on main context: " + str(cnts)) ksft_ge(sum(cnts[ :2]), 7000, "traffic on main context (1/2): " + str(cnts)) @@ -790,9 +812,10 @@ def test_rss_default_context_rule(cfg): ethtool(f"-N {cfg.ifname} {flow_generic}") defer(ethtool, f"-N {cfg.ifname} delete 1") + ports = rand_ports(2) # Specific high-priority rule for a random port that should stay on context 0. # Assign loc 0 so it is evaluated before the generic rule. - port_main = rand_port() + port_main = ports[0] flow_main = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port_main} context 0 loc 0" ethtool(f"-N {cfg.ifname} {flow_main}") defer(ethtool, f"-N {cfg.ifname} delete 0") @@ -805,12 +828,100 @@ def test_rss_default_context_rule(cfg): 'empty' : (2, 3) }) # And that traffic for any other port is steered to the new context - port_other = rand_port() + port_other = ports[1] _send_traffic_check(cfg, port_other, f"context {ctx_id}", { 'target': (2, 3), 'noise' : (0, 1) }) +def _set_flow_hash(cfg, fl_type, fields, context=0): + req = {"header": {"dev-index": cfg.ifindex}, + "flow-hash": {fl_type: fields}} + if context: + req["context"] = context + cfg.ethnl.rss_set(req) + + +def _get_flow_hash(cfg, fl_type, context=0): + req = {"header": {"dev-index": cfg.ifindex}} + if context: + req["context"] = context + rss = cfg.ethnl.rss_get(req) + return rss.get("flow-hash", {}).get(fl_type, set()) + + +def test_rss_context_flow_hash(cfg): + """ + Validate, with traffic, that an additional RSS context honors the + flow-hash field selection. If the driver lacks per-context field + configuration ("ops->rxfh_per_ctx_fields") fall back to setting the + fields on the main context, which the kernel applies device-wide. + """ + + require_ntuple(cfg) + + queue_cnt = len(_get_rx_cnts(cfg)) + if queue_cnt < 6: + try: + ksft_pr(f"Increasing queue count {queue_cnt} -> 6") + ethtool(f"-L {cfg.ifname} combined 6") + defer(ethtool, f"-L {cfg.ifname} combined {queue_cnt}") + except CmdExitFailure as exc: + raise KsftSkipEx("Not enough queues for the test") from exc + + fl_type = f"tcp{cfg.addr_ipver}" + if not _get_flow_hash(cfg, fl_type): + raise KsftSkipEx(f"Device does not report flow-hash for {fl_type}") + + # Reserve queues 0/1 for main, build a new context spanning 2..5 + ethtool(f"-X {cfg.ifname} equal 2") + defer(ethtool, f"-X {cfg.ifname} default") + ctx_id = ethtool_create(cfg, "-X", "context new start 2 equal 4") + defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete") + + port = rand_port() + flow = f"flow-type {fl_type} dst-ip {cfg.addr} dst-port {port} context {ctx_id}" + ntuple = ethtool_create(cfg, "-N", flow) + defer(ethtool, f"-N {cfg.ifname} delete {ntuple}") + + ip_only = {"ip-src", "ip-dst"} + ip_l4 = ip_only | {"l4-b-0-1", "l4-b-2-3"} + + # Try per-context flow-hash; fall back to main context if unsupported. + cfg_ctx = ctx_id + try: + orig = _get_flow_hash(cfg, fl_type, context=ctx_id) + _set_flow_hash(cfg, fl_type, ip_only, context=ctx_id) + except NlError: + ksft_pr("Per-context flow-hash not supported, using device-wide") + cfg_ctx = 0 + orig = _get_flow_hash(cfg, fl_type) + _set_flow_hash(cfg, fl_type, ip_only) + defer(_set_flow_hash, cfg, fl_type, orig, context=cfg_ctx) + + def measure(): + cnts = _get_rx_cnts(cfg) + GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000) + cnts = _get_rx_cnts(cfg, prev=cnts) + ctx_cnts = cnts[2:6] + directed = sum(ctx_cnts) + used = sum(1 for c in ctx_cnts if c > directed / 200) + return cnts, directed, used + + # IP-only hash: iperf3 streams share src/dst IP, all should land on the + # same queue inside the context's range. + cnts, directed, used = measure() + ksft_ge(directed, 20000, f"traffic on context {ctx_id} (IP-only): {cnts}") + ksft_eq(used, 1, f"IP-only hash should use one queue in context {ctx_id}, got: {cnts}") + + # IP+L4 hash: streams have distinct src ports, traffic should spread. + _set_flow_hash(cfg, fl_type, ip_l4, context=cfg_ctx) + + cnts, directed, used = measure() + ksft_ge(directed, 20000, f"traffic on context {ctx_id} (IP+L4): {cnts}") + ksft_ge(used, 2, f"IP+L4 hash should spread across context {ctx_id} queues, got: {cnts}") + + @ksft_disruptive def test_rss_context_persist_ifupdown(cfg, pre_down=False): """ @@ -918,6 +1029,7 @@ def main() -> None: test_flow_add_context_missing, test_delete_rss_context_busy, test_rss_ntuple_addition, test_rss_default_context_rule, + test_rss_context_flow_hash, test_rss_context_persist_create_and_ifdown, test_rss_context_persist_ifdown_and_create], args=(cfg, )) diff --git a/tools/testing/selftests/drivers/net/hw/rss_drv.py b/tools/testing/selftests/drivers/net/hw/rss_drv.py index 2d1a33189076..bd59dace6e15 100755 --- a/tools/testing/selftests/drivers/net/hw/rss_drv.py +++ b/tools/testing/selftests/drivers/net/hw/rss_drv.py @@ -5,9 +5,9 @@ Driver-related behavior tests for RSS. """ -from lib.py import ksft_run, ksft_exit, ksft_ge -from lib.py import ksft_variants, KsftNamedVariant, KsftSkipEx -from lib.py import defer, ethtool +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 @@ -45,6 +45,18 @@ def _maybe_create_context(cfg, create_context): 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), @@ -76,11 +88,224 @@ def indir_size_4x(cfg, create_context): _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], args=(cfg, )) + ksft_run([indir_size_4x, resize_periodic, + resize_below_user_size_reject, + resize_nonperiodic_reject, + resize_nonperiodic_no_corruption], args=(cfg, )) ksft_exit() diff --git a/tools/testing/selftests/drivers/net/hw/toeplitz.py b/tools/testing/selftests/drivers/net/hw/toeplitz.py index cd7e080e6f84..571732198b93 100755 --- a/tools/testing/selftests/drivers/net/hw/toeplitz.py +++ b/tools/testing/selftests/drivers/net/hw/toeplitz.py @@ -21,6 +21,8 @@ from lib.py import ksft_variants, KsftNamedVariant, KsftSkipEx, KsftFailEx ETH_RSS_HASH_TOP = 1 # Must match RPS_MAX_CPUS in toeplitz.c RPS_MAX_CPUS = 16 +# Cap Rx queues so IRQ pinning leaves free CPUs in the RPS_MAX_CPUS range +QUEUE_CAP = 8 def _check_rps_and_rfs_not_configured(cfg): @@ -48,6 +50,25 @@ def _get_cpu_for_irq(irq): return int(data) +def _cap_queue_count(cfg): + ehdr = {"header": {"dev-index": cfg.ifindex}} + chans = cfg.ethnl.channels_get(ehdr) + + config = {} + restore = {} + for key in ("combined-count", "rx-count"): + cur = chans.get(key, 0) + if cur > QUEUE_CAP: + config[key] = QUEUE_CAP + restore[key] = cur + + if not config: + return + + cfg.ethnl.channels_set(ehdr | config) + defer(cfg.ethnl.channels_set, ehdr | restore) + + def _get_irq_cpus(cfg): """ Read the list of IRQs for the device Rx queues. @@ -177,6 +198,7 @@ def test(cfg, proto_flag, ipver, grp): ] if grp: + _cap_queue_count(cfg) _check_rps_and_rfs_not_configured(cfg) if grp == "rss": irq_cpus = ",".join([str(x) for x in _get_irq_cpus(cfg)]) diff --git a/tools/testing/selftests/drivers/net/hw/tso.py b/tools/testing/selftests/drivers/net/hw/tso.py index 0998e68ebaf0..67f6c9ca9a64 100755 --- a/tools/testing/selftests/drivers/net/hw/tso.py +++ b/tools/testing/selftests/drivers/net/hw/tso.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0 -"""Run the tools/testing/selftests/net/csum testsuite.""" +"""A simple test for TSO.""" import fcntl import socket @@ -36,8 +36,11 @@ def tcp_sock_get_retrans(sock): 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_cmd = f"socat -{ipver} -t 2 -u TCP-LISTEN:{port},reuseport /dev/null,ignoreeof" + 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) @@ -68,7 +71,7 @@ def run_one_stream(cfg, ipver, remote_v4, remote_v6, should_lso): # 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 / 4) + ksft_lt(tcp_sock_get_retrans(sock), total_lso_wire / 16) sock.close() if should_lso: @@ -184,28 +187,24 @@ def query_nic_features(cfg) -> None: cfg.wanted_features.add(f["name"]) cfg.hw_features = set() - hw_all_features_cmd = "" for f in features["hw"]["bits"]["bit"]: if f.get("value", False): - feature = f["name"] - cfg.hw_features.add(feature) - hw_all_features_cmd += f" {feature} on" - try: - ethtool(f"-K {cfg.ifname} {hw_all_features_cmd}") - except Exception as e: - ksft_pr(f"WARNING: failure enabling all hw features: {e}") - ksft_pr("partial gso feature detection may be impacted") + 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 = cfg.hw_features - no_partial + cfg.partial_features = seg_features - no_partial ethtool(f"-K {cfg.ifname} tx-gso-partial on") restore_wanted_features(cfg) @@ -236,6 +235,9 @@ def main() -> None: ("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 = [] diff --git a/tools/testing/selftests/drivers/net/hw/userns_devmem.py b/tools/testing/selftests/drivers/net/hw/userns_devmem.py new file mode 100755 index 000000000000..2aaf6ea81715 --- /dev/null +++ b/tools/testing/selftests/drivers/net/hw/userns_devmem.py @@ -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() diff --git a/tools/testing/selftests/drivers/net/hw/uso.py b/tools/testing/selftests/drivers/net/hw/uso.py new file mode 100755 index 000000000000..6d61e56cab3c --- /dev/null +++ b/tools/testing/selftests/drivers/net/hw/uso.py @@ -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() diff --git a/tools/testing/selftests/drivers/net/hw/xdp_metadata.py b/tools/testing/selftests/drivers/net/hw/xdp_metadata.py new file mode 100644 index 000000000000..33a1985356d9 --- /dev/null +++ b/tools/testing/selftests/drivers/net/hw/xdp_metadata.py @@ -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() diff --git a/tools/testing/selftests/drivers/net/lib/py/__init__.py b/tools/testing/selftests/drivers/net/lib/py/__init__.py index 8b75faa9af6d..ee903bcf3207 100644 --- a/tools/testing/selftests/drivers/net/lib/py/__init__.py +++ b/tools/testing/selftests/drivers/net/lib/py/__init__.py @@ -3,6 +3,7 @@ """ Driver test environment. 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. @@ -17,25 +18,28 @@ 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 + from net.lib.py import NetNS, NetNSEnter, NetdevSimDev, UserNetNS from net.lib.py import EthtoolFamily, NetdevFamily, NetshaperFamily, \ - NlError, RtnlFamily, DevlinkFamily, PSPFamily + 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, wait_port_listen, wait_file + fd_read_timeout, ip, rand_port, rand_ports, tc, wait_port_listen, \ + wait_file + 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 - __all__ = ["NetNS", "NetNSEnter", "NetdevSimDev", + __all__ = ["NetNS", "NetNSEnter", "NetdevSimDev", "UserNetNS", "EthtoolFamily", "NetdevFamily", "NetshaperFamily", - "NlError", "RtnlFamily", "DevlinkFamily", "PSPFamily", + "NlError", "RtnlFamily", "DevlinkFamily", "PSPFamily", "Netlink", "CmdExitFailure", "bkg", "cmd", "bpftool", "bpftrace", "defer", "ethtool", - "fd_read_timeout", "ip", "rand_port", + "fd_read_timeout", "ip", "rand_port", "rand_ports", "tc", "wait_port_listen", "wait_file", + "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", @@ -43,12 +47,12 @@ try: "ksft_ne", "ksft_not_in", "ksft_raises", "ksft_true", "ksft_gt", "ksft_not_none", "ksft_not_none"] - from .env import NetDrvEnv, NetDrvEpEnv + from .env import NetDrvEnv, NetDrvEpEnv, NetDrvContEnv from .load import GenerateTraffic, Iperf3Runner from .remote import Remote - __all__ += ["NetDrvEnv", "NetDrvEpEnv", "GenerateTraffic", "Remote", - "Iperf3Runner"] + __all__ += ["NetDrvEnv", "NetDrvEpEnv", "NetDrvContEnv", "GenerateTraffic", + "Remote", "Iperf3Runner"] except ModuleNotFoundError as e: print("Failed importing `net` library from kernel sources") print(str(e)) diff --git a/tools/testing/selftests/drivers/net/lib/py/env.py b/tools/testing/selftests/drivers/net/lib/py/env.py index 41cc248ac848..25903f580b40 100644 --- a/tools/testing/selftests/drivers/net/lib/py/env.py +++ b/tools/testing/selftests/drivers/net/lib/py/env.py @@ -1,13 +1,17 @@ # SPDX-License-Identifier: GPL-2.0 +import ipaddress import os +import sys import time +import json from pathlib import Path from lib.py import KsftSkipEx, KsftXfailEx -from lib.py import ksft_setup, wait_file +from lib.py import ksft_pr, ksft_setup, wait_file from lib.py import cmd, ethtool, ip, CmdExitFailure -from lib.py import NetNS, NetdevSimDev +from lib.py import NetNS, NetdevSimDev, UserNetNS from .remote import Remote +from . import bpftool, RtnlFamily, Netlink class NetDrvEnvBase: @@ -27,6 +31,7 @@ class NetDrvEnvBase: # Following attrs must be set be inheriting classes self.dev = None + self.ifname = None def _load_env_file(self): env = os.environ.copy() @@ -54,6 +59,22 @@ class NetDrvEnvBase: def __del__(self): pass + def _print_dev_info(self): + """ + Show whether the test ran on real hardware or netdevsim. + Useful to confirm when results are shared on the mailing list. + """ + driver = "unknown" + try: + info = ethtool(f"-i {self.ifname}").stdout + for line in info.splitlines(): + if line.startswith("driver:"): + driver = line.split(':', 1)[1].strip() or driver + break + except (CmdExitFailure, FileNotFoundError): + pass + ksft_pr(f"Interface: {self.ifname}, driver: {driver}") + def __enter__(self): ip(f"link set dev {self.dev['ifname']} up") wait_file(f"/sys/class/net/{self.dev['ifname']}/carrier", @@ -90,6 +111,7 @@ class NetDrvEnv(NetDrvEnvBase): self.dev = self._ns.nsims[0].dev self.ifname = self.dev['ifname'] self.ifindex = self.dev['ifindex'] + self._print_dev_info() def __del__(self): if self._ns: @@ -110,10 +132,11 @@ class NetDrvEpEnv(NetDrvEnvBase): nsim_v4_pfx = "192.0.2." nsim_v6_pfx = "2001:db8::" - def __init__(self, src_path, nsim_test=None): + def __init__(self, src_path, nsim_test=None, queue_count=None): super().__init__(src_path) self._stats_settle_time = None + self._queue_count = queue_count # Things we try to destroy self.remote = None @@ -155,16 +178,11 @@ class NetDrvEpEnv(NetDrvEnvBase): self.remote = Remote(kind, args, src_path) - self.addr_ipver = "6" if self.addr_v["6"] else "4" - self.addr = self.addr_v[self.addr_ipver] - self.remote_addr = self.remote_addr_v[self.addr_ipver] - - # Bracketed addresses, some commands need IPv6 to be inside [] - self.baddr = f"[{self.addr_v['6']}]" if self.addr_v["6"] else self.addr_v["4"] - self.remote_baddr = f"[{self.remote_addr_v['6']}]" if self.remote_addr_v["6"] else self.remote_addr_v["4"] + self.set_ipver("6" if self.addr_v["6"] else "4") self.ifname = self.dev['ifname'] self.ifindex = self.dev['ifindex'] + self._print_dev_info() # resolve remote interface name self.remote_ifname = self.resolve_remote_ifc() @@ -175,9 +193,13 @@ class NetDrvEpEnv(NetDrvEnvBase): self._required_cmd = {} def create_local(self): + nsim_kwargs = {} + if self._queue_count: + nsim_kwargs["queue_count"] = self._queue_count + self._netns = NetNS() - self._ns = NetdevSimDev() - self._ns_peer = NetdevSimDev(ns=self._netns) + self._ns = NetdevSimDev(**nsim_kwargs) + self._ns_peer = NetdevSimDev(ns=self._netns, **nsim_kwargs) with open("/proc/self/ns/net") as nsfd0, \ open("/var/run/netns/" + self._netns.name) as nsfd1: @@ -248,6 +270,25 @@ class NetDrvEpEnv(NetDrvEnvBase): if not self.addr_v[ipver] or not self.remote_addr_v[ipver]: raise KsftSkipEx(f"Test requires IPv{ipver} connectivity") + def set_ipver(self, ipver): + """ + Modify the IP version used by the generic address fields. + """ + if ipver == getattr(self, "addr_ipver", None): + return + + self.require_ipver(ipver) + + self.addr_ipver = ipver + self.addr = self.addr_v[ipver] + self.remote_addr = self.remote_addr_v[ipver] + + # Bracketed addresses, some commands need IPv6 to be inside [] + self.baddr = (f"[{self.addr_v['6']}]" if ipver == "6" + else self.addr_v["4"]) + self.remote_baddr = (f"[{self.remote_addr_v['6']}]" if ipver == "6" + else self.remote_addr_v["4"]) + def require_nsim(self, nsim_test=True): """Require or exclude netdevsim for this test""" if nsim_test and self._ns is None: @@ -255,6 +296,15 @@ class NetDrvEpEnv(NetDrvEnvBase): if nsim_test is False and self._ns is not None: raise KsftXfailEx("Test does not work on netdevsim") + def get_local_nsim_dev(self): + """Returns the local netdevsim device or None. + Using this method is discouraged, as it makes tests nsim-specific. + Standard interfaces available on all HW should ideally be used. + This method is intended for the few cases where nsim-specific + assertions need to be verified which cannot be verified otherwise. + """ + return self._ns + def _require_cmd(self, comm, key, host=None): cached = self._required_cmd.get(comm, {}) if cached.get(key) is None: @@ -285,7 +335,288 @@ class NetDrvEpEnv(NetDrvEnvBase): if "Operation not supported" not in e.cmd.stderr: raise - self._stats_settle_time = 0.025 + \ - data.get('stats-block-usecs', 0) / 1000 / 1000 + self._stats_settle_time = \ + 1.25 * data.get('stats-block-usecs', 20000) / 1000 / 1000 time.sleep(self._stats_settle_time) + + +class NetDrvContEnv(NetDrvEpEnv): + """ + Class for an environment with a netkit pair setup for forwarding traffic + between the physical interface and a network namespace. + NETIF = "eth0" + LOCAL_V6 = "2001:db8:1::1" + REMOTE_V6 = "2001:db8:1::2" + LOCAL_PREFIX_V6 = "2001:db8:2::0/64" + + +-----------------------------+ +------------------------------+ + dst | INIT NS | | TEST NS | + 2001: | +---------------+ | | | + db8:2::2| | NETIF | | bpf | | + +---|>| 2001:db8:1::1 | |redirect| +-------------------------+ | + | | | |-----------|--------|>| Netkit | | + | | +---------------+ | _peer | | nk_guest | | + | | +-------------+ Netkit pair | | | fe80::2/64 | | + | | | Netkit |.............|........|>| 2001:db8:2::2/64 | | + | | | nk_host | | | +-------------------------+ | + | | | fe80::1/64 | | | | + | | +-------------+ | | route: | + | | | | default | + | | route: | | via fe80::1 dev nk_guest | + | | 2001:db8:2::2/128 | +------------------------------+ + | | via fe80::2 dev nk_host | + | +-----------------------------+ + | + | +---------------+ + | | REMOTE | + +---| 2001:db8:1::2 | + +---------------+ + """ + + def __init__(self, src_path, rxqueues=1, primary_rx_redirect=False, + userns=False, **kwargs): + self.netns = None + self._userns = userns + self.nk_host_ifname = None + self.nk_guest_ifname = None + self._tc_clsact_added = False + self._tc_attached = False + self._primary_rx_redirect_attached = False + self._primary_rx_redirect_clsact_added = False + self._bpf_prog_pref = None + self._bpf_prog_id = None + self._init_ns_attached = False + self._remote_route_added = False + self._old_fwd = None + self._old_accept_ra = None + + super().__init__(src_path, **kwargs) + + self.require_ipver("6") + local_prefix = self.env.get("LOCAL_PREFIX_V6") + if not local_prefix: + raise KsftSkipEx("LOCAL_PREFIX_V6 required") + + net = ipaddress.IPv6Network(local_prefix, strict=False) + self.ipv6_prefix = str(net.network_address) + self.nk_host_ipv6 = f"{self.ipv6_prefix}2:1" + self.nk_guest_ipv6 = f"{self.ipv6_prefix}2:2" + + local_v6 = ipaddress.IPv6Address(self.addr_v["6"]) + if local_v6 in net: + raise KsftSkipEx("LOCAL_V6 must not fall within LOCAL_PREFIX_V6") + + 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) + netkit_links = [link for link in all_links + if link.get('linkinfo', {}).get('info_kind') == 'netkit' + and 'UP' not in link.get('flags', [])] + + if len(netkit_links) != 2: + raise KsftSkipEx("Failed to create netkit pair") + + netkit_links.sort(key=lambda x: x['ifindex']) + self.nk_host_ifname = netkit_links[1]['ifname'] + self.nk_guest_ifname = netkit_links[0]['ifname'] + self.nk_host_ifindex = netkit_links[1]['ifindex'] + self.nk_guest_ifindex = netkit_links[0]['ifindex'] + + self._setup_ns() + self.attach_bpf() + if primary_rx_redirect: + self._attach_primary_rx_redirect_bpf() + + def __del__(self): + if self._primary_rx_redirect_attached: + cmd(f"tc filter del dev {self.nk_host_ifname} ingress", fail=False) + self._primary_rx_redirect_attached = False + + if self._primary_rx_redirect_clsact_added: + cmd(f"tc qdisc del dev {self.nk_host_ifname} clsact", fail=False) + self._primary_rx_redirect_clsact_added = False + + if self._tc_attached: + cmd(f"tc filter del dev {self.ifname} ingress pref {self._bpf_prog_pref}") + self._tc_attached = False + + if self._tc_clsact_added: + cmd(f"tc qdisc del dev {self.ifname} clsact") + self._tc_clsact_added = False + + if self._remote_route_added: + cmd(f"ip -6 route del {self.nk_guest_ipv6}/128", + host=self.remote, fail=False) + self._remote_route_added = False + + if self.nk_host_ifname: + cmd(f"ip link del dev {self.nk_host_ifname}") + self.nk_host_ifname = None + self.nk_guest_ifname = None + + if self._init_ns_attached: + cmd("ip netns del init", fail=False) + self._init_ns_attached = False + + if self.netns: + del self.netns + self.netns = None + + if self._old_fwd is not None: + with open("/proc/sys/net/ipv6/conf/all/forwarding", "w", + encoding="utf-8") as f: + f.write(self._old_fwd) + self._old_fwd = None + if self._old_accept_ra is not None: + with open("/proc/sys/net/ipv6/conf/all/accept_ra", "w", + encoding="utf-8") as f: + f.write(self._old_accept_ra) + self._old_accept_ra = None + + super().__del__() + + def _setup_ns(self): + fwd_path = "/proc/sys/net/ipv6/conf/all/forwarding" + ra_path = "/proc/sys/net/ipv6/conf/all/accept_ra" + with open(fwd_path, encoding="utf-8") as f: + self._old_fwd = f.read().strip() + with open(ra_path, encoding="utf-8") as f: + self._old_accept_ra = f.read().strip() + with open(fwd_path, "w", encoding="utf-8") as f: + f.write("1") + with open(ra_path, "w", encoding="utf-8") as f: + f.write("2") + + self.netns = UserNetNS() if self._userns else NetNS() + cmd("ip netns attach init 1") + self._init_ns_attached = True + ip("netns set init 0", ns=self.netns) + ip(f"link set dev {self.nk_guest_ifname} netns {self.netns.name}") + nk_guest_dev = ip(f"link show dev {self.nk_guest_ifname}", + json=True, ns=self.netns)[0] + self.nk_guest_ifindex = nk_guest_dev['ifindex'] + ip(f"link set dev {self.nk_host_ifname} up") + ip(f"-6 addr add fe80::1/64 dev {self.nk_host_ifname} nodad") + ip(f"-6 route add {self.nk_guest_ipv6}/128 via fe80::2 dev {self.nk_host_ifname}") + + ip("link set lo up", ns=self.netns) + ip(f"link set dev {self.nk_guest_ifname} up", ns=self.netns) + ip(f"-6 addr add fe80::2/64 dev {self.nk_guest_ifname}", ns=self.netns) + ip(f"-6 addr add {self.nk_guest_ipv6}/64 dev {self.nk_guest_ifname} nodad", ns=self.netns) + ip(f"-6 route add default via fe80::1 dev {self.nk_guest_ifname}", ns=self.netns) + + def _tc_ensure_clsact(self, ifname=None): + """Ensure a clsact qdisc exists on @ifname. + + Returns True if this call added the qdisc, otherwise returns False. + """ + if ifname is None: + ifname = self.ifname + qdisc = json.loads(cmd(f"tc -j qdisc show dev {ifname}").stdout) + for q in qdisc: + if q['kind'] == 'clsact': + return False + cmd(f"tc qdisc add dev {ifname} clsact") + return True + + def _get_bpf_prog_ids(self): + filters = json.loads(cmd(f"tc -j filter show dev {self.ifname} ingress").stdout) + for bpf in filters: + if 'options' not in bpf: + continue + if bpf['options']['bpf_name'].startswith('nk_forward.bpf'): + return (bpf['pref'], bpf['options']['prog']['id']) + raise Exception("Failed to get BPF prog ID") + + def _find_bss_map_id(self, prog_id): + """Find the .bss map ID for a loaded BPF program.""" + prog_info = bpftool(f"prog show id {prog_id}", json=True) + for map_id in prog_info.get("map_ids", []): + map_info = bpftool(f"map show id {map_id}", json=True) + if map_info.get("name", "").endswith("bss"): + return map_id + raise Exception(f"Failed to find .bss map for prog {prog_id}") + + def _find_bpf_obj(self, name): + bpf_obj = self.test_dir / name + if bpf_obj.exists(): + return bpf_obj + bpf_obj = self.test_dir / "hw" / name + if bpf_obj.exists(): + return bpf_obj + return None + + def detach_bpf(self): + if self._tc_attached: + cmd(f"tc filter del dev {self.ifname} ingress pref " + f"{self._bpf_prog_pref}", fail=False) + self._tc_attached = False + + def attach_bpf(self): + bpf_obj = self._find_bpf_obj("nk_forward.bpf.o") + if not bpf_obj: + raise KsftSkipEx("BPF prog nk_forward.bpf.o not found") + + if self._tc_ensure_clsact(): + self._tc_clsact_added = True + cmd(f"tc filter add dev {self.ifname} ingress bpf obj {bpf_obj}" + " sec tc/ingress direct-action") + self._tc_attached = True + + (self._bpf_prog_pref, self._bpf_prog_id) = self._get_bpf_prog_ids() + bss_map_id = self._find_bss_map_id(self._bpf_prog_id) + + ipv6_addr = ipaddress.IPv6Address(self.ipv6_prefix) + ipv6_bytes = ipv6_addr.packed + ifindex_bytes = self.nk_host_ifindex.to_bytes(4, byteorder='little') + value = ipv6_bytes + ifindex_bytes + value_hex = ' '.join(f'{b:02x}' for b in value) + bpftool(f"map update id {bss_map_id} key hex 00 00 00 00 value hex {value_hex}") + + def _attach_primary_rx_redirect_bpf(self): + """Attach BPF redirect program on the primary netkit ingress.""" + bpf_obj = self._find_bpf_obj("nk_primary_rx_redirect.bpf.o") + if not bpf_obj: + raise KsftSkipEx("nk_primary_rx_redirect.bpf.o not found") + + if self._tc_ensure_clsact(self.nk_host_ifname): + self._primary_rx_redirect_clsact_added = True + cmd(f"tc filter add dev {self.nk_host_ifname} ingress" + f" bpf obj {bpf_obj} sec tc/ingress direct-action") + self._primary_rx_redirect_attached = True + + ip(f"-6 route add {self.nk_guest_ipv6}/128 via {self.addr_v['6']}", + host=self.remote) + self._remote_route_added = True + + filters = json.loads( + cmd(f"tc -j filter show dev {self.nk_host_ifname} ingress").stdout) + redirect_prog_id = None + for bpf in filters: + if 'options' not in bpf: + continue + if bpf['options']['bpf_name'].startswith('nk_primary_rx_redirect'): + redirect_prog_id = bpf['options']['prog']['id'] + break + if redirect_prog_id is None: + raise Exception("Failed to get primary RX redirect BPF prog ID") + + bss_map_id = self._find_bss_map_id(redirect_prog_id) + phys_ifindex_bytes = self.ifindex.to_bytes(4, byteorder=sys.byteorder) + value_hex = ' '.join(f'{b:02x}' for b in phys_ifindex_bytes) + bpftool(f"map update id {bss_map_id} key hex 00 00 00 00 value hex {value_hex}") diff --git a/tools/testing/selftests/drivers/net/lib/py/load.py b/tools/testing/selftests/drivers/net/lib/py/load.py index f181fa2d38fc..e24660e5c27f 100644 --- a/tools/testing/selftests/drivers/net/lib/py/load.py +++ b/tools/testing/selftests/drivers/net/lib/py/load.py @@ -48,7 +48,10 @@ class Iperf3Runner: Starts the iperf3 client with the configured options. """ cmdline = self._build_client(streams, duration, reverse) - return cmd(cmdline, background=background, host=self.env.remote) + kwargs = {"background": background, "host": self.env.remote} + if not background: + kwargs["timeout"] = duration + 5 + return cmd(cmdline, **kwargs) def measure_bandwidth(self, reverse=False): """ diff --git a/tools/testing/selftests/drivers/net/lib/sh/lib_netcons.sh b/tools/testing/selftests/drivers/net/lib/sh/lib_netcons.sh index 02dcdeb723be..a9a01a64b7b3 100644 --- a/tools/testing/selftests/drivers/net/lib/sh/lib_netcons.sh +++ b/tools/testing/selftests/drivers/net/lib/sh/lib_netcons.sh @@ -251,7 +251,7 @@ function listen_port_and_save_to() { # Just wait for 3 seconds timeout 3 ip netns exec "${NAMESPACE}" \ - socat "${SOCAT_MODE}":"${PORT}",fork "${OUTPUT}" 2> /dev/null + socat "${SOCAT_MODE}":"${PORT}",fork,shut-none "${OUTPUT}" 2> /dev/null } # Only validate that the message arrived properly @@ -360,8 +360,8 @@ function check_for_taskset() { # This is necessary if running multiple tests in a row function pkill_socat() { - PROCESS_NAME4="socat UDP-LISTEN:6666,fork ${OUTPUT_FILE}" - PROCESS_NAME6="socat UDP6-LISTEN:6666,fork ${OUTPUT_FILE}" + PROCESS_NAME4="socat UDP-LISTEN:6666,fork,shut-none ${OUTPUT_FILE}" + PROCESS_NAME6="socat UDP6-LISTEN:6666,fork,shut-none ${OUTPUT_FILE}" # socat runs under timeout(1), kill it if it is still alive # do not fail if socat doesn't exist anymore set +e diff --git a/tools/testing/selftests/drivers/net/macsec.py b/tools/testing/selftests/drivers/net/macsec.py new file mode 100755 index 000000000000..9a83d9542e04 --- /dev/null +++ b/tools/testing/selftests/drivers/net/macsec.py @@ -0,0 +1,343 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 + +"""MACsec tests.""" + +import os + +from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_raises +from lib.py import ksft_variants, KsftNamedVariant +from lib.py import CmdExitFailure, KsftSkipEx +from lib.py import NetDrvEpEnv +from lib.py import cmd, ip, defer, ethtool + +MACSEC_KEY = "12345678901234567890123456789012" +MACSEC_VLAN_VID = 10 + +# Unique prefix per run to avoid collisions in the shared netns. +# Keep it short: IFNAMSIZ is 16 (incl. NUL), and VLAN names append ".<vid>". +MACSEC_PFX = f"ms{os.getpid()}_" + + +def _macsec_name(idx=0): + return f"{MACSEC_PFX}{idx}" + + +def _get_macsec_offload(dev): + """Returns macsec offload mode string from ip -d link show.""" + info = ip(f"-d link show dev {dev}", json=True)[0] + return info.get("linkinfo", {}).get("info_data", {}).get("offload") + + +def _get_features(dev): + """Returns ethtool features dict for a device.""" + return ethtool(f"-k {dev}", json=True)[0] + + +def _require_ip_macsec(cfg): + """SKIP if iproute2 on local or remote lacks 'ip macsec' support.""" + for host in [None, cfg.remote]: + out = cmd("ip macsec help", fail=False, host=host) + if "Usage" not in out.stdout + out.stderr: + where = "remote" if host else "local" + raise KsftSkipEx(f"iproute2 too old on {where}," + " missing macsec support") + + +def _require_ip_macsec_offload(): + """SKIP if local iproute2 doesn't understand 'ip macsec offload'.""" + out = cmd("ip macsec help", fail=False) + if "offload" not in out.stdout + out.stderr: + raise KsftSkipEx("iproute2 too old, missing macsec offload") + + +def _require_macsec_offload(cfg): + """SKIP if local device doesn't support macsec-hw-offload.""" + _require_ip_macsec_offload() + 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("macsec-hw-offload", {}).get("active"): + raise KsftSkipEx("macsec-hw-offload not supported") + + +def _get_mac(ifname, host=None): + """Gets MAC address of an interface.""" + dev = ip(f"link show dev {ifname}", json=True, host=host) + return dev[0]["address"] + + +def _setup_macsec_sa(cfg, name): + """Adds matching TX/RX SAs on both ends.""" + local_mac = _get_mac(name) + remote_mac = _get_mac(name, host=cfg.remote) + + ip(f"macsec add {name} tx sa 0 pn 1 on key 01 {MACSEC_KEY}") + ip(f"macsec add {name} rx port 1 address {remote_mac}") + ip(f"macsec add {name} rx port 1 address {remote_mac} " + f"sa 0 pn 1 on key 02 {MACSEC_KEY}") + + ip(f"macsec add {name} tx sa 0 pn 1 on key 02 {MACSEC_KEY}", + host=cfg.remote) + ip(f"macsec add {name} rx port 1 address {local_mac}", host=cfg.remote) + ip(f"macsec add {name} rx port 1 address {local_mac} " + f"sa 0 pn 1 on key 01 {MACSEC_KEY}", host=cfg.remote) + + +def _setup_macsec_devs(cfg, name, offload): + """Creates macsec devices on both ends. + + Only the local device gets HW offload; the remote always uses software + MACsec since it may not support offload at all. + """ + offload_arg = "mac" if offload else "off" + + ip(f"link add link {cfg.ifname} {name} " + f"type macsec encrypt on offload {offload_arg}") + defer(ip, f"link del {name}") + ip(f"link add link {cfg.remote_ifname} {name} " + f"type macsec encrypt on", host=cfg.remote) + defer(ip, f"link del {name}", host=cfg.remote) + + +def _set_offload(name, offload): + """Sets offload on the local macsec device only.""" + offload_arg = "mac" if offload else "off" + + ip(f"link set {name} type macsec encrypt on offload {offload_arg}") + + +def _setup_vlans(cfg, name, vid): + """Adds VLANs on top of existing macsec devs.""" + vlan_name = f"{name}.{vid}" + + ip(f"link add link {name} {vlan_name} type vlan id {vid}") + defer(ip, f"link del {vlan_name}") + ip(f"link add link {name} {vlan_name} type vlan id {vid}", host=cfg.remote) + defer(ip, f"link del {vlan_name}", host=cfg.remote) + + +def _setup_vlan_ips(cfg, name, vid): + """Adds VLANs and IPs and brings up the macsec + VLAN devices.""" + local_ip = "198.51.100.1" + remote_ip = "198.51.100.2" + vlan_name = f"{name}.{vid}" + + ip(f"addr add {local_ip}/24 dev {vlan_name}") + ip(f"addr add {remote_ip}/24 dev {vlan_name}", host=cfg.remote) + ip(f"link set {name} up") + ip(f"link set {name} up", host=cfg.remote) + ip(f"link set {vlan_name} up") + ip(f"link set {vlan_name} up", host=cfg.remote) + + return vlan_name, remote_ip + + +def test_offload_api(cfg) -> None: + """MACsec offload API: create SecY, add SA/rx, toggle offload.""" + + _require_macsec_offload(cfg) + ms0 = _macsec_name(0) + ms1 = _macsec_name(1) + ms2 = _macsec_name(2) + + # Create 3 SecY with offload + ip(f"link add link {cfg.ifname} {ms0} type macsec " + f"port 4 encrypt on offload mac") + defer(ip, f"link del {ms0}") + + ip(f"link add link {cfg.ifname} {ms1} type macsec " + f"address aa:bb:cc:dd:ee:ff port 5 encrypt on offload mac") + defer(ip, f"link del {ms1}") + + ip(f"link add link {cfg.ifname} {ms2} type macsec " + f"sci abbacdde01020304 encrypt on offload mac") + defer(ip, f"link del {ms2}") + + # Add TX SA + ip(f"macsec add {ms0} tx sa 0 pn 1024 on " + "key 01 12345678901234567890123456789012") + + # Add RX SC + SA + ip(f"macsec add {ms0} rx port 1234 address 1c:ed:de:ad:be:ef") + ip(f"macsec add {ms0} rx port 1234 address 1c:ed:de:ad:be:ef " + "sa 0 pn 1 on key 00 0123456789abcdef0123456789abcdef") + + # Can't disable offload when SAs are configured + with ksft_raises(CmdExitFailure): + ip(f"link set {ms0} type macsec offload off") + with ksft_raises(CmdExitFailure): + ip(f"macsec offload {ms0} off") + + # Toggle offload via rtnetlink on SA-free device + ip(f"link set {ms2} type macsec offload off") + ip(f"link set {ms2} type macsec encrypt on offload mac") + + # Toggle offload via genetlink + ip(f"macsec offload {ms2} off") + ip(f"macsec offload {ms2} mac") + + +def test_max_secy(cfg) -> None: + """nsim-only test for max number of SecYs.""" + + cfg.require_nsim() + _require_ip_macsec_offload() + ms0 = _macsec_name(0) + ms1 = _macsec_name(1) + ms2 = _macsec_name(2) + ms3 = _macsec_name(3) + + ip(f"link add link {cfg.ifname} {ms0} type macsec " + f"port 4 encrypt on offload mac") + defer(ip, f"link del {ms0}") + + ip(f"link add link {cfg.ifname} {ms1} type macsec " + f"address aa:bb:cc:dd:ee:ff port 5 encrypt on offload mac") + defer(ip, f"link del {ms1}") + + ip(f"link add link {cfg.ifname} {ms2} type macsec " + f"sci abbacdde01020304 encrypt on offload mac") + defer(ip, f"link del {ms2}") + with ksft_raises(CmdExitFailure): + ip(f"link add link {cfg.ifname} {ms3} " + f"type macsec port 8 encrypt on offload mac") + + +def test_max_sc(cfg) -> None: + """nsim-only test for max number of SCs.""" + + cfg.require_nsim() + _require_ip_macsec_offload() + ms0 = _macsec_name(0) + + ip(f"link add link {cfg.ifname} {ms0} type macsec " + f"port 4 encrypt on offload mac") + defer(ip, f"link del {ms0}") + ip(f"macsec add {ms0} rx port 1234 address 1c:ed:de:ad:be:ef") + with ksft_raises(CmdExitFailure): + ip(f"macsec add {ms0} rx port 1235 address 1c:ed:de:ad:be:ef") + + +def test_offload_state(cfg) -> None: + """Offload state reflects configuration changes.""" + + _require_macsec_offload(cfg) + ms0 = _macsec_name(0) + + # Create with offload on + ip(f"link add link {cfg.ifname} {ms0} type macsec " + f"encrypt on offload mac") + cleanup = defer(ip, f"link del {ms0}") + + ksft_eq(_get_macsec_offload(ms0), "mac", + "created with offload: should be mac") + feats_on_1 = _get_features(ms0) + + ip(f"link set {ms0} type macsec offload off") + ksft_eq(_get_macsec_offload(ms0), "off", + "offload disabled: should be off") + feats_off_1 = _get_features(ms0) + + ip(f"link set {ms0} type macsec encrypt on offload mac") + ksft_eq(_get_macsec_offload(ms0), "mac", + "offload re-enabled: should be mac") + ksft_eq(_get_features(ms0), feats_on_1, + "features should match first offload-on snapshot") + + # Delete and recreate without offload + cleanup.exec() + ip(f"link add link {cfg.ifname} {ms0} type macsec") + defer(ip, f"link del {ms0}") + ksft_eq(_get_macsec_offload(ms0), "off", + "created without offload: should be off") + ksft_eq(_get_features(ms0), feats_off_1, + "features should match first offload-off snapshot") + + ip(f"link set {ms0} type macsec encrypt on offload mac") + ksft_eq(_get_macsec_offload(ms0), "mac", + "offload enabled after create: should be mac") + ksft_eq(_get_features(ms0), feats_on_1, + "features should match first offload-on snapshot") + + +def _check_nsim_vid(cfg, vid, expected) -> None: + """Checks if a VLAN is present. Only works on netdevsim.""" + + nsim = cfg.get_local_nsim_dev() + if not nsim: + return + + vlan_path = os.path.join(nsim.nsims[0].dfs_dir, "vlan") + with open(vlan_path, encoding="utf-8") as f: + vids = f.read() + found = f"ctag {vid}\n" in vids + ksft_eq(found, expected, + f"VLAN {vid} {'expected' if expected else 'not expected'}" + f" in debugfs") + + +@ksft_variants([ + KsftNamedVariant("offloaded", True), + KsftNamedVariant("software", False), +]) +def test_vlan(cfg, offload) -> None: + """Ping through VLAN-over-macsec.""" + + _require_ip_macsec(cfg) + if offload: + _require_macsec_offload(cfg) + else: + _require_ip_macsec_offload() + name = _macsec_name() + _setup_macsec_devs(cfg, name, offload=offload) + _setup_macsec_sa(cfg, name) + _setup_vlans(cfg, name, MACSEC_VLAN_VID) + vlan_name, remote_ip = _setup_vlan_ips(cfg, name, MACSEC_VLAN_VID) + _check_nsim_vid(cfg, MACSEC_VLAN_VID, offload) + # nsim doesn't handle the data path for offloaded macsec, so skip + # the ping when offloaded on nsim. + if not offload or not cfg.get_local_nsim_dev(): + cmd(f"ping -I {vlan_name} -c 1 -W 5 {remote_ip}") + + +@ksft_variants([ + KsftNamedVariant("on_to_off", True), + KsftNamedVariant("off_to_on", False), +]) +def test_vlan_toggle(cfg, offload) -> None: + """Toggle offload: VLAN filters propagate/remove correctly.""" + + _require_ip_macsec(cfg) + _require_macsec_offload(cfg) + name = _macsec_name() + _setup_macsec_devs(cfg, name, offload=offload) + _setup_vlans(cfg, name, MACSEC_VLAN_VID) + _check_nsim_vid(cfg, MACSEC_VLAN_VID, offload) + _set_offload(name, offload=not offload) + _check_nsim_vid(cfg, MACSEC_VLAN_VID, not offload) + vlan_name, remote_ip = _setup_vlan_ips(cfg, name, MACSEC_VLAN_VID) + _setup_macsec_sa(cfg, name) + # nsim doesn't handle the data path for offloaded macsec, so skip + # the ping when the final state is offloaded on nsim. + if offload or not cfg.get_local_nsim_dev(): + cmd(f"ping -I {vlan_name} -c 1 -W 5 {remote_ip}") + + +def main() -> None: + """Main program.""" + with NetDrvEpEnv(__file__) as cfg: + ksft_run([test_offload_api, + test_max_secy, + test_max_sc, + test_offload_state, + test_vlan, + test_vlan_toggle, + ], args=(cfg,)) + ksft_exit() + + +if __name__ == "__main__": + main() diff --git a/tools/testing/selftests/drivers/net/netconsole/Makefile b/tools/testing/selftests/drivers/net/netconsole/Makefile index b56c70b7e274..f0674c0017fc 100644 --- a/tools/testing/selftests/drivers/net/netconsole/Makefile +++ b/tools/testing/selftests/drivers/net/netconsole/Makefile @@ -13,6 +13,7 @@ TEST_PROGS := \ netcons_resume.sh \ netcons_sysdata.sh \ netcons_torture.sh \ + netcons_userdata.sh \ # end of TEST_PROGS include ../../../lib.mk diff --git a/tools/testing/selftests/drivers/net/netconsole/netcons_basic.sh b/tools/testing/selftests/drivers/net/netconsole/netcons_basic.sh index 59cf10013ecd..7976206523b2 100755 --- a/tools/testing/selftests/drivers/net/netconsole/netcons_basic.sh +++ b/tools/testing/selftests/drivers/net/netconsole/netcons_basic.sh @@ -58,7 +58,11 @@ do # Send the message echo "${MSG}: ${TARGET}" > /dev/kmsg # Wait until socat saves the file to disk - busywait "${BUSYWAIT_TIMEOUT}" test -s "${OUTPUT_FILE}" + if ! busywait "${BUSYWAIT_TIMEOUT}" test -s "${OUTPUT_FILE}" + then + echo "FAIL: Timed out waiting (${BUSYWAIT_TIMEOUT} ms) for netconsole message in ${OUTPUT_FILE}" >&2 + exit "${ksft_fail}" + fi # Make sure the message was received in the dst part # and exit diff --git a/tools/testing/selftests/drivers/net/netconsole/netcons_cmdline.sh b/tools/testing/selftests/drivers/net/netconsole/netcons_cmdline.sh index 96d704b8d9d9..4436567abc94 100755 --- a/tools/testing/selftests/drivers/net/netconsole/netcons_cmdline.sh +++ b/tools/testing/selftests/drivers/net/netconsole/netcons_cmdline.sh @@ -50,7 +50,7 @@ do # Send the message echo "${MSG}: ${TARGET}" > /dev/kmsg # Wait until socat saves the file to disk - busywait "${BUSYWAIT_TIMEOUT}" test -s "${OUTPUT_FILE}" + busywait "${BUSYWAIT_TIMEOUT}" test -s "${OUTPUT_FILE}" || true # Make sure the message was received in the dst part # and exit validate_msg "${OUTPUT_FILE}" diff --git a/tools/testing/selftests/drivers/net/netconsole/netcons_fragmented_msg.sh b/tools/testing/selftests/drivers/net/netconsole/netcons_fragmented_msg.sh index 0dc7280c3080..fc3db40c1df5 100755 --- a/tools/testing/selftests/drivers/net/netconsole/netcons_fragmented_msg.sh +++ b/tools/testing/selftests/drivers/net/netconsole/netcons_fragmented_msg.sh @@ -104,7 +104,7 @@ wait_local_port_listen "${NAMESPACE}" "${PORT}" udp # Send the message echo "${MSG}: ${TARGET}" > /dev/kmsg # Wait until socat saves the file to disk -busywait "${BUSYWAIT_TIMEOUT}" test -s "${OUTPUT_FILE}" +busywait "${BUSYWAIT_TIMEOUT}" test -s "${OUTPUT_FILE}" || true # Check if the message was not corrupted validate_fragmented_result "${OUTPUT_FILE}" @@ -117,6 +117,6 @@ disable_release_append listen_port_and_save_to "${OUTPUT_FILE}" & wait_local_port_listen "${NAMESPACE}" "${PORT}" udp echo "${MSG}: ${TARGET}" > /dev/kmsg -busywait "${BUSYWAIT_TIMEOUT}" test -s "${OUTPUT_FILE}" +busywait "${BUSYWAIT_TIMEOUT}" test -s "${OUTPUT_FILE}" || true validate_fragmented_result "${OUTPUT_FILE}" exit "${ksft_pass}" diff --git a/tools/testing/selftests/drivers/net/netconsole/netcons_resume.sh b/tools/testing/selftests/drivers/net/netconsole/netcons_resume.sh index cb59cf436dd0..b379dff9087e 100755 --- a/tools/testing/selftests/drivers/net/netconsole/netcons_resume.sh +++ b/tools/testing/selftests/drivers/net/netconsole/netcons_resume.sh @@ -44,7 +44,8 @@ function trigger_reactivation() { # Restore MACs ip netns exec "${NAMESPACE}" ip link set "${DSTIF}" \ address "${SAVED_DSTMAC}" - if [ "${BINDMODE}" == "mac" ]; then + if [ "${BINDMODE}" == "mac" ] && + [ "$(mac_get "${SRCIF}")" != "${SAVED_SRCMAC}" ]; then ip link set dev "${SRCIF}" down ip link set dev "${SRCIF}" address "${SAVED_SRCMAC}" # Rename device in order to trigger target resume, as initial @@ -107,7 +108,7 @@ do # Send the message echo "${MSG}: ${TARGET}" > /dev/kmsg # Wait until socat saves the file to disk - busywait "${BUSYWAIT_TIMEOUT}" test -s "${OUTPUT_FILE}" + busywait "${BUSYWAIT_TIMEOUT}" test -s "${OUTPUT_FILE}" || true # Make sure the message was received in the dst part # and exit validate_msg "${OUTPUT_FILE}" diff --git a/tools/testing/selftests/drivers/net/netconsole/netcons_sysdata.sh b/tools/testing/selftests/drivers/net/netconsole/netcons_sysdata.sh index 3fb8c4afe3d2..7089f7bd1e34 100755 --- a/tools/testing/selftests/drivers/net/netconsole/netcons_sysdata.sh +++ b/tools/testing/selftests/drivers/net/netconsole/netcons_sysdata.sh @@ -197,7 +197,7 @@ function runtest { # Send the message taskset -c "${CPU}" echo "${MSG}: ${TARGET}" > /dev/kmsg # Wait until socat saves the file to disk - busywait "${BUSYWAIT_TIMEOUT}" test -s "${OUTPUT_FILE}" + busywait "${BUSYWAIT_TIMEOUT}" test -s "${OUTPUT_FILE}" || true } # ========== # diff --git a/tools/testing/selftests/drivers/net/netconsole/netcons_userdata.sh b/tools/testing/selftests/drivers/net/netconsole/netcons_userdata.sh new file mode 100755 index 000000000000..113903f4ce1c --- /dev/null +++ b/tools/testing/selftests/drivers/net/netconsole/netcons_userdata.sh @@ -0,0 +1,229 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: GPL-2.0 + +# Exercise the netconsole userdata payload. +# +# The first part checks that the payload the target transmits follows what +# configfs says: a value shows up in the next message, an update replaces the +# previous one, clearing the value drops the entry, and so does removing the +# key. +# +# The second part rewrites values, creates and deletes keys, and clears the +# payload entirely while messages are being sent, so the transmit path keeps +# picking up payloads that are being replaced underneath it. It runs twice, +# once with a payload small enough to fit in a single packet and once large +# enough to be fragmented. +# +# Author: Breno Leitao <leitao@debian.org> + +set -euo pipefail + +SCRIPTDIR=$(dirname "$(readlink -e "${BASH_SOURCE[0]}")") + +source "${SCRIPTDIR}"/../lib/sh/lib_netcons.sh + +# Number of times each torture worker loops +ITERATIONS=${1:-200} + +# Keys owned by each torture worker. Workers do not share keys, so a failing +# configfs operation means a real problem and not a lost race. +CHURN_KEY="churnkey" +TRANSIENT_KEY="transientkey" +# Number of keys used to push a message past MAX_PRINT_CHUNK +BULK_KEYS=8 + +USERDATA_DIR="${NETCONS_PATH}/userdata" +# Values are capped at MAX_EXTRADATA_VALUE_LEN(200) bytes, so ${BULK_KEYS} +# entries of this size are enough to force fragmentation +LONG_VALUE=$(printf -- 'v%.0s' {1..190}) + +function write_key() { + local KEY="${1}" + local VALUE="${2}" + + mkdir -p "${USERDATA_DIR}/${KEY}" + echo "${VALUE}" > "${USERDATA_DIR}/${KEY}/value" +} + +# Send a single message and capture it on the destination interface +function send_and_capture() { + rm -f "${OUTPUT_FILE}" + + listen_port_and_save_to "${OUTPUT_FILE}" & + wait_for_port "${NAMESPACE}" "${PORT}" "${IP_VERSION}" + echo "${MSG}: ${TARGET}" > /dev/kmsg + busywait "${BUSYWAIT_TIMEOUT}" test -s "${OUTPUT_FILE}" || true + pkill_socat + validate_msg "${OUTPUT_FILE}" +} + +function expect_in_msg() { + local WANTED="${1}" + + if ! grep -q -- "${WANTED}" "${OUTPUT_FILE}"; then + echo "FAIL: '${WANTED}' not found in ${OUTPUT_FILE}" >&2 + cat "${OUTPUT_FILE}" >&2 + exit "${ksft_fail}" + fi +} + +function expect_not_in_msg() { + local UNWANTED="${1}" + + if grep -q -- "${UNWANTED}" "${OUTPUT_FILE}"; then + echo "FAIL: '${UNWANTED}' found in ${OUTPUT_FILE}" >&2 + cat "${OUTPUT_FILE}" >&2 + exit "${ksft_fail}" + fi +} + +# Every write publishes a new payload and frees the previous one. An empty +# value is skipped when the payload is formatted, so this also drives the +# target through having no payload at all. +function churn_value() { + local i + + for i in $(seq "${ITERATIONS}") + do + echo "value${i}" > "${USERDATA_DIR}/${CHURN_KEY}/value" + echo > "${USERDATA_DIR}/${CHURN_KEY}/value" + done +} + +# Create and delete a key underneath the sender +function churn_key() { + local i + + for i in $(seq "${ITERATIONS}") + do + mkdir "${USERDATA_DIR}/${TRANSIENT_KEY}" + echo "transient${i}" > "${USERDATA_DIR}/${TRANSIENT_KEY}/value" + rmdir "${USERDATA_DIR}/${TRANSIENT_KEY}" + done +} + +# Keep the transmit path busy while the payload is being replaced +function send_messages() { + local i + + for i in $(seq "${ITERATIONS}") + do + echo "${MSG}: ${TARGET} ${i}" > /dev/kmsg + done +} + +# Run the workers concurrently and fail if any of them hits an error +function run_workers() { + local PIDS=() + local WORKER + local RET=0 + local PID + + for WORKER in "$@" + do + "${WORKER}" & + PIDS+=("$!") + done + + # Reap every worker before reporting a failure, otherwise a surviving + # worker keeps writing to configfs while the exit trap cleans it up. + for PID in "${PIDS[@]}" + do + wait "${PID}" || RET=1 + done + + if [[ "${RET}" -ne 0 ]] + then + echo "FAIL: userdata torture worker failed" >&2 + exit "${ksft_fail}" + fi +} + +function create_bulk_keys() { + local i + + for i in $(seq "${BULK_KEYS}") + do + write_key "bulk${i}" "${LONG_VALUE}" + done +} + +function delete_bulk_keys() { + local i + + for i in $(seq "${BULK_KEYS}") + do + rmdir "${USERDATA_DIR}/bulk${i}" + done +} + +# ========== # +# Start here # +# ========== # + +modprobe netdevsim 2> /dev/null || true +modprobe netconsole 2> /dev/null || true + +IP_VERSION="ipv4" +# The content of kmsg will be saved to the following file +OUTPUT_FILE="/tmp/${TARGET}" + +# Check for basic system dependency and exit if not found +check_for_dependencies +# Set current loglevel to KERN_INFO(6), and default to KERN_NOTICE(5) +echo "6 5" > /proc/sys/kernel/printk +# Remove the namespace, interfaces and netconsole target on exit +trap cleanup EXIT +# Create one namespace and two interfaces +set_network "${IP_VERSION}" +# Create a dynamic target for netconsole +create_dynamic_target + +# =================================================== +# TEST #1 +# A value written to configfs reaches the destination +# =================================================== +write_key "${USERDATA_KEY}" "first" +send_and_capture +expect_in_msg "${USERDATA_KEY}=first" + +# =================================================== +# TEST #2 +# Updating the value replaces the previous payload +# =================================================== +write_key "${USERDATA_KEY}" "second" +send_and_capture +expect_in_msg "${USERDATA_KEY}=second" +expect_not_in_msg "${USERDATA_KEY}=first" + +# =================================================== +# TEST #3 +# Clearing the value drops the entry +# =================================================== +echo > "${USERDATA_DIR}/${USERDATA_KEY}/value" +send_and_capture +expect_not_in_msg "${USERDATA_KEY}=" + +# =================================================== +# TEST #4 +# Removing the key drops the entry +# =================================================== +write_key "${USERDATA_KEY}" "third" +rmdir "${USERDATA_DIR}/${USERDATA_KEY}" +send_and_capture +expect_not_in_msg "${USERDATA_KEY}=" +rm "${OUTPUT_FILE}" + +# =================================================== +# TEST #5 +# Torture the payload while messages are being sent, +# first unfragmented and then fragmented +# =================================================== +write_key "${CHURN_KEY}" "${USERDATA_VALUE}" +run_workers churn_value churn_key send_messages + +create_bulk_keys +run_workers churn_value churn_key send_messages +delete_bulk_keys + +exit "${ksft_pass}" diff --git a/tools/testing/selftests/drivers/net/netdevsim/Makefile b/tools/testing/selftests/drivers/net/netdevsim/Makefile index 1a228c5430f5..9808c2fbae9e 100644 --- a/tools/testing/selftests/drivers/net/netdevsim/Makefile +++ b/tools/testing/selftests/drivers/net/netdevsim/Makefile @@ -11,7 +11,6 @@ TEST_PROGS := \ fib.sh \ fib_notifications.sh \ hw_stats_l3.sh \ - macsec-offload.sh \ nexthop.sh \ peer.sh \ psample.sh \ diff --git a/tools/testing/selftests/drivers/net/netdevsim/devlink.sh b/tools/testing/selftests/drivers/net/netdevsim/devlink.sh index 1b529ccaf050..22a626c6cde3 100755 --- a/tools/testing/selftests/drivers/net/netdevsim/devlink.sh +++ b/tools/testing/selftests/drivers/net/netdevsim/devlink.sh @@ -5,7 +5,8 @@ lib_dir=$(dirname $0)/../../../net/forwarding ALL_TESTS="fw_flash_test params_test \ params_default_test regions_test reload_test \ - netns_reload_test resource_test dev_info_test \ + netns_reload_test resource_test resource_dump_test \ + port_resource_doit_test dev_info_test \ empty_reporter_test dummy_reporter_test rate_test" NUM_NETIFS=0 source $lib_dir/lib.sh @@ -482,6 +483,56 @@ resource_test() log_test "resource test" } +resource_dump_test() +{ + RET=0 + + local port_jq + local dev_jq + local dl_jq + local count + + dl_jq="with_entries(select(.key | startswith(\"$DL_HANDLE\")))" + port_jq="[.[] | $dl_jq | keys |" + port_jq+=" map(select(test(\"/.+/\"))) | length] | add" + dev_jq="[.[] | $dl_jq | keys |" + dev_jq+=" map(select(test(\"/.+/\")|not)) | length] | add" + + if ! devlink resource help 2>&1 | grep -q "scope"; then + echo "SKIP: devlink resource show not supported" + return + fi + + devlink resource show > /dev/null 2>&1 + check_err $? "Failed to dump all resources" + + count=$(cmd_jq "devlink resource show -j" "$port_jq") + [ "$count" -gt "0" ] + check_err $? "missing port resources in resource dump" + + count=$(cmd_jq "devlink resource show -j" "$dev_jq") + [ "$count" -gt "0" ] + check_err $? "missing device resources in resource dump" + + count=$(cmd_jq "devlink resource show scope dev -j" "$dev_jq") + [ "$count" -gt "0" ] + check_err $? "dev scope missing device resources" + + count=$(cmd_jq "devlink resource show scope dev -j" "$port_jq") + [ "$count" -eq "0" ] + check_err $? "dev scope returned port resources" + + count=$(cmd_jq "devlink resource show scope port -j" "$port_jq") + [ "$count" -gt "0" ] + check_err $? "port scope missing port resources" + + count=$(cmd_jq "devlink resource show scope port -j" "$dev_jq") + [ "$count" -eq "0" ] + check_err $? "port scope returned device resources" + + log_test "resource dump test" +} + info_get() { local name=$1 @@ -768,6 +819,32 @@ rate_node_del() devlink port function rate del $handle } +port_resource_doit_test() +{ + RET=0 + + local port_handle="${DL_HANDLE}/0" + local name + local size + + if ! devlink resource help 2>&1 | grep -q "PORT_INDEX"; then + echo "SKIP: devlink resource show with port not supported" + return + fi + + name=$(cmd_jq "devlink resource show $port_handle -j" \ + '.[][][].name') + [ "$name" == "test_resource" ] + check_err $? "wrong port resource name (got $name)" + + size=$(cmd_jq "devlink resource show $port_handle -j" \ + '.[][][].size') + [ "$size" == "20" ] + check_err $? "wrong port resource size (got $size)" + + log_test "port resource doit test" +} + rate_test() { RET=0 diff --git a/tools/testing/selftests/drivers/net/netdevsim/ethtool-coalesce.sh b/tools/testing/selftests/drivers/net/netdevsim/ethtool-coalesce.sh index 9adfba8f87e6..b9fcafad4258 100755 --- a/tools/testing/selftests/drivers/net/netdevsim/ethtool-coalesce.sh +++ b/tools/testing/selftests/drivers/net/netdevsim/ethtool-coalesce.sh @@ -116,12 +116,14 @@ done # bool settings which ethtool displays on the same line ethtool -C $NSIM_NETDEV adaptive-rx on -s=$(ethtool -c $NSIM_NETDEV | grep -q "Adaptive RX: on TX: off") -check $? "$s" "" +s=$(ethtool -c $NSIM_NETDEV) +echo "$s" | grep -q "Adaptive RX: on TX: off" +check $? "" "" ethtool -C $NSIM_NETDEV adaptive-tx on -s=$(ethtool -c $NSIM_NETDEV | grep -q "Adaptive RX: on TX: on") -check $? "$s" "" +s=$(ethtool -c $NSIM_NETDEV) +echo "$s" | grep -q "Adaptive RX: on TX: on" +check $? "" "" if [ $num_errors -eq 0 ]; then echo "PASSED all $((num_passes)) checks" diff --git a/tools/testing/selftests/drivers/net/netdevsim/macsec-offload.sh b/tools/testing/selftests/drivers/net/netdevsim/macsec-offload.sh deleted file mode 100755 index 98033e6667d2..000000000000 --- a/tools/testing/selftests/drivers/net/netdevsim/macsec-offload.sh +++ /dev/null @@ -1,117 +0,0 @@ -#!/bin/bash -# SPDX-License-Identifier: GPL-2.0-only - -source ethtool-common.sh - -NSIM_NETDEV=$(make_netdev) -MACSEC_NETDEV=macsec_nsim - -set -o pipefail - -if ! ethtool -k $NSIM_NETDEV | grep -q 'macsec-hw-offload: on'; then - echo "SKIP: netdevsim doesn't support MACsec offload" - exit 4 -fi - -if ! ip link add link $NSIM_NETDEV $MACSEC_NETDEV type macsec offload mac 2>/dev/null; then - echo "SKIP: couldn't create macsec device" - exit 4 -fi -ip link del $MACSEC_NETDEV - -# -# test macsec offload API -# - -ip link add link $NSIM_NETDEV "${MACSEC_NETDEV}" type macsec port 4 offload mac -check $? - -ip link add link $NSIM_NETDEV "${MACSEC_NETDEV}2" type macsec address "aa:bb:cc:dd:ee:ff" port 5 offload mac -check $? - -ip link add link $NSIM_NETDEV "${MACSEC_NETDEV}3" type macsec sci abbacdde01020304 offload mac -check $? - -ip link add link $NSIM_NETDEV "${MACSEC_NETDEV}4" type macsec port 8 offload mac 2> /dev/null -check $? '' '' 1 - -ip macsec add "${MACSEC_NETDEV}" tx sa 0 pn 1024 on key 01 12345678901234567890123456789012 -check $? - -ip macsec add "${MACSEC_NETDEV}" rx port 1234 address "1c:ed:de:ad:be:ef" -check $? - -ip macsec add "${MACSEC_NETDEV}" rx port 1234 address "1c:ed:de:ad:be:ef" sa 0 pn 1 on \ - key 00 0123456789abcdef0123456789abcdef -check $? - -ip macsec add "${MACSEC_NETDEV}" rx port 1235 address "1c:ed:de:ad:be:ef" 2> /dev/null -check $? '' '' 1 - -# can't disable macsec offload when SAs are configured -ip link set "${MACSEC_NETDEV}" type macsec offload off 2> /dev/null -check $? '' '' 1 - -ip macsec offload "${MACSEC_NETDEV}" off 2> /dev/null -check $? '' '' 1 - -# toggle macsec offload via rtnetlink -ip link set "${MACSEC_NETDEV}2" type macsec offload off -check $? - -ip link set "${MACSEC_NETDEV}2" type macsec offload mac -check $? - -# toggle macsec offload via genetlink -ip macsec offload "${MACSEC_NETDEV}2" off -check $? - -ip macsec offload "${MACSEC_NETDEV}2" mac -check $? - -for dev in ${MACSEC_NETDEV}{,2,3} ; do - ip link del $dev - check $? -done - - -# -# test ethtool features when toggling offload -# - -ip link add link $NSIM_NETDEV $MACSEC_NETDEV type macsec offload mac -TMP_FEATS_ON_1="$(ethtool -k $MACSEC_NETDEV)" - -ip link set $MACSEC_NETDEV type macsec offload off -TMP_FEATS_OFF_1="$(ethtool -k $MACSEC_NETDEV)" - -ip link set $MACSEC_NETDEV type macsec offload mac -TMP_FEATS_ON_2="$(ethtool -k $MACSEC_NETDEV)" - -[ "$TMP_FEATS_ON_1" = "$TMP_FEATS_ON_2" ] -check $? - -ip link del $MACSEC_NETDEV - -ip link add link $NSIM_NETDEV $MACSEC_NETDEV type macsec -check $? - -TMP_FEATS_OFF_2="$(ethtool -k $MACSEC_NETDEV)" -[ "$TMP_FEATS_OFF_1" = "$TMP_FEATS_OFF_2" ] -check $? - -ip link set $MACSEC_NETDEV type macsec offload mac -check $? - -TMP_FEATS_ON_3="$(ethtool -k $MACSEC_NETDEV)" -[ "$TMP_FEATS_ON_1" = "$TMP_FEATS_ON_3" ] -check $? - - -if [ $num_errors -eq 0 ]; then - echo "PASSED all $((num_passes)) checks" - exit 0 -else - echo "FAILED $num_errors/$((num_errors+num_passes)) checks" - exit 1 -fi diff --git a/tools/testing/selftests/drivers/net/psp.py b/tools/testing/selftests/drivers/net/psp.py index 864d9fce1094..a5b1e14f120f 100755 --- a/tools/testing/selftests/drivers/net/psp.py +++ b/tools/testing/selftests/drivers/net/psp.py @@ -5,6 +5,7 @@ import errno import fcntl +import os import socket import struct import termios @@ -14,9 +15,15 @@ from lib.py import defer from lib.py import ksft_run, ksft_exit, ksft_pr from lib.py import ksft_true, ksft_eq, ksft_ne, ksft_gt, ksft_raises from lib.py import ksft_not_none -from lib.py import KsftSkipEx -from lib.py import NetDrvEpEnv, PSPFamily, NlError +from lib.py import ksft_variants, KsftNamedVariant +from lib.py import KsftSkipEx, KsftFailEx +from lib.py import NetDrvEpEnv, NetDrvContEnv +from lib.py import Netlink, NlError, PSPFamily, RtnlFamily +from lib.py import NetNSEnter from lib.py import bkg, rand_port, wait_port_listen +from lib.py import ip + +TCP_ULP = 31 def _get_outq(s): @@ -117,11 +124,13 @@ def _get_stat(cfg, key): # Test case boiler plate # -def _init_psp_dev(cfg): +def _init_psp_dev(cfg, use_psp_ifindex=False): if not hasattr(cfg, 'psp_dev_id'): # Figure out which local device we are testing against + # For NetDrvContEnv: use psp_ifindex instead of ifindex + target_ifindex = cfg.psp_ifindex if use_psp_ifindex else cfg.ifindex for dev in cfg.pspnl.dev_get({}, dump=True): - if dev['ifindex'] == cfg.ifindex: + if dev['ifindex'] == target_ifindex: cfg.psp_info = dev cfg.psp_dev_id = cfg.psp_info['id'] break @@ -326,6 +335,50 @@ def assoc_version_mismatch(cfg): ksft_eq(the_exception.nl_msg.error, -errno.EINVAL) +def _require_tls_ulp(): + with socket.create_server(("localhost", 0)) as srv, \ + socket.create_connection(srv.getsockname()) as s: + try: + s.setsockopt(socket.SOL_TCP, TCP_ULP, b"tls") + except OSError as exc: + raise KsftSkipEx("kTLS not available") from exc + + +def assoc_psp_ulp_exclusive(cfg): + """ Test that a TCP ULP cannot be attached to a PSP socket """ + _init_psp_dev(cfg) + _require_tls_ulp() + + with _make_clr_conn(cfg) as s: + try: + cfg.pspnl.rx_assoc({"version": 0, + "dev-id": cfg.psp_dev_id, + "sock-fd": s.fileno()}) + with ksft_raises(OSError) as cm: + s.setsockopt(socket.SOL_TCP, TCP_ULP, b"tls") + ksft_eq(cm.exception.errno, errno.EINVAL) + finally: + _close_conn(cfg, s) + + +def assoc_ulp_psp_exclusive(cfg): + """ Test that a PSP assoc cannot be added to a socket with a TCP ULP """ + _init_psp_dev(cfg) + _require_tls_ulp() + + with _make_clr_conn(cfg) as s: + try: + s.setsockopt(socket.SOL_TCP, TCP_ULP, b"tls") + with ksft_raises(NlError) as cm: + cfg.pspnl.rx_assoc({"version": 0, + "dev-id": cfg.psp_dev_id, + "sock-fd": s.fileno()}) + ksft_eq(cm.exception.nl_msg.error, -errno.EINVAL) + ksft_eq(cm.exception.nl_msg.extack['bad-attr'], ".sock-fd") + finally: + _close_conn(cfg, s) + + def assoc_twice(cfg): """ Test reusing Tx assoc for two sockets """ _init_psp_dev(cfg) @@ -571,33 +624,388 @@ def removal_device_bi(cfg): _close_conn(cfg, s) -def psp_ip_ver_test_builder(name, test_func, psp_ver, ipver): - """Build test cases for each combo of PSP version and IP version""" - def test_case(cfg): - cfg.require_ipver(ipver) - test_func(cfg, psp_ver, ipver) +def _get_psp_ver_ip_variants(): + for ver in range(4): + for ipv in ("4", "6"): + yield KsftNamedVariant(f"v{ver}_ip{ipv}", ver, ipv) + + +def _get_ip_variants(): + for ipv in ("4", "6"): + yield KsftNamedVariant(f"ip{ipv}", ipv) + + +@ksft_variants(_get_psp_ver_ip_variants()) +def data_basic_send(cfg, version, ipver): + """Test basic PSP data send.""" + cfg.require_ipver(ipver) + _data_basic_send(cfg, version, ipver) + + +@ksft_variants(_get_ip_variants()) +def data_mss_adjust(cfg, ipver): + """Test MSS adjustment with PSP.""" + cfg.require_ipver(ipver) + _data_mss_adjust(cfg, ipver) + + +def _check_assoc_list(cfg, psp_dev_id, ifindex, nsid=None): + """Verify assoc-list contains device with given ifindex, no duplicates.""" + dev_info = cfg.pspnl.dev_get({'id': psp_dev_id}) + + ksft_true('assoc-list' in dev_info, + "No assoc-list in dev_get() response after association") + found = False + for assoc in dev_info['assoc-list']: + if assoc['ifindex'] != ifindex: + continue + if nsid is not None and assoc['nsid'] != nsid: + continue + ksft_eq(found, False, "Duplicate assoc entry found") + found = True + ksft_eq(found, True, + "Associated device not found in dev_get() response") + + +def _data_basic_send_netkit_psp_assoc(cfg, version, ipver): + """ + Test basic data send with netkit interface associated with PSP dev. + """ + _assoc_nk_guest(cfg) + + # Enter guest namespace (netns) to run PSP test + with NetNSEnter(cfg.netns.name): + cfg.pspnl = PSPFamily() + + sock = _make_psp_conn(cfg, version, ipver) + + rx_assoc = cfg.pspnl.rx_assoc({"version": version, + "dev-id": cfg.psp_dev_id, + "sock-fd": sock.fileno()}) + rx_key = rx_assoc['rx-key'] + tx_key = _spi_xchg(sock, rx_key) + + cfg.pspnl.tx_assoc({"dev-id": cfg.psp_dev_id, + "version": version, + "tx-key": tx_key, + "sock-fd": sock.fileno()}) + + data_len = _send_careful(cfg, sock, 100) + _check_data_rx(cfg, data_len) + _close_psp_conn(cfg, sock) + + +def _assoc_check_list(cfg): + """Test that assoc-list is correctly populated after dev-assoc.""" + _assoc_nk_guest(cfg) + _check_assoc_list(cfg, cfg.psp_dev_id, cfg.nk_guest_ifindex, + cfg.psp_dev_peer_nsid) - test_case.__name__ = f"{name}_v{psp_ver}_ip{ipver}" - return test_case +def _get_psp_ver_ip6_variants(): + for ver in range(4): + yield KsftNamedVariant(f"v{ver}_ip6", ver, "6") -def ipver_test_builder(name, test_func, ipver): - """Build test cases for each IP version""" - def test_case(cfg): - cfg.require_ipver(ipver) - test_func(cfg, ipver) - test_case.__name__ = f"{name}_ip{ipver}" - return test_case +@ksft_variants(_get_psp_ver_ip6_variants()) +def data_basic_send_netkit_psp_assoc(cfg, version, ipver): + """Test PSP data send via netkit with dev-assoc.""" + cfg.require_ipver(ipver) + _data_basic_send_netkit_psp_assoc(cfg, version, ipver) + + +def _key_rotation_notify_multi_ns_netkit(cfg): + """ Test key rotation notifications across multiple namespaces using netkit """ + _assoc_nk_guest(cfg) + + # Create listener in guest namespace; socket stays bound to that ns + with NetNSEnter(cfg.netns.name): + peer_pspnl = PSPFamily() + peer_pspnl.ntf_subscribe('use') + + # Create listener in main namespace + main_pspnl = PSPFamily() + main_pspnl.ntf_subscribe('use') + + # Trigger key rotation on the PSP device + cfg.pspnl.key_rotate({"id": cfg.psp_dev_id}) + + # Poll both sockets from main thread + for pspnl, label in [(main_pspnl, "main"), (peer_pspnl, "guest")]: + for ntf in pspnl.poll_ntf(duration=10): + if ntf['msg'].get('id') == cfg.psp_dev_id: + break + else: + raise KsftFailEx( + f"No key rotation notification received" + f" in {label} namespace") + + +def _dev_change_notify_multi_ns_netkit(cfg): + """ Test dev_change notifications across multiple namespaces using netkit """ + _assoc_nk_guest(cfg) + + # Create listener in guest namespace; socket stays bound to that ns + with NetNSEnter(cfg.netns.name): + peer_pspnl = PSPFamily() + peer_pspnl.ntf_subscribe('mgmt') + + # Create listener in main namespace + main_pspnl = PSPFamily() + main_pspnl.ntf_subscribe('mgmt') + + # Trigger dev_change by calling dev_set (notification is always sent) + cfg.pspnl.dev_set({'id': cfg.psp_dev_id, + 'psp-versions-ena': cfg.psp_info['psp-versions-cap']}) + + # Poll both sockets from main thread + for pspnl, label in [(main_pspnl, "main"), (peer_pspnl, "guest")]: + for ntf in pspnl.poll_ntf(duration=10): + if ntf['msg'].get('id') == cfg.psp_dev_id: + break + else: + raise KsftFailEx( + f"No dev_change notification received" + f" in {label} namespace") + + +def _psp_dev_get_check_netkit_psp_assoc(cfg): + """ Check psp dev-get output with netkit interface associated with PSP dev """ + _assoc_nk_guest(cfg) + + # Check 1: In default netns, verify dev-get has correct ifindex and assoc-list + dev_info = cfg.pspnl.dev_get({'id': cfg.psp_dev_id}) + ksft_eq(dev_info['ifindex'], cfg.psp_ifindex) + _check_assoc_list(cfg, cfg.psp_dev_id, cfg.nk_guest_ifindex, + cfg.psp_dev_peer_nsid) + + # Check 2: In guest netns, verify dev-get has assoc-list with nk_guest device + with NetNSEnter(cfg.netns.name): + peer_pspnl = PSPFamily() + + # Dump all devices in the guest namespace + peer_devices = peer_pspnl.dev_get({}, dump=True) + + # Find the device with by-association flag + peer_dev = None + for dev in peer_devices: + if dev.get('by-association'): + peer_dev = dev + break + + ksft_not_none(peer_dev, "No PSP device found with by-association flag in guest netns") + + # Verify assoc-list contains the nk_guest device + ksft_true('assoc-list' in peer_dev and len(peer_dev['assoc-list']) > 0, + "Guest device should have assoc-list with local devices") + + # Verify the assoc-list contains nk_guest ifindex with nsid=-1 (same namespace) + found = False + for assoc in peer_dev['assoc-list']: + if assoc['ifindex'] == cfg.nk_guest_ifindex: + ksft_eq(assoc['nsid'], -1, + "nsid should be -1 (NETNSA_NSID_NOT_ASSIGNED) for same-namespace device") + found = True + break + ksft_true(found, "nk_guest ifindex not found in assoc-list") + + +def _dev_assoc_no_nsid(cfg): + """ Test dev-assoc and dev-disassoc without nsid attribute """ + _init_psp_dev(cfg, True) + + # Associate without nsid - should look up ifindex in caller's netns + cfg.pspnl.dev_assoc({'id': cfg.psp_dev_id, + 'ifindex': cfg.nk_host_ifindex}) + defer(_try_disassoc, cfg, + cfg.psp_dev_id, cfg.nk_host_ifindex) + defer(delattr, cfg, 'psp_dev_id') + defer(delattr, cfg, 'psp_info') + + # Verify assoc-list contains the device (match by ifindex only) + _check_assoc_list(cfg, cfg.psp_dev_id, cfg.nk_host_ifindex) + + # Disassociate without nsid - should also use caller's netns + cfg.pspnl.dev_disassoc({'id': cfg.psp_dev_id, + 'ifindex': cfg.nk_host_ifindex}) + + # Verify assoc-list no longer contains the device + dev_info = cfg.pspnl.dev_get({'id': cfg.psp_dev_id}) + found = False + if 'assoc-list' in dev_info: + for assoc in dev_info['assoc-list']: + if assoc['ifindex'] == cfg.nk_host_ifindex: + found = True + break + ksft_true(not found, "Device should not be in assoc-list after disassociation") + + +def _psp_dev_assoc_cleanup_on_netkit_del(cfg): + """Test that assoc-list is cleared when associated netkit is deleted. + + Creates a disposable netkit pair for this test to avoid destroying + the shared environment. + """ + _init_psp_dev(cfg, True) + defer(delattr, cfg, 'psp_dev_id') + defer(delattr, cfg, 'psp_info') + + existing = {cfg.nk_host_ifindex, cfg.nk_guest_ifindex} + + # Create a temporary netkit pair + tmp_host_name = "tmp_nk_host" + tmp_guest_name = "tmp_nk_guest" + rtnl = RtnlFamily() + rtnl.newlink( + { + "ifname": tmp_host_name, + "linkinfo": { + "kind": "netkit", + "data": { + "mode": "l2", + "policy": "forward", + "peer-policy": "forward", + }, + }, + }, + flags=[Netlink.NLM_F_CREATE, Netlink.NLM_F_EXCL], + ) + cleanup_netkit = defer(ip, f"link del {tmp_host_name}") + + # Find the peer by diffing against existing netkit ifindexes + all_links = ip("-d link show", json=True) + tmp_peer = [link for link in all_links + if link.get('linkinfo', {}).get('info_kind') == 'netkit' + and link['ifindex'] not in existing + and link['ifname'] != tmp_host_name] + ksft_eq(len(tmp_peer), 1, + "Failed to find temporary netkit peer") + guest_name = tmp_peer[0]['ifname'] + + # Rename and move guest end into the test namespace + ip(f"link set dev {guest_name} name {tmp_guest_name}") + ip(f"link set dev {tmp_guest_name} netns {cfg.netns.name}") + tmp_guest_dev = ip(f"link show dev {tmp_guest_name}", + json=True, ns=cfg.netns)[0] + tmp_guest_ifindex = tmp_guest_dev['ifindex'] + ip(f"link set dev {tmp_guest_name} up", ns=cfg.netns) + + # Associate PSP device with the temporary guest interface + cfg.pspnl.dev_assoc({'id': cfg.psp_dev_id, + 'ifindex': tmp_guest_ifindex, + 'nsid': cfg.psp_dev_peer_nsid}) + + # Verify assoc-list contains the temporary device + _check_assoc_list(cfg, cfg.psp_dev_id, tmp_guest_ifindex, + cfg.psp_dev_peer_nsid) + + # Delete the temporary netkit pair (deleting one end removes both) + ip(f"link del {tmp_host_name}") + cleanup_netkit.cancel() + + # Verify assoc-list is cleared after netkit deletion + dev_info = cfg.pspnl.dev_get({'id': cfg.psp_dev_id}) + ksft_true('assoc-list' not in dev_info + or len(dev_info['assoc-list']) == 0, + "assoc-list should be empty after netkit deletion") + + +def _try_disassoc(cfg, psp_dev_id, ifindex, nsid=None): + """Best-effort disassociate, ignoring errors if already removed.""" + try: + params = {'id': psp_dev_id, 'ifindex': ifindex} + if nsid is not None: + params['nsid'] = nsid + cfg.pspnl.dev_disassoc(params) + except NlError: + pass + + +def _assoc_nk_guest(cfg): + """Associate nk_guest with PSP device and register cleanup via defer().""" + _init_psp_dev(cfg, True) + + cfg.pspnl.dev_assoc({'id': cfg.psp_dev_id, + 'ifindex': cfg.nk_guest_ifindex, + 'nsid': cfg.psp_dev_peer_nsid}) + defer(_disassoc_nk_guest, cfg, + cfg.psp_dev_id, cfg.nk_guest_ifindex) + + +def _disassoc_nk_guest(cfg, psp_dev_id, nk_guest_ifindex): + """Disassociate nk_guest and reset cfg PSP state.""" + pspnl = PSPFamily() + pspnl.dev_disassoc({'id': psp_dev_id, 'ifindex': nk_guest_ifindex, + 'nsid': cfg.psp_dev_peer_nsid}) + cfg.pspnl = pspnl + del cfg.psp_dev_id + del cfg.psp_info + + +def _get_nsid(ns_name): + """Get the nsid for a namespace.""" + for entry in ip("netns list-id", json=True): + if entry.get("name") == str(ns_name): + return entry["nsid"] + raise KsftSkipEx(f"nsid not found for namespace {ns_name}") + + +def _setup_psp_attributes(cfg): + # pylint: disable=protected-access + """ + Set up PSP-specific attributes on the environment. + + This sets attributes needed for PSP tests based on whether we're using + netdevsim or a real NIC. + """ + if cfg._ns is not None: + # netdevsim case: PSP device is the local dev (in host namespace) + cfg.psp_dev = cfg._ns.nsims[0].dev + cfg.psp_ifname = cfg.psp_dev['ifname'] + cfg.psp_ifindex = cfg.psp_dev['ifindex'] + + # PSP peer device is the remote dev (in _netns, where psp_responder runs) + cfg.psp_dev_peer = cfg._ns_peer.nsims[0].dev + cfg.psp_dev_peer_ifname = cfg.psp_dev_peer['ifname'] + cfg.psp_dev_peer_ifindex = cfg.psp_dev_peer['ifindex'] + else: + # Real NIC case: PSP device is the local interface + cfg.psp_dev = cfg.dev + cfg.psp_ifname = cfg.ifname + cfg.psp_ifindex = cfg.ifindex + + # PSP peer device is the remote interface + cfg.psp_dev_peer = cfg.remote_dev + cfg.psp_dev_peer_ifname = cfg.remote_ifname + cfg.psp_dev_peer_ifindex = cfg.remote_ifindex + + # Get nsid for the guest namespace (netns) where nk_guest is + cfg.psp_dev_peer_nsid = _get_nsid(cfg.netns.name) + def main() -> None: """ Ksft boiler plate main """ - with NetDrvEpEnv(__file__) as cfg: + # Make sure LOCAL_PREFIX_V6 is set + if "LOCAL_PREFIX_V6" not in os.environ: + os.environ["LOCAL_PREFIX_V6"] = "2001:db8:2::" + + try: + env = NetDrvContEnv(__file__, primary_rx_redirect=True) + has_cont = True + except KsftSkipEx: + env = NetDrvEpEnv(__file__) + has_cont = False + + with env as cfg: cfg.pspnl = PSPFamily() + if has_cont: + _setup_psp_attributes(cfg) + # Set up responder and communication sock + # psp_responder runs in _netns (remote namespace with psp_dev_peer) responder = cfg.remote.deploy("psp_responder") cfg.comm_port = rand_port() @@ -611,17 +1019,18 @@ def main() -> None: cfg.comm_port), timeout=1) - cases = [ - psp_ip_ver_test_builder( - "data_basic_send", _data_basic_send, version, ipver - ) - for version in range(0, 4) - for ipver in ("4", "6") - ] - cases += [ - ipver_test_builder("data_mss_adjust", _data_mss_adjust, ipver) - for ipver in ("4", "6") - ] + cases = [data_basic_send, data_mss_adjust] + + if has_cont: + cases += [ + _assoc_check_list, + data_basic_send_netkit_psp_assoc, + _key_rotation_notify_multi_ns_netkit, + _dev_change_notify_multi_ns_netkit, + _psp_dev_get_check_netkit_psp_assoc, + _dev_assoc_no_nsid, + _psp_dev_assoc_cleanup_on_netkit_del, + ] ksft_run(cases=cases, globs=globals(), case_pfx={"dev_", "data_", "assoc_", "removal_"}, diff --git a/tools/testing/selftests/drivers/net/ring_reconfig.py b/tools/testing/selftests/drivers/net/ring_reconfig.py index f9530a8b0856..2bc329b77134 100755 --- a/tools/testing/selftests/drivers/net/ring_reconfig.py +++ b/tools/testing/selftests/drivers/net/ring_reconfig.py @@ -5,10 +5,25 @@ Test channel and ring size configuration via ethtool (-L / -G). """ +import socket +import struct +import time + from lib.py import ksft_run, ksft_exit, ksft_pr from lib.py import ksft_eq +from lib.py import KsftSkipEx, KsftXfailEx from lib.py import NetDrvEpEnv, EthtoolFamily, GenerateTraffic -from lib.py import defer, NlError +from lib.py import cmd, defer, rand_port, tc, NlError + +# Added in Python 3.13; fallback to 61 for x86/ARM/MIPS +SO_TXTIME = getattr(socket, "SO_TXTIME", 61) + +# Not always exported by the socket module; asm-generic value (x86/ARM/MIPS). +SO_SNDBUFFORCE = getattr(socket, "SO_SNDBUFFORCE", 32) + +# TX ring size the test shrinks to so the ring fills quickly. +MIN_TX_RING = 32 +MAX_TX_RING = 1024 def channels(cfg) -> None: @@ -151,14 +166,248 @@ def ringparam(cfg) -> None: GenerateTraffic(cfg).wait_pkts_and_stop(10000) +def _write_file(path, val): + """Write val to a file.""" + with open(path, "w", encoding="utf-8") as fp: + fp.write(str(val)) + + +def _write_sysfs(path, val): + """Write val to a sysfs file, restoring the original value on exit.""" + with open(path, "r", encoding="utf-8") as fp: + orig_val = fp.read().strip() + if str(val) == orig_val: + return + _write_file(path, val) + defer(_write_file, path, orig_val) + + +def _get_qdisc_backlog(cfg, mq_handle, queue): + """Return the qdisc backlog (bytes) for the given TX queue's leaf.""" + target_parent = f"{mq_handle}{queue + 1:x}" + for q in tc(f"-s qdisc show dev {cfg.ifname}", json=True): + if q.get("parent", "") == target_parent: + return q.get("backlog") or 0 + return 0 + + +def _setup_fq_qdisc(cfg, port, target_queue, other_queue, flow_limit): + """Put an fq qdisc on target_queue's leaf and return the mq handle in use. + + We must not disturb the device's existing TX/RX qdisc policy. On a real + NIC the root mq already has an addressable handle, so we leave the root + and every other queue alone and only swap this one leaf, restoring its + original qdisc afterwards. + + @flow_limit raises fq's per-flow packet limit (default 100) so a single + flow can back up more packets than the Tx ring holds and thus overflow it. + """ + qdiscs = tc(f"qdisc show dev {cfg.ifname}", json=True) + root = next((q for q in qdiscs if q.get("root")), None) + + if root and root["kind"] == "mq" and root["handle"] != "0:": + # Addressable mq (previously-configured): touch only the target queue's + # leaf and restore its original qdisc afterwards. + mq_handle = root["handle"] + parent = f"{mq_handle}{target_queue + 1:x}" + orig = next((q for q in qdiscs if q.get("parent") == parent), None) + orig_kind = orig["kind"] if orig else \ + cmd("sysctl -n net.core.default_qdisc").stdout.strip() + defer(tc, f"qdisc replace dev {cfg.ifname} parent {parent} {orig_kind}") + elif root is None or root["kind"] in ("mq", "noqueue"): + # The auto-attached root mq has handle 0: on any device (real or sim), + # which the kernel rejects as a qdisc parent. A 0: handle means the mq + # is the untouched kernel default - no custom child qdiscs can hang off + # an unaddressable parent - so installing a real handle and restoring + # the default mq on exit preserves the device's effective policy. + mq_handle = "1:" + tc(f"qdisc replace dev {cfg.ifname} root handle {mq_handle} mq") + defer(tc, f"qdisc replace dev {cfg.ifname} root mq") + parent = f"{mq_handle}{target_queue + 1:x}" + else: + raise KsftSkipEx(f"root qdisc '{root['kind']}' is not mq; " + "refusing to disturb existing qdisc policy") + + try: + tc(f"qdisc replace dev {cfg.ifname} parent {parent} fq " + f"flow_limit {flow_limit} limit {flow_limit * 2}") + except Exception as exc: + raise KsftSkipEx( + f"fq not available (CONFIG_NET_SCH_FQ): {exc}") from exc + + qdisc_j = tc(f"qdisc show dev {cfg.ifname}", json=True) + has_clsact = any(q['kind'] == 'clsact' for q in qdisc_j) + if not has_clsact: + tc(f"qdisc add dev {cfg.ifname} clsact") + defer(tc, f"qdisc del dev {cfg.ifname} clsact") + + proto = "ipv6" if int(cfg.addr_ipver) == 6 else "ip" + try: + tc(f"filter add dev {cfg.ifname} egress protocol {proto} " + f"pref 1 flower ip_proto udp dst_port {port} " + f"action skbedit queue_mapping {target_queue}") + except Exception as exc: + raise KsftSkipEx("tc flower/act_skbedit not available") from exc + defer(tc, f"filter del dev {cfg.ifname} egress pref 1") + + tc(f"filter add dev {cfg.ifname} egress pref 101 " + f"matchall action skbedit queue_mapping {other_queue}") + defer(tc, f"filter del dev {cfg.ifname} egress pref 101") + + return mq_handle + + +def _create_sotxtime_socket(cfg, sndbuf): + """Create a UDP socket with SO_TXTIME enabled, bound to the test device.""" + sock = socket.socket(socket.AF_INET6 if cfg.addr_ipver == "6" + else socket.AF_INET, socket.SOCK_DGRAM) + try: + sock.setsockopt(socket.SOL_SOCKET, SO_TXTIME, struct.pack("Ii", 1, 0)) + except OSError as exc: + sock.close() + raise KsftSkipEx("SO_TXTIME not supported") from exc + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, + cfg.ifname.encode()) + # Deferred completions keep every in-flight skb charged to the socket, so + # size the send buffer to hold the whole burst. SO_SNDBUFFORCE bypasses + # net.core.wmem_max (the test runs as root). + try: + sock.setsockopt(socket.SOL_SOCKET, SO_SNDBUFFORCE, sndbuf) + except OSError: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, sndbuf) + return sock + + +def _send_sotxtime_burst(cfg, sock, port, count, delay_ns, pkt_size): + """Send count UDP packets scheduled delay_ns ahead using SO_TXTIME.""" + payload = b'\x00' * pkt_size + txtime_ns = time.clock_gettime_ns(time.CLOCK_MONOTONIC) + delay_ns + + ancdata = [(socket.SOL_SOCKET, SO_TXTIME, struct.pack("Q", txtime_ns))] + if int(cfg.addr_ipver) == 6: + dest = (cfg.remote_addr, port, 0, 0) + else: + dest = (cfg.remote_addr, port) + for _ in range(count): + sock.sendmsg([payload], ancdata, 0, dest) + + +def _set_small_tx_ring(cfg, ehdr): + """Set the Tx ring to the smallest size the driver accepts. + + Start at 32 so the ring fills quickly, then grow exponentially (64, + 128, 256, ...) up to 1024. Some drivers enforce a minimum well above 32 + (e.g. bnxt needs a large ring for software UDP segmentation), so raise + the lower bound until the driver accepts it, giving up past 1024. + """ + size = MIN_TX_RING + while size <= MAX_TX_RING: + try: + cfg.eth.rings_set(ehdr | {'tx': size}) + return size + except NlError: + size = size * 2 + continue + raise KsftSkipEx("driver rejects all tx ring sizes up to 1024") + + +def reconfig_tx_stall(cfg) -> None: + """Test that qdisc backlog drains after ring reconfiguration.""" + target_queue = 1 + other_queue = 0 + + ehdr = {'header': {'dev-index': cfg.ifindex}} + chans = cfg.eth.channels_get(ehdr) + + if "combined-max" not in chans: + raise KsftSkipEx("device does not support combined channels") + if chans.get("combined-max", 0) < 2: + raise KsftSkipEx("device does not support 2+ combined channels") + if chans["combined-count"] < 2: + defer(cfg.eth.channels_set, + ehdr | {"combined-count": chans["combined-count"]}) + cfg.eth.channels_set(ehdr | {"combined-count": 2}) + + rings = cfg.eth.rings_get(ehdr) + if 'rx' not in rings or 'tx' not in rings: + raise KsftSkipEx("device does not expose rx/tx ring params") + tx_cur = rings['tx'] + if tx_cur <= MIN_TX_RING: + raise KsftSkipEx("tx ring size already at minimum") + defer(cfg.eth.rings_set, ehdr | {'tx': tx_cur}) + + # Use the smallest Tx ring the driver accepts (32, growing to 1024). + tx_ring = _set_small_tx_ring(cfg, ehdr) + + # Slow completions so the ring stays full after FQ releases packets + napi_defer = f"/sys/class/net/{cfg.ifname}/napi_defer_hard_irqs" + gro_timeout = f"/sys/class/net/{cfg.ifname}/gro_flush_timeout" + _write_sysfs(napi_defer, 100) + _write_sysfs(gro_timeout, 1000000000) + + port = rand_port() + # A single flow must overflow the ring, so send twice the ring depth and + # let fq hold that many packets for the flow. + pkt_count = tx_ring * 2 + mq_handle = _setup_fq_qdisc(cfg, port, target_queue, other_queue, + tx_ring * 2) + + # Size each packet to one MTU (less L3/L4 headers to avoid fragmentation). + pkt_size = cfg.dev['mtu'] - (48 if int(cfg.addr_ipver) == 6 else 28) + + # Each queued skb charges the socket its truesize (~2x the payload), so + # budget the send buffer for the whole in-flight burst. + sock = _create_sotxtime_socket(cfg, pkt_count * pkt_size * 2) + defer(sock.close) + + for delay_ms in [100, 200, 500]: + _send_sotxtime_burst(cfg, sock, port, pkt_count, + delay_ms * 1_000_000, pkt_size) + ksft_pr(f"Sent {pkt_count} SO_TXTIME packets (+{delay_ms}ms)") + time.sleep(delay_ms / 1000 + 0.3) + + backlog = _get_qdisc_backlog(cfg, mq_handle, target_queue) + if backlog: + break + else: + # A device that completes Tx synchronously (e.g. a software/virtual + # driver like netdevsim) never keeps the ring full long enough for a + # backlog to form, so the wake-vs-start behavior can't be exercised. + # Treat that as an expected failure rather than a hard failure. + raise KsftXfailEx("could not build qdisc backlog") + + ksft_pr(f"Backlog before reconfig: {backlog} bytes") + + # Trigger ring reconfig — driver should call wake, not just start. + # Grow back to the original size so the driver actually switches channels + # (setting the current size is a no-op the driver short-circuits). + cfg.eth.rings_set(ehdr | {'tx': tx_cur}) + + # Let completions proceed normally + _write_sysfs(napi_defer, 0) + _write_sysfs(gro_timeout, 0) + + # Poll for backlog to drain + for _ in range(100): + backlog = _get_qdisc_backlog(cfg, mq_handle, target_queue) + if not backlog: + break + time.sleep(0.1) + + ksft_eq(0, backlog, + comment=f"qdisc backlog stuck on queue {target_queue} " + f"after ring reconfig") + + def main() -> None: """ Ksft boiler plate main """ - with NetDrvEpEnv(__file__) as cfg: + with NetDrvEpEnv(__file__, queue_count=2) as cfg: cfg.eth = EthtoolFamily() ksft_run([channels, - ringparam], + ringparam, + reconfig_tx_stall], args=(cfg, )) ksft_exit() diff --git a/tools/testing/selftests/drivers/net/settings b/tools/testing/selftests/drivers/net/settings new file mode 100644 index 000000000000..eef533824a3c --- /dev/null +++ b/tools/testing/selftests/drivers/net/settings @@ -0,0 +1 @@ +timeout=360 diff --git a/tools/testing/selftests/drivers/net/shaper.py b/tools/testing/selftests/drivers/net/shaper.py index 11310f19bfa0..a53316726f69 100755 --- a/tools/testing/selftests/drivers/net/shaper.py +++ b/tools/testing/selftests/drivers/net/shaper.py @@ -1,11 +1,54 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0 +# pylint: disable=too-many-lines -from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_true, KsftSkipEx +import errno +import glob + +from lib.py import ksft_run, ksft_exit +from lib.py import ksft_eq, ksft_true, ksft_raises, KsftSkipEx from lib.py import EthtoolFamily, NetshaperFamily from lib.py import NetDrvEnv from lib.py import NlError -from lib.py import cmd +from lib.py import cmd, defer + +def _delete_shaper(cfg, nl_shaper, handle) -> None: + """ Delete the shaper identified by handle, ignoring a missing-shaper error. """ + try: + nl_shaper.delete({'ifindex': cfg.ifindex, + 'handle': handle}) + except NlError as e: + if e.error != errno.ENOENT: + raise + +def _require_queues(cfg, count): + """ Return the netdev TX queue count, skipping the test if fewer than count exist. """ + qcnt = len(glob.glob(f"/sys/class/net/{cfg.ifname}/queues/tx-*")) + if qcnt < count: + raise KsftSkipEx(f"netdev has {qcnt} queues, {count} required") + return qcnt + +def _cap_get(cfg, nl_shaper, scope): + """ Return the shaper capabilities for the given scope, caching them on cfg. """ + if not hasattr(cfg, 'cap_cache'): + cfg.cap_cache = {} + if scope not in cfg.cap_cache: + cfg.cap_cache[scope] = nl_shaper.cap_get({'ifindex': cfg.ifindex, + 'scope': scope}) + + return cfg.cap_cache[scope] + +def _require_caps(cfg, nl_shaper, scope, caps, msg) -> None: + """ Skip the test unless the given scope advertises all the required caps. """ + try: + supported = _cap_get(cfg, nl_shaper, scope) + except NlError as e: + if e.error == errno.EOPNOTSUPP: + raise KsftSkipEx(f"{scope} scope shapers not supported by the device") + raise + + if not set(caps).issubset(supported): + raise KsftSkipEx(msg) def get_shapers(cfg, nl_shaper) -> None: try: @@ -41,17 +84,8 @@ def set_qshapers(cfg, nl_shaper) -> None: if not 'support-bw-max' in caps or not 'support-metric-bps' in caps: raise KsftSkipEx("device does not support queue scope shapers with bw_max and metric bps") - cfg.queues = True; - netnl = EthtoolFamily() - channels = netnl.channels_get({'header': {'dev-index': cfg.ifindex}}) - if channels['combined-count'] == 0: - cfg.rx_type = 'rx' - cfg.nr_queues = channels['rx-count'] - else: - cfg.rx_type = 'combined' - cfg.nr_queues = channels['combined-count'] - if cfg.nr_queues < 3: - raise KsftSkipEx(f"device does not support enough queues min 3 found {cfg.nr_queues}") + _require_queues(cfg, 3) + cfg.queues = True nl_shaper.set({'ifindex': cfg.ifindex, 'handle': {'scope': 'queue', 'id': 1}, @@ -134,77 +168,370 @@ def del_nshapers(cfg, nl_shaper) -> None: shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(len(shapers), 0) -def basic_groups(cfg, nl_shaper) -> None: - if not cfg.netdev: - raise KsftSkipEx("netdev shaper not supported by the device") - if cfg.nr_queues < 3: - raise KsftSkipEx(f"netdev does not have enough queues min 3 reported {cfg.nr_queues}") +def set_all_supported_attrs(cfg, nl_shaper) -> None: + """ Set every queue-scope attribute the device advertises and verify the read-back. """ + _require_queues(cfg, 1) - try: - caps = nl_shaper.cap_get({'ifindex': cfg.ifindex, - 'scope':'queue'}) - except NlError as e: - if e.error == 95: - raise KsftSkipEx("shapers not supported by the device") - raise - if not 'support-weight' in caps: - raise KsftSkipEx("device does not support queue scope shapers with weight") + _require_caps(cfg, nl_shaper, 'queue', [], + "queue scope shapers not supported by the device") + caps = _cap_get(cfg, nl_shaper, 'queue') + + attrs = {'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}} + expected = {'ifindex': cfg.ifindex, + 'parent': {'scope': 'netdev'}, + 'handle': {'scope': 'queue', 'id': 0}} + + rate_attrs = {'support-bw-min': ('bw-min', 10000, 100), + 'support-bw-max': ('bw-max', 20000, 200), + 'support-burst': ('burst', 3000, 30)} + rate_attr_supported = any(cap in caps for cap in rate_attrs) + bps_supported = 'support-metric-bps' in caps + pps_supported = 'support-metric-pps' in caps + + def add_rate_attrs(metric, value_idx) -> None: + attrs['metric'] = metric + expected['metric'] = metric + for cap, (attr, bps_value, pps_value) in rate_attrs.items(): + if cap not in caps: + continue + + value = bps_value if value_idx == 0 else pps_value + attrs[attr] = value + expected[attr] = value + + if rate_attr_supported: + if bps_supported: + add_rate_attrs('bps', 0) + elif pps_supported: + add_rate_attrs('pps', 1) + + if 'support-priority' in caps: + attrs['priority'] = 1 + expected['priority'] = 1 + if 'support-weight' in caps: + attrs['weight'] = 2 + expected['weight'] = 2 + + if len(attrs) == 2: + raise KsftSkipEx("device does not advertise any supported queue shaper attributes") + + nl_shaper.set(attrs) + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 0}) + + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}}) + ksft_eq(shaper, expected) + + if rate_attr_supported and bps_supported and pps_supported: + add_rate_attrs('pps', 1) + nl_shaper.set(attrs) + + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}}) + ksft_eq(shaper, expected) + + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': 0}) + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + +def invalid_set_preserves_state(cfg, nl_shaper) -> None: + """ Verify a rejected .set leaves the existing shaper configuration unchanged. """ + nq = _require_queues(cfg, 1) + _require_caps(cfg, nl_shaper, 'queue', + ['support-bw-max', 'support-metric-bps'], + "device does not support queue scope bw_max with bps metric") + + initial = {'ifindex': cfg.ifindex, + 'parent': {'scope': 'netdev'}, + 'handle': {'scope': 'queue', 'id': 0}, + 'metric': 'bps', + 'bw-max': 10000} + nl_shaper.set({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}, + 'metric': 'bps', + 'bw-max': 10000}) + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 0}) + + with ksft_raises(NlError): + nl_shaper.set({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': 0}, + 'metric': 'bps', + 'bw-max': 20000}) + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}}) + ksft_eq(shaper, initial) + + with ksft_raises(NlError): + nl_shaper.set({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': nq}, + 'metric': 'bps', + 'bw-max': 20000}) + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}}) + ksft_eq(shaper, initial) + + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': 0}) + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + +def mixed_parent_group_requires_parent(cfg, nl_shaper) -> None: + r"""Grouping leaves from different nodes requires an explicit parent. + + netdev netdev + / \ parent=netdev + N1 N2 group N + | | {Q0,Q1} / \ + Q0 Q1 -------> Q0 Q1 + + Without an explicit parent the group is rejected; parent=netdev + collapses the leaves into one new node. + """ + _require_queues(cfg, 2) + _require_caps(cfg, nl_shaper, 'node', + ['support-bw-max', 'support-metric-bps'], + "device does not support node scope shapers with bw_max and metric bps") + _require_caps(cfg, nl_shaper, 'queue', + ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + + n1_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 0}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': 10000}) + n1_id = n1_handle['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 0}) + + n2_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 2}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': 20000}) + n2_id = n2_handle['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 1}) + + with ksft_raises(NlError): + nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 0}, + 'weight': 3}, + {'handle': {'scope': 'queue', 'id': 1}, + 'weight': 4}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': 30000}) + + shaper_q0 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}}) + ksft_eq(shaper_q0, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': n1_id}, + 'handle': {'scope': 'queue', 'id': 0}, + 'weight': 1}) + shaper_q1 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 1}}) + ksft_eq(shaper_q1, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': n2_id}, + 'handle': {'scope': 'queue', 'id': 1}, + 'weight': 2}) node_handle = nl_shaper.group({ - 'ifindex': cfg.ifindex, - 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, - 'weight': 1}, - {'handle': {'scope': 'queue', 'id': 2}, - 'weight': 2}], - 'handle': {'scope':'netdev'}, - 'metric': 'bps', - 'bw-max': 10000}) + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 0}, + 'weight': 3}, + {'handle': {'scope': 'queue', 'id': 1}, + 'weight': 4}], + 'handle': {'scope':'node'}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': 30000}) + node_id = node_handle['handle']['id'] + + for old_id in (n1_id, n2_id): + with ksft_raises(NlError): + nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': old_id}}) + + shaper_q0 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}}) + ksft_eq(shaper_q0, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node_id}, + 'handle': {'scope': 'queue', 'id': 0}, + 'weight': 3}) + shaper_q1 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 1}}) + ksft_eq(shaper_q1, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node_id}, + 'handle': {'scope': 'queue', 'id': 1}, + 'weight': 4}) + + for i in range(2): + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': i}) + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + +def recursive_empty_node_cleanup(cfg, nl_shaper) -> None: + r"""Deleting the last leaf recursively removes the emptied ancestors. + + netdev netdev + | del Q0 + N1 ------> (N1 and N2 removed too) + | + N2 + | + Q0 + """ + _require_queues(cfg, 1) + _require_caps(cfg, nl_shaper, 'node', + ['support-bw-max', 'support-metric-bps', 'support-nesting'], + "device does not support nested node scope shapers") + _require_caps(cfg, nl_shaper, 'queue', + ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + + n1_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 0}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': 10000}) + n1_id = n1_handle['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 0}) + + n2_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 0}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'parent': {'scope': 'node', 'id': n1_id}, + 'metric': 'bps', + 'bw-max': 5000}) + n2_id = n2_handle['handle']['id'] + + shaper_q0 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}}) + ksft_eq(shaper_q0, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': n2_id}, + 'handle': {'scope': 'queue', 'id': 0}, + 'weight': 1}) + + nl_shaper.delete({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}}) + + for handle in ({'scope': 'queue', 'id': 0}, + {'scope': 'node', 'id': n2_id}, + {'scope': 'node', 'id': n1_id}): + with ksft_raises(NlError): + nl_shaper.get({'ifindex': cfg.ifindex, 'handle': handle}) + + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + +def _group_under_netdev(cfg, nl_shaper, bw_max=None): + r"""Group queues under a netdev-scope node; caller owns node teardown. + + netdev netdev + / \ del Q1,Q2 + Q1 Q2 -------> (netdev node persists) + """ + group_args = { + 'ifindex': cfg.ifindex, + 'leaves': [{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}, + {'handle': {'scope': 'queue', 'id': 2}, + 'weight': 2}], + 'handle': {'scope': 'netdev'}} + if bw_max: + group_args['metric'] = 'bps' + group_args['bw-max'] = bw_max + + node_handle = nl_shaper.group(group_args) ksft_eq(node_handle, {'ifindex': cfg.ifindex, 'handle': {'scope': 'netdev'}}) + del_node = defer(_delete_shaper, cfg, nl_shaper, {'scope': 'netdev'}) + del_queues = [defer(_delete_shaper, cfg, nl_shaper, + {'scope': 'queue', 'id': qid}) + for qid in (1, 2)] + shaper = nl_shaper.get({'ifindex': cfg.ifindex, 'handle': {'scope': 'queue', 'id': 1}}) ksft_eq(shaper, {'ifindex': cfg.ifindex, 'parent': {'scope': 'netdev'}, 'handle': {'scope': 'queue', 'id': 1}, - 'weight': 1 }) + 'weight': 1}) + for dq in del_queues: + dq.exec() - nl_shaper.delete({'ifindex': cfg.ifindex, - 'handle': {'scope': 'queue', 'id': 2}}) - nl_shaper.delete({'ifindex': cfg.ifindex, - 'handle': {'scope': 'queue', 'id': 1}}) + # Caller owns the node teardown so it can verify the netdev-scope node + # survives leaf deletion before removing it. + return del_node + +def basic_groups(cfg, nl_shaper) -> None: + r"""Group queues under a netdev-scope node, then tear it down. + + netdev + / \ + Q1 Q2 + """ + _require_queues(cfg, 3) + + _require_caps(cfg, nl_shaper, 'netdev', [], "netdev scope not supported by the device") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "queue scope not supported with nesting and weight") + + del_node = _group_under_netdev(cfg, nl_shaper) + + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(shapers, [{'ifindex': cfg.ifindex, + 'handle': {'scope': 'netdev'}}]) + + del_node.exec() + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + +def basic_groups_with_rate(cfg, nl_shaper) -> None: + r"""Rate-limited netdev-scope node outlives deletion of its leaves. + + netdev[10kbps] netdev[10kbps] + / \ del Q1,Q2 + Q1 Q2 -------> (node persists) + """ + bw_max = 10000 + + _require_queues(cfg, 3) + + _require_caps(cfg, nl_shaper, 'netdev', ['support-bw-max', 'support-metric-bps'], + "device does not support netdev scope rate limiting") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support queue scope shapers with nesting and weight") + + del_node = _group_under_netdev(cfg, nl_shaper, bw_max=bw_max) # Deleting all the leaves shaper does not affect the node one # when the latter has 'netdev' scope. shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) - ksft_eq(len(shapers), 1) + ksft_eq(shapers, [{'ifindex': cfg.ifindex, + 'handle': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': bw_max}]) - nl_shaper.delete({'ifindex': cfg.ifindex, - 'handle': {'scope': 'netdev'}}) + del_node.exec() def qgroups(cfg, nl_shaper) -> None: - if cfg.nr_queues < 4: - raise KsftSkipEx(f"netdev does not have enough queues min 4 reported {cfg.nr_queues}") - try: - caps = nl_shaper.cap_get({'ifindex': cfg.ifindex, - 'scope':'node'}) - except NlError as e: - if e.error == 95: - raise KsftSkipEx("shapers not supported by the device") - raise - if not 'support-bw-max' in caps or not 'support-metric-bps' in caps: - raise KsftSkipEx("device does not support node scope shapers with bw_max and metric bps") - try: - caps = nl_shaper.cap_get({'ifindex': cfg.ifindex, - 'scope':'queue'}) - except NlError as e: - if e.error == 95: - raise KsftSkipEx("shapers not supported by the device") - raise - if not 'support-nesting' in caps or not 'support-weight' in caps or not 'support-metric-bps' in caps: - raise KsftSkipEx("device does not support nested queue scope shapers with weight") + _require_queues(cfg, 4) + _require_caps(cfg, nl_shaper, 'node', + ['support-bw-max', 'support-metric-bps'], + "device does not support node scope shapers with bw_max and metric bps") + _require_caps(cfg, nl_shaper, 'queue', + ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") - cfg.groups = True; node_handle = nl_shaper.group({ 'ifindex': cfg.ifindex, 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, @@ -281,18 +608,116 @@ def qgroups(cfg, nl_shaper) -> None: shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(len(shapers), 0) +def set_node_shaper(cfg, nl_shaper) -> None: + """ Verify a node-scope shaper rate can be updated via .set. """ + _require_queues(cfg, 2) + _require_caps(cfg, nl_shaper, 'node', ['support-bw-max', 'support-metric-bps'], + "device does not support node scope shapers with bw_max and metric bps") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + + node_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': 10000}) + node_id = node_handle['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 1}) + + # Update the node's rate via .set + nl_shaper.set({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}, + 'metric': 'bps', + 'bw-max': 20000}) + + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': 20000}) + + # Cleanup + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': 1}) + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + +def group_update_rate(cfg, nl_shaper) -> None: + """ Verify re-grouping a node updates its rate while leaving the leaves untouched. """ + _require_queues(cfg, 3) + _require_caps(cfg, nl_shaper, 'node', ['support-bw-max', 'support-metric-bps'], + "device does not support node scope shapers with bw_max and metric bps") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + + # Create node with Q1, Q2 at bw_max=10000 + node_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}, + {'handle': {'scope': 'queue', 'id': 2}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': 10000}) + node_id = node_handle['handle']['id'] + for i in range(1, 3): + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': i}) + + # Update rate via .group on the same node + nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}, + {'handle': {'scope': 'queue', 'id': 2}, + 'weight': 1}], + 'handle': {'scope':'node', 'id': node_id}, + 'metric': 'bps', + 'bw-max': 50000}) + + # Verify rate updated + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': 50000}) + + # Verify leaves unchanged + shaper_q1 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 1}}) + ksft_eq(shaper_q1, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node_id}, + 'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}) + shaper_q2 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 2}}) + ksft_eq(shaper_q2, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node_id}, + 'handle': {'scope': 'queue', 'id': 2}, + 'weight': 1}) + + # Make sure we only have 3 shapers including 2 queues and the node + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 3) + + # Cleanup + for i in range(1, 3): + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': i}) + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + def delegation(cfg, nl_shaper) -> None: - if not cfg.groups: - raise KsftSkipEx("device does not support node scope") - try: - caps = nl_shaper.cap_get({'ifindex': cfg.ifindex, - 'scope':'node'}) - except NlError as e: - if e.error == 95: - raise KsftSkipEx("node scope shapers not supported by the device") - raise - if not 'support-nesting' in caps: - raise KsftSkipEx("device does not support node scope shapers nesting") + _require_queues(cfg, 4) + _require_caps(cfg, nl_shaper, 'node', + ['support-bw-max', 'support-metric-bps', 'support-nesting'], + "device does not support node scope shapers with bw_max, metric bps and nesting") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") node_handle = nl_shaper.group({ 'ifindex': cfg.ifindex, @@ -372,20 +797,466 @@ def delegation(cfg, nl_shaper) -> None: shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(len(shapers), 0) +def nested_depth_limit(cfg, nl_shaper) -> None: + r"""Nest nodes as deep as the device allows to find the max depth. + + netdev + | + N1 -- Q1 + | + N2 -- Q2 + | + N3 -- Q3 + : (deepen until the driver rejects) + """ + bw_max = 10000 + + _require_caps(cfg, nl_shaper, 'node', + ['support-bw-max', 'support-metric-bps', 'support-nesting'], + "device does not support node scope shapers with bw_max, metric bps and nesting") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + + nq = _require_queues(cfg, 3) + + node_ids = [] + cleanups = [] + queue_id = 1 + max_depth = 0 + limit_err = None + + # Create initial node with a queue leaf + node_id = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves': [{'handle': {'scope': 'queue', 'id': queue_id}, + 'weight': 1}], + 'handle': {'scope': 'node'}, + 'metric': 'bps', + 'bw-max': bw_max})['handle']['id'] + node_ids.append(node_id) + cleanups.append(defer(_delete_shaper, cfg, nl_shaper, + {'scope': 'node', 'id': node_id})) + cleanups.append(defer(_delete_shaper, cfg, nl_shaper, + {'scope': 'queue', 'id': queue_id})) + max_depth = 1 + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': bw_max}) + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': queue_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node_id}, + 'handle': {'scope': 'queue', 'id': queue_id}, + 'weight': 1}) + queue_id += 1 + + # Keep nesting deeper until the driver rejects or queues run out. + while queue_id < nq: + parent_id = node_ids[-1] + try: + node_id = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves': [{'handle': {'scope': 'queue', + 'id': queue_id}, + 'weight': 1}], + 'handle': {'scope': 'node'}, + 'parent': {'scope': 'node', + 'id': parent_id}, + 'metric': 'bps', + 'bw-max': bw_max})['handle']['id'] + except NlError as e: + # Only treat "cannot nest deeper" errors as the depth limit; + # drivers report it differently (EOPNOTSUPP/ENOSPC/E2BIG/EINVAL). + # Anything else (ENOMEM, EIO, EPERM, driver bug) is a real failure. + if e.error not in (errno.EOPNOTSUPP, errno.ENOSPC, + errno.E2BIG, errno.EINVAL): + raise + limit_err = e + break + + node_ids.append(node_id) + cleanups.append(defer(_delete_shaper, cfg, nl_shaper, + {'scope': 'node', 'id': node_id})) + cleanups.append(defer(_delete_shaper, cfg, nl_shaper, + {'scope': 'queue', 'id': queue_id})) + max_depth += 1 + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}, + 'parent': {'scope': 'node', 'id': parent_id}, + 'metric': 'bps', + 'bw-max': bw_max}) + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', + 'id': queue_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node_id}, + 'handle': {'scope': 'queue', 'id': queue_id}, + 'weight': 1}) + queue_id += 1 + + if limit_err: + print(f"# max nesting depth supported: {max_depth} (errno {limit_err.error})") + else: + print(f"# max nesting depth tested: {max_depth}") + ksft_true(max_depth >= 2, + f"max nesting depth: {max_depth}") + + # Cleanup: exec the deferred deletes in reverse creation order, so each + # queue leaf and deeper node is removed before its parent node. + for cleanup in reversed(cleanups): + cleanup.exec() + ksft_eq(len(nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)), 0) + +def delete_child_reparent(cfg, nl_shaper) -> None: + r"""Deleting a child node reparents its queue leaf to the parent. + + netdev netdev + | | + N1 del N2 N1 + / | \ -----> / | \ + Q1 Q2 N2 Q1 Q2 Q3 + | + Q3 + """ + n1_bw_max = 10000 + n2_bw_max = 5000 + + _require_caps(cfg, nl_shaper, 'node', + ['support-bw-max', 'support-metric-bps', 'support-nesting'], + "device does not support node scope shapers with bw_max, metric bps and nesting") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + + _require_queues(cfg, 4) + + # Create parent node N1 with Q1, Q2 + n1_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}, + {'handle': {'scope': 'queue', 'id': 2}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': n1_bw_max}) + n1_id = n1_handle['handle']['id'] + for i in range(1, 3): + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': i}) + + # Create child node N2 under N1 with Q3 + n2_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 3}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'parent': {'scope': 'node', 'id': n1_id}, + 'metric': 'bps', + 'bw-max': n2_bw_max}) + n2_id = n2_handle['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 3}) + + # Delete child N2 - Q3 should reparent to N1 + nl_shaper.delete({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': n2_id}}) + + with ksft_raises(NlError): + nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': n2_id}}) + + shaper_n1 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': n1_id}}) + ksft_eq(shaper_n1, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': n1_id}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': n1_bw_max}) + shaper_q3 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 3}}) + ksft_eq(shaper_q3, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': n1_id}, + 'handle': {'scope': 'queue', 'id': 3}, + 'weight': 1}) + + # Cleanup + for i in range(1, 4): + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': i}) + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + +def move_queue_between_nodes(cfg, nl_shaper) -> None: + r"""Move a queue between nodes by re-grouping the destination node. + + netdev netdev + / \ .group N2 / \ + N1 N2 {Q1,Q3} N1 N2 + / \ | -------> | / \ + Q1 Q2 Q3 Q2 Q1 Q3 + """ + n1_bw_max = 10000 + n2_bw_max = 20000 + + _require_caps(cfg, nl_shaper, 'node', + ['support-bw-max', 'support-metric-bps', 'support-nesting'], + "device does not support node scope shapers with bw_max, metric bps and nesting") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + + _require_queues(cfg, 4) + + # Create N1 with Q1, Q2 + n1_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}, + {'handle': {'scope': 'queue', 'id': 2}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': n1_bw_max}) + n1_id = n1_handle['handle']['id'] + for i in range(1, 3): + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': i}) + + # Create N2 with Q3 + n2_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 3}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': n2_bw_max}) + n2_id = n2_handle['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 3}) + + # Move Q1 from N1 to N2 by re-grouping N2 with Q1, Q3 + nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 2}, + {'handle': {'scope': 'queue', 'id': 3}, + 'weight': 1}], + 'handle': {'scope':'node', 'id': n2_id}, + 'metric': 'bps', + 'bw-max': n2_bw_max}) + + shaper_n1 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': n1_id}}) + ksft_eq(shaper_n1, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': n1_id}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': n1_bw_max}) + shaper_n2 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': n2_id}}) + ksft_eq(shaper_n2, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': n2_id}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': n2_bw_max}) + + # Verify Q1 moved to N2 + shaper_q1 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 1}}) + ksft_eq(shaper_q1, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': n2_id}, + 'handle': {'scope': 'queue', 'id': 1}, + 'weight': 2}) + + # Verify Q2 still under N1 + shaper_q2 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 2}}) + ksft_eq(shaper_q2, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': n1_id}, + 'handle': {'scope': 'queue', 'id': 2}, + 'weight': 1}) + + # Verify Q3 remained under N2 + shaper_q3 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 3}}) + ksft_eq(shaper_q3, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': n2_id}, + 'handle': {'scope': 'queue', 'id': 3}, + 'weight': 1}) + + # Cleanup + for i in range(1, 4): + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': i}) + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + +def reject_reparenting(cfg, nl_shaper) -> None: + r"""Reject reparenting an existing node; the hierarchy stays intact. + + netdev + / \ rejected: N3 -> netdev + N1 N2 rejected: N1 -> N2 + / \ | (both EOPNOTSUPP) + Q1 N3 Q2 + | + Q3 + """ + node1_bw_max = 10000 + node2_bw_max = 5000 + node3_bw_max = 20000 + + _require_caps(cfg, nl_shaper, 'node', + ['support-bw-max', 'support-metric-bps', 'support-nesting'], + "device does not support node scope shapers with bw_max, metric bps and nesting") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + + _require_queues(cfg, 4) + + # Create Node1 under netdev with Q1. + node1_id = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': node1_bw_max})['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 1}) + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'node', 'id': node1_id}) + + # Create Node2 under netdev with Q2. + node2_id = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 2}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': node2_bw_max})['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 2}) + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'node', 'id': node2_id}) + + # Create Node3 nested under Node1 with Q3. + node3_id = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 3}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': node3_bw_max, + 'parent': {'scope': 'node', 'id': node1_id}})['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 3}) + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'node', 'id': node3_id}) + + # Reparenting a nested node up to netdev must fail. + with ksft_raises(NlError) as cm: + nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 3}, + 'weight': 1}], + 'handle': {'scope':'node', 'id': node3_id}, + 'parent': {'scope': 'netdev'}}) + if cm.exception: + ksft_eq(cm.exception.error, errno.EOPNOTSUPP) + + # Reparenting a node under another node must fail as well. + with ksft_raises(NlError) as cm: + nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}], + 'handle': {'scope':'node', 'id': node1_id}, + 'parent': {'scope': 'node', 'id': node2_id}}) + if cm.exception: + ksft_eq(cm.exception.error, errno.EOPNOTSUPP) + + # Updating a node with the same parent must succeed. + nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 5}], + 'handle': {'scope':'node', 'id': node1_id}, + 'parent': {'scope': 'netdev'}}) + + # Updating a node without specifying the parent must succeed. + nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 2}, + 'weight': 7}], + 'handle': {'scope':'node', 'id': node2_id}}) + + # The rejected reparents must have left the hierarchy intact. + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node1_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node1_id}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': node1_bw_max}) + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node2_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node2_id}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': node2_bw_max}) + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node3_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node3_id}, + 'parent': {'scope': 'node', 'id': node1_id}, + 'metric': 'bps', + 'bw-max': node3_bw_max}) + + # Verify the leaf weights were updated and parents unchanged. + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 1}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node1_id}, + 'handle': {'scope': 'queue', 'id': 1}, + 'weight': 5}) + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 2}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node2_id}, + 'handle': {'scope': 'queue', 'id': 2}, + 'weight': 7}) + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 3}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node3_id}, + 'handle': {'scope': 'queue', 'id': 3}, + 'weight': 1}) + + # Cleanup. Delete the nodes explicitly instead of relying on the + # empty-node auto-delete: a kernel that wrongly accepts a reparent may + # mishandle the leaf accounting and leave a node behind. Removing them + # by handle keeps a failing run from leaking state into later tests. + for i in range(1, 4): + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': i}) + for nid in (node1_id, node2_id, node3_id): + _delete_shaper(cfg, nl_shaper, {'scope': 'node', 'id': nid}) + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + def queue_update(cfg, nl_shaper) -> None: - if cfg.nr_queues < 4: - raise KsftSkipEx(f"netdev does not have enough queues min 4 reported {cfg.nr_queues}") + nq = _require_queues(cfg, 4) if not cfg.queues: raise KsftSkipEx("device does not support queue scope") + netnl = EthtoolFamily() + channels = netnl.channels_get({'header': {'dev-index': cfg.ifindex}}) + ch_type = 'combined' if channels['combined-count'] else 'tx' + for i in range(3): nl_shaper.set({'ifindex': cfg.ifindex, 'handle': {'scope': 'queue', 'id': i}, 'metric': 'bps', 'bw-max': (i + 1) * 1000}) + defer(cmd, f"ethtool -L {cfg.dev['ifname']} {ch_type} {nq}") + # Delete a channel, with no shapers configured on top of the related # queue: no changes expected - cmd(f"ethtool -L {cfg.dev['ifname']} {cfg.rx_type} 3", timeout=10) + cmd(f"ethtool -L {cfg.dev['ifname']} {ch_type} 3") shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(shapers, [{'ifindex': cfg.ifindex, 'parent': {'scope': 'netdev'}, @@ -405,7 +1276,7 @@ def queue_update(cfg, nl_shaper) -> None: # Delete a channel, with a shaper configured on top of the related # queue: the shaper must be deleted, too - cmd(f"ethtool -L {cfg.dev['ifname']} {cfg.rx_type} 2", timeout=10) + cmd(f"ethtool -L {cfg.dev['ifname']} {ch_type} 2") shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(shapers, [{'ifindex': cfg.ifindex, @@ -420,7 +1291,7 @@ def queue_update(cfg, nl_shaper) -> None: 'bw-max': 2000}]) # Restore the original channels number, no expected changes - cmd(f"ethtool -L {cfg.dev['ifname']} {cfg.rx_type} {cfg.nr_queues}", timeout=10) + cmd(f"ethtool -L {cfg.dev['ifname']} {ch_type} {nq}") shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(shapers, [{'ifindex': cfg.ifindex, 'parent': {'scope': 'netdev'}, @@ -438,22 +1309,62 @@ def queue_update(cfg, nl_shaper) -> None: nl_shaper.delete({'ifindex': cfg.ifindex, 'handle': {'scope': 'queue', 'id': i}}) +def dup_leaves(cfg, nl_shaper) -> None: + """ Ensure that the kernel rejects duplicate leaves. """ + _require_caps(cfg, nl_shaper, 'node', ['support-bw-max', 'support-metric-bps'], + "device does not support node scope shapers with bw_max and metric bps") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + + node_handle = None + with ksft_raises(NlError) as cm: + node_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 0}, + 'weight': 1}, + {'handle': {'scope': 'queue', 'id': 0}, + 'weight': 2}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': 10000}) + + # Clean up in case the kernel wrongly accepted the request. + if node_handle: + _delete_shaper(cfg, nl_shaper, node_handle['handle']) + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': 0}) + + # ksft_raises() has already recorded the failure if nothing was raised. + if cm.exception is None: + return + ksft_eq(cm.exception.error, errno.EINVAL) + def main() -> None: with NetDrvEnv(__file__, queue_count=4) as cfg: cfg.queues = False cfg.netdev = False - cfg.groups = False - cfg.nr_queues = 0 ksft_run([get_shapers, get_caps, set_qshapers, del_qshapers, set_nshapers, del_nshapers, + set_all_supported_attrs, + invalid_set_preserves_state, + mixed_parent_group_requires_parent, + recursive_empty_node_cleanup, basic_groups, + basic_groups_with_rate, qgroups, + set_node_shaper, + group_update_rate, delegation, - queue_update], args=(cfg, NetshaperFamily())) + nested_depth_limit, + delete_child_reparent, + move_queue_between_nodes, + reject_reparenting, + dup_leaves, + queue_update], + args=(cfg, NetshaperFamily())) ksft_exit() diff --git a/tools/testing/selftests/drivers/net/so_txtime.c b/tools/testing/selftests/drivers/net/so_txtime.c new file mode 100644 index 000000000000..55a386f3d1b9 --- /dev/null +++ b/tools/testing/selftests/drivers/net/so_txtime.c @@ -0,0 +1,538 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Test the SO_TXTIME API + * + * Takes a stream of { payload, delivery time }[], to be sent across two + * processes. Start this program on two separate network namespaces or + * connected hosts, one instance in transmit mode and the other in receive + * mode using the '-r' option. Receiver will compare arrival timestamps to + * the expected stream. Sender will read transmit timestamps from the error + * queue. The streams can differ due to out-of-order delivery and drops. + */ + +#define _GNU_SOURCE + +#include <arpa/inet.h> +#include <error.h> +#include <errno.h> +#include <inttypes.h> +#include <linux/net_tstamp.h> +#include <linux/errqueue.h> +#include <linux/if_ether.h> +#include <linux/ipv6.h> +#include <linux/udp.h> +#include <stdbool.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <sys/types.h> +#include <time.h> +#include <unistd.h> +#include <poll.h> + +#include "kselftest.h" + +static int cfg_clockid = CLOCK_TAI; +static uint16_t cfg_port = 8000; +static int cfg_variance_us = 8000; +static bool cfg_machine_slow; +static uint64_t cfg_start_time_ns; +static int cfg_mark; +static bool cfg_rx; + +static uint64_t glob_tstart; +static uint64_t tdeliver_max; + +static int errors; + +/* encode one timed transmission (of a 1B payload) */ +struct timed_send { + char data; + int64_t delay_us; +}; + +#define MAX_NUM_PKT 8 +static struct timed_send cfg_buf[MAX_NUM_PKT]; +static int cfg_num_pkt; + +static int cfg_errq_level; +static int cfg_errq_type; + +static struct sockaddr_storage cfg_dst_addr; +static struct sockaddr_storage cfg_src_addr; +static socklen_t cfg_alen; + +static uint64_t gettime_ns(clockid_t clock) +{ + struct timespec ts; + + if (clock_gettime(clock, &ts)) + error(1, errno, "gettime"); + + return ts.tv_sec * (1000ULL * 1000 * 1000) + ts.tv_nsec; +} + +static void do_send_one(int fdt, struct timed_send *ts) +{ + char control[CMSG_SPACE(sizeof(uint64_t))]; + struct msghdr msg = {0}; + struct iovec iov = {0}; + struct cmsghdr *cm; + uint64_t tdeliver; + int ret; + + iov.iov_base = &ts->data; + iov.iov_len = 1; + + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_name = (struct sockaddr *)&cfg_dst_addr; + msg.msg_namelen = cfg_alen; + + if (ts->delay_us >= 0) { + memset(control, 0, sizeof(control)); + msg.msg_control = &control; + msg.msg_controllen = sizeof(control); + + tdeliver = glob_tstart + ts->delay_us * 1000; + tdeliver_max = tdeliver_max > tdeliver ? + tdeliver_max : tdeliver; + + cm = CMSG_FIRSTHDR(&msg); + cm->cmsg_level = SOL_SOCKET; + cm->cmsg_type = SCM_TXTIME; + cm->cmsg_len = CMSG_LEN(sizeof(tdeliver)); + memcpy(CMSG_DATA(cm), &tdeliver, sizeof(tdeliver)); + } + + ret = sendmsg(fdt, &msg, 0); + if (ret == -1) + error(1, errno, "write"); + if (ret == 0) + error(1, 0, "write: 0B"); + +} + +static void do_recv_one(int fdr, struct timed_send *ts) +{ + int64_t tstop, texpect; + char rbuf[2]; + int ret; + + ret = recv(fdr, rbuf, sizeof(rbuf), 0); + if (ret == -1 && errno == EAGAIN) + error(1, EAGAIN, "recv: timeout"); + if (ret == -1) + error(1, errno, "read"); + if (ret != 1) + error(1, 0, "read: %dB", ret); + + tstop = (gettime_ns(cfg_clockid) - glob_tstart) / 1000; + texpect = ts->delay_us >= 0 ? ts->delay_us : 0; + + fprintf(stderr, "payload:%c delay:%lld expected:%lld (us)\n", + rbuf[0], (long long)tstop, (long long)texpect); + + if (rbuf[0] != ts->data) { + fprintf(stderr, "payload mismatch. expected %c\n", ts->data); + errors++; + } + + if (llabs(tstop - texpect) > cfg_variance_us) { + fprintf(stderr, "exceeds variance (%d us)\n", cfg_variance_us); + if (!cfg_machine_slow) + errors++; + } +} + +static void do_recv_verify_empty(int fdr) +{ + char rbuf[1]; + int ret; + + ret = recv(fdr, rbuf, sizeof(rbuf), 0); + if (ret != -1 || errno != EAGAIN) + error(1, 0, "recv: not empty as expected (%d, %d)", ret, errno); +} + +static int do_recv_errqueue_timeout(int fdt) +{ + char control[CMSG_SPACE(sizeof(struct sock_extended_err)) + + CMSG_SPACE(sizeof(struct sockaddr_in6))] = {0}; + char data[sizeof(struct ethhdr) + sizeof(struct ipv6hdr) + + sizeof(struct udphdr) + 1]; + struct sock_extended_err *err; + int ret, num_tstamp = 0; + struct msghdr msg = {0}; + struct iovec iov = {0}; + struct cmsghdr *cm; + int64_t tstamp = 0; + + iov.iov_base = data; + iov.iov_len = sizeof(data); + + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + msg.msg_control = control; + msg.msg_controllen = sizeof(control); + + while (1) { + const char *reason = NULL; + + ret = recvmsg(fdt, &msg, MSG_ERRQUEUE); + if (ret == -1 && errno == EAGAIN) + break; + if (ret == -1) + error(1, errno, "errqueue"); + if (msg.msg_flags != MSG_ERRQUEUE) + error(1, 0, "errqueue: flags 0x%x\n", msg.msg_flags); + + cm = CMSG_FIRSTHDR(&msg); + if (cm->cmsg_level != cfg_errq_level || + cm->cmsg_type != cfg_errq_type) + error(1, 0, "errqueue: type 0x%x.0x%x\n", + cm->cmsg_level, cm->cmsg_type); + + err = (struct sock_extended_err *)CMSG_DATA(cm); + if (err->ee_origin != SO_EE_ORIGIN_TXTIME) + error(1, 0, "errqueue: origin 0x%x\n", err->ee_origin); + + switch (err->ee_errno) { + case ECANCELED: + if (err->ee_code != SO_EE_CODE_TXTIME_MISSED) + error(1, 0, "errqueue: unknown ECANCELED %u\n", + err->ee_code); + reason = "missed txtime"; + break; + case EINVAL: + if (err->ee_code != SO_EE_CODE_TXTIME_INVALID_PARAM) + error(1, 0, "errqueue: unknown EINVAL %u\n", + err->ee_code); + reason = "invalid txtime"; + break; + default: + error(1, 0, "errqueue: errno %u code %u\n", + err->ee_errno, err->ee_code); + } + + tstamp = ((int64_t) err->ee_data) << 32 | err->ee_info; + tstamp -= (int64_t) glob_tstart; + tstamp /= 1000 * 1000; + fprintf(stderr, "send: pkt %c at %" PRId64 "ms dropped: %s\n", + data[ret - 1], tstamp, reason); + + msg.msg_flags = 0; + msg.msg_controllen = sizeof(control); + num_tstamp++; + } + + return num_tstamp; +} + +static void recv_errqueue_msgs(int fdt) +{ + struct pollfd pfd = { .fd = fdt, .events = POLLERR }; + const int timeout_ms = 10; + int ret, num_tstamp = 0; + + do { + ret = poll(&pfd, 1, timeout_ms); + if (ret == -1) + error(1, errno, "poll"); + + if (ret && (pfd.revents & POLLERR)) + num_tstamp += do_recv_errqueue_timeout(fdt); + + if (num_tstamp == cfg_num_pkt) + break; + + } while (gettime_ns(cfg_clockid) < tdeliver_max); +} + +static void start_time_wait(void) +{ + uint64_t now; + int err; + + if (!cfg_start_time_ns) + return; + + now = gettime_ns(CLOCK_REALTIME); + if (cfg_start_time_ns < now) { + fprintf(stderr, "FAIL: start time already passed\n"); + if (!cfg_machine_slow) + errors++; + return; + } + + err = usleep((cfg_start_time_ns - now) / 1000); + if (err) + error(1, errno, "usleep"); +} + +static void setsockopt_txtime(int fd) +{ + struct sock_txtime so_txtime_val = { .clockid = cfg_clockid }; + struct sock_txtime so_txtime_val_read = { 0 }; + socklen_t vallen = sizeof(so_txtime_val); + + so_txtime_val.flags = SOF_TXTIME_REPORT_ERRORS; + + if (setsockopt(fd, SOL_SOCKET, SO_TXTIME, + &so_txtime_val, sizeof(so_txtime_val))) + error(1, errno, "setsockopt txtime"); + + if (getsockopt(fd, SOL_SOCKET, SO_TXTIME, + &so_txtime_val_read, &vallen)) + error(1, errno, "getsockopt txtime"); + + if (vallen != sizeof(so_txtime_val) || + memcmp(&so_txtime_val, &so_txtime_val_read, vallen)) + error(1, 0, "getsockopt txtime: mismatch"); +} + +static int setup_tx(struct sockaddr *addr, socklen_t alen) +{ + int fd; + + fd = socket(addr->sa_family, SOCK_DGRAM, 0); + if (fd == -1) + error(1, errno, "socket t"); + + if (connect(fd, addr, alen)) + error(1, errno, "connect"); + + setsockopt_txtime(fd); + + if (cfg_mark && + setsockopt(fd, SOL_SOCKET, SO_MARK, &cfg_mark, sizeof(cfg_mark))) + error(1, errno, "setsockopt mark"); + + return fd; +} + +static int setup_rx(struct sockaddr *addr, socklen_t alen) +{ + struct timeval tv = { .tv_usec = 100 * 1000 }; + int fd; + + fd = socket(addr->sa_family, SOCK_DGRAM, 0); + if (fd == -1) + error(1, errno, "socket r"); + + if (bind(fd, addr, alen)) + error(1, errno, "bind"); + + if (cfg_machine_slow) + tv.tv_sec = 2; + + if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv))) + error(1, errno, "setsockopt rcv timeout"); + + return fd; +} + +static void do_test_tx(struct sockaddr *addr, socklen_t alen) +{ + int fdt, i; + + fprintf(stderr, "\nSO_TXTIME ipv%c clock %s\n", + addr->sa_family == PF_INET ? '4' : '6', + cfg_clockid == CLOCK_TAI ? "tai" : "monotonic"); + + fdt = setup_tx(addr, alen); + + start_time_wait(); + glob_tstart = gettime_ns(cfg_clockid); + + for (i = 0; i < cfg_num_pkt; i++) + do_send_one(fdt, &cfg_buf[i]); + + recv_errqueue_msgs(fdt); + + if (close(fdt)) + error(1, errno, "close t"); +} + +static void do_test_rx(struct sockaddr *addr, socklen_t alen) +{ + int fdr, i; + + fdr = setup_rx(addr, alen); + + start_time_wait(); + glob_tstart = gettime_ns(cfg_clockid); + + for (i = 0; i < cfg_num_pkt; i++) + do_recv_one(fdr, &cfg_buf[i]); + + do_recv_verify_empty(fdr); + + if (close(fdr)) + error(1, errno, "close r"); +} + +static void setup_sockaddr(int domain, const char *str_addr, + struct sockaddr_storage *sockaddr) +{ + struct sockaddr_in6 *addr6 = (void *) sockaddr; + struct sockaddr_in *addr4 = (void *) sockaddr; + + switch (domain) { + case PF_INET: + memset(addr4, 0, sizeof(*addr4)); + addr4->sin_family = AF_INET; + addr4->sin_port = htons(cfg_port); + if (str_addr && + inet_pton(AF_INET, str_addr, &(addr4->sin_addr)) != 1) + error(1, 0, "ipv4 parse error: %s", str_addr); + break; + case PF_INET6: + memset(addr6, 0, sizeof(*addr6)); + addr6->sin6_family = AF_INET6; + addr6->sin6_port = htons(cfg_port); + if (str_addr && + inet_pton(AF_INET6, str_addr, &(addr6->sin6_addr)) != 1) + error(1, 0, "ipv6 parse error: %s", str_addr); + break; + } +} + +static int parse_io(const char *optarg, struct timed_send *array) +{ + char *arg, *tok; + int aoff = 0; + + arg = strdup(optarg); + if (!arg) + error(1, errno, "strdup"); + + while ((tok = strtok(arg, ","))) { + arg = NULL; /* only pass non-zero on first call */ + + if (aoff / 2 == MAX_NUM_PKT) + error(1, 0, "exceeds max pkt count (%d)", MAX_NUM_PKT); + + if (aoff & 1) { /* parse delay */ + array->delay_us = strtol(tok, NULL, 0) * 1000; + array++; + } else { /* parse character */ + array->data = tok[0]; + } + + aoff++; + } + + free(arg); + + return aoff / 2; +} + +static void usage(const char *progname) +{ + fprintf(stderr, "\nUsage: %s [options] <payload>\n" + "Options:\n" + " -4 only IPv4\n" + " -6 only IPv6\n" + " -c <clock> monotonic or tai (default)\n" + " -D <addr> destination IP address (server)\n" + " -S <addr> source IP address (client)\n" + " -r run rx mode\n" + " -t <nsec> start time (UTC nanoseconds)\n" + " -m <mark> socket mark\n" + "\n", + progname); + exit(1); +} + +static void parse_opts(int argc, char **argv) +{ + char *daddr = NULL, *saddr = NULL; + int domain = PF_UNSPEC; + int c; + + while ((c = getopt(argc, argv, "46c:S:D:rt:m:")) != -1) { + switch (c) { + case '4': + if (domain != PF_UNSPEC) + error(1, 0, "Pass one of -4 or -6"); + domain = PF_INET; + cfg_alen = sizeof(struct sockaddr_in); + cfg_errq_level = SOL_IP; + cfg_errq_type = IP_RECVERR; + break; + case '6': + if (domain != PF_UNSPEC) + error(1, 0, "Pass one of -4 or -6"); + domain = PF_INET6; + cfg_alen = sizeof(struct sockaddr_in6); + cfg_errq_level = SOL_IPV6; + cfg_errq_type = IPV6_RECVERR; + break; + case 'c': + if (!strcmp(optarg, "tai")) + cfg_clockid = CLOCK_TAI; + else if (!strcmp(optarg, "monotonic") || + !strcmp(optarg, "mono")) + cfg_clockid = CLOCK_MONOTONIC; + else + error(1, 0, "unknown clock id %s", optarg); + break; + case 'S': + saddr = optarg; + break; + case 'D': + daddr = optarg; + break; + case 'r': + cfg_rx = true; + break; + case 't': + cfg_start_time_ns = strtoll(optarg, NULL, 0); + break; + case 'm': + cfg_mark = strtol(optarg, NULL, 0); + break; + default: + usage(argv[0]); + } + } + + if (argc - optind != 1) + usage(argv[0]); + + if (domain == PF_UNSPEC) + error(1, 0, "Pass one of -4 or -6"); + if (!daddr) + error(1, 0, "-D <server addr> required\n"); + if (!cfg_rx && !saddr) + error(1, 0, "-S <client addr> required\n"); + + setup_sockaddr(domain, daddr, &cfg_dst_addr); + setup_sockaddr(domain, saddr, &cfg_src_addr); + + cfg_num_pkt = parse_io(argv[optind], cfg_buf); + + cfg_machine_slow = getenv("KSFT_MACHINE_SLOW"); +} + +int main(int argc, char **argv) +{ + parse_opts(argc, argv); + + if (cfg_rx) + do_test_rx((void *)&cfg_dst_addr, cfg_alen); + else + do_test_tx((void *)&cfg_src_addr, cfg_alen); + + if (errors) { + fprintf(stderr, "FAIL: %d errors\n", errors); + return KSFT_FAIL; + } + + return KSFT_PASS; +} diff --git a/tools/testing/selftests/drivers/net/so_txtime.py b/tools/testing/selftests/drivers/net/so_txtime.py new file mode 100755 index 000000000000..a097fae0b335 --- /dev/null +++ b/tools/testing/selftests/drivers/net/so_txtime.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 + +"""Regression tests for the SO_TXTIME interface. + +Test delivery time in FQ and ETF qdiscs. +""" + +import os +import time + +from lib.py import ksft_exit, ksft_run, ksft_variants +from lib.py import KsftNamedVariant, KsftSkipEx +from lib.py import NetDrvEpEnv, bkg, cmd, defer, tc +from lib.py import CmdExitFailure + + +def test_so_txtime(cfg, clockid, ipver, args_tx, args_rx, expect_success): + """Main function. Run so_txtime as sender and receiver.""" + slow_machine = os.environ.get('KSFT_MACHINE_SLOW') + + if not hasattr(cfg, "bin_remote"): + cfg.bin_local = cfg.test_dir / "so_txtime" + cfg.bin_remote = cfg.remote.deploy(cfg.bin_local) + + tstart = time.time_ns() + (2000_000_000 if slow_machine else 200_000_000) + + cmd_addr = f"-S {cfg.addr_v[ipver]} -D {cfg.remote_addr_v[ipver]}" + cmd_args = f"-{ipver} -c {clockid} -t {tstart} {cmd_addr}" + cmd_rx = f"{cfg.bin_remote} {cmd_args} {args_rx} -r" + cmd_tx = f"{cfg.bin_local} -m 100 {cmd_args} {args_tx}" + + expect_fail = not expect_success + if slow_machine: + expect_success = False + + with bkg(cmd_rx, host=cfg.remote, fail=expect_success, + expect_fail=expect_fail, exit_wait=True): + cmd(cmd_tx) + + +def _qdisc_setup(ifname, qdisc, optargs=""): + """Replace root qdisc. Restore the original after the test. + + If the original is mq, children will be of type default_qdisc. + """ + orig = tc(f"qdisc show dev {ifname} root", json=True)[0].get("kind", None) + defer(tc, f"qdisc replace dev {ifname} root {orig}") + try: + tc(f"qdisc del dev {ifname} root") + except CmdExitFailure: + pass + tc(f"qdisc replace dev {ifname} root handle 1: {qdisc} {optargs}") + + +def _test_variants_fq(): + for ipver in ["4", "6"]: + for testcase in [ + ["no_delay", "a,-1", "a,-1"], + ["zero_delay", "a,0", "a,0"], + ["one_pkt", "a,10", "a,10"], + ["in_order", "a,10,b,20", "a,10,b,20"], + ["reverse_order", "a,20,b,10", "b,10,a,20"], + ]: + name = f"v{ipver}_{testcase[0]}" + yield KsftNamedVariant(name, ipver, testcase[1], testcase[2]) + + +@ksft_variants(_test_variants_fq()) +def test_so_txtime_fq_mono(cfg, ipver, args_tx, args_rx): + """Run all variants of monotonic (fq) tests.""" + cfg.require_ipver(ipver) + _qdisc_setup(cfg.ifname, "fq") + test_so_txtime(cfg, "mono", ipver, args_tx, args_rx, True) + + +@ksft_variants(_test_variants_fq()) +def test_so_txtime_fq_tai(cfg, ipver, args_tx, args_rx): + """Run all variants of fq tests, but pass CLOCK_TAI to test conversion.""" + cfg.require_ipver(ipver) + _qdisc_setup(cfg.ifname, "fq") + test_so_txtime(cfg, "tai", ipver, args_tx, args_rx, True) + + +def _test_variants_etf(): + for ipver in ["4", "6"]: + for testcase in [ + ["no_delay", "a,-1", "a,-1", False], + ["zero_delay", "a,0", "a,0", False], + ["one_pkt", "a,10", "a,10", True], + ["in_order", "a,10,b,20", "a,10,b,20", True], + ["reverse_order", "a,20,b,10", "b,10,a,20", True], + ]: + name = f"v{ipver}_{testcase[0]}" + yield KsftNamedVariant( + name, ipver, testcase[1], testcase[2], testcase[3] + ) + + +@ksft_variants(_test_variants_etf()) +def test_so_txtime_etf(cfg, ipver, args_tx, args_rx, expect_fail): + """Run all variants of etf tests.""" + cfg.require_ipver(ipver) + + # root qdisc for background traffic (e.g., bkg()) + _qdisc_setup(cfg.ifname, "prio") + + # leaf ETF qdisc only for intended packets + try: + etf_args = "clockid CLOCK_TAI delta 400000" + tc(f"qdisc add dev {cfg.ifname} parent 1:1 handle 10: etf {etf_args}") + except Exception as e: + raise KsftSkipEx("tc does not support qdisc etf. skipping") from e + + # redirect mark 100 to leaf + filter_args = "protocol all handle 100 fw flowid 1:1" + tc(f"filter add dev {cfg.ifname} parent 1: {filter_args}") + + test_so_txtime(cfg, "tai", ipver, args_tx, args_rx, expect_fail) + + +def main() -> None: + """Boilerplate ksft main.""" + with NetDrvEpEnv(__file__) as cfg: + ksft_run( + [test_so_txtime_fq_mono, test_so_txtime_fq_tai, test_so_txtime_etf], + args=(cfg,), + ) + ksft_exit() + + +if __name__ == "__main__": + main() diff --git a/tools/testing/selftests/drivers/net/team/Makefile b/tools/testing/selftests/drivers/net/team/Makefile index 45a3e7ad3dcb..7c58cf82121e 100644 --- a/tools/testing/selftests/drivers/net/team/Makefile +++ b/tools/testing/selftests/drivers/net/team/Makefile @@ -2,13 +2,18 @@ # Makefile for net selftests TEST_PROGS := \ + decoupled_enablement.sh \ dev_addr_lists.sh \ + non_ether_header_ops.sh \ options.sh \ propagation.sh \ refleak.sh \ + teamd_activebackup.sh \ + transmit_failover.sh \ # end of TEST_PROGS TEST_INCLUDES := \ + team_lib.sh \ ../bonding/lag_lib.sh \ ../../../net/forwarding/lib.sh \ ../../../net/in_netns.sh \ diff --git a/tools/testing/selftests/drivers/net/team/config b/tools/testing/selftests/drivers/net/team/config index 558e1d0cf565..8f04ae419c53 100644 --- a/tools/testing/selftests/drivers/net/team/config +++ b/tools/testing/selftests/drivers/net/team/config @@ -1,7 +1,13 @@ +CONFIG_BONDING=y CONFIG_DUMMY=y CONFIG_IPV6=y CONFIG_MACVLAN=y CONFIG_NETDEVSIM=m +CONFIG_NET_IPGRE=y CONFIG_NET_TEAM=y CONFIG_NET_TEAM_MODE_ACTIVEBACKUP=y +CONFIG_NET_TEAM_MODE_BROADCAST=y CONFIG_NET_TEAM_MODE_LOADBALANCE=y +CONFIG_NET_TEAM_MODE_RANDOM=y +CONFIG_NET_TEAM_MODE_ROUNDROBIN=y +CONFIG_VETH=y diff --git a/tools/testing/selftests/drivers/net/team/decoupled_enablement.sh b/tools/testing/selftests/drivers/net/team/decoupled_enablement.sh new file mode 100755 index 000000000000..0d3d9c98e9f5 --- /dev/null +++ b/tools/testing/selftests/drivers/net/team/decoupled_enablement.sh @@ -0,0 +1,249 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 + +# These tests verify the decoupled RX and TX enablement of team driver member +# interfaces. +# +# Topology +# +# +---------------------+ NS1 +# | test_team1 | +# | | | +# | eth0 | +# | | | +# | | | +# +---------------------+ +# | +# +---------------------+ NS2 +# | | | +# | | | +# | eth0 | +# | | | +# | test_team2 | +# +---------------------+ + +export ALL_TESTS=" + team_test_tx_enablement + team_test_rx_enablement +" + +test_dir="$(dirname "$0")" +# shellcheck disable=SC1091 +source "${test_dir}/../../../net/lib.sh" +# shellcheck disable=SC1091 +source "${test_dir}/team_lib.sh" + +NS1="" +NS2="" +export NODAD="nodad" +PREFIX_LENGTH="64" +NS1_IP="fd00::1" +NS2_IP="fd00::2" +NS1_IP4="192.168.0.1" +NS2_IP4="192.168.0.2" +MEMBERS=("eth0") +PING_COUNT=5 +PING_TIMEOUT_S=1 +PING_INTERVAL=0.1 + +while getopts "4" opt; do + case $opt in + 4) + echo "IPv4 mode selected." + export NODAD= + PREFIX_LENGTH="24" + NS1_IP="${NS1_IP4}" + NS2_IP="${NS2_IP4}" + ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + exit 1 + ;; + esac +done + +# This has to be sourced after opts are gathered... +export REQUIRE_MZ=no +export NUM_NETIFS=0 +# shellcheck disable=SC1091 +source "${test_dir}/../../../net/forwarding/lib.sh" + +# Create the network namespaces, veth pair, and team devices in the specified +# mode. +# Globals: +# RET - Used by test infra, set by `check_err` functions. +# Arguments: +# mode - The team driver mode to use for the team devices. +environment_create() +{ + trap cleanup_all_ns EXIT + setup_ns ns1 ns2 + NS1="${NS_LIST[0]}" + NS2="${NS_LIST[1]}" + + # Create the interfaces. + ip -n "${NS1}" link add eth0 type veth peer name eth0 netns "${NS2}" + ip -n "${NS1}" link add test_team1 type team + ip -n "${NS2}" link add test_team2 type team + + # Set up the receiving network namespace's team interface. + setup_team "${NS2}" test_team2 roundrobin "${NS2_IP}" \ + "${PREFIX_LENGTH}" "${MEMBERS[@]}" +} + +# Set a particular option value for team or team port. +# Arguments: +# namespace - The namespace name that has the team. +# option_name - The option name to set. +# option_value - The value to set the option to. +# team_name - The name of team to set the option for. +# member_name - The (optional) optional name of the member port. +set_option_value() +{ + local namespace="$1" + local option_name="$2" + local option_value="$3" + local team_name="$4" + local member_name="$5" + local port_flag="--port=${member_name}" + + ip netns exec "${namespace}" teamnl "${team_name}" setoption \ + "${option_name}" "${option_value}" "${port_flag}" + return $? +} + +# Send some pings and return the ping command return value. +try_ping() +{ + ip netns exec "${NS1}" ping -i "${PING_INTERVAL}" -c "${PING_COUNT}" \ + "${NS2_IP}" -W "${PING_TIMEOUT_S}" +} + +# Checks tcpdump output from net/forwarding lib, and checks if there are any +# ICMP(4 or 6) packets. +# Arguments: +# interface - The interface name to search for. +# ip_address - The destination IP address (4 or 6) to search for. +did_interface_receive_icmp() +{ + local interface="$1" + local ip_address="$2" + local packet_count + + packet_count=$(tcpdump_show "$interface" | grep -c \ + "> ${ip_address}: ICMP") + echo "Packet count for ${interface} was ${packet_count}" + + if [[ "$packet_count" -gt 0 ]]; then + true + else + false + fi +} + +# Test JUST tx enablement with a given mode. +# Globals: +# RET - Used by test infra, set by `check_err` functions. +# Arguments: +# mode - The mode to set the team interfaces to. +team_test_mode_tx_enablement() +{ + local mode="$1" + export RET=0 + + # Set up the sender team with the correct mode. + setup_team "${NS1}" test_team1 "${mode}" "${NS1_IP}" \ + "${PREFIX_LENGTH}" "${MEMBERS[@]}" + check_err $? "Failed to set up sender team" + + ### Scenario 1: Member interface initially enabled. + # Expect ping to pass + try_ping + check_err $? "Ping failed when TX enabled" + + ### Scenario 2: One tx-side interface disabled. + # Expect ping to fail. + set_option_value "${NS1}" tx_enabled false test_team1 eth0 + check_err $? "Failed to disable TX" + tcpdump_start eth0 "${NS2}" + try_ping + check_fail $? "Ping succeeded when TX disabled" + tcpdump_stop eth0 + # Expect no packets to be transmitted, since TX is disabled. + did_interface_receive_icmp eth0 "${NS2_IP}" + check_fail $? "eth0 IS transmitting when TX disabled" + tcpdump_cleanup eth0 + + ### Scenario 3: The interface has tx re-enabled. + # Expect ping to pass. + set_option_value "${NS1}" tx_enabled true test_team1 eth0 + check_err $? "Failed to reenable TX" + try_ping + check_err $? "Ping failed when TX reenabled" + + log_test "TX failover of '${mode}' test" +} + +# Test JUST rx enablement with a given mode. +# Globals: +# RET - Used by test infra, set by `check_err` functions. +# Arguments: +# mode - The mode to set the team interfaces to. +team_test_mode_rx_enablement() +{ + local mode="$1" + export RET=0 + + # Set up the sender team with the correct mode. + setup_team "${NS1}" test_team1 "${mode}" "${NS1_IP}" \ + "${PREFIX_LENGTH}" "${MEMBERS[@]}" + check_err $? "Failed to set up sender team" + + ### Scenario 1: Member interface initially enabled. + # Expect ping to pass + try_ping + check_err $? "Ping failed when RX enabled" + + ### Scenario 2: One rx-side interface disabled. + # Expect ping to fail. + set_option_value "${NS1}" rx_enabled false test_team1 eth0 + check_err $? "Failed to disable RX" + tcpdump_start eth0 "${NS2}" + try_ping + check_fail $? "Ping succeeded when RX disabled" + tcpdump_stop eth0 + # Expect packets to be transmitted, since only RX is disabled. + did_interface_receive_icmp eth0 "${NS2_IP}" + check_err $? "eth0 not transmitting when RX disabled" + tcpdump_cleanup eth0 + + ### Scenario 3: The interface has rx re-enabled. + # Expect ping to pass. + set_option_value "${NS1}" rx_enabled true test_team1 eth0 + check_err $? "Failed to reenable RX" + try_ping + check_err $? "Ping failed when RX reenabled" + + log_test "RX failover of '${mode}' test" +} + +team_test_tx_enablement() +{ + team_test_mode_tx_enablement broadcast + team_test_mode_tx_enablement roundrobin + team_test_mode_tx_enablement random +} + +team_test_rx_enablement() +{ + team_test_mode_rx_enablement broadcast + team_test_mode_rx_enablement roundrobin + team_test_mode_rx_enablement random +} + +require_command teamnl +require_command tcpdump +require_command ping +environment_create +tests_run +exit "${EXIT_STATUS}" diff --git a/tools/testing/selftests/drivers/net/team/dev_addr_lists.sh b/tools/testing/selftests/drivers/net/team/dev_addr_lists.sh index b1ec7755b783..26469f3be022 100755 --- a/tools/testing/selftests/drivers/net/team/dev_addr_lists.sh +++ b/tools/testing/selftests/drivers/net/team/dev_addr_lists.sh @@ -42,8 +42,6 @@ team_cleanup() } -require_command teamd - trap cleanup EXIT tests_run diff --git a/tools/testing/selftests/drivers/net/team/non_ether_header_ops.sh b/tools/testing/selftests/drivers/net/team/non_ether_header_ops.sh new file mode 100755 index 000000000000..948a43576bdc --- /dev/null +++ b/tools/testing/selftests/drivers/net/team/non_ether_header_ops.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# shellcheck disable=SC2154 +# +# Reproduce the non-Ethernet header_ops confusion scenario with: +# g0 (gre) -> b0 (bond) -> t0 (team) +# +# Before the fix, direct header_ops inheritance in this stack could call +# callbacks with the wrong net_device context and crash. + +lib_dir=$(dirname "$0") +source "$lib_dir"/../../../net/lib.sh + +trap cleanup_all_ns EXIT + +setup_ns ns1 + +ip -n "$ns1" link add d0 type dummy +ip -n "$ns1" addr add 10.10.10.1/24 dev d0 +ip -n "$ns1" link set d0 up + +ip -n "$ns1" link add g0 type gre local 10.10.10.1 +ip -n "$ns1" link add b0 type bond mode active-backup +ip -n "$ns1" link add t0 type team + +ip -n "$ns1" link set g0 master b0 +ip -n "$ns1" link set b0 master t0 + +ip -n "$ns1" link set g0 up +ip -n "$ns1" link set b0 up +ip -n "$ns1" link set t0 up + +# IPv6 address assignment triggers MLD join reports that call +# dev_hard_header() on t0, exercising the inherited header_ops path. +ip -n "$ns1" -6 addr add 2001:db8:1::1/64 dev t0 nodad +for i in $(seq 1 20); do + ip netns exec "$ns1" ping -6 -I t0 ff02::1 -c1 -W1 &>/dev/null || true +done + +echo "PASS: non-Ethernet header_ops stacking did not crash" +exit "$EXIT_STATUS" diff --git a/tools/testing/selftests/drivers/net/team/options.sh b/tools/testing/selftests/drivers/net/team/options.sh index 44888f32b513..66c0cb896dad 100755 --- a/tools/testing/selftests/drivers/net/team/options.sh +++ b/tools/testing/selftests/drivers/net/team/options.sh @@ -11,10 +11,14 @@ if [[ $# -eq 0 ]]; then exit $? fi -ALL_TESTS=" +export ALL_TESTS=" team_test_options + team_test_enabled_implicit_changes + team_test_rx_enabled_implicit_changes + team_test_tx_enabled_implicit_changes " +# shellcheck disable=SC1091 source "${test_dir}/../../../net/lib.sh" TEAM_PORT="team0" @@ -176,12 +180,105 @@ team_test_options() team_test_option mcast_rejoin_count 0 5 team_test_option mcast_rejoin_interval 0 5 team_test_option enabled true false "${MEMBER_PORT}" + team_test_option rx_enabled true false "${MEMBER_PORT}" + team_test_option tx_enabled true false "${MEMBER_PORT}" team_test_option user_linkup true false "${MEMBER_PORT}" team_test_option user_linkup_enabled true false "${MEMBER_PORT}" team_test_option priority 10 20 "${MEMBER_PORT}" team_test_option queue_id 0 1 "${MEMBER_PORT}" } +team_test_enabled_implicit_changes() +{ + export RET=0 + + attach_port_if_specified "${MEMBER_PORT}" + check_err $? "Couldn't attach ${MEMBER_PORT} to master" + + # Set enabled to true. + set_and_check_get enabled true "--port=${MEMBER_PORT}" + check_err $? "Failed to set 'enabled' to true" + + # Show that both rx enabled and tx enabled are true. + get_and_check_value rx_enabled true "--port=${MEMBER_PORT}" + check_err $? "'Rx_enabled' wasn't implicitly set to true" + get_and_check_value tx_enabled true "--port=${MEMBER_PORT}" + check_err $? "'Tx_enabled' wasn't implicitly set to true" + + # Set enabled to false. + set_and_check_get enabled false "--port=${MEMBER_PORT}" + check_err $? "Failed to set 'enabled' to false" + + # Show that both rx enabled and tx enabled are false. + get_and_check_value rx_enabled false "--port=${MEMBER_PORT}" + check_err $? "'Rx_enabled' wasn't implicitly set to false" + get_and_check_value tx_enabled false "--port=${MEMBER_PORT}" + check_err $? "'Tx_enabled' wasn't implicitly set to false" + + log_test "'Enabled' implicit changes" +} + +team_test_rx_enabled_implicit_changes() +{ + export RET=0 + + attach_port_if_specified "${MEMBER_PORT}" + check_err $? "Couldn't attach ${MEMBER_PORT} to master" + + # Set enabled to true. + set_and_check_get enabled true "--port=${MEMBER_PORT}" + check_err $? "Failed to set 'enabled' to true" + + # Set rx_enabled to false. + set_and_check_get rx_enabled false "--port=${MEMBER_PORT}" + check_err $? "Failed to set 'rx_enabled' to false" + + # Show that enabled is false. + get_and_check_value enabled false "--port=${MEMBER_PORT}" + check_err $? "'enabled' wasn't implicitly set to false" + + # Set rx_enabled to true. + set_and_check_get rx_enabled true "--port=${MEMBER_PORT}" + check_err $? "Failed to set 'rx_enabled' to true" + + # Show that enabled is true. + get_and_check_value enabled true "--port=${MEMBER_PORT}" + check_err $? "'enabled' wasn't implicitly set to true" + + log_test "'Rx_enabled' implicit changes" +} + +team_test_tx_enabled_implicit_changes() +{ + export RET=0 + + attach_port_if_specified "${MEMBER_PORT}" + check_err $? "Couldn't attach ${MEMBER_PORT} to master" + + # Set enabled to true. + set_and_check_get enabled true "--port=${MEMBER_PORT}" + check_err $? "Failed to set 'enabled' to true" + + # Set tx_enabled to false. + set_and_check_get tx_enabled false "--port=${MEMBER_PORT}" + check_err $? "Failed to set 'tx_enabled' to false" + + # Show that enabled is false. + get_and_check_value enabled false "--port=${MEMBER_PORT}" + check_err $? "'enabled' wasn't implicitly set to false" + + # Set tx_enabled to true. + set_and_check_get tx_enabled true "--port=${MEMBER_PORT}" + check_err $? "Failed to set 'tx_enabled' to true" + + # Show that enabled is true. + get_and_check_value enabled true "--port=${MEMBER_PORT}" + check_err $? "'enabled' wasn't implicitly set to true" + + log_test "'Tx_enabled' implicit changes" +} + + require_command teamnl setup tests_run diff --git a/tools/testing/selftests/drivers/net/team/settings b/tools/testing/selftests/drivers/net/team/settings new file mode 100644 index 000000000000..694d70710ff0 --- /dev/null +++ b/tools/testing/selftests/drivers/net/team/settings @@ -0,0 +1 @@ +timeout=300 diff --git a/tools/testing/selftests/drivers/net/team/team_lib.sh b/tools/testing/selftests/drivers/net/team/team_lib.sh new file mode 100644 index 000000000000..02ef0ee02d6a --- /dev/null +++ b/tools/testing/selftests/drivers/net/team/team_lib.sh @@ -0,0 +1,174 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 + +test_dir="$(dirname "$0")" +export REQUIRE_MZ=no +export NUM_NETIFS=0 +# shellcheck disable=SC1091 +source "${test_dir}/../../../net/forwarding/lib.sh" + +TCP_PORT="43434" + +# Create a team interface inside of a given network namespace with a given +# mode, members, and IP address. +# Arguments: +# namespace - Network namespace to put the team interface into. +# team - The name of the team interface to setup. +# mode - The team mode of the interface. +# ip_address - The IP address to assign to the team interface. +# prefix_length - The prefix length for the IP address subnet. +# $@ - members - The member interfaces of the aggregation. +setup_team() +{ + local namespace=$1 + local team=$2 + local mode=$3 + local ip_address=$4 + local prefix_length=$5 + shift 5 + local members=("$@") + + # Prerequisite: team must have no members + for member in "${members[@]}"; do + ip -n "${namespace}" link set "${member}" nomaster + done + + # Prerequisite: team must have no address in order to set it + # shellcheck disable=SC2086 + ip -n "${namespace}" addr del "${ip_address}/${prefix_length}" \ + ${NODAD} dev "${team}" + + echo "Setting team in ${namespace} to mode ${mode}" + + if ! ip -n "${namespace}" link set "${team}" down; then + echo "Failed to bring team device down" + return 1 + fi + if ! ip netns exec "${namespace}" teamnl "${team}" setoption mode \ + "${mode}"; then + echo "Failed to set ${team} mode to '${mode}'" + return 1 + fi + + # Aggregate the members into teams. + for member in "${members[@]}"; do + ip -n "${namespace}" link set "${member}" master "${team}" + done + + # Bring team devices up and give them addresses. + if ! ip -n "${namespace}" link set "${team}" up; then + echo "Failed to set ${team} up" + return 1 + fi + + # shellcheck disable=SC2086 + if ! ip -n "${namespace}" addr add "${ip_address}/${prefix_length}" \ + ${NODAD} dev "${team}"; then + echo "Failed to give ${team} IP address in ${namespace}" + return 1 + fi +} + +# This is global used to keep track of the sender's iperf3 process, so that it +# can be terminated. +declare sender_pid + +# Start sending and receiving TCP traffic with iperf3. +# Globals: +# sender_pid - The process ID of the iperf3 sender process. Used to kill it +# later. +start_listening_and_sending() +{ + ip netns exec "${NS2}" iperf3 -s -p "${TCP_PORT}" --logfile /dev/null & + # Wait for server to become reachable before starting client. + slowwait 5 ip netns exec "${NS1}" iperf3 -c "${NS2_IP}" -p \ + "${TCP_PORT}" -t 1 --logfile /dev/null + ip netns exec "${NS1}" iperf3 -c "${NS2_IP}" -p "${TCP_PORT}" -b 1M -l \ + 1K -t 0 --logfile /dev/null & + sender_pid=$! +} + +# Stop sending TCP traffic with iperf3. +# Globals: +# sender_pid - The process ID of the iperf3 sender process. +stop_sending_and_listening() +{ + kill "${sender_pid}" && wait "${sender_pid}" 2>/dev/null || true +} + +# Monitor for TCP traffic with Tcpdump, save results to temp files. +# Arguments: +# namespace - The network namespace to run tcpdump inside of. +# $@ - interfaces - The interfaces to listen to. +save_tcpdump_outputs() +{ + local namespace=$1 + shift 1 + local interfaces=("$@") + + for interface in "${interfaces[@]}"; do + tcpdump_start "${interface}" "${namespace}" + done + + sleep 1 + + for interface in "${interfaces[@]}"; do + tcpdump_stop_nosleep "${interface}" + done +} + +clear_tcpdump_outputs() +{ + local interfaces=("$@") + + for interface in "${interfaces[@]}"; do + tcpdump_cleanup "${interface}" + done +} + +# Read Tcpdump output, determine packet counts. +# Arguments: +# interface - The name of the interface to count packets for. +# ip_address - The destination IP address. +did_interface_receive() +{ + local interface="$1" + local ip_address="$2" + local packet_count + + packet_count=$(tcpdump_show "$interface" | grep -c \ + "> ${ip_address}.${TCP_PORT}") + echo "Packet count for ${interface} was ${packet_count}" + + if [[ "${packet_count}" -gt 0 ]]; then + true + else + false + fi +} + +# Return true if the given interface in the given namespace does NOT receive +# traffic over a 1 second period. +# Arguments: +# interface - The name of the interface. +# ip_address - The destination IP address. +# namespace - The name of the namespace that the interface is in. +check_no_traffic() +{ + local interface="$1" + local ip_address="$2" + local namespace="$3" + local rc + + save_tcpdump_outputs "${namespace}" "${interface}" + did_interface_receive "${interface}" "${ip_address}" + rc=$? + + clear_tcpdump_outputs "${interface}" + + if [[ "${rc}" -eq 0 ]]; then + return 1 + else + return 0 + fi +} diff --git a/tools/testing/selftests/drivers/net/team/teamd_activebackup.sh b/tools/testing/selftests/drivers/net/team/teamd_activebackup.sh new file mode 100755 index 000000000000..2b26a697e179 --- /dev/null +++ b/tools/testing/selftests/drivers/net/team/teamd_activebackup.sh @@ -0,0 +1,246 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 + +# These tests verify that teamd is able to enable and disable ports via the +# active backup runner. +# +# Topology: +# +# +-------------------------+ NS1 +# | test_team1 | +# | + | +# | eth0 | eth1 | +# | +---+---+ | +# | | | | +# +-------------------------+ +# | | +# +-------------------------+ NS2 +# | | | | +# | +-------+ | +# | eth0 | eth1 | +# | + | +# | test_team2 | +# +-------------------------+ + +export ALL_TESTS="teamd_test_active_backup" + +test_dir="$(dirname "$0")" +# shellcheck disable=SC1091 +source "${test_dir}/../../../net/lib.sh" +# shellcheck disable=SC1091 +source "${test_dir}/team_lib.sh" + +NS1="" +NS2="" +export NODAD="nodad" +PREFIX_LENGTH="64" +NS1_IP="fd00::1" +NS2_IP="fd00::2" +NS1_IP4="192.168.0.1" +NS2_IP4="192.168.0.2" +NS1_TEAMD_CONF="" +NS2_TEAMD_CONF="" +NS1_TEAMD_PID="" +NS2_TEAMD_PID="" + +while getopts "4" opt; do + case $opt in + 4) + echo "IPv4 mode selected." + export NODAD= + PREFIX_LENGTH="24" + NS1_IP="${NS1_IP4}" + NS2_IP="${NS2_IP4}" + ;; + \?) + echo "Invalid option: -${OPTARG}" >&2 + exit 1 + ;; + esac +done + +teamd_config_create() +{ + local runner=$1 + local dev=$2 + local conf + + conf=$(mktemp) + + cat > "${conf}" <<-EOF + { + "device": "${dev}", + "runner": {"name": "${runner}"}, + "ports": { + "eth0": {}, + "eth1": {} + } + } + EOF + echo "${conf}" +} + +# Create the network namespaces, veth pair, and team devices in the specified +# runner. +# Globals: +# RET - Used by test infra, set by `check_err` functions. +# Arguments: +# runner - The Teamd runner to use for the Team devices. +environment_create() +{ + local runner=$1 + + echo "Setting up two-link aggregation for runner ${runner}" + echo "Teamd version is: $(teamd --version)" + trap environment_destroy EXIT + + setup_ns ns1 ns2 + NS1="${NS_LIST[0]}" + NS2="${NS_LIST[1]}" + + for link in $(seq 0 1); do + ip -n "${NS1}" link add "eth${link}" type veth peer name \ + "eth${link}" netns "${NS2}" + check_err $? "Failed to create veth pair" + done + + NS1_TEAMD_CONF=$(teamd_config_create "${runner}" "test_team1") + NS2_TEAMD_CONF=$(teamd_config_create "${runner}" "test_team2") + echo "Conf files are ${NS1_TEAMD_CONF} and ${NS2_TEAMD_CONF}" + + ip netns exec "${NS1}" teamd -d -f "${NS1_TEAMD_CONF}" + check_err $? "Failed to create team device in ${NS1}" + NS1_TEAMD_PID=$(pgrep -f "teamd -d -f ${NS1_TEAMD_CONF}") + + ip netns exec "${NS2}" teamd -d -f "${NS2_TEAMD_CONF}" + check_err $? "Failed to create team device in ${NS2}" + NS2_TEAMD_PID=$(pgrep -f "teamd -d -f ${NS2_TEAMD_CONF}") + + echo "Created team devices" + echo "Teamd PIDs are ${NS1_TEAMD_PID} and ${NS2_TEAMD_PID}" + + ip -n "${NS1}" link set test_team1 up + check_err $? "Failed to set test_team1 up in ${NS1}" + ip -n "${NS2}" link set test_team2 up + check_err $? "Failed to set test_team2 up in ${NS2}" + + ip -n "${NS1}" addr add "${NS1_IP}/${PREFIX_LENGTH}" "${NODAD}" dev \ + test_team1 + check_err $? "Failed to add address to team device in ${NS1}" + ip -n "${NS2}" addr add "${NS2_IP}/${PREFIX_LENGTH}" "${NODAD}" dev \ + test_team2 + check_err $? "Failed to add address to team device in ${NS2}" + + slowwait 2 timeout 0.5 ip netns exec "${NS1}" ping -W 1 -c 1 "${NS2_IP}" +} + +# Tear down the environment: kill teamd and delete network namespaces. +environment_destroy() +{ + echo "Tearing down two-link aggregation" + + rm "${NS1_TEAMD_CONF}" + rm "${NS2_TEAMD_CONF}" + + # First, try graceful teamd teardown. + ip netns exec "${NS1}" teamd -k -t test_team1 + ip netns exec "${NS2}" teamd -k -t test_team2 + + # If teamd can't be killed gracefully, then sigkill. + if kill -0 "${NS1_TEAMD_PID}" 2>/dev/null; then + echo "Sending sigkill to teamd for test_team1" + kill -9 "${NS1_TEAMD_PID}" + rm -f /var/run/teamd/test_team1.{pid,sock} + fi + if kill -0 "${NS2_TEAMD_PID}" 2>/dev/null; then + echo "Sending sigkill to teamd for test_team2" + kill -9 "${NS2_TEAMD_PID}" + rm -f /var/run/teamd/test_team2.{pid,sock} + fi + cleanup_all_ns +} + +# Change the active port for an active-backup mode team. +# Arguments: +# namespace - The network namespace that the team is in. +# team - The name of the team. +# active_port - The port to make active. +set_active_port() +{ + local namespace=$1 + local team=$2 + local active_port=$3 + + ip netns exec "${namespace}" teamdctl "${team}" state item set \ + runner.active_port "${active_port}" + slowwait 2 bash -c "ip netns exec ${namespace} teamdctl ${team} state \ + item get runner.active_port | grep -q ${active_port}" +} + +# Wait for an interface to stop receiving traffic. If it keeps receiving traffic +# for the duration of the timeout, then return an error. +# Arguments: +# - namespace - The network namespace that the interface is in. +# - interface - The name of the interface. +wait_to_stop_receiving() +{ + local namespace=$1 + local interface=$2 + + echo "Waiting for ${interface} in ${namespace} to stop receiving" + slowwait 10 check_no_traffic "${interface}" "${NS2_IP}" \ + "${namespace}" +} + +# Test that active backup runner can change active ports. +# Globals: +# RET - Used by test infra, set by `check_err` functions. +teamd_test_active_backup() +{ + export RET=0 + + start_listening_and_sending + + ### Scenario 1: Don't manually set active port, just make sure team + # works. + save_tcpdump_outputs "${NS2}" test_team2 + did_interface_receive test_team2 "${NS2_IP}" + check_err $? "Traffic did not reach team interface in NS2." + clear_tcpdump_outputs test_team2 + + ### Scenario 2: Choose active port. + set_active_port "${NS1}" test_team1 eth1 + set_active_port "${NS2}" test_team2 eth1 + + wait_to_stop_receiving "${NS2}" eth0 + save_tcpdump_outputs "${NS2}" eth0 eth1 + did_interface_receive eth0 "${NS2_IP}" + check_fail $? "eth0 IS transmitting when inactive" + did_interface_receive eth1 "${NS2_IP}" + check_err $? "eth1 not transmitting when active" + clear_tcpdump_outputs eth0 eth1 + + ### Scenario 3: Change active port. + set_active_port "${NS1}" test_team1 eth0 + set_active_port "${NS2}" test_team2 eth0 + + wait_to_stop_receiving "${NS2}" eth1 + save_tcpdump_outputs "${NS2}" eth0 eth1 + did_interface_receive eth0 "${NS2_IP}" + check_err $? "eth0 not transmitting when active" + did_interface_receive eth1 "${NS2_IP}" + check_fail $? "eth1 IS transmitting when inactive" + clear_tcpdump_outputs eth0 eth1 + + log_test "teamd active backup runner test" + + stop_sending_and_listening +} + +require_command teamd +require_command teamdctl +require_command iperf3 +require_command tcpdump +environment_create activebackup +tests_run +exit "${EXIT_STATUS}" diff --git a/tools/testing/selftests/drivers/net/team/transmit_failover.sh b/tools/testing/selftests/drivers/net/team/transmit_failover.sh new file mode 100755 index 000000000000..b2bdcd27bc98 --- /dev/null +++ b/tools/testing/selftests/drivers/net/team/transmit_failover.sh @@ -0,0 +1,158 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 + +# These tests verify the basic failover capability of the team driver via the +# `enabled` team driver option across different team driver modes. This does not +# rely on teamd, and instead just uses teamnl to set the `enabled` option +# directly. +# +# Topology: +# +# +-------------------------+ NS1 +# | test_team1 | +# | + | +# | eth0 | eth1 | +# | +---+---+ | +# | | | | +# +-------------------------+ +# | | +# +-------------------------+ NS2 +# | | | | +# | +-------+ | +# | eth0 | eth1 | +# | + | +# | test_team2 | +# +-------------------------+ + +export ALL_TESTS="team_test_failover" + +test_dir="$(dirname "$0")" +# shellcheck disable=SC1091 +source "${test_dir}/../../../net/lib.sh" +# shellcheck disable=SC1091 +source "${test_dir}/team_lib.sh" + +NS1="" +NS2="" +export NODAD="nodad" +PREFIX_LENGTH="64" +NS1_IP="fd00::1" +NS2_IP="fd00::2" +NS1_IP4="192.168.0.1" +NS2_IP4="192.168.0.2" +MEMBERS=("eth0" "eth1") + +while getopts "4" opt; do + case $opt in + 4) + echo "IPv4 mode selected." + export NODAD= + PREFIX_LENGTH="24" + NS1_IP="${NS1_IP4}" + NS2_IP="${NS2_IP4}" + ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + exit 1 + ;; + esac +done + +# Create the network namespaces, veth pair, and team devices in the specified +# mode. +# Globals: +# RET - Used by test infra, set by `check_err` functions. +# Arguments: +# mode - The team driver mode to use for the team devices. +environment_create() +{ + trap cleanup_all_ns EXIT + setup_ns ns1 ns2 + NS1="${NS_LIST[0]}" + NS2="${NS_LIST[1]}" + + # Create the interfaces. + ip -n "${NS1}" link add eth0 type veth peer name eth0 netns "${NS2}" + ip -n "${NS1}" link add eth1 type veth peer name eth1 netns "${NS2}" + ip -n "${NS1}" link add test_team1 type team + ip -n "${NS2}" link add test_team2 type team + + # Set up the receiving network namespace's team interface. + setup_team "${NS2}" test_team2 roundrobin "${NS2_IP}" \ + "${PREFIX_LENGTH}" "${MEMBERS[@]}" +} + + +# Check that failover works for a specific team driver mode. +# Globals: +# RET - Used by test infra, set by `check_err` functions. +# Arguments: +# mode - The mode to set the team interfaces to. +team_test_mode_failover() +{ + local mode="$1" + export RET=0 + + # Set up the sender team with the correct mode. + setup_team "${NS1}" test_team1 "${mode}" "${NS1_IP}" \ + "${PREFIX_LENGTH}" "${MEMBERS[@]}" + check_err $? "Failed to set up sender team" + + start_listening_and_sending + + ### Scenario 1: All interfaces initially enabled. + save_tcpdump_outputs "${NS2}" "${MEMBERS[@]}" + did_interface_receive eth0 "${NS2_IP}" + check_err $? "eth0 not transmitting when both links enabled" + did_interface_receive eth1 "${NS2_IP}" + check_err $? "eth1 not transmitting when both links enabled" + clear_tcpdump_outputs "${MEMBERS[@]}" + + ### Scenario 2: One tx-side interface disabled. + ip netns exec "${NS1}" teamnl test_team1 setoption enabled false \ + --port=eth1 + slowwait 2 bash -c "ip netns exec ${NS1} teamnl test_team1 getoption \ + enabled --port=eth1 | grep -q false" + + save_tcpdump_outputs "${NS2}" "${MEMBERS[@]}" + did_interface_receive eth0 "${NS2_IP}" + check_err $? "eth0 not transmitting when enabled" + did_interface_receive eth1 "${NS2_IP}" + check_fail $? "eth1 IS transmitting when disabled" + clear_tcpdump_outputs "${MEMBERS[@]}" + + ### Scenario 3: The interface is re-enabled. + ip netns exec "${NS1}" teamnl test_team1 setoption enabled true \ + --port=eth1 + slowwait 2 bash -c "ip netns exec ${NS1} teamnl test_team1 getoption \ + enabled --port=eth1 | grep -q true" + + save_tcpdump_outputs "${NS2}" "${MEMBERS[@]}" + did_interface_receive eth0 "${NS2_IP}" + check_err $? "eth0 not transmitting when both links enabled" + did_interface_receive eth1 "${NS2_IP}" + check_err $? "eth1 not transmitting when both links enabled" + clear_tcpdump_outputs "${MEMBERS[@]}" + + log_test "Failover of '${mode}' test" + + # Clean up + stop_sending_and_listening +} + +team_test_failover() +{ + team_test_mode_failover broadcast + team_test_mode_failover roundrobin + team_test_mode_failover random + # Don't test `activebackup` or `loadbalance` modes, since they are too + # complicated for just setting `enabled` to work. They use more than + # the `enabled` option for transmit. +} + +require_command teamnl +require_command iperf3 +require_command tcpdump +environment_create +tests_run +exit "${EXIT_STATUS}" diff --git a/tools/testing/selftests/drivers/net/xdp.py b/tools/testing/selftests/drivers/net/xdp.py index e54df158dfe9..0369929f3c51 100755 --- a/tools/testing/selftests/drivers/net/xdp.py +++ b/tools/testing/selftests/drivers/net/xdp.py @@ -13,10 +13,11 @@ from enum import Enum from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_ge, ksft_ne, ksft_pr from lib.py import KsftNamedVariant, ksft_variants -from lib.py import KsftFailEx, NetDrvEpEnv +from lib.py import KsftFailEx, KsftSkipEx, NetDrvEpEnv from lib.py import EthtoolFamily, NetdevFamily, NlError from lib.py import bkg, cmd, rand_port, wait_port_listen -from lib.py import ip, bpftool, defer +from lib.py import ip, defer +from lib.py import bpf_map_set, bpf_map_dump, bpf_prog_map_ids class TestConfig(Enum): @@ -69,7 +70,7 @@ def _exchg_udp(cfg, port, test_string): cfg.require_cmd("socat", remote=True) rx_udp_cmd = f"socat -{cfg.addr_ipver} -T 2 -u UDP-RECV:{port},reuseport STDOUT" - tx_udp_cmd = f"echo -n {test_string} | socat -t 2 -u STDIN UDP:{cfg.baddr}:{port}" + tx_udp_cmd = f"echo -n {test_string} | socat -t 2 -u STDIN UDP:{cfg.baddr}:{port},shut-none" with bkg(rx_udp_cmd, exit_wait=True) as nc: wait_port_listen(port, proto="udp") @@ -122,47 +123,11 @@ def _load_xdp_prog(cfg, bpf_info): xdp_info = ip(f"-d link show dev {cfg.ifname}", json=True)[0] prog_info["id"] = xdp_info["xdp"]["prog"]["id"] prog_info["name"] = xdp_info["xdp"]["prog"]["name"] - prog_id = prog_info["id"] - - map_ids = bpftool(f"prog show id {prog_id}", json=True)["map_ids"] - prog_info["maps"] = {} - for map_id in map_ids: - name = bpftool(f"map show id {map_id}", json=True)["name"] - prog_info["maps"][name] = map_id + prog_info["maps"] = bpf_prog_map_ids(prog_info["id"]) return prog_info -def format_hex_bytes(value): - """ - Helper function that converts an integer into a formatted hexadecimal byte string. - - Args: - value: An integer representing the number to be converted. - - Returns: - A string representing hexadecimal equivalent of value, with bytes separated by spaces. - """ - hex_str = value.to_bytes(4, byteorder='little', signed=True) - return ' '.join(f'{byte:02x}' for byte in hex_str) - - -def _set_xdp_map(map_name, key, value): - """ - Updates an XDP map with a given key-value pair using bpftool. - - Args: - map_name: The name of the XDP map to update. - key: The key to update in the map, formatted as a hexadecimal string. - value: The value to associate with the key, formatted as a hexadecimal string. - """ - key_formatted = format_hex_bytes(key) - value_formatted = format_hex_bytes(value) - bpftool( - f"map update name {map_name} key hex {key_formatted} value hex {value_formatted}" - ) - - def _get_stats(xdp_map_id): """ Retrieves and formats statistics from an XDP map. @@ -177,25 +142,11 @@ def _get_stats(xdp_map_id): Raises: KsftFailEx: If the stats retrieval fails. """ - stats_dump = bpftool(f"map dump id {xdp_map_id}", json=True) - if not stats_dump: + stats = bpf_map_dump(xdp_map_id) + if not stats: raise KsftFailEx(f"Failed to get stats for map {xdp_map_id}") - stats_formatted = {} - for key in range(0, 5): - val = stats_dump[key]["formatted"]["value"] - if stats_dump[key]["formatted"]["key"] == XDPStats.RX.value: - stats_formatted[XDPStats.RX.value] = val - elif stats_dump[key]["formatted"]["key"] == XDPStats.PASS.value: - stats_formatted[XDPStats.PASS.value] = val - elif stats_dump[key]["formatted"]["key"] == XDPStats.DROP.value: - stats_formatted[XDPStats.DROP.value] = val - elif stats_dump[key]["formatted"]["key"] == XDPStats.TX.value: - stats_formatted[XDPStats.TX.value] = val - elif stats_dump[key]["formatted"]["key"] == XDPStats.ABORT.value: - stats_formatted[XDPStats.ABORT.value] = val - - return stats_formatted + return stats def _test_pass(cfg, bpf_info, msg_sz): @@ -211,8 +162,8 @@ def _test_pass(cfg, bpf_info, msg_sz): prog_info = _load_xdp_prog(cfg, bpf_info) port = rand_port() - _set_xdp_map("map_xdp_setup", TestConfig.MODE.value, XDPAction.PASS.value) - _set_xdp_map("map_xdp_setup", TestConfig.PORT.value, port) + bpf_map_set("map_xdp_setup", TestConfig.MODE.value, XDPAction.PASS.value) + bpf_map_set("map_xdp_setup", TestConfig.PORT.value, port) ksft_eq(_test_udp(cfg, port, msg_sz), True, "UDP packet exchange failed") stats = _get_stats(prog_info["maps"]["map_xdp_stats"]) @@ -221,25 +172,45 @@ def _test_pass(cfg, bpf_info, msg_sz): ksft_eq(stats[XDPStats.RX.value], stats[XDPStats.PASS.value], "RX and PASS stats mismatch") -def test_xdp_native_pass_sb(cfg): +_ipvers = [ + KsftNamedVariant("ipv4", "4"), + KsftNamedVariant("ipv6", "6"), +] + + +def _set_ipver_defer_restore(cfg, ipver): + old_ipver = cfg.addr_ipver + cfg.set_ipver(ipver) + defer(cfg.set_ipver, old_ipver) + + +@ksft_variants(_ipvers) +def test_xdp_native_pass_sb(cfg, ipver): """ Tests the XDP_PASS action for single buffer case. Args: cfg: Configuration object containing network settings. + ipver: IP version to use ("4" or "6"). """ + _set_ipver_defer_restore(cfg, ipver) + bpf_info = BPFProgInfo("xdp_prog", "xdp_native.bpf.o", "xdp", 1500) _test_pass(cfg, bpf_info, 256) -def test_xdp_native_pass_mb(cfg): +@ksft_variants(_ipvers) +def test_xdp_native_pass_mb(cfg, ipver): """ Tests the XDP_PASS action for a multi-buff size. Args: cfg: Configuration object containing network settings. + ipver: IP version to use ("4" or "6"). """ + _set_ipver_defer_restore(cfg, ipver) + bpf_info = BPFProgInfo("xdp_prog_frags", "xdp_native.bpf.o", "xdp.frags", 9000) _test_pass(cfg, bpf_info, 8000) @@ -258,8 +229,8 @@ def _test_drop(cfg, bpf_info, msg_sz): prog_info = _load_xdp_prog(cfg, bpf_info) port = rand_port() - _set_xdp_map("map_xdp_setup", TestConfig.MODE.value, XDPAction.DROP.value) - _set_xdp_map("map_xdp_setup", TestConfig.PORT.value, port) + bpf_map_set("map_xdp_setup", TestConfig.MODE.value, XDPAction.DROP.value) + bpf_map_set("map_xdp_setup", TestConfig.PORT.value, port) ksft_eq(_test_udp(cfg, port, msg_sz), False, "UDP packet exchange should fail") stats = _get_stats(prog_info["maps"]["map_xdp_stats"]) @@ -268,25 +239,33 @@ def _test_drop(cfg, bpf_info, msg_sz): ksft_eq(stats[XDPStats.RX.value], stats[XDPStats.DROP.value], "RX and DROP stats mismatch") -def test_xdp_native_drop_sb(cfg): +@ksft_variants(_ipvers) +def test_xdp_native_drop_sb(cfg, ipver): """ Tests the XDP_DROP action for a signle-buff case. Args: cfg: Configuration object containing network settings. + ipver: IP version to use ("4" or "6"). """ + _set_ipver_defer_restore(cfg, ipver) + bpf_info = BPFProgInfo("xdp_prog", "xdp_native.bpf.o", "xdp", 1500) _test_drop(cfg, bpf_info, 256) -def test_xdp_native_drop_mb(cfg): +@ksft_variants(_ipvers) +def test_xdp_native_drop_mb(cfg, ipver): """ Tests the XDP_DROP action for a multi-buff case. Args: cfg: Configuration object containing network settings. + ipver: IP version to use ("4" or "6"). """ + _set_ipver_defer_restore(cfg, ipver) + bpf_info = BPFProgInfo("xdp_prog_frags", "xdp_native.bpf.o", "xdp.frags", 9000) _test_drop(cfg, bpf_info, 8000) @@ -305,8 +284,8 @@ def _test_xdp_native_tx(cfg, bpf_info, payload_lens): prog_info = _load_xdp_prog(cfg, bpf_info) port = rand_port() - _set_xdp_map("map_xdp_setup", TestConfig.MODE.value, XDPAction.TX.value) - _set_xdp_map("map_xdp_setup", TestConfig.PORT.value, port) + bpf_map_set("map_xdp_setup", TestConfig.MODE.value, XDPAction.TX.value) + bpf_map_set("map_xdp_setup", TestConfig.PORT.value, port) expected_pkts = 0 for payload_len in payload_lens: @@ -320,7 +299,7 @@ def _test_xdp_native_tx(cfg, bpf_info, payload_lens): # Writing zero bytes to stdin gets ignored by socat, # but with the shut-null flag socat generates a zero sized packet # when the socket is closed. - tx_cmd_suffix = ",shut-null" if payload_len == 0 else "" + tx_cmd_suffix = ",shut-null" if payload_len == 0 else ",shut-none" tx_udp = f"echo -n {test_string} | socat -t 2 " + \ f"-u STDIN UDP:{cfg.baddr}:{port}{tx_cmd_suffix}" @@ -336,13 +315,17 @@ def _test_xdp_native_tx(cfg, bpf_info, payload_lens): ksft_eq(stats[XDPStats.TX.value], expected_pkts, "TX stats mismatch") -def test_xdp_native_tx_sb(cfg): +@ksft_variants(_ipvers) +def test_xdp_native_tx_sb(cfg, ipver): """ Tests the XDP_TX action for a single-buff case. Args: cfg: Configuration object containing network settings. + ipver: IP version to use ("4" or "6"). """ + _set_ipver_defer_restore(cfg, ipver) + bpf_info = BPFProgInfo("xdp_prog", "xdp_native.bpf.o", "xdp", 1500) # Ensure there's enough room for an ETH / IP / UDP header @@ -351,13 +334,17 @@ def test_xdp_native_tx_sb(cfg): _test_xdp_native_tx(cfg, bpf_info, [0, 1500 // 2, 1500 - pkt_hdr_len]) -def test_xdp_native_tx_mb(cfg): +@ksft_variants(_ipvers) +def test_xdp_native_tx_mb(cfg, ipver): """ Tests the XDP_TX action for a multi-buff case. Args: cfg: Configuration object containing network settings. + ipver: IP version to use ("4" or "6"). """ + _set_ipver_defer_restore(cfg, ipver) + bpf_info = BPFProgInfo("xdp_prog_frags", "xdp_native.bpf.o", "xdp.frags", 9000) # The first packet ensures we exercise the fragmented code path. @@ -454,15 +441,15 @@ def _test_xdp_native_tail_adjst(cfg, pkt_sz_lst, offset_lst): prog_info = _load_xdp_prog(cfg, bpf_info) # Configure the XDP map for tail adjustment - _set_xdp_map("map_xdp_setup", TestConfig.MODE.value, XDPAction.TAIL_ADJST.value) - _set_xdp_map("map_xdp_setup", TestConfig.PORT.value, port) + bpf_map_set("map_xdp_setup", TestConfig.MODE.value, XDPAction.TAIL_ADJST.value) + bpf_map_set("map_xdp_setup", TestConfig.PORT.value, port) for offset in offset_lst: tag = format(random.randint(65, 90), "02x") - _set_xdp_map("map_xdp_setup", TestConfig.ADJST_OFFSET.value, offset) + bpf_map_set("map_xdp_setup", TestConfig.ADJST_OFFSET.value, offset) if offset > 0: - _set_xdp_map("map_xdp_setup", TestConfig.ADJST_TAG.value, int(tag, 16)) + bpf_map_set("map_xdp_setup", TestConfig.ADJST_TAG.value, int(tag, 16)) for pkt_sz in pkt_sz_lst: test_str = "".join(random.choice(string.ascii_lowercase) for _ in range(pkt_sz)) @@ -496,13 +483,17 @@ def _test_xdp_native_tail_adjst(cfg, pkt_sz_lst, offset_lst): return {"status": "pass"} -def test_xdp_native_adjst_tail_grow_data(cfg): +@ksft_variants(_ipvers) +def test_xdp_native_adjst_tail_grow_data(cfg, ipver): """ Tests the XDP tail adjustment by growing packet data. Args: cfg: Configuration object containing network settings. + ipver: IP version to use ("4" or "6"). """ + _set_ipver_defer_restore(cfg, ipver) + pkt_sz_lst = [512, 1024, 2048] offset_lst = [1, 16, 32, 64, 128, 256] res = _test_xdp_native_tail_adjst( @@ -514,13 +505,17 @@ def test_xdp_native_adjst_tail_grow_data(cfg): _validate_res(res, offset_lst, pkt_sz_lst) -def test_xdp_native_adjst_tail_shrnk_data(cfg): +@ksft_variants(_ipvers) +def test_xdp_native_adjst_tail_shrnk_data(cfg, ipver): """ Tests the XDP tail adjustment by shrinking packet data. Args: cfg: Configuration object containing network settings. + ipver: IP version to use ("4" or "6"). """ + _set_ipver_defer_restore(cfg, ipver) + pkt_sz_lst = [512, 1024, 2048] offset_lst = [-16, -32, -64, -128, -256] res = _test_xdp_native_tail_adjst( @@ -574,8 +569,8 @@ def _test_xdp_native_head_adjst(cfg, prog, pkt_sz_lst, offset_lst): prog_info = _load_xdp_prog(cfg, BPFProgInfo(prog, "xdp_native.bpf.o", "xdp.frags", 9000)) port = rand_port() - _set_xdp_map("map_xdp_setup", TestConfig.MODE.value, XDPAction.HEAD_ADJST.value) - _set_xdp_map("map_xdp_setup", TestConfig.PORT.value, port) + bpf_map_set("map_xdp_setup", TestConfig.MODE.value, XDPAction.HEAD_ADJST.value) + bpf_map_set("map_xdp_setup", TestConfig.PORT.value, port) hds_thresh = get_hds_thresh(cfg) for offset in offset_lst: @@ -584,7 +579,7 @@ def _test_xdp_native_head_adjst(cfg, prog, pkt_sz_lst, offset_lst): # after we eat into it. We send large-enough packets, but if HDS # is enabled head will only contain headers. Don't try to eat # more than 28 bytes (UDPv4 + eth hdr left: (14 + 20 + 8) - 14) - l2_cut_off = 28 if cfg.addr_ipver == 4 else 48 + l2_cut_off = 28 if cfg.addr_ipver == "4" else 48 if pkt_sz > hds_thresh and offset > l2_cut_off: ksft_pr( f"Failed run: pkt_sz ({pkt_sz}) > HDS threshold ({hds_thresh}) and " @@ -595,11 +590,8 @@ def _test_xdp_native_head_adjst(cfg, prog, pkt_sz_lst, offset_lst): test_str = ''.join(random.choice(string.ascii_lowercase) for _ in range(pkt_sz)) tag = format(random.randint(65, 90), '02x') - _set_xdp_map("map_xdp_setup", - TestConfig.ADJST_OFFSET.value, - offset) - _set_xdp_map("map_xdp_setup", TestConfig.ADJST_TAG.value, int(tag, 16)) - _set_xdp_map("map_xdp_setup", TestConfig.ADJST_OFFSET.value, offset) + bpf_map_set("map_xdp_setup", TestConfig.ADJST_OFFSET.value, offset) + bpf_map_set("map_xdp_setup", TestConfig.ADJST_TAG.value, int(tag, 16)) recvd_str = _exchg_udp(cfg, port, test_str) @@ -631,18 +623,22 @@ def _test_xdp_native_head_adjst(cfg, prog, pkt_sz_lst, offset_lst): return {"status": "pass"} -def test_xdp_native_adjst_head_grow_data(cfg): +@ksft_variants(_ipvers) +def test_xdp_native_adjst_head_grow_data(cfg, ipver): """ Tests the XDP headroom growth support. Args: cfg: Configuration object containing network settings. + ipver: IP version to use ("4" or "6"). This function sets up the packet size and offset lists, then calls the _test_xdp_native_head_adjst_mb function to perform the actual test. The test is passed if the headroom is successfully extended for given packet sizes and offsets. """ + _set_ipver_defer_restore(cfg, ipver) + pkt_sz_lst = [512, 1024, 2048] # Negative values result in headroom shrinking, resulting in growing of payload @@ -652,18 +648,22 @@ def test_xdp_native_adjst_head_grow_data(cfg): _validate_res(res, offset_lst, pkt_sz_lst) -def test_xdp_native_adjst_head_shrnk_data(cfg): +@ksft_variants(_ipvers) +def test_xdp_native_adjst_head_shrnk_data(cfg, ipver): """ Tests the XDP headroom shrinking support. Args: cfg: Configuration object containing network settings. + ipver: IP version to use ("4" or "6"). This function sets up the packet size and offset lists, then calls the _test_xdp_native_head_adjst_mb function to perform the actual test. The test is passed if the headroom is successfully shrunk for given packet sizes and offsets. """ + _set_ipver_defer_restore(cfg, ipver) + pkt_sz_lst = [512, 1024, 2048] # Positive values result in headroom growing, resulting in shrinking of payload @@ -673,12 +673,19 @@ def test_xdp_native_adjst_head_shrnk_data(cfg): _validate_res(res, offset_lst, pkt_sz_lst) -@ksft_variants([ - KsftNamedVariant("pass", XDPAction.PASS), - KsftNamedVariant("drop", XDPAction.DROP), - KsftNamedVariant("tx", XDPAction.TX), -]) -def test_xdp_native_qstats(cfg, act): +def _qstats_variants(): + actions = [ + ("pass", XDPAction.PASS), + ("drop", XDPAction.DROP), + ("tx", XDPAction.TX), + ] + for ipver in ["4", "6"]: + for name, act in actions: + yield KsftNamedVariant(f"{name}_ipv{ipver}", act, ipver) + + +@ksft_variants(_qstats_variants()) +def test_xdp_native_qstats(cfg, act, ipver): """ Send 1000 messages. Expect XDP action specified in @act. Make sure the packets were counted to interface level qstats @@ -686,13 +693,14 @@ def test_xdp_native_qstats(cfg, act): """ cfg.require_cmd("socat") + _set_ipver_defer_restore(cfg, ipver) bpf_info = BPFProgInfo("xdp_prog", "xdp_native.bpf.o", "xdp", 1500) prog_info = _load_xdp_prog(cfg, bpf_info) port = rand_port() - _set_xdp_map("map_xdp_setup", TestConfig.MODE.value, act.value) - _set_xdp_map("map_xdp_setup", TestConfig.PORT.value, port) + bpf_map_set("map_xdp_setup", TestConfig.MODE.value, act.value) + bpf_map_set("map_xdp_setup", TestConfig.PORT.value, port) # Discard the input, but we need a listener to avoid ICMP errors rx_udp = f"socat -{cfg.addr_ipver} -T 2 -u UDP-RECV:{port},reuseport " + \ @@ -745,6 +753,34 @@ def test_xdp_native_qstats(cfg, act): ksft_ge(after['tx-packets'], before['tx-packets']) +def test_xdp_native_update_mb_to_sb(cfg): + """ + Test multi-buf to single-buf replacement with jumbo MTU. + """ + obj = cfg.net_lib_dir / "xdp_dummy.bpf.o" + mtu = 9000 + + ip(f"link set dev {cfg.ifname} mtu {mtu}") + defer(ip, f"link set dev {cfg.ifname} mtu {cfg.dev['mtu']} xdpdrv off") + + attach = cmd(f"ip link set dev {cfg.ifname} xdpdrv obj {obj} sec xdp", fail=False) + if attach.ret == 0: + raise KsftSkipEx(f"device supports single-buffer XDP with mtu {mtu}") + + attach = cmd(f"ip link set dev {cfg.ifname} xdpdrv obj {obj} sec xdp.frags", fail=False) + if attach.ret != 0: + ksft_pr(attach) + raise KsftSkipEx("device does not support multi-buffer XDP") + + # Verify updating mb -> mb program works. + cmd(f"ip -force link set dev {cfg.ifname} xdpdrv obj {obj} sec xdp.frags") + + # Verify updating mb -> sb program does not work. + update = cmd(f"ip -force link set dev {cfg.ifname} xdpdrv obj {obj} sec xdp", fail=False) + if update.ret == 0: + raise KsftFailEx("device unexpectedly updates non-multi-buffer XDP") + + def main(): """ Main function to execute the XDP tests. @@ -770,6 +806,7 @@ def main(): test_xdp_native_adjst_head_grow_data, test_xdp_native_adjst_head_shrnk_data, test_xdp_native_qstats, + test_xdp_native_update_mb_to_sb, ], args=(cfg,)) ksft_exit() diff --git a/tools/testing/selftests/drivers/ntsync/config b/tools/testing/selftests/drivers/ntsync/config index 60539c826d06..0aa68de147af 100644 --- a/tools/testing/selftests/drivers/ntsync/config +++ b/tools/testing/selftests/drivers/ntsync/config @@ -1 +1 @@ -CONFIG_WINESYNC=y +CONFIG_NTSYNC=y diff --git a/tools/testing/selftests/drivers/ntsync/ntsync.c b/tools/testing/selftests/drivers/ntsync/ntsync.c index e6a37214aa46..1f0dc43bb4c0 100644 --- a/tools/testing/selftests/drivers/ntsync/ntsync.c +++ b/tools/testing/selftests/drivers/ntsync/ntsync.c @@ -8,12 +8,18 @@ #define _GNU_SOURCE #include <sys/ioctl.h> #include <sys/stat.h> +#include <sys/wait.h> #include <fcntl.h> +#include <sched.h> #include <time.h> #include <pthread.h> #include <linux/ntsync.h> #include "kselftest_harness.h" +#ifndef CLONE_NEWTIME +#define CLONE_NEWTIME 0x00000080 +#endif + static int read_sem_state(int sem, __u32 *count, __u32 *max) { struct ntsync_sem_args args; @@ -968,7 +974,7 @@ TEST(wake_all) auto_event_args.manual = false; auto_event_args.signaled = true; objs[3] = ioctl(fd, NTSYNC_IOC_CREATE_EVENT, &auto_event_args); - EXPECT_EQ(0, objs[3]); + EXPECT_LE(0, objs[3]); wait_args.timeout = get_abs_timeout(1000); wait_args.objs = (uintptr_t)objs; @@ -1340,4 +1346,129 @@ TEST(stress_wait) close(stress_device); } +TEST(wait_args_validation) +{ + struct ntsync_sem_args sem_args = { .count = 1, .max = 1 }; + struct ntsync_wait_args wait_args = {0}; + struct timespec timeout; + int fd, fd2, sem, ret; + __u32 index; + + fd = open("/dev/ntsync", O_CLOEXEC | O_RDONLY); + ASSERT_GE(fd, 0); + + fd2 = open("/dev/ntsync", O_CLOEXEC | O_RDONLY); + ASSERT_GE(fd2, 0); + + sem = ioctl(fd, NTSYNC_IOC_CREATE_SEM, &sem_args); + EXPECT_GE(sem, 0); + + ret = wait_any(fd, 1, &sem, 0, &index); + EXPECT_EQ(-1, ret); + EXPECT_EQ(EINVAL, errno); + + ret = wait_all(fd, 1, &sem, 0, &index); + EXPECT_EQ(-1, ret); + EXPECT_EQ(EINVAL, errno); + + clock_gettime(CLOCK_MONOTONIC, &timeout); + wait_args.timeout = timeout.tv_sec * 1000000000ULL + timeout.tv_nsec; + wait_args.count = 0; + wait_args.objs = 0; + wait_args.owner = 123; + wait_args.pad = 1; + ret = ioctl(fd, NTSYNC_IOC_WAIT_ANY, &wait_args); + EXPECT_EQ(-1, ret); + EXPECT_EQ(EINVAL, errno); + + ret = wait_any(fd2, 1, &sem, 123, &index); + EXPECT_EQ(-1, ret); + EXPECT_EQ(EINVAL, errno); + + close(sem); + close(fd2); + close(fd); +} + +/* + * Absolute MONOTONIC timeouts must honour the caller's time namespace. + * With a negative monotonic offset, a 100 ms wait must still take ~100 ms + * of namespace time (not return immediately against the host clock). + */ +TEST(wait_any_monotonic_timens) +{ + struct ntsync_sem_args sem_args = {0}; + struct ntsync_wait_args wait_args = {0}; + struct timespec start, end; + char buf[64]; + __u64 elapsed_ns; + int fd, offset_fd, sem, ret, status, len; + pid_t pid; + + if (access("/proc/self/ns/time", F_OK)) + SKIP(return, "Time namespaces are not supported"); + + fd = open("/dev/ntsync", O_CLOEXEC | O_RDONLY); + if (fd < 0) + SKIP(return, "/dev/ntsync is not available"); + + ret = unshare(CLONE_NEWTIME); + if (ret) { + close(fd); + if (errno == EPERM) + SKIP(return, "need CAP_SYS_ADMIN for CLONE_NEWTIME"); + ASSERT_EQ(0, ret); + } + + len = snprintf(buf, sizeof(buf), "%d %d 0", CLOCK_MONOTONIC, -10); + offset_fd = open("/proc/self/timens_offsets", O_WRONLY); + ASSERT_LE(0, offset_fd); + ASSERT_EQ(len, write(offset_fd, buf, len)); + close(offset_fd); + + pid = fork(); + ASSERT_LE(0, pid); + if (!pid) { + int obj; + + sem_args.count = 0; + sem_args.max = 1; + sem = ioctl(fd, NTSYNC_IOC_CREATE_SEM, &sem_args); + if (sem < 0) + _exit(1); + + obj = sem; + wait_args.timeout = get_abs_timeout(100); + wait_args.objs = (uintptr_t)&obj; + wait_args.count = 1; + wait_args.owner = 123; + wait_args.index = 0xdeadbeef; + + if (clock_gettime(CLOCK_MONOTONIC, &start)) + _exit(2); + ret = ioctl(fd, NTSYNC_IOC_WAIT_ANY, &wait_args); + if (clock_gettime(CLOCK_MONOTONIC, &end)) + _exit(2); + + if (ret != -1 || errno != ETIMEDOUT) + _exit(3); + + elapsed_ns = (end.tv_sec - start.tv_sec) * 1000000000ULL + + (end.tv_nsec - start.tv_nsec); + /* Without timens conversion this returns in ~0 ms. */ + if (elapsed_ns < 50 * 1000000ULL) + _exit(4); + if (elapsed_ns > 1000 * 1000000ULL) + _exit(5); + + _exit(0); + } + + ASSERT_EQ(pid, waitpid(pid, &status, 0)); + EXPECT_TRUE(WIFEXITED(status)); + EXPECT_EQ(0, WEXITSTATUS(status)); + + close(fd); +} + TEST_HARNESS_MAIN |
