Capture Packets in OpenWRT with eBPF, network tester and switch with cables on wooden desk
eBPF in OpenWRT
William  

Capture Packets in OpenWRT With eBPF: What to Know First

Start with tcpdump if you need a pcap file; use eBPF when you need kernel-side counters, filters, or custom events. For an openwrt packet capture on a small router, that split keeps the job honest: tcpdump gives Wireshark a standard capture, while eBPF trades full packet visibility for lower overhead and targeted telemetry. I build and flash custom firmware so I can run a practical OpenWRT eBPF packet capture on small routers without guessing. I focus on concrete steps: kernel config flags, compiling from source, and verifying support with bpftool.

Last updated: 2026-07-31

I explain three workable capture paths: quick XDP loads through iproute2 or xdp-tools, debugfs prints using bpf_trace_printk, and a user-space loader that polls perf stats. Each method trades CPU, RAM, and flash for observability. I call out common pitfalls: endianness, header paths in staging_dir, and architecture mismatches. You’ll also see how HTTP tracing hooks accept4/read/write/close push truncated payloads through perf buffers to limit overhead.

Key takeaways

  • Use tcpdump first when you need a normal packet capture for Wireshark.
  • Use eBPF when you need targeted counters, XDP actions, or process-level socket tracing.
  • Enable CONFIG_BPF and CONFIG_BPF_SYSCALL before building the firmware.
  • Add CONFIG_NET_CLS_BPF and CONFIG_NET_ACT_BPF when you need the traffic-control path.
  • Verify support with bpftool prog list, interface counters, and the kernel log.
  • Keep payloads short. Full packet data costs memory, storage, and user-space processing.
  • On small routers, libbpf is the practical choice for a deployed loader. BCC is mainly for fast experiments.
  • A bubblewrap EPERM from PR_CAPBSET_DROP or PR_SET_NO_NEW_PRIVS usually points to sandbox or kernel policy, not broken BPF code.

What do you need before starting on OpenWRT?

Start by pinning the target hardware, firmware branch, and toolchain. OpenWrt builds are tied to the target architecture, kernel headers, feeds, and C library. If those drift apart, the compiler gives you errors that look unrelated until you compare the paths.

Match the target and subtarget to the router SoC. Back up the configuration before flashing, and keep a known-good image available. A custom kernel is useful only while the router still boots.

I confirm resources on both machines:

  • The build host needs enough storage for the source tree, toolchain, staging files, and image artifacts.
  • The router needs enough free flash for kernel modules and user-space tools.
  • The router also needs enough RAM for BPF maps, perf buffers, and the loader.
  • An attached storage device or remote capture host is safer than filling /tmp.

For a broader setup sequence, use the guide on building the BPF toolchain on OpenWRT. Keep the source tree, SDK, and staging directory in one workspace. That avoids the usual missing-header chase.

Which OpenWrt version, toolchain, and router resources matter?

Use a supported OpenWrt release branch that matches your device. Do not copy a kernel object built for another target and hope the loader will forgive it. The architecture, kernel headers, compiler, and C library all need to agree.

I record the build identity before compiling:

git clone https://git.openwrt.org/openwrt/openwrt.git
cd openwrt

git rev-parse HEAD
make menuconfig
make defconfig

The commit output tells you which source tree produced the image. make defconfig resolves defaults before the expensive build starts.

For an existing SDK, export the staging path before compiling BPF objects:

export STAGING_DIR="$PWD/staging_dir"

Then point the compiler at the SDK's BPF headers rather than headers installed on the host. Host headers describe the host. That is not a small difference.

What build dependencies and SDK setup are required?

Install the normal OpenWrt build tools on a Linux host:

sudo apt update
sudo apt install build-essential git libncurses5-dev gawk gettext unzip file zlib1g-dev

The source tree supplies the OpenWrt build system. The SDK supplies target headers, libraries, and the cross-compiler. Use the SDK when you are building an out-of-tree loader or BPF object for an existing firmware target.

For production, I choose libbpf. It gives you a small C-based loader with direct control over maps, links, and polling. I use BCC for rapid prototypes only when the target can afford its runtime and language dependencies.

Run a verbose dry build before changing BPF code:

make defconfig
make -j"$(nproc)" V=s

The verbose output shows the actual compiler, include paths, linker flags, and target architecture. Save it. When a build fails later, the command line is more useful than the final error line.

How do you enable kernel support for eBPF?

Enable the BPF options in make menuconfig, then rebuild the kernel and modules. Do not assume a router with a recent-looking kernel already has the options you need. OpenWrt trims kernels to fit the target, and unused features are often left out.

For a basic eBPF build, enable:

CONFIG_BPF=y
CONFIG_BPF_SYSCALL=y
CONFIG_NET_CLS_BPF=m
CONFIG_NET_ACT_BPF=m

CONFIG_BPF enables the core BPF machinery. CONFIG_BPF_SYSCALL allows user space to load and inspect programs through the BPF system call. The NET_CLS and NET_ACT options support BPF through the traffic-control path.

XDP also depends on the network driver and attach mode. A program can pass the kernel checks and still fail to attach natively because the driver does not support that path. Generic XDP may work, but it costs more than driver-level attachment.

Use the OpenWrt eBPF kernel configuration guide when you need to compare the firmware configuration with the running device.

Which kernel options should you check first?

Check the running configuration instead of guessing from the firmware name. If the kernel exposes its configuration, run:

zgrep -E 'CONFIG_BPF|CONFIG_NET_CLS_BPF|CONFIG_NET_ACT_BPF' /proc/config.gz

If /proc/config.gz does not exist, inspect the configuration used by the build tree. The important part is that the running kernel and the BPF object agree.

Then install and run bpftool:

opkg update
opkg install bpftool

bpftool prog list
bpftool map list

bpftool prog list shows loaded programs, their types, and attachment details. bpftool map list shows whether the maps your loader expects actually exist. An empty list is not a failure by itself. It means nothing is loaded yet.

For a quick syscall check, trace the loader:

strace -f -e bpf,prctl,capset,execve ./loader

The bpf calls show where loading fails. The prctl and capset calls expose sandbox and capability problems that otherwise get reduced to a useless EPERM.

How do you build, flash, and confirm the firmware?

Build the image with verbose output:

make -j"$(nproc)" V=s

The firmware and packages appear below bin/targets. Select the file that matches the exact device profile. A similar filename is not good enough.

Flash through LuCI or sysupgrade, depending on the device and recovery path. Keep a serial console or recovery method available if the router supports one. Do not test an experimental kernel on the only router in the house.

After rebooting, confirm the kernel and tools before loading a program:

uname -a
which bpftool
bpftool prog list
ip -details link show

Then load a no-op program or attach a test program to the intended interface. Confirm the attachment with bpftool, ip -details link show, or xdp-loader status. A successful build is not proof that the running router can load the object.

What common build problems should you expect?

Most failures come from target mismatch, stale headers, or a loader linked against the wrong C library. The error message is usually accurate. It is the assumption behind the command that is wrong.

ProblemWhat you seeWhat I check
Wrong image architectureThe router will not boot, or modules refuse to loadTarget and subtarget under bin/targets
Missing staging headersOut-of-tree BPF code fails to compileSTAGING_DIR and SDK include paths
Endianness mismatchCounters or map values look wrongHost-to-target serialization
Wrong C libraryAttach calls fail with vague errorsCross-compiler and target libc
Missing kernel optionLoader reports an unsupported operationRunning kernel configuration
Driver lacks native XDPXDP attach fails in driver modeGeneric or supported attach mode
Full /tmpCapture or loader exits while writingdf -h and capture destination

Do not fix a header problem by adding random host include directories. That can make the build pass while producing an object for the wrong kernel. Compare the compiler command and the staging path first.

How do you run an OpenWRT packet capture with eBPF?

Capture Packets in OpenWRT with eBPF, shown with connected network devices and cables on a desk

Use tcpdump when the deliverable is a pcap. Use eBPF when the deliverable is a decision, such as packet counters, early drops, socket events, or a narrow protocol signal. An eBPF packet capture is not automatically better because it uses newer kernel features.

Start with tcpdump when you need packets

tcpdump uses libpcap, so install the matching package from the OpenWrt feed:

opkg update
opkg list | grep '^tcpdump'
opkg install tcpdump

A full build has more protocol and link-layer support. A smaller build such as tcpdump-mini uses fewer resources but may omit protocol decoders or filter features. Check what is installed instead of assuming the package variant.

List capture interfaces:

tcpdump -D

Then capture on the interface that actually carries the traffic:

tcpdump -ni br-lan -s 0 -w /tmp/capture.pcap 'host 192.168.1.10'

-D tells you which interfaces libpcap can see. -n avoids reverse DNS lookups. -i selects the interface. -s 0 keeps the full packet where the driver and memory allow it. The filter limits the capture before it reaches the output file.

For a live Wireshark session, stream the pcap over SSH instead of filling router storage:

ssh root@router 'tcpdump -ni br-lan -s 0 -w - "host 192.168.1.10"' \
 | wireshark -k -i -

This is usually better than writing to /tmp, which is commonly backed by RAM. If the command runs for a long time, save the capture on the workstation or attached storage.

Use filters that match the question

Choose the interface first, then add the smallest filter that answers the question.

tcpdump -ni br-lan 'tcp port 80 or tcp port 443'
tcpdump -ni br-lan 'ip6'
tcpdump -ni br-lan 'ether host aa:bb:cc:dd:ee:ff'
tcpdump -ni br-lan 'net 192.168.1.0/24'

If a filter fails on a minimal build, capture without the filter and reduce the output later. A missing protocol parser is a package limitation, not a network fault.

rpcapd can provide remote capture through the rpcap protocol, but I do not make it the default. It adds another daemon, another listener, and another authentication problem. An SSH pipe is easier to inspect and leaves less exposed on the router.

Use XDP when you need early packet events

For fast attachment, use iproute2:

ip link set dev eth0 xdp obj prog.o sec xdp_pass
bpftool prog list
ip -details link show dev eth0

The first command loads the object and attaches the xdp_pass section. The next two commands confirm that the program exists and that the interface reports an XDP attachment.

With xdp-tools, use the loader and status commands:

xdp-loader load -d eth0 prog.o
xdp-loader status

xdp-loader status tells you whether the program is attached and which mode the tool selected. If native driver attachment fails, test a generic mode supported by that installed version. Generic mode is useful for diagnosis, but it is not free.

XDP is good for counters, early filtering, and packet decisions. It is a poor choice when you need a complete pcap with every protocol detail. For that job, keep tcpdump.

Use debugfs when the router has little user space

For a small device, add a controlled bpf_trace_printk call and read the trace buffer:

mount -t debugfs debugfs /sys/kernel/debug
cat /sys/kernel/debug/tracing/trace

This path needs no full user-space loader. It is useful for proving that a hook runs and that a field has the value you expect.

It is also easy to abuse. Do not print once per packet during a busy capture. Rate-limit the event, print only the fields needed to prove the path, and remove the debug call after the test. The trace buffer is a debugging aid, not a packet transport.

Use a user-space loader for live counters

A small loader gives you control over attachment, maps, event polling, and shutdown. I keep the design narrow:

  1. Open the object compiled for the target kernel.
  2. Load the program and maps.
  3. Attach to XDP, traffic control, or the chosen trace hook.
  4. Poll counters or perf buffers.
  5. Print or export only the fields needed by the monitor.
  6. Detach cleanly when the process exits.

The loader reads counters such as rx_packets and rx_bytes, then computes packets per second from timestamps. It can pin maps so a restart does not discard state, provided the map layout remains compatible.

Handle endianness and map sizes explicitly. A loader that works on the build host can still report nonsense on the router if it reads target data with host assumptions.

MethodUse it forMain cost
tcpdump and libpcapFull packet capture and Wireshark analysisStorage and packet-copy overhead
ip link XDP attachFast tests and early packet handlingLimited user-space visibility
xdp-loaderManaged XDP attachment and statusExtra tool and mode differences
bpf_trace_printkQuick on-device debuggingVerbose trace output
libbpf loaderLive counters, maps, and perf eventsMore code and RAM

How do HTTP hooks and perf buffers fit into packet capture?

Trace socket syscalls when you need process-level HTTP flow data, not wire-level packet bytes. Hook accept4, read, write, and close with entry and exit probes.

On entry, store the arguments in a map keyed by process ID and file descriptor. On return, read the saved context, check the return value, and record the result. This avoids losing the original pointer or descriptor while the syscall runs.

A syscall hook sees what a process reads or writes. It does not preserve packet boundaries, and HTTPS payloads are encrypted unless you trace after decryption inside the process. That distinction matters. Calling this a packet capture does not make it one.

Why should payloads go through perf buffers?

Send small, structured events through perf buffers rather than copying arbitrary payloads into every event. I split the data into separate event types:

  • socket_open for connection metadata.
  • socket_data for bounded payload fragments.
  • Close events for cleanup and final status.

Each record can carry the total size, offset, direction, process ID, file descriptor, and timestamp. Set a fixed payload limit and reconstruct larger messages in user space from several events.

The limit protects the router from a single large write. It also keeps the user-space parser predictable. The real problem is not only CPU use. It is the combined cost of map writes, perf-buffer copies, event polling, and output formatting.

How do XDP counters expose useful traffic information?

Keep the XDP record small:

struct xdp_stats {
 __u64 rx_packets;
 __u64 rx_bytes;
 __u64 last_ts;
};

Update the record in the XDP path, then poll it from user space. Compute packet and byte rates from the timestamp rather than treating one read as a complete measurement.

Use bpftool map dump when you need to inspect values without the loader:

bpftool map list
bpftool map dump id MAP_ID

The map output tells you whether the program is updating state. If the counters stay at zero, check the interface, attachment mode, map lookup path, and packet direction before changing the arithmetic.

For a reusable monitor, the traffic monitoring with eBPF guide covers the same map-and-counter pattern in more detail.

Can kernel tools detect wireless interference and fading?

Capture Packets in OpenWRT with eBPF beside a Wi-Fi router, tangled cables, speaker, and microwave

Use kernel counters to confirm traffic behavior, but do not expect XDP to provide radio measurements. The Linux XDP path does not expose signal-to-noise ratio or signal strength. Driver statistics and wireless tools provide that information.

For co-channel interference, run iperf3 while bringing up an overlapping access point. Log packet and byte counters, retransmits, and throughput over time. A falling rate with rising retransmits is useful evidence, but it does not tell you which radio condition caused it.

For fading, change distance and add physical obstructions. Record the same counters while the link changes. Keep the test repeatable, or you will confuse interference, rate control, and ordinary client movement.

Why does bubblewrap return EPERM from prctl_caps?

A bubblewrap failure mentioning prctl_caps, PR_CAPBSET_DROP, or PR_SET_NO_NEW_PRIVS usually means the sandbox cannot apply one of its requested process restrictions. That is separate from whether the kernel can load an eBPF program.

PR_CAPBSET_DROP removes a capability from the process capability bounding set. PR_SET_NO_NEW_PRIVS prevents the process from gaining new privileges across an execve. Both can return EPERM when the process lacks the required privilege or the kernel was built without the needed namespace or security support.

Trace the failing call:

strace -f -e prctl,capset,execve bwrap ...

Then check the kernel log:

dmesg -T | tail -50
logread | tail -50

If the failure occurs before your loader starts, do not change BPF programs yet. Check whether the firmware includes the kernel features bubblewrap expects, whether the process is already inside a restricted container, and whether the launcher is trying to create a user namespace.

Do not “fix” this with a broad capability grant or a permissive file mode. That hides the policy failure and weakens the device. Run the loader without bubblewrap for a controlled test, or rebuild the sandbox assumptions for the target kernel.

What should you do next for reliability and performance?

Keep the first test small. Load one program, attach one interface, update one map, and inspect the output. A large multi-hook build gives you too many places to blame when nothing works.

I use this order:

  1. Confirm the interface with ip -br link and ip -br addr.
  2. Capture a small sample with tcpdump.
  3. Load a no-op BPF program.
  4. Confirm the program with bpftool prog list.
  5. Add one counter or event.
  6. Reproduce the traffic.
  7. Compare the counter with tcpdump output.
  8. Only then add payload handling or multiple hooks.

Use journalctl on a systemd-based build, but use logread and dmesg on the usual OpenWrt image. Always add a time window or reproduce the fault while following the log. Old messages are how you end up debugging a failure from the previous boot.

For production loaders, prefer libbpf and keep the binary small. Use BCC when you need to answer a question quickly and the device can support it. Do not ship a Python-heavy prototype to a router with limited flash and RAM because the first test was convenient.

FAQ

Can eBPF replace tcpdump on OpenWrt?

No. eBPF can count, filter, classify, and trace selected events, but it does not automatically produce the full pcap workflow that Wireshark expects. Use tcpdump for packet files and eBPF for targeted kernel telemetry.

Can an XDP program see traffic on every OpenWrt interface?

No. XDP attaches to a specific network interface and depends on how that interface is implemented. Check the physical interface, bridge layout, VLAN path, and driver support before assuming traffic will reach the hook.

Why do my eBPF counters disagree with tcpdump?

They may run at different points in the stack. XDP sees packets early, while tcpdump uses a packet-socket path later in processing. VLAN handling, dropped packets, cloned traffic, and direction can all change the totals.

Should I store captured payloads in a BPF map?

Usually not. Maps are good for state and counters, not an unbounded packet queue. Emit bounded records through a perf buffer or ring buffer, then write the data from user space where you can control storage and backpressure.

Related: A Practical Linux Tcpdump Workflow for Packet Capture