BPF的理解

BPF的理解

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

tcpdump -i eth0 "dst host 192.168.1.0"

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

答案1

什么是 BPF?

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

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

eBPF 计划的限制

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

  • 该计划包含不超过4096针对非特权用户的总共说明。

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

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

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

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

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

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

验证者是相当复杂并做了更多的事情,尽管它本身就是严肃的 安全 虫子,至少当bpf(2)系统调用不是对非特权用户禁用

查看代码

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

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

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

理解代码

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

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

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

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

aa aa aa aa aa aa bb bb bb bb bb bb 08 00 45 00
00 1c 77 71 40 00 40 01 3f 92 c0 a8 01 8e c0 a8
01 01 08 00 c1 c0 36 0e 00 01

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

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

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

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

更高级的例子

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

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

Linux 内核还有一些 BPF 扩展,它们通过使用负偏移量 + 特定扩展偏移量“重载”k 参数来与加载指令类一起使用。这种 BPF 扩展的结果被加载到 A 中。

支持的 BPF 扩展有:

扩大 描述
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_tci skb_vlan_tag_get(skb)
vlan_avail skb_vlan_tag_present(skb)
vlan_tpid skb->vlan_proto
兰特 prandom_u32()

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

    ld #cpu
    jneq #3, drop
    ret #262144
drop:
    ret #0

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

4,32 0 0 4294963236,21 0 1 1,6 0 0 262144,6 0 0 0,

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

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

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

如果这看起来很强大,请记住这都是 cBPF。虽然cBPF内部被翻译成eBPF,但这一切都是没有什么与原始 eBPF 的功能相比!

了解更多信息

我强烈推荐你阅读本文了解如何tcpdump使用 cBPF。

读完后,请阅读这个解释如何tcpdump将表达式转换为字节码。

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

答案2

eBPF 程序不在用户空间中执行。相反,应用程序创建一个 eBPF 程序并将其发送到内核,然后由内核执行。

为了补充@forest的好答案,我们也许可以详细说明这些程序是如何执行的。

tcpdump 使用的 cBPF 几乎没有钩子:它可以连接到插座, 为了当数据包到达时运行(这就是 tcpdump 的作用,过滤套接字上收到的数据包,并仅将所需的数据包传递到用户空间),或者它们可以附加到seccomp 钩子,以便对系统调用及其参数进行一些过滤。

的重要特征之一电子BPF是它可以附加到更广泛的钩子选择在内核中(尽管它不执行 seccomp)。对于网络来说,有插座, 但是也TC(交通控制)挂钩,XDP(用于快速网络的驱动程序级挂钩),或其他一些。关于你的问题:程序也可以附加到跟踪点在内核中(某些特定函数上的预定义挂钩,例如系统调用或内核中的“重要”函数),或在内核探测器上(k探针),使他们能够跟踪任何函数在内核中(前提是它没有在编译时内联)。然后其他类型存在,例如用于安全用例的 LSM。

追踪通常依靠跟踪点或 kprobes将 eBPF 程序附加到函数,并在每次在内核中调用该函数时运行它。程序可以访问函数的参数或(如果它附加在出口处)返回值。通过使用地图,特殊的内核内存区域,例如数组或哈希图,专用于共享数据在 eBPF 程序和/或用户空间之间,程序可以收集指标或共享状态连续运行之间。

例如,打开监听open()from BCC 将附加到 和系统调用的入口和出口处的跟踪点openat()。在入口处,它收集正在打开的文件的路径以及打开该文件的进程的PID,并将其存储在哈希映射中。当系统调用退出时,第二个探测器收集返回值,并根据 PID 更新哈希映射中的相关条目。然后,用户空间可以收集并转储哈希映射中的所有条目,以显示哪些进程打开了哪些文件以及返回值是什么。

https://ebpf.io/是开始使用 eBPF 的好地方。

相关内容