XDP与eBPF:Linux网络性能优化实战指南
XDP与eBPF:Linux网络性能优化实战指南
1.XDP与eBPF:Linux网络性能优化实战指南
掌握 XDP(eXpress Data Path)和 eBPF(extended Berkeley Packet Filter)的使用与开发,是深入 Linux 网络、安全、可观测性等领域的重要技能。以下是系统性的学习路径和关键知识点,帮助你从入门到进阶:
一、基础知识准备
1. Linux 内核基础
- 理解网络栈(如 netdev、sk_buff、NAPI)
- 系统调用、内核模块机制(虽然 eBPF 不需要写内核模块)
- 用户态与内核态通信机制(如 BPF map、perf event)
2. C 语言编程
- eBPF 程序通常用 C 编写(受限子集)
- 熟悉指针、结构体、位操作等
3. 网络协议基础
- 以太网帧、IP、TCP/UDP 头部结构
- 抓包分析(tcpdump、Wireshark)
二、eBPF 核心概念
1. eBPF 是什么?
- 一种在内核中安全运行沙箱程序的技术
- 无需修改内核源码或加载内核模块
- 支持多种 hook 点:网络(XDP、TC)、跟踪(kprobe/uprobe)、安全(LSM)等
2. 关键组件
- eBPF 程序:运行在内核中的字节码(由 clang/LLVM 编译)
- BPF Map:用户态与内核态共享数据的高效机制(hash、array、ringbuf 等)
- Verifier:确保 eBPF 程序安全(无死循环、内存越界等)
- Helper 函数:内核提供的 API(如 bpf_map_lookup_elem)
3. 开发工具链
- clang + LLVM:编译 eBPF C 代码为 BPF 字节码
- libbpf:现代推荐的用户态库(替代 BCC)
- bpftool:调试、加载、查看 eBPF 程序和 map
- BCC(BPF Compiler Collection):适合快速原型(Python/C++ 前端),但生产环境推荐 libbpf + CO-RE
三、XDP 专项
1. XDP 是什么?
- eBPF 的一个高性能网络处理 hook,位于网络驱动层(早于内核网络栈)
- 支持三种模式:
- native XDP(驱动支持,性能最高)
- skb mode(通用,通过 sk_buff)
- generic XDP(用于测试,走标准网络栈)
2. XDP 返回值
#define XDP_ABORTED 0
#define XDP_DROP 1
#define XDP_PASS 2
#define XDP_TX 3 // 重发回网卡
#define XDP_REDIRECT 4 // 重定向到其他设备或 CPU
3. 典型应用场景
- DDoS 防御(丢弃恶意包)
- 负载均衡(L4/L3)
- 流量采样与监控
- 快速 NAT 或 ACL
四、开发实践步骤
步骤 1:环境搭建
- Linux 内核 ≥ 5.8(推荐 ≥ 6.1)
- 安装 clang、llvm、libbpf-dev、bpftool
- 启用 CONFIG_BPF、CONFIG_XDP_SOCKETS 等内核选项(大多数发行版默认开启)
步骤 2:编写第一个 XDP 程序(丢包)
// xdp_drop.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
SEC("xdp")
int xdp_drop_func(struct xdp_md *ctx) {
return XDP_DROP;
}
char _license[] SEC("license") = "GPL";
步骤 3:编译(使用 libbpf + Makefile 或 bpftool)
clang -O2 -target bpf -c xdp_drop.c -o xdp_drop.o
步骤 4:加载到网卡
sudo ip link set dev eth0 xdp obj xdp_drop.o sec xdp
步骤 5:验证与卸载
bpftool net show
sudo ip link set dev eth0 xdp off
五、进阶方向
1. 使用 BPF Map 实现统计
- 统计每秒丢包数
- 使用 per-CPU array 或 hash map 存储状态
2. 用户态交互
- 用 libbpf 编写用户态程序读取 map 数据
- 使用 ring buffer 高效上报事件
3. CO-RE(Compile Once – Run Everywhere)
- 解决内核版本兼容问题
- 使用
bpf_core_read()替代直接指针访问
4. 性能调优
- 避免复杂逻辑(verifier 限制)
- 减少 map 查找次数
- 利用 per-CPU 结构避免锁竞争
六、推荐学习资源
官方文档
书籍
- 《BPF Performance Tools》by Brendan Gregg
- 《Learning eBPF》by Liz Rice
工具项目
- libbpf-bootstrap:快速启动模板(GitHub: libbpf/libbpf-bootstrap)
- xdp-tutorial:官方 XDP 教程(GitHub: xdp-project/xdp-tutorial)
- Cilium:生产级 eBPF 网络方案(含大量 XDP/eBPF 实践)
七、实战建议
- 从简单开始:先写一个统计 TCP 包数量的 XDP 程序
- 结合 tcpdump/Wireshark 验证行为
- 使用 bpftool prog dump 查看加载的字节码
- 阅读内核源码 中的 samples/bpf/
- 参与社区:eBPF Summit、xdp-project Slack
如果你有具体目标(如“实现一个 L4 负载均衡器”或“用 XDP 做 SYN flood 防护”),我可以提供针对性代码示例和架构建议。欢迎继续提问!
2.先写一个统计 TCP 包数量的 XDP 程序
好的!下面是一个完整的 统计 TCP 包数量的 XDP 程序,包含:
- 内核态 eBPF/XDP 程序(C 语言)
- 用户态加载与读取程序(使用
libbpf) - 编译与运行说明
该程序会在 XDP 层解析以太网帧 → IP 头 → 判断是否为 TCP,如果是,则在 BPF map 中计数,并将包 XDP_PASS 给内核继续处理。
一、目录结构(推荐)
tcp_counter/
├── src/
│ ├── tcp_counter_kern.c # 内核态 XDP 程序
│ └── tcp_counter_user.c # 用户态加载/读取程序
├── Makefile
└── bpftool-obj/ # 编译输出(可选)
二、内核态程序:src/tcp_counter_kern.c
// src/tcp_counter_kern.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
// 定义一个 BPF_MAP_TYPE_ARRAY 类型的 map,用于计数
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, __u64);
} tcp_count SEC(".maps");
SEC("xdp")
int xdp_tcp_counter(struct xdp_md *ctx) {
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if (data + sizeof(*eth) > data_end)
return XDP_PASS;
// 只处理 IPv4
if (eth->h_proto != __constant_htons(ETH_P_IP))
return XDP_PASS;
struct iphdr *ip = (struct iphdr *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return XDP_PASS;
// 检查是否为 TCP
if (ip->protocol != IPPROTO_TCP)
return XDP_PASS;
// 增加计数
__u32 key = 0;
__u64 *count = bpf_map_lookup_elem(&tcp_count, &key);
if (count) {
__sync_fetch_and_add(count, 1); // 原子加(多 CPU 安全)
}
return XDP_PASS; // 让包继续进入内核网络栈
}
char _license[] SEC("license") = "GPL";
✅ 注意:
- 使用
__sync_fetch_and_add是因为BPF_MAP_TYPE_ARRAY不自动原子操作。- 如果追求更高性能且允许多核计数分离,可用
BPF_MAP_TYPE_PERCPU_ARRAY。
三、用户态程序:src/tcp_counter_user.c
// src/tcp_counter_user.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#include "tcp_counter_kern.skel.h" // 自动生成的 skeleton
static volatile bool exiting = false;
void sig_handler(int sig) {
exiting = true;
}
int main(int argc, char **argv) {
struct tcp_counter_kern *skel;
int err;
if (argc != 2) {
fprintf(stderr, "Usage: %s <ifname>\n", argv[0]);
return 1;
}
signal(SIGINT, sig_handler);
signal(SIGTERM, sig_handler);
// 打开并加载 BPF 程序
skel = tcp_counter_kern__open_and_load();
if (!skel) {
fprintf(stderr, "Failed to open and load BPF skeleton\n");
return 1;
}
// 将 XDP 程序 attach 到指定网卡
err = bpf_xdp_attach(if_nametoindex(argv[1]), bpf_program__fd(skel->progs.xdp_tcp_counter), 0, NULL);
if (err) {
fprintf(stderr, "Failed to attach XDP program to %s: %d\n", argv[1], err);
goto cleanup;
}
printf("Successfully started! Counting TCP packets on %s. Press Ctrl+C to exit.\n", argv[1]);
// 每秒打印一次计数
while (!exiting) {
__u32 key = 0;
__u64 value = 0;
err = bpf_map__lookup_elem(skel->maps.tcp_count, &key, sizeof(key), &value, sizeof(value), 0);
if (err) {
fprintf(stderr, "Failed to lookup map\n");
} else {
printf("TCP packets counted: %llu\n", value);
}
sleep(1);
}
printf("\nDetaching XDP program...\n");
cleanup:
bpf_xdp_detach(if_nametoindex(argv[1]), 0, NULL);
tcp_counter_kern__destroy(skel);
return 0;
}
四、Makefile(基于 libbpf-bootstrap 风格)
# Makefile
BPFTOOL ?= bpftool
CC := gcc
CLANG ?= clang
LLVM_STRIP ?= llvm-strip
ARCH := $(shell uname -m | sed 's/x86_64/x86/' | sed 's/aarch64/arm64/')
VMLINUX := /sys/kernel/btf/vmlinux
ifeq ($(VMLINUX),)
$(error Please enable CONFIG_DEBUG_INFO_BTF and install vmlinux)
endif
APP := tcp_counter
XDP_OBJ := src/$(APP)_kern.o
.PHONY: all clean run
all: $(APP)
$(APP): src/$(APP)_user.c $(XDP_OBJ)
$(CC) -g -O2 -Wall -I . src/$(APP)_user.c -o $(APP) \
-lelf -lz -lbpf
src/$(APP)_kern.o: src/$(APP)_kern.c
$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) \
-I /usr/include/bpf \
-c $< -o $@
$(LLVM_STRIP) -g $@ # 移除调试符号(可选)
clean:
rm -f $(APP) src/*.o src/*.skel.h
run: $(APP)
sudo ./tcp_counter eth0 # 替换 eth0 为你的网卡名
💡 提示:首次编译前需生成 skeleton 头文件。更推荐使用 libbpf-bootstrap 模板自动生成 Makefile 和 skel。
五、快速体验(简化版:不用 skeleton)
如果你不想用 skeleton,也可以手动加载:
内核程序不变,用户态改用 bpftool + 简单 C:
1. 编译内核程序
clang -O2 -g -target bpf -c src/tcp_counter_kern.c -o tcp_counter_kern.o
2. 加载到网卡
sudo ip link set dev eth0 xdp obj tcp_counter_kern.o sec xdp
3. 查看 map 值(每秒)
while true; do
sudo bpftool map dump name tcp_count
sleep 1
done
注意:map 名字必须全局唯一,或通过
bpftool map list找 ID。
六、注意事项
- 网卡支持:XDP native 模式需要驱动支持(如
ixgbe,i40e,virtio_net)。虚拟机可用veth或lo测试(但 lo 不支持 XDP,建议用vethpair)。 - 权限:需要 root 权限加载 XDP。
- 卸载:务必
ip link set dev eth0 xdp off,否则程序退出后 XDP 仍生效。 - 性能:此程序只做简单解析,性能极高(百万 PPS 级别)。
七、测试方法
# 终端1:运行程序
sudo ./tcp_counter eth0
# 终端2:发送 TCP 包(例如 HTTP 请求)
curl http://example.com
# 或用 hping3 发 TCP 包
sudo hping3 -S -p 80 -c 10 example.com
你应该看到计数增加!
3.实现一个 L4 负载均衡器
实现一个 L4(传输层)负载均衡器 使用 XDP + eBPF 是一个非常经典且高性能的场景。我们将构建一个 基于 XDP 的 TCP/UDP 四层负载均衡器,其核心功能包括:
- 监听一个虚拟 VIP(Virtual IP)
- 将发往 VIP 的流量 DNAT 到后端真实服务器(Real Server)列表
- 使用简单轮询(Round Robin)选择后端
- 支持连接跟踪(可选,简化版先不维护完整 conntrack)
⚠️ 注意:由于 XDP 位于网络栈极早期(驱动层),不能直接修改目的 MAC 地址以外的包内容并重发(如修改 IP/TCP 头需 careful 处理 checksum)。但我们可以:
- 修改 目的 MAC(二层转发)
- 或使用 XDP_REDIRECT 到另一个设备(如 veth pair)
- 或在 tc (traffic control) BPF 中做 L3/L4 修改(更灵活)
但为了 真正实现 L4 负载均衡(修改 IP+端口),XDP 并不是最佳位置 —— 更推荐使用 tc BPF(clsact qdisc),因为它可以安全地修改 L3/L4 头部并更新 checksum。
不过,我们仍可以用 XDP 实现“L4 负载均衡”的核心逻辑,并将包 redirect 到本机 loopback 或 veth,由另一个 tc 程序或用户态程序完成 NAT。但为简化,这里采用 “XDP + 修改 MAC 层” 的 DSR(Direct Server Return)模式,或 仅做调度决策 + 重定向到后端(假设后端在同一 L2 网络)。
✅ 方案选择:L2 DSR 模式(推荐用于 XDP)
- 前端:客户端 → VIP(如
10.0.0.100) - 负载均衡器(XDP)收到包后:
- 若目的 IP == VIP,则选择一个后端(如
10.0.0.10,10.0.0.11) - 只修改目的 MAC 地址为后端服务器的 MAC
- IP 和 TCP/UDP 头不变
- 若目的 IP == VIP,则选择一个后端(如
- 后端服务器必须:
- 配置 VIP(
10.0.0.100)在 lo 接口(ip addr add 10.0.0.100/32 dev lo) - 开启
arp_ignore=1,arp_announce=2 - 直接响应客户端(DSR:回程不经过 LB)
- 配置 VIP(
这是 XDP 最适合的负载均衡模型,性能极高(百万 PPS),被 Cilium、Cloudflare 等广泛使用。
一、目标
- VIP:
10.0.0.100 - 后端列表(Real Servers):
10.0.0.10→ MAC:aa:bb:cc:dd:ee:0110.0.0.11→ MAC:aa:bb:cc:dd:ee:02
- 协议:TCP & UDP
- 调度算法:简单轮询(per-CPU)
二、eBPF/XDP 程序(内核态)
文件:lb_xdp_kern.c
// lb_xdp_kern.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/udp.h>
#include <arpa/inet.h>
#ifndef VIP
#define VIP 0x6400000a // 10.0.0.100 in network byte order (hex: 0a.00.00.64 -> 0x6400000a)
#endif
// 后端结构
struct real_definition {
__u32 ip;
unsigned char mac[6];
};
// 最多支持 8 个后端
#define MAX_REALS 8
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, MAX_REALS);
__type(key, __u32);
__type(value, struct real_definition);
} reals_map SEC(".maps");
// 每 CPU 的索引,用于轮询
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, __u32);
} rr_index SEC(".maps");
SEC("xdp")
int xdp_lb_func(struct xdp_md *ctx) {
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if (data + sizeof(*eth) > data_end)
return XDP_PASS;
if (eth->h_proto != __constant_htons(ETH_P_IP))
return XDP_PASS;
struct iphdr *ip = (struct iphdr *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return XDP_PASS;
// 只处理发往 VIP 的包
if (ip->daddr != VIP)
return XDP_PASS;
// 获取后端数量(硬编码为 2,也可用 map 存 count)
const __u32 nr_reals = 2;
// 获取轮询索引
__u32 key = 0;
__u32 *index = bpf_map_lookup_elem(&rr_index, &key);
if (!index)
return XDP_PASS;
__u32 backend_idx = *index % nr_reals;
(*index)++;
// 获取后端信息
struct real_definition *real = bpf_map_lookup_elem(&reals_map, &backend_idx);
if (!real)
return XDP_PASS;
// 修改目的 MAC
__builtin_memcpy(eth->h_dest, real->mac, 6);
// 注意:源 MAC 不变(仍是 LB 的 MAC),后端需能处理
return XDP_TX; // 从接收网卡直接发回(要求网卡支持)
}
char _license[] SEC("license") = "GPL";
三、用户态加载程序(填充后端)
文件:lb_xdp_user.c
// lb_xdp_user.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#include "lb_xdp_kern.skel.h"
// 后端服务器(IP + MAC)
struct backend {
const char *ip_str;
const char *mac_str; // aa:bb:cc:dd:ee:ff
};
static struct backend backends[] = {
{"10.0.0.10", "aa:bb:cc:dd:ee:01"},
{"10.0.0.11", "aa:bb:cc:dd:ee:02"},
};
static void parse_mac(const char *mac_str, unsigned char *mac) {
int values[6];
if (sscanf(mac_str, "%x:%x:%x:%x:%x:%x",
&values[0], &values[1], &values[2],
&values[3], &values[4], &values[5]) != 6) {
fprintf(stderr, "Invalid MAC: %s\n", mac_str);
exit(1);
}
for (int i = 0; i < 6; i++)
mac[i] = (unsigned char) values[i];
}
int main(int argc, char **argv) {
struct lb_xdp_kern *skel;
int ifindex;
if (argc != 2) {
fprintf(stderr, "Usage: %s <ifname>\n", argv[0]);
return 1;
}
ifindex = if_nametoindex(argv[1]);
if (!ifindex) {
perror("if_nametoindex");
return 1;
}
skel = lb_xdp_kern__open();
if (!skel) {
fprintf(stderr, "Failed to open BPF skeleton\n");
return 1;
}
// 设置 VIP(可选,也可在编译时定义)
// 这里我们已在内核代码中硬编码 VIP = 10.0.0.100
if (lb_xdp_kern__load(skel)) {
fprintf(stderr, "Failed to load BPF skeleton\n");
goto cleanup;
}
// 填充后端 map
for (int i = 0; i < sizeof(backends)/sizeof(backends[0]); i++) {
struct real_definition real = {0};
if (inet_pton(AF_INET, backends[i].ip_str, &real.ip) != 1) {
fprintf(stderr, "Invalid IP: %s\n", backends[i].ip_str);
goto cleanup;
}
parse_mac(backends[i].mac_str, real.mac);
if (bpf_map_update_elem(bpf_map__fd(skel->maps.reals_map), &i, &real, BPF_ANY)) {
perror("bpf_map_update_elem reals_map");
goto cleanup;
}
}
// Attach XDP
if (bpf_xdp_attach(ifindex, bpf_program__fd(skel->progs.xdp_lb_func), XDP_FLAGS_DRV_MODE, NULL)) {
fprintf(stderr, "Failed to attach XDP\n");
goto cleanup;
}
printf("L4 Load Balancer (DSR mode) running on %s for VIP 10.0.0.100\n", argv[1]);
printf("Backends: %s (%s), %s (%s)\n",
backends[0].ip_str, backends[0].mac_str,
backends[1].ip_str, backends[1].mac_str);
printf("Press Ctrl+C to stop.\n");
pause(); // 等待信号
cleanup:
bpf_xdp_detach(ifindex, XDP_FLAGS_DRV_MODE, NULL);
lb_xdp_kern__destroy(skel);
return 0;
}
四、编译与运行
1. 安装依赖(Ubuntu/Debian)
sudo apt install libbpf-dev clang llvm make gcc
2. 使用 libbpf-bootstrap 生成项目(推荐)
或者手动编译:
# 生成 skeleton
bpftool gen skeleton lb_xdp_kern.o > lb_xdp_kern.skel.h
# 编译用户态
gcc -g -O2 -Wall -I . -lelf -lbpf lb_xdp_user.c -o lb_xdp
3. 运行
sudo ./lb_xdp eth0
五、后端服务器配置(DSR 模式)
在每台后端服务器上执行:
# 添加 VIP 到 loopback
sudo ip addr add 10.0.0.100/32 dev lo
# 防止 ARP 泄露
echo 1 | sudo tee /proc/sys/net/ipv4/conf/all/arp_ignore
echo 2 | sudo tee /proc/sys/net/ipv4/conf/all/arp_announce
这样,后端可以直接响应客户端,回程流量不经过负载均衡器。
六、进阶方向
- 支持连接亲和性(Session Affinity):用
BPF_MAP_TYPE_LRU_HASH存储 src_ip+src_port → backend - 健康检查:用户态定期探测后端,动态更新
reals_map - 支持 UDP:当前代码已兼容(只要 IP 层匹配 VIP)
- 使用 XDP_REDIRECT 到 AF_XDP:将包交给用户态做复杂处理
- 迁移到 tc BPF:如果需要修改 IP/Port(非 DSR 模式)
七、验证
# 从客户端访问 VIP
curl http://10.0.0.100
# 在后端服务器抓包
tcpdump -i eth0 host 10.0.0.100
# 应看到请求,且目的 IP 仍是 10.0.0.100(DSR 特征)
如果你希望我提供 非 DSR 模式(修改 IP+Port)的 tc BPF 实现,或者 带连接跟踪的完整 L4 LB,也可以继续提问!
4.用 XDP 做 SYN flood 防护
使用 XDP(eXpress Data Path) 实现 SYN Flood 防护 是一个非常高效且实用的场景。XDP 位于网络驱动层,可以在内核协议栈之前极早地丢弃恶意流量,从而极大减轻系统负载。
下面我们将实现一个 基于源 IP 的 SYN 包速率限制 的 XDP 防护程序:
- 检测 TCP SYN 包(非 SYN+ACK)
- 对每个源 IP 统计单位时间内的 SYN 包数量
- 超过阈值则丢弃(
XDP_DROP) - 使用 BPF_MAP_TYPE_LRU_HASH 自动淘汰不活跃 IP
- 支持配置阈值(如每秒 100 个 SYN)
✅ 设计要点
| 特性 | 说明 |
|---|---|
| Hook 点 | XDP(驱动层,最早处理) |
| 匹配条件 | IPv4 + TCP + tcp.flags & TCP_FLAG_SYN && !(tcp.flags & TCP_FLAG_ACK) |
| 限速机制 | 每个源 IP 的滑动窗口计数(简化为“每秒计数 + 定时清零”) |
| 数据结构 | BPF_MAP_TYPE_LRU_HASH:自动淘汰冷 IP,防内存耗尽 |
| 用户态控制 | 可动态设置阈值、查看被拦截 IP |
⚠️ 注意:XDP 中无法使用定时器直接清零 map,因此我们采用 “带时间戳的计数” 方式实现滑动窗口。
一、内核态 XDP 程序:syn_flood_xdp_kern.c
// syn_flood_xdp_kern.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/in.h>
#ifndef THRESHOLD
#define THRESHOLD 100 // 默认每秒 10 个 SYN 包(测试时可调低)
#endif
#define MAX_ENTRIES 65536 // 最多跟踪 65K 个源 IP
struct syn_entry {
__u64 last_seen; // 最后一次看到的时间(纳秒)
__u32 count; // 当前周期内的 SYN 计数
};
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, __u32); // src IP (network byte order)
__type(value, struct syn_entry);
} syn_track SEC(".maps");
// 全局阈值(可通过用户态更新)
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, __u32);
} config_map SEC(".maps");
static __always_inline bool is_syn_packet(struct tcphdr *tcp) {
return (tcp->syn == 1) && (tcp->ack == 0);
}
SEC("xdp")
int xdp_syn_flood_filter(struct xpd_md *ctx) {
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if (data + sizeof(*eth) > data_end)
return XDP_PASS;
if (eth->h_proto != __constant_htons(ETH_P_IP))
return XDP_PASS;
struct iphdr *ip = (struct iphdr *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return XDP_PASS;
if (ip->protocol != IPPROTO_TCP)
return XDP_PASS;
struct tcphdr *tcp = (struct tcphdr *)(ip + 1);
if ((void *)(tcp + 1) > data_end)
return XDP_PASS;
// 只处理纯 SYN 包(非握手响应)
if (!is_syn_packet(tcp))
return XDP_PASS;
__u32 src_ip = ip->saddr;
// 获取当前时间(纳秒)
__u64 now = bpf_ktime_get_ns();
__u64 window_size = 1000000000ULL; // 1 second in nanoseconds
// 获取配置阈值
__u32 cfg_key = 0;
__u32 *threshold = bpf_map_lookup_elem(&config_map, &cfg_key);
__u32 thresh = threshold ? *threshold : THRESHOLD;
struct syn_entry *entry = bpf_map_lookup_elem(&syn_track, &src_ip);
if (!entry) {
// 第一次见到该 IP,初始化
struct syn_entry new_entry = {
.last_seen = now,
.count = 1
};
bpf_map_update_elem(&syn_track, &src_ip, &new_entry, BPF_ANY);
return XDP_PASS;
}
// 检查是否在窗口期内
if (now - entry->last_seen > window_size) {
// 窗口已过,重置计数
entry->last_seen = now;
entry->count = 1;
return XDP_PASS;
}
// 窗口期内,增加计数
entry->count++;
// 超过阈值?丢弃!
if (entry->count > thresh) {
return XDP_DROP;
}
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
🔍 注意:
- 使用
bpf_ktime_get_ns()获取高精度时间- LRU hash 自动淘汰长时间未访问的条目,防止 DoS 导致内存爆炸
- 仅过滤 纯 SYN(
SYN=1, ACK=0),避免误杀正常连接
二、用户态控制程序:syn_flood_xdp_user.c
功能:
- 加载 XDP 程序
- 设置 SYN 阈值(默认 100)
- 打印被拦截的 IP(可选,需 ring buffer)
- 支持 Ctrl+C 优雅退出
// syn_flood_xdp_user.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <net/if.h>
#include <arpa/inet.h>
#include <bpf/libbpf.h>
#include "syn_flood_xdp_kern.skel.h"
static volatile bool exiting = false;
void sig_handler(int sig) {
exiting = true;
}
int main(int argc, char **argv) {
struct syn_flood_xdp_kern *skel;
int ifindex;
__u32 key = 0;
__u32 threshold = 50; // 测试时设为 50 SYN/秒
if (argc != 2) {
fprintf(stderr, "Usage: %s <interface>\n", argv[0]);
return 1;
}
ifindex = if_nametoindex(argv[1]);
if (!ifindex) {
perror("Invalid interface");
return 1;
}
signal(SIGINT, sig_handler);
signal(SIGTERM, sig_handler);
skel = syn_flood_xdp_kern__open_and_load();
if (!skel) {
fprintf(stderr, "Failed to open/load BPF program\n");
return 1;
}
// 设置阈值
bpf_map_update_elem(bpf_map__fd(skel->maps.config_map), &key, &threshold, BPF_ANY);
// Attach XDP
if (bpf_xdp_attach(ifindex, bpf_program__fd(skel->progs.xdp_syn_flood_filter),
XDP_FLAGS_DRV_MODE, NULL)) {
fprintf(stderr, "Failed to attach XDP\n");
goto cleanup;
}
printf("SYN Flood protection active on %s (threshold: %u SYN/sec)\n", argv[1], threshold);
printf("Press Ctrl+C to exit.\n");
while (!exiting) {
sleep(1);
}
cleanup:
bpf_xdp_detach(ifindex, XDP_FLAGS_DRV_MODE, NULL);
syn_flood_xdp_kern__destroy(skel);
return 0;
}
三、编译(使用 libbpf + skeleton)
# 编译内核程序
clang -O2 -g -target bpf -D__TARGET_ARCH_x86 \
-I /usr/include/bpf \
-c syn_flood_xdp_kern.c -o syn_flood_xdp_kern.o
# 生成 skeleton
bpftool gen skeleton syn_flood_xdp_kern.o > syn_flood_xdp_kern.skel.h
# 编译用户态
gcc -g -O2 -Wall -lelf -lbpf syn_flood_xdp_user.c -o syn_flood_xdp
四、运行与测试
1. 启动防护
sudo ./syn_flood_xdp eth0
2. 模拟 SYN Flood(另一台机器或本机)
# 使用 hping3 发送大量 SYN
sudo hping3 -S -p 80 --flood --rand-source 192.168.1.100
💡 本机测试注意:XDP 不处理 loopback (
lo),建议用两台机器,或创建 veth pair。
3. 验证效果
- 正常连接(少量 SYN):应成功建立 TCP
- 洪水攻击(高频 SYN):连接超时,
netstat -s | grep -i listen中 overflow 不增加 - 查看 map 内容(调试):
sudo bpftool map dump name syn_track | head -20
五、增强建议
| 功能 | 实现方式 |
|---|---|
| 日志记录攻击 IP | 使用 BPF_MAP_TYPE_RINGBUF 上报到用户态 |
| 动态调整阈值 | 用户态监听信号或 socket 控制 |
| 白名单 | 增加 BPF_MAP_TYPE_HASH 存放可信 IP |
| 支持 IPv6 | 添加 IPv6 解析分支 |
| 更精确的滑动窗口 | 使用 per-CPU counter + 定时清理(需 workqueue,XDP 不支持) |
六、注意事项
- 性能:此程序每包只做一次 hash 查找,性能极高(>10M PPS)
- 内存安全:LRU hash 防止内存耗尽
- 误杀风险:高并发合法客户端(如爬虫)可能被限,可结合白名单
- 卸载:务必
ip link set dev eth0 xdp off或程序正常退出
更多推荐

所有评论(0)