mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1982 字
6 分钟
TC:流量控制与 eBPF
2026-03-14

XDP 在驱动层提供了极致的性能,但它也有局限——无法访问完整的 sk_buff 信息、无法做 egress 处理、无法利用内核协议栈的辅助功能。TC(Traffic Control)层的 eBPF 弥补了这些不足:它在 sk_buff 分配之后执行,可以访问完整的网络栈上下文,支持 ingress 和 egress 双向处理。

Cilium 的核心 eBPF 管线就同时使用了 XDP 和 TC——XDP 负责快速丢弃和负载均衡,TC 负责连接跟踪、NAT 和策略执行。

一、TC 子系统概述#

1.1 TC 的架构#

Linux TC 子系统由三个核心组件构成:

flowchart TB subgraph TC架构 Q["Qdisc(排队规则)<br/>管理数据包的排队与调度"] C["Class(类别)<br/>将数据包分类到不同队列"] F["Filter/Classifier(过滤器)<br/>决定数据包属于哪个类别"] end PKT_IN["入方向数据包"] --> INGRESS["ingress qdisc"] INGRESS -->|"cls_bpf"| BPF_IN["eBPF 分类器<br/>ingress 处理"] BPF_IN --> STACK["内核协议栈"] STACK --> OUT["出方向数据包"] OUT --> EGRESS["egress qdisc"] EGRESS -->|"cls_bpf"| BPF_OUT["eBPF 分类器<br/>egress 处理"] BPF_OUT --> NIC["网卡发送"]

1.2 TC eBPF 的位置#

特性XDPTC eBPF
执行位置网卡驱动层内核协议栈入口/出口
数据结构xdp_mdsk_buff
方向仅 ingressingress + egress
可访问信息仅数据包内容完整 sk_buff(路由、Socket 等)
可修改内容数据包头部数据包 + sk_buff 元数据
性能极高
灵活性

二、TC eBPF 程序#

2.1 程序类型#

TC eBPF 使用 BPF_PROG_TYPE_SCHED_CLS 程序类型:

#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
SEC("tc")
int tc_ingress(struct __sk_buff *skb)
{
// 可以访问完整的 sk_buff 信息
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
// 解析数据包
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return TC_ACT_OK;
// 处理逻辑...
return TC_ACT_OK; // 继续处理
}
char LICENSE[] SEC("license") = "GPL";

2.2 TC 返回动作#

返回值名称语义
0TC_ACT_OK继续处理,允许通过
1TC_ACT_SHOT丢弃数据包
2TC_ACT_STOLEN数据包已被消费(由 eBPF 处理)
3TC_ACT_REDIRECT重定向到其他接口
4TC_ACT_PIPE继续下一个过滤器
7TC_ACT_UNSPEC未指定动作

2.3 TC 辅助函数#

TC eBPF 程序可以使用一系列专用辅助函数来操作数据包和 sk_buff:

辅助函数功能典型用途
bpf_skb_store_bytes()修改数据包内容改写 IP/端口字段
bpf_skb_load_bytes()读取数据包内容解析深层协议头
bpf_l3_csum_replace()替换 L3 校验和修改 IP 地址后更新校验和
bpf_l4_csum_replace()替换 L4 校验和修改端口后更新 TCP/UDP 校验和
bpf_skb_change_proto()修改协议类型IPv4↔IPv6 转换
bpf_skb_change_tail()修改数据包长度增删协议头
bpf_skb_change_head()推入新头部封装 VXLAN/Geneve
bpf_skb_pull_data()拉取数据到线性区访问非线性区(分片)数据
bpf_redirect()重定向到指定网卡跨接口转发
bpf_redirect_neigh()重定向并做邻居查找直接路由转发
bpf_redirect_peer()对端命名空间重定向容器 veth 对加速
bpf_clone_redirect()克隆并重定向镜像流量到采集口
bpf_csum_diff()增量校验和计算NAT 场景高效更新校验和
bpf_skb_set_tunnel_key()设置隧道元数据封装隧道外层信息
bpf_skb_get_tunnel_key()读取隧道元数据解封装时获取内层信息

2.4 TC 重定向实战#

bpf_redirect()bpf_redirect_neigh() 是 TC 层实现跨接口转发的核心手段,Cilium 大量使用它们完成 Pod 到 Pod、Pod 到 Node 的数据包路由:

// 将数据包重定向到另一个网卡接口
struct {
__uint(type, BPF_MAP_TYPE_DEVMAP);
__uint(max_entries, 64);
__type(key, __u32); // ifindex
__type(value, __u32); // redirect ifindex
} redirect_map SEC(".maps");
SEC("tc")
int tc_redirect(struct __sk_buff *skb)
{
// 查找目标接口
__u32 *target_ifindex = bpf_map_lookup_elem(&redirect_map, &skb->ifindex);
if (target_ifindex)
return bpf_redirect(*target_ifindex, 0);
// redirect_neigh:自动做邻居解析,适合 L3 转发
// return bpf_redirect_neigh(*target_ifindex, NULL, 0, 0);
return TC_ACT_OK;
}
Note

bpf_redirect() 直接将数据包发到目标网卡,不做邻居解析;bpf_redirect_neigh() 会自动查找邻居表(ARP/NDP),适合 L3 路由转发场景。bpf_redirect_peer() 专用于 veth 对,可将数据包直接送到对端命名空间的 ingress 队列,跳过宿主机协议栈。

2.5 sk_buff 结构#

TC eBPF 程序可以访问 struct __sk_buff 的丰富信息:

字段类型说明
len__u32数据包总长度
pkt_type__u32包类型(BROADCAST/MULTICAST/HOST 等)
mark__u32Socket 标记
ifindex__u32接口索引
protocol__u32协议类型
priority__u32优先级
cb[5]__u32控制缓冲区(可存储自定义数据)
tc_index__u16TC 索引
tc_classid__u16TC 类别 ID
data__u32数据起始地址
data_end__u32数据结束地址
family__u32地址族(AF_INET/AF_INET6)
remote_ip4__u32远端 IPv4 地址
local_ip4__u32本地 IPv4 地址
remote_port__u32远端端口
local_port__u32本地端口

三、Direct Action 模式#

3.1 传统模式 vs Direct Action#

flowchart LR subgraph 传统模式["传统模式(分类 + 动作分离)"] F1["Filter 1<br/>匹配规则"] -->|"分类结果"| C1["Class 1<br/>执行动作"] F2["Filter 2<br/>匹配规则"] -->|"分类结果"| C2["Class 2<br/>执行动作"] end subgraph DA模式["Direct Action 模式(分类即动作)"] F3["eBPF Filter<br/>匹配 + 返回动作"] end

Direct Action(DA)模式是 TC eBPF 的推荐方式——eBPF 程序直接返回动作,无需额外的分类-动作映射:

# 附加 TC eBPF 程序(Direct Action 模式)
sudo tc qdisc add dev eth0 clsact
sudo tc filter add dev eth0 ingress bpf da obj tc_prog.o sec tc-ingress
sudo tc filter add dev eth0 egress bpf da obj tc_prog.o sec tc-egress

3.2 DA 模式的优势#

优势说明
简化配置一个 eBPF 程序完成分类和动作
性能提升减少分类-动作映射的间接层
灵活性eBPF 程序可以实现任意复杂的分类逻辑
可编程动态调整策略无需修改 TC 配置

四、TC eBPF 实战#

4.1 入方向防火墙#

#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
// 规则:源 IP + 端口 → 动作
struct rule_key {
__u32 src_ip;
__u16 dst_port;
__u8 protocol;
__u8 pad;
};
struct rule_value {
__u32 action; // TC_ACT_OK or TC_ACT_SHOT
};
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 65536);
__type(key, struct rule_key);
__type(value, struct rule_value);
} firewall_rules SEC(".maps");
SEC("tc")
int tc_ingress_firewall(struct __sk_buff *skb)
{
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return TC_ACT_OK;
if (eth->h_proto != bpf_htons(ETH_P_IP))
return TC_ACT_OK;
struct iphdr *iph = (void *)(eth + 1);
if ((void *)(iph + 1) > data_end)
return TC_ACT_OK;
// 解析 TCP/UDP 端口
struct rule_key key = {};
key.src_ip = iph->saddr;
key.protocol = iph->protocol;
if (iph->protocol == IPPROTO_TCP) {
struct tcphdr *tcp = (void *)(iph + 1);
if ((void *)(tcp + 1) > data_end)
return TC_ACT_OK;
key.dst_port = tcp->dest;
} else if (iph->protocol == IPPROTO_UDP) {
struct udphdr *udp = (void *)(iph + 1);
if ((void *)(udp + 1) > data_end)
return TC_ACT_OK;
key.dst_port = udp->dest;
}
// 查找规则
struct rule_value *rule = bpf_map_lookup_elem(&firewall_rules, &key);
if (rule)
return rule->action;
// 默认允许
return TC_ACT_OK;
}
char LICENSE[] SEC("license") = "GPL";

4.2 出方向 NAT#

SEC("tc")
int tc_egress_nat(struct __sk_buff *skb)
{
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return TC_ACT_OK;
if (eth->h_proto != bpf_htons(ETH_P_IP))
return TC_ACT_OK;
struct iphdr *iph = (void *)(eth + 1);
if ((void *)(iph + 1) > data_end)
return TC_ACT_OK;
// SNAT:修改源 IP
__u32 orig_src = iph->saddr;
__u32 *new_src = bpf_map_lookup_elem(&nat_map, &orig_src);
if (new_src) {
// 修改源 IP
iph->saddr = *new_src;
// 重新计算 IP 校验和
iph->check = 0;
iph->check = bpf_csum_diff(0, 0, (void *)iph, sizeof(*iph), 0);
// 重新计算 TCP/UDP 校验和
if (iph->protocol == IPPROTO_TCP) {
struct tcphdr *tcp = (void *)(iph + 1);
if ((void *)(tcp + 1) > data_end)
return TC_ACT_OK;
tcp->check = bpf_csum_diff(&orig_src, 4, new_src, 4, tcp->check);
}
}
return TC_ACT_OK;
}

4.3 使用 sk_buff cb 字段传递数据#

sk_buff->cb[5] 是 TC eBPF 程序间传递元数据的关键机制——ingress 程序写入,egress 程序读取,全程无需额外 Map 查找。Cilium 正是利用 cb 字段在 ingress 标记连接跟踪结果,egress 阶段直接读取,避免重复解析:

// ingress 程序:解析并标记
SEC("tc")
int tc_ingress_mark(struct __sk_buff *skb)
{
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return TC_ACT_OK;
// cb[0]:连接跟踪 ID(从 conntrack Map 查得)
// cb[1]:身份标识(Cilium security ID)
// cb[2]:NAT 标记(是否已做 DNAT)
skb->cb[0] = conntrack_id;
skb->cb[1] = sec_id;
skb->cb[2] = 0; // 未做 DNAT
return TC_ACT_OK;
}
// egress 程序:读取标记并执行策略
SEC("tc")
int tc_egress_policy(struct __sk_buff *skb)
{
__u32 ct_id = skb->cb[0];
__u32 sec_id = skb->cb[1];
__u32 nat_flag = skb->cb[2];
// 根据 ingress 阶段的标记做策略判断
if (sec_id == BLOCKED_ID)
return TC_ACT_SHOT;
// 如果未做 DNAT,egress 阶段需要补 SNAT
if (!nat_flag)
do_snat(skb, ct_id);
return TC_ACT_OK;
}
Note

cb 字段只在同一 sk_buff 的生命周期内有效。数据包经过路由或隧道封装后,cb 内容可能被覆盖。如果需要跨封装传递元数据,应使用 skb->mark 或自定义外层协议头。

4.4 TC 与 Netfilter 对比#

TC eBPF 和 Netfilter(iptables/nftables)都能在内核网络栈中做包过滤和 NAT,但架构差异显著:

维度TC eBPFNetfilter (iptables/nftables)
匹配复杂度O(1) Map 查找O(n) 规则遍历(iptables)/ O(1) 集合查找(nftables)
可编程性完全可编程(C/Go/Rust)声明式规则,受限于语法
egress 支持原生支持通过 POSTROUTING 等钩子支持
连接跟踪自定义 LRU Map内核 conntrack 子系统
规则更新Map 原子更新,毫秒级iptables-restore 全量替换,秒级
多核扩展Per-CPU Map,无锁全局 xt_table 锁竞争
生态兼容需自建工具链成熟,大量运维工具
学习曲线陡峭(需理解 eBPF 开发)平缓(声明式语法)
典型用户Cilium、Katran、Merbridge传统运维、Firewalld、Kube-proxy
Warning

TC eBPF 和 Netfilter 钩子可以同时生效,但执行顺序和交互可能导致难以调试的问题。Cilium 在 kubeProxyReplacement=strict 模式下会跳过 Netfilter 钩子,避免两者冲突。

4.5 TC eBPF 尾调用#

当单个 TC 程序逻辑过于复杂时,可以用尾调用(tail call)将处理拆分为多个程序,通过 BPF_MAP_TYPE_PROG_ARRAY 串联:

struct {
__uint(type, BPF_MAP_TYPE_PROG_ARRAY);
__uint(max_entries, 8);
__type(key, __u32);
__type(value, __u32); // program fd
} tc_progs SEC(".maps");
// 主程序:解析后分发
SEC("tc")
int tc_dispatcher(struct __sk_buff *skb)
{
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return TC_ACT_OK;
if (eth->h_proto == bpf_htons(ETH_P_IP))
bpf_tail_call(skb, &tc_progs, 0); // → IPv4 处理
else if (eth->h_proto == bpf_htons(ETH_P_IPV6))
bpf_tail_call(skb, &tc_progs, 1); // → IPv6 处理
return TC_ACT_OK; // 兜底:尾调用失败则放行
}

尾调用不会返回——被调用程序完全替代当前程序执行,栈帧被复用,因此不存在递归栈溢出风险。内核限制最多 33 层尾调用嵌套,实际使用中 2-3 层即可覆盖绝大多数场景。

五、TC 与 XDP 的协作#

5.1 Cilium 的 XDP + TC 管线#

flowchart TB NIC["网卡"] --> XDP["XDP 程序<br/>DDoS 防护<br/>NodePort 加速<br/>L3/L4 负载均衡"] XDP -->|"XDP_PASS"| TC_IN["TC ingress<br/>连接跟踪<br/>NAT<br/>策略执行<br/>L4/L7 过滤"] TC_IN --> STACK["内核协议栈"] STACK --> TC_OUT["TC egress<br/>SNAT<br/>策略执行"] TC_OUT --> SEND["网卡发送"]

5.2 选择策略#

场景选择原因
DDoS 防护XDP需要最低延迟的丢弃
L3/L4 负载均衡XDP高 PPS,简单转发
连接跟踪TC需要 sk_buff 信息
NATTC需要修改 sk_buff 元数据
L7 策略TC需要解析应用层协议
egress 处理TCXDP 不支持 egress
快速丢弃XDP驱动层丢弃,零开销
Warning

不要在 XDP 和 TC 中重复处理同一逻辑。XDP 负责”快速路径”(简单、高频),TC 负责”慢速路径”(复杂、需要完整上下文)。两者协作而非竞争。

六、TC eBPF 的管理#

6.1 使用 tc 命令管理#

# 创建 clsact qdisc(必须先创建)
sudo tc qdisc add dev eth0 clsact
# 附加 ingress 程序
sudo tc filter add dev eth0 ingress bpf da obj tc_prog.o sec tc-ingress
# 附加 egress 程序
sudo tc filter add dev eth0 egress bpf da obj tc_prog.o sec tc-egress
# 查看已附加的程序
sudo tc filter show dev eth0 ingress
sudo tc filter show dev eth0 egress
# 删除所有 TC 过滤器
sudo tc filter del dev eth0 ingress
sudo tc filter del dev eth0 egress
# 删除 clsact qdisc
sudo tc qdisc del dev eth0 clsact

6.2 使用 bpftool 管理#

# 查看 TC 类型的 eBPF 程序
sudo bpftool prog show type sched_cls
# 查看程序详情
sudo bpftool prog show id <id>
# 查看程序字节码
sudo bpftool prog dump xlated id <id>

七、动手实践#

7.1 部署 TC 防火墙#

# 编译 TC eBPF 程序
clang -O2 -target bpf -c tc_firewall.bpf.c -o tc_firewall.bpf.o
# 创建 clsact qdisc
sudo tc qdisc add dev eth0 clsact
# 附加 ingress 程序
sudo tc filter add dev eth0 ingress \
prio 1 handle 1 bpf da obj tc_firewall.bpf.o sec tc-ingress
# 查看附加结果
sudo tc filter show dev eth0 ingress
# 测试:添加黑名单规则(通过用户态程序更新 Map)
# 清理
sudo tc filter del dev eth0 ingress
sudo tc qdisc del dev eth0 clsact

7.2 TC ingress + egress 双向处理#

# 附加 ingress 和 egress
sudo tc qdisc add dev eth0 clsact
sudo tc filter add dev eth0 ingress bpf da obj tc_prog.o sec tc-in
sudo tc filter add dev eth0 egress bpf da obj tc_prog.o sec tc-out
# 查看双向过滤器
sudo tc filter show dev eth0 ingress
sudo tc filter show dev eth0 egress

7.3 使用 bpftrace 追踪 TC 处理#

# 追踪 TC 分类器调用
sudo bpftrace -e 'kprobe:cls_bpf_classify {
printf("TC classify: skb=%p\n", arg0);
}'
# 追踪 TC 动作
sudo bpftrace -e 'kretprobe:cls_bpf_classify {
printf("TC action: %d\n", retval);
}'
flowchart TB IN["网卡收包"] --> CLS["clsact 分类"] --> ING["TC ingress Hook<br/>eBPF 程序"] ING --> DROP1{"Drop?"} -->|"是"| D1["丢弃"] DROP1 -->|"否"| FWD["转发/修改"] --> OUT["TC egress Hook"] --> SEND["网卡发包"]

八、本章小结#

上一章了解了XDP 高性能数据包处理。 本章详解了 TC 层 eBPF 的完整技术栈:

主题核心要点关键词
TC 架构Qdisc + Class + Filter 三层模型,cls_bpf 将 eBPF 作为分类器TC 架构
Direct ActioneBPF 程序直接返回动作,简化配置,提升性能Direct Action
sk_buff 访问比 XDP 的 xdp_md 更丰富,包含路由、Socket、连接跟踪信息sk_buff 访问
ingress/egressTC 支持 ingress 和 egress 双向处理,XDP 仅支持 ingressingress/egress
与 XDP 协作XDP 负责快速路径(DDoS/LB),TC 负责慢速路径(NAT/策略)与 XDP 协作

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

TC:流量控制与 eBPF
https://blog.souloss.com/posts/ebpf/ebpf-tc/
作者
Souloss
发布于
2026-03-14
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

相关文章 智能推荐
1
eBPF 与 WebAssembly
eBPF eBPF 提供内核可编程能力,WebAssembly 提供跨平台可移植性——两者的融合会带来什么?本章详解 Wasm-eBPF 项目、用户态 eBPF 运行时、eBPF 程序的 Wasm 封装,以及 eBPF + Wasm 在边缘计算、插件系统、跨平台可观测性中的应用前景。
2
eBPF 与内存管理
eBPF eBPF 程序的内存使用是生产部署中的关键考量——Map 占用多少内存?eBPF 程序的栈空间有多大?容器环境中的 eBPF 内存如何计费?本章详解 eBPF 的内存模型、Map 内存开销计算、容器内存追踪、eBPF-mm 子系统,以及内存限制下的优化策略。
3
eBPF 网络全景
eBPF eBPF 正在重新定义 Linux 网络栈——从连接跟踪、NAT 到 kube-proxy 替代,从 Socket Filter 到 Sk_msg 重定向,eBPF 提供了比 iptables/netfilter 更高性能、更灵活的网络方案。本章从宏观视角展示 eBPF 网络的全景,详解连接跟踪、NAT、kube-proxy 替代、Socket 层 eBPF,并对比 eBPF 与传统网络方案的架构差异。
4
eBPF Hook 点:kprobe/tracepoint/uprobe
eBPF eBPF 程序的价值在于它能挂载到内核的各种检查点——Hook 点。本章详解三大 Hook 机制——kprobe(动态内核函数追踪)、tracepoint(静态追踪点)、uprobe(用户态函数追踪),以及 USDT 静态用户态追踪点,并通过实战代码展示每种 Hook 的使用方式与适用场景。
5
eBPF 可观测性
eBPF eBPF 最大的应用场景是可观测性——零侵入、低开销、内核级的全链路追踪。本章详解三大可观测性工具链——bpftrace(一行命令追踪内核)、BCC(Python 前端 + 丰富工具集)、Beyla(零侵入应用性能监控),并通过实战展示性能分析、分布式追踪、应用性能监控的完整工作流。