summaryrefslogtreecommitdiff
path: root/tools/testing/selftests/drivers/net
diff options
context:
space:
mode:
Diffstat (limited to 'tools/testing/selftests/drivers/net')
-rw-r--r--tools/testing/selftests/drivers/net/.gitignore1
-rw-r--r--tools/testing/selftests/drivers/net/Makefile2
-rw-r--r--tools/testing/selftests/drivers/net/bonding/Makefile2
-rwxr-xr-xtools/testing/selftests/drivers/net/bonding/bond_lacp_strict.sh347
-rwxr-xr-xtools/testing/selftests/drivers/net/bonding/bond_vlan_real_dev.sh180
-rw-r--r--tools/testing/selftests/drivers/net/config8
-rwxr-xr-xtools/testing/selftests/drivers/net/gro.py29
-rw-r--r--tools/testing/selftests/drivers/net/hw/Makefile3
-rw-r--r--tools/testing/selftests/drivers/net/hw/config5
-rwxr-xr-xtools/testing/selftests/drivers/net/hw/devmem.py77
-rw-r--r--tools/testing/selftests/drivers/net/hw/devmem_lib.py222
-rwxr-xr-xtools/testing/selftests/drivers/net/hw/gro_hw.py10
-rw-r--r--tools/testing/selftests/drivers/net/hw/iou-zcrx.c6
-rwxr-xr-xtools/testing/selftests/drivers/net/hw/iou-zcrx.py38
-rw-r--r--tools/testing/selftests/drivers/net/hw/lib/py/__init__.py4
-rw-r--r--tools/testing/selftests/drivers/net/hw/ncdevmem.c58
-rwxr-xr-xtools/testing/selftests/drivers/net/hw/nk_devmem.py46
-rw-r--r--tools/testing/selftests/drivers/net/hw/nk_primary_rx_redirect.bpf.c39
-rwxr-xr-xtools/testing/selftests/drivers/net/hw/nk_qlease.py306
-rwxr-xr-xtools/testing/selftests/drivers/net/hw/ntuple.py5
-rwxr-xr-xtools/testing/selftests/drivers/net/hw/rss_ctx.py98
-rwxr-xr-xtools/testing/selftests/drivers/net/hw/tso.py19
-rwxr-xr-xtools/testing/selftests/drivers/net/hw/userns_devmem.py49
-rw-r--r--tools/testing/selftests/drivers/net/lib/py/__init__.py9
-rw-r--r--tools/testing/selftests/drivers/net/lib/py/env.py160
-rwxr-xr-xtools/testing/selftests/drivers/net/netconsole/netcons_cmdline.sh2
-rwxr-xr-xtools/testing/selftests/drivers/net/netconsole/netcons_fragmented_msg.sh4
-rwxr-xr-xtools/testing/selftests/drivers/net/netconsole/netcons_resume.sh5
-rwxr-xr-xtools/testing/selftests/drivers/net/netconsole/netcons_sysdata.sh2
-rwxr-xr-xtools/testing/selftests/drivers/net/psp.py423
-rw-r--r--tools/testing/selftests/drivers/net/settings1
-rw-r--r--tools/testing/selftests/drivers/net/so_txtime.c538
-rwxr-xr-xtools/testing/selftests/drivers/net/so_txtime.py118
33 files changed, 2572 insertions, 244 deletions
diff --git a/tools/testing/selftests/drivers/net/.gitignore b/tools/testing/selftests/drivers/net/.gitignore
index 585ecb4d5dc4..e5314ce4bb2d 100644
--- a/tools/testing/selftests/drivers/net/.gitignore
+++ b/tools/testing/selftests/drivers/net/.gitignore
@@ -1,3 +1,4 @@
# SPDX-License-Identifier: GPL-2.0-only
napi_id_helper
psp_responder
+so_txtime
diff --git a/tools/testing/selftests/drivers/net/Makefile b/tools/testing/selftests/drivers/net/Makefile
index b72080c6d06b..d5bf4cb638a8 100644
--- a/tools/testing/selftests/drivers/net/Makefile
+++ b/tools/testing/selftests/drivers/net/Makefile
@@ -7,6 +7,7 @@ TEST_INCLUDES := $(wildcard lib/py/*.py) \
TEST_GEN_FILES := \
napi_id_helper \
+ so_txtime \
# end of TEST_GEN_FILES
TEST_PROGS := \
@@ -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/bonding/Makefile b/tools/testing/selftests/drivers/net/bonding/Makefile
index 9af5f84edd37..6364ca02642d 100644
--- a/tools/testing/selftests/drivers/net/bonding/Makefile
+++ b/tools/testing/selftests/drivers/net/bonding/Makefile
@@ -8,10 +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_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/config b/tools/testing/selftests/drivers/net/config
index fd16994366f4..2070e890e064 100644
--- a/tools/testing/selftests/drivers/net/config
+++ b/tools/testing/selftests/drivers/net/config
@@ -4,9 +4,17 @@ CONFIG_DEBUG_INFO_BTF_MODULES=n
CONFIG_INET_PSP=y
CONFIG_IPV6=y
CONFIG_MACSEC=m
+CONFIG_NET_CLS_ACT=y
+CONFIG_NET_CLS_BPF=y
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_PPP=y
+CONFIG_PPPOE=y
CONFIG_VLAN_8021Q=m
CONFIG_XDP_SOCKETS=y
diff --git a/tools/testing/selftests/drivers/net/gro.py b/tools/testing/selftests/drivers/net/gro.py
index 221f27e57147..6ab8c97880d1 100755
--- a/tools/testing/selftests/drivers/net/gro.py
+++ b/tools/testing/selftests/drivers/net/gro.py
@@ -40,7 +40,7 @@ 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, KsftNamedVariant
@@ -132,11 +132,21 @@ def _get_queue_stats(cfg, queue_id):
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-*"))
@@ -313,6 +323,12 @@ 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", "ip6ip6"]:
for test_name in common_tests:
@@ -325,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):
@@ -349,6 +370,12 @@ def test(cfg, mode, protocol, test_name):
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
diff --git a/tools/testing/selftests/drivers/net/hw/Makefile b/tools/testing/selftests/drivers/net/hw/Makefile
index 82809d5b2478..fd0535a96d84 100644
--- a/tools/testing/selftests/drivers/net/hw/Makefile
+++ b/tools/testing/selftests/drivers/net/hw/Makefile
@@ -35,6 +35,7 @@ TEST_PROGS = \
irq.py \
loopback.sh \
nic_timestamp.py \
+ nk_devmem.py \
nk_netns.py \
nk_qlease.py \
ntuple.py \
@@ -46,12 +47,14 @@ TEST_PROGS = \
rss_input_xfrm.py \
toeplitz.py \
tso.py \
+ userns_devmem.py \
uso.py \
xdp_metadata.py \
xsk_reconfig.py \
#
TEST_FILES := \
+ devmem_lib.py \
ethtool_lib.sh \
#
diff --git a/tools/testing/selftests/drivers/net/hw/config b/tools/testing/selftests/drivers/net/hw/config
index 8c132ace2b8d..ed8642b68094 100644
--- a/tools/testing/selftests/drivers/net/hw/config
+++ b/tools/testing/selftests/drivers/net/hw/config
@@ -3,6 +3,7 @@ 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
@@ -10,12 +11,16 @@ CONFIG_INET_ESP_OFFLOAD=y
CONFIG_IO_URING=y
CONFIG_IPV6=y
CONFIG_IPV6_GRE=y
+CONFIG_IPV6_SIT=y
+CONFIG_IPV6_TUNNEL=y
CONFIG_NET_CLS_ACT=y
CONFIG_NET_CLS_BPF=y
CONFIG_NET_IPGRE=y
CONFIG_NET_IPGRE_DEMUX=y
+CONFIG_NET_IPIP=y
CONFIG_NETKIT=y
CONFIG_NET_SCH_INGRESS=y
CONFIG_UDMABUF=y
+CONFIG_USER_NS=y
CONFIG_VXLAN=y
CONFIG_XFRM_USER=y
diff --git a/tools/testing/selftests/drivers/net/hw/devmem.py b/tools/testing/selftests/drivers/net/hw/devmem.py
index ee863e90d1e0..031cf9905f65 100755
--- a/tools/testing/selftests/drivers/net/hw/devmem.py
+++ b/tools/testing/selftests/drivers/net/hw/devmem.py
@@ -2,91 +2,40 @@
# 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
+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}"
-
- 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}")
+ """Run the HDS test."""
+ run_rx_hds(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)
-
+ setup_test(cfg, path.abspath(path.dirname(__file__) + "/ncdevmem"))
ksft_run([check_rx, check_tx, check_tx_chunks, check_rx_hds],
- args=(cfg, ))
+ 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..0921ff03eb81
--- /dev/null
+++ b/tools/testing/selftests/drivers/net/hw/devmem_lib.py
@@ -0,0 +1,222 @@
+# SPDX-License-Identifier: GPL-2.0
+"""Shared helpers for devmem TCP selftests."""
+
+import re
+
+from lib.py import (bkg, cmd, defer, ethtool, rand_port, wait_port_listen,
+ ksft_eq, KsftSkipEx, NetNSEnter, EthtoolFamily,
+ NetdevFamily)
+
+
+def require_devmem(cfg):
+ """Probe ncdevmem on cfg.ifname and SKIP the test if devmem isn't supported."""
+ if not hasattr(cfg, "devmem_probed"):
+ probe_command = f"{cfg.bin_local} -f {cfg.ifname}"
+ cfg.devmem_supported = cmd(probe_command, fail=False, shell=True).ret == 0
+ cfg.devmem_probed = True
+
+ if not cfg.devmem_supported:
+ raise KsftSkipEx("Test requires devmem support")
+
+
+def configure_nic(cfg):
+ """Channels, rings, RSS, queue lease for netkit devmem."""
+ if not hasattr(cfg, 'netns'):
+ return
+
+ cfg.require_ipver('6')
+ ethnl = EthtoolFamily()
+
+ channels = ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
+ channels = channels['combined-count']
+ if channels < 2:
+ raise KsftSkipEx(
+ 'Test requires NETIF with at least 2 combined channels'
+ )
+
+ rings = ethnl.rings_get({'header': {'dev-index': cfg.ifindex}})
+ orig_rx_rings = rings['rx']
+ orig_hds_thresh = rings.get('hds-thresh', 0)
+ orig_data_split = rings.get('tcp-data-split', 'unknown')
+
+ ethnl.rings_set({'header': {'dev-index': cfg.ifindex},
+ 'tcp-data-split': 'enabled',
+ 'hds-thresh': 0,
+ 'rx': min(64, orig_rx_rings)})
+ defer(ethnl.rings_set, {'header': {'dev-index': cfg.ifindex},
+ 'tcp-data-split': orig_data_split,
+ 'hds-thresh': orig_hds_thresh,
+ 'rx': orig_rx_rings})
+
+ cfg.src_queue = channels - 1
+ ethtool(f"-X {cfg.ifname} equal {cfg.src_queue}")
+ defer(ethtool, f"-X {cfg.ifname} default")
+
+ if not hasattr(cfg, 'nk_queue'):
+ with NetNSEnter(str(cfg.netns)):
+ netdevnl = NetdevFamily()
+ lease_result = netdevnl.queue_create({
+ "ifindex": cfg.nk_guest_ifindex,
+ "type": "rx",
+ "lease": {
+ "ifindex": cfg.ifindex,
+ "queue": {"id": cfg.src_queue, "type": "rx"},
+ "netns-id": 0,
+ },
+ })
+ cfg.nk_queue = lease_result['id']
+
+
+def set_flow_rule(cfg, port):
+ """Install a flow rule steering to src_queue and return the flow rule ID."""
+ output = ethtool(
+ f"-N {cfg.ifname} flow-type tcp6 dst-port {port}"
+ f" action {cfg.src_queue}"
+ ).stdout
+ return int(re.search(r'ID (\d+)', output).group(1))
+
+
+def ncdevmem_rx(cfg, port, verify=True, fail_on_linear=False, flow_steer=False):
+ """Build the ncdevmem RX listener command."""
+ if hasattr(cfg, 'netns'):
+ flow_rule_id = set_flow_rule(cfg, port)
+ defer(ethtool, f"-N {cfg.ifname} delete {flow_rule_id}")
+
+ ifname = cfg.nk_guest_ifname
+ addr = cfg.nk_guest_ipv6
+ extras = [f"-t {cfg.nk_queue}", "-q 1", "-n"]
+ else:
+ ifname = cfg.ifname
+ addr = cfg.addr
+ extras = []
+ if flow_steer:
+ extras.append(f"-c {cfg.remote_addr}")
+
+ if verify:
+ extras.append("-v 7")
+ if fail_on_linear:
+ extras.append("-L")
+
+ parts = [cfg.bin_local, "-l", f"-f {ifname}", f"-s {addr}",
+ f"-p {port}", *extras]
+ return " ".join(parts)
+
+
+def ncdevmem_tx(cfg, port, chunk_size=0):
+ """Build the ncdevmem TX send command."""
+ if hasattr(cfg, 'netns'):
+ ifname = cfg.nk_guest_ifname
+ addr = cfg.remote_addr_v['6']
+ extras = ["-t 0", "-q 1", "-n"]
+ else:
+ ifname = cfg.ifname
+ addr = cfg.remote_addr
+ extras = []
+
+ if chunk_size:
+ extras.append(f"-z {chunk_size}")
+
+ parts = [cfg.bin_local, f"-f {ifname}", f"-s {addr}",
+ f"-p {port}", *extras]
+ return " ".join(parts)
+
+
+def socat_send(cfg, port, buf_size=0):
+ """Socat command for sending to the devmem listener.
+
+ When buf_size > 0, force one TCP segment per write of exactly that size by
+ setting socat's buffer (-b) and disabling Nagle (TCP_NODELAY).
+ """
+ proto = f"TCP{cfg.addr_ipver}"
+
+ if hasattr(cfg, 'netns'):
+ addr = f"[{cfg.nk_guest_ipv6}]"
+ else:
+ addr = cfg.baddr
+
+ suffix = f",bind={cfg.remote_baddr}:{port}"
+
+ buf = ""
+ if buf_size:
+ buf = f"-b {buf_size}"
+ suffix += ",nodelay"
+
+ return f"socat {buf} -u - {proto}:{addr}:{port}{suffix}"
+
+
+def socat_listen(cfg, port):
+ """Socat listen command for TX tests."""
+ return f"socat -U - TCP{cfg.addr_ipver}-LISTEN:{port}"
+
+
+def setup_test(cfg, bin_local):
+ """Stash the local ncdevmem path on cfg and deploy it to the remote."""
+ cfg.bin_local = bin_local
+ cfg.bin_remote = cfg.remote.deploy(cfg.bin_local)
+
+
+def run_rx(cfg):
+ """Run the devmem RX test."""
+ require_devmem(cfg)
+ configure_nic(cfg)
+ port = rand_port()
+ socat = socat_send(cfg, port)
+ data_pipe = (f"yes $(echo -e \x01\x02\x03\x04\x05\x06) | head -c 1K"
+ f" | {socat}")
+ netns = getattr(cfg, "netns", None)
+
+ listen_cmd = ncdevmem_rx(cfg, port, flow_steer=not hasattr(cfg, 'netns'))
+ with bkg(listen_cmd, exit_wait=True, ns=netns) as ncdevmem:
+ wait_port_listen(port, proto="tcp", ns=netns)
+ cmd(data_pipe, host=cfg.remote, shell=True)
+ ksft_eq(ncdevmem.ret, 0)
+
+
+def run_tx(cfg):
+ """Run the devmem TX test."""
+ require_devmem(cfg)
+ configure_nic(cfg)
+ netns = getattr(cfg, "netns", None)
+ port = rand_port()
+ tx_cmd = ncdevmem_tx(cfg, port)
+ listen_cmd = socat_listen(cfg, port)
+
+ with bkg(listen_cmd, host=cfg.remote, exit_wait=True) as socat:
+ wait_port_listen(port, host=cfg.remote)
+ cmd(f"bash -c 'echo -e \"hello\\nworld\" | {tx_cmd}'", ns=netns, shell=True)
+ ksft_eq(socat.stdout.strip(), "hello\nworld")
+
+
+def run_tx_chunks(cfg):
+ """Run the devmem TX chunking test."""
+ require_devmem(cfg)
+ configure_nic(cfg)
+ netns = getattr(cfg, "netns", None)
+ port = rand_port()
+ tx_cmd = ncdevmem_tx(cfg, port, chunk_size=3)
+ listen_cmd = socat_listen(cfg, port)
+
+ with bkg(listen_cmd, host=cfg.remote, exit_wait=True) as socat:
+ wait_port_listen(port, host=cfg.remote)
+ cmd(f"bash -c 'echo -e \"hello\\nworld\" | {tx_cmd}'", ns=netns, shell=True)
+ ksft_eq(socat.stdout.strip(), "hello\nworld")
+
+
+def run_rx_hds(cfg):
+ """Run the HDS test by running devmem RX across a segment size sweep."""
+ require_devmem(cfg)
+ configure_nic(cfg)
+ netns = getattr(cfg, "netns", None)
+
+ for size in [1, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192]:
+ port = rand_port()
+
+ listen_cmd = ncdevmem_rx(cfg, port, verify=False,
+ fail_on_linear=True)
+ socat = socat_send(cfg, port, buf_size=size)
+
+ with bkg(listen_cmd, exit_wait=True, ns=netns) as ncdevmem:
+ wait_port_listen(port, proto="tcp", ns=netns)
+ cmd(f"dd if=/dev/zero bs={size} count=1 2>/dev/null | "
+ f"{socat}", host=cfg.remote, shell=True)
+ ksft_eq(ncdevmem.ret, 0, f"HDS failed for payload size {size}")
diff --git a/tools/testing/selftests/drivers/net/hw/gro_hw.py b/tools/testing/selftests/drivers/net/hw/gro_hw.py
index 10e08b22ee0e..70e76e3888bd 100755
--- a/tools/testing/selftests/drivers/net/hw/gro_hw.py
+++ b/tools/testing/selftests/drivers/net/hw/gro_hw.py
@@ -51,11 +51,21 @@ def _resolve_dmac(cfg, ipver):
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-*"))
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 e81724cb5542..b7a225fe4bea 100755
--- a/tools/testing/selftests/drivers/net/hw/iou-zcrx.py
+++ b/tools/testing/selftests/drivers/net/hw/iou-zcrx.py
@@ -2,6 +2,7 @@
# 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
@@ -41,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']
@@ -100,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}"
@@ -121,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"
@@ -134,6 +163,7 @@ def test_zcrx_large_chunks(cfg) -> None:
"""Test zcrx with large buffer chunks."""
cfg.require_ipver('6')
+ _require_ntuple(cfg)
hp_file = "/proc/sys/vm/nr_hugepages"
with open(hp_file, 'r+', encoding='utf-8') as f:
@@ -144,7 +174,10 @@ def test_zcrx_large_chunks(cfg) -> None:
defer(lambda: open(hp_file, 'w', encoding='utf-8').write(str(nr_hugepages)))
single(cfg)
- rx_cmd = f"{cfg.bin_local} -s -p {cfg.port} -i {cfg.ifname} -q {cfg.target} -x 2"
+ 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)
@@ -154,6 +187,9 @@ def test_zcrx_large_chunks(cfg) -> None:
mp_clear_wait(cfg)
with bkg(rx_cmd, exit_wait=True):
wait_port_listen(cfg.port, proto="tcp")
+
+ check_iou_rx_buf_len(cfg, rx_buf_len)
+
cmd(tx_cmd, host=cfg.remote)
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 84a4dab6c649..8a58cb17cc06 100644
--- a/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
+++ b/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
@@ -18,7 +18,7 @@ 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, Netlink
from net.lib.py import CmdExitFailure
@@ -34,7 +34,7 @@ try:
from drivers.net.lib.py import GenerateTraffic, Remote, Iperf3Runner
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", "Netlink",
"CmdExitFailure",
diff --git a/tools/testing/selftests/drivers/net/hw/ncdevmem.c b/tools/testing/selftests/drivers/net/hw/ncdevmem.c
index e098d6534c3c..d96e8a3b5a65 100644
--- a/tools/testing/selftests/drivers/net/hw/ncdevmem.c
+++ b/tools/testing/selftests/drivers/net/hw/ncdevmem.c
@@ -93,6 +93,7 @@ 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;
@@ -828,7 +829,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 +852,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 +1055,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 +1411,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:n")) != -1) {
switch (opt) {
case 'L':
fail_on_linear = true;
@@ -1436,6 +1443,9 @@ int main(int argc, char *argv[])
case 'z':
max_chunk = atoi(optarg);
break;
+ case 'n':
+ skip_config = 1;
+ 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..300ed2a70ab4
--- /dev/null
+++ b/tools/testing/selftests/drivers/net/hw/nk_devmem.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""Test devmem TCP with netkit."""
+
+import os
+from devmem_lib import setup_test, run_rx, run_tx, run_tx_chunks, run_rx_hds
+from lib.py import ksft_run, ksft_exit, ksft_disruptive
+from lib.py import NetDrvContEnv
+
+
+@ksft_disruptive
+def check_nk_rx(cfg) -> None:
+ """Run the devmem RX test through netkit."""
+ run_rx(cfg)
+
+
+@ksft_disruptive
+def check_nk_tx(cfg) -> None:
+ """Run the devmem TX test through netkit."""
+ run_tx(cfg)
+
+
+@ksft_disruptive
+def check_nk_tx_chunks(cfg) -> None:
+ """Run the devmem TX chunking test through netkit."""
+ run_tx_chunks(cfg)
+
+
+def check_nk_rx_hds(cfg) -> None:
+ """Run the HDS test through netkit."""
+ run_rx_hds(cfg)
+
+
+def main() -> None:
+ """Run the netkit devmem test cases."""
+ with NetDrvContEnv(__file__, rxqueues=2, primary_rx_redirect=True) as cfg:
+ setup_test(cfg,
+ os.path.join(os.path.dirname(os.path.abspath(__file__)),
+ "ncdevmem"))
+ ksft_run([check_nk_rx, check_nk_tx, check_nk_tx_chunks,
+ check_nk_rx_hds], args=(cfg,))
+ ksft_exit()
+
+
+if __name__ == "__main__":
+ main()
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
index aa83dc321328..4f53034c9a50 100755
--- a/tools/testing/selftests/drivers/net/hw/nk_qlease.py
+++ b/tools/testing/selftests/drivers/net/hw/nk_qlease.py
@@ -18,8 +18,10 @@ from lib.py import (
NetNSEnter,
EthtoolFamily,
NetdevFamily,
+ RtnlFamily,
)
from lib.py import (
+ Netlink,
bkg,
cmd,
defer,
@@ -30,10 +32,138 @@ from lib.py import (
)
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 set_flow_rule(cfg):
+
+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 {cfg.src_queue}"
+ 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)
@@ -41,6 +171,8 @@ def set_flow_rule(cfg):
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}})
@@ -65,40 +197,121 @@ def test_iou_zcrx(cfg) -> None:
},
)
- ethtool(f"-X {cfg.ifname} equal {cfg.src_queue}")
+ ethtool(f"-X {cfg.ifname} equal {src_queue}")
defer(ethtool, f"-X {cfg.ifname} default")
- flow_rule_id = set_flow_rule(cfg)
+ flow_rule_id = set_flow_rule(cfg, src_queue)
defer(ethtool, f"-N {cfg.ifname} delete {flow_rule_id}")
- rx_cmd = f"ip netns exec {cfg.netns.name} {cfg.bin_local} -s -p {cfg.port} -i {cfg._nk_guest_ifname} -q {cfg.nk_queue}"
+ 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):
+ 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": cfg.src_queue, "type": "rx"}
+ {"ifindex": cfg.ifindex, "id": src_queue, "type": "rx"}
)
- ksft_eq(queue_info["id"], cfg.src_queue)
+ 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"], cfg.nk_queue)
+ 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}})
@@ -123,18 +336,21 @@ def test_attach_xdp_with_mp(cfg) -> None:
},
)
- ethtool(f"-X {cfg.ifname} equal {cfg.src_queue}")
+ ethtool(f"-X {cfg.ifname} equal {src_queue}")
defer(ethtool, f"-X {cfg.ifname} default")
netdevnl = NetdevFamily()
- rx_cmd = f"ip netns exec {cfg.netns.name} {cfg.bin_local} -s -p {cfg.port} -i {cfg._nk_guest_ifname} -q {cfg.nk_queue}"
- with bkg(rx_cmd):
+ 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": cfg.src_queue, "type": "rx"}
+ {"ifindex": cfg.ifindex, "id": src_queue, "type": "rx"}
)
ksft_in("io-uring", queue_info)
@@ -144,13 +360,15 @@ def test_attach_xdp_with_mp(cfg) -> None:
time.sleep(0.1)
queue_info = netdevnl.queue_get(
- {"ifindex": cfg.ifindex, "id": cfg.src_queue, "type": "rx"}
+ {"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}})
@@ -175,16 +393,19 @@ def test_destroy(cfg) -> None:
},
)
- ethtool(f"-X {cfg.ifname} equal {cfg.src_queue}")
+ ethtool(f"-X {cfg.ifname} equal {src_queue}")
defer(ethtool, f"-X {cfg.ifname} default")
- rx_cmd = f"ip netns exec {cfg.netns.name} {cfg.bin_local} -s -p {cfg.port} -i {cfg._nk_guest_ifname} -q {cfg.nk_queue}"
- rx_proc = cmd(rx_cmd, background=True)
+ 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": cfg.src_queue, "type": "rx"}
+ {"ifindex": cfg.ifindex, "id": src_queue, "type": "rx"}
)
ksft_in("io-uring", queue_info)
@@ -193,23 +414,20 @@ def test_destroy(cfg) -> None:
kill_timer = threading.Timer(1, rx_proc.proc.terminate)
kill_timer.start()
- ip(f"link del dev {cfg._nk_host_ifname}")
+ ip(f"link del dev {cfg.nk_host_ifname}")
kill_timer.join()
- cfg._nk_host_ifname = None
- cfg._nk_guest_ifname = None
+ cfg.nk_host_ifname = None
+ cfg.nk_guest_ifname = None
queue_info = netdevnl.queue_get(
- {"ifindex": cfg.ifindex, "id": cfg.src_queue, "type": "rx"}
+ {"ifindex": cfg.ifindex, "id": src_queue, "type": "rx"}
)
ksft_not_in("io-uring", queue_info)
- cmd(f"tc filter del dev {cfg.ifname} ingress pref {cfg._bpf_prog_pref}")
- cfg._tc_attached = False
-
- flow_rule_id = set_flow_rule(cfg)
+ 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 {cfg.src_queue}"
+ 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")
@@ -217,7 +435,7 @@ def test_destroy(cfg) -> None:
# 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": cfg.src_queue, "type": "rx"}
+ {"ifindex": cfg.ifindex, "id": src_queue, "type": "rx"}
)
ksft_not_in("io-uring", queue_info)
@@ -230,32 +448,14 @@ def main() -> None:
cfg.bin_remote = cfg.remote.deploy(cfg.bin_local)
cfg.port = rand_port()
- 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")
-
- cfg.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": cfg.src_queue, "type": "rx"},
- "netns-id": 0,
- },
- }
- )
- cfg.nk_queue = bind_result["id"]
-
- # test_destroy must be last because it destroys the netkit devices
ksft_run(
- [test_iou_zcrx, test_attrs, test_attach_xdp_with_mp, test_destroy],
+ [
+ test_iou_zcrx,
+ test_iou_zcrx_large_buf,
+ test_attrs,
+ test_attach_xdp_with_mp,
+ test_destroy,
+ ],
args=(cfg,),
)
ksft_exit()
diff --git a/tools/testing/selftests/drivers/net/hw/ntuple.py b/tools/testing/selftests/drivers/net/hw/ntuple.py
index 232733142c02..ef4604bfa8ef 100755
--- a/tools/testing/selftests/drivers/net/hw/ntuple.py
+++ b/tools/testing/selftests/drivers/net/hw/ntuple.py
@@ -22,7 +22,10 @@ class NtupleField(Enum):
def _require_ntuple(cfg):
features = ethtool(f"-k {cfg.ifname}", json=True)[0]
if not features["ntuple-filters"]["active"]:
- 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 _get_rx_cnts(cfg, prev=None):
diff --git a/tools/testing/selftests/drivers/net/hw/rss_ctx.py b/tools/testing/selftests/drivers/net/hw/rss_ctx.py
index 51f4e7bc3e5d..f36f76d6ca59 100755
--- a/tools/testing/selftests/drivers/net/hw/rss_ctx.py
+++ b/tools/testing/selftests/drivers/net/hw/rss_ctx.py
@@ -9,7 +9,7 @@ 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 rand_port, rand_ports
from lib.py import cmd, ethtool, ip, defer, CmdExitFailure, wait_file
@@ -57,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):
@@ -828,6 +829,94 @@ def test_rss_default_context_rule(cfg):
'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):
"""
@@ -935,6 +1024,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/tso.py b/tools/testing/selftests/drivers/net/hw/tso.py
index bb675e3dac88..802bb4868046 100755
--- a/tools/testing/selftests/drivers/net/hw/tso.py
+++ b/tools/testing/selftests/drivers/net/hw/tso.py
@@ -187,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)
@@ -239,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/lib/py/__init__.py b/tools/testing/selftests/drivers/net/lib/py/__init__.py
index 2b5ec0505672..ee903bcf3207 100644
--- a/tools/testing/selftests/drivers/net/lib/py/__init__.py
+++ b/tools/testing/selftests/drivers/net/lib/py/__init__.py
@@ -18,12 +18,13 @@ 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, Netlink
from net.lib.py import CmdExitFailure
from net.lib.py import bkg, cmd, bpftool, bpftrace, defer, ethtool, \
- fd_read_timeout, ip, rand_port, rand_ports, wait_port_listen, wait_file
+ 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, \
@@ -31,12 +32,12 @@ try:
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", "Netlink",
"CmdExitFailure",
"bkg", "cmd", "bpftool", "bpftrace", "defer", "ethtool",
- "fd_read_timeout", "ip", "rand_port", "rand_ports",
+ "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",
diff --git a/tools/testing/selftests/drivers/net/lib/py/env.py b/tools/testing/selftests/drivers/net/lib/py/env.py
index 24ce122abd9c..e4ab99b905b1 100644
--- a/tools/testing/selftests/drivers/net/lib/py/env.py
+++ b/tools/testing/selftests/drivers/net/lib/py/env.py
@@ -2,13 +2,14 @@
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 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
@@ -336,15 +337,20 @@ class NetDrvContEnv(NetDrvEpEnv):
+---------------+
"""
- def __init__(self, src_path, rxqueues=1, **kwargs):
+ def __init__(self, src_path, rxqueues=1, primary_rx_redirect=False,
+ userns=False, **kwargs):
self.netns = None
- self._nk_host_ifname = None
- self._nk_guest_ifname = 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
@@ -389,15 +395,25 @@ class NetDrvContEnv(NetDrvEpEnv):
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_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()
+ 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
@@ -406,10 +422,15 @@ class NetDrvContEnv(NetDrvEpEnv):
cmd(f"tc qdisc del dev {self.ifname} clsact")
self._tc_clsact_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._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)
@@ -444,28 +465,37 @@ class NetDrvContEnv(NetDrvEpEnv):
with open(ra_path, "w", encoding="utf-8") as f:
f.write("2")
- self.netns = NetNS()
+ 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}")
- 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(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)
+ 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):
- qdisc = json.loads(cmd(f"tc -j qdisc show dev {self.ifname}").stdout)
+ 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
- cmd(f"tc qdisc add dev {self.ifname} clsact")
- self._tc_clsact_added = True
+ 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)
@@ -476,28 +506,43 @@ class NetDrvContEnv(NetDrvEpEnv):
return (bpf['pref'], bpf['options']['prog']['id'])
raise Exception("Failed to get BPF prog ID")
- def _attach_bpf(self):
- bpf_obj = self.test_dir / "nk_forward.bpf.o"
- if not bpf_obj.exists():
- raise KsftSkipEx("BPF prog not found")
+ 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")
- self._tc_ensure_clsact()
+ 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()
- prog_info = bpftool(f"prog show id {self._bpf_prog_id}", json=True)
- map_ids = prog_info.get("map_ids", [])
-
- bss_map_id = None
- for map_id in map_ids:
- map_info = bpftool(f"map show id {map_id}", json=True)
- if map_info.get("name").endswith("bss"):
- bss_map_id = map_id
-
- if bss_map_id is None:
- raise Exception("Failed to find .bss map")
+ bss_map_id = self._find_bss_map_id(self._bpf_prog_id)
ipv6_addr = ipaddress.IPv6Address(self.ipv6_prefix)
ipv6_bytes = ipv6_addr.packed
@@ -505,3 +550,36 @@ class NetDrvContEnv(NetDrvEpEnv):
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/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/psp.py b/tools/testing/selftests/drivers/net/psp.py
index 864d9fce1094..315648a770d0 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,13 @@ 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
def _get_outq(s):
@@ -117,11 +122,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
@@ -571,33 +578,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)
- test_case.__name__ = f"{name}_v{psp_ver}_ip{ipver}"
- return test_case
+def _get_ip_variants():
+ for ipv in ("4", "6"):
+ yield KsftNamedVariant(f"ip{ipv}", ipv)
-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_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)
+
+
+def _get_psp_ver_ip6_variants():
+ for ver in range(4):
+ yield KsftNamedVariant(f"v{ver}_ip6", ver, "6")
+
+
+@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 +973,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/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/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..adf6c848d6d8
--- /dev/null
+++ b/tools/testing/selftests/drivers/net/so_txtime.py
@@ -0,0 +1,118 @@
+#!/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
+
+
+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} {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}")
+ tc(f"qdisc replace dev {ifname} root {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)
+ try:
+ _qdisc_setup(cfg.ifname, "etf", "clockid CLOCK_TAI delta 400000")
+ except Exception as e:
+ raise KsftSkipEx("tc does not support qdisc etf. skipping") from e
+
+ 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()