目录

cilium-chain创建Endpoint

概述

本文主要介绍 Cilium 在 cilium-chain 的模式下,Endpoint 及其对应的一些 eBPF 程序是如何生成的。

集群配置

集群以 Flannel 作为主 CNI,Cilium 是以 Chain 的方式部署的,一般可以在这种情况下通过 Cilium 来给集群增加网络策略的功能。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# k get no -o wide
NAME     STATUS     ROLES           AGE   VERSION   INTERNAL-IP    OS-IMAGE              KERNEL-VERSION                      CONTAINER-RUNTIME
master   Ready      control-plane   36d   v1.24.8   192.168.1.200  openEuler 22.03 LTS   5.10.0-60.125.0.152.oe2203.x86_64   docker://19.3.13
node1    Ready      <none>          36d   v1.24.8   192.168.1.201  openEuler 22.03 LTS   5.10.0-60.125.0.152.oe2203.x86_64   docker://19.3.13
node2    Ready      <none>          36d   v1.24.8   192.168.1.202  openEuler 22.03 LTS   5.10.0-60.125.0.152.oe2203.x86_64   docker://19.3.13

# k get po -A -o wide
NAMESPACE     NAME                             READY   STATUS    RESTARTS         AGE    IP              NODE     
kube-system   cilium-lng4k                     1/1     Running   0                4d7h   192.168.1.201   node1    
kube-system   cilium-operator-85c7767d4c-nnm7t 1/1     Running   0                4d7h   192.168.1.202   node2    
kube-system   cilium-spbbl                     1/1     Running   0                4d7h   192.168.1.202   node2    
kube-system   cilium-x8sm9                     1/1     Running   0                4d7h   192.168.1.200   master   
kube-system   etcd-master                      1/1     Running   3 (6d21h ago)    36d    192.168.1.200   master   
kube-system   kube-apiserver-master            1/1     Running   6 (6d21h ago)    36d    192.168.1.200   master   
kube-system   kube-controller-manager-master   1/1     Running   27 (6d21h ago)   36d    192.168.1.200   master   
kube-system   kube-flannel-ds-cfbp7            1/1     Running   0                4d8h   192.168.1.200   master   
kube-system   kube-flannel-ds-kw75c            1/1     Running   0                4d8h   192.168.1.201   node1    
kube-system   kube-flannel-ds-td7g7            1/1     Running   0                4d8h   192.168.1.202   node2    
kube-system   kube-scheduler-master            1/1     Running   24 (6d21h ago)   36d    192.168.1.200   master

测试资源

创建一个 DaemonSet,目的是让所有节点都可以调度和创建一个 Pod,下面的容器会下通过 nginx 命令,开启一个 80 端口的 web 服务,然后再通过 sleep inf 让 Pod 保持 RUNNING 的状态。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: apps/v1
kind: DaemonSet
metadata:
  namespace: kube-system
  name: nm
spec:
  selector:
    matchLabels:
      app: network-multitool
  template:
    metadata:
      labels:
        app: network-multitool
    spec:
      containers:
        - name: network-multitool
          image: runzhliu/network-multitool:latest
          command: ["/bin/bash", "-c", "nginx && sleep inf"]
          securityContext:
            privileged: true

另外创建一个 Policy 用于测试,测试很简单,在创建完网络策略之后,主要是两个方向,一是测试从宿主机 curl 其中一个 PodIP 的80端口是否能通,二是测试从宿主机直接 PING PodIP 能不能正常返回,然后删除网络策略,再重复一次。

 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
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l4-rule"
spec:
  endpointSelector:
    matchLabels:
      app: network-multitool
  ingress:
    - fromEndpoints:
        - matchLabels:
            app: network-multitool
      toPorts:
        - ports:
            - port: "80"
              protocol: TCP
---
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "icmp-rule"
spec:
  endpointSelector:
    matchLabels:
      app: network-multitool
  egress:
    - icmps:
        - fields:
            - type: 8
              family: IPv4
            - type: 128
              family: IPv6

测试的结果可以参考如下。

 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
40
41
42
43
44
45
[root@master yaml]# k apply -f b-l4-rule.yaml
ciliumnetworkpolicy.cilium.io/l4-rule configured
ciliumnetworkpolicy.cilium.io/icmp-rule created
[root@master yaml]# k get pods -o wide
NAME                               READY   STATUS    RESTARTS   AGE     IP              NODE     NOMINATED NODE   READINESS GATES
cilium-mhthn                       1/1     Running   0          162m    192.168.1.202   node2    <none>           <none>
cilium-nbkmh                       1/1     Running   0          162m    192.168.1.201   node1    <none>           <none>
cilium-operator-55d86f9fc9-66vhc   1/1     Running   0          162m    192.168.1.202   node2    <none>           <none>
cilium-operator-55d86f9fc9-whzg2   1/1     Running   0          162m    192.168.1.201   node1    <none>           <none>
cilium-rq8nh                       1/1     Running   0          162m    172.17.0.1      master   <none>           <none>
coredns-6d4b75cb6d-cpkgq           1/1     Running   0          162m    10.244.2.4      node2    <none>           <none>
coredns-6d4b75cb6d-gld9b           1/1     Running   0          162m    10.244.0.106    master   <none>           <none>
etcd-master                        1/1     Running   1          3h59m   172.17.0.1      master   <none>           <none>
kube-apiserver-master              1/1     Running   0          3h59m   172.17.0.1      master   <none>           <none>
kube-controller-manager-master     1/1     Running   0          3h59m   172.17.0.1      master   <none>           <none>
kube-flannel-ds-7mk6b              1/1     Running   0          175m    192.168.1.201   node1    <none>           <none>
kube-flannel-ds-d9wpn              1/1     Running   0          175m    172.17.0.1      master   <none>           <none>
kube-flannel-ds-x55jn              1/1     Running   0          175m    192.168.1.202   node2    <none>           <none>
kube-proxy-2cfd4                   1/1     Running   0          176m    192.168.1.202   node2    <none>           <none>
kube-proxy-6wkq6                   1/1     Running   0          176m    192.168.1.201   node1    <none>           <none>
kube-proxy-t9znf                   1/1     Running   0          176m    172.17.0.1      master   <none>           <none>
kube-scheduler-master              1/1     Running   0          3h59m   172.17.0.1      master   <none>           <none>
nm-gn5cx                           1/1     Running   0          3m41s   10.244.1.7      node1    <none>           <none>
nm-rxcb2                           1/1     Running   0          3m41s   10.244.2.10     node2    <none>           <none>
[root@master yaml]# ping 10.244.1.7
PING 10.244.1.7 (10.244.1.7) 56(84) 字节的数据。
^C
--- 10.244.1.7 ping 统计 ---
已发送 3 个包, 已接收 0 个包, 100% packet loss, time 2069ms

[root@master yaml]# curl 10.244.1.7
^C
[root@master yaml]# k delete -f b-l4-rule.yaml
ciliumnetworkpolicy.cilium.io "l4-rule" deleted
ciliumnetworkpolicy.cilium.io "icmp-rule" deleted
[root@master yaml]# ping 10.244.1.7
PING 10.244.1.7 (10.244.1.7) 56(84) 字节的数据。
64 字节,来自 10.244.1.7: icmp_seq=1 ttl=63 时间=0.385 毫秒
64 字节,来自 10.244.1.7: icmp_seq=2 ttl=63 时间=0.413 毫秒
^C
--- 10.244.1.7 ping 统计 ---
已发送 2 个包, 已接收 2 个包, 0% packet loss, time 1062ms
rtt min/avg/max/mdev = 0.385/0.399/0.413/0.014 ms
[root@master yaml]# curl 10.244.1.7
Praqma Network MultiTool based on nginx:alpine

创建Endpoint

下面是创建 Pod 的时候相关的日志,我们可以根据这些日志关键词,在源码中,找到创建 Endpoint 的具体的逻辑。

 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
PUT /endpoint/{id} request
Create endpoint request
Endpoint creation
New create request
Assigned new identity to endpoint
removing old and adding new identity
Triggering endpoint regeneration due to updated security labels
Getting CEP during an initialization
Regenerating endpoint
Starting policy recalculation...
Completed endpoint policy recalculation
Registered BPF map
preparing new cache transaction: upserting 1 entries, deleting 0 entries
inserting resource into cache
writing header file
writing header file with DNSRules
Endpoint labels unchanged, skipping resolution of identity
Registered BPF map
Preparing to compile BPF
Compiling datapath
Launching compiler
storing CEP UID after create
Compiled new BPF template
Watching template path
Finished writing ELF
Loading CollectionSpec from ELF
Attaching program to interface
Successfully attached program to interface
Attaching program to interface
Successfully attached program to interface
Rewrote endpoint BPF program
Waiting for proxy updates to complete...
attempting to make temporary directory new directory for endpoint programs
Completed endpoint regeneration
End of create request

Chain CNI

通过 Chain 的方式应用 Cilium 与普通模式是有所区别的,从宿主机的 CNI 配置文件可以知道,kubelet 会根据 generic-veth 的配置,先调用 Flannel 来给容器配置好网络命名空间的配置,然后再调用 cilium-cni。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
  "name": "generic-veth",
  "cniVersion": "0.3.1",
  "plugins": [
    {
      "type": "flannel",
      "delegate": {
        "hairpinMode": true,
        "isDefaultGateway": true
      }
    },
    {
      "type": "cilium-cni"
    }
  ]
}

那么 cilium-cni 是如何根据 Pod 来创建 Endpoint 的呢,可以参考源码 plugins/cilium-cni/chaining/generic-veth/generic-veth.go,整体的流程如下。

1
2
3
4
5
6
7
8
// 先是通过Flannel创建了容器的网络命名空间,然后再轮到cilium-chain发挥作用
Add
|- cniVersion.ParsePrevResult(&pluginCtx.NetConf.NetConf) // 获取Flannel的网络配置之后的结果
|- ns.GetNS(pluginCtx.Args.Netns) // 获取Netns
|- veth // 从上面的NS里获取容器veth相关的配置
|- peer // 从宿主机端或者peer对端的网络配置
|- models.EndpointChangeRequest // 构建cilium的请求
|- cli.EndpointCreate(ep) // 发送请求

Endpoint同步

下面是 cilium-agent 创建 Endpoint 的过程,这个过程在 Cilium 的实现中是以事件的方式串联的,其中 daemon 作为 server 会接收到上面说的 cilium cni 的 EndpointCreate 请求。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
daemon
|- d.createEndpoint(params.HTTPRequest.Context(), h.d, epTemplate) // 尝试创建与指定的更改请求相对应的端点
  |- WaitForFirstRegeneration
  |- d.endpointManager.AddEndpoint
    |- expose
      |- mgr.RunK8sCiliumEndpointSync // 启动一个控制器,将端点同步到相应的k8s CiliumEndpoint CRD。预计每个CEP有1个控制器对其进行更新,并保留本地副本并且仅推送更新
        |- ciliumClient.CiliumEndpoints(namespace).Create
      
endpointManager // 用于包含有关本地运行端点集合的状态的结构
|- RestoreEndpoint // 通过管理器向其他子系统公开指定的端点
|- AddEndpoint // 获取准备好的端点对象并开始管理它
  |- EndpointCreated

ELF的编译和加载

ELF 是文件系统中 BPF ELF 对象的内存表示。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CompileOrLoad // 加载指定端点的BPF数据路径程序
|- stats.BpfWriteELF.Start()
|- opts, strings := ELFSubstitutions(ep)
  |- elfVariableSubstitutions(ep) // 返回ELF模板对象文件中必须出现的数据替换集,以更新指定端点的静态数据
  |- elfMapSubstitutions(ep) // 
|- template.Write(dstPath, opts, strings)
|- stats.BpfWriteELF.End
|- l.ReloadDatapath(ctx, ep, stats) // 重新加载指定端点的BPF数据路径程序
  |- stats.BpfLoadProg.Start()
  |- err = l.reloadDatapath(ctx, ep, &dirs)
    |- reloadHostDatapath // 处理主机Endpoint
    |- progs := []progDefinition{{progName: symbolFromEndpoint, direction: dirIngress}} // 收集需要配置的程序
    |- replaceDatapath(ctx, ep.InterfaceName(), objPath, progs, "") // 替换端点或XDP程序的qdisc和BPF程序
      |- bpf.LoadCollectionSpec(objPath) // 从磁盘加载ELF
        |- ebpf.LoadCollectionSpec(path) // 在给定路径加载eBPF ELF并将其解析为CollectionSpec
        |- iproute2Compat(spec) // 解析CollectionSpec中每个MapSpec的Extra字段
        |- classifyProgramTypes(spec) // 设置ProgramSpec的类型,由于它们位于无法识别的ELF部分,因此库无法自动分类
      |- bpf.LoadCollection(spec, opts) // 将CollectionSpec加载到内核中,在此过程中从bpffs中获取任何固定的映射
      |- attachProgram() // 将程序附加到接口
        |- replaceQdisc(link) //
          |- netlink.QdiscReplace(qdisc) //
        |- netlink.FilterReplace(filter) // 
  |- stats.BpfLoadProg.End(err == nil)

简单看一下 elfVariableSubstitutions 方法,实际的意义就是根据 Endpoint 的内容,将 BPF 模板文件里对应的地方替换成 Endpoint 的内容,这点跟用 fmt.Sprintf() 是类似的,因此也可以了解到一部分的头文件或者 C 文件都是需要经过替换才能进行编译的,否则可能连文件内的基本格式都是有问题的。

 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
func elfVariableSubstitutions(ep datapath.Endpoint) map[string]uint64 {
	result := make(map[string]uint64)

	if ipv6 := ep.IPv6Address().AsSlice(); ipv6 != nil {
		// Corresponds to DEFINE_IPV6() in bpf/lib/utils.h
		result["LXC_IP_1"] = sliceToBe64(ipv6[0:8])
		result["LXC_IP_2"] = sliceToBe64(ipv6[8:16])
	}
	if ipv4 := ep.IPv4Address().AsSlice(); ipv4 != nil {
		result["LXC_IPV4"] = uint64(byteorder.NetIPv4ToHost32(net.IP(ipv4)))
	}

	mac := ep.GetNodeMAC()
	result["NODE_MAC_1"] = uint64(sliceToBe32(mac[0:4]))
	result["NODE_MAC_2"] = uint64(sliceToBe16(mac[4:6]))

	if ep.IsHost() {
		if option.Config.EnableNodePort {
			result["NATIVE_DEV_IFINDEX"] = 0
		}
		if option.Config.EnableIPv4Masquerade && option.Config.EnableBPFMasquerade {
			if option.Config.EnableIPv4 {
				result["IPV4_MASQUERADE"] = 0
			}
		}
		result["SECCTX_FROM_IPCACHE"] = uint64(SecctxFromIpcacheDisabled)
	} else {
		result["LXC_ID"] = uint64(ep.GetID())
	}

    ...
}

加载就是在 Attaching program 的过程里进行,这里主要看看 attachProgram 这个方法,如果对 tc 命令比较熟悉,应该非常清楚下面的函数的逻辑。

 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
func attachProgram(link netlink.Link, prog *ebpf.Program, progName string, qdiscParent uint32, xdpFlags uint32) error {
    ...

	if err := replaceQdisc(link); err != nil {
		return fmt.Errorf("replacing clsact qdisc for interface %s: %w", link.Attrs().Name, err)
	}

	filter := &netlink.BpfFilter{
		FilterAttrs: netlink.FilterAttrs{
			LinkIndex: link.Attrs().Index,
			Parent:    qdiscParent,
			Handle:    1,
			Protocol:  unix.ETH_P_ALL,
			Priority:  option.Config.TCFilterPriority,
		},
		Fd:           prog.FD(),
		Name:         fmt.Sprintf("%s-%s", progName, link.Attrs().Name),
		DirectAction: true,
	}

	// 实际调用的就是tc
	if err := netlink.FilterReplace(filter); err != nil {
		return fmt.Errorf("replacing tc filter: %w", err)
	}

	return nil
}

总结

本文主要介绍了当 Cilium 以 Chain 的方式部署的时候,Endpoint 生成以及如何体现在 Pod 的网卡上加载 eBPF 程序的过程。从上面的分析过程我们可以总结出,Cilium Chain 是通过获取 Flannel 配置好的容器网络命名空间的配置,构建 Cilium Endpoint,然后 Endpoint 的信息又会通过编写 ELF 程序来具体的 eBPF 程序的参数,最后再 Attach 到对应的网卡。

参考资料

  1. Cilium相关命令解读
  2. 云原生网络利器–Cilium 之eBPF篇
  3. ebpf intro
  4. ebpf
警告
本文最后更新于 2024年1月2日,文中内容可能已过时,请谨慎参考。