* [PATCH v6 0/5] Wireshark external capture for DPDK
@ 2026-07-24 21:11 Stephen Hemminger
2026-07-24 21:11 ` [PATCH 1/5] pcapng: extend interface statistics Stephen Hemminger
` (4 more replies)
0 siblings, 5 replies; 6+ messages in thread
From: Stephen Hemminger @ 2026-07-24 21:11 UTC (permalink / raw)
To: dev; +Cc: Stephen Hemminger
This series adds live packet capture from a running DPDK primary over the
telemetry socket. A new capture library installs Rx/Tx callbacks on an
ethdev port and writes pcapng to a caller-supplied path or FIFO, with an
optional libpcap filter and per-queue selection. No application changes are
needed beyond linking the library.
Two front ends use it: a Wireshark extcap plugin that makes each DPDK port
appear in the Wireshark interface list, and a dpdk-dumpcap.py for
file capture without a secondary process.
The pcapng interface-statistics block gains a filter-accepted count,
which changes the rte_pcapng_write_stats() signature.
dpdk-dumpcap.py is not installed yet, since the name would shadow the
existing C dpdk-dumpcap.The long term plan is to remove pdump and
old dumpcap code and only support this new code.
Will take several releases to stablize first.
Stephen Hemminger (5):
pcapng: extend interface statistics
capture: infrastructure wireshark packet capture
test: add test for capture hooks
usertools/dpdk-wireshark-extcap.py: script for external capture
usertools/dpdk-dumpcap: add script for file capture
MAINTAINERS | 4 +
app/dumpcap/main.c | 16 +-
app/test/meson.build | 1 +
app/test/test_capture.c | 512 ++++++++++++
app/test/test_pcapng.c | 6 +-
doc/guides/rel_notes/release_26_11.rst | 11 +
doc/guides/tools/index.rst | 1 +
doc/guides/tools/wireshark_extcap.rst | 144 ++++
lib/capture/capture.c | 1032 ++++++++++++++++++++++++
lib/capture/capture_impl.h | 56 ++
lib/capture/filter.c | 108 +++
lib/capture/meson.build | 19 +
lib/meson.build | 1 +
lib/pcapng/rte_pcapng.c | 32 +-
lib/pcapng/rte_pcapng.h | 30 +-
usertools/dpdk-dumpcap.py | 426 ++++++++++
usertools/dpdk-wireshark-extcap.py | 374 +++++++++
17 files changed, 2744 insertions(+), 29 deletions(-)
create mode 100644 app/test/test_capture.c
create mode 100644 doc/guides/tools/wireshark_extcap.rst
create mode 100644 lib/capture/capture.c
create mode 100644 lib/capture/capture_impl.h
create mode 100644 lib/capture/filter.c
create mode 100644 lib/capture/meson.build
create mode 100755 usertools/dpdk-dumpcap.py
create mode 100755 usertools/dpdk-wireshark-extcap.py
--
2.53.0
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH 1/5] pcapng: extend interface statistics
2026-07-24 21:11 [PATCH v6 0/5] Wireshark external capture for DPDK Stephen Hemminger
@ 2026-07-24 21:11 ` Stephen Hemminger
2026-07-24 21:11 ` [PATCH 2/5] capture: infrastructure wireshark packet capture Stephen Hemminger
` (3 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Stephen Hemminger @ 2026-07-24 21:11 UTC (permalink / raw)
To: dev; +Cc: Stephen Hemminger
The number of packets filtered was not implemented in original
code but useful for analysis.
Signed-off-by: Stephen Hemminger <stephen@networkplumber.org>
---
app/dumpcap/main.c | 16 ++++++-------
app/test/test_pcapng.c | 6 +++--
doc/guides/rel_notes/release_26_11.rst | 5 ++++
lib/pcapng/rte_pcapng.c | 32 ++++++++++++++++++--------
lib/pcapng/rte_pcapng.h | 30 ++++++++++++++++--------
5 files changed, 60 insertions(+), 29 deletions(-)
diff --git a/app/dumpcap/main.c b/app/dumpcap/main.c
index 46a6cb251e..baec68b0b9 100644
--- a/app/dumpcap/main.c
+++ b/app/dumpcap/main.c
@@ -574,8 +574,8 @@ static void
report_packet_stats(dumpcap_out_t out)
{
struct rte_pdump_stats pdump_stats;
+ struct rte_pcapng_interface_stats isb;
struct interface *intf;
- uint64_t ifrecv, ifdrop;
double percent;
fputc('\n', stderr);
@@ -584,22 +584,22 @@ report_packet_stats(dumpcap_out_t out)
continue;
/* do what Wiretap does */
- ifrecv = pdump_stats.accepted + pdump_stats.filtered;
- ifdrop = pdump_stats.nombuf + pdump_stats.ringfull;
+ isb.ifrecv = pdump_stats.accepted + pdump_stats.filtered;
+ isb.ifdrop = pdump_stats.nombuf + pdump_stats.ringfull;
+ isb.filteraccept = pdump_stats.accepted;
if (use_pcapng)
- rte_pcapng_write_stats(out.pcapng, intf->port,
- ifrecv, ifdrop, NULL);
+ rte_pcapng_write_stats(out.pcapng, intf->port, &isb, sizeof(isb), NULL);
- if (ifrecv == 0)
+ if (isb.ifrecv == 0)
percent = 0;
else
- percent = 100. * ifrecv / (ifrecv + ifdrop);
+ percent = 100. * isb.ifrecv / (isb.ifrecv + isb.ifdrop);
fprintf(stderr,
"Packets received/dropped on interface '%s': "
"%"PRIu64 "/%" PRIu64 " (%.1f)\n",
- intf->name, ifrecv, ifdrop, percent);
+ intf->name, isb.ifrecv, isb.ifdrop, percent);
}
}
diff --git a/app/test/test_pcapng.c b/app/test/test_pcapng.c
index d14ea84f0d..f6fd85faad 100644
--- a/app/test/test_pcapng.c
+++ b/app/test/test_pcapng.c
@@ -570,8 +570,10 @@ test_write_packets(void)
goto fail;
/* write a statistics block */
- ret = rte_pcapng_write_stats(pcapng, port_id,
- count, 0, "end of test");
+ struct rte_pcapng_interface_stats isb = {
+ .ifrecv = count,
+ };
+ ret = rte_pcapng_write_stats(pcapng, port_id, &isb, sizeof(isb), "end of test");
if (ret <= 0) {
printf("Write of statistics failed\n");
goto fail;
diff --git a/doc/guides/rel_notes/release_26_11.rst b/doc/guides/rel_notes/release_26_11.rst
index 938617ca75..b27494fee7 100644
--- a/doc/guides/rel_notes/release_26_11.rst
+++ b/doc/guides/rel_notes/release_26_11.rst
@@ -84,6 +84,11 @@ API Changes
Also, make sure to start the actual text at the margin.
=======================================================
+* **pcapng: add packet filtering statistic.**
+
+ The API for ``rte_pcapng_write_stats`` was changed to include
+ recording the number of filtered packets.
+
ABI Changes
-----------
diff --git a/lib/pcapng/rte_pcapng.c b/lib/pcapng/rte_pcapng.c
index b5d1026891..455809d412 100644
--- a/lib/pcapng/rte_pcapng.c
+++ b/lib/pcapng/rte_pcapng.c
@@ -394,9 +394,10 @@ rte_pcapng_add_interface(rte_pcapng_t *self, uint16_t port, uint16_t link_type,
RTE_EXPORT_SYMBOL(rte_pcapng_write_stats)
ssize_t
rte_pcapng_write_stats(rte_pcapng_t *self, uint16_t port_id,
- uint64_t ifrecv, uint64_t ifdrop,
- const char *comment)
+ const struct rte_pcapng_interface_stats *stats,
+ size_t size, const char *comment)
{
+ struct rte_pcapng_interface_stats isb;
struct pcapng_statistics *hdr;
struct pcapng_option *opt;
uint64_t start_time = self->clock.ns_base;
@@ -410,12 +411,19 @@ rte_pcapng_write_stats(rte_pcapng_t *self, uint16_t port_id,
if (comment && strlen(comment) > PCAPNG_STR_MAX)
return -EINVAL;
+ /* Future proof for more/less stats - all UINT64_MAX */
+ memset(&isb, 0xff, sizeof(isb));
+ memcpy(&isb, stats, RTE_MIN(size, sizeof(*stats)));
+
optlen = 0;
- if (ifrecv != UINT64_MAX)
- optlen += pcapng_optlen(sizeof(ifrecv));
- if (ifdrop != UINT64_MAX)
- optlen += pcapng_optlen(sizeof(ifdrop));
+ /* compute how many stats will be added. */
+ if (isb.ifrecv != UINT64_MAX)
+ optlen += pcapng_optlen(sizeof(isb.ifrecv));
+ if (isb.ifdrop != UINT64_MAX)
+ optlen += pcapng_optlen(sizeof(isb.ifdrop));
+ if (isb.filteraccept != UINT64_MAX)
+ optlen += pcapng_optlen(sizeof(isb.filteraccept));
if (start_time != 0)
optlen += pcapng_optlen(sizeof(start_time));
@@ -439,12 +447,16 @@ rte_pcapng_write_stats(rte_pcapng_t *self, uint16_t port_id,
if (start_time != 0)
opt = pcapng_add_option(opt, PCAPNG_ISB_STARTTIME,
&start_time, sizeof(start_time));
- if (ifrecv != UINT64_MAX)
+ if (isb.ifrecv != UINT64_MAX)
opt = pcapng_add_option(opt, PCAPNG_ISB_IFRECV,
- &ifrecv, sizeof(ifrecv));
- if (ifdrop != UINT64_MAX)
+ &isb.ifrecv, sizeof(uint64_t));
+ if (isb.ifdrop != UINT64_MAX)
opt = pcapng_add_option(opt, PCAPNG_ISB_IFDROP,
- &ifdrop, sizeof(ifdrop));
+ &isb.ifdrop, sizeof(uint64_t));
+ if (isb.filteraccept != UINT64_MAX)
+ opt = pcapng_add_option(opt, PCAPNG_ISB_FILTERACCEPT,
+ &isb.filteraccept, sizeof(uint64_t));
+
if (optlen != 0)
opt = pcapng_add_option(opt, PCAPNG_OPT_END, NULL, 0);
diff --git a/lib/pcapng/rte_pcapng.h b/lib/pcapng/rte_pcapng.h
index d8d328f710..172455af60 100644
--- a/lib/pcapng/rte_pcapng.h
+++ b/lib/pcapng/rte_pcapng.h
@@ -178,21 +178,33 @@ ssize_t
rte_pcapng_write_packets(rte_pcapng_t *self,
struct rte_mbuf *pkts[], uint16_t nb_pkts);
+/**
+ * A structure to store interface statistics in pcapng
+ * Interface statistics block.
+ * If the statistic is unavailable or unknown, use UINT64_MAX.
+ *
+ * Subset of definitions come from IETF pcapng standard.
+ * The osdrop and usrdeliv statistics are not included because
+ * DPDK does not use OS and does not deliver to application over sockets.
+ */
+struct rte_pcapng_interface_stats {
+ uint64_t ifrecv; /**< Packets received on interface during capture. */
+ uint64_t ifdrop; /**< Packets dropped by interface due to lack of resources. */
+ uint64_t filteraccept; /**< Packets accepted by filter. */
+};
+
/**
* Write an Interface statistics block.
- * For statistics, use 0 if don't know or care to report it.
* Should be called before closing capture to report results.
*
* @param self
* The handle to the packet capture file
* @param port
* The Ethernet port to report stats on.
- * @param ifrecv
- * The number of packets received by capture.
- * Optional: use UINT64_MAX if not known.
- * @param ifdrop
- * The number of packets missed by the capture process.
- * Optional: use UINT64_MAX if not known.
+ * @param stats
+ * The statistics to write.
+ * @param stats_sz
+ * The sizeof statistics structure.
* @param comment
* Optional comment to add to statistics.
* @return
@@ -203,8 +215,8 @@ rte_pcapng_write_packets(rte_pcapng_t *self,
*/
ssize_t
rte_pcapng_write_stats(rte_pcapng_t *self, uint16_t port,
- uint64_t ifrecv, uint64_t ifdrop,
- const char *comment);
+ const struct rte_pcapng_interface_stats *stats,
+ size_t stats_sz, const char *comment);
#ifdef __cplusplus
}
--
2.53.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH 2/5] capture: infrastructure wireshark packet capture
2026-07-24 21:11 [PATCH v6 0/5] Wireshark external capture for DPDK Stephen Hemminger
2026-07-24 21:11 ` [PATCH 1/5] pcapng: extend interface statistics Stephen Hemminger
@ 2026-07-24 21:11 ` Stephen Hemminger
2026-07-24 21:11 ` [PATCH 3/5] test: add test for capture hooks Stephen Hemminger
` (2 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Stephen Hemminger @ 2026-07-24 21:11 UTC (permalink / raw)
To: dev; +Cc: Stephen Hemminger
This provides a telemetry extension to provide packet capture.
It is intended to be used with a front end script to provide
external packet capture for wireshark.
Signed-off-by: Stephen Hemminger <stephen@networkplumber.org>
---
MAINTAINERS | 1 +
doc/guides/rel_notes/release_26_11.rst | 4 +
lib/capture/capture.c | 1032 ++++++++++++++++++++++++
lib/capture/capture_impl.h | 56 ++
lib/capture/filter.c | 108 +++
lib/capture/meson.build | 19 +
lib/meson.build | 1 +
7 files changed, 1221 insertions(+)
create mode 100644 lib/capture/capture.c
create mode 100644 lib/capture/capture_impl.h
create mode 100644 lib/capture/filter.c
create mode 100644 lib/capture/meson.build
diff --git a/MAINTAINERS b/MAINTAINERS
index e99a65d197..fcd350ad94 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1720,6 +1720,7 @@ F: doc/guides/sample_app_ug/qos_scheduler.rst
Packet capture
M: Reshma Pattan <reshma.pattan@intel.com>
M: Stephen Hemminger <stephen@networkplumber.org>
+F: lib/capture/
F: lib/pdump/
F: doc/guides/prog_guide/pdump_lib.rst
F: app/test/test_pdump.*
diff --git a/doc/guides/rel_notes/release_26_11.rst b/doc/guides/rel_notes/release_26_11.rst
index b27494fee7..8291c2c4e5 100644
--- a/doc/guides/rel_notes/release_26_11.rst
+++ b/doc/guides/rel_notes/release_26_11.rst
@@ -55,6 +55,10 @@ New Features
Also, make sure to start the actual text at the margin.
=======================================================
+* **Added wireshark capture support.**
+
+ * Added ``capture`` library for packet capture via telemetry API.
+
Removed Items
-------------
diff --git a/lib/capture/capture.c b/lib/capture/capture.c
new file mode 100644
index 0000000000..1b115ccf4a
--- /dev/null
+++ b/lib/capture/capture.c
@@ -0,0 +1,1032 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2026 Stephen Hemminger
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <net/if.h>
+#include <poll.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/queue.h>
+#include <sys/stat.h>
+#include <sys/utsname.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <rte_branch_prediction.h>
+#include <rte_common.h>
+#include <rte_cycles.h>
+#include <rte_debug.h>
+#include <rte_ethdev.h>
+#include <rte_log.h>
+#include <rte_malloc.h>
+#include <rte_memory.h>
+#include <rte_mempool.h>
+#include <rte_mbuf.h>
+#include <rte_pcapng.h>
+#include <rte_pause.h>
+#include <rte_ring.h>
+#include <rte_spinlock.h>
+#include <rte_stdatomic.h>
+#include <rte_string_fns.h>
+#include <rte_telemetry.h>
+#include <rte_thread.h>
+#include <rte_version.h>
+
+#include "capture_impl.h"
+
+#define CAPTURE_EXTCAP_PREFIX "dpdk"
+#define CAP_CMD_MAX 1024
+#define DEFAULT_SNAPLEN 262144u /* from tcpdump et.al. */
+#define CAPTURE_BURST_SIZE 32u
+#define MBUF_POOL_CACHE_SIZE 32
+#define CAPTURE_RING_SIZE 256
+#define CAPTURE_POOL_SIZE 1024
+#define SLEEP_THRESHOLD 100
+#define SLEEP_US 100
+
+#define ALL_QUEUES -1
+
+RTE_LOG_REGISTER_DEFAULT(rte_capture_logtype, NOTICE);
+
+/*
+ * List of active captures.
+ *
+ * This is a control-plane only structure: it is created, walked and torn down
+ * from the telemetry handler thread and from the per-capture drain threads,
+ * never from the dataplane. A plain spinlock is therefore enough; the EAL
+ * shared tailq (rte_tailq) is not used because captures are not visible to
+ * secondary processes in this design.
+ */
+TAILQ_HEAD(capture_list, capture);
+static struct capture_list capture_list = TAILQ_HEAD_INITIALIZER(capture_list);
+static rte_spinlock_t capture_lock = RTE_SPINLOCK_INITIALIZER;
+
+/* Parameter values: only used on stack inside parsing */
+struct capture_config {
+ uint16_t port_id;
+ uint32_t snaplen;
+ int queue;
+ const char *filter_str;
+ const char *output;
+};
+
+/*
+ * Data used by callback
+ * This per-queue to avoid cache thrashing
+ */
+struct __rte_cache_aligned capture_rxtx_cb {
+ RTE_ATOMIC(uint32_t) use_count;
+ const struct rte_eth_rxtx_callback *cb;
+
+ struct capture_stats {
+ RTE_ATOMIC(uint64_t) accepted; /**< Number of packets accepted by filter. */
+ RTE_ATOMIC(uint64_t) filtered; /**< Number of packets rejected by filter. */
+ RTE_ATOMIC(uint64_t) nombuf; /**< Number of mbuf allocation failures. */
+ RTE_ATOMIC(uint64_t) ringfull; /**< Number of missed packets due to ring full. */
+ } stats;
+};
+
+/*
+ * Per-capture instance state.
+ */
+struct capture {
+ RTE_ATOMIC(bool) running;
+ struct rte_capture_filter *filter;
+ struct rte_ring *ring; /* ring from dataplane to capture thread */
+ struct rte_mempool *mp; /* mempool for capture mbufs */
+
+ uint32_t snaplen; /* amount of data to copy */
+ int queue;
+ uint16_t tx_queues;
+ uint16_t rx_queues;
+ uint16_t port_id;
+
+ unsigned int idx;
+ char *output; /* filename of out */
+
+ TAILQ_ENTRY(capture) next; /* links into capture_list */
+
+ /* per-queue data sized to max(tx_queue, rx_queues) */
+ struct capture_cbs {
+ struct capture_rxtx_cb tx_cb;
+ struct capture_rxtx_cb rx_cb;
+ } cbs[];
+};
+
+/* Wait for callbacks to be idle before free */
+static void
+capture_cb_wait(struct capture_rxtx_cb *cbs)
+{
+ /* make sure all previous loads and stores are completed */
+ rte_atomic_thread_fence(rte_memory_order_seq_cst);
+ uint32_t puse = rte_atomic_load_explicit(&cbs->use_count,
+ rte_memory_order_acquire);
+
+ /* in use, busy wait till current RX/TX iteration is finished */
+ if (puse & 1)
+ RTE_WAIT_UNTIL_MASKED(&cbs->use_count, UINT32_MAX, !=, puse,
+ rte_memory_order_acquire);
+}
+
+/* Mark datapath iteration in progress: count becomes odd. */
+static inline __rte_hot void
+capture_cb_hold(struct capture_rxtx_cb *cbs)
+{
+ rte_atomic_fetch_add_explicit(&cbs->use_count, 1, rte_memory_order_seq_cst);
+}
+
+/* Iteration finished: count becomes even again. */
+static inline __rte_hot void
+capture_cb_release(struct capture_rxtx_cb *cbs)
+{
+ rte_atomic_fetch_add_explicit(&cbs->use_count, 1, rte_memory_order_release);
+}
+
+/* Create a clone of mbuf to be placed into ring. */
+static inline __rte_hot void
+capture_copy_burst(uint16_t port_id, uint16_t queue_id,
+ enum rte_pcapng_direction direction,
+ struct rte_mbuf **pkts, unsigned int nb_pkts,
+ const struct capture *cap,
+ struct capture_stats *stats)
+{
+ unsigned int i, ring_enq, d_pkts = 0;
+ struct rte_mbuf *dup_bufs[CAPTURE_BURST_SIZE]; /* duplicated packets */
+ struct rte_ring *ring = cap->ring;
+ struct rte_mempool *mp = cap->mp;
+ uint32_t snaplen = cap->snaplen;
+ struct rte_mbuf *p;
+
+ RTE_ASSERT(nb_pkts <= CAPTURE_BURST_SIZE);
+
+ for (i = 0; i < nb_pkts; i++) {
+ /*
+ * This uses same BPF return value convention as socket filter
+ * and pcap_offline_filter. If program returns zero then packet
+ * doesn't match the filter (will be ignored).
+ */
+ if (cap->filter) {
+ if (__rte_capture_filter(cap->filter, pkts[i]) == 0) {
+ rte_atomic_fetch_add_explicit(&stats->filtered, 1,
+ rte_memory_order_relaxed);
+ continue;
+ }
+ }
+
+ p = rte_pcapng_copy(port_id, queue_id, pkts[i], mp, snaplen, direction, NULL);
+ if (unlikely(p == NULL))
+ rte_atomic_fetch_add_explicit(&stats->nombuf, 1, rte_memory_order_relaxed);
+ else
+ dup_bufs[d_pkts++] = p;
+ }
+
+ if (d_pkts == 0)
+ return;
+
+ rte_atomic_fetch_add_explicit(&stats->accepted, d_pkts, rte_memory_order_relaxed);
+
+ ring_enq = rte_ring_enqueue_burst(ring, (void *)&dup_bufs[0], d_pkts, NULL);
+ if (unlikely(ring_enq < d_pkts)) {
+ unsigned int drops = d_pkts - ring_enq;
+
+ rte_atomic_fetch_add_explicit(&stats->ringfull, drops, rte_memory_order_relaxed);
+ rte_pktmbuf_free_bulk(&dup_bufs[ring_enq], drops);
+ }
+}
+
+/* Create a clone of mbuf to be placed into ring. */
+static __rte_hot inline void
+capture_copy(uint16_t port_id, uint16_t queue_id,
+ enum rte_pcapng_direction direction,
+ struct rte_mbuf **pkts, uint16_t nb_pkts,
+ const struct capture *cap,
+ struct capture_stats *stats)
+{
+ unsigned int offs = 0;
+
+ do {
+ unsigned int n = RTE_MIN(nb_pkts - offs, CAPTURE_BURST_SIZE);
+
+ capture_copy_burst(port_id, queue_id, direction, &pkts[offs], n, cap, stats);
+ offs += n;
+ } while (offs < nb_pkts);
+}
+
+static __rte_hot uint16_t
+capture_rx(uint16_t port, uint16_t queue,
+ struct rte_mbuf **pkts, uint16_t nb_pkts,
+ uint16_t max_pkts __rte_unused, void *user_params)
+{
+ struct capture *cap = user_params;
+ struct capture_rxtx_cb *cbs = &cap->cbs[queue].rx_cb;
+
+ capture_cb_hold(cbs);
+ capture_copy(port, queue, RTE_PCAPNG_DIRECTION_IN, pkts, nb_pkts, cap, &cbs->stats);
+ capture_cb_release(cbs);
+
+ return nb_pkts;
+}
+
+static __rte_hot uint16_t
+capture_tx(uint16_t port, uint16_t queue,
+ struct rte_mbuf **pkts, uint16_t nb_pkts, void *user_params)
+{
+ struct capture *capture = user_params;
+ struct capture_rxtx_cb *cbs = &capture->cbs[queue].tx_cb;
+
+ capture_cb_hold(cbs);
+ capture_copy(port, queue, RTE_PCAPNG_DIRECTION_OUT, pkts, nb_pkts, capture, &cbs->stats);
+ capture_cb_release(cbs);
+
+ return nb_pkts;
+}
+
+
+/* Install callbacks */
+static int
+capture_add_callbacks(struct capture *cap)
+{
+ for (unsigned int q = 0; q < cap->tx_queues; q++) {
+ struct capture_rxtx_cb *tx_cb = &cap->cbs[q].tx_cb;
+
+ if (cap->queue >= 0 && (unsigned int)cap->queue != q)
+ continue;
+
+ tx_cb->cb = rte_eth_add_tx_callback(cap->port_id, q, capture_tx, cap);
+ if (tx_cb->cb == NULL) {
+ CAPTURE_LOG(ERR, "Register tx callback for %u:%u failed",
+ cap->port_id, q);
+ return -1;
+ }
+ }
+
+ for (unsigned int q = 0; q < cap->rx_queues; q++) {
+ struct capture_rxtx_cb *rx_cb = &cap->cbs[q].rx_cb;
+
+ if (cap->queue >= 0 && (unsigned int)cap->queue != q)
+ continue;
+
+ rx_cb->cb = rte_eth_add_rx_callback(cap->port_id, q, capture_rx, cap);
+ if (rx_cb->cb == NULL) {
+ CAPTURE_LOG(ERR, "Register rx callback for %u:%u failed",
+ cap->port_id, q);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+/* Cleanup call backs */
+static void
+capture_remove_callbacks(struct capture *cap)
+{
+ for (unsigned int q = 0; q < cap->tx_queues; q++) {
+ struct capture_rxtx_cb *tx_cb = &cap->cbs[q].tx_cb;
+ if (tx_cb->cb) {
+ rte_eth_remove_tx_callback(cap->port_id, q, tx_cb->cb);
+ capture_cb_wait(tx_cb);
+ tx_cb->cb = NULL;
+ }
+ }
+
+ for (unsigned int q = 0; q < cap->rx_queues; q++) {
+ struct capture_rxtx_cb *rx_cb = &cap->cbs[q].rx_cb;
+ if (rx_cb->cb) {
+ rte_eth_remove_rx_callback(cap->port_id, q, rx_cb->cb);
+ capture_cb_wait(rx_cb);
+ rx_cb->cb = NULL;
+ }
+ }
+}
+
+/* Helper that returns error to telemetry and logs it */
+static void __rte_format_printf(2, 3)
+capture_err(struct rte_tel_data *d, const char *format, ...)
+{
+ va_list ap;
+ char msg[1024];
+
+ va_start(ap, format);
+ vsnprintf(msg, sizeof(msg), format, ap);
+ va_end(ap);
+
+ rte_tel_data_start_dict(d);
+ rte_tel_data_add_dict_string(d, "error", msg);
+ rte_log(RTE_LOG_NOTICE, RTE_LOGTYPE_CAPTURE, "CAPTURE: %s\n", msg);
+}
+
+/*
+ * Break the comma separated parameter string into tokens
+ * and fill in the capture config structure.
+ *
+ * Does not use rte_kvargs because that would mangle [] etc in filter expression.
+ */
+static int
+parse_params(char *str, struct capture_config *cfg, struct rte_tel_data *d)
+{
+ uint32_t snaplen = DEFAULT_SNAPLEN;
+ char *args[8];
+ int nargs;
+
+ /* Need at least the port id */
+ nargs = rte_strsplit(str, strlen(str), args, RTE_DIM(args), ',');
+ if (nargs < 1) {
+ capture_err(d, "missing parameters '%s'", str);
+ return -1;
+ }
+
+ /* Parse port id (required) */
+ char *endp;
+ errno = 0;
+ unsigned long port_id = strtoul(args[0], &endp, 10);
+ if (errno != 0 || *endp != '\0' || port_id >= RTE_MAX_ETHPORTS) {
+ capture_err(d, "invalid port_id='%s'", args[0]);
+ return -1;
+ }
+
+ /* parse remainder as name=value parameters */
+ for (int i = 1; i < nargs; i++) {
+ char *key = args[i];
+
+ /* split at the = */
+ char *eq = strchr(args[i], '=');
+
+ /* all current options require argument after = */
+ if (eq == NULL || eq[1] == '\0') {
+ capture_err(d, "missing value for '%s'", key);
+ return -1;
+ }
+ *eq = '\0';
+ char *value = eq + 1;
+
+ if (strcmp(key, "out") == 0) {
+ cfg->output = value;
+ } else if (strcmp(key, "filter") == 0) {
+ cfg->filter_str = value;
+ } else if (strcmp(key, "queue") == 0) {
+ errno = 0;
+ unsigned long q = strtoul(value, &endp, 10);
+ if (errno != 0 || *endp != '\0' || q >= RTE_MAX_QUEUES_PER_PORT) {
+ capture_err(d, "invalid queue '%lu'", q);
+ return -1;
+ }
+ cfg->queue = q;
+ } else if (strcmp(key, "snaplen") == 0) {
+ errno = 0;
+ unsigned long len = strtoul(value, &endp, 10);
+ if (errno != 0 || *endp != '\0' || len >= UINT32_MAX) {
+ capture_err(d, "invalid snaplen '%lu'", len);
+ return -1;
+ }
+ snaplen = len;
+ } else {
+ capture_err(d, "unknown parameter '%s'", key);
+ return -1;
+ }
+ }
+
+ if (cfg->output == NULL) {
+ capture_err(d, "missing output parameter");
+ return -1;
+ }
+
+ cfg->port_id = port_id;
+
+ /*
+ * Default is 256K from tcpdump legacy
+ * using snaplen=0 means everything.
+ */
+ cfg->snaplen = snaplen > 0 ? snaplen : UINT32_MAX;
+ return 0;
+}
+
+static bool is_empty_or_fifo(const struct stat *stb)
+{
+ if (S_ISFIFO(stb->st_mode))
+ return true;
+ else if (S_ISREG(stb->st_mode))
+ return stb->st_size == 0;
+ else
+ return false; /* not a FIFO or regular file */
+}
+
+
+/*
+ * Create the file handle for pcapng output
+ * Note: can't really tell wireshark about errors since this in an
+ * independent thread.
+ */
+static __rte_cold rte_pcapng_t *
+capture_pcapng_open(const char *path, int *fd, uint16_t port_id, const char *filter )
+{
+ rte_pcapng_t *pcapng = NULL;
+ char port_name[RTE_ETH_NAME_MAX_LEN];
+ char appname[128];
+ char ifname[IFNAMSIZ];
+ char *ifdescr = NULL;
+ struct utsname uts;
+ char *osname = NULL;
+
+ /* OS name is optional, just keep going if not found */
+ if (uname(&uts) == 0 && asprintf(&osname, "%s %s", uts.sysname, uts.release) < 0)
+ osname = NULL;
+
+ /* add DPDK internal name */
+ if (rte_eth_dev_get_name_by_port(port_id, port_name) != 0) {
+ CAPTURE_LOG(NOTICE, "Could not find port name for %u", port_id);
+ return NULL;
+ }
+
+ /* match name convention used by dpdk-wireshark-extcap.py */
+ snprintf(ifname, sizeof(ifname), CAPTURE_EXTCAP_PREFIX ":%u", port_id);
+ if (asprintf(&ifdescr, "DPDK %s", port_name) < 0)
+ ifdescr = NULL;
+
+ /* mirror what other applications do for name */
+ snprintf(appname, sizeof(appname), CAPTURE_EXTCAP_PREFIX " (%s)", rte_version());
+
+ /*
+ * Open the output in non-block mode in case it is a FIFO
+ * without a reader. Wireshark must open the read end before
+ * asking us to capture.
+ */
+ *fd = open(path, O_WRONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK);
+ if (*fd < 0) {
+ CAPTURE_LOG(ERR, "Could not open %s: %s", path, strerror(errno));
+ return NULL;
+ }
+
+ /* recheck that it is ok to use */
+ struct stat sb;
+ if (fstat(*fd, &sb) < 0 || !is_empty_or_fifo(&sb)) {
+ CAPTURE_LOG(ERR, "Not safe to use %s", path);
+ goto close_fd;
+ }
+
+ /* writes in the drain loop should block normally */
+ int flags = fcntl(*fd, F_GETFL, 0);
+ if (flags < 0 || fcntl(*fd, F_SETFL, flags & ~O_NONBLOCK) < 0) {
+ CAPTURE_LOG(ERR, "fcntl %s: %s", path, strerror(errno));
+ goto close_fd;
+ }
+
+ /* put pcapng header on and setup */
+ pcapng = rte_pcapng_fdopen(*fd, osname, NULL, appname, NULL);
+ if (pcapng == NULL) {
+ CAPTURE_LOG(ERR, "Add section block failed");
+ goto close_fd;
+ }
+
+ if (rte_pcapng_add_interface(pcapng, port_id, DLT_EN10MB, ifname, ifdescr, filter) < 0) {
+ CAPTURE_LOG(ERR, "Add interface for port %u:%s failed", port_id, ifname);
+ rte_pcapng_close(pcapng); /* closes fd */
+ pcapng = NULL;
+ }
+ goto cleanup;
+
+close_fd:
+ close(*fd);
+cleanup:
+ free(osname);
+ free(ifdescr);
+ return pcapng;
+}
+
+static void
+capture_link(struct capture *cap)
+{
+ rte_spinlock_lock(&capture_lock);
+ TAILQ_INSERT_TAIL(&capture_list, cap, next);
+ rte_spinlock_unlock(&capture_lock);
+}
+
+static void
+capture_unlink(struct capture *cap)
+{
+ rte_spinlock_lock(&capture_lock);
+ TAILQ_REMOVE(&capture_list, cap, next);
+ rte_spinlock_unlock(&capture_lock);
+}
+
+static void
+capture_free(struct capture *cap)
+{
+ if (cap == NULL)
+ return;
+
+ free(cap->output);
+ __rte_capture_filter_free(cap->filter);
+ rte_ring_free(cap->ring);
+ rte_mempool_free(cap->mp);
+ rte_free(cap);
+}
+
+/* Generate unique id for naming and telemetry */
+static unsigned int
+get_unique_id(void)
+{
+ static RTE_ATOMIC(unsigned int) capture_instance;
+
+ return rte_atomic_fetch_add_explicit(&capture_instance, 1, rte_memory_order_relaxed);
+}
+
+/*
+ * Convert configuration into running state
+ */
+static struct capture *
+capture_alloc(const struct capture_config *cfg,
+ const struct rte_eth_dev_info *dev_info,
+ struct rte_tel_data *d)
+{
+ struct capture *cap;
+ char ring_name[RTE_RING_NAMESIZE];
+ uint16_t mbuf_size;
+
+ /* try and put capture data struct on same node as device. */
+ int socket_id = rte_eth_dev_socket_id(cfg->port_id);
+ if (socket_id < 0)
+ socket_id = SOCKET_ID_ANY;
+
+ uint16_t num_queues = RTE_MAX(dev_info->nb_tx_queues, dev_info->nb_rx_queues);
+ size_t cb_size = sizeof(*cap) + num_queues * sizeof(cap->cbs[0]);
+ cap = rte_zmalloc_socket("capture", cb_size, RTE_CACHE_LINE_SIZE, socket_id);
+ if (cap == NULL) {
+ capture_err(d, "Could not allocate capture struct");
+ goto error;
+ }
+
+ cap->idx = get_unique_id();
+ snprintf(ring_name, sizeof(ring_name), "capture-%u", cap->idx);
+ cap->ring = rte_ring_create(ring_name, CAPTURE_RING_SIZE, socket_id, 0);
+ if (cap->ring == NULL) {
+ capture_err(d, "Could not create ring");
+ goto error;
+ }
+
+ /*
+ * If snapshot length is smaller than one mbuf segment then pool
+ * element size can be reduced; otherwise can just use the default
+ * and rte_pktmbuf_copy handle multiple segments.
+ */
+ if (cfg->snaplen < RTE_MBUF_DEFAULT_BUF_SIZE)
+ mbuf_size = rte_pcapng_mbuf_size(cfg->snaplen);
+ else
+ mbuf_size = RTE_MBUF_DEFAULT_BUF_SIZE;
+
+ cap->mp = rte_pktmbuf_pool_create_by_ops(ring_name, CAPTURE_POOL_SIZE,
+ MBUF_POOL_CACHE_SIZE, 0, mbuf_size,
+ socket_id, "ring_mp_mc");
+ if (cap->mp == NULL) {
+ capture_err(d, "Could not create mempool");
+ goto error;
+ }
+
+ if (cfg->filter_str) {
+ cap->filter = __rte_capture_filter_create(cfg->filter_str);
+ if (cap->filter == NULL) {
+ capture_err(d, "Could not compile filter: %s", cfg->filter_str);
+ goto error;
+ }
+ }
+
+ cap->port_id = cfg->port_id;
+ cap->output = strdup(cfg->output);
+ if (cap->output == NULL) {
+ capture_err(d, "Could not strdup '%s'", cfg->output);
+ goto error;
+ }
+ rte_atomic_store_explicit(&cap->running, true, rte_memory_order_relaxed);
+ cap->snaplen = cfg->snaplen;
+ cap->queue = cfg->queue;
+ cap->tx_queues = dev_info->nb_tx_queues;
+ cap->rx_queues = dev_info->nb_rx_queues;
+
+ return cap;
+
+error:
+ capture_free(cap);
+ return NULL;
+}
+
+/* Aggregate per-queue counters of a capture instance. */
+struct capture_total {
+ uint64_t accepted;
+ uint64_t filtered;
+ uint64_t nombuf;
+ uint64_t ringfull;
+};
+
+static void
+capture_sum_one(struct capture_total *t, const struct capture_stats *s)
+{
+ t->accepted += rte_atomic_load_explicit(&s->accepted, rte_memory_order_relaxed);
+ t->filtered += rte_atomic_load_explicit(&s->filtered, rte_memory_order_relaxed);
+ t->nombuf += rte_atomic_load_explicit(&s->nombuf, rte_memory_order_relaxed);
+ t->ringfull += rte_atomic_load_explicit(&s->ringfull, rte_memory_order_relaxed);
+}
+
+/* Sum the rx and tx counters across all queues. Caller holds capture_lock. */
+static void
+capture_sum_stats(const struct capture *cap, struct capture_total *t)
+{
+ *t = (struct capture_total){ };
+
+ for (unsigned int q = 0; q < cap->rx_queues; q++)
+ capture_sum_one(t, &cap->cbs[q].rx_cb.stats);
+ for (unsigned int q = 0; q < cap->tx_queues; q++)
+ capture_sum_one(t, &cap->cbs[q].tx_cb.stats);
+}
+
+static void
+capture_write_stats(rte_pcapng_t *pcapng, const struct capture *cap)
+{
+ struct capture_total t;
+ struct rte_pcapng_interface_stats isb;
+
+ capture_sum_stats(cap, &t);
+
+ /* Unlike libpcap the ifrecv is the total number of packets
+ * and filteraccept is the subset that passed.
+ */
+ isb.ifrecv = t.accepted + t.filtered + t.nombuf;
+ isb.filteraccept = t.accepted + t.nombuf;
+ isb.ifdrop = t.nombuf + t.ringfull;
+
+ rte_pcapng_write_stats(pcapng, cap->port_id, &isb, sizeof(isb), NULL);
+}
+
+/*
+ * Check that output file is still ok to write (i.e FIFO not closed)
+ * Also block for up to SLEEP_US to wait for wireshark to catch up
+ */
+static int
+check_fifo_status(int fd)
+{
+ struct pollfd pfd = { .fd = fd };
+
+ if (poll(&pfd, 1, 0) < 0) {
+ CAPTURE_LOG(ERR, "poll failed: %s", strerror(errno));
+ return -1;
+ }
+ if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) {
+ CAPTURE_LOG(NOTICE, "fifo reader closed");
+ return -1;
+ }
+ return 0;
+}
+
+static ssize_t
+capture_process_ring(struct rte_ring *ring, rte_pcapng_t *pcapng,
+ unsigned int *available)
+{
+ struct rte_mbuf *pkts[CAPTURE_BURST_SIZE];
+ unsigned int n;
+ ssize_t written;
+
+ n = rte_ring_sc_dequeue_burst(ring, (void **) pkts,
+ CAPTURE_BURST_SIZE, available);
+ if (n == 0)
+ return 0;
+
+ written = rte_pcapng_write_packets(pcapng, pkts, n);
+ rte_pktmbuf_free_bulk(pkts, n);
+
+ return written;
+}
+
+static void
+capture_flush_ring(struct rte_ring *ring)
+{
+ for (;;) {
+ struct rte_mbuf *pkts[CAPTURE_BURST_SIZE];
+ unsigned int n;
+
+ n = rte_ring_sc_dequeue_burst(ring, (void **) pkts,
+ CAPTURE_BURST_SIZE, NULL);
+ if (n == 0)
+ break;
+
+ rte_pktmbuf_free_bulk(pkts, n);
+ }
+}
+
+/* The capture thread that moves packets from ring to the pcapng out */
+static uint32_t __rte_hot
+capture_thread(void *arg)
+{
+ struct capture *cap = arg;
+ unsigned int empty_count = 0;
+ bool reader_gone = false;
+ int fd = -1;
+
+ CAPTURE_LOG(INFO, "capture thread starting");
+
+ char name[RTE_THREAD_NAME_SIZE];
+ snprintf(name, sizeof(name), "dpdk-cap-%u", cap->idx);
+ rte_thread_set_name(rte_thread_self(), name);
+
+ /* This thread wants to detect when file gets closed (for FIFO) */
+ sigset_t set;
+ sigemptyset(&set);
+ sigaddset(&set, SIGPIPE);
+ pthread_sigmask(SIG_BLOCK, &set, NULL);
+
+ rte_pcapng_t *pcapng = capture_pcapng_open(cap->output, &fd, cap->port_id,
+ __rte_capture_filter_string(cap->filter));
+ if (pcapng == NULL) {
+ capture_remove_callbacks(cap);
+ goto error;
+ }
+
+ while (rte_atomic_load_explicit(&cap->running, rte_memory_order_relaxed)) {
+ ssize_t written;
+ unsigned int avail;
+
+ written = capture_process_ring(cap->ring, pcapng, &avail);
+ if (written < 0) {
+ CAPTURE_LOG(NOTICE, "write to file failed: %s",
+ strerror(errno));
+ reader_gone = true;
+ break;
+ }
+
+ if (written > 0) {
+ /* are there more packets? */
+ empty_count = (avail == 0);
+ } else if (empty_count < SLEEP_THRESHOLD) {
+ /* spin a few times before checking */
+ ++empty_count;
+ rte_pause();
+ } else if (check_fifo_status(fd) != 0) {
+ /* output FIFO has closed */
+ reader_gone = true;
+ break;
+ } else {
+ /* avoid consuming 100% CPU polling */
+ rte_delay_us_sleep(SLEEP_US);
+ }
+ }
+
+ /* Capture exiting */
+ CAPTURE_LOG(INFO, "capture thread stopping");
+ capture_remove_callbacks(cap);
+
+ /* Process residual */
+ if (reader_gone)
+ capture_flush_ring(cap->ring);
+ else {
+ while (capture_process_ring(cap->ring, pcapng, NULL) > 0)
+ continue;
+
+ capture_write_stats(pcapng, cap);
+ }
+
+ rte_pcapng_close(pcapng);
+
+error:
+ capture_unlink(cap);
+ capture_free(cap);
+
+ return 0;
+}
+
+/*
+ * Callback handler for telemetry library to start capture.
+ *
+ * Need to handle: <iface>,snaplen=<n>,filter=<str>
+ */
+static int
+capture_start_req(const char *cmd, const char *params, struct rte_tel_data *d)
+{
+ struct capture *cap = NULL;
+ struct capture_config cfg = { .queue = ALL_QUEUES };
+ struct rte_eth_dev_info dev_info;
+
+ if (params == NULL || *params == '\0') {
+ CAPTURE_LOG(ERR, "missing parameters");
+ return -1;
+ }
+
+ CAPTURE_LOG(DEBUG, "telemetry: %s %s", cmd, params);
+
+ if (rte_eal_process_type() != RTE_PROC_PRIMARY) {
+ CAPTURE_LOG(ERR, "capture can only be started from primary");
+ return -1;
+ }
+
+ /* Note: params is const so need non-const copy for parsing
+ * Alternative would be to change telemetry to allow this.
+ */
+ char tmp[CAP_CMD_MAX];
+ if (strlcpy(tmp, params, CAP_CMD_MAX) >= CAP_CMD_MAX) {
+ CAPTURE_LOG(ERR, "params too long");
+ return -1;
+ }
+
+ if (parse_params(tmp, &cfg, d) < 0)
+ return 0;
+
+ /* Lookup number of queues etc, also validates port_id */
+ if (rte_eth_dev_info_get(cfg.port_id, &dev_info) < 0) {
+ capture_err(d, "can not get info for port %u", cfg.port_id);
+ return 0;
+ }
+
+ if (cfg.queue != ALL_QUEUES &&
+ cfg.queue >= RTE_MAX(dev_info.nb_rx_queues, dev_info.nb_tx_queues)) {
+ capture_err(d, "queue %d out of range", cfg.queue);
+ return 0;
+ }
+
+ struct stat sb;
+ if (stat(cfg.output, &sb) < 0) {
+ capture_err(d, "output %s:%s", cfg.output, strerror(errno));
+ return 0;
+ }
+
+ if (!is_empty_or_fifo(&sb)) {
+ capture_err(d, "output %s is not empty", cfg.output);
+ return 0;
+ }
+
+ cap = capture_alloc(&cfg, &dev_info, d);
+ if (cap == NULL)
+ return 0;
+
+ if (capture_add_callbacks(cap) < 0) {
+ capture_err(d, "can not register callbacks");
+ goto error_callback_remove;
+ }
+
+ /*
+ * Publish into the active list before starting the drain thread so the
+ * thread is guaranteed to find itself there when it removes itself on
+ * exit (it may exit immediately, e.g. if the FIFO reader is already
+ * gone). On thread-create failure we undo the insertion here.
+ */
+ unsigned int idx = cap->idx;
+ capture_link(cap);
+
+ /*
+ * Make a new thread to do the capture work
+ * Thread will inherit affinity from the telemetry handler that calls us
+ */
+ rte_thread_t thread_id;
+ int ret = rte_thread_create(&thread_id, NULL, capture_thread, cap);
+ if (ret != 0) {
+ capture_err(d, "thread start failed: %s", strerror(ret));
+ goto error_unlink;
+ }
+
+ rte_thread_detach(thread_id);
+
+ /* Return id back for later use. */
+ rte_tel_data_start_dict(d);
+ rte_tel_data_add_dict_uint(d, "id", idx);
+ rte_tel_data_add_dict_string(d, "status", "running");
+ return 0;
+
+error_unlink:
+ capture_unlink(cap);
+error_callback_remove:
+ capture_remove_callbacks(cap);
+ capture_free(cap);
+
+ /* return 0 since error reported "error":"XXX" in respons */
+ return 0;
+}
+
+/* Telemetry: stop active capture. */
+static int
+capture_stop_req(const char *cmd, const char *params, struct rte_tel_data *d)
+{
+ if (params == NULL || *params == '\0') {
+ CAPTURE_LOG(ERR, "missing parameters");
+ return -1;
+ }
+
+ CAPTURE_LOG(DEBUG, "telemetry %s %s", cmd, params);
+
+ errno = 0;
+ char *endp;
+ unsigned long idx = strtoul(params, &endp, 10);
+ if (errno != 0 || *endp != '\0') {
+ capture_err(d, "invalid capture index '%s'", params);
+ return 0;
+ }
+
+ rte_spinlock_lock(&capture_lock);
+ struct capture *cap;
+ TAILQ_FOREACH(cap, &capture_list, next) {
+ if (cap->idx == idx)
+ break;
+ }
+ if (cap == NULL) {
+ rte_spinlock_unlock(&capture_lock);
+ capture_err(d, "capture index %lu not found", idx);
+ return 0;
+ }
+
+ rte_atomic_store_explicit(&cap->running, false, rte_memory_order_relaxed);
+ rte_spinlock_unlock(&capture_lock);
+
+ rte_tel_data_start_dict(d);
+ rte_tel_data_add_dict_string(d, "status", "stopped");
+ return 0;
+}
+
+/* Telemetry: list the ids of all active captures. */
+static int
+capture_list_req(const char *cmd __rte_unused, const char *params __rte_unused,
+ struct rte_tel_data *d)
+{
+ struct capture *cap;
+
+ CAPTURE_LOG(DEBUG, "telemetry %s", cmd);
+ rte_tel_data_start_array(d, RTE_TEL_UINT_VAL);
+
+ rte_spinlock_lock(&capture_lock);
+ TAILQ_FOREACH(cap, &capture_list, next)
+ rte_tel_data_add_array_uint(d, cap->idx);
+ rte_spinlock_unlock(&capture_lock);
+
+ return 0;
+}
+
+
+/* Telemetry: report configuration and counters for one capture. */
+static int
+capture_stats_req(const char *cmd, const char *params,
+ struct rte_tel_data *d)
+{
+ struct capture *cap;
+ struct capture_total t;
+ char *endp;
+
+ if (params == NULL || *params == '\0') {
+ CAPTURE_LOG(ERR, "missing parameters");
+ return -1;
+ }
+
+ CAPTURE_LOG(DEBUG, "telemetry %s %s", cmd, params);
+
+ errno = 0;
+ unsigned long idx = strtoul(params, &endp, 10);
+ if (errno != 0 || *endp != '\0') {
+ capture_err(d, "invalid capture index '%s'", params);
+ return 0;
+ }
+
+ /* Find the instance and snapshot what we need while holding the lock. */
+ rte_spinlock_lock(&capture_lock);
+ TAILQ_FOREACH(cap, &capture_list, next) {
+ if (cap->idx == idx)
+ break;
+ }
+ if (cap == NULL) {
+ rte_spinlock_unlock(&capture_lock);
+ capture_err(d, "capture index %lu not found", idx);
+ return 0;
+ }
+
+ rte_tel_data_start_dict(d);
+ rte_tel_data_add_dict_uint(d, "port_id", cap->port_id);
+ if (cap->filter)
+ rte_tel_data_add_dict_string(d, "filter",
+ __rte_capture_filter_string(cap->filter));
+ rte_tel_data_add_dict_int(d, "running",
+ rte_atomic_load_explicit(&cap->running,
+ rte_memory_order_relaxed));
+ rte_tel_data_add_dict_uint(d, "snaplen", cap->snaplen);
+ rte_tel_data_add_dict_uint(d, "rx_queues", cap->rx_queues);
+ rte_tel_data_add_dict_uint(d, "tx_queues", cap->tx_queues);
+ capture_sum_stats(cap, &t);
+ rte_spinlock_unlock(&capture_lock);
+
+ rte_tel_data_add_dict_uint(d, "accepted", t.accepted);
+ rte_tel_data_add_dict_uint(d, "filtered", t.filtered);
+ rte_tel_data_add_dict_uint(d, "nombuf", t.nombuf);
+ rte_tel_data_add_dict_uint(d, "ringfull", t.ringfull);
+
+ return 0;
+}
+
+RTE_INIT(capture_telemetry)
+{
+ rte_telemetry_register_cmd("/ethdev/capture/list", capture_list_req,
+ "List ids of active captures. Takes no parameters.");
+ rte_telemetry_register_cmd("/ethdev/capture/stats", capture_stats_req,
+ "Report configuration and counters for a capture. Parameters: id");
+ rte_telemetry_register_cmd("/ethdev/capture/start", capture_start_req,
+ "Start capture. Parameters: "
+ "port_id,out=<path>,snaplen=N(optional),queue=N(optional),filter=string(optional)");
+ rte_telemetry_register_cmd("/ethdev/capture/stop", capture_stop_req,
+ "Stop an active capture. Parameters: id");
+}
diff --git a/lib/capture/capture_impl.h b/lib/capture/capture_impl.h
new file mode 100644
index 0000000000..adee734b6c
--- /dev/null
+++ b/lib/capture/capture_impl.h
@@ -0,0 +1,56 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2026 Stephen Hemminger
+ */
+#ifndef CAPTURE_IMPL_H
+#define CAPTURE_IMPL_H
+
+#define RTE_LOGTYPE_CAPTURE rte_capture_logtype
+extern int rte_capture_logtype;
+#define CAPTURE_LOG(level, ...) \
+ RTE_LOG_LINE_PREFIX(level, CAPTURE, "%s(): ", __func__, __VA_ARGS__)
+
+struct rte_capture_filter;
+
+#ifdef RTE_HAS_LIBPCAP
+struct rte_capture_filter *__rte_capture_filter_create(const char *str);
+const char *__rte_capture_filter_string(struct rte_capture_filter *filter);
+void __rte_capture_filter_free(struct rte_capture_filter *filter);
+uint64_t __rte_capture_filter(const struct rte_capture_filter *filter, struct rte_mbuf *mb);
+
+#else /* !RTE_HAS_LIBPCAP */
+
+/* Stub version if pcap is not available */
+static inline struct rte_capture_filter *
+__rte_capture_filter_create(const char *str)
+{
+ RTE_SET_USED(str);
+ return NULL; /* not supported */
+}
+
+static inline const char *
+__rte_capture_filter_string(struct rte_capture_filter *filter)
+{
+ RTE_SET_USED(filter);
+ return NULL;
+}
+
+static inline void
+__rte_capture_filter_free(struct rte_capture_filter *filter)
+{
+ RTE_SET_USED(filter);
+}
+
+/*
+ * This will be zero if the packet doesn't match the filter and non-zero if
+ * the packet matches the filter.
+ */
+static inline uint64_t
+__rte_capture_filter(const struct rte_capture_filter *filter, struct rte_mbuf *mb)
+{
+ RTE_SET_USED(filter);
+ RTE_SET_USED(mb);
+ return 1;
+}
+
+#endif /* !RTE_HAS_LIBPCAP */
+#endif /* CAPTURE_IMPL_H */
diff --git a/lib/capture/filter.c b/lib/capture/filter.c
new file mode 100644
index 0000000000..ecb5e8a765
--- /dev/null
+++ b/lib/capture/filter.c
@@ -0,0 +1,108 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2026 Stephen Hemminger
+ */
+
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <pcap/pcap.h>
+
+#include <rte_bpf.h>
+#include <rte_errno.h>
+#include <rte_log.h>
+#include <rte_malloc.h>
+#include <rte_mbuf.h>
+
+#include "capture_impl.h"
+
+struct rte_capture_filter {
+ struct rte_bpf *bpf;
+ struct rte_bpf_jit jit;
+ char expr[]; /* original filter text */
+};
+
+/*
+ * Convert text string into an eBPF program
+ */
+struct rte_capture_filter *
+__rte_capture_filter_create(const char *filter)
+{
+ struct rte_capture_filter *flt = NULL;
+ struct rte_bpf_prm *prm = NULL;
+
+ /* libpcap needs a handle */
+ pcap_t *pcap = pcap_open_dead(DLT_EN10MB, UINT16_MAX);
+ if (!pcap) {
+ CAPTURE_LOG(ERR, "pcap: can not open handle");
+ return NULL;
+ }
+
+ flt = rte_zmalloc("capture_filter", sizeof(*flt) + strlen(filter) + 1, 0);
+ if (flt == NULL) {
+ CAPTURE_LOG(ERR, "capture filter alloc failed");
+ goto error;
+ }
+
+ /* convert string to cBPF program */
+ struct bpf_program bf;
+ if (pcap_compile(pcap, &bf, filter, 1, PCAP_NETMASK_UNKNOWN) != 0) {
+ CAPTURE_LOG(ERR, "pcap: can not compile filter: %s",
+ pcap_geterr(pcap));
+ goto error;
+ }
+ strcpy(flt->expr, filter);
+
+ /* convert cBPF to eBPF */
+ prm = rte_bpf_convert(&bf);
+ pcap_freecode(&bf); /* drop the cBPF program */
+
+ if (prm == NULL) {
+ CAPTURE_LOG(ERR, "BPF convert interface %s(%d)",
+ rte_strerror(rte_errno), rte_errno);
+ goto error;
+ }
+
+ flt->bpf = rte_bpf_load(prm);
+ if (flt->bpf == NULL) {
+ CAPTURE_LOG(ERR, "BPF load failed: %s(%d)",
+ rte_strerror(rte_errno), rte_errno);
+ goto error;
+ }
+
+ rte_bpf_get_jit(flt->bpf, &flt->jit);
+ if (flt->jit.func == NULL)
+ CAPTURE_LOG(NOTICE, "No JIT available for filter");
+
+ pcap_close(pcap);
+ rte_free(prm);
+ return flt;
+
+error:
+ pcap_close(pcap);
+ rte_free(prm);
+ rte_free(flt);
+ return NULL;
+}
+
+const char *__rte_capture_filter_string(struct rte_capture_filter *filter)
+{
+ return filter ? filter->expr : NULL;
+}
+
+void __rte_capture_filter_free(struct rte_capture_filter *filter)
+{
+ if (filter == NULL)
+ return;
+
+ rte_bpf_destroy(filter->bpf);
+ rte_free(filter);
+}
+
+uint64_t __rte_capture_filter(const struct rte_capture_filter *filter, struct rte_mbuf *mb)
+{
+ if (filter->jit.func)
+ return filter->jit.func(mb);
+ else
+ return rte_bpf_exec(filter->bpf, mb);
+}
diff --git a/lib/capture/meson.build b/lib/capture/meson.build
new file mode 100644
index 0000000000..4dbe0d1a78
--- /dev/null
+++ b/lib/capture/meson.build
@@ -0,0 +1,19 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2026 Stephen Hemminger
+
+if is_windows
+ build = false
+ reason = 'not supported on Windows'
+ subdir_done()
+endif
+
+sources = files('capture.c')
+
+deps += ['ethdev', 'pcapng', 'bpf']
+
+if dpdk_conf.has('RTE_HAS_LIBPCAP')
+ sources += files('filter.c')
+ ext_deps += pcap_dep
+else
+ warning('libpcap is missing, capture filtering will be disabled')
+endif
diff --git a/lib/meson.build b/lib/meson.build
index af5c160cb8..6d9992f61f 100644
--- a/lib/meson.build
+++ b/lib/meson.build
@@ -49,6 +49,7 @@ libraries = [
'lpm',
'member',
'pcapng',
+ 'capture', # depends on pcapng and bpf
'power',
'rawdev',
'regexdev',
--
2.53.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH 3/5] test: add test for capture hooks
2026-07-24 21:11 [PATCH v6 0/5] Wireshark external capture for DPDK Stephen Hemminger
2026-07-24 21:11 ` [PATCH 1/5] pcapng: extend interface statistics Stephen Hemminger
2026-07-24 21:11 ` [PATCH 2/5] capture: infrastructure wireshark packet capture Stephen Hemminger
@ 2026-07-24 21:11 ` Stephen Hemminger
2026-07-24 21:11 ` [PATCH 4/5] usertools/dpdk-wireshark-extcap.py: script for external capture Stephen Hemminger
2026-07-24 21:11 ` [PATCH 5/5] usertools/dpdk-dumpcap: add script for file capture Stephen Hemminger
4 siblings, 0 replies; 6+ messages in thread
From: Stephen Hemminger @ 2026-07-24 21:11 UTC (permalink / raw)
To: dev; +Cc: Stephen Hemminger
Provide tests to exercise telemetry based packet capture.
Signed-off-by: Stephen Hemminger <stephen@networkplumber.org>
---
MAINTAINERS | 2 +
app/test/meson.build | 1 +
app/test/test_capture.c | 512 ++++++++++++++++++++++++++++++++++++++++
3 files changed, 515 insertions(+)
create mode 100644 app/test/test_capture.c
diff --git a/MAINTAINERS b/MAINTAINERS
index fcd350ad94..5ce969d02f 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1721,6 +1721,8 @@ Packet capture
M: Reshma Pattan <reshma.pattan@intel.com>
M: Stephen Hemminger <stephen@networkplumber.org>
F: lib/capture/
+F: usertools/dpdk-wireshark-extcap.py
+F: doc/guides/tools/wireshark_extcap.rst
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 51abeeb732..b73ae8ca07 100644
--- a/app/test/meson.build
+++ b/app/test/meson.build
@@ -37,6 +37,7 @@ source_file_deps = {
'test_bpf.c': ['bpf', 'net'],
'test_bpf_validate.c': ['bpf'],
'test_byteorder.c': [],
+ 'test_capture.c': ['net_null', 'net', 'ethdev', 'bus_vdev', 'capture'],
'test_cfgfile.c': ['cfgfile'],
'test_cksum.c': ['net'],
'test_cksum_perf.c': ['net'],
diff --git a/app/test/test_capture.c b/app/test/test_capture.c
new file mode 100644
index 0000000000..c37b2a961d
--- /dev/null
+++ b/app/test/test_capture.c
@@ -0,0 +1,512 @@
+/* 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. The output is a path ('out=') that the primary opens
+ * itself and writes pcapng into; the caller never receives packet data over
+ * the control socket. This test therefore behaves like an external capture
+ * tool. It:
+ *
+ * 1. builds a virtual ethdev backed by net_null. Rx synthesizes packets and
+ * Tx is a sink that frees whatever it is handed, which is all this test
+ * needs and avoids the per-queue ring bookkeeping of net_ring. The port
+ * is given NB_QUEUES queues so per-queue selection can be exercised;
+ * 2. creates a FIFO and opens its read end (so the primary's non-blocking
+ * O_WRONLY open finds a reader);
+ * 3. connects to this process's own telemetry socket;
+ * 4. runs three phases:
+ * - lifecycle: start an all-queue capture, check a pcapng stream
+ * appears, the capture is listed, stats account for traffic injected
+ * on every queue in both directions, and it tears down when the
+ * reader disconnects;
+ * - parameters: an out-of-range queue index is rejected, not started;
+ * - selection: a queue=CAPTURE_QUEUE capture installs callbacks on that
+ * queue only, so traffic on any other queue is not captured while
+ * traffic on that one is, in both directions. The selected queue is
+ * neither the first nor the last of NB_QUEUES, so an off-by-one in
+ * either install loop is caught. Both directions are steerable here:
+ * the test names the queue in rte_eth_rx_burst()/rte_eth_tx_burst()
+ * and net_null sources or sinks on whichever queue it is handed.
+ *
+ * The test is skipped (not failed) if telemetry is not enabled or the net_null
+ * driver is not available.
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <limits.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/stat.h>
+#include <sys/un.h>
+
+#include <rte_bus_vdev.h>
+#include <rte_cycles.h>
+#include <rte_eal.h>
+#include <rte_ethdev.h>
+#include <rte_mbuf.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 NULL_VDEV_NAME "net_null_capture"
+#define NB_QUEUES 4
+#define CAPTURE_QUEUE 2 /* not first or last of NB_QUEUES */
+#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 uint16_t test_port = RTE_MAX_ETHPORTS;
+static char fifo_path[PATH_MAX];
+
+/* --- 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 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;
+}
+
+/* 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;
+}
+
+/* Query the accepted counter for a capture id via telemetry. */
+static int
+capture_accepted(int sock, uint64_t id, uint64_t *accepted)
+{
+ char cmd[64], reply[REPLY_LEN];
+
+ snprintf(cmd, sizeof(cmd), "%s,%" PRIu64, CAPTURE_STATS, id);
+ if (tel_cmd(sock, cmd, reply, sizeof(reply)) < 0)
+ return -1;
+ return json_uint(reply, "\"accepted\"", accepted);
+}
+
+/*
+ * Poll the capture list until it is empty (the capture has torn down).
+ * Returns 0 once empty, -1 on timeout (~2s) or telemetry failure.
+ */
+static int
+wait_list_empty(int sock)
+{
+ char reply[REPLY_LEN];
+ uint64_t id;
+
+ for (int i = 0; i < 200; i++) {
+ if (tel_cmd(sock, CAPTURE_LIST, reply, sizeof(reply)) < 0)
+ return -1;
+ if (json_first_array_uint(reply, &id) < 0)
+ return 0;
+ rte_delay_ms(10);
+ }
+ return -1;
+}
+
+/* --- packet injection --------------------------------------------------- */
+
+/*
+ * Pull count packets from a queue. net_null synthesizes them and pulling runs
+ * the capture Rx callback on each; the mbufs are ours to free.
+ */
+static int
+inject_rx(uint16_t queue, unsigned int count)
+{
+ struct rte_mbuf *bufs[NB_PKTS];
+ uint16_t got;
+
+ if (count > NB_PKTS)
+ count = NB_PKTS;
+
+ got = rte_eth_rx_burst(test_port, queue, bufs, count);
+ rte_pktmbuf_free_bulk(bufs, got);
+ return got == count ? 0 : -1; /* net_null fills the whole request */
+}
+
+/*
+ * Transmit count packets on a queue. The capture Tx callback runs first (it
+ * only copies) and net_null then frees the originals, so nothing is reclaimed
+ * here.
+ */
+static int
+inject_tx(uint16_t queue, unsigned int count)
+{
+ struct rte_mbuf *bufs[NB_PKTS];
+
+ 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;
+ }
+
+ /* net_null accepts the whole burst. */
+ return rte_eth_tx_burst(test_port, queue, bufs, count) == count ? 0 : -1;
+}
+
+/*
+ * Inject on every queue except skip; pass NB_QUEUES to cover them all.
+ */
+static int
+inject_rx_except(uint16_t skip, unsigned int count)
+{
+ for (uint16_t q = 0; q < NB_QUEUES; q++) {
+ if (q != skip && inject_rx(q, count) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static int
+inject_tx_except(uint16_t skip, unsigned int count)
+{
+ for (uint16_t q = 0; q < NB_QUEUES; q++) {
+ if (q != skip && inject_tx(q, count) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+/* --- fixture ------------------------------------------------------------ */
+
+static int
+build_port(void)
+{
+ struct rte_eth_conf conf = { 0 };
+
+ 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;
+
+ if (rte_vdev_init(NULL_VDEV_NAME, NULL) < 0)
+ return -1;
+ if (rte_eth_dev_get_port_by_name(NULL_VDEV_NAME, &test_port) != 0)
+ return -1;
+
+ if (rte_eth_dev_configure(test_port, NB_QUEUES, NB_QUEUES, &conf) < 0)
+ return -1;
+ for (uint16_t q = 0; q < NB_QUEUES; q++) {
+ if (rte_eth_rx_queue_setup(test_port, q, RING_SIZE, rte_socket_id(),
+ NULL, test_mp) < 0)
+ return -1;
+ if (rte_eth_tx_queue_setup(test_port, q, RING_SIZE, rte_socket_id(),
+ NULL) < 0)
+ return -1;
+ }
+
+ return rte_eth_dev_start(test_port);
+}
+
+static void
+teardown_port(void)
+{
+ if (test_port != RTE_MAX_ETHPORTS) {
+ rte_eth_dev_stop(test_port);
+ /*
+ * Uninit drives the driver's remove, which closes and releases
+ * the ethdev port and removes the vdev in one path. No separate
+ * rte_eth_dev_close() is needed and null's close is idempotent,
+ * so this is safe even if a tree's close already released it.
+ */
+ rte_vdev_uninit(NULL_VDEV_NAME);
+ test_port = RTE_MAX_ETHPORTS;
+ }
+ rte_mempool_free(test_mp);
+ test_mp = NULL;
+}
+
+/* Create the capture FIFO in the runtime dir; -1 on failure. */
+static int
+make_fifo(void)
+{
+ snprintf(fifo_path, sizeof(fifo_path), "%s/capture_test.%d",
+ rte_eal_get_runtime_dir(), (int)getpid());
+ unlink(fifo_path); /* clear any stale node */
+ if (mkfifo(fifo_path, 0600) < 0)
+ return -1;
+ return 0;
+}
+
+/* --- the test ----------------------------------------------------------- */
+
+static int
+test_capture(void)
+{
+ char cmd[PATH_MAX + 64], reply[REPLY_LEN], pcapng[REPLY_LEN];
+ int sock = -1, rd = -1;
+ int ret = TEST_FAILED;
+ uint64_t id, accepted;
+ struct timeval tv;
+ fd_set rfds;
+ ssize_t n;
+
+ fifo_path[0] = '\0';
+
+ /*
+ * The library writes to the FIFO; a closed reader must give the writer
+ * EPIPE, not a fatal SIGPIPE. The library masks SIGPIPE on its drain
+ * thread, but ignore it here too so the test process is safe regardless.
+ */
+ 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 net_null test port, skipping\n");
+ ret = TEST_SKIPPED;
+ goto out;
+ }
+
+ if (make_fifo() < 0)
+ goto out;
+
+ /*
+ * Phase 1: all-queue lifecycle.
+ *
+ * Open the read end before starting: the primary opens the FIFO
+ * O_WRONLY|O_NONBLOCK and would get ENXIO with no reader present.
+ * O_RDONLY|O_NONBLOCK returns immediately even with no writer yet.
+ */
+ rd = open(fifo_path, O_RDONLY | O_NONBLOCK);
+ if (rd < 0)
+ goto out;
+
+ snprintf(cmd, sizeof(cmd), "%s,%u,out=%s", CAPTURE_START, test_port, fifo_path);
+ TEST_ASSERT_SUCCESS(tel_cmd(sock, cmd, reply, sizeof(reply)),
+ "capture start command failed");
+ TEST_ASSERT(strstr(reply, "error") == NULL,
+ "capture start returned an error: %s", reply);
+
+ /*
+ * Inject traffic on every queue: with no queue= the callbacks must be
+ * installed on all of them. 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_except(NB_QUEUES, NB_PKTS),
+ "rx injection failed");
+
+ /* A pcapng stream (at least the section header) must appear. */
+ FD_ZERO(&rfds);
+ FD_SET(rd, &rfds);
+ tv = (struct timeval){ .tv_sec = 2 };
+ TEST_ASSERT(select(rd + 1, &rfds, NULL, NULL, &tv) > 0,
+ "no pcapng output within timeout");
+ n = read(rd, 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, on every queue. */
+ TEST_ASSERT_SUCCESS(capture_accepted(sock, id, &accepted),
+ "capture stats query failed");
+ TEST_ASSERT_EQUAL(accepted, (uint64_t)(NB_QUEUES * NB_PKTS),
+ "accepted %" PRIu64 " != %d", accepted, NB_QUEUES * NB_PKTS);
+
+ /* Same for the Tx side of every queue. */
+ TEST_ASSERT_SUCCESS(inject_tx_except(NB_QUEUES, NB_PKTS),
+ "tx injection failed");
+ TEST_ASSERT_SUCCESS(capture_accepted(sock, id, &accepted),
+ "capture stats query failed");
+ TEST_ASSERT_EQUAL(accepted, (uint64_t)(2 * NB_QUEUES * NB_PKTS),
+ "accepted %" PRIu64 " != %d", accepted, 2 * NB_QUEUES * NB_PKTS);
+
+ /*
+ * Close the reader: the capture should detect the hangup and tear down.
+ * The drain thread's idle poll notices POLLERR on the write end on its
+ * own; the extra injection just shortens the wait if it was mid-drain.
+ */
+ close(rd);
+ rd = -1;
+ inject_rx(0, NB_PKTS); /* any queue; all are captured here */
+ TEST_ASSERT_SUCCESS(wait_list_empty(sock),
+ "capture did not tear down after reader closed");
+
+ /*
+ * Phase 2: parameter validation. An out-of-range queue index must be
+ * rejected. The range check runs before the output is opened, so no
+ * reader is needed and no capture should be left behind.
+ */
+ snprintf(cmd, sizeof(cmd), "%s,%u,out=%s,queue=%u",
+ CAPTURE_START, test_port, fifo_path, NB_QUEUES);
+ TEST_ASSERT_SUCCESS(tel_cmd(sock, cmd, reply, sizeof(reply)),
+ "capture start command failed");
+ TEST_ASSERT(strstr(reply, "error") != NULL,
+ "out-of-range queue=%u was not rejected: %s", NB_QUEUES, reply);
+ TEST_ASSERT_SUCCESS(wait_list_empty(sock),
+ "rejected start left a capture behind");
+
+ /*
+ * Phase 3: single-queue selection. With queue=CAPTURE_QUEUE the
+ * callbacks are installed on that queue only: traffic on any other
+ * queue must not be captured, traffic on that one must be, in both
+ * directions. The counters are bumped synchronously inside
+ * rx_burst/tx_burst, so every check here is race-free.
+ */
+ rd = open(fifo_path, O_RDONLY | O_NONBLOCK);
+ TEST_ASSERT(rd >= 0, "reopen fifo failed: %s", strerror(errno));
+
+ snprintf(cmd, sizeof(cmd), "%s,%u,out=%s,queue=%u",
+ CAPTURE_START, test_port, fifo_path, CAPTURE_QUEUE);
+ TEST_ASSERT_SUCCESS(tel_cmd(sock, cmd, reply, sizeof(reply)),
+ "queue=%u capture start failed", CAPTURE_QUEUE);
+ TEST_ASSERT(strstr(reply, "error") == NULL,
+ "queue=%u capture start returned an error: %s",
+ CAPTURE_QUEUE, reply);
+ TEST_ASSERT_SUCCESS(json_uint(reply, "\"id\"", &id),
+ "no id in start reply: %s", reply);
+
+ /* Unselected queues: callbacks never installed, nothing captured. */
+ TEST_ASSERT_SUCCESS(inject_rx_except(CAPTURE_QUEUE, NB_PKTS),
+ "rx inject on unselected queues failed");
+ TEST_ASSERT_SUCCESS(inject_tx_except(CAPTURE_QUEUE, NB_PKTS),
+ "tx inject on unselected queues failed");
+ TEST_ASSERT_SUCCESS(capture_accepted(sock, id, &accepted),
+ "capture stats query failed");
+ TEST_ASSERT_EQUAL(accepted, (uint64_t)0,
+ "other queues captured under queue=%u (accepted %" PRIu64 ")",
+ CAPTURE_QUEUE, accepted);
+
+ /* Selected queue: captured, on Rx and on Tx. */
+ TEST_ASSERT_SUCCESS(inject_rx(CAPTURE_QUEUE, NB_PKTS),
+ "rx inject on queue %u failed", CAPTURE_QUEUE);
+ TEST_ASSERT_SUCCESS(capture_accepted(sock, id, &accepted),
+ "capture stats query failed");
+ TEST_ASSERT_EQUAL(accepted, (uint64_t)NB_PKTS,
+ "rx queue %u not captured (accepted %" PRIu64 ")",
+ CAPTURE_QUEUE, accepted);
+
+ TEST_ASSERT_SUCCESS(inject_tx(CAPTURE_QUEUE, NB_PKTS),
+ "tx inject on queue %u failed", CAPTURE_QUEUE);
+ TEST_ASSERT_SUCCESS(capture_accepted(sock, id, &accepted),
+ "capture stats query failed");
+ TEST_ASSERT_EQUAL(accepted, (uint64_t)(2 * NB_PKTS),
+ "tx queue %u not captured (accepted %" PRIu64 ")",
+ CAPTURE_QUEUE, accepted);
+
+ close(rd);
+ rd = -1;
+ TEST_ASSERT_SUCCESS(wait_list_empty(sock),
+ "queue=%u capture did not tear down", CAPTURE_QUEUE);
+
+ ret = TEST_SUCCESS;
+out:
+ if (rd >= 0)
+ close(rd);
+ if (fifo_path[0] != '\0')
+ unlink(fifo_path);
+ if (sock >= 0)
+ close(sock);
+ teardown_port();
+ return ret;
+}
+
+REGISTER_FAST_TEST(capture_autotest, NOHUGE_OK, ASAN_OK, test_capture);
--
2.53.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH 4/5] usertools/dpdk-wireshark-extcap.py: script for external capture
2026-07-24 21:11 [PATCH v6 0/5] Wireshark external capture for DPDK Stephen Hemminger
` (2 preceding siblings ...)
2026-07-24 21:11 ` [PATCH 3/5] test: add test for capture hooks Stephen Hemminger
@ 2026-07-24 21:11 ` Stephen Hemminger
2026-07-24 21:11 ` [PATCH 5/5] usertools/dpdk-dumpcap: add script for file capture Stephen Hemminger
4 siblings, 0 replies; 6+ messages in thread
From: Stephen Hemminger @ 2026-07-24 21:11 UTC (permalink / raw)
To: dev; +Cc: Stephen Hemminger
Provide glue script that wireshark can use to access
telemetry based packet capture. It is dual licensed because
it maybe desirable to put this in wireshark repository.
See https://www.wireshark.org/docs/man-pages/extcap.html
Signed-off-by: Stephen Hemminger <stephen@networkplumber.org>
---
doc/guides/rel_notes/release_26_11.rst | 1 +
doc/guides/tools/index.rst | 1 +
doc/guides/tools/wireshark_extcap.rst | 144 ++++++++++
usertools/dpdk-wireshark-extcap.py | 374 +++++++++++++++++++++++++
4 files changed, 520 insertions(+)
create mode 100644 doc/guides/tools/wireshark_extcap.rst
create mode 100755 usertools/dpdk-wireshark-extcap.py
diff --git a/doc/guides/rel_notes/release_26_11.rst b/doc/guides/rel_notes/release_26_11.rst
index 8291c2c4e5..9835807fcc 100644
--- a/doc/guides/rel_notes/release_26_11.rst
+++ b/doc/guides/rel_notes/release_26_11.rst
@@ -58,6 +58,7 @@ New Features
* **Added wireshark capture support.**
* Added ``capture`` library for packet capture via telemetry API.
+ * Added python script for wireshark external capture integration.
Removed Items
diff --git a/doc/guides/tools/index.rst b/doc/guides/tools/index.rst
index 13f75a5bc6..01db27511a 100644
--- a/doc/guides/tools/index.rst
+++ b/doc/guides/tools/index.rst
@@ -14,6 +14,7 @@ DPDK Tools User Guides
pmdinfo
dumpcap
pdump
+ wireshark_extcap
telemetrywatcher
dmaperf
flow-perf
diff --git a/doc/guides/tools/wireshark_extcap.rst b/doc/guides/tools/wireshark_extcap.rst
new file mode 100644
index 0000000000..f3d932f651
--- /dev/null
+++ b/doc/guides/tools/wireshark_extcap.rst
@@ -0,0 +1,144 @@
+.. SPDX-License-Identifier: BSD-3-Clause
+ Copyright(c) 2026 Stephen Hemminger
+
+Wireshark Extcap Plugin
+=======================
+
+The ``dpdk-wireshark-extcap.py`` script is an external capture (extcap)
+plugin that lets Wireshark capture live traffic from the Ethernet ports of a
+running DPDK application.
+Each DPDK port appears as a capture interface in the Wireshark interface list,
+alongside the host's own network interfaces.
+
+The plugin uses the DPDK telemetry API to query and start capture.
+It passes the path of the fifo file that Wireshark created to the DPDK application,
+which opens the file itself and writes pcapng packets straight into it;
+the plugin never touches packet data.
+Wireshark signals the plugin on exit and that results in closing
+the capture session.
+
+
+Requirements
+------------
+
+* A DPDK application built with the capture library and with telemetry enabled.
+ Telemetry is enabled by default.
+
+* Since the plugin is started by Wireshark, Wireshark and the DPDK application
+ must have the same permissions. See `Permissions`_.
+
+
+Installation
+------------
+
+For Wireshark to discover the plugin it must be present in an extcap
+directory. The configured locations are listed in Wireshark under
+*Help > About Wireshark > Folders*. Copy or symbolically link the script into
+the personal extcap directory, for example::
+
+ ln -s $RTE_SDK/usertools/dpdk-wireshark-extcap.py \
+ ~/.local/lib/wireshark/extcap/
+
+The DPDK ports then appear in the interface list the next time the capture
+options dialog is opened.
+
+
+Usage
+-----
+
+In normal use the plugin is not run by hand; Wireshark invokes it.
+The ports of a running DPDK application appear in the interface list as
+``DPDK <name>``, where ``<name>`` is the device name reported by
+DPDK ethdev, such as ``net_tap0``.
+
+The plugin can also be run directly, which is useful for confirming that a
+DPDK application is reachable::
+
+ $ usertools/dpdk-wireshark-extcap.py --extcap-interfaces
+ extcap {version=0.1}{display=DPDK telemetry capture}
+ interface {value=dpdk:0}{display=DPDK net_tap0}
+
+
+Capture options
+---------------
+
+The following options are offered in the Wireshark capture options dialog for
+a DPDK interface:
+
+Snapshot length
+ Number of bytes captured from each packet. ``0`` captures the whole
+ packet. The default is 262144.
+
+Queue
+ Capture a single hardware queue, or all queues (the default). The choices
+ are bounded to the port's configured queue count.
+
+Capture filter
+ A libpcap filter expression, applied by the DPDK application to the
+ captured traffic.
+
+
+Permissions
+-----------
+
+The DPDK runtime directory is created mode ``0700``, so only the user that
+started the DPDK application can reach its telemetry socket.
+Wireshark, and the plugin it launches, must run as that same user. If run as a
+different user, the interface list is simply empty; running the plugin directly
+with ``--extcap-interfaces`` prints a diagnostic to standard error explaining
+the permission failure.
+
+No privilege beyond access to the telemetry socket is required: if you can
+run ``dpdk-dumpcap`` against an application, you can capture from it with this
+plugin.
+
+
+Selecting a DPDK application
+----------------------------
+
+A host usually runs a single DPDK application, started with the default
+file-prefix (``rte``), and no configuration is needed: its ports appear
+automatically as ``DPDK <name>``.
+
+Running several DPDK applications on one host is also supported. In that case
+each application is started with a distinct ``--file-prefix`` so that its
+runtime state is kept separate. The plugin lists the ports of *all* running
+applications at once, so the application is chosen from the interface list
+rather than before Wireshark is launched. There is no environment variable to
+select one.
+
+Ports of the default prefix are shown plainly, as ``DPDK <name>`` with the
+interface value ``dpdk:<port>``. Ports of any other prefix are qualified with
+the prefix, shown as ``DPDK <name>@<prefix>`` with the interface value
+``dpdk:<prefix>:<port>``, so applications with colliding port numbers stay
+distinct in the list.
+
+
+Environment variables
+----------------------
+
+``DPDK_EXTCAP_PATH``
+ Overrides the base DPDK runtime directory that holds the per-prefix
+ subdirectories. Use it when the runtime directory is in a non-standard
+ location. It gives the base directory that holds the per-prefix
+ subdirectories, one of which is scanned for each running application.
+
+
+Troubleshooting
+---------------
+
+The DPDK ports do not appear in Wireshark
+ Confirm the application is running and was built with the capture library
+ and telemetry. Confirm Wireshark runs as the same user as the application;
+ see `Permissions`_. An application started with a non-default
+ ``--file-prefix`` is listed with its ports qualified as
+ ``DPDK <name>@<prefix>``; see `Selecting a DPDK application`_.
+
+ Running the plugin directly with ``--extcap-interfaces`` prints
+ diagnostics to standard error that the Wireshark GUI does not surface.
+
+A port is listed as ``portN`` instead of a device name
+ The port was reported by the application, but its details could not be
+ read, usually because the application stopped between listing and naming
+ its ports. A capture started against it will fail; restart the
+ application.
diff --git a/usertools/dpdk-wireshark-extcap.py b/usertools/dpdk-wireshark-extcap.py
new file mode 100755
index 0000000000..6c6b792a77
--- /dev/null
+++ b/usertools/dpdk-wireshark-extcap.py
@@ -0,0 +1,374 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0-or-later
+# Copyright(c) 2026 Stephen Hemminger
+
+"""
+Wireshark extcap plugin for live capture from DPDK ethdev ports.
+
+Capture path: Wireshark creates a FIFO and hands this plugin its path. The
+plugin opens the FIFO -- which rendezvous with Wireshark's read end -- and then
+passes the FIFO *path* to the DPDK primary process over the telemetry socket as
+the 'out=' parameter. DPDK opens the FIFO itself and writes pcapng straight into
+it; this plugin never touches packet data.
+
+Stopping: Wireshark stops a capture by closing the FIFO read end and/or sending
+SIGTERM. On either, this plugin asks DPDK to stop (/ethdev/capture/stop,<id>) so
+it can flush the trailing packets and the closing interface statistics block
+while a reader is still attached. If the read end is already gone, DPDK detects
+the same hangup and stops on its own; the explicit stop is then a harmless
+no-op. Note that when stopped from the Wireshark GUI the read-end close usually
+precedes the signal, so the statistics block is delivered reliably only for
+standalone (Ctrl+C) runs and for file output.
+
+Interface values: 'dpdk:<port>' for the default file-prefix ('rte'), and
+'dpdk:<prefix>:<port>' for any other primary. The default prefix is left
+implicit so the common single-instance case stays unadorned.
+"""
+
+import argparse
+import json
+import os
+import select
+import signal
+import socket
+import sys
+
+EXTCAP_VERSION = "0.1"
+TELEMETRY_SOCKET = "dpdk_telemetry.v2"
+CAPTURE_START_CMD = "/ethdev/capture/start"
+CAPTURE_STOP_CMD = "/ethdev/capture/stop"
+ETHDEV_LIST = "/ethdev/list"
+ETHDEV_INFO = "/ethdev/info"
+DEFAULT_SNAPLEN = 262144
+DEFAULT_PREFIX = "rte" # EAL HUGEFILE_PREFIX_DEFAULT
+DLT_EN10MB = 1
+
+
+# --- DPDK runtime directory / socket discovery ---------------------------
+
+
+def dpdk_dir():
+ """Directory holding the per-file-prefix runtime subdirectories."""
+ override = os.environ.get("DPDK_EXTCAP_PATH")
+ if override:
+ return override
+ if os.geteuid() == 0:
+ base = "/var/run"
+ else:
+ base = os.environ.get("XDG_RUNTIME_DIR", "/tmp")
+ return os.path.join(base, "dpdk")
+
+
+def socket_path(prefix):
+ return os.path.join(dpdk_dir(), prefix, TELEMETRY_SOCKET)
+
+
+def list_prefixes():
+ """Yield (prefix, path) for each DPDK telemetry socket found."""
+ root = dpdk_dir()
+ try:
+ entries = os.listdir(root)
+ except FileNotFoundError:
+ # No DPDK runtime dir -> no DPDK application is running.
+ return
+ except PermissionError:
+ # The runtime dir is mode 0700; a different user can see nothing.
+ sys.stderr.write(
+ f"cannot read {root}: permission denied. The DPDK runtime "
+ "directory is created mode 0700, so capture must run as the same "
+ "user as the DPDK application, or set DPDK_EXTCAP_PATH.\n"
+ )
+ return
+
+ for prefix in sorted(entries):
+ path = os.path.join(root, prefix, TELEMETRY_SOCKET)
+ if os.path.exists(path):
+ yield prefix, path
+
+
+def iface_value(prefix, port):
+ """Interface value for a (prefix, port). The default prefix is left
+ implicit so a single default-prefix instance shows 'dpdk:<port>'; any
+ other primary is spelled out as 'dpdk:<prefix>:<port>'."""
+ if prefix == DEFAULT_PREFIX:
+ return f"dpdk:{port}"
+ return f"dpdk:{prefix}:{port}"
+
+
+# --- Telemetry transport -------------------------------------------------
+
+
+class Telemetry:
+ """Minimal client for the DPDK v2 telemetry socket (SOCK_SEQPACKET)."""
+
+ def __init__(self, path):
+ self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
+ self.sock.connect(path)
+ info = json.loads(self.sock.recv(1024).decode())
+ self.max_output_len = info.get("max_output_len", 16384)
+ self.pid = info.get("pid")
+ self.version = info.get("version")
+
+ def command(self, cmd):
+ """Send a command, return the decoded JSON reply, or None if empty."""
+ self.sock.send(cmd.encode())
+ reply = self.sock.recv(self.max_output_len)
+ if not reply:
+ return None
+ return json.loads(reply.decode())
+
+ def close(self):
+ self.sock.close()
+
+
+def port_queue_count(tel, port):
+ """Max of the rx/tx queue counts for a port, 0 if unknown."""
+ try:
+ reply = tel.command(f"{ETHDEV_INFO},{port}")
+ except OSError:
+ reply = None
+ info = (reply or {}).get(ETHDEV_INFO) or {}
+ return max(info.get("nb_rx_queues", 0), info.get("nb_tx_queues", 0))
+
+
+# --- extcap query operations --------------------------------------------
+
+
+def port_name(tel, port):
+ """Device name for a port via /ethdev/info, or 'port<N>' if unreadable.
+ For a physical port this is the PCI address (0000:c1:00.0); for a vdev it
+ is the vdev name (net_tap0). Reuses the caller's open connection."""
+ try:
+ reply = tel.command(f"{ETHDEV_INFO},{port}")
+ except OSError:
+ reply = None
+ info = (reply or {}).get(ETHDEV_INFO) or {}
+ return info.get("name") or f"port{port}"
+
+
+def cmd_interfaces():
+ print(f"extcap {{version={EXTCAP_VERSION}}}{{display=DPDK telemetry capture}}")
+ for prefix, path in list_prefixes():
+ try:
+ tel = Telemetry(path)
+ except OSError as e:
+ sys.stderr.write(f"cannot query {path}: {e}\n")
+ continue
+ # One connection per prefix for the whole enumeration: list the ports,
+ # then name each over the same socket (each connection costs the
+ # primary a handler thread).
+ try:
+ ports = (tel.command(ETHDEV_LIST) or {}).get(ETHDEV_LIST) or []
+ for port in ports:
+ name = port_name(tel, port)
+ # Name the prefix in the label only when it is not the default.
+ if prefix == DEFAULT_PREFIX:
+ display = f"DPDK {name}"
+ else:
+ display = f"DPDK {name}@{prefix}"
+ print(
+ f"interface {{value={iface_value(prefix, port)}}}"
+ f"{{display={display}}}"
+ )
+ except OSError as e:
+ sys.stderr.write(f"cannot query {path}: {e}\n")
+ finally:
+ tel.close()
+
+
+def cmd_dlts(_iface):
+ print(f"dlt {{number={DLT_EN10MB}}}{{name=EN10MB}}{{display=Ethernet}}")
+
+
+def cmd_config(iface):
+ print(
+ f"arg {{number=0}}{{call=--snaplen}}{{display=Snapshot length}}"
+ f"{{tooltip=Bytes captured per packet (0 = whole packet)}}"
+ f"{{type=integer}}{{range=0,{DEFAULT_SNAPLEN}}}"
+ f"{{default={DEFAULT_SNAPLEN}}}{{group=Capture}}"
+ )
+
+ # Bound the queue dropdown to the port's actual queue count. If the primary
+ # is unreachable, just omit the selector -- capture still defaults to all.
+ nqueues = 0
+ if iface:
+ prefix, port = parse_iface(iface)
+ try:
+ tel = Telemetry(socket_path(prefix))
+ except OSError:
+ tel = None
+ if tel is not None:
+ try:
+ nqueues = port_queue_count(tel, port)
+ finally:
+ tel.close()
+
+ if nqueues > 0:
+ print(
+ f"arg {{number=1}}{{call=--queue}}{{display=Queue}}"
+ f"{{tooltip=Capture a single hardware queue}}"
+ f"{{type=selector}}{{group=Capture}}"
+ )
+ print("value {arg=1}{value=-1}{display=All queues}{default=true}")
+ for q in range(nqueues):
+ print(f"value {{arg=1}}{{value={q}}}{{display=Queue {q}}}")
+
+
+# --- capture -------------------------------------------------------------
+
+
+def parse_iface(iface):
+ """Inverse of iface_value: accept 'dpdk:<port>' (default prefix) or
+ 'dpdk:<prefix>:<port>'."""
+ parts = iface.split(":")
+ if parts[0] != "dpdk":
+ raise SystemExit(f"unsupported interface scheme in '{iface}'")
+ if len(parts) == 2:
+ prefix, port = DEFAULT_PREFIX, parts[1]
+ elif len(parts) == 3:
+ prefix, port = parts[1], parts[2]
+ else:
+ raise SystemExit(f"malformed interface '{iface}'")
+ try:
+ port = int(port)
+ except ValueError:
+ raise SystemExit(f"malformed interface '{iface}'")
+ return prefix, port
+
+
+def wait_for_stop(fifo_fd):
+ """Block until Wireshark stops us: either it closes the FIFO read end
+ (POLLERR on our write fd) or it sends SIGINT/SIGTERM. Watching the fd as
+ well as the signal matters because the signal may be missed -- Wireshark's
+ reliable stop is closing the pipe."""
+ rd, wr = os.pipe()
+ os.set_blocking(wr, False)
+ signal.set_wakeup_fd(wr)
+ for sig in (signal.SIGINT, signal.SIGTERM):
+ signal.signal(sig, lambda *_: None)
+
+ poller = select.poll()
+ poller.register(fifo_fd, select.POLLERR)
+ poller.register(rd, select.POLLIN)
+ poller.poll()
+
+ signal.set_wakeup_fd(-1)
+ os.close(rd)
+ os.close(wr)
+
+
+def cmd_capture(iface, fifo, snaplen, queue, cfilter):
+ prefix, port = parse_iface(iface)
+ path = socket_path(prefix)
+
+ # Blocking open of the FIFO Wireshark created. This rendezvous guarantees a
+ # reader is attached before we ask DPDK to open the write end (DPDK opens
+ # O_WRONLY|O_NONBLOCK and would fail with ENXIO if no reader were present).
+ # We then hold this fd for the whole session: it keeps the pipe from
+ # EOF-ing in the window before the DPDK capture thread opens its own write
+ # end, and it is our reliable stop signal -- POLLERR here means Wireshark
+ # closed the read end.
+ fifo_fd = os.open(fifo, os.O_WRONLY)
+
+ try:
+ tel = Telemetry(path)
+ except OSError as e:
+ os.close(fifo_fd)
+ raise SystemExit(f"cannot connect to DPDK telemetry at {path}: {e}")
+
+ params = [str(port), f"out={fifo}"]
+ if snaplen is not None:
+ params.append(f"snaplen={snaplen}")
+ if queue is not None and queue >= 0:
+ params.append(f"queue={queue}")
+ if cfilter:
+ params.append(f"filter={cfilter}")
+ cmd = CAPTURE_START_CMD + "," + ",".join(params)
+
+ try:
+ reply = tel.command(cmd)
+ except OSError as e:
+ os.close(fifo_fd)
+ tel.close()
+ raise SystemExit(f"capture start failed: {e}")
+
+ result = (reply or {}).get(CAPTURE_START_CMD) or {}
+ if "error" in result:
+ os.close(fifo_fd)
+ tel.close()
+ raise SystemExit(f"capture start failed: {result['error']}")
+
+ cap_id = result.get("id")
+
+ # Run until Wireshark stops us (signal or read-end close). DPDK now holds
+ # its own write end, so closing ours below won't EOF the reader prematurely.
+ wait_for_stop(fifo_fd)
+
+ # Ask DPDK to stop while we still hold the write end, so if the reader is
+ # still attached it drains the tail and writes the statistics block before
+ # the final EOF. If the read end is already gone DPDK has already stopped
+ # on the same hangup and this is a no-op.
+ if cap_id is not None:
+ try:
+ tel.command(f"{CAPTURE_STOP_CMD},{cap_id}")
+ except OSError:
+ pass
+
+ os.close(fifo_fd)
+ tel.close()
+
+
+# --- entry point ---------------------------------------------------------
+
+
+def main():
+ p = argparse.ArgumentParser(
+ prog="dpdk-wireshark-extcap.py",
+ allow_abbrev=False,
+ description="Wireshark extcap plugin for live packet capture from the "
+ "Ethernet ports of a running DPDK application. Normally "
+ "invoked by Wireshark; see the DPDK Wireshark extcap guide.",
+ )
+ p.add_argument("--version", action="version", version=f"%(prog)s {EXTCAP_VERSION}")
+
+ p.add_argument("--extcap-interfaces", action="store_true")
+ p.add_argument("--extcap-dlts", action="store_true")
+ p.add_argument("--extcap-config", action="store_true")
+ p.add_argument("--capture", action="store_true")
+ p.add_argument("--extcap-interface")
+ p.add_argument("--fifo")
+ p.add_argument("--extcap-capture-filter")
+ p.add_argument("--extcap-version", nargs="?")
+ p.add_argument("--snaplen", type=int)
+ p.add_argument("--queue", type=int)
+ args, _ = p.parse_known_args()
+
+ if args.extcap_interfaces:
+ cmd_interfaces()
+ elif args.extcap_dlts:
+ cmd_dlts(args.extcap_interface)
+ elif args.extcap_config:
+ cmd_config(args.extcap_interface)
+ elif args.capture:
+ if not args.extcap_interface or not args.fifo:
+ raise SystemExit("--capture requires --extcap-interface and --fifo")
+ cmd_capture(
+ args.extcap_interface,
+ args.fifo,
+ args.snaplen,
+ args.queue,
+ args.extcap_capture_filter,
+ )
+ elif args.extcap_capture_filter:
+ # Wireshark validates a capture filter by invoking the extcap with the
+ # filter but without --capture (see doc/extcap_example.py upstream). We
+ # accept it: Wireshark already syntax-checks it with libpcap against our
+ # DLT (EN10MB), and DPDK compiles it again at capture start. Exiting 0
+ # with no output means "accepted"; printing a line would mark it invalid.
+ pass
+ else:
+ raise SystemExit("no extcap operation specified")
+
+
+if __name__ == "__main__":
+ main()
--
2.53.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH 5/5] usertools/dpdk-dumpcap: add script for file capture
2026-07-24 21:11 [PATCH v6 0/5] Wireshark external capture for DPDK Stephen Hemminger
` (3 preceding siblings ...)
2026-07-24 21:11 ` [PATCH 4/5] usertools/dpdk-wireshark-extcap.py: script for external capture Stephen Hemminger
@ 2026-07-24 21:11 ` Stephen Hemminger
4 siblings, 0 replies; 6+ messages in thread
From: Stephen Hemminger @ 2026-07-24 21:11 UTC (permalink / raw)
To: dev; +Cc: Stephen Hemminger
This is a python version using capture library that provides
the same syntax and functionality as wireshark dumpcap and
the existing secondary process version dpdk-dumpcap.
Since it is a python script and uses capture it does not
need secondary process support and should be portable
to other OS environments.
It is designed to be a replacement for dpdk-dumpcap
program which uses pdump.
Signed-off-by: Stephen Hemminger <stephen@networkplumber.org>
---
MAINTAINERS | 1 +
doc/guides/rel_notes/release_26_11.rst | 1 +
usertools/dpdk-dumpcap.py | 426 +++++++++++++++++++++++++
3 files changed, 428 insertions(+)
create mode 100755 usertools/dpdk-dumpcap.py
diff --git a/MAINTAINERS b/MAINTAINERS
index 5ce969d02f..e682823df9 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1722,6 +1722,7 @@ M: Reshma Pattan <reshma.pattan@intel.com>
M: Stephen Hemminger <stephen@networkplumber.org>
F: lib/capture/
F: usertools/dpdk-wireshark-extcap.py
+F: usertools/dpdk-dumpcap.py
F: doc/guides/tools/wireshark_extcap.rst
F: lib/pdump/
F: doc/guides/prog_guide/pdump_lib.rst
diff --git a/doc/guides/rel_notes/release_26_11.rst b/doc/guides/rel_notes/release_26_11.rst
index 9835807fcc..6590899585 100644
--- a/doc/guides/rel_notes/release_26_11.rst
+++ b/doc/guides/rel_notes/release_26_11.rst
@@ -59,6 +59,7 @@ New Features
* Added ``capture`` library for packet capture via telemetry API.
* Added python script for wireshark external capture integration.
+ * Added python version of dpdk-dumpcap.
Removed Items
diff --git a/usertools/dpdk-dumpcap.py b/usertools/dpdk-dumpcap.py
new file mode 100755
index 0000000000..bdec55964a
--- /dev/null
+++ b/usertools/dpdk-dumpcap.py
@@ -0,0 +1,426 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2026 Stephen Hemminger
+
+"""
+dpdk-dumpcap: capture packets from a running DPDK primary process.
+
+A tcpdump/dumpcap-style front end for the DPDK 'capture' library. Unlike the
+legacy secondary-process dpdk-dumpcap (which used pdump and required the primary
+to initialise the capture framework), this drives the primary directly over its
+telemetry socket: the primary opens the output file itself and writes pcapng
+into it. It works with any primary that links lib/capture; no application
+changes are needed.
+
+Examples:
+ dpdk-dumpcap --list-interfaces
+ dpdk-dumpcap -i 0 -w /tmp/port0.pcapng
+ dpdk-dumpcap -i 0 -c 100 -f 'tcp port 80'
+"""
+
+import argparse
+import json
+import os
+import signal
+import socket
+import sys
+import tempfile
+import time
+
+TELEMETRY_SOCKET = "dpdk_telemetry.v2"
+CAPTURE_START_CMD = "/ethdev/capture/start"
+CAPTURE_STOP_CMD = "/ethdev/capture/stop"
+CAPTURE_STATS_CMD = "/ethdev/capture/stats"
+ETHDEV_LIST = "/ethdev/list"
+ETHDEV_INFO = "/ethdev/info"
+DEFAULT_SNAPLEN = 262144
+COUNT_POLL_INTERVAL = 0.1 # fast stats poll cadence when -c is set
+POLL_TICK = 0.5 # loop wake granularity; bounds Ctrl-C latency when idle
+DEFAULT_STATS_INTERVAL = 0.5 # seconds between liveness updates
+
+
+# --- DPDK runtime directory / socket discovery ---------------------------
+
+
+def dpdk_dir():
+ override = os.environ.get("DPDK_EXTCAP_PATH")
+ if override:
+ return override
+ base = (
+ "/var/run" if os.geteuid() == 0 else os.environ.get("XDG_RUNTIME_DIR", "/tmp")
+ )
+ return os.path.join(base, "dpdk")
+
+
+def socket_path(prefix):
+ return os.path.join(dpdk_dir(), prefix, TELEMETRY_SOCKET)
+
+
+def list_prefixes():
+ """Yield (prefix, path) for each DPDK telemetry socket found."""
+ root = dpdk_dir()
+ try:
+ entries = os.listdir(root)
+ except FileNotFoundError:
+ return
+ except PermissionError:
+ sys.stderr.write(
+ f"cannot read {root}: permission denied. The DPDK runtime directory "
+ "is mode 0700, so capture must run as the same user as the DPDK "
+ "application, or set DPDK_EXTCAP_PATH.\n"
+ )
+ return
+ for prefix in sorted(entries):
+ path = os.path.join(root, prefix, TELEMETRY_SOCKET)
+ if os.path.exists(path):
+ yield prefix, path
+
+
+def resolve_prefix(requested):
+ """Pick the file-prefix to talk to. If one was requested, use it; else use
+ the sole running prefix, or error if there are zero or several."""
+ prefixes = [p for p, _ in list_prefixes()]
+ if requested is not None:
+ if requested not in prefixes:
+ raise SystemExit(f"no DPDK process with file-prefix '{requested}'")
+ return requested
+ if not prefixes:
+ raise SystemExit("no running DPDK process found")
+ if len(prefixes) > 1:
+ raise SystemExit(
+ "multiple DPDK processes running; select one with --file-prefix: "
+ + ", ".join(prefixes)
+ )
+ return prefixes[0]
+
+
+# --- Telemetry transport -------------------------------------------------
+
+
+class Telemetry:
+ """Minimal client for the DPDK v2 telemetry socket (SOCK_SEQPACKET)."""
+
+ def __init__(self, path):
+ self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
+ self.sock.connect(path)
+ info = json.loads(self.sock.recv(1024).decode())
+ self.max_output_len = info.get("max_output_len", 16384)
+
+ def command(self, cmd):
+ self.sock.send(cmd.encode())
+ reply = self.sock.recv(self.max_output_len)
+ if not reply:
+ return None
+ return json.loads(reply.decode())
+
+ def close(self):
+ self.sock.close()
+
+
+# --- operations ----------------------------------------------------------
+
+
+def port_name(tel, port):
+ """Device name for a port via /ethdev/info, or 'port<N>' if unreadable.
+ For a physical port this is the PCI address; for a vdev, the vdev name."""
+ try:
+ reply = tel.command(f"{ETHDEV_INFO},{port}")
+ except OSError:
+ reply = None
+ info = (reply or {}).get(ETHDEV_INFO) or {}
+ return info.get("name") or f"port{port}"
+
+
+def list_interfaces():
+ """List ports as '<port>. <name>', matching Wireshark/C dpdk-dumpcap -D.
+ Port ids are per-primary; if several primaries run, capture disambiguates
+ with --file-prefix (see resolve_prefix)."""
+ any_found = False
+ for _prefix, path in list_prefixes():
+ try:
+ tel = Telemetry(path)
+ except OSError as e:
+ sys.stderr.write(f"cannot query {path}: {e}\n")
+ continue
+ try:
+ ports = (tel.command(ETHDEV_LIST) or {}).get(ETHDEV_LIST) or []
+ for port in ports:
+ any_found = True
+ print(f"{port}. {port_name(tel, port)}")
+ except OSError as e:
+ sys.stderr.write(f"cannot query {path}: {e}\n")
+ finally:
+ tel.close()
+ if not any_found:
+ sys.stderr.write("no DPDK interfaces available for capture\n")
+
+
+def resolve_interface(tel, spec):
+ """Port id to capture on the connected primary. spec None selects the first
+ interface (like Wireshark and the C dpdk-dumpcap); otherwise it is a port id
+ or a device name as shown by --list-interfaces."""
+ ports = (tel.command(ETHDEV_LIST) or {}).get(ETHDEV_LIST) or []
+ if not ports:
+ raise SystemExit("no interfaces on this DPDK process")
+ if spec is None:
+ return ports[0]
+ try:
+ port = int(spec)
+ except ValueError:
+ port = None
+ if port is not None:
+ if port in ports:
+ return port
+ raise SystemExit(f"no port {port} on this DPDK process")
+ for port in ports:
+ if port_name(tel, port) == spec:
+ return port
+ raise SystemExit(f"no interface '{spec}' on this DPDK process")
+
+
+def default_output(name):
+ r"""Auto-named output in the temp dir, matching how current Wireshark
+ dumpcap names its default file: '<tool>_<iface><unique>.pcapng', with a
+ random unique suffix and no timestamp.
+ basename() strips any path-like parts of the device name, as Wireshark does;
+ mkstemp pre-creates the file empty, which is what the capture library wants."""
+ basename = os.path.basename(name)
+ fd, path = tempfile.mkstemp(prefix=f"dpdk-dumpcap_{basename}", suffix=".pcapng")
+ os.close(fd)
+ return path
+
+
+def read_stats(tel, cap_id):
+ """Return the stats dict, or None if the telemetry peer has closed (the
+ primary exited). An empty reply from a SEQPACKET socket is EOF."""
+ reply = tel.command(f"{CAPTURE_STATS_CMD},{cap_id}")
+ if reply is None:
+ return None
+ return reply.get(CAPTURE_STATS_CMD) or {}
+
+
+def print_progress(stats):
+ """Live packet counter, overwritten in place (carriage return, no newline)
+ like Wireshark: the running line reads 'Packets:', and print_summary
+ finalises it as 'Packets captured:' with a trailing newline."""
+ captured = stats.get("accepted", 0) - stats.get("ringfull", 0)
+ sys.stderr.write(f"\rPackets: {captured} ")
+ sys.stderr.flush()
+
+
+def print_summary(stats, name):
+ r"""dumpcap-style end-of-capture summary on stderr. The leading '\r'
+ finalises the in-place live counter (same line, overwritten). Counters map
+ onto the pcapng ISB the primary writes, so this agrees with the file trailer:
+ received (ifrecv) = accepted + filtered + nombuf -- true total seen
+ dropped (ifdrop) = nombuf + ringfull
+ captured = accepted - ringfull -- written to file
+ The percentage is 'fraction not dropped', off the true total (ifrecv already
+ includes the drops). An empty capture prints 0.0%, as dumpcap does."""
+ accepted = stats.get("accepted", 0)
+ filtered = stats.get("filtered", 0)
+ nombuf = stats.get("nombuf", 0)
+ ringfull = stats.get("ringfull", 0)
+
+ captured = accepted - ringfull
+ received = accepted + filtered + nombuf
+ dropped = nombuf + ringfull
+ pct = 100.0 * (received - dropped) / received if received else 0.0
+
+ print(f"\rPackets captured: {captured}", file=sys.stderr)
+ print(
+ f"Packets received/dropped on interface '{name}': "
+ f"{received}/{dropped} ({pct:.1f}%)",
+ file=sys.stderr,
+ )
+
+
+def capture(args):
+ prefix = resolve_prefix(args.file_prefix)
+ path = socket_path(prefix)
+
+ try:
+ tel = Telemetry(path)
+ except OSError as e:
+ raise SystemExit(f"cannot connect to DPDK telemetry at {path}: {e}")
+
+ port = resolve_interface(tel, args.interface)
+ name = port_name(tel, port)
+
+ output = args.write or default_output(name)
+
+ # The capture library opens the output O_WRONLY|O_NOFOLLOW without O_CREAT
+ # and rejects a non-empty file, so create/truncate it to an empty regular
+ # file here before handing over the path.
+ try:
+ open(output, "wb").close()
+ except OSError as e:
+ tel.close()
+ raise SystemExit(f"cannot create output {output}: {e}")
+
+ params = [str(port), f"out={output}"]
+ if args.snaplen is not None:
+ params.append(f"snaplen={args.snaplen}")
+ if args.queue is not None and args.queue >= 0:
+ params.append(f"queue={args.queue}")
+ if args.filter:
+ params.append(f"filter={args.filter}")
+ cmd = CAPTURE_START_CMD + "," + ",".join(params)
+
+ result = (tel.command(cmd) or {}).get(CAPTURE_START_CMD) or {}
+ if "error" in result:
+ tel.close()
+ raise SystemExit(f"capture start failed: {result['error']}")
+ cap_id = result.get("id")
+ if cap_id is None:
+ tel.close()
+ raise SystemExit("capture start did not return an id")
+
+ qnote = "" if args.queue is None or args.queue < 0 else f" queue {args.queue}"
+ print(f"File: {output}", file=sys.stderr)
+ print(f"Capturing on '{name}'{qnote}", file=sys.stderr)
+
+ # Stop on Ctrl-C/SIGTERM; the handler flips a flag the loop checks.
+ stop = {"now": False}
+ for sig in (signal.SIGINT, signal.SIGTERM):
+ signal.signal(sig, lambda *_: stop.update(now=True))
+
+ interval = args.stats_interval
+ # Wake on a short tick so a signal stops us promptly; poll fast when -c must
+ # bound overshoot, otherwise just often enough to notice the signal. Stats
+ # are read only when -c is set or a heartbeat is due, so an idle capture
+ # barely touches the telemetry socket.
+ tick = COUNT_POLL_INTERVAL if args.count is not None else POLL_TICK
+ next_beat = time.monotonic() + interval if interval > 0 else None
+
+ stats = {}
+ try:
+ while not stop["now"]:
+ time.sleep(tick)
+ now = time.monotonic()
+ due = next_beat is not None and now >= next_beat
+ if args.count is not None or due:
+ # The stats round-trip is itself the liveness check: if the
+ # primary has exited, the telemetry socket errors or returns
+ # EOF, and the capture (and its half-written pcapng) is gone.
+ try:
+ latest = read_stats(tel, cap_id)
+ except OSError:
+ latest = None
+ if latest is None:
+ print(
+ "\nDPDK telemetry connection lost; capture stopped",
+ file=sys.stderr,
+ )
+ break
+ if "error" in latest:
+ break # capture instance no longer present
+ if latest:
+ stats = latest
+ if args.count is not None and stats.get("accepted", 0) >= args.count:
+ break
+ if due:
+ print_progress(stats)
+ next_beat = now + interval
+ # snapshot final counters while the instance is still alive, then stop:
+ # after stop the primary tears the instance down and stats disappear.
+ try:
+ latest = read_stats(tel, cap_id)
+ if latest and "error" not in latest:
+ stats = latest
+ except OSError:
+ pass
+ finally:
+ try:
+ tel.command(f"{CAPTURE_STOP_CMD},{cap_id}")
+ except OSError:
+ pass
+
+ print_summary(stats, name)
+ tel.close()
+
+
+# --- entry point ---------------------------------------------------------
+
+
+def main():
+ p = argparse.ArgumentParser(
+ prog="dpdk-dumpcap",
+ description="Capture packets from a running DPDK primary process.",
+ )
+ p.add_argument(
+ "-D",
+ "--list-interfaces",
+ action="store_true",
+ help="list available DPDK interfaces and exit",
+ )
+ p.add_argument(
+ "-i",
+ "--interface",
+ metavar="IFACE",
+ help="port id or device name to capture from "
+ "(default: first interface; see --list-interfaces)",
+ )
+ p.add_argument(
+ "--file-prefix",
+ help="EAL file-prefix of the target primary "
+ "(required only if several are running)",
+ )
+ p.add_argument(
+ "-w", "--write", metavar="FILE", help="output pcapng file (default: auto-named)"
+ )
+ p.add_argument(
+ "-s",
+ "--snapshot-length",
+ dest="snaplen",
+ type=int,
+ metavar="LEN",
+ help="bytes to capture per packet (0 = all)",
+ )
+ p.add_argument(
+ "--queue",
+ type=int,
+ metavar="ID",
+ help="capture a single hardware queue (default: all queues)",
+ )
+ p.add_argument(
+ "-c",
+ "--packet-count",
+ dest="count",
+ type=int,
+ metavar="N",
+ help="stop after roughly N packets",
+ )
+ p.add_argument(
+ "--stats-interval",
+ type=float,
+ metavar="SECS",
+ default=DEFAULT_STATS_INTERVAL,
+ help="seconds between liveness updates "
+ f"(0 to disable; default {DEFAULT_STATS_INTERVAL})",
+ )
+ p.add_argument(
+ "-f",
+ "--filter",
+ dest="filter",
+ metavar="EXPR",
+ help="capture filter in libpcap syntax",
+ )
+ p.add_argument(
+ "filter_args", nargs="*", help="capture filter (if not given with -f)"
+ )
+ args = p.parse_args()
+
+ if args.list_interfaces:
+ list_interfaces()
+ return
+
+ # tcpdump-style trailing filter expression, if -f was not used.
+ if not args.filter and args.filter_args:
+ args.filter = " ".join(args.filter_args)
+
+ capture(args)
+
+
+if __name__ == "__main__":
+ main()
--
2.53.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-07-24 21:23 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-24 21:11 [PATCH v6 0/5] Wireshark external capture for DPDK Stephen Hemminger
2026-07-24 21:11 ` [PATCH 1/5] pcapng: extend interface statistics Stephen Hemminger
2026-07-24 21:11 ` [PATCH 2/5] capture: infrastructure wireshark packet capture Stephen Hemminger
2026-07-24 21:11 ` [PATCH 3/5] test: add test for capture hooks Stephen Hemminger
2026-07-24 21:11 ` [PATCH 4/5] usertools/dpdk-wireshark-extcap.py: script for external capture Stephen Hemminger
2026-07-24 21:11 ` [PATCH 5/5] usertools/dpdk-dumpcap: add script for file capture Stephen Hemminger
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox