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 实践)

七、实战建议

  1. 从简单开始:先写一个统计 TCP 包数量的 XDP 程序
  2. 结合 tcpdump/Wireshark 验证行为
  3. 使用 bpftool prog dump 查看加载的字节码
  4. 阅读内核源码 中的 samples/bpf/
  5. 参与社区: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。


六、注意事项

  1. 网卡支持:XDP native 模式需要驱动支持(如 ixgbe, i40e, virtio_net)。虚拟机可用 vethlo 测试(但 lo 不支持 XDP,建议用 veth pair)。
  2. 权限:需要 root 权限加载 XDP。
  3. 卸载:务必 ip link set dev eth0 xdp off,否则程序退出后 XDP 仍生效。
  4. 性能:此程序只做简单解析,性能极高(百万 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 头不变
  • 后端服务器必须:
    • 配置 VIP(10.0.0.100)在 lo 接口(ip addr add 10.0.0.100/32 dev lo
    • 开启 arp_ignore=1, arp_announce=2
    • 直接响应客户端(DSR:回程不经过 LB)

这是 XDP 最适合的负载均衡模型,性能极高(百万 PPS),被 Cilium、Cloudflare 等广泛使用。


一、目标

  • VIP: 10.0.0.100
  • 后端列表(Real Servers):
    • 10.0.0.10 → MAC: aa:bb:cc:dd:ee:01
    • 10.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

这样,后端可以直接响应客户端,回程流量不经过负载均衡器


六、进阶方向

  1. 支持连接亲和性(Session Affinity):用 BPF_MAP_TYPE_LRU_HASH 存储 src_ip+src_port → backend
  2. 健康检查:用户态定期探测后端,动态更新 reals_map
  3. 支持 UDP:当前代码已兼容(只要 IP 层匹配 VIP)
  4. 使用 XDP_REDIRECT 到 AF_XDP:将包交给用户态做复杂处理
  5. 迁移到 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 导致内存爆炸
  • 仅过滤 纯 SYNSYN=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 不支持)

六、注意事项

  1. 性能:此程序每包只做一次 hash 查找,性能极高(>10M PPS)
  2. 内存安全:LRU hash 防止内存耗尽
  3. 误杀风险:高并发合法客户端(如爬虫)可能被限,可结合白名单
  4. 卸载:务必 ip link set dev eth0 xdp off 或程序正常退出

Logo

更多推荐