網絡監控
絕大多數的現代操做系統都提供了對底層網絡數據包捕獲的機制,在捕獲機制之上能夠創建網絡監控 (Network Monitoring)應用軟件。網絡監控也常簡稱爲sniffer,其最初的目的在於對網絡通訊狀況進行監控,以對網絡的一些異常狀況進行調試處理。但 隨着互連網的快速普及和網絡攻擊行爲的頻繁出現,保護網絡的運行安全也成爲監控軟件的另外一個重要目的。例如,網絡監控在路由器,防火牆、入侵檢查等方面使 用也很普遍。除此而外,它也是一種比較有效的黑客手段,例如,美國政府安所有門的"肉食動物"計劃。windows
包捕獲機制
從廣義的角度上看,一個包捕獲機制包含三個主要部分:最底層是針對特定操做系統的包捕獲機制,最高層是針對用戶程序的接口,第三部分是包過濾機制。數組
不 同的操做系統實現的底層包捕獲機制多是不同的,但從形式上看大同小異。數據包常規的傳輸路徑依次爲網卡、設備驅動層、數據鏈路層、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 和 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 中,最後從鏈表中提取第一個接口做爲捕獲設備。其中 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_DGRAM。SOCK_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_t。pcap_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 建立函數。
static int live_open_new(pcap_t *handle, const char *device, int promisc, int to_ms, char *ebuf) { /* 若是設備給定,則打開一個 RAW 類型的套接字,不然,打開 DGRAM 類型的套接字 */ sock_fd = device ? socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL)) : socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL)); /* 取得迴路設備接口的索引 */ handle->md.lo_ifindex = iface_get_id(sock_fd, "lo", ebuf); /* 若是設備給定,但接口類型未知或是某些必須工做在加工模式下的特定類型,則使用加工模式 */ if (device) { /* 取得接口的硬件類型 */ arptype = iface_get_arptype(sock_fd, device, ebuf); /* linux 使用 ARPHRD_xxx 標識接口的硬件類型,而 libpcap 使用DLT_xxx 來標識。本函數是對上述兩者的作映射變換,設置句柄的鏈路層類型爲 DLT_xxx,並設置句柄的偏移量爲合適的值,使其與鏈路層頭部之和爲 4 的倍數,目的是邊界對齊 */ map_arphrd_to_dlt(handle, arptype, 1); /* 若是接口是前面談到的不支持鏈路層頭部的類型,則退而求其次,使用 SOCK_DGRAM 模式 */ if (handle->linktype == xxx) { close(sock_fd); sock_fd = socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL)); } /* 得到給定的設備名的索引 */ device_id = iface_get_id(sock_fd, device, ebuf); /* 把套接字和給定的設備綁定,意味着只從給定的設備上捕獲數據包 */ iface_bind(sock_fd, device_id, ebuf); } else { /* 如今是加工模式 */ handle->md.cooked = 1; /* 數據包鏈路層頭部爲結構 sockaddr_ll, SLL 大概是結構名稱的簡寫形式 */ handle->linktype = DLT_LINUX_SLL; device_id = -1; } /* 設置給定設備爲混雜模式 */ if (device && promisc) { memset(&mr, 0, sizeof(mr)); mr.mr_ifindex = device_id; mr.mr_type = PACKET_MR_PROMISC; setsockopt(sock_fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP, &mr, sizeof(mr)); } /* 最後把建立的 socket 保存在句柄 pcap_t 中 */ handle->fd = sock_fd; } /* 2.0 內核下函數要簡單的多,由於只有惟一的一種 socket 方式 */ static int live_open_old(pcap_t *handle, const char *device, int promisc, int to_ms, char *ebuf) { /* 首先建立一個SOCK_PACKET類型的 socket */ handle->fd = socket(PF_INET, SOCK_PACKET, htons(ETH_P_ALL)); /* 2.0 內核下,不支持捕獲全部接口,設備必須給定 */ if (!device) { strncpy(ebuf, "pcap_open_live: The \"any\" device isn't supported on 2.0[.x]-kernel systems", PCAP_ERRBUF_SIZE); break; } /* 把 socket 和給定的設備綁定 */ iface_bind_old(handle->fd, device, ebuf); /*如下的處理和 2.2 版本下的類似,有所區別的是若是接口鏈路層類型未知,則 libpcap 直接退出 */ arptype = iface_get_arptype(handle->fd, device, ebuf); map_arphrd_to_dlt(handle, arptype, 0); if (handle->linktype == -1) { snprintf(ebuf, PCAP_ERRBUF_SIZE, "unknown arptype %d", arptype); break; } /* 設置給定設備爲混雜模式 */ if (promisc) { memset(&ifr, 0, sizeof(ifr)); strncpy(ifr.ifr_name, device, sizeof(ifr.ifr_name)); ioctl(handle->fd, SIOCGIFFLAGS, &ifr); ifr.ifr_flags |= IFF_PROMISC; ioctl(handle->fd, SIOCSIFFLAGS, &ifr); } }
比較上面兩個函數的代碼,還有兩個細節上的區別。首先是 socket 與接口綁定所使用的結構:老式的綁定使用告終構 sockaddr,而新式的則使用了 2.2 內核中定義的通用鏈路頭部層結構 sockaddr_ll。
iface_bind_old(int fd, const char *device, char *ebuf) { struct sockaddr saddr; memset(&saddr, 0, sizeof(saddr)); strncpy(saddr.sa_data, device, sizeof(saddr.sa_data)); bind(fd, &saddr, sizeof(saddr)); } iface_bind(int fd, int ifindex, char *ebuf) { struct sockaddr_ll sll; memset(&sll, 0, sizeof(sll)); sll.sll_family = AF_PACKET; sll.sll_ifindex = ifindex; sll.sll_protocol = htons(ETH_P_ALL); bind(fd, (struct sockaddr *) &sll, sizeof(sll); }
第二個是在 2.2 版本中設置設備爲混雜模式時,使用了函數 setsockopt(),以及新的標誌 PACKET_ADD_MEMBERSHIP 和結構 packet_mreq。我估計這種方式主要是但願提供一個統一的調用接口,以代替傳統的(混亂的)ioctl 調用。
struct packet_mreq { int mr_ifindex; /* 接口索引號 */ unsigned short mr_type; /* 要執行的操做(號) */ unsigned short mr_alen; /* 地址長度 */ unsigned char mr_address[8]; /* 物理層地址 */ };
用戶應用程序接口
Libpcap 提供的用戶程序接口比較簡單,經過反覆調用函數pcap_next()[pcap.c] 則可得到捕獲到的數據包。下面是一些使用到的數據結構:
/* 單個數據包結構,包含數據包元信息和數據信息 */ struct singleton [pcap.c] { struct pcap_pkthdr hdr; /* libpcap 自定義數據包頭部 */ const u_char * pkt; /* 指向捕獲到的網絡數據 */ }; /* 自定義頭部在把數據包保存到文件中也被使用 */ struct pcap_pkthdr { struct timeval ts; /* 捕獲時間戳 */ bpf_u_int32 caplen; /* 捕獲到數據包的長度 */ bpf_u_int32 len; /* 數據包的真正長度 */ } /* 函數 pcap_next() 其實是對函數 pcap_dispatch()[pcap.c] 的一個包裝 */ const u_char * pcap_next(pcap_t *p, struct pcap_pkthdr *h) { struct singleton s; s.hdr = h; /*入參"1"表明收到1個數據包就返回;回調函數 pcap_oneshot() 是對結構 singleton 的屬性賦值 */ if (pcap_dispatch(p, 1, pcap_oneshot, (u_char*)&s) <= 0) return (0); return (s.pkt); /* 返回數據包緩衝區的指針 */ }
pcap_dispatch() 簡單的調用捕獲句柄 pcap_t 中定義的特定操做系統的讀數據函數:return p->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 可能爲前面討論到的三種方式中的某一種,所以對數據緩衝區的結構有相應的處理,主要表如今加工模式下對僞鏈路層頭部的合成。具體代碼分析以下:
static int pcap_read_packet(pcap_t *handle, pcap_handler callback, u_char *userdata) { /* 數據包緩衝區指針 */ u_char * bp; /* bp 與捕獲句柄 pcap_t 中 handle->buffer 之間的偏移量,其目的是爲在加工模式捕獲狀況下,爲合成的僞數據鏈路層頭部留出空間 */ int offset; /* PACKET_SOCKET 方式下,recvfrom() 返回 scokaddr_ll 類型,而在SOCK_PACKET 方式下, 返回 sockaddr 類型 */ #ifdef HAVE_PF_PACKET_SOCKETS struct sockaddr_ll from; struct sll_header * hdrp; #else struct sockaddr from; #endif socklen_t fromlen; int packet_len, caplen; /* libpcap 自定義的頭部 */ struct pcap_pkthdr pcap_header; #ifdef HAVE_PF_PACKET_SOCKETS /* 若是是加工模式,則爲合成的鏈路層頭部留出空間 */ if (handle->md.cooked) offset = SLL_HDR_LEN; /* 其它兩中方式下,鏈路層頭部不作修改的被返回,不須要留空間 */ else offset = 0; #else offset = 0; #endif bp = handle->buffer + handle->offset; /* 從內核中接收一個數據包,注意函數入參中對 bp 的位置進行修正 */ packet_len = recvfrom( handle->fd, bp + offset, handle->bufsize - offset, MSG_TRUNC, (struct sockaddr *) &from, &fromlen); #ifdef HAVE_PF_PACKET_SOCKETS /* 若是是迴路設備,則只捕獲接收的數據包,而拒絕發送的數據包。顯然,咱們只能在 PF_PACKET 方式下這樣作,由於 SOCK_PACKET 方式下返回的鏈路層地址類型爲 sockaddr_pkt,缺乏了判斷數據包類型的信息。*/ if (!handle->md.sock_packet && from.sll_ifindex == handle->md.lo_ifindex && from.sll_pkttype == PACKET_OUTGOING) return 0; #endif #ifdef HAVE_PF_PACKET_SOCKETS /* 若是是加工模式,則合成僞鏈路層頭部 */ if (handle->md.cooked) { /* 首先修正捕包數據的長度,加上鍊路層頭部的長度 */ packet_len += SLL_HDR_LEN; hdrp = (struct sll_header *)bp; /* 如下的代碼分別對僞鏈路層頭部的數據賦值 */ hdrp->sll_pkttype = xxx; hdrp->sll_hatype = htons(from.sll_hatype); hdrp->sll_halen = htons(from.sll_halen); memcpy(hdrp->sll_addr, from.sll_addr, (from.sll_halen > SLL_ADDRLEN) ? SLL_ADDRLEN : from.sll_halen); hdrp->sll_protocol = from.sll_protocol; } #endif /* 修正捕獲的數據包的長度,根據前面的討論,SOCK_PACKET 方式下長度多是不許確的 */ caplen = packet_len; if (caplen > handle->snapshot) caplen = handle->snapshot; /* 若是沒有使用內核級的包過濾,則在用戶空間進行過濾*/ if (!handle->md.use_bpf && handle->fcode.bf_insns) { if (bpf_filter(handle->fcode.bf_insns, bp, packet_len, caplen) == 0) { /* 沒有經過過濾,數據包被丟棄 */ return 0; } } /* 填充 libpcap 自定義數據包頭部數據:捕獲時間,捕獲的長度,真實的長度 */ ioctl(handle->fd, SIOCGSTAMP, &pcap_header.ts); pcap_header.caplen = caplen; pcap_header.len = packet_len; /* 累加捕獲數據包數目,注意到在不一樣內核/捕獲方式狀況下數目可能不許確 */ handle->md.stat.ps_recv++; /* 調用用戶定義的回調函數 */ callback(userdata, &pcap_header, bp); }
數據包過濾機制
大 量的網絡監控程序目的不一樣,指望的數據包類型也不一樣,但絕大多數狀況都都只須要全部數據包的一(小)部分。例如:對郵件系統進行監控可能只須要端口號爲 25(smtp)和 110(pop3) 的 TCP 數據包,對 DNS 系統進行監控就只須要端口號爲 53 的 UDP 數據包。包過濾機制的引入就是爲了解決上述問題,用戶程序只需簡單的設置一系列過濾條件,最終便能得到知足條件的數據包。包過濾操做能夠在用戶空間執行, 也能夠在內核空間執行,但必須注意到數據包從內核空間拷貝到用戶空間的開銷很大,因此若是能在內核空間進行過濾,會極大的提升捕獲的效率。內核過濾的優點 在低速網絡下表現不明顯,但在高速網絡下是很是突出的。在理論研究和實際應用中,包捕獲和包過濾從語意上並無嚴格的區分,關鍵在於認識到捕獲數據包必然 有過濾操做。基本上能夠認爲,包過濾機制在包捕獲機制中佔中心地位。
包過濾機制其實是針對數據包的布爾值操做函數,若是函數最終返回 true,則經過過濾,反之則被丟棄。形式上包過濾由一個或多個謂詞判斷的並操做(AND)和或操做(OR)構成,每個謂詞判斷基本上對應了數據包的協 議類型或某個特定值,例如:只須要 TCP 類型且端口爲 110 的數據包或 ARP 類型的數據包。包過濾機制在具體的實現上與數據包的協議類型並沒有多少關係,它只是把數據包簡單的當作一個字節數組,而謂詞判斷會根據具體的協議映射到數組 特定位置的值。如判斷ARP類型數據包,只須要判斷數組中第 13、14 個字節(以太頭中的數據包類型)是否爲 0X0806。從理論研究的意思上看,包過濾機制是一個數學問題,或者說是一個算法問題,其中心任務是如何使用最少的判斷操做、最少的時間完成過濾處理, 提升過濾效率。
BPF
Libpcap 重點使用 BPF(BSD Packet Filter)包過濾機制,BPF 於 1992 年被設計出來,其設計目的主要是解決當時已存在的過濾機制效率低下的問題。BPF的工做步驟以下:當一個數據包到達網絡接口時,數據鏈路層的驅動會把它向 系統的協議棧傳送。但若是 BPF 監聽接口,驅動首先調用 BPF。BPF 首先進行過濾操做,而後把數據包存放在過濾器相關的緩衝區中,最後設備驅動再次得到控制。注意到BPF是先對數據包過濾再緩衝,避免了相似 sun 的 NIT 過濾機制先緩衝每一個數據包直到用戶讀數據時再過濾所形成的效率問題。參考資料D是關於 BPF 設計思想最重要的文獻。
BPF 的設計思想和當時的計算機硬件的發展有很大聯繫,相對老式的過濾方式CSPF(CMU/Stanford Packet Filter)它有兩大特色。1:基於寄存器的過濾機制,而不是早期內存堆棧過濾機制,2:直接使用獨立的、非共享的內存緩衝區。同時,BPF 在過濾算法是也有很大進步,它使用無環控制流圖(CFG control flow graph),而不是老式的布爾表達式樹(boolean expression tree)。布爾表達式樹理解上比較直觀,它的每個葉子節點便是一個謂詞判斷,而非葉子節點則爲 AND 操做或 OR操做。CSPF 有三個主要的缺點。1:過濾操做使用的棧在內存中被模擬,維護棧指針須要使用若干的加/減等操做,而內存操做是現代計算機架構的主要瓶頸。2:布爾表達式 樹形成了不須要的重複計算。3:不能分析數據包的變長頭部。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 有對此兩個工具的講解。
flex -Ppcap_ -t scanner.l > $$.scanner.c; mv $$.scanner.c scanner.c bison -y -p pcap_ -d grammar.y mv y.tab.c grammar.c mv y.tab.h tokdefs.h
編譯過濾字符串調用了函數 pcap_compile()[getcode.c],形式爲:
int pcap_compile(pcap_t *p, struct bpf_program *program, char *buf, int optimize, bpf_u_int32 mask)
其中 buf 指向用戶過濾字符串,編譯後的 BPF 代碼存在在結構 bpf_program中,標誌 optimize 指示是否對 BPF 代碼進行優化。
/* [pcap-bpf.h] */ struct bpf_program { u_int bf_len; /* BPF 代碼中謂詞判斷指令的數目 */ struct bpf_insn *bf_insns; /* 第一個謂詞判斷指令 */ }; /* 謂詞判斷指令結構,含意在前面已描述 [pcap-bpf.h] */ struct bpf_insn { u_short code; u_char jt; u_char jf; bpf_int32 k; };
過濾代碼的安裝
前 面咱們曾經提到,在內核空間過濾數據包對整個捕獲機制的效率是相當重要的。早期使用 SOCK_PACKET 方式的 Linux 不支持內核過濾,所以過濾操做只能在用戶空間執行(請參閱函數 pcap_read_packet() 代碼),在《UNIX 網絡編程(第一卷)》(參考資料 B)的第 26 章中對此有明確的描述。不過如今看起來狀況已經發生改變,linux 在 PF_PACKET 類型的 socket 上支持內核過濾。Linux 內核容許咱們把一個名爲 LPF(Linux Packet Filter) 的過濾器直接放到 PF_PACKET 類型 socket 的處理過程當中,過濾器在網卡接收中斷執行後當即執行。LSF 基於 BPF 機制,但二者在實現上有略微的不一樣。實際代碼以下:
/* 在包捕獲設備上附加 BPF 代碼 [pcap-linux.c]*/ static int pcap_setfilter_linux(pcap_t *handle, struct bpf_program *filter) { #ifdef SO_ATTACH_FILTER struct sock_fprog fcode; int can_filter_in_kernel; int err = 0; #endif /* 檢查句柄和過濾器結構的正確性 */ if (!handle) return -1; if (!filter) { strncpy(handle->errbuf, "setfilter: No filter specified", sizeof(handle->errbuf)); return -1; } /* 具體描述以下 */ if (install_bpf_program(handle, filter) < 0) return -1; /* 缺省狀況下在用戶空間運行過濾器,但若是在內核安裝成功,則值爲 1 */ handle->md.use_bpf = 0; /* 嘗試在內核安裝過濾器 */ #ifdef SO_ATTACH_FILTER #ifdef USHRT_MAX if (handle->fcode.bf_len > USHRT_MAX) { /*過濾器代碼太長,內核不支持 */ fprintf(stderr, "Warning: Filter too complex for kernel\n"); fcode.filter = NULL; can_filter_in_kernel = 0; } else #endif /* USHRT_MAX */ { /* linux 內核設置過濾器時使用的數據結構是 sock_fprog,/ /* 而不是 BPF 的結構 bpf_program ,所以應作結構之間的轉換 */ switch (fix_program(handle, &fcode)) { /* 嚴重錯誤,直接退出 */ case -1: default: return -1; /* 經過檢查,但不能工做在內核中 */ case 0: can_filter_in_kernel = 0; break; /* BPF 能夠在內核中工做 */ case 1: can_filter_in_kernel = 1; break; } } /* 若是能夠在內核中過濾,則安裝過濾器到內核中 */ if (can_filter_in_kernel) { if ((err = set_kernel_filter(handle, &fcode)) == 0) { /* 安裝成功 !!! */ handle->md.use_bpf = 1; } else if (err == -1) /* 出現非致命性錯誤 */ { if (errno != ENOPROTOOPT && errno != EOPNOTSUPP) { fprintf(stderr, "Warning: Kernel filter failed: %s\n",pcap_strerror(errno)); } } } /* 若是不能在內核中使用過濾器,則去掉曾經可能在此 socket 上安裝的內核過濾器。主要目的是爲了不存在的過濾器對數據包過濾的干擾 */ if (!handle->md.use_bpf) reset_kernel_filter(handle);[pcap-linux.c] #endif } /* 把 BPF 代碼拷貝到 pcap_t 數據結構的 fcode 上 */ int install_bpf_program(pcap_t *p, struct bpf_program *fp) { size_t prog_size; /* 首先釋放可能已存在的 BPF 代碼 */ pcap_freecode(&p->fcode); /* 計算過濾代碼的長度,分配內存空間 */ prog_size = sizeof(*fp->bf_insns) * fp->bf_len; p->fcode.bf_len = fp->bf_len; p->fcode.bf_insns = (struct bpf_insn *)malloc(prog_size); if (p->fcode.bf_insns == NULL) { snprintf(p->errbuf, sizeof(p->errbuf), "malloc: %s", pcap_strerror(errno)); return (-1); } /* 把過濾代碼保存在捕獲句柄中 */ memcpy(p->fcode.bf_insns, fp->bf_insns, prog_size); return (0); } /* 在內核中安裝過濾器 */ static int set_kernel_filter(pcap_t *handle, struct sock_fprog *fcode) { int total_filter_on = 0; int save_mode; int ret; int save_errno; /*在設置過濾器前,socket 的數據包接收隊列中可能已存在若干數據包。當設置過濾器後, 這些數據包極有可能不知足過濾條件,但它們不被過濾器丟棄。這意味着, 傳遞到用戶空間的頭幾個數據包不知足過濾條件。注意到在用戶空間過濾這不是問題, 由於用戶空間的過濾器是在包進入隊列後執行的。Libpcap 解決這個問題的方法是在設置過濾器以前,首先讀完接收隊列中全部的數據包。 具體步驟以下。*/ /*爲了不無限循環的狀況發生(反覆的讀數據包並丟棄,但新的數據包不停的到達),*/ /*首先設置一個過濾器,阻止全部的包進入 */ setsockopt(handle->fd, SOL_SOCKET, SO_ATTACH_FILTER, &total_fcode, sizeof(total_fcode); /* 保存 socket 當前的屬性 */ save_mode = fcntl(handle->fd, F_GETFL, 0); /* 設置 socket 它爲非阻塞模式 */ fcntl(handle->fd, F_SETFL, save_mode | O_NONBLOCK); /* 反覆讀隊列中的數據包,直到沒有數據包可讀。這意味着接收隊列已被清空 */ while (recv(handle->fd, &drain, sizeof drain, MSG_TRUNC) >= 0); /* 恢復曾保存的 socket 屬性 */ fcntl(handle->fd, F_SETFL, save_mode); /* 如今安裝新的過濾器 */ setsockopt(handle->fd, SOL_SOCKET, SO_ATTACH_FILTER, fcode, sizeof(*fcode)); } /* 釋放 socket 上可能有的內核過濾器 */ static int reset_kernel_filter(pcap_t *handle) { int dummy; return setsockopt(handle->fd, SOL_SOCKET, SO_DETACH_FILTER, &dummy, sizeof(dummy)); }
linux 在安裝和卸載過濾器時都使用了函數 setsockopt(),其中標誌SOL_SOCKET 表明了對 socket 進行設置,而 SO_ATTACH_FILTER 和 SO_DETACH_FILTER 則分別對應了安裝和卸載。下面是 linux 2.4.29 版本中的相關代碼:
[net/core/sock.c] #ifdef CONFIG_FILTER case SO_ATTACH_FILTER: …… /* 把過濾條件結構從用戶空間拷貝到內核空間 */ if (copy_from_user(&fprog, optval, sizeof(fprog))) break; /* 在 socket 上安裝過濾器 */ ret = sk_attach_filter(&fprog, sk); …… case SO_DETACH_FILTER: /* 使用自旋鎖鎖住 socket */ spin_lock_bh(&sk->lock.slock); filter = sk->filter; /* 若是在 socket 上有過濾器,則簡單設置爲空,並釋放過濾器內存 */ if (filter) { sk->filter = NULL; spin_unlock_bh(&sk->lock.slock); sk_filter_release(sk, filter); break; } spin_unlock_bh(&sk->lock.slock); ret = -ENONET; break; #endif
上面出現的 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)操做系統的網絡內核實現,對我的能力的提升有很大幫助