基于 linux 平台的 libpcap 源代码分析文档格式.docx
《基于 linux 平台的 libpcap 源代码分析文档格式.docx》由会员分享,可在线阅读,更多相关《基于 linux 平台的 libpcap 源代码分析文档格式.docx(20页珍藏版)》请在冰豆网上搜索。
协议,也即在编译内核时打开配置选项CONFIG_PACKET(选项缺省为打开)。
./configure
./make
./makeinstall
libpcap源代码由20多个C文件构成,但在Linux系统下并不是所有文件都用到。
可以通过查看命令make的输出了解实际所用的文件。
本文所针对的libpcap版本号为0.8.3,网络类型为常规以太网。
Libpcap应用程序从形式上看很简单,下面是一个简单的程序框架:
char*device;
/*用来捕获数据包的网络接口的名称*/
pcap_t*p;
/*捕获数据包句柄,最重要的数据结构*/
structbpf_programfcode;
/*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))
…………
ether_type==ntohs(ETHERTYPE_ARP))
}
/*最后一步:
关闭捕获句柄,一个简单技巧是在程序初始化时增加信号处理函数,
以便在程序退出前执行本条代码*/
pcap_close(p);
检查网络设备
libpcap程序的第一步通常是在系统中找到合适的网络接口设备。
网络接口在Linux网络体系中是一个很重要的概念,它是对具体网络硬件设备的一个抽象,在它的下面是具体的网卡驱动程序,而其上则是网络协议层。
Linux中最常见的接口设备名eth0和lo。
Lo称为回路设备,是一种逻辑意义上的设备,其主要目的是为了调试网络程序之间的通讯功能。
eth0对应了实际的物理网卡,在真实网络环境下,数据包的发送和接收都要通过eht0。
如果计算机有多个网卡,则还可以有更多的网络接口,如eth1,eth2等等。
调用命令ifconfig可以列出当前所有活跃的接口及相关信息,注意对eth0的描述中既有物理网卡的MAC地址,也有网络协议的IP地址。
查看文件/proc/net/dev也可获得接口信息。
Libpcap中检查网络设备中主要使用到的函数关系如下图:
libpcap调用pcap_lookupdev()函数获得可用网络接口的设备名。
首先利用函数getifaddrs()获得所有网络接口的地址,以及对应的网络掩码、广播地址、目标地址等相关信息,再利用add_addr_to_iflist()、add_or_find_if()、get_instance()把网络接口的信息增加到结构链表pcap_if中,最后从链表中提取第一个接口作为捕获设备。
其中get_instanced()的功能是从设备名开始,找第一个是数字的字符,做为接口的实例号。
网络接口的设备号越小,则排在链表的越前面,因此,通常函数最后返回的设备名为eth0。
虽然libpcap可以工作在回路接口上,但显然libpcap开发者认为捕获本机进程之间的数据包没有多大意义。
在检查网络设备操作中,主要用到的数据结构和代码如下:
/*libpcap自定义的接口信息链表[pcap.h]*/
structpcap_if
structpcap_if*next;
char*name;
/*接口设备名*/
char*description;
/*接口描述*/
/*接口的IP地址,地址掩码,广播地址,目的地址*/
structpcap_addraddresses;
bpf_u_int32flags;
/*接口的参数*/
};
char*pcap_lookupdev(registerchar*errbuf)
pcap_if_t*alldevs;
……
pcap_findalldevs(&
alldevs,errbuf);
……
strlcpy(device,alldevs->
name,sizeof(device));
}
打开网络设备
当设备找到后,下一步工作就是打开设备以准备捕获数据包。
Libpcap的包捕获是建立在具体的操作系统所提供的捕获机制上,而Linux系统随着版本的不同,所支持的捕获机制也有所不同。
2.0及以前的内核版本使用一个特殊的socket类型SOCK_PACKET,调用形式是socket(PF_INET,SOCK_PACKET,intprotocol),但Linux内核开发者明确指出这种方式已过时。
Linux在2.2及以后的版本中提供了一种新的协议簇PF_PACKET来实现捕获机制。
PF_PACKET的调用形式为socket(PF_PACKET,intsocket_type,intprotocol),其中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的具体构成。
structpcap[pcap-int.h]
{
intfd;
/*文件描述字,实际就是socket*/
/*在socket上,可以使用select()和poll()等I/O复用类型函数*/
intselectable_fd;
intsnapshot;
/*用户期望的捕获数据包最大长度*/
intlinktype;
/*设备类型*/
inttzoff;
/*时区位置,实际上没有被使用*/
intoffset;
/*边界对齐偏移量*/
intbreak_loop;
/*强制从读数据包循环中跳出的标志*/
structpcap_sfsf;
/*数据包保存到文件的相关配置数据结构*/
structpcap_mdmd;
/*具体描述如下*/
intbufsize;
/*读缓冲区的长度*/
u_charbuffer;
/*读缓冲区指针*/
u_char*bp;
intcc;
u_char*pkt;
/*相关抽象操作的函数指针,最终指向特定操作系统的处理函数*/
int(*read_op)(pcap_t*,intcnt,pcap_handler,u_char*);
int(*setfilter_op)(pcap_t*,structbpf_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*,structpcap_stat*);
void(*close_op)(pcap_t*);
/*如果BPF过滤代码不能在内核中执行,则将其保存并在用户空间执行*/
structbpf_programfcode;
/*函数调用出错信息缓冲区*/
charerrbuf[PCAP_ERRBUF_SIZE+1];
/*当前设备支持的、可更改的数据链路类型的个数*/
intdlt_count;
/*可更改的数据链路类型号链表,在linux下没有使用*/
int*dlt_list;
/*数据包自定义头部,对数据包捕获时间、捕获长度、真实长度进行描述[pcap.h]*/
structpcap_pkthdrpcap_header;
/*包含了捕获句柄的接口、状态、过滤信息[pcap-int.h]*/
structpcap_md{
/*捕获状态结构[pcap.h]*/
structpcap_statstat;
intuse_bpf;
/*如果为1,则代表使用内核过滤*/
u_longTotPkts;
u_longTotAccepted;
/*被接收数据包数目*/
u_longTotDrops;
/*被丢弃数据包数目*/
longTotMissed;
/*在过滤进行时被接口丢弃的数据包数目*/
longOrigMissed;
/*在过滤进行前被接口丢弃的数据包数目*/
#ifdeflinux
intsock_packet;
/*如果为1,则代表使用2.0内核的SOCK_PACKET模式*/
inttimeout;
/*pcap_open_live()函数超时返回时间*/
intclear_promisc;
/*关闭时设置接口为非混杂模式*/
intcooked;
/*使用SOCK_DGRAM类型*/
intlo_ifindex;
/*回路设备索引号*/
char*device;
/*接口设备名称*/
/*以混杂模式打开SOCK_PACKET类型socket的pcap_t链表*/
structpcap*next;
#endif
函数pcap_open_live()的调用形式是pcap_t*pcap_open_live(constchar*device,intsnaplen,intpromisc,intto_ms,char*ebuf),其中如果device为NULL或"
,则对所有接口捕获,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创建函数。
staticint
live_open_new(pcap_t*handle,constchar*device,intpromisc,
intto_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));
/*取得回路设备接口的索引*/
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{/*现在是加工模式*/
md.cooked=1;
/*数据包链路层头部为结构sockaddr_ll,SLL大概是结构名称的简写形式*/
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中*/
fd=sock_fd;
/*2.0内核下函数要简单的多,因为只有唯一的一种socket方式*/
live_open_old(pcap_t*handle,constchar*device,intpromisc,
/*首先创建一个SOCK_PACKET类型的socket*/
fd=socket(PF_INET,SOCK_PACKET,htons(ETH_P_ALL));
/*2.0内核下,不支持捕获所有接口,设备必须给定*/
if(!
device){
strncpy(ebuf,"
pcap_open_live:
The\"
any\"
deviceisn'
tsupportedon2.0[.x]-kernelsystems"
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);
linktype==-1){
snprintf(ebuf,PCAP_ERRBUF_SIZE,"
unknownarptype%d"
arptype);
if(promisc){
ifr,0,sizeof(ifr));
strncpy(ifr.ifr_name,device,sizeof(ifr.ifr_name));
ioctl(handle->
fd,SIOCGIFFLAGS,&
ifr);
ifr.ifr_flags|=IFF_PROMISC;
fd,SIOCSIFFLAGS,&
比较上面两个函数的代码,还有两个细节上的区别。
首先是socket与接口绑定所使用的结构:
老式的绑定使用了结构sockaddr,而新式的则使用了2.2内核中定义的通用链路头部层结构sockaddr_ll。
iface_bind_old(intfd,constchar*device,char*ebuf)
structsockaddrsaddr;
saddr,0,sizeof(saddr));
strncpy(saddr.sa_data,device,sizeof(saddr.sa_data));
bind(fd,&
saddr,sizeof(saddr));
iface_bind(int