Kubernetes 網絡學習之 Cilium 與 eBPF
開始之前說點題外話,距離上一篇 Flannel CNI 的發布已經快一個月了。這篇本想趁著勢頭在去年底完成的,正好在一個月內完成計劃的所有內容。但上篇發布后不久,我中招了花了一個多周的時間才恢復。然而,恢復后的狀態讓我有點懵,總感覺很難集中精力,很容易精神渙散。可能接近網上流傳的“腦霧”吧,而且 Cilium 也有點類似一團迷霧。再疊加網絡知識的不足,eBPF 也未從涉足,學習的過程中斷斷續續,我曾經一度懷疑這篇會不會流產。
文章中不免會有問題,如果有發現問題或者建議,望不吝賜教。
背景
去年曾經寫過一篇文章 《使用 Cilium 增強 Kubernetes 網絡安全》[1] 接觸過 Cilium,借助 Cilium 的網絡策略從網絡層面對 pod 間的通信進行限制。但當時我不曾深入其實現原理,對 Kubernetes 網絡和 CNI 的了解也不夠深入。這次我們通過實際的環境來探尋 Cilium 的網絡。
這篇文章使用的 Cilium 版本是 v1.12.3,操作系統是 Ubuntu 20.04,內核版本是 5.4.0-91-generic。
Cilium 簡介
Cilium[2] 是一個開源軟件,用于提供、保護和觀察容器工作負載(云原生)之間的網絡連接,由革命性的內核技術 eBPF[3] 推動。
cilium-on-kubernetes
eBPF 是什么?
Linux 內核一直是實現監控/可觀測性、網絡和安全功能的理想地方。 不過很多情況下這并非易事,因為這些工作需要修改內核源碼或加載內核模塊, 最終實現形式是在已有的層層抽象之上疊加新的抽象。 eBPF 是一項革命性技術,它能在內核中運行沙箱程序(sandbox programs), 而無需修改內核源碼或者加載內核模塊。
將 Linux 內核變成可編程之后,就能基于現有的(而非增加新的)抽象層來打造更加智能、 功能更加豐富的基礎設施軟件,而不會增加系統的復雜度,也不會犧牲執行效率和安全性。
Linux 的內核在網絡棧上提供了一組 BPF 鉤子,通過這些鉤子可以觸發 BPF 程序的執行。Cilium datapah 使用這些鉤子加載 BPF 程序,創建出更高級的網絡結構。
通過閱讀 Cilium 參考文檔 eBPF Datapath[4] 得知 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[5],因為 k8e 使用 Cilium 作為默認的 CNI 實現。
還是在我的 homelab 上做個雙節點(ubuntu-dev2: 192.168.1.12、ubuntu-dev3: 192.168.1.13)的集群。
Master 節點:
Worker 節點:
部署示例應用,將其調度到不同的節點上:
為了使用方便,將示例應用、cilium pod 等信息設置為環境變量:
Debug 流量
還是以前的套路,從請求發起方開始一路追尋網絡包。這次使用 Service 來進行訪問:curl http://10.42.0.51:80/get。
第 1 步:容器發送請求
檢查 pod curl 的路由表:
可知網絡包就發往以太接口 eth0,然后從使用 arp 查到其 MAC 地址 ae:36:76:3e:c3:03:
查看接口 eth0 的信息:
發現其 MAC 地址并不是 ae:36:76:3e:c3:03,從名字上的 @if43 可以得知其 veth 對的索引是 43,接著 登錄到節點 NODE1 查詢該索引接口的信息:
我們看到這個接口 lxc48c4aa0637ce 的 MAC 正好就是 ae:36:76:3e:c3:03。
按照 過往的經驗[6],這個虛擬的以太接口 lxc48c4aa0637ce 是個 虛擬以太網口,位于主機的根網絡命名空間,一方面與容器的以太接口 eth0 間通過隧道相連,發送到任何一端的網絡包都會直達對端;另一方面應該與主機命名空間上的網橋相連,但是從上面的結果中并未找到網橋的名字。
通過 ip link 查看:
我們看到了多個以太接口:cilium_net、cilium_host、cilium_vxlan、cilium_health 以及與容器網絡命名空間的以太接口的隧道對端 lxcxxxx。
cilium-cross-node
網絡包到了 lxcxxx 這里再怎么走?接下來就輪到 eBPF 出場了。
注意 cilium_net、cilium_host 和 cilium_health 在文中不會涉及,因此不在后面的圖中體現。
第 2 步:Pod1 LXC BPF Ingress
進入到當前節點的 cilium pod 也就是前面設置的變量 $cilium1 中使用 bpftool 命令檢查附加該 veth 上 BPF 程序。
也可以登錄到節點 $NODE1 上使用 tc 命令來查詢。注意,這里我們指定了 ingress,在文章開頭 datapath 部分。因為容器的 eth0 與主機網絡命名空間的 lxc 組成通道,因此容器的出口(Egress)流量就是 lxc 的入口 Ingress 流量。同理,容器的入口流量就是 lxc 的出口流量。
可以通過程序 id 2901 查看詳細信息。
可以看出,這里加載了 BPF 程序 bpf_lxc.o 的 from-container 部分。到 Cilium 的源碼 bpf_lxc.c[7]的 __section("from-container") 部分,程序名 handle_xgress:
(1):網絡包的頭信息發送給 handle_xgress,然后檢查其 L3 的協議。
(2):所有 IPv4 的網絡包都交由 tail_handle_ipv4 來處理。
(3):核心的邏輯都在 handle_ipv4_from_lxc。tail_handle_ipv4 是如何跳轉到 handle_ipv4_from_lxc,這里用到了 Tails Call[8] 。Tails call 允許我們配置在某個 BPF 程序執行完成并滿足某個條件時執行指定的另一個程序,且無需返回原程序。這里不做展開有興趣的可以參考 官方的文檔[9]。
(4):接著從 eBPF map cilium_ipcache 中查詢目標 endpoint,查詢到 tunnel endpoint 192.168.1.13,這個地址是目標所在的節點 IP 地址,類型是。
(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 程序。
容器的出口流量對 cilium_vxlan 來說也是 engress,因此這里的程序是 to-overlay。
程序位于 `bpf_overlay.c`[10] 中,這個程序的處理很簡單,如果是 IPv6 協議會將封包使用 IPv6 的地址封裝一次。這里是 IPv4 ,直接返回 CTX_ACT_OK。將網絡包交給內核網絡棧,進入 eth0 接口。
第 4 步:NODE1 NIC BPF Egress
先看看 BPF 程序。
egress 程序 to-netdev 位于 `bpf_host.c`[11]。實際上沒做重要的處理,只是返回 CTX_ACT_OK 交給內核網絡棧繼續處理:將網絡包發送到 vxlan 隧道發送到對端,也就是節點 192.168.1.13 。中間數據的傳輸,實際上用的還是 underlaying 網絡,從主機的 eth0 接口經過 underlaying 網絡到達目標主機的 eth0接口。
第 5 步:NODE2 NIC BPF Ingress
vxlan 網絡包到達節點的 eth0 接口,也會觸發 BPF 程序。
這次觸發的是 from-netdev,位于 bpf_host.c[12] 中。
對 vxlan tunnel 模式來說,這里的邏輯很簡單。當判斷網絡包是 vxlan 的并確認允許 vlan 后,直接返回 CTX_ACT_OK 將處理交給內核網絡棧。
第 6 步:NODE2 vxlan BPF Ingress
網絡包通過內核網絡棧來到了接口 cilium_vxlan。
程序位于 `bpf_overlay.c`[13] 中。
(1):lookup_ip4_endpoint 會在 eBPF map cilium_lxc 中檢查目標地址是否在當前節點中(這個 map 只保存了當前節點中的 endpoint)。
這里查到目標 endpoint 的信息:id、以太網口索引、mac 地址。在 NODE2 的節點上,查看接口信息發現,這個網口是虛擬以太網設備 lxc65015af813d1,正好是 pod httpbin 接口 eth0 的對端。
(2):ipv4_local_delivery 的邏輯位于 `l3.h`[14] 中,這里會 tail-call 通過 endpoint 的 LXC ID(29)定位的 BPF 程序。
第 7 步:Pod2 LXC BPF Egress
執行下面的命令并不會找到想想中的 egress to-container(與 from-container)。
前面用的 BPF 程序都是附加到接口上的,而這里是直接有 vxlan 附加的程序直接 tail call 的。to-container 可以在 `bpf-lxc.c`[15] 中找到。
(1):ipv4_policy 會執行配置的策略
(2):如果策略通過,會調用 redirect_ep 將網絡包發送到虛擬以太接口 lxc65015af813d1,進入到 veth 后會直達與其相連的容器 eth0 接口。
第 8 步:到達 Pod2
網絡包到達 pod2,附上一張完成的圖。
cilium-packet-flow
總結
說說個人看法吧,本文設計的內容還只是 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(端點)。
- [^2]: eBPF 的 map 可以用來存儲數據,在 Cilium 中 cilium-agent 監控 api-server,將信息寫入 map 中。比如這里cilium_lb4_services_v2 中維護著所有 Kubernetes Service 的信息。
參考資料
[1] 《使用 Cilium 增強 Kubernetes 網絡安全》: https://atbug.com/enhance-kubernetes-network-security-with-cilium/
[2] Cilium: https://cilium.io
[3] eBPF: https://ebpf.io
[4] Cilium 參考文檔 eBPF Datapath: https://docs.cilium.io/en/stable/concepts/ebpf/intro/
[5] k8e: https://getk8e.com
[6] 過往的經驗: https://atbug.com/deep-dive-k8s-network-mode-and-communication/
[7] bpf_lxc.c: https://github.com/cilium/cilium/blob/1c466d26ff0edfb5021d024f755d4d00bc744792/bpf/bpf_lxc.c#L1320
[8] Tails Call: https://docs.cilium.io/en/stable/bpf/#tail-calls
[9] 官方的文檔: https://docs.cilium.io/en/stable/bpf/#tail-calls
[10] bpf_overlay.c: https://github.com/cilium/cilium/blob/1c466d26ff0edfb5021d024f755d4d00bc744792/bpf/bpf_overlay.c#L528
[11] bpf_host.c: https://github.com/cilium/cilium/blob/1c466d26ff0edfb5021d024f755d4d00bc744792/bpf/bpf_host.c#L1081
[12] bpf_host.c: https://github.com/cilium/cilium/blob/1c466d26ff0edfb5021d024f755d4d00bc744792/bpf/bpf_host.c#L1040
[13] bpf_overlay.c: https://github.com/cilium/cilium/blob/1c466d26ff0edfb5021d024f755d4d00bc744792/bpf/bpf_overlay.c#L430
[14] l3.h: https://github.com/cilium/cilium/blob/1c466d26ff0edfb5021d024f755d4d00bc744792/bpf/lib/l3.h#L114
[15] bpf-lxc.c: https://github.com/cilium/cilium/blob/1c466d26ff0edfb5021d024f755d4d00bc744792/bpf/bpf_lxc.c#L2131