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 子系统由三个核心组件构成:
1.2 TC eBPF 的位置
| 特性 | XDP | TC eBPF |
|---|---|---|
| 执行位置 | 网卡驱动层 | 内核协议栈入口/出口 |
| 数据结构 | xdp_md | sk_buff |
| 方向 | 仅 ingress | ingress + 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 返回动作
| 返回值 | 名称 | 语义 |
|---|---|---|
| 0 | TC_ACT_OK | 继续处理,允许通过 |
| 1 | TC_ACT_SHOT | 丢弃数据包 |
| 2 | TC_ACT_STOLEN | 数据包已被消费(由 eBPF 处理) |
| 3 | TC_ACT_REDIRECT | 重定向到其他接口 |
| 4 | TC_ACT_PIPE | 继续下一个过滤器 |
| 7 | TC_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;}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 | __u32 | Socket 标记 |
| ifindex | __u32 | 接口索引 |
| protocol | __u32 | 协议类型 |
| priority | __u32 | 优先级 |
| cb[5] | __u32 | 控制缓冲区(可存储自定义数据) |
| tc_index | __u16 | TC 索引 |
| tc_classid | __u16 | TC 类别 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
Direct Action(DA)模式是 TC eBPF 的推荐方式——eBPF 程序直接返回动作,无需额外的分类-动作映射:
# 附加 TC eBPF 程序(Direct Action 模式)sudo tc qdisc add dev eth0 clsactsudo tc filter add dev eth0 ingress bpf da obj tc_prog.o sec tc-ingresssudo tc filter add dev eth0 egress bpf da obj tc_prog.o sec tc-egress3.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;}cb 字段只在同一 sk_buff 的生命周期内有效。数据包经过路由或隧道封装后,cb 内容可能被覆盖。如果需要跨封装传递元数据,应使用 skb->mark 或自定义外层协议头。
4.4 TC 与 Netfilter 对比
TC eBPF 和 Netfilter(iptables/nftables)都能在内核网络栈中做包过滤和 NAT,但架构差异显著:
| 维度 | TC eBPF | Netfilter (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 |
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 管线
5.2 选择策略
| 场景 | 选择 | 原因 |
|---|---|---|
| DDoS 防护 | XDP | 需要最低延迟的丢弃 |
| L3/L4 负载均衡 | XDP | 高 PPS,简单转发 |
| 连接跟踪 | TC | 需要 sk_buff 信息 |
| NAT | TC | 需要修改 sk_buff 元数据 |
| L7 策略 | TC | 需要解析应用层协议 |
| egress 处理 | TC | XDP 不支持 egress |
| 快速丢弃 | XDP | 驱动层丢弃,零开销 |
不要在 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 ingresssudo tc filter show dev eth0 egress
# 删除所有 TC 过滤器sudo tc filter del dev eth0 ingresssudo tc filter del dev eth0 egress
# 删除 clsact qdiscsudo tc qdisc del dev eth0 clsact6.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 qdiscsudo 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 ingresssudo tc qdisc del dev eth0 clsact7.2 TC ingress + egress 双向处理
# 附加 ingress 和 egresssudo tc qdisc add dev eth0 clsactsudo tc filter add dev eth0 ingress bpf da obj tc_prog.o sec tc-insudo tc filter add dev eth0 egress bpf da obj tc_prog.o sec tc-out
# 查看双向过滤器sudo tc filter show dev eth0 ingresssudo tc filter show dev eth0 egress7.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);}'八、本章小结
上一章了解了XDP 高性能数据包处理。 本章详解了 TC 层 eBPF 的完整技术栈:
| 主题 | 核心要点 | 关键词 |
|---|---|---|
| TC 架构 | Qdisc + Class + Filter 三层模型,cls_bpf 将 eBPF 作为分类器 | TC 架构 |
| Direct Action | eBPF 程序直接返回动作,简化配置,提升性能 | Direct Action |
| sk_buff 访问 | 比 XDP 的 xdp_md 更丰富,包含路由、Socket、连接跟踪信息 | sk_buff 访问 |
| ingress/egress | TC 支持 ingress 和 egress 双向处理,XDP 仅支持 ingress | ingress/egress |
| 与 XDP 协作 | XDP 负责快速路径(DDoS/LB),TC 负责慢速路径(NAT/策略) | 与 XDP 协作 |
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






