eBPF/XDP 在网卡驱动层实现百万 PPS UDP DDoS 清洗

面对百万 PPS 的 UDP DDoS 攻击,传统内核协议栈处理能力有限。eBPF/XDP 在网卡驱动层直接丢包,可大幅提升清洗效率。本文从实操角度讲解如何编写、加载和调优 XDP 程序,并讨论与云防护结合时的取舍与不确定性。

eBPF/XDP 在网卡驱动层实现百万 PPS UDP DDoS 清洗
封面图:ZuCDN · ZuCDN 原创

当 UDP 洪水以百万 PPS 打过来时,很多团队的直觉是加高防 IP 或扩容。但如果你自己管理服务器,或者流量已进入机房,eBPF/XDP 在网卡驱动层直接丢包,往往是最快、最省 CPU 的清洗手段。这篇文章直接回答:XDP 能不能扛住百万 PPS?怎么判断该用 XDP 而不是 iptables?以及落地时你会踩哪些坑。

先确认瓶颈:为什么内核协议栈扛不住百万 PPS

一台普通 x86 服务器,处理 100 万 PPS 的 UDP 小包,内核收包路径(中断、软中断、协议栈处理)通常会成为瓶颈。即使 CPU 多,锁竞争和内存分配也会拖慢处理。而 XDP 在网卡驱动收到报文后、分配 skb 之前执行,丢包几乎不消耗协议栈资源。官方文档指出,AWS Shield 在网络和传输层(L3/L4)自动缓解攻击,但那是云边界的能力;在自建场景,XDP 就是你的本地防线。

环境要求:不是所有网卡都支持 XDP

XDP 依赖网卡驱动和内核支持。首先,内核版本建议 4.18+,且开启 CONFIG_BPFCONFIG_XDP。其次,网卡驱动必须支持 XDP 的 native 模式(如 i40e、mlx5、ixgbe 等)。你可以用 ethtool -L eth0 combined 4 开启多队列,但要注意:如果驱动不支持 native XDP,内核会回退到 generic 模式,性能大打折扣。判断方法:加载程序后执行 ip link show,如果显示 prog/xdp 且无 generic 标记,说明是 native。

编写最小 XDP 程序:只丢 UDP 洪水包

目标是只丢弃目标端口为 53(DNS)或 123(NTP)的 UDP 包,因为这些端口常被放大攻击利用。用 C 写一个 XDP 程序,核心逻辑:解析以太网帧,判断 IP 协议类型为 UDP,再检查目标端口,匹配则返回 XDP_DROP,否则 XDP_PASS。注意:XDP 程序运行在网卡驱动上下文,不能调用普通内核函数,只能用 BPF 辅助函数,且必须通过 verifier 检查。

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>

int udp_drop(struct xdp_md *ctx) {
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;
    struct ethhdr *eth = data;
    if ((void *)eth + sizeof(*eth) > data_end) return XDP_PASS;
    if (eth->h_proto != htons(ETH_P_IP)) return XDP_PASS;
    struct iphdr *ip = (void *)(eth + 1);
    if ((void *)ip + sizeof(*ip) > data_end) return XDP_PASS;
    if (ip->protocol != IPPROTO_UDP) return XDP_PASS;
    struct udphdr *udp = (void *)(ip + 1);
    if ((void *)udp + sizeof(*udp) > data_end) return XDP_PASS;
    if (ntohs(udp->dest) == 53 || ntohs(udp->dest) == 123) return XDP_DROP;
    return XDP_PASS;
}

编译与加载:用 clang 和 ip 命令

编译需要 clang 和 llvm,目标为 BPF 字节码:

clang -O2 -target bpf -c udp_drop.c -o udp_drop.o

然后用 ip 命令加载到网卡:

ip link set dev eth0 xdp obj udp_drop.o sec .text

如果加载失败,先检查内核日志 dmesg | tail,常见错误是 verifier 拒绝,比如越界访问。验证是否生效:

ip link show eth0

输出中应包含 prog/xdp 及程序 ID。卸载则用 ip link set dev eth0 xdp off

压测与调优:如何逼近百万 PPS

pktgenhping3 构造 UDP 洪水,观察 CPU 和丢包率。如果达不到百万 PPS,优先检查:

  • 是否启用了多队列和 RSS?ethtool -l eth0 查看队列数,必要时开启 numa_balancingirqbalance
  • 是否开启 busy poll?可以降低延迟但增加 CPU 占用。
  • XDP 程序是否过于复杂?每增加一条指令都会影响性能,尽量精简。
  • 尝试 XDP_TXXDP_REDIRECT 组合,但纯丢包场景 XDP_DROP 最快。

注意:单核处理能力有限,要利用多核,需要将 XDP 程序加载到所有队列,并确保流量分散到各队列。

与云防护的取舍:XDP 不能替代所有层

AWS 文档指出,Shield 自动缓解 L3/L4 攻击,但对于 L7 攻击,默认不自动缓解,以免误伤合法流量。这意味着,云边界防护和本机 XDP 各有分工。XDP 适合处理已知端口的洪水,但无法识别应用层攻击(如 Slowloris)。如果你在 AWS 上,可以结合 Shield Advanced 和 VPC 网络 ACL,但要注意:网络 ACL 是 stateless 的,配置不当会阻断合法流量。XDP 则更灵活,可以基于源 IP 或端口精确丢包,但需要自行维护规则。

常见误区与失败条件

  • 误以为 XDP 能处理所有 DDoS:XDP 只工作在网卡层,对于已进入协议栈的连接型攻击(如 SYN Flood)效果有限,需要结合 conntrack 或 SYN Proxy。
  • 忽略合法性检查:如果误丢正常 UDP 流量(如 QUIC 或游戏),会导致业务受损。务必先抓包分析攻击特征。
  • 加载 generic XDP 而不自知:某些云环境(如虚拟机)不支持 native XDP,性能大幅下降,应改用其他方案。
  • 忘记持久化:XDP 程序在重启后丢失,需要借助 systemd 或 bpftool 固化。

总结与行动清单

eBPF/XDP 能在网卡驱动层实现百万 PPS 级 UDP 丢包,但前提是硬件支持、程序精简、多队列调优。在云环境,先确认是否支持 native XDP,否则考虑云厂商的 DDoS 防护服务。以下步骤供你落地:

  1. 确认内核和网卡支持 native XDP。
  2. 编写最小丢包程序,只匹配明确的攻击特征。
  3. ip link 加载并验证。
  4. 用真实流量压测,观察 CPU 和丢包率。
  5. 结合云防护(如 AWS Shield)或高防 CDN 形成纵深防御。

参考资料

延伸阅读