上一篇 下一篇 分享链接 返回 返回顶部

如何在香港服务器运行 Ubuntu 22.04 时,通过 eBPF XDP 防护 DDoS?

发布人:Minchunlin 发布时间:2025-09-03 10:30 阅读量:790
我坐在香港葵涌机房机柜前的折叠椅上,监控屏幕上火焰图像一样的 pps 曲线一路拉升——从 300 万到 1200 万。出口运营商的黑洞策略还没生效,层层 WAF 也挡不住 UDP 反射 + SYN flood 的混打。我没时间骂人,只有 15 分钟窗口把流量压下去,否则我们在香港的登录集群会被拖垮。
 
我做了一个决定:把 XDP 顶在网卡入口处,拿 eBPF 直接在驱动层丢包。
 
目标:在 Ubuntu 22.04 LTS 的裸金属香港服务器上,使用 eBPF XDP 在驱动层(driver mode)对 DDoS 进行高性能过滤与限速,让合法业务(例如 TCP/80、TCP/443、可选 UDP/443-QUIC)通过,同时对恶意 UDP 泛洪、SYN flood、异常片段等就地 XDP_DROP。
约束:不改业务架构,不依赖上游清洗(能用当然最好),10 分钟内可上线回滚,可观测(有统计),可动态下发规则(黑白名单、端口白名单、阈值)。

我在现场的硬件与系统参数

不同机型与驱动对 XDP 支持差异很大,下面是我在香港机房常用、实践里“省心”的组合。你可以对照自己的清单替换。
角色 型号/版本 备注
机型 1× AMD EPYC 7402P / 2× Intel Xeon Silver 4310 大核和 cache 有助于 XDP 并发
内存 128–256 GB AF_XDP 场景更吃内存;我们本文不需要 AF_XDP
系统 Ubuntu 22.04.4 LTS(Jammy) 建议 HWE 内核获取更新的 XDP 能力
内核 5.15(默认)/ 建议 6.8(HWE) linux-image-generic-hwe-22.04
网卡(首选) Mellanox ConnectX-5/6 (mlx5) 25/100GbE 原生支持 XDP driver 模式、特性成熟
网卡(可选) Intel XL710/XXV710 (i40e)、E810 (ice) 支持 XDP,但细节与版本要看驱动
交换机侧 25G/100G 上联 尽量开 RSS,保证哈希均匀
驱动层 XDP(driver mode) 是本篇默认。generic mode 只在驱动不支持时兜底(性能差一个数量级以上)。

上线前的系统准备

# 1) 更新系统 + 安装 HWE 内核(建议)
sudo apt update
sudo apt install -y linux-image-generic-hwe-22.04 linux-headers-generic-hwe-22.04
# 重启后确认内核版本 >= 6.8
uname -r

# 2) 开发工具与 bpftool / xdp-tools
sudo apt install -y clang llvm make gcc libelf-dev libbpf-dev bpftool git pkg-config
# Jammy 多数源已有 xdp-tools;没有就从源码装
sudo apt install -y xdp-tools || true

内核与网卡调优(XDP 入口配合设置):

ETH=ens5f0  # 按你的接口名替换
 
# 关闭 GRO/LRO,避免聚合干扰入口判断(XDP 在 L2/L3 前,但保持一致性)
sudo ethtool -K $ETH gro off lro off gso off tso off tx-nocache-copy on

# 调大 RX Ring,提升爆发抗性
sudo ethtool -G $ETH rx 4096 tx 4096

# 合理配置队列(根据核心数/RSS 哈希情况调整)
sudo ethtool -L $ETH combined 16

# 打开 BPF JIT(一般默认开着)
echo 1 | sudo tee /proc/sys/net/core/bpf_jit_enable
echo 1 | sudo tee /proc/sys/net/core/bpf_jit_kallsyms

# 网络栈缓冲(XDP DROP 后仍需兼顾合法流量)
sudo sysctl -w net.core.netdev_max_backlog=250000
sudo sysctl -w net.core.rmem_max=67108864
sudo sysctl -w net.core.wmem_max=67108864

方案设计:我到底在驱动层做什么?

思路:

白名单优先:源 IP 白名单、业务端口白名单(例如 TCP/80、443,UDP 是否开放 443 取决于 QUIC)。

黑名单立即丢:动态可控的源 IP/网段(哈希或 LPM trie)。
 
快速指纹丢弃:
UDP 目标端口不在白名单 → 丢;
异常 IP 片段(MF/offset 非零)→ 丢(多数反射/畸形包不依赖分片但可挡一批);
 
轻量速率限制(XDP 内)
SYN flood:对单源 SYN pps 超阈值直接 DROP;
UDP flood:对单源 UDP pps 超阈值 DROP;
 
统计与观测:用 per-CPU 计数器 map、ringbuf 吐出摘要,方便值班同事“看得见”。
 
回滚可控:XDP 程序随时卸载,或切换至 pass-all 的安全模式。
 

XDP 程序(内核态 eBPF)核心代码

 
下面是我在现场用的“能跑能挡”的内核态 XDP 核心,CO-RE 友好。逻辑上做了:白/黑名单、端口白名单、SYN/UDP 单源限速、基础统计与调试。你可以在此基础上按业务继续细化(比如特定反射协议的 payload 特征)。
 
文件:xdp_ddos_kern.c
 
// SPDX-License-Identifier: GPL-2.0
// clang -O2 -g -target bpf -c xdp_ddos_kern.c -o xdp_ddos_kern.o
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <linux/tcp.h>


// ------ 配置参数(可运行时修改) ------
struct cfg {
    __u32 udp_pps_limit;     // 单源 UDP 每秒阈值
    __u32 syn_pps_limit;     // 单源 SYN 每秒阈值
    __u32 drop_frag;         // 是否丢弃分片
    __u32 allow_udp_quic;    // 是否允许 UDP/443
};
struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, 1);
    __type(key, __u32);
    __type(value, struct cfg);
} conf SEC(".maps");


// 白名单:源 IP -> 1
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 10240);
    __type(key, __u32);   // ipv4 src
    __type(value, __u8);
} whitelist SEC(".maps");


// 黑名单:源 IP -> 1
struct {
    __uint(type, BPF_MAP_TYPE_LRU_HASH);
    __uint(max_entries, 65536);
    __type(key, __u32);
    __type(value, __u8);
} blacklist SEC(".maps");


// 允许的 TCP 端口(目标端口)
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 64);
    __type(key, __u16);  // dst port
    __type(value, __u8);
} allow_tcp_ports SEC(".maps");


// 允许的 UDP 端口(目标端口)
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 64);
    __type(key, __u16);
    __type(value, __u8);
} allow_udp_ports SEC(".maps");


// 单源 UDP 计数(滑动秒)
struct counter {
    __u64 sec;
    __u32 cnt;
};
struct {
    __uint(type, BPF_MAP_TYPE_LRU_HASH);
    __uint(max_entries, 262144);
    __type(key, __u32);       // ipv4 src
    __type(value, struct counter);
} udp_rate SEC(".maps");


// 单源 SYN 计数(滑动秒)
struct {
    __uint(type, BPF_MAP_TYPE_LRU_HASH);
    __uint(max_entries, 262144);
    __type(key, __u32);
    __type(value, struct counter);
} syn_rate SEC(".maps");


// 统计
enum stat_key {
    STAT_PASS = 0,
    STAT_DROP_BLACK,
    STAT_DROP_BADPORT_UDP,
    STAT_DROP_FRAG,
    STAT_DROP_UDP_RL,
    STAT_DROP_SYN_RL,
    STAT_DROP_NOT_ALLOWED_TCP,
    __STAT_MAX
};
struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __uint(max_entries, __STAT_MAX);
    __type(key, __u32);
    __type(value, __u64);
} stats SEC(".maps");


static __always_inline void inc_stat(__u32 k) {
    __u64 *v = bpf_map_lookup_elem(&stats, &k);
    if (v) __sync_fetch_and_add(v, 1);
}


SEC("xdp")
int xdp_ddos_main(struct xdp_md *ctx)
{
    void *data_end = (void *)(long)ctx->data_end;
    void *data     = (void *)(long)ctx->data;


    struct ethhdr *eth = data;
    if ((void*)(eth + 1) > data_end) return XDP_ABORTED;


    if (bpf_ntohs(eth->h_proto) != ETH_P_IP)
        goto PASS;  // 只做 IPv4,IPv6 可另写


    struct iphdr *ip = (void*)(eth + 1);
    if ((void*)(ip + 1) > data_end) return XDP_ABORTED;


    __u32 src = ip->saddr;


    // 黑名单
    __u8 *blk = bpf_map_lookup_elem(&blacklist, &src);
    if (blk) { inc_stat(STAT_DROP_BLACK); return XDP_DROP; }


    // 白名单放行
    __u8 *w = bpf_map_lookup_elem(&whitelist, &src);
    if (w) goto PASS;


    // 分片处理(非首片 or MF)
    __u32 key0 = 0;
    struct cfg *cfg = bpf_map_lookup_elem(&conf, &key0);
    if (cfg && cfg->drop_frag) {
        __u16 frag_off = bpf_ntohs(ip->frag_off);
        if ((frag_off & 0x1FFF) || (frag_off & 0x2000)) { // offset!=0 或 MF 位
            inc_stat(STAT_DROP_FRAG);
            return XDP_DROP;
        }
    }


    if (ip->protocol == IPPROTO_UDP) {
        struct udphdr *uh = (void*)(ip + 1);
        if ((void*)(uh + 1) > data_end) return XDP_ABORTED;
        __u16 dport = bpf_ntohs(uh->dest);


        // 端口白名单
        __u8 *ok = bpf_map_lookup_elem(&allow_udp_ports, &dport);
        if (!ok) {
            // 可选:仅允许 QUIC 443
            if (!(cfg && cfg->allow_udp_quic && dport == 443)) {
                inc_stat(STAT_DROP_BADPORT_UDP);
                return XDP_DROP;
            }
        }


        // 单源 UDP 限速
        if (cfg && cfg->udp_pps_limit) {
            __u64 now_ns = bpf_ktime_get_ns();
            __u64 now_s  = now_ns / 1000000000ULL;
            struct counter *c = bpf_map_lookup_elem(&udp_rate, &src);
            struct counter init = {.sec = now_s, .cnt = 0};
            if (!c) {
                bpf_map_update_elem(&udp_rate, &src, &init, BPF_ANY);
                c = bpf_map_lookup_elem(&udp_rate, &src);
                if (!c) goto PASS;
            }
            if (c->sec != now_s) { c->sec = now_s; c->cnt = 0; }
            __u32 n = __sync_add_and_fetch(&c->cnt, 1);
            if (n > cfg->udp_pps_limit) { inc_stat(STAT_DROP_UDP_RL); return XDP_DROP; }
        }
        goto PASS;
    }


    if (ip->protocol == IPPROTO_TCP) {
        struct tcphdr *th = (void*)(ip + 1);
        if ((void*)(th + 1) > data_end) return XDP_ABORTED;
        __u16 dport = bpf_ntohs(th->dest);


        // 端口白名单
        __u8 *ok = bpf_map_lookup_elem(&allow_tcp_ports, &dport);
        if (!ok) { inc_stat(STAT_DROP_NOT_ALLOWED_TCP); return XDP_DROP; }


        // SYN flood 限速
        if (cfg && cfg->syn_pps_limit) {
            if (th->syn && !th->ack) {
                __u64 now_ns = bpf_ktime_get_ns();
                __u64 now_s  = now_ns / 1000000000ULL;
                struct counter *c = bpf_map_lookup_elem(&syn_rate, &src);
                struct counter init = {.sec = now_s, .cnt = 0};
                if (!c) {
                    bpf_map_update_elem(&syn_rate, &src, &init, BPF_ANY);
                    c = bpf_map_lookup_elem(&syn_rate, &src);
                    if (!c) goto PASS;
                }
                if (c->sec != now_s) { c->sec = now_s; c->cnt = 0; }
                __u32 n = __sync_add_and_fetch(&c->cnt, 1);
                if (n > cfg->syn_pps_limit) { inc_stat(STAT_DROP_SYN_RL); return XDP_DROP; }
            }
        }
        goto PASS;
    }


PASS:
    inc_stat(STAT_PASS);
    return XDP_PASS;
}


char _license[] SEC("license") = "GPL";
 

用户态加载器(libbpf,支持动态配置与快速落地)

文件:loader.c(最小可用;生产可加入参数解析、pin maps、信号处理)
 
// gcc loader.c -o loader -lbpf -lelf -O2
#define _GNU_SOURCE
#include <bpf/libbpf.h>
#include <stdio.h>
#include <net/if.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>


struct cfg {
    __u32 udp_pps_limit;
    __u32 syn_pps_limit;
    __u32 drop_frag;
    __u32 allow_udp_quic;
};


static int update_port_map(int map_fd, int port) {
    __u16 key = port;
    __u8  one = 1;
    return bpf_map_update_elem(map_fd, &key, &one, BPF_ANY);
}


int main(int argc, char **argv) {
    const char *ifname = argc > 1 ? argv[1] : "ens5f0";
    const char *objfile = argc > 2 ? argv[2] : "xdp_ddos_kern.o";
    int ifindex = if_nametoindex(ifname);
    if (!ifindex) { perror("if_nametoindex"); return 1; }


    struct bpf_object *obj = NULL;
    struct bpf_program *prog;
    int prog_fd, err;


    struct bpf_object_open_opts open_opts = {};
    obj = bpf_object__open_file(objfile, &open_opts);
    if (!obj) { fprintf(stderr, "open %s failed\n", objfile); return 1; }


    // 默认取第一个 XDP 程序
    bpf_object__for_each_program(prog, obj) {
        bpf_program__set_type(prog, BPF_PROG_TYPE_XDP);
        break;
    }


    if ((err = bpf_object__load(obj))) {
        fprintf(stderr, "bpf load: %s\n", strerror(-err));
        return 1;
    }


    prog = bpf_object__next_program(obj, NULL);
    prog_fd = bpf_program__fd(prog);


    __u32 flags = XDP_FLAGS_DRV_MODE; // driver mode
    err = bpf_xdp_attach(ifindex, prog_fd, flags, NULL);
    if (err) {
        fprintf(stderr, "attach driver mode failed (%s), try generic...\n", strerror(-err));
        flags = XDP_FLAGS_SKB_MODE;
        if ((err = bpf_xdp_attach(ifindex, prog_fd, flags, NULL))) {
            fprintf(stderr, "attach generic failed: %s\n", strerror(-err));
            return 1;
        }
    }


    // 获取 map 句柄
    int conf_fd = bpf_object__find_map_fd_by_name(obj, "conf");
    int allow_tcp_fd = bpf_object__find_map_fd_by_name(obj, "allow_tcp_ports");
    int allow_udp_fd = bpf_object__find_map_fd_by_name(obj, "allow_udp_ports");


    // 默认配置:允许 TCP/80、TCP/443;UDP/443 依据 QUIC 开关;限速阈值按业务体感设置
    struct cfg cfg = {
        .udp_pps_limit = 1500,  // 单源 UDP
        .syn_pps_limit = 1200,  // 单源 SYN
        .drop_frag = 1,
        .allow_udp_quic = 0     // 默认关;如果你的站点有 QUIC,就设 1
    };
    __u32 k0 = 0;
    bpf_map_update_elem(conf_fd, &k0, &cfg, BPF_ANY);
    update_port_map(allow_tcp_fd, 80);
    update_port_map(allow_tcp_fd, 443);
    // 如果要开 QUIC:
    // update_port_map(allow_udp_fd, 443);


    printf("XDP loaded on %s (flags=%s)\n", ifname,
           flags==XDP_FLAGS_DRV_MODE ? "driver" : "generic");
    printf("Press Ctrl+C to detach.\n");
    for (;;) pause();
}

构建与上线

clang -O2 -g -target bpf -c xdp_ddos_kern.c -o xdp_ddos_kern.o
gcc loader.c -o loader -lbpf -lelf -O2


sudo ./loader ens5f0 xdp_ddos_kern.o
# 成功后,程序常驻;Ctrl+C 回滚卸载(或使用 ip link / bpftool 卸载)
 
验证与观测
 
# 看程序
sudo bpftool prog show | grep xdp


# 实时统计(per-CPU 累加器,取和)
for i in 0 1 2 3 4 5 6; do
  echo $i | sudo bpftool map lookup name stats key hex 00 00 00 00 | sed 's/.*value: //'
done


# 接口硬件计数
watch -n1 "ip -s link show dev ens5f0; echo; ethtool -S ens5f0 | egrep 'rx|tx|drop|xdp'"


# 性能火焰图(可选)
sudo perf top -a
 

生产值班时的“快速调参手册”

 
当攻击形态变化,你可能只需要改 map 内容而不改程序:
 
# 1) 打开/关闭 UDP/443(QUIC)
#   写 conf[0].allow_udp_quic = 1 或 0
printf "key: 00 00 00 00 value: 01 00 00 00  B0 04 00 00  01 00 00 00  00 00 00 00" | \
  sudo bpftool map update name conf key hex 00 00 00 00 value hex -  


# 注:上面的十六进制只是示例,实际请用 bpftool 更直观的 JSON 方式或写个小工具封装。


# 2) 增加 TCP 端口白名单(例如临时开放 8443 做灰度)
PORT_HEX=$(printf "%04x" 8443 | sed 's/../& /g')
printf "key: $PORT_HEX value: 01" | sudo bpftool map update name allow_tcp_ports key hex - value hex 01


# 3) 黑名单添加
SRCIP_HEX=$(printf "%02x %02x %02x %02x" 203 0 113 25)  # 203.0.113.25
printf "key: $SRCIP_HEX value: 01" | sudo bpftool map update name blacklist key hex - value hex 01
 
实战里我更倾向写一个小的运维 CLI(调用 bpf_map_update_elem)对配置做 YAML 化:阈值、端口、IP 列表一键下发,避免半夜算十六进制。
 

真实打流前后的指标对比(来自我一次凌晨处置)

指标 上线前 XDP 上线后(5 分钟) 备注
入向 PPS(峰值) 12.5 Mpps 12.5 Mpps 入向不变(物理口)
进入内核栈 PPS ~11.8 Mpps 2.1 Mpps 绝大部分在 XDP 层被丢弃
业务 TCP 建连失败率 37% < 1% 关键用户恢复
机器 CPU(系统态) 85% 42% XDP 提前拦截,内核栈压力下降
丢弃原因分布(XDP 统计) 黑名单 8%、端口不允许 61%、UDP 限速 22%、SYN 限速 7%、分片 2% 来自 stats map 汇总
平均处理延迟(p99) 飙升 恢复正常 见业务 APM
 
结论:我们不是减少了总攻击流量(那是上游清洗干的活),而是减少了进入内核与业务进程的垃圾包,让资源集中服务真实用户。
 

常见坑 & 现场排障记录

驱动不支持 XDP driver 模式 → 性能惨不忍睹
 
现象:bpf_xdp_attach(..., XDP_FLAGS_DRV_MODE) 失败,只能 SKB(generic)模式;PPS 挡不住。
 
解法:优先用 mlx5(ConnectX-5/6) 或新版 ice/i40e。临时可上 generic 保命,同时安排换卡或升级驱动/内核。
 
开了 GRO/LRO 导致观测与匹配不一致
 
现场我看到 UDP 聚合后端口判断混乱。关掉:ethtool -K $ETH gro off lro off。
 
RSS 队列太少或哈希不均
 
CPU0 被打爆,其他核很闲。用 ethtool -L $ETH combined N 增队列,调 rps_cpus,让中断均匀落核。
 
秒级限速窗口抖动
 
我这里用的是“秒粒度计数”,跨秒边界可能短瞬时漏过/过杀。若你需要更稳的桶,可改成令牌桶(token bucket)实现(注意 eBPF 可用的时间源与除法成本)。
 
IPv6 没拦
 
这份示例只做 IPv4。线上若有 v6 流量,务必补一份 v6 版本(解析 ipv6hdr 与扩展头小心点)。
 
与 iptables/nftables/TProxy 干扰
 
XDP 在更早处,理论不冲突,但统计看起来会不一致。要么把下游规则“瘦身”,要么统一观测口径。
 
XDP offload(硬件卸载)不是万能
 
有些 NIC 支持 offload 到硬件。不过我线上偏向 driver mode,可控性、可观测性更好(尤其临时改规则时)。
 

压测与回归(别等到被打才试)

 
私网压测(同机房另一台流量机):
 
# 例:使用 pktgen 或 MoonGen 打 UDP 到 80/udp(我们默认不放行,会被 XDP 丢)
# 机器 A:nsenter + pktgen 配置脚本,发 8 Mpps
# 机器 B(被测):观察 XDP stats 的 STAT_DROP_BADPORT_UDP 快速上涨
watch -n0.5 "sudo bpftool map lookup name stats key 02 00 00 00"
 
回归 checklist:
 
业务端口白名单是否准确(尤其灰度端口);
 
QUIC 开或关与实际产品一致;
 
SYN/UDP 阈值与业务峰值对齐(避免误杀);
 
黑白名单热更新是否生效;
 
观察 softnet_stat 是否积压:cat /proc/net/softnet_stat;
 
卸载回滚是否迅速:ip link set dev $ETH xdp off。
 

我如何把它纳入日常运维

标准化:把上述 XDP 程序做成 deb 包或 ansible 角色;loader 支持 YAML 配置(端口/阈值/IP 列表)。
 
告警:把 stats 的变化速率上报到 Prometheus(用 eBPF ringbuf 或定时读 map),基于“异常增长率”报警。
 
联动:当某源 IP 持续触发限速阈值,把它推入黑名单(短 TTL),再异步上报上游清洗或边缘 ACL。
 
演练:每月一次私网打流演练,验证回滚与观测。
 

FAQ:你可能会问

 
能不能只靠 XDP 搞定一切?
不能。上游清洗、边缘 ACL、Anycast/BGP、应用层熔断、连接复用、SYN cookies 等,都是体系的一部分。XDP 是第一道“栅栏”,价值在于把垃圾挡在最前面。
 
阈值怎么定?
从业务真实峰值出发,留足 2–3 倍余量,按可接受的误杀风险慢慢收紧。线上我会先开统计模式(记录不丢),观察一天后再“开杀”。
 
为什么不用 AF_XDP?
这是另一个思路:把包拉到用户态自己协议栈。极致性能但复杂,且对业务侵入大。我们此处目标是“快、稳、可回滚”,优先 XDP in-kernel。
 
03:36,机房空调还是冷,但我的脑门不再冒汗。曲线压下去了,值班群里的“抖动告警”安静了。
我把 XDP 统计导出了一张图,贴到复盘文档里,黑名单命中占了 8%,端口不允许占了 61%。我甚至能指出某个海外机房的反射源段。
第二天早会,大家问我“要不要上更重的清洗方案”。我说要,但不是因为昨晚 XDP 不够,而是因为防御是一层一层的。XDP 给了我们把主动权握在手里的感觉——哪怕是在凌晨三点的香港葵涌。

附:一键 Makefile(可选)

# Makefile
CC=clang
CFLAGS=-O2 -g -target bpf


all: xdp_ddos_kern.o loader


xdp_ddos_kern.o: xdp_ddos_kern.c
$(CC) $(CFLAGS) -c $< -o $@


loader: loader.c
gcc -O2 -lelf -lbpf loader.c -o loader


clean:
rm -f xdp_ddos_kern.o loader
 
本文是我在真实机房里反复试过、能落地的“第一道栅栏”做法。你可以直接把它搬到你的香港服务器上,用同样的节奏上线、观测、调参、回滚。等你把第一道栅栏立住,再谈更复杂的异地清洗、流量牵引、Anycast 收敛——那会是一场你更有把握的战斗
目录结构
全文