基于 linux 平台的 libpcap 源代码分析.docx
《基于 linux 平台的 libpcap 源代码分析.docx》由会员分享,可在线阅读,更多相关《基于 linux 平台的 libpcap 源代码分析.docx(20页珍藏版)》请在冰豆网上搜索。
基于linux平台的libpcap源代码分析
基于linux平台的libpcap源代码分析
关于作者:
施聪,成都人,高级程序员、网络设计师。
从事基于UNIX/LINUX下的c/c++程序设计和数据库建模工作已10年。
libpcap是unix/linux平台下的网络数据包捕获函数包,大多数网络监控软件都以它为基础。
Libpcap可以在绝大多数类unix平台下工作,本文分析了libpcap在linux下的源代码实现,其中重点是linux的底层包捕获机制和过滤器设置方式,同时也简要的讨论了libpcap使用的包过滤机制BPF。
网络监控
绝大多数的现代操作系统都提供了对底层网络数据包捕获的机制,在捕获机制之上可以建立网络监控(NetworkMonitoring)应用软件。
网络监控也常简称为sniffer,其最初的目的在于对网络通信情况进行监控,以对网络的一些异常情况进行调试处理。
但随着互连网的快速普及和网络攻击行为的频繁出现,保护网络的运行安全也成为监控软件的另一个重要目的。
例如,网络监控在路由器,防火墙、入侵检查等方面使用也很广泛。
除此而外,它也是一种比较有效的黑客手段,例如,美国政府安全部门的"肉食动物"计划。
包捕获机制
从广义的角度上看,一个包捕获机制包含三个主要部分:
最底层是针对特定操作系统的包捕获机制,最高层是针对用户程序的接口,第三部分是包过滤机制。
不同的操作系统实现的底层包捕获机制可能是不一样的,但从形式上看大同小异。
数据包常规的传输路径依次为网卡、设备驱动层、数据链路层、IP层、传输层、最后到达应用程序。
而包捕获机制是在数据链路层增加一个旁路处理,对发送和接收到的数据包做过滤/缓冲等相关处理,最后直接传递到应用程序。
值得注意的是,包捕获机制并不影响操作系统对数据包的网络栈处理。
对用户程序而言,包捕获机制提供了一个统一的接口,使用户程序只需要简单的调用若干函数就能获得所期望的数据包。
这样一来,针对特定操作系统的捕获机制对用户透明,使用户程序有比较好的可移植性。
包过滤机制是对所捕获到的数据包根据用户的要求进行筛选,最终只把满足过滤条件的数据包传递给用户程序。
Libpcap应用程序框架
Libpcap提供了系统独立的用户级别网络数据包捕获接口,并充分考虑到应用程序的可移植性。
Libpcap可以在绝大多数类unix平台下工作,参考资料A中是对基于libpcap的网络应用程序的一个详细列表。
在windows平台下,一个与libpcap很类似的函数包winpcap提供捕获功能,其官方网站是http:
//winpcap.polito.it/。
Libpcap软件包可从http:
//www.tcpdump.org/下载,然后依此执行下列三条命令即可安装,但如果希望libpcap能在linux上正常工作,则必须使内核支持"packet"协议,也即在编译内核时打开配置选项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))
…………
if(eth->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或"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创建函数。
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));
/*取得回路设备接口的索引*/
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方式*/
staticint
live_open_old(pcap_t*handle,constchar*device,intpromisc,
intto_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\"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);
if(handle->linktype==-1){
snprintf(ebuf,PCAP_ERRBUF_SIZE,"unknownarptype%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(intfd,constchar*device,char*ebuf)
{
structsockaddrsaddr;
memset(&saddr,0,sizeof(saddr));
strncpy(saddr.sa_data,device,sizeof(saddr.sa_data));
bind(fd,&saddr,sizeof(saddr));
}
iface_bind(int