tcpdump/libpcap中捕获数据包的时间戳

tcpdump从libpcap获取time-stamp,libpcap从OS内核获取time stamp

Q: When is a packet time-stamped? Howaccurate are the time stamps?

Tcpdump gets time stamps from libpcap, andlibpcap gets them from the OS kernel, so tcpdump - and any other programusing libpcap, such as Ethereal or snoop - is at the mercy of the time stampingcode in the OS for time stamps.

In most OSes on which tcpdump and libpcaprun, the packet is time stamped as part of the process of the networkinterface's device driver, or the networking stack, handling it. This meansthat the packet is not time stamped at the instant that it arrives at thenetwork interface; after the packet arrives at the network interface, therewill be a delay until an interrupt is delivered or the network interface ispolled (i.e., the network interface might not interrupt the host immediately -the driver may be set up to poll the interface if network traffic is heavy, toreduce the number of interrupts and process more packets per interrupt), andthere will be a further delay between the point at which the interrupt startsbeing processed and the time stamp is generated.

On some OSes, such as HP-UX, the OS kerneldoes not time stamp the packet at all; instead, it's time stamped by libpcap atthe time it reads the packet from the OS kernel, which means that there will bean even greater delay between the time the packet arrives and the time thatit's time-stamped.

Thus, the packet time stamp is notnecessarily a very accurate indication of the time it arrived at the machinethat captured the packet.

 

Tcpdump uses libpcap to capture networktraffic (as do several other applications).  The way the packet timestamps are obtained by libpcap depends on the capture mechanism libpcapuses:

   in systems with BPF, such as*BSD, Mac OS X, and AIX, BPF supplies time stamps - typically, each packet istime stamped by reading the system clock when it's processed by BPF;

WinDump, the Windows port of tcpdump, usesWinPcap, the Windows port of libpcap.  The time stamps come from theWinPcap driver, which might, depending on how it's configured, read thesystem clock for each packet, or might read it when it starts and, foreach packet, add a value from the performance counter to it.  In thelatter case, the time stamps might drift from the system clock value.

 

这几天为了研究linux中的libpcap中捕获数据包的时间戳是怎么来的做了不少工作。

首先查阅操作系统是如何计时的。

内核如何决定发包收包的时刻。

libcap.h中的结构struct pcap_pkthdr里的ts赋的值是从哪里得来的。

struct pcap_pkthdr {
        struct timeval ts;      /* time stamp */
        bpf_u_int32 caplen;     /* length of portion present */
        bpf_u_int32 len;        /* length this packet (off wire) */
};

经过查看源代码是利用 ioctl ()调用得到的 ts 值。代码如下:

if (ioctl(handle->fd,SIOCGSTAMP, &pcap_header.ts) == -1) {
   snprintf(handle->errbuf,PCAP_ERRBUF_SIZE,
    "SIOCGSTAMP: %s",pcap_strerror(errno));
   return -1;
}

pf_packet man 文档中有这样一句话: SIOCGSTAMP 用来接收最新收到的分组的时间戳,它的参数是 timeval 结构。接着查找有关 SIOCGSTAMP 的信息,在 man 7 socket 中发现了一句话: SIOCGSTAMP   返回  timeval  类型的结构,其中包括有发送给用户的最后一个包接收时的时间戳。被用来测量精确的 RTT round trip time 时间 .

1.使用ioctl得到的时间是不是内核记录下的,保存在哪里,是不是保存在struct skb中?而且调用ioctl是在用户态,一般是接收到报文后再发出这个调用。中间有段间隔时间,如果在这段时间又收到报文,那得到的时间就不是刚才收到的那个分组时的时刻!这又该如何处理?

2.通过创建pf_packet类型的套接口不仅可以捕获网卡接口接收到的报文,而且还能收到从该网络接口出去的报文。这又是怎么实现的,具体的代码在内核源代码的哪块?

上面两个问题已经知道答案了,留在这里用于以后检查自己。

 

libpcap应用程序框架

libpcap提供了系统独立的用户级别网络数据包捕获接口,并充分考虑到应用程序的可移植性。libpcap可以在绝大多数类unix平台下工作,参考资料 A 中是对基于 libpcap 的网络应用程序的一个详细列表。在windows平台下,一个与libpcap 很类似的函数包 winpcap 提供捕获功能,其官方网站是http://winpcap.polito.it/

libpcap 软件包可从 http://www.tcpdump.org/ 下载,然后依此执行下列三条命令即可安装,但如果希望libpcap能在Linux上正常工作,则必须使内核支持"packet"协议,也即在编译内核时打开配置选项 CONFIG_PACKET(选项缺省为打开)

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 = (structlibnet_ethernet_hdr *)ptr;
 
/* 第六步:对以太头部进行分析,判断所包含的数据包类型,做进一步的处理 */
if(eth->ether_type ==ntohs(ETHERTYPE_IP))
…………
if(eth->ether_type ==ntohs(ETHERTYPE_ARP))
…………
}
      
/* 最后一步:关闭捕获句柄,一个简单技巧是在程序初始化时增加信号处理函数,
以便在程序退出前执行本条代码 */
pcap_close(p);

检查网络设备

libpcap调用pcap_lookupdev()函数获得可用网络接口的设备名。首先利用函数 getifaddrs() 获得所有网络接口的地址,以及对应的网络掩码、广播地址、目标地址等相关信息,再利用 add_addr_to_iflist()add_or_find_if()get_instance() 把网络接口的信息增加到结构链表 pcap_if 中,最后从链表中提取第一个接口作为捕获设备。其中 get_instanced()的功能是从设备名开始,找第一个是数字的字符,做为接口的实例号。网络接口的设备号越小,则排在链表的越前面,因此,通常函数最后返回的设备名为 eth0。虽然 libpcap 可以工作在回路接口上,但显然libpcap 开发者认为捕获本机进程之间的数据包没有多大意义。在检查网络设备操作中,主要用到的数据结构和代码如下:

libpcap 程序的第一步通常是在系统中找到合适的网络接口设备。网络接口在Linux网络体系中是一个很重要的概念,它是对具体网络硬件设备的一个抽象,在它的下面是具体的网卡驱动程序,而其上则是网络协议层。Linux中最常见的接口设备名eth0loLo 称为回路设备,是一种逻辑意义上的设备,其主要目的是为了调试网络程序之间的通讯功能。eth0对应了实际的物理网卡,在真实网络环境下,数据包的发送和接收都要通过 eht0。如果计算机有多个网卡,则还可以有更多的网络接口,如eth1,eth2 等等。调用命令ifconfig可以列出当前所有活跃的接口及相关信息,注意对eth0的描述中既有物理网卡的MAC地址,也有网络协议的IP地址。查看文件/proc/net/dev也可获得接口信息。

/* libpcap 自定义的接口信息链表 [pcap.h] */
struct pcap_if
{
struct pcap_if *next;
char *name; /* 接口设备名 */
char *description; /* 接口描述 */
                      
/*接口的 IP 地址, 地址掩码, 广播地址,目的地址 */
struct pcap_addraddresses;
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内核开发者明确指出这种方式已过时。Linux2.2及以后的版本中提供了一种新的协议簇PF_PACKET来实现捕获机制。PF_PACKET的调用形式为socket(PF_PACKET,intsocket_type,intprotocol),其中socket类型可以是SOCK_RAWSOCK_DGRAMSOCK_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_tpcap_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_statstat; 
 
       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),其中如果deviceNULL"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 int
live_open_new(pcap_t *handle, const char *device, intpromisc,
   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 int
live_open_old(pcap_t *handle, const char *device, intpromisc,
             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 on2.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);
}
}

 

比较上面两个函数的代码,还有两个细节上的区别。首先是socket与接口绑定所使用的结构:老式的绑定使用了结构sockaddr,而新式的则使用了2.2内核中定义的通用链路头部层结构sockaddr_ll

iface_bind_old(int fd, const char *device, char *ebuf)
{
struct sockaddr             saddr;
memset(&saddr, 0, sizeof(saddr));
strncpy(saddr.sa_data, device, sizeof(saddr.sa_data));
bind(fd, &saddr, sizeof(saddr));
}
 
iface_bind(int fd, int ifindex, char *ebuf)
{
struct sockaddr_ll         sll;
memset(&sll, 0, sizeof(sll));
sll.sll_family = AF_PACKET;
sll.sll_ifindex = ifindex;
sll.sll_protocol              =htons(ETH_P_ALL);
bind(fd, (struct sockaddr *) &sll, sizeof(sll);
}
 
第二个是在 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
{
                       structtimeval ts; /* 捕获时间戳 */
                       bpf_u_int32caplen; /* 捕获到数据包的长度 */
                       bpf_u_int32len; /* 数据包的真正长度 */
}
 
/* 函数 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 中定义的特定操作系统的读数据函数:returnp->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 int
pcap_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 类型 */
#ifdefHAVE_PF_PACKET_SOCKETS
                                      structsockaddr_ll  from;
                                      structsll_header   * hdrp;
#else
                                      structsockaddr                     from;
#endif
 
socklen_t                       fromlen;
int                                  packet_len,caplen;
 
/* libpcap 自定义的头部 */
structpcap_pkthdr       pcap_header;
 
#ifdefHAVE_PF_PACKET_SOCKETS
/* 如果是加工模式,则为合成的链路层头部留出空间 */
if(handle->md.cooked)
offset =SLL_HDR_LEN;
 
/* 其它两中方式下,链路层头部不做修改的被返回,不需要留空间 */
else
offset = 0;
#else
offset = 0;
#endif
 
bp =handle->buffer + handle->offset;
      
/* 从内核中接收一个数据包,注意函数入参中对 bp 的位置进行修正 */
packet_len =recvfrom( handle->fd, bp + offset,
handle->bufsize- offset, MSG_TRUNC,
(structsockaddr *) &from, &fromlen);
      
#ifdefHAVE_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
 
#ifdefHAVE_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);
}

 

数据包过滤机制

大量的网络监控程序目的不同,期望的数据包类型也不同,但绝大多数情况都都只需要所有数据包的一(小)部分。例如:对邮件系统进行监控可能只需要端口号为 25smtp)和 110pop3) TCP 数据包,对 DNS 系统进行监控就只需要端口号为 53 UDP数据包。包过滤机制的引入就是为了解决上述问题,用户程序只需简单的设置一系列过滤条件,最终便能获得满足条件的数据包。包过滤操作可以在用户空间执行,也可以在内核空间执行,但必须注意到数据包从内核空间拷贝到用户空间的开销很大,所以如果能在内核空间进行过滤,会极大的提高捕获的效率。内核过滤的优势在低速网络下表现不明显,但在高速网络下是非常突出的。在理论研究和实际应用中,包捕获和包过滤从语意上并没有严格的区分,关键在于认识到捕获数据包必然有过滤操作。基本上可以认为,包过滤机制在包捕获机制中占中心地位。

包过滤机制实际上是针对数据包的布尔值操作函数,如果函数最终返回true,则通过过滤,反之则被丢弃。形式上包过滤由一个或多个谓词判断的并操作(AND)和或操作(OR)构成,每一个谓词判断基本上对应了数据包的协议类型或某个特定值,例如:只需要 TCP 类型且端口为110的数据包或ARP类型的数据包。包过滤机制在具体的实现上与数据包的协议类型并无多少关系,它只是把数据包简单的看成一个字节数组,而谓词判断会根据具体的协议映射到数组特定位置的值。如判断ARP类型数据包,只需要判断数组中第 1314 个字节(以太头中的数据包类型)是否为0X0806。从理论研究的意思上看,包过滤机制是一个数学问题,或者说是一个算法问题,其中心任务是如何使用最少的判断操作、最少的时间完成过滤处理,提高过滤效率。

BPF

libpcap重点使用 BPFBSD Packet Filter包过滤机制,BPF 1992 年被设计出来,其设计目的主要是解决当时已存在的过滤机制效率低下的问题。BPF的工作步骤如下:当一个数据包到达网络接口时,数据链路层的驱动会把它向系统的协议栈传送。但如果 BPF 监听接口,驱动首先调用 BPFBPF 首先进行过滤操作,然后把数据包存放在过滤器相关的缓冲区中,最后设备驱动再次获得控制。注意到BPF是先对数据包过滤再缓冲,避免了类似sunNIT过滤机制先缓冲每个数据包直到用户读数据时再过滤所造成的效率问题。参考资料D是关于BPF设计思想最重要的文献。

BPF 的设计思想和当时的计算机硬件的发展有很大联系,相对老式的过滤方式CSPFCMU/Stanford Packet Filter)它有两大特点。1:基于寄存器的过滤机制,而不是早期内存堆栈过滤机制,2:直接使用独立的、非共享的内存缓冲区。同时,BPF 在过滤算法是也有很大进步,它使用无环控制流图(CFG control flow graph,而不是老式的布尔表达式树(booleanexpression tree)。布尔表达式树理解上比较直观,它的每一个叶子节点即是一个谓词判断,而非叶子节点则为 AND 操作或 OR操作。CSPF有三个主要的缺点。1:过滤操作使用的栈在内存中被模拟,维护栈指针需要使用若干的加/减等操作,而内存操作是现代计算机架构的主要瓶颈。2:布尔表达式树造成了不需要的重复计算。3:不能分析数据包的变长头部。BPF 使用的CFG 算法实际上是一种特殊的状态机,每一节点代表了一个谓词判断,而左右边分别对应了判断失败和成功后的跳转,跳转后又是谓词判断,这样反复操作,直到到达成功或失败的终点。CFG算法的优点在于把对数据包的分析信息直接建立在图中,从而不需要重复计算。直观的看,CFG 是一种"快速的、一直向前"的算法。

总结

 1994 libpcap 的第一个版本被发布,到现在已有 11 年的历史,如今libpcap 被广泛的应用在各种网络监控软件中。libpcap最主要的优点在于平台无关性,用户程序几乎不需做任何改动就可移植到其它 unix 平台上;其次,libpcap也能适应各种过滤机制,特别对BPF的支持最好。分析它的源代码,可以学习开发者优秀的设计思想和实现技巧,也能了解到(Linux)操作系统的网络内核实现,对个人能力的提高有很大帮助。

 参考http://blog.csdn.net/tiandishenyou/article/details/8267501





  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值