From: Stephen Hemminger <stephen@networkplumber.org>
To: dev@dpdk.org
Cc: Stephen Hemminger <stephen@networkplumber.org>,
Thomas Monjalon <thomas@monjalon.net>,
Reshma Pattan <reshma.pattan@intel.com>
Subject: [RFC 3/4] test: add test for capture hooks
Date: Tue, 9 Jun 2026 14:02:04 -0700 [thread overview]
Message-ID: <20260609210540.768074-4-stephen@networkplumber.org> (raw)
In-Reply-To: <20260609210540.768074-1-stephen@networkplumber.org>
Provide tests to exercise telemetry based packet capture.
Signed-off-by: Stephen Hemminger <stephen@networkplumber.org>
---
MAINTAINERS | 1 +
app/test/meson.build | 1 +
app/test/test_capture.c | 365 ++++++++++++++++++++++++++++++++++++++++
3 files changed, 367 insertions(+)
create mode 100644 app/test/test_capture.c
diff --git a/MAINTAINERS b/MAINTAINERS
index dd359d956e..ff5f31c770 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1724,6 +1724,7 @@ Packet capture
M: Reshma Pattan <reshma.pattan@intel.com>
M: Stephen Hemminger <stephen@networkplumber.org>
F: lib/capture/
+F: app/test/test_capture.c
F: lib/pdump/
F: doc/guides/prog_guide/pdump_lib.rst
F: app/test/test_pdump.*
diff --git a/app/test/meson.build b/app/test/meson.build
index 61024125a7..e1806ec4ca 100644
--- a/app/test/meson.build
+++ b/app/test/meson.build
@@ -137,6 +137,7 @@ source_file_deps = {
'test_net_ip6.c': ['net'],
'test_pcapng.c': ['net_null', 'net', 'ethdev', 'pcapng', 'bus_vdev'],
'test_pdcp.c': ['eventdev', 'pdcp', 'net', 'timer', 'security'],
+ 'test_capture.c': ['net_ring', 'net', 'ethdev', 'bus_vdev', 'telemetry'],
'test_pdump.c': ['pdump'] + sample_packet_forward_deps,
'test_per_lcore.c': [],
'test_pflock.c': [],
diff --git a/app/test/test_capture.c b/app/test/test_capture.c
new file mode 100644
index 0000000000..ac4dfc43c9
--- /dev/null
+++ b/app/test/test_capture.c
@@ -0,0 +1,365 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2026 Stephen Hemminger
+ */
+
+/*
+ * Functional test for the capture library.
+ *
+ * The capture library has no public C API: it is driven entirely through the
+ * telemetry socket, and the pcapng output is delivered over a file descriptor
+ * passed to the primary process with SCM_RIGHTS. This test therefore behaves
+ * like an external capture tool. It:
+ *
+ * 1. builds a virtual ethdev backed by rings (net_ring), like test_pdump.c;
+ * 2. connects to this process's own telemetry socket;
+ * 3. starts a capture, passing the write end of a pipe as the output fd;
+ * 4. injects packets through the port and checks that
+ * - a pcapng stream appears on the pipe,
+ * - /ethdev/capture/list reports the capture,
+ * - /ethdev/capture/stats reports the expected accepted count;
+ * 5. closes the read end and checks the capture tears itself down and
+ * disappears from /ethdev/capture/list.
+ *
+ * The test is skipped (not failed) if telemetry is not enabled or the ring
+ * driver is not available.
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <sys/select.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+
+#include <rte_cycles.h>
+#include <rte_eal.h>
+#include <rte_ethdev.h>
+#include <rte_eth_ring.h>
+#include <rte_mbuf.h>
+#include <rte_ring.h>
+
+#include "test.h"
+
+#define TELEMETRY_VERSION "v2"
+#define CAPTURE_START "/ethdev/capture/start"
+#define CAPTURE_LIST "/ethdev/capture/list"
+#define CAPTURE_STATS "/ethdev/capture/stats"
+
+#define RING_SIZE 256
+#define NB_MBUFS 1024
+#define MBUF_CACHE 32
+#define NB_PKTS 32
+#define PKT_LEN 64
+#define REPLY_LEN 16384
+
+/* pcapng Section Header Block type, byte-order independent on disk. */
+static const uint8_t pcapng_shb_magic[4] = { 0x0a, 0x0d, 0x0d, 0x0a };
+
+static struct rte_mempool *test_mp;
+static struct rte_ring *rx_ring, *tx_ring;
+static uint16_t test_port = RTE_MAX_ETHPORTS;
+
+/* --- telemetry client helpers ------------------------------------------ */
+
+/* Connect to this process's telemetry socket; -1 (and skip) if unavailable. */
+static int
+tel_connect(void)
+{
+ struct sockaddr_un addr = { .sun_family = AF_UNIX };
+ char buf[REPLY_LEN];
+ int s;
+
+ snprintf(addr.sun_path, sizeof(addr.sun_path), "%s/dpdk_telemetry.%s",
+ rte_eal_get_runtime_dir(), TELEMETRY_VERSION);
+
+ s = socket(AF_UNIX, SOCK_SEQPACKET, 0);
+ if (s < 0)
+ return -1;
+
+ if (connect(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
+ close(s);
+ return -1;
+ }
+
+ /* Server greets with an info message; consume it. */
+ if (recv(s, buf, sizeof(buf), 0) <= 0) {
+ close(s);
+ return -1;
+ }
+ return s;
+}
+
+/* Send a command (no fd) and read the reply. */
+static int
+tel_cmd(int s, const char *cmd, char *reply, size_t reply_sz)
+{
+ ssize_t n;
+
+ if (send(s, cmd, strlen(cmd), 0) < 0)
+ return -1;
+ n = recv(s, reply, reply_sz - 1, 0);
+ if (n < 0)
+ return -1;
+ reply[n] = '\0';
+ return 0;
+}
+
+/* Send a command passing one fd as SCM_RIGHTS, discard the reply. */
+static int
+tel_cmd_fd(int s, const char *cmd, int fd)
+{
+ char cbuf[CMSG_SPACE(sizeof(int))] = { 0 };
+ char reply[REPLY_LEN];
+ struct iovec iov = { .iov_base = (void *)(uintptr_t)cmd, .iov_len = strlen(cmd) };
+ struct msghdr msg = {
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ .msg_control = cbuf,
+ .msg_controllen = sizeof(cbuf),
+ };
+ struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
+
+ cmsg->cmsg_level = SOL_SOCKET;
+ cmsg->cmsg_type = SCM_RIGHTS;
+ cmsg->cmsg_len = CMSG_LEN(sizeof(int));
+ memcpy(CMSG_DATA(cmsg), &fd, sizeof(int));
+
+ if (sendmsg(s, &msg, 0) < 0)
+ return -1;
+ if (recv(s, reply, sizeof(reply), 0) < 0)
+ return -1;
+ return 0;
+}
+
+/* Minimal JSON scanning: find "key" and read the unsigned number after it. */
+static int
+json_uint(const char *s, const char *key, uint64_t *out)
+{
+ const char *p = strstr(s, key);
+
+ if (p == NULL)
+ return -1;
+ for (p += strlen(key); *p != '\0' && !isdigit((unsigned char)*p); p++)
+ ;
+ if (*p == '\0')
+ return -1;
+ *out = strtoull(p, NULL, 10);
+ return 0;
+}
+
+/* Read the first element of the array in a list reply; -1 if empty/absent. */
+static int
+json_first_array_uint(const char *s, uint64_t *out)
+{
+ const char *p = strchr(s, '[');
+
+ if (p == NULL)
+ return -1;
+ for (p++; *p == ' '; p++)
+ ;
+ if (*p == ']' || !isdigit((unsigned char)*p))
+ return -1;
+ *out = strtoull(p, NULL, 10);
+ return 0;
+}
+
+/* --- packet injection --------------------------------------------------- */
+
+/* Push NB_PKTS minimal packets through the port's Rx path. */
+static int
+inject_rx(unsigned int count)
+{
+ struct rte_mbuf *bufs[NB_PKTS];
+ uint16_t got;
+
+ if (count > NB_PKTS)
+ count = NB_PKTS;
+
+ for (unsigned int i = 0; i < count; i++) {
+ struct rte_mbuf *m = rte_pktmbuf_alloc(test_mp);
+
+ if (m == NULL) {
+ rte_pktmbuf_free_bulk(bufs, i);
+ return -1;
+ }
+ m->pkt_len = m->data_len = PKT_LEN;
+ memset(rte_pktmbuf_mtod(m, void *), 0, PKT_LEN);
+ bufs[i] = m;
+ }
+
+ if (rte_ring_enqueue_bulk(rx_ring, (void **)bufs, count, NULL) != count) {
+ rte_pktmbuf_free_bulk(bufs, count);
+ return -1;
+ }
+
+ /* Pulling from the port runs the capture Rx callback on each packet. */
+ got = rte_eth_rx_burst(test_port, 0, bufs, count);
+ rte_pktmbuf_free_bulk(bufs, got);
+ return 0;
+}
+
+/* --- fixture ------------------------------------------------------------ */
+
+static int
+build_port(void)
+{
+ struct rte_eth_conf conf = { 0 };
+ int ret;
+
+ test_mp = rte_pktmbuf_pool_create("capture_test_mp", NB_MBUFS, MBUF_CACHE,
+ 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());
+ if (test_mp == NULL)
+ return -1;
+
+ rx_ring = rte_ring_create("capture_test_rx", RING_SIZE, rte_socket_id(),
+ RING_F_SP_ENQ | RING_F_SC_DEQ);
+ tx_ring = rte_ring_create("capture_test_tx", RING_SIZE, rte_socket_id(),
+ RING_F_SP_ENQ | RING_F_SC_DEQ);
+ if (rx_ring == NULL || tx_ring == NULL)
+ return -1;
+
+ ret = rte_eth_from_rings("net_capture_test", &rx_ring, 1, &tx_ring, 1, rte_socket_id());
+ if (ret < 0)
+ return -1;
+ test_port = ret;
+
+ if (rte_eth_dev_configure(test_port, 1, 1, &conf) < 0)
+ return -1;
+ if (rte_eth_rx_queue_setup(test_port, 0, RING_SIZE, rte_socket_id(), NULL, test_mp) < 0)
+ return -1;
+ if (rte_eth_tx_queue_setup(test_port, 0, RING_SIZE, rte_socket_id(), NULL) < 0)
+ return -1;
+ if (rte_eth_dev_start(test_port) < 0)
+ return -1;
+
+ return 0;
+}
+
+static void
+teardown_port(void)
+{
+ if (test_port != RTE_MAX_ETHPORTS) {
+ rte_eth_dev_stop(test_port);
+ rte_eth_dev_close(test_port);
+ test_port = RTE_MAX_ETHPORTS;
+ }
+ rte_ring_free(rx_ring);
+ rte_ring_free(tx_ring);
+ rte_mempool_free(test_mp);
+ rx_ring = tx_ring = NULL;
+ test_mp = NULL;
+}
+
+/* --- the test ----------------------------------------------------------- */
+
+static int
+test_capture(void)
+{
+ char cmd[128], reply[REPLY_LEN], pcapng[REPLY_LEN];
+ int sock = -1, pipefd[2] = { -1, -1 };
+ int ret = TEST_FAILED;
+ uint64_t id, accepted;
+ struct timeval tv;
+ fd_set rfds;
+ ssize_t n;
+
+ /* The drain thread writes to the pipe; a closed reader must give EPIPE,
+ * not a fatal SIGPIPE. (The library itself should arguably ignore
+ * SIGPIPE too; see review notes.)
+ */
+ signal(SIGPIPE, SIG_IGN);
+
+ sock = tel_connect();
+ if (sock < 0) {
+ printf("telemetry socket not available, skipping\n");
+ return TEST_SKIPPED;
+ }
+
+ if (build_port() < 0) {
+ printf("could not build ring-backed test port, skipping\n");
+ ret = TEST_SKIPPED;
+ goto out;
+ }
+
+ if (pipe(pipefd) < 0)
+ goto out;
+
+ /* Start the capture, handing it the write end of the pipe. */
+ snprintf(cmd, sizeof(cmd), "%s,%u", CAPTURE_START, test_port);
+ TEST_ASSERT_SUCCESS(tel_cmd_fd(sock, cmd, pipefd[1]),
+ "capture start command failed");
+
+ /* The library now holds its own dup of the write end; drop ours so the
+ * capture sees a hangup once we close the read end below.
+ */
+ close(pipefd[1]);
+ pipefd[1] = -1;
+
+ /* Inject traffic. Rx callbacks run synchronously inside rx_burst, so the
+ * accepted counter is up to date as soon as this returns.
+ */
+ TEST_ASSERT_SUCCESS(inject_rx(NB_PKTS), "packet injection failed");
+
+ /* A pcapng stream (at least the section header) must appear. */
+ FD_ZERO(&rfds);
+ FD_SET(pipefd[0], &rfds);
+ tv = (struct timeval){ .tv_sec = 2 };
+ TEST_ASSERT(select(pipefd[0] + 1, &rfds, NULL, NULL, &tv) > 0,
+ "no pcapng output within timeout");
+ n = read(pipefd[0], pcapng, sizeof(pcapng));
+ TEST_ASSERT(n >= 4, "short pcapng read (%zd)", n);
+ TEST_ASSERT(memcmp(pcapng, pcapng_shb_magic, sizeof(pcapng_shb_magic)) == 0,
+ "output does not start with a pcapng section header block");
+
+ /* The capture must show up in the list. */
+ TEST_ASSERT_SUCCESS(tel_cmd(sock, CAPTURE_LIST, reply, sizeof(reply)),
+ "capture list command failed");
+ TEST_ASSERT_SUCCESS(json_first_array_uint(reply, &id),
+ "no capture id in list reply: %s", reply);
+
+ /* Stats must report exactly the packets we injected. */
+ snprintf(cmd, sizeof(cmd), "%s,%" PRIu64, CAPTURE_STATS, id);
+ TEST_ASSERT_SUCCESS(tel_cmd(sock, cmd, reply, sizeof(reply)),
+ "capture stats command failed");
+ TEST_ASSERT_SUCCESS(json_uint(reply, "\"accepted\"", &accepted),
+ "no accepted counter in stats reply: %s", reply);
+ TEST_ASSERT_EQUAL(accepted, (uint64_t)NB_PKTS,
+ "accepted %" PRIu64 " != %d", accepted, NB_PKTS);
+
+ /* Close the reader: the capture should tear itself down. The drain
+ * thread only notices on its next write, so nudge it with more traffic.
+ */
+ close(pipefd[0]);
+ pipefd[0] = -1;
+ inject_rx(NB_PKTS);
+
+ for (int i = 0; i < 200; i++) { /* up to ~2s */
+ TEST_ASSERT_SUCCESS(tel_cmd(sock, CAPTURE_LIST, reply, sizeof(reply)),
+ "capture list command failed");
+ if (json_first_array_uint(reply, &id) < 0) {
+ ret = TEST_SUCCESS;
+ goto out;
+ }
+ rte_delay_ms(10);
+ }
+ printf("capture did not tear down after reader closed: %s\n", reply);
+
+out:
+ if (pipefd[0] >= 0)
+ close(pipefd[0]);
+ if (pipefd[1] >= 0)
+ close(pipefd[1]);
+ if (sock >= 0)
+ close(sock);
+ teardown_port();
+ return ret;
+}
+
+REGISTER_TEST_COMMAND(capture_autotest, test_capture);
--
2.53.0
next prev parent reply other threads:[~2026-06-09 21:06 UTC|newest]
Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-09 21:02 [RFC 0/4] alternative capture mechanism Stephen Hemminger
2026-06-09 21:02 ` [RFC 1/4] telemetry: allow commands to receive file descriptors Stephen Hemminger
2026-06-16 12:32 ` Bruce Richardson
2026-06-16 14:26 ` Stephen Hemminger
2026-06-09 21:02 ` [RFC 2/4] capture: infrastructure wireshark packet capture Stephen Hemminger
2026-06-09 21:02 ` Stephen Hemminger [this message]
2026-06-09 21:02 ` [RFC 4/4] usertools/dpdk-wireshark-extcap.py: script for external capture Stephen Hemminger
2026-06-16 12:37 ` [RFC 0/4] alternative capture mechanism Bruce Richardson
2026-06-16 14:28 ` Stephen Hemminger
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=20260609210540.768074-4-stephen@networkplumber.org \
--to=stephen@networkplumber.org \
--cc=dev@dpdk.org \
--cc=reshma.pattan@intel.com \
--cc=thomas@monjalon.net \
/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 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.