PF_PACKET 设备层编程接口

来源:互联网 发布:linux如何查看path 编辑:程序博客网 时间:2024/05/19 23:24

一、描述

解释说明:
#include <sys/socket.h>
#include <features.h> /* 需要里面的 glibc 版本号 */
#if __GLIBC__ >= 2 && __GLIBC_MINOR >= 1
#include <netpacket/packet.h>
#include <net/ethernet.h> /* 链路层(L2)协议 */
#else
#include <asm/types.h>
#include <linux/if_packet.h>
#include <linux/if_ether.h> /* 链路层协议 */
#endif

packet_socket=socket(PF_PACKET,intsocket_type,intprotocol);

DESCRIPTION 描述
    分组套接口(也译为插口或套接字)被用于在设备层(OSI 的链路层) 收发原始(raw )分组。它允许用户在用户空间实现在物理层之上的协议模块。对于包含链路层报头的原始分组,socket_type 参数是 SOCK_RAW;对于去除了链路层报头的加工过的分组,socket_type 参数是 SOCK_DGRAM。链路层报头信息可在作为一般格式的 sockaddr_ll 中的中得到。socket 的 protocol 参数指的是 IEEE 802.3 的按网络层排序的协议号,在头文件中有所有被允许的协议的列表。当 protocol 被设置为 htons(ETH_P_ALL)时,可以接收所有的协议。到来的此种类型的分组在传送到在内核实现的协议之前要先传送给分组套接口。

    只有有效 uid 是 0 或有 CAP_NET_RAW 能力的进程可以打开分组套接口。

    传送到设备和从设备传送来的 SOCK_RAW 分组不改变任何分组数据。当收到一个 SOCK_RAW 分组时, 地址仍被分析并传送到一个标准的 sockaddr_ll 地址结构中。当发送一个 SOCK_RAW 分组时, 用户供给的缓冲区应该包含物理层报头。接着此分组不加修改的放入目的地址定义的接口的网络驱动程序的队列中。一些设备驱动程序总是增加其他报头。 SOCK_RAW 分组与已被废弃的 Linux 2.0 的 SOCK_PACKET 分组类似但不兼容。

    对 SOCK_DGRAM 分组的操作要稍微高一层次。在分组被传送到用户之前物理报头已被去除。从 SOCK_DGRAM分组套接口送出的分组在被放入网络驱动程序的队列之前,基于在 sockaddr_ll 中的目的地址得到一个适合的物理层报头。

    缺省的所有特定协议类型的分组被发送到分组套接口。为了只从特定的接口得到分组,使用bind(2)来指定一个在 sockaddr_ll 结构中的地址,以此把一个分组套接口绑定到一个接口上。只有地址字段 sll_protocol 和 sll_ifindex 被绑定用途所使用。

不支持在分组套接口上的 connect(2) 操作。(不能作为客户端使用)

ADDRESS TYPES 地址类型
sockaddr_ll 是设备无关的物理层地址。

struct sockaddr_ll
{
unsigned short sll_family; /* 总是 AF_PACKET */
unsigned short sll_protocol; /* 物理层的协议 */
int sll_ifindex; /* 接口号 */
unsigned short sll_hatype; /* 报头类型 */
unsigned char sll_pkttype; /* 分组类型 */
unsigned char sll_halen; /* 地址长度 */
unsigned char sll_addr[8]; /* 物理层地址 */
};


sll_protocol 是在 linux/if_ether.h 头文件中定义的按网络层排序的标准的以太桢协议类型。sll_ifindex 是接口的索引号(参见 netdevice(2));0 匹配所有的接口(当然只有合法的才用于绑定)。 sll_hatype 是在 linux/if_arp.h 中定义的 ARP 硬件地址类型。 sll_pkttype 包含分组类型。有效的分组类型是:目标地址是本地主机的分组用的 PACKET_HOST,物理层广播分组用的 PACKET_BROADCAST ,发送到一个物理层多路广播地址的分组用的 PACKET_MULTICAST,在混杂(promiscuous)模式下的设备驱动器发向其他主机的分组用的 PACKET_OTHERHOST,本源于本地主机的分组被环回到分组套接口用的 PACKET_OUTGOING。这些类型只对接收到的分组有意义。sll_addr 和 sll_halen 包括物理层(例如 IEEE 802.3)地址和地址长度。精确的解释依赖于设备。

译注: (1) 对于以太网(ethernet) OSI 模型不完全适用,以太桢定义包括物理层和链路层的基本内容, 所谓的以太桢协议类型标识的是网络层的协议。IEEE 802 委员会为与 OSI 相一致,把以太桢定义称为 MAC(medium access control)层,在 MAC 层与网络层之间加入 LLC (logical link control)层,补充上了 OSI 标准的链路层。但在BSD TCP/IP 中是为了兼容官方标准才被实现的。对于 TCP/IP 协议族 OSI 模型也不完全适用,TCP/IP 没定义链路层,只能用 UNIX 的设备驱动程序去对应链路层。无论如何这是既成事实,在本手册页中物理层、链路层、设备层指的都是以太网的 MAC 层。余以为不必严格按层次划分去理解问题,现在这个协议栈是优胜劣汰的结果,不是委员会讨论出来的。 (2) 以太网地址分为三类,物理地址(最高位为0),多路广播地址 (最高位为1),广播地址(全是1)。以 DP8390 为例,它的接收配置寄存器的 D2 位用来指定 NIC 是否接受广播桢,D3 位用来指定 NIC 是否对多路广播桢进行过滤,D4 位用来指定 NIC是否接受所有的物理地址桢。混杂(Promiscuous)模式就是接收所有物理地址桢。

SOCKET OPTIONS 套接口选项
分组套接口可被用来配置物理层的多路广播和混杂模式。配置通过调用 setsockopt(2)实现,套接口参数是一个分组套接口、层次参数为 SOL_PACKET 、选项参数中的 PACKET_ADD_MEMBERSHIP 用于增加一个绑定,选项参数中的 PACKET_DROP_MEMBERSHIP 用于删除一个绑定。两个选项都需要作为参数的 packet_mreq 结构:


struct packet_mreq
{
int mr_ifindex; /* 接口索引号 */
unsigned short mr_type; /* 动作 */
unsigned short mr_alen; /* 地址长度 */
unsigned char mr_address[8]; /* 物理层地址 */
};


mr_ifindex 包括接口的接口索引号,mr_ifindex 的状态是可以改变的。mr_type 参数指定完成那个动作。PACKET_MR_PROMISC 允许接收在共享介质上的所有分组,这种接受状态常被称为混杂模式; PACKET_MR_MULTICAST 把套接口绑定到由mr_address 和 mr_alen 指定的物理层多路广播组上;PACKET_MR_ALLMULTI 设置套接口接收所有的来到接口的多路广播分组。
除此之外传统的 ioctls 如 SIOCSIFFLAGS, SIOCADDMULTI, SIOCDELMULTI 也能用于实现同样的目的。

IOCTLS 输入输出控制
SIOCGSTAMP 用来接收最新收到的分组的时间戳。它的参数是 timeval 结构。
除此之外,所有的在 netdevice(7) 和 socket(7) 中定义的标准的 ioctl 在分组套接口上均有效。

ERROR HANDLING 错误处理
分组套接只对传送分组到设备驱动程序时发生的错误做错误处理,其他不做错误处理。这里没有等待解决的错误的概念。

COMPATIBILITY 兼容性
在 Linux 2.0 中,得到分组套接口的唯一方法是调用 socket(PF_INET, SOCK_PACKET, protocol)。它仍被支持但变得没有价值。两种方法的主要不同在于 SOCK_PACKET 使用老的 sockaddr_pkt 结构来指定一个接口,没有提供物理层接口无关性。 (依赖于物理设备)

struct sockaddr_pkt
{
unsigned short spkt_family;
unsigned char spkt_device[14];
unsigned short spkt_protocol;
};

spkt_family 包括设备类型,spkt_protocol 是在中定义的 IEEE 802.3 协议类型,spkt_device 是表示设备名的 null 终结的字符串,例如 eth0。译注: "who is nntp" 就是一个以 null (' ')终结的字符串。
这个结构已经被废弃,不应在新的代码中使用。


NOTES 注意
不建议对要求可移植的程序通过 pcap(3) 使用 PF_PACKET 协议族;它只覆盖了 PF_PACKET 特征的一个子集。
译注:该函数库可在 ftp://ftp.ee.lbl.gov/libpcap.tar.Z 得到。

SOCK_DGRAM 分组套接口对 IEEE 802.3 桢不做生成或分析 IEEE 802.2 LLC 报头的尝试。当在套接口中指定了 ETH_P_802_3 协议,告知内核生成 802.3 桢,并填写了长度字段;用户必须提供提供 LLC 报头来产生符合标准的分组。到来的 802.3 分组不在协议字段 DSAP/SSAP 上实现多路复用;而是故意的把 ETH_P_802_2 协议的 LLC 报头提供给用户。所以不可能绑定到 ETH_P_802_3;而可以绑定到 ETH_P_802_2 并自己做多路复用。缺省的发送的是标准的以太网 DIX 封装并填写协议字段。

译注: 长度字段和协议字段其实都是以太桢的第四字段,这个字段的值在小于 1518 时表示此以太桢是 IEEE 802.3 桢,在大于1536 时表示此以太桢是 DIX 桢。DIX 中的 D 代表 DEC,I 代表 Intel, X 代表 Xerox。
分组套接口不是输入或输出防火墙的系列主题。

ERRORS 错误信息
ENETDOWN
接口未启动。

ENOTCONN
未传递接口地址。

ENODEV
在接口地址中指定了未知的设备名或接口索引。

EMSGSIZE
分组比接口的 MTU(最大传输单元)大。

ENOBUFS
没有足够的内存分配给分组。

EFAULT
用户传递了无效的地址。

EINVAL
无效参数。

ENXIO
接口地址包含非法接口索引号。

EPERM
用户没有足够的权限来执行这个操作。

EADDRNOTAVAIL
传递了未知的多路广播组地址。

ENOENT
未收到分组。
除此之外,底层的驱动程序可能产生其他的错误信息。

VERSIONS 版本
PF_PACKET 是 Linux 2.2 的新特征。Linux 的早期版本只支持 SOCK_PACKET。


二、代码示例

先 放一段示例程序,这段程序我在RH9和AS4下编译通过,程序功能就是用从2.2内核加入的PF_PACKET协议族来进行底层数据包捕获并显示。 PF_PACKET协议族是与系统TCP/IP协议栈并行的同级别模块,即从PF_PACKET协议族得到的数据包是没有经过系统TCP/IP协议栈处理 的。而且,通过设置混杂模式,可以很容易的实现sniffer。

#include<errno.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/socket.h>
#include<fcntl.h>
#include<netpacket/packet.h>
#include<net/if.h>
#include<net/if_arp.h>
#include<netinet/in.h>
#include<netinet/ip.h>
#include<linux/if_ether.h>
#include<arpa/inet.h>
#include<sys/ioctl.h>
#include<unistd.h>

#define RED      "\E[31m\E[1m"
#define GREEN    "\E[32m\E[1m"
#define YELLOW   "\E[33m\E[1m"
#define BLUE     "\E[34m\E[1m"
#define NORMAL   "\E[m"

int Get_IfaceIndex(int fd, const char* interfaceName)
{
struct ifreq ifr;
if (interfaceName == NULL)
{
   return -1;
}
memset(&ifr, 0, sizeof(ifr));
strcpy(ifr.ifr_name, interfaceName);
if (ioctl(fd, SIOCGIFINDEX, &ifr) == -1)
{
   printf("RED ioctl error\n");
   return -1;
}
return ifr.ifr_ifindex;
}

int set_Iface_promisc(int fd, int dev_id)
{
struct packet_mreq mr;
memset(&mr,0,sizeof(mr));
mr.mr_ifindex = dev_id;
mr.mr_type = PACKET_MR_PROMISC;

if(setsockopt(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP,&mr,sizeof(mr))==-1)
{
   fprintf(stderr,"GREEN set promisc failed! \n");
   return -1;
}
return 0;
}

void usage(char *exename)
{
fprintf(stderr,RED"%s <interface> <packets num to capture>\n"NORMAL, exename);
}

int main(int argc, char **argv)
{
int listen_fd;
int ipak=0,maxk=0;
char buffer[8192];
int frmlen;
struct sockaddr_ll sll;
struct iphdr *ip;

if(argc <3)
{
   usage(argv[0]);
   return -1;
}

listen_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
sll.sll_family = AF_PACKET;
sll.sll_ifindex = Get_IfaceIndex(listen_fd,argv[1]);
sll.sll_protocol = htons(ETH_P_ALL);

if(bind(listen_fd,(struct sockaddr *)(&sll),sizeof(sll))==-1)
{
   fprintf(stderr,YELLOW"bind error:%s !\n"NORMAL,strerror(errno));

   goto FAIL;
}

if(set_Iface_promisc(listen_fd,sll.sll_ifindex) == -1)
{
   fprintf(stderr,"BLUE set promisc failed !\n");
   goto FAIL;
}

maxk = atoi(argv[2]);

while(!maxk || ipak < maxk)
{
   frmlen = recv(listen_fd,buffer,8192,MSG_TRUNC); //0->flags (MSG_PEEK,MSG_OOB,MSG_WAITALL,MSG_TRUNC)
   ip = (struct iphdr *)(buffer + sizeof(struct ethhdr));
   printf("NORMAL IP Packet from: %s\n",inet_ntoa(*(struct in_addr*)&ip->saddr));
   printf("NORMAL --IP Packet To: %s\n",inet_ntoa(*(struct in_addr*)&ip->daddr));
   printf("NORMAL ---IP Protocol: %d\n",(ip->protocol));
   printf("NORMAL -Buffer Length: %d bytes\n",frmlen);
   printf("NORMAL buffer Content: %s\n\n",buffer);
   ipak++;
}

return 0;
FAIL:
close(listen_fd);
return -1;
}

BUGS 缺陷
glibc 2.1 没有定义 SOL_PACKET。建议的补救是使用
#ifndef SOL_PACKET
#define SOL_PACKET 263
#endif

在此以后的 glibc 版本中更正了错误并且在 libc5 系统上不会发生。
没有对 IEEE 802.2/803.3 LLC 的处理被认为是缺陷。

三、附加

在这篇文章中,我们将提及以太网,着重讲局域网技术。我们后面再来解释,开始之前,我们假设源主机和目的主机在同一个局域网中。

首先,我们来简单回顾一下一个普通以太网卡是怎样工作的。对于那些对这方面很熟悉的人就可以跳过这一段了。用户程序产生的IP数据包被封装到以太网帧(这是通过以太网分组传递的数据包的名字),以太网帧只是一个更大的底层包而已,它包含了IP数据包和一些必须的使它能到达目的主机的信息(如图1)。需要特别注意的是,目的IP地址已经被ARP装换成了目的以太网地址(通常叫MAC地址)。这样,数据帧就带着IP包从源主机通过电缆传输到了目的主机。期间可能会经过集线器和交换机这些网络设备,但我们已经假设了不会有局域网的交叉,路由器或者网关会使实验复杂化。


(图1)

在以太网层不会有路由处理过程。换句话说,源主机发送的帧不会只是直接被发送到目的主机;而会被拷贝到局域网中的所有电缆传输,所有的网卡都会接收到它(如图2)。接收到后,每个网卡开始读取帧的头6个字节(包含了上面提到的目的MAC地址),但是只有一个网卡会识别它并提取这个帧。然后,网络驱动器会分离这个帧并将IP包通过协议栈传送到接收应用程序。


                        图2

更准确的说,网络驱动器会查看以太网帧头部中的协议类型(见图1),然后根据这个值来传送数据包到对应的协议接收功能模块。大多数时候包都是IP包,接收功能模块会去掉IP头部并且将有效负荷传递给TCP或UDP接收模块。这些协议会被传递给套接字处理函数,并通过这些函数最终将数据传输给用户。在整个传输过程中,数据包丢弃了所有的相关网络信息,如源地址(IP和MAC)和端口、IP选项、TCP参数等等。此外,如果目的主机没有打开相应的套接字或者参数不正确,数据包将被丢弃而不会传递到应用层。

因此,我们将解决两个问题来实现嗅探网络数据包。一个是关于以太网地址---我们不能读取不是指定到我们主机的包;另一个问题是关于协议栈的处理---为了使数据包不被丢弃,我们应该为各个端口建立一个监听套接口。此外,部分包信息会在协议栈处理过程中被丢弃。

第一问题不是必须的,因为我们可能不会对发送给其他主机的数据包感兴趣而只想嗅探发送到本主机的包。第二个问题是应该解决的,我们将分别讨论怎样解决这些问题,下面开始。

PF_PACKET协议

当你使用标准调用sock = socket(domain, type, protocol)打开一个套接口时,必须指定你将使用哪个域(或者协议族)。常用的协议族有PF_UNIX,这将表示在本地主机通信,还有PF_INET,表示基于IPv4的通信。此外,你必须为套接口指定一个类型,类型取决于你所指定的协议族。对于PF_INET协议族,套接字类型通常有SOCK_STREAM(具有代表性的是TCP)和SOCK_DGRAM(具有代表性的是UDP)。套接字类型将影响到数据包在传递到应用层之前内核的处理方式。最后,指定你需要套接字处理的协议(更多信息可以在socket(3)man page找到)。

在较新的内核版本(2.0之后)中,一个叫PF_PACKET的新协议族被引入。这个协议族允许应用程序直接从网络驱动器发送和接收数据包,这样就避开了协议栈操作(如IP/TCP或者IP/UDP操作)。也就是说,任何使用套接字发送出去的数据包直接通过以太网接口发出,任何以太网接口接收到的包直接传送给应用程序。

PF_PACKET协议族支持两种不同的套接字类型,SOCK_DGRAM和SOCK_RAW。前者将去掉以太网头部再传输。后者给予应用程序完整的数据包。Socket()函数的协议字段必须于某个以太网ID匹配(定义在/usr/include/linux/if_ehter.h中),这样表示指定的协议的包能在以太网帧中传送。如果不指定特定的协议,典型的是用ETH_P_IP,这样将包含所有使用IP协议的数据包(如TCP、UDP、ICMP、原始IP等等)。

因为这样可能会有很严重的安全方面的问题(比如,你伪造一个欺骗的MAC地址),所以PF_PACKET协议族的套接字必须用管理员身份创建。

使用PF_PACKET很容易的解决了我们抓到包的协议栈处理问题。让我们看看它是怎么做到的,例子在列表1中。我们打开一个PF_PACKET协议族的套接字,指定套接字类型为SOCK_RAW,协议为IP。然后我们从套接口读取数据包,经过一些正确性检测后,我们输出一些以太网头部和IP头部信息。对照图1检查输出的地址,你将发现应用程序访问网络数据是多么的简单。

Listing 1. Protocol Stack-Handling Sniffed Packets 

假设你的机器连接到局域网中,你可以运行下我们简短的例子来查看从其他主机发送到你的主机的数据包(你可以ping或者telnet自己的主机)。你将看到所有包都是指向你的,而不会有指向其他主机的包出现。

混杂模式和非混杂模式

PF_PACKET协议族允许应用程序接收传递给自己主机的数据包,但不能接收到发送给其他主机的包。正如我们前面看到的,这是因为网卡丢弃了不属于自己MAC地址的所有帧---这叫非混杂模式,也就是每个网卡只关心发送到它自己的帧。这里有三个例外:一个目的MAC地址为广播地址(FF:FF:FF:FF:FF:FF)的帧会被所有网卡接收;一个目的MAC地址为多播地址的帧会被所在多播组的并且已设置为混杂模式的网卡接收。

最后一种情况,也是我们最有兴趣的一个。设置网卡为混杂模式,我们需要做的是对已打开的套接字使用ioctl()的函数,因为这是一个潜在的安全威胁,所以这个操作仅能被管理员使用。假定sock是一个已经打开的套接字,下面代码将做到:

strncpy(ethreq.ifr_name,"eth0",IFNAMSIZ);
ioctl(sock, SIOCGIFFLAGS, &ethreq);
ethreq.ifr_flags |= IFF_PROMISC;
ioctl(sock, SIOCSIFFLAGS, &ethreq);

(ethreq是一个ifreq结构体,定义在/us/include/net/if.h中)。第一个ioctl读取最近的以太网卡标志;将标志与IFF_PROMISC进行“或运算”,也就是指定将网卡设置为混杂模式,再在第二个ioctl将修改后的标志写到网卡中。

我们来看更多完整的例子(Listing 2.ftp://ftp.ssc.com/pub/lj/listings/issue86/ ) 。如果你使用管理员权限编译并运行这个例子,你将会看到所有通过电缆的数据包,即使那些包并不是发送给你的。这是因为你的网卡现在工作在混杂模式。你可以简单的检测到这点,使用ifconfig命令,并注意输出的第三行。

注意,如果你的局域网使用的是交换机而不是集线器,你将只能看到主机所在的分支的所有数据包。这是由于交换机的工作方式所致,而你毫无办法(除非通过MAC地址欺骗来骗过交换机,但这不是这篇文章所要讨论的)。要得到更多关于集线器和交换机的信息,请在本文的资源部分选择一本书来研究。

Linux 包过滤器

似乎我们所有的问题都解决了,但是依然有一个很重要的问题需要考虑:如果你确实想尝试我们的Listing 2,而你的局域网只是一个适中的网络流量(几台WINDOWS主机就可以因为NETBIOSB包而浪费足够的带宽(译者:这里应该是贬义吧?哈哈)),这样你将发现我们的sniffer会输出很多数据。随着网络流量的增加,sniffer因为电脑来不及处理这而开始丢包。

解决这个问题的办法是过滤接收到的数据包,仅输出你感兴趣的信息。一个办法是在源程序中加入"if语句";这会使你的输出看起来很干净,但是这样做并不能对性能有多大提升。内核依然会抓取网络上所有的包,这样会浪费处理时间,并且sniffer依然会去检测每个数据包头部以确定那些包该输出哪些不输出。

解决这个问题最好的办法是将过滤器尽可能放在这条数据包处理链的前端(从网络驱动器一直到应用层,如图3)。Linux内核允许我们使用一个叫LPF的过滤器,已经直接内置到PF_PACKET协议处理机制中,它在网卡接收到中断后立即执行。过滤器决定哪些包应该传递给应用程序,哪些该丢弃。


        图3

为了使之尽可能的灵活,而且不限制程序员的预先条件设定,过滤器引擎事实上是以状态机的形式来运行用户定义的程序的。这些程序是用一种叫做BPF(for Berkeley packet fileter)的伪机器代码写的,灵感来源于由SteveMcCanne和Van Jacobson写的“老文献”(译者:我这里刚好有这个文献,PDF版,是1992年写的,需要的可以跟我联系)。BPF看起来像是汇编语言,用一些寄存器来存储指令和值,执行算法操作和条件转换等。

过滤代码对每个包进行检测,BPF处理器的存储空间仅用于存放数据的字节(译者:这里问题大)。过滤器的结果是一个整数n,这个整数表示告知socket将数据包(如果有的话)的n字节将传输给应用层。这样还有另一个优点,因为你通常只对数据包的部分字节感兴趣,你可以通过避免拷贝大量数据而节约处理时间。

(Not)设计过滤器

尽管BPF语言很简单很容易学,但是大多数人可能认为用人类可读形式来写过滤语句用起来更舒适。所以,我们将不写关于BPF的细节和用法(你可以在上面提到的文献中找到),我们来讨论如何通过自然语言获得可用的过滤语言。

首先,我们需要安装tcpdump,(从LBL获得)。当你读到这篇文章的时候,可能你已经知道并会使用tcpdump了。它的第一个版本是由那些写BPF的人写的,也是BPF的第一个应用版。实际上,tcpdump用BPF,是以在libpcap库中运用的形式来捕获并过滤数据包。这个库是独立于系统的BPF引擎。当在Linux机器上使用时,BPF就依附于Linux packet filter来使用。

一个libpcap提供的很有用的函数是pcap_compile(),它将一段自然语言作为输入并转换成BPF过来代码输出。Tcpdump用这个函数来翻译用户在命令行输入的表达式为BPF代码。有趣的是tcpdump有一个开关,选项-d,来输出BPF代码。

举个例子,输入tcpdump host 192.168.9.10 开始嗅探,并只输出源/目的IP地址为192.168.9.10的数据包。输入tcpdump -d host 192.168.9.10将输出这条过滤语句对应的BPF代码,如下Listing 3。

Listing 3. Tcpdump -d Results

echer:~# tcpdump -d host 192.168.9.10

(000) ldh      [12]

(001) jeq      #0x800           jt 2    jf 6

(002) ld       [26]

(003) jeq      #0xc0a8090a      jt 12   jf 4

(004) ld       [30]

(005) jeq      #0xc0a8090a      jt 12   jf 13

(006) jeq      #0x806           jt 8    jf 7

(007) jeq      #0x8035          jt 8    jf 13

(008) ld       [28]

(009) jeq      #0xc0a8090a      jt 12   jf 10

(010) ld       [38]

(011) jeq      #0xc0a8090a      jt 12   jf 13

(012) ret      #68

(013) ret      #0

让我们来简单讲解下这些代码,0-1行和6-7行确定抓到的真是IP、ARP或者RARP协议包,通过比较他们的协议ID(定义在/usr/include/linux/if_ether.h中),ID值在帧头部偏移位第12位开始(见图1)。如果测试失败,此包将被丢弃(第13行)。

第2-5行和8-11行将源/目的IP地址和192.168.9.10进行比较。注意,对于不同的协议,地址的偏移位置是不同的;如果是IP协议,偏移位为第26字节和30字节处,否则,在第28和38字节处(ARP和RARP)。如果其中一个地址匹配,则该包被接收,并且数据包的前68字节被传送给应用程序(第12行)。

过滤代码也不总是最优化的,因为它是为一般的BPF机器而产生的并没有调整为指定的结构来运行过滤引擎。LPF中有一些特殊情况,过滤器运行在PF_PACKET的常规处理中,这可能已经检查了以太网协议。这依赖于你在socket()协议字段所指定的协议,如果不是ETH_P_ALL(这样意味着所有的以太网帧都会被抓取),那么只有指定以太网协议的数据帧会被送到过滤器。举例说明,如指定为ETH_P_IP的情况,我们可以重写上面的代码以使得它更快而且更严密,如下:

(000) ld       [26]
(001) jeq      #0xc0a8090a      jt 4    jf 2
(002) ld       [30]
(003) jeq      #0xc0a8090a      jt 4    jf 5
(004) ret      #68
(005) ret      #0

安装过滤器

安装LPF是很简单的:你需要添加一个sock_filter结构体,此结构体包含过滤代码并与打开的套接字关联。

过滤器结构体很容易的通过使用tcpdump -dd选项来获得。过滤代码将以C语言数组的形式输出,这样你就可以拷贝粘贴到代码中了,见Listing 4。然后,使用setsockopt()调用就可以简单的将过滤器与套接字关联了。

Listing 4. Tcpdump with --dd Switch

一个完整的例子

我们将通过一个完整的例子来得出这篇文章的结论(Listing 5:ftp://ftp.ssc.com/pub/lj/listings/issue86/)。这其实和前面两个例子很像,加入了LSF代码和setsockopt()调用。过滤器已经被配置为只接收UDP数据包、源/目的IP地址为192.168.9.10、源端口号为5000。

为了测试这个例子,你需要一个简单的能产生UDP数据包的方法(如sendip或者apsend,从此获得:http://freshmeat.net/)。你可能希望改变IP地址来与你所在局域网匹配。要做到这点,只需要修改过滤代码中的0xc0a8090a 为你指定IP的十六进制就行了。

最后我们要注意当我们退出程序时网卡的状态。因为我们没有重置以太网标志,所以网卡会一直保持混杂模式。解决这个问题,你只需要在退出程序之前使用Control+C(SIGINT)信号来重置以太网标志为先前的值(这样你讲回到与IFF_PROMISC按位或之前的状态)。


0 0