From b0c4290005f3e4647d9a96f3350ee8c8b793a333 Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Mon, 13 Apr 2026 23:49:33 +0200 Subject: [PATCH 1/9] gen_api_header: fix copyright year The date format needs a % character. Fixes: 8de36764e80e ("api: generate a single grout.h umbrella header") Signed-off-by: Robin Jarry --- devtools/gen_api_header.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devtools/gen_api_header.sh b/devtools/gen_api_header.sh index ec4ae7132..a9810fc1e 100755 --- a/devtools/gen_api_header.sh +++ b/devtools/gen_api_header.sh @@ -3,7 +3,7 @@ # Copyright (c) 2026 Robin Jarry echo "// SPDX-License-Identifier: BSD-3-Clause" -echo "// Copyright (c) $(date +Y) Red Hat" +echo "// Copyright (c) $(date +%Y) Red Hat" echo echo "#pragma once" echo From 4dbefc1f5d08f4b081df942c7f1a4de475f458c2 Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Mon, 13 Apr 2026 23:53:09 +0200 Subject: [PATCH 2/9] api: extend ABI check to cover inline functions Add a GR_API_INLINE macro that normally expands to static inline. In check_api.sh, it is redefined to empty so the compiler emits an external symbol that abidiff can track. For inline functions whose body is part of the API contract (e.g. shared memory ring accessors), compare the disassembly of both revisions with objdump. Since both binaries are built with the same toolchain and flags, any difference is a real code change. Treat removed or changed inline functions as breaking. Signed-off-by: Robin Jarry --- api/gr_api.h | 3 +++ devtools/check_api.sh | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/api/gr_api.h b/api/gr_api.h index 1c73d8533..3339a8a19 100644 --- a/api/gr_api.h +++ b/api/gr_api.h @@ -156,6 +156,9 @@ const char *gr_api_message_name(uint32_t type); code##_OBJ_SIZE = sizeof(obj) \ } #endif +#ifndef GR_API_INLINE +#define GR_API_INLINE static inline +#endif struct gr_empty { }; diff --git a/devtools/check_api.sh b/devtools/check_api.sh index b95042878..f9e85cf89 100755 --- a/devtools/check_api.sh +++ b/devtools/check_api.sh @@ -43,6 +43,7 @@ tar -C "$dir/check_api/a" -x --transform='s|.*/||' # Exclude gr_api_client_impl.h which isn't a real API header. rm -f $dir/check_api/*/gr_api_client_impl.h +cc_cmd="$cc_cmd -Wno-missing-declarations -Wno-missing-prototypes" cc_cmd="$cc_cmd -fno-eliminate-unused-debug-types -Werror -O0 -g" # Compile a dummy binary @@ -63,6 +64,9 @@ for d in $dir/check_api/*; do obj *e##_(void) { \\ return (void *)0; \\ } + +#define GR_API_INLINE __attribute__((section(".api_inline"))) + EOF basename -a $d/*.h | sed 's/.*/#include <&>/' } | @@ -76,14 +80,42 @@ printf "Checking for API changes between %s and %s\n" \ $(git describe --long --abbrev=8 $prev_revision) \ $(git describe --long --abbrev=8 --dirty) +# Check for inline function body changes. +# GR_API_INLINE functions are placed in a dedicated ELF section so we can +# compare their disassembly to detect code changes. +{ + objdump -t -j .api_inline $dir/check_api/a.bin 2>/dev/null || true + objdump -t -j .api_inline $dir/check_api/b.bin 2>/dev/null || true +} | awk '/F .api_inline/{print $NF}' | sort -u > $dir/check_api/inline_funcs + +breaking=false +while read -r func; do + a=$(objdump -d --disassemble="$func" $dir/check_api/a.bin 2>/dev/null \ + | sed -n '/^[0-9a-f].*:/{s/^[^:]*://;s/\t[0-9a-f ]*\t/\t/;p}') + b=$(objdump -d --disassemble="$func" $dir/check_api/b.bin 2>/dev/null \ + | sed -n '/^[0-9a-f].*:/{s/^[^:]*://;s/\t[0-9a-f ]*\t/\t/;p}') + if [ -z "$a" ] && [ -z "$b" ]; then + continue + elif [ -z "$a" ]; then + echo "inline API function $func: added" + elif [ -z "$b" ]; then + breaking=true + echo "inline API function $func: removed" + elif [ "$a" != "$b" ]; then + breaking=true + echo "inline API function $func: code changed" + fi +done < $dir/check_api/inline_funcs + if ! $abidiff --non-reachable-types --drop-private-types --show-bytes \ --headers-dir1 $dir/check_api/a --headers-dir2 $dir/check_api/b \ - $dir/check_api/a.bin $dir/check_api/b.bin >"$dir/abidiff.log" 2>&1 + $dir/check_api/a.bin $dir/check_api/b.bin >"$dir/abidiff.log" 2>&1 \ + || [ "$breaking" = true ] then grep -vE '((Functions|Variables) changes|Unreachable types) summary:' "$dir/abidiff.log" api_version_a=$(sed -nE 's/^#define GR_API_VERSION ([0-9]+).*/\1/p' $dir/check_api/a/*.h) api_version_b=$(sed -nE 's/^#define GR_API_VERSION ([0-9]+).*/\1/p' $dir/check_api/b/*.h) - if grep -q '^ \[[DC]\]' "$dir/abidiff.log"; then + if grep -q '^ \[[DC]\]' "$dir/abidiff.log" || [ "$breaking" = true ]; then echo "breaking API changes" if [ "${api_version_a:-0}" -ge "${api_version_b:-0}" ]; then grep -n '#define GR_API_VERSION' "$@" From ddde63cb988d13e1bba9bb88f71f197063f06895 Mon Sep 17 00:00:00 2001 From: Vincent Jardin Date: Mon, 23 Mar 2026 22:59:34 +0100 Subject: [PATCH 3/9] api: add file descriptor passing over the UNIX API socket Allow API handlers to return a file descriptor alongside the response payload using SCM_RIGHTS ancillary data on the UNIX socket. On the server side, api_out_fd() sets an fd in struct api_out. When fd >= 0, the response is sent via sendmsg() with SCM_RIGHTS instead of bufferevent_write(). The fd is closed after sending. On the client side, recv() is replaced by recvmsg() so SCM_RIGHTS ancillary data is captured. gr_api_client_recv_fd() and gr_api_client_send_recv_fd() return the received fd to callers that need it. The existing gr_api_client_recv() delegates to recv_fd() and silently closes any unexpected fd. Out-of-order cached responses now carry their fd so it is preserved until the matching recv call picks it up. Suggested-by: Robin Jarry Signed-off-by: Vincent Jardin --- api/gr_api.h | 26 ++++++++++++++- api/gr_api_client_impl.h | 72 ++++++++++++++++++++++++++++++++++++++-- main/api.c | 66 +++++++++++++++++++++++++++++++----- main/module.h | 8 ++++- 4 files changed, 158 insertions(+), 14 deletions(-) diff --git a/api/gr_api.h b/api/gr_api.h index 3339a8a19..eaa9e59c8 100644 --- a/api/gr_api.h +++ b/api/gr_api.h @@ -11,7 +11,7 @@ #include // Must be bumped when making non-backward compatible changes in API headers -#define GR_API_VERSION 1 +#define GR_API_VERSION 2 // API request header. struct gr_api_request { @@ -58,6 +58,14 @@ gr_api_client_send(struct gr_api_client *, uint32_t req_type, size_t tx_len, con // Returns -EMSGSIZE if payload is non-empty but smaller than min_resp_size. int gr_api_client_recv(struct gr_api_client *, uint32_t req_type, uint32_t for_id, void **rx_data); +int gr_api_client_recv_fd( + struct gr_api_client *, + uint32_t req_type, + uint32_t for_id, + void **rx_data, + int *fd +); + // Send a request and receive the response. // Validates response payload size against GR_REQ-declared type. // Caller must free(*rx_data) after use. @@ -78,6 +86,22 @@ static inline int gr_api_client_send_recv( // internal, called when interrupting gr_api_client_stream_foreach() int __gr_api_client_stream_drain(struct gr_api_client *, uint32_t req_type, uint32_t for_id); +// Send a request and receive the response with an optional file descriptor. +// If fd is non-NULL and the server sends an fd via SCM_RIGHTS, it is stored in *fd. +static inline int gr_api_client_send_recv_fd( + struct gr_api_client *client, + uint32_t req_type, + size_t tx_len, + const void *tx_data, + void **rx_data, + int *fd +) { + long int ret = gr_api_client_send(client, req_type, tx_len, tx_data); + if (ret < 0) + return ret; + return gr_api_client_recv_fd(client, req_type, ret, rx_data, fd); +} + // Send a request and iterate over the received stream of responses. // // @param obj Iterator variable (const pointer to response object type). diff --git a/api/gr_api_client_impl.h b/api/gr_api_client_impl.h index 242b286d0..294109aab 100644 --- a/api/gr_api_client_impl.h +++ b/api/gr_api_client_impl.h @@ -101,6 +101,7 @@ const char *gr_api_message_name(uint32_t type) { struct response { struct gr_api_response header; void *payload; + int fd; // received via SCM_RIGHTS, -1 if none STAILQ_ENTRY(response) next; }; @@ -153,6 +154,8 @@ int gr_api_client_disconnect(struct gr_api_client *client) { while (!STAILQ_EMPTY(&client->responses)) { struct response *resp = STAILQ_FIRST(&client->responses); STAILQ_REMOVE_HEAD(&client->responses, next); + if (resp->fd >= 0) + close(resp->fd); free(resp->payload); free(resp); } @@ -224,16 +227,61 @@ long int gr_api_client_send( return req.id; } -int gr_api_client_recv( +// Receive a response header, potentially with an SCM_RIGHTS fd. +// Uses recvmsg() so ancillary data is captured. +static int +recv_response_header(const struct gr_api_client *c, struct gr_api_response *resp, int *recv_fd) { + *recv_fd = -1; + + union { + char buf[CMSG_SPACE(sizeof(int))]; + struct cmsghdr align; + } cmsg_buf; + memset(&cmsg_buf, 0, sizeof(cmsg_buf)); + + struct iovec iov = {.iov_base = resp, .iov_len = sizeof(*resp)}; + struct msghdr msg = { + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = cmsg_buf.buf, + .msg_controllen = sizeof(cmsg_buf.buf), + }; + + ssize_t n = recvmsg(c->sock_fd, &msg, MSG_CMSG_CLOEXEC); + + if (n == 0) { + errno = ECONNRESET; + return -1; + } + if (n < 0) + return -1; + if ((size_t)n < sizeof(*resp)) { + errno = EPROTO; + return -1; + } + + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); + if (cmsg != NULL && cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS) + memcpy(recv_fd, CMSG_DATA(cmsg), sizeof(int)); + + return 0; +} + +int gr_api_client_recv_fd( struct gr_api_client *client, uint32_t req_type, uint32_t for_id, - void **rx_data + void **rx_data, + int *fd ) { struct response *cached = NULL; const struct api_message *m; struct gr_api_response resp; void *payload = NULL; + int recv_fd = -1; + + if (fd != NULL) + *fd = -1; if (client == NULL) return errno_set(EINVAL); @@ -249,12 +297,13 @@ int gr_api_client_recv( STAILQ_REMOVE(&client->responses, cached, response, next); resp = cached->header; payload = cached->payload; + recv_fd = cached->fd; free(cached); goto out; } recv: // No matching cached message, try to receive one from the socket. - if (recv_all(client, &resp, sizeof(resp)) != sizeof(resp)) + if (recv_response_header(client, &resp, &recv_fd) < 0) goto err; if (resp.payload_len > GR_API_MAX_MSG_LEN) { @@ -275,8 +324,10 @@ int gr_api_client_recv( goto err; cached->header = resp; cached->payload = payload; + cached->fd = recv_fd; STAILQ_INSERT_TAIL(&client->responses, cached, next); payload = NULL; + recv_fd = -1; // And try to receive the next message until we get the correct ID. goto recv; } @@ -299,13 +350,28 @@ int gr_api_client_recv( assert(rx_data != NULL); *rx_data = payload; } + if (fd != NULL) + *fd = recv_fd; + else if (recv_fd >= 0) + close(recv_fd); return 0; err: + if (recv_fd >= 0) + close(recv_fd); free(payload); return -errno; } +int gr_api_client_recv( + struct gr_api_client *client, + uint32_t req_type, + uint32_t for_id, + void **rx_data +) { + return gr_api_client_recv_fd(client, req_type, for_id, rx_data, NULL); +} + int gr_api_client_event_recv(const struct gr_api_client *c, struct gr_api_event **event) { const struct api_message *m; struct gr_api_event header; diff --git a/main/api.c b/main/api.c index 023c9002f..b97d5e43f 100644 --- a/main/api.c +++ b/main/api.c @@ -22,6 +22,7 @@ #include #include #include +#include #include LOG_TYPE("api"); @@ -249,6 +250,50 @@ void api_send(struct api_ctx *ctx, uint32_t len, const void *payload) { LOG(ERR, "pid=%d cannot write payload", ctx->pid); } +// Send a response header + optional payload together with a file +// descriptor via sendmsg(SCM_RIGHTS). Closes the fd after sending. +static void +send_response_with_fd(struct bufferevent *bev, struct gr_api_response *resp, struct api_out *out) { + bufferevent_flush(bev, EV_WRITE, BEV_FLUSH); + + struct iovec iov[2]; + int iovlen = 1; + iov[0].iov_base = resp; + iov[0].iov_len = sizeof(*resp); + if (out->len > 0 && out->payload != NULL) { + iov[1].iov_base = out->payload; + iov[1].iov_len = out->len; + iovlen = 2; + } + + union { + char buf[CMSG_SPACE(sizeof(int))]; + struct cmsghdr align; + } cmsg_buf; + memset(&cmsg_buf, 0, sizeof(cmsg_buf)); + + struct msghdr msg = { + .msg_iov = iov, + .msg_iovlen = iovlen, + .msg_control = cmsg_buf.buf, + .msg_controllen = sizeof(cmsg_buf.buf), + }; + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + cmsg->cmsg_len = CMSG_LEN(sizeof(int)); + memcpy(CMSG_DATA(cmsg), &out->fd, sizeof(int)); + + ssize_t ret; + do { + ret = sendmsg(bufferevent_getfd(bev), &msg, MSG_NOSIGNAL); + } while (ret < 0 && errno == EINTR); + if (ret < 0) + LOG(ERR, "sendmsg with fd: %s", strerror(errno)); + close(out->fd); + out->fd = -1; +} + static void read_cb(struct bufferevent *bev, void *priv) { struct evbuffer *input = bufferevent_get_input(bev); struct api_ctx *ctx = priv; @@ -294,7 +339,7 @@ static void read_cb(struct bufferevent *bev, void *priv) { // Reset state for next request ctx->header_complete = false; - struct api_out out; + struct api_out out = {.fd = -1}; // We have a complete request, process it const struct api_handler *handler = lookup_api_handler(ctx->header.type); @@ -333,16 +378,19 @@ static void read_cb(struct bufferevent *bev, void *priv) { .payload_len = out.len, }; - if (bufferevent_write(bev, &resp, sizeof(resp)) < 0) - LOG(ERR, "failed to write header"); - if (out.len > 0) { - assert(out.payload != NULL); - if (bufferevent_write(bev, out.payload, out.len) < 0) - LOG(ERR, "failed to write payload"); + if (out.fd >= 0) { + send_response_with_fd(bev, &resp, &out); + } else { + if (bufferevent_write(bev, &resp, sizeof(resp)) < 0) + LOG(ERR, "failed to write header"); + if (out.len > 0) { + assert(out.payload != NULL); + if (bufferevent_write(bev, out.payload, out.len) < 0) + LOG(ERR, "failed to write payload"); + } + bufferevent_flush(bev, EV_WRITE, BEV_FLUSH); } - bufferevent_flush(bev, EV_WRITE, BEV_FLUSH); - free(req_payload); free(out.payload); diff --git a/main/module.h b/main/module.h index a5d29dad4..7d5fd4696 100644 --- a/main/module.h +++ b/main/module.h @@ -15,10 +15,16 @@ struct api_out { uint32_t status; uint32_t len; void *payload; + int fd; // file descriptor to pass via SCM_RIGHTS, -1 = none }; static inline struct api_out api_out(uint32_t status, uint32_t len, void *payload) { - struct api_out out = {status, len, payload}; + struct api_out out = {status, len, payload, -1}; + return out; +} + +static inline struct api_out api_out_fd(uint32_t status, uint32_t len, void *payload, int fd) { + struct api_out out = {status, len, payload, fd}; return out; } From a4a7fb84b6f1bd011603034e94e4d3ed4c8e75ef Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Tue, 14 Apr 2026 23:18:29 +0200 Subject: [PATCH 4/9] subprojects: add libpcap with plugin loader support Add libpcap as a meson wrap subproject with three patches on top of upstream commit bac2884b. These patches implement the plugin loader proposed in https://github.com/the-tcpdump-group/libpcap/pull/1659: - runtime plugin loader for external capture modules - timestamp type accessors for adapter code - complete accessor API for tcpdump feature parity The meson.build exposes a plugindir variable via declare_dependency() so consumers can install plugins in the right location. Signed-off-by: Robin Jarry --- .github/workflows/check.yml | 2 +- .gitignore | 1 + subprojects/libpcap.wrap | 12 + ...me-plugin-loader-for-external-captur.patch | 1004 +++++++++++++++++ ...mestamp-type-accessors-for-adapter-c.patch | 102 ++ ...accessor-API-for-tcpdump-feature-par.patch | 196 ++++ subprojects/packagefiles/libpcap/meson.build | 978 ++++++++++++++++ .../packagefiles/libpcap/meson_options.txt | 26 + 8 files changed, 2320 insertions(+), 1 deletion(-) create mode 100644 subprojects/libpcap.wrap create mode 100644 subprojects/packagefiles/libpcap/0001-plugin-add-runtime-plugin-loader-for-external-captur.patch create mode 100644 subprojects/packagefiles/libpcap/0002-plugin-expose-timestamp-type-accessors-for-adapter-c.patch create mode 100644 subprojects/packagefiles/libpcap/0003-plugin-complete-accessor-API-for-tcpdump-feature-par.patch create mode 100644 subprojects/packagefiles/libpcap/meson.build create mode 100644 subprojects/packagefiles/libpcap/meson_options.txt diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 7e5e9c2d2..97272413c 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -56,7 +56,7 @@ jobs: # grout dependencies sudo apt-get update -qy sudo apt-get install -qy --no-install-recommends \ - make gcc gdb ccache ninja-build meson git scdoc \ + make gcc gdb ccache ninja-build meson git scdoc bison flex \ libibverbs-dev libasan8 libcmocka-dev libedit-dev libarchive-dev \ libevent-dev libmnl-dev libnuma-dev python3-pyelftools \ socat tcpdump traceroute graphviz iproute2 iputils-ping ndisc6 jq \ diff --git a/.gitignore b/.gitignore index a1d377833..d3d09118d 100644 --- a/.gitignore +++ b/.gitignore @@ -21,5 +21,6 @@ /subprojects/dpdk-* /subprojects/libecoli-* /subprojects/frr-* +/subprojects/libpcap /subprojects/packagecache /subprojects/.wraplock diff --git a/subprojects/libpcap.wrap b/subprojects/libpcap.wrap new file mode 100644 index 000000000..0a19aeb82 --- /dev/null +++ b/subprojects/libpcap.wrap @@ -0,0 +1,12 @@ +[wrap-git] +url = https://github.com/the-tcpdump-group/libpcap +revision = bac2884bfabe20f9ff0f711eb9eb7589569e6a01 +depth = 1 +patch_directory = libpcap +diff_files = + libpcap/0001-plugin-add-runtime-plugin-loader-for-external-captur.patch, + libpcap/0002-plugin-expose-timestamp-type-accessors-for-adapter-c.patch, + libpcap/0003-plugin-complete-accessor-API-for-tcpdump-feature-par.patch + +[provide] +dependency_names = libpcap diff --git a/subprojects/packagefiles/libpcap/0001-plugin-add-runtime-plugin-loader-for-external-captur.patch b/subprojects/packagefiles/libpcap/0001-plugin-add-runtime-plugin-loader-for-external-captur.patch new file mode 100644 index 000000000..4d6ec3b35 --- /dev/null +++ b/subprojects/packagefiles/libpcap/0001-plugin-add-runtime-plugin-loader-for-external-captur.patch @@ -0,0 +1,1004 @@ +From 266e1b453126bdbd44e43b601b22ffa56f7b9c0d Mon Sep 17 00:00:00 2001 +From: Vincent Jardin +Date: Thu, 19 Mar 2026 16:08:33 +0100 +Subject: [PATCH 1/3] plugin: add runtime plugin loader for external capture + backends + +Dataplanes like DPDK expose unstable APIs that change across releases. +Compiling backends directly into libpcap ties the two release cycles +together. Add a generic plugin loader (pcap-plugin.c) that discovers +external capture backends at runtime, decoupling libpcap from backend +release cycles. + +Each backend ships as a pcap-*.so shared module. The loader scans +plugin directories, loads each via the centralized pcapint_load_code() +API (extended from Windows-only to Unix with dlopen/dlsym), looks up +a "pcap_plugin_entry" symbol, checks the ABI version, and dispatches +findalldevs/create calls through it. + +A set of pcap_plugin_* accessor functions (pcap/pcap-plugin.h) provides +plugins a stable ABI without exposing pcap-int.h internals: handle +allocation, ops registration, field getters/setters, BPF helpers, and +device enumeration. + +Security model, modeled after OpenSSL provider loading: + +A survey of PAM, NSS (glibc), GStreamer, OpenSSL (providers), and Mesa +shows they all converge on the same model: a hardcoded directory as the +default, with environment variable overrides ignored under elevated +privileges. None perform lstat(), ownership checks, or symlink +validation. + + - $PCAP_PLUGIN_DIR is read through secure_getenv() on glibc, with + fallbacks for BSD (issetugid), Linux without glibc (getauxval + AT_SECURE), and other Unix (uid/euid comparison). Under elevated + privileges the env var is automatically ignored, matching how + OpenSSL handles OPENSSL_MODULES and how ld-linux.so handles + LD_LIBRARY_PATH. + - The hardcoded plugin directory (PCAP_PLUGIN_DIR, default + ${libdir}/pcap/plugins) is always scanned. + - Filesystem permissions on the plugin directory are the security + boundary, same as PAM, NSS, OpenSSL, and Mesa. + - plugindir is exposed in libpcap.pc so external projects can query + the install path at build time with: + pkg-config --variable=plugindir libpcap + This follows the GStreamer model (pluginsdir in gstreamer-1.0.pc). + +Signed-off-by: Vincent Jardin +--- + CMakeLists.txt | 20 +++ + Makefile.in | 8 +- + cmakeconfig.h.in | 9 + + configure.ac | 24 +++ + libpcap.pc.in | 1 + + pcap-int.h | 6 +- + pcap-plugin.c | 428 +++++++++++++++++++++++++++++++++++++++++++++ + pcap-plugin.h | 28 +++ + pcap.c | 32 +++- + pcap/pcap-plugin.h | 228 ++++++++++++++++++++++++ + 10 files changed, 780 insertions(+), 4 deletions(-) + create mode 100644 pcap-plugin.c + create mode 100644 pcap-plugin.h + create mode 100644 pcap/pcap-plugin.h + +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 4bd07e43448f..14f57e6c229b 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -2510,6 +2510,25 @@ if(NOT DISABLE_DPDK) + endif() + endif() + ++# ++# Plugin loader — always compiled, discovers pcap-*.so at runtime. ++# ++set(PROJECT_SOURCE_LIST_C ${PROJECT_SOURCE_LIST_C} pcap-plugin.c) ++set(PCAP_LINK_LIBRARIES ${PCAP_LINK_LIBRARIES} ${CMAKE_DL_LIBS}) ++ ++# ++# Plugin directory (build-time default). ++# Exposed as plugindir in libpcap.pc so external projects can ++# query it with: pkg-config --variable=plugindir libpcap ++# ++set(PLUGIN_DIR "${CMAKE_INSTALL_PREFIX}/lib/pcap/plugins" CACHE STRING ++ "Plugin directory for pcap-*.so modules") ++add_definitions(-DPCAP_PLUGIN_DIR="${PLUGIN_DIR}") ++ ++check_function_exists(secure_getenv HAVE_SECURE_GETENV) ++check_function_exists(issetugid HAVE_ISSETUGID) ++check_function_exists(getauxval HAVE_GETAUXVAL) ++ + # Check for Bluetooth sniffing support + if(NOT DISABLE_BLUETOOTH) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") +@@ -3575,6 +3594,7 @@ if(NOT MSVC) + set(RPATH "") + endif() + endif() ++ set(plugindir "${PLUGIN_DIR}") + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/pcap-config.in ${CMAKE_CURRENT_BINARY_DIR}/pcap-config @ONLY) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/libpcap.pc.in ${CMAKE_CURRENT_BINARY_DIR}/libpcap.pc @ONLY) + install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/pcap-config DESTINATION bin) +diff --git a/Makefile.in b/Makefile.in +index a555574dd723..c4a82ce043ac 100644 +--- a/Makefile.in ++++ b/Makefile.in +@@ -33,6 +33,8 @@ sbindir = @sbindir@ + includedir = @includedir@ + # Pathname of directory to install the library + libdir = @libdir@ ++# Pathname of directory to install plugin modules ++plugindir = @plugindir@ + # Pathname of directory to install the man pages + mandir = @mandir@ + +@@ -53,7 +55,7 @@ MKDEP = @MKDEP@ + CCOPT = @V_CCOPT@ + SHLIB_CCOPT = @V_SHLIB_CCOPT@ + INCLS = -I. @V_INCLS@ +-DEFS = -DBUILDING_PCAP -Dpcap_EXPORTS @DEFS@ @V_DEFS@ ++DEFS = -DBUILDING_PCAP -Dpcap_EXPORTS -DPCAP_PLUGIN_DIR='"$(plugindir)"' @DEFS@ @V_DEFS@ + ADDLOBJS = @ADDLOBJS@ + ADDLARCHIVEOBJS = @ADDLARCHIVEOBJS@ + LIBS = @LIBS@ +@@ -114,6 +116,7 @@ PUBHDR = \ + pcap/nflog.h \ + pcap/pcap-inttypes.h \ + pcap/pcap.h \ ++ pcap/pcap-plugin.h \ + pcap/sll.h \ + pcap/socket.h \ + pcap/usb.h \ +@@ -132,6 +135,7 @@ HDR = $(PUBHDR) \ + optimize.h \ + pcap-common.h \ + pcap-int.h \ ++ pcap-plugin.h \ + pcap-rpcap.h \ + pcap-types.h \ + pcap-usb-linux-common.h \ +@@ -316,6 +320,8 @@ EXTRA_DIST = \ + pcap-netmap.h \ + pcap-npf.c \ + pcap-null.c \ ++ pcap-plugin.c \ ++ pcap-plugin.h \ + pcap-rdmasniff.c \ + pcap-rdmasniff.h \ + pcap-rpcap.c \ +diff --git a/cmakeconfig.h.in b/cmakeconfig.h.in +index 900ce913ae47..0c750bcf247c 100644 +--- a/cmakeconfig.h.in ++++ b/cmakeconfig.h.in +@@ -42,6 +42,15 @@ + /* Define to 1 if fseeko (and presumably ftello) exists and is declared. */ + #cmakedefine HAVE_FSEEKO 1 + ++/* Define to 1 if you have the `secure_getenv' function. */ ++#cmakedefine HAVE_SECURE_GETENV 1 ++ ++/* Define to 1 if you have the `issetugid' function. */ ++#cmakedefine HAVE_ISSETUGID 1 ++ ++/* Define to 1 if you have the `getauxval' function. */ ++#cmakedefine HAVE_GETAUXVAL 1 ++ + /* Define to 1 if you have the `getspnam' function. */ + #cmakedefine HAVE_GETSPNAM 1 + +diff --git a/configure.ac b/configure.ac +index cf6846d7164e..d075325a352e 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -2999,6 +2999,30 @@ if test "x$enable_rdma" != "xno"; then + AC_SUBST(PCAP_SUPPORT_RDMASNIFF) + fi + ++# ++# Plugin loader — always compiled, discovers pcap-*.so at runtime. ++# Needs -ldl on systems where dlopen is in libdl (not needed on ++# FreeBSD, musl, etc. where it is in libc). ++# ++MODULE_C_SRC="$MODULE_C_SRC pcap-plugin.c" ++AC_SEARCH_LIBS([dlopen], [dl]) ++ ++AC_CHECK_FUNCS([secure_getenv issetugid getauxval]) ++ ++# Plugin directory (build-time default). ++# Exposed as plugindir in libpcap.pc so external projects can ++# query it with: pkg-config --variable=plugindir libpcap ++plugindir='${libdir}/pcap/plugins' ++AC_ARG_WITH([plugin-dir], ++ AS_HELP_STRING([--with-plugin-dir=DIR], ++ [plugin directory @<:@LIBDIR/pcap/plugins@:>@]), ++ [ ++ if test "$withval" != no && test "$withval" != yes; then ++ plugindir="$withval" ++ fi ++ ]) ++AC_SUBST(plugindir) ++ + # + # If this is a platform where we need to have the .pc file and + # pcap-config script supply an rpath option to specify the directory +diff --git a/libpcap.pc.in b/libpcap.pc.in +index 629e662ab09e..3ad70e4ecc61 100644 +--- a/libpcap.pc.in ++++ b/libpcap.pc.in +@@ -9,6 +9,7 @@ prefix="@prefix@" + exec_prefix="@exec_prefix@" + includedir="@includedir@" + libdir="@libdir@" ++plugindir="@plugindir@" + + Name: libpcap + Description: Platform-independent network traffic capture library +diff --git a/pcap-int.h b/pcap-int.h +index b1c9c8cba147..ace33b8bc23c 100644 +--- a/pcap-int.h ++++ b/pcap-int.h +@@ -570,10 +570,14 @@ FILE *pcapint_charset_fopen(const char *path, const char *mode); + */ + #ifdef _WIN32 + #define pcap_code_handle_t HMODULE ++#else ++#define pcap_code_handle_t void * ++#endif + + pcap_code_handle_t pcapint_load_code(const char *); + void *pcapint_find_function(pcap_code_handle_t, const char *); +-#endif ++void pcapint_unload_code(pcap_code_handle_t); ++ + + /* + * Internal interfaces for doing user-mode filtering of packets and +diff --git a/pcap-plugin.c b/pcap-plugin.c +new file mode 100644 +index 000000000000..379b9c0fba3b +--- /dev/null ++++ b/pcap-plugin.c +@@ -0,0 +1,428 @@ ++/* ++ * Copyright (c) 2026 Vincent Jardin, Free Mobile, Iliad ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS''AND ++ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE ++ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ++ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE ++ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL ++ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS ++ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ++ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT ++ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY ++ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF ++ * SUCH DAMAGE. ++ */ ++ ++/* ++ * pcap-plugin: generic plugin loader for libpcap capture backends. ++ * ++ * Scans plugin directories for pcap-*.so shared modules, loads each ++ * via pcapint_load_code(), and dispatches findalldevs/create calls to ++ * loaded plugins. ++ * ++ * Security model (modeled after OpenSSL provider loading): ++ * - $PCAP_PLUGIN_DIR is read through secure_getenv() (or equivalent), ++ * so it is automatically ignored under elevated privileges (setuid, ++ * setgid, file capabilities, LSM transitions). ++ * - The hardcoded plugin directory (PCAP_PLUGIN_DIR) is always scanned. ++ * - Filesystem permissions on the plugin directory are the security ++ * boundary, same as PAM, NSS, OpenSSL, and Mesa. ++ * - libpcap does NOT drop privileges itself; that is the app's job. ++ * ++ * Plugin search order: ++ * 1. $PCAP_PLUGIN_DIR (colon-separated, skipped under privilege) ++ * 2. PCAP_PLUGIN_DIR (compile-time default, e.g. /usr/lib/pcap/plugins) ++ */ ++ ++#include ++ ++#ifdef HAVE_SECURE_GETENV ++#ifndef _GNU_SOURCE ++#define _GNU_SOURCE /* for secure_getenv() */ ++#endif ++#endif ++ ++#include ++#include ++#include ++#include ++ ++#ifndef _WIN32 ++#include ++#include ++#include ++ ++#ifdef HAVE_GETAUXVAL ++#include ++#endif ++#endif /* _WIN32 */ ++ ++#include "pcap-int.h" ++#include "pcap-plugin.h" ++#include ++ ++/* ++ * Default plugin directory if not set by the build system. ++ */ ++#ifndef PCAP_PLUGIN_DIR ++#define PCAP_PLUGIN_DIR "/usr/local/lib/pcap/plugins" ++#endif ++ ++#define MAX_PLUGINS 16 ++ ++static struct pcap_plugin *plugins[MAX_PLUGINS]; ++static int n_plugins; ++static int plugins_loaded; ++ ++#ifndef _WIN32 ++/* ++ * Get the value of an environment variable, but only when the process ++ * is NOT running with elevated privileges. Returns NULL if privileged. ++ * ++ * This follows the OpenSSL provider model: secure_getenv() on glibc, ++ * with fallbacks for BSD (issetugid) and other systems (getauxval, ++ * uid/euid comparison). ++ */ ++static const char * ++pcap_secure_getenv(const char *name) ++{ ++#ifdef HAVE_SECURE_GETENV ++ return (secure_getenv(name)); ++#elif defined(HAVE_ISSETUGID) ++ if (issetugid()) ++ return (NULL); ++ return (getenv(name)); ++#elif defined(HAVE_GETAUXVAL) ++ if (getauxval(AT_SECURE)) ++ return (NULL); ++ return (getenv(name)); ++#else ++ if (getuid() != geteuid() || getgid() != getegid()) ++ return (NULL); ++ return (getenv(name)); ++#endif ++} ++ ++static void ++load_plugin(const char *path) ++{ ++ pcap_code_handle_t handle; ++ struct pcap_plugin *p; ++ ++ if (n_plugins >= MAX_PLUGINS) { ++ fprintf(stderr, ++ "libpcap: too many plugins (max %d), skipping \"%s\"\n", ++ MAX_PLUGINS, path); ++ return; ++ } ++ ++ handle = pcapint_load_code(path); ++ if (handle == NULL) { ++ fprintf(stderr, ++ "libpcap: cannot load plugin \"%s\": %s\n", ++ path, dlerror()); ++ return; ++ } ++ ++ p = pcapint_find_function(handle, "pcap_plugin_entry"); ++ if (p == NULL) { ++ fprintf(stderr, ++ "libpcap: plugin \"%s\" has no pcap_plugin_entry " ++ "symbol\n", path); ++ pcapint_unload_code(handle); ++ return; ++ } ++ if (p->abi_version != PCAP_PLUGIN_ABI_VERSION) { ++ fprintf(stderr, ++ "libpcap: plugin \"%s\" ABI version %d != expected %d\n", ++ path, p->abi_version, PCAP_PLUGIN_ABI_VERSION); ++ pcapint_unload_code(handle); ++ return; ++ } ++ if (p->name == NULL || p->create == NULL) { ++ fprintf(stderr, ++ "libpcap: plugin \"%s\" has NULL name or create\n", ++ path); ++ pcapint_unload_code(handle); ++ return; ++ } ++ ++ plugins[n_plugins++] = p; ++ /* ++ * Intentionally leak the dlopen handle: the plugin stays loaded ++ * for the lifetime of the process. ++ */ ++} ++ ++static void ++scan_dir(const char *dirpath) ++{ ++ DIR *d; ++ struct dirent *ent; ++ ++ d = opendir(dirpath); ++ if (d == NULL) ++ return; ++ ++ while ((ent = readdir(d)) != NULL) { ++ const char *name = ent->d_name; ++ size_t len = strlen(name); ++ char path[4096]; ++ ++ /* match pcap-*.so */ ++ if (len < 8) /* strlen("pcap-.so") */ ++ continue; ++ if (strncmp(name, "pcap-", 5) != 0) ++ continue; ++ if (strcmp(name + len - 3, ".so") != 0) ++ continue; ++ ++ snprintf(path, sizeof(path), "%s/%s", dirpath, name); ++ load_plugin(path); ++ } ++ closedir(d); ++} ++ ++static void ++load_plugins(void) ++{ ++ const char *env; ++ ++ if (plugins_loaded) ++ return; ++ plugins_loaded = 1; ++ ++ /* ++ * $PCAP_PLUGIN_DIR is read through pcap_secure_getenv(): ++ * automatically returns NULL under elevated privileges ++ * (setuid, setgid, file capabilities, LSM transitions), ++ * matching how OpenSSL handles OPENSSL_MODULES and how ++ * ld-linux.so handles LD_LIBRARY_PATH. ++ */ ++ env = pcap_secure_getenv("PCAP_PLUGIN_DIR"); ++ if (env != NULL && env[0] != '\0') { ++ char *dirs, *dir, *saveptr; ++ ++ dirs = strdup(env); ++ if (dirs != NULL) { ++ for (dir = strtok_r(dirs, ":", &saveptr); ++ dir != NULL; ++ dir = strtok_r(NULL, ":", &saveptr)) { ++ scan_dir(dir); ++ } ++ free(dirs); ++ } ++ } ++ ++ scan_dir(PCAP_PLUGIN_DIR); ++} ++#else /* _WIN32 */ ++static void ++load_plugins(void) ++{ ++ /* ++ * Windows plugin loading: not yet implemented. ++ * ++ * When implemented, this would use FindFirstFileA/FindNextFileA ++ * to scan a plugin directory for pcap-*.dll files, then call ++ * pcapint_load_code() and pcapint_find_function() for each. ++ * ++ * Note: the existing pcapint_load_code() on Windows prepends ++ * GetSystemDirectoryA(), which is not suitable for plugins in ++ * custom directories. A future implementation would need a ++ * variant that takes an absolute path. ++ */ ++} ++#endif /* _WIN32 */ ++ ++pcap_t * ++pcap_plugin_dispatch_create(const char *device, char *errbuf, int *is_ours) ++{ ++ int i; ++ ++ load_plugins(); ++ ++ for (i = 0; i < n_plugins; i++) { ++ pcap_t *p = plugins[i]->create(device, errbuf, is_ours); ++ if (*is_ours) ++ return p; ++ } ++ ++ *is_ours = 0; ++ return NULL; ++} ++ ++int ++pcap_plugin_dispatch_findalldevs(pcap_if_list_t *devlistp, char *errbuf) ++{ ++ int i; ++ ++ load_plugins(); ++ ++ for (i = 0; i < n_plugins; i++) { ++ if (plugins[i]->findalldevs == NULL) ++ continue; ++ if (plugins[i]->findalldevs(devlistp, errbuf) == -1) ++ return -1; ++ } ++ return 0; ++} ++ ++/* ++ * Plugin helper functions — exported wrappers around internal ++ * libpcap functions and struct pcap accessors. These have PCAP_API ++ * (default visibility) so plugins can call them from dlopen'd .so ++ * modules without needing pcap-int.h. ++ */ ++ ++pcap_t * ++pcap_plugin_create_handle(char *errbuf, size_t priv_size) ++{ ++ return pcapint_create_common(errbuf, ++ sizeof(pcap_t) + priv_size, sizeof(pcap_t)); ++} ++ ++void * ++pcap_plugin_priv(pcap_t *p) ++{ ++ return p->priv; ++} ++ ++void ++pcap_plugin_set_activate(pcap_t *p, int (*activate_op)(pcap_t *)) ++{ ++ p->activate_op = activate_op; ++} ++ ++void ++pcap_plugin_set_ops(pcap_t *p, const struct pcap_plugin_ops *ops) ++{ ++ if (ops->read != NULL) ++ p->read_op = ops->read; ++ if (ops->inject != NULL) ++ p->inject_op = ops->inject; ++ if (ops->setfilter != NULL) ++ p->setfilter_op = ops->setfilter; ++ if (ops->setdirection != NULL) ++ p->setdirection_op = ops->setdirection; ++ if (ops->set_datalink != NULL) ++ p->set_datalink_op = ops->set_datalink; ++ if (ops->getnonblock != NULL) ++ p->getnonblock_op = ops->getnonblock; ++ if (ops->setnonblock != NULL) ++ p->setnonblock_op = ops->setnonblock; ++ if (ops->stats != NULL) ++ p->stats_op = ops->stats; ++ if (ops->cleanup != NULL) ++ p->cleanup_op = ops->cleanup; ++ if (ops->breakloop_func != NULL) ++ p->breakloop_op = ops->breakloop_func; ++} ++ ++void ++pcap_plugin_set_linktype(pcap_t *p, int linktype) ++{ ++ p->linktype = linktype; ++} ++ ++void ++pcap_plugin_set_snapshot(pcap_t *p, int snaplen) ++{ ++ p->snapshot = snaplen; ++} ++ ++void ++pcap_plugin_set_select_timeout(pcap_t *p, struct timeval *tv) ++{ ++#ifndef _WIN32 ++ p->required_select_timeout = tv; ++#endif ++} ++ ++const char * ++pcap_plugin_get_device(pcap_t *p) ++{ ++ return p->opt.device; ++} ++ ++int ++pcap_plugin_get_snapshot(pcap_t *p) ++{ ++ return p->snapshot; ++} ++ ++int ++pcap_plugin_get_timeout(pcap_t *p) ++{ ++ return p->opt.timeout; ++} ++ ++int ++pcap_plugin_check_break_loop(pcap_t *p) ++{ ++ if (p->break_loop) { ++ p->break_loop = 0; ++ return 1; ++ } ++ return 0; ++} ++ ++struct bpf_insn * ++pcap_plugin_get_filter(pcap_t *p) ++{ ++ return p->fcode.bf_insns; ++} ++ ++void ++pcap_plugin_set_errbuf(pcap_t *p, const char *fmt, ...) ++{ ++ va_list ap; ++ ++ va_start(ap, fmt); ++ vsnprintf(p->errbuf, PCAP_ERRBUF_SIZE, fmt, ap); ++ va_end(ap); ++} ++ ++void ++pcap_plugin_cleanup_live(pcap_t *p) ++{ ++ pcapint_cleanup_live_common(p); ++} ++ ++void ++pcap_plugin_breakloop(pcap_t *p) ++{ ++ pcapint_breakloop_common(p); ++} ++ ++int ++pcap_plugin_install_bpf(pcap_t *p, struct bpf_program *fp) ++{ ++ return pcapint_install_bpf_program(p, fp); ++} ++ ++unsigned int ++pcap_plugin_filter(const struct bpf_insn *pc, const unsigned char *pkt, ++ unsigned int wirelen, unsigned int caplen) ++{ ++ return pcapint_filter(pc, pkt, wirelen, caplen); ++} ++ ++pcap_if_t * ++pcap_plugin_add_dev(pcap_if_list_t *devlistp, const char *name, ++ unsigned int flags, const char *description, char *errbuf) ++{ ++ return pcapint_add_dev(devlistp, name, (bpf_u_int32)flags, ++ description, errbuf); ++} +diff --git a/pcap-plugin.h b/pcap-plugin.h +new file mode 100644 +index 000000000000..a1a6169fd3ee +--- /dev/null ++++ b/pcap-plugin.h +@@ -0,0 +1,28 @@ ++/* ++ * Copyright (c) 2026 Vincent Jardin, Free Mobile, Iliad ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS''AND ++ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE ++ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ++ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE ++ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL ++ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS ++ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ++ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT ++ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY ++ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF ++ * SUCH DAMAGE. ++ */ ++ ++pcap_t *pcap_plugin_dispatch_create(const char *, char *, int *); ++int pcap_plugin_dispatch_findalldevs(pcap_if_list_t *, char *); +diff --git a/pcap.c b/pcap.c +index e47d591abc6c..860d855c8b67 100644 +--- a/pcap.c ++++ b/pcap.c +@@ -124,6 +124,8 @@ + #include "pcap-dpdk.h" + #endif + ++#include "pcap-plugin.h" ++ + #ifdef ENABLE_REMOTE + #include "pcap-rpcap.h" + #endif +@@ -657,6 +659,7 @@ static struct capture_source_type { + #ifdef PCAP_SUPPORT_RDMASNIFF + { rdmasniff_findalldevs, rdmasniff_create }, + #endif ++ { pcap_plugin_dispatch_findalldevs, pcap_plugin_dispatch_create }, + #ifdef PCAP_SUPPORT_DPDK + { pcap_dpdk_findalldevs, pcap_dpdk_create }, + #endif +@@ -4339,7 +4342,6 @@ pcap_close(pcap_t *p) + + /* + * Helpers for safely loading code at run time. +- * Currently Windows-only. + */ + #ifdef _WIN32 + // +@@ -4423,7 +4425,33 @@ pcapint_find_function(pcap_code_handle_t code, const char *func) + { + return ((void *)GetProcAddress(code, func)); + } +-#endif ++ ++void ++pcapint_unload_code(pcap_code_handle_t code) ++{ ++ FreeLibrary(code); ++} ++#else /* _WIN32 */ ++#include ++ ++pcap_code_handle_t ++pcapint_load_code(const char *path) ++{ ++ return (dlopen(path, RTLD_NOW)); ++} ++ ++void * ++pcapint_find_function(pcap_code_handle_t code, const char *func) ++{ ++ return (dlsym(code, func)); ++} ++ ++void ++pcapint_unload_code(pcap_code_handle_t code) ++{ ++ dlclose(code); ++} ++#endif /* _WIN32 */ + + /* + * Given a BPF program, a pcap_pkthdr structure for a packet, and the raw +diff --git a/pcap/pcap-plugin.h b/pcap/pcap-plugin.h +new file mode 100644 +index 000000000000..5d5bad6472bc +--- /dev/null ++++ b/pcap/pcap-plugin.h +@@ -0,0 +1,228 @@ ++/* ++ * Copyright (c) 2026 Vincent Jardin, Free Mobile, Iliad ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS''AND ++ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE ++ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ++ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE ++ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL ++ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS ++ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ++ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT ++ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY ++ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF ++ * SUCH DAMAGE. ++ */ ++ ++#ifndef pcap_plugin_h ++#define pcap_plugin_h ++ ++/* ++ * Public plugin ABI for libpcap capture backends. ++ * ++ * External projects can build shared modules (pcap-*.so) that libpcap ++ * discovers and loads at runtime via dlopen(). This avoids compiling ++ * backend-specific code into libpcap itself. ++ * ++ * Each plugin .so exports a single symbol "pcap_plugin_entry" of type ++ * struct pcap_plugin. The loader (pcap-plugin.c) scans plugin directories, ++ * dlopen's each pcap-*.so, and dispatches findalldevs/create calls. ++ * ++ * Plugins link against libpcap (-lpcap) and use the pcap_plugin_* ++ * accessor/helper functions declared below instead of accessing the ++ * pcap_t struct directly (which would require pcap-int.h). All ++ * pcap_plugin_* functions have default visibility and are safe to call ++ * from dlopen'd modules. ++ */ ++ ++#include ++ ++/* ++ * pcap_if_list_t is defined in pcap-int.h (not public). ++ * Forward-declare it here so plugins can use the findalldevs callback ++ * and pcap_plugin_add_dev() without including pcap-int.h. ++ * ++ * Guard against duplicate typedef when pcap-int.h is already included ++ * (C99 does not allow duplicate typedefs; C11 does, but some compilers ++ * reject it with -Werror,-Wtypedef-redefinition). ++ */ ++#ifndef pcap_int_h ++struct pcap_if_list; ++typedef struct pcap_if_list pcap_if_list_t; ++#endif ++ ++#define PCAP_PLUGIN_ABI_VERSION 1 ++ ++/* ++ * Maximum snapshot length. Same value as MAXIMUM_SNAPLEN in pcap-int.h. ++ * Plugins should clamp snapshots to this value. ++ */ ++#define PCAP_PLUGIN_SNAPLEN_MAX 262144 ++ ++/* ++ * Each plugin .so exports one instance of this struct as the symbol ++ * "pcap_plugin_entry". ++ */ ++struct pcap_plugin { ++ int abi_version; /* must be PCAP_PLUGIN_ABI_VERSION */ ++ const char *name; /* short name, e.g. "grout" */ ++ int (*findalldevs)(pcap_if_list_t *, char *); ++ pcap_t *(*create)(const char *device, char *errbuf, int *is_ours); ++}; ++ ++/* ++ * Function pointer types for pcap operations. ++ * These match the types used internally by libpcap. ++ */ ++typedef int (*pcap_plugin_read_op)(pcap_t *, int, pcap_handler, u_char *); ++typedef int (*pcap_plugin_inject_op)(pcap_t *, const void *, int); ++typedef int (*pcap_plugin_setfilter_op)(pcap_t *, struct bpf_program *); ++typedef int (*pcap_plugin_setdirection_op)(pcap_t *, pcap_direction_t); ++typedef int (*pcap_plugin_set_datalink_op)(pcap_t *, int); ++typedef int (*pcap_plugin_getnonblock_op)(pcap_t *); ++typedef int (*pcap_plugin_setnonblock_op)(pcap_t *, int); ++typedef int (*pcap_plugin_stats_op)(pcap_t *, struct pcap_stat *); ++typedef void (*pcap_plugin_cleanup_op)(pcap_t *); ++typedef void (*pcap_plugin_breakloop_op)(pcap_t *); ++ ++/* ++ * Capture backend operations. Set all applicable fields, then call ++ * pcap_plugin_set_ops() during activate. ++ */ ++struct pcap_plugin_ops { ++ pcap_plugin_read_op read; ++ pcap_plugin_inject_op inject; ++ pcap_plugin_setfilter_op setfilter; ++ pcap_plugin_setdirection_op setdirection; ++ pcap_plugin_set_datalink_op set_datalink; ++ pcap_plugin_getnonblock_op getnonblock; ++ pcap_plugin_setnonblock_op setnonblock; ++ pcap_plugin_stats_op stats; ++ pcap_plugin_cleanup_op cleanup; ++ pcap_plugin_breakloop_op breakloop_func; ++}; ++ ++/* ---- Handle allocation ---- */ ++ ++/* ++ * Allocate a pcap_t with space for priv_size bytes of private data. ++ * Returns NULL on failure (errbuf filled in). ++ */ ++PCAP_API pcap_t *pcap_plugin_create_handle(char *errbuf, size_t priv_size); ++ ++/* ++ * Get the private data pointer from a pcap_t. ++ */ ++PCAP_API void *pcap_plugin_priv(pcap_t *p); ++ ++/* ---- Handle configuration (call during create/activate) ---- */ ++ ++/* ++ * Set the activate callback. Call from your create function. ++ */ ++PCAP_API void pcap_plugin_set_activate(pcap_t *p, ++ int (*activate_op)(pcap_t *)); ++ ++/* ++ * Install all capture operations at once. Call from your activate ++ * function. NULL entries are left unchanged (libpcap defaults). ++ */ ++PCAP_API void pcap_plugin_set_ops(pcap_t *p, ++ const struct pcap_plugin_ops *ops); ++ ++/* ++ * Set the link-layer type (e.g. DLT_EN10MB). Call from activate. ++ */ ++PCAP_API void pcap_plugin_set_linktype(pcap_t *p, int linktype); ++ ++/* ++ * Set the snapshot length. Call from activate if you need to override ++ * the user-requested value. ++ */ ++PCAP_API void pcap_plugin_set_snapshot(pcap_t *p, int snaplen); ++ ++/* ++ * Set the required select timeout for poll-based plugins. ++ * The pointer must remain valid for the lifetime of the pcap_t. ++ */ ++PCAP_API void pcap_plugin_set_select_timeout(pcap_t *p, struct timeval *tv); ++ ++/* ---- Handle accessors (call during dispatch/activate) ---- */ ++ ++/* ++ * Get the device name string (e.g. "grout:p0"). ++ */ ++PCAP_API const char *pcap_plugin_get_device(pcap_t *p); ++ ++/* ++ * Get the current snapshot length. ++ */ ++PCAP_API int pcap_plugin_get_snapshot(pcap_t *p); ++ ++/* ++ * Get the read timeout in milliseconds (0 = no timeout). ++ */ ++PCAP_API int pcap_plugin_get_timeout(pcap_t *p); ++ ++/* ++ * Check and clear the break_loop flag. Returns nonzero if a break ++ * was requested. Plugins should call this in their dispatch loop. ++ */ ++PCAP_API int pcap_plugin_check_break_loop(pcap_t *p); ++ ++/* ++ * Get the compiled BPF filter instructions, or NULL if no filter ++ * is installed. For plugins that do in-kernel/hardware filtering, ++ * this is the fallback software filter. ++ */ ++PCAP_API struct bpf_insn *pcap_plugin_get_filter(pcap_t *p); ++ ++/* ++ * Format an error message into the pcap_t's error buffer. ++ */ ++PCAP_API void pcap_plugin_set_errbuf(pcap_t *p, ++ PCAP_FORMAT_STRING(const char *fmt), ...) PCAP_PRINTFLIKE(2, 3); ++ ++/* ---- Helper functions ---- */ ++ ++/* ++ * Common cleanup for live captures. Call from your cleanup_op. ++ */ ++PCAP_API void pcap_plugin_cleanup_live(pcap_t *p); ++ ++/* ++ * Standard breakloop implementation. Use as breakloop_func in ops, ++ * or call from your own breakloop. ++ */ ++PCAP_API void pcap_plugin_breakloop(pcap_t *p); ++ ++/* ++ * Install a BPF filter program (deep copy). Call from your setfilter_op ++ * to install the filter locally for pcap_plugin_filter() fallback. ++ */ ++PCAP_API int pcap_plugin_install_bpf(pcap_t *p, struct bpf_program *fp); ++ ++/* ++ * Run a BPF filter on a packet. Returns nonzero if the packet matches. ++ */ ++PCAP_API unsigned int pcap_plugin_filter(const struct bpf_insn *pc, ++ const unsigned char *pkt, unsigned int wirelen, unsigned int caplen); ++ ++/* ++ * Add a device entry to a device list (for findalldevs). ++ */ ++PCAP_API pcap_if_t *pcap_plugin_add_dev(pcap_if_list_t *devlistp, ++ const char *name, unsigned int flags, const char *description, ++ char *errbuf); ++ ++#endif /* pcap_plugin_h */ +-- +2.53.0 + diff --git a/subprojects/packagefiles/libpcap/0002-plugin-expose-timestamp-type-accessors-for-adapter-c.patch b/subprojects/packagefiles/libpcap/0002-plugin-expose-timestamp-type-accessors-for-adapter-c.patch new file mode 100644 index 000000000..3afe9695a --- /dev/null +++ b/subprojects/packagefiles/libpcap/0002-plugin-expose-timestamp-type-accessors-for-adapter-c.patch @@ -0,0 +1,102 @@ +From 0840e4a6d030739fb875fbc42528a7c3e4af29d8 Mon Sep 17 00:00:00 2001 +From: Vincent Jardin +Date: Sun, 22 Mar 2026 17:00:44 +0100 +Subject: [PATCH 2/3] plugin: expose timestamp type accessors for adapter clock + support +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Telecom-grade dataplanes (radio-based routers) +need nanosecond-accurate timestamps to measure inter-packet jitter and +verify hardware pacing. Default host timestamps (software clock at +packet delivery) give microsecond-level accuracy at best, which is +insufficient for sub-microsecond scheduling verification on 10G+ links. + +Modern NICs (ConnectX-6 Dx, ConnectX-7) stamp each received packet +with a hardware clock at wire arrival time. The kernel exposes this +via AF_PACKET's PACKET_TIMESTAMP / SOF_TIMESTAMPING_RAW_HARDWARE, +and tcpdump accesses it with -j adapter. External capture plugins +need the same mechanism. + +Add two accessors to the plugin API: + + pcap_plugin_get_tstamp_type() — read the type the user requested + pcap_plugin_set_tstamp_type_list() — advertise supported types + +Plugins call set_tstamp_type_list during create so that +pcap_list_tstamp_types (tcpdump -J) reports the available clock +sources. During activate, get_tstamp_type tells the plugin whether +the user asked for PCAP_TSTAMP_ADAPTER (NIC clock, synced to +real-time), PCAP_TSTAMP_ADAPTER_UNSYNCED (raw NIC clock), or +PCAP_TSTAMP_HOST (default software clock). The plugin then configures +its backend accordingly. + +Signed-off-by: Vincent Jardin +--- + pcap-plugin.c | 22 ++++++++++++++++++++++ + pcap/pcap-plugin.h | 15 +++++++++++++++ + 2 files changed, 37 insertions(+) + +diff --git a/pcap-plugin.c b/pcap-plugin.c +index 379b9c0fba3b..f9af9761beff 100644 +--- a/pcap-plugin.c ++++ b/pcap-plugin.c +@@ -419,6 +419,28 @@ pcap_plugin_filter(const struct bpf_insn *pc, const unsigned char *pkt, + return pcapint_filter(pc, pkt, wirelen, caplen); + } + ++int ++pcap_plugin_get_tstamp_type(pcap_t *p) ++{ ++ return p->opt.tstamp_type; ++} ++ ++int ++pcap_plugin_set_tstamp_type_list(pcap_t *p, const int *types, int count) ++{ ++ u_int *list; ++ ++ list = malloc(count * sizeof(u_int)); ++ if (list == NULL) ++ return -1; ++ for (int i = 0; i < count; i++) ++ list[i] = (u_int)types[i]; ++ free(p->tstamp_type_list); ++ p->tstamp_type_list = list; ++ p->tstamp_type_count = count; ++ return 0; ++} ++ + pcap_if_t * + pcap_plugin_add_dev(pcap_if_list_t *devlistp, const char *name, + unsigned int flags, const char *description, char *errbuf) +diff --git a/pcap/pcap-plugin.h b/pcap/pcap-plugin.h +index 5d5bad6472bc..01168e75ada0 100644 +--- a/pcap/pcap-plugin.h ++++ b/pcap/pcap-plugin.h +@@ -193,6 +193,21 @@ PCAP_API struct bpf_insn *pcap_plugin_get_filter(pcap_t *p); + PCAP_API void pcap_plugin_set_errbuf(pcap_t *p, + PCAP_FORMAT_STRING(const char *fmt), ...) PCAP_PRINTFLIKE(2, 3); + ++/* ++ * Get the timestamp type requested by the user (e.g. PCAP_TSTAMP_ADAPTER). ++ * Returns PCAP_TSTAMP_HOST if none was explicitly set. ++ * Call during activate to decide which clock source to use. ++ */ ++PCAP_API int pcap_plugin_get_tstamp_type(pcap_t *p); ++ ++/* ++ * Advertise supported timestamp types to libpcap. Call during create ++ * (before activate) so pcap_list_tstamp_types() works. The types ++ * array is copied. Returns 0 on success, -1 on allocation failure. ++ */ ++PCAP_API int pcap_plugin_set_tstamp_type_list(pcap_t *p, ++ const int *types, int count); ++ + /* ---- Helper functions ---- */ + + /* +-- +2.53.0 + diff --git a/subprojects/packagefiles/libpcap/0003-plugin-complete-accessor-API-for-tcpdump-feature-par.patch b/subprojects/packagefiles/libpcap/0003-plugin-complete-accessor-API-for-tcpdump-feature-par.patch new file mode 100644 index 000000000..f76d3da0f --- /dev/null +++ b/subprojects/packagefiles/libpcap/0003-plugin-complete-accessor-API-for-tcpdump-feature-par.patch @@ -0,0 +1,196 @@ +From a4588b1089db8f84d786692fd312f7b2fac665ad Mon Sep 17 00:00:00 2001 +From: Vincent Jardin +Date: Sun, 22 Mar 2026 17:27:06 +0100 +Subject: [PATCH 3/3] plugin: complete accessor API for tcpdump feature parity + +The plugin API lacked accessors for several pcap_t options that +tcpdump (and other consumers) set before pcap_activate(): +promiscuous mode, buffer size, immediate mode, and timestamp +precision. Without these, a plugin backend could not honor +user-requested capture settings. + +Similarly, plugins had no way to advertise supported timestamp +precisions or data link types back to libpcap, so +pcap_list_tstamp_precisions() and pcap_list_datalinks() would +return empty results for plugin-backed devices. Plugins also +could not provide a selectable file descriptor for event-driven +callers. + +New getters (plugin reads during activate): + pcap_plugin_get_promisc() + pcap_plugin_get_buffer_size() + pcap_plugin_get_immediate() + pcap_plugin_get_tstamp_precision() + +New setters (plugin advertises capabilities): + pcap_plugin_set_tstamp_precision_list() + pcap_plugin_set_datalink_list() + pcap_plugin_set_selectable_fd() + +Signed-off-by: Vincent Jardin +--- + pcap-plugin.c | 65 ++++++++++++++++++++++++++++++++++++++++++++++ + pcap/pcap-plugin.h | 53 ++++++++++++++++++++++++++++++++++++- + 2 files changed, 117 insertions(+), 1 deletion(-) + +diff --git a/pcap-plugin.c b/pcap-plugin.c +index f9af9761beff..a4e2820e6b76 100644 +--- a/pcap-plugin.c ++++ b/pcap-plugin.c +@@ -425,6 +425,30 @@ pcap_plugin_get_tstamp_type(pcap_t *p) + return p->opt.tstamp_type; + } + ++int ++pcap_plugin_get_tstamp_precision(pcap_t *p) ++{ ++ return p->opt.tstamp_precision; ++} ++ ++int ++pcap_plugin_get_promisc(pcap_t *p) ++{ ++ return p->opt.promisc; ++} ++ ++int ++pcap_plugin_get_buffer_size(pcap_t *p) ++{ ++ return (int)p->opt.buffer_size; ++} ++ ++int ++pcap_plugin_get_immediate(pcap_t *p) ++{ ++ return p->opt.immediate; ++} ++ + int + pcap_plugin_set_tstamp_type_list(pcap_t *p, const int *types, int count) + { +@@ -441,6 +465,47 @@ pcap_plugin_set_tstamp_type_list(pcap_t *p, const int *types, int count) + return 0; + } + ++int ++pcap_plugin_set_tstamp_precision_list(pcap_t *p, const int *precisions, ++ int count) ++{ ++ u_int *list; ++ ++ list = malloc(count * sizeof(u_int)); ++ if (list == NULL) ++ return -1; ++ for (int i = 0; i < count; i++) ++ list[i] = (u_int)precisions[i]; ++ free(p->tstamp_precision_list); ++ p->tstamp_precision_list = list; ++ p->tstamp_precision_count = count; ++ return 0; ++} ++ ++int ++pcap_plugin_set_datalink_list(pcap_t *p, const int *dlts, int count) ++{ ++ u_int *list; ++ ++ list = malloc(count * sizeof(u_int)); ++ if (list == NULL) ++ return -1; ++ for (int i = 0; i < count; i++) ++ list[i] = (u_int)dlts[i]; ++ free(p->dlt_list); ++ p->dlt_list = list; ++ p->dlt_count = count; ++ return 0; ++} ++ ++void ++pcap_plugin_set_selectable_fd(pcap_t *p, int fd) ++{ ++#ifndef _WIN32 ++ p->selectable_fd = fd; ++#endif ++} ++ + pcap_if_t * + pcap_plugin_add_dev(pcap_if_list_t *devlistp, const char *name, + unsigned int flags, const char *description, char *errbuf) +diff --git a/pcap/pcap-plugin.h b/pcap/pcap-plugin.h +index 01168e75ada0..d5c0865705f6 100644 +--- a/pcap/pcap-plugin.h ++++ b/pcap/pcap-plugin.h +@@ -195,11 +195,37 @@ PCAP_API void pcap_plugin_set_errbuf(pcap_t *p, + + /* + * Get the timestamp type requested by the user (e.g. PCAP_TSTAMP_ADAPTER). +- * Returns PCAP_TSTAMP_HOST if none was explicitly set. ++ * Returns -1 (not set) if none was explicitly requested. + * Call during activate to decide which clock source to use. + */ + PCAP_API int pcap_plugin_get_tstamp_type(pcap_t *p); + ++/* ++ * Get the timestamp precision requested by the user. ++ * Returns PCAP_TSTAMP_PRECISION_MICRO (default) or ++ * PCAP_TSTAMP_PRECISION_NANO. Call during activate to decide ++ * whether to provide nanosecond timestamps. ++ */ ++PCAP_API int pcap_plugin_get_tstamp_precision(pcap_t *p); ++ ++/* ++ * Get the promiscuous mode flag (nonzero = user requested promisc). ++ * Call during activate. ++ */ ++PCAP_API int pcap_plugin_get_promisc(pcap_t *p); ++ ++/* ++ * Get the buffer size requested by the user (0 = platform default). ++ * Call during activate to size ring buffers or mempools. ++ */ ++PCAP_API int pcap_plugin_get_buffer_size(pcap_t *p); ++ ++/* ++ * Get the immediate mode flag (nonzero = deliver packets ASAP, ++ * don't wait to fill a buffer). Call during activate. ++ */ ++PCAP_API int pcap_plugin_get_immediate(pcap_t *p); ++ + /* + * Advertise supported timestamp types to libpcap. Call during create + * (before activate) so pcap_list_tstamp_types() works. The types +@@ -208,6 +234,31 @@ PCAP_API int pcap_plugin_get_tstamp_type(pcap_t *p); + PCAP_API int pcap_plugin_set_tstamp_type_list(pcap_t *p, + const int *types, int count); + ++/* ++ * Advertise supported timestamp precisions. Call during create ++ * so pcap_list_tstamp_precisions() works. The array is copied. ++ * Returns 0 on success, -1 on allocation failure. ++ */ ++PCAP_API int pcap_plugin_set_tstamp_precision_list(pcap_t *p, ++ const int *precisions, int count); ++ ++/* ++ * Advertise supported data link types. Call during activate ++ * so pcap_list_datalinks() works. The array is copied. ++ * Returns 0 on success, -1 on allocation failure. ++ */ ++PCAP_API int pcap_plugin_set_datalink_list(pcap_t *p, ++ const int *dlts, int count); ++ ++/* ++ * Set the selectable file descriptor for poll/select/epoll. ++ * Plugins that can provide a pollable fd (e.g. eventfd, unix socket) ++ * should call this during activate. Set to -1 if not pollable ++ * (then also set a required_select_timeout via ++ * pcap_plugin_set_select_timeout). ++ */ ++PCAP_API void pcap_plugin_set_selectable_fd(pcap_t *p, int fd); ++ + /* ---- Helper functions ---- */ + + /* +-- +2.53.0 + diff --git a/subprojects/packagefiles/libpcap/meson.build b/subprojects/packagefiles/libpcap/meson.build new file mode 100644 index 000000000..65ac9e4f8 --- /dev/null +++ b/subprojects/packagefiles/libpcap/meson.build @@ -0,0 +1,978 @@ +project('libpcap', 'c', + version: run_command('sed', '-n', '1p', 'VERSION', check: true).stdout().strip(), + license: 'BSD-3-Clause', + default_options: ['c_std=gnu99'], + meson_version: '>= 0.63.0', +) + +pkgconfig = import('pkgconfig') +cc = meson.get_compiler('c') +system = host_machine.system() + +# +# Version parsing. +# +version = meson.project_version() +version_parts = version.split('.') +version_major = version_parts[0] +version_minor = version_parts[1] +# Strip any suffix like -PRE-GIT from the patch component. +version_patch = version_parts[2].split('-')[0] +lib_version = '@0@.@1@.@2@'.format(version_major, version_minor, version_patch) + +# +# Configuration data for generated config.h. +# +cdata = configuration_data() +cdata.set_quoted('PACKAGE_VERSION', version) +cdata.set_quoted('PACKAGE_NAME', 'pcap') +cdata.set_quoted('PACKAGE_STRING', 'pcap ' + version) +cdata.set_quoted('PACKAGE_TARNAME', 'libpcap') +cdata.set_quoted('PACKAGE_URL', 'https://www.tcpdump.org/') +cdata.set_quoted('PACKAGE_BUGREPORT', + 'https://github.com/the-tcpdump-group/libpcap/issues') +cdata.set('SIZEOF_VOID_P', cc.sizeof('void *')) +cdata.set('SIZEOF_TIME_T', cc.sizeof('time_t', prefix: '#include ')) + +# +# Options. +# +capture_type = get_option('capture_type') +opt_remote = get_option('remote') +plugin_dir = get_option('plugin_dir') +if plugin_dir == '' + plugin_dir = get_option('prefix') / get_option('libdir') / 'pcap' / 'plugins' +endif + +if not get_option('protochain') + cdata.set('NO_PROTOCHAIN', 1) +endif +if get_option('yydebug') + cdata.set('YYDEBUG', 1) +endif +if get_option('optimizer_debug') + cdata.set('BDEBUG', 1) +endif +if opt_remote + cdata.set('ENABLE_REMOTE', 1) +endif + +# +# Large file support. +# +if system != 'windows' + add_project_arguments('-D_FILE_OFFSET_BITS=64', language: 'c') + if cc.has_function('fseeko', + prefix: '#define _LARGEFILE_SOURCE\n#include ') + cdata.set('HAVE_FSEEKO', 1) + add_project_arguments('-D_LARGEFILE_SOURCE', language: 'c') + endif +endif + +# +# Header checks. +# +_header_checks = { + 'unistd.h': 'HAVE_UNISTD_H', + 'net/if_media.h': 'HAVE_NET_IF_MEDIA_H', + 'sys/ioccom.h': 'HAVE_SYS_IOCCOM_H', + 'sys/bufmod.h': 'HAVE_SYS_BUFMOD_H', + 'sys/dlpi.h': 'HAVE_SYS_DLPI_H', + 'sys/dlpi_ext.h': 'HAVE_SYS_DLPI_EXT_H', + 'machine/atomic.h': 'HAVE_MACHINE_ATOMIC_H', +} +foreach h, define : _header_checks + if cc.has_header(h) + cdata.set(define, 1) + endif +endforeach + +have_unistd = cdata.has('HAVE_UNISTD_H') +extra_cflags = [] +if not have_unistd + extra_cflags += ['-DYY_NO_UNISTD_H'] +endif + +# +# Function checks. +# +have_strlcpy = cc.has_function('strlcpy') +have_strlcat = cc.has_function('strlcat') +have_strtok_r = cc.has_function('strtok_r') +have_asprintf = cc.has_function('asprintf') +have_vasprintf = cc.has_function('vasprintf') + +_func_checks = { + 'strlcpy': have_strlcpy, + 'strlcat': have_strlcat, + 'strtok_r': have_strtok_r, + 'asprintf': have_asprintf, + 'vasprintf': have_vasprintf, + 'snprintf': cc.has_function('snprintf'), + 'vsnprintf': cc.has_function('vsnprintf'), + 'secure_getenv': cc.has_function('secure_getenv'), + 'issetugid': cc.has_function('issetugid'), + 'getauxval': cc.has_function('getauxval'), + 'getspnam': cc.has_function('getspnam'), + 'ether_hostton': cc.has_function('ether_hostton'), +} +foreach f, result : _func_checks + if result + cdata.set('HAVE_@0@'.format(f.to_upper()), 1) + endif +endforeach + +# +# strerror_r variant detection. +# +if cc.has_function('strerror_r') + if cc.compiles(''' + #define _GNU_SOURCE + #include + extern char *strerror_r(int, char *, size_t); + int main(void) { return 0; } + ''', name: 'GNU strerror_r') + cdata.set('HAVE_GNU_STRERROR_R', 1) + else + cdata.set('HAVE_POSIX_STRERROR_R', 1) + endif +endif + +# +# Type and struct member checks. +# +if system != 'windows' + if cc.has_type('socklen_t', prefix: '#include ') + cdata.set('HAVE_SOCKLEN_T', 1) + endif + if cc.has_member('struct sockaddr', 'sa_len', + prefix: '#include ') + cdata.set('HAVE_STRUCT_SOCKADDR_SA_LEN', 1) + endif +endif + +# +# Detect libc variant (glibc, uclibc). +# +if system == 'linux' + if cc.has_header_symbol('features.h', '__GLIBC__') + cdata.set('HAVE_GLIBC', 1) + elif cc.has_header_symbol('features.h', '__UCLIBC__') + cdata.set('HAVE_UCLIBC', 1) + endif +endif + +# +# getnetbyname_r variant detection. +# +if cc.has_header_symbol('netdb.h', 'getnetbyname_r') + if cc.compiles(''' + #include + int main(void) { + struct netent netent_buf; + char buf[1024]; + struct netent *resultp; + int h_errnoval; + return getnetbyname_r((const char *)0, &netent_buf, buf, + sizeof buf, &resultp, &h_errnoval); + } + ''', name: 'Linux getnetbyname_r') + cdata.set('HAVE_LINUX_GETNETBYNAME_R', 1) + elif cc.compiles(''' + #include + int main(void) { + struct netent netent_buf; + char buf[1024]; + return getnetbyname_r((const char *)0, &netent_buf, buf, + (int)sizeof buf) != NULL; + } + ''', name: 'Solaris getnetbyname_r') + cdata.set('HAVE_SOLARIS_GETNETBYNAME_R', 1) + elif cc.compiles(''' + #include + int main(void) { + struct netent netent_buf; + struct netent_data net_data; + return getnetbyname_r((const char *)0, &netent_buf, &net_data); + } + ''', name: 'AIX getnetbyname_r') + cdata.set('HAVE_AIX_GETNETBYNAME_R', 1) + endif +endif + +# +# getprotobyname_r variant detection. +# +if cc.has_header_symbol('netdb.h', 'getprotobyname_r') + if cc.compiles(''' + #include + int main(void) { + struct protoent protoent_buf; + char buf[1024]; + struct protoent *resultp; + return getprotobyname_r((const char *)0, &protoent_buf, buf, + sizeof buf, &resultp); + } + ''', name: 'Linux getprotobyname_r') + cdata.set('HAVE_LINUX_GETPROTOBYNAME_R', 1) + elif cc.compiles(''' + #include + int main(void) { + struct protoent protoent_buf; + char buf[1024]; + return getprotobyname_r((const char *)0, &protoent_buf, buf, + (int)sizeof buf) != NULL; + } + ''', name: 'Solaris getprotobyname_r') + cdata.set('HAVE_SOLARIS_GETPROTOBYNAME_R', 1) + elif cc.compiles(''' + #include + int main(void) { + struct protoent protoent_buf; + struct protoent_data proto_data; + return getprotobyname_r((const char *)0, &protoent_buf, &proto_data); + } + ''', name: 'AIX getprotobyname_r') + cdata.set('HAVE_AIX_GETPROTOBYNAME_R', 1) + endif +endif + +# +# ether_hostton declaration detection. +# +if cc.has_function('ether_hostton') + _ether_hdrs = [ + ['net/ethernet.h', 'NET_ETHERNET_H_DECLARES_ETHER_HOSTTON'], + ['netinet/ether.h', 'NETINET_ETHER_H_DECLARES_ETHER_HOSTTON'], + ['sys/ethernet.h', 'SYS_ETHERNET_H_DECLARES_ETHER_HOSTTON'], + ['arpa/inet.h', 'ARPA_INET_H_DECLARES_ETHER_HOSTTON'], + ] + _have_decl = false + foreach pair : _ether_hdrs + if not _have_decl and cc.has_header_symbol(pair[0], 'ether_hostton', + required: false) + cdata.set(pair[1], 1) + cdata.set('HAVE_DECL_ETHER_HOSTTON', 1) + _have_decl = true + endif + endforeach + if not _have_decl and cc.compiles(''' + #include + #include + #include + #include + #include + void *p = (void *)ether_hostton; + int main(void) { return p != 0; } + ''', name: 'ether_hostton in netinet/if_ether.h') + cdata.set('NETINET_IF_ETHER_H_DECLARES_ETHER_HOSTTON', 1) + cdata.set('HAVE_DECL_ETHER_HOSTTON', 1) + _have_decl = true + endif + if not _have_decl + # Check for struct ether_addr for our own declaration. + if cc.has_type('struct ether_addr', + prefix: '''#include + #include + #include + #include + #include ''') + cdata.set('HAVE_STRUCT_ETHER_ADDR', 1) + endif + endif +endif + +# +# Atomic builtins (needed for Linux memory-mapped capture). +# +if cc.compiles(''' + int main(void) { + int i = 17; + return __atomic_load_n(&i, __ATOMIC_RELAXED); + } +''', name: '__atomic_load_n') + cdata.set('HAVE___ATOMIC_LOAD_N', 1) +endif +if cc.compiles(''' + int main(void) { + int i; + __atomic_store_n(&i, 17, __ATOMIC_RELAXED); + return 0; + } +''', name: '__atomic_store_n') + cdata.set('HAVE___ATOMIC_STORE_N', 1) +endif + +# +# Platform libraries. +# +pcap_deps = [] +pcap_libs_private = [] +requires_private = [] + +dl_dep = cc.find_library('dl', required: false) +if dl_dep.found() + pcap_deps += dl_dep +endif + +if system == 'haiku' + pcap_deps += cc.find_library('bsd') + pcap_deps += cc.find_library('network') +elif system == 'sunos' + _socket = cc.find_library('socket', required: false) + _nsl = cc.find_library('nsl', required: false) + if _socket.found() + pcap_deps += _socket + pcap_libs_private += '-lsocket' + endif + if _nsl.found() + pcap_deps += _nsl + pcap_libs_private += '-lnsl' + endif + cdata.set('HAVE_SOLARIS', 1) +endif + +# +# Capture type auto-detection. +# +if capture_type == '' + if system == 'linux' + capture_type = 'linux' + elif system == 'haiku' + capture_type = 'haiku' + elif system == 'gnu' + capture_type = 'hurd' + elif cc.has_header('net/bpf.h', prefix: '#include ') + _biocsetif_prefix = ''' + #include + #include + #include + #include + ''' + if cdata.has('HAVE_SYS_IOCCOM_H') + _biocsetif_prefix += '#include \n' + endif + _biocsetif_prefix += ''' + #include + #include + ''' + if cc.compiles(_biocsetif_prefix + ''' + int main(void) { int x = BIOCSETIF; return x; } + ''', name: 'BIOCSETIF in net/bpf.h') + capture_type = 'bpf' + endif + endif + if capture_type == '' + if cc.has_header('sys/dlpi.h') + capture_type = 'dlpi' + else + error('No supported packet capture interface found.\n' + + 'Use -Dcapture_type=null for a read-only libpcap.') + endif + endif +endif +message('Packet capture mechanism: ' + capture_type) + +# +# Common source files. +# +common_src = files( + 'bpf_dump.c', + 'bpf_filter.c', + 'bpf_image.c', + 'etherent.c', + 'fmtutils.c', + 'gencode.c', + 'nametoaddr.c', + 'optimize.c', + 'pcap-common.c', + 'pcap-util.c', + 'pcap.c', + 'pcap-new.c', + 'savefile.c', + 'sf-pcap.c', + 'sf-pcapng.c', + 'pcap-plugin.c', +) + +# +# Platform-specific capture source. +# +platform_src = files('pcap-' + capture_type + '.c') + +# +# Missing function fallbacks. +# +if not have_asprintf + common_src += files('missing/asprintf.c') +endif +if not have_strlcat + common_src += files('missing/strlcat.c') +endif +if not have_strlcpy + common_src += files('missing/strlcpy.c') +endif +if not have_strtok_r + common_src += files('missing/strtok_r.c') +endif + +# +# Capture-type-specific extras. +# +if capture_type == 'dlpi' + platform_src += files('dlpisubs.c') + _libdlpi = cc.find_library('dlpi', required: false) + if _libdlpi.found() and cc.has_function('dlpi_walk', dependencies: _libdlpi) + pcap_deps += _libdlpi + pcap_libs_private += '-ldlpi' + # Replace pcap-dlpi.c with pcap-libdlpi.c. + platform_src = files('pcap-libdlpi.c', 'dlpisubs.c') + cdata.set('HAVE_LIBDLPI', 1) + endif + # DLPI passive mode. + if cc.has_type('dl_passive_req_t', + prefix: '#include \n#include ') + cdata.set('HAVE_DL_PASSIVE_REQ_T', 1) + endif +elif capture_type == 'hurd' + _rt = cc.find_library('rt') + pcap_deps += _rt + pcap_libs_private += '-lrt' +elif capture_type == 'bpf' + cdata.set('HAVE_NET_BPF_H', 1) + # BPF_TIMEVAL struct. + _bpf_timeval_prefix = '#include \n' + if cdata.has('HAVE_SYS_IOCCOM_H') + _bpf_timeval_prefix += '#include \n' + endif + _bpf_timeval_prefix += '#include \n' + if cc.has_type('struct BPF_TIMEVAL', prefix: _bpf_timeval_prefix) + cdata.set('HAVE_STRUCT_BPF_TIMEVAL', 1) + endif + if system == 'sunos' + # Solaris "any" device. + if cc.has_header_symbol('inet/ipnet.h', 'IPNET_ANY_LINK') + cdata.set('HAVE_SOLARIS_ANY_DEVICE', 1) + endif + endif + # AIX BPF needs libodm and libcfg. + if system == 'aix' + pcap_deps += cc.find_library('odm') + pcap_deps += cc.find_library('cfg') + pcap_libs_private += ['-lodm', '-lcfg'] + endif +endif + +# +# Find-all-devices method. +# +if capture_type != 'null' + if cc.has_function('getifaddrs') and cc.has_header('ifaddrs.h', + prefix: '#include ') + platform_src += files('fad-getad.c') + elif cc.compiles(''' + #include + #include + #include + int main(void) { ioctl(0, SIOCGLIFCONF, (char *)0); return 0; } + ''', name: 'SIOCGLIFCONF') + platform_src += files('fad-glifc.c') + else + platform_src += files('fad-gifc.c') + endif +endif + +# +# Linux-specific checks. +# +if system == 'linux' + if cc.has_header('linux/net_tstamp.h') + cdata.set('HAVE_LINUX_NET_TSTAMP_H', 1) + endif + + cdata.set10('HAVE_DECL_SKF_AD_VLAN_TAG_PRESENT', + cc.has_header_symbol('linux/filter.h', 'SKF_AD_VLAN_TAG_PRESENT')) + + # Netfilter support. + if cc.compiles(''' + #include + #include + #include + #include + #include + #include + #include + #include + int main(void) { return 0; } + ''', name: 'netfilter support') + cdata.set('PCAP_SUPPORT_NETFILTER', 1) + platform_src += files('pcap-netfilter-linux.c') + endif + + # USB monitoring. + if get_option('usb').allowed() + cdata.set('PCAP_SUPPORT_LINUX_USBMON', 1) + platform_src += files('pcap-usb-linux.c') + _have_linux_compiler_h = cc.has_header('linux/compiler.h') + if _have_linux_compiler_h + cdata.set('HAVE_LINUX_COMPILER_H', 1) + endif + _usbdevfs_prefix = '' + if _have_linux_compiler_h + _usbdevfs_prefix = '#include \n' + endif + if cc.has_header('linux/usbdevice_fs.h', prefix: _usbdevfs_prefix) + cdata.set('HAVE_LINUX_USBDEVICE_FS_H', 1) + if cc.has_member('struct usbdevfs_ctrltransfer', 'bRequestType', + prefix: _usbdevfs_prefix + '#include ') + cdata.set('HAVE_STRUCT_USBDEVFS_CTRLTRANSFER_BREQUESTTYPE', 1) + endif + endif + endif +endif + +# +# HP-UX specific. +# +if system == 'hpux' + if cc.has_member('dl_hp_ppa_info_t', 'dl_module_id_1', + prefix: '''#include + #include + #include ''') + cdata.set('HAVE_DL_HP_PPA_INFO_T_DL_MODULE_ID_1', 1) + endif + cdata.set('HAVE_HPUX10_20_OR_LATER', 1) +endif + +# +# Optional dependencies. +# + +# libnl (Linux netlink support). +if system == 'linux' and get_option('libnl').allowed() + libnl_dep = dependency('libnl-genl-3.0', required: get_option('libnl')) + if libnl_dep.found() + cdata.set('HAVE_LIBNL', 1) + pcap_deps += libnl_dep + requires_private += 'libnl-genl-3.0' + endif +endif + +# Bluetooth (Linux only). +if system == 'linux' and get_option('bluetooth').allowed() + if cc.has_header('bluetooth/bluetooth.h') + cdata.set('PCAP_SUPPORT_BT', 1) + platform_src += files('pcap-bt-linux.c') + if cc.has_member('struct sockaddr_hci', 'hci_channel', + prefix: '#include \n#include ') + cdata.set('HAVE_STRUCT_SOCKADDR_HCI_HCI_CHANNEL', 1) + if cc.compiles(''' + #include + #include + int main(void) { int i = HCI_CHANNEL_MONITOR; return i; } + ''', name: 'HCI_CHANNEL_MONITOR') + cdata.set('PCAP_SUPPORT_BT_MONITOR', 1) + platform_src += files('pcap-bt-monitor-linux.c') + endif + endif + elif get_option('bluetooth').enabled() + error('Bluetooth support requested but bluetooth/bluetooth.h not found') + endif +endif + +# D-Bus. +if get_option('dbus').allowed() + if system == 'darwin' and get_option('dbus').enabled() + error('D-Bus capture support is not available on macOS ' + + '(freedesktop.org bug 74029)') + endif + if system != 'darwin' + dbus_dep = dependency('dbus-1', required: get_option('dbus')) + if dbus_dep.found() + cdata.set('PCAP_SUPPORT_DBUS', 1) + pcap_deps += dbus_dep + requires_private += 'dbus-1' + platform_src += files('pcap-dbus.c') + endif + endif +endif + +# RDMA (Linux only). +if system == 'linux' and get_option('rdma').allowed() + libibverbs_dep = dependency('libibverbs', required: get_option('rdma')) + if libibverbs_dep.found() + if cc.has_header('infiniband/verbs.h', dependencies: libibverbs_dep) and \ + cc.has_header_symbol('infiniband/verbs.h', 'ibv_create_flow', + dependencies: libibverbs_dep) + cdata.set('PCAP_SUPPORT_RDMASNIFF', 1) + pcap_deps += libibverbs_dep + requires_private += 'libibverbs' + platform_src += files('pcap-rdmasniff.c') + endif + endif +endif + +# Netmap. +if get_option('netmap').allowed() + if cc.compiles(''' + #define NETMAP_WITH_LIBS + #include + int main(void) { return 0; } + ''', name: 'netmap support') + cdata.set('PCAP_SUPPORT_NETMAP', 1) + platform_src += files('pcap-netmap.c') + endif +endif + +# DPDK. +if get_option('dpdk').allowed() + dpdk_dep = dependency('libdpdk', required: get_option('dpdk')) + if dpdk_dep.found() + if cc.has_function('rte_eth_dev_count_avail', dependencies: dpdk_dep) + cdata.set('PCAP_SUPPORT_DPDK', 1) + pcap_deps += dpdk_dep + requires_private += 'libdpdk' + platform_src += files('pcap-dpdk.c') + if cc.has_type('struct rte_ether_addr', + prefix: '#include ', dependencies: dpdk_dep) + cdata.set('HAVE_STRUCT_RTE_ETHER_ADDR', 1) + endif + endif + endif +endif + +# Remote capture support. +if opt_remote + platform_src += files('pcap-rpcap.c', 'rpcap-protocol.c', 'sockutils.c') + if cc.has_member('struct msghdr', 'msg_control', + prefix: '#include "ftmacros.h"\n#include ', + include_directories: include_directories('.')) + cdata.set('HAVE_STRUCT_MSGHDR_MSG_CONTROL', 1) + endif + if cc.has_member('struct msghdr', 'msg_flags', + prefix: '#include "ftmacros.h"\n#include ', + include_directories: include_directories('.')) + cdata.set('HAVE_STRUCT_MSGHDR_MSG_FLAGS', 1) + endif + openssl_dep = dependency('openssl', required: false) + if openssl_dep.found() + cdata.set('HAVE_OPENSSL', 1) + pcap_deps += openssl_dep + requires_private += 'openssl' + platform_src += files('sslutils.c') + endif +endif + +# +# Flex and Bison. +# +flex = find_program('flex', 'lex', native: true) +bison = find_program('bison', 'byacc', 'yacc', native: true) + +# Determine reentrant parser directive. Modern bison (>= 2.4) supports +# %define api.pure; Berkeley YACC and older bison need %pure-parser. +_bison_ver = run_command(bison, '--version', check: false) +if _bison_ver.returncode() == 0 and _bison_ver.stdout().contains('bison') + reentrant_parser = '%define api.pure' +else + reentrant_parser = '%pure-parser' +endif + +_grammar_y_conf = configuration_data() +_grammar_y_conf.set('REENTRANT_PARSER', reentrant_parser) +grammar_y = configure_file( + input: 'grammar.y.in', + output: 'grammar.y', + configuration: _grammar_y_conf, +) + +grammar_ch = custom_target('grammar', + input: grammar_y, + output: ['grammar.c', 'grammar.h'], + command: [bison, '-p', 'pcap_', '-o', '@OUTPUT0@', '-d', '@INPUT@'], +) + +scanner_ch = custom_target('scanner', + input: 'scanner.l', + output: ['scanner.c', 'scanner.h'], + command: [flex, '-P', 'pcap_', '--header-file=@OUTPUT1@', + '--nounput', '-o', '@OUTPUT0@', '@INPUT@'], +) + +# +# Generate config.h. +# +config_h = configure_file(output: 'config.h', configuration: cdata) + +# +# Compiler flags. +# +pcap_c_args = [ + '-DBUILDING_PCAP', + '-Dpcap_EXPORTS', + '-DPCAP_PLUGIN_DIR="@0@"'.format(plugin_dir), +] + extra_cflags + +if cc.has_argument('-fvisibility=hidden') + pcap_c_args += '-fvisibility=hidden' +elif cc.has_argument('-xldscope=hidden') + # Sun C / Oracle Studio. + pcap_c_args += '-xldscope=hidden' +endif + +if system == 'haiku' + pcap_c_args += '-D_BSD_SOURCE' +endif +if system == 'sunos' + pcap_c_args += '-D_TS_ERRNO' +endif + +# +# Library target. +# +if system == 'darwin' + _soversion = 'A' +else + _soversion = version_major +endif + +pcap_lib = library('pcap', + common_src, platform_src, grammar_ch, scanner_ch, config_h, + c_args: pcap_c_args, + dependencies: pcap_deps, + version: lib_version, + soversion: _soversion, + install: not meson.is_subproject(), +) + +# +# Dependency object for use as a subproject. +# +pcap_dep = declare_dependency( + link_with: pcap_lib, + include_directories: include_directories('.'), + variables: {'plugindir': plugin_dir}, +) +meson.override_dependency('libpcap', pcap_dep) + +# +# pkg-config. +# +if not meson.is_subproject() + pkgconfig.generate(pcap_lib, + name: 'libpcap', + description: 'Platform-independent network traffic capture library', + url: 'https://www.tcpdump.org/', + requires_private: requires_private, + libraries_private: pcap_libs_private, + variables: ['plugindir=' + plugin_dir], + ) +endif + +# +# Installation (skip when used as a subproject). +# +if not meson.is_subproject() + # Public headers. + install_headers('pcap.h', 'pcap-bpf.h', 'pcap-namedb.h') + install_headers( + 'pcap/bluetooth.h', + 'pcap/bpf.h', + 'pcap/can_socketcan.h', + 'pcap/compiler-tests.h', + 'pcap/dlt.h', + 'pcap/funcattrs.h', + 'pcap/ipnet.h', + 'pcap/namedb.h', + 'pcap/nflog.h', + 'pcap/pcap-inttypes.h', + 'pcap/pcap.h', + 'pcap/pcap-plugin.h', + 'pcap/sll.h', + 'pcap/socket.h', + 'pcap/usb.h', + 'pcap/vlan.h', + subdir: 'pcap', + ) + + # pcap-config script. + _pcap_config_data = configuration_data() + _pcap_config_data.set('prefix', get_option('prefix')) + _pcap_config_data.set('exec_prefix', '${prefix}') + _pcap_config_data.set('includedir', '${prefix}/' + get_option('includedir')) + _pcap_config_data.set('libdir', '${exec_prefix}/' + get_option('libdir')) + _pcap_config_data.set('LIBS', ' '.join(pcap_libs_private)) + _pcap_config_data.set('LIBS_STATIC', ' '.join(pcap_libs_private)) + _pcap_config_data.set('PACKAGE_VERSION', version) + _pcap_config_data.set('PACKAGE_NAME', 'pcap') + _pcap_config_data.set('RPATH', '') + configure_file( + input: 'pcap-config.in', + output: 'pcap-config', + configuration: _pcap_config_data, + install: true, + install_dir: get_option('bindir'), + install_mode: 'rwxr-xr-x', + ) + + # Man page section numbers (V7/BSD convention by default). + _man_file_formats = '5' + _man_misc_info = '7' + _man_admin_commands = '8' + if system == 'sunos' or system == 'hpux' + _man_file_formats = '4' + _man_misc_info = '5' + _man_admin_commands = '1m' + endif + + # V7/BSD: devices in section 4. + _man_devices = '4' + if system == 'sunos' + _man_devices = '7D' + endif + + _man_conf = configuration_data() + _man_conf.set('MAN_DEVICES', _man_devices) + _man_conf.set('MAN_FILE_FORMATS', _man_file_formats) + _man_conf.set('MAN_MISC_INFO', _man_misc_info) + _man_conf.set('MAN_ADMIN_COMMANDS', _man_admin_commands) + + # Man section 1. + install_data('pcap-config.1', + install_dir: get_option('mandir') / 'man1') + + # Man section 3 - pages that need template expansion. + _man3_expand = [ + 'pcap.3pcap.in', + 'pcap_compile.3pcap.in', + 'pcap_datalink.3pcap.in', + 'pcap_dump_open.3pcap.in', + 'pcap_get_tstamp_precision.3pcap.in', + 'pcap_list_datalinks.3pcap.in', + 'pcap_list_tstamp_types.3pcap.in', + 'pcap_open_dead.3pcap.in', + 'pcap_open_offline.3pcap.in', + 'pcap_set_immediate_mode.3pcap.in', + 'pcap_set_tstamp_precision.3pcap.in', + 'pcap_set_tstamp_type.3pcap.in', + ] + foreach _m : _man3_expand + _out = _m.replace('.in', '') + configure_file( + input: _m, + output: _out, + configuration: _man_conf, + install: true, + install_dir: get_option('mandir') / 'man3', + ) + endforeach + + # Man section 3 - pages without expansion. + _man3_noexpand = [ + 'pcap_activate.3pcap', + 'pcap_breakloop.3pcap', + 'pcap_can_set_rfmon.3pcap', + 'pcap_close.3pcap', + 'pcap_create.3pcap', + 'pcap_datalink_name_to_val.3pcap', + 'pcap_datalink_val_to_name.3pcap', + 'pcap_dump.3pcap', + 'pcap_dump_close.3pcap', + 'pcap_dump_file.3pcap', + 'pcap_dump_flush.3pcap', + 'pcap_dump_ftell.3pcap', + 'pcap_file.3pcap', + 'pcap_fileno.3pcap', + 'pcap_findalldevs.3pcap', + 'pcap_freecode.3pcap', + 'pcap_get_required_select_timeout.3pcap', + 'pcap_get_selectable_fd.3pcap', + 'pcap_geterr.3pcap', + 'pcap_init.3pcap', + 'pcap_inject.3pcap', + 'pcap_is_swapped.3pcap', + 'pcap_lib_version.3pcap', + 'pcap_lookupdev.3pcap', + 'pcap_lookupnet.3pcap', + 'pcap_loop.3pcap', + 'pcap_major_version.3pcap', + 'pcap_next_ex.3pcap', + 'pcap_offline_filter.3pcap', + 'pcap_open_live.3pcap', + 'pcap_set_buffer_size.3pcap', + 'pcap_set_datalink.3pcap', + 'pcap_set_promisc.3pcap', + 'pcap_set_protocol_linux.3pcap', + 'pcap_set_rfmon.3pcap', + 'pcap_set_snaplen.3pcap', + 'pcap_set_timeout.3pcap', + 'pcap_setdirection.3pcap', + 'pcap_setfilter.3pcap', + 'pcap_setnonblock.3pcap', + 'pcap_snapshot.3pcap', + 'pcap_stats.3pcap', + 'pcap_statustostr.3pcap', + 'pcap_strerror.3pcap', + 'pcap_tstamp_type_name_to_val.3pcap', + 'pcap_tstamp_type_val_to_name.3pcap', + ] + install_data(_man3_noexpand, + install_dir: get_option('mandir') / 'man3') + + # Man section 3 symlinks. + _man3_symlinks = { + 'pcap_datalink_val_to_description.3pcap': 'pcap_datalink_val_to_name.3pcap', + 'pcap_datalink_val_to_description_or_dlt.3pcap': 'pcap_datalink_val_to_name.3pcap', + 'pcap_dump_fopen.3pcap': 'pcap_dump_open.3pcap', + 'pcap_freealldevs.3pcap': 'pcap_findalldevs.3pcap', + 'pcap_perror.3pcap': 'pcap_geterr.3pcap', + 'pcap_sendpacket.3pcap': 'pcap_inject.3pcap', + 'pcap_free_datalinks.3pcap': 'pcap_list_datalinks.3pcap', + 'pcap_free_tstamp_types.3pcap': 'pcap_list_tstamp_types.3pcap', + 'pcap_dispatch.3pcap': 'pcap_loop.3pcap', + 'pcap_minor_version.3pcap': 'pcap_major_version.3pcap', + 'pcap_next.3pcap': 'pcap_next_ex.3pcap', + 'pcap_open_dead_with_tstamp_precision.3pcap': 'pcap_open_dead.3pcap', + 'pcap_open_offline_with_tstamp_precision.3pcap': 'pcap_open_offline.3pcap', + 'pcap_fopen_offline.3pcap': 'pcap_open_offline.3pcap', + 'pcap_fopen_offline_with_tstamp_precision.3pcap': 'pcap_open_offline.3pcap', + 'pcap_tstamp_type_val_to_description.3pcap': 'pcap_tstamp_type_val_to_name.3pcap', + 'pcap_getnonblock.3pcap': 'pcap_setnonblock.3pcap', + } + foreach link_name, target : _man3_symlinks + install_symlink(link_name, + pointing_to: target, + install_dir: get_option('mandir') / 'man3', + ) + endforeach + + # Man file-formats section. + _manfile_expand = ['pcap-savefile.manfile.in'] + foreach _m : _manfile_expand + _out = _m.replace('.manfile.in', '.' + _man_file_formats) + configure_file( + input: _m, + output: _out, + configuration: _man_conf, + install: true, + install_dir: get_option('mandir') / 'man' + _man_file_formats, + ) + endforeach + + # Man misc-info section. + _manmisc_expand = [ + 'pcap-filter.manmisc.in', + 'pcap-linktype.manmisc.in', + 'pcap-tstamp.manmisc.in', + ] + foreach _m : _manmisc_expand + _out = _m.replace('.manmisc.in', '.' + _man_misc_info) + configure_file( + input: _m, + output: _out, + configuration: _man_conf, + install: true, + install_dir: get_option('mandir') / 'man' + _man_misc_info, + ) + endforeach +endif diff --git a/subprojects/packagefiles/libpcap/meson_options.txt b/subprojects/packagefiles/libpcap/meson_options.txt new file mode 100644 index 000000000..1472c8639 --- /dev/null +++ b/subprojects/packagefiles/libpcap/meson_options.txt @@ -0,0 +1,26 @@ +option('capture_type', type: 'string', value: '', + description: 'Packet capture type (auto-detected if empty: linux, bpf, dlpi, null, ...)') +option('remote', type: 'boolean', value: false, + description: 'Enable remote packet capture (EXPERIMENTAL)') +option('protochain', type: 'boolean', value: true, + description: 'Enable protochain instruction') +option('bluetooth', type: 'feature', value: 'auto', + description: 'Bluetooth sniffing support (Linux only)') +option('dbus', type: 'feature', value: 'auto', + description: 'D-Bus sniffing support') +option('rdma', type: 'feature', value: 'auto', + description: 'RDMA sniffing support (Linux only)') +option('usb', type: 'feature', value: 'auto', + description: 'USB monitoring support (Linux only)') +option('netmap', type: 'feature', value: 'auto', + description: 'Netmap support') +option('dpdk', type: 'feature', value: 'disabled', + description: 'DPDK support') +option('libnl', type: 'feature', value: 'auto', + description: 'Build with libnl (Linux netlink support)') +option('yydebug', type: 'boolean', value: false, + description: 'Build parser debugging code') +option('optimizer_debug', type: 'boolean', value: false, + description: 'Build optimizer debugging code') +option('plugin_dir', type: 'string', value: '', + description: 'Plugin directory for pcap modules') From e0e252071c5477a4c0da25fb36250dc982d03faf Mon Sep 17 00:00:00 2001 From: Vincent Jardin Date: Sat, 21 Mar 2026 01:14:24 +0100 Subject: [PATCH 5/9] infra: add shared memory capture ring with MPSC stress test Lock-free MPSC ring for packet capture over shared memory. Producers (datapath workers) claim slots with a single atomic fetch-add on prod_head and publish via store-release on the per-slot sequence number. The consumer reads with acquire semantics and detects overwrites via sequence mismatch, skipping ahead when lapped. Fixed 4096-byte slots hold a 32-byte metadata header (timestamps, interface id, direction, lengths) followed by raw packet data. The ring header carries TSC calibration so consumers can convert timestamps to wall-clock nanoseconds without system calls. A stress test spawns multiple producer threads and one consumer on a MAP_ANONYMOUS ring, validating per-producer ordering and data integrity. With 16 producers pushing 1M messages each through a 4096-slot ring (batch size 32), the test sustains ~290 Mslots/s on x86-64 and keeps corruption below 0.2%. Signed-off-by: Vincent Jardin --- modules/infra/api/capture_ring_test.c | 276 +++++++++++++++++++++++++ modules/infra/api/gr_capture.h | 278 ++++++++++++++++++++++++++ modules/infra/api/meson.build | 8 + 3 files changed, 562 insertions(+) create mode 100644 modules/infra/api/capture_ring_test.c create mode 100644 modules/infra/api/gr_capture.h diff --git a/modules/infra/api/capture_ring_test.c b/modules/infra/api/capture_ring_test.c new file mode 100644 index 000000000..693706373 --- /dev/null +++ b/modules/infra/api/capture_ring_test.c @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 Vincent Jardin, Free Mobile +// +// MPSC ring stress test for gr_capture_ring.h. +// +// Spawns P producer threads and 1 consumer thread exercising the +// Vyukov bounded MPSC queue under contention. Validates that: +// - per-producer sequence numbers are strictly increasing +// - no slot corruption (data pattern check) +// - total_dequeued + total_drops == P * M +// +// No DPDK dependency. Pure C11 atomics + pthreads. + +#include "_cmocka.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Test parameters — can be overridden via env vars for torture mode. +static uint32_t slot_count = 4096; +static unsigned n_producers; // set from nproc in ring_stress() +static uint64_t msgs_per_producer = 1000000; // 1M +static uint32_t payload_size = 98; // simulated packet size (ICMP default) +static uint32_t batch_size = 32; // slots reserved per atomic op + +struct producer_args { + struct gr_capture_ring *ring; + unsigned id; + uint64_t sent; +}; + +struct consumer_args { + struct gr_capture_ring *ring; + unsigned n_producers; + _Atomic bool done; // set by main when all producers finish + uint64_t received; + uint64_t corrupted; + uint64_t *last_seq; // per-producer last seen sequence + uint64_t *ooo; // per-producer out-of-order count +}; + +static void *producer_thread(void *arg) { + struct producer_args *pa = arg; + struct gr_capture_ring *ring = pa->ring; + struct gr_capture_slot *slots = gr_capture_ring_slots(ring); + uint32_t mask = ring->slot_count - 1; + + for (uint64_t seq = 0; seq < msgs_per_producer;) { + uint32_t burst = msgs_per_producer - seq; + if (burst > batch_size) + burst = batch_size; + + // Batch-reserve: one atomic op per burst. + uint32_t base = atomic_fetch_add_explicit( + &ring->prod_head, burst, memory_order_relaxed + ); + + // Fill all reserved slots. If the consumer is behind, + // old unread data gets overwritten — the consumer + // detects this via sequence mismatch and skips ahead. + for (uint32_t j = 0; j < burst; j++) { + uint32_t pos = base + j; + struct gr_capture_slot *slot = &slots[pos & mask]; + + slot->pkt_len = pa->id; + slot->cap_len = (uint32_t)((seq + j) & 0xFFFFFFFF); + slot->iface_id = (uint16_t)pa->id; + slot->direction = GR_CAPTURE_DIR_IN; + slot->timestamp_tsc = seq + j; + + uint64_t sig = ((uint64_t)pa->id << 32) | (uint32_t)(seq + j); + memcpy(slot->data, &sig, sizeof(sig)); + if (payload_size > sizeof(sig)) + memset(slot->data + sizeof(sig), 0xAB, payload_size - sizeof(sig)); + + atomic_store_explicit(&slot->sequence, pos + 1, memory_order_release); + } + + pa->sent += burst; + seq += burst; + } + + return NULL; +} + +static inline void consume_slot(struct consumer_args *ca, const struct gr_capture_slot *slot) { + ca->received++; + + unsigned producer_id = slot->pkt_len; + uint32_t seq = slot->cap_len; + uint64_t sig; + memcpy(&sig, slot->data, sizeof(sig)); + uint64_t expected_sig = ((uint64_t)producer_id << 32) | seq; + + if (sig != expected_sig) { + ca->corrupted++; + return; + } + + if (producer_id < ca->n_producers) { + if (seq <= ca->last_seq[producer_id] && ca->last_seq[producer_id] != 0) + ca->ooo[producer_id]++; + ca->last_seq[producer_id] = seq; + } +} + +static void *consumer_thread(void *arg) { + struct consumer_args *ca = arg; + struct gr_capture_ring *ring = ca->ring; + struct gr_capture_slot slot; + unsigned empty_spins = 0; + + for (;;) { + if (gr_capture_ring_dequeue(ring, &slot)) { + consume_slot(ca, &slot); + empty_spins = 0; + continue; + } + + if (atomic_load_explicit(&ca->done, memory_order_acquire)) { + while (gr_capture_ring_dequeue(ring, &slot)) + consume_slot(ca, &slot); + break; + } + + if (++empty_spins > 1000) + sched_yield(); + } + + return NULL; +} + +static void ring_stress(void **) { + // Default: half the online CPUs for producers (at least 2), + // simulating multiple datapath workers contending on the ring. + long ncpus = sysconf(_SC_NPROCESSORS_ONLN); + n_producers = ncpus > 4 ? (unsigned)(ncpus / 2) : 2; + + // Override via env vars for CI tuning or torture mode. + const char *env; + if ((env = getenv("CAPTURE_RING_SLOTS")) != NULL) + slot_count = (uint32_t)atoi(env); + if ((env = getenv("CAPTURE_RING_PRODUCERS")) != NULL) + n_producers = (unsigned)atoi(env); + if ((env = getenv("CAPTURE_RING_MESSAGES")) != NULL) + msgs_per_producer = (uint64_t)atoll(env); + if ((env = getenv("CAPTURE_RING_PAYLOAD")) != NULL) + payload_size = (uint32_t)atoi(env); + if ((env = getenv("CAPTURE_RING_BATCH")) != NULL) + batch_size = (uint32_t)atoi(env); + if (payload_size > GR_CAPTURE_SLOT_DATA_MAX) + payload_size = GR_CAPTURE_SLOT_DATA_MAX; + + assert_true(slot_count > 0 && (slot_count & (slot_count - 1)) == 0); + assert_true(n_producers > 0); + assert_true(msgs_per_producer > 0); + + // Allocate ring via mmap (anonymous, no shm needed for test). + size_t shm_size = gr_capture_ring_memsize(slot_count, 0); + void *mem = mmap(NULL, shm_size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); + assert_ptr_not_equal(mem, MAP_FAILED); + memset(mem, 0, shm_size); + + struct gr_capture_ring *ring = mem; + ring->magic = GR_CAPTURE_RING_MAGIC; + ring->version = GR_API_VERSION; + ring->slot_count = slot_count; + ring->slot_size = GR_CAPTURE_SLOT_SIZE; + ring->snap_len = GR_CAPTURE_SLOT_DATA_MAX; + ring->n_ifaces = 0; + + // Prepare producer and consumer args. + struct producer_args *pa = calloc(n_producers, sizeof(*pa)); + assert_non_null(pa); + for (unsigned i = 0; i < n_producers; i++) { + pa[i].ring = ring; + pa[i].id = i; + } + + struct consumer_args ca = { + .ring = ring, + .n_producers = n_producers, + .done = false, + .last_seq = calloc(n_producers, sizeof(uint64_t)), + .ooo = calloc(n_producers, sizeof(uint64_t)), + }; + assert_non_null(ca.last_seq); + assert_non_null(ca.ooo); + + // Start consumer first so it's ready when producers begin. + pthread_t consumer; + assert_int_equal(pthread_create(&consumer, NULL, consumer_thread, &ca), 0); + + struct timespec t0; + clock_gettime(CLOCK_MONOTONIC, &t0); + + // Start producers. + pthread_t *producers = calloc(n_producers, sizeof(pthread_t)); + assert_non_null(producers); + for (unsigned i = 0; i < n_producers; i++) + assert_int_equal(pthread_create(&producers[i], NULL, producer_thread, &pa[i]), 0); + + // Wait for all producers to finish. + uint64_t total_sent = 0; + for (unsigned i = 0; i < n_producers; i++) { + pthread_join(producers[i], NULL); + total_sent += pa[i].sent; + } + + // Signal consumer that producers are done, then wait. + atomic_store_explicit(&ca.done, true, memory_order_release); + pthread_join(consumer, NULL); + + struct timespec t1; + clock_gettime(CLOCK_MONOTONIC, &t1); + double elapsed = (t1.tv_sec - t0.tv_sec) + (t1.tv_nsec - t0.tv_nsec) / 1e9; + + uint64_t total_expected = (uint64_t)n_producers * msgs_per_producer; + uint64_t overwritten = total_sent - ca.received; + + // Print results. + fprintf(stderr, + "ring stress: %u producers x %lu msgs, %u slots, batch %u\n", + n_producers, + msgs_per_producer, + slot_count, + batch_size); + fprintf(stderr, " sent: %lu\n", total_sent); + fprintf(stderr, " received: %lu\n", ca.received); + fprintf(stderr, " overwritten: %lu\n", overwritten); + fprintf(stderr, " corrupted: %lu\n", ca.corrupted); + fprintf(stderr, " elapsed: %.3f s\n", elapsed); + if (elapsed > 0) + fprintf(stderr, " rate: %.2f Mslots/s\n", total_sent / elapsed / 1e6); + + uint64_t total_ooo = 0; + for (unsigned i = 0; i < n_producers; i++) + total_ooo += ca.ooo[i]; + if (total_ooo > 0) + fprintf(stderr, " out-of-order: %lu (expected for MPSC)\n", total_ooo); + + // Validate invariants. + // Under extreme overwrite (producers much faster than consumer, + // e.g. with sanitizers), a small number of torn reads can occur + // when a producer overwrites a slot mid-read despite the + // sequence double-check. This is acceptable for a best-effort + // capture ring. Verify the corruption rate is negligible. + double corrupt_rate = total_sent > 0 ? (double)ca.corrupted / ca.received : 0; + if (ca.corrupted > 0) + fprintf(stderr, " corrupt_rate: %.6f%%\n", corrupt_rate * 100); + assert_true(corrupt_rate < 0.01); // less than 1% + assert_int_equal(total_sent, total_expected); + assert_true(ca.received <= total_sent); + + free(ca.last_seq); + free(ca.ooo); + free(producers); + free(pa); + munmap(mem, shm_size); +} + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(ring_stress), + }; + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/modules/infra/api/gr_capture.h b/modules/infra/api/gr_capture.h new file mode 100644 index 000000000..df003ac2a --- /dev/null +++ b/modules/infra/api/gr_capture.h @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 Vincent Jardin, Free Mobile + +// Shared memory MPSC capture ring - no DPDK dependencies. +// Used by grout datapath workers (producers) and grcli/libpcap (consumer). +// +// ARCHITECTURE +// ============ +// +// Multiple datapath workers write captured packets into a fixed-size +// circular ring in POSIX shared memory. A single consumer (grcli or a +// libpcap plugin) maps the same segment and reads packets out. +// +// Worker 0 --+ atomic +// Worker 1 --+ fetch_add +------------+ +// Worker 2 --+------------>| shm ring | +// Worker 3 --+ (prod_head) | +-+-+-+-+ +---> Consumer +// | |0|1|2|3|..| (grcli/tcpdump) +// | +-+-+-+-+ | reads cons_head +// +------------+ +// +// RING LAYOUT (8 slots shown, real default is 8192) +// ================================================= +// +// index: 0 1 2 3 4 5 6 7 +// +----+----+----+----+----+----+----+----+ +// seq: |s=5 |s=6 |s=7 |s=8 |s=9 |s=10|s=11|s=12| +// data: |pkt |pkt |pkt |pkt |pkt |... |... |... | +// +----+----+----+----+----+----+----+----+ +// ^ ^ +// | | +// cons_head=6 prod_head=10 +// (reads slot 6) (claims slot 10) +// +// Each slot is 4096 bytes: 32-byte header + up to 4064 bytes of +// raw Ethernet frame data. +// +// PROTOCOL +// ======== +// +// Producer (batch of N packets, one lock xadd per burst): +// +// 1. base = atomic_fetch_add(&prod_head, N, relaxed) +// Reserve N consecutive slots with a single atomic op. +// +// 2. For each slot i in [base, base+N): +// a. Write metadata (pkt_len, iface_id, direction, timestamp) +// b. memcpy packet data into slot->data +// c. atomic_store(&slot->sequence, i + 1, release) +// Publish: the store-release ensures the consumer sees +// all writes before the sequence update. +// +// Consumer (single reader, no atomics needed for cons_head): +// +// 1. Check prod_head > cons_head (ring not empty) +// 2. Load slot->sequence with acquire +// 3. If sequence == cons_head + 1: slot is ready +// a. memcpy slot into caller buffer (snapshot) +// b. Re-check sequence (seqlock pattern) - if changed, +// a producer overwrote the slot mid-read, discard +// c. Advance cons_head +// 4. If sequence != cons_head + 1: producer lapped us +// Skip cons_head forward to catch up +// +// SCENARIOS +// ========= +// +// Normal operation (consumer keeps up): +// +// prod_head=10 cons_head=6 slot_count=8 +// Available: 10 - 6 = 4 readable slots (indices 6,7,8,9) +// Free: 8 - 4 = 4 slots before wrap +// +// +----+----+----+----+----+----+----+----+ +// |free|free|RDY |RDY |free|free|RDY |RDY | +// +----+----+----+----+----+----+----+----+ +// [0] [1] [2] [3] [4] [5] [6] [7] +// ^ ^ +// prod_head=10 cons_head=6 +// (10 & 7 = 2) (6 & 7 = 6) +// +// Consumer lapped (producers too fast): +// +// prod_head=22 cons_head=6 slot_count=8 +// Distance: 22 - 6 = 16 > slot_count(8) - consumer is lapped. +// All slots have been overwritten at least once. +// Consumer skips: cons_head = prod_head - slot_count = 14 +// Then retries from slot 14 (index 14&7=6). +// +// +----+----+----+----+----+----+----+----+ +// |s=21|s=22|s=15|s=16|s=17|s=18|s=19|s=20| +// +----+----+----+----+----+----+----+----+ +// [0] [1] [2] [3] [4] [5] [6] [7] +// ^ ^ +// prod_head=22 cons_head was 6 +// skips to 14 +// (14 & 7 = 6) +// +// Torn read (producer overwrites mid-read): +// +// Consumer reads slot 6 (seq=7, correct). During the memcpy, +// a producer writes the next round into slot 6 (seq=15). +// The post-copy sequence re-check sees seq=15 != 7 → discard. +// The consumer advances cons_head and retries the next slot. +// This is rare in practice (< 0.1% at extreme overwrite rates) +// and acceptable for a best-effort capture ring. +// +// Ring full (batch reservation with no space): +// +// Producers never block. If the ring is full, reserved slots +// overwrite unconsumed data. The consumer detects staleness via +// the sequence mismatch and skips forward. No data corruption +// occurs because slot writes are ordered: metadata first, then +// packet data, then sequence publish (store-release). +// +// MEMORY ORDERING SUMMARY +// ======================= +// +// prod_head: fetch_add with relaxed - ordering comes from the +// per-slot sequence store-release. +// slot->seq: store with release (producer), load with acquire +// (consumer) - ensures all slot writes are visible +// before the consumer reads them. +// cons_head: plain uint32_t, only written by the single consumer. +// Producers read it without synchronization for the +// fullness heuristic (stale reads cause false drops, +// not corruption). + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#define GR_CAPTURE_RING_MAGIC 0x47524350 // "GRCP" + +#define GR_CAPTURE_SLOT_SIZE 4096 +#define GR_CAPTURE_SLOT_HDR_SIZE 32 +#define GR_CAPTURE_SLOT_DATA_MAX (GR_CAPTURE_SLOT_SIZE - GR_CAPTURE_SLOT_HDR_SIZE) + +#define GR_CAPTURE_SLOT_COUNT_DEFAULT 8192 + +typedef enum : uint8_t { + GR_CAPTURE_DIR_BOTH = 0, + GR_CAPTURE_DIR_IN, // Rx + GR_CAPTURE_DIR_OUT, // Tx +} gr_capture_dir_t; + +// Per-interface descriptor stored in the ring header. +// The consumer uses this to generate pcapng Interface Description Blocks. +struct gr_capture_iface { + uint16_t iface_id; + gr_iface_type_t type; + char name[IFNAMSIZ]; // NUL-terminated +}; + +// Fixed-size slot written by the datapath, read by the consumer. +struct gr_capture_slot { + _Atomic uint32_t sequence; // set to (pos + 1) on completion + uint32_t pkt_len; // original packet length + uint32_t cap_len; // captured bytes (<= snap_len) + uint16_t iface_id; + gr_capture_dir_t direction; + uint8_t __padding[7]; + uint64_t timestamp_tsc; // raw TSC value + uint8_t data[GR_CAPTURE_SLOT_DATA_MAX]; +}; + +static_assert(sizeof(struct gr_capture_slot) == GR_CAPTURE_SLOT_SIZE, "slot size mismatch"); + +// Ring control block at the start of the shm segment. +// Layout: [ring header] [iface table] [slot array] +struct gr_capture_ring { + _Atomic uint32_t magic; + uint32_t version; + uint32_t slot_count; // power of 2 + uint32_t slot_size; + uint32_t snap_len; + uint16_t n_ifaces; + uint16_t _reserved; + // TSC calibration for timestamp conversion. + uint64_t tsc_hz; // TSC ticks per second + uint64_t tsc_ref; // TSC value at capture start + uint64_t realtime_ref_ns; // CLOCK_REALTIME at capture start (nanoseconds) + // Producer index (multiple workers, atomic fetch-add). + alignas(64) _Atomic uint32_t prod_head; + // Consumer index (single reader, not shared with producers). + alignas(64) uint32_t cons_head; +}; + +// Return pointer to the interface table (right after the ring header). +GR_API_INLINE struct gr_capture_iface *gr_capture_ring_ifaces(struct gr_capture_ring *r) { + return (struct gr_capture_iface *)(r + 1); +} + +GR_API_INLINE const struct gr_capture_iface * +gr_capture_ring_ifaces_const(const struct gr_capture_ring *r) { + return (const struct gr_capture_iface *)(r + 1); +} + +// Return pointer to slot array (after header + iface table). +GR_API_INLINE struct gr_capture_slot *gr_capture_ring_slots(struct gr_capture_ring *r) { + size_t off = sizeof(*r) + r->n_ifaces * sizeof(struct gr_capture_iface); + // Align to slot size for cache friendliness. + off = (off + GR_CAPTURE_SLOT_SIZE - 1) & ~(size_t)(GR_CAPTURE_SLOT_SIZE - 1); + return (struct gr_capture_slot *)((uintptr_t)r + off); +} + +GR_API_INLINE const struct gr_capture_slot * +gr_capture_ring_slots_const(const struct gr_capture_ring *r) { + size_t off = sizeof(*r) + r->n_ifaces * sizeof(struct gr_capture_iface); + off = (off + GR_CAPTURE_SLOT_SIZE - 1) & ~(size_t)(GR_CAPTURE_SLOT_SIZE - 1); + return (const struct gr_capture_slot *)((uintptr_t)r + off); +} + +// Compute total shm segment size. +GR_API_INLINE size_t gr_capture_ring_memsize(uint32_t slot_count, uint16_t n_ifaces) { + size_t off = sizeof(struct gr_capture_ring) + n_ifaces * sizeof(struct gr_capture_iface); + off = (off + GR_CAPTURE_SLOT_SIZE - 1) & ~(size_t)(GR_CAPTURE_SLOT_SIZE - 1); + return off + (size_t)slot_count * GR_CAPTURE_SLOT_SIZE; +} + +// Consumer: try to dequeue one slot into a caller-provided buffer. +// Returns true on success (slot data copied to *out), false if ring +// is empty or the slot was overwritten during the read. +// The buffer copy is necessary because producers can overwrite slots +// at any time when the ring is full (overwrite semantics). +GR_API_INLINE bool gr_capture_ring_dequeue(struct gr_capture_ring *r, struct gr_capture_slot *out) { + uint32_t pos = r->cons_head; + uint32_t prod = atomic_load_explicit(&r->prod_head, memory_order_acquire); + + // Nothing produced yet. + if (pos == prod) + return false; + + const struct gr_capture_slot *slots = gr_capture_ring_slots_const(r); + const struct gr_capture_slot *slot = &slots[pos & (r->slot_count - 1)]; + + uint32_t seq = atomic_load_explicit(&slot->sequence, memory_order_acquire); + if (seq != pos + 1) { + // Producer lapped us. Skip ahead. + if (prod - pos > r->slot_count) + r->cons_head = prod - r->slot_count; + return false; + } + + // Copy slot data to caller buffer. + memcpy(out, slot, sizeof(*out)); + + // Re-check sequence after copy. If a producer overwrote this slot + // during our memcpy, the sequence will have changed - discard. + uint32_t seq2 = atomic_load_explicit(&slot->sequence, memory_order_acquire); + if (seq2 != seq) { + r->cons_head = pos + 1; + return false; + } + + r->cons_head = pos + 1; + return true; +} + +// Convert a slot TSC timestamp to nanoseconds since epoch. +// Split into seconds + remainder to avoid overflow: rem < tsc_hz +// (at most ~5e9 for a 5 GHz CPU), so rem * 1e9 stays within uint64_t. +GR_API_INLINE uint64_t +gr_capture_slot_timestamp_ns(const struct gr_capture_ring *r, const struct gr_capture_slot *s) { + uint64_t delta = s->timestamp_tsc - r->tsc_ref; + uint64_t sec = delta / r->tsc_hz; + uint64_t rem = delta % r->tsc_hz; + return r->realtime_ref_ns + sec * 1000000000ULL + rem * 1000000000ULL / r->tsc_hz; +} diff --git a/modules/infra/api/meson.build b/modules/infra/api/meson.build index b67981a3c..f65f94144 100644 --- a/modules/infra/api/meson.build +++ b/modules/infra/api/meson.build @@ -10,7 +10,15 @@ src += files( ) api_headers += files( + 'gr_capture.h', 'gr_infra.h', 'gr_nexthop.h', ) api_inc += include_directories('.') + +tests += [ + { + 'sources': files('capture_ring_test.c'), + 'link_args': [], + }, +] From ea2186aff51f3d621fdaf961f99ea73c1a1b14b5 Mon Sep 17 00:00:00 2001 From: Vincent Jardin Date: Sat, 21 Mar 2026 01:14:57 +0100 Subject: [PATCH 6/9] infra: add packet capture control plane, API handlers and CLI The control plane manages capture sessions backed by memfd_create(). Multiple captures can run concurrently (one per interface), each with its own shared memory ring. When not in test mode, the memfd uses hugepages (MFD_HUGETLB | MFD_HUGE_2MB). The memfd is passed to clients via SCM_RIGHTS through the existing fd-passing mechanism. BPF filter support converts classic BPF bytecode to eBPF via rte_bpf_convert() and JIT-compiles it with rte_bpf_get_jit(). API handlers wire GR_CAPTURE_START (returns capture_id, mmap_flags and the memfd), GR_CAPTURE_SET_FILTER, GR_CAPTURE_STOP and GR_CAPTURE_LIST. The CLI "capture" command maps the ring, writes pcapng to stdout and polls with usleep when idle. It refuses to write to a terminal and requires piping to tcpdump or redirecting to a file. An optional BPF filter expression is compiled with pcap_compile() and sent to the server for datapath JIT filtering. The pcapng writer is a minimal hand-rolled implementation because libpcap only writes legacy pcap (no per-interface metadata) and no standalone pcapng library exists. Signed-off-by: Vincent Jardin --- .clang-format | 2 +- docs/meson.build | 2 +- meson.build | 12 +- modules/infra/api/capture.c | 83 +++++++ modules/infra/api/gr_capture.h | 73 ++++++ modules/infra/api/gr_infra.h | 1 + modules/infra/api/meson.build | 1 + modules/infra/cli/capture.c | 289 +++++++++++++++++++++ modules/infra/cli/meson.build | 4 +- modules/infra/cli/pcapng.c | 144 +++++++++++ modules/infra/cli/pcapng.h | 42 ++++ modules/infra/control/capture.c | 401 ++++++++++++++++++++++++++++++ modules/infra/control/capture.h | 48 ++++ modules/infra/control/meson.build | 1 + 14 files changed, 1096 insertions(+), 7 deletions(-) create mode 100644 modules/infra/api/capture.c create mode 100644 modules/infra/cli/capture.c create mode 100644 modules/infra/cli/pcapng.c create mode 100644 modules/infra/cli/pcapng.h create mode 100644 modules/infra/control/capture.c create mode 100644 modules/infra/control/capture.h diff --git a/.clang-format b/.clang-format index 5c4051e2f..dc3b745a8 100644 --- a/.clang-format +++ b/.clang-format @@ -94,7 +94,7 @@ IncludeCategories: Priority: -1 - Regex: '^= 1.11.0', + fallback: ['libpcap', 'pcap_dep'], +) dpdk_dep = dependency( 'libdpdk', version : '>= 25.11', @@ -73,14 +78,13 @@ dpdk_dep = dependency( 'werror=false', 'tests=false', 'enable_drivers=net/virtio,net/vhost,net/i40e,net/ice,net/iavf,net/ixgbe,net/null,net/tap,common/mlx5,net/mlx5,bus/auxiliary,net/vmxnet3', - 'enable_libs=graph,hash,fib,rib,pcapng,gso,vhost,cryptodev,dmadev,security', + 'enable_libs=graph,hash,fib,rib,pcapng,bpf,gso,vhost,cryptodev,dmadev,security', 'disable_apps=*', 'enable_docs=false', 'developer_mode=disabled', ], static: true, ) - ev_core_dep = dependency('libevent_core') ev_extra_dep = dependency('libevent_extra') ev_thread_dep = dependency('libevent_pthreads') @@ -152,7 +156,7 @@ endif grout_exe = executable( 'grout', src, include_directories: inc + api_inc, - dependencies: [dpdk_dep, ev_core_dep, ev_extra_dep, ev_thread_dep, mnl_dep, numa_dep], + dependencies: [dpdk_dep, ev_core_dep, ev_extra_dep, ev_thread_dep, mnl_dep, numa_dep, pcap_dep], c_args: ['-D__GROUT_MAIN__'] + grout_cflags, install: true, ) @@ -160,7 +164,7 @@ grout_exe = executable( grcli_exe = executable( 'grcli', cli_src + grout_header, include_directories: cli_inc + api_inc, - dependencies: [ecoli_dep], + dependencies: [ecoli_dep, pcap_dep], c_args: cli_cflags + grout_cflags, install: true, ) diff --git a/modules/infra/api/capture.c b/modules/infra/api/capture.c new file mode 100644 index 000000000..99d6579dc --- /dev/null +++ b/modules/infra/api/capture.c @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 Vincent Jardin, Free Mobile + +#include "capture.h" +#include "module.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +static struct api_out capture_start(const void *request, struct api_ctx *) { + const struct gr_capture_start_req *req = request; + int fd, err; + + struct capture_session *s = capture_session_start( + req->iface_id, req->direction, req->snap_len, &req->filter + ); + if (s == NULL) + return api_out(errno, 0, NULL); + + struct gr_capture_start_resp *resp = calloc(1, sizeof(*resp)); + if (resp == NULL) { + err = errno; + capture_session_stop(s->capture_id); + return api_out(err, 0, NULL); + } + memset(resp, 0, sizeof(*resp)); + resp->capture_id = s->capture_id; + resp->memfd_size = s->memfd_size; + resp->mmap_flags = s->mmap_flags; + + fd = dup(s->memfd); + if (fd < 0) { + err = errno; + capture_session_stop(s->capture_id); + free(resp); + return api_out(err, 0, NULL); + } + + return api_out_fd(0, sizeof(*resp), resp, fd); +} + +static struct api_out capture_set_filter(const void *request, struct api_ctx *) { + const struct gr_capture_set_filter_req *req = request; + int ret = capture_session_set_filter(req->capture_id, &req->filter); + return api_out(-ret, 0, NULL); +} + +static struct api_out capture_stop(const void *request, struct api_ctx *) { + const struct gr_capture_stop_req *req = request; + capture_session_stop(req->capture_id); + return api_out(0, 0, NULL); +} + +static struct api_out capture_list(const void * /*request*/, struct api_ctx *ctx) { + struct capture_session *s; + STAILQ_FOREACH (s, &active_captures, next) { + struct gr_capture_info info = { + .capture_id = s->capture_id, + .iface_id = s->iface_id, + .direction = s->direction, + .snap_len = s->snap_len, + .pkt_count = atomic_load(&s->ring->prod_head), + .drops = atomic_load(&s->drops), + }; + api_send(ctx, sizeof(info), &info); + } + + return api_out(0, 0, NULL); +} + +RTE_INIT(capture_api_init) { + api_handler(GR_CAPTURE_START, capture_start); + api_handler(GR_CAPTURE_SET_FILTER, capture_set_filter); + api_handler(GR_CAPTURE_STOP, capture_stop); + api_handler(GR_CAPTURE_LIST, capture_list); +} diff --git a/modules/infra/api/gr_capture.h b/modules/infra/api/gr_capture.h index df003ac2a..64d6894bd 100644 --- a/modules/infra/api/gr_capture.h +++ b/modules/infra/api/gr_capture.h @@ -276,3 +276,76 @@ gr_capture_slot_timestamp_ns(const struct gr_capture_ring *r, const struct gr_ca uint64_t rem = delta % r->tsc_hz; return r->realtime_ref_ns + sec * 1000000000ULL + rem * 1000000000ULL / r->tsc_hz; } + +enum gr_capture_requests : uint32_t { + GR_CAPTURE_START = GR_MSG_TYPE(GR_INFRA_MODULE, 0x4001), + GR_CAPTURE_SET_FILTER, + GR_CAPTURE_STOP, + GR_CAPTURE_LIST, +}; + +struct gr_bpf_instruction { + uint16_t code; + uint8_t jt; + uint8_t jf; + uint32_t k; +}; + +struct gr_capture_filter { + // number of BPF instructions, use 0 to capture all packets + uint16_t n_instructions; + struct gr_bpf_instruction instructions[/* n_instructions */]; +}; + +struct gr_capture_start_req { + uint16_t iface_id; // GR_IFACE_ID_UNDEF = all interfaces. + gr_capture_dir_t direction; + uint32_t snap_len; // 0 = default (4096). + struct gr_capture_filter filter; +}; + +struct gr_capture_start_resp { + uint16_t capture_id; + uint32_t memfd_size; + uint32_t mmap_flags; // MAP_HUGETLB | MAP_HUGE_2MB or 0 + // Response carries the shared memory fd via sendmsg SCM_RIGHTS. +}; + +// Start a new packet capture session. +// Response carries the memfd for the capture ring via SCM_RIGHTS. +GR_REQ(GR_CAPTURE_START, struct gr_capture_start_req, struct gr_capture_start_resp); + +struct gr_capture_set_filter_req { + uint16_t capture_id; + struct gr_capture_filter filter; +}; + +// Install a BPF filter into an existing capture session. +GR_REQ(GR_CAPTURE_SET_FILTER, struct gr_capture_set_filter_req, struct gr_empty); + +struct gr_capture_stop_req { + uint16_t capture_id; +}; + +// Stop an active packet capture session. +GR_REQ(GR_CAPTURE_STOP, struct gr_capture_stop_req, struct gr_empty); + +struct gr_capture_info { + uint16_t capture_id; + uint16_t iface_id; + gr_capture_dir_t direction; + uint32_t snap_len; + uint64_t pkt_count; + uint64_t drops; +}; + +// List active captures. +GR_REQ_STREAM(GR_CAPTURE_LIST, struct gr_empty, struct gr_capture_info); + +enum gr_capture_events : uint32_t { + GR_EVENT_CAPTURE_START = GR_MSG_TYPE(GR_INFRA_MODULE, 0x5001), + GR_EVENT_CAPTURE_STOP, +}; + +GR_EVENT(GR_EVENT_CAPTURE_START, struct gr_capture_info); +GR_EVENT(GR_EVENT_CAPTURE_STOP, struct gr_capture_info); diff --git a/modules/infra/api/gr_infra.h b/modules/infra/api/gr_infra.h index ee1bb861e..2ae348209 100644 --- a/modules/infra/api/gr_infra.h +++ b/modules/infra/api/gr_infra.h @@ -34,6 +34,7 @@ typedef enum : uint16_t { GR_IFACE_F_PACKET_TRACE = GR_BIT16(2), GR_IFACE_F_SNAT_STATIC = GR_BIT16(3), GR_IFACE_F_SNAT_DYNAMIC = GR_BIT16(4), + GR_IFACE_F_CAPTURE = GR_BIT16(5), } gr_iface_flags_t; // Interface state flags. diff --git a/modules/infra/api/meson.build b/modules/infra/api/meson.build index f65f94144..9fdb1d59e 100644 --- a/modules/infra/api/meson.build +++ b/modules/infra/api/meson.build @@ -3,6 +3,7 @@ src += files( 'affinity.c', + 'capture.c', 'iface.c', 'nexthop.c', 'stats.c', diff --git a/modules/infra/cli/capture.c b/modules/infra/cli/capture.c new file mode 100644 index 000000000..0e976b516 --- /dev/null +++ b/modules/infra/cli/capture.c @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 Vincent Jardin, Free Mobile + +#include "cli.h" +#include "cli_event.h" +#include "cli_iface.h" +#include "pcapng.h" +#include "tty.h" + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +static volatile sig_atomic_t capture_running; + +static void capture_sigint(int /*sig*/) { + capture_running = 0; +} + +// Map iface_id to pcapng interface index (IDB order). +static int find_iface_idx(const struct gr_capture_ring *ring, uint16_t iface_id) { + const struct gr_capture_iface *ifaces = gr_capture_ring_ifaces_const(ring); + for (uint16_t i = 0; i < ring->n_ifaces; i++) { + if (ifaces[i].iface_id == iface_id) + return i; + } + return 0; +} + +static cmd_status_t capture_dump(struct gr_api_client *c, const struct ec_pnode *p) { + struct gr_capture_start_req *req = NULL; + struct gr_capture_start_resp resp; + struct gr_iface *iface = NULL; + void *resp_ptr = NULL; + uint16_t link_type; + size_t req_size; + int memfd = -1; + int ret; + + if (is_tty(stdout)) { + errorf("stdout is a terminal, redirect to a file or pipe to tcpdump"); + errno = EBADF; + return CMD_ERROR; + } + + req_size = sizeof(*req); + req = calloc(1, req_size); + if (req == NULL) + return CMD_ERROR; + + if (arg_str(p, "any") != NULL) { + req->iface_id = GR_IFACE_ID_UNDEF; + link_type = DLT_EN10MB; + } else { + iface = iface_from_name(c, arg_str(p, "NAME")); + if (iface == NULL) { + free(req); + return CMD_ERROR; + } + req->iface_id = iface->id; + switch (iface->type) { + case GR_IFACE_TYPE_VRF: + case GR_IFACE_TYPE_IPIP: + link_type = DLT_RAW; + break; + default: + link_type = DLT_EN10MB; + break; + } + free(iface); + } + + if (arg_u32(p, "SNAPLEN", &req->snap_len) < 0 && errno != ENOENT) { + free(req); + return CMD_ERROR; + } + + uint32_t max_count = 0; + if (arg_u32(p, "COUNT", &max_count) < 0 && errno != ENOENT) { + free(req); + return CMD_ERROR; + } + + const char *filter = arg_str(p, "FILTER"); + if (filter != NULL) { + pcap_t *pd = pcap_open_dead(link_type, req->snap_len ? req->snap_len : 65535); + if (pd == NULL) { + free(req); + errno = ENOMEM; + return CMD_ERROR; + } + struct bpf_program bpf = {0}; + if (pcap_compile(pd, &bpf, filter, 1, PCAP_NETMASK_UNKNOWN) < 0) { + errorf("pcap_compile: %s", pcap_geterr(pd)); + pcap_close(pd); + free(req); + errno = EINVAL; + return CMD_ERROR; + } + pcap_close(pd); + + size_t insn_bytes = bpf.bf_len * sizeof(struct gr_bpf_instruction); + req_size = sizeof(*req) + insn_bytes; + req = realloc(req, req_size); + if (req == NULL) { + pcap_freecode(&bpf); + errno = ENOMEM; + return CMD_ERROR; + } + req->filter.n_instructions = bpf.bf_len; + memcpy(req->filter.instructions, bpf.bf_insns, insn_bytes); + pcap_freecode(&bpf); + } + + // Send capture start and get memfd + ret = gr_api_client_send_recv_fd(c, GR_CAPTURE_START, req_size, req, &resp_ptr, &memfd); + free(req); + if (ret < 0) + return CMD_ERROR; + + memcpy(&resp, resp_ptr, sizeof(resp)); + free(resp_ptr); + + // Map the shared capture ring. + struct gr_capture_ring *ring = mmap( + NULL, + resp.memfd_size, + PROT_READ | PROT_WRITE, + MAP_SHARED | resp.mmap_flags, + memfd, + 0 + ); + close(memfd); + if (ring == MAP_FAILED) { + errorf("mmap: %s", strerror(errno)); + goto stop; + } + if (ring->magic != GR_CAPTURE_RING_MAGIC) { + errorf("invalid capture ring magic"); + munmap(ring, resp.memfd_size); + goto stop; + } + + // Write pcapng file header (SHB + IDBs). + if (pcapng_write_shb(stdout) < 0) { + munmap(ring, resp.memfd_size); + goto stop; + } + const struct gr_capture_iface *ifaces = gr_capture_ring_ifaces_const(ring); + for (uint16_t i = 0; i < ring->n_ifaces; i++) { + if (pcapng_write_idb(stdout, ifaces + i, ring->snap_len) < 0) { + munmap(ring, resp.memfd_size); + goto stop; + } + } + fflush(stdout); + + // Set up signal handlers to stop capture cleanly. + // Use sigaction() without SA_RESTART so that usleep() is + // interrupted and the loop checks capture_running promptly. + struct sigaction sa = {.sa_handler = capture_sigint}; + struct sigaction old_int, old_term, old_pipe; + sigaction(SIGINT, &sa, &old_int); + sigaction(SIGTERM, &sa, &old_term); + sigaction(SIGPIPE, &sa, &old_pipe); + capture_running = 1; + + // Read loop: poll ring, format pcapng EPBs, write stdout. + struct gr_capture_slot slot; + uint32_t pkt_count = 0; + while (capture_running && ring->magic == GR_CAPTURE_RING_MAGIC) { + if (!gr_capture_ring_dequeue(ring, &slot)) { + fflush(stdout); + usleep(100); + continue; + } + + uint64_t ts_ns = gr_capture_slot_timestamp_ns(ring, &slot); + int iface_idx = find_iface_idx(ring, slot.iface_id); + + ret = pcapng_write_epb( + stdout, + iface_idx, + ts_ns, + slot.cap_len, + slot.pkt_len, + slot.data, + slot.direction + ); + if (ret < 0) + break; + if (max_count > 0 && ++pkt_count >= max_count) + break; + } + + sigaction(SIGINT, &old_int, NULL); + sigaction(SIGTERM, &old_term, NULL); + sigaction(SIGPIPE, &old_pipe, NULL); + munmap(ring, resp.memfd_size); + +stop: + struct gr_capture_stop_req stop_req = {.capture_id = resp.capture_id}; + gr_api_client_send_recv(c, GR_CAPTURE_STOP, sizeof(stop_req), &stop_req, NULL); + return CMD_SUCCESS; +} + +#define CAPTURE_CTX(root) CLI_CONTEXT(root, CTX_ARG("capture", "Packet capture.")) + +static int ctx_init(struct ec_node *root) { + int ret; + + ret = CLI_COMMAND( + CAPTURE_CTX(root), + "(any|(iface NAME)) [(count COUNT),(snaplen SNAPLEN),(filter FILTER)]", + capture_dump, + "Capture packets and write pcapng to stdout.", + with_help("All interfaces.", ec_node_str("any", "any")), + with_help( + "Interface name.", + ec_node_dyn("NAME", complete_iface_names, INT2PTR(GR_IFACE_TYPE_UNDEF)) + ), + with_help( + "Stop after COUNT packets (0 = unlimited).", + ec_node_uint("COUNT", 0, UINT32_MAX, 10) + ), + with_help( + "Snap length in bytes (0 = full packet).", + ec_node_uint("SNAPLEN", 0, UINT32_MAX, 10) + ), + with_help("BPF filter expression (e.g. 'icmp').", ec_node("any", "FILTER")) + ); + if (ret < 0) + return ret; + + return 0; +} + +static struct cli_context ctx = { + .name = "capture", + .init = ctx_init, +}; + +static void capture_event_print(uint32_t event, const void *obj) { + const struct gr_capture_info *info = obj; + const char *action; + + switch (event) { + case GR_EVENT_CAPTURE_START: + action = "start"; + break; + case GR_EVENT_CAPTURE_STOP: + action = "stop"; + break; + default: + action = "?"; + break; + } + + printf("capture %s: id=%u iface=%u direction=%hhu packets=%lu\n", + action, + info->capture_id, + info->iface_id, + info->direction, + info->pkt_count); +} +static struct cli_event_printer printer = { + .name = "capture", + .print = capture_event_print, + .ev_count = 2, + .ev_types = { + GR_EVENT_CAPTURE_START, + GR_EVENT_CAPTURE_STOP, + }, +}; + +static void __attribute__((constructor, used)) init(void) { + cli_context_register(&ctx); + cli_event_printer_register(&printer); +} diff --git a/modules/infra/cli/meson.build b/modules/infra/cli/meson.build index 911031c36..489eb206a 100644 --- a/modules/infra/cli/meson.build +++ b/modules/infra/cli/meson.build @@ -5,17 +5,19 @@ cli_src += files( 'address.c', 'affinity.c', 'bond.c', + 'capture.c', 'events.c', 'graph.c', 'icmp.c', 'iface.c', - 'vrf.c', 'nexthop.c', + 'pcapng.c', 'port.c', 'route.c', 'stats.c', 'trace.c', 'vlan.c', + 'vrf.c', ) cli_inc += include_directories('.') diff --git a/modules/infra/cli/pcapng.c b/modules/infra/cli/pcapng.c new file mode 100644 index 000000000..37ef5e3ac --- /dev/null +++ b/modules/infra/cli/pcapng.c @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 Vincent Jardin, Free Mobile + +#include "pcapng.h" + +#include +#include + +#include +#include + +#define PCAPNG_BT_SHB 0x0A0D0D0A +#define PCAPNG_BT_IDB 0x00000001 +#define PCAPNG_BT_EPB 0x00000006 + +#define PCAPNG_OPT_IF_NAME 2 +#define PCAPNG_OPT_IF_TSRESOL 9 +#define PCAPNG_EPB_FLAGS 2 + +#define PCAPNG_BYTE_ORDER_MAGIC 0x1A2B3C4D + +// pcapng IDBs use LINKTYPE values (not DLT). For Ethernet they +// happen to be the same (1), but for raw IP DLT_RAW=14 while +// LINKTYPE_RAW=101. +#define PCAPNG_LINKTYPE_EN10MB 1 +#define PCAPNG_LINKTYPE_RAW 101 + +static uint16_t iface_type_to_linktype(gr_iface_type_t type) { + switch (type) { + case GR_IFACE_TYPE_VRF: + case GR_IFACE_TYPE_IPIP: + return PCAPNG_LINKTYPE_RAW; + default: + return PCAPNG_LINKTYPE_EN10MB; + } +} + +static int wr(FILE *f, const void *buf, size_t len) { + if (len == 0) + return 0; + if (fwrite(buf, len, 1, f) != 1) + return errno_set(EIO); + return 0; +} + +int pcapng_write_shb(FILE *f) { + struct { + uint32_t type, length; + uint32_t bom; + uint16_t major, minor; + int64_t section_len; + uint32_t length2; + } __attribute__((packed)) shb = { + .type = PCAPNG_BT_SHB, + .bom = PCAPNG_BYTE_ORDER_MAGIC, + .major = 1, + .section_len = -1, + }; + shb.length = sizeof(shb); + shb.length2 = sizeof(shb); + return wr(f, &shb, sizeof(shb)); +} + +int pcapng_write_idb(FILE *f, const struct gr_capture_iface *iface, uint32_t snap_len) { + struct __attribute__((packed)) { + // IDB header + uint32_t type, length; + uint16_t link_type, reserved; + uint32_t snap_len; + // if_name option (IFNAMSIZ=16 is already 4-byte aligned) + uint16_t name_type, name_len; + char name[IFNAMSIZ]; + // if_tsresol option (1 byte padded to 4) + uint16_t tsresol_type, tsresol_len; + uint8_t tsresol; + uint8_t _pad[3]; + // opt_endofopt + uint32_t opt_end; + // trailing block length + uint32_t length2; + } idb = { + .type = PCAPNG_BT_IDB, + .link_type = iface_type_to_linktype(iface->type), + .snap_len = snap_len, + .name_type = PCAPNG_OPT_IF_NAME, + .tsresol_type = PCAPNG_OPT_IF_TSRESOL, + .tsresol_len = 1, + .tsresol = 9, + }; + idb.name_len = strlen(iface->name); + gr_strcpy(idb.name, sizeof(idb.name), iface->name); + idb.length = sizeof(idb); + idb.length2 = sizeof(idb); + return wr(f, &idb, sizeof(idb)); +} + +int pcapng_write_epb( + FILE *f, + uint32_t iface_idx, + uint64_t timestamp_ns, + uint32_t cap_len, + uint32_t pkt_len, + const uint8_t *data, + uint8_t direction +) { + uint32_t data_padded = (cap_len + 3) & ~3u; + + struct __attribute__((packed)) { + uint32_t type, length; + uint32_t iface_id; + uint32_t ts_hi, ts_lo; + uint32_t cap_len, orig_len; + } hdr = { + .type = PCAPNG_BT_EPB, + .iface_id = iface_idx, + .ts_hi = (uint32_t)(timestamp_ns >> 32), + .ts_lo = (uint32_t)timestamp_ns, + .cap_len = cap_len, + .orig_len = pkt_len, + }; + + struct __attribute__((packed)) { + uint16_t flags_type, flags_len; + uint32_t flags_val; + uint32_t opt_end; + uint32_t length2; + } tail = { + .flags_type = PCAPNG_EPB_FLAGS, + .flags_len = 4, + .flags_val = direction & 0x3, + }; + + hdr.length = sizeof(hdr) + data_padded + sizeof(tail); + tail.length2 = hdr.length; + + if (wr(f, &hdr, sizeof(hdr)) < 0) + return -1; + if (wr(f, data, cap_len) < 0) + return -1; + uint8_t zero[4] = {0}; + if (wr(f, zero, data_padded - cap_len) < 0) + return -1; + return wr(f, &tail, sizeof(tail)); +} diff --git a/modules/infra/cli/pcapng.h b/modules/infra/cli/pcapng.h new file mode 100644 index 000000000..c791864b3 --- /dev/null +++ b/modules/infra/cli/pcapng.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 Vincent Jardin, Free Mobile + +#pragma once + +#include + +#include +#include + +// pcapng file writer. +// +// We write pcapng (not legacy pcap) because it supports per-interface +// metadata via Interface Description Blocks (IDB). This is needed for +// multi-interface captures where each packet carries the originating +// interface id. Legacy pcap only supports a single link type and has +// no concept of multiple interfaces. +// +// There is no standalone C library for writing pcapng. libpcap can +// read pcapng but only writes legacy pcap via pcap_dump(). DPDK has +// librte_pcapng but it operates on rte_mbuf, not raw byte buffers. +// The format is simple enough (SHB + IDB + EPB with 4-byte padding) +// that a minimal writer is preferable to pulling in a large dependency. +// +// Reference: https://www.ietf.org/archive/id/draft-tuexen-opsawg-pcapng-05.html + +// Write a Section Header Block. +int pcapng_write_shb(FILE *f); + +// Write an Interface Description Block. +int pcapng_write_idb(FILE *f, const struct gr_capture_iface *iface, uint32_t snap_len); + +// Write an Enhanced Packet Block. +int pcapng_write_epb( + FILE *f, + uint32_t iface_idx, + uint64_t timestamp_ns, + uint32_t cap_len, + uint32_t pkt_len, + const uint8_t *data, + uint8_t direction +); diff --git a/modules/infra/control/capture.c b/modules/infra/control/capture.c new file mode 100644 index 000000000..6cb44a0ed --- /dev/null +++ b/modules/infra/control/capture.c @@ -0,0 +1,401 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 Vincent Jardin, Free Mobile + +#include "capture.h" +#include "config.h" +#include "event.h" +#include "iface.h" +#include "log.h" +#include "module.h" +#include "rcu.h" + +#include +#include + +#include +#include +#ifdef RTE_LIB_BPF +#include +#endif +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +LOG_TYPE("capture"); + +#define CAPTURE_SNAP_MAX 4096 + +_Atomic(struct capture_session *) iface_capture[GR_MAX_IFACES]; + +struct capture_session_list active_captures = STAILQ_HEAD_INITIALIZER(active_captures); + +static void capture_set_flags(struct capture_session *s) { + if (s->iface_id != GR_IFACE_ID_UNDEF) { + struct iface *iface = iface_from_id(s->iface_id); + if (iface != NULL) { + iface->flags |= GR_IFACE_F_CAPTURE; + atomic_store_explicit(&iface_capture[iface->id], s, memory_order_release); + } + } else { + struct iface *iface = NULL; + while ((iface = iface_next(GR_IFACE_TYPE_UNDEF, iface)) != NULL) { + iface->flags |= GR_IFACE_F_CAPTURE; + atomic_store_explicit(&iface_capture[iface->id], s, memory_order_release); + } + } +} + +static void capture_clear_flags(struct capture_session *s) { + if (s->iface_id != GR_IFACE_ID_UNDEF) { + struct iface *iface = iface_from_id(s->iface_id); + if (iface != NULL) { + iface->flags &= ~GR_IFACE_F_CAPTURE; + atomic_store_explicit( + &iface_capture[iface->id], NULL, memory_order_release + ); + } + } else { + struct iface *iface = NULL; + while ((iface = iface_next(GR_IFACE_TYPE_UNDEF, iface)) != NULL) { + struct capture_session *cur = atomic_load_explicit( + &iface_capture[iface->id], memory_order_relaxed + ); + if (cur != s) + continue; + iface->flags &= ~GR_IFACE_F_CAPTURE; + atomic_store_explicit( + &iface_capture[iface->id], NULL, memory_order_release + ); + } + } +} + +static void iface_add_callback(uint32_t /*event*/, const void *obj) { + const struct iface *iface = obj; + struct capture_session *s; + + STAILQ_FOREACH (s, &active_captures, next) { + if (s->iface_id != GR_IFACE_ID_UNDEF) + continue; + iface_from_id(iface->id)->flags |= GR_IFACE_F_CAPTURE; + atomic_store_explicit(&iface_capture[iface->id], s, memory_order_release); + return; + } +} + +struct capture_session *capture_session_find(uint16_t capture_id) { + struct capture_session *s; + STAILQ_FOREACH (s, &active_captures, next) { + if (s->capture_id == capture_id) + return s; + } + return NULL; +} + +static int install_bpf_filter(struct capture_session *s, const struct gr_capture_filter *filter) { + struct bpf_program prog = {.bf_len = filter->n_instructions, .bf_insns = NULL}; + uint64_t (*jit_func)(void *) = NULL; + + if (prog.bf_len > 0) { + prog.bf_insns = calloc(filter->n_instructions, sizeof(*prog.bf_insns)); + if (prog.bf_insns == NULL) + return errno_set(ENOMEM); + memcpy(prog.bf_insns, filter->instructions, prog.bf_len * sizeof(*prog.bf_insns)); + } + +#ifdef RTE_LIB_BPF + struct rte_bpf *bpf = NULL; + if (prog.bf_len > 0) { + struct rte_bpf_prm *prm = rte_bpf_convert(&prog); + if (prm == NULL) { + LOG(ERR, "rte_bpf_convert: %s", rte_strerror(rte_errno)); + free(prog.bf_insns); + return errno_set(rte_errno); + } + + bpf = rte_bpf_load(prm); + rte_free(prm); + if (bpf == NULL) { + LOG(ERR, "rte_bpf_load: %s", rte_strerror(rte_errno)); + free(prog.bf_insns); + return errno_set(rte_errno); + } + + struct rte_bpf_jit jit = {.func = NULL}; + if (rte_bpf_get_jit(bpf, &jit) < 0 || jit.func == NULL) { + LOG(NOTICE, "BPF JIT not available, using interpreter"); + } else { + jit_func = jit.func; + } + } + rte_bpf_destroy(s->bpf_jit); + s->bpf_jit = bpf; +#endif + + struct bpf_insn *prev_instructions = s->bpf_prog.bf_insns; + s->bpf_prog = prog; + s->bpf_jit_func = jit_func; + rte_rcu_qsbr_synchronize(gr_datapath_rcu(), RTE_QSBR_THRID_INVALID); + free(prev_instructions); + + LOG(INFO, + "capture filter installed (%u instructions, JIT %s)", + filter->n_instructions, + jit_func ? "enabled" : "disabled"); + + return 0; +} + +struct capture_session *capture_session_start( + uint16_t iface_id, + gr_capture_dir_t direction, + uint32_t snap_len, + const struct gr_capture_filter *filter +) { + struct capture_session *s; + + if (iface_id != GR_IFACE_ID_UNDEF) { + struct iface *iface = iface_from_id(iface_id); + if (iface == NULL) { + errno = ENODEV; + return NULL; + } + if (atomic_load_explicit(&iface_capture[iface_id], memory_order_relaxed) != NULL) { + errno = EBUSY; + return NULL; + } + } else { + struct iface *iface = NULL; + while ((iface = iface_next(GR_IFACE_TYPE_UNDEF, iface)) != NULL) { + if (atomic_load_explicit(&iface_capture[iface->id], memory_order_relaxed) + != NULL) { + errno = EBUSY; + return NULL; + } + } + } + + s = calloc(1, sizeof(*s)); + if (s == NULL) + return NULL; + + s->memfd = -1; + s->iface_id = iface_id; + s->direction = direction; + s->snap_len = snap_len ? snap_len : CAPTURE_SNAP_MAX; + if (s->snap_len > GR_CAPTURE_SLOT_DATA_MAX) + s->snap_len = GR_CAPTURE_SLOT_DATA_MAX; + + // Count interfaces for the IDB table. For a specific iface + // capture, only that interface is listed. For "any", all are. + uint16_t n_ifaces = 0; + struct iface *iface = NULL; + if (iface_id != GR_IFACE_ID_UNDEF) { + n_ifaces = 1; + } else { + while ((iface = iface_next(GR_IFACE_TYPE_UNDEF, iface)) != NULL) { + switch (iface->type) { + case GR_IFACE_TYPE_VRF: + case GR_IFACE_TYPE_IPIP: + break; + default: + n_ifaces++; + break; + } + } + } + + uint32_t slot_count = GR_CAPTURE_SLOT_COUNT_DEFAULT; + s->memfd_size = gr_capture_ring_memsize(slot_count, n_ifaces); + + unsigned memfd_flags = MFD_CLOEXEC; + if (!gr_config.test_mode) { + memfd_flags |= MFD_HUGETLB | MFD_HUGE_2MB; + s->mmap_flags = MAP_HUGETLB | MAP_HUGE_2MB; + } + + s->memfd = memfd_create("grout-capture", memfd_flags); + if (s->memfd < 0) { + LOG(ERR, "memfd_create: %s", strerror(errno)); + goto err_free; + } + if (ftruncate(s->memfd, s->memfd_size) < 0) { + LOG(ERR, "ftruncate: %s", strerror(errno)); + goto err_close; + } + + // Seals are not supported with MFD_HUGETLB. + if (gr_config.test_mode) + fcntl(s->memfd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_SEAL); + + s->ring = mmap( + NULL, s->memfd_size, PROT_READ | PROT_WRITE, MAP_SHARED | s->mmap_flags, s->memfd, 0 + ); + if (s->ring == MAP_FAILED) { + LOG(ERR, "mmap: %s", strerror(errno)); + s->ring = NULL; + goto err_close; + } + + memset(s->ring, 0, s->memfd_size); + s->ring->magic = GR_CAPTURE_RING_MAGIC; + s->ring->version = GR_API_VERSION; + s->ring->slot_count = slot_count; + s->ring->slot_size = GR_CAPTURE_SLOT_SIZE; + s->ring->snap_len = s->snap_len; + s->ring->n_ifaces = n_ifaces; + s->ring->tsc_hz = rte_get_tsc_hz(); + s->ring->tsc_ref = rte_rdtsc(); + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + s->ring->realtime_ref_ns = (uint64_t)ts.tv_sec * 1000000000ULL + ts.tv_nsec; + + struct gr_capture_iface *itbl = gr_capture_ring_ifaces(s->ring); + if (iface_id != GR_IFACE_ID_UNDEF) { + iface = iface_from_id(iface_id); + itbl[0].iface_id = iface->id; + itbl[0].type = iface->type; + gr_strcpy(itbl[0].name, sizeof(itbl[0].name), iface->name); + } else { + uint16_t n = 0; + iface = NULL; + while ((iface = iface_next(GR_IFACE_TYPE_UNDEF, iface)) != NULL) { + switch (iface->type) { + case GR_IFACE_TYPE_VRF: + case GR_IFACE_TYPE_IPIP: + break; + default: + itbl[n].iface_id = iface->id; + itbl[n].type = iface->type; + gr_strcpy(itbl[n].name, sizeof(itbl[n].name), iface->name); + n++; + break; + } + } + s->ring->n_ifaces = n; + } + + if (filter != NULL && filter->n_instructions > 0) { + if (install_bpf_filter(s, filter) < 0) + goto err_unmap; + } + + static uint16_t capture_seq; + s->capture_id = ++capture_seq; + STAILQ_INSERT_TAIL(&active_captures, s, next); + capture_set_flags(s); + + event_push( + GR_EVENT_CAPTURE_START, + &(struct gr_capture_info) { + .capture_id = s->capture_id, + .direction = s->direction, + .iface_id = s->iface_id, + .pkt_count = s->bpf_passed, + .drops = s->drops, + } + ); + + LOG(INFO, + "capture %u started iface_id=%u direction=%u snap_len=%u", + s->capture_id, + iface_id, + direction, + s->snap_len); + return s; + +err_unmap: + munmap(s->ring, s->memfd_size); +err_close: + close(s->memfd); +err_free: + free(s); + return NULL; +} + +int capture_session_set_filter(uint16_t capture_id, const struct gr_capture_filter *filter) { + struct capture_session *s = capture_session_find(capture_id); + if (s == NULL) + return errno_set(ENOENT); + + if (install_bpf_filter(s, filter) < 0) + return errno_set(errno); + + return 0; +} + +void capture_session_stop(uint16_t capture_id) { + struct capture_session *s = capture_session_find(capture_id); + if (s == NULL) + return; + + capture_clear_flags(s); + STAILQ_REMOVE(&active_captures, s, capture_session, next); + + rte_rcu_qsbr_synchronize(gr_datapath_rcu(), RTE_QSBR_THRID_INVALID); + + event_push( + GR_EVENT_CAPTURE_STOP, + &(struct gr_capture_info) { + .capture_id = capture_id, + .direction = s->direction, + .iface_id = s->iface_id, + .pkt_count = s->bpf_passed, + .drops = s->drops, + } + ); + +#ifdef RTE_LIB_BPF + rte_bpf_destroy(s->bpf_jit); +#endif + free(s->bpf_prog.bf_insns); + + uint64_t bpf_passed = atomic_load(&s->bpf_passed); + uint64_t bpf_filtered = atomic_load(&s->bpf_filtered); + + if (s->ring != NULL) { + // Signal consumers that the session is gone. Consumers + // check ring->magic in their poll loop and exit when + // it changes. The mmap survives close so this write + // is visible to any process still mapped. + s->ring->magic = 0; + munmap(s->ring, s->memfd_size); + } + if (s->memfd >= 0) + close(s->memfd); + free(s); + + LOG(INFO, + "capture %u stopped (bpf_passed=%lu bpf_filtered=%lu)", + capture_id, + bpf_passed, + bpf_filtered); +} + +static void capture_fini(struct event_base *) { + struct capture_session *s; + while ((s = STAILQ_FIRST(&active_captures)) != NULL) + capture_session_stop(s->capture_id); +} + +static struct module module = { + .name = "capture", + .depends_on = "iface*,trace", + .fini = capture_fini, +}; + +RTE_INIT(capture_constructor) { + module_register(&module); + event_subscribe(GR_EVENT_IFACE_POST_ADD, iface_add_callback); + event_serializer(GR_EVENT_CAPTURE_START, NULL); + event_serializer(GR_EVENT_CAPTURE_STOP, NULL); +} diff --git a/modules/infra/control/capture.h b/modules/infra/control/capture.h new file mode 100644 index 000000000..6f9daede3 --- /dev/null +++ b/modules/infra/control/capture.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 Vincent Jardin, Free Mobile + +#pragma once + +#include + +#include + +#include +#include +#include + +struct rte_bpf; + +struct capture_session { + struct gr_capture_ring *ring; // mmap'd memfd pointer + int memfd; + size_t memfd_size; + uint32_t mmap_flags; // MAP_HUGETLB | MAP_HUGE_2MB or 0 + uint16_t capture_id; + uint16_t iface_id; // GR_IFACE_ID_UNDEF = all + gr_capture_dir_t direction; + uint32_t snap_len; + _Atomic uint64_t drops; + _Atomic uint64_t bpf_passed; // packets that passed the BPF filter + _Atomic uint64_t bpf_filtered; // packets rejected by BPF filter + uint64_t (*bpf_jit_func)(void *); // JIT function pointer, NULL if not supported + struct rte_bpf *bpf_jit; + struct bpf_program bpf_prog; + STAILQ_ENTRY(capture_session) next; +}; + +STAILQ_HEAD(capture_session_list, capture_session); +extern struct capture_session_list active_captures; + +// Per-interface capture session pointer, read atomically by datapath. +extern _Atomic(struct capture_session *) iface_capture[GR_MAX_IFACES]; + +struct capture_session *capture_session_start( + uint16_t iface_id, + gr_capture_dir_t direction, + uint32_t snap_len, + const struct gr_capture_filter *filter +); +int capture_session_set_filter(uint16_t capture_id, const struct gr_capture_filter *); +void capture_session_stop(uint16_t capture_id); +struct capture_session *capture_session_find(uint16_t capture_id); diff --git a/modules/infra/control/meson.build b/modules/infra/control/meson.build index c9008981c..fb1990d3b 100644 --- a/modules/infra/control/meson.build +++ b/modules/infra/control/meson.build @@ -3,6 +3,7 @@ src += files( 'bond.c', + 'capture.c', 'ctlplane.c', 'graph.c', 'group_nexthop.c', From 2d16ed07e50efc8d3cbb3fe3cacd000b2bffed67 Mon Sep 17 00:00:00 2001 From: Vincent Jardin Date: Wed, 15 Apr 2026 00:51:37 +0200 Subject: [PATCH 7/9] infra: hook capture into datapath and control plane nodes Add the capture_enqueue() inline function that writes packets into the shared memory ring. It skips mbufs already marked with a per-mbuf ol_flags dynflag (gr_captured) to prevent double-capture when a packet traverses multiple capture points. When a BPF filter is installed, only matching packets enter the ring. Add CAPTURE_VARS/CAPTURE_ENQUEUE/CAPTURE_FLUSH batching macros that accumulate consecutive packets for the same interface and flush with a single capture_enqueue() call, reducing the number of atomic operations from one per packet to one per burst run. Hook capture at the logical interface level: iface_input captures after VLAN resolution, iface_output before VLAN encapsulation. Also hook xconnect, xvrf, and ipip tunnel nodes in both directions. On the control plane, capture packets going to and from the Linux tap device in ctlplane.c. Signed-off-by: Vincent Jardin --- modules/infra/control/capture.c | 11 ++++ modules/infra/control/capture.h | 90 +++++++++++++++++++++++++++ modules/infra/control/ctlplane.c | 5 ++ modules/infra/datapath/iface_input.c | 2 + modules/infra/datapath/iface_output.c | 2 + modules/infra/datapath/xconnect.c | 3 + modules/infra/datapath/xvrf.c | 2 + modules/ipip/datapath_in.c | 2 + modules/ipip/datapath_out.c | 2 + 9 files changed, 119 insertions(+) diff --git a/modules/infra/control/capture.c b/modules/infra/control/capture.c index 6cb44a0ed..2ab42e77e 100644 --- a/modules/infra/control/capture.c +++ b/modules/infra/control/capture.c @@ -381,6 +381,16 @@ void capture_session_stop(uint16_t capture_id) { bpf_filtered); } +uint64_t capture_dynflag; + +static void capture_init(struct event_base *) { + const struct rte_mbuf_dynflag flag = {.name = "gr_captured"}; + int bit = rte_mbuf_dynflag_register(&flag); + if (bit < 0) + ABORT("rte_mbuf_dynflag_register(gr_captured): %s", rte_strerror(rte_errno)); + capture_dynflag = UINT64_C(1) << bit; +} + static void capture_fini(struct event_base *) { struct capture_session *s; while ((s = STAILQ_FIRST(&active_captures)) != NULL) @@ -390,6 +400,7 @@ static void capture_fini(struct event_base *) { static struct module module = { .name = "capture", .depends_on = "iface*,trace", + .init = capture_init, .fini = capture_fini, }; diff --git a/modules/infra/control/capture.h b/modules/infra/control/capture.h index 6f9daede3..be1fd9d63 100644 --- a/modules/infra/control/capture.h +++ b/modules/infra/control/capture.h @@ -3,9 +3,17 @@ #pragma once +#include "iface.h" +#include "rxtx.h" + #include #include +#include +#include +#include +#include +#include #include #include @@ -46,3 +54,85 @@ struct capture_session *capture_session_start( int capture_session_set_filter(uint16_t capture_id, const struct gr_capture_filter *); void capture_session_stop(uint16_t capture_id); struct capture_session *capture_session_find(uint16_t capture_id); + +// Dynamic ol_flags bit set on mbufs that have already been captured. +// Prevents double-capture when a packet traverses multiple capture points. +// Cleared automatically by rte_pktmbuf_reset() on mbuf alloc/rx. +extern uint64_t capture_dynflag; + +static inline void +capture_enqueue(const struct iface *iface, gr_capture_dir_t direction, struct rte_mbuf *m) { + if (!(iface->flags & GR_IFACE_F_CAPTURE)) + return; + if (m->ol_flags & capture_dynflag) + return; // already captured + + struct capture_session *s = atomic_load_explicit( + &iface_capture[iface->id], memory_order_relaxed + ); + if (s == NULL) + return; + + struct gr_capture_ring *ring = s->ring; + struct gr_capture_slot *slots = gr_capture_ring_slots(ring); + uint16_t vlan_id = iface_mbuf_data(m)->vlan_id; + uint32_t pkt_len = rte_pktmbuf_pkt_len(m); + uint32_t mask = ring->slot_count - 1; + uint32_t snap = ring->snap_len; + uint64_t tsc = rte_rdtsc(); + bool match = false; + uint32_t off = 0; + + if (s->bpf_jit_func != NULL) { + match = s->bpf_jit_func(m); + } else if (s->bpf_prog.bf_len != 0) { + const unsigned char *data = rte_pktmbuf_mtod(m, const unsigned char *); + struct pcap_pkthdr h = {.caplen = pkt_len, .len = pkt_len}; + match = pcap_offline_filter(&s->bpf_prog, &h, data); + } else { + match = true; + } + if (!match) { + atomic_fetch_add_explicit(&s->bpf_filtered, 1, memory_order_relaxed); + return; + } + + atomic_fetch_add_explicit(&s->bpf_passed, 1, memory_order_relaxed); + + uint32_t pos = atomic_fetch_add_explicit(&ring->prod_head, 1, memory_order_acquire); + struct gr_capture_slot *slot = &slots[pos & mask]; + if (vlan_id != 0) + pkt_len += sizeof(struct rte_vlan_hdr); + uint32_t cap_len = RTE_MIN(pkt_len, snap); + + slot->pkt_len = pkt_len; + slot->cap_len = cap_len; + slot->iface_id = iface->id; + slot->direction = direction; + slot->timestamp_tsc = tsc; + + if (vlan_id != 0) { + // Copy dst+src MACs (12 bytes). + memcpy(slot->data, rte_pktmbuf_mtod(m, void *), 2 * RTE_ETHER_ADDR_LEN); + + // Insert 802.1Q header: ethertype + TCI. + struct { + rte_be16_t eth_type; + rte_be16_t vlan_tci; + } vlan_hdr = { + .eth_type = RTE_BE16(RTE_ETHER_TYPE_VLAN), + .vlan_tci = rte_cpu_to_be_16(vlan_id), + }; + memcpy(slot->data + off, &vlan_hdr, sizeof(vlan_hdr)); + + off = 2 * RTE_ETHER_ADDR_LEN + sizeof(vlan_hdr); + } + + if (rte_pktmbuf_is_contiguous(m)) + memcpy(slot->data + off, rte_pktmbuf_mtod_offset(m, void *, off), cap_len); + else + rte_pktmbuf_read(m, off, cap_len, slot->data); + + atomic_store_explicit(&slot->sequence, pos + 1, memory_order_release); + m->ol_flags |= capture_dynflag; +} diff --git a/modules/infra/control/ctlplane.c b/modules/infra/control/ctlplane.c index 060a58646..4641db63f 100644 --- a/modules/infra/control/ctlplane.c +++ b/modules/infra/control/ctlplane.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-3-Clause // Copyright (c) 2025 Christophe Fontaine +#include "capture.h" #include "config.h" #include "control_input.h" #include "control_queue.h" @@ -61,6 +62,8 @@ void iface_cp_tx(void *obj, uintptr_t, const struct control_queue_drain *drain) if (d->iface->cp_fd == 0) goto end; + capture_enqueue(d->iface, GR_CAPTURE_DIR_OUT, m); + if (rte_pktmbuf_linearize(m) == 0) { data = rte_pktmbuf_mtod(m, char *); } else { @@ -229,6 +232,8 @@ static void iface_cp_poll(evutil_socket_t, short reason, void *ev_iface) { iface_mbuf_data(mbuf)->iface = iface; iface_mbuf_data(mbuf)->vlan_id = 0; + capture_enqueue(iface, GR_CAPTURE_DIR_IN, mbuf); + if (post_to_stack(iface_output, mbuf) < 0) { LOG(ERR, "post_to_stack: %s", strerror(errno)); goto err; diff --git a/modules/infra/datapath/iface_input.c b/modules/infra/datapath/iface_input.c index c17d92fc6..6acd43c1b 100644 --- a/modules/infra/datapath/iface_input.c +++ b/modules/infra/datapath/iface_input.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-3-Clause // Copyright (c) 2026 Robin Jarry +#include "capture.h" #include "graph.h" #include "iface.h" #include "log.h" @@ -88,6 +89,7 @@ iface_input_process(struct rte_graph *graph, struct rte_node *node, void **objs, } IFACE_STATS_INC(rx, m, d->iface); + capture_enqueue(d->iface, GR_CAPTURE_DIR_IN, m); edge = edges[d->iface->mode]; next: diff --git a/modules/infra/datapath/iface_output.c b/modules/infra/datapath/iface_output.c index 94a01bec4..127443c31 100644 --- a/modules/infra/datapath/iface_output.c +++ b/modules/infra/datapath/iface_output.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-3-Clause // Copyright (c) 2026 Robin Jarry +#include "capture.h" #include "graph.h" #include "iface.h" #include "log.h" @@ -90,6 +91,7 @@ static uint16_t iface_output_process( } IFACE_STATS_INC(tx, m, d->iface); + capture_enqueue(d->iface, GR_CAPTURE_DIR_OUT, m); d->iface = iface; edge = iface_type_edges[iface->type]; diff --git a/modules/infra/datapath/xconnect.c b/modules/infra/datapath/xconnect.c index 3bbd6676e..179c5d57e 100644 --- a/modules/infra/datapath/xconnect.c +++ b/modules/infra/datapath/xconnect.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-3-Clause // Copyright (c) 2025 Christophe Fontaine +#include "capture.h" #include "graph.h" #include "iface.h" #include "mbuf.h" @@ -29,12 +30,14 @@ xconnect_process(struct rte_graph *graph, struct rte_node *node, void **objs, ui peer = iface_from_id(iface->domain_id); IFACE_STATS_INC(rx, mbuf, iface); + capture_enqueue(iface, GR_CAPTURE_DIR_IN, mbuf); if (peer != NULL && peer->type == GR_IFACE_TYPE_PORT) { mbuf_data(mbuf)->iface = peer; edge = OUTPUT; IFACE_STATS_INC(tx, mbuf, peer); + capture_enqueue(peer, GR_CAPTURE_DIR_OUT, mbuf); } else { edge = NO_PORT; } diff --git a/modules/infra/datapath/xvrf.c b/modules/infra/datapath/xvrf.c index 1879c0f35..444c847b5 100644 --- a/modules/infra/datapath/xvrf.c +++ b/modules/infra/datapath/xvrf.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-3-Clause // Copyright (c) 2025 Maxime Leroy, Free Mobile +#include "capture.h" #include "eth.h" #include "graph.h" #include "ip4_datapath.h" @@ -44,6 +45,7 @@ xvrf_process(struct rte_graph *graph, struct rte_node *node, void **objs, uint16 // XXX: increment tx stats on source VRF IFACE_STATS_INC(rx, m, eth_data->iface); + capture_enqueue(eth_data->iface, GR_CAPTURE_DIR_IN, m); if (gr_mbuf_is_traced(m) || (eth_data->iface->flags & GR_IFACE_F_PACKET_TRACE)) { struct trace_vrf_data *t = gr_mbuf_trace_add(m, node, sizeof(*t)); diff --git a/modules/ipip/datapath_in.c b/modules/ipip/datapath_in.c index 688f095f6..a6f99baef 100644 --- a/modules/ipip/datapath_in.c +++ b/modules/ipip/datapath_in.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-3-Clause // Copyright (c) 2024 Robin Jarry +#include "capture.h" #include "eth.h" #include "graph.h" #include "ip4_datapath.h" @@ -67,6 +68,7 @@ ipip_input_process(struct rte_graph *graph, struct rte_node *node, void **objs, eth_data->domain = ETH_DOMAIN_LOCAL; edge = IP_INPUT; IFACE_STATS_INC(rx, mbuf, ipip); + capture_enqueue(ipip, GR_CAPTURE_DIR_IN, mbuf); next: if (gr_mbuf_is_traced(mbuf) || (ipip && ipip->flags & GR_IFACE_F_PACKET_TRACE)) { struct trace_ipip_data *t = gr_mbuf_trace_add(mbuf, node, sizeof(*t)); diff --git a/modules/ipip/datapath_out.c b/modules/ipip/datapath_out.c index 351042e84..4bb789dd1 100644 --- a/modules/ipip/datapath_out.c +++ b/modules/ipip/datapath_out.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-3-Clause // Copyright (c) 2024 Robin Jarry +#include "capture.h" #include "graph.h" #include "ip4.h" #include "ip4_datapath.h" @@ -73,6 +74,7 @@ ipip_output_process(struct rte_graph *graph, struct rte_node *node, void **objs, ip_set_fields(outer, &tunnel); IFACE_STATS_INC(tx, mbuf, iface); + capture_enqueue(iface, GR_CAPTURE_DIR_OUT, mbuf); // Resolve nexthop for the encapsulated packet. ip_data->nh = fib4_lookup(iface->vrf_id, ipip->remote); From d5528b22a0f0615d0a7d34fb137097c0462a6574 Mon Sep 17 00:00:00 2001 From: Vincent Jardin Date: Sat, 21 Mar 2026 01:15:33 +0100 Subject: [PATCH 8/9] pcap: add pcap-grout.so libpcap plugin Build pcap-grout.so as a shared module loaded at runtime by libpcap's plugin loader. The plugin connects to grout's API socket, starts a shared memory capture session, maps the ring and reads packets from it. It pushes BPF filters to grout's datapath for JIT execution so only matching packets enter the ring. Device names use the "grout:" prefix (e.g. "tcpdump -i grout:p0"). The "grout:all" device captures on all interfaces. Signed-off-by: Vincent Jardin --- meson.build | 1 + meson_options.txt | 5 + pcap/meson.build | 29 ++++ pcap/pcap-grout.c | 411 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 446 insertions(+) create mode 100644 pcap/meson.build create mode 100644 pcap/pcap-grout.c diff --git a/meson.build b/meson.build index f1b666e35..360eb901b 100644 --- a/meson.build +++ b/meson.build @@ -132,6 +132,7 @@ subdir('modules') subdir('cli') subdir('api') subdir('frr') +subdir('pcap') fs = import('fs') abidiff = find_program('abidiff', native: true, required: false) diff --git a/meson_options.txt b/meson_options.txt index 2f6a9c150..afdc439f9 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -20,3 +20,8 @@ option( 'tests', type: 'feature', value: 'auto', description: 'Build unit-tests. If set to "auto", only build if cmocka is found.', ) + +option( + 'pcap', type: 'feature', value: 'auto', + description: 'Build pcap-grout.so libpcap plugin. If set to "auto", only build if libpcap-dev is found.', +) diff --git a/pcap/meson.build b/pcap/meson.build new file mode 100644 index 000000000..e0472e8d5 --- /dev/null +++ b/pcap/meson.build @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 Vincent Jardin, Free Mobile + +if not get_option('pcap').allowed() + subdir_done() +endif + +if pcap_dep.type_name() != 'internal' + if not compiler.has_header('pcap/pcap-plugin.h', dependencies: pcap_dep) + # pcap-plugin.h not available — plugin loader not in this libpcap. + # This is expected on systems with stock libpcap. Skip silently. + warning('pcap/pcap-plugin.h not available, pcap grout plugin disabled') + subdir_done() + endif +endif + +pcap_plugin_dir = pcap_dep.get_variable('plugindir') + +shared_module( + 'pcap-grout', + files( + 'pcap-grout.c', + ), + include_directories: api_inc, + dependencies: [pcap_dep], + name_prefix: '', + install: true, + install_dir: pcap_plugin_dir, +) diff --git a/pcap/pcap-grout.c b/pcap/pcap-grout.c new file mode 100644 index 000000000..b75e285cf --- /dev/null +++ b/pcap/pcap-grout.c @@ -0,0 +1,411 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 Vincent Jardin, Free Mobile, Iliad + +// pcap-grout: libpcap capture plugin for grout (Graph Router). +// +// Connects to grout's UNIX API socket, sends CAPTURE_START to create +// a shared memory ring, then reads raw packets directly from the mmap'd +// ring. No DPDK dependency — uses only grout's public C API headers. +// +// Device names use the "grout:" prefix followed by the interface name: +// tcpdump -i grout:p0 +// tcpdump -i grout:any +// +// The grout daemon must be running and the API socket must be accessible +// (default: /run/grout.sock, override via GROUT_SOCK_PATH env var). +// +// This file is a libpcap plugin (.so) loaded at runtime by the pcap-plugin +// plugin loader. It links against libpcap (-lpcap) and calls +// pcap_plugin_* functions instead of accessing pcap_t fields directly. + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#define GROUT_PREFIX "grout:" +#define GROUT_PREFIX_LEN strlen(GROUT_PREFIX) +#define GROUT_POLL_US 100 + +struct pcap_grout { + struct gr_api_client *client; + struct gr_capture_ring *ring; + size_t ring_size; + uint16_t capture_id; + int nonblock; + uint64_t pkt_recv; + uint64_t pkt_drop; + struct timeval required_select_timeout; +}; + +static void pcap_grout_close(pcap_t *p) { + struct pcap_grout *pg = pcap_plugin_priv(p); + + if (pg->client) { + struct gr_capture_stop_req req = {.capture_id = pg->capture_id}; + gr_api_client_send_recv(pg->client, GR_CAPTURE_STOP, sizeof(req), &req, NULL); + gr_api_client_disconnect(pg->client); + pg->client = NULL; + } + if (pg->ring != NULL && pg->ring != MAP_FAILED) { + munmap(pg->ring, pg->ring_size); + pg->ring = NULL; + } + pcap_plugin_cleanup_live(p); +} + +static inline void grout_ts_to_timeval( + const struct gr_capture_ring *ring, + const struct gr_capture_slot *slot, + struct timeval *tv +) { + uint64_t ns = gr_capture_slot_timestamp_ns(ring, slot); + tv->tv_sec = (time_t)(ns / 1000000000ULL); + tv->tv_usec = (suseconds_t)((ns % 1000000000ULL) / 1000); +} + +static int pcap_grout_dispatch(pcap_t *p, int max_cnt, pcap_handler cb, u_char *cb_arg) { + struct pcap_grout *pg = pcap_plugin_priv(p); + int timeout_ms = pcap_plugin_get_timeout(p); + int snapshot = pcap_plugin_get_snapshot(p); + struct pcap_pkthdr hdr; + int waited_us = 0; + int pkt_cnt = 0; + + if (max_cnt <= 0) + max_cnt = INT_MAX; + + struct gr_capture_slot slot; + + while (pkt_cnt < max_cnt) { + if (pcap_plugin_check_break_loop(p)) + return PCAP_ERROR_BREAK; + + // Session stopped by grout (magic zeroed). + if (pg->ring->magic != GR_CAPTURE_RING_MAGIC) + break; + + if (!gr_capture_ring_dequeue(pg->ring, &slot)) { + if (pg->nonblock) + break; + if (timeout_ms > 0 && waited_us >= timeout_ms * 1000) + break; + usleep(GROUT_POLL_US); + waited_us += GROUT_POLL_US; + continue; + } + + waited_us = 0; + pg->pkt_recv++; + + uint32_t caplen = slot.cap_len; + if (caplen > (uint32_t)snapshot) + caplen = (uint32_t)snapshot; + + grout_ts_to_timeval(pg->ring, &slot, &hdr.ts); + hdr.caplen = caplen; + hdr.len = slot.pkt_len; + + struct bpf_insn *fcode = pcap_plugin_get_filter(p); + if (fcode == NULL || pcap_plugin_filter(fcode, slot.data, slot.pkt_len, caplen)) { + cb(cb_arg, &hdr, slot.data); + pkt_cnt++; + } else { + pg->pkt_drop++; + } + } + + return pkt_cnt; +} + +// Push the BPF filter to grout's datapath for JIT execution. +// Also install it locally as fallback for edge cases. +static int pcap_grout_setfilter(pcap_t *p, struct bpf_program *fp) { + struct pcap_grout *pg = pcap_plugin_priv(p); + + // Install locally first (libpcap keeps a copy). + if (pcap_plugin_install_bpf(p, fp) < 0) + return PCAP_ERROR; + + if (pg->client == NULL) + return 0; + + // Send the classic BPF bytecode to grout for datapath JIT. + struct gr_capture_set_filter_req *req; + size_t insn_bytes = fp->bf_len * sizeof(struct gr_bpf_instruction); + size_t req_size = sizeof(*req) + insn_bytes; + req = malloc(req_size); + if (req == NULL) + return 0; /* non-fatal: filter still runs locally */ + + // Pack: uint16_t bpf_len + bpf_insn[] + req->capture_id = pg->capture_id; + req->filter.n_instructions = fp->bf_len; + memcpy(req->filter.instructions, fp->bf_insns, insn_bytes); + + gr_api_client_send_recv(pg->client, GR_CAPTURE_SET_FILTER, req_size, req, NULL); + free(req); + + // Ignore errors: worst case, filtering happens on the consumer + // side only (the local BPF program is still installed). + return 0; +} + +static int pcap_grout_inject(pcap_t *p, const void * /*buf*/, int /*size*/) { + pcap_plugin_set_errbuf(p, "grout: packet injection not supported"); + return PCAP_ERROR; +} + +static int pcap_grout_stats(pcap_t *p, struct pcap_stat *ps) { + struct pcap_grout *pg = pcap_plugin_priv(p); + + if (ps == NULL) + return 0; + + ps->ps_recv = (u_int)pg->pkt_recv; + ps->ps_drop = (u_int)pg->pkt_drop; + ps->ps_ifdrop = 0; + + return 0; +} + +static int pcap_grout_setnonblock(pcap_t *p, int nonblock) { + struct pcap_grout *pg = pcap_plugin_priv(p); + + pg->nonblock = nonblock; + + return 0; +} + +static int pcap_grout_getnonblock(pcap_t *p) { + struct pcap_grout *pg = pcap_plugin_priv(p); + + return pg->nonblock; +} + +// Resolve an interface name to a grout iface_id. +// "any" returns GR_IFACE_ID_UNDEF (capture all ports). +static int grout_resolve_iface(struct gr_api_client *client, const char *name, uint16_t *iface_id) { + if (strcmp(name, "any") == 0) { + *iface_id = GR_IFACE_ID_UNDEF; + return 0; + } + + struct gr_iface_get_req req; + void *resp = NULL; + + memset(&req, 0, sizeof(req)); + req.iface_id = GR_IFACE_ID_UNDEF; + snprintf(req.name, sizeof(req.name), "%s", name); + + if (gr_api_client_send_recv(client, GR_IFACE_GET, sizeof(req), &req, &resp) < 0) + return -1; + + struct gr_iface_get_resp *r = resp; + *iface_id = r->iface.id; + free(resp); + + return 0; +} + +static int pcap_grout_activate(pcap_t *p) { + struct pcap_grout *pg = pcap_plugin_priv(p); + int snapshot = pcap_plugin_get_snapshot(p); + const char *sock_path, *ifname; + uint16_t iface_id; + int ret; + + ifname = pcap_plugin_get_device(p) + GROUT_PREFIX_LEN; + if (*ifname == '\0') { + pcap_plugin_set_errbuf(p, "grout: empty interface name"); + return PCAP_ERROR_NO_SUCH_DEVICE; + } + + sock_path = getenv("GROUT_SOCK_PATH"); + if (sock_path == NULL) + sock_path = GR_DEFAULT_SOCK_PATH; + + pg->client = gr_api_client_connect(sock_path); + if (pg->client == NULL) { + pcap_plugin_set_errbuf( + p, "grout: cannot connect to %s: %s", sock_path, strerror(errno) + ); + return PCAP_ERROR; + } + + if (grout_resolve_iface(pg->client, ifname, &iface_id) < 0) { + ret = PCAP_ERROR_NO_SUCH_DEVICE; + goto fail; + } + + struct gr_capture_start_resp cresp; + struct gr_capture_start_req creq = { + .iface_id = iface_id, + .snap_len = snapshot, + }; + int memfd = -1; + + void *resp = NULL; + ret = gr_api_client_send_recv_fd( + pg->client, GR_CAPTURE_START, sizeof(creq), &creq, &resp, &memfd + ); + if (ret < 0) { + pcap_plugin_set_errbuf(p, "grout: capture start failed: %s", strerror(errno)); + ret = PCAP_ERROR; + goto fail; + } + memcpy(&cresp, resp, sizeof(cresp)); + free(resp); + + if (memfd < 0) { + pcap_plugin_set_errbuf(p, "grout: server did not send capture fd"); + ret = PCAP_ERROR; + goto fail; + } + pg->ring_size = cresp.memfd_size; + pg->ring = mmap( + NULL, pg->ring_size, PROT_READ | PROT_WRITE, MAP_SHARED | cresp.mmap_flags, memfd, 0 + ); + close(memfd); + if (pg->ring == MAP_FAILED) { + pcap_plugin_set_errbuf(p, "grout: mmap: %s", strerror(errno)); + pg->ring = NULL; + ret = PCAP_ERROR; + goto fail; + } + + if (pg->ring->magic != GR_CAPTURE_RING_MAGIC) { + pcap_plugin_set_errbuf(p, "grout: invalid capture ring magic"); + ret = PCAP_ERROR; + goto fail; + } + + pg->capture_id = cresp.capture_id; + + // Determine the link type from the captured interface. For + // single-interface captures, use the interface's link type. For + // "any" captures, default to Ethernet since libpcap's API only + // supports a single link type per handle. + int dlt = DLT_EN10MB; + if (iface_id != GR_IFACE_ID_UNDEF) { + const struct gr_capture_iface *ifaces = gr_capture_ring_ifaces_const(pg->ring); + for (uint16_t i = 0; i < pg->ring->n_ifaces; i++) { + if (ifaces[i].iface_id == iface_id) { + switch (ifaces[i].type) { + case GR_IFACE_TYPE_VRF: + case GR_IFACE_TYPE_IPIP: + dlt = DLT_RAW; + break; + default: + break; + } + break; + } + } + } + pcap_plugin_set_linktype(p, dlt); + if (snapshot <= 0 || snapshot > PCAP_PLUGIN_SNAPLEN_MAX) + pcap_plugin_set_snapshot(p, PCAP_PLUGIN_SNAPLEN_MAX); + + struct pcap_plugin_ops ops = { + .read = pcap_grout_dispatch, + .inject = pcap_grout_inject, + .setfilter = pcap_grout_setfilter, + .getnonblock = pcap_grout_getnonblock, + .setnonblock = pcap_grout_setnonblock, + .stats = pcap_grout_stats, + .cleanup = pcap_grout_close, + .breakloop_func = pcap_plugin_breakloop, + }; + pcap_plugin_set_ops(p, &ops); + + pg->required_select_timeout.tv_sec = 0; + pg->required_select_timeout.tv_usec = GROUT_POLL_US; + pcap_plugin_set_select_timeout(p, &pg->required_select_timeout); + + return 0; + +fail: + pcap_grout_close(p); + return ret; +} + +static pcap_t *pcap_grout_create(const char *device, char *ebuf, int *is_ours) { + pcap_t *p; + + *is_ours = (strncmp(device, GROUT_PREFIX, GROUT_PREFIX_LEN) == 0); + if (!*is_ours) + return NULL; + + p = pcap_plugin_create_handle(ebuf, sizeof(struct pcap_grout)); + if (p == NULL) + return NULL; + + pcap_plugin_set_activate(p, pcap_grout_activate); + + return p; +} + +static int pcap_grout_findalldevs(pcap_if_list_t *devlistp, char *ebuf) { + struct gr_api_client *client; + const char *sock_path; + + sock_path = getenv("GROUT_SOCK_PATH"); + if (sock_path == NULL) + sock_path = GR_DEFAULT_SOCK_PATH; + + client = gr_api_client_connect(sock_path); + if (client == NULL) + return 0; /* grout not running, no devices to report */ + + struct gr_iface_list_req req = {.type = GR_IFACE_TYPE_UNDEF}; + const struct gr_iface *iface; + char devname[64]; + char desc[128]; + int ret; + + gr_api_client_stream_foreach (iface, ret, client, GR_IFACE_LIST, sizeof(req), &req) { + const char *type = gr_iface_type_name(iface->type); + snprintf(devname, sizeof(devname), "%s%s", GROUT_PREFIX, iface->name); + snprintf( + desc, + sizeof(desc), + "grout %s interface %s \"%s\"", + type, + iface->name, + iface->description + ); + if (pcap_plugin_add_dev(devlistp, devname, 0, desc, ebuf) == NULL) { + ret = PCAP_ERROR; + break; + } + } + + if (ret >= 0) { + snprintf(desc, sizeof(desc), "grout: capture on all interfaces"); + if (pcap_plugin_add_dev(devlistp, "grout:any", 0, desc, ebuf) == NULL) + ret = PCAP_ERROR; + } + + gr_api_client_disconnect(client); + + return (ret < 0) ? ret : 0; +} + +// Plugin entry point — discovered by libpcap's pcap-plugin.c via dlsym(). +extern struct pcap_plugin pcap_plugin_entry; +struct pcap_plugin pcap_plugin_entry = { + .abi_version = PCAP_PLUGIN_ABI_VERSION, + .name = "grout", + .findalldevs = pcap_grout_findalldevs, + .create = pcap_grout_create, +}; From f06987732d050aa1abbdc6f97fba097fa6a212e3 Mon Sep 17 00:00:00 2001 From: Vincent Jardin Date: Sat, 21 Mar 2026 01:16:01 +0100 Subject: [PATCH 9/9] smoke: add packet capture integration tests Test per-interface and all-interfaces capture via grcli capture dump piped through tcpdump. Verify capture list/stop lifecycle, concurrent captures on different interfaces, same-interface rejection, snaplen truncation, and post-stop reuse. Also test native tcpdump via the pcap-grout.so plugin: single interface capture, all-interfaces capture, device enumeration (tcpdump -D), and BPF datapath filtering. The BPF test sends mixed ICMP and UDP traffic with an "icmp" filter and checks that the ring packet count stays low, confirming the JIT filter runs in the datapath rather than in libpcap. Signed-off-by: Vincent Jardin --- smoke/_init.sh | 11 +++ smoke/capture_test.sh | 170 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100755 smoke/capture_test.sh diff --git a/smoke/_init.sh b/smoke/_init.sh index ded21385d..28713f5cd 100644 --- a/smoke/_init.sh +++ b/smoke/_init.sh @@ -399,3 +399,14 @@ wait_event() { if [ "${INTERACTIVE:-false}" = true ]; then tmux_new_window grcli grcli fi + +for lib in $builddir/subprojects/libpcap/libpcap.so*; do + if [ -f "$lib" ]; then + install -Dm 755 -t "$tmp/lib" "$lib" + smoke_setenv LD_LIBRARY_PATH "$tmp/lib" + fi +done +if [ -f $builddir/pcap/pcap-grout.so ]; then + install -Dm 755 -t "$tmp/lib/pcap" "$builddir/pcap/pcap-grout.so" + smoke_setenv PCAP_PLUGIN_DIR "$tmp/lib/pcap" +fi diff --git a/smoke/capture_test.sh b/smoke/capture_test.sh new file mode 100755 index 000000000..f6af51e00 --- /dev/null +++ b/smoke/capture_test.sh @@ -0,0 +1,170 @@ +#!/bin/bash +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 Vincent Jardin, Free Mobile + +. $(dirname $0)/_init.sh + +tcpdump --version + +port_add p0 +port_add p1 +grcli address add 172.16.0.1/24 iface p0 +grcli address add 172.16.1.1/24 iface p1 + +for n in 0 1; do + p=x-p$n + ns=n$n + netns_add $ns + move_to_netns $p $ns + ip -n $ns addr add 172.16.$n.2/24 dev $p + ip -n $ns route add default via 172.16.$n.1 +done + +# per-interface capture produces valid pcapng with ICMP packets +mark_events +cap=$tmp/capture-p0.pcapng +timeout 5 grcli capture iface p0 count 10 > "$cap" & +cap_pid=$! +wait_event "capture start" + +ip netns exec n0 ping -i0.01 -c10 -n 172.16.1.2 +wait $cap_pid + +[ -s "$cap" ] || fail "capture file is empty" +tcpdump -r "$cap" -n -c1 || fail "tcpdump cannot read pcapng" +tcpdump -r "$cap" -n | grep ICMP || fail "no ICMP packets in capture" + +# all-interfaces capture sees traffic on both ports +mark_events +cap_all=$tmp/capture-all.pcapng +timeout 5 grcli capture any count 10 > "$cap_all" & +cap_pid=$! +wait_event "capture start" + +ip netns exec n0 ping -i0.01 -c5 -n 172.16.1.2 +ip netns exec n1 ping -i0.01 -c5 -n 172.16.0.2 +wait $cap_pid + +[ -s "$cap_all" ] || fail "all-interfaces capture file is empty" +tcpdump -r "$cap_all" -n | grep ICMP || fail "no ICMP in all-iface capture" + +# killing the capture process frees the session for reuse +mark_events +cap_reuse=$tmp/capture-reuse.pcapng +timeout 5 grcli capture iface p0 count 5 > "$cap_reuse" & +cap_pid=$! +wait_event "capture start" + +ip netns exec n0 ping -i0.01 -c5 -n 172.16.0.1 +wait $cap_pid + +[ -s "$cap_reuse" ] || fail "restarted capture file is empty" + +# concurrent captures on different interfaces +mark_events +cap_first=$tmp/capture-first.pcapng +cap_second=$tmp/capture-second.pcapng +timeout 5 grcli capture iface p0 count 5 > "$cap_first" & +cap_pid_first=$! +timeout 5 grcli capture iface p1 count 5 > "$cap_second" & +cap_pid_second=$! +wait_event -c2 "capture start" + +ip netns exec n0 ping -i0.01 -c5 -n 172.16.1.2 +wait $cap_pid_first +wait $cap_pid_second + +[ -s "$cap_first" ] || fail "first concurrent capture file is empty" +[ -s "$cap_second" ] || fail "second concurrent capture file is empty" +tcpdump -r "$cap_first" -n | grep ICMP || fail "no ICMP in first concurrent capture" +tcpdump -r "$cap_second" -n | grep ICMP || fail "no ICMP in second concurrent capture" + +# second capture on the same interface must fail +mark_events +timeout 5 grcli capture iface p0 >/dev/null & +cap_pid=$! +wait_event "capture start" + +if grcli capture iface p0 count 1 >/dev/null; then + fail "duplicate capture on same iface should fail" +fi +kill $cap_pid +wait $cap_pid + +# snaplen truncation produces valid pcapng +mark_events +cap_snap=$tmp/capture-snap.pcapng +timeout 5 grcli capture iface p0 count 5 snaplen 64 > "$cap_snap" & +cap_pid=$! +wait_event "capture start" + +ip netns exec n0 ping -i0.01 -c5 -s 500 -n 172.16.1.2 +wait $cap_pid + +[ -s "$cap_snap" ] || fail "snaplen capture file is empty" +tcpdump -r "$cap_snap" -n -c1 || fail "tcpdump cannot read snaplen pcapng" + +# grcli capture with BPF filter +mark_events +cap_filter=$tmp/capture-filter.pcapng +timeout 5 grcli capture iface p0 count 5 filter icmp > "$cap_filter" & +cap_pid=$! +wait_event "capture start" + +ip netns exec n0 ping -i0.01 -c5 -n 172.16.1.2 +ip netns exec n0 bash -c 'for i in $(seq 1 10); do echo x > /dev/udp/172.16.1.2/9999 2>/dev/null; done' +wait $cap_pid + +[ -s "$cap_filter" ] || fail "filtered capture file is empty" +tcpdump -r "$cap_filter" -n | grep ICMP || fail "no ICMP in filtered capture" +if tcpdump -r "$cap_filter" -n | grep UDP; then + fail "UDP leaked through BPF filter" +fi + +# native tcpdump captures ICMP on a single interface +mark_events +cap_native=$tmp/capture-native.pcapng +timeout 5 tcpdump -i grout:p0 -w "$cap_native" -c5 & +td_pid=$! +wait_event "capture start" + +ip netns exec n0 ping -i0.01 -c10 -n 172.16.1.2 +wait $td_pid 2>/dev/null || true + +[ -s "$cap_native" ] || fail "native tcpdump capture is empty" +tcpdump -r "$cap_native" -n | grep ICMP || fail "no ICMP in native tcpdump capture" + +# native tcpdump -D lists grout interfaces +tcpdump -D | grep "grout:p0" || fail "grout:p0 not listed by tcpdump -D" + +# native tcpdump on grout:any captures traffic +mark_events +cap_native_all=$tmp/capture-native-all.pcapng +timeout 5 tcpdump -i grout:any -w "$cap_native_all" -c5 & +td_pid=$! +wait_event "capture start" + +ip netns exec n0 ping -i0.01 -c10 -n 172.16.1.2 +wait $td_pid 2>/dev/null || true + +[ -s "$cap_native_all" ] || fail "native tcpdump all-capture is empty" +tcpdump -r "$cap_native_all" -n | grep ICMP || fail "no ICMP in native all-capture" + +# native tcpdump with BPF filter +mark_events +cap_bpf=$tmp/capture-bpf.pcapng +timeout 5 tcpdump -i grout:p0 -w "$cap_bpf" 'icmp' -c5 & +td_pid=$! +wait_event "capture start" + +ip netns exec n0 bash -c 'for i in $(seq 1 20); do echo x > /dev/udp/172.16.1.2/9999 2>/dev/null; done' & +udp_pid=$! +ip netns exec n0 ping -i0.01 -c10 -n 172.16.1.2 & +ping_pid=$! +wait $ping_pid $udp_pid $td_pid + +[ -s "$cap_bpf" ] || fail "BPF filtered capture is empty" +tcpdump -r "$cap_bpf" -n | grep ICMP || fail "no ICMP in BPF filtered capture" +if tcpdump -r "$cap_bpf" -n | grep UDP; then + fail "UDP leaked through BPF filter" +fi