土法炼钢兴趣小组的算法知识备份

XDP:在网卡驱动层就把包丢掉

目录

你的服务器正在被 DDoS,每秒几百万个垃圾包砸过来。你加了 iptables 规则,CPU 占用却纹丝不动地钉在 100%。为什么?

因为 iptables 工作在 netfilter 框架里。每个包到达 netfilter 之前,内核已经做了一大堆事:分配 sk_buff、解析协议头、查路由表……你的 DROP 规则确实生效了,但内核在丢包之前已经把最贵的活都干完了。

这就像在餐厅门口立了块「满座」的牌子,但每个客人都已经坐下来点完了菜才被请走。

XDP(eXpress Data Path)的思路完全不同:在网卡驱动收到包的那一刻就做决定。不分配 sk_buff,不进协议栈,不查路由。垃圾包在最早的时刻被丢弃,CPU 开销降到最低。

如果你还没读过 eBPF 的基础知识,建议先看 eBPF:Linux 内核的隐藏武器。XDP 本质上就是 eBPF 程序挂在网卡收包路径上的一种用法。


一、XDP 在网络栈里的位置:比 netfilter 早多少

理解 XDP 的关键是理解它在 Linux 网络收包路径中的位置。下面这张图展示了一个包从网卡到应用程序的完整旅程:

XDP 在 Linux 网络栈中的位置

为什么”早”意味着”快”

从 NIC 中断到 netfilter,内核要做大量工作:

步骤 操作 开销
1 DMA 将包数据拷贝到 ring buffer 硬件完成
2 XDP 检查点 ~50-100 ns
3 分配 sk_buff 结构体 ~200-500 ns
4 解析 L2/L3/L4 头部 ~100-300 ns
5 tc 分类器 ~100-200 ns
6 netfilter 规则匹配 ~500-2000 ns
7 路由表查找 ~200-500 ns

XDP 在第 2 步就能做出丢包决定,而 iptables 的 DROP 在第 6 步才执行。面对每秒几百万个包的 DDoS 时,这些微秒是致命的。

方案 单核丢包速率 延迟
iptables DROP ~2-3 Mpps ~2-5 μs/包
XDP_DROP (native) ~24-26 Mpps ~50-100 ns/包
XDP_DROP (offload) 线速 (100Gbps+) 硬件延迟

10 倍的性能差距。这不是优化,这是架构级别的降维打击。


二、三种运行模式:native / offload / generic

XDP 并不是只有一种运行方式。根据执行位置的不同,分为三种模式:

XDP 三种运行模式对比

Native XDP(原生模式)

eBPF 程序在网卡驱动的 NAPI poll 函数里执行,在 sk_buff 分配之前。这是最常用也是性能最好的软件模式。

# 加载 XDP 程序到 eth0(native 模式是默认的)
ip link set dev eth0 xdp obj xdp_prog.o sec xdp

限制:需要网卡驱动显式支持 XDP。不是所有驱动都行。

Offload XDP(卸载模式)

eBPF 程序被编译成网卡固件能理解的指令,直接在 SmartNIC 硬件上执行。CPU 完全不参与包处理。

# 卸载到网卡硬件执行
ip link set dev eth0 xdpoffload obj xdp_prog.o sec xdp

限制:只有极少数 SmartNIC 支持(主要是 Netronome/Agilio 系列),而且 eBPF 功能子集更小——很多 helper 函数不能用。

Generic XDP(通用模式)

netif_receive_skb() 之后执行,此时 sk_buff 已经分配了。本质上就是个方便调试的模拟器,性能和 tc-bpf 差不多。

# 强制使用 generic 模式(任何网卡都支持)
ip link set dev eth0 xdpgeneric obj xdp_prog.o sec xdp

注意:generic 模式的性能和 native 差距巨大。它的意义在于:你的笔记本网卡不支持 native XDP,但你需要开发和调试。

三种模式性能对比

特性 Native Offload Generic
执行位置 网卡驱动 NAPI SmartNIC 硬件 netif_receive_skb 之后
sk_buff 分配 ❌ 未分配 ❌ 不需要 ✅ 已分配
CPU 占用 几乎为零 中等
单核吞吐 ~24 Mpps 线速 ~3-5 Mpps
驱动要求 需要支持 需要 SmartNIC 任何网卡
功能完整性 完整 受限子集 完整
适用场景 生产环境 超高性能场景 开发调试

哪些网卡驱动支持 Native XDP

截至 Linux 6.x 内核,主流支持列表:

驱动 网卡型号 备注
i40e Intel X710/XXV710 数据中心主流
ixgbe Intel 82599/X520 10G 经典款
ice Intel E810 100G 系列
mlx5 Mellanox ConnectX-5/6/7 支持最完善
nfp Netronome Agilio 唯一支持 offload
virtio_net 虚拟机 virtio 云环境常用
veth 虚拟以太网对 容器网络
ena AWS ENA EC2 实例
# 检查你的网卡是否支持 XDP
ethtool -i eth0 | grep driver
ip link show eth0  # 看 xdp 字段

三、XDP 的五种动作码

XDP 程序的返回值决定了包的命运。一共有五种动作码:

动作码 含义 典型场景
XDP_ABORTED 0 出错,丢弃并记录 trace 程序异常、调试
XDP_DROP 1 静默丢弃 DDoS 防护、黑名单
XDP_PASS 2 交给内核协议栈 正常流量放行
XDP_TX 3 从同一网卡发回去 SYN Cookie、反射
XDP_REDIRECT 4 转发到另一个网卡/CPU 负载均衡、AF_XDP
// XDP 程序的基本骨架
SEC("xdp")
int xdp_prog(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_ABORTED;  // 包太短,异常丢弃

    // 只处理 IPv4
    if (eth->h_proto != htons(ETH_P_IP))
        return XDP_PASS;  // 非 IPv4 直接放行

    // ... 你的过滤逻辑 ...

    return XDP_PASS;  // 默认放行
}

关键细节:每次访问包数据之前,都必须做 data_end 边界检查。这不是好习惯,而是硬性要求——eBPF 验证器会检查每一次内存访问是否越界。少一行检查,程序直接加载失败。


四、实战:写一个极简 XDP 防火墙

下面我们写一个真正能用的 XDP 防火墙:基于 IP 黑名单的包过滤器。用户态程序可以动态添加和删除黑名单 IP。

内核态 XDP 程序

// xdp_firewall.c -- XDP 黑名单防火墙
// 编译:clang -O2 -g -target bpf -c xdp_firewall.c -o xdp_firewall.o

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

// 黑名单 Map:key 是 IPv4 地址(u32),value 是丢包计数器(u64)
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 10240);
    __type(key, __u32);      // IPv4 源地址
    __type(value, __u64);    // 该 IP 被丢弃的包计数
} blacklist SEC(".maps");

SEC("xdp")
int xdp_firewall_prog(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_ABORTED;

    // 只处理 IPv4 流量,其他协议直接放行
    if (eth->h_proto != bpf_htons(ETH_P_IP))
        return XDP_PASS;

    // 第二步:解析 IP 头部
    struct iphdr *ip = (void *)(eth + 1);
    if ((void *)(ip + 1) > data_end)
        return XDP_ABORTED;

    // 第三步:查黑名单
    __u32 src_ip = ip->saddr;
    __u64 *blocked = bpf_map_lookup_elem(&blacklist, &src_ip);

    if (blocked) {
        (*blocked)++;          // 更新丢包计数
        return XDP_DROP;       // 直接丢,不进协议栈
    }

    return XDP_PASS;
}

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

用户态管理工具

// xdp_fw_manager.c -- 用户态黑名单管理(核心逻辑)
// 编译:gcc -o xdp_fw_manager xdp_fw_manager.c -lbpf
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <bpf/bpf.h>

// 通过 pinned map 路径获取 fd
// XDP 程序加载后,map 会被 pin 到 /sys/fs/bpf/ 下
static int get_map_fd(const char *name)
{
    char path[256];
    snprintf(path, sizeof(path), "/sys/fs/bpf/%s", name);
    return bpf_obj_get(path);
}

// 添加 IP 到黑名单
static int cmd_add(const char *ip_str)
{
    int fd = get_map_fd("blacklist");
    struct in_addr addr;
    inet_pton(AF_INET, ip_str, &addr);
    __u64 count = 0;
    bpf_map_update_elem(fd, &addr.s_addr, &count, BPF_ANY);
    printf("已添加 %s 到黑名单\n", ip_str);
    return 0;
}

// 遍历并打印所有黑名单条目
static int cmd_list(void)
{
    int fd = get_map_fd("blacklist");
    __u32 key = 0, next_key;
    __u64 value;
    char ip_str[INET_ADDRSTRLEN];
    printf("%-18s %s\n", "IP 地址", "丢包计数");
    while (bpf_map_get_next_key(fd, &key, &next_key) == 0) {
        bpf_map_lookup_elem(fd, &next_key, &value);
        inet_ntop(AF_INET, &next_key, ip_str, sizeof(ip_str));
        printf("%-18s %llu\n", ip_str, value);
        key = next_key;
    }
    return 0;
}

int main(int argc, char **argv)
{
    if (argc >= 3 && strcmp(argv[1], "add") == 0)
        return cmd_add(argv[2]);
    if (argc >= 3 && strcmp(argv[1], "del") == 0) {
        // 类似 add,调用 bpf_map_delete_elem
        int fd = get_map_fd("blacklist");
        struct in_addr addr;
        inet_pton(AF_INET, argv[2], &addr);
        bpf_map_delete_elem(fd, &addr.s_addr);
        printf("已移除 %s\n", argv[2]);
        return 0;
    }
    if (argc >= 2 && strcmp(argv[1], "list") == 0)
        return cmd_list();
    fprintf(stderr, "用法: %s add|del|list [IP]\n", argv[0]);
    return 1;
}

编译、加载、测试

# 1. 安装依赖
sudo apt install clang llvm libbpf-dev linux-headers-$(uname -r)

# 2. 编译 XDP 内核态程序
clang -O2 -g -target bpf -D__TARGET_ARCH_x86 \
    -I/usr/include/x86_64-linux-gnu \
    -c xdp_firewall.c -o xdp_firewall.o

# 3. 编译用户态管理工具
gcc -O2 -o xdp_fw_manager xdp_fw_manager.c -lbpf

# 4. 加载 XDP 程序并 pin maps(用户态工具通过 pin 路径访问 Map)
sudo bpftool prog load xdp_firewall.o /sys/fs/bpf/xdp_firewall \
    pinmaps /sys/fs/bpf/

# 5. 附加到网卡(native 模式)
sudo bpftool net attach xdp pinned /sys/fs/bpf/xdp_firewall dev eth0

# 6. 验证
sudo bpftool prog list | grep xdp
ip link show eth0  # 看 xdp 字段

# 7. 测试黑名单
sudo ./xdp_fw_manager add 192.168.1.100
sudo ./xdp_fw_manager list
# 从 192.168.1.100 发 ping——应该无回复;其他 IP 正常

# 8. 卸载
sudo bpftool net detach xdp dev eth0
sudo rm -rf /sys/fs/bpf/xdp_firewall /sys/fs/bpf/blacklist

五、XDP 进阶:负载均衡和包修改

XDP 不只能丢包。XDP_TXXDP_REDIRECT 让你可以修改包内容并转发,这是负载均衡和网关功能的基础。

XDP_TX:修改包头并从同一网卡发回

交换源/目的 MAC 地址后从同一网卡发回——SYN Cookie 和 DDoS 反射防护的基础:

// xdp_reflect.c -- 交换 MAC 地址后从同一网卡发回
SEC("xdp")
int xdp_reflect_prog(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_ABORTED;

    // 交换源和目的 MAC 地址
    __u8 tmp[ETH_ALEN];
    __builtin_memcpy(tmp, eth->h_dest, ETH_ALEN);
    __builtin_memcpy(eth->h_dest, eth->h_source, ETH_ALEN);
    __builtin_memcpy(eth->h_source, tmp, ETH_ALEN);

    return XDP_TX;  // 从同一个网卡发回
}

修改 IP 地址:简易 NAT

修改 IP 头部需要增量更新校验和:

SEC("xdp")
int xdp_nat_prog(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;

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

    // 如果目的 IP 是虚拟 IP 10.0.0.1,改成真实后端 10.0.0.100
    __u32 vip = bpf_htonl(0x0A000001);       // 10.0.0.1
    __u32 backend = bpf_htonl(0x0A000064);   // 10.0.0.100

    if (ip->daddr == vip) {
        // 增量更新校验和——只修改变化的字段,不用重算整个头
        __u32 old_addr = ip->daddr;
        ip->daddr = backend;
        __u32 sum = (~ip->check & 0xFFFF) +
                    (~old_addr & 0xFFFF) + (~(old_addr >> 16) & 0xFFFF) +
                    (backend & 0xFFFF) + ((backend >> 16) & 0xFFFF);
        sum = (sum & 0xFFFF) + (sum >> 16);
        ip->check = ~((sum & 0xFFFF) + (sum >> 16));

        // 修改目的 MAC(实际应从 Map 查,这里硬编码演示)
        __u8 backend_mac[] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55};
        __builtin_memcpy(eth->h_dest, backend_mac, ETH_ALEN);

        return XDP_TX;
    }

    return XDP_PASS;
}

IP/TCP 校验和增量更新:RFC 1624 算法

上面的 NAT 示例里用了一段手写的增量校验和更新,但那段代码其实有 bug 风险。校验和增量更新是 XDP 编程里最容易出错的地方,值得展开讲清楚。

RFC 1624 给出的增量更新公式:

HC' = ~(~HC + ~m + m')

其中:
  HC  = 旧校验和
  ~HC = HC 的反码
  m   = 被修改前的字段值
  m'  = 修改后的字段值
  所有运算都是 16 位反码加法(进位回卷)

核心思想:校验和是所有 16-bit word 的反码求和的反码。你只需要减掉旧值、加上新值,不用重算整个头部。

正确实现(处理了 32 位地址拆分为两个 16-bit word 和进位回卷):

// 正确的增量校验和更新——修改 IPv4 地址时使用
// 适用于 IP 头部校验和和 TCP/UDP 校验和
static __always_inline void update_csum_l3(struct iphdr *ip,
                                            __be32 old_addr,
                                            __be32 new_addr)
{
    __u32 sum;
    // RFC 1624: HC' = ~(~HC + ~m + m')
    // 把 32 位地址拆成两个 16 位 word 处理
    sum  = (~ntohs(ip->check)) & 0xFFFF;
    sum += (~(old_addr & 0xFFFF)) & 0xFFFF;       // 减去旧值低 16 位
    sum += (~((old_addr >> 16) & 0xFFFF)) & 0xFFFF; // 减去旧值高 16 位
    sum += (new_addr & 0xFFFF);                     // 加上新值低 16 位
    sum += ((new_addr >> 16) & 0xFFFF);             // 加上新值高 16 位

    // 进位回卷:把超过 16 位的高位加回来,可能需要两次
    sum = (sum & 0xFFFF) + (sum >> 16);
    sum = (sum & 0xFFFF) + (sum >> 16);

    ip->check = htons(~sum & 0xFFFF);
}

// TCP/UDP 校验和也需要更新(它们包含伪头部中的 IP 地址)
static __always_inline void update_csum_l4(struct tcphdr *tcp,
                                            __be32 old_addr,
                                            __be32 new_addr)
{
    __u32 sum;
    sum  = (~ntohs(tcp->check)) & 0xFFFF;
    sum += (~(old_addr & 0xFFFF)) & 0xFFFF;
    sum += (~((old_addr >> 16) & 0xFFFF)) & 0xFFFF;
    sum += (new_addr & 0xFFFF);
    sum += ((new_addr >> 16) & 0xFFFF);
    sum = (sum & 0xFFFF) + (sum >> 16);
    sum = (sum & 0xFFFF) + (sum >> 16);
    tcp->check = htons(~sum & 0xFFFF);
}

常见 buggy 版本(只做了一次进位回卷,大多数时候能工作,偶尔算出错误校验和):

// ❌ 错误:只做了一次进位回卷
// 当中间结果的高位部分再次产生进位时,校验和会错
static __always_inline void update_csum_BUGGY(struct iphdr *ip,
                                               __be32 old_addr,
                                               __be32 new_addr)
{
    __u32 sum;
    sum = (~ip->check & 0xFFFF) +
          (~old_addr & 0xFFFF) + (~(old_addr >> 16) & 0xFFFF) +
          (new_addr & 0xFFFF) + ((new_addr >> 16) & 0xFFFF);
    // BUG: 只回卷一次——如果 (sum & 0xFFFF) + (sum >> 16) > 0xFFFF,
    //      结果会多出 1,导致校验和错误
    //      概率不高(大约 1/65536),但流量大了一定会遇到
    ip->check = ~((sum & 0xFFFF) + (sum >> 16));
}

关键区别:两次回卷 vs 一次回卷。一次回卷在极端情况下会产生 17 位结果,导致校验和多 1。这种 bug 极难复现——几百万个包里才出现一次校验和错误,对端会静默丢弃,你看到的只是”偶尔丢包”。

实际使用中,推荐直接调用 eBPF 内置的 bpf_csum_diff() helper,让内核帮你算:

// 最安全的方式:用 bpf_csum_diff helper
__be32 old_addr = ip->daddr;
ip->daddr = new_addr;
// bpf_csum_diff 计算旧值和新值的校验和差异
ip->check = bpf_csum_diff(&old_addr, 4, &new_addr, 4, ~ip->check);

XDP_REDIRECT 跨多网卡转发与 Per-CPU Map

在多网卡负载均衡场景下(比如一台服务器有 4 个 10G 网卡做入站,需要按规则分发到不同网卡),XDP_REDIRECT 配合 BPF_MAP_TYPE_DEVMAP_HASH 和 Per-CPU 处理是高性能的关键。

核心思路:用 bpf_redirect_map() 把包从当前网卡转发到目标网卡,同时利用 Per-CPU Map 避免多核竞争。

// devmap_redirect.c -- 跨多网卡 XDP 转发示例
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

// 设备映射:支持按 ifindex 哈希查找,比 DEVMAP 更灵活
struct {
    __uint(type, BPF_MAP_TYPE_DEVMAP_HASH);
    __uint(max_entries, 64);
    __type(key, __u32);    // 目标 ifindex
    __type(value, struct bpf_devmap_val);
} tx_port_map SEC(".maps");

// Per-CPU 统计:每个 CPU 独立计数,无锁
struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __uint(max_entries, 64);
    __type(key, __u32);
    __type(value, __u64);
} redirect_stats SEC(".maps");

// 路由表:源子网 → 目标 ifindex
struct {
    __uint(type, BPF_MAP_TYPE_LPM_TRIE);
    __uint(max_entries, 1024);
    __type(key, struct lpm_key);
    __type(value, __u32);
    __uint(map_flags, BPF_F_NO_PREALLOC);
} route_table SEC(".maps");

struct lpm_key {
    __u32 prefixlen;
    __be32 addr;
};

SEC("xdp")
int xdp_multi_nic_redirect(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_ABORTED;
    if (eth->h_proto != bpf_htons(ETH_P_IP))
        return XDP_PASS;

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

    // 根据目标 IP 查路由表,决定从哪个网卡发出
    struct lpm_key lpm = { .prefixlen = 32, .addr = ip->daddr };
    __u32 *target_ifindex = bpf_map_lookup_elem(&route_table, &lpm);
    if (!target_ifindex)
        return XDP_PASS;  // 无匹配路由,交给内核协议栈

    // Per-CPU 统计更新(无锁,每个 CPU 核写自己的计数器)
    __u32 idx = *target_ifindex;
    __u64 *cnt = bpf_map_lookup_elem(&redirect_stats, &idx);
    if (cnt)
        (*cnt)++;

    // bpf_redirect_map:把包转发到目标网卡
    // 最后一个参数是 fallback action(Map 查找失败时的动作)
    return bpf_redirect_map(&tx_port_map, *target_ifindex, XDP_PASS);
}

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

用户态配置 CPU 亲和性,确保每个网卡的中断绑定到不同的 CPU 核心:

# 1. 填充设备 Map(用户态)
# 假设 eth1=ifindex 3, eth2=ifindex 4, eth3=ifindex 5
bpftool map update pinned /sys/fs/bpf/tx_port_map \
    key 3 0 0 0 value 3 0 0 0 0 0 0 0
bpftool map update pinned /sys/fs/bpf/tx_port_map \
    key 4 0 0 0 value 4 0 0 0 0 0 0 0

# 2. 绑定网卡中断到特定 CPU(避免跨 NUMA 转发)
# 查看网卡中断号
grep eth0 /proc/interrupts
# 假设中断号是 36-43(8 个队列)
# 绑定到 CPU 0-7
for i in $(seq 0 7); do
    echo $i > /proc/irq/$((36+i))/smp_affinity_list
done

# 3. 设置 XDP 程序的 CPU 亲和
# 确保 XDP 处理和中断在同一 CPU 上(避免跨核数据传输)
ethtool -L eth0 combined 8   # 8 个收发队列
ethtool -N eth0 rx-flow-hash udp4 sdfn  # 按四元组哈希分流

关键性能要点:XDP_REDIRECT 的转发路径是 per-CPU 的——包从哪个 CPU 收到,就在哪个 CPU 上做转发决策和 Map 查找。如果目标网卡的 TX 队列在另一个 CPU 上,需要跨核 IPI(Inter-Processor Interrupt),性能会下降 30-50%。所以 CPU 亲和配置和 NUMA 拓扑对齐是 XDP 多网卡转发性能的关键

XDP_REDIRECT:跨网卡转发

XDP_TX 只能从收到包的同一网卡发出。跨网卡转发需要 XDP_REDIRECT 配合 BPF_MAP_TYPE_DEVMAP

// 设备映射表:ifindex -> 转发目标
struct {
    __uint(type, BPF_MAP_TYPE_DEVMAP);
    __uint(max_entries, 64);
    __type(key, __u32);
    __type(value, __u32);
} tx_port SEC(".maps");

SEC("xdp")
int xdp_redirect_prog(struct xdp_md *ctx)
{
    // 从 eth0 收到的包转发到 eth1 (ifindex=3)
    __u32 dest_ifindex = 3;
    return bpf_redirect_map(&tx_port, dest_ifindex, 0);
}

这就是 Cilium 等 CNI 插件实现容器网络的核心机制之一。更多关于 eBPF 在高性能网络中的应用,可以看 eBPF + io_uring:Linux 高性能网络栈的终极形态


六、XDP 的限制和踩坑

XDP 很快,但不是万能的。以下是你必须知道的限制:

1. 不能处理分片包

XDP 只能看到单个包的内容。如果 TCP 段被 IP 分片,XDP 可能看到没有 TCP 头的分片——你无法在 XDP 里重组分片。

// IP 分片检查——frag_off 低 13 位是偏移,高位是 MF 标志
if (ip->frag_off & bpf_htons(IP_MF | IP_OFFSET)) {
    // 分片包,偏移 > 0 时可能没有 TCP/UDP 头
    return XDP_PASS;  // 保守做法:放行让内核处理
}

2. 无法访问 TCP 连接状态

XDP 工作在协议栈之下,没有 socket 概念。你不能做「只允许已建立连接」这种有状态防火墙规则。想做有状态过滤?要么在 XDP Map 里自己维护连接跟踪表(复杂且受限),要么用 tc-bpf 或 netfilter conntrack(牺牲一部分性能换取完整状态)。

3. MTU 限制

XDP_TX 不自动分片。如果你通过 bpf_xdp_adjust_head 添加封装头(如 VXLAN)导致包超过 MTU,网卡会静默丢弃。没有错误提示,没有 ICMP 回复。你需要自己检查包长度。

4. 调试困难

调试手段极其有限。XDP 运行在内核态,唯一的输出是 bpf_printk()

bpf_printk("src_ip: %x, action: DROP\n", src_ip);
# 读取 trace 输出
sudo cat /sys/kernel/debug/tracing/trace_pipe

警告bpf_printk 有严重性能开销,生产环境中必须删除

5. 验证器限制和单程序限制

eBPF 验证器对 XDP 同样严格:最大 100 万条指令、禁止无限循环、栈空间仅 512 字节。此外,每个网卡只能挂一个 XDP 程序。需要组合多个功能时,用 tail call 链式调用:

// 用 tail call 实现多个 XDP 程序的链式调用
struct {
    __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
    __uint(max_entries, 8);
    __type(key, __u32);
    __type(value, __u32);
} jmp_table SEC(".maps");

SEC("xdp")
int xdp_entry(struct xdp_md *ctx)
{
    // 先执行防火墙逻辑
    // ...

    // 然后跳到下一个程序(限速器)
    bpf_tail_call(ctx, &jmp_table, 1);

    // 如果 tail call 失败(目标程序未加载),继续执行
    return XDP_PASS;
}

参考资料


总结

XDP 的核心价值很简单:在最早的时刻做出最简单的决定。它不是要取代 iptables——netfilter 有完整的连接追踪能力,XDP 做不到。XDP 解决的是一个特定问题:当你需要以极低开销处理海量包时,在协议栈之前做决定是唯一的出路。

如果你对 Linux 高性能网络栈感兴趣,建议把 eBPF:Linux 内核的隐藏武器eBPF + io_uring:Linux 高性能网络栈的终极形态 也读一下。eBPF 是基础,XDP 是应用,io_uring 是另一个维度的加速——三者结合,才是现代 Linux 网络栈的全貌。


By .