
Protect Linux from DDoS with eBPF
I build small, safe kernel programs that stop high-rate packet floods at the NIC driver level so the system stays responsive under stress.
XDP runs before the Linux networking stack, letting an ebpf program parse headers, count packets per source IP, and drop bursts with minimal latency.
My approach uses a BPF hash map keyed by source IP, bpf_ktime_get_ns() for a one-second window, and a 250 pps threshold as a starting point.
I outline how to compile C code with clang -target bpf, attach via ip link set dev IFACE xdp obj file.o sec xdp, and test using Docker Nginx plus hping3.
Safety checks dominate: bounds-checked header parsing, zero-initialized map entries, and defaults that pass non-IPv4 frames. For monitoring packet drops, I also link to a practical guide: monitor packet drops with ebpf.
Key Takeaways
- I show a baseline XDP program that parses Ethernet/IPv4 headers and counts packets per source IP.
- Dropping at ingress reduces CPU load and keeps application latency predictable.
- Build with clang/LLVM and attach via ip link or libbpf; test with Docker Nginx and hping3.
- Use a 1-second rolling window and bpf_ktime_get_ns() to detect bursts safely.
- Bounds-check every access and prefer simple, verifier-friendly code paths.
- Loopback tests differ from a physical NIC; expect best gains on real hardware.
Why XDP and eBPF work for high-rate DDoS mitigation
I execute tiny, verifier-friendly programs at the NIC to cut latency and CPU work per packet. This runs code at the earliest kernel entry point so the device decides packet fate before the rest of the network stack touches it.
Early drops and steering reduce kernel queue growth and keep application latency predictable. Maps let kernel and user space share counters and rules without heavy copies—fast lookups, cheap updates, and per-CPU scaling.
Common use cases and clear benefits
Typical tasks: block abusive sources, rate‑limit per IP or prefix, steer flows for L4 load balancing, and sample traffic for analysis. In production, always-on programs scale across CPUs and avoid busy polling.
| Capability | Benefit | Tradeoff / Note |
|---|---|---|
| Early packet decision | Lower per-packet CPU cost and reduced queuing | Best in driver/native mode on supported devices |
| Shared maps | Fast counters and config exchange with user space | Design maps for low contention on multi-CPU systems |
| Sampling & steering | Analyze traffic with minimal kernel overhead | Use perf buffers or ring buffers for per-packet data |
| Offload support | Push logic into NIC to cut host load | Hardware support varies by vendor and firmware |
Cloudflare’s move to an always-on model proves viability: compiled rules, per-packet sampling, and multi-CPU scale. In short—place simple programs at this level and you keep the kernel and network services responsive during heavy attacks.
Plan your mitigation: per‑IP rate limiting at the earliest point in the stack
I rate-limit at ingress by counting packets per source IP and acting when a single sender crosses a set threshold. This keeps the kernel and apps responsive under volumetric stress.
Track packet rate per source IP in a BPF hash map
The baseline program uses a BPF_MAP_TYPE_HASH keyed by IPv4 source. Each entry holds last_update and packet_count. I call bpf_ktime_get_ns() to compare against a fixed 1-second window (TIME_WINDOW_NS) and a THRESHOLD of 250.
When the window rolls, I reset the count. If count > THRESHOLD, the packet is dropped. Hash maps are simple and easy to reason about, but they can contend when many CPUs touch the same entry.
Alternative: PerCpuArray counters to reduce contention
PerCpuArray keeps a local counter per CPU. Each CPU increments its slot without atomics. Aggregation happens from user space or during occasional lookups. This pattern cuts contention in high-rate UDP floods.
| Design | Where counters live | Pros | Cons |
|---|---|---|---|
| Hash map | Central map entry per src IP | Simple logic, easy gating | Contends under many CPUs |
| PerCpuArray | Counter per CPU per key | Low contention, fast increments | Needs aggregation, more memory |
| Windowing | last_update + packet_count | Predictable thresholds | Sliding windows add complexity |
Environment and prerequisites on a modern Linux kernel
Start by matching your system: kernel version, toolchain, and network utilities must line up.
I run this on Linux kernel 5.15 or newer—older kernels miss helpers and map features you’ll need. Use Debian or Ubuntu for repeatable package names and behavior.
- sudo apt-get install -y clang llvm libbpf-dev iproute2
- These tools cover compilation, loading, and interface control.
Compile with a single command: clang -O2 -g -target bpf -c xdp_ebpf_prog.c -o xdp_ebpf_prog.o. Keep the build minimal—no linking—so the loader finds the XDP section. This generates bytecode for the Berkeley Packet Filter VM in the kernel.
Operational notes:
- Raise RLIMIT_MEMLOCK if your loader needs it.
- Test on a non-production NIC; use SKB mode on laptops when native driver mode is unavailable.
- Know the rollback command: ip link set dev IFACE xdp off.
| Item | Why | Action |
|---|---|---|
| Kernel | Feature parity | Use ≥5.15 |
| Packages | Build & load | clang, llvm, libbpf-dev |
| Interface | Safe testing | Non-production NIC first |
Understand the eBPF XDP program that drops abusive traffic
I explain the program flow step by step so you can see why a packet is passed or dropped.
The code begins with standard headers: linux/bpf.h, if_ether.h, IPv4 headers and bpf_helpers. Constants follow: THRESHOLD = 250 and TIME_WINDOW_NS = 1000000000. These make tuning simple.

A BPF hash map stores per-source state keyed by __u32 src_ip. Each entry holds last_update and packet_count. This keeps per-IP counters in kernel memory with low latency.
Parsing is strict. The program bounds-checks Ethernet and IPv4 headers. Non-IPv4 frames get XDP_PASS. IPv4 packets yield saddr only after checking packet data pointers.
Time is monotonic: the code calls bpf_ktime_get_ns(). If the current time is within the 1-second window, the packet_count increments. If the window expired, the entry resets and starts at one.
| Event | Action | Result |
|---|---|---|
| Under threshold | update count | XDP_PASS |
| Exceeds threshold | immediate update | XDP_DROP |
| Non-IPv4 | skip counters | XDP_PASS |
I keep the program straight-line and loop-free to satisfy the verifier. The file includes a “GPL” license tag so helpers are available. This is the core of the rate-limiting path that keeps the stack responsive during high-rate attacks.
Build, attach, and verify your XDP program on an interface
This section walks through compiling a BPF object, attaching it to a device, and checking that the program runs at the expected level.
Compile the object
Run:
- clang -O2 -g -target bpf -c xdp_ebpf_prog.c -o xdp_ebpf_prog.o — -O2 optimizes for speed, -g keeps debug symbols, -target bpf emits bytecode for the Berkeley Packet Filter VM.
Attach and detach
Attach:
- IFACE=eth0; sudo ip link set dev “$IFACE” xdp obj xdp_ebpf_prog.o sec xdp — sec picks the XDP section name to load onto the device.
- Detach quickly: sudo ip link set dev “$IFACE” xdp off.
Verify the load and mode
Checks to run:
- ip -d link show dev “$IFACE” — look for XDP in driver (native) or skb mode and any attach errors.
- bpftool prog show and bpftool net — confirm program IDs, maps, and attachments.
- When using a libbpf loader, bump RLIMIT_MEMLOCK first; ip link handles basic cases.
Note: attaching to lo is useful for demos, but a physical NIC shows real offload and driver behavior. I keep a tested rollback step in scripts so I can remove the program under pressure.
Test DDoS behavior and interpret packet loss during mitigation
I spin up a local server and hammer it with synthetic traffic to measure drop rates and latency.
Start the target: docker run -p 1234:80 nginx. Attach the XDP program to lo for a safe demo: sudo ip link set dev lo xdp obj xdp_prog.o sec xdp.
Generate a SYN flood: sudo hping3 -i u1000 -S -p 1234 127.0.0.1. That sends one packet every 1 ms. Stop after a few seconds and read hping3 stats.
How to read the numbers
Look at total packets sent, responses received, and packet loss. High loss means the kernel dropped many packets after the threshold. Example: 3176 sent, 332 received → ~90% packet loss.
Check RTT min/avg/max—e.g., 0.2/4.4/11.4 ms. Stable avg RTT implies the server kept responding despite load. A rising avg or large max suggests queuing or CPU saturation.
Loopback vs physical NIC
Loopback tests validate logic only. They run in software and do not exercise driver native paths or NIC offload. Expect better real-world performance on a physical interface.
Move tests to a physical NIC and monitor CPU softirq, interface drop counters, and system load to measure true mitigation impact.
| Metric | Command / Source | Meaning | Action |
|---|---|---|---|
| Packets sent / received | hping3 summary | Shows how many packets reached or were answered by the server | Confirm drop path is active when loss is high |
| Packet loss (%) | hping3 calculation | Percent of sent packets not answered — direct indicator of drops | Tune THRESHOLD or window if loss is too aggressive |
| RTT min/avg/max | hping3 output | Latency spread under load — stability check | Investigate CPU or queue growth if avg rises |
eBPF for DDoS protection in production: tuning, tooling, and lessons
I focus on measurable knobs—CPU mode, offload, sampling rate—so mitigation stays predictable under load.
CPU modes, offload options, and tradeoffs
Choose native (driver) mode when you need peak performance. Use SKB as a safe fallback when drivers lack support.
Offload to the NIC can cut host CPU use, but verify feature parity and visibility before you trust it in production.
Rule delivery and scaling
Complex match logic can exceed verifier limits. Compiled ebpf programs often scale better than huge map-driven matchers.
Sampling, verifier limits, and updates
- Sample before drop with perf buffers and prandom helpers so analysis still sees representative traffic.
- Keep code straight-line and branch-light to satisfy the verifier.
- Swap programs atomically; validate loads and fall back on error.
| Area | What to watch | Action |
|---|---|---|
| Maps | growth and stale entries | size to real traffic; implement aging |
| CPU | softirq and app latency | measure under attack; tune thresholds |
| Tooling | loader and rollback | track versions; atomic swaps |
Operational hygiene matters: detach XDP, free pinned maps, and monitor resource use so your security posture and applications remain stable during attacks.
Where to go next: from lab demo to a reliable mitigation strategy
Move your tests off loopback and validate behavior on a real NIC. Measure CPU, latency, and queue metrics during controlled traffic so you see true kernel impact.
I expand the ebpf program to track multiple keys—source IP, port, or tuple—while keeping code verifier-friendly. Add a perf-buffer to sample packet data and inspect traffic even when most packets are dropped.
Parameterize THRESHOLD and TIME_WINDOW via maps or ELF constants. Build a small loader that compiles, toggles, and rolls back programs as part of CI. Export packet and packets counts, per-CPU counters, and map sizes to your monitoring system.
Harden parsing, plan escalation (prefix groups or reputation lists), and document rollback steps. Write a short post with measured results so your mitigation strategy is repeatable and data-driven.
FAQ
What does "Protect Linux from DDoS with eBPF" mean in practice?
Why use XDP and the Berkeley Packet Filter target for high-rate mitigation?
What common tasks do these programs handle?
How should I plan per‑IP rate limiting at the earliest stack point?
When should I use PerCpuArray counters instead of a hash map?
What kernel and toolchain prerequisites do I need?
How is a typical XDP packet-drop program structured?
How do you safely parse Ethernet and IPv4 headers in kernel code?
How is rate calculated inside the program?
What actions can an XDP program return?
How do I compile and attach an XDP program to an interface?
How can I confirm the program is running and in which CPU mode?
How should I test DDoS behavior in a lab?
What results should I expect when validating thresholds?
What are common caveats when testing on loopback versus real NICs?
What tuning and operational considerations matter in production?
How do I deliver rules at scale without causing load or verifier issues?
How can I analyze traffic before discarding packets?
How do I manage verifier limits and code complexity?
What operational hygiene should I follow for map sizing and cleanup?
Where do I go next after a successful lab demo?
Related: XDP Offload vs Native: Which Mode Runs on Your Hardware
