K8S 网络之 Cilium 源码分析
2023-8-15
| 2024-2-2
0  |  阅读时长 0 分钟
type
status
date
slug
summary
tags
category
icon
password

0x01 一些官方介绍

都是一些概念,浏览一遍即可,没说很具体
Kubernetes 网络模型
Kubernetes 网络解决四方面的问题:
  • 集群网络在不同 Pod 之间提供通信。
 
 
集群网络系统
集群网络系统
集群网络系统是 Kubernetes 的核心部分,但是想要准确了解它的工作原理可是个不小的挑战。 下面列出的是网络系统的的四个主要问题: 高度耦合的容器间通信:这个已经被 Pod 和 localhost 通信解决了。 Pod 间通信:这是本文档讲述的重点。 Pod 与 Service 间通信:涵盖在 Service 中。 外部与 Service 间通信:也涵盖在 Service 中。 Kubernetes 的宗旨就是在应用之间共享机器。 通常来说,共享机器需要两个应用之间不能使用相同的端口,但是在多个应用开发者之间 去大规模地协调端口是件很困难的事情,尤其是还要让用户暴露在他们控制范围之外的集群级别的问题上。 动态分配端口也会给系统带来很多复杂度 - 每个应用都需要设置一个端口的参数, 而 API 服务器还需要知道如何将动态端口数值插入到配置模块中,服务也需要知道如何找到对方等等。 与其去解决这些问题,Kubernetes 选择了其他不同的方法。 要了解 Kubernetes 网络模型,请参阅此处。 如何实现 Kubernetes 的网络模型 网络模型由每个节点上的容器运行时实现。最常见的容器运行时使用 Container Network Interface (CNI) 插件来管理其网络和安全功能。 许多不同的 CNI 插件来自于许多不同的供应商。其中一些仅提供添加和删除网络接口的基本功能, 而另一些则提供更复杂的解决方案,例如与其他容器编排系统集成、运行多个 CNI 插件、高级 IPAM 功能等。 请参阅此页面了解 Kubernetes 支持的网络插件的非详尽列表。 接下来 网络模型的早期设计、运行原理以及未来的一些计划, 都在联网设计文档里有更详细的描述。
集群网络系统
  1. 高度耦合的容器间通信:这个已经被 Pod 和 localhost 通信解决了。
  1. Pod 间通信:这是本文档讲述的重点。
  1. Pod 与 Service 间通信:涵盖在 Service 中。
  1. 外部与 Service 间通信:也涵盖在 Service 中。
 
 

0x02 不安装网络插件Pod本地通信

containerd 通过 runC 创建的容器,需要直接通过宿主机网络通信
在master节点创建记得先把污点去掉~ kubectl describe node 把 taint 里面的都 kubectl taint nodes archlinux node-role.kubernetes.io/control-plane- 或 kubectl taint nodes --all node-role.kubernetes.io/control-plane-
notion image
 
使用主机网络
正常创建
notion image
此时Pod使用的网卡为宿主机网络
 

0x03 安装插件Pod通信 - cilium

对于不同的网络插件,下面可能会有较大的差异
安装 cilium 的操作系统要求(新版 archlinux 默认满足)
 
安装完插cilium件后,master节点宿主机会多出几个网卡
notion image
节点也会多出张网卡,不过和master节点有点不同
notion image
 
接下来分析一下 cilium 官方的安装 yaml 文件,这个很长的安装文件由几部分组成
  1. # Source: cilium/templates/cilium-agent-serviceaccount.yaml
  1. # Source: cilium/templates/cilium-operator-serviceaccount.yaml
  1. # Source: cilium/templates/cilium-configmap.yaml
  1. # Source: cilium/templates/cilium-agent-clusterrole.yaml
  1. # Source: cilium/templates/cilium-operator-clusterrole.yaml
  1. # Source: cilium/templates/cilium-agent-clusterrolebinding.yaml
  1. # Source: cilium/templates/cilium-operator-clusterrolebinding.yaml
  1. # Source: cilium/templates/cilium-agent-daemonset.yaml
  1. # Source: cilium/templates/cilium-operator-deployment.yaml
 
1-2 4-7 是定义了Pod的sa用户以及相应的集群角色和集群角色绑定
3 通过 configmap 定义了一些配置项
8 为 cilium agent 端部署文件
9 为 cilium operator 端部署文件
 
关于 Agent 和 Operator 的描述
  • Agent 运行于集群中的每个节点上,通过Kubernetes或API接收描述网络、服务负载均衡、网络策略以及可视化和监控需求的配置。它监听来自编排系统(如Kubernetes)的事件,以了解容器或工作负载何时启动和停止。Agent 也管理Linux内核使用的eBPF程序,用于控制这些容器内/外的所有网络访问
  • Operator 一般位于master节点上,负责用于IP地址管理(IPAM),给Pod分配新的IP CIDR地址和更新kvstore心跳
notion image

0x01 路由

默认情况下,Cilium 使用 VXLAN 封包协议,当然可以在 https://github.com/cilium/cilium/blob/v1.9/install/kubernetes/quick-install.yaml#L107-L112 tunnel 中设置
  • vxlan
  • geneve
  • disabled
Encapsulation Mode
Port Range / Protocol
VXLAN (Default)
8472/UDP
Geneve
6081/UDP
notion image
查看监听端口即可知道使用的是哪种,Geneve 比 VXLAN 新,为 VXLAN 的拓展协议,考虑兼容性一般用 VXLAN 多些
 
当选项为 disabled 时,启用原生包转发,本机数据包转发模式利用Cilium运行网络的路由功能,而不是执行VXLAN等封包。
 
使用原生转发需要连接集群节点的网络必须能够路由PodCIDR,即在 cilium-configmap 中配置 ipv4-native-routing-cidr: x.x.x.x/y
notion image
这里的 Pod 内的 eth0 和 lxc1 网桥连接和之前我在 Linux命名空间机制及其隔离不当导致的漏洞 | tari’s Blog 文章处的实验是类似的,不过路由不走 Linux 内核,直接通过 eBPF转发数据包
 

0x02 IP管理(IPAM)

0x01 Cilium 集群作用域(默认)
 
Cilium Opertor 通过v2.CiliumNode资源来管理每个节点的PodCIDR。这种模式的优点是,它不依赖于 K8S 为每个节点分发的PodCIDR。
notion image
在此模式下,Cilium Agent 将在启动时等待,直到通过Cilium Node v2.CiliumNode对象通过v2.Cilium节点中设置的资源字段为所有启用的地址族提供podCIDRs范围:
 
输出
spec.ipam.podCIDRs 字段正常,可以看到和集群初始化的 CIDR 是不一样的
 
0x02 K8S主机作用域
 
configmap 配置为 ipam: kubernetes 启用 K8S IPAM模式,并将地址分配委托给集群中的节点。IP由K8S在与每个节点关联的PodCIDR范围之外分配的。
notion image
在此模式下,Cilium Agent将在启动时等待,直到通过Kubernetes v1.Node对象通过以下方法启用地址族提供PodCIDR范围
  1. 通过 v1.Node resource 字段
Field
Description
spec.podCIDRs
IPv4 and/or IPv6 PodCIDR range
spec.podCIDR
IPv4 or IPv6 PodCIDR range
需要 kube-controller-manager 通过--allocate-node cidrs 启动指定应分配PodCIDR范围
  1. 通过 v1.Node annotation 字段
Annotation
Description
network.cilium.io/ipv4-pod-cidr
IPv4 PodCIDR range
network.cilium.io/ipv6-pod-cidr
IPv6 PodCIDR range
network.cilium.io/ipv4-cilium-host
IPv4 address of the cilium host interface
network.cilium.io/ipv6-cilium-host
IPv6 address of the cilium host interface
network.cilium.io/ipv4-health-ip
IPv4 address of the cilium-health endpoint
network.cilium.io/ipv6-health-ip
IPv6 address of the cilium-health endpoint
基于annotation的机制主要与旧的Kubernetes版本结合使用,这些版本还不支持spec.podCIDR,但支持IPv4和IPv6。
 

0x03 Masquerading

 
用于pod的IPv4地址是从RFC1918私有地址块分配的,不可公开路由。Cilium将自动将离开集群的所有流量的源IP地址masquerade(伪装)为K8S节点的IPv4地址,因为节点的IP地址已经可以在网络上路由。
notion image
对于IPv6地址,只有在使用iptables实现模式时才执行masquerading,可以通过选项 enable-ipv4-masquerade:falseenable-ipv6-masquerady:false 禁用。
 
当然,也可以通过 ipv4-native-routing-cidr: 10.0.0.0/8 或 ipv6-native-routing-cidr: fd00::/100 来取消masquerading
 
在 cilium masquerading 实现有两种
  1. 基于 eBPF(默认
需要Linux内核大于等于4.19,并且 configmap 配置为 bpf.masquerade=true
 
eBPF虽高效,但目前存在一些问题
  • 当用 cilium 完全代替 kube-proxy时不支持 --nodeport-addresses - 官方说后面会解决
  • Pod → 外部K8S节点 IP 通信无法 masquerading
  • 目前不支持 IPv6流量masquerading
  1. 基于 iptables
默认 masquerade 所有从非 Cilium 网口经过的流量
 
当然可以通过 egress-masquerade-interfaces: eth0 参数指定 masquerade 特定网口的流量

0x04 IPv4 分段

IPv4 分片处理是指当 IP 数据包太大而无法在网络上传输时,路由器将其分成更小的数据包进行传输。在这个过程中,每个数据包包含有关原始数据包位置和大小的信息。
 
Cilium 使用 IPv4 分片来帮助处理大型数据流,并降低网络延迟。此外,它还可以通过缓存和重组分片数据包来提高网络吞吐量和性能。
 

0x05 网络策略(安全)

Cilium 支持
  1. K8S原生L3-L4的POD入站和出站网络策略
  1. 通过CRD(CustomResourceDefinition) 定义 CiliumNetworkPolicy 拓展策略以在L3-L7层支持入站和出站网络策略
  1. CiliumClusterwideNetworkPolicy 集群范围 CRD,由Cilium强制执行的集群范围的策略
目前存在疑问🤔️
  1. kubelet 和 cilium 是如何协调,在容器中创建出网卡的,可以看看 [1] 这篇文章,好像是 调用 cni 二进制程序做到的,难道这部分是写在 cni 里的?https://github.com/containernetworking/cni/blob/main/SPEC.md
  1. 容器内的 vxlan 是如何跨容器进行通信,以及这个 vxlan 是如何被维护的
 

0x04 Cilium 和 kubelet源码阅读

  • kubelet 版本: v1.26.3 commit: 9e644106593f3f4aa98f8a84b23db5fa378900bd
  • Cilium 版本: v1.9 commit: 638f8ddac2ffc05a036df66ccf9c5a920c9e5323
 
由上可知, Cilium 部署时主要部署2个组件
  1. cilium-operator deployment
  1. cilium-agent daemonset

0x01 Cilium operator

operator 默认为 cluster-pool 模式作为 ipam mode,主要负责网段划分
该模式通过v2.CiliumNode资源来管理每个节点的PodCIDR
 
[github.com/cilium/cilium/operator]
• main.go:314 - alloc.Init()
[src/github.com/cilium/cilium/pkg/ipam/allocator/clusterpool]
• clusterpool.go:69 - newCIDRSets(false, operatorOption.Config.ClusterPoolIPv4CIDR, operatorOption.Config.NodeCIDRMaskSizeIPv4)
• clusterpool.go:133 - newCIDRSet(isV6, addr, cidr, maskSize)
由上调用栈知 ipam 会依次遍历多个 cidr,一个网段消耗完了继续使用下一个网段,和 K8S 本身的 PodCIDR 就没有很大关系了
 

0x02 Cilium Agent

Agent 组件会从其对应的子网段出再去划分出每一个 pod ip
 
containerd启动流程调用栈
众所周知,程序执行时,golang的包被 import 引入时,就会查看包内是否存在 init() 函数,有则掉用,比 main() 函数更先调用
[github.com/containerd/containerd/cmd/containerd]
• main.go:28 -
[github.com/containerd/containerd/cmd/containerd/builtins]
• cri.go:22
[github.com/containerd/containerd/pkg/cri]
• cri.go:43 - config := criconfig.DefaultConfig()
 
[github.com/containerd/containerd/pkg/cri/config]
这里初始化了默认的 cni 路径 NetworkPluginBinDir: "/opt/cni/bin"NetworkPluginConfDir: "/etc/cni/net.d", 、默认运行时runc、监听地址等
• config_unix.go:30 - func DefaultConfig() PluginConfig
 
[github.com/containerd/containerd/pkg/cri]
• cri.go:61 - pluginConfig := ic.Config.(*criconfig.PluginConfig)
• cri.go:66 -
到这里网络插件相关路径就初始化的差不多啦
接下来就是正常的 main 函数流程,查看有没有设置 config 参数,从而去覆盖默认的参数
• main.go:115 - config = defaultConfig()
• main.go:122 - configPath := context.GlobalString("config")
• main.go:125 - srvconfig.LoadConfig(configPath, config);
• main.go:195 - server.New(ctx, config)
• server.go:112 - plugins, err := LoadPlugins(ctx, config)
 
Kubelet源码分析 | tari’s Blog 知,kubelet 通过调用 containrd 创建 Pod。
Containerd 创建 Pod 网络在创建 Pod 中的调用栈
[github.com/containerd/containerd/vendor/k8s.io/cri-api/pkg/apis/runtime/v1]
• api.proto:40 - rpc RunPodSandbox(RunPodSandboxRequest) returns (RunPodSandboxResponse) {}
为啥不是 github.com/containerd/containerd/third_party/k8s.io/cri-api/pkg/apis/runtime/v1alpha2?
因为 RunPodSandbox 的方法的接收器为 criService,而 criService 实现的是 CRIServiceCRIService 接口里定义了 runtime.RuntimeServiceServer
notion image
RuntimeServiceServer 接口对应 api.proto:34 service RuntimeService {
 
[github.com/containerd/containerd/pkg/cri/server]
• sandbox_run.go:58 - func (c *criService) RunPodSandbox(ctx context.Context, r *runtime.RunPodSandboxRequest) (_ *runtime.RunPodSandboxResponse, retErr error)
• sandbox_run.go:59 - config := r.GetConfig()
• sandbox_run.go:91 - 101
--> RuntimeHandler: r.GetRuntimeHandler(),
注意这个 Goland 自动跳转是不准的,会跳到 PodSandboxStatus 里,正确的应该是跳到 RunPodSandboxRequest
[github.com/containerd/containerd/vendor/k8s.io/cri-api/pkg/apis/runtime/v1]
api.pb.go:1499-1504
对应
[github.com/containerd/containerd/vendor/k8s.io/cri-api/pkg/apis/runtime/v1]
api.proto:455-464
462行带有一个官方文档
 
即这个 RuntimeHandler 就是我们平常认识的运行时 runC、Kata、gVisor
 
网络设置
[github.com/containerd/containerd/pkg/cri/server]
• sandbox_run.go:336 或 • sandbox_run.go:457 - c.setupPodNetwork(ctx, &sandbox);
• sandbox_run.go:531 - func (c *criService) setupPodNetwork(ctx context.Context, sandbox *sandboxstore.Sandbox) error
• sandbox_run.go:536 - netPlugin = c.getNetworkPlugin(sandbox.RuntimeHandler)
想知道这个 RuntimeHandler 是什么
sandbox.RuntimeHandler 在 sandbox_run.go:91 - 101 中初始化,即对应上面的 RuntimeHandler: r.GetRuntimeHandler()
• sandbox_run.go:367 - netPlugin.Setup(ctx, id, path, opts...)
[github.com/containerd/containerd/vendor/github.com/containerd/go-cni]
• cni.go:163 - c.attachNetworks(ctx, ns)
• cni.go:218 - go asynchAttach(ctx, i, network, ns, &wg, rc)
• cni.go:206 - n.Attach(ctx, ns)
[github.com/containerd/containerd/vendor/github.com/containerd/go-cni]
• namespace.go:33 - n.cni.AddNetworkList(ctx, n.config, ns.config(n.ifName))
[github.com/containerd/containerd/vendor/github.com/containernetworking/cni/libcni]
• api.go:422 - c.addNetwork(ctx, list.Name, list.CNIVersion, net, result, rt)
 
• api.go:395 - pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path)
这里的 cCNIConfig 即上面 containerd启动流程调用栈 里分析出来的路径 /opt/cni/binnet.Network.Type 经分析为 /etc/cni/net.d/05-cilium.conf 里的 type 字段
所以 pluginPath/opt/cni/bin/cilium-cni
 
• api.go:414 - invoke.ExecPluginWithResult(ctx, pluginPath, newConf.Bytes, c.args("ADD", rt), c.exec) [github.com/containerd/containerd/vendor/github.com/containernetworking/cni/pkg/invoke]
• exec.go:126 - stdoutBytes, err := exec.ExecPlugin(ctx, pluginPath, netconf, args.AsEnv())
 
 
即kubelet 在创建 Pod 时会根据 cni 标准调用 /opt/cni/bin/cilium-cni 二进制文件来为当前 Pod 创建网络。
cilium-cni 创建 Pod 网络调用栈
先看 main 函数
cniVersion 参数指定插件支持的 cni 版本,这里最高支持 0.3.1,这与 https://github.com/containernetworking/cni/blob/spec-v0.3.1/SPEC.md#parameters 文档描述 cni 插件 0.3.1 必须支持的一致,即容器增加网络、删除网络和直接运行 cilium-cni 时,返回支持的 cni 版本(注意与spec-v1.0版本是有不同的),可以看这个表 https://github.com/containernetworking/cni/blob/main/SPEC.md#released-versions
 
跟进 cmdAdd研究如何增加容器网络
[src/github.com/cilium/cilium/plugins/cilium-cni]
• cilium-cni.go:80 - skel.PluginMain(cmdAdd,
• cilium-cni.go:358 - configResult, err := c.ConfigGet()
• config.go:25 - resp, err := c.Daemon.GetConfig(nil)
[github.com/cilium/cilium/api/v1/client/daemon]
• daemon_client.go:99 -
通过调用接口获取配置向 cilium-agent (作为服务端)获取配置
 
cilium-agent 接口定义
[github.com/cilium/cilium/api/v1/server/restapi]
• cilium_api_api.go:629 - o.handlers["GET"]["/config"] = daemon.NewGetConfig(o.context, o.DaemonGetConfigHandler)
最终调用
[github.com/cilium/cilium/daemon/cmd]
• config.go:108 - func (h *getConfig) Handle(params GetConfigParams) middleware.Responder
 
接下来根据从服务端读到的配置,选择相应的虚拟化网卡模式
个人觉得主要区别是 veth 实现逻辑更简单,ipvlan 更复杂,但相应的 ipvlan 在使用 vepa 模式时,性能更高
[src/github.com/cilium/cilium/plugins/cilium-cni]
• cilium-cni.go:381 - case datapathOption.DatapathModeVeth:
• cilium-cni.go:410 - case datapathOption.DatapathModeIpvlan:
默认一般用 veth 模式较多
 
• cilium-cni.go:387 - veth, peer, tmpIfName, err = connector.SetupVeth(ep.ContainerID, int(conf.DeviceMTU), ep)
创建 veth pair,并通过 SetupVethWithNames 设置 mac mtu等信息
  • host 侧网卡命名: lxc + sha256(containerID)),如 lxceb7fcc8be80b63438e0f737d76aae32e9d2aabc857c33def4ec6098e5c2c7624;
  • container 侧网卡命名:tmp + maxLen(containerID, 5)
notion image
 
SetupVethWithNames(lxcIfName, tmpIfName, mtu, ep) 底层通过系统调用创建 AF_NETLINK 套接字使用原始套接字向Linux内核发送Netlink消息(用户态 → 内核态)创建 veth
[github.com/cilium/cilium/vendor/github.com/vishvananda/netlink/nl]
• nl_linux.go:428 - s, err = getNetlinkSocket(sockType)
• nl_linux.go:516 - fd, err := unix.Socket(unix.AF_NETLINK, unix.SOCK_RAW|unix.SOCK_CLOEXEC, protocol)
• nl_linux.go:446 - s.Send(req)
 
[github.com/cilium/cilium/plugins/cilium-cni]
• cilium-cni.go:400 - netlink.LinkSetNsFd(*peer, int(netNs.Fd()))
• cilium-cni.go:405 - connector.SetupVethRemoteNs(netNs, tmpIfName, args.IfName)
上述两行代码实现把容器侧的网卡放入新的命名空间中,并重命名网卡名称
 
配置容器侧路由
• cilium-cni.go:481 - prepareIP(ep.Addressing.IPV4, false, &state, int(conf.RouteMTU))
• cilium-cni.go:238 - state.IP4routes, err = connector.IPv4Routes(state.HostAddr, mtu); • cilium-cni.go:506- macAddrStr, err = configureIface(ipam, args.IfName, &state)
 
代码执行结果可以通过下面操作查看
找到容器的的进程id pid
进入容器网络命名空间并查看容器侧路由表
notion image
继续往下走
 
/opt/cni/bin/cilium-cni 二进制文件作为客户端调用 Cilium Agent Pod 服务端分配IP。
• cilium-cni.go:427 - ipam, err = c.IPAMAllocate("", podName, true)
notion image
[github.com/cilium/cilium/pkg/client]
• ipam.go:29 - func (c *Client) IPAMAllocate(family, owner string, expiration bool) (*models.IPAMResponse, error)
• ipam.go:42 - resp, err := c.Ipam.PostIpam(params)
 
[github.com/cilium/cilium/api/v1/client/ipam]
• ipam_client.go:85 - func (a *Client) PostIpam(params *PostIpamParams) (*PostIpamCreated, error)
 
服务端 ipam server ipam 模块逻辑 可看
主要逻辑还是通过本机 pod cidr 中分配出一个 pod ip
[github.com/cilium/cilium/daemon/cmd/ipam]
• ipam_client.go:85 -
 
[github.com/cilium/cilium/pkg/ipam]
• ipam.go:379 -
• ipam.go:111 -
 
通过一系列函数调用,会调用 hostScopeAllocator.AllocateNext() 来获取 pod ip
• hostscope.go:32 -
• hostscope.go:65 -
 
和上文说到 cilium 使用 k8s 源码中从 cluster cidr 划分多个 pod cidr 一样,cilium 也是复用了 k8s 源码中从 pod cidr 中划分出一个个 pod ip 的逻辑,cilium 为了防止引入其他 k8s 依赖包,单独把 k8s 源码中 ip allocator 逻辑 单独出来一个包 cilium/ipam
该包支持 pod ip 两种分配策略:随机顺序分配 randomScanStrategy 和连续顺序分配 contiguousScanStrategy,默认使用随机顺序分配(这种 IP 分配是DHCP服务端会根据POD的mac地址取哈希的方式去获取一个IP)。
 
总之,cilium cni 二进制作为客户端调用 cilium daemon 服务端 IPAM 模块,来从 pod cidr 中随机分配获取 pod ip,并在上文第一步中配置 pod 网卡。
 

0x03 eBPF

最后 cilium 对每个 pod 创建对应的 CiliumEndpoint 对象,在这一步会下发 tc eBPF 程序到 pod 网卡上
[github.com/cilium/cilium/plugins/cilium-cni]
• cilium-cni.go:526 - c.EndpointCreate(ep)
[github.com/cilium/cilium/pkg/client]
• endpoint.go:51 - c.Endpoint.PutEndpointID(params)
[github.com/cilium/cilium/api/v1/client/endpoint]
• endpoint_client.go:436 - func (a *Client) PutEndpointID(params *PutEndpointIDParams) (*PutEndpointIDCreated, error)
 
服务端
[github.com/cilium/cilium/api/v1/server/restapi]
• cilium_api_api.go:761 - o.handlers["PUT"]["/endpoint/{id}"] = endpoint.NewPutEndpointID(o.context, o.EndpointPutEndpointIDHandler)
[github.com/cilium/cilium/daemon/cmd]
• endpoint.go:532 - func (h *putEndpointID) Handle(params PutEndpointIDParams) (resp middleware.Responder)
• endpoint.go:308 - func (d *Daemon) createEndpoint(ctx context.Context, owner regeneration.Owner, epTemplate *models.EndpointChangeRequest) (*endpoint.Endpoint, int, error)
方法createEndpoint 先对 endpoint 的信息和 pod的注解进行了一些处理,然后创建 network policy ,这些地方先不关注, 重点关注 eBPF 程序的下发逻辑
• endpoint.go:500 - ep.Regenerate(regenMetadata)
 
有点不是很明白,ep.Regenerate(regenMetadata) 是怎么调用到这里的,先继续往下走
[github.com/cilium/cilium/pkg/endpoint]
• policy.go:315 - func (e *Endpoint) regenerate(context *regenerationContext) (retErr error)
这里需要注意下 ,context上下文赋值的路径,因为后面下发 bpf 程序的目录会用到,最终得到宿主机目录为 /var/run/cilium/state
• policy.go:368 - origDir := e.StateDirectoryPath()
• policy.go:369 - context.datapathRegenerationContext.currentDir = origDir
• policy.go:375 - context.datapathRegenerationContext.nextDir = tmpDir
 
其中 e.StateDirectoryPath() 获取逻辑为
[github.com/cilium/cilium/daemon/cmd]
• daemon_main.go:1055 - option.Config.StateDir = filepath.Join(option.Config.RunDir, defaults.StateDir)
  1. option.Config.RunDir -
    1. github.com/cilium/cilium/pkg/option • config.go:2639 c.RunDir = viper.GetString(StateDir) -
    2. github.com/cilium/cilium/daemon/cmd • daemon_main.go:702 flags.String(option.StateDir, defaults.RuntimePath, "Directory path to store runtime state")
    3. github.com/cilium/cilium/pkg/defaults • defaults.go:41 - RuntimePath = "/var/run/cilium"
  1. defaults.StateDir - github.com/cilium/cilium/pkg/defaults • defaults.go:50 StateDir = "state”
调用的入口为
[github.com/cilium/cilium/daemon]
• main.go:22 - cmd.Execute()
 
 
• policy.go:416 - revision, stateDirComplete, err = e.regenerateBPF(context)
regenerateBPF函数主要为重写 bpf headers
比如这样子
• bpf.go:595 - headerfileChanged, err = e.runPreCompilationSteps(regenContext)
如一行行向 ep_config.h 写入 C代码
notion image
还有更新 bpf map 信息,下发到网卡中的bpf程序执行逻辑时会去获取bpf map数据
 
接下来就是编译和加载bpf程序
• bpf.go:624 - compilationExecuted, err = e.realizeBPFState(regenContext)
• bpf.go:705 - e.owner.Datapath().Loader().CompileAndLoad(datapathRegenCtxt.completionCtx, datapathRegenCtxt.epInfoCache, &stats.datapathRealization)
• loader.go:396 - compileDatapath(ctx, dirs, ep.IsHost(), debug, ep.Logger(Subsystem))
notion image
notion image
编译 bpf C 程序 Cilium 代码逻辑基本上就是执行类似如下命令
 
加载 bpf 程序
• loader.go:403 - l.reloadDatapath(ctx, ep, dirs)
• netlink.go:57 - func (l *Loader) replaceDatapath(ctx context.Context, ifName, objPath, progSec, progDirection string) error
notion image
下发 bpf 程序到主机/POD网卡
 
以上代码逻辑已经编译并下发多个 BPF 程序到不同的网卡。可以通过如下命令查看:
 
假设容器中 ping clusterip-service-ip,出发走到另外一台机器的pod容器,会经过 from-container -> from-host -> to-netdev -> from-netdev -> to-host BPF 程序
 
from-container BPF
[github.com/cilium/cilium/bpf]
• bpf_lxc.c:841 -
ipv4 则调用 tail_handle_ipv4 然后调用
• bpf_lxc.c:796 - handle_ipv4_from_lxc
函数主要完成:看看目标地址是否是 service ip,如果是则从 BPF Map 中找出一个 pod 作为目标地址,实现了 service 负载均衡
 
policy_can_egress4() 查看是否需要走 network policy,本文默认没有使用 network policy
 
ipv4_l3() 封包或者进行主机路由,设置 ttl 以及存储 src/dst mac 地址
 
from-container 经过 tc eBPF 后进入内核网络协议栈
上文介绍过容器内的路由网关是 cilium_host,packet 达到 cilium_host 网卡的 tc egress, 即走 from-host BPF 程序
调用 handle_netdev → do_netdev,这里的 identity,如果cilium使用默认 vxlan 模式,就是 vxlan id,通过 BPF map 维护
 
tail_handle_ipv4 → handle_ipv4 根据 目标 ipv4 查找 endpoint,即 pod ip
 
根据本机路由表,packet 会被转发给 eth0 网卡,会走 to-netdev BPF 程序
该 BPF 程序只会处理 NodePort service 或 主机防火墙的流量。本文暂不考虑 NodePort service packet。
 
经过以上 BPF 找到目标地址是另一台机器的 pod ip,包达到另一台机器,走 from-netdev BPF 程序,同样基本调用 from-netdev() 函数,逻辑和 from-host BPF 程序 基本一样,这里暂不赘述。
 
packet 到达 cilium_host 网卡走 to-host BPF 程序
调用 ctx_redirect_to_proxy_firstctx_redirect_to_proxy_ingress4,把 packet 转发给 pod 网卡,这样可以跳过 Linux 内核协议栈 netfilter,性能更高

0x05 Reference

[6] 云原生技术与架构实践年货小红书 P145 - P152 涉及 Kubernetes 容器网络模型和典型实现
 
  • Kubernetes
  • 云安全
  • 通过Falco检测K8S容器逃逸行为CSA-CCPTP考证记
    • GitTalk
    目录