1.Libpcap 的工作原理
Libpcap的工作原理可以描述为,当一个数据包到达网卡时,通过网络分接口(即旁路机制)将数据包发给BPF过滤器,匹配通过的数据包可以被libpcap利用创建的套接字PF_PACKET从链路层驱动程序中获得。进而在用户空间提供独立于系统的用户级API接口。
一个数据包的捕捉分为三个主要部分
- 面向底层包捕获、
- 面向中间层的数据包过滤
- 面向应用层的用户接口
这与Linux操作系统对数据包的处理流程是相同的
网卡->网卡驱动->数据链路层->IP层->传输层->应用程序
2、Libpcap的实现机制
这里实现的包捕获机制是在数据链路层增加一个旁路处理,并不干扰系统自身的网路协议栈的处理,对发送和接收的数据包通过Linux内核做过滤和缓冲处理,最后直接传递给上层应用程序。因此libpcap在捕获到达网卡的数据包后绕开了传统linux协议栈处理,直接使用链路层PF_PACKET协议族原始套接字方式向用户空间传递报文。
接下来,对照上图分层解释报文从网卡最终到达用户空间的处理流程:
- 网络报文的接收源自网络设备(网卡)。
---------------------------物理层
- 网络设备在接收到一个报文之后,通过中断IRQ告知CPU。网卡驱动程序需要注册对该中断事件的处理函数,以处理接收到的报文。在中断中执行以下操作:
- 分配一个缓冲区sk_buff,把接收的数据拷贝进去;(第一次拷贝)
- 对缓冲区结构内的一些参数做初始化以告知较高层协议数据是什么类型skb->protocol;
- 非NAPI:调用netif_rx( )函数通知内核,将帧放入CPU的softnet_data->input_pkt_queue。netif_rx会调用网络接口函数netif_rx_schedule(使用softdate_net结构中内嵌的backlog_dev作为dev参数)
- NAPI:帧存放在每个设备自己的队列之中。调用netif_rx_schedule函数(直接以对应设备的dev结构为参数)
- 然后触发相关联的软IRQ--NET_RX_SOFTIRQ,此时网卡驱动程序已经将输入设备排入轮询列表poll_list,接下来执行net_rx_action函数:
- 浏览poll_list设备列表,这些设备的入口队列都有数据;
- 非NAPI设备:执行process_backlog函数(backlog_dev->poll)
若时间片用完或者配额用尽,将该设备放置列表尾部等待下一次中断到来时继续被调用;若处理完input_pkt_queue列表中的全部报文,则将设备退出poll_list同时打开设备中断服务继续监听下一个报文到来。
- NAPI设备:执行poll函数
dev->poll可以做一些轮询的工作,如果网络设备已经接收了多个报文,可以一次性处理。就算设备此刻所接收到的报文都已经处理完了,驱动程序也可以根据某种方式预判设备在很短的一段时间内还将收到报文,于是依然将自己对应的dev结构留在poll_list中,处于轮询状态。增大了报文接收的平均延时,但避免了大量中断带来的开销。dev设备退出poll_list同时打开设备中断服务。
- 接着调用netif_receive_skb函数:
- 如果有抓包程序,由网络分接口进入BPF过滤器,将规则匹配的报文拷贝到系统内核缓存 (第二次拷贝)否则直接丢弃数据包;*注 : linux 在 PF_PACKET 类型的 socket 上支持内核过滤。Linux 内核允许我们把一个名为 LPF(Linux Packet Filter) 的过滤器直接放到 PF_PACKET 类型 socket 的处理过程中,过滤器在网卡接收中断执行后立即执行 *
- 处理数据链路层的桥接功能;
- 根据skb->protocol字段确定上层协议并提交给网络层处理-->进入网络协议栈
|
|
| 内核空间
|
/
---------------------------数据链路层
/
|
| 用户空间
|
|
- libpcap绕过了Linux内核收包流程中协议栈部分的处理,使得用户空间API可以直接调用套接字PF_PACKET从链路层驱动程序中获得数据报文的拷贝,将其从内核缓冲区拷贝至用户空间缓冲区(第三次拷贝)
fd=socket(PF_PACKET,sock_RAW,htons(ETH_P_ALL))
libpcap 函数库注册的报文接收类型为 ETH_P_ALL,即接收所有的网络数据帧,其处理函数为 packet_rcv()。该函数工作在数据链路层。
进而调用recvfrom函数获得捕获的报文 (需要进行系统调用):
packet_rcv() 函数将直接调用 skb_queue_tail() 将数据报文存放在代表相应网络连接控制结构(struct sock)的接收队列 receive_queue 中。这样数据报文在接收过程中就绕过了 TCP 层和 IP 层繁琐的协议处理过程。最后,睡眠在 sk 等待队列上的函数 packet_recvmsg() 会接收链路层数据帧并将该数据帧直接拷贝到应用程序缓冲区中。
- 最后libpcap面向用户空间提供独立于系统的可调用的函数接口
3、BPF过滤器
BPF本质上来说是一也个设备驱动(device driver),能够被应用程序用来读取网络上通过这个网络适配器的包。但是BPF又是一个特殊的驱动,因为它并没有直接控制网络适配器,而是网络适配器真正的设备驱动调用BPF来传递数据。
BPF正常情况下被用作诊断工具去检查与本机相连的网络的流通状况。一个BPF设备能够配置一个filter,根据这个filter的特征,来忽略或者接收到来的包。
BPF拥有两个组件:
the network tap
和the packet filter
。the network tap
收集来自网络设备驱动的包的一个拷贝,并把它专递给监听程序。the packet filter
决定是否接收这个包并且把它拷贝给监听程序BPF为每一个要求服务的抓包程序关联一个filter和两个buffer。BPF分配buffer 且通常情况下它的额度是4KB
the store buffer
被使用来接收来自适配器的数据;the hold buffer
被使用来拷贝包到应用程序通常情况下, 当一个包到达网络接口时, 数据链路设备驱动将把它发送到系统协议栈。但是当BPF在这个接口上面监听时,网络设备驱动将首先调用 BPF的
network tap
函数。这个tap函数将包送入每一个监听程序的filter
。而用户定义的filter
决定: 是否接收这个包; 每一个包有多少字节将会被保存。如果filter接收这个包, 那么tap 将会从数据链路层驱动的缓存中拷贝这个数目的字节数到 与这个filter关联的store buffer中(store buffer在内核中定义)。同时,网络接口的设备驱动将会重新获得控制权,且正常的协议处理将会进行。监听进程执行read系统调用去从BPF(hold buffer)接收包,并将阻塞于此。当hold buffer 满的时候(或者当超时发生时),BPF将会拷贝这些数据到进程内存空间,且唤醒这个进程。监听程序能够一次接收多个包。
4、关键函数
- 未使用NAPI的网络设备驱动程序通过
netif_rx
通知内核帧已经接收
skb = dev_alloc_skb(pkt_len + 5) ;
... ... ...
if (skb!=NULL) {
skb->dev = dev;
skb_reserve(skb,2); //把IP对齐在16字节边界上
... ... ...
/*把DATA数据拷贝到sk_buff结构*/
... ... ...
skb->protocol = eth_type_trans(skb,dev) ;
netif_rx(skb) ;
dev->last_rx=jiffies;
... ... ...
}
5、用户级API
1)获取数据包捕获描述字
函数名称:pcap_t *pcap_open_live(char *device, int snaplen, int promisc, int to_ms, char *ebuf)
函数功能:获得用于捕获网络数据包的数据包捕获描述字。
参数说明:device参数为指定打开的网络设备名。snaplen参数定义捕获数据的最大字节数。Promisc 指定是否将网络接口置于混杂模式。to_ms参数指*定超时时间(毫秒)。ebuf参数则仅在pcap_open_live()函数出错返回NULL时用于传递错误消息。
2)打开保存捕获数据包文件
函数名称:pcap_t *pcap_open_offline(char *fname, char *ebuf)
函数功能:打开以前保存捕获数据包的文件,用于读取。
参数说明:fname参数指定打开的文件名。该文件中的数据格式与tcpdump和tcpslice兼容。”-“为标准输入。ebuf参数则仅在pcap_open_offline()函数出错返回NULL时用于传递错误消息。
3)转储数据包
函数名称:pcap_dumper_t *pcap_dump_open(pcap_t *p, char *fname)
函数功能:打开用于保存捕获数据包的文件,用于写入。
参数说明:fname参数为”-“时表示标准输出。出错时返回NULL。p参数为调用pcap_open_offline() 或pcap_open_live()函数后返回的pcap结构指针,即网卡句柄。fname参数指定打开的文件名,存盘的文件名。如果返回NULL,则可调用pcap_geterr()函数获取错误消息。
4)查找网络设备
函数名称:char *pcap_lookupdev(char *errbuf)
函数功能:用于返回可被pcap_open_live()或pcap_lookupnet()函数调用的网络设备名指针。
返回值:如果函数出错,则返回NULL,同时errbuf中存放相关的错误消息。
5)获取网络号和掩码
函数名称:int pcap_lookupnet(char *device, bpf_u_int32 *netp,bpf_u_int32 *maskp, char *errbuf)
函数功能:获得指定网络设备的网络号和掩码。
参数说明:netp参数和maskp参数都是bpf_u_int32指针。
返回值:如果函数出错,则返回-1,同时errbuf中存放相关的错误消息。
6)捕获并处理数据包
** 函数名称**:int pcap_dispatch(pcap_t *p, int cnt,pcap_handler callback, u_char *user)
函数功能:捕获并处理数据包。
参数说明:cnt参数指定函数返回前所处理数据包的最大值。cnt= -1表示在一个缓冲区中处理所有的数据包。cnt=0表示处理所有数据包,直到产生以下错误之一:读取到EOF;超时读取。callback参数指定一个带有三个参数的回调函数,这三个参数为:一个从pcap_dispatch()函数传递过来的u_char指针,一个pcap_pkthdr结构的指针,和一个数据包大小的u_char指针。
返回值:如果成功则返回读取到的字节数。读取到EOF时则返回零值。出错时则返回-1,此时可调用pcap_perror()或pcap_geterr()函数获取错误消息。
** 7)捕获和处理数据包**
** 函数名称:int pcap_loop(pcap_t *p, int cnt,pcap_handler callback, u_char *user)
** 函数功能:功能基本与pcap_dispatch()函数相同,只不过此函数在cnt个数据包被处理或出现错误时才返回,但读取超时不会返回。而如果为pcap_open_live()函数指定了一个非零值的超时设置,然后调用pcap_dispatch()函数,则当超时发生时pcap_dispatch()函数会返回。cnt参数为负值时pcap_loop()函数将始终循环运行,除非出现错误。
** 8)输出数据包**
函数名称:void pcap_dump(u_char *user, struct pcap_pkthdr *h,u_char *sp)
** 函数功能**:向调用pcap_dump_open()函数打开的文件输出一个数据包。该函数可作为pcap_dispatch()函数的回调函数。
参数说明: 参数1: 所建立的文件pcap_dump_open()的返回值,要进行强制转换.;参数2: 数据包特有的内容.;参数 3: 数据包内容指针
9)编译字串至过滤程序
函数名称:int pcap_compile(pcap_t *p, struct bpf_program *fp,char *str, int optimize, bpf_u_int32 netmask)
函数功能:将str参数指定的字符串编译到过滤程序中。
参数说明:fp是一个bpf_program结构的指针,在pcap_compile()函数中被赋值。optimize参数控制结果代码的优化。netmask参数指定本地网络的网络掩码。
10)指定过滤程序
函数名称:int pcap_setfilter(pcap_t p, struct bpf_program fp)
函数功能:指定一个过滤程序。
参数说明:fp参数是bpf_program结构指针,通常取自pcap_compile()函数调用。
** 返回值:出错时返回-1;成功时返回0
11)获取下一个数据包
函数名称:u_char pcap_next(pcap_t p, struct pcap_pkthdr *h)
** 函数功能:返回指向下一个数据包的u_char指针
12)获取数据链路层类型
函数名称:int pcap_datalink(pcap_t *p)
** 函数功能**:返回数据链路层类型,例如DLT_EN10MB
13)获取快照参数值
函数名称:int pcap_snapshot(pcap_t *p)
** 函数功能**:返回pcap_open_live被调用后的snapshot参数值
14)检测字节顺序
函数名称:int pcap_is_swapped(pcap_t *p)
函数功能:返回当前系统主机字节与被打开文件的字节顺序是否不同
15)获取主版本号
** 函数名称**:int pcap_major_version(pcap_t *p)
函数功能:返回写入被打开文件所使用的pcap函数的主版本号
16)获取辅版本号
函数名称:int pcap_minor_version(pcap_t *p)
** 函数功能**:返回写入被打开文件所使用的pcap函数的辅版本号
17)结构赋值
函数名称:int pcap_stats(pcap_t *p, struct pcap_stat *ps)
函数功能:向pcap_stat结构赋值。成功时返回0。这些数值包括了从开始捕获数据以来至今共捕获到的数据包统计。如果出错或不支持数据包统计,则返回-1,且可调用pcap_perror()或pcap_geterr()函数来获取错误消息。
18)获取打开文件名
函数名称:FILE *pcap_file(pcap_t *p)
函数功能:返回被打开文件的文件名。
19)获取描述字号码
函数名称:int pcap_fileno(pcap_t *p)
函数功能:返回被打开文件的文件描述字号码
** 20)显示错误消息**
函数名称:void pcap_perror(pcap_t *p, char *prefix)
函数功能:在标准输出设备上显示最后一个pcap库错误消息。以prefix参数指定的字符串为消息头。
6、参考文档
- 基于 linux 平台的 libpcap 源代码分析
- libpcap使用方法
- libpcap编程小结
- tcpdump sniffex
- PF_PACKET
- 原始套接字 SOCK_RAW
- udp数据报从网卡驱动到用户空间流程总结
- PACKET_MMAP实现原理分析
- linux网络报文接收发送浅析
- 高速网络环境下基于零拷贝的报文捕获机制
- 基于Linux平台的libpcap源码分析和优化