OpenWrt-compatible router board with Ethernet cables, exposed processor, memory chips, and glowing status LEDs
eBPF in OpenWRT
William  

OpenWrt eBPF Kernel Setup: Which Options to Enable

Installing bpftool is not the fix. Enable eBPF on OpenWrt by rebuilding the kernel with CONFIG_BPF, CONFIG_BPF_SYSCALL, and the options required by your attachment point. Add CONFIG_BPF_JIT only when the target architecture supports it. Installing tools does not add a missing system call or compiler backend. Leave CONFIG_HAVE_EBPF_JIT alone. The architecture selects that capability.

Last updated: 2026-08-03

What eBPF support on OpenWrt actually means

Extended Berkeley Packet Filter, or eBPF, lets verified programs run inside the Linux kernel. On a router, those programs can inspect packets, collect counters, classify traffic, or drop packets before normal firewall processing.

However, eBPF support has three separate parts:

  • The kernel must contain the BPF core and bpf() system call.
  • The kernel must expose the hook your program uses, such as traffic control or XDP.
  • User-space tools must load and attach the program.

Installing tools fixes only the last part. If CONFIG_BPF_SYSCALL is off, no package manager can turn it on after boot. You need another kernel.

Also, eBPF is not automatically faster than nftables. A small classifier may earn its keep. A pile of oversized maps and tracing programs can waste memory on a router that already needs connection tracking, packet buffers, and wireless drivers.

Use eBPF where it gives you a hook or state model the normal networking stack does not. Replacing working firewall rules because eBPF sounds newer buys you risk without solving a problem.

Release, target, and CPU architecture decide support

Treat OpenWrt eBPF support as a property of one firmware image, not the project name. The release, target, subtarget, kernel configuration, CPU architecture, and network driver all matter.

Run this first on the router:

ubus call system board
uname -a
uname -m

The board response identifies the OpenWrt release and target. uname tells you which kernel and CPU are actually running. Save that output before building anything.

Next, inspect the selected target inside the build tree:

grep -R \
 -e '^CONFIG_BPF=' \
 -e '^CONFIG_BPF_SYSCALL=' \
 -e '^CONFIG_BPF_JIT=' \
 -e '^CONFIG_HAVE_EBPF_JIT=' \
 target/linux/generic target/linux/"$TARGET" 2>/dev/null

OpenWrt stores target-specific kernel fragments under target/linux/<target>/config-<kernel-version>. For example, the ipq807 target has used target/linux/ipq807/config-5.15. Copying that file into another target is wrong. It is evidence about ipq807, not a universal template.

Architecture matters most for the just-in-time compiler, or JIT. The JIT converts BPF instructions into native CPU instructions. Without an architecture backend, programs may use the interpreter or may be unavailable when the build requires JIT-only operation.

XDP still depends on the driver

XDP adds another dependency: the network driver must expose a suitable XDP path. A kernel can load BPF programs yet reject native XDP on one interface.

Generic XDP may still work, but it runs later and does not give you the same packet path.

Which kernel options must be enabled for eBPF?

Close-up embedded router motherboard showing processor, memory, network ports, and cooling hardware

Start with the program you intend to run. Then enable its dependencies. Ticking every BPF-related option produces a larger kernel and still does not fix an unsupported driver.

Kernel optionWhat it controlsMy judgment
CONFIG_BPFCore BPF infrastructureRequired
CONFIG_BPF_SYSCALLThe bpf() system call used by loadersRequired for normal user-space loading
CONFIG_BPF_JITArchitecture JIT support when availableEnable on a supported target
CONFIG_BPF_JIT_ALWAYS_ONRemoves the interpreter and requires JIT operationUse only after proving the JIT works
CONFIG_DEBUG_INFO_BTFBPF Type Format data for Compile Once, Run Everywhere programsEnable when the loader or program needs kernel BTF
CONFIG_BPF_EVENTSBPF integration with performance and tracing eventsNeeded for several tracing workflows
CONFIG_CGROUP_BPFBPF attachment to control groupsSkip unless the workload uses cgroup hooks
CONFIG_NET_CLS_BPFBPF classifiers in traffic controlRequired for classifier-based tc programs
CONFIG_NET_ACT_BPFBPF actions in traffic controlRequired when the program acts through tc
CONFIG_XDP_SOCKETSAF_XDP sockets for user-space packet processingNot required for ordinary XDP programs

CONFIG_BPF without CONFIG_BPF_SYSCALL is the classic half-built setup. The kernel contains BPF internals, but your loader has no usable door into them.

BTF is a separate requirement

BPF Type Format, or BTF, is easy to misunderstand. It describes kernel types and supports Compile Once, Run Everywhere, usually shortened to CO-RE.

BTF does not enable BPF by itself. It also adds build requirements such as a compatible pahole tool.

The Linux kernel BPF documentation describes these interfaces from the source side. Read the documentation for your kernel tree, because helpers and program types change across kernel lines.

Leave CONFIG_HAVE_EBPF_JIT to the architecture

CONFIG_HAVE_EBPF_JIT is an architecture capability marker. The CPU architecture selects it when that kernel tree contains a suitable eBPF JIT backend.

Do not add this to a target fragment:

CONFIG_HAVE_EBPF_JIT=y

That line does not create a compiler backend. At best, the kernel configuration machinery removes it. At worst, you end up reading a fragment that claims support the generated configuration never accepted.

Inspect the generated kernel configuration instead:

grep -E \
 '^(CONFIG_HAVE_EBPF_JIT|CONFIG_BPF_JIT|CONFIG_BPF_JIT_ALWAYS_ON)=' \
 build_dir/target-*/linux-*/linux-*/.config

CONFIG_HAVE_EBPF_JIT=y tells you the architecture has the capability. CONFIG_BPF_JIT=y tells you the build requested it. You need both facts before blaming the loader.

I leave CONFIG_BPF_JIT_ALWAYS_ON off during the first bring-up. An interpreter fallback is useful while proving an unfamiliar target. Once the JIT loads programs correctly, you can decide whether removing that fallback fits the threat model.

Networking options follow the attachment point

The attachment point decides the networking configuration. A loader can create maps and pass the verifier, then fail because the requested hook does not exist.

Use this map before opening kernel_menuconfig:

WorkloadKernel pieces to inspectUser-space attachment tool
Socket filterCONFIG_BPF_SYSCALL and socket supportApplication loader or bpftool
Traffic control classifierCONFIG_NET_SCHED, CONFIG_NET_CLS, CONFIG_NET_CLS_BPFtc
Traffic control actionCONFIG_NET_SCHED, action support, CONFIG_NET_ACT_BPFtc
XDPBPF syscall support and a compatible network driverip, bpftool, or a project loader
Cgroup programCONFIG_CGROUPS, CONFIG_CGROUP_BPFProject loader or bpftool
Tracing programTracing, performance event, and BPF event supportbpftool, bpftrace, or a custom loader
CO-RE programBTF in the running kernel or a matching external BTF fileA libbpf-compatible loader
Netlink attachmentThe target hook plus matching traffic control or link supportUsually tc or ip

Maps need the BPF core and system call, but individual map types can have more dependencies. Stack traces need symbol information. Performance event arrays need performance event support. Cgroup storage needs cgroups.

Meanwhile, XDP support must be tested per interface. Wireless devices, software bridges, tunnels, and hardware offload paths do not all behave like a plain Ethernet port. The driver gets a vote. Linux networking has never been shy about adding another layer.

Enable eBPF in the OpenWrt build tree

Use make kernel_menuconfig, preserve the resulting target changes, and inspect the generated kernel configuration before building firmware. The top-level package menu does not replace the kernel menu.

  1. Prepare the build tree and select the exact target.
./scripts/feeds update -a
./scripts/feeds install -a
make menuconfig

Select the target, subtarget, device profile, and required packages. Save, then normalize the configuration:

make defconfig

That tells the build system which kernel tree and target fragments apply.

  1. Open the kernel configuration.
make kernel_menuconfig

Search for each symbol by pressing /. Enable the core options first, then the networking or tracing dependencies required by your program.

How do you preserve accepted kernel settings?

  1. Save the kernel configuration changes.

After leaving the menu, refresh the target configuration:

make target/linux/refresh V=s
git diff -- target/linux

Read that diff. Keep unrelated defaults out of the commit if a menu tool rewrote the target fragment.

./scripts/diffconfig.sh is still useful for recording the top-level firmware and package selection:

./scripts/diffconfig.sh > openwrt-build.config

However, that file is not proof that target kernel symbols were preserved. The target fragment and generated kernel .config are the evidence.

  1. Prepare the kernel tree and check the result.
make target/linux/prepare V=s

grep -E \
'^(CONFIG_BPF|CONFIG_BPF_SYSCALL|CONFIG_BPF_JIT|CONFIG_DEBUG_INFO_BTF|CONFIG_NET_CLS_BPF|CONFIG_NET_ACT_BPF)=' \
build_dir/target-*/linux-*/linux-*/.config

That output tells you what Kconfig accepted after dependencies were resolved. A checked menu box that disappears here had an unmet dependency.

  1. Build the firmware.
make -j"$(nproc)"

If the build fails, stop the parallel noise and rerun it verbosely:

make -j1 V=s

The single-threaded output shows the first real compiler or host-tool failure. That is usually several screens above the final generic error.

Add the required tools and libraries

Add bpftool, the required iproute2 traffic control components, and your loader during the firmware build. Compilers belong on the build host unless the router is large enough to be a development machine.

The OpenWrt package name for the eBPF inspection utility is bpftool. Select it through make menuconfig when you want it in the image from first boot.

Useful components include:

  • bpftool for feature probes, program listings, maps, links, and pins. Start here when the loader’s error says nothing useful.
  • A full tc command when attaching traffic control programs. A cut-down build can load a program yet leave you with no way to attach it.
  • libbpf when the chosen loader uses it.
  • clang and LLVM when compiling C-based BPF objects.
  • strace for seeing the exact failing bpf() system call instead of guessing from the loader’s summary.
  • Any kernel modules required by the selected traffic control path.

Package availability changes by release and target. Search the package menu and read the dependency result before building.

Which package manager does your branch use?

Package-manager commands depend on the OpenWrt branch. Older branches use opkg:

opkg update
opkg install bpftool

Newer branches and main snapshots use APK:

apk update
apk add bpftool

This was an intentional package-manager change, not a renamed opkg subcommand. Check the documentation for the branch you are building before putting package commands into scripts.

For custom programs, cross-compile on the host against the target architecture. The OpenWrt BPF toolchain build guide covers the compiler and library side. Installing Clang on the router is usually a poor trade for flash and memory.

Check eBPF support after boot

Configured embedded router handling network traffic with active Ethernet links and illuminated status indicators

Check the running kernel, then probe features, then load a real object. A successful package install proves only that the package manager worked.

  1. Read the running kernel configuration.
if [ -r /proc/config.gz ]; then
zcat /proc/config.gz | grep -E '^(CONFIG_BPF|CONFIG_BPF_SYSCALL|CONFIG_BPF_JIT|CONFIG_DEBUG_INFO_BTF)='
elif [ -r "/boot/config-$(uname -r)" ]; then
grep -E '^(CONFIG_BPF|CONFIG_BPF_SYSCALL|CONFIG_BPF_JIT|CONFIG_DEBUG_INFO_BTF)=' \
"/boot/config-$(uname -r)"
else
echo "Running kernel configuration is not exposed"
fi

Missing /proc/config.gz does not mean BPF is disabled. It means the firmware did not expose the compressed configuration through that path.

  1. Probe the running kernel.
bpftool feature probe kernel

Run it as root. The output reports available program types, map types, helpers, and JIT-related features. This is more useful than grepping for every symbol because it tests the active kernel interface.

Can the running kernel load a real object?

  1. Check BTF and the BPF filesystem.
test -r /sys/kernel/btf/vmlinux && echo "kernel BTF present"
mountpoint /sys/fs/bpf || echo "bpffs is not mounted"

The BPF filesystem, usually called bpffs, stores pinned programs and maps. Mount it if your loader expects pins:

mkdir -p /sys/fs/bpf
mount -t bpf bpf /sys/fs/bpf
  1. Check unprivileged loading policy.
sysctl kernel.unprivileged_bpf_disabled 2>/dev/null

This setting concerns non-root callers. It does not explain every root load failure, so it is a poor universal diagnosis.

  1. Load a known object built for this target.
bpftool -d prog load ./router-test.o /sys/fs/bpf/router-test type classifier
bpftool prog show pinned /sys/fs/bpf/router-test

The first command asks the kernel verifier to accept the object and pins the resulting program. The second proves the pin resolves to a loaded program.

A blank bpftool prog list means no visible programs are loaded. It is not a full feature test. A real load is the proof.

Diagnose a failed eBPF load from the output

Capture the loader’s debug output, the kernel log, and the failing bpf() call before changing the config. Otherwise you are matching error messages from a different kernel on a different architecture.

Start here:

bpftool -d prog load ./router-test.o /sys/fs/bpf/router-test type classifier
dmesg | tail

The debug output includes verifier details when the kernel returns them. dmesg may also show JIT, verifier, allocation, or driver errors in the kernel ring buffer.

Use the error as a direction, not a complete diagnosis:

ResultWhat it usually points toWhat to check next
ENOSYSThe bpf() system call is unavailableRunning kernel config and firmware identity
EINVALUnsupported command, program type, flags, or object metadataFeature probe, loader version, program type
EACCESVerifier rejection or access policyFull verifier log and program instructions
EPERMMissing privilege, disabled unprivileged BPF, or resource policyUser identity, capabilities, sysctl, loader log
ENOENTMissing map, BTF file, pin, interface, or attachment objectExact path and loader configuration
EOPNOTSUPPThe requested hook or driver path is unsupportedInterface driver and attachment mode

Which system call actually failed?

When the message stays vague, trace the loader:

strace -f \
 -e trace=bpf,perf_event_open,setrlimit \
 -o /tmp/bpf-load.strace \
 bpftool -d prog load ./router-test.o /sys/fs/bpf/router-test type classifier

tail -n 40 /tmp/bpf-load.strace

That output tells you which system call failed and its exit code. If bpf() succeeds but the later netlink attachment fails, the kernel accepted the program. Your problem is the hook, interface, or tc request.

Verifier failures need code changes. Common causes include invalid pointer arithmetic, uninitialized stack reads, unsafe packet bounds, unavailable helpers, and map definitions the kernel does not understand. Rebuilding the same object with more kernel options will not make unsafe instructions valid.

For packet work, capturing traffic with eBPF on OpenWrt gives you a smaller first workload than a custom NAT path. Make the basic loader and attachment reproducible before adding stateful packet rewriting.

Build and run einat-ebpf on OpenWrt

Build einat-ebpf against the exact target architecture and confirm its required hooks before putting it on the router. Generic OpenWrt eBPF support does not prove that this project has everything it expects.

First, collect the target facts:

ubus call system board
uname -m
uname -r
bpftool feature probe kernel
test -r /sys/kernel/btf/vmlinux && echo "BTF available"

Next, read the project’s build files and loader code from the source tree. That tells you whether it expects Clang and libbpf, another language toolchain, embedded BPF bytecode, CO-RE relocation, pinned maps, or a particular attachment type.

Cross-compile on the host using the OpenWrt target toolchain. Match the CPU architecture and byte order. A BPF object can compile cleanly and still fail because its user-space loader was built for the host. Its type information may also not match the router.

Did the project attach to the intended hook?

Copy the loader, object files, and configuration to a temporary directory first:

scp einat-ebpf root@router:/tmp/einat-ebpf
scp einat-ebpf.o root@router:/tmp/einat-ebpf.o

Before starting it, record the baseline:

bpftool prog list
bpftool map list
find /sys/fs/bpf -maxdepth 3 -type f 2>/dev/null

Start the loader in the foreground with its debug logging enabled. Use the options printed by that exact binary. Flags copied from another project are noise, not troubleshooting.

After attachment, inspect all three layers:

bpftool prog list
bpftool map list
tc filter show
dmesg | tail

The program list proves loading. The map list proves state was created. The tc output proves attachment if the project uses traffic control. Kernel and loader logs tell you whether packets are reaching the intended hook.

Then stop the process and repeat those checks. Pinned maps and programs can outlive the loader. If the project leaves them behind, remove only its known pins. Clearing /sys/fs/bpf because one program leaked state can remove another service’s live objects.

For the routing side, use the OpenWrt eBPF routing workflow after the loader works in isolation. NAT is a poor first test because one wrong attachment can take your management connection with it.

Safe loading, inspection, and removal

Attach to a test interface first and keep a second management path open. An accepted eBPF program can still drop the SSH packets you need to remove it.

The following traffic control workflow uses a pinned classifier. Replace the placeholders with a test interface and an unused filter preference:

DEV=<test-interface>
PREF=<unused-preference>
PIN=/sys/fs/bpf/router-test

mkdir -p /sys/fs/bpf
mountpoint /sys/fs/bpf || mount -t bpf bpf /sys/fs/bpf

bpftool -d prog load ./router-test.o "$PIN" type classifier
tc qdisc replace dev "$DEV" clsact
tc filter add dev "$DEV" ingress pref "$PREF" bpf da pinned "$PIN"

Now inspect the attachment and counters:

bpftool prog show pinned "$PIN"
bpftool map list
tc -s filter show dev "$DEV" ingress

That sequence proves different things. bpftool shows the loaded program and maps. tc shows whether the program is attached to the expected interface and whether packets reach it.

How do you remove the program without breaking shared state?

Remove the filter before deleting the pin:

tc filter del dev "$DEV" ingress pref "$PREF"
rm -f "$PIN"

Leave the clsact queueing discipline in place unless you created it and know no other filters use it. Removing shared traffic control state is how a careful rollback becomes a second outage.

For persistence, use an OpenWrt procd init script that loads the program after the required interfaces exist. Pinning keeps an object alive after the loader exits, but it does not make volatile runtime state survive a reboot.

Keep the management path separate

Keep the management interface out of the first deployment. Test on a spare port, VLAN, or lab router.

Production routing belongs on the box you are not learning to recover over SSH.

FAQ

Can LuCI enable eBPF without rebuilding OpenWrt?

No. LuCI works above the kernel, so it can install bpftool and change runtime settings but cannot add a missing bpf() system call. If the required symbols or hook are absent, build and flash another firmware image.

Can hardware flow offload and eBPF run together?

Yes, but do not assume both paths see the same packets. Hardware-offloaded flows can bypass software hooks after setup. If BPF counters stop moving, check where the flow moved before changing the map or program.

Can nftables and eBPF filter the same traffic?

Yes, but give each layer a clear job. An XDP drop happens before nftables, while traffic control hooks run elsewhere in the path. If two layers own the same decision, the first drop hides the second rule and makes the logs harder to trust.

Will a sysupgrade preserve eBPF support?

Only when the replacement firmware contains the same required kernel options. Files under /etc may survive through configuration backup, but the old kernel does not. Treat every firmware replacement as a new kernel and rerun the feature probe and load test.

Should non-root services load eBPF programs on a router?

Usually not. Use a small privileged startup process to load and pin the objects, then drop rights for the long-lived service where the design allows it. Giving a network daemon broad BPF privileges leaves a larger failure path for no useful return.

Related on this blog