
XDP Firewall on OpenWRT: Early Packet Drop at Line Rate
XDP is not a firewall you install, it's a hook you program. It gives you the earliest interception point in the Linux kernel's receive path, running before the kernel builds an sk_buff for the packet, which is exactly what you want for high-volume drop and redirect decisions like DDoS mitigation and load balancing. It's the wrong tool for stateful, rule-heavy policy that netfilter and nftables already do correctly. If you run an XDP firewall on OpenWRT and can't explain why a packet was dropped by reading your program's verdict counters, you don't have a firewall. You have a black box that fails silently under real attack traffic.
Last updated: 2026-07-21
I use an OpenWRT XDP firewall to stop unwanted packet flows right at the driver. It saves CPU and keeps latency low. Below I walk through the practical way to verify kernel support, build a tiny program, attach it to an interface, and confirm it actually does what you think. You get immediate feedback: counters, packet drops, and measurable CPU relief. I keep an out-of-band access plan the whole time and show quick tests with ping, curl, and tcpdump so you can confirm behavior safely before you trust it.
What is XDP and why does it sit ahead of the normal firewall path?
XDP stands for eXpress Data Path. It's an eBPF hook that runs inside the network driver, the moment a packet lands in the receive ring, before the kernel allocates a socket buffer for it. That timing is the whole point. Every layer of the normal stack costs work per packet, and XDP lets you decide before any of that work happens.
Netfilter, the machinery behind iptables and nftables, runs much deeper. By the time a packet reaches an nftables rule, the kernel has already built the sk_buff, walked part of the stack, and paid for it. For a packet you were going to drop anyway, that's wasted CPU on every single one. Under a flood, that waste is the attack.
So the mental model is layering, not replacement. XDP handles the crude, high-volume verdicts at the door. Netfilter handles everything that needs context: connection state, NAT, rich protocol rules. You can read more on the reasoning in using XDP for low-latency processing.
| Layer | When to use | Strength | Limit |
|---|---|---|---|
| Driver (XDP) | Early drop or redirect | Low latency, high throughput | You parse headers by hand |
| Netfilter (iptables/nftables) | Stateful, complex rules | Connection tracking, NAT | Higher CPU per packet |
| TC/eBPF | Egress control | Policing and shaping | Weaker fit for raw ingress |
Can XDP actually replace a traditional firewall?
No, and anyone selling it as a drop-in firewall is skipping the part that matters. XDP wins decisively at one job: dropping or redirecting huge volumes of packets cheaply, before the stack touches them. Volumetric DDoS filtering, blocklists at line rate, simple L4 steering. That's its lane and nothing conventional beats it there.
Where it loses is anything stateful. XDP has no connection tracking of its own. It sees one packet, in isolation, with the raw bytes and nothing else. If you want to allow return traffic for established connections, do NAT, or write rules that reason about protocol state, that's netfilter's job and it already does it correctly. Rebuilding conntrack in eBPF by hand is how you spend a month reinventing a wheel that ships in the kernel.
The honest split: use XDP to shed the obvious garbage so it never costs you, and let nftables handle the policy that needs a brain. On a home or edge router the real win is dropping obvious floods before the CPU spends cycles on state tracking it was never going to keep.
How do the offloaded, native, and generic XDP modes actually differ?
Three modes, and the difference decides your performance ceiling. Native mode runs your program in the driver itself, which is where XDP is meant to live. Offloaded mode pushes the program onto a SmartNIC so the host CPU never sees the dropped packets at all. Generic mode is the fallback: the kernel runs your program in the stack, after the sk_buff already exists, which throws away most of the speed advantage.
Native mode needs a driver that implements the XDP hooks. On the server side, drivers like mlx4, mlx5, and i40 support native XDP. On consumer and embedded hardware, driver support is thin, and that's the gotcha nobody mentions: if the driver lacks the hook, the loader quietly gives you generic mode instead of failing. You think you're running at the driver. You aren't.
So verify the mode, don't assume it. After you attach, run ip link show dev <iface> and read the flag: xdp means native, xdpgeneric means fallback, xdpoffload means the NIC took it. If you see xdpgeneric and expected native, stop and check ethtool -i <iface> for the driver before you benchmark anything.
How does XDP mitigate DDoS attacks in practice?
You drop the flood at the door, before it costs you a socket buffer. That's the mechanism, and it's XDP's clearest win. A packet that matches your drop condition returns XDP_DROP inside the driver, and the kernel never allocates memory for it, never runs conntrack, never wakes a softirq to process it. The attacker's volume stops mapping to your CPU.
The pattern that scales is a per-CPU map holding your counters and rate state. Per-CPU means each core updates its own copy with no lock contention, which matters when you're fielding millions of packets a second across a queue per core. You read the drop counters back from user space and now you can prove what your program is doing under load.
Rate limiting lives in the same map: track packets per source or per prefix, drop past a threshold. Keep the fast path dumb and the lookups O(1). The moment your XDP program starts walking lists or doing string work, you've lost the reason you came to XDP. If you want a worked example of the blocklist pattern, see blocking IP addresses with eBPF.
What does a minimal XDP firewall program actually look like?
Here is what happens in a real program: you get a pointer to the raw packet, you check bounds by hand, you parse just enough header to decide, and you return a verdict. The verifier will not let you touch a byte you haven't proven is inside the packet, so every access is guarded by a data_end check.
The verdict codes are the whole interface. XDP_PASS sends the packet up the stack. XDP_DROP frees it right there. XDP_TX bounces it back out the same interface. XDP_REDIRECT sends it to another interface or a CPU. XDP_ABORTED signals an error and fires a tracepoint you can watch.
A skeleton drop-by-protocol program reads like this:
SEC("xdp")
int xdp_fw(struct xdp_md *ctx) {
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end) // bounds check first, always
return XDP_PASS;
if (eth->h_proto != bpf_htons(ETH_P_IP))
return XDP_PASS;
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return XDP_PASS;
if (ip->protocol == IPPROTO_ICMP)
return XDP_DROP;
return XDP_PASS;
}
When the verifier rejects this, read the actual error it prints. It tells you the exact instruction and register that failed its safety proof. Most rejections are a missing bounds check or a value the verifier couldn't prove was in range. That's a real bug it caught, not the verifier being difficult. Don't paste a wider tutorial to make the message go away.
How do you debug an XDP program that isn't dropping what you expect?
Run this first: bpftool prog show to confirm your program is actually loaded and attached to the interface you think it is. Half of "my drops aren't working" turns out to be a program attached to the wrong device, or an old one still loaded from a failed run. Check the output before you touch the C.
Next, read the counters from the source of truth. bpftool map dump shows your per-CPU verdict and drop maps with live values. If the drop counter isn't moving while you send traffic that should match, your parsing logic is wrong, not your attach. Add a bpf_printk and watch /sys/kernel/debug/tracing/trace_pipe to see what your program actually decides per packet. That tells you the branch you're taking.
For attach and mode problems, dmesg and the kernel ring buffer carry the driver's own complaints. A native attach that fell back to generic usually left a line there explaining why. Tie every check to traffic you generate yourself with ping or curl, so you can match a counter change to the exact moment you sent the packet. Logs without a time anchor waste an hour.
How does OpenWrt implement eBPF and XDP on consumer and embedded hardware?

OpenWrt can run XDP, but the constraints are real and worth knowing before you commit a night to it. The kernel side needs eBPF and XDP support compiled in. For OpenWRT that means a Linux kernel of 4.13 or greater. Older device builds may simply not have the config enabled, and you'll be rebuilding an image before you write a line of C.
The harder limit is driver support. Embedded NICs on consumer routers rarely implement native XDP hooks, so most OpenWrt boxes run generic mode whether you asked for it or not. Generic mode still works and still drops packets. It just does it after the sk_buff exists, so you lose the CPU savings that were the reason to reach for XDP. Verify the mode after attach and set your expectations to the hardware in front of you.
Flash and RAM are the other squeeze. clang, llvm, and headers are large, so build the program off-box on a workstation or in an SDK container, then copy the compiled object over with scp and load it on the router. For the toolchain side, see building a BPF toolchain on OpenWRT. Match your build headers to the running kernel or you'll chase type mismatches that aren't really bugs.
Can XDP handle load balancing as well as dedicated tools?
For L4 load balancing at volume, yes, and this is the second thing XDP is genuinely good at. The pattern is the one Meta's Katran uses: hash the packet's connection tuple, pick a backend from a consistent hashing ring, and XDP_TX or XDP_REDIRECT it there without ever building a socket buffer. Consistent hashing means a backend leaving reshuffles only its share of flows, not everyone's.
The honest limit is that XDP does L4 and only L4. It reads IP and port, it does not terminate TLS, it does not read HTTP paths, it does not do L7 routing. If you need content-based balancing, header rewriting beyond the basics, or health checking with real logic, that lives in user space or a purpose-built proxy. XDP moves packets fast; it doesn't understand your application.
So the split mirrors the firewall one. Use XDP for the stateless, high-rate steering where its speed is the whole point, and keep the smart, stateful routing in a tool built for it.
Which companies run XDP in production, and what does that prove?

Meta runs Katran, an XDP-based L4 load balancer, in front of large parts of its infrastructure, and Cloudflare uses XDP for DDoS mitigation at the edge. That's real evidence XDP holds up at scale that most of us will never touch. It is not an endorsement that you should run it on your home router.
What actually transfers is narrow and worth extracting. These deployments prove the core mechanics: dropping and steering packets in the driver, per-CPU maps, consistent hashing, all work under punishing load. What does not transfer is their engineering budget. They have teams reading kernel source, custom drivers, and NICs chosen for offload. Copy the pattern, not the assumption that it runs itself. Their scale is the proof the technique is sound, nothing more.
Which eBPF frameworks should you use to write XDP programs?
Start with libxdp and libbpf, because they're the closest to how the kernel actually loads and runs these programs, and that closeness pays off the first time something breaks. You compile with clang, load with xdp-loader, and inspect with bpftool, and every layer maps to something you can read the source of. That's the whole persona: understand the failure or it comes back.
BCC is friendlier for quick tracing experiments because it compiles at runtime, but that runtime compile is a dependency and a startup cost you don't want on an embedded router. It's a lab tool, not a production loader. Cilium's tooling is production-grade and powerful, but it's built around Kubernetes networking, so pulling it in for a single router firewall is far more machinery than the job needs.
| Framework | Best for | Watch out for |
|---|---|---|
| libxdp / libbpf | Production XDP, small footprint | You write more C yourself |
| BCC | Quick tracing, prototyping | Runtime compile, heavy deps |
| Cilium | Kubernetes-scale networking | Overkill for one router |
For a single OpenWrt firewall, libxdp and libbpf are the right call. They give you the least between your code and the kernel, which is exactly where you want to be when a packet isn't dropping and you need to know why.
FAQ
Does an XDP program see outbound traffic too?
No. XDP is ingress-only by design, it hooks the receive path and nothing else. If you need to filter or shape packets leaving the box, that's the traffic control (TC) eBPF hook, which runs on egress. Trying to make XDP handle outbound is a category error; reach for TC instead.
Will attaching an XDP program lock me out of my router?
It can, which is why an out-of-band access plan comes first. A drop rule that's too broad, or a program that swallows your management traffic, cuts your own SSH session. Before you attach anything, keep a serial console or a second path in, and know the unload command. Test on the LAN interface before you ever touch the WAN one.
How much latency does XDP add per packet?
Very little, and that's the point of using it. For an AF_XDP socket path, the round-trip latency measures around 6.5 microseconds compared with passing through the full kernel network stack. A pure XDP_DROP never even reaches user space, so the cost of shedding a flood packet is smaller still.
Can I write XDP firewall rules without knowing C?
Partly. Tools like xdp-filter from the xdp-tools project give you prebuilt drop-and-allow behavior with no code, which covers simple blocklists. The moment you want custom logic, per-source rate limiting, or anything the prebuilt tools don't expose, you're writing eBPF C and reading verifier output. There's no way around understanding the program if you want to trust it under attack.
Is generic mode worth using at all if my NIC won't do native?
Yes, with clear eyes about what you're getting. Generic mode still drops packets correctly and still spares you the deeper stack processing, so it beats doing the same filtering in nftables under a flood. You just don't get the full driver-level CPU savings. Run it, measure the actual relief with your counters, and decide from real numbers rather than the mode name.
