1

Cilium eBPF实现机制源码分析

 2 years ago
source link: https://www.cnxct.com/how-does-cilium-use-ebpf-with-go-and-c/
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.

本文目录 [显示]

本文面向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 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策略对象将代理流量重定向到Cilium用户空间代理实例。使用Envoy实例作为其用户空间代理。然后,根据配置的L7策略转发流量。

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

cilium的Datapath图

综合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行

err = d.initMaps()

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

// initMaps opens all BPF maps (and creates them if they do not exist). This
// must be done *before* any operations which read BPF maps, especially
// restoring endpoints and services.
func (d *Daemon) initMaps() error {
    lxcmap.LXCMap.OpenOrCreate()
    ipcachemap.IPCache.OpenParallel()
    metricsmap.Metrics.OpenOrCreate()
    tunnel.TunnelMap.OpenOrCreate()
    egressmap.EgressMap.OpenOrCreate()
    eventsmap.InitMap(possibleCPUs)
    signalmap.InitMap(possibleCPUs)
    policymap.InitCallMap() 
}

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行

eventsMap := bpf.NewMap(MapName,
        bpf.MapTypePerfEventArray,
        &Key{},
        int(unsafe.Sizeof(Key{})),
        &Value{},
        int(unsafe.Sizeof(Value{})),
        MaxEntries,
        0,
        0,
        bpf.ConvertKeyValue,
    )

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

eBPF代码编译

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

err = d.init()

//跳转到285行的init函数
// Remove any old sockops and re-enable with _new_ programs if flag is set
sockops.SockmapDisable()
sockops.SkmsgDisable()

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

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

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行。

prog := filepath.Join(option.Config.BpfDir, "init.sh")
cmd := exec.CommandContext(ctx, prog, args...)
cmd.Env = bpf.Environment()
if _, err := cmd.CombinedOutput(log, true); err != nil {
    return err
}

是的,你没看错,调用了外部的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行使用iptc 命令对字节码文件进行加载,使内核加载新的字节码。完成新老字节码的注入替换。

C跟go结构体格式校验

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

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字节码编译与加载。

// compileAndLoadXDPProg compiles bpf_xdp.c for the given XDP device and loads it.
func compileAndLoadXDPProg(ctx context.Context, xdpDev, xdpMode string, extraCArgs []string) error {
    args, err := xdpCompileArgs(xdpDev, extraCArgs)
    if err != nil {
        return fmt.Errorf("failed to derive XDP compile extra args: %w", err)
    }

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

    objPath := path.Join(dirs.Output, prog.Output)
    return replaceDatapath(ctx, xdpDev, objPath, symbolFromHostNetdevEp, "", true, xdpMode)
}

函数中,先进行参数重组,在调用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_MAP BPF_MAP_TYPE_PERF_EVENT_ARRAY signal.h ?

LB4_REVERSE_NAT_SK_MAP BPF_MAP_TYPE_LRU_HASH bpf_sock.c ?

LB6_REVERSE_NAT_SK_MAP BPF_MAP_TYPE_LRU_HASH bpf_sock.c ?

CIDR4_HMAP_NAME BPF_MAP_TYPE_HASH bpf_xdp.c ?

CIDR4_LMAP_NAME BPF_MAP_TYPE_LPM_TRIE bpf_xdp.c

CIDR6_HMAP_NAME BPF_MAP_TYPE_HASH bpf_xdp.c

CIDR6_LMAP_NAME BPF_MAP_TYPE_LPM_TRIE bpf_xdp.c

bytecount_map BPF_MAP_TYPE_HASH bytecount.h

cilium_xdp_scratch BPF_MAP_TYPE_PERCPU_ARRAY xdp.h

EVENTS_MAP BPF_MAP_TYPE_PERF_EVENT_ARRAY event.h

IPV4_FRAG_DATAGRAMS_MAP BPF_MAP_TYPE_LRU_HASH ipv4.h

LB6_REVERSE_NAT_MAP BPF_MAP_TYPE_HASH lb.h

LB6_SERVICES_MAP_V2 BPF_MAP_TYPE_HASH lb.h

ENDPOINTS_MAP BPF_MAP_TYPE_HASH maps.h

METRICS_MAP BPF_MAP_TYPE_PERCPU_HASH maps.h

POLICY_CALL_MAP BPF_MAP_TYPE_PROG_ARRAY

THROTTLE_MAP BPF_MAP_TYPE_HASH

EP_POLICY_MAP BPF_MAP_TYPE_HASH_OF_MAPS maps.h ? Map to link endpoint id to per endpoint cilium_policy map

POLICY_MAP BPF_MAP_TYPE_HASH maps.h ? Per-endpoint policy enforcement map

EVENTS_MAP BPF_MAP_TYPE_SOCKHASH bpf_sockops.h ?

太多了,而且比较偏向cilium的业务功能,偏离本文主题,不写了。后面会按照数据流向分三类,总结说明。

map作用分类

内核态自用

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

//bpf/sockops/bpf_sockops.c line 127 
__section("sockops")
int bpf_sockmap(struct bpf_sock_ops *skops)
{
    // 调用bpf_sock_ops_ipv4 函数
    sock_hash_update(skops, &SOCK_OPS_MAP, &key, BPF_NOEXIST);
}
//bpf/sockops/bpf_redir.c line 42
__section("sk_msg")
int bpf_redir_proxy(struct sk_msg_md *msg)
{
    msg_redirect_hash(msg, &SOCK_OPS_MAP, &key, flags);
}

内核态写,用户态读

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

EVENTS_MAP map创建

MapName = "cilium_events" //eventsmap.go line 19
eventsMap := bpf.NewMap(MapName,
        bpf.MapTypePerfEventArray,
        &Key{},
        int(unsafe.Sizeof(Key{})),
        &Value{},
        int(unsafe.Sizeof(Value{})),
        MaxEntries,
        0,
        0,
        bpf.ConvertKeyValue,
    )

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

// Path to where bpffs is mounted , /sys/fs/bpf
mapRoot = defaults.DefaultMapRoot

// Prefix for all maps (default: tc/globals)
mapPrefix = defaults.DefaultMapPrefix
m.path = filepath.Join(mapRoot, mapPrefix, name)
// 即 /sys/fs/bpf/tc/globals/cilium_events

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

ret, _, err := unix.Syscall(
    unix.SYS_BPF,
    BPF_MAP_CREATE,
    uintptr(unsafe.Pointer(&uba)),
    unsafe.Sizeof(uba),
)

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

map数据写入

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

static __always_inline void cilium_dbg(struct __ctx_buff *ctx, __u8 type, __u32 arg1, __u32 arg2)
{
    struct debug_msg msg = {
        __notify_common_hdr(CILIUM_NOTIFY_DBG_MSG, type),
        .arg1   = arg1,
        .arg2   = arg2,
    };

ctx_event_output(ctx, &EVENTS_MAP, BPF_F_CURRENT_CPU,
             &msg, sizeof(msg));
}

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

cDefinesMap["EVENTS_MAP"] = eventsmap.MapName
cDefinesMap["SIGNAL_MAP"] = signalmap.MapName
cDefinesMap["POLICY_CALL_MAP"] = policymap.PolicyCallMapName
cDefinesMap["EP_POLICY_MAP"] = eppolicymap.MapName
cDefinesMap["LB6_REVERSE_NAT_MAP"] = "cilium_lb6_reverse_nat"
cDefinesMap["LB6_SERVICES_MAP_V2"] = "cilium_lb6_services_v2"

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

struct bpf_elf_map __section_maps EVENTS_MAP = {
    .type       = BPF_MAP_TYPE_PERF_EVENT_ARRAY,
    .size_key   = sizeof(__u32),
    .size_value = sizeof(__u32),
    .pinning    = PIN_GLOBAL_NS,
    .max_elem   = __NR_CPUS__,
};

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

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不是同一个,而是重新定义一个。影响代码分析。

path := oldBPF.MapPath(eventsMapName)
eventsMap, err := ebpf.LoadPinnedMap(path, nil)

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

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

    if record.LostSamples > 0 {
        // 丢失数据大小统计
        a.MonitorStatus.Lost += int64(record.LostSamples) 
        // 通知所有内部消费者,告诉他们数据丢失部分大小
        a.notifyPerfEventLostLocked(record.LostSamples, record.CPU)

        // 存入外部订阅者队列,在队列的消费处,发送给所有监听者
        a.sendToListenersLocked(&payload.Payload{
            CPU:  record.CPU,
            Lost: record.LostSamples,
            Type: payload.RecordLost,
        })

    } 
    // ...
}

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

  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,来支持本机进程间数据通讯。

func ServeMonitorAPI(monitor *Agent) error {
    listener, err := buildServer(defaults.MonitorSockPath1_2)
    if err != nil {
        return err
    }

    s := &server{
        listener: listener,
        monitor:  monitor,
    }

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

    go s.connectionHandler1_2(monitor.Context())

    return nil
}

对于内核态程序、稳定性质量做监控,结合内核态数据对服务更好的掌控。
事件的派发角色需要结合业务,进程内消费角色如何划分(对账、解码),进程间消费角色如何设计,多版本升级,通许协议如何设计等。
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选择

for _, cidr := range cidrs {
    ones, bits := cidr.Mask.Size()
    which := p.selectMap(ones, bits)
    if which == mapCount || p.maps[which] == nil {
        ret = fmt.Errorf("No map enabled for CIDR string %s", cidr.String())
        break
    }
    err := p.maps[which].InsertCIDR(cidr)
    if err != nil {
        ret = fmt.Errorf("Error inserting CIDR string %s: %s", cidr.String(), err)
        break
    } else {
        undoQueue = append(undoQueue, cidr)
    }
}

循环遍历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丢弃包。

#ifdef CIDR4_LPM_PREFILTER
    if (map_lookup_elem(&CIDR4_LMAP_NAME, &pfx))
        return CTX_ACT_DROP;
#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函数里。

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等安全防御产品,需要让资源给业务,严格控制自身资源使用。

CFC4N的博客 由 CFC4N 创作,采用 知识共享 署名-非商业性使用-相同方式共享(3.0未本地化版本)许可协议进行许可。基于https://www.cnxct.com上的作品创作。转载请注明转自:Cilium eBPF实现机制源码分析


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK