部署安装
安装很方便,注意环境要求,官方说明文档基于的内核版本是ubuntu5.19.0.可参考https://tetragon.cilium.io/docs/getting-started/kubernetes-quickstart-guide/. 安装完成后,查看tetragon监听端口如下所示:
# netstat -apn|grep tetragon
tcp 0 0 127.0.0.1:54321 0.0.0.0:* LISTEN 4172154/tetragon
tcp 0 0 127.0.0.1:8118 0.0.0.0:* LISTEN 4172154/tetragon
tcp 0 0 10.1.0.1:54108 10.1.0.1:443 ESTABLISHED 4172154/tetragon
tcp6 0 0 :::2112 :::* LISTEN 4172154/tetragon
其中54321是grpc server监听端口,8118是gops监听端口;2112是metrics-server监听端口。54108是tetragon和k8s server建立的连接端口,用户获取集群信息。(daemonset)tetragon 里面有另个容器 tetragon 和 export-stdout, 另外还有一个已结束的初始化容器 tetragon-operator.
下载编译
https://github.com/cilium/tetragon 下载源码,直接make. 编译步骤可查看https://tetragon.cilium.io/docs/contribution-guide/development-setup/ .注意golang版本,安装了GUN make,libcap and libelf,并已启动了docker进程。
编译完成后可以通过命令sudo ./tetragon --bpf-lib bpf/objs --server-address 0.0.0.0:5678
启动tetragon进程.
- bpf-lib 指示了编译好的 BPF 程序目录
- server-address表示监听地址,不填写的话默认是127.0.0.1:54321,该端口是grpc server
配置
tetragon使用configmap的形式进行配置,配置说明可以参考https://tetragon.cilium.io/docs/reference/tetragon-configuration/#configuration-precedence. 除了configmap中的配置之外,tetragon还有默认的配置如下:
config-dir:/etc/tetragon/tetragon.conf.d/
data-cache-size:1024
event-queue-size:10000
export-aggregation-buffer-size:10000
export-aggregation-window-size:15s
netns-dir:/var/run/docker/netns/
process-cache-size:65536 procfs:/procRoot rb-size:0 rb-size-total:0
release-pinned-bpf:true
server-address:localhost:54321
tracing-policy: tracing-policy-dir:/etc/tetragon/tetragon.tp.d
可以动态修改log level:
- sudo kill -s SIGRTMIN+20 $(pidof tetragon) //debug level
- sudo kill -s SIGRTMIN+21 $(pidof tetragon) //trace level
- sudo kill -s SIGRTMIN+22 $(pidof tetragon) //original log level
主函数流程
tetragon/cmd/tetragon/tetragon.go main().execute() -->
- readConfigSettings()
- startGopsServer(), 常用8118端口,默认不开启,使用https://github.com/google/gops/list and diagnose Go processes on your machine
- tetragonExecute() -->
- 检查mountFS /sys/fs/bpf(sensor map), /run/tetragon/cgroup2(隔离、资源限制),ConfigureResourceLimits(删除RAM限制)等.
- DetectCgroupMode()探测cgroup版本并获取资源隔离种类
- 创建observer, the link between the BPF perf ring and the listeners
- 初试化sensormanager, InitSensorManager(), policyfilter/state.go 的init()会自动初始化好容器的hook函数用于policy过滤匹配
- WatchTracePolicy() 开启Watch TracingPolicy crd 资源
- LogPinnedBpf()
- LogSensorsAndProbes()
- observer.RunEvents() 处理上报事件
sensor
sensor是tetragon里一个很重要的概念,它是一组BPF programs and maps的集合单元,主要包含一组program.Program对象和一组program.Map对象。最终会调用loadProgram()把bpf对象loads and attach到observer的sensor上。整个程序最重要的两个变量是:
var (
// list of registered policy handlers, see RegisterPolicyHandlerAtInit()
registeredPolicyHandlers = map[string]policyHandler{}
// list of registers loaders, see registerProbeType()
registeredProbeLoad = map[string]probeLoader{}
)
tetragon进程默认创建一个名为“base”的sensor, 它有几个map:execve_map、execve_map_stats、execve_calls、names_map、tcpmon_map和tg_conf_map. “base”还有两个program:bpf_exit.o和bpf_fork.o,对应的probe是bpf_execve_event_v53.o,类型为execve. 每添加一个tracingpolicy对象,就会在生成一系列gkp-sensor-2文件,序号全局计数,tetragon重启后再从1开始计算。pkg/sensors/tracing包下由两个文件对传感器进行默认注册,分别是generictracepoint.go,generickprobe.go和genericuprobe.go,写入如下两个Sensors到registeredTracingSensors中。
- observerKprobeSensor ,kprobe类型HOOK
- observerTracepointSensor, tracepoint类型HOOK
- observerUprobeSensor, uprobe类型HOOK
base sensor
我们先来看默认的sensor(base)是如何工作的。首先是加载默认sensor,使用函数base.GetInitialSensor().Load(observerDir, observerDir)
实现。然后开始获取系统调用,当有挂载点事件发生时,挂载在内核的程序会把数据写入对应的map中,同时用户空间的程序通过RunEvents()函数读取map数据。通过过滤器xx,从xx读取map数据,通过export xx,把最终的log打印出来。
加载具体实现在pkg/sensors/base/base.go文件中. 以execve调用为例,base.go文件中定义的program如下:
Execve = program.Builder(
"bpf_execve_event.o", //由bpf/process/bpf_execve_event.c编译而成,放在objs文件夹中
"sched/sched_process_exec", //挂载点
"tracepoint/sys_execve", //Label is the program section name to load from program.
"event_execve", //pinFile 在/sys/fs/bpf/tetragon文件夹中,名字是event_execve
"execve", //探针种类
)
除了execve, base sensor还默认加载了Exit和Fork程序;tcpmon_map、execve_map等6个map是event_execve程序使用的map. 可参考函数GetDefaultPrograms()和GetDefaultMaps(). 可以通过bpftool map list; bpftool map dump name execve_map
来查看map execve_map中的数据;通过bpftool prog list;bpftool prog dump xlated name event_execve
来查看ebpf 程序event_execve的汇编输出。
那么加载这些ebpf program的流程是什么样的呢?简述如下:
tetragon使用的是 libbpf 库,可以通过 bpf_obj_pin(fd, path) 将 map fd 绑定到文件系统中的指定文件;接下来,其他程序获取这个 fd,只需执行 bpf_obj_get(pinned_file)。pin ebpf prog也是和map一样调用了同样的pin接口。这样bpf数据和程序不再绑定到单个执行线程。 信息可以由不同的应用程序共享,并且BPF程序甚至可以在创建它们的应用程序终止后运行。 如果没有BPF文件系统,这将为他们提供额外的级别或可用性.
上面bpf_obj_pin函数的参数fd指的是由bpf/process/.c编译而成的elf文件.o, 可以通过readelf -a bpf_execve_event.o
读取elf文件。在readelf输出的符号表(Symbol table)中,我们看到一个Type为FUNC的符号bpf_prog,这个就是我们编写的BPF程序的入口,以bpf_execve_event为例,有两个程序入口,分别是tracepoint/sys_execve和tracepoint/0. 从readelf输出可以看到:bpf_prog(即序号为157的section)的Size为30952,但是它的内容是什么呢?这个readelf提示无法展开linux BPF类型的section。我们使用另外一个工具llvm-objdump将bpf_prog的内容展开:llvm-objdump -d bpf_execve_event.o
llvm-objdump输出的bpf_prog的内容其实就是BPF的字节码.BPF程序就是以字节码形式加载到内核中的,这是为了安全,增加了BPF虚拟机这层屏障。在BPF程序加载到内核的过程中,BPF虚拟机会对BPF字节码进行验证并运行JIT编译将字节码编译为机器码。而Pin到Linux 文件系统/sys/fs/bpf/tetragon 下面的的BPF程序也是字节码形式。
最后,使用尾调来扩展函数。例如tail_call(ctx, &execve_calls, 0);
就是调用类型为 BPF_MAP_TYPE_PROG_ARRAY的map execve_calls里面的第0个程序继续处理。这些尾调函数主要是为了处理挂载点抓到的数据。
以base sensor监听的execve事件为例。当系统发生execve调用时,挂载在对应tracepoint点上的程序把数据写入map中
apiVersion: v1
data:
enable-k8s-api: "true"
enable-process-cred: "false"
enable-process-ns: "false"
export-allowlist: '{"event_set":["PROCESS_EXEC", "PROCESS_EXIT", "PROCESS_KPROBE",
"PROCESS_UPROBE"]}'
export-denylist: |-
{"health_check":true}
{"namespace":["", "cilium", "kube-system"]}
export-file-compress: "false"
export-file-max-backups: "5"
export-file-max-size-mb: "10"
export-filename: /var/run/cilium/tetragon/tetragon.log
export-rate-limit: "-1"
field-filters: '{}'
gops-address: localhost:8118
log-level: debug
metrics-server: :2112
process-cache-size: "65536"
procfs: /procRoot
server-address: localhost:54321
按理说系统中会发生大量的execve事件,但是我们查看export-stdout容器的输出却没有那么多。这是因为过滤了,过滤的原则定义在了configmap中,运行PROCESS_EXEC", "PROCESS_EXIT", "PROCESS_KPROBE","PROCESS_UPROBE"类型事件,但是不显示namespace为空或者是"cilium", "kube-system"容器的这些事件,且只显示容器的事件信息。那这是如何做到的呢?查看execve_map中的数据确实包含了所有execve事件,包括容器和非容器的,而export读取后根据配置进行过滤并组合成较好理解的字符串进行展示。但是如果使用这个命令kubectl exec -it -n kube-system ds/tetragon -c tetragon -- tetra getevents -o compact
可以看到所有execve事件,不经过过滤器。
/var/run/cilium/tetragon/tetragon.log 或者 export-stdout输出的信息格式是 api/v1/tetragon/tetragon.proto中定义的, HandleMessage() 函数会把读取到的event数据封装发送给tetragon grpcserver,虽然这里的server是grpc server,但是没有用到grpc调用,用了类似回调函数的原理。先是oberver添加一个listener(processmanager),并启动RunEvents()从perfReader中读取map数据,然后把数据交给listerner(即processmanager)处理。同时,server添加一个listener(AddListener),这个监听者对应的通知者是processmanager.当processmanager有数据时就Notify server,然后server就调用encode把事件数据输出。输出的格式默认为protojson格式,即pkg/v1/tetragon/tetragon.proto中定义的Process格式。而这个encoder是在tetragon的main.go startExporter()函数中定义:protoencoder := encoder.NewProtojsonEncoder(writer)
, 最终输出实现是在pkg/encoder/encoders.go中对应的Encode()中,该文件还有compact带颜色的输出格式。sensors/exec/exec.go里面也有一个处理函数handleExecve(),不过这个函数看上去就是打印了trace log,没有其他的作用。这里绕来绕去的是为啥?要是你来设计的话会这么设计吗?
而export-stdout容器很简单就运行一个脚本hubble-export-stdout,该脚本的内容如下,即把参数文件的内容输出。
#!/bin/sh
set -e
tail -q -F "$@" 2> /dev/null
添加动态sensor
base sensor不包含action.我们将在下面的例子中解析一个包含action的sensor. 通过创建新的tracingpolicy来动态添加sensor.首先是定义一个YAML文件格式的tracingpolicy,如下所示:
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
name: "fd-install"
spec:
kprobes:
- call: "fd_install"
syscall: false
args:
- index: 0
type: "int"
- index: 1
type: "file"
selectors:
- matchArgs:
- index: 1
operator: "Equal"
values:
- "/tmp/tetragon"
matchActions:
- action: Sigkill
使用kubectl apply -f xxx.yaml
添加tracingpolicy, 最终会在/sys/fs/bpf/tetragon/文件加下看到一些列gkp-sensor-x的文件,即对应新加sensor. 首先是tetragon watch到新tracingpolicy的创建addTracingPolicy(),然后startSensorManager()中会收到信号,触发sensor的addTracingPolicy()函数解析出sensor并加载。在newPfMap()函数中的kernels.GenericKprobeObjs()中会安装通用的bpf程序bpf_generic_kprobe.o
查看bpf/process/bpf_generic_kprobe.c中的代码会看到和base sensor不同,客制化的kprobe探针都是挂载到统一的点“kprobe/generic_kprobe”,然后再通过尾调函数进行处理。另外,还有一个挂载点挂载点kprobe/generic_kprobe_override 处理函数结果覆盖的。通用的 kprobe 伪代码如下:
- 进行进程 ID 过滤 -> 如果没有匹配项,则丢弃
- 进行命名空间过滤 -> 如果没有匹配项,则丢弃
- 进行权限过滤 -> 如果没有匹配项,则丢弃
- 进行命名空间变更过滤 -> 如果没有匹配项,则丢弃
- 进行权限变更过滤 -> 如果没有匹配项,则丢弃
- 复制参数缓冲区
- 进行选择器过滤 -> 如果没有匹配项,则丢弃
- 生成环形缓冲区事件
首先,我们通过进程 ID 进行过滤,这允许我们快速丢弃与当前不相关的事件。如果我们需要复制大字符串值,这非常有帮助。然后,我们复制参数,然后运行完整的选择器逻辑。我们跟踪通过了初始过滤的进程 ID,以避免两次运行进程 ID 过滤器。对于 4.19 版本的内核,我们必须使用尾调用基础架构以确保指令数不超过 4K。对于 5.x+ 版本的内核,有 1 百万条指令,不是问题。
尾调函数 kprobe 0-10 都是过滤参数的,kprobe 11是处理action的,kprobe 12 是处理输出的。这里着重看一下action的处理。
在bpf_generic_kprobe.c 的尾调函数kprobe 11中的generic_actions找到处理函数do_actions-->do_action, 如果action类型是signal_kill,则最终会通过send_signal把信号发送出去。整个处理action的流程都是在BPF程序完成的,在挂载点完成的。