Kernel Bypass 实战:基于 XDP 的 DDoS 清洗,100 行代码扛住 100Gbps SYN Flood
摘要:面对每秒千万级的 SYN Flood 攻击,传统的
iptables或 nftables由于必须经过内核协议栈的完整处理(软中断、连接跟踪),CPU 很快会成为瓶颈。本文将深入 XDP (Express Data Path) 机制,演示如何让数据包在刚从网卡 DMA 过来时就被直接 Drop 掉。全程手写 C + Go 代码,实现无需购买昂贵清洗设备的线速防御。1. 为什么 iptables 救不了你?
当 SYN Flood 流量打过来时,数据包的处理路径是这样的:
- NIC 收到包。
- DMA 拷贝到内存。
- 硬中断:通知 CPU。
- 软中断 (Softirq):内核协议栈开始处理 IP/TCP 头。
- Netfilter:遍历
iptables规则链。 - Conntrack:建立连接跟踪表(最耗 CPU)。
瓶颈:在第 4 步和第 6 步,CPU 已经被海量的垃圾包占满,无法处理合法请求。
XDP 的解决方案:在 第 2 步之后,第 3 步之前,直接挂载 eBPF 程序。此时内核甚至还没来得及给这个包分配
sk_buff结构体,直接基于原始 DMA 缓冲区做判断。特性 | iptables | XDP |
|---|---|---|
处理位置 | 内核协议栈 (TCP/IP Layer) | 网卡驱动层 (L2) |
数据结构 | sk_buff(复杂,耗内存) | xdp_md(原始指针) |
性能上限 | ~1-2 Mpps (百万包/秒) | 10-20 Mpps (取决于 CPU 单核性能) |
适用场景 | 业务防火墙 | DDoS 清洗、负载均衡 |
2. 核心原理:XDP 的三种工作模式
- XDP_DROP:立刻丢弃数据包,直接 Recycle 内存。这是我们今天用的模式。
- XDP_PASS:放行,交给内核协议栈。
- XDP_TX:直接在网卡层面回包(常用于负载均衡,如 Google 的 Maglev)。
3. 实战:100 行代码实现 SYN Flood 杀手
3.1 环境准备
- 网卡支持:Intel (ixgbe, i40e) 或 Mellanox (mlx5) 网卡最佳。
- 内核:>= 4.18 (推荐 5.4+)。
- 关闭 NIC 卸载:某些网卡需要关闭
gro。
3.2 内核态代码 (C语言)
创建
xdp_drop.c。注意,这段代码极其精简,没有任何多余逻辑。// xdp_drop.c#include <linux/bpf.h>#include <linux/if_ether.h>#include <linux/ip.h>#include <linux/tcp.h>#include <bpf/bpf_helpers.h>// 定义一个 Map 用于控制开关(可选)// 0 = 放行所有, 1 = 拦截 SYNstruct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 1);
__type(key, u32);
__type(value, u32);
} control_map SEC(".maps");// 辅助宏:获取 TCP 头#define ETH_HLEN 14SEC("xdp")int xdp_syn_drop(struct xdp_md *ctx) { void *data = (void *)(long)ctx->data; void *data_end = (void *)(long)ctx->data_end; // 1. 解析 Ethernet Header
struct ethhdr *eth = data; if ((void *)(eth + 1) > data_end) return XDP_PASS; // 我们只关心 IPv4
if (eth->h_proto != __constant_htons(ETH_P_IP)) return XDP_PASS; // 2. 解析 IP Header
struct iphdr *ip = data + ETH_HLEN; if ((void *)(ip + 1) > data_end) return XDP_PASS; // 3. 只处理 TCP 协议
if (ip->protocol != IPPROTO_TCP) return XDP_PASS; // 4. 解析 TCP Header
// 注意:IP Header 长度不固定,需要计算 IHL
int ip_hdr_len = ip->ihl * 4; struct tcphdr *tcp = data + ETH_HLEN + ip_hdr_len; if ((void *)(tcp + 1) > data_end) return XDP_PASS; // 5. 判断是否是 SYN 包
// TCP Flag 位在 tcp->syn (bit 1)
// 如果是 SYN 包且没有 ACK 标志 (通常是第一次握手)
if (tcp->syn && !tcp->ack) { // 这里可以添加更复杂的逻辑,比如源IP限速 (Token Bucket)
// 简单粗暴:直接丢弃
// bpf_printk("Dropped SYN packet from %pI4\n", &ip->saddr);
return XDP_DROP;
} // 6. 非 SYN 包放行
return XDP_PASS;
}char __license[] SEC("license") = "GPL";3.3 编译
clang -O2 -g -target bpf \ -c xdp_drop.c -o xdp_drop.o
3.4 用户态加载器 (Go语言)
创建
main.go。这次我们需要绑定到具体的网卡。// main.gopackage mainimport ( "fmt"
"log"
"net"
"os"
"os/signal"
"syscall"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/rlimit")func main() { if len(os.Args) < 2 {
log.Fatal("Usage: go run main.go <network_interface>")
}
ifaceName := os.Args[1] // 1. 解除内存锁定限制
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatal(err)
} // 2. 加载 XDP 程序
spec, err := ebpf.LoadCollectionSpec("xdp_drop.o") if err != nil {
log.Fatal(err)
}
coll, err := ebpf.NewCollection(spec) if err != nil {
log.Fatal(err)
} defer coll.Close()
prog := coll.Programs["xdp_syn_drop"] // 3. 查找网卡
iface, err := net.InterfaceByName(ifaceName) if err != nil {
log.Fatalf("找不到网卡 %s: %v", ifaceName, err)
} // 4. 挂载 XDP 程序
// link.XDP 会自动选择合适的模式 (Driver/Native > Generic)
xdpLink, err := link.AttachXDP(link.XDPOptions{
Program: prog,
Interface: iface.Index,
Flags: link.XDPGenericMode, // 如果网卡不支持 Native,用 Generic (仅调试用)
}) if err != nil {
log.Fatalf("挂载 XDP 失败: %v (尝试使用 sudo 或检查网卡驱动)", err)
} defer xdpLink.Close()
fmt.Printf("✅ XDP DDoS 防御已启动,正在监听网卡: %s\n", ifaceName)
fmt.Println("⚠️ 警告:所有入站 SYN 包将被无条件丢弃!")
fmt.Println(" 按 Ctrl+C 停止。") // 5. 保持运行
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
fmt.Println("\n🛑 正在卸载 XDP 程序...")
}4. 测试:模拟 100Gbps 攻击
4.1 启动防御端
sudo go run main.go eth0
4.2 攻击端(另一台机器)
使用
hping3或 nping发起 SYN Flood:# 安装 hping3sudo apt install hping3# 发起攻击 (-S 是 SYN, --flood 是洪水模式)sudo hping3 -S --flood -p 80 <目标IP>
4.3 效果验证
在防御端使用
sar或 top观察:- CPU 使用率:几乎没有变化(因为包在网卡层就被丢了)。
- 网卡统计:
ethtool -S eth0可以看到rx_packets暴增,但softirq很低。
5. 专家级优化(这才是大厂面试点)
上面的代码虽然能用,但在 100Gbps 场景下还不够完美。以下是进阶优化点:
5.1 必须开启 Driver Mode (Native XDP)
如果你看到日志里有
Generic XDP,那性能只有 Native 模式的 1/10。- 检查方法:
ethtool -i eth0 - 要求:Intel ixgbe/i40e 或 Mellanox mlx5 驱动,且内核编译时开启了
CONFIG_XDP_SOCKETS。
5.2 使用 BPF_MAP_TYPE_LRU_HASH 做 IP 黑名单
上面的代码是“无差别攻击”。真实场景下,我们应该做限速。
// 伪代码:令牌桶算法struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 10000);
__type(key, u32); // src_ip
__type(value, u64); // last_seen_timestamp} ip_blacklist SEC(".maps");// 在 XDP 程序中查询 Map// 如果同一个 IP 在 1 秒内发了超过 100 个 SYN,加入黑名单5.3 避免 bpf_printk
bpf_printk会写 trace_pipe,在高吞吐下会导致内核锁死。生产环境严禁打印日志。5.4 利用多队列 (RSS)
现代网卡有 8~16 个队列。你需要将 XDP 程序绑定到所有的 CPU 核心,才能跑满 100Gbps。可以使用
xdp-loader工具集。6. 总结
通过 XDP,我们用不到 100 行代码实现了一个内核旁路(Kernel Bypass)的 DDoS 清洗系统。这证明了 eBPF 不仅仅是一个“监控工具”,它是重构 Linux 网络数据平面的革命性技术。
方案 | 成本 | 性能 | 灵活性 |
|---|---|---|---|
硬件清洗设备 | 数百万 | 极高 | 低 |
iptables | 零 | 低 | 中 |
XDP (本文) | 零 | 极高 | 极高 |
