# 洞悉 Cilium 如何傳遞 K8s Pod 網路封包 ## 1. Preface 本篇文章會在 CNI 使用 Cilium 的 Kubernetes 環境驗證,pod 跟 pod 之間互相溝通時,在同節點或在不同節點,網路的封包會如何傳遞,並搭配 tcpdump + wireshark 分析封包。 ## 2. 環境資訊 1. K8s 節點及版本資訊 ``` kubectl get nodes -o wide ``` 執行結果: ``` NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME kubeadm-c1.bbg.com Ready control-plane 27d v1.34.1 172.20.6.11 <none> Ubuntu 24.04.3 LTS 6.8.0-85-generic containerd://2.1.4 kubeadm-c2.bbg.com Ready control-plane 27d v1.34.1 172.20.6.12 <none> Ubuntu 24.04.3 LTS 6.8.0-85-generic containerd://2.1.4 kubeadm-c3.bbg.com Ready control-plane 26d v1.34.1 172.20.6.13 <none> Ubuntu 24.04.3 LTS 6.8.0-85-generic containerd://2.1.4 kubeadm-w1.bbg.com Ready worker 26d v1.34.1 172.20.6.14 <none> Ubuntu 24.04.3 LTS 6.8.0-85-generic containerd://2.1.4 kubeadm-w2.bbg.com Ready worker 26d v1.34.1 172.20.6.15 <none> Ubuntu 24.04.3 LTS 6.8.0-85-generic containerd://2.1.4 kubeadm-w3.bbg.com Ready worker 26d v1.34.1 172.20.6.16 <none> Ubuntu 24.04.3 LTS 6.8.0-85-generic containerd://2.1.4 ``` 2. cilium 設定資訊 ``` helm -n kube-system get values cilium ``` 執行結果: ``` USER-SUPPLIED VALUES: # 忽略此行 bpf: masquerade: true hubble: relay: enabled: true ui: enabled: true service: type: NodePort ipam: mode: kubernetes ipv4NativeRoutingCIDR: 10.244.0.0/16 k8sServiceHost: 172.20.6.10 k8sServicePort: 6443 kubeProxyReplacement: true ``` 3. 確認 cilium 版本、routing 模式和 device mode ``` kubectl -n kube-system exec ds/cilium -- cilium-dbg status | grep -iE "^cilium:|^routing|^device mode" ``` 執行結果: ``` Cilium: Ok 1.18.2 (v1.18.2-5bd307a8) Routing: Network: Tunnel [vxlan] Host: BPF Device Mode: veth ``` > 本篇文章測試環境 routing-mode 為 `tunnel`,tunnel-protocol 使用 `vxlan`,沒有特別 tunning。 4. 建立 NGINX pods ``` kubectl create deploy nginx --image=docker.io/library/nginx:latest --replicas=2 ``` 5. 建立 Client pods,replicas 設成 `2`,並指定跑在 worker node ``` echo 'apiVersion: apps/v1 kind: Deployment metadata: labels: app: client name: client spec: replicas: 2 selector: matchLabels: app: client template: metadata: labels: app: client spec: nodeSelector: node-role.kubernetes.io/worker: "" containers: - image: quay.io/cooloo9871/debug.alp name: debug-alp securityContext: privileged: true' | kubectl apply -f - ``` 6. 檢視 pods 各至在哪一個節點 ``` kubectl get pods -o wide ``` 執行結果類似以下螢幕輸出: ``` NAME STATUS NODE client-65766844b7-72zg2 Running kubeadm-w1.bbg.com client-65766844b7-c4w29 Running kubeadm-w2.bbg.com nginx-66686b6766-6vd9x Running kubeadm-w2.bbg.com nginx-66686b6766-s25sh Running kubeadm-w3.bbg.com ``` ## 3. 檢視單一 Pod 的網路架構 1. 查看 client pod 的 ip address 和 subnet mask ``` kubectl exec -it client-65766844b7-c4w29 -- ip a ``` 執行結果: ``` ...以上省略 34: eth0@if35: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP link/ether d6:c4:d2:00:8c:2e brd ff:ff:ff:ff:ff:ff inet 10.244.7.250/32 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::d4c4:d2ff:fe00:8c2e/64 scope link valid_lft forever preferred_lft forever ``` > **得知 subnet mask 為 `/32`,所以 pod 要上網一定會走 Default Gateway** 2. 查看 client Pod 的 Default Gateway ``` kubectl exec -it client-65766844b7-c4w29 -- ip r ``` 執行結果: ``` default via 10.244.7.247 dev eth0 10.244.7.247 dev eth0 scope link ``` > **確認 Default Gateway 的值為 `10.244.7.247`** 3. 查看 client Pod 裡的 Neighbour Table ``` kubectl exec -it client-65766844b7-c4w29 -- ip n ``` 執行結果: ``` 10.244.7.247 dev eth0 lladdr 1e:49:e0:67:ec:e7 used 0/0/0 probes 4 STALE ``` > **得知 Default Gateway `10.244.7.247` 對應的 Mac Address 為 `1e:49:e0:67:ec:e7`** 4. 到 pod 所在的節點查找 pod 內看到 Default Gateway 對應在主機是哪張網卡 ``` ip a s | grep -A 3 -B 2 10.244.7.247 ``` 執行結果: ``` 4: cilium_host@cilium_net: <BROADCAST,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether ba:af:34:f7:09:12 brd ff:ff:ff:ff:ff:ff inet 10.244.7.247/32 scope global cilium_host valid_lft forever preferred_lft forever inet6 fe80::b8af:34ff:fef7:912/64 scope link valid_lft forever preferred_lft forever ``` > **發現 `10.244.7.247` 這張網卡對應的 Mac Address 竟然是 `ba:af:34:f7:09:12` 而不是在 client pod 裡面看到的 `1e:49:e0:67:ec:e7`** 5. 繼續在 pod 所在的節點查找 `1e:49:e0:67:ec:e7` 是哪張網卡 ``` ip a s | grep -A 3 -B 1 '1e:49:e0:67:ec:e7' ``` 執行結果: ``` 35: lxc7904b406cec6@if34: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether 1e:49:e0:67:ec:e7 brd ff:ff:ff:ff:ff:ff link-netns cni-a59f663a-3aed-32da-7743-287142c3ccda inet6 fe80::1c49:e0ff:fe67:ece7/64 scope link valid_lft forever preferred_lft forever ``` > 由網卡名稱可以得知是 veth pair,並且另一端連接在 `cni-a59f663a-3aed-32da-7743-287142c3ccda` Linux Network Namespace 6. 進入這個 Linux Network Namespace 查看網卡資訊 ``` sudo ip netns exec cni-a59f663a-3aed-32da-7743-287142c3ccda ip a ``` 執行結果: ``` 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 34: eth0@if35: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default link/ether d6:c4:d2:00:8c:2e brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet 10.244.7.250/32 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::d4c4:d2ff:fe00:8c2e/64 scope link valid_lft forever preferred_lft forever ``` > 確認 client pod `eth0` 在主機側的網卡名稱是 `lxc7904b406cec6` 7. 確認初始狀態下 pod 內部 arp cache ``` kubectl exec -it client-65766844b7-c4w29 -- arp -a ``` 執行結果 ``` ``` > 得知 pod 在初始狀態下 arp cache 應為空 ### 小結論 - Client Pod `eth0` 網卡對應在 host 主機這側的網卡是 `lxc7904b406cec6` - 在 Client Pod 內部 Neighbour Table 紀錄 Default Gateway 的 IP 是在 host 主機 `cilium_host` 網卡的 ip,但是 Mac Address 卻是 pod 在 host 主機側網卡的 mac address - pod 在初始狀態下無任何 arp cache - 網路架構如下圖 ![image](https://hackmd.io/_uploads/ByPWFdOybl.png) ## 4. 檢視同一台節點內 Pods 之間網路封包如何傳遞 1. 查詢 nginx pod 網卡資訊,在 pod 所在的節點執行以下命令 ``` pod_name="nginx-66686b6766-6vd9x" sudo ip netns exec $(sudo ip netns identify $(sudo crictl inspect $(sudo crictl ps -a | grep ${pod_name} | cut -d " " -f 1) | jq -r '.info.pid // .status.pid')) ip a s eth0 ``` 執行結果: ``` 32: eth0@if33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default link/ether 9a:08:1c:60:8e:77 brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet 10.244.7.203/32 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::9808:1cff:fe60:8e77/64 scope link valid_lft forever preferred_lft forever ``` > 得知 nginx pod 網卡資訊: > - 名稱為 `eth0` > - ip address 為 `10.244.7.203/32` > - mac address 為 `9a:08:1c:60:8e:77` 2. 找到 nginx pod 在主機側的網卡,在 pod 所在的節點執行以下命令 ``` pod_name="nginx-66686b6766-6vd9x" ip a s | grep -A 3 ^$(sudo ip netns exec $(sudo ip netns identify $(sudo crictl inspect $(sudo crictl ps -a | grep ${pod_name} | cut -d " " -f 1) | jq -r '.info.pid // .status.pid')) ip a s eth0 | head -n 1 | cut -d ":" -f 2 | tail -c 3) ``` 執行結果: ``` 33: lxcdad8b6341b55@if32: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether 2a:e0:31:24:98:d1 brd ff:ff:ff:ff:ff:ff link-netns cni-90146bc6-03b3-1ff1-72c8-61b61819bb95 inet6 fe80::28e0:31ff:fe24:98d1/64 scope link valid_lft forever preferred_lft forever ``` > 得知 nginx pod 在主機側網卡資訊: > - 名稱為 `lxcdad8b6341b55` > - mac address 為 `2a:e0:31:24:98:d1` 1. 額外開啟 4 個終端機,分別都 ssh 到 client 和 nginx pod 所在的同一台節點上,並依序執行以下 4 個收集封包的動作 1. 在 client pod 主機側的網卡上,透過 `tcpdump` 命令收集 ARP 或 ICMP 封包 ``` sudo tcpdump -n -i lxc7904b406cec6 arp or icmp -vvv -w client-pod-host.pcap ``` 2. 在 nginx pod 主機側的網卡上,透過 `tcpdump` 命令收集 ARP 或 ICMP 封包 ``` sudo tcpdump -n -i lxcdad8b6341b55 arp or icmp -vvv -w nginx-pod-host.pcap ``` 3. 在 client pod Container 側的網卡上,透過 `tcpdump` 命令收集 ARP 或 ICMP 封包 ``` sudo ip netns exec cni-a59f663a-3aed-32da-7743-287142c3ccda tcpdump -n -i eth0 arp or icmp -vvv -w client-pod.pcap ``` 4. 在 nginx pod Container 側的網卡上,透過 `tcpdump` 命令收集 ARP 或 ICMP 封包 ``` sudo ip netns exec cni-90146bc6-03b3-1ff1-72c8-61b61819bb95 tcpdump -n -i eth0 arp or icmp -vvv -w nginx-pod.pcap ``` 4. 切換到可執行 `kubectl` 主機的終端機,進到 `client` pod 去 ping `nginx` pod ``` kubectl exec -it client-65766844b7-c4w29 -- ping -c 1 10.244.7.203 ``` 執行結果: ``` PING 10.244.7.203 (10.244.7.203): 56 data bytes 64 bytes from 10.244.7.203: seq=0 ttl=63 time=0.198 ms --- 10.244.7.203 ping statistics --- 1 packets transmitted, 1 packets received, 0% packet loss round-trip min/avg/max = 0.198/0.198/0.198 ms ``` ### 小總結 ![image](https://hackmd.io/_uploads/SkwtoKt1Zg.png) 1. client Pod 去 `ping` 同一節點的 nginx pod,因 subnet mask 為 `/32`,網路封包一定往 Default Gateway `10.244.7.247` 送,由於初始化 pod 內部 arp cache 為空,故觸發 arp 通訊協定來去得到 Default Gateway 的 mac address。 ![image](https://hackmd.io/_uploads/SJNv6q_y-e.png) 封包詳細資訊: ![image](https://hackmd.io/_uploads/HknfCcOyWg.png) 2. 由 arp 通訊協定得知 Default Gateway 的 mac address 為 `1e:49:e0:67:ec:e7`,這 mac address 對應的網卡是 client pod 在主機側的網卡 `lxc7904b406cec6`。 ![image](https://hackmd.io/_uploads/B1lD05dkbe.png) 封包詳細資訊: ![image](https://hackmd.io/_uploads/Hyf9AcuJWg.png) 3. 更詳細的運作流程: 1. client Pod 以廣播的方式發送 arp request 封包 2. arp request 被在主機側的網卡收到,此時 cilium bpf `cil_from_container` 程式會立刻接手 4. 經過一連串判斷,會用自己的 MAC address 假裝自己是 gateway 來回復這個 arp request,詳情請看 [shiun 大佬的源碼分析](https://ithelp.ithome.com.tw/m/articles/10387997#:~:text=bpf_lxc.c%20%E3%80%82-,cil_from_container,-cil_from_container%20%E5%9C%A8%E5%81%9A%E4%BB%80%E9%BA%BC) > `cil_from_container` 在做什麼? > 簡單一句話來解釋就是:「`cil_from_container` 是 application container 網路的出口警衛,專門檢查所有從 app container 送出的網路封包,並決定它們的下一步該怎麼走。」 5. client pod 收到被偽造 Mac address 的 arp reply 4. 此時 icmp echo request 封包從 pod 內發送到在主機側的 `lxc7904b406cec6` 網卡 ![image](https://hackmd.io/_uploads/rkW3HnOy-e.png) 封包詳細資訊: ![image](https://hackmd.io/_uploads/By31DadJbg.png) 5. 立刻又被 Cilium 掛在該網卡上的 BPF 程式 `cil_from_container` 接住,並作出以下判斷 - 如果目標 pod 是同一個 Node → 找到目標 pod 在主機側的網卡 → 然後將封包送進 Container 側的網卡 - 更詳細運作流程: 1. 拿到封包後,把目的 MAC Address 和來源的 MAC Address 的換掉,確保能送到正確的 Pod 2. 直接送達:它不把包裹交給大樓的收發室 (Linux Network Stack),而是直接搭電梯(tail_call_policy)衝到收件 Pod 的門口(在主機側的網卡),把包裹交給門口的警衛 (Ingress BPF 程式) 做安全檢查 (Policy Enforcement) - 以下是從 client pod 在主機側網卡收到的 icmp echo request 封包資訊 (為更換 mac address 前) ![image](https://hackmd.io/_uploads/Sy-8maOkWg.png) 封包詳細資訊: ![image](https://hackmd.io/_uploads/B107vaOkWl.png) 遺憾的是沒收到更換完 mac address 的 icmp echo request 封包 - 如果目標 pod 是跨 Node → 會轉交 `cilium_vxlan` / `ens33` 網卡 6. 很明顯此次的目標 pod 跟來源 pod 是在同一個 node,經過 cilium bpf 程式一番查找得知要傳給目標 pod 在主機側的網卡,所以 icmp echo request 封包會發送到 nginx pod 在主機側的網卡 `lxcdad8b6341b55` 7. Nginx pod 在主機側的網卡收到 icmp echo request 封包後,cilium bpf 程式 `cil_from_container` 立馬接手,執行 nginx Pod 的 Ingress Policy 檢查。 8. 檢查沒問題後,BPF 對 kernel 說:「我檢查完了,這封包沒問題,你接手處理吧!」。 9. 此時封包會送進 nginx pod 的 Linux Network Namspace,由於之前已經修改了目的地和來源的 mac address,封包會被正確路由到 nginx Pod 在 Container 側的網卡。 ![image](https://hackmd.io/_uploads/B1soBTdybl.png) 封包詳細資訊: ![image](https://hackmd.io/_uploads/rky-U6u1-x.png) 10. nginx pod 收到 icmp echo request 封包後,會需要回傳 icmp echo reply 的網路封包,同樣會因為 subnet mask 為 `/32`,網路封包一定往 Default Gateway `10.244.7.247` 送,**由於初始化 pod 內部 arp cache 為空,故又觸發 arp 通訊協定來去得到 Default Gateway 的 mac address(實際上會得到的是 nginx pod 在主機側網卡的 mac address)** (arp 詳細運作流程不再重複描述) ![image](https://hackmd.io/_uploads/BkSie0OkWx.png) 11. 透過 arp 通訊協定得知 nginx 在主機側網卡 mac 位址後,icmp echo reply 封包會發送至 nginx pod 在主機側的網卡 ![image](https://hackmd.io/_uploads/HkZV7Ruk-g.png) 封包詳細資訊: ![image](https://hackmd.io/_uploads/B1WIQCuybg.png) 12. icmp echo reply 封包傳送到在 nginx pod 主機側的網卡後,又又又被 cilium bpf 程式 `cil_from_container` 接手,並作出以下判斷 : 1. 目標 pod 是同一個 Node 2. 把目的 MAC Address 和來源的 MAC Address 的換掉,確保能送回 client pod 3. 直接將網路封包送達 client pod 在主機側的網卡 12. client pod 在主機側的網卡收到 icmp echo reply 封包後,cilium bpf 程式 `cil_from_container` 接手,執行 client Pod 的 Ingress Policy 檢查,確認沒問題後最後傳回給 client pod 在 container 側的網卡。 ![image](https://hackmd.io/_uploads/H1he8Ru1Zx.png) 封包詳細資訊: ![image](https://hackmd.io/_uploads/SynGUAd1be.png) ### 補充說明 安裝 cilium 後,會在主機的 os 裡面,看到 `cilium_host`, `cilium_net`, 和 `cilium_vxlan` 這 3 個網路介面,目前根據上面的驗證結果可得知在同節點內 pod 跟 pod 之間互相溝通,並不會經過這 3 張網卡。 如果想驗證,可在 pods 之間互 ping 之前,執行以下收集封包的命令 1. 收集 `cilium_host` 網路介面的封包 ``` sudo tcpdump -n -i cilium_host arp or icmp -vvv -w cilium_host.pcap ``` 2. 收集 `cilium_net` 網路介面的封包 ``` sudo tcpdump -n -i cilium_net arp or icmp -vvv -w cilium_net.pcap ``` 3. 收集 `cilium_vxlan` 網路介面的封包 ``` sudo tcpdump -n -i cilium_net arp or icmp -vvv -w cilium_vxlan ``` 收集結果會看到 icmp 封包,但 icmp 封包內要傳遞的 data 一定會跟 pod 之間互 ping 的 data 不一樣 ![image](https://hackmd.io/_uploads/HkmTKKtJ-e.png) 所以可以確定在同節點內 pod 跟 pod 之間互相溝通,並不會經過 `cilium_host`, `cilium_net`, 和 `cilium_vxlan` 這 3 張網卡。 ## 5. 參考文章 - [[Day 12] 探索 Cilium Pod to Pod Datapath (1) 背後竟然有 ARP 偽造?](https://ithelp.ithome.com.tw/m/articles/10387997) - [[Day 13] 探索 Cilium Pod to Pod Datapath (2) eBPF 走捷徑直接送封包到目的 Pod 面前](https://ithelp.ithome.com.tw/m/articles/10388758)