ebpf学习-初探ebpf

ebpf是什么

Linux内核一直是实现监视/可观察性,网络和安全性的理想场所。不幸的是,这通常是不切实际的,因为它需要更改内核源代码或加载内核模块,并导致彼此堆叠的抽象层。 eBPF是一项革命性的技术,可以在Linux内核中运行沙盒程序,而无需更改内核源代码或加载内核模块。通过使Linux内核可编程,基础架构软件可以利用现有的层,从而使它们更加智能和功能丰富,而无需继续为系统增加额外的复杂性层。

eBPF导致了网络,安全性,应用程序配置/跟踪和性能故障排除等领域的新一代工具的开发,这些工具不再依赖现有的内核功能,而是在不影响执行效率或安全性的情况下主动重新编程运行时行为。

如果直接解释eBPF,有点不明所以。那我们就看看有哪些基于eBPF的工程,这些工程或许你已经知道,或是已经经常使用,也许你会明白eBPF距离我们并不遥远。

基于eBPF的项目

  1. bcc
    BCC是用于创建基于eBPF的高效内核跟踪和操作程序的工具包,其中包括一些有用的命令行工具和示例。 BCC简化了用C进行内核检测的eBPF程序的编写,包括LLVM的包装器以及Python和Lua的前端。它还提供了用于直接集成到应用程序中的高级库。

  2. bpftrace
    bpftrace是Linux eBPF的高级跟踪语言。它的语言受awk和C以及DTrace和SystemTap等以前的跟踪程序的启发。 bpftrace使用LLVM作为后端将脚本编译为eBPF字节码,并利用BCC作为与Linux eBPF子系统以及现有Linux跟踪功能和连接点进行交互的库。

  3. Cilium
    Cilium是一个开源项目,提供基于eBPF的联网,安全性和可观察性。它是从头开始专门设计的,旨在将eBPF的优势带入Kubernetes的世界,并满足容器工作负载的新可伸缩性,安全性和可见性要求。

  4. Falco
    Falco是一种行为活动监视器,旨在检测应用程序中的异常活动。 Falco在eBPF的帮助下审核Linux内核层的系统。它使用其他输入流(例如容器运行时度量标准和Kubernetes度量标准)丰富了收集的数据,并允许连续监视和检测容器,应用程序,主机和网络活动。

  5. Katran
    Katran是一个C ++库和eBPF程序,用于构建高性能的第4层负载平衡转发平面。 Katran利用Linux内核中的XDP基础结构来提供用于快速数据包处理的内核功能。它的性能与NIC接收队列的数量成线性比例,并且使用RSS友好的封装转发到L7负载平衡器。

  6. Sysdig

    Sysdig是提供深层系统可见性的简单工具,并具有对容器的原生支持。
    其他基于eBPF技术的项目还有很多,比如kubectl-trace ,ply 等,这里不再赘述。

一个简单的DEMO

下面我们写一个简单的demo来展示下开发一个ebpf程序需要哪些步骤,这里我们采用Cilium/ebpf库进行开发,因为ebpf的用户态代码可以用golang开发。:P

程序是通过ebpf的tracepoint监听syscalls/sys_enter_execve,在执行该方法时候获取pid和commd。通过ebpf的map将pid和commd传到用户态。

step.1 环境搭建

系统配置: Ubuntu 20.04.4
系统依赖: llvm,clang-10.0.0-4ubuntu1,还有linux的kernel这里源码在路径(/kernel-src),golang环境

step.2 编写ebpf的kernel侧的程序-c代码

  1. 编写代码并且用下面命令将代码变成bpf_program.o
    clang -O2 -target bpf -c bpf_program.c -I/kernel-src/tools/testing/selftests/bpf -o bpf_program.o
  2. 将bpf_program.o文件copy到golang程序下

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <linux/bpf.h>
#include <bpf_helpers.h>
#include <stdio.h>
#include <linux/types.h>

#define SEC(NAME) __attribute__((section(NAME), used))

//定义c的结构,将pid和comm封装到该结构体,将该结构体通过tracker_map传递给用户态
struct event_data_t {
__u32 pid;
char comm[20];
};

//定义ebpf的map
struct bpf_map_def SEC("maps") tracker_map = {
.type = BPF_MAP_TYPE_HASH,
.key_size = sizeof(int),
.value_size = sizeof(struct event_data_t),
.max_entries = 2048,
};

//ebpf程序挂载点,
SEC("tracepoint/syscalls/sys_enter_execve")
int bpf_prog(void *ctx) {
int index = 0;

struct event_data_t evt = {};


__u64 id = bpf_get_current_pid_tgid();
evt.pid = id >> 32;

bpf_get_current_comm(&evt.comm, sizeof(evt.comm));
bpf_map_update_elem(&tracker_map, &index, &evt, BPF_ANY);

return 0;
}

//协议
char _license[] SEC("license") = "GPL";

step3用户态侧代码-golang的代码

  • 编写go代码
  • 执行go build ./
  • 执行go程序,这里是go-tools
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
var mapKey uint32 = 0

// ebpf标签的说明
// BpfProg对应的是c程序中的bSEC(tracepoinst/...)的方法名
// TrackerMap对应的c程序中的SEC(map)
type bpfPrograms struct {
BpfProg *ebpf.Program `ebpf:"bpf_prog"`
TrackerMap *ebpf.Map `ebpf:"tracker_map"`
}

func (p *bpfPrograms) Close() error {
err := p.BpfProg.Close()
err = p.TrackerMap.Close()
return err
}

// 字段类型,对应c程序中的event_data_t类型,注意Comm是uint8
type EventData struct {
Pid uint32
Comm [20]uint8
}

func (e EventData) CommHex() string {
ba := []byte{}
for _, b := range e.Comm {
ba = append(ba, b)
}
return string(ba)
}

//程序
func LoadTestEbpf() {

//
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatal(err)
}

//加载c编译出来的.o文件的相对路径,这个地址一定不能出错
spec, err := ebpf.LoadCollectionSpec("ebpf/bpf_program.o")
if err != nil {
panic(err)
}

//赋值obj
obj := bpfPrograms{}
if err := spec.LoadAndAssign(&obj, nil); err != nil {
panic(err)
}
defer obj.Close()

tp, err := link.Tracepoint("syscalls", "sys_enter_execve", obj.BpfProg)
if err != nil {
panic(err)
}
defer tp.Close()

// Read loop reporting the total amount of times the kernel
// function was entered, once per second.
ticker := time.NewTicker(1 * time.Second)
log.Println("Waiting for events..")
for range ticker.C {

var value []byte
//c的结构体序列化成bytes(value)
if err := obj.TrackerMap.Lookup(&mapKey, &value); err != nil {
log.Printf("reading map: %v", err)
} else {
var event EventData
//c的结构体序列化成golang的喜爱那个并且打印出来
if err := binary.Read(bytes.NewBuffer(value), binary.LittleEndian, &event); err != nil {
log.Printf("parsing perf event: %s", err)
continue
}
log.Printf("pid:%v,comm:%v", event.Pid, event.CommHex())
}
}
}

程序入口

1
2
3
func main() {
ebpf.LoadTestEbpf()
}

step3,运行查看效果

  • 在终端1上运行:./go-tools
  • 在中断2上输入任何指令
  • 查看终端1的输出
1
2
2022/03/11 17:01:26 reading map: lookup: key does not exist
2022/03/11 17:01:30 pid:60184,comm:sshd