* [PATCH net v2 1/2] psp: fix transport mode encap with ipv6 extension headers
2026-10-22 22:30 [PATCH net v2 0/2] psp: fix transport mode with ipv6 extension headers Todd Freed
@ 2026-10-22 22:30 ` Todd Freed
2026-10-22 22:30 ` [PATCH net v2 2/2] selftests: drv-net: psp: test ipv6 tcp socket with " Todd Freed
1 sibling, 0 replies; 3+ messages in thread
From: Todd Freed @ 2026-10-22 22:30 UTC (permalink / raw)
To: netdev, toddfreed
Cc: Daniel Zahka, Jakub Kicinski, Willem de Bruijn, David S. Miller,
Eric Dumazet, Paolo Abeni, Simon Horman, Andrew Lunn, Shuah Khan,
Raed Salem, Rahul Rameshbabu, Cosmin Ratiu, linux-kernel,
linux-kselftest, Björn Töpel
psp_dev_encapsulate() inserts the udp and psp headers and rewrites the
l3 next header to udp.
if the packet is an ipv6 packet with extension headers, the transport
header comes after them, so rewriting the next header of the ipv6 fixed
header produces a malformed packet.
a psp tcp socket emits such packets if certain socket options are set on
it (e.g. IPV6_DSTOPTS). with mlx5, these packets also go out
unencrypted.
fix by walking the extension header chain to rewrite the nexthdr on the
last one in the chain instead. the walk looks at the extension header
types only to find their lengths, and to reject these,
- NEXTHDR_FRAGMENT. this could work, but udp+psp would need to come
before it, which is different than how the other option types are
handled. for now, keep fragments unsupported for transport mode.
- NEXTHDR_ROUTING. the tcp checksum covers the final destination, but
drivers seed tso checksums from the daddr in the fixed hdr
(mlx5e_psp_handle_tx_skb()), so this would produce bad checksums.
could be supported with more work.
- NEXTHDR_NONE, and types which ipv6_ext_hdr() doesn't know.
on rx, the inverse process is performed in psp_dev_rcv(). transport mode
psp fragments are rejected on this path as before.
note that the stack doesn't fragment ipv6 tcp packets, so a psp socket
won't be able to exercise the tx fragment path.
Suggested-by: Daniel Zahka <daniel.zahka@gmail.com>
Fixes: fc724515741a ("psp: provide encapsulation helper for drivers")
Fixes: 0eddb8023cee ("psp: provide decapsulation and receive helper for drivers")
Signed-off-by: Todd Freed <toddfreed@meta.com>
---
net/psp/psp_main.c | 78 +++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 70 insertions(+), 8 deletions(-)
diff --git a/net/psp/psp_main.c b/net/psp/psp_main.c
index c9c1a8826b7f..0c009d7b08c0 100644
--- a/net/psp/psp_main.c
+++ b/net/psp/psp_main.c
@@ -4,6 +4,7 @@
#include <linux/list.h>
#include <linux/netdevice.h>
#include <linux/xarray.h>
+#include <net/ipv6.h>
#include <net/net_namespace.h>
#include <net/psp.h>
#include <net/udp.h>
@@ -185,6 +186,42 @@ unsigned int psp_key_size(u32 version)
}
EXPORT_SYMBOL(psp_key_size);
+/**
+ * psp_ipv6_nexthdr_off() - find the offset to the l4 proto nexthdr
+ * @hdr: ipv6 fixed header
+ * @len: l3 header total length
+ *
+ * Return: offset from @hdr, or -1 to drop the packet
+ */
+static int psp_ipv6_nexthdr_off(const u8 *hdr, unsigned int len)
+{
+ unsigned int nh = offsetof(struct ipv6hdr, nexthdr);
+ unsigned int off = sizeof(struct ipv6hdr);
+ const struct ipv6_opt_hdr *eh;
+ unsigned int rem;
+ u8 type;
+
+ while (off < len) {
+ eh = (const void *)(hdr + off);
+ type = hdr[nh];
+
+ if (!ipv6_ext_hdr(type) || type == NEXTHDR_NONE ||
+ type == NEXTHDR_FRAGMENT || type == NEXTHDR_ROUTING)
+ return -1;
+ rem = len - off;
+ if (rem < sizeof(*eh))
+ return -1;
+
+ nh = off + offsetof(struct ipv6_opt_hdr, nexthdr);
+ if (type == NEXTHDR_AUTH)
+ off += ipv6_authlen(eh);
+ else
+ off += ipv6_optlen(eh);
+ }
+
+ return off == len ? nh : -1;
+}
+
static void psp_write_headers(struct net *net, struct sk_buff *skb, __be32 spi,
u8 ver, unsigned int udp_len, __be16 sport)
{
@@ -243,7 +280,7 @@ static void psp_write_headers(struct net *net, struct sk_buff *skb, __be32 spi,
}
/* Encapsulate a TCP packet with PSP by adding the UDP+PSP headers and filling
- * them in.
+ * them in. With IPv6, they go after any extension headers.
*/
bool psp_dev_encapsulate(struct net *net, struct sk_buff *skb, __be32 spi,
u8 ver, __be16 sport)
@@ -251,10 +288,17 @@ bool psp_dev_encapsulate(struct net *net, struct sk_buff *skb, __be32 spi,
u32 network_len = skb_network_header_len(skb);
u32 ethr_len = skb_mac_header_len(skb);
u32 bufflen = ethr_len + network_len;
-
- if (skb->protocol != htons(ETH_P_IP) &&
- skb->protocol != htons(ETH_P_IPV6))
+ int nh_off = 0;
+
+ if (skb->protocol == htons(ETH_P_IPV6)) {
+ /* UDP + PSP after the extension headers, if any */
+ nh_off = psp_ipv6_nexthdr_off(skb_network_header(skb),
+ network_len);
+ if (nh_off < 0)
+ return false;
+ } else if (skb->protocol != htons(ETH_P_IP)) {
return false;
+ }
if (skb_cow_head(skb, PSP_ENCAP_HLEN))
return false;
@@ -272,7 +316,7 @@ bool psp_dev_encapsulate(struct net *net, struct sk_buff *skb, __be32 spi,
ip_hdr(skb)->check =
ip_fast_csum((u8 *)ip_hdr(skb), ip_hdr(skb)->ihl);
} else {
- ipv6_hdr(skb)->nexthdr = IPPROTO_UDP;
+ skb_network_header(skb)[nh_off] = IPPROTO_UDP;
be16_add_cpu(&ipv6_hdr(skb)->payload_len, PSP_ENCAP_HLEN);
}
@@ -331,8 +375,23 @@ int psp_dev_rcv(struct sk_buff *skb, u16 dev_id, u8 generation, bool strip_icv)
return -EINVAL;
} else {
struct ipv6hdr *ipv6h = (struct ipv6hdr *)(skb->data + l2_hlen);
+ u8 nexthdr = ipv6h->nexthdr;
+ __be16 frag_off;
+ int off;
- is_udp = ipv6h->nexthdr == IPPROTO_UDP;
+ /* UDP + PSP come after any extension headers. Fragment and
+ * routing headers are not supported in transport mode.
+ */
+ off = ipv6_skip_exthdr(skb, l2_hlen + sizeof(*ipv6h), &nexthdr,
+ &frag_off);
+ if (unlikely(off < 0 || frag_off))
+ return -EINVAL;
+
+ is_udp = nexthdr == IPPROTO_UDP;
+ l3_hlen = off - l2_hlen;
+ if (l3_hlen != sizeof(struct ipv6hdr) &&
+ !pskb_may_pull(skb, l2_hlen + l3_hlen + PSP_ENCAP_HLEN))
+ return -EINVAL;
}
if (unlikely(!is_udp))
@@ -386,11 +445,14 @@ int psp_dev_rcv(struct sk_buff *skb, u16 dev_id, u8 generation, bool strip_icv)
iph->check = ip_fast_csum((u8 *)iph, iph->ihl);
} else {
struct ipv6hdr *ipv6h = (struct ipv6hdr *)(skb->data + l2_hlen);
+ int nh_off = psp_ipv6_nexthdr_off((u8 *)ipv6h, l3_hlen);
- if (unlikely(ntohs(ipv6h->payload_len) < encap))
+ if (unlikely(nh_off < 0 ||
+ ntohs(ipv6h->payload_len) <
+ l3_hlen - sizeof(*ipv6h) + encap))
return -EINVAL;
- ipv6h->nexthdr = psph->nexthdr;
+ ((u8 *)ipv6h)[nh_off] = psph->nexthdr;
ipv6h->payload_len = htons(ntohs(ipv6h->payload_len) - encap);
}
--
2.53.0-Meta
^ permalink raw reply [flat|nested] 3+ messages in thread* [PATCH net v2 2/2] selftests: drv-net: psp: test ipv6 tcp socket with extension headers
2026-10-22 22:30 [PATCH net v2 0/2] psp: fix transport mode with ipv6 extension headers Todd Freed
2026-10-22 22:30 ` [PATCH net v2 1/2] psp: fix transport mode encap " Todd Freed
@ 2026-10-22 22:30 ` Todd Freed
1 sibling, 0 replies; 3+ messages in thread
From: Todd Freed @ 2026-10-22 22:30 UTC (permalink / raw)
To: netdev, toddfreed
Cc: Daniel Zahka, Jakub Kicinski, Willem de Bruijn, David S. Miller,
Eric Dumazet, Paolo Abeni, Simon Horman, Andrew Lunn, Shuah Khan,
Raed Salem, Rahul Rameshbabu, Cosmin Ratiu, linux-kernel,
linux-kselftest, Björn Töpel
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 and counted.
2/ an extension header set on the socket - with destination or
hop-by-hop options, the data is sent, counted, and received by the
peer. with a routing header, the packets are dropped, tx-packets is
unchanged, nothing reaches the peer, and the data stays queued.
3/ option removed - any queued data, then new data, is sent, counted,
and received.
phases 2 and 3 run three times: with a destination options header
(IPV6_DSTOPTS) and with a hop-by-hop options header (IPV6_HOPOPTS), each
holding one padn option, and with a segment routing header
(IPV6_RTHDR) whose only segment is the peer.
in phase 2, a software filter on the peer also counts udp packets from
the socket's address to the peer's. the psp packets reach the peer as
decrypted tcp packets, as expected. malformed or unencrypted packets
show up as udp, and the test asserts there are none. if the peer can't
take the filter, 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 the preceding patch:
- with netdevsim, the peer receives none of the data of phase 2, 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@meta.com>
---
tools/testing/selftests/drivers/net/config | 1 +
tools/testing/selftests/drivers/net/psp.py | 138 ++++++++++++++++++++-
2 files changed, 137 insertions(+), 2 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 a5b1e14f120f..47e8cb19f376 100755
--- a/tools/testing/selftests/drivers/net/psp.py
+++ b/tools/testing/selftests/drivers/net/psp.py
@@ -16,12 +16,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
@@ -120,6 +120,57 @@ 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_udp_from(cfg, src, dst):
+ """
+ Count what reaches the remote from @src to @dst as UDP, with a software
+ filter on its ingress (offloaded filter counters are refreshed
+ periodically). Returns a function reading the count, or None if the
+ filter can't be installed.
+ """
+ flt = (f"dev {cfg.remote_ifname} ingress protocol ipv6 pref 1 "
+ f"handle 0x5053 flower")
+ try:
+ qdiscs = tc(f"qdisc show dev {cfg.remote_ifname}", json=True,
+ host=cfg.remote)
+ if not any(q['kind'] == 'clsact' 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 src_ip {src} dst_ip {dst} "
+ f"ip_proto udp action drop", host=cfg.remote)
+ except CmdExitFailure as e:
+ ksft_pr(f"Can't count UDP 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
#
@@ -624,6 +675,89 @@ def removal_device_bi(cfg):
_close_conn(cfg, s)
+def data_send_ipv6_exthdr(cfg):
+ """ Test PSP on IPv6 packets with extension headers """
+ cfg.require_ipver("6")
+ _init_psp_dev(cfg)
+
+ s = _make_psp_conn(cfg, 0, "6")
+ try:
+ # In happy path, PSP traffic arrives at the remote decrypted as TCP
+ remote_udp = _remote_udp_from(cfg, s.getsockname()[0],
+ s.getpeername()[0])
+ if not remote_udp:
+ raise KsftSkipEx("can't count UDP on the remote")
+
+ 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 options: sent and counted
+ 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)
+
+ # An extension header with one PadN option, as destination options
+ # and as hop-by-hop options, and a segment routing header with the
+ # peer as its only segment
+ padn = bytes([0, 0, 1, 4, 0, 0, 0, 0])
+ srh = bytes([0, 2, 4, 0, 0, 0, 0, 0]) + \
+ socket.inet_pton(socket.AF_INET6, s.getpeername()[0])
+ for name, opt, val, sent in (
+ ("IPV6_DSTOPTS", socket.IPV6_DSTOPTS, padn, True),
+ ("IPV6_HOPOPTS", socket.IPV6_HOPOPTS, padn, True),
+ ("IPV6_RTHDR", socket.IPV6_RTHDR, srh, False)):
+ # 2. With the option. Destination and hop-by-hop options: sent
+ # encrypted, with UDP + PSP after the extension header, counted,
+ # and received. Routing header: dropped, not counted, and the
+ # data stays queued. Nothing reaches the peer as plain UDP.
+ s.setsockopt(socket.IPPROTO_IPV6, opt, val)
+ tx_pkts = _get_quiesced_stat(cfg, 'tx-packets')
+ udp = remote_udp()
+ queued = _send_careful(cfg, s, 20)
+ if sent:
+ total += queued
+ wait_outq_empty()
+ ksft_gt(_get_quiesced_stat(cfg, 'tx-packets'), tx_pkts,
+ comment=name)
+ else:
+ _check_data_outq(s, queued, force_wait=True)
+ ksft_eq(_get_quiesced_stat(cfg, 'tx-packets'), tx_pkts,
+ comment=name)
+ ksft_eq(remote_udp(), udp, comment=f"{name}: UDP at the remote")
+ _check_data_rx(cfg, total)
+ if not sent:
+ total += queued
+
+ # 3. Option removed: any queued data, then new data, sent and
+ # counted
+ 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,
+ comment=name)
+ _check_data_rx(cfg, total)
+ 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
^ permalink raw reply [flat|nested] 3+ messages in thread