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,2 @@
include.sh
getsockopt
@@ -0,0 +1,23 @@
# SPDX-License-Identifier: GPL-2.0
all:
@echo mk_build_dir="$(shell pwd)" > include.sh
TEST_PROGS := rds_run.sh
TEST_GEN_PROGS := getsockopt
TEST_FILES := \
include.sh \
settings \
test.py \
# end of TEST_FILES
EXTRA_CLEAN := \
include.sh \
/tmp/rds_logs \
# end of EXTRA_CLEAN
CFLAGS += $(KHDR_INCLUDES)
include ../../lib.mk
@@ -0,0 +1,77 @@
RDS self-tests
==============
These scripts provide a coverage test for RDS-TCP and RDS-RDMA (over
RoCE/RXE) by setting up two endpoints and running RDS packets between
them. The TCP path creates two network namespaces; the RDMA path uses
an RXE (soft RoCE) device backed by a veth pair. A workload of 50000
hashes, each 64 characters in size, is passed over an RDS socket on
this test network with an optional probability of packet loss or
corruption. A passing test means the RDS stack was able to recover
properly. The provided config.sh can be used to compile the kernel
with the necessary gcov options; pass -r to also enable the kernel
configs required for the RDMA transport. The kernel may optionally be
configured to omit the coverage report as well.
USAGE:
rds_run.sh [-d logdir] [-l packet_loss] [-c packet_corruption]
[-u packet_duplicate] [-t timeout]
[-T tcp|rdma|tcp,rdma]
OPTIONS:
-d Log directory. If set, logs will be stored in the
given dir, or skipped if unset. Log dir can also be
set through the RDS_LOG_DIR env variable
-l Simulates a percentage of packet loss
-c Simulates a percentage of packet corruption
-u Simulates a percentage of packet duplication.
-t Test timeout. Defaults to tools/testing/selftests/net/rds/settings
-T Comma-separated list of transports to test. Accepts
"tcp", "rdma", or "tcp,rdma". Defaults to "tcp". Use
config.sh -r to enable required RDMA configs
ENV VARIABLES:
RDS_LOG_DIR Log directory. If set, logs will be stored in
the given dir, or skipped if unset. Log dir
can also be set with the -d flag.
Use with --rwdir on the CI path to retain logs after
test compleation. Log dir end point must be within
the specified --rwdir path for logs to persist on
the host.
SUDO_USER The user name that should be used for tcpdump
--relinquish-privileges. Set this to a user
belonging to the sudoers group to avoid drop
privilege errors with the vng 9p filesystem
which may result in empty pcaps
EXAMPLE:
# Create a suitable gcov enabled .config
tools/testing/selftests/net/rds/config.sh -g
# Optionally add RDMA configs (CONFIG_RDS_RDMA, CONFIG_RDMA_RXE)
tools/testing/selftests/net/rds/config.sh -r
# Alternatly create a gcov disabled .config
tools/testing/selftests/net/rds/config.sh
# Config paths may also be specified with the -c flag
tools/testing/selftests/net/rds/config.sh -c .config.local
# build the kernel
vng --build --config .config
# launch the tests in a VM
vng -v --rwdir ./ --run . --user root --cpus 4 -- \
"export PYTHONPATH=tools/testing/selftests/net/; \
export SUDO_USER=example_user; \
export RDS_LOG_DIR=tools/testing/selftests/net/rds/rds_logs; \
tools/testing/selftests/net/rds/rds_run.sh -T tcp,rdma"
@@ -0,0 +1,5 @@
CONFIG_NET_NS=y
CONFIG_NET_SCH_NETEM=y
CONFIG_RDS=y
CONFIG_RDS_TCP=y
CONFIG_VETH=y
+68
View File
@@ -0,0 +1,68 @@
#! /bin/bash
# SPDX-License-Identifier: GPL-2.0
set -e
set -u
set -x
unset KBUILD_OUTPUT
CONF_FILE=""
FLAGS=()
GENERATE_GCOV_REPORT=0
ENABLE_RDMA=0
while getopts "gc:r" opt; do
case ${opt} in
g)
GENERATE_GCOV_REPORT=1
;;
c)
CONF_FILE=$OPTARG
;;
r)
ENABLE_RDMA=1
;;
:)
echo "USAGE: config.sh [-g] [-c config] [-r]"
exit 1
;;
?)
echo "Invalid option: -${OPTARG}."
exit 1
;;
esac
done
if [[ "$CONF_FILE" != "" ]]; then
FLAGS=(--file "$CONF_FILE")
fi
# enable RDS
scripts/config "${FLAGS[@]}" --enable CONFIG_RDS
scripts/config "${FLAGS[@]}" --enable CONFIG_RDS_TCP
if [ "$GENERATE_GCOV_REPORT" -eq 1 ]; then
# instrument RDS and only RDS
scripts/config "${FLAGS[@]}" --enable CONFIG_GCOV_KERNEL
scripts/config "${FLAGS[@]}" --disable GCOV_PROFILE_ALL
scripts/config "${FLAGS[@]}" --enable GCOV_PROFILE_RDS
else
scripts/config "${FLAGS[@]}" --disable CONFIG_GCOV_KERNEL
scripts/config "${FLAGS[@]}" --disable GCOV_PROFILE_ALL
scripts/config "${FLAGS[@]}" --disable GCOV_PROFILE_RDS
fi
# need network namespaces to run tests with veth network interfaces
scripts/config "${FLAGS[@]}" --enable CONFIG_NET_NS
scripts/config "${FLAGS[@]}" --enable CONFIG_VETH
# simulate packet loss
scripts/config "${FLAGS[@]}" --enable CONFIG_NET_SCH_NETEM
if [ "$ENABLE_RDMA" -eq 1 ]; then
# enable RDS over InfiniBand / RDMA (rds_rdma test)
scripts/config "${FLAGS[@]}" --enable CONFIG_INFINIBAND
scripts/config "${FLAGS[@]}" --enable CONFIG_INFINIBAND_ADDR_TRANS
scripts/config "${FLAGS[@]}" --enable CONFIG_RDMA_RXE
scripts/config "${FLAGS[@]}" --enable CONFIG_RDS_RDMA
fi
@@ -0,0 +1,208 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Exercise the RDS getsockopt() paths that were converted to the
* getsockopt_iter() / sockopt_t callback.
*
* Three distinct paths are covered:
*
* - RDS_RECVERR and SO_RDS_TRANSPORT, which now return their int value
* through copy_to_iter() and report the written length in opt->optlen.
*
* - RDS_INFO_*, which pins the userspace buffer with
* iov_iter_extract_pages() (including a non-zero starting page offset)
* and lets the info producers memcpy the snapshot in under a spinlock.
*
* The kvec (in-kernel buffer) -> -EOPNOTSUPP path of rds_info_getsockopt()
* is not reachable from a userspace getsockopt() and so is not tested here.
*/
#include <errno.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <linux/rds.h>
#include "../../kselftest_harness.h"
#ifndef AF_RDS
#define AF_RDS 21
#endif
FIXTURE(rds) {
int fd;
};
FIXTURE_SETUP(rds)
{
self->fd = socket(AF_RDS, SOCK_SEQPACKET, 0);
if (self->fd < 0)
SKIP(return, "AF_RDS unavailable (errno %d) - load the rds module",
errno);
}
FIXTURE_TEARDOWN(rds)
{
if (self->fd >= 0)
close(self->fd);
}
/* RDS_RECVERR defaults to 0 and is reported back as a 4-byte int. */
TEST_F(rds, recverr_default)
{
socklen_t len = sizeof(int);
int val = 0xdeadbeef;
ASSERT_EQ(0, getsockopt(self->fd, SOL_RDS, RDS_RECVERR, &val, &len));
EXPECT_EQ(sizeof(int), len);
EXPECT_EQ(0, val);
}
/* A value set via setsockopt() must be readable back unchanged. */
TEST_F(rds, recverr_set_get)
{
socklen_t len = sizeof(int);
int val = 1;
ASSERT_EQ(0, setsockopt(self->fd, SOL_RDS, RDS_RECVERR, &val, len));
val = 0;
ASSERT_EQ(0, getsockopt(self->fd, SOL_RDS, RDS_RECVERR, &val, &len));
EXPECT_EQ(sizeof(int), len);
EXPECT_EQ(1, val);
}
/* A buffer smaller than an int is rejected with EINVAL, not silently. */
TEST_F(rds, recverr_short_buffer)
{
socklen_t len = sizeof(int) - 1;
char buf[sizeof(int)];
EXPECT_EQ(-1, getsockopt(self->fd, SOL_RDS, RDS_RECVERR, buf, &len));
EXPECT_EQ(EINVAL, errno);
}
/* An unbound socket reports RDS_TRANS_NONE for SO_RDS_TRANSPORT. */
TEST_F(rds, transport_unbound)
{
socklen_t len = sizeof(int);
int val = 0;
ASSERT_EQ(0, getsockopt(self->fd, SOL_RDS, SO_RDS_TRANSPORT, &val,
&len));
EXPECT_EQ(sizeof(int), len);
EXPECT_EQ(RDS_TRANS_NONE, (unsigned int)val);
}
TEST_F(rds, transport_short_buffer)
{
socklen_t len = sizeof(int) - 1;
char buf[sizeof(int)];
EXPECT_EQ(-1, getsockopt(self->fd, SOL_RDS, SO_RDS_TRANSPORT, buf,
&len));
EXPECT_EQ(EINVAL, errno);
}
/*
* RDS_INFO_COUNTERS with a zero-length buffer is the "probe" call: it must
* fail with ENOSPC and report the required snapshot size in optlen.
*/
TEST_F(rds, info_counters_probe)
{
socklen_t len = 0;
EXPECT_EQ(-1, getsockopt(self->fd, SOL_RDS, RDS_INFO_COUNTERS, NULL,
&len));
EXPECT_EQ(ENOSPC, errno);
EXPECT_GT(len, 0);
/* The snapshot is an array of fixed-size counter records. */
EXPECT_EQ(0, len % (socklen_t)sizeof(struct rds_info_counter));
}
/*
* A real snapshot into an unaligned userspace buffer exercises the
* iov_iter_extract_pages() path, including the non-zero offset0 handling
* that the patch reworked. Place the buffer at a non-page-aligned address
* spanning into the next page to make sure multi-page pinning works too.
*/
TEST_F(rds, info_counters_snapshot)
{
struct rds_info_counter *ctr;
socklen_t need = 0, len;
long pagesz = sysconf(_SC_PAGESIZE);
size_t offset, map_len;
unsigned int i, n;
char *region, *buf;
int ret;
/* Probe for the required size. */
getsockopt(self->fd, SOL_RDS, RDS_INFO_COUNTERS, NULL, &need);
ASSERT_GT(need, 0);
/*
* Place the buffer at a non-page-aligned offset that runs past the
* first page boundary, and size the mapping from the probed length so
* the test keeps working if the counter set grows.
*/
offset = pagesz - 64;
map_len = ((offset + need + pagesz - 1) / pagesz) * pagesz;
region = mmap(NULL, map_len, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
ASSERT_NE(MAP_FAILED, region);
buf = region + offset;
/*
* On success the RDS_INFO path returns the positive per-element size
* (lens.each) rather than 0, and writes the full snapshot length back
* into optlen.
*/
len = need;
ret = getsockopt(self->fd, SOL_RDS, RDS_INFO_COUNTERS, buf, &len);
ASSERT_GE(ret, 0) {
TH_LOG("getsockopt snapshot failed: errno %d", errno);
}
EXPECT_EQ(sizeof(struct rds_info_counter), ret);
EXPECT_EQ(need, len);
/* The counter names must be NUL-terminated, non-empty strings. */
ctr = (struct rds_info_counter *)buf;
n = len / sizeof(*ctr);
ASSERT_GT(n, 0);
for (i = 0; i < n; i++) {
size_t namelen = strnlen((char *)ctr[i].name,
sizeof(ctr[i].name));
EXPECT_GT(namelen, 0);
EXPECT_LT(namelen, sizeof(ctr[i].name));
}
munmap(region, map_len);
}
/*
* A non-zero but too-small buffer must report ENOSPC and the full required
* length, without corrupting memory past the buffer.
*/
TEST_F(rds, info_counters_short_buffer)
{
socklen_t need = 0, len;
char small[sizeof(struct rds_info_counter)];
getsockopt(self->fd, SOL_RDS, RDS_INFO_COUNTERS, NULL, &need);
ASSERT_GT(need, 0);
/* Ask with a buffer guaranteed smaller than the full snapshot. */
if (need <= (socklen_t)sizeof(small))
SKIP(return, "snapshot fits in one record; nothing to test");
len = 1; /* < sizeof(struct rds_info_counter) */
EXPECT_EQ(-1, getsockopt(self->fd, SOL_RDS, RDS_INFO_COUNTERS, small,
&len));
EXPECT_EQ(ENOSPC, errno);
EXPECT_EQ(need, len);
}
TEST_HARNESS_MAIN
+319
View File
@@ -0,0 +1,319 @@
#! /bin/bash
# SPDX-License-Identifier: GPL-2.0
set -e
set -u
unset KBUILD_OUTPUT
current_dir="$(realpath "$(dirname "$0")")"
build_dir="$current_dir"
build_include="$current_dir/include.sh"
if test -f "$build_include"; then
# this include will define "$mk_build_dir" as the location the test was
# built. We will need this if the tests are installed in a location
# other than the kernel source
source "$build_include"
build_dir="$mk_build_dir"
fi
# Source settings for timeout value (also used by ksft runner)
source "$current_dir"/settings
# This test requires kernel source and the *.gcda data therein
# Locate the top level of the kernel source, and the net/rds
# subfolder with the appropriate *.gcno object files
ksrc_dir="$(realpath "$build_dir"/../../../../../)"
kconfig="$ksrc_dir/.config"
obj_dir="$ksrc_dir/net/rds"
GCOV_CMD=gcov
#check to see if the host has the required packages to generate a gcov report
check_gcov_env()
{
if ! which "$GCOV_CMD" > /dev/null 2>&1; then
echo "# Warning: Could not find gcov. "
GENERATE_GCOV_REPORT=0
return
fi
# the gcov version must match the gcc version
GCC_VER=$(gcc -dumpfullversion)
GCOV_VER=$($GCOV_CMD -v | grep gcov | awk '{print $3}'| awk 'BEGIN {FS="-"}{print $1}')
if [ "$GCOV_VER" != "$GCC_VER" ]; then
#attempt to find a matching gcov version
GCOV_CMD=gcov-$(gcc -dumpversion)
if ! which "$GCOV_CMD" > /dev/null 2>&1; then
echo "# Warning: Could not find an appropriate gcov installation. \
gcov version must match gcc version"
GENERATE_GCOV_REPORT=0
return
fi
#recheck version number of found gcov executable
GCOV_VER=$($GCOV_CMD -v | grep gcov | awk '{print $3}'| \
awk 'BEGIN {FS="-"}{print $1}')
if [ "$GCOV_VER" != "$GCC_VER" ]; then
echo "# Warning: Could not find an appropriate gcov installation. \
gcov version must match gcc version"
GENERATE_GCOV_REPORT=0
else
echo "# Warning: Mismatched gcc and gcov detected. Using $GCOV_CMD"
fi
fi
}
# Check to see if the kconfig has the required configs to generate a coverage report
check_gcov_conf()
{
if ! grep -x "CONFIG_GCOV_PROFILE_RDS=y" "$kconfig" > /dev/null 2>&1; then
echo "# INFO: CONFIG_GCOV_PROFILE_RDS should be enabled for coverage reports"
GENERATE_GCOV_REPORT=0
fi
if ! grep -x "CONFIG_GCOV_KERNEL=y" "$kconfig" > /dev/null 2>&1; then
echo "# INFO: CONFIG_GCOV_KERNEL should be enabled for coverage reports"
GENERATE_GCOV_REPORT=0
fi
if grep -x "CONFIG_GCOV_PROFILE_ALL=y" "$kconfig" > /dev/null 2>&1; then
echo "# INFO: CONFIG_GCOV_PROFILE_ALL should be disabled for coverage reports"
GENERATE_GCOV_REPORT=0
fi
if [ "$GENERATE_GCOV_REPORT" -eq 0 ]; then
echo "# To enable gcov reports, please run "\
"\"tools/testing/selftests/net/rds/config.sh -g\" and rebuild the kernel"
else
# if we have the required kernel configs, proceed to check the environment to
# ensure we have the required gcov packages
check_gcov_env
fi
}
# Checks if a kconfig is enabled (set to =y or =m)
# $1: kconfig symbol to check
# $2: (optional) module name backing $1
# Ex: check_conf_enabled CONFIG_RDS_TCP rds_tcp
# Modules for configs set to =m will be probed
# If omitted, only a built-in (=y) config is accepted.
# Returns on success. exits 4 on failure
# Kselftest framework requirement - SKIP code is 4.
check_conf_enabled() {
if grep -x "$1=y" "$kconfig" > /dev/null 2>&1; then
return
fi
if [ -n "${2:-}" ] && grep -x "$1=m" "$kconfig" > /dev/null 2>&1; then
probe_module "$2"
return
fi
echo "selftests: [SKIP] This test requires $1 enabled"
echo "Please run" \
"tools/testing/selftests/net/rds/config.sh and rebuild the kernel"
exit 4
}
check_rdma_conf_enabled() {
if grep -x "$1=y" "$kconfig" > /dev/null 2>&1; then
return
fi
if [ -n "${2:-}" ] && grep -x "$1=m" "$kconfig" > /dev/null 2>&1; then
probe_module "$2"
return
fi
echo "selftests: [XFAIL] rdma transport requires $1 enabled"
echo "To enable, run" \
"tools/testing/selftests/net/rds/config.sh -r and rebuild"
exit 2
}
# Load the module backing a config that is built as a loadable module
# (=m). Built-in (=y) configs are already available and don't reach
# here. Exits with the SKIP code if a required module cannot be loaded.
probe_module() {
if ! modprobe -q "$1"; then
echo "selftests: [SKIP] could not load required module $1"
exit 4
fi
}
check_conf() {
check_conf_enabled CONFIG_NET_SCH_NETEM sch_netem
check_conf_enabled CONFIG_VETH veth
check_conf_enabled CONFIG_NET_NS
check_conf_enabled CONFIG_RDS_TCP rds_tcp
check_conf_enabled CONFIG_RDS rds
}
# Check kernel config and host environment for RDS-RDMA support.
# Exits with XFAIL (2) if the user requested rdma but prerequisites
# are not met.
check_rdma_conf()
{
case "$TRANSPORT" in
*rdma*) ;;
*) return ;;
esac
# Kconfig will enforce CONFIG_INFINIBAND_* as dependencies
# of CONFIG_RDMA_RXE
check_rdma_conf_enabled CONFIG_RDMA_RXE rdma_rxe
check_rdma_conf_enabled CONFIG_RDS_RDMA rds_rdma
if ! which rdma > /dev/null 2>&1; then
echo "selftests: [XFAIL] rdma transport requires the 'rdma'" \
"tool (iproute2)"
exit 2
fi
}
check_env()
{
if ! test -d "$obj_dir"; then
echo "selftests: [SKIP] This test requires a kernel source tree"
exit 4
fi
if ! test -e "$kconfig"; then
echo "selftests: [SKIP] This test requires a configured kernel source tree"
exit 4
fi
if ! which strace > /dev/null 2>&1; then
echo "selftests: [SKIP] Could not run test without strace"
exit 4
fi
if ! which tcpdump > /dev/null 2>&1; then
echo "selftests: [SKIP] Could not run test without tcpdump"
exit 4
fi
if ! which python3 > /dev/null 2>&1; then
echo "selftests: [SKIP] Could not run test without python3"
exit 4
fi
python_major=$(python3 -c "import sys; print(sys.version_info[0])")
python_minor=$(python3 -c "import sys; print(sys.version_info[1])")
if [[ python_major -lt 3 || ( python_major -eq 3 && python_minor -lt 9 ) ]] ; then
echo "selftests: [SKIP] Could not run test without at least python3.9"
python3 -V
exit 4
fi
}
LOG_DIR="${RDS_LOG_DIR:-}"
TIMEOUT=$timeout
GENERATE_GCOV_REPORT=1
TRANSPORT=tcp
FLAGS=()
while getopts "d:l:c:u:t:T:" opt; do
case ${opt} in
d)
LOG_DIR=${OPTARG}
;;
l)
FLAGS+=("-l" "${OPTARG}")
;;
c)
FLAGS+=("-c" "${OPTARG}")
;;
t)
TIMEOUT=${OPTARG}
;;
u)
FLAGS+=("-u" "${OPTARG}")
;;
T)
TRANSPORT=${OPTARG}
;;
:)
echo "USAGE: rds_run.sh [-d logdir] [-l packet_loss]" \
"[-c packet_corruption] [-u packet_duplicate] [-t timeout]" \
"[-T tcp|rdma|tcp,rdma]"
exit 1
;;
?)
echo "Invalid option: -${OPTARG}."
exit 1
;;
esac
done
# Validate transport tokens
IFS=',' read -ra transports <<< "$TRANSPORT"
for t in "${transports[@]}"; do
if [ "$t" != "tcp" ] && [ "$t" != "rdma" ]; then
echo "rds_run.sh: unknown transport '$t' (expected tcp or rdma)"
exit 1
fi
done
FLAGS+=("--transport" "${TRANSPORT}")
check_env
check_conf
check_gcov_conf
check_rdma_conf
TRACE_CMD=()
if [[ -n "$LOG_DIR" ]]; then
FLAGS+=("-d" "$LOG_DIR")
TRACE_FILE="${LOG_DIR}/rds-strace.txt"
COVR_DIR="${LOG_DIR}/coverage/"
DMESG_FILE="${LOG_DIR}/rds-dmesg.out"
mkdir -p "$LOG_DIR"
mkdir -p "$COVR_DIR"
rm -f "$TRACE_FILE"
rm -f "$DMESG_FILE"
rm -f "$LOG_DIR"/rds-*.pcap
rm -f "$COVR_DIR"/gcovr*
echo "# Traces will be logged to ${TRACE_FILE}"
TRACE_CMD=(strace -T -tt -o "${TRACE_FILE}")
fi
set +e
echo "# running RDS tests..."
"${TRACE_CMD[@]}" python3 "$(dirname "$0")/test.py" "${FLAGS[@]}" -t "$TIMEOUT"
test_rc=$?
if [[ -n "$LOG_DIR" ]]; then
dmesg > "${DMESG_FILE}"
fi
if [[ -n "$LOG_DIR" ]] && [ "$GENERATE_GCOV_REPORT" -eq 1 ]; then
echo "# saving coverage data..."
# Ensure debugfs is mounted before reading gcov data.
if ! mountpoint -q /sys/kernel/debug 2>/dev/null; then
mount -t debugfs debugfs /sys/kernel/debug 2>/dev/null || true
fi
(set +x; cd /sys/kernel/debug/gcov; find ./* -name '*.gcda' | \
while read -r f
do
cat < "/sys/kernel/debug/gcov/$f" > "/$f"
done)
echo "# running gcovr..."
gcovr -s --html-details --gcov-executable "$GCOV_CMD" --gcov-ignore-parse-errors \
--root "${ksrc_dir}" -o "${COVR_DIR}/gcovr" "${ksrc_dir}/net/rds/" \
> "${LOG_DIR}/gcovr.log" 2>&1
echo "# gcovr log: ${LOG_DIR}/gcovr.log"
else
echo "# Coverage report will be skipped"
fi
if [ "$test_rc" -eq 0 ]; then
echo "# PASS: Test completed successfully"
else
echo "# FAIL: Test failed"
fi
exit "$test_rc"
@@ -0,0 +1 @@
timeout=800
+556
View File
@@ -0,0 +1,556 @@
#! /usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""
This module provides functional testing for the net/rds component.
"""
import argparse
import atexit
import ctypes
import errno
import hashlib
import os
import select
import re
import signal
import socket
import subprocess
import sys
import time
# Allow utils module to be imported from different directory
this_dir = os.path.dirname(os.path.realpath(__file__))
sys.path.append(os.path.join(this_dir, "../"))
# pylint: disable-next=wrong-import-position,import-error,no-name-in-module
from lib.py.utils import ip, cmd # noqa: E402
# pylint: disable-next=wrong-import-position,import-error,no-name-in-module
from lib.py.ksft import ksft_pr # noqa: E402
libc = ctypes.cdll.LoadLibrary('libc.so.6')
setns = libc.setns
NET0 = 'net0'
NET1 = 'net1'
VETH0 = 'veth0'
VETH1 = 'veth1'
tcpdump_procs = []
tcp_addrs = [
# we technically don't need different port numbers, but this will
# help identify traffic in the network analyzer
('10.0.0.1', 10000),
('10.0.0.2', 20000),
]
# RDMA network configs
RXE_DEV0 = 'rxe0'
RXE_DEV1 = 'rxe1'
VETH_RDMA0 = 'veth_rdma0'
VETH_RDMA1 = 'veth_rdma1'
rdma_addrs = [
('10.0.0.3', 30000),
('10.0.0.4', 30000),
]
# send_packets flag space
OP_FLAG_TCP = 0x1
OP_FLAG_RDMA = 0x2
# from include/uapi/linux/rds.h: SO_RDS_TRANSPORT pins a socket to a
# specific RDS transport so connection setup cannot silently fall back
# to another (e.g. loopback) transport.
SOL_RDS = 276
SO_RDS_TRANSPORT = 8
RDS_TRANS_TCP = 2
RDS_TRANS_IB = 0
signal_handler_label = ""
tap_idx = 0
nr_pass = 0
nr_fail = 0
# Helper function for creating a socket inside a network namespace.
# We need this because otherwise RDS will detect that the two TCP
# sockets are on the same interface and use the loop transport instead
# of the TCP transport.
def netns_socket(netns, *sock_args):
"""
Creates sockets inside of network namespace
:param netns: the name of the network namespace
:param sock_args: socket family and type
"""
u0, u1 = socket.socketpair(socket.AF_UNIX, socket.SOCK_SEQPACKET)
child = os.fork()
if child == 0:
try:
# change network namespace
with open(f'/var/run/netns/{netns}', encoding='utf-8') as f:
setns(f.fileno(), 0)
# create socket in target namespace
sock = socket.socket(*sock_args)
# send resulting socket to parent
socket.send_fds(u0, [], [sock.fileno()])
os._exit(0)
except BaseException:
os._exit(1)
# receive socket from child
_, fds, _, _ = socket.recv_fds(u1, 0, 1)
_, status = os.waitpid(child, 0)
u0.close()
u1.close()
if not os.WIFEXITED(status) or os.WEXITSTATUS(status) != 0:
raise RuntimeError(
f"netns_socket child failed in netns {netns} (status={status})")
return socket.fromfd(fds[0], *sock_args)
def send_burst(socks, ip_addrs, snd_hashes, nr_sent, nr_total):
"""Send until blocked or nr_total reached. Return updated nr_sent."""
while nr_sent < nr_total:
data = hashlib.sha256(
f'packet {nr_sent}'.encode('utf-8')).hexdigest().encode('utf-8')
# pseudo-random send/receive pattern
snd_idx = nr_sent % 2
rcv_idx = 1 - (nr_sent % 3) % 2
snd = socks[snd_idx]
rcv = socks[rcv_idx]
try:
snd.sendto(data, ip_addrs[rcv_idx])
except BlockingIOError:
return nr_sent
except OSError as e:
if e.errno in (errno.ENOBUFS, errno.ECONNRESET, errno.EPIPE):
return nr_sent
raise
snd_hashes.setdefault((snd.fileno(), rcv.fileno()),
hashlib.sha256()).update(f'<{data}>'.encode('utf-8'))
nr_sent += 1
return nr_sent
def recv_burst(epoll, socks, ip_addrs, rcv_hashes, nr_rcv):
"""Drain whatever's readable from epoll. Return updated nr_recv."""
for filen, evntmask in epoll.poll():
if not evntmask & select.EPOLLRDNORM:
continue
rcv = next(s for s in socks if s.fileno() == filen)
while True:
try:
data, adr = rcv.recvfrom(1024)
except BlockingIOError:
break
snd_idx = ip_addrs.index(adr)
snd = socks[snd_idx]
rcv_hashes.setdefault((snd.fileno(), rcv.fileno()),
hashlib.sha256()).update(f'<{data}>'.encode('utf-8'))
nr_rcv += 1
return nr_rcv
def check_info(socks):
"""
Check all rds info pages for errors
:param socks: list of sockets to check
"""
# the Python socket module doesn't know these
rds_info_first = 10000
rds_info_last = 10017
nr_success = 0
nr_error = 0
for sock in socks:
for optname in range(rds_info_first, rds_info_last + 1):
# Sigh, the Python socket module doesn't allow us to pass
# buffer lengths greater than 1024 for some reason. RDS
# wants multiple pages.
try:
sock.getsockopt(socket.SOL_RDS, optname, 1024)
nr_success = nr_success + 1
except OSError as e:
nr_error = nr_error + 1
if e.errno == errno.ENOSPC:
# ignore
pass
ksft_pr(f"getsockopt(): {nr_success}/{nr_error}")
def verify_hashes(snd_hashes, rcv_hashes):
"""Compare send/recv hashes per (sender, receiver) pair."""
for key, snd_hash in snd_hashes.items():
rcv_hash = rcv_hashes.get(key)
if rcv_hash is None:
ksft_pr("FAIL: No data received")
return 1
if snd_hash.hexdigest() != rcv_hash.hexdigest():
ksft_pr("FAIL: Send/recv mismatch")
ksft_pr("hash expected:", snd_hash.hexdigest())
ksft_pr("hash received:", rcv_hash.hexdigest())
return 1
ksft_pr(f"{key[0]}/{key[1]}: ok")
return 0
def snd_rcv_packets(env):
"""
Send packets on the given network interfaces
:param env: transport-environment dict for setup_tcp() / setup_rdma().
"addrs": list of (ip, port) tuples matching the sockets
"netns": list of netns names for TCP or None for RDMA
"flags": OP_FLAG_TCP or OP_FLAG_RDMA, selects sockets
"""
addrs = env["addrs"]
netns_list = env["netns"]
flags = env.get("flags", 0)
if (flags & OP_FLAG_TCP) and (flags & OP_FLAG_RDMA):
raise RuntimeError(f"Invalid transport flag sets multiple transports: {flags}")
if flags & OP_FLAG_TCP:
sockets = [
netns_socket(netns_list[0], socket.AF_RDS, socket.SOCK_SEQPACKET),
netns_socket(netns_list[1], socket.AF_RDS, socket.SOCK_SEQPACKET),
]
# Pin the sockets to the TCP transport so it doesn't fail over to a
# different transport during this test
for s in sockets:
s.setsockopt(SOL_RDS, SO_RDS_TRANSPORT, RDS_TRANS_TCP)
elif flags & OP_FLAG_RDMA:
sockets = [
socket.socket(socket.AF_RDS, socket.SOCK_SEQPACKET),
socket.socket(socket.AF_RDS, socket.SOCK_SEQPACKET),
]
# Pin the sockets to the RDMA transport so it doesn't fail over to a
# different transport during this test
for s in sockets:
s.setsockopt(SOL_RDS, SO_RDS_TRANSPORT, RDS_TRANS_IB)
else:
raise RuntimeError(f"Invalid transport flag sets no transports: {flags}")
for s, addr in zip(sockets, addrs):
s.bind(addr)
s.setblocking(0)
send_hashes = {}
recv_hashes = {}
ep = select.epoll()
for s in sockets:
ep.register(s, select.EPOLLRDNORM)
num_packets = 50000
nr_send = 0
nr_recv = 0
while nr_send < num_packets:
# Send as much as we can without blocking
ksft_pr("sending...", nr_send, nr_recv)
nr_send = send_burst(sockets, addrs, send_hashes, nr_send, num_packets)
# Receive as much as we can without blocking
ksft_pr("receiving...", nr_send, nr_recv)
while nr_recv < nr_send:
nr_recv = recv_burst(ep, sockets, addrs, recv_hashes, nr_recv)
# exercise net/rds/tcp.c:rds_tcp_sysctl_reset()
if netns_list:
for net in netns_list:
ip(f"netns exec {net} /usr/sbin/sysctl net.rds.tcp.rds_tcp_rcvbuf=10000")
ip(f"netns exec {net} /usr/sbin/sysctl net.rds.tcp.rds_tcp_sndbuf=10000")
ksft_pr("done", nr_send, nr_recv)
check_info(sockets)
# We're done sending and receiving stuff, now let's check if what
# we received is what we sent.
rc = verify_hashes(send_hashes, recv_hashes)
ep.close()
for s in sockets:
s.close()
return rc
def stop_pcaps():
"""Stop tcpdump processes.
We use pop() here to drain the list in the event that the test
completes after the signal handler is fired. List will be empty
if logdir is not set
"""
if not tcpdump_procs:
return
ksft_pr("Stopping network packet captures")
while tcpdump_procs:
proc = tcpdump_procs.pop()
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait()
def signal_handler(_sig, _frame):
"""
Test timed out signal handler
"""
ksft_pr(f"Test timed out: {signal_handler_label}")
print(f"not ok {tap_idx} rds selftest {signal_handler_label}")
sys.exit(1)
def setup_tcp():
"""
Configure tcp network
"""
# clean up any leftovers from a previously interrupted run
teardown_tcp()
ip(f"netns add {NET0}")
ip(f"netns add {NET1}")
ip("link add type veth")
# Move TCP interfaces into separate namespaces so they can no longer be
# bound directly; this prevents rds from switching over from the tcp
# transport to the loop transport.
ip(f"link set {VETH0} netns {NET0} up")
ip(f"link set {VETH1} netns {NET1} up")
# add addresses
ip(f"-n {NET0} addr add {tcp_addrs[0][0]}/32 dev {VETH0}")
ip(f"-n {NET1} addr add {tcp_addrs[1][0]}/32 dev {VETH1}")
# add routes
ip(f"-n {NET0} route add {tcp_addrs[1][0]}/32 dev {VETH0}")
ip(f"-n {NET1} route add {tcp_addrs[0][0]}/32 dev {VETH1}")
# sanity check that our two interfaces/addresses are correctly set up
# and communicating by doing a single ping
ip(f"netns exec {NET0} ping -c 1 {tcp_addrs[1][0]}")
# Start a packet capture on each network
if logdir is not None:
for netn in [NET0, NET1]:
pcap = logdir+'/rds-'+netn+'.pcap'
tcpdump_cmd = ['ip', 'netns', 'exec', netn, '/usr/sbin/tcpdump']
sudo_user = os.environ.get('SUDO_USER')
if sudo_user:
tcpdump_cmd.extend(['-Z', sudo_user])
tcpdump_cmd.extend(['-i', 'any', '-w', pcap])
# pylint: disable-next=consider-using-with
p = subprocess.Popen(tcpdump_cmd,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
tcpdump_procs.append(p)
# simulate packet loss, duplication and corruption
for netn, iface in [(NET0, VETH0), (NET1, VETH1)]:
ip(f"netns exec {netn} /usr/sbin/tc qdisc add dev {iface} root netem \
corrupt {PACKET_CORRUPTION} loss {PACKET_LOSS} duplicate \
{PACKET_DUPLICATE}")
def teardown_tcp():
"""
Tear down the tcp network configured by setup_tcp().
Removing the namespaces also removes the veth pair, addresses,
routes, and netem qdisc that live inside them. fail=False so
this is safe to call in error paths after a partial or complete setup.
"""
cmd(f"ip netns del {NET0}", fail=False)
cmd(f"ip netns del {NET1}", fail=False)
def get_iface_mac(iface):
"""Return the MAC address of a local network interface."""
out = subprocess.check_output(['ip', 'link', 'show', iface], text=True)
mac = re.search(r'link/ether\s+([0-9a-f:]+)', out)
if not mac:
raise RuntimeError(f"Cannot determine MAC address of {iface}")
return mac.group(1)
def setup_rdma():
"""
Configure rdma network
"""
# remove links left over by previously interrupted run.
teardown_rdma()
# use call here since modprobe may fail if the rdma_rxe
# module is built-in
subprocess.call(['modprobe', 'rdma_rxe'],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
ip(f"link add {VETH_RDMA0} type veth peer name {VETH_RDMA1}")
ip(f"link set {VETH_RDMA0} up")
ip(f"link set {VETH_RDMA1} up")
# Since both addresses are in the same namespace, the source address
# is always local, so enable accept_local
cmd(f"/usr/sbin/sysctl -q net.ipv4.conf.{VETH_RDMA0}.accept_local=1")
cmd(f"/usr/sbin/sysctl -q net.ipv4.conf.{VETH_RDMA1}.accept_local=1")
# Reverse path filters must be disabled so that the local routes don't
# cause RPF failures.
cmd(f"/usr/sbin/sysctl -q net.ipv4.conf.{VETH_RDMA0}.rp_filter=0")
cmd(f"/usr/sbin/sysctl -q net.ipv4.conf.{VETH_RDMA1}.rp_filter=0")
# add addresses
ip(f"addr add {rdma_addrs[0][0]}/32 dev {VETH_RDMA0}")
ip(f"addr add {rdma_addrs[1][0]}/32 dev {VETH_RDMA1}")
# add routes
ip(f"route add {rdma_addrs[1][0]}/32 dev {VETH_RDMA0}")
ip(f"route add {rdma_addrs[0][0]}/32 dev {VETH_RDMA1}")
# ARP will not resolve neighbor IPs on /32 routes without a subnet.
# Avoid this by adding neighbors directly so RDMA CM can populate path
# records with correct mac addrs without waiting for the ARP.
mac0 = get_iface_mac(VETH_RDMA0)
mac1 = get_iface_mac(VETH_RDMA1)
ip(f"neigh add {rdma_addrs[1][0]} lladdr {mac1} dev {VETH_RDMA0} nud permanent")
ip(f"neigh add {rdma_addrs[0][0]} lladdr {mac0} dev {VETH_RDMA1} nud permanent")
cmd(f'rdma link add {RXE_DEV0} type rxe netdev {VETH_RDMA0}')
cmd(f'rdma link add {RXE_DEV1} type rxe netdev {VETH_RDMA1}')
time.sleep(1) # allow RXE devices to initialise
# Start a packet capture on each network
if logdir is not None:
for iface in [VETH_RDMA0, VETH_RDMA1]:
pcap = logdir+'/rds-roce-'+iface+'.pcap'
tcpdump_cmd = ['/usr/sbin/tcpdump']
sudo_user = os.environ.get('SUDO_USER')
if sudo_user:
tcpdump_cmd.extend(['-Z', sudo_user])
tcpdump_cmd.extend(['-i', iface, '-w', pcap])
# pylint: disable-next=consider-using-with
p = subprocess.Popen(tcpdump_cmd,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
tcpdump_procs.append(p)
# simulate packet loss, duplication and corruption
for iface in [VETH_RDMA0, VETH_RDMA1]:
cmd(f"/usr/sbin/tc qdisc add dev {iface} root netem \
corrupt {PACKET_CORRUPTION} loss {PACKET_LOSS} duplicate \
{PACKET_DUPLICATE}")
def teardown_rdma():
"""
Tear down the rdma network configured by setup_rdma().
"""
# remove links left over by previously interrupted run.
cmd(f'rdma link del {RXE_DEV0}', fail=False)
cmd(f'rdma link del {RXE_DEV1}', fail=False)
cmd(f'ip link del {VETH_RDMA0}', fail=False)
#Parse out command line arguments. We take an optional
# timeout parameter and an optional log output folder
parser = argparse.ArgumentParser(description="init script args",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("-d", "--logdir", action="store",
help="directory to store logs", default=None)
parser.add_argument("-T", "--transport", default="tcp",
help="Comma-separated list of transports to test: "
"tcp, rdma, or tcp,rdma. Each matching test "
"is run once per transport. "
"'rdma' requires CONFIG_RDS_RDMA and rdma_rxe.")
parser.add_argument('-t', '--timeout', help="timeout to terminate hung test",
type=int, default=0)
parser.add_argument('-l', '--loss', help="Simulate tcp packet loss",
type=int, default=0)
parser.add_argument('-c', '--corruption', help="Simulate tcp packet corruption",
type=int, default=0)
parser.add_argument('-u', '--duplicate', help="Simulate tcp packet duplication",
type=int, default=0)
args = parser.parse_args()
logdir=args.logdir
PACKET_LOSS=str(args.loss)+'%'
PACKET_CORRUPTION=str(args.corruption)+'%'
PACKET_DUPLICATE=str(args.duplicate)+'%'
# check transport is either tcp or rdma
transports = [t.strip() for t in args.transport.split(',')]
for t in transports:
if t not in ('tcp', 'rdma'):
raise SystemExit(f"test.py: unknown transport: {t!r}")
# Register stop_pcaps before any network setups so that any partially setup
# tcpdumps are still cleaned up on error
atexit.register(stop_pcaps)
# Set up all requested transports upfront so network plumbing is
# ready before any test runs.
transport_envs = {}
FLAGS = 0
if 'tcp' in transports:
# Register cleanups before setups to handle partial setups that error'd out
atexit.register(teardown_tcp)
setup_tcp()
transport_envs['tcp'] = {
'addrs': tcp_addrs,
'netns': [NET0, NET1],
'flags': FLAGS | OP_FLAG_TCP,
}
if 'rdma' in transports:
atexit.register(teardown_rdma)
setup_rdma()
transport_envs['rdma'] = {
'addrs': rdma_addrs,
'netns': None,
'flags': FLAGS | OP_FLAG_RDMA,
}
print("TAP version 13")
print(f"1..{len(transport_envs)}")
for transport, tenv in transport_envs.items():
tap_idx += 1
# add a timeout
if args.timeout > 0:
signal_handler_label = transport
signal.alarm(args.timeout)
signal.signal(signal.SIGALRM, signal_handler)
ret = snd_rcv_packets(tenv)
# cancel timeout
signal.alarm(0)
if ret == 0:
ksft_pr("Success")
print(f"ok {tap_idx} rds selftest {transport}")
nr_pass += 1
else:
print(f"not ok {tap_idx} rds selftest {transport}")
nr_fail += 1
ksft_pr(f"Totals: pass:{nr_pass} fail:{nr_fail} skip:0")
sys.exit(1 if nr_fail else 0)