eBPF/XDP 实战:在网卡驱动层无锁清洗 UDP 洪水,把 DDoS 挡在门外

UDP 洪水攻击每秒能消耗数百 Gbps 带宽,传统 iptables 或用户态程序根本无法招架。本文用大白话拆解 eBPF 和 XDP 的工作原理,解释为什么它们能在网卡驱动层实现无锁化的超高并发清洗,把攻击流量扼杀在硬件入口,帮你的业务扛住最猛烈的 DDoS。

eBPF/XDP 实战:在网卡驱动层无锁清洗 UDP 洪水,把 DDoS 挡在门外
封面图:ZuCDN · ZuCDN 原创

eBPF XDP:你遇没遇到过这种情况?

配置前的检查

关于eBPF XDP,最值得先弄清楚的是配置边界和排错顺序。某个下午,网站突然卡成幻灯片,ping 延迟从 10ms 飙升到 5000ms,服务器 CPU 被软中断占满,连 SSH 都登不进去。一看流量监控——几百 Gbps 的 UDP 小包正像决堤的洪水一样涌向你的公网 IP。这就是经典的 UDP 洪水攻击,DDoS 中最常见也最粗暴的一种。

传统防御方案要么靠硬件防火墙(贵,且扩容慢),要么靠 iptables 规则(CPU 直接被打爆)。而今天我们要聊的 eBPF + XDP 组合,能把清洗逻辑直接塞进网卡驱动层,用无锁化的方式处理每个 UDP 包——速度堪比高速公路上的 ETC 通道,而不是收费站人工窗口。

先搞清楚三个核心概念与eBPF XDP

UDP 洪水到底在干嘛?

攻击者伪造大量源 IP,向你的服务器发送海量 UDP 包。这些包通常是小尺寸(比如 64 字节),目的是占满带宽、填满 NIC(网卡)的 RX 队列、耗尽 CPU 的中断资源。服务器收到包后,内核协议栈会逐层处理:从 NIC 驱动到 IP 层再到 UDP 层,最后发现没有对应端口监听,于是发送 ICMP 不可达——但这个过程本身已经消耗了大量 CPU 周期。攻击流量越大,系统越接近瘫痪。

XDP:把“安检”搬到机场跑道入口

XDP(eXpress Data Path,快速数据路径)是 Linux 内核提供的一种高性能网络数据包处理框架。传统的网络包从网卡到应用程序要经过:网卡 DMA → 驱动 → 内核协议栈 → socket → 用户态。XDP 在驱动层、甚至在网卡 DMA 之后、协议栈之前就插入了一个钩子(hook)。也就是说,包刚被网卡收上来,还在驱动程序的早期处理阶段,XDP 程序就能执行。

你可以把 XDP 想象成机场安检的预检通道:乘客还没进航站楼,在停机坪的廊桥出口就已经被分流了。合法的包(目的 IP 是你的服务、端口正常)放进行李检查(协议栈),攻击包直接在廊桥口就被保安(XDP 程序)拦下,连航站楼的大门都进不去。这样,CPU 不用为这些恶意包分配 sk_buff(内核包描述符)、不用走路由和 iptables 匹配,省下的资源能用来处理真正的业务。

eBPF:给网卡写“程序”的瑞士军刀

eBPF(extended Berkeley Packet Filter)是一种能在 Linux 内核中安全运行沙箱化程序的技术。以前你想在内核里做点定制逻辑(比如过滤 UPD 包),只能修改内核源码或加载内核模块——风险高、兼容性差。eBPF 允许你用 C 语言写一段程序,通过编译器生成字节码,然后动态加载到内核,经过验证器检查(确保无死循环、无越界访问)后,挂载到 XDP 或其他钩子点。

换句话说,eBPF 让你可以“编程”内核的网络路径,而不用重新编译内核。对于 DDoS 清洗,我们就在 XDP 钩子上挂载一个 eBPF 程序,专门分析 UDP 包、判断是正常流量还是攻击流量,然后做出 XDP_PASS(放行)、XDP_DROP(丢弃)或 XDP_TX(原路返回,常用于“反射”流量清洗)动作。

为什么要在“网卡驱动层”做清洗?

实际操作要点

很多人问:iptables 也能做 UDP 过滤,为什么非要用 XDP?因为 性能差距是百倍级的

  • iptables 工作在协议栈内部,包已经经历了 DMA、sk_buff 分配、IP 层路由决策之后才轮到 netfilter 钩子。整个过程 CPU 已经消耗了上百个指令周期。
  • XDP 工作在 NIC 驱动上下文,甚至在分配 sk_buff 之前。程序直接处理原始网络帧(Raw Frame),CPU 缓存命中率高,延迟极低。

在公有云场景下,虚拟化环境(如 AWS Nitro、阿里云 eRDMA)的弹性网卡通常支持 XDP 原语(通过 virtio 或硬件卸载)。一台主机的数十 Gbps 带宽可以被几十个 XDP 核心线性扩展,每个核心处理一个 RX 队列,且不需要在队列之间加锁,这就是“无锁化”的基础。

无锁化:让每个 CPU 独自干活,不抢玩具

配置前的检查

无锁化的核心思想是:不共享,就不需要锁。现代网卡支持多队列(RSS,Receive Side Scaling),每个 RX 队列绑定一个特定的 CPU 核心。每个核心独立处理自己队列中的包,互不干扰。XDP 程序只针对当前包做决策,不涉及跨核的一个全局状态(如果需要全局速率限制,可以用 per-CPU 计数器再周期汇总,但清洗逻辑本身无锁)。

对比传统做法:如果多个 CPU 共享一个过滤器规则表,更新规则时需要对整个表加锁,读操作也要等待。而 eBPF map(如哈希表、数组)虽然存在并发写,但通过 RCU(Read-Copy-Update) 机制实现无锁读。XDP 程序通常只从 map 中查表(比如匹配黑名单 IP 或白名单),写入黑名单由用户态程序通过 bpf_map_update_elem 触发,更新过程是安全的,不会影响正在执行的 XDP 程序(RCU 保证读取要么看到旧版本要么看到新版本)。

这种架构下,假设你有 32 核心,每个核心能处理 5 Mpps(百万包/秒),总吞吐就能达到 160 Mpps——足以扛住大部分 UDP 洪水攻击。

一个简化版的清洗流程

容易忽略的细节

假设你有一台运行 Linux 5.10+ 的公有云主机,网卡支持 XDP。你写一个 eBPF 程序挂载到网卡上:

  1. 包到达:网卡 DMA 到内存,CPU 0 的 RX 队列接收。
  2. XDP 程序触发:程序检查以太网头,确认是 IPv4 且协议为 UDP。
  3. 查表:从 eBPF map 中查询源 IP 是否在 临时封禁列表白名单 中。攻击者 IP 通常由控制平面(如流量分析引擎)实时计算并写入 map。
  4. 决策:如果是攻击 IP → 返回 XDP_DROP,直接丢弃,不给协议栈任何负担。如果是正常流量,且目的端口是你的 UDP 服务端口 → 返回 XDP_PASS,包正常进入协议栈。如果攻击者使用大量随机源 IP(实际常见的是 IP 欺骗),则可能启用 动态速率限制:通过 per-CPU 计数器统计单位时间内来自某个 IP 段或目的端口的包数量,超过阈值就丢弃后续包。
  5. 中断和 NAPI:XDP 程序丢弃后,网卡驱动依然会触发 NAPI 轮询,但丢弃的包不会分配 sk_buff,因此 CPU 消耗极低。正常包继续走 NAPI 流程。
  6. 整个过程没有锁竞争,没有内核额外开销,每个包的处理时间通常只需几十纳秒。相比 iptables 中每个包需要遍历规则链(可能数百条),XDP 的确定性延迟非常有吸引力。

    需要注意的坑和风险

    我的处理经验

    虽然 eBPF/XDP 很强大,但面向小白的你需要注意以下几点,避免生产环境“翻车”:

    • XDP 程序不能复杂:验证器规定 eBPF 程序必须有界循环,禁止调用某些内核函数。这意味着你不能在里面做复杂的模式匹配或字符串解析。通常只做 IP/端口/协议级别的检查。
    • 网卡兼容性:不是所有网卡都支持 XDP offload(硬件卸载)。在公有云上,大多数虚拟机使用的是 virtio 网卡,XDP 工作在驱动层(native mode),性能依然很好,但需要内核版本和驱动支持。建议先测试。
    • 回滚方案:加载 XDP 程序后,如果程序崩溃或误判,可以通过 ip link set dev eth0 xdp off 卸载。你可以准备一个“应急脚本”,一旦流量异常立即卸载,恢复到传统的 iptables 规则或直接启用云服务商提供的 DDoS 高防。
    • 控制平面与数据平面分离:XDP 程序本身只做快速查表和丢弃,攻击特征的更新(如动态黑名单、速率阈值)应该由用户态守护进程负责,它周期性分析日志或从云监控 API 获取攻击趋势,然后更新 eBPF map。

    总结:为什么值得学?

    我的处理经验

    对于运维和开发来说,eBPF/XDP 不再是“黑科技”,而是已经进入主流 Linux 内核(4.15+ 支持 XDP,5.3+ 功能更完善)。在公有云上,你可以把自己的 DDoS 清洗逻辑直接运行在网卡驱动层,实现毫秒级响应、线速处理。相比租用高防 IP(成本高、流量清洗中心可能成为瓶颈),eBPF/XDP 让你在服务器本地就获得接近硬件防火墙的性能。

    往大了说,这是网络可编程性的未来——不再依赖固定的内核网络栈,而是用 eBPF 编写自己的流量处理管线。下次再遇到 UDP 洪水,希望你脑子里第一个蹦出的不是 iptables,而是 XDP_DROP。真正做好eBPF XDP,靠的不是参数堆砌,而是持续验证。

    延伸阅读