XDP 与 eBPF 高性能网络实战:内核旁路丢包抗 DDoS、bpftool 观测与 AF_XDP 零拷贝全流程

当 iptables 扛不住的时候

个人站长平时用 iptables 或 nftables 挡 DDoS,几百兆的流量没问题。但一旦遇到几 Gbps 级的攻击,问题就来了:iptables 走的是内核 netfilter 框架,每个包都要经过 conntrack 连接跟踪、规则逐条匹配,在高包速率(PPS)下 CPU 会被软中断吃满,正常的业务流量也被拖垮。这时候你会看到 sar 里 %soft 飙到 90% 以上,而真正的用户请求全部超时。

XDP(eXpress Data Path)是绕过整个网络协议栈的方案:它在网卡驱动层、数据包刚进内存、还没分配 sk_buff 之前,就把包交给一段 eBPF 程序处理。这段程序可以返回四个动作——XDP_PASS(放行)、XDP_DROP(丢弃)、XDP_TX(原路发回)、XDP_REDIRECT(转发到别的网卡或 AF_XDP)。因为跳过了整个协议栈,单核就能处理上千万 PPS,这是它对抗大流量攻击的底气。本文讲清楚怎么用起来、怎么观测、以及什么时候不该用它。

前置条件与挂载方式

XDP 需要三样东西:内核 4.8+(生产建议 5.4 LTS 以上)、支持 XDP 的网卡驱动、以及 iproute2 与 bpftool。查看网卡是否支持原生 XDP:

ethtool -i eth0
ip -s link show eth0
# 尝试挂载 native 模式,成功即支持
ip link set dev eth0 xdp obj xdp_drop.o sec xdp

XDP 有三种挂载模式,选择直接决定性能:

  • native(xdpdrv)——驱动原生支持,程序在驱动收包路径中执行,性能最好。优先用这个。
  • offload(xdpoffload)——程序烧进网卡,网卡自己跑,CPU 完全不参与。只有部分 SmartNIC 支持,个人 VPS 基本没有。
  • generic(xdpgeneric)——内核通用路径模拟,作为兼容兜底。性能比 native 差很多,只适合开发调试,别在生产用。

挂载命令里明确指定模式,避免默认落到 generic:

ip link set dev eth0 xdpdrv obj xdp_drop.o sec xdp
ip link show dev eth0
# 输出中应出现 xdp id N 字样,表示已挂载

编译一个 XDP 丢包程序

XDP 程序用 C 写,通过 clang 编译成 eBPF 字节码。先装依赖(Debian 系):

apt install -y clang llvm libbpf-dev linux-headers-$(uname -r) bpftool

一个最简的"丢弃指定源 IP 所有包"的 XDP 程序:

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

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 1024);
    __type(key, __u32);
    __type(value, __u32);
} blocklist SEC(".maps");

SEC("xdp")
int xdp_drop_blocklist(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 != __constant_htons(ETH_P_IP))
        return XDP_PASS;

    struct iphdr *ip = (void *)(eth + 1);
    if ((void *)(ip + 1) > data_end)
        return XDP_PASS;

    __u32 src = ip->saddr;
    __u32 *hit = bpf_map_lookup_elem(&blocklist, &src);
    if (hit)
        return XDP_DROP;

    return XDP_PASS;
}

char LICENSE[] SEC("license") = "GPL";

编译成目标文件:

clang -O2 -g -target bpf -c xdp_drop.c -o xdp_drop.o
bpftool prog load xdp_drop.o /sys/fs/bpf/xdp_drop type xdp

程序里两个细节决定成败:第一,每次访问包内数据前必须做边界检查(if ((void *)(ip + 1) > data_end) return XDP_PASS;),因为 eBPF 校验器不允许越界访问,漏了检查会直接拒绝加载;第二,blocklist 用 eBPF map 而不是硬编码,这样运行期可以动态增删要封的 IP,不用重新编译加载。往 map 里加 IP 用 bpftool map update:

bpftool map update name blocklist key 0x1e1e1e1e 0 0 0 0 value 1 0 0 0
# 0x1e1e1e1e 是小端存储的 30.30.30.30

观测:bpftool 与 bpftrace

XDP 程序跑在内核里,看不见输出,调试靠工具。查已加载的程序和 map:

bpftool prog list
bpftool map list
bpftool net show dev eth0

给程序加统计计数,用 BPF_MAP_TYPE_PERCPU_ARRAY 累加丢包数,然后读出来。更轻量的观测方式是 bpftrace,不用改程序就能看内核事件。比如实时看谁在往你发 SYN:

bpftrace -e 'tracepoint:tcp:tcp_rcv_state_process { @[args->saddr] = count(); }'

或者直接用 bpftrace 观测网卡软中断的分布,判断 CPU 是否被单核打满:

bpftrace -e 'kprobe:netif_receive_skb { @[cpu] = count(); } interval:s:5 { print(@); clear(@); }'

如果所有包都堆在一个 CPU 上,说明网卡多队列或 RPS 没配好,这时候即便用了 XDP,单核也可能成为瓶颈——XDP 的优势建立在包能被分散到多个队列、多个 CPU 并行处理的基础上。

AF_XDP:零拷贝的用户态收包

XDP 家族里还有一个进阶玩法 AF_XDP。它不是一个动作,而是一个新的 socket 类型,允许用户态程序通过共享内存环(UMEM)直接从网卡取包,实现零拷贝。典型用途是自建高性能 DNS、负载均衡或者抓包分析。

它的价值在于:传统 recvfrom 要把包从内核拷贝到用户态,AF_XDP 则让用户态和内核共用一块内存缓冲区,包根本不拷贝。代价是编程复杂——要自己管理 UMEM 环形缓冲区、填充环(FILL ring)、接收环(RX ring)。对绝大多数个人站长场景,AF_XDP 属于"知道有这东西,但不必自己写"的阶段;真有需求可以直接用 xsk 或 libxdp 封装好的库。

为什么 XDP 单核能扛千万 PPS

要理解 XDP 的性能来源,得先明白传统路径的瓶颈在哪。一个包从网卡进来,内核要依次做:分配 sk_buff 结构体(几十上百字节的内存开销)、经过 netfilter 的各个 hook 点逐条匹配 iptables 规则、做 conntrack 连接跟踪、再交给 TCP/IP 协议栈。这些步骤在低流量时无感,但包速率一高,内存分配和规则匹配就变成主要开销——CPU 大部分时间花在"给包准备接收环境"上,而不是处理包本身。

XDP 的做法是把这些环境准备全部推迟。程序直接在 DMA 缓冲区上读包,用指针偏移解析以太头、IP 头,判断完就去查 eBPF map。整个过程没有内存分配、没有锁、没有协议栈调用,是一段可以在 CPU 流水线上高效执行的线性代码。因此同样一颗核心,iptables 可能只能处理几十万 PPS,而 XDP 能到上千万 PPS,这就是数量级的差距。

这里要强调一个常被混淆的概念:抗攻击看的是 PPS,不是带宽。很多小包攻击(比如每个包 64 字节的 SYN flood)带宽只有几百 Mbps,看起来不高,但 PPS 可以达到每秒数百万,恰好命中 iptables 的软肋。反过来说,如果是大包带宽攻击,走的是另一套处理路径,XDP 的优势就没那么绝对。所以判断"要不要上 XDP"时,第一件事是看攻击的包速率,而不是只盯着带宽计费单上的数字。理解了这一点,就不会出现"带宽没打满却卡死"的困惑——卡住的从来不是流量,而是包的数量。

XDP 与 iptables 的取舍

别把 XDP 当成 iptables 的全面替代,它们的定位完全不同。下表是实战中的选择依据:

  • 抗大流量攻击(>1 Gbps、PPS 极高)——用 XDP,在协议栈之前丢包,CPU 消耗极低。
  • 复杂的有状态规则、NAT、端口转发——继续用 iptables/nftables,XDP 不适合做复杂状态机。
  • 基于连接状态、日志审计的访问控制——用 iptables,XDP 看不到连接状态。
  • 高频次的规则变更——用 iptables 更简单;XDP 改规则要操作 map,链路更长。

我的实践是两者协同:XDP 放在最前面当"粗筛",iptables 放在后面做"细规则"。XDP 只负责丢弃黑名单 IP 和明显的攻击流量(比如 SYN flood),把绝大多数恶意包在协议栈之前就打掉;剩下的正常流量进入内核后,再用 iptables 做精细的端口和状态控制。

真实复盘:一次 XDP 上线后"连不上"的事故

说个必须讲的反面案例。某次为了抗攻击,我给生产网卡挂了一个 XDP_DROP_CIDR 程序,上线十分钟后服务器彻底失联,SSH、HTTP 全部不通,只能重启。事后复盘发现两个错误叠加。

第一个错误:程序里对 ethh->h_proto 的判断有笔误,导致非 IP 包的处理逻辑走了异常分支,把 ARP 应答也丢了,网关误以为机器下线。第二个更致命:没有留后门放行管理 IP。XDP 程序一旦挂上,连 iptables 都无法介入,因为包在更前面就被处理了。正确的做法是在程序开头加上 if (src == MY_ADMIN_IP) return XDP_PASS;,并且挂载时用 ip link set dev eth0 xdpgeneric 先小范围灰度,确认无误再切 native。

# 出问题时立即卸载 XDP 程序
ip link set dev eth0 xdp off

这次事故的核心教训:XDP 的包处理发生在协议栈之前,它既带来了性能,也带来了"一旦写错就整机失联"的风险。任何 XDP 上线,都必须准备一条不经网络的管理通道(VPS 控制台的 VNC/串口)作为保险,否则一个边界判断错误就能让你连不上去修。

总结

XDP 是应对大流量攻击和高 PPS 场景的一把利器,核心原理是在驱动层提前决策,用 XDP_PASS/DROP/TX/REDIRECT 四个动作处理包,靠 eBPF map 在运行期动态调整策略。落地要点:优先 native 模式、程序里做严格边界检查、用 bpftool/bpftrace 观测、给管理 IP 留后门、准备带外管理通道。它的定位是 iptables 之前的"粗粒度高速筛网",而非替代品。对个人站长来说,平时用不到,可一旦被几 Gbps 的攻击盯上,XDP 往往是那个能让机器在攻击中存活下来的方案——但前提是你得先在测试环境把程序写对,再小心地灰度上线。

Last modification:October 10th, 2026 at 12:27 pm

Leave a Comment