
每个 eBPF 程序都属于特定的类型,不同类型 eBPF 程序的触发事件是不同的。既然是网络的性能优化,自然应该去考虑网络类的 eBPF 程序。根据触发事件的不同,网络类 eBPF 程序可以分为 XDP 程序、TC 程序、套接字程序以及 cgroup 程序。这几类程序的触发事件和常用场景分别为:
由于支持卸载到硬件,XDP 的性能应该是最好的;而由于直接作用在套接字上,套接字程序和 cgroup 程序是最接近应用的。
根据原理的不同,套接字 eBPF 程序又分为很多不同的类型。其中,BPF_PROG_TYPE_SOCK_OPS、BPF_PROG_TYPE_SK_SKB、BPF_PROG_TYPE_SK_MSG 等类型的 eBPF 程序可以与套接字映射(如 BPF_MAP_TYPE_SOCKMAP 或 BPF_MAP_TYPE_SOCKHASH)配合,实现套接字的转发。
套接字 eBPF 程序工作在内核空间中,无需把网络数据发送到用户空间就能完成转发。因此,我们可以先猜测,它应该是可以提升网络转发的性能(当然,具体能不能提升,还需要接下来的测试验证)。
具体来说,使用套接字映射转发网络包需要以下几个步骤:
1、创建套接字映射;
首先,第一步是创建一个套接字类型的映射。以 BPF_MAP_TYPE_SOCKHASH 类型的套接字映射为例,它的值总是套接字文件描述符,而键则需要我们去定义。比如,可以定义一个包含 IP 协议五元组的结构体,作为套接字映射的键类型:
struct sock_key
{
__u32 sip; //源IP
__u32 dip; //目的IP
__u32 sport; //源端口
__u32 dport; //目的端口
__u32 family; //协议
};有了键类型之后,就可以使用 SEC 关键字来定义套接字映射了,如下所示:
#include <linux/bpf.h>
struct bpf_map_def SEC("maps") sock_ops_map = {
.type = BPF_MAP_TYPE_SOCKHASH,
.key_size = sizeof(struct sock_key),
.value_size = sizeof(int),
.max_entries = 65535,
.map_flags = 0,
};为了方便后续在 eBPF 程序中引用这两个数据结构,你可以把它们保存到一个头文件 sockops.h 中。
2、在 BPF_PROG_TYPE_SOCK_OPS 类型的 eBPF 程序中,将新创建的套接字存入套接字映射中;
套接字映射准备好之后,第二步就是在 BPF_PROG_TYPE_SOCK_OPS 类型的 eBPF 程序中跟踪套接字事件,并把套接字信息保存到 SOCKHASH 映射中。
可以使用如下的格式来定义这个 eBPF 程序:
SEC("sockops")
int bpf_sockmap(struct bpf_sock_ops *skops)
{
// TODO: 添加套接字映射更新操作
}在添加具体的套接字映射更新逻辑之前,还需要你先从 struct bpf_sock_ops中获取作为键类型的五元组。
可以直接使用它们来定义映射中所需要的键。下面就是 sock_key 的定义方法,注意这里把 local_port 转换为了同其他字段一样的网络字节序:
struct sock_key key = {
.dip = skops->remote_ip4,
.sip = skops->local_ip4,
.sport = bpf_htonl(skops->local_port),
.dport = skops->remote_port,
.family = skops->family,
};有了键之后,还不能立刻就去更新套接字映射。这是因为 BPF_PROG_TYPE_SOCK_OPS 程序跟踪了所有类型的套接字操作,而我们只需要把新创建的套接字更新到映射中。
接下来就是调用 BPF 辅助函数去更新套接字映射,如下所示:
bpf_sock_hash_update(skops, &sock_ops_map, &key, BPF_NOEXIST);其中,BPF_NOEXIST 表示键不存在的时候才添加新元素。再加上必要的头文件,完整的 eBPF 程序如下所示:
#include <linux/bpf.h>
#include <bpf/bpf_endian.h>
#include <bpf/bpf_helpers.h>
#include <sys/socket.h>
#include "sockops.h"
SEC("sockops")
int bpf_sockmap(struct bpf_sock_ops *skops)
{
/* skip if the packet is not ipv4 */
if (skops->family != AF_INET)
{
return BPF_OK;
}
/* skip if it is not established op */
if (skops->op != BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB && skops->op != BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB) {
return BPF_OK;
}
struct sock_key key = {
.dip = skops->remote_ip4,
.sip = skops->local_ip4,
/* convert to network byte order */
.sport = (bpf_htonl(skops->local_port)),
.dport = skops->remote_port,
.family = skops->family,
};
bpf_sock_hash_update(skops, &sock_ops_map, &key, BPF_NOEXIST);
return BPF_OK;
}
char LICENSE[] SEC("license") = "Dual BSD/GPL";把上述代码保存到 sockops.bpf.c 文件中,然后执行下面的命令,将其编译为 BPF 字节码:
clang -g -O2 -target bpf -D__TARGET_ARCH_x86 -I/usr/include/x86_64-linux-gnu -I. -c sockops.bpf.c -o sockops.bpf.o3、在流解析类的 eBPF 程序(如 BPF_PROG_TYPE_SK_SKB 或 BPF_PROG_TYPE_SK_MSG )中,从套接字映射中提取套接字信息,并调用 BPF 辅助函数转发网络包;
套接字转发可以使用 BPF_PROG_TYPE_SK_MSG 类型的 eBPF 程序,捕获套接字中的发送数据包,并根据上述的套接字映射进行转发。根据内核头文件中的定义格式,它的参数格式为 struct sk_msg_md。struct sk_msg_md 的定义格式如下所示,也已经包含了套接字映射所需的五元组信息:
struct sk_msg_md {
...
__u32 family;
__u32 remote_ip4; /* Stored in network byte order */
__u32 local_ip4; /* Stored in network byte order */
__u32 remote_port; /* Stored in network byte order */
__u32 local_port; /* stored in host byte order */
...
};接下来创建一个新的文件(如 sockredir.bpf.c),用于保存 BPF_PROG_TYPE_SK_MSG 程序。添加如下的代码,就定义了一个名为 bpf_redir 的 eBPF 程序:
SEC("sk_msg")
int bpf_redir(struct sk_msg_md *msg)
{
//TODO: 添加套接字转发逻辑
}在这个 eBPF 程序中,既然还要访问相同的套接字映射,也就需要从参数 struct sk_msg_md 中提取五元组信息,并存入套接字映射所需要的键 struct sock_key 中。如下所示,我们就定义了一个新的 struct sock_key(注意,这里同样需要把 local_port 转换为网络字节序):
struct sock_key key = {
.sip = msg->remote_ip4,
.dip = msg->local_ip4,
.dport = bpf_htonl(msg->local_port),
.sport = msg->remote_port,
.family = msg->family,
};有了套接字映射所需要的键之后,最后还剩下添加套接字转发逻辑的步骤。参考 BPF 辅助函数文档(你可以执行 man bpf-helpers 查询),bpf_msg_redirect_hash() 正好跟我们的需求完全匹配。
再加上必要的头文件之后,完整的 eBPF 程序如下所示:
#include <linux/bpf.h>
#include <bpf/bpf_endian.h>
#include <bpf/bpf_helpers.h>
#include <sys/socket.h>
#include "sockops.h"
SEC("sk_msg")
int bpf_redir(struct sk_msg_md *msg)
{
struct sock_key key = {
.sip = msg->remote_ip4,
.dip = msg->local_ip4,
.dport = bpf_htonl(msg->local_port),
.sport = msg->remote_port,
.family = msg->family,
};
bpf_msg_redirect_hash(msg, &sock_ops_map, &key, BPF_F_INGRESS);
return SK_PASS;
}
char LICENSE[] SEC("license") = "Dual BSD/GPL";4、加载并挂载 eBPF 程序到套接字事件。
得到套接字映射更新和转发这两个 BPF 字节码之后,还需要把它们加载到内核之中,再挂载到特定的内核事件之后才会生效。
通过命令行工具 bpftool 加载和挂载 eBPF 程序。首先,对于 sockops 程序 sockops.bpf.o 来说,你可以执行下面的命令,将其加载到内核中:
sudo bpftool prog load sockops.bpf.o /sys/fs/bpf/sockops type sockops pinmaps /sys/fs/bpf这条命令将 sockops.bpf.o 中的 eBPF 程序和映射加载到内核中,并固定到 BPF 文件系统中。固定到 BPF 文件系统的好处是,即便 bpftool 命令已经执行结束,eBPF 程序还会继续在内核中运行,并且 eBPF 映射也会继续存在内核内存中。
加载成功后,你还可以执行 bpftool prog show 和 bpftool map show 命令确认它们的加载结果。执行成功后,你会看到类似下面的输出:
$ sudo bpftool prog show name bpf_sockmap
1062: sock_ops name bpf_sockmap tag e37ef726a3a85a2e gpl
loaded_at 2022-02-04T13:07:28+0000 uid 0
xlated 256B jited 140B memlock 4096B map_ids 90
btf_id 234
$ sudo bpftool map show name sock_ops_map
90: sockhash name sock_ops_map flags 0x0
key 20B value 4B max_entries 65535 memlock 1572864BBPF 字节码加载成功之后,其中的 eBPF 程序还不会自动运行,因为这时候它还没有与内核事件挂载。
对 sockops 程序来说,它支持挂载到 cgroups,从而对 cgroups 所拥有的所有进程生效,这跟我们案例的容器场景也是匹配的。
可以执行下面的 mount 命令,查询当前系统的 cgroups 的挂载路径:
$ mount | grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot)通常情况下,主流的发行版都会把 cgroups 挂载到 /sys/fs/cgroup 路径下。接着,再执行下面的 bpftool cgroup attach 命令,把 sockops 程序挂载到 cgroups 路径中:
sudo bpftool cgroup attach /sys/fs/cgroup/ sock_ops pinned /sys/fs/bpf/sockops到这里,sockops 程序的加载和挂载就完成了。接下来,再执行下面的命令,加载并挂载 sk_msg 程序 sockredir.bpf.o:
sudo bpftool prog load sockredir.bpf.o /sys/fs/bpf/sockredir type sk_msg map name sock_ops_map pinned /sys/fs/bpf/sock_ops_map
sudo bpftool prog attach pinned /sys/fs/bpf/sockredir msg_verdict pinned /sys/fs/bpf/sock_ops_map从这两条命令中你可以看到,sk_msg 程序的加载和挂载过程跟 sockops 程序是类似的,区别只在于它们的程序类型和挂载类型不同:
由于 sk_msg 程序需要访问 sockops 程序创建的套接字映射,所以上述命令通过 BPF 文件系统路径 /sys/fs/bpf/sock_ops_map 对套接字映射进行了绑定。
到这里,两个 eBPF 程序的加载和挂载就都完成了。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。