eBPF for DDoS protection
eBPF Use Cases
William  

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.

Table of Contents

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.

CapabilityBenefitTradeoff / Note
Early packet decisionLower per-packet CPU cost and reduced queuingBest in driver/native mode on supported devices
Shared mapsFast counters and config exchange with user spaceDesign maps for low contention on multi-CPU systems
Sampling & steeringAnalyze traffic with minimal kernel overheadUse perf buffers or ring buffers for per-packet data
Offload supportPush logic into NIC to cut host loadHardware 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.

DesignWhere counters liveProsCons
Hash mapCentral map entry per src IPSimple logic, easy gatingContends under many CPUs
PerCpuArrayCounter per CPU per keyLow contention, fast incrementsNeeds aggregation, more memory
Windowinglast_update + packet_countPredictable thresholdsSliding 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.
ItemWhyAction
KernelFeature parityUse ≥5.15
PackagesBuild & loadclang, llvm, libbpf-dev
InterfaceSafe testingNon-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 modern workspace featuring a sleek desk with a high-end laptop displaying a terminal window filled with intricate eBPF program code. In the foreground, focus on a close-up of a network diagram, highlighting packet flow and data paths, with vibrant colors illustrating connections. The middle ground includes a professional, diverse team of IT engineers collaborating, dressed in business casual attire, displaying expressions of concentration and teamwork. The background showcases a softly lit office with tech gadgets, monitors showing real-time traffic statistics, and a large window revealing a cityscape bathed in gentle morning light. The atmosphere conveys innovation and focus, emphasizing the importance of DDoS protection with eBPF technology.

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.

EventActionResult
Under thresholdupdate countXDP_PASS
Exceeds thresholdimmediate updateXDP_DROP
Non-IPv4skip countersXDP_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 xdpsec 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.

MetricCommand / SourceMeaningAction
Packets sent / receivedhping3 summaryShows how many packets reached or were answered by the serverConfirm drop path is active when loss is high
Packet loss (%)hping3 calculationPercent of sent packets not answered — direct indicator of dropsTune THRESHOLD or window if loss is too aggressive
RTT min/avg/maxhping3 outputLatency spread under load — stability checkInvestigate 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.
AreaWhat to watchAction
Mapsgrowth and stale entriessize to real traffic; implement aging
CPUsoftirq and app latencymeasure under attack; tune thresholds
Toolingloader and rollbacktrack 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?

It means running a small packet program inside the kernel that inspects and acts on packets at wire speed. I place logic at the earliest point in the network stack so abusive flows are dropped before they consume sockets, CPUs, or userland resources.

Why use XDP and the Berkeley Packet Filter target for high-rate mitigation?

XDP hooks at the NIC driver level, so it gives the lowest latency and highest throughput. Compiling to the BPF target with clang/LLVM produces a verified kernel-safe binary that can run in native, generic, or offload modes for different performance profiles.

What common tasks do these programs handle?

Typical use cases: selective packet filtering, per-source rate limiting, simple load distribution, and lightweight telemetry. I also use packet sampling to analyze suspicious flows before dropping them.

How should I plan per‑IP rate limiting at the earliest stack point?

Track packet counts per source IP inside a BPF hash map keyed by the IP. Enforce a time window and threshold to decide when to drop. Keep maps sized for expected cardinality and plan eviction or TTL logic.

When should I use PerCpuArray counters instead of a hash map?

Use PerCpuArray counters when you need to avoid update contention on high packet rates and when you can shard counters by CPU. That reduces atomic operations and improves throughput at the cost of more memory and merging logic.

What kernel and toolchain prerequisites do I need?

A modern Linux kernel with BPF and XDP support, clang/LLVM for BPF target, iproute2 with XDP support, and bpftool for inspection. I also recommend kernel headers and libbpf for userspace helpers.

How is a typical XDP packet-drop program structured?

The program includes header parsing, map declarations, a threshold and time-window policy, and a tiny decision path. It verifies bounds on packets, reads timestamps with bpf_ktime_get_ns, updates counters, and returns XDP_PASS or XDP_DROP.

How do you safely parse Ethernet and IPv4 headers in kernel code?

Always check packet bounds before accessing header fields. Use helper macros or explicit pointer checks to ensure you don’t read past data_end. The verifier enforces safety but explicit checks avoid runtime panics and verifier rejections.

How is rate calculated inside the program?

I read a high-resolution timestamp (bpf_ktime_get_ns), compare it to a stored window start, and update per-IP counters. If the counter exceeds the configured threshold within the window, the packet gets dropped.

What actions can an XDP program return?

Common actions are XDP_PASS to forward the packet into the stack and XDP_DROP to silently discard it. Other modes exist, like XDP_REDIRECT for redirecting flows to another interface or AF_XDP sockets for userland processing.

How do I compile and attach an XDP program to an interface?

Compile with clang/LLVM targeting BPF and link with the proper section name. Attach using ip link set dev xdp obj sec or use xdp-loader tools. Verify load with bpftool prog show and check interface state via ip link.

How can I confirm the program is running and in which CPU mode?

Use bpftool prog show to see the loaded program and its ID. ip -details link shows XDP info and whether it’s native or generic. Check dmesg for verifier messages and perf stats to verify activity.

How should I test DDoS behavior in a lab?

Run a simple service (for example, an nginx container) and generate traffic with tools like hping3 or tcpreplay. Measure packet loss, RTT, and the effect of thresholds. Test against a physical NIC—loopback behaves differently.

What results should I expect when validating thresholds?

You should see packet drops increase once a source exceeds the configured rate and a corresponding rise in packet loss metrics. Latency should remain low for allowed traffic; abused flows should be curtailed early in the stack.

What are common caveats when testing on loopback versus real NICs?

Loopback bypasses driver and hardware features and often uses the generic XDP path, so it underestimates performance. Real NICs allow native XDP and offload features—expect different timing and throughput characteristics.

What tuning and operational considerations matter in production?

Decide CPU mode (native vs generic vs offload), size maps conservatively, monitor verifier limits, and plan a safe update path for code. Use perf buffers for sampling before dropping, and automate cleanup of maps on restart.

How do I deliver rules at scale without causing load or verifier issues?

Prefer compiled, merged programs over pushing many individual filters. Use policy distribution tools to update maps rather than reloading programs frequently. Test verifier acceptance and measure CPU cost before rollout.

How can I analyze traffic before discarding packets?

Sample packets into a perf buffer or redirect a small subset to AF_XDP for userland analysis. That gives visibility into attack patterns while keeping the bulk path in-kernel and fast.

How do I manage verifier limits and code complexity?

Keep programs small and linear. Avoid deeply nested branches and large stack usage. Offload complex logic to userland via maps or AF_XDP if needed. Test with bpftool and incrementally add features.

What operational hygiene should I follow for map sizing and cleanup?

Allocate maps based on expected concurrent sources and set eviction or TTL policies. Ensure startup scripts clear stale maps and provide graceful shutdown to remove attached XDP programs and free resources.

Where do I go next after a successful lab demo?

Move to staged rollouts: test on a small production-facing interface, monitor CPU and packet metrics, iterate on thresholds, and integrate with existing observability and incident response tooling.

Related: XDP Offload vs Native: Which Mode Runs on Your Hardware

Related: Block IP Addresses With eBPF: XDP Attach Syntax