基于Linux平台的libpcap源代码分析a

libpcap   unix/linux  平台下的网络数据包捕获函数包,大多数网络监控软件都以它为基础。 Libpcap  可以在绝大多数类  unix  平台下工作,本文分析了  libpcap   linux  下的源代码实现,其中重点是  linux  的底层包捕获机制和过滤器设置方式 , 同时也简要的讨论了  libpcap  使用的包过滤机制  BPF
网络监控
绝大多数的现代操作系统都提供了对底层网络数据包捕获的机制,在捕获机制之上可以建立网络监控( Network Monitoring )应用软件。网络监控也常简称为 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
./make install



libpcap 
源代码由  20  多个  C  文件构成,但在  Linux  系统下并不是所有文件都用到。可以通过查看命令  make  的输出了解实际所用的文件。本文所针对的 libpcap  版本号为  0.8.3 ,网络类型为常规以太网。 Libpcap  应用程序从形式上看很简单,下面是一个简单的程序框架:



char * device; /* 
用来捕获数据包的网络接口的名称  */
pcap_t * p; /* 
捕获数据包句柄,最重要的数据结构  */
struct bpf_program fcode; /* 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 = (struct libnet_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] */
struct pcap_if 
{
struct pcap_if *next; 
char *name; /* 
接口设备名  */
char *description; /* 
接口描述  */
           
/*
接口的  IP  地址 地址掩码 广播地址 , 目的地址  */
struct pcap_addr addresses; 
bpf_u_int32 flags;      /* 
接口的参数  */
};

char * pcap_lookupdev(register char * 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, int protocol) ,但  Linux  内核开发者明确指出这种方式已过时。 Linux   2.2  及以后的版本中提供了一种新的协议簇  PF_PACKET  来实现捕获机制。 PF_PACKET  的调用形式为  socket(PF_PACKET, int socket_type, int protocol) ,其中  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  的具体构成。



struct pcap [pcap-int.h]

     int fd; /* 
文件描述字,实际就是  socket */
     
           /* 
 socket  上,可以使用  select()   poll()   I/O  复用类型函数  */
     int selectable_fd; 

     int snapshot; /* 
用户期望的捕获数据包最大长度  */
     int linktype; /* 
设备类型  */
     int tzoff;            /* 
时区位置,实际上没有被使用  */
     int offset;      /* 
边界对齐偏移量  */

     int break_loop; /* 
强制从读数据包循环中跳出的标志  */

     struct pcap_sf sf; /* 
数据包保存到文件的相关配置数据结构  */
     struct pcap_md md; /* 
具体描述如下  */
     
     int bufsize; /* 
读缓冲区的长度  */
     u_char buffer; /* 
读缓冲区指针  */
     u_char *bp;
     int cc;
     u_char *pkt;

     /* 
相关抽象操作的函数指针,最终指向特定操作系统的处理函数  */
     int      (*read_op)(pcap_t *, int cnt, pcap_handler, u_char *);
     int      (*setfilter_op)(pcap_t *, struct bpf_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 *, struct pcap_stat *);
     void (*close_op)(pcap_t *);

     /*
如果  BPF  过滤代码不能在内核中执行 , 则将其保存并在用户空间执行  */
     struct bpf_program fcode; 

     /* 
函数调用出错信息缓冲区  */
     char errbuf[PCAP_ERRBUF_SIZE + 1]; 
     
     /* 
当前设备支持的、可更改的数据链路类型的个数  */
     int dlt_count;
     /* 
可更改的数据链路类型号链表,在  linux  下没有使用  */
     int *dlt_list;

     /* 
数据包自定义头部,对数据包捕获时间、捕获长度、真实长度进行描述  [pcap.h] */
     struct pcap_pkthdr pcap_header;      
};

/* 
包含了捕获句柄的接口、状态、过滤信息  [pcap-int.h] */
struct pcap_md {
/* 
捕获状态结构  [pcap.h] */
struct pcap_stat stat; 

     int use_bpf; /* 
如果为 1 ,则代表使用内核过滤 */ 
     u_long      TotPkts; 
     u_long      TotAccepted; /* 
被接收数据包数目  */ 
     u_long      TotDrops;      /* 
被丢弃数据包数目  */ 
     long      TotMissed;      /* 
在过滤进行时被接口丢弃的数据包数目  */
     long      OrigMissed; /*
在过滤进行前被接口丢弃的数据包数目 */
#ifdef linux
     int      sock_packet; /* 
如果为  1 ,则代表使用  2.0  内核的  SOCK_PACKET  模式  */
     int      timeout;      /* pcap_open_live() 
函数超时返回时间 */ 
     int      clear_promisc; /* 
关闭时设置接口为非混杂模式  */ 
     int      cooked;            /* 
使用  SOCK_DGRAM  类型  */
     int      lo_ifindex;      /* 
回路设备索引号  */
     char *device;      /* 
接口设备名称  */ 
     
/* 
以混杂模式打开  SOCK_PACKET  类型  socket   pcap_t  链表 */
struct pcap *next;      
#endif
};



函数  pcap_open_live()  的调用形式

 pcap_t * pcap_open_live(const char *device, int snaplen, int promisc, int to_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 创建函数。

 

你可能感兴趣的:(数据结构,linux,socket,网络,平台,代码分析)