容器——seccomp-bpf¶
BPF¶
BPF 本来指的是 Berkeley Packet Filter,如其字面义,是用在网络包的过滤这个场景上的,最早应用在 tcpdump 上,tcpdump 会将过滤表达式转换成 BPF 字节码然后调用内核的接口让其执行这段字节码程序来过滤网络包。
# tcpdump -p -ni eth0 -d "ip and src 1.1.1.1"
(000) ldh [12]
(001) jeq #0x800 jt 2 jf 5
(002) ld [26]
(003) jeq #0x1010101 jt 4 jf 5
(004) ret #262144
(005) ret #0
这段程序的意思翻译过来如下:
(000)加载 ethernet packet 第 12 个字节开始的 2 个字节(ethernet frame 的类型字段)。
(001)检查加载的值是否为 0x800 (是否为 IP 包),是的话跳到 002 执行,否则 005。
(002)加载 ethernet packat 第 26 个字节开始的 4 个字节(IP 包的原地址字段)。
(003)检查加载的值是否为 0x1010101(IP 是否为 1.1.1.1),是的话跳到 004 执行,否则 005。
(004)返回包匹配。
(005)返回包不匹配。
上面打印出得是翻译过的给人看字节码,我们也可以打印出给机器读的字节码:
# tcpdump -p -ni eth0 -ddd "ip and src 1.1.1.1"
6
40 0 0 12
21 0 3 2048
32 0 0 26
21 0 1 16843009
6 0 0 262144
6 0 0 0
tcpdump 然后使用类似下面程序的方式将 bpf 程序提交给内核去执行,后面内核只会将匹配 bpf 程序的包发送给 tcpdump。
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <linux/if_ether.h>
/* BPF 字节码结构体
struct sock_filter {
__u16 code; 字节码
__u8 jt; 成功跳转
__u8 jf; 失败跳转
__u32 k; 根据字节码不同意义不一样
};
*/
struct sock_filter code[] = {
{40, 0, 0, 12},
{21, 0, 3, 2048},
{32, 0, 0, 26},
{21, 0, 1, 16843009},
{6, 0, 0, 262144},
{6, 0, 0, 0},
};
/* BPF 程序元信息
struct sock_fprog {
unsigned short len;
struct sock_filter __user *filter;
};
*/
struct sock_fprog bpf = {
.len = ARRAY_SIZE(code),
.filter = code,
};
sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
ret = setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf));
seccomp-bpf¶
目前 bpf 在 Linux 上已经应用在方方面面各种场景下, seccomp-bpf 就是 bpf 在 seccomp 上的应用。容器使用 seccomp-bpf + capability 来限制一个容器进程可以调用的系统调用。
#include <errno.h>
#include <linux/audit.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <linux/unistd.h>
#include <stddef.h>
#include <stdio.h>
#include <sys/prctl.h>
#include <unistd.h>
int main() {
printf("hey there!\n");
struct sock_filter filter[] = {
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, arch))),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, AUDIT_ARCH_X86_64, 0, 3),
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, nr))),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_write, 0, 1),
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
};
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
.filter = filter,
};
// 让 seccomp-bpf 程序生效后即使后面程序执行了 execve 也能继续生效
// https://www.kernel.org/doc/Documentation/prctl/no_new_privs.txt
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
perror("prctl(NO_NEW_PRIVS)");
return 1;
}
// 设置 seccomp-bpf
if (prctl(PR_SET_SECCOMP, 2, &prog)) {
perror("prctl(PR_SET_SECCOMP)");
return 1;
}
printf("it will not definitely print this here\n");
return 0;
}
上面的 bpf 程序使用各种宏来写的,翻译成 human readable 的格式就是:
(001) ldh seccomp_data.arch
(002) jeq AUDIT_ARCH_X86_64 jt 3 jf 6
(003) ldh seccomp_data.nr
(004) jeq __NR_write jt 5 jf 6
(005) ret SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)
(006) ret SECCOMP_RET_ALLOW
运行程序只会打印出第一个 hey there!
,最后 printf 的 it will not ...
打印不出来,因为 printf 最后调用系统调用 write 的时候会返回错误 EPERM。strace 可以看到结果如下:
write(1, "it will not definitely print thi"..., 39) = -1 EPERM (Operation not permitted)
参考: