找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1167

积分

0

好友

167

主题
发表于 前天 18:06 | 查看: 5| 回复: 0

Traffic Control (TC,流量控制) 允许我们在网络数据包进入或离开网络接口时,运行可编程的逻辑。eBPF程序可以附加到TC的钩子点上,实现对网络流量的深度监控与处理。

本文将以一个简单的示例程序入手,解析其工作原理与挂载流程。

eBPF 程序解析

参考示例:https://github.com/libbpf/libbpf-bootstrap/blob/master/examples/c/tc.bpf.c

SEC(“tc”)
int tc_ingress(struct __sk_buff *ctx) {
    void *data_end = (void *)(__u64)ctx->data_end;
    void *data = (void *)(__u64)ctx->data;
    struct ethhdr *l2;
    struct iphdr *l3;

    if (ctx->protocol != bpf_htons(ETH_P_IP))
        return TC_ACT_OK;

    l2 = data;
    if ((void *)(l2 + 1) > data_end)
        return TC_ACT_OK;

    l3 = (struct iphdr *)(l2 + 1);
    if ((void *)(l3 + 1) > data_end)
        return TC_ACT_OK;

    bpf_printk(“Got IP packet: tot_len: %d, ttl: %d”, bpf_ntohs(l3->tot_len), l3->ttl);
    return TC_ACT_OK;
}

示例程序中,section 被定义为 SEC(“tc”)。根据 libbpf 的源码定义tcclassifier 都是历史遗留的等价定义,现代写法更推荐使用 tcx

程序逻辑本身并不复杂,但必须重视对数据包长度(如 datadata_end)的边界检查。因为 eBPF 校验器在编译和加载时会强制验证这些安全检查逻辑,缺少它们将导致程序加载失败。

此外,细心的读者可能会发现,与 socketfilter 类型的程序不同,这里的程序直接通过结构体指针(如 l3->tot_len)访问数据包字段。这背后的原因与程序类型的安全模型有关:

  • Socket Filter (套接字过滤器):这类程序最初设计允许非特权用户加载过滤规则。直接解引用复杂的内核结构体指针存在安全风险,例如可能访问到未初始化或内核内部的敏感字段。因此,访问数据包内容必须通过 Helper Functions 进行。
  • TC (流量控制):它运行在 struct __sk_buff 上下文(在 Linux 内核源码中定义)中。内核为 TC 程序显式暴露了该结构体中部分被认为是安全且稳定的字段,允许直接访问(例如 ifindexpriority 等)。对于修改数据包或访问非线性数据等更复杂的操作,则仍需调用特定的 Helper Functions。

用户态挂载逻辑分析

用户态程序负责将编译好的 eBPF 程序加载并附加到目标网络接口上。核心步骤如下:

首先,定义一个 bpf_tc_hook,指定要附加的网络接口和方向。

DECLARE_LIBBPF_OPTS(bpf_tc_hook, tc_hook,
                    .ifindex = LO_IFINDEX,
                    .attach_point = BPF_TC_INGRESS);
  • .ifindex = LO_IFINDEX: 指定要挂载 eBPF 程序的网络接口索引。LO_IFINDEX 通常指回环接口 lo
  • .attach_point = BPF_TC_INGRESS: 指定挂载方向为入口(Ingress),即在网卡接收数据包时触发程序执行。

接着,配置 bpf_tc_opts,设置优先级和句柄。

DECLARE_LIBBPF_OPTS(bpf_tc_opts, tc_opts,
                    .handle = 1,
                    .priority = 1);
  • .priority (优先级):决定多个过滤器(Filter)的执行顺序。数值越小,优先级越高
    • 内核会先执行 priority = 1 的程序。
    • 如果该程序返回 TC_ACT_OK 或未匹配,内核才会继续执行 priority = 2 的程序,依此类推。
    • 若前面的程序决定丢弃 (TC_ACT_SHOT),后续程序将无法看到该数据包。
  • .handle (句柄):在同一优先级下,用于唯一标识一个具体的规则实例。如果设为 0,内核会分配一个随机 ID。若需更新正在运行的程序(例如修复 Bug),可以用相同的 .priority.handle 配合 BPF_TC_F_REPLACE 标志重新加载,内核会以原子方式替换旧程序,避免流量中断或规则重复。

然后,调用 bpf_tc_hook_create 创建 TC 钩子。

err = bpf_tc_hook_create(&tc_hook);

这个函数是理解TC挂载底层原理的关键。它本质上是对 Netlink 协议的封装,用于配置 Linux 内核的流量控制子系统。Netlink 是Linux内核与用户空间进行通信的专用 IPC 机制。

bpf_tc_hook_create 的实现(通常在 libbpf 的 src/net.c 中)会构建一个 Netlink 消息,其核心是填充 struct tcmsg 结构,并通过 TLV (Type-Length-Value) 格式附加属性。关键步骤包括:

  1. 设置消息类型 RTM_NEWQDISC,表示要创建一个新的队列规则 (qdisc)。
  2. tcmsg 中,将 tcm_parent 设置为 TC_H_CLSACT。这告知内核要创建的是一个专为 eBPF 设计的、高效的 clsact qdisc。
  3. 添加 TCA_KIND 属性,值为 ”clsact”,明确指定 qdisc 的类型。

整个函数的目的,就是将用户态的配置请求“翻译”成内核能理解的 Netlink 数据结构,并通过 socket 发送给内核,从而完成对网络配置的修改。

最后,将 eBPF 程序附加到创建好的钩子上。

tc_opts.prog_fd = bpf_program__fd(skel->progs.tc_ingress);
err = bpf_tc_attach(&tc_hook, &tc_opts);
// … 程序运行 …
err = bpf_tc_detach(&tc_hook, &tc_opts); // 卸载程序

bpf_program__fd 从 eBPF 骨架对象中获取编译好的程序文件描述符(FD)。bpf_tc_attach 最终将这个 FD 与之前配置好的 TC 钩子关联起来。对应的 bpf_tc_detach 则用于卸载程序。




上一篇:S7-200 SMART运动控制实战指南:从向导配置到GOTO指令详解
下一篇:RDMA乱序传输支持深度解析:高性能网络中的多路径与AR问题处理
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-17 17:29 , Processed in 0.117651 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表