网路监控
绝大多数的现代操作系统都提供了对底层网路数据包捕获的机制,在捕获机制之上可以构建网路监控(NetworkMonitoring)应用软件。网路监控也常简称为sniffer,其最初的目的在于对网路通讯情况进行监控,以对网路的一些异常情况进行调试处理。但随着互连网的快速普及和网路功击行为的频繁出现,保护网路的运行安全也成为监控软件的另一个重要目的。诸如,网路监控在路由器,防火墙、入侵检测等方面使用也很广泛。除此而外,它也是一种比较有效的黑客手段,比如,英国政府安全部门的"肉食植物"计划。
包捕获机制
从广义的角度上看,一个包捕获机制包含三个主要部份:最底层是针对特定操作系统的包捕获机制,最高层是针对用户程序的插口,第三部份是包过滤机制。
不同的操作系统实现的底层包捕获机制可能是不一样的,但从方式上看长治小异。数据包常规的传输路径依次为网卡、设备驱动层、数据链路层、IP层、传输层、最后抵达应用程序。而包捕获机制是在数据链路层降低一个旁路处理,对发送和接收到的数据包做过滤/缓冲等相关处理,最后直接传递到应用程序。值得注意的是,包捕获机制并不影响操作系统对数据包的网路栈处理。对用户程序而言,包捕获机制提供了一个统一的插口,使用户程序只须要简单的调用若干函数能够获得所期望的数据包。这样一来,针对特定操作系统的捕获机制对用户透明,使用户程序有比较好的可移植性。包过滤机制是对所捕获到的数据包按照用户的要求进行筛选,最终只把满足过滤条件的数据包传递给用户程序。
Libpcap应用程序框架
Libpcap提供了系统独立的用户级别网路数据包捕获插口,并充分考虑到应用程序的可移植性。Libpcap可以在绝大多数类unix平台下工作,参考资料A中是对基于libpcap的网路应用程序的一个详尽列表。在windows平台下,一个与libpcap很类似的函数包winpcap提供捕获功能,其官方网站是。
Libpcap软件包可从下载,之后依此执行下述三条命令即可安装,但若果希望libpcap能在linux上正常工作,则必须使内核支持"packet"合同,也即在编译内核时打开配置选项CONFIG_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也可获得插口信息。
Libpcap中检测网路设备中主要使用到的函数关系如右图:
libpcap调用pcap_lookupdev()函数获得可用网路插口的设备名。首先借助函数getifaddrs()获得所有网路插口的地址,以及对应的网路网段、广播地址、目标地址等相关信息,再借助add_addr_to_iflist()、add_or_find_if()、get_instance()把网路插口的信息降低到结构数组pcap_if中linux操作系统论文,最后从数组中提取第一个插口作为捕获设备。其中get_instanced()的功能是从设备名开始,找第一个是数字的字符,做为插口的实例号。网路插口的设备号越小,则排在数组的越后面,因而,一般函数最后返回的设备名为eth0。其实libpcap可以工作在回路插口上,但其实libpcap开发者觉得捕获本机进程之间的数据包没有多大意义。在检测网路设备操作中,主要用到的数据结构和代码如下:
打开网路设备
当设备找到后,下一步工作就是打开设备以打算捕获数据包。Libpcap的包捕获是构建在具体的操作系统所提供的捕获机制上,而Linux系统随着版本的不同,所支持的捕获机制也有所不同。
2.0及曾经的内核版本使用一个特殊的socket类型SOCK_PACKET,调用方式是socket(PF_INET,SOCK_PACKET,intprotocol),但Linux内核开发者明晰强调这些方法已过时。Linux在2.2及之后的版本中提供了一种新的合同簇PF_PACKET来实现捕获机制。PF_PACKET的调用方式为socket(PF_PACKET,intsocket_type,intprotocol),其中socket类型可以是SOCK_RAW和SOCK_DGRAM。SOCK_RAW类型促使数据包从数据链路层取得后,不做任何更改直接传递给用户程序,而SOCK_DRRAM则要对数据包进行加工(cooked),把数据包的数据链路层背部去除,而使用一个通用结构sockaddr_ll来保存链路信息。
使用2.0版本内核捕获数据包存在多个问题:首先kali linux,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)。
打开网路设备的主函数是pcap_open_live()[pcap-linux.c],其任务就是通过给定的插口设备名,获得一个捕获句柄:结构pcap_t。pcap_t是大多数libpcap函数都要用到的参数,其中最重要的属性则是前面讨论到的三种socket形式中的某一种。首先我们瞧瞧pcap_t的具体构成。
函数pcap_open_live()的调用方式是pcap_t*pcap_open_live(constchar*device,intsnaplen,intpromisc,intto_ms,char*ebuf),其中假如device为NULL或"any",则对所有插口捕获,snaplen代表用户期望的捕获数据包最大宽度,promisc代表设置插口为混杂模式(捕获所有抵达插口的数据包,但只有在设备给定的情况下有意义),to_ms代表函数超时返回的时间。本函数的代码比较简单,其执行步骤如下:
下边我们依次剖析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_dispatch()简单的调用捕获句柄pcap_t中定义的特定操作系统的读数据函数:returnp->read_op(p,cnt,callback,user)。在linux系统下,对应的读函数为pcap_read_linux()(在创建捕获句柄时已定义[pcap-linux.c]),而pcap_read_linux()则是直接调用pcap_read_packet()([pcap-linux.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。从理论研究的意思上看,包过滤机制是一个物理问题,或则说是一个算法问题,其中心任务是怎样使用最少的判定操作、最少的时间完成过滤处理,增强过滤效率。
BPF
Libpcap重点使用BPF(BSDPacketFilter)包过滤机制,BPF于1992年被设计下来,其设计目的主要是解决当时已存在的过滤机制效率低下的问题。BPF的工作步骤如下:当一个数据包抵达网路插口时,数据链路层的驱动会把它向系统的合同栈传送。但若果BPF窃听插口,驱动首先调用BPF。BPF首先进行过滤操作,之后把数据包储存在过滤器相关的缓冲区中,最后设备驱动再度获得控制。注意到BPF是先对数据包过滤再缓冲linux内核防火墙源代码分析,防止了类似sun的NIT过滤机制先缓冲每位数据包直至用户读数据时再过滤所导致的效率问题。参考资料D是关于BPF设计思想最重要的文献。
BPF的设计思想和当时的计算机硬件的发展有很大联系,相对旧式的过滤方法CSPF(CMU/StanfordPacketFilter)它有两大特征。1:基于寄存器的过滤机制,而不是初期显存堆栈过滤机制,2:直接使用独立的、非共享的显存缓冲区。同时,BPF在过滤算法是也有很大进步,它使用无环控制流图(CFGcontrolflowgraph),而不是旧式的布尔表达式树(booleanexpressiontree)。布尔表达式树理解上比较直观,它的每一个叶子节点即是一个子句判定,而非叶子节点则为AND操作或OR操作。CSPF有三个主要的缺点。1:过滤操作使用的栈在显存中被模拟,维护栈表针须要使用若干的加/减等操作,而显存操作是现代计算机构架的主要困局。2:布尔表达式树引起了不须要的重复估算。3:不能剖析数据包的变长颈部。BPF使用的CFG算法实际上是一种特殊的状态机,每一节点代表了一个子句判定,而左右边分别对应了判定失败和成功后的跳转,跳转后又是子句判定,这样反复操作,直至抵达成功或失败的终点。CFG算法的优点在于把对数据包的剖析信息直接构建在图中,因而不须要重复估算。直观的看linux内核防火墙源代码分析,CFG是一种"快速的、一直往前"的算法。
过滤代码的编译
BPF对CFG算法的代码实现极其复杂,它使用伪机器形式。BPF伪机器是一个轻量级的,高效的状态机,对BPF过滤代码进行解释处理。BPF过滤代码方式为"opcodejtjfk",分别代表了操作码和主存形式、判断正确的跳转、判断失败的跳转、操作使用的通用数据域。BPF过滤代码从逻辑上看很类似于汇编语言,但它实际上是机器语言,注意到上述4个域的数据类型都是int和char型。其实,由用户来写过滤代码太过复杂,因而libpcap容许用户书写高层的、容易理解的过滤字符串,之后将其编译为BPF代码。
Libpcap使用了4个源程序gencode.c、optimize.c、grammar.c、scanner.c完成编译操作,其中前两个实现了对过滤字符串的编译和优化,后两个主要是为编译提供从合同相关过滤条件到合同无关(的字符字段)位置信息的映射,但是它们由词汇剖析器生成器flex和bison生成。参考资料C有对此两个工具的讲解。
编译过滤字符串调用了函数pcap_compile()[getcode.c],方式为:
其中buf指向用户过滤字符串,编译后的BPF代码存在在结构bpf_program中,标志optimize指示是否对BPF代码进行优化。
过滤代码的安装
后面我们当初提及,在内核空间过滤数据包对整个捕获机制的效率是至关重要的。初期使用SOCK_PACKET形式的Linux不支持内核过滤,因而过滤操作只能在用户空间执行(请参阅函数pcap_read_packet()代码),在《UNIX网路编程(第一卷)》(参考资料B)的第26章中对此有明晰的描述。不过如今看上去情况早已发生改变,linux在PF_PACKET类型的socket上支持内核过滤。Linux内核容许我们把一个名为LPF(LinuxPacketFilter)的过滤器直接放在PF_PACKET类型socket的处理过程中,过滤器在网卡接收中断执行后立刻执行。LSF基于BPF机制,但二者在实现上有略微的不同。实际代码如下:
linux在安装和卸载过滤器时都使用了函数setsockopt(),其中标志SOL_SOCKET代表了对socket进行设置,而SO_ATTACH_FILTER和SO_DETACH_FILTER则分别对应了安装和卸载。下边是linux2.4.29版本中的相关代码:
里面出现的sk_attach_filter()定义在net/core/filter.c,它把结构sock_fprog转换为结构sk_filter,最后把此结构设置为socket的过滤器:sk->filter=fp。
其他代码
libpcap还提供了其它若干函数,但基本上是提供辅助或扩充功能,重要性相对弱一点。我个人觉得,函数pcap_dump_open()和pcap_open_offline()可能比较有用,使用它们能把在线的数据包写入文件并事后进行剖析处理。
总结
1994年libpcap的第一个版本被发布,到如今已有11年的历史,现在libpcap被广泛的应用在各类网路监控软件中。Libpcap最主要的优点在于平台无关性,用户程序几乎不需做任何改动就可移植到其它unix平台上;其次,libpcap也能适应各类过滤机制,非常对BPF的支持最好。剖析它的源代码,可以学习开发者优秀的设计思想和实现方法,也能了解到(linux)操作系统的网路内核实现,对个人能力的提升有很大帮助。
参考资料
A:《Libpcap,winpcap,libdnet,andlibnetapplicationsandresources》
B:《UNIX网路编程(第一卷)》W.RichardStevens
C:《使用lex和yacc编译代码》PeterSeebach
D:《TheBSDPacketFilter:ANewArchitectureforUser-levelPacketCapture》StevenMcCanneandVanJacobson
E:linux联机帮助指南:socket(2)、socket(7)、packet等
F:《xPFPacketFilteringforLow-CostNetworkMonitoring》
G:《Plabapacketcaptureandanalysisarchitecture》
H:《AcompilerforPacketFilters》