0%

eBPF:简单的ICMP数据拦截器(Python + BCC)

我觉得兴趣是最好的老师,所以我想用一个直观的案例,让大家感受到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的占用。

XDP架构

看看代码

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 BPF
import time
import sys

device="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层数据包开头的头指针。

TCPIP网络数据包格式

在网络层的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,就表明我们的实验成功了。