XDP and eBPF Packet Filtering: Mitigating Multi-Gigabit DDoS at the Driver Layer
Technical Overview & Threat Model#
Volumetric Distributed Denial of Service (DDoS) attacks pose a fundamental resource exhaustion problem for Linux edge routers, container ingress gateways, and application hosts. High-rate SYN floods, UDP amplification, and DNS reflection vectors routinely generate tens of millions of packets per second (Mpps). At these volumes, host failure rarely stems from raw network bandwidth saturation; rather, the Linux operating system exhausts CPU cycles inside the core networking subsystem.
In standard Linux network packet processing, every incoming frame received by a Network Interface Card (NIC) follows a computationally intensive path:
- Ring Buffer Ingestion & Interrupt Handling: The NIC places packets into a circular Direct Memory Access (DMA) ring buffer and triggers a hard interrupt (IRQ), followed by a SoftIRQ (
NET_RX_SOFTIRQ) executed by thenapi_poll()routine. - Socket Buffer (
sk_buff) Allocation: The kernel allocates a complex, memory-heavystruct sk_buff(approximately 240 bytes plus packet payload buffers) to represent the packet. - Subsystem Traversal: The packet traverses the netfilter connection tracker (
conntrack), route table lookups, and packet filter chains (iptables/nftables).
Under a flood of 15–30 Mpps, memory allocation overhead for sk_buff structures and spinlock contention across conntrack hash tables consume 100% of CPU time, dropping legitimate traffic before firewall rules can even evaluate the packet headers.
sequenceDiagram
autonumber
participant NIC as Network Interface Card (NIC)
participant Driver as Device Driver Ring
participant XDP as eXpress Data Path (eBPF)
participant KernelNet as Linux Kernel Network Stack (sk_buff)
participant App as Application Socket
NIC->>Driver: Receive Raw Ethernet Frame (DMA)
Driver->>XDP: Execute In-Driver eBPF Hook (xdp_buff)
alt Packet Matched in Blocklist / Rate Limit Exceeded
XDP-->>Driver: Return XDP_DROP
Driver-->>NIC: Recycle RX Buffer Immediately (Zero sk_buff Overhead)
else Legitimate Inbound Traffic
XDP->>Driver: Return XDP_PASS
Driver->>KernelNet: Allocate sk_buff & Forward to IP/TCP Stack
KernelNet->>App: Deliver to Userspace Socket (epoll/read)
endThe eXpress Data Path (XDP) eliminates this bottleneck by executing verified eBPF bytecode directly inside the network driver subsystem before sk_buff allocation occurs. Operating directly on raw page buffers (struct xdp_buff), XDP enables drop rates exceeding 20–35 million packets per second per CPU core on commodity 100GbE hardware.
Architecture & Kernel Packet Flow#
XDP operates across three distinct deployment modes depending on hardware capabilities and virtualization layers:
- Offloaded XDP: The eBPF program is compiled into native NPU/NIC microcode and runs directly on a SmartNIC processor, achieving zero CPU host utilization.
- Native / Driver XDP: The program executes inside the device driver's main receive path prior to
sk_buffallocation (supported natively in modern drivers such asmlx5,ixgbe,virtio_net). - Generic XDP: A fallback software mode running after initial
sk_buffallocation innetif_receive_skb(), intended for testing and unsupported legacy hardware.
flowchart TD
subgraph Hardware [Physical Network Hardware]
PhysicalNIC[NIC PHY / MAC Layer]
DMARing[DMA Receive Ring Buffer]
end
subgraph DriverLayer [Driver & XDP Execution Boundary]
DriverPoll[NAPI Poll Loop - Driver Layer]
XDPHook{XDP eBPF Program}
BPFMap[(eBPF Hash Map: IP Blocklist & Token Buckets)]
end
subgraph CoreKernel [Standard Linux Network Subsystem]
AllocSKB[Allocate struct sk_buff]
Conntrack[Netfilter conntrack Evaluation]
RoutingTable[IP Routing & Socket Dispatch]
UserApp[Userspace Application Daemon]
end
PhysicalNIC --> DMARing
DMARing --> DriverPoll
DriverPoll --> XDPHook
XDPHook <--> BPFMap
XDPHook -->|XDP_DROP| DMARing
XDPHook -->|XDP_PASS| AllocSKB
AllocSKB --> Conntrack
Conntrack --> RoutingTable
RoutingTable --> UserAppThe table below contrasts traditional packet filtering technologies against XDP processing modes:
| Technology | Hook Location | Processing Unit | Drop Performance | State Memory Cost | Common Bottleneck |
|---|---|---|---|---|---|
| iptables (Netfilter) | Kernel IP Stack (NF_INET_PRE_ROUTING) |
Full sk_buff |
1.5 – 3.0 Mpps/core | High (~240B + overhead) | conntrack table locks, slab allocations |
| nftables | Kernel IP Stack (netfilter) |
Full sk_buff |
2.5 – 4.5 Mpps/core | High (~240B + overhead) | Memory allocation, SoftIRQ context switches |
TC BPF (cls_bpf) |
Traffic Control Ingress/Egress | Full sk_buff |
5.0 – 9.0 Mpps/core | Moderate (post-allocation) | Pre-allocated skb manipulation overhead |
| XDP (Generic) | Device Ingress Trampoline | sk_buff pointer |
4.0 – 8.0 Mpps/core | Moderate (post-allocation) | Software interrupt queue backlog |
| XDP (Native Driver) | Driver RX Descriptor Ring | Raw xdp_buff |
25.0 – 40.0 Mpps/core | Negligible (zero heap alloc) | PCIe bus throughput, CPU memory bandwidth |
Technical Walkthrough: In-Driver eBPF Filter Implementation#
The implementation below provides an in-driver XDP packet filter written with libbpf and BPF CO-RE. It validates Ethernet and IP headers, performs zero-copy lookups against an eBPF LRU hash map containing malicious IP addresses, and applies token-bucket rate limiting against TCP SYN floods.
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
char LICENSE[] SEC("license") = "Dual BSD/GPL";
#define ETH_P_IP 0x0800
#define IPPROTO_TCP 6
#define IPPROTO_UDP 17
struct rate_limit_t {
u64 last_seen_ns;
u32 packet_count;
};
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 256000);
__type(key, u32);
__type(value, u8);
} blocked_ips SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 128000);
__type(key, u32);
__type(value, struct rate_limit_t);
} rate_limit_map SEC(".maps");
SEC("xdp")
int xdp_ddos_filter(struct xdp_md *ctx)
{
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
// Bounds check: Ethernet Header
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_PASS;
if (eth->h_proto != bpf_htons(ETH_P_IP))
return XDP_PASS;
// Bounds check: IPv4 Header
struct iphdr *iph = (void *)(eth + 1);
if ((void *)(iph + 1) > data_end)
return XDP_PASS;
u32 src_ip = iph->saddr;
// Direct Hash Table Lookup for Known Malicious IP
u8 *blocked = bpf_map_lookup_elem(&blocked_ips, &src_ip);
if (blocked)
return XDP_DROP;
// Inspect TCP Layer for SYN Flood Mitigation
if (iph->protocol == IPPROTO_TCP) {
struct tcphdr *tcph = (void *)iph + (iph->ihl * 4);
if ((void *)(tcph + 1) > data_end)
return XDP_PASS;
if (tcph->syn && !tcph->ack) {
u64 now = bpf_ktime_get_ns();
struct rate_limit_t *rl = bpf_map_lookup_elem(&rate_limit_map, &src_ip);
if (rl) {
// Window: 1 second (1,000,000,000 ns)
if (now - rl->last_seen_ns < 1000000000ULL) {
if (rl->packet_count > 50) {
return XDP_DROP;
}
rl->packet_count++;
} else {
rl->last_seen_ns = now;
rl->packet_count = 1;
}
} else {
struct rate_limit_t new_rl = {
.last_seen_ns = now,
.packet_count = 1,
};
bpf_map_update_elem(&rate_limit_map, &src_ip, &new_rl, BPF_ANY);
}
}
}
return XDP_PASS;
}
Deployment & Verification Workflow#
Attaching and managing XDP programs on production network interfaces requires native tooling via iproute2 or specialized eBPF management daemons.
The unified CLI workflow below demonstrates compilation with clang, binding the compiled bytecode to a physical network interface in native driver mode, dynamically populating the IP blocklist map, and monitoring live packet drops via bpftool:
## 1. Compile eBPF source into ELF bytecode object
clang -O2 -g -target bpf -D__TARGET_ARCH_x86 -c xdp_filter.c -o xdp_filter.o
## 2. Attach program to network interface in native driver mode (eth0)
## Use 'xdpgeneric' if the interface driver does not support native XDP
ip link set dev eth0 xdpoff
ip link set dev eth0 xdpdrv obj xdp_filter.o sec xdp
## 3. Verify driver binding and obtain loaded program ID
ip link show dev eth0
bpftool prog show --type xdp
## 4. Dynamically block an attacking IPv4 address (198.51.100.44 = 0x2C6453C6)
## Format: bpftool map update id <MAP_ID> key <HEX_BYTES> value <HEX_BYTES>
MAP_ID=$(bpftool map show name blocked_ips | awk -F: '{print $1}')
bpftool map update id ${MAP_ID} key hex c6 53 64 2c value hex 01
## 5. Monitor real-time packet statistics and driver drop metrics
ethtool -S eth0 | grep -iE "rx_xdp_drop|rx_packets|rx_bytes"
To gracefully detach the XDP program and restore standard operating mode:
ip link set dev eth0 xdpoff
Engineering Trade-offs & Practical Takeaways#
Deploying packet filtering at driver line rate introduces specific operational considerations that network engineering teams must balance:
- Header Offset Fragility: XDP runs on raw bytes. Any variable-length options (such as IPv4 IHL fields or VLAN 802.1Q tags) require explicit boundary math. The eBPF verifier will reject any program lacking mathematical proof that pointer arithmetic cannot exceed
ctx->data_end. - Zero Host Conntrack Context: XDP operates prior to connection tracking. While ideal for stateless SYN flood drops, complex application-layer protocol inspection (e.g., HTTP header inspection, TLS SNI filtering) cannot occur cleanly at line rate in XDP and belongs in userspace reverse proxies.
- Driver Architecture Dependencies: True multi-gigabit line-rate performance requires a NIC driver with native XDP support (
xdpdrv). Running inxdpgenericmode provides rapid development testing but sacrifices the CPU saving gained from bypassingsk_buffallocations.
Authoritative References#
- Linux Kernel AF_XDP and Driver Architecture Documentation
- eBPF.io Project Documentation & Specification
- Cloudflare Research on XDP Architecture: High-Performance In-Kernel DDoS Mitigation
- IETF RFC 4987: TCP SYN Flooding Attacks and Common Mitigations
- Linux Programmer's Manual: ip-link(8) XDP Subsystem Configuration
Comments
Post a Comment