0%

使用clang + libbpf 编写eBPF XDP程序

前情提要

之前介绍了使用python + BCC套件的方式来编写eBPF程序,应该比较容易理解。

但在实际的生产场景中,如果要与现有的程序相结合,python可能并不是一个特别好的选择,比如我想在Android平台开发eBPF功能,系统没有python,那咋开发咧?

而且使用python + BCC的方式,本质上还是要写C代码,而编写的过程中由于没有C的自动补全和跳转,可能开发反而不方便,所以还是回归本质,直接用C开发eBPF程序。

什么是libbpf?

libbpf仓库地址放在文末引用。

那么什么是libbpf呢?我在谷歌上搜出一篇博客中介绍到了

libbpf是一个C库,提供eBPF实用函数和定义,用户空间程序可以使用它来管理内核空间的eBPF程序。 这个应用程序栈选项是最新的,是上游Linux源代码仓库的一部分,与最新的eBPF功能保持一致,并用于测试新的内核eBPF功能。

CFC4N博客对于libbpf的介绍原文放在文末引用。

使用libbpf前,需要在系统安装libbpf工具包,ubuntu下安装命令为

1
sudo apt-get install libbpf-dev

实现ICMP REJECT

上一次我们通过XDP DROP 实现了对ICMP数据进行丢包,这一次的话,我们要稍微进阶一点,尝试修改ICMP数据包,并将数据返回给发送端。

Show you the code

先上代码

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// #include <uapi/linux/bpf.h>
#include <linux/bpf.h>
#include <linux/ip.h>
#include <linux/icmp.h>
#include <linux/in.h>
#include <stdint.h>
#include<linux/if_ether.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

#define ICMP_ECHO_LEN 64
//#define SEC(NAME) __attribute__((section(NAME), used))


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

static __always_inline
void swap_src_dst_mac(void *data)
{
unsigned short *p = data;
unsigned short dst[3];
//交换源地址和目的地址
dst[0] = p[0];
dst[1] = p[1];
dst[2] = p[2];
p[0] = p[3];
p[1] = p[4];
p[2] = p[5];
p[3] = dst[0];
p[4] = dst[1];
p[5] = dst[2];
}


static __always_inline int icmp_check(struct xdp_md *ctx, int type)
{
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
struct icmphdr *icmph;
struct iphdr *iph;

if (data + sizeof(*eth) + sizeof(*iph) + ICMP_ECHO_LEN > data_end)
return XDP_PASS;

if (eth->h_proto != bpf_htons(ETH_P_IP))
return XDP_PASS;

iph = data + sizeof(*eth);

if (iph->protocol != IPPROTO_ICMP)
return XDP_PASS;

if (bpf_ntohs(iph->tot_len) - sizeof(*iph) != ICMP_ECHO_LEN)
return XDP_PASS;

icmph = data + sizeof(*eth) + sizeof(*iph);

if (icmph->type != type)
return XDP_PASS;

return XDP_TX;
}


SEC("prog")
int xdp_drop_icmp(struct xdp_md* ctx){
void* data_end = (void*)(long)ctx->data_end;
void *data = (void *)(uintptr_t)ctx->data;
struct ethhdr *eth = data;
struct iphdr *iph = (struct iphdr *)(eth + 1);
struct icmphdr *icmph = (struct icmphdr *)(iph + 1);

//判断数据包格式是否符合需求
int ret = icmp_check(ctx, ICMP_ECHO);//ECHO_REQUEST

if(ret != XDP_TX){
return ret;
}

//获取ip头和icmp头指针
iph = data + sizeof(*eth);
icmph = data + sizeof(*eth) + sizeof(*iph);

void* icmp_data = data + sizeof(*eth) + sizeof(*iph) + sizeof(*icmph);
memcpy(icmp_data, iph, sizeof(*iph) + sizeof(*icmph));

//切换MAC和IP的源地址和目的地址
__be32 saddr = iph->saddr;
swap_src_dst_mac(data);
iph->saddr = iph->daddr;
iph->daddr = saddr;

//修改包类型和状态码
icmph->type = ICMP_DEST_UNREACH;
icmph->code = ICMP_PORT_UNREACH;
return XDP_TX;
}

char __license[] SEC("license") = "GPL";

以上的代码,相比之前DROP功能,我们在判断出ICMP数据包后,会更进一步,计算出icmp层包头部分的地址,并修改其中的type和code字段,从而实现ICMP REJECT,具体的原因是”端口不可达“。

以上代码我保存的文件名是 icmp_reject.c

编译eBPF XDP程序

编译的命令是

1
clang -Wall -O2 -target bpf -I/usr/include/aarch64-linux-gnu -c icmp_reject.c -o icmp_reject.o

我实在aarch64系统的虚拟机上编译,在你自己的系统上编译时注意要修改这一部分 -I/usr/include/aarch64-linux-gnu

编译后我们会看到生成了icmp_reject.o文件,这个文件就是eBPF bytecode字节码。

挂载XDP程序到网卡

现在我们可以将这个程序挂载到网卡上运行。

1
sudo ip link set dev enp0s6 xdpgeneric obj icmp_reject.o

注意enp0s6 是要挂载的网卡的名称
xdpgeneric 表示当前以generic模式加载XDP程序。

如果你在物理机上运行并且网卡支持的话,你可以尝试使用native模式加载,性能会更好

取消挂载的命令是这个

1
sudo ip link set dev enp0s6 xdpgeneric off

挂载后尝试ping我们指定网卡的IP地址,输出如下

1
2
3
4
5
magicdian@MacBook-Pro-14 ~ % ping 10.211.55.6
PING 10.211.55.6 (10.211.55.6): 56 data bytes
Request timeout for icmp_seq 0
Request timeout for icmp_seq 1
Request timeout for icmp_seq 2

为什么终端显示timeout?

按道理来说,我的程序已经将数据包的type修改成ICMP_DEST_UNREACH , 而code 也修改成了ICMP_PORT_UNREACH,为什么还是显示timeout?

让我们打开wireshark一探究竟。

在wireshark中选定对应的网卡进行监听,在wireshark中过滤icmp数据,并再次执行ping。

我们可以看到,wireshark中有显示返回的被修改后REJECT的数据包,但是我们的ECHO请求确表示”no response found!”

我们点进回复的数据包,并选中查看Internet Control Message Protocol,我们会发现Checksum一栏是黄色,并提示 incorrect, should be 0xd9b7

意思就是,数据包的checksum校验和失败,因此这条数据实际上被drop掉了。

那么为什么checksum校验和失败了呢?

因为我修改了ICMP数据包的中的内容,修改了type和code类型,而checksum却没有更新。

添加checksum相关代码

libbpf中有帮助方法,可以便捷地计算checksum。计算之前我们要将checksum先改成0,然后再进行计算

这样一来,完整的代码就变成了这样

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// #include <uapi/linux/bpf.h>
#include <linux/bpf.h>
#include <linux/ip.h>
#include <linux/icmp.h>
#include <linux/in.h>
#include <stdint.h>
#include<linux/if_ether.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

#define ICMP_ECHO_LEN 64
//#define SEC(NAME) __attribute__((section(NAME), used))


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


static __always_inline __u16 csum_fold_helper(__wsum sum)
{
sum = (sum & 0xffff) + (sum >> 16);
return ~((sum & 0xffff) + (sum >> 16));
}

static __always_inline __u16 ipv4_csum(void *data_start, int data_size)
{
__wsum sum;

sum = bpf_csum_diff(0, 0, data_start, data_size, 0);
return csum_fold_helper(sum);
}

static __always_inline
void swap_src_dst_mac(void *data)
{
unsigned short *p = data;
unsigned short dst[3];
//交换源地址和目的地址
dst[0] = p[0];
dst[1] = p[1];
dst[2] = p[2];
p[0] = p[3];
p[1] = p[4];
p[2] = p[5];
p[3] = dst[0];
p[4] = dst[1];
p[5] = dst[2];
}


static __always_inline int icmp_check(struct xdp_md *ctx, int type)
{
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
struct icmphdr *icmph;
struct iphdr *iph;

if (data + sizeof(*eth) + sizeof(*iph) + ICMP_ECHO_LEN > data_end)
return XDP_PASS;

if (eth->h_proto != bpf_htons(ETH_P_IP))
return XDP_PASS;

iph = data + sizeof(*eth);

if (iph->protocol != IPPROTO_ICMP)
return XDP_PASS;

if (bpf_ntohs(iph->tot_len) - sizeof(*iph) != ICMP_ECHO_LEN)
return XDP_PASS;

icmph = data + sizeof(*eth) + sizeof(*iph);

if (icmph->type != type)
return XDP_PASS;

return XDP_TX;
}


SEC("prog")
int xdp_drop_icmp(struct xdp_md* ctx){
void* data_end = (void*)(long)ctx->data_end;
void *data = (void *)(uintptr_t)ctx->data;
struct ethhdr *eth = data;
struct iphdr *iph = (struct iphdr *)(eth + 1);
struct icmphdr *icmph = (struct icmphdr *)(iph + 1);

//判断数据包格式是否符合需求
int ret = icmp_check(ctx, ICMP_ECHO);//ECHO_REQUEST

if(ret != XDP_TX){
return ret;
}

//获取ip头和icmp头指针
iph = data + sizeof(*eth);
icmph = data + sizeof(*eth) + sizeof(*iph);

void* icmp_data = data + sizeof(*eth) + sizeof(*iph) + sizeof(*icmph);
memcpy(icmp_data, iph, sizeof(*iph) + sizeof(*icmph));

//切换MAC和IP的源地址和目的地址
__be32 saddr = iph->saddr;
swap_src_dst_mac(data);
iph->saddr = iph->daddr;
iph->daddr = saddr;

//修改包类型和状态码
icmph->type = ICMP_DEST_UNREACH;
icmph->code = ICMP_PORT_UNREACH;

icmph->checksum = 0;
//重新计算checksum,计算前必须将原有checksum置为0
icmph->checksum = ipv4_csum(icmph, ICMP_ECHO_LEN);

return XDP_TX;
}

char __license[] SEC("license") = "GPL";

重新编译并挂载后再尝试ping

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
magicdian@MacBook-Pro-14 ~ % ping 10.211.55.6
PING 10.211.55.6 (10.211.55.6): 56 data bytes
64 bytes from ubuntu-server-22.04.shared (10.211.55.6): Destination Port Unreachable
Vr HL TOS Len ID Flg off TTL Pro cks Src Dst
4 5 00 5400 2027 0 0000 40 01 d6d4 10.211.55.2 10.211.55.6

Request timeout for icmp_seq 0
64 bytes from ubuntu-server-22.04.shared (10.211.55.6): Destination Port Unreachable
Vr HL TOS Len ID Flg off TTL Pro cks Src Dst
4 5 00 5400 5eef 0 0000 40 01 980c 10.211.55.2 10.211.55.6

Request timeout for icmp_seq 1
64 bytes from ubuntu-server-22.04.shared (10.211.55.6): Destination Port Unreachable
Vr HL TOS Len ID Flg off TTL Pro cks Src Dst
4 5 00 5400 2c26 0 0000 40 01 cad5 10.211.55.2 10.211.55.6


可以看到已经提示Destination Port Unreachable了,效果与使用iptables命令一样

1
sudo iptables -A INPUT -p icmp --icmp-type echo-request -j REJECT

但是由于我们的XDP程序在网络数据包收包的时候运行在内核协议栈之前,所以相比iptables使用的netfilter可以更早的执行REJECT,效率也就更高了。