标签 eBPF 下的文章

JavaScript 调查表明:多数人使用 React 但不满意

从专门 针对 JavaScript 的 2021 年度调查中可以发现,在 JavaScript 框架和库方面,React 的使用率继续领先,其次是 Angular 和 Vue.js。React 的满意度明显下降,在 Vue.js、Solid 和 Svelte 之后排名第四。Solid 在满意度方面排名第一,在兴趣方面排名第二,即使在实际使用它的人方面排名倒数第二。

老王点评:这个调查报告还披露了很多信息,值得一看。

声称恢复 GPU 挖矿性能的工具实际上是恶意软件

一些英伟达的新显卡带有性能限制,以减少该显卡对加密货币矿工的吸引力。大量的 GPU 显卡被用来挖矿,使得游戏玩家和其他人很难获得这种硬件,而且导致价格昂贵。前两天,有网站介绍了一个用于可以打开 RTX 显卡的加密货币挖掘性能限制。不过,第二天他们发现“该工具非但没有解决挖矿性能上限的问题,反而使主机系统感染了恶意软件”。目前还不清楚该恶意软件到底是做什么的,可能是键盘记录、信息窃取,也有可能是加密货币挖掘。

老王点评:真是哭笑不得的事情。好吧,现在没人和玩家们抢新显卡了。

红帽扩展 eBPF 用于输入设备的 HID 子系统

在内核中运行沙盒程序的 eBPF 非常有用,它已经超越了起源于网络子系统中的最初的 BPF,它对其他领域,如安全、追踪和内核内 JIT 虚拟机等也非常实用。红帽发出了 一组补丁 为 HID 设备引入了 eBPF 支持。其中一个非常有用的领域是用于有问题或奇怪的设备,这可以将修复放在一些外部仓库中,并作为 eBPF 程序在启动时加载,以避免需要创建新的内核,而经历漫长的上下游过程。

老王点评:如果你还没有了解过 eBFP,建议了解一下,它已经越来越强大重要了。

微软演示在无 TPM、VBS 保护的计算机上如何黑进 Windows

微软在新的 Windows 11 中强制要求 TPM 2.0 和基于虚拟化的安全(VBS)等安全措施,引来了不少批评。为了捍卫自己的做法和观点,微软还展示了一个 视频演示,显示了潜在的黑客是如何轻松掌控这种没有这些功能的脆弱机器的。该视频的第一部分展示了成功利用一个脆弱的开放远程桌面协议(RDP)端口获得管理员权限,并在一台没有启用 TPM 2.0 和安全启动的 Windows 10 PC 上分发模拟的勒索软件。之后,还展示了在一台没有 VBS 的 PC 上绕过指纹认证过程对用户登录的本地利用。

老王点评:这种现身说法让用户有直观感受,从这一点上,我是支持微软加强安全要求的。

苹果公司认为对 Epic 的胜利还不够

之前在苹果公司和 Epic 诉讼案的裁决中,苹果公司赢得了实质上的大部分胜利:只是苹果公司必须允许应用程序将客户引向外部网站,这对苹果的影响甚微。当时苹果公司的法律部们得意洋洋地认为这是“响亮的胜利”,而 Epic 则表示要对败诉的九项判决提起上述。不过,上周五苹果公司 要求法院暂停执行该裁决,准备发起上诉。这样一来,这场诉讼有可能扯上几年,而苹果也无需现在对其产品和服务做出改变。

老王点评:果然大公司都是法律达人啊。

沃尔玛把内核功能也“摆上了货架”

eBPF 是一种革命性的技术,允许我们在操作系统内核中运行沙盒程序。沃尔玛的 L3AF 项目的愿景是“内核功能即服务”:L3AF 团队、开源社区和其他企业可以开发独立的 eBPF 程序,并在一个 eBPF 的“内核功能市场”中共享。用户可以下载签名的 eBPF 程序,并编排它们来解决独特的业务需求。沃尔玛开源了 L3AF,并将其 托管到 Linux 基金会 之下。

老王点评:果然是超市大佬,连内核功能都“放上了货架” :D 。这是容器之后非常值得重视的内核技术。

亚马逊将监控客服人员键盘输入和鼠标点击以提升安全性

根据一份亚马逊机密文件显示,亚马逊计划监控客户服务员工的键盘和鼠标动作,以试图阻止流氓员工、冒名顶替者或黑客访问客户的数据。但并不是为了准确记录员工的输入内容或监控他们的通信,而是根据键盘和鼠标动作生成一个档案,然后验证是否似乎是同一个人在控制员工的账户,以抓住可能随后窃取数据的黑客或冒名者。

这是一个有趣的思路,可以根据一个人的键盘和鼠标“指纹”来识别异常信号。

黑客退还 6 亿美元加密货币,声称是出于好玩

我们之前报道过,黑客利用了一个 Poly Network 智能合约的漏洞,窃取了超过 6 亿美元的加密资产。该事件之后,迫于各方追查的压力,该黑客陆陆续续的地几乎将所窃取的所有加密资产退回了项目方留下的地址。黑客自称是出于安全考虑将加密货币转移到个人钱包的,担心如果将漏洞告诉其他人(包括项目团队)可能有人会无法抵挡如此巨大一笔金钱的吸引力。

有些嘲讽的是,去中心化的区块链世界,最终却需要中心化的威慑和帮助来拯救。

Linux 基金会旗下成立了 eBPF 基金会

作为基础设施领域最具影响力的技术之一,eBPF 已在过去几年迎来了爆发式的增长。包括微软、谷歌、Facebook、Netflix 等在内的多家企业宣布,在 Linux 基金会麾下成立了一个全新的 eBPF 基金会,以规划和协调 eBPF 的发展。

操作系统内核由于其核心作用、以及对稳定性和安全性的高要求而难以演进。与在操作系统之外实现的功能相比,操作系统级别的创新速度通常都是较低的。而 eBPF 通过允许沙盒程序在操作系统中运行,使开发人员能够创建在运行时向操作系统添加功能的 eBPF 程序。

eBPF 的崛起真是令人吃惊,想不到最初的一个防火墙程序最终成为了这样重要的一个技术。

阿里云正式发布它的首个 CentOS 兼容发行版 Anolis OS 8.2

在红帽宣布“停更” CentOS Linux 后,引来了技术圈的颇多争议,国内外涌现出多款 CentOS 8 的替代发行版,并已在日前陆续发布。

昨日,由阿里云支持的 Anolis OS 宣布发布首个正式发布版本。此次发布的 8.2 支持 x86\_64 和 aarch64 架构,搭载双内核 RHCK(RHEL 兼容内核)和 ANCK(OpenAnolis 云内核),其中 ANCK 是由社区 Cloud Kernel SIG 组基于上游 4.19 LTS 内核研发,增强了稳定、性能、隔离能力;对飞腾、海光、兆芯、鲲鹏芯片提供了完善支持;支持阿里的龙井云原生 Java 运行时;还提供了 CentOS 系统到 Anolis OS 的迁移工具。更多特性可见发行公告

阿里云宣布 Anolis OS 已经有快半年了,从目前披露的正式版本特性来看,还是值得一看的。

微软要将 Linux 工具 eBPF 引入 Windows 10

eBPF 是一项重要的性能观测和调优技术,最初来自于 BSD 的一个防火墙程序 BPF,后在 Linux 系统上衍生为扩展 BPF,即 eBPF。eBPF 对于网络过滤、分析和管理非常有用,但它的工作远不止这些。eBPF 也被用于系统调用过滤和进程上下文跟踪。它已经成为编程跟踪、系统剖析以及收集和汇总低级自定义指标的一把瑞士军刀。在更高层次上,eBPF 已经成为安全程序的基础。

微软启动了一个新的开源项目,试图将 eBPF 引入到 Windows 10 和 Windows Server 2016 及以后的版本中。这个项目将现有 eBPF 工具链和 API 作为子模块,并在其间添加一层,使其可以在 Windows 之上运行。目前该项目处于非常早期的阶段,还没有公布时间表。

好吧,微软又从 Linux 宝库中挖掘到一个好东西。

微软将威胁和漏洞管理能力引入 Linux

微软正在使 IT 专业人员能够使用该公司的端点防御产品对 Linux 设备的安全进行监控。微软的威胁和漏洞管理(TVM)可用于 Windows 和 Windows 服务器,现在也在公开预览中支持 macOS 和 Linux。TVM 允许用户审查应用程序漏洞和整个 Linux 系统的潜在错误配置,并补救任何受影响的管理和非管理设备。目前,用户可以利用这一功能在 macOS 和 Linux 中发现、优先处理和补救 30 多个已知的不安全配置。今年夏天晚些时候,TVM 也将支持 Android 和 iOS。

微软的“野心”越来越大了,不过我觉得未尝不是一件好事。

长文预警:在最新的 Linux 内核(>=4.4)中使用 eBPF,你可以将任何内核函数调用转换为一个带有任意数据的用户空间事件。这通过 bcc 来做很容易。这个探针是用 C 语言写的,而数据是由 Python 来处理的。

如果你对 eBPF 或者 Linux 跟踪不熟悉,那你应该好好阅读一下整篇文章。本文尝试逐步去解决我在使用 bcc/eBPF 时遇到的困难,以为您节省我在搜索和挖掘上花费的时间。

在 Linux 的世界中关于推送与轮询的一个看法

当我开始在容器上工作的时候,我想知道我们怎么基于一个真实的系统状态去动态更新一个负载均衡器的配置。一个通用的策略是这样做的,无论什么时候只要一个容器启动,容器编排器触发一个负载均衡配置更新动作,负载均衡器去轮询每个容器,直到它的健康状态检查结束。它也许只是简单进行 “SYN” 测试。

虽然这种配置方式可以有效工作,但是它的缺点是你的负载均衡器为了让一些系统变得可用需要等待,而不是 … 让负载去均衡。

可以做的更好吗?

当你希望在一个系统中让一个程序对一些变化做出反应,这里有两种可能的策略。程序可以去 轮询 系统去检测变化,或者,如果系统支持,系统可以 推送 事件并且让程序对它作出反应。你希望去使用推送还是轮询取决于上下文环境。一个好的经验法则是,基于处理时间的考虑,如果事件发生的频率较低时使用推送,而当事件发生的较快或者让系统变得不可用时切换为轮询。例如,一般情况下,网络驱动程序将等待来自网卡的事件,但是,像 dpdk 这样的框架对事件将主动轮询网卡,以达到高吞吐低延迟的目的。

理想状态下,我们将有一些内核接口告诉我们:

  • “容器管理器,你好,我刚才为容器 servestaticfiles 的 Nginx-ware 创建了一个套接字,或许你应该去更新你的状态?
  • “好的,操作系统,感谢你告诉我这个事件”

虽然 Linux 有大量的接口去处理事件,对于文件事件高达 3 个,但是没有专门的接口去得到套接字事件提示。你可以得到路由表事件、邻接表事件、连接跟踪事件、接口变化事件。唯独没有套接字事件。或者,也许它深深地隐藏在一个 Netlink 接口中。

理想情况下,我们需要一个做这件事的通用方法,怎么办呢?

内核跟踪和 eBPF,一些它们的历史

直到最近,内核跟踪的唯一方式是对内核上打补丁或者借助于 SystemTap。SytemTap 是一个 Linux 系统跟踪器。简单地说,它提供了一个 DSL,编译进内核模块,然后被内核加载运行。除了一些因安全原因禁用动态模块加载的生产系统之外,包括在那个时候我开发的那一个。另外的方式是为内核打一个补丁程序以触发一些事件,可能是基于 netlink。但是这很不方便。深入内核所带来的缺点包括 “有趣的” 新 “特性” ,并增加了维护负担。

从 Linux 3.15 开始给我们带来了希望,它支持将任何可跟踪内核函数可安全转换为用户空间事件。在一般的计算机科学中,“安全” 是指 “某些虚拟机”。在此也不例外。自从 Linux 2.1.75 在 1997 年正式发行以来,Linux 已经有这个多好年了。但是,它被称为伯克利包过滤器,或简称 BPF。正如它的名字所表达的那样,它最初是为 BSD 防火墙开发的。它仅有两个寄存器,并且它仅允许向前跳转,这意味着你不能使用它写一个循环(好吧,如果你知道最大迭代次数并且去手工实现它,你也可以实现循环)。这一点保证了程序总会终止,而不会使系统处于挂起的状态。还不知道它有什么用?你用过 iptables 的话,其作用就是 CloudFlare 的 DDos 防护的基础

好的,因此,随着 Linux 3.15,BPF 被扩展 成为了 eBPF。对于 “扩展的” BPF。它从两个 32 位寄存器升级到 10 个 64 位寄存器,并且增加了它们之间向后跳转的特性。然后它 在 Linux 3.18 中被进一步扩展,并将被移出网络子系统中,并且增加了像映射(map)这样的工具。为保证安全,它 引进了一个检查器,它验证所有的内存访问和可能的代码路径。如果检查器不能保证代码会终止在固定的边界内,它一开始就要拒绝程序的插入。

关于它的更多历史,可以看 Oracle 的关于 eBPF 的一个很棒的演讲

让我们开始吧!

来自 inet\_listen 的问候

因为写一个汇编程序并不是件十分容易的任务,甚至对于很优秀的我们来说,我将使用 bcc。bcc 是一个基于 LLVM 的工具集,并且用 Python 抽象了底层机制。探针是用 C 写的,并且返回的结果可以被 Python 利用,可以很容易地写一些不算简单的应用程序。

首先安装 bcc。对于一些示例,你可能会需要使用一个最新的内核版本(>= 4.4)。如果你想亲自去尝试一下这些示例,我强烈推荐你安装一台虚拟机, 而不是 一个 Docker 容器。你不能在一个容器中改变内核。作为一个非常新的很活跃的项目,其安装教程高度依赖于平台/版本。你可以在 https://github.com/iovisor/bcc/blob/master/INSTALL.md 上找到最新的教程。

现在,我希望不管在什么时候,只要有任何程序开始监听 TCP 套接字,我将得到一个事件。当我在一个 AF_INET + SOCK_STREAM 套接字上调用一个 listen() 系统调用时,其底层的内核函数是 inet_listen。我将从钩在一个“Hello World” kprobe 的入口上开始。

from bcc import BPF

# Hello BPF Program
bpf_text = """ 
#include <net/inet_sock.h>
#include <bcc/proto.h>

// 1. Attach kprobe to "inet_listen"
int kprobe__inet_listen(struct pt_regs *ctx, struct socket *sock, int backlog)
{
    bpf_trace_printk("Hello World!\\n");
    return 0;
};
"""

# 2. Build and Inject program
b = BPF(text=bpf_text)

# 3. Print debug output
while True:
    print b.trace_readline()

这个程序做了三件事件:

  1. 它通过命名惯例来附加到一个内核探针上。如果函数被调用,比如说 my_probe 函数,它会使用 b.attach_kprobe("inet_listen", "my_probe") 显式附加。
  2. 它使用 LLVM 新的 BPF 后端来构建程序。使用(新的) bpf() 系统调用去注入结果字节码,并且按匹配的命名惯例自动附加探针。
  3. 从内核管道读取原生输出。

注意:eBPF 的后端 LLVM 还很新。如果你认为你遇到了一个 bug,你也许应该去升级。

注意到 bpf_trace_printk 调用了吗?这是一个内核的 printk() 精简版的调试函数。使用时,它产生跟踪信息到一个专门的内核管道 /sys/kernel/debug/tracing/trace_pipe 。就像名字所暗示的那样,这是一个管道。如果多个读取者在读取它,仅有一个将得到一个给定的行。对生产系统来说,这样是不合适的。

幸运的是,Linux 3.19 引入了对消息传递的映射,以及 Linux 4.4 带来了对任意 perf 事件的支持。在这篇文章的后面部分,我将演示基于 perf 事件的方式。

# From a first console
ubuntu@bcc:~/dev/listen-evts$ sudo /python tcv4listen.py 
              nc-4940  [000] d... 22666.991714: : Hello World!

# From a second console
ubuntu@bcc:~$ nc -l 0 4242
^C

搞定!

抓取 backlog

现在,让我们输出一些很容易访问到的数据,比如说 “backlog”。backlog 是正在建立 TCP 连接的、即将被 accept() 的连接的数量。

只要稍微调整一下 bpf_trace_printk

bpf_trace_printk("Listening with with up to %d pending connections!\\n", backlog);

如果你用这个 “革命性” 的改善重新运行这个示例,你将看到如下的内容:

(bcc)ubuntu@bcc:~/dev/listen-evts$ sudo python tcv4listen.py 
              nc-5020  [000] d... 25497.154070: : Listening with with up to 1 pending connections!

nc 是个单连接程序,因此,其 backlog 是 1。而 Nginx 或者 Redis 上的 backlog 将在这里输出 128 。但是,那是另外一件事。

简单吧?现在让我们获取它的端口。

抓取端口和 IP

正在研究的 inet_listen 来源于内核,我们知道它需要从 socket 对象中取得 inet_sock。只需要从源头拷贝,然后插入到跟踪器的开始处:

// cast types. Intermediate cast not needed, kept for readability
struct sock *sk = sock->sk;
struct inet_sock *inet = inet_sk(sk);

端口现在可以按网络字节顺序(就是“从小到大、大端”的顺序)从 inet->inet_sport 访问到。很容易吧!因此,我们只需要把 bpf_trace_printk 替换为:

bpf_trace_printk("Listening on port %d!\\n", inet->inet_sport);

然后运行:

ubuntu@bcc:~/dev/listen-evts$ sudo /python tcv4listen.py 
...
R1 invalid mem access 'inv'
...
Exception: Failed to load BPF program kprobe__inet_listen

抛出的异常并没有那么简单,Bcc 现在提升了 许多。直到写这篇文章的时候,有几个问题已经被处理了,但是并没有全部处理完。这个错误意味着内核检查器可以证实程序中的内存访问是正确的。看这个显式的类型转换。我们需要一点帮助,以使访问更加明确。我们将使用 bpf_probe_read 可信任的函数去读取一个任意内存位置,同时确保所有必要的检查都是用类似这样方法完成的:

// Explicit initialization. The "=0" part is needed to "give life" to the variable on the stack
u16 lport = 0;

// Explicit arbitrary memory access. Read it:
//    Read into 'lport', 'sizeof(lport)' bytes from 'inet->inet_sport' memory location
bpf_probe_read(&lport, sizeof(lport), &(inet->inet_sport));

读取 IPv4 边界地址和它基本上是相同的,使用 inet->inet_rcv_saddr 。如果我把这些一起放上去,我们将得到 backlog、端口和边界 IP:

from bcc import BPF  

# BPF Program  
bpf_text = """   
#include <net/sock.h>  
#include <net/inet_sock.h>  
#include <bcc/proto.h>  

// Send an event for each IPv4 listen with PID, bound address and port  
int kprobe__inet_listen(struct pt_regs *ctx, struct socket *sock, int backlog)  
{  
    // Cast types. Intermediate cast not needed, kept for readability  
    struct sock *sk = sock->sk;  
    struct inet_sock *inet = inet_sk(sk);  

    // Working values. You *need* to initialize them to give them "life" on the stack and use them afterward  
    u32 laddr = 0;  
    u16 lport = 0;  

    // Pull in details. As 'inet_sk' is internally a type cast, we need to use 'bpf_probe_read'  
    // read: load into 'laddr' 'sizeof(laddr)' bytes from address 'inet->inet_rcv_saddr'  
    bpf_probe_read(&laddr, sizeof(laddr), &(inet->inet_rcv_saddr));  
    bpf_probe_read(&lport, sizeof(lport), &(inet->inet_sport));  

    // Push event
    bpf_trace_printk("Listening on %x %d with %d pending connections\\n", ntohl(laddr), ntohs(lport), backlog);  
    return 0;
};  
"""  

# Build and Inject BPF  
b = BPF(text=bpf_text)  

# Print debug output  
while True:  
  print b.trace_readline()

测试运行输出的内容像下面这样:

(bcc)ubuntu@bcc:~/dev/listen-evts$ sudo python tcv4listen.py 
              nc-5024  [000] d... 25821.166286: : Listening on 7f000001 4242 with 1 pending connections

这证明你的监听是在本地主机上的。因为没有处理为友好的输出,这里的地址以 16 进制的方式显示,但是这是没错的,并且它很酷。

注意:你可能想知道为什么 ntohsntohl 可以从 BPF 中被调用,即便它们并不可信。这是因为它们是宏,并且是从 “.h” 文件中来的内联函数,并且,在写这篇文章的时候一个小的 bug 已经 修复了

全部达成了,还剩下一些:我们希望获取相关的容器。在一个网络环境中,那意味着我们希望取得网络的命名空间。网络命名空间是一个容器的构建块,它允许它们拥有独立的网络。

抓取网络命名空间:被迫引入的 perf 事件

在用户空间中,网络命名空间可以通过检查 /proc/PID/ns/net 的目标来确定,它将看起来像 net:[4026531957] 这样。方括号中的数字是网络空间的 inode 编号。这就是说,我们可以通过 /proc 来取得,但是这并不是好的方式,我们或许可以临时处理时用一下。我们可以从内核中直接抓取 inode 编号。幸运的是,那样做很容易:

// Create an populate the variable
u32 netns = 0;

// Read the netns inode number, like /proc does
netns = sk->__sk_common.skc_net.net->ns.inum;

很容易!而且它做到了。

但是,如果你看到这里,你可能猜到那里有一些错误。它在:

bpf_trace_printk("Listening on %x %d with %d pending connections in container %d\\n", ntohl(laddr), ntohs(lport), backlog, netns);

如果你尝试去运行它,你将看到一些令人难解的错误信息:

(bcc)ubuntu@bcc:~/dev/listen-evts$ sudo python tcv4listen.py
error: in function kprobe__inet_listen i32 (%struct.pt_regs*, %struct.socket*, i32)
too many args to 0x1ba9108: i64 = Constant<6>

clang 想尝试去告诉你的是 “嗨,哥们,bpf_trace_printk 只能带四个参数,你刚才给它传递了 5 个”。在这里我不打算继续追究细节了,但是,那是 BPF 的一个限制。如果你想继续去深入研究,这里是一个很好的起点

去修复它的唯一方式是 … 停止调试并且准备投入使用。因此,让我们开始吧(确保运行在内核版本为 4.4 的 Linux 系统上)。我将使用 perf 事件,它支持传递任意大小的结构体到用户空间。另外,只有我们的读者可以获得它,因此,多个没有关系的 eBPF 程序可以并发产生数据而不会出现问题。

去使用它吧,我们需要:

  1. 定义一个结构体
  2. 声明事件
  3. 推送事件
  4. 在 Python 端重新声明事件(这一步以后将不再需要)
  5. 处理和格式化事件

这看起来似乎很多,其它并不多,看下面示例:

// At the begining of the C program, declare our event
struct listen_evt_t {
    u64 laddr;
    u64 lport;
    u64 netns;
    u64 backlog;
};
BPF_PERF_OUTPUT(listen_evt);

// In kprobe__inet_listen, replace the printk with
struct listen_evt_t evt = {
    .laddr = ntohl(laddr),
    .lport = ntohs(lport),
    .netns = netns,
    .backlog = backlog,
};
listen_evt.perf_submit(ctx, &evt, sizeof(evt));

Python 端将需要一点更多的工作:

# We need ctypes to parse the event structure
import ctypes

# Declare data format
class ListenEvt(ctypes.Structure):
    _fields_ = [
        ("laddr",   ctypes.c_ulonglong),
        ("lport",   ctypes.c_ulonglong),
        ("netns",   ctypes.c_ulonglong),
        ("backlog", ctypes.c_ulonglong),
    ]

# Declare event printer
def print_event(cpu, data, size):
    event = ctypes.cast(data, ctypes.POINTER(ListenEvt)).contents
    print("Listening on %x %d with %d pending connections in container %d" % (
        event.laddr,
        event.lport,
        event.backlog,
        event.netns,
    ))

# Replace the event loop
b["listen_evt"].open_perf_buffer(print_event)
while True:
    b.kprobe_poll()

来试一下吧。在这个示例中,我有一个 redis 运行在一个 Docker 容器中,并且 nc 运行在主机上:

(bcc)ubuntu@bcc:~/dev/listen-evts$ sudo python tcv4listen.py
Listening on 0 6379 with 128 pending connections in container 4026532165
Listening on 0 6379 with 128 pending connections in container 4026532165
Listening on 7f000001 6588 with 1 pending connections in container 4026531957

结束语

现在,所有事情都可以在内核中使用 eBPF 将任何函数的调用设置为触发事件,并且你看到了我在学习 eBPF 时所遇到的大多数的问题。如果你希望去看这个工具的完整版本,像 IPv6 支持这样的一些技巧,看一看 https://github.com/iovisor/bcc/blob/master/tools/solisten.py。它现在是一个官方的工具,感谢 bcc 团队的支持。

更进一步地去学习,你可能需要去关注 Brendan Gregg 的博客,尤其是 关于 eBPF 映射和统计的文章。他是这个项目的主要贡献人之一。


via: https://blog.yadutaf.fr/2016/03/30/turn-any-syscall-into-event-introducing-ebpf-kernel-probes/

作者:Jean-Tiare Le Bigot 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

今天,我喜欢的 meetup 网站上有一篇我超爱的文章!Suchakra Sharma@tuxology 在 twitter/github)的一篇非常棒的关于传统 BPF 和在 Linux 中最新加入的 eBPF 的讨论文章,正是它促使我想去写一个 eBPF 的程序!

这篇文章就是 —— BSD 包过滤器:一个新的用户级包捕获架构

我想在讨论的基础上去写一些笔记,因为,我觉得它超级棒!

开始前,这里有个 幻灯片 和一个 pdf。这个 pdf 非常好,结束的位置有一些链接,在 PDF 中你可以直接点击这个链接。

什么是 BPF?

在 BPF 出现之前,如果你想去做包过滤,你必须拷贝所有的包到用户空间,然后才能去过滤它们(使用 “tap”)。

这样做存在两个问题:

  1. 如果你在用户空间中过滤,意味着你将拷贝所有的包到用户空间,拷贝数据的代价是很昂贵的。
  2. 使用的过滤算法很低效。

问题 #1 的解决方法似乎很明显,就是将过滤逻辑移到内核中。(虽然具体实现的细节并没有明确,我们将在稍后讨论)

但是,为什么过滤算法会很低效?

如果你运行 tcpdump host foo,它实际上运行了一个相当复杂的查询,用下图的这个树来描述它:

评估这个树有点复杂。因此,可以用一种更简单的方式来表示这个树,像这样:

然后,如果你设置 ether.type = IPip.src = foo,你必然明白匹配的包是 host foo,你也不用去检查任何其它的东西了。因此,这个数据结构(它们称为“控制流图” ,或者 “CFG”)是表示你真实希望去执行匹配检查的程序的最佳方法,而不是用前面的树。

为什么 BPF 要工作在内核中

这里的关键点是,包仅仅是个字节的数组。BPF 程序是运行在这些字节的数组之上。它们不允许有循环(loop),但是,它们 可以 有聪明的办法知道 IP 包头(IPv6 和 IPv4 长度是不同的)以及基于它们的长度来找到 TCP 端口:

x = ip_header_length
port = *(packet_start + x + port_offset) 

(看起来不一样,其实它们基本上都相同)。在这个论文/幻灯片上有一个非常详细的虚拟机的描述,因此,我不打算解释它。

当你运行 tcpdump host foo 后,这时发生了什么?就我的理解,应该是如下的过程。

  1. 转换 host foo 为一个高效的 DAG 规则
  2. 转换那个 DAG 规则为 BPF 虚拟机的一个 BPF 程序(BPF 字节码)
  3. 发送 BPF 字节码到 Linux 内核,由 Linux 内核验证它
  4. 编译这个 BPF 字节码程序为一个 原生 native 代码。例如,这是个ARM 上的 JIT 代码 以及 x86 的机器码
  5. 当包进入时,Linux 运行原生代码去决定是否过滤这个包。对于每个需要去处理的包,它通常仅需运行 100 - 200 个 CPU 指令就可以完成,这个速度是非常快的!

现状:eBPF

毕竟 BPF 出现已经有很长的时间了!现在,我们可以拥有一个更加令人激动的东西,它就是 eBPF。我以前听说过 eBPF,但是,我觉得像这样把这些片断拼在一起更好(我在 4 月份的 netdev 上我写了这篇 XDP & eBPF 的文章回复)

关于 eBPF 的一些事实是:

  • eBPF 程序有它们自己的字节码语言,并且从那个字节码语言编译成内核原生代码,就像 BPF 程序一样
  • eBPF 运行在内核中
  • eBPF 程序不能随心所欲的访问内核内存。而是通过内核提供的函数去取得一些受严格限制的所需要的内容的子集
  • 它们 可以 与用户空间的程序通过 BPF 映射进行通讯
  • 这是 Linux 3.18 的 bpf 系统调用

kprobes 和 eBPF

你可以在 Linux 内核中挑选一个函数(任意函数),然后运行一个你写的每次该函数被调用时都运行的程序。这样看起来是不是很神奇。

例如:这里有一个 名为 disksnoop 的 BPF 程序,它的功能是当你开始/完成写入一个块到磁盘时,触发它执行跟踪。下图是它的代码片断:

BPF_HASH(start, struct request *);
void trace_start(struct pt_regs *ctx, struct request *req) {
    // stash start timestamp by request ptr
    u64 ts = bpf_ktime_get_ns();
    start.update(&req, &ts);
}
...
b.attach_kprobe(event="blk_start_request", fn_name="trace_start")
b.attach_kprobe(event="blk_mq_start_request", fn_name="trace_start")

本质上它声明一个 BPF 哈希(它的作用是当请求开始/完成时,这个程序去触发跟踪),一个名为 trace_start 的函数将被编译进 BPF 字节码,然后附加 trace_start 到内核函数 blk_start_request 上。

这里使用的是 bcc 框架,它可以让你写 Python 式的程序去生成 BPF 代码。你可以在 https://github.com/iovisor/bcc 找到它(那里有非常多的示例程序)。

uprobes 和 eBPF

因为我知道可以附加 eBPF 程序到内核函数上,但是,我不知道能否将 eBPF 程序附加到用户空间函数上!那会有更多令人激动的事情。这是 在 Python 中使用一个 eBPF 程序去计数 malloc 调用的示例

附加 eBPF 程序时应该考虑的事情

  • 带 XDP 的网卡(我之前写过关于这方面的文章)
  • tc egress/ingress (在网络栈上)
  • kprobes(任意内核函数)
  • uprobes(很明显,任意用户空间函数??像带调试符号的任意 C 程序)
  • probes 是为 dtrace 构建的名为 “USDT probes” 的探针(像 这些 mysql 探针)。这是一个 使用 dtrace 探针的示例程序
  • JVM
  • 跟踪点
  • seccomp / landlock 安全相关的事情
  • 等等

这个讨论超级棒

在幻灯片里有很多非常好的链接,并且在 iovisor 仓库里有个 LINKS.md。虽然现在已经很晚了,但是我马上要去写我的第一个 eBPF 程序了!


via: https://jvns.ca/blog/2017/06/28/notes-on-bpf---ebpf/

作者:Julia Evans 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出