加载中…
个人资料
南冠彤
南冠彤
  • 博客等级:
  • 博客积分:0
  • 博客访问:412,918
  • 关注人气:59
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
相关博文
推荐博文
谁看过这篇博文
加载中…
正文 字体大小:

[转载]BPF(BSD Packet Filter)数据包过滤

(2014-06-16 16:36:02)
标签:

转载

分类: 网络/p2p

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 对所有接口进行捕获,为了使包过滤机制能在所有类型的数据包上正常工作,要求所有的数据包有相同的数据链路头部。

BPF是一个过滤机制,它用于过滤送往特定地点比如用户空间的数据包,它被设计成一种类似汇编语言的语言,可以称之为伪汇编码。虽然被设计用来过滤数据包,但这种设计方式更适合用于操作硬件,特别用来编写需要写少量固定序列的硬件驱动程序。不管用于什么,BPF的设计是优秀的,是状态机实现控制逻辑的完美实例。BPF实际上是一组基于状态机的匹配过滤序列,用于简单的数据包模式匹配。每个匹配包含四个元素,定义为一个结构体:
struct socket_filter
{
    __u16    code;   //操作码,可以实现数值运算,加载,比较等操作
        __u8    jt;    //如果匹配跳转到哪里
        __u8    jf;    //如果不匹配跳转到哪里
        __u32    k;      //参数字段,对于不同的操作码有不同的用途。比如在操作码是比较时存放比较键,操作码为加载时存放载入数据在数据包(链路帧/数据报)的偏移
}
匹配序列很像一个汇编程序,有其自身的操作码,操作数以及分支跳转功能,于是这段匹配序列的执行过程自然就类似一个冯诺依曼机器上单进程的执行绪了,它的本质从执行上讲是一个状态机(从数据角度讲,进程又是一个过滤器,它的名字恰就是过滤器...),很显然其实现应该是一个状态驱动的循环:
while(序列中还有匹配){
    switch(当前操作码)
        case 加减乘除:
            ...
        case 加载:
            载入当前匹配项的k值便宜的数据,设为d
            下一个匹配项
        case 比较跳转:
            程序计数器 += 比较结果?当前匹配项的jt字段:jf字段
        ...
}
看看linux实现的代码,它基本就是这么实现的:
int sk_run_filter(struct sk_buff *skb, struct sock_filter *filter, int flen)
{
    ...//定义中间变量,保存临时计算结果
    int k;
    int pc; //程序计数器,用于分支跳转
    for (pc = 0; pc < flen; pc++) {
        fentry = &filter[pc];
        switch (fentry->code) {
        case BPF_ALU|BPF_ADD|BPF_X:
            A += X;
            continue;
        ...//类似实现减法,乘法,除法,取反,与,或..等操作
        case BPF_JMP|BPF_JA: //涉及分支跳转
            pc += fentry->k;
            continue;
        case BPF_JMP|BPF_JGT|BPF_K:  //大于
            pc += (A > fentry->k) ? fentry->jt : fentry->jf;
            continue;
        ...//类似实现小于等于等比较操作,然后分支跳转
 load_w:    //加载操作,类似x86汇编中的mov,这些load操作也是要区分大小的,比如是load一个字还是双字,还是字节...
            if (k >= 0 && (unsigned int)(k+sizeof(u32)) <= len) {
                A = ntohl(*(u32*)&data[k]);
                continue;
            }
    ...
}
BPF用于很多抓包程序,在linux中,一般内核自动编译进了af_packet这个驱动,因此只需要在用户态配制一个PACKET类型的socket,然后将filter配制进内核即可--使用setsockopt的SO_ATTACH_FILTER命令,这个filter是在用户空间配制的,比如tcpdump应用程序,tcpdump和内核BPF过滤器的关系类似iptables和netfilter的关系,只是netfilter实现了match/target的复杂配合,而BPF的target仅仅是“该数据包要”和“该数据包不要”。当在用户态配制
tcpdump -i eth0 host 1.2.3.4 ...
的时候,实际上进入内核的filter就是以下的序列,每个{}中的都是一个socket_filter:
...
n:    {加载,0,0,源ip地址在以太帧中的偏移},
n+1:    {比较跳转,n+3,n+2,"1.2.3.4"},
n+2:    {加载,0,0,目标ip地址在以太帧中的偏移},
n+3:    {比较跳转,n+4,n+m,"1.2.3.4"},
n+4:    {...},
...
n+m:    {返回...}
然后当有数据包进来的时候,由于tcpdump的socket事先注册进了ptype_all这个list,那么数据包将会复制一份给了tcpdump的socket,然后在其packet_type的func函数中调用run_filter来进行数据包过滤,确定到底需不需要将这个包交给tcpdump。
     在windows中,由于其羸弱的网络处理能力以及过渡的分层,或者说为了创立业界标准而导致过度接口化的实现,其内核并没有直接包含BPF,需要一个NDIS过滤驱动来实现,这个实现起来也是蛮简单的,很模块化的。在上面盖一个类似libpcap的接口,这样就可以实现ethereal了。不管在什么操作系统上,如果能将这种伪汇编指令及时编译成机器指令,利用冯诺依曼机器cpu状态机的本质来代替软件函数--比如sk_run_filter,那性能将会有很大的提升。
     最后看看BPF的设计理念用于硬件驱动程序的情形,首先定义一个结构体,类似linux的BPF中的socket_filter,但是更加紧凑冗余了,实际上没有必要实现这么多的字段,不过那样的话driver函数就要更复杂了,总之理念一致即可:
struct sequence_item {
        int opt;    //操作码:读/写/加减乘除,取反...
        int data;    //操作数
        int port;    //第二操作数,可以为端口
        int flag;    //标志,可存储是否使用中间结果
    char reverse[0] //预留
};
int driver(struct sequence_item *sequence, unsigned int len)
{
        int i = 0;
    int result = -1;
    struct sequence_item si;
    for (; i < len; i++) {
        si = sequence[i];
         if (si.opt == 0) {
            outb_p(si.flag?result:si.data, si.port);
        } else if (si.opt == 1){
            result = inb_p(si.port);
        } else {
            switch (si.opt) {
                case '~':
                    result ~= si.data;
                    break;
                case '^':
                    result ^= si.data;
                    break;
                ...
            }
        }
    }
    return 1;
}
[PS]:这个代码是从很早之前(3 years ago)我写的一个驱动程序中抽出来的,所使用的思想竟然和BPF(2 years ago)的一致。

 BPF:Berkeley Packet Filter

 http://blog.csdn.net/maeom/article/details/6092457

  英文高手可以直接看原文:http://www.gsp.com/cgi-bin/man.cgi?section=4&topic=bpf#1

或许有一部分人会看不太懂,那么结合下面第二部分的封包结构,就会容易一些。let's go


首先,要确保我们从socket中读取的是packet,也就是说是 MAC头+IP头+TCP/UDP头,这个样子的才可以。当然,如果用linux下lpf也可以在IP包上做过滤。一样的道理。

BPF(Berkeley Packet Filter)

(1) 需要包含的头文件

  1. #include <sys/types.h>  
  2. #include <sys/time.h>  
  3. #include <sys/ioctl.h>  
  4. #include <net/bpf.h>  

(2) FILTER MACHINE
  1. //这个就是bpf instruction的缩写了  
  2. struct bpf_insn  
  3.         u_short code;  
  4.         u_char  jt;  
  5.         u_char  jf;  
  6.         u_long k;  
  7. };  
  8. BPF_LD  //将值拷贝进寄存器(accumulator),没学过汇编。我就当做赋给一个变量了  
  9. BPF_LDX //将值拷贝进索引寄存器(index register)  
  10. BPF_LD+BPF_W+BPF_ABS    <- P[k:4]     //将一个Word 即4 byte赋给寄存器(accumulator)  
  11. BPF_LD+BPF_H+BPF_ABS    <- P[k:2]     //将一个Half Word 即2 byte赋给寄存器(accumulator)  
  12. BPF_LD+BPF_B+BPF_ABS    <- P[k:1]     //将一个Byte 赋给寄存器(accumulator)  
  13. BPF_LD+BPF_W+BPF_IND    <- P[X+k:4]   //偏移X寄存器后,将一个Word 即4 byte赋给寄存器(accumulator)  
  14. BPF_LD+BPF_H+BPF_IND    <- P[X+k:2]  
  15. BPF_LD+BPF_B+BPF_IND    <- P[X+k:1]  
  16. BPF_LD+BPF_W+BPF_LEN    <- len        //the packet length 不知道什么意思 :(  
  17. BPF_LD+BPF_IMM          <-          //将常量k赋给寄存器(accumulator)  
  18. BPF_LD+BPF_MEM          <- M[k]       //将一个Word的地址为k的内存部分赋给寄存器(accumulator)  
  19. //下面的部分是将值load进index register 大家自己理解吧  
  20. BPF_LDX+BPF_W+BPF_IMM   <-  
  21. BPF_LDX+BPF_W+BPF_MEM   <- M[k]  
  22. BPF_LDX+BPF_W+BPF_LEN   <- len  
  23. BPF_LDX+BPF_B+BPF_MSH   <- 4*(P[k:1]&0xf)  
  24. //来看看两个宏  
  25. #define BPF_STMT(code, k) (unsigned short)(code), 0, 0, }  
  26. #define BPF_JUMP(code, k, jt, jf) (unsigned short)(code), jt, jf, }  
  27. //关键的判断指令忘贴了  
  28. BPF_JMP+BPF_JA          pc +=  
  29. BPF_JMP+BPF_JGT+BPF_K   pc += (A k) jt jf  
  30. BPF_JMP+BPF_JGE+BPF_K   pc += (A >= k) jt jf   
  31.  //这个BPF_JEQ用的比较多,就拿它开刀了。 一看就知道,是用来判断是否相等的东西 
  32. //这个会判断我们给出的数,和A(也就是accumulator寄存器)的内容是否相等, 
  33. //结果就不用我说了,三目运算符。  
  34. BPF_JMP+BPF_JEQ+BPF_K   pc += (A == k) jt jf  
  35. BPF_JMP+BPF_JSET+BPF_K  pc += (A k) jt jf  
  36. BPF_JMP+BPF_JGT+BPF_X   pc += (A X) jt jf  
  37. BPF_JMP+BPF_JGE+BPF_X   pc += (A >= X) jt jf  
  38. BPF_JMP+BPF_JEQ+BPF_X   pc += (A == X) jt jf  
  39. BPF_JMP+BPF_JSET+BPF_X  pc += (A X) jt jf  
  40. //返回指令  
  41. BPF_RET+BPF_A           //接受 寄存器中的数量bytes  
  42. BPF_RET+BPF_K           //接受常量 bytes  
  43. //下面我们就直接看个实例吧  
  44. // 前提条件,这个filter的前提是我们抓的包是将物理层都抓下来的情况下。 
  45. // 不懂的 物理头-IP头-TCP/UDP头 的兄弟们结合下面的图吧,方便理解与记忆。 
  46. // 这是一个过滤TCP源端口不为79,目的端口为79的包(TCP Finger)  
  47. struct bpf_insn insns[]  
  48.     //物理头,偏移12byte后,指向type     
  49.     BPF_STMT(BPF_LD+BPF_H+BPF_ABS, 12),  
  50.     //进行比较,是否为IP协议。 true的话 0,  false 10 
  51.     /这里说一下了,由于本人没学过汇编,对这玩意开始时相当困惑。对于学过汇编的兄弟,应该是小菜吧。 
  52.     //true,则跳过0条指令执行。 就是继续执行下面的一条指令 
  53.     //false,则跳过10条指令执行。 数数!结果就数到最后一条。即BPF_STMT(BPF_RET+BPF_K, 0),就返回了 
  54.     //下面的照这个慢慢分析就OK了。   
  55.     BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, ETHERTYPE_IP, 0, 10),  
  56.     BPF_STMT(BPF_LD+BPF_B+BPF_ABS, 23),  
  57.     BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, IPPROTO_TCP, 0, 8),  
  58.     BPF_STMT(BPF_LD+BPF_H+BPF_ABS, 20),  
  59.     BPF_JUMP(BPF_JMP+BPF_JSET+BPF_K, 0x1fff, 6, 0),  
  60.     BPF_STMT(BPF_LDX+BPF_B+BPF_MSH, 14),  
  61.     BPF_STMT(BPF_LD+BPF_H+BPF_IND, 14),  
  62.     BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, 79, 2, 0),  
  63.     BPF_STMT(BPF_LD+BPF_H+BPF_IND, 16),  
  64.     BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, 79, 0, 1),  
  65.     BPF_STMT(BPF_RET+BPF_K, (u_int)-1),  
  66.     BPF_STMT(BPF_RET+BPF_K, 0),  
  67. }; 
 

下面我们来看下网络包的格式,才能对上面如何编写。温习一下网络结构

Ethernet Header

EthDHost :

Destination address (6 bytes ). //目的MAC地址

EthSHost

Source address (6 bytes ).

EthType

Encapsulated packet type (2 bytes ). It is ETHERTYPE_IP for IP based communication.   //所承载的协议,IP协议则为ETHERTYPE_IP

 

上面为IP头格式,一行为32为,即4 bytes

 

下面来看看TCP头和UDP头

 

TCP的头这么复杂和IP头差不多了。

UDP的包头就简单多了

0

  • 评论加载中,请稍候...
发评论

    发评论

    以上网友发言只代表其个人观点,不代表新浪网的观点或立场。

      

    新浪BLOG意见反馈留言板 电话:4000520066 提示音后按1键(按当地市话标准计费) 欢迎批评指正

    新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 会员注册 | 产品答疑

    新浪公司 版权所有