From: Todd Freed <toddfreed@meta.com>
To: <netdev@vger.kernel.org>, <toddfreed@meta.com>
Cc: "David S. Miller" <davem@davemloft.net>,
"Andrew Lunn" <andrew+netdev@lunn.ch>,
"Björn Töpel" <bjorn@kernel.org>,
bpf@vger.kernel.org, "Daniel Borkmann" <daniel@iogearbox.net>,
"Daniel Zahka" <daniel.zahka@gmail.com>,
"Donald Hunter" <donald.hunter@gmail.com>,
"Eric Dumazet" <edumazet@kernel.org>,
"Jakub Kicinski" <kuba@kernel.org>,
"Jamal Hadi Salim" <jhs@mojatatu.com>,
"Jiri Pirko" <jiri@resnulli.us>,
"John Fastabend" <john.fastabend@gmail.com>,
linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org,
linux-kselftest@vger.kernel.org, linux-rdma@vger.kernel.org,
"Martin KaFai Lau" <martin.lau@linux.dev>,
"Paolo Abeni" <pabeni@redhat.com>,
"Saeed Mahameed" <saeedm@nvidia.com>,
"Simon Horman" <horms@kernel.org>,
"Tariq Toukan" <tariqt@nvidia.com>,
"Wei Wang" <weiwan@google.com>,
"Willem de Bruijn" <willemdebruijn.kernel@gmail.com>
Subject: [PATCH net-next v1 11/15] selftests: drv-net: psp: test socket-less associations and the tc psp action
Date: Tue, 10 Nov 2026 16:55:29 -0800 [thread overview]
Message-ID: <20261111005533.6436-12-toddfreed@meta.com> (raw)
In-Reply-To: <20261111005533.6436-1-toddfreed@meta.com>
Cover the socket-less association commands and the tc psp action.
The data path tests connect a plain local TCP socket to a PSP socket of
the responder, and give the local side PSP with tc and socket-less
associations: on ingress, flower psp_spi, psp_ver and psp_dev match what
was decrypted with the local Rx SPI, and psp decap removes its PSP
state; on egress, psp encap attaches the Tx association to the socket's
packets, which the device encrypts.
Test cases,
assoc_sockless_basic rx-spi-alloc / assoc-add / get / dump / del,
only the SPI is ever reported
assoc_sockless_stale key rotation marks socket-less Rx assocs
stale, and not Tx ones
assoc_sockless_bad bad device, key size, missing key
assoc_sockless_tc encap only takes a Tx association, decap none
data_tc_basic_send data to the responder and its acks back, for
each PSP version and IP version
data_tc_no_decap decrypted packets don't reach a plain socket
data_tc_assoc_del associations deleted while tc uses them,
encap holding its Tx one
data_tc_decap_match decap only takes the packets of the filter's
SPI, version and device
data_tc_encap_wrong_dev packets redirected to another device drop
data_tc_encap_short_hdr an IPv4 packet shorter than its header
length is dropped by encap
data_tc_encap_too_big packets which won't fit the MTU once
encapsulated are dropped, with and without GSO
data_tc_encap_psp_sock packets of a PSP socket, which are marked
decrypted, are dropped by psp encap action
data_tc_encap_ingress packets with an assoc aren't redirected from
egress to ingress
removal_device_tc PSP device removed while tc holds associations
Note that the tc tests require a newer tc, which has support for the psp
action and the flower psp matches. If it's detected that tc is missing
that support, the tc tests simply skip.
Signed-off-by: Todd Freed <toddfreed@meta.com>
---
tools/testing/selftests/drivers/net/config | 3 +
tools/testing/selftests/drivers/net/psp.py | 593 ++++++++++++++++++++-
2 files changed, 593 insertions(+), 3 deletions(-)
diff --git a/tools/testing/selftests/drivers/net/config b/tools/testing/selftests/drivers/net/config
index 4838adf27fa1..baf1c9d0dd13 100644
--- a/tools/testing/selftests/drivers/net/config
+++ b/tools/testing/selftests/drivers/net/config
@@ -1,9 +1,12 @@
CONFIG_CONFIGFS_FS=y
CONFIG_DEBUG_INFO_BTF=y
CONFIG_DEBUG_INFO_BTF_MODULES=n
+CONFIG_DUMMY=m
CONFIG_INET_PSP=y
CONFIG_IPV6=y
CONFIG_MACSEC=m
+CONFIG_NET_ACT_MIRRED=m
+CONFIG_NET_ACT_PSP=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..fb7544410051 100755
--- a/tools/testing/selftests/drivers/net/psp.py
+++ b/tools/testing/selftests/drivers/net/psp.py
@@ -15,7 +15,8 @@ from contextlib import contextmanager
from lib.py import defer
from lib.py import ksft_run, ksft_exit, ksft_pr
-from lib.py import ksft_true, ksft_eq, ksft_ne, ksft_gt, ksft_raises
+from lib.py import ksft_true, ksft_eq, ksft_ne, ksft_gt, ksft_ge
+from lib.py import ksft_raises
from lib.py import ksft_not_none
from lib.py import ksft_variants, KsftNamedVariant
from lib.py import KsftSkipEx, KsftFailEx
@@ -23,9 +24,10 @@ 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 cmd, ip, tc
TCP_ULP = 31
+UDP_SEGMENT = 103
def _get_outq(s):
@@ -683,6 +685,591 @@ def data_mss_adjust(cfg, ipver):
_data_mss_adjust(cfg, ipver)
+#
+# Socket-less associations and the tc psp action
+#
+
+def _assoc_del(cfg, assoc_id):
+ """ Delete a socket-less assoc, unless the test already did """
+ try:
+ cfg.pspnl.assoc_del({"id": assoc_id})
+ except NlError as e:
+ if e.nl_msg.error != -errno.ENOENT:
+ raise
+
+
+def _sockless_assocs(cfg, version=0):
+ """ Allocate an Rx SPI and add a Tx assoc with its key """
+ rx = cfg.pspnl.rx_spi_alloc({"dev-id": cfg.psp_dev_id,
+ "version": version})
+ defer(_assoc_del, cfg, rx['id'])
+ tx = cfg.pspnl.assoc_add({"dev-id": cfg.psp_dev_id,
+ "version": version,
+ "tx-key": rx['rx-key']})
+ defer(_assoc_del, cfg, tx['id'])
+ return rx, tx
+
+
+def _require_tc_psp(cfg):
+ if not hasattr(cfg, 'tc_psp'):
+ # tc exits with an error either way, look at what it says. The
+ # tests need both the psp action and the flower psp keys.
+ try:
+ act = cmd("tc action add action psp help", fail=False)
+ flt = cmd("tc filter add dev lo ingress flower help",
+ fail=False)
+ except FileNotFoundError:
+ cfg.tc_psp = False
+ else:
+ cfg.tc_psp = ("psp encap" in act.stdout + act.stderr and
+ "psp_spi" in flt.stdout + flt.stderr)
+ if not cfg.tc_psp:
+ raise KsftSkipEx("tc does not support the psp action and matches")
+
+
+def _require_psp_version(cfg, version):
+ name = cfg.pspnl.consts["version"].entries_by_val[version].name
+ if name not in cfg.psp_info['psp-versions-cap']:
+ raise KsftSkipEx("PSP version not supported", name)
+
+
+def _tc_proto(ipver):
+ return "ip" if ipver == "4" else "ipv6"
+
+
+def _tc_clsact(ifname, host=None):
+ """ Add clsact to @ifname, unless it has one already: the env or the
+ host may use it too, so only remove it if it was added here
+ """
+ qdiscs = tc(f"qdisc show dev {ifname}", json=True, host=host)
+ if any(q['kind'] == 'clsact' for q in qdiscs):
+ return
+ tc(f"qdisc add dev {ifname} clsact", host=host)
+ defer(tc, f"qdisc del dev {ifname} clsact", host=host)
+
+
+def _make_tc_conn(cfg, version, ipver):
+ """ Connect a plain socket to a PSP socket of the responder """
+ _send_with_ack(cfg, b'conn psp\0' + struct.pack('BB', version, version))
+ family = socket.AF_INET if ipver == "4" else socket.AF_INET6
+ s = socket.socket(family, socket.SOCK_STREAM)
+ # The stack doesn't know tc will add PSP headers (40 bytes), leave room
+ # for them. TCP takes its options off the MSS.
+ mtu = ip(f"link show dev {cfg.ifname}", json=True)[0]['mtu']
+ iph = 20 if ipver == "4" else 40
+ s.setsockopt(socket.IPPROTO_TCP, socket.TCP_MAXSEG,
+ mtu - 40 - iph - 20)
+ s.connect((cfg.remote_addr_v[ipver], cfg.comm_port))
+ defer(_tc_conn_close, cfg, s)
+ return s
+
+
+def _tc_conn_close(cfg, s):
+ """ Close a test's connection, unless the test got to do it """
+ if s.fileno() != -1:
+ _close_psp_conn(cfg, s)
+
+
+def _tc_psp_conn(cfg, version, ipver, decap=True):
+ """
+ Connect a plain socket to a PSP socket of the responder, and give that
+ flow PSP encap using socket-less assocs and tc: decap what was decrypted
+ with its Rx SPI, version and device on ingress, encap its packets on
+ egress, which the device encrypts. Returns the socket and the assocs.
+ """
+ proto = _tc_proto(ipver)
+
+ # The Rx SPI and the decap rule first: once connected, the responder
+ # waits for our key, and an error in between would leave the comm
+ # channel out of step
+ rx = cfg.pspnl.rx_spi_alloc({"dev-id": cfg.psp_dev_id,
+ "version": version})
+ defer(_assoc_del, cfg, rx['id'])
+
+ _tc_clsact(cfg.ifname)
+ # The responder starts sending with our SPI as soon as it has our key
+ if decap:
+ tc(f"filter add dev {cfg.ifname} ingress pref 10 handle 1 "
+ f"protocol {proto} flower psp_spi {rx['rx-key']['spi']:#x} "
+ f"psp_ver {version} psp_dev {cfg.psp_dev_id} action psp decap")
+ defer(tc, f"filter del dev {cfg.ifname} ingress pref 10 handle 1 "
+ f"protocol {proto} flower")
+
+ s = _make_tc_conn(cfg, version, ipver)
+ tx_key = _spi_xchg(s, rx['rx-key'])
+ tx = cfg.pspnl.assoc_add({"dev-id": cfg.psp_dev_id,
+ "version": version,
+ "tx-key": tx_key})
+ defer(_assoc_del, cfg, tx['id'])
+
+ # Make sure the SPI exchange got through in clear text
+ _check_data_outq(s, 0)
+
+ # The comm socket goes to the same port, match on ours
+ sport = s.getsockname()[1]
+ tc(f"filter add dev {cfg.ifname} egress pref 10 handle 1 "
+ f"protocol {proto} flower ip_proto tcp src_port {sport} "
+ f"action psp encap assoc {tx['id']}")
+ defer(tc, f"filter del dev {cfg.ifname} egress pref 10 handle 1 "
+ f"protocol {proto} flower")
+
+ return s, rx, tx
+
+
+def _tc_action_stats(cfg, direction):
+ """ Stats of the psp action of the pref 10 filter """
+ filters = tc(f"-s filter show dev {cfg.ifname} {direction} pref 10",
+ json=True)
+ for flt in filters:
+ for act in flt.get("options", {}).get("actions", []):
+ if act["kind"] == "psp":
+ return act["stats"]
+ raise KsftFailEx(f"no psp action on {direction}")
+
+
+def _wait_data_outq(s, exp_len, timeout=5):
+ """ Like _check_data_outq() but leave time for retransmits """
+ outq = 0
+ end = time.monotonic() + timeout
+ while time.monotonic() < end:
+ outq = _get_outq(s)
+ if outq == exp_len:
+ break
+ time.sleep(0.05)
+ ksft_eq(outq, exp_len)
+
+
+def assoc_sockless_basic(cfg):
+ """ Test socket-less associations """
+ _init_psp_dev(cfg)
+
+ rx, tx = _sockless_assocs(cfg)
+ ksft_eq(rx['dev-id'], cfg.psp_dev_id)
+ ksft_gt(rx['rx-key']['spi'], 0)
+ ksft_eq(len(rx['rx-key']['key']), 16)
+ ksft_eq(tx['dev-id'], cfg.psp_dev_id)
+ ksft_ne(tx['id'], rx['id'])
+
+ # Only the SPI is ever reported, never the key
+ version = cfg.pspnl.consts["version"].entries_by_val[0].name
+ spi = {'spi': rx['rx-key']['spi']}
+ assoc = cfg.pspnl.assoc_get({"id": rx['id']})
+ ksft_eq(assoc['dev-id'], cfg.psp_dev_id)
+ ksft_eq(assoc['version'], version)
+ ksft_eq(assoc['rx-key'], spi)
+ ksft_true('tx-key' not in assoc)
+ assoc = cfg.pspnl.assoc_get({"id": tx['id']})
+ ksft_eq(assoc['dev-id'], cfg.psp_dev_id)
+ ksft_eq(assoc['version'], version)
+ ksft_eq(assoc['tx-key'], spi)
+ ksft_true('rx-key' not in assoc)
+
+ ids = [x['id'] for x in cfg.pspnl.assoc_get({}, dump=True)]
+ ksft_true(rx['id'] in ids)
+ ksft_true(tx['id'] in ids)
+
+ for assoc_id in (rx['id'], tx['id']):
+ cfg.pspnl.assoc_del({"id": assoc_id})
+ with ksft_raises(NlError) as cm:
+ cfg.pspnl.assoc_get({"id": assoc_id})
+ ksft_eq(cm.exception.nl_msg.error, -errno.ENOENT)
+ with ksft_raises(NlError) as cm:
+ cfg.pspnl.assoc_del({"id": assoc_id})
+ ksft_eq(cm.exception.nl_msg.error, -errno.ENOENT)
+
+
+def assoc_sockless_stale(cfg):
+ """ Test key rotation marks socket-less Rx assocs stale """
+ _init_psp_dev(cfg)
+
+ rx, tx = _sockless_assocs(cfg)
+ for assoc_id in (rx['id'], tx['id']):
+ ksft_true('stale' not in cfg.pspnl.assoc_get({"id": assoc_id}))
+ # The second rotation retires the device key they were created under
+ for i in range(2):
+ cfg.pspnl.key_rotate({"id": cfg.psp_dev_id})
+ stale = 'stale' in cfg.pspnl.assoc_get({"id": rx['id']})
+ ksft_eq(stale, i == 1, comment=f"rotation {i + 1}")
+ # A Tx assoc holds the peer's key, which the device key rotation
+ # doesn't affect
+ ksft_true('stale' not in cfg.pspnl.assoc_get({"id": tx['id']}))
+
+
+def assoc_sockless_bad(cfg):
+ """ Test socket-less associations with bad arguments """
+ _init_psp_dev(cfg)
+
+ with ksft_raises(NlError) as cm:
+ cfg.pspnl.rx_spi_alloc({"dev-id": cfg.psp_dev_id + 1234567,
+ "version": 0})
+ ksft_eq(cm.exception.nl_msg.error, -errno.ENODEV)
+
+ with ksft_raises(NlError) as cm:
+ cfg.pspnl.assoc_add({"dev-id": cfg.psp_dev_id + 1234567,
+ "version": 0,
+ "tx-key": {"spi": 1, "key": b'\0' * 16}})
+ ksft_eq(cm.exception.nl_msg.error, -errno.ENODEV)
+
+ # Key of the wrong size for the version
+ with ksft_raises(NlError) as cm:
+ cfg.pspnl.assoc_add({"dev-id": cfg.psp_dev_id,
+ "version": 0,
+ "tx-key": {"spi": 1, "key": b'\0' * 32}})
+ ksft_eq(cm.exception.nl_msg.error, -errno.EINVAL)
+
+ with ksft_raises(NlError) as cm:
+ cfg.pspnl.assoc_add({"dev-id": cfg.psp_dev_id, "version": 0})
+ ksft_eq(cm.exception.nl_msg.error, -errno.EINVAL)
+
+
+def assoc_sockless_tc(cfg):
+ """ Test the tc psp action takes the right kind of assoc """
+ _init_psp_dev(cfg)
+ _require_tc_psp(cfg)
+
+ rx, tx = _sockless_assocs(cfg)
+ unknown = max(x['id'] for x in cfg.pspnl.assoc_get({}, dump=True)) + 1
+
+ # encap takes a Tx assoc, decap none
+ for act in (f"encap assoc {unknown}", f"encap assoc {rx['id']}",
+ f"decap assoc {rx['id']}", f"decap assoc {tx['id']}"):
+ ret = cmd(f"tc action add action psp {act}", fail=False)
+ ksft_ne(ret.ret, 0, comment=act)
+
+ for act in (f"encap assoc {tx['id']}", "decap"):
+ tc(f"action add action psp {act} index 100")
+ defer(tc, "action del action psp index 100").exec()
+
+
+@ksft_variants(_get_psp_ver_ip_variants())
+def data_tc_basic_send(cfg, version, ipver):
+ """ Test data send with socket-less assocs and the tc psp action """
+ cfg.require_ipver(ipver)
+ _init_psp_dev(cfg)
+ _require_tc_psp(cfg)
+ if version:
+ _require_psp_version(cfg, version)
+
+ rx_pkts = _get_stat(cfg, 'rx-packets')
+ tx_pkts = _get_stat(cfg, 'tx-packets')
+
+ s, _, _ = _tc_psp_conn(cfg, version, ipver)
+
+ data_len = _send_careful(cfg, s, 100)
+ _check_data_rx(cfg, data_len)
+ # ... and the ACKs got through decap
+ _check_data_outq(s, 0)
+
+ ksft_gt(_get_stat(cfg, 'rx-packets'), rx_pkts)
+ ksft_gt(_get_stat(cfg, 'tx-packets'), tx_pkts)
+ ksft_gt(_tc_action_stats(cfg, "ingress")["packets"], 0)
+ ksft_gt(_tc_action_stats(cfg, "egress")["packets"], 0)
+ _close_psp_conn(cfg, s)
+
+
+def data_tc_no_decap(cfg):
+ """ Test decrypted packets don't reach a plain socket without decap """
+ _init_psp_dev(cfg)
+ _require_tc_psp(cfg)
+
+ s, _, _ = _tc_psp_conn(cfg, 0, cfg.addr_ipver, decap=False)
+
+ # Without ACKs we can't send more than the initial window
+ data_len = _send_careful(cfg, s, 1)
+ _check_data_rx(cfg, data_len)
+ # The responder's ACKs come encrypted, and get dropped
+ _check_data_outq(s, data_len, force_wait=True)
+ _close_psp_conn(cfg, s)
+
+
+def data_tc_assoc_del(cfg):
+ """ Test deleting assocs while tc uses them: encap holds its Tx one """
+ _init_psp_dev(cfg)
+ _require_tc_psp(cfg)
+
+ s, rx, tx = _tc_psp_conn(cfg, 0, cfg.addr_ipver)
+
+ data_len = _send_careful(cfg, s, 10)
+ _check_data_rx(cfg, data_len)
+
+ cfg.pspnl.assoc_del({"id": rx['id']})
+ cfg.pspnl.assoc_del({"id": tx['id']})
+
+ data_len += _send_careful(cfg, s, 10)
+ _check_data_rx(cfg, data_len)
+ _check_data_outq(s, 0)
+ _close_psp_conn(cfg, s)
+
+
+def data_tc_decap_match(cfg):
+ """ Test decap only takes what the filter's PSP matches allow """
+ _init_psp_dev(cfg)
+ _require_tc_psp(cfg)
+
+ ipver = cfg.addr_ipver
+ s, rx, _ = _tc_psp_conn(cfg, 0, ipver)
+ spi, dev = rx['rx-key']['spi'], cfg.psp_dev_id
+
+ def match(m_spi, m_ver, m_dev):
+ return f"psp_spi {m_spi:#x} psp_ver {m_ver} psp_dev {m_dev}"
+
+ data_len = _send_careful(cfg, s, 10)
+ _check_data_rx(cfg, data_len)
+ _check_data_outq(s, 0)
+
+ # flower can only replace a filter with one matching the same keys
+ def decap_filter(keys):
+ tc(f"filter replace dev {cfg.ifname} ingress pref 10 handle 1 "
+ f"protocol {_tc_proto(ipver)} flower {keys} action psp decap")
+
+ for keys in (match(spi ^ 1, 0, dev), match(spi, 1, dev),
+ match(spi, 0, dev + 1)):
+ # The responder's ACKs don't match, and the socket drops them
+ decap_filter(keys)
+ n = _send_careful(cfg, s, 1)
+ data_len += n
+ _check_data_rx(cfg, data_len)
+ _check_data_outq(s, n, force_wait=True)
+
+ # Once they match again, the retransmits get through
+ decap_filter(match(spi, 0, dev))
+ _wait_data_outq(s, 0)
+ _close_psp_conn(cfg, s)
+
+
+def data_tc_encap_wrong_dev(cfg):
+ """ Test packets with an assoc sent to another device are dropped """
+ _init_psp_dev(cfg)
+ _require_tc_psp(cfg)
+
+ ip("link add psp-dummy0 type dummy")
+ defer(ip, "link del psp-dummy0")
+ ip("link set psp-dummy0 up")
+
+ ipver = cfg.addr_ipver
+ s, _, tx = _tc_psp_conn(cfg, 0, ipver)
+
+ sport = s.getsockname()[1]
+ tc(f"filter replace dev {cfg.ifname} egress pref 10 handle 1 "
+ f"protocol {_tc_proto(ipver)} flower ip_proto tcp src_port {sport} "
+ f"action psp encap assoc {tx['id']} pipe "
+ f"action mirred egress redirect dev psp-dummy0")
+
+ def dummy_drops():
+ dev = ip("-s link show dev psp-dummy0", json=True)[0]
+ return dev['stats64']['tx']['dropped']
+
+ drops = dummy_drops()
+ data_len = _send_careful(cfg, s, 10)
+ _check_data_outq(s, data_len, force_wait=True)
+ _check_data_rx(cfg, 0)
+ ksft_gt(dummy_drops(), drops)
+ _close_psp_conn(cfg, s)
+
+
+def data_tc_encap_short_hdr(cfg):
+ """ Test encap drops an IPv4 packet shorter than its header length """
+ cfg.require_ipver("4")
+ _init_psp_dev(cfg)
+ _require_tc_psp(cfg)
+
+ _, tx = _sockless_assocs(cfg)
+ loc, rem = cfg.addr_v["4"], cfg.remote_addr_v["4"]
+ _tc_clsact(cfg.ifname)
+ # An IP protocol nothing else sends, so the filter only takes the
+ # test's frame
+ tc(f"filter add dev {cfg.ifname} egress pref 10 handle 1 protocol ip "
+ f"flower ip_proto 0xfd dst_ip {rem} "
+ f"action psp encap assoc {tx['id']}")
+ defer(tc, f"filter del dev {cfg.ifname} egress pref 10 handle 1 "
+ f"protocol ip flower")
+
+ # A 20 byte IPv4 header which claims 60 (ihl 15), and nothing after
+ # it. Encap must not take the 40 missing bytes for header. (Its total
+ # length check would drop it too: this covers the IPv4 length checks
+ # together.)
+ iph = struct.pack("!BBHHHBBH4s4s", 0x4f, 0, 60, 0, 0, 64, 253, 0,
+ socket.inet_aton(loc), socket.inet_aton(rem))
+ eth = (b"\xff" * 6 + bytes.fromhex(cfg.dev["address"].replace(":", "")) +
+ struct.pack("!H", 0x0800))
+
+ drops = _tc_action_stats(cfg, "egress")["drops"]
+ with socket.socket(socket.AF_PACKET, socket.SOCK_RAW) as ps:
+ ps.bind((cfg.ifname, 0x0800))
+ try:
+ ps.send(eth + iph)
+ except OSError as e:
+ # The drop on egress is reported to the sender
+ if e.errno != errno.ENOBUFS:
+ raise
+ ksft_eq(_tc_action_stats(cfg, "egress")["drops"], drops + 1,
+ comment="short IPv4 header dropped by encap")
+
+
+def data_tc_encap_too_big(cfg):
+ """ Test packets too big for the MTU once encapsulated are dropped """
+ _init_psp_dev(cfg)
+ _require_tc_psp(cfg)
+
+ ipver = cfg.addr_ipver
+ _, tx = _sockless_assocs(cfg)
+ port = rand_port(socket.SOCK_DGRAM)
+ _tc_clsact(cfg.ifname)
+ tc(f"filter add dev {cfg.ifname} egress pref 10 handle 1 "
+ f"protocol {_tc_proto(ipver)} flower ip_proto udp dst_port {port} "
+ f"action psp encap assoc {tx['id']}")
+ defer(tc, f"filter del dev {cfg.ifname} egress pref 10 handle 1 "
+ f"protocol {_tc_proto(ipver)} flower")
+
+ # The UDP payload which fits the MTU once the device adds UDP, the PSP
+ # header and the ICV (40 bytes)
+ mtu = ip(f"link show dev {cfg.ifname}", json=True)[0]['mtu']
+ room = mtu - 40 - (20 if ipver == "4" else 40) - 8
+
+ family = socket.AF_INET if ipver == "4" else socket.AF_INET6
+ dst = (cfg.remote_addr_v[ipver], port)
+ with socket.socket(family, socket.SOCK_DGRAM) as s:
+ s.sendto(b'x' * room, dst)
+ with ksft_raises(OSError) as cm:
+ s.sendto(b'x' * (room + 1), dst)
+ ksft_eq(cm.exception.errno, errno.EMSGSIZE)
+
+ # GSO (UDP_SEGMENT): each segment must fit
+ s.setsockopt(socket.IPPROTO_UDP, UDP_SEGMENT, room)
+ s.sendto(b'x' * room * 2, dst)
+ s.setsockopt(socket.IPPROTO_UDP, UDP_SEGMENT, room + 1)
+ with ksft_raises(OSError) as cm:
+ s.sendto(b'x' * (room + 1) * 2, dst)
+ ksft_eq(cm.exception.errno, errno.EMSGSIZE)
+ ksft_ge(_tc_action_stats(cfg, "egress")["packets"], 4)
+
+
+def data_tc_encap_psp_sock(cfg):
+ """
+ Test psp encap drops packets of a PSP socket, which already have a crypto
+ offload
+ """
+
+ _init_psp_dev(cfg)
+ _require_tc_psp(cfg)
+
+ s = _make_psp_conn(cfg)
+ defer(_tc_conn_close, cfg, s)
+ rx = cfg.pspnl.rx_assoc({"version": 0,
+ "dev-id": cfg.psp_dev_id,
+ "sock-fd": s.fileno()})['rx-key']
+ tx = _spi_xchg(s, rx)
+ cfg.pspnl.tx_assoc({"dev-id": cfg.psp_dev_id,
+ "version": 0,
+ "tx-key": tx,
+ "sock-fd": s.fileno()})
+ data_len = _send_careful(cfg, s, 10)
+ _check_data_rx(cfg, data_len)
+
+ _, sl_tx = _sockless_assocs(cfg)
+ _tc_clsact(cfg.ifname)
+ sport = s.getsockname()[1]
+ tc(f"filter add dev {cfg.ifname} egress pref 10 handle 1 "
+ f"protocol {_tc_proto(cfg.addr_ipver)} flower ip_proto tcp "
+ f"src_port {sport} action psp encap assoc {sl_tx['id']}")
+ flt = defer(tc, f"filter del dev {cfg.ifname} egress pref 10 handle 1 "
+ f"protocol {_tc_proto(cfg.addr_ipver)} flower")
+
+ n = _send_careful(cfg, s, 1)
+ _check_data_outq(s, n, force_wait=True)
+ _check_data_rx(cfg, data_len)
+ ksft_gt(_tc_action_stats(cfg, "egress")["drops"], 0)
+
+ # Without the rule, the retransmits get through
+ flt.exec()
+ _wait_data_outq(s, 0)
+ _check_data_rx(cfg, data_len + n)
+ _close_psp_conn(cfg, s)
+
+
+def data_tc_encap_ingress(cfg):
+ """ Test packets with an assoc aren't redirected to ingress """
+ _init_psp_dev(cfg)
+ _require_tc_psp(cfg)
+
+ _, tx = _sockless_assocs(cfg)
+ # Traffic to a local address leaves through lo, and comes back on its
+ # ingress. With the dst gone, input routing takes a loopback source for
+ # a martian, and needs accept_local for a local one.
+ sysctl = "net.ipv4.conf.lo.accept_local"
+ old = cmd(f"sysctl -n {sysctl}").stdout.strip()
+ cmd(f"sysctl -w {sysctl}=1")
+ defer(cmd, f"sysctl -w {sysctl}={old}")
+ ip("link add psp-dummy1 type dummy")
+ defer(ip, "link del psp-dummy1")
+ ip("addr add 203.0.113.1/32 dev psp-dummy1")
+ fams = [(socket.AF_INET, "203.0.113.1", "ip")]
+ if cmd("ip addr add 2001:db8:ff::1/128 dev psp-dummy1 nodad",
+ fail=False).ret == 0:
+ fams.append((socket.AF_INET6, "2001:db8:ff::1", "ipv6"))
+ else:
+ ksft_pr("no IPv6 on psp-dummy1, IPv4 only")
+ ip("link set psp-dummy1 up")
+ _tc_clsact("lo")
+ for family, addr, proto in fams:
+ with socket.socket(family, socket.SOCK_DGRAM) as srv, \
+ socket.socket(family, socket.SOCK_DGRAM) as s:
+ srv.bind((addr, 0))
+ srv.settimeout(0.5)
+ port = srv.getsockname()[1]
+ flt = (f"dev lo egress pref 10 handle 1 protocol {proto} "
+ f"flower ip_proto udp dst_port {port}")
+ redir = "action mirred ingress redirect dev lo"
+
+ tc(f"filter add {flt} {redir}")
+ del_flt = defer(tc, "filter del dev lo egress pref 10 "
+ f"handle 1 protocol {proto} flower")
+ s.sendto(b'clear', (addr, port))
+ ksft_eq(srv.recv(64), b'clear', comment=proto)
+
+ # After encap the packet is marked decrypted: received, that
+ # would read as decrypted by the device
+ tc(f"filter replace {flt} action psp encap assoc {tx['id']} "
+ f"pipe {redir}")
+ s.sendto(b'encap', (addr, port))
+ with ksft_raises(socket.timeout):
+ srv.recv(64)
+ flts = tc("-s filter show dev lo egress pref 10", json=True)
+ mirred = [a["stats"] for f in flts
+ for a in f.get("options", {}).get("actions", [])
+ if a["kind"] == "mirred"]
+ ksft_eq(mirred[0]["overlimits"], 1, comment=proto)
+ del_flt.exec()
+
+
+def removal_device_tc(cfg):
+ """ Test removing a PSD with tc actions holding its assocs """
+ cfg.require_nsim()
+ _require_tc_psp(cfg)
+ # Only look the device up: enabling PSP would register an undo for the
+ # device which the test removes
+ if not hasattr(cfg, 'psp_dev_id'):
+ for dev in cfg.pspnl.dev_get({}, dump=True):
+ if dev['ifindex'] == cfg.ifindex:
+ cfg.psp_info = dev
+ cfg.psp_dev_id = dev['id']
+ break
+ else:
+ raise KsftSkipEx("No PSP devices found")
+
+ rx, tx = _sockless_assocs(cfg)
+ tc(f"action add action psp encap assoc {tx['id']} index 100")
+ defer(tc, "action del action psp index 100")
+
+ __nsim_psp_rereg(cfg)
+
+ # Unregistering the device drops its socket-less assocs
+ for assoc_id in (rx['id'], tx['id']):
+ with ksft_raises(NlError) as cm:
+ cfg.pspnl.assoc_get({"id": assoc_id})
+ ksft_eq(cm.exception.nl_msg.error, -errno.ENOENT)
+
+
def _check_assoc_list(cfg, psp_dev_id, ifindex, nsid=None):
"""Verify assoc-list contains device with given ifindex, no duplicates."""
dev_info = cfg.pspnl.dev_get({'id': psp_dev_id})
@@ -1173,7 +1760,7 @@ def main() -> None:
cfg.comm_port),
timeout=1)
- cases = [data_basic_send, data_mss_adjust]
+ cases = [data_basic_send, data_mss_adjust, data_tc_basic_send]
if has_cont:
cases += [
--
2.53.0-Meta
next prev parent reply other threads:[~2026-11-11 0:56 UTC|newest]
Thread overview: 16+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-11 0:55 [PATCH net-next v1 00/15] psp: introduce socket-less associations Todd Freed
2026-11-11 0:55 ` [PATCH net-next v1 01/15] psp: get netns from net_device instead of sock Todd Freed
2026-11-11 0:55 ` [PATCH net-next v1 02/15] psp: take the psp nexthdr from the packet on encap Todd Freed
2026-11-11 0:55 ` [PATCH net-next v1 03/15] psp: declare psp_assoc_set_tx() in psp.h Todd Freed
2026-11-11 0:55 ` [PATCH net-next v1 04/15] psp: add support for socket-less psp associations Todd Freed
2026-11-11 0:55 ` [PATCH net-next v1 05/15] net: add METADATA_PSP dst to carry a psp association on an skb Todd Freed
2026-11-11 0:55 ` [PATCH net-next v1 06/15] net/sched: act_mirred: don't send decrypted skbs from egress to ingress Todd Freed
2026-11-11 0:55 ` [PATCH net-next v1 07/15] bpf: don't redirect " Todd Freed
2026-11-11 0:55 ` [PATCH net-next v1 08/15] psp: only give time-wait sockets of psp connections a validator Todd Freed
2026-11-11 0:55 ` [PATCH net-next v1 09/15] net/sched: add act_psp for psp transport mode Todd Freed
2026-11-11 0:55 ` [PATCH net-next v1 10/15] net/sched: flower: add psp_spi, psp_ver, and psp_dev matches Todd Freed
2026-11-11 0:55 ` Todd Freed [this message]
2026-11-11 0:55 ` [PATCH net-next v1 12/15] selftests: drv-net: psp: test that software encap on ingress drops Todd Freed
2026-11-11 0:55 ` [PATCH net-next v1 13/15] selftests: tc-testing: run dependsOn probes outside the test namespace Todd Freed
2026-11-11 0:55 ` [PATCH net-next v1 14/15] selftests: tc-testing: add tests for the psp action Todd Freed
2026-11-11 0:55 ` [PATCH net-next v1 15/15] Documentation: networking: psp: document socket-less associations Todd Freed
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261111005533.6436-12-toddfreed@meta.com \
--to=toddfreed@meta.com \
--cc=andrew+netdev@lunn.ch \
--cc=bjorn@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=daniel.zahka@gmail.com \
--cc=daniel@iogearbox.net \
--cc=davem@davemloft.net \
--cc=donald.hunter@gmail.com \
--cc=edumazet@kernel.org \
--cc=horms@kernel.org \
--cc=jhs@mojatatu.com \
--cc=jiri@resnulli.us \
--cc=john.fastabend@gmail.com \
--cc=kuba@kernel.org \
--cc=linux-doc@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=linux-rdma@vger.kernel.org \
--cc=martin.lau@linux.dev \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=saeedm@nvidia.com \
--cc=tariqt@nvidia.com \
--cc=weiwan@google.com \
--cc=willemdebruijn.kernel@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®