├── ebpf-guide ├── eBPF基础 │ ├── BPF学习资料.md │ ├── 为什么你应该关注eBPF.md │ ├── BPF(eBPF)最初是来源于网络流量过滤的需求.md │ ├── 计算机领域最有前途基础软件技术eBPF.md │ ├── eBPF技术背景.md │ ├── 初识eBPF,eBPF发展现状.md │ ├── eBPF概念和基本原理.md │ ├── eBPF的实现原理.md │ ├── eBPF基本架构及使用.md │ ├── eBPF解读-基础篇.md │ └── eBPF完全入门指南.md ├── eBPF实战应用 │ ├── eBPF技术的5G实现思路.md │ ├── eBPF揭示隐藏的超能力.md │ ├── eBPF实战.md │ └── eBPF快速定位网络抖动.md └── eBPF高级 │ ├── eBPF允许您在内核中编写自定义代码.md │ ├── eBPF是一个基于寄存器的虚拟机.md │ ├── eBPF指令集.md │ ├── 区分三种类型的eBPF重定向.md │ └── eBPF捕获生产流量的实用指南.md ├── .gitignore └── README.md /ebpf-guide/eBPF基础/BPF学习资料.md: -------------------------------------------------------------------------------- 1 | # BPF学习资料 2 | 3 | ### 书籍 4 | 5 | - 《Linux内核观测技术BPF》 6 | - 《BPF之巅:洞悉Linux系统和应用性能》 7 | - 《Systems Performance》 8 | - 《BPF Performance Tools》 9 | 10 | ### Brendan Gregg大神的个人网站 11 | 12 | https://www.brendangregg.com/index.html 13 | 14 | ### Github 15 | 16 | Linux基金会的IO Visor项目:https://github.com/iovisor https://github.com/zoidbergwill/awesome-ebpf 17 | 18 | ### 网站 19 | 20 | Cilium eBPF:https://ebpf.io 21 | 22 | ### BPF原始论文 23 | 24 | https://www.tcpdump.org/papers/bpf-usenix93.pdf 25 | 26 | ### 本文原直播视频 27 | 28 | https://www.bilibili.com/video/BV1LX4y157Gp?share_source=copy_web -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | *.idea 4 | 5 | 6 | .DS_Store 7 | 8 | 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | *$py.class 13 | 14 | # C extensions 15 | *.so 16 | D:\temporary 17 | # Distribution / packaging 18 | .Python 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | wheels/ 31 | *.egg-info/ 32 | 33 | *.egg 34 | MANIFEST 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # celery beat schedule file 85 | celerybeat-schedule 86 | 87 | # SageMath parsed files 88 | *.sage.py 89 | 90 | # Environments 91 | .env 92 | .venv 93 | env/ 94 | venv/ 95 | ENV/ 96 | env.bak/ 97 | venv.bak/ 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # mypy 110 | .mypy_cache/ 111 | 112 | venv/bin/python3 113 | venv/bin/python 114 | venv/bin/activate 115 | *.zip 116 | *.aab 117 | *.apk 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eBPF指南 2 | ### ebpf指南开源文档: 3 | #### eBPF基础 4 | #### eBPF高级 5 | #### eBPF实战应用 6 | ## 指南介绍 7 | - 欢迎Star,Fork,同时也欢迎您的参与 8 | - 更优雅的阅读方式:开源文档github.io[程序员开发指南 Developer Guide for Programmers](https://guide.996station.com/#/) 9 | - 更多更好的内容,请访问[996station技术栈](https://www.996station.com) 10 | 11 | ## 指南目录 12 | - **eBPF指南** 13 | - eBPF基础 14 | - [eBPF完全入门指南](https://guide.996station.com/#/ebpf-guide/eBPF基础/eBPF完全入门指南.md) 15 | - [BPF学习资料](https://guide.996station.com/#/ebpf-guide/eBPF基础/BPF学习资料.md) 16 | - [eBPF基本架构及使用](https://guide.996station.com/#/ebpf-guide/eBPF基础/eBPF基本架构及使用.md) 17 | - [eBPF技术背景](https://guide.996station.com/#/ebpf-guide/eBPF基础/eBPF技术背景.md) 18 | - [eBPF概念和基本原理](https://guide.996station.com/#/ebpf-guide/eBPF基础/eBPF概念和基本原理.md) 19 | - [eBPF的实现原理](https://guide.996station.com/#/ebpf-guide/eBPF基础/eBPF的实现原理.md) 20 | - [eBPF解读-基础篇](https://guide.996station.com/#/ebpf-guide/eBPF基础/eBPF解读-基础篇.md) 21 | - [为什么你应该关注eBPF](https://guide.996station.com/#/ebpf-guide/eBPF基础/为什么你应该关注eBPF.md) 22 | - [初识eBPF,eBPF发展现状](https://guide.996station.com/#/ebpf-guide/eBPF基础/初识eBPF,eBPF发展现状.md) 23 | - [计算机领域最有前途基础软件技术eBPF](https://guide.996station.com/#/ebpf-guide/eBPF基础/计算机领域最有前途基础软件技术eBPF.md) 24 | - [BPF(eBPF)最初是来源于网络流量过滤的需求](https://guide.996station.com/#/ebpf-guide/eBPF基础/BPF(eBPF)最初是来源于网络流量过滤的需求.md) 25 | - eBPF高级 26 | - [eBPF捕获生产流量的实用指南](https://guide.996station.com/#/ebpf-guide/eBPF高级/eBPF捕获生产流量的实用指南.md) 27 | - [eBPF指令集](https://guide.996station.com/#/ebpf-guide/eBPF高级/eBPF指令集.md) 28 | - [eBPF允许您在内核中编写自定义代码](https://guide.996station.com/#/ebpf-guide/eBPF高级/eBPF允许您在内核中编写自定义代码.md) 29 | - [eBPF是一个基于寄存器的虚拟机](https://guide.996station.com/#/ebpf-guide/eBPF高级/eBPF是一个基于寄存器的虚拟机.md) 30 | - [区分三种类型的eBPF重定向](https://guide.996station.com/#/ebpf-guide/eBPF高级/区分三种类型的eBPF重定向.md) 31 | - eBPF实战应用 32 | - [eBPF实战](https://guide.996station.com/#/ebpf-guide/eBPF实战应用/eBPF实战.md) 33 | - [eBPF快速定位网络抖动](https://guide.996station.com/#/ebpf-guide/eBPF实战应用/eBPF快速定位网络抖动.md) 34 | - [eBPF技术的5G实现思路](https://guide.996station.com/#/ebpf-guide/eBPF实战应用/eBPF技术的5G实现思路) 35 | - [eBPF揭示隐藏的超能力](https://guide.996station.com/#/ebpf-guide/eBPF实战应用/eBPF揭示隐藏的超能力.md) 36 | 37 | -------------------------------------------------------------------------------- /ebpf-guide/eBPF基础/为什么你应该关注eBPF.md: -------------------------------------------------------------------------------- 1 | # 为什么你应该关注eBPF 2 | 3 | [KingSun](https://www.996station.com/author/kingsun) 4小时 前 4 | 5 | 6 | 7 | 虽然它远非主流主题,但我们已经从我们关注的聪明人那里听到了大量关于 eBPF 的讨论。在 RedMonk,当新兴技术引起我们尊重的人的兴趣时,我们学会了注意,因为从历史上看,这种模式往往表明相关性并暗示未来重要性的潜力。事实上,在这种情况下,所讨论的技术与 DTrace 相关的事实只会给我们带来更多的风险。 8 | 9 | 这是一个快速的、不全面的 eBPF 入门读物,适合好奇的人: 10 | 11 | - eBPF[代表什么](https://ebpf.io/what-is-ebpf/)。它曾经是 Extended Berkeley Packet Filter 的首字母缩写词,但根据文档,它“不再是任何东西的首字母缩写词”。 12 | - eBPF 允许您在 Linux 内核中运行事件驱动程序。程序员可以在沙盒内核环境中运行自定义字节码,而无需直接修改内核源代码本身。 13 | - eBPF 被认为比 Linux 可加载内核模块 (LKM) 更安全,因为在代码执行之前必须通过额外的安全检查和验证。 14 | - 事件可以由各种内核挂钩驱动。触发 eBPF 程序的常见内核挂钩包括系统调用、网络事件(如网络包到达)、对内核中特定函数的调用以及被命中的跟踪点。 15 | - 人们通常不会手写字节码,通常它是从 C 或 Rust 编译而来的。eBPF 程序在被触发后被 JIT 编译为机器码。 16 | - eBPF[与 DTrace 相关](https://www.infoq.com/articles/gentle-linux-ebpf-introduction/)。 17 | 18 | *更正*:这篇文章之前说 eBPF 受到 DTrace 的启发,但 Brendan Gregg 在我发表后友善地提供了以下更正。非常感谢他的评论,我在这里分享这些评论是为了向其他人提供此背景信息: 19 | 20 | > eBPF 并没有受到 DTrace 的启发;它起源于软件定义网络(在此之前,BPF 本身是用于数据包过滤的)。早期 eBPF 的共同创建者(Alexei Starovoitov,然后在 SDN 初创公司 PLUMgrid 工作)意识到它不仅可以用于实现 SDN,而且我参与使用 eBPF 重新实现我的 DTrace 工具。我们确实有一个名为 bpftrace 的受 DTrace 启发的前端,但那是几年后才出现的。换句话说,DTrace 可以作为一个 eBPF 程序来实现,但 eBPF 本身要大得多。eBPF 不是跟踪器——它是内核执行环境。 21 | 22 | - eBPF 的主要用例分为三个总体类别:网络、可观察性和安全性。 23 | 24 | eBPF 绝对是一项我们将继续密切关注的技术。特别是随着[Istio Ambient Mesh](https://istio.io/latest/blog/2022/introducing-ambient-mesh/)等技术的引入,eBPF 有望在生态系统中扮演越来越重要的角色。 25 | 26 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126083840411.png?imageView2/0/format/webp/q/75) 27 | 28 | ## 其他资源: 29 | 30 | - Corey Quinn 对[Liz Rice 进行了精彩的采访,](https://share.transistor.fm/s/cba9541b)讨论了 eBPF 和 Cilium 31 | - [eBPF 文档](https://ebpf.io/what-is-ebpf/) 32 | - 来自 Tigera 的指南:[eBPF 解释](https://www.tigera.io/learn/guides/ebpf/) 33 | - InfoQ:[对 eBPF 的简要介绍](https://www.infoq.com/articles/gentle-linux-ebpf-introduction/) 34 | - [Brendan Gregg 的推文](https://twitter.com/brendangregg),通常充满了 eBPF 优点 35 | 36 | ## 作者 37 | 38 | Rachel Stephens 39 | 40 | ## 原文链接 41 | 42 | -------------------------------------------------------------------------------- /ebpf-guide/eBPF实战应用/eBPF技术的5G实现思路.md: -------------------------------------------------------------------------------- 1 | # eBPF技术的5G实现思路 2 | 3 | 在讨论利用eBPF程序的具体好处和方法时,很容易直接进入技术兔子洞。该技术非常详细,可用于各种用例。因此,谈话可能会很快围绕细节展开,同时掩盖技术的基本要素。与任何较新的技术一样,设置水平并退后一步讨论基础知识通常很有帮助。这篇文章将用于实现这一目标——提供对eBPF程序基础知识的高级视图,更具体地说,是用于 5G SA 可见性的eBPF程序。 4 | 5 | *eBPF**是关于内核的* 6 | 7 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126085232293.png?imageView2/0/format/webp/q/75) 8 | 9 | [正如我们在之前的博文中所讨论的那样,](https://www.mantisnet.com/blog/ebpf-v-sidecar-containers-5g-observability) eBPF是一种通过在系统内核级别部署来绑定操作系统原语的方法。 10 | 11 | 右图显示了一个用于监控 5G 资源的示例eBPF程序。请注意,eBPF程序/代理的“插入点”在节点级别,而不是 pod/容器级别(因为其他基于代理的方法,例如 sidecar 容器,依赖于n)。 12 | 13 | eBPF解决方案作为存在于内核空间中的沙盒程序运行,它们不需要对内核进行任何更改。这不是内核模块,而是在用户空间中定义的独立实体,可以将容器环境内省为在内核空间中运行的独立程序。eBPF程序可以通过部署为单个 pod/容器来自省整个节点环境(所有 pod 到 pod 的通信、进程、接口等)。它对内核检测的独特关注是 eBPF 将自己与其他云原生技术区分开来的一种方式。 14 | 15 | eBPF 是关于消息的 16 | 17 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126085239747.png?imageView2/0/format/webp/q/75) 18 | 19 | eBPF程序非常高效。与 sidecar 容器(和vTAP 、span 端口、服务网格等)不同, eBPF程序在内核级别充当事件处理程序——它们不会搅动环境中的每条信息,而是为非常特定的类型设置挂钩交通。 20 | 21 | **在监控程序的上下文中,可以将其视为关注消息与关注数据包**之间的区别。eBPF程序*可以瞄准并观察*基于 HTTP/2 的环境的核心消息传递。eBPF程序为其正在寻找的特定消息/事件(例如,特定于 5G 的 SBI 消息传递)设置挂钩,并在这些特定挂钩上触发以开始监视通信。 22 | 23 | 24 | 25 | 这种方法与前面提到的方法(vTAPS 、sidecars 等)有很大的不同,后者是非常大量的数据包进程,通常处理环境中的所有数据(而不是为非常具体的消息设置挂钩)。这些传统方法需要计算资源来访问和处理所有这些数据包数据,从而导致生产计算资源承受巨大压力。另一方面, eBPF允许使用一种基本方法,该方法基于以下因素将**资源利用率降低一个数量级** 26 | 27 | : - 它关注消息,而不是数据包 28 | \- 它如何访问数据的目标性质 29 | 30 | *eBPF**占用空间小* 31 | 32 | 将监控解决方案部署到分布式 k8s环境时,最大的考虑因素之一是解决方案本身的实际占用空间。使用eBPF程序,代理被部署为daemonsets ,并与生产资源一起自动扩展。这意味着每个节点将有一 (1) 个代理(无论节点是裸机还是基于 VM 都无关紧要)。这些代理可以查看该节点中存在的 pod 和容器内部和之间的所有活动。请看下面的例子,每个节点有 10 个 pod: 33 | 34 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126085245187.png?imageView2/0/format/webp/q/75) 35 | 36 | 与替代方法相比,这种方法显着减少了足迹。eBPF 程序的“部署足迹”远小于边车容器方法。如果使用 sidecar 容器,则必须在上面的示例中部署六十 (60) 个代理,而基于 eBPF 的方法只需要六 (6) 个代理。 37 | 38 | *eBPF 与供应商无关* 39 | 40 | 同样,eBPF程序通过在**节点级别**(linux内核)集成到 k8s 集群来访问数据。eBPF程序不访问容器或 pod 级别的信息。 41 | 42 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126085251177.png?imageView2/0/format/webp/q/75) 43 | 44 | 这导致一个解决方案只需要来自生产系统的一个集成依赖——内核支持 eBPF。实际上,这意味着正在运行的linux内核版本需要是大约在过去 13 年发布的版本(当时eBPF最初 被采用并包含在linux基金会中)。对应用程序层的依赖性为零——这意味着不存在 基于生产环境中存在的供应商的集成依赖性。 45 | 46 | 这在整个云原生环境中非常有益,但在 5G 电信云原生环境中尤其如此,随着时间的推移,服务提供商的趋势是从单一供应商、整体解决方案转向最佳网络功能选择现在有多个供应商出现在 5G 堆栈的各个方面。 47 | 48 | 49 | 50 | ## 作者 51 | 52 | Mike Fecher 53 | 54 | ## 原文链接 55 | 56 | https://www.mantisnet.com/blog/back-to-the-basics-ebpf-fundamentals -------------------------------------------------------------------------------- /ebpf-guide/eBPF基础/BPF(eBPF)最初是来源于网络流量过滤的需求.md: -------------------------------------------------------------------------------- 1 | # BPF(eBPF)最初是来源于网络流量过滤的需求 2 | 3 | 一般我们会听到类似这样的说法:BPF/eBPF 最初是来源于网络流量过滤的需求,但它现在已被扩展到一般的内核观测中。那就先来看看,它为何是产生自网络传输领域的。 4 | 5 | ### **怎样快速过滤** 6 | 7 | 现代网络传输有个特点:流量巨大,因此需要快速过滤。那如何才能快速?比如这样一个过滤器: 8 | 9 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126102459310.png?imageView2/0/format/webp/q/75) 10 | 11 | 如果采用基于传统的 tree 的结构来实现,大致是这样的: 12 | 13 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126102508782.png?imageView2/0/format/webp/q/75) 14 | 15 | 假设被过滤的 packet 是一个 ARP 包或者 IP 包,那么通过这样一种树形结构来判断是很快的,但如果既不是 ARP 也不是 IP 包呢(也就是 P1 和 P2 这两个条件都为 false),那么 AND1 和 AND2 就是没必要的。 16 | 17 | 而 BPF 采用基于 CFG (Control Flow Graph) 的结构实现: 18 | 19 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126102517283.png?imageView2/0/format/webp/q/75) 20 | 21 | 判断从顶点的 P1 开始,当条件为 false 时走右路,true 时走左路,直到抵达末端的 false 或者 true。这样,当 P1 和 P2 都为 false 时,就可以直达最后的 false,而不需要再判断 P3 和 P4。 22 | 23 | 好,接下来再来看看它是如何被应用到内核行为的观测中的。 24 | 25 | ### **过滤的输入 - 前端** 26 | 27 | 观测需要数据,准确的说是你感兴趣的那些数据,那它们来自哪里?内核依靠执行一条条的指令运行,所以可以在执行指令的点位抓取数据。 28 | 29 | 说到如何抓取,不管是静态的的 tracepoint 和 ftrace,还是动态的 kprobe,都可以被视作提供了 event source 的一种 hook 行为,而 perf, systemtap 等工具都依赖于这些作为数据输入的「前端」,在 kernel tracing 中发挥作用的 BPF/eBPF 也不例外。 30 | 31 | ### **过滤的输出 - 后端** 32 | 33 | 需要劫持哪些 hook 点,劫持后需要获取哪些数据,以及之后怎样处理这些数据,就是所谓「后端」做的事了。具体而言,比较简便快速的方法是在用户态编写 perf, systemtap 等对应的脚本来指定。 34 | 35 | 在这一点上,BPF/eBPF 可以说和前两者并没有本质上的区别,它支持用 bpftrace (融合了 awk 和 C 的语法)或者 python (比如 bcc-tools)等语言来编写脚本,然后经过 LLVM 的处理,转化为可以在机器上执行的代码。 36 | 37 | ### **为什么需要虚拟机** 38 | 39 | 只不过,针对 BPF/eBPF,是先转换为面向 BPF 虚拟机的 bytecode。为啥需要一个 VM 呢?其中一个原因是前面说到的 BPF 采用的 CFG 结构,这种结构很适合用 bytecode 的形式来表达。 40 | 41 | 既然是 VM,通常就有自己的一套指令集 (ISA) 和寄存器,且由于最终运行在内核,所以应尽量保持和 Linux 的 calling convention 的兼容,这就要求其功能设计上应尽量保持和真实 CPU 在寄存器/指令集上的接近。 42 | 43 | 44 | 45 | 诞生于上世纪 90 年代初的 BPF 只有 2 个 寄存器,随着 CPU 技术的不断迭代,寄存器已普遍步入了 64 位时代,且产生了一些专门面向多核处理器的新指令。正是由于 BPF 虚拟机和底层 CPU 存在的这种关联,这种寄存器和指令都非常有限的设计,已越来越不能利用现代处理器发展带来的红利。 46 | 47 | 这也是 20 年后的 eBPF 选择在这方面进行扩展的原因,其中就寄存器这一块,已经被扩展到了 10 个(包括 R0 到 R9,未将作为只读 frame pointer 的 R10 涵盖在内),基本可以形成和硬件寄存器一对一的映射关系,以 x86-64 体系为例,其对应关系如下: 48 | 49 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126102526928.png?imageView2/0/format/webp/q/75) 50 | 51 | 经过 eBPF 的改良,多种网络过滤的 benchmark 的结果显示(以 3.15 内核为例),其相较 BPF 在性能有了 1.5 到 4 倍的提升。 52 | 53 | 不过其目前在使用上还是存在一些限制的,比如函数参数不能超过 5 个,只允许 1 个返回值(因为只有 rax 作为存放 return value 的寄存器)等。 54 | 55 | 56 | 57 | 既然是 VM,那么 bytecode 还需要转为主机上真实硬件架构(比如 x86)的汇编指令,这里就要用到 JIT 来作为解释器。下图的蓝色箭头部分就展示了上述的这一过程,即 eBPF 中作为 filter 的程序是如何流转和工作的。 58 | 59 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126102533147.png?imageView2/0/format/webp/q/75) 60 | 61 | 内核通过探测点获得了数据后,又该如何传递给用户态呢?答案是存储在 eBPF maps 中。这些 maps 采用 key/value 的形式(比如组织为 hash 表),因而可以包含不同类型的数据,这也是 "eBPF" 里这个 "e" 所代表的 extended 的一个体现。 62 | 63 | 上图的红色箭头部分,呈现的正是 eBPF maps 作为用户态和内核态共享数据的方式(当然它也可以作为 eBPF 的内核态程序之间进行数据交互的渠道)。 64 | 65 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126102539348.png?imageView2/0/format/webp/q/75) 66 | 67 | 这里就要说到 eBPF 的一个优势了。相比于 perf 需要将采集数据存储在 buffer 里,然后传送到用户态解析的方式不同,eBPF 支持在内核态直接处理一些数据(比如生成直方图),这可以减少用户态和内核态的数据拷贝,有利于降低观测工具带来的开销,因而更适合作为生产环境的 performance tool【注-1】。 68 | 69 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126102545474.png?imageView2/0/format/webp/q/75) 70 | 71 | 其适用于生产环境的另一个重要原因是它的安全性。VM 为 eBPF 程序的运行提供了一个类似 sandbox 的环境,是保障其不会造成内核 crash 的基石之一,但如果希望 eBPF 像 systemtap 那样,可以通过修改函数的返回值来实现 fault injection 的功能,那么很可能就会破坏这一层保障,这也算是 production safe 对 eBPF 适用面扩展的一个掣肘吧。 72 | 73 | 注-1:自 eBPF 出现后,BPF 已被替代,所以目前说到 "BPF",有时就是指 "eBPF"。 74 | 75 | ## 作者 76 | 77 | 术道经纬 78 | 79 | ## 原文链接 80 | 81 | https://mp.weixin.qq.com/s/udpHAaB27DpVPm1ynvUxuA -------------------------------------------------------------------------------- /ebpf-guide/eBPF基础/计算机领域最有前途基础软件技术eBPF.md: -------------------------------------------------------------------------------- 1 | # 计算机领域最有前途基础软件技术eBPF 2 | 3 | 4 | 5 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126101021927.png?imageView2/0/format/webp/q/75) 6 | 7 | 如果非要说当前计算机领域最有前途的两个基础软件技术,那非eBPF和wasm莫属了。 8 | 9 | ------ 10 | 11 | ### **什么是eBPF?** 12 | 13 | Linux内核一直是实现监视/可观察性,网络和安全性的理想场所。不幸的是,这通常是不切实际的,因为它需要更改内核源代码或加载内核模块,并导致彼此堆叠的抽象层。eBPF是一项革命性的技术,可以在Linux内核中运行沙盒程序,而无需更改内核源代码或加载内核模块。通过使Linux内核可编程,基础架构软件可以利用现有的层,从而使它们更加智能和功能丰富,而无需继续为系统增加额外的复杂性层。 14 | 15 | eBPF导致了网络,安全性,应用程序配置/跟踪和性能故障排除等领域的新一代工具的开发,这些工具不再依赖现有的内核功能,而是在不影响执行效率或安全性的情况下主动重新编程运行时行为。 16 | 17 | 18 | 19 | 如果直接解释eBPF,有点不明所以。那我们就看看有哪些基于eBPF的工程,这些工程或许你已经知道,或是已经经常使用,也许你会明白eBPF距离我们并不遥远。 20 | 21 | ------ 22 | 23 | ### **基于eBPF的项目** 24 | 25 | **1:bcc** 26 | 27 | BCC是用于创建基于eBPF的高效内核跟踪和操作程序的工具包,其中包括一些有用的命令行工具和示例。BCC简化了用C进行内核检测的eBPF程序的编写,包括LLVM的包装器以及Python和Lua的前端。它还提供了用于直接集成到应用程序中的高级库。 28 | 29 | **2:bpftrace** 30 | 31 | bpftrace是Linux eBPF的高级跟踪语言。它的语言受awk和C以及DTrace和SystemTap等以前的跟踪程序的启发。bpftrace使用LLVM作为后端将脚本编译为eBPF字节码,并利用BCC作为与Linux eBPF子系统以及现有Linux跟踪功能和连接点进行交互的库。 32 | 33 | **3:Cilium** 34 | 35 | Cilium是一个开源项目,提供基于eBPF的联网,安全性和可观察性。它是从头开始专门设计的,旨在将eBPF的优势带入Kubernetes的世界,并满足容器工作负载的新可伸缩性,安全性和可见性要求。 36 | 37 | **4:Falco** 38 | 39 | Falco是一种行为活动监视器,旨在检测应用程序中的异常活动。Falco在eBPF的帮助下审核Linux内核层的系统。它使用其他输入流(例如容器运行时度量标准和Kubernetes度量标准)丰富了收集的数据,并允许连续监视和检测容器,应用程序,主机和网络活动。 40 | 41 | **5:Katran** 42 | 43 | Katran是一个C ++库和eBPF程序,用于构建高性能的第4层负载平衡转发平面。Katran利用Linux内核中的XDP基础结构来提供用于快速数据包处理的内核功能。它的性能与NIC接收队列的数量成线性比例,并且使用RSS友好的封装转发到L7负载平衡器。 44 | 45 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126113056551.png?imageView2/0/format/webp/q/75) 46 | 47 | **6:Sysdig** 48 | 49 | Sysdig是提供深层系统可见性的简单工具,并具有对容器的原生支持。 50 | 51 | 其他基于eBPF技术的项目还有很多,比如kubectl-trace ,ply 等,这里不再赘述。 52 | 53 | ------ 54 | 55 | ### **如何编写一个eBPF程序?** 56 | 57 | 在很多情况下,不是直接使用eBPF,而是通过Cilium,bcc或bpftrace等项目间接使用eBPF,这些项目在eBPF之上提供了抽象,并且不需要直接编写程序,而是提供了指定基于意图的定义的功能,然后使用eBPF实施。 58 | 59 | 如果不存在更高级别的抽象,则需要直接编写程序。Linux内核希望eBPF程序以字节码的形式加载。虽然当然可以直接编写字节码,但更常见的开发实践是利用LLVM之类的编译器套件将伪C代码编译为eBPF字节码。 60 | 61 | 在编写eBPF程序之前,需要简单了解几个概念。 62 | 63 | 1)map(映射) :BPF最令人着迷的方面之一是,内核上运行的代码和加载了该代码的程序可以在运行时使用消息传递相互通信。 64 | 65 | BPF映射是驻留在内核中的键/值存储。任何BPF程序都可以访问它们。在用户态中运行的程序也可以使用文件描述符访问这些映射。只要事先正确指定数据大小,就可以在映射中存储任何类型的数据。内核将键和值视为二进制 blobs,它并不关心您在映射中保留的内容。 66 | 67 | 68 | 69 | BPF验证程序包括多种保护措施,以确保您创建和访问映射的方式是安全的。当我们解释如何访问这些映射中的数据时,我们也将解释这些保护措施。 70 | 71 | 当然BPF映射类型有很多,比如哈希表映射,数组映射,Cgroup 数组映射等,分别满足不同的场景。 72 | 73 | 2)验证器:BPF验证程序也是在您的系统上运行的程序,因此,对其进行严格审查是确保其正确执行工作的目标。 74 | 75 | 验证程序执行的第一项检查是对VM即将加载的代码的静态分析。第一次检查的目的是确保程序有预期的结果。为此,验证程序将使用代码创建有向循环图(DAG)。验证程序分析的每个指令将成为图中的一个节点,并且每个节点都链接到下一条指令。验证程序生成此图后,它将执行深度优先搜索(DFS),以确保程序完成并且代码不包含危险路径。这意味着它将遍历图的每个分支,一直到分支的底部,以确保没有递归循环。 76 | 77 | 这些是验证器在第一次检查期间可能拒绝您的代码的情形,要求有以下几个方面: 78 | 79 | - 该程序不包含控制循环。为确保程序不会陷入无限循环,验证程序会拒绝任何类型的控制循环。已经提出了在BPF程序中允许循环的建议,但是截至撰写本文时,没有一个被采用。 80 | - 该程序不会尝试执行超过内核允许的最大指令数的指令。此时,可执行的最大指令数为4,096。此限制是为了防止BPF永远运行。在第3章,我们讨论如何嵌套不同的BPF程序,以安全的方式解决此限制。 81 | - 该程序不包含任何无法访问的指令,例如从未执行过的条件或功能。这样可以防止在VM中加载无效代码,这也会延迟BPF程序的终止。 82 | - 该程序不会尝试越界。 83 | 84 | 验证者执行的第二项检查是BPF程序的空运行。这意味着验证者将尝试分析程序将要执行的每条指令,以确保它不会执行任何无效的指令。此执行还将检查所有内存指针是否均已正确访问和取消引用。最后,空运行向验证程序通知程序中的控制流,以确保无论程序采用哪个控制路径,它都会到达BPF_EXIT指令。为此,验证程序会跟踪堆栈中所有访问过的分支路径,并在采用新路径之前对其进行评估,以确保它不会多次访问特定路径。经过这两项检查后,验证者认为程序可以安全执行。 85 | 86 | \3) hook :由于eBPF是事件驱动的,所以ebpf是作用于具体的hook的。根据不同的作用,常用的有XDP,trace,套接字等。 87 | 88 | 89 | 90 | 4)帮助函数:eBPF程序无法调用任意内核功能。允许这样做会将eBPF程序绑定到特定的内核版本,并使程序的兼容性复杂化。取而代之的是,eBPF程序可以调用帮助函数,该函数是内核提供的众所周知且稳定的API。 91 | 92 | ------ 93 | 94 | ### **总结** 95 | 96 | 安全,网络,负载均衡,故障分析,追踪等领域都是eBPF的主战场。 97 | 98 | 对于云原生领域,Cilium 已经使用eBPF 实现了无kube-proxy的容器网络。利用eBPF解决iptables带来的性能问题。 99 | 100 | 整个eBPF生态发展比较好,社区已经提供了诸多工具方便大家编写自己的eBPF程序。 101 | 102 | ------ 103 | 104 | ## 作者 105 | 106 | iyacontrol 107 | 108 | ## 原文链接 109 | 110 | https://mp.weixin.qq.com/s/GxjcRzcgPGhzK3Q3shNbLg -------------------------------------------------------------------------------- /ebpf-guide/eBPF高级/eBPF允许您在内核中编写自定义代码.md: -------------------------------------------------------------------------------- 1 | # eBPF允许您在内核中编写自定义代码 2 | 3 | eBPF 非常强大,因为它在所有魔法发生的地方(Linux 内核)根深蒂固。eBPF 允许您在内核中编写自定义代码。 4 | 5 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126085012594.png?imageView2/0/format/webp/q/75) 6 | 7 | ## 什么是 eBPF? 8 | 9 | > eBPF(不再是任何东西的缩写)是一项革命性的技术,起源于 Linux 内核,可以在操作系统内核等特权上下文中运行沙盒程序。它用于安全有效地扩展内核的功能,而无需更改内核源代码或加载内核模块。 10 | 11 | ## eBPF 是如何工作的? 12 | 13 | > eBPF 程序是事件驱动的,当内核或应用程序通过某个挂钩点时运行。预定义的挂钩包括系统调用、函数入口/出口、内核跟踪点、网络事件等。 14 | 15 | [![eBPF 概述由 eBPF.io 提供](https://shortcdn.com/devopsish/ebpf-overview.webp)](https://ebpf.io/) 16 | 17 | eBPF 概述由[eBPF.io](https://ebpf.io/)根据[Creative Commons Attribution 4.0 International License 提供](https://creativecommons.org/licenses/by/4.0/)。 18 | 19 | 关于 eBPF,您应该立即为两个站点添加书签: 20 | 21 | - [https://ebpf.foundation/(Linux](https://ebpf.foundation/)基金会网站) 22 | - [https://ebpf.io](https://ebpf.io/)(由 Daniel Borkmann 运营) 23 | 24 | 在撰写本文时,这两个网站看起来惊人地相似,但运营它们的人却不同。出于“原因”,该`.foundation`网站决定从该`.io`网站的一个分支开始。是的,我知道有几个 SEO 正在读这篇文章,他们刚刚吐了他们选择的饮料。冷静一下。您的设备可能是防水的¯\_(ツ)_/¯。 25 | 26 | 如果您不熟悉[Isovalent](https://isovalent.com/),它是制作企业级[Cilium](https://cilium.io/)产品(Cilium 容器网络接口 (CNI))的人,我的同事[Liz Rice](https://twitter.com/lizrice)和[Duffie Cooley](https://twitter.com/mauilion)就在这里工作。如果您还记得的话,我今年早些时候与他们坐下[来聊了聊他们在 KubeCon EU 2022 之前的计划](https://chrisshort.net/video/aws-container-days-eu-2022-day-3/#cilium-on-eks-anywhere--liz-rice-chief-open-source-officer-isovalent---duffie-cooley-field-cto-isovalent)。Isovalent 网站上的标语是“基于 eBPF 的网络、安全性和可观察性”。您可以使用 eBPF 做很多艰苦的工作。 27 | 28 | 29 | 30 | 如果您像我一样,在添加和删除模块以优化系统或使独特的硬件工作之前一直深入了解内核,您就会知道这通常会非常具有破坏性或破坏性。eBPF 使您能够以新的和令人兴奋的方式处理内核,而无需运行单个`modprobe`命令甚至重新启动。它们通常也比内核模块更安全。[为了保证eBPF 的安全](https://ebpf.io/what-is-ebpf/#ebpf-safety),我们付出了很多努力。 31 | 32 | 您编写的 eBPF 程序会触发 Linux 内核中的不同事件,或者完全阻止它们发生。因此,eBPF 非常强大,因为它在所有魔法发生的地方(Linux 内核)根深蒂固。eBPF 允许您在内核中编写自定义代码。由于活动发生在内核中,它通常使 eBPF 程序快速高效。例如,您编写的程序甚至可以在访问网络堆栈之前拦截网络访问,或者提供有关由哪些程序进行的调用的详细执行信息以实现可观察性。 33 | 34 | 35 | 36 | 这是很多人的学习路径出现分歧的地方。有些人会想阅读所有的东西。好消息是[BPF 和 XDP 参考指南](https://docs.cilium.io/en/latest/bpf/)以及[HOWTO 与 BPF 子系统的交互](https://www.kernel.org/doc/html/latest/bpf/bpf_devel_QA.html)是*非常棒*的深入研究。其他人希望看到一些实现。如果您想挑选,请查看[awesome-ebpf 存储](https://github.com/zoidbergwill/awesome-ebpf)库。想看一些实现吗?首先,我推荐观看[Liz Rice 在 GOTO 2021 上使用 Go 进行 eBPF 编程的初学者指南](https://youtu.be/uBqRv8bDroc)。另外,[eBPF 到底是什么,为什么 Kubernetes 管理员应该关心?](https://www.groundcover.com/blog/what-is-ebpf)如果您在 Kubernetes 上使用 eBPF,这是一个很好的参考。 37 | 38 | ## eBPF 程序 39 | 40 | 什么样的程序可以利用 eBPF?实际上有很多: 41 | 42 | - [pixie](https://github.com/pixie-io/pixie) : Instant Kubernetes-Native Application Observability (aka FM: F'ing Magic) 43 | - [boopkit](https://github.com/kris-nova/boopkit):基于 TCP 的 Linux eBPF 后门。在先前的特权访问上生成反向 shell,RCE。少本金,多东京。 44 | - [Calico](https://projectcalico.docs.tigera.io/about/about-calico):一个开源网络和网络安全解决方案,适用于容器、虚拟机和基于主机的本地工作负载(他们的[eBPF 页面有漂亮的图片](https://projectcalico.docs.tigera.io/about/about-ebpf)) 45 | - [kubectl trace](https://github.com/iovisor/kubectl-trace):kubectl 插件,允许您在 Kubernetes 集群中安排 bpftrace 程序的执行 46 | - [bpftrace](https://bpftrace.org/):Linux 系统的高级跟踪语言 47 | - [Falco](https://falco.org/blog/choosing-a-driver/#ebpf-probe):Falco eBPF 探针在内核模块不受信任或不允许但 eBPF 程序可用的环境中是一个可行的选择 48 | - [SysmonForLinux](https://github.com/Sysinternals/SysmonForLinux) : Sysmon For Linux 是 Windows Sysmon 工具的一个端口,驱动程序被 eBPF 程序替换 49 | - [tracee](https://github.com/aquasecurity/tracee) : 使用 eBPF 的 Linux 运行时安全和取证 50 | - [ebpf-for-windows](https://github.com/microsoft/ebpf-for-windows):在 Windows 之上运行的 eBPF 实现 51 | - [Katran](https://engineering.fb.com/2018/05/22/open-source/open-sourcing-katran-a-scalable-network-load-balancer/):Facebook 创建的网络负载均衡器 52 | 53 | [eBPF Project Landscape](https://ebpf.io/projects)中还有一个可爱的项目列表。 54 | 55 | ## eBPF 值得炒作吗? 56 | 57 | 是的!eBPF 是个好东西,只会随着采用率的提高而改进。我正在等待合适的项目推出上述程序之一,以深入研究性能问题或查看传递给内核的[系统调用。](https://syscall.sh/) 58 | 59 | “任何足够先进的技术都与魔法没有区别”适用于此。但是,eBPF 是一个橡皮锤,你不能用它来解决所有问题。你可以用 eBPF 掩盖很多错误。你可以用它找到附近的任何东西,这是任何人都可以给你的最好的开始。 60 | 61 | 62 | 63 | 如果您想为 eBPF 或 eBPF 开发工具链做出贡献,请随时在[ebpf.io/contribute](https://ebpf.io/contribute)开始您的旅程。感谢 Alexei Starovoitov、Daniel Borkmann 和整个 eBPF 社区创造了如此出色的技术。 64 | 65 | ## 作者 66 | 67 | 克里斯·肖特 68 | 69 | ## 原文链接 70 | 71 | https://chrisshort.net/intro-to-ebpf/ -------------------------------------------------------------------------------- /ebpf-guide/eBPF实战应用/eBPF揭示隐藏的超能力.md: -------------------------------------------------------------------------------- 1 | # eBPF揭示隐藏的超能力 2 | 3 | 如果你是性能工程师/网络工程师甚至是安全工程师,以后接触到eBPF技术的几率是非常大的。eBPF 现在拥有庞大的用户社区,包括像 Meta、Google、Cloudflare 和 Netflix 这样的大公司,他们都在日常运营中使用这项技术。 4 | 5 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126085437312.png?imageView2/0/format/webp/q/75) 6 | 7 | # 序幕 8 | 9 | 让我以一个真实的故事开始博客。一年前,我的一个朋友打电话给我讨论技术(这是我们之间很常见的事情)。我们分享我们每个人在工作场所或我们的任何同行面临的不同技术挑战,这些讨论导致一些信息丰富和创造性的知识共享会议。在这样的讨论中,他描述了在一家大型云提供商工作的堂兄所面临的具体挑战。挑战在于动态限制某些 IP,因为它们提供诸如 DOS(拒绝服务攻击)之类的威胁,我的应用程序开发人员的大脑冲动地回答说这些应该在防火墙级别处理,或者可以编写中间件来检查数据包的来源并为恶意发件人维护黑名单并忽略请求(是的,我来自 NodeJS 和 Go 背景,所以最初的解决方案作为中间件罢工)。我的朋友耐心地解释了这需要执行的规模和性能,这超出了我的理解范围。经过一个新手的疑惑清理会议后,我们同意他想要的规模只能在内核级别实现。我祝他好运(讽刺地)编写内核补丁并提出 PR,希望操作系统维护者将内核补丁包含在即将发布的内核版本中,并且他可以在发布时使用此功能。作为对我讽刺的回应,他与我分享了一篇文章的链接,该文章详细介绍了名为“eBPF”(扩展的 Berkeley 数据包过滤器)的东西。 10 | 11 | 根据 eBPF,你可以直接将代码注入内核,而无需编写补丁,等待 OS 维护者的批准, 12 | 13 | “直接在操作系统内核中运行您的自定义代码” **— LIZ RICE** 14 | 15 | “Linux 的超能力”。—**布伦丹·格雷格斯** 16 | 17 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126085508415.png?imageView2/0/format/webp/q/75) 18 | 19 | 注意:我在参考部分添加了一些视频和博客链接,请查看 eBPF 的一些精彩会议和博客。 20 | 21 | # 历史 22 | 23 | eBPF 于 2014 年问世,在 Linux 内核 3.18 中引入,从而解锁了 Linux 内核的上帝模式。任何阅读此博客的人自然会怀疑这个名字。如果这是一个“扩展的伯克利包过滤器”,那么应该有一个 BPF“伯克利包过滤器”。嗯,你是对的。BSD 包过滤器并不是一个新概念。它是从 90 年代开始的。这颗宝石多年来一直隐藏在雷达之下,Xennails 是真正的创新者。BPF 非常基础,它唯一的工作就是在内核级别过滤数据包,因此得名。 24 | 25 | 注意:我添加了 1992 年 12 月 19 日发表的原始 BPF 论文,这是一篇非常有趣的文章。 26 | 27 | eBPF 已经从 BPF 走了很长一段路,BPF 只是一个数据包过滤实用程序,到考虑内核的微服务架构或他们称之为微内核。如今,所有大规模运营的顶级科技公司每天都在使用 eBPF。现在的 CNCF 社区依靠 eBPF 呼吸和生存,如果您是 DevOps 工程师或系统管理员,您会听说过 cilium 和 Falco,它们在 Kubernetes 用户和基于 eBPF 编写的生产工具中都很流行。2018 年 Linux 宣布将在内核中用 eBPF 版本替换其基于 iptable 的实现(用任何解决方案替换 iptable 会更好),回退和使用 iptables 的缺点超出了本文的范围,请转到参考部分并找到一篇关于它的写得很好的文章。Kubernetes 主要将 iptables 用于以下用例 28 | 29 | - Kube-proxy — 通过 DNAT iptables 规则实现服务和负载均衡的组件 30 | - 大多数 CNI 插件都使用 iptables 作为网络策略 31 | 32 | Cilium 通过消除性能下降的 iptable 使其更加高效。你可以参考[这里](https://cilium.io/blog/2018/04/17/why-is-the-kernel-community-replacing-iptables)的细节。 33 | 34 | # 程序执行 Bozo 的指南 35 | 36 | 要解释 eBPF 的重要性,就需要解释一下程序在 Linux 中是如何执行的,我会尝试从 1000ft 的角度为大家解释。 37 | 38 | 注意:Windows 用户?那么你为什么还要阅读这篇文章,你们没有所有这些很酷的功能。 39 | 40 | Linux内存分为两部分 41 | 42 | 1. 内核空间 43 | 2. 用户空间 44 | 45 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126085457970.png?imageView2/0/format/webp/q/75) 46 | 47 | 图像本身解释了这两者之间的区别。您编写的所有程序都只是作为内核 API 的系统调用的集合。举个例子,通过你最喜欢的编程语言打开一个文件,它只是在内核中转换成一个 fileopen 系统调用。 48 | 49 | 当您的应用程序向内核请求某些东西时,内核空间中的一大块数据经常被复制到用户空间中。我们必须这样做,因为操作系统严格划分内核使用的内存区域,因此不可能简单地向用户空间程序提供指向内核内存某个区域的指针。这被称为“跨越用户/内核边界”,由于复制操作,此类操作可能会对性能产生重大影响。 50 | 51 | 52 | 53 | 虽然系统调用几乎涵盖了所有情况,但有时会出现这还不够的情况,例如当我们需要内核级性能或编写新的驱动程序时,等等。依赖操作系统维护人员为所有这些小用例打补丁是一种浪费时间和一个不可能的过程。这就是 ebpf 发挥作用的地方。 54 | 55 | eBPF 帮助您在用户空间编写程序,这些程序被打包并直接注入内核,这些程序在内核中的 VM 上运行,指令集有限,从而扩展了基本内核模块的功能。 56 | 57 | # eBPF 剖析 58 | 59 | eBPF 是为各种进程运行在内核上运行的自定义代码的规定,例如 60 | 61 | - 可观察性(追踪) 62 | - 调试 63 | - 防火墙 64 | - 负载均衡 65 | - 网络相关活动 66 | 67 | 任何在内核中跟踪过各种程序的人都知道它的难度。Linux 系统中可用的半生不熟的实用程序不足以配置复杂的系统,甚至不足以扩展 perf 工具。 68 | 69 | Ebpf 是事件驱动的,这意味着它会在以下情况下触发 70 | 71 | - 一个系统调用 72 | - 函数入口/出口 73 | - 当数据包进入或离开时 74 | - K 探头或 U 探头 75 | 76 | 这些程序是用一种称为 restricted c 的语言编写的,这种语言具有有限的指令集。BPF 编译器 BCC 将其转换为字节码,并加载到内核中执行。在编译之前运行验证程序以确保没有无限循环或可能导致内核崩溃的永无止境的 I/O 操作。 77 | 78 | # 袖子下的额外技巧 79 | 80 | ebpf 确实是一个您可以随身携带的强大工具。在处理高性能项目时,调整数据包或扩展跟踪功能都可以帮助您更好地观察系统正在发生的事情。尽管现阶段应用开发者遇到ebpf是很无力的,但如果你是性能工程师/网络工程师,甚至是安全工程师,未来遇到ebpf的机率将是千钧一发。 81 | 82 | 在编写 ebpf 程序时有一些注意事项,由于 ebpf 在 sudo 权限下运行,因此已经发生了几次利用 ebpf 的权限升级攻击。在利用内核内存漏洞时,ebpf 程序可以用作强大的辅助工具。Qualys 发现了利用此类漏洞的详细文章,他们有一篇文章,您可以从[此处](https://www.qualys.com/2021/07/20/cve-2021-33909/sequoia-local-privilege-escalation-linux.txt)参考。 83 | 84 | # 结论 85 | 86 | 正如蜘蛛侠电影中所说的“能力越大,责任越大”,当你解锁 Linux 的上帝模式时,你就只能靠自己了,保护你的程序免于破坏整体的守卫现在不可用了。使用 Ebpf 有特定的用例,它不是解决所有性能问题的瑞士刀。社区现在非常庞大,包括像 meta、google、Cloudflare 和 Netflix 这样的大公司每天都在使用这项技术。该技术具有巨大的增长潜力,近年来已经看到了针对 ebpf 爱好者的单独会议。 87 | 88 | 这个博客为不了解这项酷技术的人提供了一个小机会,所以请进行研究。网上有大量关于 ebpf 和基于它构建的开源项目的资源。我将写一篇后续文章,详细介绍如何编写示例 ebpf 程序并执行它。 89 | 90 | # 参考 91 | 92 | - BPF 研究论文链接发表于 1992 年 — https://www.tcpdump.org/papers/bpf-usenix93.pdf 93 | - Brendann Gregg 谈论 eBPF - 94 | - https://www.facebook.com/atscaleevents/videos/1693888610884236/ 95 | - https://www.youtube.com/watch?v=w8nFRoFJ6EQ 96 | - iptables 博客上的 Ebpf — https://cilium.io/blog/2018/04/17/why-is-the-kernel-community-replacing-iptables 97 | - Qualys 漏洞 — https://www.qualys.com/2021/07/20/cve-2021-33909/sequoia-local-privilege-escalation-linux.txt 98 | 99 | 100 | 101 | 102 | 103 | ## 原文链接 104 | 105 | https://coffeebeans-brewinginnovations.medium.com/ebpf-divulging-the-hidden-super-power-181f96291ef7 -------------------------------------------------------------------------------- /ebpf-guide/eBPF基础/eBPF技术背景.md: -------------------------------------------------------------------------------- 1 | # eBPF技术背景 2 | 3 | **发展历史** 4 | 5 | BPF,是类 Unix 系统上数据链路层的一种原始接口,提供原始链路层封包的收发。1992 年,Steven McCanne 和 Van Jacobson 写了一篇名为《The BSD Packet Filter: A New Architecture for User-level Packet Capture[2]》的论文。在文中,作者描述了他们如何在 Unix 内核实现网络数据包过滤,这种新的技术比当时最先进的数据包过滤技术快 20 倍。 6 | 7 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126103914383.png?imageView2/0/format/webp/q/75) 8 | 9 | BPF 在数据包过滤上引入了两大革新: 10 | 11 | - 一个新的虚拟机 (VM) 设计,可以有效地工作在基于寄存器结构的 CPU 之上 12 | - 应用程序使用缓存只复制与过滤数据包相关的数据,不会复制数据包的所有信息,这样可以最大程度地减少BPF 处理的数据 13 | 14 | 由于这些巨大的改进,所有的 Unix 系统都选择采用 BPF 作为网络数据包过滤技术,直到今天,许多 Unix 内核的派生系统中(包括 Linux 内核)仍使用该实现。tcpdump 的底层采用 BPF 作为底层包过滤技术,我们可以在命令后面增加 -d 来查看 tcpdump 过滤条件的底层汇编指令。 15 | 16 | ``` 17 | $ tcpdump -d 'ip and tcp port 8080' 18 | (000) ldh [12] 19 | (001) jeq #0x800 jt 2 jf 12 20 | (002) ldb [23] 21 | (003) jeq #0x6 jt 4 jf 12 22 | (004) ldh [20] 23 | (005) jset #0x1fff jt 12 jf 6 24 | (006) ldxb 4*([14]&0xf) 25 | (007) ldh [x + 14] 26 | (008) jeq #0x1f90 jt 11 jf 9 27 | (009) ldh [x + 16] 28 | (010) jeq #0x1f90 jt 11 jf 12 29 | (011) ret #262144 30 | (012) ret #0 31 | ``` 32 | 33 | 2014 年初,Alexei Starovoitov 实现了 eBPF(extended Berkeley Packet Filter)。经过重新设计,eBPF 演进为一个通用执行引擎,可基于此开发性能分析工具、软件定义网络等诸多场景。 34 | 35 | eBPF 最早出现在 3.18 内核中,此后原来的 BPF 就被称为经典 BPF,缩写 cBPF(classic BPF),cBPF 现在已经基本废弃。现在,Linux 内核只运行 eBPF,内核会将加载的 cBPF 字节码透明地转换成 eBPF 再执行。 36 | 37 | **eBPF 与 cBPF** 38 | 39 | eBPF 新的设计针对现代硬件进行了优化,所以 eBPF 生成的指令集比旧的 BPF 解释器生成的机器码执行得更快。扩展版本也增加了虚拟机中的寄存器数量,将原有的 2 个 32 位寄存器增加到 10 个 64 位寄存器。 40 | 41 | 由于寄存器数量和宽度的增加,开发人员可以使用函数参数自由交换更多的信息,编写更复杂的程序。总之,这些改进使 eBPF 版本的速度比原来的 BPF 提高了 4 倍。 42 | 43 | | 维度 | cBPF | eBPF | 44 | | -------------- | ------------------------- | ------------------------------------------------------------ | 45 | | 内核版本 | Linux 2.1.75(1997 年) | Linux 3.18(2014 年)[4.x for kprobe/uprobe/tracepoint/perf-event] | 46 | | 寄存器数目 | 2 个:A,X | 10个:R0–R9,另外 R10 是一个只读的帧指针R0:eBPF 中内核函数的返回值和退出值R1 - R5:eBF 程序在内核中的参数值R6 - R9:内核函数将保存的被调用者callee保存的寄存器R10:一个只读的堆栈帧指针 | 47 | | 寄存器宽度 | 32 位 | 64 位 | 48 | | 存储 | 16 个内存位: M[0–15] | 512 字节堆栈,无限制大小的 map 存储 | 49 | | 限制的内核调用 | 非常有限,仅限于 JIT 特定 | 有限,通过 bpf_call 指令调用 | 50 | | 目标事件 | 数据包、 seccomp-BPF | 数据包、内核函数、用户函数、跟踪点 PMCs 等 | 51 | 52 | 2014 年 6 月,eBPF 扩展到用户空间,这也成为了 BPF 技术的转折点。正如 Alexei 在提交补丁的注释中写到:「这个补丁展示了 eBPF 的潜力」。当前,eBPF 不再局限于网络栈,已经成为内核顶级的子系统。 53 | 54 | **eBPF 与内核模块** 55 | 56 | 对比 Web 的发展,eBPF 与内核的关系有点类似于 JavaScript 与浏览器内核的关系,eBPF 相比于直接修改内核和编写内核模块提供了一种新的内核可编程的选项。eBPF 程序架构强调安全性和稳定性,看上去更像内核模块,但与内核模块不同,eBPF 程序不需要重新编译内核,并且可以确保 eBPF 程序运行完成,而不会造成系统的崩溃。 57 | 58 | | 维度 | Linux 内核模块 | eBPF | 59 | | ------------------- | ------------------------------------ | ---------------------------------------------- | 60 | | kprobes/tracepoints | 支持 | 支持 | 61 | | 安全性 | 可能引入安全漏洞或导致内核 Panic | 通过验证器进行检查,可以保障内核安全 | 62 | | 内核函数 | 可以调用内核函数 | 只能通过 BPF Helper 函数调用 | 63 | | 编译性 | 需要编译内核 | 不需要编译内核,引入头文件即可 | 64 | | 运行 | 基于相同内核运行 | 基于稳定 ABI 的 BPF 程序可以编译一次,各处运行 | 65 | | 与应用程序交互 | 打印日志或文件 | 通过 perf_event 或 map 结构 | 66 | | 数据结构 | 丰富性 | 一般丰富 | 67 | | 入门门槛 | 高 | 低 | 68 | | 升级 | 需要卸载和加载,可能导致处理流程中断 | 原子替换升级,不会造成处理流程中断 | 69 | | 内核内置 | 视情况而定 | 内核内置支持 | 70 | 71 | **eBPF 架构** 72 | 73 | eBPF 分为用户空间程序和内核程序两部分: 74 | 75 | - 用户空间程序负责加载 BPF 字节码至内核,如需要也会负责读取内核回传的统计信息或者事件详情 76 | - 内核中的 BPF 字节码负责在内核中执行特定事件,如需要也会将执行的结果通过 maps 或者 perf-event 事件发送至用户空间 77 | - 其中用户空间程序与内核 BPF 字节码程序可以使用 map 结构实现双向通信,这为内核中运行的 BPF 字节码程序提供了更加灵活的控制 78 | 79 | eBPF 整体结构图如下: 80 | 81 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126103924639.png?imageView2/0/format/webp/q/75) 82 | 83 | 用户空间程序与内核中的 BPF 字节码交互的流程主要如下: 84 | 85 | 1、使用 LLVM 或者 GCC 工具将编写的 BPF 代码程序编译成 BPF 字节码 86 | 87 | 2、使用加载程序 Loader 将字节码加载至内核 88 | 89 | 3、内核使用验证器(Verfier) 组件保证执行字节码的安全性,以避免对内核造成灾难,在确认字节码安全后将其加载对应的内核模块执行 90 | 91 | 4、内核中运行的 BPF 字节码程序可以使用两种方式将数据回传至用户空间: 92 | 93 | - maps 方式可用于将内核中实现的统计摘要信息(比如测量延迟、堆栈信息)等回传至用户空间; 94 | - perf-event 用于将内核采集的事件实时发送至用户空间,用户空间程序实时读取分析。 95 | 96 | **eBPF 限制** 97 | 98 | eBPF 技术虽然强大,但是为了保证内核的处理安全和及时响应,内核中的 eBPF 技术也给予了诸多限制,当然随着技术的发展和演进,限制也在逐步放宽或者提供了对应的解决方案。 99 | 100 | eBPF 程序不能调用任意的内核参数,只限于内核模块中列出的 BPF Helper 函数,函数支持列表也随着内核的演进在不断增加。 101 | 102 | eBPF 程序不允许包含无法到达的指令,防止加载无效代码,延迟程序的终止。 103 | 104 | eBPF 程序中循环次数限制且必须在有限时间内结束,这主要是用来防止在 kprobes 中插入任意的循环,导致锁住整个系统;解决办法包括展开循环,并为需要循环的常见用途添加辅助函数。Linux 5.3 在 BPF 中包含了对有界循环的支持,它有一个可验证的运行时间上限。 105 | 106 | eBPF 堆栈大小被限制在 MAX_BPF_STACK,截止到内核 Linux 5.8 版本,被设置为 512;参见 include/linux/filter.h[3],这个限制特别是在栈上存储多个字符串缓冲区时:一个char[256]缓冲区会消耗这个栈的一半。目前没有计划增加这个限制,解决方法是改用 bpf 映射存储,它实际上是无限的。 107 | 108 | ``` 109 | /* BPF program can access up to 512 bytes of stack space. */ 110 | #define MAX_BPF_STACK 512 111 | ``` 112 | 113 | eBPF 字节码大小最初被限制为 4096 条指令,截止到内核 Linux 5.8 版本, 当前已将放宽至 100 万指令( BPF_COMPLEXITY_LIMIT_INSNS),参见:include/linux/bpf.h[4],对于无权限的BPF程序,仍然保留4096条限制 ( BPF_MAXINSNS );新版本的 eBPF 也支持了多个 eBPF 程序级联调用,虽然传递信息存在某些限制,但是可以通过组合实现更加强大的功能。 114 | 115 | ``` 116 | #define BPF_COMPLEXITY_LIMIT_INSNS 1000000 /* yes. 1M insns */ 117 | ``` 118 | 119 | ## 120 | 121 | -------------------------------------------------------------------------------- /ebpf-guide/eBPF实战应用/eBPF实战.md: -------------------------------------------------------------------------------- 1 | # eBPF实战 2 | 3 | 在深入介绍 eBPF 特性之前,让我们 Get Hands Dirty,切切实实的感受 eBPF 程序到底是什么,我们该如何开发 eBPF 程序。随着 eBPF 生态的演进,现在已经有越来越多的工具链用于开发 eBPF 程序,在后文也会详细介绍: 4 | 5 | - 基于 bcc 开发:bcc 提供了对 eBPF 开发,前段提供 Python API,后端 eBPF 程序通过 C 实现。特点是简单易用,但是性能较差。 6 | - 基于 libebpf-bootstrap 开发:libebpf-bootstrap 提供了一个方便的脚手架。 7 | - 基于内核源码开发:内核源码开发门槛较高,但是也更加切合 eBPF 底层原理,所以这里以这个方法作为示例。 8 | 9 | **内核源码编译** 10 | 11 | 系统环境如下,采用腾讯云 CVM,Ubuntu 20.04,内核版本 5.4.0。 12 | 13 | ``` 14 | $ uname -a 15 | Linux VM-1-3-ubuntu 5.4.0-42-generic #46-Ubuntu SMP Fri Jul 10 00:24:02 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux 16 | ``` 17 | 18 | 首先安装必要依赖: 19 | 20 | ``` 21 | sudo apt install -y bison build-essential cmake flex git libedit-dev pkg-config libmnl-dev \ 22 | python zlib1g-dev libssl-dev libelf-dev libcap-dev libfl-dev llvm clang pkg-config \ 23 | gcc-multilib luajit libluajit-5.1-dev libncurses5-dev libclang-dev clang-tools 24 | ``` 25 | 26 | 一般情况下推荐采用 apt 方式的安装源码,安装简单而且只安装当前内核的源码,源码的大小在 200M 左右。 27 | 28 | ``` 29 | # apt-cache search linux-source 30 | 31 | # apt install linux-source-5.4.0 32 | ``` 33 | 34 | 源码安装至 /usr/src/ 目录下。 35 | 36 | ``` 37 | $ ls -hl 38 | total 4.0K 39 | drwxr-xr-x 4 root root 4.0K Nov 9 13:22 linux-source-5.4.0 40 | lrwxrwxrwx 1 root root 45 Oct 15 10:28 linux-source-5.4.0.tar.bz2 -> linux-source-5.4.0/linux-source-5.4.0.tar.bz2 41 | $ tar -jxvf linux-source-5.4.0.tar.bz2 42 | $ cd linux-source-5.4.0 43 | 44 | $ cp -v /boot/config-$(uname -r) .config # make defconfig 或者 make menuconfig 45 | $ make headers_install 46 | $ make modules_prepare 47 | $ make scripts # 可选 48 | $ make M=samples/bpf # 如果配置出错,可以使用 make oldconfig && make prepare 修复 49 | ``` 50 | 51 | 编译成功后,可以在 samples/bpf 目录下看到一系列的目标文件和二进制文件。 52 | **Hello World** 53 | 前面说到 eBPF 通常由内核空间程序和用户空间程序两部分组成,现在 samples/bpf 目录下有很多这种程序,内核空间程序以 _kern.c 结尾,用户空间程序以 _user.c 结尾。先不看这些复杂的程序,我们手动写一个 eBPF 程序的 Hello World。 54 | 内核中的程序 hello_kern.c: 55 | 56 | ``` 57 | #include 58 | #include "bpf_helpers.h" 59 | 60 | #define SEC(NAME) __attribute__((section(NAME), used)) 61 | 62 | SEC("tracepoint/syscalls/sys_enter_execve") 63 | int bpf_prog(void *ctx) 64 | { 65 | char msg[] = "Hello BPF from houmin!\n"; 66 | bpf_trace_printk(msg, sizeof(msg)); 67 | return 0; 68 | } 69 | 70 | char _license[] SEC("license") = "GPL"; 71 | ``` 72 | 73 | 函数入口: 74 | 75 | 上述代码和普通的C语言编程有一些区别。 76 | 77 | - 程序的入口通过编译器的 pragama __section("tracepoint/syscalls/sys_enter_execve") 指定的。 78 | - 入口的参数不再是 argc, argv, 它根据不同的 prog type 而有所差别。我们的例子中,prog type 是 BPF_PROG_TYPE_TRACEPOINT, 它的入口参数就是 void *ctx。 79 | 80 | 头文件: 81 | 82 | ``` 83 | #include 84 | ``` 85 | 86 | 这个头文件的来源是kernel source header file 。它安装在 /usr/include/linux/bpf.h中。 87 | 88 | 它提供了bpf 编程需要的很多symbol。例如: 89 | 90 | - enum bpf_func_id 定义了所有的kerne helper function 的id 91 | - enum bpf_prog_type 定义了内核支持的所有的prog 的类型。 92 | - struct __sk_buff 是bpf 代码中访问内核struct sk_buff的接口。 93 | 94 | 等等 95 | 96 | ``` 97 | #include “bpf_helpers.h” 98 | ``` 99 | 100 | 来自libbpf ,需要自行安装。我们引用这个头文件是因为调用了bpf_printk()。这是一个kernel helper function。 101 | 102 | 程序解释: 103 | 104 | 这里我们简单解读下内核态的 ebpf 程序,非常简单: 105 | 106 | - bpf_trace_printk 是一个 eBPF helper 函数,用于打印信息到 trace_pipe (/sys/kernel/debug/tracing/trace_pipe),详见这里[5] 107 | - 代码声明了 SEC 宏,并且定义了 GPL 的 License,这是因为加载进内核的 eBPF 程序需要有 License 检查,类似于内核模块 108 | 109 | 加载 BPF 代码: 110 | 111 | 用户态程序 hello_user.c: 112 | 113 | ``` 114 | #include 115 | #include "bpf_load.h" 116 | 117 | int main(int argc, char **argv) 118 | { 119 | if(load_bpf_file("hello_kern.o") != 0) 120 | { 121 | printf("The kernel didn't load BPF program\n"); 122 | return -1; 123 | } 124 | 125 | read_trace_pipe(); 126 | return 0; 127 | } 128 | ``` 129 | 130 | 在用户态 ebpf 程序中,解读如下: 131 | 132 | - 通过 load_bpf_file 将编译出的内核态 ebpf 目标文件加载到内核 133 | - 通过 read_trace_pipe 从 trace_pipe 读取 trace 信息,打印到控制台中 134 | 135 | 修改 samples/bpf 目录下的 Makefile 文件,在对应的位置添加以下三行: 136 | 137 | ``` 138 | hostprogs-y += hello 139 | hello-objs := bpf_load.o hello_user.o 140 | always += hello_kern.o 141 | ``` 142 | 143 | 重新编译,可以看到编译成功的文件: 144 | 145 | ``` 146 | $ make M=samples/bpf 147 | $ ls -hl samples/bpf/hello* 148 | -rwxrwxr-x 1 ubuntu ubuntu 404K Mar 30 17:48 samples/bpf/hello 149 | -rw-rw-r-- 1 ubuntu ubuntu 317 Mar 30 17:47 samples/bpf/hello_kern.c 150 | -rw-rw-r-- 1 ubuntu ubuntu 3.8K Mar 30 17:48 samples/bpf/hello_kern.o 151 | -rw-rw-r-- 1 ubuntu ubuntu 246 Mar 30 17:47 samples/bpf/hello_user.c 152 | -rw-rw-r-- 1 ubuntu ubuntu 2.2K Mar 30 17:48 samples/bpf/hello_user.o 153 | ``` 154 | 155 | 进入到对应的目录运行 hello 程序,可以看到输出结果如下: 156 | 157 | ``` 158 | $ sudo ./hello 159 | <...>-102735 [001] .... 6733.481740: 0: Hello BPF from houmin! 160 | 161 | <...>-102736 [000] .... 6733.482884: 0: Hello BPF from houmin! 162 | 163 | <...>-102737 [002] .... 6733.483074: 0: Hello BPF from houmin! 164 | ``` 165 | 166 | **代码解读** 167 | 168 | 前面提到 load_bpf_file 函数将 LLVM 编译出来的 eBPF 字节码加载进内核,这到底是如何实现的呢? 169 | 170 | 经过搜查,可以看到 load_bpf_file 也是在 samples/bpf 目录下实现的,具体的参见 bpf_load.c[6]。 171 | 172 | 173 | 174 | 阅读 load_bpf_file 代码可以看到,它主要是解析 ELF 格式的 eBPF 字节码,然后调用 load_and_attach[7] 函数。 175 | 176 | 在 load_and_attach 函数中,我们可以看到其调用了 bpf_load_program 函数,这是 libbpf 提供的函数。 177 | 178 | 调用的 bpf_load_program 中的 license、kern_version 等参数来自于解析 eBPF ELF 文件,prog_type 来自于 bpf 代码里面 SEC 字段指定的类型。 179 | 180 | ``` 181 | static int load_and_attach(const char *event, struct bpf_insn *prog, int size) 182 | { 183 | bool is_socket = strncmp(event, "socket", 6) == 0; 184 | bool is_kprobe = strncmp(event, "kprobe/", 7) == 0; 185 | bool is_kretprobe = strncmp(event, "kretprobe/", 10) == 0; 186 | bool is_tracepoint = strncmp(event, "tracepoint/", 11) == 0; 187 | bool is_raw_tracepoint = strncmp(event, "raw_tracepoint/", 15) == 0; 188 | bool is_xdp = strncmp(event, "xdp", 3) == 0; 189 | bool is_perf_event = strncmp(event, "perf_event", 10) == 0; 190 | bool is_cgroup_skb = strncmp(event, "cgroup/skb", 10) == 0; 191 | bool is_cgroup_sk = strncmp(event, "cgroup/sock", 11) == 0; 192 | bool is_sockops = strncmp(event, "sockops", 7) == 0; 193 | bool is_sk_skb = strncmp(event, "sk_skb", 6) == 0; 194 | bool is_sk_msg = strncmp(event, "sk_msg", 6) == 0; 195 | 196 | //... 197 | 198 | fd = bpf_load_program(prog_type, prog, insns_cnt, license, kern_version, 199 | bpf_log_buf, BPF_LOG_BUF_SIZE); 200 | if (fd < 0) { 201 | printf("bpf_load_program() err=%d\n%s", errno, bpf_log_buf); 202 | return -1; 203 | } 204 | //... 205 | } 206 | ``` 207 | 208 | ## -------------------------------------------------------------------------------- /ebpf-guide/eBPF高级/eBPF是一个基于寄存器的虚拟机.md: -------------------------------------------------------------------------------- 1 | # eBPF是一个基于寄存器的虚拟机 2 | 3 | ## 1. 前言 4 | 5 | **有兴趣了解更多关于 eBPF 技术的底层细节?那么请继续移步,我们将深入研究 eBPF 的底层细节,从其虚拟机机制和工具,到在远程资源受限的嵌入式设备上运行跟踪。** 6 | 7 | 注意:本系列博客文章将集中在 eBPF 技术,因此对于我们来讲,文中 BPF 和 eBPF 等同,可相互使用。BPF 名字/缩写已经没有太大的意义,因为这个项目的发展远远超出了它最初的范围。BPF 和 eBPF 在该系列中会交替使用。 8 | 9 | - **第 1 部分**[1]和**第 2 部分**[2] 为新人或那些希望通过深入了解 eBPF 技术栈的底层技术来进一步了解 eBPF 技术的人提供了深入介绍。 10 | - **第 3 部分**[3]是对用户空间工具的概述,旨在提高生产力,建立在第 1 部分和第 2 部分中介绍的底层虚拟机机制之上。 11 | - **第 4 部分**[4]侧重于在资源有限的嵌入式系统上运行 eBPF 程序,在嵌入式系统中完整的工具链技术栈(BCC/LLVM/python 等)是不可行的。我们将使用占用资源较小的嵌入式工具在 32 位 ARM 上交叉编译和运行 eBPF 程序。只对该部分感兴趣的读者可选择跳过其他部分。 12 | - **第 5 部分**[5]是关于用户空间追踪。到目前为止,我们的努力都集中在内核追踪上,所以是时候我们关注一下用户进程了。 13 | 14 | 如有疑问时,可使用该流程图: 15 | 16 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126100504957.png?imageView2/0/format/webp/q/75) 17 | 18 | ## 2. eBPF 是什么? 19 | 20 | eBPF 是一个基于寄存器的虚拟机,使用自定义的 64 位 RISC 指令集,能够在 Linux 内核内运行即时本地编译的 "BPF 程序",并能访问内核功能和内存的一个子集。这是一个完整的虚拟机实现,不要与基于内核的虚拟机(KVM)相混淆,后者是一个模块,目的是使 Linux 能够作为其他虚拟机的管理程序。eBPF 也是主线内核的一部分,所以它不像其他框架那样需要任何第三方模块(**LTTng**[6] 或 **SystemTap**[7]),而且几乎所有的 Linux 发行版都默认启用。熟悉 DTrace 的读者可能会发现 **DTrace/BPFtrace 对比**[8]非常有用。 21 | 22 | 在内核内运行一个完整的虚拟机主要是考虑便利和安全。虽然 eBPF 程序所做的操作都可以通过正常的内核模块来处理,但直接的内核编程是一件非常危险的事情 - 这可能会导致系统锁定、内存损坏和进程崩溃,从而导致安全漏洞和其他意外的效果,特别是在生产设备上(eBPF 经常被用来检查生产中的系统),所以通过一个安全的虚拟机运行本地 JIT 编译的快速内核代码对于安全监控和沙盒、网络过滤、程序跟踪、性能分析和调试都是非常有价值的。部分简单的样例可以在这篇优秀的 **eBPF 参考**[9]中找到。 23 | 24 | 25 | 26 | 基于设计,eBPF 虚拟机和其程序有意地设计为**不是**图灵完备的:即不允许有循环(正在进行的工作是支持有界循环【译者注:已经支持有界循环,#pragma unroll 指令】),所以每个 eBPF 程序都需要保证完成而不会被挂起、所有的内存访问都是有界和类型检查的(包括寄存器,一个 MOV 指令可以改变一个寄存器的类型)、不能包含空解引用、一个程序必须最多拥有 BPF_MAXINSNS 指令(默认 4096)、"主"函数需要一个参数(context)等等。当 eBPF 程序被加载到内核中,其指令被验证模块解析为有向环状图,上述的限制使得正确性可以得到简单而快速的验证。 27 | 28 | > 译者注:BPF_MAXINSNS 这个限制已经被放宽至 100 万条指令(BPF_COMPLEXITY_LIMIT_INSNS),但是非特权执行的 BPF 程序这个限制仍然会保留。 29 | 30 | 历史上,eBPF (cBPF) 虚拟机只在内核中可用,用于过滤网络数据包,与用户空间程序没有交互,因此被称为 "伯克利数据包过滤器"【译者注:早期的 BPF 实现被称为经典 cBPF】。从内核 v3.18(2014 年)开始,该虚拟机也通过 **bpf() syscall**[10] 和**uapi/linux/bpf.h**[11] 暴露在用户空间,这导致其指令集在当时被冻结,成为公共 ABI,尽管后来仍然可以(并且已经)添加新指令。 31 | 32 | 因为内核内的 eBPF 实现是根据 GPLv2 授权的,它不能轻易地被非 GPL 用户重新分发,所以也有一个替代的 Apache 授权的用户空间 eBPF 虚拟机实现,称为 "uBPF"。撇开法律条文不谈,基于用户空间的实现对于追踪那些需要避免内核-用户空间上下文切换成本的性能关键型应用很有用。 33 | 34 | ## 3. eBPF 是怎么工作的? 35 | 36 | eBPF 程序在事件触发时由内核运行,所以可以被看作是一种函数挂钩或事件驱动的编程形式。从用户空间运行按需 eBPF 程序的价值较小,因为所有的按需用户调用已经通过正常的非 VM 内核 API 调用("syscalls")来处理,这里 VM 字节码带来的价值很小。事件可由 kprobes/uprobes、tracepoints、dtrace probes、socket 等产生。这允许在内核和用户进程的指令中钩住(hook)和检查任何函数的内存、拦截文件操作、检查特定的网络数据包等等。一个比较好的参考是 **Linux 内核版本对应的 BPF 功能**[12]。 37 | 38 | 如前所述,事件触发了附加的 eBPF 程序的执行,后续可以将信息保存至 map 和环形缓冲区(ringbuffer)或调用一些特定 API 定义的内核函数的子集。一个 eBPF 程序可以链接到多个事件,不同的 eBPF 程序也可以访问相同的 map 以共享数据。一个被称为 "program array" 的特殊读/写 map 存储了对通过 bpf() 系统调用加载的其他 eBPF 程序的引用,在该 map 中成功的查找则会触发一个跳转,而且并不返回到原来的 eBPF 程序。这种 eBPF 嵌套也有限制,以避免无限的递归循环。 39 | 40 | 运行 eBPF 程序的步骤: 41 | 42 | 1. 用户空间将字节码和程序类型一起发送到内核,程序类型决定了可以访问的内核区域【译者注:主要是 BPF 帮助函数的各种子集】。 43 | 2. 内核在字节码上运行验证器,以确保程序可以安全运行(kernel/bpf/verifier.c)。 44 | 3. 内核将字节码编译为本地代码,并将其插入(或附加到)指定的代码位置。【译者注:如果启用了 JIT 功能,字节码编译为本地代码】。 45 | 4. 插入的代码将数据写入环形缓冲区或通用键值 map。 46 | 5. 用户空间从共享 map 或环形缓冲区中读取结果值。 47 | 48 | map 和环形缓冲区结构是由内核管理的(就像管道和 FIFO 一样),独立于挂载的 eBPF 或访问它们的用户程序。对 map 和环形缓冲区结构的访问是异步的,通过文件描述符和引用计数实现,可确保只要有至少一个程序还在访问,结构就能够存在。加载的 JIT 后代码通常在加载其的用户进程终止时被删除,尽管在某些情况下,它仍然可以在加载进程的生命期之后继续存在。 49 | 50 | 为了方便编写 eBPF 程序和避免进行原始的 bpf()系统调用,内核提供了方便的 **libbpf 库**[13],包含系统调用函数包装器,如**bpf_load_program**[14] 和结构定义(如 **bpf_map**[15]),在 LGPL 2.1 和 BSD 2-Clause 下双重许可,可以静态链接或作为 DSO。内核代码也提供了一些使用 libbpf 简洁的例子,位于目录 **samples/bpf/**[16] 中。 51 | 52 | ## 4. 样例学习 53 | 54 | 内核开发者非常可怜,因为内核是一个独立的项目,因而没有用户空间诸如 Glibc、LLVM、JavaScript 和 WebAssembly 诸如此类的好东西! - 这就是为什么内核中 eBPF 例子中会包含原始字节码或通过 libbpf 加载预组装的字节码文件。我们可以在 **sock_example.c**[17] 中看到这一点,这是一个简单的用户空间程序,使用 eBPF 来计算环回接口上统计接收到 TCP、UDP 和 ICMP 协议包的数量。 55 | 56 | 我们跳过微不足道的的 **main**[18] 和 **open_raw_sock**[19] 函数,而专注于神奇的代码 **test_sock**[20]。 57 | 58 | ``` 59 | static int test_sock(void) 60 | { 61 | int sock = -1, map_fd, prog_fd, i, key; 62 | long long value = 0, tcp_cnt, udp_cnt, icmp_cnt; 63 | 64 | map_fd = bpf_create_map(BPF_MAP_TYPE_ARRAY, sizeof(key), sizeof(value), 256, 0); 65 | if (map_fd < 0) {printf("failed to create map'%s'\n", strerror(errno)); 66 | goto cleanup; 67 | } 68 | 69 | struct bpf_insn prog[] = {BPF_MOV64_REG(BPF_REG_6, BPF_REG_1), 70 | BPF_LD_ABS(BPF_B, ETH_HLEN + offsetof(struct iphdr, protocol) /* R0 = ip->proto */), 71 | BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_0, -4), /* *(u32 *)(fp - 4) = r0 */ 72 | BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), 73 | BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), /* r2 = fp - 4 */ 74 | BPF_LD_MAP_FD(BPF_REG_1, map_fd), 75 | BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem), 76 | BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 2), 77 | BPF_MOV64_IMM(BPF_REG_1, 1), /* r1 = 1 */ 78 | BPF_RAW_INSN(BPF_STX | BPF_XADD | BPF_DW, BPF_REG_0, BPF_REG_1, 0, 0), /* xadd r0 += r1 */ 79 | BPF_MOV64_IMM(BPF_REG_0, 0), /* r0 = 0 */ 80 | BPF_EXIT_INSN(),}; 81 | size_t insns_cnt = sizeof(prog) / sizeof(struct bpf_insn); 82 | 83 | prog_fd = bpf_load_program(BPF_PROG_TYPE_SOCKET_FILTER, prog, insns_cnt, 84 | "GPL", 0, bpf_log_buf, BPF_LOG_BUF_SIZE); 85 | if (prog_fd < 0) {printf("failed to load prog'%s'\n", strerror(errno)); 86 | goto cleanup; 87 | } 88 | 89 | sock = open_raw_sock("lo"); 90 | 91 | if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd)) < 0) {printf("setsockopt %s\n", strerror(errno)); 92 | goto cleanup; 93 | } 94 | ``` 95 | 96 | 首先,通过 libbpf API 创建一个 BPF map,该行为就像一个最大 256 个元素的固定大小的数组。按 **IPROTO_\***[21] 定义的键索引网络协议(2 字节的 word),值代表各自的数据包计数(4 字节大小)。除了数组,eBPF 映射还实现了**其他数据结构类型**[22],如栈或队列。 97 | 98 | 接下来,eBPF 的字节码指令数组使用方便的**内核宏**[23]进行定义。在这里,我们不会讨论字节码的细节(这将在第 2 部分描述机器后进行)。更高的层次上,字节码从数据包缓冲区中读取协议字,在 map 中查找,并增加特定的数据包计数。 99 | 100 | 101 | 102 | 然后 BPF 字节码被加载到内核中,并通过 libbpf 的 bpf_load_program 返回 fd 引用来验证正确/安全。调用指定了 eBPF **程序类型**[24],这决定了它可以访问哪些内核子集。因为样例是一个 SOCKET_FILTER 类型,因此提供了一个指向当前网络包的参数。最后,eBPF 的字节码通过套接字层被附加到一个特定的原始套接字上,之后在原始套接字上接受到的每一个数据包运行 eBPF 字节码,无论协议如何。 103 | 104 | 剩余的工作就是让用户进程开始轮询共享 map 的数据。 105 | 106 | ``` 107 | for (i = 0; i < 10; i++) { 108 | key = IPPROTO_TCP; 109 | assert(bpf_map_lookup_elem(map_fd, &key, &tcp_cnt) == 0); 110 | 111 | key = IPPROTO_UDP; 112 | assert(bpf_map_lookup_elem(map_fd, &key, &udp_cnt) == 0); 113 | 114 | key = IPPROTO_ICMP; 115 | assert(bpf_map_lookup_elem(map_fd, &key, &icmp_cnt) == 0); 116 | 117 | printf("TCP %lld UDP %lld ICMP %lld packets\n", 118 | tcp_cnt, udp_cnt, icmp_cnt); 119 | sleep(1); 120 | } 121 | } 122 | ``` 123 | 124 | ## 5. 总结 125 | 126 | 第 1 部分介绍了 eBPF 的基础知识,我们通过如何加载字节码和与 eBPF 虚拟机通信的例子进行了讲述。由于篇幅限制,编译和运行例子作为留给读者的练习。我们也有意不去分析具体的 eBPF 字节码指令,因为这将是第 2 部分的重点。在我们研究的例子中,用户空间通过 libbpf 直接用 C 语言从内核虚拟机中读取 eBPF map 值(使用 10 次 1 秒的睡眠!),这很笨重,而且容易出错,而且很快就会变得很复杂,所以在第 3 部分,我们将研究更高级别的工具,通过脚本或特定领域的语言自动与虚拟机交互。 127 | 128 | ## 作者 129 | 130 | 狄卫华 131 | 132 | ## 原文链接 133 | 134 | https://mp.weixin.qq.com/s/-K3GD2xWN5glFcbd8iSesg -------------------------------------------------------------------------------- /ebpf-guide/eBPF高级/eBPF指令集.md: -------------------------------------------------------------------------------- 1 | # eBPF指令集 2 | 3 | 并不是每个开发 BPF 程序的人都知道存在多个版本的指令集。鉴于有关该主题的文档很少,这并不奇怪。那么让我们来看看不同的 eBPF 指令集,它们存在的原因,以及它们的选择为何重要。 4 | 5 | ### LLVM 的后端选择器 6 | 7 | 如果你一直使用`llc`它来编译你的 BPF 程序,你可能已经注意到一个`-mcpu`参数。帮助输出为我们提供了以下信息: 8 | 9 | ``` 10 | $ llc -march=bpf -mcpu=help 11 | Available CPUs for this target: 12 | 13 | generic - Select the generic processor. 14 | probe - Select the probe processor. 15 | v1 - Select the v1 processor. 16 | v2 - Select the v2 processor. 17 | v3 - Select the v3 processor. 18 | 19 | Available features for this target: 20 | 21 | alu32 - Enable ALU32 instructions. 22 | dummy - unused feature. 23 | dwarfris - Disable MCAsmInfo DwarfUsesRelocationsAcrossSections. 24 | 25 | Use +feature to enable a feature, or -feature to disable it. 26 | For example, llc -mcpu=mycpu -mattr=+feature1,-feature2 27 | ``` 28 | 29 | 参数使用`-mcpu`如下: 30 | 31 | ``` 32 | $ clang -O2 -Wall -target bpf -emit-llvm -c example.c -o example.bc 33 | $ llc example.bc -march=bpf -mcpu=probe -filetype=obj -o example.o 34 | ``` 35 | 36 | 该参数允许我们告诉 LLVM 使用哪个 eBPF 指令集。它默认为,最旧指令集 `generic`的别名。将选择您的内核支持的最新指令集。我们将在下面看到,选择较新的版本可以让 LLVM 生成更小、更高效的字节码。`v1``probe` 37 | 38 | ### 先决条件 39 | 40 | 基本指令集的两个扩展(v2 和 v3)添加了对新跳转指令的支持。具体来说,v2 添加了对低于跳跃的支持,而以前只有大于跳跃可用。当然,第一种跳转可以重写为第二种,但这需要额外的寄存器加载: 41 | 42 | ``` 43 | // Using mcpu=v1: 44 | 0: r2 = 7 45 | 1: if r2 s> r1 goto pc+1 46 | // Using mcpu=v2's BPF_JSLT: 47 | 0: if r1 s< 7 goto pc+1 48 | ``` 49 | 50 | 第二个扩展 v3 添加了现有条件 64 位跳转的 32 位变体。同样,您可以通过清除 32 个最高有效位来解决缺少 32 位条件跳转的问题。但是使用 32 位条件跳转更短: 51 | 52 | ``` 53 | 0: call bpf_skb_load_bytes 54 | // Using mcpu=v2's 64-bit jumps: 55 | 1: r0 <<= 32 56 | 2: r0 s>>= 32 57 | 3: if r0 s< 0 goto +1785 58 | // Using mcpu=v3's 32-bit jumps: 59 | 1: if w0 s< 0 goto +1689 60 | ``` 61 | 62 | `w0`是 的 32 位子寄存器`r0`。 63 | 64 | 您需要足够新的 Linux 和 LLVM 版本才能使用 v2 和 v3 扩展。下表对其进行了总结。 65 | 66 | | BPF ISA version | New instructions | Linux version | LLVM version | 67 | | --------------- | ---------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | 68 | | v1 (generic) | - | [v3.18](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=daedfb22451dd02b35c0549566cbb7cc06bdd53b) | [v3.7](https://reviews.llvm.org/rL227008) | 69 | | v2 | `BPF_J{LT,LE,SLT,SLE}` | [v4.14](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=92b31a9af73b3a3fc801899335d6c47966351830) | [v6.0](https://reviews.llvm.org/rL311522) | 70 | | `mattr=+alu32` | 32-bit calling convention | [v5.0](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=2dc6b100f928aac8d7532bf7112d3f8d3f952bad)[1](https://pchaigno.github.io/bpf/2021/10/20/ebpf-instruction-sets.html#fn:alu32-support) | [v7.0](https://reviews.llvm.org/rL325983) | 71 | | v3 | 32-bit variants of all jumps | [v5.1](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=092ed0968bb648cd18e8a0430cd0a8a71727315c) | [v9.0](https://reviews.llvm.org/rL353384), with `mattr=+alu32` | 72 | 73 | [BPF 常见问题解答](https://github.com/torvalds/linux/blob/28806e4d9b97865b450d72156e9ad229f2067f0b/Documentation/bpf/bpf_design_QA.rst#q-why-bpf-jlt-and-bpf-jle-instructions-were-not-introduced-in-the-beginning)还很好地了解了为什么存在这些指令集扩展: 74 | 75 | > **为什么一开始没有引入BPF_JLT和BPF_JLE指令?** 76 | > 77 | > 答:因为经典 BPF 没有它们,BPF 作者认为编译器变通方案是可以接受的。结果是程序由于缺少这些比较指令而失去了性能,并且添加了它们。这两条指令是新的 BPF 指令的完美示例,可以接受并且可以在将来添加。这两个在本机 CPU 中已经有了等效的指令。不接受与硬件指令没有一对一映射的新指令。 78 | 79 | ### 对程序大小和复杂性的影响 80 | 81 | 为什么这一切很重要?使用默认的 v1 指令集有那么糟糕吗?我们可以设置`mcpu=probe`吗? 82 | 83 | 让我们首先看一下对程序大小的影响。为此,我们可以使用[Cilium 的 BPF 程序](https://github.com/cilium/cilium/tree/master/bpf)。它们是开源的,大小不一,用于生产系统。`check-complexity.sh`Cilium 存储库中的脚本加载内核中的程序并检索各种统计信息。在下文中,我使用的是 LLVM 10.0.0。 84 | 85 | ``` 86 | $ git checkout v1.10.0-rc0 87 | $ for v in v1 v2 v3 "v1 -mattr=+alu32" "v2 -mattr=+alu32"; do \ 88 | sed -i "s/mcpu=v[1-3].*/mcpu=$v/" bpf/Makefile.bpf && \ 89 | make -C bpf KERNEL=netnext && \ 90 | sudo ./test/bpf/check-complexity.sh > ${v/ /-}.txt; \ 91 | done 92 | ``` 93 | 94 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126091401390.png?imageView2/0/format/webp/q/75) 95 | 96 | 97 | 98 | 正如预期的那样,每个更新的指令集版本都会生成更小的 BPF 程序。由于新指令具有与 x86 指令的一对一映射,我们可以预期对 JIT 编译程序的大小有类似的影响。因此,在大多数情况下,使用较新的指令集时,您可以期望获得较小的性能优势。 99 | 100 | 的影响`mattr=+alu32`更为细微——单击图例以显示它。它有时会增加程序大小,尤其是与 结合使用时`mcpu=v1`,而不是减少它。除非您使用`mcpu=v3`,否则程序的许多部分仍然需要 64 位指令和操作。因此,可能更细微的影响是由于在 32 位和 64 位值之间转换需要额外的指令。 101 | 102 | 103 | 104 | 对于 v5.2 [2](https://pchaigno.github.io/bpf/2021/10/20/ebpf-instruction-sets.html#fn:4k-limit)之前的较大程序和内核,v2 和 v3 指令集还可以让您将程序大小减少到验证程序规定的 4096 条指令限制以下。然而,这并不是验证者施加的唯一限制。大型程序更常见的问题来源是验证器分析的指令数量限制。 105 | 106 | 当验证程序分析通过程序的所有路径时,它会计算已经分析了多少条指令,并在给定限制后停止(例如,Linux 5.2+ 上的 100 万条)。我们将验证者分析的指令数称为BPF 程序的*复杂度。*在最坏的情况下,复杂性会随着程序[3](https://pchaigno.github.io/bpf/2021/10/20/ebpf-instruction-sets.html#fn:state-pruning)中条件的数量呈指数增长。 107 | 108 | 109 | 110 | `check-complexity.sh`还报告了每个加载的 BPF 程序的复杂性。我在 Linux 5.10 上执行它并在下图中报告结果。 111 | 112 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126091347358.png?imageView2/0/format/webp/q/75) 113 | 114 | 115 | 116 | 通过单击图例隐藏 v3,我们可以注意到 v1 和 v2 非常接近。然而,前两个版本和最后一个版本之间存在显着差异。v3 指令集有时会降低复杂性,有时会加剧复杂性。添加`mattr=+alu32`具有类似的影响。 117 | 118 | 目前尚不清楚为什么较新的指令集在减少指令数量时有时会增加复杂性。鉴于它们没有显着修改控制流,可能是它们降低了[验证者状态修剪](https://pchaigno.github.io/ebpf/2021/04/12/bmc-accelerating-memcached-using-bpf-and-xdp.html#bpfs-complexity-constraint)的效率。 119 | 120 | 121 | 122 | 总而言之,如果您遇到复杂性问题(即达到验证者的阈值),您需要在进行切换之前仔细测试每个指令集的影响。唯一明确的情况是从 v2 + alu32 切换到 v3,而 v3 几乎总是保持较低的复杂性。 123 | 124 | ### 结论 125 | 126 | 我们已经看到,Linux 内核支持的 eBPF 指令集不止一种,而是三种!这些指令集对程序大小和性能有影响,在大多数情况下,您最好设置`mcpu=probe`为使用最新的受支持版本。如果你有非常大的 BPF 程序,版本切换可能会导致内核验证器拒绝,如果你达到了复杂性限制,那么你应该在进行切换之前进行彻底的测试。 127 | 128 | 1. 据我所知,它应该从 v3.19 开始支持[第一次 helper 调用](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=d0003ec01c667b731c139e23de3306a8b328ccf5),但大多数程序在 v5.0 之前中断,因为不[支持 32 位有符号右移](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=2dc6b100f928aac8d7532bf7112d3f8d3f952bad)。 [![↩](https://s.w.org/images/core/emoji/14.0.0/svg/21a9.svg)](https://pchaigno.github.io/bpf/2021/10/20/ebpf-instruction-sets.html#fnref:alu32-support) 129 | 2. Linux 5.2 中为特权用户取消了 4096 条指令对程序大小的限制。 [![↩](https://s.w.org/images/core/emoji/14.0.0/svg/21a9.svg)](https://pchaigno.github.io/bpf/2021/10/20/ebpf-instruction-sets.html#fnref:4k-limit) 130 | 3. 在实践中,验证者使用状态修剪来识别等效路径并减少要分析的指令数。 [![↩](https://s.w.org/images/core/emoji/14.0.0/svg/21a9.svg)](https://pchaigno.github.io/bpf/2021/10/20/ebpf-instruction-sets.html#fnref:state-pruning) 131 | 132 | 133 | 134 | 原文链接 135 | 136 | https://pchaigno.github.io/bpf/2021/10/20/ebpf-instruction-sets.html -------------------------------------------------------------------------------- /ebpf-guide/eBPF基础/初识eBPF,eBPF发展现状.md: -------------------------------------------------------------------------------- 1 | # 初识eBPF,eBPF发展现状 2 | 3 | 4 | 5 | > eBPF 作为一颗在基础软件领域冉冉上升的新星,可谓前途大好,越来越多的基于 eBPF 的应用如雨后春笋般蓬勃涌现,这是 eBPF 展现出的惊人力量。本文就将带着大家了解 eBPF。 6 | 7 | ------ 8 | 9 | 11月,「DaoCloud 道客」正式加入了 eBPF 基金会 ,是继 8 月 12 日创始成员 Facebook、Google、Isovalent、Microsoft 和 Netflix 之后,**第一家正式获准加入的中国公司**。 10 | 11 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126101723585.png?imageView2/0/format/webp/q/75) 12 | 13 | 14 | 15 | ## **什么是 eBPF,与 eBPF 基金会?** 16 | 17 | 简单来说,**eBPF 是 Linux 内核中一个非常灵活与高效的类虚拟机 (virtual machine-like) 组件, 能够在许多内核 hook 点安全地执行字节码 (bytecode)。**很多内核子系统都已经使用了 BPF,例如常见的网络、跟踪与安全。 18 | 19 | **eBPF 基金会 (https://ebpf.io) 是一个为 eBPF 技术而创建的非盈利性组织,隶属于 Linux 基金会,其意在推动 eBPF 更好地发展,使其得到更加广泛的运用。**eBPF 基金会每年都会举办 eBPF 峰会,来自社会和各个企业的 eBPF 爱好、技术专家齐聚一堂,深度交流 eBPF 技术热点,分享创新成果。当前,eBPF 技术得到了企业的广泛应用。 20 | 21 | 22 | 23 | eBPF 技术给云原生和现代化应用带来了一些全新的解决方案和巨大的技术红利,包括可观的性能提升、CPU 开销降低。「DaoCloud 道客」作为国内云原生平台的头部供应商,非常重视 eBPF 技术给 Linux 社区、kubernetes 社区带来的技术革命。 24 | 25 | ## **eBPF如何变化演进?** 26 | 27 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126101739537.png?imageView2/0/format/webp/q/75) 28 | 29 | 此图展示在过去的 Linux 内核版本中,引入的几个 eBPF 代表性能力,截止 Linux5.14 内核版本,已经拥有了 32 种 eBPF 程序类型。 30 | 31 | eBPF 的全称是 extended Berkeley Packet Filter,eBPF 技术的前身称为 BPF (Berkeley Packet Filter),或者 cBPF (classic BPF),在 1992 年 Steven McCanne 和 Van Jacobson 的一篇论文 《The BSD Packet Filter: A New Architecture for User-level Packet Capture》 中被第一次被提及。 32 | 33 | 34 | 35 | 最初的 Berkeley Packet Filter (BPF) 是为捕捉和过滤符合特定规则的网络包而设计的,过滤器为运行在基于寄存器的虚拟机上的程序。 36 | 37 | 在内核中运行用户指定的程序被证明是一种有用的设计,但最初 BPF 设计中的一些特性却并没有得到很好的支持。例如,虚拟机的指令集架构 (ISA) 相对落后,现在处理器已经使用 64 位的寄存器,并为多核系统引入了新的指令,如原子指令 XADD。BPF 提供的一小部分 RISC 指令已经无法在现有的处理器上使用。 38 | 39 | 因此 Alexei Starovoitov 在 eBPF 的设计中介绍了如何利用现代硬件,使 eBPF 虚拟机更接近当代处理器,eBPF 指令更接近硬件的 ISA,便于提升性能。其中**最大的变动之一是使用了 64 位的寄存器,并将寄存器的数量从 2 提升到了 10 个。**现代架构使用的寄存器远远大于 10 个,这样就可以像本机硬件一样将参数通过 eBPF 虚拟机寄存器传递给对应的函数。 40 | 41 | ## **eBPF可以做什么?** 42 | 43 | 一个 eBPF 程序会附加到指定的内核代码路径中,当执行该代码路径时,会执行对应的 eBPF 程序。鉴于它的起源,**eBPF 特别适合编写网络程序**,将该网络程序附加到网络 socket,进行流量过滤、流量分类以及执行网络分类器的动作。eBPF 程序甚至可以修改一个已建链的网络 socket 的配置。XDP 工程会在网络栈的底层运行 eBPF 程序,高性能地处理接收到的报文。从下图可以看到 eBPF 支持的功能: 44 | 45 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126101754108.png?imageView2/0/format/webp/q/75) 46 | 47 | eBPF 对调试内核和执行性能分析也具有很大的帮助,程序可以附加到跟踪点、kprobes 和 perf 事件。因为 eBPF 可以访问内核数据结构,**开发者可以在不编译内核的前提下编写并测试代码**。对于工作繁忙的工程师,通过该方式可以方便地调试一个在线运行的系统。此外,还可以通过静态定义的追踪点调试用户空间的程序 (即 BCC 调试用户程序,如 Mysql)。 48 | 49 | 使用 eBPF 可以发挥其两大优势:**快速和安全**。为了更好地使用 eBPF,最好是全方位了解它是如何工作的。 50 | 51 | ## **eBPF如何进行工作?** 52 | 53 | **eBPF 程序是在内核中被事件触发的。**在一些特定的指令被执行时,这些事件会在 hook 处被捕获。Hook 被触发就会执行 eBPF 程序,对数据进行捕获和操作。接下来将系统介绍 eBPF 是如何工作的,你将了解到校验器流程、系统调用以及后续工作中所涉及到的程序类型、数据结构和辅助函数等内容。内核的 eBPF 校验器 54 | 55 | **在内核中运行用户空间的代码可能会存在安全和稳定性风险。因此,在加载 eBPF 程序前需要进行大量校验。** 56 | 57 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126101829763.png?imageView2/0/format/webp/q/75) 58 | 59 | 校验器流程图 60 | 61 | **首先通过对程序控制流的深度优先搜索保证 eBPF 能够正常结束,不会因为任何循环导致内核锁定**。严禁使用无法到达的指令;任何包含无法到达的指令的程序都会导致加载失败。 62 | 63 | **第二个阶段涉及使用校验器模拟执行 eBPF 程序 (每次执行一个指令)**。在每次指令执行前后都需要校验虚拟机的状态,保证寄存器和栈的状态都是有效的。严禁越界 (代码) 跳跃,以及访问越界数据。 64 | 65 | 66 | 67 | 校验器不会检查程序的每条路径,它能够知道程序的当前状态是否是已经检查过的程序的子集。由于前面的所有路径都必须是有效的 (否则程序会加载失败),当前的路径也必须是有效的,因此允许验证器“修剪”当前分支并跳过其模拟阶段。 68 | 69 | 校验器有一个 "安全模式",禁止指针运算。当一个没有 CAP_SYS_ADMIN 特权的用户加载 eBPF 程序时会启用安全模式,确保不会将内核地址泄露给非特权用户,且不会将指针写入内存。如果没有启用安全模式,则仅允许在执行检查之后进行指针运算。例如,所有的指针访问时都会检查类型,对齐和边界冲突。 70 | 71 | 无法读取包含未初始化内容的寄存器,尝试读取这类寄存器中的内容将导致加载失败。R0-R5 的寄存器内容在函数调用期间被标记未不可读状态,可以通过存储一个特殊值来测试任何对未初始化寄存器的读取行为;对于读取堆栈上的变量的行为也进行了类似的检查,确保没有指令会写入只读的帧指针寄存器。 72 | 73 | **最后,校验器会使用 eBPF 程序类型来限制可以从 eBPF 程序调用哪些内核函数,以及访问哪些数据结构**。例如,一些程序类型可以直接访问网络报文。bpf () 系统调用 74 | 75 | 使用 bpf() 系统调用和 BPF_PROG_LOAD 命令加载程序。该系统调用的原型为: 76 | 77 | int bpf(int cmd, union bpf_attr *attr, unsigned int size); 78 | 79 | BPF_PROG_LOAD 加载的命令可以用于创建和修改 eBPF maps,maps 是普通的 key/value 数据结构,用于在 eBPF 程序和内核空间或用户空间之间通信。其他命令允许将 eBPF 程序附加到一个控制组目录或 socket 文件描述符上,迭代所有的 maps 和程序,以及将 eBPF 对象固定到文件,这样在加载 eBPF 程序的进程结束后不会被销毁 (后者由 tc 分类器 / 操作代码使用,因此可以将 eBPF 程序持久化,而不需要加载的进程保持活动状态)。完整的命令可以参考 bpf() 帮助文档。 80 | 81 | 82 | 83 | 虽然可能存在很多不同的命令,但大体可以分为以下几类:**与 eBPF 程序交互的命令、与 eBPF maps 交互的命令,或同时与程序和 maps 交互的命令(统称为对象)**。 84 | 85 | eBPF 程序类型的作用 86 | 87 | 使用 BPF_PROG_LOAD 加载的程序类型确定了四件事: 88 | 89 | 1. 附加程序的位置; 90 | 2. 验证器允许调用的内核辅助函数; 91 | 3. 是否可以直接访问网络数据报文; 92 | 4. 传递给程序的第一个参数对象的类型。 93 | 94 | 实际上,**程序类型本质上定义了一个 API**。创建新的程序类型甚至纯粹是为了区分不同的可调用函数列表 (例如,BPF_PROG_TYPE_CGROUP_SKB 和BPF_PROG_TYPE_SOCKET_FILTER)。 95 | 96 | 随着新程序类型的增加,内核开发人员也会发现需要添加新的数据结构。 97 | 98 | eBPF 数据结构 99 | 100 | **eBPF 使用的主要的数据结构是 eBPF map,这是一个通用的数据结构,用于在内核或内核和用户空间传递数据。**其名称 "map" 也意味着数据的存储和检索需要用到 key。 101 | 102 | 使用 bpf() 系统调用创建和管理 map。当成功创建一个 map 后,会返回与该 map 关联的文件描述符。关闭相应的文件描述符的同时会销毁 map。每个 map 定义了四个值:类型,元素最大数目,数值的字节大小,以及 key 的字节大小。eBPF 提供了不同的 map 类型,不同类型的 map 提供了不同的特性。 103 | 104 | 以下将会列举一下常见的类型: 105 | 106 | ``` 107 | BPF_MAP_TYPE_HASH : a hash table「哈希表」 108 | BPF_MAP_TYPE_ARRAY : an array map, optimized for fast lookup speeds, often used for counters「数组映射,已针对快速查找速度进行优化,通常用于计数器」 109 | BPF_MAP_TYPE_PROG_ARRAY : an array of file descriptors corresponding to eBPF programs; used to implement jump tables and sub-programs to handle specific packet protocols「对应 eBPF 程序的文件描述符数组;用于实现跳转表和子程序处理特定的数据包协议」 110 | BPF_MAP_TYPE_PERCPU_ARRAY : a per-CPU array, used to implement histograms of latency「每个 CPU 的阵列,用于实现延迟的直方图」 111 | BPF_MAP_TYPE_PERF_EVENT_ARRAY : stores pointers to struct perf_event, used to read and store perf event counters「存储指向 struct perf_event 的指针,用于读取和存储 perf 事件计数器」 112 | BPF_MAP_TYPE_CGROUP_ARRAY : stores pointers to control groups「存储指向控制组的指针」 113 | BPF_MAP_TYPE_PERCPU_HASH : a per-CPU hash table「每个 CPU 的哈希表」 114 | BPF_MAP_TYPE_LRU_HASH : a hash table that only retains the most recently used items「仅保留最近使用项目的哈希表」 115 | BPF_MAP_TYPE_LRU_PERCPU_HASH : a per-CPU hash table that only retains the most recently used items「每个 CPU 的哈希表,仅保留最近使用的项目」 116 | BPF_MAP_TYPE_LPM_TRIE : a longest-prefix match trie, good for matching IP addresses to a range「最长前缀匹配数,适用于将 IP 地址匹配到某个范围」 117 | BPF_MAP_TYPE_STACK_TRACE : stores stack traces「存储堆栈跟踪」 118 | BPF_MAP_TYPE_ARRAY_OF_MAPS : a map-in-map data structure「map-in-map 数据结构」 119 | BPF_MAP_TYPE_HASH_OF_MAPS : a map-in-map data structure「map-in-map 数据结构」 120 | BPF_MAP_TYPE_DEVICE_MAP : for storing and looking up network device references「用于存储和查找网络设备引用」 121 | BPF_MAP_TYPE_SOCKET_MA : stores and looks up sockets and allows socket redirection with BPF helper functions「存储和查找套接字,并允许使用 BPF 辅助函数进行套接字重定向」 122 | ``` 123 | 124 | 125 | 126 | 所有的 map 都可以通过 eBPF 或在用户空间的程序中使用 bpf_map_lookup_elem() 和 bpf_map_update_elem() 函数进行访问。某些map类型,如 socket map,会使用其他执行特殊任务的 eBPF 辅助函数。 127 | 128 | eBPF 的更多细节可以参见官方帮助文档eBPF 辅助函数 129 | 130 | eBPF 程序被触发时,会调用辅助函数。这些特别的函数让 eBPF 能够有访问内存的丰富功能。 131 | 132 | 可以参考官方帮助文档查看 libbpf 库提供的辅助函数。 133 | 134 | 官方文档给出了现有的 eBPF 辅助函数。更多的实例可以参见内核源码的 samples/bpf/ 和 tools/testing/selftests/bpf/ 目录。 135 | 136 | ## **eBPF相关开源项目** 137 | 138 | 使用了 eBPF 的开源项目有近百项,其中包括了如下一些耳熟能详的项目: 139 | 140 | Ciliumkubernetes 平台上一个完全基于 eBPF 实现数据转发的 CNI 网络插件。 141 | 142 | https://github.com/cilium/cilium 143 | 144 | Bcc 145 | 提供了一个基于 python 的 eBPF 编程框架 146 | 147 | https://github.com/iovisor/bcc 148 | 149 | Bpftrace提供了基于 eBPF 的 Linux 内核观测工具 150 | 151 | https://github.com/iovisor/bpftrace 152 | 153 | Falcokubernetes 平台上的一个安全监控项目 154 | 155 | https://github.com/falcosecurity/falco 156 | 157 | Katran 158 | 159 | 一个实现四层负载均衡转发的项目 160 | 161 | https://github.com/facebookincubator/katran 162 | 163 | 本文作为 eBPF 系列的第一篇科普,简单的介绍了 eBPF。eBPF 这项技术是无法简单地用言语来表达出它的魅力,只有切身体验后,才能明白这项技术的神奇。 164 | 165 | ## 作者 166 | 167 | 道客船长 168 | 169 | ## 原文链接 170 | 171 | https://mp.weixin.qq.com/s/IKR64ryK2cn3nr6CJKDgYA -------------------------------------------------------------------------------- /ebpf-guide/eBPF基础/eBPF概念和基本原理.md: -------------------------------------------------------------------------------- 1 | # eBPF概念和基本原理 2 | 3 | 大约一年前,有个朋友想要用 Rust 开发一个 EVM Assembler。在他的一再要求之下,我开始帮忙编写单元测试。那时候我还不大了解操作系统的相关知识,只好开始学习一些语法和词法方面的东西。很快这个事情就无以为继了,然而我对操作系统有了一些整体了解。之后他对 eBPF 赞赏有加时,我觉得我的春天又来了。 4 | 5 | eBPF 的门槛有点高,有 500 字的小品,也有 Cilium 铺天盖地的文档。我编写本文的目的,是针对这一新技术读者提供一个全面的入门介绍,用户可以以此为基础,进行进一步的探索。后续可以阅读 Linux Weekly News、Brenden Gregg 的网站 以及 Cilium 文档学习更多相关知识。本文设计的内容包括: 6 | 7 | - eBPF 的用处 8 | - eBPF 的原理 9 | - eBPF 的实例 10 | - 如何开始使用 eBPF 11 | 12 | ## eBPF 的用处 13 | 14 | 有了 eBPF,无需修改内核,也不用加载内核模块,程序员也能在内核中执行自定义的字节码。eBPF 和内核紧密联系,下面先介绍一些相关的基本概念。 15 | 16 | Linux 系统分为内核空间和用户空间。内核空间是操作系统的核心,对所有硬件都具备不受限制的完整的访问能力,例如内存、存储以及 CPU 等。内核既然具备了这样的超级权限,势必需要严加保护,仅允许运行最可靠的代码。而用户空间运行的就是非内核的进程——例如 I/O、文件系统等。这些进程仅能通过内核开放的系统调用,对硬件进行有限的访问。换句话说,用户空间的程序一定要经过内核空间的过滤。 17 | 18 | 19 | 20 | 系统调用接口能够满足绝大多数需要,开发者在面对新的硬件、文件系统、网络协议甚至自定义的系统调用时,还是需要更多的弹性的。在不修改内核源码的情况下,用户代码要直接访问硬件怎么办呢?可以使用 Linux 内核模块(LKM)。用户空间一般是需要通过系统调用来访问内核空间,而 LKM 是直接加载到内核的,是内核的一部分。LKM 最有价值的特点之一,就是可以在运行时加载,不用编译内核也不用重启机器。 21 | 22 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126102208153.png?imageView2/0/format/webp/q/75) 23 | 24 | LKM 非常有用,但是也引入了很多风险。内核和用户空间不同,要进行不同的安全考量。内核空间是为了操作系统内核这样的特权代码准备的。系统调用连接了内核和用户空间,让用户空间能够对硬件进行合适的操作。换个说法,LKM 是能够让内核崩溃的。模块和内核的紧密关系,使得安全和升级成本直线升高。 25 | 26 | ### eBPF 是什么 27 | 28 | eBPF 是一个用于访问 Linux 内核服务和硬件的新方法。这一新技术已经用于网络、出错、跟踪以及防火墙等方面。 29 | 30 | `dtrace` 是一个 Solaris 和 BSD 操作系统上的动态跟踪工具,eBPF 受到 `dtrace` 的启发,原意是设计一个更好的 Linux 跟踪工具。跟 `dtrace` 不同的是,Linux 无法获取运行中系统的鸟瞰视图,它被系统调用、库调用以及函数所限制。一小撮工程师在 Berkeley Packet Filter(BPF)基础之上,构建一个内核虚拟机级别的包过滤机制,提供了类似 `dtrace` 的功能。2014 年第一个版本适配了 Linux 3.18,提供的功能相对较少。要使用完整的 eBPF,需要 Linux 4.4 或以上。 31 | 32 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126102216871.png?imageView2/0/format/webp/q/75) 33 | 34 | 上图对 eBPF 架构进行了一个简单的展示。eBPF 程序需要满足一系列的需求,才能被加载到内核。Verifier 中有一万多行代码用来对 eBPF 程序进行检查。Verifier 会遍历对 eBPF 程序在内核中可能的执行路径进行遍历,确保程序能够在不出现导致内核锁定的循环的情况下运行完成。除此之外还有其它必须满足的检查,例如有效的寄存器状态、程序大小以及越界等。安全控制方面,eBPF 和 LKM 是颇有差异的。 35 | 36 | 如果所有的检查都通过了,eBPF 程序被加载并编译到内核中,并监听特定的信号。该信号以事件的形式出现,会被传递给被加载的 eBPF 程序。一旦被触发,字节码就会根据其中的指令执行并收集信息。 37 | 38 | 39 | 40 | 所以 eBPF 到底做了什么?程序员能够在不增加或者修改内核代码的情况下,就能够在 Linux 内核中执行自定义的字节码。虽说还远不能整体取代 LKM,eBPF 程序可以自定义代码来和受保护的硬件资源进行交互,对内核的威胁最小。 41 | 42 | ## eBPF 的机制 43 | 44 | 前面介绍了 eBPF 的基础架构。这些能力是由多个组件协同实现的,每一种都有自己的复杂度。 45 | 46 | ### eBPF 程序剖析 47 | 48 | #### 事件和钩子 49 | 50 | eBPF 程序是在内核中被事件触发的。在一些特定的指令被执行时时,这些事件会在钩子处被捕获。钩子被触发就会执行 eBPF 程序,对数据进行捕获和操作。钩子定位的多样性正是 eBPF 的闪光点之一。例如下面几种: 51 | 52 | - 系统调用:当用户空间程序通过系统调用执行内核功能时。 53 | - 功能的进入和退出:在函数退出之前拦截调用。 54 | - 网络事件:当接收到数据包时。 55 | - kprobe 和 uprobe:挂接到内核或用户函数中。 56 | 57 | #### 辅助函数 58 | 59 | eBPF 程序被触发时,会调用辅助函数。这些特别的函数让 eBPF 能够有访问内存的丰富功能。例如 Helper 能够执行一系列的任务: 60 | 61 | - 在数据表中对键值对进行搜索、更新以及删除。 62 | - 生成伪随机数。 63 | - 搜集和标记隧道元数据。 64 | - 把 eBPF 程序连接起来,这个功能被称为 `tail call`。 65 | - 执行 Socket 相关任务,例如绑定、获取 Cookie、数据包重定向等。 66 | 67 | 这些助手函数必须是内核定义的,换句话说,eBPF 程序的调用能力是受到一个白名单限制的。这个名单很长,并且还在持续增长之中。 68 | 69 | #### Map 70 | 71 | 要在 eBPF 程序和内核以及用户空间之间存储和共享数据,eBPF 需要使用 Map。正如其名,Map 是一种键值对。Map 能够支持多种数据结构,eBPF 程序能够通过辅助函数在 Map 中发送和接收数据。 72 | 73 | ### 执行 eBPF 程序 74 | 75 | #### 加载和校验 76 | 77 | 所有 eBPF 程序都是以字节码的形式执行的,因此需要有办法把高级语言编译成这种字节码。eBPF 使用 LLVM 作为后端,前端可以介入任何语言。因为 eBPF 使用 C 编写的,所以前端使用的是 Clang。但在字节码被 Hook 之前,必须通过一系列的检查。在一个类似虚拟机的环境下用内核 Verifier阻止带有循环、权限不正确或者导致崩溃的程序运行。如果程序通过了所有的检查,字节码会使用 `bpf()` 系统调用被载入到 Hook 上。 78 | 79 | #### JIT 编译器 80 | 81 | 校验结束后,eBPF 字节码会被 JIT 编译器转译成本地机器码。eBPF 是 64 位编码,共有 11 个寄存器,因此 eBPF 和 x86、ARM 以及 arm64 等硬件都能紧密对接。虽然 eBPF 受到 VM 限制,JIT 过程保障了它的运行性能。 82 | 83 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126102227295.png?imageView2/0/format/webp/q/75) 84 | 85 | ### 总结 86 | 87 | 上面的概念们放在一起,eBPF 程序通过安全检查后插入钩子,被事件触发之后,程序会启动执行,用辅助函数和 Map 来对数据进行存储和操作。下一届我们来研究一下它们的协同方式。 88 | 89 | ## 一个例子 90 | 91 | 在 Gravitational 有一个叫做 Teleport 的开源项目,其中使用了 eBPF 程序进行跟踪和网络操作。有的组织希望知道 SSH 会话中发生了什么,Teleport 提供 SSH 访问途径的同时,加入了对用户行为的记录。可以通过对命令编码、在 Shell 脚本中运行命令的方式来进行混淆,从而阻碍对会话的记录。 92 | 93 | Teleport 4.2 中,我们引入了会话记录,其中用了三个 eBPF 程序来获取 SSH 会话,并将其转化为结构化的事件。 94 | 95 | 96 | 97 | 例如 `echo Y3VybCBodHRwOi8vd3d3LmV4YW1wbGUuY29tCg== | base64 --decode | sh`,我们能够在终端抓取到这个命令,但是这并无意义,用户已经对命令进行了混淆,但是有了 eBPF,我们就能知道,用户试图隐藏的是 `curl`: 98 | 99 | ``` 100 | { 101 | "event": "session.command", 102 | "path": "/bin/sh", 103 | "program": "sh", 104 | "argv": [], 105 | "login": "centos", 106 | "user": "jsmith"} 107 | { 108 | "event": "session.command", 109 | "path": "/bin/base64", 110 | "program": "base64", 111 | "argv": [ "--decode" 112 | ], 113 | "login": "centos", 114 | "user": "jsmith"} 115 | { 116 | "event": "session.command", 117 | "path": "/bin/curl", 118 | "argv": [ "http://www.example.com" 119 | ], 120 | "program": "curl", 121 | "return_code": 0, 122 | "login": "centos", 123 | "user": "jsmith"} 124 | { 125 | "event": "session.network", 126 | "program": "curl", 127 | "src_addr": "172.31.43.104", 128 | "dst_addr": "93.184.216.34", 129 | "dst_port": 80, 130 | "login": "centos", 131 | "user": "jsmith", 132 | "version": 4} 133 | ``` 134 | 135 | 借助 eBPF 的能力,我们把这种混淆行为转换为事件流,便于导出和分析。Teleport 用 `execsnoop`、`opensnoop` 以及 `tcpconnect` 来恢复这些事件。特别会关注的是 `tcpconnect`,它最终将信息以 JSON 的形式返回来。 136 | 137 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126102240499.png?imageView2/0/format/webp/q/75) 138 | 139 | `tcpconnect` 会跟踪 TCP 连接。像 Teleport 这样用 SSH 证书管理访问的工具来说,必须要知道 TCP 连接的发起情况。`tcpconnect` 能跟踪 `connect()` 系统调用,该调用会在 Socket 上初始化一个连接。要对这个情况进行跟踪,tcpconnect 在内核中插入了一个 `kprobe`,能够动态进入任何例程: 140 | 141 | ``` 142 | # initializeBPF b = BPF(text=bpf_text) b.attach_kprobe(event="tcp_v4_connect", fn_name="trace_connect_entry") b.attach_kretprobe(event="tcp_v4_connect", fn_name="trace_connect_v4_return") 143 | ``` 144 | 145 | 程序被触发以后,`tcpconnect` 会开始输出信息,下表展示的就是这样的信息: 146 | 147 | ``` 148 | $ ./tcpconnect 149 | PID COMM SADDR DADDR DPORT 150 | ----------------------------------------------------- 151 | 2315 curl 172.31.43.104 93.184.216.34 80 152 | ``` 153 | 154 | 所有这些数据都是用辅助函数收集而来。如果看看 Python 代码,会发现 `tcpconnect` 试用了来自 bcc 的 BPF 库的辅助函数来对上述输出内容进行格式化。 155 | 156 | ``` 157 | ...struct ipv4_data_t data4 = {.pid = pid, .ip = ipver}; 158 | data4.saddr = skp->__sk_common.skc_rcv_saddr; 159 | data4.daddr = skp->__sk_common.skc_daddr; 160 | data4.dport = ntohs(dport); 161 | bpf_get_current_comm(&data4.task, sizeof(data4.task)); 162 | ... 163 | ``` 164 | 165 | ## eBPF 入门 166 | 167 | 行文至此,我希望读者已经对 eBPF 有了一个最基础的了解,知道了他的重要性以及基本原理。是时候浏览更多技术文档和文章了。本文中提供了不少链接,不过这里最推荐的是 Quinten Monnet 的博客。 168 | 169 | 自行编写代码,开发自己的 eBPF 可能有点难。但是很多开源的开发工具链正在涌现,简化了很多 eBPF 的相关场景。下面介绍几个最流行的: 170 | 171 | - BCC:BCC 是一个工具包用于创建高效的内核跟踪和处理程序,并包含了很多有用的工具和示例。BCC 简化了 BPF 程序的开发,内核指令使用 C 指令(包含了 LLVM 的封装),前端使用的是 Python 和 LUA。BCC 有很多用途,例如性能分析和网络流量控制。BCC 还为其它程序提供了 API。 172 | - bpftrace:BPFtrace 是一个高级跟踪语言,用 LLVM 作为后端把脚本编译为 BPF 字节码,并用 BCC 和 Linux BPF 系统进行交互,并支持现有的 Linux 跟踪能力:kprobe、uprobe 以及 `tracepoint`。 173 | - Go、C/C++ 以及 Rust 的通用库。 174 | 175 | ## 结论 176 | 177 | eBPF 还是个很新鲜的技术,让程序员在不修改内核空间的情况下,能够在内核中执行自定义的字节码并从内核函数中获取更多信息。原本这些目标需要通过系统调用或内核模块来完成,eBPF 降低了所需的复杂度和危险性。简单来说,eBPF 的工作流程: 178 | 179 | - 把 eBPF 程序编译成字节码。 180 | - 在载入到 Hook 之前,在虚拟机中对程序进行校验。 181 | - 把程序附加到内核之中,被特定事件触发。 182 | - JIT 编译。 183 | - 在程序被触发时,调用辅助函数处理数据。 184 | - 在用户空间和内核空间之间使用键值对共享数据。 185 | 186 | ## 推荐阅读 187 | 188 | - SCP - Familiar, Simple, Insecure, and Slow 189 | - Greed is Good: Creating Teleport’s Discovery Protocol 190 | - Gracefully Restarting a Go Program Without Downtime 191 | 192 | ## 相关链接 193 | 194 | - **What is eBPF and How Does it Work?**:`https://gravitational.com/blog/what-is-ebpf/` 195 | - **Virag Mody**:`https://www.linkedin.com/in/virag-mody-650974a9` 196 | - **EVM Assembler**:`https://medium.com/mycrypto/the-ethereum-virtual-machine-how-does-it-work-9abac2b7c9e` 197 | - **Cilium**:`https://cilium.io/` 198 | - **Linux Weekly News**:`https://lwn.net/Articles/740157/` 199 | - **Brenden Gregg 的网站**:`http://www.brendangregg.com/index.html` 200 | - **Cilium 文档**:`https://docs.cilium.io/en/stable/bpf/` 201 | - **Linux 内核模块(LKM)**:`https://tldp.org/LDP/lkmpg/2.6/html/lkmpg.html` 202 | - **what is ebpf 1**:`image/what-is-ebpf-1.png` 203 | - **what is ebpf 2**:`images/what-is-ebpf-2.png` 204 | - **Verifier**:`https://github.com/torvalds/linux/blob/master/kernel/bpf/verifier.c` 205 | - **名单**:`https://man7.org/linux/man-pages/man7/bpf-helpers.7.html` 206 | - **LLVM**:`https://llvm.org/` 207 | - **Clang**:`https://clang.llvm.org/` 208 | - **内核 Verifier**:`https://elixir.bootlin.com/linux/latest/source/kernel/bpf/verifier.c` 209 | - **what-is-ebpf-3.png**:`images/what-is-ebpf-3.png` 210 | - **Teleport**:`https://gravitational.com/teleport` 211 | - **Teleport 4.2**:`https://gravitational.com/blog/teleport-release-4-2` 212 | - **what-is-ebpf-4.png**:`images/what-is-ebpf-4.png` 213 | - **Python 代码**:`https://github.com/iovisor/bcc/blob/ec3747ed6b16f9eec36a204dfbe3506d3778dcb4/tools/tcpconnect.py` 214 | - **bcc 的 BPF 库**:`https://github.com/iovisor/bcc/blob/master/src/cc/export/helpers.h` 215 | - **Quinten Monnet**:`https://qmonnet.github.io/whirl-offload/2016/09/01/dive-into-bpf/` 216 | - **BCC**:`https://github.com/iovisor/bcc` 217 | - **bpftrace**:`https://github.com/ajor/bpftrace` 218 | - **Go**:`https://github.com/iovisor/gobpf` 219 | - **C/C++**:`https://github.com/libbpf/libbpf` 220 | - **Rust**:`https://github.com/redsift/redbpf` 221 | - **SCP - Familiar, Simple, Insecure, and Slow**:`https://gravitational.com/blog/scp-familiar-simple-insecure-slow/` 222 | - **Greed is Good: Creating Teleport’s Discovery Protocol**:`https://gravitational.com/blog/teleport-discovery-protocol/` 223 | - **Gracefully Restarting a Go Program Without Downtime**:`https://gravitational.com/blog/golang-ssh-bastion-graceful-restarts/` 224 | 225 | 文章转载自伪架构师。[点击这里阅读原文了解更多](https://mp.weixin.qq.com/s?__biz=MzIxMDY5ODM1OA==&mid=2247485238&idx=1&sn=c4a2be9542210f51506fac520b540c5d&scene=21#wechat_redirect)。 -------------------------------------------------------------------------------- /ebpf-guide/eBPF基础/eBPF的实现原理.md: -------------------------------------------------------------------------------- 1 | # eBPF的实现原理 2 | 3 | 在介绍 eBPF 的实现原理前,我们先来回顾一下 eBPF 的架构图: 4 | 5 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126100216640.png?imageView2/0/format/webp/q/75) 6 | 7 | 这幅图对理解 eBPF 实现原理有非常大的作用,在分析 eBPF 实现原理时,要经常参照这幅图来进行分析。 8 | 9 | ## eBPF虚拟机 10 | 11 | 其实我不太想介绍 eBPF 虚拟机的,因为一般来说很少会用到 eBPF 汇编来写程序。但是,不介绍 eBPF 虚拟机的话,又不能说清 eBPF 的原理。 12 | 13 | 所以,还是先简单介绍一下 eBPF 虚拟机的原理,这样对分析 eBPF 实现有很大的帮助。 14 | 15 | ### eBPF汇编 16 | 17 | eBPF 本质上是一个虚拟机(Virtual Machine),可以执行 eBPF 字节码。 18 | 19 | 用户可以使用 eBPF 汇编或者 C 语言来编写程序,然后编译成 eBPF 字节码,再由 eBPF 虚拟机执行。 20 | 21 | > **什么是虚拟机?** 22 | > 23 | > 官方的解释是:虚拟机(VM)是一种创建于物理硬件系统(位于外部或内部)、充当虚拟计算机系统的虚拟环境,它模拟出了自己的整套硬件,包括 CPU、内存、网络接口和存储器。通过名为虚拟机监控程序的软件,用户可以将机器的资源与硬件分开并进行适当设置,以供虚拟机使用。 24 | > 25 | > 通俗的解释:虚拟机就是模拟计算机的运行环境,你可以把它当成是一台虚拟出来的计算机。 26 | > 27 | > 计算机的最本质功能就是执行代码,所以 eBPF 虚拟机也一样,可以运行 eBPF 字节码。 28 | 29 | 用户编写的 eBPF 程序最终会被编译成 eBPF 字节码,eBPF 字节码使用 `bpf_insn` 结构来表示,如下: 30 | 31 | ``` 32 | struct bpf_insn { 33 | __u8 code; // 操作码 34 | __u8 dst_reg:4; // 目标寄存器 35 | __u8 src_reg:4; // 源寄存器 36 | __s16 off; // 偏移量 37 | __s32 imm; // 立即操作数 38 | }; 39 | ``` 40 | 41 | 下面介绍一下 `bpf_insn` 结构各个字段的作用: 42 | 43 | 1. `code`:指令操作码,如 mov、add 等。 44 | 2. `dst_reg`:目标寄存器,用于指定要操作哪个寄存器。 45 | 3. `src_reg`:源寄存器,用于指定数据来源于哪个寄存器。 46 | 4. `off`:偏移量,用于指定某个结构体的成员。 47 | 5. `imm`:立即操作数,当数据是一个常数时,直接在这里指定。 48 | 49 | eBPF 程序会被 LLVM/Clang 编译成 `bpf_insn` 结构数组,当内核要执行 eBPF 字节码时,会调用 `__bpf_prog_run()` 函数来执行。 50 | 51 | 如果开启了 JIT(即时编译技术),内核会将 eBPF 字节码编译成本地机器码(Native Code)。这样就可以直接执行,而不需要虚拟机来执行。 52 | 53 | 54 | 55 | 关于 eBPF 汇编相关的知识点可以参考《[eBPF汇编指令介绍](https://mp.weixin.qq.com/s?__biz=MzA3NzYzODg1OA==&mid=2648466638&idx=2&sn=f57ace9864169e4fedb3af543e990861&scene=21#wechat_redirect)》,这里就不作深入的分析,我们只需要记住 eBPF 程序会被编译成 eBPF 字节码即可。 56 | 57 | ### eBPF虚拟机 58 | 59 | eBPF 虚拟机的作用就是执行 eBPF 字节码,eBPF 虚拟机比较简单(只有300行代码左右),由 `__bpf_prog_run()` 函数实现。 60 | 61 | > 通用虚拟机因为要模拟真实的计算机,所以通常来说实现比较复杂(如Qemu、Virtual Box等)。 62 | > 63 | > 但像 eBPF 虚拟机这种用于特定功能的虚拟机,由于只需要模拟计算机的小部分功能,所以实现通常比较简单。 64 | 65 | eBPF 虚拟机的运行环境只有 1 个 512KB 的栈和 11 个寄存器(还有一个 PC 寄存器,用于指向当前正在执行的 eBPF 字节码)。如下图所示: 66 | 67 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126100228854.png?imageView2/0/format/webp/q/75) 68 | 69 | 如果内核支持 JIT(Just In Time)运行模式,那么内核将会把 eBPF 字节码编译成本地机器码,这时可以直接运行这些机器码,而不需要使用虚拟机来运行。 70 | 71 | 可以通过以下命令打开 JIT 运行模式: 72 | 73 | ``` 74 | $ echo 1 > /proc/sys/net/core/bpf_jit_enable 75 | ``` 76 | 77 | ## 将 C 程序编译成 eBPF 字节码 78 | 79 | 由于使用 eBPF 汇编编写程序比较麻烦,所以 eBPF 提供了功能受限的 C 语言来编写 eBPF 程序,并且可以使用 Clang/LLVM 将 C 程序编译成 eBPF 字节码。 80 | 81 | 使用 Clang 编译 eBPF 程序时,需要加上 `-target bpf` 参数才能编译成功。 82 | 83 | 下面我们用一个简单的例子来介绍怎么使用 Clang 编译 eBPF 程序,我们新建一个文件 `hello.c` 并且输入以下代码: 84 | 85 | ``` 86 | #include 87 | 88 | static int (*bpf_trace_printk)(const char *fmt, int fmtsize, ...) 89 | = (void *)BPF_FUNC_trace_printk; 90 | 91 | int hello_world(void *ctx) 92 | { 93 | char msg[] = "Hello World\n"; 94 | bpf_trace_printk(msg, sizeof(msg)-1); 95 | return 0; 96 | } 97 | ``` 98 | 99 | 然后我们使用以下命令编译程序: 100 | 101 | ``` 102 | $ clang -target bpf -Wall -O2 -c hello.c -o hello.o 103 | ``` 104 | 105 | 编译后会得到一个名为 `hello.o` 的文件,我们可以通过下面命令来看到编译后的字节码: 106 | 107 | ``` 108 | $ readelf -x .text hello.o 109 | Hex dump of section '.text': 110 | 111 | 0x00000000 18010000 00000000 00000000 00000000 ................ 112 | 0x00000010 b7020000 0c000000 85000000 06000000 ................ 113 | 0x00000020 b7000000 00000000 95000000 00000000 ................ 114 | ``` 115 | 116 | 由于编译出来的字节码是二进制的,不利于人类查阅。所以,可以通过以下命令将 eBPF 程序编译成 eBPF 汇编代码: 117 | 118 | ``` 119 | $ clang -target bpf -S -o hello.s hello.c 120 | ``` 121 | 122 | 编译后会得到一个名为 `hello.s` 的文件,我们可以使用文本编辑器来查看其汇编代码: 123 | 124 | ``` 125 | ... 126 | hello_world: 127 | *(u64 *)(r10 - 8) = r1 # 把r1的值保存到栈 128 | r1 = bpf_trace_printk ll # 129 | r1 = *(u64 *)(r1 + 0) # r1赋值为 bpf_trace_printk 函数地址 130 | r2 = .L.str ll # r2赋值为 "Hello World\n" 131 | r3 = 12 # r3赋值为12 132 | *(u64 *)(r10 - 16) = r1 # 把r1的值保存到栈 133 | r1 = r2 # 调用 bpf_trace_printk 函数的参数1 134 | r2 = r3 # 调用 bpf_trace_printk 函数的参数2 135 | r3 = *(u64 *)(r10 - 16) # 获取 bpf_trace_printk 函数地址 136 | callx r3 # 调用 bpf_trace_printk 函数 137 | r1 = 0 # r1赋值为0 138 | *(u64 *)(r10 - 24) = r0 # 把r0的值保存到栈 139 | r0 = r1 # 返回0 140 | exit # 退出eBPF程序 141 | ... 142 | ``` 143 | 144 | > eBPF 虚拟机的规范: 145 | > 146 | > 1. **寄存器 `r1-r5`**:作为函数调用参数使用。在 eBPF 程序启动时,寄存器 `r1` 包含 "上下文" 参数指针。 147 | > 2. **寄存器 `r0`**:存储函数的返回值,包括函数调用和当前程序退出。 148 | > 3. **寄存器 `r10`**:eBPF程序的栈指针。 149 | 150 | ## eBPF 加载器 151 | 152 | eBPF 程序是由用户编写的,编译成 eBPF 字节码后,需要加载到内核才能被内核使用。 153 | 154 | 用户态可以通过调用 `sys_bpf()` 系统调用把 eBPF 程序加载到内核,而 `sys_bpf()` 系统调用会通过调用 `bpf_prog_load()` 内核函数加载 eBPF 程序。 155 | 156 | 我们来看看 `bpf_prog_load()` 函数的实现(经过精简后): 157 | 158 | ``` 159 | static int bpf_prog_load(union bpf_attr *attr) 160 | { 161 | enum bpf_prog_type type = attr->prog_type; 162 | struct bpf_prog *prog; 163 | int err; 164 | ... 165 | 166 | // 创建 bpf_prog 对象,用于保存 eBPF 字节码和相关信息 167 | prog = bpf_prog_alloc(bpf_prog_size(attr->insn_cnt), GFP_USER); 168 | 169 | ... 170 | prog->len = attr->insn_cnt; // eBPF 字节码长度(也就是有多少条 eBPF 字节码) 171 | 172 | err = -EFAULT; 173 | // 把 eBPF 字节码从用户态复制到 bpf_prog 对象中 174 | if (copy_from_user(prog->insns, u64_to_ptr(attr->insns), 175 | prog->len * sizeof(struct bpf_insn)) != 0) 176 | goto free_prog; 177 | 178 | ... 179 | // 这里主要找到特定模块的相关处理函数(如修正helper函数) 180 | err = find_prog_type(type, prog); 181 | 182 | // 检查 eBPF 字节码是否合法 183 | err = bpf_check(&prog, attr); 184 | 185 | // 修正helper函数的偏移量 186 | fixup_bpf_calls(prog); 187 | 188 | // 尝试将 eBPF 字节码编译成本地机器码(JIT模式) 189 | err = bpf_prog_select_runtime(prog); 190 | 191 | // 申请一个文件句柄用于与 bpf_prog 对象关联 192 | err = bpf_prog_new_fd(prog); 193 | 194 | return err; 195 | ... 196 | } 197 | ``` 198 | 199 | `bpf_prog_load()` 函数主要完成以下几个工作: 200 | 201 | 1. 创建一个 `bpf_prog` 对象,用于保存 eBPF 字节码和 eBPF 程序的相关信息。 202 | 2. 把 eBPF 字节码从用户态复制到 `bpf_prog` 对象的 `insns` 成员中,`insns` 成员是一个类型为 `bpf_insn` 结构的数组。 203 | 3. 根据 eBPF 程序所属的类型(如 `socket`、`kprobes` 或 `xdp` 等),找到其相关处理函数(如 `helper` 函数对应的修正函数,下面会介绍)。 204 | 4. 检查 eBPF 字节码是否合法。由于 eBPF 程序运行在内核态,所以要保证其安全性,否则将会导致内核崩溃。 205 | 5. 修正 `helper` 函数的偏移量(下面会介绍)。 206 | 6. 尝试将 eBPF 字节码编译成本地机器码,主要为了提高 eBPF 程序的执行效率。 207 | 7. 申请一个文件句柄用于与 `bpf_prog` 对象关联,这个文件句柄将会返回给用户态,用户态可以通过这个文件句柄来读取内核中的 eBPF 程序。 208 | 209 | ## 修正 helper 函数 210 | 211 | `helper` 函数是 eBPF 提供给用户使用的一些辅助函数。 212 | 213 | 由于 eBPF 程序运行在内核态,所为了安全,eBPF 程序中不能随意调用内核函数,只能调用 eBPF 提供的辅助函数(helper functions)。 214 | 215 | 调用 eBPF 的 `helper` 函数与调用普通的函数并不一样,调用 `helper` 函数时并不是直接调用的,而是通过 `helper` 函数的编号来进行调用。 216 | 217 | 218 | 219 | 每个 eBPF 的 `helper` 函数都有一个编号(通过枚举类型 `bpf_func_id` 来定义),定义在 `include/uapi/linux/bpf.h` 文件中,定义如下(只列出一部分): 220 | 221 | ``` 222 | enum bpf_func_id { 223 | BPF_FUNC_unspec, // 0 224 | BPF_FUNC_map_lookup_elem, // 1 225 | BPF_FUNC_map_update_elem, // 2 226 | BPF_FUNC_map_delete_elem, // 3 227 | BPF_FUNC_probe_read, // 4 228 | BPF_FUNC_ktime_get_ns, // 5 229 | BPF_FUNC_trace_printk, // 6 230 | BPF_FUNC_get_prandom_u32, // 7 231 | BPF_FUNC_get_smp_processor_id, // 8 232 | BPF_FUNC_skb_store_bytes, // 9 233 | BPF_FUNC_l3_csum_replace, // 10 234 | BPF_FUNC_l4_csum_replace, // 11 235 | BPF_FUNC_tail_call, // 12 236 | BPF_FUNC_clone_redirect, // 13 237 | BPF_FUNC_get_current_pid_tgid, // 14 238 | BPF_FUNC_get_current_uid_gid, // 15 239 | ... 240 | __BPF_FUNC_MAX_ID, 241 | }; 242 | ``` 243 | 244 | 下面我们来看看在 eBPF 程序中怎么调用 `helper` 函数: 245 | 246 | ``` 247 | #include 248 | 249 | // 声明要调用的helper函数为:BPF_FUNC_trace_printk 250 | static int (*bpf_trace_printk)(const char *fmt, int fmtsize, ...) 251 | = (void *)BPF_FUNC_trace_printk; 252 | 253 | int hello_world(void *ctx) 254 | { 255 | char msg[] = "Hello World\n"; 256 | 257 | // 调用helper函数 258 | bpf_trace_printk(msg, sizeof(msg)-1); 259 | return 0; 260 | } 261 | ``` 262 | 263 | 从上面的代码可以知道,当要调用 `helper` 函数时,需要先定义一个函数指针,并且将函数指针赋值为 `helper` 函数的编号,然后才能调用这个 `helper` 函数。 264 | 265 | > 定义函数指针的原因是:指定调用函数时的参数。 266 | 267 | 所以,调用的 `helper` 函数其实并不是真实的函数地址。那么内核是怎么找到真实的 `helper` 函数地址呢? 268 | 269 | 这里就是通过上面说的修正 `helper` 函数来实现的。 270 | 271 | 在介绍加载 eBPF 程序时说过,加载器会通过调用 `fixup_bpf_calls()` 函数来修正 `helper` 函数的地址。我们来看看 `fixup_bpf_calls()` 函数的实现: 272 | 273 | ``` 274 | static void fixup_bpf_calls(struct bpf_prog *prog) 275 | { 276 | const struct bpf_func_proto *fn; 277 | int i; 278 | 279 | // 遍历所有的 eBPF 字节码 280 | for (i = 0; i < prog->len; i++) { 281 | struct bpf_insn *insn = &prog->insnsi[i]; 282 | 283 | // 如果是函数调用指令 284 | if (insn->code == (BPF_JMP | BPF_CALL)) { 285 | ... 286 | // 通过 helper 函数的编号获取其真实地址 287 | fn = prog->aux->ops->get_func_proto(insn->imm); 288 | 289 | ... 290 | // 由于 bpf_insn 结构的 imm 字段类型为 int, 291 | // 为了能够将 helper 函数的地址(64位)保存到一个 int 中, 292 | // 所以减去一个基础函数地址,调用的时候加上这个基础函数地址即可。 293 | insn->imm = fn->func - __bpf_call_base; 294 | } 295 | } 296 | } 297 | ``` 298 | 299 | `fixup_bpf_calls()` 函数主要完成修正 `helper` 函数的地址,其工作原理如下: 300 | 301 | 1. 遍历 eBPF 程序的所有字节码。 302 | 2. 如果字节码指令是一个函数调用,那么将进行函数地址修正,修正过程如下: 303 | - 根据 `helper` 函数的编号获取其真实的函数地址。 304 | - 将 `helper` 函数的真实地址减去 `__bpf_call_base` 函数的地址,并且保存到字节码的 `imm` 字段中。 305 | 306 | 从上面修正 `helper` 函数地址的过程可知,当调用 `helper` 函数时需要加上 `__bpf_call_base` 函数的地址。 307 | 308 | ## eBPF 程序运行时机 309 | 310 | 上面介绍了 eBPF 程序的运行机制,现在来说说内核什么时候执行 eBPF 程序。 311 | 312 | 在《[eBPF的简单使用](https://mp.weixin.qq.com/s?__biz=MzA3NzYzODg1OA==&mid=2648466643&idx=1&sn=422dadbb1aafd524cb59a9138cf77f44&scene=21#wechat_redirect)》一文中介绍过,eBPF 程序需要挂载到某个内核路径(挂在点)才能被执行。 313 | 314 | 根据挂载点功能的不同,大概可以分为以下几个模块: 315 | 316 | - 性能跟踪(kprobes/uprobes/tracepoints) 317 | - 网络(socket/xdp) 318 | - 容器(cgroup) 319 | - 安全(seccomp) 320 | 321 | 比如要将 eBPF 程序挂载在 socket(套接字) 上,可以使用 `setsockopt()` 函数来实现,代码如下: 322 | 323 | ``` 324 | setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd)); 325 | ``` 326 | 327 | 下面说说 `setsockopt()` 函数各个参数的意义: 328 | 329 | - **sock**:要挂载 eBPF 程序的 socket 句柄。 330 | - **SOL_SOCKET**:设置的选项的级别,如果想要在套接字级别上设置选项,就必须设置为 `SOL_SOCKET`。 331 | - **SO_ATTACH_BPF**:表示挂载 eBPF 程序到 socket 上。 332 | - **prog_fd**:通过调用 `bpf()` 系统调用加载 eBPF 程序到内核后返回的文件句柄。 333 | 334 | 通过上面的代码,就能将 eBPF 程序挂载到 socket 上,当 socket 接收到数据包时,将会执行这个 eBPF 程序对数据包进行过滤。 335 | 336 | 我们看看当 socket 接收到数据包时的操作: 337 | 338 | ``` 339 | // file: net/packet/af_packet.c 340 | 341 | static int 342 | packet_rcv(struct sk_buff *skb, 343 | struct net_device *dev, 344 | struct packet_type *pt, 345 | struct net_device *orig_dev) 346 | { 347 | ... 348 | // 执行 eBPF 程序 349 | res = run_filter(skb, sk, snaplen); 350 | if (!res) 351 | goto drop_n_restore; 352 | ... 353 | } 354 | ``` 355 | 356 | 当 socket 接收到数据包时,会调用 `run_filter()` 函数执行 eBPF 程序。 357 | 358 | ## 总结 359 | 360 | 本文主要介绍了 eBPF 的实现原理,当然本文只是按大体思路去分析,有很多细节需要读者自己阅读源码来了解。 361 | 362 | 下篇文章将会介绍 kprobes 是怎么结合 eBPF 进行内核函数追踪的。 363 | 364 | ## 作者 365 | 366 | Linux内核那些事 367 | 368 | ## 原文链接 369 | 370 | https://mp.weixin.qq.com/s/rvXIC96iDclB0tRX2JirUg -------------------------------------------------------------------------------- /ebpf-guide/eBPF基础/eBPF基本架构及使用.md: -------------------------------------------------------------------------------- 1 | # eBPF基本架构及使用 2 | 3 | **eBPF 介绍** 4 | 5 | Tcpdump 是 Linux 平台常用的网络数据包抓取及分析工具,tcpdump 主要通过 libpcap 实现,而 libpcap 就是基于 eBPF。 6 | 7 | 先介绍 BPF(Berkeley Packet Filter),BPF 是基于寄存器虚拟机实现的,支持 JIT(Just-In-Time),比基于栈实现的性能高很多。它能载入用户态代码并且在内核环境下运行,内核提供 BPF 相关的接口,用户可以将代码编译成字节码,通过 BPF 接口加载到 BPF 虚拟机中,当然用户代码跑在内核环境中是有风险的,如有处理不当,可能会导致内核崩溃。因此在用户代码跑在内核环境之前,内核会先做一层严格的检验,确保没问题才会被成功加载到内核环境中。 8 | 9 | `eBPF`(`extended Berkeley Packet Filter`)起源于`BPF`,它提供了内核的数据包过滤机制。其扩充了 `BPF` 的功能,丰富了指令集。 10 | 11 | 最初,eBPF 仅在内核内部使用,并且 cBPF 程序在幕后无缝转换。但是随着 2014 年的 daedfb22451d 提交,eBPF 虚拟机直接暴露给用户空间。 12 | 13 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126095627966.png?imageView2/0/format/webp/q/75) 14 | 15 | eBPF 分用户空间和内核空间,用户空间和内核空间的交互有 2 种方式: 16 | 17 | - BPF map:统计摘要数据 18 | - perf-event:用户空间获取实时监测数据 19 | 20 | 如上,一般 eBPF 的工作逻辑是: 21 | 22 | 1. BPF Program 通过 LLVM/Clang 编译成 eBPF 定义的字节码 prog.bpf。 23 | 2. 通过系统调用 bpf() 将 bpf 字节码指令传入内核中。 24 | 3. 经过 verifier 检验字节码的安全性、合规性。 25 | 4. 在确认字节码安全后将其加载对应的内核模块执行,通过 Helper/hook 机制,eBPF 与内核可以交换数据/逻辑。BPF 观测技术相关的程序程序类型可能是 kprobes/uprobes/tracepoint/perf_events 中的一个或多个,其中: 26 | 27 | - kprobes:实现内核中动态跟踪。kprobes 可以跟踪到 Linux 内核中的函数入口或返回点,但是不是稳定 ABI 接口,可能会因为内核版本变化导致,导致跟踪失效。理论上可以跟踪到所有导出的符号 /proc/kallsyms。 28 | - uprobes:用户级别的动态跟踪。与 kprobes 类似,只是跟踪的函数为用户程序中的函数。 29 | - tracepoints:内核中静态跟踪。tracepoints 是内核开发人员维护的跟踪点,能够提供稳定的 ABI 接口,但是由于是研发人员维护,数量和场景可能受限。 30 | - perf_events:定时采样和 PMC。 31 | 32 | 1. 用户空间通过 BPF map 与内核通信。 33 | 34 | ## eBPF 可以做什么 35 | 36 | eBPF 主要功能列表 37 | 38 | | 特性 | 引入版本 | 功能介绍 | 应用场景 | 39 | | ------------------- | -------- | ------------------------------------------------------------ | ------------- | 40 | | Tc-bpf | 4.1 | eBPF 重构内核流分类 | 网络 | 41 | | XDP | 4.8 | 网络数据面编程技术(主要面向 L2/L3 层业务) | 网络 | 42 | | Cgroup socket | 4.10 | Cgroup 内 socket 支持 eBPF 扩展逻辑 | 容器 | 43 | | AF_XDP | 4.18 | 网络原始报文直送用户态(类似 DPDK) | 网络 | 44 | | Sockmap | 4.20 | 支持 socket 短接 | 容器 | 45 | | Device JIT | 4.20 | JIT/ISA 解耦,host 可以编译指定 device 形态的 ISA 指令 | 异构编程 | 46 | | Cgroup sysctl | 5.2 | Cgroup 内支持控制系统调用权限 | 容器 | 47 | | Struct ops Prog ext | 5.3 | 内核逻辑可动态替换 eBPF Prog 可动态替换 | 框架基础 | 48 | | Bpf trampoline | 5.5 | 三种用途:1.内核中代替 K(ret)probe,性能更优 2.eBPF Prog 中使用,解决 eBPF Prog 调试问题 3.实现 eBPF Prog 动态链接功能(未来功能) | 性能跟踪 | 49 | | KRSI(lsm + eBPF) | 5.7 | 内核运行时安全策略可定制 | 安全 | 50 | | Ring buffer | 5.8 | 提供 CPU 间共享的环形 buffer,并能实现跨 CPU 的事件保序记录。用以代替 perf/ftrace 等 buffer。 | 跟踪/性能分析 | 51 | 52 | > eBPF 在 Linux 3.18 版本以后引入,并不代表只能在内核 3.18+ 版本上运行,低版本的内核升级到最新也可以使用 eBPF 能力,只是可能部分功能受限,比如我就是在 Linux 发行版本 CentOS Linux release 7.7.1908 内核版本 3.10.0-1062.9.1.el7.x86_64 上运行 eBPF 在生产环境上搜集和排查网络问题。 53 | 54 | 和内核模块对比 55 | 56 | | 维度 | Linux 内核模块 | eBPF | 57 | | ------------------- | ------------------------------------ | ---------------------------------------------- | 58 | | kprobes/tracepoints | 支持 | 支持 | 59 | | 安全性 | 可能引入安全漏洞或导致内核 Panic | 通过验证器进行检查,可以保障内核安全 | 60 | | 内核函数 | 可以调用内核函数 | 只能通过 BPF Helper 函数调用 | 61 | | 编译性 | 需要编译内核 | 不需要编译内核,引入头文件即可 | 62 | | 运行 | 基于相同内核运行 | 基于稳定 ABI 的 BPF 程序可以编译一次,各处运行 | 63 | | 与应用程序交互 | 打印日志或文件 | 通过 perf_event 或 map 结构 | 64 | | 数据结构丰富性 | 一般 | 丰富 | 65 | | 入门门槛 | 高 | 低 | 66 | | 升级 | 需要卸载和加载,可能导致处理流程中断 | 原子替换升级,不会造成处理流程中断 | 67 | | 内核内置 | 视情况而定 | 内核内置支持 | 68 | 69 | ## eBPF 的使用场景 70 | 71 | ### 网络场景 72 | 73 | 在网络加速场景中,DPDK 技术大行其道,在某些场景 DPDK 成了唯一选择。XDP 的出现为厂商提供了一种新的选择,借助于 kernel eBPF 社区的蓬勃发展,为网络加速场景注入了一股清流。下面我们总结下两种差异: 74 | 75 | - DPDK 优势/价值:优势(性能、生态)、价值(带动硬件销售) 76 | - 性能:总体上 XDP 性能全面弱于 DPDK(但是差距不大),注意:只是比较 DPDK/XDP 自身性能 77 | - 生态:DPDK 历经多年发展,生态体现在:驱动支持丰富、基础库丰富(无锁队列、大页内存、多核调度、性能分析工具等)、协议支持丰富(社区强大,例如 VPP,支持众多协议 ARP/VLAN/IP/MPLS 等) 78 | - 价值:将网络类专有硬件的工作转嫁给软件实现,进而拓展硬件厂商市场范围。 79 | - XDP 优势:可编程、内核协同工作 80 | - 可编程:在网络硬件智能化趋势下,可编程可以适用多种场景。 81 | - 内核协同:XDP 并不是完全 bypass kernel,所以在必要的时候可以与内核协同工作,利于网络统一管理、部署。 82 | - DPDK 一些固有缺陷: 83 | - 独占 Device:设备利用率低。 84 | - 部署复杂:由于独占 Device,网络部署需要与 OS 协议栈协同部署。 85 | - 开发困难:DPDK 定位就是网络数据面开发包,所以它对使用者要求具备专业网络知识、专业硬件知识,所以入门门槛高。 86 | - 端到端性能不高:DPDK 只是提供数据包从 NIC 到用户态软件的零拷贝,但是用户态传输协议依然需要 CPU 参与。所以端到端性能不高。进阶阅读 Polycube 项目。 87 | 88 | ### 容器场景 89 | 90 | **背景**:云原生场景中容器比虚拟化技术有着更好的低底噪、轻便、易管理等优点,基本已经成为云原生应用的事实标准。容器场景对网络需求实际是应用对网络的需求,即面向应用的网络服务。 91 | 92 | - 云原生应用特点以及对网络的诉求: 93 | - 生命周期短:要求提供基于 PoD 静态身份信息实施的网络安全策略。 94 | - (不能基于 IP/Port) 租户间隔离:要求提供 API 级别的网络隔离策略。 95 | - ServiceMesh 拓扑管理:要求提供 side-car 加速。 96 | - 服务入口位置透明:要求提供跨集群 Ingress 服务能力。 97 | - 安全策略跨集群:要求网络安全策略能够在集群间共享、继承。 98 | - 服务实例冗余保证高可用性:要求提供 L3/4 层 LB 能力。进阶阅读 Cilium 项目。 99 | 100 | ### 安全场景 101 | 102 | **背景**:Linux 系统的运行安全始终是在动态平衡中,系统安全性通常要评估两方面的契合度:signals(系统中一些异常活动迹象)、mitigation(针对 signals 的一些补救措施)。 103 | 104 | 内核中的 signal/mitigation 设置散布在多个地方,配置时费时费力。 105 | 106 | **解决方案**:引入 eBPF,提供一些 eBPF Helper 实现“unified policy API”,由 API 来统一配置 signal 和 mitigation。 107 | 108 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126095750622.png?imageView2/0/format/webp/q/75) 109 | 110 | #### eBPF 的使用 111 | 112 | eBPF 提供多种使用方式:BCC、BPFTrace、libbpf C/C++ Library、eBPF GO library 等 113 | 114 | 更早期的工具使用 C 语言来编写 BPF 程序,使用 LLVM clang 编译成 BPF 代码,这对于普通使用者上手有不少门槛当前仅限于对于 eBPF 技术更加深入的学习场景。 115 | 116 | 对于大多数开发者而言,更多的是基于 BPF 技术之上编写解决我们日常遇到的各种问题。 117 | 118 | BCC 和 BPFTrace 作为 BPF 的两个前端,当前这两个项目在观测和性能分析上已经有了诸多灵活且功能强大的工具箱,完全可以满足我们日常使用。 119 | 120 | - BCC 提供了更高阶的抽象,可以让用户采用 Python、C++ 和 Lua 等高级语言快速开发 BPF 程序 121 | - BPFTrace 采用类似于 awk 语言快速编写 eBPF 程序 122 | 123 | ## libbpf C/C++ Library 124 | 125 | 基于 libbpf C/C++ library 的开发架构如下: 126 | 127 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126095800859.png?imageView2/0/format/webp/q/75) 128 | 129 | 获取 libbpf: 130 | 131 | ``` 132 | $ git clone https://github.com/libbpf/libbpf 133 | $ cd libbpf/src 134 | $ make -j8 && make install 135 | ``` 136 | 137 | ### 原生 C Hello world 138 | 139 | 参考:https://github.com/bpftools/linux-observability-with-bpf/tree/master/code/chapter-2/hello_world 140 | 141 | ``` 142 | $ git clone https://github.com/bpftools/linux-observability-with-bpf 143 | $ cd linux-observability-with-bpf/code/chapter-2/hello_world 144 | ``` 145 | 146 | 获取内核源码,将 Makefile 中 kenel-src 路径替换为实际内核源码路径 147 | 148 | ``` 149 | $ make 150 | ``` 151 | 152 | make 后会创建 BPF ELF `bpf-program.o` 及 Loader `monitor-exec` 153 | 154 | 这时执行 155 | 156 | ``` 157 | $ ./monitor-exec 158 | ``` 159 | 160 | 将 bpf 指令加载至内核。 161 | 162 | 之后,执行任意的 execve 系统调用都会打印:Hello, BPF World! 163 | 164 | 如执行 ls: 165 | 166 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126095841688.png?imageView2/0/format/webp/q/75) 167 | 168 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126095850348.png?imageView2/0/format/webp/q/75) 169 | 170 | 此时可以看到 BPF 程序打印出 Hello, BPF World! 171 | 172 | > **注意**:centos 默认 yum 安装的 clang 版本是 3.4,不支持 tagert bpf,需要升级 clang 至 3.9 173 | 174 | ## BCC 的安装及使用 175 | 176 | bcc 即 BPF Compiler Collection,bcc 是一个关于 BPF 技术的工具集。 177 | 178 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126095901183.png?imageView2/0/format/webp/q/75) 179 | 180 | 以 CentOS 7 为例 181 | 182 | ### 安装 183 | 184 | Linux 3.15 开始引入 `eBPF,而又因为bcc 在5以上的内核版本中存在bug(https://github.com/iovisor/bcc/issues/2329),建议将内核升级至4+,如lt 版本4.19.` 185 | 186 | #### 升级 Linux 内核 187 | 188 | 因为多数 elrepo 中的 kernel 版本默认是最新的 5.4 或 5.12 等,可以直接下载 4.19 的 kernel rpm 包本地安装; 189 | 190 | rpm 包参考:https://buildlogs.centos.org/c7-kernels.x86_64/kernel/20190918210642/4.19.72-300.el7.x86_64/ 191 | 192 | 下载 rpm 包至本地: 193 | 194 | ``` 195 | kernel-4.19.72-300.el7.x86_64.rpm 196 | kernel-core-4.19.72-300.el7.x86_64.rpm 197 | kernel-modules-4.19.72-300.el7.x86_64.rpm 198 | kernel-headers-4.19.72-300.el7.x86_64.rpm 199 | ``` 200 | 201 | 本地安装: 202 | 203 | ``` 204 | $ yum localinstall kernel-core-4.19.72-300.el7.x86_64.rpm kernel-4.19.72-300.el7.x86_64.rpm kernel-modules-4.19.72-300.el7.x86_64.rpm kernel-headers-4.19.72-300.el7.x86_64.rpm 205 | ``` 206 | 207 | 更新 Grub 后重启: 208 | 209 | ``` 210 | $ grub2-mkconfig -o /boot/grub2/grub.cfg 211 | $ awk -F\' '$1=="menuentry " {print i++ " : " $2}' /etc/grub2.cfg 212 | 0 : CentOS Linux (5.2.8-1.el7.elrepo.x86_64) 7 (Core) 213 | 1 : CentOS Linux (3.10.0-862.14.4.el7.x86_64) 7 (Core) 214 | 215 | $ grub2-set-default 0 216 | $ reboot 217 | ``` 218 | 219 | 重新登录后确认当前内核版本 220 | 221 | ``` 222 | $ grub2-editenv list uname -r 223 | ``` 224 | 225 | #### 安装 bcc-tools 226 | 227 | ``` 228 | $ yum install -y bcc-tools 229 | $ export PATH=$PATH:/usr/share/bcc/tools 230 | ``` 231 | 232 | #### 使用 bcc-tools 233 | 234 | 如对于一些生命周期很短的进程很难通过 top 工具去监测,这是可以通过 execsnoop 去监测: 235 | 236 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126095918418.png?imageView2/0/format/webp/q/75) 237 | 238 | BCC 的程序一般情况下都需要 root 用户或 sudo 来运行。 239 | 240 | #### BCC hello world 241 | 242 | BCC 前端绑定语言 Python 243 | 244 | ``` 245 | #!/usr/bin/python3 246 | 247 | from bcc import BPF 248 | 249 | # This may not work for 4.17 on x64, you need replace kprobe__sys_clone with kprobe____x64_sys_clone 250 | prog = """ 251 | int kprobe__sys_clone(void *ctx) { 252 | bpf_trace_printk("Hello, World!\\n"); 253 | return 0; 254 | } 255 | """ 256 | 257 | b = BPF(text=prog, debug=0x04) 258 | b.trace_print()1.2.3.4.5.6.7.8.9.10.11.12.13.14. 259 | ``` 260 | 261 | 其中, 262 | 263 | - `text='...':自定义的`C 代码 BPF 程序。 264 | - `kprobe__sys_clone()`:通过 kprobes 执行内核动态追踪的捷径。以`kprobe__为前缀的C函数,被当作内核函数名使用,本文是``sys_clone()。` 265 | - `void *ctx`: ctx 传递参数,当前不传递参数则使用`void *。` 266 | - `bpf_trace_printk():`一个简单的内核工具,用于 printf 输出至 trace_pipe (/sys/kernel/debug/tracing/trace_pipe)。对于一些简单的用法是没问题的,不过有三个限制:最多 3 个参数、只有 1%s、trace_pipe 全局共享,所以当前程序的输出会有不清晰的情况。更好的接口是利用 BPF_PERF_OUTPUT(),而后覆盖。 267 | - `return 0;`: 必要的步骤 (参考 #139)。 268 | - `.trace_print()`: 常规的 bcc 代码,读取 trace_pipe 并打印输出。 269 | 270 | 输出:bash-21720 是 ls,11789 是执行 C BPF 程序 ./monitor-exec 271 | 272 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126095939414.png?imageView2/0/format/webp/q/75) 273 | 274 | ## BPFTrace 275 | 276 | BPFTrace 使用 LLVM 将脚本编译成 BPF 二进制码,后续使用 BCC 与 Linux 内核进行交互。 277 | 278 | 从功能层面上讲,BPFTrace 的定制性和灵活性不如 BCC,但是比 BCC 工具更加易于理解和使用,降低了 BPF 技术的使用门槛。 279 | 280 | ``` 281 | # 获取bpftrace 源码: 282 | $ git clone https://github.com/iovisor/bpftrace 283 | $ cd bpftrace 284 | $ mkdir build; cd build; cmake -DCMAKE_BUILD_TYPE=Release .. 285 | $ make -j8 && make install 286 | # 统计内核中函数堆栈的次数 287 | $ bpftrace -e 'profile:hz:99 { @[kstack] = count(); }' 288 | ``` 289 | 290 | ## Further Reading 291 | 292 | - https://github.com/xdp-project/xdp-tutorial 293 | 294 | ## eBPF 发展历程 295 | 296 | - 1992 年:BPF 全称 Berkeley Packet Filter,诞生初衷提供一种内核中自定义报文过滤的手段(类汇编),提升抓包效率。(tcpdump) 297 | - 2011 年:linux kernel 3.2 版本对 BPF 进行重大改进,引入 BPF JIT,使其性能得到大幅提升。 298 | - 2014 年:linux kernel 3.15 版本,BPF 扩展成 eBPF,其功能范畴扩展至:内核跟踪、性能调优、协议栈 QoS 等方面。与之配套改进包括:扩展 BPF ISA 指令集、提供高级语言(C)编程手段、提供 MAP 机制、提供 Help 机制、引入 Verifier 机制等。 299 | - 2016 年:linux kernel 4.8 版本,eBPF 支持 XDP,进一步拓展该技术在网络领域的应用。随后 Netronome 公司提出 eBPF 硬件卸载方案。 300 | - 2018 年:linux kernel 4.18 版本,引入 BTF,将内核中 BPF 对象(Prog/Map)由字节码转换成统一结构对象,这有利于 eBPF 对象与 Kernel 版本的配套管理,为 eBPF 的发展奠定基础。 301 | - 2018 年:从 kernel 4.20 版本开始,eBPF 成为内核最活跃的项目之一,新增特性包括:sysctrl hook、flow dissector、struct_ops、lsm hook、ring buffer 等。场景范围覆盖容器、安全、网络、跟踪等。 302 | 303 | ## 作者 304 | 305 | Linux云计算网络 306 | 307 | ## 原文链接 308 | 309 | https://mp.weixin.qq.com/s/K-E3_MC9Hp3ppGBznaCUEQ -------------------------------------------------------------------------------- /ebpf-guide/eBPF基础/eBPF解读-基础篇.md: -------------------------------------------------------------------------------- 1 | # eBPF解读-基础篇 2 | 3 | **1.简介** 4 | 5 | > BPF,即Berkeley Packet Filter,是一个古老的网络封包过滤机制。它允许从用户空间注入一段简短的字节码到内核来定制封包处理逻辑。Linux从2.5开始移植了BPF,tcpdump就是基于BPF的应用。 6 | 7 | 所谓eBPF(extended BPF),则是从3.18引入的,对BPF的改造和功能增强: 8 | 9 | 1. 使用类似于X86的体系结构,eBPF设计了一个通用的RISC指令集,支持11个64bit寄存器(32bit子寄存器)r0-r10,使用512字节的栈空间 10 | 2. 引入了JIT编译,取代了BPF解释器。eBPF程序直接被编译为目标体系结构的机器码 11 | 3. 和网络子系统进行了解耦。它的数据模型是通用的,eBPF程序可以挂钩到 Kprobe或Tracepoint 12 | 4. 使用Maps来存储全局数据,这是一种通用的键值存储。可用作不同eBPF程序、eBPF和用户空间程序的状态共享 13 | 5. 助手函数(Helper Functions),这些函数供eBPF程序调用,可以实现封包改写、Checksum计算、封包克隆等能力 14 | 6. 尾调用(Tail Calls),可以用于将程序控制权从一个eBPF转移给另外一个。老版本的eBPF对程序长度有4096字节的限制,通过尾调用可以规避 15 | 7. 用于Pin对象(Maps、eBPF程序)的伪文件系统 16 | 8. 支持将eBPF Offload给智能硬件的基础设施 17 | 18 | 以上增强,让eBPF不仅仅限于网络封包处理,当前eBPF的应用领域包括: 19 | 20 | 1. 网络封包处理:XDP、TC、socket progs、kcm、calico、cilium等 21 | 2. 内核跟踪和性能监控:KProbes、UProbes、TracePoints 22 | 3. 安全领域:Secomp、landlock等。例如阻止部分类型的系统调用 23 | 24 | 现在BPF一般都是指eBPF,而老的BPF一般称为cBPF(classic BPF)。 25 | 26 | 性能是eBPF的另外一个优势,由于所有代码都在内核空间运行,避免了复制数据到用户空间、上下文切换等开销。甚至编译过程都在尽可能的优化,例如助手函数会被内联到eBPF程序中,避免函数调用的开销。 27 | 28 | 用户提供的代码在内核中运行,安全性需要得到保证。eBPF校验器会对字节码进行各方面的检查,确保它不会导致内核崩溃或锁死。 29 | 30 | eBPF具有非常好的灵活性、动态性,可以随时的注入、卸载,不需要重启内核或者中断网络连接。 31 | 32 | eBPF程序可以在不同体系结构之间移植。 33 | 34 | **2.eBPF基础** 35 | 36 | **BPF架构** 37 | 38 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126095400633.png?imageView2/0/format/webp/q/75) 39 | 40 | 如上图所示,eBPF应用程序,从开发到运行的典型流程如下: 41 | 42 | 1. 利用Clang,将C语言开发的代码编译为eBPF object文件 43 | 2. 在用户空间将eBPF object文件载入内核。载入前,可能对object文件进行各种修改。这一步骤,可能通过iproute2之类的BPF ELF loader完成,也可能通过自定义的控制程序完成 44 | 3. BPF Verifier在VM中进行安全性校验 45 | 4. JIT编译器将字节码编译为机器码,返回BPF程序的文件描述符 46 | 5. 使用文件描述符将BPF程序挂钩到某个子系统(例如networking)的挂钩点。子系统有可能将BPF程序offload给硬件(例如智能网卡) 47 | 6. 用户空间通过eBPF Map和内核空间交换数据,获知eBPF程序的执行结果 48 | 49 | **挂钩点** 50 | 51 | eBPF程序以事件驱动的方式执行,具体来说,就是在内核的代码路径上,存在大量挂钩点(Hook Point)。eBPF程序会注册到某些挂钩点,当内核运行到挂钩点后,就执行eBPF程序。 52 | 53 | 挂钩点主要包括以下几类: 54 | 55 | 1. 网络事件,例如封包到达 56 | 2. Kprobes / Uprobes 57 | 3. 系统调用 58 | 4. 函数的入口/退出点 59 | 60 | **BPF Verifier** 61 | 62 | 在加载之后,BPF校验器负责验证eBPF程序是否安全,它会模拟所有的执行路径,并且: 63 | 64 | 1. 检查程序控制流,发现循环 65 | 2. 检测越界的跳转、不可达指令 66 | 3. 跟踪Context的访问、栈移除 67 | 4. 检查unpriviledged的指针泄漏 68 | 5. 检查助手函数调用参数 69 | 70 | **BPF JITs** 71 | 72 | 在校验之后,eBPF程序被JIT编译器编译为Native代码。 73 | 74 | **BPF Maps** 75 | 76 | 键值对形式的存储,通过文件描述符来定位,值是不透明的Blob(任意数据)。用于跨越多次调用共享数据,或者与用户空间应用程序共享数据。 77 | 78 | 一个eBPF程序可以直接访问最多64个Map,多个eBPF程序可以共享同一Map。 79 | 80 | **Pinning** 81 | 82 | BPF Maps和程序都是内核资源,仅能通过文件描述符访问到。文件描述符对应了内核中的匿名inodes。 83 | 84 | 用户空间程序可以使用大部分基于文件描述符的API,但是文件描述符是限制在进程的生命周期内的,这导致Map难以被共享。比较显著的例子是iproute2,当tc或XDP加载eBPF程序之后,自身会立刻退出。这导致无法从用户空间访问Map。 85 | 86 | 87 | 88 | 为了解决上面的问题,引入了一个最小化的、内核空间中的BPF文件系统。BPF程序和Map会被pin到一个被称为object pinning的进程。bpf系统调用有两个命令BPF_OBJ_PIN、BPF_OBJ_GET分别用于钉住、取回对象。 89 | 90 | tc这样的工具就是利用Pinning在ingress/egress端共享Map。 91 | 92 | **尾调用** 93 | 94 | 尾调用允许一个BPF程序调用另外一个,这种调用没有函数调用那样的开销。其实现方式是long jump,重用当前stack frame。 95 | 96 | 注意:只用相同类型的BPF程序才能相互尾调用。 97 | 98 | 要使用尾调用,需要一个BPF_MAP_TYPE_PROG_ARRAY类型的Map,其内容目前必须由用户空间产生,值是需要被尾调用的BPF程序的文件描述符。通过助手函数bpf_tail_call触发尾调用,内核会将此调用内联到一个特殊的BPF指令。 99 | 100 | **BPF-BPF调用** 101 | 102 | BPF - BPF调用是一个新添加的特性。在此特性引入之前,典型的BPF C程序需要将所有可重用的代码声明为always_inline的,这样才能确保LLVM生成的object包含所有函数。这会导致函数在每个object文件中都反复(只要它被调用超过一次)出现,增加体积。 103 | 104 | ``` 105 | 106 | #include 107 | 108 | #ifndef __section 109 | # define __section(NAME) \ 110 | __attribute__((section(NAME), used)) 111 | #endif 112 | 113 | #ifndef __inline 114 | # define __inline \ 115 | inline __attribute__((always_inline)) 116 | #endif 117 | 118 | // 总是内联 119 | static __inline int foo(void) 120 | { 121 | return XDP_DROP; 122 | } 123 | 124 | __section("prog") 125 | int xdp_drop(struct xdp_md *ctx) 126 | { 127 | return foo(); 128 | } 129 | 130 | char __license[] __section("license") = "GPL"; 131 | ``` 132 | 133 | 总是需要内联的原因是BPF的Loader/Verifier/Interpreter/JITs不支持函数调用。但是从内核4.16和LLVM 6.0开始,此限制消除,BPF程序不再总是需要always_inline。上面程序的__inline可以去掉了。 134 | 135 | 目前x86_64/arm64的JIT编译器支持BPF to BPF调用,这是很重要的性能优化,因为它大大简化了生成的object文件的尺寸,对CPU指令缓存更加友好。 136 | 137 | 138 | 139 | JIT编译器为每个函数生成独立的映像(Image),并且在JIT的最后一个步骤中修复映像中的函数调用地址。 140 | 141 | 到5.9为止,你不能同时使用BPF-BPF调用(BPF子程序)和尾调用。从5.10开始,可以混合使用,但是仍然存在一些限制。此外,混合使用两者可能导致内核栈溢出,原因是尾调用在跳转之前仅会unwind当前栈帧。 142 | 143 | **Offloading** 144 | 145 | BPF网络程序,特别是tc和XDP,提供了将BPF代码offload给NIC执行的特性。这个特性需要驱动的支持。 146 | 147 | **BPF前端工具** 148 | 149 | 能够加载BPF程序的前端工具有很多,包括bcc、perf、iproute2等。内核也在tools/lib/bpf目录下提供了用户空间库,被perf用来加载BPF追踪应用程序到内核。这是一个通用的库,你也可以直接调用它。BCC是面向追踪的工具箱。内核在samples/bpf下也提供了一些BPF示例,这些示例解析Object文件,并且直接通过系统调用将其载入内核。 150 | 151 | 基于不同前端工具,实现BPF程序的语法、语义(例如对于段名的约定)有所不同。 152 | 153 | **相关sysctl** 154 | 155 | **/proc/sys/net/core/bpf_jit_enable** 156 | 157 | 启用或禁用BPF JIT编译器: 158 | 159 | 0 仅用,仅仅使用解释器,默认值 160 | 1 启用JIT编译器 161 | 2 启用JIT编译器并且生成debugging trace到内核日志 162 | 163 | 设置为2,可以使用bpf_jit_disasm处理debugging trace 164 | 165 | **/proc/sys/net/core/bpf_jit_harden** 166 | 167 | 启用或禁用JIT加固,加固和性能是对立的,但是可以缓和JIT spraying: 168 | 169 | 0 禁用JIT加固,默认值 170 | 1 对非特权用户启用 171 | 2 对所有用户启用 172 | 173 | **/proc/sys/net/core/bpf_jit_kallsyms** 174 | 175 | 启用或禁用JITed的程序的内核符号导出(导出到/proc/kallsyms),这样可以和perf工具一起使用,还能够让内核对BPF程序的地址感知(用于stack unwinding): 176 | 177 | 0 启用 178 | 1 仅对特权用户启用 179 | 180 | **/proc/sys/kernel/unprivileged_bpf_disabled** 181 | 182 | 是否启用非特权的bpf系统调用。默认启用,一旦禁用,重启前无法恢复启用状态。不会影响seccomp等不使用bpf2系统调用的cBPF程序: 183 | 184 | 0 启用 185 | 1 禁用 186 | 187 | **助手函数** 188 | 189 | eBPF程序可以调用助手函数,完成各种任务,例如: 190 | 191 | 1. 在Map中搜索、更新、删除键值对 192 | 2. 生成伪随机数 193 | 3. 读写隧道元数据 194 | 4. 尾调用 —— 将eBPF程序链在一起 195 | 5. 执行套接字相关操作,例如绑定、查询Cookies、重定向封包 196 | 6. 打印调试信息 197 | 7. 获取系统启动到现在的时间 198 | 199 | 助手函数是定义在内核中的,有一个白名单,决定哪些内核函数可以被eBPF程序调用。 200 | 201 | 根据eBPF的约定,助手函数的参数数量不超过5。 202 | 203 | 编译后,助手函数的代码是内联到eBPF程序中的,因而不存在函数调用的开销(栈帧处理开销、CPU流水线预取指令失效开销)。 204 | 205 | 返回int的类型的助手函数,通常操作成功返回0,否则返回负数。如果不是如此,会特别说明。 206 | 207 | 助手函数不可以随意调用,不同类型的eBPF程序,可以调用不同的助手函数子集。 208 | 209 | **3.iproute2** 210 | 211 | iproute2提供的BPF前端,主要用来载入BPF网络程序,这些程序的类型包括XDP、tc、lwt。只要是为iproute2编写的BPF程序,共享统一的加载逻辑。 212 | 213 | **XDP** 214 | 215 | **加载XDP程序** 216 | 217 | 编译好的XDP类型(BPF_PROG_TYPE_XDP)的BPF程序 ,可以使用如下命令载入到支持XDP的网络设备: 218 | 219 | ip link set dev eth0 xdp obj prog.o 220 | 221 | 上述命令假设程序位于名为prog的段中。如果不使用默认段名,则需要指定sec参数: 222 | 223 | \# 如果程序放在foobar段 224 | ip link set dev em1 xdp obj prog.o sec foobar 225 | 226 | 如果程序没有标注段,也就是位于默认的.text段,则也可以用上面的命令加载。 227 | 228 | 如果已经存在挂钩到网络设备的XDP程序,默认情况下命令会报错,可以用-force参数强制替换: 229 | 230 | ip -force link set dev em1 xdp obj prog.o 231 | 232 | 大多数支持XDP的驱动,能够原子的替换XDP程序,而不会引起流量中断。出于性能的考虑同时只能有一个XDP程序挂钩, 可以利用前文提到的尾调用来组织多个XDP程序。 233 | 234 | 如果网络设备挂钩了XDP程序,则ip link命令会显示xdp标记和程序的ID。使用bpftool传入ID可以查看更多细节信息。 235 | 236 | **卸载XDP程序** 237 | 238 | ip link set dev eth0 xdp off 239 | 240 | **XDP操作模式** 241 | 242 | iproute2实现了XDP所支持的三种操作模式: 243 | 244 | 1. xdpdrv:即native XDP,BPF程序在驱动的接收路径的最早时刻被调用。这是正常的XDP模式,上游内核的所有主要10G/40G+网络驱动(包括virtio)都实现了XDP支持,也就是可使用该模式 245 | 2. xdpoffload:由智能网卡的驱动(例如Netronome的nfp驱动)实现,将整个XDP程序offload到硬件中,网卡每接收到封包都会执行XDP程序。该模式比native XDP的性能更高,缺点是,并非所有助手函数、Map类型可用。 246 | 3. xdpgeneric:即generic XDP,作为尚不支持native XDP的驱动的试验台。挂钩点比native XDP晚很多,已经进入网络栈的主接收路径,生成了skb对象,因此性能比native XDP差很多,不会用于生产环境 247 | 248 | 在切换驱动的XDP模式时,驱动通常需要重新配置它的接收/发送Rings,以保证接收到的封包线性的(linearly)存放到单个内存页中。 249 | 250 | 调用ip link set dev xxx xdp命令时,内核会首先尝试在native XDP模式下载入,如果驱动不支持,则自动使用generic XDP模式。要强制使用native XDP,则可以使用: 251 | 252 | 253 | 254 | \# 强制使用native XDP 255 | ip -force link set dev eth0 xdpdrv obj prog.o 256 | 257 | 使用类似的方式可以强制使用xdpgeneric、xdpoffload。 258 | 259 | 切换操作模式目前不能原子的进行,但是在单个操作模式下替换XDP程序则可以。 260 | 261 | 使用 verb选项,可以显示详尽的BPF校验日志: 262 | 263 | ip link set dev eth0 xdp obj xdp-example.o verb 264 | 265 | 除了从文件加载BPF程序,也可以直接从BPF伪文件系统中得到程序并使用: 266 | 267 | ip link set dev eth0 xdp pinned /sys/fs/bpf/prog 268 | \# m:表示BPF文件系统的挂载点,默认/sys/fs/bpf/ 269 | ip link set dev eth0 xdp pinned m:prog 270 | 271 | **tc** 272 | 273 | 对于为tc设计的BPF程序(BPF_PROG_TYPE_SCHED_CLS、BPF_PROG_TYPE_SCHED_ACT),可以使用tc命令加载并挂钩到网络设备。和XDP不同,tc程序没有对驱动的依赖。 274 | 275 | clsact是4.1引入了一个特殊的dummy qdisc,它持有classifier和action,但是不能执行实际的queueing。要挂钩BPF classifier,clsact是必须启用的: 276 | 277 | ``` 278 | $ tc qdisc add dev eth0 clsact 279 | ``` 280 | 281 | clsact提供了两个特殊的钩子 ingress、 egress,对应了BPF classifier可用的两个挂钩点。这两个钩子位于网络数据路径的中心位置,任何封包都必须经过 282 | 283 | 下面的命令,将BPF程序挂钩到eth0的ingress路径上: 284 | 285 | ``` 286 | $ tc filter add dev eth0 ingress bpf da obj prog.o 287 | ``` 288 | 289 | 下面的命令将BPF程序挂钩到eth0的egress路径上: 290 | 291 | ``` 292 | $ tc filter add dev eth0 egress bpf da obj prog.o 293 | ``` 294 | 295 | ingress钩子在内核中由 __netif_receive_skb_core() -> sch_handle_ingress()调用。 296 | 297 | egress钩子在内核中由 __dev_queue_xmit() -> sch_handle_egress()调用。 298 | 299 | clsact是以无锁方式处理的,支持挂钩到虚拟的、没有队列概念的网络设备,例如veth。 300 | 301 | da即direct-action模式,这是推荐的模式,应当总是在命令中指定。da模式表示BPF classifier不需要调用外部的tc action模块,因为BPF程序会将封包修改、转发或者其它动作都完成,这正是BPF性能优势所在。 302 | 303 | 类似XDP,如果不使用默认的段名,需要用sec选项: 304 | 305 | ``` 306 | $ tc filter add dev eth0 egress bpf da obj prog.o sec foobar 307 | ``` 308 | 309 | 已经挂钩到设备的tc程序的列表,可以用下面的命令查看: 310 | 311 | ``` 312 | 313 | tc filter show dev em1 ingress 314 | filter protocol all pref 49152 bpf 315 | 316 | # 针对的L3协议 优先级 分类器类型 分类器句柄 317 | filter protocol all pref 49152 bpf handle 0x1 318 | # 从prog.o的ingress段加载了程序 319 | prog.o:[ingress] 320 | # BPF程序运行在da模式 321 | direct-action 322 | # 程序ID是全局范围唯一的BPF程序标识符,可以被bpftool使用 323 | id 1 324 | # 程序指令流的哈希,哈希可以用来关联到Object文件,perf报告栈追踪的时候使用此哈希 325 | tag c5f7825e5dac396f 326 | 327 | 328 | tc filter show dev em1 egress 329 | filter protocol all pref 49152 bpf 330 | filter protocol all pref 49152 bpf handle 0x1 prog.o:[egress] direct-action id 2 tag b2fd5adc0f262714 331 | ``` 332 | 333 | tc可以挂钩多个BPF程序,这和XDP不同,它提供了多个其它的、可以链接在一起的classifier。尽管如此,单个da模式的BPF程序可以满足所有封包操作需求,它可以直接返回action断言,例如 TC_ACT_OK, TC_ACT_SHOT。使用单个BPF程序是推荐的用法。 334 | 335 | 除非打算自动替换挂钩的BPF程序,建议初次挂钩时明确的指定pref和handle,这样,在后续手工替换的时候就不需要查询获取pref、handle: 336 | 337 | ``` 338 | $ tc filter add dev eth0 ingress pref 1 handle 1 bpf da obj prog.o sec foobar 339 | ``` 340 | 341 | 使用下面的命令原子的替换BPF程序: 342 | 343 | ``` 344 | $ tc filter replace dev eth0 ingress pref 1 handle 1 bpf da obj prog.o sec foobar 345 | ``` 346 | 347 | 要移除所有以及挂钩的BPF程序,执行: 348 | 349 | ``` 350 | $ tc filter del dev eth0 ingress 351 | $ tc filter del dev eth0 egress 352 | ``` 353 | 354 | 要从网络设备上移除整个clsact qdisc,可以: 355 | 356 | ``` 357 | $ tc qdisc del dev eth0 clsact 358 | ``` 359 | 360 | 类似于XDP程序,tc程序也支持offload给职能网卡。你需要首先启用hw-tc-offload: 361 | 362 | ``` 363 | $ ethtool -K eth0 hw-tc-offload on 364 | ``` 365 | 366 | 然后再启用clsact并挂钩BPF程序。XDP和tc的offloading不能同时开启。 367 | 368 | **netdevsim** 369 | 370 | 内核提供了一个dummy驱动netdevsim,它实现了XDP/tc BPF的offloading接口,用于测试目的。 371 | 372 | 下面的命令可以启用netdevsim设备: 373 | 374 | ``` 375 | modprobe netdevsim 376 | echo "1 1" > /sys/bus/netdevsim/new_device 377 | devlink dev 378 | # netdevsim/netdevsim1 379 | devlink port 380 | # netdevsim/netdevsim1/0: type eth netdev eth0 flavour physical 381 | ip l 382 | # 4: eth0:mtu 1500 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 383 | # link/ether 2a:d5:cd:08:d1:3f brd ff:ff:ff:ff:ff:ff 384 | ``` 385 | 386 | **4.XDP** 387 | 388 | **简介** 389 | 390 | 在网络封包处理方面,出现过一种提升性能的技术 —— 内核旁路(Kernel Bypass ):完全在用户空间实现网络驱动和整个网络栈,避免上下文切换、内核网络层次、中断处理。具体实现包括Intel的DPDK (Data Plane Development Kit)、Cisco的VPP等。 391 | 392 | 内核旁路技术的缺点是: 393 | 394 | 1. 作为硬件资源的抽象层,内核是经过良好测试和验证的。在用户空间重新实现驱动,稳定性、可复用性欠佳 395 | 2. 实现网络栈也是困难的 396 | 3. 作为一个沙盒,网络处理程序难以和内核其它部分集成/交互 397 | 4. 无法使用内核提供的安全层 398 | 399 | eXpress Data Path,为内核提供了一个基于eBPF的、高性能的、可编程的、运行在驱动层的封包处理框架,它提升性能的思路和内核旁路技术相反 —— 完全在内核空间实现封包处理逻辑,例如过滤、映射、路由等。XDP通过在网络接收路径的最早期挂钩eBPF程序来实现高速封包过滤。最早期意味着:NIC驱动刚刚从receiver rings接收到封包,任何高成本操作,例如分配skb并将封包推入网络栈,尚未进行。 400 | 401 | XDP的起源来自于对DDoS攻击的防范。Cloudflare依赖(leverages heavily on)iptables进行封包过滤,在配置相当好的服务器上,可以处理1Mpps的流量。但是当出现DDoS攻击时,流量会高达3Mpps,这会导致Linux系统overflooded by IRQ请求,直到系统变得不稳定。 402 | 403 | 404 | 405 | 由于Cloudflare希望继续使用iptables以及其它内核网络栈功能,它不考虑使用DPDK这样的完全控制硬件的方案,而是使用了所谓部分内核旁路(partial kernel bypass),NIC的一部分队列继续附到内核,另外一部分队列则附到一个用户空间应用程序,此程序决定封包是否应该被丢弃。通过在网络栈的最底部就决定是否应该丢弃封包,需要经由内核网络子系统的封包数量大大减少了。 406 | 407 | Cloudflare利用了Netmap工具包实现部分内核旁路。但是这个思路可以延伸为,在内核网络栈中增加一个Checkpoint,这个点应该离NIC接收到封包的时刻尽可能的近。这个Checkpoint将把封包交给用户编写的程序,决定是应该丢弃,还是继续正常处理路径。 408 | 409 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126095135113.png?imageView2/0/format/webp/q/75) 410 | 411 | XDP对应的BPF程序类型是:BPF_PROG_TYPE_XDP。XDP程序可以读写封包,调用助手函数解析封包、计算Checksum,这些操作都不会牵涉系统调用的开销(都在内核空间执行)。 412 | 413 | 尽管XDP的基本用途是,尽早的决定封包是否应该丢弃。但是,由于网络函数无非是读、写、转发、丢弃等原语的组合,XDP可以用来实现任何网络功能。 414 | 415 | XDP的主要优势包括: 416 | 417 | 1. 可以使用各种内核基础设施,例如路由表、套接字、网络栈 418 | 2. 运行在内核中,使用和内核其它部分一致的安全模型 419 | 3. 运行在内核中,不需要跨越用户/内核空间边界,能够灵活的转发封包给其它内核实体,例如命名空间、网络栈 420 | 4. 支持动态替换XDP程序,不会引起网络中断 421 | 5. 保证封包的线性(linearly)布局,封包位于单个DMAed内存页中,访问起来很方便 422 | 6. 保证封包有256字节可用的额外headroom,可以用于(使用助手函数 bpf_xdp_adjust_head、 bpf_xdp_adjust_meta)添加自定义的封装包头 423 | 424 | 从内核4.8+开始,主要发行版中XDP可用,大部分10G+网络驱动支持XDP。 425 | 426 | **应用场景** 427 | 428 | **DDoS缓解** 429 | 430 | XDP的高性能特征,让它非常适合实现DDoS攻击缓解,以及一般性防火墙。 431 | 432 | **封包转发** 433 | 434 | BPF程序可以对封包进行任意的修改,甚至是通过助手函数任意的增减headroom大小实现封装/解封装。 435 | 436 | 处理完的封包通过XDP_REDIRECT动作即可转发封包给其它NIC,或者转发给其它CPU(利用BPF的cpumap) 437 | 438 | **负载均衡** 439 | 440 | 使用XDP_TX动作,hairpinned LB可以将修改后的封包从接收它的网卡发送回去。 441 | 442 | **流量采样和监控** 443 | 444 | XDP支持将部分或截断的封包内容存放到无锁的per-CPU的内存映射ring buffer中。此ring buffer由Linux perf基础设施提供,可以被用户空间访问。 445 | 446 | **编程接口** 447 | 448 | xdp_buff 449 | 450 | 在XDP中,代表当前封包的结构是: 451 | 452 | ``` 453 | struct xdp_buff { 454 | // 内存页中,封包数据的开始点指针 455 | void *data; 456 | // 内存页中,封包数据的结束点指针 457 | void *data_end; 458 | // 最初和和data指向同一位置。后续可以被bpf_xdp_adjust_meta()调整,向data_hard_start方向移动 459 | // 可以用于为元数据提供空间。这种元数据对于正常的内核网络栈是不可见的,但是能够被tc BPF程序读取, 460 | // 因为元数据会从XDP传送到skb中 461 | // data_meta可以仅仅适用于在尾调用之间传递信息,类似于可被tc访问的skb->cb[] 462 | void *data_meta; 463 | // XDP支持headroom,这个字段给出页中,此封包可以使用的,最小的地址 464 | // 如果封包被封装,则需要调用bpf_xdp_adjust_head(),将data向data_hard_start方向移动 465 | // 解封装时,也可以使用bpf_xdp_adjust_head()移动指针 466 | void *data_hard_start; 467 | // 提供一些额外的per receive queue元数据,这些元数据在ring setup time生成 468 | struct xdp_rxq_info *rxq; 469 | }; 470 | 471 | // 接收队列信息 472 | struct xdp_rxq_info { 473 | struct net_device *dev; 474 | u32 queue_index; 475 | u32 reg_state; 476 | } ____cacheline_aligned; // 缓存线(默认一般是64KB),CPU以缓存线为单位读取内存到CPU高速缓存 477 | 它通过BPF context传递给XDP程序。 478 | xdp_action 479 | enum xdp_action { 480 | // 提示BPF出现错误,和DROP的区别仅仅是会发送一个trace_xdp_exception追踪点 481 | XDP_ABORTED = 0, 482 | // 应当在驱动层丢弃封包,不必再浪费额外资源。对于DDos缓和、一般性防火墙很有用 483 | XDP_DROP, 484 | // 允许封包通过,进入网络栈进行常规处理 485 | // 处理此封包的CPU后续将分配skb,将封包信息填充进去,然后传递给GRO引擎 486 | XDP_PASS, 487 | // 将封包从接收到的网络接口发送回去,可用于实现hairpinned LB 488 | XDP_TX, 489 | // 重定向封包给另外一个NIC 490 | XDP_REDIRECT, 491 | }; 492 | ``` 493 | 494 | 这个枚举是XDP程序需要返回的断言,告知驱动应该如何处理封包。 495 | 496 | ## 作者 497 | 498 | CMIT云原生 499 | 500 | ## 原文链接 501 | 502 | https://mp.weixin.qq.com/s/JLHg14ZUpZzGHWaY_o455w -------------------------------------------------------------------------------- /ebpf-guide/eBPF实战应用/eBPF快速定位网络抖动.md: -------------------------------------------------------------------------------- 1 | # eBPF 快速定位网络抖动 2 | 3 | ## **1.** **前言** 4 | 5 | 趣头条的容器化已经开展了一年有余,累计完成了近 1000 个服务的容器化工作,微服务集群的规模也达到了千台以上的规模。随着容器化服务数量和集群规模的不断增大,除了常规的 API Server 参数优化、Scheduler 优化等常规优化外,近期我们还碰到了 kubernetes 底层负载均衡 ipvs 模块导致的网络抖动问题,在此把整个问题的分析、排查和解决的思路进行总结,希望能为有类似问题场景解决提供一种思路。 6 | 7 | 涉及到的 k8s 集群和机器操作系统版本如下: 8 | 9 | - k8s 阿里云 ACK 1.14.8 版本,网络模型为 CNI 插件 **terway****[1]** 中的 terway-eniip 模式; 10 | - 操作系统为 CentOS 7.7.1908,内核版本为 3.10.0-1062.9.1.el7.x86_64; 11 | 12 | ## 2. 网络抖动问题 13 | 14 | 在容器集群中新部署的服务 A,在测试初期发现通过服务注册发现访问下游服务 B(在同一个容器集群) 调用延时 999 线偶发抖动,测试 QPS 比较小,从业务监控上看起来比较明显,最大的延时可以达到 200 ms。 15 | 16 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126104517802.png?imageView2/0/format/webp/q/75) 17 | 18 | 图 2-1 服务调用延时 19 | 20 | 服务间的访问通过 gRPC 接口访问,节点发现基于 consul 的服务注册发现。通过在服务 A 容器内的抓包分析和排查,经过了以下分析和排查: 21 | 22 | - 服务 B 部分异常注册节点,排除异常节点后抖动情况依然存在; 23 | - HTTP 接口延时测试, 抖动情况没有改善; 24 | - 服务 A 在 VM(ECS)上部署测试,抖动情况没有改善; 25 | 26 | 经过上述的对比测试,我们逐步把范围缩小至服务 B 所在的主机上的底层网络抖动。 27 | 28 | 经过多次 ping 包测试,我们寻找到了某台主机 A 与 主机 B 两者之间的 ping 延时抖动与服务调用延时抖动规律比较一致,由于 ping 包 的分析比 gRPC 的分析更加简单直接,因此我们将目标转移至底层网络的 ping 包测试的轨道上。 29 | 30 | 能够稳定复现的主机环境如下图,通过主机 A ping 主机 B 中的容器实例 172.23.14.144 实例存在 ping 延时抖动。 31 | 32 | ``` 33 | # 主机 B 中的 Pod IP 地址 34 | # ip route|grep 172.23.14.144 35 | 172.23.14.144 dev cali95f3fd83a87 scope link 36 | ``` 37 | 38 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126104533618.png?imageView2/0/format/webp/q/75) 39 | 40 | 41 | 基于主机B 网络 eth1 和容器网络 cali-xxx 进行 ping 的对比结果如图: 42 | 43 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126104539374.png?imageView2/0/format/webp/q/75) 44 | 45 | 图 2-3 ping 主机与容器网络详情 46 | 47 | 通过多次测试我们发现至 Node 主机 B 主机网络的 ping 未有抖动,容器网络 cali-xx 存在比较大的抖动,最高达到 133 ms。 48 | 49 | 在 ping 测试过程中分别在主机 A 和主机 B 上使用 tcpdump 抓包分析,发现在主机 B 上的 eth1 与网卡 cali95f3fd83a87 之间的延时达 133 ms。 50 | 51 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126104547802.png?imageView2/0/format/webp/q/75) 52 | 53 | 图 2-4 主机 B 上的 ping 包延时 54 | 55 | 到此为止问题已经逐步明确,在主机 B 上接收到 ping 包在转发过程中有 100 多ms 的延时,那么是什么原因导致的 ping 数据包在主机 B转发的延时呢? 56 | 57 | ## 3. 问题分析 58 | 59 | 在分析 ping 数据包转发延时的情况之前,我们首先简单回顾一下网络数据包在内核中工作机制和数据流转路径。 60 | 61 | ### 3.1 网络数据包内核中的处理流程 62 | 63 | 在内核中,网络设备驱动是通过中断的方式来接受和处理数据包。当网卡设备上有数据到达的时候,会触发一个硬件中断来通知 CPU 来处理数据,此类处理中断的程序一般称作 ISR (Interrupt Service Routines)。ISR 程序不宜处理过多逻辑,否则会它设备的中断处理无法及时响应。因此 Linux 中将中断处理函数分为上半部和下半部。上半部是只进行最简单的工作,快速处理然后释放 CPU。剩下将绝大部分的工作都放到下半部中,下半部中逻辑有内核线程选择合适时机进行处理。 64 | 65 | Linux 2.4 以后内核版本采用的下半部实现方式是软中断,由 ksoftirqd 内核线程全权处理, 正常情况下每个 CPU 核上都有自己的软中断处理数队列和 `ksoftirqd` 内核线程。软中断实现只是通过给内存中设置一个对应的二进制值来标识,软中断处理的时机主要为以下 2 种: 66 | 67 | - 硬件中断 `irq_exit`退出时; 68 | - 被唤醒 `ksoftirqd` 内核线程进行处理软中断; 69 | 70 | 常见的软中断类型如下: 71 | 72 | ``` 73 | enum{ 74 | HI_SOFTIRQ=0, 75 | TIMER_SOFTIRQ, 76 | NET_TX_SOFTIRQ, // 网络数据包发送软中断 77 | NET_RX_SOFTIRQ, // 网络数据包接受软中断 78 | //... 79 | }; 80 | ``` 81 | 82 | 代码 3-1 Linux 软中断类型 83 | 84 | 优先级自上而下,HI_SOFTIRQ 的优先级最高。其中 `NET_TX_SOFTIRQ` 对应于网络数据包的发送, `NET_RX_SOFTIRQ` 对应于网络数据包接受,两者共同完成网络数据包的发送和接收。 85 | 86 | 网络相关的中断程序在网络子系统初始化的时候进行注册, `NET_RX_SOFTIRQ` 的对应函数为 `net_rx_action()` ,在 `net_rx_action()` 函数中会调用网卡设备设置的 `poll` 函数,批量收取网络数据包并调用上层注册的协议函数进行处理,如果是为 ip 协议,则会调用 `ip_rcv`,上层协议为 icmp 的话,继续调用 `icmp_rcv` 函数进行后续的处理。 87 | 88 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126104557977.png?imageView2/0/format/webp/q/75) 89 | 90 | 图 3-1 网卡设备数据包接收示意图 91 | 92 | ``` 93 | //net/core/dev.c 94 | 95 | static int __init net_dev_init(void){ 96 | 97 | ...... 98 | for_each_possible_cpu(i) { 99 | struct softnet_data *sd = &per_cpu(softnet_data, i); 100 | 101 | memset(sd, 0, sizeof(*sd)); 102 | skb_queue_head_init(&sd->input_pkt_queue); 103 | skb_queue_head_init(&sd->process_queue); 104 | sd->completion_queue = NULL; 105 | INIT_LIST_HEAD(&sd->poll_list); // 软中断的处理中的 poll 函数列表 106 | // ...... 107 | } 108 | ...... 109 | open_softirq(NET_TX_SOFTIRQ, net_tx_action); // 注册网络数据包发送的软中断 110 | open_softirq(NET_RX_SOFTIRQ, net_rx_action); // 注册网络数据包接受的软中断 111 | } 112 | 113 | subsys_initcall(net_dev_init); 114 | ``` 115 | 116 | 代码 3-2 软中断数据及网络软中断注册 117 | 118 | 网络数据的收发的延时,多数场景下都会和系统软中断处理相关,这里我们将重点分析 ping 包抖动时的软中断情况。这里我们采用基于 **BCC****[2]** 的 **traceicmpsoftirq.py****[3]** 来协助定位 ping 包处理的内核情况。 119 | 120 | > BCC 为 Linux 内核 BPF 技术的前端程序,主要提供 Python 语言的绑定,`traceicmpsoftirq.py` 脚本依赖于 BCC 库,需要先安装 BCC 项目,各操作系统安装参见 **INSTALL.md****[4]**。 121 | > 122 | > `traceicmpsoftirq.py` 脚本在 Linux 3.10 内核与 Linux 4.x 内核上的读写方式有差异,需要根据内核略有调整。 123 | 124 | 使用 `traceicmpsoftirq.py` 在主机 B 上运行,我们发现出现抖动延时的时内核运行的内核线程都为 `ksoftirqd/0`。 125 | 126 | ``` 127 | #主机 主机 A#A 128 | # ping -c 150 -i 0.01 172.23.14.144 |grep -E "[0-9]{2,}[\.0-9]+ ms" 129 | 130 | # 主机 B 131 | # ./traceicmpsoftirq.py 132 | tgid pid comm icmp_seq 133 | ... 134 | 0 0 swapper/0 128 135 | 6 6 ksoftirqd/0 129 136 | 6 6 ksoftirqd/0 130 137 | ... 138 | ``` 139 | 140 | 代码 3-3 `traceicmpsoftirq.py` ping 主机 B 容器 IP 抖动时的详情 141 | 142 | `[ksoftirqd/0]` 这个给了我们两个重要的信息: 143 | 144 | - 从主机 A ping 主机 B 中容器 IP 的地址,每次处理包的处理都会固定落到 CPU#0 上; 145 | - 出现延时的时候该 CPU#0 都在运行软中断处理内核线程 `ksoftirqd/0`,即在处理软中断的过程中调用的数据包处理,软中断另外一种处理时机如上所述 `irq_exit` 硬中断退出时; 146 | 147 | 如果 ping 主机 B 中的容器 IP 地址落在 CPU#0 核上,那么按照我们的测试过程, ping 主机 B 的宿主机 IP 地址没有抖动,那么处理的 CPU 一定不在 #0 号上,才能符合测试场景,我们继续使用主机 B 主机 IP 地址进行测试: 148 | 149 | ``` 150 | # 主机 A 151 | # ping -c 150 -i 0.01 172.23.14.144 |grep -E "[0-9]{2,}[\.0-9]+ ms" 152 | 153 | # 主机 B 154 | # ./traceicmpsoftirq.py 155 | tgid pid comm icmp_seq 156 | ... 157 | 0 0 swapper/19 55 158 | 0 0 swapper/19 56 159 | 0 0 swapper/19 57 160 | ... 161 | ``` 162 | 163 | 代码 3-4 `traceicmpsoftirq.py` ping 主机 B 主机 IP 详情 164 | 165 | 通过实际的测试验证,ping 主机 B 宿主机 IP 地址时候,全部都落在了 CPU#19 上。问题排查至此处,我们可以断定是 CPU#0 与 CPU#19 在软中断处理的负载上存在差异,但是此处我们有带来另外一个疑问,为什么我们的 ping 包的处理总是固定落到同一个 CPU 核上呢?通过查询资料和主机配置确认,主机上默认启用了 RPS 的技术。RPS 全称是 Receive Packet Steering,这是 Google 工程师 Tom Herbert 提交的内核补丁, 在 2.6.35 进入 Linux 内核,采用软件模拟的方式,实现了多队列网卡所提供的功能,分散了在多 CPU 系统上数据接收时的负载,把软中断分到各个CPU处理,而不需要硬件支持,大大提高了网络性能。简单点讲,就是在软中断的处理函数 `net_rx_action()` 中依据 RPS 的配置,使用接收到的数据包头部(比如源 IP 地址端口等信息)信息进行作为 key 进行 Hash 到对应的 CPU 核上去处理,算法具体参见 **get_rps_cpu****[5]** 函数。 166 | 167 | Linux 环境下的 RPS 配置,可以通过下面的命令检查: 168 | 169 | ``` 170 | # cat /sys/class/net/*/queues/rx-*/rps_cpus 171 | ``` 172 | 173 | 通过对上述情况的综合分析,我们把问题定位在 CPU#0 在内核线程中对于软中断处理的问题上。 174 | 175 | ### 3.2 CPU 软中断处理排查 176 | 177 | 问题排查到这里,我们将重点开始排查 CPU#0 上的 CPU 内核态的性能指标,看看是否有运行的函数导致了软中断处理的延期。 178 | 179 | 首先我们使用 `perf` 命令对于 CPU#0 进行内核态使用情况进行分析。 180 | 181 | ``` 182 | # perf top -C 0 -U 183 | ``` 184 | 185 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126104609937.png?imageView2/0/format/webp/q/75) 186 | 187 | 图 3-2 perf top CPU#0 内核性能数据 188 | 189 | 通过 `perf top` 命令我们注意到 CPU#0 的内核态中,`estimation_timer` 这个函数的使用率一直占用比较高,同样我们通过对于 CPU#0 上的火焰图分析,也基本与 `perf top` 的结果一致。 190 | 191 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126104615204.png?imageView2/0/format/webp/q/75) 192 | 193 | 为了弄清楚 `estimation_timer` 的内核占用情况,我们继续使用 开源项目 **perf-tools****[6]**(作者为 Brendan Gregg)中的 **funcgraph****[7]** 工具分析函数 `estimation_timer` 在内核中的调用关系图和占用延时。 194 | 195 | ``` 196 | # -m 1最大堆栈为 1 层,-a 显示全部信息 -d 6 跟踪 6秒 197 | #./funcgraph -m 1 -a -d 6 estimation_timer 198 | ``` 199 | 200 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126104638808.png?imageView2/0/format/webp/q/75) 201 | 202 | 同时我们注意到 `estimation_timer` 函数在 CPU#0 内核中的遍历一次遍历时间为 119 ms,在内核处理软中断的情况中占用过长的时间,这一定会影响到其他软中断的处理。 203 | 204 | 为了进一步确认 CPU#0 上的软中断处理情况,我们基于 BCC 项目中的 **softirqs.py****[8]** 脚本(本地略有修改),观察 CPU#0 上的软中断数量变化和整体耗时分布,发现 CPU#0 上的软中断数量增长并不是太快,但是 timer 的直方图却又异常点数据, 通过 timer 在持续 10s 内的 timer 数据分析,我们发现执行的时长分布在 [65 - 130] ms区间的记录有 5 条。这个结论完全与通过 `funcgraph` 工具抓取到的 `estimation_timer` 在 CPU#0 上的延时一致。。 205 | 206 | ``` 207 | # -d 采用直方图 10 表示 10s 做一次聚合, 1 显示一次 -C 0 为我们自己修改的功能,用于过滤 CPU#0 208 | # /usr/share/bcc/tools/softirqs -d 10 1 -C 0 209 | ``` 210 | 211 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126104648759.png?imageView2/0/format/webp/q/75) 212 | 213 | 图 3-5 CPU#0 软中断之 timer 的执行时长直方图 214 | 215 | 通过上述分析我们得知 `estimation_timer` 来自于 ipvs 模块(参见图 3-4),kubernets 中 kube-proxy 组件负载均衡器正是基于 ipvs 模块,那么问题基本上出现在 kube-proxy 进程上。 216 | 217 | 我们在主机 B 上仅保留测试的容器实例,在停止 kubelet 服务后,手工停止 kube-proxy 容器进程,经过重新测试,ping 延时抖动的问题果然消失了。 218 | 219 | 220 | 221 | 到此问题的根源我们可以确定是 kube-proxy 中使用的 ipvs 内核模块中的 `estimation_timer` 函数执行时间过长,导致网络软中断处理延迟,从而使 ping 包的出现抖动,那么 `estimation_timer[ipvs]` 的作用是什么?什么情况下导致的该函数执行如此之长呢? 222 | 223 | ### 3.3 ipvs estimation_timer 定时器 224 | 225 | 谜底终将揭晓! 226 | 227 | 我们通过阅读 ipvs 相关的源码,发现 `estimation_timer()[ipvs]` 函数针对每个 Network Namespace 创建时候的通过 **ip_vs_core.c****[9]** 中的 `__ip_vs_init` 初始化的, 228 | 229 | ``` 230 | /* 231 | * Initialize IP Virtual Server netns mem. 232 | */ 233 | static int __net_init __ip_vs_init(struct net *net) 234 | { 235 | struct netns_ipvs *ipvs; 236 | // ... 237 | if (ip_vs_estimator_net_init(ipvs) < 0) // 初始化 238 | goto estimator_fail; 239 | } 240 | ``` 241 | 242 | 代码 3-5 ipvs 初始化函数 243 | 244 | `ip_vs_estimator_net_init` 函数在文件 **ip_vs_est.c****[10]** 中,定义如下: 245 | 246 | ``` 247 | int __net_init ip_vs_estimator_net_init(struct netns_ipvs *ipvs) 248 | { 249 | INIT_LIST_HEAD(&ipvs->est_list); 250 | spin_lock_init(&ipvs->est_lock); 251 | timer_setup(&ipvs->est_timer, estimation_timer, 0); // 设置定时器函数 estimation_timer 252 | mod_timer(&ipvs->est_timer, jiffies + 2 * HZ); // 启动第一次计时器,2秒启动 253 | return 0; 254 | } 255 | ``` 256 | 257 | 代码 3-6 ipvs estimator 初始化函数 258 | 259 | `estimation_timer` 也定义在 **ip_vs_est.c****[11]** 文件中。 260 | 261 | ``` 262 | static void estimation_timer(struct timer_list *t) 263 | { 264 | // ... 265 | spin_lock(&ipvs->est_lock); 266 | list_for_each_entry(e, &ipvs->est_list, list) { 267 | s = container_of(e, struct ip_vs_stats, est); 268 | 269 | spin_lock(&s->lock); 270 | ip_vs_read_cpu_stats(&s->kstats, s->cpustats); 271 | 272 | /* scaled by 2^10, but divided 2 seconds */ 273 | rate = (s->kstats.conns - e->last_conns) << 9; 274 | e->last_conns = s->kstats.conns; 275 | e->cps += ((s64)rate - (s64)e->cps) >> 2; 276 | 277 | // ... 278 | } 279 | spin_unlock(&ipvs->est_lock); 280 | mod_timer(&ipvs->est_timer, jiffies + 2*HZ); // 2 秒后启动新的一轮统计 281 | } 282 | ``` 283 | 284 | 代码 3-7 ipvs estimation_timer 函数 285 | 286 | 从 `estimation_timer` 的函数实现来看,会首先调用 spin_lock 进行锁的操作,然后遍历当前 Network Namespace 下的全部 ipvs 规则。由于我们集群的某些历史原因导致生产集群中的 Service 比较多,因此导致一次遍历的时候会占用比较长的时间。 287 | 288 | 该函数的统计最终体现在 `ipvsadm --stat` 的结果中(Conns InPkts OutPkts InBytes OutBytes): 289 | 290 | ``` 291 | # ipvsadm -Ln --stats 292 | IP Virtual Server version 1.2.1 (size=4096) 293 | Prot LocalAddress:Port Conns InPkts OutPkts InBytes OutBytes # 相关统计 294 | -> RemoteAddress:Port 295 | TCP 10.85.0.10:9153 0 0 0 0 0 296 | -> 172.22.34.187:9153 0 0 0 0 0 297 | ``` 298 | 299 | 对于我们集群中的 `ipvs` 规则进行统计,我们发现大概在 30000 左右。 300 | 301 | ``` 302 | # ipvsadm -Ln --stats|wc -l 303 | ``` 304 | 305 | 既然每个 Network Namespace 下都会有 `estimation_timer` 的遍历,为什么只有 CPU#0 上的规则如此多呢? 306 | 307 | 这是因为只有主机的 Host Network Namespace 中才会有全部的 ipvs 规则,这个我们也可以通过 `ipvsadm -Ln` (执行在 Host Network Namespace 下) 验证。从现象来看,CPU#0 是 ipvs 模块加载的时候用于处理宿主机 Host Network Namespace 中的 ipvs 规则,当然这个核的加载完全是随机的。 308 | 309 | ## 4. 问题解决 310 | 311 | ### 4.1 问题解决方案 312 | 313 | 到此,问题已经彻底定位,由于我们服务早期部署的历史原因,短期内调整 Service 的数目会导致大量的迁移工作,中间还有云厂商 SLB 产生的大量规则,也没有办法彻底根除,单从技术上解决的话,我们可以采用的方式有以下 3 种: 314 | 315 | 1. 动态感知到宿主机 Network Namespace 中 ipvs `estimation_timer` 函数的函数,在 RPS 中设置关闭该 CPU 映射;该方式需要调整 RPS 的配置,而且 ipvs 处理主机 Network Namespace 的核数不固定,需要识别并调整配置,还需要处理重启时候的 ipvs 主机 Network Namespace 的变动; 316 | 2. 由于我们不需要 ipvs 这种统计的功能,可以通过修改 ipvs 驱动的方式来规避该问题;修改 ipvs 的驱动模块,需要重新加载该内核模块,也会导致主机服务上的短暂中断; 317 | 3. ipvs 模块将内核遍历统计调整成一个独立的内核线程进行统计; 318 | 319 | ipvs 规则在内核 timer 中遍历是 ipvs 移植到 k8s 上场景未适配的问题,社区应该需要把在 timer 中的遍历独立出去,但是这个方案需要社区的推动解决,远水解不了近渴。 320 | 321 | 通过上述 3 种方案的对比,解决我们当前抖动的问题都不太容易实施,为了保证生产环境的稳定和实施的难易程度,最终我们把眼光定位在 Linux Kernel 热修的 **kpatch****[12]** 方案上, kpath 实现的 livepatch 功能可以实时为正在运行的内核提供功能增强,无需重新启动系统。 322 | 323 | ### 4.2 kpatch livepatch 324 | 325 | Kpatch 是给 Linux 内核 livepatch 的工具,由 Redhat 公司出品。最早出现的打热补丁工具是 Ksplice。但是 Ksplice 被 Oracle 收购后,一些发行版生产商就不得不开发自己的热补丁工具,分别是 Redhat 的 Kpatch 和 Suse 的 KGraft。同时,在这两家厂商的推进下,kernel 4.0 开始,开始集成了 livepatch 技术。Kpatch 虽然是 Redhat 研发,但其也支持 Ubuntu、Debian、Oracle Linux 等的发行版。 326 | 327 | 这里我们简单同步一下实施的步骤,更多的文档可以从 kpath 项目中获取。 328 | 329 | #### 4.2.1 获取 kpath 编译和安装 330 | 331 | ``` 332 | $ git clone https://github.com/dynup/kpatch.git 333 | $ source test/integration/lib.sh 334 | # 中间会使用 yum 安装相关的依赖包,安装时间视网络情况而定,在阿里云的环境下需要的时间比较长 335 | $ sudo kpatch_dependencies 336 | $ cd kpatch 337 | 338 | # 进行编译 339 | $ make 340 | 341 | # 默认安装到 /usr/local,需要注意 kpatch-build 在目录 /usr/local/bin/ 下,而 kpatch 在 /usr/local/sbin/ 目录 342 | $ sudo make install 343 | ``` 344 | 345 | #### 4.2.2 生成内核源码 patch 346 | 347 | 在 kpatch 的使用过程中,需要使用到内核的源码,源码拉取的方式可以参考这里**我需要内核的源代码****[13]**。。 348 | 349 | ``` 350 | $ rpm2cpio kernel-3.10.0-1062.9.1.el7.src.rpm |cpio -div 351 | $ xz -d linux-3.10.0-1062.9.1.el7.tar.xz 352 | $ tar -xvf linux-3.10.0-1062.9.1.el7.tar 353 | $ cp -ra linux-3.10.0-1062.9.1.el7/ linux-3.10.0-1062.9.1.el7-patch 354 | ``` 355 | 356 | 此处我们将 `estimation_timer` 函数的实现设置为空 357 | 358 | ``` 359 | static void estimation_timer(unsigned long arg) 360 | { 361 | printk("hotfix estimation_timer patched\n"); 362 | return; 363 | } 364 | ``` 365 | 366 | 并生成对应的 patch 文件 367 | 368 | ``` 369 | # diff -u linux-3.10.0-1062.9.1.el7/net/netfilter/ipvs/ip_vs_est.c linux-3.10.0-1062.9.1.el7-patch/net/netfilter/ipvs/ip_vs_est.c > ip_vs_timer_v1.patch 370 | ``` 371 | 372 | #### 4.2.3 生产内核补丁并 livepatch 373 | 374 | 然后生成相关的 patch ko 文件并应用到内核: 375 | 376 | ``` 377 | # /usr/local/bin/kpatch-build ip_vs_timer_v1.patch --skip-gcc-check --skip-cleanup -r /root/kernel-3.10.0-1062.9.1.el7.src.rpm 378 | 379 | # 编译成功后会在当前目录生成 livepatch-ip_vs_timer_v1.ko 文件 380 | # 应用到内核中. 381 | 382 | # /usr/local/sbin/kpatch load livepatch-ip_vs_timer_v1.ko 383 | ``` 384 | 385 | 通过内核日志查看确认 386 | 387 | ``` 388 | $ dmesg -T 389 | [Thu Dec 3 19:50:50 2020] livepatch: enabling patch 'livepatch_ip_vs_timer_v1' 390 | [Thu Dec 3 19:50:50 2020] livepatch: 'livepatch_ip_vs_timer_v1': starting patching transition 391 | [Thu Dec 3 19:50:50 2020] hotfix estimation_timer patched 392 | ``` 393 | 394 | 至此通过我们的 livepatch 成功修订了 `estimation_timer` 的调用,一切看起来很成功。然后通过 `funcgraph` 工具查看 `estimation_timer` 函数不再出现在调用关系中。 395 | 396 | > 如果仅仅把函数设置为空的实现,等于是关闭了 `estimation_timer` 的调用,即使通过命令 unload 掉 livepatch,该函数的也不会恢复,因此在生产环境中建议将函数的 2s 调用设置成个可以接受的时间范围内,比如 5 分钟,这样在 unload 以后,可以在 5 分钟以后恢复 `estimation_timer` 的继续调用。 397 | 398 | ### 4.3 使用 kpatch 注意事项 399 | 400 | - kpatch 是基于内核版本生成的 ko 内核模块,必须保证后续 livepatch 的内核版本与编译机器的内核完全一致。 401 | - 通过手工 livepatch 的方式修复,如果保证机器在重启以后仍然生效需要通过 `install` 来启用 kpathc 服务进行保证。 402 | 403 | ``` 404 | # /usr/local/sbin/kpatch install livepatch-ip_vs_timer_v1.ko 405 | # systemctl start kpatch 406 | ``` 407 | 408 | - 在其他的机器上进行 livepatch 需要文件`kpatch`、`livepatch-ip_vs_timer_v1.ko` 和 `kpatch.service`(用于 install 后重启生效) 3 个文件即可。 409 | 410 | ## 5. 总结 411 | 412 | 网络抖动问题的排查,涉及应用层、网络协议栈和内核中运作机制等多方面的协调,排查过程中需要逐层排查、逐步缩小范围,在整个过程中,合适的工具至关重要,在我们本次问题的排查过程中, BPF 技术为我们排查的方向起到了至关重要的作用。BPF 技术的出现为我们观测和跟踪内核中的事件,提供了更加灵活的数据采集和数据分析的能力,在生产环境中我们已经将其广泛用于了监控网络底层的重传和抖动等维度,极大提升提升我们在偶发场景下的问题排查效率,希望更多的人能够从 BPF 技术中受益。 413 | 414 | **参考资料** 415 | [1] 416 | 417 | terway: *https://github.com/AliyunContainerService/terway*[2] 418 | 419 | BCC: *https://github.com/iovisor/bcc*[3] 420 | 421 | traceicmpsoftirq.py: *https://gist.github.com/DavadDi/62ee75228f03631c845c51af292c2b17*[4] 422 | 423 | INSTALL.md: *https://github.com/iovisor/bcc/blob/master/INSTALL.md*[5] 424 | 425 | 426 | 427 | get_rps_cpu: *https://elixir.bootlin.com/linux/v5.8/source/net/core/dev.c#L4305*[6] 428 | 429 | 430 | 431 | perf-tools: *https://github.com/brendangregg/perf-tools*[7] 432 | 433 | funcgraph: *https://github.com/brendangregg/perf-tools/blob/master/bin/funcgraph*[8] 434 | 435 | softirqs.py: *https://github.com/iovisor/bcc/blob/master/tools/softirqs.py*[9] 436 | 437 | ip_vs_core.c: *https://elixir.bootlin.com/linux/v5.8/source/net/netfilter/ipvs/ip_vs_core.c#L2469*[10] 438 | 439 | ip_vs_est.c: *https://elixir.bootlin.com/linux/v5.8/source/net/netfilter/ipvs/ip_vs_est.c#L187*[11] 440 | 441 | ip_vs_est.c: *https://elixir.bootlin.com/linux/v5.8/source/net/netfilter/ipvs/ip_vs_est.c#L96*[12] 442 | 443 | kpatch: *https://github.com/dynup/kpatch*[13] 444 | 445 | 我需要内核的源代码: *https://wiki.centos.org/zh/HowTos/I_need_the_Kernel_Source* 446 | 447 | ## 作者 448 | 449 | 狄卫华,溪恒 深入浅出BPF 450 | 451 | ## 原文链接 452 | 453 | https://mp.weixin.qq.com/s/dT3afS50qLS2GRpwj7X98g 454 | 455 | -------------------------------------------------------------------------------- /ebpf-guide/eBPF高级/区分三种类型的eBPF重定向.md: -------------------------------------------------------------------------------- 1 | # 区分三种类型的eBPF重定向 2 | 3 | Linux 内核中存在三种 eBPF 重定向方式,可能经常让开发人员感到困惑: 4 | 5 | 1. `bpf_redirect_peer()` 6 | 2. `bpf_redirect_neighbor()` 7 | 3. `bpf_redirect()` 8 | 9 | 这篇文章通过按历史顺序深入研究代码来帮助澄清它们,还讨论了现实世界中的用法和相关问题。 10 | 11 | # 1 基金会:`bpf_redirect()`, 2015 12 | 13 | 这个 BPF 助手是在 2015 年通过[这个补丁引入的,](https://github.com/torvalds/linux/commit/27b29f63058d2) 14 | 15 | ``` 16 | bpf: add bpf_redirect() helper 17 | 18 | Existing bpf_clone_redirect() helper clones skb before redirecting it to RX or 19 | TX of destination netdev. Introduce bpf_redirect() helper that does that without cloning. 20 | ... 21 | 22 | ``` 23 | 24 | ## 1.1 文档 25 | 26 | ### 描述 27 | 28 | `long bpf_redirect(ifindex, flags)`可以用来**将给定的数据包重定向到给定的网络设备** 用 index 标识`ifindex`。这个助手有点类似于 `bpf_clone_redirect()`,除了数据包没有被克隆,它提供了更高的性能(与`clone_redirect()`提交消息相比提高了 25% pps)。 29 | 30 | 返回值:**TC/XDP 判决**. 31 | 32 | ### 比较`bpf_clone_redirect()` 33 | 34 | | | `bpf_clone_redirect()` | **`bpf_redirect()`** | 35 | | --------------------------- | ------------------------ | ------------------------------------------------------------ | 36 | | 效率 | 低(涉及skb clone) | 高的 | 37 | | 重定向发生的地方 | 内部函数调用 | **函数调用后**(这个函数只返回一个结论,真正的重定向发生在**`skb_do_redirect()`**) | 38 | | 可以在 eBPF 程序之外使用 | 是的 | 不 | 39 | | 可能会改变底层的 skb 缓冲区 | 是(需要更多的重新验证) | 不 | 40 | | 跨网络重定向 | 不 | 不 | 41 | 42 | ## 1.2 内核实现/更改 43 | 44 | 现在让我们看看 Linux 内核做了哪些改变来支持这个功能。 45 | 46 | ### 1.增加TC动作类型**`TC_ACT_REDIRECT`** 47 | 48 | ``` 49 | diff --git a/include/uapi/linux/pkt_cls.h b/include/uapi/linux/pkt_cls.h 50 | @@ -87,6 +87,7 @@ enum { 51 | #define TC_ACT_STOLEN 4 52 | #define TC_ACT_QUEUED 5 53 | #define TC_ACT_REPEAT 6 54 | +#define TC_ACT_REDIRECT 7 55 | ``` 56 | 57 | ### 2. 添加新的 BPF 助手和系统调用 58 | 59 | ``` 60 | +static u64 bpf_redirect(u64 ifindex, u64 flags, u64 r3, u64 r4, u64 r5) 61 | +{ 62 | + struct redirect_info *ri = this_cpu_ptr(&redirect_info); 63 | + 64 | + ri->ifindex = ifindex; 65 | + ri->flags = flags; 66 | + return TC_ACT_REDIRECT; 67 | +} 68 | ``` 69 | 70 | 我们可以看到这个助手只设置`ifindex`然后`flags`返回一个 `TC_ACT_REDIRECT`给调用者,这就是为什么我们说**真正的重定向发生在 bpf_redirect() 完成之后**. 71 | 72 | ### 3. TC BPF中的流程重定向逻辑 73 | 74 | 当 BPF 程序(with `bpf_redirect()`in the program)被附加到 TC ingress hook 时, `bpf_redirect()`将在**tc_classify()**方法: 75 | 76 | ``` 77 | @@ -3670,6 +3670,14 @@ static inline struct sk_buff *handle_ing(struct sk_buff *skb, 78 | switch(tc_classify()) { // <-- bpf_redirect() executes in the tc_classify() method 79 | ... 80 | case TC_ACT_QUEUED: 81 | kfree_skb(skb); 82 | return NULL; 83 | + case TC_ACT_REDIRECT: 84 | + /* skb_mac_header check was done by cls/act_bpf, so 85 | + * we can safely push the L2 header back before 86 | + * redirecting to another netdev 87 | + */ 88 | + __skb_push(skb, skb->mac_len); 89 | + skb_do_redirect(skb); 90 | + return NULL; 91 | + 92 | +struct redirect_info { 93 | + u32 ifindex; 94 | + u32 flags; 95 | +}; 96 | +static DEFINE_PER_CPU(struct redirect_info, redirect_info); 97 | + 98 | +int skb_do_redirect(struct sk_buff *skb) 99 | +{ 100 | + struct redirect_info *ri = this_cpu_ptr(&redirect_info); 101 | + struct net_device *dev; 102 | + 103 | + dev = dev_get_by_index_rcu(dev_net(skb->dev), ri->ifindex); 104 | + ri->ifindex = 0; 105 | + 106 | + if (BPF_IS_REDIRECT_INGRESS(ri->flags)) 107 | + return dev_forward_skb(dev, skb); 108 | + 109 | + skb->dev = dev; 110 | + return dev_queue_xmit(skb); 111 | +} 112 | ``` 113 | 114 | 如果返回`TC_ACT_REDIRECT`,`skb_do_redirect()`则将执行真正的重定向。 115 | 116 | ## 1.3 调用栈 117 | 118 | ``` 119 | pkt -> NIC -> TC ingress -> handle_ing() 120 | |-verdict = tc_classify() // exec BPF code 121 | | |-bpf_redirect() // return verdict 122 | | 123 | |-switch (verdict) { 124 | case TC_ACK_REDIRECT: 125 | skb_do_redirect() // to the target net device 126 | |-if ingress: 127 | | dev_forward_skb() 128 | |-else: 129 | dev_queue_xmit() 130 | ``` 131 | 132 | 从最后几行我们可以看出 `bpf_redirect()` **支持入口和出口重定向**. 133 | 134 | # 2 出口优化:`bpf_redirect_neighbor()`, 2020 135 | 136 | `bpf_redirect()`在 Linux 内核中出现 五年后,在[补丁](https://github.com/torvalds/linux/commit/b4ab31414970a)中为其引入了出口端优化: 137 | 138 | ``` 139 | bpf: Add redirect_neigh helper as redirect drop-in 140 | 141 | Add a redirect_neigh() helper as redirect() drop-in replacement 142 | for the xmit side. Main idea for the helper is to be very similar 143 | in semantics to the latter just that the skb gets injected into 144 | the neighboring subsystem in order to let the stack do the work 145 | it knows best anyway to populate the L2 addresses of the packet 146 | and then hand over to dev_queue_xmit() as redirect() does. 147 | 148 | This solves two bigger items: 149 | i) skbs don't need to go up to the stack on the host facing veth ingress side for traffic egressing 150 | the container to achieve the same for populating L2 which also has the huge advantage that 151 | ii) the skb->sk won't get orphaned in ip_rcv_core() when entering the IP routing layer on the host stack. 152 | 153 | Given that skb->sk neither gets orphaned when crossing the netns 154 | as per 9c4c325 ("skbuff: preserve sock reference when scrubbing 155 | the skb.") the helper can then push the skbs directly to the phys 156 | device where FQ scheduler can do its work and TCP stack gets proper 157 | backpressure given we hold on to skb->sk as long as skb is still 158 | residing in queues. 159 | 160 | With the helper used in BPF data path to then push the skb to the 161 | phys device, I observed a stable/consistent TCP_STREAM improvement 162 | on veth devices for traffic going container -> host -> host -> 163 | container from ~10Gbps to ~15Gbps for a single stream in my test 164 | environment. 165 | ``` 166 | 167 | ## 2.1 比较`bpf_redirect()` 168 | 169 | | | `bpf_redirect()` | **`bpf_redirect_neighbor()`** | 170 | | -------------------------------------- | ---------------- | ----------------------------------------------------- | 171 | | 支持方向 | 入口和出口 | **仅出口** | 172 | | 通过内核堆栈填充 L2 地址(邻居子系统) | 不 | 是的 (**根据 pkt 中的 L3 信息填写例如 MAC 地址)** | 173 | | 跨网络重定向 | 不 | 不 | 174 | | 其他 | | `flags`参数保留且必须为 0;目前仅支持 tc BPF 程序类型 | 175 | 176 | 返回:成功时返回**TC_ACT_REDIRECT**,错误时返回**TC_ACT_SHOT 。** 177 | 178 | ## 2.2 内核实现/更改 179 | 180 | ### 1.修改`skb_do_redirect()`,喜欢新的 181 | 182 | ``` 183 | int skb_do_redirect(struct sk_buff *skb) 184 | { 185 | struct bpf_redirect_info *ri = this_cpu_ptr(&bpf_redirect_info); 186 | struct net_device *dev; 187 | + u32 flags = ri->flags; 188 | 189 | dev = dev_get_by_index_rcu(dev_net(skb->dev), ri->tgt_index); 190 | ri->tgt_index = 0; 191 | @@ -2231,7 +2439,22 @@ int skb_do_redirect(struct sk_buff *skb) 192 | return -EINVAL; 193 | } 194 | 195 | - return __bpf_redirect(skb, dev, ri->flags); 196 | + return flags & BPF_F_NEIGH ? 197 | + __bpf_redirect_neigh(skb, dev) : 198 | + __bpf_redirect(skb, dev, flags); 199 | +} 200 | ``` 201 | 202 | ### 2. 添加`bpf_redirect_neigh()`助手/包装器和系统调用 203 | 204 | 略,看下面的调用栈。 205 | 206 | ## 2.3 调用栈 207 | 208 | ``` 209 | skb_do_redirect 210 | |-__bpf_redirect_neigh(skb, dev) : 211 | |-__bpf_redirect_neigh_v4 212 | |-rt = ip_route_output_flow() 213 | |-skb_dst_set(skb, &rt->dst); 214 | |-bpf_out_neigh_v4(net, skb) 215 | |-neigh = ip_neigh_for_gw(rt, skb, &is_v6gw); 216 | |-sock_confirm_neigh(skb, neigh); 217 | |-neigh_output(neigh, skb, is_v6gw); // xmit with L2 header properly set 218 | |-neigh->output() 219 | |-neigh_direct_output?? 220 | |-dev_queue_xmi() 221 | 222 | ``` 223 | 224 | 请注意,虽然这是对 的优化`bpf_redirec()`,但仍然需要**遍历整个内核堆栈**. 225 | 226 | # 3 Ingress优化:`bpf_redirect_peer()`, 2020 227 | 228 | 在2020 年的[补丁](https://github.com/torvalds/linux/commit/9aa1206e8f482)中引入, 229 | 230 | ``` 231 | bpf: Add redirect_peer helper 232 | 233 | Add an efficient ingress to ingress netns switch that can be used out of tc BPF 234 | programs in order to redirect traffic from host ns ingress into a container 235 | veth device ingress without having to go via CPU backlog queue [0]. 236 | 237 | For local containers this can also be utilized and path via CPU backlog queue only needs 238 | to be taken once, not twice. On a high level this borrows from ipvlan which does 239 | similar switch in __netif_receive_skb_core() and then iterates via another_round. 240 | This helps to reduce latency for mentioned use cases. 241 | 242 | ``` 243 | 244 | ## 3.1 比较`bpf_redirect()` 245 | 246 | | | `bpf_redirect()` | **`bpf_redirect_peer()`** | 247 | | ------------ | ---------------- | ------------------------------------------------------------ | 248 | | 支持方向 | 入口和出口 | **仅入口** | 249 | | 跨网络重定向 | 不 | **是(netns 切换发生在从入口到入口之间,无需经过 CPU 的积压队列)** | 250 | | 其他 | | `flags`参数保留且必须为 0;目前仅支持 tc BPF 程序类型;对等设备必须位于不同的网络中 | 251 | 252 | 返回:成功时返回**TC_ACT_REDIRECT**,错误时返回**TC_ACT_SHOT 。** 253 | 254 | ## 3.2 内核实现/更改 255 | 256 | ### 1.添加新的重定向标志 257 | 258 | ``` 259 | diff --git a/net/core/filter.c b/net/core/filter.c 260 | index 5da44b11e1ec..fab951c6be57 100644 261 | --- a/net/core/filter.c 262 | +++ b/net/core/filter.c 263 | @@ -2380,8 +2380,9 @@ static int __bpf_redirect_neigh(struct sk_buff *skb, struct net_device *dev) 264 | 265 | /* Internal, non-exposed redirect flags. */ 266 | enum { 267 | - BPF_F_NEIGH = (1ULL << 1), 268 | -#define BPF_F_REDIRECT_INTERNAL (BPF_F_NEIGH) 269 | + BPF_F_NEIGH = (1ULL << 1), 270 | + BPF_F_PEER = (1ULL << 2), 271 | +#define BPF_F_REDIRECT_INTERNAL (BPF_F_NEIGH | BPF_F_PEER) 272 | ``` 273 | 274 | ### 2. 添加助手/系统调用 275 | 276 | ``` 277 | +BPF_CALL_2(bpf_redirect_peer, u32, ifindex, u64, flags) 278 | +{ 279 | + struct bpf_redirect_info *ri = this_cpu_ptr(&bpf_redirect_info); 280 | + 281 | + if (unlikely(flags)) 282 | + return TC_ACT_SHOT; 283 | + 284 | + ri->flags = BPF_F_PEER; 285 | + ri->tgt_index = ifindex; 286 | + 287 | + return TC_ACT_REDIRECT; 288 | +} 289 | ``` 290 | 291 | ### 3.允许重新进入TC ingress处理(这里针对对端设备) 292 | 293 | ``` 294 | @@ -5163,7 +5167,12 @@ static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc, 295 | skip_taps: 296 | #ifdef CONFIG_NET_INGRESS 297 | if (static_branch_unlikely(&ingress_needed_key)) { 298 | - skb = sch_handle_ingress(skb, &pt_prev, &ret, orig_dev); 299 | + bool another = false; 300 | + 301 | + skb = sch_handle_ingress(skb, &pt_prev, &ret, orig_dev, 302 | + &another); 303 | + if (another) 304 | + goto another_round; 305 | ``` 306 | 307 | `sch_handle_ingress()`变化: 308 | 309 | ``` 310 | @@ -4974,7 +4974,11 @@ sch_handle_ingress(struct sk_buff *skb, struct packet_type **pt_prev, int *ret, 311 | * redirecting to another netdev 312 | */ 313 | __skb_push(skb, skb->mac_len); 314 | - skb_do_redirect(skb); 315 | + if (skb_do_redirect(skb) == -EAGAIN) { 316 | + __skb_pull(skb, skb->mac_len); 317 | + *another = true; 318 | + break; 319 | + } 320 | 321 | int skb_do_redirect(struct sk_buff *skb) 322 | { 323 | struct bpf_redirect_info *ri = this_cpu_ptr(&bpf_redirect_info); 324 | + struct net *net = dev_net(skb->dev); 325 | struct net_device *dev; 326 | u32 flags = ri->flags; 327 | - 328 | - dev = dev_get_by_index_rcu(dev_net(skb->dev), ri->tgt_index); 329 | + dev = dev_get_by_index_rcu(net, ri->tgt_index); 330 | ri->tgt_index = 0; 331 | + ri->flags = 0; 332 | + if (flags & BPF_F_PEER) { 333 | + const struct net_device_ops *ops = dev->netdev_ops; 334 | + 335 | + dev = ops->ndo_get_peer_dev(dev); 336 | + if (unlikely(!dev || !is_skb_forwardable(dev, skb) || net_eq(net, dev_net(dev)))) 337 | + goto out_drop; 338 | + skb->dev = dev; 339 | + return -EAGAIN; 340 | } 341 | - 342 | return flags & BPF_F_NEIGH ? __bpf_redirect_neigh(skb, dev) : __bpf_redirect(skb, dev, flags); 343 | } 344 | ``` 345 | 346 | ## 3.3 调用栈 347 | 348 | [附上我的网络堆栈帖子](http://arthurchiao.art/blog/linux-net-stack-implementation-rx-zh/)中的图片 , 349 | 350 | ![img](http://arthurchiao.art/assets/img/linux-net-stack/netif_receive_skb_list_internal.png) 351 | 352 | 图 进入内核栈:L2 处理步骤 353 | 354 | ``` 355 | __netif_receive_skb_core 356 | | 357 | |-// Device driver processing, e.g. update device's rx/tx stats 358 | | 359 | |-another_round: <------------------<-----------+ 360 | |-// Generic XDP processing | 361 | | | with skb->dev changed to the peer device, the next round 362 | |-// Tap processing if not skipped | "G-XDP -> TAP -> TC" processings will be for the peer device, 363 | | | which means we successfully bypassed tons of stuffs 364 | |-// TC BPF ingress processing if not skipped | (and entered container's netns from the default/host netns) 365 | |-sch_handle_ingress(&another) | as shown in the above picture 366 | |-if another: | 367 | | goto another_round -------------->-----------+ 368 | | 369 | |-// Netfilter processing if not skipped 370 | ``` 371 | 372 | 一些解释: 373 | 374 | 1. 第一次执行**`sch_handle_ingress()`**用于当前网络设备(例如`eth0`物理主机); 375 | 2. 如果它返回**`another==true`**,然后执行将转到**`another_round`**; 然后 376 | 3. 我们来到**`sch_handle_ingress()`**第二次,这一次,我们在`eth0`重定向到的设备(例如容器内部)的 TC 入口挂钩上执行。 377 | 378 | ## 3.4 用例和性能评估 379 | 380 | Cilium网络解决方案中的两种场景: 381 | 382 | 1. **物理网卡 -> 容器网卡**重定向 383 | 2. **“容器 A -> 容器 B”重定向**在同一个主机 384 | 385 | 而在 Cilium 中,此行为由专用选项控制**`--enable-host-legacy-routing=true/false`**: 386 | 387 | 1. With `true`: 关闭对等重定向优化,仍然像往常一样遍历整个内核堆栈; 388 | 2. 使用`false`: 打开对等重定向(如果内核支持),预计会获得显着的性能提升。 389 | 390 | 性能基准参见[Cilium 1.9 Release Notes](https://cilium.io/blog/2020/11/10/cilium-19/#veth),我们已经在集群中使用 Cilium 1.10.7 + 5.10 内核双重确认基准。 391 | 392 | ## 3.5 影响和已知问题 393 | 394 | ### Kubernetes:错误的 Pod 入口统计信息 395 | 396 | `kubelet`通过 cadvisor/netlink 收集每个 pod 的网络统计信息(例如 rx_packets、rx_bytes),并通过 10250 指标端口公开这些指标。 397 | 398 | > 有关更多信息,请参阅[kubelet 文档](https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/)。 399 | 400 | 在 Kubernetes 节点上: 401 | 402 | ``` 403 | $ curl -H 'Authorization: Bearer eyJh...' -X GET -d '{"num_stats": 1,"containerName": "/kubepods","subContainers": true}' --insecure https://127.0.0.1:10250/stats/ > stats.json 404 | ``` 405 | 406 | 获取特定 pod 的统计信息: 407 | 408 | ``` 409 | $ cat stats.json | jq '."/kubepods/burstable/pod42ef7dc5-a27f-4ee5-ac97-54c3ce93bc9b/47585f9593"."stats"[0]."network"' | head -n8 | egrep "(rx_bytes|rx_packets|tx_bytes|tx_packets)" 410 | "rx_bytes": 34344009, 411 | "rx_packets": 505446, 412 | "tx_bytes": 5344339889, 413 | "tx_packets": 6214124, 414 | ``` 415 | 416 | 这些统计的数据来源其实来自**sysfs/procfs**. 例如,pod 的 rx_bytes 是通过pod 的网络接口`cat /sys/class/net//statistics/rx_bytes` 在哪里检索的。`` 417 | 418 | **出现问题**使用时`bpf_redirect_peer()`,因为数据包从物理 NIC 的 TC 入口点直接飞到 Pod 的 TC 入口点,这 **跳过 pod 的 NIC 的驱动程序处理步骤**,所以像上面这样的 rx/tx 统计数据将不会正确更新(只有少数数据包会通过驱动程序)。因此,入口统计数据(如 pps/带宽)将几乎为零。 419 | 420 | 421 | 422 | ## 作者 423 | 424 | arthurchiao 425 | 426 | ## 原文链接 427 | 428 | http://arthurchiao.art/blog/differentiate-bpf-redirects/ -------------------------------------------------------------------------------- /ebpf-guide/eBPF高级/eBPF捕获生产流量的实用指南.md: -------------------------------------------------------------------------------- 1 | # 构建基于eBPF的协议跟踪器,eBPF捕获生产流量的实用指南 2 | 3 | > - 使用tcpdump 等典型网络捕获工具[监控 HTTP 会话](https://www.datadoghq.com/blog/ebpf-guide/#challenges-with-monitoring-http-sessions)的挑战 4 | > - [eBPF 是什么](https://www.datadoghq.com/blog/ebpf-guide/#what-exactly-is-ebpf)以及它如何克服这些挑战 5 | > - 如何[构建基于 eBPF 的协议跟踪器](https://www.datadoghq.com/blog/ebpf-guide/#building-an-ebpf-based-traffic-capturer)以最小难度和低开销捕获生产流量 6 | > - 使用 eBPF 捕获生产流量的实用指南 7 | 8 | 监视 HTTP 会话提供了一种潜在的强大方法来了解您的 Web 服务器,但在实践中,这样做可能很复杂且占用大量资源。[扩展的 Berkeley 数据包过滤器 (eBPF)](https://ebpf.io/)技术使您能够克服这些挑战,为您提供一种简单高效的方法来处理应用层流量以满足您的故障排除需求。 9 | 10 | 在这篇文章中,我们将介绍: 11 | 12 | - 使用tcpdump 等典型网络捕获工具[监控 HTTP 会话](https://www.datadoghq.com/blog/ebpf-guide/#challenges-with-monitoring-http-sessions)的挑战 13 | - [eBPF 是什么](https://www.datadoghq.com/blog/ebpf-guide/#what-exactly-is-ebpf)以及它如何克服这些挑战 14 | - 如何[构建基于 eBPF 的协议跟踪器](https://www.datadoghq.com/blog/ebpf-guide/#building-an-ebpf-based-traffic-capturer)以最小难度和低开销捕获生产流量 15 | 16 | ## [监控 HTTP 会话的挑战](https://www.datadoghq.com/blog/ebpf-guide/#challenges-with-monitoring-http-sessions) 17 | 18 | 当您注意到 HTTP 服务器的行为异常时,确定导致问题的原因并不总是那么容易。通常,您可能会通过查看配置设置或筛选日志条目以获取见解来开始故障排除。但是,如果这些初步调查没有充分阐明问题,您接下来可能会开始考虑如何检查 HTTP 流量以收集更多信息。 19 | 20 | 查看构成 HTTP 会话的请求和响应线程中的详细信息确实可以提供有关影响 HTTP 服务器的问题的重要信息。但在实践中,分析 HTTP 会话数据很复杂,最常用的网络流量捕获方法对此用途有限。 21 | 22 | 23 | 24 | [例如, Tcpdump](https://www.tcpdump.org/)是用于捕获生产中流量的最常见解决方案之一。但是 tcpdump 并没有整齐地显示 HTTP 会话供您分析,它只是为您提供数百兆字节(甚至千兆字节)的单独数据包,然后您需要将这些数据包梳理并拼凑成会话。 25 | 26 | 使用 tcpdump 的另一种方法是在源代码中添加一个算法,自动排序和显示有关 HTTP 会话的信息。但是,此方法需要您对生产代码进行检测,并且以这种方式处理所有 HTTP 流量会导致严重的性能损失。 27 | 28 | 29 | 30 | 这正是[eBPF](https://www.infoq.com/articles/gentle-linux-ebpf-introduction/)的用武之地。eBPF于 2014 年发布,是 Linux 应用程序在 Linux 内核空间中执行代码的一种机制。借助 eBPF,您可以创建功能强大的流量捕获工具,其功能远远超过标准工具。更具体地说,eBPF 允许您添加多个过滤层并直接从内核捕获流量。这些功能将输出限制为仅相关数据,使您能够处理和过滤您的应用程序流量,即使在吞吐量很高时也只会对性能产生有限的影响。 31 | 32 | ## [eBPF到底是什么?](https://www.datadoghq.com/blog/ebpf-guide/#what-exactly-is-ebpf) 33 | 34 | 为了更好地理解 eBPF,了解一点原始或经典的 Berkeley Packet Filter (BPF) 会有所帮助。BPF 定义了一种数据包过滤器,实现为虚拟机,可以在 Linux 内核中运行。在 BPF 之前,数据包过滤器仅在用户空间运行,这比内核级过滤更占用 CPU。BPF 通常用于需要高效捕获和分析数据包的程序。例如,它允许 tcpdump 非常快速地过滤掉不相关的数据包。 35 | 36 | 但是请注意,BPF(以及 tcpdump)快速处理*数据包*的能力不足以处理 HTTP*会话*。BPF 允许您检查单个数据包的有效负载。另一方面,HTTP 会话通常由多个 TCP 数据包组成,因此需要在第 7 层(应用层)对流量进行更复杂的处理。BPF 不提供处理此类过滤的方法。 37 | 38 | 39 | 40 | BPF 的 eBPF 扩展正是为此目的而创建的。这种较新的技术允许您向内核系统调用 (syscalls) 和函数(包括与网络相关的函数)添加hook,以提供对流量有效负载和函数结果(成功/失败)的可见性。因此,使用 eBPF,您可以独立于向内核发送数据的应用程序启用复杂的功能和网络流量处理,包括第 7 层过滤。多亏了 eBPF,事实上,许多公司现在可以提供安全性和可观察性功能,甚至不需要您检测服务器端代码——或者完全不了解该代码。有关 eBPF 的更多信息,您可以访问 ebpf.io 上的项目[页面](https://ebpf.io/)。 41 | 42 | 现在我们已经介绍了什么是 eBPF 以及它使我们能够做什么,我们可以开始构建 eBPF 协议跟踪器。 43 | 44 | ## [构建基于 eBPF 的流量捕获器](https://www.datadoghq.com/blog/ebpf-guide/#building-an-ebpf-based-traffic-capturer) 45 | 46 | 在本演练中,我们将使用 eBPF 捕获由 Go 编写的 REST API 服务器处理的网络流量。作为典型的 eBPF 代码,我们的捕获工具将包括一个执行系统调用hook的*内核代理*和一个处理通过hook从内核发送的事件的*用户模式代理。* 47 | 48 | > **笔记** 49 | > 50 | > 该演练的灵感来自[Pixie Lab](https://pixielabs.ai/)基于 eBPF 的数据收集器,示例代码片段取自 Pixie tracer[公共 repo](https://github.com/pixie-io/pixie)。演练的完整代码可以在[这里](https://github.com/DataDog/ebpf-training/tree/main/workshop1)找到。为简单起见,下面的代码片段仅代表关键部分。 51 | 52 | 要执行此演练,我们需要一台运行任何基本 Linux 发行版(例如 Ubuntu 或 Debian)并安装以下组件的机器: 53 | 54 | - [BPF 编译器集合 (BCC) 工具](https://www.containiq.com/post/bcc-tools)包。按照[此处](https://github.com/iovisor/bcc/blob/master/INSTALL.md)的安装指南进行操作。 55 | - Golang 版本 1.16+。按照[此处](https://go.dev/doc/install)的安装指南进行操作。 56 | 57 | 为简单起见,我们创建了一个具有上述依赖项的 Docker 容器(基于 Debian)。查看[存储库](https://github.com/DataDog/ebpf-training/tree/main/workshop1)以获取运行说明。 58 | 59 | ### [启动网络服务器](https://www.datadoghq.com/blog/ebpf-guide/#starting-the-web-server) 60 | 61 | 以下是接收单个 POST 请求并使用随机生成的负载进行响应的 HTTP Web 服务器的示例。 62 | 63 | ``` 64 | package main 65 | 66 | ... 67 | 68 | const ( 69 | defaultPort = "8080" 70 | maxPayloadSize = 10 * 1024 * 1024 // 10 MB 71 | letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" 72 | ) 73 | 74 | ... 75 | 76 | // customResponse holds the requested size for the response payload. 77 | type customResponse struct { 78 | Size int `json:"size"` 79 | } 80 | 81 | func postCustomResponse(context *gin.Context) { 82 | 83 | var customResp customResponse 84 | 85 | if err := context.BindJSON(&customResp); err != nil { 86 | _ = context.AbortWithError(http.StatusBadRequest, err) 87 | return 88 | } 89 | if customResp.Size > maxPayloadSize { 90 | _ = context.AbortWithError(http.StatusBadRequest, fmt.Errorf("requested size %d is bigger than max allowed %d", customResp, maxPayloadSize)) 91 | return 92 | } 93 | 94 | context.JSON(http.StatusOK, map[string]string{"answer": randStringBytes(customResp.Size)}) 95 | } 96 | 97 | func main() { 98 | engine := gin.New() 99 | 100 | engine.Use(gin.Recovery()) 101 | engine.POST("/customResponse", postCustomResponse) 102 | 103 | port := os.Getenv("PORT") 104 | 105 | if port == "" { 106 | port = defaultPort 107 | } 108 | 109 | fmt.Printf("listening on 0.0.0.0:%s\n", port) 110 | 111 | if err := engine.Run(fmt.Sprintf("0.0.0.0:%s", port)); err != nil { 112 | log.Fatal(err) 113 | } 114 | } 115 | ``` 116 | 117 | 我们可以使用以下命令运行此服务器: 118 | 119 | ``` 120 | go run server.go 121 | ``` 122 | 123 | 接下来,我们可以使用以下命令来触发服务器的输出并验证服务器是否正常工作: 124 | 125 | ``` 126 | curl -X POST http://localhost:8080/customResponse -d '{"size": 100}' 127 | ``` 128 | 129 | ### [查找要跟踪的系统调用](https://www.datadoghq.com/blog/ebpf-guide/#finding-the-syscalls-to-track) 130 | 131 | 一旦 Web 服务器启动并运行,在构建跟踪器之前我们需要做的第一件事就是确定哪些系统调用正在用于 HTTP 通信。我们将使用该`strace`工具来完成这项任务。 132 | 133 | 更具体地说,我们可以运行服务器`strace`并使用该`-f`选项从服务器线程捕获系统调用。通过该`-o`选项,我们可以将所有输出写入一个我们可以命名的文本文件**系统调用转储.txt**. 为此,我们运行以下命令: 134 | 135 | ``` 136 | sudo strace -f -o syscalls_dump.txt go run server.go 137 | ``` 138 | 139 | 接下来,如果我们重新运行上面的`curl`命令并检查**系统调用转储.txt**,我们可以观察到以下情况: 140 | 141 | ``` 142 | 38988 accept4(3, 143 | 38987 nanosleep({tv_sec=0, tv_nsec=20000}, 144 | 38988 <... accept4 resumed>{sa_family=AF_INET, sin_port=htons(57594), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_CLOEXEC|SOCK_NONBLOCK) = 7 145 | ... 146 | 38988 read(7, 147 | 38987 nanosleep({tv_sec=0, tv_nsec=20000}, 148 | 38988 <... read resumed>"POST /customResponse HTTP/1.1\r\nH"..., 4096) = 175 149 | ... 150 | 38988 write(7, "HTTP/1.1 200 OK\r\nContent-Type: a"..., 237 151 | ... 152 | 38989 close(7) 153 | ``` 154 | 155 | 我们可以看到,起初,服务器使用`accept4`系统调用来接受新连接。我们还可以看到新套接字的文件描述符 (FD) 是 7(系统调用的返回码)。此外,我们可以看到对于每个其他系统调用,第一个参数(即 FD)是 7,因此所有操作都发生在同一个套接字上。 156 | 157 | 这是流程: 158 | 159 | 1. 使用`accept4`系统调用接受新连接。 160 | 2. `read`使用套接字文件描述符上的系统调用从套接字中读取内容。 161 | 3. `write`使用套接字文件描述符上的系统调用将响应写入套接字。 162 | 4. `close`最后,使用系统调用关闭文件描述符。 163 | 164 | 现在我们了解了服务器是如何工作的,我们可以继续构建我们的 HTTP 捕获工具。 165 | 166 | ### [构建内核代理(eBPF HOOK)](https://www.datadoghq.com/blog/ebpf-guide/#building-the-kernel-agent-ebpf-hooks) 167 | 168 | [BCC](https://www.containiq.com/post/bcc-tools)和[libbpf](https://www.containiq.com/post/libbpf)是两个主要的开发框架,可用于为 BPF 和 eBPF 创建内核代理。为简单起见,我们将在本演练中使用 BCC 框架,因为它在今天更为普遍。(不过,一般来说,我们建议使用 libbpf。有关这些框架的更多信息,请参阅[本文](https://devops.com/libbpf-vs-bcc-for-bpf-development/)。) 169 | 170 | 为了构建内核代理,我们将实现八个hook(用于`accept4`、`read`、`write`和`close`系统调用的入口和出口hook)。钩子驻留在内核中,用 C 语言编写。我们需要所有这些钩子的组合来执行完整的捕获过程。在创建内核代理时,我们还将使用辅助结构和映射来存储系统调用的参数。我们将在下面解释这些元素中的每一个的基础知识,但要了解更多信息,您还可以在我们的存储库中查看整个[内核代码](https://github.com/seek-ret/ebpf-training/blob/main/workshop1/capture-traffic/sourcecode.c)。 171 | 172 | #### [hook accept4系统调用](https://www.datadoghq.com/blog/ebpf-guide/#hooking-the-accept4-syscall) 173 | 174 | 首先,我们需要hook `accept4`系统调用。在 eBPF 中,我们可以在每个系统调用进入和退出时(换句话说,就在代码运行之前和之后)放置一个钩子。该条目对于获取系统调用的输入参数很有用,而返回对于了解系统调用是否按预期工作很有用。 175 | 176 | 在下面的代码片段中,我们声明了一个结构来将输入参数保存在`accept4`系统调用的入口中。然后我们在系统调用的出口中使用此信息,我们可以在其中确定系统调用是否成功。 177 | 178 | ``` 179 | // Copyright (c) 2018 The Pixie Authors. 180 | // Licensed under the Apache License, Version 2.0 (the "License") 181 | // Original source: https://github.com/pixie-io/pixie/blob/main/src/stirling/source%5C_connectors/socket%5C_tracer/bcc%5C_bpf/socket%5C_trace.c 182 | 183 | // A helper struct that holds the addr argument of the syscall. 184 | struct accept_args_t { 185 | struct sockaddr_in* addr; 186 | }; 187 | 188 | // A helper map that will help us cache the input arguments of the accept syscall 189 | // between the entry hook and the return hook. 190 | BPF_HASH(active_accept_args_map, uint64_t, struct accept_args_t); 191 | 192 | // Hooking the entry of accept4 193 | // the signature of the syscall is int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 194 | int syscall__probe_entry_accept4(struct pt_regs* ctx, int sockfd, struct sockaddr* addr, socklen_t* addrlen) { 195 | // Getting a unique ID for the relevant thread in the relevant pid. 196 | // That way we can link different calls from the same thread. 197 | 198 | uint64_t id = bpf_get_current_pid_tgid(); 199 | 200 | // Keep the addr in a map to use during the accept4 exit hook. 201 | struct accept_args_t accept_args = {}; 202 | accept_args.addr = (struct sockaddr_in *)addr; 203 | active_accept_args_map.update(&id, &accept_args); 204 | 205 | return 0; 206 | } 207 | 208 | // Hooking the exit of accept4 209 | int syscall__probe_ret_accept4(struct pt_regs* ctx) { 210 | uint64_t id = bpf_get_current_pid_tgid(); 211 | 212 | // Pulling the addr from the map. 213 | struct accept_args_t* accept_args = active_accept_args_map.lookup(&id); 214 | // If the id exist in the map, we will get a non empty pointer that holds 215 | // the input address argument from the entry of the syscall. 216 | if (accept_args != NULL) { 217 | process_syscall_accept(ctx, id, accept_args); 218 | } 219 | 220 | // Anyway, in the end clean the map. 221 | active_accept_args_map.delete(&id); 222 | 223 | return 0; 224 | } 225 | ``` 226 | 227 | 上面的代码片段向我们展示了hook系统调用的入口和出口的最少代码,以及将输入参数保存在系统调用入口中以便稍后在系统调用出口中使用的方法。 228 | 229 | 我们为什么要做这个?由于我们无法知道系统调用在其进入期间是否会成功,并且我们无法在其退出期间访问输入参数,因此我们需要存储参数,直到我们确定系统调用成功为止。只有这样我们才能执行我们的逻辑。 230 | 231 | 232 | 233 | 我们的特殊逻辑在 中`process_syscall_accept`,它检查系统调用是否成功完成。然后,我们将连接信息保存在全局映射中,以便我们可以在其他系统调用(`read`、`write`和`close`)中使用它。 234 | 235 | 在下面的代码片段中,我们创建了一个函数 ( `process_syscall_accept`) 供`accept4`钩子使用,并在我们自己的映射中注册与服务器建立的任何新连接。然后,在代码片段的最后一部分,我们提醒用户模式代理服务器接受了新连接。 236 | 237 | ``` 238 | // Copyright (c) 2018 The Pixie Authors. 239 | // Licensed under the Apache License, Version 2.0 (the "License") 240 | // Original source: https://github.com/pixie-io/pixie/blob/main/src/stirling/source%5C_connectors/socket%5C_tracer/bcc%5C_bpf/socket%5C_trace.c 241 | 242 | // A struct representing a unique ID that is composed of the pid, the file 243 | // descriptor and the creation time of the struct. 244 | struct conn_id_t { 245 | // Process ID 246 | uint32_t pid; 247 | 248 | // The file descriptor to the opened network connection. 249 | int32_t fd; 250 | 251 | // Timestamp at the initialization of the struct. 252 | uint64_t tsid; 253 | }; 254 | 255 | // This struct contains information collected when a connection is established, 256 | // via an accept4() syscall. 257 | struct conn_info_t { 258 | // Connection identifier. 259 | struct conn_id_t conn_id; 260 | 261 | // The number of bytes written/read on this connection. 262 | int64_t wr_bytes; 263 | int64_t rd_bytes; 264 | 265 | // A flag indicating we identified the connection as HTTP. 266 | bool is_http; 267 | }; 268 | 269 | // A struct describing the event that we send to the user mode upon a new connection. 270 | struct socket_open_event_t { 271 | // The time of the event. 272 | uint64_t timestamp_ns; 273 | 274 | // A unique ID for the connection. 275 | struct conn_id_t conn_id; 276 | 277 | // The address of the client. 278 | struct sockaddr_in addr; 279 | }; 280 | 281 | // A map of the active connections. The name of the map is conn_info_map 282 | // the key is of type uint64_t, the value is of type struct conn_info_t, 283 | // and the map won't be bigger than 128KB. 284 | BPF_HASH(conn_info_map, uint64_t, struct conn_info_t, 131072); 285 | 286 | // A perf buffer that allows us send events from kernel to user mode. 287 | // This perf buffer is dedicated for special type of events - open events. 288 | BPF_PERF_OUTPUT(socket_open_events); 289 | 290 | // A helper function that checks if the syscall finished successfully and if it did 291 | // saves the new connection in a dedicated map of connections 292 | static __inline void process_syscall_accept(struct pt_regs* ctx, uint64_t id, const struct accept_args_t* args) { 293 | // Extracting the return code, and checking if it represent a failure, 294 | // if it does, we abort as we have nothing to do. 295 | int ret_fd = PT_REGS_RC(ctx); 296 | 297 | if (ret_fd <= 0) { 298 | return; 299 | } 300 | 301 | struct conn_info_t conn_info = {}; 302 | uint32_t pid = id >> 32; 303 | conn_info.conn_id.pid = pid; 304 | conn_info.conn_id.fd = ret_fd; 305 | conn_info.conn_id.tsid = bpf_ktime_get_ns(); 306 | 307 | uint64_t pid_fd = ((uint64_t)pid << 32) | (uint32_t)ret_fd; 308 | // Saving the connection info in a global map, so in the other syscalls 309 | // (read, write and close) we will be able to know that we have seen 310 | // the connection 311 | conn_info_map.update(&pid_fd, &conn_info); 312 | 313 | // Sending an open event to the user mode, to let the user mode know that we 314 | // have identified a new connection. 315 | struct socket_open_event_t open_event = {}; 316 | open_event.timestamp_ns = bpf_ktime_get_ns(); 317 | open_event.conn_id = conn_info.conn_id; 318 | bpf_probe_read(&open_event.addr, sizeof(open_event.addr), args->addr); 319 | 320 | socket_open_events.perf_submit(ctx, &open_event, sizeof(struct socket_open_event_t)); 321 | } 322 | ``` 323 | 324 | #### [hook read和write系统调用](https://www.datadoghq.com/blog/ebpf-guide/#hooking-the-read-and-write-syscalls) 325 | 326 | 在以下代码片段中,我们为`read`系统调用创建了hook。您可以看到我们编写的第一个钩子 (for `accept4`) 和这个新钩子之间的相似之处。以下代码使用类似的帮助程序结构和映射,并定义相同的整体操作序列(hook进入和退出、验证退出代码和处理有效负载)。 327 | 328 | ``` 329 | // Copyright (c) 2018 The Pixie Authors. 330 | // Licensed under the Apache License, Version 2.0 (the "License") 331 | // Original source: https://github.com/pixie-io/pixie/blob/main/src/stirling/source%5C_connectors/socket%5C_tracer/bcc%5C_bpf/socket%5C_trace.c 332 | 333 | // A helper struct to cache input argument of read/write syscalls between the 334 | // entry hook and the exit hook. 335 | struct data_args_t { 336 | int32_t fd; 337 | const char* buf; 338 | }; 339 | 340 | // Helper map to store read syscall arguments between entry and exit hooks. 341 | BPF_HASH(active_read_args_map, uint64_t, struct data_args_t); 342 | 343 | // original signature: ssize_t read(int fd, void *buf, size_t count); 344 | int syscall__probe_entry_read(struct pt_regs* ctx, int fd, char* buf, size_t count) { 345 | uint64_t id = bpf_get_current_pid_tgid(); 346 | 347 | // Stash arguments. 348 | struct data_args_t read_args = {}; 349 | read_args.fd = fd; 350 | read_args.buf = buf; 351 | active_read_args_map.update(&id, &read_args); 352 | 353 | return 0; 354 | } 355 | 356 | int syscall__probe_ret_read(struct pt_regs* ctx) { 357 | uint64_t id = bpf_get_current_pid_tgid(); 358 | 359 | // The return code the syscall is the number of bytes read as well. 360 | ssize_t bytes_count = PT_REGS_RC(ctx); 361 | struct data_args_t* read_args = active_read_args_map.lookup(&id); 362 | if (read_args != NULL) { 363 | // kIngress is an enum value that allows the process_data function 364 | // to know whether the input buffer is incoming or outgoing. 365 | process_data(ctx, id, kIngress, read_args, bytes_count); 366 | } 367 | 368 | active_read_args_map.delete(&id); 369 | return 0; 370 | } 371 | ``` 372 | 373 | 在以下代码片段中,我们创建了辅助函数来处理`read`系统调用。我们的辅助函数通过检查读取的字节数来确定`read`系统调用是否成功完成。然后它检查正在读取的数据是否描述 HTTP。如果是这样,我们将它作为事件发送到用户模式。 374 | 375 | ``` 376 | // Copyright (c) 2018 The Pixie Authors. 377 | // Licensed under the Apache License, Version 2.0 (the "License") 378 | // Original source: https://github.com/pixie-io/pixie/blob/main/src/stirling/source%5C_connectors/socket%5C_tracer/bcc%5C_bpf/socket%5C_trace.c 379 | 380 | // Data buffer message size. BPF can submit at most this amount of data to a perf buffer. 381 | // Kernel size limit is 32KiB. See for more details. 382 | #define MAX_MSG_SIZE 30720 // 30KiB 383 | 384 | struct socket_data_event_t { 385 | // We split attributes into a separate struct, because BPF gets upset if you do lots of 386 | // size arithmetic. This makes it so that its attributes are followed by a message. 387 | struct attr_t { 388 | // The timestamp when syscall completed (return probe was triggered). 389 | uint64_t timestamp_ns; 390 | 391 | // Connection identifier (PID, FD, etc.). 392 | struct conn_id_t conn_id; 393 | 394 | // The type of the actual data that the msg field encodes, which is used by the caller 395 | // to determine how to interpret the data. 396 | enum traffic_direction_t direction; 397 | 398 | // The size of the original message. We use this to truncate msg field to minimize the amount 399 | // of data being transferred. 400 | uint32_t msg_size; 401 | 402 | // A 0-based position number for this event on the connection, in terms of byte position. 403 | // The position is for the first byte of this message. 404 | uint64_t pos; 405 | } attr; 406 | 407 | char msg[MAX_MSG_SIZE]; 408 | }; 409 | 410 | // Perf buffer to send to the user-mode the data events. 411 | BPF_PERF_OUTPUT(socket_data_events); 412 | 413 | ... 414 | 415 | // A helper function that handles read/write syscalls. 416 | static inline __attribute__((__always_inline__)) void process_data(struct pt_regs* ctx, uint64_t id, 417 | enum traffic_direction_t direction, 418 | const struct data_args_t* args, ssize_t bytes_count) { 419 | // Always check access to pointers before accessing them. 420 | if (args->buf == NULL) { 421 | return; 422 | } 423 | 424 | // For read and write syscall, the return code is the number of bytes written or read, so zero means nothing 425 | // was written or read, and negative means that the syscall failed. Anyhow, we have nothing to do with that syscall. 426 | if (bytes_count <= 0) { 427 | return; 428 | } 429 | 430 | uint32_t pid = id >> 32; 431 | uint64_t pid_fd = ((uint64_t)pid << 32) | (uint32_t)args->fd; 432 | struct conn_info_t* conn_info = conn_info_map.lookup(&pid_fd); 433 | if (conn_info == NULL) { 434 | // The FD being read/written does not represent an IPv4 socket FD. 435 | return; 436 | } 437 | 438 | // Check if the connection is already HTTP, or check if that's a new connection, check protocol and return true if that's HTTP. 439 | if (is_http_connection(conn_info, args->buf, bytes_count)) { 440 | // allocate a new event. 441 | uint32_t kZero = 0; 442 | struct socket_data_event_t* event = socket_data_event_buffer_heap.lookup(&kZero); 443 | if (event == NULL) { 444 | return; 445 | } 446 | 447 | // Fill the metadata of the data event. 448 | event->attr.timestamp_ns = bpf_ktime_get_ns(); 449 | event->attr.direction = direction; 450 | event->attr.conn_id = conn_info->conn_id; 451 | 452 | // Another helper function that splits the given buffer to chunks if it is too large. 453 | perf_submit_wrapper(ctx, direction, args->buf, bytes_count, conn_info, event); 454 | } 455 | 456 | // Update the conn_info total written/read bytes. 457 | switch (direction) { 458 | case kEgress: 459 | conn_info->wr_bytes += bytes_count; 460 | break; 461 | case kIngress: 462 | conn_info->rd_bytes += bytes_count; 463 | break; 464 | } 465 | } 466 | ``` 467 | 468 | 接下来我们可以构建系统调用,这与系统调用hoo `write`非常相似。`read`(作为提醒,您可以查看[我们的 repo 中的相关代码](https://github.com/seek-ret/ebpf-training/blob/main/workshop1/capture-traffic/sourcecode.c)。) 469 | 470 | #### [hook close系统调用](https://www.datadoghq.com/blog/ebpf-guide/#hooking-the-close-syscall) 471 | 472 | 此时,我们只需要处理`close`系统调用,就完成了。这里的hook也与其他hook非常相似。有关详细信息,请参阅[我们的存储库](https://github.com/DataDog/ebpf-training/blob/main/workshop1/capture-traffic/sourcecode.c)。 473 | 474 | ### [构建用户模式代理](https://www.datadoghq.com/blog/ebpf-guide/#building-the-user-mode-agent) 475 | 476 | 用户模式代理是使用[gobpf](https://github.com/iovisor/gobpf)库用 Go 语言编写的。该代理从文件中读取内核代码,并在客户端用户模式代理启动期间使用[Clang 工具在运行时编译源代码。](https://clang.llvm.org/) 477 | 478 | 以下部分仅描述其主要方面。有关完整代码,请参阅[本演练的存储库](https://github.com/DataDog/ebpf-training/blob/main/workshop1/capture-traffic/main.go)。 479 | 480 | 第一步是编译代码: 481 | 482 | ``` 483 | bpfModule := bcc.NewModule(string(bpfSourceCodeContent), nil) 484 | defer bpfModule.Close() 485 | ``` 486 | 487 | 然后,我们创建一个连接工厂,负责保存所有连接实例、打印就绪连接以及删除不活动或格式错误的连接。 488 | 489 | ``` 490 | // Create connection factory and set 1m as the inactivity threshold 491 | // Meaning connections that didn't get any event within the last minute are being closed. 492 | connectionFactory := connections.NewFactory(time.Minute) 493 | // A go routine that runs every 10 seconds and prints ready connections 494 | // And deletes inactive or malformed connections. 495 | go func() { 496 | for { 497 | connectionFactory.HandleReadyConnections() 498 | time.Sleep(10 * time.Second) 499 | } 500 | }() 501 | ``` 502 | 503 | 接下来,我们加载 perf 缓冲区处理程序,它从我们的内核hook接收输出并处理它们: 504 | 505 | ``` 506 | if err := bpfwrapper.LaunchPerfBufferConsumers(bpfModule, connectionFactory); err != nil { 507 | log.Panic(err) 508 | } 509 | ``` 510 | 511 | 请注意,每个 perf 缓冲区处理程序都通过通道 ( `inputChan`) 获取事件,并且每个事件都是字节数组 ( `[]byte`) 类型。我们会将每个事件转换为该结构的 Golang 表示。 512 | 513 | ``` 514 | // ConnID is a conversion of the following C-Struct into GO. 515 | // struct conn_id_t { 516 | // uint32_t pid; 517 | // int32_t fd; 518 | // uint64_t tsid; 519 | // };. 520 | type ConnID struct { 521 | PID uint32 522 | FD int32 523 | TsID uint64 524 | } 525 | 526 | ... 527 | ``` 528 | 529 | 接下来我们需要修复事件的时间戳,因为内核模式返回单调时钟而不是实时时钟。然后,我们用新事件更新连接对象字段。 530 | 531 | ``` 532 | func socketCloseEventCallback(inputChan chan []byte, connectionFactory *connections.Factory) { 533 | for data := range inputChan { 534 | if data == nil { 535 | return 536 | } 537 | 538 | var event structs.SocketCloseEvent 539 | if err := binary.Read(bytes.NewReader(data), bpf.GetHostByteOrder(), &event); err != nil { 540 | log.Printf("Failed to decode received data: %+v", err) 541 | continue 542 | } 543 | event.TimestampNano += settings.GetRealTimeOffset() 544 | connectionFactory.GetOrCreate(event.ConnID).AddCloseEvent(event) 545 | } 546 | } 547 | ``` 548 | 549 | 对于最后一部分,我们附上hook。 550 | 551 | ``` 552 | if err := bpfwrapper.AttachKprobes(bpfModule); err != nil { 553 | log.Panic(err) 554 | } 555 | ``` 556 | 557 | ## [测试示踪剂](https://www.datadoghq.com/blog/ebpf-guide/#testing-the-tracer) 558 | 559 | 要测试新的跟踪器,首先向 HTTP 服务器发送客户端 curl 请求: 560 | 561 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126125811381.png?imageView2/0/format/webp/q/75) 562 | 563 | 嗅探器从 curl 请求中捕获以下信息: 564 | 565 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126125822793.png?imageView2/0/format/webp/q/75) 566 | 567 | 如我们所见,我们能够同时捕获 HTTP 请求和响应,尽管内核中没有与 HTTP 相关的函数。我们仅通过使用 eBPF HOOK与 HTTP 通信关联的系统调用,就成功地检索了完整的有效负载——包括请求和响应的主体。 568 | 569 | ## [概括](https://www.datadoghq.com/blog/ebpf-guide/#summary) 570 | 571 | 我们已经完成了为 HTTP 会话流量创建基于 eBPF 的协议跟踪器的过程。如您所见,理解系统调用并为它们实现第一个钩子是最困难的部分。一旦你学会了如何实现你的第一个系统调用钩子,编写其他类似的钩子就会变得容易得多。 572 | 573 | 除了 HTTP 会话监控之外,团队还可以使用 eBPF 监控任何类型的应用程序级流量。实际上,Datadog 使用 eBPF 的这些功能让您可以了解环境的更多方面,而无需您检测任何代码。例如,Datadog 最近利用 eBPF 构建了[Universal Service Monitoring](https://www.datadoghq.com/blog/universal-service-monitoring-datadog/),它仅通过监控应用程序级别的流量,就可以让团队检测到其基础设施中运行的所有服务。2022年8月,Datadog还[宣布收购Seekret](https://www.datadoghq.com/about/latest-news/press-releases/datadog-acquires-seekret-to-make-api-observability-accessible/),其技术利用 eBPF 让组织轻松发现和管理其环境中的 API。Datadog 计划将这些功能整合到其平台中,并使用 eBPF 构建额外的强大功能,以改变团队管理其资源的健康、可用性和安全性的方式。 574 | 575 | 576 | 577 | 有关 eBPF 如何工作以及 Datadog 如何使用它的更多深入信息,您可以观看我们的[Datadog on eBPF 视频](https://www.youtube.com/watch?v=58KtGtpn0_g)。要开始使用 Datadog,您可以注册我们的[14 天免费试用](https://www.datadoghq.com/blog/ebpf-guide/#)。 578 | 579 | ## [参考](https://www.datadoghq.com/blog/ebpf-guide/#references) 580 | 581 | - [pixie: Instant Kubernetes-Native Application Observability](https://github.com/pixie-io/pixie) - Kubernetes 应用程序的开源可观察性工具,下载pixie的源码_GitHub_帮酷 582 | - [eBPF - 介绍、教程和社区资源](https://ebpf.io/) 583 | - [eBPF 简介](https://www.infoq.com/articles/gentle-linux-ebpf-introduction/) 584 | 585 | ## 作者 586 | 587 | 盖·阿比特曼 588 | 589 | ## 原文链接 590 | 591 | https://www.datadoghq.com/blog/ebpf-guide/ -------------------------------------------------------------------------------- /ebpf-guide/eBPF基础/eBPF完全入门指南.md: -------------------------------------------------------------------------------- 1 | # eBPF完全入门指南 2 | 3 | 4 | 5 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126113432460.png?imageView2/0/format/webp/q/75) 6 | 7 | eBPF 源于 **BPF**[1],本质上是处于内核中的一个高效与灵活的虚类虚拟机组件,以一种安全的方式在许多内核 hook 点执行字节码。BPF 最初的目的是用于高效网络报文过滤,经过重新设计,eBPF 不再局限于网络协议栈,已经成为内核顶级的子系统,演进为一个通用执行引擎。开发者可基于 eBPF 开发性能分析工具、软件定义网络、安全等诸多场景。本文将介绍 eBPF 的前世今生,并构建一个 eBPF 环境进行开发实践,文中所有的代码可以在我的 **Github**[2] 中找到。 8 | 9 | ## 技术背景 10 | 11 | ### 发展历史 12 | 13 | BPF,是类 Unix 系统上数据链路层的一种原始接口,提供原始链路层封包的收发。1992 年,Steven McCanne 和 Van Jacobson 写了一篇名为 **The BSD Packet Filter: A New Architecture for User-level Packet Capture**[3] 的论文。在文中,作者描述了他们如何在 Unix 内核实现网络数据包过滤,这种新的技术比当时最先进的数据包过滤技术快 20 倍。 14 | 15 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126114410510.png?imageView2/0/format/webp/q/75) 16 | 17 | BPF 在数据包过滤上引入了两大革新: 18 | 19 | - 一个新的虚拟机 (VM) 设计,可以有效地工作在基于寄存器结构的 CPU 之上 20 | - 应用程序使用缓存只复制与过滤数据包相关的数据,不会复制数据包的所有信息,这样可以最大程度地减少 BPF 处理的数据 21 | 22 | **云原生实验室** 23 | 24 | 战略上藐视云原生,战术上重视云原生 25 | 26 | 141篇原创内容 27 | 28 | 公众号 29 | 30 | 由于这些巨大的改进,所有的 Unix 系统都选择采用 BPF 作为网络数据包过滤技术,直到今天,许多 Unix 内核的派生系统中(包括 Linux 内核)仍使用该实现。tcpdump 的底层采用 BPF 作为底层包过滤技术,我们可以在命令后面增加 `-d` 来查看 tcpdump 过滤条件的底层汇编指令。 31 | 32 | ``` 33 | $ tcpdump -d 'ip and tcp port 8080' 34 | (000) ldh [12] 35 | (001) jeq #0x800 jt 2 jf 12 36 | (002) ldb [23] 37 | (003) jeq #0x6 jt 4 jf 12 38 | (004) ldh [20] 39 | (005) jset #0x1fff jt 12 jf 6 40 | (006) ldxb 4*([14]&0xf) 41 | (007) ldh [x + 14] 42 | (008) jeq #0x1f90 jt 11 jf 9 43 | (009) ldh [x + 16] 44 | (010) jeq #0x1f90 jt 11 jf 12 45 | (011) ret #262144 46 | (012) ret #0 47 | ``` 48 | 49 | 2014 年初,Alexei Starovoitov 实现了 eBPF(extended Berkeley Packet Filter)。经过重新设计,eBPF 演进为一个通用执行引擎,可基于此开发性能分析工具、软件定义网络等诸多场景。**eBPF 最早出现在 3.18 内核中,此后原来的 BPF 就被称为经典 BPF,缩写 cBPF(classic BPF),cBPF 现在已经基本废弃。现在,Linux 内核只运行 eBPF,内核会将加载的 cBPF 字节码透明地转换成 eBPF 再执行**。 50 | 51 | ### eBPF 与 cBPF 52 | 53 | eBPF 新的设计针对现代硬件进行了优化,所以 eBPF 生成的指令集比旧的 BPF 解释器生成的机器码执行得更快。扩展版本也增加了虚拟机中的寄存器数量,将原有的 2 个 32 位寄存器增加到 10 个 64 位寄存器。由于寄存器数量和宽度的增加,开发人员可以使用函数参数自由交换更多的信息,编写更复杂的程序。总之,这些改进使 eBPF 版本的速度比原来的 BPF 提高了 4 倍。 54 | 55 | | 维度 | cBPF | eBPF | 56 | | -------------- | ------------------------- | ------------------------------------------------------------ | 57 | | 内核版本 | Linux 2.1.75(1997 年) | Linux 3.18(2014 年)[4.x for kprobe/uprobe/tracepoint/perf-event] | 58 | | 寄存器数目 | 2 个:A, X | 10 个:R0–R9, 另外 R10 是一个只读的帧指针 - R0 eBPF 中内核函数的返回值和退出值 - R1 - R5 eBF 程序在内核中的参数值 - R6 - R9 内核函数将保存的被调用者 callee 保存的寄存器 - R10 一个只读的堆栈帧指针 | 59 | | 寄存器宽度 | 32 位 | 64 位 | 60 | | 存储 | 16 个内存位: M[0–15] | 512 字节堆栈,无限制大小的 `map` 存储 | 61 | | 限制的内核调用 | 非常有限,仅限于 JIT 特定 | 有限,通过 bpf_call 指令调用 | 62 | | 目标事件 | 数据包、 seccomp-BPF | 数据包、内核函数、用户函数、跟踪点 PMCs 等 | 63 | 64 | 2014 年 6 月,**eBPF 扩展到用户空间,这也成为了 BPF 技术的转折点**。正如 Alexei 在提交补丁的注释中写到:「这个补丁展示了 eBPF 的潜力」。当前,eBPF 不再局限于网络栈,已经成为内核顶级的子系统。 65 | 66 | ### eBPF 与内核模块 67 | 68 | 对比 Web 的发展,eBPF 与内核的关系有点类似于 JavaScript 与浏览器内核的关系,eBPF 相比于直接修改内核和编写内核模块提供了一种新的内核可编程的选项。eBPF 程序架构强调安全性和稳定性,看上去更像内核模块,但与内核模块不同,eBPF 程序不需要重新编译内核,并且可以确保 eBPF 程序运行完成,而不会造成系统的崩溃。 69 | 70 | | 维度 | Linux 内核模块 | eBPF | 71 | | ------------------- | ------------------------------------ | ---------------------------------------------- | 72 | | kprobes/tracepoints | 支持 | 支持 | 73 | | **安全性** | 可能引入安全漏洞或导致内核 Panic | 通过验证器进行检查,可以保障内核安全 | 74 | | 内核函数 | 可以调用内核函数 | 只能通过 BPF Helper 函数调用 | 75 | | 编译性 | 需要编译内核 | 不需要编译内核,引入头文件即可 | 76 | | 运行 | 基于相同内核运行 | 基于稳定 ABI 的 BPF 程序可以编译一次,各处运行 | 77 | | 与应用程序交互 | 打印日志或文件 | 通过 perf_event 或 map 结构 | 78 | | 数据结构丰富性 | 一般 | 丰富 | 79 | | **入门门槛** | 高 | 低 | 80 | | **升级** | 需要卸载和加载,可能导致处理流程中断 | 原子替换升级,不会造成处理流程中断 | 81 | | 内核内置 | 视情况而定 | 内核内置支持 | 82 | 83 | ### eBPF 架构 84 | 85 | eBPF 分为用户空间程序和内核程序两部分: 86 | 87 | - 用户空间程序负责加载 BPF 字节码至内核,如需要也会负责读取内核回传的统计信息或者事件详情 88 | - 内核中的 BPF 字节码负责在内核中执行特定事件,如需要也会将执行的结果通过 maps 或者 perf-event 事件发送至用户空间 89 | - 其中用户空间程序与内核 BPF 字节码程序可以使用 map 结构实现双向通信,这为内核中运行的 BPF 字节码程序提供了更加灵活的控制 90 | 91 | eBPF 整体结构图如下: 92 | 93 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126114345890.png?imageView2/0/format/webp/q/75) 94 | 95 | 用户空间程序与内核中的 BPF 字节码交互的流程主要如下: 96 | 97 | 1. 使用 LLVM 或者 GCC 工具将编写的 BPF 代码程序编译成 BPF 字节码 98 | 2. 使用加载程序 Loader 将字节码加载至内核 99 | 3. 内核使用验证器(Verfier) 组件保证执行字节码的安全性,以避免对内核造成灾难,在确认字节码安全后将其加载对应的内核模块执行 100 | 4. 内核中运行的 BPF 字节码程序可以使用两种方式将数据回传至用户空间 101 | - **maps** 方式可用于将内核中实现的统计摘要信息(比如测量延迟、堆栈信息)等回传至用户空间; 102 | - **perf-event** 用于将内核采集的事件实时发送至用户空间,用户空间程序实时读取分析; 103 | 104 | ### eBPF 限制 105 | 106 | eBPF 技术虽然强大,但是为了保证内核的处理安全和及时响应,内核中的 eBPF 技术也给予了诸多限制,当然随着技术的发展和演进,限制也在逐步放宽或者提供了对应的解决方案。 107 | 108 | - eBPF 程序不能调用任意的内核参数,只限于内核模块中列出的 BPF Helper 函数,函数支持列表也随着内核的演进在不断增加。 109 | - eBPF 程序不允许包含无法到达的指令,防止加载无效代码,延迟程序的终止。 110 | - eBPF 程序中循环次数限制且必须在有限时间内结束,这主要是用来防止在 kprobes 中插入任意的循环,导致锁住整个系统;解决办法包括展开循环,并为需要循环的常见用途添加辅助函数。Linux 5.3 在 BPF 中包含了对有界循环的支持,它有一个可验证的运行时间上限。 111 | - eBPF 堆栈大小被限制在 MAX_BPF_STACK,截止到内核 Linux 5.8 版本,被设置为 512;参见 **include/linux/filter.h**[4],这个限制特别是在栈上存储多个字符串缓冲区时:一个 char[256]缓冲区会消耗这个栈的一半。目前没有计划增加这个限制,解决方法是改用 bpf 映射存储,它实际上是无限的。`*/\* BPF program can access up to 512 bytes of stack space. \*/*#define MAX_BPF_STACK 512` 112 | - eBPF 字节码大小最初被限制为 4096 条指令,截止到内核 Linux 5.8 版本, 当前已将放宽至 100 万指令( BPF_COMPLEXITY_LIMIT_INSNS),参见:**include/linux/bpf.h**[5],对于无权限的 BPF 程序,仍然保留 4096 条限制 ( BPF_MAXINSNS );新版本的 eBPF 也支持了多个 eBPF 程序级联调用,虽然传递信息存在某些限制,但是可以通过组合实现更加强大的功能。`#define BPF_COMPLEXITY_LIMIT_INSNS 1000000 */\* yes. 1M insns \*/*` 113 | 114 | ## eBPF 实战 115 | 116 | 在深入介绍 eBPF 特性之前,让我们 `Get Hands Dirty`,切切实实的感受 eBPF 程序到底是什么,我们该如何开发 eBPF 程序。随着 eBPF 生态的演进,现在已经有越来越多的工具链用于开发 eBPF 程序,在后文也会详细介绍: 117 | 118 | - 基于 bcc 开发:bcc 提供了对 eBPF 开发,前段提供 Python API,后端 eBPF 程序通过 C 实现。特点是简单易用,但是性能较差。 119 | - 基于 libebpf-bootstrap 开发:libebpf-bootstrap 提供了一个方便的脚手架 120 | - 基于内核源码开发:内核源码开发门槛较高,但是也更加切合 eBPF 底层原理,所以这里以这个方法作为示例 121 | 122 | ### 内核源码编译 123 | 124 | 系统环境如下,采用腾讯云 CVM,Ubuntu 20.04,内核版本 5.4.0 125 | 126 | ``` 127 | $ uname -a 128 | Linux VM-1-3-ubuntu 5.4.0-42-generic #46-Ubuntu SMP Fri Jul 10 00:24:02 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux 129 | ``` 130 | 131 | 首先安装必要依赖: 132 | 133 | ``` 134 | $ sudo apt install -y bison build-essential cmake flex git libedit-dev pkg-config libmnl-dev \ 135 | python zlib1g-dev libssl-dev libelf-dev libcap-dev libfl-dev llvm clang pkg-config \ 136 | gcc-multilib luajit libluajit-5.1-dev libncurses5-dev libclang-dev clang-tools 137 | ``` 138 | 139 | 一般情况下推荐采用 apt 方式的安装源码,安装简单而且只安装当前内核的源码,源码的大小在 200M 左右。 140 | 141 | ``` 142 | $ apt-cache search linux-source 143 | $ apt install linux-source-5.4.0 144 | ``` 145 | 146 | 源码安装至 `/usr/src/` 目录下。 147 | 148 | ``` 149 | $ ls -hl 150 | total 4.0K 151 | drwxr-xr-x 4 root root 4.0K Nov 9 13:22 linux-source-5.4.0 152 | lrwxrwxrwx 1 root root 45 Oct 15 10:28 linux-source-5.4.0.tar.bz2 -> linux-source-5.4.0/linux-source-5.4.0.tar.bz2 153 | $ tar -jxvf linux-source-5.4.0.tar.bz2 154 | $ cd linux-source-5.4.0 155 | 156 | $ cp -v /boot/config-$(uname -r) .config # make defconfig 或者 make menuconfig 157 | $ make headers_install 158 | $ make modules_prepare 159 | $ make scripts # 可选 160 | $ make M=samples/bpf # 如果配置出错,可以使用 make oldconfig && make prepare 修复 161 | ``` 162 | 163 | 编译成功后,可以在 `samples/bpf` 目录下看到一系列的目标文件和二进制文件。 164 | 165 | ### Hello World 166 | 167 | 前面说到 eBPF 通常由内核空间程序和用户空间程序两部分组成,现在 `samples/bpf` 目录下有很多这种程序,内核空间程序以 `_kern.c` 结尾,用户空间程序以 `_user.c` 结尾。先不看这些复杂的程序,我们手动写一个 eBPF 程序的 Hello World。 168 | 169 | 内核中的程序 `hello_kern.c`: 170 | 171 | ``` 172 | #include 173 | #include "bpf_helpers.h" 174 | 175 | #define SEC(NAME) __attribute__((section(NAME), used)) 176 | 177 | SEC("tracepoint/syscalls/sys_enter_execve") 178 | int bpf_prog(void *ctx) 179 | { 180 | char msg[] = "Hello BPF from houmin!\n"; 181 | bpf_trace_printk(msg, sizeof(msg)); 182 | return 0; 183 | } 184 | 185 | char _license[] SEC("license") = "GPL"; 186 | ``` 187 | 188 | #### 函数入口 189 | 190 | 上述代码和普通的 C 语言编程有一些区别。 191 | 192 | 1. 程序的入口通过编译器的 `pragama __section("tracepoint/syscalls/sys_enter_execve")` 指定的。 193 | 2. 入口的参数不再是 `argc, argv`, 它根据不同的 prog type 而有所差别。我们的例子中,prog type 是 `BPF_PROG_TYPE_TRACEPOINT`, 它的入口参数就是 `void *ctx`。 194 | 195 | #### 头文件 196 | 197 | ``` 198 | **#include ** 199 | ``` 200 | 201 | 这个头文件的来源是 kernel source header file 。它安装在 `/usr/include/linux/bpf.h`中。 202 | 203 | 它提供了 bpf 编程需要的很多 symbol。例如 204 | 205 | 1. enum bpf_func_id 定义了所有的 kerne helper function 的 id 206 | 2. enum bpf_prog_type 定义了内核支持的所有的 prog 的类型。 207 | 3. struct __sk_buff 是 bpf 代码中访问内核 struct sk_buff 的接口。 208 | 209 | 等等 210 | 211 | **#include “bpf_helpers.h”** 212 | 213 | 来自 libbpf ,需要自行安装。我们引用这个头文件是因为调用了 bpf_printk()。这是一个 kernel helper function。 214 | 215 | #### 程序解释 216 | 217 | 这里我们简单解读下内核态的 `ebpf` 程序,非常简单: 218 | 219 | - `bpf_trace_printk` 是一个 eBPF helper 函数,用于打印信息到 `trace_pipe` (/sys/kernel/debug/tracing/trace_pipe),**详见这里**[6] 220 | - 代码声明了 `SEC` 宏,并且定义了 GPL 的 License,这是因为加载进内核的 eBPF 程序需要有 License 检查,类似于内核模块 221 | 222 | #### 加载 BPF 代码 223 | 224 | 用户态程序 `hello_user.c` 225 | 226 | ``` 227 | #include 228 | #include "bpf_load.h" 229 | 230 | int main(int argc, char **argv) 231 | { 232 | if(load_bpf_file("hello_kern.o") != 0) 233 | { 234 | printf("The kernel didn't load BPF program\n"); 235 | return -1; 236 | } 237 | 238 | read_trace_pipe(); 239 | return 0; 240 | } 241 | ``` 242 | 243 | 在用户态 `ebpf` 程序中,解读如下: 244 | 245 | - 通过 `load_bpf_file` 将编译出的内核态 ebpf 目标文件加载到内核 246 | - 通过 **`read_trace_pipe`**[7] 从 `trace_pipe` 读取 trace 信息,打印到控制台中 247 | 248 | 修改 `samples/bpf` 目录下的 `Makefile` 文件,在对应的位置添加以下三行: 249 | 250 | ``` 251 | hostprogs-y += hello 252 | hello-objs := bpf_load.o hello_user.o 253 | always += hello_kern.o 254 | ``` 255 | 256 | 重新编译,可以看到编译成功的文件 257 | 258 | ``` 259 | $ make M=samples/bpf 260 | $ ls -hl samples/bpf/hello* 261 | -rwxrwxr-x 1 ubuntu ubuntu 404K Mar 30 17:48 samples/bpf/hello 262 | -rw-rw-r-- 1 ubuntu ubuntu 317 Mar 30 17:47 samples/bpf/hello_kern.c 263 | -rw-rw-r-- 1 ubuntu ubuntu 3.8K Mar 30 17:48 samples/bpf/hello_kern.o 264 | -rw-rw-r-- 1 ubuntu ubuntu 246 Mar 30 17:47 samples/bpf/hello_user.c 265 | -rw-rw-r-- 1 ubuntu ubuntu 2.2K Mar 30 17:48 samples/bpf/hello_user.o 266 | ``` 267 | 268 | 进入到对应的目录运行 `hello` 程序,可以看到输出结果如下: 269 | 270 | ``` 271 | $ sudo ./hello 272 | <...>-102735 [001] .... 6733.481740: 0: Hello BPF from houmin! 273 | 274 | <...>-102736 [000] .... 6733.482884: 0: Hello BPF from houmin! 275 | 276 | <...>-102737 [002] .... 6733.483074: 0: Hello BPF from houmin! 277 | ``` 278 | 279 | ### 代码解读 280 | 281 | 前面提到 `load_bpf_file` 函数将 LLVM 编译出来的 eBPF 字节码加载进内核,这到底是如何实现的呢? 282 | 283 | - 经过搜查,可以看到 `load_bpf_file` 也是在 `samples/bpf` 目录下实现的,具体的参见 **`bpf_load.c`**[8] 284 | - 阅读 `load_bpf_file` 代码可以看到,它主要是解析 ELF 格式的 eBPF 字节码,然后调用 **`load_and_attach`**[9] 函数 285 | - 在 `load_and_attach` 函数中,我们可以看到其调用了 `bpf_load_program` 函数,这是 libbpf 提供的函数。 286 | - 调用的 `bpf_load_program` 中的 `license`、`kern_version` 等参数来自于解析 eBPF ELF 文件,prog_type 来自于 bpf 代码里面 SEC 字段指定的类型。 287 | 288 | ``` 289 | static int load_and_attach(const char *event, struct bpf_insn *prog, int size) 290 | { 291 | bool is_socket = strncmp(event, "socket", 6) == 0; 292 | bool is_kprobe = strncmp(event, "kprobe/", 7) == 0; 293 | bool is_kretprobe = strncmp(event, "kretprobe/", 10) == 0; 294 | bool is_tracepoint = strncmp(event, "tracepoint/", 11) == 0; 295 | bool is_raw_tracepoint = strncmp(event, "raw_tracepoint/", 15) == 0; 296 | bool is_xdp = strncmp(event, "xdp", 3) == 0; 297 | bool is_perf_event = strncmp(event, "perf_event", 10) == 0; 298 | bool is_cgroup_skb = strncmp(event, "cgroup/skb", 10) == 0; 299 | bool is_cgroup_sk = strncmp(event, "cgroup/sock", 11) == 0; 300 | bool is_sockops = strncmp(event, "sockops", 7) == 0; 301 | bool is_sk_skb = strncmp(event, "sk_skb", 6) == 0; 302 | bool is_sk_msg = strncmp(event, "sk_msg", 6) == 0; 303 | 304 | //... 305 | 306 | fd = bpf_load_program(prog_type, prog, insns_cnt, license, kern_version, 307 | bpf_log_buf, BPF_LOG_BUF_SIZE); 308 | if (fd < 0) { 309 | printf("bpf_load_program() err=%d\n%s", errno, bpf_log_buf); 310 | return -1; 311 | } 312 | //... 313 | } 314 | ``` 315 | 316 | ## eBPF 特性 317 | 318 | ### Hook Overview 319 | 320 | eBPF 程序都是事件驱动的,它们会在内核或者应用程序经过某个确定的 Hook 点的时候运行,这些 Hook 点都是提前定义的,包括系统调用、函数进入/退出、内核 `tracepoints`、网络事件等。 321 | 322 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126114319155.png?imageView2/0/format/webp/q/75) 323 | 324 | 如果针对某个特定需求的 Hook 点不存在,可以通过 `kprobe` 或者 `uprobe` 来在内核或者用户程序的几乎所有地方挂载 eBPF 程序。 325 | 326 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126114253887.png?imageView2/0/format/webp/q/75) 327 | 328 | ### Verification 329 | 330 | > With great power there must also come great responsibility. 331 | 332 | 每一个 eBPF 程序加载到内核都要经过 `Verification`,用来保证 eBPF 程序的安全性,主要包括: 333 | 334 | - 要保证 加载 eBPF 程序的进程有必要的特权级,除非节点开启了 335 | 336 | ``` 337 | unpriviledged 338 | ``` 339 | 340 | 特性,只有特权级的程序才能够加载 eBPF 程序 341 | 342 | - 内核提供了一个配置项 `/proc/sys/kernel/unprivileged_bpf_disabled` 来禁止非特权用户使用 `bpf(2)` 系统调用,可以通过 `sysctl` 命令修改 343 | - 比较特殊的一点是,这个配置项特意设计为**一次性开关**(one-time kill switch), 这意味着一旦将它设为 `1`,就没有办法再改为 `0` 了,除非重启内核 344 | - 一旦设置为 `1` 之后,只有初始命名空间中有 `CAP_SYS_ADMIN` 特权的进程才可以调用 `bpf(2)` 系统调用 。Cilium 启动后也会将这个配置项设为 1: 345 | 346 | ``` 347 | $ echo 1 > /proc/sys/kernel/unprivileged_bpf_disabled 348 | ``` 349 | 350 | - 要保证 eBPF 程序不会崩溃或者使得系统出故障 351 | - 要保证 eBPF 程序不能陷入死循环,能够 `runs to completion` 352 | - 要保证 eBPF 程序必须满足系统要求的大小,过大的 eBPF 程序不允许被加载进内核 353 | - 要保证 eBPF 程序的复杂度有限,`Verifier` 将会评估 eBPF 程序所有可能的执行路径,必须能够在有限时间内完成 eBPF 程序复杂度分析 354 | 355 | ### JIT Compilation 356 | 357 | `Just-In-Time(JIT)` 编译用来将通用的 eBPF 字节码翻译成与机器相关的指令集,从而极大加速 BPF 程序的执行: 358 | 359 | - 与解释器相比,它们可以降低每个指令的开销。通常,指令可以 1:1 映射到底层架构的原生指令 360 | - 这也会减少生成的可执行镜像的大小,因此对 CPU 的指令缓存更友好 361 | - 特别地,对于 CISC 指令集(例如 `x86`),JIT 做了很多特殊优化,目的是为给定的指令产生可能的最短操作码,以降低程序翻译过程所需的空间 362 | 363 | 64 位的 `x86_64`、`arm64`、`ppc64`、`s390x`、`mips64`、`sparc64` 和 32 位的 `arm` 、`x86_32` 架构都内置了 in-kernel eBPF JIT 编译器,它们的功能都是一样的,可以用如下方式打开: 364 | 365 | ``` 366 | $ echo 1 > /proc/sys/net/core/bpf_jit_enable 367 | ``` 368 | 369 | 32 位的 `mips`、`ppc` 和 `sparc` 架构目前内置的是一个 cBPF JIT 编译器。这些只有 cBPF JIT 编译器的架构,以及那些甚至完全没有 BPF JIT 编译器的架构,需要通过**内核中的解释器**(in-kernel interpreter)执行 eBPF 程序。 370 | 371 | 要判断哪些平台支持 eBPF JIT,可以在内核源文件中 grep `HAVE_EBPF_JIT`: 372 | 373 | ``` 374 | $ git grep HAVE_EBPF_JIT arch/ 375 | arch/arm/Kconfig: select HAVE_EBPF_JIT if !CPU_ENDIAN_BE32 376 | arch/arm64/Kconfig: select HAVE_EBPF_JIT 377 | arch/powerpc/Kconfig: select HAVE_EBPF_JIT if PPC64 378 | arch/mips/Kconfig: select HAVE_EBPF_JIT if (64BIT && !CPU_MICROMIPS) 379 | arch/s390/Kconfig: select HAVE_EBPF_JIT if PACK_STACK && HAVE_MARCH_Z196_FEATURES 380 | arch/sparc/Kconfig: select HAVE_EBPF_JIT if SPARC64 381 | arch/x86/Kconfig: select HAVE_EBPF_JIT if X86_64 382 | ``` 383 | 384 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126114242884.png?imageView2/0/format/webp/q/75) 385 | 386 | ### Maps 387 | 388 | BPF Map 是**驻留在内核空间**中的高效 `Key/Value store`,包含多种类型的 Map,由内核实现其功能,具体实现可以参考 **我的这篇博文**[10]。 389 | 390 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126114234449.png?imageView2/0/format/webp/q/75) 391 | 392 | BPF Map 的交互场景有以下几种: 393 | 394 | - BPF 程序和用户态程序的交互:BPF 程序运行完,得到的结果存储到 map 中,供用户态程序通过文件描述符访问 395 | - BPF 程序和内核态程序的交互:和 BPF 程序以外的内核程序交互,也可以使用 map 作为中介 396 | - BPF 程序间交互:如果 BPF 程序内部需要用全局变量来交互,但是由于安全原因 BPF 程序不允许访问全局变量,可以使用 map 来充当全局变量 397 | - BPF Tail call:Tail call 是一个 BPF 程序跳转到另一 BPF 程序,BPF 程序首先通过 `BPF_MAP_TYPE_PROG_ARRAY` 类型的 map 来知道另一个 BPF 程序的指针,然后调用 `tail_call()` 的 helper function 来执行 Tail call 398 | 399 | 共享 map 的 BPF 程序不要求是相同的程序类型,例如 tracing 程序可以和网络程序共享 map,**单个 BPF 程序目前最多可直接访问 64 个不同 map**。 400 | 401 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126114224530.png?imageView2/0/format/webp/q/75) 402 | 403 | 当前可用的 **通用 map** 有: 404 | 405 | - `BPF_MAP_TYPE_HASH` 406 | - `BPF_MAP_TYPE_ARRAY` 407 | - `BPF_MAP_TYPE_PERCPU_HASH` 408 | - `BPF_MAP_TYPE_PERCPU_ARRAY` 409 | - `BPF_MAP_TYPE_LRU_HASH` 410 | - `BPF_MAP_TYPE_LRU_PERCPU_HASH` 411 | - `BPF_MAP_TYPE_LPM_TRIE` 412 | 413 | 以上 map 都使用相同的一组 BPF 辅助函数来执行查找、更新或删除操作,但各自实现了不同的后端,这些后端各有不同的语义和性能特点。随着多 CPU 架构的成熟发展,BPF Map 也引入了 **per-cpu** 类型,如`BPF_MAP_TYPE_PERCPU_HASH`、`BPF_MAP_TYPE_PERCPU_ARRAY`等,当你使用这种类型的 BPF Map 时,每个 CPU 都会存储并看到它自己的 Map 数据,从属于不同 CPU 之间的数据是互相隔离的,这样做的好处是,在进行查找和聚合操作时更加高效,性能更好,尤其是你的 BPF 程序主要是在做收集时间序列型数据,如流量数据或指标等。 414 | 415 | 当前内核中的 **非通用 map** 有: 416 | 417 | - `BPF_MAP_TYPE_PROG_ARRAY`:一个数组 map,用于 hold 其他的 BPF 程序 418 | - `BPF_MAP_TYPE_PERF_EVENT_ARRAY` 419 | - `BPF_MAP_TYPE_CGROUP_ARRAY`:用于检查 skb 中的 cgroup2 成员信息 420 | - `BPF_MAP_TYPE_STACK_TRACE`:用于存储栈跟踪的 MAP 421 | - `BPF_MAP_TYPE_ARRAY_OF_MAPS`:持有(hold) 其他 map 的指针,这样整个 map 就可以在运行时实现原子替换 422 | - `BPF_MAP_TYPE_HASH_OF_MAPS`:持有(hold) 其他 map 的指针,这样整个 map 就可以在运行时实现原子替换 423 | 424 | ### Helper Calls 425 | 426 | eBPF 程序不能够随意调用内核函数,如果这么做的话会导致 eBPF 程序与特定的内核版本绑定,相反它内核定义的一系列 `Helper functions`。`Helper functions` 使得 BPF 能够通过一组内核定义的稳定的函数调用来从内核中查询数据,或者将数据推送到内核。**所有的 BPF 辅助函数都是核心内核的一部分,无法通过内核模块来扩展或添加**。**当前可用的 BPF 辅助函数已经有几十个,并且数量还在不断增加**,你可以在 **Linux Manual Page: bpf-helpers**[11] 看到当前 Linux 支持的 `Helper functions`。 427 | 428 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126114216333.png?imageView2/0/format/webp/q/75) 429 | 430 | **不同类型的 BPF 程序能够使用的辅助函数可能是不同的**,例如: 431 | 432 | - 与 attach 到 tc 层的 BPF 程序相比,attach 到 socket 的 BPF 程序只能够调用前者可以调用的辅助函数的一个子集 433 | - `lightweight tunneling` 使用的封装和解封装辅助函数,只能被更低的 tc 层使用;而推送通知到用户态所使用的事件输出辅助函数,既可以被 tc 程序使用也可以被 XDP 程序使用 434 | 435 | **所有的辅助函数都共享同一个通用的、和系统调用类似的函数方法**,其定义如下: 436 | 437 | ``` 438 | u64 fn(u64 r1, u64 r2, u64 r3, u64 r4, u64 r5) 439 | ``` 440 | 441 | 内核将辅助函数抽象成 `BPF_CALL_0()` 到 `BPF_CALL_5()` 几个宏,形式和相应类型的系统调用类似,这里宏的定义可以参见 **include/linux/filter.h**[12] 。以 **`bpf_map_update_elem`**[13] 为例,可以看到它通过调用相应 map 的回调函数完成更新 map 元素的操作: 442 | 443 | ``` 444 | /* /kernel/bpf/helpers.c */ 445 | BPF_CALL_4(bpf_map_update_elem, struct bpf_map *, map, void *, key, 446 | void *, value, u64, flags) 447 | { 448 | WARN_ON_ONCE(!rcu_read_lock_held()); 449 | return map->ops->map_update_elem(map, key, value, flags); 450 | } 451 | 452 | const struct bpf_func_proto bpf_map_update_elem_proto = { 453 | .func = bpf_map_update_elem, 454 | .gpl_only = false, 455 | .ret_type = RET_INTEGER, 456 | .arg1_type = ARG_CONST_MAP_PTR, 457 | .arg2_type = ARG_PTR_TO_MAP_KEY, 458 | .arg3_type = ARG_PTR_TO_MAP_VALUE, 459 | .arg4_type = ARG_ANYTHING, 460 | }; 461 | ``` 462 | 463 | 这种方式有很多优点: 464 | 465 | > 虽然 cBPF 允许其加载指令(load instructions)进行超出范围的访问(overload),以便从一个看似不可能的包偏移量(packet offset)获取数据以唤醒多功能辅助函数,但每个 cBPF JIT 仍然需要为这个 cBPF 扩展实现对应的支持。而在 eBPF 中,JIT 编译器会以一种透明和高效的方式编译新加入的辅助函数,这意味着 JIT 编 译器只需要发射(emit)一条调用指令(call instruction),因为寄存器映射的方式使得 BPF 排列参数的方式(assignments)已经和底层架构的调用约定相匹配了。这使得基于辅助函数扩展核心内核(core kernel)非常方便。**所有的 BPF 辅助函数都是核心内核的一部分,无法通过内核模块(kernel module)来扩展或添加**。 466 | > 467 | > 前面提到的函数签名还允许校验器执行类型检测(type check)。上面的 `struct bpf_func_proto` 用于存放**校验器必需知道的所有关于该辅助函数的信息**,这 样校验器可以确保辅助函数期望的类型和 BPF 程序寄存器中的当前内容是匹配的。 468 | > 469 | > 参数类型范围很广,从任意类型的值,到限制只能为特定类型,例如 BPF 栈缓冲区(stack buffer)的 `pointer/size` 参数对,辅助函数可以从这个位置读取数据或向其写入数据。对于这种情况,校验器还可以执行额外的检查,例如,缓冲区是否已经初始化过了。 470 | 471 | ### Tail Calls 472 | 473 | 尾调用的机制是指:一个 BPF 程序可以调用另一个 BPF 程序,并且调用完成后不用返回到原来的程序。 474 | 475 | - 和普通函数调用相比,这种调用方式开销最小,因为它是**用长跳转(long jump)实现的,复用了原来的栈帧** (stack frame) 476 | - BPF 程序都是独立验证的,因此要传递状态,要么使用 per-CPU map 作为 scratch 缓冲区 ,要么如果是 tc 程序的话,还可以使用 `skb` 的某些字段(例如 `cb[]`) 477 | - **相同类型的程序才可以尾调用**,而且它们还要与 JIT 编译器相匹配,因此要么是 JIT 编译执行,要么是解释器执行(invoke interpreted programs),但不能同时使用两种方式 478 | 479 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126114141523.png?imageView2/0/format/webp/q/75) 480 | 481 | ### BPF to BPF Calls 482 | 483 | 除了 BPF 辅助函数和 BPF 尾调用之外,BPF 核心基础设施最近刚加入了一个新特性:`BPF to BPF calls`。**在这个特性引入内核之前,典型的 BPF C 程序必须 将所有需要复用的代码进行特殊处理,例如,在头文件中声明为 `always_inline`**。当 LLVM 编译和生成 BPF 对象文件时,所有这些函数将被内联,因此会在生成的对象文件中重 复多次,导致代码尺寸膨胀: 484 | 485 | ``` 486 | #include 487 | 488 | #ifndef __section 489 | # define __section(NAME) \ 490 | __attribute__((section(NAME), used)) 491 | #endif 492 | 493 | #ifndef __inline 494 | # define __inline \ 495 | inline __attribute__((always_inline)) 496 | #endif 497 | 498 | static __inline int foo(void) 499 | { 500 | return XDP_DROP; 501 | } 502 | 503 | __section("prog") 504 | int xdp_drop(struct xdp_md *ctx) 505 | { 506 | return foo(); 507 | } 508 | 509 | char __license[] __section("license") = "GPL"; 510 | ``` 511 | 512 | 之所以要这样做是因为 **BPF 程序的加载器、校验器、解释器和 JIT 中都缺少对函数调用的支持**。从 `Linux 4.16` 和 `LLVM 6.0` 开始,这个限制得到了解决,BPF 程序不再需要到处使用 `always_inline` 声明了。因此,上面的代码可以更自然地重写为: 513 | 514 | ``` 515 | #include 516 | 517 | #ifndef __section 518 | # define __section(NAME) \ 519 | __attribute__((section(NAME), used)) 520 | #endif 521 | 522 | static int foo(void) 523 | { 524 | return XDP_DROP; 525 | } 526 | 527 | __section("prog") 528 | int xdp_drop(struct xdp_md *ctx) 529 | { 530 | return foo(); 531 | } 532 | 533 | char __license[] __section("license") = "GPL"; 534 | ``` 535 | 536 | BPF 到 BPF 调用是一个重要的性能优化,极大减小了生成的 BPF 代码大小,因此 **对 CPU 指令缓存(instruction cache,i-cache)更友好**。 537 | 538 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126114130231.png?imageView2/0/format/webp/q/75) 539 | 540 | BPF 辅助函数的调用约定也适用于 BPF 函数间调用: 541 | 542 | - `r1` - `r5` 用于传递参数,返回结果放到 `r0` 543 | - `r1` - `r5` 是 scratch registers,`r6` - `r9` 像往常一样是保留寄存器 544 | - 最大嵌套调用深度是 `8` 545 | - 调用方可以传递指针(例如,指向调用方的栈帧的指针) 给被调用方,但反过来不行 546 | 547 | **当前,BPF 函数间调用和 BPF 尾调用是不兼容的**,因为后者需要复用当前的栈设置( stack setup),而前者会增加一个额外的栈帧,因此不符合尾调用期望的布局。 548 | 549 | BPF JIT 编译器为每个函数体发射独立的镜像(emit separate images for each function body),稍后在最后一通 JIT 处理(final JIT pass)中再修改镜像中函数调用的地址 。已经证明,这种方式需要对各种 JIT 做最少的修改,因为在实现中它们可以将 BPF 函数间调用当做常规的 BPF 辅助函数调用。 550 | 551 | ### Object Pinning 552 | 553 | **BPF map 和程序作为内核资源只能通过文件描述符访问,其背后是内核中的匿名 inode。**这带来了很多优点: 554 | 555 | - 用户空间应用程序能够使用大部分文件描述符相关的 API 556 | - 传递给 Unix socket 的文件描述符是透明工作等等 557 | 558 | 但同时,**文件描述符受限于进程的生命周期,使得 map 共享之类的操作非常笨重**,这给某些特定的场景带来了很多复杂性。 559 | 560 | > 例如 iproute2,其中的 tc 或 XDP 在准备环境、加载程序到内核之后最终会退出。在这种情况下,从用户空间也无法访问这些 map 了,而本来这些 map 其实是很有用的。例如,在 data path 的 ingress 和 egress 位置共享的 map(可以统计包数、字节数、PPS 等信息)。另外,第三方应用可能希望在 BPF 程序运行时监控或更新 map。 561 | 562 | **为了解决这个问题,内核实现了一个最小内核空间 BPF 文件系统,BPF map 和 BPF 程序 都可以 pin 到这个文件系统内**,这个过程称为 `object pinning`。BPF 相关的文件系统**不是单例模式**(singleton),它支持多挂载实例、硬链接、软连接等等。 563 | 564 | 相应的,BPF 系统调用扩展了两个新命令,如下图所示: 565 | 566 | - `BPF_OBJ_PIN`:钉住一个对象 567 | - `BPF_OBJ_GET`:获取一个被钉住的对象 568 | 569 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126114002844.png?imageView2/0/format/webp/q/75) 570 | 571 | ### Hardening 572 | 573 | #### Protection Execution Protection 574 | 575 | 为了避免代码被损坏,BPF 会在程序的生命周期内,在内核中将 BPF 解释器**解释后的整个镜像**(`struct bpf_prog`)和 **JIT 编译之后的镜像**(`struct bpf_binary_header`)锁定为只读的。在这些位置发生的任何数据损坏(例如由于某些内核 bug 导致的)会触发通用的保护机制,因此会造成内核崩溃而不是允许损坏静默地发生。 576 | 577 | 查看哪些平台支持将镜像内存(image memory)设置为只读的,可以通过下面的搜索: 578 | 579 | ``` 580 | $ git grep ARCH_HAS_SET_MEMORY | grep select 581 | arch/arm/Kconfig: select ARCH_HAS_SET_MEMORY 582 | arch/arm64/Kconfig: select ARCH_HAS_SET_MEMORY 583 | arch/s390/Kconfig: select ARCH_HAS_SET_MEMORY 584 | arch/x86/Kconfig: select ARCH_HAS_SET_MEMORY 585 | ``` 586 | 587 | `CONFIG_ARCH_HAS_SET_MEMORY` 选项是不可配置的,因此平台要么内置支持,要么不支持,那些目前还不支持的架构未来可能也会支持。 588 | 589 | #### Mitigation Against Spectre 590 | 591 | 为了防御 ![👉](https://s.w.org/images/core/emoji/14.0.0/svg/1f449.svg)Spectre v2 攻击,Linux 内核提供了 `CONFIG_BPF_JIT_ALWAYS_ON` 选项,打开这个开关后 BPF 解释器将会从内核中完全移除,永远启用 JIT 编译器: 592 | 593 | - 如果应用在一个基于虚拟机的环境,客户机内核将不会复用内核的 BPF 解释器,因此可以避免某些相关的攻击 594 | - 如果是基于容器的环境,这个配置是可选的,如果 JIT 功能打开了,解释器仍然可能会在编译时被去掉,以降低内核的复杂度 595 | - 对于主流架构(例如 `x86_64` 和 `arm64`)上的 JIT 通常都建议打开这个开关 596 | 597 | 将 `/proc/sys/net/core/bpf_jit_harden` 设置为 `1` 会为非特权用户的 JIT 编译做一些额外的加固工作。这些额外加固会稍微降低程序的性能,但在有非受信用户在系统上进行操作的情况下,能够有效地减小潜在的受攻击面。但与完全切换到解释器相比,这些性能损失还是比较小的。对于 `x86_64` JIT 编译器,如果设置了 `CONFIG_RETPOLINE`,尾调用的间接跳转( indirect jump)就会用 `retpoline` 实现。写作本文时,在大部分现代 Linux 发行版上这个配置都是打开的。 598 | 599 | #### Constant Blinding 600 | 601 | 当前,启用加固会在 JIT 编译时**盲化**(blind)BPF 程序中用户提供的所有 32 位和 64 位常量,以防御 **JIT spraying 攻击**,这些攻击会将原生操作码作为立即数注入到内核。这种攻击有效是因为:**立即数驻留在可执行内核内存(executable kernel memory)中**,因此某些内核 bug 可能会触发一个跳转动作,如果跳转到立即数的开始位置,就会把它们当做原生指令开始执行。 602 | 603 | 盲化 JIT 常量通过对真实指令进行随机化(randomizing the actual instruction)实现 。在这种方式中,通过对指令进行重写,将原来**基于立即数的操作**转换成**基于寄存器的操作**。指令重写将加载值的过程分解为两部分: 604 | 605 | 1. 加载一个盲化后的(blinded)立即数 `rnd ^ imm` 到寄存器 606 | 2. 将寄存器和 `rnd` 进行异或操作(xor) 607 | 608 | 这样原始的 `imm` 立即数就驻留在寄存器中,可以用于真实的操作了。这里介绍的只是加载操作的盲化过程,实际上所有的通用操作都被盲化了。下面是加固关闭的情况下,某个程序的 JIT 编译结果: 609 | 610 | ``` 611 | $ echo 0 > /proc/sys/net/core/bpf_jit_harden 612 | 613 | ffffffffa034f5e9 + : 614 | [...] 615 | 39: mov $0xa8909090,%eax 616 | 3e: mov $0xa8909090,%eax 617 | 43: mov $0xa8ff3148,%eax 618 | 48: mov $0xa89081b4,%eax 619 | 4d: mov $0xa8900bb0,%eax 620 | 52: mov $0xa810e0c1,%eax 621 | 57: mov $0xa8908eb4,%eax 622 | 5c: mov $0xa89020b0,%eax 623 | [...] 624 | ``` 625 | 626 | 加固打开之后,以上程序被某个非特权用户通过 BPF 加载的结果(这里已经进行了常量盲化): 627 | 628 | ``` 629 | $ echo 1 > /proc/sys/net/core/bpf_jit_harden 630 | 631 | ffffffffa034f1e5 + : 632 | [...] 633 | 39: mov $0xe1192563,%r10d 634 | 3f: xor $0x4989b5f3,%r10d 635 | 46: mov %r10d,%eax 636 | 49: mov $0xb8296d93,%r10d 637 | 4f: xor $0x10b9fd03,%r10d 638 | 56: mov %r10d,%eax 639 | 59: mov $0x8c381146,%r10d 640 | 5f: xor $0x24c7200e,%r10d 641 | 66: mov %r10d,%eax 642 | 69: mov $0xeb2a830e,%r10d 643 | 6f: xor $0x43ba02ba,%r10d 644 | 76: mov %r10d,%eax 645 | 79: mov $0xd9730af,%r10d 646 | 7f: xor $0xa5073b1f,%r10d 647 | 86: mov %r10d,%eax 648 | 89: mov $0x9a45662b,%r10d 649 | 8f: xor $0x325586ea,%r10d 650 | 96: mov %r10d,%eax 651 | [...] 652 | ``` 653 | 654 | 两个程序在语义上是一样的,但在第二种方式中,原来的立即数在反汇编之后的程序中不再可见。同时,加固还会禁止任何 JIT 内核符合(kallsyms)暴露给特权用户,JIT 镜像地址不再出现在 `/proc/kallsyms` 中。 655 | 656 | ### Offloads 657 | 658 | BPF 网络程序,尤其是 tc 和 XDP BPF 程序在内核中都有一个 offload 到硬件的接口,这样就可以直接在网卡上执行 BPF 程序。 659 | 660 | 当前,Netronome 公司的 `nfp` 驱动支持通过 JIT 编译器 offload BPF,它会将 BPF 指令翻译成网卡实现的指令集。另外,它还支持将 BPF maps offload 到网卡,因此 offloaded BPF 程序可以执行 map 查找、更新和删除操作。 661 | 662 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126113940930.png?imageView2/0/format/webp/q/75) 663 | 664 | ## eBPF 接口 665 | 666 | ### BPF 系统调用 667 | 668 | eBPF 提供了 **`bpf()`**[14] 系统调用来对 BPF Map 或 程序进行操作,其函数原型如下: 669 | 670 | ``` 671 | #include 672 | int bpf(int cmd, union bpf_attr *attr, unsigned int size); 673 | ``` 674 | 675 | 函数有三个参数,其中: 676 | 677 | - `cmd` 指定了 bpf 系统调用执行的命令类型,每个 cmd 都会附带一个参数 `attr` 678 | - `bpf_attr union` 允许在内核和用户空间之间传递数据,确切的格式取决于 `cmd` 这个参数 679 | - `size` 这个参数表示`bpf_attr union` 这个对象以字节为单位的大小 680 | 681 | `cmd` 可以为一下几种类型,基本上可以分为操作 eBPF Map 和操作 eBPF 程序两种类型: 682 | 683 | - `BPF_MAP_CREATE`:创建一个 `eBPF Map` 并且返回指向该 Map 的文件描述符 684 | - `BPF_MAP_LOOKUP_ELEM`:在某个 Map 中根据 key 查找元素并返回其 value 685 | - `BPF_MAP_UPDATE_ELEM`:在某个 Map 中创建或者更新一个元素 key/value 对 686 | - `BPF_MAP_DELETE_ELEM`:在某个 Map 中根据 key 删除一个元素 687 | - `BPF_MAP_GET_NEXT_KEY`:在某个 Map 中根据 key 查找元素然后返回下一个元素的 key 688 | - `BPF_PROG_LOAD`:校验并加载 eBPF 程序,返回与该程序关联的文件描述符 689 | - … 690 | 691 | `bpf_attr union` 的结构如下所示,根据不同的 `cmd` 可以填充不同的信息。 692 | 693 | ``` 694 | union bpf_attr { 695 | struct { /* Used by BPF_MAP_CREATE */ 696 | __u32 map_type; 697 | __u32 key_size; /* size of key in bytes */ 698 | __u32 value_size; /* size of value in bytes */ 699 | __u32 max_entries; /* maximum number of entries in a map */ 700 | }; 701 | 702 | struct { /* Used by BPF_MAP_*_ELEM and BPF_MAP_GET_NEXT_KEY commands */ 703 | __u32 map_fd; 704 | __aligned_u64 key; 705 | union { 706 | __aligned_u64 value; 707 | __aligned_u64 next_key; 708 | }; 709 | __u64 flags; 710 | }; 711 | 712 | struct { /* Used by BPF_PROG_LOAD */ 713 | __u32 prog_type; 714 | __u32 insn_cnt; 715 | __aligned_u64 insns; /* 'const struct bpf_insn *' */ 716 | __aligned_u64 license; /* 'const char *' */ 717 | __u32 log_level; /* verbosity level of verifier */ 718 | __u32 log_size; /* size of user buffer */ 719 | __aligned_u64 log_buf; /* user supplied 'char *' buffer */ 720 | __u32 kern_version; /* checked when prog_type=kprobe (since Linux 4.1) */ 721 | }; 722 | } __attribute__((aligned(8))); 723 | ``` 724 | 725 | #### 使用 eBPF 程序的命令 726 | 727 | `BPF_PROG_LOAD` 命令用于校验和加载 eBPF 程序,其需要填充的参数 `bpf_xattr`,下面展示了在 `libbpf` 中 **`bpf_load_program`**[15] 的实现,可以看到最终是调用了 `bpf` 系统调用。 728 | 729 | ``` 730 | /* /tools/lib/bpf/bpf.c */ 731 | int bpf_load_program(enum bpf_prog_type type, const struct bpf_insn *insns, 732 | size_t insns_cnt, const char *license, 733 | __u32 kern_version, char *log_buf, 734 | size_t log_buf_sz) 735 | { 736 | struct bpf_load_program_attr load_attr; 737 | 738 | memset(&load_attr, 0, sizeof(struct bpf_load_program_attr)); 739 | load_attr.prog_type = type; 740 | load_attr.expected_attach_type = 0; 741 | load_attr.name = NULL; 742 | load_attr.insns = insns; 743 | load_attr.insns_cnt = insns_cnt; 744 | load_attr.license = license; 745 | load_attr.kern_version = kern_version; 746 | 747 | return bpf_load_program_xattr(&load_attr, log_buf, log_buf_sz); 748 | } 749 | 750 | int bpf_load_program_xattr(const struct bpf_load_program_attr *load_attr, 751 | char *log_buf, size_t log_buf_sz) 752 | { 753 | // ... 754 | fd = sys_bpf_prog_load(&attr, sizeof(attr)); 755 | if (fd >= 0) 756 | return fd; 757 | // ... 758 | } 759 | 760 | static inline int sys_bpf_prog_load(union bpf_attr *attr, unsigned int size) 761 | { 762 | int fd; 763 | 764 | do { 765 | fd = sys_bpf(BPF_PROG_LOAD, attr, size); 766 | } while (fd < 0 && errno == EAGAIN); 767 | 768 | return fd; 769 | } 770 | ``` 771 | 772 | #### 使用 eBPF Map 的命令 773 | 774 | 和前面一样,查看 `libbpf` 中 **`bpf_create_map`**[16] 的实现,可以看到最终也调用了 bpf 系统调用: 775 | 776 | ``` 777 | /* /tools/lib/bpf/bpf.c */ 778 | int bpf_create_map(enum bpf_map_type map_type, int key_size, 779 | int value_size, int max_entries, __u32 map_flags) 780 | { 781 | struct bpf_create_map_attr map_attr = {}; 782 | 783 | map_attr.map_type = map_type; 784 | map_attr.map_flags = map_flags; 785 | map_attr.key_size = key_size; 786 | map_attr.value_size = value_size; 787 | map_attr.max_entries = max_entries; 788 | 789 | return bpf_create_map_xattr(&map_attr); 790 | } 791 | 792 | int bpf_create_map_xattr(const struct bpf_create_map_attr *create_attr) 793 | { 794 | union bpf_attr attr; 795 | 796 | memset(&attr, '\0', sizeof(attr)); 797 | 798 | attr.map_type = create_attr->map_type; 799 | attr.key_size = create_attr->key_size; 800 | attr.value_size = create_attr->value_size; 801 | attr.max_entries = create_attr->max_entries; 802 | attr.map_flags = create_attr->map_flags; 803 | if (create_attr->name) 804 | memcpy(attr.map_name, create_attr->name, 805 | min(strlen(create_attr->name), BPF_OBJ_NAME_LEN - 1)); 806 | attr.numa_node = create_attr->numa_node; 807 | attr.btf_fd = create_attr->btf_fd; 808 | attr.btf_key_type_id = create_attr->btf_key_type_id; 809 | attr.btf_value_type_id = create_attr->btf_value_type_id; 810 | attr.map_ifindex = create_attr->map_ifindex; 811 | attr.inner_map_fd = create_attr->inner_map_fd; 812 | 813 | return sys_bpf(BPF_MAP_CREATE, &attr, sizeof(attr)); 814 | } 815 | ``` 816 | 817 | `libbpf` 中 **`bpf_map_lookup_elem`**[17] 的实现: 818 | 819 | ``` 820 | /* /tools/lib/bpf/bpf.c */ 821 | int bpf_map_lookup_elem(int fd, const void *key, void *value) 822 | { 823 | union bpf_attr attr; 824 | 825 | memset(&attr, 0, sizeof(attr)); 826 | attr.map_fd = fd; 827 | attr.key = ptr_to_u64(key); 828 | attr.value = ptr_to_u64(value); 829 | 830 | return sys_bpf(BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr)); 831 | } 832 | ``` 833 | 834 | `libbpf` 中 **`bpf_map_update_elem`**[18] 的实现: 835 | 836 | ``` 837 | /* /tools/lib/bpf/bpf.c */ 838 | int bpf_map_update_elem(int fd, const void *key, const void *value, 839 | __u64 flags) 840 | { 841 | union bpf_attr attr; 842 | 843 | memset(&attr, 0, sizeof(attr)); 844 | attr.map_fd = fd; 845 | attr.key = ptr_to_u64(key); 846 | attr.value = ptr_to_u64(value); 847 | attr.flags = flags; 848 | 849 | return sys_bpf(BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr)); 850 | } 851 | ``` 852 | 853 | `libbpf` 中 **`bpf_map_delete_elem`**[19] 的实现: 854 | 855 | ``` 856 | /* /tools/lib/bpf/bpf.c */ 857 | int bpf_map_delete_elem(int fd, const void *key) 858 | { 859 | union bpf_attr attr; 860 | 861 | memset(&attr, 0, sizeof(attr)); 862 | attr.map_fd = fd; 863 | attr.key = ptr_to_u64(key); 864 | 865 | return sys_bpf(BPF_MAP_DELETE_ELEM, &attr, sizeof(attr)); 866 | } 867 | ``` 868 | 869 | `libbpf` 中 **`bpf_map_get_next_key`**[20] 的实现: 870 | 871 | ``` 872 | /* /tools/lib/bpf/bpf.c */ 873 | int bpf_map_get_next_key(int fd, const void *key, void *next_key) 874 | { 875 | union bpf_attr attr; 876 | 877 | memset(&attr, 0, sizeof(attr)); 878 | attr.map_fd = fd; 879 | attr.key = ptr_to_u64(key); 880 | attr.next_key = ptr_to_u64(next_key); 881 | 882 | return sys_bpf(BPF_MAP_GET_NEXT_KEY, &attr, sizeof(attr)); 883 | } 884 | ``` 885 | 886 | 注意,这里的 `libbpf` 函数和之前提到的 `helper functions` 还不太一样,你可以在 **Linux Manual Page: bpf-helpers**[21] 看到当前 Linux 支持的 `Helper functions`。以 `bpf_map_update_elem` 为例,eBPF 程序通过调用 `helper function`,其参数如下: 887 | 888 | ``` 889 | struct msg { 890 | __s32 seq; 891 | __u64 cts; 892 | __u8 comm[MAX_LENGTH]; 893 | }; 894 | 895 | struct bpf_map_def SEC("maps") map = { 896 | .type = BPF_MAP_TYPE_PERF_EVENT_ARRAY, 897 | .key_size = sizeof(int), 898 | .value_size = sizeof(__u32), 899 | .max_entries = 0, 900 | }; 901 | 902 | void *bpf_map_lookup_elem(struct bpf_map *map, const void *key) 903 | ``` 904 | 905 | 这里的第一个参数来自于 `SEC(".maps")` 语法糖创建的 `bpf_map`。 906 | 907 | 对于用户态程序,则其函数原型如下,其中通过 fd 来访问 eBPF map。 908 | 909 | ``` 910 | int bpf_map_lookup_elem(int fd, const void *key, void *value) 911 | ``` 912 | 913 | ### BPF 程序类型 914 | 915 | 函数`BPF_PROG_LOAD`加载的程序类型规定了四件事: 916 | 917 | - 程序可以附加在哪里 918 | - 验证器允许调用内核中的哪些帮助函数 919 | - 网络包的数据是否可以直接访问 920 | - 作为第一个参数传递给程序的对象类型 921 | 922 | 实际上,程序类型本质上定义了一个 API。甚至还创建了新的程序类型,以区分允许调用的不同的函数列表(比如`BPF_PROG_TYPE_CGROUP_SKB` 对比 `BPF_PROG_TYPE_SOCKET_FILTER`)。 923 | 924 | bpf 程序会被 hook 到内核不同的 hook 点上。不同的 hook 点的入口参数,能力有所不同。因而定义了不同的 prog type。不同的 prog type 的 bpf 程序能够调用的 kernel function 集合也不一样。当 bpf 程序加载到内核时,内核的 verifier 程序会根据 bpf prog type,检查程序的入口参数,调用了哪些 helper function。 925 | 926 | 目前内核支持的 eBPF 程序类型列表如下所示: 927 | 928 | - `BPF_PROG_TYPE_SOCKET_FILTER`:一种网络数据包过滤器 929 | - `BPF_PROG_TYPE_KPROBE`:确定 kprobe 是否应该触发 930 | - `BPF_PROG_TYPE_SCHED_CLS`:一种网络流量控制分类器 931 | - `BPF_PROG_TYPE_SCHED_ACT`:一种网络流量控制动作 932 | - `BPF_PROG_TYPE_TRACEPOINT`:确定 tracepoint 是否应该触发 933 | - `BPF_PROG_TYPE_XDP`:从设备驱动程序接收路径运行的网络数据包过滤器 934 | - `BPF_PROG_TYPE_PERF_EVENT`:确定是否应该触发 perf 事件处理程序 935 | - `BPF_PROG_TYPE_CGROUP_SKB`:一种用于控制组的网络数据包过滤器 936 | - `BPF_PROG_TYPE_CGROUP_SOCK`:一种由于控制组的网络包筛选器,它被允许修改套接字选项 937 | - `BPF_PROG_TYPE_LWT_*`:用于轻量级隧道的网络数据包过滤器 938 | - `BPF_PROG_TYPE_SOCK_OPS`:一个用于设置套接字参数的程序 939 | - `BPF_PROG_TYPE_SK_SKB`:一个用于套接字之间转发数据包的网络包过滤器 940 | - `BPF_PROG_CGROUP_DEVICE`:确定是否允许设备操作 941 | 942 | 随着新程序类型的添加,内核开发人员同时发现也需要添加新的数据结构。 943 | 944 | 举个例子 BPF_PROG_TYPE_SCHED_CLS bpf prog , 能够访问哪些 bpf helper function 呢?让我们来看看源代码是如何实现的。 945 | 946 | 每一种 prog type 会定义一个 `struct bpf_verifier_ops` 结构体。当 prog load 到内核时,内核会根据它的 type,调用相应结构体的 get_func_proto 函数。 947 | 948 | ``` 949 | const struct bpf_verifier_ops tc_cls_act_verifier_ops = { 950 | .get_func_proto = tc_cls_act_func_proto, 951 | .convert_ctx_access = tc_cls_act_convert_ctx_access, 952 | }; 953 | ``` 954 | 955 | 对于 BPF_PROG_TYPE_SCHED_CLS 类型的 BPF 代码,verifier 会调用 `tc_cls_act_func_proto` ,以检查程序调用的 helper function 是否都是合法的。 956 | 957 | ### BPF 代码调用时机 958 | 959 | 每一种 prog type 的调用时机都不同。 960 | 961 | #### BPF_PROG_TYPE_SCHED_CLS 962 | 963 | BPF_PROG_TYPE_SCHED_CLS 的调用过程如下。 964 | 965 | ##### Egress 方向 966 | 967 | egress 方向上,tcp/ip 协议栈运行之后,有一个 hook 点。这个 hook 点可以 attach BPF_PROG_TYPE_SCHED_CLS type 的 egress 方向的 bpf prog。在这段 bpf 代码执行之后,才会运行 qos,tcpdump, xmit 到网卡 driver 的代码。在这段 bpf 代码中你可以修改报文里面的内容,地址等。修改之后,通过 tcpdump 可以看到,因为 tcpdump 代码在此之后才执行。 968 | 969 | ``` 970 | static int __dev_queue_xmit(struct sk_buff *skb, struct net_device *sb_dev) 971 | 972 | { 973 | skb = sch_handle_egress(skb, &rc, dev); 974 | // enqueue tc qos 975 | // dequeue tc qos 976 | // dev_hard_start_xmit 977 | // tcpdump works here! dev_queue_xmit_nit 978 | // nic driver->ndo_start_xmit 979 | } 980 | ``` 981 | 982 | ##### Ingress 方向 983 | 984 | ingress 方向上,在 deliver to tcp/ip 协议栈之前,在 tcpdump 之后,有一个 hook 点。这个 hook 点可以 attach BPF_PROG_TYPE_SCHED_CLS type 的 ingress 方向的 bpf prog。在这里你也可以修改报文。但是修改之后的结果在 tcpdump 中是看不到的。 985 | 986 | ``` 987 | static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc, 988 | struct packet_type **ppt_prev) 989 | { 990 | // generic xdp bpf hook 991 | // tcpdump 992 | // tc ingress hook 993 | skb = sch_handle_ingress(skb, &pt_prev, &ret, orig_dev, &another); 994 | // deliver to tcp/ip stack or bridge/ipvlan device 995 | } 996 | ``` 997 | 998 | ##### 执行入口 cls_bpf_classify 999 | 1000 | 无论 egress 还是 ingress 方向,真正执行 bpf 指令的入口都是 cls_bpf_classify。它遍历 tcf_proto 中的 bpf prog link list, 对每一个 bpf prog 执行 BPF_PROG_RUN(prog->filter, skb) 1001 | 1002 | ``` 1003 | static int cls_bpf_classify(struct sk_buff *skb, const struct tcf_proto *tp, 1004 | struct tcf_result *res) 1005 | { 1006 | struct cls_bpf_head *head = rcu_dereference_bh(tp->root); 1007 | struct cls_bpf_prog *prog; 1008 | 1009 | list_for_each_entry_rcu(prog, &head->plist, link) { 1010 | int filter_res; 1011 | if (tc_skip_sw(prog->gen_flags)) { 1012 | filter_res = prog->exts_integrated ? TC_ACT_UNSPEC : 0; 1013 | } else if (at_ingress) { 1014 | /* It is safe to push/pull even if skb_shared() */ 1015 | __skb_push(skb, skb->mac_len); 1016 | bpf_compute_data_pointers(skb); 1017 | filter_res = BPF_PROG_RUN(prog->filter, skb); 1018 | __skb_pull(skb, skb->mac_len); 1019 | } else { 1020 | bpf_compute_data_pointers(skb); 1021 | filter_res = BPF_PROG_RUN(prog->filter, skb); 1022 | } 1023 | } 1024 | ``` 1025 | 1026 | BPF_PROG_RUN 会执行 JIT compile 的 bpf 指令,如果内核不支持 JIT,则会调用解释器执行 bpf 的 byte code。 1027 | 1028 | BPF_PROG_RUN 传给 bpf prog 的入口参数是 skb,其类型是 `struct sk_buff`, 定义在文件 include/linux/skbuff.h 中。 1029 | 1030 | 但是在 bpf 代码中,为了安全,不能直接访问 `sk_buff`。bpf 中是通过访问 `struct __sk_buff` 来访问 struct sk_buff 的。`__sk_buff` 是 `sk_buff` 的一个子集,是 sk_buff 面向 bpf 程序的接口。bpf 代码中对 `__sk_buff` 的访问会在 verifier 程序中翻译成对 sk_buff 相应 fileds 的访问。 1031 | 1032 | 在加载 bpf prog 的时候,verifier 会调用上面 `tc_cls_act_verifier_ops` 结构体里面的 tc_cls_act_convert_ctx_access 的钩子。它最终会调用下面的函数修改 ebpf 的指令,使得对 `__sk_buff` 的访问变成对 `struct sk_buff` 的访问。 1033 | 1034 | ### BPF Attach type 1035 | 1036 | 一种 type 的 bpf prog 可以挂到内核中不同的 hook 点,这些不同的 hook 点就是不同的 attach type。 1037 | 1038 | 其对应关系在 **下面函数**[22] 中定义了。 1039 | 1040 | ``` 1041 | attach_type_to_prog_type(enum bpf_attach_type attach_type) 1042 | { 1043 | switch (attach_type) { 1044 | case BPF_CGROUP_INET_INGRESS: 1045 | case BPF_CGROUP_INET_EGRESS: 1046 | return BPF_PROG_TYPE_CGROUP_SKB; 1047 | case BPF_CGROUP_INET_SOCK_CREATE: 1048 | case BPF_CGROUP_INET_SOCK_RELEASE: 1049 | case BPF_CGROUP_INET4_POST_BIND: 1050 | case BPF_CGROUP_INET6_POST_BIND: 1051 | return BPF_PROG_TYPE_CGROUP_SOCK; 1052 | ..... 1053 | } 1054 | ``` 1055 | 1056 | 当 bpf prog 通过系统调用 bpf() attach 到具体的 hook 点时,其入口参数中就需要指定 attach type。 1057 | 1058 | 有趣的是,BPF_PROG_TYPE_SCHED_CLS 类型的 bpf prog 不能通过 bpf 系统调用来 attach,因为它没有定义对应的 attach type。故它的 attach 需要通过 netlink interface 额外的实现,还是非常复杂的。 1059 | 1060 | ### 常用 prog type 介绍 1061 | 1062 | 内核中的 prog type 目前有 30 种。每一种 type 能做的事情有所差异,这里只讲讲我平时工作用过的几种。 1063 | 1064 | 理解一种 prog type 的最好的方法是 1065 | 1066 | - 查表 attach_type_to_prog_type,得到它的 attach type, 1067 | - 再搜索内核代码,看这些 attach type 在内核哪里被调用了。 1068 | - 最后看看它的入口参数和 return value 的处理过程,基本就能理解其作用了。 1069 | 1070 | ``` 1071 | include/uapi/linux/bpf.h 1072 | 1073 | enum bpf_prog_type { 1074 | } 1075 | ``` 1076 | 1077 | #### BPF_PROG_TYPE_SOCKET_FILTER 1078 | 1079 | 是第一个被添加到内核的程序类型。当你 attach 一个 bpf 程序到 socket 上,你可以获取到被 socket 处理的所有数据包。socket 过滤不允许你修改这些数据包以及这些数据包的目的地。仅仅是提供给你观察这些数据包。在你的程序中可以获取到诸如 protocol type 类型等。 1080 | 1081 | 以 tcp 为 example,调用的地点是 tcp_v4_rcv->tcp_filter->sk_filter_trim_cap 作用是过滤报文,或者 trim 报文。udp, icmp 中也有相关的调用。 1082 | 1083 | #### BPF_PROG_TYPE_SOCK_OPS 1084 | 1085 | 在 tcp 协议 event 发生时调用的 bpf 钩子,定义了 15 种 event。这些 event 的 attach type 都是 BPF_CGROUP_SOCK_OPS。不同的调用点会传入不同的 enum, 比如: 1086 | 1087 | - BPF_SOCK_OPS_TCP_CONNECT_CB 是主动 tcp connect call 的; 1088 | - BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB 是被动 connect 成功时调用的。 1089 | 1090 | 主要作用:tcp 调优,event 统计等。 1091 | 1092 | BPF_PROG_TYPE_SOCK_OPS 这种程序类型,允许你当数据包在内核网络协议栈的各个阶段传输的时候,去修改套接字的链接选项。他们 attach 到 cgroups 上,和 BPF_PROG_TYPE_CGROUP_SOCK 以及 BPF_PROG_TYPE_CGROUP_SKB 很像,但是不同的是,他们可以在整个连接的生命周期内被调用好多次。你的 bpf 程序会接受到一个 op 的参数,该参数代表内核将通过套接字链接执行的操作。因此,你知道在链接的生命周期内何时调用该程序。另一方面,你可以获取 ip 地址,端口等。你还可以修改链接的链接的选项以设置超时并更改数据包的往返延迟时间。 1093 | 1094 | 举个例子,Facebook 使用它来为同一数据中心内的连接设置短恢复时间目标(RTO)。RTO 是一种时间,它指的是网络在出现故障后的恢复时间,这个指标也表示网络在受到不可接受到情况下的,不能被使用的时间。Facebook 认为,在同一数据中心中,应该有一个很短的 RTO,Facebook 修改了这个时间,使用 bpf 程序。 1095 | 1096 | #### BPF_PROG_TYPE_CGROUP_SOCK_ADDR 1097 | 1098 | 它对应很多 attach type,一般在 bind, connect 时调用, 传入 sock 的地址。 1099 | 1100 | 主要作用:例如 cilium 中 clusterip 的实现,在主动 connect 时,修改了目的 ip 地址,就是利用这个。 1101 | 1102 | BPF_PROG_TYPE_CGROUP_SOCK_ADDR,这种类型的程序使您可以在由特定 cgroup 控制的用户空间程序中操纵 IP 地址和端口号。在某些情况下,当您要确保一组特定的用户空间程序使用相同的 IP 地址和端口时,系统将使用多个 IP 地址.当您将这些用户空间程序放在同一 cgroup 中时,这些 BPF 程序使您可以灵活地操作这些绑定。这样可以确保这些应用程序的所有传入和传出连接均使用 BPF 程序提供的 IP 和端口。 1103 | 1104 | #### BPF_PROG_TYPE_SK_MSG 1105 | 1106 | BPF_PROG_TYPE_SK_MSG, These types of programs let you control whether a message sent to a socket should be delivered.当内核创建了一个 socket,它会被存储在前面提到的 map 中。当你 attach 一个程序到这个 socket map 的时候,所有的被发送到那些 socket 的 message 都会被 filter。在 filter message 之前,内核拷贝了这些 data,因此你可以读取这些 message,而且可以给出你的决定:例如,SK_PASS 和 SK_DROP。 1107 | 1108 | #### BPF_PROG_TYPE_SK_SKB 1109 | 1110 | 调用点:tcp sendmsg 时会调用。 1111 | 1112 | 主要作用:做 sock redir 用的。 1113 | 1114 | BPF_PROG_TYPE_SK_SKB,这类程序可以让你获取 socket maps 和 socket redirects。socket maps 可以让你获得一些 socket 的引用。当你有了这些引用,你可以使用相关的 helpers,去重定向一个 incoming 的 packet ,从一个 socket 去另外一个 scoket.这在使用 BPF 来做负载均衡时是非常有用的。你可以在 socket 之间转发网络数据包,而不需要离开内核空间。Cillium 和 facebook 的 Katran 广泛的使用这种类型的程序去做流量控制。 1115 | 1116 | #### BPF_PROG_TYPE_CGROUP_SOCKOPT 1117 | 1118 | 调用点:getsockopt, setsockopt 1119 | 1120 | #### BPF_PROG_TYPE_KPROBE 1121 | 1122 | 类似 ftrace 的 kprobe,在函数出入口的 hook 点,debug 用的。 1123 | 1124 | #### BPF_PROG_TYPE_TRACEPOINT 1125 | 1126 | 类似 ftrace 的 tracepoint。 1127 | 1128 | #### BPF_PROG_TYPE_SCHED_CLS 1129 | 1130 | 如上面的例子 1131 | 1132 | #### BPF_PROG_TYPE_XDP 1133 | 1134 | 网卡驱动收到 packet 时,尚未生成 sk_buff 数据结构之前的一个 hook 点。 1135 | 1136 | BPF_PROG_TYPE_XDP 允许你的 bpf 程序,在网络数据包到达 kernel 很早的时候。在这样的 bpf 程序中,你仅仅可能获取到一点点的信息,因为 kernel 还没有足够的时间去处理。因为时间足够的早,所以你可以在网络很高的层面上去处理这些 packet。 1137 | 1138 | XDP 定义了很多的处理方式,例如 1139 | 1140 | - XDP_PASS 就意味着,你会把 packet 交给内核的另一个子系统去处理 1141 | - XDP_DROP 就意味着,内核应该丢弃这个数据包 1142 | - XDP_TX 意味着,你可以把这个包转发到 network interface card(NIC)第一次接收到这个包的时候 1143 | 1144 | #### BPF_PROG_TYPE_CGROUP_SKB 1145 | 1146 | BPF_PROG_TYPE_CGROUP_SKB 允许你过滤整个 cgroup 的网络流量。在这种程序类型中,你可以在网络流量到达这个 cgoup 中的程序前做一些控制。内核试图传递给同一 cgroup 中任何进程的任何数据包都将通过这些过滤器之一。同时,您可以决定 cgroup 中的进程通过该接口发送网络数据包时该怎么做。其实,你可以发现它和 BPF_PROG_TYPE_SOCKET_FILTER 的类型很类似。最大的不同是 cgroup_skb 是 attach 到这个 cgroup 中的所有进程,而不是特殊的进程。在 container 的环境中,bpf 是非常有用的。 1147 | 1148 | - ingress 方向上,tcp 收到报文时(tcp_v4_rcv),会调用这个 bpf 做过滤。 1149 | - egress 方向上,ip 在出报文时(ip_finish_output)会调用它做丢包过滤 输入参数是 skb。 1150 | 1151 | #### BPF_PROG_TYPE_CGROUP_SOCK 1152 | 1153 | 在 sock create, release, post_bind 时调用的。主要用来做一些权限检查的。 1154 | 1155 | BPF_PROG_TYPE_CGROUP_SOCK,这种类型的 bpf 程序允许你,在一个 cgroup 中的任何进程打开一个 socket 的时候,去执行你的 Bpf 程序。这个行为和 CGROUP_SKB 的行为类似,但是它是提供给你 cgoup 中的进程打开一个新的 socket 的时候的情况,而不是给你网络数据包通过的权限控制。这对于为可以打开套接字的程序组提供安全性和访问控制很有用,而不必分别限制每个进程的功能。 1156 | 1157 | ## eBPF 工具链 1158 | 1159 | ### bcc 1160 | 1161 | BCC 是 BPF 的编译工具集合,前端提供 Python/Lua API,本身通过 C/C++ 语言实现,集成 LLVM/Clang 对 BPF 程序进行重写、编译和加载等功能, 提供一些更人性化的函数给用户使用。 1162 | 1163 | 虽然 BCC 竭尽全力地简化 BPF 程序开发人员的工作,但其“黑魔法” (使用 Clang 前端修改了用户编写的 BPF 程序)使得出现问题时,很难找到问题的所在以及解决方法。必须记住命名约定和自动生成的跟踪点结构 。且由于 libbcc 库内部集成了庞大的 LLVM/Clang 库,使其在使用过程中会遇到一些问题: 1164 | 1165 | 1. 在每个工具启动时,都会占用较高的 CPU 和内存资源来编译 BPF 程序,在系统资源已经短缺的服务器上运行可能引起问题; 1166 | 2. 依赖于内核头文件包,必须将其安装在每个目标主机上。即便如此,如果需要内核中未 export 的内容,则需要手动将类型定义复制/粘贴到 BPF 代码中; 1167 | 3. 由于 BPF 程序是在运行时才编译,因此很多简单的编译错误只能在运行时检测到,影响开发体验。 1168 | 1169 | 随着 BPF CO-RE 的落地,我们可以直接使用内核开发人员提供的 libbpf 库来开发 BPF 程序,开发方式和编写普通 C 用户态程序一样:一次编译生成小型的二进制文件。Libbpf 作为 BPF 程序加载器,接管了重定向、加载、验证等功能,BPF 程序开发者只需要关注 BPF 程序的正确性和性能即可。这种方式将开销降到了最低,且去除了庞大的依赖关系,使得整体开发流程更加顺畅。 1170 | 1171 | 性能优化大师 Brendan Gregg 在用 libbpf + BPF CO-RE 转换一个 BCC 工具后给出了性能对比数据: 1172 | 1173 | > As my colleague Jason pointed out, the memory footprint of opensnoop as CO-RE is much lower than opensnoop.py. 9 Mbytes for CO-RE vs 80 Mbytes for Python. 1174 | 1175 | 我们可以看到在运行时相比 BCC 版本,libbpf + BPF CO-RE 版本节约了近 9 倍的内存开销,这对于物理内存资源已经紧张的服务器来说会更友好。 1176 | 1177 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126113835921.png?imageView2/0/format/webp/q/75) 1178 | 1179 | 关于 BCC 可以参考 **我的这篇文章介绍**[23] 1180 | 1181 | ### bpftrace 1182 | 1183 | > bpftrace is a high-level tracing language for Linux eBPF and available in recent Linux kernels (4.x). bpftrace uses LLVM as a backend to compile scripts to eBPF bytecode and makes use of BCC for interacting with the Linux eBPF subsystem as well as existing Linux tracing capabilities: kernel dynamic tracing (kprobes), user-level dynamic tracing (uprobes), and tracepoints. The bpftrace language is inspired by awk, C and predecessor tracers such as DTrace and SystemTap. 1184 | 1185 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126113822257.png?imageView2/0/format/webp/q/75) 1186 | 1187 | ### eBPF Go Library 1188 | 1189 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126113803273.png?imageView2/0/format/webp/q/75) 1190 | 1191 | ### libbpf 1192 | 1193 | ![img](https://996station.com/wp-content/uploads/2022/11/20221126113623645.png?imageView2/0/format/webp/q/75) 1194 | 1195 | ## 参考资料 1196 | 1197 | - **The BSD Packet Filter: A New Architecture for User-level Packet Capture, Steven McCanne and Van Jacobso, December 19, 1992**[24] 1198 | - **eBPF Documentation: What is eBPF?**[25] 1199 | - **LWN: A thorough introduction to eBPF**[26] 1200 | - **Cilium Documentation: BPF and XDP Reference Guide**[27] 1201 | - **eBPF summit: The Future of eBPF based Networking and Security**[28] 1202 | - **eBPF - The Future of Networking & Security**[29] 1203 | - **eBPF - Rethinking the Linux Kernel**[30] 1204 | - **Linux Manual Page: bpf(2)**[31] 1205 | - **Linux Manual Page: bpf-helpers**[32] 1206 | - **Linux Kernel Documentation: Linux Socket Filtering aka Berkeley Packet Filter (BPF)**[33] 1207 | - **Dive into BPF: a list of reading material**[34] 1208 | - **LWN: eBPF materials**[35] 1209 | - **基于 Ubuntu 20.04 的 eBPF 环境搭建**[36] 1210 | 1211 | 相关文章推荐 1212 | 1213 | - **eBPF 指令集**[37] 1214 | - **eBPF tc 子系统**[38] 1215 | - **Linux Traffic Control**[39] 1216 | - **网卡聚合 Bonding**[40] 1217 | - **Linux 网络包收发流程**[41] 1218 | 1219 | ### 引用链接 1220 | 1221 | [1] 1222 | 1223 | BPF: *https://en.wikipedia.org/wiki/Berkeley_Packet_Filter*[2] 1224 | 1225 | Github: *https://github.com/*[3] 1226 | 1227 | The BSD Packet Filter: A New Architecture for User-level Packet Capture: *http://www.tcpdump.org/papers/bpf-usenix93.pdf*[4] 1228 | 1229 | include/linux/filter.h: *https://github.com/torvalds/linux/blob/v5.8/include/linux/filter.h*[5] 1230 | 1231 | include/linux/bpf.h: *https://github.com/torvalds/linux/blob/v5.8/include/linux/bpf.h*[6] 1232 | 1233 | 详见这里: *https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md#1-bpf_trace_printk*[7] 1234 | 1235 | `read_trace_pipe`: *https://elixir.bootlin.com/linux/latest/source/tools/testing/selftests/bpf/trace_helpers.c#L120*[8] 1236 | 1237 | `bpf_load.c`: *https://elixir.bootlin.com/linux/v5.4/source/samples/bpf/bpf_load.c#L659*[9] 1238 | 1239 | `load_and_attach`: *https://elixir.bootlin.com/linux/v5.4/source/samples/bpf/bpf_load.c#L76*[10] 1240 | 1241 | 我的这篇博文: *https://houmin.cc*[11] 1242 | 1243 | Linux Manual Page: bpf-helpers: *https://man7.org/linux/man-pages/man7/bpf-helpers.7.html*[12] 1244 | 1245 | include/linux/filter.h: *https://elixir.bootlin.com/linux/v5.4/source/include/linux/filter.h#L479*[13] 1246 | 1247 | `bpf_map_update_elem`: *https://elixir.bootlin.com/linux/v5.4/source/kernel/bpf/helpers.c#L41*[14] 1248 | 1249 | `bpf()`: *https://man7.org/linux/man-pages/man2/bpf.2.html*[15] 1250 | 1251 | `bpf_load_program`: *https://elixir.bootlin.com/linux/v5.4/source/tools/lib/bpf/bpf.c#L316*[16] 1252 | 1253 | `bpf_create_map`: *https://elixir.bootlin.com/linux/v5.4/source/tools/lib/bpf/bpf.c#L123*[17] 1254 | 1255 | `bpf_map_lookup_elem`: *https://elixir.bootlin.com/linux/v5.4/source/tools/lib/bpf/bpf.c#L371*[18] 1256 | 1257 | `bpf_map_update_elem`: *https://elixir.bootlin.com/linux/v5.4/source/tools/lib/bpf/bpf.c#L357*[19] 1258 | 1259 | `bpf_map_delete_elem`: *https://elixir.bootlin.com/linux/v5.4/source/tools/lib/bpf/bpf.c#L408*[20] 1260 | 1261 | `bpf_map_get_next_key`: *https://elixir.bootlin.com/linux/v5.4/source/tools/lib/bpf/bpf.c#L419*[21] 1262 | 1263 | Linux Manual Page: bpf-helpers: *https://man7.org/linux/man-pages/man7/bpf-helpers.7.html*[22] 1264 | 1265 | 下面函数: *https://elixir.bootlin.com/linux/v5.17-rc8/source/kernel/bpf/syscall.c#L3137*[23] 1266 | 1267 | 我的这篇文章介绍: *https://houmin.cc/posts/6a8748a1/*[24] 1268 | 1269 | The BSD Packet Filter: A New Architecture for User-level Packet Capture, Steven McCanne and Van Jacobso, December 19, 1992: *http://www.tcpdump.org/papers/bpf-usenix93.pdf*[25] 1270 | 1271 | eBPF Documentation: What is eBPF?: *https://ebpf.io/what-is-ebpf/*[26] 1272 | 1273 | LWN: A thorough introduction to eBPF: *https://lwn.net/Articles/740157/*[27] 1274 | 1275 | Cilium Documentation: BPF and XDP Reference Guide: *https://docs.cilium.io/en/stable/bpf/*[28] 1276 | 1277 | eBPF summit: The Future of eBPF based Networking and Security: *https://www.youtube.com/watch?v=slBAYUDABDA*[29] 1278 | 1279 | eBPF - The Future of Networking & Security: *https://cilium.io/blog/2020/11/10/ebpf-future-of-networking/*[30] 1280 | 1281 | eBPF - Rethinking the Linux Kernel: *https://www.youtube.com/watch?v=f-oTe-dmfyI*[31] 1282 | 1283 | Linux Manual Page: bpf(2): *https://man7.org/linux/man-pages/man2/bpf.2.html*[32] 1284 | 1285 | Linux Manual Page: bpf-helpers: *https://man7.org/linux/man-pages/man7/bpf-helpers.7.html*[33] 1286 | 1287 | Linux Kernel Documentation: Linux Socket Filtering aka Berkeley Packet Filter (BPF): *https://www.kernel.org/doc/Documentation/networking/filter.txt*[34] 1288 | 1289 | Dive into BPF: a list of reading material: *https://qmonnet.github.io/whirl-offload/2016/09/01/dive-into-bpf/*[35] 1290 | 1291 | LWN: eBPF materials: *https://lwn.net/Kernel/Index/#Berkeley_Packet_Filter*[36] 1292 | 1293 | 基于 Ubuntu 20.04 的 eBPF 环境搭建: *https://www.ebpf.top/post/ebpf_c_env/*[37] 1294 | 1295 | eBPF 指令集: *https://houmin.cc/posts/5150fab3/*[38] 1296 | 1297 | eBPF tc 子系统: *https://houmin.cc/posts/28ca4f79/*[39] 1298 | 1299 | Linux Traffic Control: *https://houmin.cc/posts/8278f23c/*[40] 1300 | 1301 | 网卡聚合 Bonding: *https://houmin.cc/posts/e9c4d3e9/*[41] 1302 | 1303 | Linux 网络包收发流程: *https://houmin.cc/posts/941301e/* 1304 | 1305 | ## 作者 1306 | 1307 | 云原生实验室 1308 | 1309 | ## 原文链接 1310 | 1311 | https://mp.weixin.qq.com/s/zCjk5WmnwLD0J3J9gC4e0Q 1312 | --------------------------------------------------------------------------------