什么是eBPF和tc?
eBPF简介
eBPF是一种革命性的内核技术,它允许用户在不修改内核源代码或load内核模块的情况下,安全高效地扩展内核功能。eBPF程序运行在内核的沙盒环境中,通过验证器确保其安全性。
tc简介
tc(Traffic Control)是Linux内核提供的网络流量控制工具,用于实现QoS(服务质量)功能,包括流量整形、调度和过滤等。tc提供了丰富的分类器和动作,而eBPF可以作为一种强大的分类器。
为什么选择eBPF+tc?
-
高性能:eBPF程序在内核空间运行,avoid了用户空间和内核空间之间的上下文切换
-
安全性:所有eBPF程序都必须通过验证器的严格检查
-
灵活性:可以动态load和unload,无需重启系统
-
可编程性:使用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抓包,可看到报文符合代码处理逻辑。