libpcap是跨平臺網絡數據包捕獲函數庫,本文將基於Linux平臺對其源碼以及核心原理進行深入分析
備註: 以下分析都基於libpcap-1.8.1版本進行
以下分析按照庫的核心API爲線索展開
以下分析源碼時只列出核心邏輯
1. API: pcap_open_live
描述: 針對指定的網絡接口創建一個捕獲句柄,用於後續捕獲數據
實現邏輯分析:
@device - 指定網絡接口名,比如"eth0"。如果傳入NULL或"any",則意味着對所有接口進行捕獲
@snaplen - 設置每個數據包的捕捉長度,上限MAXIMUM_SNAPLEN
@promisc - 是否打開混雜模式
@to_ms - 設置獲取數據包時的超時時間(ms)
備註:to_ms值會影響3個捕獲函數(pcap_next、pcap_loop、pcap_dispatch)的行爲
pcap_t *pcap_open_live(const char *device, int snaplen, int promisc, int to_ms, char *errbuf)
{
pcap_t *p;
// 基於指定的設備接口創建一個pcap句柄
p = pcap_create(device, errbuf);
// 設置最大捕獲包的長度
status = pcap_set_snaplen(p, snaplen);
// 設置數據包的捕獲模式
status = pcap_set_promisc(p, promisc);
// 設置執行捕獲操作的持續時間
status = pcap_set_timeout(p, to_ms);
// 使指定pcap句柄進入活動狀態,這裏實際包含了創建捕獲套接字的動作
status = pcap_activate(p);
return p;
}
pcap_t *pcap_create(const char *device, char *errbuf)
{
pcap_t *p;
char *device_str;
// 轉儲傳入的設備名,如果傳入NULL,則設置爲"any"
if (device == NULL)
device_str = strdup("any");
else
device_str = strdup(device);
// 創建一個普通網絡接口類型的pcap句柄
p = pcap_create_interface(device_str, errbuf);
p->opt.device = device_str;
return p;
}
pcap_t *pcap_create_interface(const char *device, char *ebuf)
{
pcap_t *handle;
// 創建並初始化一個包含私有空間struct pcap_linux的pcap句柄
handle = pcap_create_common(ebuf, sizeof (struct pcap_linux));
// 在剛創建了該pcap句柄後,這裏首先覆蓋了2個回調函數
handle->activate_op = pcap_activate_linux;
handle->can_set_rfmon_op = pcap_can_set_rfmon_linux;
}
pcap_t *pcap_create_common(char *ebuf, size_t size)
{
pcap_t *p;
// 申請一片連續內存,用作包含size長度私有空間的pcap句柄,其中私有空間緊跟在該pcap結構後
p = pcap_alloc_pcap_t(ebuf, size);
// 爲新建的pcap句柄註冊一系列缺省的回調函數,這些缺省的回調函數大部分會在後面覆蓋爲linux下對應回調函數
p->can_set_rfmon_op = pcap_cant_set_rfmon;
initialize_ops(p);
return p;
}
int pcap_set_snaplen(pcap_t *p, int snaplen)
{
// 設置該pcap句柄捕獲包的最大長度前,需要確保當前並未處於活動狀態
if (pcap_check_activated(p))
return (PCAP_ERROR_ACTIVATED);
// 如果傳入了無效的最大包長,則會設置爲缺省值
if (snaplen <= 0 || snaplen > MAXIMUM_SNAPLEN)
snaplen = MAXIMUM_SNAPLEN;
p->snapshot = snaplen;
}
int pcap_set_promisc(pcap_t *p, int promisc)
{
// 設置該pcap句柄關聯接口的數據包捕獲模式前,需要確保當前並未處於活動狀態
if (pcap_check_activated(p))
return (PCAP_ERROR_ACTIVATED);
p->opt.promisc = promisc;
}
int pcap_set_timeout(pcap_t *p, int timeout_ms)
{
// 設置該pcap句柄執行捕獲操作的持續時間前,需要確保當前並未處於活動狀態
if (pcap_check_activated(p))
return (PCAP_ERROR_ACTIVATED);
p->opt.timeout = timeout_ms;
}
int pcap_activate(pcap_t *p)
{
int status;
// 確保沒有進行重複激活
if (pcap_check_activated(p))
return (PCAP_ERROR_ACTIVATED);
// 調用事先註冊的activate_op方法,完成對該pcap句柄的激活,這個過程中會創建用於捕獲的套接字,以及嘗試開啓PACKET_MMAP機制
status = p->activate_op(p);
return status;
}
int pcap_activate_linux(pcap_t *handle)
{
int status;
// 獲取該pcap句柄的私有空間
struct pcap_linux *handlep = handle->priv;
device = handle->opt.device;
// 爲該pcap句柄註冊linux平臺相關的一系列回調函數(linux平臺的回調函數又分爲2組,這裏缺省註冊了一組不使用PACKET_MMAP機制的回調)
handle->inject_op = pcap_inject_linux;
handle->setfilter_op = pcap_setfilter_linux;
handle->setdirection_op = pcap_setdirection_linux;
handle->set_datalink_op = pcap_set_datalink_linux;
handle->getnonblock_op = pcap_getnonblock_fd;
handle->setnonblock_op = pcap_setnonblock_fd;
handle->cleanup_op = pcap_cleanup_linux;
handle->read_op = pcap_read_linux;
handle->stats_op = pcap_stats_linux;
// "any"設備不支持混雜模式
if (strcmp(device, "any") == 0) {
if (handle->opt.promisc)
handle->opt.promisc = 0;
}
handlep->device = strdup(device);
handlep->timeout = handle->opt.timeout;
// 開啓混雜模式的接口,需要先從/proc/net/dev中獲取該接口當前"drop"報文數量
if (handle->opt.promisc)
handlep->proc_dropped = linux_if_drops(handlep->device);
/* 創建用於捕獲接口收到的原始報文的套接字
* 備註:舊版本的kernel使用SOCK_PACKET類型套接字來實現對原始報文的捕獲,這種過時的方式不再展開分析
* 較新的kernel使用PF_PACKET來實現該功能
*/
ret = activate_new(handle);
// 這裏只分析成功使用PF_PACKET創建套接字的情況
if (ret == 1) {
/* 嘗試對新創建的套接字開啓PACKET_MMAP功能
* 備註:舊版本的kernel不支持開啓PACKET_MMAP功能,所以只能作爲普通的原始套接字使用
* 較新版本的kernel逐漸開始支持v1、v2、v3版本的PACKET_MMAP,這裏將會嘗試開啓當前系統支持的最高版本PACKET_MMAP
*/
switch (activate_mmap(handle, &status)) {
case 1: // 返回1意味着成功開啓PACKET_MMAP功能,這裏是爲poll選擇一個合適的超時時間
set_poll_timeout(handlep);
return status;
case 0: // 返回0意味着kernel不支持PACKET_MMAP
break;
}
}
/* 程序運行到這裏只有2種可能:
* 通過新式的PF_PACKET創建了套接字,但不支持PACKET_MMAP特性時
* 通過老式的SOCKET_PACKET創建了套接字之後
*/
// 如果配置了套接字接收緩衝區長度,就在這裏進行設置
if (handle->opt.buffer_size != 0) {
setsockopt(handle->fd, SOL_SOCKET, SO_RCVBUF,&handle->opt.buffer_size,sizeof(handle->opt.buffer_size));
}
// 不開啓PACKET_MMAP的情況下,就在這裏分配用戶空間接收緩衝區
handle->buffer = malloc(handle->bufsize + handle->offset);
handle->selectable_fd = handle->fd;
}
int activate_new(pcap_t *handle)
{
struct pcap_linux *handlep = handle->priv;
const char *device = handle->opt.device;
int is_any_device = (strcmp(device, "any") == 0);
// 如果是名爲"any"的接口,則創建SOCK_DGRAM類型的套接字;通常情況下都是創建SOCK_RAW類型的套接字
sock_fd = is_any_device ?
socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL)) :
socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
// 記錄下環回接口的序號
handlep->lo_ifindex = iface_get_id(sock_fd, "lo", handle->errbuf);
handle->offset = 0;
if (!is_any_device) {
// 獲取該接口的硬件類型,linux中專門用ARPHRD_*來標識設備接口類型
arptype = iface_get_arptype(sock_fd, device, handle->errbuf);
/* pcap中使用DLT_*來標識設備接口,所以這裏就是將ARPHRD_*映射成對應的DLT_*
* 除此之外,還會根據接口類型修改offset,從而確保 offset + 2層頭長度 實現4字節對齊
*/
}
}
相關數據結構:
// 對一個接口執行捕獲操作的句柄結構
struct pcap {
read_op_t read_op; // 在該接口上進行讀操作的回調函數(linux上就是 pcap_read_linux / pcap_read_linux_mmap_v3)
int fd; // 該接口關聯的套接字
int selectable_fd; // 通常就是fd
u_int bufsize; // 接收緩衝區的有效大小,該值初始時來自用戶配置的snapshot,當開啓PACKET_MMAP時,跟配置的接收環形緩衝區tp_frame_size值同步
void *buffer; /* 當開啓PACKET_MMAP時,指向一個成員爲union thdr結構的數組,記錄了接收環形緩衝區中每個幀的幀頭;
* 當不支持PACKET_MMAP時,指向用戶空間的接收緩衝區,其大小爲 bufsize + offset
*/
int cc; // 跟配置的接收環形緩衝區tp_frame_nr值同步(由於pcap中內存塊數量和幀數量相等,所以本字段也就是內存塊數量)
int break_loop; // 標識是否強制退出循環捕獲
void *priv; // 指向該pcap句柄的私有空間(緊跟在本pcap結構後),linux下就是struct pcap_linux
struct pcap *next; // 這張鏈表記錄了所有已經打開的pcap句柄,目的是可以被用於關閉操作
int snapshot; // 該pcap句柄支持的最大捕獲包的長度,對於普通的以太網接口可以設置爲1518,對於環回口可以設置爲65549,其他情況下可以設置爲MAXIMUM_SNAPLEN
int linktype; // 接口的鏈路類型,對於以太網設備/環回設備,通常就是DLT_EN10MB
int offset; // 該值跟接口鏈路類型相關,目的是確保 offset + 2層頭長度 實現4字節對齊
int activated; // 標識該pcap句柄是否處於運作狀態,處於運作狀態的pcap句柄將不允許進行修改
struct pcap_opt opt; // 該句柄包含的一個子結構
pcap_direction_t direction; // 捕包方向
struct bpf_program fcode; // BPF過濾模塊
int dlt_count; // 該設備對應的dlt_list中元素數量,通常爲2
u_int *dlt_list; // 指向該設備對應的DLT_*列表
activate_op_t activate_op; // 對應回調函數:pcap_activate_linux
can_set_rfmon_op_t can_set_rfmon_op; // 對應回調函數:pcap_can_set_rfmon_linux
inject_op_t inject_op; // 對應回調函數:pcap_inject_linux
setfilter_op_t setfilter_op; // 對應回調函數:pcap_setfilter_linux / pcap_setfilter_linux_mmap
setdirection_op_t setdirection_op; // 對應回調函數:pcap_setdirection_linux
set_datalink_op_t set_datalink_op; // 對應回調函數:pcap_set_datalink_linux
getnonblock_op_t getnonblock_op; // 對應回調函數:pcap_getnonblock_fd / pcap_getnonblock_mmap
setnonblock_op_t setnonblock_op; // 對應回調函數:pcap_setnonblock_fd / pcap_setnonblock_mmap
stats_op_t stats_op; // 對應回調函數:pcap_stats_linux
pcap_handler oneshot_callback; // 對應回調函數:pcap_oneshot_mmap
cleanup_op_t cleanup_op; // 對應回調函數:pcap_cleanup_linux_mmap
}
// pcap句柄包含的一個子結構
struct pcap_opt {
char *device; // 接口名,比如"eth0"
int timeout; // 該pcap句柄進行捕獲操作的持續時間(ms),0意味着不超時
u_int buffer_size; // 接收緩衝區長度,缺省就是2M. 當PACKET_MMAP開啓時,該值用來配置接收環形緩衝區;當不支持PACKET_MMAP時,該值用來配置套接字的接收緩衝區
int promisc; // 標識該pcap句柄是否開啓混雜模式,需要注意的是,"any"設備不允許開啓混雜模式
int rfmon; // 表示該pcap句柄是否開啓監聽模式,該模式只用於無線網卡
int immediate; // 標識收到報文時是否立即傳遞給用戶
int tstamp_type; // 該pcap句柄使用的時間戳類型
int tstamp_precision; // 該pcap句柄使用的時間戳精度
}
// 跟pcap句柄關聯的linux平臺私有空間
struct pcap_linux {
u_int packets_read; // 統計捕獲到的包數量
long proc_dropped; // 統計丟棄的包數量
char *device; // 接口名,同步自pcap->opt.device
int filter_in_userland; // 標識用戶空間是否需要過濾包
int timeout; // 進行捕獲操作的持續時間,同步自pcap->opt.timeout
int sock_packet; // 0意味着使用了PF_PACKET方式創建的套接字
int cooked; // 1意味着使用了SOCK_DGRAM類型套接字,0意味着使用了SOCK_RAW類型套接字
int lo_ifindex; // 記錄了環回接口序號
}