1. 程式人生 > >tcpdump/libpcap中捕獲資料包的時間戳

tcpdump/libpcap中捕獲資料包的時間戳

tcpdump從libpcap獲取time-stamp,libpcap從OS核心獲取time stamp

Q: When is a packet time-stamped? Howaccurate are the time stamps?

Tcpdump gets time stamps from libpcap, andlibpcap gets them from the OS kernel, so tcpdump - and any other programusing libpcap, such as Ethereal or snoop - is at the mercy of the time stampingcode in the OS for time stamps.

In most OSes on which tcpdump and libpcaprun, the packet is time stamped as part of the process of the networkinterface's device driver, or the networking stack, handling it. This meansthat the packet is not time stamped at the instant that it arrives at thenetwork interface; after the packet arrives at the network interface, therewill be a delay until an interrupt is delivered or the network interface ispolled (i.e., the network interface might not interrupt the host immediately -the driver may be set up to poll the interface if network traffic is heavy, toreduce the number of interrupts and process more packets per interrupt), andthere will be a further delay between the point at which the interrupt startsbeing processed and the time stamp is generated.

On some OSes, such as HP-UX, the OS kerneldoes not time stamp the packet at all; instead, it's time stamped by libpcap atthe time it reads the packet from the OS kernel, which means that there will bean even greater delay between the time the packet arrives and the time thatit's time-stamped.

Thus, the packet time stamp is notnecessarily a very accurate indication of the time it arrived at the machinethat captured the packet.

Tcpdump uses libpcap to capture networktraffic (as do several other applications).  The way the packet timestamps are obtained by libpcap depends on the capture mechanism libpcapuses:

   in systems with BPF, such as*BSD, Mac OS X, and AIX, BPF supplies time stamps - typically, each packet istime stamped by reading the system clock when it's processed by BPF;

WinDump, the Windows port of tcpdump, usesWinPcap, the Windows port of libpcap.  The time stamps come from theWinPcap driver, which might, depending on how it's configured, read thesystem clock for each packet, or might read it when it starts and, foreach packet, add a value from the performance counter to it.  In thelatter case, the time stamps might drift from the system clock value.

這幾天為了研究linux中的libpcap中捕獲資料包的時間戳是怎麼來的做了不少工作。

首先查閱作業系統是如何計時的。

核心如何決定發包收包的時刻。

libcap.h中的結構struct pcap_pkthdr裡的ts賦的值是從哪裡得來的。

struct pcap_pkthdr {
        struct timeval ts;      /* time stamp */
        bpf_u_int32 caplen;     /* length of portion present */
        bpf_u_int32 len;        /* length this packet (off wire) */
};

經過檢視原始碼是利用ioctl()呼叫得到的ts值。程式碼如下:
if (ioctl(handle->fd,SIOCGSTAMP, &pcap_header.ts) == -1) {
   snprintf(handle->errbuf,PCAP_ERRBUF_SIZE,
    "SIOCGSTAMP: %s",pcap_strerror(errno));
   return -1;
}

pf_packetman文件中有這樣一句話:SIOCGSTAMP 用來接收最新收到的分組的時間戳,它的引數是 timeval 結構。接著查詢有關SIOCGSTAMP的資訊,在man7socket中發現了一句話:SIOCGSTAMP返回 timeval 型別的結構,其中包括有傳送給使用者的最後一個包接收時的時間戳。被用來測量精確的 RTT round trip time時間.

1.使用ioctl得到的時間是不是核心記錄下的,儲存在哪裡,是不是儲存在struct skb中?而且呼叫ioctl是在使用者態,一般是接收到報文後再發出這個呼叫。中間有段間隔時間,如果在這段時間又收到報文,那得到的時間就不是剛才收到的那個分組時的時刻!這又該如何處理?

2.通過建立pf_packet型別的套介面不僅可以捕獲網絡卡介面接收到的報文,而且還能收到從該網路接口出去的報文。這又是怎麼實現的,具體的程式碼在核心原始碼的哪塊?

上面兩個問題已經知道答案了,留在這裡用於以後檢查自己。

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

軟體包可從 http://www.tcpdump.org/ 下載,然後依此執行下列三條命令即可安裝,但如果希望能在Linux上正常工作,則必須使核心支援"packet"協議,也即在編譯核心時開啟配置選項 CONFIG_PACKET(選項預設為開啟)

原始碼由20多個C檔案構成,但在Linux系統下並不是所有檔案都用到。可以通過檢視命令make的輸出瞭解實際所用的檔案。本文所針對的版本號為0.8.3,網路型別為常規乙太網。應用程式從形式上看很簡單,下面是一個簡單的程式框架:

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 = (structlibnet_ethernet_hdr *)ptr;
 
/* 第六步:對以太頭部進行分析,判斷所包含的資料包型別,做進一步的處理 */
if(eth->ether_type ==ntohs(ETHERTYPE_IP))
…………
if(eth->ether_type ==ntohs(ETHERTYPE_ARP))
…………
}
      
/* 最後一步:關閉捕獲控制代碼,一個簡單技巧是在程式初始化時增加訊號處理函式,
以便在程式退出前執行本條程式碼 */
pcap_close(p);

檢查網路裝置

呼叫pcap_lookupdev()函式獲得可用網路介面的裝置名。首先利用函式 getifaddrs() 獲得所有網路介面的地址,以及對應的網路掩碼、廣播地址、目標地址等相關資訊,再利用 add_addr_to_iflist()add_or_find_if()get_instance() 把網路介面的資訊增加到結構連結串列 pcap_if 中,最後從連結串列中提取第一個介面作為捕獲裝置。其中 get_instanced()的功能是從裝置名開始,找第一個是數字的字元,做為介面的例項號。網路介面的裝置號越小,則排在連結串列的越前面,因此,通常函式最後返回的裝置名為 eth0。雖然可以工作在迴路介面上,但顯然開發者認為捕獲本機程序之間的資料包沒有多大意義。在檢查網路裝置操作中,主要用到的資料結構和程式碼如下:

程式的第一步通常是在系統中找到合適的網路介面裝置。網路介面在Linux網路體系中是一個很重要的概念,它是對具體網路硬體裝置的一個抽象,在它的下面是具體的網絡卡驅動程式,而其上則是網路協議層。Linux中最常見的介面裝置名eth0loLo 稱為迴路裝置,是一種邏輯意義上的裝置,其主要目的是為了除錯網路程式之間的通訊功能。eth0對應了實際的物理網絡卡,在真實網路環境下,資料包的傳送和接收都要通過 eht0。如果計算機有多個網絡卡,則還可以有更多的網路介面,如eth1,eth2 等等。呼叫命令ifconfig可以列出當前所有活躍的介面及相關資訊,注意對eth0的描述中既有物理網絡卡的MAC地址,也有網路協議的IP地址。檢視檔案/proc/net/dev也可獲得介面資訊。

/* libpcap 自定義的介面資訊連結串列 [pcap.h] */
struct pcap_if
{
struct pcap_if *next;
char *name; /* 介面裝置名 */
char *description; /* 介面描述 */
                      
/*介面的 IP 地址, 地址掩碼, 廣播地址,目的地址 */
struct pcap_addraddresses;
bpf_u_int32 flags;         /* 介面的引數 */
};
 
char *pcap_lookupdev(register char * errbuf)
{
       pcap_if_t *alldevs;
       ……
                       pcap_findalldevs(&alldevs, errbuf);
                       ……
                       strlcpy(device, alldevs->name,sizeof(device));
       }

 

開啟網路裝置當裝置找到後,下一步工作就是開啟裝置以準備捕獲資料包。的包捕獲是建立在具體的作業系統所提供的捕獲機制上,而Linux系統隨著版本的不同,所支援的捕獲機制也有所不同。

2.0及以前的核心版本使用一個特殊的socket型別SOCK_PACKET,呼叫形式是socket(PF_INET,SOCK_PACKET,int protocol),但Linux核心開發者明確指出這種方式已過時。Linux2.2及以後的版本中提供了一種新的協議簇PF_PACKET來實現捕獲機制。PF_PACKET的呼叫形式為socket(PF_PACKET,intsocket_type,intprotocol),其中socket型別可以是SOCK_RAWSOCK_DGRAMSOCK_RAW型別使得資料包從資料鏈路層取得後,不做任何修改直接傳遞給使用者程式,而SOCK_DRRAM則要對資料包進行加工(cooked),把資料包的資料鏈路層頭部去掉,而使用一個通用結構sockaddr_ll來儲存鏈路資訊。

使用2.0版本核心捕獲資料包存在多個問題:首先,SOCK_PACKET方式使用結構sockaddr_pkt來儲存資料鏈路層資訊,但該結構缺乏包型別資訊;其次,如果引數MSG_TRUNC傳遞給讀包函式recvmsg()recv()recvfrom()等,則函式返回的資料包長度是實際讀到的包資料長度,而不是資料包真正的長度。的開發者在原始碼中明確建議不使用2.0版本進行捕獲。

相對2.0版本SOCK_PACKET方式,2.2版本的PF_PACKET方式則不存在上述兩個問題。在實際應用中,使用者程式顯然希望直接得到"原始"的資料包,因此使用SOCK_RAW型別最好。但在下面兩種情況下,不得不使用SOCK_DGRAM型別,從而也必須為資料包合成一個""鏈路層頭部(sockaddr_ll)。

某些型別的裝置資料鏈路層頭部不可用:例如Linux核心的 PPP 協議實現程式碼對 PPP 資料包頭部的支援不可靠。

在捕獲裝置為"any"時:所有裝置意味著對所有介面進行捕獲,為了使包過濾機制能在所有型別的資料包上正常工作,要求所有的資料包有相同的資料鏈路頭部。

開啟網路裝置的主函式是pcap_open_live()[pcap-Linux.c],其任務就是通過給定的介面裝置名,獲得一個捕獲控制代碼:結構pcap_tpcap_t是大多數函式都要用到的引數,其中最重要的屬性則是上面討論到的三種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_statstat; 
 
       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),其中如果deviceNULL"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, intpromisc,
   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, intpromisc,
             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 on2.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]; /* 物理層地址 */
};
 

使用者應用程式介面

提供的使用者程式介面比較簡單,通過反覆呼叫函式pcap_next()[pcap.c]則可獲得捕獲到的資料包。下面是一些使用到的資料結構:

/* 單個數據包結構,包含資料包元資訊和資料資訊 */
struct singleton [pcap.c]
{
struct pcap_pkthdr hdr; /* libpcap 自定義資料包頭部 */
const u_char * pkt; /* 指向捕獲到的網路資料 */
};
 
/* 自定義頭部在把資料包儲存到檔案中也被使用 */
struct pcap_pkthdr
{
                       structtimeval ts; /* 捕獲時間戳 */
                       bpf_u_int32caplen; /* 捕獲到資料包的長度 */
                       bpf_u_int32len; /* 資料包的真正長度 */
}
 
/* 函式 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 中定義的特定作業系統的讀資料函式:returnp->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 型別 */
#ifdefHAVE_PF_PACKET_SOCKETS
                                      structsockaddr_ll  from;
                                      structsll_header   * hdrp;
#else
                                      structsockaddr                     from;
#endif
 
socklen_t                       fromlen;
int                                  packet_len,caplen;
 
/* libpcap 自定義的頭部 */
structpcap_pkthdr       pcap_header;
 
#ifdefHAVE_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,
(structsockaddr *) &from, &fromlen);
      
#ifdefHAVE_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
 
#ifdefHAVE_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);
}

 

資料包過濾機制

大量的網路監控程式目的不同,期望的資料包型別也不同,但絕大多數情況都都只需要所有資料包的一(小)部分。例如:對郵件系統進行監控可能只需要埠號為 25smtp)和 110pop3) TCP 資料包,對 DNS 系統進行監控就只需要埠號為 53 UDP資料包。包過濾機制的引入就是為了解決上述問題,使用者程式只需簡單的設定一系列過濾條件,最終便能獲得滿足條件的資料包。包過濾操作可以在使用者空間執行,也可以在核心空間執行,但必須注意到資料包從核心空間拷貝到使用者空間的開銷很大,所以如果能在核心空間進行過濾,會極大的提高捕獲的效率。核心過濾的優勢在低速網路下表現不明顯,但在高速網路下是非常突出的。在理論研究和實際應用中,包捕獲和包過濾從語意上並沒有嚴格的區分,關鍵在於認識到捕獲資料包必然有過濾操作。基本上可以認為,包過濾機制在包捕獲機制中佔中心地位。

包過濾機制實際上是針對資料包的布林值操作函式,如果函式最終返回true,則通過過濾,反之則被丟棄。形式上包過濾由一個或多個謂詞判斷的並操作(AND)和或操作(OR)構成,每一個謂詞判斷基本上對應了資料包的協議型別或某個特定值,例如:只需要 TCP 型別且埠為110的資料包或ARP型別的資料包。包過濾機制在具體的實現上與資料包的協議型別並無多少關係,它只是把資料包簡單的看成一個位元組陣列,而謂詞判斷會根據具體的協議對映到陣列特定位置的值。如判斷ARP型別資料包,只需要判斷陣列中第 1314 個位元組(以太頭中的資料包型別)是否為0X0806。從理論研究的意思上看,包過濾機制是一個數學問題,或者說是一個演算法問題,其中心任務是如何使用最少的判斷操作、最少的時間完成過濾處理,提高過濾效率。

BPF

重點使用 BPFBSD Packet Filter包過濾機制,BPF 1992 年被設計出來,其設計目的主要是解決當時已存在的過濾機制效率低下的問題。BPF的工作步驟如下:當一個數據包到達網路介面時,資料鏈路層的驅動會把它向系統的協議棧傳送。但如果 BPF 監聽介面,驅動首先呼叫 BPFBPF 首先進行過濾操作,然後把資料包存放在過濾器相關的緩衝區中,最後裝置驅動再次獲得控制。注意到BPF是先對資料包過濾再緩衝,避免了類似sunNIT過濾機制先緩衝每個資料包直到使用者讀資料時再過濾所造成的效率問題。參考資料D是關於BPF設計思想最重要的文獻。

BPF 的設計思想和當時的計算機硬體的發展有很大聯絡,相對老式的過濾方式CSPFCMU/Stanford Packet Filter)它有兩大特點。1:基於暫存器的過濾機制,而不是早期記憶體堆疊過濾機制,2:直接使用獨立的、非共享的記憶體緩衝區。同時,BPF 在過濾演算法是也有很大進步,它使用無環控制流圖(CFG control flow graph,而不是老式的布林表示式樹(booleanexpression tree)。布林表示式樹理解上比較直觀,它的每一個葉子節點即是一個謂詞判斷,而非葉子節點則為 AND 操作或 OR操作。CSPF有三個主要的缺點。1:過濾操作使用的棧在記憶體中被模擬,維護棧指標需要使用若干的加/減等操作,而記憶體操作是現代計算機架構的主要瓶頸。2:布林表示式樹造成了不需要的重複計算。3:不能分析資料包的變長頭部。BPF 使用的CFG 演算法實際上是一種特殊的狀態機,每一節點代表了一個謂詞判斷,而左右邊分別對應了判斷失敗和成功後的跳轉,跳轉後又是謂詞判斷,這樣反覆操作,直到到達成功或失敗的終點。CFG演算法的優點在於把對資料包的分析資訊直接建立在圖中,從而不需要重複計算。直觀的看,CFG 是一種"快速的、一直向前"的演算法。

總結

1994 的第一個版本被髮布,到現在已有 11 年的歷史,如今被廣泛的應用在各種網路監控軟體中。最主要的優點在於平臺無關性,使用者程式幾乎不需做任何改動就可移植到其它 unix 平臺上;其次,也能適應各種過濾機制,特別對BPF的支援最好。分析它的原始碼,可以學習開發者優秀的設計思想和實現技巧,也能瞭解到(Linux)作業系統的網路核心實現,對個人能力的提高有很大幫助。