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

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.PacketConnSetBPF方法设置过滤器:

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语言的代码片段。

对了,这个库是阡陌