* [RFC net-next 0/6] psp: use virt cookie as Rx steering hint
@ 2026-08-22 22:55 Jakub Kicinski
2026-08-22 22:55 ` [RFC net-next 1/6] psp: steer Rx queues with the virtualization cookie Jakub Kicinski
` (6 more replies)
0 siblings, 7 replies; 18+ messages in thread
From: Jakub Kicinski @ 2026-08-22 22:55 UTC (permalink / raw)
To: daniel.zahka, willemdebruijn.kernel
Cc: edumazet, cratiu, borisp, kuniyu, netdev, Jakub Kicinski
Hi!
This PoC series uses a field of the PSP header intended for tunnels
to auto-steer Rx traffic. Various attempts have been made at trying
to get Rx traffic to land close to the core where the application runs.
By default RSS picks the Rx queue based on the flow hash.
I'm not going to cover all previous solutions in detail but broadly
- we have RFS in SW which looks on which CPU Tx happens and backlogs
Rx packets there, it is quite efficient. aRFS is built on top
of RFS but tries to program flows into the NIC. Some NICs have
a "cache" and try to automatically remember the flow to queue
association.
All those solutions are entirely local to the receiver.
Ideally we would want the solution to look something like
TCP timestamp option - we send an opaque cookie to the peer,
and the peer echoes it back to us. Our NIC can steer based
on that echoed cookie.
This patch set implements exactly that using the optional PSP
Virtualization Cookie field. The PSP standard doesn't have much
to say about this field:
Virtualization Cookie - 64b
An optional field, present if and only if V is set.
It may contain a Virtual Network Identifier (VNI) or other data,
as defined by the implementation.
IOW it's a field in the header which can be used as VNI, so presumably
most PSP-capable NICs will be able to feed it into some TCAM lookup.
The main use of this field is when PSP is used for tunneling.
Nothing in the standard precludes it's use in transport mode.
This patchset splits this field as follows:
63 48 47 32 31 16 15 0
+---------------+---------------+---------------+---------------+
| reserved | req qid | reserved | dst qid |
+---------------+---------------+---------------+---------------+
"req" is the value we want sender to put in "dst" when they respond.
When the feature is enabled we expect the NIC to create low-priority
steering rules matching on "dst" (bottom 16b of the Virt Cookie).
The mapping is direct today so dst=2 means queue=2 within the receiving
interface (see doc in patch 1 for more info). Any explicit steering
rules (ethtool, TC etc) still take precedence over PSP steering,
we are only overriding the RSS queue assignment.
I'm sharing this as an RFC because I _think_ it's a good idea
(feedback most welcome). We need some vendor cooperation to get this
implemented - specifically IDK how to make mlx5 (the only PSP-capable
NIC I have access to) to do the steering :(
Jakub Kicinski (6):
psp: steer Rx queues with the virtualization cookie
netdevsim: support PSP VC based queue steering
selftests: drv-net: psp: move the PSP test plumbing into psp_lib.py
selftests: drv-net: psp_steer: test PSP VC based queue steering
selftests: drv-net: psp_steer: test where PSP steering sits in the Rx
pipeline
selftests: drv-net: psp_steer: cover corner cases and races
MAINTAINERS | 1 +
Documentation/netlink/specs/psp.yaml | 42 ++
Documentation/networking/psp.rst | 75 +++
tools/testing/selftests/drivers/net/Makefile | 5 +
include/net/psp/types.h | 93 +++-
drivers/net/netdevsim/netdevsim.h | 4 +-
include/net/psp/functions.h | 54 +-
include/uapi/linux/psp.h | 17 +
.../mellanox/mlx5/core/en_accel/psp_rxtx.c | 2 +-
drivers/net/netdevsim/netdev.c | 5 +-
drivers/net/netdevsim/psp.c | 26 +-
net/psp/psp-nl-gen.c | 5 +-
net/psp/psp_main.c | 53 +-
net/psp/psp_nl.c | 21 +-
net/psp/psp_sock.c | 35 ++
.../selftests/drivers/net/psp_responder.c | 42 ++
tools/testing/selftests/drivers/net/psp.py | 287 +++--------
.../testing/selftests/drivers/net/psp_lib.py | 184 +++++++
.../selftests/drivers/net/psp_steer.py | 473 ++++++++++++++++++
19 files changed, 1187 insertions(+), 237 deletions(-)
create mode 100644 tools/testing/selftests/drivers/net/psp_lib.py
create mode 100644 tools/testing/selftests/drivers/net/psp_steer.py
--
2.55.0
^ permalink raw reply [flat|nested] 18+ messages in thread
* [RFC net-next 1/6] psp: steer Rx queues with the virtualization cookie
2026-08-22 22:55 [RFC net-next 0/6] psp: use virt cookie as Rx steering hint Jakub Kicinski
@ 2026-08-22 22:55 ` Jakub Kicinski
2026-08-23 15:31 ` Daniel Zahka
2026-08-23 18:18 ` Willem de Bruijn
2026-08-22 22:55 ` [RFC net-next 2/6] netdevsim: support PSP VC based queue steering Jakub Kicinski
` (5 subsequent siblings)
6 siblings, 2 replies; 18+ messages in thread
From: Jakub Kicinski @ 2026-08-22 22:55 UTC (permalink / raw)
To: daniel.zahka, willemdebruijn.kernel
Cc: edumazet, cratiu, borisp, kuniyu, netdev, Jakub Kicinski
PSP leaves the 64b virtualization cookie undefined in transport mode.
Put it to use: let both ends of a connection tell each other which Rx
queue they want traffic on, so that a flow can be pinned to a queue
without the receiver having to install a per-flow steering rule, and
without the sender having to know anything about the receiver's queue
layout. The cookie holds a queue ID the sender is asking the peer to
send to ("req") and the queue ID the peer last asked for, granted
("dst"). Each ID gets a 32b word of the cookie to itself and uses only
the low half of it, so that either can grow to 32b later without the
fields moving.
The two directions are configured separately:
* rx asks peers to send to the queue paired with the flow's Tx queue,
so traffic this host receives gets steered. The receiver installs one
low priority rule per Rx queue matching "dst", which wins over RSS,
so this needs vc-steer-cap.
* tx grants the requests peers make, so traffic this host sends gets
steered at the far end. The queue is the peer's to pick and the rules
are the peer's to install, so this needs nothing from the local
device and can be turned on where vc-steer-cap is absent.
Splitting them is what makes one sided deployment work. Turn granting on
everywhere, cheaply, and asking wherever the NIC can actually do it.
The steering itself is entirely a device matter, the core only has
to move the two queue IDs around. That is why the Tx side hooks
psp_validate_xmit(): it is the one PSP-specific callback which runs
after netdev_core_pick_tx() has stamped the queue and still has the
netdev at hand.
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
Documentation/netlink/specs/psp.yaml | 42 +++++++++
Documentation/networking/psp.rst | 75 +++++++++++++++
include/net/psp/types.h | 93 ++++++++++++++++++-
include/net/psp/functions.h | 54 ++++++++++-
include/uapi/linux/psp.h | 17 ++++
.../mellanox/mlx5/core/en_accel/psp_rxtx.c | 2 +-
drivers/net/netdevsim/psp.c | 2 +-
net/psp/psp-nl-gen.c | 5 +-
net/psp/psp_main.c | 53 +++++++----
net/psp/psp_nl.c | 21 ++++-
net/psp/psp_sock.c | 35 +++++++
11 files changed, 373 insertions(+), 26 deletions(-)
diff --git a/Documentation/netlink/specs/psp.yaml b/Documentation/netlink/specs/psp.yaml
index e9c2ee7e28e0..a65274920d21 100644
--- a/Documentation/netlink/specs/psp.yaml
+++ b/Documentation/netlink/specs/psp.yaml
@@ -11,6 +11,24 @@ name: psp
name: version
entries: [hdr0-aes-gcm-128, hdr0-aes-gcm-256,
hdr0-aes-gmac-128, hdr0-aes-gmac-256]
+ -
+ type: enum
+ name: vc-steer
+ doc: Directions of traffic which take part in VC based queue steering.
+ entries:
+ -
+ name: tx
+ doc: |
+ Grant the requests peers make, so that traffic this device
+ sends is steered at the far end. Costs nothing but the room in
+ the header, the queue is the peer's to pick, so this does not
+ need vc-steer-cap.
+ -
+ name: rx
+ doc: |
+ Ask peers to send to the Rx queue paired with the flow's Tx
+ queue, so that traffic this device receives is steered. Needs
+ vc-steer-cap, the rules doing the steering are ours.
attribute-sets:
-
@@ -72,6 +90,27 @@ name: psp
Present when in associated namespace, absent when in primary/host
namespace.
type: flag
+ -
+ name: vc-steer-cap
+ doc: |
+ Device can steer received traffic on the PSP virtualization
+ cookie (VC). The VC is split into a 32b reserved part, a 16b
+ queue ID the sender is asking the peer to send to, and a 16b
+ queue ID granting the peer's own request. Steering installs low
+ priority rules matching the latter, which win over the RSS table
+ result. Only needed for the rx direction; granting a peer's
+ request is just header generation and needs no device support.
+ type: flag
+ -
+ name: vc-steer-ena
+ doc: |
+ Directions taking part in VC based queue steering. Leave the
+ attribute out of a dev-set request to keep the current setting.
+ Applies to associations created from then on, existing ones keep
+ the setting they were created with.
+ type: u32
+ enum: vc-steer
+ enum-as-flags: true
-
name: assoc
@@ -207,6 +246,8 @@ name: psp
- psp-versions-ena
- assoc-list
- by-association
+ - vc-steer-cap
+ - vc-steer-ena
pre: psp-device-get-locked
post: psp-device-unlock
dump:
@@ -231,6 +272,7 @@ name: psp
attributes:
- id
- psp-versions-ena
+ - vc-steer-ena
reply:
attributes: []
pre: psp-device-get-locked-admin
diff --git a/Documentation/networking/psp.rst b/Documentation/networking/psp.rst
index 4ac09e64e95a..4ebb3853dc6e 100644
--- a/Documentation/networking/psp.rst
+++ b/Documentation/networking/psp.rst
@@ -132,6 +132,68 @@ numbers in a way that deletes a prefix of the PSP protected part of
the TCP stream. If userspace cares to mitigate this type of attack, a
special "start of PSP" message should be exchanged after ``tx-assoc``.
+Queue steering
+--------------
+
+The PSP header may carry an optional 64 bit "virtualization cookie" (VC).
+The protocol assigns it no meaning in transport mode, so Linux uses it to
+let the two ends of a connection tell each other which Rx queue they want
+traffic delivered to. The cookie carries two queue IDs::
+
+ 63 48 47 32 31 16 15 0
+ +---------------+---------------+---------------+---------------+
+ | reserved | req qid | reserved | dst qid |
+ +---------------+---------------+---------------+---------------+
+
+``req`` is the Rx queue the sender is asking the peer to send to, and
+``dst`` is the queue this packet is to be delivered to, which holds the
+``req`` the sender last saw from the peer. Each side's request is what
+becomes the other side's destination. ``0xffff`` means "no queue" and
+reserved bits must be zero.
+
+Each ID gets a 32 bit word to itself, of which only the low half is used.
+Queue counts fit in 16 bits today; should that stop being true, an ID can
+grow into the reserved half of its word without the fields moving.
+
+The two directions are enabled independently, with ``vc-steer-ena``:
+
+ * ``rx`` asks peers to send to the queue paired with the flow's Tx queue,
+ so traffic this host *receives* gets steered. This needs
+ ``vc-steer-cap``: the driver is required to arrange the appropriate Rx
+ steering, and whatever rules it uses to do so are implicit, not visible
+ to the user. They have lower priority than any explicitly configured
+ flow steering, but do take precedence over the RSS table.
+ * ``tx`` grants the requests peers make, so traffic this host *sends*
+ gets steered at the far end. The queue is the peer's to choose and the
+ rules are the peer's to install, so this needs no device support at
+ all, and can be turned on even where ``vc-steer-cap`` is absent.
+
+Enabling either direction grows the PSP header by the size of the cookie,
+and the MSS shrinks accordingly. The setting is therefore sampled when an
+association is created; changing it later applies to new associations
+only, and existing connections keep the header size they were set up
+with.
+
+The queue the local end asks for is refreshed from the Tx queue the stack
+picks for the flow, assuming that Rx and Tx queues are paired by index.
+
+Trust model
+~~~~~~~~~~~
+
+VC steering as implemented is not robust against queue DDoS attacks, that
+is a coordinated overload of a single Rx queue, because any peer can name
+any queue. The expectation is that PSP is not used to talk to untrusted
+peers while VC steering is enabled. Note that use of the cookie requires
+PSP, so the *machine* as a whole may still talk to untrusted peers, as
+long as it hands out no PSP keys to them.
+
+The intended way of handling a mix of trusted and untrusted peers is to
+extend the queue ID and stop using it as a direct index, making it a per
+queue cookie instead. An untrusted peer should then not be able to guess
+a tag which was never communicated to it, and the secrets can be rotated
+periodically and gradually, one queue at a time. It is important that
+changes to the implementation do not prevent this future extension.
+
Rotation notifications
----------------------
@@ -172,6 +234,19 @@ 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.
+Every driver has to carry the cookie, not just those which advertise
+``vc-steer-cap`` - granting a peer's request needs no help from the
+device, so the ``tx`` direction of ``vc-steer-ena`` may be turned on
+anywhere. Drivers must ask ``psp_assoc_vc_tx_get()`` for the cookie to
+place in the Tx header, and report the queue IDs a received cookie held
+in ``psp_skb_ext.vc_req`` and ``vc_dst`` (``psp_dev_rcv()`` does this for
+drivers which let the core strip the headers). Reporting is what allows
+the core to grant the peer's request. The steering itself is only
+expected of drivers which advertise ``vc-steer-cap``.
+
+When VC steering is enabled GRO implementations are allowed to ignore
+changes in the cookie for transport mode PSP.
+
Kernel implementation notes
---------------------------
diff --git a/include/net/psp/types.h b/include/net/psp/types.h
index 87991a1ea02d..87ceb16b1a82 100644
--- a/include/net/psp/types.h
+++ b/include/net/psp/types.h
@@ -3,9 +3,12 @@
#ifndef __NET_PSP_H
#define __NET_PSP_H
+#include <linux/bitfield.h>
+#include <linux/bits.h>
#include <linux/mutex.h>
#include <linux/refcount.h>
#include <net/net_trackers.h>
+#include <uapi/linux/psp.h>
struct netlink_ext_ack;
@@ -35,13 +38,58 @@ struct psphdr {
#define PSPHDR_VERFL_ONE BIT(0)
#define PSP_HDRLEN_NOOPT ((sizeof(struct psphdr) - 8) / 8)
+#define PSP_HDRLEN_VC (PSP_HDRLEN_NOOPT + 1)
+
+/* Virtualization cookie (VC) based Rx queue steering.
+ *
+ * The VC is a 64b cookie which the PSP spec leaves to the implementation
+ * in transport mode. We use it to let the two ends of a connection tell
+ * each other which Rx queue they'd like traffic delivered to:
+ *
+ * 63 48 47 32 31 16 15 0
+ * +---------------+---------------+---------------+---------------+
+ * | reserved | req qid | reserved | dst qid |
+ * +---------------+---------------+---------------+---------------+
+ *
+ * @req is the Rx queue the sender is asking the peer to send to.
+ * @dst is the Rx queue this packet is to be delivered to, and holds the
+ * @req the sender most recently saw from the peer. Each side's request
+ * is what becomes the other side's destination.
+ *
+ * Each ID gets a 32b word to itself, of which only the low half is used
+ * today. Queue counts fit in 16b for now, but growing an ID to 32b later
+ * is then a matter of widening its mask, with no reshuffling of the
+ * cookie and no change to what an old peer puts on the wire.
+ *
+ * The two directions are enabled separately, see enum psp_vc_steer.
+ * %PSP_VC_STEER_RX fills in @req and needs the device to install low
+ * priority steering rules matching on @dst, which take precedence over
+ * the RSS table result - @dst sits in the low bits so that those rules
+ * only need to mask off the bottom 16b of the cookie. %PSP_VC_STEER_TX
+ * fills in @dst and needs nothing from the device, it only helps the
+ * peer.
+ *
+ * The wire is a two party structure, so it is named from the sender's
+ * side, and so are the queue IDs a received packet reports in
+ * psp_skb_ext. The association records the same two IDs from our own
+ * side instead: see psp_assoc.vc_loc and psp_assoc.vc_rem.
+ *
+ * %PSP_VC_QID_NONE means "no queue" and is what both fields hold before
+ * anything has been learned. Reserved bits are 0 on Tx, ignored on Rx.
+ */
+#define PSP_VC_REQ_QID GENMASK_ULL(47, 32)
+#define PSP_VC_DST_QID GENMASK_ULL(15, 0)
+
+#define PSP_VC_QID_NONE 0xffff
/**
* struct psp_dev_config - PSP device configuration
* @versions: PSP versions enabled on the device
+ * @vc_steer: directions taking part in VC steering, mask of enum psp_vc_steer
*/
struct psp_dev_config {
u32 versions;
+ u32 vc_steer;
};
/* Max number of devices that can be associated with a single PSP device.
@@ -133,25 +181,51 @@ struct psp_dev_caps {
* Determines the size of struct psp_assoc::drv_data
*/
u32 assoc_drv_spc;
+
+ /**
+ * @vc_steer: device can steer received traffic on the VC
+ * Only gates PSP_VC_STEER_RX, granting a peer's request needs
+ * nothing from the device.
+ */
+ bool vc_steer;
};
#define PSP_MAX_KEY 32
-#define PSP_HDR_SIZE 16 /* We don't support optional fields, yet */
+#define PSP_HDR_SIZE 16 /* Fixed part of the PSP header */
+#define PSP_VC_SIZE 8 /* Optional virtualization cookie */
#define PSP_TRL_SIZE 16 /* AES-GCM/GMAC trailer size */
+/* Keep free of padding, the whole struct gets memcmp()ed by GRO */
struct psp_skb_ext {
__be32 spi;
u16 dev_id;
u8 generation;
u8 version;
+ u16 vc_req; /* Queue the sender asked for, or PSP_VC_QID_NONE */
+ u16 vc_dst; /* Queue the sender addressed, or PSP_VC_QID_NONE */
};
+static_assert(sizeof(struct psp_skb_ext) == 12,
+ "struct psp_skb_ext must not contain padding");
+
struct psp_key_parsed {
__be32 spi;
u8 key[PSP_MAX_KEY];
};
+/**
+ * enum psp_assoc_flags - flags of struct psp_assoc
+ * @PSP_ASSOC_VC_TX: grant the queue the peer asks for in the cookie
+ * @PSP_ASSOC_VC_RX: ask the peer for a queue in the cookie
+ */
+enum psp_assoc_flags {
+ PSP_ASSOC_VC_TX = BIT(0),
+ PSP_ASSOC_VC_RX = BIT(1),
+};
+
+#define PSP_ASSOC_VC_ANY (PSP_ASSOC_VC_TX | PSP_ASSOC_VC_RX)
+
struct psp_assoc {
struct psp_dev *psd;
@@ -159,6 +233,23 @@ struct psp_assoc {
u8 generation;
u8 version;
u8 peer_tx;
+ /* enum psp_assoc_flags. Written under psd->lock, additionally read
+ * on the Tx fast path without it. A snapshot of the device config
+ * taken when the association was created, so that the header size,
+ * and with it the MSS, cannot change under an established
+ * connection.
+ */
+ u8 flags;
+
+ /* Queue IDs for the VC, ours and the peer's. @vc_loc is refreshed
+ * from the Tx queue selection and goes out as the cookie's request,
+ * @vc_rem is learned from the peer's requests and goes back out as
+ * the destination. Both are PSP_VC_QID_NONE until something is
+ * learned. Written without the socket lock, always use
+ * READ_ONCE()/WRITE_ONCE().
+ */
+ u16 vc_loc;
+ u16 vc_rem;
u32 upgrade_seq;
diff --git a/include/net/psp/functions.h b/include/net/psp/functions.h
index c5c23a54774e..cd123868aab3 100644
--- a/include/net/psp/functions.h
+++ b/include/net/psp/functions.h
@@ -18,7 +18,7 @@ psp_dev_create(struct net_device *netdev, struct psp_dev_ops *psd_ops,
struct psp_dev_caps *psd_caps, void *priv_ptr);
void psp_dev_unregister(struct psp_dev *psd);
bool psp_dev_encapsulate(struct net *net, struct sk_buff *skb, __be32 spi,
- u8 ver, __be16 sport);
+ u8 ver, __be16 sport, u64 vc);
int psp_dev_rcv(struct sk_buff *skb, u16 dev_id, u8 generation, bool strip_icv);
/* Kernel-facing API */
@@ -29,6 +29,22 @@ static inline void *psp_assoc_drv_data(struct psp_assoc *pas)
return pas->drv_data;
}
+/**
+ * psp_assoc_vc_tx_get() - build the virtualization cookie for an association
+ * @pas: association the packet belongs to
+ *
+ * Return: cookie to place in the PSP header, or 0 if the association does
+ * not use VC steering and the header should carry no cookie.
+ */
+static inline u64 psp_assoc_vc_tx_get(const struct psp_assoc *pas)
+{
+ if (likely(!(pas->flags & PSP_ASSOC_VC_ANY)))
+ return 0;
+
+ return FIELD_PREP(PSP_VC_REQ_QID, READ_ONCE(pas->vc_loc)) |
+ FIELD_PREP(PSP_VC_DST_QID, READ_ONCE(pas->vc_rem));
+}
+
#if IS_ENABLED(CONFIG_INET_PSP)
unsigned int psp_key_size(u32 version);
void psp_sk_assoc_free(struct sock *sk);
@@ -88,6 +104,31 @@ psp_pse_matches_pas(struct psp_skb_ext *pse, struct psp_assoc *pas)
pas->dev_id == pse->dev_id;
}
+/**
+ * psp_assoc_vc_rx_update() - note the queue the peer is asking for
+ * @pas: association the packet arrived on
+ * @pse: PSP info extracted from the packet
+ *
+ * The peer's request becomes the destination in the cookies we send it,
+ * which is what makes its device steer our traffic. Only tracked if we
+ * are going to grant it. Called for every PSP packet, so the common
+ * cases have to be cheap: a peer which sends no cookie leaves @vc_req
+ * at %PSP_VC_QID_NONE, which is also what @vc_rem was initialised to,
+ * so both "no cookie" and "request unchanged" cost a single compare and
+ * no store.
+ */
+static inline void
+psp_assoc_vc_rx_update(struct psp_assoc *pas, const struct psp_skb_ext *pse)
+{
+ if (likely(!(pas->flags & PSP_ASSOC_VC_TX)))
+ return;
+
+ if (likely(pse->vc_req == READ_ONCE(pas->vc_rem)))
+ return;
+
+ WRITE_ONCE(pas->vc_rem, pse->vc_req);
+}
+
static inline enum skb_drop_reason
__psp_sk_rx_policy_check(struct sk_buff *skb, struct psp_assoc *pas)
{
@@ -100,6 +141,8 @@ __psp_sk_rx_policy_check(struct sk_buff *skb, struct psp_assoc *pas)
if (unlikely(!pas->peer_tx))
pas->peer_tx = 1;
+ psp_assoc_vc_rx_update(pas, pse);
+
return 0;
}
@@ -150,9 +193,14 @@ static inline struct psp_assoc *psp_skb_get_assoc_rcu(struct sk_buff *skb)
static inline unsigned int psp_sk_overhead(const struct sock *sk)
{
int psp_encap = sizeof(struct udphdr) + PSP_HDR_SIZE + PSP_TRL_SIZE;
- bool has_psp = rcu_access_pointer(sk->psp_assoc);
+ struct psp_assoc *pas = psp_sk_assoc(sk);
- return has_psp ? psp_encap : 0;
+ if (!pas)
+ return 0;
+ if (pas->flags & PSP_ASSOC_VC_ANY)
+ psp_encap += PSP_VC_SIZE;
+
+ return psp_encap;
}
#else
static inline void psp_sk_assoc_free(struct sock *sk) { }
diff --git a/include/uapi/linux/psp.h b/include/uapi/linux/psp.h
index 1c8899cd4da5..15e276ea84fb 100644
--- a/include/uapi/linux/psp.h
+++ b/include/uapi/linux/psp.h
@@ -17,6 +17,21 @@ enum psp_version {
PSP_VERSION_HDR0_AES_GMAC_256,
};
+/**
+ * enum psp_vc_steer - Directions of traffic which take part in VC based queue
+ * steering.
+ * @PSP_VC_STEER_TX: Grant the requests peers make, so that traffic this device
+ * sends is steered at the far end. Costs nothing but the room in the header,
+ * the queue is the peer's to pick, so this does not need vc-steer-cap.
+ * @PSP_VC_STEER_RX: Ask peers to send to the Rx queue paired with the flow's
+ * Tx queue, so that traffic this device receives is steered. Needs
+ * vc-steer-cap, the rules doing the steering are ours.
+ */
+enum psp_vc_steer {
+ PSP_VC_STEER_TX,
+ PSP_VC_STEER_RX,
+};
+
enum {
PSP_A_ASSOC_DEV_INFO_IFINDEX = 1,
PSP_A_ASSOC_DEV_INFO_NSID,
@@ -33,6 +48,8 @@ enum {
PSP_A_DEV_ASSOC_LIST,
PSP_A_DEV_NSID,
PSP_A_DEV_BY_ASSOCIATION,
+ PSP_A_DEV_VC_STEER_CAP,
+ PSP_A_DEV_VC_STEER_ENA,
__PSP_A_DEV_MAX,
PSP_A_DEV_MAX = (__PSP_A_DEV_MAX - 1)
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..5df17efae127 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
@@ -171,7 +171,7 @@ bool mlx5e_psp_handle_tx_skb(struct net_device *netdev,
return true;
/* psp_encap of the packet */
- if (!psp_dev_encapsulate(net, skb, psp_st->spi, psp_st->ver, 0)) {
+ if (!psp_dev_encapsulate(net, skb, psp_st->spi, psp_st->ver, 0, 0)) {
kfree_skb_reason(skb, SKB_DROP_REASON_PSP_OUTPUT);
atomic_inc(&priv->psp->tx_drop);
return false;
diff --git a/drivers/net/netdevsim/psp.c b/drivers/net/netdevsim/psp.c
index 6b3532b5e360..32d95205a8cc 100644
--- a/drivers/net/netdevsim/psp.c
+++ b/drivers/net/netdevsim/psp.c
@@ -44,7 +44,7 @@ nsim_do_psp(struct sk_buff *skb, struct netdevsim *ns,
}
net = sock_net(skb->sk);
- if (!psp_dev_encapsulate(net, skb, pas->tx.spi, pas->version, 0)) {
+ if (!psp_dev_encapsulate(net, skb, pas->tx.spi, pas->version, 0, 0)) {
rc = SKB_DROP_REASON_PSP_OUTPUT;
goto out_unlock;
}
diff --git a/net/psp/psp-nl-gen.c b/net/psp/psp-nl-gen.c
index 0e426ffac398..743de854aca5 100644
--- a/net/psp/psp-nl-gen.c
+++ b/net/psp/psp-nl-gen.c
@@ -23,9 +23,10 @@ static const struct nla_policy psp_dev_get_nl_policy[PSP_A_DEV_ID + 1] = {
};
/* PSP_CMD_DEV_SET - do */
-static const struct nla_policy psp_dev_set_nl_policy[PSP_A_DEV_PSP_VERSIONS_ENA + 1] = {
+static const struct nla_policy psp_dev_set_nl_policy[PSP_A_DEV_VC_STEER_ENA + 1] = {
[PSP_A_DEV_ID] = NLA_POLICY_MIN(NLA_U32, 1),
[PSP_A_DEV_PSP_VERSIONS_ENA] = NLA_POLICY_MASK(NLA_U32, 0xf),
+ [PSP_A_DEV_VC_STEER_ENA] = NLA_POLICY_MASK(NLA_U32, 0x3),
};
/* PSP_CMD_KEY_ROTATE - do */
@@ -89,7 +90,7 @@ static const struct genl_split_ops psp_nl_ops[] = {
.doit = psp_nl_dev_set_doit,
.post_doit = psp_device_unlock,
.policy = psp_dev_set_nl_policy,
- .maxattr = PSP_A_DEV_PSP_VERSIONS_ENA,
+ .maxattr = PSP_A_DEV_VC_STEER_ENA,
.flags = GENL_ADMIN_PERM | GENL_CMD_CAP_DO,
},
{
diff --git a/net/psp/psp_main.c b/net/psp/psp_main.c
index c9c1a8826b7f..cf4bb4e44c55 100644
--- a/net/psp/psp_main.c
+++ b/net/psp/psp_main.c
@@ -186,7 +186,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, unsigned int udp_len, __be16 sport,
+ u64 vc)
{
struct udphdr *uh = udp_hdr(skb);
struct psphdr *psph = (struct psphdr *)(uh + 1);
@@ -234,54 +235,62 @@ static void psp_write_headers(struct net *net, struct sk_buff *skb, __be32 spi,
udp_set_len(uh, udp_len);
psph->nexthdr = IPPROTO_TCP;
- psph->hdrlen = PSP_HDRLEN_NOOPT;
+ psph->hdrlen = vc ? PSP_HDRLEN_VC : PSP_HDRLEN_NOOPT;
psph->crypt_offset = 0;
psph->verfl = FIELD_PREP(PSPHDR_VERFL_VERSION, ver) |
+ FIELD_PREP(PSPHDR_VERFL_VIRT, !!vc) |
FIELD_PREP(PSPHDR_VERFL_ONE, 1);
psph->spi = spi;
memset(&psph->iv, 0, sizeof(psph->iv));
+ if (vc)
+ psph->vc[0] = cpu_to_be64(vc);
}
/* Encapsulate a TCP packet with PSP by adding the UDP+PSP headers and filling
- * them in.
+ * them in. @vc is the virtualization cookie to place in the header, 0 for
+ * a header with no optional fields.
*/
bool psp_dev_encapsulate(struct net *net, struct sk_buff *skb, __be32 spi,
- u8 ver, __be16 sport)
+ u8 ver, __be16 sport, u64 vc)
{
u32 network_len = skb_network_header_len(skb);
u32 ethr_len = skb_mac_header_len(skb);
u32 bufflen = ethr_len + network_len;
+ u32 encap_len = PSP_ENCAP_HLEN;
if (skb->protocol != htons(ETH_P_IP) &&
skb->protocol != htons(ETH_P_IPV6))
return false;
- if (skb_cow_head(skb, PSP_ENCAP_HLEN))
+ if (vc)
+ encap_len += PSP_VC_SIZE;
+
+ if (skb_cow_head(skb, encap_len))
return false;
- skb_push(skb, PSP_ENCAP_HLEN);
- skb->mac_header -= PSP_ENCAP_HLEN;
- skb->network_header -= PSP_ENCAP_HLEN;
- skb->transport_header -= PSP_ENCAP_HLEN;
- memmove(skb->data, skb->data + PSP_ENCAP_HLEN, bufflen);
+ skb_push(skb, encap_len);
+ skb->mac_header -= encap_len;
+ skb->network_header -= encap_len;
+ skb->transport_header -= encap_len;
+ memmove(skb->data, skb->data + encap_len, bufflen);
if (skb->protocol == htons(ETH_P_IP)) {
ip_hdr(skb)->protocol = IPPROTO_UDP;
- be16_add_cpu(&ip_hdr(skb)->tot_len, PSP_ENCAP_HLEN);
+ be16_add_cpu(&ip_hdr(skb)->tot_len, encap_len);
ip_hdr(skb)->check = 0;
ip_hdr(skb)->check =
ip_fast_csum((u8 *)ip_hdr(skb), ip_hdr(skb)->ihl);
} else {
ipv6_hdr(skb)->nexthdr = IPPROTO_UDP;
- be16_add_cpu(&ipv6_hdr(skb)->payload_len, PSP_ENCAP_HLEN);
+ be16_add_cpu(&ipv6_hdr(skb)->payload_len, encap_len);
}
skb_set_inner_ipproto(skb, IPPROTO_TCP);
skb_set_inner_transport_header(skb, skb_transport_offset(skb) +
- PSP_ENCAP_HLEN);
+ encap_len);
skb->encapsulation = 1;
psp_write_headers(net, skb, spi, ver,
- skb->len - skb_transport_offset(skb), sport);
+ skb->len - skb_transport_offset(skb), sport, vc);
return true;
}
@@ -290,9 +299,10 @@ EXPORT_SYMBOL(psp_dev_encapsulate);
/* Receive handler for PSP packets.
*
* Accepts only already-authenticated packets. The full PSP header is
- * stripped according to psph->hdrlen; any optional fields it advertises
- * (virtualization cookies, etc.) are ignored and discarded along with the
- * rest of the header. The caller should ensure that skb->data is pointing
+ * stripped according to psph->hdrlen; the virtualization cookie is recorded
+ * in the skb extension, any other optional fields are ignored and discarded
+ * along with the rest of the header. The caller should ensure that skb->data
+ * is pointing
* to the mac header, and that skb->mac_len is set. This function does not
* currently adjust skb->csum (CHECKSUM_COMPLETE is not supported).
*/
@@ -370,6 +380,15 @@ int psp_dev_rcv(struct sk_buff *skb, u16 dev_id, u8 generation, bool strip_icv)
pse->dev_id = dev_id;
pse->generation = generation;
pse->version = FIELD_GET(PSPHDR_VERFL_VERSION, psph->verfl);
+ pse->vc_req = PSP_VC_QID_NONE;
+ pse->vc_dst = PSP_VC_QID_NONE;
+ if (unlikely(psph->verfl & PSPHDR_VERFL_VIRT) &&
+ psp_hlen >= sizeof(*psph) + PSP_VC_SIZE) {
+ u64 vc = be64_to_cpu(psph->vc[0]);
+
+ pse->vc_req = FIELD_GET(PSP_VC_REQ_QID, vc);
+ pse->vc_dst = FIELD_GET(PSP_VC_DST_QID, vc);
+ }
encap = sizeof(struct udphdr) + psp_hlen;
encap += strip_icv ? PSP_TRL_SIZE : 0;
diff --git a/net/psp/psp_nl.c b/net/psp/psp_nl.c
index f91665748dde..b5f1bfd8bdcf 100644
--- a/net/psp/psp_nl.c
+++ b/net/psp/psp_nl.c
@@ -299,6 +299,11 @@ psp_nl_dev_fill(struct psp_dev *psd, struct sk_buff *rsp,
nla_put_u32(rsp, PSP_A_DEV_PSP_VERSIONS_ENA, psd->config.versions))
goto err_cancel_msg;
+ if (psd->caps->vc_steer && nla_put_flag(rsp, PSP_A_DEV_VC_STEER_CAP))
+ goto err_cancel_msg;
+ if (nla_put_u32(rsp, PSP_A_DEV_VC_STEER_ENA, psd->config.vc_steer))
+ goto err_cancel_msg;
+
if (cur_net == dev_net(psd->main_netdev)) {
/* Primary device - dump assoc list */
err = psp_nl_fill_assoc_dev_list(psd, rsp, cur_net, NULL);
@@ -418,11 +423,25 @@ int psp_nl_dev_set_doit(struct sk_buff *skb, struct genl_info *info)
NL_SET_ERR_MSG(info->extack, "Requested PSP versions not supported by the device");
return -EINVAL;
}
- } else {
+ } else if (!info->attrs[PSP_A_DEV_VC_STEER_ENA]) {
NL_SET_ERR_MSG(info->extack, "No settings present");
return -EINVAL;
}
+ if (info->attrs[PSP_A_DEV_VC_STEER_ENA]) {
+ new_config.vc_steer =
+ nla_get_u32(info->attrs[PSP_A_DEV_VC_STEER_ENA]);
+ /* Granting a peer's request is just header generation,
+ * only steering our own Rx needs the device to help.
+ */
+ if (new_config.vc_steer & (1 << PSP_VC_STEER_RX) &&
+ !psd->caps->vc_steer) {
+ NL_SET_BAD_ATTR(info->extack,
+ info->attrs[PSP_A_DEV_VC_STEER_ENA]);
+ return -EOPNOTSUPP;
+ }
+ }
+
rsp = psp_nl_reply_new(info);
if (!rsp)
return -ENOMEM;
diff --git a/net/psp/psp_sock.c b/net/psp/psp_sock.c
index 1a2a6b7516b0..1594767e5184 100644
--- a/net/psp/psp_sock.c
+++ b/net/psp/psp_sock.c
@@ -26,6 +26,32 @@ struct psp_dev *psp_dev_get_for_sock(struct sock *sk)
return psd;
}
+/* Refresh the local queue ID we ask the peer to send to.
+ *
+ * Rx and Tx queues are assumed to be paired by index, so the Tx queue the
+ * stack has just picked for this flow doubles as the Rx queue we expect
+ * the flow's replies to land on. This is the Tx side counterpart of an
+ * aRFS update, and runs for every skb of a steered PSP socket, so the
+ * unchanged case has to stay down to a load and a compare.
+ */
+static void psp_assoc_vc_tx_update(struct psp_assoc *pas,
+ struct net_device *dev,
+ struct sk_buff *skb)
+{
+ u16 qid = skb_get_queue_mapping(skb);
+
+ /* A device may have more Tx than Rx queues, in which case the top
+ * Tx queues have no Rx queue to pair with.
+ */
+ if (unlikely(qid >= READ_ONCE(dev->real_num_rx_queues)))
+ qid = PSP_VC_QID_NONE;
+
+ if (likely(qid == READ_ONCE(pas->vc_loc)))
+ return;
+
+ WRITE_ONCE(pas->vc_loc, qid);
+}
+
static struct sk_buff *
psp_validate_xmit(struct sock *sk, struct net_device *dev, struct sk_buff *skb)
{
@@ -35,6 +61,8 @@ psp_validate_xmit(struct sock *sk, struct net_device *dev, struct sk_buff *skb)
rcu_read_lock();
pas = psp_skb_get_assoc_rcu(skb);
good = !pas || rcu_access_pointer(dev->psp_dev) == pas->psd;
+ if (good && pas && pas->flags & PSP_ASSOC_VC_RX)
+ psp_assoc_vc_tx_update(pas, dev, skb);
rcu_read_unlock();
if (!good) {
sk_skb_reason_drop(sk, skb, SKB_DROP_REASON_PSP_OUTPUT);
@@ -58,6 +86,13 @@ struct psp_assoc *psp_assoc_create(struct psp_dev *psd)
pas->psd = psd;
pas->dev_id = psd->id;
pas->generation = psd->generation;
+ pas->vc_loc = PSP_VC_QID_NONE;
+ pas->vc_rem = PSP_VC_QID_NONE;
+ /* Snapshot the config, the header size may not change later on */
+ if (psd->config.vc_steer & (1 << PSP_VC_STEER_TX))
+ pas->flags |= PSP_ASSOC_VC_TX;
+ if (psd->config.vc_steer & (1 << PSP_VC_STEER_RX))
+ pas->flags |= PSP_ASSOC_VC_RX;
psp_dev_get(psd);
refcount_set(&pas->refcnt, 1);
--
2.55.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [RFC net-next 2/6] netdevsim: support PSP VC based queue steering
2026-08-22 22:55 [RFC net-next 0/6] psp: use virt cookie as Rx steering hint Jakub Kicinski
2026-08-22 22:55 ` [RFC net-next 1/6] psp: steer Rx queues with the virtualization cookie Jakub Kicinski
@ 2026-08-22 22:55 ` Jakub Kicinski
2026-08-22 22:55 ` [RFC net-next 3/6] selftests: drv-net: psp: move the PSP test plumbing into psp_lib.py Jakub Kicinski
` (4 subsequent siblings)
6 siblings, 0 replies; 18+ messages in thread
From: Jakub Kicinski @ 2026-08-22 22:55 UTC (permalink / raw)
To: daniel.zahka, willemdebruijn.kernel
Cc: edumazet, cratiu, borisp, kuniyu, netdev, Jakub Kicinski
Gives the PSP steering selftest something to run against without
hardware. nsim_start_xmit() already mirrors the Tx queue index onto Rx,
so all the cookie does is override that choice.
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
drivers/net/netdevsim/netdevsim.h | 4 ++--
drivers/net/netdevsim/netdev.c | 5 +++--
drivers/net/netdevsim/psp.c | 26 ++++++++++++++++++++++++--
3 files changed, 29 insertions(+), 6 deletions(-)
diff --git a/drivers/net/netdevsim/netdevsim.h b/drivers/net/netdevsim/netdevsim.h
index 55aec41237b9..3b1c52e43d46 100644
--- a/drivers/net/netdevsim/netdevsim.h
+++ b/drivers/net/netdevsim/netdevsim.h
@@ -453,13 +453,13 @@ void nsim_psp_uninit(struct netdevsim *ns);
void nsim_psp_handle_ext(struct sk_buff *skb, struct skb_ext *psp_ext);
enum skb_drop_reason
nsim_do_psp(struct sk_buff *skb, struct netdevsim *ns,
- struct netdevsim *peer_ns, struct skb_ext **psp_ext);
+ struct netdevsim *peer_ns, struct skb_ext **psp_ext, int *rxq);
#else
static inline int nsim_psp_init(struct netdevsim *ns) { return 0; }
static inline void nsim_psp_uninit(struct netdevsim *ns) {}
static inline enum skb_drop_reason
nsim_do_psp(struct sk_buff *skb, struct netdevsim *ns,
- struct netdevsim *peer_ns, struct skb_ext **psp_ext)
+ struct netdevsim *peer_ns, struct skb_ext **psp_ext, int *rxq)
{
return 0;
}
diff --git a/drivers/net/netdevsim/netdev.c b/drivers/net/netdevsim/netdev.c
index b4a99f3ceac6..04e416d1a377 100644
--- a/drivers/net/netdevsim/netdev.c
+++ b/drivers/net/netdevsim/netdev.c
@@ -147,11 +147,12 @@ static netdev_tx_t nsim_start_xmit(struct sk_buff *skb, struct net_device *dev)
peer_dev = peer_ns->netdev;
}
- dr = nsim_do_psp(skb, ns, peer_ns, &psp_ext);
+ rxq = skb_get_queue_mapping(skb);
+
+ dr = nsim_do_psp(skb, ns, peer_ns, &psp_ext, &rxq);
if (dr)
goto out_drop_free;
- rxq = skb_get_queue_mapping(skb);
if (rxq >= peer_dev->num_rx_queues)
rxq = rxq % peer_dev->num_rx_queues;
rq = peer_ns->rq[rxq];
diff --git a/drivers/net/netdevsim/psp.c b/drivers/net/netdevsim/psp.c
index 32d95205a8cc..5aa5078889eb 100644
--- a/drivers/net/netdevsim/psp.c
+++ b/drivers/net/netdevsim/psp.c
@@ -14,9 +14,26 @@ void nsim_psp_handle_ext(struct sk_buff *skb, struct skb_ext *psp_ext)
__skb_ext_set(skb, SKB_EXT_PSP, psp_ext);
}
+/* Pick the Rx queue for a decapsulated frame. Devices which support
+ * PSP_VC_STEER_RX match the destination queue ID carried in the
+ * cookie ahead of consulting the RSS table.
+ */
+static int nsim_psp_steer(struct netdevsim *peer_ns, struct sk_buff *skb,
+ int rxq)
+{
+ struct net_device *dev = peer_ns->netdev;
+ struct psp_skb_ext *pse;
+
+ pse = skb_ext_find(skb, SKB_EXT_PSP);
+ if (!pse || pse->vc_dst >= dev->real_num_rx_queues)
+ return rxq;
+
+ return pse->vc_dst;
+}
+
enum skb_drop_reason
nsim_do_psp(struct sk_buff *skb, struct netdevsim *ns,
- struct netdevsim *peer_ns, struct skb_ext **psp_ext)
+ struct netdevsim *peer_ns, struct skb_ext **psp_ext, int *rxq)
{
enum skb_drop_reason rc = 0;
struct psp_dev *peer_psd;
@@ -44,7 +61,8 @@ nsim_do_psp(struct sk_buff *skb, struct netdevsim *ns,
}
net = sock_net(skb->sk);
- if (!psp_dev_encapsulate(net, skb, pas->tx.spi, pas->version, 0, 0)) {
+ if (!psp_dev_encapsulate(net, skb, pas->tx.spi, pas->version, 0,
+ psp_assoc_vc_tx_get(pas))) {
rc = SKB_DROP_REASON_PSP_OUTPUT;
goto out_unlock;
}
@@ -73,6 +91,9 @@ nsim_do_psp(struct sk_buff *skb, struct netdevsim *ns,
goto out_unlock;
}
+ if (peer_psd->config.vc_steer & (1 << PSP_VC_STEER_RX))
+ *rxq = nsim_psp_steer(peer_ns, skb, *rxq);
+
*psp_ext = skb->extensions;
refcount_inc(&(*psp_ext)->refcnt);
skb->decrypted = 1;
@@ -216,6 +237,7 @@ static struct psp_dev_caps nsim_psp_caps = {
1 << PSP_VERSION_HDR0_AES_GCM_256 |
1 << PSP_VERSION_HDR0_AES_GMAC_256,
.assoc_drv_spc = sizeof(void *),
+ .vc_steer = true,
};
static void __nsim_psp_uninit(struct netdevsim *ns, bool teardown)
--
2.55.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [RFC net-next 3/6] selftests: drv-net: psp: move the PSP test plumbing into psp_lib.py
2026-08-22 22:55 [RFC net-next 0/6] psp: use virt cookie as Rx steering hint Jakub Kicinski
2026-08-22 22:55 ` [RFC net-next 1/6] psp: steer Rx queues with the virtualization cookie Jakub Kicinski
2026-08-22 22:55 ` [RFC net-next 2/6] netdevsim: support PSP VC based queue steering Jakub Kicinski
@ 2026-08-22 22:55 ` Jakub Kicinski
2026-08-22 22:55 ` [RFC net-next 4/6] selftests: drv-net: psp_steer: test PSP VC based queue steering Jakub Kicinski
` (3 subsequent siblings)
6 siblings, 0 replies; 18+ messages in thread
From: Jakub Kicinski @ 2026-08-22 22:55 UTC (permalink / raw)
To: daniel.zahka, willemdebruijn.kernel
Cc: edumazet, cratiu, borisp, kuniyu, netdev, Jakub Kicinski
Pure refactor, no behaviour change. A second PSP test is coming and
would otherwise need byte-identical copies of the responder chatter,
connection setup and data helpers.
Only what would be copied verbatim moves; the test cases and everything
specific to them stay in psp.py. main()'s responder spawning becomes a
context manager, which is the one place the shape changes.
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
MAINTAINERS | 1 +
tools/testing/selftests/drivers/net/Makefile | 4 +
tools/testing/selftests/drivers/net/psp.py | 287 +++++-------------
.../testing/selftests/drivers/net/psp_lib.py | 179 +++++++++++
4 files changed, 265 insertions(+), 206 deletions(-)
create mode 100644 tools/testing/selftests/drivers/net/psp_lib.py
diff --git a/MAINTAINERS b/MAINTAINERS
index 460cb7268845..536fbc1d244d 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -21770,6 +21770,7 @@ F: include/net/psp/
F: include/net/psp.h
F: include/uapi/linux/psp.h
F: net/psp/
+F: tools/testing/selftests/drivers/net/psp*
K: struct\ psp(_assoc|_dev|hdr)\b
PSTORE FILESYSTEM
diff --git a/tools/testing/selftests/drivers/net/Makefile b/tools/testing/selftests/drivers/net/Makefile
index d5bf4cb638a8..de6e4d7f2dda 100644
--- a/tools/testing/selftests/drivers/net/Makefile
+++ b/tools/testing/selftests/drivers/net/Makefile
@@ -27,6 +27,10 @@ TEST_PROGS := \
xdp.py \
# end of TEST_PROGS
+TEST_FILES := \
+ psp_lib.py \
+ #
+
# YNL files, must be before "include ..lib.mk"
YNL_GEN_FILES := psp_responder
TEST_GEN_FILES += $(YNL_GEN_FILES)
diff --git a/tools/testing/selftests/drivers/net/psp.py b/tools/testing/selftests/drivers/net/psp.py
index 315648a770d0..766d263803b1 100755
--- a/tools/testing/selftests/drivers/net/psp.py
+++ b/tools/testing/selftests/drivers/net/psp.py
@@ -4,11 +4,8 @@
"""Test suite for PSP capable drivers."""
import errno
-import fcntl
import os
import socket
-import struct
-import termios
import time
from lib.py import defer
@@ -20,136 +17,38 @@ 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 bkg, rand_port, wait_port_listen
from lib.py import ip
-
-def _get_outq(s):
- one = b'\0' * 4
- outq = fcntl.ioctl(s.fileno(), termios.TIOCOUTQ, one)
- return struct.unpack("I", outq)[0]
-
-
-def _send_with_ack(cfg, msg):
- cfg.comm_sock.send(msg)
- response = cfg.comm_sock.recv(4)
- if response != b'ack\0':
- raise RuntimeError("Unexpected server response", response)
-
-
-def _remote_read_len(cfg):
- cfg.comm_sock.send(b'read len\0')
- return int(cfg.comm_sock.recv(1024)[:-1].decode('utf-8'))
-
-
-def _make_clr_conn(cfg, ipver=None):
- _send_with_ack(cfg, b'conn clr\0')
- remote_addr = cfg.remote_addr_v[ipver] if ipver else cfg.remote_addr
- s = socket.create_connection((remote_addr, cfg.comm_port), )
- return s
-
-
-def _make_psp_conn(cfg, version=0, ipver=None):
- _send_with_ack(cfg, b'conn psp\0' + struct.pack('BB', version, version))
- remote_addr = cfg.remote_addr_v[ipver] if ipver else cfg.remote_addr
- s = socket.create_connection((remote_addr, cfg.comm_port), )
- return s
-
-
-def _close_conn(cfg, s):
- _send_with_ack(cfg, b'data close\0')
- s.close()
+from psp_lib import check_data_rx, close_conn, get_outq, get_stat, \
+ init_psp_dev, make_clr_conn, make_psp_conn, send_careful, spi_xchg
+from psp_lib import responder as psp_responder
def _close_psp_conn(cfg, s):
- _close_conn(cfg, s)
-
-
-def _spi_xchg(s, rx):
- s.send(struct.pack('I', rx['spi']) + rx['key'])
- tx = s.recv(4 + len(rx['key']))
- return {
- 'spi': struct.unpack('I', tx[:4])[0],
- 'key': tx[4:]
- }
-
-
-def _send_careful(cfg, s, rounds):
- data = b'0123456789' * 200
- for i in range(rounds):
- n = 0
- for _ in range(10): # allow 10 retries
- try:
- n += s.send(data[n:], socket.MSG_DONTWAIT)
- if n == len(data):
- break
- except BlockingIOError:
- time.sleep(0.05)
- else:
- rlen = _remote_read_len(cfg)
- outq = _get_outq(s)
- report = f'sent: {i * len(data) + n} remote len: {rlen} outq: {outq}'
- raise RuntimeError(report)
-
- return len(data) * rounds
-
-
-def _check_data_rx(cfg, exp_len):
- read_len = -1
- for _ in range(30):
- cfg.comm_sock.send(b'read len\0')
- read_len = int(cfg.comm_sock.recv(1024)[:-1].decode('utf-8'))
- if read_len == exp_len:
- break
- time.sleep(0.01)
- ksft_eq(read_len, exp_len)
+ close_conn(cfg, s)
def _check_data_outq(s, exp_len, force_wait=False):
outq = 0
for _ in range(10):
- outq = _get_outq(s)
+ outq = get_outq(s)
if not force_wait and outq == exp_len:
break
time.sleep(0.01)
ksft_eq(outq, exp_len)
-def _get_stat(cfg, key):
- return cfg.pspnl.get_stats({'dev-id': cfg.psp_dev_id})[key]
-
#
# Test case boiler plate
#
-def _init_psp_dev(cfg, use_psp_ifindex=False):
- if not hasattr(cfg, 'psp_dev_id'):
- # Figure out which local device we are testing against
- # For NetDrvContEnv: use psp_ifindex instead of ifindex
- target_ifindex = cfg.psp_ifindex if use_psp_ifindex else cfg.ifindex
- for dev in cfg.pspnl.dev_get({}, dump=True):
- if dev['ifindex'] == target_ifindex:
- cfg.psp_info = dev
- cfg.psp_dev_id = cfg.psp_info['id']
- break
- else:
- raise KsftSkipEx("No PSP devices found")
-
- # Enable PSP if necessary
- cap = cfg.psp_info['psp-versions-cap']
- ena = cfg.psp_info['psp-versions-ena']
- if cap != ena:
- cfg.pspnl.dev_set({'id': cfg.psp_dev_id, 'psp-versions-ena': cap})
- defer(cfg.pspnl.dev_set, {'id': cfg.psp_dev_id,
- 'psp-versions-ena': ena })
-
#
# Test cases
#
def dev_list_devices(cfg):
""" Dump all devices """
- _init_psp_dev(cfg)
+ init_psp_dev(cfg)
devices = cfg.pspnl.dev_get({}, dump=True)
@@ -161,7 +60,7 @@ from lib.py import ip
def dev_get_device(cfg):
""" Get the device we intend to use """
- _init_psp_dev(cfg)
+ init_psp_dev(cfg)
dev = cfg.pspnl.dev_get({'id': cfg.psp_dev_id})
ksft_eq(dev['id'], cfg.psp_dev_id)
@@ -180,22 +79,22 @@ from lib.py import ip
def dev_rotate(cfg):
""" Test key rotation """
- _init_psp_dev(cfg)
+ init_psp_dev(cfg)
- prev_rotations = _get_stat(cfg, 'key-rotations')
+ prev_rotations = get_stat(cfg, 'key-rotations')
rot = cfg.pspnl.key_rotate({"id": cfg.psp_dev_id})
ksft_eq(rot['id'], cfg.psp_dev_id)
rot = cfg.pspnl.key_rotate({"id": cfg.psp_dev_id})
ksft_eq(rot['id'], cfg.psp_dev_id)
- cur_rotations = _get_stat(cfg, 'key-rotations')
+ cur_rotations = get_stat(cfg, 'key-rotations')
ksft_eq(cur_rotations, prev_rotations + 2)
def dev_rotate_spi(cfg):
""" Test key rotation and SPI check """
- _init_psp_dev(cfg)
+ init_psp_dev(cfg)
top_a = top_b = 0
with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as s:
@@ -217,7 +116,7 @@ from lib.py import ip
def assoc_basic(cfg):
""" Test creating associations """
- _init_psp_dev(cfg)
+ init_psp_dev(cfg)
with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as s:
assoc = cfg.pspnl.rx_assoc({"version": 0,
@@ -237,7 +136,7 @@ from lib.py import ip
def assoc_bad_dev(cfg):
""" Test creating associations with bad device ID """
- _init_psp_dev(cfg)
+ init_psp_dev(cfg)
with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as s:
with ksft_raises(NlError) as cm:
@@ -249,23 +148,23 @@ from lib.py import ip
def assoc_sk_only_conn(cfg):
""" Test creating associations based on socket """
- _init_psp_dev(cfg)
+ init_psp_dev(cfg)
- with _make_clr_conn(cfg) as s:
+ with make_clr_conn(cfg) as s:
assoc = cfg.pspnl.rx_assoc({"version": 0,
"sock-fd": s.fileno()})
ksft_eq(assoc['dev-id'], cfg.psp_dev_id)
cfg.pspnl.tx_assoc({"version": 0,
"tx-key": assoc['rx-key'],
"sock-fd": s.fileno()})
- _close_conn(cfg, s)
+ close_conn(cfg, s)
def assoc_sk_only_mismatch(cfg):
""" Test creating associations based on socket (dev mismatch) """
- _init_psp_dev(cfg)
+ init_psp_dev(cfg)
- with _make_clr_conn(cfg) as s:
+ with make_clr_conn(cfg) as s:
with ksft_raises(NlError) as cm:
cfg.pspnl.rx_assoc({"version": 0,
"dev-id": cfg.psp_dev_id + 1234567,
@@ -273,14 +172,14 @@ from lib.py import ip
the_exception = cm.exception
ksft_eq(the_exception.nl_msg.extack['bad-attr'], ".dev-id")
ksft_eq(the_exception.nl_msg.error, -errno.EINVAL)
- _close_conn(cfg, s)
+ close_conn(cfg, s)
def assoc_sk_only_mismatch_tx(cfg):
""" Test creating associations based on socket (dev mismatch) """
- _init_psp_dev(cfg)
+ init_psp_dev(cfg)
- with _make_clr_conn(cfg) as s:
+ with make_clr_conn(cfg) as s:
with ksft_raises(NlError) as cm:
assoc = cfg.pspnl.rx_assoc({"version": 0,
"sock-fd": s.fileno()})
@@ -291,12 +190,12 @@ from lib.py import ip
the_exception = cm.exception
ksft_eq(the_exception.nl_msg.extack['bad-attr'], ".dev-id")
ksft_eq(the_exception.nl_msg.error, -errno.EINVAL)
- _close_conn(cfg, s)
+ close_conn(cfg, s)
def assoc_sk_only_unconn(cfg):
""" Test creating associations based on socket (unconnected, should fail) """
- _init_psp_dev(cfg)
+ init_psp_dev(cfg)
with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as s:
with ksft_raises(NlError) as cm:
@@ -309,7 +208,7 @@ from lib.py import ip
def assoc_version_mismatch(cfg):
""" Test creating associations where Rx and Tx PSP versions do not match """
- _init_psp_dev(cfg)
+ init_psp_dev(cfg)
versions = list(cfg.psp_info['psp-versions-cap'])
if len(versions) < 2:
@@ -335,7 +234,7 @@ from lib.py import ip
def assoc_twice(cfg):
""" Test reusing Tx assoc for two sockets """
- _init_psp_dev(cfg)
+ init_psp_dev(cfg)
def rx_assoc_check(s):
assoc = cfg.pspnl.rx_assoc({"version": 0,
@@ -369,7 +268,7 @@ from lib.py import ip
def _data_basic_send(cfg, version, ipver):
""" Test basic data send """
- _init_psp_dev(cfg)
+ init_psp_dev(cfg)
# Version 0 is required by spec, don't let it skip
if version:
@@ -383,21 +282,21 @@ from lib.py import ip
ksft_eq(cm.exception.nl_msg.error, -errno.EOPNOTSUPP)
raise KsftSkipEx("PSP version not supported", name)
- s = _make_psp_conn(cfg, version, ipver)
+ s = make_psp_conn(cfg, version, ipver)
rx_assoc = cfg.pspnl.rx_assoc({"version": version,
"dev-id": cfg.psp_dev_id,
"sock-fd": s.fileno()})
rx = rx_assoc['rx-key']
- tx = _spi_xchg(s, rx)
+ tx = spi_xchg(s, rx)
cfg.pspnl.tx_assoc({"dev-id": cfg.psp_dev_id,
"version": version,
"tx-key": tx,
"sock-fd": s.fileno()})
- data_len = _send_careful(cfg, s, 100)
- _check_data_rx(cfg, data_len)
+ data_len = send_careful(cfg, s, 100)
+ check_data_rx(cfg, data_len)
_close_psp_conn(cfg, s)
@@ -410,63 +309,63 @@ from lib.py import ip
"tx-key": tx,
"sock-fd": s.fileno()})
- data_len = _send_careful(cfg, s, 20)
+ data_len = send_careful(cfg, s, 20)
_check_data_outq(s, data_len, force_wait=True)
- _check_data_rx(cfg, 0)
+ check_data_rx(cfg, 0)
_close_psp_conn(cfg, s)
def data_send_bad_key(cfg):
""" Test send data with bad key """
- _init_psp_dev(cfg)
+ init_psp_dev(cfg)
- s = _make_psp_conn(cfg)
+ s = make_psp_conn(cfg)
rx_assoc = cfg.pspnl.rx_assoc({"version": 0,
"dev-id": cfg.psp_dev_id,
"sock-fd": s.fileno()})
rx = rx_assoc['rx-key']
- tx = _spi_xchg(s, rx)
+ tx = spi_xchg(s, rx)
tx['key'] = (tx['key'][0] ^ 0xff).to_bytes(1, 'little') + tx['key'][1:]
__bad_xfer_do(cfg, s, tx)
def data_send_disconnect(cfg):
""" Test socket close after sending data """
- _init_psp_dev(cfg)
+ init_psp_dev(cfg)
- with _make_psp_conn(cfg) as s:
+ with make_psp_conn(cfg) as s:
assoc = cfg.pspnl.rx_assoc({"version": 0,
"sock-fd": s.fileno()})
- tx = _spi_xchg(s, assoc['rx-key'])
+ tx = spi_xchg(s, assoc['rx-key'])
cfg.pspnl.tx_assoc({"version": 0,
"tx-key": tx,
"sock-fd": s.fileno()})
- data_len = _send_careful(cfg, s, 100)
- _check_data_rx(cfg, data_len)
+ data_len = send_careful(cfg, s, 100)
+ check_data_rx(cfg, data_len)
s.shutdown(socket.SHUT_RDWR)
s.close()
def _data_mss_adjust(cfg, ipver):
- _init_psp_dev(cfg)
+ init_psp_dev(cfg)
# First figure out what the MSS would be without any adjustments
- s = _make_clr_conn(cfg, ipver)
+ s = make_clr_conn(cfg, ipver)
s.send(b"0123456789abcdef" * 1024)
- _check_data_rx(cfg, 16 * 1024)
+ check_data_rx(cfg, 16 * 1024)
mss = s.getsockopt(socket.IPPROTO_TCP, socket.TCP_MAXSEG)
- _close_conn(cfg, s)
+ close_conn(cfg, s)
- s = _make_psp_conn(cfg, 0, ipver)
+ s = make_psp_conn(cfg, 0, ipver)
try:
rx_assoc = cfg.pspnl.rx_assoc({"version": 0,
"dev-id": cfg.psp_dev_id,
"sock-fd": s.fileno()})
rx = rx_assoc['rx-key']
- tx = _spi_xchg(s, rx)
+ tx = spi_xchg(s, rx)
rxmss = s.getsockopt(socket.IPPROTO_TCP, socket.TCP_MAXSEG)
ksft_eq(mss, rxmss)
@@ -479,8 +378,8 @@ from lib.py import ip
txmss = s.getsockopt(socket.IPPROTO_TCP, socket.TCP_MAXSEG)
ksft_eq(mss, txmss + 40)
- data_len = _send_careful(cfg, s, 100)
- _check_data_rx(cfg, data_len)
+ data_len = send_careful(cfg, s, 100)
+ check_data_rx(cfg, data_len)
_check_data_outq(s, 0)
txmss = s.getsockopt(socket.IPPROTO_TCP, socket.TCP_MAXSEG)
@@ -491,30 +390,30 @@ from lib.py import ip
def data_stale_key(cfg):
""" Test send on a double-rotated key """
- _init_psp_dev(cfg)
+ init_psp_dev(cfg)
- prev_stale = _get_stat(cfg, 'stale-events')
- s = _make_psp_conn(cfg)
+ prev_stale = get_stat(cfg, 'stale-events')
+ s = make_psp_conn(cfg)
try:
rx_assoc = cfg.pspnl.rx_assoc({"version": 0,
"dev-id": cfg.psp_dev_id,
"sock-fd": s.fileno()})
rx = rx_assoc['rx-key']
- tx = _spi_xchg(s, rx)
+ 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, 100)
- _check_data_rx(cfg, data_len)
+ data_len = send_careful(cfg, s, 100)
+ check_data_rx(cfg, data_len)
_check_data_outq(s, 0)
cfg.pspnl.key_rotate({"id": cfg.psp_dev_id})
cfg.pspnl.key_rotate({"id": cfg.psp_dev_id})
- cur_stale = _get_stat(cfg, 'stale-events')
+ cur_stale = get_stat(cfg, 'stale-events')
ksft_gt(cur_stale, prev_stale)
s.send(b'0123456789' * 200)
@@ -544,7 +443,7 @@ from lib.py import ip
# netdevsim only for now
cfg.require_nsim()
- s = _make_clr_conn(cfg)
+ s = make_clr_conn(cfg)
try:
rx_assoc = cfg.pspnl.rx_assoc({"version": 0,
"dev-id": cfg.psp_dev_id,
@@ -553,7 +452,7 @@ from lib.py import ip
__nsim_psp_rereg(cfg)
finally:
- _close_conn(cfg, s)
+ close_conn(cfg, s)
def removal_device_bi(cfg):
@@ -564,7 +463,7 @@ from lib.py import ip
# netdevsim only for now
cfg.require_nsim()
- s = _make_clr_conn(cfg)
+ s = make_clr_conn(cfg)
try:
rx_assoc = cfg.pspnl.rx_assoc({"version": 0,
"dev-id": cfg.psp_dev_id,
@@ -575,7 +474,7 @@ from lib.py import ip
"sock-fd": s.fileno()})
__nsim_psp_rereg(cfg)
finally:
- _close_conn(cfg, s)
+ close_conn(cfg, s)
def _get_psp_ver_ip_variants():
@@ -631,21 +530,21 @@ from lib.py import ip
with NetNSEnter(cfg.netns.name):
cfg.pspnl = PSPFamily()
- sock = _make_psp_conn(cfg, version, ipver)
+ sock = make_psp_conn(cfg, version, ipver)
rx_assoc = cfg.pspnl.rx_assoc({"version": version,
"dev-id": cfg.psp_dev_id,
"sock-fd": sock.fileno()})
rx_key = rx_assoc['rx-key']
- tx_key = _spi_xchg(sock, rx_key)
+ tx_key = spi_xchg(sock, rx_key)
cfg.pspnl.tx_assoc({"dev-id": cfg.psp_dev_id,
"version": version,
"tx-key": tx_key,
"sock-fd": sock.fileno()})
- data_len = _send_careful(cfg, sock, 100)
- _check_data_rx(cfg, data_len)
+ data_len = send_careful(cfg, sock, 100)
+ check_data_rx(cfg, data_len)
_close_psp_conn(cfg, sock)
@@ -766,7 +665,7 @@ from lib.py import ip
def _dev_assoc_no_nsid(cfg):
""" Test dev-assoc and dev-disassoc without nsid attribute """
- _init_psp_dev(cfg, True)
+ init_psp_dev(cfg, True)
# Associate without nsid - should look up ifindex in caller's netns
cfg.pspnl.dev_assoc({'id': cfg.psp_dev_id,
@@ -800,7 +699,7 @@ from lib.py import ip
Creates a disposable netkit pair for this test to avoid destroying
the shared environment.
"""
- _init_psp_dev(cfg, True)
+ init_psp_dev(cfg, True)
defer(delattr, cfg, 'psp_dev_id')
defer(delattr, cfg, 'psp_info')
@@ -877,7 +776,7 @@ from lib.py import ip
def _assoc_nk_guest(cfg):
"""Associate nk_guest with PSP device and register cleanup via defer()."""
- _init_psp_dev(cfg, True)
+ init_psp_dev(cfg, True)
cfg.pspnl.dev_assoc({'id': cfg.psp_dev_id,
'ifindex': cfg.nk_guest_ifindex,
@@ -937,7 +836,6 @@ from lib.py import ip
cfg.psp_dev_peer_nsid = _get_nsid(cfg.netns.name)
-
def main() -> None:
""" Ksft boiler plate main """
@@ -960,46 +858,23 @@ from lib.py import ip
# Set up responder and communication sock
# psp_responder runs in _netns (remote namespace with psp_dev_peer)
- responder = cfg.remote.deploy("psp_responder")
+ with psp_responder(cfg):
+ cases = [data_basic_send, data_mss_adjust]
- cfg.comm_port = rand_port()
- srv = None
- try:
- with bkg(responder + f" -p {cfg.comm_port} -i {cfg.remote_ifindex}",
- host=cfg.remote, exit_wait=True) as srv:
- wait_port_listen(cfg.comm_port, host=cfg.remote)
+ if has_cont:
+ cases += [
+ _assoc_check_list,
+ data_basic_send_netkit_psp_assoc,
+ _key_rotation_notify_multi_ns_netkit,
+ _dev_change_notify_multi_ns_netkit,
+ _psp_dev_get_check_netkit_psp_assoc,
+ _dev_assoc_no_nsid,
+ _psp_dev_assoc_cleanup_on_netkit_del,
+ ]
- cfg.comm_sock = socket.create_connection((cfg.remote_addr,
- cfg.comm_port),
- timeout=1)
-
- cases = [data_basic_send, data_mss_adjust]
-
- if has_cont:
- cases += [
- _assoc_check_list,
- data_basic_send_netkit_psp_assoc,
- _key_rotation_notify_multi_ns_netkit,
- _dev_change_notify_multi_ns_netkit,
- _psp_dev_get_check_netkit_psp_assoc,
- _dev_assoc_no_nsid,
- _psp_dev_assoc_cleanup_on_netkit_del,
- ]
-
- ksft_run(cases=cases, globs=globals(),
- case_pfx={"dev_", "data_", "assoc_", "removal_"},
- args=(cfg, ))
-
- cfg.comm_sock.send(b"exit\0")
- cfg.comm_sock.close()
- finally:
- if srv and (srv.stdout or srv.stderr):
- ksft_pr("")
- ksft_pr(f"Responder logs ({srv.ret}):")
- if srv and srv.stdout:
- ksft_pr("STDOUT:\n# " + srv.stdout.strip().replace("\n", "\n# "))
- if srv and srv.stderr:
- ksft_pr("STDERR:\n# " + srv.stderr.strip().replace("\n", "\n# "))
+ ksft_run(cases=cases, globs=globals(),
+ case_pfx={"dev_", "data_", "assoc_"},
+ args=(cfg, ))
ksft_exit()
diff --git a/tools/testing/selftests/drivers/net/psp_lib.py b/tools/testing/selftests/drivers/net/psp_lib.py
new file mode 100644
index 000000000000..1fc4bff84fb1
--- /dev/null
+++ b/tools/testing/selftests/drivers/net/psp_lib.py
@@ -0,0 +1,179 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+
+"""
+Helpers shared by the PSP tests.
+
+Only code which the tests would otherwise have to copy verbatim belongs
+here, mostly talking to psp_responder on the other end of the link.
+"""
+
+import fcntl
+import socket
+import struct
+import termios
+import time
+from contextlib import contextmanager
+
+from lib.py import defer
+from lib.py import ksft_eq, ksft_pr
+from lib.py import KsftSkipEx, KsftFailEx
+from lib.py import bkg, rand_port, wait_port_listen
+
+
+def get_outq(s):
+ one = b'\0' * 4
+ outq = fcntl.ioctl(s.fileno(), termios.TIOCOUTQ, one)
+ return struct.unpack("I", outq)[0]
+
+
+def send_with_ack(cfg, msg):
+ cfg.comm_sock.send(msg)
+ response = cfg.comm_sock.recv(4)
+ if response != b'ack\0':
+ raise RuntimeError("Unexpected server response", response)
+
+
+def remote_read_len(cfg):
+ cfg.comm_sock.send(b'read len\0')
+ return int(cfg.comm_sock.recv(1024)[:-1].decode('utf-8'))
+
+
+def make_clr_conn(cfg, ipver=None):
+ send_with_ack(cfg, b'conn clr\0')
+ remote_addr = cfg.remote_addr_v[ipver] if ipver else cfg.remote_addr
+ s = socket.create_connection((remote_addr, cfg.comm_port), )
+ return s
+
+
+def make_psp_conn(cfg, version=0, ipver=None):
+ send_with_ack(cfg, b'conn psp\0' + struct.pack('BB', version, version))
+ remote_addr = cfg.remote_addr_v[ipver] if ipver else cfg.remote_addr
+ s = socket.create_connection((remote_addr, cfg.comm_port), )
+ return s
+
+
+def close_conn(cfg, s):
+ send_with_ack(cfg, b'data close\0')
+ s.close()
+
+
+def spi_xchg(s, rx):
+ s.send(struct.pack('I', rx['spi']) + rx['key'])
+ tx = s.recv(4 + len(rx['key']))
+ return {
+ 'spi': struct.unpack('I', tx[:4])[0],
+ 'key': tx[4:]
+ }
+
+
+def send_careful(cfg, s, rounds):
+ data = b'0123456789' * 200
+ for i in range(rounds):
+ n = 0
+ for _ in range(10): # allow 10 retries
+ try:
+ n += s.send(data[n:], socket.MSG_DONTWAIT)
+ if n == len(data):
+ break
+ except BlockingIOError:
+ time.sleep(0.05)
+ else:
+ rlen = remote_read_len(cfg)
+ outq = get_outq(s)
+ report = f'sent: {i * len(data) + n} remote len: {rlen} outq: {outq}'
+ raise RuntimeError(report)
+
+ return len(data) * rounds
+
+
+def check_data_rx(cfg, exp_len):
+ read_len = -1
+ for _ in range(30):
+ cfg.comm_sock.send(b'read len\0')
+ read_len = int(cfg.comm_sock.recv(1024)[:-1].decode('utf-8'))
+ if read_len == exp_len:
+ break
+ time.sleep(0.01)
+ ksft_eq(read_len, exp_len)
+
+
+def get_stat(cfg, key):
+ return cfg.pspnl.get_stats({'dev-id': cfg.psp_dev_id})[key]
+
+def init_psp_dev(cfg, use_psp_ifindex=False):
+ if not hasattr(cfg, 'psp_dev_id'):
+ # Figure out which local device we are testing against
+ # For NetDrvContEnv: use psp_ifindex instead of ifindex
+ target_ifindex = cfg.psp_ifindex if use_psp_ifindex else cfg.ifindex
+ for dev in cfg.pspnl.dev_get({}, dump=True):
+ if dev['ifindex'] == target_ifindex:
+ cfg.psp_info = dev
+ cfg.psp_dev_id = cfg.psp_info['id']
+ break
+ else:
+ raise KsftSkipEx("No PSP devices found")
+
+ # Enable PSP if necessary
+ cap = cfg.psp_info['psp-versions-cap']
+ ena = cfg.psp_info['psp-versions-ena']
+ if cap != ena:
+ cfg.pspnl.dev_set({'id': cfg.psp_dev_id, 'psp-versions-ena': cap})
+ defer(cfg.pspnl.dev_set, {'id': cfg.psp_dev_id,
+ 'psp-versions-ena': ena })
+
+
+def recv_careful(s, target, rounds=100):
+ """Read exactly target bytes, tolerating short reads"""
+ data = b''
+ for _ in range(rounds):
+ try:
+ data += s.recv(target - len(data), socket.MSG_DONTWAIT)
+ if len(data) == target:
+ return data
+ except BlockingIOError:
+ time.sleep(0.001)
+ raise KsftFailEx(f"short read, got {len(data)} of {target} bytes")
+
+
+def req_echo(cfg, s):
+ """Ask the peer to echo, and check the reply arrives intact"""
+ send_with_ack(cfg, b'data echo\0')
+ ksft_eq(recv_careful(s, 5), b'echo\0')
+
+
+def psp_txrx(cfg, s, rounds, sent=0):
+ """Send data both ways, and return the total bytes sent to the peer"""
+ sent += send_careful(cfg, s, rounds)
+ check_data_rx(cfg, sent)
+ req_echo(cfg, s)
+ return sent
+
+
+@contextmanager
+def responder(cfg):
+ """Run psp_responder on the remote end and open the comm socket to it"""
+ binary = cfg.remote.deploy("psp_responder")
+
+ cfg.comm_port = rand_port()
+ srv = None
+ try:
+ with bkg(binary + f" -p {cfg.comm_port} -i {cfg.remote_ifindex}",
+ host=cfg.remote, exit_wait=True) as srv:
+ wait_port_listen(cfg.comm_port, host=cfg.remote)
+
+ cfg.comm_sock = socket.create_connection((cfg.remote_addr,
+ cfg.comm_port),
+ timeout=1)
+ yield cfg
+
+ cfg.comm_sock.send(b"exit\0")
+ cfg.comm_sock.close()
+ finally:
+ if srv and (srv.stdout or srv.stderr):
+ ksft_pr("")
+ ksft_pr(f"Responder logs ({srv.ret}):")
+ if srv and srv.stdout:
+ ksft_pr("STDOUT:\n# " + srv.stdout.strip().replace("\n", "\n# "))
+ if srv and srv.stderr:
+ ksft_pr("STDERR:\n# " + srv.stderr.strip().replace("\n", "\n# "))
--
2.55.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [RFC net-next 4/6] selftests: drv-net: psp_steer: test PSP VC based queue steering
2026-08-22 22:55 [RFC net-next 0/6] psp: use virt cookie as Rx steering hint Jakub Kicinski
` (2 preceding siblings ...)
2026-08-22 22:55 ` [RFC net-next 3/6] selftests: drv-net: psp: move the PSP test plumbing into psp_lib.py Jakub Kicinski
@ 2026-08-22 22:55 ` Jakub Kicinski
2026-08-22 22:55 ` [RFC net-next 5/6] selftests: drv-net: psp_steer: test where PSP steering sits in the Rx pipeline Jakub Kicinski
` (2 subsequent siblings)
6 siblings, 0 replies; 18+ messages in thread
From: Jakub Kicinski @ 2026-08-22 22:55 UTC (permalink / raw)
To: daniel.zahka, willemdebruijn.kernel
Cc: edumazet, cratiu, borisp, kuniyu, netdev, Jakub Kicinski
Cover the netlink interface for PSP VC steering.
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
tools/testing/selftests/drivers/net/Makefile | 1 +
.../selftests/drivers/net/psp_responder.c | 42 +++
.../testing/selftests/drivers/net/psp_lib.py | 5 +
.../selftests/drivers/net/psp_steer.py | 256 ++++++++++++++++++
4 files changed, 304 insertions(+)
create mode 100644 tools/testing/selftests/drivers/net/psp_steer.py
diff --git a/tools/testing/selftests/drivers/net/Makefile b/tools/testing/selftests/drivers/net/Makefile
index de6e4d7f2dda..e8719fc106ba 100644
--- a/tools/testing/selftests/drivers/net/Makefile
+++ b/tools/testing/selftests/drivers/net/Makefile
@@ -19,6 +19,7 @@ TEST_PROGS := \
netpoll_basic.py \
ping.py \
psp.py \
+ psp_steer.py \
queues.py \
ring_reconfig.py \
shaper.py \
diff --git a/tools/testing/selftests/drivers/net/psp_responder.c b/tools/testing/selftests/drivers/net/psp_responder.c
index a26e7628bbb1..f8df6b8da9b4 100644
--- a/tools/testing/selftests/drivers/net/psp_responder.c
+++ b/tools/testing/selftests/drivers/net/psp_responder.c
@@ -23,6 +23,7 @@ static bool should_quit;
struct opts {
int port;
int ifindex;
+ int devid;
bool verbose;
};
@@ -118,6 +119,36 @@ static void send_str(int sock, int value)
send(sock, buf, ret + 1, MSG_WAITALL);
}
+static void
+handle_dev_steer(struct ynl_sock *ys, struct opts *opts, char *data,
+ int comm_sock)
+{
+ struct psp_dev_set_req *req;
+ struct psp_dev_set_rsp *rsp;
+
+ if (opts->devid < 0) {
+ fprintf(stderr, "WARN: dev steer but no PSP device\n");
+ send_err(comm_sock);
+ return;
+ }
+
+ req = psp_dev_set_req_alloc();
+
+ psp_dev_set_req_set_id(req, opts->devid);
+ psp_dev_set_req_set_vc_steer_ena(req, *data);
+
+ rsp = psp_dev_set(ys, req);
+ psp_dev_set_req_free(req);
+ if (!rsp) {
+ perror("ERROR: failed to set device features");
+ send_err(comm_sock);
+ return;
+ }
+ psp_dev_set_rsp_free(rsp);
+
+ send_ack(comm_sock);
+}
+
static void
run_session(struct ynl_sock *ys, struct opts *opts,
int server_sock, int comm_sock)
@@ -210,6 +241,9 @@ run_session(struct ynl_sock *ys, struct opts *opts,
match; \
})
+#define cmd_w_msg(_name, _type) \
+ (off >= sizeof(_name) + sizeof(_type) && cmd(_name))
+
do {
consumed = false;
@@ -224,6 +258,11 @@ run_session(struct ynl_sock *ys, struct opts *opts,
fprintf(stderr, "WARN: echo but no data sock\n");
send_ack(comm_sock);
}
+ if (cmd_w_msg("dev steer", __u8)) {
+ handle_dev_steer(ys, opts, buf,
+ comm_sock);
+ __consume(sizeof(__u8));
+ }
if (cmd("data close")) {
if (data_sock >= 0) {
close(data_sock);
@@ -254,6 +293,7 @@ run_session(struct ynl_sock *ys, struct opts *opts,
}
if (cmd("exit"))
should_quit = true;
+#undef cmd_w_msg
#undef cmd
if (!consumed) {
@@ -461,6 +501,8 @@ int main(int argc, char **argv)
goto err_close;
}
+ opts.devid = devid;
+
ret = run_responder(ys, &opts);
if (devid >= 0 && ver_ena != ver_cap &&
diff --git a/tools/testing/selftests/drivers/net/psp_lib.py b/tools/testing/selftests/drivers/net/psp_lib.py
index 1fc4bff84fb1..b2bf205ed327 100644
--- a/tools/testing/selftests/drivers/net/psp_lib.py
+++ b/tools/testing/selftests/drivers/net/psp_lib.py
@@ -58,6 +58,11 @@ from lib.py import bkg, rand_port, wait_port_listen
s.close()
+def remote_dev_steer(cfg, mode):
+ """Set vc-steer-ena on the remote PSP device"""
+ send_with_ack(cfg, b'dev steer\0' + struct.pack('B', mode))
+
+
def spi_xchg(s, rx):
s.send(struct.pack('I', rx['spi']) + rx['key'])
tx = s.recv(4 + len(rx['key']))
diff --git a/tools/testing/selftests/drivers/net/psp_steer.py b/tools/testing/selftests/drivers/net/psp_steer.py
new file mode 100644
index 000000000000..0401a7c359e7
--- /dev/null
+++ b/tools/testing/selftests/drivers/net/psp_steer.py
@@ -0,0 +1,256 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+
+"""Test suite for PSP virtualization cookie based Rx queue steering."""
+
+import errno
+import os
+import socket
+
+from lib.py import defer
+from lib.py import ksft_run, ksft_exit
+from lib.py import ksft_eq, ksft_ge, ksft_in, ksft_ne, ksft_raises
+from lib.py import KsftSkipEx
+from lib.py import NetDrvEpEnv
+from lib.py import NetdevFamily, NlError, PSPFamily
+
+from psp_lib import close_conn, init_psp_dev, make_psp_conn, psp_txrx, \
+ remote_conn_steer, remote_dev_steer, spi_xchg
+from psp_lib import responder as psp_responder
+
+# Not exposed by the socket module
+_SO_INCOMING_NAPI_ID = 56
+
+_VC_TX = 1 << 0
+_VC_RX = 1 << 1
+_VC_BOTH = _VC_TX | _VC_RX
+_VC_SIZE = 8
+
+
+def _require_steer(cfg):
+ """Skip unless the device can do VC steering"""
+ init_psp_dev(cfg)
+
+ if 'vc-steer-cap' not in cfg.psp_info:
+ raise KsftSkipEx("Device does not support PSP VC steering")
+
+
+def _require_queues(cfg, cnt):
+ if cfg.rx_queue_cnt < cnt or cfg.tx_queue_cnt < cnt:
+ raise KsftSkipEx(f"Test needs at least {cnt} Rx and Tx queues")
+
+
+def _set_steer(cfg, mode):
+ """Set vc-steer-ena locally for the duration of the test case"""
+ dev = cfg.pspnl.dev_get({'id': cfg.psp_dev_id})
+ prev = dev['vc-steer-ena']
+
+ cfg.pspnl.dev_set({'id': cfg.psp_dev_id, 'vc-steer-ena': mode})
+ defer(cfg.pspnl.dev_set, {'id': cfg.psp_dev_id, 'vc-steer-ena': prev})
+
+
+def _set_remote_steer(cfg, mode):
+ remote_dev_steer(cfg, mode)
+ defer(remote_dev_steer, cfg, 0)
+
+
+def _enable_steer(cfg, local=_VC_BOTH, remote=_VC_BOTH):
+ """Turn steering on at both ends for the duration of the test case"""
+ _set_steer(cfg, local)
+ if remote is not None:
+ _set_remote_steer(cfg, remote)
+
+
+def _mss(s):
+ return s.getsockopt(socket.IPPROTO_TCP, socket.TCP_MAXSEG)
+
+
+def _force_tx_queue(cfg, qid):
+ """Point XPS at a single Tx queue, so we know the flow's Tx queue
+
+ The queue we ask the peer to steer us to is taken from the Tx queue
+ the stack picks for the flow, so pinning XPS is what makes the
+ outcome predictable.
+ """
+ all_cpus = f'{(1 << os.cpu_count()) - 1:x}'
+ for i in range(cfg.tx_queue_cnt):
+ mask = all_cpus if i == qid else '0'
+ with open(f'/sys/class/net/{cfg.ifname}/queues/tx-{i}/xps_cpus',
+ 'w', encoding='ascii') as fp:
+ fp.write(mask)
+
+
+def _psp_conn(cfg):
+ """Open a PSP connection, whatever the device is configured for"""
+ s = make_psp_conn(cfg)
+
+ rx = cfg.pspnl.rx_assoc({'version': 0, 'dev-id': cfg.psp_dev_id,
+ 'sock-fd': s.fileno()})
+ tx = spi_xchg(s, rx['rx-key'])
+ cfg.pspnl.tx_assoc({'dev-id': cfg.psp_dev_id, 'version': 0,
+ 'tx-key': tx, 'sock-fd': s.fileno()})
+ return s
+
+
+def _settled_rx_queue(cfg, s, sent):
+ """Run traffic until the peer picked our request up, report the queue
+
+ The first exchange carries our request to the peer, the second comes
+ back already steered.
+ """
+ sent = psp_txrx(cfg, s, 1, sent)
+ sent = psp_txrx(cfg, s, 1, sent)
+
+ napi_id = s.getsockopt(socket.SOL_SOCKET, _SO_INCOMING_NAPI_ID)
+ ksft_ne(napi_id, 0, comment="socket saw no traffic?")
+ ksft_in(napi_id, cfg.napi2queue, comment="unknown NAPI id")
+ return cfg.napi2queue[napi_id], sent
+
+
+#
+# Test cases
+#
+
+def dev_feature_toggle(cfg):
+ """ Set each direction in turn, check it is reported back """
+ _require_steer(cfg)
+
+ dev = cfg.pspnl.dev_get({'id': cfg.psp_dev_id})
+ defer(cfg.pspnl.dev_set, {'id': cfg.psp_dev_id,
+ 'vc-steer-ena': dev['vc-steer-ena']})
+
+ for mode in ({'tx'}, {'rx'}, {'tx', 'rx'}, set()):
+ cfg.pspnl.dev_set({'id': cfg.psp_dev_id, 'vc-steer-ena': mode})
+ dev = cfg.pspnl.dev_get({'id': cfg.psp_dev_id})
+ ksft_eq(dev['vc-steer-ena'], mode)
+
+
+def dev_feature_tx_needs_no_cap(cfg):
+ """ Granting a peer's request must not depend on the device """
+ init_psp_dev(cfg)
+
+ dev = cfg.pspnl.dev_get({'id': cfg.psp_dev_id})
+ defer(cfg.pspnl.dev_set, {'id': cfg.psp_dev_id,
+ 'vc-steer-ena': dev['vc-steer-ena']})
+
+ cfg.pspnl.dev_set({'id': cfg.psp_dev_id, 'vc-steer-ena': {'tx'}})
+ dev = cfg.pspnl.dev_get({'id': cfg.psp_dev_id})
+ ksft_eq(dev['vc-steer-ena'], {'tx'})
+
+
+def dev_feature_rx_needs_cap(cfg):
+ """ Steering our own Rx does need the device to play along """
+ init_psp_dev(cfg)
+
+ if 'vc-steer-cap' in cfg.psp_info:
+ raise KsftSkipEx("Device can steer, nothing to reject")
+
+ with ksft_raises(NlError) as cm:
+ cfg.pspnl.dev_set({'id': cfg.psp_dev_id, 'vc-steer-ena': {'rx'}})
+ ksft_eq(cm.exception.nl_msg.error, -errno.EOPNOTSUPP)
+
+
+def dev_feature_bad_value(cfg):
+ """ Only the two direction bits are valid """
+ _require_steer(cfg)
+
+ with ksft_raises(NlError) as cm:
+ cfg.pspnl.dev_set({'id': cfg.psp_dev_id, 'vc-steer-ena': 0xdeadbeef})
+ ksft_eq(cm.exception.nl_msg.error, -errno.EINVAL)
+
+
+def data_mss_adjust(cfg):
+ """ The cookie is 8B of extra header, the MSS has to account for it """
+ _require_steer(cfg)
+
+ _set_steer(cfg, 0)
+ with _psp_conn(cfg) as s:
+ plain = _mss(s)
+ close_conn(cfg, s)
+
+ # Either direction puts a cookie in every header we send
+ for mode in (_VC_TX, _VC_RX, _VC_BOTH):
+ _set_steer(cfg, mode)
+ with _psp_conn(cfg) as s:
+ ksft_eq(plain - _mss(s), _VC_SIZE, comment=f"mode {mode}")
+ close_conn(cfg, s)
+
+
+def data_mss_sampled_at_assoc(cfg):
+ """ Turning steering on must not resize a live connection's header """
+ _require_steer(cfg)
+
+ _set_steer(cfg, 0)
+ with _psp_conn(cfg) as s:
+ before = _mss(s)
+ _set_steer(cfg, _VC_BOTH)
+ ksft_eq(_mss(s), before)
+ close_conn(cfg, s)
+
+
+def data_steer_follows_tx_queue(cfg):
+ """ Traffic must land on the Rx queue paired with our Tx queue """
+ _require_steer(cfg)
+ _require_queues(cfg, 3)
+ _enable_steer(cfg)
+
+ defer(_force_tx_queue, cfg, -1)
+
+ with _psp_conn(cfg) as s:
+ sent = 0
+ for qid in (1, 2):
+ _force_tx_queue(cfg, qid)
+ qid_seen, sent = _settled_rx_queue(cfg, s, sent)
+ ksft_eq(qid_seen, qid)
+
+ close_conn(cfg, s)
+
+
+def data_steer_one_sided(cfg):
+ """ We ask, the peer only grants: our Rx still gets steered """
+ _require_steer(cfg)
+ _require_queues(cfg, 2)
+ _enable_steer(cfg, local=_VC_RX, remote=_VC_TX)
+
+ defer(_force_tx_queue, cfg, -1)
+ _force_tx_queue(cfg, 1)
+
+ with _psp_conn(cfg) as s:
+ qid, _ = _settled_rx_queue(cfg, s, 0)
+ ksft_eq(qid, 1)
+ close_conn(cfg, s)
+
+
+def _queue_info(cfg):
+ """Map NAPI ids to Rx queue ids, and count the queues"""
+ netnl = NetdevFamily()
+ queues = netnl.queue_get({'ifindex': cfg.ifindex}, dump=True)
+
+ cfg.napi2queue = {}
+ cfg.rx_queue_cnt = 0
+ cfg.tx_queue_cnt = 0
+ for q in queues:
+ if q['type'] == 'rx':
+ cfg.rx_queue_cnt += 1
+ if 'napi-id' in q:
+ cfg.napi2queue[q['napi-id']] = q['id']
+ elif q['type'] == 'tx':
+ cfg.tx_queue_cnt += 1
+
+
+def main() -> None:
+ """ Ksft boiler plate main """
+
+ with NetDrvEpEnv(__file__, queue_count=4) as cfg:
+ cfg.pspnl = PSPFamily()
+ _queue_info(cfg)
+
+ with psp_responder(cfg):
+ ksft_run(globs=globals(),
+ case_pfx={"dev_", "assoc_", "data_"},
+ args=(cfg, ))
+ ksft_exit()
+
+
+if __name__ == "__main__":
+ main()
--
2.55.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [RFC net-next 5/6] selftests: drv-net: psp_steer: test where PSP steering sits in the Rx pipeline
2026-08-22 22:55 [RFC net-next 0/6] psp: use virt cookie as Rx steering hint Jakub Kicinski
` (3 preceding siblings ...)
2026-08-22 22:55 ` [RFC net-next 4/6] selftests: drv-net: psp_steer: test PSP VC based queue steering Jakub Kicinski
@ 2026-08-22 22:55 ` Jakub Kicinski
2026-08-22 22:55 ` [RFC net-next 6/6] selftests: drv-net: psp_steer: cover corner cases and races Jakub Kicinski
2026-08-23 17:48 ` [RFC net-next 0/6] psp: use virt cookie as Rx steering hint Willem de Bruijn
6 siblings, 0 replies; 18+ messages in thread
From: Jakub Kicinski @ 2026-08-22 22:55 UTC (permalink / raw)
To: daniel.zahka, willemdebruijn.kernel
Cc: edumazet, cratiu, borisp, kuniyu, netdev, Jakub Kicinski
Cover the steering priority / Rx pipeline ordering.
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
.../selftests/drivers/net/psp_steer.py | 171 +++++++++++++++++-
1 file changed, 164 insertions(+), 7 deletions(-)
diff --git a/tools/testing/selftests/drivers/net/psp_steer.py b/tools/testing/selftests/drivers/net/psp_steer.py
index 0401a7c359e7..fac2d7532d61 100644
--- a/tools/testing/selftests/drivers/net/psp_steer.py
+++ b/tools/testing/selftests/drivers/net/psp_steer.py
@@ -10,17 +10,35 @@ import socket
from lib.py import defer
from lib.py import ksft_run, ksft_exit
from lib.py import ksft_eq, ksft_ge, ksft_in, ksft_ne, ksft_raises
-from lib.py import KsftSkipEx
+from lib.py import CmdExitFailure, KsftSkipEx
from lib.py import NetDrvEpEnv
from lib.py import NetdevFamily, NlError, PSPFamily
+from lib.py import ethtool
-from psp_lib import close_conn, init_psp_dev, make_psp_conn, psp_txrx, \
- remote_conn_steer, remote_dev_steer, spi_xchg
+from psp_lib import close_conn, init_psp_dev, make_clr_conn, make_psp_conn, \
+ psp_txrx, remote_conn_steer, remote_dev_steer, spi_xchg
from psp_lib import responder as psp_responder
# Not exposed by the socket module
_SO_INCOMING_NAPI_ID = 56
+
+# Mirrors the helpers of the same name in hw/rss_ctx.py, which lives in a
+# directory this test cannot import from.
+def ethtool_create(cfg, act, opts):
+ output = ethtool(f"{act} {cfg.ifname} {opts}").stdout
+ # "New RSS context is 1" / "Added rule with ID 7", we want the integer
+ return int(output.split()[-1])
+
+
+def require_ntuple(cfg):
+ features = ethtool(f"-k {cfg.ifname}", json=True)[0]
+ if not features["ntuple-filters"]["active"]:
+ if features["ntuple-filters"]["fixed"]:
+ raise KsftSkipEx("Device does not support ntuple-filters")
+ ethtool(f"-K {cfg.ifname} ntuple-filters on")
+ defer(ethtool, f"-K {cfg.ifname} ntuple-filters off")
+
_VC_TX = 1 << 0
_VC_RX = 1 << 1
_VC_BOTH = _VC_TX | _VC_RX
@@ -92,6 +110,14 @@ _VC_SIZE = 8
return s
+def _rx_queue(cfg, s):
+ """Rx queue the socket's last packet arrived on"""
+ napi_id = s.getsockopt(socket.SOL_SOCKET, _SO_INCOMING_NAPI_ID)
+ ksft_ne(napi_id, 0, comment="socket saw no traffic?")
+ ksft_in(napi_id, cfg.napi2queue, comment="unknown NAPI id")
+ return cfg.napi2queue[napi_id]
+
+
def _settled_rx_queue(cfg, s, sent):
"""Run traffic until the peer picked our request up, report the queue
@@ -101,10 +127,52 @@ _VC_SIZE = 8
sent = psp_txrx(cfg, s, 1, sent)
sent = psp_txrx(cfg, s, 1, sent)
- napi_id = s.getsockopt(socket.SOL_SOCKET, _SO_INCOMING_NAPI_ID)
- ksft_ne(napi_id, 0, comment="socket saw no traffic?")
- ksft_in(napi_id, cfg.napi2queue, comment="unknown NAPI id")
- return cfg.napi2queue[napi_id], sent
+ return _rx_queue(cfg, s), sent
+
+
+def _rss_pin(cfg, qid, context=None):
+ """Point an RSS indirection table at a single queue"""
+ ctx = f"context {context} " if context is not None else ""
+ weights = " ".join("1" if i == qid else "0"
+ for i in range(cfg.rx_queue_cnt))
+ ethtool(f"-X {cfg.ifname} {ctx}weight {weights}")
+
+
+def _require_rss_steering(cfg):
+ """Skip unless the Rx queue actually follows the RSS table
+
+ Steering can only be shown to outrank RSS on a device where RSS has
+ a say in the first place - netdevsim, for one, ignores the table.
+ """
+ probe = cfg.rx_queue_cnt - 1
+
+ try:
+ _rss_pin(cfg, probe)
+ except CmdExitFailure as exc:
+ raise KsftSkipEx("Device does not support RSS table updates") from exc
+ defer(ethtool, f"-X {cfg.ifname} default")
+
+ with make_clr_conn(cfg) as s:
+ psp_txrx(cfg, s, 1)
+ landed = _rx_queue(cfg, s)
+ close_conn(cfg, s)
+
+ if landed != probe:
+ raise KsftSkipEx("Rx queue does not follow the RSS table")
+
+
+def _ntuple_l3_rule(cfg, target):
+ """Steer this host's traffic with an L3 only rule, and clean it up
+
+ L3 only on purpose: whether the classifier sees the inner TCP ports
+ of a PSP packet or just the outer UDP encapsulation is up to the
+ device, the addresses are there either way.
+ """
+ flow = (f"flow-type ip{cfg.addr_ipver} "
+ f"src-ip {cfg.remote_addr} dst-ip {cfg.addr} {target}")
+ rule = ethtool_create(cfg, "-N", flow)
+ defer(ethtool, f"-N {cfg.ifname} delete {rule}")
+ return rule
#
@@ -221,6 +289,95 @@ _VC_SIZE = 8
close_conn(cfg, s)
+def data_steer_no_grant(cfg):
+ """ We ask and nobody grants: nothing is steered """
+ _require_steer(cfg)
+ _require_queues(cfg, 3)
+ _require_rss_steering(cfg)
+ _enable_steer(cfg, local=_VC_RX, remote=0)
+
+ # RSS says 2, we would be asking for 1 if anyone were listening
+ _rss_pin(cfg, 2)
+ defer(_force_tx_queue, cfg, -1)
+ _force_tx_queue(cfg, 1)
+
+ with _psp_conn(cfg) as s:
+ qid, _ = _settled_rx_queue(cfg, s, 0)
+ ksft_eq(qid, 2, comment="steered without the peer granting anything")
+ close_conn(cfg, s)
+
+
+def data_steer_beats_rss(cfg):
+ """ Steering has to win over the RSS table """
+ _require_steer(cfg)
+ _require_queues(cfg, 3)
+ _enable_steer(cfg)
+ _require_rss_steering(cfg)
+
+ # RSS says 2, steering is going to ask for 1
+ _rss_pin(cfg, 2)
+ defer(_force_tx_queue, cfg, -1)
+ _force_tx_queue(cfg, 1)
+
+ with _psp_conn(cfg) as s:
+ qid, _ = _settled_rx_queue(cfg, s, 0)
+ ksft_eq(qid, 1)
+ close_conn(cfg, s)
+
+
+def data_steer_beats_rss_ctx(cfg):
+ """ Steering has to win over an additional RSS context as well """
+ _require_steer(cfg)
+ _require_queues(cfg, 3)
+ _enable_steer(cfg)
+ _require_rss_steering(cfg)
+ require_ntuple(cfg)
+
+ # Three distinct answers: default RSS says 0, the context says 2,
+ # and steering is going to ask for 1.
+ _rss_pin(cfg, 0)
+ ctx = ethtool_create(cfg, "-X", "context new")
+ defer(ethtool, f"-X {cfg.ifname} context {ctx} delete")
+ _rss_pin(cfg, 2, context=ctx)
+ _ntuple_l3_rule(cfg, f"context {ctx}")
+
+ # Without a cookie the context has to be the one deciding, otherwise
+ # the check below would pass without steering doing anything.
+ with make_clr_conn(cfg) as s:
+ psp_txrx(cfg, s, 1)
+ ksft_eq(_rx_queue(cfg, s), 2, comment="RSS context not in use")
+ close_conn(cfg, s)
+
+ defer(_force_tx_queue, cfg, -1)
+ _force_tx_queue(cfg, 1)
+
+ with _psp_conn(cfg) as s:
+ qid, _ = _settled_rx_queue(cfg, s, 0)
+ ksft_eq(qid, 1, comment="steering did not escape the RSS context")
+ close_conn(cfg, s)
+
+
+def data_ntuple_beats_steer(cfg):
+ """ An ntuple rule naming a queue outranks steering """
+ _require_steer(cfg)
+ _require_queues(cfg, 3)
+ _enable_steer(cfg)
+ _require_rss_steering(cfg)
+ require_ntuple(cfg)
+
+ # RSS says 0, the rule says 2, steering is going to ask for 1
+ _rss_pin(cfg, 0)
+ _ntuple_l3_rule(cfg, "action 2")
+
+ defer(_force_tx_queue, cfg, -1)
+ _force_tx_queue(cfg, 1)
+
+ with _psp_conn(cfg) as s:
+ qid, _ = _settled_rx_queue(cfg, s, 0)
+ ksft_eq(qid, 2, comment="steering overrode an explicit ntuple rule")
+ close_conn(cfg, s)
+
+
def _queue_info(cfg):
"""Map NAPI ids to Rx queue ids, and count the queues"""
netnl = NetdevFamily()
--
2.55.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [RFC net-next 6/6] selftests: drv-net: psp_steer: cover corner cases and races
2026-08-22 22:55 [RFC net-next 0/6] psp: use virt cookie as Rx steering hint Jakub Kicinski
` (4 preceding siblings ...)
2026-08-22 22:55 ` [RFC net-next 5/6] selftests: drv-net: psp_steer: test where PSP steering sits in the Rx pipeline Jakub Kicinski
@ 2026-08-22 22:55 ` Jakub Kicinski
2026-08-23 17:48 ` [RFC net-next 0/6] psp: use virt cookie as Rx steering hint Willem de Bruijn
6 siblings, 0 replies; 18+ messages in thread
From: Jakub Kicinski @ 2026-08-22 22:55 UTC (permalink / raw)
To: daniel.zahka, willemdebruijn.kernel
Cc: edumazet, cratiu, borisp, kuniyu, netdev, Jakub Kicinski
Test a PSP cookie naming a queue which went away.
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
.../selftests/drivers/net/psp_steer.py | 80 ++++++++++++++++---
1 file changed, 70 insertions(+), 10 deletions(-)
diff --git a/tools/testing/selftests/drivers/net/psp_steer.py b/tools/testing/selftests/drivers/net/psp_steer.py
index fac2d7532d61..f61479befcc0 100644
--- a/tools/testing/selftests/drivers/net/psp_steer.py
+++ b/tools/testing/selftests/drivers/net/psp_steer.py
@@ -6,17 +6,18 @@
import errno
import os
import socket
+import time
from lib.py import defer
from lib.py import ksft_run, ksft_exit
-from lib.py import ksft_eq, ksft_ge, ksft_in, ksft_ne, ksft_raises
+from lib.py import ksft_eq, ksft_ge, ksft_in, ksft_lt, ksft_ne, ksft_raises
from lib.py import CmdExitFailure, KsftSkipEx
from lib.py import NetDrvEpEnv
from lib.py import NetdevFamily, NlError, PSPFamily
from lib.py import ethtool
from psp_lib import close_conn, init_psp_dev, make_clr_conn, make_psp_conn, \
- psp_txrx, remote_conn_steer, remote_dev_steer, spi_xchg
+ psp_txrx, remote_dev_steer, req_echo, spi_xchg
from psp_lib import responder as psp_responder
# Not exposed by the socket module
@@ -43,6 +44,8 @@ _VC_TX = 1 << 0
_VC_RX = 1 << 1
_VC_BOTH = _VC_TX | _VC_RX
_VC_SIZE = 8
+_IDLE_TIME = 0.5
+_FALLBACK_QUEUE = 3
def _require_steer(cfg):
@@ -138,18 +141,19 @@ _VC_SIZE = 8
ethtool(f"-X {cfg.ifname} {ctx}weight {weights}")
-def _require_rss_steering(cfg):
- """Skip unless the Rx queue actually follows the RSS table
+def _rss_steers(cfg):
+ """Does the Rx queue actually follow the RSS table?
- Steering can only be shown to outrank RSS on a device where RSS has
- a say in the first place - netdevsim, for one, ignores the table.
+ Steering can only be compared against RSS on a device where RSS has a
+ say in the first place - netdevsim, for one, ignores the table. Leaves
+ the table as it found it.
"""
probe = cfg.rx_queue_cnt - 1
try:
_rss_pin(cfg, probe)
- except CmdExitFailure as exc:
- raise KsftSkipEx("Device does not support RSS table updates") from exc
+ except CmdExitFailure:
+ return False
defer(ethtool, f"-X {cfg.ifname} default")
with make_clr_conn(cfg) as s:
@@ -157,10 +161,22 @@ _VC_SIZE = 8
landed = _rx_queue(cfg, s)
close_conn(cfg, s)
- if landed != probe:
+ ethtool(f"-X {cfg.ifname} default")
+ return landed == probe
+
+
+def _require_rss_steering(cfg):
+ """Skip unless the Rx queue follows the RSS table"""
+ if not _rss_steers(cfg):
raise KsftSkipEx("Rx queue does not follow the RSS table")
+def _set_queue_cnt(cfg, cnt):
+ """Reconfigure the device, and re-read the NAPI ids it hands out"""
+ ethtool(f"-L {cfg.ifname} combined {cnt}")
+ _queue_info(cfg)
+
+
def _ntuple_l3_rule(cfg, target):
"""Steer this host's traffic with an L3 only rule, and clean it up
@@ -378,6 +394,50 @@ _VC_SIZE = 8
close_conn(cfg, s)
+def data_steer_stale_queue(cfg):
+ """ A cookie naming a queue which went away has to fall back to RSS """
+ _require_steer(cfg)
+ _require_queues(cfg, 8)
+ _enable_steer(cfg)
+
+ nq = cfg.rx_queue_cnt
+ rss = _rss_steers(cfg)
+ if rss:
+ # Point RSS at a queue of our choosing so that "it fell back to
+ # RSS" is a statement we can actually check. Deliberately not
+ # queue 0: plenty of devices use that as a default or error queue,
+ # and landing there would prove nothing - it would also be a
+ # thundering herd waiting to happen if every stale flow went there.
+ _rss_pin(cfg, _FALLBACK_QUEUE)
+
+ defer(_set_queue_cnt, cfg, nq)
+ defer(_force_tx_queue, cfg, -1)
+ _force_tx_queue(cfg, nq - 1)
+
+ with _psp_conn(cfg) as s:
+ qid, _ = _settled_rx_queue(cfg, s, 0)
+ ksft_eq(qid, nq - 1)
+
+ # Go properly idle first. A delayed ACK landing after the
+ # reconfiguration would carry a fresh request and teach the peer a
+ # live queue, and we would end up measuring nothing.
+ time.sleep(_IDLE_TIME)
+
+ # Take the queue away without telling the peer, which goes on
+ # asking for it in every cookie it sends.
+ _set_queue_cnt(cfg, nq - 1)
+
+ # One packet, so that our ACK cannot teach the peer a new queue
+ # before we get to look at where this one landed.
+ req_echo(cfg, s)
+ qid = _rx_queue(cfg, s)
+ close_conn(cfg, s)
+
+ ksft_lt(qid, nq - 1, comment="delivered to a queue which no longer exists")
+ if rss:
+ ksft_eq(qid, _FALLBACK_QUEUE, comment="stale request did not fall back to RSS")
+
+
def _queue_info(cfg):
"""Map NAPI ids to Rx queue ids, and count the queues"""
netnl = NetdevFamily()
@@ -398,7 +458,7 @@ _VC_SIZE = 8
def main() -> None:
""" Ksft boiler plate main """
- with NetDrvEpEnv(__file__, queue_count=4) as cfg:
+ with NetDrvEpEnv(__file__, queue_count=8) as cfg:
cfg.pspnl = PSPFamily()
_queue_info(cfg)
--
2.55.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* Re: [RFC net-next 1/6] psp: steer Rx queues with the virtualization cookie
2026-08-22 22:55 ` [RFC net-next 1/6] psp: steer Rx queues with the virtualization cookie Jakub Kicinski
@ 2026-08-23 15:31 ` Daniel Zahka
2026-08-24 15:01 ` Jakub Kicinski
2026-08-23 18:18 ` Willem de Bruijn
1 sibling, 1 reply; 18+ messages in thread
From: Daniel Zahka @ 2026-08-23 15:31 UTC (permalink / raw)
To: Jakub Kicinski, daniel.zahka, willemdebruijn.kernel
Cc: edumazet, cratiu, borisp, kuniyu, netdev
On Sat Aug 22, 2026 at 6:55 PM EDT, Jakub Kicinski wrote:
> @@ -72,6 +90,27 @@ name: psp
> Present when in associated namespace, absent when in primary/host
> namespace.
> type: flag
> + -
> + name: vc-steer-cap
> + doc: |
> + Device can steer received traffic on the PSP virtualization
> + cookie (VC). The VC is split into a 32b reserved part, a 16b
> + queue ID the sender is asking the peer to send to, and a 16b
> + queue ID granting the peer's own request. Steering installs low
> + priority rules matching the latter, which win over the RSS table
> + result. Only needed for the rx direction; granting a peer's
> + request is just header generation and needs no device support.
> + type: flag
> + -
> + name: vc-steer-ena
> + doc: |
> + Directions taking part in VC based queue steering. Leave the
> + attribute out of a dev-set request to keep the current setting.
> + Applies to associations created from then on, existing ones keep
> + the setting they were created with.
> + type: u32
> + enum: vc-steer
> + enum-as-flags: true
>
Should vc-steer-ena be a connection level setting? Maybe it could go
into rx-assoc. The way it's implemented here, the state is already per
assoc.
> +Every driver has to carry the cookie, not just those which advertise
> +``vc-steer-cap`` - granting a peer's request needs no help from the
> +device, so the ``tx`` direction of ``vc-steer-ena`` may be turned on
> +anywhere. Drivers must ask ``psp_assoc_vc_tx_get()`` for the cookie to
> +place in the Tx header, and report the queue IDs a received cookie held
> +in ``psp_skb_ext.vc_req`` and ``vc_dst`` (``psp_dev_rcv()`` does this for
> +drivers which let the core strip the headers). Reporting is what allows
> +the core to grant the peer's request. The steering itself is only
> +expected of drivers which advertise ``vc-steer-cap``.
> +
> +When VC steering is enabled GRO implementations are allowed to ignore
> +changes in the cookie for transport mode PSP.
> +
Makes sense. Do we would need to update __psp_skb_coalesce_diff() here
then for the sw gro?
> struct psp_assoc {
> struct psp_dev *psd;
>
> @@ -159,6 +233,23 @@ struct psp_assoc {
> u8 generation;
> u8 version;
> u8 peer_tx;
> + /* enum psp_assoc_flags. Written under psd->lock, additionally read
> + * on the Tx fast path without it. A snapshot of the device config
> + * taken when the association was created, so that the header size,
> + * and with it the MSS, cannot change under an established
> + * connection.
> + */
> + u8 flags;
> +
> + /* Queue IDs for the VC, ours and the peer's. @vc_loc is refreshed
> + * from the Tx queue selection and goes out as the cookie's request,
> + * @vc_rem is learned from the peer's requests and goes back out as
> + * the destination. Both are PSP_VC_QID_NONE until something is
> + * learned. Written without the socket lock, always use
> + * READ_ONCE()/WRITE_ONCE().
> + */
> + u16 vc_loc;
> + u16 vc_rem;
>
Unrelated, but this reminds me: some of the other fields here:
generation, spi, peer_tx, have some reads/writes that probably require
READ/WRITE_ONCE() and maybe deserve a similar comment here... Probably
all bugs that I introduced :/
> /* Encapsulate a TCP packet with PSP by adding the UDP+PSP headers and filling
> - * them in.
> + * them in. @vc is the virtualization cookie to place in the header, 0 for
> + * a header with no optional fields.
> */
> bool psp_dev_encapsulate(struct net *net, struct sk_buff *skb, __be32 spi,
> - u8 ver, __be16 sport)
> + u8 ver, __be16 sport, u64 vc)
> {
> u32 network_len = skb_network_header_len(skb);
> u32 ethr_len = skb_mac_header_len(skb);
> u32 bufflen = ethr_len + network_len;
> + u32 encap_len = PSP_ENCAP_HLEN;
>
> if (skb->protocol != htons(ETH_P_IP) &&
> skb->protocol != htons(ETH_P_IPV6))
> return false;
>
> - if (skb_cow_head(skb, PSP_ENCAP_HLEN))
> + if (vc)
> + encap_len += PSP_VC_SIZE;
> +
I suppose vc of 0 is probably valid. We might need something else to
disambiguate "not present".
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [RFC net-next 0/6] psp: use virt cookie as Rx steering hint
2026-08-22 22:55 [RFC net-next 0/6] psp: use virt cookie as Rx steering hint Jakub Kicinski
` (5 preceding siblings ...)
2026-08-22 22:55 ` [RFC net-next 6/6] selftests: drv-net: psp_steer: cover corner cases and races Jakub Kicinski
@ 2026-08-23 17:48 ` Willem de Bruijn
2026-08-24 15:05 ` Cosmin Ratiu
2026-08-24 15:11 ` Jakub Kicinski
6 siblings, 2 replies; 18+ messages in thread
From: Willem de Bruijn @ 2026-08-23 17:48 UTC (permalink / raw)
To: Jakub Kicinski, daniel.zahka, willemdebruijn.kernel
Cc: edumazet, cratiu, borisp, kuniyu, netdev, Jakub Kicinski
Jakub Kicinski wrote:
> Hi!
>
> This PoC series uses a field of the PSP header intended for tunnels
> to auto-steer Rx traffic. Various attempts have been made at trying
> to get Rx traffic to land close to the core where the application runs.
> By default RSS picks the Rx queue based on the flow hash.
> I'm not going to cover all previous solutions in detail but broadly
> - we have RFS in SW which looks on which CPU Tx happens and backlogs
> Rx packets there, it is quite efficient. aRFS is built on top
> of RFS but tries to program flows into the NIC. Some NICs have
> a "cache" and try to automatically remember the flow to queue
> association.
>
> All those solutions are entirely local to the receiver.
> Ideally we would want the solution to look something like
> TCP timestamp option - we send an opaque cookie to the peer,
> and the peer echoes it back to us. Our NIC can steer based
> on that echoed cookie.
Another option is to reverse RSS entropy. This requires knowledge of
the RSS secret.
Especially with PSP, the outer UDP source port is defined as flow
hash, so can be used for this.
Have the receiver compute a 4-tuple hash such that it knows the RSS
block will select the intended queue. The only free variable here in
general is the source port. Then communicate this preferred source
port to the sender.
> This patch set implements exactly that using the optional PSP
> Virtualization Cookie field. The PSP standard doesn't have much
> to say about this field:
>
> Virtualization Cookie - 64b
> An optional field, present if and only if V is set.
> It may contain a Virtual Network Identifier (VNI) or other data,
> as defined by the implementation.
If using the PSP option space, no need to (ab)use the VC:
"When the Hdr Ext Len is greater than 1, a Virtualization Cookie
and/or other header extension fields may be present. The presence of
a Virtualization Cookie is determined by the state of the V bit. For
example, given Hdr Ext Len of 3, when V bit is set, there is an 8B VC
after the IV, and another 8B extension header after the VC. The
format of other header extension fields is determined by the
applications and is opaque to the PSP hardware. "
Right now all use besides VC is opaque to the device.
For use-cases that should be interoperable across vendors, we should
probably define a standard option space, with well defined options.
In a manner that is cheap to parse at high rate, so no arbitrary order
variable length headers.
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [RFC net-next 1/6] psp: steer Rx queues with the virtualization cookie
2026-08-22 22:55 ` [RFC net-next 1/6] psp: steer Rx queues with the virtualization cookie Jakub Kicinski
2026-08-23 15:31 ` Daniel Zahka
@ 2026-08-23 18:18 ` Willem de Bruijn
1 sibling, 0 replies; 18+ messages in thread
From: Willem de Bruijn @ 2026-08-23 18:18 UTC (permalink / raw)
To: Jakub Kicinski, daniel.zahka, willemdebruijn.kernel
Cc: edumazet, cratiu, borisp, kuniyu, netdev, Jakub Kicinski
Jakub Kicinski wrote:
> PSP leaves the 64b virtualization cookie undefined in transport mode.
> Put it to use: let both ends of a connection tell each other which Rx
> queue they want traffic on, so that a flow can be pinned to a queue
> without the receiver having to install a per-flow steering rule, and
> without the sender having to know anything about the receiver's queue
> layout. The cookie holds a queue ID the sender is asking the peer to
> send to ("req") and the queue ID the peer last asked for, granted
> ("dst"). Each ID gets a 32b word of the cookie to itself and uses only
> the low half of it, so that either can grow to 32b later without the
> fields moving.
>
> The two directions are configured separately:
>
> * rx asks peers to send to the queue paired with the flow's Tx queue,
> so traffic this host receives gets steered. The receiver installs one
> low priority rule per Rx queue matching "dst", which wins over RSS,
> so this needs vc-steer-cap.
> * tx grants the requests peers make, so traffic this host sends gets
> steered at the far end. The queue is the peer's to pick and the rules
> are the peer's to install, so this needs nothing from the local
> device and can be turned on where vc-steer-cap is absent.
>
> Splitting them is what makes one sided deployment work. Turn granting on
> everywhere, cheaply, and asking wherever the NIC can actually do it.
Split the feature patch also? In three parts, one to add generic
extension header support (and all zero VC), one that adds tx granting
and finally one that adds rx requests.
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [RFC net-next 1/6] psp: steer Rx queues with the virtualization cookie
2026-08-23 15:31 ` Daniel Zahka
@ 2026-08-24 15:01 ` Jakub Kicinski
2026-08-24 15:09 ` Cosmin Ratiu
0 siblings, 1 reply; 18+ messages in thread
From: Jakub Kicinski @ 2026-08-24 15:01 UTC (permalink / raw)
To: Daniel Zahka
Cc: willemdebruijn.kernel, edumazet, cratiu, borisp, kuniyu, netdev
On Sun, 23 Aug 2026 11:31:03 -0400 Daniel Zahka wrote:
> On Sat Aug 22, 2026 at 6:55 PM EDT, Jakub Kicinski wrote:
> > @@ -72,6 +90,27 @@ name: psp
> > Present when in associated namespace, absent when in primary/host
> > namespace.
> > type: flag
> > + -
> > + name: vc-steer-cap
> > + doc: |
> > + Device can steer received traffic on the PSP virtualization
> > + cookie (VC). The VC is split into a 32b reserved part, a 16b
> > + queue ID the sender is asking the peer to send to, and a 16b
> > + queue ID granting the peer's own request. Steering installs low
> > + priority rules matching the latter, which win over the RSS table
> > + result. Only needed for the rx direction; granting a peer's
> > + request is just header generation and needs no device support.
> > + type: flag
> > + -
> > + name: vc-steer-ena
> > + doc: |
> > + Directions taking part in VC based queue steering. Leave the
> > + attribute out of a dev-set request to keep the current setting.
> > + Applies to associations created from then on, existing ones keep
> > + the setting they were created with.
> > + type: u32
> > + enum: vc-steer
> > + enum-as-flags: true
> >
>
> Should vc-steer-ena be a connection level setting? Maybe it could go
> into rx-assoc. The way it's implemented here, the state is already per
> assoc.
That's what I started with but then given the security implications
of the current simple design I could not think of a reason do configure
this connection by connection. Besides the SW stack responsible for
security is likely somewhat orthogonal to steering configuration.
Should have put this in the cover letter as well.
> > +Every driver has to carry the cookie, not just those which advertise
> > +``vc-steer-cap`` - granting a peer's request needs no help from the
> > +device, so the ``tx`` direction of ``vc-steer-ena`` may be turned on
> > +anywhere. Drivers must ask ``psp_assoc_vc_tx_get()`` for the cookie to
> > +place in the Tx header, and report the queue IDs a received cookie held
> > +in ``psp_skb_ext.vc_req`` and ``vc_dst`` (``psp_dev_rcv()`` does this for
> > +drivers which let the core strip the headers). Reporting is what allows
> > +the core to grant the peer's request. The steering itself is only
> > +expected of drivers which advertise ``vc-steer-cap``.
> > +
> > +When VC steering is enabled GRO implementations are allowed to ignore
> > +changes in the cookie for transport mode PSP.
> > +
>
> Makes sense. Do we would need to update __psp_skb_coalesce_diff() here
> then for the sw gro?
Ack.
> Unrelated, but this reminds me: some of the other fields here:
> generation, spi, peer_tx, have some reads/writes that probably require
> READ/WRITE_ONCE() and maybe deserve a similar comment here... Probably
> all bugs that I introduced :/
>
> > /* Encapsulate a TCP packet with PSP by adding the UDP+PSP headers and filling
> > - * them in.
> > + * them in. @vc is the virtualization cookie to place in the header, 0 for
> > + * a header with no optional fields.
> > */
> > bool psp_dev_encapsulate(struct net *net, struct sk_buff *skb, __be32 spi,
> > - u8 ver, __be16 sport)
> > + u8 ver, __be16 sport, u64 vc)
> > {
> > u32 network_len = skb_network_header_len(skb);
> > u32 ethr_len = skb_mac_header_len(skb);
> > u32 bufflen = ethr_len + network_len;
> > + u32 encap_len = PSP_ENCAP_HLEN;
> >
> > if (skb->protocol != htons(ETH_P_IP) &&
> > skb->protocol != htons(ETH_P_IPV6))
> > return false;
> >
> > - if (skb_cow_head(skb, PSP_ENCAP_HLEN))
> > + if (vc)
> > + encap_len += PSP_VC_SIZE;
> > +
>
> I suppose vc of 0 is probably valid. We might need something else to
> disambiguate "not present".
Ah, good point. We should probably encode the queue as idx+1,
I was going back and forth on this. Doesn't feel super clean but
it solves a bunch of corner cases, so the benefits probably outweigh
the slightly confusing indexing.
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [RFC net-next 0/6] psp: use virt cookie as Rx steering hint
2026-08-23 17:48 ` [RFC net-next 0/6] psp: use virt cookie as Rx steering hint Willem de Bruijn
@ 2026-08-24 15:05 ` Cosmin Ratiu
2026-08-25 9:52 ` Cosmin Ratiu
2026-08-24 15:11 ` Jakub Kicinski
1 sibling, 1 reply; 18+ messages in thread
From: Cosmin Ratiu @ 2026-08-24 15:05 UTC (permalink / raw)
To: daniel.zahka@gmail.com, willemdebruijn.kernel@gmail.com,
kuba@kernel.org
Cc: edumazet@google.com, Boris Pismenny, kuniyu@google.com,
netdev@vger.kernel.org
On Sun, 2026-08-23 at 13:48 -0400, Willem de Bruijn wrote:
> Jakub Kicinski wrote:
> > Hi!
> >
> > This PoC series uses a field of the PSP header intended for tunnels
> > to auto-steer Rx traffic. Various attempts have been made at trying
> > to get Rx traffic to land close to the core where the application
> > runs.
> > By default RSS picks the Rx queue based on the flow hash.
> > I'm not going to cover all previous solutions in detail but broadly
> > - we have RFS in SW which looks on which CPU Tx happens and
> > backlogs
> > Rx packets there, it is quite efficient. aRFS is built on top
> > of RFS but tries to program flows into the NIC. Some NICs have
> > a "cache" and try to automatically remember the flow to queue
> > association.
> >
> > All those solutions are entirely local to the receiver.
> > Ideally we would want the solution to look something like
> > TCP timestamp option - we send an opaque cookie to the peer,
> > and the peer echoes it back to us. Our NIC can steer based
> > on that echoed cookie.
>
> Another option is to reverse RSS entropy. This requires knowledge of
> the RSS secret.
>
> Especially with PSP, the outer UDP source port is defined as flow
> hash, so can be used for this.
>
> Have the receiver compute a 4-tuple hash such that it knows the RSS
> block will select the intended queue. The only free variable here in
> general is the source port. Then communicate this preferred source
> port to the sender.
I was thinking of something along these lines as an alternative.
When initializing PSP for a connection, look at what the device uses
with .get_rxfh(), and find a value sport (either by linear algebra or
linear probing) such that the 4-tuple hash results in the desired
queue. This might require some non-trivial code changes to reimplement
the hashes in SW though, but it's local to this sport selection only.
psp_dev_encapsulate() and psp_write_headers() both conveniently already
have a sport argument, which is currently ignored, so plumbing this is
easy.
This would result in zero RX changes for driver implementors.
An easier alternative to avoid reverse engineering RSS hash
implementations would be to vary the desired sport across multiple
packets and until packets start coming to the desired queue. Presumably
this could converge to the desired queue in O(num_queues) TX packets +
acks. Maybe could be done with TCP keep alives at connection setup?
Cosmin.
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [RFC net-next 1/6] psp: steer Rx queues with the virtualization cookie
2026-08-24 15:01 ` Jakub Kicinski
@ 2026-08-24 15:09 ` Cosmin Ratiu
2026-08-24 15:19 ` Jakub Kicinski
0 siblings, 1 reply; 18+ messages in thread
From: Cosmin Ratiu @ 2026-08-24 15:09 UTC (permalink / raw)
To: daniel.zahka@gmail.com, kuba@kernel.org
Cc: netdev@vger.kernel.org, willemdebruijn.kernel@gmail.com,
edumazet@google.com, Boris Pismenny, kuniyu@google.com
On Mon, 2026-08-24 at 08:01 -0700, Jakub Kicinski wrote:
> On Sun, 23 Aug 2026 11:31:03 -0400 Daniel Zahka wrote:
> > On Sat Aug 22, 2026 at 6:55 PM EDT, Jakub Kicinski wrote:
> > > @@ -72,6 +90,27 @@ name: psp
> > > Present when in associated namespace, absent when in
> > > primary/host
> > > namespace.
> > > type: flag
> > > + -
> > > + name: vc-steer-cap
> > > + doc: |
> > > + Device can steer received traffic on the PSP
> > > virtualization
> > > + cookie (VC). The VC is split into a 32b reserved part,
> > > a 16b
> > > + queue ID the sender is asking the peer to send to, and
> > > a 16b
> > > + queue ID granting the peer's own request. Steering
> > > installs low
> > > + priority rules matching the latter, which win over the
> > > RSS table
> > > + result. Only needed for the rx direction; granting a
> > > peer's
> > > + request is just header generation and needs no device
> > > support.
> > > + type: flag
> > > + -
> > > + name: vc-steer-ena
> > > + doc: |
> > > + Directions taking part in VC based queue steering.
> > > Leave the
> > > + attribute out of a dev-set request to keep the current
> > > setting.
> > > + Applies to associations created from then on, existing
> > > ones keep
> > > + the setting they were created with.
> > > + type: u32
> > > + enum: vc-steer
> > > + enum-as-flags: true
> > >
> >
> > Should vc-steer-ena be a connection level setting? Maybe it could
> > go
> > into rx-assoc. The way it's implemented here, the state is already
> > per
> > assoc.
>
> That's what I started with but then given the security implications
> of the current simple design I could not think of a reason do
> configure
> this connection by connection. Besides the SW stack responsible for
> security is likely somewhat orthogonal to steering configuration.
> Should have put this in the cover letter as well.
I looked at implementing the current design in mlx5 and a per-assoc
setting wouldn't be enough, because flipping this on requires device-
level steering changes (basically adding num_queues steering rules).
For device-level settings, we conveniently have the .set_config()
callback. There's no per-assoc callbacks, but theoretically, we could
detect the first use of VC-based steering and do things. Doesn't feel
that clean though compared to device-level.
Cosmin.
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [RFC net-next 0/6] psp: use virt cookie as Rx steering hint
2026-08-23 17:48 ` [RFC net-next 0/6] psp: use virt cookie as Rx steering hint Willem de Bruijn
2026-08-24 15:05 ` Cosmin Ratiu
@ 2026-08-24 15:11 ` Jakub Kicinski
2026-08-24 18:04 ` Willem de Bruijn
1 sibling, 1 reply; 18+ messages in thread
From: Jakub Kicinski @ 2026-08-24 15:11 UTC (permalink / raw)
To: Willem de Bruijn; +Cc: daniel.zahka, edumazet, cratiu, borisp, kuniyu, netdev
On Sun, 23 Aug 2026 13:48:05 -0400 Willem de Bruijn wrote:
> Jakub Kicinski wrote:
> > This PoC series uses a field of the PSP header intended for tunnels
> > to auto-steer Rx traffic. Various attempts have been made at trying
> > to get Rx traffic to land close to the core where the application runs.
> > By default RSS picks the Rx queue based on the flow hash.
> > I'm not going to cover all previous solutions in detail but broadly
> > - we have RFS in SW which looks on which CPU Tx happens and backlogs
> > Rx packets there, it is quite efficient. aRFS is built on top
> > of RFS but tries to program flows into the NIC. Some NICs have
> > a "cache" and try to automatically remember the flow to queue
> > association.
> >
> > All those solutions are entirely local to the receiver.
> > Ideally we would want the solution to look something like
> > TCP timestamp option - we send an opaque cookie to the peer,
> > and the peer echoes it back to us. Our NIC can steer based
> > on that echoed cookie.
>
> Another option is to reverse RSS entropy. This requires knowledge of
> the RSS secret.
>
> Especially with PSP, the outer UDP source port is defined as flow
> hash, so can be used for this.
>
> Have the receiver compute a 4-tuple hash such that it knows the RSS
> block will select the intended queue. The only free variable here in
> general is the source port. Then communicate this preferred source
> port to the sender.
Yes, we toyed with this a little. Communicating the hash to remote
is far less trivial. The thing that made me switch to the PSP idea
is that modern NICs can RSS on the flow label. Which is nice for
non-steering capable clients, and it conflicts with pre-computing.
Maybe the benefit is not big enough to matter.
Do you have any practical experience deploying reverse RSS?
Maybe I shied away from it too quickly..
> > This patch set implements exactly that using the optional PSP
> > Virtualization Cookie field. The PSP standard doesn't have much
> > to say about this field:
> >
> > Virtualization Cookie - 64b
> > An optional field, present if and only if V is set.
> > It may contain a Virtual Network Identifier (VNI) or other data,
> > as defined by the implementation.
>
> If using the PSP option space, no need to (ab)use the VC:
>
> "When the Hdr Ext Len is greater than 1, a Virtualization Cookie
> and/or other header extension fields may be present. The presence of
> a Virtualization Cookie is determined by the state of the V bit. For
> example, given Hdr Ext Len of 3, when V bit is set, there is an 8B VC
> after the IV, and another 8B extension header after the VC. The
> format of other header extension fields is determined by the
> applications and is opaque to the PSP hardware. "
>
> Right now all use besides VC is opaque to the device.
>
> For use-cases that should be interoperable across vendors, we should
> probably define a standard option space, with well defined options.
>
> In a manner that is cheap to parse at high rate, so no arbitrary order
> variable length headers.
TBH I don't see the need for this at all. VC is well defined, and fed
into TCAMs. IMHO classifying the use of the VC as VNI vs steering tag
can be left entirely to SW / association state. The non-V options
should remain completely opaque to the HW IMHO.
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [RFC net-next 1/6] psp: steer Rx queues with the virtualization cookie
2026-08-24 15:09 ` Cosmin Ratiu
@ 2026-08-24 15:19 ` Jakub Kicinski
0 siblings, 0 replies; 18+ messages in thread
From: Jakub Kicinski @ 2026-08-24 15:19 UTC (permalink / raw)
To: Cosmin Ratiu
Cc: daniel.zahka@gmail.com, netdev@vger.kernel.org,
willemdebruijn.kernel@gmail.com, edumazet@google.com,
Boris Pismenny, kuniyu@google.com
On Mon, 24 Aug 2026 15:09:31 +0000 Cosmin Ratiu wrote:
> On Mon, 2026-08-24 at 08:01 -0700, Jakub Kicinski wrote:
> > On Sun, 23 Aug 2026 11:31:03 -0400 Daniel Zahka wrote:
> > > On Sat Aug 22, 2026 at 6:55 PM EDT, Jakub Kicinski wrote:
> > > Should vc-steer-ena be a connection level setting? Maybe it could
> > > go
> > > into rx-assoc. The way it's implemented here, the state is already
> > > per
> > > assoc.
> >
> > That's what I started with but then given the security implications
> > of the current simple design I could not think of a reason do
> > configure
> > this connection by connection. Besides the SW stack responsible for
> > security is likely somewhat orthogonal to steering configuration.
> > Should have put this in the cover letter as well.
>
> I looked at implementing the current design in mlx5 and a per-assoc
> setting wouldn't be enough, because flipping this on requires device-
> level steering changes (basically adding num_queues steering rules).
> For device-level settings, we conveniently have the .set_config()
> callback. There's no per-assoc callbacks, but theoretically, we could
> detect the first use of VC-based steering and do things. Doesn't feel
> that clean though compared to device-level.
To be clear - the per-assoc would be in addition to the device level
setting. We'd have to enable the feature and then instead of
snapshoting the device config in psp_assoc_create() we'd have explicit
Netlink flags. We can do this later, the two flags I added here can be
thought of as "enable for all assocs", we can add "enable per-assoc"
device config later. Tho for Tx not echoing a request would be pure
spite...
I implemented the per-assoc things first but then I thought about
deploying this and really there's no reason to plumb this policy thru
Fizz/Thrift handshaking which creates assocs.
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [RFC net-next 0/6] psp: use virt cookie as Rx steering hint
2026-08-24 15:11 ` Jakub Kicinski
@ 2026-08-24 18:04 ` Willem de Bruijn
0 siblings, 0 replies; 18+ messages in thread
From: Willem de Bruijn @ 2026-08-24 18:04 UTC (permalink / raw)
To: Jakub Kicinski, Willem de Bruijn
Cc: daniel.zahka, edumazet, cratiu, borisp, kuniyu, netdev
Jakub Kicinski wrote:
> On Sun, 23 Aug 2026 13:48:05 -0400 Willem de Bruijn wrote:
> > Jakub Kicinski wrote:
> > > This PoC series uses a field of the PSP header intended for tunnels
> > > to auto-steer Rx traffic. Various attempts have been made at trying
> > > to get Rx traffic to land close to the core where the application runs.
> > > By default RSS picks the Rx queue based on the flow hash.
> > > I'm not going to cover all previous solutions in detail but broadly
> > > - we have RFS in SW which looks on which CPU Tx happens and backlogs
> > > Rx packets there, it is quite efficient. aRFS is built on top
> > > of RFS but tries to program flows into the NIC. Some NICs have
> > > a "cache" and try to automatically remember the flow to queue
> > > association.
> > >
> > > All those solutions are entirely local to the receiver.
> > > Ideally we would want the solution to look something like
> > > TCP timestamp option - we send an opaque cookie to the peer,
> > > and the peer echoes it back to us. Our NIC can steer based
> > > on that echoed cookie.
> >
> > Another option is to reverse RSS entropy. This requires knowledge of
> > the RSS secret.
> >
> > Especially with PSP, the outer UDP source port is defined as flow
> > hash, so can be used for this.
> >
> > Have the receiver compute a 4-tuple hash such that it knows the RSS
> > block will select the intended queue. The only free variable here in
> > general is the source port. Then communicate this preferred source
> > port to the sender.
>
> Yes, we toyed with this a little. Communicating the hash to remote
> is far less trivial. The thing that made me switch to the PSP idea
No need to communicate the hash, just the source port.
> is that modern NICs can RSS on the flow label. Which is nice for
> non-steering capable clients, and it conflicts with pre-computing.
> Maybe the benefit is not big enough to matter.
>
> Do you have any practical experience deploying reverse RSS?
> Maybe I shied away from it too quickly..
Definitely implemented and it is quite straightforward. Using the
probing approach that Cosmin mentioned. No fancy algebraic solution,
though that would be preferable if has faster convergence.
Not sure whether we ever actually deployed it.
> > > This patch set implements exactly that using the optional PSP
> > > Virtualization Cookie field. The PSP standard doesn't have much
> > > to say about this field:
> > >
> > > Virtualization Cookie - 64b
> > > An optional field, present if and only if V is set.
> > > It may contain a Virtual Network Identifier (VNI) or other data,
> > > as defined by the implementation.
> >
> > If using the PSP option space, no need to (ab)use the VC:
> >
> > "When the Hdr Ext Len is greater than 1, a Virtualization Cookie
> > and/or other header extension fields may be present. The presence of
> > a Virtualization Cookie is determined by the state of the V bit. For
> > example, given Hdr Ext Len of 3, when V bit is set, there is an 8B VC
> > after the IV, and another 8B extension header after the VC. The
> > format of other header extension fields is determined by the
> > applications and is opaque to the PSP hardware. "
> >
> > Right now all use besides VC is opaque to the device.
> >
> > For use-cases that should be interoperable across vendors, we should
> > probably define a standard option space, with well defined options.
> >
> > In a manner that is cheap to parse at high rate, so no arbitrary order
> > variable length headers.
>
> TBH I don't see the need for this at all. VC is well defined, and fed
> into TCAMs. IMHO classifying the use of the VC as VNI vs steering tag
> can be left entirely to SW / association state. The non-V options
> should remain completely opaque to the HW IMHO.
The spec should have been more precise than "It may contain a Virtual
Network Identifier (VNI) or other data, as defined by the
implementation.". That second part is a pretty big loophole.
VNI is not clearly defined. A minimal interpretation is that no two
VNIs must have the same value. Yet thay may definitely have the same
queue mapping.
On TCAMs: is the assumption that the VC cookie today already, if
present, is used to steer among VFs? And therefore it can also be
used to select among queues? But this is a different TCAM mask, using
only 16b, and as a direct queue id.
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [RFC net-next 0/6] psp: use virt cookie as Rx steering hint
2026-08-24 15:05 ` Cosmin Ratiu
@ 2026-08-25 9:52 ` Cosmin Ratiu
2026-08-25 18:55 ` Jakub Kicinski
0 siblings, 1 reply; 18+ messages in thread
From: Cosmin Ratiu @ 2026-08-25 9:52 UTC (permalink / raw)
To: daniel.zahka@gmail.com, willemdebruijn.kernel@gmail.com,
kuba@kernel.org
Cc: edumazet@google.com, Boris Pismenny, kuniyu@google.com,
netdev@vger.kernel.org
On Mon, 2026-08-24 at 17:05 +0200, Cosmin Ratiu wrote:
> On Sun, 2026-08-23 at 13:48 -0400, Willem de Bruijn wrote:
> > Jakub Kicinski wrote:
> > > Hi!
> > >
> > > This PoC series uses a field of the PSP header intended for
> > > tunnels
> > > to auto-steer Rx traffic. Various attempts have been made at
> > > trying
> > > to get Rx traffic to land close to the core where the application
> > > runs.
> > > By default RSS picks the Rx queue based on the flow hash.
> > > I'm not going to cover all previous solutions in detail but
> > > broadly
> > > - we have RFS in SW which looks on which CPU Tx happens and
> > > backlogs
> > > Rx packets there, it is quite efficient. aRFS is built on top
> > > of RFS but tries to program flows into the NIC. Some NICs have
> > > a "cache" and try to automatically remember the flow to queue
> > > association.
> > >
> > > All those solutions are entirely local to the receiver.
> > > Ideally we would want the solution to look something like
> > > TCP timestamp option - we send an opaque cookie to the peer,
> > > and the peer echoes it back to us. Our NIC can steer based
> > > on that echoed cookie.
> >
> > Another option is to reverse RSS entropy. This requires knowledge
> > of
> > the RSS secret.
> >
> > Especially with PSP, the outer UDP source port is defined as flow
> > hash, so can be used for this.
> >
> > Have the receiver compute a 4-tuple hash such that it knows the RSS
> > block will select the intended queue. The only free variable here
> > in
> > general is the source port. Then communicate this preferred source
> > port to the sender.
>
> I was thinking of something along these lines as an alternative.
> When initializing PSP for a connection, look at what the device uses
> with .get_rxfh(), and find a value sport (either by linear algebra or
> linear probing) such that the 4-tuple hash results in the desired
> queue. This might require some non-trivial code changes to
> reimplement
> the hashes in SW though, but it's local to this sport selection only.
>
> psp_dev_encapsulate() and psp_write_headers() both conveniently
> already
> have a sport argument, which is currently ignored, so plumbing this
> is
> easy.
>
> This would result in zero RX changes for driver implementors.
>
> An easier alternative to avoid reverse engineering RSS hash
> implementations would be to vary the desired sport across multiple
> packets and until packets start coming to the desired queue.
> Presumably
> this could converge to the desired queue in O(num_queues) TX packets
> +
> acks. Maybe could be done with TCP keep alives at connection setup?
>
Another issue we have with a potential mlx5 implementation is that HW
GRO cannot work with this feature as proposed, but there are options.
For HW GRO, mlx5 NICs need to decapsulate packets before sending them
to the HW object which does GRO. The SPI and PSP version are passed as
CQE fields. There's no more CQE space to pass req_qid alongside those.
Option 1 would be to only set req_qid on _changes_ (edge transitions)
and keep it 0 otherwise. Also make it mean queue = req_qid - 1 to allow
the use of queue 0. Ideally, only the first packet in each direction
would have req_qid != 0. These packets cannot be decapped and cannot go
through HW GRO, but all subsequent ones with req_qid == 0 may. dst_qid
is of course != 0 in all packets and is honored.
But then you need to make sure those packets aren't lost, and maybe
retransmit req_qid until it gets received. I thought about it a bit,
maybe using TCP acks as an indication? Or just trying best-effort a
couple of times...
Option 2 would be to just disallow HW GRO with req_qid.
And option 3: sport selection to get the desired RSS result makes this
interaction non-existent.
Cosmin.
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [RFC net-next 0/6] psp: use virt cookie as Rx steering hint
2026-08-25 9:52 ` Cosmin Ratiu
@ 2026-08-25 18:55 ` Jakub Kicinski
0 siblings, 0 replies; 18+ messages in thread
From: Jakub Kicinski @ 2026-08-25 18:55 UTC (permalink / raw)
To: Cosmin Ratiu
Cc: daniel.zahka@gmail.com, willemdebruijn.kernel@gmail.com,
edumazet@google.com, Boris Pismenny, kuniyu@google.com,
netdev@vger.kernel.org
On Tue, 25 Aug 2026 09:52:09 +0000 Cosmin Ratiu wrote:
> Option 1 would be to only set req_qid on _changes_ (edge transitions)
> and keep it 0 otherwise. Also make it mean queue = req_qid - 1 to allow
> the use of queue 0. Ideally, only the first packet in each direction
> would have req_qid != 0. These packets cannot be decapped and cannot go
> through HW GRO, but all subsequent ones with req_qid == 0 may. dst_qid
> is of course != 0 in all packets and is honored.
>
> But then you need to make sure those packets aren't lost, and maybe
> retransmit req_qid until it gets received. I thought about it a bit,
> maybe using TCP acks as an indication? Or just trying best-effort a
> couple of times...
>
> Option 2 would be to just disallow HW GRO with req_qid.
I'd go with Option 2 FWIW and revisit it later if we find out that more
NICs need some sort of workaround.
> And option 3: sport selection to get the desired RSS result makes this
> interaction non-existent.
^ permalink raw reply [flat|nested] 18+ messages in thread
end of thread, other threads:[~2026-08-25 18:55 UTC | newest]
Thread overview: 18+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-22 22:55 [RFC net-next 0/6] psp: use virt cookie as Rx steering hint Jakub Kicinski
2026-08-22 22:55 ` [RFC net-next 1/6] psp: steer Rx queues with the virtualization cookie Jakub Kicinski
2026-08-23 15:31 ` Daniel Zahka
2026-08-24 15:01 ` Jakub Kicinski
2026-08-24 15:09 ` Cosmin Ratiu
2026-08-24 15:19 ` Jakub Kicinski
2026-08-23 18:18 ` Willem de Bruijn
2026-08-22 22:55 ` [RFC net-next 2/6] netdevsim: support PSP VC based queue steering Jakub Kicinski
2026-08-22 22:55 ` [RFC net-next 3/6] selftests: drv-net: psp: move the PSP test plumbing into psp_lib.py Jakub Kicinski
2026-08-22 22:55 ` [RFC net-next 4/6] selftests: drv-net: psp_steer: test PSP VC based queue steering Jakub Kicinski
2026-08-22 22:55 ` [RFC net-next 5/6] selftests: drv-net: psp_steer: test where PSP steering sits in the Rx pipeline Jakub Kicinski
2026-08-22 22:55 ` [RFC net-next 6/6] selftests: drv-net: psp_steer: cover corner cases and races Jakub Kicinski
2026-08-23 17:48 ` [RFC net-next 0/6] psp: use virt cookie as Rx steering hint Willem de Bruijn
2026-08-24 15:05 ` Cosmin Ratiu
2026-08-25 9:52 ` Cosmin Ratiu
2026-08-25 18:55 ` Jakub Kicinski
2026-08-24 15:11 ` Jakub Kicinski
2026-08-24 18:04 ` Willem de Bruijn
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox