Classical BPF(cBPF, 伯克利包过滤器)是一种用来过滤网络数据包的技术。它像一个钩子一样挂载在网络栈的关键路径上,可以在数据包进入协议栈之前,根据预设规则来过滤或处理网络数据包。
相比于一般的软件包过滤方案,Classical BPF有以下优点:
- 效率高:因为它运行在内核空间,可以避免不必要的内核态和用户态切换,也省去多次数据复制的开销。
- 安全:它不能随意访问系统内存或修改数据包,只能根据规则过滤,不会引起安全隐患。
- 灵活:过滤规则可以动态更新,使包过滤功能更加灵活。
Classical BPF通常应用于网络监控、防火墙、流量控制等场景。它为包过滤提供了一个高效、安全、灵活的解决方案。但功能较为受限,只能过滤包不能修改。
我在百度做了三年多的网络监控了,我们会使用各种各样的方式来监控整个百度的物理网络,这些监控方式不同于普通的TCP Server/Client或者 UDP程序,一般我们会采用raw socket的方式来做包的探测和网络监控,为了高效的使用raw socket,避免把内核协议层的所有包都复制到应用层,我们会使用cBPF对收到的包进行过滤,我们只从内核层复制特定类型的包到应用层, 比如只复制UDP协议目的端口在20000 ~ 21000的数据包。
怎么做到呢?就是使用cBPF。
我在先前的文章使用BPF, 将Go网络程序的吞吐提升8倍举了一个使用cBPF的例子:
|
|
可以使用ipv4.PacketConn
的SetBPF
方法设置过滤器:
|
|
这里我们根据IP协议进行简单的分析。这里我们没有做过多的兼容检查,因为我们自己知道我们处理的是IPv4的包,而且包中也没有Option选项:
IP的头部20个字节,payload是UDP包:
可以看到UDP的前两个字节是源端口, 接下来两个字节是目的端口。
所以从IP header开始,第22 ~ 24字节是目的端口,所以bpf.LoadAbsolute{Off: 22, Size: 2},
就是把这两个字节读取出来,和我们的值进行比较,看看是不是我们期望的值。
那么如果想使用cBPF
,就得会写bpf.Instruction
, 你得熟悉各种协议,以及bpf的指令。
不想学啊!累,麻烦!易出错!不好调试!
没关系,我写了一个库,只要你会使用tcpdump/wireshark,会使用他们的过滤器写法,就能写出相应的指令来。
比如 tcpdump -i any -nn -vvvv tcp port 8080
这样一个命令,它的过滤器是tcp port 8080
, 你这个使用这个库的下面的函数:
|
|
调用这个函数你会得到编译好的指令[]bpf.RawInstruction
,然后调用pconn.SetBPF(raws)
就可以了。
如果,你想得到它的Go代码形式,你可以调用s = CreateInstructionsFromExpr(layers.LinkTypeIPv4, "dst host 8.8.8.8 and icmp")
,
它会过滤只保留目的IP地址是8.8.8.8
并且是icmp的包,生成的指令如下:
|
|
bpf.LoadAbsolute{Off: 16,Size: 4},
是加载IP头中的目的IP地址,检查是不是等于8.8.8.8
,如果是,则检查协议(odd:9)是不是ICMP(icmp的协议号是1)。
所以即使你不熟悉各种协议,根据tcpdump的过滤表达式也能生成编译好的bpf代码,或者得到Go语言的代码片段。
对了,这个库是阡陌。