Featured image of post eBPF例程——TC

eBPF例程——TC

基于libbpf编写的eBPF/TC程序,示例在TC的入方向捕获到IP数据包,并解析其长度与TTL字段,并将信息输出至系统日志。

TC

在Linux网络子系统中,TC(Traffic Control)是一个非常核心的流量控制框架。从协议栈上看,TC位于链路层,其所在位置已经完成了sk_buff的分配(因此要晚于XDP)。它用于对网络数据包进行队列管理、流量整形(shaping)、优先级调度、限速等操作。传统的TC工作流程是在网卡的ingress(入方向)或egress(出方向)挂载qdisc(队列规则),然后通过filter(过滤器)来匹配和处理数据包。

随着eBPF的发展,TC引入了对eBPF程序的支持,这让开发者可以将自定义的BPF程序作为filter挂载到ingress和egress。在网络数据包到达网卡(或即将发出)时,就可以触发运行eBPF程序,实现灵活的包过滤、修改、统计、监控、重定向等功能。

运行eBPF程序

本示例用TC在本地回环接口的Ingress方向附加了一个BPF程序。BPF程序捕获到进入回环接口的IP包时,用bpf_printk()函数打印数据包的长度与TTL编译并运行eBPF程序。

1
2
make tc
sudo ./tc

eBPF将数据包信息输出到日志,可以通过以下命令实时查看。

1
sudo cat /sys/kernel/debug/tracing/trace_pipe

eBPF程序:tc.bpf.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <vmlinux.h>
#include <bpf/bpf_endian.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

// TC返回值,0表示包正常放行
#define TC_ACT_OK 0
// IPv4的以太网协议类型
#define ETH_P_IP  0x0800

// 标记tc_ingress是一个挂载到TC的eBPF程序
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;

    // 判断协议类型是否是IPv4
    if (ctx->protocol != bpf_htons(ETH_P_IP))
        // 不是IPv4的数据包,直接放行
        return TC_ACT_OK;

    // 解析以太网头部
    l2 = data;
    // 判断读取完整以太网头是否越界,若越界就不处理
    if ((void *)(l2 + 1) > data_end)
        return TC_ACT_OK;

    // 解析IPv4头部
    l3 = (struct iphdr *)(l2 + 1);
    // 判断读取完整IPv4头是否越界,若越界就不处理
    if ((void *)(l3 + 1) > data_end)
        return TC_ACT_OK;

    // 输出IP包的字段信息
    bpf_printk("Got IP packet: tot_len: %d, ttl: %d", bpf_ntohs(l3->tot_len), l3->ttl);
    return TC_ACT_OK;
}

char __license[] SEC("license") = "GPL";

用户态程序:tc.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#include <signal.h>
#include <unistd.h>
#include "tc.skel.h"

// 环回设备的接口索引
#define LO_IFINDEX 1

// 标记程序是否收到退出信号
static volatile sig_atomic_t exiting = 0;

// 信号处理回调函数
static void sig_int(int signo)
{
    exiting = 1;
}

// libbp日志打印回调函数
static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
    return vfprintf(stderr, format, args);
}

int main(int argc, char **argv)
{
    // 描述定义了名为tc_hook的TC挂载点,其网络接口索引为LO_IFINDEX,挂载点为入方向
    DECLARE_LIBBPF_OPTS(bpf_tc_hook, tc_hook, .ifindex = LO_IFINDEX, .attach_point = BPF_TC_INGRESS);
    // 描述定义了名为tc_opts的附加选项,设置了TC过滤器的句柄标识符为1、优先级为1
    DECLARE_LIBBPF_OPTS(bpf_tc_opts, tc_opts, .handle = 1, .priority = 1);
    bool hook_created = false;
    struct tc_bpf *skel;
    int err;

    // 设置libbp日志打印的回调函数
    libbpf_set_print(libbpf_print_fn);

    // 打开并加载eBPF程序
    skel = tc_bpf__open_and_load();
    if (!skel) {
        fprintf(stderr, "Failed to open BPF skeleton\n");
        return 1;
    }
    
    // 创建TC钩子,即创建qdisc
    err = bpf_tc_hook_create(&tc_hook);
    if (!err)
        hook_created = true;
    if (err && err != -EEXIST) {
        fprintf(stderr, "Failed to create TC hook: %d\n", err);
        goto cleanup;
    }

    // 获取eBPF程序tc_ingress的文件描述符,并赋值到tc_opts结构体中
    tc_opts.prog_fd = bpf_program__fd(skel->progs.tc_ingress);
    // 根据tc_opts,将eBPF程序tc_ingress附加到名为tc_hook的TC钩子上
    err = bpf_tc_attach(&tc_hook, &tc_opts);
    if (err) {
        fprintf(stderr, "Failed to attach TC: %d\n", err);
        goto cleanup;
    }

    // 设置SIGINT信号处理
    if (signal(SIGINT, sig_int) == SIG_ERR) {
        err = errno;
        fprintf(stderr, "Can't set signal handler: %s\n", strerror(errno));
        goto cleanup;
    }

    printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "
           "to see output of the BPF program.\n");

    // 主循环
    while (!exiting) {
        fprintf(stderr, ".");
        sleep(1);
    }

    // 清空tc_opts中的字段
    tc_opts.flags = tc_opts.prog_fd = tc_opts.prog_id = 0;
    // 将eBPF程序从TC钩子上卸载掉
    err = bpf_tc_detach(&tc_hook, &tc_opts);
    if (err) {
        fprintf(stderr, "Failed to detach TC: %d\n", err);
        goto cleanup;
    }

cleanup:
    // 清楚之前创建的TC钩子
    if (hook_created)
        bpf_tc_hook_destroy(&tc_hook);
    // 清理eBPF环境
    tc_bpf__destroy(skel);
    return -err;
}
Licensed under CC BY-NC-SA 4.0
皖ICP备2025083746号-1
公安备案 陕公网安备61019002003315号



使用 Hugo 构建
主题 StackJimmy 设计