首页 / 科技 / Kernel Bypass 实战:基于 XDP 的 DDoS 清洗,100 行代码扛住 100Gbps SYN Flood

Kernel Bypass 实战:基于 XDP 的 DDoS 清洗,100 行代码扛住 100Gbps SYN Flood

摸鱼不慌
摸鱼不慌管理员
摘要:面对每秒千万级的 SYN Flood 攻击,传统的 iptablesnftables由于必须经过内核协议栈的完整处理(软中断、连接跟踪),CPU 很快会成为瓶颈。本文将深入 XDP (Express Data Path) 机制,演示如何让数据包在刚从网卡 DMA 过来时就被直接 Drop 掉。全程手写 C + Go 代码,实现无需购买昂贵清洗设备的线速防御。

1. 为什么 iptables 救不了你?

当 SYN Flood 流量打过来时,数据包的处理路径是这样的:
  1. NIC 收到包。


  2. DMA 拷贝到内存。


  3. 硬中断:通知 CPU。


  4. 软中断 (Softirq):内核协议栈开始处理 IP/TCP 头。


  5. Netfilter:遍历 iptables规则链。


  6. 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 的三种工作模式

  1. XDP_DROP:立刻丢弃数据包,直接 Recycle 内存。这是我们今天用的模式


  2. XDP_PASS:放行,交给内核协议栈。


  3. 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 攻击端(另一台机器)

使用 hping3nping发起 SYN Flood:
# 安装 hping3sudo apt install hping3# 发起攻击 (-S 是 SYN, --flood 是洪水模式)sudo hping3 -S --flood -p 80 <目标IP>

4.3 效果验证

在防御端使用 sartop观察:
  • 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 (本文)
极高
极高