searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

EBPF tc编程入门与踩坑记录

2025-06-06 08:26:33
0
0

什么是eBPF和tc?

eBPF简介

eBPF是一种革命性的内核技术,它允许用户在不修改内核源代码或load内核模块的情况下,安全高效地扩展内核功能。eBPF程序运行在内核的沙盒环境中,通过验证器确保其安全性。

tc简介

tc(Traffic Control)是Linux内核提供的网络流量控制工具,用于实现QoS(服务质量)功能,包括流量整形、调度和过滤等。tc提供了丰富的分类器和动作,而eBPF可以作为一种强大的分类器。

为什么选择eBPF+tc?

  1. 高性能:eBPF程序在内核空间运行,avoid了用户空间和内核空间之间的上下文切换

  2. 安全性:所有eBPF程序都必须通过验证器的严格检查

  3. 灵活性:可以动态load和unload,无需重启系统

  4. 可编程性:使用C等高级语言编写,比传统tc命令更强大

开发环境准备

在开始编写eBPF tc程序前,我们需要准备以下环境:

# 安装必要的工具
sudo apt-get update
sudo apt-get install -y clang llvm libbpf-dev libelf-dev build-essential linux-tools-common linux-tools-generic

# 检查内核eBPF支持
sudo grep -i ebpf /boot/config-$(uname -r)

第一个eBPF tc程序

让我们从一个简单的例子开始:统计通过某个网络接口的TCP SYN包数量。

1. 编写eBPF程序

创建文件syn_counter.c

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <bpf/bpf_helpers.h>

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __type(key, __u32);
    __type(value, __u64);
    __uint(max_entries, 1);
} syn_counter SEC(".maps");

SEC("classifier")
int count_syn(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 + sizeof(*eth) > data_end)
        return TC_ACT_OK;
    
    if (eth->h_proto != htons(ETH_P_IP))
        return TC_ACT_OK;
    
    struct iphdr *ip = data + sizeof(*eth);
    if ((void *)ip + sizeof(*ip) > data_end)
        return TC_ACT_OK;
    
    if (ip->protocol != IPPROTO_TCP)
        return TC_ACT_OK;
    
    struct tcphdr *tcp = (void *)ip + sizeof(*ip);
    if ((void *)tcp + sizeof(*tcp) > data_end)
        return TC_ACT_OK;
    
    if (tcp->syn && !tcp->ack) {
        __u32 key = 0;
        __u64 *count = bpf_map_lookup_elem(&syn_counter, &key);
        if (count) {
            (*count)++;
        }
    }
    
    return TC_ACT_OK;
}

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

2. 编译eBPF程序

使用以下命令编译:

clang -O2 -target bpf -c syn_counter.c -o syn_counter.o

3. load eBPF程序到tc

假设我们要监控eth0接口:

# 创建clsact qdisc(如果不存在)
sudo tc qdisc add dev eth0 clsact

# load eBPF程序到ingress方向
sudo tc filter add dev eth0 ingress bpf obj syn_counter.o sec classifier direct-action

# 查看load的filter
sudo tc filter show dev eth0 ingress

4. 查看统计结果

我们可以使用bpftool查看统计结果:

# 首先找到map的id
sudo bpftool map list

# 然后查看map内容(替换上面的map id)
sudo bpftool map dump id <map_id>

eBPF tc编程踩坑点

一般来讲tc编程所需要用的helper函数都能在bpf.h里找到相应的注释说明,但实际代码过程中笔者依然遇到几个较坑的点

1、解析数据包时,skb->data_end-skb->data要小于skb->len,这是由于数据包skb是非线性的,也就是data_end-data其实是线性区的大小,而skb->len是线性区加非线性区的大小,前者其实是skb->data_len,但是BPF程序中无法从__sk_buff中直接拿到这个值,我们可以使用bpf_skb_pull_data(skb,len)直接访问非线性数据,这个helper函数的官方解释如下

  • 如果 skb 是非线性的并且len 由线性和非线性部分组成,则pull入非线性数据,使得 skb 中的 len 字节可读可写,如果为 len 传递了一个零值,则拉取整个 skb 长度。
  • 此 helper 仅用于通过 direct packet access 进行读取和写入。
  • 对于direct packet access,如果偏移量无效,或者如果请求的数据在 skb 的非线性部分中,则访问偏移量在数据包边界内的测试(在 skb->data_end 上测试)很容易失败。失败时,程序可以退出,或者在非线性缓冲区的情况下,使用helper程序使数据可用。 bpf_skb_load_bytes() helper是访问数据的第一个解决方案。另一种方法是使用 bpf_skb_pull_data 拉入一次非线性部分,然后重新测试并最终访问数据。
  • 同时,这也保证了skb是未克隆的,这是direct write的必要条件。由于这仅需要是写入部分的不变量,因此验证程序检测写入并添加一个调用 bpf_skb_pull_data 的prologue​​,以从一开始就有效地取消克隆 skb,以防它确实被克隆。
  • 对这个helper的调用很容易改变底层的数据包缓冲区。因此,在load时,如果helper与direct packet access结合使用,则verifier先前对指针所做的所有检查都将无效并且必须再次执行。

2、tc程序对数据包头部进行剪裁,并load到网卡的ingress方向,使用tcpdump抓包发现报文并未进行相应的修改,原因是入方向tcpdump抓包点要早于tc程序,在tc程序中使用重定向将报文发到另一张网卡再使用tcp dump抓包,可看到报文符合代码处理逻辑。

0条评论
0 / 1000
c****4
4文章数
0粉丝数
c****4
4 文章 | 0 粉丝
原创

EBPF tc编程入门与踩坑记录

2025-06-06 08:26:33
0
0

什么是eBPF和tc?

eBPF简介

eBPF是一种革命性的内核技术,它允许用户在不修改内核源代码或load内核模块的情况下,安全高效地扩展内核功能。eBPF程序运行在内核的沙盒环境中,通过验证器确保其安全性。

tc简介

tc(Traffic Control)是Linux内核提供的网络流量控制工具,用于实现QoS(服务质量)功能,包括流量整形、调度和过滤等。tc提供了丰富的分类器和动作,而eBPF可以作为一种强大的分类器。

为什么选择eBPF+tc?

  1. 高性能:eBPF程序在内核空间运行,avoid了用户空间和内核空间之间的上下文切换

  2. 安全性:所有eBPF程序都必须通过验证器的严格检查

  3. 灵活性:可以动态load和unload,无需重启系统

  4. 可编程性:使用C等高级语言编写,比传统tc命令更强大

开发环境准备

在开始编写eBPF tc程序前,我们需要准备以下环境:

# 安装必要的工具
sudo apt-get update
sudo apt-get install -y clang llvm libbpf-dev libelf-dev build-essential linux-tools-common linux-tools-generic

# 检查内核eBPF支持
sudo grep -i ebpf /boot/config-$(uname -r)

第一个eBPF tc程序

让我们从一个简单的例子开始:统计通过某个网络接口的TCP SYN包数量。

1. 编写eBPF程序

创建文件syn_counter.c

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <bpf/bpf_helpers.h>

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __type(key, __u32);
    __type(value, __u64);
    __uint(max_entries, 1);
} syn_counter SEC(".maps");

SEC("classifier")
int count_syn(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 + sizeof(*eth) > data_end)
        return TC_ACT_OK;
    
    if (eth->h_proto != htons(ETH_P_IP))
        return TC_ACT_OK;
    
    struct iphdr *ip = data + sizeof(*eth);
    if ((void *)ip + sizeof(*ip) > data_end)
        return TC_ACT_OK;
    
    if (ip->protocol != IPPROTO_TCP)
        return TC_ACT_OK;
    
    struct tcphdr *tcp = (void *)ip + sizeof(*ip);
    if ((void *)tcp + sizeof(*tcp) > data_end)
        return TC_ACT_OK;
    
    if (tcp->syn && !tcp->ack) {
        __u32 key = 0;
        __u64 *count = bpf_map_lookup_elem(&syn_counter, &key);
        if (count) {
            (*count)++;
        }
    }
    
    return TC_ACT_OK;
}

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

2. 编译eBPF程序

使用以下命令编译:

clang -O2 -target bpf -c syn_counter.c -o syn_counter.o

3. load eBPF程序到tc

假设我们要监控eth0接口:

# 创建clsact qdisc(如果不存在)
sudo tc qdisc add dev eth0 clsact

# load eBPF程序到ingress方向
sudo tc filter add dev eth0 ingress bpf obj syn_counter.o sec classifier direct-action

# 查看load的filter
sudo tc filter show dev eth0 ingress

4. 查看统计结果

我们可以使用bpftool查看统计结果:

# 首先找到map的id
sudo bpftool map list

# 然后查看map内容(替换上面的map id)
sudo bpftool map dump id <map_id>

eBPF tc编程踩坑点

一般来讲tc编程所需要用的helper函数都能在bpf.h里找到相应的注释说明,但实际代码过程中笔者依然遇到几个较坑的点

1、解析数据包时,skb->data_end-skb->data要小于skb->len,这是由于数据包skb是非线性的,也就是data_end-data其实是线性区的大小,而skb->len是线性区加非线性区的大小,前者其实是skb->data_len,但是BPF程序中无法从__sk_buff中直接拿到这个值,我们可以使用bpf_skb_pull_data(skb,len)直接访问非线性数据,这个helper函数的官方解释如下

  • 如果 skb 是非线性的并且len 由线性和非线性部分组成,则pull入非线性数据,使得 skb 中的 len 字节可读可写,如果为 len 传递了一个零值,则拉取整个 skb 长度。
  • 此 helper 仅用于通过 direct packet access 进行读取和写入。
  • 对于direct packet access,如果偏移量无效,或者如果请求的数据在 skb 的非线性部分中,则访问偏移量在数据包边界内的测试(在 skb->data_end 上测试)很容易失败。失败时,程序可以退出,或者在非线性缓冲区的情况下,使用helper程序使数据可用。 bpf_skb_load_bytes() helper是访问数据的第一个解决方案。另一种方法是使用 bpf_skb_pull_data 拉入一次非线性部分,然后重新测试并最终访问数据。
  • 同时,这也保证了skb是未克隆的,这是direct write的必要条件。由于这仅需要是写入部分的不变量,因此验证程序检测写入并添加一个调用 bpf_skb_pull_data 的prologue​​,以从一开始就有效地取消克隆 skb,以防它确实被克隆。
  • 对这个helper的调用很容易改变底层的数据包缓冲区。因此,在load时,如果helper与direct packet access结合使用,则verifier先前对指针所做的所有检查都将无效并且必须再次执行。

2、tc程序对数据包头部进行剪裁,并load到网卡的ingress方向,使用tcpdump抓包发现报文并未进行相应的修改,原因是入方向tcpdump抓包点要早于tc程序,在tc程序中使用重定向将报文发到另一张网卡再使用tcp dump抓包,可看到报文符合代码处理逻辑。

文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
0
0