国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

UNIX網(wǎng)絡(luò)編程卷一 學(xué)習(xí)筆記 第二十九章 數(shù)據(jù)鏈路訪問

這篇具有很好參考價值的文章主要介紹了UNIX網(wǎng)絡(luò)編程卷一 學(xué)習(xí)筆記 第二十九章 數(shù)據(jù)鏈路訪問。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

目前大多操作系統(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:
UNIX網(wǎng)絡(luò)編程卷一 學(xué)習(xí)筆記 第二十九章 數(shù)據(jù)鏈路訪問,UNIX網(wǎng)絡(luò)編程卷一(第三版),unix
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模塊:
UNIX網(wǎng)絡(luò)編程卷一 學(xué)習(xí)筆記 第二十九章 數(shù)據(jù)鏈路訪問,UNIX網(wǎng)絡(luò)編程卷一(第三版),unix
從概念上來說,這兩個模塊類似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校驗和。
UNIX網(wǎng)絡(luò)編程卷一 學(xué)習(xí)筆記 第二十九章 數(shù)據(jù)鏈路訪問,UNIX網(wǎng)絡(luò)編程卷一(第三版),unix
我們把自行構(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ù):
UNIX網(wǎng)絡(luò)編程卷一 學(xué)習(xí)筆記 第二十九章 數(shù)據(jù)鏈路訪問,UNIX網(wǎng)絡(luò)編程卷一(第三版),unix
以下是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):
UNIX網(wǎng)絡(luò)編程卷一 學(xué)習(xí)筆記 第二十九章 數(shù)據(jù)鏈路訪問,UNIX網(wǎng)絡(luò)編程卷一(第三版),unix
ts成員是分組捕獲設(shè)備讀入該分組的時間,而不是該分組真正遞送到進程的時間。caplen成員是實際捕獲的數(shù)據(jù)量(我們的snaplen變量設(shè)為200后,又將其作為pcap_open_live函數(shù)的第二個參數(shù)),分組捕獲機制旨在捕獲每個分組的各個首部,而非捕獲其中所有數(shù)據(jù)。len成員是該分組在電纜上出現(xiàn)的完整長度,caplen總是小于len。
UNIX網(wǎng)絡(luò)編程卷一 學(xué)習(xí)筆記 第二十九章 數(shù)據(jù)鏈路訪問,UNIX網(wǎng)絡(luò)編程卷一(第三版),unix
由上圖,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命令行選項顯示詳細信息:
UNIX網(wǎng)絡(luò)編程卷一 學(xué)習(xí)筆記 第二十九章 數(shù)據(jù)鏈路訪問,UNIX網(wǎng)絡(luò)編程卷一(第三版),unix
之后我們針對一個未開啟UDP校驗和的本地名字服務(wù)器(我們的freebsd4主機)運行udpcksum(不開啟UDP校驗和的名字服務(wù)器越來越少了):
UNIX網(wǎng)絡(luò)編程卷一 學(xué)習(xí)筆記 第二十九章 數(shù)據(jù)鏈路訪問,UNIX網(wǎng)絡(luò)編程卷一(第三版),unix
以下是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ū)別,編寫出可移植的代碼。

不同系統(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)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經(jīng)查實,立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費用

相關(guān)文章

  • UNIX網(wǎng)絡(luò)編程卷一 學(xué)習(xí)筆記 第三十章 客戶/服務(wù)器程序設(shè)計范式

    UNIX網(wǎng)絡(luò)編程卷一 學(xué)習(xí)筆記 第三十章 客戶/服務(wù)器程序設(shè)計范式

    開發(fā)一個Unix服務(wù)器程序時,我們本書做過的進程控制: 1.迭代服務(wù)器(iterative server),它的適用情形極為有限,因為這樣的服務(wù)器在完成對當(dāng)前客戶的服務(wù)前無法處理已等待服務(wù)的新客戶。 2.并發(fā)服務(wù)器(concurrent server),為每個客戶調(diào)用fork派生一個子進程。傳統(tǒng)上大多U

    2024年02月09日
    瀏覽(24)
  • Unix 網(wǎng)絡(luò)編程:Socket 狀態(tài)圖&編程參數(shù)

    Unix 網(wǎng)絡(luò)編程:Socket 狀態(tài)圖&編程參數(shù)

    ? ? Flags (9 bits) (aka Control bits) . Contains 9 1-bit flags NS (1 bit): ECN-nonce - concealment protection (experimental: see RFC 3540). CWR (1 bit): Congestion Window Reduced (CWR) flag is set by the sending host to indicate that it received a TCP segment with the ECE flag set and had responded in congestion control mechanism (added to header by RFC 31

    2024年02月02日
    瀏覽(26)
  • 嵌入式學(xué)習(xí)第二十五天!(網(wǎng)絡(luò)的概念、UDP編程)

    嵌入式學(xué)習(xí)第二十五天!(網(wǎng)絡(luò)的概念、UDP編程)

    ? ? 可以用來: 數(shù)據(jù)傳輸 、 數(shù)據(jù)共享 ? ? 1. OSI協(xié)議模型: 應(yīng)用層 實際收發(fā)的數(shù)據(jù) 表示層 發(fā)送的數(shù)據(jù)是否加密 會話層 是否建立會話連接 傳輸層 數(shù)據(jù)傳輸?shù)姆绞剑〝?shù)據(jù)包,流式) 網(wǎng)絡(luò)層 數(shù)據(jù)的路由(如何從一個局域網(wǎng)到達另一個局域網(wǎng)) 數(shù)據(jù)鏈路層 局域網(wǎng)下如何通信

    2024年03月17日
    瀏覽(30)
  • 嵌入式學(xué)習(xí)第二十六天!(網(wǎng)絡(luò)傳輸:TCP編程)

    嵌入式學(xué)習(xí)第二十六天?。ňW(wǎng)絡(luò)傳輸:TCP編程)

    ? ? ? ? socket ? -? connect? -? send? -? recv ? -? close ? ? ? ? socket ? -? bind ? -? listen ? -? accept ? - recv ? -? send ? -? close ????????1. connect: ? ? ? ? ? ? ? 功能: 發(fā)送鏈接請求 ? ? ? ? ? ? ? 參數(shù): ? ? ? ? ? ? ? ? ? sockfd: 套接字文件描述符 ? ? ? ? ? ? ? ? ?

    2024年03月09日
    瀏覽(40)
  • UNIX網(wǎng)絡(luò)編程:socket實現(xiàn)client/server通信

    UNIX網(wǎng)絡(luò)編程:socket實現(xiàn)client/server通信

    閱讀 UNIX網(wǎng)絡(luò)編程 卷1:套接字聯(lián)網(wǎng)API 第3版 的前4個章節(jié),覺得有必要對書籍上的源碼案例進行復(fù)現(xiàn),并推敲TCP的C/S通信過程。 ?? 測試環(huán)境:CentOS7.6 x64 編譯server.c 和 client.c gcc server.c -g -std=gnu99 -o server 和 gcc client.c -g -std=gnu99 -o client 運行測試: ?? server.c僅僅實現(xiàn)對單個客戶

    2024年02月06日
    瀏覽(24)
  • unix網(wǎng)絡(luò)編程-簡易服務(wù)器與客戶端程序解析

    a -- address f -- file? ? ? ? eg: fputs() -- file put stream fd -- file descriptor h - host(主機) in/inet -- internet? ? ? ? eg: sockaddr_in; inet_aton n -- network(網(wǎng)絡(luò)字節(jié)序)/numeric(數(shù)值) p -- protocol(協(xié)議)/presentation(表達/呈現(xiàn)形式) s -- socket? ? ? ? eg: sin -- socket internet t -- type,用于指定某種

    2024年01月16日
    瀏覽(30)
  • UNIX網(wǎng)絡(luò)編程:socket & fork()多進程 實現(xiàn)clients/server通信

    UNIX網(wǎng)絡(luò)編程:socket & fork()多進程 實現(xiàn)clients/server通信

    UNIX網(wǎng)絡(luò)編程:socket實現(xiàn)client/server通信 隨筆簡單介紹了TCP Server服務(wù)單客戶端的socket通信,但是并未涉及多客戶端通信。 對于網(wǎng)絡(luò)編程肯定涉及到多客戶端通信和并發(fā)編程 (指在同時有大量的客戶鏈接到同一服務(wù)器),故本隨筆補充這部分知識。 而且并發(fā)并發(fā)編程涉及到多進程

    2024年02月06日
    瀏覽(23)
  • lv7 嵌入式開發(fā)-網(wǎng)絡(luò)編程開發(fā) 13 UNIX域套接字

    目錄 1 UNIX 域流式套接字 2 UNIX 域數(shù)據(jù)報套接字 UNIX 域流式套接字(UNIX domain stream socket)是一種在同一臺主機上的進程之間進行通信的機制。它不依賴于網(wǎng)絡(luò)協(xié)議棧,而是使用文件系統(tǒng)作為通信的基礎(chǔ)。 UNIX 域流式套接字提供可靠的、雙向的、面向連接的通信方式。與傳統(tǒng)的

    2024年02月07日
    瀏覽(22)
  • 【Java學(xué)習(xí)筆記】 68 - 網(wǎng)絡(luò)——TCP編程、UDP編程

    【Java學(xué)習(xí)筆記】 68 - 網(wǎng)絡(luò)——TCP編程、UDP編程

    https://github.com/yinhai1114/Java_Learning_Code/tree/main/IDEA_Chapter21/src 目錄 項目代碼 網(wǎng)絡(luò) 一、網(wǎng)絡(luò)相關(guān)概念 1.網(wǎng)絡(luò)通訊 2.網(wǎng)絡(luò) 3.IP地址 4.域名 5.端口號 6.網(wǎng)絡(luò)通訊協(xié)議 TCP協(xié)議:傳輸控制協(xié)議 UDP協(xié)議: 二、InetAddress類 1.相關(guān)方法 三、Socket 1.基本介紹 2.TCP網(wǎng)絡(luò)通信編程 基本介紹 應(yīng)用案例

    2024年02月04日
    瀏覽(26)
  • Java學(xué)習(xí)筆記37——網(wǎng)絡(luò)編程01

    計算機網(wǎng)絡(luò) 是指將地理位置不同的具有獨立功能的多臺計算機及其外部設(shè)備,通過通信線路連接起來,在網(wǎng)絡(luò)操作系統(tǒng),網(wǎng)絡(luò)管理軟件及網(wǎng)絡(luò)通信協(xié)議的管理和協(xié)調(diào)下,實現(xiàn)資源共享和信息傳遞的計算機系統(tǒng) 網(wǎng)絡(luò)編程 在網(wǎng)絡(luò)通信協(xié)議下,實現(xiàn)網(wǎng)絡(luò)互連的不同計算機上運行的

    2024年02月07日
    瀏覽(55)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包