基於Linux平臺的libpcap源代碼分析a

libpcap  unix/linux 平臺下的網絡數據包捕獲函數包,大多數網絡監控軟件都以它爲基礎。Libpcap 可以在絕大多數類 unix 平臺下工作,本文分析了 libpcap  linux 下的源代碼實現,其中重點是 linux 的底層包捕獲機制和過濾器設置方式,同時也簡要的討論了 libpcap 使用的包過濾機制 BPF
網絡監控
絕大多數的現代操作系統都提供了對底層網絡數據包捕獲的機制,在捕獲機制之上可以建立網絡監控(Network Monitoring)應用軟件。網絡監控也常簡稱爲sniffer,其 最初的目的在於對網絡通信情況進行監控,以對網絡的一些異常情況進行調試處理。但隨着互連網的快速普及和網絡攻擊行爲的頻繁出現,保護網絡的運行安全也成 爲監控軟件的另一個重要目的。例如,網絡監控在路由器,防火牆、入侵檢查等方面使用也很廣泛。除此而外,它也是一種比較有效的黑客手段,例如,美國政府安 全部門的"肉食動物"計劃。

包捕獲機制
從廣義的角度上看,一個包捕獲機制包含三個主要部分:最底層是針對特定操作系統的包捕獲機制,最高層是針對用戶程序的接口,第三部分是包過濾機制。

不同的操作系統實現的底層包捕獲機制可能是不一樣的,但從形式上看大同小異。數據包常規的傳輸路徑依次爲網卡、設備驅動層、數據鏈路層、IP 層、傳輸層、最後到達應用程序。而包捕獲機制是在數據鏈路層增加一個旁路處理,對發送和接收到的數據包做過濾/緩 衝等相關處理,最後直接傳遞到應用程序。值得注意的是,包捕獲機制並不影響操作系統對數據包的網絡棧處理。對用戶程序而言,包捕獲機制提供了一個統一的接 口,使用戶程序只需要簡單的調用若干函數就能獲得所期望的數據包。這樣一來,針對特定操作系統的捕獲機制對用戶透明,使用戶程序有比較好的可移植性。包過 濾機制是對所捕獲到的數據包根據用戶的要求進行篩選,最終只把滿足過濾條件的數據包傳遞給用戶程序。

Libpcap 
應用程序框架
Libpcap 
提供了系統獨立的用戶級別網絡數據包捕獲接口,並充分考慮到應用程序的可移植性。Libpcap 可以在絕大多數類 unix 平臺下工作,參考資料 A 中是對基於 libpcap 的網絡應用程序的一個詳細列表。在 windows 平臺下,一個與libpcap 很類似的函數包 winpcap 提供捕獲功能,其官方網站是http://winpcap.polito.it/

Libpcap 
軟件包可從 http://www.tcpdump.org/  下載,然後依此執行下列三條命令即可安裝,但如果希望 libpcap 能在 linux 上正常工作,則必須使內核支持"packet"協議,也即在編譯內核時打開配置選項 CONFIG_PACKET(選項缺省爲打開)



./configure
./make
./make install



libpcap 
源代碼由 20 多個 C 文件構成,但在 Linux 系統下並不是所有文件都用到。可以通過查看命令 make 的輸出瞭解實際所用的文件。本文所針對的libpcap 版本號爲 0.8.3,網絡類型爲常規以太網。Libpcap 應用程序從形式上看很簡單,下面是一個簡單的程序框架:



char * device; /* 
用來捕獲數據包的網絡接口的名稱 */
pcap_t * p; /* 
捕獲數據包句柄,最重要的數據結構 */
struct bpf_program fcode; /* BPF 
過濾代碼結構 */

/* 
第一步:查找可以捕獲數據包的設備 */
device = pcap_lookupdev(errbuf)


/* 
第二步:創建捕獲句柄,準備進行捕獲 */
p = pcap_open_live(device, 8000, 1, 500, errbuf)


/* 
第三步:如果用戶設置了過濾條件,則編譯和安裝過濾代碼 */
pcap_compile(p, &fcode, filter_string, 0, netmask)

pcap_setfilter(p, &fcode)


/* 
第四步:進入(死)循環,反覆捕獲數據包 */
for( ; ; )
{
while((ptr = (char *)(pcap_next(p, &hdr))) == NULL);
           
/* 
第五步:對捕獲的數據進行類型轉換,轉化成以太數據包類型 */
eth = (struct libnet_ethernet_hdr *)ptr;

/* 
第六步:對以太頭部進行分析,判斷所包含的數據包類型,做進一步的處理 */
if(eth->ether_type == ntohs(ETHERTYPE_IP)) 
…………
if(eth->ether_type == ntohs(ETHERTYPE_ARP)) 
…………
}
     
/* 
最後一步:關閉捕獲句柄,一個簡單技巧是在程序初始化時增加信號處理函數,
以便在程序退出前執行本條代碼 */
pcap_close(p)




檢查網絡設備
libpcap 
程序的第一步通常是在系統中找到合適的網絡接口設備。網絡接口在Linux 網絡體系中是一個很重要的概念,它是對具體網絡硬件設備的一個抽象,在它的下面是具體的網卡驅動程序,而其上則是網絡協議層。Linux 中最常見的接口設備名 eth0  loLo 稱爲迴路設備,是一種邏輯意義上的設備,其主要目的是爲了調試網絡程序之間的通訊功能。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 中,最後從鏈表中提取第一個接口作爲捕獲設備。其中 get_instanced() 的功能是從設備名開始,找第一個是數字的字符,做爲接口的實例號。網絡接口的設備號越小,則排在鏈表的越前面,因此,通常函數最後返回的設備名爲 eth0。雖然 libpcap 可以工作在迴路接口上,但顯然 libpcap 開發者認爲捕獲本機進程之間的數據包沒有多大意義。在檢查網絡設備操作中,主要用到的數據結構和代碼如下:



     /* libpcap 
自定義的接口信息鏈表 [pcap.h] */
struct pcap_if 
{
struct pcap_if *next; 
char *name; /* 
接口設備名 */
char *description; /* 
接口描述 */
           
/*
接口的 IP 地址地址掩碼廣播地址,目的地址 */
struct pcap_addr addresses; 
bpf_u_int32 flags;      /* 
接口的參數 */
};

char * pcap_lookupdev(register char * errbuf)
{
     pcap_if_t *alldevs;
     ……
           pcap_findalldevs(&alldevs, errbuf)

           ……
           strlcpy(device, alldevs->name, sizeof(device));
     }
     


打開網絡設備
當設備找到後,下一步工作就是打開設備以準備捕獲數據包。Libpcap 的包捕獲是建立在具體的操作系統所提供的捕獲機制上,而 Linux 系統隨着版本的不同,所支持的捕獲機制也有所不同。

2.0 
及以前的內核版本使用一個特殊的 socket 類型 SOCK_PACKET,調用形式是 socket(PF_INET, SOCK_PACKET, int protocol),但 Linux 內核開發者明確指出這種方式已過時。Linux  2.2 及以後的版本中提供了一種新的協議簇 PF_PACKET 來實現捕獲機制。PF_PACKET 的調用形式爲 socket(PF_PACKET, int socket_type, int protocol),其中 socket 類型可以是 SOCK_RAW  SOCK_DGRAMSOCK_RAW 類型使得數據包從數據鏈路層取得後,不做任何修改直接傳遞給用戶程序,而 SOCK_DRRAM 則要對數據包進行加工(cooked),把數據包的數據鏈路層頭部去掉,而使用一個通用結構 sockaddr_ll 來保存鏈路信息。

使用 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_tpcap_t 是大多數 libpcap 函數都要用到的參數,其中最重要的屬性則是上面討論到的三種 socket 方式中的某一種。首先我們看看 pcap_t 的具體構成。



struct pcap [pcap-int.h]

     int fd; /* 
文件描述字,實際就是 socket */
     
           /* 
 socket 上,可以使用 select()  poll()  I/O 複用類型函數 */
     int selectable_fd; 

     int snapshot; /* 
用戶期望的捕獲數據包最大長度 */
     int linktype; /* 
設備類型 */
     int tzoff;            /* 
時區位置,實際上沒有被使用 */
     int offset;      /* 
邊界對齊偏移量 */

     int break_loop; /* 
強制從讀數據包循環中跳出的標誌 */

     struct pcap_sf sf; /* 
數據包保存到文件的相關配置數據結構 */
     struct pcap_md md; /* 
具體描述如下 */
     
     int bufsize; /* 
讀緩衝區的長度 */
     u_char buffer; /* 
讀緩衝區指針 */
     u_char *bp;
     int cc;
     u_char *pkt;

     /* 
相關抽象操作的函數指針,最終指向特定操作系統的處理函數 */
     int      (*read_op)(pcap_t *, int cnt, pcap_handler, u_char *);
     int      (*setfilter_op)(pcap_t *, struct bpf_program *);
     int      (*set_datalink_op)(pcap_t *, int);
     int      (*getnonblock_op)(pcap_t *, char *);
     int      (*setnonblock_op)(pcap_t *, int, char *);
     int      (*stats_op)(pcap_t *, struct pcap_stat *);
     void (*close_op)(pcap_t *);

     /*
如果 BPF 過濾代碼不能在內核中執行,則將其保存並在用戶空間執行 */
     struct bpf_program fcode; 

     /* 
函數調用出錯信息緩衝區 */
     char errbuf[PCAP_ERRBUF_SIZE + 1]; 
     
     /* 
當前設備支持的、可更改的數據鏈路類型的個數 */
     int dlt_count;
     /* 
可更改的數據鏈路類型號鏈表,在 linux 下沒有使用 */
     int *dlt_list;

     /* 
數據包自定義頭部,對數據包捕獲時間、捕獲長度、真實長度進行描述 [pcap.h] */
     struct pcap_pkthdr pcap_header;      
};

/* 
包含了捕獲句柄的接口、狀態、過濾信息 [pcap-int.h] */
struct pcap_md {
/* 
捕獲狀態結構 [pcap.h] */
struct pcap_stat stat; 

     int use_bpf; /* 
如果爲1,則代表使用內核過濾*/ 
     u_long      TotPkts; 
     u_long      TotAccepted; /* 
被接收數據包數目 */ 
     u_long      TotDrops;      /* 
被丟棄數據包數目 */ 
     long      TotMissed;      /* 
在過濾進行時被接口丟棄的數據包數目 */
     long      OrigMissed; /*
在過濾進行前被接口丟棄的數據包數目*/
#ifdef linux
     int      sock_packet; /* 
如果爲 1,則代表使用 2.0 內核的 SOCK_PACKET 模式 */
     int      timeout;      /* pcap_open_live() 
函數超時返回時間*/ 
     int      clear_promisc; /* 
關閉時設置接口爲非混雜模式 */ 
     int      cooked;            /* 
使用 SOCK_DGRAM 類型 */
     int      lo_ifindex;      /* 
迴路設備索引號 */
     char *device;      /* 
接口設備名稱 */ 
     
/* 
以混雜模式打開 SOCK_PACKET 類型 socket  pcap_t 鏈表*/
struct pcap *next;      
#endif
};



函數 pcap_open_live() 的調用形式

 pcap_t * pcap_open_live(const char *device, int snaplen, int promisc, int to_ms, char *ebuf),其中如果 device  NULL "any",則對所有接口捕獲,snaplen 代表用戶期望的捕獲數據包最大長度,promisc 代表設置接口爲混雜模式(捕獲所有到達接口的數據包,但只有在設備給定的情況下有意義),to_ms 代表函數超時返回的時間。本函數的代碼比較簡單,其執行步驟如下:

爲結構 pcap_t 分配空間並根據函數入參對其部分屬性進行初試化。
分別利用函數 live_open_new()  live_open_old() 嘗試創建 PF_PACKET 方式或 SOCK_PACKET 方式的 socket,注意函數名中一個爲"new",另一個爲"old"
根據 socket 的方式,設置捕獲句柄的讀緩衝區長度,並分配空間。
爲捕獲句柄 pcap_t 設置 linux 系統下的特定函數,其中最重要的是讀數據包函數和設置過濾器函數。(注意到這種從抽象模式到具體模式的設計思想在 linux 源代碼中也多次出現,如 VFS 文件系統)
handle->read_op = pcap_read_linux
 handle->setfilter_op = pcap_setfilter_linux
下面我們依次分析 2.2  2.0 內核版本下的 socket 創建函數。

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章