目前大多操作系統(tǒng)都為程序提供訪問數(shù)據(jù)鏈路層的功能,此功能可提供以下能力:
1.能監(jiān)視由數(shù)據(jù)鏈路層接收的分組,使得tcpdump之類的程序能運行,而無需專門的硬件設(shè)備來監(jiān)視分組。如果結(jié)合使用網(wǎng)絡(luò)接口進入混雜模式(promiscuous mode)的能力,那么應(yīng)用甚至能監(jiān)視本地電纜上流通的所有分組,而不僅僅是以程序運行所在主機為目的地的分組。
網(wǎng)絡(luò)接口進入混雜模式的能力在日益普及的交換式網(wǎng)絡(luò)(即使用交換機連接多個設(shè)備的計算機網(wǎng)絡(luò),在交換式網(wǎng)絡(luò)中,交換機充當(dāng)著網(wǎng)絡(luò)通信的中心,交換機的每個接口都直接與一個主機相連)中用處不大,因為交換機僅僅把傳給目的主機的單播、多播或廣播分組傳到目的主機的物理網(wǎng)絡(luò)接口上。為了監(jiān)視流經(jīng)其他交換機端口的分組,連接到我們主機的交換機的端口必須配置成接收其他分組的,這稱為監(jiān)視器模式(monitor mode)或端口鏡像(port mirroring)。有些你可能認(rèn)為沒有交換器的存儲轉(zhuǎn)發(fā)能力的設(shè)備實際也具有這種能力,如雙速率10/100 Mbit/s集線器也可近似看成一個雙端口的交換機,一個端口上連接100Mbit/s系統(tǒng),另一個端口上連接10Mbit/s系統(tǒng)。
2.能夠作為應(yīng)用進程而非內(nèi)核的一部分運行某些程序,如RARP服務(wù)器的大多數(shù)Unix版本是普通的應(yīng)用進程,它們從數(shù)據(jù)鏈路讀入RARP請求,又往數(shù)據(jù)鏈路寫出RARP應(yīng)答(RARP請求和應(yīng)答都不是IP數(shù)據(jù)報)。
Unix上訪問數(shù)據(jù)鏈路層的3個常用方法是BSD的分組過濾器BPF、SVR 4的數(shù)據(jù)鏈路提供者接口DLPI、Linux的SOCK_PACKET接口。我們先介紹這3個數(shù)據(jù)鏈路訪問接口,然后講解libpcap這個公開可得的分組捕獲函數(shù)庫,該函數(shù)庫在這3個系統(tǒng)上都能工作,因此使用此函數(shù)庫能使我們編寫?yīng)毩⒂诓僮飨到y(tǒng)提供的實際數(shù)據(jù)鏈路訪問接口的程序。
4.4 BSD及源自Berkeley的許多其他實現(xiàn)都支持BSD分組過濾器(BSD Packet Filter,BPF),BPF的實現(xiàn)在TCPv2中有講解。
發(fā)送一個分組之前或在接收一個分組之后會調(diào)用BPF:
TCPv2中給出了某個以太網(wǎng)接口驅(qū)動程序中調(diào)用BPF的例子。在分組接收后盡早調(diào)用BPF以及在發(fā)送分組前盡晚調(diào)用BPF的原因是為了提供精確的時間戳。
盡管往數(shù)據(jù)鏈路中安置一個用于捕獲所有分組的代碼并不困難,BPF強大在它的過濾能力,打開一個BPF設(shè)備的應(yīng)用進程可以裝載各自的過濾器,這個過濾器隨后由BPF應(yīng)用于每個分組,有些過濾器比較簡單(如只接收UDP或TCP分組),但更復(fù)雜的過濾器可以檢查分組首部某些字段是否為特定值,如以下過濾器:
tcp and port 80 and tcp[13:1] & 0x7 != 0
只收集去往或來自端口80的,設(shè)置了SYN、FIN、RST標(biāo)志的TCP分節(jié),其中表達式tcp[13:1]
指代從TCP首部開始位置起字節(jié)偏移量為13那個位置開始的1字節(jié)值。
BPF實現(xiàn)一個基于注冊的過濾機器,該過濾機器對每個收到的數(shù)據(jù)包應(yīng)用特定于應(yīng)用程序的過濾。雖然可以用這個偽機器的機器語言編寫過濾程序,但最簡單的接口是使用pcap_compile函數(shù)把類似上面的ASCII字符串編譯成BPF偽機器的機器語言。
BPF使用以下3個技術(shù)降低開銷:
1.BPF過濾在內(nèi)核中進行,以此把BPF到應(yīng)用進程的數(shù)據(jù)復(fù)制量減少到最小。如果不在內(nèi)核中過濾,需要從內(nèi)核空間到用戶空間的復(fù)制分組,這種復(fù)制開銷高昂,如果每個分組都這么復(fù)制,BPF可能跟不上快速的數(shù)據(jù)鏈路。
2.由BPF傳遞到應(yīng)用進程的只是每個分組的一段定長部分,這個長度稱為捕獲長度(capture length),也稱為快照長度(snapshot length,簡寫為snaplen)。大多應(yīng)用進程只需要分組首部而不需要分組數(shù)據(jù),這個技術(shù)減少了由BPF復(fù)制到應(yīng)用進程的數(shù)據(jù)量,例如,tcpdump默認(rèn)把該值設(shè)置為96字節(jié),能容納一個14字節(jié)的以太網(wǎng)首部、一個40字節(jié)的IPv6首部、一個20字節(jié)的TCP首部以及22字節(jié)的數(shù)據(jù),如果需要顯示來自其他協(xié)議(如DNS或NFS)的額外信息,用戶就得在運行tcpdump時增大該值。
3.BPF為每個應(yīng)用進程分別緩沖數(shù)據(jù),只有當(dāng)緩沖區(qū)已滿或讀超時時,該緩沖區(qū)中的數(shù)據(jù)才復(fù)制到應(yīng)用進程,該超時值可由應(yīng)用進程指定,例如tcpdump把它設(shè)置為1000ms,RARP守護進程把它設(shè)置為0(因為RARP分組極少,且RARP服務(wù)器需要一接收請求就發(fā)送應(yīng)答)。如此緩沖的目的在于減少系統(tǒng)調(diào)用的次數(shù)。盡管從BPF復(fù)制到應(yīng)用進程的仍然是相同數(shù)量的分組,但每次系統(tǒng)調(diào)用都有一定的開銷,因而減少系統(tǒng)調(diào)用次數(shù)就能降低開銷。
盡管我們在圖29-1中只顯示了一個緩沖區(qū),BPF其實為每個應(yīng)用進程維護兩個緩沖區(qū),在其中一個緩沖區(qū)中的數(shù)據(jù)被復(fù)制到應(yīng)用進程期間,另一個緩沖區(qū)被用于裝填數(shù)據(jù),這就是標(biāo)準(zhǔn)的雙緩沖技術(shù)。
我們在圖29-1中只顯示了BPF的分組接收,包括由數(shù)據(jù)鏈路從下方(網(wǎng)絡(luò))接收的分組和由數(shù)據(jù)鏈路從上方(IP)接收的分組。應(yīng)用進程也可以寫往BPF,使分組通過數(shù)據(jù)鏈路往外(向上或向下)發(fā)送出去,但大多數(shù)應(yīng)用進程僅僅讀BPF。沒有理由通過寫往BPF發(fā)送IP數(shù)據(jù)報,因為IP_HDRINCL套接字選項允許我們寫出任何期望的IP數(shù)據(jù)報(包括IP首部)。寫往BPF的唯一理由是為了自行發(fā)送不是IP數(shù)據(jù)報的網(wǎng)絡(luò)分組,如RARP守護進程就如此發(fā)送不是IP數(shù)據(jù)報的RARP應(yīng)答。
為了訪問BPF,我們必須打開一個當(dāng)前關(guān)閉著的BPF設(shè)備,例如,我們可以嘗試打開/dev/bpf0,如果返回EBUSY錯誤,就嘗試打開/etc/bpf1,一旦打開一個BPF設(shè)備,我們可以使用一些ioctl命令設(shè)置該設(shè)備的特征,包括裝載過濾器、設(shè)置讀超時、設(shè)置緩沖區(qū)大小、將一個數(shù)據(jù)鏈路(即網(wǎng)絡(luò)接口)連接到BPF設(shè)備、啟用混雜模式等,然后就使用read和write函數(shù)執(zhí)行IO。
SVR 4通過數(shù)據(jù)鏈路提供者接口(Datalink Provider Interface,DLPI)提供數(shù)據(jù)鏈路訪問,DLPI是一個由AT&T設(shè)計的獨立于協(xié)議的訪問數(shù)據(jù)鏈路層所提供服務(wù)的接口,其訪問通過發(fā)送和接收流消息(STREAMS message)實施。
DLPI有兩種打開風(fēng)格:一種是應(yīng)用進程先打開一個設(shè)備,然后通過DLPI的DL_ATTACH_REQ請求要使用的網(wǎng)絡(luò)接口;另一種是直接打開某個網(wǎng)絡(luò)接口設(shè)備(如le0)。為了提升效率,需要壓入2個流模塊(STREAMS module):在內(nèi)核中進行分組過濾的pfmod模塊和為應(yīng)用進程緩沖數(shù)據(jù)的bufmod模塊:
從概念上來說,這兩個模塊類似BPF開銷降低的技術(shù):pfmod在內(nèi)核中使用偽機器支持過濾;bufmod通過支持快照長度和讀取超時來減少數(shù)據(jù)量和系統(tǒng)調(diào)用次數(shù)。
然而,一個有趣的區(qū)別在于BPF和pfmod過濾器支持的偽機器類型。BPF過濾器是一個有向無環(huán)控制流圖,而pfmod則使用布爾表達式樹。前者自然地映射為寄存器型機器代碼,而后者自然地映射為堆棧型機器代碼[McCanne and Jacobson 1993]。該論文表明,BPF使用的CFG實現(xiàn)通常比布爾表達式樹快3到20倍,具體取決于過濾器的復(fù)雜性。
另外,BPF總是在復(fù)制分組前作出過濾決策,以避免復(fù)制過濾器將會丟棄的數(shù)據(jù)包。根據(jù)DLPI的實現(xiàn),數(shù)據(jù)包可能會被復(fù)制給pfmod,然后可能會被pfmod丟棄。
Linux先后有兩個從數(shù)據(jù)鏈路層接收分組的方法。較舊的方法是創(chuàng)建類型為SOCK_PACKET的套接字,此方法更普適但缺乏靈活性;較新的方法創(chuàng)建協(xié)議族為PF_PACKET的套接字,這個方法引入了更過的過濾和性能特性。我們需要有足夠的權(quán)限才能創(chuàng)建這兩種套接字,且調(diào)用socket的第三個參數(shù)必須是指定以太網(wǎng)幀的某個非0值。創(chuàng)建PF_PACKET套接字時,調(diào)用socket的第二個參數(shù)既可以是SOCK_DGRAM,表示扣除鏈路層首部的幀,也可以是SOCK_RAW,表示完整的鏈路層幀。SOCK_PACKET套接字只返回完整的鏈路層幀。以下方式可以從數(shù)據(jù)鏈路接收所有幀:
fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); // 較新方法
fd = socket(AF_INET, SOCK_PACKET, htons(ETH_P_ALL)); // 較舊方法
如果只想捕獲IPv4幀:
fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP)); // 較新方法
fd = socket(AF_INET, SOCK_PACKET, htons(ETH_P_IP)); // 較舊方法
用作socket調(diào)用的第三個參數(shù)的常值還有ETH_P_ARP、ETH_P_IPV6等,它們告知數(shù)據(jù)鏈路應(yīng)該把接收到的哪些類型的幀傳遞給所創(chuàng)建的套接字。如果數(shù)據(jù)鏈路支持混雜模式(如以太網(wǎng)),如果需要的話可以將設(shè)備改為混雜模式。對于PF_PACKET套接字,把一個網(wǎng)絡(luò)接口改為混雜模式可通過PACKET_ADD_MEMBERSHIP套接字選項完成,此時setsockopt函數(shù)的第四個參數(shù)的類型為packet_mreq,在此結(jié)構(gòu)中指定網(wǎng)絡(luò)接口以及PACKET_MR_PROMISC行為;對于SOCK_PACKET套接字,改為混雜模式需要使用SIOCGIFFLAGS標(biāo)志調(diào)用ioctl以獲取標(biāo)志,然后將IFF_PROMISC加入獲取到的標(biāo)志,再以SIOCSIFFLAGS調(diào)用ioctl存儲標(biāo)志,不幸的是,若采用此方法,多個程序同時設(shè)置混雜模式時可能會互相干擾,且有缺陷的程序可能在退出后還保持著混雜模式。
Lunix的數(shù)據(jù)鏈路訪問方法與BPF和DLPI存在如下差別:
1.Linux方法不提供內(nèi)核緩沖,且只有較新的方法才提供內(nèi)核過濾(需要用SO_ATTACK_FILTER套接字選項安裝),盡管這些套接字有普通的套接字接收緩沖區(qū),但多個幀不能緩沖在一起由單個讀操作一次性地傳遞給應(yīng)用進程。這樣會增加從內(nèi)核到應(yīng)用進程復(fù)制的數(shù)據(jù)的開銷。
2.Linux較舊的方法不提供針對設(shè)備的過濾,較新的方法可通過調(diào)用bind與某個設(shè)備關(guān)聯(lián)。如果調(diào)用socket時指定了ETH_P_IP,那么來自任何設(shè)備(如以太網(wǎng)、PPP鏈路、環(huán)回設(shè)備)的所有IPv4分組都被傳遞到所創(chuàng)建的套接字。recvfrom函數(shù)將返回一個通用套接字地址結(jié)構(gòu),其中的sa_data成員含有設(shè)備名字(如eth0),應(yīng)用進程必須自行丟棄來自不關(guān)注的設(shè)備的數(shù)據(jù)。這里仍然會有太多數(shù)據(jù)返回到應(yīng)用進程,從而妨礙對于高速網(wǎng)絡(luò)的監(jiān)視。
libpcap是訪問操作系統(tǒng)所提供的分組捕獲機制的分組捕獲函數(shù)庫,它是與實現(xiàn)無關(guān)的。目前它只支持分組的讀入(當(dāng)然只需往該函數(shù)庫中增加一些代碼行就可以讓調(diào)用者往數(shù)據(jù)鏈路寫出分組)。libnet函數(shù)庫不僅支持往數(shù)據(jù)鏈路寫分組,還能構(gòu)造任意協(xié)議的分組。
libpcap目前支持源自Berkeley內(nèi)核中的BPF、Solaris 2.x和HP-UX中的DLPI、SunOS 4.1.x中的NIT(網(wǎng)絡(luò)接口層,Network Interface Layer)、Linux的SOCK_PACKET套接字和PF_PACKET套接字,以及若干其他操作系統(tǒng)。tcpdump就使用該函數(shù)庫。libpcap由大約25個函數(shù)組成,我們稍后給出使用其中常用函數(shù)的一個例子,所有庫函數(shù)均以pcap_前綴打頭。
libpcap函數(shù)庫可從http://www.tcpdump.org
公開獲取。
libnet函數(shù)庫可構(gòu)造任意協(xié)議的分組并將其輸出到網(wǎng)絡(luò)中的接口,它以與實現(xiàn)無關(guān)的方式提供原始套接字訪問方式和數(shù)據(jù)鏈路訪問方式。
libnet隱藏了構(gòu)造IP、UDP、TCP首部的許多細節(jié),并提供簡單且便于移植的數(shù)據(jù)鏈路和原始套接字寫出訪問接口。稍后給出一些libnet庫函數(shù)的使用例子。libnet的所有庫函數(shù)均以libnet_前綴打頭。
現(xiàn)開發(fā)一個程序,它向一個名字服務(wù)器發(fā)送含有某個DNS查詢的UDP數(shù)據(jù)報,然后使用分組捕獲函數(shù)庫讀入應(yīng)答,確定這個名字服務(wù)器是否計算UDP校驗和。對于IPv4,UDP校驗和的計算是可選的,如今大多系統(tǒng)默認(rèn)開啟校驗和,但較老系統(tǒng)(如SunOS 4.1.x)默認(rèn)禁止校驗和。當(dāng)今所有系統(tǒng)(特別是運行名字服務(wù)器的系統(tǒng))都總是應(yīng)該開啟UDP校驗和,否則DNS服務(wù)器收到的受損數(shù)據(jù)報可能破壞DNS服務(wù)器的數(shù)據(jù)庫,存入錯誤的信息。
開啟和禁止UDP校驗和通常是基于系統(tǒng)范圍設(shè)置的。
我們將自行構(gòu)造UDP數(shù)據(jù)報(DNS查詢),并把它寫到一個原始套接字,這個查詢使用普通的UDP套接字就可發(fā)送,但我們想展示如何使用IP_HDRINCL套接字選項構(gòu)造一個完整的IP數(shù)據(jù)報。
并且,我們無法在從普通UDP套接字讀入時獲取UDP校驗和,UDP或TCP分組也不會傳到原始套接字,因此我們必須使用分組捕獲機制獲取含有名字服務(wù)器的應(yīng)答的完整UDP數(shù)據(jù)報。
我們會檢查所獲取UDP首部中的校驗和字段,如果其值為0,那么該名字服務(wù)器沒有開啟UDP校驗和。
我們把自行構(gòu)造的UDP數(shù)據(jù)報寫出到原始套接字,然后使用libpcap讀回其應(yīng)答。UDP模塊也接收到這個來自名字服務(wù)器的應(yīng)答,并將響應(yīng)以一個ICMP端口不可達錯誤,因為UDP模塊根本不知道我們自行構(gòu)造的UDP數(shù)據(jù)報選用的端口號。名字服務(wù)器將忽略這個ICMP錯誤。使用TCP編寫一個這樣的測試程序比較困難,盡管我們可以很容易地把構(gòu)造的TCP分節(jié)寫出到網(wǎng)絡(luò),但對于此分節(jié)的應(yīng)答我們的TCP模塊響應(yīng)以一個RST,結(jié)果是連三路握手都完成不了。
繞過這個難題的方法之一是以發(fā)送主機所在子網(wǎng)上某個未使用的IP地址為源地址發(fā)送TCP分節(jié),且事先在發(fā)送主機上為這個未使用IP地址增加一個ARP表項,使得發(fā)送主機能回答這個未使用地址的ARP請求,但不把這個未使用IP地址作為別名地址配置在發(fā)送主機上,這將導(dǎo)致發(fā)送主機上的IP協(xié)議棧丟棄所接收的目的地址為未使用地址的分組,前提是發(fā)送主機不用作路由器。
以下是構(gòu)成udpcksum程序的函數(shù):
以下是udpcksum.h頭文件:
#include "unp.h"
#include <pcap.h>
#include <netinet/in_systm.h> /* required for ip.h */
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_var.h>
#include <netinet/udp.h>
#include <netinet/udp_var.h>
#include <net/if.h>
#include <netinet/if_ether.h>
#define TTL_OUT 64 /* outgoing TTL */
/* declare global variables */
extern struct sockaddr *dest, *local;
extern socklen_t destlen, locallen;
extern int datalink;
extern char *device;
extern pcap_t *pd;
extern int rawfd;
extern int snaplen;
extern int verbose;
extern int zerosum;
/* function prototypes */
void cleanup(int);
char *next_pcap(int *);
void open_output(void);
void open_pcap(void);
void send_dns_query(void);
void test_udp(void);
void udp_write(char *, int);
struct udpiphdr *udp_read(void);
以下是udpcksum的main函數(shù):
#include "udpcksum.h"
/* define global variables */
struct sockaddr *desc, *local;
struct sockaddr_in locallookup;
socklen_t destlen, locallen;
int datalink; /* from pcap_datalink(), in <net/bpf.h> */
char *device; /* pcap device */
pcap_t *pd; /* packet capture struct pointer */
int rawfd; /* raw socket to write on */
int snaplen = 200; /* amount of data to capture */
int verbose;
int zerosum; /* send UDP query with no checksum */
static void usage(const char *);
int main(int argc, char *argv[]) {
int c, lopt = 0;
char *ptr, localname[1024], *localport;
struct addrinfo *aip;
opterr = 0; /* don't want getopt() writing to stderr */
// getopt函數(shù)可以接受數(shù)字選項,如此處的0
while ((c = getopt(argc, argv, "0i:l:v")) != -1) {
switch (c) {
// -0選項要求不設(shè)置UDP校驗和就發(fā)送UDP查詢,以便查看服務(wù)器對它的處理是否不同于設(shè)置了校驗和的數(shù)據(jù)報
case '0':
zerosum = 1;
break;
// -i選項用于指定接收服務(wù)器的應(yīng)答的接口,如果接口未指定,分組捕獲函數(shù)庫將會選擇一個
// 但函數(shù)庫選定的接口在多宿主機上可能不是即將接收DNS應(yīng)答的接口
// 從分組捕獲設(shè)備讀入與從普通套接字讀入的差別之一就體現(xiàn)在此:
// 使用套接字我們可以使用通配地址,從而接收到達任意接口的分組
// 但使用分組捕獲設(shè)備就只能在單個接口上接收到達的分組
// Linux的SOCK_PACKET方法沒有把它的數(shù)據(jù)鏈路捕獲限定在單個設(shè)備
// 盡管如此,libpcap卻基于其默認(rèn)設(shè)置或我們的-i選項提供限定接口形式的過濾
case 'i':
device = optarg; /* pcap device */
break;
// -l選項用于指定源IP地址和源端口號,本選項的參數(shù)中,端口號(或服務(wù)名)是最后一個點號之后的部分
// 源IP地址是組后一個點號之前的部分
case 'l': /* local IP address and port #: a.b.c.d.p */
if ((ptr = strrchr(optarg, '.')) == NULL) {
usage("invalid -l option");
}
*ptr++ = 0; /* null replaces final period */
localport = ptr; /* service name or port number */
strncpy(localname, optarg, sizeof(localname));
lopt = 1;
break;
case 'v':
verbose = 1;
break;
case '?':
usage("unrecognized option");
}
}
// 剩余命令行參數(shù)必須恰好是兩個:運行DNS服務(wù)器的目的主機名(或目的IP)和服務(wù)名(或端口號)
if (optind != argc - 2) {
usage("missing <host> and/or <serv>");
}
/* convert destination name and service */
// 調(diào)用我們的host_serv將目的主機名(或目的IP)和服務(wù)名(或端口號)轉(zhuǎn)換成套接字地址結(jié)構(gòu)
aip = Host_serv(argv[optind], argv[optind + 1], AF_INET, SOCK_DGRAM);
dest = aip->ai_addr; /* don't freeaddrinfo() */
destlen = aip->ai_addrlen;
/*
* Need local IP address for source IP address for UDP datagrams.
* Can't specify 0 adn let IP choose, as we need to know it for
* the pseudoheader to calculate the UDP checksum.
* If -l option supplied, then use those valuse; otherwise,
* connect a UDP socket to the destination to determine the
* right source address.
*/
// 我們自定構(gòu)造UDP首部,因此我們在寫出該UDP數(shù)據(jù)報前必須知道源IP地址,我們不能讓IP模塊為其選擇值
// 因為源IP地址是UDP偽首部的一部分,計算UDP校驗和時會使用偽首部
// 如果有-l選項,將本地地址和端口轉(zhuǎn)換為套接字地址結(jié)構(gòu)
if (lopt) {
/* convert local name and service */
aip = Host_serv(localname, localport, AF_INET, SOCK_DGRAM);
local = aip->ai_addr; /* don't freeaddrinfo() */
locallen = aip->ai_addrlen;
// 否則通過把一個UDP套接字連接到目的地確定內(nèi)核選定的本地IP地址和臨時端口號
} else {
int s;
s = Socket(AF_INET, SOCK_DGRAM, 0);
Connect(s, dest, destlen);
/* kernel chooses correct local address for dest */
locallen = sizeof(locallookup);
local = (struct sockaddr *)&locallookup;
Getsockname(s, local, &locallen);
if (locallookup.sin_addr.s_addr == htonl(INADDR_ANY)) {
err_quit("Can't determine local address - use -l\n");
}
close(s);
}
// 調(diào)用open_output創(chuàng)建一個原始套接字并開啟IP_HDRINCL套接字選項
// 我們于是可以往這個套接字寫出包括IP首部在內(nèi)的完整IP數(shù)據(jù)報
// open_output函數(shù)還有一個使用libnet實現(xiàn)的版本
open_output(); /* open output, either raw socket or libnet */
// 調(diào)用open_pcap打開分組捕獲設(shè)備
open_pcap(); /* open packet capture device */
// 創(chuàng)建原始套接字和打開分組捕獲設(shè)備都需要超級用戶特權(quán),但具體取決于實現(xiàn)
// 如對于BPF,管理員可設(shè)置/dev/bpf設(shè)備的訪問權(quán)限
// 既然已經(jīng)完成特權(quán)操作,我們此處放棄這個特權(quán),假定這個特權(quán)是通過設(shè)置用戶id而獲取的
// 具有超級用戶特權(quán)的進程調(diào)用setuid把它的實際用戶ID、有效用戶ID、保存的設(shè)置用戶ID都設(shè)為當(dāng)前的實際用戶ID
setuid(getuid()); /* don't need superuser privileges anymore */
// 防止用戶在程序運行完前強行終止它
Signal(SIGTERM, cleanup);
Signal(SIGINT, cleanup);
Signal(SIGHUP, cleanup);
// test_udp函數(shù)發(fā)送一個DNS查詢,并讀入服務(wù)器的應(yīng)答
test_udp();
// cleanup函數(shù)顯示來自分組捕獲函數(shù)庫的統(tǒng)計結(jié)果后終止進程
cleanup(0);
}
open_pcap函數(shù)由main函數(shù)調(diào)用以打開分組捕獲設(shè)備:
#include "udpcksum.h"
#define CMD "udp and src host %s and src port %d"
void open_pcap(void) {
uint32_t localnet, netmask;
char cmd[MAXLINE], errbuf[PCAP_ERRBUF_SIZE], str1[INET_ADDRSTRLEN], str2[INET_ADDRSTRLEN];
struct bpf_program fcode;
// 如果沒有指定分組捕獲設(shè)備(通過-i命令行選項),就調(diào)用pcap_lookupdev選擇一個設(shè)備
// pcap_lookupdev函數(shù)以SIOCGIFCONF為參數(shù)調(diào)用ioctl,找到索引號最小的UP狀態(tài)的接口設(shè)備(除環(huán)回接口外)
if (device == NULL) {
// 許多pcap庫函數(shù)在出錯時填寫一個出錯消息串
// 傳給pcap_lookupdev函數(shù)的唯一參數(shù)就是一個用于填寫出錯消息的字符數(shù)組
if ((device = pcap_lookupdev(errbuf)) == NULL) {
err_quit("pcap_lookup: %s", errbuf);
}
}
printf("device = %s\n", device);
/* hardcode: promisc=0, to_ms=500 */
// 調(diào)用pcap_open_live打開這個設(shè)備,函數(shù)名中的live表明所打開的是一個真實設(shè)備
// 而不是一個含有先前保存的分組的文件
// device參數(shù)是設(shè)備名,snaplen參數(shù)是每個分組保存的字節(jié)數(shù),第三個參數(shù)為是否設(shè)置混雜模式
// 第四個參數(shù)為以毫秒為單位的超時值,第五個參數(shù)是指向用于返回出錯字符串的字符數(shù)組指針
// 如果設(shè)置了混雜模式,網(wǎng)絡(luò)接口就被投入混雜模式,導(dǎo)致它接收電纜上流經(jīng)的所有分組
// 對于tcpdump混雜模式是通常的模式,但對于我們的例子,來自DNS服務(wù)器的應(yīng)答會被發(fā)送到本主機,因此無需設(shè)置混雜模式
// 超時參數(shù)指讀超時,如果每收到一個分組就讓設(shè)備把該分組返送到應(yīng)用進程,會引起從內(nèi)核到應(yīng)用進程的大量個體分組復(fù)制
// 因此效率比較低,libpcap僅當(dāng)設(shè)備的讀緩沖區(qū)被填滿或讀超時發(fā)生時才返送分組
// 如果超時值被設(shè)為0,則每個分組一經(jīng)接收就被返送
if ((pd = pcap_open_live(device, snaplen, 0, 500, errbuf)) == NULL) {
err_quit("pcap_open_live: %s", errbuf);
}
// pcap_lookupnet函數(shù)返回分組捕獲設(shè)備的網(wǎng)絡(luò)地址和子網(wǎng)掩碼
// 我們接下來調(diào)用pcap_compile時必須指定這個子網(wǎng)掩碼
// 因為分組過濾器需要用子網(wǎng)掩碼判斷一個IP地址是否為一個子網(wǎng)定向廣播地址
if (pcap_lookupnet(device, &localnet, &netmask, errbuf) < 0) {
err_quit("pcap_lookupnet: %s", errbuf);
}
if (verbose) {
printf("localnet = %s, netmask = %s\n", Inet_ntop(AF_INET, &localnet, str1, sizeof(str1)),
Inet_ntop(AF_INET, &netmask, str2, sizeof(str2)));
}
snprintf(cmd, sizeof(cmd), CMD, Sock_ntop_host(dest, destlen),
ntohs(sock_get_port(dest, destlen)));
if (verbose) {
printf("cmd = %s\n", cmd);
}
// pcap_compile函數(shù)把我們在cmd字符數(shù)組中構(gòu)造的過濾器字符串編譯成一個過濾器程序
// 將其存放在fcode中,這個過濾器將選擇我們希望接收的分組
if (pcap_compile(pd, &fcode, cmd, 0, netmask) < 0) {
err_quit("pcap_compile: %s", pcap_geterr(pd));
}
// pcap_setfilter函數(shù)把我們剛編譯出來的過濾器程序裝載到分組捕獲設(shè)備
if (pcap_setfilter(pd, &fcode) < 0) {
err_quit("pcap_setfilter: %s", pcap_geterr(pd));
}
// pcap_datalink函數(shù)返回分組捕獲設(shè)備的數(shù)據(jù)鏈路類型,接收分組時我們根據(jù)該值確定數(shù)據(jù)鏈路首部大小
if ((datalink = pcap_datalink(pd)) < 0) {
err_quit("pcap_datalink: %s", pcap_geterr(pd));
}
if (verbose) {
printf("datalink = %d\n", datalink);
}
}
test_udp函數(shù)發(fā)送一個DNS查詢,并讀入服務(wù)器的應(yīng)答:
void test_udp(void) {
// 我們希望這兩個自動變量從信號處理函數(shù)siglongjmp到本函數(shù)前后值保持不變
// 加上volatile限定詞可以防止編譯器優(yōu)化導(dǎo)致跳回后nsent當(dāng)做初始值0使用(因為從定義到使用看起來沒有修改過它的值)
volatile int nsent = 0, timeout = 3;
struct udpiphdr *ui;
Signal(SIGALRM, sig_alrm);
// 首次調(diào)用sigsetjmp時,它返回0,從siglongjmp函數(shù)跳回時,它返回1
// sigsetjmp函數(shù)的第二個參數(shù)非0時,會將當(dāng)前的信號屏蔽字保存在jmpbuf參數(shù)中
// 從而從siglongjmp函數(shù)跳回時恢復(fù)信號屏蔽字
// 進入信號處理函數(shù)時,會將該信號信號加入屏蔽字,從而跳回來時恢復(fù)信號屏蔽字
if (sigsetjmp(jmpbuf, 1)) {
// 進入此處說明是從SIGALRM信號處理函數(shù)中調(diào)用siglongjmp跳轉(zhuǎn)回來的
// 即我們發(fā)送了一個請求,但沒有收到應(yīng)答,從而超時導(dǎo)致進入SIGALRM信號處理函數(shù),然后跳轉(zhuǎn)回來
// 如果3次請求都超時,則終止進程
if (nsent >= 3) {
err_quit("no response");
}
// 否則顯示一條消息并倍增超時值(通過指數(shù)回退增加)
printf("timeout\n");
// timeout的初始值為3,表示首次超時值為3秒,然后依次是6秒、12秒
timeout *= 2; /* exponential backoff: 3, 6, 12 */
}
// 我們像這樣使用sigsetjmp和siglongjmp函數(shù),而非簡單地判斷讀函數(shù)是否錯誤返回EINTR
// 是因為分組捕獲函數(shù)庫的讀函數(shù)(由我們的udp_read函數(shù)調(diào)用)在read函數(shù)返回EINTR時重啟讀操作
// 而我們不想為了返回EINTR錯誤而修改庫函數(shù),唯一的解決方法是捕獲SIGALRM信號并執(zhí)行一個非本地的長跳轉(zhuǎn)
// 從而讓控制流返回到本函數(shù),而非庫函數(shù)中
// 信號處理函數(shù)建立后和sigsetjmp首次調(diào)用前,SIGALRM信號也有可能被遞交,因此此時再打開該標(biāo)志
// 即使程序本身不會導(dǎo)致產(chǎn)生SIGALRM信號,它也可能通過其他方式產(chǎn)生,如使用kill命令
canjump = 1; /* siglongjmp is now OK */
// send_dns_query函數(shù)向DNS服務(wù)器發(fā)送一個DNS查詢
send_dns_query();
++nsent;
// udp_read函數(shù)用于讀入DNS服務(wù)器的應(yīng)答,讀應(yīng)答前先調(diào)用alarm防止讀操作永遠阻塞
// 超時時,內(nèi)核將產(chǎn)生SIGALRM信號,而我們的信號處理函數(shù)會調(diào)用siglongjmp
alarm(timeout);
ui = udp_read();
canjump = 0;
alarm(0);
if (ui->ui_sum == 0) {
printf("UDP checksums off\n");
} else {
printf("UDP checksums on\n");
}
if (verbose) {
printf("received UDP checksum = %x\n", ntohs(ui->ui_sum));
}
}
以下是我們的SIGALRM的信號處理函數(shù)sig_alrm,以下內(nèi)容與test_udp函數(shù)放在同一文件:
#include "udpcksum.h"
#include <setjmp.h>
static sigjmp_buf jmpbuf;
static int canjump;
void sig_alrm(int signo) {
// canjmp是test_udp函數(shù)中初始化跳轉(zhuǎn)緩沖區(qū)后設(shè)置的,并在讀入應(yīng)答后清除
if (canjmp == 0) {
return;
}
siglongjmp(jmpbuf, 1);
}
以下send_dns_query函數(shù)構(gòu)造一個DNS查詢,并通過原始套接字把該UDP數(shù)據(jù)報發(fā)送給名字服務(wù)器:
void send_dns_query(void) {
size_t nbytes;
char *buf, *ptr;
// 分配緩沖區(qū),它足以存放20字節(jié)IP首部、8字節(jié)UDP首部、100字節(jié)用戶數(shù)據(jù)
buf = Malloc(sizeof(struct udpiphdr) + 100);
// ptr指向用戶數(shù)據(jù)的第一個字節(jié)
ptr = buf + sizeof(struct udpiphdr); /* leave room for IP/UDP headers */
// DNS標(biāo)識字段設(shè)為1234
*((uint16_t *)ptr) = htons(1234); /* identification */
ptr += 2;
// DNS標(biāo)志字段
*((uint16_t *)ptr) = htons(0x0100); /* flags: recursion desired */
ptr += 2;
// DNS問題數(shù)字段為1,表示DNS查詢中包含1個問題
*((uint16_t *)ptr) = htons(1); /* # questions */
ptr += 2;
// 把回答的RR數(shù)、權(quán)威RR數(shù)、額外RR數(shù)都設(shè)為0
*((uint16_t *)ptr) = 0; /* # answer RRs */
ptr += 2;
*((uint16_t *)ptr) = 0; /* # authority RRs */
ptr += 2;
*((uint16_t *)ptr) = 0; /* # additional RRs */
ptr += 2;
// 查詢a.root-servers.net的IP地址
// \001是1個8進制字節(jié),表示此標(biāo)簽長度為1個字節(jié),其他8進制字節(jié)同理
memcpy(ptr, "\001a\012root-servers\003net\000", 20);
ptr += 20;
// DNS查詢類型為A查詢
*((uint16_t *)ptr) = htons(1); /* query type = A */
ptr += 2;
*((uint16_t *)ptr) = htons(1); /* query class = 1 (IP addr) */
ptr += 2;
// 這個消息由36字節(jié)的用戶數(shù)據(jù)構(gòu)成(8個2字節(jié)字段和1個20字節(jié)域名)
nbytes = (ptr - buf) - sizeof(struct udpiphdr);
// 調(diào)用我們的udp_write構(gòu)造UDP和IP首部,并把構(gòu)造完的IP數(shù)據(jù)報寫到原始套接字
udp_write(buf, nbytes);
if (verbose) {
printf("sent: %s bytes of data\n", nbytes);
}
}
以下是open_output函數(shù):
// 存放原始套接字描述符的全局變量
int rawfd; /* raw socket to write on */
void open_output(void) {
int on = 1;
/*
* Need a raw socket to write our own IP datagrams to.
* Process must have superuser privileges to create this socket.
* Also must set IP_HDRINCL so we can write our own IP headers.
*/
rawfd = Socket(dest->sa_family, SOCK_RAW, 0);
// 開啟IP_HDRINCL套接字選項,該選項允許我們往套接字寫出包括IP首部在內(nèi)的完整IP數(shù)據(jù)報
Setsockopt(rawfd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on));
}
以下udp_write函數(shù)構(gòu)造IP和UDP首部,并把結(jié)果數(shù)據(jù)報寫出到原始套接字,以下內(nèi)容與open_output放在同一文件中:
void udp_write(char *buf, int userlen) {
struct udpiphdr *ui;
struct ip *ip;
/* fill in and checksum UDP header */
// ip指向IP首部的開始位置,ui也指向相同位置,但udpiphdr結(jié)構(gòu)是IP和UDP首部的組合
ip = (struct ip *)buf;
ui = (struct udpiphdr *)buf;
// 顯式清0首部區(qū)域,以免可能留在緩沖區(qū)中的剩余數(shù)據(jù)影響校驗和的計算
// 此處的早先版本顯式清零udpiphdr結(jié)構(gòu)中的每個成員,但該結(jié)構(gòu)有一些實現(xiàn)相關(guān)的細節(jié),不同系統(tǒng)之間會有差異
// 在顯式構(gòu)造首部時,這是一個典型的移植性問題
bzero(ui, sizeof(*ui));
// ui_len是UDP首部字節(jié)數(shù)(8字節(jié))加上UDP用戶數(shù)據(jù)字節(jié)數(shù),此值就是UDP首部中的長度字段值
ui->ui_len = htons((uint16_t)(sizeof(struct udphdr) + userlen));
/* then add 28 for IP datagram length */
// userlen是整個IP數(shù)據(jù)報的長度,包括IP首部
// 其值為UDP首部之后的UDP用戶數(shù)據(jù)字節(jié)數(shù)加上28字節(jié)(20字節(jié)IP首部+8字節(jié)UDP首部)
userlen += sizeof(struct udpiphdr);
// UDP校驗和計算不僅涵蓋UDP首部和UDP數(shù)據(jù),還涉及來自IP首部的若干字段,這些來自IP首部的字段構(gòu)成偽首部
// 校驗和計算涵蓋偽首部能提供如下額外驗證:如果校驗和正確,則數(shù)據(jù)報確實已遞送到正確的主機和正確的協(xié)議處理代碼
// 從此處開始到ui_ulen的賦值為值,都是構(gòu)成偽首部的字段
ui->ui_pr = IPPROTO_UDP;
ui->ui_src.s_addr = ((struct sockaddr_in *)local)->sin_addr.s_addr;
ui->ui_dst.s_addr = ((struct sockaddr_in *)dest)->sin_addr.s_addr;
ui->ui_sport = ((struct sockaddr_in *)local)->sin_port;
ui->ui_dport = ((struct sockaddr_in *)dest)->sin_port;
ui->ui_ulen = ui->ui_len;
// 如果計算校驗和(即沒有設(shè)置-0命令行參數(shù))
if (zerosum == 0) {
#if 1 /* change to if 0 for Solaris 2.x, x < 6 */
// 如果計算出的校驗和為0,就改為存入0xffff,在一的補數(shù)(one's complement)中這兩個值是同義的
// UDP通過設(shè)置校驗和為0值指示發(fā)送者沒有存放UDP校驗和
// 在第二十八章中,我們沒有檢查計算出的校驗和是否為0,因為ICMPv4校驗和是必需的,其值為0不指示沒有校驗和
if ((ui->ui_sum = in_cksum((u_int16_t *)ui, userlen)) == 0) {
ui->ui_sum = 0xffff;
}
// Solaris 2.x(x<6)對于通過設(shè)置了IP_HDRINCL套接字選項的原始套接字發(fā)送的TCP分節(jié)或UDP數(shù)據(jù)報而言
// 在校驗和字段上有一個缺陷,這些校驗和由內(nèi)核計算,但進程必須把ui_sum成員設(shè)置為TCP或UDP的長度
#else
ui->ui_sum = ui->ui_len;
#endif
}
/* fill in rest of IP header */
/* ip_output() calculates & stores IP header checksum */
// 既然開啟了IP_HDRINCL套接字選項,我們就要手動填寫IP首部中的大多數(shù)字段
ip->ip_v = IPVERSION;
ip->ip_hl = sizeof(struct ip) >> 2;
ip->ip_tos = 0;
// ip_len成員需要根據(jù)所用系統(tǒng)決定按主機字節(jié)序設(shè)置還是網(wǎng)絡(luò)字節(jié)序設(shè)置,這是使用原始套接字時的一個移植性問題
#if defined(linux) || defined(__OpenBSD__)
ip->ip_len = htons(userlen); /* network byte order */
#else
ip->ip_len = userlen; /* host byte order */
#endif
// 把IP首部的標(biāo)識字段設(shè)為0,以告知IP模塊去設(shè)置這個字段,主機每發(fā)送一份IP數(shù)據(jù)報,標(biāo)識字段的值就會加1
// 如果IP數(shù)據(jù)報需要進行分片發(fā)送,則每個分片的IP首部標(biāo)識字段都是一致的
// IP模塊還會計算IP首部校驗和
ip->ip_id = 0; /* let IP set this */
/* frag offset, MF and DF flags */
// MF是More Fragments的簡稱,值為1代表后面還有分片的數(shù)據(jù)報,值為0代表當(dāng)前數(shù)據(jù)報已是最后一個分片
// DF是Don't Fragment的簡稱,表示不能對IP數(shù)據(jù)報進行分片
ip->ip_off = 0;
ip->ip_ttl = TTL_OUT;
Sendto(rawfd, buf, userlen, 0, dest, destlen);
}
以下是udp_read函數(shù),它從分組捕獲設(shè)備讀入下一個分組:
struct udpiphdr *udp_read(void) {
int len;
char *ptr;
struct ether_header *eptr;
for (; ; ) {
// 調(diào)用我們的next_pcap函數(shù)從分組捕獲設(shè)備獲取下一個分組
ptr = next_pcap(&len);
// 既然數(shù)據(jù)鏈路首部依照實際設(shè)備類型存在差異,我們根據(jù)pcap_datalink函數(shù)返回的datalink變量選擇分支
switch (datalink) {
case DLT_NULL: /* loopback header = 4 bytes */
return udp_check(ptr + 4, len - 4);
// 雖然名字里有10MB限定詞,這個數(shù)據(jù)鏈路類型也用于100 Mbit/s以太網(wǎng)
case DLT_EN10MB:
eptr = (struct ether_header *)ptr;
if (ntohs(eptr->ether_type) != ETHERTYPE_IP) {
err_quit("Ethernet type %x not IP", ntohs(eptr->ether_type));
}
return udp_check(ptr + 14, len - 14);
// SLIP(Serial Line Internet Protocol)鏈路利用串行端口發(fā)送和接收IP數(shù)據(jù)包
case DLT_SLIP: /* SLIP header = 24 bytes */
return udp_check(ptr + 24, len - 24);
case DLT_PPP: /* PPP header = 24 bytes */
return udp_check(ptr + 24, len - 24);
default:
err_quit("unsupported datalink (%d)", datalink);
}
}
}
以上函數(shù)中所示的針對SLIP和PPP的24字節(jié)偏移量適用于BSD/OS 2.1版本。
以下是next_pcap函數(shù),它返回來自分組捕獲設(shè)備的下一個分組:
char *next_pcap(int *len) {
char *ptr;
struct pcap_pkthdr hdr;
/* keep looking until packet ready */
// 庫函數(shù)pcap_next或者返回下一個分組,或者因超時返回NULL
// 我們在一個循環(huán)中調(diào)用pcap_next,直到返回一個分組(或者被SIGALRM信號中斷,從而在信號處理函數(shù)中跳回test_udp函數(shù))
// pcap_next函數(shù)的返回值是指向所返回分組的一個指針,它的第二個參數(shù)指向的pcap_pkthdr結(jié)構(gòu)也在返回時被填寫
while ((ptr = (char *)pcap_next(pd, &hdr)) == NULL);
// 捕獲到的數(shù)據(jù)長度通過len參數(shù)指針返回給調(diào)用者,本函數(shù)的返回值則是指向所捕獲分組的指針
*len = hdr.caplen; /* capture length */
// 函數(shù)返回值指向的數(shù)據(jù)鏈路首部,對于以太網(wǎng)幀是14字節(jié)的以太網(wǎng)首部,對于環(huán)回接口是4字節(jié)的偽鏈路首部
return ptr;
}
pcap_next函數(shù)返回分組時填寫的pcap_pkthdr結(jié)構(gòu):
ts成員是分組捕獲設(shè)備讀入該分組的時間,而不是該分組真正遞送到進程的時間。caplen成員是實際捕獲的數(shù)據(jù)量(我們的snaplen變量設(shè)為200后,又將其作為pcap_open_live函數(shù)的第二個參數(shù)),分組捕獲機制旨在捕獲每個分組的各個首部,而非捕獲其中所有數(shù)據(jù)。len成員是該分組在電纜上出現(xiàn)的完整長度,caplen總是小于len。
由上圖,pcap_next函數(shù)內(nèi)部實現(xiàn)中,pcap_read函數(shù)依賴于分組捕獲設(shè)備的類型,如BPF實現(xiàn)調(diào)用read、DLPI實現(xiàn)調(diào)用getmsg、Linux調(diào)用recvfrom。
以下cleanup函數(shù)由main函數(shù)在程序即將終止時調(diào)用,同時也用于鍵盤輸入的中斷本程序的信號的信號處理函數(shù):
void cleanup(int signo) {
struct pcap_stat stat;
putc('\n', stdout);
if (verbose) {
// 調(diào)用pcap_stats獲取分組捕獲統(tǒng)計信息
if (pcap_stats(pd, &stat) < 0) {
err_quit("pcap_stats: %s\n", pcap_geterr(pd));
}
// 由過濾器接收的分組總數(shù)
printf("%d packets received by filter\n", stat.ps_recv);
// 由內(nèi)核丟棄的分組總數(shù),丟棄原因為分組到來時沒有足夠的緩沖區(qū)空間存放它
printf("%d packets dropped by kernel\n", stat.ps_drop);
}
exit(0);
}
以下是udp_check函數(shù),它驗證IP和UDP首部中的多個字段,我們需要執(zhí)行這些驗證工作,因為由分組捕獲設(shè)備傳遞給我們的分組繞過了IP層,這一點不同于原始套接字:
struct udpiphdr *udp_check(char *ptr, int len) {
int hlen;
struct ip *ip;
struct udpiphdr *ui;
// 分組長度必須至少包括IP和UDP首部
if (len < sizeof(struct ip) + sizeof(struct udphdr)) {
err_quit("len = %d", len);
}
/* minimal verification of IP header */
ip = (struct ip *)ptr;
// 驗證IP版本
if (ip->ip_v != IPVERSION) {
err_quit("ip_v = %d", ip->ip_v);
}
hlen = ip->ip_hl << 2;
// 驗證IP首部長度
if (hlen < sizeof(struct ip)) {
err_quit("ip_hl = %d", ip->ip_hl);
}
if (len < hlen + sizeof(struct udphdr)) {
err_quit("len = %d, hlen = %d", len, hlen);
}
// 驗證IP首部校驗和
if ((ip->ip_sum = in_cksum((uint16_t *)ip, hlen)) != 0) {
err_quit("ip checksum error");
}
// 如果協(xié)議字段表明這是一個UDP數(shù)據(jù)報,就返回指向IP/UDP組合首部結(jié)構(gòu)的指針
if (ip->ip_p == IPPROTO_UDP) {
ui = (struct udpiphdr *)ip;
return ui;
// 否則就終止程序,因為我們在pcap_setfilter函數(shù)中指定了不返回其他類型的分組
} else {
err_quit("not a UDP packet");
}
}
首先使用-0命令行選項運行udpcksum程序,以驗證名字服務(wù)器對于不帶校驗和的到達數(shù)據(jù)報也給出響應(yīng),同時還指定-v命令行選項顯示詳細信息:
之后我們針對一個未開啟UDP校驗和的本地名字服務(wù)器(我們的freebsd4主機)運行udpcksum(不開啟UDP校驗和的名字服務(wù)器越來越少了):
以下是open_output和send_dns_query這兩個函數(shù)用libnet取代原始套接字實現(xiàn)的版本,libnet替我們關(guān)心許多細節(jié)問題,包括校驗和和IP首部字節(jié)序的可移植性。以下是使用libnet的open_output函數(shù):
// libnet使用一個不透明數(shù)據(jù)類型libnet_t作為調(diào)用者和函數(shù)庫的連接
static libnet_t *l; /* libnet descriptor */
void open_output(void) {
char errbuf[LIBNET_ERRBUF_SIZE];
/* Initialize libnet with an IPv4 raw socket */
// libnet_init函數(shù)返回一個libnet_t指針,調(diào)用者把它傳遞給以后的libnet函數(shù)以指示所期望的libnet實例
// 從這個意義上來說,它類似于套接字和pcap_t類型的pcap描述符
// 第一個參數(shù)為LIBNET_RAW4,會請求libnet_init函數(shù)打開一個IPv4原始套接字
// 如果發(fā)生錯誤,libnet_init函數(shù)將在它的errbuf參數(shù)中返回出錯信息,并返回空指針
l = libnet_init(LIBNET_RAW4, NULL, errbuf);
if (l == NULL) {
err_quit("Can't initialize libnet: %s", errbuf);
}
}
以下是使用libnet的send_dns_query函數(shù),可將它與使用原始套接字的send_dns_query和udp_write函數(shù)相比較:
void send_dns_query(void) {
char qbuf[24], *ptr;
u_int16_t one;
int packet_size = LIBNET_UDP_H + LIBNET_DNSV4_H + 24;
static libnet_ptag_t ip_tag, udp_tag, dns_tag;
/* build query portion of DNS packet */
// 構(gòu)造DNS分組的查詢問題部分
ptr = qbuf;
memcpy(ptr, "\001a\012root-servers\003net\000", 20);
ptr += 20;
ont = htons(1);
memcpy(ptr, &one, 2); /* query type A */
ptr += 2;
memcpy(ptr, &one, 2); /* query class = 1 (IP addr) */
/* build DNS packet */
// libnet_build_dnsv4函數(shù)接受用戶參數(shù),用來構(gòu)造DNS首部
dns_tag = libnet_build_dnsv4(1234, /* identification */
0x0100, /* flags: recursion desired */
1, /* # questions */
0, /* # answer RRs */
0, /* # authority RRs */
0, /* # additional RRs */
qbuf, /* query */
24, /* length of query */
l, dns_tag);
/* build UDP header */
// lib_build_udp函數(shù)接受用戶參數(shù),用來構(gòu)造UDP首部
udp_tag = libnet_build_udp(((struct sockaddr_in *)local)->sin_port, /* soure port */
((struct sockaddr_in *)dest)->sin_port, /* dest port */
packet_size, /* length */
0, /* checksum, libnet將自動計算校驗和并存入該字段 */
NULL, /* payload */
0, /* payload length */
l, udp_tag);
/* Since we specified the checksum as 0, libnet will automatically */
/* calculate the UDP checksum. Turn it off if the user doesn't want it. */
// 如果用戶請求不計算UDP校驗和,必須顯式禁止UDP校驗和計算
if (zerosum) {
if (libnet_toggle_checksum(1, udp_tag, LIBNET_OFF) < 0) {
err_quit("turning off checksums: %s\n", libnet_geterror(1));
}
}
/* build IP header */
// libnet_build_ipv4函數(shù)接受用戶參數(shù),用來構(gòu)造IPv4首部
// libnet會自動留意ip_len字段是否為網(wǎng)絡(luò)字節(jié)序,這是通過使用libnet令移植性得以改善的一個例子
ip_tag = libnet_build_ipv4(packet_size + LIBNET_IPV4_H, /* len */
0, /* tos */
0, /* IP ID */
0, /* fragment */
TTL_OUT, /* ttl */
IPPROTO_UDP, /* protocol */
0, /* checksum */
((struct sockaddr_in *)local)->sin_addr.s_addr, /* source */
((struct sockaddr_in *)dest)->sin_addr.s_addr, /* dest */
NULL, /* payload */
0, /* payload length */
l, ip_tag);
// libnet_write函數(shù)把組裝成的數(shù)據(jù)報寫出到網(wǎng)絡(luò)
if (libnet_write(l) < 0) {
err_quit("libnet_write: %s\n", libnet_geterror(1));
}
if (verbose) {
printf("sent: %d bytes of data\n", packet_size);
}
}
send_dns_query函數(shù)的libnet版本只有67行,而原始套接字版本(send_dns_query和udp_write函數(shù)的組合)卻有96行,且含有至少兩個移植性小問題。
原始套接字使我們有能力讀寫內(nèi)核不理解的IP數(shù)據(jù)報,數(shù)據(jù)鏈路層訪問則把這個能力進一步擴展成讀寫任何類型的數(shù)據(jù)鏈路幀,而不僅僅是IP數(shù)據(jù)報。tcpdump也許是直接訪問數(shù)據(jù)鏈路層的最常用程序。
不同操作系統(tǒng)有不同的數(shù)據(jù)鏈路層訪問方法,如源自Berkeley的BPF、SVR 4的DLPI、Linux的SOCK_PACKET,如果我們使用公開可得的分組捕獲函數(shù)庫libpcap,我們就可以忽略所有這些區(qū)別,編寫出可移植的代碼。文章來源:http://www.zghlxwxcb.cn/news/detail-683385.html
不同系統(tǒng)上編寫原始數(shù)據(jù)報可能各不相同,公開可得的libnet函數(shù)庫隱藏了這些差異,所提供的輸出接口既可在原始套接字輸出,也可在數(shù)據(jù)鏈路上直接輸出。文章來源地址http://www.zghlxwxcb.cn/news/detail-683385.html
到了這里,關(guān)于UNIX網(wǎng)絡(luò)編程卷一 學(xué)習(xí)筆記 第二十九章 數(shù)據(jù)鏈路訪問的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!