All of lore.kernel.org
 help / color / mirror / Atom feed
* [RFC PATCH net-next 0/2] netdevsim: add TLS device offload emulation and test
@ 2026-07-28 12:56 Jiayuan Chen
  2026-07-28 12:56 ` [RFC PATCH net-next 1/2] netdevsim: add TLS device offload emulation Jiayuan Chen
  2026-07-28 12:56 ` [RFC PATCH net-next 2/2] selftests: netdevsim: add a kTLS device offload test Jiayuan Chen
  0 siblings, 2 replies; 3+ messages in thread
From: Jiayuan Chen @ 2026-07-28 12:56 UTC (permalink / raw)
  To: netdev
  Cc: Jiayuan Chen, Andrew Lunn, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Shuah Khan, linux-kernel,
	linux-kselftest

The kTLS device offload path in net/tls/tls_device.c has no test
coverage upstream -- selftests/net/tls.c is software only, and the
offload code needs a driver that implements it.

So teach netdevsim to fake it.  Patch 1 implements tlsdev_ops and
advertises the TLS features, patch 2 adds a selftest.

No crypto is done: TX puts the plaintext record on the wire, RX just
sets skb->decrypted.  That only works netdevsim to netdevsim, but it
covers record assembly and the offload lifecycle.  Real ciphertext, and
more on top of it, can come later.

Jiayuan Chen (2):
  netdevsim: add TLS device offload emulation
  selftests: netdevsim: add a kTLS device offload test

 drivers/net/Kconfig                           |   1 +
 drivers/net/netdevsim/Makefile                |   4 +
 drivers/net/netdevsim/netdev.c                |   5 +
 drivers/net/netdevsim/netdevsim.h             |  35 ++
 drivers/net/netdevsim/tls.c                   | 418 +++++++++++++++
 .../drivers/net/netdevsim/.gitignore          |   2 +
 .../selftests/drivers/net/netdevsim/Makefile  |   7 +
 .../selftests/drivers/net/netdevsim/tls.sh    | 271 ++++++++++
 .../drivers/net/netdevsim/tls_offload.c       | 498 ++++++++++++++++++
 9 files changed, 1241 insertions(+)
 create mode 100644 drivers/net/netdevsim/tls.c
 create mode 100644 tools/testing/selftests/drivers/net/netdevsim/.gitignore
 create mode 100755 tools/testing/selftests/drivers/net/netdevsim/tls.sh
 create mode 100644 tools/testing/selftests/drivers/net/netdevsim/tls_offload.c

-- 
2.43.0


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

* [RFC PATCH net-next 1/2] netdevsim: add TLS device offload emulation
  2026-07-28 12:56 [RFC PATCH net-next 0/2] netdevsim: add TLS device offload emulation and test Jiayuan Chen
@ 2026-07-28 12:56 ` Jiayuan Chen
  2026-07-28 12:56 ` [RFC PATCH net-next 2/2] selftests: netdevsim: add a kTLS device offload test Jiayuan Chen
  1 sibling, 0 replies; 3+ messages in thread
From: Jiayuan Chen @ 2026-07-28 12:56 UTC (permalink / raw)
  To: netdev
  Cc: Jiayuan Chen, Andrew Lunn, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Shuah Khan, linux-kernel,
	linux-kselftest

Implement tlsdev_ops and advertise NETIF_F_HW_TLS_TX/RX, so kTLS sockets
routed over netdevsim take the device path in net/tls/tls_device.c
instead of the software one.

No crypto is done: TX puts the plaintext record on the wire and RX just
sets skb->decrypted, so both ends have to be netdevsim.  The one thing a
device would still do is write the authentication tag, and the
placeholder the stack leaves for it comes from a page frag that is never
zeroed, so clear it instead of putting stale page contents on the wire.
Records are located with tls_get_record(), the way the real drivers do
it; on RX, where there is no socket yet, connections are matched on the
tuple from ->tls_dev_add() the way the hardware does.

NETIF_F_RXCSUM comes along because netdev_fix_features() drops
NETIF_F_HW_TLS_RX without it, and NETDEVSIM now depends on TLS since it
calls tls_get_record().

Signed-off-by: Jiayuan Chen <jiayuan.chen@linux.dev>
---
 drivers/net/Kconfig               |   1 +
 drivers/net/netdevsim/Makefile    |   4 +
 drivers/net/netdevsim/netdev.c    |   5 +
 drivers/net/netdevsim/netdevsim.h |  35 +++
 drivers/net/netdevsim/tls.c       | 418 ++++++++++++++++++++++++++++++
 5 files changed, 463 insertions(+)
 create mode 100644 drivers/net/netdevsim/tls.c

diff --git a/drivers/net/Kconfig b/drivers/net/Kconfig
index ff79c466712d..cb4d4bb87afc 100644
--- a/drivers/net/Kconfig
+++ b/drivers/net/Kconfig
@@ -607,6 +607,7 @@ config NETDEVSIM
 	depends on INET
 	depends on PSAMPLE || PSAMPLE=n
 	depends on PTP_1588_CLOCK_MOCK || PTP_1588_CLOCK_MOCK=n
+	depends on TLS || TLS=n
 	select NET_DEVLINK
 	select PAGE_POOL
 	select NET_SHAPER
diff --git a/drivers/net/netdevsim/Makefile b/drivers/net/netdevsim/Makefile
index 87718204fb4d..c7f7621323b2 100644
--- a/drivers/net/netdevsim/Makefile
+++ b/drivers/net/netdevsim/Makefile
@@ -25,3 +25,7 @@ endif
 ifneq ($(CONFIG_MACSEC),)
 netdevsim-objs += macsec.o
 endif
+
+ifneq ($(CONFIG_TLS_DEVICE),)
+netdevsim-objs += tls.o
+endif
diff --git a/drivers/net/netdevsim/netdev.c b/drivers/net/netdevsim/netdev.c
index 4e9d7e10b527..8dc0b0a7d1a1 100644
--- a/drivers/net/netdevsim/netdev.c
+++ b/drivers/net/netdevsim/netdev.c
@@ -151,6 +151,8 @@ static netdev_tx_t nsim_start_xmit(struct sk_buff *skb, struct net_device *dev)
 	if (dr)
 		goto out_drop_free;
 
+	nsim_do_tls(skb, ns, peer_ns);
+
 	rxq = skb_get_queue_mapping(skb);
 	if (rxq >= peer_dev->num_rx_queues)
 		rxq = rxq % peer_dev->num_rx_queues;
@@ -1066,6 +1068,7 @@ static int nsim_init_netdevsim(struct netdevsim *ns)
 
 	nsim_macsec_init(ns);
 	nsim_ipsec_init(ns);
+	nsim_tls_init(ns);
 
 	err = register_netdevice(ns->netdev);
 	if (err)
@@ -1093,6 +1096,7 @@ static int nsim_init_netdevsim(struct netdevsim *ns)
 	RCU_INIT_POINTER(ns->peer, NULL);
 	unregister_netdevice(ns->netdev);
 err_ipsec_teardown:
+	nsim_tls_teardown(ns);
 	nsim_ipsec_teardown(ns);
 	nsim_macsec_teardown(ns);
 	nsim_bpf_uninit(ns);
@@ -1195,6 +1199,7 @@ void nsim_destroy(struct netdevsim *ns)
 	RCU_INIT_POINTER(ns->peer, NULL);
 	unregister_netdevice(dev);
 	if (nsim_dev_port_is_pf(ns->nsim_dev_port)) {
+		nsim_tls_teardown(ns);
 		nsim_macsec_teardown(ns);
 		nsim_ipsec_teardown(ns);
 		nsim_bpf_uninit(ns);
diff --git a/drivers/net/netdevsim/netdevsim.h b/drivers/net/netdevsim/netdevsim.h
index 64f77f93d937..582a76ace9d3 100644
--- a/drivers/net/netdevsim/netdevsim.h
+++ b/drivers/net/netdevsim/netdevsim.h
@@ -56,6 +56,20 @@ struct nsim_ipsec {
 	u32 tx;
 };
 
+struct nsim_tls {
+	struct list_head conns;
+	spinlock_t lock;	/* protects conns and the counters below */
+	struct dentry *dfile;
+	u32 count;
+	u32 tx_conn;
+	u32 rx_conn;
+	atomic64_t tx_packets;
+	atomic64_t tx_bytes;
+	atomic64_t rx_packets;
+	atomic64_t rx_bytes;
+	atomic64_t resyncs;
+};
+
 #define NSIM_MACSEC_MAX_SECY_COUNT 3
 #define NSIM_MACSEC_MAX_RXSC_COUNT 1
 struct nsim_rxsc {
@@ -141,6 +155,7 @@ struct netdevsim {
 	bool bpf_map_accept;
 	struct nsim_ipsec ipsec;
 	struct nsim_macsec macsec;
+	struct nsim_tls tls;
 	struct nsim_vlan vlan;
 	struct {
 		u32 inject_error;
@@ -435,6 +450,26 @@ static inline bool nsim_ipsec_tx(struct netdevsim *ns, struct sk_buff *skb)
 }
 #endif
 
+#if IS_ENABLED(CONFIG_TLS_DEVICE)
+void nsim_tls_init(struct netdevsim *ns);
+void nsim_tls_teardown(struct netdevsim *ns);
+void nsim_do_tls(struct sk_buff *skb, struct netdevsim *ns,
+		 struct netdevsim *peer_ns);
+#else
+static inline void nsim_tls_init(struct netdevsim *ns)
+{
+}
+
+static inline void nsim_tls_teardown(struct netdevsim *ns)
+{
+}
+
+static inline void nsim_do_tls(struct sk_buff *skb, struct netdevsim *ns,
+			       struct netdevsim *peer_ns)
+{
+}
+#endif
+
 #if IS_ENABLED(CONFIG_MACSEC)
 void nsim_macsec_init(struct netdevsim *ns);
 void nsim_macsec_teardown(struct netdevsim *ns);
diff --git a/drivers/net/netdevsim/tls.c b/drivers/net/netdevsim/tls.c
new file mode 100644
index 000000000000..f695f2e2beb8
--- /dev/null
+++ b/drivers/net/netdevsim/tls.c
@@ -0,0 +1,418 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <linux/ipv6.h>
+#include <linux/list.h>
+#include <linux/skbuff.h>
+#include <linux/slab.h>
+#include <linux/tcp.h>
+#include <net/ip.h>
+#include <net/ipv6.h>
+#include <net/tls.h>
+
+#include "netdevsim.h"
+
+/* Emulated kTLS offload.  No crypto is performed, so this is only self
+ * consistent between two netdevsim ports.
+ */
+
+#define NSIM_TLS_MAX_CONN	32
+
+/* netdev_fix_features() drops NETIF_F_HW_TLS_RX unless the device also does
+ * RX checksums, which every offload capable NIC does.
+ */
+#define NSIM_TLS_FEATURES	(NETIF_F_HW_TLS_TX | NETIF_F_HW_TLS_RX | \
+				 NETIF_F_RXCSUM)
+
+struct nsim_tls_conn {
+	struct list_head list;
+	struct rcu_head rcu;
+
+	/* Identity as seen on the wire, from the point of view of the port
+	 * the offload was installed on: l* is this side, r* is the peer.
+	 */
+	struct in6_addr laddr;
+	struct in6_addr raddr;
+	__be16 lport;
+	__be16 rport;
+	u16 family;
+
+	enum tls_offload_ctx_dir dir;
+	u32 start_sn;
+	u16 cipher_type;
+	const struct tls_context *tls_ctx;
+};
+
+struct nsim_tls_tuple {
+	struct in6_addr saddr;
+	struct in6_addr daddr;
+	__be16 sport;
+	__be16 dport;
+};
+
+static bool nsim_tls_parse(const struct sk_buff *skb,
+			   struct nsim_tls_tuple *t)
+{
+	const struct tcphdr *th;
+
+	switch (skb->protocol) {
+	case htons(ETH_P_IP): {
+		const struct iphdr *iph = ip_hdr(skb);
+
+		if (iph->protocol != IPPROTO_TCP)
+			return false;
+		ipv6_addr_set_v4mapped(iph->saddr, &t->saddr);
+		ipv6_addr_set_v4mapped(iph->daddr, &t->daddr);
+		break;
+	}
+	case htons(ETH_P_IPV6): {
+		const struct ipv6hdr *ip6h = ipv6_hdr(skb);
+
+		if (ip6h->nexthdr != IPPROTO_TCP)
+			return false;
+		t->saddr = ip6h->saddr;
+		t->daddr = ip6h->daddr;
+		break;
+	}
+	default:
+		return false;
+	}
+
+	th = tcp_hdr(skb);
+	t->sport = th->source;
+	t->dport = th->dest;
+
+	return true;
+}
+
+/* On RX there is no socket to look at yet, so match the tuple installed at
+ * ->tls_dev_add() time, which is what the hardware does.  The packet is
+ * arriving, so the peer of the connection is its source.
+ */
+static bool nsim_tls_rx_offloaded(struct netdevsim *ns,
+				  const struct nsim_tls_tuple *t)
+{
+	struct nsim_tls_conn *conn;
+
+	list_for_each_entry_rcu(conn, &ns->tls.conns, list)
+		if (conn->dir == TLS_OFFLOAD_CTX_DIR_RX &&
+		    conn->lport == t->dport && conn->rport == t->sport &&
+		    ipv6_addr_equal(&conn->laddr, &t->daddr) &&
+		    ipv6_addr_equal(&conn->raddr, &t->saddr))
+			return true;
+
+	return false;
+}
+
+/* The stack leaves a tag sized placeholder at the end of every record for
+ * the device to write the authentication tag into.  We do not encrypt, so
+ * nothing ever fills it in, and it comes from a page frag that was never
+ * zeroed - clear it rather than leak uninitialized memory onto the wire.
+ *
+ * tls_append_frag() either grows the last frag or adds one, so the tag is
+ * always the tail of the record's last frag.  Clearing it there rather than
+ * in the skb covers every segment the record was split into, and any
+ * retransmit of them, since they all share these pages.
+ */
+static void nsim_tls_tx_zero_tags(struct sk_buff *skb)
+{
+	struct tls_context *ctx = tls_get_ctx(skb->sk);
+	const struct tcphdr *th = tcp_hdr(skb);
+	struct tls_offload_context_tx *tx_ctx;
+	u32 seq, end, tag_size;
+	unsigned long flags;
+	unsigned int off;
+
+	off = skb_transport_offset(skb) + __tcp_hdrlen(th);
+	if (off >= skb->len)
+		return;
+
+	tag_size = ctx->prot_info.tag_size;
+	seq = ntohl(th->seq);
+	end = seq + skb->len - off;
+	tx_ctx = tls_offload_ctx_tx(ctx);
+
+	spin_lock_irqsave(&tx_ctx->lock, flags);
+	while (before(seq, end)) {
+		struct tls_record_info *record;
+		skb_frag_t *frag;
+		u64 rcd_sn;
+
+		record = tls_get_record(tx_ctx, seq, &rcd_sn);
+		if (!record || tls_record_is_start_marker(record))
+			break;
+
+		frag = &record->frags[record->num_frags - 1];
+		memset(skb_frag_address(frag) + skb_frag_size(frag) - tag_size,
+		       0, tag_size);
+
+		seq = record->end_seq;
+	}
+	spin_unlock_irqrestore(&tx_ctx->lock, flags);
+}
+
+/* Stand in for the inline encryption the hardware would do on the way out.
+ * The stack has already framed the record and we do not encrypt, so the tag
+ * is all that is left to deal with.  The socket is right here, so identify
+ * the connection the way the real drivers do.
+ */
+static void nsim_tls_tx(struct netdevsim *ns, struct sk_buff *skb)
+{
+	if (!tls_is_skb_tx_device_offloaded(skb))
+		return;
+
+	nsim_tls_tx_zero_tags(skb);
+	atomic64_inc(&ns->tls.tx_packets);
+	atomic64_add(skb->len, &ns->tls.tx_bytes);
+}
+
+/* Stand in for the inline decryption the peer's hardware would do, since we
+ * are about to hand the skb to its stack.  We do not decrypt either, so all
+ * that is needed is the flag that tells the kTLS core the payload is
+ * already plaintext.
+ */
+static void nsim_tls_rx(struct netdevsim *ns, struct sk_buff *skb)
+{
+	struct nsim_tls_tuple t;
+
+	if (list_empty(&ns->tls.conns))
+		return;
+
+	if (!nsim_tls_parse(skb, &t))
+		return;
+
+	if (!nsim_tls_rx_offloaded(ns, &t))
+		return;
+
+	skb->decrypted = 1;
+	atomic64_inc(&ns->tls.rx_packets);
+	atomic64_add(skb->len, &ns->tls.rx_bytes);
+}
+
+/* netdevsim has no wire: nsim_start_xmit() hands the skb straight to the
+ * peer's receive path, so this one call site stands in for the hardware of
+ * both ports.  It has to run before nsim_forward_skb() moves the skb over,
+ * which resets the headers.
+ */
+void nsim_do_tls(struct sk_buff *skb, struct netdevsim *ns,
+		 struct netdevsim *peer_ns)
+{
+	/* nsim_do_psp() ran first and may have wrapped the record stream, so
+	 * this is no longer a plain TLS over TCP packet.  No NIC chains the
+	 * two inline offloads either, so leave it alone.
+	 */
+	if (skb->encapsulation)
+		return;
+
+	nsim_tls_tx(ns, skb);
+	nsim_tls_rx(peer_ns, skb);
+}
+
+static int nsim_tls_dev_add(struct net_device *netdev, struct sock *sk,
+			    enum tls_offload_ctx_dir direction,
+			    struct tls_crypto_info *crypto_info,
+			    u32 start_offload_tcp_sn)
+{
+	struct netdevsim *ns = netdev_priv(netdev);
+	struct nsim_tls_conn *conn;
+	int ret = 0;
+
+	/* Mirror the cipher support of a typical offload capable NIC. */
+	switch (crypto_info->cipher_type) {
+	case TLS_CIPHER_AES_GCM_128:
+	case TLS_CIPHER_AES_GCM_256:
+		break;
+	default:
+		return -EOPNOTSUPP;
+	}
+
+	conn = kzalloc_obj(*conn);
+	if (!conn)
+		return -ENOMEM;
+
+	if (sk->sk_family == AF_INET6) {
+		conn->laddr = sk->sk_v6_rcv_saddr;
+		conn->raddr = sk->sk_v6_daddr;
+	} else {
+		ipv6_addr_set_v4mapped(sk->sk_rcv_saddr, &conn->laddr);
+		ipv6_addr_set_v4mapped(sk->sk_daddr, &conn->raddr);
+	}
+	conn->family = sk->sk_family;
+	conn->lport = htons(inet_sk(sk)->inet_num);
+	conn->rport = sk->sk_dport;
+	conn->dir = direction;
+	conn->start_sn = start_offload_tcp_sn;
+	conn->cipher_type = crypto_info->cipher_type;
+	conn->tls_ctx = tls_get_ctx(sk);
+
+	spin_lock_bh(&ns->tls.lock);
+	if (ns->tls.count >= NSIM_TLS_MAX_CONN) {
+		ret = -ENOSPC;
+		goto out_unlock;
+	}
+	list_add_tail_rcu(&conn->list, &ns->tls.conns);
+	ns->tls.count++;
+	if (direction == TLS_OFFLOAD_CTX_DIR_TX)
+		ns->tls.tx_conn++;
+	else
+		ns->tls.rx_conn++;
+out_unlock:
+	spin_unlock_bh(&ns->tls.lock);
+
+	if (ret)
+		kfree(conn);
+
+	return ret;
+}
+
+static void nsim_tls_dev_del(struct net_device *netdev,
+			     struct tls_context *tls_ctx,
+			     enum tls_offload_ctx_dir direction)
+{
+	struct netdevsim *ns = netdev_priv(netdev);
+	struct nsim_tls_conn *conn;
+
+	spin_lock_bh(&ns->tls.lock);
+	list_for_each_entry(conn, &ns->tls.conns, list) {
+		if (conn->tls_ctx != tls_ctx || conn->dir != direction)
+			continue;
+
+		list_del_rcu(&conn->list);
+		ns->tls.count--;
+		if (direction == TLS_OFFLOAD_CTX_DIR_TX)
+			ns->tls.tx_conn--;
+		else
+			ns->tls.rx_conn--;
+		spin_unlock_bh(&ns->tls.lock);
+
+		kfree_rcu(conn, rcu);
+		return;
+	}
+	spin_unlock_bh(&ns->tls.lock);
+
+	netdev_err(netdev, "TLS %s context not found on del\n",
+		   direction == TLS_OFFLOAD_CTX_DIR_TX ? "tx" : "rx");
+}
+
+/* Nothing is ever out of sync here: the emulation does not decrypt, so it
+ * never loses the record boundaries and never asks for a resync.  The core
+ * can still call in on the RX path, so account for it and move on.
+ */
+static int nsim_tls_dev_resync(struct net_device *netdev, struct sock *sk,
+			       u32 seq, u8 *rcd_sn,
+			       enum tls_offload_ctx_dir direction)
+{
+	struct netdevsim *ns = netdev_priv(netdev);
+
+	atomic64_inc(&ns->tls.resyncs);
+
+	return 0;
+}
+
+static const struct tlsdev_ops nsim_tlsdev_ops = {
+	.tls_dev_add	= nsim_tls_dev_add,
+	.tls_dev_del	= nsim_tls_dev_del,
+	.tls_dev_resync	= nsim_tls_dev_resync,
+};
+
+static ssize_t nsim_tls_dbg_read(struct file *filp, char __user *buffer,
+				 size_t count, loff_t *ppos)
+{
+	struct netdevsim *ns = filp->private_data;
+	struct nsim_tls_conn *conn;
+	size_t bufsize;
+	char *buf, *p;
+	int len;
+
+	/* Two full IPv6 addresses and ports fit in 160 bytes a line. */
+	bufsize = (NSIM_TLS_MAX_CONN * 160) + 200;
+	buf = kzalloc(bufsize, GFP_KERNEL);
+	if (!buf)
+		return -ENOMEM;
+
+	p = buf;
+
+	spin_lock_bh(&ns->tls.lock);
+	p += scnprintf(p, bufsize - (p - buf),
+		       "conn count=%u tx=%u rx=%u\n",
+		       ns->tls.count, ns->tls.tx_conn, ns->tls.rx_conn);
+	p += scnprintf(p, bufsize - (p - buf),
+		       "tx_packets=%llu tx_bytes=%llu rx_packets=%llu rx_bytes=%llu resyncs=%llu\n",
+		       atomic64_read(&ns->tls.tx_packets),
+		       atomic64_read(&ns->tls.tx_bytes),
+		       atomic64_read(&ns->tls.rx_packets),
+		       atomic64_read(&ns->tls.rx_bytes),
+		       atomic64_read(&ns->tls.resyncs));
+
+	list_for_each_entry(conn, &ns->tls.conns, list) {
+		if (conn->family == AF_INET6)
+			p += scnprintf(p, bufsize - (p - buf),
+				       "%s [%pI6c]:%u -> [%pI6c]:%u cipher=%u sn=%u\n",
+				       conn->dir == TLS_OFFLOAD_CTX_DIR_TX ?
+				       "tx" : "rx",
+				       &conn->laddr, ntohs(conn->lport),
+				       &conn->raddr, ntohs(conn->rport),
+				       conn->cipher_type, conn->start_sn);
+		else
+			p += scnprintf(p, bufsize - (p - buf),
+				       "%s %pI4:%u -> %pI4:%u cipher=%u sn=%u\n",
+				       conn->dir == TLS_OFFLOAD_CTX_DIR_TX ?
+				       "tx" : "rx",
+				       &conn->laddr.s6_addr32[3],
+				       ntohs(conn->lport),
+				       &conn->raddr.s6_addr32[3],
+				       ntohs(conn->rport),
+				       conn->cipher_type, conn->start_sn);
+	}
+	spin_unlock_bh(&ns->tls.lock);
+
+	len = simple_read_from_buffer(buffer, count, ppos, buf, p - buf);
+
+	kfree(buf);
+
+	return len;
+}
+
+static const struct file_operations nsim_tls_dbg_fops = {
+	.owner = THIS_MODULE,
+	.open = simple_open,
+	.read = nsim_tls_dbg_read,
+	.llseek = default_llseek,
+};
+
+void nsim_tls_init(struct netdevsim *ns)
+{
+	INIT_LIST_HEAD(&ns->tls.conns);
+	spin_lock_init(&ns->tls.lock);
+
+	ns->netdev->tlsdev_ops = &nsim_tlsdev_ops;
+	ns->netdev->features |= NSIM_TLS_FEATURES;
+	ns->netdev->hw_features |= NSIM_TLS_FEATURES;
+
+	ns->tls.dfile = debugfs_create_file("tls", 0400,
+					    ns->nsim_dev_port->ddir, ns,
+					    &nsim_tls_dbg_fops);
+}
+
+void nsim_tls_teardown(struct netdevsim *ns)
+{
+	struct nsim_tls_conn *conn, *tmp;
+	u32 left;
+
+	debugfs_remove_recursive(ns->tls.dfile);
+
+	spin_lock_bh(&ns->tls.lock);
+	left = ns->tls.count;
+	list_for_each_entry_safe(conn, tmp, &ns->tls.conns, list) {
+		list_del_rcu(&conn->list);
+		kfree_rcu(conn, rcu);
+	}
+	ns->tls.count = 0;
+	ns->tls.tx_conn = 0;
+	ns->tls.rx_conn = 0;
+	spin_unlock_bh(&ns->tls.lock);
+
+	if (left)
+		netdev_err(ns->netdev,
+			   "tearing down TLS offload with %u connections left\n",
+			   left);
+}
-- 
2.43.0


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

* [RFC PATCH net-next 2/2] selftests: netdevsim: add a kTLS device offload test
  2026-07-28 12:56 [RFC PATCH net-next 0/2] netdevsim: add TLS device offload emulation and test Jiayuan Chen
  2026-07-28 12:56 ` [RFC PATCH net-next 1/2] netdevsim: add TLS device offload emulation Jiayuan Chen
@ 2026-07-28 12:56 ` Jiayuan Chen
  1 sibling, 0 replies; 3+ messages in thread
From: Jiayuan Chen @ 2026-07-28 12:56 UTC (permalink / raw)
  To: netdev
  Cc: Jiayuan Chen, Andrew Lunn, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Shuah Khan, linux-kernel,
	linux-kselftest

Run kTLS over a linked netdevsim pair, one port per netns.  The test
reads /proc/net/tls_stat around the setsockopt() calls and fails unless
both directions landed on the device path, so it cannot quietly pass on
the software one.  It then does a bulk transfer both ways, small
MSG_MORE writes, splice() with TLS_TX_ZEROCOPY_RO, and a run with the
record limit at its minimum so that whole records pack several to a
segment, checking the payload each time, plus the ethtool off/on path.

  # ./tls.sh
  PASS: tls-hw-tx-offload advertised and on by default
  PASS: tls-hw-rx-offload advertised and on by default
  PASS: per-port debugfs tls file exists
  PASS: offloaded TLS data transfer
  PASS: tx and rx contexts installed on both ports
  PASS: both ends used the device path
  PASS: no silent fallback to the software path
  PASS: no decrypt errors
  PASS: driver counted offloaded packets both ways
  PASS: all offload contexts released on close
  PASS: no device contexts left behind
  PASS: offload declined once the feature is off
  PASS: software path used when offload is off
  PASS: offload works again after re-enabling

  passed: 14 failed: 0

Signed-off-by: Jiayuan Chen <jiayuan.chen@linux.dev>
---
 .../drivers/net/netdevsim/.gitignore          |   2 +
 .../selftests/drivers/net/netdevsim/Makefile  |   7 +
 .../selftests/drivers/net/netdevsim/tls.sh    | 271 ++++++++++
 .../drivers/net/netdevsim/tls_offload.c       | 498 ++++++++++++++++++
 4 files changed, 778 insertions(+)
 create mode 100644 tools/testing/selftests/drivers/net/netdevsim/.gitignore
 create mode 100755 tools/testing/selftests/drivers/net/netdevsim/tls.sh
 create mode 100644 tools/testing/selftests/drivers/net/netdevsim/tls_offload.c

diff --git a/tools/testing/selftests/drivers/net/netdevsim/.gitignore b/tools/testing/selftests/drivers/net/netdevsim/.gitignore
new file mode 100644
index 000000000000..9d4f57cb2baa
--- /dev/null
+++ b/tools/testing/selftests/drivers/net/netdevsim/.gitignore
@@ -0,0 +1,2 @@
+# SPDX-License-Identifier: GPL-2.0-only
+tls_offload
diff --git a/tools/testing/selftests/drivers/net/netdevsim/Makefile b/tools/testing/selftests/drivers/net/netdevsim/Makefile
index 9808c2fbae9e..5beb4ed64eb5 100644
--- a/tools/testing/selftests/drivers/net/netdevsim/Makefile
+++ b/tools/testing/selftests/drivers/net/netdevsim/Makefile
@@ -1,5 +1,7 @@
 # SPDX-License-Identifier: GPL-2.0+ OR MIT
 
+CFLAGS += $(KHDR_INCLUDES)
+
 TEST_PROGS := \
 	devlink.sh \
 	devlink_in_netns.sh \
@@ -15,9 +17,14 @@ TEST_PROGS := \
 	peer.sh \
 	psample.sh \
 	tc-mq-visibility.sh \
+	tls.sh \
 	udp_tunnel_nic.sh \
 # end of TEST_PROGS
 
+TEST_GEN_FILES := \
+	tls_offload
+# end of TEST_GEN_FILES
+
 TEST_FILES := \
 	ethtool-common.sh
 # end of TEST_FILES
diff --git a/tools/testing/selftests/drivers/net/netdevsim/tls.sh b/tools/testing/selftests/drivers/net/netdevsim/tls.sh
new file mode 100755
index 000000000000..399e2c4d382d
--- /dev/null
+++ b/tools/testing/selftests/drivers/net/netdevsim/tls.sh
@@ -0,0 +1,271 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Exercise netdevsim's emulated kTLS device offload over a linked
+# netdevsim pair, one port per network namespace.
+#
+# shellcheck disable=SC2154 # ksft_skip comes from lib.sh
+
+lib_dir=$(dirname "$0")
+# shellcheck source=./../../../net/lib.sh
+# shellcheck disable=SC1091
+source "$lib_dir"/../../../net/lib.sh
+
+NSIM_DEV_1_ID=$((256 + RANDOM % 256))
+NSIM_DEV_1_SYS=/sys/bus/netdevsim/devices/netdevsim$NSIM_DEV_1_ID
+NSIM_DEV_2_ID=$((512 + RANDOM % 256))
+NSIM_DEV_2_SYS=/sys/bus/netdevsim/devices/netdevsim$NSIM_DEV_2_ID
+
+NSIM_DEV_SYS_NEW=/sys/bus/netdevsim/new_device
+NSIM_DEV_SYS_DEL=/sys/bus/netdevsim/del_device
+NSIM_DEV_SYS_LINK=/sys/bus/netdevsim/link_device
+
+DEBUGFS=/sys/kernel/debug/netdevsim
+NSIM_DEV_1_TLS=$DEBUGFS/netdevsim$NSIM_DEV_1_ID/ports/0/tls
+NSIM_DEV_2_TLS=$DEBUGFS/netdevsim$NSIM_DEV_2_ID/ports/0/tls
+
+SRV_IP=192.168.13.1
+CLI_IP=192.168.13.2
+PORT=4433
+
+SYNCDIR=
+BIN=$lib_dir/tls_offload
+
+num_pass=0
+num_fail=0
+
+check()
+{
+	local msg="$1"
+	local ret="$2"
+
+	if [ "$ret" -eq 0 ]; then
+		echo "PASS: $msg"
+		num_pass=$((num_pass + 1))
+	else
+		echo "FAIL: $msg"
+		num_fail=$((num_fail + 1))
+	fi
+}
+
+# shellcheck disable=SC2317,SC2329 # invoked from the EXIT trap
+cleanup()
+{
+	ip netns del nscl 2>/dev/null
+	ip netns del nssv 2>/dev/null
+	echo "$NSIM_DEV_2_ID" > "$NSIM_DEV_SYS_DEL" 2>/dev/null
+	echo "$NSIM_DEV_1_ID" > "$NSIM_DEV_SYS_DEL" 2>/dev/null
+	[ -n "$SYNCDIR" ] && rm -rf "$SYNCDIR"
+}
+
+setup()
+{
+	set -e
+
+	echo "$NSIM_DEV_1_ID" > "$NSIM_DEV_SYS_NEW"
+	echo "$NSIM_DEV_2_ID" > "$NSIM_DEV_SYS_NEW"
+	udevadm settle 2>/dev/null || sleep 1
+
+	NSIM_DEV_1_NAME=$(find "$NSIM_DEV_1_SYS"/net -maxdepth 1 -type d ! \
+		-path "$NSIM_DEV_1_SYS"/net -exec basename {} \;)
+	NSIM_DEV_2_NAME=$(find "$NSIM_DEV_2_SYS"/net -maxdepth 1 -type d ! \
+		-path "$NSIM_DEV_2_SYS"/net -exec basename {} \;)
+
+	ip netns add nssv
+	ip netns add nscl
+
+	ip link set "$NSIM_DEV_1_NAME" netns nssv
+	ip link set "$NSIM_DEV_2_NAME" netns nscl
+
+	ip netns exec nssv ip addr add "$SRV_IP/24" dev "$NSIM_DEV_1_NAME"
+	ip netns exec nscl ip addr add "$CLI_IP/24" dev "$NSIM_DEV_2_NAME"
+
+	ip netns exec nssv ip link set dev "$NSIM_DEV_1_NAME" up
+	ip netns exec nscl ip link set dev "$NSIM_DEV_2_NAME" up
+
+	NSIM_DEV_1_FD=$((256 + RANDOM % 256))
+	exec {NSIM_DEV_1_FD}</var/run/netns/nssv
+	NSIM_DEV_1_IFIDX=$(ip netns exec nssv \
+		cat /sys/class/net/"$NSIM_DEV_1_NAME"/ifindex)
+
+	NSIM_DEV_2_FD=$((256 + RANDOM % 256))
+	exec {NSIM_DEV_2_FD}</var/run/netns/nscl
+	NSIM_DEV_2_IFIDX=$(ip netns exec nscl \
+		cat /sys/class/net/"$NSIM_DEV_2_NAME"/ifindex)
+
+	echo "$NSIM_DEV_1_FD:$NSIM_DEV_1_IFIDX $NSIM_DEV_2_FD:$NSIM_DEV_2_IFIDX" \
+		> "$NSIM_DEV_SYS_LINK"
+
+	SYNCDIR=$(mktemp -d)
+	set +e
+}
+
+feature()
+{
+	local netns="$1"
+	local dev="$2"
+	local feat="$3"
+
+	ip netns exec "$netns" ethtool -k "$dev" 2>/dev/null | \
+		sed -n "s/^$feat: \([a-z]*\).*/\1/p"
+}
+
+dbg_field()
+{
+	sed -n "s/.*\<$2=\([0-9]*\).*/\1/p" "$1" | head -1
+}
+
+tls_stat()
+{
+	ip netns exec "$1" cat /proc/net/tls_stat | \
+		sed -n "s/^$2 \([0-9]*\)/\1/p"
+}
+
+# Both ends park once their offload is installed and before any data is
+# sent, so the driver's context count can be sampled without racing the
+# transfer.  Pass "nosample" when the offload is expected to be refused,
+# since then neither end ever reaches the barrier.
+run_pair()
+{
+	local sample="${1:-sample}"
+	local srv_rc cli_rc waited=0
+
+	rm -f "$SYNCDIR"/*.ready "$SYNCDIR"/go
+	CONNS_1=0
+	CONNS_2=0
+
+	ip netns exec nssv "$BIN" server "$SRV_IP" "$PORT" "$SYNCDIR" &
+	local srv_pid=$!
+	ip netns exec nscl "$BIN" client "$SRV_IP" "$PORT" "$SYNCDIR" &
+	local cli_pid=$!
+
+	if [ "$sample" = "sample" ]; then
+		while [ ! -e "$SYNCDIR/server.ready" ] ||
+		      [ ! -e "$SYNCDIR/client.ready" ]; do
+			[ "$waited" -ge 200 ] && break
+			sleep 0.05
+			waited=$((waited + 1))
+		done
+		CONNS_1=$(dbg_field "$NSIM_DEV_1_TLS" count)
+		CONNS_2=$(dbg_field "$NSIM_DEV_2_TLS" count)
+		: "${CONNS_1:=0}"
+		: "${CONNS_2:=0}"
+	fi
+	touch "$SYNCDIR/go"
+
+	wait "$srv_pid"; srv_rc=$?
+	wait "$cli_pid"; cli_rc=$?
+
+	[ "$srv_rc" -eq 0 ] && [ "$cli_rc" -eq 0 ]
+}
+
+###
+### Code start
+###
+
+if [ "$(id -u)" -ne 0 ]; then
+	echo "SKIP: need root"
+	exit "$ksft_skip"
+fi
+
+if ! command -v ethtool >/dev/null; then
+	echo "SKIP: ethtool not found"
+	exit "$ksft_skip"
+fi
+
+if [ ! -x "$BIN" ]; then
+	echo "SKIP: $BIN not built"
+	exit "$ksft_skip"
+fi
+
+modprobe netdevsim 2>/dev/null
+if [ ! -d /sys/bus/netdevsim ]; then
+	echo "SKIP: netdevsim not available"
+	exit "$ksft_skip"
+fi
+
+modprobe tls 2>/dev/null
+if [ ! -e /proc/net/tls_stat ]; then
+	echo "SKIP: kernel TLS not available"
+	exit "$ksft_skip"
+fi
+
+trap cleanup EXIT
+setup
+
+# The offload has to be advertised, and on by default like the other
+# netdevsim crypto offloads.
+for f in tls-hw-tx-offload tls-hw-rx-offload; do
+	[ "$(feature nssv "$NSIM_DEV_1_NAME" "$f")" = "on" ]
+	check "$f advertised and on by default" $?
+done
+
+[ -e "$NSIM_DEV_1_TLS" ]
+check "per-port debugfs tls file exists" $?
+
+# Main data path run.
+run_pair
+check "offloaded TLS data transfer" $?
+
+# Sampled at the barrier, so each port must be holding exactly the TX and
+# the RX context of its own socket.
+[ "$CONNS_1" -eq 2 ] && [ "$CONNS_2" -eq 2 ]
+check "tx and rx contexts installed on both ports" $?
+
+# Both ends must have gone through the device path, not the SW fallback.
+[ "$(tls_stat nssv TlsTxDevice)" -ge 1 ] && \
+	[ "$(tls_stat nssv TlsRxDevice)" -ge 1 ] && \
+	[ "$(tls_stat nscl TlsTxDevice)" -ge 1 ] && \
+	[ "$(tls_stat nscl TlsRxDevice)" -ge 1 ]
+check "both ends used the device path" $?
+
+[ "$(tls_stat nssv TlsTxSw)" -eq 0 ] && [ "$(tls_stat nscl TlsTxSw)" -eq 0 ]
+check "no silent fallback to the software path" $?
+
+[ "$(tls_stat nssv TlsDecryptError)" -eq 0 ] && \
+	[ "$(tls_stat nscl TlsDecryptError)" -eq 0 ]
+check "no decrypt errors" $?
+
+# The driver must have seen the records go by in both directions.
+[ "$(dbg_field "$NSIM_DEV_1_TLS" tx_packets)" -ge 1 ] && \
+	[ "$(dbg_field "$NSIM_DEV_1_TLS" rx_packets)" -ge 1 ] && \
+	[ "$(dbg_field "$NSIM_DEV_2_TLS" tx_packets)" -ge 1 ] && \
+	[ "$(dbg_field "$NSIM_DEV_2_TLS" rx_packets)" -ge 1 ]
+check "driver counted offloaded packets both ways" $?
+
+# Sockets are closed by now, so every context must have been given back.
+[ "$(dbg_field "$NSIM_DEV_1_TLS" count)" -eq 0 ] && \
+	[ "$(dbg_field "$NSIM_DEV_2_TLS" count)" -eq 0 ]
+check "all offload contexts released on close" $?
+
+[ "$(tls_stat nssv TlsCurrTxDevice)" -eq 0 ] && \
+	[ "$(tls_stat nssv TlsCurrRxDevice)" -eq 0 ]
+check "no device contexts left behind" $?
+
+# Turning the feature off has to make the offload refuse the connection;
+# the test binary insists on the device path, so it must now fail.
+ip netns exec nssv ethtool -K "$NSIM_DEV_1_NAME" tls-hw-tx-offload off
+ip netns exec nssv ethtool -K "$NSIM_DEV_1_NAME" tls-hw-rx-offload off
+ip netns exec nscl ethtool -K "$NSIM_DEV_2_NAME" tls-hw-tx-offload off
+ip netns exec nscl ethtool -K "$NSIM_DEV_2_NAME" tls-hw-rx-offload off
+
+run_pair nosample
+rc=$?
+[ "$rc" -ne 0 ]
+check "offload declined once the feature is off" $?
+
+[ "$(tls_stat nssv TlsTxSw)" -ge 1 ]
+check "software path used when offload is off" $?
+
+ip netns exec nssv ethtool -K "$NSIM_DEV_1_NAME" tls-hw-tx-offload on
+ip netns exec nssv ethtool -K "$NSIM_DEV_1_NAME" tls-hw-rx-offload on
+ip netns exec nscl ethtool -K "$NSIM_DEV_2_NAME" tls-hw-tx-offload on
+ip netns exec nscl ethtool -K "$NSIM_DEV_2_NAME" tls-hw-rx-offload on
+
+run_pair
+check "offload works again after re-enabling" $?
+
+echo
+echo "passed: $num_pass failed: $num_fail"
+[ "$num_fail" -eq 0 ] && exit 0
+exit 1
diff --git a/tools/testing/selftests/drivers/net/netdevsim/tls_offload.c b/tools/testing/selftests/drivers/net/netdevsim/tls_offload.c
new file mode 100644
index 000000000000..ebfb4bfcd4f4
--- /dev/null
+++ b/tools/testing/selftests/drivers/net/netdevsim/tls_offload.c
@@ -0,0 +1,498 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * kTLS device offload data path exercise, driven by tls.sh.
+ *
+ * One instance runs as the server and one as the client, each in its own
+ * network namespace, connected back to back by a linked netdevsim pair.
+ * Both ends enable kTLS and rely on netdevsim's emulated TLS offload, so
+ * every record travels through net/tls/tls_device.c rather than the
+ * software path.
+ *
+ * The two processes rendezvous through a shared directory so that neither
+ * side sends before the other has installed its RX offload.
+ */
+
+#define _GNU_SOURCE
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <linux/tls.h>
+
+#ifndef SOL_TLS
+#define SOL_TLS			282
+#endif
+
+#ifndef TCP_ULP
+#define TCP_ULP			31
+#endif
+
+#define BULK_LEN		(200 * 1024)
+#define MORE_FRAGS		64
+#define SPLICE_FRAG_LEN		4096
+#define SPLICE_FRAGS		8
+
+/* TLS_MIN_RECORD_SIZE_LIM and TLS_MAX_PAYLOAD_SIZE, which are not uapi. */
+#define REC_LIM_MIN		64
+#define REC_LIM_MAX		16384
+
+#define SMALL_RECS		100
+#define SMALL_LEN		(REC_LIM_MIN * SMALL_RECS)
+
+#define SYNC_TIMEOUT_MS		20000
+#define CONNECT_TIMEOUT_MS	20000
+
+static const char *role;
+
+static void die(const char *what)
+{
+	fprintf(stderr, "%s: %s: %s\n", role, what, strerror(errno));
+	exit(1);
+}
+
+static void fail(const char *fmt, ...)
+{
+	va_list ap;
+
+	fprintf(stderr, "%s: ", role);
+	va_start(ap, fmt);
+	vfprintf(stderr, fmt, ap);
+	va_end(ap);
+	fprintf(stderr, "\n");
+	exit(1);
+}
+
+static void msleep(unsigned int ms)
+{
+	struct timespec ts = {
+		.tv_sec = ms / 1000,
+		.tv_nsec = (ms % 1000) * 1000000L,
+	};
+
+	nanosleep(&ts, NULL);
+}
+
+/* /proc/net/tls_stat is per netns, so both ends can check that their own
+ * connection really landed on the device path.
+ */
+static unsigned long read_tls_stat(const char *name)
+{
+	char line[256];
+	unsigned long val;
+	FILE *f;
+
+	f = fopen("/proc/net/tls_stat", "r");
+	if (!f)
+		die("open /proc/net/tls_stat");
+
+	while (fgets(line, sizeof(line), f)) {
+		char key[64];
+
+		if (sscanf(line, "%63s %lu", key, &val) != 2)
+			continue;
+		if (!strcmp(key, name)) {
+			fclose(f);
+			return val;
+		}
+	}
+
+	fclose(f);
+	fail("%s not found in /proc/net/tls_stat", name);
+	return 0;
+}
+
+static void fill_pattern(char *buf, size_t len, unsigned int seed)
+{
+	size_t i;
+
+	for (i = 0; i < len; i++)
+		buf[i] = (char)(seed + i * 31 + (i >> 8) * 7);
+}
+
+static void check_pattern(const char *buf, size_t len, unsigned int seed,
+			  const char *what)
+{
+	char *want = malloc(len);
+	size_t i;
+
+	if (!want)
+		die("malloc");
+
+	fill_pattern(want, len, seed);
+	for (i = 0; i < len; i++) {
+		if (buf[i] != want[i])
+			fail("%s: payload mismatch at byte %zu: got 0x%02x want 0x%02x",
+			     what, i, (unsigned char)buf[i],
+			     (unsigned char)want[i]);
+	}
+
+	free(want);
+}
+
+static void write_all(int fd, const char *buf, size_t len)
+{
+	size_t done = 0;
+
+	while (done < len) {
+		ssize_t n = send(fd, buf + done, len - done, 0);
+
+		if (n < 0) {
+			if (errno == EINTR)
+				continue;
+			die("send");
+		}
+		done += n;
+	}
+}
+
+static void read_all(int fd, char *buf, size_t len)
+{
+	size_t done = 0;
+
+	while (done < len) {
+		ssize_t n = recv(fd, buf + done, len - done, 0);
+
+		if (n < 0) {
+			if (errno == EINTR)
+				continue;
+			die("recv");
+		}
+		if (n == 0)
+			fail("peer closed after %zu of %zu bytes", done, len);
+		done += n;
+	}
+}
+
+static void enable_ktls(int fd)
+{
+	struct tls12_crypto_info_aes_gcm_128 ci = {};
+	unsigned long tx_before, rx_before;
+
+	tx_before = read_tls_stat("TlsTxDevice");
+	rx_before = read_tls_stat("TlsRxDevice");
+
+	if (setsockopt(fd, IPPROTO_TCP, TCP_ULP, "tls", sizeof("tls")))
+		die("setsockopt(TCP_ULP, tls)");
+
+	ci.info.version = TLS_1_2_VERSION;
+	ci.info.cipher_type = TLS_CIPHER_AES_GCM_128;
+	memset(ci.iv, 'i', sizeof(ci.iv));
+	memset(ci.key, 'k', sizeof(ci.key));
+	memset(ci.salt, 's', sizeof(ci.salt));
+	memset(ci.rec_seq, 0, sizeof(ci.rec_seq));
+
+	if (setsockopt(fd, SOL_TLS, TLS_TX, &ci, sizeof(ci)))
+		die("setsockopt(TLS_TX)");
+	if (setsockopt(fd, SOL_TLS, TLS_RX, &ci, sizeof(ci)))
+		die("setsockopt(TLS_RX)");
+
+	/* The whole point of the exercise: refuse to silently fall back to
+	 * the software path, otherwise the test would pass without ever
+	 * touching tls_device.c.
+	 */
+	if (read_tls_stat("TlsTxDevice") != tx_before + 1)
+		fail("TX did not land on the device path (TlsTxDevice %lu -> %lu)",
+		     tx_before, read_tls_stat("TlsTxDevice"));
+	if (read_tls_stat("TlsRxDevice") != rx_before + 1)
+		fail("RX did not land on the device path (TlsRxDevice %lu -> %lu)",
+		     rx_before, read_tls_stat("TlsRxDevice"));
+}
+
+static void sync_path(char *out, size_t len, const char *dir, const char *who)
+{
+	if ((size_t)snprintf(out, len, "%s/%s.ready", dir, who) >= len)
+		fail("sync dir path too long");
+}
+
+static void rendezvous(const char *dir, const char *me, const char *peer)
+{
+	char mine[PATH_MAX], theirs[PATH_MAX];
+	unsigned int waited = 0;
+	int fd;
+
+	sync_path(mine, sizeof(mine), dir, me);
+	sync_path(theirs, sizeof(theirs), dir, peer);
+
+	fd = open(mine, O_CREAT | O_WRONLY, 0600);
+	if (fd < 0)
+		die("create sync file");
+	close(fd);
+
+	while (access(theirs, F_OK)) {
+		if (waited >= SYNC_TIMEOUT_MS)
+			fail("timed out waiting for %s", peer);
+		msleep(20);
+		waited += 20;
+	}
+}
+
+/* Both ends stop here with their offload installed and no data sent yet,
+ * so that the driver state can be inspected from the outside.
+ */
+static void wait_for_go(const char *dir)
+{
+	unsigned int waited = 0;
+	char go[PATH_MAX];
+
+	if ((size_t)snprintf(go, sizeof(go), "%s/go", dir) >= sizeof(go))
+		fail("sync dir path too long");
+
+	while (access(go, F_OK)) {
+		if (waited >= SYNC_TIMEOUT_MS)
+			fail("timed out waiting for go");
+		msleep(20);
+		waited += 20;
+	}
+}
+
+/* Small writes with MSG_MORE accumulate into one open record before it is
+ * pushed, which is the interesting part of tls_push_data().
+ */
+static void send_msg_more(int fd, unsigned int seed)
+{
+	char buf[MORE_FRAGS + 1];
+	int i;
+
+	fill_pattern(buf, sizeof(buf), seed);
+
+	for (i = 0; i < MORE_FRAGS; i++) {
+		if (send(fd, buf + i, 1, MSG_MORE) != 1)
+			die("send(MSG_MORE)");
+	}
+	if (send(fd, buf + MORE_FRAGS, 1, 0) != 1)
+		die("send(last)");
+}
+
+/* splice() reaches tls_push_data() with MSG_SPLICE_PAGES once
+ * TLS_TX_ZEROCOPY_RO is enabled, which is a distinct fragment path.
+ */
+static void send_splice(int fd, unsigned int seed)
+{
+	char buf[SPLICE_FRAG_LEN];
+	int val = 1;
+	int i;
+
+	if (setsockopt(fd, SOL_TLS, TLS_TX_ZEROCOPY_RO, &val, sizeof(val)))
+		die("setsockopt(TLS_TX_ZEROCOPY_RO)");
+
+	for (i = 0; i < SPLICE_FRAGS; i++) {
+		int p[2];
+
+		fill_pattern(buf, sizeof(buf), seed + i * SPLICE_FRAG_LEN);
+
+		if (pipe(p))
+			die("pipe");
+		if (write(p[1], buf, sizeof(buf)) != sizeof(buf))
+			die("write to pipe");
+		if (splice(p[0], NULL, fd, NULL, sizeof(buf),
+			   i == SPLICE_FRAGS - 1 ? 0 : SPLICE_F_MORE) !=
+		    sizeof(buf))
+			die("splice");
+		close(p[0]);
+		close(p[1]);
+	}
+
+	val = 0;
+	if (setsockopt(fd, SOL_TLS, TLS_TX_ZEROCOPY_RO, &val, sizeof(val)))
+		die("setsockopt(TLS_TX_ZEROCOPY_RO off)");
+}
+
+/* A record can be up to 16K, so normally one segment carries a piece of a
+ * single record.  Shrinking the limit puts a dozen or so whole records in
+ * every segment instead, which is the multi-record path through the driver.
+ */
+static void send_small_records(int fd, unsigned int seed)
+{
+	char buf[SMALL_LEN];
+	uint16_t limit;
+
+	limit = REC_LIM_MIN;
+	if (setsockopt(fd, SOL_TLS, TLS_TX_MAX_PAYLOAD_LEN, &limit,
+		       sizeof(limit)))
+		die("setsockopt(TLS_TX_MAX_PAYLOAD_LEN)");
+
+	fill_pattern(buf, sizeof(buf), seed);
+	write_all(fd, buf, sizeof(buf));
+
+	limit = REC_LIM_MAX;
+	if (setsockopt(fd, SOL_TLS, TLS_TX_MAX_PAYLOAD_LEN, &limit,
+		       sizeof(limit)))
+		die("setsockopt(TLS_TX_MAX_PAYLOAD_LEN restore)");
+}
+
+#define SEED_C2S_BULK	0x11
+#define SEED_S2C_BULK	0x22
+#define SEED_C2S_MORE	0x33
+#define SEED_C2S_SPLICE	0x44
+#define SEED_C2S_SMALL	0x55
+
+static void run_client(int fd)
+{
+	char *buf = malloc(BULK_LEN);
+
+	if (!buf)
+		die("malloc");
+
+	fill_pattern(buf, BULK_LEN, SEED_C2S_BULK);
+	write_all(fd, buf, BULK_LEN);
+
+	read_all(fd, buf, BULK_LEN);
+	check_pattern(buf, BULK_LEN, SEED_S2C_BULK, "server -> client bulk");
+
+	send_msg_more(fd, SEED_C2S_MORE);
+	send_splice(fd, SEED_C2S_SPLICE);
+	send_small_records(fd, SEED_C2S_SMALL);
+
+	/* Wait for the server's verdict before tearing anything down. */
+	read_all(fd, buf, 1);
+	if (buf[0] != 'k')
+		fail("server reported a failure");
+
+	free(buf);
+}
+
+static void run_server(int fd)
+{
+	size_t splice_len = (size_t)SPLICE_FRAG_LEN * SPLICE_FRAGS;
+	char *buf = malloc(BULK_LEN);
+	char more[MORE_FRAGS + 1];
+	char *sbuf;
+	char ok = 'k';
+	int i;
+
+	sbuf = malloc(splice_len);
+	if (!buf || !sbuf)
+		die("malloc");
+
+	read_all(fd, buf, BULK_LEN);
+	check_pattern(buf, BULK_LEN, SEED_C2S_BULK, "client -> server bulk");
+
+	fill_pattern(buf, BULK_LEN, SEED_S2C_BULK);
+	write_all(fd, buf, BULK_LEN);
+
+	read_all(fd, more, sizeof(more));
+	check_pattern(more, sizeof(more), SEED_C2S_MORE, "client -> server MSG_MORE");
+
+	read_all(fd, sbuf, splice_len);
+	for (i = 0; i < SPLICE_FRAGS; i++)
+		check_pattern(sbuf + (size_t)i * SPLICE_FRAG_LEN,
+			      SPLICE_FRAG_LEN, SEED_C2S_SPLICE +
+			      i * SPLICE_FRAG_LEN, "client -> server splice");
+
+	read_all(fd, buf, SMALL_LEN);
+	check_pattern(buf, SMALL_LEN, SEED_C2S_SMALL,
+		      "client -> server small records");
+
+	write_all(fd, &ok, 1);
+
+	free(sbuf);
+	free(buf);
+}
+
+static int do_server(const char *ip, int port, const char *syncdir)
+{
+	struct sockaddr_in sa = {};
+	int lfd, fd, one = 1;
+
+	lfd = socket(AF_INET, SOCK_STREAM, 0);
+	if (lfd < 0)
+		die("socket");
+	if (setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)))
+		die("SO_REUSEADDR");
+
+	sa.sin_family = AF_INET;
+	sa.sin_port = htons(port);
+	if (inet_pton(AF_INET, ip, &sa.sin_addr) != 1)
+		fail("bad bind address %s", ip);
+
+	if (bind(lfd, (struct sockaddr *)&sa, sizeof(sa)))
+		die("bind");
+	if (listen(lfd, 1))
+		die("listen");
+
+	fd = accept(lfd, NULL, NULL);
+	if (fd < 0)
+		die("accept");
+	close(lfd);
+
+	enable_ktls(fd);
+	rendezvous(syncdir, "server", "client");
+	wait_for_go(syncdir);
+
+	run_server(fd);
+
+	close(fd);
+	return 0;
+}
+
+static int do_client(const char *ip, int port, const char *syncdir)
+{
+	struct sockaddr_in sa = {};
+	unsigned int waited = 0;
+	int fd;
+
+	sa.sin_family = AF_INET;
+	sa.sin_port = htons(port);
+	if (inet_pton(AF_INET, ip, &sa.sin_addr) != 1)
+		fail("bad server address %s", ip);
+
+	for (;;) {
+		fd = socket(AF_INET, SOCK_STREAM, 0);
+		if (fd < 0)
+			die("socket");
+		if (!connect(fd, (struct sockaddr *)&sa, sizeof(sa)))
+			break;
+		close(fd);
+		if (waited >= CONNECT_TIMEOUT_MS)
+			die("connect");
+		msleep(20);
+		waited += 20;
+	}
+
+	enable_ktls(fd);
+	rendezvous(syncdir, "client", "server");
+	wait_for_go(syncdir);
+
+	run_client(fd);
+
+	close(fd);
+	return 0;
+}
+
+int main(int argc, char **argv)
+{
+	int port;
+
+	if (argc != 5) {
+		fprintf(stderr,
+			"usage: %s server|client <ip> <port> <syncdir>\n",
+			argv[0]);
+		return 2;
+	}
+
+	role = argv[1];
+	port = atoi(argv[3]);
+
+	if (!strcmp(role, "server"))
+		return do_server(argv[2], port, argv[4]);
+	if (!strcmp(role, "client"))
+		return do_client(argv[2], port, argv[4]);
+
+	fprintf(stderr, "unknown role %s\n", role);
+	return 2;
+}
-- 
2.43.0


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

end of thread, other threads:[~2026-07-28 12:57 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-28 12:56 [RFC PATCH net-next 0/2] netdevsim: add TLS device offload emulation and test Jiayuan Chen
2026-07-28 12:56 ` [RFC PATCH net-next 1/2] netdevsim: add TLS device offload emulation Jiayuan Chen
2026-07-28 12:56 ` [RFC PATCH net-next 2/2] selftests: netdevsim: add a kTLS device offload test Jiayuan Chen

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.