检查网络设备
libpcap 程序的第一步通常是在系统中找到合适的网络接口设备。网络接口在Linux 网络体系中是一个很重要的概念,它是对具体网络硬件设备的一个抽象,在它的下面是具体的网卡驱动程序,而其上则是网络协议层。Linux 中最常见的接口设备名 eth0 和 lo。Lo 称为回路设备,是一种逻辑意义上的设备,其主要目的是为了调试网络程序之间的通讯功能。eth0 对应了实际的物理网卡,在真实网络环境下,数据包的发送和接收都要通过 eht0。如果计算机有多个网卡,则还可以有更多的网络接口,如 eth1,eth2 等等。调用命令 ifconfig 可以列出当前所有活跃的接口及相关信息,注意对 eth0 的描述中既有物理网卡的 MAC 地址,也有网络协议的 IP 地址。查看文件 /proc/net/dev 也可获得接口信息。
Libpcap 中检查网络设备中主要使用到的函数关系如下图:
libpcap 调用 pcap_lookupdev() 函数获得可用网络接口的设备名。首先利用函数 getifaddrs() 获得所有网络接口的地址,以及对应的网络掩码、广播地址、目标地址等相关信息,再利用 add_addr_to_iflist()、add_or_find_if()、get_instance() 把网络接口的信息增加到结构链表 pcap_if 中,最后从链表中提取第一个接口作为捕获设备。其中 get_instanced() 的功能是从设备名开始,找第一个是数字的字符,做为接口的实例号。网络接口的设备号越小,则排在链表的越前面,因此,通常函数最后返回的设备名为 eth0。虽然 libpcap 可以工作在回路接口上,但显然 libpcap 开发者认为捕获本机进程之间的数据包没有多大意义。在检查网络设备操作中,主要用到的数据结构和代码如下:
-
/* 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 版本进行捕获。
相对 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_t。pcap_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_stat stat;
-
int use_bpf; /* 如果为1,则代表使用内核过滤*/
-
u_long TotPkts;
-
u_long TotAccepted; /* 被接收数据包数目 */
-
u_long TotDrops; /* 被丢弃数据包数目 */
-
long TotMissed; /* 在过滤进行时被接口丢弃的数据包数目 */
-
long OrigMissed; /*在过滤进行前被接口丢弃的数据包数目*/
-
-
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;
-
-
};
函数 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);
-
}
-
}
-
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);
-
}
-
struct packet_mreq
-
{
-
int mr_ifindex; /* 接口索引号 */
-
unsigned short mr_type; /* 要执行的操作(号) */
-
unsigned short mr_alen; /* 地址长度 */
-
unsigned char mr_address[ 8]; /* 物理层地址 */
-
};