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

sockmap分析

2023-05-16 07:06:33
153
0

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里的报文拷贝给应用程序;

 

0条评论
作者已关闭评论
郑****勇
4文章数
0粉丝数
郑****勇
4 文章 | 0 粉丝
郑****勇
4文章数
0粉丝数
郑****勇
4 文章 | 0 粉丝
原创

sockmap分析

2023-05-16 07:06:33
153
0

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里的报文拷贝给应用程序;

 

文章来自个人专栏
内核 && 网络
4 文章 | 1 订阅
0条评论
作者已关闭评论
作者已关闭评论
0
0