我觉得兴趣是最好的老师,所以我想用一个直观的案例,让大家感受到eBPF的奥妙。
有些路由器上有禁止ICMP
的功能,打开后ping这个路由器就无响应了,今天这篇文章通过eBPF技术,来实现针对ICMP包丢包的功能。
再看本文前,建议先根据前面的教程搭建好开发环境,本文中使用的的是Python3 + BCC工具链。
eBPF XDP拦截ICMP与iptables拦截ICMP iptables方式数据包传输路径 iptables有两种方式配置拦截ICMP请求包,一种是reject,一种是drop
1 sudo iptables -A INPUT -p icmp --icmp-type echo-request -j REJECT
采用这种方式时,对端会提示 Destination port unreachable.
如果采用drop方式,命令则为
1 sudo iptables -A INPUT -p icmp --icmp-type echo-request -j DROP
对端则会提示Request timed out.
ICMP请求->网卡->内核TCP/IP协议栈
进入内核TCP/IP协议栈后,如果配置过iptables,则在ip_rcv()函数之后会有netfilter的hook处理,规则也是在这些hook部分生效的。
XDP方式丢包 eBPF XDP支持Ingress链路,也就是接收网络数据的时候可以生效,并且在内核协议栈之前生效,简要数据流程如下ICMP请求->网卡->XDP_DROP
相比iptables配置netfilter方式来说,XDP的生效更早,可以更快地进行丢包的动作,减少CPU的占用。
看看代码 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 from bcc import BPFimport timeimport sysdevice="enp0s6" c_code = """ #include <uapi/linux/bpf.h> #include <linux/ip.h> #include <linux/icmp.h> #include <linux/in.h> int xdp_drop_icmp(struct xdp_md* ctx){ void* data_end = (void*)(long)ctx->data_end; void *data = (void *)(uintptr_t)ctx->data; struct iphdr *iph = data + sizeof(struct ethhdr); if(IPPROTO_ICMP == iph->protocol){ return XDP_DROP; } return XDP_PASS; } """ xdp_mode = BPF.XDP_FLAGS_SKB_MODE b = BPF(text=c_code) func = b.load_func("xdp_drop_icmp" , BPF.XDP) b.attach_xdp(device, func, xdp_mode) running = True while running: user_input = input ("输入后退出" ) if user_input: running = False b.remove_xdp(device, xdp_mode) print ("Exit" )
以上是一个简单的XDP DROP的示例,我们可以专注于发挥丢包作用的部分代码,C代码非常简短
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <uapi/linux/bpf.h> #include <linux/ip.h> #include <linux/icmp.h> #include <linux/in.h> int xdp_drop_icmp (struct xdp_md* ctx) { void * data_end = (void *)(long )ctx->data_end; void *data = (void *)(uintptr_t )ctx->data; struct iphdr *iph = data + sizeof (struct ethhdr); if (IPPROTO_ICMP == iph->protocol){ return XDP_DROP; } return XDP_PASS; }
首先,从xdp_md的上下文获取到以太网数据包的头指针,然后,在data
头指针的基础上,加上struct ethhdr
的长度,就可以得到IP层数据包开头的头指针。
在网络层的iphdr结构体中,有一个protocol
的变量,表明了上层协议的类型,当我们通过以上的protocol字段判断出上层协议如果是ICMP的话,我们就通过XDP_DROP返回值将这个数据包丢弃。
运行失败? 加载eBPF程序需要root权限,所以我们运行的时候要用root用户或者使用sudo来执行。
但是,你可能会发现,程序为什么跑不起来?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 magicdian@magicdians-arm-ubuntu-server:~/eBPF/icmp_xdp_drop_python_demo$ sudo python3 drop.py [sudo] password for magicdian: bpf: Failed to load program: Permission denied ; void *data = (void *)(uintptr_t)ctx->data; 0: (61) r1 = *(u32 *)(r1 +0) ; if (IPPROTO_ICMP == iph->protocol){ 1: (71) r1 = *(u8 *)(r1 +23) invalid access to packet, off=23 size=1, R1(id =0,off=23,r=0) R1 offset is outside of the packet processed 2 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0 Traceback (most recent call last): File "/home/magicdian/eBPF/icmp_xdp_drop_python_demo/drop.py" , line 31, in <module> func = b.load_func("xdp_drop_icmp" , BPF.XDP) File "/usr/lib/python3/dist-packages/bcc-0.29.1+487331fe-py3.10.egg/bcc/__init__.py" , line 526, in load_func Exception: Failed to load BPF program b'xdp_drop_icmp' : Permission denied
报错提示加载失败,报错的原因是非法访问,为什么呢?
因为eBPF程序运行在内核里,所以要确保程序运行的稳定性,因此eBPF程序加载的时候,会有一个叫做verifier
的验证器存在。
这个验证器会判断你的程序中是否有非法的指针、内存操作,如果有非法操作的话,就会拒绝程序的加载。
在我们的案例中,我们收到的包的数据,如果data到data_end部分的长度没办法覆盖到完整的iphdr头部的话,我们尝试访问protocol字段就属于非法的内存访问,所以我们要先做一些保护性的判断。
添加保护判断 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <uapi/linux/bpf.h> #include <linux/ip.h> #include <linux/icmp.h> #include <linux/in.h> int xdp_drop_icmp (struct xdp_md* ctx) { void * data_end = (void *)(long )ctx->data_end; void *data = (void *)(uintptr_t )ctx->data; if (data + sizeof (struct ethhdr)+sizeof (struct iphdr) > data_end){ return XDP_PASS; } struct iphdr *iph = data + sizeof (struct ethhdr); if (IPPROTO_ICMP == iph->protocol){ return XDP_DROP; } return XDP_PASS; }
修改后,如果data头指针加上以太网头和网络层头部后大于data_end指向的位置,就表明这个数据包无法正常访问到iphdr的protocol字段,这种情况将这个数据包透传交付给TCP/IP内核协议栈做进一步的处理。
按上面的方式修改后,我们尝试ping测试机的IP地址(python脚本中指定的网卡接口的地址),会发现提示Request timeout for icmp_seq
,而当我们退出python脚本后,又可以正常ping,就表明我们的实验成功了。