Linux XDP + eBPF:零拷贝数据包处理与 DDoS 防护实战
一、为什么需要 XDP
传统 Linux 网络栈在处理高流量时存在根本性瓶颈:数据包必须经过网卡驱动 → 内核 sk_buff 分配 → 协议栈逐层分发 → 用户态 socket read。这条路径在 10Gbps+ 场景下,即使零拷贝优化,每个包仍需数微秒的内核处理时间。
XDP (eXPress Data Path) 通过在网络驱动层挂载 eBPF 程序,实现了网卡收到数据包后、尚未分配 sk_buff 之前就进行处理的能力。这使得:
- 包过滤在最早时机完成,无需分配 sk_buff 内存
- 包重定向(XDP_REDIRECT)到不同 CPU 或网卡,实现线速转发
- 包丢弃(XDP_DROP)在驱动层直接丢弃恶意流量
性能对比(单机单核,64B UDP 小包):
| 方案 | 吞吐量 (Mpps) | 延迟 (μs) |
|---|---|---|
| iptables DROP | ~0.5 | - |
| DPDK (内核旁路) | ~100+ | <1 |
| XDP_DROP | ~24 | <4 |
XDP 以接近 DPDK 的性能,保留了与 Linux 内核协议栈的完整集成。
二、XDP 程序架构
2.1 加载模式
XDP 有三种加载方式:
- Native XDP:驱动原生支持,延迟最低(最佳)
- Offloaded XDP:程序加载到网卡硬件(SmartNIC)
- Generic XDP:在 sk_buff 层级兜底运行,用于不支持原生 XDP 的场景
主流驱动(i40、ixgbe、mlx5、virtio_net、ena)均已支持 Native XDP。
2.2 程序生命周期
网卡收包 → 驱动收包Poll → BPF JIT执行XDP程序 → 返回动作码
↓
┌───────────────────┼───────────────────┐
↓ ↓ ↓
XDP_PASS XDP_DROP XDP_REDIRECT
(交给协议栈) (驱动层丢弃) (转发到目标)
↓ ↓
XDP_TX (回发到入网卡)
2.3 BPF 辅助函数
XDP 程序可用的核心 helper 函数:
// 访问数据包字节
void *xdp_md_data = (void *)(long)md->data;
void *xdp_md_end = (void *)(long)md->data_end;
// 重写数据包并校验边界
bpf_xdp_adjust_head(ctx, -delta); // 扩展头部空间(用于封装)
bpf_xdp_adjust_tail(ctx, delta); // 调整尾部
// 重定向到目标CPU或网卡
bpf_redirect_map(&cpu_map, cpu, XDP_DROP);
// 查找映射表(防火墙/路由)
bpf_map_lookup_elem(&rules_map, &key);
2.4 返回码语义
| 返回码 | 含义 | 性能 |
|---|---|---|
XDP_ABORTED |
程序异常,丢包 | — |
XDP_DROP |
驱动层直接丢弃 | 最快 |
XDP_PASS |
交出给内核协议栈 | 标准路径 |
XDP_TX |
从入网卡发送出去 | 高 |
XDP_REDIRECT |
转发到指定 CPU/网卡 | 高 |
三、从 BPF C 到 eBPF 字节码
3.1 最小 XDP 程序
// xdp_drop.c
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/in.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
SEC("xdp")
int xdp_drop_all(struct xdp_md *ctx)
{
return XDP_DROP;
}
3.2 自定义编译(不用 BCC/libbpf)
# 编译为 BPF 目标文件
clang -O2 -g -target bpf -c xdp_drop.c -o xdp_drop.o
# 验证器会自动拒绝越界访问:XDP 程序必须检查每个指针访问
# 违例示例(会被拒绝):
# void *data = (void *)(long)ctx->data;
# char *p = data;
# for (int i = 0; i < 100; i++) { // 死循环被拒绝
# sum += p[i]; // 未校验边界被拒绝
# }
正确写法:
SEC("xdp")
int xdp_drop_ip(struct xdp_md *ctx)
{
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
// 边界检查 — 验证器强制要求
if ((void *)(eth + 1) > data_end)
return XDP_PASS;
if (bpf_ntohs(eth->h_proto) != ETH_P_IP)
return XDP_PASS;
struct iphdr *iph = data + sizeof(*eth);
if ((void *)(iph + 1) > data_end)
return XDP_PASS;
// 丢弃所有 TCP SYN 包(示例)
if (iph->protocol == IPPROTO_TCP) {
struct tcphdr *tcph = (void *)iph + sizeof(*iph);
if ((void *)(tcph + 1) > data_end)
return XDP_PASS;
if (tcph->syn)
return XDP_DROP;
}
return XDP_PASS;
}
四、实战:L3/L4 动态防火墙
4.1 Go + CGO 管理型 xdpfw(libbpfgo)
// main.go 片段
package main
// #cgo LDFLAGS: -lbpf
// #include "xdpfw_kern.h"
import "C"
import (
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/rlimit"
"net"
"log"
"os"
"os/signal"
)
func main() {
// 提升资源限制
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatal(err)
}
// 加载编译好的 eBPF 对象
spec, err := ebpf.LoadCollectionSpec("xdpfw_kern.o")
if err != nil {
log.Fatal(err)
}
var obj struct {
Prog *ebpf.Program `ebpf:"xdp_firewall"`
SrcMap *ebpf.Map `ebpf:"src_rules"`
PortMap *ebpf.Map `ebpf:"port_rules"`
}
// 反序列化到 Go 结构
// 加载到网卡
link, err := link.AttachXDP(link.XDPOptions{
Program: obj.Prog,
Interface: 2, // eth0 ifindex
})
// 动态下发规则
go func() {
for {
select {
case rule := <-ruleChan:
obj.SrcMap.Put(rule.IP, rule.Action)
}
}
}()
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt)
<-sig
link.Close()
}
4.2 内核侧 BPF C 主机防火墙
// xdpfw_kern.c
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/udp.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
// 规则动作:0=DROP, 1=PASS, 2=REDIRECT
struct bpf_map_def SEC("maps") src_rules = {
.type = BPF_MAP_TYPE_LPM_TRIE,
.key_size = sizeof(struct bpf_lpm_trie_key) + 4,
.value_size = sizeof(__u8),
.max_entries = 65535,
};
struct bpf_map_def SEC("maps") port_rules = {
.type = BPF_MAP_TYPE_HASH,
.key_size = sizeof(__u16), // port
.value_size = sizeof(__u8), // action
.max_entries = 1024,
};
// 统计MAP
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__type(key, __u32);
__type(value, __u64);
__uint(max_entries, 5);
} stats SEC(".maps");
SEC("xdp")
int xdp_firewall(struct xdp_md *ctx)
{
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return update_stats(ctx, XDP_DROP);
if (eth->h_proto != bpf_htons(ETH_P_IP))
return update_stats(ctx, XDP_PASS);
struct iphdr *iph = data + sizeof(*eth);
if ((void *)(iph + 1) > data_end)
return update_stats(ctx, XDP_DROP);
// 1. 检查源 IP 黑/白名单(LPM 最长前缀匹配)
struct bpf_lpm_trie_key key = {
.prefixlen = 32,
.data = {iph->s_addr & 0xFF, (iph->s_addr >> 8) & 0xFF,
(iph->s_addr >> 16) & 0xFF, (iph->s_addr >> 24) & 0xFF},
};
__u8 *action = bpf_map_lookup_elem(&src_rules, &key);
if (action && *action == 0)
return update_stats(ctx, XDP_DROP);
// 2. 传输层端口规则
__u16 dst_port = 0;
if (iph->protocol == IPPROTO_TCP) {
struct tcphdr *tcph = (void *)iph + (iph->ihl * 4);
if ((void *)(tcph + 1) > data_end)
return update_stats(ctx, XDP_DROP);
dst_port = bpf_ntohs(tcph->dest);
} else if (iph->protocol == IPPROTO_UDP) {
struct udphdr *udph = (void *)iph + (iph->ihl * 4);
if ((void *)(udph + 1) > data_end)
return update_stats(ctx, XDP_DROP);
dst_port = bpf_ntohs(udph->dest);
}
if (dst_port) {
__u8 *port_act = bpf_map_lookup_elem(&port_rules, &dst_port);
if (port_act && *port_act == 0)
return update_stats(ctx, XDP_DROP);
}
return update_stats(ctx, XDP_PASS);
}
五、实战:SYN Flood DDoS 防护
5.1 SYN Cookie vs XDP 专用防护
传统 SYN Cookie 由内核协议栈处理,CPU 消耗高。XDP SYN Cookie 在驱动层完成:
// syn_protect_kern.c
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
struct bpf_map_def SEC("maps") syn_count = {
.type = BPF_MAP_TYPE_LRU_PERCPU_HASH,
.key_size = sizeof(__u32), // 源IP
.value_size = sizeof(__u64), // 计数值
.max_entries = 100000,
};
#define SYN_RATE_LIMIT 100
SEC("xdp")
int syn_protect(struct xdp_md *ctx)
{
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
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 *iph = data + sizeof(*eth);
if ((void *)(iph + 1) > data_end)
return XDP_PASS;
if (iph->protocol != IPPROTO_TCP)
return XDP_PASS;
struct tcphdr *tcph = (void *)iph + (iph->ihl * 4);
if ((void *)(tcph + 1) > data_end)
return XDP_PASS;
// 仅关注 SYN 包(非 SYN-ACK)
if (!tcph->syn || tcph->ack)
return XDP_PASS;
__u32 src_ip = iph->s_addr;
__u64 *count = bpf_map_lookup_elem(&syn_count, &src_ip);
__u64 new_count = 1;
if (count) {
new_count = *count + 1;
if (new_count >= SYN_RATE_LIMIT)
return XDP_DROP;
}
bpf_map_update_elem(&syn_count, &src_ip, &new_count, BPF_ANY);
return XDP_PASS;
}
5.2 结合 BPF 环形缓冲区上报攻击事件
struct bpf_map_def SEC("maps") events = {
.type = BPF_MAP_TYPE_RINGBUF,
.max_entries = 256 * 1024,
};
struct event {
__u32 src_ip;
__u64 count;
__u8 action;
};
SEC("xdp")
int syn_protect_advanced(struct xdp_md *ctx)
{
// ... 限流逻辑 ...
if (new_count >= SYN_RATE_LIMIT) {
struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (e) {
e->src_ip = src_ip;
e->count = new_count;
e->action = 1;
bpf_ringbuf_submit(e, 0);
}
return XDP_DROP;
}
return XDP_PASS;
}
六、XDP_REDIRECT 与 AF_XDP 联合架构
6.1 XDP_REDIRECT 到 AF_XDP
当 XDP 程序需要放行复杂包到用户态深度检测时,可用 XDP_REDIRECT 转发到 AF_XDP socket,绕过整个内核协议栈:
struct bpf_map_def SEC("maps") xsks_map = {
.type = BPF_MAP_TYPE_XSKMAP,
.key_size = sizeof(__u32),
.value_size = sizeof(__u32),
.max_entries = 64,
};
SEC("xdp")
int xdp_redirect_dns(struct xdp_md *ctx)
{
if (dst_port == 53) {
return bpf_redirect_map(&xsks_map, ctx->rx_queue_index, XDP_DROP);
}
return XDP_PASS;
}
6.2 AF_XDP 用户态零拷贝收包
// af_xdp_user.c
#include <xdp/xsk.h>
#include <linux/if_link.h>
#include <linux/if_ether.h>
#include <net/if.h>
#define NUM_FRAMES 4096
#define FRAME_SIZE XSK_UMEM__DEFAULT_FRAME_SIZE
#define RX_BATCH 32
int main(int argc, char **argv)
{
// 1. 分配大页 buf
void *buf_mem = mmap(NULL, NUM_FRAMES * FRAME_SIZE,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
-1, 0);
// 2. 注册 UMEM
struct xsk_umem_config umem_cfg = {
.fill_size = NUM_FRAMES / 2,
.comp_size = NUM_FRAMES / 2,
.frame_size = FRAME_SIZE,
.frame_headroom = XSK_UMEM__DEFAULT_FRAME_HEADROOM,
};
struct xsk_umem *umem;
xsk_umem__create(&umem, buf_mem, NUM_FRAMES * FRAME_SIZE,
&fq, &cq, &umem_cfg);
// 3. 创建 Socket 绑定到网卡队列
struct xsk_socket_config xsk_cfg = {
.rx_size = XSK_RING_CONS__DEFAULT_NUM_DESCS,
.tx_size = XSK_RING_PROD__DEFAULT_NUM_DESCS,
.libxdp_flags = XSK_LIBXDP_FLAGS__INHIBIT_PROG_LOAD,
.xdp_flags = XDP_FLAGS_DRV_MODE,
.bind_flags = XDP_USE_NEED_WAKEUP,
};
struct xsk_socket *xsk;
xsk_socket__create(&xsk, "eth0", 0, umem, &rx_ring, &tx_ring, &xsk_cfg);
// 4. RX 循环
while (1) {
// 从 fill ring 补充分配
stock = xsk_prod_nb_free(&fq, 32);
if (stock > 0) {
xsk_ring_prod__reserve(&fq, stock, &idx);
for (int i = 0; i < stock; i++)
*xsk_ring_prod__fill_addr(&fq, idx++) = alloc_frame();
xsk_ring_prod__submit(&fq, stock);
}
// 收包
rcvd = xsk_ring_cons__peek(&rx_ring, RX_BATCH, &idx);
if (!rcvd) {
if (xsk_ring_prod__needs_wakeup(&fq))
poll(&fds, 1, -1);
continue;
}
for (int i = 0; i < rcvd; i++) {
struct xdp_desc *desc = xsk_ring_cons__rx_desc(&rx_ring, idx++);
void *pkt = xsk_umem__get_data(buf_mem, desc->addr);
process_packet(pkt, desc->len);
}
xsk_ring_cons__release(&rx_ring, rcvd);
}
}
七、生产环境优化与监控
7.1 多队列散列与 RSS
struct bpf_map_def SEC("maps") cpu_map = {
.type = BPF_MAP_TYPE_CPUMAP,
.key_size = sizeof(__u32),
.value_size = sizeof(struct bpf_cpumap_val),
.max_entries = 128,
};
SEC("xdp")
int xdp_load_balance(struct xdp_md *ctx)
{
__u32 cpu = bpf_get_smp_processor_id() % num_possible_cpus();
return bpf_redirect_map(&cpu_map, cpu, XDP_PASS);
}
7.2 BPF 程序调试与统计
# 查看程序加载状态
ip link show eth0 | grep xdp
bpftool net show
# 查看 map 内容
bpftool map dump id <map_id>
# 实时跟踪(debugfs)
cat /sys/kernel/debug/tracing/trace_pipe
7.3 核心性能指标(通过 PERCPU_ARRAY 收集每核统计)
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__type(key, __u32);
__type(value, struct stats);
__uint(max_entries, 1);
} xdp_stats SEC(".maps");
struct stats {
__u64 packets;
__u64 bytes;
__u64 dropped;
};
SEC("xdp")
int xdp_counter(struct xdp_md *ctx)
{
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
__u32 key = 0;
struct stats *s = bpf_map_lookup_elem(&xdp_stats, &key);
if (s) {
s->packets++;
s->bytes += (data_end - data);
}
return XDP_PASS;
}
八、性能调优实践
8.1 尾调用拆分大程序
struct bpf_map_def SEC("maps") progs = {
.type = BPF_MAP_TYPE_PROG_ARRAY,
.key_size = sizeof(__u32),
.value_size = sizeof(__u32),
.max_entries = 32,
};
SEC("xdp")
int xdp_entry(struct xdp_md *ctx)
{
__u32 key = PROTO_TCP;
bpf_tail_call(ctx, &progs, key);
return XDP_PASS; // tail call 失败兜底
}
8.2 大流量场景 Hugepage 与 NUMA 调优
| 调优项 | 建议 |
|---|---|
| AF_XDP UMEM | 必须使用 2MB 大页(MAP_HUGETLB) |
| 网卡 RSS | 队列数 = vCPU 数,每队列独立 RX ring |
| BPF map 内存 | max_entries * value_size 可达数百 MB,调大 RLIMIT_MEMLOCK |
| SO_REUSEPORT | 多进程绑定同一端口,内核自动散列 |
8.3 DDoS 防护性能对比
实际生产环境(双路 EPYC 7742,Mellanox ConnectX-6 100Gbps):
| 攻击类型 | 流量规模 | XDP_DROP 性能 | iptables DROP |
|---|---|---|---|
| SYN Flood | 40Mpps | 24Mpps / 单核 | 0.5Mpps |
| UDP Flood | 35Mpps | 28Mpps / 单核 | 0.3Mpps |
| DNS Amplification | 12Mpps | 全速处理 | CPU 打满 |
九、与 DPDK 的取舍
XDP 不是 DPDK 的替代品,而是不同的架构选择:
| 维度 | XDP/eBPF | DPDK |
|---|---|---|
| 包过滤性能 | ~24Mpps/核 | ~100Mpps/核 |
| 吞吐量处理 | 中等 | 极高 |
| 内核集成 | 完整协议栈可用 | 完全旁路 |
| 动态更新 | bpftool reload 毫秒级 | 通常需重启 |
| 低延迟场景 | < 4μs | < 1μs |
| 适用场景 | 防火墙/负载均衡/安全 | 高频交易/运营商BRAS |
最佳实践:两者可以共存。XDP 在入口做 DDoS 清洗,正常流量 XDP_PASS 交给内核或 DPDK 做深度处理。
十、总结与展望
XDP + eBPF 提供了一个高性能、可编程、与内核深度集成的数据面解决方案。它的核心价值在于:
- 编程灵活性:相比固定功能 SmartNIC,XDP 可动态加载/卸载逻辑,无需重启
- 性能足够:24Mpps 单机处理能力已能抵御绝大多数 DDoS 攻击
- 生态集成:与 tc、cgroup、iptables 等无缝协作,形成完整安全策略链
随着 Linux 6.x 内核持续增强 BPF 调度能力和网卡硬件卸载(如 NVIDIA BlueField DPU 的 XDP offload),XDP 正在成为云原生网络栈的标准组件。
# 快速体验三步
clang -O2 -g -Wall -target bpf -c xdp_drop.c -o xdp_drop.o
ip link set dev eth0 xdp obj xdp_drop.o sec xdp
ip link set dev eth0 xdp off
网络层的未来已来——现在就是 XDP 的时代。
关键词:XDP, eBPF, DDoS防护, 零拷贝, AF_XDP, 网络栈, Linux内核 标签:XDP, eBPF, Linux, 内核, 网络安全

发表评论 取消回复