Linux下Libpcap源碼分析和包過濾機制 (2)

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

當設備找到後,下一步工作就是打開設備以準備捕獲數據包。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]; /* 物理層地址 */ 
};


第二個是在 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);
}

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