vlambda博客
学习文章列表

使用原始套接字和 BPF 编写 Linux 数据包嗅探器

本文介绍如何在linux平台下编写一个不依赖其他库的网络数据包嗅探器。


介绍


首先需要回顾一下 tcpdump 是如何实现的。根据官方文档https://www.tcpdump.org/,tcpdump 是建立在 libpcap 库之上的,该库是在伯克利大学开发的,具体可以参考这篇论文。https://www.tcpdump.org/papers/bpf-usenix93.pdf
CS出身的同学都知道,不同的操作系统具有不同的网络堆栈内部实现。libpcap 涵盖了所有差异,并为用户级数据包捕获提供了与系统无关的接口。但是在这篇文章中,重点关注 Linux 平台,那么 libpcap 在 Linux 系统上是如何工作的呢?根据一些文档https://stackoverflow.com/questions/21200009/capturing-performance-with-pcap-vs-raw-socket,libpcap 使用 PF_PACKET 套接字来捕获网络接口上的数据包。
那么下一个问题是:PF_PACKET 套接字是什么?


PF_PACKET 套接字


套接字接口是 TCP/IP 的世界之窗。在大多数包含 TCP/IP 的现代系统中,套接字接口是应用程序可以使用 TCP/IP 协议套件的唯一方式。

PF_SOCKET

本文通过检查创建新套接字时执行的系统调用来更深入地了解套接字:

int socket(int domain, int type, int protocol);
当您想使用上述系统调用创建套接字时,必须指定要与该套接字一起使用的域(或协议族)作为第一个参数。最常用的族是 PF_INET,它是基于 IPv4 协议的通信(创建 TCP 服务器时,使用该族)。此外必须为套接字指定一个类型作为第二个参数。值取决于您指定的系列。例如,在处理 PF_INET 系列时,类型的值包括 SOCK_STREAM(用于 TCP)和 SOCK_DGRAM(用于 UDP)。有关套接字系统调用的其他详细信息,可以参考 socket(3) 手册页。
在手册页中可以找到 domain 参数的一个潜在值,如下所示:
AF_PACKET    Low-level packet interface
注意:AF_PACKET 和 PF_PACKET 相同。历史上叫PF_PACKET,后来改名为AF_PACKET。PF 表示协议族,AF 表示地址族。在本文使用 PF_PACKET。
与 PF_INET 套接字不同,它可以给你 TCP 段。通过 PF_PACKET 套接字,可以获得绕过 TCP/IP 堆栈的上层处理的原始以太网帧。也就是说,接收到的任何数据包都将直接传递给应用程序。
为了更好地理解 PF_PACKET 套接字,更深入地检查接收到的数据包从网络接口到应用程序级别的路径。
(如上图所示)当网络接口卡(NIC)接收到一个数据包时,它由驱动程序处理。驱动程序在内部维护一个称为环形缓冲区的结构。并通过直接内存访问(DMA)将数据包写入内核内存(内存预先分配有环形缓冲区)。数据包被放置在 sk_buff 的结构中(与内核网络子系统相关的最重要的结构之一)。
数据包进入内核空间后,经过层层协议栈处理,如IP处理、TCP/UDP处理等。数据包通过套接字接口进入应用程序。
但是对于 PF_PACKET 套接字,sk_buff 中的数据包被克隆,然后它跳过协议栈,直接进入应用程序。内核需要克隆操作,因为一份副本被 PF_PACKET 套接字消耗,另一份通过通常的协议栈。
下一步看看如何在代码级别创建一个 PF_PACKET 套接字。
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <errno.h>#include <unistd.h>#include <sys/socket.h>#include <sys/types.h>#include <sys/ioctl.h>#include <linux/if_ether.h>#include <linux/filter.h>#include <net/if.h>#include <arpa/inet.h>
char* transport_protocol(unsigned int code) { switch(code) { case 1: return "icmp"; case 2: return "igmp"; case 6: return "tcp"; case 17: return "udp"; default: return "unknown"; }}
int main(int argc, char **argv) { int sock, n; char buffer[2048]; unsigned char *iphead, *ethhead;
if ((sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP))) < 0) { perror("socket"); exit(1); } // bind to eth0 interface only const char *opt; opt = "eth0"; if (setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, opt, strlen(opt) + 1) < 0) { perror("setsockopt bind device"); close(sock); exit(1); } /* set the network card in promiscuos mode*/ // An ioctl() request has encoded in it whether the argument is an in parameter or out parameter // SIOCGIFFLAGS 0x8913 /* get flags */ // SIOCSIFFLAGS 0x8914 /* set flags */ struct ifreq ethreq; strncpy(ethreq.ifr_name, "eth0", IF_NAMESIZE); if (ioctl(sock, SIOCGIFFLAGS, &ethreq) == -1) { perror("ioctl"); close(sock); exit(1); } ethreq.ifr_flags |= IFF_PROMISC; if (ioctl(sock, SIOCSIFFLAGS, &ethreq) == -1) { perror("ioctl"); close(sock); exit(1); }
// attach the filter to the socket // the filter code is generated by running: tcpdump tcp struct sock_filter BPF_code[] = { { 0x28, 0, 0, 0x0000000c }, { 0x15, 0, 5, 0x000086dd }, { 0x30, 0, 0, 0x00000014 }, { 0x15, 6, 0, 0x00000006 }, { 0x15, 0, 6, 0x0000002c }, { 0x30, 0, 0, 0x00000036 }, { 0x15, 3, 4, 0x00000006 }, { 0x15, 0, 3, 0x00000800 }, { 0x30, 0, 0, 0x00000017 }, { 0x15, 0, 1, 0x00000006 }, { 0x6, 0, 0, 0x00040000 }, { 0x6, 0, 0, 0x00000000 } }; struct sock_fprog Filter; // error prone code, .len field should be consistent with the real length of the filter code array Filter.len = sizeof(BPF_code)/sizeof(BPF_code[0]); Filter.filter = BPF_code;

if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &Filter, sizeof(Filter)) < 0) { perror("setsockopt attach filter"); close(sock); exit(1); }
while(1) { printf("-----------\n"); n = recvfrom(sock, buffer, 2048, 0, NULL, NULL); printf("%d bytes read\n", n);
/* Check to see if the packet contains at least * complete Ethernet (14), IP (20) and TCP/UDP * (8) headers. */ if (n < 42) { perror("recvfrom():"); printf("Incomplete packet (errno is %d)\n", errno); close(sock); exit(0); }
ethhead = buffer; printf("Source MAC address: %.2x:%.2x:%.2x:%.2x:%.2x:%.2x\n", ethhead[0], ethhead[1], ethhead[2], ethhead[3], ethhead[4], ethhead[5] ); printf("Destination MAC address: %.2x:%.2x:%.2x:%.2x:%.2x:%.2x\n", ethhead[6], ethhead[7], ethhead[8], ethhead[9], ethhead[10], ethhead[11] );
iphead = buffer + 14;
if (*iphead==0x45) { /* Double check for IPv4 * and no options present */ printf("Source host %d.%d.%d.%d\n", iphead[12],iphead[13], iphead[14],iphead[15]); printf("Dest host %d.%d.%d.%d\n", iphead[16],iphead[17], iphead[18],iphead[19]); printf("Source,Dest ports %d,%d\n", (iphead[20]<<8)+iphead[21], (iphead[22]<<8)+iphead[23]); printf("Layer-4 protocol %s\n", transport_protocol(iphead[9])); } }}

上面代码片段命名为create_socket.c

请确保包含系统头文件:<sys/socket.h> <sys/types.h>


绑定到一个网络接口


如果没有其他设置,嗅探器会捕获在所有网络设备上接收到的所有数据包。下一步尝试将嗅探器绑定到特定的网络设备。
首先可以使用 ifconfig 命令列出机器上所有可用的网络接口。网络接口是网络硬件的软件接口。
例如,下图显示了网络接口 eth0 的信息
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 192.168.230.49 netmask 255.255.240.0 broadcast 192.168.239.255 inet6 fe80::215:5dff:fefb:e31f prefixlen 64 scopeid 0x20<link> ether 00:15:5d:fb:e3:1f txqueuelen 1000 (Ethernet) RX packets 260 bytes 87732 (87.7 KB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 178 bytes 29393 (29.3 KB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
将嗅探器绑定到 eth0,如下所示bind.c:
// bind to eth0 interface onlyconst char *opt;opt = "eth0";if (setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, opt, strlen(opt) + 1) < 0) { perror("setsockopt bind device"); close(sock); exit(1);}

通过调用 setsockopt 系统调用来完成。 

现在嗅探器只捕获在指定网卡上接收到的网络数据包。


非混杂和混杂模式


默认情况下,每个网卡都只管自己的事,只读取指向它的帧。这意味着网卡丢弃所有不包含自己MAC地址的数据包,称为非混杂模式。
接下来让嗅探器可以在混杂模式下工作。这样它能检索所有的数据包。甚至那些不是发给它的主机的。
要将网络接口设置为混杂模式,我们所要做的就是向该接口上的打开套接字发出 ioctl() 系统调用, promiscuous_mode.c。
/* set the network card in promiscuos mode*/// An ioctl() request has encoded in it whether the argument is an in parameter or out parameter// SIOCGIFFLAGS 0x8913 /* get flags */// SIOCSIFFLAGS 0x8914 /* set flags */struct ifreq ethreq;strncpy(ethreq.ifr_name, "eth0", IF_NAMESIZE);if (ioctl(sock, SIOCGIFFLAGS, &ethreq) == -1) { perror("ioctl"); close(sock); exit(1);}ethreq.ifr_flags |= IFF_PROMISC;if (ioctl(sock, SIOCSIFFLAGS, &ethreq) == -1) { perror("ioctl"); close(sock); exit(1);}

ioctl 代表 I/O 控制,它操纵特定文件的底层设备参数。ioctl 接受三个参数:

第一个参数必须是打开的文件描述符。在我们的例子中使用绑定到网络接口的套接字文件描述符。
第二个参数是设备相关的请求代码。你可以看到我们调用了 ioctl 两次。第一次调用使用请求代码 SIOCGIFFLAGS 来获取标志,第二次调用使用请求代码 SIOCSIFFLAGS 来设置标志。不要被这两个拼写相同的常量值所迷惑。
第三个参数用于向请求进程返回信息。
现在,嗅探器可以检索网卡上接收到的所有数据包,无论数据包发往哪个主机。


使用 BPF 进行数据包过滤


到目前为止,嗅探器捕获了网卡上接收到的所有网络数据包。但是像 tcpdump 这样强大的网络嗅探器应该提供包过滤功能。例如,嗅探器只能捕获 TCP 段(并跳过 UPD),或者它只能捕获来自特定源 IP 地址的数据包。接下来继续探索如何做到这一点。


BPF的背景


Berkeley Packet Filter (BPF) 是类 Unix 操作系统中数据包捕获的基本底层技术。网上搜索BPF作为关键词,结果很混乱。事实证明,BPF 不断发展,并且有几个相关的概念,例如 BPF cBPF eBPF 和 LSF。沿着时间线检查这些概念:
  • 1992 年,BPF 首次被引入 BSD Unix 系统,用于过滤不需要的网络数据包。BPF 的提议来自劳伦斯伯克利实验室的研究人员,他们还开发了 libpcap 和 tcpdump。

  • 1997年,Linux Socket Filter(LSF)基于BPF开发,并引入Linux内核版本2.1.75。注意,LSF 和 BPF 有一些明显的区别,但在 Linux 上下文中,谈到 BPF 或 LSF 时,我们指的是 Linux 内核中相同的包过滤机制。在接下来的部分中研究 BPF 的详细理论和设计。

  • 最初,BPF 被设计为网络数据包过滤器。但是在 2013 年,BPF 得到了广泛的扩展,它可以用于非网络用途,例如性能分析和故障排除。如今,扩展的 BPF 被称为 eBPF,而原始和过时的版本被重命名为经典 BPF(cBPF)。注意,在本文中研究的是 cBPF,而 eBPF 不在本文的讨论范围内。eBPF 是当今软件界最火的技术。


BPF 的放置位置


第一个要回答的问题是我们应该把过滤器放在哪里。
这个问题的最佳解决方案是尽早将过滤器放在路径中。由于将大量数据从内核空间复制到用户空间会产生巨大的开销,这会对系统性能产生很大影响。所以 BPF 是一个内核特性。当网络接口接收到数据包时,应该立即触发过滤器。正如原始 BPF 论文(https://www.tcpdump.org/papers/bpf-usenix93.pdf)所说,为了最大限度地减少内存流量,这是大多数现代系统的主要瓶颈,数据包应该被“就地”过滤,而不是在过滤之前复制到其他内核缓冲区。
通过检查以下内核源代码来验证此行为(注意本文中显示的内核代码基于 2.6 版,其中包含 cBPF 实现。):
/* source code file of net/packet/af_packet.c *//* packet_create: create socket */static int packet_create(struct net *net, struct socket *sock, int protocol){ /* some code omitted ... */ po = pkt_sk(sk); sk->sk_family = PF_PACKET; po->num = proto;
spin_lock_init(&po->bind_lock); po->prot_hook.func = packet_rcv; // attach hook function to socket
if (sock->type == SOCK_PACKET) po->prot_hook.func = packet_rcv_spkt; // attach hook function to socket
if (proto) { po->prot_hook.type = proto; dev_add_pack(&po->prot_hook); sock_hold(sk); po->running = 1; }}
packet_create 函数在应用程序调用套接字系统调用时处理套接字创建。在第 11 行和第 14 行,它将钩子函数附加到套接字。 hook函数在收到数据包时执行。
以下代码块显示了hook函数 packet_rcv:
/* hook function packet_rcv is triggered, when the packet is received */static int packet_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev){ /* some code omitted ... */ sk = pt->af_packet_priv; snaplen = skb->len; res = run_filter(skb, sk, snaplen); // filter logic if (!res) goto drop_n_restore; // drop the packet
__skb_queue_tail(&sk->sk_receive_queue, skb); // put the packet into the queue}
packet_rcv函数调用run_filter,这只是BPF的逻辑部分(目前你可以把它当成一个黑盒子,下一节会详细分析)。根据 run_filter 的返回值,可以将数据包过滤掉或放入队列。
到目前为止,可以理解 BPF(或包过滤)在内核空间内工作。但是数据包嗅探器是一个用户空间应用程序。下一个问题是如何将用户空间中的过滤规则链接到内核空间中的过滤处理程序。
要回答这个问题,必须了解 BPF 本身。

BPF机器


正如上面提到的,BPF 是在伯克利的研究人员撰写的这篇原始论文(上文有连接)中介绍的。强烈建议阅读这篇论文。


虚拟 CPU


数据包过滤器只是数据包上的布尔值函数。如果函数的值为真,内核为应用程序复制数据包;如果为假,则忽略该数据包。
为了尽可能灵活并且不将应用程序限制在一组预定义的条件下,BPF 实际上是作为基于寄存器的虚拟机实现的运行用户定义的程序。
PS:(对于基于堆栈和基于寄存器的虚拟机的区别,以后的文章会介绍)
您可以将 BPF 视为虚拟 CPU。它由一个累加器、一个索引寄存器 (x)、一个暂存存储器和一个隐式程序计数器组成。如果你不熟悉这些概念,我添加一些简单的说明如下:
  • 累加器是 CPU 中包含的一种寄存器。它充当临时存储位置,在数学和逻辑计算中保存中间值。例如,在“1+2+3”的运算中,累加器会保存值 1,然后是值 3,然后是值 6。累加器的好处是不需要显式引用。

在 BPF 机器中,累加器用于算术运算,而索引寄存器提供到数据包或暂存存储器区域的偏移量。


指令集和寻址方式


与物理 CPU 一样,BPF 提供了如下的一小组算术、逻辑和跳转指令,这些指令在 BPF 虚拟机(或 CPU)上运行:

使用原始套接字和 BPF 编写 Linux 数据包嗅探器

第一列 opcodes 列出了以汇编语言风格编写的 BPF 指令。例如,ld、ldh 和 ldb 表示将指示的值复制到累加器中。ldx 表示将指示的值复制到索引寄存器中。jeq 表示如果累加器等于指示值,则跳转到目标指令。ret 表示返回指定的值。您可以检查论文中详细设置的说明的功能。
这种类似汇编的风格更具可读性。但是当我们开发一个应用程序时(比如本文写的嗅探器),直接使用二进制代码作为BPF指令。这种二进制格式称为 BPF Bytecode。稍后将研究将这种汇编语言转换为字节码的方法。
第二列 addr mode 列出了每条指令允许的寻址模式。寻址方式的语义如下表所示:

使用原始套接字和 BPF 编写 Linux 数据包嗅探器


示例 BPF 程序


现在根据上面的知识来尝试理解下面这个小 BPF 程序,bpf_ip.asm:

(000) ldh [12](001) jeq #0x800 jt 2 jf 3(002) ret #262144(003) ret #0
BPF 程序由一组 BPF 指令组成。例如,上面的 BPF 程序包含 4 条指令。
第一条指令 ldh 从以太网数据包的偏移量 12 开始将半字(16 位)值加载到累加器中。根据下图所示的以太网帧格式,该值就是以太网类型字段。以太网类型用于指示帧的负载中封装了哪种协议(例如,0x0806 表示 ARP,0x0800 表示 IPv4,0x86DD 表示 IPv6)。
第二条指令 jeq 将累加器(当前存储以太网类型字段)与 0x800(代表 IPv4)进行比较。如果比较失败,则返回零,并拒绝数据包。如果成功,则返回一个非零值,并接受该数据包。所以小 BPF 程序过滤并接受所有 IP 数据包。可以在原始论文中找到其他 BPF 程序。去读一读会感受到 BPF 的灵活性以及设计之美。


BPF的内核实现


接下来看看内核是如何实现 BPF 的。如上所述,hook函数 packet_rcv 调用 run_filter 来处理过滤逻辑。run_filter 定义如下:
/* Copied from net/packet/af_packet.c *//* function run_filter is called in packet_rcv*/static inline unsigned int run_filter(struct sk_buff *skb, struct sock *sk, unsigned int res){ struct sk_filter *filter;
rcu_read_lock_bh(); filter = rcu_dereference(sk->sk_filter); // get the filter bound to the socket if (filter != NULL) res = sk_run_filter(skb, filter->insns, filter->len); // the filtering is inside sk_run_filter function rcu_read_unlock_bh();
return res;}

可以发现真正的过滤逻辑在sk_run_filter里面:

unsigned int sk_run_filter(struct sk_buff *skb, struct sock_filter *filter, int flen){ struct sock_filter *fentry; /* We walk down these */ void *ptr; u32 A = 0; /* Accumulator */ u32 X = 0; /* Index Register */ u32 mem[BPF_MEMWORDS]; /* Scratch Memory Store */ u32 tmp; int k; int pc;
/* * Process array of filter instructions. */ for (pc = 0; pc < flen; pc++) { fentry = &filter[pc];
switch (fentry->code) { case BPF_ALU|BPF_ADD|BPF_X: A += X; continue; case BPF_ALU|BPF_ADD|BPF_K: A += fentry->k; continue; case BPF_ALU|BPF_SUB|BPF_X: A -= X; continue; case BPF_ALU|BPF_SUB|BPF_K: A -= fentry->k; continue; case BPF_ALU|BPF_MUL|BPF_X: A *= X; continue; /* some code omitted ... */ case BPF_RET|BPF_K: return fentry->k; case BPF_RET|BPF_A: return A; case BPF_ST: mem[fentry->k] = A; continue; case BPF_STX: mem[fentry->k] = X; continue; default: WARN_ON(1); return 0; } }
return 0;}
正如我们提到的,sk_run_filter 只是一个数据包上的布尔值函数。它将累加器、索引寄存器等作为局部变量进行维护。并在 for 循环中处理 BPF 过滤器指令数组。每条指令都会更新局部变量的值。通过这种方式,它模拟了一个虚拟 CPU。

BPF JIT


由于每个网络数据包都必须经过过滤功能,因此成为整个系统的性能瓶颈。
2011 年将即时 (JIT) 编译器引入内核,以加速 BPF 字节码的执行。
什么是 JIT 编译器?JIT 编译器在程序启动后运行,并将代码(通常是字节码或某种类型的 VM 指令)即时(或及时)编译成通常更快的形式,通常是主机 CPU 的本机指令集。这与在程序首次运行之前将所有代码编译为机器语言的传统编译器形成对比。
在 BPF 的情况下,JIT 编译器直接将 BPF 字节码翻译成主机系统的汇编代码,可以大大优化性能。


在嗅探器中设置 BPF


接下来将 BPF 添加到我们的数据包嗅探器中。正如上面在应用层提到的,BPF 指令应该使用字节码格式,数据结构如下:
struct sock_filter { /* Filter block */ __u16 code; /* Actual filter code */ __u8 jt; /* Jump true */ __u8 jf; /* Jump false */ __u32 k; /* Generic multiuse field */};
如何将 BPF 汇编语言转换为字节码?有两种解决方案。首先,有一个叫做bpf_asm的工具(随Linux内核提供),可以把它看成是BPF汇编语言解释器。但不建议应用程序开发人员使用。
其次可以使用 tcpdump,它提供了转换功能。可以从 tcpdump 手册页中找到以下信息:
  • -d:以可读的形式将编译的数据包匹配代码转储到标准输出并停止。

  • -dd:将数据包匹配代码转储为 C 程序片段。

  • -ddd:将数据包匹配代码转储为十进制数(前面有一个计数)。

tcpdump ip 表示要捕获所有 IP 数据包。使用选项 -d、-dd 和 -ddd,输出如下:
baoqger@ubuntu:~$ sudo tcpdump -d ip[sudo] password for baoqger:(000) ldh [12](001) jeq #0x800 jt 2 jf 3(002) ret #262144(003) ret #0
baoqger@SLB-C8JWZH3:~$ sudo tcpdump -dd ip{ 0x28, 0, 0, 0x0000000c },{ 0x15, 0, 1, 0x00000800 },{ 0x6, 0, 0, 0x00040000 },{ 0x6, 0, 0, 0x00000000 },
baoqger@SLB-C8JWZH3:~$ sudo tcpdump -ddd ip440 0 0 1221 0 1 20486 0 0 2621446 0 0 0
选项 -d 以汇编语言打印 BPF 指令(与上面显示的示例 BPF 程序相同)。选项 -dd 将字节码打印为 C 程序片段。所以当你想获取 BPF 字节码时,tcpdump 是最方便的工具。
BPF过滤器字节码(包装在sock_fprog结构中)可以通过setsockopt系统调用传递给内核,如下所示:
// attach the filter to the socket// the filter code is generated by running: tcpdump tcpstruct sock_filter BPF_code[] = { { 0x28, 0, 0, 0x0000000c }, { 0x15, 0, 1, 0x00000800 }, { 0x6, 0, 0, 0x00040000 }, { 0x6, 0, 0, 0x00000000 }}; struct sock_fprog Filter;// error prone code, .len field should be consistent with the real length of the filter code arrayFilter.len = sizeof(BPF_code)/sizeof(BPF_code[0]); Filter.filter = BPF_code;

if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &Filter, sizeof(Filter)) < 0) { perror("setsockopt attach filter"); close(sock); exit(1);}
setsockopt 系统调用会触发两个内核函数:sock_setsockopt 和 sk_attach_filter,它们将过滤器绑定到套接字。并且在run_filter内核函数(上面提到过)中,它可以从socket中获取过滤器,并对数据包执行过滤器。
到目前为止,每个部分都已连接。BPF的难题解决了。BPF 机器允许用户空间应用程序将定制的 BPF 程序直接注入内核。一旦加载并验证,BPF 程序就会在内核上下文中执行。这些 BPF 程序在内核内存空间内运行,可以访问所有可用的内部内核状态。例如,使用网络数据包数据的 cBPF 机器。但是这种能力可以扩展为 eBPF,它可以用于许多其他不同的应用程序。在某种程度上,eBPF 对内核的作用就像 Javascript 对网站的作用:创建各种新的应用程序。


处理数据包


在上一节中大量研究了内核级别的 BPF 过滤理论。但是对于我们的小型嗅探器需要做的最后一步是处理网络数据包。
  • 首先,recvfrom 系统调用从套接字读取数据包。我们将系统调用放在一个while循环中以继续读取传入的数据包。
  • 然后打印数据包中的源 MAC 地址和目标 MAC 地址(得到的数据包应该是第 2 层中的原始以太网帧)。如果这个以太网帧包含的是一个 IP4 数据包,那么我们打印出源 IP 地址和目标 IP 地址。要了解更多信息,您可以研究各种网络协议的标头格式。 


while(1) { printf("-----------\n"); n = recvfrom(sock, buffer, 2048, 0, NULL, NULL); printf("%d bytes read\n", n);
/* Check to see if the packet contains at least * complete Ethernet (14), IP (20) and TCP/UDP * (8) headers. */ if (n < 42) { perror("recvfrom():"); printf("Incomplete packet (errno is %d)\n", errno); close(sock); exit(0); }
ethhead = buffer; printf("Source MAC address: %.2x:%.2x:%.2x:%.2x:%.2x:%.2x\n", ethhead[0], ethhead[1], ethhead[2], ethhead[3], ethhead[4], ethhead[5] ); printf("Destination MAC address: %.2x:%.2x:%.2x:%.2x:%.2x:%.2x\n", ethhead[6], ethhead[7], ethhead[8], ethhead[9], ethhead[10], ethhead[11] );
iphead = buffer + 14;
if (*iphead==0x45) { /* Double check for IPv4 * and no options present */ printf("Source host %d.%d.%d.%d\n", iphead[12],iphead[13], iphead[14],iphead[15]); printf("Dest host %d.%d.%d.%d\n", iphead[16],iphead[17], iphead[18],iphead[19]); printf("Source,Dest ports %d,%d\n", (iphead[20]<<8)+iphead[21], (iphead[22]<<8)+iphead[23]); printf("Layer-4 protocol %s\n", transport_protocol(iphead[9])); }}

总结


本文研究了什么是 PF_PACKET 套接字,它是如何工作的,以及为什么应用程序可以获取原始以太网数据包。此外还讨论了如何将嗅探器绑定到一个特定的网络接口,以及如何使嗅探器在混杂模式下工作。研究了如何嗅探器添加过滤器。分析为什么过滤器应该在内核空间而不是应用程序空间内运行。然后在论文的基础上详细讨论了 BPF 机器的设计和实现。查看了内核源代码以了解如何实现 BPF 虚拟机。