From: Sabrina Dubroca <sd@queasysnail.net>
To: Rishikesh Jethwani <rjethwani@purestorage.com>
Cc: netdev@vger.kernel.org, saeedm@nvidia.com, tariqt@nvidia.com,
mbloch@nvidia.com, borisp@nvidia.com, john.fastabend@gmail.com,
kuba@kernel.org, davem@davemloft.net, pabeni@redhat.com,
edumazet@google.com, leon@kernel.org
Subject: Re: [PATCH v11 6/6] selftests: net: add TLS hardware offload test
Date: Thu, 2 Apr 2026 18:21:17 +0200 [thread overview]
Message-ID: <ac6XfZJ9I6itcXxE@krikkit> (raw)
In-Reply-To: <20260331163757.149343-7-rjethwani@purestorage.com>
2026-03-31, 10:37:57 -0600, Rishikesh Jethwani wrote:
> diff --git a/tools/testing/selftests/drivers/net/hw/tls_hw_offload.c b/tools/testing/selftests/drivers/net/hw/tls_hw_offload.c
> new file mode 100644
> index 000000000000..69ee834bfabd
> --- /dev/null
> +++ b/tools/testing/selftests/drivers/net/hw/tls_hw_offload.c
[...]
> +static int do_rekey;
> +static int num_rekeys = 1;
> +static int rekeys_done;
> +static int cipher_type = 128;
> +static int tls_version = 13;
You could directly use the uapi constants (TLS_1_3_VERSION,
TLS_CIPHER_AES_GCM_128) instead of coming up with your own.
> +static int server_port = 4433;
> +static char *server_ip;
> +static int addr_family = AF_INET;
> +
> +static int send_size = 16384;
> +static int random_size_max;
> +
> +static int detect_addr_family(const char *ip)
The python code only runs IPv4 tests.
(do_client, do_server, and main also have some v6 stuff that will
never get used)
[...]
> +static void derive_key_256(struct tls12_crypto_info_aes_gcm_256 *key,
> + int generation)
That's almost an exact c/p of derive_key_128, except for a few s/128/256/.
[...]
> +/* Send TLS 1.3 KeyUpdate handshake message */
> +static int send_tls_key_update(int fd, int request_update)
> +{
> + char cmsg_buf[CMSG_SPACE(sizeof(unsigned char))];
> + unsigned char key_update_msg[5];
> + struct msghdr msg = {0};
> + struct cmsghdr *cmsg;
> + struct iovec iov;
> +
> + key_update_msg[0] = TLS_HANDSHAKE_KEY_UPDATE;
> + key_update_msg[1] = 0;
> + key_update_msg[2] = 0;
> + key_update_msg[3] = 1;
> + key_update_msg[4] = request_update ? KEY_UPDATE_REQUESTED
> + : KEY_UPDATE_NOT_REQUESTED;
All callers are already passing KEY_UPDATE_REQUESTED or
KEY_UPDATE_NOT_REQUESTED. But I'm not sure why you're bothering with
this, since the RX side only checks buf[0] == TLS_HANDSHAKE_KEY_UPDATE.
[...]
> +static int recv_tls_message(int fd, char *buf, size_t buflen, int *record_type)
> +{
> + char cmsg_buf[CMSG_SPACE(sizeof(unsigned char))];
> + struct msghdr msg = {0};
> + struct cmsghdr *cmsg;
> + struct iovec iov;
> + int ret;
> +
> + iov.iov_base = buf;
> + iov.iov_len = buflen;
> +
> + msg.msg_iov = &iov;
> + msg.msg_iovlen = 1;
> + msg.msg_control = cmsg_buf;
> + msg.msg_controllen = sizeof(cmsg_buf);
> +
> + ret = recvmsg(fd, &msg, 0);
> + if (ret <= 0)
> + return ret;
> +
> + *record_type = TLS_RECORD_TYPE_APPLICATION_DATA; /* default */
Is a default value actually needed? When userspace provides cmsg
space, tls_sw_recvmsg always fills it.
> + cmsg = CMSG_FIRSTHDR(&msg);
> + if (cmsg && cmsg->cmsg_level == SOL_TLS &&
> + cmsg->cmsg_type == TLS_GET_RECORD_TYPE)
> + *record_type = *((unsigned char *)CMSG_DATA(cmsg));
> +
> + return ret;
> +}
[...]
> +static void check_ekeyexpired(int fd)
> +{
> + char buf[16];
> + int ret;
> +
> + ret = recv(fd, buf, sizeof(buf), MSG_DONTWAIT);
> + if (ret == -1 && errno == EKEYEXPIRED)
> + printf("recv() returned EKEYEXPIRED as expected\n");
> + else if (ret == -1 && errno == EAGAIN)
> + printf("recv() returned EAGAIN (no pending data)\n");
> + else if (ret == -1)
> + printf("recv() returned error: %s\n", strerror(errno));
Nothing for "we expected EKEYEXPIRED but actually got data"?
And this is only for logging/debugging? It's not having on impact on
the pass/fail results.
> +}
> +
> +static int do_tls_rekey(int fd, int is_tx, int generation, int cipher)
That's almost exact c/p of setup_tls_key. Please clean up all this
duplicated code.
[...]
> +static int do_client(void)
> +{
> + struct sockaddr_storage sa;
> + char *buf = NULL, *echo_buf = NULL;
> + int max_size, rekey_interval;
> + ssize_t echo_total, echo_n;
> + int csk = -1, ret, i, j;
> + int test_result = 0;
> + int current_gen = 0;
> + int next_rekey_at;
> + socklen_t sa_len;
> + ssize_t n;
> +
> + if (!server_ip) {
> + printf("ERROR: Client requires -s <ip> option\n");
> + return -1;
> + }
> +
> + max_size = random_size_max > 0 ? random_size_max : send_size;
> + if (max_size < MIN_BUF_SIZE)
> + max_size = MIN_BUF_SIZE;
> + buf = malloc(max_size);
> + echo_buf = malloc(max_size);
> + if (!buf || !echo_buf) {
> + printf("failed to allocate buffers\n");
> + test_result = -1;
> + goto out;
> + }
> +
> + csk = socket(addr_family, SOCK_STREAM, IPPROTO_TCP);
> + if (csk < 0) {
> + printf("failed to create socket: %s\n", strerror(errno));
> + test_result = -1;
> + goto out;
> + }
> +
> + memset(&sa, 0, sizeof(sa));
> + if (addr_family == AF_INET6) {
This will never get used.
[...]
> + if (setup_tls_key(csk, 1, 0, cipher_type) < 0 ||
> + setup_tls_key(csk, 0, 0, cipher_type) < 0) {
This could just use TLS_TX/TLS_RX directly instead of is_tx={0,1}.
> + test_result = -1;
> + goto out;
> + }
> +
> + if (do_rekey)
> + printf("TLS %s setup complete. Will perform %d rekey(s).\n",
> + cipher_name(cipher_type), num_rekeys);
> + else
> + printf("TLS setup complete.\n");
> +
> + if (random_size_max > 0)
> + printf("Sending %d messages of random size (1..%d bytes)...\n",
> + TEST_ITERATIONS, random_size_max);
> + else
> + printf("Sending %d messages of %d bytes...\n",
> + TEST_ITERATIONS, send_size);
> +
> + rekey_interval = TEST_ITERATIONS / (num_rekeys + 1);
> + if (rekey_interval < 1)
> + rekey_interval = 1;
> + next_rekey_at = rekey_interval;
> +
> + for (i = 0; i < TEST_ITERATIONS; i++) {
> + int this_size;
> +
> + if (random_size_max > 0)
> + this_size = (rand() % random_size_max) + 1;
> + else
> + this_size = send_size;
> +
> + for (j = 0; j < this_size; j++)
> + buf[j] = rand() & 0xFF;
> +
> + n = send(csk, buf, this_size, 0);
> + if (n != this_size) {
> + printf("FAIL: send failed: %s\n", strerror(errno));
> + test_result = -1;
> + break;
> + }
> + printf("Sent %zd bytes (iteration %d)\n", n, i + 1);
> +
> + echo_total = 0;
> + while (echo_total < n) {
> + echo_n = recv(csk, echo_buf + echo_total,
> + n - echo_total, 0);
> + if (echo_n < 0) {
> + printf("FAIL: Echo recv failed: %s\n",
> + strerror(errno));
> + test_result = -1;
> + break;
> + }
> + if (echo_n == 0) {
> + printf("FAIL: Connection closed during echo\n");
> + test_result = -1;
> + break;
> + }
> + echo_total += echo_n;
> + }
> + if (test_result != 0)
> + break;
nit: replacing the breaks from the echo_total loop with gotos would be
more readable than this double break (but then it probably makes sense
to also replace all the "test_result = -1; break;" from the whole
TEST_ITERATIONS loop)
> +
> + if (memcmp(buf, echo_buf, n) != 0) {
> + printf("FAIL: Echo data mismatch!\n");
> + test_result = -1;
> + break;
> + }
> + printf("Received echo %zd bytes (ok)\n", echo_total);
> +
> + /* Rekey at intervals: send KeyUpdate, update TX, recv KeyUpdate, update RX */
> + if (do_rekey && rekeys_done < num_rekeys &&
> + (i + 1) == next_rekey_at) {
> + current_gen++;
> + printf("\n=== Client Rekey #%d (gen %d) ===\n",
> + rekeys_done + 1, current_gen);
If I'm reading this loop correctly, rekeys_done+1 is always equal to
current_gen here, since we'll either break out or also increment
rekeys_done at the end of the block. (same in do_server)
[...]
> +static int do_server(void)
> +{
> + struct sockaddr_storage sa;
> + int lsk = -1, csk = -1, ret;
> + ssize_t n, total = 0, sent;
> + int current_gen = 0;
> + int test_result = 0;
> + int recv_count = 0;
> + char *buf = NULL;
> + int record_type;
> + socklen_t sa_len;
> + int max_size;
> + int one = 1;
> +
> + max_size = random_size_max > 0 ? random_size_max : send_size;
The server side is always reading max_size if it can. Just skip the
"random" on server? Otherwise make it receive and echo back random
amounts of bytes at each iteration.
[...]
> + /* Main receive loop - detect KeyUpdate via MSG_PEEK + recvmsg */
> + while (1) {
> + n = recv(csk, buf, max_size, MSG_PEEK | MSG_DONTWAIT);
> + if (n < 0 &&
> + (errno == EIO || errno == ENOMSG || errno == EAGAIN)) {
I don't think ktls ever produces ENOMSG.
> + n = recv_tls_message(csk, buf, max_size, &record_type);
> + } else if (n > 0) {
> + n = recv_tls_message(csk, buf, max_size, &record_type);
Why the PEEK if you're always going to call recv_tls_message afterwards?
> + } else if (n == 0) {
> + printf("Connection closed by client\n");
> + break;
> + }
> +
> + if (n <= 0) {
> + if (n < 0)
> + printf("recv failed: %s\n", strerror(errno));
> + break;
> + }
> +
> + /* Handle KeyUpdate: update RX, send response, update TX */
> + if (record_type == TLS_RECORD_TYPE_HANDSHAKE &&
> + n >= 1 && buf[0] == TLS_HANDSHAKE_KEY_UPDATE) {
We just excluded n <= 0, so n >= 1 is always true here.
[...]
> + sent = send(csk, buf, n, 0);
> + if (sent < 0) {
> + printf("Echo send failed: %s\n", strerror(errno));
> + break;
> + }
> + if (sent != n)
> + printf("Echo partial: %zd of %zd bytes\n", sent, n);
If this happens, the client will fall out of sync with the server and
get stuck in the "echo_total < n" loop?
> + printf("Echoed %zd bytes back to client\n", sent);
> + }
> +
> + printf("Connection closed. Total received: %zd bytes\n", total);
> + if (do_rekey)
> + printf("Rekeys completed: %d\n", rekeys_done);
> +
> +out:
> + if (csk >= 0)
> + close(csk);
> + if (lsk >= 0)
> + close(lsk);
> + free(buf);
> + return test_result;
> +}
> +
> +static void parse_rekey_option(const char *arg)
> +{
> + int requested;
> +
> + if (strncmp(arg, "--rekey=", 8) == 0) {
> + requested = atoi(arg + 8);
> + if (requested < 1) {
> + printf("WARNING: Invalid rekey count, using 1\n");
> + num_rekeys = 1;
> + } else if (requested > MAX_REKEYS) {
> + printf("WARNING: Rekey count %d > max %d, using %d\n",
> + requested, MAX_REKEYS, MAX_REKEYS);
Why not just abort the test if we get invalid input? When called
through the python wrapper we'll never get broken arguments, and if
invoked manually I think it's better to abort than do something
semi-random.
> + num_rekeys = MAX_REKEYS;
> + } else {
> + num_rekeys = requested;
> + }
> + do_rekey = 1;
> + } else if (strcmp(arg, "--rekey") == 0) {
The python wrapper never uses that.
> + do_rekey = 1;
> + num_rekeys = 1;
> + }
> +}
> +
[...]
> +static void print_usage(const char *prog)
> +{
> + printf("TLS Hardware Offload Two-Node Test\n\n");
> + printf("Usage:\n");
> + printf(" %s server [OPTIONS]\n", prog);
> + printf(" %s client -s <ip> [OPTIONS]\n", prog);
> + printf("\nOptions:\n");
> + printf(" -s <ip> Server IP to connect (client, required)\n");
> + printf(" Supports both IPv4 and IPv6 addresses\n");
> + printf(" -6 Use IPv6 (server only, default: IPv4)\n");
> + printf(" -p <port> Server port (default: 4433)\n");
> + printf(" -b <size> Send buffer (record) size (default: 16384)\n");
nit: not record sizes since this can go beyond the maximum record
size.
> + printf(" -r <max> Use random send buffer sizes (1..<max>)\n");
> + printf(" -v <version> TLS version: 1.2 or 1.3 (default: 1.3)\n");
> + printf(" -c <cipher> Cipher: 128 or 256 (default: 128)\n");
> + printf(" --rekey[=N] Enable rekey (default: 1, TLS 1.3 only)\n");
> + printf(" --help Show this help message\n");
> + printf("\nExample (IPv4):\n");
> + printf(" Node A: %s server\n", prog);
> + printf(" Node B: %s client -s 192.168.20.2\n", prog);
> + printf("\nExample (IPv6):\n");
> + printf(" Node A: %s server -6\n", prog);
> + printf(" Node B: %s client -s 2001:db8::1\n", prog);
> + printf("\nRekey Example (3 rekeys, TLS 1.3 only):\n");
> + printf(" Node A: %s server --rekey=3\n", prog);
> + printf(" Node B: %s client -s 192.168.20.2 --rekey=3\n", prog);
> +}
> +
> +int main(int argc, char *argv[])
> +{
> + int i;
> +
> +
> + for (i = 1; i < argc; i++) {
> + if (strcmp(argv[i], "--help") == 0 ||
> + strcmp(argv[i], "-h") == 0) {
> + print_usage(argv[0]);
> + return 0;
> + }
> + }
> +
> + for (i = 1; i < argc; i++) {
I'm not a huge fan of getopt(3) but... please use it.
[...]
> + if (tls_version == 12 && do_rekey) {
> + printf("WARNING: TLS 1.2 does not support rekey\n");
I'd also abort the test here.
[...]
> diff --git a/tools/testing/selftests/drivers/net/hw/tls_hw_offload.py b/tools/testing/selftests/drivers/net/hw/tls_hw_offload.py
> new file mode 100755
> index 000000000000..5d14cb7d2e3c
> --- /dev/null
> +++ b/tools/testing/selftests/drivers/net/hw/tls_hw_offload.py
> @@ -0,0 +1,281 @@
> +#!/usr/bin/env python3
> +# SPDX-License-Identifier: GPL-2.0
> +
> +"""Test kTLS hardware offload using a C helper binary."""
> +
> +from lib.py import ksft_run, ksft_exit, ksft_pr, KsftSkipEx, ksft_true
> +from lib.py import NetDrvEpEnv
> +from lib.py import cmd, bkg, wait_port_listen, rand_port
> +import time
> +
> +
> +def check_tls_support(cfg):
> + try:
> + cmd("test -f /proc/net/tls_stat")
> + cmd("test -f /proc/net/tls_stat", host=cfg.remote)
> + except Exception as e:
> + raise KsftSkipEx(f"kTLS not supported: {e}")
> +
> +
> +def read_tls_stats():
> + stats = {}
> + output = cmd("cat /proc/net/tls_stat")
> + for line in output.stdout.strip().split('\n'):
> + parts = line.split()
> + if len(parts) == 2:
> + stats[parts[0]] = int(parts[1])
> + return stats
> +
> +
> +def verify_tls_counters(stats_before, stats_after, expected_rekeys, is_server):
> + tx_device_diff = (stats_after.get('TlsTxDevice', 0) -
> + stats_before.get('TlsTxDevice', 0))
Not a big python person, but maybe use defaultdict to make this a bit
more readable?
> + rx_device_diff = (stats_after.get('TlsRxDevice', 0) -
> + stats_before.get('TlsRxDevice', 0))
> + tx_sw_diff = (stats_after.get('TlsTxSw', 0) -
> + stats_before.get('TlsTxSw', 0))
> + rx_sw_diff = (stats_after.get('TlsRxSw', 0) -
> + stats_before.get('TlsRxSw', 0))
> + decrypt_err_diff = (stats_after.get('TlsDecryptError', 0) -
> + stats_before.get('TlsDecryptError', 0))
> +
> + used_tx_hw = tx_device_diff >= 1
> + used_rx_hw = rx_device_diff >= 1
> + used_tx_sw = tx_sw_diff >= 1
> + used_rx_sw = rx_sw_diff >= 1
> +
> + errors = 0
> +
> + role = 'Server' if is_server else 'Client'
> + ksft_pr(f"=== Counter Verification ({role}) ===")
> +
> + tx_dev_before = stats_before.get('TlsTxDevice', 0)
> + tx_dev_after = stats_after.get('TlsTxDevice', 0)
> + ksft_pr(f"TlsTxDevice: {tx_dev_before} -> {tx_dev_after} "
> + f"(diff: {tx_device_diff})")
> +
> + tx_sw_before = stats_before.get('TlsTxSw', 0)
> + tx_sw_after = stats_after.get('TlsTxSw', 0)
> + ksft_pr(f"TlsTxSw: {tx_sw_before} -> {tx_sw_after} "
> + f"(diff: {tx_sw_diff})")
I don't know how nice we want the selftest code to be but... this
whole function is just the same "get before/get after/diff/print"
repeated all over. Please clean this up.
[...]
> +def run_tls_test(cfg, cipher="128", tls_version="1.3", rekey=0, buffer_size=None, random_max=None):
> + port = rand_port()
> +
> + server_cmd = f"{cfg.bin_remote} server -p {port} -c {cipher} -v {tls_version}"
> + if rekey > 0:
> + server_cmd += f" --rekey={rekey}"
The rekey option has no effect on the server side.
[...]
> +def read_tls_stats_remote(cfg):
> + stats = {}
> + output = cmd("cat /proc/net/tls_stat", host=cfg.remote)
> + for line in output.stdout.strip().split('\n'):
> + parts = line.split()
> + if len(parts) == 2:
> + stats[parts[0]] = int(parts[1])
> + return stats
And that's again almost exactly a c/p of read_tls_stats().
> +def test_tls_offload_basic(cfg):
> + cfg.require_ipver("4")
> + check_tls_support(cfg)
> + run_tls_test(cfg, cipher="128", tls_version="1.3", rekey=0)
> +
> +
> +def test_tls_offload_aes256(cfg):
> + cfg.require_ipver("4")
> + check_tls_support(cfg)
> + run_tls_test(cfg, cipher="256", tls_version="1.3", rekey=0)
Looks like you want test variants.
(tools/testing/selftests/drivers/net/README.rst "ksft_variants")
--
Sabrina
next prev parent reply other threads:[~2026-04-02 16:21 UTC|newest]
Thread overview: 10+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-03-31 16:37 [PATCH net-next v11 0/6] tls: Add TLS 1.3 hardware offload support Rishikesh Jethwani
2026-03-31 16:37 ` [PATCH v11 1/6] net: tls: reject TLS 1.3 offload in chcr_ktls and nfp drivers Rishikesh Jethwani
2026-03-31 16:37 ` [PATCH v11 2/6] net/mlx5e: add TLS 1.3 hardware offload support Rishikesh Jethwani
2026-03-31 16:37 ` [PATCH v11 3/6] tls: " Rishikesh Jethwani
2026-03-31 16:37 ` [PATCH v11 4/6] tls: split tls_set_sw_offload into init and finalize stages Rishikesh Jethwani
2026-03-31 16:37 ` [PATCH v11 5/6] tls: add hardware offload key update support Rishikesh Jethwani
2026-03-31 16:37 ` [PATCH v11 6/6] selftests: net: add TLS hardware offload test Rishikesh Jethwani
2026-04-02 16:21 ` Sabrina Dubroca [this message]
2026-03-31 22:58 ` [PATCH net-next v11 0/6] tls: Add TLS 1.3 hardware offload support Jakub Kicinski
2026-04-02 3:16 ` Jakub Kicinski
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=ac6XfZJ9I6itcXxE@krikkit \
--to=sd@queasysnail.net \
--cc=borisp@nvidia.com \
--cc=davem@davemloft.net \
--cc=edumazet@google.com \
--cc=john.fastabend@gmail.com \
--cc=kuba@kernel.org \
--cc=leon@kernel.org \
--cc=mbloch@nvidia.com \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=rjethwani@purestorage.com \
--cc=saeedm@nvidia.com \
--cc=tariqt@nvidia.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox