看代码的时候突然遇到了一个很奇怪的结构体
struct sock_filter
当时就是一脸茫然,这是什么?
通过百度大概了解了一下皮毛,这个是过滤器,可以配置规则来过滤一些报文,只提取自己感兴趣的报文。
比较详细的介绍可以参考Linux内核工程导论——网络:Filter(LSF、BPF、eBPF)
本文大概简单介绍一下。
设置BPF过滤器是通过setsockopt调用来完成的,格式如下:
1 | setsockopt(sd, SOL_SOCKET, SO_ATTACH_FILTER, &Filter, sizeof(Filter)); |
参数FilterFilter的定义是struct sock_fprog Filter; 此结构在linux/filter.h当中有定义:
1 | struct sock_fprog /* Required for SO_ATTACH_FILTER. */ |
其中的filter指针指向结构为struct sock_filter的BPF过滤代码。结构同样也在同一个文件当中定义:
1 | struct sock_filter /* Filter block */ |
其实我们并不关心如何具体的编写struct sock_filter内的东西,因为tcpdump已经内置了这样的功能。例如,想要对所接受的数据包过滤,只想接收udp数据包,那么在tcpdump当中的命令就是tcpdump udp。如果你想让tcpdump帮你编译这样的过滤器,则用tcpdump udp -d,可以得到输出:
1 | root@as3522:~# tcpdump udp -d |
ld开头的表示加载某地址数据,jeq是比较啦,jt就是jump when true,jf呢就是jump when false,后面表示行号。不过这样的东西用在程序里还是不习惯,再用tcpdump udp -dd,可以得到:
1 | root@as3522:~# tcpdump udp -dd |
像c当中的数组的定义。这个就是过滤udp包的struct sock_filter的数组代码。把这部分复制到程序当中,将Filter.filter指向这个数组,Filter.len设置长度,就可以用setsockopt设置过滤器了。
不过使用这样的过滤器还是有一些需要注意的问题的,例如,设置一个过滤器,只允许两个源MAC地址的数据包进入,我们先用:
1 | [root@Kernel26 root]# tcpdump ether src 01:02:03:04:05:06 or ether src 04:05:06:07:08:09 -dd |
生成模板,我们注意到第2、4行比较了第一个MAC地址,第5、7行比较了第二个MAC地址,所以我们只需要在我们的程序当中动态的改变这四行当中的数值就可以了,例如:
1 | SetFilter(char *mac1, char *mac2) |
这里,需要用ntohl/ntohs等函数将网络字节序转换为主机字节序。但是这段代码是有逻辑问题的。它首先比较第一个mac地址的后4个字节,如果不正确转入比较第二个mac地址,如果正确转入比较第一个mac地址的高2个字节。因此,如果打算将这个代码用作通用的mac比较,那么在输入的两个mac地址后4字节都相同的情况下就会出现逻辑覆盖错误,即无法对满足第二个mac地址的条件进行判断。因此在这种情况下必须要准备两段比较代码,根据情况进行设置。具体不再累述。
此外,这段BPF代码还存在的一个问题是,一般情况下tcpdump只返回所捕获包的头96字节,也就是0×60字节,可见代码的倒数第二行是ret #96。对于需要完整的包处理还是不行的,因此你需要将其设置为0×0000ffff,或者在用tcpdump生成的时候用tcpdump -s 65535 -dd … 来生成。
最后,用tcpdump生成的BPF代码只能用于SOCK_RAW的socket,这类socket是可以直接操作数据链路层的,如果你打算将BPF用于ip层等较高层次的socket,那么你需要手工修改部分行的code.k,也就是修改如ldh [12]当中的[12]这个数值,因为这个数值的偏移量是按照从链路层开始计算得到的,在没有链路层之后,这个值就发生了变化,这个是需要注意的。
举个栗子
过滤以太网报文协议是0x8888的报文,先使用tcpdumo获取BPFcode
1 | root@as3522:~# tcpdump -dd ether proto 0x8888 |
代码
1 | struct sock_fprog Filter; |