UDP打洞与防护:利用香港服务器的 eBPF XDP 限速与 DDoS(L3/L4/L7)联动清洗实战

1. 凌晨 2:17 的 pager 与 2600 万 pps 的警报
凌晨 2:17,我被 PagerDuty 拉醒。香港区域的 NAT 穿透中继(我们内部叫 rendezvous 节点)延时从 12ms 猛涨到 800ms,UDP 丢包直奔 60%。Zabbix 图上“鲨鱼鳍”一样的入向曲线把我看懵了:2,600 万 pps,主要是 1200~1500 字节的随机 UDP Payload,典型的 CLDAP/SSDP 混洗。
我远程跳进边界路由器,ACL 基本满打满算,iptables 顶不住,Conntrack 也被拖死。好消息是:前一周我们刚把 rendezvous 节点换成支持 XDP 的 i40e/ixgbe/mlx5,预案里写着“先上 XDP,后联动清洗”。我照 runbook 一步一步把链路压住,5 分钟后业务延迟回落到 30ms,15 分钟内完成清洗回注,流量恢复平稳。后来复盘时,大家都说“这次真像在机房里救火一样”。
2. 需求与目标
-
业务场景:P2P/VoIP/游戏的 UDP 打洞(STUN-like)+ 会话中继(TURN-like/自研 UDP Relay)。
-
攻击面:L3/L4 UDP Flood、反射/放大、低速慢报、碎片攻击;L7 维度为 STUN/自定义握手的异常模式、字节段特征。
-
目标:
-
在驱动态(XDP-DRV)快速丢弃垃圾报文,保住核上的调度与队列;
-
以 可动态下发 的 eBPF map 执行(按源 IP/5Tuple/端口段)令牌桶限速;
-
联动外部清洗:BGP RTBH/Flowspec、GRE 清洗回注;
-
L7 网关(QUIC/HTTP/自定义 UDP)协同,特征提取 + 动态封禁;
-
用 可观测 的指标(pps、bpf_drops、rate_limited、hot IP)闭环风控。
-
3. 机房与硬件清单(可复用)
| 项 | 型号/规格 | 关键参数 | 用途 |
|---|---|---|---|
| 机柜交换 | Arista 7280SR(ToR) | 48×10G SFP+, 6×100G QSFP28 | 上联清洗中心/核心 |
| 边界路由 | Juniper MX204 | BGP full table, Flowspec, 4×100G | RTBH/Flowspec 下发 |
| XDP 节点 | Supermicro 1U + AMD EPYC 7313P | 16C/32T, 128GB RAM | Rendezvous/Relay |
| 网卡(主) | Intel X710-DA4(i40e) | 4×10G, XDP-DRV 支持 | XDP 丢弃与限速 |
| 网卡(备) | Mellanox ConnectX-5(mlx5) | 2×25G, RSS/TSO/GRO | 冗余与迁移 |
| 系统盘 | 2× NVMe 1.92TB (RAID1) | EXT4 | 日志/pcap 缓冲 |
| 时钟 | PTP/Chrony | 同步误差 < 100µs | 指标与攻击回放对齐 |
注:CentOS 7 默认内核 3.10 对 eBPF/XDP 支持不足,必须使用 ELRepo kernel-ml ≥ 5.4,本文以 5.10 LTS 为例。
4. 网络拓扑与 IP 规划
(Internet) ──> [边界路由 MX204]
│ ▲ BGP(Flowspec/RTBH)
▼ │
[ToR Arista 7280SR]
│ 10G/25G
┌───────────┼───────────┐
▼ ▼
[XDP Node A] [XDP Node B]
│ ▲ GRE 回注 │ ▲
▼ │ ▼ │
[清洗中心/云] <───────BGP/Netconf───────>
-
自治域:自有 ASN,边界与上游/清洗厂商建立 eBGP;
-
业务段:203.0.113.0/24(示例),rendezvous/relay 分片段;
-
BGP 社区:
65000:666(RTBH 黑洞),65000:123(回注优先级); -
GRE 回注:
tun0端点与清洗中心双向健康检查(BFD)。
5. 系统与内核准备(CentOS 7.9)
# 1) 升级到 kernel-ml 5.10
sudo yum install -y https://www.elrepo.org/elrepo-release-7.el7.elrepo.noarch.rpm
sudo yum --enablerepo=elrepo-kernel install -y kernel-ml kernel-ml-devel
sudo grub2-set-default 0 && sudo reboot
# 2) 工具链与 bpftool/xdp-tools
sudo yum groupinstall -y "Development Tools"
sudo yum install -y clang llvm elfutils-libelf-devel libcap-ng-devel
# bpftool(推荐从源码或发行包构建)
# xdp-tools 用于 xdp-loader / xdp-filter
# 3) 驱动与网卡设置
sudo ethtool -K eth0 gro off lro off tso off gso off
sudo ethtool -G eth0 rx 4096 tx 4096
sudo sysctl -w net.core.rmem_max=67108864
sudo sysctl -w net.core.wmem_max=67108864
sudo sysctl -w net.core.netdev_max_backlog=250000
# 4) IRQ 绑核(示例)
echo 1 > /proc/irq/$(grep eth0 /proc/interrupts | awk '{print $1}' | tr -d :) /smp_affinity_list
6. XDP 设计:按源 IP 的令牌桶限速 + 特征早丢
策略:
-
特征早丢:基于端口/长度/首部字段/速率阈值,直接 XDP_DROP;
-
令牌桶:对 src IP、可选 5-Tuple 建 LRU 哈希 map,按 pps/kpps 限速;
-
动态热区:把“热点 IP/网段”写入 eBPF map,限速更严;
-
观测:将
drop/limit/pass计数器暴露给 Prometheus Exporter。
6.1 eBPF/XDP 程序(C, 简化示例)
// xdp_udp_rlimit.c (简化示例,仅演示思路)
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>
struct bucket { __u64 last_ns; __u32 tokens; };
struct config { __u32 pps; __u32 burst; __u16 dst_port_min, dst_port_max; };
struct bpf_map_def SEC("maps") rate_map = {
.type = BPF_MAP_TYPE_LRU_HASH,
.key_size = sizeof(__u32), // src IPv4
.value_size = sizeof(struct bucket),
.max_entries = 65536,
};
struct bpf_map_def SEC("maps") cfg = {
.type = BPF_MAP_TYPE_ARRAY,
.key_size = sizeof(__u32),
.value_size = sizeof(struct config),
.max_entries = 1,
};
struct bpf_map_def SEC("maps") counters = {
.type = BPF_MAP_TYPE_PERCPU_ARRAY,
.key_size = sizeof(__u32),
.value_size = sizeof(__u64), // 0:pass 1:drop 2:limit
.max_entries = 3,
};
static __always_inline void count(__u32 idx) {
__u32 k = idx; __u64 *v = bpf_map_lookup_elem(&counters, &k);
if (v) __sync_fetch_and_add(v, 1);
}
SEC("xdp")
int xdp_udp_rlimit(struct xdp_md *ctx) {
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if ((void*)(eth + 1) > data_end) return XDP_PASS;
if (eth->h_proto != __constant_htons(ETH_P_IP)) return XDP_PASS;
struct iphdr *ip = (void*)(eth + 1);
if ((void*)(ip + 1) > data_end) return XDP_PASS;
if (ip->protocol != IPPROTO_UDP) return XDP_PASS;
struct udphdr *udp = (void*)(ip + 1);
if ((void*)(udp + 1) > data_end) return XDP_PASS;
__u32 zero = 0; struct config *c = bpf_map_lookup_elem(&cfg, &zero);
if (!c) return XDP_PASS;
// 早丢:目的端口不在业务范围
__u16 dport = bpf_ntohs(udp->dest);
if (dport < c->dst_port_min || dport > c->dst_port_max) {
count(1); return XDP_DROP;
}
// 令牌桶:src IPv4
__u32 sip = ip->saddr;
struct bucket *b = bpf_map_lookup_elem(&rate_map, &sip);
__u64 now = bpf_ktime_get_ns();
if (!b) {
struct bucket nb = {.last_ns = now, .tokens = c->burst};
bpf_map_update_elem(&rate_map, &sip, &nb, BPF_ANY);
count(0); return XDP_PASS;
}
__u64 elapsed = now - b->last_ns; // ns
__u64 add = (elapsed * c->pps) / 1000000000ULL; // 每秒 c->pps
__u32 tokens = b->tokens;
if (add > 0) {
tokens = tokens + add;
if (tokens > c->burst) tokens = c->burst;
b->last_ns = now;
}
if (tokens == 0) { count(2); return XDP_DROP; }
b->tokens = tokens - 1;
count(0); return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
6.2 编译与加载
clang -O2 -target bpf -c xdp_udp_rlimit.c -o xdp_udp_rlimit.o
# 使用 xdp-loader (xdp-tools)
sudo xdp-loader load -m drv -n xdp_udp_rlimit eth0 xdp_udp_rlimit.o
# 初始化配置:每源 IP 20k pps,突发 40k;业务端口 40000-41000
sudo bpftool map update pinned /sys/fs/bpf/tc/globals/cfg key 0 0 0 0 value \
0x20 0x4e 0x00 0x00 \ # pps=20000 (little endian 注意按需调整)
0x80 0x9c 0x00 0x00 \ # burst=40000
0x40 0x9c \ # dst_port_min=40000
0x10 0xa0 # dst_port_max=41000 (示意)
# 查看计数器(per-cpu 需汇总)
sudo bpftool map dump pinned /sys/fs/bpf/tc/globals/counters
小技巧:生产里我会把 cfg/rate_map 固定 pin 到
/sys/fs/bpf/xdp/…,并写一个简洁的 REST 小服务(或直接用bpftool map update)供风控下发表更新。
7. tc/iptables 协同与七层策略
管线:NIC → XDP (drop/limit) → tc ingress → iptables → 应用。
-
tc(ingress)用于:
-
小流量 SYN/ACK 限制、frag early drop;
-
针对某些难在 XDP 做的匹配(如更复杂的掩码)进行补刀;
-
-
iptables:
-
ipset 热名单(/24 或 /20 段);
-
连接跟踪的最小化使用(对 UDP 仅在握手阶段打开)。
-
# ipset 热名单
ipset create hotnet hash:net maxelem 65536
ipset add hotnet 198.51.100.0/24
iptables -I INPUT -p udp -m set --match-set hotnet src -j DROP
# 针对 UDP 碎片(非首片)
iptables -I INPUT -p udp -f -j DROP
L7(以自研 UDP 握手为例):
-
第一次握手强制携带 8 字节 cookie(根据源 IP/时间片/HMAC 生成);
-
未携带 cookie 的报文在 XDP 先行限速(低阈值),握手通过后将
srcIP→高阈值写入 eBPF map; -
QUIC/HTTP3 或 STUN 类流量:
-
对 STUN Binding Request/Response 的方法码做白名单;
-
限制
CHANGE-REQUEST的组合,防止反射链条被利用。
-
8. 联动清洗:RTBH/Flowspec 与 GRE 回注
8.1 触发逻辑(Prometheus + Alertmanager)
-
XDP 指标:
bpf_drops_total{reason="limit|sig"}、hot_ip_count、topk_src_pps; -
接口指标:
if_in_ucast_pkts,if_in_discards; -
告警阈值(示例):
topk_src_pps > 200k pps OR if_in_ucast_pkts > 6m pps持续 60s。
8.2 RTBH/Flowspec 下发
# RTBH:向上游宣告 /32 黑洞
set community 65000:666 add
announce route 203.0.113.123/32 next-hop self community [65000:666]
# Flowspec:基于 UDP 目的端口 40000-41000 且包长>1000 的限速/丢弃
# (逻辑示意,实际以厂商语法为准)
flow route add match ip-proto 17 dst-port 40000-41000 packet-length >1000 then rate-limit 50M
8.3 清洗回注(GRE)
ip tunnel add tun0 mode gre remote <scrubber_ip> local <xdp_node_ip> ttl 255
ip link set tun0 up
ip addr add 10.10.10.2/30 dev tun0
ip route add 203.0.113.0/24 dev tun0 table 200
ip rule add to 203.0.113.0/24 lookup 200
常见坑:
-
GRE 头开销导致 MTU 下降,记得把回注链路与客户端路径 MTU 协调到 1476/1450;
-
某些 i40e 驱动在高 PPS + GRE 环境下出现 XDP_TX 抖动,避免 RX/TX 混用,优先 XDP_DROP + 回注转发;
-
BFD 失步要有自动降级策略:回退至 RTBH。
9. 可观测性与排障
9.1 指标
| 指标名 | 含义 | 期望/阈值 |
xdp_packets_total |
进入 XDP 的总包 | 参考接口 PPS |
xdp_drop_sig_total |
特征早丢计数 | 攻击期明显上升 |
xdp_drop_rl_total |
令牌桶限速丢弃 | 平稳限制在可承受范围 |
hot_ip_count |
热 IP 数量 | >100 触发风控游标调高 |
9.2 可视化
-
Grafana:入向 PPS、
bpf_*计数、topk表格; -
Loki:抽样 pcaps 元数据(长度分布、端口分布);
-
FlameGraph:应用侧 CPU hotspot,确认 XDP 护住了核。
9.3 快速抓包
# XDP 前镜像:硬件镜像到旁路盒或使用 AF_XDP 采样(生产慎用)
# 主机侧:只在低峰或抽样抓
tcpdump -ni eth0 udp and portrange 40000-41000 -s96 -w /data/pcaps/a.pcap
10. 压测与演练脚本
# 1) iperf3 UDP 压测(攻击模拟)
iperf3 -c <rendezvous_ip> -u -b 5G -l 1200 -t 60 -p 40001
# 2) mausezahn 随机端口/长度
mz eth0 -A rand -B <victim> -t udp "sp=rand,dp=40000-41000,len=rand(200,1400)" -c 0 -q
# 3) 自研握手压测(通过 cookie)
./udp_handshake_bench -dst <ip> -port 40000 -concurrency 20000 -pps 200k
演练关注点:
-
限速前后延时/抖动;
-
应用 QPS 及 95/99 延迟;
-
热 IP 自动上/下线时,阈值是否平滑。
11. 风控与演练清单(Runbook)
11.1 触发与处置
| 步骤 | 条件 | 动作 | 备注 |
| 1 | if_in_ucast_pkts > 4m pps 60s |
XDP cfg:pps=10k/burst=20k |
低阈值预收缩 |
| 2 | hot_ip_count > 100 |
将 top50 源 IP 写入 hot map(更严限速) | 自动化脚本 |
| 3 | xdp_drop_rl_total 持续升高且业务丢包>5% |
下发 Flowspec 限速 | 逐步从 50M→20M |
| 4 | 仍异常 | 对特定 /24 执行 RTBH | 与业务确认影响 |
| 5 | 回注链路就绪 | 开启 GRE 回注 | 验证 MTU/回路 |
11.2 复盘与回退
-
导出 15 分钟窗口的 pcaps 样本(1:10k 抽样);
-
保存 XDP map 快照(
bpftool map dump); -
回滚 cfg 到常态:
pps=20k/burst=40k; -
关闭 GRE,撤销 Flowspec/RTBH。
12. 部署“坑点”与现场解法
-
CentOS 7 + 新内核:升级 kernel-ml 后,旧 dkms 模块失配,提前验证驱动版本;
-
i40e 队列数与 RSS:默认队列少导致单核打满,需
ethtool -L eth0 combined 16+ RPS; -
GRO/LRO:不关闭就会导致包聚合,XDP 看不到真实 PPS;
-
XDP 与 VLAN:某部分交换机 QinQ 出口未剥头,XDP 程序没匹配到 IP,需在 ToR 修策略;
-
回注 MTU:GRE 降 MTU 忘同步导致“黑洞”,上线前做端到端 MTU 探测;
-
eBPF map 热更新:bpftool 更新时偶发
EBUSY,采用“影子 map + 原子指针切换”策略; -
应用握手超时:XDP 把无 cookie 报文限得过严,新客首包丢;通过“临时白名单 + 平滑升阈”解决。
13. 配置片段备忘
13.1 sysctl
cat <<'EOF' | sudo tee /etc/sysctl.d/99-udp-xdp.conf
net.core.netdev_max_backlog = 250000
net.core.rmem_max = 67108864
net.core.wmem_max = 67108864
net.ipv4.udp_rmem_min = 4096
net.ipv4.udp_wmem_min = 4096
net.ipv4.udp_mem = 262144 524288 1048576
net.ipv4.conf.all.rp_filter = 0
EOF
sysctl --system
13.2 systemd 单元(示例)
[Unit]
Description=XDP UDP rate limit
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/sbin/xdp-loader load -m drv -n xdp_udp_rlimit eth0 /opt/xdp/xdp_udp_rlimit.o
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
14. 验收与容量规划
-
单机 10G:在 5~6M pps 入向下,XDP 保持 <20% 核占用;
-
瓶颈:缓存 miss 与 L2/L3 cache 热点,尽量将热 map 控制 < 32K;
-
扩容:横向堆 XDP 节点,ECMP 负载;
-
容灾:BFD/GRE 掉线自动转 RTBH;
-
压测基线:每季度一次全链路演练,记录阈值曲线。
15. 回到凌晨的机房
当天 03:02,我在机房的冷风里看着曲线慢慢回到基线,耳机里是 NOC 值班同事的“OK,延迟回 14ms 了”。复盘会有人问:为啥不用更大更贵的硬件? 我笑着摊手:我们要的不是钢板,而是能在最靠近网卡的地方,最早把垃圾丢掉;要的是能把“限速—清洗—回注—恢复”这条链路练熟。
后来我们又做了 3 次演练:一次针对 STUN 反射,一次模拟 QUIC 握手异常,一次回注链路双断。每次我都在 runbook 里多加了一行备注。现在,PagerDuty 再响,我不再惊醒,因为我知道我们有一套从驱动到七层的完整工具链。