BSD分组过滤程序(BPF)是一种软件设备,用于过滤网络接口的数据流,即给网络接口加上开关。应用进程打开/dev/bpf0、
/dev/bpf1等等后,可以读取BPF设备,每个应用进程一次只能打开一个BPF设备。
通过若干ioctl命令,可以配置BPF设备,把它与某个网络接口相关联,并安装过滤程序,从而能够选择性地接收输入的分组。
BPF设备打开后,应用进程通过读写设备来接收分组,或将分组放入网络接口队列中。
BPF设备工作的前提是网络接口必须能够支持BPF。之前提到的以太网和环回接口的驱动程序都调用了bpfattach,用于配置
读取BPF设备的接口。
1.bpf_if结构
BPF维护一个链表,包含所有支持BPF的网络接口,每个接口都有一个bpf_if结构描述,全局指针bpf_iflist指向表中的第一个
结构,下图给出了BPF接口结构。
bif_next指向链表中的下一个BPF接口结构。
bif_dlist指向另一个链表,包括所有已打开并配置过的BPF设备。
如果某个网络接口已配置了BPF设备,即被加上了开关,则bif_driverp为空。为某个网络接口配置BPF设备是时,*bif_driverp
将指向bif_if结构,从而告诉接口可以开始向BPF传递分组。
接口类型保存在bif_dlt中。下图列出了几个接口所对应的常量值。
下图给出了每个输入分组中附加的bpf_hdr结构。
bh_tstamp记录了分组被捕捉的时间。bh_caplen等于BPF保存的字节数,bh_datalen等于原始分组中的字节数。bh_headlen
等于bpf_hdr的大小加上所需填充字节的长度。它用于解释从BPF设备中读取的分组,应该等同于接收接口的bif_hdrlen。
BPF接收的所有分组都有一个附加的BPF首部。bif_hdrlen等于首部大小,最后,bif_ifp指向对应接口的ifnet结构。
下图给出了 bpf_if结构是如何与ifnet结构建立连接的。
注意:bif_driverp指向网络接口的if_bpf和sc_bpf指针,而不是接口结构。
按照各接口驱动程序调用bpfattach时给出的信息,对3个接口初始化链路类型和首部长度成员变量。每个设备驱动程序初始化
调用bpfattach时,将构建BPF接口结构链表。该函数的大概处理流程如下:
1.为bpf_if创建空间。
2.初始化bpf_if结构。
3.计算BPF首部大小。下图列出了前述3种接口上,各自捕捉到的BPF分组的总体结构。
4.初始化bpf_dtab表。
5.打印控制台信息。
2.dpf_d结构
为了能够选择性地接收输入报文,应用程序首先打开一个BPF设备,调用若干ictl命令规定BPF过滤程序的条件,指明接口、
读缓存大小和超时时限。每个BPF设备都有一个相关的bpf_d结构,如下图所示。
如果同一个网络接口上配置了多个BPF设备,与之相应的bpf_d结构将组成一个链表。bd_next指向链表中的下一个结构。
分组缓存:
每个bpf_d结构都有两个分组缓存,输入分组通常保存在bd_sbuf(存储缓存)所对应的缓存中,另一个缓存要么对应于bd_fbuf
(空闲缓存),意味着缓存为空,或者对应于bd_hbuf(暂留缓存),意味着缓存中有分组等待应用进程读取,bd_slen和bd_hlen
分别就了保存在存储缓存和暂留缓存中的字节数。
如果存储缓存已满,它将被连接到bd_hbuf,而空闲缓存将被连接到bd_sbuf。当暂留缓存清空时,它被连接到bd_fbuf。
bd_bufsize记录与设备相连的两个缓存的大小,其默认值等于4096字节。
bd_bif指向BPF设备对应的bpf_if结构。
bd_rtout是等待分组时,延迟的滴答数。
bd_filter指向BPF设备的过滤程序代码。
两个统计值,应用进程可通过BIOGSTATS读取,分别保存在bd_rcount和bd_dcount中。
bd_promisc通过BIOCPROMISC命令置位,从而使接口工作在混杂状态。
bd_state未使用。
bd_immediate通过BIOCIMMEDIATE命令置位,促使驱动程序收到分组后即返回,不在等待暂留缓存填满。
bd_pad填充bpf_d结构,从而与长字节边界对齐。
bd_sel保存的selinfo结构,可用于select系统调用。
2.1.bpfopen函数
应用进程调用open,视图打开一个BPF设备时,该调用将被转到bpfopen,该函数用于分配bpf_d结构。
2.2.bpfioctl函数
设备打开后,可通过ioctl命令进行配置,下图总结了与BPF设备有关的ioctl命令。
下图给出了打开第二个BPF设备,并连接到同一个以太网网络接口后的各结构变量的状态。
连接到以太网接口的BPF设备:
连接到以太网接口的两个BPF设备:
第二个BPF设备打开时,在bpf_dtab表中分配一个新的dpf_d结构。
2.3.bpf_setif函数
bpf_setif函数,负责建立BPF描述符与网络接口间的连接,大概流程如下:
1.寻找匹配的ifnet结构。
2.连接bpf_d结构。
2.4.bpf_attachd函数
该函数建立起BPF描述符与BPF设备和网络接口间的对应关系。
3.BPF输入
一旦BPF设备打开并配置完成,应用进程就通过read系统调用从接口中接收分组。BPF过滤程序复制输入分组,因此不会
干扰正常的网络处理。输入分组保存在于BPF设备连接的存储缓存和暂留缓存中。
3.1.bpf_tap函数
下面列出了LANCE设备驱动程序调用bpf_tap的代码:
bpf_tap(le->sc_if.if_bpf, buf, len + sizeof(struct ether_header));
在该函数中,for循环遍历连接到的网络接口的BPF设备链表。对于每个设备,分组被递交给bpf_filter。如果过滤程序接受了
分组,它返回捕捉的字节数,并调用catchpacket复制分组。如果过滤程序拒绝了分组,长度等于0,循环继续。
3.2.catchpacket函数
该函数的大概处理流程如下:
1.判断分组是否放入缓存。如果空闲缓存可用,则轮转缓存,并通过bpf_wakeup唤醒所有等待输入数据的应用程序。
2.立即方式处理。如果设备处于立即方式,则唤醒所有等待进程以处理进入分组--内核中没有分组的缓存。
3.添加BPF首部。
3.3.bpfread函数
内核把针对BPF设备的read转交给bpfread处理。该函数的大概处理流程如下:
1.等待数据。因为多个应用进程能够从同一个BPF设备中读取数据,如果有某个进程已先读取了数据,while循环将强迫读
操作继续。如果暂留缓存中存在数据,循环被跳过。这与两个应用进程通过两个不同的BPF设备过滤同一个网络接口的情况
是不同的。
2.立即方式。如果设备处于立即方式,且存储缓存中有数据,则轮回缓存,while循环被终止。
3.无可用的分组。如果设备不处于立即方式,或者存储缓存中没有数据,则应用程序进入休眠状态,直到某个信号到达。
4.查看暂留缓存。如果定时器超时,且暂留缓存中存在数据,则循环终止。
5.查看存储缓存。如果定时器超时,且存储缓存中没有数据,则read返回0.应用进程执行限时读取时,必须考虑到这种情况。
如果定时器超时,且存储缓存中存在数据,则把存储缓存转给暂留缓存,循环终止。
6.分组可用。从暂留缓存中移出相应的字节,交给应用程序,把暂留缓存转给空闲缓存,清除缓存计数器,函数返回。