拒绝割裂:XDP 与 tc BPF 协同下的高性能抗 D 架构设计与限速实践
在现代网络安全防护体系中,DDoS(分布式拒绝服务)攻击的流量量级和变化频率正以前所未有的速度增长。传统的基于 Linux 内核网络栈(如 iptables / netfilter)的防护方案,由于在处理数据包时必须先经历硬中断、软中断并分配昂贵的 sk_buff 结构体,在面对数千万 PPS(每秒数据包数)的线速小包攻击时,CPU 往往会因上下文切换和内存分配开销而迅速耗尽。
为了解决这一痛点,基于 eBPF(extended Berkeley Packet Filter)的 XDP(eXpress Data Path) 诞生了。然而,随着抗 D 业务场景向精细化发展,开发者们发现,仅仅依靠 XDP 并不能完美解决所有问题。XDP + tc BPF(Traffic Control BPF)的双重防线协同架构,成为了当前业界公认的最优解。
本文将深入探讨这两者的协同机制,并提供无锁高并发限速策略的设计细节与核心代码实现。
一、 为什么抗 D 场景需要 XDP 与 tc BPF 联手?
要理解协同的必要性,首先需要看清它们在 Linux 内核网络路径中所处的位置差异:
+-------------------------------------------------------------+
| Network Card |
+-------------------------------------------------------------+
| (DMA Transfer)
v
+-------------------------------------------------------------+
| XDP (Driver Mode) | <--- 极速丢弃、重定向
| - Runs before skb allocation | 无 skb 负担,性能上限极高
| - Only Ingress (No Egress support in native XDP) |
+-------------------------------------------------------------+
| (Pass / sk_buff Allocated)
v
+-------------------------------------------------------------+
| tc BPF (tc ingress/egress) | <--- 精细化流控、队列管理
| - Runs after skb allocation | 支持 L3/L4 重写、重定向、出向流控
| - Access to full skb metadata |
+-------------------------------------------------------------+
|
v
+-------------------------------------------------------------+
| TCP/IP Protocol Stack |
+-------------------------------------------------------------+
1. XDP:无情的“前沿重炮”
XDP 程序直接挂载在网卡驱动的 RX 队列上。在数据包刚被 DMA 传输到内存、尚未创建 sk_buff 结构体之前,XDP 就已经执行。这使得它拥有恐怖的包处理性能。
- 优势:丢包(
XDP_DROP)和重定向(XDP_REDIRECT)开销极小。 - 劣势:
- 单向性:只支持入向(Ingress)流量,无法直接控制出向(Egress)流量。
- 上下文信息受限:此时没有
skb结构体,无法直接使用内核丰富的路由信息、套接字(Socket)状态等。 - 功能局限:不支持分组排队(Queuing Disciplines, qdisc)和复杂的整形控制。
2. tc BPF:精密的“后方防线”
tc BPF 位于内核网络栈的流量控制层(clsact 挂载点),在 sk_buff 分配完成之后运行。
- 优势:
- 双向控制:完美支持入向和出向。
- 元数据丰富:可以直接访问、修改
sk_buff的所有字段(如 Mark、Priority)。 - 无缝集成 qdisc:可直接干预包的排队与分发,是做限速(Policing)和整形(Shaping)的天然温床。
- 劣势:由于必须经过
skb的分配,其抗 volumetric 丢包性能明显逊于 XDP。
3. 协同的本质:分层治理
在抗 D 场景下,单靠 XDP 会导致限速和流控手段过于粗暴,容易“误杀”正常流量;单靠 tc BPF 则容易被海量垃圾包冲垮。协同架构的逻辑是:XDP 负责“粗筛”,暴力干掉黑名单内的、协议格式畸形的恶意大流量;tc BPF 负责“精滤”与“整形”,对通过首轮筛选的流量进行基于令牌桶等算法的动态限速,并在出向对回包进行流控。
二、 双生架构:基于 BPF Map 的状态共享
要实现 XDP 与 tc BPF 的无缝协同,核心在于状态共享。eBPF 提供了各种类型的 BPF Map,作为它们在内核中交换情报的桥梁。
1. 架构流向设计
- 全局限速与黑名单 Map (
BPF_MAP_TYPE_LRU_HASH):记录被惩罚的 IP、当前连接数及流量计数。 - XDP 过滤链:
- 读取 IP 包头。
- 查询黑名单 Map。若命中,直接返回
XDP_DROP。 - 若未命中,执行轻量级预估。如果某 IP 流量突然爆发,将上下文标记写入 BPF Map 传递,或者直接返回
XDP_PASS递交给上层。
- tc BPF 精细限速:
- 此时数据包已转换为
skb。 - tc BPF 提取 IP 并在专用的限速 Map 中更新其令牌桶状态。
- 如果该 IP 的流量超出了设定的阈值(Burst limit),tc BPF 在
clsact处丢弃包或进行降级(例如重写 IP 头的 DSCP 字段),同时将该 IP 判定为恶意源,直接写入共享的黑名单 Map 中,通知前面的 XDP 拦截。
- 此时数据包已转换为
[Packet In] -> [XDP] --(Hit Blacklist Map?)--> Yes -> [XDP_DROP]
|
No (XDP_PASS)
v
[skb Alloc]
v
[tc] --(Update Token Bucket Map)--> Over Limit? -> Yes -> [Drop & Update Blacklist Map]
|
No -> [Pass to Stack]
三、 核心策略:在 eBPF 中实现无锁高并发限速
在高吞吐场景下,传统的锁机制(如 Spinlock)会导致严重的 CPU 自旋等待,拖垮多核性能。我们必须在 eBPF 中利用**原子操作(Atomics)**或 Per-CPU Map 来实现高并发下的无锁计数与限速。
下面是一个在 eBPF 中实现的**令牌桶(Token Bucket)**限速算法的核心设计:
1. 令牌桶数据结构
为了在 BPF 中维护每个 IP 的桶状态,我们定义如下结构体:
struct bucket_state {
__u64 last_time; // 上次填充令牌的时间戳(纳秒)
__u64 tokens; // 当前可用令牌数(字节数或包数)
};
2. BPF Map 定义
使用 LRU HASH 避免内存无限膨胀,同时保证热点 IP 始终在缓存中。
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 1000000); // 支持百万级并发 IP 跟踪
__type(key, __u32); // 客户端 IP (IPv4)
__type(value, struct bucket_state);
} limit_map SEC(".maps");
3. 令牌桶核心算法实现(C 语法)
在 BPF 中,无法直接在常规 HASH Map 的 Value 上进行复杂的事务操作,但我们可以利用 bpf_ktime_get_ns() 获取高精度时间,并通过原子操作或条件更新来逼近无锁流控。
以下是 tc BPF 中实现限速的核心逻辑:
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/pkt_cls.h>
#include <bpf/bpf_helpers.h>
#define NSEC_PER_SEC 1000000000ULL
// 限速配置参数
#define RATE_LIMIT_BYTES_PER_SEC 10485760ULL // 10MB/s
#define BUCKET_CAPACITY 5242880ULL // 桶容量 5MB
SEC("tc")
int tc_rate_limiter(struct __sk_buff *skb) {
void *data_end = (void *)(long)skb->data_end;
void *data = (void *)(long)skb->data;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return TC_ACT_SHOT;
if (eth->h_proto != __constant_htons(ETH_P_IP))
return TC_ACT_OK;
struct iphdr *iph = (void *)(eth + 1);
if ((void *)(iph + 1) > data_end)
return TC_ACT_SHOT;
__u32 src_ip = iph->saddr;
__u64 now = bpf_ktime_get_ns();
__u32 pkt_len = skb->len;
struct bucket_state *state = bpf_map_lookup_elem(&limit_map, &src_ip);
if (!state) {
// 首次访问,初始化桶
struct bucket_state init_state = {
.last_time = now,
.tokens = BUCKET_CAPACITY - pkt_len
};
bpf_map_update_elem(&limit_map, &src_ip, &init_state, BPF_NOEXIST);
return TC_ACT_OK;
}
// 1. 计算时间差与新增令牌
__u64 elapsed = now - state->last_time;
// 增加的令牌 = 时间差(秒) * 填充速率
__u64 new_tokens = (elapsed * RATE_LIMIT_BYTES_PER_SEC) / NSEC_PER_SEC;
// 2. 更新桶内令牌(防止溢出)
__u64 current_tokens = state->tokens + new_tokens;
if (current_tokens > BUCKET_CAPACITY) {
current_tokens = BUCKET_CAPACITY;
}
// 3. 校验并扣减令牌
if (current_tokens < pkt_len) {
// 令牌不足,执行限速丢弃
// 同时可以考虑在此处将恶意 IP 直接加入 XDP 共享的黑名单 Map 中
return TC_ACT_SHOT;
}
// 4. 更新状态
state->tokens = current_tokens - pkt_len;
state->last_time = now;
return TC_ACT_OK;
}
char _license[] SEC("license") = "GPL";
注意:上述实现在高度并发时可能存在轻微的竞态条件(Race Condition),在极高并发要求下,可以使用 BPF_MAP_TYPE_PERCPU_HASH 避免多核冲突,或者在支持的内核版本上使用 bpf_spin_lock。
四、 协同演练:XDP 动态黑名单联动
当 tc BPF 发现某个 IP 频繁触发限速(例如在 1 秒内被丢包超过 100 次),它会直接将该 IP 写入另一个共享 Map:blacklist_map。
1. 共享黑名单 Map
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 500000);
__type(key, __u32); // IP Address
__type(value, __u64); // Block expire timestamp (ns)
} blacklist_map SEC(".maps");
2. XDP 端的极速拦截
在最前端挂载的 XDP 程序,其逻辑必须保持绝对纯粹和高效,只做黑名单查询和快速丢弃:
SEC("xdp")
int xdp_filter(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_DROP;
if (eth->h_proto != __constant_htons(ETH_P_IP))
return XDP_PASS;
struct iphdr *iph = (void *)(eth + 1);
if ((void *)(iph + 1) > data_end)
return XDP_DROP;
__u32 src_ip = iph->saddr;
// 查询黑名单
__u64 *expire = bpf_map_lookup_elem(&blacklist_map, &src_ip);
if (expire) {
if (bpf_ktime_get_ns() < *expire) {
return XDP_DROP; // 仍在惩罚期内,直接在最底层丢弃!
}
// 过期了,清理黑名单
bpf_map_delete_elem(&blacklist_map, &src_ip);
}
return XDP_PASS;
}
五、 生产环境下的避坑指南与调优细节
Map 类型的合理选择
- LRU Map 的开销:在高 PPS 下,普通
BPF_MAP_TYPE_HASH在容量满时的清理成本极高。BPF_MAP_TYPE_LRU_HASH虽有 LRU 链表的维护开销,但能有效防止哈希表爆满导致的软锁死。 - 对于计数器这类不需要跨核精确同步的数据,优先使用
BPF_MAP_TYPE_PERCPU_ARRAY。
- LRU Map 的开销:在高 PPS 下,普通
JIT 编译与 Helper 函数开销
- 生产环境中必须开启 BPF JIT 编译(
sysctl -w net.core.bpf_jit_enable=1)。 - 在 XDP 中尽量减少对
bpf_ktime_get_ns()的频繁调用,该 Helper 函数会读取硬件时钟源,高频调用下有可观测的 CPU 开销。可通过合并逻辑或粗粒度时间戳进行优化。
- 生产环境中必须开启 BPF JIT 编译(
网卡驱动与 10G/40G/100G 网卡适配
- 要发挥 XDP 极致性能,必须运行在 Native Mode(驱动模式)。确保网卡驱动(如
ixgbe,i40e,mlx5_core)支持 Native XDP。 - 如果是在云虚拟化环境,可能只能运行在 Generic Mode。此时 XDP 性能会大幅下滑,因为
sk_buff已经在进入 XDP 之前被分配了。
- 要发挥 XDP 极致性能,必须运行在 Native Mode(驱动模式)。确保网卡驱动(如
与内核路由表的联动
- 在 tc 层级,如果需要做更高级的 ACL,可以调用
bpf_fib_lookup直接在 eBPF 中快速查找路由,避免将脏流量送入传统路由协议栈,进一步减少开销。
- 在 tc 层级,如果需要做更高级的 ACL,可以调用
总结
XDP 与 tc BPF 并非非此即彼的关系。在构建现代化高防网络架构时,XDP 扮演的是高性能防火墙的“外壳”,阻挡绝大多数泛洪型无状态攻击;而 tc BPF 则是内部精细化管理的“内核”,负责有状态的流控、QoS 和限速。 通过共享 BPF Map 建立的数据反馈环,两层防线有机地结合在一起,这正是大厂构建百 G 级高防系统的核心技术底座。