1、背景介绍
sockmap是基于ebpf程序的一种map类型,sockmap里面存储了socket的信息;主要有sk_msg、sk_skb两种模式;本文主要介绍sk_msg,包括其原理、使用等;
1.1、sk_msg
sockkmap最初的版本提供了一种在本机TCP socket之间直接进行skb(struct sk_buff,其表示一个包含包头的数据包,后续均简称为skb)转发的机制。这种机制允许将原先用户态处理部分的逻辑卸载到内核BPF程序中进行处理,处理后的数据包skb直接在内核态中转发到另一个socket中去进行数据包的发送。整个过程无需的用户态内核态之间上下文切换,也无需任何用户态内核态之间的数据拷贝,大大缩短了数据转发路径。
1.2、sk_skb
sk_skb提供了一种在本机TCP socket之间直接进行skb转发的机制。这种机制允许将下图左边用户态处理部分的逻辑卸载到内核BPF程序中进行处理,处理后的数据包skb直接在内核态中转发到另一个socket中去进行数据包的发送。整个过程无需的用户态内核态之间上下文切换,也无需任何用户态内核态之间的数据拷贝,大大缩短了数据转发路径。
2、原理分析(sk_msg)
2.1、概述
sk_msg依赖于cgroup2和两个bpf程序;
1)、cgroup2用于指定要监听哪个范围内的 sockets 事件,进而决定了稍后要对哪些 socket 做重定向;sockmap 需要关联到某个 cgroup2,然后这个 cgroup2 内的所有 socket 就都会执行加 载的 BPF 程序;
2)、两个bpf程序,一段 BPF 程序监听所有的内核 socket 事件,并将新建的 socket 记录到这个 map;(bpf_sockops_v4.c);另一段 BPF 程序拦截所有 sendmsg 系统调用,然后去 map 里查找 socket 对端,之后 调用 BPF 函数绕过 TCP/IP 协议栈,直接将数据发送到对端的 socket queue(bpf_tcpip_bypass.c);
2.2、使用方式介绍
2.2.1、load.sh
bpf程序加载过程
#!/bin/bash
# enable debug output for each executed command
# to disable: set +x
set -x
# exit if any command fails
set -e
sudo mkdir -p /workspace/cgroup-test/demo/
sudo mount -t cgroup2 none /workspace/cgroup-test/demo/
# Mount the bpf filesystem
sudo mount -t bpf bpf /sys/fs/bpf/
# Compile the bpf_sockops_v4 program
# clang -O2 -g -target bpf -I/usr/include/linux/ -I/usr/src/linux-headers-5.0.0-23/include/ -c bpf_sockops_v4.c -o bpf_sockops_v4.o
clang -O2 -g -target bpf -c bpf_sockops_v4.c -o bpf_sockops_v4.o
# Load and attach the bpf_sockops_v4 program
# 加载bpf_sockops_v4.o,同时将其持久化到/sys/fs/bpf/bpf_sockops文件,这样通过该文件的indoe->private就能找到这个prog程序
sudo bpftool prog load bpf_sockops_v4.o "/sys/fs/bpf/bpf_sockops"
sudo bpftool cgroup attach "/sys/fs/cgroup/unified/" sock_ops pinned "/sys/fs/bpf/bpf_sockops"
# 先从/sys/fs/bpf/bpf_sockops里获取prog程序(也就是bpf_sockops_v4.o),然后执行cgroup attach,将这个prog程序
# 挂载到cgroup(/workspace/cgroup-test/demo)的bpf.effective[type]
sudo bpftool cgroup attach /workspace/cgroup-test/demo sock_ops pinned /sys/fs/bpf/bpf_sockops
# Extract the id of the sockhash map used by the bpf_sockops_v4 program
# This map is then pinned to the bpf virtual file system
# 获取/sys/fs/bpf/bpf_sockops这个prog程序的map id
MAP_ID=$(sudo bpftool prog show pinned "/sys/fs/bpf/bpf_sockops" | grep -o -E 'map_ids [0-9]+' | cut -d ' ' -f2-)
# 将找到的map持久化到/sys/fs/bpf/sock_ops_map,这样通过/sys/fs/bpf/sock_ops_map对应的indoe->private就能找到对应的map
sudo bpftool map pin id $MAP_ID "/sys/fs/bpf/sock_ops_map"
# Load and attach the bpf_tcpip_bypass program to the sock_ops_map
#clang -O2 -g -Wall -target bpf -I/usr/include/linux/ -I/usr/src/linux-headers-5.0.0-23/include/ -c bpf_tcpip_bypass.c -o bpf_tcpip_bypass.o
clang -O2 -g -target bpf -c bpf_tcpip_bypass.c -o bpf_tcpip_bypass.o
# 加载bpf_tcpip_bypass.o这个prog,同时持久化到/sys/fs/bpf/bpf_tcpip_bypass; 然后获取/sys/fs/bpf/sock_ops_map里对应的map,将bpf_tcpip_bypass.o这个
# prog里的map对象替换成/sys/fs/bpf/sock_ops_map找到的map,这样bpf_tcpip_bypass.o、bpf_sockops_v4.o这两个prog程序的map对象就是同一个了
sudo bpftool prog load bpf_tcpip_bypass.o "/sys/fs/bpf/bpf_tcpip_bypass" map name sock_ops_map pinned "/sys/fs/bpf/sock_ops_map"
# 获取/sfs/fs/bpf/bpf_tcp_ip_bypass里保存的prog程序; 将该prog程序跟/sys./fs/bpf/sock_ops_map里的map关联起来;
# 这里的attach type为msg_verdict,最终bpftool会将其转换成BPF_SK_MSG_VERDICT;
sudo bpftool prog attach pinned "/sys/fs/bpf/bpf_tcpip_bypass" msg_verdict pinned "/sys/fs/bpf/sock_ops_map"
2.2.2 unload.sh
bpf程序卸载过程
#!/bin/bash
set -x
# Detach and unload the bpf_tcpip_bypass program
sudo bpftool prog detach pinned "/sys/fs/bpf/bpf_tcpip_bypass" msg_verdict pinned "/sys/fs/bpf/sock_ops_map"
sudo rm "/sys/fs/bpf/bpf_tcpip_bypass"
# Detach and unload the bpf_sockops_v4 program
sudo bpftool cgroup detach /workspace/cgroup-test/demo sock_ops pinned "/sys/fs/bpf/bpf_sockops"
sudo rm "/sys/fs/bpf/bpf_sockops"
# Delete the map
sudo rm "/sys/fs/bpf/sock_ops_map
2.2.3、bpf_sockops_v4.c
#include "vmlinux.h"
#include "bpf_sockops.h"
/*
* extract the key identifying the socket source of the TCP event
*/
static inline
void sk_extractv4_key(struct bpf_sock_ops *ops,
struct sock_key *key)
{
// keep ip and port in network byte order
key->dip4 = ops->remote_ip4;
key->sip4 = ops->local_ip4;
key->family = 1;
// local_port is in host byte order, and
// remote_port is in network byte order
key->sport = (bpf_htonl(ops->local_port) >> 16);
key->dport = FORCE_READ(ops->remote_port) >> 16;
}
static inline
void bpf_sock_ops_ipv4(struct bpf_sock_ops *skops)
{
struct sock_key key = {};
// 解析sk的四元组信息
sk_extractv4_key(skops, &key);
// insert the source socket in the sock_ops_map
// 将sk的四元组信息加入到sock_ops_map表里
int ret = sock_hash_update(skops, &sock_ops_map, &key, BPF_NOEXIST);
printk("<<< ipv4 op = %d, port %d --> %d\n",
skops->op, skops->local_port, bpf_ntohl(skops->remote_port));
if (ret != 0) {
printk("FAILED: sock_hash_update ret: %d\n", ret);
}
}
__section("sockops")
int bpf_sockops_v4(struct bpf_sock_ops *skops)
{
uint32_t family, op;
family = skops->family;
op = skops->op;
switch (op) {
/*
* 服务端收到3ard-ack时,状态为BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB;
* 客户端收到syn-ack后,状态为BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB;
*/
case BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB:
case BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB:
if (family == 2) { //AF_INET
bpf_sock_ops_ipv4(skops);
}
break;
default:
break;
}
return 0;
}
char ____license[] __section("license") = "GPL";
int _version __section("version") = 1;
在bpf_sockops_v4的bpf程序里,主要就是判断sk的状态是否为刚建立连接的状态,是的话,就将sk的四元组信息加入到sock_ops_map表里;通过这个bpf程序,在本机间连接连接的tcp服务端、客户端都会进入到这个流程里,然后将对应的四元组信息保存下来,这样后面在sk通信的时候,通过该map表就能找到对端信息,找到对端信息的话就认为是本机间的sk通信,然后利用bpf_tcpip_bypass的bpf程序直接将报文转发到目的sk上;
2.2.4、bpf_tcpip_bypass.c
#include "vmlinux.h"
#include "bpf_sockops.h"
/* extract the key that identifies the destination socket in the sock_ops_map */
static inline
void sk_msg_extract4_key(struct sk_msg_md *msg,
struct sock_key *key)
{
/*
* 这里生成的key与sockops里生成key正好相反;这里将sk的目的ip作为key的源ip,将sk的本地ip作为key的目的ip;将sk的目的端口作为key的源端口,将sk的源端口作为key的目的端口;
*
*/
key->sip4 = msg->remote_ip4;
key->dip4 = msg->local_ip4;
key->family = 1;
key->dport = (bpf_htonl(msg->local_port) >> 16);
key->sport = FORCE_READ(msg->remote_port) >> 16;
}
__section("sk_msg")
int bpf_tcpip_bypass(struct sk_msg_md *msg)
{
struct sock_key key = {};
// 解析sk的四元组信息
sk_msg_extract4_key(msg, &key);
// 在sock_ops_map里查找是否有该四元组,有的话,则认为是本机间通信,修改msg->sk_redir为目的sk,然后将报文转发到sk_redir上;
msg_redirect_hash(msg, &sock_ops_map, &key, BPF_F_INGRESS);
return SK_PASS;
}
char ____license[] __section("license") = "GPL";
在bpf_tcpip_bypass的bpf程序主要是在sendmsg的时候调用的,主要就是看sock_ops_map里是否存在该sk的key信息,存在的话,说明这个key是对端sk在本地存放的,因此直接修改msg→sk_redir为目的sk,然后将报文转给目的sk;
2.3、bpf程序load过程分析
sudo bpftool prog load bpf_sockops_v4.o "/sys/fs/bpf/bpf_sockops"
创建一个bpf文件/sys/fs/bpf/bpf_sockops,该文件的inode里关联了bpf.sockops_v4.o这个prog程序,后面可以通过这个bpf文件找到对应的prog程序;
do_prog
do_load
load_with_options
file = GET_ARG(); (bpf程序对应的.o文件)
pinfile = GET_ARG(); (需要pin住的bpf文件系统文件)
bpf_object__load_xattr(将bpf程序加载到内存)
bpf_obj_pin(bpf_program__fd(prog), pinfile);(prog程序pin住bpf文件)
bpf_obj_pin(执行内核的pin系统调用)
bpf_obj_pin_user
bpf_fd_probe_obj(获取obj信息)
bpf_obj_do_pin(创建pin文件)
vfs_mkobj
bpf_mkprog(设置bpf文件的ops处理函数)
bpf_mkobj_ops(创建bpf文件的inode)
inode->i_private = raw;(将prog对象保存到inode->i_private里,这样后面就可以通过这个inode获取到prog程序)
2.4、bpf程序attach过程分析
2.4.1、bpf_sockops_v4.c
bpf_prog_attach
attach_type_to_prog_type(获取prog type)
bpf_prog_get_type(获取上面load的prog程序)
cgroup_bpf_prog_attach
cgrp = cgroup_get_from_fd(attr->target_fd); (获取cgroup)
cgroup_bpf_attach(将prog attach到cgroup上)
__cgroup_bpf_attach
struct list_head *progs = &cgrp->bpf.progs[type];(先从cgroup里获取该type对应的progs链表)
pl = kmalloc(sizeof(*pl), GFP_KERNEL);(分配一个bpf_prog_list,将其挂到progs链表上)
pl->prog = prog;(将当前attach的prog保存到pl->prog里)
update_effective_progs(将上面保存到cgroup->bpf.progs[type]里的prog迁移到cgrp->bpf.effective[type]上)
compute_effective_progs(desc, type, &desc->bpf.inactive); (先把progs迁移到cgroup->bpf.inactive上)
activate_effective_progs(desc, type, desc->bpf.inactive);(再把inactive的progs迁移到cgrp->bpf.effective上,后面在tcp_call_bpf的时候,就是从cgrp->bpf.effective里获取相关的prog)
bpf_sockops_v4的bpf程序在attach的时候,主要就是将prog程序加载到关联的cgroup的bpf.effective[type]上,这里的type类型为BPF_CGROUP_SOCK_OPS;
2.4.2、bpf_tcpip_bypass.c
bpf_prog_attach
sock_map_get_from_fd
map = __bpf_map_get(f); (获取sock_ops map)
sock_map_prog_update
sock_map_progs(map);(获取bpf_htab的progs)
psock_set_prog(&progs->msg_parser, prog); (将sk_msg的prog注册到progs的msg_parser里)
bpf_tcpip_bypass在attach的时候,主要先获取map信息(bpf_tcpip_bypass.c和bpf_sockops_v4.c使用的是同一个map),然后获取map关联的bpf_htab,最后将prog程序注册到bpf_htab->msg_parser里;在attach流程里,这里先把prog程序注册到bpf_htab→msg_parser里,后面等bpf_sock_ops_v4的bpf程序触发执行的时候,会为sk分配一个psock,然后就可以通过map表找到该bpf程序,将该bpf程序注册到psock里,这样sk就可以通过psock获取到该bpf prog程序;
2.5、bpf程序执行过程分析
2.5.1、bpf_sockops_v4.c
1)、服务端收到3ard-ack:
tcp_rcv_state_process
tcp_init_transfer(sk, BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB); (服务端的bpf ops为BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB)
tcp_call_bpf
BPF_CGROUP_RUN_PROG_SOCK_OPS
__cgroup_bpf_run_filter_sock_ops
struct cgroup *cgrp = sock_cgroup_ptr(&sk->sk_cgrp_data);(先找到该sk关联的cgroup id)
BPF_PROG_RUN_ARRAY(cgrp->bpf.effective[type], sock_ops,BPF_PROG_RUN);(从cgroup的effective数组里找到之前load的sock_ops bpf程序,并进入bpf程序分发处理处理(bpf_dispatcher_nop_func))
这里的type为BPF_CGROUP_SOCK_OPS)
BPF_PROG_RUN
bpf_dispatcher_nop_func
bpf_func(执行prog程序的hook回调函数,这里表示:bpf_sockops_v4)
bpf_sockops_v4(判断当前的op状态为BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB,因此将当前sk的四元组信息保存到sock_ops_map里)
sock_hash_update
bpf_sock_hash_update(怎么从sock_hash_update调用到bpf_sock_hash_update?)
sock_hash_update_common
link = sk_psock_init_link(); (先分配一个link数据结构,link用来保存sock_ops map表和sk的对应关系)
sock_map_link
msg_parser = READ_ONCE(progs->msg_parser); (先从bpf_htab的progs里获取msg_parser,这里的msg_parser是在attach sk_msg的时候添加上去的)
sk_psock_init(初始化psock)
rcu_assign_sk_user_data_nocopy(将psock设置到sk的user data里)
psock_set_prog(&psock->progs.msg_parser, msg_parser);(将sk_msg的prog注册到psock->progs.msg_parser)
sock_map_init_proto
tcp_bpf_get_proto(获取tcp_bpf_prots,这里会修改一些tcp的hook函数点)
sk_psock_update_proto(将新的tcp_bpf_prots更新到当前sk的sk_prot里)
WRITE_ONCE(sk->sk_prot, ops);
hash = sock_hash_bucket_hash(key, key_size);(根据key生成一个hash值)
sock_hash_select_bucket(找到对应的hash桶)
sock_hash_alloc_elem(分配一个elem,将传入的sk的四元组key保存到elem->key字段里,同时elem的sk字段指向当前的sk)
sock_map_add_link(将elem和map的对应关系保存到link里,并将link挂到psock的链表里)
上面的流程里会在sock_map_init_proto里替换tcp的prots处理函数,bpf的prots是在系统启动的时候,注册好的,需要替换哪些函数也是一开始就设置好的;
tcp_bpf_v4_build_proto(core_initcall)
tcp_bpf_rebuild_protos
static void tcp_bpf_rebuild_protos(struct proto prot[TCP_BPF_NUM_CFGS],
struct proto *base)
{
prot[TCP_BPF_BASE] = *base;
prot[TCP_BPF_BASE].unhash = sock_map_unhash;
prot[TCP_BPF_BASE].close = sock_map_close;
prot[TCP_BPF_BASE].recvmsg = tcp_bpf_recvmsg;
prot[TCP_BPF_BASE].stream_memory_read = tcp_bpf_stream_read;
prot[TCP_BPF_TX] = prot[TCP_BPF_BASE];
prot[TCP_BPF_TX].sendmsg = tcp_bpf_sendmsg;
prot[TCP_BPF_TX].sendpage = tcp_bpf_sendpage;
}
2)、客户端收到syn-ack时:
tcp_rcv_synsent_state_process
tcp_finish_connect
tcp_init_transfer(sk, BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB);
tcp_call_bpf(剩下的流程与服务端的类似)
通过以上的两个触发时间点,在本机建联的服务端、客户端最终都会执行到bpf_sockops_v4程序,然后把sk的四元组信息加入到sock_ops_map里;
2.5.2、bpf_tcpip_bypass.c
通过以上的bpf_sockops_v4程序,在本地建联的sk的四元组信息都会保存到sock_ops_map里,同时替换tcp的prot处理函数,当tcp sk执行sendmsg时,进入tcp_bpf_sendmsg,然后就行psock,获取psock的msg_parser(bpf_tcpip_bypass的bpf prog),执行prog程序,生成sk的key信息后,在sock_ops_map里查找目的sk是否在本地,是的话,将目的sk存放到msg→sk_redir里,然后执行tcp_bpf_sendmsg_redir,将报文存放到sk_redir关联的psock的ingress_msg链表里;最后唤醒sk_redir收取报文;
tcp_bpf_sendmsg
psock = sk_psock_get(sk);(获取sk关联的psock,这里正常是有值)
tcp_bpf_send_verdict
sk_psock_msg_verdict
prog = READ_ONCE(psock->progs.msg_parser);(获取sk_msg的prog)
bpf_prog_run_pin_on_cpu(执行sk_msg的prog)
bpf_tcpip_bypass(sk_msg prog的hook处理函数)
sk_msg_extract4_key(解析sk的四元组信息)
msg_redirect_hash(msg, &sock_ops_map, &key, BPF_F_INGRESS); (查看要目的地址是否在sock_ops_map里有记录,这里使用BPF_F_INGRESS参数,表示希望找到目的sk接收)
bpf_msg_redirect_hash(执行bpf heleper函数)
__sock_hash_lookup_elem(查看sock_ops_map里是有该四元组信息)
msg->flags = flags;(将BPF_F_INGRESS的参数保存到msg->flags里)
msg->sk_redir = sk; (找到的话,返回需要转发的sk,然后返回SK_PASS,否则返回SK_DROP)
psock->sk_redir = msg->sk_redir;(将bpf程序里解析的sk_redir保存到psock里)
tcp_bpf_sendmsg_redir(将报文转发到sk_redir指定的sk上)
bool ingress = sk_msg_to_ingress(msg); (根据上面保存的BPF_F_INGRESS参数,判断是需要进入接收流程)
bpf_tcp_ingress
sk_psock_queue_msg(将报文挂到sk_redir关联的psock->ingress_msg链表里)
sk_psock_data_ready(唤醒sk_redir的sk来接收报文)
tcp_bpf_recvmsg
psock = sk_psock_get(sk); (获取psock)
__tcp_bpf_recvmsg(从psock->ingress_msg里获取报文)
2.5.3、bpf程序执行过程总结
1)、在服务端收到3ard-ack(BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB)及客户端收到syn-ack(BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB)时,通过调用tcp_call_bpf接口触发sock_ops的prog处理;
2)、sock_ops的prog处理程序最终会进入sock_hash_update流程,将当前的sk四元组信息保存到sock_ops_map里;在sock_hash_update里,会同步创建一个psock填加到当前sk的userdata里;
3)、在sock_hash_update里,同样会通过map的指针找到htab hash表,然后将之前attach sk_msg prog的时候,保存在htab hash表里的msg_parser取出来,重新赋值到psock->progs.msg_parser上;供后续sk_msg的过程使用;
4)、通过sock_map_init_proto替换tcp的proto处理函数,比如sendmsg、recvmsg等,这样后面sk的send、recv就会进入tcp_bpf_sendmsg、tcp_bpf_recvmsg;
5)、sk执行报文发送时,进入tcp_bpf_sendmsg,先获取sk对应的psock,然后获取psock->progs.msg_parser,执行msg_parser,进入sk_msg的prog处理程序,最终进入bpf_msg_redirect_hash,根据sock_ops_map里保存的key信息,
判定当前是否是发给本机的其它sk,是的话,sock_ops_map里保存的目的sk保存到msg->sk_redir里,最终执行完bpf程序后,会将msg->sk_redir重新赋值到psock->sk_redir里;
6)、最终会将报文保存到目的sk的psock->ingress_msg里,然后唤醒目的sk来接收报文;
7)、目的sk在tcp_bpf_recvmsg里将保存在psck→ingress_msg里的报文拷贝给应用程序;