mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH net-next v1 00/15] psp: introduce socket-less associations
@ 2026-11-11  0:55 Todd Freed
  2026-11-11  0:55 ` [PATCH net-next v1 01/15] psp: get netns from net_device instead of sock Todd Freed
                   ` (14 more replies)
  0 siblings, 15 replies; 16+ messages in thread
From: Todd Freed @ 2026-11-11  0:55 UTC (permalink / raw)
  To: netdev, toddfreed
  Cc: David S. Miller, Andrew Lunn, Björn Töpel, bpf,
	Daniel Borkmann, Daniel Zahka, Donald Hunter, Eric Dumazet,
	Jakub Kicinski, Jamal Hadi Salim, Jiri Pirko, John Fastabend,
	linux-doc, linux-kernel, linux-kselftest, linux-rdma,
	Martin KaFai Lau, Paolo Abeni, Saeed Mahameed, Simon Horman,
	Tariq Toukan, Wei Wang, Willem de Bruijn

psp associations today are bound to a tcp socket when created. traffic
on that socket is encapsulated in transport-mode psp, and encrypted.

this series adds associations which aren't bound to a socket, which
allows psp encapsulation and encryption to be applied to forwarded
traffic. you can use this to transparently apply psp tunnels to flows,
with help from userspace to manage the tc rules and orchestrate key
exchange.

the lifecycle of socket-less associations is managed over the psp
netlink family. socket-less associations are stored in a small registry,
and are addressable by id. the registry does not contain socket
associations.

socket-less associations are on the device's association lists, like
socket associations. rx ones are therefore marked stale by key rotation
in the same way. in this series, nothing acts on socket-less rx
associations on key rotation. instead, userspace should replace decap
rules for a SPI before its device key has been rotated twice. a
follow-up will bring enforcement.

series structure,

1, 2/ prepares psp to work with non-tcp packets

3, 4/ adds the netlink commands for managing socket-less associations

5/ adds a METADATA_PSP dst which carries the socket-less psp association
on an skb for tx, and the transmit checks for it

6, 7/ keep mirred and bpf from sending decrypted skbs from egress to
ingress, since skb->decrypted depends on direction

8/ limit time-wait psp socket validator to sockets actually in
time-wait. the new psp actions check for presence of the validator

9/ adds the act_psp tc actions, psp encap and psp decap. in software,
encap is egress only. 

10/ adds flower matches on psp header fields, for rx matching

11, 12/ add drv-net tests (netdevsim and hardware)

13/ makes tc-testing run dependsOn probes outside the test namespace,
where they currently always fail

14/ adds tc-testing tests

15/ Documentation for socket-less psp associations

a following series adds hardware offload of the psp actions and flower
matches, psp tunnel mode (which is offload only and therefore not
included here), encap on ingress (also offload only), and the mlx5
implementation for switchdev mode. It's available in the tree below.

relation to the psp fixes in net:

this series is based on net-next, and overlaps with "psp, tls: don't
send offloaded contents in cleartext" in net:

 https://lore.kernel.org/netdev/20261111001831.3710881-1-toddfreed@meta.com

when net is merged into net-next:

 - net/psp/psp_main.c conflicts around psp_dev_encap_ok(), which both
   add: net's only allows tcp, this series' (patch 2) takes any l4
   protocol. keep this series' version of the function, and its
   comments above it and above psp_dev_encapsulate().
 - tools/testing/selftests/drivers/net/psp.py and
   tools/testing/selftests/drivers/net/config conflict: keep both sides
   (imports, constants, the list of test cases; the NET_ACT_* options).

relation to the netfilter fixes in nf-next:

packets which a psp device decrypted don't have to end in a local psp
socket. they can already be forwarded or bridged today, but with
socket-less associations that is the intended use, e.g. a router that
decaps and forwards. netfilter handles two cases itself, outside the ip
stack paths which the net series fixes, and would send such a packet's
decrypted contents in clear text:

 - the bridge refragments packets which bridge conntrack reassembled
   (br_ip6_fragment(), nf_br_ip_fragment()), without the check of "net:
   don't fragment decrypted skbs".
 - the reject code builds its own icmp errors and tcp resets
   (nf_reject_skb_v4_unreach() and friends), without icmp_send() and
   icmpv6_send(), which the net series' icmp patches cover.

a separate series to nf-next applies the net series' rules to both:

  "netfilter: bridge: don't refragment decrypted skbs"
  "netfilter: reject: don't reply to decrypted skbs"

  https://lore.kernel.org/netfilter-devel/20261111003111.3826942-1-toddfreed@meta.com

it doesn't depend on this series, and the two don't conflict.

trees:

the full tree, with the net fixes, the nf-next series, this series and
the mlx5 offload series, and the conflicts above resolved, is at:

 https://github.com/toddfreed/psptc branch psptc

iproute2 tree, with support for the psp action and the psp flower
matches, based on iproute2-next. the selftests need it:

 https://github.com/toddfreed/iproute2-psptc branch tc-psp

testing:

 - netdevsim: drivers/net psp.py passes (57 tests), and tc-testing
   psp.json (17 tests).
 - mlx5 (ConnectX-7, FW 28.48), two hosts: drivers/net psp.py passes:
   46 pass, 8 skip (GMAC versions not supported by the device),
   3 xfail (netdevsim only).
 - each patch builds without warnings with gcc and clang, allmodconfig,
   W=1.
Todd Freed (15):
  psp: get netns from net_device instead of sock
  psp: take the psp nexthdr from the packet on encap
  psp: declare psp_assoc_set_tx() in psp.h
  psp: add support for socket-less psp associations
  net: add METADATA_PSP dst to carry a psp association on an skb
  net/sched: act_mirred: don't send decrypted skbs from egress to
    ingress
  bpf: don't redirect decrypted skbs from egress to ingress
  psp: only give time-wait sockets of psp connections a validator
  net/sched: add act_psp for psp transport mode
  net/sched: flower: add psp_spi, psp_ver, and psp_dev matches
  selftests: drv-net: psp: test socket-less associations and the tc psp
    action
  selftests: drv-net: psp: test that software encap on ingress drops
  selftests: tc-testing: run dependsOn probes outside the test namespace
  selftests: tc-testing: add tests for the psp action
  Documentation: networking: psp: document socket-less associations

 Documentation/netlink/specs/psp.yaml          |  98 ++-
 Documentation/netlink/specs/tc.yaml           |  14 +
 Documentation/networking/psp.rst              | 139 +++-
 MAINTAINERS                                   |   3 +
 .../mellanox/mlx5/core/en_accel/psp_rxtx.c    |   2 +-
 .../net/ethernet/mellanox/mlx5/core/en_main.c |  33 +
 drivers/net/netdevsim/psp.c                   |   2 +-
 include/linux/skbuff.h                        |   4 +
 include/net/dst_metadata.h                    |  19 +
 include/net/flow_dissector.h                  |  18 +
 include/net/psp/functions.h                   |  91 ++-
 include/net/psp/types.h                       |   1 +
 include/net/tc_act/tc_psp.h                   |  23 +
 include/uapi/linux/pkt_cls.h                  |   6 +
 include/uapi/linux/psp.h                      |   6 +
 include/uapi/linux/tc_act/tc_psp.h            |  28 +
 net/core/dev.c                                |  48 +-
 net/core/dst.c                                |   5 +
 net/core/filter.c                             |   9 +
 net/core/flow_dissector.c                     |  27 +
 net/psp/Makefile                              |   2 +-
 net/psp/psp-nl-gen.c                          |  60 ++
 net/psp/psp-nl-gen.h                          |   8 +
 net/psp/psp.h                                 |   7 +
 net/psp/psp_assoc.c                           | 120 +++
 net/psp/psp_main.c                            |  54 +-
 net/psp/psp_nl.c                              | 261 ++++++
 net/psp/psp_sock.c                            |  32 +-
 net/sched/Kconfig                             |  13 +
 net/sched/Makefile                            |   1 +
 net/sched/act_mirred.c                        |   6 +
 net/sched/act_psp.c                           | 439 ++++++++++
 net/sched/cls_flower.c                        |  64 ++
 tools/testing/selftests/drivers/net/config    |   5 +
 tools/testing/selftests/drivers/net/psp.py    | 750 +++++++++++++++++-
 tools/testing/selftests/tc-testing/config     |   2 +
 .../tc-testing/tc-tests/actions/psp.json      | 459 +++++++++++
 tools/testing/selftests/tc-testing/tdc.py     |   2 +-
 tools/testing/selftests/tc-testing/tdc.sh     |   1 +
 39 files changed, 2812 insertions(+), 50 deletions(-)
 create mode 100644 include/net/tc_act/tc_psp.h
 create mode 100644 include/uapi/linux/tc_act/tc_psp.h
 create mode 100644 net/psp/psp_assoc.c
 create mode 100644 net/sched/act_psp.c
 create mode 100644 tools/testing/selftests/tc-testing/tc-tests/actions/psp.json


base-commit: ed04b202027249e2769420893220f54a56e0c59a
-- 
2.53.0-Meta


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH net-next v1 01/15] psp: get netns from net_device instead of sock
  2026-11-11  0:55 [PATCH net-next v1 00/15] psp: introduce socket-less associations Todd Freed
@ 2026-11-11  0:55 ` 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
                   ` (13 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Todd Freed @ 2026-11-11  0:55 UTC (permalink / raw)
  To: netdev, toddfreed
  Cc: David S. Miller, Andrew Lunn, Björn Töpel, bpf,
	Daniel Borkmann, Daniel Zahka, Donald Hunter, Eric Dumazet,
	Jakub Kicinski, Jamal Hadi Salim, Jiri Pirko, John Fastabend,
	linux-doc, linux-kernel, linux-kselftest, linux-rdma,
	Martin KaFai Lau, Paolo Abeni, Saeed Mahameed, Simon Horman,
	Tariq Toukan, Wei Wang, Willem de Bruijn

every psp skb is related to a socket today, but that will no longer
be true once a psp association can be attached to the skb by other
means, such as tc flower rules added later in the series.

get the netns from the transmitting device instead.

for the outer source port range, psp_write_headers() now explicitly uses
sock_net(sk) in the socket case specifically, to maintain the existing
behavior.

Signed-off-by: Todd Freed <toddfreed@meta.com>
---
 drivers/net/ethernet/mellanox/mlx5/core/en_accel/psp_rxtx.c | 2 +-
 drivers/net/netdevsim/psp.c                                 | 2 +-
 net/psp/psp_main.c                                          | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/drivers/net/ethernet/mellanox/mlx5/core/en_accel/psp_rxtx.c b/drivers/net/ethernet/mellanox/mlx5/core/en_accel/psp_rxtx.c
index 348fd7a96261..8b474f75dd61 100644
--- a/drivers/net/ethernet/mellanox/mlx5/core/en_accel/psp_rxtx.c
+++ b/drivers/net/ethernet/mellanox/mlx5/core/en_accel/psp_rxtx.c
@@ -165,7 +165,7 @@ bool mlx5e_psp_handle_tx_skb(struct net_device *netdev,
 			     struct mlx5e_accel_tx_psp_state *psp_st)
 {
 	struct mlx5e_priv *priv = netdev_priv(netdev);
-	struct net *net = sock_net(skb->sk);
+	struct net *net = dev_net(netdev);
 
 	if (!mlx5e_psp_set_state(priv, skb, psp_st))
 		return true;
diff --git a/drivers/net/netdevsim/psp.c b/drivers/net/netdevsim/psp.c
index b7452c49b581..0701f38aee6d 100644
--- a/drivers/net/netdevsim/psp.c
+++ b/drivers/net/netdevsim/psp.c
@@ -36,7 +36,7 @@ nsim_do_psp(struct sk_buff *skb, struct netdevsim *ns,
 		goto out_unlock;
 	}
 
-	net = sock_net(skb->sk);
+	net = dev_net(ns->netdev);
 	if (!psp_dev_encapsulate(net, skb, pas->tx.spi, pas->version, 0)) {
 		rc = SKB_DROP_REASON_PSP_OUTPUT;
 		goto out_unlock;
diff --git a/net/psp/psp_main.c b/net/psp/psp_main.c
index 273b010d2355..e09f6fec948d 100644
--- a/net/psp/psp_main.c
+++ b/net/psp/psp_main.c
@@ -221,7 +221,7 @@ static void psp_write_headers(struct net *net, struct sk_buff *skb, __be32 spi,
 		/* These operations are cheap, no need to cache the result
 		 * in another socket field.
 		 */
-		inet_get_local_port_range(net, &min, &max);
+		inet_get_local_port_range(sock_net(sk), &min, &max);
 		/* Since this is being sent on the wire obfuscate hash a bit
 		 * to minimize possibility that any useful information to an
 		 * attacker is leaked. Only upper 16 bits are relevant in the
-- 
2.53.0-Meta


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH net-next v1 02/15] psp: take the psp nexthdr from the packet on encap
  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 ` Todd Freed
  2026-11-11  0:55 ` [PATCH net-next v1 03/15] psp: declare psp_assoc_set_tx() in psp.h Todd Freed
                   ` (12 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Todd Freed @ 2026-11-11  0:55 UTC (permalink / raw)
  To: netdev, toddfreed
  Cc: David S. Miller, Andrew Lunn, Björn Töpel, bpf,
	Daniel Borkmann, Daniel Zahka, Donald Hunter, Eric Dumazet,
	Jakub Kicinski, Jamal Hadi Salim, Jiri Pirko, John Fastabend,
	linux-doc, linux-kernel, linux-kselftest, linux-rdma,
	Martin KaFai Lau, Paolo Abeni, Saeed Mahameed, Simon Horman,
	Tariq Toukan, Wei Wang, Willem de Bruijn

psp sockets support has thus far been limited to tcp, and
psp_dev_encapsulate() hardcodes IPPROTO_TCP as the psp nexthdr

socket-less psp associations will also support udp traffic. populate the
psp nexthdr from the ip header of the original packet instead.

with the psp nexthdr taken from the fixed header, psp_dev_encapsulate()
indicates some cases which are unsupported,

 - ipv6 extension headers present. more generally, any packet whose
   transport header isn't right after the fixed ip header. some of these
   could be supported in the future. not a behavior change for psp
   sockets.
 - ipv4 fragments. non-initial fragments have no l4 header, so this
   cannot work. not a behavior change for psp sockets.
 - already encapsulated for a tunnel (skb->encapsulation). their inner
   headers are already set up.

both callers of psp_dev_encapsulate(), mlx5 and netdevsim, drop the packet
when it returns false.

mlx5e also fixes up the tcp checksum of gso skbs for the device, which
is only valid for tcp. have the stack segment other gso skbs, e.g. udp
from socket-less associations, in software. likewise the device only
completes a tcp or udp checksum behind the psp header: have the stack
checksum the other CHECKSUM_PARTIAL skbs.

Signed-off-by: Todd Freed <toddfreed@meta.com>
---
 .../net/ethernet/mellanox/mlx5/core/en_main.c | 33 ++++++++++++
 net/psp/psp_main.c                            | 50 ++++++++++++++++---
 2 files changed, 75 insertions(+), 8 deletions(-)

diff --git a/drivers/net/ethernet/mellanox/mlx5/core/en_main.c b/drivers/net/ethernet/mellanox/mlx5/core/en_main.c
index c3c2c5720c6f..d091de1a5992 100644
--- a/drivers/net/ethernet/mellanox/mlx5/core/en_main.c
+++ b/drivers/net/ethernet/mellanox/mlx5/core/en_main.c
@@ -5132,6 +5132,23 @@ static netdev_features_t mlx5e_tunnel_features_check(struct mlx5e_priv *priv,
 	return features & ~(NETIF_F_CSUM_MASK | NETIF_F_GSO_MASK);
 }
 
+/* The L4 protocol right after the fixed IP header, which the PSP Tx path
+ * requires, is one the device can complete the checksum of
+ */
+static bool mlx5e_psp_l4_csum_ok(const struct sk_buff *skb)
+{
+	u8 proto;
+
+	if (skb->protocol == htons(ETH_P_IP))
+		proto = ip_hdr(skb)->protocol;
+	else if (skb->protocol == htons(ETH_P_IPV6))
+		proto = ipv6_hdr(skb)->nexthdr;
+	else
+		return false;
+
+	return proto == IPPROTO_TCP || proto == IPPROTO_UDP;
+}
+
 netdev_features_t mlx5e_features_check(struct sk_buff *skb,
 				       struct net_device *netdev,
 				       netdev_features_t features)
@@ -5140,6 +5157,22 @@ netdev_features_t mlx5e_features_check(struct sk_buff *skb,
 
 	features = vlan_features_check(skb, features);
 
+	/* The PSP tx path fixes up the TCP checksum of a GSO skb for the
+	 * device, which is only valid for TCP, and the device only completes
+	 * a TCP or UDP checksum behind the PSP header. Have the stack segment
+	 * and checksum other skbs.
+	 */
+	if ((skb_is_gso(skb) || skb->ip_summed == CHECKSUM_PARTIAL) &&
+	    mlx5e_psp_is_offload(skb, netdev)) {
+		if (skb_is_gso(skb) &&
+		    !(skb_shinfo(skb)->gso_type &
+		      (SKB_GSO_TCPV4 | SKB_GSO_TCPV6)))
+			features &= ~NETIF_F_GSO_MASK;
+		if (skb->ip_summed == CHECKSUM_PARTIAL &&
+		    !mlx5e_psp_l4_csum_ok(skb))
+			features &= ~NETIF_F_CSUM_MASK;
+	}
+
 	/* Validate if the tunneled packet is being offloaded by HW */
 	if (skb->encapsulation &&
 	    (features & NETIF_F_CSUM_MASK || features & NETIF_F_GSO_MASK))
diff --git a/net/psp/psp_main.c b/net/psp/psp_main.c
index e09f6fec948d..1eb1617aaf2b 100644
--- a/net/psp/psp_main.c
+++ b/net/psp/psp_main.c
@@ -189,7 +189,8 @@ unsigned int psp_key_size(u32 version)
 EXPORT_SYMBOL(psp_key_size);
 
 static void psp_write_headers(struct net *net, struct sk_buff *skb, __be32 spi,
-			      u8 ver, unsigned int udp_len, __be16 sport)
+			      u8 ver, u8 nexthdr, unsigned int udp_len,
+			      __be16 sport)
 {
 	struct udphdr *uh = udp_hdr(skb);
 	struct psphdr *psph = (struct psphdr *)(uh + 1);
@@ -236,7 +237,7 @@ static void psp_write_headers(struct net *net, struct sk_buff *skb, __be32 spi,
 	uh->check = 0;
 	udp_set_len(uh, udp_len);
 
-	psph->nexthdr = IPPROTO_TCP;
+	psph->nexthdr = nexthdr;
 	psph->hdrlen = PSP_HDRLEN_NOOPT;
 	psph->crypt_offset = 0;
 	psph->verfl = FIELD_PREP(PSPHDR_VERFL_VERSION, ver) |
@@ -245,8 +246,36 @@ static void psp_write_headers(struct net *net, struct sk_buff *skb, __be32 spi,
 	memset(&psph->iv, 0, sizeof(psph->iv));
 }
 
-/* Encapsulate a TCP packet with PSP by adding the UDP+PSP headers and filling
- * them in.
+/* udp+psp goes in at the transport header, the protocol of the fixed ip
+ * header becomes udp, and the psp next header takes the protocol it had: the
+ * transport header must directly follow the fixed header. it doesn't with
+ * ipv6 extension headers, whose last one holds the l4 protocol. for ipv6,
+ * check the next header too, not only the network header length: the seg6,
+ * rpl and ioam6 lwtunnels point the transport header at the extension header
+ * they insert. a packet already encapsulated for a tunnel has its inner
+ * headers set up for the tunnel, and an ipv4 fragment (e.g. refragmented
+ * after tc encap) has no l4 header, or not all of it.
+ */
+static bool psp_dev_encap_ok(const struct sk_buff *skb)
+{
+	u32 network_len = skb_network_header_len(skb);
+
+	if (skb->encapsulation)
+		return false;
+	if (skb->protocol == htons(ETH_P_IP))
+		return network_len == ip_hdrlen(skb) &&
+		       !ip_is_fragment(ip_hdr(skb));
+	if (skb->protocol == htons(ETH_P_IPV6))
+		return !ipv6_ext_hdr(ipv6_hdr(skb)->nexthdr) &&
+		       network_len == sizeof(struct ipv6hdr);
+	return false;
+}
+
+/* Encapsulate a packet with PSP by adding the UDP+PSP headers and filling
+ * them in. The PSP next header is taken from the IP header: packets with
+ * IPv6 extension headers, or whose transport header isn't right after the IP
+ * header, are refused, as are IPv4 fragments and packets already
+ * encapsulated for a tunnel.
  */
 bool psp_dev_encapsulate(struct net *net, struct sk_buff *skb, __be32 spi,
 			 u8 ver, __be16 sport)
@@ -254,11 +283,16 @@ 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;
+	u8 nexthdr;
 
-	if (skb->protocol != htons(ETH_P_IP) &&
-	    skb->protocol != htons(ETH_P_IPV6))
+	if (!psp_dev_encap_ok(skb))
 		return false;
 
+	if (skb->protocol == htons(ETH_P_IP))
+		nexthdr = ip_hdr(skb)->protocol;
+	else
+		nexthdr = ipv6_hdr(skb)->nexthdr;
+
 	if (skb_cow_head(skb, PSP_ENCAP_HLEN))
 		return false;
 
@@ -279,11 +313,11 @@ bool psp_dev_encapsulate(struct net *net, struct sk_buff *skb, __be32 spi,
 		be16_add_cpu(&ipv6_hdr(skb)->payload_len, PSP_ENCAP_HLEN);
 	}
 
-	skb_set_inner_ipproto(skb, IPPROTO_TCP);
+	skb_set_inner_ipproto(skb, nexthdr);
 	skb_set_inner_transport_header(skb, skb_transport_offset(skb) +
 						    PSP_ENCAP_HLEN);
 	skb->encapsulation = 1;
-	psp_write_headers(net, skb, spi, ver,
+	psp_write_headers(net, skb, spi, ver, nexthdr,
 			  skb->len - skb_transport_offset(skb), sport);
 
 	return true;
-- 
2.53.0-Meta


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH net-next v1 03/15] psp: declare psp_assoc_set_tx() in psp.h
  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 ` Todd Freed
  2026-11-11  0:55 ` [PATCH net-next v1 04/15] psp: add support for socket-less psp associations Todd Freed
                   ` (11 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Todd Freed @ 2026-11-11  0:55 UTC (permalink / raw)
  To: netdev, toddfreed
  Cc: David S. Miller, Andrew Lunn, Björn Töpel, bpf,
	Daniel Borkmann, Daniel Zahka, Donald Hunter, Eric Dumazet,
	Jakub Kicinski, Jamal Hadi Salim, Jiri Pirko, John Fastabend,
	linux-doc, linux-kernel, linux-kselftest, linux-rdma,
	Martin KaFai Lau, Paolo Abeni, Saeed Mahameed, Simon Horman,
	Tariq Toukan, Wei Wang, Willem de Bruijn

socket-less psp associations will need to install a tx key the same way
sockets do.

make psp_assoc_set_tx() non-static, add a declaration within net/psp

Signed-off-by: Todd Freed <toddfreed@meta.com>
---
 net/psp/psp.h      |  3 +++
 net/psp/psp_sock.c | 15 ++++++++++++---
 2 files changed, 15 insertions(+), 3 deletions(-)

diff --git a/net/psp/psp.h b/net/psp/psp.h
index b123c2427905..de4be3320009 100644
--- a/net/psp/psp.h
+++ b/net/psp/psp.h
@@ -24,6 +24,9 @@ void psp_nl_notify_disassoc(struct psp_dev *psd, struct net *net);
 struct psp_assoc *psp_assoc_create(struct psp_dev *psd);
 struct psp_dev *psp_dev_get_for_sock(struct sock *sk);
 void psp_dev_tx_key_del(struct psp_dev *psd, struct psp_assoc *pas);
+int psp_assoc_set_tx(struct psp_dev *psd, struct psp_assoc *pas,
+		     struct psp_key_parsed *key,
+		     struct netlink_ext_ack *extack);
 int psp_sock_assoc_set_rx(struct sock *sk, struct psp_assoc *pas,
 			  struct psp_key_parsed *key,
 			  struct netlink_ext_ack *extack);
diff --git a/net/psp/psp_sock.c b/net/psp/psp_sock.c
index f2cba3fa3a23..d04146273bf2 100644
--- a/net/psp/psp_sock.c
+++ b/net/psp/psp_sock.c
@@ -185,9 +185,18 @@ int psp_sock_assoc_set_rx(struct sock *sk, struct psp_assoc *pas,
 	return err;
 }
 
-static int psp_assoc_set_tx(struct psp_dev *psd, struct psp_assoc *pas,
-			    struct psp_key_parsed *key,
-			    struct netlink_ext_ack *extack)
+/**
+ * psp_assoc_set_tx() - install a Tx key for an association
+ * @psd:	PSP device, must be locked
+ * @pas:	association, must belong to @psd and not have a Tx key yet
+ * @key:	Tx SPI and key
+ * @extack:	netlink extended ack
+ *
+ * Return: 0 on success, negative errno otherwise.
+ */
+int psp_assoc_set_tx(struct psp_dev *psd, struct psp_assoc *pas,
+		     struct psp_key_parsed *key,
+		     struct netlink_ext_ack *extack)
 {
 	int err;
 
-- 
2.53.0-Meta


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH net-next v1 04/15] psp: add support for socket-less psp associations
  2026-11-11  0:55 [PATCH net-next v1 00/15] psp: introduce socket-less associations Todd Freed
                   ` (2 preceding siblings ...)
  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 ` 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
                   ` (10 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Todd Freed @ 2026-11-11  0:55 UTC (permalink / raw)
  To: netdev, toddfreed
  Cc: David S. Miller, Andrew Lunn, Björn Töpel, bpf,
	Daniel Borkmann, Daniel Zahka, Donald Hunter, Eric Dumazet,
	Jakub Kicinski, Jamal Hadi Salim, Jiri Pirko, John Fastabend,
	linux-doc, linux-kernel, linux-kselftest, linux-rdma,
	Martin KaFai Lau, Paolo Abeni, Saeed Mahameed, Simon Horman,
	Tariq Toukan, Wei Wang, Willem de Bruijn

currently, a psp association is bound to a tcp socket when it is
created, and can hold both an rx and a tx key.

add a small registry of un-bound associations. These have either an rx
or tx flavor, and are referenced by an id. other parts of the stack can
then attach them to packets. see the subsequent patches, where the tc
psp action does just this.

an rx association records the spi, version, and device key generation,
primarily for describe (assoc-get). a tc filter matches these fields
with the flower keys psp_spi, psp_ver, and psp_dev instead of
referencing the association by id, to facilitate hw offload. these
matches are added in a subsequent patch.

adds the following new netlink commands for managing these associations.
note, these are admin-only and restricted to the netns of the psp device

  rx-spi-alloc  allocate an rx spi + derived key, and publish an rx
                association for it, returns its id. the key is
                delivered to the peer out of band
  assoc-add     create a tx association and install a tx key, returns
                its id
  assoc-del     drop the registry's reference to an association
  assoc-get     get / dump associations ; only the spi is reported

assoc-get and assoc-del operate on both rx and tx associations.

socket associations do not go into the registry, and these apis do not
address them. so for example, assoc-get does not list them.

socket-less associations are on the device's association lists, like
socket associations. they are therefore marked stale by key rotation in
the same way, except tx ones: they hold the peer's key, which the
rotation of the device key doesn't affect. assoc-get reports it
(stale), so that user space can identify such entries.

key material enters through netlink and is never reported back. no key
is stored for rx associations, and users reference associations by id
and never handle keys

published associations are dropped from the registry when their psp
device is unregistered.

Signed-off-by: Todd Freed <toddfreed@meta.com>
---
 Documentation/netlink/specs/psp.yaml |  98 +++++++++-
 include/net/psp/functions.h          |  10 +
 include/net/psp/types.h              |   1 +
 include/uapi/linux/psp.h             |   6 +
 net/psp/Makefile                     |   2 +-
 net/psp/psp-nl-gen.c                 |  60 ++++++
 net/psp/psp-nl-gen.h                 |   8 +
 net/psp/psp.h                        |   4 +
 net/psp/psp_assoc.c                  | 120 ++++++++++++
 net/psp/psp_main.c                   |   2 +
 net/psp/psp_nl.c                     | 261 +++++++++++++++++++++++++++
 net/psp/psp_sock.c                   |   9 +-
 12 files changed, 577 insertions(+), 4 deletions(-)
 create mode 100644 net/psp/psp_assoc.c

diff --git a/Documentation/netlink/specs/psp.yaml b/Documentation/netlink/specs/psp.yaml
index f3266763c325..d7b8469a82c3 100644
--- a/Documentation/netlink/specs/psp.yaml
+++ b/Documentation/netlink/specs/psp.yaml
@@ -103,6 +103,21 @@ attribute-sets:
         name: sock-fd
         doc: Sockets which should be bound to the association immediately.
         type: u32
+      -
+        name: id
+        doc: |
+          ID of an association which is not bound to a socket.
+          Such associations can be attached to packets with the tc psp action.
+        type: u32
+        checks:
+          min: 1
+      -
+        name: stale
+        doc: |
+          Key rotation has retired the device key the association was
+          created under. The device no longer decrypts the SPI of such an
+          association.
+        type: flag
   -
     name: keys
     attributes:
@@ -132,8 +147,9 @@ attribute-sets:
         name: stale-events
         type: uint
         doc: |
-          Number of times a socket's Rx got shut down due to using
-          a key which went stale (fully rotated out).
+          Number of times an Rx association, of a socket or socket-less,
+          got shut down due to using a key which went stale (fully rotated
+          out).
           Kernel statistic.
       -
         name: rx-packets
@@ -350,6 +366,84 @@ operations:
           attributes: []
         pre: psp-device-get-locked
         post: psp-device-unlock
+    -
+      name: rx-spi-alloc
+      doc: |
+        Allocate a new Rx key + SPI pair without binding it to a socket.
+        The key is derived by the device and must be passed to the peer
+        out of band, to be installed there with assoc-add. Also adds an
+        Rx association, which records the SPI, version, and the device
+        key generation, and returns its ID. A tc rule can check
+        decrypted packets against these fields with the flower psp_spi,
+        psp_dev and psp_ver keys.
+      attribute-set: assoc
+      flags: [admin-perm]
+      do:
+        request:
+          attributes:
+            - dev-id
+            - version
+        reply:
+          attributes:
+            - id
+            - dev-id
+            - rx-key
+        pre: psp-assoc-device-get-locked-admin
+        post: psp-device-unlock
+    -
+      name: assoc-add
+      doc: |
+        Add a PSP Tx association which is not bound to a socket.
+        Returns the ID of the new association.
+      attribute-set: assoc
+      flags: [admin-perm]
+      do:
+        request:
+          attributes:
+            - dev-id
+            - version
+            - tx-key
+        reply:
+          attributes:
+            - id
+            - dev-id
+        pre: psp-assoc-device-get-locked-admin
+        post: psp-device-unlock
+    -
+      name: assoc-del
+      doc: |
+        Delete a PSP association which is not bound to a socket, Tx or Rx.
+        Packets and tc actions which still reference it keep it alive.
+      attribute-set: assoc
+      flags: [admin-perm]
+      do:
+        request:
+          attributes:
+            - id
+        reply:
+          attributes: []
+    -
+      name: assoc-get
+      doc: |
+        Get / dump associations which are not bound to a socket.
+        Key material is never reported: Tx associations carry tx-key and
+        Rx associations rx-key, with the SPI only.
+      attribute-set: assoc
+      flags: [admin-perm]
+      do:
+        request:
+          attributes:
+            - id
+        reply: &assoc-all
+          attributes:
+            - id
+            - dev-id
+            - version
+            - tx-key
+            - rx-key
+            - stale
+      dump:
+        reply: *assoc-all
 
 mcast-groups:
   list:
diff --git a/include/net/psp/functions.h b/include/net/psp/functions.h
index 410adb6718c1..d0789a9694bc 100644
--- a/include/net/psp/functions.h
+++ b/include/net/psp/functions.h
@@ -23,6 +23,16 @@ int psp_dev_rcv(struct sk_buff *skb, u16 dev_id, u8 generation, bool strip_icv);
 
 /* Kernel-facing API */
 void psp_assoc_put(struct psp_assoc *pas);
+struct psp_assoc *psp_assoc_get_by_id(struct net *net, u32 id);
+
+/* An association from the ID registry is either a Tx one, from assoc-add,
+ * or an Rx one, from rx-spi-alloc. PSP SPIs are never 0.
+ */
+static inline bool psp_assoc_is_tx(const struct psp_assoc *pas)
+{
+	DEBUG_NET_WARN_ON_ONCE(!pas->id);    /* only socket-less ones */
+	return pas->tx.spi != 0;
+}
 
 #if IS_ENABLED(CONFIG_INET_PSP)
 unsigned int psp_key_size(u32 version);
diff --git a/include/net/psp/types.h b/include/net/psp/types.h
index b8905efbd604..49fa36ad9583 100644
--- a/include/net/psp/types.h
+++ b/include/net/psp/types.h
@@ -154,6 +154,7 @@ struct psp_key_parsed {
 
 struct psp_assoc {
 	struct psp_dev *psd;
+	u32 id;
 
 	u16 dev_id;
 	u8 generation;
diff --git a/include/uapi/linux/psp.h b/include/uapi/linux/psp.h
index 1c8899cd4da5..54b50324211b 100644
--- a/include/uapi/linux/psp.h
+++ b/include/uapi/linux/psp.h
@@ -44,6 +44,8 @@ enum {
 	PSP_A_ASSOC_RX_KEY,
 	PSP_A_ASSOC_TX_KEY,
 	PSP_A_ASSOC_SOCK_FD,
+	PSP_A_ASSOC_ID,
+	PSP_A_ASSOC_STALE,
 
 	__PSP_A_ASSOC_MAX,
 	PSP_A_ASSOC_MAX = (__PSP_A_ASSOC_MAX - 1)
@@ -87,6 +89,10 @@ enum {
 	PSP_CMD_GET_STATS,
 	PSP_CMD_DEV_ASSOC,
 	PSP_CMD_DEV_DISASSOC,
+	PSP_CMD_RX_SPI_ALLOC,
+	PSP_CMD_ASSOC_ADD,
+	PSP_CMD_ASSOC_DEL,
+	PSP_CMD_ASSOC_GET,
 
 	__PSP_CMD_MAX,
 	PSP_CMD_MAX = (__PSP_CMD_MAX - 1)
diff --git a/net/psp/Makefile b/net/psp/Makefile
index eb5ff3c5bfb2..d7f026f937cb 100644
--- a/net/psp/Makefile
+++ b/net/psp/Makefile
@@ -2,4 +2,4 @@
 
 obj-$(CONFIG_INET_PSP) += psp.o
 
-psp-y := psp_main.o psp_nl.o psp_sock.o psp-nl-gen.o
+psp-y := psp_main.o psp_nl.o psp_sock.o psp_assoc.o psp-nl-gen.o
diff --git a/net/psp/psp-nl-gen.c b/net/psp/psp-nl-gen.c
index 0e426ffac398..dbdbe79b569e 100644
--- a/net/psp/psp-nl-gen.c
+++ b/net/psp/psp-nl-gen.c
@@ -67,6 +67,29 @@ static const struct nla_policy psp_dev_disassoc_nl_policy[PSP_A_DEV_NSID + 1] =
 	[PSP_A_DEV_NSID] = { .type = NLA_S32, },
 };
 
+/* PSP_CMD_RX_SPI_ALLOC - do */
+static const struct nla_policy psp_rx_spi_alloc_nl_policy[PSP_A_ASSOC_VERSION + 1] = {
+	[PSP_A_ASSOC_DEV_ID] = NLA_POLICY_MIN(NLA_U32, 1),
+	[PSP_A_ASSOC_VERSION] = NLA_POLICY_MAX(NLA_U32, 3),
+};
+
+/* PSP_CMD_ASSOC_ADD - do */
+static const struct nla_policy psp_assoc_add_nl_policy[PSP_A_ASSOC_TX_KEY + 1] = {
+	[PSP_A_ASSOC_DEV_ID] = NLA_POLICY_MIN(NLA_U32, 1),
+	[PSP_A_ASSOC_VERSION] = NLA_POLICY_MAX(NLA_U32, 3),
+	[PSP_A_ASSOC_TX_KEY] = NLA_POLICY_NESTED(psp_keys_nl_policy),
+};
+
+/* PSP_CMD_ASSOC_DEL - do */
+static const struct nla_policy psp_assoc_del_nl_policy[PSP_A_ASSOC_ID + 1] = {
+	[PSP_A_ASSOC_ID] = NLA_POLICY_MIN(NLA_U32, 1),
+};
+
+/* PSP_CMD_ASSOC_GET - do */
+static const struct nla_policy psp_assoc_get_nl_policy[PSP_A_ASSOC_ID + 1] = {
+	[PSP_A_ASSOC_ID] = NLA_POLICY_MIN(NLA_U32, 1),
+};
+
 /* Ops table for psp */
 static const struct genl_split_ops psp_nl_ops[] = {
 	{
@@ -151,6 +174,43 @@ static const struct genl_split_ops psp_nl_ops[] = {
 		.maxattr	= PSP_A_DEV_NSID,
 		.flags		= GENL_ADMIN_PERM | GENL_CMD_CAP_DO,
 	},
+	{
+		.cmd		= PSP_CMD_RX_SPI_ALLOC,
+		.pre_doit	= psp_assoc_device_get_locked_admin,
+		.doit		= psp_nl_rx_spi_alloc_doit,
+		.post_doit	= psp_device_unlock,
+		.policy		= psp_rx_spi_alloc_nl_policy,
+		.maxattr	= PSP_A_ASSOC_VERSION,
+		.flags		= GENL_ADMIN_PERM | GENL_CMD_CAP_DO,
+	},
+	{
+		.cmd		= PSP_CMD_ASSOC_ADD,
+		.pre_doit	= psp_assoc_device_get_locked_admin,
+		.doit		= psp_nl_assoc_add_doit,
+		.post_doit	= psp_device_unlock,
+		.policy		= psp_assoc_add_nl_policy,
+		.maxattr	= PSP_A_ASSOC_TX_KEY,
+		.flags		= GENL_ADMIN_PERM | GENL_CMD_CAP_DO,
+	},
+	{
+		.cmd		= PSP_CMD_ASSOC_DEL,
+		.doit		= psp_nl_assoc_del_doit,
+		.policy		= psp_assoc_del_nl_policy,
+		.maxattr	= PSP_A_ASSOC_ID,
+		.flags		= GENL_ADMIN_PERM | GENL_CMD_CAP_DO,
+	},
+	{
+		.cmd		= PSP_CMD_ASSOC_GET,
+		.doit		= psp_nl_assoc_get_doit,
+		.policy		= psp_assoc_get_nl_policy,
+		.maxattr	= PSP_A_ASSOC_ID,
+		.flags		= GENL_ADMIN_PERM | GENL_CMD_CAP_DO,
+	},
+	{
+		.cmd	= PSP_CMD_ASSOC_GET,
+		.dumpit	= psp_nl_assoc_get_dumpit,
+		.flags	= GENL_ADMIN_PERM | GENL_CMD_CAP_DUMP,
+	},
 };
 
 static const struct genl_multicast_group psp_nl_mcgrps[] = {
diff --git a/net/psp/psp-nl-gen.h b/net/psp/psp-nl-gen.h
index 24d51bff997f..27474f1ce112 100644
--- a/net/psp/psp-nl-gen.h
+++ b/net/psp/psp-nl-gen.h
@@ -24,6 +24,9 @@ int psp_assoc_device_get_locked(const struct genl_split_ops *ops,
 int psp_device_get_locked_dev_assoc(const struct genl_split_ops *ops,
 				    struct sk_buff *skb,
 				    struct genl_info *info);
+int psp_assoc_device_get_locked_admin(const struct genl_split_ops *ops,
+				      struct sk_buff *skb,
+				      struct genl_info *info);
 void
 psp_device_unlock(const struct genl_split_ops *ops, struct sk_buff *skb,
 		  struct genl_info *info);
@@ -38,6 +41,11 @@ int psp_nl_get_stats_doit(struct sk_buff *skb, struct genl_info *info);
 int psp_nl_get_stats_dumpit(struct sk_buff *skb, struct netlink_callback *cb);
 int psp_nl_dev_assoc_doit(struct sk_buff *skb, struct genl_info *info);
 int psp_nl_dev_disassoc_doit(struct sk_buff *skb, struct genl_info *info);
+int psp_nl_rx_spi_alloc_doit(struct sk_buff *skb, struct genl_info *info);
+int psp_nl_assoc_add_doit(struct sk_buff *skb, struct genl_info *info);
+int psp_nl_assoc_del_doit(struct sk_buff *skb, struct genl_info *info);
+int psp_nl_assoc_get_doit(struct sk_buff *skb, struct genl_info *info);
+int psp_nl_assoc_get_dumpit(struct sk_buff *skb, struct netlink_callback *cb);
 
 enum {
 	PSP_NLGRP_MGMT,
diff --git a/net/psp/psp.h b/net/psp/psp.h
index de4be3320009..cadeb1ad04a2 100644
--- a/net/psp/psp.h
+++ b/net/psp/psp.h
@@ -35,6 +35,10 @@ int psp_sock_assoc_set_tx(struct sock *sk, struct psp_dev *psd,
 			  struct netlink_ext_ack *extack);
 void psp_assocs_key_rotated(struct psp_dev *psd);
 
+int psp_assoc_publish(struct psp_assoc *pas);
+void psp_assoc_unpublish(struct psp_assoc *pas);
+struct psp_assoc *psp_assoc_get_next(struct net *net, unsigned long *index);
+
 static inline void psp_dev_get(struct psp_dev *psd)
 {
 	refcount_inc(&psd->refcnt);
diff --git a/net/psp/psp_assoc.c b/net/psp/psp_assoc.c
new file mode 100644
index 000000000000..db681a17af8c
--- /dev/null
+++ b/net/psp/psp_assoc.c
@@ -0,0 +1,120 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* PSP associations which are not bound to a socket.
+ *
+ * They are created and deleted over netlink, and referenced by ID by other
+ * kernel users, e.g. the tc psp action. The registry holds one reference
+ * on each published association.
+ */
+
+#include <linux/xarray.h>
+#include <net/psp.h>
+
+#include "psp.h"
+
+static DEFINE_XARRAY_ALLOC1(psp_assocs);
+static u32 psp_assoc_next_id;
+
+/**
+ * psp_assoc_publish() - add an association to the ID registry
+ * @pas: association: an Rx one, or a Tx one with its key installed
+ *
+ * On success @pas->id is set and the registry holds its own reference.
+ *
+ * Return: 0 on success, negative errno otherwise.
+ */
+int psp_assoc_publish(struct psp_assoc *pas)
+{
+	int err;
+
+	refcount_inc(&pas->refcnt);
+	err = xa_alloc_cyclic(&psp_assocs, &pas->id, pas, xa_limit_31b,
+			      &psp_assoc_next_id, GFP_KERNEL);
+	if (err < 0) {
+		pas->id = 0;
+		refcount_dec(&pas->refcnt);
+		return err;
+	}
+
+	return 0;
+}
+
+/**
+ * psp_assoc_unpublish() - remove an association from the ID registry
+ * @pas: association
+ *
+ * Drops the registry's reference, unless a concurrent caller already did.
+ */
+void psp_assoc_unpublish(struct psp_assoc *pas)
+{
+	if (xa_cmpxchg(&psp_assocs, pas->id, pas, NULL, 0) == pas)
+		psp_assoc_put(pas);
+}
+
+static int psp_assoc_check_access(struct psp_assoc *pas, struct net *net)
+{
+	struct psp_dev *psd = pas->psd;
+	int err;
+
+	mutex_lock(&psd->lock);
+	if (psp_dev_is_registered(psd))
+		err = psp_dev_check_access(psd, net, true);
+	else
+		err = -ENODEV;
+	mutex_unlock(&psd->lock);
+
+	return err;
+}
+
+/**
+ * psp_assoc_get_by_id() - look up a published association
+ * @net: netns of the caller, must be the netns of the PSP device
+ * @id:  association ID
+ *
+ * Return: association with a reference taken, or NULL.
+ */
+struct psp_assoc *psp_assoc_get_by_id(struct net *net, u32 id)
+{
+	struct psp_assoc *pas;
+
+	rcu_read_lock();
+	pas = xa_load(&psp_assocs, id);
+	if (pas && !refcount_inc_not_zero(&pas->refcnt))
+		pas = NULL;
+	rcu_read_unlock();
+
+	if (pas && psp_assoc_check_access(pas, net)) {
+		psp_assoc_put(pas);
+		pas = NULL;
+	}
+
+	return pas;
+}
+EXPORT_SYMBOL_GPL(psp_assoc_get_by_id);
+
+/**
+ * psp_assoc_get_next() - iterate over published associations
+ * @net:   netns of the caller
+ * @index: in: first ID to consider, out: ID of the returned association
+ *
+ * Return: association with a reference taken, or NULL when done.
+ */
+struct psp_assoc *psp_assoc_get_next(struct net *net, unsigned long *index)
+{
+	struct psp_assoc *pas;
+
+	for (;; (*index)++) {
+		rcu_read_lock();
+		pas = xa_find(&psp_assocs, index, ULONG_MAX, XA_PRESENT);
+		if (pas && !refcount_inc_not_zero(&pas->refcnt)) {
+			rcu_read_unlock();
+			continue;
+		}
+		rcu_read_unlock();
+
+		if (!pas)
+			return NULL;
+		if (!psp_assoc_check_access(pas, net))
+			return pas;
+		psp_assoc_put(pas);
+	}
+}
diff --git a/net/psp/psp_main.c b/net/psp/psp_main.c
index 1eb1617aaf2b..40d3e9897b94 100644
--- a/net/psp/psp_main.c
+++ b/net/psp/psp_main.c
@@ -151,6 +151,8 @@ void psp_dev_unregister(struct psp_dev *psd)
 		if (psp_assoc_needs_tx_key_del(pas))
 			psp_dev_tx_key_del(psd, pas);
 		list_del(&pas->assocs_list);
+		if (pas->id)
+			psp_assoc_unpublish(pas);
 	}
 
 	list_for_each_entry_safe(entry, entry_tmp, &psd->assoc_dev_list,
diff --git a/net/psp/psp_nl.c b/net/psp/psp_nl.c
index cdfc2d72fb39..ace58bee9401 100644
--- a/net/psp/psp_nl.c
+++ b/net/psp/psp_nl.c
@@ -743,6 +743,19 @@ int psp_assoc_device_get_locked(const struct genl_split_ops *ops,
 	return err;
 }
 
+int psp_assoc_device_get_locked_admin(const struct genl_split_ops *ops,
+				      struct sk_buff *skb,
+				      struct genl_info *info)
+{
+	if (GENL_REQ_ATTR_CHECK(info, PSP_A_ASSOC_DEV_ID))
+		return -EINVAL;
+
+	info->user_ptr[0] = psp_device_get_and_lock(genl_info_net(info),
+						    info->attrs[PSP_A_ASSOC_DEV_ID],
+						    true);
+	return PTR_ERR_OR_ZERO(info->user_ptr[0]);
+}
+
 static int
 psp_nl_parse_key(struct genl_info *info, u32 attr, struct psp_key_parsed *key,
 		 unsigned int key_sz)
@@ -900,6 +913,254 @@ int psp_nl_tx_assoc_doit(struct sk_buff *skb, struct genl_info *info)
 	return err;
 }
 
+int psp_nl_rx_spi_alloc_doit(struct sk_buff *skb, struct genl_info *info)
+{
+	struct psp_dev *psd = info->user_ptr[0];
+	struct psp_key_parsed key;
+	struct psp_assoc *pas;
+	struct sk_buff *rsp;
+	u32 version;
+	int err;
+
+	if (GENL_REQ_ATTR_CHECK(info, PSP_A_ASSOC_VERSION))
+		return -EINVAL;
+
+	version = nla_get_u32(info->attrs[PSP_A_ASSOC_VERSION]);
+	if (!(psd->caps->versions & (1 << version))) {
+		NL_SET_BAD_ATTR(info->extack, info->attrs[PSP_A_ASSOC_VERSION]);
+		return -EOPNOTSUPP;
+	}
+
+	rsp = psp_nl_reply_new(info);
+	if (!rsp)
+		return -ENOMEM;
+
+	err = psd->ops->rx_spi_alloc(psd, version, &key, info->extack);
+	if (err)
+		goto err_free_rsp;
+
+	pas = psp_assoc_create(psd);
+	if (!pas) {
+		err = -ENOMEM;
+		goto err_free_rsp;
+	}
+	pas->version = version;
+	pas->rx.spi = key.spi;
+
+	err = psp_assoc_publish(pas);
+	if (err)
+		goto err_put_pas;
+
+	if (nla_put_u32(rsp, PSP_A_ASSOC_ID, pas->id) ||
+	    nla_put_u32(rsp, PSP_A_ASSOC_DEV_ID, psd->id) ||
+	    psp_nl_put_key(rsp, PSP_A_ASSOC_RX_KEY, version, &key)) {
+		psp_assoc_unpublish(pas);
+		err = -EMSGSIZE;
+		goto err_put_pas;
+	}
+
+	psp_assoc_put(pas);
+	memzero_explicit(&key, sizeof(key));
+
+	return psp_nl_reply_send(rsp, info);
+
+err_put_pas:
+	psp_assoc_put(pas);
+err_free_rsp:
+	memzero_explicit(&key, sizeof(key));
+	nlmsg_free(rsp);
+	return err;
+}
+
+int psp_nl_assoc_add_doit(struct sk_buff *skb, struct genl_info *info)
+{
+	struct psp_dev *psd = info->user_ptr[0];
+	struct psp_key_parsed key;
+	struct psp_assoc *pas;
+	struct sk_buff *rsp;
+	unsigned int key_sz;
+	u32 version;
+	int err;
+
+	if (GENL_REQ_ATTR_CHECK(info, PSP_A_ASSOC_VERSION) ||
+	    GENL_REQ_ATTR_CHECK(info, PSP_A_ASSOC_TX_KEY))
+		return -EINVAL;
+
+	version = nla_get_u32(info->attrs[PSP_A_ASSOC_VERSION]);
+	if (!(psd->caps->versions & (1 << version))) {
+		NL_SET_BAD_ATTR(info->extack, info->attrs[PSP_A_ASSOC_VERSION]);
+		return -EOPNOTSUPP;
+	}
+
+	key_sz = psp_key_size(version);
+	if (!key_sz)
+		return -EINVAL;
+
+	err = psp_nl_parse_key(info, PSP_A_ASSOC_TX_KEY, &key, key_sz);
+	if (err < 0)
+		return err;
+
+	rsp = psp_nl_reply_new(info);
+	if (!rsp) {
+		err = -ENOMEM;
+		goto err_clear_key;
+	}
+
+	pas = psp_assoc_create(psd);
+	if (!pas) {
+		err = -ENOMEM;
+		goto err_free_rsp;
+	}
+	pas->version = version;
+
+	err = psp_assoc_set_tx(psd, pas, &key, info->extack);
+	if (err)
+		goto err_put_pas;
+
+	err = psp_assoc_publish(pas);
+	if (err)
+		goto err_put_pas;
+
+	if (nla_put_u32(rsp, PSP_A_ASSOC_ID, pas->id) ||
+	    nla_put_u32(rsp, PSP_A_ASSOC_DEV_ID, psd->id)) {
+		psp_assoc_unpublish(pas);
+		err = -EMSGSIZE;
+		goto err_put_pas;
+	}
+
+	psp_assoc_put(pas);
+	memzero_explicit(&key, sizeof(key));
+
+	return psp_nl_reply_send(rsp, info);
+
+err_put_pas:
+	psp_assoc_put(pas);
+err_free_rsp:
+	nlmsg_free(rsp);
+err_clear_key:
+	memzero_explicit(&key, sizeof(key));
+	return err;
+}
+
+int psp_nl_assoc_del_doit(struct sk_buff *skb, struct genl_info *info)
+{
+	struct psp_assoc *pas;
+	struct sk_buff *rsp;
+
+	if (GENL_REQ_ATTR_CHECK(info, PSP_A_ASSOC_ID))
+		return -EINVAL;
+
+	pas = psp_assoc_get_by_id(genl_info_net(info),
+				  nla_get_u32(info->attrs[PSP_A_ASSOC_ID]));
+	if (!pas) {
+		NL_SET_BAD_ATTR(info->extack, info->attrs[PSP_A_ASSOC_ID]);
+		return -ENOENT;
+	}
+
+	rsp = psp_nl_reply_new(info);
+	if (!rsp) {
+		psp_assoc_put(pas);
+		return -ENOMEM;
+	}
+
+	psp_assoc_unpublish(pas);
+	psp_assoc_put(pas);
+
+	return psp_nl_reply_send(rsp, info);
+}
+
+static int
+psp_nl_assoc_fill(struct psp_assoc *pas, struct sk_buff *rsp,
+		  const struct genl_info *info)
+{
+	struct nlattr *nest;
+	void *hdr;
+
+	hdr = genlmsg_iput(rsp, info);
+	if (!hdr)
+		return -EMSGSIZE;
+
+	if (nla_put_u32(rsp, PSP_A_ASSOC_ID, pas->id) ||
+	    nla_put_u32(rsp, PSP_A_ASSOC_DEV_ID, pas->dev_id) ||
+	    nla_put_u32(rsp, PSP_A_ASSOC_VERSION, pas->version))
+		goto err_cancel_msg;
+	/* Key rotation marks it stale under the device lock */
+	if ((READ_ONCE(pas->generation) & ~PSP_GEN_VALID_MASK) &&
+	    nla_put_flag(rsp, PSP_A_ASSOC_STALE))
+		goto err_cancel_msg;
+
+	/* Report the SPI only, never the key */
+	nest = nla_nest_start(rsp, psp_assoc_is_tx(pas) ? PSP_A_ASSOC_TX_KEY :
+							 PSP_A_ASSOC_RX_KEY);
+	if (!nest ||
+	    nla_put_u32(rsp, PSP_A_KEYS_SPI,
+			be32_to_cpu(psp_assoc_is_tx(pas) ? pas->tx.spi :
+							   pas->rx.spi)))
+		goto err_cancel_msg;
+	nla_nest_end(rsp, nest);
+
+	genlmsg_end(rsp, hdr);
+	return 0;
+
+err_cancel_msg:
+	genlmsg_cancel(rsp, hdr);
+	return -EMSGSIZE;
+}
+
+int psp_nl_assoc_get_doit(struct sk_buff *skb, struct genl_info *info)
+{
+	struct psp_assoc *pas;
+	struct sk_buff *rsp;
+	int err;
+
+	if (GENL_REQ_ATTR_CHECK(info, PSP_A_ASSOC_ID))
+		return -EINVAL;
+
+	pas = psp_assoc_get_by_id(genl_info_net(info),
+				  nla_get_u32(info->attrs[PSP_A_ASSOC_ID]));
+	if (!pas) {
+		NL_SET_BAD_ATTR(info->extack, info->attrs[PSP_A_ASSOC_ID]);
+		return -ENOENT;
+	}
+
+	rsp = genlmsg_new(GENLMSG_DEFAULT_SIZE, GFP_KERNEL);
+	if (!rsp) {
+		err = -ENOMEM;
+		goto err_put_pas;
+	}
+
+	err = psp_nl_assoc_fill(pas, rsp, info);
+	if (err)
+		goto err_free_msg;
+
+	psp_assoc_put(pas);
+	return genlmsg_reply(rsp, info);
+
+err_free_msg:
+	nlmsg_free(rsp);
+err_put_pas:
+	psp_assoc_put(pas);
+	return err;
+}
+
+int psp_nl_assoc_get_dumpit(struct sk_buff *rsp, struct netlink_callback *cb)
+{
+	unsigned long index = cb->args[0];
+	struct psp_assoc *pas;
+	int err = 0;
+
+	while ((pas = psp_assoc_get_next(sock_net(rsp->sk), &index))) {
+		err = psp_nl_assoc_fill(pas, rsp, genl_info_dump(cb));
+		psp_assoc_put(pas);
+		if (err)
+			break;
+		index++;
+	}
+	cb->args[0] = index;
+
+	return err;
+}
+
 static int
 psp_nl_stats_fill(struct psp_dev *psd, struct sk_buff *rsp,
 		  const struct genl_info *info)
diff --git a/net/psp/psp_sock.c b/net/psp/psp_sock.c
index d04146273bf2..609b06c7af65 100644
--- a/net/psp/psp_sock.c
+++ b/net/psp/psp_sock.c
@@ -140,6 +140,7 @@ void psp_assoc_put(struct psp_assoc *pas)
 	if (pas && refcount_dec_and_test(&pas->refcnt))
 		call_rcu(&pas->rcu, psp_assoc_free_queue);
 }
+EXPORT_SYMBOL_GPL(psp_assoc_put);
 
 void psp_sk_assoc_free(struct sock *sk)
 {
@@ -310,7 +311,13 @@ void psp_assocs_key_rotated(struct psp_dev *psd)
 	 * be able to Rx any traffic.
 	 */
 	list_for_each_entry_safe(pas, next, &psd->prev_assocs, assocs_list) {
-		pas->generation |= ~PSP_GEN_VALID_MASK;
+		/* A socket-less Tx association holds the peer's key, which
+		 * the rotation of the device key doesn't affect
+		 */
+		if (pas->id && psp_assoc_is_tx(pas))
+			continue;
+		WRITE_ONCE(pas->generation,
+			   pas->generation | ~PSP_GEN_VALID_MASK);
 		psd->stats.stales++;
 	}
 	list_splice_init(&psd->prev_assocs, &psd->stale_assocs);
-- 
2.53.0-Meta


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH net-next v1 05/15] net: add METADATA_PSP dst to carry a psp association on an skb
  2026-11-11  0:55 [PATCH net-next v1 00/15] psp: introduce socket-less associations Todd Freed
                   ` (3 preceding siblings ...)
  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 ` 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
                   ` (9 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Todd Freed @ 2026-11-11  0:55 UTC (permalink / raw)
  To: netdev, toddfreed
  Cc: David S. Miller, Andrew Lunn, Björn Töpel, bpf,
	Daniel Borkmann, Daniel Zahka, Donald Hunter, Eric Dumazet,
	Jakub Kicinski, Jamal Hadi Salim, Jiri Pirko, John Fastabend,
	linux-doc, linux-kernel, linux-kselftest, linux-rdma,
	Martin KaFai Lau, Paolo Abeni, Saeed Mahameed, Simon Horman,
	Tariq Toukan, Wei Wang, Willem de Bruijn

Add a metadata_dst type holding a reference to a psp association, so
that an association can be attached to an skb that has no socket, e.g.
by a tc rule.

change psp_skb_get_assoc_rcu() to check for METADATA_PSP then fall back
to the socket, so that existing driver tx paths (mlx5, netdevsim)
encrypt such packets without any changes.

once an skb has been marked for psp xmit, it is necessary to ensure that
it is transmitted on the correct device, otherwise it may go onto the
wire in cleartext. for packets on a psp socket, sk_validate_xmit_skb()
handles this.

for socket-less psp packets, the psp metadata dst carries this
information. but __dev_queue_xmit() normally releases the dst before
queueing. add a new check for a psp metadata dst at that point. if it's
the wrong device, drop the packet ; if it's the right device, save the
dst so that the driver can use it.

the right device is the psp device's main netdev: devices associated
with it (dev-assoc, e.g. veth or netkit) have its psp_dev too, but
forward the packet rather than encrypt it, and drop the dst on the way.
prevent xmit on those.

also check that the packet will still fit the device's mtu once the
device adds the psp encapsulation (udp+psp+icv, 40 bytes), and drop it
with SKB_DROP_REASON_PKT_TOO_BIG otherwise. note that no icmp error is
sent in this case. psp sockets do leave room for it (psp_sk_overhead()).

check a packet with a METADATA_PSP dst again at dequeue, in
sk_validate_xmit_skb(): the psp device may have been unregistered while
the packet was queued, and its keys deleted.

Signed-off-by: Todd Freed <toddfreed@meta.com>
---
 include/net/dst_metadata.h  | 19 +++++++++
 include/net/psp/functions.h | 81 ++++++++++++++++++++++++++++++++++++-
 net/core/dev.c              | 35 +++++++++++++---
 net/core/dst.c              |  5 +++
 4 files changed, 133 insertions(+), 7 deletions(-)

diff --git a/include/net/dst_metadata.h b/include/net/dst_metadata.h
index f45d1e3163f0..73755bce2df0 100644
--- a/include/net/dst_metadata.h
+++ b/include/net/dst_metadata.h
@@ -13,6 +13,7 @@ enum metadata_type {
 	METADATA_HW_PORT_MUX,
 	METADATA_MACSEC,
 	METADATA_XFRM,
+	METADATA_PSP,
 };
 
 struct hw_port_info {
@@ -30,6 +31,14 @@ struct xfrm_md_info {
 	struct dst_entry *dst_orig;
 };
 
+/**
+ * struct psp_md_info - PSP association to encrypt a packet with
+ * @pas: association, the metadata_dst holds a reference on it
+ */
+struct psp_md_info {
+	struct psp_assoc *pas;
+};
+
 struct metadata_dst {
 	struct dst_entry		dst;
 	enum metadata_type		type;
@@ -38,6 +47,7 @@ struct metadata_dst {
 		struct hw_port_info	port_info;
 		struct macsec_info	macsec_info;
 		struct xfrm_md_info	xfrm_info;
+		struct psp_md_info	psp_info;
 	} u;
 };
 
@@ -51,6 +61,13 @@ static inline struct metadata_dst *skb_metadata_dst(const struct sk_buff *skb)
 	return NULL;
 }
 
+static inline bool skb_psp_md(const struct sk_buff *skb)
+{
+	struct metadata_dst *md_dst = skb_metadata_dst(skb);
+
+	return md_dst && md_dst->type == METADATA_PSP;
+}
+
 static inline struct ip_tunnel_info *
 skb_tunnel_info(const struct sk_buff *skb)
 {
@@ -125,6 +142,8 @@ static inline int skb_metadata_dst_cmp(const struct sk_buff *skb_a,
 	case METADATA_XFRM:
 		return memcmp(&a->u.xfrm_info, &b->u.xfrm_info,
 			      sizeof(a->u.xfrm_info));
+	case METADATA_PSP:
+		return a->u.psp_info.pas != b->u.psp_info.pas;
 	default:
 		return 1;
 	}
diff --git a/include/net/psp/functions.h b/include/net/psp/functions.h
index d0789a9694bc..796e5f36f249 100644
--- a/include/net/psp/functions.h
+++ b/include/net/psp/functions.h
@@ -6,6 +6,8 @@
 #include <linux/skbuff.h>
 #include <linux/rcupdate.h>
 #include <linux/udp.h>
+#include <net/dst_metadata.h>
+#include <net/gso.h>
 #include <net/sock.h>
 #include <net/tcp.h>
 #include <net/psp/types.h>
@@ -22,6 +24,7 @@ bool psp_dev_encapsulate(struct net *net, struct sk_buff *skb, __be32 spi,
 int psp_dev_rcv(struct sk_buff *skb, u16 dev_id, u8 generation, bool strip_icv);
 
 /* Kernel-facing API */
+#if IS_ENABLED(CONFIG_INET_PSP)
 void psp_assoc_put(struct psp_assoc *pas);
 struct psp_assoc *psp_assoc_get_by_id(struct net *net, u32 id);
 
@@ -34,7 +37,6 @@ static inline bool psp_assoc_is_tx(const struct psp_assoc *pas)
 	return pas->tx.spi != 0;
 }
 
-#if IS_ENABLED(CONFIG_INET_PSP)
 unsigned int psp_key_size(u32 version);
 void psp_sk_assoc_free(struct sock *sk);
 void psp_twsk_init(struct inet_timewait_sock *tw, const struct sock *sk);
@@ -144,14 +146,78 @@ static inline struct psp_assoc *psp_sk_get_assoc_rcu(const struct sock *sk)
 	return pas;
 }
 
+static inline struct psp_assoc *psp_skb_md_assoc(const struct sk_buff *skb)
+{
+	struct metadata_dst *md_dst = skb_metadata_dst(skb);
+
+	if (md_dst && md_dst->type == METADATA_PSP)
+		return md_dst->u.psp_info.pas;
+
+	return NULL;
+}
+
 static inline struct psp_assoc *psp_skb_get_assoc_rcu(struct sk_buff *skb)
 {
+	struct psp_assoc *pas;
+
+	pas = psp_skb_md_assoc(skb);
+	if (pas)
+		return pas;
+
 	if (!skb->decrypted || !skb->sk)
 		return NULL;
 
 	return psp_sk_get_assoc_rcu(skb->sk);
 }
 
+/**
+ * psp_xmit_md() - validate psp association attached as a metadata dst
+ * @skb: packet about to be queued for transmission
+ * @dev: device it is queued on
+ *
+ * If the skb has a psp association attached as metadata dst, this indicates
+ * intent to transmit encrypted, which will only happen when transmit on
+ * the main netdev of its psp device.
+ *
+ * Also check that the packet will still fit @dev's MTU once the device has
+ * added the PSP encapsulation. Sockets leave room for it (psp_sk_overhead()),
+ * whoever attached the association may not have.
+ *
+ * returns:
+ *   0 if @skb carries no psp association
+ *   1 if it does and the dst must be kept for the driver
+ *  -EINVAL if the packet must be dropped, @dev isn't of its psp device
+ *  -EMSGSIZE if the packet must be dropped, too big once encapsulated
+ */
+static inline int psp_xmit_md(const struct sk_buff *skb,
+			      const struct net_device *dev)
+{
+	unsigned int psp_encap = sizeof(struct udphdr) + PSP_HDR_SIZE +
+				 PSP_TRL_SIZE;
+	struct psp_assoc *pas = psp_skb_md_assoc(skb);
+	unsigned int mtu;
+
+	if (likely(!pas))
+		return 0;
+
+	if (rcu_access_pointer(dev->psp_dev) != pas->psd ||
+	    dev != pas->psd->main_netdev)
+		return -EINVAL;
+
+	mtu = READ_ONCE(dev->mtu);
+	if (mtu <= psp_encap)
+		return -EMSGSIZE;
+	mtu -= psp_encap;
+	if (skb_is_gso(skb)) {
+		if (!skb_gso_validate_network_len(skb, mtu))
+			return -EMSGSIZE;
+	} else if (skb->len - skb_network_offset(skb) > mtu) {
+		return -EMSGSIZE;
+	}
+
+	return 1;
+}
+
 static inline unsigned int psp_sk_overhead(const struct sock *sk)
 {
 	int psp_encap = sizeof(struct udphdr) + PSP_HDR_SIZE + PSP_TRL_SIZE;
@@ -160,6 +226,19 @@ static inline unsigned int psp_sk_overhead(const struct sock *sk)
 	return pas && pas->tx.spi ? psp_encap : 0;
 }
 #else
+static inline void psp_assoc_put(struct psp_assoc *pas) { }
+
+static inline struct psp_assoc *psp_assoc_get_by_id(struct net *net, u32 id)
+{
+	return NULL;
+}
+
+static inline int psp_xmit_md(const struct sk_buff *skb,
+			      const struct net_device *dev)
+{
+	return 0;
+}
+
 static inline void psp_sk_assoc_free(struct sock *sk) { }
 static inline void
 psp_twsk_init(struct inet_timewait_sock *tw, const struct sock *sk) { }
diff --git a/net/core/dev.c b/net/core/dev.c
index 9665f3b2f5cb..2cca5f0d302f 100644
--- a/net/core/dev.c
+++ b/net/core/dev.c
@@ -111,6 +111,7 @@
 #include <net/pkt_cls.h>
 #include <net/checksum.h>
 #include <net/xfrm.h>
+#include <net/psp/functions.h>
 #include <net/tcx.h>
 #include <linux/highmem.h>
 #include <linux/init.h>
@@ -4134,9 +4135,13 @@ int skb_csum_hwoffload_help(struct sk_buff *skb,
 }
 EXPORT_SYMBOL(skb_csum_hwoffload_help);
 
-/* Checks if this SKB belongs to an HW offloaded socket
- * and whether any SW fallbacks are required based on dev.
- * Check decrypted mark in case skb_orphan() cleared socket.
+/* Validate an skb for device encryption offload. If the skb belongs to an HW
+ * offloaded socket, apply the socket's callback. Otherwise, an skb which
+ * carries a PSP association is checked again against dev: the PSP device may
+ * have been unregistered while the skb was queued. An skb marked decrypted
+ * (to be encrypted by the device) without either is dropped rather than sent
+ * in clear text. The mark survives losing the socket to skb_orphan(), or the
+ * PSP metadata dst to routing.
  */
 static struct sk_buff *sk_validate_xmit_skb(struct sk_buff *skb,
 					    struct net_device *dev)
@@ -4156,6 +4161,15 @@ static struct sk_buff *sk_validate_xmit_skb(struct sk_buff *skb,
 
 	if (sk_validate) {
 		skb = sk_validate(sk, dev, skb);
+	} else if (unlikely(skb_psp_md(skb))) {
+		int psp = psp_xmit_md(skb, dev);
+
+		if (psp <= 0) {
+			kfree_skb_reason(skb, psp == -EMSGSIZE ?
+					 SKB_DROP_REASON_PKT_TOO_BIG :
+					 SKB_DROP_REASON_PSP_OUTPUT);
+			skb = NULL;
+		}
 	} else if (unlikely(skb_is_decrypted(skb))) {
 		pr_warn_ratelimited("unencrypted skb with no associated socket - dropping\n");
 		kfree_skb(skb);
@@ -4981,7 +4995,7 @@ int __dev_queue_xmit(struct sk_buff *skb, struct net_device *sb_dev)
 	struct net_device *dev = skb->dev;
 	struct netdev_queue *txq = NULL;
 	enum skb_drop_reason reason;
-	int cpu, rc = -ENOMEM;
+	int cpu, psp, rc = -ENOMEM;
 	bool again = false;
 	struct Qdisc *q;
 
@@ -5030,10 +5044,19 @@ int __dev_queue_xmit(struct sk_buff *skb, struct net_device *sb_dev)
 			txq = netdev_tx_queue_mapping(dev, skb);
 	}
 #endif
+	psp = psp_xmit_md(skb, dev);
+	if (unlikely(psp < 0)) {
+		reason = psp == -EMSGSIZE ? SKB_DROP_REASON_PKT_TOO_BIG :
+					    SKB_DROP_REASON_PSP_OUTPUT;
+		rc = psp;
+		goto drop;
+	}
+
 	/* If device/qdisc don't need skb->dst, release it right now while
-	 * its hot in this cpu cache.
+	 * its hot in this cpu cache. A PSP metadata dst carries the packet's
+	 * association to the driver, so keep it in that case.
 	 */
-	if (dev->priv_flags & IFF_XMIT_DST_RELEASE)
+	if ((dev->priv_flags & IFF_XMIT_DST_RELEASE) && likely(!psp))
 		skb_dst_drop(skb);
 	else
 		skb_dst_force(skb);
diff --git a/net/core/dst.c b/net/core/dst.c
index 092861133023..38ad136cb46f 100644
--- a/net/core/dst.c
+++ b/net/core/dst.c
@@ -26,6 +26,7 @@
 
 #include <net/dst.h>
 #include <net/dst_metadata.h>
+#include <net/psp/functions.h>
 
 int dst_discard_out(struct net *net, struct sock *sk, struct sk_buff *skb)
 {
@@ -313,6 +314,8 @@ void metadata_dst_free(struct metadata_dst *md_dst)
 #endif
 	if (md_dst->type == METADATA_XFRM)
 		dst_release(md_dst->u.xfrm_info.dst_orig);
+	if (md_dst->type == METADATA_PSP)
+		psp_assoc_put(md_dst->u.psp_info.pas);
 	kfree(md_dst);
 }
 EXPORT_SYMBOL_GPL(metadata_dst_free);
@@ -349,6 +352,8 @@ void metadata_dst_free_percpu(struct metadata_dst __percpu *md_dst)
 #endif
 		if (one_md_dst->type == METADATA_XFRM)
 			dst_release(one_md_dst->u.xfrm_info.dst_orig);
+		if (one_md_dst->type == METADATA_PSP)
+			psp_assoc_put(one_md_dst->u.psp_info.pas);
 	}
 	free_percpu(md_dst);
 }
-- 
2.53.0-Meta


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH net-next v1 06/15] net/sched: act_mirred: don't send decrypted skbs from egress to ingress
  2026-11-11  0:55 [PATCH net-next v1 00/15] psp: introduce socket-less associations Todd Freed
                   ` (4 preceding siblings ...)
  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 ` Todd Freed
  2026-11-11  0:55 ` [PATCH net-next v1 07/15] bpf: don't redirect " Todd Freed
                   ` (8 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Todd Freed @ 2026-11-11  0:55 UTC (permalink / raw)
  To: netdev, toddfreed
  Cc: David S. Miller, Andrew Lunn, Björn Töpel, bpf,
	Daniel Borkmann, Daniel Zahka, Donald Hunter, Eric Dumazet,
	Jakub Kicinski, Jamal Hadi Salim, Jiri Pirko, John Fastabend,
	linux-doc, linux-kernel, linux-kselftest, linux-rdma,
	Martin KaFai Lau, Paolo Abeni, Saeed Mahameed, Simon Horman,
	Tariq Toukan, Wei Wang, Willem de Bruijn

on transmit, skb->decrypted means the device must encrypt the packet.
For example, a packet of a ktls or psp offload socket, or, with the tc
psp action added later in this series, one which psp encap attached a
tx association to.

on receive, skb->decrypted means the device decrypted the packet.

a packet redirected or mirrored from egress to ingress is by definition
not going to the device that would encrypt it, and on ingress
skb->decrypted now (incorrectly, for this packet) indicates it was
decrypted by the device.

this is less of a problem with socket offloads, whose packets are by
definition originated locally; more of a problem with socket-less psp,
which can also apply to forwarded traffic.

mirred also drops the dst, which for socket-less psp carries the tx
association. clearing the bit instead would let the packet be forwarded
in clear text, so that wouldn't work.

don't send decrypted skbs from egress to ingress: a redirected one is
dropped, a mirrored one isn't copied.

Signed-off-by: Todd Freed <toddfreed@meta.com>
---
 net/sched/act_mirred.c | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/net/sched/act_mirred.c b/net/sched/act_mirred.c
index 1c98e4d81ebe..52c3eeab381a 100644
--- a/net/sched/act_mirred.c
+++ b/net/sched/act_mirred.c
@@ -285,6 +285,12 @@ static int tcf_mirred_to_dev(struct sk_buff *skb, struct tcf_mirred *m,
 		goto err_cant_do;
 	}
 
+	/* On transmit, decrypted asks the device to encrypt the packet. Sent
+	 * to ingress, the packet would read as decrypted by the device.
+	 */
+	if (unlikely(want_ingress && !at_ingress && skb_is_decrypted(skb)))
+		goto err_cant_do;
+
 	/* we could easily avoid the clone only if called by ingress and clsact;
 	 * since we can't easily detect the clsact caller, skip clone only for
 	 * ingress - that covers the TC S/W datapath.
-- 
2.53.0-Meta


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH net-next v1 07/15] bpf: don't redirect decrypted skbs from egress to ingress
  2026-11-11  0:55 [PATCH net-next v1 00/15] psp: introduce socket-less associations Todd Freed
                   ` (5 preceding siblings ...)
  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 ` 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
                   ` (7 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Todd Freed @ 2026-11-11  0:55 UTC (permalink / raw)
  To: netdev, toddfreed
  Cc: David S. Miller, Andrew Lunn, Björn Töpel, bpf,
	Daniel Borkmann, Daniel Zahka, Donald Hunter, Eric Dumazet,
	Jakub Kicinski, Jamal Hadi Salim, Jiri Pirko, John Fastabend,
	linux-doc, linux-kernel, linux-kselftest, linux-rdma,
	Martin KaFai Lau, Paolo Abeni, Saeed Mahameed, Simon Horman,
	Tariq Toukan, Wei Wang, Willem de Bruijn

on transmit, skb->decrypted means the device must encrypt the packet.
For example, a packet of a ktls or psp offload socket, or, with the tc
psp action added later in this series, one which psp encap attached a
tx association to.

on receive, skb->decrypted means the device decrypted the packet.

bpf_redirect() and bpf_clone_redirect() with BPF_F_INGRESS send the
packet to a device's receive path with the bit still set, where it
indicates the packet was decrypted by the device.

this is less of a problem with socket offloads, whose packets are by
definition generated locally; more of a problem with socket-less psp,
which can also apply to forwarded traffic.

in addition, the redirect drops the dst (skb_scrub_packet()), which for
socket-less psp carries the tx association.

drop decrypted skbs which bpf redirects from egress to ingress. this
matches the behavior of mirred, since the previous patch.

Signed-off-by: Todd Freed <toddfreed@meta.com>
---
 net/core/filter.c | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/net/core/filter.c b/net/core/filter.c
index aca8160b26a1..a178a9dd3007 100644
--- a/net/core/filter.c
+++ b/net/core/filter.c
@@ -2226,6 +2226,15 @@ static int __bpf_redirect_common(struct sk_buff *skb, struct net_device *dev,
 static int __bpf_redirect(struct sk_buff *skb, struct net_device *dev,
 			  u32 flags)
 {
+	/* On transmit, decrypted asks the device to encrypt the packet. Sent
+	 * to ingress, the packet would read as decrypted by the device.
+	 */
+	if (unlikely((flags & BPF_F_INGRESS) && !skb_at_tc_ingress(skb) &&
+		     skb_is_decrypted(skb))) {
+		kfree_skb(skb);
+		return -EINVAL;
+	}
+
 	if (dev_is_mac_header_xmit(dev))
 		return __bpf_redirect_common(skb, dev, flags);
 	else
-- 
2.53.0-Meta


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH net-next v1 08/15] psp: only give time-wait sockets of psp connections a validator
  2026-11-11  0:55 [PATCH net-next v1 00/15] psp: introduce socket-less associations Todd Freed
                   ` (6 preceding siblings ...)
  2026-11-11  0:55 ` [PATCH net-next v1 07/15] bpf: don't redirect " Todd Freed
@ 2026-11-11  0:55 ` Todd Freed
  2026-11-11  0:55 ` [PATCH net-next v1 09/15] net/sched: add act_psp for psp transport mode Todd Freed
                   ` (6 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Todd Freed @ 2026-11-11  0:55 UTC (permalink / raw)
  To: netdev, toddfreed
  Cc: David S. Miller, Andrew Lunn, Björn Töpel, bpf,
	Daniel Borkmann, Daniel Zahka, Donald Hunter, Eric Dumazet,
	Jakub Kicinski, Jamal Hadi Salim, Jiri Pirko, John Fastabend,
	linux-doc, linux-kernel, linux-kselftest, linux-rdma,
	Martin KaFai Lau, Paolo Abeni, Saeed Mahameed, Simon Horman,
	Tariq Toukan, Wei Wang, Willem de Bruijn

psp_twsk_init() sets tw_validate_xmit_skb on tcp time-wait sockets.
today, that becomes set whether or not the socket has psp offload, which
is not needed but is harmless. psp_validate_xmit() passes such packets.

but a validator makes a socket look like it has an encryption offload of
its own, and the tc psp action, added in the next patch, relies on that:
encap refuses the packets of such a socket, and sk_validate_xmit_skb()
drops a packet with a psp association whose socket has a validator.

set the validator only when the socket has a psp association.

Signed-off-by: Todd Freed <toddfreed@meta.com>
---
 net/psp/psp_sock.c | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/net/psp/psp_sock.c b/net/psp/psp_sock.c
index 609b06c7af65..664955bfb4fd 100644
--- a/net/psp/psp_sock.c
+++ b/net/psp/psp_sock.c
@@ -330,10 +330,14 @@ void psp_twsk_init(struct inet_timewait_sock *tw, const struct sock *sk)
 {
 	struct psp_assoc *pas = psp_sk_assoc(sk);
 
-	if (pas)
+	if (pas) {
 		refcount_inc(&pas->refcnt);
+		/* Only for PSP connections: a validator makes the socket
+		 * look like it has an encryption offload of its own
+		 */
+		tw->tw_validate_xmit_skb = psp_validate_xmit;
+	}
 	rcu_assign_pointer(tw->psp_assoc, pas);
-	tw->tw_validate_xmit_skb = psp_validate_xmit;
 }
 
 void psp_twsk_assoc_free(struct inet_timewait_sock *tw)
-- 
2.53.0-Meta


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH net-next v1 09/15] net/sched: add act_psp for psp transport mode
  2026-11-11  0:55 [PATCH net-next v1 00/15] psp: introduce socket-less associations Todd Freed
                   ` (7 preceding siblings ...)
  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 ` 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
                   ` (5 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Todd Freed @ 2026-11-11  0:55 UTC (permalink / raw)
  To: netdev, toddfreed
  Cc: David S. Miller, Andrew Lunn, Björn Töpel, bpf,
	Daniel Borkmann, Daniel Zahka, Donald Hunter, Eric Dumazet,
	Jakub Kicinski, Jamal Hadi Salim, Jiri Pirko, John Fastabend,
	linux-doc, linux-kernel, linux-kselftest, linux-rdma,
	Martin KaFai Lau, Paolo Abeni, Saeed Mahameed, Simon Horman,
	Tariq Toukan, Wei Wang, Willem de Bruijn

adds tc action act_psp for psp transport mode encapsulation and
encryption.

encap attaches a psp tx association to the skb as a METADATA_PSP dst.
The packet must then be transmitted on the association's PSP device's
main netdev, which encrypts it: encap on that netdev's egress, or a
redirect there from another netdev's egress. Any other device drops it.

encap on ingress is hardware offload only, like tunnel mode: in
software, the action drops the packet. an ingress-encapped packet would
go through the stack with only its METADATA_PSP dst to keep it from
leaving in clear text, and many paths replace the dst: routing,
netfilter, bpf redirects, tc actions on the way out.

on egress, encap sets skb->decrypted, so that sk_validate_xmit_skb()
drops the packet if it loses the METADATA_PSP dst on the way. mirred
and bpf don't send such a packet back to ingress (previous patches).

Some kinds of packets are not supported and will be dropped,

 - ipv4 fragments. encap inserts udp+psp between the l3 and l4 headers,
   so transport mode can't carry fragments. tunnel mode psp, which a
   later series adds, does support them.
 - ipv6 packets with extension headers, including fragments. some
   extension headers could be supported in the future.
 - packets already encapsulated for a tunnel (skb->encapsulation). some
   tunnel types could be supported in the future.
 - packets which are too large. on transmit, packets which won't fit the
   device's mtu once encapsulated are dropped (see the METADATA_PSP
   patch). the traffic's mtu needs to leave room for the psp encap.
 - packets with another encryption offload: already marked decrypted,
   or of a psp or ktls offload socket, for which sk_validate_xmit_skb()
   would apply the socket's validator instead of checking the
   association. a socket can gain an offload while its packet is queued,
   so sk_validate_xmit_skb() also drops a packet with a METADATA_PSP dst
   whose socket has a validator.

decap removes the PSP Rx state of a packet the device decrypted. to
match packets of only one association, a rule should match the psp
header fields corresponding to the association, e.g. psp_spi / psp_ver /
psp_dev. those flower matches are added in a subsequent patch.

Signed-off-by: Todd Freed <toddfreed@meta.com>
---
 MAINTAINERS                        |   3 +
 include/net/tc_act/tc_psp.h        |  23 ++
 include/uapi/linux/pkt_cls.h       |   1 +
 include/uapi/linux/tc_act/tc_psp.h |  28 ++
 net/core/dev.c                     |  13 +-
 net/sched/Kconfig                  |  13 +
 net/sched/Makefile                 |   1 +
 net/sched/act_psp.c                | 439 +++++++++++++++++++++++++++++
 8 files changed, 519 insertions(+), 2 deletions(-)
 create mode 100644 include/net/tc_act/tc_psp.h
 create mode 100644 include/uapi/linux/tc_act/tc_psp.h
 create mode 100644 net/sched/act_psp.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 76fb13c8730b..a8e45efd8baf 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -22533,8 +22533,11 @@ F:	Documentation/netlink/specs/psp.yaml
 F:	Documentation/networking/psp.rst
 F:	include/net/psp/
 F:	include/net/psp.h
+F:	include/net/tc_act/tc_psp.h
 F:	include/uapi/linux/psp.h
+F:	include/uapi/linux/tc_act/tc_psp.h
 F:	net/psp/
+F:	net/sched/act_psp.c
 K:	struct\ psp(_assoc|_dev|hdr)\b
 
 PSTORE FILESYSTEM
diff --git a/include/net/tc_act/tc_psp.h b/include/net/tc_act/tc_psp.h
new file mode 100644
index 000000000000..8640d7685475
--- /dev/null
+++ b/include/net/tc_act/tc_psp.h
@@ -0,0 +1,23 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef __NET_TC_PSP_H
+#define __NET_TC_PSP_H
+
+#include <net/act_api.h>
+#include <net/dst_metadata.h>
+#include <linux/tc_act/tc_psp.h>
+
+struct tcf_psp_params {
+	struct rcu_head		rcu;
+	int			action;
+	int			psp_action;
+	struct metadata_dst	*md;
+};
+
+struct tcf_psp {
+	struct tc_action		common;
+	struct tcf_psp_params __rcu	*params;
+};
+
+#define to_psp(a) ((struct tcf_psp *)a)
+
+#endif /* __NET_TC_PSP_H */
diff --git a/include/uapi/linux/pkt_cls.h b/include/uapi/linux/pkt_cls.h
index 28d94b11d1aa..14cff8f5da74 100644
--- a/include/uapi/linux/pkt_cls.h
+++ b/include/uapi/linux/pkt_cls.h
@@ -139,6 +139,7 @@ enum tca_id {
 	TCA_ID_MPLS,
 	TCA_ID_CT,
 	TCA_ID_GATE,
+	TCA_ID_PSP,
 	/* other actions go here */
 	__TCA_ID_MAX = 255
 };
diff --git a/include/uapi/linux/tc_act/tc_psp.h b/include/uapi/linux/tc_act/tc_psp.h
new file mode 100644
index 000000000000..e58ebcae77be
--- /dev/null
+++ b/include/uapi/linux/tc_act/tc_psp.h
@@ -0,0 +1,28 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+#ifndef __LINUX_TC_PSP_H
+#define __LINUX_TC_PSP_H
+
+#include <linux/pkt_cls.h>
+
+#define TCA_PSP_ACT_ENCAP	1	/* attach a PSP association */
+#define TCA_PSP_ACT_DECAP	2	/* drop PSP Rx state */
+
+struct tc_psp {
+	tc_gen;
+};
+
+enum {
+	TCA_PSP_UNSPEC,
+	TCA_PSP_TM,
+	TCA_PSP_PARMS,
+	TCA_PSP_PAD,
+	TCA_PSP_ACTION,		/* u32, TCA_PSP_ACT_* */
+	TCA_PSP_ASSOC_ID,	/* u32, encap only: Tx association ID from
+				 * the PSP netlink family (assoc-add)
+				 */
+	__TCA_PSP_MAX,
+};
+
+#define TCA_PSP_MAX (__TCA_PSP_MAX - 1)
+
+#endif
diff --git a/net/core/dev.c b/net/core/dev.c
index 2cca5f0d302f..353c0409b3c0 100644
--- a/net/core/dev.c
+++ b/net/core/dev.c
@@ -4154,13 +4154,22 @@ static struct sk_buff *sk_validate_xmit_skb(struct sk_buff *skb,
 	sk_validate = NULL;
 	if (sk) {
 		if (sk_fullsock(sk))
-			sk_validate = sk->sk_validate_xmit_skb;
+			sk_validate = READ_ONCE(sk->sk_validate_xmit_skb);
 		else if (sk_is_inet(sk) && sk->sk_state == TCP_TIME_WAIT)
 			sk_validate = inet_twsk(sk)->tw_validate_xmit_skb;
 	}
 
 	if (sk_validate) {
-		skb = sk_validate(sk, dev, skb);
+		/* act_psp doesn't give an association to the packets of a
+		 * socket with its own offload, but the socket can gain one
+		 * while such a packet is queued
+		 */
+		if (unlikely(skb_psp_md(skb))) {
+			kfree_skb_reason(skb, SKB_DROP_REASON_PSP_OUTPUT);
+			skb = NULL;
+		} else {
+			skb = sk_validate(sk, dev, skb);
+		}
 	} else if (unlikely(skb_psp_md(skb))) {
 		int psp = psp_xmit_md(skb, dev);
 
diff --git a/net/sched/Kconfig b/net/sched/Kconfig
index 6ddff028b81a..35f9d76c4cae 100644
--- a/net/sched/Kconfig
+++ b/net/sched/Kconfig
@@ -939,6 +939,19 @@ config NET_ACT_GATE
 	  To compile this code as a module, choose M here: the
 	  module will be called act_gate.
 
+config NET_ACT_PSP
+	tristate "PSP encryption tc action"
+	depends on NET_CLS_ACT && INET_PSP
+	help
+	  Say Y here to allow PSP associations, created through the PSP
+	  netlink family, to be attached to packets. PSP capable devices can
+	  then encrypt them on transmit. Enables transparent psp tunnels for
+	  forwarded traffic.
+
+	  If unsure, say N.
+	  To compile this code as a module, choose M here: the
+	  module will be called act_psp.
+
 config NET_IFE_SKBMARK
 	tristate "Support to encoding decoding skb mark on IFE action"
 	depends on NET_ACT_IFE
diff --git a/net/sched/Makefile b/net/sched/Makefile
index 5078ea84e6ad..9fe8cc0abfdf 100644
--- a/net/sched/Makefile
+++ b/net/sched/Makefile
@@ -31,6 +31,7 @@ obj-$(CONFIG_NET_IFE_SKBTCINDEX)	+= act_meta_skbtcindex.o
 obj-$(CONFIG_NET_ACT_TUNNEL_KEY)+= act_tunnel_key.o
 obj-$(CONFIG_NET_ACT_CT)	+= act_ct.o
 obj-$(CONFIG_NET_ACT_GATE)	+= act_gate.o
+obj-$(CONFIG_NET_ACT_PSP)	+= act_psp.o
 obj-$(CONFIG_NET_SCH_FIFO)	+= sch_fifo.o
 obj-$(CONFIG_NET_SCH_HTB)	+= sch_htb.o
 obj-$(CONFIG_NET_SCH_HFSC)	+= sch_hfsc.o
diff --git a/net/sched/act_psp.c b/net/sched/act_psp.c
new file mode 100644
index 000000000000..4ff54203c9f5
--- /dev/null
+++ b/net/sched/act_psp.c
@@ -0,0 +1,439 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * tc action that attaches a PSP association to packets.
+ *
+ * encap: attach a tx association referenced by its ID, as a METADATA_PSP dst.
+ *        The PSP device's netdev encrypts the packet on transmit. In
+ *        software, egress only: like tunnel mode, encap on ingress is
+ *        hardware offload only.
+ * decap: drop the PSP Rx state (SKB_EXT_PSP) of a decrypted packet. It
+ *        takes no association. a rule which should only take the packets
+ *        of one association matches its properties against psp packet header
+ *        fields (flower psp_spi / psp_ver / psp_dev).
+ */
+
+#include <linux/ip.h>
+#include <linux/ipv6.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/skbuff.h>
+#include <net/dst.h>
+#include <net/inet_timewait_sock.h>
+#include <net/ip.h>
+#include <net/netlink.h>
+#include <net/pkt_cls.h>
+#include <net/pkt_sched.h>
+#include <net/psp/functions.h>
+#include <net/tc_act/tc_psp.h>
+
+static struct tc_action_ops act_psp_ops;
+
+/* The device inserts UDP + PSP between the IP header and the transport
+ * header, so the transport header must be right by the time the packet
+ * gets there. On a forwarded packet it may not be. Transport mode doesn't
+ * support IP fragments or IPv6 extension headers.
+ */
+static bool tcf_psp_set_transport_header(struct sk_buff *skb)
+{
+	unsigned int off = skb_network_offset(skb);
+	const struct iphdr *iph;
+	unsigned int hlen, len;
+
+	switch (skb->protocol) {
+	case htons(ETH_P_IP):
+		if (!pskb_may_pull(skb, off + sizeof(*iph)))
+			return false;
+		iph = ip_hdr(skb);
+		if (iph->version != 4 || iph->ihl < 5 || ip_is_fragment(iph))
+			return false;
+		hlen = iph->ihl * 4;
+		len = ntohs(iph->tot_len);
+		if (!pskb_may_pull(skb, off + hlen))
+			return false;
+		break;
+	case htons(ETH_P_IPV6):
+		if (!pskb_may_pull(skb, off + sizeof(struct ipv6hdr)))
+			return false;
+		if (ipv6_hdr(skb)->version != 6 ||
+		    ipv6_ext_hdr(ipv6_hdr(skb)->nexthdr))
+			return false;
+		hlen = sizeof(struct ipv6hdr);
+		len = hlen + ntohs(ipv6_hdr(skb)->payload_len);
+		break;
+	default:
+		return false;
+	}
+
+	/* The device takes the length of the payload from the packet: it must
+	 * hold the IP length, and L2 padding, e.g. of a minimum size frame,
+	 * must not be encrypted as payload. A GSO skb's header doesn't always
+	 * carry its length (BIG TCP).
+	 */
+	if (!skb_is_gso(skb)) {
+		if (len < hlen || skb->len - off < len)
+			return false;
+		if (pskb_trim_rcsum(skb, off + len))
+			return false;
+	}
+
+	skb_set_transport_header(skb, off + hlen);
+	return true;
+}
+
+/* The socket of the packet has its own encryption offload, kTLS or PSP:
+ * sk_validate_xmit_skb() then applies the socket's validator instead of
+ * checking the association.
+ */
+static bool tcf_psp_sk_offload(const struct sk_buff *skb)
+{
+#ifdef CONFIG_SOCK_VALIDATE_XMIT
+	struct sock *sk = skb->sk;
+
+	if (!sk)
+		return false;
+	if (sk_fullsock(sk))
+		return !!READ_ONCE(sk->sk_validate_xmit_skb);
+	return sk_is_inet(sk) && sk->sk_state == TCP_TIME_WAIT &&
+	       inet_twsk(sk)->tw_validate_xmit_skb;
+#else
+	return false;
+#endif
+}
+
+static int tcf_psp_drop(struct tcf_psp *p, struct sk_buff *skb,
+			enum skb_drop_reason reason)
+{
+	tcf_action_inc_drop_qstats(&p->common);
+	tcf_set_drop_reason(skb, reason);
+	return TC_ACT_SHOT;
+}
+
+static int tcf_psp_act(struct sk_buff *skb, const struct tc_action *a,
+		       struct tcf_result *res)
+{
+	struct tcf_psp *p = to_psp(a);
+	struct tcf_psp_params *params;
+
+	params = rcu_dereference_bh(p->params);
+
+	tcf_lastuse_update(&p->tcf_tm);
+	tcf_action_update_bstats(&p->common, skb);
+
+	switch (params->psp_action) {
+	case TCA_PSP_ACT_ENCAP:
+		/* Encap on ingress is hardware offload only, like tunnel
+		 * mode: the packet would go through the stack with nothing
+		 * but its dst, which routing, netfilter or a redirect may
+		 * replace, to keep it from leaving in clear text
+		 */
+		if (unlikely(skb_at_tc_ingress(skb)))
+			return tcf_psp_drop(p, skb, SKB_DROP_REASON_PSP_OUTPUT);
+		/* PSP on a tunnel isn't supported: the encap would overwrite
+		 * the inner headers which the device's tunnel offloads use
+		 */
+		if (unlikely(skb->encapsulation))
+			return tcf_psp_drop(p, skb, SKB_DROP_REASON_PSP_OUTPUT);
+		/* Don't mix the association with another encryption
+		 * offload: the packet is already marked decrypted, or its
+		 * socket has an offload of its own
+		 */
+		if (unlikely(skb_is_decrypted(skb) || tcf_psp_sk_offload(skb)))
+			return tcf_psp_drop(p, skb, SKB_DROP_REASON_PSP_OUTPUT);
+		if (unlikely(!tcf_psp_set_transport_header(skb)))
+			return tcf_psp_drop(p, skb, SKB_DROP_REASON_PSP_OUTPUT);
+		skb_dst_drop(skb);
+		skb_dst_set(skb, dst_clone(&params->md->dst));
+		/* Must be encrypted: sk_validate_xmit_skb() drops it unless
+		 * it still carries the association when it is transmitted
+		 */
+		skb->decrypted = 1;
+		break;
+	case TCA_PSP_ACT_DECAP:
+		/* Only a packet the device decrypted: on transmit,
+		 * skb->decrypted means "device, please encrypt", which decap
+		 * must not undo.
+		 */
+		if (!skb_ext_find(skb, SKB_EXT_PSP))
+			break;
+		skb_ext_del(skb, SKB_EXT_PSP);
+		skb->decrypted = 0;
+		break;
+	}
+
+	return params->action;
+}
+
+static const struct nla_policy psp_policy[TCA_PSP_MAX + 1] = {
+	[TCA_PSP_PARMS]		= NLA_POLICY_EXACT_LEN(sizeof(struct tc_psp)),
+	[TCA_PSP_ACTION]	= NLA_POLICY_RANGE(NLA_U32, TCA_PSP_ACT_ENCAP,
+						   TCA_PSP_ACT_DECAP),
+	[TCA_PSP_ASSOC_ID]	= NLA_POLICY_MIN(NLA_U32, 1),
+};
+
+static void tcf_psp_release_params_rcu(struct rcu_head *head)
+{
+	struct tcf_psp_params *params = container_of(head, typeof(*params),
+						     rcu);
+
+	if (params->md)
+		dst_release(&params->md->dst);
+	kfree(params);
+}
+
+static void tcf_psp_release_params(struct tcf_psp_params *params)
+{
+	if (params)
+		call_rcu(&params->rcu, tcf_psp_release_params_rcu);
+}
+
+static struct metadata_dst *
+tcf_psp_md_create(struct net *net, struct nlattr *id_attr,
+		  struct netlink_ext_ack *extack)
+{
+	struct metadata_dst *md;
+	struct psp_assoc *pas;
+
+	pas = psp_assoc_get_by_id(net, nla_get_u32(id_attr));
+	if (!pas) {
+		NL_SET_ERR_MSG_ATTR(extack, id_attr,
+				    "PSP association not found");
+		return ERR_PTR(-ENOENT);
+	}
+	if (!psp_assoc_is_tx(pas)) {
+		NL_SET_ERR_MSG_ATTR(extack, id_attr,
+				    "encap needs a Tx association (assoc-add)");
+		psp_assoc_put(pas);
+		return ERR_PTR(-EINVAL);
+	}
+
+	md = metadata_dst_alloc(0, METADATA_PSP, GFP_KERNEL);
+	if (!md) {
+		psp_assoc_put(pas);
+		return ERR_PTR(-ENOMEM);
+	}
+	/* The dst now owns the reference on the association */
+	md->u.psp_info.pas = pas;
+
+	return md;
+}
+
+static int tcf_psp_init(struct net *net, struct nlattr *nla,
+			struct nlattr *est, struct tc_action **a,
+			struct tcf_proto *tp, u32 flags,
+			struct netlink_ext_ack *extack)
+{
+	struct tc_action_net *tn = net_generic(net, act_psp_ops.net_id);
+	bool bind = flags & TCA_ACT_FLAGS_BIND;
+	struct nlattr *tb[TCA_PSP_MAX + 1];
+	struct tcf_psp_params *params_new;
+	struct metadata_dst *md = NULL;
+	struct tcf_chain *goto_ch = NULL;
+	struct tc_psp *parm;
+	bool exists = false;
+	int ret = 0, err;
+	struct tcf_psp *p;
+	u32 psp_action;
+	u32 index;
+
+	if (!nla) {
+		NL_SET_ERR_MSG(extack, "PSP action requires attributes");
+		return -EINVAL;
+	}
+
+	err = nla_parse_nested(tb, TCA_PSP_MAX, nla, psp_policy, extack);
+	if (err < 0)
+		return err;
+
+	if (!tb[TCA_PSP_PARMS]) {
+		NL_SET_ERR_MSG(extack, "Missing PSP action parameters");
+		return -EINVAL;
+	}
+
+	parm = nla_data(tb[TCA_PSP_PARMS]);
+	index = parm->index;
+	err = tcf_idr_check_alloc(tn, &index, a, bind);
+	if (err < 0)
+		return err;
+	exists = err;
+	if (exists && bind)
+		return ACT_P_BOUND;
+
+	if (NL_REQ_ATTR_CHECK(extack, nla, tb, TCA_PSP_ACTION)) {
+		NL_SET_ERR_MSG(extack, "Missing PSP action, encap or decap");
+		ret = -EINVAL;
+		goto err_out;
+	}
+	psp_action = nla_get_u32(tb[TCA_PSP_ACTION]);
+
+	switch (psp_action) {
+	case TCA_PSP_ACT_ENCAP:
+		if (NL_REQ_ATTR_CHECK(extack, nla, tb, TCA_PSP_ASSOC_ID)) {
+			NL_SET_ERR_MSG(extack, "Missing PSP association ID");
+			ret = -EINVAL;
+			goto err_out;
+		}
+		md = tcf_psp_md_create(net, tb[TCA_PSP_ASSOC_ID], extack);
+		if (IS_ERR(md)) {
+			ret = PTR_ERR(md);
+			md = NULL;
+			goto err_out;
+		}
+		break;
+	case TCA_PSP_ACT_DECAP:
+		if (tb[TCA_PSP_ASSOC_ID]) {
+			NL_SET_ERR_MSG_ATTR(extack, tb[TCA_PSP_ASSOC_ID],
+					    "decap takes no PSP association");
+			ret = -EINVAL;
+			goto err_out;
+		}
+		break;
+	}
+
+	if (!exists) {
+		ret = tcf_idr_create_from_flags(tn, index, est, a,
+						&act_psp_ops, bind, flags);
+		if (ret)
+			goto release_md;
+
+		ret = ACT_P_CREATED;
+	} else if (!(flags & TCA_ACT_FLAGS_REPLACE)) {
+		NL_SET_ERR_MSG(extack, "PSP action already exists");
+		ret = -EEXIST;
+		goto release_md;
+	}
+
+	err = tcf_action_check_ctrlact(parm->action, tp, &goto_ch, extack);
+	if (err < 0) {
+		ret = err;
+		exists = true;
+		goto release_md;
+	}
+	p = to_psp(*a);
+
+	params_new = kzalloc_obj(*params_new);
+	if (!params_new) {
+		ret = -ENOMEM;
+		exists = true;
+		goto put_chain;
+	}
+	params_new->action = parm->action;
+	params_new->psp_action = psp_action;
+	params_new->md = md;
+
+	spin_lock_bh(&p->tcf_lock);
+	goto_ch = tcf_action_set_ctrlact(*a, parm->action, goto_ch);
+	params_new = rcu_replace_pointer(p->params, params_new,
+					 lockdep_is_held(&p->tcf_lock));
+	spin_unlock_bh(&p->tcf_lock);
+
+	tcf_psp_release_params(params_new);
+	if (goto_ch)
+		tcf_chain_put_by_act(goto_ch);
+
+	return ret;
+
+put_chain:
+	if (goto_ch)
+		tcf_chain_put_by_act(goto_ch);
+release_md:
+	if (md)
+		dst_release(&md->dst);
+err_out:
+	if (exists)
+		tcf_idr_release(*a, bind);
+	else
+		tcf_idr_cleanup(tn, index);
+	return ret;
+}
+
+static void tcf_psp_cleanup(struct tc_action *a)
+{
+	struct tcf_psp *p = to_psp(a);
+
+	tcf_psp_release_params(rcu_dereference_protected(p->params, 1));
+}
+
+static int tcf_psp_dump(struct sk_buff *skb, struct tc_action *a,
+			int bind, int ref)
+{
+	unsigned char *b = skb_tail_pointer(skb);
+	struct tcf_psp *p = to_psp(a);
+	struct tcf_psp_params *params;
+	struct tc_psp opt = {
+		.index    = p->tcf_index,
+		.refcnt   = refcount_read(&p->tcf_refcnt) - ref,
+		.bindcnt  = atomic_read(&p->tcf_bindcnt) - bind,
+	};
+	struct tcf_t t;
+
+	rcu_read_lock();
+	params = rcu_dereference(p->params);
+	opt.action = params->action;
+
+	if (nla_put(skb, TCA_PSP_PARMS, sizeof(opt), &opt) ||
+	    nla_put_u32(skb, TCA_PSP_ACTION, params->psp_action))
+		goto nla_put_failure;
+
+	if (params->md &&
+	    nla_put_u32(skb, TCA_PSP_ASSOC_ID, params->md->u.psp_info.pas->id))
+		goto nla_put_failure;
+
+	tcf_tm_dump(&t, &p->tcf_tm);
+	if (nla_put_64bit(skb, TCA_PSP_TM, sizeof(t), &t, TCA_PSP_PAD))
+		goto nla_put_failure;
+	rcu_read_unlock();
+
+	return skb->len;
+
+nla_put_failure:
+	rcu_read_unlock();
+	nlmsg_trim(skb, b);
+	return -1;
+}
+
+static struct tc_action_ops act_psp_ops = {
+	.kind		=	"psp",
+	.id		=	TCA_ID_PSP,
+	.owner		=	THIS_MODULE,
+	.act		=	tcf_psp_act,
+	.dump		=	tcf_psp_dump,
+	.init		=	tcf_psp_init,
+	.cleanup	=	tcf_psp_cleanup,
+	.size		=	sizeof(struct tcf_psp),
+};
+MODULE_ALIAS_NET_ACT("psp");
+
+static __net_init int psp_init_net(struct net *net)
+{
+	struct tc_action_net *tn = net_generic(net, act_psp_ops.net_id);
+
+	return tc_action_net_init(net, tn, &act_psp_ops);
+}
+
+static void __net_exit psp_exit_net(struct list_head *net_list)
+{
+	tc_action_net_exit(net_list, act_psp_ops.net_id);
+}
+
+static struct pernet_operations psp_net_ops = {
+	.init = psp_init_net,
+	.exit_batch = psp_exit_net,
+	.id   = &act_psp_ops.net_id,
+	.size = sizeof(struct tc_action_net),
+};
+
+static int __init psp_init_module(void)
+{
+	return tcf_register_action(&act_psp_ops, &psp_net_ops);
+}
+
+static void __exit psp_cleanup_module(void)
+{
+	tcf_unregister_action(&act_psp_ops, &psp_net_ops);
+}
+
+module_init(psp_init_module);
+module_exit(psp_cleanup_module);
+
+MODULE_DESCRIPTION("PSP encryption tc action");
+MODULE_LICENSE("GPL");
-- 
2.53.0-Meta


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH net-next v1 10/15] net/sched: flower: add psp_spi, psp_ver, and psp_dev matches
  2026-11-11  0:55 [PATCH net-next v1 00/15] psp: introduce socket-less associations Todd Freed
                   ` (8 preceding siblings ...)
  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 ` Todd Freed
  2026-11-11  0:55 ` [PATCH net-next v1 11/15] selftests: drv-net: psp: test socket-less associations and the tc psp action Todd Freed
                   ` (4 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Todd Freed @ 2026-11-11  0:55 UTC (permalink / raw)
  To: netdev, toddfreed
  Cc: David S. Miller, Andrew Lunn, Björn Töpel, bpf,
	Daniel Borkmann, Daniel Zahka, Donald Hunter, Eric Dumazet,
	Jakub Kicinski, Jamal Hadi Salim, Jiri Pirko, John Fastabend,
	linux-doc, linux-kernel, linux-kselftest, linux-rdma,
	Martin KaFai Lau, Paolo Abeni, Saeed Mahameed, Simon Horman,
	Tariq Toukan, Wei Wang, Willem de Bruijn

Add a flow dissector key filled from the PSP Rx state (SKB_EXT_PSP) the
device attached to a decrypted packet, and corresponding flower matches.
SPI the packet was decrypted with (psp_spi, with a mask), PSP version
(psp_ver), and the ID of the PSP device which decrypted it (psp_dev).

For example, to take the packets of one Rx association, (created with
rx-spi-alloc), and remove their PSP state so that they can be forwarded,

  tc filter add dev eth0 ingress protocol ipv6 flower
      psp_spi 0x80000001 psp_ver 0 psp_dev 1
      action psp decap

Together these matches tie a rule to one association, in a form which
hw can match on (offload comes in a follow-up series). The SPI is only
unique on its PSP device, and an association has one PSP version. This
matches the per-packet check that psp sockets have against its
association, with one exception - the generation of the device key is
not checked.

this means that a rule matching packets that correspond to an rx
association which is stale (two rotations have passed), can still match.

It is therefore important that a rule which matches a SPI should be
replaced before the device key has had two rotations. follow up work
will bring enforcement of this based on key rotation events.

Note that this only ever matches packets which were decrypted, even if
the mask is 0. So "psp_spi 0/0" matches any decrypted PSP packet.

Signed-off-by: Todd Freed <toddfreed@meta.com>
---
 Documentation/netlink/specs/tc.yaml | 14 +++++++
 include/linux/skbuff.h              |  4 ++
 include/net/flow_dissector.h        | 18 ++++++++
 include/uapi/linux/pkt_cls.h        |  5 +++
 net/core/flow_dissector.c           | 27 ++++++++++++
 net/sched/cls_flower.c              | 64 +++++++++++++++++++++++++++++
 6 files changed, 132 insertions(+)

diff --git a/Documentation/netlink/specs/tc.yaml b/Documentation/netlink/specs/tc.yaml
index b6ccc645b0e3..044938316ffd 100644
--- a/Documentation/netlink/specs/tc.yaml
+++ b/Documentation/netlink/specs/tc.yaml
@@ -2908,6 +2908,20 @@ attribute-sets:
         byte-order: big-endian
         enum: flower-key-ctrl-flags
         enum-as-flags: true
+      -
+        name: key-psp-spi
+        type: u32
+        byte-order: big-endian
+      -
+        name: key-psp-spi-mask
+        type: u32
+        byte-order: big-endian
+      -
+        name: key-psp-version
+        type: u8
+      -
+        name: key-psp-dev
+        type: u32
   -
     name: flower-key-enc-opts-attrs
     name-prefix: tca-flower-key-enc-opts-
diff --git a/include/linux/skbuff.h b/include/linux/skbuff.h
index 73365c5de6f9..e65de3f5c136 100644
--- a/include/linux/skbuff.h
+++ b/include/linux/skbuff.h
@@ -1660,6 +1660,10 @@ void skb_flow_dissect_hash(const struct sk_buff *skb,
 			   struct flow_dissector *flow_dissector,
 			   void *target_container);
 
+void skb_flow_dissect_psp(const struct sk_buff *skb,
+			  struct flow_dissector *flow_dissector,
+			  void *target_container);
+
 static inline __u32 skb_get_hash_net(const struct net *net, struct sk_buff *skb)
 {
 	if (!skb->l4_hash && !skb->sw_hash)
diff --git a/include/net/flow_dissector.h b/include/net/flow_dissector.h
index ced79dc8e856..151ae03d5ad5 100644
--- a/include/net/flow_dissector.h
+++ b/include/net/flow_dissector.h
@@ -339,6 +339,23 @@ struct flow_dissector_key_cfm {
 	u8	opcode;
 };
 
+/**
+ * struct flow_dissector_key_psp - PSP Rx state of a decrypted packet
+ * @spi: SPI of the PSP association the packet was decrypted with
+ * @dev_id: ID of the PSP device which decrypted the packet
+ * @version: PSP version (enum psp_version) the packet was decrypted with
+ * @present: packet was decrypted by a PSP device
+ *
+ * Taken from the PSP Rx state the device attached to the skb, not from the
+ * packet, which no longer carries the PSP header.
+ */
+struct flow_dissector_key_psp {
+	__be32	spi;
+	u16	dev_id;
+	u8	version;
+	u8	present;
+};
+
 #define FLOW_DIS_CFM_MDL_MASK GENMASK(7, 5)
 #define FLOW_DIS_CFM_MDL_MAX 7
 
@@ -376,6 +393,7 @@ enum flow_dissector_key_id {
 	FLOW_DISSECTOR_KEY_L2TPV3, /* struct flow_dissector_key_l2tpv3 */
 	FLOW_DISSECTOR_KEY_CFM, /* struct flow_dissector_key_cfm */
 	FLOW_DISSECTOR_KEY_IPSEC, /* struct flow_dissector_key_ipsec */
+	FLOW_DISSECTOR_KEY_PSP, /* struct flow_dissector_key_psp */
 
 	FLOW_DISSECTOR_KEY_MAX,
 };
diff --git a/include/uapi/linux/pkt_cls.h b/include/uapi/linux/pkt_cls.h
index 14cff8f5da74..0cfa7425404b 100644
--- a/include/uapi/linux/pkt_cls.h
+++ b/include/uapi/linux/pkt_cls.h
@@ -561,6 +561,11 @@ enum {
 	TCA_FLOWER_KEY_ENC_FLAGS,	/* be32 */
 	TCA_FLOWER_KEY_ENC_FLAGS_MASK,	/* be32 */
 
+	TCA_FLOWER_KEY_PSP_SPI,		/* be32 */
+	TCA_FLOWER_KEY_PSP_SPI_MASK,	/* be32 */
+	TCA_FLOWER_KEY_PSP_VERSION,	/* u8, enum psp_version */
+	TCA_FLOWER_KEY_PSP_DEV,		/* u32, PSP device ID */
+
 	__TCA_FLOWER_MAX,
 };
 
diff --git a/net/core/flow_dissector.c b/net/core/flow_dissector.c
index 1be6c739e3c7..95d3fa3b1568 100644
--- a/net/core/flow_dissector.c
+++ b/net/core/flow_dissector.c
@@ -28,6 +28,7 @@
 #include <linux/ptp_classify.h>
 #include <net/flow_dissector.h>
 #include <net/pkt_cls.h>
+#include <net/psp/types.h>
 #include <scsi/fc/fc_fcoe.h>
 #include <uapi/linux/batadv_packet.h>
 #include <linux/bpf.h>
@@ -511,6 +512,32 @@ void skb_flow_dissect_hash(const struct sk_buff *skb,
 }
 EXPORT_SYMBOL(skb_flow_dissect_hash);
 
+void skb_flow_dissect_psp(const struct sk_buff *skb,
+			  struct flow_dissector *flow_dissector,
+			  void *target_container)
+{
+#if IS_ENABLED(CONFIG_INET_PSP)
+	struct flow_dissector_key_psp *key;
+	struct psp_skb_ext *pse;
+
+	if (!dissector_uses_key(flow_dissector, FLOW_DISSECTOR_KEY_PSP))
+		return;
+
+	pse = skb_ext_find(skb, SKB_EXT_PSP);
+	if (!pse)
+		return;
+
+	key = skb_flow_dissector_target(flow_dissector,
+					FLOW_DISSECTOR_KEY_PSP,
+					target_container);
+	key->spi = pse->spi;
+	key->dev_id = pse->dev_id;
+	key->version = pse->version;
+	key->present = 1;
+#endif
+}
+EXPORT_SYMBOL(skb_flow_dissect_psp);
+
 static enum flow_dissect_ret
 __skb_flow_dissect_mpls(const struct sk_buff *skb,
 			struct flow_dissector *flow_dissector,
diff --git a/net/sched/cls_flower.c b/net/sched/cls_flower.c
index 23eba4a00a94..f983deab1a6d 100644
--- a/net/sched/cls_flower.c
+++ b/net/sched/cls_flower.c
@@ -81,6 +81,7 @@ struct fl_flow_key {
 	struct flow_dissector_key_l2tpv3 l2tpv3;
 	struct flow_dissector_key_ipsec ipsec;
 	struct flow_dissector_key_cfm cfm;
+	struct flow_dissector_key_psp psp;
 } __aligned(BITS_PER_LONG / 8); /* Ensure that we can do comparisons as longs. */
 
 struct fl_flow_mask_range {
@@ -343,6 +344,7 @@ TC_INDIRECT_SCOPE int fl_classify(struct sk_buff *skb,
 				    ARRAY_SIZE(fl_ct_info_to_flower_map),
 				    post_ct, zone);
 		skb_flow_dissect_hash(skb, &mask->dissector, &skb_key);
+		skb_flow_dissect_psp(skb, &mask->dissector, &skb_key);
 		skb_flow_dissect(skb, &mask->dissector, &skb_key,
 				 FLOW_DISSECTOR_F_STOP_BEFORE_ENCAP);
 
@@ -633,6 +635,11 @@ static void *fl_get(struct tcf_proto *tp, u32 handle)
 	return __fl_get(head, handle);
 }
 
+/* SKB_EXT_PSP holds the PSP device ID in 16 bits */
+static const struct netlink_range_validation fl_psp_dev_range = {
+	.max = U16_MAX,
+};
+
 static const struct nla_policy fl_policy[TCA_FLOWER_MAX + 1] = {
 	[TCA_FLOWER_UNSPEC]		= { .strict_start_type =
 						TCA_FLOWER_L2_MISS },
@@ -745,6 +752,12 @@ static const struct nla_policy fl_policy[TCA_FLOWER_MAX + 1] = {
 	[TCA_FLOWER_KEY_SPI]		= { .type = NLA_U32 },
 	[TCA_FLOWER_KEY_SPI_MASK]	= { .type = NLA_U32 },
 	[TCA_FLOWER_L2_MISS]		= NLA_POLICY_MAX(NLA_U8, 1),
+	[TCA_FLOWER_KEY_PSP_SPI]	= { .type = NLA_BE32 },
+	[TCA_FLOWER_KEY_PSP_SPI_MASK]	= { .type = NLA_BE32 },
+	/* the version field of the PSP header has 4 bits */
+	[TCA_FLOWER_KEY_PSP_VERSION]	= NLA_POLICY_MAX(NLA_U8, 15),
+	[TCA_FLOWER_KEY_PSP_DEV]	= NLA_POLICY_FULL_RANGE(NLA_U32,
+							&fl_psp_dev_range),
 	[TCA_FLOWER_KEY_CFM]		= { .type = NLA_NESTED },
 	[TCA_FLOWER_KEY_ENC_FLAGS]	= NLA_POLICY_MASK(NLA_BE32,
 							  TCA_FLOWER_KEY_ENC_FLAGS_POLICY_MASK),
@@ -2077,6 +2090,33 @@ static int fl_set_key(struct net *net, struct nlattr *tca_opts,
 			return ret;
 	}
 
+	if (tb[TCA_FLOWER_KEY_PSP_SPI_MASK] && !tb[TCA_FLOWER_KEY_PSP_SPI]) {
+		NL_SET_ERR_ATTR_MISS(extack, tca_opts, TCA_FLOWER_KEY_PSP_SPI);
+		return -EINVAL;
+	}
+	if (tb[TCA_FLOWER_KEY_PSP_SPI] || tb[TCA_FLOWER_KEY_PSP_VERSION] ||
+	    tb[TCA_FLOWER_KEY_PSP_DEV]) {
+		if (!IS_ENABLED(CONFIG_INET_PSP)) {
+			NL_SET_ERR_MSG(extack, "PSP isn't enabled");
+			return -EOPNOTSUPP;
+		}
+		fl_set_key_val(tb, &key->psp.spi, TCA_FLOWER_KEY_PSP_SPI,
+			       &mask->psp.spi, TCA_FLOWER_KEY_PSP_SPI_MASK,
+			       sizeof(key->psp.spi));
+		if (tb[TCA_FLOWER_KEY_PSP_VERSION]) {
+			key->psp.version =
+				nla_get_u8(tb[TCA_FLOWER_KEY_PSP_VERSION]);
+			mask->psp.version = 0xff;
+		}
+		if (tb[TCA_FLOWER_KEY_PSP_DEV]) {
+			key->psp.dev_id = nla_get_u32(tb[TCA_FLOWER_KEY_PSP_DEV]);
+			mask->psp.dev_id = 0xffff;
+		}
+		/* Only match packets which were actually decrypted */
+		key->psp.present = 1;
+		mask->psp.present = 0xff;
+	}
+
 	if (tb[TCA_FLOWER_KEY_ENC_IPV4_SRC] ||
 	    tb[TCA_FLOWER_KEY_ENC_IPV4_DST]) {
 		key->enc_control.addr_type = FLOW_DISSECTOR_KEY_IPV4_ADDRS;
@@ -2263,6 +2303,8 @@ static void fl_init_dissector(struct flow_dissector *dissector,
 			     FLOW_DISSECTOR_KEY_IPSEC, ipsec);
 	FL_KEY_SET_IF_MASKED(mask, keys, cnt,
 			     FLOW_DISSECTOR_KEY_CFM, cfm);
+	FL_KEY_SET_IF_MASKED(mask, keys, cnt,
+			     FLOW_DISSECTOR_KEY_PSP, psp);
 
 	skb_flow_dissector_init(dissector, keys, cnt);
 }
@@ -3651,6 +3693,28 @@ static int fl_dump_key(struct sk_buff *skb, struct net *net,
 			    sizeof(key->ipsec.spi)))
 		goto nla_put_failure;
 
+	if (mask->psp.present &&
+	    fl_dump_key_val(skb, &key->psp.spi, TCA_FLOWER_KEY_PSP_SPI,
+			    &mask->psp.spi, TCA_FLOWER_KEY_PSP_SPI_MASK,
+			    sizeof(key->psp.spi)))
+		goto nla_put_failure;
+	/* With a 0 SPI mask and no version or device, the SPI still carries
+	 * the match on a decrypted packet: dump it with its mask
+	 */
+	if (mask->psp.present && !mask->psp.spi && !mask->psp.version &&
+	    !mask->psp.dev_id &&
+	    (nla_put(skb, TCA_FLOWER_KEY_PSP_SPI, sizeof(key->psp.spi),
+		     &key->psp.spi) ||
+	     nla_put(skb, TCA_FLOWER_KEY_PSP_SPI_MASK, sizeof(mask->psp.spi),
+		     &mask->psp.spi)))
+		goto nla_put_failure;
+	if (mask->psp.version &&
+	    nla_put_u8(skb, TCA_FLOWER_KEY_PSP_VERSION, key->psp.version))
+		goto nla_put_failure;
+	if (mask->psp.dev_id &&
+	    nla_put_u32(skb, TCA_FLOWER_KEY_PSP_DEV, key->psp.dev_id))
+		goto nla_put_failure;
+
 	if ((key->basic.ip_proto == IPPROTO_TCP ||
 	     key->basic.ip_proto == IPPROTO_UDP ||
 	     key->basic.ip_proto == IPPROTO_SCTP) &&
-- 
2.53.0-Meta


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH net-next v1 11/15] selftests: drv-net: psp: test socket-less associations and the tc psp action
  2026-11-11  0:55 [PATCH net-next v1 00/15] psp: introduce socket-less associations Todd Freed
                   ` (9 preceding siblings ...)
  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
  2026-11-11  0:55 ` [PATCH net-next v1 12/15] selftests: drv-net: psp: test that software encap on ingress drops Todd Freed
                   ` (3 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Todd Freed @ 2026-11-11  0:55 UTC (permalink / raw)
  To: netdev, toddfreed
  Cc: David S. Miller, Andrew Lunn, Björn Töpel, bpf,
	Daniel Borkmann, Daniel Zahka, Donald Hunter, Eric Dumazet,
	Jakub Kicinski, Jamal Hadi Salim, Jiri Pirko, John Fastabend,
	linux-doc, linux-kernel, linux-kselftest, linux-rdma,
	Martin KaFai Lau, Paolo Abeni, Saeed Mahameed, Simon Horman,
	Tariq Toukan, Wei Wang, Willem de Bruijn

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


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH net-next v1 12/15] selftests: drv-net: psp: test that software encap on ingress drops
  2026-11-11  0:55 [PATCH net-next v1 00/15] psp: introduce socket-less associations Todd Freed
                   ` (10 preceding siblings ...)
  2026-11-11  0:55 ` [PATCH net-next v1 11/15] selftests: drv-net: psp: test socket-less associations and the tc psp action Todd Freed
@ 2026-11-11  0:55 ` 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
                   ` (2 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Todd Freed @ 2026-11-11  0:55 UTC (permalink / raw)
  To: netdev, toddfreed
  Cc: David S. Miller, Andrew Lunn, Björn Töpel, bpf,
	Daniel Borkmann, Daniel Zahka, Donald Hunter, Eric Dumazet,
	Jakub Kicinski, Jamal Hadi Salim, Jiri Pirko, John Fastabend,
	linux-doc, linux-kernel, linux-kselftest, linux-rdma,
	Martin KaFai Lau, Paolo Abeni, Saeed Mahameed, Simon Horman,
	Tariq Toukan, Wei Wang, Willem de Bruijn

encap on ingress is hardware offload only. In software, the packet is
dropped.

Verify it on traffic of a client behind a veth, with encap on the veth's
ingress: forwarded, the packets must not reach the remote in clear text,
and to a local address, they must not reach the local socket.

  data_tc_encap_routed  routed packets are dropped by encap, IPv4 and
                        IPv6
  data_tc_encap_local   so are packets to a local address

Signed-off-by: Todd Freed <toddfreed@meta.com>
---
 tools/testing/selftests/drivers/net/config |   2 +
 tools/testing/selftests/drivers/net/psp.py | 159 ++++++++++++++++++++-
 2 files changed, 159 insertions(+), 2 deletions(-)

diff --git a/tools/testing/selftests/drivers/net/config b/tools/testing/selftests/drivers/net/config
index baf1c9d0dd13..b39de6e64263 100644
--- a/tools/testing/selftests/drivers/net/config
+++ b/tools/testing/selftests/drivers/net/config
@@ -5,6 +5,7 @@ CONFIG_DUMMY=m
 CONFIG_INET_PSP=y
 CONFIG_IPV6=y
 CONFIG_MACSEC=m
+CONFIG_NET_ACT_GACT=m
 CONFIG_NET_ACT_MIRRED=m
 CONFIG_NET_ACT_PSP=m
 CONFIG_NET_ACT_SKBEDIT=m
@@ -25,5 +26,6 @@ CONFIG_NET_SCH_PRIO=m
 CONFIG_PPP=y
 CONFIG_PPPOE=y
 CONFIG_TLS=y
+CONFIG_VETH=m
 CONFIG_VLAN_8021Q=m
 CONFIG_XDP_SOCKETS=y
diff --git a/tools/testing/selftests/drivers/net/psp.py b/tools/testing/selftests/drivers/net/psp.py
index fb7544410051..f7613fcd2fbf 100755
--- a/tools/testing/selftests/drivers/net/psp.py
+++ b/tools/testing/selftests/drivers/net/psp.py
@@ -22,7 +22,7 @@ from lib.py import ksft_variants, KsftNamedVariant
 from lib.py import KsftSkipEx, KsftFailEx
 from lib.py import NetDrvEpEnv, NetDrvContEnv
 from lib.py import Netlink, NlError, PSPFamily, RtnlFamily
-from lib.py import NetNSEnter
+from lib.py import NetNS, NetNSEnter
 from lib.py import bkg, rand_port, wait_port_listen
 from lib.py import cmd, ip, tc
 
@@ -1242,6 +1242,160 @@ def data_tc_encap_ingress(cfg):
             del_flt.exec()
 
 
+def _tc_act_stats(dev, pref, host=None):
+    """ Stats of the first action of the ingress filter @pref of @dev """
+    filters = tc(f"-s filter show dev {dev} ingress pref {pref}",
+                 json=True, host=host)
+    for flt in filters:
+        for act in flt.get("options", {}).get("actions", []):
+            return act["stats"]
+    raise KsftFailEx(f"no action on the {dev} pref {pref} filter")
+
+
+def _tc_act_pkts(dev, pref, host=None):
+    """ Packets of the first action of the ingress filter @pref of @dev """
+    return _tc_act_stats(dev, pref, host)["packets"]
+
+
+def _fwd_client(cfg, ipver):
+    """
+    Set up a client in a netns behind veth psp-fwd0, and forward its
+    traffic: only from psp-fwd0, the host's own forwarding settings stay as
+    they are. Return a UDP socket of the client.
+    """
+    ns = NetNS()
+    defer(ns.__del__)
+    ip(f"link add psp-fwd0 type veth peer name psp-fwd1 netns {ns.name}")
+    defer(ip, "link del psp-fwd0")
+    if ipver == "4":
+        fwd = "/proc/sys/net/ipv4/conf/psp-fwd0/forwarding"
+        local, client, plen = "198.51.100.1", "198.51.100.2", 24
+    else:
+        fwd = "/proc/sys/net/ipv6/conf/psp-fwd0/force_forwarding"
+        local, client, plen = "2001:db8:fe::1", "2001:db8:fe::2", 64
+    if not os.path.exists(fwd):
+        raise KsftSkipEx(f"{fwd} not supported")
+    # Goes away with the device
+    with open(fwd, "w", encoding="utf-8") as fp:
+        fp.write("1")
+    # IPv6 may be disabled on new devices
+    if cmd(f"ip addr add {local}/{plen} dev psp-fwd0 nodad",
+           fail=False).ret:
+        raise KsftSkipEx(f"can't add {local} to a new device")
+    ip("link set psp-fwd0 up")
+    ip(f"-n {ns.name} addr add {client}/{plen} dev psp-fwd1 nodad")
+    ip(f"-n {ns.name} link set psp-fwd1 up")
+    ip(f"-n {ns.name} route add default via {local}")
+
+    family = socket.AF_INET if ipver == "4" else socket.AF_INET6
+    with NetNSEnter(ns.name):
+        s = socket.socket(family, socket.SOCK_DGRAM)
+    defer(s.close)
+    return s
+
+
+def _remote_filter(cfg, pref, proto, match):
+    """
+    Add a flower filter on the remote's ingress, to count what gets there.
+    In software only: drivers refresh the counters of offloaded filters
+    periodically, and these counts must be exact.
+    """
+    _tc_clsact(cfg.remote_ifname, host=cfg.remote)
+    tc(f"filter add dev {cfg.remote_ifname} ingress pref {pref} handle 1 "
+       f"protocol {proto} flower skip_hw {match}", host=cfg.remote)
+    defer(tc, f"filter del dev {cfg.remote_ifname} ingress pref {pref} "
+          f"handle 1 protocol {proto} flower", host=cfg.remote)
+
+
+def _tc_encap_ingress(proto, port, tx):
+    """ psp encap on psp-fwd0's ingress, which software drops """
+    _tc_clsact("psp-fwd0")
+    tc(f"filter add dev psp-fwd0 ingress pref 10 protocol {proto} "
+       f"flower ip_proto udp dst_port {port} "
+       f"action psp encap assoc {tx['id']}")
+
+
+@ksft_variants(_get_ip_variants())
+def data_tc_encap_routed(cfg, ipver):
+    """ Test software encap on ingress drops routed packets """
+    cfg.require_ipver(ipver)
+    _init_psp_dev(cfg)
+    _require_tc_psp(cfg)
+
+    proto = _tc_proto(ipver)
+    _, tx = _sockless_assocs(cfg)
+    s = _fwd_client(cfg, ipver)
+
+    port = rand_port(socket.SOCK_DGRAM)
+    _remote_filter(cfg, 10, proto,
+                   f"ip_proto udp dst_port {port} action drop")
+
+    def remote_pkts():
+        return _tc_act_pkts(cfg.remote_ifname, 10, host=cfg.remote)
+
+    def send(n):
+        for _ in range(n):
+            s.sendto(b'0123456789', (cfg.remote_addr_v[ipver], port))
+            time.sleep(0.01)
+        time.sleep(0.2)
+
+    # Without encap the packets are forwarded to the remote, unless
+    # something else, e.g. a FORWARD policy, stops them
+    send(10)
+    sent = remote_pkts()
+    if not sent:
+        raise KsftSkipEx("forwarded packets don't reach the remote")
+
+    # Encap on ingress is hardware offload only: in software the action
+    # drops the packets. They must not reach the remote in clear text.
+    _tc_encap_ingress(proto, port, tx)
+    send(10)
+    ksft_eq(_tc_act_pkts("psp-fwd0", 10), 10)
+    ksft_eq(_tc_act_stats("psp-fwd0", 10)["drops"], 10,
+            comment="dropped by encap")
+    ksft_eq(remote_pkts(), sent)
+
+
+@ksft_variants(_get_ip_variants())
+def data_tc_encap_local(cfg, ipver):
+    """ Test software encap on ingress drops packets to a local address """
+    cfg.require_ipver(ipver)
+    _init_psp_dev(cfg)
+    _require_tc_psp(cfg)
+
+    _, tx = _sockless_assocs(cfg)
+    s = _fwd_client(cfg, ipver)
+    fam = "inet" if ipver == "4" else "inet6"
+    addrs = ip("addr show dev psp-fwd0", json=True)[0]["addr_info"]
+    local = [a["local"] for a in addrs
+             if a["family"] == fam and a["scope"] == "global"][0]
+
+    family = socket.AF_INET if ipver == "4" else socket.AF_INET6
+    with socket.socket(family, socket.SOCK_DGRAM) as srv:
+        srv.bind((local, 0))
+        srv.settimeout(0.5)
+        port = srv.getsockname()[1]
+
+        # Without encap the packets are delivered, unless something
+        # else, e.g. an INPUT policy, stops them
+        s.sendto(b'clear', (local, port))
+        try:
+            got = srv.recv(64)
+        except socket.timeout as e:
+            raise KsftSkipEx("the client's packets aren't delivered") from e
+        ksft_eq(got, b'clear')
+
+        # Encap on ingress is hardware offload only: in software the
+        # action drops the packets, rather than let them be received
+        _tc_encap_ingress(_tc_proto(ipver), port, tx)
+        s.sendto(b'encap', (local, port))
+        with ksft_raises(socket.timeout):
+            srv.recv(64)
+        ksft_eq(_tc_act_pkts("psp-fwd0", 10), 1)
+        ksft_eq(_tc_act_stats("psp-fwd0", 10)["drops"], 1,
+                comment="dropped by encap")
+
+
 def removal_device_tc(cfg):
     """ Test removing a PSD with tc actions holding its assocs """
     cfg.require_nsim()
@@ -1760,7 +1914,8 @@ def main() -> None:
                                                           cfg.comm_port),
                                                          timeout=1)
 
-                cases = [data_basic_send, data_mss_adjust, data_tc_basic_send]
+                cases = [data_basic_send, data_mss_adjust, data_tc_basic_send,
+                         data_tc_encap_routed, data_tc_encap_local]
 
                 if has_cont:
                     cases += [
-- 
2.53.0-Meta


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH net-next v1 13/15] selftests: tc-testing: run dependsOn probes outside the test namespace
  2026-11-11  0:55 [PATCH net-next v1 00/15] psp: introduce socket-less associations Todd Freed
                   ` (11 preceding siblings ...)
  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 ` 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
  14 siblings, 0 replies; 16+ messages in thread
From: Todd Freed @ 2026-11-11  0:55 UTC (permalink / raw)
  To: netdev, toddfreed
  Cc: David S. Miller, Andrew Lunn, Björn Töpel, bpf,
	Daniel Borkmann, Daniel Zahka, Donald Hunter, Eric Dumazet,
	Jakub Kicinski, Jamal Hadi Salim, Jiri Pirko, John Fastabend,
	linux-doc, linux-kernel, linux-kselftest, linux-rdma,
	Martin KaFai Lau, Paolo Abeni, Saeed Mahameed, Simon Horman,
	Tariq Toukan, Wei Wang, Willem de Bruijn

a test's dependsOn probe runs as an execute stage command, but is run
before the test's namespace is created. nsPlugin prefixes the command
with "ip netns exec tcut", which causes the command to fail. this makes
every test with a probe that requires nsPlugin skipped currently. for
example the mirred blockid tests and the tunnel_key nofrag test.

probes are meant to check what tc and the kernel supports, not anything
in the test's namespace. run them as their own stage ("probe"), which
makes them run in the base namespace.

Signed-off-by: Todd Freed <toddfreed@meta.com>
---
 tools/testing/selftests/tc-testing/tdc.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tools/testing/selftests/tc-testing/tdc.py b/tools/testing/selftests/tc-testing/tdc.py
index 511d66c36a2a..541caf984519 100755
--- a/tools/testing/selftests/tc-testing/tdc.py
+++ b/tools/testing/selftests/tc-testing/tdc.py
@@ -399,7 +399,7 @@ def run_one_test(pm, args, index, tidx):
     if 'dependsOn' in tidx:
         if (args.verbose > 0):
             print('probe command for test skip')
-        (p, procout) = exec_cmd(tidx, args, pm, 'execute', tidx['dependsOn'])
+        (p, procout) = exec_cmd(tidx, args, pm, 'probe', tidx['dependsOn'])
         if p:
             if (p.returncode != 0):
                 res = TestResult(tidx['id'], tidx['name'])
-- 
2.53.0-Meta


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH net-next v1 14/15] selftests: tc-testing: add tests for the psp action
  2026-11-11  0:55 [PATCH net-next v1 00/15] psp: introduce socket-less associations Todd Freed
                   ` (12 preceding siblings ...)
  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 ` Todd Freed
  2026-11-11  0:55 ` [PATCH net-next v1 15/15] Documentation: networking: psp: document socket-less associations Todd Freed
  14 siblings, 0 replies; 16+ messages in thread
From: Todd Freed @ 2026-11-11  0:55 UTC (permalink / raw)
  To: netdev, toddfreed
  Cc: David S. Miller, Andrew Lunn, Björn Töpel, bpf,
	Daniel Borkmann, Daniel Zahka, Donald Hunter, Eric Dumazet,
	Jakub Kicinski, Jamal Hadi Salim, Jiri Pirko, John Fastabend,
	linux-doc, linux-kernel, linux-kselftest, linux-rdma,
	Martin KaFai Lau, Paolo Abeni, Saeed Mahameed, Simon Horman,
	Tariq Toukan, Wei Wang, Willem de Bruijn

Add / replace / delete / flush psp decap actions, check that encap
without an association, associations which don't exist, and decap with
an association are rejected, and add flower filters matching on
psp_spi, psp_ver and psp_dev, out of range psp_ver and psp_dev
rejected.

Creating associations takes the PSP netlink family and a PSP device,
the drv-net psp.py test covers actions using them.

The tests need a tc with the psp action and the flower psp_* keys; they
skip without it (dependsOn).

Signed-off-by: Todd Freed <toddfreed@meta.com>
---
 tools/testing/selftests/tc-testing/config     |   2 +
 .../tc-testing/tc-tests/actions/psp.json      | 459 ++++++++++++++++++
 tools/testing/selftests/tc-testing/tdc.sh     |   1 +
 3 files changed, 462 insertions(+)
 create mode 100644 tools/testing/selftests/tc-testing/tc-tests/actions/psp.json

diff --git a/tools/testing/selftests/tc-testing/config b/tools/testing/selftests/tc-testing/config
index 0e5618be0335..7828161cc3c8 100644
--- a/tools/testing/selftests/tc-testing/config
+++ b/tools/testing/selftests/tc-testing/config
@@ -101,6 +101,8 @@ CONFIG_NET_ACT_TUNNEL_KEY=m
 CONFIG_NET_ACT_CT=m
 CONFIG_NET_ACT_MPLS=m
 CONFIG_NET_ACT_GATE=m
+CONFIG_INET_PSP=y
+CONFIG_NET_ACT_PSP=m
 CONFIG_NET_IFE_SKBMARK=m
 CONFIG_NET_IFE_SKBPRIO=m
 CONFIG_NET_IFE_SKBTCINDEX=m
diff --git a/tools/testing/selftests/tc-testing/tc-tests/actions/psp.json b/tools/testing/selftests/tc-testing/tc-tests/actions/psp.json
new file mode 100644
index 000000000000..990e8bd1ce9a
--- /dev/null
+++ b/tools/testing/selftests/tc-testing/tc-tests/actions/psp.json
@@ -0,0 +1,459 @@
+[
+    {
+        "id": "8357",
+        "name": "Add psp decap action",
+        "category": [
+            "actions",
+            "psp"
+        ],
+        "dependsOn": "$TC actions add action psp help 2>&1 | grep -q decap",
+        "plugins": {
+            "requires": "nsPlugin"
+        },
+        "setup": [
+            [
+                "$TC actions flush action psp",
+                0,
+                1,
+                255
+            ]
+        ],
+        "cmdUnderTest": "$TC actions add action psp decap index 1",
+        "expExitCode": "0",
+        "verifyCmd": "$TC actions get action psp index 1",
+        "matchPattern": "action order [0-9]+: psp decap pipe.*index 1 ref",
+        "matchCount": "1",
+        "teardown": [
+            "$TC actions flush action psp"
+        ]
+    },
+    {
+        "id": "7676",
+        "name": "Add psp decap action with drop control",
+        "category": [
+            "actions",
+            "psp"
+        ],
+        "dependsOn": "$TC actions add action psp help 2>&1 | grep -q decap",
+        "plugins": {
+            "requires": "nsPlugin"
+        },
+        "setup": [
+            [
+                "$TC actions flush action psp",
+                0,
+                1,
+                255
+            ]
+        ],
+        "cmdUnderTest": "$TC actions add action psp decap drop index 2",
+        "expExitCode": "0",
+        "verifyCmd": "$TC actions get action psp index 2",
+        "matchPattern": "action order [0-9]+: psp decap drop.*index 2 ref",
+        "matchCount": "1",
+        "teardown": [
+            "$TC actions flush action psp"
+        ]
+    },
+    {
+        "id": "9a26",
+        "name": "Add psp decap action without index",
+        "category": [
+            "actions",
+            "psp"
+        ],
+        "dependsOn": "$TC actions add action psp help 2>&1 | grep -q decap",
+        "plugins": {
+            "requires": "nsPlugin"
+        },
+        "setup": [
+            [
+                "$TC actions flush action psp",
+                0,
+                1,
+                255
+            ]
+        ],
+        "cmdUnderTest": "$TC actions add action psp decap",
+        "expExitCode": "0",
+        "verifyCmd": "$TC actions list action psp",
+        "matchPattern": "action order [0-9]+: psp decap pipe",
+        "matchCount": "1",
+        "teardown": [
+            "$TC actions flush action psp"
+        ]
+    },
+    {
+        "id": "156e",
+        "name": "Replace psp decap action",
+        "category": [
+            "actions",
+            "psp"
+        ],
+        "dependsOn": "$TC actions add action psp help 2>&1 | grep -q decap",
+        "plugins": {
+            "requires": "nsPlugin"
+        },
+        "setup": [
+            [
+                "$TC actions flush action psp",
+                0,
+                1,
+                255
+            ],
+            "$TC actions add action psp decap index 3"
+        ],
+        "cmdUnderTest": "$TC actions replace action psp decap pass index 3",
+        "expExitCode": "0",
+        "verifyCmd": "$TC actions get action psp index 3",
+        "matchPattern": "action order [0-9]+: psp decap pass.*index 3 ref",
+        "matchCount": "1",
+        "teardown": [
+            "$TC actions flush action psp"
+        ]
+    },
+    {
+        "id": "006b",
+        "name": "Add psp action with an existing index",
+        "category": [
+            "actions",
+            "psp"
+        ],
+        "dependsOn": "$TC actions add action psp help 2>&1 | grep -q decap",
+        "plugins": {
+            "requires": "nsPlugin"
+        },
+        "setup": [
+            [
+                "$TC actions flush action psp",
+                0,
+                1,
+                255
+            ],
+            "$TC actions add action psp decap index 4"
+        ],
+        "cmdUnderTest": "$TC actions add action psp decap pass index 4",
+        "expExitCode": "255",
+        "verifyCmd": "$TC actions get action psp index 4",
+        "matchPattern": "action order [0-9]+: psp decap pipe.*index 4 ref",
+        "matchCount": "1",
+        "teardown": [
+            "$TC actions flush action psp"
+        ]
+    },
+    {
+        "id": "3f41",
+        "name": "Delete psp action",
+        "category": [
+            "actions",
+            "psp"
+        ],
+        "dependsOn": "$TC actions add action psp help 2>&1 | grep -q decap",
+        "plugins": {
+            "requires": "nsPlugin"
+        },
+        "setup": [
+            [
+                "$TC actions flush action psp",
+                0,
+                1,
+                255
+            ],
+            "$TC actions add action psp decap index 5"
+        ],
+        "cmdUnderTest": "$TC actions del action psp index 5",
+        "expExitCode": "0",
+        "verifyCmd": "$TC actions list action psp",
+        "matchPattern": "action order [0-9]+: psp",
+        "matchCount": "0",
+        "teardown": [
+            "$TC actions flush action psp"
+        ]
+    },
+    {
+        "id": "2b21",
+        "name": "Flush psp actions",
+        "category": [
+            "actions",
+            "psp"
+        ],
+        "dependsOn": "$TC actions add action psp help 2>&1 | grep -q decap",
+        "plugins": {
+            "requires": "nsPlugin"
+        },
+        "setup": [
+            [
+                "$TC actions flush action psp",
+                0,
+                1,
+                255
+            ],
+            "$TC actions add action psp decap index 6",
+            "$TC actions add action psp decap drop index 7"
+        ],
+        "cmdUnderTest": "$TC actions flush action psp",
+        "expExitCode": "0",
+        "verifyCmd": "$TC actions list action psp",
+        "matchPattern": "action order [0-9]+: psp",
+        "matchCount": "0",
+        "teardown": [
+            "$TC actions flush action psp"
+        ]
+    },
+    {
+        "id": "2d50",
+        "name": "Add psp action without encap or decap",
+        "category": [
+            "actions",
+            "psp"
+        ],
+        "dependsOn": "$TC actions add action psp help 2>&1 | grep -q decap",
+        "plugins": {
+            "requires": "nsPlugin"
+        },
+        "setup": [
+            [
+                "$TC actions flush action psp",
+                0,
+                1,
+                255
+            ]
+        ],
+        "cmdUnderTest": "$TC actions add action psp index 12",
+        "expExitCode": "255",
+        "verifyCmd": "$TC actions list action psp",
+        "matchPattern": "action order [0-9]+: psp",
+        "matchCount": "0",
+        "teardown": [
+            "$TC actions flush action psp"
+        ]
+    },
+    {
+        "id": "cab5",
+        "name": "Add psp encap action without assoc, with index",
+        "category": [
+            "actions",
+            "psp"
+        ],
+        "dependsOn": "$TC actions add action psp help 2>&1 | grep -q decap",
+        "plugins": {
+            "requires": "nsPlugin"
+        },
+        "setup": [
+            [
+                "$TC actions flush action psp",
+                0,
+                1,
+                255
+            ]
+        ],
+        "cmdUnderTest": "$TC actions add action psp encap index 8",
+        "expExitCode": "255",
+        "verifyCmd": "$TC actions list action psp",
+        "matchPattern": "action order [0-9]+: psp",
+        "matchCount": "0",
+        "teardown": [
+            "$TC actions flush action psp"
+        ]
+    },
+    {
+        "id": "3203",
+        "name": "Add psp encap action with an unknown assoc",
+        "category": [
+            "actions",
+            "psp"
+        ],
+        "dependsOn": "$TC actions add action psp help 2>&1 | grep -q decap",
+        "plugins": {
+            "requires": "nsPlugin"
+        },
+        "setup": [
+            [
+                "$TC actions flush action psp",
+                0,
+                1,
+                255
+            ]
+        ],
+        "cmdUnderTest": "$TC actions add action psp encap assoc 4294967295 index 9",
+        "expExitCode": "255",
+        "verifyCmd": "$TC actions list action psp",
+        "matchPattern": "action order [0-9]+: psp",
+        "matchCount": "0",
+        "teardown": [
+            "$TC actions flush action psp"
+        ]
+    },
+    {
+        "id": "0c17",
+        "name": "Reject psp decap action with an assoc",
+        "category": [
+            "actions",
+            "psp"
+        ],
+        "dependsOn": "$TC actions add action psp help 2>&1 | grep -q decap",
+        "plugins": {
+            "requires": "nsPlugin"
+        },
+        "setup": [
+            [
+                "$TC actions flush action psp",
+                0,
+                1,
+                255
+            ]
+        ],
+        "cmdUnderTest": "$TC actions add action psp decap assoc 1 index 10",
+        "expExitCode": "255",
+        "verifyCmd": "$TC actions list action psp",
+        "matchPattern": "action order [0-9]+: psp",
+        "matchCount": "0",
+        "teardown": [
+            "$TC actions flush action psp"
+        ]
+    },
+    {
+        "id": "a744",
+        "name": "Add flower psp_spi filter with psp decap",
+        "category": [
+            "actions",
+            "psp",
+            "filter"
+        ],
+        "dependsOn": "$TC filter add flower help 2>&1 | grep -q psp_spi",
+        "plugins": {
+            "requires": "nsPlugin"
+        },
+        "setup": [
+            "$TC qdisc add dev $DEV1 ingress"
+        ],
+        "cmdUnderTest": "$TC filter add dev $DEV1 ingress protocol ip pref 1 flower psp_spi 0x1234 action psp decap",
+        "expExitCode": "0",
+        "verifyCmd": "$TC filter show dev $DEV1 ingress",
+        "matchPattern": "psp_spi 0x1234.*psp decap pipe",
+        "matchCount": "1",
+        "teardown": [
+            "$TC qdisc del dev $DEV1 ingress"
+        ]
+    },
+    {
+        "id": "be4e",
+        "name": "Add flower psp_spi filter with mask",
+        "category": [
+            "actions",
+            "psp",
+            "filter"
+        ],
+        "dependsOn": "$TC filter add flower help 2>&1 | grep -q psp_spi",
+        "plugins": {
+            "requires": "nsPlugin"
+        },
+        "setup": [
+            "$TC qdisc add dev $DEV1 ingress"
+        ],
+        "cmdUnderTest": "$TC filter add dev $DEV1 ingress protocol ipv6 pref 1 flower psp_spi 0x80001234/0x8000ffff action drop",
+        "expExitCode": "0",
+        "verifyCmd": "$TC filter show dev $DEV1 ingress",
+        "matchPattern": "psp_spi 0x80001234/0x8000ffff",
+        "matchCount": "1",
+        "teardown": [
+            "$TC qdisc del dev $DEV1 ingress"
+        ]
+    },
+    {
+        "id": "4e1f",
+        "name": "Add flower psp_spi, psp_ver and psp_dev filter with psp decap",
+        "category": [
+            "actions",
+            "psp",
+            "filter"
+        ],
+        "dependsOn": "$TC filter add flower help 2>&1 | grep -q psp_spi",
+        "plugins": {
+            "requires": "nsPlugin"
+        },
+        "setup": [
+            "$TC qdisc add dev $DEV1 ingress"
+        ],
+        "cmdUnderTest": "$TC filter add dev $DEV1 ingress protocol ip pref 1 flower psp_spi 0x1234 psp_ver 1 psp_dev 7 action psp decap",
+        "expExitCode": "0",
+        "verifyCmd": "$TC filter show dev $DEV1 ingress",
+        "matchPattern": "psp_spi 0x1234.*psp_ver 1.*psp_dev 7.*psp decap pipe",
+        "matchCount": "1",
+        "teardown": [
+            "$TC qdisc del dev $DEV1 ingress"
+        ]
+    },
+    {
+        "id": "7c3a",
+        "name": "Add flower psp_ver 0 filter",
+        "category": [
+            "actions",
+            "psp",
+            "filter"
+        ],
+        "dependsOn": "$TC filter add flower help 2>&1 | grep -q psp_spi",
+        "plugins": {
+            "requires": "nsPlugin"
+        },
+        "setup": [
+            "$TC qdisc add dev $DEV1 ingress"
+        ],
+        "cmdUnderTest": "$TC filter add dev $DEV1 ingress protocol ipv6 pref 1 flower psp_ver 0 action drop",
+        "expExitCode": "0",
+        "verifyCmd": "$TC filter show dev $DEV1 ingress",
+        "matchPattern": "psp_ver 0",
+        "matchCount": "1",
+        "teardown": [
+            "$TC qdisc del dev $DEV1 ingress"
+        ]
+    },
+    {
+        "id": "b58d",
+        "name": "Add flower psp_ver filter with an invalid version",
+        "category": [
+            "actions",
+            "psp",
+            "filter"
+        ],
+        "dependsOn": "$TC filter add flower help 2>&1 | grep -q psp_spi",
+        "plugins": {
+            "requires": "nsPlugin"
+        },
+        "setup": [
+            "$TC qdisc add dev $DEV1 ingress"
+        ],
+        "cmdUnderTest": "$TC filter add dev $DEV1 ingress protocol ip pref 1 flower psp_ver 16 action drop",
+        "expExitCode": "2",
+        "verifyCmd": "$TC filter show dev $DEV1 ingress",
+        "matchPattern": "psp_ver",
+        "matchCount": "0",
+        "teardown": [
+            "$TC qdisc del dev $DEV1 ingress"
+        ]
+    },
+    {
+        "id": "2f96",
+        "name": "Add flower psp_dev filter with an invalid device",
+        "category": [
+            "actions",
+            "psp",
+            "filter"
+        ],
+        "dependsOn": "$TC filter add flower help 2>&1 | grep -q psp_spi",
+        "plugins": {
+            "requires": "nsPlugin"
+        },
+        "setup": [
+            "$TC qdisc add dev $DEV1 ingress"
+        ],
+        "cmdUnderTest": "$TC filter add dev $DEV1 ingress protocol ip pref 1 flower psp_dev 65536 action drop",
+        "expExitCode": "2",
+        "verifyCmd": "$TC filter show dev $DEV1 ingress",
+        "matchPattern": "psp_dev",
+        "matchCount": "0",
+        "teardown": [
+            "$TC qdisc del dev $DEV1 ingress"
+        ]
+    }
+]
diff --git a/tools/testing/selftests/tc-testing/tdc.sh b/tools/testing/selftests/tc-testing/tdc.sh
index 25cdc256287d..433af92364ac 100755
--- a/tools/testing/selftests/tc-testing/tdc.sh
+++ b/tools/testing/selftests/tc-testing/tdc.sh
@@ -24,6 +24,7 @@ try_modprobe act_mpls
 try_modprobe act_nat
 try_modprobe act_pedit
 try_modprobe act_police
+try_modprobe act_psp
 try_modprobe act_sample
 try_modprobe act_simple
 try_modprobe act_skbedit
-- 
2.53.0-Meta


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH net-next v1 15/15] Documentation: networking: psp: document socket-less associations
  2026-11-11  0:55 [PATCH net-next v1 00/15] psp: introduce socket-less associations Todd Freed
                   ` (13 preceding siblings ...)
  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 ` Todd Freed
  14 siblings, 0 replies; 16+ messages in thread
From: Todd Freed @ 2026-11-11  0:55 UTC (permalink / raw)
  To: netdev, toddfreed
  Cc: David S. Miller, Andrew Lunn, Björn Töpel, bpf,
	Daniel Borkmann, Daniel Zahka, Donald Hunter, Eric Dumazet,
	Jakub Kicinski, Jamal Hadi Salim, Jiri Pirko, John Fastabend,
	linux-doc, linux-kernel, linux-kselftest, linux-rdma,
	Martin KaFai Lau, Paolo Abeni, Saeed Mahameed, Simon Horman,
	Tariq Toukan, Wei Wang, Willem de Bruijn

Describe PSP associations, and how they differ for the socket and tc use
cases.

Document the new netlink commands for manging socket-less associations.

Document the psp tc action, and flower psp_spi, psp_ver, and psp_dev
matches.

Add a new section to describe pre-existing PSP device stats.

Signed-off-by: Todd Freed <toddfreed@meta.com>
---
 Documentation/networking/psp.rst | 139 +++++++++++++++++++++++++++----
 1 file changed, 123 insertions(+), 16 deletions(-)

diff --git a/Documentation/networking/psp.rst b/Documentation/networking/psp.rst
index 0f9b6b73f244..79b101db5dfc 100644
--- a/Documentation/networking/psp.rst
+++ b/Documentation/networking/psp.rst
@@ -69,20 +69,42 @@ The PSP netlink family defines operations to retrieve information
 about the PSP devices available on the system, configure them and
 access PSP related statistics.
 
-Securing a connection
----------------------
-
-PSP encryption is currently only supported for TCP connections.
-Rx and Tx keys are allocated separately. First the ``rx-assoc``
-Netlink command needs to be issued, specifying a target TCP socket.
-Kernel will allocate a new PSP Rx key from the NIC and associate it
-with given socket. At this stage socket will accept both PSP-secured
-and plain text TCP packets.
+PSP Associations
+----------------
 
-Tx keys are installed using the ``tx-assoc`` Netlink command.
-Once the Tx keys are installed, all data read from the socket will
-be PSP-secured. In other words act of installing Tx keys has a secondary
-effect on the Rx direction.
+PSP encryption and decryption happen in the context of an association.
+An association holds the PSP device, PSP version, and either Rx state
+for decrypting and authenticating packets (the SPI, and for sockets the
+Rx key), a Tx key for encrypting packets, or both.
+
+Associations are used in two ways. First, a TCP socket can be bound to
+an association, and all packets flowing over that socket inherit its
+association. Second, socket-less associations, either Rx or Tx, are
+used with tc. This allows PSP for forwarded traffic, or for non-TCP
+traffic. A tc action attaches a Tx association to packets. An Rx
+association reserves an SPI: the device decrypts packets sent to it,
+and a tc filter matches the SPI, version and PSP device a packet was
+decrypted with.
+
+An association is of one type or the other; that is, sockets may not
+use socket-less associations, and tc actions do not address associations
+which are attached to a socket. Device statistics count both.
+
+Securing a socket connection
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+PSP encryption can be enabled for TCP sockets. Other kinds of sockets
+are not supported. For a TCP socket, Rx and Tx keys are allocated
+separately. First the ``rx-assoc`` Netlink command needs to be issued,
+specifying a target TCP socket. Kernel will allocate a new PSP Rx key
+from the NIC, create an association with it, and bind it to the given
+socket. At this stage socket will accept both PSP-secured and plain text
+TCP packets.
+
+The Tx key is installed into the socket's association with the
+``tx-assoc`` Netlink command. Once it is installed, all data read from
+the socket will be PSP-secured. In other words act of installing Tx keys
+has a secondary effect on the Rx direction.
 
 There is an intermediate period after ``tx-assoc`` successfully
 returns and before the TCP socket encounters it's first PSP
@@ -140,6 +162,55 @@ The PSP assoc state of a socket is not reset when the connection is
 torn down. ``connect()`` on a socket that has PSP assoc state will
 return ``-EINVAL``.
 
+Socket-less associations
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Associations which are not bound to a socket are managed with these
+netlink commands:
+
+- ``rx-spi-alloc`` allocates an Rx SPI and key, like ``rx-assoc`` but
+  without a socket. It also creates an Rx association, which records the
+  SPI, the version and the device key generation, and returns its ID.
+  The key should be passed to the peer out of band, and installed with
+  ``assoc-add``.
+- ``assoc-add`` adds a Tx association with the peer's SPI and key, and
+  returns its ID.
+- ``assoc-del`` deletes an association. tc actions and packets which still
+  reference it keep it alive. The device keeps decrypting the SPI of a
+  deleted Rx association until its device key is rotated out: remove the
+  decap rule to stop taking its packets.
+- ``assoc-get`` reports associations, never their keys, and for Rx
+  associations whether key rotation has made them stale.
+
+The ``psp`` tc action attaches these associations to packets:
+
+- ``psp encap assoc ID`` attaches a Tx association. The packet must then
+  be transmitted on the main netdev of the association's PSP device, which
+  encrypts it (``mirred egress redirect``). If the association is removed
+  before transmit, or if the packet is transmitted on any other device,
+  it will be dropped, rather than sent in the clear. Encap on ingress is
+  hardware offload only.
+
+- ``psp decap`` removes the PSP Rx state (the ``SKB_EXT_PSP`` extension and
+  ``skb->decrypted``) of a decrypted packet, so that it can be forwarded. It
+  takes no association. Its filter should match ``psp_spi``, ``psp_ver``
+  and ``psp_dev``.
+
+The flower classifier matches what a packet was decrypted with:
+``psp_spi SPI[/MASK]``, ``psp_ver VERSION`` and ``psp_dev DEV_ID``, the
+ID of the PSP device which decrypted it. These never match a clear text
+packet, so ``psp_spi 0/0`` matches any packet decrypted with PSP.
+Together they tie a decap rule to one Rx association.
+
+Once the device key an SPI was created from has been rotated twice, that
+association is considered stale. Unlike sockets, tc does not handle key
+rotation. userspace should replace such rules before two rotations.
+
+The device adds 40 bytes to each packet on transmit: the UDP and PSP
+headers, and the ICV. The operator must size the MTU of the traffic
+source for this. A packet which would not fit the PSP device's MTU once
+encapsulated is dropped on transmit.
+
 Rotation notifications
 ----------------------
 
@@ -149,6 +220,38 @@ The PSP netlink family will generate a notification whenever keys
 are rotated. The applications are expected to re-establish connections
 before keys are rotated again.
 
+Fragments
+---------
+
+PSP transport mode does not support IP fragments, either for sockets or
+socket-less associations.
+
+Statistics
+----------
+
+The ``get-stats`` netlink command reports the PSP device statistics:
+kernel statistics (key rotations, stale events), and the device statistics
+defined by the PSP specification. The device statistics count what the
+device decrypts and encrypts.
+
+The tc action counts the packets it processes and the ones it drops
+(``tc -s``). Encapsulated packets dropped on transmit count in the
+device's ``tx_dropped``. Drops are reported with these drop reasons:
+
+- ``SKB_DROP_REASON_PSP_OUTPUT``: encap ran on ingress in software, or
+  failed to parse the packet, or it is an IPv4 fragment or has IPv6
+  extension headers, which transport mode doesn't support, or it is
+  already encapsulated for a tunnel, already marked decrypted, or of a
+  socket with its own encryption offload; or an encapsulated packet
+  reached a device which isn't its PSP device's netdev, or its PSP
+  device was unregistered while it was queued.
+- ``SKB_DROP_REASON_PKT_TOO_BIG``: an encapsulated packet would not fit
+  the device's MTU once the device adds the PSP encapsulation.
+
+A packet encapsulated on egress which lost its association on the way is
+dropped on transmit by ``sk_validate_xmit_skb()``, without a specific drop
+reason.
+
 Kernel implementation
 =====================
 
@@ -177,14 +280,18 @@ device key twice), to avoid potentially using a SPI+key which previous
 OS instance already had access to.
 
 Drivers must use ``psp_skb_get_assoc_rcu()`` to check if PSP Tx offload
-was requested for given skb. On Rx drivers should allocate and populate
-the ``SKB_EXT_PSP`` skb extension, and set the skb->decrypted bit to 1.
+was requested for given skb. The association comes from the skb's socket
+or, for socket-less associations, from a ``METADATA_PSP`` dst which the
+core keeps on the skb for the driver to use. On Rx drivers should
+allocate and populate the ``SKB_EXT_PSP`` skb extension, and set the
+``skb->decrypted`` bit to 1.
 
 Kernel implementation notes
 ---------------------------
 
 PSP implementation follows the TLS offload more closely than the IPsec
-offload, with per-socket state, and the use of skb->decrypted to prevent
+offload, with per-socket state (or per-association state, for
+socket-less associations), and the use of ``skb->decrypted`` to prevent
 clear text leaks.
 
 PSP device is separate from netdev, to make it possible to "delegate"
-- 
2.53.0-Meta


^ permalink raw reply	[flat|nested] 16+ messages in thread

end of thread, other threads:[~2026-11-11  0:56 UTC | newest]

Thread overview: 16+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
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 ` [PATCH net-next v1 11/15] selftests: drv-net: psp: test socket-less associations and the tc psp action Todd Freed
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

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®