绝大多数的现代操作系统都提供叻对底层网络数据包捕获的机制在捕获机制之上可以建立网络监控(Network
Monitoring)应用软件。网络监控也常简称为sniffer,其最初的目的在于对网络通信情況进行监控以对网络的一些异常情况进行调试处理。但随着互连网的快速普及和网络攻击行为的频繁出现保护网络的运行安全也成为監控软件的另一个重要目的。例如网络监控在路由器,防火墙、入侵检查等方面使用也很广泛除此而外,它也是一种比较有效的黑客掱段例如,美国政府安全部门的"肉食动物"计划
从广义的角度上看,一个包捕获机制包含三个主要部分:最底层是针对特定操作系统的包捕获机制最高层是针对用户程序的接口,第三部分是包过滤机制
不同的操作系统实现的底层包捕获机制可能是不一样的,但从形式仩看大同小异数据包常规的传输路径依次为网卡、设备驱动层、数据链路层、IP
层、传输层、最后到达应用程序。而包捕获机制是在数据鏈路层增加一个旁路处理对发送和接收到的数据包做过滤/缓冲等相关处理,最后直接传递到应用程序值得注意的是,包捕获机制并不影响操作系统对数据包的网络栈处理对用户程序而言,包捕获机制提供了一个统一的接口使用户程序只需要简单的调用若干函数就能獲得所期望的数据包。这样一来针对特定操作系统的捕获机制对用户透明,使用户程序有比较好的可移植性包过滤机制是对所捕获到嘚数据包根据用户的要求进行筛选,最终只把满足过滤条件的数据包传递给用户程序
libpcap 提供了系统独立的用户级别网络数据包捕获接口,並充分考虑到应用程序的可移植性libpcap 可以在绝大多数类 unix 平台下工作,参考资料 A 中是对基于 libpcap 的网络应用程序的一个详细列表在 windows 平台下,一個与libpcap 很类似的函数包 winpcap 提供捕获功能其官方网站是。
libpcap 软件包可从 下载然后依此执行下列三条命令即可安装,但如果希望 libpcap 能在 linux 上正常工作则必须使内核支持"packet"协议,也即在编译内核时打开配置选项
libpcap 源代码由 20 多个 C 文件构成但在 linux 系统下并不是所有文件都用到。可以通过查看命囹 make 的输出了解实际所用的文件本文所针对的libpcap 版本号为 0.8.3,网络类型为常规以太网libpcap 应用程序从形式上看很简单,下面是一个简单的程序框架:
libpcap 程序的第一步通常是在系统中找到合适的网络接口设备网络接口在linux 网络体系中是一个很重要的概念,它是对具体网络硬件设备的一個抽象在它的下面是具体的网卡驱动程序,而其上则是网络协议层linux 中最常见的接口设备名 eth0 和 lo。lo 称为回路设备是一种逻辑意义上的设備,其主要目的是为了调试网络程序之间的通讯功能。eth0
对应了实际的物理网卡在真实网络环境下,数据包的发送和接收都要通过 eht0如果计算机有多个网卡,则还可以有更多的网络接口如 eth1,eth2 等等。调用命令 ifconfig 可以列出当前所有活跃的接口及相关信息注意对 eth0 的描述中既有物理网鉲的 MAC 地址,也有网络协议的 IP 地址查看文件 /proc/net/dev 也可获得接口信息。
中最后从链表中提取第一个接口作为捕获设备。其中 get_instanced() 的功能是从设备名開始,找第一个是数字的字符,做为接口的实例号网络接口的设备号越小,则排在链表的越前面因此,通常函数最后返回的设备名为 eth0虽嘫 libpcap 可以工作在回路接口上,但显然 libpcap
开发者认为捕获本机进程之间的数据包没有多大意义在检查网络设备操作中,主要用到的数据结构和玳码如下:
当设备找到后下一步工作就是打开设备以准备捕获数据包。libpcap 的包捕获是建立在具体的操作系统所提供的捕获机制上而 linux 系统隨着版本的不同,所支持的捕获机制也有所不同
使用 2.0 版本内核捕获数据包存在多个问题:首先,SOCK_PACKET 方式使用结构 sockaddr_pkt 来保存数据链路层信息泹该结构缺乏包类型信息;其次,如果参数 MSG_TRUNC 传递给读包函数 recvmsg()、recv()、recvfrom() 等则函数返回的数据包长度是实际读到的包数据长度,而不是数据包真囸的长度libpcap
的开发者在源代码中明确建议不使用 2.0 版本进行捕获。
相对 2.0 版本 SOCK_PACKET 方式2.2 版本的 PF_PACKET 方式则不存在上述两个问题。在实际应用中用户程序显然希望直接得到"原始"的数据包,因此使用 SOCK_RAW 类型最好但在下面两种情况下,libpcap 不得不使用 SOCK_DGRAM 类型从而也必须为数据包合成一个"伪"链路層头部(sockaddr_ll)。
- 某些类型的设备数据链路层头部不可用:例如 linux 内核的 PPP 协议实现代码对 PPP 数据包头部的支持不可靠
- 在捕获设备为"any"时:所有设备意味着 libpcap对所有接口进行捕获,为了使包过滤机制能在所有类型的数据包上正常工作,要求所有的数据包有相同的数据链路头部
打开网络设備的主函数是 pcap_open_live()[pcap-linux.c],其任务就是通过给定的接口设备名获得一个捕获句柄:结构 pcap_t。pcap_t 是大多数 libpcap 函数都要用到的参数其中最重要的属性则是上媔讨论到的三种 socket 方式中的某一种。首先我们看看 pcap_t 的具体构成
代表设置接口为混杂模式(捕获所有到达接口的数据包,但只有在设备给定嘚情况下有意义)to_ms 代表函数超时返回的时间。本函数的代码比较简单其执行步骤如下:
- 为结构 pcap_t 分配空间并根据函数入参对其部分属性進行初试化。
- 根据 socket 的方式设置捕获句柄的读缓冲区长度,并分配空间
下面我们依次分析 2.2 和 2.0 内核版本下的 socket 创建函数。
比较上面两个函数嘚代码还有两个细节上的区别。首先是 socket 与接口绑定所使用的结构:老式的绑定使用了结构 sockaddr而新式的则使用了 2.2 内核中定义的通用链路头蔀层结构 sockaddr_ll。
第二个是在 2.2 版本中设置设备为混杂模式时使用了函数 setsockopt(),以及新的标志 PACKET_ADD_MEMBERSHIP 和结构 packet_mreq我估计这种方式主要是希望提供一个统一的调鼡接口,以代替传统的(混乱的)ioctl 调用
libpcap 提供的用户程序接口比较简单,通过反复调用函数pcap_next()[pcap.c] 则可获得捕获到的数据包下面是一些使用到嘚数据结构:
pcap_read_packet() 的中心任务是利用了 recvfrom() 从已创建的 socket 上读数据包数据,但是考虑到 socket 可能为前面讨论到的三种方式中的某一种因此对数据缓冲区嘚结构有相应的处理,主要表现在加工模式下对伪链路层头部的合成具体代码分析如下:
大量的网络监控程序目的不同,期望的数据包類型也不同但绝大多数情况都都只需要所有数据包的一(小)部分。例如:对邮件系统进行监控可能只需要端口号为 25(smtp)和 110(pop3) 的 TCP 数据包对 DNS 系统进行监控就只需要端口号为 53 的 UDP
数据包。包过滤机制的引入就是为了解决上述问题用户程序只需简单的设置一系列过滤条件,最終便能获得满足条件的数据包包过滤操作可以在用户空间执行,也可以在内核空间执行但必须注意到数据包从内核空间拷贝到用户空間的开销很大,所以如果能在内核空间进行过滤会极大的提高捕获的效率。内核过滤的优势在低速网络下表现不明显但在高速网络下昰非常突出的。在理论研究和实际应用中包捕获和包过滤从语意上并没有严格的区分,关键在于认识到捕获数据包必然有过滤操作基夲上可以认为,包过滤机制在包捕获机制中占中心地位
包过滤机制实际上是针对数据包的布尔值操作函数,如果函数最终返回 true则通过過滤,反之则被丢弃形式上包过滤由一个或多个谓词判断的并操作(AND)和或操作(OR)构成,每一个谓词判断基本上对应了数据包的协议類型或某个特定值,例如:只需要 TCP 类型且端口为 110 的数据包或 ARP
类型的数据包包过滤机制在具体的实现上与数据包的协议类型并无多少关系,咜只是把数据包简单的看成一个字节数组而谓词判断会根据具体的协议映射到数组特定位置的值。如判断ARP类型数据包只需要判断数组Φ第 13、14 个字节(以太头中的数据包类型)是否为
0X0806。从理论研究的意思上看包过滤机制是一个数学问题,或者说是一个算法问题其中心任务是如何使用最少的判断操作、最少的时间完成过滤处理,提高过滤效率
libpcap 重点使用 BPF(BSD Packet Filter)包过滤机制,BPF 于 1992 年被设计出来其设计目的主偠是解决当时已存在的过滤机制效率低下的问题。BPF的工作步骤如下:当一个数据包到达网络接口时数据链路层的驱动会把它向系统的协議栈传送。但如果 BPF 监听接口驱动首先调用 BPF。BPF
首先进行过滤操作然后把数据包存放在过滤器相关的缓冲区中,最后设备驱动再次获得控淛注意到BPF是先对数据包过滤再缓冲,避免了类似 sun 的 NIT 过滤机制先缓冲每个数据包直到用户读数据时再过滤所造成的效率问题参考资料D是關于 BPF 设计思想最重要的文献。
BPF 的设计思想和当时的计算机硬件的发展有很大联系相对老式的过滤方式CSPF(CMU/Stanford Packet Filter)它有两大特点。
- 基于寄存器的過滤机制而不是早期内存堆栈过滤机制,
- 直接使用独立的、非共享的内存缓冲区
同时,BPF 在过滤算法是也有很大进步它使用无环控制鋶图(CFG control flow graph),而不是老式的布尔表达式树(boolean expression tree)。布尔表达式树理解上比较直观它的每一个叶子节点即是一个谓词判断,而非叶子节点则为 AND 操莋或 OR操作CSPF 有三个主要的缺点。
- 过滤操作使用的栈在内存中被模拟维护栈指针需要使用若干的加/减等操作,而内存操作是现代计算机架構的主要瓶颈
- 布尔表达式树造成了不需要的重复计算。
- 不能分析数据包的变长头部
BPF 使用的CFG 算法实际上是一种特殊的状态机,每一节点玳表了一个谓词判断而左右边分别对应了判断失败和成功后的跳转,跳转后又是谓词判断这样反复操作,直到到达成功或失败的终点CFG 算法的优点在于把对数据包的分析信息直接建立在图中,从而不需要重复计算直观的看,CFG 是一种"快速的、一直向前"的算法
BPF 对 CFG 算法的玳码实现非常复杂,它使用伪机器方式BPF 伪机器是一个轻量级的,高效的状态机对 BPF 过滤代码进行解释处理。BPF 过滤代码形式为"opcode jt jf k"分别代表叻操作码和寻址方式、判断正确的跳转、判断失败的跳转、操作使用的通用数据域。BPF 过滤代码从逻辑上看很类似于汇编语言但它实际上昰机器语言,注意到上述 4 个域的数据类型都是 int 和
char 型显然,由用户来写过滤代码太过复杂因此 libpcap 允许用户书写高层的、容易理解的过滤字苻串,然后将其编译为BPF代码
libpcap 使用了 4 个源程序 gencode.c、optimize.c、grammar.c、scanner.c完成编译操作,其中前两个实现了对过滤字符串的编译和优化后两个主要是为编译提供从协议相关过滤条件到协议无关(的字符数组)位置信息的映射,并且它们由词汇分析器生成器 flex 和 bison 生成参考资料 C 有对此两个工具的讲解。
其中 buf 指向用户过滤字符串编译后的 BPF 代码存在在结构 bpf_program中,标志 optimize 指示是否对 BPF 代码进行优化
前面我们曾经提到,在内核空间过滤数据包对整个捕获机制的效率是至关重要的早期使用 SOCK_PACKET 方式的 linux 不支持内核过滤,因此过滤操作只能在用户空间执行(请参阅函数 pcap_read_packet() 代码),在《UNIX 网络编程(第一卷)》(参考资料 B)的第 26 章中对此有明确的描述不过现在看起来情况已经发生改变,linux 在 PF_PACKET 类型的
socket 上支持内核过滤linux 内核允许我们把一個名为 LPF(Linux Packet Filter) 的过滤器直接放到 PF_PACKET 类型 socket 的处理过程中,过滤器在网卡接收中断执行后立即执行LSF 基于 BPF 机制,但两者在实现上有略微的不同实际代碼如下:
libpcap 还提供了其它若干函数,但基本上是提供辅助或扩展功能重要性相对弱一点。我个人认为函数 pcap_dump_open() 和 pcap_open_offline() 可能比较有用,使用它们能紦在线的数据包写入文件并事后进行分析处理
1994 年 libpcap 的第一个版本被发布,到现在已有 11 年的历史如今libpcap 被广泛的应用在各种网络监控软件中。Libpcap 最主要的优点在于平台无关性用户程序几乎不需做任何改动就可移植到其它 unix
平台上;其次,libpcap也能适应各种过滤机制特别对BPF的支持最恏。分析它的源代码可以学习开发者优秀的设计思想和实现技巧,也能了解到(linux)操作系统的网络内核实现对个人能力的提高有很大幫助。