说明
之前分析了 vArmor-ebpf中的部分涉及思路,具体文章参考字节vArmor代码解读。
本文主要是针对vArmor
的客户端代码进行分析,对应的代码仓库是 vArmor
本文主要是分别从behavior
,bpfenforcer
以及规则实现进行简要分析。
bpfenforcer
bpfenforcer
主要是加载内核中的bpfenforcer eBPF
相关代码的.具体代码位于 enforcer.go
由于整个项目比较庞大,代码也比较多,所以这里只是简要分析一下其中加载eBPF
代码的逻辑.加载eBPF
的代码基本上都是在initBPF()
中实现.
loadBpf
loadBpf
函数用于解析eBPF
代码并将其解析为CollectionSpec
1 | // loadBpf returns the embedded CollectionSpec for bpf. |
AttachLSM
1 | enforcer.log.Info("attach VarmorSocketConnect to the LSM hook point") |
这段代码就是将VarmorSocketConnect
的程序附加到LSM钩子点,并将相关的链接保存在enforcer对象的sockConnLink字段中.其中enforcer.objs.VarmorSocketConnect
就是定义的ebpf:"varmor_socket_connect"
当执行AttachLSM()
方法,也就是将eBPF
程序加载到了内核中.
1 | type bpfPrograms struct { |
上面的代码就是通过github.com/cilium/ebpf
加载eBPF
程序的一个基本流程. 更多使用ebpf的例子也可以参考 examples.
netInnerMap
1 | // Create a mock inner map for the network rules |
这个就是定义和netInnerMap
相关的代码,这个netInnerMap
是用于保存规则的,具体规则的定义在后面会分析。
tracer
接下来介绍有关tracer
客户端相关的代码,对应于内核态中的bpftracer
。
initBPF
1 | // See ebpf.CollectionSpec.LoadAndAssign documentation for details. |
在initBPF()
函数中,关键的就是调用loadBpfObjects()
函数,将eBPF
程序加载到内核中。这个代码逻辑和bpfenforcer
中的loadBpf()
函数基本一致。
attachBpfToTracepoint
因为在加载eBPF
时需要具体指定对应的时间类型和eBPF
相关的代码段,所以这里需要先定义一个attachBpfToTracepoint
函数,用于将eBPF
代码段和对应的事件类型进行绑定。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21func (tracer *Tracer) attachBpfToTracepoint() error {
execLink, err := link.AttachRawTracepoint(link.RawTracepointOptions{
Name: "sched_process_exec",
Program: tracer.objs.TracepointSchedSchedProcessExec,
})
if err != nil {
return err
}
tracer.execLink = execLink
forkLink, err := link.AttachRawTracepoint(link.RawTracepointOptions{
Name: "sched_process_fork",
Program: tracer.objs.TracepointSchedSchedProcessFork,
})
if err != nil {
return err
}
tracer.forkLink = forkLink
return nil
}
在代码中的tracer.objs
变量就是前面通过initBPF()
函数加载到内核中的eBPF
代码段。在attachBpfToTracepoint()
中通过如下类似代码:1
2
3
4
5
6
7
8execLink, err := link.AttachRawTracepoint(link.RawTracepointOptions{
Name: "sched_process_exec",
Program: tracer.objs.TracepointSchedSchedProcessExec,
})
if err != nil {
return err
}
tracer.execLink = execLink
将内核代码和用户代码相互关联,这样就完成了eBPF
代码的加载。
EventsReader
在加载了eBPF
相关程序之后,接下来就是读取eBPF
程序中的事件。这个过程是通过EventsReader
函数实现的。
1 |
|
根据以上两个函数的定义和实现,基本上也可以知道这两个函数的作用。
createBpfEventsReader
用于创建一个events reader
对象,这个对象就是关联了perf events
。handleTraceEvents
通过tracer.reader.Read()
实时获取perf events
中的数据,然后通过binary.Read
将数据解析为bpfEvent
结构体,最后将解析后的数据通过eventCh
传递给其他的goroutine
。
通过以上的分析,对于整个eBPF
的加载逻辑和事件读取逻辑应该就比较清晰了。
规则更新
内核代码
首先,分析在内核态如何获取以及使用规则。还是以varmor_socket_connect
例子为例。具体代码例子位于 enforcer.c#L249
其中有关规则的代码是:
1 | struct { |
v_net_outer
是一个BPF_MAP_TYPE_HASH_OF_MAPS
类型的map
,用于保存规则信息。get_net_inner_map(mnt_ns)
通过namespace
信息得到对应得规则信息。
综合这两个部分的代码,可以知道v_net_outer
就是将namespace
作为key,对应的规则信息作为value保存在map
中。
接下来,查看规则匹配的逻辑:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21struct net_rule {
u32 flags;
unsigned char address[16];
unsigned char mask[16];
u32 port;
};
static struct net_rule *get_net_rule(u32 *vnet_inner, u32 rule_id) {
return bpf_map_lookup_elem(vnet_inner, &rule_id);
}
for(inner_id=0; inner_id<NET_INNER_MAP_ENTRIES_MAX; inner_id++) {
// The key of the inner map must start from 0
struct net_rule *rule = get_net_rule(vnet_inner, inner_id);
if (rule == NULL) {
DEBUG_PRINT("");
DEBUG_PRINT("access allowed");
return 0;
}
}
通过get_net_rule(vnet_inner, inner_id)
,得到对应的规则信息,然后进行匹配。规则信息的格式是:1
2
3
4
5
6struct net_rule {
u32 flags;
unsigned char address[16];
unsigned char mask[16];
u32 port;
};
因为后面的匹配逻辑比较简单,所以这里就不再分析了。
用户态代码
既然知道了在内核中是如何是用规则的,那么接下来就是看如何在用户端设置规则。
v_net_outer
既然知道规则是通过v_net_outer
这种map类型传输的,同样看bpfenforcer
中有关v_net_outer
相关的代码.
代码文件:pkg/lsm/bpfenforcer/enforcer.go
1 | netInnerMap := ebpf.MapSpec{ |
在这段代码中,定义了v_net_outer
,这种类型就和内核代码中的如下定义相对应.1
2
3
4
5
6struct {
__uint(type, BPF_MAP_TYPE_HASH_OF_MAPS);
__uint(max_entries, OUTER_MAP_ENTRIES_MAX);
__type(key, u32);
__type(value, u32);
} v_net_outer SEC(".maps");
v_net_inner
有关规则的定义,则是在文件pkg/lsm/bpfenforcer/profile.go
中定义.
1 | mapName := fmt.Sprintf("v_net_inner_%d", nsID) |
和前面代码中的Name: "v_net_inner_",
对应.
rule
前面定义了mapName := fmt.Sprintf("v_net_inner_%d", nsID)
,接下来就是定义规则,并将规则放入到v_net_inner_%d
中
1 | for i, network := range bpfContent.Networks { |
这段代码主要逻辑就是解释规则,然后将规则放入到v_net_inner_%d
中.其中最关键的两行代码是:1
2var index uint32 = uint32(i)
err = innerMap.Put(&index, &rule)
和内核态中的struct net_rule *rule = get_net_rule(vnet_inner, inner_id);
对应.
内核态中的net_rule
定义是:1
2
3
4
5
6struct net_rule {
u32 flags;
unsigned char address[16];
unsigned char mask[16];
u32 port;
};
用户态中的bpfNetworkRule
定义是:1
2
3
4
5
6type bpfNetworkRule struct {
Flags uint32
Address [16]byte
Mask [16]byte
Port uint32
}
两者的数据结构也是完全一致的.
V_netOuter
最后关键的代码是:1
2
3
4err = enforcer.objs.V_netOuter.Put(&nsID, innerMap)
if err != nil {
return err
}
将v_net_inner_%d
放入到v_net_outer
中,这样就完成了规则的设置.其中nsID
作为v_net_outer
的key,v_net_inner_%d
作为v_net_outer
的value.
这个代码和内核中的u32 *vnet_inner = get_net_inner_map(mnt_ns)
也是对应的.
总结
整体来说,VArmor
整体代码逻辑十分清晰,对于想了解和学习eBPF
开发相关的人来说,是一个很好的学习资料。同时由于VArmor
的代码量比较大,本文也仅仅只是分析了其中的eBPF
的加载机制部分。整个代码还有更多的设计和考虑,可以参考对应的PPT,从0到1打造云原生容器沙箱vArmor
后续有机会,也会对vArmor
的其他部分进行分析。
参考
https://github.com/bytedance/vArmor
从0到1打造云原生容器沙箱vArmor