linux环境下libpcap 源代码分析
韩大卫@吉林师范大学
libpcap 源代码官方下载地址:
git clonehttps://github.com/the-tcpdump-group/libpcap.git
tcpdumpm源代码官方下载地址:
git clone git://bpf.tcpdump.org/tcpdump
tcpdump.c使用libpcap里的pcap_open_live和pcap_loop 完成两个最关键的动作:获取捕获报文的接口,和捕获报文并将报文交给callback。 (关于tcpdump源代码的构架,请参考作者的tcpdump源代码分析)
现结合libpcap源代码分析pcap_open_live和pcap_loop的实现机制,并进入linux内核,展示linux内核对这两个API的响应动作。
tcpdump.c对pcap_open_live的使用是:
pd = pcap_open_live(device, snaplen, !pflag, 1000, ebuf);
pcap_open_live定义如下:
pcap_t *pcap_open_live(const char *source, int snaplen,int promisc, int to_ms, char *errbuf)
source 为指定的网络接口。
snaplen 为最大报文长度。
Promisc 是否将设备设置为混杂模式。
to_ms 超时时间。
errbuf 为错误信息描述字符。
返回值为cap_t类型的指针,pcap_t 定义是:
typedef struct pcap pcap_t;
struct pcap {
/*typedef int (*read_op_t)(pcap_t *, int cnt,pcap_handler, u_char *);
read_op为从网络接口读取报文的函数指针,待其得到赋值后,调用实现函数*/
read_op_tread_op;
//从文件里读取报文的函数指针
int(*next_packet_op)(pcap_t *, struct pcap_pkthdr *, u_char **);
//文件描述符,即socket
int fd;
intselectable_fd;
intbufsize; //read缓冲区大小
u_char *buffer;//read缓冲区指针
u_char *bp;
int cc;
...
int snapshot;
intlinktype; /* Network linktype */
intlinktype_ext;
int tzoff; /* timezone offset */
intoffset; /* offset forproper alignment */
intactivated; /* true if the capture isreally started */
intoldstyle; /* if we're opening withpcap_open_live() */
struct pcap_optopt;
u_char *pkt;
...
//激活函数,激活函数在得到调用后,会建立起与底层IPC的socket
activate_op_tactivate_op;
...
};
pcap_t *
pcap_open_live(const char *source, int snaplen, intpromisc, int to_ms, char *errbuf){
pcap_t *p;
int status;
//创建捕获报文的接口句柄
p =pcap_create(source, errbuf);
if (p == NULL)
return(NULL);
//设置最大报文长度
status =pcap_set_snaplen(p, snaplen);
if (status <0)
goto fail;
//将设备设为混杂模式
status =pcap_set_promisc(p, promisc);
if (status <0)
goto fail;
//设置超时时间
status =pcap_set_timeout(p, to_ms);
if (status <0)
goto fail;
p->oldstyle= 1;
//pcap_avtivate调用pcap_t的activate_op, 建立起与底层IPC通道
status =pcap_activate(p);
if (status <0)
goto fail;
return (p);
...
}
pcap_t *pcap_create(const char *source, char*errbuf){
size_t i;
int is_theirs;
pcap_t *p;
if (source ==NULL)
source ="any";
//在capture_source_types数组里寻找是否有特定API集合的接口对应source
for (i = 0;capture_source_types[i].create_op != NULL; i++) {
is_theirs =0;
p =capture_source_types[i].create_op(source, errbuf, &is_theirs);
if(is_theirs) {
return (p);
}
}
//如果没有,那么就将source作为普通网络接口
return(pcap_create_interface(source, errbuf));
}
pcap_create_interface() 函数在libpcap下有多个实现,可由编译宏来指定特定的pcap_create_interface来初始化read_op等函数指针。linux环境里默认是libpcap/pcap-linux.c中的 pcap_create_interface():
pcap_t *
pcap_create_interface(const char *device, char *ebuf)
{
pcap_t *handle;
/*可将 pcap_create_common看做pcap_t结构的构造函数,初始化一个pcap_t*/
handle =pcap_create_common(device, ebuf, sizeof (struct pcap_linux));
if (handle ==NULL)
returnNULL;
//为pcap_t 的激活函数指针填充具体实现函数
handle->activate_op = pcap_activate_linux;
handle->can_set_rfmon_op = pcap_can_set_rfmon_linux;
return handle;
}
完成后回到pcap_open_live,设置snaplen,promisc,to_ms后,调用status = pcap_activate(p),该函数执行status = p->activate_op(p) ,
进而调用 pcap_activate_linux(), 完成read_op等重要函数指针的具体赋值。
static int
pcap_activate_linux(pcap_t *handle)
{
structpcap_linux *handlep = handle->priv;
const char *device;
int status = 0;
device =handle->opt.source;
handle->inject_op = pcap_inject_linux;
handle->setfilter_op = pcap_setfilter_linux;
handle->setdirection_op = pcap_setdirection_linux;
handle->set_datalink_op = pcap_set_datalink_linux;
handle->getnonblock_op = pcap_getnonblock_fd;
handle->setnonblock_op = pcap_setnonblock_fd;
handle->cleanup_op = pcap_cleanup_linux;
//最重要的函数指针read_op
handle->read_op = pcap_read_linux;
handle->stats_op = pcap_stats_linux;
if(strcmp(device, "any") == 0) {
if(handle->opt.promisc) {
handle->opt.promisc = 0;
/* Justa warning. */
snprintf(handle->errbuf, PCAP_ERRBUF_SIZE,
"Promiscuous mode not supported>先使用activete_new()
status =activate_new(handle);
if (status < 0) {
goto fail;
}
//根据错误值具体处理
if (status ==1) {
switch(activate_mmap(handle, &status)) {
case 1:
returnstatus;
case 0:
break;
case -1:
gotofail;
}
}
//如果status为0,再尝试使用activete_old()函数
else if (status== 0) {
/*Non-fatal error; try old way */
if ((status= activate_old(handle)) != 1) {
gotofail;
}
}
status = 0;
if(handle->opt.buffer_size != 0) {
//设置socket的缓冲区和缓冲区长度
if(setsockopt(handle->fd, SOL_SOCKET, SO_RCVBUF,
&handle->opt.buffer_size,
sizeof(handle->opt.buffer_size)) == -1) {
snprintf(handle->errbuf, PCAP_ERRBUF_SIZE,
"SO_RCVBUF: %s",pcap_strerror(errno));
status= PCAP_ERROR;
gotofail;
}
}
handle->selectable_fd = handle->fd;
return status;
...
}
static int
activate_new(pcap_t *handle)
{
struct pcap_linux*handlep = handle->priv;
const char *device = handle->opt.source;
int is_any_device = (strcmp(device,"any") == 0);
int sock_fd = -1, arptype;
int err = 0;
structpacket_mreq mr;
/*指定网口情况下用PF_PACKET协议通信得到原始以太网数据帧数据
关于socket()函数,我个人认为可以将其理解为open():
open()打开不同的文件,这样在返回的句柄里就可使用这个文件设备模块提供的ops,
socket()打开不同的协议,返回句柄里也包括了该协议的底层模块提供的ops. 只不过linux下面没法将网络协议当作普通文件(如/dev/xx)处理,所以才有了另一套socket特定的APIs*/
sock_fd =is_any_device ?
socket(PF_PACKET,SOCK_DGRAM, htons(ETH_P_ALL)) :
socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
...
handlep->sock_packet = 0;
/*iface_get_id()使用ioctl(fd, SIOCGIFINDEX, &ifr)获取lo还回设备的索引值*/
handlep->lo_ifindex = iface_get_id(sock_fd, "lo",handle->errbuf);
handle->offset = 0;
if(!is_any_device) {
handlep->cooked = 0;
if(handle->opt.rfmon) {
err =enter_rfmon_mode(handle, sock_fd, device);
if (err< 0) {
close(sock_fd);
return err;
}
if (err== 0) {
close(sock_fd);
return PCAP_ERROR_RFMON_NOTSUP;
}
if(handlep->mondevice != NULL)
device= handlep->mondevice;
}
/*iface_get_arptype()调用ioctl(fd, SIOCGIFHWADDR, &ifr)获取硬件类型 */
arptype =iface_get_arptype(sock_fd, device, handle->errbuf);
if (arptype< 0) {
close(sock_fd);
returnarptype;
}
map_arphrd_to_dlt(handle, arptype, 1);
...
//获取指定设备的索引值
handlep->ifindex = iface_get_id(sock_fd, device,
handle->errbuf);
if(handlep->ifindex == -1) {
close(sock_fd);
returnPCAP_ERROR;
/*iface_bind()将设备的索引值作为struct socketadd_ll的索引值与socket绑定
structsockaddr_ll sll;
sll.sll_family =AF_PACKET;
sll.sll_ifindex = ifindex;
sll.sll_protocol =htons(ETH_P_ALL);
bind(fd, (struct sockaddr *) &sll,sizeof(sll)) == -1 */
if ((err =iface_bind(sock_fd, handlep->ifindex,
handle->errbuf)) != 1) {
close(sock_fd);
if (err< 0)
return err;
else
return 0; /* try old mechanism*/
}
...
}
if(!is_any_device && handle->opt.promisc) {
memset(&mr, 0, sizeof(mr));
mr.mr_ifindex = handlep->ifindex;
mr.mr_type = PACKET_MR_PROMISC;
if(setsockopt(sock_fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP,
&mr, sizeof(mr)) == -1) {
snprintf(handle->errbuf, PCAP_ERRBUF_SIZE,
"setsockopt: %s", pcap_strerror(errno));
close(sock_fd);
returnPCAP_ERROR;
}
}
if(handlep->cooked) {
if(handle->snapshot < SLL_HDR_LEN + 1)
handle->snapshot = SLL_HDR_LEN + 1;
}
handle->bufsize = handle->snapshot;
//根据以太网链路层类型决定VLAN Tag在报文中的偏移值
switch(handle->linktype) {
caseDLT_EN10MB:
handlep->vlan_offset = 2 * ETH_ALEN;
break;
case DLT_LINUX_SLL:
handlep->vlan_offset = 14;
break;
default:
handlep->vlan_offset = -1; /* unknown */
break;
}
//将sock_fd作为pcap_t的fd
handle->fd =sock_fd;
...
}
至此,通过pcap_open_live完成全部准备阶段的内容,之后就可以使用pcap_loop()来获取来自底层的数据并提交给callback函数进行应用处理, tcpdump.c 对pcap_loop的使用是:
status = pcap_loop(pd, cnt, callback, pcap_userdata);
//cnt 为指定捕获报文的个数
在libpcap/pcap.c里有pcap_loop的定义:
int
pcap_loop(pcap_t *p, int cnt, pcap_handler callback,u_char *user)
{
register int n;
for (;;) {
if(p->rfile != NULL) {
//从文件里读取报文
n =pcap_offline_read(p, cnt, callback, user);
} else {
//从指定网口读取报文
do {
//read_op即为pcap_read_packet
n =p->read_op(p, cnt, callback, user);
} while(n == 0);
}
//当n<0时退出循环,退出pcap_loop
if (n <=0)
return(n);
//如果达到捕获报文个数,退出pcap_loop
if (cnt> 0) {
cnt -=n;
if (cnt<= 0)
return (0);
}
}
函数指针read_op指向的就是pcap_read_linux_mmap
pcap_get_ring_frame(handle, TP_STATUS_USER)进行取数据, 这里的数据是是通过应用程序进行mmap一块内核的内存,内核取得的数据保存在mmap所映射的区域,进行取。具体可以搜索下pcap_get_ring_frame这接口,网络上有说明的
bp = (unsigned char*)h.raw + tp_mac;获取之后的数据给bp
callback(user, &pcaphdr, bp);调用这个进行打印
static void print_packet(u_char *user, const struct pcap_pkthdr *h, const u_char *sp) 接着调用这个
hdrlen = (*print_info->p.printer)(h, sp);调用这个进行打印
经过初始化,调用u_int prism_if_print(const struct pcap_pkthdr *h, const u_char *p)
return PRISM_HDR_LEN + ieee802_11_print(p + PRISM_HDR_LEN, length - PRISM_HDR_LEN, caplen - PRISM_HDR_LEN, 0, 0);这里把源数据去掉PRISM_HDR_LEN的长度。
fc = EXTRACT_LE_16BITS(p);
hdrlen = extract_header_length(fc);进行去掉额外的头信息
p += hdrlen;源数据偏移26字节
p+=8再进行偏移八个字节
ip4 = (const struct ip *)p;
up = (struct udphdr *)((const u_char *)ip4 + IP_HL(ip4) * 4);
dport = EXTRACT_16BITS(&up->uh_dport);
最后就能得到相应的源地址的ip的端口号了,上述跟踪的是udp的广播包的形式打印的
}