mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
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


  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®