乐趣区

关于kubernetes:Kubernetes-网络学习之-Cilium-与-eBPF

这是 Kubernetes 网络学习的第五篇笔记,也是之前打算中的最初一篇。

  • 深刻摸索 Kubernetes 网络模型和网络通信
  • 认识一下容器网络接口 CNI
  • 源码剖析:从 kubelet、容器运行时看 CNI 的应用
  • 从 Flannel 学习 Kubernetes VXLAN 网络
  • Cilium CNI 与 eBPF(本篇)

开始之前说点题外话,间隔上一篇 Flannel CNI 的公布曾经快一个月了。这篇本想趁着势头在去年底实现的,正好在一个月内实现打算的所有内容。但上篇公布后不久,我中招了花了一个多周的工夫才复原。然而,复原后的状态让我有点懵,总感觉很难集中精力,很容易精神涣散。可能靠近网上流传的“脑雾”吧,而且 Cilium 也有点相似一团迷雾。再叠加网络常识的有余,eBPF 也未从涉足,学习的过程中断断续续,我已经一度狐疑这篇会不会流产。

文章中未免会有问题,如果有发现问题或者倡议,望不吝赐教。


背景

去年已经写过一篇文章《应用 Cilium 加强 Kubernetes 网络安全》接触过 Cilium,借助 Cilium 的网络策略从网络层面对 pod 间的通信进行限度。但过后我未曾深刻其实现原理,对 Kubernetes 网络和 CNI 的理解也不够深刻。这次咱们通过理论的环境来探寻 Cilium 的网络。

这篇文章应用的 Cilium 版本是 v1.12.3,操作系统是 Ubuntu 20.04,内核版本是 5.4.0-91-generic。

Cilium 简介

Cilium 是一个开源软件,用于提供、爱护和察看容器工作负载(云原生)之间的网络连接,由革命性的内核技术 eBPF 推动。

eBPF 是什么?

Linux 内核始终是实现监控 / 可观测性、网络和平安性能的现实中央。不过很多状况下这并非易事,因为这些工作须要批改内核源码或加载内核模块,最终实现模式是在已有的层层形象之上叠加新的形象。eBPF 是一项革命性技术,它能在内核中运行沙箱程序(sandbox programs),而无需批改内核源码或者加载内核模块。

将 Linux 内核变成可编程之后,就能基于现有的(而非减少新的)形象层来打造更加智能、性能更加丰盛的基础设施软件,而不会减少零碎的复杂度,也不会就义执行效率和安全性。

Linux 的内核在网络栈上提供了一组 BPF 钩子,通过这些钩子能够触发 BPF 程序的执行。Cilium datapah 应用这些钩子加载 BPF 程序,创立出更高级的网络结构。

通过浏览 Cilium 参考文档 eBPF Datapath 得悉 Cilium 应用了上面几种钩子:

  • XDP:这是网络驱动中接管网络包时就能够触发 BPF 程序的钩子,也是最早的点。因为此时还没有执行其余操作,比方将网络包写入内存,所以它非常适合运行删除歹意或意外流量的过滤程序,以及其余常见的 DDOS 爱护机制。
  • Traffic Control Ingress/Egress:附加到流量管制(traffic control,简称 tc)ingress 钩子上的 BPF 程序,能够被附加到网络接口上。这种钩子在网络栈的 L3 之前执行,并能够拜访网络包的大部分元数据。适宜解决本节点的操作,比方利用 L3/L4 的端点 1 策略、转发流量到端点。CNI 通常应用虚拟机以太接口对 veth 将容器连贯到主机的网络命名空间。应用附加到主机端 veth 的 tc ingress 钩子,能够监控来到容器的所有流量,并执行策略。同时将另一个 BPF 程序附加到 tc egress 钩子,Cilium 能够监控所有进出节点的流量并执行策略 .
  • Socket operations:套接字操作钩子附加到特定的 cgroup 并在 TCP 事件上运行。Cilium 将 BPF 套接字操作程序附加到根 cgroup,并应用它来监控 TCP 状态转换,特地是 ESTABLISHED 状态转换。当套接字状态变为 ESTABLISHED 时,如果 TCP 套接字的对端也在以后节点(也可能是本地代理),则会附加 Socket send/recv 程序。
  • Socket send/recv:这个钩子在 TCP 套接字执行的每个发送操作上运行。此时钩子能够查看音讯并抛弃音讯、将音讯发送到 TCP 层,或者将音讯重定向到另一个套接字。Cilium 应用它来减速数据门路重定向。

因为前面会用到,这里着重介绍了这几种钩子。

环境搭建

后面几篇文章,我都是应用 k3s 并手动装置 CNI 插件来搭建试验环境。这次,咱们间接应用 k8e,因为 k8e 应用 Cilium 作为默认的 CNI 实现。

还是在我的 homelab 上做个双节点(ubuntu-dev2: 192.168.1.12ubuntu-dev3: 192.168.1.13)的集群。

Master 节点:

curl -sfL https://getk8e.com/install.sh | API_SERVER_IP=192.168.1.12 K8E_TOKEN=ilovek8e INSTALL_K8E_EXEC="server --cluster-init --write-kubeconfig-mode 644 --write-kubeconfig ~/.kube/config" sh -

Worker 节点:

curl -sfL https://getk8e.com/install.sh | K8E_TOKEN=ilovek8e K8E_URL=https://192.168.1.12:6443 sh -

部署示例利用,将其调度到不同的节点上:

NODE1=ubuntu-dev2
NODE2=ubuntu-dev3
kubectl apply -n default -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  labels:
    app: curl
  name: curl
spec:
  containers:
  - image: curlimages/curl
    name: curl
    command: ["sleep", "365d"]
  nodeName: $NODE1
---
apiVersion: v1
kind: Pod
metadata:
  labels:
    app: httpbin
  name: httpbin
spec:
  containers:
  - image: kennethreitz/httpbin
    name: httpbin
  nodeName: $NODE2
EOF

为了使用方便,将示例利用、cilium pod 等信息设置为环境变量:

NODE1=ubuntu-dev2
NODE2=ubuntu-dev3

cilium1=$(kubectl get po -n kube-system -l k8s-app=cilium --field-selector spec.nodeName=$NODE1 -o jsonpath='{.items[0].metadata.name}')
cilium2=$(kubectl get po -n kube-system -l k8s-app=cilium --field-selector spec.nodeName=$NODE2 -o jsonpath='{.items[0].metadata.name}')

Debug 流量

还是以前的套路,从申请发起方开始一路追寻网络包。这次应用 Service 来进行拜访:curl http://10.42.0.51:80/get

kubectl get po httpbin -n default -o wide
NAME      READY   STATUS    RESTARTS   AGE   IP           NODE          NOMINATED NODE   READINESS GATES
httpbin   1/1     Running   0          3m   10.42.0.51   ubuntu-dev3   <none>           <none>

第 1 步:容器发送申请

查看 pod curl 的路由表:

kubectl exec curl -n default -- ip route get 10.42.0.51
10.42.0.51 via 10.42.1.247 dev eth0  src 10.42.1.80

可知网络包就发往以太接口 eth0,而后从应用 arp 查到其 MAC 地址 ae:36:76:3e:c3:03

kubectl exec curl -n default -- arp -n
? (10.42.1.247) at ae:36:76:3e:c3:03 [ether]  on eth0

查看接口 eth0 的信息:

kubectl exec curl -n default -- ip link show eth0
42: eth0@if43: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP qlen 1000
    link/ether f6:00:50:f9:92:a1 brd ff:ff:ff:ff:ff:ff

发现其 MAC 地址并不是 ae:36:76:3e:c3:03,从名字上的 @if43 能够得悉其 veth 对的索引是 43,接着 登录到节点 NODE1 查问该索引接口的信息:

ip link | grep -A1 ^43
43: lxc48c4aa0637ce@if42: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether ae:36:76:3e:c3:03 brd ff:ff:ff:ff:ff:ff link-netns cni-407cd7d8-7c02-cfa7-bf93-22946f923ffd

咱们看到这个接口 lxc48c4aa0637ce 的 MAC 正好就是 ae:36:76:3e:c3:03

依照 过往的教训,这个虚构的以太接口 lxc48c4aa0637ce 是个 虚构以太网口 ,位于主机的根网络命名空间,一方面与容器的以太接口 eth0 间通过隧道相连,发送到任何一端的网络包都会中转对端;另一方面应该与主机命名空间上的网桥相连,然而从下面的后果中并未找到网桥的名字。

通过 ip link 查看:

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether fa:cb:49:4a:28:21 brd ff:ff:ff:ff:ff:ff
3: cilium_net@cilium_host: <BROADCAST,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 36:d5:5a:2a:ce:80 brd ff:ff:ff:ff:ff:ff
4: cilium_host@cilium_net: <BROADCAST,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 12:82:fb:78:16:6a brd ff:ff:ff:ff:ff:ff
5: cilium_vxlan: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/ether fa:42:4d:22:b7:d0 brd ff:ff:ff:ff:ff:ff
25: lxc_health@if24: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 3e:4f:b3:56:67:2b brd ff:ff:ff:ff:ff:ff link-netnsid 0
33: lxc113dd6a50a7a@if32: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 32:3a:5b:15:44:ff brd ff:ff:ff:ff:ff:ff link-netns cni-07cffbd8-83dd-dcc1-0b57-5c59c1c037e9
43: lxc48c4aa0637ce@if42: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether ae:36:76:3e:c3:03 brd ff:ff:ff:ff:ff:ff link-netns cni-407cd7d8-7c02-cfa7-bf93-22946f923ffd

咱们看到了多个以太接口:cilium_netcilium_hostcilium_vxlancilium_health 以及与容器网络命名空间的以太接口的隧道对端 lxcxxxx

网络包到了 lxcxxx 这里再怎么走?接下来就轮到 eBPF 出场了。

留神 cilium_netcilium_hostcilium_health 在文中不会波及,因而不在前面的图中体现。

第 2 步:Pod1 LXC BPF Ingress

进入到以后节点的 cilium pod 也就是后面设置的变量 $cilium1 中应用 bpftool 命令查看附加该 veth 上 BPF 程序。

kubectl exec -n kube-system $cilium1 -c cilium-agent -- bpftool net show dev lxc48c4aa0637ce
xdp:

tc:
lxc48c4aa0637ce(43) clsact/ingress bpf_lxc.o:[from-container] id 2901

flow_dissector:

也能够登录到节点 $NODE1 上应用 tc 命令来查问。留神,这里咱们指定了 ingress,在文章结尾 datapath 局部。因为容器的 eth0 与主机网络命名空间的 lxc 组成通道,因而容器的进口(Egress)流量就是 lxc 的入口 Ingress 流量。同理,容器的入口流量就是 lxc 的进口流量。

#on NODE1
tc filter show dev lxc48c4aa0637ce ingress
filter protocol all pref 1 bpf chain 0
filter protocol all pref 1 bpf chain 0 handle 0x1 bpf_lxc.o:[from-container] direct-action not_in_hw id 2901 tag d578585f7e71464b jited

能够通过程序 id 2901 查看详细信息。

kubectl exec -n kube-system $cilium1 -c cilium-agent -- bpftool prog show id 2901
2901: sched_cls  name handle_xgress  tag d578585f7e71464b  gpl
    loaded_at 2023-01-09T19:29:52+0000  uid 0
    xlated 688B  jited 589B  memlock 4096B  map_ids 572,86
    btf_id 301

能够看出,这里加载了 BPF 程序 bpf_lxc.ofrom-container 局部。到 Cilium 的源码 bpf_lxc.c 的 __section("from-container") 局部,程序名 handle_xgress

handle_xgress #1
  validate_ethertype(ctx, &proto)
  tail_handle_ipv4 #2
    handle_ipv4_from_lxc #3
      lookup_ip4_remote_endpoint => ipcache_lookup4 #4
      policy_can_access #5
      if TUNNEL_MODE #6
        encap_and_redirect_lxc
          ctx_redirect(ctx, ENCAP_IFINDEX, 0)
      if ENABLE_ROUTING
        ipv4_l3
      return CTX_ACT_OK;

(1):网络包的头信息发送给 handle_xgress,而后查看其 L3 的协定。

(2):所有 IPv4 的网络包都交由 tail_handle_ipv4 来解决。

(3):外围的逻辑都在 handle_ipv4_from_lxctail_handle_ipv4 是如何跳转到 handle_ipv4_from_lxc,这里用到了 Tails Call。Tails call 容许咱们配置在某个 BPF 程序执行实现并满足某个条件时执行指定的另一个程序,且无需返回原程序。这里不做开展有趣味的能够参考 官网的文档。

(4):接着从 eBPF map cilium_ipcache 中查问指标 endpoint,查问到 tunnel endpoint 192.168.1.13,这个地址是指标所在的节点 IP 地址,类型是。

kubectl exec -n kube-system $cilium1 -c cilium-agent -- cilium map get cilium_ipcache | grep 10.42.0.51
10.42.0.51/32     identity=15773 encryptkey=0 tunnelendpoint=192.168.1.13   sync

(5):policy_can_access 这里是执行进口策略的查看,本文不波及故不开展。

(6):之后的解决会有两种模式:

  • 间接路由:交由内核网络栈进行解决,或者 underlaying SDN 的反对。
  • 隧道:会将网络包再次封装,通过隧道传输,比方 vxlan。

这里咱们应用的也是隧道模式。网络包交给 encap_and_redirect_lxc 解决,应用 tunnel endpoint 作为隧道对端。最终转发给 ENCAP_IFINDEX(这个值是接口的索引值,由 cilium-agent 启动时获取的),就是以太网接口 cilium_vxlan

第 3 步:NODE 1 vxlan BPF Egress

先看下这个接口上的 BPF 程序。

kubectl exec -n kube-system $cilium1 -c cilium-agent -- bpftool net show dev cilium_vxlan
xdp:

tc:
cilium_vxlan(5) clsact/ingress bpf_overlay.o:[from-overlay] id 2699
cilium_vxlan(5) clsact/egress bpf_overlay.o:[to-overlay] id 2707

flow_dissector:

容器的进口流量对 cilium_vxlan 来说也是 engress,因而这里的程序是 to-overlay

程序位于 bpf_overlay.c 中,这个程序的解决很简略,如果是 IPv6 协定会将封包应用 IPv6 的地址封装一次。这里是 IPv4,间接返回 CTX_ACT_OK。将网络包交给内核网络栈,进入 eth0 接口。

第 4 步:NODE1 NIC BPF Egress

先看看 BPF 程序。

kubectl exec -n kube-system $cilium1 -c cilium-agent -- bpftool net show dev eth0
xdp:

tc:
eth0(2) clsact/ingress bpf_netdev_eth0.o:[from-netdev] id 2823
eth0(2) clsact/egress bpf_netdev_eth0.o:[to-netdev] id 2832

flow_dissector:

egress 程序 to-netdev 位于 bpf_host.c。实际上没做重要的解决,只是返回 CTX_ACT_OK 交给内核网络栈持续解决:将网络包发送到 vxlan 隧道发送到对端,也就是节点 192.168.1.13。两头数据的传输,实际上用的还是 underlaying 网络,从主机的 eth0 接口通过 underlaying 网络达到指标主机的 eth0 接口。

第 5 步:NODE2 NIC BPF Ingress

vxlan 网络包达到节点的 eth0 接口,也会触发 BPF 程序。

kubectl exec -n kube-system $cilium2 -c cilium-agent -- bpftool net show dev eth0
xdp:

tc:
eth0(2) clsact/ingress bpf_netdev_eth0.o:[from-netdev] id 4556
eth0(2) clsact/egress bpf_netdev_eth0.o:[to-netdev] id 4565

flow_dissector:

这次触发的是 from-netdev,位于 bpf_host.c 中。

from_netdev
  if vlan
    allow_vlan
    return CTX_ACT_OK

对 vxlan tunnel 模式来说,这里的逻辑很简略。当判断网络包是 vxlan 的并确认容许 vlan 后,间接返回 CTX_ACT_OK 将解决交给内核网络栈。

第 6 步:NODE2 vxlan BPF Ingress

网络包通过内核网络栈来到了接口 cilium_vxlan

kubectl exec -n kube-system $cilium2 -c cilium-agent -- bpftool net show dev cilium_vxlan
xdp:

tc:
cilium_vxlan(5) clsact/ingress bpf_overlay.o:[from-overlay] id 4468
cilium_vxlan(5) clsact/egress bpf_overlay.o:[to-overlay] id 4476

flow_dissector:

程序位于 bpf_overlay.c 中。

from_overlay
  validate_ethertype
    tail_handle_ipv4
      handle_ipv4
        lookup_ip4_endpoint 1#
          map_lookup_elem
        ipv4_local_delivery 2#
          tail_call_dynamic 3#

(1):lookup_ip4_endpoint 会在 eBPF map cilium_lxc 中查看指标地址是否在以后节点中(这个 map 只保留了以后节点中的 endpoint)。

kubectl exec -n kube-system $cilium2 -c cilium-agent -- cilium map get cilium_lxc | grep 10.42.0.51
10.42.0.51:0    id=2826  flags=0x0000 ifindex=29  mac=96:86:44:A6:37:EC nodemac=D2:AD:65:4D:D0:7B   sync

这里查到指标 endpoint 的信息:id、以太网口索引、mac 地址。在 NODE2 的节点上,查看接口信息发现,这个网口是虚构以太网设施 lxc65015af813d1,正好是 pod httpbin 接口 eth0 的对端。

ip link | grep -B1 -i d2:ad
29: lxc65015af813d1@if28: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether d2:ad:65:4d:d0:7b brd ff:ff:ff:ff:ff:ff link-netns cni-395674eb-172b-2234-a9ad-1db78b2a5beb

kubectl exec -n default httpbin -- ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
28: eth0@if29: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 96:86:44:a6:37:ec brd ff:ff:ff:ff:ff:ff link-netnsid

(2):ipv4_local_delivery 的逻辑位于 l3.h 中,这里会 tail-call 通过 endpoint 的 LXC ID(29)定位的 BPF 程序。

第 7 步:Pod2 LXC BPF Egress

执行上面的命令并不会找到想想中的 egress to-container(与 from-container)。

kubectl exec -n kube-system $cilium2 -c cilium-agent -- bpftool net show | grep 29
lxc65015af813d1(29) clsact/ingress bpf_lxc.o:[from-container] id 4670

后面用的 BPF 程序都是附加到接口上的,而这里是间接有 vxlan 附加的程序间接 tail call 的。to-container 能够在 bpf-lxc.c 中找到。

handle_to_container
  tail_ipv4_to_endpoint
    ipv4_policy #1
      policy_can_access_ingress
    redirect_ep
      ctx_redirect

(1):ipv4_policy 会执行配置的策略

(2):如果策略通过,会调用 redirect_ep 将网络包发送到虚构以太接口 lxc65015af813d1,进入到 veth 后会中转与其相连的容器 eth0 接口。

第 8 步:达到 Pod2

网络包达到 pod2,附上一张实现的图。

总结

说说集体认识吧,本文设计的内容还只是 Cilium 的冰山一角,对于内核常识和 C 语言欠缺的我来说钻研起来十分吃力。Cilium 除此之外还有很多的内容,也还没有深刻去钻研。不得不感叹,Cilium 真是简单,以我目前的理解,Cilium 保护了一套本人的数据在 BPF map 中,比方 endpoint、节点、策略、路由、连贯状态等相当多的数据,这些都是保留在内核中;再就是 BPF 程序的开发和保护老本会随着性能的复杂度而收缩,很难设想如果用 BPF 程序去开发 L7 的性能会多简单。这应该是为什么会借助代理去解决 L7 的场景。

最初分享下学习 Cilium 过程中的教训吧。

首先是 BPF 程序的浏览,在我的项目的 bpf 的代码都是动态的代码,外面散布着很多的与配置相干的 if else,运行时会依据配置进行编译。这种状况下能够进入 Cilium pod,在目录 /run/cilium/state/templates 下有利用配置后的源文件,代码量会少很多;在 /run/cilium/state/globals/node_config 下是以后应用的配置,能够联合这些配置来浏览代码。

脚注


  1. Cilium 通过为容器调配 IP 地址使其在网络上可用。多个容器能够共享同一个 IP 地址,就像 一个 Kubernetes Pod 中能够有多个容器,这些容器之间共享网络命名空间,应用同一个 IP 地址。这些共享同一个地址的容器,Cilium 将其组合起来,成为 Endpoint(端点)。↩
退出移动版