项目背景
随着 k8s/云原生等技术日渐普及,docker 容器在生产中的应用愈加广泛。由于 docker 并不是一个完整的操作系统,使用的内核依然是宿主机内核,则在 docker 实际使用过程中,可能会遭受攻击或产生泄露,从而威胁其他 docker 或宿主机。因而我们需要对 docker 内部的进程行为、文件修改等进行监视,在出现问题后便于回溯。
设计思路
整体设计
项目整体采用 MVC 的设计方式,设计思路如下图所示:
项目主要分为信息采集、信息过滤、数据库、信息展示四个模块,其工作方式如下:
- 信息采集模块,在各宿主机上安装、并以 root 权限启动,负责监听由 Linux 内核发出的 netlink connector 消息、audit 审计消息,将其整理为有关进程的、有关文件的数据,送入数据库中。
- 在使用时,用信息过滤模块连接数据库,该模块将从数据库中取出所有的消息并过滤无关内容,得到以 docker 守护进程为根的进程树;并在此树的基础上,对数据库中关于文件的记录进行过滤与整理。完成后,将过滤得到的数据送入数据库。
- 信息展示模块,简要地展示过滤得到的、有关 docker 的数据。
信息采集
信息采集模块对应本项目下的 listener 目录,主要负责收集 Linux-kernel 发出的 audit 系统审计消息、netlink connector 进程消息。分为四个协程,各自功能如下:
- 1 号协程,接收来自内核的 audit 审计消息,传递给 2 号协程
- 2 号,拿到 1 号发来的消息。对于 audit 审计消息而言,一个事件会被拆分为多条消息发送,但使用相同的时间戳、事务号。因而 2 号将收到的消息使用正则表达式进行简单解析,并用哈希表按照事务号存储,直到收到 eoe(本事件到此结束),将 hash 表中整理得到的 Event 事件发送给 4 号协程。
- 3 号,接收来自内核的 connector 消息,获取其中的进程事件(fork/exit)及进程号(ppid/parentTgid/pid/tgid),并通过/proc 文件系统查询 pid 对应的命令行参数 args、当前运行目录 cwd、根文件系统 rootfs、docker id(从 cgroup 查看),整理为 Event 事件,发送给 4 号协程
- 4 号,接收 Event 事件,判断其类型,分别处理。代码中事件类型主要有进程复制、进程退出、进程执行文件(execve)、文件打开、文件关闭、写文件、切换根系统(pivot_root)等几种。
信息过滤
进程过滤与优化
首先,由于 listener 模块在插入时采用了多线程,可能出现同一个进程的两条消息被并行处理、数据库中出现两条记录,因而第一步,是将相同 pid 的多条记录合并为一条。
现在开始考虑清洗数据的问题。Docker 是一个 C/S 架构的服务,因而我们真正关心的 docker 有关进程一定是 docker 守护进程的后代(虽然可能作为孤儿进程被 systemd 收养)。过滤进程数据,只需要构建以守护进程为根的进程树。在信息收集过程中,我们对 docker 守护进程(/usr/bin/dockerd
)进行了特殊记录,标记了该 pid 的star=true
。在过滤过程中,主要工作即围绕该 pid 展开。
- 我们记录的条目以 pid 区分,而这里的 pid 实质上指的是 task id、可能是线程,tgid 才是 task group id 应当理解为进程。因而,为了构建进程树,最简单的办法是将各个 pid 按照 tgid 区分,成为一个新的结构;这些结构代表着进程、是进程树的节点,因而称为 tgidNode。在此过程中,我们也可以整理得到每个 tgid 的所有子代 tgid 编号。
- 整理出来若干 tgidNode,从标记了 star 的 tgidNode 开始,采用广度优先遍历,得到整个进程树上的所有 tgidNode
接着,进行数据优化:
- 同一个 docker id 使用相同的 rootfs。在记录中,同一个 docker id 只有一个进程进行过 pivot_root,因而需要加以处理。
- 同一个进程(tgid)的不同线程(pid)可能 ppid/parentTgid 不一样。原因为,在进程(pid==tgid)创建的时候,父进程一定还在;但过一会创建线程的时候,原父进程可能已死、该进程已经被 systemd 收容,所以记录的 ppid/parentTgid 不对。为解决该问题,需要检查每个 pid,如果存在该问题则进行修正,防止在按 pid 溯源时出错。
- 部分 pid 可能并未收到对应的退出消息。为了部分地解决该问题,我们将进程退出时间(也就是 pid==tgid 的 pid 的退出时间)记录为没有 exit timestamp 的 pid 的退出时间。这样的补全是为了接下来在处理文件时使用。
文件过滤
众所周知,Linux 环境下,进程操作文件使用的是系统调用+文件描述符。
在记录的时候,由于 Linux 下进程是通过 open 系统调用,传入文件名和权限,得到文件描述符,使用、关闭时都是操作文件描述符而非文件名,所以记录时应当把已经关闭的和尚未关闭的区分开来。写文件时,在已经打开但尚未关闭的文件里按照 pid+fd 查找,记录写入时间;关闭时,将记录从 fd 表删除,加上关闭时间后存储到关闭的文件里。
但在整理时,二者都有写入记录,应该等同视之。将两张表的所有记录提取出来进行筛选,只保留 pid 在进程树上的那些文件记录;而后,对于尚未关闭的文件,查询 pid 退出时间,如有记录,则认为该文件在 pid 退出时才关闭。
最后,将处理得到的 tgidNode 构成的进程树、筛选之后的文件,全部记录到数据库里。
信息展示
现在已经获取了进程树和文件修改的详细记录,展示即可。本项目目前是在过滤完成之后,直接由过滤模块将进程树、进程详细信息、文件修改记录全部打印到标准输出。
编译与运行
本项目的编译运行较为简单。
在将本项目克隆下来后:
git submodule --init
cd listener
go build -o godo
cd ../filter
go build -o filter
编译完成后,将 godo 放置在宿主机上运行,godo 必须以 root 权限运行。有若干命令行参数,可以通过sudo ./godo -h
查看。注意:
- 指定参数使用等号,如
-diag
参数表示将内核原始 audit 消息输出到指定文件,使用时即sudo ./godo -diag=1.txt
- 默认的数据库是本机的 mongodb,端口 27017;如要连接别的数据库,需要使用
-mongo
参数指定其链接,格式为ip:port
。本处并未设置 mongodb 的用户名、密码,而是放开了权限直接登录。使用的数据库名为"test"。 - backlog 大小默认为 1GB,最好只大不小。以字节为单位。
- filter 放置在数据库所在的机器上,连接数据库。使用的数据库为 test,写入的数据库为 cooked。
而filter程序则直接放置在数据库所在机器上,在需要回溯的时候,直接运行filter程序(数据库没有账号密码控制),会输出进程树、每个进程的参数,及最终受改变的文件列表。