学习cillium开源项目tetragon

发布时间 2023-09-26 15:33:08作者: shininglight

部署安装

安装很方便,注意环境要求,官方说明文档基于的内核版本是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:

  1. sudo kill -s SIGRTMIN+20 $(pidof tetragon) //debug level
  2. sudo kill -s SIGRTMIN+21 $(pidof tetragon) //trace level
  3. 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中。

  1. observerKprobeSensor ,kprobe类型HOOK
  2. observerTracepointSensor, tracepoint类型HOOK
  3. 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.ollvm-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 伪代码如下:

  1. 进行进程 ID 过滤 -> 如果没有匹配项,则丢弃
  2. 进行命名空间过滤 -> 如果没有匹配项,则丢弃
  3. 进行权限过滤 -> 如果没有匹配项,则丢弃
  4. 进行命名空间变更过滤 -> 如果没有匹配项,则丢弃
  5. 进行权限变更过滤 -> 如果没有匹配项,则丢弃
  6. 复制参数缓冲区
  7. 进行选择器过滤 -> 如果没有匹配项,则丢弃
  8. 生成环形缓冲区事件
    首先,我们通过进程 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程序完成的,在挂载点完成的。