[PATCH] examples/rpcapd: demo version of packet capture daemon
From: Stephen Hemminger <stephen@networkplumber.org>
Date: 2026-09-08 21:08:44
Subsystem:
the rest · Maintainer:
Linus Torvalds
This example adds RPCAP support over localhost TCP integrated with DPDK. It uses a secondary process that allows connections from using tcpdump defacto protocol rpcap. See: doc/guides/sample_app_ug/rpcap.rst for more info. This does not preclude using pdump, dumpcap, or the wireshark extcap integration. Signed-off-by: Stephen Hemminger <stephen@networkplumber.org> --- doc/guides/rel_notes/release_26_11.rst | 4 + doc/guides/sample_app_ug/index.rst | 1 + doc/guides/sample_app_ug/rpcapd.rst | 206 +++++ examples/meson.build | 1 + examples/rpcapd/main.c | 1014 ++++++++++++++++++++++++ examples/rpcapd/meson.build | 11 + examples/rpcapd/rpcap-protocol.h | 96 +++ 7 files changed, 1333 insertions(+) create mode 100644 doc/guides/sample_app_ug/rpcapd.rst create mode 100644 examples/rpcapd/main.c create mode 100644 examples/rpcapd/meson.build create mode 100644 examples/rpcapd/rpcap-protocol.h
diff --git a/doc/guides/rel_notes/release_26_11.rst b/doc/guides/rel_notes/release_26_11.rst
index 87c7e81bde..9dabd80c56 100644
--- a/doc/guides/rel_notes/release_26_11.rst
+++ b/doc/guides/rel_notes/release_26_11.rst@@ -55,6 +55,10 @@ New Features Also, make sure to start the actual text at the margin. ======================================================= +* **Added an example of tcpdump remote pcap daemon.** + + Added an example that implements rpcap to allow live capture in tcpdump. + Removed Items -------------
diff --git a/doc/guides/sample_app_ug/index.rst b/doc/guides/sample_app_ug/index.rst
index f12623bb66..61ed870318 100644
--- a/doc/guides/sample_app_ug/index.rst
+++ b/doc/guides/sample_app_ug/index.rst@@ -31,6 +31,7 @@ Sample Applications User Guides l3_forward_graph l3_forward_power_man link_status_intr + rpcapd server_node_efd service_cores multi_process
diff --git a/doc/guides/sample_app_ug/rpcapd.rst b/doc/guides/sample_app_ug/rpcapd.rst
new file mode 100644
index 0000000000..e899a4d991
--- /dev/null
+++ b/doc/guides/sample_app_ug/rpcapd.rst@@ -0,0 +1,206 @@ +.. SPDX-License-Identifier: BSD-3-Clause + Copyright(c) 2026 + +.. _rpcapd_app: + +dpdk-rpcapd Sample Application +============================== + +The ``dpdk-rpcapd`` sample application is a Data Plane Development Kit +(DPDK) implementation of the remote packet capture daemon protocol +(``rpcap``) used by libpcap. It runs as a DPDK secondary process and +allows libpcap-aware tools such as ``tcpdump`` and Wireshark to capture +packets from a DPDK primary process live, without writing to an +intermediate file. + +The ``dpdk-rpcapd`` tool implements a subset of the protocol spoken by +the libpcap project's ``rpcapd``. See +https://github.com/the-tcpdump-group/libpcap/tree/master/rpcapd for the +reference implementation. Clients connect to ``dpdk-rpcapd`` using a +``rpcap://`` URL, request the list of available interfaces (which are +the ports of the DPDK primary), open one, and stream packets from it. + +The intended workflow is one-step capture: start the primary, start +``dpdk-rpcapd``, and point a familiar tool at it. No intermediate files, +no separate post-processing step. + +.. warning:: + + ``dpdk-rpcapd`` listens on an unauthenticated, unencrypted TCP port + (default 2002, bound to ``127.0.0.1``). Any local user able to + reach the port can list DPDK ports and capture all traffic flowing + through them. This is a sample application intended for + development, debugging, and demonstration use only. **Do not run + ``dpdk-rpcapd`` on a production system.** + + The default bind address is ``127.0.0.1`` so the listener is not + reachable from other hosts. An operator may override this with + ``--bind <addr>`` but should expect that the resulting deployment + exposes captured traffic to anyone who can reach that address; do + not do this on an untrusted network. + + +.. note:: + + * The ``dpdk-rpcapd`` tool can only be used in conjunction with a + primary application that has the packet capture framework + initialized already. In DPDK, only ``dpdk-testpmd`` is modified to + initialize the packet capture framework; other applications must + be modified to call ``rte_pdump_init()`` if they are to be + capturable. + + * ``dpdk-rpcapd`` does not replace ``dpdk-dumpcap``. ``dpdk-dumpcap`` + produces pcapng files; ``dpdk-rpcapd`` produces a live rpcap + stream. The two tools serve different workflows and may be used + in parallel. + + * For Wireshark users specifically, the Wireshark ``extcap`` plugin + interface is the preferred live-capture path; see :doc:`extcap`. + ``extcap`` integrates directly with Wireshark and is simpler to + deploy. ``dpdk-rpcapd`` is intended for users who want to use + ``tcpdump`` or other rpcap-aware libpcap clients, where ``extcap`` + does not apply. + + +Running the Application +----------------------- + +The application has a small set of command-line options: + +* ``-p <port>``, ``--port <port>`` + + TCP port to listen on. Default is 2002, the IANA-assigned rpcap + port. + +* ``-b <addr>``, ``--bind <addr>`` + + IPv4 address to bind the listener to. Default is ``127.0.0.1`` + (loopback only). Setting any other address exposes captured + traffic to the network and should not be done on untrusted + networks. + +* ``-N <ring_size>`` + + Size of the per-session capture ring in packets. Default is 2048. + +* ``-h``, ``--help`` + + Print usage and exit. + +EAL options are supplied automatically; the application runs as a +secondary process and does not need EAL options on its command line for +typical use. + + +Client Setup +------------ + +Most Linux distributions ship libpcap built without ``rpcap`` support +because the libpcap project leaves ``--enable-remote`` off by default. +To use ``dpdk-rpcapd`` from ``tcpdump`` or Wireshark on Linux, libpcap +must be rebuilt with remote support enabled. Approximate steps: + +.. code-block:: console + + wget https://www.tcpdump.org/release/libpcap-1.10.6.tar.xz + tar xf libpcap-1.10.6.tar.xz + cd libpcap-1.10.6 + ./configure --enable-remote + make + sudo make install + sudo ldconfig + +To verify that the resulting library has rpcap support: + +.. code-block:: console + + nm -D /usr/local/lib/libpcap.so | grep ' T pcap_open$' + +The symbol ``pcap_open`` should be present. If not, the ``--enable-remote`` +flag did not take effect. + +``tcpdump`` rebuilt against this libpcap can be used as a client without +further changes. Wireshark on Windows and macOS ships with rpcap support +enabled by default. + + +Example +------- + +Start a primary application with the packet capture framework +initialized. ``dpdk-testpmd`` is the simplest: + +.. code-block:: console + + sudo ./<build_dir>/app/dpdk-testpmd -- --no-mlockall --vdev=net_tap0 + +In another window, start ``dpdk-rpcapd``: + +.. code-block:: console + + sudo ./<build_dir>/app/dpdk-rpcapd + RPCAPD: listening on TCP port 2002 + +In a third window, list available interfaces using a libpcap-based +``tcpdump`` rebuilt with remote support: + +.. code-block:: console + + sudo /usr/local/sbin/tcpdump --list-remote-interfaces=rpcap://localhost:2002/ + rpcap://localhost:2002/net_tap0 Network adapter 'DPDK port' on remote node localhost + +Capture live from a port: + +.. code-block:: console + + sudo /usr/local/sbin/tcpdump -i rpcap://localhost:2002/net_tap0 -nn -c 20 + +Or save to a file readable by any pcap consumer: + +.. code-block:: console + + sudo /usr/local/sbin/tcpdump -i rpcap://localhost:2002/net_tap0 -w /tmp/capture.pcap + + +Limitations +----------- + +The following limitations apply to this initial version of +``dpdk-rpcapd`` and are expected to be addressed in subsequent patches: + +* **Single client.** Only one client may be connected at a time. + Subsequent clients are queued by the listening socket but not + serviced until the first disconnects. Multi-client support + requires an event-driven main loop (planned). + +* **No BPF filter support.** ``UPDATEFILTER`` requests are + acknowledged and ignored. Capture-side filtering requires an + extension to ``rte_pdump`` to support filter updates on an active + callback. + +* **No authentication.** ``AUTH`` requests are acknowledged with an + empty reply (libpcap "version 0, null auth" semantics). This + sample application does not implement password authentication. + +* **TCP transport only; not for production use.** The rpcap protocol + over TCP is unauthenticated and unencrypted; any client that can + reach the listening port has full access to captured traffic. + Binding to ``127.0.0.1`` by default mitigates remote exposure but + does not address local users on a shared host. See the warning at + the top of this document. + +* **Microsecond timestamp resolution.** The rpcap protocol carries + timestamps at microsecond resolution. + + +See Also +-------- + +* :doc:`extcap` -- Wireshark ``extcap`` plugin for direct integration + with Wireshark, without going through the rpcap protocol. + +* :doc:`../tools/dumpcap` -- file-based capture writing pcapng + output. + +* The libpcap project's ``rpcapd`` reference implementation: + https://github.com/the-tcpdump-group/libpcap/tree/master/rpcapd
diff --git a/examples/meson.build b/examples/meson.build
index 25d9c88457..24b6184353 100644
--- a/examples/meson.build
+++ b/examples/meson.build@@ -45,6 +45,7 @@ all_examples = [ 'ptpclient', 'qos_meter', 'qos_sched', + 'rpcapd', 'rxtx_callbacks', 'server_node_efd/efd_node', 'server_node_efd/efd_server',
diff --git a/examples/rpcapd/main.c b/examples/rpcapd/main.c
new file mode 100644
index 0000000000..e16f5aa3e0
--- /dev/null
+++ b/examples/rpcapd/main.c@@ -0,0 +1,1014 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 + * + * Proof-of-concept DPDK rpcapd: the libpcap remote packet capture + * daemon, implemented on top of DPDK pdump. A libpcap client (e.g. + * Wireshark or tcpdump using "rpcap://host[:port]/portname") can + * connect, list DPDK ports, open one, and stream live packets from it. + * + * Based on the DPDK dumpcap application and on rpcapd from libpcap: + * https://github.com/the-tcpdump-group/libpcap/tree/master/rpcapd + * + * Only the bits of the RPCAP protocol that are needed for an + * unauthenticated, passive-mode capture session are implemented. + * Configuration files, BPF filters, active mode, statistics, sampling, + * IPv6 and concurrent clients are intentionally omitted to keep the + * example small. + */ + +#include <arpa/inet.h> +#include <errno.h> +#include <getopt.h> +#include <netinet/in.h> +#include <netdb.h> +#include <poll.h> +#include <signal.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <sys/time.h> +#include <sys/types.h> +#include <sys/uio.h> +#include <unistd.h> + +#include <rte_alarm.h> +#include <rte_common.h> +#include <rte_debug.h> +#include <rte_eal.h> +#include <rte_errno.h> +#include <rte_ether.h> +#include <rte_ethdev.h> +#include <rte_lcore.h> +#include <rte_log.h> +#include <rte_mbuf.h> +#include <rte_mempool.h> +#include <rte_pdump.h> +#include <rte_stdatomic.h> +#include <rte_ring.h> +#include <rte_version.h> + +#include "rpcap-protocol.h" + +#define BURST_SIZE 32 +#define MBUF_CACHE_SIZE 32 +#define DEFAULT_RING_SIZE 2048 +#define DEFAULT_SNAPLEN RTE_MBUF_DEFAULT_BUF_SIZE +#define PRIMARY_MONITOR_INTERVAL_US (500 * 1000) + +/* Logging. Use --log-level=rpcapd:debug to enable debug output. */ +RTE_LOG_REGISTER(rpcapd_logtype, rpcapd, INFO); +#define RTE_LOGTYPE_RPCAPD rpcapd_logtype + +/* Per-client capture session state. */ +struct session { + int ctrl_fd; + int data_fd; + uint16_t port; /* DPDK ethdev port being captured */ + char name[RTE_ETH_NAME_MAX_LEN]; + uint32_t snaplen; + uint32_t npkt; /* packet sequence for rpcap_pkthdr */ + bool capture_on; + struct rte_ring *ring; + struct rte_mempool *mp; +}; + +/* Command-line options */ +static uint16_t listen_port = RPCAP_DEFAULT_NETPORT; +static uint32_t ring_size = DEFAULT_RING_SIZE; +static const char *lcore_arg; +static const char *file_prefix; +static const char *bind_arg; /* -b argument, resolved after option parsing */ +static const char *debug_file; /* --debug-file argument */ +static bool ipv4_only; /* -4: restrict to IPv4 */ +static bool debug_log; /* -D: enable RPCAPD debug logging */ + +/* Bind address for the listener and the per-session data port. + * Defaults to IPv4 loopback because the rpcap protocol is insecure. + * It exposes captured traffic to anyone who can reach the port. + * An operator who knowingly accepts that risk can override with + * --bind <addr>. IPv4 and IPv6 numeric addresses are both accepted. + */ +static struct sockaddr_storage listen_addr; +static socklen_t listen_addrlen; + +static void stop_capture(struct session *s); + +static void +set_sockaddr_port(struct sockaddr_storage *ss, uint16_t port) +{ + if (ss->ss_family == AF_INET6) + ((struct sockaddr_in6 *)ss)->sin6_port = htons(port); + else + ((struct sockaddr_in *)ss)->sin_port = htons(port); +} + +static uint16_t +get_sockaddr_port(const struct sockaddr_storage *ss) +{ + if (ss->ss_family == AF_INET6) + return ntohs(((const struct sockaddr_in6 *)ss)->sin6_port); + return ntohs(((const struct sockaddr_in *)ss)->sin_port); +} + +static bool +is_loopback(const struct sockaddr_storage *ss) +{ + if (ss->ss_family == AF_INET) { + const struct sockaddr_in *sin = (const void *)ss; + + return (ntohl(sin->sin_addr.s_addr) >> 24) == 127; + } + if (ss->ss_family == AF_INET6) { + const struct sockaddr_in6 *sin6 = (const void *)ss; + + return IN6_IS_ADDR_LOOPBACK(&sin6->sin6_addr); + } + return false; +} + +static void +parse_bind_addr(const char *str, int family) +{ + struct addrinfo hints = { + .ai_family = family, + .ai_socktype = SOCK_STREAM, + .ai_flags = AI_NUMERICHOST | AI_PASSIVE, + }; + struct addrinfo *res; + int rc; + + rc = getaddrinfo(str, NULL, &hints, &res); + if (rc != 0) + rte_exit(EXIT_FAILURE, "Invalid bind address '%s': %s\n", + str, gai_strerror(rc)); + memcpy(&listen_addr, res->ai_addr, res->ai_addrlen); + listen_addrlen = res->ai_addrlen; + freeaddrinfo(res); +} + +static RTE_ATOMIC(bool) quit_signal; + +static void +signal_handler(int sig __rte_unused) +{ + rte_atomic_store_explicit(&quit_signal, true, rte_memory_order_relaxed); +} + +/* Read exactly len bytes; return 0 on success, -1 on error or EOF. */ +static int +recv_full(int fd, void *buf, size_t len) +{ + uint8_t *p = buf; + + while (len > 0) { + ssize_t n = recv(fd, p, len, 0); + if (rte_atomic_load_explicit(&quit_signal, rte_memory_order_relaxed)) + return -1; + + if (n < 0 && errno == EINTR) + continue; + + if (n <= 0) + return -1; + + p += n; + len -= n; + } + return 0; +} + +static int +send_iov_full(int fd, struct iovec *iov, int iovcnt, int flags) +{ + struct msghdr msg = { + .msg_iov = iov, + .msg_iovlen = iovcnt, + }; + + while (sendmsg(fd, &msg, flags | MSG_NOSIGNAL) < 0) { + if (errno != EINTR) + return -1; + } + return 0; +} + +static int +rpcap_send_msg(int fd, uint8_t type, uint16_t value, const void *payload, uint32_t plen) +{ + struct rpcap_header hdr = { + .ver = RPCAP_VERSION, + .type = type, + .value = htons(value), + .plen = htonl(plen), + }; + struct iovec iov[2] = { + { .iov_base = &hdr, .iov_len = sizeof(hdr) }, + { .iov_base = (void *)(uintptr_t)payload, .iov_len = plen }, + }; + + return send_iov_full(fd, iov, plen > 0 ? 2 : 1, 0); +} + +static int +rpcap_send_error(int fd, uint16_t errcode, const char *msg) +{ + RTE_LOG(WARNING, RPCAPD, "sending error to client: %s\n", msg); + return rpcap_send_msg(fd, RPCAP_MSG_ERROR, errcode, msg, strlen(msg)); +} + +static int +rpcap_recv_header(int fd, struct rpcap_header *hdr) +{ + if (recv_full(fd, hdr, sizeof(*hdr)) < 0) + return -1; + hdr->value = ntohs(hdr->value); + hdr->plen = ntohl(hdr->plen); + return 0; +} + +/* Throw away plen bytes of payload we don't care about. */ +static int +rpcap_discard(int fd, uint32_t plen) +{ + uint8_t buf[256]; + + while (plen > 0) { + size_t chunk = plen > sizeof(buf) ? sizeof(buf) : plen; + + if (recv_full(fd, buf, chunk) < 0) + return -1; + plen -= chunk; + } + return 0; +} + +/* Build and send the list of available DPDK ports. */ +static int +handle_findallif(int fd) +{ + uint8_t *buf = NULL; + size_t buflen = 0; + uint16_t nif = 0; + uint16_t p; + int rc; + + RTE_ETH_FOREACH_DEV(p) { + static const char desc[] = "DPDK port"; + char name[RTE_ETH_NAME_MAX_LEN]; + size_t namelen, desclen, entry; + uint8_t *nb; + + if (rte_eth_dev_get_name_by_port(p, name) < 0) { + RTE_LOG(INFO, RPCAPD, "can not find name for port %u\n", p); + continue; + } + + RTE_LOG(INFO, RPCAPD, "findallif: port %u -> '%s'\n", p, name); + namelen = strlen(name); + desclen = strlen(desc); + entry = sizeof(struct rpcap_findalldevs_if) + namelen + desclen; + + nb = realloc(buf, buflen + entry); + if (nb == NULL) { + RTE_LOG(ERR, RPCAPD, "out of memory in findallif\n"); + free(buf); + return rpcap_send_error(fd, 0, "out of memory"); + } + buf = nb; + + struct rpcap_findalldevs_if iface = { + .namelen = htons(namelen), + .desclen = htons(desclen), + .flags = htonl(PCAP_IF_UP | PCAP_IF_RUNNING), + }; + memcpy(buf + buflen, &iface, sizeof(iface)); + memcpy(buf + buflen + sizeof(iface), name, namelen); + memcpy(buf + buflen + sizeof(iface) + namelen, desc, desclen); + buflen += entry; + nif++; + } + + RTE_LOG(INFO, RPCAPD, "findallif: %u interface(s)\n", nif); + rc = rpcap_send_msg(fd, RPCAP_MSG_FINDALLIF_REPLY, nif, buf, buflen); + free(buf); + return rc; +} + +/* OPEN_REQ: payload is the interface name (no NUL). */ +static int +handle_open(int fd, uint32_t plen, struct session *s) +{ + struct rpcap_openreply reply = { + .linktype = htonl(DLT_EN10MB), + }; + uint16_t port; + + if (s->capture_on) + stop_capture(s); + + if (plen >= sizeof(s->name)) { + rpcap_discard(fd, plen); + return rpcap_send_error(fd, 0, "interface name too long"); + } + if (recv_full(fd, s->name, plen) < 0) + return -1; + s->name[plen] = '\0'; + + if (rte_eth_dev_get_port_by_name(s->name, &port) < 0) { + RTE_LOG(WARNING, RPCAPD, "open: no such port '%s'\n", s->name); + return rpcap_send_error(fd, 0, "unknown interface"); + } + s->port = port; + + RTE_LOG(DEBUG, RPCAPD, "open: '%s' -> dpdk port %u\n", s->name, port); + return rpcap_send_msg(fd, RPCAP_MSG_OPEN_REPLY, 0, &reply, sizeof(reply)); +} + +/* Open an ephemeral TCP listening socket; return fd, set *port_out. */ +static int +open_data_listener(uint16_t *port_out) +{ + struct sockaddr_storage addr = listen_addr; + socklen_t alen; + int fd; + + set_sockaddr_port(&addr, 0); + + fd = socket(addr.ss_family, SOCK_STREAM, 0); + if (fd < 0) { + RTE_LOG(ERR, RPCAPD, "data socket: %s\n", strerror(errno)); + return -1; + } + + alen = listen_addrlen; + if (bind(fd, (struct sockaddr *)&addr, alen) < 0 || + listen(fd, 1) < 0 || + getsockname(fd, (struct sockaddr *)&addr, &alen) < 0) { + RTE_LOG(ERR, RPCAPD, "data port bind/listen: %s\n", strerror(errno)); + close(fd); + return -1; + } + *port_out = get_sockaddr_port(&addr); + return fd; +} + +static struct rte_ring * +create_capture_ring(uint16_t port) +{ + char name[RTE_RING_NAMESIZE]; + + snprintf(name, sizeof(name), "rpcapd_r_%u_%d", port, getpid()); + return rte_ring_create(name, ring_size, rte_socket_id(), 0); +} + +static struct rte_mempool * +create_capture_mempool(uint16_t port, uint32_t snaplen) +{ + char name[RTE_MEMPOOL_NAMESIZE]; + uint32_t mbuf_size = RTE_PKTMBUF_HEADROOM + snaplen; + + snprintf(name, sizeof(name), "rpcapd_p_%u_%d", port, getpid()); + return rte_pktmbuf_pool_create(name, ring_size * 2, MBUF_CACHE_SIZE, 0, + mbuf_size, rte_socket_id()); +} + +/* Tear down anything that handle_startcap brought up. Safe to call + * after partial setup as well as after a successful capture. + */ +static void +stop_capture(struct session *s) +{ + struct rte_mbuf *pkts[BURST_SIZE]; + unsigned int n; + + if (s->capture_on) { + rte_pdump_disable(s->port, RTE_PDUMP_ALL_QUEUES, RTE_PDUMP_FLAG_RXTX); + RTE_LOG(INFO, RPCAPD, "capture stopped on %s (%u packets)\n", + s->name, s->npkt); + } + s->capture_on = false; + + if (s->ring != NULL) { + while ((n = rte_ring_sc_dequeue_burst(s->ring, (void **)pkts, + BURST_SIZE, NULL)) > 0) + rte_pktmbuf_free_bulk(pkts, n); + rte_ring_free(s->ring); + s->ring = NULL; + } + if (s->mp != NULL) { + rte_mempool_free(s->mp); + s->mp = NULL; + } + if (s->data_fd >= 0) { + close(s->data_fd); + s->data_fd = -1; + } +} + +/* + * STARTCAP_REQ: open the data connection and arm the pdump callback. + * We use passive mode with the server-allocated data port: + * - the server picks an ephemeral port and listens on it + * - the server returns that port in startcapreply.portdata + * - the client connects back to that port for the packet stream + */ +static int +handle_startcap(int fd, uint32_t plen, struct session *s) +{ + struct rpcap_startcapreq req; + uint16_t data_port; + int data_listen; + int data_fd; + + if (s->capture_on) + stop_capture(s); + + if (plen < sizeof(req)) { + rpcap_discard(fd, plen); + return rpcap_send_error(fd, 0, "short startcap request"); + } + if (recv_full(fd, &req, sizeof(req)) < 0) + return -1; + /* Skip any embedded BPF filter; not supported here. */ + if (rpcap_discard(fd, plen - sizeof(req)) < 0) + return -1; + + s->snaplen = ntohl(req.snaplen); + if (s->snaplen == 0 || s->snaplen > DEFAULT_SNAPLEN) + s->snaplen = DEFAULT_SNAPLEN; + + s->ring = create_capture_ring(s->port); + s->mp = create_capture_mempool(s->port, s->snaplen); + if (s->ring == NULL || s->mp == NULL) { + RTE_LOG(ERR, RPCAPD, "ring/mempool alloc failed: %s\n", + rte_strerror(rte_errno)); + stop_capture(s); + return rpcap_send_error(fd, 0, "DPDK alloc failed"); + } + + data_listen = open_data_listener(&data_port); + if (data_listen < 0) { + stop_capture(s); + return rpcap_send_error(fd, 0, "data port setup failed"); + } + + struct rpcap_startcapreply reply = { + .bufsize = htonl(s->snaplen * BURST_SIZE), + .portdata = htons(data_port), + }; + if (rpcap_send_msg(fd, RPCAP_MSG_STARTCAP_REPLY, 0, &reply, sizeof(reply)) < 0) { + close(data_listen); + stop_capture(s); + return -1; + } + + RTE_LOG(INFO, RPCAPD, "awaiting connection\n"); + + data_fd = accept(data_listen, NULL, NULL); + close(data_listen); + if (data_fd < 0) { + RTE_LOG(ERR, RPCAPD, "accept on data port: %s\n", strerror(errno)); + stop_capture(s); + return -1; + } + + s->data_fd = data_fd; + + if (rte_pdump_enable(s->port, RTE_PDUMP_ALL_QUEUES, RTE_PDUMP_FLAG_RXTX, + s->ring, s->mp, NULL) < 0) { + RTE_LOG(ERR, RPCAPD, "rte_pdump_enable port %u failed: %s\n", + s->port, rte_strerror(rte_errno)); + stop_capture(s); + return -1; + } + s->capture_on = true; + s->npkt = 0; + + RTE_LOG(INFO, RPCAPD, + "capture started on %s (snaplen %u, data port %u)\n", + s->name, s->snaplen, data_port); + return 0; +} + +/* + * Pull a burst from the ring, frame each packet into an RPCAP_MSG_PACKET + * message, and send it on the data connection. MSG_MORE on all but the + * last send tells the kernel to coalesce the burst into full segments. + */ +static int +process_ring(struct session *s) +{ + struct rte_mbuf *pkts[BURST_SIZE]; + unsigned int i, n; + + n = rte_ring_sc_dequeue_burst(s->ring, (void **)pkts, BURST_SIZE, NULL); + if (n == 0) + return 0; + + for (i = 0; i < n; i++) { + struct rte_mbuf *m = pkts[i]; + uint8_t buf[RTE_ETHER_MAX_JUMBO_FRAME_LEN]; + uint32_t pktlen = rte_pktmbuf_pkt_len(m); + uint32_t caplen = pktlen < s->snaplen ? pktlen : s->snaplen; + const void *data; + struct timeval tv; + + s->npkt++; + + struct rpcap_header hdr = { + .ver = RPCAP_VERSION, + .type = RPCAP_MSG_PACKET, + .plen = htonl(sizeof(struct rpcap_pkthdr) + caplen), + }; + + gettimeofday(&tv, NULL); + + struct rpcap_pkthdr pkthdr = { + .timestamp_sec = htonl((uint32_t)tv.tv_sec), + .timestamp_usec = htonl((uint32_t)tv.tv_usec), + .caplen = htonl(caplen), + .len = htonl(pktlen), + .npkt = htonl(s->npkt), + }; + + data = rte_pktmbuf_read(m, 0, caplen, buf); + + struct iovec iov[3] = { + { .iov_base = &hdr, .iov_len = sizeof(hdr) }, + { .iov_base = &pkthdr, .iov_len = sizeof(pkthdr) }, + { .iov_base = (void *)(uintptr_t)data, .iov_len = caplen }, + }; + if (send_iov_full(s->data_fd, iov, 3, + i + 1 < n ? MSG_MORE : 0) < 0) { + RTE_LOG(NOTICE, RPCAPD, "data connection closed: %s\n", strerror(errno)); + goto error; + } + rte_pktmbuf_free(m); + } + + return (int)n; + +error: + rte_pktmbuf_free_bulk(pkts + i, n - i); + return -1; +} + +/* + * Stay in the capture loop until either: + * - a control message arrives (typically ENDCAP), + * - the data connection breaks, or + * - a quit signal is delivered. + */ +static int +capture_loop(int ctrl_fd, struct session *s) +{ + struct pollfd pfd = { .fd = ctrl_fd, .events = POLLIN }; + unsigned int idle = 0; + + while (!rte_atomic_load_explicit(&quit_signal, rte_memory_order_relaxed)) { + int n; + + if (poll(&pfd, 1, 0) > 0 && (pfd.revents & POLLIN)) + return 0; + + n = process_ring(s); + if (n < 0) + return -1; + if (n == 0) { + if (idle++ < 1000) + continue; + usleep(1000); + idle = 0; + } else { + idle = 0; + } + } + return 0; +} + +static int +handle_endcap(int fd, uint32_t plen, struct session *s) +{ + if (rpcap_discard(fd, plen) < 0) + return -1; + stop_capture(s); + return rpcap_send_msg(fd, RPCAP_MSG_ENDCAP_REPLY, 0, NULL, 0); +} + +static int +handle_stats(int fd, uint32_t plen, const struct session *s) +{ + struct rte_eth_stats es = { 0 }; + + if (rpcap_discard(fd, plen) < 0) + return -1; + + if (s->capture_on) + rte_eth_stats_get(s->port, &es); + + struct rpcap_stats reply = { + .ifrecv = htonl((uint32_t)es.ipackets), + .ifdrop = htonl((uint32_t)es.ierrors), + .krnldrop = 0, + .svrcapt = htonl(s->npkt), + }; + return rpcap_send_msg(fd, RPCAP_MSG_STATS_REPLY, 0, &reply, sizeof(reply)); +} + +/* Service a single client until it disconnects. */ +static void +handle_client(int ctrl_fd) +{ + struct sockaddr_storage peer; + socklen_t plen = sizeof(peer); + char host[NI_MAXHOST] = "?"; + struct session s = { .ctrl_fd = ctrl_fd, .data_fd = -1 }; + + if (getpeername(ctrl_fd, (struct sockaddr *)&peer, &plen) == 0) + getnameinfo((struct sockaddr *)&peer, plen, + host, sizeof(host), NULL, 0, NI_NUMERICHOST); + RTE_LOG(INFO, RPCAPD, "client %s connected\n", host); + + while (!rte_atomic_load_explicit(&quit_signal, rte_memory_order_relaxed)) { + struct rpcap_header hdr; + + if (rpcap_recv_header(ctrl_fd, &hdr) < 0) + break; + + switch (hdr.type) { + case RPCAP_MSG_AUTH_REQ: + /* No auth: discard credentials, ack with empty reply. + * libpcap treats a zero-length AUTH_REPLY as "version + * 0 only, same byte order". + */ + if (rpcap_discard(ctrl_fd, hdr.plen) < 0 || + rpcap_send_msg(ctrl_fd, RPCAP_MSG_AUTH_REPLY, 0, NULL, 0) < 0) + goto done; + break; + case RPCAP_MSG_FINDALLIF_REQ: + if (rpcap_discard(ctrl_fd, hdr.plen) < 0 || handle_findallif(ctrl_fd) < 0) + goto done; + break; + case RPCAP_MSG_OPEN_REQ: + if (handle_open(ctrl_fd, hdr.plen, &s) < 0) + goto done; + break; + case RPCAP_MSG_STARTCAP_REQ: + if (handle_startcap(ctrl_fd, hdr.plen, &s) < 0) + goto done; + if (capture_loop(ctrl_fd, &s) < 0) + goto done; + break; + case RPCAP_MSG_UPDATEFILTER_REQ: + /* Filters not implemented; ack and ignore. */ + if (rpcap_discard(ctrl_fd, hdr.plen) < 0 || + rpcap_send_msg(ctrl_fd, RPCAP_MSG_UPDATEFILTER_REPLY, + 0, NULL, 0) < 0) + goto done; + break; + case RPCAP_MSG_ENDCAP_REQ: + if (handle_endcap(ctrl_fd, hdr.plen, &s) < 0) + goto done; + break; + case RPCAP_MSG_STATS_REQ: + if (handle_stats(ctrl_fd, hdr.plen, &s) < 0) + goto done; + break; + case RPCAP_MSG_CLOSE: + rpcap_discard(ctrl_fd, hdr.plen); + goto done; + default: + RTE_LOG(WARNING, RPCAPD, "unsupported request type 0x%02x\n", hdr.type); + rpcap_discard(ctrl_fd, hdr.plen); + rpcap_send_error(ctrl_fd, 0, "unsupported request"); + break; + } + } +done: + stop_capture(&s); + close(ctrl_fd); + RTE_LOG(INFO, RPCAPD, "client %s disconnected\n", host); +} + +static int +open_listen_socket(uint16_t port) +{ + struct sockaddr_storage addr = listen_addr; + char host[NI_MAXHOST]; + int fd, one = 1; + + set_sockaddr_port(&addr, port); + + fd = socket(addr.ss_family, SOCK_STREAM, 0); + if (fd < 0) + rte_exit(EXIT_FAILURE, "socket: %s\n", strerror(errno)); + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + + if (bind(fd, (struct sockaddr *)&addr, listen_addrlen) < 0) + rte_exit(EXIT_FAILURE, "bind(%u): %s\n", port, strerror(errno)); + + int err = getnameinfo((struct sockaddr *)&listen_addr, listen_addrlen, + host, sizeof(host), NULL, 0, NI_NUMERICHOST); + if (err != 0) + rte_exit(EXIT_FAILURE, "Listen address lookup failed: %s\n", + gai_strerror(err)); + + RTE_LOG(INFO, RPCAPD, "listening on %s port %u\n", host, listen_port); + + if (!is_loopback(&listen_addr)) + RTE_LOG(WARNING, RPCAPD, + "bound to non-loopback address %s; " + "rpcap is unauthenticated and unencrypted, " + "captured traffic is exposed to the network\n", + host); + + if (listen(fd, 1) < 0) + rte_exit(EXIT_FAILURE, "listen: %s\n", strerror(errno)); + + return fd; +} + +static void +usage(FILE *f, const char *progname) +{ + fprintf(f, "Usage: %s [options]\n", progname); + fprintf(f, + " -p, --port <port> listen port (default %u)\n" + " -b, --bind <addr> bind address (default 127.0.0.1)\n" + " -4 use only IPv4 (reject IPv6 bind addresses)\n" + " -N <ring size> ring size in packets (default %u)\n" + " -D, --debug enable rpcapd debug log messages\n" + " --debug-file <f> redirect log output to file <f> (append mode)\n" + " --version print version and exit\n" + " -h, --help print this help and exit\n" + " --lcore=<core> CPU core to run on (default: any)\n" + " --file-prefix=<p> prefix to use for multi-process\n" + "\n" + "WARNING: rpcap is unauthenticated and unencrypted. Binding to\n" + "any non-loopback address exposes captured traffic to the\n" + "network. Sample application; not for production use.\n", + RPCAP_DEFAULT_NETPORT, DEFAULT_RING_SIZE); +} + +static void +print_version(void) +{ + printf("rpcapd, a remote packet capture daemon (DPDK pdump backend)\n" + "Built against %s\n", rte_version()); +} + +static void +parse_opts(int argc, char **argv) +{ + enum { + OPT_LONG_ONLY = 0x100, + OPT_DEBUG_FILE, + OPT_VERSION, + }; + static const struct option long_options[] = { + { "port", required_argument, NULL, 'p' }, + { "bind", required_argument, NULL, 'b' }, + { "debug", no_argument, NULL, 'D' }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, OPT_VERSION }, + { "debug-file", required_argument, NULL, OPT_DEBUG_FILE }, + { "file-prefix", required_argument, NULL, 0 }, + { "lcore", required_argument, NULL, 0 }, + { NULL, 0, NULL, 0 }, + }; + int option_index, c; + + while ((c = getopt_long(argc, argv, "hD4p:b:N:", + long_options, &option_index)) != -1) { + switch (c) { + case 'p': { + unsigned long u = strtoul(optarg, NULL, 0); + + if (u == 0 || u > UINT16_MAX) + rte_exit(EXIT_FAILURE, "Invalid port: %s\n", optarg); + listen_port = (uint16_t)u; + break; + } + case 'b': + bind_arg = optarg; + break; + case '4': + ipv4_only = true; + break; + case 'N': + ring_size = strtoul(optarg, NULL, 0); + if (ring_size < 64) + rte_exit(EXIT_FAILURE, "Ring size too small\n"); + break; + case 'D': + debug_log = true; + break; + case 'h': + usage(stdout, argv[0]); + exit(0); + case OPT_VERSION: + print_version(); + exit(0); + case OPT_DEBUG_FILE: + debug_file = optarg; + break; + case 0: { + const char *longopt = long_options[option_index].name; + + if (!strcmp(longopt, "lcore")) { + lcore_arg = optarg; + break; + } else if (!strcmp(longopt, "file-prefix")) { + file_prefix = optarg; + break; + } + } + /* fallthrough */ + default: + usage(stderr, argv[0]); + exit(EXIT_FAILURE); + } + } + + /* Resolve the bind address now that -4 has been seen. */ + parse_bind_addr(bind_arg ? bind_arg : "127.0.0.1", + ipv4_only ? AF_INET : AF_UNSPEC); +} + +/* + * Periodic check that the DPDK primary process is still alive. + * If it dies our shared-memory state (rings, mempools, pdump) becomes + * unsafe to touch, so we set quit_signal and let the main loop tear + * down cleanly on its next iteration. The callback runs on the EAL + * interrupt thread; quit_signal is atomic so the read in the main + * loop is well-defined. + */ +static void +monitor_primary(void *arg __rte_unused) +{ + if (rte_atomic_load_explicit(&quit_signal, rte_memory_order_relaxed)) + return; + + if (rte_eal_primary_proc_alive(NULL)) { + rte_eal_alarm_set(PRIMARY_MONITOR_INTERVAL_US, monitor_primary, NULL); + return; + } + + RTE_LOG(NOTICE, RPCAPD, "primary process exited, shutting down\n"); + rte_atomic_store_explicit(&quit_signal, true, rte_memory_order_relaxed); +} + +static void +enable_primary_monitor(void) +{ + if (rte_eal_alarm_set(PRIMARY_MONITOR_INTERVAL_US, monitor_primary, NULL) < 0) + RTE_LOG(WARNING, RPCAPD, "failed to install primary process monitor\n"); +} + +static void +disable_primary_monitor(void) +{ + rte_eal_alarm_cancel(monitor_primary, NULL); +} + +/* + * Bring up EAL as a secondary process so that pdump can attach to a + * running primary DPDK application. Mirrors dumpcap's approach: the + * RPCAP user sees a small set of options (port, ring size) rather + * than the full DPDK EAL command line. + */ +static int +dpdk_init(void) +{ + static const char * const args[] = { + "rpcapd", + "--proc-type", "secondary", + "--log-level", "info", /* EAL stays quiet */ + }; + int eal_argc = RTE_DIM(args); + rte_cpuset_t cpuset = { }; + char **eal_argv; + unsigned int i; + + if (file_prefix != NULL) + eal_argc += 2; + + if (lcore_arg != NULL) + eal_argc += 2; + + eal_argv = calloc(eal_argc + 1, sizeof(char *)); + if (eal_argv == NULL) + return -1; + + for (i = 0; i < RTE_DIM(args); i++) { + eal_argv[i] = strdup(args[i]); + if (eal_argv[i] == NULL) + return -1; + } + + if (file_prefix != NULL && *file_prefix != '\0') { + eal_argv[i++] = strdup("--file-prefix"); + eal_argv[i++] = strdup(file_prefix); + if (eal_argv[i - 1] == NULL || eal_argv[i - 2] == NULL) + return -1; + } + + if (lcore_arg != NULL) { + eal_argv[i++] = strdup("--lcores"); + eal_argv[i++] = strdup(lcore_arg); + if (eal_argv[i - 1] == NULL || eal_argv[i - 2] == NULL) + return -1; + } + eal_argc = i; + + /* + * Need to get the original cpuset, before EAL init changes + * the affinity of this thread (main lcore). + */ + if (lcore_arg == NULL && + rte_thread_get_affinity_by_id(rte_thread_self(), &cpuset) != 0) + rte_panic("rte_thread_getaffinity failed\n"); + + if (rte_eal_init(eal_argc, eal_argv) < 0) + rte_exit(EXIT_FAILURE, "EAL init failed: is the primary process running?\n"); + + /* + * If no lcore argument was specified, + * then run this program as a normal process + * which can be scheduled on any non-isolated CPU. + */ + if (lcore_arg == NULL && + rte_thread_set_affinity_by_id(rte_thread_self(), &cpuset) != 0) + RTE_LOG(INFO, RPCAPD, + "Can not restore original CPU affinity\n"); + + if (rte_pdump_init() < 0) + rte_exit(EXIT_FAILURE, "rte_pdump_init failed\n"); + + return 0; +} + +int +main(int argc, char **argv) +{ + struct sigaction action = { + .sa_handler = signal_handler, + }; + int srv_fd; + + parse_opts(argc, argv); + + /* + * Redirect log output before EAL init so EAL's own messages are + * captured too. The FILE handle is intentionally never closed: + * the kernel reclaims it at process exit. + */ + if (debug_file != NULL) { + FILE *fp = fopen(debug_file, "a"); + + if (fp == NULL) + rte_exit(EXIT_FAILURE, "Cannot open debug file '%s': %s\n", + debug_file, strerror(errno)); + setvbuf(fp, NULL, _IOLBF, 0); + rte_openlog_stream(fp); + } + + if (dpdk_init() < 0) + rte_exit(EXIT_FAILURE, "EAL init failure\n"); + + if (debug_log) + rte_log_set_level(rpcapd_logtype, RTE_LOG_DEBUG); + + if (rte_eth_dev_count_avail() == 0) + rte_exit(EXIT_FAILURE, "No Ethernet ports found\n"); + + sigaction(SIGTERM, &action, NULL); + sigaction(SIGINT, &action, NULL); + + /* If peer closes, this detected in next recv() */ + signal(SIGPIPE, SIG_IGN); + + srv_fd = open_listen_socket(listen_port); + + enable_primary_monitor(); + + while (!rte_atomic_load_explicit(&quit_signal, rte_memory_order_relaxed)) { + int cfd = accept(srv_fd, NULL, NULL); + if (cfd < 0) { + if (errno == EINTR) + continue; + RTE_LOG(ERR, RPCAPD, "accept: %s\n", strerror(errno)); + break; + } + handle_client(cfd); + } + + disable_primary_monitor(); + RTE_LOG(INFO, RPCAPD, "shutting down\n"); + close(srv_fd); + rte_pdump_uninit(); + return rte_eal_cleanup() ? EXIT_FAILURE : 0; +}
diff --git a/examples/rpcapd/meson.build b/examples/rpcapd/meson.build
new file mode 100644
index 0000000000..5eb1a8487c
--- /dev/null
+++ b/examples/rpcapd/meson.build@@ -0,0 +1,11 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright(c) 2026 + +if is_windows + build = false + reason = 'not supported on Windows' + subdir_done() +endif + +sources = files('main.c') +deps += ['ethdev', 'pdump']
diff --git a/examples/rpcapd/rpcap-protocol.h b/examples/rpcapd/rpcap-protocol.h
new file mode 100644
index 0000000000..46d87928aa
--- /dev/null
+++ b/examples/rpcapd/rpcap-protocol.h@@ -0,0 +1,96 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 + * + * On-the-wire RPCAP protocol definitions, transcribed from libpcap's + * rpcap-protocol.h. See: + * https://github.com/the-tcpdump-group/libpcap/blob/master/rpcap-protocol.h + * + * Only the subset needed by the DPDK rpcd POC is included here. All + * multi-byte fields in the structures below are big-endian on the wire. + */ + +#ifndef _RPCAP_PROTOCOL_H_ +#define _RPCAP_PROTOCOL_H_ + +#include <stdint.h> + +#define RPCAP_VERSION 0 +#define RPCAP_DEFAULT_NETPORT 2002 + +/* Message types */ +#define RPCAP_MSG_ERROR 0x01 +#define RPCAP_MSG_FINDALLIF_REQ 0x02 +#define RPCAP_MSG_OPEN_REQ 0x03 +#define RPCAP_MSG_STARTCAP_REQ 0x04 +#define RPCAP_MSG_UPDATEFILTER_REQ 0x05 +#define RPCAP_MSG_CLOSE 0x06 +#define RPCAP_MSG_PACKET 0x07 +#define RPCAP_MSG_AUTH_REQ 0x08 +#define RPCAP_MSG_STATS_REQ 0x09 +#define RPCAP_MSG_ENDCAP_REQ 0x0a +#define RPCAP_MSG_IS_REPLY 0x80 + +#define RPCAP_MSG_FINDALLIF_REPLY (RPCAP_MSG_FINDALLIF_REQ | RPCAP_MSG_IS_REPLY) +#define RPCAP_MSG_OPEN_REPLY (RPCAP_MSG_OPEN_REQ | RPCAP_MSG_IS_REPLY) +#define RPCAP_MSG_STARTCAP_REPLY (RPCAP_MSG_STARTCAP_REQ | RPCAP_MSG_IS_REPLY) +#define RPCAP_MSG_UPDATEFILTER_REPLY (RPCAP_MSG_UPDATEFILTER_REQ | RPCAP_MSG_IS_REPLY) +#define RPCAP_MSG_AUTH_REPLY (RPCAP_MSG_AUTH_REQ | RPCAP_MSG_IS_REPLY) +#define RPCAP_MSG_ENDCAP_REPLY (RPCAP_MSG_ENDCAP_REQ | RPCAP_MSG_IS_REPLY) +#define RPCAP_MSG_STATS_REPLY (RPCAP_MSG_STATS_REQ | RPCAP_MSG_IS_REPLY) + +/* Subset of pcap interface flags (pcap.h) */ +#define PCAP_IF_UP 0x00000002 +#define PCAP_IF_RUNNING 0x00000004 + +/* DLT_EN10MB - ethernet, the only link type we report */ +#define DLT_EN10MB 1 + +struct rpcap_header { + uint8_t ver; + uint8_t type; + uint16_t value; + uint32_t plen; +}; + +struct rpcap_findalldevs_if { + uint16_t namelen; + uint16_t desclen; + uint32_t flags; + uint16_t naddr; + uint16_t dummy; +}; + +struct rpcap_openreply { + int32_t linktype; + int32_t tzoff; +}; + +struct rpcap_startcapreq { + uint32_t snaplen; + uint32_t read_timeout; + uint16_t flags; + uint16_t portdata; +}; + +struct rpcap_startcapreply { + int32_t bufsize; + uint16_t portdata; + uint16_t dummy; +}; + +struct rpcap_stats { + uint32_t ifrecv; + uint32_t ifdrop; + uint32_t krnldrop; + uint32_t svrcapt; +}; + +struct rpcap_pkthdr { + uint32_t timestamp_sec; + uint32_t timestamp_usec; + uint32_t caplen; + uint32_t len; + uint32_t npkt; +}; + +#endif /* _RPCAP_PROTOCOL_H_ */
--
2.53.0