Resguardo inicial

This commit is contained in:
lastdragon
2026-09-06 12:53:21 -06:00
parent 662efe3c2a
commit ad2088d178
94853 changed files with 43200404 additions and 0 deletions
@@ -0,0 +1,3 @@
# SPDX-License-Identifier: GPL-2.0-only
/dma-buf/udmabuf
/s390x/uvdevice/test_uvdevice
@@ -0,0 +1,8 @@
# SPDX-License-Identifier: GPL-2.0-only
CFLAGS += $(KHDR_INCLUDES)
TEST_GEN_PROGS := udmabuf
top_srcdir ?=../../../../..
include ../../lib.mk
@@ -0,0 +1 @@
CONFIG_UDMABUF=y
@@ -0,0 +1,277 @@
// SPDX-License-Identifier: GPL-2.0
#define _GNU_SOURCE
#define __EXPORTED_HEADERS__
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <malloc.h>
#include <stdbool.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <sys/mman.h>
#include <linux/memfd.h>
#include <linux/udmabuf.h>
#include "kselftest.h"
#define TEST_PREFIX "drivers/dma-buf/udmabuf"
#define NUM_PAGES 4
#define NUM_ENTRIES 4
#define MEMFD_SIZE 1024 /* in pages */
static unsigned int page_size;
static int create_memfd_with_seals(off64_t size, bool hpage)
{
int memfd, ret;
unsigned int flags = MFD_ALLOW_SEALING;
if (hpage)
flags |= MFD_HUGETLB;
memfd = memfd_create("udmabuf-test", flags);
if (memfd < 0) {
ksft_print_msg("%s: [skip,no-memfd]\n", TEST_PREFIX);
exit(KSFT_SKIP);
}
ret = fcntl(memfd, F_ADD_SEALS, F_SEAL_SHRINK);
if (ret < 0) {
ksft_print_msg("%s: [skip,fcntl-add-seals]\n", TEST_PREFIX);
exit(KSFT_SKIP);
}
ret = ftruncate(memfd, size);
if (ret == -1) {
ksft_print_msg("%s: [FAIL,memfd-truncate]\n", TEST_PREFIX);
exit(KSFT_FAIL);
}
return memfd;
}
static int create_udmabuf_list(int devfd, int memfd, off64_t memfd_size)
{
struct udmabuf_create_list *list;
int ubuf_fd, i;
list = malloc(sizeof(struct udmabuf_create_list) +
sizeof(struct udmabuf_create_item) * NUM_ENTRIES);
if (!list) {
ksft_print_msg("%s: [FAIL, udmabuf-malloc]\n", TEST_PREFIX);
exit(KSFT_FAIL);
}
for (i = 0; i < NUM_ENTRIES; i++) {
list->list[i].memfd = memfd;
list->list[i].offset = i * (memfd_size / NUM_ENTRIES);
list->list[i].size = getpagesize() * NUM_PAGES;
}
list->count = NUM_ENTRIES;
list->flags = UDMABUF_FLAGS_CLOEXEC;
ubuf_fd = ioctl(devfd, UDMABUF_CREATE_LIST, list);
free(list);
if (ubuf_fd < 0) {
ksft_print_msg("%s: [FAIL, udmabuf-create]\n", TEST_PREFIX);
exit(KSFT_FAIL);
}
return ubuf_fd;
}
static void write_to_memfd(void *addr, off64_t size, char chr)
{
int i;
for (i = 0; i < size / page_size; i++) {
*((char *)addr + (i * page_size)) = chr;
}
}
static void *mmap_fd(int fd, off64_t size)
{
void *addr;
addr = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) {
ksft_print_msg("%s: ubuf_fd mmap fail\n", TEST_PREFIX);
exit(KSFT_FAIL);
}
return addr;
}
static int compare_chunks(void *addr1, void *addr2, off64_t memfd_size)
{
off64_t off;
int i = 0, j, k = 0, ret = 0;
char char1, char2;
while (i < NUM_ENTRIES) {
off = i * (memfd_size / NUM_ENTRIES);
for (j = 0; j < NUM_PAGES; j++, k++) {
char1 = *((char *)addr1 + off + (j * getpagesize()));
char2 = *((char *)addr2 + (k * getpagesize()));
if (char1 != char2) {
ret = -1;
goto err;
}
}
i++;
}
err:
munmap(addr1, memfd_size);
munmap(addr2, NUM_ENTRIES * NUM_PAGES * getpagesize());
return ret;
}
int main(int argc, char *argv[])
{
struct udmabuf_create create;
int devfd, memfd, buf, ret;
off64_t size;
void *addr1, *addr2;
ksft_print_header();
ksft_set_plan(7);
devfd = open("/dev/udmabuf", O_RDWR);
if (devfd < 0) {
ksft_print_msg(
"%s: [skip,no-udmabuf: Unable to access DMA buffer device file]\n",
TEST_PREFIX);
exit(KSFT_SKIP);
}
memfd = memfd_create("udmabuf-test", MFD_ALLOW_SEALING);
if (memfd < 0) {
ksft_print_msg("%s: [skip,no-memfd]\n", TEST_PREFIX);
exit(KSFT_SKIP);
}
ret = fcntl(memfd, F_ADD_SEALS, F_SEAL_SHRINK);
if (ret < 0) {
ksft_print_msg("%s: [skip,fcntl-add-seals]\n", TEST_PREFIX);
exit(KSFT_SKIP);
}
size = getpagesize() * NUM_PAGES;
ret = ftruncate(memfd, size);
if (ret == -1) {
ksft_print_msg("%s: [FAIL,memfd-truncate]\n", TEST_PREFIX);
exit(KSFT_FAIL);
}
memset(&create, 0, sizeof(create));
/* should fail (offset not page aligned) */
create.memfd = memfd;
create.offset = getpagesize()/2;
create.size = getpagesize();
buf = ioctl(devfd, UDMABUF_CREATE, &create);
if (buf >= 0)
ksft_test_result_fail("%s: [FAIL,test-1]\n", TEST_PREFIX);
else
ksft_test_result_pass("%s: [PASS,test-1]\n", TEST_PREFIX);
/* should fail (size not multiple of page) */
create.memfd = memfd;
create.offset = 0;
create.size = getpagesize()/2;
buf = ioctl(devfd, UDMABUF_CREATE, &create);
if (buf >= 0)
ksft_test_result_fail("%s: [FAIL,test-2]\n", TEST_PREFIX);
else
ksft_test_result_pass("%s: [PASS,test-2]\n", TEST_PREFIX);
/* should fail (not memfd) */
create.memfd = 0; /* stdin */
create.offset = 0;
create.size = size;
buf = ioctl(devfd, UDMABUF_CREATE, &create);
if (buf >= 0)
ksft_test_result_fail("%s: [FAIL,test-3]\n", TEST_PREFIX);
else
ksft_test_result_pass("%s: [PASS,test-3]\n", TEST_PREFIX);
/* should work */
page_size = getpagesize();
addr1 = mmap_fd(memfd, size);
write_to_memfd(addr1, size, 'a');
create.memfd = memfd;
create.offset = 0;
create.size = size;
buf = ioctl(devfd, UDMABUF_CREATE, &create);
if (buf < 0)
ksft_test_result_fail("%s: [FAIL,test-4]\n", TEST_PREFIX);
else
ksft_test_result_pass("%s: [PASS,test-4]\n", TEST_PREFIX);
munmap(addr1, size);
close(buf);
close(memfd);
/* should work (migration of 4k size pages)*/
size = MEMFD_SIZE * page_size;
memfd = create_memfd_with_seals(size, false);
addr1 = mmap_fd(memfd, size);
write_to_memfd(addr1, size, 'a');
buf = create_udmabuf_list(devfd, memfd, size);
addr2 = mmap_fd(buf, NUM_PAGES * NUM_ENTRIES * getpagesize());
write_to_memfd(addr1, size, 'b');
ret = compare_chunks(addr1, addr2, size);
if (ret < 0)
ksft_test_result_fail("%s: [FAIL,test-5]\n", TEST_PREFIX);
else
ksft_test_result_pass("%s: [PASS,test-5]\n", TEST_PREFIX);
close(buf);
close(memfd);
/* should work (migration of 2MB size huge pages)*/
page_size = getpagesize() * 512; /* 2 MB */
size = MEMFD_SIZE * page_size;
memfd = create_memfd_with_seals(size, true);
addr1 = mmap_fd(memfd, size);
write_to_memfd(addr1, size, 'a');
buf = create_udmabuf_list(devfd, memfd, size);
addr2 = mmap_fd(buf, NUM_PAGES * NUM_ENTRIES * getpagesize());
write_to_memfd(addr1, size, 'b');
ret = compare_chunks(addr1, addr2, size);
if (ret < 0)
ksft_test_result_fail("%s: [FAIL,test-6]\n", TEST_PREFIX);
else
ksft_test_result_pass("%s: [PASS,test-6]\n", TEST_PREFIX);
close(buf);
close(memfd);
/* same test as above but we pin first before writing to memfd */
page_size = getpagesize() * 512; /* 2 MB */
size = MEMFD_SIZE * page_size;
memfd = create_memfd_with_seals(size, true);
buf = create_udmabuf_list(devfd, memfd, size);
addr2 = mmap_fd(buf, NUM_PAGES * NUM_ENTRIES * getpagesize());
addr1 = mmap_fd(memfd, size);
write_to_memfd(addr1, size, 'a');
write_to_memfd(addr1, size, 'b');
ret = compare_chunks(addr1, addr2, size);
if (ret < 0)
ksft_test_result_fail("%s: [FAIL,test-7]\n", TEST_PREFIX);
else
ksft_test_result_pass("%s: [PASS,test-7]\n", TEST_PREFIX);
close(buf);
close(memfd);
close(devfd);
ksft_print_msg("%s: ok\n", TEST_PREFIX);
ksft_print_cnts();
return 0;
}
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0
# Runs API tests for struct drm_mm (DRM range manager)
if ! /sbin/modprobe -n -q test-drm_mm; then
echo "drivers/gpu/drm_mm: module test-drm_mm is not found in /lib/modules/`uname -r` [skip]"
exit 77
fi
if /sbin/modprobe -q test-drm_mm; then
/sbin/modprobe -q -r test-drm_mm
echo "drivers/gpu/drm_mm: ok"
else
echo "drivers/gpu/drm_mm: module test-drm_mm could not be removed [FAIL]"
exit 1
fi
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0
# Runs hardware independent tests for i915 (drivers/gpu/drm/i915)
if ! /sbin/modprobe -q -r i915; then
echo "drivers/gpu/i915: [SKIP]"
exit 77
fi
if /sbin/modprobe -q i915 mock_selftests=-1; then
/sbin/modprobe -q -r i915
echo "drivers/gpu/i915: ok"
else
echo "drivers/gpu/i915: [FAIL]"
exit 1
fi
@@ -0,0 +1,4 @@
# SPDX-License-Identifier: GPL-2.0-only
napi_id_helper
psp_responder
so_txtime
@@ -0,0 +1,39 @@
# SPDX-License-Identifier: GPL-2.0
CFLAGS += $(KHDR_INCLUDES)
TEST_INCLUDES := $(wildcard lib/py/*.py) \
$(wildcard lib/sh/*.sh) \
../../net/lib.sh \
TEST_GEN_FILES := \
napi_id_helper \
so_txtime \
# end of TEST_GEN_FILES
TEST_PROGS := \
gro.py \
hds.py \
macsec.py \
napi_id.py \
napi_threaded.py \
netpoll_basic.py \
ping.py \
psp.py \
queues.py \
ring_reconfig.py \
shaper.py \
so_txtime.py \
stats.py \
xdp.py \
# end of TEST_PROGS
# YNL files, must be before "include ..lib.mk"
YNL_GEN_FILES := psp_responder
TEST_GEN_FILES += $(YNL_GEN_FILES)
include ../../lib.mk
# YNL build
YNL_GENS := psp
include ../../net/ynl.mk
@@ -0,0 +1,303 @@
.. SPDX-License-Identifier: GPL-2.0
Running driver tests
====================
Networking driver tests are executed within kselftest framework like any
other tests. They support testing both real device drivers and emulated /
software drivers (latter mostly to test the core parts of the stack).
SW mode
~~~~~~~
By default, when no extra parameters are set or exported, tests execute
against software drivers such as netdevsim. No extra preparation is required
the software devices are created and destroyed as part of the test.
In this mode the tests are indistinguishable from other selftests and
(for example) can be run under ``virtme-ng`` like the core networking selftests.
HW mode
~~~~~~~
Executing tests against a real device requires external preparation.
The netdevice against which tests will be run must exist, be running
(in UP state) and be configured with an IP address.
Refer to list of :ref:`Variables` later in this file to set up running
the tests against a real device.
The current support for bash tests restricts the use of the same interface name
on the local system and the remote one and will bail if this case is
encountered.
Both modes required
~~~~~~~~~~~~~~~~~~~
All tests in drivers/net must support running both against a software device
and a real device. SW-only tests should instead be placed in net/ or
drivers/net/netdevsim, HW-only tests in drivers/net/hw.
Variables
=========
The variables can be set in the environment or by creating a net.config
file in the same directory as this README file. Example::
$ NETIF=eth0 ./some_test.sh
or::
$ cat tools/testing/selftests/drivers/net/net.config
# Variable set in a file
NETIF=eth0
Please note that the config parser is very simple, if there are
any non-alphanumeric characters in the value it needs to be in
double quotes.
Local test (which don't require endpoint for sending / receiving traffic)
need only the ``NETIF`` variable. Remaining variables define the endpoint
and communication method.
NETIF
~~~~~
Name of the netdevice against which the test should be executed.
When empty or not set software devices will be used.
LOCAL_V4, LOCAL_V6, REMOTE_V4, REMOTE_V6
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Local and remote endpoint IP addresses.
LOCAL_PREFIX_V6
~~~~~~~~~~~~~~~
Local IP prefix/subnet which can be used to allocate extra IP addresses (for
network name spaces behind macvlan, veth, netkit devices). DUT must be
reachable using these addresses from the endpoint.
LOCAL_PREFIX_V6 must NOT match LOCAL_V6.
Example:
NETIF = "eth0"
LOCAL_V6 = "2001:db8:1::1"
REMOTE_V6 = "2001:db8:1::2"
LOCAL_PREFIX_V6 = "2001:db8:2::0/64"
+-----------------------------+ +------------------------------+
dst | INIT NS | | TEST NS |
2001: | +---------------+ | | |
db8:2::2| | NETIF | | bpf | |
+---|>| 2001:db8:1::1 | |redirect| +-------------------------+ |
| | | |-----------|--------|>| Netkit | |
| | +---------------+ | _peer | | nk_guest | |
| | +-------------+ Netkit pair | | | fe80::2/64 | |
| | | Netkit |.............|........|>| 2001:db8:2::2/64 | |
| | | nk_host | | | +-------------------------+ |
| | | fe80::1/64 | | | |
| | +-------------+ | | route: |
| | | | default |
| | route: | | via fe80::1 dev nk_guest |
| | 2001:db8:2::2/128 | +------------------------------+
| | via fe80::2 dev nk_host |
| +-----------------------------+
|
| +---------------+
| | REMOTE |
+---| 2001:db8:1::2 |
+---------------+
REMOTE_TYPE
~~~~~~~~~~~
Communication method used to run commands on the remote endpoint.
Test framework has built-in support for ``netns`` and ``ssh`` channels.
``netns`` assumes the "remote" interface is part of the same
host, just moved to the specified netns.
``ssh`` communicates with remote endpoint over ``ssh`` and ``scp``.
Using persistent SSH connections is strongly encouraged to avoid
the latency of SSH connection setup on every command.
Communication methods are defined by classes in ``lib/py/remote_{name}.py``.
It should be possible to add a new method without modifying any of
the framework, by simply adding an appropriately named file to ``lib/py``.
REMOTE_ARGS
~~~~~~~~~~~
Arguments used to construct the communication channel.
Communication channel dependent::
for netns - name of the "remote" namespace
for ssh - name/address of the remote host
Example
=======
Build the selftests::
# make -C tools/testing/selftests/ TARGETS="drivers/net drivers/net/hw"
"Install" the tests and copy them over to the target machine::
# make -C tools/testing/selftests/ TARGETS="drivers/net drivers/net/hw" \
install INSTALL_PATH=/tmp/ksft-net-drv
# rsync -ra --delete /tmp/ksft-net-drv root@192.168.1.1:/root/
On the target machine, running the tests will use netdevsim by default::
[/root] # ./ksft-net-drv/run_kselftest.sh -t drivers/net:ping.py
TAP version 13
1..1
# timeout set to 45
# selftests: drivers/net: ping.py
# KTAP version 1
# 1..3
# ok 1 ping.test_v4
# ok 2 ping.test_v6
# ok 3 ping.test_tcp
# # Totals: pass:3 fail:0 xfail:0 xpass:0 skip:0 error:0
ok 1 selftests: drivers/net: ping.py
Create a config with remote info::
[/root] # cat > ./ksft-net-drv/drivers/net/net.config <<EOF
NETIF=eth0
LOCAL_V4=192.168.1.1
REMOTE_V4=192.168.1.2
REMOTE_TYPE=ssh
REMOTE_ARGS=root@192.168.1.2
EOF
Run the test::
[/root] # ./ksft-net-drv/drivers/net/ping.py
KTAP version 1
1..3
ok 1 ping.test_v4
ok 2 ping.test_v6 # SKIP Test requires IPv6 connectivity
ok 3 ping.test_tcp
# Totals: pass:2 fail:0 xfail:0 xpass:0 skip:1 error:0
Dependencies
~~~~~~~~~~~~
The tests have a handful of dependencies. For Fedora / CentOS::
dnf -y install netsniff-ng python-yaml socat iperf3
Guidance for test authors
=========================
This section mostly applies to Python tests but some of the guidance
may be more broadly applicable.
Kernel config
~~~~~~~~~~~~~
Each test directory has a ``config`` file listing which kernel
configuration options the tests depend on. This file must be kept
up to date, the CIs build minimal kernels for each test group.
Adding checks inside the tests to validate that the necessary kernel
configs are enabled is discouraged. The test author may include such
checks, but standalone patches to make tests compatible e.g. with
distro kernel configs are unlikely to be accepted.
Avoid libraries and frameworks
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Test files should be relatively self contained. The libraries should
only include very core or non-trivial code.
It may be tempting to "factor out" the common code to lib/py/, but fight that
urge. Library code increases the barrier of entry, and complexity in general.
Avoid mixing test code and boilerplate
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In Python, try to avoid adding code in the ``main()`` function which
instantiates ``NetDrvEnv()`` and calls ``ksft_run()``. It's okay to
set up global resources (e.g. open an RtNetlink socket used by multiple
tests), but any complex logic, test-specific environment configuration
and validation should be done in the tests (even if it means it has to
be repeated).
Local host is the DUT
~~~~~~~~~~~~~~~~~~~~~
Dual-host tests (tests with an endpoint) should be written from the DUT
perspective. IOW the local machine should be the one tested, remote is
just for traffic generation.
Avoid modifying remote
~~~~~~~~~~~~~~~~~~~~~~
Avoid making configuration changes to the remote system as much as possible.
Remote system may be used concurrently by multiple DUTs.
defer()
~~~~~~~
The env must be clean after test exits. Register a ``defer()`` for any
action that needs an "undo" as soon as possible. If you need to run
the cancel action as part of the test - ``defer()`` returns an object
you can ``.exec()``-ute.
ksft_pr()
~~~~~~~~~
Use ``ksft_pr()`` instead of ``print()`` to avoid breaking TAP format.
ksft_disruptive
~~~~~~~~~~~~~~~
By default the tests are expected to be able to run on
single-interface systems. All tests which may disconnect ``NETIF``
must be annotated with ``@ksft_disruptive``.
ksft_variants
~~~~~~~~~~~~~
Use the ``@ksft_variants`` decorator to run a test with multiple sets
of inputs as separate test cases. This avoids duplicating test functions
that only differ in parameters.
Parameters can be a single value, a tuple, or a ``KsftNamedVariant``
(which gives an explicit name to the sub-case). The argument to the
decorator can be a list or a generator.
Example::
@ksft_variants([
KsftNamedVariant("main", False),
KsftNamedVariant("ctx", True),
])
def resize_periodic(cfg, create_context):
# test body receives (cfg, create_context) where create_context
# is False for the "main" variant and True for "ctx"
pass
or::
def _gro_variants():
for mode in ["sw", "hw"]:
for protocol in ["tcp4", "tcp6"]:
yield (mode, protocol)
@ksft_variants(_gro_variants())
def test(cfg, mode, protocol):
pass
Linters
~~~~~~~
We expect clean ``ruff check`` and ``pylint --disable=R``.
The code should be clean, avoid disabling pylint warnings explicitly!
Running tests CI-style
======================
See https://github.com/linux-netdev/nipa/wiki for instructions on how
to easily run the tests using ``virtme-ng``.
@@ -0,0 +1,35 @@
# SPDX-License-Identifier: GPL-2.0
# Makefile for net selftests
TEST_PROGS := \
bond-arp-interval-causes-panic.sh \
bond-break-lacpdu-tx.sh \
bond-eth-type-change.sh \
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 \
netcons_over_bonding.sh \
# end of TEST_PROGS
TEST_FILES := \
bond_topo_2d1c.sh \
bond_topo_3d1c.sh \
lag_lib.sh \
# end of TEST_FILES
TEST_INCLUDES := \
../../../net/lib.sh \
../lib/sh/lib_netcons.sh \
../../../net/forwarding/lib.sh \
# end of TEST_INCLUDES
include ../../../lib.mk
@@ -0,0 +1,46 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0
#
# cause kernel oops in bond_rr_gen_slave_id
DEBUG=${DEBUG:-0}
set -e
test ${DEBUG} -ne 0 && set -x
finish()
{
ip netns delete server || true
ip netns delete client || true
}
trap finish EXIT
client_ip4=192.168.1.198
server_ip4=192.168.1.254
# setup kernel so it reboots after causing the panic
echo 180 >/proc/sys/kernel/panic
# build namespaces
ip netns add "server"
ip netns add "client"
ip -n client link add eth0 type veth peer name eth0 netns server
ip netns exec server ip link set dev eth0 up
ip netns exec server ip addr add ${server_ip4}/24 dev eth0
ip netns exec client ip link add dev bond0 down type bond mode 1 \
miimon 100 all_slaves_active 1
ip netns exec client ip link set dev eth0 master bond0
ip netns exec client ip link set dev bond0 up
ip netns exec client ip addr add ${client_ip4}/24 dev bond0
ip netns exec client ping -c 5 $server_ip4 >/dev/null
ip netns exec client ip link set dev eth0 nomaster
ip netns exec client ip link set dev bond0 down
ip netns exec client ip link set dev bond0 type bond mode 0 \
arp_interval 1000 arp_ip_target "+${server_ip4}"
ip netns exec client ip link set dev eth0 master bond0
ip netns exec client ip link set dev bond0 up
ip netns exec client ping -c 5 $server_ip4 >/dev/null
exit 0
@@ -0,0 +1,80 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0
# Regression Test:
# Verify LACPDUs get transmitted after setting the MAC address of
# the bond.
#
# https://bugzilla.redhat.com/show_bug.cgi?id=2020773
#
# +---------+
# | fab-br0 |
# +---------+
# |
# +---------+
# | fbond |
# +---------+
# | |
# +------+ +------+
# |veth1 | |veth2 |
# +------+ +------+
#
# We use veths instead of physical interfaces
REQUIRE_MZ=no
NUM_NETIFS=0
lib_dir=$(dirname "$0")
source "$lib_dir"/../../../net/forwarding/lib.sh
set -e
cleanup() {
ip link del fab-br0 >/dev/null 2>&1 || :
ip link del fbond >/dev/null 2>&1 || :
ip link del veth1-bond >/dev/null 2>&1 || :
ip link del veth2-bond >/dev/null 2>&1 || :
}
trap cleanup 0 1 2
cleanup
# create the bridge
ip link add fab-br0 address 52:54:00:3B:7C:A6 mtu 1500 type bridge \
forward_delay 15
# create the bond
ip link add fbond type bond mode 4 miimon 200 xmit_hash_policy 1 \
ad_actor_sys_prio 65535 lacp_rate fast
# set bond address
ip link set fbond address 52:54:00:3B:7C:A6
ip link set fbond up
# set again bond sysfs parameters
ip link set fbond type bond ad_actor_sys_prio 65535
# create veths
ip link add name veth1-bond type veth peer name veth1-end
ip link add name veth2-bond type veth peer name veth2-end
# add ports
ip link set fbond master fab-br0
ip link set veth1-bond master fbond
ip link set veth2-bond master fbond
# bring up
ip link set veth1-end up
ip link set veth2-end up
ip link set fab-br0 up
ip link set fbond up
ip addr add dev fab-br0 10.0.0.3
rc=0
tc qdisc add dev veth1-end clsact
tc filter add dev veth1-end ingress protocol 0x8809 pref 1 handle 101 flower skip_hw action pass
if slowwait_for_counter 15 2 \
tc_rule_handle_stats_get "dev veth1-end ingress" 101 ".packets" "" &> /dev/null; then
echo "PASS, captured 2"
else
echo "FAIL"
rc=1
fi
exit $rc
@@ -0,0 +1,84 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# Test bond device ether type changing
#
ALL_TESTS="
bond_test_unsuccessful_enslave_type_change
bond_test_successful_enslave_type_change
"
REQUIRE_MZ=no
NUM_NETIFS=0
lib_dir=$(dirname "$0")
source "$lib_dir"/../../../net/forwarding/lib.sh
bond_check_flags()
{
local bonddev=$1
ip -d l sh dev "$bonddev" | grep -q "MASTER"
check_err $? "MASTER flag is missing from the bond device"
ip -d l sh dev "$bonddev" | grep -q "SLAVE"
check_err $? "SLAVE flag is missing from the bond device"
}
# test enslaved bond dev type change from ARPHRD_ETHER and back
# this allows us to test both MASTER and SLAVE flags at once
bond_test_enslave_type_change()
{
local test_success=$1
local devbond0="test-bond0"
local devbond1="test-bond1"
local devbond2="test-bond2"
local nonethdev="test-noneth0"
# create a non-ARPHRD_ETHER device for testing (e.g. nlmon type)
ip link add name "$nonethdev" type nlmon
check_err $? "could not create a non-ARPHRD_ETHER device (nlmon)"
ip link add name "$devbond0" type bond
if [ $test_success -eq 1 ]; then
# we need devbond0 in active-backup mode to successfully enslave nonethdev
ip link set dev "$devbond0" type bond mode active-backup
check_err $? "could not change bond mode to active-backup"
fi
ip link add name "$devbond1" type bond
ip link add name "$devbond2" type bond
ip link set dev "$devbond0" master "$devbond1"
check_err $? "could not enslave $devbond0 to $devbond1"
# change bond type to non-ARPHRD_ETHER
ip link set dev "$nonethdev" master "$devbond0" 1>/dev/null 2>/dev/null
ip link set dev "$nonethdev" nomaster 1>/dev/null 2>/dev/null
# restore ARPHRD_ETHER type by enslaving such device
ip link set dev "$devbond2" master "$devbond0"
check_err $? "could not enslave $devbond2 to $devbond0"
bond_check_flags "$devbond0"
# clean up
ip link del dev "$devbond0"
ip link del dev "$devbond1"
ip link del dev "$devbond2"
ip link del dev "$nonethdev"
}
bond_test_unsuccessful_enslave_type_change()
{
RET=0
bond_test_enslave_type_change 0
log_test "Change ether type of an enslaved bond device with unsuccessful enslave"
}
bond_test_successful_enslave_type_change()
{
RET=0
bond_test_enslave_type_change 1
log_test "Change ether type of an enslaved bond device with successful enslave"
}
tests_run
exit "$EXIT_STATUS"
@@ -0,0 +1,80 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
# Regression Test:
# Verify bond interface could up when set IPv6 link local address target.
#
# +----------------+
# | br0 |
# | | | sw
# | veth0 veth1 |
# +---+-------+----+
# | |
# +---+-------+----+
# | veth0 veth1 |
# | | | host
# | bond0 |
# +----------------+
#
# We use veths instead of physical interfaces
REQUIRE_MZ=no
NUM_NETIFS=0
lib_dir=$(dirname "$0")
source "$lib_dir"/../../../net/forwarding/lib.sh
sw="sw-$(mktemp -u XXXXXX)"
host="ns-$(mktemp -u XXXXXX)"
cleanup()
{
ip netns del $sw
ip netns del $host
}
wait_lladdr_dad()
{
$@ | grep fe80 | grep -qv tentative
}
wait_bond_up()
{
$@ | grep -q 'state UP'
}
trap cleanup 0 1 2
ip netns add $sw
ip netns add $host
ip -n $host link add veth0 type veth peer name veth0 netns $sw
ip -n $host link add veth1 type veth peer name veth1 netns $sw
ip -n $sw link add br0 type bridge
ip -n $sw link set br0 up
sw_lladdr=$(ip -n $sw addr show br0 | awk '/fe80/{print $2}' | cut -d'/' -f1)
# wait some time to make sure bridge lladdr pass DAD
slowwait 2 wait_lladdr_dad ip -n $sw addr show br0
ip -n $host link add bond0 type bond mode 1 ns_ip6_target ${sw_lladdr} \
arp_validate 3 arp_interval 1000
# add a lladdr for bond to make sure there is a route to target
ip -n $host addr add fe80::beef/64 dev bond0
ip -n $host link set bond0 up
ip -n $host link set veth0 master bond0
ip -n $host link set veth1 master bond0
ip -n $sw link set veth0 master br0
ip -n $sw link set veth1 master br0
ip -n $sw link set veth0 up
ip -n $sw link set veth1 up
slowwait 5 wait_bond_up ip -n $host link show bond0
rc=0
if ip -n $host link show bond0 | grep -q LOWER_UP; then
echo "PASS"
else
echo "FAIL"
rc=1
fi
exit $rc
@@ -0,0 +1,156 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
# IPsec over bonding offload test:
#
# +----------------+
# | bond0 |
# | | |
# | eth0 eth1 |
# +---+-------+----+
#
# We use netdevsim instead of physical interfaces
#-------------------------------------------------------------------
# Example commands
# ip x s add proto esp src 192.0.2.1 dst 192.0.2.2 \
# spi 0x07 mode transport reqid 0x07 replay-window 32 \
# aead 'rfc4106(gcm(aes))' 1234567890123456dcba 128 \
# sel src 192.0.2.1/24 dst 192.0.2.2/24
# offload dev bond0 dir out
# ip x p add dir out src 192.0.2.1/24 dst 192.0.2.2/24 \
# tmpl proto esp src 192.0.2.1 dst 192.0.2.2 \
# spi 0x07 mode transport reqid 0x07
#
#-------------------------------------------------------------------
lib_dir=$(dirname "$0")
# shellcheck disable=SC1091
source "$lib_dir"/../../../net/lib.sh
srcip=192.0.2.1
dstip=192.0.2.2
ipsec0=/sys/kernel/debug/netdevsim/netdevsim0/ports/0/ipsec
ipsec1=/sys/kernel/debug/netdevsim/netdevsim0/ports/1/ipsec
active_slave=""
# shellcheck disable=SC2317
active_slave_changed()
{
local old_active_slave=$1
local new_active_slave
# shellcheck disable=SC2154
new_active_slave=$(ip -n "${ns}" -d -j link show bond0 | \
jq -r ".[].linkinfo.info_data.active_slave")
[ "$new_active_slave" != "$old_active_slave" ] && [ "$new_active_slave" != "null" ]
}
test_offload()
{
# use ping to exercise the Tx path
ip netns exec "$ns" ping -I bond0 -c 3 -W 1 -i 0 "$dstip" >/dev/null
active_slave=$(ip -n "${ns}" -d -j link show bond0 | \
jq -r ".[].linkinfo.info_data.active_slave")
if [ "$active_slave" = "$nic0" ]; then
sysfs=$ipsec0
elif [ "$active_slave" = "$nic1" ]; then
sysfs=$ipsec1
else
check_err 1 "bond_ipsec_offload invalid active_slave $active_slave"
fi
# The tx/rx order in sysfs may changed after failover
grep -q "SA count=2 tx=3" "$sysfs" && grep -q "tx ipaddr=$dstip" "$sysfs"
check_err $? "incorrect tx count with link ${active_slave}"
log_test bond_ipsec_offload "active_slave ${active_slave}"
}
setup_env()
{
if ! mount | grep -q debugfs; then
mount -t debugfs none /sys/kernel/debug/ &> /dev/null
defer umount /sys/kernel/debug/
fi
# setup netdevsim since dummy/veth dev doesn't have offload support
if [ ! -w /sys/bus/netdevsim/new_device ] ; then
if ! modprobe -q netdevsim; then
echo "SKIP: can't load netdevsim for ipsec offload"
# shellcheck disable=SC2154
exit "$ksft_skip"
fi
defer modprobe -r netdevsim
fi
setup_ns ns
defer cleanup_ns "$ns"
}
setup_bond()
{
ip -n "$ns" link add bond0 type bond mode active-backup miimon 100
ip -n "$ns" addr add "$srcip/24" dev bond0
ip -n "$ns" link set bond0 up
echo "0 2" | ip netns exec "$ns" tee /sys/bus/netdevsim/new_device >/dev/null
nic0=$(ip netns exec "$ns" ls /sys/bus/netdevsim/devices/netdevsim0/net | head -n 1)
nic1=$(ip netns exec "$ns" ls /sys/bus/netdevsim/devices/netdevsim0/net | tail -n 1)
ip -n "$ns" link set "$nic0" master bond0
ip -n "$ns" link set "$nic1" master bond0
# we didn't create a peer, make sure we can Tx by adding a permanent
# neighbour this need to be added after enslave
ip -n "$ns" neigh add "$dstip" dev bond0 lladdr 00:11:22:33:44:55
# create offloaded SAs, both in and out
ip -n "$ns" x p add dir out src "$srcip/24" dst "$dstip/24" \
tmpl proto esp src "$srcip" dst "$dstip" spi 9 \
mode transport reqid 42
ip -n "$ns" x p add dir in src "$dstip/24" dst "$srcip/24" \
tmpl proto esp src "$dstip" dst "$srcip" spi 9 \
mode transport reqid 42
ip -n "$ns" x s add proto esp src "$srcip" dst "$dstip" spi 9 \
mode transport reqid 42 aead "rfc4106(gcm(aes))" \
0x3132333435363738393031323334353664636261 128 \
sel src "$srcip/24" dst "$dstip/24" \
offload dev bond0 dir out
ip -n "$ns" x s add proto esp src "$dstip" dst "$srcip" spi 9 \
mode transport reqid 42 aead "rfc4106(gcm(aes))" \
0x3132333435363738393031323334353664636261 128 \
sel src "$dstip/24" dst "$srcip/24" \
offload dev bond0 dir in
# does offload show up in ip output
lines=$(ip -n "$ns" x s list | grep -c "crypto offload parameters: dev bond0 dir")
if [ "$lines" -ne 2 ] ; then
check_err 1 "bond_ipsec_offload SA offload missing from list output"
fi
}
trap defer_scopes_cleanup EXIT
setup_env
setup_bond
# start Offload testing
test_offload
# do failover and re-test
ip -n "$ns" link set "$active_slave" down
slowwait 5 active_slave_changed "$active_slave"
test_offload
# make sure offload get removed from driver
ip -n "$ns" x s flush
ip -n "$ns" x p flush
line0=$(grep -c "SA count=0" "$ipsec0")
line1=$(grep -c "SA count=0" "$ipsec1")
[ "$line0" -ne 1 ] || [ "$line1" -ne 1 ]
check_fail $? "bond_ipsec_offload SA not removed from driver"
exit "$EXIT_STATUS"
@@ -0,0 +1,108 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# Testing if bond lacp per port priority works
#
# Switch (s_ns) Backup Switch (b_ns)
# +-------------------------+ +-------------------------+
# | bond0 | | bond0 |
# | + | | + |
# | eth0 | eth1 | | eth0 | eth1 |
# | +---+---+ | | +---+---+ |
# | | | | | | | |
# +-------------------------+ +-------------------------+
# | | | |
# +-----------------------------------------------------+
# | | | | | |
# | +-------+---------+---------+-------+ |
# | eth0 eth1 | eth2 eth3 |
# | + |
# | bond0 |
# +-----------------------------------------------------+
# Client (c_ns)
lib_dir=$(dirname "$0")
# shellcheck disable=SC1091
source "$lib_dir"/../../../net/lib.sh
setup_links()
{
# shellcheck disable=SC2154
ip -n "${c_ns}" link add eth0 type veth peer name eth0 netns "${s_ns}"
ip -n "${c_ns}" link add eth1 type veth peer name eth1 netns "${s_ns}"
# shellcheck disable=SC2154
ip -n "${c_ns}" link add eth2 type veth peer name eth0 netns "${b_ns}"
ip -n "${c_ns}" link add eth3 type veth peer name eth1 netns "${b_ns}"
ip -n "${c_ns}" link add bond0 type bond mode 802.3ad miimon 100 \
lacp_rate fast ad_select actor_port_prio
ip -n "${s_ns}" link add bond0 type bond mode 802.3ad miimon 100 \
lacp_rate fast
ip -n "${b_ns}" link add bond0 type bond mode 802.3ad miimon 100 \
lacp_rate fast
ip -n "${c_ns}" link set eth0 master bond0
ip -n "${c_ns}" link set eth1 master bond0
ip -n "${c_ns}" link set eth2 master bond0
ip -n "${c_ns}" link set eth3 master bond0
ip -n "${s_ns}" link set eth0 master bond0
ip -n "${s_ns}" link set eth1 master bond0
ip -n "${b_ns}" link set eth0 master bond0
ip -n "${b_ns}" link set eth1 master bond0
ip -n "${c_ns}" link set bond0 up
ip -n "${s_ns}" link set bond0 up
ip -n "${b_ns}" link set bond0 up
}
test_port_prio_setting()
{
RET=0
ip -n "${c_ns}" link set eth0 type bond_slave actor_port_prio 1000
prio=$(cmd_jq "ip -n ${c_ns} -d -j link show eth0" \
".[].linkinfo.info_slave_data.actor_port_prio")
[ "$prio" -ne 1000 ] && RET=1
ip -n "${c_ns}" link set eth2 type bond_slave actor_port_prio 10
prio=$(cmd_jq "ip -n ${c_ns} -d -j link show eth2" \
".[].linkinfo.info_slave_data.actor_port_prio")
[ "$prio" -ne 10 ] && RET=1
}
test_agg_reselect()
{
local bond_agg_id slave_agg_id
local expect_slave="$1"
RET=0
# Trigger link state change to reselect the aggregator
ip -n "${c_ns}" link set eth1 down
sleep 0.5
ip -n "${c_ns}" link set eth1 up
sleep 0.5
bond_agg_id=$(cmd_jq "ip -n ${c_ns} -d -j link show bond0" \
".[].linkinfo.info_data.ad_info.aggregator")
slave_agg_id=$(cmd_jq "ip -n ${c_ns} -d -j link show $expect_slave" \
".[].linkinfo.info_slave_data.ad_aggregator_id")
# shellcheck disable=SC2034
[ "${bond_agg_id}" -ne "${slave_agg_id}" ] && \
RET=1
}
trap cleanup_all_ns EXIT
setup_ns c_ns s_ns b_ns
setup_links
test_port_prio_setting
log_test "bond 802.3ad" "actor_port_prio setting"
test_agg_reselect eth0
log_test "bond 802.3ad" "actor_port_prio select"
# Change the actor port prio and re-test
ip -n "${c_ns}" link set eth0 type bond_slave actor_port_prio 10
ip -n "${c_ns}" link set eth2 type bond_slave actor_port_prio 1000
test_agg_reselect eth2
log_test "bond 802.3ad" "actor_port_prio switch"
exit "${EXIT_STATUS}"
@@ -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}"
@@ -0,0 +1,97 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# Test macvlan/ipvlan over bond
lib_dir=$(dirname "$0")
source ${lib_dir}/bond_topo_2d1c.sh
xvlan1_ns="xvlan1-$(mktemp -u XXXXXX)"
xvlan2_ns="xvlan2-$(mktemp -u XXXXXX)"
xvlan1_ip4="192.0.2.11"
xvlan1_ip6="2001:db8::11"
xvlan2_ip4="192.0.2.12"
xvlan2_ip6="2001:db8::12"
cleanup()
{
client_destroy
server_destroy
gateway_destroy
ip netns del ${xvlan1_ns}
ip netns del ${xvlan2_ns}
}
check_connection()
{
local ns=${1}
local target=${2}
local message=${3}
RET=0
sleep 0.25
ip netns exec ${ns} ping ${target} -c 4 -i 0.1 &>/dev/null
check_err $? "ping failed"
log_test "${bond_mode}/${xvlan_type}_${xvlan_mode}: ${message}"
}
xvlan_over_bond()
{
local param="$1"
local xvlan_type="$2"
local xvlan_mode="$3"
RET=0
# setup new bond mode
bond_reset "${param}"
ip -n ${s_ns} link add link bond0 name ${xvlan_type}0 type ${xvlan_type} mode ${xvlan_mode}
ip -n ${s_ns} link set ${xvlan_type}0 netns ${xvlan1_ns}
ip -n ${xvlan1_ns} link set dev ${xvlan_type}0 up
ip -n ${xvlan1_ns} addr add ${xvlan1_ip4}/24 dev ${xvlan_type}0
ip -n ${xvlan1_ns} addr add ${xvlan1_ip6}/24 dev ${xvlan_type}0
ip -n ${s_ns} link add link bond0 name ${xvlan_type}0 type ${xvlan_type} mode ${xvlan_mode}
ip -n ${s_ns} link set ${xvlan_type}0 netns ${xvlan2_ns}
ip -n ${xvlan2_ns} link set dev ${xvlan_type}0 up
ip -n ${xvlan2_ns} addr add ${xvlan2_ip4}/24 dev ${xvlan_type}0
ip -n ${xvlan2_ns} addr add ${xvlan2_ip6}/24 dev ${xvlan_type}0
sleep 2
check_connection "${c_ns}" "${s_ip4}" "IPv4: client->server"
check_connection "${c_ns}" "${s_ip6}" "IPv6: client->server"
check_connection "${c_ns}" "${xvlan1_ip4}" "IPv4: client->${xvlan_type}_1"
check_connection "${c_ns}" "${xvlan1_ip6}" "IPv6: client->${xvlan_type}_1"
check_connection "${c_ns}" "${xvlan2_ip4}" "IPv4: client->${xvlan_type}_2"
check_connection "${c_ns}" "${xvlan2_ip6}" "IPv6: client->${xvlan_type}_2"
check_connection "${xvlan1_ns}" "${xvlan2_ip4}" "IPv4: ${xvlan_type}_1->${xvlan_type}_2"
check_connection "${xvlan1_ns}" "${xvlan2_ip6}" "IPv6: ${xvlan_type}_1->${xvlan_type}_2"
check_connection "${s_ns}" "${c_ip4}" "IPv4: server->client"
check_connection "${s_ns}" "${c_ip6}" "IPv6: server->client"
check_connection "${xvlan1_ns}" "${c_ip4}" "IPv4: ${xvlan_type}_1->client"
check_connection "${xvlan1_ns}" "${c_ip6}" "IPv6: ${xvlan_type}_1->client"
check_connection "${xvlan2_ns}" "${c_ip4}" "IPv4: ${xvlan_type}_2->client"
check_connection "${xvlan2_ns}" "${c_ip6}" "IPv6: ${xvlan_type}_2->client"
check_connection "${xvlan2_ns}" "${xvlan1_ip4}" "IPv4: ${xvlan_type}_2->${xvlan_type}_1"
check_connection "${xvlan2_ns}" "${xvlan1_ip6}" "IPv6: ${xvlan_type}_2->${xvlan_type}_1"
ip -n ${c_ns} neigh flush dev eth0
}
trap cleanup EXIT
setup_prepare
ip netns add ${xvlan1_ns}
ip netns add ${xvlan2_ns}
bond_modes="active-backup balance-tlb balance-alb"
for bond_mode in ${bond_modes}; do
xvlan_over_bond "mode ${bond_mode}" macvlan bridge
xvlan_over_bond "mode ${bond_mode}" ipvlan l2
done
exit $EXIT_STATUS
@@ -0,0 +1,578 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# Test bonding options with mode 1,5,6
ALL_TESTS="
prio
arp_validate
num_grat_arp
fail_over_mac
vlan_over_bond
"
lib_dir=$(dirname "$0")
source ${lib_dir}/bond_topo_3d1c.sh
c_maddr="33:33:ff:00:00:10"
g_maddr="33:33:ff:00:02:54"
skip_prio()
{
local skip=1
# check if iproute support prio option
ip -n ${s_ns} link set eth0 type bond_slave prio 10
[[ $? -ne 0 ]] && skip=0
# check if kernel support prio option
ip -n ${s_ns} -d link show eth0 | grep -q "prio 10"
[[ $? -ne 0 ]] && skip=0
return $skip
}
skip_ns()
{
local skip=1
# check if iproute support ns_ip6_target option
ip -n ${s_ns} link add bond1 type bond ns_ip6_target ${g_ip6}
[[ $? -ne 0 ]] && skip=0
# check if kernel support ns_ip6_target option
ip -n ${s_ns} -d link show bond1 | grep -q "ns_ip6_target ${g_ip6}"
[[ $? -ne 0 ]] && skip=0
ip -n ${s_ns} link del bond1
return $skip
}
active_slave=""
active_slave_changed()
{
local old_active_slave=$1
local new_active_slave=$(cmd_jq "ip -n ${s_ns} -d -j link show bond0" \
".[].linkinfo.info_data.active_slave")
[ "$new_active_slave" != "$old_active_slave" -a "$new_active_slave" != "null" ]
}
check_active_slave()
{
local target_active_slave=$1
slowwait 5 active_slave_changed $active_slave
active_slave=$(cmd_jq "ip -n ${s_ns} -d -j link show bond0" ".[].linkinfo.info_data.active_slave")
test "$active_slave" = "$target_active_slave"
check_err $? "Current active slave is $active_slave but not $target_active_slave"
}
# Test bonding prio option
prio_test()
{
local param="$1"
RET=0
# create bond
bond_reset "${param}"
# set active_slave to primary eth1 specifically
ip -n ${s_ns} link set bond0 type bond active_slave eth1
# check bonding member prio value
ip -n ${s_ns} link set eth0 type bond_slave prio 0
ip -n ${s_ns} link set eth1 type bond_slave prio 10
ip -n ${s_ns} link set eth2 type bond_slave prio 11
cmd_jq "ip -n ${s_ns} -d -j link show eth0" \
".[].linkinfo.info_slave_data | select (.prio == 0)" "-e" &> /dev/null
check_err $? "eth0 prio is not 0"
cmd_jq "ip -n ${s_ns} -d -j link show eth1" \
".[].linkinfo.info_slave_data | select (.prio == 10)" "-e" &> /dev/null
check_err $? "eth1 prio is not 10"
cmd_jq "ip -n ${s_ns} -d -j link show eth2" \
".[].linkinfo.info_slave_data | select (.prio == 11)" "-e" &> /dev/null
check_err $? "eth2 prio is not 11"
bond_check_connection "setup"
# active slave should be the primary slave
check_active_slave eth1
# active slave should be the higher prio slave
ip -n ${s_ns} link set $active_slave down
check_active_slave eth2
bond_check_connection "fail over"
# when only 1 slave is up
ip -n ${s_ns} link set $active_slave down
check_active_slave eth0
bond_check_connection "only 1 slave up"
# when a higher prio slave change to up
ip -n ${s_ns} link set eth2 up
bond_check_connection "higher prio slave up"
case $primary_reselect in
"0")
check_active_slave "eth2"
;;
"1")
check_active_slave "eth0"
;;
"2")
check_active_slave "eth0"
;;
esac
local pre_active_slave=$active_slave
# when the primary slave change to up
ip -n ${s_ns} link set eth1 up
bond_check_connection "primary slave up"
case $primary_reselect in
"0")
check_active_slave "eth1"
;;
"1")
check_active_slave "$pre_active_slave"
;;
"2")
check_active_slave "$pre_active_slave"
ip -n ${s_ns} link set $active_slave down
bond_check_connection "pre_active slave down"
check_active_slave "eth1"
;;
esac
# Test changing bond slave prio
if [[ "$primary_reselect" == "0" ]];then
ip -n ${s_ns} link set eth0 type bond_slave prio 1000000
ip -n ${s_ns} link set eth1 type bond_slave prio 0
ip -n ${s_ns} link set eth2 type bond_slave prio -50
ip -n ${s_ns} -d link show eth0 | grep -q 'prio 1000000'
check_err $? "eth0 prio is not 1000000"
ip -n ${s_ns} -d link show eth1 | grep -q 'prio 0'
check_err $? "eth1 prio is not 0"
ip -n ${s_ns} -d link show eth2 | grep -q 'prio -50'
check_err $? "eth3 prio is not -50"
check_active_slave "eth1"
ip -n ${s_ns} link set $active_slave down
check_active_slave "eth0"
bond_check_connection "change slave prio"
fi
}
prio_miimon()
{
local primary_reselect
local mode=$1
for primary_reselect in 0 1 2; do
prio_test "mode $mode miimon 100 primary eth1 primary_reselect $primary_reselect"
log_test "prio" "$mode miimon primary_reselect $primary_reselect"
done
}
prio_arp()
{
local primary_reselect
local mode=$1
for primary_reselect in 0 1 2; do
prio_test "mode $mode arp_interval 100 arp_ip_target ${g_ip4} primary eth1 primary_reselect $primary_reselect"
log_test "prio" "$mode arp_ip_target primary_reselect $primary_reselect"
done
}
prio_ns()
{
local primary_reselect
local mode=$1
if skip_ns; then
log_test_skip "prio ns" "Current iproute or kernel doesn't support bond option 'ns_ip6_target'."
return 0
fi
for primary_reselect in 0 1 2; do
prio_test "mode $mode arp_interval 100 ns_ip6_target ${g_ip6} primary eth1 primary_reselect $primary_reselect"
log_test "prio" "$mode ns_ip6_target primary_reselect $primary_reselect"
done
}
prio()
{
local mode modes="active-backup balance-tlb balance-alb"
if skip_prio; then
log_test_skip "prio" "Current iproute or kernel doesn't support bond option 'prio'."
return 0
fi
for mode in $modes; do
prio_miimon $mode
done
prio_arp "active-backup"
prio_ns "active-backup"
}
wait_mii_up()
{
for i in $(seq 0 2); do
mii_status=$(cmd_jq "ip -n ${s_ns} -j -d link show eth$i" ".[].linkinfo.info_slave_data.mii_status")
[ ${mii_status} != "UP" ] && return 1
done
return 0
}
arp_validate_test()
{
local param="$1"
RET=0
# create bond
bond_reset "${param}"
bond_check_connection
[ $RET -ne 0 ] && log_test "arp_validate" "$retmsg"
# wait for a while to make sure the mii status stable
slowwait 5 wait_mii_up
for i in $(seq 0 2); do
mii_status=$(cmd_jq "ip -n ${s_ns} -j -d link show eth$i" ".[].linkinfo.info_slave_data.mii_status")
if [ ${mii_status} != "UP" ]; then
RET=1
log_test "arp_validate" "interface eth$i mii_status $mii_status"
fi
done
}
# Testing correct multicast groups are added to slaves for ns targets
arp_validate_mcast()
{
RET=0
local arp_valid=$(cmd_jq "ip -n ${s_ns} -j -d link show bond0" ".[].linkinfo.info_data.arp_validate")
local active_slave=$(cmd_jq "ip -n ${s_ns} -d -j link show bond0" ".[].linkinfo.info_data.active_slave")
for i in $(seq 0 2); do
maddr_list=$(ip -n ${s_ns} maddr show dev eth${i})
# arp_valid == 0 or active_slave should not join any maddrs
if { [ "$arp_valid" == "null" ] || [ "eth${i}" == ${active_slave} ]; } && \
echo "$maddr_list" | grep -qE "${c_maddr}|${g_maddr}"; then
RET=1
check_err 1 "arp_valid $arp_valid active_slave $active_slave, eth$i has mcast group"
# arp_valid != 0 and backup_slave should join both maddrs
elif [ "$arp_valid" != "null" ] && [ "eth${i}" != ${active_slave} ] && \
( ! echo "$maddr_list" | grep -q "${c_maddr}" || \
! echo "$maddr_list" | grep -q "${m_maddr}"); then
RET=1
check_err 1 "arp_valid $arp_valid active_slave $active_slave, eth$i has mcast group"
fi
done
# Do failover
ip -n ${s_ns} link set ${active_slave} down
# wait for active link change
slowwait 2 active_slave_changed $active_slave
active_slave=$(cmd_jq "ip -n ${s_ns} -d -j link show bond0" ".[].linkinfo.info_data.active_slave")
for i in $(seq 0 2); do
maddr_list=$(ip -n ${s_ns} maddr show dev eth${i})
# arp_valid == 0 or active_slave should not join any maddrs
if { [ "$arp_valid" == "null" ] || [ "eth${i}" == ${active_slave} ]; } && \
echo "$maddr_list" | grep -qE "${c_maddr}|${g_maddr}"; then
RET=1
check_err 1 "arp_valid $arp_valid active_slave $active_slave, eth$i has mcast group"
# arp_valid != 0 and backup_slave should join both maddrs
elif [ "$arp_valid" != "null" ] && [ "eth${i}" != ${active_slave} ] && \
( ! echo "$maddr_list" | grep -q "${c_maddr}" || \
! echo "$maddr_list" | grep -q "${m_maddr}"); then
RET=1
check_err 1 "arp_valid $arp_valid active_slave $active_slave, eth$i has mcast group"
fi
done
}
arp_validate_arp()
{
local mode=$1
local val
for val in $(seq 0 6); do
arp_validate_test "mode $mode arp_interval 100 arp_ip_target ${g_ip4} arp_validate $val"
log_test "arp_validate" "$mode arp_ip_target arp_validate $val"
done
}
arp_validate_ns()
{
local mode=$1
local val
if skip_ns; then
log_test_skip "arp_validate ns" "Current iproute or kernel doesn't support bond option 'ns_ip6_target'."
return 0
fi
for val in $(seq 0 6); do
arp_validate_test "mode $mode arp_interval 100 ns_ip6_target ${g_ip6},${c_ip6} arp_validate $val"
log_test "arp_validate" "$mode ns_ip6_target arp_validate $val"
arp_validate_mcast
log_test "arp_validate" "join mcast group"
done
}
arp_validate()
{
arp_validate_arp "active-backup"
arp_validate_ns "active-backup"
}
garp_test()
{
local param="$1"
local active_slave exp_num real_num i
RET=0
# create bond
bond_reset "${param}"
bond_check_connection
[ $RET -ne 0 ] && log_test "num_grat_arp" "$retmsg"
# Add tc rules to count GARP number
for i in $(seq 0 2); do
tc -n ${g_ns} filter add dev s$i ingress protocol arp pref 1 handle 101 \
flower skip_hw arp_op request arp_sip ${s_ip4} arp_tip ${s_ip4} action pass
done
# Do failover
active_slave=$(cmd_jq "ip -n ${s_ns} -d -j link show bond0" ".[].linkinfo.info_data.active_slave")
ip -n ${s_ns} link set ${active_slave} down
# wait for active link change
slowwait 2 active_slave_changed $active_slave
exp_num=$(echo "${param}" | cut -f6 -d ' ')
active_slave=$(cmd_jq "ip -n ${s_ns} -d -j link show bond0" ".[].linkinfo.info_data.active_slave")
slowwait_for_counter $((exp_num + 5)) $exp_num tc_rule_handle_stats_get \
"dev s${active_slave#eth} ingress" 101 ".packets" "-n ${g_ns}" &> /dev/null
# check result
real_num=$(tc_rule_handle_stats_get "dev s${active_slave#eth} ingress" 101 ".packets" "-n ${g_ns}")
if [ "${real_num}" -ne "${exp_num}" ]; then
echo "$real_num garp packets sent on active slave ${active_slave}"
RET=1
fi
for i in $(seq 0 2); do
tc -n ${g_ns} filter del dev s$i ingress
done
}
num_grat_arp()
{
local val
for val in 10 20 30; do
garp_test "mode active-backup miimon 10 num_grat_arp $val peer_notify_delay 100"
log_test "num_grat_arp" "active-backup miimon num_grat_arp $val"
done
}
check_all_mac_same()
{
RET=0
# all slaves should have same mac address (with the first port's mac)
local bond_mac=$(ip -n "$s_ns" -j link show bond0 | jq -r '.[]["address"]')
local eth0_mac=$(ip -n "$s_ns" -j link show eth0 | jq -r '.[]["address"]')
local eth1_mac=$(ip -n "$s_ns" -j link show eth1 | jq -r '.[]["address"]')
local eth2_mac=$(ip -n "$s_ns" -j link show eth2 | jq -r '.[]["address"]')
if [ "$bond_mac" != "${mac[0]}" ] || [ "$eth0_mac" != "$bond_mac" ] || \
[ "$eth1_mac" != "$bond_mac" ] || [ "$eth2_mac" != "$bond_mac" ]; then
RET=1
fi
}
check_bond_mac_same_with_first()
{
RET=0
# bond mac address should be same with the first added slave
local bond_mac=$(ip -n "$s_ns" -j link show bond0 | jq -r '.[]["address"]')
if [ "$bond_mac" != "${mac[0]}" ]; then
RET=1
fi
}
check_bond_mac_same_with_active()
{
RET=0
# bond mac address should be same with active slave
local bond_mac=$(ip -n "$s_ns" -j link show bond0 | jq -r '.[]["address"]')
local active_slave=$(cmd_jq "ip -n ${s_ns} -d -j link show bond0" ".[].linkinfo.info_data.active_slave")
local active_slave_mac=$(ip -n "$s_ns" -j link show "$active_slave" | jq -r '.[]["address"]')
if [ "$bond_mac" != "$active_slave_mac" ]; then
RET=1
fi
}
check_backup_slave_mac_not_change()
{
RET=0
# backup slave's mac address is not changed
if ip -n "$s_ns" -d -j link show type bond_slave | jq -e '.[]
| select(.linkinfo.info_slave_data.state=="BACKUP")
| select(.address != .linkinfo.info_slave_data.perm_hwaddr)' &> /dev/null; then
RET=1
fi
}
check_backup_slave_mac_inherit()
{
local backup_mac
RET=0
# backup slaves should use mac[1] or mac[2]
local backup_macs=$(ip -n "$s_ns" -d -j link show type bond_slave | \
jq -r '.[] | select(.linkinfo.info_slave_data.state=="BACKUP") | .address')
for backup_mac in $backup_macs; do
if [ "$backup_mac" != "${mac[1]}" ] && [ "$backup_mac" != "${mac[2]}" ]; then
RET=1
fi
done
}
check_first_slave_random_mac()
{
RET=0
# remove the first added slave and added it back
ip -n "$s_ns" link set eth0 nomaster
ip -n "$s_ns" link set eth0 master bond0
# the first slave should use random mac address
eth0_mac=$(ip -n "$s_ns" -j link show eth0 | jq -r '.[]["address"]')
[ "$eth0_mac" = "${mac[0]}" ] && RET=1
log_test "bond fail_over_mac follow" "random first slave mac"
# remove the first slave, the permanent MAC address should be restored back
ip -n "$s_ns" link set eth0 nomaster
eth0_mac=$(ip -n "$s_ns" -j link show eth0 | jq -r '.[]["address"]')
[ "$eth0_mac" != "${mac[0]}" ] && RET=1
}
do_active_backup_failover()
{
local active_slave=$(cmd_jq "ip -n ${s_ns} -d -j link show bond0" ".[].linkinfo.info_data.active_slave")
ip -n ${s_ns} link set ${active_slave} down
slowwait 2 active_slave_changed $active_slave
ip -n ${s_ns} link set ${active_slave} up
}
fail_over_mac()
{
# Bring down the first interface on the switch to force the bond to
# select another active interface instead of the first one that joined.
ip -n "$g_ns" link set s0 down
# fail_over_mac none
bond_reset "mode active-backup miimon 100 fail_over_mac 0"
check_all_mac_same
log_test "fail_over_mac 0" "all slaves have same mac"
do_active_backup_failover
check_all_mac_same
log_test "fail_over_mac 0" "failover: all slaves have same mac"
# fail_over_mac active
bond_reset "mode active-backup miimon 100 fail_over_mac 1"
check_bond_mac_same_with_active
log_test "fail_over_mac 1" "bond mac is same with active slave mac"
check_backup_slave_mac_not_change
log_test "fail_over_mac 1" "backup slave mac is not changed"
do_active_backup_failover
check_bond_mac_same_with_active
log_test "fail_over_mac 1" "failover: bond mac is same with active slave mac"
check_backup_slave_mac_not_change
log_test "fail_over_mac 1" "failover: backup slave mac is not changed"
# fail_over_mac follow
bond_reset "mode active-backup miimon 100 fail_over_mac 2"
check_bond_mac_same_with_first
log_test "fail_over_mac 2" "bond mac is same with first slave mac"
check_bond_mac_same_with_active
log_test "fail_over_mac 2" "bond mac is same with active slave mac"
check_backup_slave_mac_inherit
log_test "fail_over_mac 2" "backup slave mac inherit"
do_active_backup_failover
check_bond_mac_same_with_first
log_test "fail_over_mac 2" "failover: bond mac is same with first slave mac"
check_bond_mac_same_with_active
log_test "fail_over_mac 2" "failover: bond mac is same with active slave mac"
check_backup_slave_mac_inherit
log_test "fail_over_mac 2" "failover: backup slave mac inherit"
check_first_slave_random_mac
log_test "fail_over_mac 2" "first slave mac random"
}
vlan_over_bond_arp()
{
local mode="$1"
RET=0
bond_reset "mode $mode arp_interval 100 arp_ip_target 192.0.3.10"
ip -n "${s_ns}" link add bond0.3 link bond0 type vlan id 3
ip -n "${s_ns}" link set bond0.3 up
ip -n "${s_ns}" addr add 192.0.3.1/24 dev bond0.3
ip -n "${s_ns}" addr add 2001:db8::3:1/64 dev bond0.3
slowwait_for_counter 5 5 tc_rule_handle_stats_get \
"dev eth0.3 ingress" 101 ".packets" "-n ${c_ns}" &> /dev/null || RET=1
log_test "vlan over bond arp" "$mode"
}
vlan_over_bond_ns()
{
local mode="$1"
RET=0
if skip_ns; then
log_test_skip "vlan_over_bond ns" "$mode"
return 0
fi
bond_reset "mode $mode arp_interval 100 ns_ip6_target 2001:db8::3:10"
ip -n "${s_ns}" link add bond0.3 link bond0 type vlan id 3
ip -n "${s_ns}" link set bond0.3 up
ip -n "${s_ns}" addr add 192.0.3.1/24 dev bond0.3
ip -n "${s_ns}" addr add 2001:db8::3:1/64 dev bond0.3
slowwait_for_counter 5 5 tc_rule_handle_stats_get \
"dev eth0.3 ingress" 102 ".packets" "-n ${c_ns}" &> /dev/null || RET=1
log_test "vlan over bond ns" "$mode"
}
vlan_over_bond()
{
# add vlan 3 for client
ip -n "${c_ns}" link add eth0.3 link eth0 type vlan id 3
ip -n "${c_ns}" link set eth0.3 up
ip -n "${c_ns}" addr add 192.0.3.10/24 dev eth0.3
ip -n "${c_ns}" addr add 2001:db8::3:10/64 dev eth0.3
# Add tc rule to check the vlan pkts
tc -n "${c_ns}" qdisc add dev eth0.3 clsact
tc -n "${c_ns}" filter add dev eth0.3 ingress protocol arp \
handle 101 flower skip_hw arp_op request \
arp_sip 192.0.3.1 arp_tip 192.0.3.10 action pass
tc -n "${c_ns}" filter add dev eth0.3 ingress protocol ipv6 \
handle 102 flower skip_hw ip_proto icmpv6 \
type 135 src_ip 2001:db8::3:1 action pass
vlan_over_bond_arp "active-backup"
vlan_over_bond_ns "active-backup"
}
trap cleanup EXIT
setup_prepare
setup_wait
tests_run
exit $EXIT_STATUS
@@ -0,0 +1,105 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# Test if a bond interface works with lacp_active=off.
# shellcheck disable=SC2034
REQUIRE_MZ=no
NUM_NETIFS=0
lib_dir=$(dirname "$0")
# shellcheck disable=SC1091
source "$lib_dir"/../../../net/forwarding/lib.sh
# shellcheck disable=SC2317
check_port_state()
{
local netns=$1
local port=$2
local state=$3
ip -n "${netns}" -d -j link show "$port" | \
jq -e ".[].linkinfo.info_slave_data.ad_actor_oper_port_state_str | index(\"${state}\") != null" > /dev/null
}
check_pkt_count()
{
RET=0
local ns="$1"
local iface="$2"
# wait 65s, one per 30s
slowwait_for_counter 65 2 tc_rule_handle_stats_get \
"dev ${iface} egress" 101 ".packets" "-n ${ns}" &> /dev/null
}
setup() {
setup_ns c_ns s_ns
# shellcheck disable=SC2154
ip -n "${c_ns}" link add eth0 type veth peer name eth0 netns "${s_ns}"
ip -n "${c_ns}" link add eth1 type veth peer name eth1 netns "${s_ns}"
# Add tc filter to count the pkts
tc -n "${c_ns}" qdisc add dev eth0 clsact
tc -n "${c_ns}" filter add dev eth0 egress handle 101 protocol 0x8809 matchall action pass
tc -n "${s_ns}" qdisc add dev eth1 clsact
tc -n "${s_ns}" filter add dev eth1 egress handle 101 protocol 0x8809 matchall action pass
ip -n "${s_ns}" link add bond0 type bond mode 802.3ad lacp_active on lacp_rate fast
ip -n "${s_ns}" link set eth0 master bond0
ip -n "${s_ns}" link set eth1 master bond0
ip -n "${c_ns}" link add bond0 type bond mode 802.3ad lacp_active off lacp_rate fast
ip -n "${c_ns}" link set eth0 master bond0
ip -n "${c_ns}" link set eth1 master bond0
}
trap cleanup_all_ns EXIT
setup
# The bond will send 2 lacpdu pkts during init time, let's wait at least 2s
# after interface up
ip -n "${c_ns}" link set bond0 up
sleep 2
# 1. The passive side shouldn't send LACPDU.
check_pkt_count "${c_ns}" "eth0" && RET=1
log_test "802.3ad lacp_active off" "init port"
ip -n "${s_ns}" link set bond0 up
# 2. The passive side should not have the 'active' flag.
RET=0
slowwait 2 check_port_state "${c_ns}" "eth0" "active" && RET=1
log_test "802.3ad lacp_active off" "port state active"
# 3. The active side should have the 'active' flag.
RET=0
slowwait 2 check_port_state "${s_ns}" "eth0" "active" || RET=1
log_test "802.3ad lacp_active on" "port state active"
# 4. Make sure the connection is not expired.
RET=0
slowwait 5 check_port_state "${s_ns}" "eth0" "distributing"
slowwait 10 check_port_state "${s_ns}" "eth0" "expired" && RET=1
log_test "bond 802.3ad lacp_active off" "port connection"
# After testing, disconnect one port on each side to check the state.
ip -n "${s_ns}" link set eth0 nomaster
ip -n "${s_ns}" link set eth0 up
ip -n "${c_ns}" link set eth1 nomaster
ip -n "${c_ns}" link set eth1 up
# Due to Periodic Machine and Rx Machine state change, the bond will still
# send lacpdu pkts in a few seconds. sleep at lease 5s to make sure
# negotiation finished
sleep 5
# 5. The active side should keep sending LACPDU.
check_pkt_count "${s_ns}" "eth1" || RET=1
log_test "bond 802.3ad lacp_active on" "port pkt after disconnect"
# 6. The passive side shouldn't send LACPDU anymore.
check_pkt_count "${c_ns}" "eth0" && RET=1
log_test "bond 802.3ad lacp_active off" "port pkt after disconnect"
exit "$EXIT_STATUS"
@@ -0,0 +1,72 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# Test that bond_header_parse() does not infinitely recurse with stacked bonds.
#
# When a non-Ethernet device (e.g. GRE) is enslaved to a bond that is itself
# enslaved to another bond (bond1 -> bond0 -> gre), receiving a packet via
# AF_PACKET SOCK_DGRAM triggers dev_parse_header() -> bond_header_parse().
# Since parse() used skb->dev (always the topmost bond) instead of a passed-in
# dev pointer, it would recurse back into itself indefinitely.
# shellcheck disable=SC2034
ALL_TESTS="
bond_test_stacked_header_parse
"
REQUIRE_MZ=no
NUM_NETIFS=0
lib_dir=$(dirname "$0")
source "$lib_dir"/../../../net/forwarding/lib.sh
# shellcheck disable=SC2329
bond_test_stacked_header_parse()
{
local devdummy="test-dummy0"
local devgre="test-gre0"
local devbond0="test-bond0"
local devbond1="test-bond1"
# shellcheck disable=SC2034
RET=0
# Setup: dummy -> gre -> bond0 -> bond1
ip link add name "$devdummy" type dummy
ip addr add 10.0.0.1/24 dev "$devdummy"
ip link set "$devdummy" up
ip link add name "$devgre" type gre local 10.0.0.1
ip link add name "$devbond0" type bond mode active-backup
ip link add name "$devbond1" type bond mode active-backup
ip link set "$devgre" master "$devbond0"
ip link set "$devbond0" master "$devbond1"
ip link set "$devgre" up
ip link set "$devbond0" up
ip link set "$devbond1" up
# tcpdump on a non-Ethernet bond uses AF_PACKET SOCK_DGRAM (cooked
# capture), which triggers dev_parse_header() -> bond_header_parse()
# on receive. With the bug, this recurses infinitely.
timeout 5 tcpdump -c 1 -i "$devbond1" >/dev/null 2>&1 &
local tcpdump_pid=$!
sleep 1
# Send a GRE packet to 10.0.0.1 so it arrives via gre -> bond0 -> bond1
python3 -c "from scapy.all import *; send(IP(src='10.0.0.2', dst='10.0.0.1')/GRE()/IP()/UDP(), verbose=0)"
check_err $? "failed to send GRE packet (scapy installed?)"
wait "$tcpdump_pid" 2>/dev/null
ip link del "$devbond1" 2>/dev/null
ip link del "$devbond0" 2>/dev/null
ip link del "$devgre" 2>/dev/null
ip link del "$devdummy" 2>/dev/null
log_test "Stacked bond header_parse does not recurse"
}
tests_run
exit "$EXIT_STATUS"
@@ -0,0 +1,161 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# Topology for Bond mode 1,5,6 testing
#
# +-------------------------+
# | bond0 | Server
# | + | 192.0.2.1/24
# | eth0 | eth1 | 2001:db8::1/24
# | +---+---+ |
# | | | |
# +-------------------------+
# | |
# +-------------------------+
# | | | |
# | +---+-------+---+ | Gateway
# | | br0 | | 192.0.2.254/24
# | +-------+-------+ | 2001:db8::254/24
# | | |
# +-------------------------+
# |
# +-------------------------+
# | | | Client
# | + | 192.0.2.10/24
# | eth0 | 2001:db8::10/24
# +-------------------------+
REQUIRE_MZ=no
NUM_NETIFS=0
lib_dir=$(dirname "$0")
source "$lib_dir"/../../../net/forwarding/lib.sh
s_ns="s-$(mktemp -u XXXXXX)"
c_ns="c-$(mktemp -u XXXXXX)"
g_ns="g-$(mktemp -u XXXXXX)"
s_ip4="192.0.2.1"
c_ip4="192.0.2.10"
g_ip4="192.0.2.254"
s_ip6="2001:db8::1"
c_ip6="2001:db8::10"
g_ip6="2001:db8::254"
mac[0]="00:0a:0b:0c:0d:01"
mac[1]="00:0a:0b:0c:0d:02"
gateway_create()
{
ip netns add ${g_ns}
ip -n ${g_ns} link add br0 type bridge
ip -n ${g_ns} link set br0 up
ip -n ${g_ns} addr add ${g_ip4}/24 dev br0
ip -n ${g_ns} addr add ${g_ip6}/24 dev br0
}
gateway_destroy()
{
ip -n ${g_ns} link del br0
ip netns del ${g_ns}
}
server_create()
{
ip netns add ${s_ns}
ip -n ${s_ns} link add bond0 type bond mode active-backup miimon 100
for i in $(seq 0 1); do
ip -n ${s_ns} link add eth${i} type veth peer name s${i} netns ${g_ns}
ip -n "${s_ns}" link set "eth${i}" addr "${mac[$i]}"
ip -n ${g_ns} link set s${i} up
ip -n ${g_ns} link set s${i} master br0
ip -n ${s_ns} link set eth${i} master bond0
tc -n ${g_ns} qdisc add dev s${i} clsact
done
ip -n ${s_ns} link set bond0 up
ip -n ${s_ns} addr add ${s_ip4}/24 dev bond0
ip -n ${s_ns} addr add ${s_ip6}/24 dev bond0
}
# Reset bond with new mode and options
bond_reset()
{
# Count the eth link number in real-time as this function
# maybe called from other topologies.
local link_num=$(ip -n ${s_ns} -br link show | grep -c "^eth")
local param="$1"
link_num=$((link_num -1))
ip -n ${s_ns} link set bond0 down
ip -n ${s_ns} link del bond0
ip -n ${s_ns} link add bond0 type bond $param
for i in $(seq 0 ${link_num}); do
ip -n ${s_ns} link set eth$i master bond0
done
ip -n ${s_ns} link set bond0 up
ip -n ${s_ns} addr add ${s_ip4}/24 dev bond0
ip -n ${s_ns} addr add ${s_ip6}/24 dev bond0
# Wait for IPv6 address ready as it needs DAD
slowwait 2 ip netns exec ${s_ns} ping6 ${c_ip6} -c 1 -W 0.1 &> /dev/null
}
server_destroy()
{
# Count the eth link number in real-time as this function
# maybe called from other topologies.
local link_num=$(ip -n ${s_ns} -br link show | grep -c "^eth")
link_num=$((link_num -1))
for i in $(seq 0 ${link_num}); do
ip -n ${s_ns} link del eth${i}
done
ip netns del ${s_ns}
}
client_create()
{
ip netns add ${c_ns}
ip -n ${c_ns} link add eth0 type veth peer name c0 netns ${g_ns}
ip -n ${g_ns} link set c0 up
ip -n ${g_ns} link set c0 master br0
ip -n ${c_ns} link set eth0 up
ip -n ${c_ns} addr add ${c_ip4}/24 dev eth0
ip -n ${c_ns} addr add ${c_ip6}/24 dev eth0
}
client_destroy()
{
ip -n ${c_ns} link del eth0
ip netns del ${c_ns}
}
setup_prepare()
{
gateway_create
server_create
client_create
}
cleanup()
{
pre_cleanup
client_destroy
server_destroy
gateway_destroy
}
bond_check_connection()
{
local msg=${1:-"check connection"}
slowwait 2 ip netns exec ${s_ns} ping ${c_ip4} -c 1 -W 0.1 &> /dev/null
ip netns exec ${s_ns} ping ${c_ip4} -c5 -i 0.1 &>/dev/null
check_err $? "${msg}: ping failed"
ip netns exec ${s_ns} ping6 ${c_ip6} -c5 -i 0.1 &>/dev/null
check_err $? "${msg}: ping6 failed"
}
@@ -0,0 +1,45 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# Topology for Bond mode 1,5,6 testing
#
# +-------------------------------------+
# | bond0 |
# | + | Server
# | eth0 | eth1 eth2 | 192.0.2.1/24
# | +-------------------+ | 2001:db8::1/24
# | | | | |
# +-------------------------------------+
# | | |
# +-------------------------------------+
# | | | | |
# | +---+---------+---------+---+ | Gateway
# | | br0 | | 192.0.2.254/24
# | +-------------+-------------+ | 2001:db8::254/24
# | | |
# +-------------------------------------+
# |
# +-------------------------------------+
# | | | Client
# | + | 192.0.2.10/24
# | eth0 | 2001:db8::10/24
# +-------------------------------------+
source bond_topo_2d1c.sh
mac[2]="00:0a:0b:0c:0d:03"
setup_prepare()
{
gateway_create
server_create
client_create
# Add the extra device as we use 3 down links for bond0
local i=2
ip -n ${s_ns} link add eth${i} type veth peer name s${i} netns ${g_ns}
ip -n "${s_ns}" link set "eth${i}" addr "${mac[$i]}"
ip -n ${g_ns} link set s${i} up
ip -n ${g_ns} link set s${i} master br0
ip -n ${s_ns} link set eth${i} master bond0
tc -n ${g_ns} qdisc add dev s${i} clsact
}
@@ -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"
@@ -0,0 +1,22 @@
CONFIG_BONDING=y
CONFIG_BRIDGE=y
CONFIG_CONFIGFS_FS=y
CONFIG_DUMMY=y
CONFIG_INET_ESP=y
CONFIG_INET_ESP_OFFLOAD=y
CONFIG_IPV6=y
CONFIG_IPVLAN=y
CONFIG_MACVLAN=y
CONFIG_NET_ACT_GACT=y
CONFIG_NET_CLS_FLOWER=y
CONFIG_NET_CLS_MATCHALL=m
CONFIG_NETCONSOLE=m
CONFIG_NETCONSOLE_DYNAMIC=y
CONFIG_NETCONSOLE_EXTENDED_LOG=y
CONFIG_NETDEVSIM=m
CONFIG_NET_IPGRE=y
CONFIG_NET_SCH_INGRESS=y
CONFIG_NLMON=y
CONFIG_VETH=y
CONFIG_VLAN_8021Q=m
CONFIG_XFRM_USER=m
@@ -0,0 +1,109 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# Test bond device handling of addr lists (dev->uc, mc)
#
ALL_TESTS="
bond_cleanup_mode1
bond_cleanup_mode4
bond_listen_lacpdu_multicast_case_down
bond_listen_lacpdu_multicast_case_up
"
REQUIRE_MZ=no
NUM_NETIFS=0
lib_dir=$(dirname "$0")
source "$lib_dir"/../../../net/forwarding/lib.sh
source "$lib_dir"/lag_lib.sh
destroy()
{
local ifnames=(dummy1 dummy2 bond1 mv0)
local ifname
for ifname in "${ifnames[@]}"; do
ip link del "$ifname" &>/dev/null
done
}
cleanup()
{
pre_cleanup
destroy
}
# bond driver control paths vary between modes that have a primary slave
# (bond_uses_primary()) and others. Test both kinds of modes.
bond_cleanup_mode1()
{
RET=0
test_LAG_cleanup "bonding" "active-backup"
}
bond_cleanup_mode4() {
RET=0
test_LAG_cleanup "bonding" "802.3ad"
}
bond_listen_lacpdu_multicast()
{
# Initial state of bond device, up | down
local init_state=$1
local lacpdu_mc="01:80:c2:00:00:02"
ip link add dummy1 type dummy
ip link add bond1 "$init_state" type bond mode 802.3ad
ip link set dev dummy1 master bond1
if [ "$init_state" = "down" ]; then
ip link set dev bond1 up
fi
grep_bridge_fdb "$lacpdu_mc" bridge fdb show brport dummy1 >/dev/null
check_err $? "LACPDU multicast address not present on slave (1)"
ip link set dev bond1 down
not grep_bridge_fdb "$lacpdu_mc" bridge fdb show brport dummy1 >/dev/null
check_err $? "LACPDU multicast address still present on slave"
ip link set dev bond1 up
grep_bridge_fdb "$lacpdu_mc" bridge fdb show brport dummy1 >/dev/null
check_err $? "LACPDU multicast address not present on slave (2)"
cleanup
log_test "bonding LACPDU multicast address to slave (from bond $init_state)"
}
# The LACPDU mc addr is added by different paths depending on the initial state
# of the bond when enslaving a device. Test both cases.
bond_listen_lacpdu_multicast_case_down()
{
RET=0
bond_listen_lacpdu_multicast "down"
}
bond_listen_lacpdu_multicast_case_up()
{
RET=0
bond_listen_lacpdu_multicast "up"
}
trap cleanup EXIT
tests_run
exit "$EXIT_STATUS"
@@ -0,0 +1,166 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
NAMESPACES=""
# Test that a link aggregation device (bonding, team) removes the hardware
# addresses that it adds on its underlying devices.
test_LAG_cleanup()
{
local driver=$1
local mode=$2
local ucaddr="02:00:00:12:34:56"
local addr6="fe80::78:9abc/64"
local mcaddr="33:33:ff:78:9a:bc"
local name
ip link add dummy1 type dummy
ip link add dummy2 type dummy
if [ "$driver" = "bonding" ]; then
name="bond1"
ip link add "$name" up type bond mode "$mode"
ip link set dev dummy1 master "$name"
ip link set dev dummy2 master "$name"
elif [ "$driver" = "team" ]; then
name="team0"
ip link add "$name" type team
ip link set dev dummy1 master "$name"
ip link set dev dummy2 master "$name"
ip link set dev "$name" up
else
check_err 1
log_test test_LAG_cleanup ": unknown driver \"$driver\""
return
fi
# Used to test dev->uc handling
ip link add mv0 link "$name" up address "$ucaddr" type macvlan
# Used to test dev->mc handling
ip address add "$addr6" dev "$name"
# Check that addresses were added as expected
(grep_bridge_fdb "$ucaddr" bridge fdb show dev dummy1 ||
grep_bridge_fdb "$ucaddr" bridge fdb show dev dummy2) >/dev/null
check_err $? "macvlan unicast address not found on a slave"
# mcaddr is added asynchronously by addrconf_dad_work(), use busywait
(busywait 10000 grep_bridge_fdb "$mcaddr" bridge fdb show dev dummy1 ||
grep_bridge_fdb "$mcaddr" bridge fdb show dev dummy2) >/dev/null
check_err $? "IPv6 solicited-node multicast mac address not found on a slave"
ip link set dev "$name" down
ip link del "$name"
not grep_bridge_fdb "$ucaddr" bridge fdb show >/dev/null
check_err $? "macvlan unicast address still present on a slave"
not grep_bridge_fdb "$mcaddr" bridge fdb show >/dev/null
check_err $? "IPv6 solicited-node multicast mac address still present on a slave"
cleanup
log_test "$driver cleanup mode $mode"
}
# Build a generic 2 node net namespace with 2 connections
# between the namespaces
#
# +-----------+ +-----------+
# | node1 | | node2 |
# | | | |
# | | | |
# | eth0 +-------+ eth0 |
# | | | |
# | eth1 +-------+ eth1 |
# | | | |
# +-----------+ +-----------+
lag_setup2x2()
{
local state=${1:-down}
local namespaces="lag_node1 lag_node2"
# create namespaces
for n in ${namespaces}; do
ip netns add ${n}
done
# wire up namespaces
ip link add name lag1 type veth peer name lag1-end
ip link set dev lag1 netns lag_node1 $state name eth0
ip link set dev lag1-end netns lag_node2 $state name eth0
ip link add name lag1 type veth peer name lag1-end
ip link set dev lag1 netns lag_node1 $state name eth1
ip link set dev lag1-end netns lag_node2 $state name eth1
NAMESPACES="${namespaces}"
}
# cleanup all lag related namespaces
lag_cleanup()
{
for n in ${NAMESPACES}; do
ip netns delete ${n} >/dev/null 2>&1 || true
done
}
SWITCH="lag_node1"
CLIENT="lag_node2"
CLIENTIP="172.20.2.1"
SWITCHIP="172.20.2.2"
lag_setup_network()
{
lag_setup2x2 "down"
# create switch
ip netns exec ${SWITCH} ip link add br0 up type bridge
ip netns exec ${SWITCH} ip link set eth0 master br0 up
ip netns exec ${SWITCH} ip link set eth1 master br0 up
ip netns exec ${SWITCH} ip addr add ${SWITCHIP}/24 dev br0
}
lag_reset_network()
{
ip netns exec ${CLIENT} ip link del bond0
ip netns exec ${SWITCH} ip link set eth0 up
ip netns exec ${SWITCH} ip link set eth1 up
}
create_bond()
{
# create client
ip netns exec ${CLIENT} ip link set eth0 down
ip netns exec ${CLIENT} ip link set eth1 down
ip netns exec ${CLIENT} ip link add bond0 type bond $@
ip netns exec ${CLIENT} ip link set eth0 master bond0
ip netns exec ${CLIENT} ip link set eth1 master bond0
ip netns exec ${CLIENT} ip link set bond0 up
ip netns exec ${CLIENT} ip addr add ${CLIENTIP}/24 dev bond0
}
test_bond_recovery()
{
RET=0
create_bond $@
# verify connectivity
slowwait 2 ip netns exec ${CLIENT} ping ${SWITCHIP} -c 2 -W 0.1 &> /dev/null
check_err $? "No connectivity"
# force the links of the bond down
ip netns exec ${SWITCH} ip link set eth0 down
sleep 2
ip netns exec ${SWITCH} ip link set eth0 up
ip netns exec ${SWITCH} ip link set eth1 down
# re-verify connectivity
slowwait 2 ip netns exec ${CLIENT} ping ${SWITCHIP} -c 2 -W 0.1 &> /dev/null
local rc=$?
check_err $rc "Bond failed to recover"
log_test "$1 ($2) bond recovery"
lag_reset_network
}
@@ -0,0 +1,45 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
# Regression Test:
# When the bond is configured with down/updelay and the link state of
# slave members flaps if there are no remaining members up the bond
# should immediately select a member to bring up. (from bonding.txt
# section 13.1 paragraph 4)
#
# +-------------+ +-----------+
# | client | | switch |
# | | | |
# | +--------| link1 |-----+ |
# | | +-------+ | |
# | | | | | |
# | | +-------+ | |
# | | bond | link2 | Br0 | |
# +-------------+ +-----------+
# 172.20.2.1 172.20.2.2
REQUIRE_MZ=no
REQUIRE_JQ=no
NUM_NETIFS=0
lib_dir=$(dirname "$0")
source "$lib_dir"/../../../net/forwarding/lib.sh
source "$lib_dir"/lag_lib.sh
cleanup()
{
lag_cleanup
}
trap cleanup 0 1 2
lag_setup_network
test_bond_recovery mode 1 miimon 100 updelay 0
test_bond_recovery mode 1 miimon 100 updelay 200
test_bond_recovery mode 1 miimon 100 updelay 500
test_bond_recovery mode 1 miimon 100 updelay 1000
test_bond_recovery mode 1 miimon 100 updelay 2000
test_bond_recovery mode 1 miimon 100 updelay 5000
test_bond_recovery mode 1 miimon 100 updelay 10000
exit "$EXIT_STATUS"
@@ -0,0 +1,45 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
# Regression Test:
# When the bond is configured with down/updelay and the link state of
# slave members flaps if there are no remaining members up the bond
# should immediately select a member to bring up. (from bonding.txt
# section 13.1 paragraph 4)
#
# +-------------+ +-----------+
# | client | | switch |
# | | | |
# | +--------| link1 |-----+ |
# | | +-------+ | |
# | | | | | |
# | | +-------+ | |
# | | bond | link2 | Br0 | |
# +-------------+ +-----------+
# 172.20.2.1 172.20.2.2
REQUIRE_MZ=no
REQUIRE_JQ=no
NUM_NETIFS=0
lib_dir=$(dirname "$0")
source "$lib_dir"/../../../net/forwarding/lib.sh
source "$lib_dir"/lag_lib.sh
cleanup()
{
lag_cleanup
}
trap cleanup 0 1 2
lag_setup_network
test_bond_recovery mode 2 miimon 100 updelay 0
test_bond_recovery mode 2 miimon 100 updelay 200
test_bond_recovery mode 2 miimon 100 updelay 500
test_bond_recovery mode 2 miimon 100 updelay 1000
test_bond_recovery mode 2 miimon 100 updelay 2000
test_bond_recovery mode 2 miimon 100 updelay 5000
test_bond_recovery mode 2 miimon 100 updelay 10000
exit "$EXIT_STATUS"
@@ -0,0 +1,361 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-2.0
#
# This selftest exercises trying to have multiple netpoll users at the same
# time.
#
# This selftest has multiple smalls test inside, and the goal is to
# get interfaces with bonding and netconsole in different orders in order
# to catch any possible issue.
#
# The main test composes of four interfaces being created using netdevsim; two
# of them are bonded to serve as the netconsole's transmit interface. The
# remaining two interfaces are similarly bonded and assigned to a separate
# network namespace, which acts as the receive interface, where socat monitors
# for incoming messages.
#
# A netconsole message is then sent to ensure it is properly received across
# this configuration.
#
# Later, run a few other tests, to make sure that bonding and netconsole
# cannot coexist.
#
# The test's objective is to exercise netpoll usage when managed simultaneously
# by multiple subsystems (netconsole and bonding).
#
# Author: Breno Leitao <leitao@debian.org>
set -euo pipefail
SCRIPTDIR=$(dirname "$(readlink -e "${BASH_SOURCE[0]}")")
source "${SCRIPTDIR}"/../lib/sh/lib_netcons.sh
modprobe netdevsim 2> /dev/null || true
modprobe netconsole 2> /dev/null || true
modprobe bonding 2> /dev/null || true
modprobe veth 2> /dev/null || true
# The content of kmsg will be save to the following file
OUTPUT_FILE="/tmp/${TARGET}"
# Check for basic system dependency and exit if not found
check_for_dependencies
# Set current loglevel to KERN_INFO(6), and default to KERN_NOTICE(5)
echo "6 5" > /proc/sys/kernel/printk
# Remove the namespace, interfaces and netconsole target on exit
trap cleanup_bond EXIT
FORMAT="extended"
IP_VERSION="ipv4"
VETH0="veth"$(( RANDOM % 256))
VETH1="veth"$((256 + RANDOM % 256))
TXNS=""
RXNS=""
# Create "bond_tx_XX" and "bond_rx_XX" interfaces, and set DSTIF and SRCIF with
# the bonding interfaces
function setup_bonding_ifaces() {
local RAND=$(( RANDOM % 100 ))
BOND_TX_MAIN_IF="bond_tx_$RAND"
BOND_RX_MAIN_IF="bond_rx_$RAND"
# Setup TX
if ! ip -n "${TXNS}" link add "${BOND_TX_MAIN_IF}" type bond mode balance-rr
then
echo "Failed to create bond TX interface. Is CONFIG_BONDING set?" >&2
# only clean nsim ifaces and namespace. Nothing else has been
# initialized
cleanup_bond_nsim
trap - EXIT
exit "${ksft_skip}"
fi
# create_netdevsim() got the interface up, but it needs to be down
# before being enslaved.
ip -n "${TXNS}" \
link set "${BOND_TX1_SLAVE_IF}" down
ip -n "${TXNS}" \
link set "${BOND_TX2_SLAVE_IF}" down
ip -n "${TXNS}" \
link set "${BOND_TX1_SLAVE_IF}" master "${BOND_TX_MAIN_IF}"
ip -n "${TXNS}" \
link set "${BOND_TX2_SLAVE_IF}" master "${BOND_TX_MAIN_IF}"
ip -n "${TXNS}" \
link set "${BOND_TX_MAIN_IF}" up
# Setup RX
ip -n "${RXNS}" \
link add "${BOND_RX_MAIN_IF}" type bond mode balance-rr
ip -n "${RXNS}" \
link set "${BOND_RX1_SLAVE_IF}" down
ip -n "${RXNS}" \
link set "${BOND_RX2_SLAVE_IF}" down
ip -n "${RXNS}" \
link set "${BOND_RX1_SLAVE_IF}" master "${BOND_RX_MAIN_IF}"
ip -n "${RXNS}" \
link set "${BOND_RX2_SLAVE_IF}" master "${BOND_RX_MAIN_IF}"
ip -n "${RXNS}" \
link set "${BOND_RX_MAIN_IF}" up
export DSTIF="${BOND_RX_MAIN_IF}"
export SRCIF="${BOND_TX_MAIN_IF}"
}
# Create 4 netdevsim interfaces. Two of them will be bound to TX bonding iface
# and the other two will be bond to the RX interface (on the other namespace)
function create_ifaces_bond() {
BOND_TX1_SLAVE_IF=$(create_netdevsim "${NSIM_BOND_TX_1}" "${TXNS}")
BOND_TX2_SLAVE_IF=$(create_netdevsim "${NSIM_BOND_TX_2}" "${TXNS}")
BOND_RX1_SLAVE_IF=$(create_netdevsim "${NSIM_BOND_RX_1}" "${RXNS}")
BOND_RX2_SLAVE_IF=$(create_netdevsim "${NSIM_BOND_RX_2}" "${RXNS}")
}
# netdevsim link BOND_TX to BOND_RX interfaces
function link_ifaces_bond() {
local BOND_TX1_SLAVE_IFIDX
local BOND_TX2_SLAVE_IFIDX
local BOND_RX1_SLAVE_IFIDX
local BOND_RX2_SLAVE_IFIDX
local TXNS_FD
local RXNS_FD
BOND_TX1_SLAVE_IFIDX=$(ip netns exec "${TXNS}" \
cat /sys/class/net/"$BOND_TX1_SLAVE_IF"/ifindex)
BOND_TX2_SLAVE_IFIDX=$(ip netns exec "${TXNS}" \
cat /sys/class/net/"$BOND_TX2_SLAVE_IF"/ifindex)
BOND_RX1_SLAVE_IFIDX=$(ip netns exec "${RXNS}" \
cat /sys/class/net/"$BOND_RX1_SLAVE_IF"/ifindex)
BOND_RX2_SLAVE_IFIDX=$(ip netns exec "${RXNS}" \
cat /sys/class/net/"$BOND_RX2_SLAVE_IF"/ifindex)
exec {TXNS_FD}</var/run/netns/"${TXNS}"
exec {RXNS_FD}</var/run/netns/"${RXNS}"
# Linking TX ifaces to the RX ones (on the other namespace)
echo "${TXNS_FD}:$BOND_TX1_SLAVE_IFIDX $RXNS_FD:$BOND_RX1_SLAVE_IFIDX" \
> "$NSIM_DEV_SYS_LINK"
echo "${TXNS_FD}:$BOND_TX2_SLAVE_IFIDX $RXNS_FD:$BOND_RX2_SLAVE_IFIDX" \
> "$NSIM_DEV_SYS_LINK"
exec {TXNS_FD}<&-
exec {RXNS_FD}<&-
}
function create_all_ifaces() {
# setup_ns function is coming from lib.sh
setup_ns TXNS RXNS
export NAMESPACE="${RXNS}"
# Create two interfaces for RX and two for TX
create_ifaces_bond
# Link netlink ifaces
link_ifaces_bond
}
# configure DSTIF and SRCIF IPs
function configure_ifaces_ips() {
local IP_VERSION=${1:-"ipv4"}
select_ipv4_or_ipv6 "${IP_VERSION}"
ip -n "${RXNS}" addr add "${DSTIP}"/24 dev "${DSTIF}"
ip -n "${RXNS}" link set "${DSTIF}" up
ip -n "${TXNS}" addr add "${SRCIP}"/24 dev "${SRCIF}"
ip -n "${TXNS}" link set "${SRCIF}" up
}
function test_enable_netpoll_on_enslaved_iface() {
echo 0 > "${NETCONS_PATH}"/enabled
# At this stage, BOND_TX1_SLAVE_IF is enslaved to BOND_TX_MAIN_IF, and
# linked to BOND_RX1_SLAVE_IF inside the namespace.
echo "${BOND_TX1_SLAVE_IF}" > "${NETCONS_PATH}"/dev_name
# This should fail with the following message in dmesg:
# netpoll: netconsole: ethX is a slave device, aborting
set +e
enable_netcons_ns 2> /dev/null
set -e
if [[ $(cat "${NETCONS_PATH}"/enabled) -eq 1 ]]
then
echo "test failed: Bonding and netpoll cannot co-exists." >&2
exit "${ksft_fail}"
fi
}
function test_delete_bond_and_reenable_target() {
ip -n "${TXNS}" \
link delete "${BOND_TX_MAIN_IF}" type bond
# BOND_TX1_SLAVE_IF is not attached to a bond interface anymore
# netpoll can be plugged in there
echo "${BOND_TX1_SLAVE_IF}" > "${NETCONS_PATH}"/dev_name
# this should work, since the interface is not enslaved
enable_netcons_ns
if [[ $(cat "${NETCONS_PATH}"/enabled) -eq 0 ]]
then
echo "test failed: Unable to start netpoll on an unbond iface." >&2
exit "${ksft_fail}"
fi
}
# Send a netconsole message to the netconsole target
function test_send_netcons_msg_through_bond_iface() {
# Listen for netconsole port inside the namespace and
# destination interface
listen_port_and_save_to "${OUTPUT_FILE}" "${IP_VERSION}" &
# Wait for socat to start and listen to the port.
wait_for_port "${RXNS}" "${PORT}" "${IP_VERSION}"
# Send the message
echo "${MSG}: ${TARGET}" > /dev/kmsg
# Wait until socat saves the file to disk
busywait "${BUSYWAIT_TIMEOUT}" test -s "${OUTPUT_FILE}"
# Make sure the message was received in the dst part
# and exit
validate_result "${OUTPUT_FILE}" "${FORMAT}"
# kill socat in case it is still running
pkill_socat
}
# BOND_TX1_SLAVE_IF has netconsole enabled on it, bind it to BOND_TX_MAIN_IF.
# Given BOND_TX_MAIN_IF was deleted, recreate it first
function test_enslave_netcons_enabled_iface {
# netconsole got disabled while the interface was down
if [[ $(cat "${NETCONS_PATH}"/enabled) -eq 0 ]]
then
echo "test failed: netconsole expected to be enabled against BOND_TX1_SLAVE_IF" >&2
exit "${ksft_fail}"
fi
# recreate the bonding iface. it got deleted by previous
# test (test_delete_bond_and_reenable_target)
ip -n "${TXNS}" \
link add "${BOND_TX_MAIN_IF}" type bond mode balance-rr
# sub-interface need to be down before attaching to bonding
# This will also disable netconsole.
ip -n "${TXNS}" \
link set "${BOND_TX1_SLAVE_IF}" down
ip -n "${TXNS}" \
link set "${BOND_TX1_SLAVE_IF}" master "${BOND_TX_MAIN_IF}"
ip -n "${TXNS}" \
link set "${BOND_TX_MAIN_IF}" up
# netconsole got disabled while the interface was down
if [[ $(cat "${NETCONS_PATH}"/enabled) -eq 1 ]]
then
echo "test failed: Device is part of a bond iface, cannot have netcons enabled" >&2
exit "${ksft_fail}"
fi
}
# Get netconsole enabled on a bonding interface and attach a second
# sub-interface.
function test_enslave_iface_to_bond {
# BOND_TX_MAIN_IF has only BOND_TX1_SLAVE_IF right now
echo "${BOND_TX_MAIN_IF}" > "${NETCONS_PATH}"/dev_name
enable_netcons_ns
# netcons is attached to bond0 and BOND_TX1_SLAVE_IF is
# part of BOND_TX_MAIN_IF. Attach BOND_TX2_SLAVE_IF to BOND_TX_MAIN_IF.
ip -n "${TXNS}" \
link set "${BOND_TX2_SLAVE_IF}" master "${BOND_TX_MAIN_IF}"
if [[ $(cat "${NETCONS_PATH}"/enabled) -eq 0 ]]
then
echo "test failed: Netconsole should be enabled on bonding interface. Failed" >&2
exit "${ksft_fail}"
fi
}
function test_enslave_iff_disabled_netpoll_iface {
local ret
# Create two interfaces. veth interfaces it known to have
# IFF_DISABLE_NETPOLL set
if ! ip link add "${VETH0}" type veth peer name "${VETH1}"
then
echo "Failed to create veth TX interface. Is CONFIG_VETH set?" >&2
exit "${ksft_skip}"
fi
set +e
# This will print RTNETLINK answers: Device or resource busy
ip link set "${VETH0}" master "${BOND_TX_MAIN_IF}" 2> /dev/null
ret=$?
set -e
if [[ $ret -eq 0 ]]
then
echo "test failed: veth interface could not be enslaved"
exit "${ksft_fail}"
fi
}
# Given that netconsole picks the current net namespace, we need to enable it
# from inside the TXNS namespace
function enable_netcons_ns() {
ip netns exec "${TXNS}" sh -c \
"mount -t configfs configfs /sys/kernel/config && echo 1 > $NETCONS_PATH/enabled"
}
####################
# Tests start here #
####################
# Create regular interfaces using netdevsim and link them
create_all_ifaces
# Setup the bonding interfaces
# BOND_RX_MAIN_IF has BOND_RX{1,2}_SLAVE_IF
# BOND_TX_MAIN_IF has BOND_TX{1,2}_SLAVE_IF
setup_bonding_ifaces
# Configure the ips as BOND_RX1_SLAVE_IF and BOND_TX1_SLAVE_IF
configure_ifaces_ips "${IP_VERSION}"
_create_dynamic_target "${FORMAT}" "${NETCONS_PATH}"
enable_netcons_ns
set_user_data
# Test #1 : Create an bonding interface and attach netpoll into
# the bonding interface. Netconsole/netpoll should work on
# the bonding interface.
test_send_netcons_msg_through_bond_iface
echo "test #1: netpoll on bonding interface worked. Test passed" >&2
# Test #2: Attach netpoll to an enslaved interface
# Try to attach netpoll to an enslaved sub-interface (while still being part of
# a bonding interface), which shouldn't be allowed
test_enable_netpoll_on_enslaved_iface
echo "test #2: netpoll correctly rejected enslaved interface (expected behavior). Test passed." >&2
# Test #3: Unplug the sub-interface from bond and enable netconsole
# Detach the interface from a bonding interface and attach netpoll again
test_delete_bond_and_reenable_target
echo "test #3: Able to attach to an unbound interface. Test passed." >&2
# Test #4: Enslave a sub-interface that had netconsole enabled
# Try to enslave an interface that has netconsole/netpoll enabled.
# Previous test has netconsole enabled in BOND_TX1_SLAVE_IF, try to enslave it
test_enslave_netcons_enabled_iface
echo "test #4: Enslaving an interface with netpoll attached. Test passed." >&2
# Test #5: Enslave a sub-interface to a bonding interface
# Enslave an interface to a bond interface that has netpoll attached
# At this stage, BOND_TX_MAIN_IF is created and BOND_TX1_SLAVE_IF is part of
# it. Netconsole is currently disabled
test_enslave_iface_to_bond
echo "test #5: Enslaving an interface to bond+netpoll. Test passed." >&2
# Test #6: Enslave a IFF_DISABLE_NETPOLL sub-interface to a bonding interface
# At this stage, BOND_TX_MAIN_IF has both sub interface and netconsole is
# enabled. This test will try to enslave an a veth (IFF_DISABLE_NETPOLL) interface
# and it should fail, with netpoll: veth0 doesn't support polling
test_enslave_iff_disabled_netpoll_iface
echo "test #6: Enslaving IFF_DISABLE_NETPOLL ifaces to bond iface is not supported. Test passed." >&2
cleanup_bond
trap - EXIT
exit "${EXIT_STATUS}"
@@ -0,0 +1 @@
timeout=1200
@@ -0,0 +1,20 @@
CONFIG_CONFIGFS_FS=y
CONFIG_DEBUG_INFO_BTF=y
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
@@ -0,0 +1,36 @@
# SPDX-License-Identifier: GPL-2.0+ OR MIT
TEST_PROGS := \
bridge_locked_port.sh \
bridge_mdb.sh \
bridge_mld.sh \
bridge_vlan_aware.sh \
bridge_vlan_mcast.sh \
bridge_vlan_unaware.sh \
local_termination.sh \
no_forwarding.sh \
tc_actions.sh \
test_bridge_fdb_stress.sh \
# end of TEST_PROGS
TEST_FILES := \
forwarding.config \
run_net_forwarding_test.sh \
# end of TEST_FILES
TEST_INCLUDES := \
../../../net/forwarding/bridge_locked_port.sh \
../../../net/forwarding/bridge_mdb.sh \
../../../net/forwarding/bridge_mld.sh \
../../../net/forwarding/bridge_vlan_aware.sh \
../../../net/forwarding/bridge_vlan_mcast.sh \
../../../net/forwarding/bridge_vlan_unaware.sh \
../../../net/forwarding/lib.sh \
../../../net/forwarding/local_termination.sh \
../../../net/forwarding/no_forwarding.sh \
../../../net/forwarding/tc_actions.sh \
../../../net/forwarding/tc_common.sh \
../../../net/lib.sh \
# end of TEST_INCLUDES
include ../../../lib.mk
@@ -0,0 +1 @@
run_net_forwarding_test.sh
@@ -0,0 +1 @@
run_net_forwarding_test.sh
@@ -0,0 +1 @@
run_net_forwarding_test.sh
@@ -0,0 +1 @@
run_net_forwarding_test.sh
@@ -0,0 +1 @@
run_net_forwarding_test.sh
@@ -0,0 +1 @@
run_net_forwarding_test.sh
@@ -0,0 +1,2 @@
NETIF_CREATE=no
STABLE_MAC_ADDRS=yes
@@ -0,0 +1 @@
run_net_forwarding_test.sh
@@ -0,0 +1 @@
run_net_forwarding_test.sh
@@ -0,0 +1,9 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
libdir=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")
testname=$(basename "${BASH_SOURCE[0]}")
source "$libdir"/forwarding.config
cd "$libdir"/../../../net/forwarding/ || exit 1
source "./$testname" "$@"
@@ -0,0 +1 @@
run_net_forwarding_test.sh
@@ -0,0 +1 @@
run_net_forwarding_test.sh
@@ -0,0 +1,47 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
# Bridge FDB entries can be offloaded to DSA switches without holding the
# rtnl_mutex. Traditionally this mutex has conferred drivers implicit
# serialization, which means their code paths are not well tested in the
# presence of concurrency.
# This test creates a background task that stresses the FDB by adding and
# deleting an entry many times in a row without the rtnl_mutex held.
# It then tests the driver resistance to concurrency by calling .ndo_fdb_dump
# (with rtnl_mutex held) from a foreground task.
# Since either the FDB dump or the additions/removals can fail, but the
# additions and removals are performed in deferred as opposed to process
# context, we cannot simply check for user space error codes.
WAIT_TIME=1
NUM_NETIFS=1
REQUIRE_JQ="no"
REQUIRE_MZ="no"
NETIF_CREATE="no"
lib_dir=$(dirname "$0")
source "$lib_dir"/../../../net/forwarding/lib.sh
cleanup() {
echo "Cleaning up"
kill $pid && wait $pid &> /dev/null
ip link del br0
echo "Please check kernel log for errors"
}
trap 'cleanup' EXIT
eth=${NETIFS[p1]}
ip link del br0 2>&1 >/dev/null || :
ip link add br0 type bridge && ip link set $eth master br0
(while :; do
bridge fdb add 00:01:02:03:04:05 dev $eth master static
bridge fdb del 00:01:02:03:04:05 dev $eth master static
done) &
pid=$!
for i in $(seq 1 50); do
bridge fdb show > /dev/null
sleep 3
echo "$((${i} * 2))% complete..."
done
+473
View File
@@ -0,0 +1,473 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""
GRO (Generic Receive Offload) conformance tests.
Validates that GRO coalescing works correctly by running the gro
binary in different configurations and checking for correct packet
coalescing behavior.
Test cases:
- data_same: Same size data packets coalesce
- data_lrg_sml: Large packet followed by smaller one coalesces
- data_lrg_1byte: Large packet followed by 1B one coalesces (Ethernet padding)
- data_sml_lrg: Small packet followed by larger one doesn't coalesce
- ack: Pure ACK packets do not coalesce
- flags_psh: Packets with PSH flag don't coalesce
- flags_syn: Packets with SYN flag don't coalesce
- flags_rst: Packets with RST flag don't coalesce
- flags_urg: Packets with URG flag don't coalesce
- flags_cwr: Packets with CWR flag don't coalesce
- tcp_csum: Packets with incorrect checksum don't coalesce
- tcp_seq: Packets with non-consecutive seqno don't coalesce
- tcp_ts: Packets with different timestamp options don't coalesce
- tcp_opt: Packets with different TCP options don't coalesce
- ip_ecn: Packets with different ECN don't coalesce
- ip_tos: Packets with different TOS don't coalesce
- ip_ttl: (IPv4) Packets with different TTL don't coalesce
- ip_opt: (IPv4) Packets with IP options don't coalesce
- ip_frag4: (IPv4) IPv4 fragments don't coalesce
- ip_id_df*: (IPv4) IP ID field coalescing tests
- ip_frag6: (IPv6) IPv6 fragments don't coalesce
- ip_v6ext_same: (IPv6) IPv6 ext header with same payload coalesces
- ip_v6ext_diff: (IPv6) IPv6 ext header with different payload doesn't coalesce
- large_max: Packets exceeding GRO_MAX_SIZE don't coalesce
- large_rem: Large packet remainder handling
"""
import glob
import os
import re
from lib.py import ksft_run, ksft_exit, ksft_pr
from lib.py import NetDrvEpEnv, KsftFailEx, KsftXfailEx
from lib.py import NetdevFamily, EthtoolFamily
from lib.py import bkg, cmd, defer, ethtool, ip
from lib.py import ksft_variants, KsftNamedVariant
# gro.c uses hardcoded DPORT=8000
GRO_DPORT = 8000
def _resolve_dmac(cfg, ipver):
"""
Find the destination MAC address remote host should use to send packets
towards the local host. It may be a router / gateway address.
"""
attr = "dmac" + ipver
# Cache the response across test cases
if hasattr(cfg, attr):
return getattr(cfg, attr)
route = ip(f"-{ipver} route get {cfg.addr_v[ipver]}",
json=True, host=cfg.remote)[0]
gw = route.get("gateway")
# Local L2 segment, address directly
if not gw:
setattr(cfg, attr, cfg.dev['address'])
return getattr(cfg, attr)
# ping to make sure neighbor is resolved,
# bind to an interface, for v6 the GW is likely link local
cmd(f"ping -c1 -W0 -I{cfg.remote_ifname} {gw}", host=cfg.remote)
neigh = ip(f"neigh get {gw} dev {cfg.remote_ifname}",
json=True, host=cfg.remote)[0]
setattr(cfg, attr, neigh['lladdr'])
return getattr(cfg, attr)
def _write_defer_restore(cfg, path, val, defer_undo=False):
with open(path, "r", encoding="utf-8") as fp:
orig_val = fp.read().strip()
if str(val) == orig_val:
return
with open(path, "w", encoding="utf-8") as fp:
fp.write(val)
if defer_undo:
defer(_write_defer_restore, cfg, path, orig_val)
def _set_mtu_restore(dev, mtu, host):
if dev['mtu'] < mtu:
ip(f"link set dev {dev['ifname']} mtu {mtu}", host=host)
defer(ip, f"link set dev {dev['ifname']} mtu {dev['mtu']}", host=host)
def _set_ethtool_feat(dev, current, feats, host=None):
s2n = {True: "on", False: "off"}
new = ["-K", dev]
old = ["-K", dev]
no_change = True
for name, state in feats.items():
new += [name, s2n[state]]
old += [name, s2n[current[name]["active"]]]
if current[name]["active"] != state:
no_change = False
if current[name]["fixed"]:
raise KsftXfailEx(f"Device does not support {name}")
if no_change:
return
eth_cmd = ethtool(" ".join(new), host=host)
defer(ethtool, " ".join(old), host=host)
# If ethtool printed something kernel must have modified some features
if eth_cmd.stdout:
ksft_pr(eth_cmd)
def _get_queue_stats(cfg, queue_id):
"""Get stats for a specific Rx queue."""
cfg.wait_hw_stats_settle()
data = cfg.netnl.qstats_get({"ifindex": cfg.ifindex, "scope": ["queue"]},
dump=True)
for q in data:
if q.get('queue-type') == 'rx' and q.get('queue-id') == queue_id:
return q
return {}
def _require_ntuple(cfg):
features = ethtool(f"-k {cfg.ifname}", json=True)[0]
if not features["ntuple-filters"]["active"]:
if features["ntuple-filters"]["fixed"]:
raise KsftXfailEx("Device does not support ntuple-filters")
ethtool(f"-K {cfg.ifname} ntuple-filters on")
defer(ethtool, f"-K {cfg.ifname} ntuple-filters off")
def _setup_isolated_queue(cfg):
"""Set up an isolated queue for testing using ntuple filter.
Remove queue 1 from the default RSS context and steer test traffic to it.
"""
_require_ntuple(cfg)
test_queue = 1
qcnt = len(glob.glob(f"/sys/class/net/{cfg.ifname}/queues/rx-*"))
if qcnt < 2:
raise KsftXfailEx(f"Need at least 2 queues, have {qcnt}")
# Remove queue 1 from default RSS context by setting its weight to 0
weights = ["1"] * qcnt
weights[test_queue] = "0"
ethtool(f"-X {cfg.ifname} weight " + " ".join(weights))
defer(ethtool, f"-X {cfg.ifname} default")
# Set up ntuple filter to steer our test traffic to the isolated queue
flow = f"flow-type tcp{cfg.addr_ipver} "
flow += f"dst-ip {cfg.addr} dst-port {GRO_DPORT} action {test_queue}"
output = ethtool(f"-N {cfg.ifname} {flow}").stdout
ntuple_id = int(output.split()[-1])
defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
return test_queue
def _setup_queue_count(cfg, num_queues):
"""Configure the NIC to use a specific number of queues."""
channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
ch_max = channels.get('combined-max', 0)
qcnt = channels['combined-count']
if ch_max < num_queues:
raise KsftXfailEx(f"Need at least {num_queues} queues, max={ch_max}")
defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
ethtool(f"-L {cfg.ifname} combined {num_queues}")
def _run_gro_bin(cfg, test_name, protocol=None, num_flows=None,
order_check=False, verbose=False, fail=False):
"""Run gro binary with given test and return the process result."""
if not hasattr(cfg, "bin_remote"):
cfg.bin_local = cfg.net_lib_dir / "gro"
cfg.bin_remote = cfg.remote.deploy(cfg.bin_local)
if protocol is None:
ipver = cfg.addr_ipver
protocol = f"ipv{ipver}"
else:
ipver = "6" if protocol[-1] == "6" else "4"
dmac = _resolve_dmac(cfg, ipver)
base_args = [
f"--{protocol}",
f"--dmac {dmac}",
f"--smac {cfg.remote_dev['address']}",
f"--daddr {cfg.addr_v[ipver]}",
f"--saddr {cfg.remote_addr_v[ipver]}",
f"--test {test_name}",
]
if num_flows:
base_args.append(f"--num-flows {num_flows}")
if order_check:
base_args.append("--order-check")
if verbose:
base_args.append("--verbose")
args = " ".join(base_args)
rx_cmd = f"{cfg.bin_local} {args} --rx --iface {cfg.ifname}"
tx_cmd = f"{cfg.bin_remote} {args} --iface {cfg.remote_ifname}"
with bkg(rx_cmd, ksft_ready=True, exit_wait=True, fail=fail) as rx_proc:
cmd(tx_cmd, host=cfg.remote)
return rx_proc
def _setup(cfg, mode, test_name):
""" Setup hardware loopback mode for GRO testing. """
if not hasattr(cfg, "bin_remote"):
cfg.bin_local = cfg.net_lib_dir / "gro"
cfg.bin_remote = cfg.remote.deploy(cfg.bin_local)
if not hasattr(cfg, "feat"):
cfg.feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
cfg.remote_feat = ethtool(f"-k {cfg.remote_ifname}",
host=cfg.remote, json=True)[0]
# "large_*" tests need at least 4k MTU
if test_name.startswith("large_"):
_set_mtu_restore(cfg.dev, 4096, None)
_set_mtu_restore(cfg.remote_dev, 4096, cfg.remote)
if mode == "sw":
flush_path = f"/sys/class/net/{cfg.ifname}/gro_flush_timeout"
irq_path = f"/sys/class/net/{cfg.ifname}/napi_defer_hard_irqs"
_write_defer_restore(cfg, flush_path, "200000", defer_undo=True)
_write_defer_restore(cfg, irq_path, "10", defer_undo=True)
_set_ethtool_feat(cfg.ifname, cfg.feat,
{"generic-receive-offload": True,
"rx-gro-hw": False,
"large-receive-offload": False})
elif mode == "hw":
_set_ethtool_feat(cfg.ifname, cfg.feat,
{"generic-receive-offload": False,
"rx-gro-hw": True,
"large-receive-offload": False})
# Some NICs treat HW GRO as a GRO sub-feature so disabling GRO
# will also clear HW GRO. Use a hack of installing XDP generic
# to skip SW GRO, even when enabled.
feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
if not feat["rx-gro-hw"]["active"]:
ksft_pr("Driver clears HW GRO and SW GRO is cleared, using generic XDP workaround")
prog = cfg.net_lib_dir / "xdp_dummy.bpf.o"
ip(f"link set dev {cfg.ifname} xdpgeneric obj {prog} sec xdp")
defer(ip, f"link set dev {cfg.ifname} xdpgeneric off")
# Attaching XDP may change features, fetch the latest state
feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
_set_ethtool_feat(cfg.ifname, feat,
{"generic-receive-offload": True,
"rx-gro-hw": True,
"large-receive-offload": False})
elif mode == "lro":
# netdevsim advertises LRO for feature inheritance testing with
# bonding/team tests but it doesn't actually perform the offload
cfg.require_nsim(nsim_test=False)
_set_ethtool_feat(cfg.ifname, cfg.feat,
{"generic-receive-offload": False,
"rx-gro-hw": False,
"large-receive-offload": True})
try:
# Disable TSO for local tests
cfg.require_nsim() # will raise KsftXfailEx if not running on nsim
_set_ethtool_feat(cfg.remote_ifname, cfg.remote_feat,
{"tcp-segmentation-offload": False},
host=cfg.remote)
except KsftXfailEx:
pass
def _gro_variants():
"""Generator that yields all combinations of protocol and test types."""
# Tests that work for all protocols
common_tests = [
"data_same", "data_lrg_sml", "data_sml_lrg", "data_lrg_1byte",
"data_burst",
"ack",
"flags_psh", "flags_syn", "flags_rst", "flags_urg", "flags_cwr",
"tcp_csum", "tcp_seq", "tcp_ts", "tcp_opt",
"ip_ecn", "ip_tos",
"large_max", "large_rem",
]
# Tests specific to IPv4
ipv4_tests = [
"ip_csum",
"ip_ttl", "ip_opt", "ip_frag4",
"ip_id_df1_inc", "ip_id_df1_fixed",
"ip_id_df0_inc", "ip_id_df0_fixed",
"ip_id_df1_inc_fixed", "ip_id_df1_fixed_inc",
]
# Tests specific to IPv6
ipv6_tests = [
"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:
yield mode, protocol, test_name
if protocol in ["ipv4", "ipip"]:
for test_name in ipv4_tests:
yield mode, protocol, test_name
elif protocol == "ipv6":
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):
"""Run a single GRO test with retries."""
ipver = "6" if protocol[-1] == "6" else "4"
cfg.require_ipver(ipver)
_setup(cfg, mode, test_name)
# Each test is run 6 times to deflake, because given the receive timing,
# not all packets that should coalesce will be considered in the same flow
# on every try.
max_retries = 6
for attempt in range(max_retries):
fail_now = attempt >= max_retries - 1
rx_proc = _run_gro_bin(cfg, test_name, protocol=protocol,
verbose=True, fail=fail_now)
if rx_proc.ret == 0:
return
ksft_pr(rx_proc)
# ret==42 means the receiver detected over-coalescing.
# This is unambiguous proof of a bug, retries can only cause
# false negatives.
if rx_proc.ret == 42:
raise KsftFailEx(f"GRO over-coalesced in {protocol}/{test_name}")
if test_name.startswith("large_") and os.environ.get("KSFT_MACHINE_SLOW"):
ksft_pr(f"Ignoring {protocol}/{test_name} failure due to slow environment")
return
ksft_pr(f"Attempt {attempt + 1}/{max_retries} failed, retrying...")
def _capacity_variants():
"""Generate variants for capacity test: mode x queue setup."""
setups = [
("isolated", _setup_isolated_queue),
("1q", lambda cfg: _setup_queue_count(cfg, 1)),
("8q", lambda cfg: _setup_queue_count(cfg, 8)),
]
for mode in ["sw", "hw", "lro"]:
for name, func in setups:
yield KsftNamedVariant(f"{mode}_{name}", mode, func)
@ksft_variants(_capacity_variants())
def test_gro_capacity(cfg, mode, setup_func):
"""
Probe GRO capacity.
Start with 8 flows and increase by 2x on each successful run.
Retry up to 3 times on failure.
Variants combine mode (sw, hw, lro) with queue setup:
- isolated: Use a single queue isolated from RSS
- 1q: Configure NIC to use 1 queue
- 8q: Configure NIC to use 8 queues
"""
max_retries = 3
_setup(cfg, mode, "capacity")
queue_id = setup_func(cfg)
num_flows = 8
while True:
success = False
for attempt in range(max_retries):
if queue_id is not None:
stats_before = _get_queue_stats(cfg, queue_id)
rx_proc = _run_gro_bin(cfg, "capacity", num_flows=num_flows)
output = rx_proc.stdout
if queue_id is not None:
stats_after = _get_queue_stats(cfg, queue_id)
qstat_pkts = (stats_after.get('rx-packets', 0) -
stats_before.get('rx-packets', 0))
gro_pkts = (stats_after.get('rx-hw-gro-packets', 0) -
stats_before.get('rx-hw-gro-packets', 0))
qstat_str = f" qstat={qstat_pkts} hw-gro={gro_pkts}"
else:
qstat_str = ""
# Parse and print STATS line
match = re.search(
r'STATS: received=(\d+) wire=(\d+) coalesced=(\d+)', output)
if match:
received = int(match.group(1))
wire = int(match.group(2))
coalesced = int(match.group(3))
status = "PASS" if received == num_flows else "MISS"
ksft_pr(f"flows={num_flows} attempt={attempt + 1} "
f"received={received} wire={wire} "
f"coalesced={coalesced}{qstat_str} [{status}]")
if received == num_flows:
success = True
break
else:
ksft_pr(rx_proc)
ksft_pr(f"flows={num_flows} attempt={attempt + 1}"
f"{qstat_str} [FAIL - can't parse stats]")
if not success:
ksft_pr(f"Stopped at {num_flows} flows")
break
num_flows *= 2
def main() -> None:
""" Ksft boiler plate main """
with NetDrvEpEnv(__file__) as cfg:
cfg.ethnl = EthtoolFamily()
cfg.netnl = NetdevFamily()
ksft_run(cases=[test, test_gro_capacity], args=(cfg,))
ksft_exit()
if __name__ == "__main__":
main()
+329
View File
@@ -0,0 +1,329 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
import errno
import os
import random
from typing import Union
from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_raises, KsftSkipEx
from lib.py import CmdExitFailure, EthtoolFamily, NlError
from lib.py import NetDrvEnv
from lib.py import defer, ethtool, ip
def _get_hds_mode(cfg, netnl) -> str:
try:
rings = netnl.rings_get({'header': {'dev-index': cfg.ifindex}})
except NlError as e:
raise KsftSkipEx('ring-get not supported by device')
if 'tcp-data-split' not in rings:
raise KsftSkipEx('tcp-data-split not supported by device')
return rings['tcp-data-split']
def _xdp_onoff(cfg):
prog = cfg.net_lib_dir / "xdp_dummy.bpf.o"
ip("link set dev %s xdp obj %s sec xdp" %
(cfg.ifname, prog))
ip("link set dev %s xdp off" % cfg.ifname)
def _ioctl_ringparam_modify(cfg, netnl) -> None:
"""
Helper for performing a hopefully unimportant IOCTL SET.
IOCTL does not support HDS, so it should not affect the HDS config.
"""
try:
rings = netnl.rings_get({'header': {'dev-index': cfg.ifindex}})
except NlError as e:
raise KsftSkipEx('ring-get not supported by device')
if 'tx' not in rings:
raise KsftSkipEx('setting Tx ring size not supported')
try:
ethtool(f"--disable-netlink -G {cfg.ifname} tx {rings['tx'] // 2}")
except CmdExitFailure as e:
ethtool(f"--disable-netlink -G {cfg.ifname} tx {rings['tx'] * 2}")
defer(ethtool, f"-G {cfg.ifname} tx {rings['tx']}")
def get_hds(cfg, netnl) -> None:
_get_hds_mode(cfg, netnl)
def get_hds_thresh(cfg, netnl) -> None:
try:
rings = netnl.rings_get({'header': {'dev-index': cfg.ifindex}})
except NlError as e:
raise KsftSkipEx('ring-get not supported by device')
if 'hds-thresh' not in rings:
raise KsftSkipEx('hds-thresh not supported by device')
def _hds_reset(cfg, netnl, rings) -> None:
cur = netnl.rings_get({'header': {'dev-index': cfg.ifindex}})
arg = {'header': {'dev-index': cfg.ifindex}}
if cur.get('tcp-data-split') != rings.get('tcp-data-split'):
# Try to reset to "unknown" first, we don't know if the setting
# was the default or user chose it. Default seems more likely.
arg['tcp-data-split'] = "unknown"
netnl.rings_set(arg)
cur = netnl.rings_get({'header': {'dev-index': cfg.ifindex}})
if cur['tcp-data-split'] == rings['tcp-data-split']:
del arg['tcp-data-split']
else:
# Try the explicit setting
arg['tcp-data-split'] = rings['tcp-data-split']
if cur.get('hds-thresh') != rings.get('hds-thresh'):
arg['hds-thresh'] = rings['hds-thresh']
if len(arg) > 1:
netnl.rings_set(arg)
def _defer_reset_hds(cfg, netnl) -> Union[dict, None]:
try:
rings = netnl.rings_get({'header': {'dev-index': cfg.ifindex}})
if 'hds-thresh' in rings or 'tcp-data-split' in rings:
defer(_hds_reset, cfg, netnl, rings)
except NlError as e:
pass
def set_hds_enable(cfg, netnl) -> None:
_defer_reset_hds(cfg, netnl)
try:
netnl.rings_set({'header': {'dev-index': cfg.ifindex}, 'tcp-data-split': 'enabled'})
except NlError as e:
if e.error == errno.EINVAL:
raise KsftSkipEx("disabling of HDS not supported by the device")
elif e.error == errno.EOPNOTSUPP:
raise KsftSkipEx("ring-set not supported by the device")
try:
rings = netnl.rings_get({'header': {'dev-index': cfg.ifindex}})
except NlError as e:
raise KsftSkipEx('ring-get not supported by device')
if 'tcp-data-split' not in rings:
raise KsftSkipEx('tcp-data-split not supported by device')
ksft_eq('enabled', rings['tcp-data-split'])
def set_hds_disable(cfg, netnl) -> None:
_defer_reset_hds(cfg, netnl)
try:
netnl.rings_set({'header': {'dev-index': cfg.ifindex}, 'tcp-data-split': 'disabled'})
except NlError as e:
if e.error == errno.EINVAL:
raise KsftSkipEx("disabling of HDS not supported by the device")
elif e.error == errno.EOPNOTSUPP:
raise KsftSkipEx("ring-set not supported by the device")
try:
rings = netnl.rings_get({'header': {'dev-index': cfg.ifindex}})
except NlError as e:
raise KsftSkipEx('ring-get not supported by device')
if 'tcp-data-split' not in rings:
raise KsftSkipEx('tcp-data-split not supported by device')
ksft_eq('disabled', rings['tcp-data-split'])
def set_hds_thresh_zero(cfg, netnl) -> None:
_defer_reset_hds(cfg, netnl)
try:
netnl.rings_set({'header': {'dev-index': cfg.ifindex}, 'hds-thresh': 0})
except NlError as e:
if e.error == errno.EINVAL:
raise KsftSkipEx("hds-thresh-set not supported by the device")
elif e.error == errno.EOPNOTSUPP:
raise KsftSkipEx("ring-set not supported by the device")
try:
rings = netnl.rings_get({'header': {'dev-index': cfg.ifindex}})
except NlError as e:
raise KsftSkipEx('ring-get not supported by device')
if 'hds-thresh' not in rings:
raise KsftSkipEx('hds-thresh not supported by device')
ksft_eq(0, rings['hds-thresh'])
def set_hds_thresh_random(cfg, netnl) -> None:
_defer_reset_hds(cfg, netnl)
try:
rings = netnl.rings_get({'header': {'dev-index': cfg.ifindex}})
except NlError as e:
raise KsftSkipEx('ring-get not supported by device')
if 'hds-thresh' not in rings:
raise KsftSkipEx('hds-thresh not supported by device')
if 'hds-thresh-max' not in rings:
raise KsftSkipEx('hds-thresh-max not defined by device')
if rings['hds-thresh-max'] < 2:
raise KsftSkipEx('hds-thresh-max is too small')
elif rings['hds-thresh-max'] == 2:
hds_thresh = 1
else:
while True:
hds_thresh = random.randint(1, rings['hds-thresh-max'] - 1)
if hds_thresh != rings['hds-thresh']:
break
try:
netnl.rings_set({'header': {'dev-index': cfg.ifindex}, 'hds-thresh': hds_thresh})
except NlError as e:
if e.error == errno.EINVAL:
raise KsftSkipEx("hds-thresh-set not supported by the device")
elif e.error == errno.EOPNOTSUPP:
raise KsftSkipEx("ring-set not supported by the device")
rings = netnl.rings_get({'header': {'dev-index': cfg.ifindex}})
ksft_eq(hds_thresh, rings['hds-thresh'])
def set_hds_thresh_max(cfg, netnl) -> None:
_defer_reset_hds(cfg, netnl)
try:
rings = netnl.rings_get({'header': {'dev-index': cfg.ifindex}})
except NlError as e:
raise KsftSkipEx('ring-get not supported by device')
if 'hds-thresh' not in rings:
raise KsftSkipEx('hds-thresh not supported by device')
try:
netnl.rings_set({'header': {'dev-index': cfg.ifindex}, 'hds-thresh': rings['hds-thresh-max']})
except NlError as e:
if e.error == errno.EINVAL:
raise KsftSkipEx("hds-thresh-set not supported by the device")
elif e.error == errno.EOPNOTSUPP:
raise KsftSkipEx("ring-set not supported by the device")
rings = netnl.rings_get({'header': {'dev-index': cfg.ifindex}})
ksft_eq(rings['hds-thresh'], rings['hds-thresh-max'])
def set_hds_thresh_gt(cfg, netnl) -> None:
_defer_reset_hds(cfg, netnl)
try:
rings = netnl.rings_get({'header': {'dev-index': cfg.ifindex}})
except NlError as e:
raise KsftSkipEx('ring-get not supported by device')
if 'hds-thresh' not in rings:
raise KsftSkipEx('hds-thresh not supported by device')
if 'hds-thresh-max' not in rings:
raise KsftSkipEx('hds-thresh-max not defined by device')
hds_gt = rings['hds-thresh-max'] + 1
with ksft_raises(NlError) as e:
netnl.rings_set({'header': {'dev-index': cfg.ifindex}, 'hds-thresh': hds_gt})
ksft_eq(e.exception.nl_msg.error, -errno.EINVAL)
def set_xdp(cfg, netnl) -> None:
"""
Enable single-buffer XDP on the device.
When HDS is in "auto" / UNKNOWN mode, XDP installation should work.
"""
mode = _get_hds_mode(cfg, netnl)
if mode == 'enabled':
_defer_reset_hds(cfg, netnl)
netnl.rings_set({'header': {'dev-index': cfg.ifindex},
'tcp-data-split': 'unknown'})
_xdp_onoff(cfg)
def enabled_set_xdp(cfg, netnl) -> None:
"""
Enable single-buffer XDP on the device.
When HDS is in "enabled" mode, XDP installation should not work.
"""
_get_hds_mode(cfg, netnl)
netnl.rings_set({'header': {'dev-index': cfg.ifindex},
'tcp-data-split': 'enabled'})
defer(netnl.rings_set, {'header': {'dev-index': cfg.ifindex},
'tcp-data-split': 'unknown'})
with ksft_raises(CmdExitFailure) as e:
_xdp_onoff(cfg)
def set_xdp(cfg, netnl) -> None:
"""
Enable single-buffer XDP on the device.
When HDS is in "auto" / UNKNOWN mode, XDP installation should work.
"""
mode = _get_hds_mode(cfg, netnl)
if mode == 'enabled':
netnl.rings_set({'header': {'dev-index': cfg.ifindex},
'tcp-data-split': 'unknown'})
_xdp_onoff(cfg)
def enabled_set_xdp(cfg, netnl) -> None:
"""
Enable single-buffer XDP on the device.
When HDS is in "enabled" mode, XDP installation should not work.
"""
_get_hds_mode(cfg, netnl) # Trigger skip if not supported
netnl.rings_set({'header': {'dev-index': cfg.ifindex},
'tcp-data-split': 'enabled'})
defer(netnl.rings_set, {'header': {'dev-index': cfg.ifindex},
'tcp-data-split': 'unknown'})
with ksft_raises(CmdExitFailure) as e:
_xdp_onoff(cfg)
def ioctl(cfg, netnl) -> None:
mode1 = _get_hds_mode(cfg, netnl)
_ioctl_ringparam_modify(cfg, netnl)
mode2 = _get_hds_mode(cfg, netnl)
ksft_eq(mode1, mode2)
def ioctl_set_xdp(cfg, netnl) -> None:
"""
Like set_xdp(), but we perturb the settings via the legacy ioctl.
"""
mode = _get_hds_mode(cfg, netnl)
if mode == 'enabled':
netnl.rings_set({'header': {'dev-index': cfg.ifindex},
'tcp-data-split': 'unknown'})
_ioctl_ringparam_modify(cfg, netnl)
_xdp_onoff(cfg)
def ioctl_enabled_set_xdp(cfg, netnl) -> None:
"""
Enable single-buffer XDP on the device.
When HDS is in "enabled" mode, XDP installation should not work.
"""
_get_hds_mode(cfg, netnl) # Trigger skip if not supported
netnl.rings_set({'header': {'dev-index': cfg.ifindex},
'tcp-data-split': 'enabled'})
defer(netnl.rings_set, {'header': {'dev-index': cfg.ifindex},
'tcp-data-split': 'unknown'})
with ksft_raises(CmdExitFailure) as e:
_xdp_onoff(cfg)
def main() -> None:
with NetDrvEnv(__file__, queue_count=3) as cfg:
ksft_run([get_hds,
get_hds_thresh,
set_hds_disable,
set_hds_enable,
set_hds_thresh_random,
set_hds_thresh_zero,
set_hds_thresh_max,
set_hds_thresh_gt,
set_xdp,
enabled_set_xdp,
ioctl,
ioctl_set_xdp,
ioctl_enabled_set_xdp],
args=(cfg, EthtoolFamily()))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,4 @@
# SPDX-License-Identifier: GPL-2.0-only
iou-zcrx
ncdevmem
toeplitz
@@ -0,0 +1,91 @@
# SPDX-License-Identifier: GPL-2.0+ OR MIT
# Check if io_uring supports zero-copy receive
HAS_IOURING_ZCRX := $(shell \
echo -e '#include <liburing.h>\n' \
'void *func = (void *)io_uring_register_ifq;\n' \
'int main() {return 0;}' | \
$(CC) -luring -x c - -o /dev/null 2>&1 && echo y)
ifeq ($(HAS_IOURING_ZCRX),y)
COND_GEN_FILES += iou-zcrx
else
$(warning excluding iouring tests, liburing not installed or too old)
endif
TEST_GEN_FILES := \
$(COND_GEN_FILES) \
# end of TEST_GEN_FILES
TEST_PROGS = \
csum.py \
devlink_port_split.py \
devlink_rate_tc_bw.py \
devmem.py \
ethtool.sh \
ethtool_extended_state.sh \
ethtool_mm.sh \
ethtool_rmon.sh \
ethtool_std_stats.sh \
gro_hw.py \
hw_stats_l3.sh \
hw_stats_l3_gre.sh \
iou-zcrx.py \
ipsec_vxlan.py \
irq.py \
loopback.sh \
nic_timestamp.py \
nk_devmem.py \
nk_netns.py \
nk_qlease.py \
ntuple.py \
pp_alloc_fail.py \
rss_api.py \
rss_ctx.py \
rss_drv.py \
rss_flow_label.py \
rss_input_xfrm.py \
toeplitz.py \
tso.py \
userns_devmem.py \
uso.py \
xdp_metadata.py \
xsk_reconfig.py \
#
TEST_FILES := \
devmem_lib.py \
ethtool_lib.sh \
#
TEST_INCLUDES := \
$(wildcard lib/py/*.py ../lib/py/*.py) \
../../../net/lib.sh \
../../../net/forwarding/ipip_lib.sh \
../../../net/forwarding/lib.sh \
../../../net/forwarding/tc_common.sh \
#
# YNL files, must be before "include ..lib.mk"
YNL_GEN_FILES := \
ncdevmem \
toeplitz \
# end of YNL_GEN_FILES
TEST_GEN_FILES += $(YNL_GEN_FILES)
TEST_GEN_FILES += $(patsubst %.c,%.o,$(wildcard *.bpf.c))
include ../../../lib.mk
# YNL build
YNL_GENS := \
ethtool \
netdev \
# end of YNL_GENS
include ../../../net/ynl.mk
include ../../../net/bpf.mk
ifeq ($(HAS_IOURING_ZCRX),y)
$(OUTPUT)/iou-zcrx: LDLIBS += -luring
endif
@@ -0,0 +1,26 @@
CONFIG_BPF_SYSCALL=y
CONFIG_FAIL_FUNCTION=y
CONFIG_FAULT_INJECTION=y
CONFIG_FAULT_INJECTION_DEBUG_FS=y
CONFIG_FUNCTION_ERROR_INJECTION=y
CONFIG_HUGETLBFS=y
CONFIG_INET6_ESP=y
CONFIG_INET6_ESP_OFFLOAD=y
CONFIG_INET_ESP=y
CONFIG_INET_ESP_OFFLOAD=y
CONFIG_IO_URING=y
CONFIG_IPV6=y
CONFIG_IPV6_GRE=y
CONFIG_IPV6_SIT=y
CONFIG_IPV6_TUNNEL=y
CONFIG_NET_CLS_ACT=y
CONFIG_NET_CLS_BPF=y
CONFIG_NET_IPGRE=y
CONFIG_NET_IPGRE_DEMUX=y
CONFIG_NET_IPIP=y
CONFIG_NETKIT=y
CONFIG_NET_SCH_INGRESS=y
CONFIG_UDMABUF=y
CONFIG_USER_NS=y
CONFIG_VXLAN=y
CONFIG_XFRM_USER=y
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""Run the tools/testing/selftests/net/csum testsuite."""
from os import path
from lib.py import ksft_run, ksft_exit, KsftSkipEx
from lib.py import EthtoolFamily, NetDrvEpEnv
from lib.py import bkg, cmd, wait_port_listen
def test_receive(cfg, ipver="6", extra_args=None):
"""Test local nic checksum receive. Remote host sends crafted packets."""
if not cfg.have_rx_csum:
raise KsftSkipEx(f"Test requires rx checksum offload on {cfg.ifname}")
ip_args = f"-{ipver} -S {cfg.remote_addr_v[ipver]} -D {cfg.addr_v[ipver]}"
rx_cmd = f"{cfg.bin_local} -i {cfg.ifname} -n 100 {ip_args} -r 1 -R {extra_args}"
tx_cmd = f"{cfg.bin_remote} -i {cfg.remote_ifname} -n 100 {ip_args} -r 1 -T {extra_args}"
with bkg(rx_cmd, exit_wait=True):
wait_port_listen(34000, proto="udp")
cmd(tx_cmd, host=cfg.remote)
def test_transmit(cfg, ipver="6", extra_args=None):
"""Test local nic checksum transmit. Remote host verifies packets."""
if (not cfg.have_tx_csum_generic and
not (cfg.have_tx_csum_ipv4 and ipver == "4") and
not (cfg.have_tx_csum_ipv6 and ipver == "6")):
raise KsftSkipEx(f"Test requires tx checksum offload on {cfg.ifname}")
ip_args = f"-{ipver} -S {cfg.addr_v[ipver]} -D {cfg.remote_addr_v[ipver]}"
# Cannot randomize input when calculating zero checksum
if extra_args != "-U -Z":
extra_args += " -r 1"
rx_cmd = f"{cfg.bin_remote} -i {cfg.remote_ifname} -L 1 -n 100 {ip_args} -R {extra_args}"
tx_cmd = f"{cfg.bin_local} -i {cfg.ifname} -L 1 -n 100 {ip_args} -T {extra_args}"
with bkg(rx_cmd, host=cfg.remote, exit_wait=True):
wait_port_listen(34000, proto="udp", host=cfg.remote)
cmd(tx_cmd)
def test_builder(name, cfg, ipver="6", tx=False, extra_args=""):
"""Construct specific tests from the common template.
Most tests follow the same basic pattern, differing only in
Direction of the test and optional flags passed to csum."""
def f(cfg):
cfg.require_ipver(ipver)
if tx:
test_transmit(cfg, ipver, extra_args)
else:
test_receive(cfg, ipver, extra_args)
f.__name__ = f"ipv{ipver}_" + name
return f
def check_nic_features(cfg) -> None:
"""Test whether Tx and Rx checksum offload are enabled.
If the device under test has either off, then skip the relevant tests."""
cfg.have_tx_csum_generic = False
cfg.have_tx_csum_ipv4 = False
cfg.have_tx_csum_ipv6 = False
cfg.have_rx_csum = False
ethnl = EthtoolFamily()
features = ethnl.features_get({"header": {"dev-index": cfg.ifindex}})
for f in features["active"]["bits"]["bit"]:
if f["name"] == "tx-checksum-ip-generic":
cfg.have_tx_csum_generic = True
elif f["name"] == "tx-checksum-ipv4":
cfg.have_tx_csum_ipv4 = True
elif f["name"] == "tx-checksum-ipv6":
cfg.have_tx_csum_ipv6 = True
elif f["name"] == "rx-checksum":
cfg.have_rx_csum = True
def main() -> None:
with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
check_nic_features(cfg)
cfg.bin_local = cfg.net_lib_dir / "csum"
cfg.bin_remote = cfg.remote.deploy(cfg.bin_local)
cases = []
for ipver in ["4", "6"]:
cases.append(test_builder("rx_tcp", cfg, ipver, False, "-t"))
cases.append(test_builder("rx_tcp_invalid", cfg, ipver, False, "-t -E"))
cases.append(test_builder("rx_udp", cfg, ipver, False, ""))
cases.append(test_builder("rx_udp_invalid", cfg, ipver, False, "-E"))
cases.append(test_builder("tx_udp_csum_offload", cfg, ipver, True, "-U"))
cases.append(test_builder("tx_udp_zero_checksum", cfg, ipver, True, "-U -Z"))
ksft_run(cases=cases, args=(cfg, ))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,309 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
from subprocess import PIPE, Popen
import json
import time
import argparse
import collections
import sys
#
# Test port split configuration using devlink-port lanes attribute.
# The test is skipped in case the attribute is not available.
#
# First, check that all the ports with 1 lane fail to split.
# Second, check that all the ports with more than 1 lane can be split
# to all valid configurations (e.g., split to 2, split to 4 etc.)
#
# Kselftest framework requirement - SKIP code is 4
KSFT_SKIP=4
Port = collections.namedtuple('Port', 'bus_info name')
def run_command(cmd, should_fail=False):
"""
Run a command in subprocess.
Return: Tuple of (stdout, stderr).
"""
p = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True)
stdout, stderr = p.communicate()
stdout, stderr = stdout.decode(), stderr.decode()
if stderr != "" and not should_fail:
print("Error sending command: %s" % cmd)
print(stdout)
print(stderr)
return stdout, stderr
class devlink_ports(object):
"""
Class that holds information on the devlink ports, required to the tests;
if_names: A list of interfaces in the devlink ports.
"""
def get_if_names(dev):
"""
Get a list of physical devlink ports.
Return: Array of tuples (bus_info/port, if_name).
"""
arr = []
cmd = "devlink -j port show"
stdout, stderr = run_command(cmd)
assert stderr == ""
ports = json.loads(stdout)['port']
validate_devlink_output(ports, 'flavour')
for port in ports:
if dev in port:
if ports[port]['flavour'] == 'physical':
arr.append(Port(bus_info=port, name=ports[port]['netdev']))
return arr
def __init__(self, dev):
self.if_names = devlink_ports.get_if_names(dev)
def get_max_lanes(port):
"""
Get the $port's maximum number of lanes.
Return: number of lanes, e.g. 1, 2, 4 and 8.
"""
cmd = "devlink -j port show %s" % port
stdout, stderr = run_command(cmd)
assert stderr == ""
values = list(json.loads(stdout)['port'].values())[0]
if 'lanes' in values:
lanes = values['lanes']
else:
lanes = 0
return lanes
def get_split_ability(port):
"""
Get the $port split ability.
Return: split ability, true or false.
"""
cmd = "devlink -j port show %s" % port.name
stdout, stderr = run_command(cmd)
assert stderr == ""
values = list(json.loads(stdout)['port'].values())[0]
return values['splittable']
def split(k, port, should_fail=False):
"""
Split $port into $k ports.
If should_fail == True, the split should fail. Otherwise, should pass.
Return: Array of sub ports after splitting.
If the $port wasn't split, the array will be empty.
"""
cmd = "devlink port split %s count %s" % (port.bus_info, k)
stdout, stderr = run_command(cmd, should_fail=should_fail)
if should_fail:
if not test(stderr != "", "%s is unsplittable" % port.name):
print("split an unsplittable port %s" % port.name)
return create_split_group(port, k)
else:
if stderr == "":
return create_split_group(port, k)
print("didn't split a splittable port %s" % port.name)
return []
def unsplit(port):
"""
Unsplit $port.
"""
cmd = "devlink port unsplit %s" % port
stdout, stderr = run_command(cmd)
test(stderr == "", "Unsplit port %s" % port)
def exists(port, dev):
"""
Check if $port exists in the devlink ports.
Return: True is so, False otherwise.
"""
return any(dev_port.name == port
for dev_port in devlink_ports.get_if_names(dev))
def exists_and_lanes(ports, lanes, dev):
"""
Check if every port in the list $ports exists in the devlink ports and has
$lanes number of lanes after splitting.
Return: True if both are True, False otherwise.
"""
for port in ports:
max_lanes = get_max_lanes(port)
if not exists(port, dev):
print("port %s doesn't exist in devlink ports" % port)
return False
if max_lanes != lanes:
print("port %s has %d lanes, but %s were expected"
% (port, lanes, max_lanes))
return False
return True
def test(cond, msg):
"""
Check $cond and print a message accordingly.
Return: True is pass, False otherwise.
"""
if cond:
print("TEST: %-60s [ OK ]" % msg)
else:
print("TEST: %-60s [FAIL]" % msg)
return cond
def create_split_group(port, k):
"""
Create the split group for $port.
Return: Array with $k elements, which are the split port group.
"""
return list(port.name + "s" + str(i) for i in range(k))
def split_unsplittable_port(port, k):
"""
Test that splitting of unsplittable port fails.
"""
# split to max
new_split_group = split(k, port, should_fail=True)
if new_split_group != []:
unsplit(port.bus_info)
def split_splittable_port(port, k, lanes, dev):
"""
Test that splitting of splittable port passes correctly.
"""
new_split_group = split(k, port)
# Once the split command ends, it takes some time to the sub ifaces'
# to get their names. Use udevadm to continue only when all current udev
# events are handled.
cmd = "udevadm settle"
stdout, stderr = run_command(cmd)
assert stderr == ""
if new_split_group != []:
test(exists_and_lanes(new_split_group, lanes/k, dev),
"split port %s into %s" % (port.name, k))
unsplit(port.bus_info)
def validate_devlink_output(devlink_data, target_property=None):
"""
Determine if test should be skipped by checking:
1. devlink_data contains values
2. The target_property exist in devlink_data
"""
skip_reason = None
if any(devlink_data.values()):
if target_property:
skip_reason = "{} not found in devlink output, test skipped".format(target_property)
for key in devlink_data:
if target_property in devlink_data[key]:
skip_reason = None
else:
skip_reason = 'devlink output is empty, test skipped'
if skip_reason:
print(skip_reason)
sys.exit(KSFT_SKIP)
def make_parser():
parser = argparse.ArgumentParser(description='A test for port splitting.')
parser.add_argument('--dev',
help='The devlink handle of the device under test. ' +
'The default is the first registered devlink ' +
'handle.')
return parser
def main(cmdline=None):
parser = make_parser()
args = parser.parse_args(cmdline)
dev = args.dev
if not dev:
cmd = "devlink -j dev show"
stdout, stderr = run_command(cmd)
assert stderr == ""
validate_devlink_output(json.loads(stdout))
devs = json.loads(stdout)['dev']
dev = list(devs.keys())[0]
cmd = "devlink dev show %s" % dev
stdout, stderr = run_command(cmd)
if stderr != "":
print("devlink device %s can not be found" % dev)
sys.exit(1)
ports = devlink_ports(dev)
found_max_lanes = False
for port in ports.if_names:
max_lanes = get_max_lanes(port.name)
# If max lanes is 0, do not test port splitting at all
if max_lanes == 0:
continue
# If 1 lane, shouldn't be able to split
elif max_lanes == 1:
test(not get_split_ability(port),
"%s should not be able to split" % port.name)
split_unsplittable_port(port, max_lanes)
# Else, splitting should pass and all the split ports should exist.
else:
lane = max_lanes
test(get_split_ability(port),
"%s should be able to split" % port.name)
while lane > 1:
split_splittable_port(port, lane, max_lanes, dev)
lane //= 2
found_max_lanes = True
if not found_max_lanes:
print(f"Test not started, no port of device {dev} reports max_lanes")
sys.exit(KSFT_SKIP)
if __name__ == "__main__":
main()
@@ -0,0 +1,439 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""
Devlink Rate TC Bandwidth Test Suite
===================================
This test suite verifies the functionality of devlink-rate traffic class (TC)
bandwidth distribution in a virtualized environment. The tests validate that
bandwidth can be properly allocated between different traffic classes and
that TC mapping works as expected.
Test Environment:
----------------
- Creates 1 VF
- Establishes a bridge connecting the VF representor and the uplink representor
- Sets up 2 VLAN interfaces on the VF with different VLAN IDs (101, 102)
- Configures different traffic classes (TC3 and TC4) for each VLAN
Test Cases:
----------
1. test_no_tc_mapping_bandwidth:
- Verifies that without TC mapping, bandwidth is NOT distributed according to
the configured 20/80 split between TC3 and TC4
- This test should fail if bandwidth matches the 20/80 split without TC
mapping
- Expected: Bandwidth should NOT be distributed as 20/80
2. test_tc_mapping_bandwidth:
- Configures TC mapping using mqprio qdisc
- Verifies that with TC mapping, bandwidth IS distributed according to the
configured 20/80 split between TC3 and TC4
- Expected: Bandwidth should be distributed as 20/80
Bandwidth Distribution:
----------------------
- TC3 (VLAN 101): Configured for 20% of total bandwidth
- TC4 (VLAN 102): Configured for 80% of total bandwidth
- Total bandwidth: 1Gbps
- Tolerance: +-12%
Hardware-Specific Behavior (mlx5):
--------------------------
mlx5 hardware enforces traffic class separation by ensuring that each transmit
queue (SQ) is associated with a single TC. If a packet is sent on a queue that
doesn't match the expected TC (based on DSCP or VLAN priority and hypervisor-set
mapping), the hardware moves the queue to the correct TC scheduler to preserve
traffic isolation.
This behavior means that even without explicit TC-to-queue mapping, bandwidth
enforcement may still appear to work—because the hardware dynamically adjusts
the scheduling context. However, this can lead to performance issues in high
rates and HOL blocking if traffic from different TCs is mixed on the same queue.
"""
import json
import os
import subprocess
import threading
import time
from lib.py import ksft_pr, ksft_run, ksft_exit
from lib.py import KsftSkipEx, KsftFailEx, KsftXfailEx
from lib.py import NetDrvEpEnv, DevlinkFamily
from lib.py import NlError
from lib.py import cmd, defer, ethtool, ip
from lib.py import Iperf3Runner
class BandwidthValidator:
"""
Validates total bandwidth and individual shares with tolerance
relative to the overall total.
"""
def __init__(self, shares):
self.tolerance_percent = 12
self.expected_total = sum(shares.values())
self.bounds = {}
for name, exp in shares.items():
self.bounds[name] = (self.min_expected(exp), self.max_expected(exp))
def min_expected(self, value):
"""Calculates the minimum acceptable value based on tolerance."""
return value - (self.expected_total * self.tolerance_percent / 100)
def max_expected(self, value):
"""Calculates the maximum acceptable value based on tolerance."""
return value + (self.expected_total * self.tolerance_percent / 100)
def bound(self, values):
"""
Return True if all given values fall within tolerance.
"""
for name, value in values.items():
low, high = self.bounds[name]
if not low <= value <= high:
return False
return True
def setup_vf(cfg, set_tc_mapping=True):
"""
Sets up a VF on the given network interface.
Enables SR-IOV and switchdev mode, brings the VF interface up,
and optionally configures TC mapping using mqprio.
"""
try:
cmd(f"devlink dev eswitch set pci/{cfg.pci} mode switchdev")
defer(cmd, f"devlink dev eswitch set pci/{cfg.pci} mode legacy")
except Exception as exc:
raise KsftSkipEx(f"Failed to enable switchdev mode on {cfg.pci}") from exc
try:
cmd(f"echo 1 > /sys/class/net/{cfg.ifname}/device/sriov_numvfs", shell=True)
defer(cmd, f"echo 0 > /sys/class/net/{cfg.ifname}/device/sriov_numvfs", shell=True)
except Exception as exc:
raise KsftSkipEx(f"Failed to enable SR-IOV on {cfg.ifname}") from exc
time.sleep(2)
vf_ifc = (os.listdir(
f"/sys/class/net/{cfg.ifname}/device/virtfn0/net") or [None])[0]
if vf_ifc:
ip(f"link set dev {vf_ifc} up")
else:
raise KsftSkipEx("VF interface not found")
if set_tc_mapping:
cmd(f"tc qdisc add dev {vf_ifc} root handle 5 mqprio mode dcb hw 1 num_tc 8")
return vf_ifc
def setup_vlans_on_vf(vf_ifc):
"""
Sets up two VLAN interfaces on the given VF, each mapped to a different TC.
"""
vlan_configs = [
{"vlan_id": 101, "tc": 3, "ip": "198.51.100.1"},
{"vlan_id": 102, "tc": 4, "ip": "198.51.100.9"},
]
for config in vlan_configs:
vlan_dev = f"{vf_ifc}.{config['vlan_id']}"
ip(f"link add link {vf_ifc} name {vlan_dev} type vlan id {config['vlan_id']}")
ip(f"addr add {config['ip']}/29 dev {vlan_dev}")
ip(f"link set dev {vlan_dev} up")
ip(f"link set dev {vlan_dev} type vlan egress-qos-map 0:{config['tc']}")
ksft_pr(f"Created VLAN {vlan_dev} on {vf_ifc} with tc {config['tc']} and IP {config['ip']}")
def get_vf_info(cfg):
"""
Finds the VF representor interface and devlink port index
for the given PCI device used in the test environment.
"""
cfg.vf_representor = None
cfg.vf_port_index = None
out = subprocess.check_output(["devlink", "-j", "port", "show"], encoding="utf-8")
ports = json.loads(out)["port"]
for port_name, props in ports.items():
netdev = props.get("netdev")
if (port_name.startswith(f"pci/{cfg.pci}/") and
props.get("vfnum") == 0):
cfg.vf_representor = netdev
cfg.vf_port_index = int(port_name.split("/")[-1])
break
def setup_bridge(cfg):
"""
Creates and configures a Linux bridge, with both the uplink
and VF representor interfaces attached to it.
"""
bridge_name = f"br_{os.getpid()}"
ip(f"link add name {bridge_name} type bridge")
defer(cmd, f"ip link del name {bridge_name} type bridge")
ip(f"link set dev {cfg.ifname} master {bridge_name}")
rep_name = cfg.vf_representor
if rep_name:
ip(f"link set dev {rep_name} master {bridge_name}")
ip(f"link set dev {rep_name} up")
ksft_pr(f"Set representor {rep_name} up and added to bridge")
else:
raise KsftSkipEx("Could not find representor for the VF")
ip(f"link set dev {bridge_name} up")
def setup_devlink_rate(cfg):
"""
Configures devlink rate tx_max and traffic class bandwidth for the VF.
"""
port_index = cfg.vf_port_index
if port_index is None:
raise KsftSkipEx("Could not find VF port index")
try:
cfg.devnl.rate_set({
"bus-name": "pci",
"dev-name": cfg.pci,
"port-index": port_index,
"rate-tx-max": 125000000,
"rate-tc-bws": [
{"index": 0, "bw": 0},
{"index": 1, "bw": 0},
{"index": 2, "bw": 0},
{"index": 3, "bw": 20},
{"index": 4, "bw": 80},
{"index": 5, "bw": 0},
{"index": 6, "bw": 0},
{"index": 7, "bw": 0},
]
})
except NlError as exc:
if exc.error == 95: # EOPNOTSUPP
raise KsftSkipEx("devlink rate configuration is not supported on the VF") from exc
raise KsftFailEx(f"rate_set failed on VF port {port_index}") from exc
def setup_remote_vlans(cfg):
"""
Sets up VLAN interfaces on the remote side.
"""
remote_dev = cfg.remote_ifname
vlan_ids = [101, 102]
remote_ips = ["198.51.100.2", "198.51.100.10"]
for vlan_id, ip_addr in zip(vlan_ids, remote_ips):
vlan_dev = f"{remote_dev}.{vlan_id}"
cmd(f"ip link add link {remote_dev} name {vlan_dev} "
f"type vlan id {vlan_id}", host=cfg.remote)
cmd(f"ip addr add {ip_addr}/29 dev {vlan_dev}", host=cfg.remote)
cmd(f"ip link set dev {vlan_dev} up", host=cfg.remote)
defer(cmd, f"ip link del {vlan_dev}", host=cfg.remote)
def setup_test_environment(cfg, set_tc_mapping=True):
"""
Sets up the complete test environment including VF creation, VLANs,
bridge configuration and devlink rate setup.
"""
vf_ifc = setup_vf(cfg, set_tc_mapping)
ksft_pr(f"Created VF interface: {vf_ifc}")
setup_vlans_on_vf(vf_ifc)
get_vf_info(cfg)
setup_bridge(cfg)
setup_devlink_rate(cfg)
setup_remote_vlans(cfg)
def measure_bandwidth(cfg, server_ip, client_ip, barrier):
"""
Synchronizes with peers and runs an iperf3-based bandwidth measurement
between the given endpoints. Returns average Gbps.
"""
runner = Iperf3Runner(cfg, server_ip=server_ip, client_ip=client_ip)
try:
barrier.wait(timeout=10)
except Exception as exc:
raise KsftFailEx("iperf3 barrier wait timed") from exc
try:
bw_gbps = runner.measure_bandwidth(reverse=True)
except Exception as exc:
raise KsftFailEx("iperf3 bandwidth measurement failed") from exc
return bw_gbps
def run_bandwidth_test(cfg):
"""
Runs parallel bandwidth measurements for each VLAN/TC pair and collects results.
"""
def _run_measure_bandwidth_thread(local_ip, remote_ip, results, barrier, tc_ix):
results[tc_ix] = measure_bandwidth(cfg, local_ip, remote_ip, barrier)
vf_vlan_data = [
# (local_ip, remote_ip, TC)
("198.51.100.1", "198.51.100.2", 3),
("198.51.100.9", "198.51.100.10", 4),
]
results = {}
threads = []
start_barrier = threading.Barrier(len(vf_vlan_data))
for local_ip, remote_ip, tc_ix in vf_vlan_data:
thread = threading.Thread(
target=_run_measure_bandwidth_thread,
args=(local_ip, remote_ip, results, start_barrier, tc_ix)
)
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
for tc_ix, tc_bw in results.items():
if tc_bw is None:
raise KsftFailEx("iperf3 failed; cannot evaluate bandwidth")
return results
def calculate_bandwidth_percentages(results):
"""
Calculates the percentage of total bandwidth received by TC3 and TC4.
"""
if 3 not in results or 4 not in results:
raise KsftFailEx(f"Missing expected TC results in {results}")
tc3_bw = results[3]
tc4_bw = results[4]
total_bw = tc3_bw + tc4_bw
tc3_percentage = (tc3_bw / total_bw) * 100
tc4_percentage = (tc4_bw / total_bw) * 100
return {
'tc3_bw': tc3_bw,
'tc4_bw': tc4_bw,
'tc3_percentage': tc3_percentage,
'tc4_percentage': tc4_percentage,
'total_bw': total_bw
}
def print_bandwidth_results(bw_data, test_name):
"""
Prints bandwidth measurements and TC usage summary for a given test.
"""
ksft_pr(f"Bandwidth check results {test_name}:")
ksft_pr(f"TC 3: {bw_data['tc3_bw']:.2f} Gbits/sec")
ksft_pr(f"TC 4: {bw_data['tc4_bw']:.2f} Gbits/sec")
ksft_pr(f"Total bandwidth: {bw_data['total_bw']:.2f} Gbits/sec")
ksft_pr(f"TC 3 percentage: {bw_data['tc3_percentage']:.1f}%")
ksft_pr(f"TC 4 percentage: {bw_data['tc4_percentage']:.1f}%")
def verify_total_bandwidth(bw_data, validator):
"""
Ensures the total measured bandwidth falls within the acceptable tolerance.
"""
total = bw_data['total_bw']
if validator.bound({"total": total}):
return
low, high = validator.bounds["total"]
if total < low:
raise KsftSkipEx(
f"Total bandwidth {total:.2f} Gbps < minimum "
f"{low:.2f} Gbps; "
f"parent tx_max ({validator.expected_total:.1f} G) "
f"not reached, cannot validate share"
)
raise KsftFailEx(
f"Total bandwidth {total:.2f} Gbps exceeds allowed ceiling "
f"{high:.2f} Gbps "
f"(VF tx_max set to {validator.expected_total:.1f} G)"
)
def run_bandwidth_distribution_test(cfg, set_tc_mapping):
"""
Runs parallel bandwidth measurements for both TCs and collects results.
"""
setup_test_environment(cfg, set_tc_mapping)
bandwidths = run_bandwidth_test(cfg)
bw_data = calculate_bandwidth_percentages(bandwidths)
test_name = "with TC mapping" if set_tc_mapping else "without TC mapping"
print_bandwidth_results(bw_data, test_name)
verify_total_bandwidth(bw_data, cfg.traffic_bw_validator)
return cfg.tc_bw_validator.bound({"tc3": bw_data['tc3_percentage'],
"tc4": bw_data['tc4_percentage']})
def test_no_tc_mapping_bandwidth(cfg):
"""
Verifies that bandwidth is not split 20/80 without traffic class mapping.
"""
pass_bw_msg = "Bandwidth is NOT distributed as 20/80 without TC mapping"
fail_bw_msg = "Bandwidth matched 20/80 split without TC mapping"
is_mlx5 = "driver: mlx5" in ethtool(f"-i {cfg.ifname}").stdout
if run_bandwidth_distribution_test(cfg, set_tc_mapping=False):
if is_mlx5:
raise KsftXfailEx(fail_bw_msg)
raise KsftFailEx(fail_bw_msg)
if is_mlx5:
raise KsftFailEx("mlx5 behavior changed:" + pass_bw_msg)
ksft_pr(pass_bw_msg)
def test_tc_mapping_bandwidth(cfg):
"""
Verifies that bandwidth is correctly split 20/80 between TC3 and TC4
when traffic class mapping is set.
"""
if run_bandwidth_distribution_test(cfg, set_tc_mapping=True):
ksft_pr("Bandwidth is distributed as 20/80 with TC mapping")
else:
raise KsftFailEx("Bandwidth did not match 20/80 split with TC mapping")
def main() -> None:
"""
Main entry point for running the test cases.
"""
with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
cfg.devnl = DevlinkFamily()
cfg.pci = os.path.basename(
os.path.realpath(f"/sys/class/net/{cfg.ifname}/device")
)
if not cfg.pci:
raise KsftSkipEx("Could not get PCI address of the interface")
cfg.traffic_bw_validator = BandwidthValidator({"total": 1})
cfg.tc_bw_validator = BandwidthValidator({"tc3": 20, "tc4": 80})
cases = [test_no_tc_mapping_bandwidth, test_tc_mapping_bandwidth]
ksft_run(cases=cases, args=(cfg,))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
from os import path
from devmem_lib import setup_test, run_rx, run_tx, run_tx_chunks, run_rx_hds
from lib.py import ksft_run, ksft_exit, ksft_disruptive
from lib.py import NetDrvEpEnv
@ksft_disruptive
def check_rx(cfg) -> None:
"""Run the devmem RX test."""
run_rx(cfg)
@ksft_disruptive
def check_tx(cfg) -> None:
"""Run the devmem TX test."""
run_tx(cfg)
@ksft_disruptive
def check_tx_chunks(cfg) -> None:
"""Run the devmem TX chunking test."""
run_tx_chunks(cfg)
def check_rx_hds(cfg) -> None:
"""Run the HDS test."""
run_rx_hds(cfg)
def main() -> None:
"""Run the devmem test cases."""
with NetDrvEpEnv(__file__) as cfg:
setup_test(cfg, path.abspath(path.dirname(__file__) + "/ncdevmem"))
ksft_run([check_rx, check_tx, check_tx_chunks, check_rx_hds],
args=(cfg,))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,222 @@
# SPDX-License-Identifier: GPL-2.0
"""Shared helpers for devmem TCP selftests."""
import re
from lib.py import (bkg, cmd, defer, ethtool, rand_port, wait_port_listen,
ksft_eq, KsftSkipEx, NetNSEnter, EthtoolFamily,
NetdevFamily)
def require_devmem(cfg):
"""Probe ncdevmem on cfg.ifname and SKIP the test if devmem isn't supported."""
if not hasattr(cfg, "devmem_probed"):
probe_command = f"{cfg.bin_local} -f {cfg.ifname}"
cfg.devmem_supported = cmd(probe_command, fail=False, shell=True).ret == 0
cfg.devmem_probed = True
if not cfg.devmem_supported:
raise KsftSkipEx("Test requires devmem support")
def configure_nic(cfg):
"""Channels, rings, RSS, queue lease for netkit devmem."""
if not hasattr(cfg, 'netns'):
return
cfg.require_ipver('6')
ethnl = EthtoolFamily()
channels = ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
channels = channels['combined-count']
if channels < 2:
raise KsftSkipEx(
'Test requires NETIF with at least 2 combined channels'
)
rings = ethnl.rings_get({'header': {'dev-index': cfg.ifindex}})
orig_rx_rings = rings['rx']
orig_hds_thresh = rings.get('hds-thresh', 0)
orig_data_split = rings.get('tcp-data-split', 'unknown')
ethnl.rings_set({'header': {'dev-index': cfg.ifindex},
'tcp-data-split': 'enabled',
'hds-thresh': 0,
'rx': min(64, orig_rx_rings)})
defer(ethnl.rings_set, {'header': {'dev-index': cfg.ifindex},
'tcp-data-split': orig_data_split,
'hds-thresh': orig_hds_thresh,
'rx': orig_rx_rings})
cfg.src_queue = channels - 1
ethtool(f"-X {cfg.ifname} equal {cfg.src_queue}")
defer(ethtool, f"-X {cfg.ifname} default")
if not hasattr(cfg, 'nk_queue'):
with NetNSEnter(str(cfg.netns)):
netdevnl = NetdevFamily()
lease_result = netdevnl.queue_create({
"ifindex": cfg.nk_guest_ifindex,
"type": "rx",
"lease": {
"ifindex": cfg.ifindex,
"queue": {"id": cfg.src_queue, "type": "rx"},
"netns-id": 0,
},
})
cfg.nk_queue = lease_result['id']
def set_flow_rule(cfg, port):
"""Install a flow rule steering to src_queue and return the flow rule ID."""
output = ethtool(
f"-N {cfg.ifname} flow-type tcp6 dst-port {port}"
f" action {cfg.src_queue}"
).stdout
return int(re.search(r'ID (\d+)', output).group(1))
def ncdevmem_rx(cfg, port, verify=True, fail_on_linear=False, flow_steer=False):
"""Build the ncdevmem RX listener command."""
if hasattr(cfg, 'netns'):
flow_rule_id = set_flow_rule(cfg, port)
defer(ethtool, f"-N {cfg.ifname} delete {flow_rule_id}")
ifname = cfg.nk_guest_ifname
addr = cfg.nk_guest_ipv6
extras = [f"-t {cfg.nk_queue}", "-q 1", "-n"]
else:
ifname = cfg.ifname
addr = cfg.addr
extras = []
if flow_steer:
extras.append(f"-c {cfg.remote_addr}")
if verify:
extras.append("-v 7")
if fail_on_linear:
extras.append("-L")
parts = [cfg.bin_local, "-l", f"-f {ifname}", f"-s {addr}",
f"-p {port}", *extras]
return " ".join(parts)
def ncdevmem_tx(cfg, port, chunk_size=0):
"""Build the ncdevmem TX send command."""
if hasattr(cfg, 'netns'):
ifname = cfg.nk_guest_ifname
addr = cfg.remote_addr_v['6']
extras = ["-t 0", "-q 1", "-n"]
else:
ifname = cfg.ifname
addr = cfg.remote_addr
extras = []
if chunk_size:
extras.append(f"-z {chunk_size}")
parts = [cfg.bin_local, f"-f {ifname}", f"-s {addr}",
f"-p {port}", *extras]
return " ".join(parts)
def socat_send(cfg, port, buf_size=0):
"""Socat command for sending to the devmem listener.
When buf_size > 0, force one TCP segment per write of exactly that size by
setting socat's buffer (-b) and disabling Nagle (TCP_NODELAY).
"""
proto = f"TCP{cfg.addr_ipver}"
if hasattr(cfg, 'netns'):
addr = f"[{cfg.nk_guest_ipv6}]"
else:
addr = cfg.baddr
suffix = f",bind={cfg.remote_baddr}:{port}"
buf = ""
if buf_size:
buf = f"-b {buf_size}"
suffix += ",nodelay"
return f"socat {buf} -u - {proto}:{addr}:{port}{suffix}"
def socat_listen(cfg, port):
"""Socat listen command for TX tests."""
return f"socat -U - TCP{cfg.addr_ipver}-LISTEN:{port}"
def setup_test(cfg, bin_local):
"""Stash the local ncdevmem path on cfg and deploy it to the remote."""
cfg.bin_local = bin_local
cfg.bin_remote = cfg.remote.deploy(cfg.bin_local)
def run_rx(cfg):
"""Run the devmem RX test."""
require_devmem(cfg)
configure_nic(cfg)
port = rand_port()
socat = socat_send(cfg, port)
data_pipe = (f"yes $(echo -e \x01\x02\x03\x04\x05\x06) | head -c 1K"
f" | {socat}")
netns = getattr(cfg, "netns", None)
listen_cmd = ncdevmem_rx(cfg, port, flow_steer=not hasattr(cfg, 'netns'))
with bkg(listen_cmd, exit_wait=True, ns=netns) as ncdevmem:
wait_port_listen(port, proto="tcp", ns=netns)
cmd(data_pipe, host=cfg.remote, shell=True)
ksft_eq(ncdevmem.ret, 0)
def run_tx(cfg):
"""Run the devmem TX test."""
require_devmem(cfg)
configure_nic(cfg)
netns = getattr(cfg, "netns", None)
port = rand_port()
tx_cmd = ncdevmem_tx(cfg, port)
listen_cmd = socat_listen(cfg, port)
with bkg(listen_cmd, host=cfg.remote, exit_wait=True) as socat:
wait_port_listen(port, host=cfg.remote)
cmd(f"bash -c 'echo -e \"hello\\nworld\" | {tx_cmd}'", ns=netns, shell=True)
ksft_eq(socat.stdout.strip(), "hello\nworld")
def run_tx_chunks(cfg):
"""Run the devmem TX chunking test."""
require_devmem(cfg)
configure_nic(cfg)
netns = getattr(cfg, "netns", None)
port = rand_port()
tx_cmd = ncdevmem_tx(cfg, port, chunk_size=3)
listen_cmd = socat_listen(cfg, port)
with bkg(listen_cmd, host=cfg.remote, exit_wait=True) as socat:
wait_port_listen(port, host=cfg.remote)
cmd(f"bash -c 'echo -e \"hello\\nworld\" | {tx_cmd}'", ns=netns, shell=True)
ksft_eq(socat.stdout.strip(), "hello\nworld")
def run_rx_hds(cfg):
"""Run the HDS test by running devmem RX across a segment size sweep."""
require_devmem(cfg)
configure_nic(cfg)
netns = getattr(cfg, "netns", None)
for size in [1, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192]:
port = rand_port()
listen_cmd = ncdevmem_rx(cfg, port, verify=False,
fail_on_linear=True)
socat = socat_send(cfg, port, buf_size=size)
with bkg(listen_cmd, exit_wait=True, ns=netns) as ncdevmem:
wait_port_listen(port, proto="tcp", ns=netns)
cmd(f"dd if=/dev/zero bs={size} count=1 2>/dev/null | "
f"{socat}", host=cfg.remote, shell=True)
ksft_eq(ncdevmem.ret, 0, f"HDS failed for payload size {size}")
@@ -0,0 +1,297 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
ALL_TESTS="
same_speeds_autoneg_off
different_speeds_autoneg_off
combination_of_neg_on_and_off
advertise_subset_of_speeds
check_highest_speed_is_chosen
different_speeds_autoneg_on
"
NUM_NETIFS=2
lib_dir=$(dirname "$0")
source "$lib_dir"/../../../net/forwarding/lib.sh
source ethtool_lib.sh
h1_create()
{
simple_if_init $h1 192.0.2.1/24
}
h1_destroy()
{
simple_if_fini $h1 192.0.2.1/24
}
h2_create()
{
simple_if_init $h2 192.0.2.2/24
}
h2_destroy()
{
simple_if_fini $h2 192.0.2.2/24
}
setup_prepare()
{
h1=${NETIFS[p1]}
h2=${NETIFS[p2]}
h1_create
h2_create
}
cleanup()
{
pre_cleanup
h2_destroy
h1_destroy
}
same_speeds_autoneg_off()
{
# Check that when each of the reported speeds is forced, the links come
# up and are operational.
local -a speeds_arr=($(common_speeds_get $h1 $h2 0 0))
for speed in "${speeds_arr[@]}"; do
RET=0
ethtool_set $h1 speed $speed autoneg off
ethtool_set $h2 speed $speed autoneg off
setup_wait_dev_with_timeout $h1
setup_wait_dev_with_timeout $h2
ping_do $h1 192.0.2.2
check_err $? "ping with speed $speed autoneg off"
log_test "force speed $speed on both ends"
done
ethtool -s $h2 autoneg on
ethtool -s $h1 autoneg on
}
different_speeds_autoneg_off()
{
# Test that when we force different speeds, links are not up and ping
# fails.
RET=0
local -a speeds_arr=($(different_speeds_get $h1 $h2 0 0))
local speed1=${speeds_arr[0]}
local speed2=${speeds_arr[1]}
ethtool_set $h1 speed $speed1 autoneg off
ethtool_set $h2 speed $speed2 autoneg off
setup_wait_dev_with_timeout $h1
setup_wait_dev_with_timeout $h2
ping_do $h1 192.0.2.2
check_fail $? "ping with different speeds"
log_test "force of different speeds autoneg off"
ethtool -s $h2 autoneg on
ethtool -s $h1 autoneg on
}
combination_of_neg_on_and_off()
{
# Test that when one device is forced to a speed supported by both
# endpoints and the other device is configured to autoneg on, the links
# are up and ping passes.
local -a speeds_arr=($(common_speeds_get $h1 $h2 0 1))
for speed in "${speeds_arr[@]}"; do
RET=0
ethtool_set $h1 speed $speed autoneg off
setup_wait_dev_with_timeout $h1
setup_wait_dev_with_timeout $h2
ping_do $h1 192.0.2.2
check_err $? "ping with h1-speed=$speed autoneg off, h2 autoneg on"
log_test "force speed $speed vs. autoneg"
done
ethtool -s $h1 autoneg on
}
hex_speed_value_get()
{
local speed=$1; shift
local shift_size=${speed_values[$speed]}
speed=$((0x1 << $"shift_size"))
printf "%#x" "$speed"
}
subset_of_common_speeds_get()
{
local dev1=$1; shift
local dev2=$1; shift
local adver=$1; shift
local -a speeds_arr=($(common_speeds_get $dev1 $dev2 0 $adver))
local speed_to_advertise=0
local speed_to_remove=${speeds_arr[0]}
speed_to_remove+='base'
local -a speeds_mode_arr=($(common_speeds_get $dev1 $dev2 1 $adver))
for speed in ${speeds_mode_arr[@]}; do
if [[ $speed != $speed_to_remove* ]]; then
speed=$(hex_speed_value_get $speed)
speed_to_advertise=$(($speed_to_advertise | \
$speed))
fi
done
# Convert to hex.
printf "%#x" "$speed_to_advertise"
}
speed_to_advertise_get()
{
# The function returns the hex number that is composed by OR-ing all
# the modes corresponding to the provided speed.
local speed_without_mode=$1; shift
local supported_speeds=("$@"); shift
local speed_to_advertise=0
speed_without_mode+='base'
for speed in ${supported_speeds[@]}; do
if [[ $speed == $speed_without_mode* ]]; then
speed=$(hex_speed_value_get $speed)
speed_to_advertise=$(($speed_to_advertise | \
$speed))
fi
done
# Convert to hex.
printf "%#x" "$speed_to_advertise"
}
advertise_subset_of_speeds()
{
# Test that when one device advertises a subset of speeds and another
# advertises a specific speed (but all modes of this speed), the links
# are up and ping passes.
RET=0
local speed_1_to_advertise=$(subset_of_common_speeds_get $h1 $h2 1)
ethtool_set $h1 advertise $speed_1_to_advertise
if [ $RET != 0 ]; then
log_test "advertise subset of speeds"
return
fi
local -a speeds_arr_without_mode=($(common_speeds_get $h1 $h2 0 1))
# Check only speeds that h1 advertised. Remove the first speed.
unset speeds_arr_without_mode[0]
local -a speeds_arr_with_mode=($(common_speeds_get $h1 $h2 1 1))
for speed_value in ${speeds_arr_without_mode[@]}; do
RET=0
local speed_2_to_advertise=$(speed_to_advertise_get $speed_value \
"${speeds_arr_with_mode[@]}")
ethtool_set $h2 advertise $speed_2_to_advertise
setup_wait_dev_with_timeout $h1
setup_wait_dev_with_timeout $h2
ping_do $h1 192.0.2.2
check_err $? "ping with h1=$speed_1_to_advertise, h2=$speed_2_to_advertise ($speed_value)"
log_test "advertise $speed_1_to_advertise vs. $speed_2_to_advertise"
done
ethtool -s $h2 autoneg on
ethtool -s $h1 autoneg on
}
check_highest_speed_is_chosen()
{
# Test that when one device advertises a subset of speeds, the other
# chooses the highest speed. This test checks configuration without
# traffic.
RET=0
local max_speed
local chosen_speed
local speed_to_advertise=$(subset_of_common_speeds_get $h1 $h2 1)
ethtool_set $h1 advertise $speed_to_advertise
if [ $RET != 0 ]; then
log_test "check highest speed"
return
fi
local -a speeds_arr=($(common_speeds_get $h1 $h2 0 1))
max_speed=${speeds_arr[0]}
for current in ${speeds_arr[@]}; do
if [[ $current -gt $max_speed ]]; then
max_speed=$current
fi
done
setup_wait_dev_with_timeout $h1
setup_wait_dev_with_timeout $h2
chosen_speed=$(ethtool $h1 | grep 'Speed:')
chosen_speed=${chosen_speed%"Mb/s"*}
chosen_speed=${chosen_speed#*"Speed: "}
((chosen_speed == max_speed))
check_err $? "h1 advertise $speed_to_advertise, h2 sync to speed $chosen_speed"
log_test "check highest speed"
ethtool -s $h2 autoneg on
ethtool -s $h1 autoneg on
}
different_speeds_autoneg_on()
{
# Test that when we configure links to advertise different speeds,
# links are not up and ping fails.
RET=0
local -a speeds=($(different_speeds_get $h1 $h2 1 1))
local speed1=${speeds[0]}
local speed2=${speeds[1]}
speed1=$(hex_speed_value_get $speed1)
speed2=$(hex_speed_value_get $speed2)
ethtool_set $h1 advertise $speed1
ethtool_set $h2 advertise $speed2
if (($RET)); then
setup_wait_dev_with_timeout $h1
setup_wait_dev_with_timeout $h2
ping_do $h1 192.0.2.2
check_fail $? "ping with different speeds autoneg on"
fi
log_test "advertise different speeds autoneg on"
ethtool -s $h2 autoneg on
ethtool -s $h1 autoneg on
}
trap cleanup EXIT
setup_prepare
setup_wait
declare -gA speed_values
eval "speed_values=($(speeds_arr_get))"
tests_run
exit $EXIT_STATUS
@@ -0,0 +1,116 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
ALL_TESTS="
autoneg
autoneg_force_mode
no_cable
"
NUM_NETIFS=2
lib_dir=$(dirname "$0")
source "$lib_dir"/../../../net/forwarding/lib.sh
source ethtool_lib.sh
TIMEOUT=$((WAIT_TIMEOUT * 1000)) # ms
setup_prepare()
{
swp1=${NETIFS[p1]}
swp2=${NETIFS[p2]}
swp3=$NETIF_NO_CABLE
}
ethtool_ext_state()
{
local dev=$1; shift
local expected_ext_state=$1; shift
local expected_ext_substate=${1:-""}; shift
local ext_state=$(ethtool $dev | grep "Link detected" \
| cut -d "(" -f2 | cut -d ")" -f1)
local ext_substate=$(echo $ext_state | cut -sd "," -f2 \
| sed -e 's/^[[:space:]]*//')
ext_state=$(echo $ext_state | cut -d "," -f1)
if [[ $ext_state != $expected_ext_state ]]; then
echo "Expected \"$expected_ext_state\", got \"$ext_state\""
return 1
fi
if [[ $ext_substate != $expected_ext_substate ]]; then
echo "Expected \"$expected_ext_substate\", got \"$ext_substate\""
return 1
fi
}
autoneg()
{
local msg
RET=0
ip link set dev $swp1 up
msg=$(busywait $TIMEOUT ethtool_ext_state $swp1 \
"Autoneg" "No partner detected")
check_err $? "$msg"
log_test "Autoneg, No partner detected"
ip link set dev $swp1 down
}
autoneg_force_mode()
{
local msg
RET=0
ip link set dev $swp1 up
ip link set dev $swp2 up
local -a speeds_arr=($(different_speeds_get $swp1 $swp2 0 0))
local speed1=${speeds_arr[0]}
local speed2=${speeds_arr[1]}
ethtool_set $swp1 speed $speed1 autoneg off
ethtool_set $swp2 speed $speed2 autoneg off
msg=$(busywait $TIMEOUT ethtool_ext_state $swp1 \
"Autoneg" "No partner detected during force mode")
check_err $? "$msg"
msg=$(busywait $TIMEOUT ethtool_ext_state $swp2 \
"Autoneg" "No partner detected during force mode")
check_err $? "$msg"
log_test "Autoneg, No partner detected during force mode"
ethtool -s $swp2 autoneg on
ethtool -s $swp1 autoneg on
ip link set dev $swp2 down
ip link set dev $swp1 down
}
no_cable()
{
local msg
RET=0
ip link set dev $swp3 up
msg=$(busywait $TIMEOUT ethtool_ext_state $swp3 "No cable")
check_err $? "$msg"
log_test "No cable"
ip link set dev $swp3 down
}
setup_prepare
tests_run
exit $EXIT_STATUS
@@ -0,0 +1,120 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
speeds_arr_get()
{
cmd='/ETHTOOL_LINK_MODE_[^[:space:]]*_BIT[[:space:]]+=[[:space:]]+/ \
{sub(/,$/, "") \
sub(/ETHTOOL_LINK_MODE_/,"") \
sub(/_BIT/,"") \
sub(/_Full/,"/Full") \
sub(/_Half/,"/Half");\
print "["$1"]="$3}'
awk "${cmd}" /usr/include/linux/ethtool.h
}
ethtool_set()
{
local cmd="$@"
local out=$(ethtool -s $cmd 2>&1 | wc -l)
check_err $out "error in configuration. $cmd"
}
dev_linkmodes_params_get()
{
local dev=$1; shift
local adver=$1; shift
local -a linkmodes_params
local param_count
local arr
if (($adver)); then
mode="Advertised link modes"
else
mode="Supported link modes"
fi
local -a dev_linkmodes=($(dev_speeds_get $dev 1 $adver))
for ((i=0; i<${#dev_linkmodes[@]}; i++)); do
linkmodes_params[$i]=$(echo -e "${dev_linkmodes[$i]}" | \
# Replaces all non numbers with spaces
sed -e 's/[^0-9]/ /g' | \
# Squeeze spaces in sequence to 1 space
tr -s ' ')
# Count how many numbers were found in the linkmode
param_count=$(echo "${linkmodes_params[$i]}" | wc -w)
if [[ $param_count -eq 1 ]]; then
linkmodes_params[$i]="${linkmodes_params[$i]} 1"
elif [[ $param_count -ge 3 ]]; then
arr=(${linkmodes_params[$i]})
# Take only first two params
linkmodes_params[$i]=$(echo "${arr[@]:0:2}")
fi
done
echo ${linkmodes_params[@]}
}
dev_speeds_get()
{
local dev=$1; shift
local with_mode=$1; shift
local adver=$1; shift
local speeds_str
if (($adver)); then
mode="Advertised link modes"
else
mode="Supported link modes"
fi
speeds_str=$(ethtool "$dev" | \
# Snip everything before the link modes section.
sed -n '/'"$mode"':/,$p' | \
# Quit processing the rest at the start of the next section.
# When checking, skip the header of this section (hence the 2,).
sed -n '2,${/^[\t][^ \t]/q};p' | \
# Drop the section header of the current section.
cut -d':' -f2)
local -a speeds_arr=($speeds_str)
if [[ $with_mode -eq 0 ]]; then
for ((i=0; i<${#speeds_arr[@]}; i++)); do
speeds_arr[$i]=${speeds_arr[$i]%base*}
done
fi
echo ${speeds_arr[@]}
}
common_speeds_get()
{
dev1=$1; shift
dev2=$1; shift
with_mode=$1; shift
adver=$1; shift
local -a dev1_speeds=($(dev_speeds_get $dev1 $with_mode $adver))
local -a dev2_speeds=($(dev_speeds_get $dev2 $with_mode $adver))
comm -12 \
<(printf '%s\n' "${dev1_speeds[@]}" | sort -u) \
<(printf '%s\n' "${dev2_speeds[@]}" | sort -u)
}
different_speeds_get()
{
local dev1=$1; shift
local dev2=$1; shift
local with_mode=$1; shift
local adver=$1; shift
local -a speeds_arr
speeds_arr=($(common_speeds_get $dev1 $dev2 $with_mode $adver))
if [[ ${#speeds_arr[@]} < 2 ]]; then
check_err 1 "cannot check different speeds. There are not enough speeds"
fi
echo ${speeds_arr[0]} ${speeds_arr[1]}
}
@@ -0,0 +1,341 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
ALL_TESTS="
manual_with_verification_h1_to_h2
manual_with_verification_h2_to_h1
manual_without_verification_h1_to_h2
manual_without_verification_h2_to_h1
manual_failed_verification_h1_to_h2
manual_failed_verification_h2_to_h1
lldp
"
NUM_NETIFS=2
REQUIRE_MZ=no
PREEMPTIBLE_PRIO=0
lib_dir=$(dirname "$0")
source "$lib_dir"/../../../net/forwarding/lib.sh
traffic_test()
{
local if=$1; shift
local src=$1; shift
local num_pkts=10000
local before=
local after=
local delta=
if [ ${has_pmac_stats[$if]} = false ]; then
src="aggregate"
fi
before=$(ethtool_std_stats_get $if "eth-mac" "FramesTransmittedOK" $src)
$MZ $if -q -c $num_pkts -p 64 -b bcast -t ip -R $PREEMPTIBLE_PRIO
after=$(ethtool_std_stats_get $if "eth-mac" "FramesTransmittedOK" $src)
delta=$((after - before))
# Allow an extra 1% tolerance for random packets sent by the stack
[ $delta -ge $num_pkts ] && [ $delta -le $((num_pkts + 100)) ]
}
manual_with_verification()
{
local tx=$1; shift
local rx=$1; shift
RET=0
# It isn't completely clear from IEEE 802.3-2018 Figure 99-5: Transmit
# Processing state diagram whether the "send_r" variable (send response
# to verification frame) should be taken into consideration while the
# MAC Merge TX direction is disabled. That being said, at least the
# NXP ENETC does not, and requires tx-enabled on in order to respond to
# the link partner's verification frames.
ethtool --set-mm $rx tx-enabled on
ethtool --set-mm $tx verify-enabled on tx-enabled on
# Wait for verification to finish
sleep 1
ethtool --json --show-mm $tx | jq -r '.[]."verify-status"' | \
grep -q 'SUCCEEDED'
check_err "$?" "Verification did not succeed"
ethtool --json --show-mm $tx | jq -r '.[]."tx-active"' | grep -q 'true'
check_err "$?" "pMAC TX is not active"
traffic_test $tx "pmac"
check_err "$?" "Traffic did not get sent through $tx's pMAC"
ethtool --set-mm $tx verify-enabled off tx-enabled off
ethtool --set-mm $rx tx-enabled off
log_test "Manual configuration with verification: $tx to $rx"
}
manual_with_verification_h1_to_h2()
{
manual_with_verification $h1 $h2
}
manual_with_verification_h2_to_h1()
{
manual_with_verification $h2 $h1
}
manual_without_verification()
{
local tx=$1; shift
local rx=$1; shift
RET=0
ethtool --set-mm $tx verify-enabled off tx-enabled on
ethtool --json --show-mm $tx | jq -r '.[]."verify-status"' | \
grep -q 'DISABLED'
check_err "$?" "Verification is not disabled"
ethtool --json --show-mm $tx | jq -r '.[]."tx-active"' | grep -q 'true'
check_err "$?" "pMAC TX is not active"
traffic_test $tx "pmac"
check_err "$?" "Traffic did not get sent through $tx's pMAC"
ethtool --set-mm $tx verify-enabled off tx-enabled off
log_test "Manual configuration without verification: $tx to $rx"
}
manual_without_verification_h1_to_h2()
{
manual_without_verification $h1 $h2
}
manual_without_verification_h2_to_h1()
{
manual_without_verification $h2 $h1
}
manual_failed_verification()
{
local tx=$1; shift
local rx=$1; shift
RET=0
ethtool --set-mm $rx pmac-enabled off
ethtool --set-mm $tx verify-enabled on tx-enabled on
# Wait for verification to time out
sleep 1
ethtool --json --show-mm $tx | jq -r '.[]."verify-status"' | \
grep -q 'SUCCEEDED'
check_fail "$?" "Verification succeeded when it shouldn't have"
ethtool --json --show-mm $tx | jq -r '.[]."tx-active"' | grep -q 'true'
check_fail "$?" "pMAC TX is active when it shouldn't have"
traffic_test $tx "emac"
check_err "$?" "Traffic did not get sent through $tx's eMAC"
ethtool --set-mm $tx verify-enabled off tx-enabled off
ethtool --set-mm $rx pmac-enabled on
log_test "Manual configuration with failed verification: $tx to $rx"
}
manual_failed_verification_h1_to_h2()
{
manual_failed_verification $h1 $h2
}
manual_failed_verification_h2_to_h1()
{
manual_failed_verification $h2 $h1
}
smallest_supported_add_frag_size()
{
local iface=$1
local rx_min_frag_size=
rx_min_frag_size=$(ethtool --json --show-mm $iface | \
jq '.[]."rx-min-frag-size"')
if [ $rx_min_frag_size -le 60 ]; then
echo 0
elif [ $rx_min_frag_size -le 124 ]; then
echo 1
elif [ $rx_min_frag_size -le 188 ]; then
echo 2
elif [ $rx_min_frag_size -le 252 ]; then
echo 3
else
echo "$iface: RX min frag size $rx_min_frag_size cannot be advertised over LLDP"
exit 1
fi
}
expected_add_frag_size()
{
local iface=$1
local requested=$2
local min=$(smallest_supported_add_frag_size $iface)
[ $requested -le $min ] && echo $min || echo $requested
}
lldp_change_add_frag_size()
{
local add_frag_size=$1
local pattern=
lldptool -T -i $h1 -V addEthCaps addFragSize=$add_frag_size >/dev/null
# Wait for TLVs to be received
sleep 2
pattern=$(printf "Additional fragment size: %d" \
$(expected_add_frag_size $h1 $add_frag_size))
lldptool -i $h2 -t -n -V addEthCaps | grep -q "$pattern"
}
lldp()
{
RET=0
systemctl start lldpad
# Configure the interfaces to receive and transmit LLDPDUs
lldptool -L -i $h1 adminStatus=rxtx >/dev/null
lldptool -L -i $h2 adminStatus=rxtx >/dev/null
# Enable the transmission of Additional Ethernet Capabilities TLV
lldptool -T -i $h1 -V addEthCaps enableTx=yes >/dev/null
lldptool -T -i $h2 -V addEthCaps enableTx=yes >/dev/null
# Wait for TLVs to be received
sleep 2
lldptool -i $h1 -t -n -V addEthCaps | \
grep -q "Preemption capability active"
check_err "$?" "$h1 pMAC TX is not active"
lldptool -i $h2 -t -n -V addEthCaps | \
grep -q "Preemption capability active"
check_err "$?" "$h2 pMAC TX is not active"
lldp_change_add_frag_size 3
check_err "$?" "addFragSize 3"
lldp_change_add_frag_size 2
check_err "$?" "addFragSize 2"
lldp_change_add_frag_size 1
check_err "$?" "addFragSize 1"
lldp_change_add_frag_size 0
check_err "$?" "addFragSize 0"
traffic_test $h1 "pmac"
check_err "$?" "Traffic did not get sent through $h1's pMAC"
traffic_test $h2 "pmac"
check_err "$?" "Traffic did not get sent through $h2's pMAC"
systemctl stop lldpad
log_test "LLDP"
}
h1_create()
{
ip link set dev $h1 up
tc qdisc add dev $h1 root mqprio num_tc 4 map 0 1 2 3 \
queues 1@0 1@1 1@2 1@3 \
fp P E E E \
hw 1
ethtool --set-mm $h1 pmac-enabled on tx-enabled off verify-enabled off
}
h2_create()
{
ip link set dev $h2 up
ethtool --set-mm $h2 pmac-enabled on tx-enabled off verify-enabled off
tc qdisc add dev $h2 root mqprio num_tc 4 map 0 1 2 3 \
queues 1@0 1@1 1@2 1@3 \
fp P E E E \
hw 1
}
h1_destroy()
{
ethtool --set-mm $h1 pmac-enabled off tx-enabled off verify-enabled off
tc qdisc del dev $h1 root
ip link set dev $h1 down
}
h2_destroy()
{
tc qdisc del dev $h2 root
ethtool --set-mm $h2 pmac-enabled off tx-enabled off verify-enabled off
ip link set dev $h2 down
}
setup_prepare()
{
h1=${NETIFS[p1]}
h2=${NETIFS[p2]}
h1_create
h2_create
}
cleanup()
{
pre_cleanup
h2_destroy
h1_destroy
}
check_ethtool_mm_support
check_tc_fp_support
require_command lldptool
bail_on_lldpad "autoconfigure the MAC Merge layer" "configure it manually"
for netif in ${NETIFS[@]}; do
ethtool --show-mm $netif 2>&1 &> /dev/null
if [[ $? -ne 0 ]]; then
echo "SKIP: $netif does not support MAC Merge"
exit $ksft_skip
fi
if check_ethtool_pmac_std_stats_support $netif eth-mac; then
has_pmac_stats[$netif]=true
else
has_pmac_stats[$netif]=false
echo "$netif does not report pMAC statistics, falling back to aggregate"
fi
done
trap cleanup EXIT
setup_prepare
setup_wait
tests_run
exit $EXIT_STATUS
@@ -0,0 +1,159 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#shellcheck disable=SC2034 # SC does not see the global variables
#shellcheck disable=SC2317,SC2329 # unused functions
ALL_TESTS="
rmon_rx_histogram
rmon_tx_histogram
"
: "${DRIVER_TEST_CONFORMANT:=yes}"
NUM_NETIFS=2
lib_dir=$(dirname "$0")
source "$lib_dir"/../../../net/forwarding/lib.sh
source "$lib_dir"/../../../kselftest/ktap_helpers.sh
UINT32_MAX=$((2**32 - 1))
ETH_FCS_LEN=4
ETH_HLEN=$((6+6+2))
TEST_NAME=$(basename "$0" .sh)
declare -A netif_mtu
ensure_mtu()
{
local iface=$1; shift
local len=$1; shift
local required=$((len - ETH_HLEN - ETH_FCS_LEN))
local current
current=$(run_on "$iface" \
ip -j link show dev "$iface" | jq -r '.[0].mtu')
if [ "$current" -lt "$required" ]; then
run_on "$iface" ip link set dev "$iface" mtu "$required" \
|| return 1
fi
}
bucket_test()
{
local iface=$1; shift
local neigh=$1; shift
local set=$1; shift
local bucket=$1; shift
local len=$1; shift
local num_rx=10000
local num_tx=20000
local expected=
local before=
local after=
local delta=
# Mausezahn does not include FCS bytes in its length - but the
# histogram counters do
len=$((len - ETH_FCS_LEN))
len=$((len > 0 ? len : 0))
before=$(run_on "$iface" ethtool --json -S "$iface" --groups rmon | \
jq -r ".[0].rmon[\"${set}-pktsNtoM\"][$bucket].val")
# Send 10k one way and 20k in the other, to detect counters
# mapped to the wrong direction
run_on "$neigh" \
"$MZ" "$neigh" -q -c "$num_rx" -p "$len" -a own -b bcast -d 10us
run_on "$iface" \
"$MZ" "$iface" -q -c "$num_tx" -p "$len" -a own -b bcast -d 10us
after=$(run_on "$iface" ethtool --json -S "$iface" --groups rmon | \
jq -r ".[0].rmon[\"${set}-pktsNtoM\"][$bucket].val")
delta=$((after - before))
expected=$([ "$set" = rx ] && echo "$num_rx" || echo "$num_tx")
[ "$delta" -ge "$expected" ] && [ "$delta" -le "$UINT32_MAX" ]
}
rmon_histogram()
{
local iface=$1; shift
local neigh=$1; shift
local set=$1; shift
local nbuckets=0
local step=
while read -r -a bucket; do
step="$set-pkts${bucket[0]}to${bucket[1]}"
for if in "$iface" "$neigh"; do
if ! ensure_mtu "$if" "${bucket[0]}"; then
ktap_print_msg "$if does not support the required MTU for $step"
ktap_test_xfail "$TEST_NAME.$step"
return
fi
done
if ! bucket_test "$iface" "$neigh" "$set" "$nbuckets" "${bucket[0]}"; then
ktap_test_fail "$TEST_NAME.$step"
return 1
fi
ktap_test_pass "$TEST_NAME.$step"
nbuckets=$((nbuckets + 1))
done < <(run_on "$iface" ethtool --json -S "$iface" --groups rmon | \
jq -r ".[0].rmon[\"${set}-pktsNtoM\"][]|[.low, .high]|@tsv" 2>/dev/null)
if [ "$nbuckets" -eq 0 ]; then
ktap_print_msg "$iface does not support $set histogram counters"
return
fi
}
rmon_rx_histogram()
{
rmon_histogram "$h1" "$h2" rx
}
rmon_tx_histogram()
{
rmon_histogram "$h1" "$h2" tx
}
setup_prepare()
{
h1=${NETIFS[p1]}
h2=${NETIFS[p2]}
for iface in "$h1" "$h2"; do
netif_mtu["$iface"]=$(run_on "$iface" \
ip -j link show dev "$iface" | jq -r '.[0].mtu')
done
}
cleanup()
{
pre_cleanup
# Do not bring down the interfaces, just configure the initial MTU
for iface in "$h2" "$h1"; do
run_on "$iface" ip link set dev "$iface" \
mtu "${netif_mtu[$iface]}"
done
}
check_ethtool_counter_group_support
trap cleanup EXIT
bucket_count=$(ethtool --json -S "${NETIFS[p1]}" --groups rmon | \
jq -r '.[0].rmon |
"\((."rx-pktsNtoM" | length) +
(."tx-pktsNtoM" | length))"')
ktap_print_header
ktap_set_plan "$bucket_count"
setup_prepare
setup_wait
tests_run
ktap_finished
@@ -0,0 +1,206 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#shellcheck disable=SC2034 # SC does not see the global variables
#shellcheck disable=SC2317,SC2329 # unused functions
ALL_TESTS="
test_eth_ctrl_stats
test_eth_mac_stats
test_pause_stats
"
: "${DRIVER_TEST_CONFORMANT:=yes}"
STABLE_MAC_ADDRS=yes
NUM_NETIFS=2
lib_dir=$(dirname "$0")
# shellcheck source=./../../../net/forwarding/lib.sh
source "$lib_dir"/../../../net/forwarding/lib.sh
# shellcheck source=./../../../kselftest/ktap_helpers.sh
source "$lib_dir"/../../../kselftest/ktap_helpers.sh
UINT32_MAX=$((2**32 - 1))
SUBTESTS=0
TEST_NAME=$(basename "$0" .sh)
traffic_test()
{
local iface=$1; shift
local neigh=$1; shift
local num_tx=$1; shift
local pkt_format="$1"; shift
local -a counters=("$@")
local int grp cnt target exact_check
local before after delta
local num_rx=$((num_tx * 2))
local xfail_message
local src="aggregate"
local i
for i in "${!counters[@]}"; do
read -r int grp cnt target exact_check xfail_message \
<<< "${counters[$i]}"
before[i]=$(ethtool_std_stats_get "$int" "$grp" "$cnt" "$src")
done
# shellcheck disable=SC2086 # needs split options
run_on "$iface" "$MZ" "$iface" -q -c "$num_tx" $pkt_format
# shellcheck disable=SC2086 # needs split options
run_on "$neigh" "$MZ" "$neigh" -q -c "$num_rx" $pkt_format
for i in "${!counters[@]}"; do
read -r int grp cnt target exact_check xfail_message \
<<< "${counters[$i]}"
after[i]=$(ethtool_std_stats_get "$int" "$grp" "$cnt" "$src")
if [[ "${after[$i]}" == "null" ]]; then
ktap_test_skip "$TEST_NAME.$grp-$cnt"
continue;
fi
delta=$((after[i] - before[i]))
if [ "$exact_check" -ne 0 ]; then
[ "$delta" -eq "$target" ]
else
[ "$delta" -ge "$target" ] && \
[ "$delta" -le "$UINT32_MAX" ]
fi
err="$?"
if [[ $err != 0 ]] && [[ -n $xfail_message ]]; then
ktap_print_msg "$xfail_message"
ktap_test_xfail "$TEST_NAME.$grp-$cnt"
continue;
fi
if [[ $err != 0 ]]; then
ktap_print_msg "$grp-$cnt is not valid on $int (expected $target, got $delta)"
ktap_test_fail "$TEST_NAME.$grp-$cnt"
else
ktap_test_pass "$TEST_NAME.$grp-$cnt"
fi
done
}
test_eth_ctrl_stats()
{
local pkt_format="-a own -b bcast 88:08 -p 64"
local num_pkts=1000
local -a counters
counters=("$h1 eth-ctrl MACControlFramesTransmitted $num_pkts 0")
traffic_test "$h1" "$h2" "$num_pkts" "$pkt_format" \
"${counters[@]}"
counters=("$h1 eth-ctrl MACControlFramesReceived $num_pkts 0")
traffic_test "$h2" "$h1" "$num_pkts" "$pkt_format" \
"${counters[@]}"
}
SUBTESTS=$((SUBTESTS + 2))
test_eth_mac_stats()
{
local pkt_size=100
local pkt_size_fcs=$((pkt_size + 4))
local bcast_pkt_format="-a own -b bcast -p $pkt_size"
local mcast_pkt_format="-a own -b 01:00:5E:00:00:01 -p $pkt_size"
local num_pkts=2000
local octets=$((pkt_size_fcs * num_pkts))
local -a counters error_cnt collision_cnt
# Error counters should be exactly zero
counters=("$h1 eth-mac FrameCheckSequenceErrors 0 1"
"$h1 eth-mac AlignmentErrors 0 1"
"$h1 eth-mac FramesLostDueToIntMACXmitError 0 1"
"$h1 eth-mac CarrierSenseErrors 0 1"
"$h1 eth-mac FramesLostDueToIntMACRcvError 0 1"
"$h1 eth-mac InRangeLengthErrors 0 1"
"$h1 eth-mac OutOfRangeLengthField 0 1"
"$h1 eth-mac FrameTooLongErrors 0 1"
"$h1 eth-mac FramesAbortedDueToXSColls 0 1")
traffic_test "$h1" "$h2" "$num_pkts" "$bcast_pkt_format" \
"${counters[@]}"
# Collision related counters should also be zero
counters=("$h1 eth-mac SingleCollisionFrames 0 1"
"$h1 eth-mac MultipleCollisionFrames 0 1"
"$h1 eth-mac FramesWithDeferredXmissions 0 1"
"$h1 eth-mac LateCollisions 0 1"
"$h1 eth-mac FramesWithExcessiveDeferral 0 1")
traffic_test "$h1" "$h2" "$num_pkts" "$bcast_pkt_format" \
"${counters[@]}"
counters=("$h1 eth-mac BroadcastFramesXmittedOK $num_pkts 0"
"$h1 eth-mac OctetsTransmittedOK $octets 0")
traffic_test "$h1" "$h2" "$num_pkts" "$bcast_pkt_format" \
"${counters[@]}"
counters=("$h1 eth-mac BroadcastFramesReceivedOK $num_pkts 0"
"$h1 eth-mac OctetsReceivedOK $octets 0")
traffic_test "$h2" "$h1" "$num_pkts" "$bcast_pkt_format" \
"${counters[@]}"
counters=("$h1 eth-mac FramesTransmittedOK $num_pkts 0"
"$h1 eth-mac MulticastFramesXmittedOK $num_pkts 0")
traffic_test "$h1" "$h2" "$num_pkts" "$mcast_pkt_format" \
"${counters[@]}"
counters=("$h1 eth-mac FramesReceivedOK $num_pkts 0"
"$h1 eth-mac MulticastFramesReceivedOK $num_pkts 0")
traffic_test "$h2" "$h1" "$num_pkts" "$mcast_pkt_format" \
"${counters[@]}"
}
SUBTESTS=$((SUBTESTS + 22))
test_pause_stats()
{
local pkt_format="-a own -b 01:80:c2:00:00:01 88:08:00:01:00:01"
local xfail_message="software sent pause frames not detected"
local num_pkts=2000
local -a counters
local int
local i
# Check that there is pause frame support
for ((i = 1; i <= NUM_NETIFS; ++i)); do
int="${NETIFS[p$i]}"
if ! run_on "$int" ethtool -I --json -a "$int" > /dev/null 2>&1; then
ktap_test_skip "$TEST_NAME.tx_pause_frames"
ktap_test_skip "$TEST_NAME.rx_pause_frames"
return
fi
done
counters=("$h1 pause tx_pause_frames $num_pkts 0 $xfail_message")
traffic_test "$h1" "$h2" "$num_pkts" "$pkt_format" \
"${counters[@]}"
counters=("$h1 pause rx_pause_frames $num_pkts 0")
traffic_test "$h2" "$h1" "$num_pkts" "$pkt_format" \
"${counters[@]}"
}
SUBTESTS=$((SUBTESTS + 2))
setup_prepare()
{
local iface
h1=${NETIFS[p1]}
h2=${NETIFS[p2]}
h2_mac=$(mac_get "$h2")
}
ktap_print_header
ktap_set_plan $SUBTESTS
check_ethtool_counter_group_support
trap cleanup EXIT
setup_prepare
setup_wait
tests_run
ktap_finished
@@ -0,0 +1,304 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""
HW GRO tests focusing on device machinery like stats, rather than protocol
processing.
"""
import glob
import re
from lib.py import ksft_run, ksft_exit, ksft_pr
from lib.py import ksft_eq, ksft_ge, ksft_variants
from lib.py import NetDrvEpEnv, NetdevFamily
from lib.py import KsftSkipEx
from lib.py import bkg, cmd, defer, ethtool, ip
# gro.c uses hardcoded DPORT=8000
GRO_DPORT = 8000
def _get_queue_stats(cfg, queue_id):
"""Get stats for a specific Rx queue."""
cfg.wait_hw_stats_settle()
data = cfg.netnl.qstats_get({"ifindex": cfg.ifindex, "scope": ["queue"]},
dump=True)
for q in data:
if q.get('queue-type') == 'rx' and q.get('queue-id') == queue_id:
return q
return {}
def _resolve_dmac(cfg, ipver):
"""Find the destination MAC address for sending packets."""
attr = "dmac" + ipver
if hasattr(cfg, attr):
return getattr(cfg, attr)
route = ip(f"-{ipver} route get {cfg.addr_v[ipver]}",
json=True, host=cfg.remote)[0]
gw = route.get("gateway")
if not gw:
setattr(cfg, attr, cfg.dev['address'])
return getattr(cfg, attr)
cmd(f"ping -c1 -W0 -I{cfg.remote_ifname} {gw}", host=cfg.remote)
neigh = ip(f"neigh get {gw} dev {cfg.remote_ifname}",
json=True, host=cfg.remote)[0]
setattr(cfg, attr, neigh['lladdr'])
return getattr(cfg, attr)
def _require_ntuple(cfg):
features = ethtool(f"-k {cfg.ifname}", json=True)[0]
if not features["ntuple-filters"]["active"]:
if features["ntuple-filters"]["fixed"]:
raise KsftSkipEx("Device does not support ntuple-filters")
ethtool(f"-K {cfg.ifname} ntuple-filters on")
defer(ethtool, f"-K {cfg.ifname} ntuple-filters off")
def _setup_isolated_queue(cfg):
"""Set up an isolated queue for testing using ntuple filter.
Remove queue 1 from the default RSS context and steer test traffic to it.
"""
_require_ntuple(cfg)
test_queue = 1
qcnt = len(glob.glob(f"/sys/class/net/{cfg.ifname}/queues/rx-*"))
if qcnt < 2:
raise KsftSkipEx(f"Need at least 2 queues, have {qcnt}")
# Remove queue 1 from default RSS context by setting its weight to 0
weights = ["1"] * qcnt
weights[test_queue] = "0"
ethtool(f"-X {cfg.ifname} weight " + " ".join(weights))
defer(ethtool, f"-X {cfg.ifname} default")
# Set up ntuple filter to steer our test traffic to the isolated queue
flow = f"flow-type tcp{cfg.addr_ipver} "
flow += f"dst-ip {cfg.addr} dst-port {GRO_DPORT} action {test_queue}"
output = ethtool(f"-N {cfg.ifname} {flow}").stdout
ntuple_id = int(output.split()[-1])
defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
return test_queue
def _run_gro_test(cfg, test_name, num_flows=None, ignore_fail=False,
order_check=False):
"""Run gro binary with given test and return output."""
if not hasattr(cfg, "bin_remote"):
cfg.bin_local = cfg.net_lib_dir / "gro"
cfg.bin_remote = cfg.remote.deploy(cfg.bin_local)
ipver = cfg.addr_ipver
protocol = f"--ipv{ipver}"
dmac = _resolve_dmac(cfg, ipver)
base_args = [
protocol,
f"--dmac {dmac}",
f"--smac {cfg.remote_dev['address']}",
f"--daddr {cfg.addr}",
f"--saddr {cfg.remote_addr_v[ipver]}",
f"--test {test_name}",
]
if num_flows:
base_args.append(f"--num-flows {num_flows}")
if order_check:
base_args.append("--order-check")
args = " ".join(base_args)
rx_cmd = f"{cfg.bin_local} {args} --rx --iface {cfg.ifname}"
tx_cmd = f"{cfg.bin_remote} {args} --iface {cfg.remote_ifname}"
with bkg(rx_cmd, ksft_ready=True, exit_wait=True, fail=False) as rx_proc:
cmd(tx_cmd, host=cfg.remote)
if not ignore_fail:
ksft_eq(rx_proc.ret, 0)
if rx_proc.ret != 0:
ksft_pr(rx_proc)
return rx_proc.stdout
def _require_hw_gro_stats(cfg, queue_id):
"""Check if device reports HW GRO stats for the queue."""
stats = _get_queue_stats(cfg, queue_id)
required = ['rx-packets', 'rx-hw-gro-packets', 'rx-hw-gro-wire-packets']
for stat in required:
if stat not in stats:
raise KsftSkipEx(f"Driver does not report '{stat}' via qstats")
def _set_ethtool_feat(cfg, current, feats):
"""Set ethtool features with defer to restore original state."""
s2n = {True: "on", False: "off"}
new = ["-K", cfg.ifname]
old = ["-K", cfg.ifname]
no_change = True
for name, state in feats.items():
new += [name, s2n[state]]
old += [name, s2n[current[name]["active"]]]
if current[name]["active"] != state:
no_change = False
if current[name]["fixed"]:
raise KsftSkipEx(f"Device does not support {name}")
if no_change:
return
eth_cmd = ethtool(" ".join(new))
defer(ethtool, " ".join(old))
# If ethtool printed something kernel must have modified some features
if eth_cmd.stdout:
ksft_pr(eth_cmd)
def _setup_hw_gro(cfg):
"""Enable HW GRO on the device, disabling SW GRO."""
feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
# Try to disable SW GRO and enable HW GRO
_set_ethtool_feat(cfg, feat,
{"generic-receive-offload": False,
"rx-gro-hw": True,
"large-receive-offload": False})
# Some NICs treat HW GRO as a GRO sub-feature so disabling GRO
# will also clear HW GRO. Use a hack of installing XDP generic
# to skip SW GRO, even when enabled.
feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
if not feat["rx-gro-hw"]["active"]:
ksft_pr("Driver clears HW GRO when SW GRO is cleared, using generic XDP workaround")
prog = cfg.net_lib_dir / "xdp_dummy.bpf.o"
ip(f"link set dev {cfg.ifname} xdpgeneric obj {prog} sec xdp")
defer(ip, f"link set dev {cfg.ifname} xdpgeneric off")
# Attaching XDP may change features, fetch the latest state
feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
_set_ethtool_feat(cfg, feat,
{"generic-receive-offload": True,
"rx-gro-hw": True,
"large-receive-offload": False})
def _check_gro_stats(cfg, test_queue, stats_before,
expect_rx, expect_gro, expect_wire):
"""Validate GRO stats against expected values."""
stats_after = _get_queue_stats(cfg, test_queue)
rx_delta = (stats_after.get('rx-packets', 0) -
stats_before.get('rx-packets', 0))
gro_delta = (stats_after.get('rx-hw-gro-packets', 0) -
stats_before.get('rx-hw-gro-packets', 0))
wire_delta = (stats_after.get('rx-hw-gro-wire-packets', 0) -
stats_before.get('rx-hw-gro-wire-packets', 0))
ksft_eq(rx_delta, expect_rx, comment="rx-packets")
ksft_eq(gro_delta, expect_gro, comment="rx-hw-gro-packets")
ksft_eq(wire_delta, expect_wire, comment="rx-hw-gro-wire-packets")
def test_gro_stats_single(cfg):
"""
Test that a single packet doesn't affect GRO stats.
Send a single packet that cannot be coalesced (nothing to coalesce with).
GRO stats should not increase since no coalescing occurred.
rx-packets should increase by 2 (1 data + 1 FIN).
"""
_setup_hw_gro(cfg)
test_queue = _setup_isolated_queue(cfg)
_require_hw_gro_stats(cfg, test_queue)
stats_before = _get_queue_stats(cfg, test_queue)
_run_gro_test(cfg, "single")
# 1 data + 1 FIN = 2 rx-packets, no coalescing
_check_gro_stats(cfg, test_queue, stats_before,
expect_rx=2, expect_gro=0, expect_wire=0)
def test_gro_stats_full(cfg):
"""
Test GRO stats when overwhelming HW GRO capacity.
Send 500 flows to exceed HW GRO flow capacity on a single queue.
This should result in some packets not being coalesced.
Validate that qstats match what gro.c observed.
"""
_setup_hw_gro(cfg)
test_queue = _setup_isolated_queue(cfg)
_require_hw_gro_stats(cfg, test_queue)
num_flows = 500
stats_before = _get_queue_stats(cfg, test_queue)
# Run capacity test - will likely fail because not all packets coalesce
output = _run_gro_test(cfg, "capacity", num_flows=num_flows,
ignore_fail=True)
# Parse gro.c output: "STATS: received=X wire=Y coalesced=Z"
match = re.search(r'STATS: received=(\d+) wire=(\d+) coalesced=(\d+)',
output)
if not match:
raise KsftSkipEx(f"Could not parse gro.c output: {output}")
rx_frames = int(match.group(2))
gro_coalesced = int(match.group(3))
ksft_ge(gro_coalesced, 1,
comment="At least some packets should coalesce")
# received + 1 FIN, coalesced super-packets, coalesced * 2 wire packets
_check_gro_stats(cfg, test_queue, stats_before,
expect_rx=rx_frames + 1,
expect_gro=gro_coalesced,
expect_wire=gro_coalesced * 2)
@ksft_variants([4, 32, 512])
def test_gro_order(cfg, num_flows):
"""
Test that HW GRO preserves packet ordering between flows.
Packets may get delayed until the aggregate is released,
but reordering between aggregates and packet terminating
the aggregate and normal packets should not happen.
Note that this test is stricter than truly required.
Reordering packets between flows should not cause issues.
This test will also fail if traffic is run over an ECMP fabric.
"""
_setup_hw_gro(cfg)
_setup_isolated_queue(cfg)
_run_gro_test(cfg, "capacity", num_flows=num_flows, order_check=True)
def main() -> None:
""" Ksft boiler plate main """
with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
cfg.netnl = NetdevFamily()
ksft_run([test_gro_stats_single,
test_gro_stats_full,
test_gro_order], args=(cfg,))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,334 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
# +--------------------+ +----------------------+
# | H1 | | H2 |
# | | | |
# | $h1.200 + | | + $h2.200 |
# | 192.0.2.1/28 | | | | 192.0.2.18/28 |
# | 2001:db8:1::1/64 | | | | 2001:db8:2::1/64 |
# | | | | | |
# | $h1 + | | + $h2 |
# | | | | | |
# +------------------|-+ +-|--------------------+
# | |
# +------------------|-------------------------|--------------------+
# | SW | | |
# | | | |
# | $rp1 + + $rp2 |
# | | | |
# | $rp1.200 + + $rp2.200 |
# | 192.0.2.2/28 192.0.2.17/28 |
# | 2001:db8:1::2/64 2001:db8:2::2/64 |
# | |
# +-----------------------------------------------------------------+
ALL_TESTS="
ping_ipv4
ping_ipv6
test_stats_rx_ipv4
test_stats_tx_ipv4
test_stats_rx_ipv6
test_stats_tx_ipv6
respin_enablement
test_stats_rx_ipv4
test_stats_tx_ipv4
test_stats_rx_ipv6
test_stats_tx_ipv6
reapply_config
ping_ipv4
ping_ipv6
test_stats_rx_ipv4
test_stats_tx_ipv4
test_stats_rx_ipv6
test_stats_tx_ipv6
test_stats_report_rx
test_stats_report_tx
test_destroy_enabled
test_double_enable
"
NUM_NETIFS=4
lib_dir=$(dirname "$0")
source "$lib_dir"/../../../net/forwarding/lib.sh
source "$lib_dir"/../../../net/forwarding/tc_common.sh
h1_create()
{
simple_if_init $h1
vlan_create $h1 200 v$h1 192.0.2.1/28 2001:db8:1::1/64
ip route add 192.0.2.16/28 vrf v$h1 nexthop via 192.0.2.2
ip -6 route add 2001:db8:2::/64 vrf v$h1 nexthop via 2001:db8:1::2
}
h1_destroy()
{
ip -6 route del 2001:db8:2::/64 vrf v$h1 nexthop via 2001:db8:1::2
ip route del 192.0.2.16/28 vrf v$h1 nexthop via 192.0.2.2
vlan_destroy $h1 200
simple_if_fini $h1
}
h2_create()
{
simple_if_init $h2
vlan_create $h2 200 v$h2 192.0.2.18/28 2001:db8:2::1/64
ip route add 192.0.2.0/28 vrf v$h2 nexthop via 192.0.2.17
ip -6 route add 2001:db8:1::/64 vrf v$h2 nexthop via 2001:db8:2::2
}
h2_destroy()
{
ip -6 route del 2001:db8:1::/64 vrf v$h2 nexthop via 2001:db8:2::2
ip route del 192.0.2.0/28 vrf v$h2 nexthop via 192.0.2.17
vlan_destroy $h2 200
simple_if_fini $h2
}
router_rp1_200_create()
{
ip link add name $rp1.200 link $rp1 type vlan id 200
ip link set dev $rp1.200 addrgenmode eui64
ip link set dev $rp1.200 up
ip address add dev $rp1.200 192.0.2.2/28
ip address add dev $rp1.200 2001:db8:1::2/64
ip stats set dev $rp1.200 l3_stats on
}
router_rp1_200_destroy()
{
ip stats set dev $rp1.200 l3_stats off
ip address del dev $rp1.200 2001:db8:1::2/64
ip address del dev $rp1.200 192.0.2.2/28
ip link del dev $rp1.200
}
router_create()
{
ip link set dev $rp1 up
router_rp1_200_create
ip link set dev $rp2 up
vlan_create $rp2 200 "" 192.0.2.17/28 2001:db8:2::2/64
}
router_destroy()
{
vlan_destroy $rp2 200
ip link set dev $rp2 down
router_rp1_200_destroy
ip link set dev $rp1 down
}
setup_prepare()
{
h1=${NETIFS[p1]}
rp1=${NETIFS[p2]}
rp2=${NETIFS[p3]}
h2=${NETIFS[p4]}
rp1mac=$(mac_get $rp1)
rp2mac=$(mac_get $rp2)
vrf_prepare
h1_create
h2_create
router_create
forwarding_enable
}
cleanup()
{
pre_cleanup
forwarding_restore
router_destroy
h2_destroy
h1_destroy
vrf_cleanup
}
ping_ipv4()
{
ping_test $h1.200 192.0.2.18 " IPv4"
}
ping_ipv6()
{
ping_test $h1.200 2001:db8:2::1 " IPv6"
}
send_packets_rx_ipv4()
{
# Send 21 packets instead of 20, because the first one might trap and go
# through the SW datapath, which might not bump the HW counter.
$MZ $h1.200 -c 21 -d 20msec -p 100 \
-a own -b $rp1mac -A 192.0.2.1 -B 192.0.2.18 \
-q -t udp sp=54321,dp=12345
}
send_packets_rx_ipv6()
{
$MZ $h1.200 -6 -c 21 -d 20msec -p 100 \
-a own -b $rp1mac -A 2001:db8:1::1 -B 2001:db8:2::1 \
-q -t udp sp=54321,dp=12345
}
send_packets_tx_ipv4()
{
$MZ $h2.200 -c 21 -d 20msec -p 100 \
-a own -b $rp2mac -A 192.0.2.18 -B 192.0.2.1 \
-q -t udp sp=54321,dp=12345
}
send_packets_tx_ipv6()
{
$MZ $h2.200 -6 -c 21 -d 20msec -p 100 \
-a own -b $rp2mac -A 2001:db8:2::1 -B 2001:db8:1::1 \
-q -t udp sp=54321,dp=12345
}
___test_stats()
{
local dir=$1; shift
local prot=$1; shift
local a
local b
a=$(hw_stats_get l3_stats $rp1.200 ${dir} packets)
send_packets_${dir}_${prot}
"$@"
b=$(busywait "$TC_HIT_TIMEOUT" until_counter_is ">= $a + 20" \
hw_stats_get l3_stats $rp1.200 ${dir} packets)
check_err $? "Traffic not reflected in the counter: $a -> $b"
}
__test_stats()
{
local dir=$1; shift
local prot=$1; shift
RET=0
___test_stats "$dir" "$prot"
log_test "Test $dir packets: $prot"
}
test_stats_rx_ipv4()
{
__test_stats rx ipv4
}
test_stats_tx_ipv4()
{
__test_stats tx ipv4
}
test_stats_rx_ipv6()
{
__test_stats rx ipv6
}
test_stats_tx_ipv6()
{
__test_stats tx ipv6
}
# Make sure everything works well even after stats have been disabled and
# reenabled on the same device without touching the L3 configuration.
respin_enablement()
{
log_info "Turning stats off and on again"
ip stats set dev $rp1.200 l3_stats off
ip stats set dev $rp1.200 l3_stats on
}
# For the initial run, l3_stats is enabled on a completely set up netdevice. Now
# do it the other way around: enabling the L3 stats on an L2 netdevice, and only
# then apply the L3 configuration.
reapply_config()
{
log_info "Reapplying configuration"
router_rp1_200_destroy
ip link add name $rp1.200 link $rp1 type vlan id 200
ip link set dev $rp1.200 addrgenmode none
ip stats set dev $rp1.200 l3_stats on
ip link set dev $rp1.200 addrgenmode eui64
ip link set dev $rp1.200 up
ip address add dev $rp1.200 192.0.2.2/28
ip address add dev $rp1.200 2001:db8:1::2/64
}
__test_stats_report()
{
local dir=$1; shift
local prot=$1; shift
local a
local b
RET=0
a=$(hw_stats_get l3_stats $rp1.200 ${dir} packets)
send_packets_${dir}_${prot}
ip address flush dev $rp1.200
b=$(busywait "$TC_HIT_TIMEOUT" until_counter_is ">= $a + 20" \
hw_stats_get l3_stats $rp1.200 ${dir} packets)
check_err $? "Traffic not reflected in the counter: $a -> $b"
log_test "Test ${dir} packets: stats pushed on loss of L3"
ip stats set dev $rp1.200 l3_stats off
ip link del dev $rp1.200
router_rp1_200_create
}
test_stats_report_rx()
{
__test_stats_report rx ipv4
}
test_stats_report_tx()
{
__test_stats_report tx ipv4
}
test_destroy_enabled()
{
RET=0
ip link del dev $rp1.200
router_rp1_200_create
log_test "Destroy l3_stats-enabled netdev"
}
test_double_enable()
{
RET=0
___test_stats rx ipv4 \
ip stats set dev $rp1.200 l3_stats on
log_test "Test stat retention across a spurious enablement"
}
trap cleanup EXIT
setup_prepare
setup_wait
used=$(ip -j stats show dev $rp1.200 group offload subgroup hw_stats_info |
jq '.[].info.l3_stats.used')
[[ $used = true ]]
check_err $? "hw_stats_info.used=$used"
log_test "l3_stats offloaded"
tests_run
exit $EXIT_STATUS
@@ -0,0 +1,111 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
# Test L3 stats on IP-in-IP GRE tunnel without key.
# This test uses flat topology for IP tunneling tests. See ipip_lib.sh for more
# details.
ALL_TESTS="
ping_ipv4
test_stats_rx
test_stats_tx
"
NUM_NETIFS=6
lib_dir=$(dirname "$0")
source "$lib_dir"/../../../net/forwarding/lib.sh
source "$lib_dir"/../../../net/forwarding/ipip_lib.sh
source "$lib_dir"/../../../net/forwarding/tc_common.sh
setup_prepare()
{
h1=${NETIFS[p1]}
ol1=${NETIFS[p2]}
ul1=${NETIFS[p3]}
ul2=${NETIFS[p4]}
ol2=${NETIFS[p5]}
h2=${NETIFS[p6]}
ol1mac=$(mac_get $ol1)
forwarding_enable
vrf_prepare
h1_create
h2_create
sw1_flat_create gre $ol1 $ul1
sw2_flat_create gre $ol2 $ul2
ip stats set dev g1a l3_stats on
ip stats set dev g2a l3_stats on
}
cleanup()
{
pre_cleanup
ip stats set dev g1a l3_stats off
ip stats set dev g2a l3_stats off
sw2_flat_destroy $ol2 $ul2
sw1_flat_destroy $ol1 $ul1
h2_destroy
h1_destroy
vrf_cleanup
forwarding_restore
}
ping_ipv4()
{
RET=0
ping_test $h1 192.0.2.18 " gre flat"
}
send_packets_ipv4()
{
# Send 21 packets instead of 20, because the first one might trap and go
# through the SW datapath, which might not bump the HW counter.
$MZ $h1 -c 21 -d 20msec -p 100 \
-a own -b $ol1mac -A 192.0.2.1 -B 192.0.2.18 \
-q -t udp sp=54321,dp=12345
}
test_stats()
{
local dev=$1; shift
local dir=$1; shift
local a
local b
RET=0
a=$(hw_stats_get l3_stats $dev $dir packets)
send_packets_ipv4
b=$(busywait "$TC_HIT_TIMEOUT" until_counter_is ">= $a + 20" \
hw_stats_get l3_stats $dev $dir packets)
check_err $? "Traffic not reflected in the counter: $a -> $b"
log_test "Test $dir packets: $prot"
}
test_stats_tx()
{
test_stats g1a tx
}
test_stats_rx()
{
test_stats g2a rx
}
trap cleanup EXIT
setup_prepare
setup_wait
tests_run
exit $EXIT_STATUS
@@ -0,0 +1,512 @@
// SPDX-License-Identifier: GPL-2.0
#include <assert.h>
#include <errno.h>
#include <error.h>
#include <fcntl.h>
#include <limits.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <linux/mman.h>
#include <linux/errqueue.h>
#include <linux/if_packet.h>
#include <linux/ipv6.h>
#include <linux/socket.h>
#include <linux/sockios.h>
#include <net/ethernet.h>
#include <net/if.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip6.h>
#include <netinet/tcp.h>
#include <netinet/udp.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <liburing.h>
#define SKIP_CODE 42
struct t_io_uring_zcrx_ifq_reg {
__u32 if_idx;
__u32 if_rxq;
__u32 rq_entries;
__u32 flags;
__u64 area_ptr; /* pointer to struct io_uring_zcrx_area_reg */
__u64 region_ptr; /* struct io_uring_region_desc * */
struct io_uring_zcrx_offsets offsets;
__u32 zcrx_id;
__u32 rx_buf_len;
__u64 __resv[3];
};
static long page_size;
#define AREA_SIZE (8192 * page_size)
#define SEND_SIZE (512 * 4096)
#define min(a, b) \
({ \
typeof(a) _a = (a); \
typeof(b) _b = (b); \
_a < _b ? _a : _b; \
})
#define min_t(t, a, b) \
({ \
t _ta = (a); \
t _tb = (b); \
min(_ta, _tb); \
})
#define ALIGN_UP(v, align) (((v) + (align) - 1) & ~((align) - 1))
static int cfg_server;
static int cfg_client;
static int cfg_port = 8000;
static int cfg_payload_len;
static const char *cfg_ifname;
static int cfg_queue_id = -1;
static bool cfg_oneshot;
static int cfg_oneshot_recvs;
static int cfg_send_size = SEND_SIZE;
static struct sockaddr_in6 cfg_addr;
static unsigned int cfg_rx_buf_len;
static bool cfg_dry_run;
static char *payload;
static void *area_ptr;
static void *ring_ptr;
static size_t ring_size;
static struct io_uring_zcrx_rq rq_ring;
static unsigned long area_token;
static int connfd;
static bool stop;
static size_t received;
static unsigned long gettimeofday_ms(void)
{
struct timeval tv;
gettimeofday(&tv, NULL);
return (tv.tv_sec * 1000) + (tv.tv_usec / 1000);
}
static int parse_address(const char *str, int port, struct sockaddr_in6 *sin6)
{
int ret;
sin6->sin6_family = AF_INET6;
sin6->sin6_port = htons(port);
ret = inet_pton(sin6->sin6_family, str, &sin6->sin6_addr);
if (ret != 1) {
/* fallback to plain IPv4 */
ret = inet_pton(AF_INET, str, &sin6->sin6_addr.s6_addr32[3]);
if (ret != 1)
return -1;
/* add ::ffff prefix */
sin6->sin6_addr.s6_addr32[0] = 0;
sin6->sin6_addr.s6_addr32[1] = 0;
sin6->sin6_addr.s6_addr16[4] = 0;
sin6->sin6_addr.s6_addr16[5] = 0xffff;
}
return 0;
}
static inline size_t get_refill_ring_size(unsigned int rq_entries)
{
size_t size;
ring_size = rq_entries * sizeof(struct io_uring_zcrx_rqe);
/* add space for the header (head/tail/etc.) */
ring_size += page_size;
return ALIGN_UP(ring_size, page_size);
}
static void setup_zcrx(struct io_uring *ring)
{
unsigned int ifindex;
unsigned int rq_entries = 4096;
int ret;
ifindex = if_nametoindex(cfg_ifname);
if (!ifindex)
error(1, 0, "bad interface name: %s", cfg_ifname);
if (cfg_rx_buf_len && cfg_rx_buf_len != page_size) {
area_ptr = mmap(NULL,
AREA_SIZE,
PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE |
MAP_HUGETLB | MAP_HUGE_2MB,
-1,
0);
if (area_ptr == MAP_FAILED) {
printf("Can't allocate huge pages\n");
exit(SKIP_CODE);
}
} else {
area_ptr = mmap(NULL,
AREA_SIZE,
PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE,
0,
0);
if (area_ptr == MAP_FAILED)
error(1, 0, "mmap(): zero copy area");
}
ring_size = get_refill_ring_size(rq_entries);
ring_ptr = mmap(NULL,
ring_size,
PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE,
0,
0);
struct io_uring_region_desc region_reg = {
.size = ring_size,
.user_addr = (__u64)(unsigned long)ring_ptr,
.flags = IORING_MEM_REGION_TYPE_USER,
};
struct io_uring_zcrx_area_reg area_reg = {
.addr = (__u64)(unsigned long)area_ptr,
.len = AREA_SIZE,
.flags = 0,
};
struct t_io_uring_zcrx_ifq_reg reg = {
.if_idx = ifindex,
.if_rxq = cfg_queue_id,
.rq_entries = rq_entries,
.area_ptr = (__u64)(unsigned long)&area_reg,
.region_ptr = (__u64)(unsigned long)&region_reg,
.rx_buf_len = cfg_rx_buf_len,
};
ret = io_uring_register_ifq(ring, (void *)&reg);
if (cfg_rx_buf_len && (ret == -EINVAL || ret == -EOPNOTSUPP ||
ret == -ERANGE)) {
printf("Large chunks are not supported %i\n", ret);
exit(SKIP_CODE);
} else if (ret) {
error(1, 0, "io_uring_register_ifq(): %d", ret);
}
rq_ring.khead = (unsigned int *)((char *)ring_ptr + reg.offsets.head);
rq_ring.ktail = (unsigned int *)((char *)ring_ptr + reg.offsets.tail);
rq_ring.rqes = (struct io_uring_zcrx_rqe *)((char *)ring_ptr + reg.offsets.rqes);
rq_ring.rq_tail = 0;
rq_ring.ring_entries = reg.rq_entries;
area_token = area_reg.rq_area_token;
}
static void add_accept(struct io_uring *ring, int sockfd)
{
struct io_uring_sqe *sqe;
sqe = io_uring_get_sqe(ring);
io_uring_prep_accept(sqe, sockfd, NULL, NULL, 0);
sqe->user_data = 1;
}
static void add_recvzc(struct io_uring *ring, int sockfd)
{
struct io_uring_sqe *sqe;
sqe = io_uring_get_sqe(ring);
io_uring_prep_rw(IORING_OP_RECV_ZC, sqe, sockfd, NULL, 0, 0);
sqe->ioprio |= IORING_RECV_MULTISHOT;
sqe->user_data = 2;
}
static void add_recvzc_oneshot(struct io_uring *ring, int sockfd, size_t len)
{
struct io_uring_sqe *sqe;
sqe = io_uring_get_sqe(ring);
io_uring_prep_rw(IORING_OP_RECV_ZC, sqe, sockfd, NULL, len, 0);
sqe->ioprio |= IORING_RECV_MULTISHOT;
sqe->user_data = 2;
}
static void process_accept(struct io_uring *ring, struct io_uring_cqe *cqe)
{
if (cqe->res < 0)
error(1, 0, "accept()");
if (connfd)
error(1, 0, "Unexpected second connection");
connfd = cqe->res;
if (cfg_oneshot)
add_recvzc_oneshot(ring, connfd, page_size);
else
add_recvzc(ring, connfd);
}
static void process_recvzc(struct io_uring *ring, struct io_uring_cqe *cqe)
{
unsigned rq_mask = rq_ring.ring_entries - 1;
struct io_uring_zcrx_cqe *rcqe;
struct io_uring_zcrx_rqe *rqe;
struct io_uring_sqe *sqe;
uint64_t mask;
char *data;
ssize_t n;
int i;
if (cqe->res == 0 && cqe->flags == 0 && cfg_oneshot_recvs == 0) {
stop = true;
return;
}
if (cqe->res < 0)
error(1, 0, "recvzc(): %d", cqe->res);
if (cfg_oneshot) {
if (cqe->res == 0 && cqe->flags == 0 && cfg_oneshot_recvs) {
add_recvzc_oneshot(ring, connfd, page_size);
cfg_oneshot_recvs--;
}
} else if (!(cqe->flags & IORING_CQE_F_MORE)) {
add_recvzc(ring, connfd);
}
rcqe = (struct io_uring_zcrx_cqe *)(cqe + 1);
n = cqe->res;
mask = (1ULL << IORING_ZCRX_AREA_SHIFT) - 1;
data = (char *)area_ptr + (rcqe->off & mask);
for (i = 0; i < n; i++) {
if (*(data + i) != payload[(received + i)])
error(1, 0, "payload mismatch at %d", i);
}
received += n;
rqe = &rq_ring.rqes[(rq_ring.rq_tail & rq_mask)];
rqe->off = (rcqe->off & ~IORING_ZCRX_AREA_MASK) | area_token;
rqe->len = cqe->res;
io_uring_smp_store_release(rq_ring.ktail, ++rq_ring.rq_tail);
}
static void server_loop(struct io_uring *ring)
{
struct io_uring_cqe *cqe;
unsigned int count = 0;
unsigned int head;
int i, ret;
io_uring_submit_and_wait(ring, 1);
io_uring_for_each_cqe(ring, head, cqe) {
if (cqe->user_data == 1)
process_accept(ring, cqe);
else if (cqe->user_data == 2)
process_recvzc(ring, cqe);
else
error(1, 0, "unknown cqe");
count++;
}
io_uring_cq_advance(ring, count);
}
static void run_server(void)
{
unsigned int flags = 0;
struct io_uring ring;
int fd, enable, ret;
uint64_t tstop;
fd = socket(AF_INET6, SOCK_STREAM, 0);
if (fd == -1)
error(1, 0, "socket()");
enable = 1;
ret = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
if (ret < 0)
error(1, 0, "setsockopt(SO_REUSEADDR)");
ret = bind(fd, (struct sockaddr *)&cfg_addr, sizeof(cfg_addr));
if (ret < 0)
error(1, 0, "bind()");
flags |= IORING_SETUP_COOP_TASKRUN;
flags |= IORING_SETUP_SINGLE_ISSUER;
flags |= IORING_SETUP_DEFER_TASKRUN;
flags |= IORING_SETUP_SUBMIT_ALL;
flags |= IORING_SETUP_CQE32;
io_uring_queue_init(512, &ring, flags);
setup_zcrx(&ring);
if (cfg_dry_run)
return;
if (listen(fd, 1024) < 0)
error(1, 0, "listen()");
add_accept(&ring, fd);
tstop = gettimeofday_ms() + 5000;
while (!stop && gettimeofday_ms() < tstop)
server_loop(&ring);
if (!stop)
error(1, 0, "test failed\n");
}
static void run_client(void)
{
ssize_t to_send = cfg_send_size;
ssize_t sent = 0;
ssize_t chunk, res;
int fd;
fd = socket(AF_INET6, SOCK_STREAM, 0);
if (fd == -1)
error(1, 0, "socket()");
if (connect(fd, (struct sockaddr *)&cfg_addr, sizeof(cfg_addr)))
error(1, 0, "connect()");
while (to_send) {
void *src = &payload[sent];
chunk = min_t(ssize_t, cfg_payload_len, to_send);
res = send(fd, src, chunk, 0);
if (res < 0)
error(1, 0, "send(): %zd", sent);
sent += res;
to_send -= res;
}
close(fd);
}
static void usage(const char *filepath)
{
error(1, 0, "Usage: %s (-4|-6) (-s|-c) -h<server_ip> -p<port> "
"-l<payload_size> -i<ifname> -q<rxq_id>", filepath);
}
static void parse_opts(int argc, char **argv)
{
const int max_payload_len = SEND_SIZE -
sizeof(struct ipv6hdr) -
sizeof(struct tcphdr) -
40 /* max tcp options */;
struct sockaddr_in6 *addr6 = (void *) &cfg_addr;
char *addr = NULL;
int ret;
int c;
if (argc <= 1)
usage(argv[0]);
cfg_payload_len = max_payload_len;
while ((c = getopt(argc, argv, "sch:p:l:i:q:o:z:x:d")) != -1) {
switch (c) {
case 's':
if (cfg_client)
error(1, 0, "Pass one of -s or -c");
cfg_server = 1;
break;
case 'c':
if (cfg_server)
error(1, 0, "Pass one of -s or -c");
cfg_client = 1;
break;
case 'h':
addr = optarg;
break;
case 'p':
cfg_port = strtoul(optarg, NULL, 0);
break;
case 'l':
cfg_payload_len = strtoul(optarg, NULL, 0);
break;
case 'i':
cfg_ifname = optarg;
break;
case 'q':
cfg_queue_id = strtoul(optarg, NULL, 0);
break;
case 'o': {
cfg_oneshot = true;
cfg_oneshot_recvs = strtoul(optarg, NULL, 0);
break;
}
case 'z':
cfg_send_size = strtoul(optarg, NULL, 0);
break;
case 'x':
cfg_rx_buf_len = page_size * strtoul(optarg, NULL, 0);
break;
case 'd':
cfg_dry_run = true;
break;
}
}
if (cfg_server && addr)
error(1, 0, "Receiver cannot have -h specified");
memset(addr6, 0, sizeof(*addr6));
addr6->sin6_family = AF_INET6;
addr6->sin6_port = htons(cfg_port);
addr6->sin6_addr = in6addr_any;
if (addr) {
ret = parse_address(addr, cfg_port, addr6);
if (ret)
error(1, 0, "receiver address parse error: %s", addr);
}
if (cfg_payload_len > max_payload_len)
error(1, 0, "-l: payload exceeds max (%d)", max_payload_len);
}
int main(int argc, char **argv)
{
const char *cfg_test = argv[argc - 1];
int i;
page_size = sysconf(_SC_PAGESIZE);
if (page_size < 0)
return 1;
if (posix_memalign((void **)&payload, page_size, SEND_SIZE))
return 1;
parse_opts(argc, argv);
for (i = 0; i < SEND_SIZE; i++)
payload[i] = 'a' + (i % 26);
if (cfg_server)
run_server();
else if (cfg_client)
run_client();
return 0;
}
@@ -0,0 +1,210 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
import re
import resource
import time
from os import path
from lib.py import ksft_run, ksft_exit, KsftSkipEx, ksft_variants, KsftNamedVariant
from lib.py import NetDrvEpEnv
from lib.py import bkg, cmd, defer, ethtool, rand_port, wait_port_listen
from lib.py import EthtoolFamily, NetdevFamily
SKIP_CODE = 42
def mp_clear_wait(cfg):
"""Wait for io_uring memory providers to clear from all device queues."""
deadline = time.time() + 5
while time.time() < deadline:
queues = cfg.netnl.queue_get({'ifindex': cfg.ifindex}, dump=True)
if not any('io-uring' in q for q in queues):
return
time.sleep(0.1)
raise TimeoutError("Timed out waiting for memory provider to clear")
def create_rss_ctx(cfg):
output = ethtool(f"-X {cfg.ifname} context new start {cfg.target} equal 1").stdout
values = re.search(r'New RSS context is (\d+)', output).group(1)
return int(values)
def set_flow_rule(cfg):
output = ethtool(f"-N {cfg.ifname} flow-type tcp6 dst-port {cfg.port} action {cfg.target}").stdout
values = re.search(r'ID (\d+)', output).group(1)
return int(values)
def set_flow_rule_rss(cfg, rss_ctx_id):
output = ethtool(f"-N {cfg.ifname} flow-type tcp6 dst-port {cfg.port} context {rss_ctx_id}").stdout
values = re.search(r'ID (\d+)', output).group(1)
return int(values)
def check_iou_rx_buf_len(cfg, expected_rx_buf_len):
"""Check the io-uring memory provider exposes the expected rx_buf_len."""
q = cfg.netnl.queue_get({'ifindex': cfg.ifindex, 'type': 'rx', 'id': cfg.target})
napi_id = q['napi-id']
pools = cfg.netnl.page_pool_get({}, dump=True)
pools = [p for p in pools if p.get('napi-id') == napi_id
and 'io-uring' in p]
if len(pools) != 1:
raise Exception(f"Expected 1 io-uring page pool, found {len(pools)}")
rx_buf_len = pools[0]['io-uring'].get('rx-buf-len')
if rx_buf_len is None:
raise KsftSkipEx("io-uring 'rx-buf-len' attribute not supported")
if rx_buf_len != expected_rx_buf_len:
raise Exception(f'Expected io-uring rx-buf-len {expected_rx_buf_len}, '
f'got {rx_buf_len}')
def single(cfg):
channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
channels = channels['combined-count']
if channels < 2:
raise KsftSkipEx('Test requires NETIF with at least 2 combined channels')
rings = cfg.ethnl.rings_get({'header': {'dev-index': cfg.ifindex}})
rx_rings = rings['rx']
hds_thresh = rings.get('hds-thresh', 0)
cfg.ethnl.rings_set({'header': {'dev-index': cfg.ifindex},
'tcp-data-split': 'enabled',
'hds-thresh': 0,
'rx': 64})
defer(cfg.ethnl.rings_set, {'header': {'dev-index': cfg.ifindex},
'tcp-data-split': 'unknown',
'hds-thresh': hds_thresh,
'rx': rx_rings})
defer(mp_clear_wait, cfg)
cfg.target = channels - 1
ethtool(f"-X {cfg.ifname} equal {cfg.target}")
defer(ethtool, f"-X {cfg.ifname} default")
flow_rule_id = set_flow_rule(cfg)
defer(ethtool, f"-N {cfg.ifname} delete {flow_rule_id}")
def rss(cfg):
channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
channels = channels['combined-count']
if channels < 2:
raise KsftSkipEx('Test requires NETIF with at least 2 combined channels')
rings = cfg.ethnl.rings_get({'header': {'dev-index': cfg.ifindex}})
rx_rings = rings['rx']
hds_thresh = rings.get('hds-thresh', 0)
cfg.ethnl.rings_set({'header': {'dev-index': cfg.ifindex},
'tcp-data-split': 'enabled',
'hds-thresh': 0,
'rx': 64})
defer(cfg.ethnl.rings_set, {'header': {'dev-index': cfg.ifindex},
'tcp-data-split': 'unknown',
'hds-thresh': hds_thresh,
'rx': rx_rings})
defer(mp_clear_wait, cfg)
cfg.target = channels - 1
ethtool(f"-X {cfg.ifname} equal {cfg.target}")
defer(ethtool, f"-X {cfg.ifname} default")
rss_ctx_id = create_rss_ctx(cfg)
defer(ethtool, f"-X {cfg.ifname} delete context {rss_ctx_id}")
flow_rule_id = set_flow_rule_rss(cfg, rss_ctx_id)
defer(ethtool, f"-N {cfg.ifname} delete {flow_rule_id}")
def _require_ntuple(cfg):
features = ethtool(f"-k {cfg.ifname}", json=True)[0]
if not features["ntuple-filters"]["active"]:
if features["ntuple-filters"]["fixed"]:
raise KsftSkipEx("Device does not support ntuple-filters")
ethtool(f"-K {cfg.ifname} ntuple-filters on")
defer(ethtool, f"-K {cfg.ifname} ntuple-filters off")
@ksft_variants([
KsftNamedVariant("single", single),
KsftNamedVariant("rss", rss),
])
def test_zcrx(cfg, setup) -> None:
cfg.require_ipver('6')
_require_ntuple(cfg)
setup(cfg)
rx_cmd = f"{cfg.bin_local} -s -p {cfg.port} -i {cfg.ifname} -q {cfg.target}"
tx_cmd = f"{cfg.bin_remote} -c -h {cfg.addr_v['6']} -p {cfg.port} -l 12840"
with bkg(rx_cmd, exit_wait=True):
wait_port_listen(cfg.port, proto="tcp")
cmd(tx_cmd, host=cfg.remote)
@ksft_variants([
KsftNamedVariant("single", single),
KsftNamedVariant("rss", rss),
])
def test_zcrx_oneshot(cfg, setup) -> None:
cfg.require_ipver('6')
_require_ntuple(cfg)
setup(cfg)
rx_cmd = f"{cfg.bin_local} -s -p {cfg.port} -i {cfg.ifname} -q {cfg.target} -o 4"
tx_cmd = f"{cfg.bin_remote} -c -h {cfg.addr_v['6']} -p {cfg.port} -l 4096 -z 16384"
with bkg(rx_cmd, exit_wait=True):
wait_port_listen(cfg.port, proto="tcp")
cmd(tx_cmd, host=cfg.remote)
def test_zcrx_large_chunks(cfg) -> None:
"""Test zcrx with large buffer chunks."""
cfg.require_ipver('6')
_require_ntuple(cfg)
hp_file = "/proc/sys/vm/nr_hugepages"
with open(hp_file, 'r+', encoding='utf-8') as f:
nr_hugepages = int(f.read().strip())
if nr_hugepages < 64:
f.seek(0)
f.write("64")
defer(lambda: open(hp_file, 'w', encoding='utf-8').write(str(nr_hugepages)))
single(cfg)
page_size = resource.getpagesize()
nr_pages = 2
rx_buf_len = nr_pages * page_size
rx_cmd = f"{cfg.bin_local} -s -p {cfg.port} -i {cfg.ifname} -q {cfg.target} -x {nr_pages}"
tx_cmd = f"{cfg.bin_remote} -c -h {cfg.addr_v['6']} -p {cfg.port} -l 12840"
probe = cmd(rx_cmd + " -d", fail=False)
if probe.ret == SKIP_CODE:
raise KsftSkipEx(probe.stdout.strip())
mp_clear_wait(cfg)
with bkg(rx_cmd, exit_wait=True):
wait_port_listen(cfg.port, proto="tcp")
check_iou_rx_buf_len(cfg, rx_buf_len)
cmd(tx_cmd, host=cfg.remote)
def main() -> None:
with NetDrvEpEnv(__file__) as cfg:
cfg.bin_local = path.abspath(path.dirname(__file__) + "/../../../drivers/net/hw/iou-zcrx")
cfg.bin_remote = cfg.remote.deploy(cfg.bin_local)
cfg.ethnl = EthtoolFamily()
cfg.netnl = NetdevFamily()
cfg.port = rand_port()
ksft_run(globs=globals(), cases=[test_zcrx, test_zcrx_oneshot,
test_zcrx_large_chunks], args=(cfg, ))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,204 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""Traffic test for VXLAN + IPsec crypto-offload."""
import os
from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_ge
from lib.py import ksft_variants, KsftNamedVariant, KsftSkipEx
from lib.py import CmdExitFailure, NetDrvEpEnv, cmd, defer, ethtool, ip
from lib.py import Iperf3Runner
# Inner tunnel addresses - TEST-NET-2 (RFC 5737) / doc prefix (RFC 3849)
INNER_V4_LOCAL = "198.51.100.1"
INNER_V4_REMOTE = "198.51.100.2"
INNER_V6_LOCAL = "2001:db8:100::1"
INNER_V6_REMOTE = "2001:db8:100::2"
# ESP parameters
SPI_OUT = "0x1000"
SPI_IN = "0x1001"
# 128-bit key + 32-bit salt = 20 bytes hex, 128-bit ICV
ESP_AEAD = "aead 'rfc4106(gcm(aes))' 0x" + "01" * 20 + " 128"
def xfrm(args, host=None):
"""Runs 'ip xfrm' via shell to preserve parentheses in algo names."""
cmd(f"ip xfrm {args}", shell=True, host=host)
def check_xfrm_offload_support():
"""Skips if iproute2 lacks xfrm offload support."""
out = cmd("ip xfrm state help", fail=False)
if "offload" not in out.stdout + out.stderr:
raise KsftSkipEx("iproute2 too old, missing xfrm offload")
def check_esp_hw_offload(cfg):
"""Skips if device lacks esp-hw-offload support."""
check_xfrm_offload_support()
try:
feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
except (CmdExitFailure, IndexError) as e:
raise KsftSkipEx(f"can't query features: {e}") from e
if not feat.get("esp-hw-offload", {}).get("active"):
raise KsftSkipEx("Device does not support esp-hw-offload")
def get_tx_drops(cfg):
"""Returns TX dropped counter from the physical device."""
stats = ip("-s -s link show dev " + cfg.ifname, json=True)[0]
return stats["stats64"]["tx"]["dropped"]
def setup_vxlan_ipsec(cfg, outer_ipver, inner_ipver):
"""Sets up VXLAN tunnel with IPsec transport-mode crypto-offload."""
vxlan_name = f"vx{os.getpid()}"
local_addr = cfg.addr_v[outer_ipver]
remote_addr = cfg.remote_addr_v[outer_ipver]
if inner_ipver == "4":
inner_local = f"{INNER_V4_LOCAL}/24"
inner_remote = f"{INNER_V4_REMOTE}/24"
addr_extra = ""
else:
inner_local = f"{INNER_V6_LOCAL}/64"
inner_remote = f"{INNER_V6_REMOTE}/64"
addr_extra = " nodad"
if outer_ipver == "6":
vxlan_opts = "udp6zerocsumtx udp6zerocsumrx"
else:
vxlan_opts = "noudpcsum"
# VXLAN tunnel - local side
ip(f"link add {vxlan_name} type vxlan id 100 dstport 4789 {vxlan_opts} "
f"local {local_addr} remote {remote_addr} dev {cfg.ifname}")
defer(ip, f"link del {vxlan_name}")
ip(f"addr add {inner_local} dev {vxlan_name}{addr_extra}")
ip(f"link set {vxlan_name} up")
# VXLAN tunnel - remote side
ip(f"link add {vxlan_name} type vxlan id 100 dstport 4789 {vxlan_opts} "
f"local {remote_addr} remote {local_addr} dev {cfg.remote_ifname}",
host=cfg.remote)
defer(ip, f"link del {vxlan_name}", host=cfg.remote)
ip(f"addr add {inner_remote} dev {vxlan_name}{addr_extra}",
host=cfg.remote)
ip(f"link set {vxlan_name} up", host=cfg.remote)
# xfrm state - local outbound SA
xfrm(f"state add src {local_addr} dst {remote_addr} "
f"proto esp spi {SPI_OUT} "
f"{ESP_AEAD} "
f"mode transport offload crypto dev {cfg.ifname} dir out")
defer(xfrm, f"state del src {local_addr} dst {remote_addr} "
f"proto esp spi {SPI_OUT}")
# xfrm state - local inbound SA
xfrm(f"state add src {remote_addr} dst {local_addr} "
f"proto esp spi {SPI_IN} "
f"{ESP_AEAD} "
f"mode transport offload crypto dev {cfg.ifname} dir in")
defer(xfrm, f"state del src {remote_addr} dst {local_addr} "
f"proto esp spi {SPI_IN}")
# xfrm state - remote outbound SA (mirror, software crypto)
xfrm(f"state add src {remote_addr} dst {local_addr} "
f"proto esp spi {SPI_IN} "
f"{ESP_AEAD} "
f"mode transport",
host=cfg.remote)
defer(xfrm, f"state del src {remote_addr} dst {local_addr} "
f"proto esp spi {SPI_IN}", host=cfg.remote)
# xfrm state - remote inbound SA (mirror, software crypto)
xfrm(f"state add src {local_addr} dst {remote_addr} "
f"proto esp spi {SPI_OUT} "
f"{ESP_AEAD} "
f"mode transport",
host=cfg.remote)
defer(xfrm, f"state del src {local_addr} dst {remote_addr} "
f"proto esp spi {SPI_OUT}", host=cfg.remote)
# xfrm policy - local out
xfrm(f"policy add src {local_addr} dst {remote_addr} "
f"proto udp dport 4789 dir out "
f"tmpl src {local_addr} dst {remote_addr} proto esp mode transport")
defer(xfrm, f"policy del src {local_addr} dst {remote_addr} "
f"proto udp dport 4789 dir out")
# xfrm policy - local in
xfrm(f"policy add src {remote_addr} dst {local_addr} "
f"proto udp dport 4789 dir in "
f"tmpl src {remote_addr} dst {local_addr} proto esp mode transport")
defer(xfrm, f"policy del src {remote_addr} dst {local_addr} "
f"proto udp dport 4789 dir in")
# xfrm policy - remote out
xfrm(f"policy add src {remote_addr} dst {local_addr} "
f"proto udp dport 4789 dir out "
f"tmpl src {remote_addr} dst {local_addr} proto esp mode transport",
host=cfg.remote)
defer(xfrm, f"policy del src {remote_addr} dst {local_addr} "
f"proto udp dport 4789 dir out", host=cfg.remote)
# xfrm policy - remote in
xfrm(f"policy add src {local_addr} dst {remote_addr} "
f"proto udp dport 4789 dir in "
f"tmpl src {local_addr} dst {remote_addr} proto esp mode transport",
host=cfg.remote)
defer(xfrm, f"policy del src {local_addr} dst {remote_addr} "
f"proto udp dport 4789 dir in", host=cfg.remote)
def _vxlan_ipsec_variants():
"""Generates outer/inner IP version variants."""
for outer in ["4", "6"]:
for inner in ["4", "6"]:
yield KsftNamedVariant(f"outer_v{outer}_inner_v{inner}", outer, inner)
@ksft_variants(_vxlan_ipsec_variants())
def test_vxlan_ipsec_crypto_offload(cfg, outer_ipver, inner_ipver):
"""Tests VXLAN+IPsec crypto-offload has no TX drops."""
cfg.require_ipver(outer_ipver)
check_esp_hw_offload(cfg)
setup_vxlan_ipsec(cfg, outer_ipver, inner_ipver)
if inner_ipver == "4":
inner_local = INNER_V4_LOCAL
inner_remote = INNER_V4_REMOTE
ping = "ping"
else:
inner_local = INNER_V6_LOCAL
inner_remote = INNER_V6_REMOTE
ping = "ping -6"
cmd(f"{ping} -c 1 -W 2 {inner_remote}")
drops_before = get_tx_drops(cfg)
runner = Iperf3Runner(cfg, server_ip=inner_local,
client_ip=inner_remote)
bw_gbps = runner.measure_bandwidth(reverse=True)
cfg.wait_hw_stats_settle()
drops_after = get_tx_drops(cfg)
ksft_eq(drops_after - drops_before, 0,
comment="TX drops during VXLAN+IPsec")
ksft_ge(bw_gbps, 0.1,
comment="Minimum 100Mbps over VXLAN+IPsec")
def main():
"""Runs VXLAN+IPsec crypto-offload GSO selftest."""
with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
ksft_run([test_vxlan_ipsec_crypto_offload], args=(cfg,))
ksft_exit()
if __name__ == "__main__":
main()
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
from lib.py import ksft_run, ksft_exit
from lib.py import ksft_ge, ksft_eq
from lib.py import KsftSkipEx
from lib.py import ksft_disruptive
from lib.py import EthtoolFamily, NetdevFamily
from lib.py import NetDrvEnv
from lib.py import cmd, ip, defer
def read_affinity(irq) -> str:
with open(f'/proc/irq/{irq}/smp_affinity', 'r') as fp:
return fp.read().lstrip("0,").strip()
def write_affinity(irq, what) -> str:
if what != read_affinity(irq):
with open(f'/proc/irq/{irq}/smp_affinity', 'w') as fp:
fp.write(what)
def check_irqs_reported(cfg) -> None:
""" Check that device reports IRQs for NAPI instances """
napis = cfg.netnl.napi_get({"ifindex": cfg.ifindex}, dump=True)
irqs = sum(['irq' in x for x in napis])
ksft_ge(irqs, 1)
ksft_eq(irqs, len(napis))
def _check_reconfig(cfg, reconfig_cb) -> None:
napis = cfg.netnl.napi_get({"ifindex": cfg.ifindex}, dump=True)
for n in reversed(napis):
if 'irq' in n:
break
else:
raise KsftSkipEx(f"Device has no NAPI with IRQ attribute (#napis: {len(napis)}")
old = read_affinity(n['irq'])
# pick an affinity that's not the current one
new = "3" if old != "3" else "5"
write_affinity(n['irq'], new)
defer(write_affinity, n['irq'], old)
reconfig_cb(cfg)
ksft_eq(read_affinity(n['irq']), new, comment="IRQ affinity changed after reconfig")
def check_reconfig_queues(cfg) -> None:
def reconfig(cfg) -> None:
channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
if channels['combined-count'] == 0:
rx_type = 'rx'
else:
rx_type = 'combined'
cur_queue_cnt = channels[f'{rx_type}-count']
max_queue_cnt = channels[f'{rx_type}-max']
cmd(f"ethtool -L {cfg.ifname} {rx_type} 1")
cmd(f"ethtool -L {cfg.ifname} {rx_type} {max_queue_cnt}")
cmd(f"ethtool -L {cfg.ifname} {rx_type} {cur_queue_cnt}")
_check_reconfig(cfg, reconfig)
def check_reconfig_xdp(cfg) -> None:
def reconfig(cfg) -> None:
ip(f"link set dev %s xdp obj %s sec xdp" %
(cfg.ifname, cfg.net_lib_dir / "xdp_dummy.bpf.o"))
ip(f"link set dev %s xdp off" % cfg.ifname)
_check_reconfig(cfg, reconfig)
@ksft_disruptive
def check_down(cfg) -> None:
def reconfig(cfg) -> None:
ip("link set dev %s down" % cfg.ifname)
ip("link set dev %s up" % cfg.ifname)
_check_reconfig(cfg, reconfig)
def main() -> None:
with NetDrvEnv(__file__, nsim_test=False) as cfg:
cfg.ethnl = EthtoolFamily()
cfg.netnl = NetdevFamily()
ksft_run([check_irqs_reported, check_reconfig_queues,
check_reconfig_xdp, check_down],
args=(cfg, ))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,56 @@
# SPDX-License-Identifier: GPL-2.0
"""
Driver test environment (hardware-only tests).
NetDrvEnv and NetDrvEpEnv are the main environment classes.
NetDrvContEnv extends NetDrvEpEnv with netkit container support.
Former is for local host only tests, latter creates / connects
to a remote endpoint. See NIPA wiki for more information about
running and writing driver tests.
"""
import sys
from pathlib import Path
KSFT_DIR = (Path(__file__).parent / "../../../../..").resolve()
try:
sys.path.append(KSFT_DIR.as_posix())
# Import one by one to avoid pylint false positives
from net.lib.py import NetNS, NetNSEnter, NetdevSimDev, UserNetNS
from net.lib.py import EthtoolFamily, NetdevFamily, NetshaperFamily, \
NlError, RtnlFamily, DevlinkFamily, PSPFamily, Netlink
from net.lib.py import CmdExitFailure
from net.lib.py import bkg, cmd, bpftool, bpftrace, defer, ethtool, \
fd_read_timeout, ip, rand_port, rand_ports, wait_port_listen, \
wait_file, tool
from net.lib.py import bpf_map_set, bpf_map_dump, bpf_prog_map_ids
from net.lib.py import KsftSkipEx, KsftFailEx, KsftXfailEx
from net.lib.py import ksft_disruptive, ksft_exit, ksft_pr, ksft_run, \
ksft_setup, ksft_variants, KsftNamedVariant
from net.lib.py import ksft_eq, ksft_ge, ksft_in, ksft_is, ksft_lt, \
ksft_ne, ksft_not_in, ksft_raises, ksft_true, ksft_gt, ksft_not_none
from drivers.net.lib.py import GenerateTraffic, Remote, Iperf3Runner
from drivers.net.lib.py import NetDrvEnv, NetDrvEpEnv, NetDrvContEnv
__all__ = ["NetNS", "NetNSEnter", "NetdevSimDev", "UserNetNS",
"EthtoolFamily", "NetdevFamily", "NetshaperFamily",
"NlError", "RtnlFamily", "DevlinkFamily", "PSPFamily", "Netlink",
"CmdExitFailure",
"bkg", "cmd", "bpftool", "bpftrace", "defer", "ethtool",
"fd_read_timeout", "ip", "rand_port", "rand_ports",
"wait_port_listen", "wait_file", "tool",
"bpf_map_set", "bpf_map_dump", "bpf_prog_map_ids",
"KsftSkipEx", "KsftFailEx", "KsftXfailEx",
"ksft_disruptive", "ksft_exit", "ksft_pr", "ksft_run",
"ksft_setup", "ksft_variants", "KsftNamedVariant",
"ksft_eq", "ksft_ge", "ksft_in", "ksft_is", "ksft_lt",
"ksft_ne", "ksft_not_in", "ksft_raises", "ksft_true", "ksft_gt",
"ksft_not_none", "ksft_not_none",
"NetDrvEnv", "NetDrvEpEnv", "NetDrvContEnv", "GenerateTraffic",
"Remote", "Iperf3Runner"]
except ModuleNotFoundError as e:
print("Failed importing `net` library from kernel sources")
print(str(e))
sys.exit(4)
@@ -0,0 +1,103 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
# Kselftest framework requirement - SKIP code is 4.
ksft_skip=4
ALL_TESTS="loopback_test"
NUM_NETIFS=2
lib_dir=$(dirname "$0")
source "$lib_dir"/../../../net/forwarding/tc_common.sh
source "$lib_dir"/../../../net/forwarding/lib.sh
h1_create()
{
simple_if_init $h1 192.0.2.1/24
tc qdisc add dev $h1 clsact
}
h1_destroy()
{
tc qdisc del dev $h1 clsact
simple_if_fini $h1 192.0.2.1/24
}
h2_create()
{
simple_if_init $h2
}
h2_destroy()
{
simple_if_fini $h2
}
loopback_test()
{
RET=0
tc filter add dev $h1 ingress protocol arp pref 1 handle 101 flower \
skip_hw arp_op reply arp_tip 192.0.2.1 action drop
$MZ $h1 -c 1 -t arp -q
tc_check_packets "dev $h1 ingress" 101 1
check_fail $? "Matched on a filter without loopback setup"
ethtool -K $h1 loopback on
check_err $? "Failed to enable loopback"
setup_wait_dev $h1
$MZ $h1 -c 1 -t arp -q
tc_check_packets "dev $h1 ingress" 101 1
check_err $? "Did not match on filter with loopback"
ethtool -K $h1 loopback off
check_err $? "Failed to disable loopback"
$MZ $h1 -c 1 -t arp -q
tc_check_packets "dev $h1 ingress" 101 2
check_fail $? "Matched on a filter after loopback was removed"
tc filter del dev $h1 ingress protocol arp pref 1 handle 101 flower
log_test "loopback"
}
setup_prepare()
{
h1=${NETIFS[p1]}
h2=${NETIFS[p2]}
vrf_prepare
h1_create
h2_create
if ethtool -k $h1 | grep loopback | grep -q fixed; then
log_test "SKIP: dev $h1 does not support loopback feature"
exit $ksft_skip
fi
}
cleanup()
{
pre_cleanup
h2_destroy
h1_destroy
vrf_cleanup
}
trap cleanup EXIT
setup_prepare
setup_wait
tests_run
exit $EXIT_STATUS
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,225 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
# pylint: disable=locally-disabled, invalid-name, attribute-defined-outside-init, too-few-public-methods
"""
Tests related to configuration of HW timestamping
"""
import errno
import ctypes
import fcntl
import socket
from lib.py import ksft_run, ksft_exit, ksft_ge, ksft_eq, KsftSkipEx
from lib.py import NetDrvEnv, EthtoolFamily, NlError
SIOCSHWTSTAMP = 0x89b0
SIOCGHWTSTAMP = 0x89b1
class hwtstamp_config(ctypes.Structure):
""" Python copy of struct hwtstamp_config """
_fields_ = [
("flags", ctypes.c_int),
("tx_type", ctypes.c_int),
("rx_filter", ctypes.c_int),
]
class ifreq(ctypes.Structure):
""" Python copy of struct ifreq """
_fields_ = [
("ifr_name", ctypes.c_char * 16),
("ifr_data", ctypes.POINTER(hwtstamp_config)),
]
def __get_hwtimestamp_support(cfg):
""" Retrieve supported configuration information """
try:
tsinfo = cfg.ethnl.tsinfo_get({'header': {'dev-name': cfg.ifname}})
except NlError as e:
if e.error == errno.EOPNOTSUPP:
raise KsftSkipEx("timestamping configuration is not supported") from e
raise
ctx = {}
tx = tsinfo.get('tx-types', {})
rx = tsinfo.get('rx-filters', {})
bits = tx.get('bits', {})
ctx['tx'] = bits.get('bit', [])
bits = rx.get('bits', {})
ctx['rx'] = bits.get('bit', [])
return ctx
def __get_hwtimestamp_config_ioctl(cfg):
""" Retrieve current TS configuration information (via ioctl) """
config = hwtstamp_config()
req = ifreq()
req.ifr_name = cfg.ifname.encode()
req.ifr_data = ctypes.pointer(config)
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
fcntl.ioctl(sock.fileno(), SIOCGHWTSTAMP, req)
sock.close()
except OSError as e:
if e.errno == errno.EOPNOTSUPP:
raise KsftSkipEx("timestamping configuration is not supported via ioctl") from e
raise
return config
def __get_hwtimestamp_config(cfg):
""" Retrieve current TS configuration information (via netLink) """
try:
tscfg = cfg.ethnl.tsconfig_get({'header': {'dev-name': cfg.ifname}})
except NlError as e:
if e.error == errno.EOPNOTSUPP:
raise KsftSkipEx("timestamping configuration is not supported via netlink") from e
raise
return tscfg
def __set_hwtimestamp_config_ioctl(cfg, ts):
""" Setup new TS configuration information (via ioctl) """
config = hwtstamp_config()
config.rx_filter = ts['rx-filters']['bits']['bit'][0]['index']
config.tx_type = ts['tx-types']['bits']['bit'][0]['index']
req = ifreq()
req.ifr_name = cfg.ifname.encode()
req.ifr_data = ctypes.pointer(config)
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
fcntl.ioctl(sock.fileno(), SIOCSHWTSTAMP, req)
sock.close()
except OSError as e:
if e.errno == errno.EOPNOTSUPP:
raise KsftSkipEx("timestamping configuration is not supported via ioctl") from e
raise
def __set_hwtimestamp_config(cfg, ts):
""" Setup new TS configuration information (via netlink) """
ts['header'] = {'dev-name': cfg.ifname}
try:
res = cfg.ethnl.tsconfig_set(ts)
except NlError as e:
if e.error == errno.EOPNOTSUPP:
raise KsftSkipEx("timestamping configuration is not supported via netlink") from e
raise
return res
def __perform_hwtstamp_tx(cfg, is_ioctl):
"""
Test TX timestamp configuration via either netlink or ioctl.
The driver should apply provided config and report back proper state.
"""
orig_tscfg = __get_hwtimestamp_config(cfg)
ts = __get_hwtimestamp_support(cfg)
tx = ts['tx']
for t in tx:
res = None
tscfg = orig_tscfg
tscfg['tx-types']['bits']['bit'] = [t]
if is_ioctl:
__set_hwtimestamp_config_ioctl(cfg, tscfg)
else:
res = __set_hwtimestamp_config(cfg, tscfg)
if res is None:
res = __get_hwtimestamp_config(cfg)
resioctl = __get_hwtimestamp_config_ioctl(cfg)
ksft_eq(res['tx-types']['bits']['bit'], [t])
ksft_eq(resioctl.tx_type, t['index'])
__set_hwtimestamp_config(cfg, orig_tscfg)
def test_hwtstamp_tx_netlink(cfg):
"""
Test TX timestamp configuration setup via netlink.
The driver should apply provided config and report back proper state.
"""
__perform_hwtstamp_tx(cfg, False)
def test_hwtstamp_tx_ioctl(cfg):
"""
Test TX timestamp configuration setup via ioctl.
The driver should apply provided config and report back proper state.
"""
__perform_hwtstamp_tx(cfg, True)
def __perform_hwtstamp_rx(cfg, is_ioctl):
"""
Test RX timestamp configuration.
The filter configuration is taken from the list of supported filters.
The driver should apply the config without error and report back proper state.
Some extension of the timestamping scope is allowed for PTP filters.
"""
orig_tscfg = __get_hwtimestamp_config(cfg)
ts = __get_hwtimestamp_support(cfg)
rx = ts['rx']
for r in rx:
res = None
tscfg = orig_tscfg
tscfg['rx-filters']['bits']['bit'] = [r]
if is_ioctl:
__set_hwtimestamp_config_ioctl(cfg, tscfg)
else:
res = __set_hwtimestamp_config(cfg, tscfg)
if res is None:
res = __get_hwtimestamp_config(cfg)
resioctl = __get_hwtimestamp_config_ioctl(cfg)
ksft_eq(resioctl.rx_filter, res['rx-filters']['bits']['bit'][0]['index'])
if r['index'] == 0 or r['index'] == 1:
ksft_eq(res['rx-filters']['bits']['bit'][0]['index'], r['index'])
else:
# the driver can fallback to some value which has higher coverage for timestamping
ksft_ge(res['rx-filters']['bits']['bit'][0]['index'], r['index'])
__set_hwtimestamp_config(cfg, orig_tscfg)
def test_hwtstamp_rx_netlink(cfg):
"""
Test RX timestamp configuration via netlink.
The filter configuration is taken from the list of supported filters.
The driver should apply the config without error and report back proper state.
Some extension of the timestamping scope is allowed for PTP filters.
"""
__perform_hwtstamp_rx(cfg, False)
def test_hwtstamp_rx_ioctl(cfg):
"""
Test RX timestamp configuration via ioctl.
The filter configuration is taken from the list of supported filters.
The driver should apply the config without error and report back proper state.
Some extension of the timestamping scope is allowed for PTP filters.
"""
__perform_hwtstamp_rx(cfg, True)
def main() -> None:
""" Ksft boiler plate main """
with NetDrvEnv(__file__, nsim_test=False) as cfg:
cfg.ethnl = EthtoolFamily()
ksft_run([test_hwtstamp_tx_ioctl, test_hwtstamp_tx_netlink,
test_hwtstamp_rx_ioctl, test_hwtstamp_rx_netlink],
args=(cfg,))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""Test devmem TCP with netkit."""
import os
from devmem_lib import setup_test, run_rx, run_tx, run_tx_chunks, run_rx_hds
from lib.py import ksft_run, ksft_exit, ksft_disruptive
from lib.py import NetDrvContEnv
@ksft_disruptive
def check_nk_rx(cfg) -> None:
"""Run the devmem RX test through netkit."""
run_rx(cfg)
@ksft_disruptive
def check_nk_tx(cfg) -> None:
"""Run the devmem TX test through netkit."""
run_tx(cfg)
@ksft_disruptive
def check_nk_tx_chunks(cfg) -> None:
"""Run the devmem TX chunking test through netkit."""
run_tx_chunks(cfg)
def check_nk_rx_hds(cfg) -> None:
"""Run the HDS test through netkit."""
run_rx_hds(cfg)
def main() -> None:
"""Run the netkit devmem test cases."""
with NetDrvContEnv(__file__, rxqueues=2, primary_rx_redirect=True) as cfg:
setup_test(cfg,
os.path.join(os.path.dirname(os.path.abspath(__file__)),
"ncdevmem"))
ksft_run([check_nk_rx, check_nk_tx, check_nk_tx_chunks,
check_nk_rx_hds], args=(cfg,))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: GPL-2.0
#include <linux/bpf.h>
#include <linux/pkt_cls.h>
#include <linux/if_ether.h>
#include <linux/ipv6.h>
#include <linux/in6.h>
#include <bpf/bpf_endian.h>
#include <bpf/bpf_helpers.h>
#define TC_ACT_OK 0
#define ETH_P_IPV6 0x86DD
#define ctx_ptr(field) ((void *)(long)(field))
#define v6_p64_equal(a, b) (a.s6_addr32[0] == b.s6_addr32[0] && \
a.s6_addr32[1] == b.s6_addr32[1])
volatile __u32 netkit_ifindex;
volatile __u8 ipv6_prefix[16];
SEC("tc/ingress")
int tc_redirect_peer(struct __sk_buff *skb)
{
void *data_end = ctx_ptr(skb->data_end);
void *data = ctx_ptr(skb->data);
struct in6_addr *peer_addr;
struct ipv6hdr *ip6h;
struct ethhdr *eth;
peer_addr = (struct in6_addr *)ipv6_prefix;
if (skb->protocol != bpf_htons(ETH_P_IPV6))
return TC_ACT_OK;
eth = data;
if ((void *)(eth + 1) > data_end)
return TC_ACT_OK;
ip6h = data + sizeof(struct ethhdr);
if ((void *)(ip6h + 1) > data_end)
return TC_ACT_OK;
if (!v6_p64_equal(ip6h->daddr, (*peer_addr)))
return TC_ACT_OK;
return bpf_redirect_peer(netkit_ifindex, 0);
}
char __license[] SEC("license") = "GPL";
@@ -0,0 +1,29 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""
Test exercising NetDrvContEnv() itself, a NetDrvContEnv() selftest.
"""
from lib.py import ksft_run, ksft_exit
from lib.py import NetDrvContEnv
from lib.py import cmd
def test_ping(cfg) -> None:
""" Run ping between the container and the remote system. """
cfg.require_ipver("6")
cmd(f"ping -c 1 -W5 {cfg.nk_guest_ipv6}", host=cfg.remote)
cmd(f"ping -c 1 -W5 {cfg.remote_addr_v['6']}", ns=cfg.netns)
def main() -> None:
""" Ksft boiler plate main """
with NetDrvContEnv(__file__) as cfg:
ksft_run([test_ping], args=(cfg,))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: GPL-2.0
#include <linux/bpf.h>
#include <linux/pkt_cls.h>
#include <linux/if_ether.h>
#include <linux/in.h>
#include <linux/ipv6.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#define ctx_ptr(field) ((void *)(long)(field))
volatile __u32 phys_ifindex;
SEC("tc/ingress")
int nk_primary_rx_redirect(struct __sk_buff *skb)
{
void *data_end = ctx_ptr(skb->data_end);
void *data = ctx_ptr(skb->data);
struct ethhdr *eth;
struct ipv6hdr *ip6h;
eth = data;
if ((void *)(eth + 1) > data_end)
return TC_ACT_OK;
if (eth->h_proto != bpf_htons(ETH_P_IPV6))
return TC_ACT_OK;
ip6h = data + sizeof(struct ethhdr);
if ((void *)(ip6h + 1) > data_end)
return TC_ACT_OK;
if (ip6h->nexthdr == IPPROTO_ICMPV6)
return TC_ACT_OK;
return bpf_redirect_neigh(phys_ifindex, NULL, 0, 0);
}
char __license[] SEC("license") = "GPL";
@@ -0,0 +1,465 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
import re
import time
import threading
from os import path
from lib.py import (
ksft_run,
ksft_exit,
ksft_eq,
ksft_in,
ksft_not_in,
ksft_raises,
)
from lib.py import (
NetDrvContEnv,
NetNSEnter,
EthtoolFamily,
NetdevFamily,
RtnlFamily,
)
from lib.py import (
Netlink,
bkg,
cmd,
defer,
ethtool,
ip,
rand_port,
wait_port_listen,
)
from lib.py import KsftSkipEx, CmdExitFailure
# iou-zcrx exits with 42 from setup_zcrx() when the NIC does not advertise
# QCFG_RX_PAGE_SIZE (or otherwise rejects the requested rx_buf_len).
SKIP_CODE = 42
def _restore_hugepages(count):
with open("/proc/sys/vm/nr_hugepages", "w", encoding="utf-8") as f:
f.write(str(count))
def _mp_clear_wait(cfg, src_queue):
"""Wait for the io_uring memory provider to clear from the leased
physical queue; io_uring tears it down asynchronously after the
process holding the ifq exits."""
netdevnl = NetdevFamily()
deadline = time.time() + 5
while time.time() < deadline:
queue_info = netdevnl.queue_get(
{"ifindex": cfg.ifindex, "id": src_queue, "type": "rx"}
)
if "io-uring" not in queue_info:
return
time.sleep(0.1)
raise TimeoutError("Timed out waiting for memory provider to clear")
def _create_netkit_pair(cfg, rxqueues=2):
if cfg.nk_host_ifname:
cmd(f"ip link del dev {cfg.nk_host_ifname}", fail=False)
cfg.nk_host_ifname = None
cfg.nk_guest_ifname = None
cfg.detach_bpf()
all_links = ip("-d link show", json=True)
old_idxs = {
link["ifindex"]
for link in all_links
if link.get("linkinfo", {}).get("info_kind") == "netkit"
}
rtnl = RtnlFamily()
rtnl.newlink(
{
"linkinfo": {
"kind": "netkit",
"data": {
"mode": "l2",
"policy": "forward",
"peer-policy": "forward",
},
},
"num-rx-queues": rxqueues,
},
flags=[Netlink.NLM_F_CREATE, Netlink.NLM_F_EXCL],
)
all_links = ip("-d link show", json=True)
nk_links = [
link
for link in all_links
if link.get("linkinfo", {}).get("info_kind") == "netkit"
and link["ifindex"] not in old_idxs
]
if len(nk_links) != 2:
raise KsftSkipEx("Failed to create netkit pair")
nk_links.sort(key=lambda x: x["ifindex"])
cfg.nk_host_ifname = nk_links[1]["ifname"]
cfg.nk_guest_ifname = nk_links[0]["ifname"]
cfg.nk_host_ifindex = nk_links[1]["ifindex"]
cfg.nk_guest_ifindex = nk_links[0]["ifindex"]
ip(f"link set dev {cfg.nk_guest_ifname} netns {cfg.netns.name}")
ip(f"link set dev {cfg.nk_host_ifname} up")
ip(f"-6 addr add fe80::1/64 dev {cfg.nk_host_ifname} nodad")
ip(
f"-6 route add {cfg.nk_guest_ipv6}/128 via fe80::2 "
f"dev {cfg.nk_host_ifname}"
)
ip(f"link set dev {cfg.nk_guest_ifname} up", ns=cfg.netns)
ip(f"-6 addr add fe80::2/64 dev {cfg.nk_guest_ifname}", ns=cfg.netns)
ip(
f"-6 addr add {cfg.nk_guest_ipv6}/64 dev {cfg.nk_guest_ifname} nodad",
ns=cfg.netns,
)
ip(
f"-6 route add default via fe80::1 dev {cfg.nk_guest_ifname}",
ns=cfg.netns,
)
cfg.attach_bpf()
def _setup_lease(cfg, rxqueues=2):
_create_netkit_pair(cfg, rxqueues=rxqueues)
ethnl = EthtoolFamily()
channels = ethnl.channels_get({"header": {"dev-index": cfg.ifindex}})[
"combined-count"
]
if channels < 2:
raise KsftSkipEx(
"Test requires NETIF with at least 2 combined channels"
)
src_queue = channels - 1
with NetNSEnter(str(cfg.netns)):
netdevnl = NetdevFamily()
bind_result = netdevnl.queue_create(
{
"ifindex": cfg.nk_guest_ifindex,
"type": "rx",
"lease": {
"ifindex": cfg.ifindex,
"queue": {"id": src_queue, "type": "rx"},
"netns-id": 0,
},
}
)
return src_queue, bind_result["id"]
def _teardown_netkit(cfg):
if cfg.nk_host_ifname:
cmd(f"ip link del dev {cfg.nk_host_ifname}", fail=False)
cfg.nk_host_ifname = None
cfg.nk_guest_ifname = None
def set_flow_rule(cfg, src_queue):
output = ethtool(
f"-N {cfg.ifname} flow-type tcp6 dst-port {cfg.port} action {src_queue}"
).stdout
values = re.search(r"ID (\d+)", output).group(1)
return int(values)
def test_iou_zcrx(cfg) -> None:
cfg.require_ipver("6")
src_queue, nk_queue = _setup_lease(cfg)
defer(_teardown_netkit, cfg)
ethnl = EthtoolFamily()
rings = ethnl.rings_get({"header": {"dev-index": cfg.ifindex}})
rx_rings = rings["rx"]
hds_thresh = rings.get("hds-thresh", 0)
ethnl.rings_set(
{
"header": {"dev-index": cfg.ifindex},
"tcp-data-split": "enabled",
"hds-thresh": 0,
"rx": 64,
}
)
defer(
ethnl.rings_set,
{
"header": {"dev-index": cfg.ifindex},
"tcp-data-split": "unknown",
"hds-thresh": hds_thresh,
"rx": rx_rings,
},
)
ethtool(f"-X {cfg.ifname} equal {src_queue}")
defer(ethtool, f"-X {cfg.ifname} default")
flow_rule_id = set_flow_rule(cfg, src_queue)
defer(ethtool, f"-N {cfg.ifname} delete {flow_rule_id}")
rx_cmd = (
f"{cfg.bin_local} -s -p {cfg.port} "
f"-i {cfg.nk_guest_ifname} -q {nk_queue}"
)
tx_cmd = f"{cfg.bin_remote} -c -h {cfg.nk_guest_ipv6} -p {cfg.port} -l 12840"
with bkg(rx_cmd, exit_wait=True, ns=cfg.netns):
wait_port_listen(cfg.port, proto="tcp", ns=cfg.netns)
cmd(tx_cmd, host=cfg.remote)
def test_iou_zcrx_large_buf(cfg) -> None:
"""iou-zcrx with rx_buf_len > page size, going through a netkit-leased
queue. Exercises the queue rx-buf-len path via netif_mp_open_rxq()'s
lease redirect: the netkit ifindex is opaque to io_uring, but
rx_page_size is honoured by the *physical* qops because the lease
pointer rewrites the request from netkit onto the leased physical
rxq before supported_params/validate_qcfg are consulted.
"""
cfg.require_ipver("6")
src_queue, nk_queue = _setup_lease(cfg)
defer(_teardown_netkit, cfg)
ethnl = EthtoolFamily()
with open("/proc/sys/vm/nr_hugepages", "r+", encoding="utf-8") as f:
nr_hugepages = int(f.read().strip())
if nr_hugepages < 64:
f.seek(0)
f.write("64")
defer(_restore_hugepages, nr_hugepages)
rings = ethnl.rings_get({"header": {"dev-index": cfg.ifindex}})
rx_rings = rings["rx"]
hds_thresh = rings.get("hds-thresh", 0)
ethnl.rings_set(
{
"header": {"dev-index": cfg.ifindex},
"tcp-data-split": "enabled",
"hds-thresh": 0,
"rx": 64,
}
)
defer(
ethnl.rings_set,
{
"header": {"dev-index": cfg.ifindex},
"tcp-data-split": "unknown",
"hds-thresh": hds_thresh,
"rx": rx_rings,
},
)
ethtool(f"-X {cfg.ifname} equal {src_queue}")
defer(ethtool, f"-X {cfg.ifname} default")
flow_rule_id = set_flow_rule(cfg, src_queue)
defer(ethtool, f"-N {cfg.ifname} delete {flow_rule_id}")
# -x 2 asks iou-zcrx for rx_buf_len = 2 * page_size (8 KiB on x86_64),
# backed by a 2 MiB hugepage area so the chunks are physically
# contiguous, which is what zcrx requires for non-default rx_buf_len.
rx_cmd = (
f"{cfg.bin_local} -s -p {cfg.port} "
f"-i {cfg.nk_guest_ifname} -q {nk_queue} -x 2"
)
tx_cmd = f"{cfg.bin_remote} -c -h {cfg.nk_guest_ipv6} -p {cfg.port} -l 12840"
# Probe via -d (dry run): exits with SKIP_CODE if the leased physical
# qops doesn't advertise QCFG_RX_PAGE_SIZE (e.g. older bnxt FW/HW).
probe = cmd(rx_cmd + " -d", fail=False, ns=cfg.netns)
if probe.ret == SKIP_CODE:
msg = probe.stdout.strip() or "rx_buf_len not supported by leased NIC"
raise KsftSkipEx(msg)
# A successful dry run still registered the zcrx ifq on the leased
# physical queue; wait for its async teardown before the real server
# binds the same queue.
_mp_clear_wait(cfg, src_queue)
with bkg(rx_cmd, exit_wait=True, ns=cfg.netns):
wait_port_listen(cfg.port, proto="tcp", ns=cfg.netns)
cmd(tx_cmd, host=cfg.remote)
def test_attrs(cfg) -> None:
cfg.require_ipver("6")
src_queue, nk_queue = _setup_lease(cfg)
defer(_teardown_netkit, cfg)
netdevnl = NetdevFamily()
queue_info = netdevnl.queue_get(
{"ifindex": cfg.ifindex, "id": src_queue, "type": "rx"}
)
ksft_eq(queue_info["id"], src_queue)
ksft_eq(queue_info["type"], "rx")
ksft_eq(queue_info["ifindex"], cfg.ifindex)
ksft_in("lease", queue_info)
lease = queue_info["lease"]
ksft_eq(lease["ifindex"], cfg.nk_guest_ifindex)
ksft_eq(lease["queue"]["id"], nk_queue)
ksft_eq(lease["queue"]["type"], "rx")
ksft_in("netns-id", lease)
def test_attach_xdp_with_mp(cfg) -> None:
cfg.require_ipver("6")
src_queue, nk_queue = _setup_lease(cfg)
defer(_teardown_netkit, cfg)
ethnl = EthtoolFamily()
rings = ethnl.rings_get({"header": {"dev-index": cfg.ifindex}})
rx_rings = rings["rx"]
hds_thresh = rings.get("hds-thresh", 0)
ethnl.rings_set(
{
"header": {"dev-index": cfg.ifindex},
"tcp-data-split": "enabled",
"hds-thresh": 0,
"rx": 64,
}
)
defer(
ethnl.rings_set,
{
"header": {"dev-index": cfg.ifindex},
"tcp-data-split": "unknown",
"hds-thresh": hds_thresh,
"rx": rx_rings,
},
)
ethtool(f"-X {cfg.ifname} equal {src_queue}")
defer(ethtool, f"-X {cfg.ifname} default")
netdevnl = NetdevFamily()
rx_cmd = (
f"{cfg.bin_local} -s -p {cfg.port} "
f"-i {cfg.nk_guest_ifname} -q {nk_queue}"
)
with bkg(rx_cmd, ns=cfg.netns):
wait_port_listen(cfg.port, proto="tcp", ns=cfg.netns)
time.sleep(0.1)
queue_info = netdevnl.queue_get(
{"ifindex": cfg.ifindex, "id": src_queue, "type": "rx"}
)
ksft_in("io-uring", queue_info)
prog = cfg.net_lib_dir / "xdp_dummy.bpf.o"
with ksft_raises(CmdExitFailure):
ip(f"link set dev {cfg.ifname} xdp obj {prog} sec xdp.frags")
time.sleep(0.1)
queue_info = netdevnl.queue_get(
{"ifindex": cfg.ifindex, "id": src_queue, "type": "rx"}
)
ksft_not_in("io-uring", queue_info)
def test_destroy(cfg) -> None:
cfg.require_ipver("6")
src_queue, nk_queue = _setup_lease(cfg)
defer(_teardown_netkit, cfg)
ethnl = EthtoolFamily()
rings = ethnl.rings_get({"header": {"dev-index": cfg.ifindex}})
rx_rings = rings["rx"]
hds_thresh = rings.get("hds-thresh", 0)
ethnl.rings_set(
{
"header": {"dev-index": cfg.ifindex},
"tcp-data-split": "enabled",
"hds-thresh": 0,
"rx": 64,
}
)
defer(
ethnl.rings_set,
{
"header": {"dev-index": cfg.ifindex},
"tcp-data-split": "unknown",
"hds-thresh": hds_thresh,
"rx": rx_rings,
},
)
ethtool(f"-X {cfg.ifname} equal {src_queue}")
defer(ethtool, f"-X {cfg.ifname} default")
rx_cmd = (
f"{cfg.bin_local} -s -p {cfg.port} "
f"-i {cfg.nk_guest_ifname} -q {nk_queue}"
)
rx_proc = cmd(rx_cmd, background=True, ns=cfg.netns)
wait_port_listen(cfg.port, proto="tcp", ns=cfg.netns)
netdevnl = NetdevFamily()
queue_info = netdevnl.queue_get(
{"ifindex": cfg.ifindex, "id": src_queue, "type": "rx"}
)
ksft_in("io-uring", queue_info)
# ip link del will wait for all refs to drop first, but iou-zcrx is holding
# onto a ref. Terminate iou-zcrx async via a thread after a delay.
kill_timer = threading.Timer(1, rx_proc.proc.terminate)
kill_timer.start()
ip(f"link del dev {cfg.nk_host_ifname}")
kill_timer.join()
cfg.nk_host_ifname = None
cfg.nk_guest_ifname = None
queue_info = netdevnl.queue_get(
{"ifindex": cfg.ifindex, "id": src_queue, "type": "rx"}
)
ksft_not_in("io-uring", queue_info)
flow_rule_id = set_flow_rule(cfg, src_queue)
defer(ethtool, f"-N {cfg.ifname} delete {flow_rule_id}")
rx_cmd = f"{cfg.bin_local} -s -p {cfg.port} -i {cfg.ifname} -q {src_queue}"
tx_cmd = f"{cfg.bin_remote} -c -h {cfg.addr_v['6']} -p {cfg.port} -l 12840"
with bkg(rx_cmd, exit_wait=True):
wait_port_listen(cfg.port, proto="tcp")
cmd(tx_cmd, host=cfg.remote)
# Short delay since iou cleanup is async and takes a bit of time.
time.sleep(0.1)
queue_info = netdevnl.queue_get(
{"ifindex": cfg.ifindex, "id": src_queue, "type": "rx"}
)
ksft_not_in("io-uring", queue_info)
def main() -> None:
with NetDrvContEnv(__file__, rxqueues=2) as cfg:
cfg.bin_local = path.abspath(
path.dirname(__file__) + "/../../../drivers/net/hw/iou-zcrx"
)
cfg.bin_remote = cfg.remote.deploy(cfg.bin_local)
cfg.port = rand_port()
ksft_run(
[
test_iou_zcrx,
test_iou_zcrx_large_buf,
test_attrs,
test_attach_xdp_with_mp,
test_destroy,
],
args=(cfg,),
)
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,165 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""Test ethtool NFC (ntuple) flow steering rules."""
import random
from enum import Enum, auto
from lib.py import ksft_run, ksft_exit
from lib.py import ksft_eq, ksft_ge
from lib.py import ksft_variants, KsftNamedVariant
from lib.py import EthtoolFamily, NetDrvEpEnv, NetdevFamily
from lib.py import KsftSkipEx
from lib.py import cmd, ethtool, defer, rand_ports, bkg, wait_port_listen
class NtupleField(Enum):
SRC_IP = auto()
DST_IP = auto()
SRC_PORT = auto()
DST_PORT = auto()
def _require_ntuple(cfg):
features = ethtool(f"-k {cfg.ifname}", json=True)[0]
if not features["ntuple-filters"]["active"]:
if features["ntuple-filters"]["fixed"]:
raise KsftSkipEx("Device does not support ntuple-filters")
ethtool(f"-K {cfg.ifname} ntuple-filters on")
defer(ethtool, f"-K {cfg.ifname} ntuple-filters off")
def _get_rx_cnts(cfg, prev=None):
"""Get Rx packet counts for all queues, as a simple list of integers
if @prev is specified the prev counts will be subtracted"""
cfg.wait_hw_stats_settle()
data = cfg.netdevnl.qstats_get({"ifindex": cfg.ifindex, "scope": ["queue"]}, dump=True)
data = [x for x in data if x['queue-type'] == "rx"]
max_q = max([x["queue-id"] for x in data])
queue_stats = [0] * (max_q + 1)
for q in data:
queue_stats[q["queue-id"]] = q["rx-packets"]
if prev and q["queue-id"] < len(prev):
queue_stats[q["queue-id"]] -= prev[q["queue-id"]]
return queue_stats
def _ntuple_rule_add(cfg, flow_spec):
"""Install an NFC rule via ethtool."""
output = ethtool(f"-N {cfg.ifname} {flow_spec}").stdout
rule_id = int(output.split()[-1])
defer(ethtool, f"-N {cfg.ifname} delete {rule_id}")
def _setup_isolated_queue(cfg):
"""Default all traffic to queue 0, and pick a random queue to
steer NFC traffic to."""
channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
ch_max = channels['combined-max']
qcnt = channels['combined-count']
if ch_max < 2:
raise KsftSkipEx(f"Need at least 2 combined channels, max is {ch_max}")
desired_queues = min(ch_max, 4)
if qcnt >= desired_queues:
desired_queues = qcnt
else:
ethtool(f"-L {cfg.ifname} combined {desired_queues}")
defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
ethtool(f"-X {cfg.ifname} equal 1")
defer(ethtool, f"-X {cfg.ifname} default")
return random.randint(1, desired_queues - 1)
def _send_traffic(cfg, ipver, proto, dst_port, src_port, pkt_cnt=40):
"""Generate traffic with the desired flow signature."""
cfg.require_cmd("socat", remote=True)
socat_proto = proto.upper()
dst_addr = f"[{cfg.addr_v['6']}]" if ipver == '6' else cfg.addr_v['4']
extra_opts = ",nodelay" if proto == "tcp" else ",shut-null"
listen_cmd = (f"socat -{ipver} -t 2 -u "
f"{socat_proto}-LISTEN:{dst_port},reuseport /dev/null")
with bkg(listen_cmd, exit_wait=True):
wait_port_listen(dst_port, proto=proto)
send_cmd = f"""
bash -c 'for i in $(seq {pkt_cnt}); do echo msg; sleep 0.02; done' |
socat -{ipver} -u - \
{socat_proto}:{dst_addr}:{dst_port},sourceport={src_port},reuseaddr{extra_opts}
"""
cmd(send_cmd, shell=True, host=cfg.remote)
def _add_ntuple_rule_and_send_traffic(cfg, ipver, proto, fields, test_queue):
ports = rand_ports(2)
src_port = ports[0]
dst_port = ports[1]
flow_parts = [f"flow-type {proto}{ipver}"]
for field in fields:
if field == NtupleField.SRC_IP:
flow_parts.append(f"src-ip {cfg.remote_addr_v[ipver]}")
elif field == NtupleField.DST_IP:
flow_parts.append(f"dst-ip {cfg.addr_v[ipver]}")
elif field == NtupleField.SRC_PORT:
flow_parts.append(f"src-port {src_port}")
elif field == NtupleField.DST_PORT:
flow_parts.append(f"dst-port {dst_port}")
flow_parts.append(f"action {test_queue}")
_ntuple_rule_add(cfg, " ".join(flow_parts))
_send_traffic(cfg, ipver, proto, dst_port=dst_port, src_port=src_port)
def _ntuple_variants():
for ipver in ["4", "6"]:
for proto in ["tcp", "udp"]:
for fields in [[NtupleField.SRC_IP],
[NtupleField.DST_IP],
[NtupleField.SRC_PORT],
[NtupleField.DST_PORT],
[NtupleField.SRC_IP, NtupleField.DST_IP],
[NtupleField.SRC_IP, NtupleField.DST_IP,
NtupleField.SRC_PORT, NtupleField.DST_PORT]]:
name = ".".join(f.name.lower() for f in fields)
yield KsftNamedVariant(f"{proto}{ipver}.{name}",
ipver, proto, fields)
@ksft_variants(_ntuple_variants())
def queue(cfg, ipver, proto, fields):
"""Test that an NFC rule steers traffic to the correct queue."""
cfg.require_ipver(ipver)
_require_ntuple(cfg)
test_queue = _setup_isolated_queue(cfg)
cnts = _get_rx_cnts(cfg)
_add_ntuple_rule_and_send_traffic(cfg, ipver, proto, fields, test_queue)
cnts = _get_rx_cnts(cfg, prev=cnts)
ksft_ge(cnts[test_queue], 40, f"Traffic on test queue {test_queue}: {cnts}")
sum_idle = sum(cnts) - cnts[0] - cnts[test_queue]
ksft_eq(sum_idle, 0, f"Traffic on idle queues: {cnts}")
def main() -> None:
"""Ksft boilerplate main."""
with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
cfg.ethnl = EthtoolFamily()
cfg.netdevnl = NetdevFamily()
ksft_run([queue], args=(cfg,))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""
Test driver resilience vs page pool allocation failures.
"""
import errno
import time
import math
import os
from lib.py import ksft_run, ksft_exit, ksft_pr
from lib.py import KsftSkipEx, KsftFailEx
from lib.py import NetdevFamily, NlError
from lib.py import NetDrvEpEnv
from lib.py import cmd, tool, GenerateTraffic
def _write_fail_config(config):
for key, value in config.items():
path = "/sys/kernel/debug/fail_function/"
with open(path + key, "w", encoding='ascii') as fp:
fp.write(str(value) + "\n")
def _enable_pp_allocation_fail():
if not os.path.exists("/sys/kernel/debug/fail_function"):
raise KsftSkipEx("Kernel built without function error injection (or DebugFS)")
if not os.path.exists("/sys/kernel/debug/fail_function/page_pool_alloc_netmems"):
_write_fail_config({"inject": "page_pool_alloc_netmems"})
_write_fail_config({
"verbose": 0,
"interval": 511,
"probability": 100,
"times": -1,
})
def _disable_pp_allocation_fail():
if not os.path.exists("/sys/kernel/debug/fail_function"):
return
if os.path.exists("/sys/kernel/debug/fail_function/page_pool_alloc_netmems"):
_write_fail_config({"inject": ""})
_write_fail_config({
"probability": 0,
"times": 0,
})
def test_pp_alloc(cfg, netdevnl):
"""
Configure page pool allocation fail injection while traffic is running.
"""
def get_stats():
return netdevnl.qstats_get({"ifindex": cfg.ifindex}, dump=True)[0]
def check_traffic_flowing():
stat1 = get_stats()
time.sleep(1)
stat2 = get_stats()
if stat2['rx-packets'] - stat1['rx-packets'] < 4000:
raise KsftFailEx("Traffic seems low:", stat2['rx-packets'] - stat1['rx-packets'])
try:
stats = get_stats()
except NlError as e:
if e.nl_msg.error == -errno.EOPNOTSUPP:
stats = {}
else:
raise
if 'rx-alloc-fail' not in stats:
raise KsftSkipEx("Driver does not report 'rx-alloc-fail' via qstats")
set_g = False
traffic = None
try:
traffic = GenerateTraffic(cfg)
check_traffic_flowing()
_enable_pp_allocation_fail()
s1 = get_stats()
time.sleep(3)
s2 = get_stats()
seen_fails = s2['rx-alloc-fail'] - s1['rx-alloc-fail']
if seen_fails < 1:
raise KsftSkipEx("Allocation failures not increasing")
pkts = s2['rx-packets'] - s1['rx-packets']
# Expecting one failure per 512 buffers, 3.1x safety margin
want_fails = math.floor(pkts / 512 / 3.1)
if seen_fails < want_fails:
raise KsftSkipEx("Allocation increasing too slowly", seen_fails,
"packets:", pkts)
ksft_pr(f"Seen: pkts:{pkts} fails:{seen_fails} (pass thrs:{want_fails})")
# Basic failures are fine, try to wobble some settings to catch extra failures
check_traffic_flowing()
g = tool("ethtool", "-g " + cfg.ifname, json=True)[0]
if 'rx' in g and g["rx"] * 2 <= g["rx-max"]:
new_g = g['rx'] * 2
elif 'rx' in g:
new_g = g['rx'] // 2
else:
new_g = None
if new_g:
set_g = cmd(f"ethtool -G {cfg.ifname} rx {new_g}", fail=False).ret == 0
if set_g:
ksft_pr("ethtool -G change retval: success")
else:
ksft_pr("ethtool -G change retval: did not succeed", new_g)
else:
ksft_pr("ethtool -G change retval: did not try")
time.sleep(0.1)
check_traffic_flowing()
finally:
_disable_pp_allocation_fail()
if traffic:
traffic.stop()
time.sleep(0.1)
if set_g:
cmd(f"ethtool -G {cfg.ifname} rx {g['rx']}")
def main() -> None:
""" Ksft boiler plate main """
netdevnl = NetdevFamily()
with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
ksft_run([test_pp_alloc], args=(cfg, netdevnl, ))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,476 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""
API level tests for RSS (mostly Netlink vs IOCTL).
"""
import errno
import glob
import random
from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_is, ksft_ne, ksft_raises
from lib.py import KsftSkipEx, KsftFailEx
from lib.py import defer, ethtool, CmdExitFailure
from lib.py import EthtoolFamily, NlError
from lib.py import NetDrvEnv
def _require_2qs(cfg):
qcnt = len(glob.glob(f"/sys/class/net/{cfg.ifname}/queues/rx-*"))
if qcnt < 2:
raise KsftSkipEx(f"Local has only {qcnt} queues")
return qcnt
def _ethtool_create(cfg, act, opts):
output = ethtool(f"{act} {cfg.ifname} {opts}").stdout
# Output will be something like: "New RSS context is 1" or
# "Added rule with ID 7", we want the integer from the end
return int(output.split()[-1])
def _ethtool_get_cfg(cfg, fl_type, to_nl=False):
descr = ethtool(f"-n {cfg.ifname} rx-flow-hash {fl_type}").stdout
if to_nl:
converter = {
"IP SA": "ip-src",
"IP DA": "ip-dst",
"L4 bytes 0 & 1 [TCP/UDP src port]": "l4-b-0-1",
"L4 bytes 2 & 3 [TCP/UDP dst port]": "l4-b-2-3",
}
ret = set()
else:
converter = {
"IP SA": "s",
"IP DA": "d",
"L3 proto": "t",
"L4 bytes 0 & 1 [TCP/UDP src port]": "f",
"L4 bytes 2 & 3 [TCP/UDP dst port]": "n",
}
ret = ""
for line in descr.split("\n")[1:-2]:
# if this raises we probably need to add more keys to converter above
if to_nl:
ret.add(converter[line])
else:
ret += converter[line]
return ret
def test_rxfh_nl_set_fail(cfg):
"""
Test error path of Netlink SET.
"""
_require_2qs(cfg)
ethnl = EthtoolFamily()
ethnl.ntf_subscribe("monitor")
with ksft_raises(NlError):
ethnl.rss_set({"header": {"dev-name": "lo"},
"indir": None})
with ksft_raises(NlError):
ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
"indir": [100000]})
ntf = next(ethnl.poll_ntf(duration=0.2), None)
ksft_is(ntf, None)
def test_rxfh_nl_set_indir(cfg):
"""
Test setting indirection table via Netlink.
"""
qcnt = _require_2qs(cfg)
# Test some SETs with a value
reset = defer(cfg.ethnl.rss_set,
{"header": {"dev-index": cfg.ifindex}, "indir": None})
cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
"indir": [1]})
rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
ksft_eq(set(rss.get("indir", [-1])), {1})
cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
"indir": [0, 1]})
rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
ksft_eq(set(rss.get("indir", [-1])), {0, 1})
# Make sure we can't set the queue count below max queue used
with ksft_raises(CmdExitFailure):
ethtool(f"-L {cfg.ifname} combined 0 rx 1")
with ksft_raises(CmdExitFailure):
ethtool(f"-L {cfg.ifname} combined 1 rx 0")
# Test reset back to default
reset.exec()
rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
ksft_eq(set(rss.get("indir", [-1])), set(range(qcnt)))
def test_rxfh_nl_set_indir_ctx(cfg):
"""
Test setting indirection table for a custom context via Netlink.
"""
_require_2qs(cfg)
# Get setting for ctx 0, we'll make sure they don't get clobbered
dflt = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
# Create context
ctx_id = _ethtool_create(cfg, "-X", "context new")
defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
"context": ctx_id, "indir": [1]})
rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex},
"context": ctx_id})
ksft_eq(set(rss.get("indir", [-1])), {1})
ctx0 = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
ksft_eq(ctx0, dflt)
cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
"context": ctx_id, "indir": [0, 1]})
rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex},
"context": ctx_id})
ksft_eq(set(rss.get("indir", [-1])), {0, 1})
ctx0 = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
ksft_eq(ctx0, dflt)
# Make sure we can't set the queue count below max queue used
with ksft_raises(CmdExitFailure):
ethtool(f"-L {cfg.ifname} combined 0 rx 1")
with ksft_raises(CmdExitFailure):
ethtool(f"-L {cfg.ifname} combined 1 rx 0")
def test_rxfh_indir_ntf(cfg):
"""
Check that Netlink notifications are generated when RSS indirection
table was modified.
"""
_require_2qs(cfg)
ethnl = EthtoolFamily()
ethnl.ntf_subscribe("monitor")
ethtool(f"--disable-netlink -X {cfg.ifname} weight 0 1")
reset = defer(ethtool, f"-X {cfg.ifname} default")
ntf = next(ethnl.poll_ntf(duration=0.2), None)
if ntf is None:
raise KsftFailEx("No notification received")
ksft_eq(ntf["name"], "rss-ntf")
ksft_eq(set(ntf["msg"]["indir"]), {1})
reset.exec()
ntf = next(ethnl.poll_ntf(duration=0.2), None)
if ntf is None:
raise KsftFailEx("No notification received after reset")
ksft_eq(ntf["name"], "rss-ntf")
ksft_is(ntf["msg"].get("context"), None)
ksft_ne(set(ntf["msg"]["indir"]), {1})
def test_rxfh_indir_ctx_ntf(cfg):
"""
Check that Netlink notifications are generated when RSS indirection
table was modified on an additional RSS context.
"""
_require_2qs(cfg)
ctx_id = _ethtool_create(cfg, "-X", "context new")
defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
ethnl = EthtoolFamily()
ethnl.ntf_subscribe("monitor")
ethtool(f"--disable-netlink -X {cfg.ifname} context {ctx_id} weight 0 1")
ntf = next(ethnl.poll_ntf(duration=0.2), None)
if ntf is None:
raise KsftFailEx("No notification received")
ksft_eq(ntf["name"], "rss-ntf")
ksft_eq(ntf["msg"].get("context"), ctx_id)
ksft_eq(set(ntf["msg"]["indir"]), {1})
def test_rxfh_nl_set_key(cfg):
"""
Test setting hashing key via Netlink.
"""
dflt = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
defer(cfg.ethnl.rss_set,
{"header": {"dev-index": cfg.ifindex},
"hkey": dflt["hkey"], "indir": None})
# Empty key should error out
with ksft_raises(NlError) as cm:
cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
"hkey": None})
ksft_eq(cm.exception.nl_msg.extack['bad-attr'], '.hkey')
# Set key to random
mod = random.randbytes(len(dflt["hkey"]))
cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
"hkey": mod})
rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
ksft_eq(rss.get("hkey", [-1]), mod)
# Set key to random and indir tbl to something at once
mod = random.randbytes(len(dflt["hkey"]))
cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
"indir": [0, 1], "hkey": mod})
rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
ksft_eq(rss.get("hkey", [-1]), mod)
ksft_eq(set(rss.get("indir", [-1])), {0, 1})
def test_rxfh_fields(cfg):
"""
Test reading Rx Flow Hash over Netlink.
"""
flow_types = ["tcp4", "tcp6", "udp4", "udp6"]
ethnl = EthtoolFamily()
cfg_nl = ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
for fl_type in flow_types:
one = _ethtool_get_cfg(cfg, fl_type, to_nl=True)
ksft_eq(one, cfg_nl["flow-hash"][fl_type],
comment="Config for " + fl_type)
def test_rxfh_fields_set(cfg):
""" Test configuring Rx Flow Hash over Netlink. """
flow_types = ["tcp4", "tcp6", "udp4", "udp6"]
# Collect current settings
cfg_old = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
# symmetric hashing is config-order-sensitive make sure we leave
# symmetric mode, or make the flow-hash sym-compatible first
changes = [{"flow-hash": cfg_old["flow-hash"],},
{"input-xfrm": cfg_old.get("input-xfrm", {}),}]
if cfg_old.get("input-xfrm"):
changes = list(reversed(changes))
for old in changes:
defer(cfg.ethnl.rss_set, {"header": {"dev-index": cfg.ifindex},} | old)
# symmetric hashing prevents some of the configs below
if cfg_old.get("input-xfrm"):
cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
"input-xfrm": {}})
for fl_type in flow_types:
cur = _ethtool_get_cfg(cfg, fl_type)
if cur == "sdfn":
change_nl = {"ip-src", "ip-dst"}
change_ic = "sd"
else:
change_nl = {"l4-b-0-1", "l4-b-2-3", "ip-src", "ip-dst"}
change_ic = "sdfn"
cfg.ethnl.rss_set({
"header": {"dev-index": cfg.ifindex},
"flow-hash": {fl_type: change_nl}
})
reset = defer(ethtool, f"--disable-netlink -N {cfg.ifname} "
f"rx-flow-hash {fl_type} {cur}")
cfg_nl = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
ksft_eq(change_nl, cfg_nl["flow-hash"][fl_type],
comment=f"Config for {fl_type} over Netlink")
cfg_ic = _ethtool_get_cfg(cfg, fl_type)
ksft_eq(change_ic, cfg_ic,
comment=f"Config for {fl_type} over IOCTL")
reset.exec()
cfg_nl = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
ksft_eq(cfg_old["flow-hash"][fl_type], cfg_nl["flow-hash"][fl_type],
comment=f"Un-config for {fl_type} over Netlink")
cfg_ic = _ethtool_get_cfg(cfg, fl_type)
ksft_eq(cur, cfg_ic, comment=f"Un-config for {fl_type} over IOCTL")
# Try to set multiple at once, the defer was already installed at the start
change = {"ip-src"}
if change == cfg_old["flow-hash"]["tcp4"]:
change = {"ip-dst"}
cfg.ethnl.rss_set({
"header": {"dev-index": cfg.ifindex},
"flow-hash": {x: change for x in flow_types}
})
cfg_nl = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
for fl_type in flow_types:
ksft_eq(change, cfg_nl["flow-hash"][fl_type],
comment=f"multi-config for {fl_type} over Netlink")
def test_rxfh_fields_set_xfrm(cfg):
""" Test changing Rx Flow Hash vs xfrm_input at once. """
def set_rss(cfg, xfrm, fh):
cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
"input-xfrm": xfrm, "flow-hash": fh})
# Install the reset handler
cfg_old = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
# symmetric hashing is config-order-sensitive make sure we leave
# symmetric mode, or make the flow-hash sym-compatible first
changes = [{"flow-hash": cfg_old["flow-hash"],},
{"input-xfrm": cfg_old.get("input-xfrm", {}),}]
if cfg_old.get("input-xfrm"):
changes = list(reversed(changes))
for old in changes:
defer(cfg.ethnl.rss_set, {"header": {"dev-index": cfg.ifindex},} | old)
# Make sure we start with input-xfrm off, and tcp4 config non-sym
set_rss(cfg, {}, {})
set_rss(cfg, {}, {"tcp4": {"ip-src"}})
# Setting sym and fixing tcp4 config not expected to pass right now
with ksft_raises(NlError):
set_rss(cfg, {"sym-xor"}, {"tcp4": {"ip-src", "ip-dst"}})
# One at a time should work, hopefully
set_rss(cfg, 0, {"tcp4": {"ip-src", "ip-dst"}})
no_support = False
try:
set_rss(cfg, {"sym-xor"}, {})
except NlError:
try:
set_rss(cfg, {"sym-or-xor"}, {})
except NlError:
no_support = True
if no_support:
raise KsftSkipEx("no input-xfrm supported")
# Disabling two at once should not work either without kernel changes
with ksft_raises(NlError):
set_rss(cfg, {}, {"tcp4": {"ip-src"}})
def test_rxfh_fields_ntf(cfg):
""" Test Rx Flow Hash notifications. """
cur = _ethtool_get_cfg(cfg, "tcp4")
if cur == "sdfn":
change = {"ip-src", "ip-dst"}
else:
change = {"l4-b-0-1", "l4-b-2-3", "ip-src", "ip-dst"}
ethnl = EthtoolFamily()
ethnl.ntf_subscribe("monitor")
ethnl.rss_set({
"header": {"dev-index": cfg.ifindex},
"flow-hash": {"tcp4": change}
})
reset = defer(ethtool,
f"--disable-netlink -N {cfg.ifname} rx-flow-hash tcp4 {cur}")
ntf = next(ethnl.poll_ntf(duration=0.2), None)
if ntf is None:
raise KsftFailEx("No notification received after IOCTL change")
ksft_eq(ntf["name"], "rss-ntf")
ksft_eq(ntf["msg"]["flow-hash"]["tcp4"], change)
ksft_eq(next(ethnl.poll_ntf(duration=0.01), None), None)
reset.exec()
ntf = next(ethnl.poll_ntf(duration=0.2), None)
if ntf is None:
raise KsftFailEx("No notification received after Netlink change")
ksft_eq(ntf["name"], "rss-ntf")
ksft_ne(ntf["msg"]["flow-hash"]["tcp4"], change)
ksft_eq(next(ethnl.poll_ntf(duration=0.01), None), None)
def test_rss_ctx_add(cfg):
""" Test creating an additional RSS context via Netlink """
_require_2qs(cfg)
# Test basic creation
ctx = cfg.ethnl.rss_create_act({"header": {"dev-index": cfg.ifindex}})
d = defer(ethtool, f"-X {cfg.ifname} context {ctx.get('context')} delete")
ksft_ne(ctx.get("context", 0), 0)
ksft_ne(set(ctx.get("indir", [0])), {0},
comment="Driver should init the indirection table")
# Try requesting the ID we just got allocated
with ksft_raises(NlError) as cm:
ctx = cfg.ethnl.rss_create_act({
"header": {"dev-index": cfg.ifindex},
"context": ctx.get("context"),
})
ethtool(f"-X {cfg.ifname} context {ctx.get('context')} delete")
d.exec()
ksft_eq(cm.exception.nl_msg.error, -errno.EBUSY)
# Test creating with a specified RSS table, and context ID
ctx_id = ctx.get("context")
ctx = cfg.ethnl.rss_create_act({
"header": {"dev-index": cfg.ifindex},
"context": ctx_id,
"indir": [1],
})
ethtool(f"-X {cfg.ifname} context {ctx.get('context')} delete")
ksft_eq(ctx.get("context"), ctx_id)
ksft_eq(set(ctx.get("indir", [0])), {1})
def test_rss_ctx_ntf(cfg):
""" Test notifications for creating additional RSS contexts """
ethnl = EthtoolFamily()
ethnl.ntf_subscribe("monitor")
# Create / delete via Netlink
ctx = cfg.ethnl.rss_create_act({"header": {"dev-index": cfg.ifindex}})
cfg.ethnl.rss_delete_act({
"header": {"dev-index": cfg.ifindex},
"context": ctx["context"],
})
ntf = next(ethnl.poll_ntf(duration=0.2), None)
if ntf is None:
raise KsftFailEx("[NL] No notification after context creation")
ksft_eq(ntf["name"], "rss-create-ntf")
ksft_eq(ctx, ntf["msg"])
ntf = next(ethnl.poll_ntf(duration=0.2), None)
if ntf is None:
raise KsftFailEx("[NL] No notification after context deletion")
ksft_eq(ntf["name"], "rss-delete-ntf")
# Create / deleve via IOCTL
ctx_id = _ethtool_create(cfg, "--disable-netlink -X", "context new")
ethtool(f"--disable-netlink -X {cfg.ifname} context {ctx_id} delete")
ntf = next(ethnl.poll_ntf(duration=0.2), None)
if ntf is None:
raise KsftFailEx("[IOCTL] No notification after context creation")
ksft_eq(ntf["name"], "rss-create-ntf")
ntf = next(ethnl.poll_ntf(duration=0.2), None)
if ntf is None:
raise KsftFailEx("[IOCTL] No notification after context deletion")
ksft_eq(ntf["name"], "rss-delete-ntf")
def main() -> None:
""" Ksft boiler plate main """
with NetDrvEnv(__file__, nsim_test=False) as cfg:
cfg.ethnl = EthtoolFamily()
ksft_run(globs=globals(), case_pfx={"test_"}, args=(cfg, ))
ksft_exit()
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,313 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""
Driver-related behavior tests for RSS.
"""
from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_ge
from lib.py import ksft_variants, KsftNamedVariant, KsftSkipEx, ksft_raises
from lib.py import defer, ethtool, CmdExitFailure
from lib.py import EthtoolFamily, NlError
from lib.py import NetDrvEnv
def _is_power_of_two(n):
return n > 0 and (n & (n - 1)) == 0
def _get_rss(cfg, context=0):
return ethtool(f"-x {cfg.ifname} context {context}", json=True)[0]
def _test_rss_indir_size(cfg, qcnt, context=0):
"""Test that indirection table size is at least 4x queue count."""
ethtool(f"-L {cfg.ifname} combined {qcnt}")
rss = _get_rss(cfg, context=context)
indir = rss['rss-indirection-table']
ksft_ge(len(indir), 4 * qcnt, "Table smaller than 4x")
return len(indir)
def _maybe_create_context(cfg, create_context):
""" Either create a context and return its ID or return 0 for main ctx """
if not create_context:
return 0
try:
ctx = cfg.ethnl.rss_create_act({'header': {'dev-index': cfg.ifindex}})
ctx_id = ctx['context']
defer(cfg.ethnl.rss_delete_act,
{'header': {'dev-index': cfg.ifindex}, 'context': ctx_id})
except NlError:
raise KsftSkipEx("Device does not support additional RSS contexts")
return ctx_id
def _require_dynamic_indir_size(cfg, ch_max):
"""Skip if the device does not dynamically size its indirection table."""
ethtool(f"-X {cfg.ifname} default")
ethtool(f"-L {cfg.ifname} combined 2")
small = len(_get_rss(cfg)['rss-indirection-table'])
ethtool(f"-L {cfg.ifname} combined {ch_max}")
large = len(_get_rss(cfg)['rss-indirection-table'])
if small == large:
raise KsftSkipEx("Device does not dynamically size indirection table")
@ksft_variants([
KsftNamedVariant("main", False),
KsftNamedVariant("ctx", True),
])
def indir_size_4x(cfg, create_context):
"""
Test that the indirection table has at least 4 entries per queue.
Empirically network-heavy workloads like memcache suffer with the 33%
imbalance of a 2x indirection table size.
4x table translates to a 16% imbalance.
"""
channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
ch_max = channels.get('combined-max', 0)
qcnt = channels['combined-count']
if ch_max < 3:
raise KsftSkipEx(f"Not enough queues for the test: max={ch_max}")
defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
ethtool(f"-L {cfg.ifname} combined 3")
ctx_id = _maybe_create_context(cfg, create_context)
indir_sz = _test_rss_indir_size(cfg, 3, context=ctx_id)
# Test with max queue count (max - 1 if max is a power of two)
test_max = ch_max - 1 if _is_power_of_two(ch_max) else ch_max
if test_max > 3 and indir_sz < test_max * 4:
_test_rss_indir_size(cfg, test_max, context=ctx_id)
@ksft_variants([
KsftNamedVariant("main", False),
KsftNamedVariant("ctx", True),
])
def resize_periodic(cfg, create_context):
"""Test that a periodic indirection table survives channel changes.
Set a non-default periodic table ([3, 2, 1, 0] x N) via netlink,
reduce channels to trigger a fold, then increase to trigger an
unfold. Using a reversed pattern (instead of [0, 1, 2, 3]) ensures
the test can distinguish a correct fold from a driver that silently
resets the table to defaults. Verify the exact pattern is preserved
and the size tracks the channel count.
"""
channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
ch_max = channels.get('combined-max', 0)
qcnt = channels['combined-count']
if ch_max < 4:
raise KsftSkipEx(f"Not enough queues for the test: max={ch_max}")
defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
_require_dynamic_indir_size(cfg, ch_max)
ctx_id = _maybe_create_context(cfg, create_context)
# Set a non-default periodic pattern via netlink.
# Send only 4 entries (user_size=4) so the kernel replicates it
# to fill the device table. This allows folding down to 4 entries.
rss = _get_rss(cfg, context=ctx_id)
orig_size = len(rss['rss-indirection-table'])
pattern = [3, 2, 1, 0]
req = {'header': {'dev-index': cfg.ifindex}, 'indir': pattern}
if ctx_id:
req['context'] = ctx_id
else:
defer(ethtool, f"-X {cfg.ifname} default")
cfg.ethnl.rss_set(req)
# Shrink — should fold
ethtool(f"-L {cfg.ifname} combined 4")
rss = _get_rss(cfg, context=ctx_id)
indir = rss['rss-indirection-table']
ksft_ge(orig_size, len(indir), "Table did not shrink")
ksft_eq(indir, [3, 2, 1, 0] * (len(indir) // 4),
"Folded table has wrong pattern")
# Grow back — should unfold
ethtool(f"-L {cfg.ifname} combined {ch_max}")
rss = _get_rss(cfg, context=ctx_id)
indir = rss['rss-indirection-table']
ksft_eq(len(indir), orig_size, "Table size not restored")
ksft_eq(indir, [3, 2, 1, 0] * (len(indir) // 4),
"Unfolded table has wrong pattern")
@ksft_variants([
KsftNamedVariant("main", False),
KsftNamedVariant("ctx", True),
])
def resize_below_user_size_reject(cfg, create_context):
"""Test that shrinking below user_size is rejected.
Send a table via netlink whose size (user_size) sits between
the small and large device table sizes. The table is periodic,
so folding would normally succeed, but the user_size floor must
prevent it.
"""
channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
ch_max = channels.get('combined-max', 0)
qcnt = channels['combined-count']
if ch_max < 4:
raise KsftSkipEx(f"Not enough queues for the test: max={ch_max}")
defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
_require_dynamic_indir_size(cfg, ch_max)
ctx_id = _maybe_create_context(cfg, create_context)
# Measure the table size at max channels
rss = _get_rss(cfg, context=ctx_id)
big_size = len(rss['rss-indirection-table'])
# Measure the table size at reduced channels
ethtool(f"-L {cfg.ifname} combined 4")
rss = _get_rss(cfg, context=ctx_id)
small_size = len(rss['rss-indirection-table'])
ethtool(f"-L {cfg.ifname} combined {ch_max}")
if small_size >= big_size:
raise KsftSkipEx("Table did not shrink at reduced channels")
# Find a user_size
user_size = None
for div in [2, 4]:
candidate = big_size // div
if candidate > small_size and big_size % candidate == 0:
user_size = candidate
break
if user_size is None:
raise KsftSkipEx("No suitable user_size between small and big table")
# Send a periodic sub-table of exactly user_size entries.
# Pattern safe for 4 channels.
pattern = [0, 1, 2, 3] * (user_size // 4)
if len(pattern) != user_size:
raise KsftSkipEx(f"user_size ({user_size}) not divisible by 4")
req = {'header': {'dev-index': cfg.ifindex}, 'indir': pattern}
if ctx_id:
req['context'] = ctx_id
else:
defer(ethtool, f"-X {cfg.ifname} default")
cfg.ethnl.rss_set(req)
# Shrink channels — table would go to small_size < user_size.
# The table is periodic so folding would work, but user_size
# floor must reject it.
with ksft_raises(CmdExitFailure):
ethtool(f"-L {cfg.ifname} combined 4")
@ksft_variants([
KsftNamedVariant("main", False),
KsftNamedVariant("ctx", True),
])
def resize_nonperiodic_reject(cfg, create_context):
"""Test that a non-periodic table blocks channel reduction.
Set equal weight across all queues so the table is not periodic
at any smaller size, then verify channel reduction is rejected.
An additional context with a periodic table is created to verify
that validation catches the non-periodic one even when others
are fine.
"""
channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
ch_max = channels.get('combined-max', 0)
qcnt = channels['combined-count']
if ch_max < 4:
raise KsftSkipEx(f"Not enough queues for the test: max={ch_max}")
defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
_require_dynamic_indir_size(cfg, ch_max)
ctx_id = _maybe_create_context(cfg, create_context)
ctx_ref = f"context {ctx_id}" if ctx_id else ""
# Create an extra context with a periodic (foldable) table so that
# the validation must iterate all contexts to find the bad one.
extra_ctx = _maybe_create_context(cfg, True)
ethtool(f"-X {cfg.ifname} context {extra_ctx} equal 2")
ethtool(f"-X {cfg.ifname} {ctx_ref} equal {ch_max}")
if not create_context:
defer(ethtool, f"-X {cfg.ifname} default")
with ksft_raises(CmdExitFailure):
ethtool(f"-L {cfg.ifname} combined 2")
@ksft_variants([
KsftNamedVariant("main", False),
KsftNamedVariant("ctx", True),
])
def resize_nonperiodic_no_corruption(cfg, create_context):
"""Test that a failed resize does not corrupt table or channel count.
Set a non-periodic table, attempt a channel reduction (which must
fail), then verify both the indirection table contents and the
channel count are unchanged.
"""
channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
ch_max = channels.get('combined-max', 0)
qcnt = channels['combined-count']
if ch_max < 4:
raise KsftSkipEx(f"Not enough queues for the test: max={ch_max}")
defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
_require_dynamic_indir_size(cfg, ch_max)
ctx_id = _maybe_create_context(cfg, create_context)
ctx_ref = f"context {ctx_id}" if ctx_id else ""
ethtool(f"-X {cfg.ifname} {ctx_ref} equal {ch_max}")
if not create_context:
defer(ethtool, f"-X {cfg.ifname} default")
rss_before = _get_rss(cfg, context=ctx_id)
with ksft_raises(CmdExitFailure):
ethtool(f"-L {cfg.ifname} combined 2")
rss_after = _get_rss(cfg, context=ctx_id)
ksft_eq(rss_after['rss-indirection-table'],
rss_before['rss-indirection-table'],
"Indirection table corrupted after failed resize")
channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
ksft_eq(channels['combined-count'], ch_max,
"Channel count changed after failed resize")
def main() -> None:
""" Ksft boiler plate main """
with NetDrvEnv(__file__) as cfg:
cfg.ethnl = EthtoolFamily()
ksft_run([indir_size_4x, resize_periodic,
resize_below_user_size_reject,
resize_nonperiodic_reject,
resize_nonperiodic_no_corruption], args=(cfg, ))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,172 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""
Tests for RSS hashing on IPv6 Flow Label.
"""
import glob
import os
import socket
from lib.py import CmdExitFailure
from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_ge, ksft_in, \
ksft_not_in, ksft_raises, KsftSkipEx
from lib.py import bkg, cmd, defer, fd_read_timeout, rand_port
from lib.py import NetDrvEpEnv
def _check_system(cfg):
if not hasattr(socket, "SO_INCOMING_CPU"):
raise KsftSkipEx("socket.SO_INCOMING_CPU was added in Python 3.11")
qcnt = len(glob.glob(f"/sys/class/net/{cfg.ifname}/queues/rx-*"))
if qcnt < 2:
raise KsftSkipEx(f"Local has only {qcnt} queues")
for f in [f"/sys/class/net/{cfg.ifname}/queues/rx-0/rps_flow_cnt",
f"/sys/class/net/{cfg.ifname}/queues/rx-0/rps_cpus"]:
try:
with open(f, 'r') as fp:
setting = fp.read().strip()
# CPU mask will be zeros and commas
if setting.replace("0", "").replace(",", ""):
raise KsftSkipEx(f"RPS/RFS is configured: {f}: {setting}")
except FileNotFoundError:
pass
# 1 is the default, if someone changed it we probably shouldn"t mess with it
af = cmd("cat /proc/sys/net/ipv6/auto_flowlabels", host=cfg.remote).stdout
if af.strip() != "1":
raise KsftSkipEx("Remote does not have auto_flowlabels enabled")
def _ethtool_get_cfg(cfg, fl_type):
descr = cmd(f"ethtool -n {cfg.ifname} rx-flow-hash {fl_type}").stdout
converter = {
"IP SA": "s",
"IP DA": "d",
"L3 proto": "t",
"L4 bytes 0 & 1 [TCP/UDP src port]": "f",
"L4 bytes 2 & 3 [TCP/UDP dst port]": "n",
"IPv6 Flow Label": "l",
}
ret = ""
for line in descr.split("\n")[1:-2]:
# if this raises we probably need to add more keys to converter above
ret += converter[line]
return ret
def _traffic(cfg, one_sock, one_cpu):
local_port = rand_port(socket.SOCK_DGRAM)
remote_port = rand_port(socket.SOCK_DGRAM)
sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
sock.bind(("", local_port))
sock.connect((cfg.remote_addr_v["6"], 0))
if one_sock:
send = f"exec 5<>/dev/udp/{cfg.addr_v['6']}/{local_port}; " \
"for i in `seq 20`; do echo a >&5; sleep 0.02; done; exec 5>&-"
else:
send = "for i in `seq 20`; do echo a | socat -t0.02 - UDP6:" \
f"[{cfg.addr_v['6']}]:{local_port},sourceport={remote_port}; done"
cpus = set()
with bkg(send, shell=True, host=cfg.remote, exit_wait=True):
for _ in range(20):
fd_read_timeout(sock.fileno(), 1)
cpu = sock.getsockopt(socket.SOL_SOCKET, socket.SO_INCOMING_CPU)
cpus.add(cpu)
if one_cpu:
ksft_eq(len(cpus), 1,
f"{one_sock=} - expected one CPU, got traffic on: {cpus=}")
else:
ksft_ge(len(cpus), 2,
f"{one_sock=} - expected many CPUs, got traffic on: {cpus=}")
def test_rss_flow_label(cfg):
"""
Test hashing on IPv6 flow label. Send traffic over a single socket
and over multiple sockets. Depend on the remote having auto-label
enabled so that it randomizes the label per socket.
"""
cfg.require_ipver("6")
cfg.require_cmd("socat", remote=True)
_check_system(cfg)
# Enable flow label hashing for UDP6
initial = _ethtool_get_cfg(cfg, "udp6")
no_lbl = initial.replace("l", "")
if "l" not in initial:
try:
cmd(f"ethtool -N {cfg.ifname} rx-flow-hash udp6 l{no_lbl}")
except CmdExitFailure as exc:
raise KsftSkipEx("Device doesn't support Flow Label for UDP6") from exc
defer(cmd, f"ethtool -N {cfg.ifname} rx-flow-hash udp6 {initial}")
_traffic(cfg, one_sock=True, one_cpu=True)
_traffic(cfg, one_sock=False, one_cpu=False)
# Disable it, we should see no hashing (reset was already defer()ed)
cmd(f"ethtool -N {cfg.ifname} rx-flow-hash udp6 {no_lbl}")
_traffic(cfg, one_sock=False, one_cpu=True)
def _check_v4_flow_types(cfg):
for fl_type in ["tcp4", "udp4", "ah4", "esp4", "sctp4"]:
try:
cur = cmd(f"ethtool -n {cfg.ifname} rx-flow-hash {fl_type}").stdout
ksft_not_in("Flow Label", cur,
comment=f"{fl_type=} has Flow Label:" + cur)
except CmdExitFailure:
# Probably does not support this flow type
pass
def test_rss_flow_label_6only(cfg):
"""
Test interactions with IPv4 flow types. It should not be possible to set
IPv6 Flow Label hashing for an IPv4 flow type. The Flow Label should also
not appear in the IPv4 "current config".
"""
with ksft_raises(CmdExitFailure) as cm:
cmd(f"ethtool -N {cfg.ifname} rx-flow-hash tcp4 sdfnl")
ksft_in("Invalid argument", cm.exception.cmd.stderr)
_check_v4_flow_types(cfg)
# Try to enable Flow Labels and check again, in case it leaks thru
initial = _ethtool_get_cfg(cfg, "udp6")
no_lbl = initial.replace("l", "")
if "l" not in initial:
try:
cmd(f"ethtool -N {cfg.ifname} rx-flow-hash udp6 l{no_lbl}")
except CmdExitFailure as exc:
raise KsftSkipEx("Device doesn't support Flow Label for UDP6") from exc
else:
cmd(f"ethtool -N {cfg.ifname} rx-flow-hash udp6 {no_lbl}")
restore = defer(cmd, f"ethtool -N {cfg.ifname} rx-flow-hash udp6 {initial}")
_check_v4_flow_types(cfg)
restore.exec()
_check_v4_flow_types(cfg)
def main() -> None:
with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
ksft_run([test_rss_flow_label,
test_rss_flow_label_6only],
args=(cfg, ))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
import multiprocessing
import socket
from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_ge, cmd, fd_read_timeout
from lib.py import NetDrvEpEnv
from lib.py import EthtoolFamily, NetdevFamily, NlError
from lib.py import KsftSkipEx, KsftFailEx
from lib.py import defer, ksft_pr, rand_port
def traffic(cfg, local_port, remote_port, ipver):
af_inet = socket.AF_INET if ipver == "4" else socket.AF_INET6
sock = socket.socket(af_inet, socket.SOCK_DGRAM)
sock.bind(("", local_port))
sock.connect((cfg.remote_addr_v[ipver], remote_port))
tgt = f"{ipver}:[{cfg.addr_v[ipver]}]:{local_port},sourceport={remote_port}"
cmd("echo a | socat - UDP" + tgt, host=cfg.remote)
fd_read_timeout(sock.fileno(), 5)
return sock.getsockopt(socket.SOL_SOCKET, socket.SO_INCOMING_CPU)
def _rss_input_xfrm_try_enable(cfg):
"""
Check if symmetric input-xfrm is already enabled, if not try to enable it
and register a cleanup.
"""
rss = cfg.ethnl.rss_get({'header': {'dev-name': cfg.ifname}})
orig_xfrm = rss.get('input-xfrm', set())
sym_xfrm = set(filter(lambda x: 'sym' in x, orig_xfrm))
if sym_xfrm:
ksft_pr("Sym input xfrm already enabled:", sym_xfrm)
return sym_xfrm
for xfrm in cfg.ethnl.consts["input-xfrm"].entries:
# Skip non-symmetric transforms
if "sym" not in xfrm:
continue
try_xfrm = {xfrm} | orig_xfrm
try:
cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
"input-xfrm": try_xfrm})
except NlError:
continue
ksft_pr("Sym input xfrm configured:", try_xfrm)
defer(cfg.ethnl.rss_set,
{"header": {"dev-index": cfg.ifindex},
"input-xfrm": orig_xfrm})
return {xfrm}
return set()
def test_rss_input_xfrm(cfg, ipver):
"""
Test symmetric input_xfrm.
If symmetric RSS hash is configured, send traffic twice, swapping the
src/dst UDP ports, and verify that the same queue is receiving the traffic
in both cases (IPs are constant).
"""
if multiprocessing.cpu_count() < 2:
raise KsftSkipEx("Need at least two CPUs to test symmetric RSS hash")
cfg.require_cmd("socat", local=False, remote=True)
if not hasattr(socket, "SO_INCOMING_CPU"):
raise KsftSkipEx("socket.SO_INCOMING_CPU was added in Python 3.11")
# Check for symmetric xor/or-xor
input_xfrm = _rss_input_xfrm_try_enable(cfg)
if not input_xfrm:
raise KsftSkipEx("Symmetric RSS hash not supported by device")
cpus = set()
successful = 0
for _ in range(100):
try:
port1 = rand_port(socket.SOCK_DGRAM)
port2 = rand_port(socket.SOCK_DGRAM)
cpu1 = traffic(cfg, port1, port2, ipver)
cpu2 = traffic(cfg, port2, port1, ipver)
cpus.update([cpu1, cpu2])
ksft_eq(
cpu1, cpu2, comment=f"Received traffic on different cpus with ports ({port1 = }, {port2 = }) while symmetric hash is configured")
successful += 1
if successful == 10:
break
except:
continue
else:
raise KsftFailEx("Failed to run traffic")
ksft_ge(len(cpus), 2,
comment=f"Received traffic on less than two cpus {cpus = }")
def test_rss_input_xfrm_ipv4(cfg):
cfg.require_ipver("4")
test_rss_input_xfrm(cfg, "4")
def test_rss_input_xfrm_ipv6(cfg):
cfg.require_ipver("6")
test_rss_input_xfrm(cfg, "6")
def main() -> None:
with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
cfg.ethnl = EthtoolFamily()
cfg.netdevnl = NetdevFamily()
ksft_run([test_rss_input_xfrm_ipv4, test_rss_input_xfrm_ipv6],
args=(cfg, ))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1 @@
timeout=0
@@ -0,0 +1,673 @@
// SPDX-License-Identifier: GPL-2.0
/* Toeplitz test
*
* 1. Read packets and their rx_hash using PF_PACKET/TPACKET_V3
* 2. Compute the rx_hash in software based on the packet contents
* 3. Compare the two
*
* Optionally, either '-C $rx_irq_cpu_list' or '-r $rps_bitmap' may be given.
*
* If '-C $rx_irq_cpu_list' is given, also
*
* 4. Identify the cpu on which the packet arrived with PACKET_FANOUT_CPU
* 5. Compute the rxqueue that RSS would select based on this rx_hash
* 6. Using the $rx_irq_cpu_list map, identify the arriving cpu based on rxq irq
* 7. Compare the cpus from 4 and 6
*
* Else if '-r $rps_bitmap' is given, also
*
* 4. Identify the cpu on which the packet arrived with PACKET_FANOUT_CPU
* 5. Compute the cpu that RPS should select based on rx_hash and $rps_bitmap
* 6. Compare the cpus from 4 and 5
*/
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <error.h>
#include <fcntl.h>
#include <getopt.h>
#include <linux/filter.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <net/if.h>
#include <netdb.h>
#include <netinet/ip.h>
#include <netinet/ip6.h>
#include <netinet/tcp.h>
#include <netinet/udp.h>
#include <poll.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/sysinfo.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <ynl.h>
#include "ethtool-user.h"
#include "kselftest.h"
#include "../../../net/lib/ksft.h"
#define TOEPLITZ_KEY_MIN_LEN 40
#define TOEPLITZ_KEY_MAX_LEN 256
#define TOEPLITZ_STR_LEN(K) (((K) * 3) - 1) /* hex encoded: AA:BB:CC:...:ZZ */
#define TOEPLITZ_STR_MIN_LEN TOEPLITZ_STR_LEN(TOEPLITZ_KEY_MIN_LEN)
#define TOEPLITZ_STR_MAX_LEN TOEPLITZ_STR_LEN(TOEPLITZ_KEY_MAX_LEN)
#define FOUR_TUPLE_MAX_LEN ((sizeof(struct in6_addr) * 2) + (sizeof(uint16_t) * 2))
#define RSS_MAX_CPUS (1 << 16) /* real constraint is PACKET_FANOUT_MAX */
#define RSS_MAX_INDIR (1 << 16)
#define RPS_MAX_CPUS 16UL /* must be a power of 2 */
#define MIN_PKT_SAMPLES 40 /* minimum number of packets to receive */
/* configuration options (cmdline arguments) */
static uint16_t cfg_dport = 8000;
static int cfg_family = AF_INET6;
static char *cfg_ifname = "eth0";
static int cfg_num_queues;
static int cfg_num_rps_cpus;
static bool cfg_sink;
static int cfg_type = SOCK_STREAM;
static int cfg_timeout_msec = 1000;
static bool cfg_verbose;
/* global vars */
static int num_cpus;
static int ring_block_nr;
static int ring_block_sz;
/* stats */
static int frames_received;
static int frames_nohash;
static int frames_error;
#define log_verbose(args...) do { if (cfg_verbose) fprintf(stderr, args); } while (0)
/* tpacket ring */
struct ring_state {
int fd;
char *mmap;
int idx;
int cpu;
};
static unsigned int rx_irq_cpus[RSS_MAX_CPUS]; /* map from rxq to cpu */
static int rps_silo_to_cpu[RPS_MAX_CPUS];
static unsigned char toeplitz_key[TOEPLITZ_KEY_MAX_LEN];
static unsigned int rss_indir_tbl[RSS_MAX_INDIR];
static unsigned int rss_indir_tbl_size;
static struct ring_state rings[RSS_MAX_CPUS];
static inline uint32_t toeplitz(const unsigned char *four_tuple,
const unsigned char *key)
{
int i, bit, ret = 0;
uint32_t key32;
key32 = ntohl(*((uint32_t *)key));
key += 4;
for (i = 0; i < FOUR_TUPLE_MAX_LEN; i++) {
for (bit = 7; bit >= 0; bit--) {
if (four_tuple[i] & (1 << bit))
ret ^= key32;
key32 <<= 1;
key32 |= !!(key[0] & (1 << bit));
}
key++;
}
return ret;
}
/* Compare computed cpu with arrival cpu from packet_fanout_cpu */
static void verify_rss(uint32_t rx_hash, int cpu)
{
int queue;
if (rss_indir_tbl_size)
queue = rss_indir_tbl[rx_hash % rss_indir_tbl_size];
else
queue = rx_hash % cfg_num_queues;
log_verbose(" rxq %d (cpu %d)", queue, rx_irq_cpus[queue]);
if (rx_irq_cpus[queue] != cpu) {
log_verbose(". error: rss cpu mismatch (%d)", cpu);
frames_error++;
}
}
static void verify_rps(uint64_t rx_hash, int cpu)
{
int silo = (rx_hash * cfg_num_rps_cpus) >> 32;
log_verbose(" silo %d (cpu %d)", silo, rps_silo_to_cpu[silo]);
if (rps_silo_to_cpu[silo] != cpu) {
log_verbose(". error: rps cpu mismatch (%d)", cpu);
frames_error++;
}
}
static void log_rxhash(int cpu, uint32_t rx_hash,
const char *addrs, int addr_len)
{
char saddr[INET6_ADDRSTRLEN], daddr[INET6_ADDRSTRLEN];
uint16_t *ports;
if (!inet_ntop(cfg_family, addrs, saddr, sizeof(saddr)) ||
!inet_ntop(cfg_family, addrs + addr_len, daddr, sizeof(daddr)))
error(1, 0, "address parse error");
ports = (void *)addrs + (addr_len * 2);
log_verbose("cpu %d: rx_hash 0x%08x [saddr %s daddr %s sport %02hu dport %02hu]",
cpu, rx_hash, saddr, daddr,
ntohs(ports[0]), ntohs(ports[1]));
}
/* Compare computed rxhash with rxhash received from tpacket_v3 */
static void verify_rxhash(const char *pkt, uint32_t rx_hash, int cpu)
{
unsigned char four_tuple[FOUR_TUPLE_MAX_LEN] = {0};
uint32_t rx_hash_sw;
const char *addrs;
int addr_len;
if (cfg_family == AF_INET) {
addr_len = sizeof(struct in_addr);
addrs = pkt + offsetof(struct iphdr, saddr);
} else {
addr_len = sizeof(struct in6_addr);
addrs = pkt + offsetof(struct ip6_hdr, ip6_src);
}
memcpy(four_tuple, addrs, (addr_len * 2) + (sizeof(uint16_t) * 2));
rx_hash_sw = toeplitz(four_tuple, toeplitz_key);
if (cfg_verbose)
log_rxhash(cpu, rx_hash, addrs, addr_len);
if (rx_hash != rx_hash_sw) {
log_verbose(" != expected 0x%x\n", rx_hash_sw);
frames_error++;
return;
}
log_verbose(" OK");
if (cfg_num_queues)
verify_rss(rx_hash, cpu);
else if (cfg_num_rps_cpus)
verify_rps(rx_hash, cpu);
log_verbose("\n");
}
static char *recv_frame(const struct ring_state *ring, char *frame)
{
struct tpacket3_hdr *hdr = (void *)frame;
if (hdr->hv1.tp_rxhash)
verify_rxhash(frame + hdr->tp_net, hdr->hv1.tp_rxhash,
ring->cpu);
else
frames_nohash++;
return frame + hdr->tp_next_offset;
}
/* A single TPACKET_V3 block can hold multiple frames */
static bool recv_block(struct ring_state *ring)
{
struct tpacket_block_desc *block;
char *frame;
int i;
block = (void *)(ring->mmap + ring->idx * ring_block_sz);
if (!(block->hdr.bh1.block_status & TP_STATUS_USER))
return false;
frame = (char *)block;
frame += block->hdr.bh1.offset_to_first_pkt;
for (i = 0; i < block->hdr.bh1.num_pkts; i++) {
frame = recv_frame(ring, frame);
frames_received++;
}
block->hdr.bh1.block_status = TP_STATUS_KERNEL;
ring->idx = (ring->idx + 1) % ring_block_nr;
return true;
}
/* simple test: process all rings until MIN_PKT_SAMPLES packets are received,
* or the test times out.
*/
static void process_rings(void)
{
struct timeval start, now;
bool pkts_found = true;
long elapsed_usec;
int i;
gettimeofday(&start, NULL);
do {
if (!pkts_found)
usleep(100);
pkts_found = false;
for (i = 0; i < num_cpus; i++)
pkts_found |= recv_block(&rings[i]);
gettimeofday(&now, NULL);
elapsed_usec = (now.tv_sec - start.tv_sec) * 1000000 +
(now.tv_usec - start.tv_usec);
} while (frames_received - frames_nohash < MIN_PKT_SAMPLES &&
elapsed_usec < cfg_timeout_msec * 1000);
fprintf(stderr, "count: pass=%u nohash=%u fail=%u\n",
frames_received - frames_nohash - frames_error,
frames_nohash, frames_error);
}
static char *setup_ring(int fd)
{
struct tpacket_req3 req3 = {0};
void *ring;
req3.tp_retire_blk_tov = cfg_timeout_msec / 8;
req3.tp_feature_req_word = TP_FT_REQ_FILL_RXHASH;
req3.tp_frame_size = 2048;
req3.tp_frame_nr = 1 << 10;
req3.tp_block_nr = 16;
req3.tp_block_size = req3.tp_frame_size * req3.tp_frame_nr;
req3.tp_block_size /= req3.tp_block_nr;
if (setsockopt(fd, SOL_PACKET, PACKET_RX_RING, &req3, sizeof(req3)))
error(1, errno, "setsockopt PACKET_RX_RING");
ring_block_sz = req3.tp_block_size;
ring_block_nr = req3.tp_block_nr;
ring = mmap(0, req3.tp_block_size * req3.tp_block_nr,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_LOCKED | MAP_POPULATE, fd, 0);
if (ring == MAP_FAILED)
error(1, 0, "mmap failed");
return ring;
}
static void __set_filter(int fd, int off_proto, uint8_t proto, int off_dport)
{
struct sock_filter filter[] = {
BPF_STMT(BPF_LD + BPF_B + BPF_ABS, SKF_AD_OFF + SKF_AD_PKTTYPE),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, PACKET_HOST, 0, 4),
BPF_STMT(BPF_LD + BPF_B + BPF_ABS, off_proto),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, proto, 0, 2),
BPF_STMT(BPF_LD + BPF_H + BPF_ABS, off_dport),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, cfg_dport, 1, 0),
BPF_STMT(BPF_RET + BPF_K, 0),
BPF_STMT(BPF_RET + BPF_K, 0xFFFF),
};
struct sock_fprog prog = {};
prog.filter = filter;
prog.len = ARRAY_SIZE(filter);
if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &prog, sizeof(prog)))
error(1, errno, "setsockopt filter");
}
/* filter on transport protocol and destination port */
static void set_filter(int fd)
{
const int off_dport = offsetof(struct tcphdr, dest); /* same for udp */
uint8_t proto;
proto = cfg_type == SOCK_STREAM ? IPPROTO_TCP : IPPROTO_UDP;
if (cfg_family == AF_INET)
__set_filter(fd, offsetof(struct iphdr, protocol), proto,
sizeof(struct iphdr) + off_dport);
else
__set_filter(fd, offsetof(struct ip6_hdr, ip6_nxt), proto,
sizeof(struct ip6_hdr) + off_dport);
}
/* drop everything: used temporarily during setup */
static void set_filter_null(int fd)
{
struct sock_filter filter[] = {
BPF_STMT(BPF_RET + BPF_K, 0),
};
struct sock_fprog prog = {};
prog.filter = filter;
prog.len = ARRAY_SIZE(filter);
if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &prog, sizeof(prog)))
error(1, errno, "setsockopt filter");
}
static int create_ring(char **ring)
{
struct fanout_args args = {
.id = 1,
.type_flags = PACKET_FANOUT_CPU,
.max_num_members = RSS_MAX_CPUS
};
struct sockaddr_ll ll = { 0 };
int fd, val;
fd = socket(PF_PACKET, SOCK_DGRAM, 0);
if (fd == -1)
error(1, errno, "socket creation failed");
val = TPACKET_V3;
if (setsockopt(fd, SOL_PACKET, PACKET_VERSION, &val, sizeof(val)))
error(1, errno, "setsockopt PACKET_VERSION");
*ring = setup_ring(fd);
/* block packets until all rings are added to the fanout group:
* else packets can arrive during setup and get misclassified
*/
set_filter_null(fd);
ll.sll_family = AF_PACKET;
ll.sll_ifindex = if_nametoindex(cfg_ifname);
ll.sll_protocol = cfg_family == AF_INET ? htons(ETH_P_IP) :
htons(ETH_P_IPV6);
if (bind(fd, (void *)&ll, sizeof(ll)))
error(1, errno, "bind");
/* must come after bind: verifies all programs in group match */
if (setsockopt(fd, SOL_PACKET, PACKET_FANOUT, &args, sizeof(args))) {
/* on failure, retry using old API if that is sufficient:
* it has a hard limit of 256 sockets, so only try if
* (a) only testing rxhash, not RSS or (b) <= 256 cpus.
* in this API, the third argument is left implicit.
*/
if (cfg_num_queues || num_cpus > 256 ||
setsockopt(fd, SOL_PACKET, PACKET_FANOUT,
&args, sizeof(uint32_t)))
error(1, errno, "setsockopt PACKET_FANOUT cpu");
}
return fd;
}
/* setup inet(6) socket to blackhole the test traffic, if arg '-s' */
static int setup_sink(void)
{
int fd, val;
fd = socket(cfg_family, cfg_type, 0);
if (fd == -1)
error(1, errno, "socket %d.%d", cfg_family, cfg_type);
val = 1 << 20;
if (setsockopt(fd, SOL_SOCKET, SO_RCVBUFFORCE, &val, sizeof(val)))
error(1, errno, "setsockopt rcvbuf");
return fd;
}
static void setup_rings(void)
{
int i;
for (i = 0; i < num_cpus; i++) {
rings[i].cpu = i;
rings[i].fd = create_ring(&rings[i].mmap);
}
/* accept packets once all rings in the fanout group are up */
for (i = 0; i < num_cpus; i++)
set_filter(rings[i].fd);
}
static void cleanup_rings(void)
{
int i;
for (i = 0; i < num_cpus; i++) {
if (munmap(rings[i].mmap, ring_block_nr * ring_block_sz))
error(1, errno, "munmap");
if (close(rings[i].fd))
error(1, errno, "close");
}
}
static void parse_cpulist(const char *arg)
{
do {
rx_irq_cpus[cfg_num_queues++] = strtol(arg, NULL, 10);
arg = strchr(arg, ',');
if (!arg)
break;
arg++; // skip ','
} while (1);
}
static void show_cpulist(void)
{
int i;
for (i = 0; i < cfg_num_queues; i++)
fprintf(stderr, "rxq %d: cpu %d\n", i, rx_irq_cpus[i]);
}
static void show_silos(void)
{
int i;
for (i = 0; i < cfg_num_rps_cpus; i++)
fprintf(stderr, "silo %d: cpu %d\n", i, rps_silo_to_cpu[i]);
}
static void parse_toeplitz_key(const char *str, int slen, unsigned char *key)
{
int i, ret, off;
if (slen < TOEPLITZ_STR_MIN_LEN ||
slen > TOEPLITZ_STR_MAX_LEN + 1)
error(1, 0, "invalid toeplitz key");
for (i = 0, off = 0; off < slen; i++, off += 3) {
ret = sscanf(str + off, "%hhx", &key[i]);
if (ret != 1)
error(1, 0, "key parse error at %d off %d len %d",
i, off, slen);
}
}
static void parse_rps_bitmap(const char *arg)
{
unsigned long bitmap;
int i;
bitmap = strtoul(arg, NULL, 0);
if (bitmap & ~((1UL << RPS_MAX_CPUS) - 1))
error(1, 0, "rps bitmap 0x%lx out of bounds, max cpu %lu",
bitmap, RPS_MAX_CPUS - 1);
for (i = 0; i < RPS_MAX_CPUS; i++)
if (bitmap & 1UL << i)
rps_silo_to_cpu[cfg_num_rps_cpus++] = i;
}
static void read_rss_dev_info_ynl(void)
{
struct ethtool_rss_get_req *req;
struct ethtool_rss_get_rsp *rsp;
struct ynl_sock *ys;
ys = ynl_sock_create(&ynl_ethtool_family, NULL);
if (!ys)
error(1, errno, "ynl_sock_create failed");
req = ethtool_rss_get_req_alloc();
if (!req)
error(1, errno, "ethtool_rss_get_req_alloc failed");
ethtool_rss_get_req_set_header_dev_name(req, cfg_ifname);
rsp = ethtool_rss_get(ys, req);
if (!rsp)
error(1, ys->err.code, "YNL: %s", ys->err.msg);
if (!rsp->_len.hkey)
error(1, 0, "RSS key not available for %s", cfg_ifname);
if (rsp->_len.hkey < TOEPLITZ_KEY_MIN_LEN ||
rsp->_len.hkey > TOEPLITZ_KEY_MAX_LEN)
error(1, 0, "RSS key length %u out of bounds [%u, %u]",
rsp->_len.hkey, TOEPLITZ_KEY_MIN_LEN,
TOEPLITZ_KEY_MAX_LEN);
memcpy(toeplitz_key, rsp->hkey, rsp->_len.hkey);
if (rsp->_count.indir > RSS_MAX_INDIR)
error(1, 0, "RSS indirection table too large (%u > %u)",
rsp->_count.indir, RSS_MAX_INDIR);
/* If indir table not available we'll fallback to simple modulo math */
if (rsp->_count.indir) {
memcpy(rss_indir_tbl, rsp->indir,
rsp->_count.indir * sizeof(rss_indir_tbl[0]));
rss_indir_tbl_size = rsp->_count.indir;
log_verbose("RSS indirection table size: %u\n",
rss_indir_tbl_size);
}
ethtool_rss_get_rsp_free(rsp);
ethtool_rss_get_req_free(req);
ynl_sock_destroy(ys);
}
static void parse_opts(int argc, char **argv)
{
static struct option long_options[] = {
{"dport", required_argument, 0, 'd'},
{"cpus", required_argument, 0, 'C'},
{"key", required_argument, 0, 'k'},
{"iface", required_argument, 0, 'i'},
{"ipv4", no_argument, 0, '4'},
{"ipv6", no_argument, 0, '6'},
{"sink", no_argument, 0, 's'},
{"tcp", no_argument, 0, 't'},
{"timeout", required_argument, 0, 'T'},
{"udp", no_argument, 0, 'u'},
{"verbose", no_argument, 0, 'v'},
{"rps", required_argument, 0, 'r'},
{0, 0, 0, 0}
};
bool have_toeplitz = false;
int index, c;
while ((c = getopt_long(argc, argv, "46C:d:i:k:r:stT:uv", long_options, &index)) != -1) {
switch (c) {
case '4':
cfg_family = AF_INET;
break;
case '6':
cfg_family = AF_INET6;
break;
case 'C':
parse_cpulist(optarg);
break;
case 'd':
cfg_dport = strtol(optarg, NULL, 0);
break;
case 'i':
cfg_ifname = optarg;
break;
case 'k':
parse_toeplitz_key(optarg, strlen(optarg),
toeplitz_key);
have_toeplitz = true;
break;
case 'r':
parse_rps_bitmap(optarg);
break;
case 's':
cfg_sink = true;
break;
case 't':
cfg_type = SOCK_STREAM;
break;
case 'T':
cfg_timeout_msec = strtol(optarg, NULL, 0);
break;
case 'u':
cfg_type = SOCK_DGRAM;
break;
case 'v':
cfg_verbose = true;
break;
default:
error(1, 0, "unknown option %c", optopt);
break;
}
}
if (!have_toeplitz)
read_rss_dev_info_ynl();
num_cpus = get_nprocs();
if (num_cpus > RSS_MAX_CPUS)
error(1, 0, "increase RSS_MAX_CPUS");
if (cfg_num_queues && cfg_num_rps_cpus)
error(1, 0,
"Can't supply both RSS cpus ('-C') and RPS map ('-r')");
if (cfg_verbose) {
show_cpulist();
show_silos();
}
}
int main(int argc, char **argv)
{
const int min_tests = 10;
int fd_sink = -1;
parse_opts(argc, argv);
if (cfg_sink)
fd_sink = setup_sink();
setup_rings();
/* Signal to test framework that we're ready to receive */
ksft_ready();
process_rings();
cleanup_rings();
if (cfg_sink && close(fd_sink))
error(1, errno, "close sink");
if (frames_received - frames_nohash < min_tests)
error(1, 0, "too few frames for verification");
return frames_error;
}
@@ -0,0 +1,216 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""
Toeplitz Rx hashing test:
- rxhash (the hash value calculation itself);
- RSS mapping from rxhash to rx queue;
- RPS mapping from rxhash to cpu.
"""
import glob
import os
import socket
from lib.py import ksft_run, ksft_exit, ksft_pr
from lib.py import NetDrvEpEnv, EthtoolFamily, NetdevFamily
from lib.py import cmd, bkg, rand_port, defer
from lib.py import ksft_in
from lib.py import ksft_variants, KsftNamedVariant, KsftSkipEx, KsftFailEx
# "define" for the ID of the Toeplitz hash function
ETH_RSS_HASH_TOP = 1
# Must match RPS_MAX_CPUS in toeplitz.c
RPS_MAX_CPUS = 16
def _check_rps_and_rfs_not_configured(cfg):
"""Verify that RPS is not already configured."""
for rps_file in glob.glob(f"/sys/class/net/{cfg.ifname}/queues/rx-*/rps_cpus"):
with open(rps_file, "r", encoding="utf-8") as fp:
val = fp.read().strip()
if set(val) - {"0", ","}:
raise KsftSkipEx(f"RPS already configured on {rps_file}: {val}")
rfs_file = "/proc/sys/net/core/rps_sock_flow_entries"
with open(rfs_file, "r", encoding="utf-8") as fp:
val = fp.read().strip()
if val != "0":
raise KsftSkipEx(f"RFS already configured {rfs_file}: {val}")
def _get_cpu_for_irq(irq):
with open(f"/proc/irq/{irq}/smp_affinity_list", "r",
encoding="utf-8") as fp:
data = fp.read().strip()
if "," in data or "-" in data:
raise KsftFailEx(f"IRQ{irq} is not mapped to a single core: {data}")
return int(data)
def _get_irq_cpus(cfg):
"""
Read the list of IRQs for the device Rx queues.
"""
queues = cfg.netnl.queue_get({"ifindex": cfg.ifindex}, dump=True)
napis = cfg.netnl.napi_get({"ifindex": cfg.ifindex}, dump=True)
# Remap into ID-based dicts
napis = {n["id"]: n for n in napis}
queues = {f"{q['type']}{q['id']}": q for q in queues}
cpus = []
for rx in range(9999):
name = f"rx{rx}"
if name not in queues:
break
cpus.append(_get_cpu_for_irq(napis[queues[name]["napi-id"]]["irq"]))
return cpus
def _get_unused_rps_cpus(cfg, count=2):
"""
Get CPUs that are not used by Rx queues for RPS.
Returns a list of at least 'count' CPU numbers within
the RPS_MAX_CPUS supported range.
"""
# Get CPUs used by Rx queues
rx_cpus = set(_get_irq_cpus(cfg))
# Get total number of CPUs, capped by RPS_MAX_CPUS
num_cpus = min(os.cpu_count(), RPS_MAX_CPUS)
# Find unused CPUs
unused_cpus = [cpu for cpu in range(num_cpus) if cpu not in rx_cpus]
if len(unused_cpus) < count:
raise KsftSkipEx(f"Need at least {count} CPUs in range 0..{num_cpus - 1} not used by Rx queues, found {len(unused_cpus)}")
return unused_cpus[:count]
def _configure_rps(cfg, rps_cpus):
"""Configure RPS for all Rx queues."""
mask = 0
for cpu in rps_cpus:
mask |= (1 << cpu)
mask = hex(mask)
# Set RPS bitmap for all rx queues
for rps_file in glob.glob(f"/sys/class/net/{cfg.ifname}/queues/rx-*/rps_cpus"):
with open(rps_file, "w", encoding="utf-8") as fp:
# sysfs expects hex without '0x' prefix, toeplitz.c needs the prefix
fp.write(mask[2:])
return mask
def _send_traffic(cfg, proto_flag, ipver, port):
"""Send 20 packets of requested type."""
# Determine protocol and IP version for socat
if proto_flag == "-u":
proto = "UDP"
else:
proto = "TCP"
baddr = f"[{cfg.addr_v['6']}]" if ipver == "6" else cfg.addr_v["4"]
# Run socat in a loop to send traffic periodically
# Use sh -c with a loop similar to toeplitz_client.sh
socat_cmd = f"""
for i in `seq 20`; do
echo "msg $i" | socat -{ipver} -t 0.1 - {proto}:{baddr}:{port};
sleep 0.001;
done
"""
cmd(socat_cmd, shell=True, host=cfg.remote)
def _test_variants():
for grp in ["", "rss", "rps"]:
for l4 in ["tcp", "udp"]:
for l3 in ["4", "6"]:
name = f"{l4}_ipv{l3}"
if grp:
name = f"{grp}_{name}"
yield KsftNamedVariant(name, "-" + l4[0], l3, grp)
@ksft_variants(_test_variants())
def test(cfg, proto_flag, ipver, grp):
"""Run a single toeplitz test."""
cfg.require_ipver(ipver)
# Check that rxhash is enabled
ksft_in("receive-hashing: on", cmd(f"ethtool -k {cfg.ifname}").stdout)
rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
# Make sure NIC is configured to use Toeplitz hash, and no key xfrm.
if rss.get('hfunc') != ETH_RSS_HASH_TOP or rss.get('input-xfrm'):
cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
"hfunc": ETH_RSS_HASH_TOP,
"input-xfrm": {}})
defer(cfg.ethnl.rss_set, {"header": {"dev-index": cfg.ifindex},
"hfunc": rss.get('hfunc'),
"input-xfrm": rss.get('input-xfrm', {})
})
port = rand_port(socket.SOCK_DGRAM)
toeplitz_path = cfg.test_dir / "toeplitz"
rx_cmd = [
str(toeplitz_path),
"-" + ipver,
proto_flag,
"-d", str(port),
"-i", cfg.ifname,
"-T", "4000",
"-s",
"-v"
]
if grp:
_check_rps_and_rfs_not_configured(cfg)
if grp == "rss":
irq_cpus = ",".join([str(x) for x in _get_irq_cpus(cfg)])
rx_cmd += ["-C", irq_cpus]
ksft_pr(f"RSS using CPUs: {irq_cpus}")
elif grp == "rps":
# Get CPUs not used by Rx queues and configure them for RPS
rps_cpus = _get_unused_rps_cpus(cfg, count=2)
rps_mask = _configure_rps(cfg, rps_cpus)
defer(_configure_rps, cfg, [])
rx_cmd += ["-r", rps_mask]
ksft_pr(f"RPS using CPUs: {rps_cpus}, mask: {rps_mask}")
# Run rx in background, it will exit once it has seen enough packets
with bkg(" ".join(rx_cmd), ksft_ready=True, exit_wait=True) as rx_proc:
while rx_proc.proc.poll() is None:
_send_traffic(cfg, proto_flag, ipver, port)
# Check rx result
ksft_pr("Receiver output:")
ksft_pr(rx_proc.stdout.strip().replace('\n', '\n# '))
if rx_proc.stderr:
ksft_pr(rx_proc.stderr.strip().replace('\n', '\n# '))
def main() -> None:
"""Ksft boilerplate main."""
with NetDrvEpEnv(__file__) as cfg:
cfg.ethnl = EthtoolFamily()
cfg.netnl = NetdevFamily()
ksft_run(cases=[test], args=(cfg,))
ksft_exit()
if __name__ == "__main__":
main()
+263
View File
@@ -0,0 +1,263 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""Run the tools/testing/selftests/net/csum testsuite."""
import fcntl
import socket
import struct
import termios
import time
from lib.py import ksft_pr, ksft_run, ksft_exit, KsftSkipEx, KsftXfailEx
from lib.py import ksft_eq, ksft_ge, ksft_lt
from lib.py import EthtoolFamily, NetdevFamily, NetDrvEpEnv
from lib.py import bkg, cmd, defer, ethtool, ip, rand_port, wait_port_listen
def sock_wait_drain(sock, max_wait=1000):
"""Wait for all pending write data on the socket to get ACKed."""
for _ in range(max_wait):
one = b'\0' * 4
outq = fcntl.ioctl(sock.fileno(), termios.TIOCOUTQ, one)
outq = struct.unpack("I", outq)[0]
if outq == 0:
break
time.sleep(0.01)
ksft_eq(outq, 0)
def tcp_sock_get_retrans(sock):
"""Get the number of retransmissions for the TCP socket."""
info = sock.getsockopt(socket.SOL_TCP, socket.TCP_INFO, 512)
return struct.unpack("I", info[100:104])[0]
def run_one_stream(cfg, ipver, remote_v4, remote_v6, should_lso):
cfg.require_cmd("socat", local=False, remote=True)
# Set recv window clamp to avoid overwhelming receiver on debug kernels
# the 200k clamp should still let use reach > 15Gbps on real HW
port = rand_port()
listen_opts = f"{port},reuseport,tcp-window-clamp=200000"
listen_cmd = f"socat -{ipver} -t 2 -u TCP-LISTEN:{listen_opts} /dev/null,ignoreeof"
with bkg(listen_cmd, host=cfg.remote, exit_wait=True) as nc:
wait_port_listen(port, host=cfg.remote)
if ipver == "4":
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((remote_v4, port))
else:
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
sock.connect((remote_v6, port))
# Small send to make sure the connection is working.
sock.send("ping".encode())
sock_wait_drain(sock)
# Send 4MB of data, record the LSO packet count.
qstat_old = cfg.netnl.qstats_get({"ifindex": cfg.ifindex}, dump=True)[0]
buf = b"0" * 1024 * 1024 * 4
sock.send(buf)
sock_wait_drain(sock)
qstat_new = cfg.netnl.qstats_get({"ifindex": cfg.ifindex}, dump=True)[0]
# Check that at least 90% of the data was sent as LSO packets.
# System noise may cause false negatives. Also header overheads
# will add up to 5% of extra packes... The check is best effort.
total_lso_wire = len(buf) * 0.90 // cfg.dev["mtu"]
total_lso_super = len(buf) * 0.90 // cfg.dev["tso_max_size"]
# Make sure we have order of magnitude more LSO packets than
# retransmits, in case TCP retransmitted all the LSO packets.
ksft_lt(tcp_sock_get_retrans(sock), total_lso_wire / 16)
sock.close()
if should_lso:
if cfg.have_stat_super_count:
ksft_ge(qstat_new['tx-hw-gso-packets'] -
qstat_old['tx-hw-gso-packets'],
total_lso_super,
comment="Number of LSO super-packets with LSO enabled")
if cfg.have_stat_wire_count:
ksft_ge(qstat_new['tx-hw-gso-wire-packets'] -
qstat_old['tx-hw-gso-wire-packets'],
total_lso_wire,
comment="Number of LSO wire-packets with LSO enabled")
else:
if cfg.have_stat_super_count:
ksft_lt(qstat_new['tx-hw-gso-packets'] -
qstat_old['tx-hw-gso-packets'],
15, comment="Number of LSO super-packets with LSO disabled")
if cfg.have_stat_wire_count:
ksft_lt(qstat_new['tx-hw-gso-wire-packets'] -
qstat_old['tx-hw-gso-wire-packets'],
500, comment="Number of LSO wire-packets with LSO disabled")
def build_tunnel(cfg, outer_ipver, tun_info):
local_v4 = NetDrvEpEnv.nsim_v4_pfx + "1"
local_v6 = NetDrvEpEnv.nsim_v6_pfx + "1"
remote_v4 = NetDrvEpEnv.nsim_v4_pfx + "2"
remote_v6 = NetDrvEpEnv.nsim_v6_pfx + "2"
local_addr = cfg.addr_v[outer_ipver]
remote_addr = cfg.remote_addr_v[outer_ipver]
tun_type = tun_info[0]
tun_arg = tun_info[1]
ip(f"link add {tun_type}-ksft type {tun_type} {tun_arg} local {local_addr} remote {remote_addr} dev {cfg.ifname}")
defer(ip, f"link del {tun_type}-ksft")
ip(f"link set dev {tun_type}-ksft up")
ip(f"addr add {local_v4}/24 dev {tun_type}-ksft")
ip(f"addr add {local_v6}/64 dev {tun_type}-ksft")
ip(f"link add {tun_type}-ksft type {tun_type} {tun_arg} local {remote_addr} remote {local_addr} dev {cfg.remote_ifname}",
host=cfg.remote)
defer(ip, f"link del {tun_type}-ksft", host=cfg.remote)
ip(f"link set dev {tun_type}-ksft up", host=cfg.remote)
ip(f"addr add {remote_v4}/24 dev {tun_type}-ksft", host=cfg.remote)
ip(f"addr add {remote_v6}/64 dev {tun_type}-ksft", host=cfg.remote)
return remote_v4, remote_v6
def restore_wanted_features(cfg):
features_cmd = ""
for feature in cfg.hw_features:
setting = "on" if feature in cfg.wanted_features else "off"
features_cmd += f" {feature} {setting}"
try:
ethtool(f"-K {cfg.ifname} {features_cmd}")
except Exception as e:
ksft_pr(f"WARNING: failure restoring wanted features: {e}")
def test_builder(name, cfg, outer_ipver, feature, tun=None, inner_ipver=None):
"""Construct specific tests from the common template."""
def f(cfg):
cfg.require_ipver(outer_ipver)
defer(restore_wanted_features, cfg)
if not cfg.have_stat_super_count and \
not cfg.have_stat_wire_count:
raise KsftSkipEx(f"Device does not support LSO queue stats")
if feature not in cfg.hw_features:
raise KsftSkipEx(f"Device does not support {feature}")
ipver = outer_ipver
if tun:
remote_v4, remote_v6 = build_tunnel(cfg, ipver, tun)
ipver = inner_ipver
else:
remote_v4 = cfg.remote_addr_v["4"]
remote_v6 = cfg.remote_addr_v["6"]
# First test without the feature enabled.
ethtool(f"-K {cfg.ifname} {feature} off")
run_one_stream(cfg, ipver, remote_v4, remote_v6, should_lso=False)
ethtool(f"-K {cfg.ifname} tx-gso-partial off")
ethtool(f"-K {cfg.ifname} tx-tcp-mangleid-segmentation off")
if feature in cfg.partial_features:
ethtool(f"-K {cfg.ifname} tx-gso-partial on")
if ipver == "4":
ksft_pr("Testing with mangleid enabled")
ethtool(f"-K {cfg.ifname} tx-tcp-mangleid-segmentation on")
# Full feature enabled.
ethtool(f"-K {cfg.ifname} {feature} on")
run_one_stream(cfg, ipver, remote_v4, remote_v6, should_lso=True)
f.__name__ = name + ((outer_ipver + "_") if tun else "") + "ipv" + inner_ipver
return f
def query_nic_features(cfg) -> None:
"""Query and cache the NIC features."""
cfg.have_stat_super_count = False
cfg.have_stat_wire_count = False
features = cfg.ethnl.features_get({"header": {"dev-index": cfg.ifindex}})
cfg.wanted_features = set()
for f in features["wanted"]["bits"]["bit"]:
cfg.wanted_features.add(f["name"])
cfg.hw_features = set()
for f in features["hw"]["bits"]["bit"]:
if f.get("value", False):
cfg.hw_features.add(f["name"])
# Check which features are supported via GSO partial
cfg.partial_features = set()
if 'tx-gso-partial' in cfg.hw_features:
seg_features = {f for f in cfg.hw_features if "segmentation" in f}
ethtool(f"-K {cfg.ifname} " +
" ".join(f"{f} on" for f in seg_features))
ethtool(f"-K {cfg.ifname} tx-gso-partial off")
no_partial = set()
features = cfg.ethnl.features_get({"header": {"dev-index": cfg.ifindex}})
for f in features["active"]["bits"]["bit"]:
no_partial.add(f["name"])
cfg.partial_features = seg_features - no_partial
ethtool(f"-K {cfg.ifname} tx-gso-partial on")
restore_wanted_features(cfg)
stats = cfg.netnl.qstats_get({"ifindex": cfg.ifindex}, dump=True)
if stats:
if 'tx-hw-gso-packets' in stats[0]:
ksft_pr("Detected qstat for LSO super-packets")
cfg.have_stat_super_count = True
if 'tx-hw-gso-wire-packets' in stats[0]:
ksft_pr("Detected qstat for LSO wire-packets")
cfg.have_stat_wire_count = True
def main() -> None:
with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
cfg.ethnl = EthtoolFamily()
cfg.netnl = NetdevFamily()
query_nic_features(cfg)
test_info = (
# name, v4/v6 ethtool_feature tun:(type, args, inner ip versions)
("", "4", "tx-tcp-segmentation", None),
("", "6", "tx-tcp6-segmentation", None),
("vxlan", "4", "tx-udp_tnl-segmentation", ("vxlan", "id 100 dstport 4789 noudpcsum", ("4", "6"))),
("vxlan", "6", "tx-udp_tnl-segmentation", ("vxlan", "id 100 dstport 4789 udp6zerocsumtx udp6zerocsumrx", ("4", "6"))),
("vxlan_csum", "", "tx-udp_tnl-csum-segmentation", ("vxlan", "id 100 dstport 4789 udpcsum", ("4", "6"))),
("gre", "4", "tx-gre-segmentation", ("gre", "", ("4", "6"))),
("gre", "6", "tx-gre-segmentation", ("ip6gre","", ("4", "6"))),
("ip", "6", "tx-ipxip6-segmentation", ("ip6tnl","mode any", ("4", "6"))),
("ip", "4", "tx-ipxip4-segmentation", ("sit","", ("6", ))),
("ip", "4", "tx-ipxip4-segmentation", ("ipip","", ("4", ))),
)
cases = []
for outer_ipver in ["4", "6"]:
for info in test_info:
# Skip if test which only works for a specific IP version
if info[1] and outer_ipver != info[1]:
continue
if info[3]:
cases += [
test_builder(info[0], cfg, outer_ipver, info[2], info[3], inner_ipver)
for inner_ipver in info[3][2]
]
else:
cases.append(test_builder(info[0], cfg, outer_ipver, info[2], None, outer_ipver))
ksft_run(cases=cases, args=(cfg, ))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""
Devmem tests for non-init userns.
"""
import os
from devmem_lib import run_rx, run_rx_hds, run_tx, run_tx_chunks, setup_test
from lib.py import NetDrvContEnv, ksft_disruptive, ksft_exit, ksft_run
@ksft_disruptive
def check_userns_rx(cfg) -> None:
"""Run the devmem RX test through non-init userns netkit."""
run_rx(cfg)
@ksft_disruptive
def check_userns_tx(cfg) -> None:
"""Run the devmem TX test through non-init userns netkit."""
run_tx(cfg)
@ksft_disruptive
def check_userns_tx_chunks(cfg) -> None:
"""Run the devmem TX chunking test through non-init userns netkit."""
run_tx_chunks(cfg)
def check_userns_rx_hds(cfg) -> None:
"""Run the HDS test through non-init userns netkit."""
run_rx_hds(cfg)
def main() -> None:
"""Run userns devmem RX selftests against the test environment."""
with NetDrvContEnv(__file__, userns=True, rxqueues=2,
primary_rx_redirect=True) as cfg:
setup_test(cfg,
os.path.join(os.path.dirname(os.path.abspath(__file__)),
"ncdevmem"))
ksft_run([check_userns_rx, check_userns_tx, check_userns_tx_chunks,
check_userns_rx_hds], args=(cfg,))
ksft_exit()
if __name__ == "__main__":
main()
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""Test USO
Sends large UDP datagrams with UDP_SEGMENT and verifies that the peer
receives the expected total payload and that the NIC transmitted at least
the expected number of segments.
"""
import random
import socket
import string
from lib.py import ksft_run, ksft_exit, KsftSkipEx
from lib.py import ksft_eq, ksft_ge, ksft_variants, KsftNamedVariant
from lib.py import NetDrvEpEnv
from lib.py import bkg, defer, ethtool, ip, rand_port, wait_port_listen
# python doesn't expose this constant, so we need to hardcode it to enable UDP
# segmentation for large payloads
UDP_SEGMENT = 103
def _send_uso(cfg, ipver, mss, total_payload, port):
if ipver == "4":
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
dst = (cfg.remote_addr_v["4"], port)
else:
sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
dst = (cfg.remote_addr_v["6"], port)
sock.setsockopt(socket.IPPROTO_UDP, UDP_SEGMENT, mss)
payload = ''.join(random.choice(string.ascii_lowercase)
for _ in range(total_payload))
sock.sendto(payload.encode(), dst)
sock.close()
def _get_tx_packets(cfg):
stats = ip(f"-s link show dev {cfg.ifname}", json=True)[0]
return stats['stats64']['tx']['packets']
def _test_uso(cfg, ipver, mss, total_payload):
cfg.require_ipver(ipver)
cfg.require_cmd("socat", remote=True)
features = ethtool(f"-k {cfg.ifname}", json=True)
uso_was_on = features[0]["tx-udp-segmentation"]["active"]
try:
ethtool(f"-K {cfg.ifname} tx-udp-segmentation on")
except Exception as exc:
raise KsftSkipEx(
"Device does not support tx-udp-segmentation") from exc
if not uso_was_on:
defer(ethtool, f"-K {cfg.ifname} tx-udp-segmentation off")
expected_segs = (total_payload + mss - 1) // mss
port = rand_port(stype=socket.SOCK_DGRAM)
rx_cmd = f"socat -{ipver} -T 2 -u UDP-LISTEN:{port},reuseport STDOUT"
tx_before = _get_tx_packets(cfg)
with bkg(rx_cmd, host=cfg.remote, exit_wait=True) as rx:
wait_port_listen(port, proto="udp", host=cfg.remote)
_send_uso(cfg, ipver, mss, total_payload, port)
ksft_eq(len(rx.stdout), total_payload,
comment=f"Received {len(rx.stdout)}B, expected {total_payload}B")
cfg.wait_hw_stats_settle()
tx_after = _get_tx_packets(cfg)
tx_delta = tx_after - tx_before
ksft_ge(tx_delta, expected_segs,
comment=f"Expected >= {expected_segs} tx packets, got {tx_delta}")
def _uso_variants():
for ipver in ["4", "6"]:
yield KsftNamedVariant(f"v{ipver}_partial", ipver, 1400, 1400 * 10 + 500)
yield KsftNamedVariant(f"v{ipver}_exact", ipver, 1400, 1400 * 5)
@ksft_variants(_uso_variants())
def test_uso(cfg, ipver, mss, total_payload):
"""Send a USO datagram and verify the peer receives the expected segments."""
_test_uso(cfg, ipver, mss, total_payload)
def main() -> None:
"""Run USO tests."""
with NetDrvEpEnv(__file__) as cfg:
ksft_run([test_uso],
args=(cfg, ))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,146 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""
Tests for XDP metadata kfuncs (e.g. bpf_xdp_metadata_rx_hash).
These tests load device-bound XDP programs from xdp_metadata.bpf.o
that call metadata kfuncs, send traffic, and verify the extracted
metadata via BPF maps.
"""
from lib.py import ksft_run, ksft_eq, ksft_exit, ksft_ge, ksft_ne, ksft_pr
from lib.py import KsftNamedVariant, ksft_variants
from lib.py import CmdExitFailure, KsftSkipEx, NetDrvEpEnv
from lib.py import NetdevFamily
from lib.py import bkg, cmd, rand_port, wait_port_listen
from lib.py import ip, bpftool, defer
from lib.py import bpf_map_set, bpf_map_dump, bpf_prog_map_ids
def _load_xdp_metadata_prog(cfg, prog_name, bpf_file="xdp_metadata.bpf.o"):
"""Load a device-bound XDP metadata program and return prog/map info.
Returns:
dict with 'id', 'name', and 'maps' (name -> map_id).
"""
abs_path = cfg.net_lib_dir / bpf_file
pin_dir = "/sys/fs/bpf/xdp_metadata_test"
cmd(f"rm -rf {pin_dir}", shell=True, fail=False)
cmd(f"mkdir -p {pin_dir}", shell=True)
try:
bpftool(f"prog loadall {abs_path} {pin_dir} type xdp "
f"xdpmeta_dev {cfg.ifname}")
except CmdExitFailure as e:
cmd(f"rm -rf {pin_dir}", shell=True, fail=False)
raise KsftSkipEx(
f"Failed to load device-bound XDP program '{prog_name}'"
) from e
defer(cmd, f"rm -rf {pin_dir}", shell=True, fail=False)
pin_path = f"{pin_dir}/{prog_name}"
ip(f"link set dev {cfg.ifname} xdpdrv pinned {pin_path}")
defer(ip, f"link set dev {cfg.ifname} xdpdrv off")
xdp_info = ip(f"-d link show dev {cfg.ifname}", json=True)[0]
prog_id = xdp_info["xdp"]["prog"]["id"]
return {"id": prog_id,
"name": xdp_info["xdp"]["prog"]["name"],
"maps": bpf_prog_map_ids(prog_id)}
def _send_probe(cfg, port, proto="tcp"):
"""Send a single payload from the remote end using socat.
Args:
cfg: Configuration object containing network settings.
port: Port number for the exchange.
proto: Protocol to use, either "tcp" or "udp".
"""
cfg.require_cmd("socat", remote=True)
if proto == "tcp":
rx_cmd = f"socat -{cfg.addr_ipver} -T 2 TCP-LISTEN:{port},reuseport STDOUT"
tx_cmd = f"echo -n rss_hash_test | socat -t 2 -u STDIN TCP:{cfg.baddr}:{port}"
else:
rx_cmd = f"socat -{cfg.addr_ipver} -T 2 -u UDP-RECV:{port},reuseport STDOUT"
tx_cmd = f"echo -n rss_hash_test | socat -t 2 -u STDIN UDP:{cfg.baddr}:{port}"
with bkg(rx_cmd, exit_wait=True):
wait_port_listen(port, proto=proto)
cmd(tx_cmd, host=cfg.remote, shell=True)
# BPF map keys matching the enums in xdp_metadata.bpf.c
_SETUP_KEY_PORT = 1
_RSS_KEY_HASH = 0
_RSS_KEY_TYPE = 1
_RSS_KEY_PKT_CNT = 2
_RSS_KEY_ERR_CNT = 3
XDP_RSS_L4 = 0x8 # BIT(3) from enum xdp_rss_hash_type
@ksft_variants([
KsftNamedVariant("tcp", "tcp"),
KsftNamedVariant("udp", "udp"),
])
def test_xdp_rss_hash(cfg, proto):
"""Test RSS hash metadata extraction via bpf_xdp_metadata_rx_hash().
This test will only run on devices that support xdp-rx-metadata-features.
Loads the xdp_rss_hash program from xdp_metadata, sends a packet using
the specified protocol, and verifies that the program extracted a non-zero
hash with an L4 hash type.
"""
dev_info = cfg.netnl.dev_get({"ifindex": cfg.ifindex})
rx_meta = dev_info.get("xdp-rx-metadata-features", [])
if "hash" not in rx_meta:
raise KsftSkipEx("device does not support XDP rx hash metadata")
prog_info = _load_xdp_metadata_prog(cfg, "xdp_rss_hash")
port = rand_port()
bpf_map_set("map_xdp_setup", _SETUP_KEY_PORT, port)
rss_map_id = prog_info["maps"]["map_rss"]
_send_probe(cfg, port, proto=proto)
rss = bpf_map_dump(rss_map_id)
pkt_cnt = rss.get(_RSS_KEY_PKT_CNT, 0)
err_cnt = rss.get(_RSS_KEY_ERR_CNT, 0)
hash_val = rss.get(_RSS_KEY_HASH, 0)
hash_type = rss.get(_RSS_KEY_TYPE, 0)
ksft_ge(pkt_cnt, 1, comment="should have received at least one packet")
ksft_eq(err_cnt, 0, comment=f"RSS hash error count: {err_cnt}")
ksft_ne(hash_val, 0,
f"RSS hash should be non-zero for {proto.upper()} traffic")
ksft_pr(f" RSS hash: {hash_val:#010x}")
ksft_pr(f" RSS hash type: {hash_type:#06x}")
ksft_ne(hash_type & XDP_RSS_L4, 0,
f"RSS hash type should include L4 for {proto.upper()} traffic")
def main():
"""Run XDP metadata kfunc tests against a real device."""
with NetDrvEpEnv(__file__) as cfg:
cfg.netnl = NetdevFamily()
ksft_run(
[
test_xdp_rss_hash,
],
args=(cfg,))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
# This is intended to be run on a virtio-net guest interface.
# The test binds the XDP socket to the interface without setting
# the fill ring to trigger delayed refill_work. This helps to
# make it easier to reproduce the deadlock when XDP program,
# XDP socket bind/unbind, rx ring resize race with refill_work on
# the buggy kernel.
#
# The Qemu command to setup virtio-net
# -netdev tap,id=hostnet1,vhost=on,script=no,downscript=no
# -device virtio-net-pci,netdev=hostnet1,iommu_platform=on,disable-legacy=on
from lib.py import ksft_exit, ksft_run
from lib.py import KsftSkipEx, KsftFailEx
from lib.py import NetDrvEnv
from lib.py import bkg, ip, cmd, ethtool
import time
def _get_rx_ring_entries(cfg):
output = ethtool(f"-g {cfg.ifname}", json=True)
return output[0]["rx"]
def setup_xsk(cfg, xdp_queue_id = 0) -> bkg:
# Probe for support
xdp = cmd(f'{cfg.net_lib_dir / "xdp_helper"} - -', fail=False)
if xdp.ret == 255:
raise KsftSkipEx('AF_XDP unsupported')
elif xdp.ret > 0:
raise KsftFailEx('unable to create AF_XDP socket')
try:
return bkg(f'{cfg.net_lib_dir / "xdp_helper"} {cfg.ifindex} ' \
'{xdp_queue_id} -z', ksft_wait=3)
except:
raise KsftSkipEx('Failed to bind XDP socket in zerocopy.\n' \
'Please consider adding iommu_platform=on ' \
'when setting up virtio-net-pci')
def check_xdp_bind(cfg):
with setup_xsk(cfg):
ip(f"link set dev %s xdp obj %s sec xdp" %
(cfg.ifname, cfg.net_lib_dir / "xdp_dummy.bpf.o"))
ip(f"link set dev %s xdp off" % cfg.ifname)
def check_rx_resize(cfg):
with setup_xsk(cfg):
rx_ring = _get_rx_ring_entries(cfg)
ethtool(f"-G %s rx %d" % (cfg.ifname, rx_ring // 2))
ethtool(f"-G %s rx %d" % (cfg.ifname, rx_ring))
def main():
with NetDrvEnv(__file__, nsim_test=False) as cfg:
ksft_run([check_xdp_bind, check_rx_resize],
args=(cfg, ))
ksft_exit()
if __name__ == "__main__":
main()
@@ -0,0 +1,59 @@
# SPDX-License-Identifier: GPL-2.0
"""
Driver test environment.
NetDrvEnv and NetDrvEpEnv are the main environment classes.
NetDrvContEnv extends NetDrvEpEnv with netkit container support.
Former is for local host only tests, latter creates / connects
to a remote endpoint. See NIPA wiki for more information about
running and writing driver tests.
"""
import sys
from pathlib import Path
KSFT_DIR = (Path(__file__).parent / "../../../..").resolve()
try:
sys.path.append(KSFT_DIR.as_posix())
# Import one by one to avoid pylint false positives
from net.lib.py import NetNS, NetNSEnter, NetdevSimDev, UserNetNS
from net.lib.py import EthtoolFamily, NetdevFamily, NetshaperFamily, \
NlError, RtnlFamily, DevlinkFamily, PSPFamily, Netlink
from net.lib.py import CmdExitFailure
from net.lib.py import bkg, cmd, bpftool, bpftrace, defer, ethtool, \
fd_read_timeout, ip, rand_port, rand_ports, tc, wait_port_listen, \
wait_file
from net.lib.py import bpf_map_set, bpf_map_dump, bpf_prog_map_ids
from net.lib.py import KsftSkipEx, KsftFailEx, KsftXfailEx
from net.lib.py import ksft_disruptive, ksft_exit, ksft_pr, ksft_run, \
ksft_setup, ksft_variants, KsftNamedVariant
from net.lib.py import ksft_eq, ksft_ge, ksft_in, ksft_is, ksft_lt, \
ksft_ne, ksft_not_in, ksft_raises, ksft_true, ksft_gt, ksft_not_none
__all__ = ["NetNS", "NetNSEnter", "NetdevSimDev", "UserNetNS",
"EthtoolFamily", "NetdevFamily", "NetshaperFamily",
"NlError", "RtnlFamily", "DevlinkFamily", "PSPFamily", "Netlink",
"CmdExitFailure",
"bkg", "cmd", "bpftool", "bpftrace", "defer", "ethtool",
"fd_read_timeout", "ip", "rand_port", "rand_ports", "tc",
"wait_port_listen", "wait_file",
"bpf_map_set", "bpf_map_dump", "bpf_prog_map_ids",
"KsftSkipEx", "KsftFailEx", "KsftXfailEx",
"ksft_disruptive", "ksft_exit", "ksft_pr", "ksft_run",
"ksft_setup", "ksft_variants", "KsftNamedVariant",
"ksft_eq", "ksft_ge", "ksft_in", "ksft_is", "ksft_lt",
"ksft_ne", "ksft_not_in", "ksft_raises", "ksft_true", "ksft_gt",
"ksft_not_none", "ksft_not_none"]
from .env import NetDrvEnv, NetDrvEpEnv, NetDrvContEnv
from .load import GenerateTraffic, Iperf3Runner
from .remote import Remote
__all__ += ["NetDrvEnv", "NetDrvEpEnv", "NetDrvContEnv", "GenerateTraffic",
"Remote", "Iperf3Runner"]
except ModuleNotFoundError as e:
print("Failed importing `net` library from kernel sources")
print(str(e))
sys.exit(4)
@@ -0,0 +1,585 @@
# SPDX-License-Identifier: GPL-2.0
import ipaddress
import os
import sys
import time
import json
from pathlib import Path
from lib.py import KsftSkipEx, KsftXfailEx
from lib.py import ksft_setup, wait_file
from lib.py import cmd, ethtool, ip, CmdExitFailure
from lib.py import NetNS, NetdevSimDev, UserNetNS
from .remote import Remote
from . import bpftool, RtnlFamily, Netlink
class NetDrvEnvBase:
"""
Base class for a NIC / host environments
Attributes:
test_dir: Path to the source directory of the test
net_lib_dir: Path to the net/lib directory
"""
def __init__(self, src_path):
self.src_path = Path(src_path)
self.test_dir = self.src_path.parent.resolve()
self.net_lib_dir = (Path(__file__).parent / "../../../../net/lib").resolve()
self.env = self._load_env_file()
# Following attrs must be set be inheriting classes
self.dev = None
def _load_env_file(self):
env = os.environ.copy()
src_dir = Path(self.src_path).parent.resolve()
if not (src_dir / "net.config").exists():
return ksft_setup(env)
with open((src_dir / "net.config").as_posix(), 'r') as fp:
for line in fp.readlines():
full_file = line
# Strip comments
pos = line.find("#")
if pos >= 0:
line = line[:pos]
line = line.strip()
if not line:
continue
pair = line.split('=', maxsplit=1)
if len(pair) != 2:
raise Exception("Can't parse configuration line:", full_file)
env[pair[0]] = pair[1]
return ksft_setup(env)
def __del__(self):
pass
def __enter__(self):
ip(f"link set dev {self.dev['ifname']} up")
wait_file(f"/sys/class/net/{self.dev['ifname']}/carrier",
lambda x: x.strip() == "1")
return self
def __exit__(self, ex_type, ex_value, ex_tb):
"""
__exit__ gets called at the end of a "with" block.
"""
self.__del__()
class NetDrvEnv(NetDrvEnvBase):
"""
Class for a single NIC / host env, with no remote end
"""
def __init__(self, src_path, nsim_test=None, **kwargs):
super().__init__(src_path)
self._ns = None
if 'NETIF' in self.env:
if nsim_test is True:
raise KsftXfailEx("Test only works on netdevsim")
self.dev = ip("-d link show dev " + self.env['NETIF'], json=True)[0]
else:
if nsim_test is False:
raise KsftXfailEx("Test does not work on netdevsim")
self._ns = NetdevSimDev(**kwargs)
self.dev = self._ns.nsims[0].dev
self.ifname = self.dev['ifname']
self.ifindex = self.dev['ifindex']
def __del__(self):
if self._ns:
self._ns.remove()
self._ns = None
class NetDrvEpEnv(NetDrvEnvBase):
"""
Class for an environment with a local device and "remote endpoint"
which can be used to send traffic in.
For local testing it creates two network namespaces and a pair
of netdevsim devices.
"""
# Network prefixes used for local tests
nsim_v4_pfx = "192.0.2."
nsim_v6_pfx = "2001:db8::"
def __init__(self, src_path, nsim_test=None):
super().__init__(src_path)
self._stats_settle_time = None
# Things we try to destroy
self.remote = None
# These are for local testing state
self._netns = None
self._ns = None
self._ns_peer = None
self.addr_v = { "4": None, "6": None }
self.remote_addr_v = { "4": None, "6": None }
if "NETIF" in self.env:
if nsim_test is True:
raise KsftXfailEx("Test only works on netdevsim")
self._check_env()
self.dev = ip("-d link show dev " + self.env['NETIF'], json=True)[0]
self.addr_v["4"] = self.env.get("LOCAL_V4")
self.addr_v["6"] = self.env.get("LOCAL_V6")
self.remote_addr_v["4"] = self.env.get("REMOTE_V4")
self.remote_addr_v["6"] = self.env.get("REMOTE_V6")
kind = self.env["REMOTE_TYPE"]
args = self.env["REMOTE_ARGS"]
else:
if nsim_test is False:
raise KsftXfailEx("Test does not work on netdevsim")
self.create_local()
self.dev = self._ns.nsims[0].dev
self.addr_v["4"] = self.nsim_v4_pfx + "1"
self.addr_v["6"] = self.nsim_v6_pfx + "1"
self.remote_addr_v["4"] = self.nsim_v4_pfx + "2"
self.remote_addr_v["6"] = self.nsim_v6_pfx + "2"
kind = "netns"
args = self._netns.name
self.remote = Remote(kind, args, src_path)
self.addr_ipver = "6" if self.addr_v["6"] else "4"
self.addr = self.addr_v[self.addr_ipver]
self.remote_addr = self.remote_addr_v[self.addr_ipver]
# Bracketed addresses, some commands need IPv6 to be inside []
self.baddr = f"[{self.addr_v['6']}]" if self.addr_v["6"] else self.addr_v["4"]
self.remote_baddr = f"[{self.remote_addr_v['6']}]" if self.remote_addr_v["6"] else self.remote_addr_v["4"]
self.ifname = self.dev['ifname']
self.ifindex = self.dev['ifindex']
# resolve remote interface name
self.remote_ifname = self.resolve_remote_ifc()
self.remote_dev = ip("-d link show dev " + self.remote_ifname,
host=self.remote, json=True)[0]
self.remote_ifindex = self.remote_dev['ifindex']
self._required_cmd = {}
def create_local(self):
self._netns = NetNS()
self._ns = NetdevSimDev()
self._ns_peer = NetdevSimDev(ns=self._netns)
with open("/proc/self/ns/net") as nsfd0, \
open("/var/run/netns/" + self._netns.name) as nsfd1:
ifi0 = self._ns.nsims[0].ifindex
ifi1 = self._ns_peer.nsims[0].ifindex
NetdevSimDev.ctrl_write('link_device',
f'{nsfd0.fileno()}:{ifi0} {nsfd1.fileno()}:{ifi1}')
ip(f" addr add dev {self._ns.nsims[0].ifname} {self.nsim_v4_pfx}1/24")
ip(f"-6 addr add dev {self._ns.nsims[0].ifname} {self.nsim_v6_pfx}1/64 nodad")
ip(f" link set dev {self._ns.nsims[0].ifname} up")
ip(f" addr add dev {self._ns_peer.nsims[0].ifname} {self.nsim_v4_pfx}2/24", ns=self._netns)
ip(f"-6 addr add dev {self._ns_peer.nsims[0].ifname} {self.nsim_v6_pfx}2/64 nodad", ns=self._netns)
ip(f" link set dev {self._ns_peer.nsims[0].ifname} up", ns=self._netns)
def _check_env(self):
vars_needed = [
["LOCAL_V4", "LOCAL_V6"],
["REMOTE_V4", "REMOTE_V6"],
["REMOTE_TYPE"],
["REMOTE_ARGS"]
]
missing = []
for choice in vars_needed:
for entry in choice:
if entry in self.env:
break
else:
missing.append(choice)
# Make sure v4 / v6 configs are symmetric
if ("LOCAL_V6" in self.env) != ("REMOTE_V6" in self.env):
missing.append(["LOCAL_V6", "REMOTE_V6"])
if ("LOCAL_V4" in self.env) != ("REMOTE_V4" in self.env):
missing.append(["LOCAL_V4", "REMOTE_V4"])
if missing:
raise Exception("Invalid environment, missing configuration:", missing,
"Please see tools/testing/selftests/drivers/net/README.rst")
def resolve_remote_ifc(self):
v4 = v6 = None
if self.remote_addr_v["4"]:
v4 = ip("addr show to " + self.remote_addr_v["4"], json=True, host=self.remote)
if self.remote_addr_v["6"]:
v6 = ip("addr show to " + self.remote_addr_v["6"], json=True, host=self.remote)
if v4 and v6 and v4[0]["ifname"] != v6[0]["ifname"]:
raise Exception("Can't resolve remote interface name, v4 and v6 don't match")
if (v4 and len(v4) > 1) or (v6 and len(v6) > 1):
raise Exception("Can't resolve remote interface name, multiple interfaces match")
return v6[0]["ifname"] if v6 else v4[0]["ifname"]
def __del__(self):
if self._ns:
self._ns.remove()
self._ns = None
if self._ns_peer:
self._ns_peer.remove()
self._ns_peer = None
if self._netns:
del self._netns
self._netns = None
if self.remote:
del self.remote
self.remote = None
def require_ipver(self, ipver):
if not self.addr_v[ipver] or not self.remote_addr_v[ipver]:
raise KsftSkipEx(f"Test requires IPv{ipver} connectivity")
def require_nsim(self, nsim_test=True):
"""Require or exclude netdevsim for this test"""
if nsim_test and self._ns is None:
raise KsftXfailEx("Test only works on netdevsim")
if nsim_test is False and self._ns is not None:
raise KsftXfailEx("Test does not work on netdevsim")
def get_local_nsim_dev(self):
"""Returns the local netdevsim device or None.
Using this method is discouraged, as it makes tests nsim-specific.
Standard interfaces available on all HW should ideally be used.
This method is intended for the few cases where nsim-specific
assertions need to be verified which cannot be verified otherwise.
"""
return self._ns
def _require_cmd(self, comm, key, host=None):
cached = self._required_cmd.get(comm, {})
if cached.get(key) is None:
cached[key] = cmd("command -v -- " + comm, fail=False,
shell=True, host=host).ret == 0
self._required_cmd[comm] = cached
return cached[key]
def require_cmd(self, comm, local=True, remote=False):
if local:
if not self._require_cmd(comm, "local"):
raise KsftSkipEx("Test requires command: " + comm)
if remote:
if not self._require_cmd(comm, "remote", host=self.remote):
raise KsftSkipEx("Test requires (remote) command: " + comm)
def wait_hw_stats_settle(self):
"""
Wait for HW stats to become consistent, some devices DMA HW stats
periodically so events won't be reflected until next sync.
Good drivers will tell us via ethtool what their sync period is.
"""
if self._stats_settle_time is None:
data = {}
try:
data = ethtool("-c " + self.ifname, json=True)[0]
except CmdExitFailure as e:
if "Operation not supported" not in e.cmd.stderr:
raise
self._stats_settle_time = \
1.25 * data.get('stats-block-usecs', 20000) / 1000 / 1000
time.sleep(self._stats_settle_time)
class NetDrvContEnv(NetDrvEpEnv):
"""
Class for an environment with a netkit pair setup for forwarding traffic
between the physical interface and a network namespace.
NETIF = "eth0"
LOCAL_V6 = "2001:db8:1::1"
REMOTE_V6 = "2001:db8:1::2"
LOCAL_PREFIX_V6 = "2001:db8:2::0/64"
+-----------------------------+ +------------------------------+
dst | INIT NS | | TEST NS |
2001: | +---------------+ | | |
db8:2::2| | NETIF | | bpf | |
+---|>| 2001:db8:1::1 | |redirect| +-------------------------+ |
| | | |-----------|--------|>| Netkit | |
| | +---------------+ | _peer | | nk_guest | |
| | +-------------+ Netkit pair | | | fe80::2/64 | |
| | | Netkit |.............|........|>| 2001:db8:2::2/64 | |
| | | nk_host | | | +-------------------------+ |
| | | fe80::1/64 | | | |
| | +-------------+ | | route: |
| | | | default |
| | route: | | via fe80::1 dev nk_guest |
| | 2001:db8:2::2/128 | +------------------------------+
| | via fe80::2 dev nk_host |
| +-----------------------------+
|
| +---------------+
| | REMOTE |
+---| 2001:db8:1::2 |
+---------------+
"""
def __init__(self, src_path, rxqueues=1, primary_rx_redirect=False,
userns=False, **kwargs):
self.netns = None
self._userns = userns
self.nk_host_ifname = None
self.nk_guest_ifname = None
self._tc_clsact_added = False
self._tc_attached = False
self._primary_rx_redirect_attached = False
self._primary_rx_redirect_clsact_added = False
self._bpf_prog_pref = None
self._bpf_prog_id = None
self._init_ns_attached = False
self._remote_route_added = False
self._old_fwd = None
self._old_accept_ra = None
super().__init__(src_path, **kwargs)
self.require_ipver("6")
local_prefix = self.env.get("LOCAL_PREFIX_V6")
if not local_prefix:
raise KsftSkipEx("LOCAL_PREFIX_V6 required")
net = ipaddress.IPv6Network(local_prefix, strict=False)
self.ipv6_prefix = str(net.network_address)
self.nk_host_ipv6 = f"{self.ipv6_prefix}2:1"
self.nk_guest_ipv6 = f"{self.ipv6_prefix}2:2"
local_v6 = ipaddress.IPv6Address(self.addr_v["6"])
if local_v6 in net:
raise KsftSkipEx("LOCAL_V6 must not fall within LOCAL_PREFIX_V6")
rtnl = RtnlFamily()
rtnl.newlink(
{
"linkinfo": {
"kind": "netkit",
"data": {
"mode": "l2",
"policy": "forward",
"peer-policy": "forward",
},
},
"num-rx-queues": rxqueues,
},
flags=[Netlink.NLM_F_CREATE, Netlink.NLM_F_EXCL],
)
all_links = ip("-d link show", json=True)
netkit_links = [link for link in all_links
if link.get('linkinfo', {}).get('info_kind') == 'netkit'
and 'UP' not in link.get('flags', [])]
if len(netkit_links) != 2:
raise KsftSkipEx("Failed to create netkit pair")
netkit_links.sort(key=lambda x: x['ifindex'])
self.nk_host_ifname = netkit_links[1]['ifname']
self.nk_guest_ifname = netkit_links[0]['ifname']
self.nk_host_ifindex = netkit_links[1]['ifindex']
self.nk_guest_ifindex = netkit_links[0]['ifindex']
self._setup_ns()
self.attach_bpf()
if primary_rx_redirect:
self._attach_primary_rx_redirect_bpf()
def __del__(self):
if self._primary_rx_redirect_attached:
cmd(f"tc filter del dev {self.nk_host_ifname} ingress", fail=False)
self._primary_rx_redirect_attached = False
if self._primary_rx_redirect_clsact_added:
cmd(f"tc qdisc del dev {self.nk_host_ifname} clsact", fail=False)
self._primary_rx_redirect_clsact_added = False
if self._tc_attached:
cmd(f"tc filter del dev {self.ifname} ingress pref {self._bpf_prog_pref}")
self._tc_attached = False
if self._tc_clsact_added:
cmd(f"tc qdisc del dev {self.ifname} clsact")
self._tc_clsact_added = False
if self._remote_route_added:
cmd(f"ip -6 route del {self.nk_guest_ipv6}/128",
host=self.remote, fail=False)
self._remote_route_added = False
if self.nk_host_ifname:
cmd(f"ip link del dev {self.nk_host_ifname}")
self.nk_host_ifname = None
self.nk_guest_ifname = None
if self._init_ns_attached:
cmd("ip netns del init", fail=False)
self._init_ns_attached = False
if self.netns:
del self.netns
self.netns = None
if self._old_fwd is not None:
with open("/proc/sys/net/ipv6/conf/all/forwarding", "w",
encoding="utf-8") as f:
f.write(self._old_fwd)
self._old_fwd = None
if self._old_accept_ra is not None:
with open("/proc/sys/net/ipv6/conf/all/accept_ra", "w",
encoding="utf-8") as f:
f.write(self._old_accept_ra)
self._old_accept_ra = None
super().__del__()
def _setup_ns(self):
fwd_path = "/proc/sys/net/ipv6/conf/all/forwarding"
ra_path = "/proc/sys/net/ipv6/conf/all/accept_ra"
with open(fwd_path, encoding="utf-8") as f:
self._old_fwd = f.read().strip()
with open(ra_path, encoding="utf-8") as f:
self._old_accept_ra = f.read().strip()
with open(fwd_path, "w", encoding="utf-8") as f:
f.write("1")
with open(ra_path, "w", encoding="utf-8") as f:
f.write("2")
self.netns = UserNetNS() if self._userns else NetNS()
cmd("ip netns attach init 1")
self._init_ns_attached = True
ip("netns set init 0", ns=self.netns)
ip(f"link set dev {self.nk_guest_ifname} netns {self.netns.name}")
nk_guest_dev = ip(f"link show dev {self.nk_guest_ifname}",
json=True, ns=self.netns)[0]
self.nk_guest_ifindex = nk_guest_dev['ifindex']
ip(f"link set dev {self.nk_host_ifname} up")
ip(f"-6 addr add fe80::1/64 dev {self.nk_host_ifname} nodad")
ip(f"-6 route add {self.nk_guest_ipv6}/128 via fe80::2 dev {self.nk_host_ifname}")
ip("link set lo up", ns=self.netns)
ip(f"link set dev {self.nk_guest_ifname} up", ns=self.netns)
ip(f"-6 addr add fe80::2/64 dev {self.nk_guest_ifname}", ns=self.netns)
ip(f"-6 addr add {self.nk_guest_ipv6}/64 dev {self.nk_guest_ifname} nodad", ns=self.netns)
ip(f"-6 route add default via fe80::1 dev {self.nk_guest_ifname}", ns=self.netns)
def _tc_ensure_clsact(self, ifname=None):
"""Ensure a clsact qdisc exists on @ifname.
Returns True if this call added the qdisc, otherwise returns False.
"""
if ifname is None:
ifname = self.ifname
qdisc = json.loads(cmd(f"tc -j qdisc show dev {ifname}").stdout)
for q in qdisc:
if q['kind'] == 'clsact':
return False
cmd(f"tc qdisc add dev {ifname} clsact")
return True
def _get_bpf_prog_ids(self):
filters = json.loads(cmd(f"tc -j filter show dev {self.ifname} ingress").stdout)
for bpf in filters:
if 'options' not in bpf:
continue
if bpf['options']['bpf_name'].startswith('nk_forward.bpf'):
return (bpf['pref'], bpf['options']['prog']['id'])
raise Exception("Failed to get BPF prog ID")
def _find_bss_map_id(self, prog_id):
"""Find the .bss map ID for a loaded BPF program."""
prog_info = bpftool(f"prog show id {prog_id}", json=True)
for map_id in prog_info.get("map_ids", []):
map_info = bpftool(f"map show id {map_id}", json=True)
if map_info.get("name", "").endswith("bss"):
return map_id
raise Exception(f"Failed to find .bss map for prog {prog_id}")
def _find_bpf_obj(self, name):
bpf_obj = self.test_dir / name
if bpf_obj.exists():
return bpf_obj
bpf_obj = self.test_dir / "hw" / name
if bpf_obj.exists():
return bpf_obj
return None
def detach_bpf(self):
if self._tc_attached:
cmd(f"tc filter del dev {self.ifname} ingress pref "
f"{self._bpf_prog_pref}", fail=False)
self._tc_attached = False
def attach_bpf(self):
bpf_obj = self._find_bpf_obj("nk_forward.bpf.o")
if not bpf_obj:
raise KsftSkipEx("BPF prog nk_forward.bpf.o not found")
if self._tc_ensure_clsact():
self._tc_clsact_added = True
cmd(f"tc filter add dev {self.ifname} ingress bpf obj {bpf_obj}"
" sec tc/ingress direct-action")
self._tc_attached = True
(self._bpf_prog_pref, self._bpf_prog_id) = self._get_bpf_prog_ids()
bss_map_id = self._find_bss_map_id(self._bpf_prog_id)
ipv6_addr = ipaddress.IPv6Address(self.ipv6_prefix)
ipv6_bytes = ipv6_addr.packed
ifindex_bytes = self.nk_host_ifindex.to_bytes(4, byteorder='little')
value = ipv6_bytes + ifindex_bytes
value_hex = ' '.join(f'{b:02x}' for b in value)
bpftool(f"map update id {bss_map_id} key hex 00 00 00 00 value hex {value_hex}")
def _attach_primary_rx_redirect_bpf(self):
"""Attach BPF redirect program on the primary netkit ingress."""
bpf_obj = self._find_bpf_obj("nk_primary_rx_redirect.bpf.o")
if not bpf_obj:
raise KsftSkipEx("nk_primary_rx_redirect.bpf.o not found")
if self._tc_ensure_clsact(self.nk_host_ifname):
self._primary_rx_redirect_clsact_added = True
cmd(f"tc filter add dev {self.nk_host_ifname} ingress"
f" bpf obj {bpf_obj} sec tc/ingress direct-action")
self._primary_rx_redirect_attached = True
ip(f"-6 route add {self.nk_guest_ipv6}/128 via {self.addr_v['6']}",
host=self.remote)
self._remote_route_added = True
filters = json.loads(
cmd(f"tc -j filter show dev {self.nk_host_ifname} ingress").stdout)
redirect_prog_id = None
for bpf in filters:
if 'options' not in bpf:
continue
if bpf['options']['bpf_name'].startswith('nk_primary_rx_redirect'):
redirect_prog_id = bpf['options']['prog']['id']
break
if redirect_prog_id is None:
raise Exception("Failed to get primary RX redirect BPF prog ID")
bss_map_id = self._find_bss_map_id(redirect_prog_id)
phys_ifindex_bytes = self.ifindex.to_bytes(4, byteorder=sys.byteorder)
value_hex = ' '.join(f'{b:02x}' for b in phys_ifindex_bytes)
bpftool(f"map update id {bss_map_id} key hex 00 00 00 00 value hex {value_hex}")
@@ -0,0 +1,142 @@
# SPDX-License-Identifier: GPL-2.0
import re
import time
import json
from lib.py import ksft_pr, cmd, ip, rand_port, wait_port_listen
class Iperf3Runner:
"""
Sets up and runs iperf3 traffic.
"""
def __init__(self, env, port=None, server_ip=None, client_ip=None):
env.require_cmd("iperf3", local=True, remote=True)
self.env = env
self.port = rand_port() if port is None else port
self.server_ip = server_ip
self.client_ip = client_ip
def _build_server(self):
cmdline = f"iperf3 -s -1 -p {self.port}"
if self.server_ip:
cmdline += f" -B {self.server_ip}"
return cmdline
def _build_client(self, streams, duration, reverse):
host = self.env.addr if self.server_ip is None else self.server_ip
cmdline = f"iperf3 -c {host} -p {self.port} -P {streams} -t {duration} -J"
if self.client_ip:
cmdline += f" -B {self.client_ip}"
if reverse:
cmdline += " --reverse"
return cmdline
def start_server(self):
"""
Starts an iperf3 server with optional bind IP.
"""
cmdline = self._build_server()
proc = cmd(cmdline, background=True)
wait_port_listen(self.port)
time.sleep(0.1)
return proc
def start_client(self, background=False, streams=1, duration=10, reverse=False):
"""
Starts the iperf3 client with the configured options.
"""
cmdline = self._build_client(streams, duration, reverse)
kwargs = {"background": background, "host": self.env.remote}
if not background:
kwargs["timeout"] = duration + 5
return cmd(cmdline, **kwargs)
def measure_bandwidth(self, reverse=False):
"""
Runs an iperf3 measurement and returns the average bandwidth (Gbps).
Discards the first and last few reporting intervals and uses only the
middle part of the run where throughput is typically stable.
"""
self.start_server()
result = self.start_client(duration=10, reverse=reverse)
if result.ret != 0:
raise RuntimeError("iperf3 failed to run successfully")
try:
out = json.loads(result.stdout)
except json.JSONDecodeError as exc:
raise ValueError("Failed to parse iperf3 JSON output") from exc
intervals = out.get("intervals", [])
samples = [i["sum"]["bits_per_second"] / 1e9 for i in intervals]
if len(samples) < 10:
raise ValueError(f"iperf3 returned too few intervals: {len(samples)}")
# Discard potentially unstable first and last 3 seconds.
stable = samples[3:-3]
avg = sum(stable) / len(stable)
return avg
class GenerateTraffic:
def __init__(self, env, port=None):
self.env = env
self.runner = Iperf3Runner(env, port)
self._iperf_server = self.runner.start_server()
self._iperf_client = self.runner.start_client(background=True, streams=16, duration=86400)
# Wait for traffic to ramp up
if not self._wait_pkts(pps=1000):
self.stop(verbose=True)
raise Exception("iperf3 traffic did not ramp up")
def _wait_pkts(self, pkt_cnt=None, pps=None):
"""
Wait until we've seen pkt_cnt or until traffic ramps up to pps.
Only one of pkt_cnt or pss can be specified.
"""
pkt_start = ip("-s link show dev " + self.env.ifname, json=True)[0]["stats64"]["rx"]["packets"]
for _ in range(50):
time.sleep(0.1)
pkt_now = ip("-s link show dev " + self.env.ifname, json=True)[0]["stats64"]["rx"]["packets"]
if pps:
if pkt_now - pkt_start > pps / 10:
return True
pkt_start = pkt_now
elif pkt_cnt:
if pkt_now - pkt_start > pkt_cnt:
return True
return False
def wait_pkts_and_stop(self, pkt_cnt):
failed = not self._wait_pkts(pkt_cnt=pkt_cnt)
self.stop(verbose=failed)
def stop(self, verbose=None):
self._iperf_client.process(terminate=True)
if verbose:
ksft_pr(">> Client:")
ksft_pr(self._iperf_client.stdout)
ksft_pr(self._iperf_client.stderr)
self._iperf_server.process(terminate=True)
if verbose:
ksft_pr(">> Server:")
ksft_pr(self._iperf_server.stdout)
ksft_pr(self._iperf_server.stderr)
self._wait_client_stopped()
def _wait_client_stopped(self, sleep=0.005, timeout=5):
end = time.monotonic() + timeout
live_port_pattern = re.compile(fr":{self.runner.port:04X} 0[^6] ")
while time.monotonic() < end:
data = cmd("cat /proc/net/tcp*", host=self.env.remote).stdout
if not live_port_pattern.search(data):
return
time.sleep(sleep)
raise Exception(f"Waiting for client to stop timed out after {timeout}s")
@@ -0,0 +1,15 @@
# SPDX-License-Identifier: GPL-2.0
import os
import importlib
_modules = {}
def Remote(kind, args, src_path):
global _modules
if kind not in _modules:
_modules[kind] = importlib.import_module("..remote_" + kind, __name__)
dir_path = os.path.abspath(src_path + "/../")
return getattr(_modules[kind], "Remote")(args, dir_path)
@@ -0,0 +1,21 @@
# SPDX-License-Identifier: GPL-2.0
import os
import subprocess
from lib.py import cmd
class Remote:
def __init__(self, name, dir_path):
self.name = name
self.dir_path = dir_path
def cmd(self, comm):
return subprocess.Popen(["ip", "netns", "exec", self.name, "bash", "-c", comm],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def deploy(self, what):
if os.path.isabs(what):
return what
return os.path.abspath(self.dir_path + "/" + what)
@@ -0,0 +1,39 @@
# SPDX-License-Identifier: GPL-2.0
import os
import string
import subprocess
import random
from lib.py import cmd
class Remote:
def __init__(self, name, dir_path):
self.name = name
self.dir_path = dir_path
self._tmpdir = None
def __del__(self):
if self._tmpdir:
cmd("rm -rf " + self._tmpdir, host=self)
self._tmpdir = None
def cmd(self, comm):
return subprocess.Popen(["ssh", "-q", self.name, comm],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def _mktmp(self):
return ''.join(random.choice(string.ascii_lowercase) for _ in range(8))
def deploy(self, what):
if not self._tmpdir:
self._tmpdir = "/tmp/" + self._mktmp()
cmd("mkdir " + self._tmpdir, host=self)
file_name = self._tmpdir + "/" + self._mktmp() + os.path.basename(what)
if not os.path.isabs(what):
what = os.path.abspath(self.dir_path + "/" + what)
cmd(f"scp {what} {self.name}:{file_name}")
return file_name
@@ -0,0 +1,444 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-2.0
# This file contains functions and helpers to support the netconsole
# selftests
#
# Author: Breno Leitao <leitao@debian.org>
set -euo pipefail
LIBDIR=$(dirname "$(readlink -e "${BASH_SOURCE[0]}")")
SRCIF="" # to be populated later
SRCIP="" # to be populated later
SRCIP4="192.0.2.1"
SRCIP6="fc00::1"
DSTIF="" # to be populated later
DSTIP="" # to be populated later
DSTIP4="192.0.2.2"
DSTIP6="fc00::2"
PORT="6666"
MSG="netconsole selftest"
USERDATA_KEY="key"
USERDATA_VALUE="value"
TARGET=$(mktemp -u netcons_XXXXX)
DEFAULT_PRINTK_VALUES=$(cat /proc/sys/kernel/printk)
NETCONS_CONFIGFS="/sys/kernel/config/netconsole"
NETCONS_PATH="${NETCONS_CONFIGFS}"/"${TARGET}"
# NAMESPACE will be populated by setup_ns with a random value
NAMESPACE=""
# IDs for netdevsim. We either use NSIM_DEV_{1,2}_ID for standard test
# or NSIM_BOND_{T,R}X_{1,2} for the bonding tests. Not both at the
# same time.
NSIM_DEV_1_ID=$((256 + RANDOM % 256))
NSIM_DEV_2_ID=$((512 + RANDOM % 256))
NSIM_BOND_TX_1=$((768 + RANDOM % 256))
NSIM_BOND_TX_2=$((1024 + RANDOM % 256))
NSIM_BOND_RX_1=$((1280 + RANDOM % 256))
NSIM_BOND_RX_2=$((1536 + RANDOM % 256))
NSIM_DEV_SYS_NEW="/sys/bus/netdevsim/new_device"
NSIM_DEV_SYS_LINK="/sys/bus/netdevsim/link_device"
# Used to create and delete namespaces
source "${LIBDIR}"/../../../../net/lib.sh
# Create netdevsim interfaces
create_ifaces() {
echo "$NSIM_DEV_2_ID" > "$NSIM_DEV_SYS_NEW"
echo "$NSIM_DEV_1_ID" > "$NSIM_DEV_SYS_NEW"
udevadm settle 2> /dev/null || true
local NSIM1=/sys/bus/netdevsim/devices/netdevsim"$NSIM_DEV_1_ID"
local NSIM2=/sys/bus/netdevsim/devices/netdevsim"$NSIM_DEV_2_ID"
# These are global variables
SRCIF=$(find "$NSIM1"/net -maxdepth 1 -type d ! \
-path "$NSIM1"/net -exec basename {} \;)
DSTIF=$(find "$NSIM2"/net -maxdepth 1 -type d ! \
-path "$NSIM2"/net -exec basename {} \;)
}
link_ifaces() {
local NSIM_DEV_SYS_LINK="/sys/bus/netdevsim/link_device"
local SRCIF_IFIDX=$(cat /sys/class/net/"$SRCIF"/ifindex)
local DSTIF_IFIDX=$(cat /sys/class/net/"$DSTIF"/ifindex)
exec {NAMESPACE_FD}</var/run/netns/"${NAMESPACE}"
exec {INITNS_FD}</proc/self/ns/net
# Bind the dst interface to namespace
ip link set "${DSTIF}" netns "${NAMESPACE}"
# Linking one device to the other one (on the other namespace}
if ! echo "${INITNS_FD}:$SRCIF_IFIDX $NAMESPACE_FD:$DSTIF_IFIDX" > $NSIM_DEV_SYS_LINK
then
echo "linking netdevsim1 with netdevsim2 should succeed"
cleanup
exit "${ksft_skip}"
fi
}
function configure_ip() {
# Configure the IPs for both interfaces
ip netns exec "${NAMESPACE}" ip addr add "${DSTIP}"/24 dev "${DSTIF}"
ip netns exec "${NAMESPACE}" ip link set "${DSTIF}" up
ip addr add "${SRCIP}"/24 dev "${SRCIF}"
ip link set "${SRCIF}" up
}
function select_ipv4_or_ipv6()
{
local VERSION=${1}
if [[ "$VERSION" == "ipv6" ]]
then
DSTIP="${DSTIP6}"
SRCIP="${SRCIP6}"
else
DSTIP="${DSTIP4}"
SRCIP="${SRCIP4}"
fi
}
function set_network() {
local IP_VERSION=${1:-"ipv4"}
# setup_ns function is coming from lib.sh
setup_ns NAMESPACE
# Create both interfaces, and assign the destination to a different
# namespace
create_ifaces
# Link both interfaces back to back
link_ifaces
select_ipv4_or_ipv6 "${IP_VERSION}"
configure_ip
}
function _create_dynamic_target() {
local FORMAT="${1:?FORMAT parameter required}"
local NCPATH="${2:?NCPATH parameter required}"
DSTMAC=$(ip netns exec "${NAMESPACE}" \
ip link show "${DSTIF}" | awk '/ether/ {print $2}')
# Create a dynamic target
mkdir "${NCPATH}"
echo "${DSTIP}" > "${NCPATH}"/remote_ip
echo "${SRCIP}" > "${NCPATH}"/local_ip
echo "${DSTMAC}" > "${NCPATH}"/remote_mac
echo "${SRCIF}" > "${NCPATH}"/dev_name
if [ "${FORMAT}" == "basic" ]
then
# Basic target does not support release
echo 0 > "${NCPATH}"/release
echo 0 > "${NCPATH}"/extended
elif [ "${FORMAT}" == "extended" ]
then
echo 1 > "${NCPATH}"/extended
fi
}
function create_dynamic_target() {
local FORMAT=${1:-"extended"}
local NCPATH=${2:-"$NETCONS_PATH"}
_create_dynamic_target "${FORMAT}" "${NCPATH}"
echo 1 > "${NCPATH}"/enabled
# This will make sure that the kernel was able to
# load the netconsole driver configuration. The console message
# gets more organized/sequential as well.
sleep 1
}
# Generate the command line argument for netconsole following:
# netconsole=[+][src-port]@[src-ip]/[<dev>],[tgt-port]@<tgt-ip>/[tgt-macaddr]
function create_cmdline_str() {
local BINDMODE=${1:-"ifname"}
if [ "${BINDMODE}" == "ifname" ]
then
SRCDEV=${SRCIF}
else
SRCDEV=$(mac_get "${SRCIF}")
fi
DSTMAC=$(ip netns exec "${NAMESPACE}" \
ip link show "${DSTIF}" | awk '/ether/ {print $2}')
SRCPORT="1514"
TGTPORT="6666"
echo "netconsole=\"+${SRCPORT}@${SRCIP}/${SRCDEV},${TGTPORT}@${DSTIP}/${DSTMAC}\""
}
# Do not append the release to the header of the message
function disable_release_append() {
echo 0 > "${NETCONS_PATH}"/enabled
echo 0 > "${NETCONS_PATH}"/release
echo 1 > "${NETCONS_PATH}"/enabled
}
function do_cleanup() {
local NSIM_DEV_SYS_DEL="/sys/bus/netdevsim/del_device"
# Delete netdevsim devices
echo "$NSIM_DEV_2_ID" > "$NSIM_DEV_SYS_DEL"
echo "$NSIM_DEV_1_ID" > "$NSIM_DEV_SYS_DEL"
# this is coming from lib.sh
cleanup_all_ns
# Restoring printk configurations
echo "${DEFAULT_PRINTK_VALUES}" > /proc/sys/kernel/printk
}
function cleanup_netcons() {
# delete netconsole dynamic reconfiguration
# do not fail if the target is already disabled
local TARGET_PATH=${1:-${NETCONS_PATH}}
if [[ ! -d "${TARGET_PATH}" ]]
then
# in some cases this is called before netcons path is created
return
fi
if [[ $(cat "${TARGET_PATH}"/enabled) != 0 ]]
then
echo 0 > "${TARGET_PATH}"/enabled || true
fi
# Remove all the keys that got created during the selftest
find "${TARGET_PATH}/userdata/" -mindepth 1 -type d -delete
# Remove the configfs entry
rmdir "${TARGET_PATH}"
}
function cleanup() {
cleanup_netcons
do_cleanup
}
function set_user_data() {
if [[ ! -d "${NETCONS_PATH}""/userdata" ]]
then
echo "Userdata path not available in ${NETCONS_PATH}/userdata"
exit "${ksft_skip}"
fi
KEY_PATH="${NETCONS_PATH}/userdata/${USERDATA_KEY}"
mkdir -p "${KEY_PATH}"
VALUE_PATH="${KEY_PATH}""/value"
echo "${USERDATA_VALUE}" > "${VALUE_PATH}"
}
function listen_port_and_save_to() {
local OUTPUT=${1}
local IPVERSION=${2:-"ipv4"}
if [ "${IPVERSION}" == "ipv4" ]
then
SOCAT_MODE="UDP-LISTEN"
else
SOCAT_MODE="UDP6-LISTEN"
fi
# Just wait for 3 seconds
timeout 3 ip netns exec "${NAMESPACE}" \
socat "${SOCAT_MODE}":"${PORT}",fork,shut-none "${OUTPUT}" 2> /dev/null
}
# Only validate that the message arrived properly
function validate_msg() {
local TMPFILENAME="$1"
# Check if the file exists
if [ ! -f "$TMPFILENAME" ]; then
echo "FAIL: File was not generated." >&2
exit "${ksft_fail}"
fi
if ! grep -q "${MSG}" "${TMPFILENAME}"; then
echo "FAIL: ${MSG} not found in ${TMPFILENAME}" >&2
cat "${TMPFILENAME}" >&2
exit "${ksft_fail}"
fi
}
# Validate the message and userdata
function validate_result() {
local TMPFILENAME="$1"
# TMPFILENAME will contain something like:
# 6.11.1-0_fbk0_rc13_509_g30d75cea12f7,13,1822,115075213798,-;netconsole selftest: netcons_gtJHM
# key=value
validate_msg "${TMPFILENAME}"
# userdata is not supported on basic format target,
# thus, do not validate it.
if [ "${FORMAT}" != "basic" ];
then
if ! grep -q "${USERDATA_KEY}=${USERDATA_VALUE}" "${TMPFILENAME}"; then
echo "FAIL: ${USERDATA_KEY}=${USERDATA_VALUE} not found in ${TMPFILENAME}" >&2
cat "${TMPFILENAME}" >&2
exit "${ksft_fail}"
fi
fi
# Delete the file once it is validated, otherwise keep it
# for debugging purposes
rm "${TMPFILENAME}"
}
function check_for_dependencies() {
if [ "$(id -u)" -ne 0 ]; then
echo "This test must be run as root" >&2
exit "${ksft_skip}"
fi
if ! which socat > /dev/null ; then
echo "SKIP: socat(1) is not available" >&2
exit "${ksft_skip}"
fi
if ! which ip > /dev/null ; then
echo "SKIP: ip(1) is not available" >&2
exit "${ksft_skip}"
fi
if ! which udevadm > /dev/null ; then
echo "SKIP: udevadm(1) is not available" >&2
exit "${ksft_skip}"
fi
if [ ! -f /proc/net/if_inet6 ]; then
echo "SKIP: IPv6 not configured. Check if CONFIG_IPV6 is enabled" >&2
exit "${ksft_skip}"
fi
if [ ! -f "${NSIM_DEV_SYS_NEW}" ]; then
echo "SKIP: file ${NSIM_DEV_SYS_NEW} does not exist. Check if CONFIG_NETDEVSIM is enabled" >&2
exit "${ksft_skip}"
fi
if [ ! -d "${NETCONS_CONFIGFS}" ]; then
echo "SKIP: directory ${NETCONS_CONFIGFS} does not exist. Check if NETCONSOLE_DYNAMIC is enabled" >&2
exit "${ksft_skip}"
fi
if ip link show "${DSTIF}" 2> /dev/null; then
echo "SKIP: interface ${DSTIF} exists in the system. Not overwriting it." >&2
exit "${ksft_skip}"
fi
REGEXP4="inet.*(${SRCIP4}|${DSTIP4})"
REGEXP6="inet.*(${SRCIP6}|${DSTIP6})"
if ip addr list | grep -E "${REGEXP4}" 2> /dev/null; then
echo "SKIP: IPv4s already in use. Skipping it" >&2
exit "${ksft_skip}"
fi
if ip addr list | grep -E "${REGEXP6}" 2> /dev/null; then
echo "SKIP: IPv6s already in use. Skipping it" >&2
exit "${ksft_skip}"
fi
}
function check_for_taskset() {
if ! which taskset > /dev/null ; then
echo "SKIP: taskset(1) is not available" >&2
exit "${ksft_skip}"
fi
}
# This is necessary if running multiple tests in a row
function pkill_socat() {
PROCESS_NAME4="socat UDP-LISTEN:6666,fork,shut-none ${OUTPUT_FILE}"
PROCESS_NAME6="socat UDP6-LISTEN:6666,fork,shut-none ${OUTPUT_FILE}"
# socat runs under timeout(1), kill it if it is still alive
# do not fail if socat doesn't exist anymore
set +e
pkill -f "${PROCESS_NAME4}"
pkill -f "${PROCESS_NAME6}"
set -e
}
# Check if netconsole was compiled as a module, otherwise exit
function check_netconsole_module() {
if modinfo netconsole | grep filename: | grep -q builtin
then
echo "SKIP: netconsole should be compiled as a module" >&2
exit "${ksft_skip}"
fi
}
function wait_target_state() {
local TARGET=${1}
local STATE=${2}
local TARGET_PATH="${NETCONS_CONFIGFS}"/"${TARGET}"
local ENABLED=0
if [ "${STATE}" == "enabled" ]
then
ENABLED=1
fi
if [ ! -d "$TARGET_PATH" ]; then
echo "FAIL: Target does not exist." >&2
exit "${ksft_fail}"
fi
local CHECK_CMD="grep \"$ENABLED\" \"$TARGET_PATH/enabled\""
slowwait 2 sh -c "test -n \"\$($CHECK_CMD)\"" || {
echo "FAIL: ${TARGET} is not ${STATE}." >&2
exit "${ksft_fail}"
}
}
# A wrapper to translate protocol version to udp version
function wait_for_port() {
local NAMESPACE=${1}
local PORT=${2}
IP_VERSION=${3}
if [ "${IP_VERSION}" == "ipv6" ]
then
PROTOCOL="udp6"
else
PROTOCOL="udp"
fi
wait_local_port_listen "${NAMESPACE}" "${PORT}" "${PROTOCOL}"
# even after the port is open, let's wait 1 second before writing
# otherwise the packet could be missed, and the test will fail. Happens
# more frequently on IPv6
sleep 1
}
# Clean up netdevsim ifaces created for bonding test
function cleanup_bond_nsim() {
ip -n "${TXNS}" \
link delete "${BOND_TX_MAIN_IF}" type bond || true
ip -n "${RXNS}" \
link delete "${BOND_RX_MAIN_IF}" type bond || true
cleanup_netdevsim "$NSIM_BOND_TX_1"
cleanup_netdevsim "$NSIM_BOND_TX_2"
cleanup_netdevsim "$NSIM_BOND_RX_1"
cleanup_netdevsim "$NSIM_BOND_RX_2"
}
# cleanup tests that use bonding interfaces
function cleanup_bond() {
cleanup_netcons
cleanup_bond_nsim
cleanup_all_ns
ip link delete "${VETH0}" || true
}

Some files were not shown because too many files have changed in this diff Show More