bpf-ninja

module
v0.26.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 15, 2026 License: Apache-2.0

README ΒΆ

bpf-ninja

[!NOTE] This project was renamed from xdp-ninja (July 2026): the tool has outgrown XDP β€” it also observes tc-bpf programs, and more BPF hook points are planned. GitHub redirects the old repository URL, but the Go module path changed to github.com/takehaya/bpf-ninja, so go install / imports need the new path. Environment variables are now BPF_NINJA_* (the old XDP_NINJA_* names are no longer read).

bpf-ninja captures packets at BPF hook points β€” XDP, tc-bpf, cgroup-skb, and netfilter, with more planned. tcpdump runs below XDP and can't show what XDP or tc-bpf did to the packet, and cBPF filters can't walk into VXLAN / GTP / MPLS / SRv6 inner headers. Attach via fentry/fexit to a running XDP without modifying it, or --mode xdp for standalone capture on a netdev. Filters use the built-in DSL by default β€” chains like eth/ipv4/udp/vxlan/eth/ipv4/tcp. Plain tcpdump syntax via cbpfc is still accepted via --cbpf, kept for backwards compatibility and planned to retire once the DSL surface stabilises. Output is pcap (pcapng) to stdout.πŸ₯·

Install

# One-liner (downloads pre-built binary from GitHub Releases, requires jq)
curl -fsSL https://raw.githubusercontent.com/takehaya/bpf-ninja/main/scripts/install.sh | sudo bash

# Specific version
curl -fsSL https://raw.githubusercontent.com/takehaya/bpf-ninja/main/scripts/install.sh | sudo bash -s -- --version v0.1.0

# Or via go install (requires Go + libpcap-dev)
go install github.com/takehaya/bpf-ninja/cmd/bpf-ninja@latest

# Or build from source
git clone https://github.com/takehaya/bpf-ninja.git
cd bpf-ninja
make build

Modes

--mode selects the capture point; the hook kind (XDP, TC clsact, cgroup-skb, netfilter) is auto-detected from the target program's type:

Mode Attach via Existing program needed Sees return action Typical use
entry (default) fentry on the target program yes (with BTF) no β€” packet only observe what reaches the production program
exit fexit on the target program yes (with BTF) yes (XDP_PASS / TC_ACT_OK / SK_PASS / ...) observe the verdict; filter on action == ...
xdp attach as the primary XDP on the netdev no β€” fails if one is attached n/a (bpf-ninja decides; always returns XDP_PASS) capture on a netdev with no XDP, no BTF needed

Supported hooks and how to name the target:

Hook Target selection Notes
XDP -i <iface> (interface's XDP) or -p <progID>
TC clsact -p <progID> only (interface lookup for clsact is not yet wired) the kernel moves the outer VLAN tag into skb metadata first, so by default DSL filters see the frame without it β€” see --tc-vlan-reinsert
cgroup-skb --cgroup <cgroup v2 path> (enumerates attached programs) or -p <progID> packet bytes start at the IP header β€” root DSL chains at ipv4/ipv6, pcap-ng is LINKTYPE_RAW
netfilter -p <progID> (kernel 6.4+, programs attached to NF_INET_* hooks via bpf_link) packet bytes start at the IP header, same L3-start notes as cgroup-skb; exit-mode verdicts are NF_DROP / NF_ACCEPT

entry/exit are non-invasive: the target program is unmodified, attach is via BPF trampoline. xdp is the standalone path for "I just want to capture, there's nothing else here". tc-entry/tc-exit remain as deprecated aliases for entry/exit.

--mode is repeatable: --mode entry EXPR --mode exit EXPR captures only the packets that match both β€” the entry filter on the packet as the program received it, the exit filter on the packet as the program left it (after decap / rewrite) plus the verdict. The entry image is held in a per-CPU slot until the verdict is known, so you can ask for "the pre-decap header of every packet the program dropped" without exporting anything else. --emit both (default) writes the entry and the exit image, --emit entry / --emit exit just one. Entry records go to a <hook>:entry pcap-ng interface and every record carries an opaque epb_packetid (CPU + per-CPU sequence, no kernel address) shared by the two images of one invocation so they pair up. -c counts records (-c 10 with --emit both is 5 pairs); merge and convert output drops the id, so pair on the per-CPU shard files or the raw-dump. Under ring pressure an entry record can appear without its exit twin (the exit-side reservation failed after the entry was written), so treat an unpaired entry as a drop, not as a verdict:

A gated capture attaches two probes to the target, which kernels in the CVE-2026-92485 window (7.0 up to the 7.2.6 / 7.3-rc1 fix) refuse when the target performs tail calls, as Cilium and Katran's inline decap do. bpf-ninja stops with an explanation instead of attempting it, because the failed attach can leave the kernel's trampoline inconsistent and panic the machine. Capture one point at a time there, or use a kernel that carries the fix.

sudo bpf-ninja -i eth0 --mode entry "eth/ipv4/udp[dport==6081]" --mode exit "eth/ipv4/tcp where action == XDP_DROP" -w both.pcapng

Usage

# Default: observe via fentry on whatever XDP is attached to eth0
sudo bpf-ninja -i eth0 | tcpdump -n -r -

# Filter (DSL is the default β€” no flag needed)
sudo bpf-ninja -i eth0 "eth/ipv4/tcp[dport==80]" | tcpdump -n -r -

# fexit β€” filter on the XDP return action
sudo bpf-ninja -i eth0 --mode exit "eth/ipv4/tcp where action == XDP_DROP"

# Standalone XDP attach (no existing XDP needed)
sudo bpf-ninja --mode xdp -i eth0 "eth/ipv4/tcp[dport==443]" | tcpdump -n -r -

# Legacy tcpdump/cBPF syntax (--cbpf opt-in, prints a deprecation notice)
sudo bpf-ninja --cbpf -i eth0 "host 10.0.0.1 and tcp port 80" | tcpdump -n -r -

# Write to pcap file, stop after 100 packets
# (output is sharded across per-CPU files; see "Sharded output" below)
sudo bpf-ninja -i eth0 -w capture.pcap -c 100

# Attach by BPF program ID β€” works for XDP, tc clsact, cgroup-skb, and netfilter
# programs alike (the hook is auto-detected from the program type)
sudo bpf-ninja -p 42 | tcpdump -n -r -

# cgroup-skb: capture on whatever is attached to a cgroup v2 path.
# Packets start at the IP header, so chains root at ipv4/ipv6.
sudo bpf-ninja --cgroup /sys/fs/cgroup/my-service "ipv4/tcp[dport==443]" | tcpdump -n -r -
sudo bpf-ninja --cgroup /sys/fs/cgroup/my-service --mode exit "ipv4/tcp where action == SK_DROP"

# List BTF functions in the target program
sudo bpf-ninja -i eth0 --list-funcs

# Attach to a specific __noinline subfunction (entry/exit only)
sudo bpf-ninja -i eth0 --func process_packet | tcpdump -n -r -

# Attach several (program, func) pairs in one run: -p and --func are
# repeatable, and each func attaches in every listed program whose BTF
# has it. All attach points share one ringbuf and merge into one pcap.
# (e.g. UL + DL capture points living in separate per-direction programs)
sudo bpf-ninja -p 1610 -p 1611 -p 1614 \
  --func upf_capture_point_ul --func upf_capture_point_dl -w all.pcap

# Match against a runtime-updatable set (pinned BPF hash map): the map
# key IS the value to match; entries added/removed while capturing take
# effect immediately, no re-attach. Manage entries by field name via
# `bpf-ninja set` (schema comes from the map's BTF).
sudo bpf-ninja set create /sys/fs/bpf/subs --key "imsi:u64"
sudo bpf-ninja set add /sys/fs/bpf/subs imsi=999990000000001
sudo bpf-ninja -p 1661 --func upf_capture_point_ul \
  --set "subs=/sys/fs/bpf/subs" --arg-filter "@subs" -w subs.pcap
# Capacity defaults to 1024 (set create --max-entries); grow it later
# with `set resize` (entries and schema are preserved, takes effect
# from the next attach)
sudo bpf-ninja set resize /sys/fs/bpf/subs --max-entries 4096

# Key the set off a PACKET field instead of a function arg β€” for values
# that live in the packet (GTP TEID, SRv6 SID). Works on fentry and xdp.
sudo bpf-ninja set create /sys/fs/bpf/teids --key "teid:u32"
sudo bpf-ninja set add /sys/fs/bpf/teids teid=0x3039
sudo bpf-ninja -p 1661 --set "teids=/sys/fs/bpf/teids" \
  'eth/ipv4/udp/gtp[teid in @teids]' -w teids.pcap

# A 128-bit SRv6 SID (the IPv6 destination): key type "ipv6", value an IPv6 literal
sudo bpf-ninja set create /sys/fs/bpf/sids --key "dst:ipv6"
sudo bpf-ninja set add /sys/fs/bpf/sids dst=fc00::1
sudo bpf-ninja -i eth0 --mode xdp --set "sids=/sys/fs/bpf/sids" \
  'eth/ipv6[dst in @sids]'
Filter syntax

The filter expression is interpreted as the built-in DSL by default. Write it as a protocol stack chain β€” it covers everything tcpdump's cBPF can express and adds the multi-encapsulation cases cBPF can't (MPLS label stacks, VXLAN inner Ethernet, GTP-U inner IP, SRv6, …):

# IPv4/TCP, dport 443
sudo bpf-ninja -i eth0 "eth/ipv4/tcp[dport==443]"

# Up to 3 VLAN tags before IPv4
sudo bpf-ninja -i eth0 "eth/vlan{1,3}/ipv4/tcp"

# MPLS label stack (terminates at the s-bit)
sudo bpf-ninja -i eth0 "eth/mpls+/ipv4/tcp"

# VXLAN inner IPv4/TCP
sudo bpf-ninja -i eth0 "eth/ipv4/udp/vxlan/eth/ipv4/tcp"

# Capture only headers + 64 bytes when the inner TCP dport > 1024
sudo bpf-ninja -i eth0 \
  "eth/ipv4/tcp capture headers+64 where tcp.dport > 1024"

# fexit: filter on the XDP return action
sudo bpf-ninja -i eth0 --mode exit \
  "eth/ipv4/tcp where action == XDP_DROP"

A filter reads the first 512 bytes of the packet. On entry/exit only that prefix is copied for the filter, so layers and fields past it never match. On --mode xdp a chain of fixed-size headers can read further, but variable-length walks (IPv4/TCP options, IPv6 extension headers, SRv6 segments) and everything read after them must stay inside the first 512 bytes, or the packet is rejected.

At a tc target the kernel has normally already moved the outer VLAN tag into skb metadata: always at ingress, and at egress for frames sent through a VLAN device (with its default reorder_hdr on). A tagged frame written to a packet socket keeps its tag in the bytes at egress; --tc-vlan-reinsert then leaves the bytes as they are, so both kinds look the same to the filter. By default the filter reads the bytes the kernel holds: eth/ipv4/tcp also matches single-tagged frames, and a filter that requires the outer tag (eth/vlan[tci==100]/...) is a compile error, since no tagged frame carries it. An optional tag may be read (eth/vlan[tci==100]?/..., where vlan.tci == ...): on a single-tagged frame the layer is absent, so the predicate is skipped and the where atom is false. Only the outermost tag is moved: on a QinQ frame the inner C-tag stays in the bytes (so eth/vlan?/ipv4/tcp matches it, a read of vlan sees the C-tag, and eth/ipv4/tcp does not match), and tags inside a tunnel are always readable. --tc-vlan-reinsert puts the tag back into the bytes the filter reads, so filters are written against the frame as it was on the wire. Captured bytes are the kernel's either way, without a tag from metadata; with the flag, a layer-bounded capture such as capture vlan takes 4 more bytes. Details: dsl-usage.md.

Run bpf-ninja --dsl-help for the grammar + bundled protocol catalogue, or bpf-ninja --dsl-help <proto> (e.g. --dsl-help ipv4) to see a protocol's field list, dispatch parents/children, and any variable-layout note.

User-facing CLI guide: docs/ja/dsl-usage.md. Internal architecture, codegen ABI, vocab authoring, and P4-16 conformance: docs/ja/dsl-internals.md. Formal grammar (EBNF): docs/ja/dsl-grammar.md. Index: docs/ja/dsl-overview.md.

bpf-ninja's .p4 vocab files are a strict subset of P4-16 (see internals Β§5). They are NOT a full p4c-compatible program: the bundled fragments declare only header / const / parser blocks (no action / table / control / apply / extern).

Legacy: tcpdump syntax (--cbpf)

Pass --cbpf to interpret the filter expression as tcpdump syntax and compile it to eBPF via cbpfc. Kept for backwards compatibility. Each invocation prints a deprecation notice on stderr; the flag is expected to retire once the DSL surface stabilises.

sudo bpf-ninja --cbpf -i eth0 "host 10.0.0.1 and tcp port 80"
Sharded output

For high-rate captures, bpf-ninja uses a per-CPU sharded ringbuf and each CPU writes its own pcap-ng file with no lock. With -w capture.pcap:

capture.pcap          # all shards merged, time-ordered (ready to use)
capture.pcap.cpu0     # CPU 0 packets (kept)
capture.pcap.cpu1     # CPU 1 packets (kept)
...

When capture stops (Ctrl-C or -c), bpf-ninja merges the per-CPU shards into capture.pcap as a single time-ordered pcap-ng, so the base path is ready to open directly (tcpdump -r capture.pcap). The .cpuN shards are left in place; you can also read one with tcpdump -r capture.pcap.cpu0 or merge them yourself with mergecap -w merged.pcap capture.pcap.cpu*. bpf-ninja convert handles --raw-dump .raw shards, not pcap-ng shards.

Without -w (streaming to stdout), all CPUs are merged into a single pcap-ng stream (serialized), so sudo bpf-ninja ... | tcpdump -r - shows every core's packets. Use -w for high-rate captures β€” stdout serialization is for the interactive path.

Performance flags (high-rate captures)
Flag Purpose
--snaplen N Cap per-packet capture bytes (CLI override). Default = full packet (1500 B), libpcap-equivalent
--fast-reader mmap+atomic ringbuf reader (lower CPU than cilium/ebpf generic)
--no-wakeup Suppress eventfd wake per submit. Trades p50 latency for throughput. Requires --fast-reader
--ringbuf-size MB Total ringbuf data budget in MiB, a power of two (default 64); divided across possible CPU IDs, with a 64 KiB minimum per shard
--raw-dump Raw bytes path; convert offline with bpf-ninja convert
--rx-cores N Pin readers to permitted CPU IDs at or above N. All producer shards are drained. Requires --fast-reader; configure RX affinity separately.
--busy-poll Spin the fast-reader shards instead of sleeping in epoll_wait. Burns a core per shard. Requires --fast-reader
--null-output Drop output entirely (bench only)
--rx-hwts Use NIC hardware timestamps (bpf_xdp_metadata_rx_timestamp kfunc, Linux 6.8+). Requires --mode xdp; falls back to software timestamps when the kfunc or driver lacks support
--legacy-timestamp Replace the per-packet kernel bpf_ktime_get_ns() timestamp with userland time.Now() taken when the reader reads the record (shared across a read batch on the default reader). No effect with --raw-dump
--in-memory-buffer MB Buffer up to MB MiB per shard in pre-touched heap and write it out only when the buffer fills or at exit, keeping write(2) off the hot path. Memory use is MB Γ— shard count. Requires --raw-dump
--observer-prefetch Entry/exit only: always copy the full 512-byte scratch instead of only the prefix the filter needs. Costs observer CPU; on an ice-driver XDP_TX target it sped up the observed program by warming the L1 dcache. Default off

On exit, capture prints a capture status=... line to stderr for both readers. It reports selected export attempts, submitted records, reserve/lookup/copy failures, consumed and written records, intentional limits, and records drained after producers stop. A producer or output failure makes the status incomplete. Filter and parse rejections are explicitly unmeasured; the counters do not measure all observed packets. See Capture completion for the fields and the scope of complete.

Detailed flag reference + DSL capture clause's snaplen trade-off: docs/ja/dsl-usage.md.

High-rate tuning β€” which lever in which order, and what doesn't work: docs/ja/tuning.md.

Diagnostics and benchmarking flags
Flag Purpose
--bench-drop --mode xdp only (ignored otherwise): return XDP_DROP for matched packets after capturing them, so they never reach the host stack. Non-matching packets still pass. Benchmarks only
--latency-sample-period N Sample every Nth record's BPF-submit β†’ reader-read latency per shard. Requires --raw-dump and --fast-reader; otherwise nothing is sampled
--latency-sample-output FILE Write the latency samples as TSV (one ns value per line); empty prints a percentile summary to stderr
--cpuprofile FILE Write a Go CPU profile
--no-async-preempt-off By default bpf-ninja re-execs with GODEBUG=asyncpreemptoff=1; this flag keeps Go's SIGURG-based preemption enabled (diagnostic opt-out)
--no-cpu-affinity Do not pin shard readers to CPUs (also disables --rx-cores pinning). Diagnostic opt-out
Hand-test: --dump-asm

--dump-asm compiles a filter and prints the resulting eBPF asm without loading. No -i/-p needed:

# Just the filter body (kunai/cbpfc Main + Callbacks + CaptureInfo)
bpf-ninja --dump-asm filter "eth/ipv4/tcp where tcp.dport == 443"
bpf-ninja --dump-asm filter --cbpf "tcp port 443"     # legacy tcpdump syntax

# Wrapped program with prologue/epilogue (mode-aware)
bpf-ninja --dump-asm full --mode entry "eth/ipv4/tcp[dport==443]"
bpf-ninja --dump-asm full --mode exit  "eth/ipv4/tcp where action == XDP_DROP"
bpf-ninja --dump-asm full --mode xdp --cbpf "tcp port 443"

Use this to sanity-check DSL parse/type errors, inspect codegen output, or verify the wrapped program shape per mode.

Resolver warnings, such as the never-matching second member of eth/ipv4/(tcp|tcp), go to stderr as warning: lines, the same lines a live capture prints. They come before the asm, and stdout carries only the asm, so dumps can be diffed or piped.

Attaching to subfunctions (entry/exit only)

You can use --func to attach fentry/fexit to a __noinline subfunction inside the target XDP program, instead of the entry function. The subfunction must take struct xdp_md *ctx as its first argument.

Use --list-funcs to discover available functions:

sudo bpf-ninja -i eth0 --list-funcs

Both global and static __noinline subfunctions work:

/* Global β€” always survives in BTF */
__attribute__((noinline))
int classify_packet(struct xdp_md *ctx) {
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;
    if (data + 1 > data_end) return 1;
    return 2;
}

/* Static β€” also works, but the body must be non-trivial */
static __attribute__((noinline))
int parse_headers(struct xdp_md *ctx) {
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;
    if (data + 1 > data_end) return -1;
    return 2;
}

Note: The subfunction must have a non-trivial body (e.g. access ctx->data). A trivial body like return 2; will be constant-folded by clang -O2, eliminating the bpf2bpf call entirely.

Note: Arguments you want to read with --arg-filter / --arg-echo must be used by the subfunction body. clang -O2 drops unused arguments from the calling convention even though BTF still lists them, so the fentry trampoline saves whatever stale value the caller left in that register β€” --list-params looks fine, unfiltered capture works, but exact matches never hit. Pin the arguments with KEEP_ARGS() from include/keep_args.h (zero instructions, it only marks them as used):

#include "keep_args.h"

__attribute__((noinline))
int capture_point(struct xdp_md *ctx, __u64 imsi, __u32 teid) {
    KEEP_ARGS(ctx, imsi, teid);
    return 0;
}

If an exact --arg-filter never matches, check the value the trampoline actually saved with --arg-echo first.

--func, --list-funcs, --list-progs, --list-params, and --arg-filter only apply to entry/exit modes (xdp-native has no tracing args or BTF subfunction concept).

Options

Option Description Modes
-i, --interface Network interface to capture on (XDP hook) entry, exit, xdp
-p, --prog-id BPF program ID to attach to β€” any supported hook, auto-detected (alternative to -i) entry, exit
--prog-name Select target program(s) by name instead of ID (requires -i); resolved against the interface's reachable program tree. Repeatable. Kernel names are truncated to 15 chars, so a full name matches by prefix entry, exit
--cgroup cgroup v2 path; targets the cgroup-skb program(s) attached to it (alternative to -i / -p) entry, exit
--mode entry (default), exit, xdp (tc-entry/tc-exit are deprecated aliases). Repeatable: --mode entry EXPR --mode exit EXPR captures only packets matching both (see Modes above) β€”
--emit With two --mode: both (default), entry, or exit β€” which image(s) to write for a packet that matched both filters entry+exit
-w, --write Write to pcap file instead of stdout all
-c, --count Stop after N packets (0 = unlimited) all
-v, --verbose Verbose output to stderr all
--cbpf Use the legacy tcpdump/cBPF syntax (compiled via cbpfc); default is the built-in DSL. Prints a deprecation notice when used. all
--tc-vlan-reinsert At a tc target, put the outer VLAN tag the kernel moved into skb metadata back into the bytes the DSL filter reads (eth/vlan[tci==100]/... then works; eth/ipv4/tcp stops matching tagged frames). Captured bytes stay as the kernel holds them; layer-bounded captures grow by 4 bytes. Not allowed with --cbpf entry, exit
--dsl-help Print the DSL grammar + bundled protocol catalogue and exit (no -i/-p required) β€”
--dump-asm Print compiled eBPF asm and exit. Values: filter (kunai/cbpfc body only) | full (wrapped program). No -i/-p required β€”
--dump-hook Hook whose capabilities/prologue --dump-asm renders: xdp (default) | tc | cgroup-skb | netfilter (offline compiles have no target program to auto-detect from) β€”
--func Attach to a specific __noinline subfunction by BTF name entry, exit
--list-funcs List available BTF functions in the target program and exit entry, exit
--list-progs List tail call targets reachable from the target program and exit entry, exit
--list-params List filterable parameters for --func (requires --func) entry, exit
--json Print --list-progs / --list-funcs / --list-params output as JSON on stdout instead of text on stderr entry, exit
--arg-filter Filter by function argument value (requires --func); format: param=value, param>=val, param<=val, param=min..max entry, exit
--set Define a named match set backed by a pinned BPF hash map: NAME=/sys/fs/bpf/path[,key(field=arg:param,...)]. Reference it with --arg-filter @NAME or the DSL predicate layer[field in @NAME]; manage entries at runtime with bpf-ninja set. Repeatable all
--arg-echo Print the target function's integer args for each call (gated by --arg-filter if set) instead of capturing. Requires --func and a single --mode; combine with -c N entry, exit
--split-by-tag Route matched packets to one pcap per set-map value (tag): -w out.pcap yields out.<tag>.pcap (requires -w) all
--max-bytes Stop the whole capture once total output bytes reach N (works with or without --split-by-tag). Per-tag caps live on the set entries: set add ... max-bytes=N (0 / omitted = uncapped; entries sharing a tag share one budget β€” an uncapped entry makes the tag uncapped, otherwise the largest cap wins) all
--exit-when-capped Exit 0 once every entry that has a per-entry cap reached it; uncapped entries don't participate (requires --split-by-tag and at least one --set) all
--finalize-on-del When a tag's last set entry is removed, merge its shards into out.<tag>.pcap while capturing continues β€” the file appearing is the completion ack (requires --split-by-tag and at least one --set) all
-V, --version Print the version and exit β€”

Specify exactly one of -i, -p, or --cgroup.

Prerequisites

Common:

  • Linux kernel 5.8+ with BTF (/sys/kernel/btf/vmlinux)
  • Root privileges (or CAP_BPF + CAP_NET_ADMIN)
  • Go 1.25+ (build only β€” pre-built binaries don't need Go)
  • libpcap-dev (runtime + tcpdump-syntax filter compilation)

Mode-specific:

  • --mode entry / exit: a target BPF program (XDP, tc clsact, cgroup-skb, or netfilter) already loaded with BTF. With -i, an XDP program attached to the interface; tc targets go by -p <progID>; cgroup-skb targets by --cgroup <path> or -p.
  • --mode xdp: no XDP attached to the interface (bpf-ninja becomes the XDP program).
  • DSL with chain quantifier (+, *, {n,m>4}) on a fixed-length layer, parser-machine self-loop (variable-length headers like IPv6 ext / GTP options / SRv6 segments), or alternation ((a|b)): kernel 5.17+ (uses bpf_loop + bpf2bpf subprograms). A repeated variable-length layer (ipv4+, ipv6*) unrolls to MAX_DEPTH parser machines instead of a bpf_loop chain; each machine's own option or extension walk keeps the 5.17 requirement where it applies. Plain DSL chains and --cbpf filters work on 5.8+.
# Debian/Ubuntu
sudo apt install libpcap-dev clang

# Fedora/RHEL
sudo dnf install libpcap-devel clang

clang is only needed for running BPF load tests locally; not required for using bpf-ninja itself.

Development

See CONTRIBUTING.md for contribution checks.

Build
make build
Test
# Unit tests (no root required)
make test

# BPF verifier load tests (root required, needs clang)
make test-bpf

# Integration tests with veth pair (root required, needs clang + tcpdump)
make test-integration

# All tests
make test-all
Unit tests

Pure Go logic tests. No BPF or root privileges needed.

make test
# or: go test ./...
BPF load tests

Verifies that the dynamically generated fentry/fexit/xdp programs pass the kernel's BPF verifier. Tests both with and without filters. Requires root and clang.

make test-bpf
Integration tests

End-to-end tests using a veth pair and a dummy XDP program. Requires root, clang, and tcpdump.

make test-integration

This runs scripts/test/run_tests.sh which:

  1. Creates a veth pair with a dummy XDP program
  2. Tests entry/exit capture, filters, pcap output, graceful shutdown
  3. Cleans up the veth pair
Multi-kernel testing (vimto + QEMU)

BPF load tests and integration tests run on kernel 6.1, 6.6, 6.12, 6.15, 6.18, 7.0, 7.1, 7.2 and 7.3-rc4 via vimto + QEMU in GitHub Actions.

To run locally:

# Install vimto and QEMU
CGO_ENABLED=0 go install lmb.io/vimto@v0.4.0
sudo apt install qemu-system-x86

# BPF verifier load tests on a specific kernel
vimto -kernel :6.6 exec -- go test -v -count 1 -timeout 5m ./internal/program/ -run TestBpf

Publications

Acknowledgements

bpf-ninja's design was inspired by the following projects:

  • xdp-dump (xdp-tools) β€” fentry/fexit trampoline approach for tracing XDP programs
  • xdpcap (Cloudflare) β€” tcpdump filter compilation via cBPFβ†’eBPF (cbpfc), and the overall architecture of capturing XDP packets to pcap

Capture shutdown, per-tag acknowledgements, output limits and the capture status=... counters are described in Capture completion.

License

bpf-ninja is licensed under the Apache License 2.0.

The eBPF programs that bpf-ninja generates and loads into the kernel declare the GPL license because they call GPL-only kernel helpers.

Directories ΒΆ

Path Synopsis
cmd
bpf-ninja command
JSON shapes for the --list-progs / --list-funcs / --list-params commands, emitted on stdout when --json is set so the discovery output can be piped into jq or another tool instead of scraping the human-readable text.
JSON shapes for the --list-progs / --list-funcs / --list-params commands, emitted on stdout when --json is set so the discovery output can be piped into jq or another tool instead of scraping the human-readable text.
internal
attach
Package attach finds existing XDP programs on network interfaces.
Package attach finds existing XDP programs on network interfaces.
capture
Package capture reads packets from the BPF ringbuf transport.
Package capture reads packets from the BPF ringbuf transport.
capture/fastrb
Package fastrb is a minimal BPF_MAP_TYPE_RINGBUF reader that bypasses cilium/ebpf's per-record Reader API.
Package fastrb is a minimal BPF_MAP_TYPE_RINGBUF reader that bypasses cilium/ebpf's per-record Reader API.
filter
Package filter provides argument filter parsing and validation for fentry/fexit probes.
Package filter provides argument filter parsing and validation for fentry/fexit probes.
hook
Package hook is the registry of BPF attach-target kinds bpf-ninja can trace.
Package hook is the registry of BPF attach-target kinds bpf-ninja can trace.
output
Merge per-CPU shard pcap-ng files (<base>.cpuN) into a single time-ordered pcap-ng at <base>.
Merge per-CPU shard pcap-ng files (<base>.cpuN) into a single time-ordered pcap-ng at <base>.
program
Package program β€” --arg-echo diagnostic path.
Package program β€” --arg-echo diagnostic path.
setmap
Management operations behind `bpf-ninja set ...`: create a pinned set map with synthesized BTF, and add/delete/list entries by field name so nobody has to hand-assemble zero-padded little-endian hex like bpftool requires (the exact error class behind the IMSI-encoding saga).
Management operations behind `bpf-ninja set ...`: create a pinned set map with synthesized BTF, and add/delete/list entries by field name so nobody has to hand-assemble zero-padded little-endian hex like bpftool requires (the exact error class behind the IMSI-encoding saga).
testutil
Package testutil provides shared test helpers for BPF/XDP integration tests.
Package testutil provides shared test helpers for BPF/XDP integration tests.
pkg
kunai
Package kunai exposes the P4-based filter DSL for bpf-ninja.
Package kunai exposes the P4-based filter DSL for bpf-ninja.
kunai/ast
Package ast defines the AST produced by bpf-ninja's one-liner DSL parser.
Package ast defines the AST produced by bpf-ninja's one-liner DSL parser.
kunai/codegen
Package codegen emits BPF instructions for a resolved DSL program.
Package codegen emits BPF instructions for a resolved DSL program.
kunai/dsltest
Package dsltest provides an end-to-end harness for byte-level validation of kunai's DSL β†’ BPF compilation.
Package dsltest provides an end-to-end harness for byte-level validation of kunai's DSL β†’ BPF compilation.
kunai/dslvocab
Package dslvocab caches the parsed bundled protocol vocabulary.
Package dslvocab caches the parsed bundled protocol vocabulary.
kunai/host/cgroupskb
Package cgroupskb provides kunai host adapters for hosts attached as fentry / fexit on a cgroup-skb (BPF_PROG_TYPE_CGROUP_SKB) program.
Package cgroupskb provides kunai host adapters for hosts attached as fentry / fexit on a cgroup-skb (BPF_PROG_TYPE_CGROUP_SKB) program.
kunai/host/netfilter
Package netfilter provides kunai host adapters for hosts attached as fentry / fexit on a netfilter (BPF_PROG_TYPE_NETFILTER, kernel 6.4+) program.
Package netfilter provides kunai host adapters for hosts attached as fentry / fexit on a netfilter (BPF_PROG_TYPE_NETFILTER, kernel 6.4+) program.
kunai/host/tc
Package tc provides kunai host adapters for hosts attached as fentry / fexit on a tc clsact (BPF_PROG_TYPE_SCHED_CLS) program.
Package tc provides kunai host adapters for hosts attached as fentry / fexit on a tc clsact (BPF_PROG_TYPE_SCHED_CLS) program.
kunai/host/xdp
Package xdp provides kunai host adapters for hosts attached as fentry / fexit on an XDP program.
Package xdp provides kunai host adapters for hosts attached as fentry / fexit on an XDP program.
kunai/ir
Package ir is the resolved intermediate representation produced by the DSL resolver.
Package ir is the resolved intermediate representation produced by the DSL resolver.
kunai/lexer
Package lexer tokenises bpf-ninja's one-liner DSL.
Package lexer tokenises bpf-ninja's one-liner DSL.
kunai/parser
Package parser converts a one-liner DSL expression into an ast.Filter.
Package parser converts a one-liner DSL expression into an ast.Filter.
kunai/protocols
Package protocols exposes the bundled P4 vocabulary as an embed.FS.
Package protocols exposes the bundled P4 vocabulary as an embed.FS.
kunai/resolve
Package resolve binds an AST filter to a loaded vocabulary, producing a resolved IR that codegen can walk without re-consulting the vocabulary.
Package resolve binds an AST filter to a loaded vocabulary, producing a resolved IR that codegen can walk without re-consulting the vocabulary.
kunai/vocab
Package vocab loads protocol vocabulary from .p4 sources and classifies their dispatch constants for downstream one-liner resolution and codegen.
Package vocab loads protocol vocabulary from .p4 sources and classifies their dispatch constants for downstream one-liner resolution and codegen.
kunai/vocab/p4lite
p4lite/lexer.go β€” tokeniser for the strict subset of P4-16 the bpf-ninja vocab loader consumes.
p4lite/lexer.go β€” tokeniser for the strict subset of P4-16 the bpf-ninja vocab loader consumes.
spec
lean/gen/vocab2lean command
vocab2lean prints the bundled kunai vocabulary as Lean source (spec/lean/Kunai/VocabData.lean).
vocab2lean prints the bundled kunai vocabulary as Lean source (spec/lean/Kunai/VocabData.lean).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL