查看原文
其他

Cilium eBPF实现机制源码分析

CFC4N 榫卯江湖 2022-08-20


目的

本文面向eBPF开发者,旨在研究学习高质量开源产品设计思路、编码规范,学习更好地使用eBPF方法经验。内容比较干燥,谨慎阅读。

本文涉及cilium代码版本为2021-08-17的1695d9c59a版本

Cilium产品介绍

Cilium是由革命性内核技术eBPF驱动,用于提供、保护和观察容器工作负载(云原生)之间的网络连接的网络组件。
Cilium使用eBPF的强大功能来加速网络,并在Kubernetes中提供安全性和可观测性。现在 Cilium将eBPF的优势带到了Service Mesh的世界。Cilium服务网格使用eBPF来管理连接,实现了服务网格必备的流量管理、安全性和可观测性。

项目理解

从Cilium的架构图来看,位于容器编排系统和Linux Kernel之间,使用eBPF技术来控制容器网络的转发行为以及安全策略执行。其使用的eBPF功能包括宿主机网卡流量控制,容器container功能管理等。与系统交互的模块是Cilium Daemon,负责eBPF字节码生成、字节码注入到linux kernel,并进行数据读取等。


所以,本文重点将放在Cilium Daemon实现上。

Cilium Daemon模块在源码里对应https://github.com/cilium/cilium/tree/master/daemon目录,从main.go主文件开始阅读即可。但在阅读之前,先对cilium项目的目录结构做一个认识。

目录结构:

当前目标是理解分析cilium的eBPF应用,故笔者在总结时,只列出eBPF相关代码。

按照cilium官方的文档介绍github.com/cilium/cilium,从目录结构中,摘录了eBPF相关的目录。

顶级目录

  1. bpf : eBPF datapath收发包路径相关代码,eBPF源码存放目录。

  2. daemon : 各node节点上运行的cilium-agent代码,也是跟内核做交互,处理eBPF相关的核心代码。

  3. pkg : 项目依赖的各种包。

    1. pkg/bpf :eBPF运行时交互的抽象层

    2. pkg/datapath datapath交互的抽象层

    3. pkg/maps eBPF map的描述定义目录

    4. pkg/monitor eBPF datapath 监控器抽象

源码阅读

cilium C语言eBPF源码

源码功能分类

bpf目录下有很多eBPF实现的源码,文件列表如下

  1. bpf_alignchecker.c C与Go的消息结构体格式校验

  2. bpf_host.c 物理层的网卡tc ingress\egress相关过滤器

  3. bpf_lxc.c 容器上的网络环境、网络流量管控等

  4. bpf_network.c 网络控制相关

  5. bpf_overlay.c 叠加网络控制代码

  6. bpf_sock.c sock控制相关,包含流量大小控制、TCP状态变化控制

  7. bpf_xdp.c XDP层控制相关

  8. sockops 目录下有多个文件,用于sockops相关控制,流量重定向等性能优化。

  9. cilium-probe-kernel-hz.c probe测试的,忽略

作用解释

关于上面eBPF文件源码的作用,在官网也有文档eBPF Datapath Introduction解释,笔者稍做整理。

在详细阐述每个模块源码之前,我们先来复习一下linux kernel的网络栈


如图所示,入口网络流量在到达NIC后,依次经过XDP、TC、Netfilter、TCP、socket层。
cilium的ebpf相关程序,核心功能是针对pod宿主机、pod、容器几个角色之间的网络流量管控。那么其功能肯定是在这几个栈的对应部位做响应hook。

常用的nftables、iptables模块对应在Netfiliter层,站在安全的视角里,HOOK点如何选择,选在那一层,需要结合安全需求来确定。

XDP

XDP BPF hook 是网络驱动程序中最早的一关,可以在接收到数据包时触发BPF程序。在这里实现了最高效的数据包处理。这个HOOK非常适合运行过滤程序来处理恶意或意外流量,以及实现常见的 DDOS保护机制。

Traffic Control Ingress/Egress

显然,是附加到流量控制 (TC) Ingress HOOK的BPF程序,与XDP类似,区别是其在网络堆栈完成后,数据包初始处理后运行。此HOOK在内核整个网络堆栈的L3层之前运行,可以读取与数据包关联的大部分元数据。很适合进行本地节点处理,比如应用L3/L4端点策略进行流量重定向等。

容器场景常使用veth pair的虚拟设备,它充当容器与主机的虚拟网桥。通过attach到这个veth pair的宿主机的TC Ingress钩子,Cilium 可以监控管理容器的所有流量。

socket operations

sockops Hook是attach到特定的cgroup上,在TCP事件上一并运行。Cilium的实现是把BPF sockops程序attach到根cgroup上,来监视TCP状态转换,进行相关业务处理。

socket 发送/接收

该钩子在TCP socket执行的每个发送操作上运行。这个钩子可以对消息进行读取、删除、重定向到另一个socket。(PS:笔者说一句,这个就很可怕,在安全场景里,这可以很简单地实现端口复用的后门程序。复用80端口,监听unix socket做木马后门,HIDS如何发现?

cilium的eBPF场景应用

Cilium使用上面几个Hook与几个接口功能相结合,创建了以下几个网络对象。

  1. 虚拟接口(cilium_host、cilium_net)

  2. 可选接口(cilium_vxlan)

  3. linux内核加密支持

  4. 用户空间代理(Envoy)

  5. eBPF Hooks

预过滤器 prefilter

XDP层实现的网络流量过滤过滤器规则。比如,由Cilium agent提供的一组CIDR映射用于查找定位、处理丢弃等。

endpoint策略

Cilium endpoint来继承实现。使用映射查找与身份和策略相关的数据包,该层可以很好地扩展到许多端点。根据策略,该层可能会丢弃数据包、转发到本地端点、转发到服务对象或转发到 L7 策略对象以获取进一步的L7规则。这是Cilium数据路径中的主要对象,负责将数据包映射到身份并执行L3和L4策略。

Service

TC栈上的HOOK,用于L3/L4层的网络负载均衡功能。

L3 加密器

L3层处理IPsec头的流量加密解密等。

Socket Layer Enforcement

socket层的两个钩子,即sockops hook和socket send/recv hook。用来监视管理Cilium endpoint关联的所有TCP套接字,包括任何L7代理。

L7 策略

L7策略对象将代理流量重定向到Cilium用户空间代理实例。使用Envoy实例作为其用户空间代理。然后,根据配置的L7策略转发流量。

如上组件是Cilium实现的灵活高效的 datapath。下图展示端点到端点的进出口网络流量经过的链路,以及涉及的cilium相关网络对象。

总结

综合C的代码,从数据流向来看,分为两类

  1. 用户态向内核态发送控制指令、数据

  2. 内核态向用户态发送数据

第一部分,cilium调用类bpftool工具来进行eBPF字节码注入。(具体实现的方式,go代码分析时会讲到);LB部分,会直接向map写入数据内容。(lb.h)
第二部分是内核向用户态发送数据,而数据内容几乎都是其他eBPF的运行日志。尤其是dbg.h里定义的cilium_dbg* 方法,实现了skb_event_output()xdp_event_output()两种函数输出,来代替trace_printk()函数,方便用户快速读取日志。两种函数对应的事件输出都是用了perf buf类型的map来实现,对应go代码里做了详细的实现,抽象的非常好,后面笔者会重点介绍。

cilium go源码分析

eBPF map初始化

上面提到Cilium Daemon是管理eBPF的模块,那么从这个模块的入口文件开始阅读。
ebpf map是在ebpf prog加载之前,预先初始化的,在daemon/cmd/daemon.go469行

  1. err = d.initMaps()

initMaps函数实现在daemon/cmd/datapath.go文件的272行。

  1. // initMaps opens all BPF maps (and creates them if they do not exist). This

  2. // must be done *before* any operations which read BPF maps, especially

  3. // restoring endpoints and services.

  4. func (d *Daemon) initMaps() error {

  5. lxcmap.LXCMap.OpenOrCreate()

  6. ipcachemap.IPCache.OpenParallel()

  7. metricsmap.Metrics.OpenOrCreate()

  8. tunnel.TunnelMap.OpenOrCreate()

  9. egressmap.EgressMap.OpenOrCreate()

  10. eventsmap.InitMap(possibleCPUs)

  11. signalmap.InitMap(possibleCPUs)

  12. policymap.InitCallMap()

  13. }

initMaps函数中初始化了cilium的所有eBPF map,功能包括xdp、ct等网络对象处理。
eBPF maps作用博主rexrock在文章 https://rexrock.github.io/post/cilium2/中做个直观的图,见

本文挑选其中一个例子来讲。就是前提提到的events maps初始化,用于内核的ebpf字节码调试输出的日志,对应代码eventsmap.InitMap(possibleCPUs)。代码文件在pkg/map/eventsmap/eventsmap.go的53行

  1. eventsMap := bpf.NewMap(MapName,

  2. bpf.MapTypePerfEventArray,

  3. &Key{},

  4. int(unsafe.Sizeof(Key{})),

  5. &Value{},

  6. int(unsafe.Sizeof(Value{})),

  7. MaxEntries,

  8. 0,

  9. 0,

  10. bpf.ConvertKeyValue,

  11. )

从代码中可以看到,map名字是cilium_events,类型是MapTypePerfEventArray

eBPF代码编译

回到daemon/cmd/daemon.go文件,接着往下看,在816行对eBPF应用场景进行初始化。

  1. err = d.init()


  2. //跳转到285行的init函数

  3. // Remove any old sockops and re-enable with _new_ programs if flag is set

  4. sockops.SockmapDisable()

  5. sockops.SkmsgDisable()

对新老map进行删除、替换。

在237行进行datapath的重新初始化加载。

  1. if err := d.Datapath().Loader().Reinitialize(d.ctx, d, d.mtuConfig.GetDeviceMTU(), d.Datapath(), d.l7Proxy); err != nil {

datapath初始化ebpf环境

Reinitialize函数是抽象的interface的函数,具体实现在pkg/datapath/loader/base.go的230行
该函数前半部分对启动参数进行整理汇总。核心逻辑在421行。

  1. prog := filepath.Join(option.Config.BpfDir, "init.sh")

  2. cmd := exec.CommandContext(ctx, prog, args...)

  3. cmd.Env = bpf.Environment()

  4. if _, err := cmd.CombinedOutput(log, true); err != nil {

  5. return err

  6. }

是的,你没看错,调用了外部的shell命令进行ebpf代码编译。对应文件是bpf/init.sh,这个shell里会进行编译ebpf文件。

比如:bpf_compile bpf_alignchecker.c bpf_alignchecker.o obj "" ,生成eBPF字节码.o文件。后面将用于校验C跟GO的结构体对齐情况。
bpf_compile也是封装的clang的编译函数,依旧使用llvm\llc编译链接eBPF字节码文件。

eBPF字节码加载

同样bpf/init.sh也会对bpf/*.c进行编译,再调用tc等命令,对编译生成的eBPF字节码进行加载。

其次,go代码里也有加载的地方,见pkg/datapath/loader/netlink.goreplaceDatapath函数内91行使用ip 或tc 命令对字节码文件进行加载,使内核加载新的字节码。完成新老字节码的注入替换。

C跟go结构体格式校验

430行,使用go代码,验证C跟G结构体对齐情况。

  1. alignchecker.CheckStructAlignments(defaults.AlignCheckerName)

在pkg/alignchecker/alignchecker.go里,CheckStructAlignments函数会读取.o的eBPF字节码文件,按照elf格式进行解析,并获取DWARF段信息,查找.debug_*段或者.zdebug_段信息。
getStructInfosFromDWARF函数会按照elf里段内结构体名字与被检测结构体名字进行对比,验证类型,长度等等。

ebpf编译加载的其他方式

pkg/datapath/loader/base.go210行左右reinitializeXDPLocked函数
调用compileAndLoadXDPProg函数进行ebpf字节码编译与加载。

  1. // compileAndLoadXDPProg compiles bpf_xdp.c for the given XDP device and loads it.

  2. func compileAndLoadXDPProg(ctx context.Context, xdpDev, xdpMode string, extraCArgs []string) error {

  3. args, err := xdpCompileArgs(xdpDev, extraCArgs)

  4. if err != nil {

  5. return fmt.Errorf("failed to derive XDP compile extra args: %w", err)

  6. }


  7. if err := compile(ctx, prog, dirs); err != nil {

  8. return err

  9. }

  10. if err := ctx.Err(); err != nil {

  11. return err

  12. }


  13. objPath := path.Join(dirs.Output, prog.Output)

  14. return replaceDatapath(ctx, xdpDev, objPath, symbolFromHostNetdevEp, "", true, xdpMode)

  15. }

函数中,先进行参数重组,在调用pkg/datapath/loader/compile.go的compile函数进行编译。该函数依旧是调用了clang进行编译。

其他代码细节不在赘述。

go源码分析总结

  1. 编译:直接或间接调用clang/llc命令进行编译链接。

  2. 加载:调用外部bpftool\tc\ss\ip等命令加载。

  3. MAP管理:调用外部命令或go cilium/ebpf库进行map删除、创建等

  4. CORE兼容:会在每个endpoint上编译,没有使用eBPF CORE。

  5. 更新:每次重新加载都会编译。

内核态与用户态数据交互

交互map

名字类型所属文件数据流向备注
SIGNAL_MAPBPF_MAP_TYPE_PERF_EVENT_ARRAYsignal.h
LB4_REVERSE_NAT_SK_MAPBPF_MAP_TYPE_LRU_HASHbpf_sock.c?
LB6_REVERSE_NAT_SK_MAPBPF_MAP_TYPE_LRU_HASHbpf_sock.c?
CIDR4_HMAP_NAMEBPF_MAP_TYPE_HASHbpf_xdp.c?
CIDR4_LMAP_NAMEBPF_MAP_TYPE_LPM_TRIEbpf_xdp.c

CIDR6_HMAP_NAMEBPF_MAP_TYPE_HASHbpf_xdp.c

CIDR6_LMAP_NAMEBPF_MAP_TYPE_LPM_TRIEbpf_xdp.c

bytecount_mapBPF_MAP_TYPE_HASHbytecount.h

cilium_xdp_scratchBPF_MAP_TYPE_PERCPU_ARRAYxdp.h

EVENTS_MAPBPF_MAP_TYPE_PERF_EVENT_ARRAYevent.h

IPV4_FRAG_DATAGRAMS_MAPBPF_MAP_TYPE_LRU_HASHipv4.h

LB6_REVERSE_NAT_MAPBPF_MAP_TYPE_HASHlb.h

LB6_SERVICES_MAP_V2BPF_MAP_TYPE_HASHlb.h

ENDPOINTS_MAPBPF_MAP_TYPE_HASHmaps.h

METRICS_MAPBPF_MAP_TYPE_PERCPU_HASHmaps.h

POLICY_CALL_MAPBPF_MAP_TYPE_PROG_ARRAY


THROTTLE_MAPBPF_MAP_TYPE_HASH


EP_POLICY_MAPBPF_MAP_TYPE_HASH_OF_MAPSmaps.h?Map to link endpoint id to per endpoint cilium_policy map
POLICY_MAPBPF_MAP_TYPE_HASHmaps.h?Per-endpoint policy enforcement map
EVENTS_MAPBPF_MAP_TYPE_SOCKHASHbpf_sockops.h?
太多了,而且比较偏向cilium的业务功能,偏离本文主题,不写了。后面会按照数据流向分三类,总结说明。



map作用分类

内核态自用

常用与程序内部的临时缓存。比如__section("cgroup/connect4")时,TCP socket的状态每次变化,都需要将之前endpoint信息存储起来,下次状态变化时,再读取更改。举个例子🌰

  1. //bpf/sockops/bpf_sockops.c line 127

  2. __section("sockops")

  3. int bpf_sockmap(struct bpf_sock_ops *skops)

  4. {

  5. // 调用bpf_sock_ops_ipv4 函数

  6. sock_hash_update(skops, &SOCK_OPS_MAP, &key, BPF_NOEXIST);

  7. }

  8. //bpf/sockops/bpf_redir.c line 42

  9. __section("sk_msg")

  10. int bpf_redir_proxy(struct sk_msg_md *msg)

  11. {

  12. msg_redirect_hash(msg, &SOCK_OPS_MAP, &key, flags);

  13. }

内核态写,用户态读

有个典型的场景,就是eBPF字节码运行日志的输出。以cilium events map为例,该map是内核态代码的日志输出map。

EVENTS_MAP map创建

  1. MapName = "cilium_events" //eventsmap.go line 19

  2. eventsMap := bpf.NewMap(MapName,

  3. bpf.MapTypePerfEventArray,

  4. &Key{},

  5. int(unsafe.Sizeof(Key{})),

  6. &Value{},

  7. int(unsafe.Sizeof(Value{})),

  8. MaxEntries,

  9. 0,

  10. 0,

  11. bpf.ConvertKeyValue,

  12. )

map的路径会被拼接,最终全路径时/sys/fs/bpf/tc/globals/cilium_events

  1. // Path to where bpffs is mounted , /sys/fs/bpf

  2. mapRoot = defaults.DefaultMapRoot


  3. // Prefix for all maps (default: tc/globals)

  4. mapPrefix = defaults.DefaultMapPrefix

  5. m.path = filepath.Join(mapRoot, mapPrefix, name)

  6. // 即 /sys/fs/bpf/tc/globals/cilium_events

拼接好map路径后,调用os.MkdirAll创建/sys/fs/bpf/tc/globals/cilium_events目录;调用CreateMap函数,使用unix.Syacall创建BPF_MAP_CREATE操作的FD;

  1. ret, _, err := unix.Syscall(

  2. unix.SYS_BPF,

  3. BPF_MAP_CREATE,

  4. uintptr(unsafe.Pointer(&uba)),

  5. unsafe.Sizeof(uba),

  6. )

调用objPin对map ID和cgroup path绑定,保存到pkg/bpf/map_Register_linx.gomapRegister Map里,完成整个map的创建、关联。

map数据写入

map内数据写入是由dbg.hcilium_dbg*相关函数写入,代码参见

  1. static __always_inline void cilium_dbg(struct __ctx_buff *ctx, __u8 type, __u32 arg1, __u32 arg2)

  2. {

  3. struct debug_msg msg = {

  4. __notify_common_hdr(CILIUM_NOTIFY_DBG_MSG, type),

  5. .arg1 = arg1,

  6. .arg2 = arg2,

  7. };


  8. ctx_event_output(ctx, &EVENTS_MAP, BPF_F_CURRENT_CPU,

  9. &msg, sizeof(msg));

  10. }

其中,写入的map名字是EVENTS_MAP常量,定义在bpf/node_config.h里,默认是test_cilium_events,需要总控远程下发这个头文件。方便由go这边统一控制map的名字。详细代码在pkg/datapath/linux/config/config.go里WriteNodeConfig函数部分。比如

  1. cDefinesMap["EVENTS_MAP"] = eventsmap.MapName

  2. cDefinesMap["SIGNAL_MAP"] = signalmap.MapName

  3. cDefinesMap["POLICY_CALL_MAP"] = policymap.PolicyCallMapName

  4. cDefinesMap["EP_POLICY_MAP"] = eppolicymap.MapName

  5. cDefinesMap["LB6_REVERSE_NAT_MAP"] = "cilium_lb6_reverse_nat"

  6. cDefinesMap["LB6_SERVICES_MAP_V2"] = "cilium_lb6_services_v2"

回到cilium_dbg函数,是内核eBPF部分最底层的日志事件输出函数,map声明在bpf/lib/events.h

  1. struct bpf_elf_map __section_maps EVENTS_MAP = {

  2. .type = BPF_MAP_TYPE_PERF_EVENT_ARRAY,

  3. .size_key = sizeof(__u32),

  4. .size_value = sizeof(__u32),

  5. .pinning = PIN_GLOBAL_NS,

  6. .max_elem = __NR_CPUS__,

  7. };

调用的地方比较简单,不在一一赘述。

小结

map名统一由go部分控制,起到统一管理作用,避免两端不一致。

事件日志/调试日志避开trace_printk函数输出,统一发送至用户态go部分,避免人工查看/sys/kernel/debug/tracing/trace_pipe,提升工作效率。

由go部分决策如何处理。比如发送给相关模块订阅的角色,或者统一上传到日志中心,便于大规模分析展示。这是个可以借鉴的好思路。

map数据读取

在monitor/agent/agent.go里,初始化时对map进行了pin操作。

笔者要吐槽的是cilium_events map名字的常量是eventsMapName,这跟创建map时用的pkg/maps/eventsmap/eventsmap.go下的MapName不是同一个,而是重新定义一个。影响代码分析。

  1. path := oldBPF.MapPath(eventsMapName)

  2. eventsMap, err := ebpf.LoadPinnedMap(path, nil)

在handleEvents函数中进行事件读取,并对异常错误进行计数,用作数据完整性校对。(笔者还没细跟进)

  1. func (a *Agent) processPerfRecord(scopedLog *logrus.Entry, record perf.Record) {

  2. a.Lock()

  3. defer a.Unlock()


  4. if record.LostSamples > 0 {

  5. // 丢失数据大小统计

  6. a.MonitorStatus.Lost += int64(record.LostSamples)

  7. // 通知所有内部消费者,告诉他们数据丢失部分大小

  8. a.notifyPerfEventLostLocked(record.LostSamples, record.CPU)


  9. // 存入外部订阅者队列,在队列的消费处,发送给所有监听者

  10. a.sendToListenersLocked(&payload.Payload{

  11. CPU: record.CPU,

  12. Lost: record.LostSamples,

  13. Type: payload.RecordLost,

  14. })


  15. }

  16. // ...

  17. }

这里的事件发送分为两种接受者

  1. monitor进程内部的消费者,抽象为consumer.MonitorConsumer,比如数据丢失监控、事件处理dispather派发器等。对应consumers属性,使用RegisterNewConsumer函数来注册为消费者。

  2. monitor进程外部的订阅者,抽象为listener.MonitorListener,比如与其交互的外部进程,远程数据数据库、中心事件处理总控等。对应newListener属性,使用RegisterNewListener函数注册,目前只支持自定义的Version1_2,方便以后扩展。

不管是consumer还是listener,都是一对多的关系,遍历多个consumers\listener进行发送。

cilium的配套可视化组件Hubble就是作为其中一个consumer来接收数据的。

对于事件的进程外部发送,cilium采用本地unix socket的方式,监听/var/run/cilium/monitor1_2.sock,来支持本机进程间数据通讯。

  1. func ServeMonitorAPI(monitor *Agent) error {

  2. listener, err := buildServer(defaults.MonitorSockPath1_2)

  3. if err != nil {

  4. return err

  5. }


  6. s := &server{

  7. listener: listener,

  8. monitor: monitor,

  9. }


  10. log.Infof("Serving cilium node monitor v1.2 API at unix://%s", defaults.MonitorSockPath1_2)


  11. go s.connectionHandler1_2(monitor.Context())


  12. return nil

  13. }

小结

对于内核态程序、稳定性质量做监控,结合内核态数据对服务更好的掌控。
事件的派发角色需要结合业务,进程内消费角色如何划分(对账、解码),进程间消费角色如何设计,多版本升级,通许协议如何设计等。
cilium在代码层面,角色功能上做了非常好的抽象,扩展性比较好。比如MonitorListener接口设计时,只规范了Enqueue(pl *payload.Payload)Version() VersionClose()三个方法,实现的时候,可以随意扩展。

用户态写,内核态读

以XDP层的IP过滤为例,对应map path :cilium_cidr_v4_dyn,来给大家讲一下这个场景。

事件触发是由HTTP接口接收控制指令触发的,在daemon/cmd/prefilter.go的55行附近,patchPrefilter.Handle函数接收HTTP request,读取策略文件中的CIDRs,准备调用preFilter.Insert写入到eBPF Maps中。

reFilter.Insert是接口函数,抽象的实现在pkg/datapath/prefilter/prefilter.go119行Insert函数中实现。

打开

CIDRs写入到eBPF maps里之前,先进行map选择

  1. for _, cidr := range cidrs {

  2. ones, bits := cidr.Mask.Size()

  3. which := p.selectMap(ones, bits)

  4. if which == mapCount || p.maps[which] == nil {

  5. ret = fmt.Errorf("No map enabled for CIDR string %s", cidr.String())

  6. break

  7. }

  8. err := p.maps[which].InsertCIDR(cidr)

  9. if err != nil {

  10. ret = fmt.Errorf("Error inserting CIDR string %s: %s", cidr.String(), err)

  11. break

  12. } else {

  13. undoQueue = append(undoQueue, cidr)

  14. }

  15. }

循环遍历CIDRs,每个IP都判断是IPv4还是IPv6,选择对应的map,准备写入。写入的map在PreFilter.initOneMap函数里做了初始化读取。先判断IP的类型prefixesV4DynprefixesV4FixprefixesV6DynprefixesV6Fix,再调用pkg/maps/cidrmap/cidrmap.go中147行cidrmap.OpenMapElems函数打开当前map。

打开map时,会先尝试创建bpf.MapTypeLPMTrie类型(也就是BPF_MAP_TYPE_LPM_TRIE类型)的map,若不支持,则改为MapTypeHash类型,来兼容低版本内核的linux。

写入

PreFilter.Insert调用CIDRMap.InsertCIDR,再调用bpf.UpdateElement写入相应CIDRs。

内核态读取

bpf/bpf_xdp.ccheck_v4函数中,map_lookup_elem函数查找CIDR4_LMAP_NAMEeBPF map,若包含在内,则直接返回CTX_ACT_DROP丢弃包。

  1. #ifdef CIDR4_LPM_PREFILTER

  2. if (map_lookup_elem(&CIDR4_LMAP_NAME, &pfx))

  3. return CTX_ACT_DROP;

  4. #endif

这段代码在__section("from-netdev")段运行,起到XDP层就可以过滤IP的作用。

CIDR4_LMAP_NAME常量就是对应的cilium_cidr_v4_dyn eBPF map ,老样子,也是由go层的代码生成的filter_config.h头文件,会把CIDR4_LMAP_NAME改为全路径的cilium_cidr_v4_dyn

其中,go部分生成头文件的地方在pkg/datapath/prefilter/prefilter.go的59行WriteConfig函数里。

  1. fmt.Fprintf(fw, "#define CIDR4_LMAP_NAME %s\n", path.Base(p.maps[prefixesV4Dyn].String()))

小结

  • eBPF map可以做内核态用户态数据交互

  • 不同数据类型,选择不同的eBPF map类型,LPMTrie与HASH当前类库都支持。

  • 在自己的项目中,也可以考虑内核态做基本的过滤策略,且策略内容可以动态下发。

总结

Cilium产品是面向微服务场景下的网络管理方案,涉及的安全也只是网络链路的可达性。对系统安全几乎没有涉猎。
但该产品是使用eBPF技术大规模应用的优秀项目之一,分析学习他的实现,可以帮助我们快速理解eBPF在go语言中的使用技巧。

通过笔者的分析学习,可以宏观的了解到Cilium在eBPF内核技术使用时,场景覆盖网络处理的XDP、TC、SOCKET等L3、L4、L7层,业务覆盖防火墙、网络路由、网络隔离、负载均衡等。通过集中式管理eBPF文件源码,下发到各endpoint分发式编译挂载。调试日志作为eBPF map事件统一收集处理。支持用户态、内核态相互之间用eBPF map做双向通讯,实现策略下发与数据收集。具备数据对账、监控告警能力。

不足的地方在资源占用、熔断机制等功能。但考虑到cilium是宿主机上主要业务,CPU、内存等资源优先使用,对熔断机制需求不强烈。这点不同于HIDS等安全防御产品,需要让资源给业务,严格控制自身资源使用。

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存