Version 1.0
2008 年 4 月
本文基于pcap 0.9.8 版本,该版本发布于September 25, 2007 。RHEL AS4 Update3 附带的版本是0.8.3 (tcpdump --version )。
一、 pcap 简介
封装了OS 提供的底层抓包技术,对外提供一些统一的抓包(及发送)接口。实现这些功能的其他技术包括:BPF(Berkeley Packet Filter) DLPI(Data Link Provider Interface) ,NIT ,Linux 专用的SOCKET_PACKET 或PF_PACKET 等。 ,
二、 pcap Linux 安装
参考《INSTALL.txt 》。
进入pcap 源码目录,执行./configure ,这将检测系统环境,并生成Makefile 文件;执行make ;执行make install ,这将安装开发头文件、库、手册等;注意这不会安装动态库。
三、 pcap 开发介绍
2.1 API 介绍
本部分介绍API ,并对主要的API 进行详细的说明。
pcap_open_live , 打开由dev 指定的设备,
pcap_open_dead ,只是建立一个pcap_t 结构体,用处不大;
pcap_open_offline ,打开一个tcpdump/libpcap 格式的文件,从中读取数据;
pcap_dump_open
pcap_setnonblock
pcap_getnonblock
pcap_findalldevs ,获取设备列表
pcap_freealldevs ,关闭查询的设备
pcap_lookupdev ,获得设备信息,如eth0 ,只是获得找到的第一个设备
pcap_lookupnet ,获得IP/Mask 信息
pcap_dispatch ,抓包引擎,需循环调用
pcap_loop , 抓包引擎,与pcap_dispatch 的不同处在于它少一个超时返回参数;
pcap_dump
pcap_compile ,编译过滤语法
pcap_setfilter ,绑定过滤器
pcap_freecode
pcap_next ,轮询方式抓包
pcap_datalink
pcap_snapshot
pcap_is_swapped
pcap_major_version
pcap_minor_version
pcap_stats ,获取当前捕获的统计信息
pcap_file
pcap_fileno
pcap_perror
pcap_geterr
pcap_strerror
pcap_close ,关闭设备
pcap_dump_close
pcap_sendpacket ,发送一个原始数据包
说明:
1 .函数的返回值,0 表示成功,-1 表示错误;
2 . 参数errbuf 用于接收错误信息,不小于PCAP_ERRBUF_SIZE ;
2.2 使用pcap 的一般步骤
Ø pcap_lookupdev 等获得设备信息,网卡设备名、设备所在网络地址;
Ø pcap_open_live 打开设备,设置网卡成混杂模式;
Ø 循环调用pcap_loop 中实现包捕获引擎,编写包分析程序;
2.3 回调函数定义
typedef void (*pcap_handler)(u_char *, const struct pcap_pkthdr *, const u_char *);
2.4 设置过滤条件
首先使用pcap_compile 编译一个filter 字符串,然后使用pcap_setfilter 将编译结果绑定到一个设备;
char* filter = "udp port 5060";
bpf_program fp;
if(-1 == pcap_compile(cap_des, &fp, filter, 0, netp))
{
cout<<"compile err: "<<pcap_geterr(cap_des)<<endl;
return 6;
}
if(-1 == pcap_setfilter(cap_des, &fp))
{
cout<<"set filter err: "<<pcap_geterr(cap_des)<<endl;
return 7;
}
2.5 错误返回
存在两种获取错误原因的方式,一是通过函数参数的errbuf ;如果函数没有该参数,则使用pcap_geterr 获得,函数执行错误时会将错误信息写入结构体中的预分配的errbuf (其中一些是基于errno ),该函数返回该errbuf 的地址;
四、 pcap Linux 实现
4.1 函数
本部分介绍某些关键函数的实现:
1 . pcap_findalldevs ,首先使用socket() 获得一个socket 的句柄,然后使用ioctl 获得所有网卡信息;该函数会尝试打开找到的设备(add_or_find_if ),它只返回能够用于live capture 的设备;
2 . pcap_lookupdev ,调用pcap_findalldevs ,将找到的第一个device 返回。
3 . pcap_open_live ,
a) 参数device 赋空(NULL) 或“any ” 时将抓取所有网卡的数据包(这种情况下将不支持混杂式?);
b) 尝试使用live_open_new 打开设备(PF_PACKET) ,失败将使用live_open_old (SOCK_PACKET );
c) live_open_new ,对捕获单块网卡,调用socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL)) (数据带链路层头),如果需捕获所有网卡,调用socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL)) (不带链路层头);调用setsockopt 设置混杂模式;
d) 设置pcap_t 对象,设置操作系统相关的处理函数的指针,及初始化buffer (大小由参数snaplen 确定),
4 .pcap_close ,调用pcap_close_linux ,
5 . pcap_lookupnet ,先使用socket() 获得一个socket 句柄,然后调用ioctl 获得设备相关的参数;
6 .pcap_loop ,判断open 方式,循环调用pcap_offline_read 读取文件或read_op (pcap_read_linux )读取socket ,读取cnt 个packet ,并对每个packet 调用callback 函数;将参数user 传给callbask 函数;函数返回已处理的packet 数;
a) pcap_read_linux 调用pcap_read_packet ,后者调用recvfrom 将数据接收到bufsize ;如果kernel filter 没有起作用,调用bpf_filter callback 函数; 进行处理;最后调用
7 .pcap_dispatch , 仅调用一次read_op ,相对pcap_loop ,不能用于读取文件,及不循环;这样它的处理少一些;在Linux 下,每次调用只抓一个packet ;
8 .pcap_next ,调用pcap_dispatch 实现,每次只抓一个packet ,将packet 作为函数返回值;
9 .pcap_next_ex ,提供了读取文件的能力,其他处理与pcap_next 相仿;
10 . pcap_compile ,调用了lex_init 等函数——没看到这些函数的实现;
11 . pcap_setfilter ,调用了pcap_setfilter_linux (#ifdef SO_ATTACH_FILTER );filter 分内核filter 及pcap 自己实现的filter pcap 会优先使用内核filter ;如果filter 语法过于复杂(#ifdef USHRT_MAX ),会使用或经检查filter 不能在内核执行时, 两种,
a) 调用fix_program ,
b) 调用set_kernel_filter 设置内核filter ,使用setsockopt (SO_ATTACH_FILTER );
12 . pcap_inject ,调用p->inject_op ,pcap_inject_linux ,send 发送数据;
13 . pcap_sendpacket ,与pcap_inject 实现一样,只是更改了接口;
14 . pcap_stats ,调用stats_op (pcap_stats_linux )函数,内核版本需2.4 以上,调用getsockopt 获得数据;可统计数据包括:经过filter 到达pcap 的packet 数量、通过了filter 但是因为buffer 不足等原因而没有到达pcap 的packet 数量;
15 . pcap_setnonblock ,将socket 设置成阻塞或非阻塞模式;
16 . pcap_setdirection ,设置要抓取的packet 的方向,发出还是收到? ;
4.1 数据结构
本部分为pcap 的关键数据结构:
struct pcap_if {
struct pcap_if *next;
char *name; /* name to hand to "pcap_open_live()" */
char *description; /* textual description of interface, or NULL */
struct pcap_addr *addresses;
bpf_u_int32 flags; /* PCAP_IF_ interface flags */ PCAP_IF_LOOPBACK
};
struct pcap_pkthdr {
struct timeval ts; /* time stamp */ // 获得packet 的时间
bpf_u_int32 caplen; /* length of portion present */ // 抓取到的packet 长度
bpf_u_int32 len; /* length this packet (off wire) */ //packet 的真实长度
};
len 可能大于caplen
pcap_t ,摘出了Linux 相关部分:
struct pcap {
int fd;
int selectable_fd;
int send_fd;
int snapshot;
int linktype;
int tzoff; /* timezone offset */
int offset; /* offset for proper alignment */
int break_loop; /* flag set to force break from packet-reading loop */
#ifdef PCAP_FDDIPAD
int fddipad;
#endif
struct pcap_sf sf;
struct pcap_md md;
/*
* Read buffer.
*/
int bufsize;
u_char *buffer;
u_char *bp;
int cc;
/*
* Place holder for pcap_next().
*/
u_char *pkt;
/* We're accepting only packets in this direction/these directions. */
pcap_direction_t direction;
/*
* Methods.
*/
int (*read_op)(pcap_t *, int cnt, pcap_handler, u_char *);
int (*inject_op)(pcap_t *, const void *, size_t);
int (*setfilter_op)(pcap_t *, struct bpf_program *);
int (*setdirection_op)(pcap_t *, pcap_direction_t);
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 *);
/*
* Placeholder for filter code if bpf not in kernel.
*/
struct bpf_program fcode;
char errbuf[PCAP_ERRBUF_SIZE + 1];
int dlt_count;
u_int *dlt_list;
struct pcap_pkthdr pcap_header; /* This is needed for the pcap_next_ex() to work */
};
五、 一些问题
1 ,c 代码与c++ 代码风格比较
1 , C++ 使用继承结构区分共性与个性,将代表个性的数据结构放到子类中,这样区别能集中到子类中;C 中使用大量的条件编译,如#ifdef HAVE_PF_PACKET_SOCKETS ,代码混杂;
2 , C 中实现多态的方式,结构体中定义函数指针,不同的实现赋不同的值;
六、 一些测试数据
1 ,基于Winpcap ,使用filter ;
程序执行环境:Windows XP sp2 ,无线网卡;
测试方式:向10.130.24.158 拷贝一个超过lang="EN-US"1G 的文件,检查程序的性能情况;
测试数据:
|
描述 |
CPU(%) |
程序消耗(%) |
其他 |
1 |
不设置filter ,抓取所有数据 |
65 ~85 |
20 |
|
2 |
设置filter(udp) 使得不抓取数据 |
15 ~25 |
0 |
|
3 |
设置filter(tcp) 抓取所有数据 |
65 ~80 |
20 |
|
2 ,Winpcap 的发送速度
说明:本次测试只测试了发送函数的执行耗时,未检查接受端的情况,即不能保证数据真的通过网卡发出。
测试方式:每次发送300B 大小的数据包,每循环执行100000 或500000 次发送,记录每循环的耗时,取多次循环的折中值;另外24.158 机器上安装有两块千兆网卡,一块接在千兆交换机上,另一块接在百兆交换机上。
1 , pcap_sendpacket 与pcap_sendqueue_transmit 的发送速度比较:
每循环执行100000 次pcap_sendpacket 发送,耗时约6 秒,流量约40Mb/s ,且100Mb 网络稍快于1000Mb 网络;
使用pcap_sendqueue_transmit ,积累到100 个数据包时发送一次;千兆网络每循环耗时0.7 秒,流量342Mb/s ,百兆网络每循环耗时2.6 92Mb/s 。 秒,流量
结论: pcap_sendqueue_transmit 比 pcap_sendpacket 发送速度快得多。
2 , 用户buffer 、系统buffer 、每次发送数量对发送速度的影响
本部分测试用户buffer 、系统buffer 、每次发送数量对pcap_sendqueue_transmit 的发送速度的影响;本测试每循环发送500000 个包,每个包300B ;
关于pcap_sendqueue_alloc 的说明:该函数用于分配一块用户空间存储,应设置得足够大以容纳数据;测试发现它会影响到程序占用的内存,但对发送速度没有影响。
用户buffer 1M ,系统buffer1M
每次发包数 |
50 |
100 |
1200 |
1 |
1000Mb 网络(秒) |
3 |
4 |
3 |
32 |
100Mb 网络(秒) |
13 |
13 |
13 |
30 |
设置用户buffer 为lang="EN-US"8M ,系统buffer 1M ;
每次发包数 |
|
100 |
1200 |
|
1000Mb 网络(秒) |
|
2.7 |
3 |
|
100Mb 网络(秒) |
|
13 |
13.3 |
|
设置用户buffer 为lang="EN-US"64M ,系统buffer 1M ;
每次发包数 |
|
100 |
1200 |
|
1000Mb 网络(秒) |
|
2.7 |
3 |
|
100Mb 网络(秒) |
|
13 |
13.4 |
|
设置用户buffer 为lang="EN-US"1M ,系统buffer 1M ;
每次发包数 |
|
100 |
1200 |
|
1000Mb 网络(秒) |
|
2.7 |
3 |
|
100Mb 网络(秒) |
|
13 |
13.4 |
|
设置用户buffer 为lang="EN-US"1M ,系统buffer 64M ;
每次发包数 |
|
100 |
1200 |
|
1000Mb 网络(秒) |
|
3.7 |
3.1 |
|
100Mb 网络(秒) |
|
13 |
13.3 |
|
设置用户buffer 为lang="EN-US"8M ,系统buffer 8M ;
每次发包数 |
|
100 |
1200 |
12000 |
1000Mb 网络(秒) |
|
3.7 |
3 |
2.9 |
100Mb 网络(秒) |
|
13 |
13.4 |
13.6 |
设置用户buffer 为lang="EN-US"8M ,系统buffer 64M ;
每次发包数 |
|
100 |
1200 |
|
1000Mb 网络(秒) |
|
2.7 |
3 |
|
100Mb 网络(秒) |
|
13 |
13.4 |
|
七、 相关资料
参考资料
《pcap 编程深入解析.doc 》
Linux pcap man 手册(man pcap )