使用BPF过滤套接字的错误方式

2020-08-09 08:09:13

在Linux上使用BPF过滤数据包的每个人最终都会遇到这样一个错误:创建套接字,使用setsockopt应用BPF,然后使用recv从套接字读取。您读取的数据包与筛选器不匹配,但无论如何都是从套接字接收的。该错误仅在有大量流量时才会发生,即便如此,它也只会在应用程序第一次启动时发生。发生了什么事?

以下是一些受此问题影响的代码。为简明起见,删除了错误处理:

Int sock=套接字(AF_PACKET,SOCK_RAW,HTONS(ETH_P_ALL));struct sock_filter bpf_bytecode[]={...};//手工或使用";tcpdump-dd";struct sock_fprog BPF_PROGRAM={sizeof(Bpf_Bytecode)/sizeof(bpf_bytecode[0]),bpf_bytecode};int。字符缓冲区[MAX_PACKET_SIZE];int n=recv(SOCK,BUFFER,MAX_PACKET_SIZE,NO_OPTIONS);//缓冲区有时会包含与BPF不匹配的数据包。

这里的bug看起来很简单:包是在内核接收时过滤的,而不是在用户模式使用recv读取包时过滤的。因此,在创建套接字之后、调用setsockopt之前,可以接收到与BPF不匹配的数据包。即使在应用了BPF之后,这些数据包仍将保留在套接字的缓冲区中,并且稍后将通过recv传输到应用程序。

在应用BPF之后,在循环中使用recv丢弃来自套接字的数据包,直到它为空。这通常是可行的,但如果流量速率大于您可以丢弃数据包的速率,则会出现故障。(例如,如果您在一台流量非常大的机器上嗅探,并且您的应用程序通常会在套接字的缓冲区填满时丢失数据包,但这对您的用例无关紧要。)。在这种情况下,清空套接字的尝试将变成无限循环。

添加复制BPF逻辑的用户模式检查,并在接收到所有数据包后对其进行检查。重复的逻辑会导致错误。说得够多了。1个。

现在,让我们看一下修复此错误的教科书解决方案-与libpcap使用的解决方案相同:

Struct SOCK_FILTER ZERO_BYTECODE=BPF_STMT(BPF_RET|BPF_K,0);struct SOCK_FPROG ZERO_PROGRAM={1,&;ZERO_BYTECODE};IF(setsockopt(SOCK,SOL_SOCKET,SO_ATTACH_FILTER,&;ZERO_PROGRAM,sizeof(ZERO_PROGRAM))<;0){printf(";附加零BPF时出错:%d\。而(1){int bytes=recv(SOCK,DRAIN,SIZOF(DRAIN),MSG_DONTWAIT);IF(字节==-1){//我们假设这里的错误意味着没有任何东西可以从套接字读取,这正是我们想要中断的;}//bpf_program是我们要应用的实际BPF程序-就像上一个示例int err=setsockopt(sock,SOL_SOCKET,SO_ATTACH_FILTER,&;Int n=recv(SOCK,BUFFER,MAX_PACKET_SIZE,NO_OPTIONS);//缓冲区现在将始终与BPF匹配

我们在这里所做的是利用这样一个事实,即将一个BPF换出另一个是原子操作:如果您将一个BPF换成另一个,那么在任何时刻,第一个或第二个都在适当的位置,但永远不会两者都不存在。因此,我们首先应用所谓的“零BPF”,它是不匹配任何分组的BPF。然后,我们清空在应用“Zero-BPF”过滤器之前到达的所有数据包。在这一点上,套接字肯定是空的,它不能填满垃圾,因为零bpf已经就位。然后我们用我们想要的真实的BPF来代替零的BPF。因为交换是原子的,所以我们知道套接字中之后的任何数据包都必须与实际的BPF匹配。

这似乎是理论上的原因,但我在生产代码中看到它一次又一次地发生。假设您有一个bug,使得您的用户模式检查过于宽松,并且允许不匹配的数据包,但是BPF有适当的限制。您可以使用大量不匹配的数据包来测试您的应用程序,您甚至不会注意到用户模式检查过于宽松,因为BPF无论如何都会过滤掉所有不匹配的数据包。也就是说,在部署到生产中的高流量计算机之前,您永远不会注意到。[返回]