From: Stephen Hemminger <stephen@networkplumber.org>
To: dev@dpdk.org
Cc: Stephen Hemminger <stephen@networkplumber.org>
Subject: [PATCH v4 3/3] usertools: add script for wireshark capture
Date: Mon, 12 Oct 2026 20:41:05 -0700 [thread overview]
Message-ID: <20261013034145.979361-4-stephen@networkplumber.org> (raw)
In-Reply-To: <20261013034145.979361-1-stephen@networkplumber.org>
Provide glue executable that wireshark can use to access
telemetry based packet capture. It is a C program so that it
can be installed setuid if desired and it is standalone and
does not depend on any DPDK libraries or infrastructure.
It is dual licensed because it maybe desirable to put this
in wireshark repository later.
See https://www.wireshark.org/docs/man-pages/extcap.html
Signed-off-by: Stephen Hemminger <stephen@networkplumber.org>
---
MAINTAINERS | 2 +
doc/guides/prog_guide/capture_lib.rst | 8 +
doc/guides/rel_notes/release_26_11.rst | 1 +
doc/guides/tools/index.rst | 1 +
doc/guides/tools/wireshark_extcap.rst | 198 ++++
usertools/dpdk-wireshark-extcap.c | 1145 ++++++++++++++++++++++++
usertools/meson.build | 23 +
7 files changed, 1378 insertions(+)
create mode 100644 doc/guides/tools/wireshark_extcap.rst
create mode 100644 usertools/dpdk-wireshark-extcap.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 2192de83e3..5679d67f34 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1729,6 +1729,8 @@ F: app/pdump/
F: doc/guides/tools/pdump.rst
F: app/dumpcap/
F: doc/guides/tools/dumpcap.rst
+F: usertools/dpdk-wireshark-extcap.c
+F: doc/guides/tools/wireshark_extcap.rst
Packet Framework
diff --git a/doc/guides/prog_guide/capture_lib.rst b/doc/guides/prog_guide/capture_lib.rst
index a9c9591bf2..d195b45e04 100644
--- a/doc/guides/prog_guide/capture_lib.rst
+++ b/doc/guides/prog_guide/capture_lib.rst
@@ -145,6 +145,14 @@ capture fails to start if the FIFO has no reader::
--> /ethdev/capture/stop,0
{"/ethdev/capture/stop": {"status": "stopped"}}
+Using Wireshark
+---------------
+
+The ``dpdk-wireshark-extcap`` program presents the ports of a running DPDK
+application as Wireshark capture interfaces, using these telemetry commands
+with a FIFO created by Wireshark as the output.
+See :doc:`../tools/wireshark_extcap`.
+
Limitations
-----------
diff --git a/doc/guides/rel_notes/release_26_11.rst b/doc/guides/rel_notes/release_26_11.rst
index bc1d62d423..7460cd0672 100644
--- a/doc/guides/rel_notes/release_26_11.rst
+++ b/doc/guides/rel_notes/release_26_11.rst
@@ -166,6 +166,7 @@ New Features
* **Added wireshark capture support.**
* New ``capture`` library for packet capture accessed via telemetry API.
+ * Added ``dpdk-wireshark-extcap`` 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..b1a8fd7bf3
--- /dev/null
+++ b/doc/guides/tools/wireshark_extcap.rst
@@ -0,0 +1,198 @@
+.. SPDX-License-Identifier: BSD-3-Clause
+ Copyright(c) 2026 Stephen Hemminger
+
+Wireshark Extcap Plugin
+=======================
+
+The ``dpdk-wireshark-extcap`` program is an external capture (extcap)
+plugin that enables Wireshark capture of live traffic from
+the Ethernet ports of a 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 must be able to reach
+ the telemetry socket of the DPDK application. See `Permissions`_.
+
+The plugin is a standalone C program that speaks the telemetry protocol
+directly and links against nothing from DPDK. It is built and installed with
+the other DPDK tools, except on Windows. Building with ``libcap`` allows the
+capability install described under `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*. Symbolically link the program into the
+personal extcap directory, naming it ``dpdkdump`` to match the convention of
+the other programs there::
+
+ ln -s /usr/local/bin/dpdk-wireshark-extcap \
+ ~/.local/lib/wireshark/extcap/dpdkdump
+
+Or from a build tree::
+
+ ln -s $(pwd)/build/usertools/dpdk-wireshark-extcap \
+ ~/.local/lib/wireshark/extcap/dpdkdump
+
+The DPDK ports then appear in the interface list the next time the capture
+options dialog is opened.
+
+
+Permissions
+-----------
+
+The DPDK runtime directory is created mode ``0700``, so only the user that
+started the DPDK application can reach its telemetry socket. No privilege
+beyond access to that socket is required: if you can run ``dpdk-dumpcap``
+against an application, you can capture from it with this plugin.
+
+Running Wireshark as that same user needs nothing more, and a DPDK
+application need not run as root. A containerized one has its own runtime
+directory, which can be made readable by the Wireshark user. Run as a
+different user the interface list is empty, and ``--extcap-interfaces``
+prints the permission failure to standard error.
+
+The rest of this section applies only to capturing from a root application as
+an ordinary user. Install set-user-ID to the user running the application,
+root here, and restrict who may execute it::
+
+ chown root:wireshark dpdk-wireshark-extcap
+ chmod 4750 dpdk-wireshark-extcap
+
+Set-group-ID is not enough: the runtime directory is mode ``0700``, so the
+group bits grant nothing. This is unlike ``dumpcap``, which is set-group-ID
+because the ``/dev/bpf`` devices it needs are group accessible.
+
+On Linux a capability grants less and is preferred where available::
+
+ chown root:wireshark dpdk-wireshark-extcap
+ chmod 0750 dpdk-wireshark-extcap
+ setcap cap_dac_override+ep dpdk-wireshark-extcap
+
+This needs a build with ``libcap``; without it a file capability cannot be
+dropped and the plugin refuses to run. ``cap_dac_override`` rather than
+``cap_dac_read_search`` because connecting to the socket needs write
+permission. FreeBSD has no file capabilities, so set-user-ID is the only
+option there.
+
+Whoever runs Wireshark must be in the group named above, or the plugin cannot
+be executed and no DPDK interfaces appear. Link the extcap entry to the
+installed program; copying does not preserve set-user-ID or capabilities.
+
+Privilege is used only for the connect, then dropped permanently before the
+banner is read, before any reply is parsed, and before the FIFO is opened.
+The drop is verified and privilege is never raised again.
+
+After a privileged start the plugin is more restrictive: the environment is
+ignored, only ``/var/run/dpdk`` is searched, and the output must be a FIFO.
+
+
+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::
+
+ $ dpdk-wireshark-extcap --extcap-interfaces
+ extcap {version=0.1}{display=DPDK telemetry capture}
+ interface {value=dpdk:0}{display=DPDK net_tap0}
+
+When no interfaces are listed the reason is printed to standard error, which
+the Wireshark GUI does not show. ``--help`` lists the options.
+
+
+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. If the queue count
+ cannot be read the option is omitted and capture uses all queues.
+
+Capture filter
+ A libpcap filter expression, applied by the DPDK application to the
+ captured traffic.
+
+
+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
+----------------------
+
+The runtime directory location depends on the user the DPDK application runs
+as, not on who runs the plugin, so both are searched: ``/var/run/dpdk`` for an
+application started by root, and ``$XDG_RUNTIME_DIR/dpdk``, or ``/tmp/dpdk``
+if that is unset, for one started by an ordinary user. Ports found in either
+are listed together.
+
+``DPDK_EXTCAP_PATH``
+ Search this base directory instead, for a runtime directory in a
+ non-standard location. Ignored, along with ``XDG_RUNTIME_DIR``, after a
+ privileged start; see `Permissions`_.
+
+
+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,
+ or that the plugin is installed with privilege; 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 ``--extcap-interfaces`` by hand prints diagnostics to standard
+ error that the Wireshark GUI hides.
+
+A port is listed as ``portN`` instead of a device name
+ The port was reported but its details could not be read, usually because
+ the application stopped in between. A capture against it will fail;
+ restart the application.
diff --git a/usertools/dpdk-wireshark-extcap.c b/usertools/dpdk-wireshark-extcap.c
new file mode 100644
index 0000000000..377ae57ef4
--- /dev/null
+++ b/usertools/dpdk-wireshark-extcap.c
@@ -0,0 +1,1145 @@
+// 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.
+ *
+ * Wireshark creates a FIFO, this program opens it and passes the path to the
+ * DPDK application as out= in /ethdev/capture/start. The application writes
+ * pcapng straight into the FIFO; packet data never passes through here.
+ * Capture stops when the read end closes or on SIGINT/SIGTERM, then
+ * /ethdev/capture/stop is sent so the application can flush.
+ *
+ * Interface values are 'dpdk:<port>' for the default file-prefix and
+ * 'dpdk:<prefix>:<port>' otherwise.
+ *
+ * Privilege
+ * ---------
+ * The runtime directory is mode 0700 and owned by whoever runs the DPDK
+ * application, so capturing from a root application as a desktop user needs
+ * help. As in dumpcap, privilege is dropped monotonically and never regained:
+ * argv is parsed and every telemetry socket opened while still privileged,
+ * then everything is dropped and verified, and the banner, replies and FIFO
+ * are handled unprivileged. Nothing after the connect needs privilege, so
+ * there is no equivalent of dumpcap's retained CAP_NET_RAW.
+ *
+ * Install setuid to the application's user, or on Linux root:wireshark 0750
+ * with cap_dac_override+ep (needs -DHAVE_LIBCAP -lcap; without libcap a file
+ * capability cannot be dropped, so do not use that install). Connecting to
+ * the socket needs write permission, which cap_dac_read_search does not give.
+ *
+ * After a privileged exec the environment is ignored (DPDK_EXTCAP_PATH,
+ * XDG_RUNTIME_DIR) and the output must be a FIFO.
+ *
+ * The runtime directory location depends on the user the application runs as,
+ * not on who runs this, so every candidate is searched; see dpdk_dirs().
+ *
+ * Build:
+ * cc -O2 -Wall -Wextra -D_FORTIFY_SOURCE=2 -fstack-protector-strong \
+ * -fPIE -pie -Wl,-z,relro,-z,now -o dpdk-wireshark-extcap \
+ * dpdk-wireshark-extcap.c
+ */
+
+/* The DPDK build passes this; keep the standalone build above working too. */
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+#include <dirent.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/un.h>
+#include <unistd.h>
+
+#ifdef __linux__
+#include <sys/auxv.h>
+#endif
+#ifdef HAVE_LIBCAP
+#include <sys/capability.h>
+#endif
+
+#define PROG_NAME "dpdk-wireshark-extcap"
+#define EXTCAP_VERSION "0.1"
+#define TELEMETRY_SOCKET "dpdk_telemetry.v2"
+#define CAPTURE_START_CMD "/ethdev/capture/start"
+#define CAPTURE_STOP_CMD "/ethdev/capture/stop"
+#define ETHDEV_LIST "/ethdev/list"
+#define ETHDEV_INFO "/ethdev/info"
+#define DEFAULT_SNAPLEN 262144
+#define DEFAULT_PREFIX "rte"
+#define DLT_EN10MB 1
+
+#define SECURE_DIR "/var/run/dpdk"
+#define ARRAY_SIZE(a) ((int)(sizeof(a) / sizeof((a)[0])))
+#define MAX_PREFIXES 64
+#define PREFIX_MAX 64
+#define MAX_PORTS 256
+#define MAX_QUEUES 1024
+#define PARAMS_MAX 1024 /* primary limit (CAP_CMD_MAX) */
+#define CMD_BUF (PARAMS_MAX + 128)
+#define REPLY_TIMEOUT_SEC 30
+#define MIN_REPLY_LEN 1024
+#define MAX_REPLY_LEN (1 << 20)
+
+/* One connected primary. */
+struct source {
+ char prefix[PREFIX_MAX];
+ int fd;
+};
+
+static uid_t ruid, euid;
+static gid_t rgid, egid;
+static bool privileged_exec;
+
+/* --- privilege handling -------------------------------------------- */
+
+/*
+ * True when the kernel raised our privilege at exec: setuid, setgid or file
+ * capabilities. This is the same test the C library uses to decide whether to
+ * trust the environment. dumpcap compares uids instead, which misses the
+ * capability-only install where ruid == euid throughout, and counts a plain
+ * root invocation as privileged, which this does not.
+ */
+static bool
+exec_was_privileged(void)
+{
+#if defined(__linux__)
+ return getauxval(AT_SECURE) != 0;
+#elif defined(__FreeBSD__)
+ return issetugid();
+#else
+ return getuid() != geteuid() || getgid() != getegid();
+#endif
+}
+
+static void
+priv_init(void)
+{
+ ruid = getuid();
+ rgid = getgid();
+ euid = geteuid();
+ egid = getegid();
+ privileged_exec = exec_was_privileged();
+}
+
+/*
+ * Give up privilege permanently, then prove it cannot be taken back.
+ *
+ * Effective uid goes last: changing it is what takes away the right to change
+ * anything else. Capabilities are cleared whether or not this process has any,
+ * as in dumpcap's relinquish_all_capabilities().
+ */
+static void
+priv_drop(void)
+{
+ if (setresgid(rgid, rgid, rgid) != 0)
+ err(1, "setresgid");
+ if (setresuid(ruid, ruid, ruid) != 0)
+ err(1, "setresuid");
+
+ if (getuid() != ruid || geteuid() != ruid ||
+ getgid() != rgid || getegid() != rgid)
+ errx(1, "privilege drop failed");
+ if (euid != ruid && seteuid(euid) == 0)
+ errx(1, "privilege drop failed");
+ if (egid != rgid && setegid(egid) == 0)
+ errx(1, "privilege drop failed");
+
+#ifdef HAVE_LIBCAP
+ /* A file-capability install changes no id above, so this is the drop. */
+ cap_t caps = cap_init(); /* all capabilities off */
+
+ if (caps == NULL)
+ err(1, "cap_init");
+ if (cap_set_proc(caps) != 0)
+ err(1, "cap_set_proc");
+ cap_free(caps);
+#else
+ if (privileged_exec && ruid == euid && rgid == egid)
+ errx(1, "built without libcap, cannot drop a file capability");
+#endif
+}
+
+/* Make sure fds 0-2 are open so later opens cannot land on them. */
+static void
+std_fds(void)
+{
+ for (int fd = 0; fd <= 2; fd++) {
+ if (fcntl(fd, F_GETFD) < 0 && errno == EBADF) {
+ int n = open("/dev/null", O_RDWR);
+
+ if (n != fd)
+ _exit(1);
+ }
+ }
+}
+
+/* --- minimal JSON scanner ------------------------------------------ */
+
+static const char *
+skip_ws(const char *p)
+{
+ while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')
+ p++;
+ return p;
+}
+
+/* Return pointer to the value following "key": or NULL. */
+static const char *
+json_value(const char *json, const char *key)
+{
+ char pat[96];
+ const char *p = json;
+ int n = snprintf(pat, sizeof(pat), "\"%s\"", key);
+
+ if (n < 0 || (size_t)n >= sizeof(pat))
+ return NULL;
+
+ while ((p = strstr(p, pat)) != NULL) {
+ const char *v = skip_ws(p + n);
+
+ if (*v == ':')
+ return skip_ws(v + 1);
+ p += n;
+ }
+ return NULL;
+}
+
+static bool
+json_int(const char *json, const char *key, long long *val)
+{
+ const char *v = json_value(json, key);
+ char *end;
+ long long x;
+
+ if (v == NULL || !((*v >= '0' && *v <= '9') || *v == '-'))
+ return false;
+ errno = 0;
+ x = strtoll(v, &end, 10);
+ if (errno != 0 || end == v)
+ return false;
+ *val = x;
+ return true;
+}
+
+/* Copy a string value, replacing non printable characters with '_'. */
+static bool
+json_str(const char *json, const char *key, char *out, size_t outsz)
+{
+ const char *v = json_value(json, key);
+ size_t n = 0;
+
+ if (v == NULL || *v != '"')
+ return false;
+ for (v++; *v != '"'; v++) {
+ unsigned char c = *v;
+
+ if (c == '\0')
+ return false;
+ if (c == '\\') {
+ c = '_';
+ if (v[1] == '\0')
+ return false;
+ v++;
+ }
+ if (c < 0x20 || c > 0x7e)
+ c = '_';
+ if (n + 1 < outsz)
+ out[n++] = c;
+ }
+ out[n] = '\0';
+ return true;
+}
+
+static int
+json_int_array(const char *json, const char *key, long long *out, int max)
+{
+ const char *v = json_value(json, key);
+ int n = 0;
+
+ if (v == NULL || *v != '[')
+ return 0;
+ v = skip_ws(v + 1);
+ while (*v != '\0' && *v != ']') {
+ char *end;
+ long long x;
+
+ errno = 0;
+ x = strtoll(v, &end, 10);
+ if (end == v || errno != 0)
+ break;
+ if (n < max)
+ out[n++] = x;
+ v = skip_ws(end);
+ if (*v != ',')
+ break;
+ v = skip_ws(v + 1);
+ }
+ return n;
+}
+
+/* Names go into extcap sentences, keep them to a safe alphabet. */
+static void
+sanitize_label(char *s)
+{
+ for (; *s != '\0'; s++) {
+ char c = *s;
+
+ if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
+ (c >= '0' && c <= '9') || c == '.' || c == '_' ||
+ c == ':' || c == '-'))
+ *s = '_';
+ }
+}
+
+/*
+ * Why this string cannot be handed to the primary, or NULL if it can.
+ * Telemetry parameters are a comma separated list with no escaping.
+ */
+static const char *
+param_problem(const char *s)
+{
+ if (strchr(s, ',') != NULL)
+ return "must not contain a comma";
+
+ for (; *s != '\0'; s++) {
+ if ((unsigned char)*s < 0x20 || *s == 0x7f)
+ return "must not contain control characters";
+ }
+ return NULL;
+}
+
+/* --- DPDK runtime directory / socket discovery ---------------------- */
+
+static char dirbuf[PATH_MAX];
+
+/*
+ * Directories that may hold the per-prefix runtime subdirectories.
+ *
+ * Where the primary put them depends on the user it runs as, not on the user
+ * running this program: a root primary uses /var/run/dpdk whether or not the
+ * capture tool is root. So rather than guess from our own uid, return every
+ * candidate and let the caller look in each.
+ *
+ * DPDK_EXTCAP_PATH overrides the search. It is ignored after a privileged
+ * exec, where the environment is not trusted.
+ */
+static int
+dpdk_dirs(const char *dirs[], int max)
+{
+ const char *env, *xdg;
+ int n = 0;
+
+ if (!privileged_exec) {
+ env = getenv("DPDK_EXTCAP_PATH");
+ if (env != NULL && *env != '\0') {
+ dirs[n++] = env;
+ return n;
+ }
+ }
+
+ /* where a root primary puts it */
+ if (n < max)
+ dirs[n++] = SECURE_DIR;
+
+ /* where a primary running as an ordinary user puts it */
+ if (!privileged_exec) {
+ xdg = getenv("XDG_RUNTIME_DIR");
+ if (xdg == NULL || *xdg == '\0')
+ xdg = "/tmp";
+ if ((size_t)snprintf(dirbuf, sizeof(dirbuf), "%s/dpdk", xdg) >= sizeof(dirbuf))
+ errx(1, "runtime directory path too long");
+ if (n < max)
+ dirs[n++] = dirbuf;
+ }
+
+ return n;
+}
+
+static bool
+valid_prefix(const char *s)
+{
+ size_t len = strlen(s);
+
+ if (len == 0 || len >= PREFIX_MAX)
+ return false;
+ if (strcmp(s, ".") == 0 || strcmp(s, "..") == 0)
+ return false;
+ for (; *s != '\0'; s++) {
+ char c = *s;
+
+ if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
+ (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-'))
+ return false;
+ }
+ return true;
+}
+
+static void
+socket_path(const char *dir, const char *prefix, char *out, size_t sz)
+{
+ if ((size_t)snprintf(out, sz, "%s/%s/%s", dir, prefix,
+ TELEMETRY_SOCKET) >= sz)
+ errx(1, "socket path too long");
+}
+
+/* --- telemetry transport -------------------------------------------- */
+
+struct telemetry {
+ int fd;
+ size_t max_len;
+ char *buf; /* last reply, always NUL terminated */
+};
+
+/*
+ * Connect to a telemetry socket. This is the only call that needs privilege,
+ * so it is also the only thing done before priv_drop().
+ */
+static int
+tel_connect(const char *path)
+{
+ struct sockaddr_un sa;
+ size_t len = strlen(path);
+ int fd, save;
+
+ memset(&sa, 0, sizeof(sa));
+ sa.sun_family = AF_UNIX;
+ if (len >= sizeof(sa.sun_path)) {
+ errno = ENAMETOOLONG;
+ return -1;
+ }
+ memcpy(sa.sun_path, path, len);
+
+ fd = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0);
+ if (fd < 0)
+ return -1;
+
+ if (connect(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
+ save = errno;
+ close(fd);
+ errno = save;
+ return -1;
+ }
+ return fd;
+}
+
+/*
+ * Read the banner and size the reply buffer. Runs unprivileged: this is the
+ * first point at which the primary gets to send us anything. Takes ownership
+ * of fd and closes it on failure.
+ */
+static int
+tel_handshake(struct telemetry *t, int fd)
+{
+ struct timeval tv = { .tv_sec = REPLY_TIMEOUT_SEC };
+ char banner[1025];
+ long long maxlen;
+ ssize_t n;
+ int save;
+
+ setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
+
+ do {
+ n = recv(fd, banner, sizeof(banner) - 1, 0);
+ } while (n < 0 && errno == EINTR);
+ if (n <= 0) {
+ save = n < 0 ? errno : ECONNRESET;
+ close(fd);
+ errno = save;
+ return -1;
+ }
+ banner[n] = '\0';
+
+ if (!json_int(banner, "max_output_len", &maxlen))
+ maxlen = 16384;
+ if (maxlen < MIN_REPLY_LEN)
+ maxlen = MIN_REPLY_LEN;
+ if (maxlen > MAX_REPLY_LEN)
+ maxlen = MAX_REPLY_LEN;
+
+ t->buf = malloc(maxlen + 1);
+ if (t->buf == NULL) {
+ close(fd);
+ errno = ENOMEM;
+ return -1;
+ }
+ t->fd = fd;
+ t->max_len = maxlen;
+ return 0;
+}
+
+/* Send a command; reply lands in t->buf (empty string if no reply). */
+static int
+tel_command(struct telemetry *t, const char *cmd)
+{
+ size_t len = strlen(cmd);
+ ssize_t n;
+
+ do {
+ n = send(t->fd, cmd, len, 0);
+ } while (n < 0 && errno == EINTR);
+ if (n < 0)
+ return -1;
+
+ do {
+ n = recv(t->fd, t->buf, t->max_len, 0);
+ } while (n < 0 && errno == EINTR);
+ if (n < 0)
+ return -1;
+ t->buf[n] = '\0';
+ return 0;
+}
+
+static void
+tel_close(struct telemetry *t)
+{
+ close(t->fd);
+ free(t->buf);
+ t->buf = NULL;
+}
+
+static long
+port_queue_count(struct telemetry *t, long port)
+{
+ char cmd[64];
+ long long rx = 0, tx = 0;
+
+ snprintf(cmd, sizeof(cmd), ETHDEV_INFO ",%ld", port);
+ if (tel_command(t, cmd) < 0)
+ return 0;
+ json_int(t->buf, "nb_rx_queues", &rx);
+ json_int(t->buf, "nb_tx_queues", &tx);
+ return rx > tx ? (long)rx : (long)tx;
+}
+
+/* Device name via /ethdev/info, or "port<N>" if unreadable. */
+static void
+port_name(struct telemetry *t, long port, char *out, size_t sz)
+{
+ char cmd[64];
+
+ snprintf(cmd, sizeof(cmd), ETHDEV_INFO ",%ld", port);
+ if (tel_command(t, cmd) < 0 || !json_str(t->buf, "name", out, sz) ||
+ out[0] == '\0')
+ snprintf(out, sz, "port%ld", port);
+ sanitize_label(out);
+}
+
+/* --- interface naming ---------------------------------------------- */
+
+static void
+iface_value(const char *prefix, long port, char *out, size_t sz)
+{
+ if (strcmp(prefix, DEFAULT_PREFIX) == 0)
+ snprintf(out, sz, "dpdk:%ld", port);
+ else
+ snprintf(out, sz, "dpdk:%.*s:%ld", PREFIX_MAX - 1, prefix, port);
+}
+
+/* Inverse of iface_value. */
+static void
+parse_iface(const char *iface, char *prefix, size_t prefix_sz, long *port)
+{
+ const char *p, *c1, *c2, *num;
+ char *end;
+ unsigned long v;
+
+ c1 = strchr(iface, ':');
+ if (c1 == NULL || (size_t)(c1 - iface) != 4 || strncmp(iface, "dpdk", 4) != 0)
+ errx(1, "unsupported interface scheme in '%s'", iface);
+
+ p = c1 + 1;
+ c2 = strchr(p, ':');
+ if (c2 == NULL) {
+ snprintf(prefix, prefix_sz, "%s", DEFAULT_PREFIX);
+ num = p;
+ } else {
+ size_t len = c2 - p;
+
+ if (strchr(c2 + 1, ':') != NULL)
+ errx(1, "malformed interface '%s'", iface);
+ if (len == 0 || len >= prefix_sz)
+ errx(1, "malformed interface '%s'", iface);
+ memcpy(prefix, p, len);
+ prefix[len] = '\0';
+ num = c2 + 1;
+ }
+
+ if (!valid_prefix(prefix))
+ errx(1, "malformed interface '%s'", iface);
+ if (!(*num >= '0' && *num <= '9'))
+ errx(1, "malformed interface '%s'", iface);
+ errno = 0;
+ v = strtoul(num, &end, 10);
+ if (errno != 0 || *end != '\0' || v > UINT16_MAX)
+ errx(1, "malformed interface '%s'", iface);
+ *port = v;
+}
+
+/* --- privileged phase ----------------------------------------------- */
+
+static int
+cmp_source(const void *a, const void *b)
+{
+ const struct source *x = a, *y = b;
+
+ return strcmp(x->prefix, y->prefix);
+}
+
+/*
+ * Connect to every primary under the runtime directory.
+ *
+ * All of it happens before the drop, as dumpcap opens all of its capture
+ * devices before relinquishing. Nothing is read from these sockets here.
+ */
+static int
+connect_dir(const char *root, struct source *src, int n, int max, bool *looked,
+ bool *complained)
+{
+ struct dirent *de;
+ DIR *d;
+
+ d = opendir(root);
+ if (d == NULL) {
+ /*
+ * A directory that is simply not there is not worth a word: the
+ * other candidate is probably the live one. Anything else means
+ * a primary is likely there but out of reach, which is worth
+ * saying even if another directory works out.
+ */
+ if (errno == EACCES) {
+ *looked = true;
+ *complained = true;
+ warnx("cannot read %s: permission denied. The DPDK runtime "
+ "directory is created mode 0700, so capture must run "
+ "as the same user as the DPDK application, or be "
+ "installed with privilege", root);
+ } else if (errno != ENOENT) {
+ *looked = true;
+ *complained = true;
+ warn("cannot read %s", root);
+ }
+ return n;
+ }
+ *looked = true;
+
+ while (n < max && (de = readdir(d)) != NULL) {
+ char path[PATH_MAX];
+ struct stat st;
+ int fd;
+
+ if (!valid_prefix(de->d_name))
+ continue;
+ socket_path(root, de->d_name, path, sizeof(path));
+ if (lstat(path, &st) != 0 || !S_ISSOCK(st.st_mode))
+ continue;
+
+ fd = tel_connect(path);
+ if (fd < 0) {
+ warn("cannot connect to %s", path);
+ continue;
+ }
+ /* valid_prefix() bounded the length */
+ memcpy(src[n].prefix, de->d_name, strlen(de->d_name) + 1);
+ src[n].fd = fd;
+ n++;
+ }
+ closedir(d);
+
+ return n;
+}
+
+/*
+ * Connect to one named prefix, trying each candidate directory. The path tried
+ * last is left in out[] for the caller's error message.
+ */
+static int
+connect_prefix(const char *prefix, char *out, size_t outsz)
+{
+ const char *dirs[4];
+ int ndirs = dpdk_dirs(dirs, ARRAY_SIZE(dirs));
+
+ for (int i = 0; i < ndirs; i++) {
+ int fd;
+
+ socket_path(dirs[i], prefix, out, outsz);
+ fd = tel_connect(out);
+ if (fd >= 0)
+ return fd;
+ }
+ return -1;
+}
+
+static int
+connect_all(struct source *src, int max)
+{
+ const char *dirs[4];
+ bool looked = false, complained = false;
+ int ndirs, n = 0;
+
+ ndirs = dpdk_dirs(dirs, ARRAY_SIZE(dirs));
+ for (int i = 0; i < ndirs && n < max; i++)
+ n = connect_dir(dirs[i], src, n, max, &looked, &complained);
+
+ /* a reason was already given for an unreadable directory */
+ if (n == 0 && !complained) {
+ if (looked)
+ warnx("no DPDK telemetry socket found. Is a DPDK "
+ "application running with telemetry enabled?");
+ else
+ warnx("no DPDK runtime directory. Either no DPDK "
+ "application is running, or it runs as a different "
+ "user and this program was installed without privilege");
+ }
+
+ qsort(src, n, sizeof(*src), cmp_source);
+ return n;
+}
+
+/* --- extcap query operations --------------------------------------- */
+
+static void
+cmd_interfaces(struct source *src, int nsrc)
+{
+ printf("extcap {version=%s}{display=DPDK telemetry capture}\n", EXTCAP_VERSION);
+
+ for (int i = 0; i < nsrc; i++) {
+ long long ports[MAX_PORTS];
+ struct telemetry t;
+ int nports = 0;
+
+ if (tel_handshake(&t, src[i].fd) < 0) {
+ warn("no banner from %s", src[i].prefix);
+ continue;
+ }
+
+ if (tel_command(&t, ETHDEV_LIST) == 0)
+ nports = json_int_array(t.buf, ETHDEV_LIST, ports, MAX_PORTS);
+ else
+ warn("cannot query %s", src[i].prefix);
+
+ for (int j = 0; j < nports; j++) {
+ char name[128], value[160], display[256];
+
+ if (ports[j] < 0 || ports[j] > UINT16_MAX)
+ continue;
+ port_name(&t, ports[j], name, sizeof(name));
+ iface_value(src[i].prefix, ports[j], value, sizeof(value));
+ if (strcmp(src[i].prefix, DEFAULT_PREFIX) == 0)
+ snprintf(display, sizeof(display), "DPDK %.127s", name);
+ else
+ snprintf(display, sizeof(display), "DPDK %.127s@%.63s",
+ name, src[i].prefix);
+ printf("interface {value=%s}{display=%s}\n", value, display);
+ }
+ tel_close(&t);
+ }
+}
+
+static void
+cmd_dlts(void)
+{
+ printf("dlt {number=%d}{name=EN10MB}{display=Ethernet}\n", DLT_EN10MB);
+}
+
+static void
+cmd_config(int fd, long port)
+{
+ long nqueues = 0;
+
+ printf("arg {number=0}{call=--snaplen}{display=Snapshot length}"
+ "{tooltip=Bytes captured per packet (0 = whole packet)}"
+ "{type=integer}{range=0,%d}{default=%d}{group=Capture}\n",
+ DEFAULT_SNAPLEN, DEFAULT_SNAPLEN);
+
+ /*
+ * Bound the queue dropdown to the port's queue count. If the primary
+ * is unreachable just omit it, capture defaults to all queues.
+ */
+ if (fd >= 0) {
+ struct telemetry t;
+
+ if (tel_handshake(&t, fd) == 0) {
+ nqueues = port_queue_count(&t, port);
+ tel_close(&t);
+ }
+ }
+
+ if (nqueues > MAX_QUEUES)
+ nqueues = MAX_QUEUES;
+ if (nqueues > 0) {
+ printf("arg {number=1}{call=--queue}{display=Queue}"
+ "{tooltip=Capture a single hardware queue}"
+ "{type=selector}{group=Capture}\n");
+ printf("value {arg=1}{value=-1}{display=All queues}{default=true}\n");
+ for (long q = 0; q < nqueues; q++)
+ printf("value {arg=1}{value=%ld}{display=Queue %ld}\n", q, q);
+ }
+}
+
+/* --- capture -------------------------------------------------------- */
+
+static void
+cmd_append(char *cmd, const char *fmt, ...)
+ __attribute__((format(printf, 2, 3)));
+
+static void
+cmd_append(char *cmd, const char *fmt, ...)
+{
+ size_t len = strlen(cmd);
+ va_list ap;
+ int n;
+
+ va_start(ap, fmt);
+ n = vsnprintf(cmd + len, CMD_BUF - len, fmt, ap);
+ va_end(ap);
+ if (n < 0 || (size_t)n >= CMD_BUF - len)
+ errx(1, "capture parameters too long");
+}
+
+static int stop_pipe[2] = { -1, -1 };
+
+static void
+on_stop_signal(int sig)
+{
+ int saved = errno;
+ ssize_t r;
+
+ (void)sig;
+ r = write(stop_pipe[1], "x", 1);
+ (void)r;
+ errno = saved;
+}
+
+/*
+ * Block until Wireshark stops us: it closes the FIFO read end (error/hangup
+ * on our write fd) or sends SIGINT/SIGTERM. Handlers are restored before
+ * returning so a second signal terminates us the usual way.
+ */
+static void
+wait_for_stop(int fifo_fd)
+{
+ struct sigaction sa, old_int, old_term;
+ struct pollfd pfd[2];
+
+ if (pipe2(stop_pipe, O_CLOEXEC | O_NONBLOCK) != 0)
+ err(1, "pipe2");
+
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = on_stop_signal;
+ sigemptyset(&sa.sa_mask);
+ sigaction(SIGINT, &sa, &old_int);
+ sigaction(SIGTERM, &sa, &old_term);
+
+ pfd[0].fd = fifo_fd;
+ pfd[0].events = POLLERR;
+ pfd[1].fd = stop_pipe[0];
+ pfd[1].events = POLLIN;
+ for (;;) {
+ int r = poll(pfd, 2, -1);
+
+ if (r > 0 || (r < 0 && errno != EINTR))
+ break;
+ }
+
+ sigaction(SIGINT, &old_int, NULL);
+ sigaction(SIGTERM, &old_term, NULL);
+ close(stop_pipe[0]);
+ close(stop_pipe[1]);
+}
+
+static void
+cmd_capture(int fd, long port, const char *fifo, long snaplen, long queue,
+ const char *cfilter)
+{
+ char cmd[CMD_BUF];
+ char errmsg[256];
+ struct telemetry t;
+ struct stat sb;
+ long long cap_id;
+ int fifo_fd;
+
+ snprintf(cmd, sizeof(cmd), CAPTURE_START_CMD ",%ld,out=%s", port, fifo);
+ if (snaplen >= 0)
+ cmd_append(cmd, ",snaplen=%ld", snaplen);
+ if (queue >= 0)
+ cmd_append(cmd, ",queue=%ld", queue);
+ if (cfilter != NULL && *cfilter != '\0')
+ cmd_append(cmd, ",filter=%s", cfilter);
+ if (strlen(cmd) - sizeof(CAPTURE_START_CMD) >= PARAMS_MAX)
+ errx(1, "capture parameters too long");
+
+ /*
+ * Blocking open of the FIFO Wireshark created, unprivileged, which is
+ * where dumpcap opens its output too.
+ *
+ * Rendezvous: a reader is attached before the primary opens the write
+ * end (it uses O_NONBLOCK and fails with ENXIO without a reader). We
+ * hold this fd for the whole session: it keeps the pipe from EOF-ing
+ * before the primary has its own write end, and it is the reliable stop
+ * signal (error here means Wireshark closed the read end).
+ */
+ fifo_fd = open(fifo, O_WRONLY | O_CLOEXEC | O_NOFOLLOW);
+ if (fifo_fd < 0)
+ err(1, "cannot open fifo %s", fifo);
+ if (fstat(fifo_fd, &sb) != 0 || !S_ISFIFO(sb.st_mode))
+ errx(1, "%s is not a fifo", fifo);
+
+ if (tel_handshake(&t, fd) < 0)
+ err(1, "no banner from DPDK telemetry");
+
+ if (tel_command(&t, cmd) < 0)
+ err(1, "capture start failed");
+
+ if (json_str(t.buf, "error", errmsg, sizeof(errmsg)))
+ errx(1, "capture start failed: %s", errmsg);
+ if (!json_int(t.buf, "id", &cap_id) || cap_id < 0)
+ errx(1, "capture start failed: unexpected reply from primary");
+
+ /*
+ * Run until Wireshark stops us. The primary holds its own write end,
+ * so closing ours below does not EOF the reader prematurely.
+ */
+ wait_for_stop(fifo_fd);
+
+ /*
+ * Ask the primary to stop while we still hold the write end, so a
+ * still attached reader gets the tail and the statistics block before
+ * the final EOF. If the read end is gone the primary already stopped
+ * on the same hangup and this is a no-op.
+ */
+ snprintf(cmd, sizeof(cmd), CAPTURE_STOP_CMD ",%lld", cap_id);
+ tel_command(&t, cmd);
+
+ close(fifo_fd);
+ tel_close(&t);
+}
+
+/* --- entry point ---------------------------------------------------- */
+
+struct options {
+ bool interfaces, dlts, config, capture, version, help;
+ const char *iface, *fifo, *cfilter;
+ long snaplen, queue;
+};
+
+static void
+usage(FILE *f)
+{
+ fprintf(f,
+ "Usage: %s [options]\n"
+ "\n"
+ "Wireshark external capture (extcap) plugin for DPDK ports.\n"
+ "Normally run by Wireshark, not by hand.\n"
+ "\n"
+ "Extcap operations:\n"
+ " --extcap-interfaces list the ports of every running DPDK application\n"
+ " --extcap-dlts list link types of an interface\n"
+ " --extcap-config list capture options of an interface\n"
+ " --capture start a capture, needs --fifo\n"
+ " --extcap-version[=<ver>] version of the calling Wireshark\n"
+ " --extcap-interface <iface> interface to act on, as listed above\n"
+ "\n"
+ "Capture options:\n"
+ " --fifo <path> where to write pcapng, created by Wireshark\n"
+ " --extcap-capture-filter <f> pcap filter expression\n"
+ " --snaplen <n> bytes to capture per packet, 0 for all\n"
+ " --queue <n> capture one queue, -1 for all\n"
+ "\n"
+ " -h, --help this text\n"
+ " --version program version\n"
+ "\n"
+ "Environment:\n"
+ " DPDK_EXTCAP_PATH base DPDK runtime directory to search\n"
+ " (ignored if installed with privilege)\n",
+ PROG_NAME);
+}
+
+/* Match --name, --name=value or --name value. Returns 1 on match. */
+static int
+opt_value(int argc, char **argv, int *i, const char *name, const char **val,
+ bool optional)
+{
+ const char *a = argv[*i];
+ size_t len = strlen(name);
+
+ if (strncmp(a, name, len) != 0)
+ return 0;
+ if (a[len] == '=') {
+ *val = a + len + 1;
+ return 1;
+ }
+ if (a[len] != '\0')
+ return 0;
+ if (*i + 1 < argc && (!optional || argv[*i + 1][0] != '-')) {
+ *val = argv[++*i];
+ return 1;
+ }
+ if (optional) {
+ *val = NULL;
+ return 1;
+ }
+ errx(1, "%s requires a value", name);
+}
+
+static long
+parse_long(const char *name, const char *s, long lo, long hi)
+{
+ char *end;
+ long v;
+
+ errno = 0;
+ v = strtol(s, &end, 10);
+ if (errno != 0 || end == s || *end != '\0' || v < lo || v > hi)
+ errx(1, "invalid %s '%s'", name, s);
+ return v;
+}
+
+static void
+parse_args(int argc, char **argv, struct options *o)
+{
+ const char *val;
+
+ memset(o, 0, sizeof(*o));
+ o->snaplen = -1;
+ o->queue = -1;
+
+ /* Unknown options are ignored, Wireshark passes several extras. */
+ for (int i = 1; i < argc; i++) {
+ if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0)
+ o->help = true;
+ else if (strcmp(argv[i], "--version") == 0)
+ o->version = true;
+ else if (strcmp(argv[i], "--extcap-interfaces") == 0)
+ o->interfaces = true;
+ else if (strcmp(argv[i], "--extcap-dlts") == 0)
+ o->dlts = true;
+ else if (strcmp(argv[i], "--extcap-config") == 0)
+ o->config = true;
+ else if (strcmp(argv[i], "--capture") == 0)
+ o->capture = true;
+ else if (opt_value(argc, argv, &i, "--extcap-interface", &val, false))
+ o->iface = val;
+ else if (opt_value(argc, argv, &i, "--fifo", &val, false))
+ o->fifo = val;
+ else if (opt_value(argc, argv, &i, "--extcap-capture-filter", &val, false))
+ o->cfilter = val;
+ else if (opt_value(argc, argv, &i, "--extcap-version", &val, true))
+ continue;
+ else if (opt_value(argc, argv, &i, "--snaplen", &val, false))
+ o->snaplen = parse_long("--snaplen", val, 0, INT32_MAX);
+ else if (opt_value(argc, argv, &i, "--queue", &val, false))
+ o->queue = parse_long("--queue", val, -1, UINT16_MAX);
+ }
+}
+
+int
+main(int argc, char **argv)
+{
+ struct source src[MAX_PREFIXES];
+ struct options o;
+ const char *why;
+ long port = -1;
+ int nsrc = 0;
+
+ std_fds();
+ priv_init();
+ signal(SIGPIPE, SIG_IGN);
+ parse_args(argc, argv, &o);
+
+ /* Needs no privilege and no primary, so answer before anything else. */
+ if (o.help) {
+ usage(stdout);
+ return 0;
+ }
+
+ /*
+ * Privileged phase. Only the connect needs privilege, so it is the
+ * only thing here, and everything it needs from argv is checked first.
+ */
+ if (o.interfaces) {
+ nsrc = connect_all(src, MAX_PREFIXES);
+ } else if (o.config || o.capture) {
+ char path[PATH_MAX];
+
+ if (o.iface == NULL)
+ errx(1, "missing --extcap-interface");
+ if (o.capture) {
+ struct stat sb;
+
+ if (o.fifo == NULL)
+ errx(1, "--capture requires --fifo");
+ why = param_problem(o.fifo);
+ if (why != NULL)
+ errx(1, "fifo path %s", why);
+ if (o.cfilter != NULL) {
+ why = param_problem(o.cfilter);
+ if (why != NULL)
+ errx(1, "capture filter %s", why);
+ }
+
+ /*
+ * Advisory: fail before costing the primary a connection
+ * and a handler thread. lstat() to match the O_NOFOLLOW
+ * open, which is the check that counts.
+ */
+ if (lstat(o.fifo, &sb) != 0)
+ err(1, "%s", o.fifo);
+ if (!S_ISFIFO(sb.st_mode))
+ errx(1, "%s is not a fifo", o.fifo);
+ }
+
+ parse_iface(o.iface, src[0].prefix, PREFIX_MAX, &port);
+ src[0].fd = connect_prefix(src[0].prefix, path, sizeof(path));
+ if (src[0].fd < 0 && o.capture)
+ err(1, "cannot connect to DPDK telemetry at %s", path);
+ nsrc = 1;
+ }
+
+ /* Nothing below this point runs with privilege. */
+ priv_drop();
+
+ if (o.version)
+ printf("%s %s\n", PROG_NAME, EXTCAP_VERSION);
+ else if (o.interfaces)
+ cmd_interfaces(src, nsrc);
+ else if (o.dlts)
+ cmd_dlts();
+ else if (o.config)
+ cmd_config(src[0].fd, port);
+ else if (o.capture)
+ cmd_capture(src[0].fd, port, o.fifo, o.snaplen, o.queue, o.cfilter);
+ else if (o.cfilter != NULL) {
+ /*
+ * Wireshark validates a capture filter by running the extcap
+ * with the filter but without --capture. No output means
+ * accepted, any output marks it invalid. Exit status must stay
+ * 0 or Wireshark reports the filter as unknown. Only reject
+ * what the telemetry encoding cannot carry.
+ */
+ why = param_problem(o.cfilter);
+ if (why != NULL)
+ printf("capture filter %s\n", why);
+ } else {
+ warnx("no extcap operation specified");
+ usage(stderr);
+ return 1;
+ }
+
+ if (fflush(stdout) != 0)
+ err(1, "stdout");
+ return 0;
+}
diff --git a/usertools/meson.build b/usertools/meson.build
index 114d0a65b4..c2bc8715a3 100644
--- a/usertools/meson.build
+++ b/usertools/meson.build
@@ -20,3 +20,26 @@ install_subdir(
'telemetry-endpoints',
install_dir: 'share/dpdk',
strip_directory: false)
+
+# Wireshark extcap helper does not depend on anything from DPDK
+# and speaks the telemetry socket protocol directly.
+if not is_windows
+ extcap_cflags = []
+ extcap_deps = []
+
+ cap_dep = dependency('libcap', required: false)
+ if cap_dep.found()
+ extcap_cflags += '-DHAVE_LIBCAP'
+ extcap_deps += cap_dep
+ endif
+
+ executable('dpdk-wireshark-extcap',
+ files('dpdk-wireshark-extcap.c'),
+ # only for the project-wide '-include rte_config.h'
+ include_directories: config_inc,
+ c_args: extcap_cflags,
+ dependencies: extcap_deps,
+ pie: true,
+ install: true,
+ install_dir: 'bin')
+endif
--
2.53.0
next prev parent reply other threads:[~2026-10-13 3:42 UTC|newest]
Thread overview: 23+ messages / expand[flat|nested] mbox.gz Atom feed top
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
2026-07-27 22:42 ` [PATCH v2 0/4] Wireshark external capture interface Stephen Hemminger
2026-07-27 22:42 ` [PATCH v2 1/4] pcapng: extend interface statistics Stephen Hemminger
2026-07-27 22:42 ` [PATCH v2 2/4] capture: infrastructure wireshark packet capture Stephen Hemminger
2026-07-27 22:42 ` [PATCH v2 3/4] test: add test for capture hooks Stephen Hemminger
2026-07-27 22:42 ` [PATCH v2 4/4] usertools: add script for wireshark capture Stephen Hemminger
2026-07-29 23:28 ` Stephen Hemminger
2026-07-30 18:22 ` [PATCH v3 0/4] Wireshark external capture Stephen Hemminger
2026-07-30 18:22 ` [PATCH v3 1/4] pcapng: extend interface statistics Stephen Hemminger
2026-07-30 18:22 ` [PATCH v3 2/4] capture: infrastructure wireshark packet capture Stephen Hemminger
2026-07-30 18:22 ` [PATCH v3 3/4] test: add test for capture hooks Stephen Hemminger
2026-07-30 18:22 ` [PATCH v3 4/4] usertools: add script for wireshark capture Stephen Hemminger
2026-10-13 3:41 ` [PATCH v4 0/3] wireshark capture implementation Stephen Hemminger
2026-10-13 3:41 ` [PATCH v4 1/3] capture: infrastructure wireshark packet capture Stephen Hemminger
2026-10-13 3:41 ` [PATCH v4 2/3] test: add test for capture hooks Stephen Hemminger
2026-10-13 3:41 ` Stephen Hemminger [this message]
2026-10-13 8:28 ` [PATCH v4 0/3] wireshark capture implementation fengchengwen
2026-10-13 14:19 ` Stephen Hemminger
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261013034145.979361-4-stephen@networkplumber.org \
--to=stephen@networkplumber.org \
--cc=dev@dpdk.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox