ebpf socket filter 基础

最近在看一些关于 ebpf 的东西,简单记录一下关于 socket filter 的基础。

用处

可以使用 socket filter 来进行流量筛选,完成对特定流量的检测记录,比如 http 代理,socks 代理,dns 等。

基础

用户端使用的 go ebpf ,他家同时整理了一个 ebpf.io ,可以学习。
ebpf socket filter demo 可以参照内核源码 samples/bpf/sockex*_kern.c 的代码,后续的很多处理,都要走一下此流程。他其中重点关注 flow_dissector 函数,此函数就是依次解析二层到四层的数据报文,从中获取出需要的信息。假设我们处理 http 代理,也就是提取四层中数据段内容,进行解析。所以在操作之前,需要回顾一下二层到四层的网络报文是如何。网上找的图:
Untitled

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//记录一些函数。
//内核不能使用用户态的lib库,所以使用下列函数来进行字符串操作。
#define memcmp __builtin_memcmp
#ifndef memset
#define memset(dest, chr, n) __builtin_memset((dest), (chr), (n))
#endif

#ifndef memcpy
#define memcpy(dest, src, n) __builtin_memcpy((dest), (src), (n))
#endif

#ifndef memmove
#define memmove(dest, src, n) __builtin_memmove((dest), (src), (n))
#endif

//ebpf读取skb的操作
//1个字节
unsigned long long load_byte(void *skb,
unsigned long long off) asm("llvm.bpf.load.byte");
//2个字节
unsigned long long load_half(void *skb,
unsigned long long off) asm("llvm.bpf.load.half");
//4个字节
unsigned long long load_word(void *skb,
unsigned long long off) asm("llvm.bpf.load.word");

还有一点:

1
2
3
4
5
SEC("socket2")
int bpf_func_prog(struct __sk_buff *skb)
{
return 0;
}

对于 socket bpf 函数的返回值,它决定了用户端获取数据报文的长度。在文档中提到,也很重要:

A packet will be dropped if the filter pro‐
gram returns zero. If the filter program returns a nonzero
value which is less than the packet’s data length, the packet
will be truncated to the length returned.

再来看一下用户端,参照 samples/bpf/sockex*_user.c 的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static inline int open_raw_sock(const char *name)
{
struct sockaddr_ll sll;
int sock;
sock = socket(PF_PACKET, SOCK_RAW | SOCK_NONBLOCK | SOCK_CLOEXEC, htons(ETH_P_ALL));
if (sock < 0) {
printf("cannot create raw socket\n");
return -1;
}
memset(&sll, 0, sizeof(sll));
sll.sll_family = AF_PACKET;
sll.sll_ifindex = if_nametoindex(name);
sll.sll_protocol = htons(ETH_P_ALL);
if (bind(sock, (struct sockaddr *)&sll, sizeof(sll)) < 0) {
printf("bind to %s: %s\n", name, strerror(errno));
close(sock);
return -1;
}
return sock;
}

//attach
sock = open_raw_sock("lo");
setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, prog_fd,sizeof(prog_fd[0]))

下面分析一下这段代码。
首先先了解一下 PF_PACKET,他是用来发送和接收一个二层数据报文的协议族。使用 SOCK_RAW 可以获取到原始的二层数据报文。ETH_P_ALL 可以让我们接收发往本机和本机发送出去数据报文。
首先看一下 socket 函数:

1
int socket(int domain, int type, int protocol);

domain 对应的结构是 net_proto_family。
Untitled 1

比如 PF_PACKET 和 PF_INET,不同的 domain,每个 domain 对应一个 packet_type 结构:

1
2
3
4
5
static struct packet_type ip_packet_type __read_mostly = {
.type = cpu_to_be16(ETH_P_IP),
.func = ip_rcv,
.list_func = ip_list_rcv,
};

他们会在每个 domain 初始化的时候,进行注册。

1
2
3
4
5
static int __init inet_init(void)
{
....
dev_add_pack(&ip_packet_type);
....

Untitled 2

protocol 为 ETH_P_ALL 时,才可以接收发往本机和从本机发出的数据包。

1
2
3
4
5
6
7
8
static inline struct list_head *ptype_head(const struct packet_type *pt)
{
if (pt->type == htons(ETH_P_ALL))
return pt->dev ? &pt->dev->ptype_all : &ptype_all;
else
return pt->dev ? &pt->dev->ptype_specific :
&ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];
}

在二层往上层传递的时候,回去遍历 ptype_base 和 ptype_all 这两个链表。
回到 af_packet,在创建的时候:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static const struct net_proto_family packet_family_ops = {
.family = PF_PACKET,
.create = packet_create,
.owner = THIS_MODULE,
};
static int packet_create(struct net *net, struct socket *sock, int protocol,
int kern)
{
.....
po->prot_hook.func = packet_rcv;
if (sock->type == SOCK_PACKET)
po->prot_hook.func = packet_rcv_spkt;
po->prot_hook.af_packet_priv = sk;
if (proto) {
po->prot_hook.type = proto;
__register_prot_hook(sk);
}
.....

static int packet_rcv(struct sk_buff *skb, struct net_device *dev,
struct packet_type *pt, struct net_device *orig_dev)
{
....
//执行ebpf。
res = run_filter(skb, sk, snaplen);
if (!res)
goto drop_n_restore;
if (snaplen > res)
snaplen = res;
if (pskb_trim(skb, snaplen))
goto drop_n_acct;
__skb_queue_tail(&sk->sk_receive_queue, skb);
.....

在二层到三层的入口和三层到二层的入口,都会对 ptype_all 进行遍历,调用 packet_type→fun 函数进行处理。
而 setsockopt SO_ATTACH_BPF,便是把 ebpf socket filter 注册到 sock→sk_filter 上,这里涉及到到具体转换,我没有过多阅读,留着后续看。