eBPF/XDP 在网卡驱动层实现百万 PPS UDP DDoS 清洗:小白也能看懂的底层原理

当你的服务器遭遇百万 PPS 的 UDP Flood 攻击,传统 iptables 或内核协议栈会因资源耗尽而瘫痪。eBPF 和 XDP 技术允许在网卡驱动接收数据包的瞬间执行过滤,跳过协议栈、减少中断,从而实现线速清洗。本文用大白话拆解这些底层概念,告诉你为什么它们能扛住百万级攻击。

eBPF/XDP 在网卡驱动层实现百万 PPS UDP DDoS 清洗:小白也能看懂的底层原理
封面图:ZuCDN · ZuCDN 原创

UDP Flood 有多可怕?

容易忽略的细节

如果你正在处理eBPF XDP,先别急着照搬网上的参数。想象一下,你的服务器突然收到海量 UDP 小包——每秒超过一百万。每个包到达网卡后,都会触发一次中断,通知 CPU 处理。CPU 被迫从当前任务中切换出来,执行驱动接收程序,分配 sk_buff 结构体,再层层上送到内核协议栈,直到发现是 UDP 包,最后检查用户态程序发现没有对应 socket,才丢弃。这个过程在每秒百万次攻击下,CPU 几乎所有时间都花在了“拆包-检查-丢弃”上,真正要处理的合法请求反而被饿死。这就是 UDP Flood 的威力:它不靠大流量填满带宽,而是靠海量小包耗尽服务器的计算资源。

传统清洗为什么效率低?

故障定位思路

常见的 DDoS 清洗方案包括:硬件防火墙(价格昂贵,部署僵化)、云清洗服务(依赖回注线路,有延迟)、软件方案如 iptables 或 nftables(基于 Netfilter,处于内核协议栈内部)。iptables 的 DROP 规则虽然高效,但依然要经过完整的内核网络栈:中断处理 → 驱动收包 → 分配 sk_buff → IP 层检查 → 传输层检查 → Netfilter hook → 匹配规则 → 丢弃。这意味着每个包都要经历 多次内存分配、上下文切换、元数据复制。当 PPS 达到百万级别时,光是内存分配就足以拖垮系统。

eBPF XDP:认识两个关键工具:eBPF 和 XDP

eBPF(extended Berkeley Packet Filter)

eBPF 是 Linux 内核的一种“虚拟机”,允许你安全地运行沙盒程序,而不用修改内核源代码或加载有风险的模块。这些程序可以挂载到各种内核事件上,比如网络数据包到达、系统调用、跟踪点等。eBPF 被严格校验:循环次数有限、不允许无限递归、访问内存必须合法,因此即使写错了也不会导致内核崩溃。你可以把它想象成在内核里开了一个“安全脚本”通道,用来做网络过滤、性能分析和安全监控。

XDP(eXpress Data Path)

XDP 是 eBPF 的一种特殊应用,它把 eBPF 程序挂载到 网卡驱动的最早入口——甚至早于中断处理本身(通过 NAPI 轮询)。当网卡收到一个数据包,驱动通过 NAPI 循环批量读取包,在分配 sk_buff(内核网络缓冲区)之前,就把原始包缓冲区(xdp_buff)交给 eBPF 程序处理。eBPF 程序可以返回几个动作:XDP_PASS(继续正常协议栈)、XDP_DROP(直接丢弃)、XDP_REDIRECT(重定向到另一个网卡或用户态程序)、XDP_TX(原路回弹)。对于攻击流量,返回 XDP_DROP 即可。

为什么能在驱动层实现百万 PPS?——eBPF XDP

配置前的检查

核心秘密在于:绕过内核协议栈

  • 零中断开销:XDP 通常基于 NAPI 轮询模式,网卡一次性报告大量包,CPU 批量处理,避免了每个包的中断→切换成本。
  • 无 sk_buff 分配:标准网络路径中每个包都需要一个 sk_buff 结构体(Linux 处理网络数据的核心数据结构,包含头部指针、元数据等),这会触发 slab 分配器和内存操作。XDP 使用原始的 xdp_buff(通常基于页面直接映射),不需要额外内存分配。
  • 提前过滤:eBPF 程序运行在驱动层,匹配 UDP 源端口、目标端口、IP 地址等特征后直接丢弃,根本不进入协议栈。整个周期只涉及一次 CPU 指令计算和一次页面释放(或直接回收到页池),延迟从微秒级降到纳秒级。
  • 硬件助力:现代网卡支持 RSS(接收侧缩放)和 XDP 卸载,eBPF 程序可以被 JIT 编译成本地机器码,甚至在某些网卡上直接放到硬件数据路径中执行(如 Netronome SmartNIC)。

简单算一笔账:单个 CPU 核处理 XDP 丢弃的性能通常在 10-30 Mpps(百万包/秒) 之间(取决于规则复杂度和 CPU 频率)。使用多队列 RSS 分散到多个核,轻松达到 100 Mpps 以上。而同样的包走 iptables DROP,单核极限只有 1-2 Mpps。差距在 10 倍以上。

如何编写一个 UDP 清洗的 eBPF/XDP 程序?

故障定位思路

虽然本文不要求你写代码,但了解流程有助于理解原理。典型步骤:

  1. 定义黑名单规则:例如允许的源 IP 列表,或攻击特征(固定 UDP 端口 53/1900、特定包长、特定载荷模式)。
  2. 编写 C 语言 eBPF 程序:包含 SEC("xdp") 入口函数,读取 struct xdp_md *ctx 获取包指针,解析以太网头、IP 头、UDP 头,检查字段,返回 XDP_DROPXDP_PASS
  3. 编译为 BPF 字节码:使用 LLVM/clang 加上 -target bpf 目标。生成 .o 文件。
  4. 加载到网卡:通过 ip link set dev eth0 xdp obj xdp_drop.o sec xdp 或使用 bpftool 工具。
  5. 验证与监控:通过 bpftool prog show 查看加载情况,通过 ethtool -S eth0 或自定义 eBPF map 统计丢弃包数。

注意:加载 XDP 程序会暂时影响网络,建议先在测试环境验证。如果网卡驱动不支持 XDP,会有报错提示。支持 XDP 的常见驱动有 ixgbe(Intel 10G)、i40e(Intel 40G)、mlx4/mlx5(Mellanox ConnectX-4/5/6)、nfp(Netronome)等。

部署挑战和局限性

故障定位思路

虽然 XDP 很强大,但并不是银弹。

  • 复杂过滤逻辑:eBPF 指令数有限制(老版本 4096 条,新版本允许尾调用突破),并且不允许循环(除非有边界限制)。如果要解析深度嵌套的隧道协议(如 GRE、VXLAN),指令可能不够用。
  • 分片重组:XDP 工作在 IP 层以下,无法自动处理 IP 分片。UDP 可能分片,你需要自己维护分片表,但 eBPF map 有限制,复杂度高。
  • 动态规则更新:通过 eBPF map 可以实现运行时更新黑名单(如从用户态写入 map),但需要程序支持 map 查找。这比静态编译复杂一些。
  • 内核版本:XDP 要求 Linux 内核 4.8+,某些特性需要更高版本。eBPF 的发展很快,建议使用 5.10 以上的 LTS 内核。
  • 硬件支持:虚拟网卡(如 virtio)可能不支持 XDP,云环境中的弹性网卡需要确认驱动兼容性。

总结:从“被动挨打”到“第一道防线”

我的处理经验

传统 DDOS 清洗依赖高层软件,要么延迟高,要么性能低。eBPF/XDP 把清洗方阵推进到网卡驱动层,使得每个 CPU 核都能在数纳秒内丢弃攻击包,并且不干扰正常包路径。如果你正在运营面对公众的服务,尤其容易受到 UDP Flood 攻击的场景(如 DNS 递归解析器、NTP 服务器、游戏对战服务器),为网卡挂载一份简单的 XDP 丢弃程序,可以瞬间减轻 80% 以上的压力。当然,更复杂的攻击(如混合型、应用层攻击)需要配合其他机制,但 XDP 提供的早期过滤能力是性价比最高的第一道防线。

记住:当你听到“百万 PPS”时,不必再紧张。只要你的网卡支持 XDP,几行 eBPF 代码就能让你的 Linux 服务器变成一台微型清洗机。把这些步骤跑通后,eBPF XDP基本就能稳定落地。

延伸阅读