分类: C/C++
2006-10-30 21:40:04
褚蓬飞 ()
中国科学院软件技术研究所
2003年6月1日
本文在RED HAT Linux8.0+以太网环境下,利用libnet和libpcap库实现了一个以太网上用户态的单进程的TCP/IP协议软件包:minitcpip, 该软件实现了TCP协议的基本通讯功能,并提供了一个调试接口和一个与标准SOCKET接口类似的接口函数库minisocket,方便用户的调试与应用 软件的调用。这个用户态的协议软件包的实现,为学习综合使用libnet和libpcap提供了良好的范例;通过对这个软件包的学习,还可以加深对 TCP/IP协议(尤其是在以太网上)的运行原理的理解;另外,由于这个软件包运行在单进程、用户态环境下,也为调试和学习带来了极大的方便。
目前有许多不同的成熟的TCP/IP协
议的实现版本,其中大部分都在操作系统的核心实现,这种方案固然是提高TCP/IP协议软件的效率的必然所选,但却给TCP/IP协议的学习、研究和调试
带来了很大的困难。于是,如果不考虑TCP/IP协议软件实现的效率问题,在应用进程中实现一个TCP/IP协议软件,是具有一定的意义和价值的。
本文作者构造了一个单进程的TCP/IP协议软件:minitcpip,并提供了一个SOCKET接口函数库:minisocket。在实现这个协 议软件函数库时,作者选择采用了libnet+libpcap的方式在用户态下实现这个软件,不仅是因为这样可以避开一些操作系统对底层网络开发的种种限 制带来的不便,将精力集中在对协议软件本身的理解上;另外一个原因,则是为大家学习和综合使用libnet和libpcap提供一个范例。
下文首先介绍了libnet和libpcap函数库及其使用,并给出了一个利用其实现ARP协议的例程--该协议的实现也包括在minitcpip 软件之中,然后给出了本文的协议软件和SOCKET函数库实现的方案,并围绕本文主题,对涉及到的一些关键技术问题进行了分析,最后,对这种实现方法做了 一个简单的总结,指出了这种实现方法的一些局限。
目前众多
的网络安全程序、工具和软件都是基于socket设计和开发的。由于在安全程序中通常需要对网络通讯的细节(如连接双方地址/端口、服务类型、传输控制
等)进行检查、处理或控制,象数据包截获、数据包头分析、数据包重写、甚至截断连接等,都几乎在每个网络安全程序中必须实现。为了简化网络安全程序的编写
过程,提高网络安全程序的性能和健壮性,同时使代码更易重用与移植,最好的方法就是将最常用和最繁复的过程函数,如监听套接口的打开/关闭、数据包截获、
数据包构造/发送/接收等,封装起来,以API library的方式提供给开发人员使用。
在众多的API library中,对于类Unix系统平台上的网络安全工具开发而言,目前最为流行的C API library有libnet、libpcap、libnids和libicmp等。它们分别从不同层次和角度提供了不同的功能函数。使网络开发人员能够 忽略网络底层细节的实现,从而专注于程序本身具体功能的设计与开发。其中,
libnet提供的接口函数主要实现和封装了数据包的构造和发送过程。
libpcap提供的接口函数主要实现和封装了与数据包截获有关的过程。
利用这些C函数库的接口,网络安全工具开发人员可以很方便地编写出具有结构化强、健壮性好、可移植性高等特点的程序。因此,这些函数库在网络安全工 具的开发中具有很大的价值,在scanner、sniffer、firewall、IDS等领域都获得了极其广泛的应用,著名的tcpdump软件、 ethereal软件等就是在libpcap的基础上开发的。
另外也应该指出:由于其功能强大,这些函数库也被黑客用来构造TCP/IP网络程序对目标主机进行攻击。然而,TCP/IP网络的安全不可能也不应 该建立在禁止大家使用工具的基础上,一个理想的网络,首先必须是一个开放的网络,这个网络应该在使用任何工具的情况下都是安全的和健壮的。从这点考虑,这 些工具的使用,对促进现有网络系统的不断完善是大有裨益的。
libnet是一个小型的接口函数库,主要用C语言写成,提供了低层网络数据报的构造、处理和发送功能。libnet的开发目的是:建立一个简单统一的网络编程接口以屏蔽不同操作系统低层网络编程的差别,使得程序员将精力集中在解决关键问题上。他的主要特点是:
高层接口:libnet主要用C语言写成
可移植性:libnet目前可以在Linux、FreeBSD、Solaris、WindowsNT等操作系统上运行,并且提供了统一的接口
数据报构造:libnet提供了一系列的TCP/IP数据报文的构造函数以方便用户使用
数据报的处理:libnet提供了一系列的辅助函数,利用这些辅助函数,帮助用户简化那些烦琐的事务性的编程工作
数据报发送:libnet允许用户在两种不同的数据报发送方法中选择。
另外libnet允许程序获得对数据报的绝对的控制,其中一些是传统的网络程序接口所不提供的。这也是libnet的魅力之一。
libnet支持TCP/IP协议族中的多种协议,比如其上一个版本libnet1.0支持了10种协议,一些新的协议,比如对IPV6的支持还在开发之中。
libnet目前最新的版本是1.1版本,在该版本中,作者将这些函数做了进一步的封装,用户的使用步骤也得到了进一步的简化。内存的初始化、管 理、释放等以及校验和的计算等函数,在默认情况下,都无须用户直接干预,使得libnet的使用更为方便。作者还提供了基于老版本的应用程序移植到新版本 上的方法指导。利用libnet1.1函数库开发应用程序的基本步骤以及几个关键的函数使用方法简介如下:
|
该函数初始化libnet函数库,返回一个libnet_t类型的描述符,以备随后的构造数据报和发送数据报的函数中使用。 injection_type指明了发送数据报使用的接口类型,如数据链路层或者原始套接字等。Device是一个网络设备名称的字符串,在Linux下 是"eth0"等。如果函数错误,返回NULL,而err_buf字符串中将携带有错误的原因。
libnet提供了丰富的数据报的构造函数,可以构造TCP/IP协议族中大多数协议的报文,还提供了一些对某些参数取默认数值的更简练的构造函数供用户选择。比如libnet_autobuild_ipv4()等。
|
该函数将l中描述的数据报发送的网络上。成功将返回发送的字节数,如果失败,返回-1。你可以调用libnet_geterror()得到错误的原因
|
libpcap
的英文意思是 Packet Capture
library,即数据包捕获函数库。该库提供的C函数接口可用于需要捕获经过网络接口(通过将网卡设置为混杂模式,可以捕获所有经过该接口的数据报,目
标地址不一定为本机)数据包的系统开发上。著名的TCPDUMP就是在libpcap的基础上开发而成的。libpcap提供的接口函数主要实现和封装了
与数据包截获有关的过程。这个库为不同的平台提供了一致的编程接口,在安装了libpcap的平台上,以libpcap为接口写的程序,能够自由的跨平台
使用。在Linux系统下,libpcap可以使用BPF(Berkeley Packet Filter)分组捕获机制来获得很高的性能。
利用libpcap函数库开发应用程序的基本步骤以及几个关键的函数使用方法简介如下:
|
该函数用于返回可被pcap_open_live()或pcap_lookupnet()函数调用的网络设备名(一个字符串指针)。如果函数出错,则返回NULL,同时errbuf中存放相关的错误消息。
|
获得指定网络设备的网络号和掩码。netp参数和maskp参数都是bpf_u_int32指针。如果函数出错,则返回-1,同时errbuf中存放相关的错误消息。
|
获得用于捕获网络数据包的数据包捕获描述字。device参数为指定打开的网络设备名。snaplen参数定义捕获数据的最大字节数。 promisc指定是否将网络接口置于混杂模式。to_ms参数指定超时时间(毫秒)。ebuf参数则仅在pcap_open_live()函数出错返回 NULL时用于传递错误消息。
|
将str参数指定的字符串编译到过滤程序中。fp是一个bpf_program结构的指针,在pcap_compile()函数中被赋值。optimize参数控制结果代码的优化。netmask参数指定本地网络的网络掩码。
|
指定一个过滤程序。fp参数是bpf_program结构指针,通常取自pcap_compile()函数调用。出错时返回-1;成功时返回0。抓取下一个数据包
|
捕获并处理数据包。cnt参数指定函数返回前所处理数据包的最大值。cnt=-1表示在一个缓冲区中处理所有的数据包。cnt=0表示处理所有数据 包,直到产生以下错误之一:读取到EOF;超时读取。callback参数指定一个带有三个参数的回调函数,这三个参数为:一个从 pcap_dispatch()函数传递过来的u_char指针,一个pcap_pkthdr结构的指针,和一个数据包大小的u_char指针。如果成功 则返回读取到的字节数。读取到EOF时则返回零值。出错时则返回-1,此时可调用pcap_perror()或pcap_geterr()函数获取错误消 息。
|
功能基本与pcap_dispatch()函数相同,只不过此函数在cnt个数据包被处理或出现错误时才返回,但读取超时不会返回。而如果为 pcap_open_live()函数指定了一个非零值的超时设置,然后调用pcap_dispatch()函数,则当超时发生时 pcap_dispatch()函数会返回。cnt参数为负值时pcap_loop()函数将始终循环运行,除非出现错误。
|
返回指向下一个数据包的u_char指针。
|
关闭p参数相应的文件,并释放资源。
|
返回被打开文件的文件名。
|
返回被打开文件的文件描述字号码。
综合使用libnet和libpcap可以构造强有力的网络分析、诊断、和应用程序。一个具有普遍意义的综合使用libnet和libpcap的程序的原理框架如图1所示:
本节给出一个综合应用libnet和libpcap的简单例程,其功能是在接收到一个来自特定主机的ARP请求报文之后,发出ARP回应报文,通知 该主机请求的IP地址对应的MAC地址。这个程序实现了标准的ARP协议,但是却不同于操作系统内核中标准的实现方法:该程序利用了libpcap在数据 链路层抓包,利用了libnet向数据链路层发包,是使用libnet和libpcap构造TCP/IP协议软件的一个例程。该程序很简单,但已经可以说 明libnet和libpcap的综合使用方法:
|
图3
与图4是minitcpip协议软件系统的框架图。其中,minitcpip协议软件在一个单独的进程中实现。这个进程作为TCP/IP协议软件服务器建
立C/S模型向应用程序提供服务。其通讯采用了命名管道建立C/S模型的方式,任何用户的应用进程对minitcpip的使用必须作为客户端,通过
minisocket函数库进行。其通讯模型见图2。
协议软件进程一旦运行,则初始化libnet、libpcap,初始化TCP/IP连接管理表(TCB)以及接收与发送缓冲区,打开众所周知的 FIFO等操作,然后等待客户机发来的命令(通过众所周知的FIFO)。在收到合法的命令,包括建立连接、发送数据、接收数据、关闭连接和设置连接属性等 等之后,就作出相应的分析与处理。比如根据命令中指定的源IP、目的IP、源端口、目的端口开始监控和接收过滤网络上的数据(如下文,实际是监控三个文件 描述符)等等。
为了方便监控和调试协议软件内部状态,协议软件同时等待标准输入设备发来的命令,协议软件将根据标准输入的合法命令形成到标准输出设备的输出。
除了在周知口等待客户机的命令、在标准输入设备等待监控命令之外,协议软件还必须同时等待来自网络设备的数据。为了在同一个进程中可以高效率的处理 这些不同来源的数据,软件通过使用libpcap提供的函数接口int pcap_fileno(pcap_t *p),得到被打开的网络设备文件的文件描述符。在得到这个描述符之后,就可以和管道文件描述符同等的使用select()函数进行并行处理了。
在网络设备文件的文件描述字可读时,软件将调用u_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h)函数来获得下一个抓到的数据包。
该协议软件的原理性的实现代码如下:
|
如上面的流程所示,在收到网络上的数据包时,即根据TCP/IP协议IP、TCP等报文格式进行分析处理,并将接收到的数据回传给客户端应用程序。 在收到周知管道的数据包时,则根据数据包的命令类型进行相应的操作,比如对其中的一条命令--SEND命令,在接收到这条命令后,就将其后附的数据写入发 送缓冲区,在随后的循环中,这些数据将被依次发送到网络上去。在周知管道与回送管道上进行的通讯采用了一个自定义的协议,后文对此作了简单介绍。
最终系统在运行时的基本框架如下两图所示:其中,图3是系统在运行时的整体结构,图4是协议软件进程内部的结构。
TCP/IP协议数据处理模块是一组函数,与关键数据结构TCP表(TCB)等配合,负责实现TCP/IP协议的功能。
经过多年的发展,目前广泛应用的标准TCP/IP软件已经能够支持以太网、串行链路等多种物理设备,本文所讨论的实现主要是集中在以太网之上。
下面的讨论主要集中在定时器的设置和与操作系统的互斥两个问题上。
实际应用的TCP/IP协议软件是一个非常复杂的系统,具有流量控制和拥塞控制机制,一般都是由TCP/IP输出进程、输入进程、定时器进程等多个 系统进程配合完成。而实现这些机制的一个重要的基础是定时器的设置与处理。本文中minitcpip由于是在单进程中实现,难以实现复杂的精确定时功能, 所以在定时器的处理上进行了相当的简化。其中,TCP协议状态机中的TIME_WAIT状态定时间隔是一个基础参数,一旦minitcpip运行之后,这 个参数就不会改变,或者只能通过标准输入口进行调试目的的人工修改;每一个TCP/IP连接的重发定时器的策略则是根据该连接所有的数据报的接收效果而建 立的,具体实现是设置一个最小时间间隔参数和一个最大时间间隔参数,系统初始化后,重发定时间隔取最小值,此后每发生一次超时(没有收到该连接的任何数据 报或者没有收到具有ACK标志且确认序号正确的数据报),就将该重发定时间隔翻倍,直到达到最大值为止;如果在正确的收到若干数据报后都没有发生超时,就 将重发定时时间间隔减半,直到达到最小数值。系统通过重发定时间隔与系统当前时钟减去保存的上次发送的时钟值的比较结果得出是否应该重新发送分组。而 select()函数所使用的溢出时间则取所有连接的定时间隔的最小数值,以保证及时的发送分组。这样的策略当然效率不高,但是已经可以保证协议软件的正 确运行。
另外,minitcpip协议软件是在本机同时存在一个标准TCP/IP协议栈的情况下实现的,BPF的原理是复制而不是截获本机收到的数据报文。 因此,操作系统也会对收到的报文进行处理,这个问题如果处理不好,就会存在一些与操作系统内部的TCP/IP软件相互干扰的问题。比如在源主机上,如果指 定的源端口已经打开,则势必要影响本来正常运行的程序,最后导致二者都不能正常工作。同时,在使用minitcpip与远端目标主机上的标准TCP/IP 协议软件建立连接的时候,本机的操作系统也会收到目标主机的报文,并且发现这个报文指定的IP地址和端口并不在打开的端口表项中,于是操作系统认为收到了 一个不合法的报文。许多操作系统对这一事件的反应是发送一个带有RST标志的报文出去,从而导致目标主机的连接复位。
这个问题有几种可能的解决办法:
minitcpip使用了其中第三种解决办法,网卡必须设置在混杂模式。这样在网络负载大的场合,CPU占用率会比较高,丢包的概率会增加,效率会下降。因此minitcpip只适用于小负载的场合。
minitcpip向客户端提
供的服务采用了C/S模型,这个服务的模型图如前图2所示,关于其细节请参见《UNIX环境高级编程》。任何用户的应用进程对minitcpip的使用必
须通过minisocket函数库进行,而minisocket与minitcpip之间则通过一个自定义的私有协议进行。这个私有协议实现在用户进程与
协议软件进程之间利用命名管道建立的C/S通讯中。
命名管道已经是可靠的本地进程间通讯机制,然而minisocket与minitcpip协议软件之间传递的不仅仅是发送与接收的字节流,还包括对 SOCKET的控制,比如申请SOCKET号,建立TCP连接、关闭TCP连接等控制命令,所以必须设计一个简单的通讯协议来满足具体的通讯要求。详细的 通讯协议的规格说明这里不再列出。
管道通讯一般不会发生数据包的丢失和错序问题,所以该协议实质上只需要负责包的捡出(利用转义和数据长度和校验和)和分辨不同种类的数据包(信息 包、控制包)。若管道操作本身出现错误,则程序异常退出。下图5是这个私有协议使用的数据报格式:具体的数据包的检出与字符填充方案等由于与本文主题关系 不大,这里不再详述。
其中,数据域的第一个字节总是命令类型,顾名思义,命令类型定义了应用程序与协议软件之间所有的通讯数据的类型,主要包括两类命令,即控制命令和信 息传输命令,控制命令用来控制TCP连接的状态等,信息命令则是客户机真正要传输的数据或服务器收到的回发给客户机的数据。具体的命令格式列表不再详述。
客
户机通过调用minisocket接口与minitcpip通讯,这个接口封装了私有协议繁琐的细节,提供了类似与标准SOCKET接口的简单统一的函数
库给用户使用。目前实现的minisocket接口函数库还相当简单,功能也很有限,只包括TCP协议客户端的实现。在编写客户端应用程序时,用户只需要
知道少数几个数据结构和函数原型就可以了:
|
该数据结构定义在mini_socket.h中,包含了用户需要建立的TCP连接的所有参数
用户函数接口定义:
|
该函数创建一个SOCKET,struct socket*中必须正确填写欲创建的SOCKET有关的参数。
成功的返回值实际上是协议软件写入数据的客户机专用FIFO文件描述符。这样用户可以使用标准的select()函数实现单进程的多个输入输出的处理。但是在这个文件描述符上的读写操作必须使用minisocket提供的函数才能正确进行。
|
本文通过使用目前广泛流行的libnet和libpcap函数库,在Linux环境下实现了一个单进程的TCP/IP协议软件:minitcpip,并且提供了一个调用接口:minisocket,通过这个接口,可以创建基于TCP/IP协议的应用程序。
本文实现的minitcpip协议软件只具有最小的TCP/IP通讯功能,效率也非常一般。但是,作为一个学习libnet、libpcap,学习 TCP/IP软件在以太网上的实现原理的目的,这个项目却具有一定的价值。另外,采用这种方案实现的协议与libnet、libpcap、操作系统之间的 层次关系清晰,可以想象,minisocket中与环境无关的代码可以很容易的移植到目前越来越广泛使用的嵌入式设备的网络应用上,这是作者写作本文的又 一个考虑。
由于时间紧张,minitcpip协议软件的实现方法许多都有继续斟酌之处,而且作者本身也在学习过程中,错误之处在所难免,本文若能为大家学习libnet、libpcap、tcpip有所帮助,作者欣慰之至。同时希望大家能给出批评和指正,以利共同提高。