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 <[email protected]>
---
 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 <[email protected]>
 M: Stephen Hemminger <[email protected]>
 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

Reply via email to