[PATCH net v5 7/8] selftests: drv-net: psp: test that ipv4 psp packets aren't fragmented
From: Todd Freed
Date: Tue Nov 10 2026 - 19:21:52 EST
make the ipv4 packets of a psp tcp socket need fragmenting, and check
that they're dropped (IpFragFails) and not transmitted unencrypted.
tcp sizes its segments from the route mtu. with IP_PMTUDISC_OMIT, ip
fragments them to the device mtu, and doesn't set df. so the test sets a
route mtu of twice the larger of the two device mtus (at most 65535) on
both ends (the peer's mss limits ours too), keeping the device and
source address of the current route, and IP_PMTUDISC_OMIT on the
socket. tcp mtu probing (tcp_mtu_probing 2) would cap the mss at the
probing base, so the test turns it off. if the peer isn't on-link, or
the socket's mss doesn't need fragmenting, the test is skipped.
a software filter on the peer counts, and drops, the ipv4 fragments
which reach it from the socket's address, and the test asserts there are
none. IpFragFails is per netns, so it only confirms that the packets
were dropped.
while both route mtus are in place, a new ssh connection to the remote
over these addresses can stall: its segments are sized from the route
mtu, and sent with df set. so the local route is added last, and removed
before the next remote command.
no ipv6 test: the stack only fragments ipv6 tcp packets when something
like ipvs sets ignore_df on them (see "net: don't fragment decrypted
skbs"), which this test doesn't set up.
without the fix in "net: don't fragment decrypted skbs", the packets are
fragmented, and the fragments go out unencrypted. they reach the peer,
IpFragFails doesn't move, and the test fails.
Signed-off-by: Todd Freed <toddfreed@xxxxxxxx>
---
tools/testing/selftests/drivers/net/psp.py | 101 +++++++++++++++++++++
1 file changed, 101 insertions(+)
diff --git a/tools/testing/selftests/drivers/net/psp.py b/tools/testing/selftests/drivers/net/psp.py
index 7e251439a2fd..5dac62ad96b0 100755
--- a/tools/testing/selftests/drivers/net/psp.py
+++ b/tools/testing/selftests/drivers/net/psp.py
@@ -26,6 +26,8 @@ from lib.py import bkg, rand_port, wait_port_listen
from lib.py import ip, tc
TCP_ULP = 31
+IP_MTU_DISCOVER = 10
+IP_PMTUDISC_OMIT = 5
def _get_outq(s):
@@ -799,6 +801,105 @@ def data_send_ipv6_exthdr(cfg, opt, val):
_close_psp_conn(cfg, s)
+def _ipv4_snmp(key):
+ """ An Ip: counter of this netns, from /proc/net/snmp """
+ with open("/proc/net/snmp", encoding="utf-8") as f:
+ ip_lines = [line.split() for line in f if line.startswith("Ip:")]
+ return int(ip_lines[1][ip_lines[0].index(key)])
+
+
+def _ipv4_route_mtu(dst, mtu, host=None):
+ """ Add a route to @dst with @mtu, keeping the device and source of
+ the current one, which must be on-link. Returns the defer removing it.
+ """
+ route = ip(f"-4 route get {dst}", json=True, host=host)[0]
+ if route.get("gateway"):
+ raise KsftSkipEx(f"{dst} isn't on-link")
+ try:
+ ip(f"route add {dst}/32 dev {route['dev']} src {route['prefsrc']} "
+ f"mtu {mtu}", host=host)
+ except CmdExitFailure as e:
+ raise KsftSkipEx(f"can't set the route mtu to {dst}: {e}") from e
+ return defer(ip, f"route del {dst}/32 dev {route['dev']}", host=host)
+
+
+def data_send_ipv4_frags(cfg):
+ """ Test that IPv4 packets of a PSP TCP socket aren't fragmented """
+ cfg.require_ipver("4")
+ _init_psp_dev(cfg)
+
+ # Fragments which get out reach the remote. Set up the count before
+ # connecting, so a skip doesn't leave the responder mid key exchange.
+ # The socket's source is the one the kernel picks, which may not be
+ # LOCAL_V4, and the fragments would leave through the route's device
+ rem = cfg.remote_addr_v["4"]
+ route = ip(f"-4 route get {rem}", json=True)[0]
+ if route["dev"] != cfg.ifname:
+ raise KsftSkipEx(f"{rem} isn't routed through {cfg.ifname}")
+ loc = route["prefsrc"]
+ remote_frags = _remote_count(cfg, "ip",
+ f"src_ip {loc} dst_ip {rem} ip_flags frag")
+ if not remote_frags:
+ raise KsftSkipEx("can't count fragments on the remote")
+
+ # TCP sizes its segments from the route MTU. With IP_PMTUDISC_OMIT, IP
+ # fragments them to the device MTU. Set a route MTU above the device
+ # MTU on both ends, since the peer's MSS limits ours too. While both
+ # are in place, a new ssh connection to the remote over these addresses
+ # can stall, so add the local one last, and remove it first.
+ mtu = min(2 * max(cfg.dev['mtu'], cfg.remote_dev['mtu']), 65535)
+ _ipv4_route_mtu(loc, mtu, host=cfg.remote)
+ local_route = _ipv4_route_mtu(rem, mtu)
+
+ # With MTU probing on, TCP caps the MSS at the probing base, whatever
+ # the route MTU
+ probing_path = "/proc/sys/net/ipv4/tcp_mtu_probing"
+ with open(probing_path, encoding="utf-8") as fp:
+ probing = fp.read().strip()
+ if int(probing) > 1:
+ def set_probing(val):
+ with open(probing_path, "w", encoding="utf-8") as fp:
+ fp.write(val)
+ set_probing("0")
+ defer(set_probing, probing)
+
+ s = _make_psp_conn(cfg, 0, "4")
+ 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()})
+ s.setsockopt(socket.IPPROTO_IP, IP_MTU_DISCOVER, IP_PMTUDISC_OMIT)
+ mss = s.getsockopt(socket.IPPROTO_TCP, socket.TCP_MAXSEG)
+ if mss + 40 <= cfg.dev['mtu']:
+ raise KsftSkipEx(f"mss {mss} needs no fragmenting")
+
+ fails = _ipv4_snmp("FragFails")
+ # The packets are dropped, so the send may not complete (with a
+ # jumbo device MTU, the socket buffer can take all of it)
+ s.settimeout(3)
+ try:
+ s.sendall(b'f' * 200000)
+ except socket.timeout:
+ pass
+ fails = _ipv4_snmp("FragFails") - fails
+
+ # Before the next ssh to the remote, see above
+ local_route.exec()
+
+ # Fragments would go out unencrypted: the packets must be dropped
+ # instead of fragmented. FragFails is per netns, so it only
+ # confirms the drop: the remote count is what decides.
+ ksft_eq(remote_frags(), 0, comment="fragments reached the remote")
+ ksft_gt(fails, 0, comment="packets dropped instead of fragmented")
+ finally:
+ _close_psp_conn(cfg, s)
+
+
def _get_psp_ver_ip_variants():
for ver in range(4):
for ipv in ("4", "6"):
--
2.53.0-Meta