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/rpcapd.rst for more info Signed-off-by: Stephen Hemminger <[email protected]> --- v2 - Build on Linux only - add filter support - fix lots of AI review feedback MAINTAINERS | 2 + doc/guides/rel_notes/release_26_11.rst | 4 + doc/guides/sample_app_ug/index.rst | 1 + doc/guides/sample_app_ug/rpcapd.rst | 216 ++++ examples/meson.build | 1 + examples/rpcapd/main.c | 1430 ++++++++++++++++++++++++ examples/rpcapd/meson.build | 19 + examples/rpcapd/rpcap-protocol.h | 127 +++ 8 files changed, 1800 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/MAINTAINERS b/MAINTAINERS index 8c50c52933..1eca09646d 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1724,6 +1724,8 @@ F: app/pdump/ F: doc/guides/tools/pdump.rst F: app/dumpcap/ F: doc/guides/tools/dumpcap.rst +F: examples/rpcapd/ +F: doc/guides/sample_app_ug/rpcapd.rst Packet Framework diff --git a/doc/guides/rel_notes/release_26_11.rst b/doc/guides/rel_notes/release_26_11.rst index 4b3e5d995c..958f5ebc16 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..6afbfa216c --- /dev/null +++ b/doc/guides/sample_app_ug/rpcapd.rst @@ -0,0 +1,216 @@ +.. SPDX-License-Identifier: BSD-3-Clause + Copyright(c) 2026 Stephen Hemminger + +.. _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:: + + * ``dpdk-rpcapd`` is experimental and provided for demonstration purposes only. + It may change or be removed without notice, and it is not intended to be relied upon. + + +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>`` + + Numeric IPv4 or IPv6 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. + +* ``-4`` + + Use only IPv4; an IPv6 argument to ``-b`` is rejected. + +* ``-N <ring_size>`` + + Size of the per-session capture ring in packets. Default is 2048. + Rounded up to a power of two if necessary. + +* ``-D``, ``--debug`` + + Increase log verbosity. By default only notices, warnings and + errors are printed. A single ``-D`` adds session-level messages + (client connected, capture started and stopped); ``-DD`` adds + per-request protocol detail. + +* ``--debug-file <file>`` + + Append log output to ``<file>`` instead of writing it to standard + error. + +* ``--lcore <core>`` + + CPU core to run on. By default the daemon runs as an ordinary + process on any non-isolated CPU. + +* ``--file-prefix <prefix>`` + + EAL file prefix of the primary process to attach to. Needed when + the primary was started with a non-default prefix. + +* ``--version`` + + Print the version and exit. + +* ``-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.7.tar.xz + tar xf libpcap-1.10.7.tar.xz + cd libpcap-1.10.7 + ./configure --enable-remote + make + sudo make install + +Only the client side of ``rpcap`` is used for ``dpdk-rpcapd``. +Do not run libpcap's version of ``rpcapd``. + +``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 --vdev=net_tap0 -- -i + +In another window, start ``dpdk-rpcapd``: + +.. code-block:: console + + sudo ./<build_dir>/examples/dpdk-rpcapd + RPCAPD: open_listen_socket(): listening on 127.0.0.1 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 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. + +* **Original length of truncated packets is not reported.** The + capture framework in the primary process copies only the snaplen + worth of bytes and does not carry the original frame length across + to the secondary, so a truncated packet is reported to the client + with its on-the-wire length equal to its captured length. A frame + longer than the snaplen therefore appears to the client as a short + frame rather than as a truncated long one. + + +See Also +-------- + +* :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..530543ce7e --- /dev/null +++ b/examples/rpcapd/main.c @@ -0,0 +1,1430 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Stephen Hemminger + * + * Demonstration server for the rpcap protocol for DPDK. + * This allows a libpcap client (e.g. Wireshark or tcpdump) + * to use "rpcap://host[:port]/portname" as capture device. + * + * 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, active mode, sampling and concurrent clients + * are intentionally omitted to keep the example small. + * + * A capture filter may be sent with the start-capture request: + * the client compiles it, so it arrives as cBPF which is converted to + * DPDK BPF and handed to pdump. Filters cannot be changed once the + * capture is running; see the UPDATEFILTER handling. + */ + +#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 <pcap/pcap.h> + +#include <rte_alarm.h> +#include <rte_bpf.h> +#include <rte_common.h> +#include <rte_debug.h> +#include <rte_eal.h> +#include <rte_errno.h> +#include <rte_ethdev.h> +#include <rte_lcore.h> +#include <rte_log.h> +#include <rte_malloc.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 MAX_RING_SIZE (1U << 20) +#define DEFAULT_SNAPLEN RTE_MBUF_DEFAULT_DATAROOM +#define PRIMARY_MONITOR_INTERVAL_US (500 * 1000) +#define SLEEP_THRESHOLD 100 +#define SLEEP_US 100 + +#define DATA_ACCEPT_TIMEOUT_MS 10000 +#define POLL_INTERVAL_MS 500 + +#define MAX_FILTER_INSNS 4096 + +#define RTE_LOGTYPE_RPCAPD RTE_LOGTYPE_USER1 +#define RPCAPD_LOG(level, ...) \ + RTE_LOG_LINE_PREFIX(level, RPCAPD, "%s(): ", __func__, __VA_ARGS__) + +/* Per-client capture session state. */ +struct session { + 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 */ + uint32_t pdump_flags; /* RTE_PDUMP_FLAG_* in use */ + bool opened; /* OPEN_REQ has selected a port */ + bool capture_on; + bool promisc_set; /* we enabled promiscuous mode */ + struct rte_ring *ring; + struct rte_mempool *mp; + struct rte_bpf_prm *prm; /* capture filter, NULL if none */ +}; + +/* 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 unsigned int debug_log; /* -D count: raise RPCAPD log verbosity */ + +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); +} + +/* + * Wait for fd to become readable, in POLL_INTERVAL_MS slices so that a + * quit signal (from SIGINT/SIGTERM or from the primary process dying) + * is noticed while blocked. timeout_ms < 0 waits indefinitely. + * + * Returns 1 when readable, 0 on timeout, -1 on error or quit. + */ +static int +wait_readable(int fd, int timeout_ms) +{ + struct pollfd pfd = { .fd = fd, .events = POLLIN }; + + while (!rte_atomic_load_explicit(&quit_signal, rte_memory_order_relaxed)) { + int wait_ms = POLL_INTERVAL_MS; + int rc; + + if (timeout_ms >= 0) { + if (timeout_ms == 0) + return 0; + if (timeout_ms < wait_ms) + wait_ms = timeout_ms; + timeout_ms -= wait_ms; + } + + rc = poll(&pfd, 1, wait_ms); + if (rc < 0) { + if (errno == EINTR) + continue; + RPCAPD_LOG(ERR, "poll failed: %s", strerror(errno)); + return -1; + } + if (rc > 0) + return 1; + } + return -1; +} + +/* accept() with a timeout, so a stalled client cannot wedge the daemon. */ +static int +accept_timeout(int listen_fd, int timeout_ms) +{ + int fd; + + switch (wait_readable(listen_fd, timeout_ms)) { + case 1: + break; + case 0: + RPCAPD_LOG(ERR, "timed out waiting for data connection"); + return -1; + default: + return -1; + } + + fd = accept(listen_fd, NULL, NULL); + if (fd < 0) + RPCAPD_LOG(ERR, "accept: %s", strerror(errno)); + return fd; +} + +/* 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; + + /* Wait with a timeout rather than blocking in recv(), so a + * quit signal or a dead primary is acted on promptly. + */ + if (wait_readable(fd, -1) != 1) + return -1; + + n = recv(fd, p, len, 0); + if (n < 0 && errno == EINTR) + continue; + + if (n <= 0) + return -1; + + p += n; + len -= n; + } + return 0; +} + +/* + * Send all of iov, resending the remainder if sendmsg() reports a short + * count (possible when the connection breaks or a signal arrives after + * some bytes were copied). Consumes iov, so pass a scratch copy. + */ +static int +send_iov_full(int fd, struct iovec *iov, int iovcnt, int flags) +{ + struct msghdr msg = { + .msg_iov = iov, + .msg_iovlen = iovcnt, + }; + + while (msg.msg_iovlen > 0) { + ssize_t n = sendmsg(fd, &msg, flags | MSG_NOSIGNAL); + + if (n < 0) { + if (errno == EINTR) + continue; + return -1; + } + if (n == 0) + return -1; + + /* Drop whole iovecs that were fully sent, then trim the + * partially sent one. + */ + while (msg.msg_iovlen > 0 && (size_t)n >= msg.msg_iov->iov_len) { + n -= msg.msg_iov->iov_len; + msg.msg_iov++; + msg.msg_iovlen--; + } + if (n > 0) { + msg.msg_iov->iov_base = (char *)msg.msg_iov->iov_base + n; + msg.msg_iov->iov_len -= n; + } + } + 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) +{ + RPCAPD_LOG(WARNING, "sending error to client: %s", 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) { + RPCAPD_LOG(DEBUG, "can not find name for port %u", p); + continue; + } + + RPCAPD_LOG(DEBUG, "findallif: port %u -> '%s'", p, name); + namelen = strlen(name); + desclen = strlen(desc); + entry = sizeof(struct rpcap_findalldevs_if) + namelen + desclen; + + nb = realloc(buf, buflen + entry); + if (nb == NULL) { + RPCAPD_LOG(ERR, "out of memory in findallif"); + 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++; + } + + RPCAPD_LOG(DEBUG, "findallif: %u interface(s)", 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; + + /* Unconditionally, not just when capture_on: a failed UPDATEFILTER + * leaves the ring, mempool and data connection live with the capture + * already disabled, and those must not survive into a new session. + */ + 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) { + RPCAPD_LOG(WARNING, "open: no such port '%s'", s->name); + /* s->name has already been overwritten; make sure a later + * STARTCAP cannot capture the previously opened port. + */ + s->opened = false; + return rpcap_send_error(fd, 0, "unknown interface"); + } + s->port = port; + s->opened = true; + + RPCAPD_LOG(DEBUG, "open: '%s' -> dpdk port %u", 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) { + RPCAPD_LOG(ERR, "data socket: %s", 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) { + RPCAPD_LOG(ERR, "data port bind/listen: %s", 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()); +} + +/* + * Read the optional capture filter that follows a start-capture request, + * and convert it for pdump. Client passes cBPF. + */ +static int +read_filter(int fd, uint32_t plen, struct session *s) +{ + struct rpcap_filterbpf_insn winsn; + struct rpcap_filter filter; + struct bpf_program bf; + struct bpf_insn *insns; + uint32_t i, nitems; + + if (plen == 0) + return 0; /* no filter: capture everything */ + + if (plen < sizeof(filter)) { + if (rpcap_discard(fd, plen) < 0) + return -1; + return rpcap_send_error(fd, 0, "short filter header") < 0 ? -1 : 1; + } + + if (recv_full(fd, &filter, sizeof(filter)) < 0) + return -1; + plen -= sizeof(filter); + + if (ntohs(filter.filtertype) != RPCAP_UPDATEFILTER_BPF) { + if (rpcap_discard(fd, plen) < 0) + return -1; + return rpcap_send_error(fd, 0, "unsupported filter type") < 0 ? -1 : 1; + } + + /* nitems is client-supplied; bound it before trusting the length. */ + nitems = ntohl(filter.nitems); + if (nitems == 0) + return rpcap_discard(fd, plen) < 0 ? -1 : 0; + + if (nitems > MAX_FILTER_INSNS || plen < nitems * sizeof(winsn)) { + if (rpcap_discard(fd, plen) < 0) + return -1; + return rpcap_send_error(fd, 0, "bad filter length") < 0 ? -1 : 1; + } + + insns = calloc(nitems, sizeof(*insns)); + if (insns == NULL) { + if (rpcap_discard(fd, plen) < 0) + return -1; + return rpcap_send_error(fd, 0, "out of memory") < 0 ? -1 : 1; + } + + for (i = 0; i < nitems; i++) { + if (recv_full(fd, &winsn, sizeof(winsn)) < 0) { + free(insns); + return -1; + } + insns[i].code = ntohs(winsn.code); + insns[i].jt = winsn.jt; + insns[i].jf = winsn.jf; + insns[i].k = ntohl(winsn.k); + } + plen -= nitems * sizeof(winsn); + + /* Anything after the instructions is padding we do not need. */ + if (rpcap_discard(fd, plen) < 0) { + free(insns); + return -1; + } + + bf.bf_len = nitems; + bf.bf_insns = insns; + + /* Reject a malformed program here */ + if (!bpf_validate(bf.bf_insns, bf.bf_len)) { + free(insns); + return rpcap_send_error(fd, 0, "invalid filter program") < 0 ? -1 : 1; + } + + /* A filter recorded by an earlier UPDATEFILTER may still be here; + * it is about to be replaced, so do not leak it. + */ + rte_free(s->prm); + s->prm = rte_bpf_convert(&bf); + free(insns); + if (s->prm == NULL) { + RPCAPD_LOG(ERR, "rte_bpf_convert failed: %s", + rte_strerror(rte_errno)); + return rpcap_send_error(fd, 0, "cannot convert filter") < 0 ? -1 : 1; + } + + RPCAPD_LOG(DEBUG, "capture filter: %u instructions", nitems); + return 0; +} + +/* 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, s->pdump_flags); + RPCAPD_LOG(INFO, "capture stopped on %s (%u packets)", + s->name, s->npkt); + } + s->capture_on = false; + + if (s->promisc_set) { + rte_eth_promiscuous_disable(s->port); + s->promisc_set = 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; + } + + /* Only safe once pdump is disabled */ + rte_free(s->prm); + s->prm = 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; + uint16_t flags; + struct rte_bpf_prm *recorded; + int data_listen; + int data_fd; + int ret; + + recorded = s->prm; + s->prm = NULL; + stop_capture(s); + s->prm = recorded; + + if (!s->opened) { + rpcap_discard(fd, plen); + return rpcap_send_error(fd, 0, "no interface open"); + } + + 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; + + flags = ntohs(req.flags); + if (flags & RPCAP_STARTCAPREQ_FLAG_DGRAM) { + rpcap_discard(fd, plen - sizeof(req)); + return rpcap_send_error(fd, 0, "UDP data transfer not supported"); + } + + ret = read_filter(fd, plen - sizeof(req), s); + if (ret != 0) + return ret < 0 ? -1 : 0; /* error already reported to client */ + + /* Direction flags map onto pdump's RX/TX selection; neither (or both) + * means capture in both directions. + */ + s->pdump_flags = RTE_PDUMP_FLAG_RXTX; + if ((flags & (RPCAP_STARTCAPREQ_FLAG_INBOUND | + RPCAP_STARTCAPREQ_FLAG_OUTBOUND)) == + RPCAP_STARTCAPREQ_FLAG_INBOUND) + s->pdump_flags = RTE_PDUMP_FLAG_RX; + else if ((flags & (RPCAP_STARTCAPREQ_FLAG_INBOUND | + RPCAP_STARTCAPREQ_FLAG_OUTBOUND)) == + RPCAP_STARTCAPREQ_FLAG_OUTBOUND) + s->pdump_flags = RTE_PDUMP_FLAG_TX; + + 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) { + RPCAPD_LOG(ERR, "ring/mempool alloc failed: %s", + 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"); + } + + /* Leave the port alone if it is already promiscuous: it belongs to + * the primary process, and stop_capture() must not turn off + * something this daemon did not turn on. + */ + if ((flags & RPCAP_STARTCAPREQ_FLAG_PROMISC) && + rte_eth_promiscuous_get(s->port) != 1) { + if (rte_eth_promiscuous_enable(s->port) == 0) + s->promisc_set = true; + else + RPCAPD_LOG(NOTICE, "cannot enable promiscuous mode on %s", + s->name); + } + + /* Arm pdump before replying. */ + if (rte_pdump_enable_bpf(s->port, RTE_PDUMP_ALL_QUEUES, s->pdump_flags, + s->snaplen, s->ring, s->mp, s->prm) < 0) { + RPCAPD_LOG(ERR, "rte_pdump_enable_bpf port %u failed: %s", + s->port, rte_strerror(rte_errno)); + close(data_listen); + stop_capture(s); + return rpcap_send_error(fd, 0, "cannot enable capture"); + } + s->capture_on = true; + s->npkt = 0; + + 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; + } + + RPCAPD_LOG(DEBUG, "awaiting connection"); + + data_fd = accept_timeout(data_listen, DATA_ACCEPT_TIMEOUT_MS); + close(data_listen); + if (data_fd < 0) { + stop_capture(s); + return -1; + } + + s->data_fd = data_fd; + + RPCAPD_LOG(INFO, + "capture started on %s (snaplen %u, data port %u)", + s->name, s->snaplen, data_port); + return 0; +} + +/* + * UPDATEFILTER_REQ: replace the capture filter. + * + * pdump takes its filter when the callback is armed and offers no way + * to replace it, so this disables and re-enables the callback with the + * new program. Packets already in the ring are kept; only the brief + * gap between disable and enable is lost. Refusing the request is not + * an option: libpcap sends UPDATEFILTER right after STARTCAP when the + * client was opened with PCAP_OPENFLAG_NOCAPTURE_RPCAP and aborts the + * capture if it fails, and Wireshark sets that flag by default. + * + * Before the capture starts this just records the filter for the + * eventual STARTCAP. + */ +static int +handle_updatefilter(int fd, uint32_t plen, struct session *s) +{ + struct rte_bpf_prm *old = s->prm; + int ret; + + s->prm = NULL; + ret = read_filter(fd, plen, s); + if (ret != 0) { + /* Malformed request: keep running with the old filter. */ + rte_free(s->prm); + s->prm = old; + return ret < 0 ? -1 : 0; /* error already reported */ + } + + if (!s->capture_on) { + rte_free(old); + return rpcap_send_msg(fd, RPCAP_MSG_UPDATEFILTER_REPLY, 0, NULL, 0); + } + + rte_pdump_disable(s->port, RTE_PDUMP_ALL_QUEUES, s->pdump_flags); + s->capture_on = false; + + if (rte_pdump_enable_bpf(s->port, RTE_PDUMP_ALL_QUEUES, s->pdump_flags, + s->snaplen, s->ring, s->mp, s->prm) < 0) { + RPCAPD_LOG(ERR, "rte_pdump_enable_bpf port %u failed: %s", + s->port, rte_strerror(rte_errno)); + rte_free(old); + /* The capture cannot be resumed, so do not leave the ring, + * mempool and data connection behind: the client has been + * told the capture is over, and a session that is neither + * capturing nor torn down has no way back. + */ + stop_capture(s); + return rpcap_send_error(fd, 0, "cannot apply filter"); + } + s->capture_on = true; + + /* Safe now that the old program is no longer referenced. */ + rte_free(old); + + RPCAPD_LOG(DEBUG, "capture filter updated on %s", s->name); + return rpcap_send_msg(fd, RPCAP_MSG_UPDATEFILTER_REPLY, 0, NULL, 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 corks the + * socket until the ring drains, so a backlog coalesces into full + * segments instead of flushing every BURST_SIZE packets. + */ +static ssize_t +process_ring(struct session *s, unsigned int *avail) +{ + struct rte_mbuf *pkts[BURST_SIZE]; + unsigned int i, n; + ssize_t written = 0; + struct timeval tv; + + n = rte_ring_sc_dequeue_burst(s->ring, (void **)pkts, BURST_SIZE, avail); + if (n == 0) + return 0; + + /* One timestamp for the whole burst */ + gettimeofday(&tv, NULL); + + for (i = 0; i < n; i++) { + struct rte_mbuf *m = pkts[i]; + /* Sized from the same bound that clamps caplen below, so the + * two cannot drift apart. + */ + uint8_t buf[DEFAULT_SNAPLEN]; + uint32_t pktlen = rte_pktmbuf_pkt_len(m); + uint32_t caplen = pktlen < s->snaplen ? pktlen : s->snaplen; + const void *data; + + s->npkt++; + + struct rpcap_header hdr = { + .ver = RPCAP_VERSION, + .type = RPCAP_MSG_PACKET, + .plen = htonl(sizeof(struct rpcap_pkthdr) + caplen), + }; + + /* + * pdump copies at most the snaplen into the capture mempool + * and rte_pktmbuf_copy() counts only what it copied, so + * pktlen is already clamped: a truncated packet is reported + * with len == caplen. The original wire length does not + * reach this process. See the Limitations section of + * doc/guides/sample_app_ug/rpcapd.rst. + */ + 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 }, + }; + + /* more to come in this burst, or still queued in the ring */ + bool more = (i + 1 < n) || (*avail > 0); + + if (send_iov_full(s->data_fd, iov, 3, more ? MSG_MORE : 0) < 0) { + if (errno == EPIPE || errno == ECONNRESET) + RPCAPD_LOG(DEBUG, "data connection closed by client"); + else + RPCAPD_LOG(NOTICE, "send on data connection failed: %s", + strerror(errno)); + goto error; + } + rte_pktmbuf_free(m); + written += sizeof(hdr) + sizeof(pkthdr) + caplen; + } + + return written; + +error: + rte_pktmbuf_free_bulk(pkts + i, n - i); + return -1; +} + +/* Poll the control socket while idle. + * Returns 0 to keep capturing, 1 if a control message (typically + * ENDCAP) is pending, or -1 if the client has gone away. + */ +static int +check_socket_status(int ctrl_fd) +{ + struct pollfd pfd = { .fd = ctrl_fd, .events = POLLIN }; + + if (poll(&pfd, 1, 0) < 0) { + if (errno == EINTR) + return 0; + RPCAPD_LOG(ERR, "poll failed: %s", strerror(errno)); + return -1; + } + if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) { + RPCAPD_LOG(DEBUG, "client closed control connection"); + return -1; + } + if (pfd.revents & POLLIN) + return 1; + return 0; +} + +/* + * Stay in the capture loop until either: + * - a control message arrives (typically ENDCAP), + * - the data connection breaks, or + * - a quit signal is delivered. + * + * Returns 0 if the session should continue (the caller reads the + * pending control message), -1 if the client is gone. + * + * The control socket is polled once per iteration, not just when the + * ring runs dry. A client that sends a request mid-capture blocks + * waiting for the reply without draining the data socket, so under + * sustained traffic a poll that only happens while idle never runs and + * both ends wedge once the socket buffers fill. + */ +static int +capture_loop(int ctrl_fd, struct session *s) +{ + unsigned int empty_count = 0; + + while (!rte_atomic_load_explicit(&quit_signal, rte_memory_order_relaxed)) { + ssize_t written; + unsigned int avail = 0; + + switch (check_socket_status(ctrl_fd)) { + case 1: + /* control message pending, let caller service it */ + return 0; + case 0: + break; + default: + /* client is gone */ + return -1; + } + + written = process_ring(s, &avail); + if (written < 0) { + /* process_ring has already logged the reason */ + return -1; + } + + if (written > 0) { + /* are there more packets? */ + empty_count = (avail == 0); + continue; + } + + if (empty_count < SLEEP_THRESHOLD) { + /* spin a few times before checking */ + ++empty_count; + rte_pause(); + continue; + } + + /* ring has been empty for a while: stop spinning */ + rte_delay_us_sleep(SLEEP_US); + } + 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 = { .data_fd = -1 }; + + if (getpeername(ctrl_fd, (struct sockaddr *)&peer, &plen) == 0) + getnameinfo((struct sockaddr *)&peer, plen, + host, sizeof(host), NULL, 0, NI_NUMERICHOST); + RPCAPD_LOG(INFO, "client %s connected", host); + + while (!rte_atomic_load_explicit(&quit_signal, rte_memory_order_relaxed)) { + struct rpcap_header hdr; + + /* Drain the ring whenever a capture is running */ + if (s.capture_on && capture_loop(ctrl_fd, &s) < 0) + goto done; + + if (rpcap_recv_header(ctrl_fd, &hdr) < 0) + break; + + /* Only version 0 is spoken here */ + if (hdr.ver != RPCAP_VERSION) { + RPCAPD_LOG(WARNING, "unsupported protocol version %u", + hdr.ver); + if (rpcap_discard(ctrl_fd, hdr.plen) < 0 || + rpcap_send_error(ctrl_fd, PCAP_ERR_WRONGVER, + "unsupported protocol version") < 0) + goto done; + continue; + } + + 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; + break; + case RPCAP_MSG_UPDATEFILTER_REQ: + if (handle_updatefilter(ctrl_fd, hdr.plen, &s) < 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: + RPCAPD_LOG(WARNING, "unsupported request type 0x%02x", hdr.type); + if (rpcap_discard(ctrl_fd, hdr.plen) < 0 || + rpcap_send_error(ctrl_fd, 0, "unsupported request") < 0) + goto done; + break; + } + } +done: + stop_capture(&s); + close(ctrl_fd); + RPCAPD_LOG(INFO, "client %s disconnected", 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)); + + RPCAPD_LOG(NOTICE, "listening on %s port %u", host, listen_port); + + if (!is_loopback(&listen_addr)) + RPCAPD_LOG(WARNING, + "non-loopback address %s; " + "rpcap is unauthenticated and unencrypted, captured traffic is exposed to the network", + 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 increase log verbosity (-D info, -DD debug)\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': { + unsigned long u = strtoul(optarg, NULL, 0); + + /* Check the full value before narrowing it: an upper + * bound is needed anyway because rte_align32pow2() + * wraps to zero above 2^31, and that failure would + * otherwise only surface in rte_ring_create() on the + * first capture. + */ + if (u < 64 || u > MAX_RING_SIZE) + rte_exit(EXIT_FAILURE, + "Ring size must be between 64 and %u\n", + MAX_RING_SIZE); + ring_size = (uint32_t)u; + /* rte_ring_create() requires a power of two. */ + if (!rte_is_power_of_2(ring_size)) { + ring_size = rte_align32pow2(ring_size); + RPCAPD_LOG(NOTICE, "ring size rounded up to %u", + ring_size); + } + break; + } + case 'D': + debug_log++; + 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; + } + + RPCAPD_LOG(NOTICE, "primary process exited, shutting down"); + 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) + RPCAPD_LOG(WARNING, "failed to install primary process monitor"); +} + +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) + RPCAPD_LOG(INFO, "Can not restore original CPU affinity"); + + 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"); + + /* Default to NOTICE: only things the operator needs to see. + * Each -D steps down one level, to INFO then DEBUG. + */ + rte_log_set_level(RTE_LOGTYPE_RPCAPD, + debug_log >= 2 ? RTE_LOG_DEBUG : + debug_log == 1 ? RTE_LOG_INFO : RTE_LOG_NOTICE); + + 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_timeout(srv_fd, -1); + + if (cfd < 0) { + if (errno == EINTR) + continue; + break; + } + handle_client(cfd); + } + + disable_primary_monitor(); + RPCAPD_LOG(NOTICE, "shutting down"); + 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..320b262666 --- /dev/null +++ b/examples/rpcapd/meson.build @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright(c) 2026 Stephen Hemminger + +# since it relies on primary/secondary process +# this example is Linux only +if not is_linux + build = false + subdir_done() +endif + +if not dpdk_conf.has('RTE_HAS_LIBPCAP') + build = false + reason = 'missing dependency, "libpcap"' + subdir_done() +endif + +sources = files('main.c') +ext_deps += pcap_dep +deps += ['ethdev', 'pdump', 'bpf'] diff --git a/examples/rpcapd/rpcap-protocol.h b/examples/rpcapd/rpcap-protocol.h new file mode 100644 index 0000000000..b381271de7 --- /dev/null +++ b/examples/rpcapd/rpcap-protocol.h @@ -0,0 +1,127 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Stephen Hemminger + * + * 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 rpcapd example 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) + +/* Error codes carried in the 'value' field of RPCAP_MSG_ERROR */ +#define PCAP_ERR_WRONGVER 17 + +/* Filter encoding: the filter is a BPF/NPF program */ +#define RPCAP_UPDATEFILTER_BPF 1 + +/* Flags in rpcap_startcapreq.flags */ +#define RPCAP_STARTCAPREQ_FLAG_PROMISC 0x00000001 /* promiscuous mode */ +#define RPCAP_STARTCAPREQ_FLAG_DGRAM 0x00000002 /* use UDP for data */ +#define RPCAP_STARTCAPREQ_FLAG_SERVEROPEN 0x00000004 /* server connects out */ +#define RPCAP_STARTCAPREQ_FLAG_INBOUND 0x00000008 /* capture inbound only */ +#define RPCAP_STARTCAPREQ_FLAG_OUTBOUND 0x00000010 /* capture outbound only */ + +/* 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; +}; + +/* + * A filter, sent either after rpcap_startcapreq or in an + * RPCAP_MSG_UPDATEFILTER_REQ, followed by nitems instructions. + */ +struct rpcap_filter { + uint16_t filtertype; + uint16_t dummy; + uint32_t nitems; +}; + +/* One cBPF instruction, repeated nitems times after rpcap_filter. */ +struct rpcap_filterbpf_insn { + uint16_t code; + uint8_t jt; + uint8_t jf; + int32_t k; +}; + +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

