现在有两种方法可以从内核中拿到数据包:

  • “以前的方法”: 对每个数据包的文件描述符调用recvfrom函数。在老版本的内核上,只有这一个函数可用。

  • “新方法”:调用poll函数会通知libpcap有一组数据包到达,在内核与libpcap的共享内存中等待读取。比起“老办法”这种方法效率更高(使用的系统调用更少),在近大多数的内核包括Debian Lenny都支持这种办法。

  结果是,尽管Debian Lenny的内核支持“新方法”实现的AF_PACKET,但是相应的libpcap却不支持。这意味着tcpdump(依赖于libpcap)只能逐次逐个地从内核中取得数据包。

  更新版本的libpcap默认使用“新方法”从内核读取数据包。因为Lenny支持这种用法,我试着构建了一个更新版本的libpcap并修改了tcpdump。在修改过的Lenny内核上测试这个修改,我看到当我在bond上的物理设备进行嗅探时,数据包从RX路径流出。如果我把新的libpcap修改成使用“以前的方法”搜集数据包,没有数据包从RX路径流出。

  这意味着在使用“以前的方法”时,要么AF_PACKET有bug,要么多版本的libpcap有bug。

  if语句

  经过数小时痛苦地阅读代码,我找了一条if语句可以控制libpcap使用“以前的方法”读取数据包。

 // From pcap_read_packet in pcap-linux.c:
 
if (handle->md.ifindex != -1 &&
    from.sll_ifindex != handle->md.ifindex)
  return 0;
  这条if语句会进行索引判断,对比从内核中读取数据包使用的索引号是否和用户通知libpcap监控网络设备使用的索引号相同。如果索引号匹配失败,pcap_read_packet函数会直接返回,不再调用libpcap提供的回调函数。

  这段代码是为了防止内核中可能出现的竞争情况,比如虽然已经创建了socket但还没有来得及绑定到一个特定设备上,这种情况下AF_PACKET会在调用socket和bind中间把所有的数据包存储到队列中。

  然而,当数据包发向作为bond设备一部分的物理设备时这个检查会失败。

  用户向libpcap请求对这个物理设备监控,但正如我们上面看到的,当有数据包到来时内核会用netif_receive_skb中的bond设备指针覆盖dev结构。这样会造成bond设备的索引和物理设备的索引不一致。

  这条if语句是为什么发送的数据包在修改了内核以后仍然无法被类似tcpdump或者边界流量测量器捕获。

  这个检查在从内核读取数据包的“新方法”中不存在,因为支持新mmap方法的内核不会产生上述代码需要防止的竞争条件。因此,把一个更新版本的libpcap链接到tcpdump上(内核已经过修改)能看到发送给bond上物理设备的数据包。

  这个检查在当前版本的libpcap上仍然存在。

  总结

  计算机能够正常运行真是个奇迹。