你的服务器正在被 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 网络收包路径中的位置。下面这张图展示了一个包从网卡到应用程序的完整旅程:
为什么”早”意味着”快”
从 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 并不是只有一种运行方式。根据执行位置的不同,分为三种模式:
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_TX 和
XDP_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 - eXpress Data Path (kernel.org)
- BPF and XDP Reference Guide (Cilium)
- xdp-tutorial (GitHub)
- Cloudflare: How to drop 10 million packets per second
总结
XDP 的核心价值很简单:在最早的时刻做出最简单的决定。它不是要取代 iptables——netfilter 有完整的连接追踪能力,XDP 做不到。XDP 解决的是一个特定问题:当你需要以极低开销处理海量包时,在协议栈之前做决定是唯一的出路。
如果你对 Linux 高性能网络栈感兴趣,建议把 eBPF:Linux 内核的隐藏武器 和 eBPF + io_uring:Linux 高性能网络栈的终极形态 也读一下。eBPF 是基础,XDP 是应用,io_uring 是另一个维度的加速——三者结合,才是现代 Linux 网络栈的全貌。