Skip to content
ArticlesDevOpsNetworking

eBPF and XDP for DDoS Defense: High-Throughput Kernel Packet Filtering

17 min readsalahtobok
eBPF and XDP for DDoS Defense: High-Throughput Kernel Packet Filtering for Web Infrastructure

The Escalating Threat of Volumetric DDoS Attacks

Distributed Denial of Service (DDoS) attacks represent one of the most destructive threats facing modern web platforms. As cloud computing infrastructure expands and botnets compromise millions of Internet-of-Things (IoT) devices, the scale and velocity of network attacks have reached astronomical proportions. Attackers routinely generate packet floods exceeding hundreds of millions of packets per second (Mpps) and multiple terabits per second of raw network bandwidth.

Modern volumetric attacks do not attempt to crack cryptographic keys or exploit application-level SQL injection bugs. Instead, they weaponize Layer 4 transport protocols. By launching synchronized TCP SYN floods, UDP reflection amplifications via open DNS or NTP resolvers, and ICMP floods, attackers aim to exhaust the physical memory and CPU processing capacity of edge ingress servers.

When an ingress load balancer or reverse proxy is bombarded by a high-rate packet flood, traditional operating system network stacks collapse. The server is not brought down by software application crashes; it is overwhelmed by the sheer overhead of operating system kernel memory allocations and interrupt handling. To survive modern volumetric DDoS attacks, web platforms must discard malicious packets before the operating system allocates a single byte of memory.

The Linux Kernel sk_buff Bottleneck

To understand why traditional network firewalls fail during massive DDoS attacks, software engineers must examine how the standard Linux kernel processes network packets. For decades, the core data structure of the Linux network stack has been the socket buffer, known as struct sk_buff.

Packet Path Comparison: Traditional sk_buff Linux Stack vs XDP Driver Bypass
Figure 1: Tracing packet drop paths: heavy sk_buff allocation vs instant XDP driver discard.

When a physical network interface card (NIC) receives an Ethernet frame, the hardware driver places the packet into a Direct Memory Access (DMA) ring buffer. The driver then triggers a hardware interrupt, which schedules a software interrupt (NAPI softirq). The kernel invokes allocate_skb() to allocate an sk_buff struct. An sk_buff is a large, complex kernel object (typically 240 bytes or more) containing dozens of pointers, reference counters, timestamps, protocol identifiers, and memory locks.

Only after this heavy data structure is allocated does the packet enter the Netfilter subsystem, where tools like iptables and nftables evaluate firewall rules. In iptables, rules are evaluated sequentially in the PREROUTING table. If you have 500 firewall rules, every single incoming packet must traverse that list sequentially. CPU cache lines are constantly evicted, and CPU cores spend all their clock cycles allocating and freeing memory.

On modern enterprise server hardware, a single CPU core running standard Linux iptables can process and drop approximately 1.5 to 2.0 million packets per second (Mpps). If an attacker launches a modest 10 Mpps attack across four CPU cores, the kernel runs out of CPU cycles simply allocating sk_buff structures. The server freezes completely, dropping legitimate customer traffic alongside the attack traffic.

What is eBPF and How Does it Operate?

The solution to this architectural bottleneck is extended Berkeley Packet Filter (eBPF). Originally developed for basic packet filtering in tools like tcpdump, modern eBPF has evolved into a revolutionary in-kernel virtual machine. It allows software engineers to run sandboxed, custom bytecode programs directly inside the Linux kernel without modifying the kernel source code or loading dangerous out-of-tree kernel modules.

eBPF Safety and Execution Architecture: Clang/LLVM to In-Kernel JIT Compiler
Figure 2: Clang compilation, in-kernel verifier mathematical proofs, and native JIT machine code execution.

The operational lifecycle of an eBPF program follows a strict three-phase pipeline:

  1. Compilation: Engineers author filter logic in restricted C or Rust. The Clang/LLVM compiler translates the high-level code into an ELF binary containing compact 64-bit BPF bytecode instructions.
  2. In-Kernel Verification: When the user-space control program loads the bytecode into the kernel using the bpf() system call, the kernel runs the eBPF Verifier. The verifier performs rigorous formal mathematical analysis. It simulates all possible execution paths to prove that the program will never dereference a dangling pointer, access out-of-bounds memory, execute infinite loops, or panic the operating system. If the code fails safety verification, the kernel refuses to load it.
  3. Just-In-Time (JIT) Compilation: Once verified, the in-kernel JIT engine compiles the BPF bytecode directly into native machine instructions (x86_64 or ARM64). The program executes at full hardware speed, identical to hand-optimized native kernel C code.

eXpress Data Path (XDP): Line-Rate Packet Interception

While eBPF can attach to dozens of kernel tracepoints, system calls, and cgroups, its most powerful networking hook is the eXpress Data Path (XDP). Introduced in Linux kernel 4.8, XDP provides a safe, programmable, high-speed packet processing hook located at the lowest possible level of the network subsystem.

An XDP program executes inside the network interface card’s driver layer, directly inside the Receive (RX) queue ring buffer. Crucially, XDP executes before the Linux kernel allocates an sk_buff structure. When a packet arrives, XDP inspects the raw memory page containing the Ethernet frame (represented by a lightweight xdp_buff pointer). The program reads the packet header bytes, makes an instantaneous decision, and returns one of five standard action verdicts:

  • XDP_DROP: Discards the packet immediately on the wire. The driver recycles the DMA buffer without allocating an sk_buff, without notifying the OS network stack, and without context switching. The CPU cost to drop a packet is reduced from 700 nanoseconds to under 40 nanoseconds.
  • XDP_PASS: Permits the packet to proceed upwards into the standard Linux network stack. The kernel allocates an sk_buff, and the packet is processed normally by TCP sockets, HTTP servers, or Envoy reverse proxies.
  • XDP_TX: Bounces the packet back out the same network interface that received it. This is widely used for high-speed Layer 4 load balancing and stateless SYN cookie emission.
  • XDP_REDIRECT: Forwards the raw packet directly to another network interface, a different CPU core via Receive Side Scaling (RSS), or into a user-space application using an AF_XDP socket, completely bypassing the kernel TCP/IP stack.
  • XDP_ABORTED: Discards the packet due to an internal program exception and emits an eBPF tracepoint for debugging.

Because XDP_DROP operates before memory allocation, a single modern enterprise server core running XDP can process and discard between 15 and 25+ million packets per second. A standard 16-core edge server can effortlessly absorb a 100 Gbps volumetric DDoS attack while consuming less than 10% of total CPU capacity.

XDP Execution Modes: Offloaded, Native, and Generic

XDP programs can run in three distinct operational modes, depending on hardware support and infrastructure environment:

  1. Offloaded Mode (SmartNIC): The eBPF bytecode is JIT-compiled and loaded directly into the processor of a SmartNIC (such as Netronome Agilio or NVIDIA BlueField). The packets are filtered entirely on the network card hardware without utilizing host CPU cycles or PCIe bus bandwidth. This mode delivers maximum performance (wire-speed line rate), but requires specialized hardware.
  2. Native Driver Mode: The program executes inside the main network interface driver before sk_buff creation. This is supported out of the box by all major enterprise 10G/40G/100G network drivers, including Intel ixgbe and i40e, Mellanox mlx4 and mlx5, and Broadcom bnxt. Native mode is the gold standard for cloud and bare-metal production environments.
  3. Generic Mode (SKB Mode): The program executes after the kernel allocates an sk_buff, but before the Netfilter stack. Generic mode works on any Linux network interface (including virtual interfaces like veth in Docker and Kubernetes), making it ideal for testing, local development, and container routing where native driver support is unavailable.

In-Kernel BPF Maps: Stateful Filtering at Scale

A pure packet filter that only examines static IP headers is insufficient for modern dynamic attacks. DDoS mitigation systems must maintain state: tracking packet rates per source IP, maintaining dynamically updated blacklists, and collecting telemetry. However, eBPF programs cannot access arbitrary memory or use global C variables.

Instead, eBPF programs communicate and maintain state using BPF Maps. A BPF map is an efficient in-kernel key-value data structure allocated in kernel memory. BPF maps can be read and updated by both the in-kernel XDP filter and user-space management daemons (written in Go, Rust, or Java).

Key BPF map types used in DDoS defense include:

  • BPF_MAP_TYPE_HASH: A high-performance hash table providing O(1) lookups. When an attack begins, a user-space intelligence daemon populates this map with malicious IP addresses and CIDR blocks. The XDP program looks up each incoming packet’s source IP in less than 50 nanoseconds, dropping matching packets instantly via XDP_DROP.
  • BPF_MAP_TYPE_LRU_HASH: A Least-Recently-Used hash map that automatically evicts older entries when full. This is essential for tracking per-IP request rates without exhausting kernel memory during massive multi-million-IP botnet floods.
  • BPF_MAP_TYPE_PERCPU_ARRAY: Allocates a separate array on each CPU core. The XDP filter increments packet counters and byte volumes without acquiring multi-core CPU locks. User-space monitoring tools read these counters to display live metrics without impacting packet filtering speed.

Mitigating SYN Floods with XDP SYN Cookies

One of the most devastating Layer 4 attacks is the TCP SYN flood (standardized under RFC 4987). In a standard TCP handshake, the client sends a SYN packet, the server responds with SYN-ACK and allocates a half-open connection entry in its kernel listen backlog queue, and the client completes the handshake with an ACK packet.

Stateful SYN Flood Defense: In-Kernel BPF Maps and SYN Cookie Generation
Figure 3: Stateless XDP_TX SYN cookie defense absorbing connection exhaustion without backlog memory.

In a SYN flood, an attacker transmits millions of spoofed SYN packets per second and intentionally never sends the completing ACK. The server’s kernel connection backlog quickly saturates, causing all subsequent connection attempts from legitimate users to fail with connection timeouts.

Using XDP, engineers deploy an ingenious defense: Stateless In-Kernel SYN Cookies. When an incoming TCP SYN arrives at the NIC, the XDP program intercepts it before the kernel allocates any socket state:

  1. XDP extracts the source IP, destination IP, source port, and destination port (the 4-tuple).
  2. XDP computes a cryptographic hash using a secret kernel key and a timestamp counter: cookie = HASH(4-tuple, secret, timestamp).
  3. XDP modifies the packet headers directly in memory, converting the packet into a TCP SYN-ACK containing the cookie as the initial sequence number.
  4. XDP returns XDP_TX, immediately transmitting the SYN-ACK back out the network interface!

Because the server stores zero state in memory, the listen backlog never fills up. When a legitimate client responds with the final ACK packet, XDP validates the cookie sequence number. If the cookie is cryptographically valid, XDP returns XDP_PASS, allowing the Linux kernel to finally allocate the socket for the verified client. If the packet is part of a spoofed flood, no ACK arrives, and zero memory was ever wasted.

Packet Filtering Technologies Comparison Matrix

To evaluate where eBPF and XDP fit into an enterprise security architecture, we compare them against legacy and alternative packet processing frameworks:

Technology Hook Layer Kernel Memory Allocation Max Drop Rate (Mpps/core) CPU Overhead Linux Socket Integration
iptables (Netfilter) Kernel PREROUTING Full sk_buff allocated 1.5 – 2.0 Mpps Very High (Softirq & memory churn) Native Linux TCP/IP stack
nftables Kernel PREROUTING Full sk_buff allocated 2.5 – 3.5 Mpps High (Optimized VM in Netfilter) Native Linux TCP/IP stack
DPDK (Intel) User-Space Driver Zero Kernel (User Polling) 35.0 – 45.0+ Mpps 100% Constant Core Saturation None (Completely bypasses Linux)
eBPF / XDP (Driver) NIC Driver RX Ring ZERO sk_buff allocation 20.0 – 30.0+ Mpps Minimal (Only when packets arrive) Seamless (via XDP_PASS)
DDoS Defense Architecture Scorecard: iptables vs nftables vs DPDK vs XDP
Figure 4: Performance and architectural comparison across iptables, nftables, DPDK, and eBPF/XDP.

Practical Java 25 XDP Packet Filtering Engine Implementation

To demonstrate the operational dynamics of an XDP packet filter and BPF map registry, we built a comprehensive simulation engine in Java 25. The project uses pure Java standard libraries, record classes, and strict architectural isolation without any Spring Boot dependencies.

The core packet abstraction models transport Layer 3 and Layer 4 properties:

package com.webcodein.workshop.ebpf.model;

import java.util.Objects;

public record IpPacket(
        String srcIp,
        String dstIp,
        int protocol, // 6 = TCP, 17 = UDP
        int srcPort,
        int dstPort,
        int lengthBytes,
        boolean isSynFlag,
        boolean isAckFlag,
        long timestampNanos
) {
    public static IpPacket tcpSyn(String srcIp, String dstIp, int srcPort, int dstPort) {
        return new IpPacket(srcIp, dstIp, 6, srcPort, dstPort, 60, true, false, System.nanoTime());
    }

    public static IpPacket tcpData(String srcIp, String dstIp, int srcPort, int dstPort, int length) {
        return new IpPacket(srcIp, dstIp, 6, srcPort, dstPort, length, false, true, System.nanoTime());
    }
}

Next, we model the BpfMapRegistry, simulating high-speed in-kernel concurrent hash maps and atomic per-CPU counters:

package com.webcodein.workshop.ebpf.engine;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;

public class BpfMapRegistry {

    private final ConcurrentMap<String, Boolean> blacklistMap = new ConcurrentHashMap<>();
    private final ConcurrentMap<String, AtomicLong> synCounters = new ConcurrentHashMap<>();
    private final AtomicLong totalPacketsReceived = new AtomicLong();
    private final AtomicLong totalPacketsDropped = new AtomicLong();
    private final AtomicLong totalPacketsPassed = new AtomicLong();

    public void blacklistIp(String ip) {
        blacklistMap.put(ip, Boolean.TRUE);
    }

    public boolean isBlacklisted(String ip) {
        return blacklistMap.containsKey(ip);
    }

    public long incrementSynCounter(String srcIp) {
        return synCounters.computeIfAbsent(srcIp, k -> new AtomicLong(0)).incrementAndGet();
    }

    public void recordPacket(boolean dropped) {
        totalPacketsReceived.incrementAndGet();
        if (dropped) {
            totalPacketsDropped.incrementAndGet();
        } else {
            totalPacketsPassed.incrementAndGet();
        }
    }
}

Finally, the XdpPacketFilter executes the decision pipeline at sub-microsecond speeds:

package com.webcodein.workshop.ebpf.engine;

import com.webcodein.workshop.ebpf.model.FilterVerdict;
import com.webcodein.workshop.ebpf.model.IpPacket;
import com.webcodein.workshop.ebpf.model.XdpAction;

public class XdpPacketFilter {

    private final BpfMapRegistry bpfMapRegistry;
    private final int synRateLimitThreshold;

    public XdpPacketFilter(BpfMapRegistry bpfMapRegistry, int synRateLimitThreshold) {
        this.bpfMapRegistry = bpfMapRegistry;
        this.synRateLimitThreshold = synRateLimitThreshold;
    }

    public FilterVerdict processPacket(IpPacket packet) {
        long startNanos = System.nanoTime();

        // 1. O(1) BPF Blacklist Map Lookup
        if (bpfMapRegistry.isBlacklisted(packet.srcIp())) {
            bpfMapRegistry.recordPacket(true);
            return new FilterVerdict(XdpAction.XDP_DROP, "BPF_MAP_BLACKLIST_MATCH", System.nanoTime() - startNanos);
        }

        // 2. SYN Flood Mitigation
        if (packet.protocol() == 6 && packet.isSynFlag() && !packet.isAckFlag()) {
            long synCount = bpfMapRegistry.incrementSynCounter(packet.srcIp());
            if (synCount > synRateLimitThreshold) {
                bpfMapRegistry.recordPacket(true);
                return new FilterVerdict(XdpAction.XDP_DROP, "SYN_FLOOD_THRESHOLD_EXCEEDED", System.nanoTime() - startNanos);
            }
        }

        // 3. Normal traffic: PASS to Linux network stack
        bpfMapRegistry.recordPacket(false);
        return new FilterVerdict(XdpAction.XDP_PASS, "TRAFFIC_CLEARED_TO_SKBUFF", System.nanoTime() - startNanos);
    }
}

Real-World Enterprise Case Studies

Case Study 1: Cloudflare Ingress Defense Against 26 Million Request-per-Second DDoS

Cloudflare protects millions of websites against global cyberattacks. In 2022, an attacker unleashed a synchronized botnet attack generating 26 million HTTPS requests per second targeting a customer in the cryptocurrency industry. Under traditional Linux iptables filtering, the edge load balancers would have frozen within 200 milliseconds due to sk_buff memory exhaustion.

Cloudflare’s proprietary L4 DDoS mitigation architecture (codenamed Gatebot and l4drop) operates entirely within XDP driver mode. When the attack erupted, an automated user-space analysis daemon identified the malicious IP signatures and injected them into kernel BPF hash maps across all edge data centers in under two seconds. The XDP filters immediately dropped the attack traffic at line rate on the wire with zero sk_buff overhead. Customer web services experienced zero downtime and zero latency degradation.

Case Study 2: Tier-1 E-Commerce Retailer Defending Black Friday Flash Sales

A global online retail platform experienced massive TCP SYN flood attacks during its annual Black Friday sale. Rogue competitors deployed botnets to flood port 443 with half-open connections, saturating the Linux kernel tcp_max_syn_backlog. Genuine shoppers attempting to check out experienced connection dropped errors.

The infrastructure team deployed an XDP stateless SYN cookie filter in native driver mode on their bare-metal ingress clusters. The XDP program responded to all incoming SYN packets with cryptographic SYN-ACK cookies using XDP_TX, storing zero state in kernel memory. The SYN flood was completely neutralized, allowing millions of legitimate transactions to complete without error.

Case Study 3: Global Authoritative DNS Provider Absorbing 100 Gbps UDP Amplification

A managed DNS provider was targeted by a 100 Gbps NTP and DNS amplification attack. Attackers spoofed the victim’s IP address and queried open recursive NTP servers, triggering massive UDP response bursts (packet sizes >1,400 bytes) targeting port 53.

The provider deployed an XDP filter that inspected incoming UDP packets. Packets exceeding 1,400 bytes arriving on privileged ports without an active outbound query state were instantly dropped using XDP_DROP. Because XDP rejected the packets at the network driver layer, the Linux UDP socket buffers remained completely clear, allowing normal DNS queries to resolve in under 15 milliseconds.

Production Anti-Patterns and Pitfalls

Anti-Pattern 1: Relying on Application-Level Rate Limiters for Layer 4 Volumetric DDoS

Many backend development teams attempt to mitigate volumetric DDoS attacks using application-level rate limiting filters in web frameworks or reverse proxies (such as NGINX or Envoy). By the time a packet reaches user space, the Linux kernel has already allocated an sk_buff, parsed TCP streams, and performed expensive context switches. If the attack exceeds 2 Mpps, the CPU will saturate before the application code even executes.

Solution: Layer 4 volumetric filtering must occur at the XDP or SmartNIC layer. Reserve application rate limiters strictly for Layer 7 business logic governance.

Anti-Pattern 2: Authoring Unbounded Loops in eBPF C Programs

Developers transitioning to eBPF often write nested for loops to parse variable-length packet headers. When the BPF bytecode is submitted to the kernel, the eBPF Verifier strictly rejects the program with unbounded loop detected errors.

Solution: In eBPF, all loops must have compile-time bounded maximum iterations (using #pragma unroll) or modern kernel bounded loop primitives. Always verify packet bounds by comparing data + offset <= data_end before reading any memory.

Anti-Pattern 3: Synchronous BPF Map Updates on the Packet Processing Hot Path

If an XDP program performs heavy synchronous map writes or invokes heavy spinlocks across multiple CPU cores for every incoming packet, the multi-core CPU cache interconnect will saturate, degrading packet processing throughput.

Solution: Use BPF_MAP_TYPE_PERCPU_ARRAY or per-core lockless hash maps for hot-path packet accounting. Offload aggregation and complex analytics to asynchronous user-space daemons.

Frequently Asked Questions

What is the difference between eBPF and XDP?

eBPF is the underlying in-kernel virtual machine and execution environment. XDP (eXpress Data Path) is a specific networking hook located at the lowest layer of the network driver that allows eBPF programs to process raw network packets before sk_buff memory allocation.

Can an eBPF program crash or panic the Linux kernel?

No. Every eBPF program must pass the rigorous in-kernel eBPF Verifier before it can be loaded. The verifier performs mathematical proofs guaranteeing that the code has no out-of-bounds memory accesses, no dangling pointers, and no infinite loops.

Why is XDP faster than iptables and nftables?

iptables and nftables operate in the Netfilter subsystem after the Linux kernel allocates an expensive sk_buff object for every packet. XDP operates directly in the network driver RX ring buffer before sk_buff allocation, reducing packet drop latency from ~700ns to under 40ns.

Does XDP require specialized hardware or SmartNICs?

No. While XDP can be offloaded to SmartNICs for hardware acceleration, native XDP driver mode runs on standard commercial server network cards (Intel, Mellanox, Broadcom) using standard host CPU cores.

How do legitimate packets reach backend web applications when using XDP?

When the XDP program verifies that a packet is legitimate, it returns the XDP_PASS action verdict. The Linux kernel then allocates an sk_buff and routes the packet normally through the TCP/IP stack to backend applications and sockets.

Companion Workshop Code

The companion workshop project for this guide provides a complete Java 25 packet filtering engine and in-kernel BPF map simulation without external web frameworks:

For related networking and security architectures, explore our in-depth guides on HTTP/3 and QUIC Protocol Head-of-Line Blocking, Zero-Trust mTLS with Envoy Proxy, WebSockets vs. SSE vs. WebTransport, Zero-Trust Spring Security 7 eBPF Mesh, and Distributed API Rate Limiting.

Official Specifications and References

Comments