BPF的理解

Faj*_*iya 9 performance tcpdump ebpf

当我需要使用捕获一些数据包时tcpdump,我使用如下命令:

tcpdump -i eth0 "dst host 192.168.1.0"
Run Code Online (Sandbox Code Playgroud)

我一直认为dst 主机 192.168.1.0部分是称为 BPF(伯克利数据包过滤器)的东西。对我来说,这是一种过滤网络数据包的简单语言。但今天我的室友告诉我 BPF 可以用来捕获性能信息。根据他的描述,这就像Windows上的工具perfmon。这是真的吗?它与我在问题开头提到的BPF相同吗?

for*_*est 19

什么是 BPF?

\n

BPF(或更常见的是扩展版本eBPF)是一种最初专门用于过滤数据包的语言,但它的功能远不止于此。在 Linux 上,它可以用于许多其他事情,包括用于安全的系统调用过滤器、在系统内存不足时选择要终止的进程,以及复杂的性能监控,正如您所指出的。虽然 Windows 确实添加了 eBPF 支持,但这并不是 Windowsperfmon实用程序所使用的。Windows 仅添加了对依赖操作系统对 eBPF 支持的非 Windows 实用程序的兼容性支持。

\n

eBPF 程序不在用户空间中执行。相反,应用程序创建一个 eBPF 程序并将其发送到内核,然后由内核执行。它实际上是虚拟处理器的机器代码,在内核中以解释器的形式实现,尽管它也可以使用JIT 编译来显着提高性能。该程序可以访问内核中的一些基本接口,包括与性能和网络相关的接口。然后,eBPF 程序与内核进行通信,为其提供计算结果(例如丢弃数据包)。

\n

eBPF 计划的限制

\n

为了防止拒绝服务攻击或意外崩溃,内核在编译代码之前首先验证代码。在运行之前,代码需要经过几项重要的检查:

\n
    \n
  • 对于非特权用户来说,该程序总共包含不超过4096条指令。

    \n
  • \n
  • 除了有界循环和函数调用之外,不能发生向后跳转。

    \n
  • \n
  • 没有永远无法到达的指令。

    \n
  • \n
\n

结果是验证者必须能够证明 eBPF 程序停止了。当然,它还没有找到停止问题的解决方案,这就是为什么它只接受它知道会停止的程序。为此,它将程序表示为有向无环图。除此之外,它还尝试通过防止指针的实际被泄露,同时仍然允许对其执行有限的操作来防止信息泄漏和越界内存访问:

\n
    \n
  • 指针不能作为可检查的值进行比较、存储或返回。

    \n
  • \n
  • 指针算术只能针对标量(不是从指针派生的值)进行。

    \n
  • \n
  • 任何指针算术都不会导致指向指定内存映射之外。

    \n
  • \n
\n

验证程序相当复杂,而且功能要多得多,尽管它本身就是严重 安全 错误的根源,至少在bpf(2)没有为非特权用户禁用系统调用时是这样。

\n

查看代码

\n

dst host 192.168.1.0命令的组件不是 BPF。这只是 所使用的语法tcpdump。但是,您给它的命令用于生成 BPF 程序,然后将其发送到内核。请注意,本例中使用的不是 eBPF,而是较旧的 cBPF。两者之间有几个重要的区别(尽管内核内部将 cBPF 转换为 eBPF)。-d标志可用于查看要发送到内核的 cBPF 代码:

\n
# tcpdump -i eth0 "dst host 192.168.1.0" -d\n(000) ldh      [12]\n(001) jeq      #0x800           jt 2    jf 4\n(002) ld       [30]\n(003) jeq      #0xc0a80100      jt 8    jf 9\n(004) jeq      #0x806           jt 6    jf 5\n(005) jeq      #0x8035          jt 6    jf 9\n(006) ld       [38]\n(007) jeq      #0xc0a80100      jt 8    jf 9\n(008) ret      #262144\n(009) ret      #0\n
Run Code Online (Sandbox Code Playgroud)\n

更复杂的过滤器会导致更复杂的字节码。尝试联机帮助页中的一些示例并附加标志-d以查看哪些字节码将加载到内核中。为了了解如何阅读反汇编代码,请查看BPF 过滤器文档。如果您正在阅读 eBPF 程序,则应该查看虚拟 CPU 的eBPF 指令集。

\n

理解代码

\n

为简单起见,我假设您指定了目标 IP 192.168.1.1 而不是 192.168.1.0,并且只想匹配 IPv4,这会大大缩减代码,因为它不再需要处理 IPv6:

\n
# tcpdump -i eth0 "dst host 192.168.1.1 and ip" -d\n(000) ldh      [12]\n(001) jeq      #0x800           jt 2    jf 5\n(002) ld       [30]\n(003) jeq      #0xc0a80101      jt 4    jf 5\n(004) ret      #262144\n(005) ret      #0\n
Run Code Online (Sandbox Code Playgroud)\n

让我们看一下上面的字节码实际上做了什么。每次在指定接口上收到数据包时,都会运行 BPF 字节码。数据包内容(包括以太网标头,如果适用)被放入 BPF 代码可以访问的缓冲区中。如果数据包与过滤器匹配,代码将返回捕获缓冲区的大小(默认为 262144 字节),否则返回 0。

\n

假设您正在运行此过滤器,并且它收到一个从 192.168.1.142 到 192.168.1.1 发送带有空负载的 ICMP 消息的数据包。源 MAC 为 aa:aa:aa:aa:aa:aa,目标 MAC 为 bb:bb:bb:bb:bb:bb。以太网帧的内容(以十六进制表示)为:

\n
aa aa aa aa aa aa bb bb bb bb bb bb 08 00 45 00\n00 1c 77 71 40 00 40 01 3f 92 c0 a8 01 8e c0 a8\n01 01 08 00 c1 c0 36 0e 00 01\n
Run Code Online (Sandbox Code Playgroud)\n

第一条指令是ldh [12]. 这会将位于数据包偏移 12 字节处的半字(两个字节)加载到 A 寄存器中。这是值 0x0800(请记住,网络数据始终是大端字节序)。第二条指令是jeq #0x800,它将立即数与 A 寄存器中的值进行比较。如果它们相等,则跳转到指令 2,否则跳转到指令 5。以太网帧中该偏移处的值 0x800 指定 IPv4 协议。由于比较结果为 true,因此代码现在跳转到指令 2。如果有效负载不是 IPv4,则会跳转到指令 5。

\n

指令 2(第三条)是ld [30]。这会将偏移量为 30 处的整个 4 字节字加载到 A 寄存器中。在我们的以太网帧中,这是 0xc0a80101。下一条指令jeq #0xc0a80101会将立即数与 A 寄存器的内容进行比较,如果为 true,则跳转到 4,否则跳转到 5。该值是目标地址(0xc0a80101 是 192.168.1.1 的大端表示)。这些值确实匹配,因此程序计数器现在设置为 4。

\n

指令 4 是ret #262144. 这将终止 BPF 程序并将整数 262144 返回给调用程序。在这种情况下,这告诉调用程序tcpdump该数据包已被过滤器捕获,因此它从内核请求数据包的内容,更彻底地对其进行解码,并将信息写入您的终端。如果目标地址与过滤器正在寻找的地址不匹配或者协议类型不是 IPv4,则代码​​将跳转到指令 5,在那里它会遇到ret #0。这将在没有匹配的情况下终止。

\n

如果数据包中偏移量 12 处的半字是 0x800 并且偏移量 30 处的字是 0xc0a80101,这只是返回 262144 的一种方法,否则返回 0。因为这一切都是在内核中完成的(可选地在由 JIT 引擎转换为本机机器代码之后),所以不需要昂贵的上下文切换或在内核空间和用户空间之间传递缓冲区,因此过滤器速度很快

\n

更高级的例子

\n

BPF 代码不限于由tcpdump. 许多其他实用程序可以使用它。您甚至可以使用该xt_bpf模块创建带有 BPF 过滤器的 iptables 规则!但是,在生成字节码时必须小心,tcpdump -ddd因为它期望使用第 2 层标头,而 iptables 则不会。为了使它们兼容,您必须调整偏移量。

\n

此外,还提供了许多辅助函数,这些函数提供无法通过读取原始数据包内容获得的信息,例如数据包长度、有效负载起始偏移、接收数据包的 CPU、NetFilter 标记等。来自过滤器文档:

\n
\n

Linux 内核还有几个 BPF 扩展,它们与 \xe2\x80\x9coverloading\xe2\x80\x9d 的加载指令类一起使用,k 参数带有负偏移量 + 特定扩展偏移量。这种 BPF 扩展的结果被加载到 A 中。

\n
\n

支持的 BPF 扩展有:

\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
扩大描述
skb->len
原型skb->协议
类型skb->pkt_type
有效负载起始偏移
伊菲迪克斯skb->dev->ifindex
恩拉类型为 X 且偏移量为 A 的 Netlink 属性
恩兰类型 X 的嵌套 Netlink 属性,偏移量 A
标记skb->标记
队列skb->队列映射
哈型skb->dev->类型
接收哈希值skb->哈希
中央处理器raw_smp_processor_id()
vlan_tciskb_vlan_tag_get(skb)
vlan_availskb_vlan_tag_present(skb)
vlan_tpidskb->vlan_proto
兰特prandom_u32()
\n
\n

例如,要匹配 CPU 3 上收到的所有数据包,您可以执行以下操作:

\n
    ld #cpu\n    jneq #3, drop\n    ret #262144\ndrop:\n    ret #0\n
Run Code Online (Sandbox Code Playgroud)\n

请注意,这是使用与 兼容的 BPF 汇编语法bpf_asm,而此处的其他汇编列表使用的是tcpdump语法。主要区别在于前者的语法使用命名标签,而后者的 BPF 语法使用行号标记每条指令。该程序集转换为以下字节码(逗号分隔指令):

\n
4,32 0 0 4294963236,21 0 1 1,6 0 0 262144,6 0 0 0,\n
Run Code Online (Sandbox Code Playgroud)\n

然后可以将其与模块一起iptables使用xt_bpf

\n
iptables -A INPUT -m bpf --bytecode "4,32 0 0 4294963236,21 0 1 1,6 0 0 262144,6 0 0 0," -j CPU3\n
Run Code Online (Sandbox Code Playgroud)\n

这将跳转到CPU3该 CPU 上收到的任何数据包的目标链。

\n

如果这看起来很强大,请记住这都是 cBPF。虽然cBPF在内部被翻译成eBPF,但是与原始eBPF相比,这一切都算不了什么

\n

了解更多信息

\n

我强烈建议您阅读这篇文章以了解如何tcpdump使用 cBPF。

\n

读完后,请阅读有关如何将表达式转换为字节码的解释tcpdump

\n

如果您想了解有关它的其他所有内容,您可以随时查看源代码

\n

  • 很好的答案!然而,tcpdump 生成的字节码是 cBPF(经典 BPF),而不是 eBPF。即使 eBPF 源自 cBPF,它们也是两个不同的字节码。您链接到的文档仅讨论了 eBPF,但[那个](https://www.kernel.org/doc/Documentation/networking/filter.txt) 讨论了两者。 (3认同)