[RFC 4/4] usertools/dpdk-wireshark-extcap.py: script for external capture
Stephen Hemminger
stephen at networkplumber.org
Tue Jun 9 23:02:05 CEST 2026
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
Also add MAINTAINERS and release note.
Signed-off-by: Stephen Hemminger <stephen at networkplumber.org>
---
MAINTAINERS | 2 +
doc/guides/tools/index.rst | 1 +
doc/guides/tools/wireshark_extcap.rst | 155 +++++++++++++++
usertools/dpdk-wireshark-extcap.py | 274 ++++++++++++++++++++++++++
4 files changed, 432 insertions(+)
create mode 100644 doc/guides/tools/wireshark_extcap.rst
create mode 100755 usertools/dpdk-wireshark-extcap.py
diff --git a/MAINTAINERS b/MAINTAINERS
index ff5f31c770..7cb8782910 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1725,6 +1725,8 @@ M: Reshma Pattan <reshma.pattan at intel.com>
M: Stephen Hemminger <stephen at networkplumber.org>
F: lib/capture/
F: app/test/test_capture.c
+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/doc/guides/tools/index.rst b/doc/guides/tools/index.rst
index 8ec429ec53..580c7d28b1 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
dmaperf
flow-perf
securityperf
diff --git a/doc/guides/tools/wireshark_extcap.rst b/doc/guides/tools/wireshark_extcap.rst
new file mode 100644
index 0000000000..fae39fd393
--- /dev/null
+++ b/doc/guides/tools/wireshark_extcap.rst
@@ -0,0 +1,155 @@
+.. 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 does not attach to the DPDK application as a secondary process and
+never touches packet data itself. It connects to the application's telemetry
+socket, asks it to start capturing, and hands Wireshark's capture pipe to the
+application over that socket. The DPDK capture library writes pcapng packets
+directly into the pipe; the plugin only sets the capture up and tears it down
+when Wireshark closes the pipe.
+
+
+Requirements
+------------
+
+* A DPDK application built with the capture library and with telemetry
+ enabled. Telemetry is enabled by default.
+
+* Wireshark with extcap support.
+
+* The plugin, and therefore Wireshark, must run as the same user as the DPDK
+ application. 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> (port <N>)``, where ``<name>`` is the device name reported by
+the application, such as ``net_tap0``. Selecting a port and starting the
+capture is all that is required.
+
+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 (port 0)}
+
+
+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.
+
+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. 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, and no configuration is needed: its ports appear automatically.
+
+Running several DPDK applications on one host is uncommon. Each primary
+process needs its own dedicated cores, memory, and network ports, so it is
+generally done only on large hosts deliberately partitioned for the purpose.
+In that case each application is started with a distinct ``--file-prefix`` so
+that its runtime state is kept separate.
+
+Each file-prefix is an independent namespace, much like a network namespace.
+The plugin operates within exactly one of them at a time and lists only the
+ports of the application using that prefix. The prefix is selected by the
+``DPDK_EXTCAP_FILE_PREFIX`` environment variable, which corresponds to the EAL
+``--file-prefix`` option and defaults to ``rte`` (the EAL default). It must be
+present in the environment that Wireshark inherits, so it has to be set before
+Wireshark is launched, not from within the capture dialog::
+
+ DPDK_EXTCAP_FILE_PREFIX=myapp wireshark
+
+The prefix cannot be chosen per capture from the Wireshark GUI, by design.
+Wireshark builds the interface list once, before any interface or its options
+are selected, so the prefix must be known at enumeration time. It is also
+deliberately not a per-interface option: the device names in the list are
+resolved against one application, and a per-capture override would let the
+name shown disagree with the port actually captured.
+
+
+Environment variables
+----------------------
+
+``DPDK_EXTCAP_FILE_PREFIX``
+ Selects which DPDK application, by EAL file-prefix, the plugin operates
+ on. Defaults to ``rte``. See `Selecting a DPDK application`_.
+
+``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 composes with ``DPDK_EXTCAP_FILE_PREFIX``: this variable
+ gives the base directory, the prefix selects the subdirectory within it.
+
+
+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`_. If the application was started with a non-default
+ ``--file-prefix``, set ``DPDK_EXTCAP_FILE_PREFIX`` to match before
+ launching Wireshark; 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..2d710bdf5c
--- /dev/null
+++ b/usertools/dpdk-wireshark-extcap.py
@@ -0,0 +1,274 @@
+#!/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: this plugin opens the FIFO that Wireshark hands it, then passes
+that file descriptor to the DPDK primary process over the telemetry socket
+(via SCM_RIGHTS). The DPDK 'capture' library writes pcapng straight into the
+FIFO; this plugin never touches packet data. Teardown is implicit: when
+Wireshark closes the read end, both the DPDK writer and this plugin see the
+hangup.
+
+Interface values are encoded as 'dpdk:<port>'. The DPDK file-prefix is
+ambient, not part of the interface value: it comes from
+DPDK_EXTCAP_FILE_PREFIX (default 'rte') in the environment Wireshark inherits,
+so one invocation is scoped to a single primary like a namespace. See
+doc/guides/tools/wireshark_extcap.rst for the rationale and the multi-prefix
+case.
+"""
+
+import argparse
+import array
+import json
+import os
+import select
+import signal
+import socket
+import sys
+
+EXTCAP_VERSION = "0.1"
+TELEMETRY_SOCKET = "dpdk_telemetry.v2"
+CAPTURE_CMD = "/ethdev/capture/start"
+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 file_prefix():
+ """The EAL file-prefix to operate on; see the module docstring."""
+ return os.environ.get("DPDK_EXTCAP_FILE_PREFIX", DEFAULT_PREFIX)
+
+
+def socket_path():
+ return os.path.join(dpdk_dir(), file_prefix(), TELEMETRY_SOCKET)
+
+
+# --- 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, fds=None):
+ """Send a command, optionally with file descriptors as ancillary data.
+
+ Returns the decoded JSON reply, or None if the peer sent nothing.
+ """
+ if fds:
+ fd_arr = array.array("i", fds)
+ self.sock.sendmsg(
+ [cmd.encode()], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, fd_arr)]
+ )
+ else:
+ 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()
+
+
+# --- extcap query operations --------------------------------------------
+
+
+def port_name(tel, port):
+ """Device name for a port via /ethdev/info, or 'port<N>' if unreadable."""
+ 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}}")
+ path = socket_path()
+ try:
+ tel = Telemetry(path)
+ except FileNotFoundError:
+ # No telemetry socket -> no DPDK primary with this file-prefix.
+ return
+ except PermissionError:
+ # The runtime dir is mode 0700; a different user cannot traverse it.
+ sys.stderr.write(
+ f"cannot access {path}: 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 / "
+ "DPDK_EXTCAP_FILE_PREFIX).\n"
+ )
+ return
+
+ # One connection for the whole enumeration: list the ports, then name
+ # each over the same socket (each telemetry connection costs the primary
+ # a handler thread).
+ try:
+ reply = tel.command(ETHDEV_LIST)
+ ports = (reply or {}).get(ETHDEV_LIST) or []
+ for port in ports:
+ name = port_name(tel, port)
+ print(
+ f"interface {{value=dpdk:{port}}}"
+ f"{{display=DPDK {name} (port {port})}}"
+ )
+ 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}}"
+ )
+
+
+# --- capture -------------------------------------------------------------
+
+
+def parse_iface(iface):
+ """Return the port number from a 'dpdk:<port>' interface value."""
+ scheme, sep, port = iface.partition(":")
+ if scheme != "dpdk" or not sep:
+ raise SystemExit(f"unsupported interface '{iface}'")
+ try:
+ return int(port)
+ except ValueError:
+ raise SystemExit(f"malformed interface '{iface}'")
+
+
+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."""
+ 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, cfilter):
+ port = parse_iface(iface)
+ path = socket_path()
+
+ # Open the FIFO Wireshark created; this blocks until it has 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)]
+ if snaplen is not None:
+ params.append(f"snaplen={snaplen}")
+ if cfilter:
+ params.append(f"filter={cfilter}")
+ cmd = CAPTURE_CMD + "," + ",".join(params)
+
+ try:
+ tel.command(cmd, fds=[fifo_fd])
+ except OSError as e:
+ os.close(fifo_fd)
+ tel.close()
+ raise SystemExit(f"capture start failed: {e}")
+
+ # DPDK now holds its own dup of the FIFO write end. We keep ours only as a
+ # hangup sentinel: when Wireshark closes the read end we get POLLERR, the
+ # same event that stops the DPDK-side writer.
+ wait_for_stop(fifo_fd)
+
+ 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")
+ p.add_argument("--snaplen", 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.extcap_capture_filter
+ )
+ else:
+ raise SystemExit("no extcap operation specified")
+
+
+if __name__ == "__main__":
+ main()
--
2.53.0
More information about the dev
mailing list