说明之前分析了 vArmor-ebpf 中的部分涉及思路,具体文章参考字节vArmor代码解读 。
本文主要是针对vArmor
的客户端代码进行分析,对应的代码仓库是 vArmor
本文主要是分别从behavior
,bpfenforcer
以及规则实现进行简要分析。
bpfenforcerbpfenforcer
主要是加载内核中的bpfenforcer eBPF
相关代码的.具体代码位于 enforcer.go
由于整个项目比较庞大,代码也比较多,所以这里只是简要分析一下其中加载eBPF
代码的逻辑.加载eBPF
的代码基本上都是在initBPF()
中实现.
loadBpfloadBpf
函数用于解析eBPF
代码并将其解析为CollectionSpec
1 2 3 4 5 6 7 8 9 10 // 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 }
AttachLSM1 2 3 4 5 6 7 8 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
程序加载到了内核中.
1 2 3 4 5 6 7 8 9 10 11 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 .
netInnerMap1 2 3 4 5 6 7 8 9 // 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
。
initBPF1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // 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
代码段和对应的事件类型进行绑定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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()
中通过如下类似代码:
1 2 3 4 5 6 7 8 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
函数实现的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 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
其中有关规则的代码是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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
中。
接下来,查看规则匹配的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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)
,得到对应的规则信息,然后进行匹配。规则信息的格式是:
1 2 3 4 5 6 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
1 2 3 4 5 6 7 8 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
,这种类型就和内核代码中的如下定义相对应.
1 2 3 4 5 6 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
中定义.
1 2 3 4 5 6 7 8 9 10 11 12 13 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_",
对应.
rule前面定义了mapName := fmt.Sprintf("v_net_inner_%d", nsID)
,接下来就是定义规则,并将规则放入到v_net_inner_%d
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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
中.其中最关键的两行代码是:
1 2 var 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 6 struct net_rule { u32 flags; unsigned char address[16 ]; unsigned char mask[16 ]; u32 port; };
用户态中的bpfNetworkRule
定义是:
1 2 3 4 5 6 type bpfNetworkRule struct { Flags uint32 Address [16 ]byte Mask [16 ]byte Port uint32 }
两者的数据结构也是完全一致的.
V_netOuter最后关键的代码是:
1 2 3 4 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