一个险恶bug的深入分析

引子

本文将要描述一个我在某些linux版本以及libpcap(unix/linux平台下网络数据捕获包)中发现的一个非常险恶的bug。

一个有意思的现象

一位客户向我们报告,在一些安装了Debian Lenny的机器上,处于主动备份模式下的网卡不能检测到发送的数据包,边界流量检测器没有任何图形显示。我在公司里找了几台与客户硬件配置一样的机器,开始对问题展开调查。

首先,我从自己的笔记本ping目标机器。接着,我在目标机器上使用tcpdump嗅探(sniff 窃听网络上流经的数据包)绑定接口收到的ICMP包。

看来一切正常,是时候开始监听eth0了。eth0绑定的是活跃物理网卡:

结果,bond0嗅探的结果显示有ICMP输入,但是eth0绑定的物理网卡没有任何数据包输入。这就难怪我们的监测工具没有输入流量显示,因为测量仪没有检测到数据包!

这是什么原因呢?

 

设备无关层

为了调试这个问题,我首先开始检查网络协议栈的设备无关层,跟踪pcap负责处理输入数据包的相关代码。设备驱动通过调用设备无关层的netif_receive_skb函数处理从网络捕获的一组数据。

查看位于net/core/dev.c文件中的netif_receive_skb函数(简洁起见这里只摘取重要部分):

skb_bond函数会判断skb是否属于bond。如果是,那么函数要确保skb来自bond上的一个活跃设备。这个检查是为了防止高层协议为某个bond重复进行配置分配。如果skb通过了这些检查,skb的dev指针会被赋值为指向bond的设备指针,同时netif_receive_skb函数会返回原设备指针。

从概念上理解,你可以把上述代码等价于如下伪代码:

因此,看上去skb消息被当成发自一个bond中的设备而不是物理设备。

如果我们继续跟踪netif_receive_skb函数,我们会看到代码将skb转给了pcap处理:

这段代码对存储了所有pcap条目信息的列表ptype_all进行遍历, 判断pcap条目的设备结构能否匹配skb所属设备。

循环中对设备的检查非常有意思:

如果你试图嗅探来自eth0的数据包,但是由于eth0已经是bond的一部分,因而这个检查一定会失败。原因是skb->dev已经被重写为bond设备的dev结构了。

这就是为什么tcpdump和其他的测量工具嗅探bond相关的物理设备时看不到发送的数据包了!

只要简单地把if语句修改成:

因为加入了orig_dev检查,这下pcap就能够处理被dev指针被修改过的skb了

让我们来测试一下这个修改。

 

有意思的现象,第二轮观察

接下来,重新构建并安装修改后的内核(顺便说一下,这里有一份非常有用的文档),重新ping目标机器并开始嗅探发向物理设备的数据包:

 

什么情况?

为什么修改以后还是没有看到发送的数据包?

 

libpcap

让我们快速检查一下内核中负责处理AF_PACKET地址家族的libpcap接口。

AF_PACKET 在内核中是单独实现的一个地址家族,相关的代码位于net/packet/af_packet.c。libpcap通过调用socket系统调用建立一个socket,调用的第一个参数被设置为PACKET。libpcap接下来会使用bind系统调用把这个socket绑定想要嗅探的设备上。

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

• “以前的方法”: 对每个数据包的文件描述符调用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使用“以前的方法”读取数据包。

这条if语句会进行索引判断,对比从内核中读取数据包使用的索引号是否和用户通知libpcap监控网络设备使用的索引号相同。如果索引号匹配失败,pcap_read_packet函数会直接返回,不再调用libpcap提供的回调函数。

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

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

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

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

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

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

 

总结

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

 

英文原文: Joe Damato   编译:伯乐在线 – 唐尤华

【如需转载,请标注并保留原文链接、译文链接和译者等信息,谢谢合作!】

 

收藏 评论

关于作者:唐尤华

做自己喜欢的,编程、喝茶、看世界 个人主页 · 我的文章 · 18 ·    

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部