http://arthurchiao.art/blog/linux-socket-filtering-aka-bpf-zh/
本文翻译自 2021 年 Linux 5.10
内核文档: Linux Socket Filtering aka Berkeley Packet Filter (BPF), 文档源码见 Documentation/networking/filter.rst。
Linux Socket Filtering (LSF) 是最初将 BSD 系统上的数据包过滤技术 BPF(伯克利包过滤器)移植到 Linux 系统时使用的名称,但后来大家还是更多称呼其为 BPF(aka
:as known as)。本文介绍了 Linux BPF 的一些 底层设计和实现(包括 cBPF 和 eBPF),可作为 Cilium:BPF 和 XDP 参考指南(2021) 的很好补充,这两篇可能是目前除了内核源码之外,学习 BPF 的最全/最好参考。 本文适合有一定 BPF 经验的开发者阅读,不适合初学者。
由于内核文档更新不是非常及时,文中部分内容已经与 5.10 代码对不上,因此(少量) 过时内容在翻译时略去了。另外,为文中的大部分 BPF 汇编 / x86_64 汇编加了注释, 并插入了一些 5.10 代码片段或链接,方便更深入理解。
由于译者水平有限,本文不免存在遗漏或错误之处。如有疑问,请查阅原文。
以下是译文。
libpcap
过滤 socket 流量
map_lookup_elem()
传递了非法的 map_fd
map_lookup_elem()
的返回值是否为空就开始使用sk_lookup_tcp()
,未检查返回值就直接将其置 NULLsk_lookup_tcp()
但未检查返回值是否为空SPDX-License-Identifier: GPL-2.0
Linux Socket Filtering (LSF) 从 Berkeley Packet Filter(BPF)衍生而来。 虽然 BSD 和 Linux Kernel filtering 有一些重要不同,但在 Linux 语境中提到 BPF 或 LSF 时, 我们指的是 Linux 内核中的同一套过滤机制。
BPF 允许用户空间程序向任意 socket attach 过滤器(filter), 对流经 socket 的数据进行控制(放行或拒绝)。 LSF 完全遵循了 BSD BPF 的过滤器代码结构(filter code structure),因此实现过滤器时, BSD bpf.4 manpage 是很好的参考文档。
但 Linux BPF 要比 BSD BPF 简单很多:
SO_ATTACH_FILTER
选项将其发送到内核;ATTACH
/DETACH
/LOCK
操作SO_ATTACH_FILTER
用于将 filter attach 到 socket。
SO_DETACH_FILTER
用于从 socket 中 detach 过滤器。
但这种情况可能比较少,因为关闭一个 socket 时,attach 在上面的所有 filters 会被 自动删除。 另一个不太常见的场景是:向一个已经有 filter 的 socket 再 attach 一个 filter: 内核负责将老的移除,替换成新的 —— 只要新的过滤器通过了校验,否则还是老的在工作。
SO_LOCK_FILTER
选项支持将 attach 到 socket 上的 filter 锁定。 一旦锁定之后,这个过滤器就不能被删除或修改了。这样就能保证下列操作之后:
这个 filter 就会一直运行在该 socket 上,直到后者被关闭。
BPF 模块的最大用户可能就是 libpcap
。例如,对于高层过滤命令 tcpdump -i em1 port 22
,
SO_ATTACH_FILTER
就能加载到内核;-ddd
参数,可以 dump 这条命令对应的字节码:tcpdump -i em1 port 22 -ddd
。虽然我们这里讨论的都是 socket,但 Linux 中 BPF 还可用于很多其他场景。例如
xt_bpf
cls_bpf
最初的 BPF 论文:
Steven McCanne and Van Jacobson. 1993. The BSD packet filter: a new architecture for user-level packet capture. In Proceedings of the USENIX Winter 1993 Conference Proceedings on USENIX Winter 1993 Conference Proceedings (USENIX’93). USENIX Association, Berkeley, CA, USA, 2-2. [http://www.tcpdump.org/papers/bpf-usenix93.pdf]
struct sock_filter
要开发 cBPF 应用,用户空间程序需要 include <linux/filter.h>
,其中定义了下面的结构体:
struct sock_filter { /* Filter block */
__u16 code; /* Actual filter code */
__u8 jt; /* Jump true */
__u8 jf; /* Jump false */
__u32 k; /* Generic multiuse field */
};
这个结构体包含 code
jt
jf
k
四个字段。 jt
和 jf
是 jump offset,k
是一个 code
可以使用的通用字段。
struct sock_fprog
要实现 socket filtering,需要通过 setsockopt(2)
将一个 struct sock_fprog
指针传递给内核(后面有例子)。 这个结构体的定义:
struct sock_fprog { /* Required for SO_ATTACH_FILTER. */
unsigned short len; /* Number of filter blocks */
struct sock_filter __user *filter;
};
libpcap
过滤 socket 流量setsockopt()
将字节码 attach 到 socket两个结构体 struct sock_filter
和 struct sock_fprog
在前一节介绍过了:
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <linux/if_ether.h>
/* ... */
/* From the example above: tcpdump -i em1 port 22 -dd */
struct sock_filter code[] = {
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 8, 0x000086dd },
{ 0x30, 0, 0, 0x00000014 },
{ 0x15, 2, 0, 0x00000084 },
{ 0x15, 1, 0, 0x00000006 },
{ 0x15, 0, 17, 0x00000011 },
{ 0x28, 0, 0, 0x00000036 },
{ 0x15, 14, 0, 0x00000016 },
{ 0x28, 0, 0, 0x00000038 },
{ 0x15, 12, 13, 0x00000016 },
{ 0x15, 0, 12, 0x00000800 },
{ 0x30, 0, 0, 0x00000017 },
{ 0x15, 2, 0, 0x00000084 },
{ 0x15, 1, 0, 0x00000006 },
{ 0x15, 0, 8, 0x00000011 },
{ 0x28, 0, 0, 0x00000014 },
{ 0x45, 6, 0, 0x00001fff },
{ 0xb1, 0, 0, 0x0000000e },
{ 0x48, 0, 0, 0x0000000e },
{ 0x15, 2, 0, 0x00000016 },
{ 0x48, 0, 0, 0x00000010 },
{ 0x15, 0, 1, 0x00000016 },
{ 0x06, 0, 0, 0x0000ffff },
{ 0x06, 0, 0, 0x00000000 },
};
struct sock_fprog bpf = {
.len = ARRAY_SIZE(code),
.filter = code,
};
sock =