From: Stephen Hemminger <stephen@networkplumber.org>
To: dev@dpdk.org
Cc: Stephen Hemminger <stephen@networkplumber.org>,
Thomas Monjalon <thomas@monjalon.net>,
Reshma Pattan <reshma.pattan@intel.com>,
Robin Jarry <rjarry@redhat.com>
Subject: [RFC 4/4] usertools/dpdk-wireshark-extcap.py: script for external capture
Date: Tue, 9 Jun 2026 14:02:05 -0700 [thread overview]
Message-ID: <20260609210540.768074-5-stephen@networkplumber.org> (raw)
In-Reply-To: <20260609210540.768074-1-stephen@networkplumber.org>
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@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@intel.com>
M: Stephen Hemminger <stephen@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
prev parent reply other threads:[~2026-06-09 21:06 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-09 21:02 [RFC 0/4] alternative capture mechanism Stephen Hemminger
2026-06-09 21:02 ` [RFC 1/4] telemetry: allow commands to receive file descriptors Stephen Hemminger
2026-06-09 21:02 ` [RFC 2/4] capture: infrastructure wireshark packet capture Stephen Hemminger
2026-06-09 21:02 ` [RFC 3/4] test: add test for capture hooks Stephen Hemminger
2026-06-09 21:02 ` Stephen Hemminger [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260609210540.768074-5-stephen@networkplumber.org \
--to=stephen@networkplumber.org \
--cc=dev@dpdk.org \
--cc=reshma.pattan@intel.com \
--cc=rjarry@redhat.com \
--cc=thomas@monjalon.net \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox