前情提要
之前介绍了使用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 <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
#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); if(ret != XDP_TX){ return ret; } 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));
__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 <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
#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); if(ret != XDP_TX){ return ret; } 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));
__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; 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,效率也就更高了。