基于 linux 平台的 libpcap 源代码分析.docx

上传人:b****7 文档编号:8876600 上传时间:2023-02-02 格式:DOCX 页数:20 大小:34.60KB
下载 相关 举报
基于 linux 平台的 libpcap 源代码分析.docx_第1页
第1页 / 共20页
基于 linux 平台的 libpcap 源代码分析.docx_第2页
第2页 / 共20页
基于 linux 平台的 libpcap 源代码分析.docx_第3页
第3页 / 共20页
基于 linux 平台的 libpcap 源代码分析.docx_第4页
第4页 / 共20页
基于 linux 平台的 libpcap 源代码分析.docx_第5页
第5页 / 共20页
点击查看更多>>
下载资源
资源描述

基于 linux 平台的 libpcap 源代码分析.docx

《基于 linux 平台的 libpcap 源代码分析.docx》由会员分享,可在线阅读,更多相关《基于 linux 平台的 libpcap 源代码分析.docx(20页珍藏版)》请在冰豆网上搜索。

基于 linux 平台的 libpcap 源代码分析.docx

基于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

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 工程科技 > 城乡园林规划

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1