快捷搜索:

Linux下Libpcap源码分析和包过滤机制

libpcap是unix/linux平台下的收集数据包捕获函数包,大年夜多半收集监控软件都以它为根基。Libpcap可以在绝大年夜多半类unix平台下事情,本文阐发了libpcap在linux 下的源代码实现,此中重点是linux的底层包捕获机制和过滤器设置要领,同时也简要的评论争论了 libpcap应用的包过滤机制 BPF。

收集监控

绝大年夜多半的今世操作系统都供给了对底层收集数据包捕获的机制,在捕获机制之上可以建立收集监控(Network Monitoring)利用软件。收集监控也常简称为sniffer,其最初的目的在于对收集通信环境进行监控,以对收集的一些非常环境进行调试处置惩罚。但跟着互连网的快速遍及和收集进击行径的频繁呈现,保护收集的运行安然也成为监控软件的另一个紧张目的。例如,收集监控在路由器,防火墙、入侵反省等方面应用也很广泛。除此而外,它也是一种对照有效的黑客手段,例如,美国政府安然部门的"肉食动物"计划。

包捕获机制

从广义的角度上看,一个包捕获机制包孕三个主要部分:最底层是针对特定操作系统的包捕获机制,最高层是针对用户法度榜样的接口,第三部分是包过滤机制。

不合的操作系统实现的底层包捕获机制可能是不一样的,但从形式上看大年夜同小异。数据包老例的传输路径依次为网卡、设备驱动层、数据链路层、IP层、传输层、着末到达利用法度榜样。而包捕获机制是在数据链路层增添一个旁路处置惩罚,对发送和接管到的数据包做过滤/缓冲等相关处置惩罚,着末直接通报到利用法度榜样。值得留意的是,包捕获机制并不影响操作系统对数据包的收集栈处置惩罚。对用户法度榜样而言,包捕获机制供给了一个统一的接口,应用户法度榜样只必要简单的调用多少函数就能得到所期望的数据包。这样一来,针对特定操作系统的捕获机制对用户透明,应用户法度榜样有对照好的可移植性。包过滤机制是对所捕获到的数据包根据用户的要求进行筛选,终极只把满意过滤前提的数据包通报给用户法度榜样。

Libpcap利用法度榜样框架

Libpcap供给了系统自力的用户级别收集数据包捕获接口,并充分斟酌到利用法度榜样的可移植性。Libpcap可以在绝大年夜多半类unix平台下事情,参考资料 A 中是对基于 libpcap 的收集利用法度榜样的一个具体列表。在windows平台下,一个与libpcap 很类似的函数包 winpcap 供给捕获功能,其官方网站是http://winpcap.polito.it/。

Libpcap 软件包可从 http://www.tcpdump.org/ 下载,然后依此履行下列三条敕令即可安装,但假如盼望libpcap能在linux上正常事情,则必须使内核支持"packet"协议,也即在编译内核时打开设置设置设备摆设摆设选项 CONFIG_PACKET(选项缺省为打开)。

./configure

./make

./make install

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 = (struct libnet_ethernet_hdr *)ptr;

/* 第六步:对以太头部进行阐发,判断所包孕的数据包类型,做进一步的处置惩罚 */

if(eth->ether_type == ntohs(ETHERTYPE_IP))

…………

if(eth->ether_type == ntohs(ETHERTYPE_ARP))

…………

}

/* 着末一步:关闭捕获句柄,一个简单技术是在法度榜样初始化时增添旌旗灯号处置惩罚函数,

以便在法度榜样退出前履行本条代码 */

pcap_close(p);

/* libpcap 自定义的接口信息链表 [pcap.h] */

struct pcap_if

{

struct pcap_if *next;

char *name; /* 接口设备名 */

char *description; /* 接口描述 */

/*接口的 IP 地址, 地址掩码, 广播地址,目的地址 */

struct pcap_addr addresses;

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 内核开拓者明确指出这种要领已逾期。Linux 在 2.2及今后的版本中供给了一种新的协议簇 PF_PACKET 来实现捕获机制。PF_PACKET 的调用形式为 socket(PF_PACKET, int socket_type, int protocol),此中socket类型可所以 SOCK_RAW和SOCK_DGRAM。SOCK_RAW 类型使得数据包从数据链路层取得后,不做任何改动直接通报给用户法度榜样,而 SOCK_DRRAM 则要对数据包进行加工(cooked),把数据包的数据链路层头部去掉落,而应用一个通用布局 sockaddr_ll 来保存链路信息。

应用 2.0 版本内核捕获数据包存在多个问题:首先,SOCK_PACKET 要领应用布局 sockaddr_pkt来保存数据链路层信息,但该布局短缺包类型信息;其次,假如参数 MSG_TRUNC 通报给读包函数 recvmsg()、recv()、recvfrom() 等,则函数返回的数据包长度是实际读到的包数据长度,而不是数据包真正的长度。Libpcap 的开拓者在源代码中明确建议不应用 2.0 版本进行捕获。

函数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 int

live_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 int

live_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);

}

}

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)

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])。

数据包过滤机制

大年夜量的收集监控法度榜样目的不合,期望的数据包类型也不合,但绝大年夜多半环境都都只必要所稀有据包的一(小)部分。例如:对邮件系统进行监控可能只必要端口号为 25(smtp)和 110(pop3) 的 TCP 数据包,对 DNS 系统进行监控就只必要端口号为 53 的 UDP数据包。包过滤机制的引入便是为了办理上述问题,用户法度榜样只需简单的设置一系列过滤前提,终极便能得到满意前提的数据包。包过滤操作可以在用户空间履行,也可以在内核空间履行,但必须留意到数据包从内核空间拷贝到用户空间的开销很大年夜,以是假如能在内核空间进行过滤,会极大年夜的前进捕获的效率。内核过滤的上风在低速收集下体现不显着,但在高速收集下是异常凸起的。在理论钻研和实际利用中,包捕获和包过滤从语意上并没有严格的区分,关键在于熟识到捕获数据包一定有过滤操作。基础上可以觉得,包过滤机制在包捕获机制中占中间职位地方。

包过滤机制实际上是针对数据包的布尔值操作函数,假如函数终极返回true,则经由过程过滤,反之则被丢弃。形式上包过滤由一个或多个谓词判断的并操作(AND)和或操作(OR)构成,每一个谓词判断基础上对应了数据包的协议类型或某个特定值,例如:只必要 TCP 类型且端口为110的数据包或ARP类型的数据包。包过滤机制在详细的实现上与数据包的协议类型并无若干关系,它只是把数据包简单确当作一个字节数组,而谓词判断会根据详细的协议映射到数组特定位置的值。如判断ARP类型数据包,只必要判断数组中第 13、14 个字节(以太头中的数据包类型)是否为0X0806。从理论钻研的意思上看,包过滤机制是一个数学问题,或者说是一个算法问题,此中间义务是若何应用起码的判断操作、起码的光阴完成过滤处置惩罚,前进过滤效率。

您可能还会对下面的文章感兴趣: