XDP(Express Data Path)是Linux内核在4.8版本引入的一个高性能数据包处理框架,它允许eBPF程序直接挂在网卡驱动收包路径上执行。与传统的netfilter/iptables不同,XDP程序运行在sk_buff分配之前,也就是说数据包刚从网卡DMA到内存、还没进入协议栈时,XDP就已经有机会对它做出裁决了。这个特性使得XDP成为对抗DDoS攻击的利器:攻击流量在消耗系统资源之前就被丢弃,防御成本极低。业界知名的开源项目Cloudflare的DDoS防护体系,其核心就是基于XDP构建的,据他们公开的测试数据,单核CPU每秒可以处理超过2000万个数据包的丢弃操作,这是iptables完全无法企及的数字。

XDP的工作原理与性能优势
要理解XDP为什么这么快,需要先看清楚Linux网络收包的完整路径。传统路径下,网卡收到数据包后产生硬中断,中断处理程序把包放入-ring buffer,随后软中断NAPI轮询取出数据包,分配sk_buff结构体,再逐层经过IP层、netfilter钩子、TCP层处理。整个流程中,光是sk_buff的分配和协议栈的遍历就要消耗大量CPU周期,而攻击流量恰恰会把这个路径打满。
XDP把处理点提前到了-ring buffer被驱动读取的那一刻。驱动在收到包后立即调用do_xdp入口,此时eBPF虚拟机会执行你挂载的程序,程序返回一个动作码,驱动根据动作码决定包的命运。整个过程不涉及sk_buff的分配,eBPF程序又是经过JIT编译成本机机器码执行的,开销接近一条函数调用。XDP支持五种返回动作:XDP_PASS放行进入协议栈,XDP_DROP直接丢弃,XDP_TX从同一网卡反弹回去,XDP_REDIRECT转发到其他网卡或CPU,XDP_ABORTED表示程序异常。防御DDoS主要就是用XDP_DROP。
更进一步的优化是XDP卸载模式。支持的网卡(比如部分Mellanox和Intel型号)可以把eBPF程序直接编译进网卡固件执行,此时连CPU都不需要参与,丢包能力完全由网卡硬件吞吐决定。另外还有AF_XDP模式,可以把数据包零拷贝送到用户态处理,适合需要复杂业务逻辑的场景。
编写第一个XDP防御程序
开发XDP程序推荐使用BPF CO-RE(Compile Once, Run Everywhere)方式,通过libbpf库编写。下面实现一个典型的DDoS过滤程序,包含三个功能:IP黑名单过滤、基于端口的UDP Flood防护、SYN Flood的限速检测。先看代码骨架。
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
// IP黑名单映射表
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 10000);
__type(key, __u32);
__type(value, __u8);
} blacklist SEC(".maps");
// 端口限速统计表
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, __u64);
} rate_stats SEC(".maps");
SEC("xdp")
int xdp_ddos_defense(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)
return XDP_PASS;
if (eth->h_proto != bpf_htons(ETH_P_IP))
return XDP_PASS; // 只处理IPv4
// 解析IP头
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return XDP_PASS;
// 黑名单检查
__u32 src_ip = ip->saddr;
__u8 *blocked = bpf_map_lookup_elem(&blacklist, &src_ip);
if (blocked)
return XDP_DROP;
// UDP Flood防护:丢弃发往非常用UDP端口的包
if (ip->protocol == IPPROTO_UDP) {
struct udphdr *udp = (void *)ip + ip->ihl * 4;
if ((void *)(udp + 1) > data_end)
return XDP_PASS;
__u16 dport = bpf_ntohs(udp->dest);
if (dport != 53 && dport != 123 && dport > 1024)
return XDP_DROP;
}
// TCP SYN包速率检查可在此扩展
return XDP_PASS;
}
char LICENSE[] SEC("license") = "GPL";这段代码有几个值得注意的细节。第一,所有指针访问前都必须做边界检查,即(void *)(ptr + 1) > data_end这种写法,否则验证器会拒绝加载程序,这是eBPF开发中最常见的报错原因。第二,黑名单使用BPF_MAP_TYPE_LRU_HASH类型,避免攻击者用海量伪造IP把表撑爆。第三,UDP端口判断用了非常简单的白名单逻辑,实际生产中建议结合连接跟踪思路做得更精细。
加载程序需要一个用户态的控制程序,借助libbpf完成加载、挂载和map更新。核心逻辑如下。
#include <bpf/libbpf.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
struct bpf_object *obj;
struct bpf_program *prog;
int prog_fd, black_fd;
obj = bpf_object__open_file("xdp_ddos_defense.o", NULL);
bpf_object__load(obj);
prog = bpf_object__find_program_by_name(obj, "xdp_ddos_defense");
prog_fd = bpf_program__fd(prog);
// 挂载到网卡eth0,使用generic模式保证兼容性
if (bpf_xdp_attach(if_nametoindex("eth0"), prog_fd,
XDP_FLAGS_SKB_MODE, NULL)) {
perror("attach failed");
return 1;
}
// 动态添加黑名单IP
black_fd = bpf_object__find_map_fd_by_name(obj, "blacklist");
__u32 evil_ip = inet_addr("203.0.113.66");
__u8 one = 1;
bpf_map_update_elem(black_fd, &evil_ip, &one, BPF_ANY);
printf("XDP program loaded\n");
return 0;
}挂载模式有三种选择:native模式性能最好但要求驱动支持;generic模式(SKB)兼容所有网卡但性能打折,适合测试;hardware模式需要网卡固件支持。生产环境优先native,确认网卡支持后不要用generic。
SYN Flood防护与限速的实现思路
上面的例子只做了静态过滤,而真实DDoS防御的难点在于动态识别。以SYN Flood为例,攻击者发送大量伪造源IP的SYN包,把目标服务器的半连接队列占满。XDP层面的应对思路是维护一张每源IP的SYN速率表,超阈值就临时拉黑。由于eBPF程序的执行时间受验证器限制,不能做复杂计算,所以一般用BPF_MAP_TYPE_LRU_HASH记录每个源IP的包计数和时间戳,用简单的令牌桶或滑动窗口算法判断。
还有一个更优雅的方案是无状态防御,思路来自Cloudflare的实践:让XDP程序检查TCP包的合法性,对首次SYN直接回一个SYN-ACK挑战,客户端收到后如果回ACK才允许建立连接。真实客户端会完成三次握手,而伪造源IP的攻击包不会回应,从而被天然过滤。这个方案完全在XDP层闭环,不需要维护任何状态表,抗攻击能力极强,代价是需要自己构造TCP包通过XDP_TX发回去,实现复杂度较高。
对于限速表的更新,需要注意per-CPU变量和原子操作的使用。多核场景下如果多个CPU同时修改同一个map条目,要用__sync_fetch_and_add原子指令,或者干脆用BPF_MAP_TYPE_PERCPU_HASH让每个CPU维护独立计数,读取时再汇总,后者性能更好。
生产环境部署与监控
把XDP用到线上,监控是不可缺少的一环。libbpf提供了BPF_MAP_TYPE_PERCPU_ARRAY统计各动作的计数,也可以利用bpftrace或bpftool prog tracelog观察程序运行状态。一个实用技巧是在map里维护丢弃计数,每秒从用户态读取一次并写入Prometheus,这样防御效果可以量化展示。判断XDP是否生效,最直观的方法是攻击期间观察mpstat输出:普通情况下软中断会吃满CPU,挂载XDP后si列会显著下降。
规则动态更新依赖BPF map的可变性。用户态控制进程可以随时通过bpf_map_update_elem增删黑名单条目,与控制平面联动。比如把WAF或日志分析系统识别出的恶意IP实时推送给XDP map,就形成了一套检测加拦截的闭环。另外,XDP程序本身也可以热替换,用bpf_xdp_attach加载新版本程序即可实现无缝升级,不需要断流量。
最后提醒几个坑。内核配置需要开启CONFIG_BPF_SYSCALL和CONFIG_XDP_SOCKETS;虚拟机环境下virtio网卡早期版本不支持native XDP,需要升级或退回generic模式;eBPF程序的栈空间只有512字节,不要在程序里放大数组;写map时注意并发问题。只要避开这些坑,XDP配合合理的过滤策略,完全可以让你用一台普通服务器扛住中等规模的DDoS攻击,把安全防护的成本压到最低。