4

字节vArmor客户端代码解读

 11 months ago
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.
neoserver,ios ssh client

之前分析了 vArmor-ebpf中的部分涉及思路,具体文章参考字节vArmor代码解读

本文主要是针对vArmor的客户端代码进行分析,对应的代码仓库是 vArmor

本文主要是分别从behaviorbpfenforcer以及规则实现进行简要分析。

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 eventshandleTraceEvents通过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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK