数据包过滤机制
大
量的网络监控程序目的不同,期望的数据包类型也不同,但绝大多数情况都都只需要所有数据包的一(小)部分。例如:对邮件系统进行监控可能只需要端口号为
25(smtp)和 110(pop3) 的 TCP 数据包,对 DNS 系统进行监控就只需要端口号为 53 的 UDP
数据包。包过滤机制的引入就是为了解决上述问题,用户程序只需简单的设置一系列过滤条件,最终便能获得满足条件的数据包。包过滤操作可以在用户空间执行,
也可以在内核空间执行,但必须注意到数据包从内核空间拷贝到用户空间的开销很大,所以如果能在内核空间进行过滤,会极大的提高捕获的效率。内核过滤的优势
在低速网络下表现不明显,但在高速网络下是非常突出的。在理论研究和实际应用中,包捕获和包过滤从语意上并没有严格的区分,关键在于认识到捕获数据包必然
有过滤操作。基本上可以认为,包过滤机制在包捕获机制中占中心地位。
包过滤机制实际上是针对数据包的布尔值操作函数,如果函数最终返回
true,则通过过滤,反之则被丢弃。形式上包过滤由一个或多个谓词判断的并操作(AND)和或操作(OR)构成,每一个谓词判断基本上对应了数据包的协
议类型或某个特定值,例如:只需要 TCP 类型且端口为 110 的数据包或 ARP
类型的数据包。包过滤机制在具体的实现上与数据包的协议类型并无多少关系,它只是把数据包简单的看成一个字节数组,而谓词判断会根据具体的协议映射到数组
特定位置的值。如判断ARP类型数据包,只需要判断数组中第 13、14 个字节(以太头中的数据包类型)是否为
0X0806。从理论研究的意思上看,包过滤机制是一个数学问题,或者说是一个算法问题,其中心任务是如何使用最少的判断操作、最少的时间完成过滤处理,
提高过滤效率。
BPF
Libpcap
重点使用 BPF(BSD Packet Filter)包过滤机制,BPF 于 1992
年被设计出来,其设计目的主要是解决当时已存在的过滤机制效率低下的问题。BPF的工作步骤如下:当一个数据包到达网络接口时,数据链路层的驱动会把它向
系统的协议栈传送。但如果 BPF 监听接口,驱动首先调用 BPF。BPF
首先进行过滤操作,然后把数据包存放在过滤器相关的缓冲区中,最后设备驱动再次获得控制。注意到BPF是先对数据包过滤再缓冲,避免了类似 sun 的
NIT 过滤机制先缓冲每个数据包直到用户读数据时再过滤所造成的效率问题。参考资料D是关于 BPF 设计思想最重要的文献。
BPF
的设计思想和当时的计算机硬件的发展有很大联系,相对老式的过滤方式CSPF(CMU/Stanford Packet
Filter)它有两大特点。1:基于寄存器的过滤机制,而不是早期内存堆栈过滤机制,2:直接使用独立的、非共享的内存缓冲区。同时,BPF
在过滤算法是也有很大进步,它使用无环控制流图(CFG control flow graph),而不是老式的布尔表达式树(boolean
expression tree)。布尔表达式树理解上比较直观,它的每一个叶子节点即是一个谓词判断,而非叶子节点则为 AND 操作或
OR操作。CSPF
有三个主要的缺点。1:过滤操作使用的栈在内存中被模拟,维护栈指针需要使用若干的加/减等操作,而内存操作是现代计算机架构的主要瓶颈。2:布尔表达式
树造成了不需要的重复计算。3:不能分析数据包的变长头部。BPF 使用的CFG
算法实际上是一种特殊的状态机,每一节点代表了一个谓词判断,而左右边分别对应了判断失败和成功后的跳转,跳转后又是谓词判断,这样反复操作,直到到达成
功或失败的终点。CFG 算法的优点在于把对数据包的分析信息直接建立在图中,从而不需要重复计算。直观的看,CFG
是一种"快速的、一直向前"的算法。
过滤代码的编译
BPF
对 CFG 算法的代码实现非常复杂,它使用伪机器方式。BPF 伪机器是一个轻量级的,高效的状态机,对 BPF 过滤代码进行解释处理。BPF
过滤代码形式为"opcode jt jf k",分别代表了操作码和寻址方式、判断正确的跳转、判断失败的跳转、操作使用的通用数据域。BPF
过滤代码从逻辑上看很类似于汇编语言,但它实际上是机器语言,注意到上述 4 个域的数据类型都是 int 和 char
型。显然,由用户来写过滤代码太过复杂,因此 libpcap 允许用户书写高层的、容易理解的过滤字符串,然后将其编译为BPF代码。
Libpcap
使用了 4 个源程序
gencode.c、optimize.c、grammar.c、scanner.c完成编译操作,其中前两个实现了对过滤字符串的编译和优化,后两个主
要是为编译提供从协议相关过滤条件到协议无关(的字符数组)位置信息的映射,并且它们由词汇分析器生成器 flex 和 bison 生成。参考资料
C 有对此两个工具的讲解。
flex -Ppcap_ -t scanner.l > $$.scanner.c; mv $$.scanner.c scanner.c bison -y -p pcap_ -d grammar.y mv y.tab.c grammar.c mv y.tab.h tokdefs.h
|
编译过滤字符串调用了函数 pcap_compile()[getcode.c],形式为:
int pcap_compile(pcap_t *p, struct bpf_program *program, char *buf, int optimize, bpf_u_int32 mask)
|
其中 buf 指向用户过滤字符串,编译后的 BPF 代码存在在结构 bpf_program中,标志 optimize 指示是否对 BPF 代码进行优化。
/* [pcap-bpf.h] */ struct bpf_program { u_int bf_len; /* BPF 代码中谓词判断指令的数目 */ struct bpf_insn *bf_insns; /* 第一个谓词判断指令 */ }; /* 谓词判断指令结构,含意在前面已描述 [pcap-bpf.h] */ struct bpf_insn { u_short code; u_char jt; u_char jf; bpf_int32 k; };
|
过滤代码的安装
前
面我们曾经提到,在内核空间过滤数据包对整个捕获机制的效率是至关重要的。早期使用 SOCK_PACKET 方式的 Linux
不支持内核过滤,因此过滤操作只能在用户空间执行(请参阅函数 pcap_read_packet() 代码),在《UNIX
网络编程(第一卷)》(参考资料 B)的第 26 章中对此有明确的描述。不过现在看起来情况已经发生改变,linux 在 PF_PACKET
类型的 socket 上支持内核过滤。Linux 内核允许我们把一个名为 LPF(Linux Packet Filter) 的过滤器直接放到
PF_PACKET 类型 socket 的处理过程中,过滤器在网卡接收中断执行后立即执行。LSF 基于 BPF
机制,但两者在实现上有略微的不同。实际代码如下:
/* 在包捕获设备上附加 BPF 代码 [pcap-linux.c]*/ static int pcap_setfilter_linux(pcap_t *handle, struct bpf_program *filter) { #ifdef SO_ATTACH_FILTER struct sock_fprog fcode; int can_filter_in_kernel; int err = 0; #endif
/* 检查句柄和过滤器结构的正确性 */ if (!handle) return -1; if (!filter) { strncpy(handle->errbuf, "setfilter: No filter specified", sizeof(handle->errbuf)); return -1; }
/* 具体描述如下 */ if (install_bpf_program(handle, filter) < 0) return -1;
/* 缺省情况下在用户空间运行过滤器,但如果在内核安装成功,则值为 1 */ handle->md.use_bpf = 0;
/* 尝试在内核安装过滤器 */ #ifdef SO_ATTACH_FILTER #ifdef USHRT_MAX if (handle->fcode.bf_len > USHRT_MAX) { /*过滤器代码太长,内核不支持 */ fprintf(stderr, "Warning: Filter too complex for kernel\n"); fcode.filter = NULL; can_filter_in_kernel = 0; } else #endif /* USHRT_MAX */ { /* linux 内核设置过滤器时使用的数据结构是 sock_fprog,而不是 BPF 的结构 bpf_program ,因此应做结构之间的转换 */ switch (fix_program(handle, &fcode)) { /* 严重错误,直接退出 */ case -1: default: return -1; /* 通过检查,但不能工作在内核中 */ case 0: can_filter_in_kernel = 0; break;
/* BPF 可以在内核中工作 */ case 1: can_filter_in_kernel = 1; break; } }
/* 如果可以在内核中过滤,则安装过滤器到内核中 */ if (can_filter_in_kernel) { if ((err = set_kernel_filter(handle, &fcode)) == 0) { /* 安装成功 !!! */ handle->md.use_bpf = 1; } else if (err == -1) /* 出现非致命性错误 */ { if (errno != ENOPROTOOPT && errno != EOPNOTSUPP) { fprintf(stderr, "Warning: Kernel filter failed: %s\n",pcap_strerror(errno)); } } }
/* 如果不能在内核中使用过滤器,则去掉曾经可能在此 socket 上安装的内核过滤器。主要目的是为了避免存在的过滤器对数据包过滤的干扰 */ if (!handle->md.use_bpf) reset_kernel_filter(handle);[pcap-linux.c] #endif }
/* 把 BPF 代码拷贝到 pcap_t 数据结构的 fcode 上 */ int install_bpf_program(pcap_t *p, struct bpf_program *fp) { size_t prog_size;
/* 首先释放可能已存在的 BPF 代码 */ pcap_freecode(&p->fcode);
/* 计算过滤代码的长度,分配内存空间 */ prog_size = sizeof(*fp->bf_insns) * fp->bf_len; p->fcode.bf_len = fp->bf_len; p->fcode.bf_insns = (struct bpf_insn *)malloc(prog_size); if (p->fcode.bf_insns == NULL) { snprintf(p->errbuf, sizeof(p->errbuf), "malloc: %s", pcap_strerror(errno)); return (-1); }
/* 把过滤代码保存在捕获句柄中 */ memcpy(p->fcode.bf_insns, fp->bf_insns, prog_size); return (0); }
/* 在内核中安装过滤器 */ static int set_kernel_filter(pcap_t *handle, struct sock_fprog *fcode) { int total_filter_on = 0; int save_mode; int ret; int save_errno;
/*在设置过滤器前,socket 的数据包接收队列中可能已存在若干数据包。当设置过滤器后, 这些数据包极有可能不满足过滤条件,但它们不被过滤器丢弃。 这意味着,传递到用户空间的头几个数据包不满足过滤条件。 注意到在用户空间过滤这不是问题,因为用户空间的过滤器是在包进入队列后执行的。 Libpcap 解决这个问题的方法是在设置过滤器之前, 首先读完接收队列中所有的数据包。具体步骤如下。*/ /*为了避免无限循环的情况发生(反复的读数据包并丢弃,但新的数据包不停的到达),首先设置一个过滤器,阻止所有的包进入 */ setsockopt(handle->fd, SOL_SOCKET, SO_ATTACH_FILTER, &total_fcode, sizeof(total_fcode);
/* 保存 socket 当前的属性 */ save_mode = fcntl(handle->fd, F_GETFL, 0);
/* 设置 socket 它为非阻塞模式 */ fcntl(handle->fd, F_SETFL, save_mode | O_NONBLOCK);
/* 反复读队列中的数据包,直到没有数据包可读。这意味着接收队列已被清空 */ while (recv(handle->fd, &drain, sizeof drain, MSG_TRUNC) >= 0); /* 恢复曾保存的 socket 属性 */ fcntl(handle->fd, F_SETFL, save_mode); /* 现在安装新的过滤器 */ setsockopt(handle->fd, SOL_SOCKET, SO_ATTACH_FILTER, fcode, sizeof(*fcode)); }
/* 释放 socket 上可能有的内核过滤器 */ static int reset_kernel_filter(pcap_t *handle) { int dummy; return setsockopt(handle->fd, SOL_SOCKET, SO_DETACH_FILTER, &dummy, sizeof(dummy)); }
|
linux 在安装和卸载过滤器时都使用了函数
setsockopt(),其中标志SOL_SOCKET 代表了对 socket 进行设置,而 SO_ATTACH_FILTER 和
SO_DETACH_FILTER 则分别对应了安装和卸载。下面是 linux 2.4.29 版本中的相关代码:
[net/core/sock.c] #ifdef CONFIG_FILTER case SO_ATTACH_FILTER: …… /* 把过滤条件结构从用户空间拷贝到内核空间 */ if (copy_from_user(&fprog, optval, sizeof(fprog))) break; /* 在 socket 上安装过滤器 */ ret = sk_attach_filter(&fprog, sk); ……
case SO_DETACH_FILTER: /* 使用自旋锁锁住 socket */ spin_lock_bh(&sk->lock.slock);
filter = sk->filter; /* 如果在 socket 上有过滤器,则简单设置为空,并释放过滤器内存 */ if (filter) { sk->filter = NULL; spin_unlock_bh(&sk->lock.slock); sk_filter_release(sk, filter); break; } spin_unlock_bh(&sk->lock.slock); ret = -ENONET; break; #endif
|