概述
本文主要介绍 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 到对应的网卡。
参考资料
- Cilium相关命令解读
- 云原生网络利器–Cilium 之eBPF篇
- ebpf intro
- ebpf
警告
本文最后更新于 2024年1月2日,文中内容可能已过时,请谨慎参考。