[PATCH net v5 6/8] selftests: drv-net: psp: test ipv6 tcp socket with extension headers
From: Todd Freed
Date: Tue Nov 10 2026 - 19:20:35 EST
send on a psp tcp socket in three phases, checking the device's
tx-packets and what the peer received after each:
1/ no socket options. the data is sent, counted, and received.
2/ an extension header set on the socket. the packets are dropped,
tx-packets is unchanged, and nothing reaches the peer. the data
stays queued.
3/ option removed. the queued data, then new data, is sent, counted,
and received.
the test has a variant for each extension header a socket can set: a
destination options header (IPV6_DSTOPTS) and a hop-by-hop options
header (IPV6_HOPOPTS), each holding one padn option, and a segment
routing header (IPV6_RTHDR).
a software filter on the peer also counts, and drops, udp packets from
the socket's address to the peer's. psp packets reach the peer as
decrypted tcp packets, as expected. packets which were encapsulated but
are malformed or unencrypted (this is what mlx5 does) show up as udp,
and the test asserts there are none. if the filter cannot be set up the
test is skipped.
tx-packets is read after it has quiesced, because some devices refresh
counters periodically (mlx5 takes about a second).
without the fix in "psp: don't encapsulate ipv6 packets with extension
headers", netdevsim encapsulates the packets in a malformed way and
counts them in tx-packets, and the test fails on that. with mlx5
(connectx-7), the packets go out unencrypted and aren't counted in
tx-packets. the test fails on the udp packets which reach the peer.
capturing on the peer,
tcpdump -i <dev> -nn -A 'ip6 and src host <sender> and not tcp'
shows them as udp, with the test's payload readable.
Signed-off-by: Todd Freed <toddfreed@xxxxxxxx>
---
tools/testing/selftests/drivers/net/config | 1 +
tools/testing/selftests/drivers/net/psp.py | 148 ++++++++++++++++++++-
2 files changed, 146 insertions(+), 3 deletions(-)
diff --git a/tools/testing/selftests/drivers/net/config b/tools/testing/selftests/drivers/net/config
index 4838adf27fa1..409f9a96a102 100644
--- a/tools/testing/selftests/drivers/net/config
+++ b/tools/testing/selftests/drivers/net/config
@@ -4,6 +4,7 @@ CONFIG_DEBUG_INFO_BTF_MODULES=n
CONFIG_INET_PSP=y
CONFIG_IPV6=y
CONFIG_MACSEC=m
+CONFIG_NET_ACT_GACT=m
CONFIG_NET_ACT_SKBEDIT=m
CONFIG_NET_CLS_ACT=y
CONFIG_NET_CLS_BPF=y
diff --git a/tools/testing/selftests/drivers/net/psp.py b/tools/testing/selftests/drivers/net/psp.py
index 473500901879..7e251439a2fd 100755
--- a/tools/testing/selftests/drivers/net/psp.py
+++ b/tools/testing/selftests/drivers/net/psp.py
@@ -18,12 +18,12 @@ from lib.py import ksft_run, ksft_exit, ksft_pr
from lib.py import ksft_true, ksft_eq, ksft_ne, ksft_gt, ksft_raises
from lib.py import ksft_not_none
from lib.py import ksft_variants, KsftNamedVariant
-from lib.py import KsftSkipEx, KsftFailEx
+from lib.py import KsftSkipEx, KsftFailEx, CmdExitFailure
from lib.py import NetDrvEpEnv, NetDrvContEnv
from lib.py import Netlink, NlError, PSPFamily, RtnlFamily
from lib.py import NetNSEnter
from lib.py import bkg, rand_port, wait_port_listen
-from lib.py import ip
+from lib.py import ip, tc
TCP_ULP = 31
@@ -133,6 +133,58 @@ def _check_data_outq(s, exp_len, force_wait=False):
def _get_stat(cfg, key):
return cfg.pspnl.get_stats({'dev-id': cfg.psp_dev_id})[key]
+
+def _get_quiesced_stat(cfg, key, timeout=10):
+ """
+ Like _get_stat(), but wait till the value stops changing. Note
+ that some devices refresh flow counters periodically, e.g. once
+ per second with mlx5.
+ """
+ val = _get_stat(cfg, key)
+ end = time.monotonic() + timeout
+ while True:
+ time.sleep(1.5)
+ new = _get_stat(cfg, key)
+ if new == val:
+ return val
+ val = new
+ if time.monotonic() >= end:
+ ksft_pr(f"{key} still changing after {timeout}s: {val}")
+ return val
+
+
+def _remote_count(cfg, proto, match):
+ """
+ Count, and drop, the @proto packets matching flower @match which reach
+ the remote, with a software filter on its ingress (offloaded filter
+ counters are refreshed periodically). Dropping keeps such packets away
+ from the remote's stack. Returns a function reading the count, or None
+ if the filter can't be installed.
+ """
+ flt = (f"dev {cfg.remote_ifname} ingress protocol {proto} pref 1 "
+ f"handle 0x5053 flower")
+ try:
+ qdiscs = tc(f"qdisc show dev {cfg.remote_ifname}", json=True,
+ host=cfg.remote)
+ # An ingress qdisc takes the filter as well as clsact
+ if not any(q['kind'] in ('clsact', 'ingress') for q in qdiscs):
+ tc(f"qdisc add dev {cfg.remote_ifname} clsact", host=cfg.remote)
+ defer(tc, f"qdisc del dev {cfg.remote_ifname} clsact",
+ host=cfg.remote)
+ tc(f"filter add {flt} skip_hw {match} action drop", host=cfg.remote)
+ except CmdExitFailure as e:
+ ksft_pr(f"Can't install a filter on the remote: {e}")
+ return None
+ defer(tc, f"filter del {flt}", host=cfg.remote)
+
+ def count():
+ filters = tc(f"-s filter get {flt}", json=True, host=cfg.remote)
+ for f in filters:
+ for act in f.get("options", {}).get("actions", []):
+ return act["stats"]["packets"]
+ raise KsftFailEx("no action on the remote filter")
+ return count
+
#
# Test case boiler plate
#
@@ -658,6 +710,95 @@ def removal_device_bi(cfg):
_close_conn(cfg, s)
+def _ipv6_exthdr_variants():
+ padn = bytes([0, 0, 1, 4, 0, 0, 0, 0]) # one PadN option
+
+ def srh(peer):
+ """ segment routing header, with @peer as its only segment """
+ return (bytes([0, 2, 4, 0, 0, 0, 0, 0]) +
+ socket.inet_pton(socket.AF_INET6, peer))
+
+ yield KsftNamedVariant("dstopts", socket.IPV6_DSTOPTS, lambda peer: padn)
+ yield KsftNamedVariant("hopopts", socket.IPV6_HOPOPTS, lambda peer: padn)
+ yield KsftNamedVariant("rthdr", socket.IPV6_RTHDR, srh)
+
+
+def _src_addr(dst):
+ """ The source address the kernel picks for @dst """
+ with socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) as u:
+ u.connect((dst, 9))
+ return u.getsockname()[0]
+
+
+@ksft_variants(_ipv6_exthdr_variants())
+def data_send_ipv6_exthdr(cfg, opt, val):
+ """ Test that PSP drops IPv6 packets with an extension header """
+ cfg.require_ipver("6")
+ _init_psp_dev(cfg)
+
+ # Packets which were encapsulated but malformed or left unencrypted
+ # reach the remote as UDP. Set up the count before connecting, so a
+ # skip doesn't leave the responder mid key exchange.
+ peer = cfg.remote_addr_v["6"]
+ remote_udp = _remote_count(cfg, "ipv6", f"src_ip {_src_addr(peer)} "
+ f"dst_ip {peer} ip_proto udp")
+ if not remote_udp:
+ raise KsftSkipEx("can't count UDP on the remote")
+
+ s = _make_psp_conn(cfg, 0, "6")
+ try:
+ rx_assoc = cfg.pspnl.rx_assoc({"version": 0,
+ "dev-id": cfg.psp_dev_id,
+ "sock-fd": s.fileno()})
+ tx = _spi_xchg(s, rx_assoc['rx-key'])
+ cfg.pspnl.tx_assoc({"dev-id": cfg.psp_dev_id,
+ "version": 0,
+ "tx-key": tx,
+ "sock-fd": s.fileno()})
+ _check_data_outq(s, 0)
+
+ def wait_outq_empty():
+ for _ in range(100):
+ if _get_outq(s) == 0:
+ break
+ time.sleep(0.5)
+ _check_data_outq(s, 0)
+
+ # 1. No option: sent, counted, and received
+ tx_pkts = _get_quiesced_stat(cfg, 'tx-packets')
+ total = _send_careful(cfg, s, 20)
+ wait_outq_empty()
+ ksft_gt(_get_quiesced_stat(cfg, 'tx-packets'), tx_pkts)
+ _check_data_rx(cfg, total)
+
+ # 2. With the option: dropped, not counted, and nothing reaches the
+ # peer. The data stays queued.
+ s.setsockopt(socket.IPPROTO_IPV6, opt, val(peer))
+ tx_pkts = _get_quiesced_stat(cfg, 'tx-packets')
+ udp = remote_udp()
+ queued = _send_careful(cfg, s, 20)
+ _check_data_outq(s, queued, force_wait=True)
+ ksft_eq(_get_quiesced_stat(cfg, 'tx-packets'), tx_pkts)
+ ksft_eq(remote_udp(), udp, comment="UDP at the remote")
+ _check_data_rx(cfg, total)
+ total += queued
+
+ # 3. Option removed: the queued data, then new data, sent and
+ # received
+ tx_pkts = _get_quiesced_stat(cfg, 'tx-packets')
+ s.setsockopt(socket.IPPROTO_IPV6, opt, b'')
+ wait_outq_empty()
+ total += _send_careful(cfg, s, 20)
+ wait_outq_empty()
+ ksft_gt(_get_quiesced_stat(cfg, 'tx-packets'), tx_pkts)
+ _check_data_rx(cfg, total)
+ finally:
+ # With the option, the queued data, the FIN and the ACKs would be
+ # dropped, leaving an orphan holding the association
+ s.setsockopt(socket.IPPROTO_IPV6, opt, b'')
+ _close_psp_conn(cfg, s)
+
+
def _get_psp_ver_ip_variants():
for ver in range(4):
for ipv in ("4", "6"):
@@ -1173,7 +1314,8 @@ def main() -> None:
cfg.comm_port),
timeout=1)
- cases = [data_basic_send, data_mss_adjust]
+ cases = [data_basic_send, data_mss_adjust,
+ data_send_ipv6_exthdr]
if has_cont:
cases += [
--
2.53.0-Meta