使用libnet與libpcap構造TCP/IP協議軟件

使用libnet與libpcap構造TCP/IP協議軟件
內容:
概述
何謂libnet、libpcap
libnet函數庫框架和使用
libpcap函數庫框架和使用
綜合使用libnet和libpcap:ARP例程
minitcpip協議軟件系統框架:
對minitcpip的協議實現的兩點討論
私有協議簡介
minisocket用戶編程接口與例程
結論
參考資料
關於作者
在 Linux 專區還有:
教程
工具與產品
代碼與組件
項目
文章

褚蓬飛 ([email protected])
中國科學院軟件技術研究所
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函數庫實現的方案,並圍繞本文主題,對涉及到的一些關鍵技術問題進行了分析,最後,對這種實現方法做了一個簡單的總結,指出了這種實現方法的一些侷限。

何謂libnet、libpcap
目前衆多的網絡安全程序、工具和軟件都是基於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函數庫框架和使用
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函數庫開發應用程序的基本步驟以及幾個關鍵的函數使用方法簡介如下:

  1. 初始化

    
    libnet_t *libnet_init(int injection_type, char *device, char *err_buf);
    

    該函數初始化libnet函數庫,返回一個libnet_t類型的描述符,以備隨後的構造數據報和發送數據報的函數中使用。injection_type指明瞭發送數據報使用的接口類型,如數據鏈路層或者原始套接字等。Device是一個網絡設備名稱的字符串,在Linux下是"eth0"等。如果函數錯誤,返回NULL,而err_buf字符串中將攜帶有錯誤的原因。

  2. 數據報的構造

    libnet提供了豐富的數據報的構造函數,可以構造TCP/IP協議族中大多數協議的報文,還提供了一些對某些參數取默認數值的更簡練的構造函數供用戶選擇。比如libnet_autobuild_ipv4()等。

  3. 數據報的發送

    
    int libnet_write(libnet_t *l);
    

    該函數將l中描述的數據報發送的網絡上。成功將返回發送的字節數,如果失敗,返回-1。你可以調用libnet_geterror()得到錯誤的原因

  4. 退出

    
    void libnet_destroy(libnet_t *l);
    

libpcap函數庫框架和使用
libpcap的英文意思是 Packet Capture library,即數據包捕獲函數庫。該庫提供的C函數接口可用於需要捕獲經過網絡接口(通過將網卡設置爲混雜模式,可以捕獲所有經過該接口的數據報,目標地址不一定爲本機)數據包的系統開發上。著名的TCPDUMP就是在libpcap的基礎上開發而成的。libpcap提供的接口函數主要實現和封裝了與數據包截獲有關的過程。這個庫爲不同的平臺提供了一致的編程接口,在安裝了libpcap的平臺上,以libpcap爲接口寫的程序,能夠自由的跨平臺使用。在Linux系統下,libpcap可以使用BPF(Berkeley Packet Filter)分組捕獲機制來獲得很高的性能。

利用libpcap函數庫開發應用程序的基本步驟以及幾個關鍵的函數使用方法簡介如下:


  1. 
    char *pcap_lookupdev(char *errbuf)
    

    該函數用於返回可被pcap_open_live()或pcap_lookupnet()函數調用的網絡設備名(一個字符串指針)。如果函數出錯,則返回NULL,同時errbuf中存放相關的錯誤消息。


  2. 
    int pcap_lookupnet(char *device, bpf_u_int32 *netp,bpf_u_int32 *maskp, char *errbuf)
    

    獲得指定網絡設備的網絡號和掩碼。netp參數和maskp參數都是bpf_u_int32指針。如果函數出錯,則返回-1,同時errbuf中存放相關的錯誤消息。

  3. 打開設備

    
    pcap_t *pcap_open_live(char *device, int snaplen,int promisc, int to_ms,char *ebuf)
    

    獲得用於捕獲網絡數據包的數據包捕獲描述字。device參數爲指定打開的網絡設備名。snaplen參數定義捕獲數據的最大字節數。promisc指定是否將網絡接口置於混雜模式。to_ms參數指定超時時間(毫秒)。ebuf參數則僅在pcap_open_live()函數出錯返回NULL時用於傳遞錯誤消息。

  4. 編譯和設置過濾器

    
    int pcap_compile(pcap_t *p, struct bpf_program *fp,char *str, int optimize, bpf_u_int32 netmask)
    

    將str參數指定的字符串編譯到過濾程序中。fp是一個bpf_program結構的指針,在pcap_compile()函數中被賦值。optimize參數控制結果代碼的優化。netmask參數指定本地網絡的網絡掩碼。

    
    int pcap_setfilter(pcap_t *p, struct bpf_program *fp) 
    

    指定一個過濾程序。fp參數是bpf_program結構指針,通常取自pcap_compile()函數調用。出錯時返回-1;成功時返回0。抓取下一個數據包

  5. 抓取數據包

    
    int pcap_dispatch(pcap_t *p, int cnt,pcap_handler callback, u_char *user)
    

    捕獲並處理數據包。cnt參數指定函數返回前所處理數據包的最大值。cnt=-1表示在一個緩衝區中處理所有的數據包。cnt=0表示處理所有數據包,直到產生以下錯誤之一:讀取到EOF;超時讀取。callback參數指定一個帶有三個參數的回調函數,這三個參數爲:一個從pcap_dispatch()函數傳遞過來的u_char指針,一個pcap_pkthdr結構的指針,和一個數據包大小的u_char指針。如果成功則返回讀取到的字節數。讀取到EOF時則返回零值。出錯時則返回-1,此時可調用pcap_perror()或pcap_geterr()函數獲取錯誤消息。

    
    int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user) 
    

    功能基本與pcap_dispatch()函數相同,只不過此函數在cnt個數據包被處理或出現錯誤時才返回,但讀取超時不會返回。而如果爲pcap_open_live()函數指定了一個非零值的超時設置,然後調用pcap_dispatch()函數,則當超時發生時pcap_dispatch()函數會返回。cnt參數爲負值時pcap_loop()函數將始終循環運行,除非出現錯誤。

    
    u_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h) 
    

    返回指向下一個數據包的u_char指針。


  6. 
    void pcap_close(pcap_t *p)
    

    關閉p參數相應的文件,並釋放資源。

  7. 其他的輔助函數

    
    FILE *pcap_file(pcap_t *p)
    

    返回被打開文件的文件名。

    
    int pcap_fileno(pcap_t *p)
    

    返回被打開文件的文件描述字號碼。

綜合使用libnet和libpcap:ARP例程
綜合使用libnet和libpcap可以構造強有力的網絡分析、診斷、和應用程序。一個具有普遍意義的綜合使用libnet和libpcap的程序的原理框架如圖1所示:

image001.jpg

本節給出一個綜合應用libnet和libpcap的簡單例程,其功能是在接收到一個來自特定主機的ARP請求報文之後,發出ARP迴應報文,通知該主機請求的IP地址對應的MAC地址。這個程序實現了標準的ARP協議,但是卻不同於操作系統內核中標準的實現方法:該程序利用了libpcap在數據鏈路層抓包,利用了libnet向數據鏈路層發包,是使用libnet和libpcap構造TCP/IP協議軟件的一個例程。該程序很簡單,但已經可以說明libnet和libpcap的綜合使用方法:


/* tell destination host with ip 'dstip' that the host with request ip 'srcip' is with mac address srcmac
 * author: white cpf  2003.5.15.
 * compile: gcc arp.c -lnet -lpcap -o arp
 */
#include "/usr/include/libnet.h"
#include <pcap.h>
void usage(char * exename){
	printf(" tell dstip with dstmac that srcip is at srcmac. /n");
	printf(" usage: %s -d dstip -s srcip -D dstmac -S srcmac /n",exename);
	return ;
}
//程序輸入:來自命令行參數
u_char ip_src[4],ip_dst[4];
u_char enet_src[6],enet_dst[6];

extern int mac_strtochar6(u_char * enet,char * macstr);//將字符串格式的MAC地址轉換爲6字節類型r
int get_cmdline(int argc,char *argv[]);//命令行參數處理函數
int main(int argc, char *argv[]){
    libnet_t *l;
    libnet_ptag_t t;
    u_char *packet;
    u_long packet_s;
    char device[5]="eth0";
    char errbuf[LIBNET_ERRBUF_SIZE];
    char filter_str[100]="";
    struct bpf_program fp;      /* hold compiled program     */
    char *dev;
    pcap_t* descr;
    struct pcap_pkthdr hdr;     /* pcap.h    */
    u_char * packet;
    bpf_u_int32 maskp;          /* subnet mask               */
    bpf_u_int32 netp;           /* ip                        */
    int promisc=0;	             /* set to promisc mode?		*/
    int pcap_time_out=5;
    int c, ret;
    u_long i;

    if(get_cmdline(argc,argv)<=0){
usage(argv[0]);
exit(0);
    }
   
	dev = pcap_lookupdev(errbuf);
	if(dev == NULL){ 
    		fprintf(stderr,"%s/n",errbuf);
    		return -1;
    	}
    ret=pcap_lookupnet(dev,&netp,&maskp,errbuf);
	if(ret==-1){
    		fprintf(stderr,"%s/n",errbuf);
    		return -1;
	}
	descr = pcap_open_live(dev,BUFSIZ,promisc,pcap_time_out,errbuf);
    if(descr == NULL){
    	printf("pcap_open_live(): %s/n",errbuf);
    	return -1; 
    }
	sprintf(filter_str,"arp and (src net %d.%d.%d.%d)",ip_dst[0],ip_dst[1],ip_dst[2],ip_dst[3]);
	if(pcap_compile(descr,&fp,filter_str,0,netp) == -1){
		printf("Error calling pcap_compile/n"); 
		return -1;
	}
    if(pcap_setfilter(descr,&fp) == -1){ 
    	printf("Error setting filter/n"); 
    	return -1;
    }
while(1){
	printf("wait packet:filter:%s/n",filter_str);
	packet=pcap_next(descr, &hdr);
	if(packet == NULL){
	    continue;
	}
    l = libnet_init(LIBNET_LINK_ADV,device,errbuf); 
    if (l == NULL){
        fprintf(stderr, "libnet_init() failed: %s", errbuf);
        exit(EXIT_FAILURE);
    }
    t = libnet_build_arp(
            ARPHRD_ETHER,                           /* hardware addr */
            ETHERTYPE_IP,                           /* protocol addr */
            6,                                      /* hardware addr size */
            4,                                      /* protocol addr size */
            ARPOP_REPLY,                            /* operation type */
            enet_src,                               /* sender hardware addr */
            ip_src,                           /* sender protocol addr */
            enet_dst,                               /* target hardware addr */
            ip_dst,                           /* target protocol addr */
            NULL,                                   /* payload */
            0,                                      /* payload size */
            l,                                      /* libnet handle */
            0);                                     /* libnet id */
    if (t == -1){
        fprintf(stderr, "Can't build ARP header: %s/n", libnet_geterror(l));
        goto bad;
    }
    t = libnet_autobuild_ethernet(
            enet_dst,                               /* ethernet destination */
            ETHERTYPE_ARP,                          /* protocol type */
            l);                                     /* libnet handle */
    if (t == -1){
        fprintf(stderr, "Can't build ethernet header: %s/n", libnet_geterror(l));
        goto bad;
    }
    c = libnet_adv_cull_packet(l, &packet, &packet_s);
    if (c == -1){
        fprintf(stderr, "libnet_adv_cull_packet: %s/n", libnet_geterror(l));
        goto bad;
    }
    c = libnet_write(l);
    if (c == -1){
        fprintf(stderr, "Write error: %s/n", libnet_geterror(l));
        goto bad;
    }
    continue;
bad:
    libnet_destroy(l);
    return (EXIT_FAILURE);
}
    libnet_destroy(l);
    return (EXIT_FAILURE);
}
int get_cmdline(int argc,char *argv[]){
	char c;
	char string[]="d:s:D:S:h";
    while((c = getopt(argc, argv, string)) != EOF){
        if(c=='d')
            *((unsigned int*)ip_dst)=(unsigned int)inet_addr(optarg);
        else if(c== 's')
            *((unsigned int*)ip_src)=(unsigned int)inet_addr(optarg);
        else if(c=='D')
            mac_strtochar6(enet_dst,optarg);
        else if(c=='S')
            mac_strtochar6(enet_dst,optarg);
        else if(c=='h')
            return 0;
        else
            return -1;
    }
	return 1;
}

minitcpip協議軟件系統框架:
圖3與圖4是minitcpip協議軟件系統的框架圖。其中,minitcpip協議軟件在一個單獨的進程中實現。這個進程作爲TCP/IP協議軟件服務器建立C/S模型嚮應用程序提供服務。其通訊採用了命名管道建立C/S模型的方式,任何用戶的應用進程對minitcpip的使用必須作爲客戶端,通過minisocket函數庫進行。其通訊模型見圖2。

image002.jpg

協議軟件進程一旦運行,則初始化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)函數來獲得下一個抓到的數據包。

該協議軟件的原理性的實現代碼如下:


main(){
	初始化libnet、libpcap;分配接收、發送緩衝區;初始化定時器;等等
while(1){
	if(發送緩衝區有數據){
		發送數據
	}
	調用select()函數等待3個文件描述符是否準備好,這三個文件描述符分別是:PCAP文件描述符、周知口管道讀文件描述符、標準輸入文件描述符
	if(PCAP文件描述符準備好){
		調用pcap_next()函數來獲得下一個抓到的數據包
	}
	if(周知口讀文件描述符準備好){
		讀取數據
根據進程內部的TCB中的信息,按照TCP協議規範進行分析處理
	}
	if(標準輸入文件描述符準備好){
		讀取數據
分析處理,比如將內部信息回饋到標準輸出文件描述符
	}
	if(超時){
			超時處理
	}
	}
}

如上面的流程所示,在收到網絡上的數據包時,即根據TCP/IP協議IP、TCP等報文格式進行分析處理,並將接收到的數據回傳給客戶端應用程序。在收到周知管道的數據包時,則根據數據包的命令類型進行相應的操作,比如對其中的一條命令--SEND命令,在接收到這條命令後,就將其後附的數據寫入發送緩衝區,在隨後的循環中,這些數據將被依次發送到網絡上去。在周知管道與回送管道上進行的通訊採用了一個自定義的協議,後文對此作了簡單介紹。

最終系統在運行時的基本框架如下兩圖所示:其中,圖3是系統在運行時的整體結構,圖4是協議軟件進程內部的結構。

image003.jpg

image004.gif

TCP/IP協議數據處理模塊是一組函數,與關鍵數據結構TCP表(TCB)等配合,負責實現TCP/IP協議的功能。

對minitcpip的協議實現的兩點討論
經過多年的發展,目前廣泛應用的標準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標誌的報文出去,從而導致目標主機的連接復位。

這個問題有幾種可能的解決辦法:

  1. 因爲實現平臺使用了開放源代碼的Linux系統,所以可以考慮修改內核中TCP軟件實現部分(TCP軟件嚮應用進程提交數據的過濾部分)對不合法報文的缺省行爲。
  2. 如果目標主機上也使用minitcpip實現TCP/IP通訊,則我們可以修改標準TCP/IP數據報中標誌位作用的定義,比如可以忽略所有收到的帶有RST標誌的數據報,而在需要發送復位數據報的時候,改用別的保留標誌位來代替。
  3. 客戶機在建立TCP連接的時候,使用一個空閒的本網段的IP地址作爲源IP地址。這樣操作系統就會認爲所有發往該IP地址的數據報都不是本機應該接收的,從而不做反應;然而我們的minitcpip卻可以正常的收到這個數據報。當然,爲了遠端目標主機可以與這個"莫須有"的IP地址通訊,需要有一個ARP協議的運行進程,向所有發出請求的機器發送本機IP對應的MAC地址。前文中已經提供了一個類似的樣例程序,稍做修改就可實現這個功能。這個程序也可以集成在minitcpip中實現。

minitcpip使用了其中第三種解決辦法,網卡必須設置在混雜模式。這樣在網絡負載大的場合,CPU佔用率會比較高,丟包的概率會增加,效率會下降。因此minitcpip只適用於小負載的場合。

私有協議簡介
minitcpip向客戶端提供的服務採用了C/S模型,這個服務的模型圖如前圖2所示,關於其細節請參見《UNIX環境高級編程》。任何用戶的應用進程對minitcpip的使用必須通過minisocket函數庫進行,而minisocket與minitcpip之間則通過一個自定義的私有協議進行。這個私有協議實現在用戶進程與協議軟件進程之間利用命名管道建立的C/S通訊中。

命名管道已經是可靠的本地進程間通訊機制,然而minisocket與minitcpip協議軟件之間傳遞的不僅僅是發送與接收的字節流,還包括對SOCKET的控制,比如申請SOCKET號,建立TCP連接、關閉TCP連接等控制命令,所以必須設計一個簡單的通訊協議來滿足具體的通訊要求。詳細的通訊協議的規格說明這裏不再列出。

管道通訊一般不會發生數據包的丟失和錯序問題,所以該協議實質上只需要負責包的撿出(利用轉義和數據長度和校驗和)和分辨不同種類的數據包(信息包、控制包)。若管道操作本身出現錯誤,則程序異常退出。下圖5是這個私有協議使用的數據報格式:具體的數據包的檢出與字符填充方案等由於與本文主題關係不大,這裏不再詳述。

image010.gif

其中,數據域的第一個字節總是命令類型,顧名思義,命令類型定義了應用程序與協議軟件之間所有的通訊數據的類型,主要包括兩類命令,即控制命令和信息傳輸命令,控制命令用來控制TCP連接的狀態等,信息命令則是客戶機真正要傳輸的數據或服務器收到的回發給客戶機的數據。具體的命令格式列表不再詳述。

minisocket用戶編程接口與例程
客戶機通過調用minisocket接口與minitcpip通訊,這個接口封裝了私有協議繁瑣的細節,提供了類似與標準SOCKET接口的簡單統一的函數庫給用戶使用。目前實現的minisocket接口函數庫還相當簡單,功能也很有限,只包括TCP協議客戶端的實現。在編寫客戶端應用程序時,用戶只需要知道少數幾個數據結構和函數原型就可以了:


struct mini_sock{ 
                   u8 type;//=TCP or UDP
                   u32 dip;//destination ip
                   u32 dport;//destination port
                   u16 sip;//source ip
                   u16 sport;//source port
                   ……
              }

該數據結構定義在mini_socket.h中,包含了用戶需要建立的TCP連接的所有參數

用戶函數接口定義:


int mini_socket(struct mini_sock*)

該函數創建一個SOCKET,struct socket*中必須正確填寫欲創建的SOCKET有關的參數。

成功的返回值實際上是協議軟件寫入數據的客戶機專用FIFO文件描述符。這樣用戶可以使用標準的select()函數實現單進程的多個輸入輸出的處理。但是在這個文件描述符上的讀寫操作必須使用minisocket提供的函數才能正確進行。


int mini_connect(int socket)
    該函數用來通知協議軟件通過與目標主機三次握手後,建立TCP連接
int mini_recv(int socket, char* buf,int * buflen)
    該函數從指定的SOCKET接收數據
int mini_send(int socket,char*buf,int buflen)
    該函數將指定的數據發送到SOCKET
int mini_close(int socket)
    該函數用來通知協議軟件關閉指定的socket連接
使用本協議軟件庫函數實現ECHO服務的一個簡單例程如下:
/***************************************************
* Simple tcp echo client using libnet and libpcap and mini_socket.c
* file:      miniecho.c
* Date:     2003.5.
* Author:   white cpf
* compile:  gcc -Wall -lpcap -lnet miniecho.c mini_socket.c -o miniecho 
* Run:     readhat 8.0 you must be root and run ifconfig to see eth0 OK
*****************************************************/
#include <pcap.h>
#include <libnet.h>
#include "mini_socket.h"
#define uchar unsigned char
#define MAXBUFLEN 2048
char buf[MAXBUFLEN];
int buflen;
char recvbuf[MAXBUFLEN];
int recvedlen;
int main(int argc,char *argv[]){
	int ret;
	int i;
	char sip[40]="169.254.159.112";
	char sport[10]="7777";
	char dip[40]="169.254.159.111";
	char dport[10]="5000";
	struct socket ti;
	int s;
	ti.sip=inet_addr(sip);
	ti.dip=inet_addr(dip);
	ti.sport=(unsigned short)atoi(sport);
	ti.dport=(unsigned short)atoi(dport);
	s=mini_socket(&ti);
	if(s<0){
		printf("mini_socket() error/n");
		return 0;
	}
	ret=mini_connect(s);//connect to tcpip using TCP three time handshaking
	if(ret<0){
		printf("mini_connect() error/n");
		return 0;
	}
	while (1){
		//get input from stdin,  quit when EOF or "myquit!" input
		if(fgets(buf, sizeof(buf), stdin)==0)  break;
		if(strcmp(buf,"myquit!")==0)       break;
		ret=mini_send(s,buf,strlen(buf));
		if(ret<=0){
			printf("mini_send() return %d/n",ret);
			break;
		}
		ret=mini_recv(s,recvbuf,&recvedlen);
		if(ret<=0){
			printf("mini_recv() return %d/n",ret);
			break;
		}
		recvbuf[recvedlen]=0;
		printf("recved[%d bytes]:%s/n",recvedlen,recvbuf);
	}
	mini_close(s);//close tcpip using TCP three time handshaking
}

結論
本文通過使用目前廣泛流行的libnet和libpcap函數庫,在Linux環境下實現了一個單進程的TCP/IP協議軟件:minitcpip,並且提供了一個調用接口:minisocket,通過這個接口,可以創建基於TCP/IP協議的應用程序。

本文實現的minitcpip協議軟件只具有最小的TCP/IP通訊功能,效率也非常一般。但是,作爲一個學習libnet、libpcap,學習TCP/IP軟件在以太網上的實現原理的目的,這個項目卻具有一定的價值。另外,採用這種方案實現的協議與libnet、libpcap、操作系統之間的層次關係清晰,可以想象,minisocket中與環境無關的代碼可以很容易的移植到目前越來越廣泛使用的嵌入式設備的網絡應用上,這是作者寫作本文的又一個考慮。

由於時間緊張,minitcpip協議軟件的實現方法許多都有繼續斟酌之處,而且作者本身也在學習過程中,錯誤之處在所難免,本文若能爲大家學習libnet、libpcap、tcpip有所幫助,作者欣慰之至。同時希望大家能給出批評和指正,以利共同提高。

參考資源

  • www.tcpdump.org

  • www.packetfactory.net/libnet

  • 《UNIX環境高級編程》 機械工業出版社 2000年2月 W.Richard Stevens著

  • 《用TCP/IP進行網際互連 第二卷:設計、實現和內部構成》第2版 電子工業出版社 1998年 DOUGLES E.COMER DAVID L.STEVENS著

  • 《一個TCP調試系統的設計與實現》 章淼 熊勇強 吳建平 清華大學計算機系網絡研究所

關於作者
褚蓬飛,男,1976年生。1998年清華大學畢業後入青島海信集團技術中心工作。期間參加了863項目-306主題中的項目--數字化家庭信息系統家庭網關的研發工作。2002年入中國科學院軟件技術研究所多媒體通訊與網絡教研室學習,研究方向:多媒體通訊與網絡技術。你可以通過 [email protected] 與其聯繫。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章