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 // 重定向到其他设备或 CPU3. 典型应用场景
- 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 结构避免锁竞争
六、推荐学习资源
官方文档
- https://ebpf.io
- https://docs.cilium.io/en/stable/bpf/
- Linux Kernel BPF Documentation
书籍
- 《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.o2. 加载到网卡
sudo ip link set dev eth0 xdp obj tcp_counter_kern.o sec xdp3. 查看 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 gcc2. 使用 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_xdp3. 运行
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 eth02. 模拟 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或程序正常退出