IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    何时以及如何高效的使用经典的bpf, 它能到来什么好处?

    smallnest发表于 2023-12-13 13:09:16
    love 0

    Classical BPF(cBPF, 伯克利包过滤器)是一种用来过滤网络数据包的技术。它像一个钩子一样挂载在网络栈的关键路径上,可以在数据包进入协议栈之前,根据预设规则来过滤或处理网络数据包。

    相比于一般的软件包过滤方案,Classical BPF有以下优点:

    • 效率高:因为它运行在内核空间,可以避免不必要的内核态和用户态切换,也省去多次数据复制的开销。
    • 安全:它不能随意访问系统内存或修改数据包,只能根据规则过滤,不会引起安全隐患。
    • 灵活:过滤规则可以动态更新,使包过滤功能更加灵活。

    Classical BPF通常应用于网络监控、防火墙、流量控制等场景。它为包过滤提供了一个高效、安全、灵活的解决方案。但功能较为受限,只能过滤包不能修改。

    我在百度做了三年多的网络监控了,我们会使用各种各样的方式来监控整个百度的物理网络,这些监控方式不同于普通的TCP Server/Client或者 UDP程序,一般我们会采用raw socket的方式来做包的探测和网络监控,为了高效的使用raw socket,避免把内核协议层的所有包都复制到应用层,我们会使用cBPF对收到的包进行过滤,我们只从内核层复制特定类型的包到应用层, 比如只复制UDP协议目的端口在20000 ~ 21000的数据包。

    怎么做到呢?就是使用cBPF。

    我在先前的文章使用BPF, 将Go网络程序的吞吐提升8倍举了一个使用cBPF的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    conn, err := net.ListenPacket("ip4:udp", *addr)
    if err != nil {
    panic(err)
    }
    cc := conn.(*net.IPConn)
    cc.SetReadBuffer(20 * 1024 * 1024)
    cc.SetWriteBuffer(20 * 1024 * 1024)
    pconn := ipv4.NewPacketConn(conn)
    var assembled []bpf.RawInstruction
    if assembled, err = bpf.Assemble(filter); err != nil {
    log.Print(err)
    return
    }
    pconn.SetBPF(assembled)
    handleConn(conn)

    可以使用ipv4.PacketConn的SetBPF方法设置过滤器:

    1
    2
    3
    4
    5
    6
    7
    8
    type Filter []bpf.Instruction
    var filter = Filter{
    bpf.LoadAbsolute{Off: 22, Size: 2}, // 加载目的端口
    bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(*port), SkipFalse: 1}, // 如果目的端口 != 8972,则跳过一行,到了最后一,包丢弃了instruction
    bpf.RetConstant{Val: 0xffff}, // 返回 0xffff,接收包
    bpf.RetConstant{Val: 0x0}, // 返回0字节, 代表忽略这个包
    }

    这里我们根据IP协议进行简单的分析。这里我们没有做过多的兼容检查,因为我们自己知道我们处理的是IPv4的包,而且包中也没有Option选项:

    IP的头部20个字节,payload是UDP包:

    可以看到UDP的前两个字节是源端口, 接下来两个字节是目的端口。

    所以从IP header开始,第22 ~ 24字节是目的端口,所以bpf.LoadAbsolute{Off: 22, Size: 2},就是把这两个字节读取出来,和我们的值进行比较,看看是不是我们期望的值。

    那么如果想使用cBPF,就得会写bpf.Instruction, 你得熟悉各种协议,以及bpf的指令。
    不想学啊!累,麻烦!易出错!不好调试!

    没关系,我写了一个库,只要你会使用tcpdump/wireshark,会使用他们的过滤器写法,就能写出相应的指令来。

    比如 tcpdump -i any -nn -vvvv tcp port 8080这样一个命令,它的过滤器是tcp port 8080, 你这个使用这个库的下面的函数:

    1
    raws, err := ParseTcpdumpFitlerExpr(layers.LinkTypeIPv4, "tcp port 8080")

    调用这个函数你会得到编译好的指令[]bpf.RawInstruction,然后调用pconn.SetBPF(raws)就可以了。

    如果,你想得到它的Go代码形式,你可以调用s = CreateInstructionsFromExpr(layers.LinkTypeIPv4, "dst host 8.8.8.8 and icmp"),
    它会过滤只保留目的IP地址是8.8.8.8并且是icmp的包,生成的指令如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var filter = []bpf.Instruction {
    bpf.LoadConstant{Dst: 0,Val: 0},
    bpf.LoadAbsolute{Off: 16,Size: 4},
    bpf.JumpIf{Cond: 1,Val: 134744072,SkipTrue: 3} // 134744072 = 0x8080808,
    bpf.LoadAbsolute{Off: 9,Size: 1},
    bpf.JumpIf{Cond: 1,Val: 1,SkipTrue: 1},
    bpf.RetConstant{Val: 0x1},
    bpf.RetConstant{Val: 0x0},
    }

    bpf.LoadAbsolute{Off: 16,Size: 4},是加载IP头中的目的IP地址,检查是不是等于8.8.8.8,如果是,则检查协议(odd:9)是不是ICMP(icmp的协议号是1)。

    所以即使你不熟悉各种协议,根据tcpdump的过滤表达式也能生成编译好的bpf代码,或者得到Go语言的代码片段。

    对了,这个库是阡陌。



沪ICP备19023445号-2号
友情链接