在Linux实现抓取以太网络上的数据包主要有libpcap,raw socket以及从内核中获取的方式。
我尝试选择使用raw socket来抓取以太网络上的数据包。
##socket地址域(协议簇)##
在Linux的manual中,有两个篇包含socket相关的说明,分别是socket(2)和socket(7),这两篇中的socket定义不完全相同:
socket(2): int socket(int domain, int type, int protocol);
socket(7): mysocket = socket(int socket_family, int socket_type, int protocol);
主要的区别在第一个参数,一个是域(domain),其参数的定义皆为AF_XXX;一个是簇(family),其参数定义皆为PF_XXX。AF与PF的区别一个是地址域,一个是协议簇,从定义上来说这两者是有细微区别,但是从Linux中的定义可以看到这两者是基本上无需区分的(sys/socket.h)。
在较新的socket(2)手册中对域的解释是通信域,在此理解为socket所使用的通信协议吧。
在以太网络的socket通信中,使用了两类协议,AF_INET和AF_PACKET。
AF_INET指代IPv4协议,使用该协议的socket与内核进行网络层的通信,将网络层的数据包发往内核,可以由用户决定IP首部由用户还是socket进行创建。
AF_PACKET指代(Low level packet interface),这是socket(7)中关于AF_PACKET解释的原话,在以太网中自然指代的是以太链路层。
可以理解为使用AF_INET即是对IP层以上的数据进行处理,使用AF_PACKET即是对链路层以上的数据进行处理。
socket的第二个参数是socket_type,其表示在socket所要处理的通信域上使用何种协议进行通信。例如,使用TCP或UDP协议通信即是在AF_INET域上使用SOCK_STREAM或SOCK_DGRAM进行流式或报文式的通信。SOCK_RAW即是与这些通信方式平等的一种,但是操作系统并不帮助用户对数据报进行管理,用户需要自主组建上层或当前层网络协议首部,这就给了用户发送报文更大的自由。
socket的第三个参数在定义里为protocol(协议),在创建TCP或UDP通信socket的过程中,通常将该参数置为0,其含义表示由socket自己选择合适的协议。实际上当使用SOCK_STREAM时socket自动选择了IPPRPTO_TCP,使用SOCK_DGRAM时选择了IPPROTO_UDP。由IPPROTO_XXX可以看出,实际上这是表示IP以上所使用的协议是什么的协议标识,当编写链路层的SOCK_RAW套接字是,我们也需要选择所使用的网络协议标识是ETH_P_IP、ETH_P_ARP、ETH_P_ALL或是其他,前三者即分别表示链路层以上使用IP、ARP或者是全部协议都可以。
AF_INET域的SOCK_RAW在 RAW(7) 和 IP(7) 中有较为详细的描述
AF_PACKET域的SOCK_RAW在 PACKET(7) 中有较为详细的描述
创建网络层和以太网层的原始套接字语句即为:
socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
和
socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
AF_INET域SOCK_RAW的默认协议当为IPPROTO_RAW,该socket类型仅用于发送数据包(是否包含IP首部在本文后续进行说明),若需要接收网络层的IP数据包,则考虑使用AF_PACKET域的SOCK_RAW,并将socket的协议设定为ETH_P_IP(参见RAW(7))。
在使用AF_PACKET域的SOCK_RAW进行以太网嗅探时,ETH_P_ALL可替换为ETH_P_IP或其他定义在中的网络协议标识来获取特定协议的以太网数据包。
##设置socket——setsockopt##
可以使用 setsockopt 来对许多的socket选项进行设置,同时可以使用 getsockopt 来获取这些选项的值。 setsockopt的定义为:
setsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen)
setsockopt的第二个参数为level,这里我就理解为层,例如SOL_SOCKET表示设置socket层面的选项,而IPPROTO_IP表示设置IP层面的选项。第三个和第四个选项是设置的参数值以及值的长度。
对AF_INET域的SOCK_RAW需要的使用setsockopt进行的设置可能有:
setsockopt(socket_id, IPPROTO_IP, IP_HDRINCL, &flag, sizeof(flag));
该语句由用户定义IP首部,并认为发送的报文是从IP首部开始的。 (在上个语句中,flag=1,注意flag在这里需要是int型/四字节,设置/获取的选项不同,后两个参数也不同,详细参考 setsockopt |如果不设置IP_HDRINCL,则发送的报文内核认为是从IP首部之后开始的,内核会依据源和目的地址添加IP首部后再进行发送)
setsockopt(socket_id, SOL_SOCKET, SO_BROADCAST, &flag, sizeof(flag));
该语句设置socket可以发送广播报文,否则进行send/sendto时会回复目的地址错误。
setsockopt(iSockId, SOL_SOCKET, SO_BINDTODEVICE, device, IFNAMSIZ);
该语句设置socket所绑定的接口,如果不对特定的接口进行绑定,Linux内核会将所有抓取到的符合socket对应协议的数据包都发送给该socket(参见RAW(7))。使用该语句进行绑定是将socket与特定的接口进行绑定,而是用bind则是将socket与特定的网络地址进行绑定(猜想,如果一块网卡上有多个IP地址,那么使用setsockopt的SO_BINDTODEVICE进行绑定则会获取到该网卡上所有IP地址接收的数据包?)。
对于AF_PACKET域的SOCK_RAW无法使用setsockopt的SO_BINDTODEVICE进行网络接口的绑定。在SOCKET(7)中特别申明,该方法仅对某些类型的socket生效,AF_INET域的socket是其中之一,而AF_PACKET域不生效,可以使用bind进行接口绑定。
##设置网络接口及获取其信息##
在我使用AF_PACKET域socket的过程中,使用了ioctl来对网络接口进行信息获取及设置。
首先使用ioctl获取接口的index信息,以便AF_PACKET域的socket能够与网络接口进行绑定:
struct ifreq ifr_re; // ifreq结构用于获取接口信息
strncpy(ifr_re.ifr_name, device, IFNAMSIZ);
// 获取指定网卡接口的INDEX
if(ioctl(iSockId, SIOCGIFINDEX, (char*)&ifr_re))
{
return -1;
}
struct sockaddr_ll RawHWAddr;
memset(&RawHWAddr, 0, sizeof(RawHWAddr));
RawHWAddr.sll_ifindex = ifr_re.ifr_ifindex;
RawHWAddr.sll_family = AF_PACKET;
RawHWAddr.sll_protocol = htons(ETH_P_ALL);
RawHWAddr.sll_hatype = 0;
RawHWAddr.sll_pkttype = PACKET_HOST;
RawHWAddr.sll_halen = ETH_ALEN;
// 需要使用struct sockaddr_ll 结构绑定RawSocket与硬件地址
// 在 packet(7) 的man手册中有说明, For bind only sll_protocol and sll_ifindex are used
if(bind(iSockId, (struct sockaddr*)&RawHWAddr, sizeof(RawHWAddr)))
{
return -1;
}
再次使用ioctl对网络接口进行设置开启混杂模式,使得AF_PACKET域的socket能够不仅仅抓取到目的地址为与之绑定的网络接口的数据包,而能够获取到网卡加入的链路上的所有数据包:
struct ifreq ifr_re; // ifreq结构用于获取接口信息
strncpy(ifr_re.ifr_name, device, IFNAMSIZ);
if(ioctl(iSockId, SIOCGIFFLAGS, (char*)&ifr_re))
{
return -1;
}
ifr_re.ifr_flags |= IFF_PROMISC;
if(ioctl(iSockId, SIOCSIFFLAGS, (char*)&ifr_re))
{
return -1;
}
将AF_PACKET域的socket与接口绑定并开启混杂模式后,就能够在特定接口上抓取以太网数据包了。