基於Linux平臺的libpcap源代碼分析b

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]; /* 
物理層地址 */ 
};



用戶應用程序接口
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);
}



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

包過濾機制實際上是針對數據包的布爾值操作函數,如果函數最終返回 true,則通過過濾,反之則被丟棄。形式上包過濾由一個或多個謂詞判斷的並操作(AND)和或操作(OR)構成,每一個謂詞判斷基本上對應了數據包的協議類型或某個特定值,例如:只需要 TCP 類型且端口爲 110 的數據包或 ARP 類型的數據包。包過濾機制在具體的實現上與數據包的協議類型並無多少關係,它只是把數據包簡單的看成一個字節數組,而謂詞判斷會根據具體的協議映射到數組特定位置的值。如判斷ARP類型數據包,只需要判斷數組中第 1314 個字節(以太頭中的數據包類型)是否爲 0X0806。從理論研究的意思上看,包過濾機制是一個數學問題,或者說是一個算法問題,其中心任務是如何使用最少的判斷操作、最少的時間完成過濾處理,提高過濾效率。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章