libpcap原理

来源:互联网 发布:黑马程序员 达内 编辑:程序博客网 时间:2024/05/22 15:21

LibPcap通过pcap_open_live()系统调用socket()创建一个socket.而系统调用socket()则是通过sys_socketcall()这个入口找到sys_socket()->sock_create()->__sock_create()->rcu_dereference(net_families[family])根据协议簇执行create。LibPcap选用的协议簇PF_PACKET通过af_packet.c中的packet_init()调用snapgear/linux-2.6.x/net/socket.c中的sock_register()被初始化注册进net_families[],其.create=packet create。因此LibPcap创建socket最终调用了packet_create(),在packet_create()中创建了sk并有sock->ops = &packet_ops;po->prot_hook.func=packet_rcv;而static const struct proto_ops packet_ops.recvmsg=packet_recvmsg,这便是用户程序通过LibPcap从socket取得数据包的入口。因此用户程序通过LibPcap获取数据包的整个过程可以简易描述为:由packet_rcv()接收来自底层的包(具体什么位置,我没有搞明白),并分配出一段buffer,在sk_receive_queue资源不足以再容纳下一段数据时,直接将数据丢弃kfree_skb(),并记录tp_drops(即我们通过pcap_stats()得到的ps_drop);而用户程序则是不时调用packet_recvmsg()从队列中一次性获取数据,并最后释放资源skb_free_datagram()。

  其实到这里,我还未交代主题,那么导致LibPcap丢包的原因在哪里呢?了解了LibPcap捕获数据包的过程再来查找就没那么茫然了,debugging发现packet_recvmsg()的执行频度远小于packet_rcv(),所以在packet_rcv()接收数据并充盈sk_rmem_alloc后,packet_recvmsg()并不能及时将其清空,在这个时间差中只能丢包了。那么为什么packet_recvmsg()执行的频度不够呢?这可能是更底层的问题,限于能力,我在此无法给出解释。

函数 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 创建函数。

static intlive_open_new(pcap_t *handle, const char *device, int promisc,   int to_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 方式 */static intlive_open_old(pcap_t *handle, const char *device, int promisc,       int to_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\" device isn't supported          on 2.0[.x]-kernel systems", 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, "unknown arptype %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);}}

 

第二个是在 2.2 版本中设置设备为混杂模式时,使用了函数 setsockopt(),以及新的标志 PACKET_ADD_MEMBERSHIP 和结构 packet_mreq。我估计这种方式主要是希望提供一个统一的调用接口,以代替传统的(混乱的)ioctl 调用。

struct packet_mreq{int             mr_ifindex;    /* 接口索引号 */unsigned short  mr_type;       /* 要执行的操作(号) */unsigned short  mr_alen;       /* 地址长度 */unsigned char   mr_address[8]; /* 物理层地址 */ };

用户应用程序接口

Libpcap 提供的用户程序接口比较简单,通过反复调用函数pcap_next()[pcap.c] 则可获得捕获到的数据包。下面是一些使用到的数据结构:

/* 单个数据包结构,包含数据包元信息和数据信息 */struct singleton [pcap.c]{struct pcap_pkthdr hdr; /* libpcap 自定义数据包头部 */const u_char * pkt; /* 指向捕获到的网络数据 */};/* 自定义头部在把数据包保存到文件中也被使用 */struct pcap_pkthdr {  struct timeval ts; /* 捕获时间戳 */   bpf_u_int32 caplen; /* 捕获到数据包的长度 */  bpf_u_int32 len; /* 数据包的真正长度 */}/* 函数 pcap_next() 实际上是对函数 pcap_dispatch()[pcap.c] 的一个包装 */const u_char * pcap_next(pcap_t *p, struct pcap_pkthdr *h){struct singleton s;s.hdr = h;/*入参"1"代表收到1个数据包就返回;回调函数 pcap_oneshot() 是对结构 singleton 的属性赋值 */if (pcap_dispatch(p, 1, pcap_oneshot, (u_char*)&s) <= 0)return (0);return (s.pkt); /* 返回数据包缓冲区的指针 */}

pcap_dispatch() 简单的调用捕获句柄 pcap_t 中定义的特定操作系统的读数据函数:return p->read_op(p, cnt, callback, user)。在 linux 系统下,对应的读函数为 pcap_read_linux()(在创建捕获句柄时已定义 [pcap-linux.c]),而pcap_read_linux() 则是直接调用 pcap_read_packet()([pcap-linux.c])。

pcap_read_packet() 的中心任务是利用了 recvfrom() 从已创建的 socket 上读数据包数据但是考虑到 socket 可能为前面讨论到的三种方式中的某一种,因此对数据缓冲区的结构有相应的处理,主要表现在加工模式下对伪链路层头部的合成。具体代码分析如下:

static intpcap_read_packet(pcap_t *handle, pcap_handler callback, u_char *userdata){/* 数据包缓冲区指针 */u_char * bp;/* bp 与捕获句柄 pcap_t 中 handle->buffer之间的偏移量,其目的是为在加工模式捕获情况下,为合成的伪数据链路层头部留出空间 */int offset;/* PACKET_SOCKET 方式下,recvfrom() 返回 scokaddr_ll 类型,而在SOCK_PACKET 方式下,返回 sockaddr 类型 */#ifdef HAVE_PF_PACKET_SOCKETS    struct sockaddr_ll from;   struct sll_header * hdrp;#else   struct sockaddr  from;#endifsocklen_t  fromlen;int   packet_len, caplen;/* libpcap 自定义的头部 */struct pcap_pkthdr pcap_header;#ifdef HAVE_PF_PACKET_SOCKETS/* 如果是加工模式,则为合成的链路层头部留出空间 */if (handle->md.cooked)offset = SLL_HDR_LEN;/* 其它两中方式下,链路层头部不做修改的被返回,不需要留空间 */elseoffset = 0;#elseoffset = 0;#endifbp = handle->buffer + handle->offset; /* 从内核中接收一个数据包,注意函数入参中对 bp 的位置进行修正 */packet_len = recvfrom( handle->fd, bp + offset,handle->bufsize - offset, MSG_TRUNC,(struct sockaddr *) &from, &fromlen); #ifdef HAVE_PF_PACKET_SOCKETS /* 如果是回路设备,则只捕获接收的数据包,而拒绝发送的数据包。显然,我们只能在 PF_PACKET方式下这样做,因为 SOCK_PACKET 方式下返回的链路层地址类型为sockaddr_pkt,缺少了判断数据包类型的信息。*/if (!handle->md.sock_packet &&from.sll_ifindex == handle->md.lo_ifindex &&from.sll_pkttype == PACKET_OUTGOING)return 0;#endif#ifdef HAVE_PF_PACKET_SOCKETS/* 如果是加工模式,则合成伪链路层头部 */if (handle->md.cooked) {/* 首先修正捕包数据的长度,加上链路层头部的长度 */packet_len += SLL_HDR_LEN;  hdrp = (struct sll_header *)bp;  /* 以下的代码分别对伪链路层头部的数据赋值 */hdrp->sll_pkttype = xxx;hdrp->sll_hatype = htons(from.sll_hatype);hdrp->sll_halen = htons(from.sll_halen);memcpy(hdrp->sll_addr, from.sll_addr, (from.sll_halen > SLL_ADDRLEN) ? SLL_ADDRLEN : from.sll_halen);hdrp->sll_protocol = from.sll_protocol;}#endif /* 修正捕获的数据包的长度,根据前面的讨论,SOCK_PACKET 方式下长度可能是不准确的 */caplen = packet_len;if (caplen > handle->snapshot)caplen = handle->snapshot;/* 如果没有使用内核级的包过滤,则在用户空间进行过滤*/if (!handle->md.use_bpf && handle->fcode.bf_insns) {if (bpf_filter(handle->fcode.bf_insns, bp,packet_len, caplen) == 0){/* 没有通过过滤,数据包被丢弃 */return 0;}}/* 填充 libpcap 自定义数据包头部数据:捕获时间,捕获的长度,真实的长度 */ioctl(handle->fd, SIOCGSTAMP, &pcap_header.ts);pcap_header.caplen = caplen;pcap_header.len  = packet_len; /* 累加捕获数据包数目,注意到在不同内核/捕获方式情况下数目可能不准确 */handle->md.stat.ps_recv++;/* 调用用户定义的回调函数 */callback(userdata, &pcap_header, bp);}

原创粉丝点击