字节vArmor客户端代码解读
source link: https://blog.spoock.com/2023/08/26/eBPF-vArmor-client/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
之前分析了 vArmor-ebpf中的部分涉及思路,具体文章参考字节vArmor代码解读。
本文主要是针对vArmor
的客户端代码进行分析,对应的代码仓库是 vArmor
本文主要是分别从behavior
,bpfenforcer
以及规则实现进行简要分析。
bpfenforcer
bpfenforcer
主要是加载内核中的bpfenforcer eBPF
相关代码的.具体代码位于 enforcer.go
由于整个项目比较庞大,代码也比较多,所以这里只是简要分析一下其中加载eBPF
代码的逻辑.加载eBPF
的代码基本上都是在initBPF()
中实现.
loadBpf
loadBpf
函数用于解析eBPF
代码并将其解析为CollectionSpec
// loadBpf returns the embedded CollectionSpec for bpf.
func loadBpf() (*ebpf.CollectionSpec, error) {
reader := bytes.NewReader(_BpfBytes)
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
if err != nil {
return nil, fmt.Errorf("can't load bpf: %w", err)
}
return spec, err
}
AttachLSM
enforcer.log.Info("attach VarmorSocketConnect to the LSM hook point")
sockConnLink, err := link.AttachLSM(link.LSMOptions{
Program: enforcer.objs.VarmorSocketConnect,
})
if err != nil {
return err
}
enforcer.sockConnLink = sockConnLink
这段代码就是将VarmorSocketConnect
的程序附加到LSM钩子点,并将相关的链接保存在enforcer对象的sockConnLink字段中.其中enforcer.objs.VarmorSocketConnect
就是定义的ebpf:"varmor_socket_connect"
当执行AttachLSM()
方法,也就是将eBPF
程序加载到了内核中.
type bpfPrograms struct {
VarmorBprmCheckSecurity *ebpf.Program `ebpf:"varmor_bprm_check_security"`
VarmorCapable *ebpf.Program `ebpf:"varmor_capable"`
VarmorFileOpen *ebpf.Program `ebpf:"varmor_file_open"`
VarmorPathLink *ebpf.Program `ebpf:"varmor_path_link"`
VarmorPathLinkTail *ebpf.Program `ebpf:"varmor_path_link_tail"`
VarmorPathRename *ebpf.Program `ebpf:"varmor_path_rename"`
VarmorPathRenameTail *ebpf.Program `ebpf:"varmor_path_rename_tail"`
VarmorPathSymlink *ebpf.Program `ebpf:"varmor_path_symlink"`
VarmorSocketConnect *ebpf.Program `ebpf:"varmor_socket_connect"`
}
上面的代码就是通过github.com/cilium/ebpf
加载eBPF
程序的一个基本流程. 更多使用ebpf的例子也可以参考 examples.
netInnerMap
// Create a mock inner map for the network rules
netInnerMap := ebpf.MapSpec{
Name: "v_net_inner_",
Type: ebpf.Hash,
KeySize: 4,
ValueSize: 4*2 + 16*2,
MaxEntries: uint32(varmortypes.MaxBpfNetworkRuleCount),
}
collectionSpec.Maps["v_net_outer"].InnerMap = &netInnerMap
这个就是定义和netInnerMap
相关的代码,这个netInnerMap
是用于保存规则的,具体规则的定义在后面会分析。
tracer
接下来介绍有关tracer
客户端相关的代码,对应于内核态中的bpftracer
。
initBPF
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
spec, err := loadBpf()
if err != nil {
return err
}
return spec.LoadAndAssign(obj, opts)
}
func (tracer *Tracer) initBPF() error {
......
// Load pre-compiled programs and maps into the kernel.
tracer.log.Info("load bpf program and maps into the kernel")
if err := loadBpfObjects(&tracer.objs, nil); err != nil {
return fmt.Errorf("loadBpfObjects() failed: %v", err)
}
......
}
在initBPF()
函数中,关键的就是调用loadBpfObjects()
函数,将eBPF
程序加载到内核中。这个代码逻辑和bpfenforcer
中的loadBpf()
函数基本一致。
attachBpfToTracepoint
因为在加载eBPF
时需要具体指定对应的时间类型和eBPF
相关的代码段,所以这里需要先定义一个attachBpfToTracepoint
函数,用于将eBPF
代码段和对应的事件类型进行绑定。
func (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()
中通过如下类似代码:
execLink, 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
函数实现的。
type bpfEvent struct {
Type uint32
ParentPid uint32
ParentTgid uint32
ChildPid uint32
ChildTgid uint32
ParentTask [16]uint8
ChildTask [16]uint8
Filename [64]uint8
Env [256]uint8
Num uint32
}
func (tracer *Tracer) createBpfEventsReader() error {
reader, err := perf.NewReader(tracer.objs.Events, 8192*128)
if err != nil {
return err
}
tracer.reader = reader
return nil
}
func (tracer *Tracer) handleTraceEvents() {
var event bpfEvent
for {
record, err := tracer.reader.Read()
........
// Parse the perf event entry into a bpfEvent structure.
if err := binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, &event); err != nil {
tracer.log.Error(err, "parsing perf event failed")
continue
}
for _, eventCh := range tracer.bpfEventChs {
eventCh <- event
}
}
}
根据以上两个函数的定义和实现,基本上也可以知道这两个函数的作用。
createBpfEventsReader
用于创建一个events reader
对象,这个对象就是关联了perf events
。handleTraceEvents
通过tracer.reader.Read()
实时获取perf events
中的数据,然后通过binary.Read
将数据解析为bpfEvent
结构体,最后将解析后的数据通过eventCh
传递给其他的goroutine
。
通过以上的分析,对于整个eBPF
的加载逻辑和事件读取逻辑应该就比较清晰了。
首先,分析在内核态如何获取以及使用规则。还是以varmor_socket_connect
例子为例。具体代码例子位于 enforcer.c#L249
其中有关规则的代码是:
struct {
__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");
static u32 *get_net_inner_map(u32 mnt_ns) {
return bpf_map_lookup_elem(&v_net_outer, &mnt_ns);
}
SEC("lsm/socket_connect")
int BPF_PROG(varmor_socket_connect, struct socket *sock, struct sockaddr *address, int addrlen) {
.....
u32 mnt_ns = get_task_mnt_ns_id(current);
u32 *vnet_inner = get_net_inner_map(mnt_ns);
....
}
v_net_outer
是一个BPF_MAP_TYPE_HASH_OF_MAPS
类型的map
,用于保存规则信息。get_net_inner_map(mnt_ns)
通过namespace
信息得到对应得规则信息。
综合这两个部分的代码,可以知道v_net_outer
就是将namespace
作为key,对应的规则信息作为value保存在map
中。
接下来,查看规则匹配的逻辑:
struct 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);
}
#define NET_INNER_MAP_ENTRIES_MAX 50
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)
,得到对应的规则信息,然后进行匹配。规则信息的格式是:
struct 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
netInnerMap := ebpf.MapSpec{
Name: "v_net_inner_",
Type: ebpf.Hash,
KeySize: 4,
ValueSize: 4*2 + 16*2,
MaxEntries: uint32(varmortypes.MaxBpfNetworkRuleCount),
}
collectionSpec.Maps["v_net_outer"].InnerMap = &netInnerMap
在这段代码中,定义了v_net_outer
,这种类型就和内核代码中的如下定义相对应.
struct {
__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
中定义.
mapName := fmt.Sprintf("v_net_inner_%d", nsID)
innerMapSpec := ebpf.MapSpec{
Name: mapName,
Type: ebpf.Hash,
KeySize: 4,
ValueSize: 4*2 + 16*2,
MaxEntries: uint32(varmortypes.MaxBpfNetworkRuleCount),
}
innerMap, err := ebpf.NewMap(&innerMapSpec)
if err != nil {
return err
}
defer innerMap.Close()
和前面代码中的Name: "v_net_inner_",
对应.
前面定义了mapName := fmt.Sprintf("v_net_inner_%d", nsID)
,接下来就是定义规则,并将规则放入到v_net_inner_%d
中
for i, network := range bpfContent.Networks {
var rule bpfNetworkRule
rule.Flags = network.Flags
rule.Port = network.Port
ip := net.ParseIP(network.Address)
if ip.To4() != nil {
copy(rule.Address[:], ip.To4())
} else {
copy(rule.Address[:], ip.To16())
}
if network.CIDR != "" {
_, ipNet, err := net.ParseCIDR(network.CIDR)
if err != nil {
return err
}
copy(rule.Mask[:], ipNet.Mask)
}
var index uint32 = uint32(i)
err = innerMap.Put(&index, &rule)
if err != nil {
return err
}
}
这段代码主要逻辑就是解释规则,然后将规则放入到v_net_inner_%d
中.其中最关键的两行代码是:
var index uint32 = uint32(i)
err = innerMap.Put(&index, &rule)
和内核态中的struct net_rule *rule = get_net_rule(vnet_inner, inner_id);
对应.
内核态中的net_rule
定义是:
struct net_rule {
u32 flags;
unsigned char address[16];
unsigned char mask[16];
u32 port;
};
用户态中的bpfNetworkRule
定义是:
type bpfNetworkRule struct {
Flags uint32
Address [16]byte
Mask [16]byte
Port uint32
}
两者的数据结构也是完全一致的.
V_netOuter
最后关键的代码是:
err = 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
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK