antony@notes:~/kubernetes$ cat "CNI-運作原理-理解-Container-Network-Interface-如何讓跨主機的-Pod-通訊.md"
CNI 運作原理:理解 Container Network Interface 如何讓跨主機的 Pod 通訊
CNI 運作原理:理解 Container Network Interface 如何讓跨主機的 Pod 通訊
TL;DR
當 worker-1 上的 Pod A ping worker-2 上的 Pod B:
- Pod 內
eth0(/32IP、169.254.1.1link-local default gateway)→ 透過 Calico CNI 建的 veth pair 出到 host netns - Host netns 的 routing 表把對端 podCIDR 指向
flannel.1(VXLAN device,VNI 1) - Linux kernel 用
ip neigh+bridge fdb查到對端 underlay IP,做 VXLAN encap(UDP port 8472) - 封包經 node container 的
eth0→ host bridgepodman1→ 對端 node 的eth0→ 對端flannel.1做 VXLAN decap → veth → Pod Beth0
Calico 負責建 veth、設 proxy_arp;Flannel 負責跨 node 的 host route 與 VXLAN fdb;kernel 負責真正的 encap/decap。
不熟悉 veth pair / encap / overlay / fdb / VXLAN 等術語的讀者,建議先翻 Appendix B 術語表 一次再讀正文。
目錄
- TL;DR
- 0. 前提條件
- 1. Kubernetes networking model 三條公約
- 2. Canal 架構快速理解
- 3. 環境探勘(Podman 特有步驟)
- 4. 部署 demo workload
- 5. 第一層觀察:Pod 內看到什麼
- 6. 第二層觀察:node container 內看到什麼
- 7. 第三層觀察:抓真正的 VXLAN 封包
- 8. Packet flow 圖
- 9. 常見驗證 checklist
- 10. 收尾
- Appendix A. 指令速查表
- Appendix B. 術語表
- References
目標
在一個 1 master + 2 workers 的 Kubernetes cluster(用 Podman 建立、運作方式類似 kind),CNI 採用 Canal 的情境下,回答一個具體問題:
當 worker-1 上的 Pod A
pingworker-2 上的 Pod B,封包到底走了哪些 interface、被誰 encap、又被誰 decap?
讀完本文後,應能在自己的 cluster 上用 kubectl、podman exec、ip、tcpdump 等工具,把整條 datapath 親眼看一遍。
適合對象:已熟悉 Pod / Deployment / Service 的使用者,但對 overlay network、VXLAN、CNI plugin chain 仍是黑盒子。
驗證環境:cluster
tk8s(v1.35.0、container runtime containerd 2.2.0、node imagequay.io/cloudwalker/taroko:v1.35.0、3 node:tk8s-control-plane/tk8s-worker1/tk8s-worker2)。其他環境若 IP/CIDR 不同,照樣換值即可。
0. 前提條件
這節重點:確認 cluster 是 1m2w、INTERNAL-IP 落在 Podman bridge 上、Canal 已安裝(每 node 一個 canal pod,內含
calico-node+kube-flannel兩個 container)。
(1) 看 cluster 拓樸:
kubectl get node -o wideNAME STATUS ROLES AGE VERSION INTERNAL-IP ...
tk8s-control-plane Ready control-plane 2d v1.35.0 172.22.0.1 ...
tk8s-worker1 Ready taroko-worker 2d v1.35.0 172.22.0.2 ... ← 等等 demo Pod A 落腳點
tk8s-worker2 Ready taroko-gateway 2d v1.35.0 172.22.0.3 ... ← Pod B 落腳點(2) 看 Canal 是否安裝、每個 canal pod 由哪些 container 組成:
kubectl get pods -n kube-system -l k8s-app=canal -o wide
kubectl get pods -n kube-system -l k8s-app=canal \
-o jsonpath='{.items[0].spec.containers[*].name}'; echoNAME READY STATUS RESTARTS AGE IP NODE
canal-4wvhj 2/2 Running 0 2d 172.22.0.1 tk8s-control-plane
canal-qsnqh 2/2 Running 0 2d 172.22.0.3 tk8s-worker2
canal-vjcm9 2/2 Running 0 2d 172.22.0.2 tk8s-worker1
calico-node kube-flannel ← 兩個 container:calico-node 跑 Felix、kube-flannel 跑 flanneld找不到 canal pod 時,改用
kubectl get pods -n kube-system | grep -E 'canal|calico|flannel'比對;不同 manifest 的 label 命名可能不同。
1. Kubernetes networking model 三條公約
這節重點:K8s 不規定怎麼做,只規定三件事必須成立;CNI plugin 的工作就是把它變成現實。
依 kubernetes.io 的規範:
| # | 公約 | 通俗說法 |
|---|---|---|
| 1 | 每個 Pod 拿到一個 cluster 內 routable 的 IP | 不用 port forwarding、不會 port 衝突 |
| 2 | Pod-to-Pod 通訊不經 NAT | Pod A 看到自己的 IP,跟 Pod B 看到 Pod A 的 IP,必須一致 |
| 3 | Node agent(kubelet 等)能直連該 node 上的 Pod | host 和 Pod 之間沒有牆 |
CNI plugin 的職責:
- IPAM:給每個 Pod 配 IP
- Plumbing:在 host 與 Pod 之間建立 veth
- Forwarding:設好 routing / encap,讓跨 node 的 Pod 互通
2. Canal 架構快速理解
這節重點:Canal = Flannel(負責 networking)+ Calico(負責 NetworkPolicy)。Calico 在這個組合下跑 policy-only mode、不跑 BGP。
| 角色 | 實作者 | 跑在哪裡 |
|---|---|---|
| CNI plugin(建立 veth、配 IP) | Calico CNI | node 上的 /opt/cni/bin/calico binary |
| Overlay backend(跨 node 封裝) | Flannel VXLAN | canal pod 的 kube-flannel container |
| NetworkPolicy 執行 | Calico Felix | canal pod 的 calico-node container |
| Routing protocol(BGP) | 不啟用 | CALICO_NETWORKING_BACKEND=none |
本文只示範 datapath 連通;NetworkPolicy 不在範圍內。
3. 環境探勘(Podman 特有步驟)
這節重點:node 是 host 上的 Podman container;
node 的 eth0=host 的 podman1 bridge 上的 veth。之後抓 underlay 封包就要看這座 bridge。本 cluster 的 Podman 跑在 rootful 模式,所以指令要加
sudo。
(1) 列出 node container:
sudo podman ps --format '{{.Names}}\t{{.Image}}\t{{.Status}}'tk8s-control-plane quay.io/cloudwalker/taroko:v1.35.0 Up 2 days
tk8s-worker1 quay.io/cloudwalker/taroko:v1.35.0 Up 2 days
tk8s-worker2 quay.io/cloudwalker/taroko:v1.35.0 Up 2 days(2) 找 nodes 共用的 Podman network:
sudo podman network ls
sudo podman network inspect tk8sNETWORK ID NAME DRIVER
2f259bab93aa podman bridge ← 預設、未使用
2543f305ded9 tk8s bridge ← cluster 真正用的[{
"name": "tk8s",
"driver": "bridge",
"network_interface": "podman1", ← 對應 host 上的 bridge 名稱
"subnets": [{
"subnet": "172.22.0.0/24",
"gateway": "172.22.0.254"
}]
// ... 其他欄位省略
}]結論(記住這三件事,後面都會用到):
- Podman network
tk8s↔ host bridgepodman1 - 三個 node 都掛在
podman1上、IP 落在172.22.0.0/24 - node 內
eth0= 接到podman1的 veth;node 的 INTERNAL-IP 就是它在 bridge 上的 IP
4. 部署 demo workload
這節重點:用
podAntiAffinity強制兩個nicolaka/netshootPod 各落到一台 worker;確認每 node 的 podCIDR 是/25切片;設好$POD_A/$POD_B等變數方便後續所有指令引用。
(1) 部署並等 Ready:
kubectl apply -f docs/manifests/cni-demo.yaml
kubectl wait --for=condition=Ready pod -l app=cni-demo --timeout=60s
kubectl get pod -l app=cni-demo -o widedeployment.apps/cni-demo created
pod/cni-demo-548d4c6cbc-bgjqk condition met
pod/cni-demo-548d4c6cbc-lmk44 condition met
NAME READY STATUS IP NODE
cni-demo-548d4c6cbc-bgjqk 1/1 Running 10.244.0.153 tk8s-worker1 ← Pod A
cni-demo-548d4c6cbc-lmk44 1/1 Running 10.244.1.22 tk8s-worker2 ← Pod BPod 名稱與 IP 每次部署都會變動;只要結構一致(兩個 replica、各落在不同 worker、IP 在各自 podCIDR 內)即正確。
(2) 看 podCIDR 切片(cluster pod CIDR 10.244.0.0/16 被切成 /25 分給每個 node):
kubectl get node -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.podCIDR}{"\n"}{end}'tk8s-control-plane 10.244.0.0/25
tk8s-worker1 10.244.0.128/25 ← Pod A 來自這個區段
tk8s-worker2 10.244.1.0/25 ← Pod B 來自這個區段(3) 設定 shell 變數(後面所有節都會用到):
POD_A=$(kubectl get pod -l app=cni-demo --field-selector spec.nodeName=tk8s-worker1 -o jsonpath='{.items[0].metadata.name}')
POD_B=$(kubectl get pod -l app=cni-demo --field-selector spec.nodeName=tk8s-worker2 -o jsonpath='{.items[0].metadata.name}')
POD_A_IP=$(kubectl get pod $POD_A -o jsonpath='{.status.podIP}')
POD_B_IP=$(kubectl get pod $POD_B -o jsonpath='{.status.podIP}')
echo "POD_A=$POD_A ($POD_A_IP @ tk8s-worker1)"
echo "POD_B=$POD_B ($POD_B_IP @ tk8s-worker2)"POD_A=cni-demo-548d4c6cbc-bgjqk (10.244.0.153 @ tk8s-worker1)
POD_B=cni-demo-548d4c6cbc-lmk44 (10.244.1.22 @ tk8s-worker2)5. 第一層觀察:Pod 內看到什麼
這節重點:Pod 內只有
eth0、IP 是/32、default gateway 是 dummy 的169.254.1.1。Pod 不知道 overlay、不知道對端 node。
kubectl exec $POD_A -- ip a show eth0
kubectl exec $POD_A -- ip route
kubectl exec $POD_A -- ping -c 3 $POD_B_IP2: eth0@if29: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ... ← MTU 1500(Calico 預設)
link/ether 32:90:8e:80:e6:7a ...
inet 10.244.0.153/32 scope global eth0 ← 注意是 /32
default via 169.254.1.1 dev eth0 ← link-local dummy gateway
169.254.1.1 dev eth0 scope link
PING 10.244.1.22 (10.244.1.22) 56(84) bytes of data.
64 bytes from 10.244.1.22: icmp_seq=1 ttl=62 time=0.614 ms ← ttl 從 64 掉到 62
64 bytes from 10.244.1.22: icmp_seq=2 ttl=62 time=0.370 ms
64 bytes from 10.244.1.22: icmp_seq=3 ttl=62 time=0.399 ms
--- 10.244.1.22 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2084ms為什麼會這樣?
/32mask:Pod 把 host 視為 point-to-point 對端,沒有 subnet 概念169.254.1.1是 Calico CNI 寫死的 dummy next hop(Calico CNI 原始碼:gw := net.IPv4(169, 254, 1, 1));host-side veth 上設定proxy_arp=1,所以 ARP 永遠回應 host MAC,然後封包就到 host netns 了- ttl 從 64 → 62 = 兩端
flannel.1各 decrement 一次(VXLAN device 跨 layer-3 forwarding) - MTU 1500 vs 後面會看到的
flannel.1MTU 1450 → § 8 MTU mismatch 小提醒
6. 第二層觀察:node container 內看到什麼
這節重點:node netns 上有三類 interface(
eth0、flannel.1、caliXXXXveth);routing / fdb / neigh 三張表加起來決定封包要怎麼走。本節指令都用
sudo podman exec tk8s-worker1 <cmd>從 host 跑;想拿 shell 互動的話,改用sudo podman exec -it tk8s-worker1 bash。
6.1 Interface 全貌
sudo podman exec tk8s-worker1 ip -br alo UNKNOWN 127.0.0.1/8 ::1/128
eth0@if5 UP 172.22.0.2/24 ... ← 接到 host podman1 的 veth
flannel.1 UNKNOWN 10.244.0.128/32 ... ← VXLAN device,IP 為 podCIDR 首 IP
cali96e9f6c0c5f@if2 UP ... ┐
cali431d010c6bb@if2 UP ... │
cali6c6a1f0801c@if2 UP ... ├ Calico CNI 為各本機 Pod 建的 veth
cali55122502246@if2 UP ... │
caliaf66b54b075@if2 UP ... │
cali58f769715fe@if2 UP ... ┘ ← 這條對應到 demo Pod A確認 VXLAN 參數:
sudo podman exec tk8s-worker1 ip -d link show flannel.15: flannel.1: ... mtu 1450 ...
link/ether e6:16:30:64:14:d3 ...
vxlan id 1 ← VNI 1
local 172.22.0.2 ← encap 後外層 src IP
dev eth0 ← 從 eth0 寄出
dstport 8472 ← Flannel VXLAN UDP port
nolearning ...定位 Pod A 對應的 host-side veth(用 ifindex 對照):
POD_A_IFLINK=$(kubectl exec $POD_A -- cat /sys/class/net/eth0/iflink)
echo "POD_A_IFLINK=$POD_A_IFLINK"
sudo podman exec tk8s-worker1 sh -c "ip link | grep -A1 \"^${POD_A_IFLINK}:\""POD_A_IFLINK=29
29: cali58f769715fe@if2: ... mtu 1500 ...
link/ether ee:ee:ee:ee:ee:ee ...
link-netns cni-d3b2a847-5b76-c640-8ffa-0e3952643e1b ← 對端 netns(Pod 的 netns)ifindex 與
caliXXXXhash 每個 Pod 都不同;29/cali58f769715fe只是這次的值。 三類 interface 角色一覽:
Interface 角色 eth0underlay 進出口;連到 host bridge podman1flannel.1VXLAN device(VNI 1、dstport 8472、IP 為 podCIDR 首 IP) caliXXXX每個本機 Pod 一條 veth(host 端)
6.2 Routing 表
sudo podman exec tk8s-worker1 ip routedefault via 172.22.0.254 dev eth0 proto static metric 100 ← (a) 對外 default
10.244.0.0/25 via 10.244.0.0 dev flannel.1 onlink ← (c) 跨 node:到 control-plane podCIDR
10.244.0.130 dev cali96e9f6c0c5f scope link ┐
10.244.0.131 dev cali431d010c6bb scope link │
10.244.0.132 dev cali6c6a1f0801c scope link │
10.244.0.136 dev cali55122502246 scope link ├ (b) 本機 Pod:每個 /32 走對應 cali veth
10.244.0.138 dev caliaf66b54b075 scope link │
10.244.0.153 dev cali58f769715fe scope link ┘ ← demo Pod A
10.244.1.0/25 via 10.244.1.0 dev flannel.1 onlink ← (c) 跨 node:到 worker2 podCIDR ★
172.22.0.0/24 dev eth0 proto kernel scope link src 172.22.0.2 ← (d) host underlay subnet封包送往 Pod B(10.244.1.22)時,命中第 (c) ★ 條規則:
- next-hop 是
10.244.1.0—— 正好是對端 worker2 上flannel.1的 IP onlink告訴 kernel:「直接視為同一鏈路」,不必再對 next-hop 做 ARP- 出 device 是本機
flannel.1—— 等於告訴 kernel「這條要做 VXLAN encap」
| 類型 | 範例 | 由誰寫的 |
|---|---|---|
| (a) 對外 default | default via 172.22.0.254 dev eth0 | node 啟動時 DHCP/Podman |
| (b) 本機 Pod /32 | 10.244.0.153 dev cali58f769715fe | Calico CNI 每建一個 Pod 寫一條 |
| (c) 跨 node Pod CIDR | 10.244.1.0/25 via 10.244.1.0 dev flannel.1 onlink | Flannel daemon |
| (d) host underlay | 172.22.0.0/24 dev eth0 | kernel 自動 |
6.3 VXLAN forwarding database 與 neighbor
sudo podman exec tk8s-worker1 bridge fdb show dev flannel.1
sudo podman exec tk8s-worker1 ip neigh show dev flannel.1# bridge fdb show dev flannel.1 ← 對端 flannel.1 MAC → 對端 underlay IP
d2:41:35:4c:5e:90 dst 172.22.0.3 self permanent ← worker2
d6:e5:aa:e1:8a:0e dst 172.22.0.1 self permanent ← control-plane
# ip neigh show dev flannel.1 ← 對端 flannel.1 IP → 對端 flannel.1 MAC
10.244.1.0 lladdr d2:41:35:4c:5e:90 PERMANENT ← worker2
10.244.0.0 lladdr d6:e5:aa:e1:8a:0e PERMANENT ← control-planekernel 對 Pod B 做 VXLAN encap 的四步查表:
Step 1 ip route 10.244.1.22 命中 10.244.1.0/25 via 10.244.1.0 dev flannel.1 onlink
Step 2 ip neigh 10.244.1.0 → MAC d2:41:35:4c:5e:90
Step 3 bridge fdb MAC d2:41:35:4c:5e:90 → underlay dst 172.22.0.3
Step 4 build VXLAN packet outer = UDP 172.22.0.2:?? → 172.22.0.3:8472, VNI 1
inner = original ICMP (10.244.0.153 → 10.244.1.22)
送出 device = eth0這兩張表是 Flannel daemon 透過 Kubernetes API 蒐集各 node 的
flannel.alpha.coreos.com/*annotation 後,主動寫到 kernel 的(PERMANENT條目)—— 完全不需要 multicast、不需要 BGP。
6.4 Calico Felix 的痕跡
即使沒有 NetworkPolicy,Felix 也會建立基礎 chain 結構。本文不展開、只示範它存在。
sudo podman exec tk8s-worker1 iptables -t filter -L | grep -i cali | headcali-INPUT ... /* cali:Cz_u1IQiXIMmKD4c */ ┐
cali-FORWARD ... /* cali:wUHhoiAYhphO9Mso */ ├ Felix 的三個 hook chain
cali-OUTPUT ... /* cali:tVnHkvAo15HuiPy0 */ ┘
ACCEPT ... /* Policy explicitly accepted packet. */ mark match 0x10000/0x10000
MARK ... MARK or 0x10000
MARK ... MARK and 0xffe5ffff
cali-from-hep-forward ... ← host endpoint forward dispatcher
cali-from-wl-dispatch ... ← workload (Pod) ingress dispatcher
cali-to-wl-dispatch ... ← workload (Pod) egress dispatcher7. 第三層觀察:抓真正的 VXLAN 封包
這節重點:在 host 對
podman1bridge 抓 UDP 8472,能同時看到外層 underlay 與內層 Pod-to-Pod ICMP。本 cluster 的 node image 沒裝
tcpdump(sudo podman exec tk8s-worker1 which tcpdump回 missing),所以直接在 host 抓最快。
開兩個 terminal:
# Terminal 1:host 上對 podman1 抓 UDP 8472(用 host filter 收緊噪音)
sudo tcpdump -i podman1 -nn -c 8 \
'udp port 8472 and host 172.22.0.2 and host 172.22.0.3'
# Terminal 2:發 ping
kubectl exec $POD_A -- ping -c 4 $POD_B_IPlistening on podman1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
11:40:13.754197 IP 172.22.0.2.60047 > 172.22.0.3.8472: OTV, flags [I] (0x08), overlay 0, instance 1
IP 10.244.0.153 > 10.244.1.22: ICMP echo request, id 9, seq 1, length 64
11:40:13.754358 IP 172.22.0.3.60047 > 172.22.0.2.8472: OTV, flags [I] (0x08), overlay 0, instance 1
IP 10.244.1.22 > 10.244.0.153: ICMP echo reply, id 9, seq 1, length 64
... (其餘略)逐欄對照:
| 欄位 | 意義 |
|---|---|
IP 172.22.0.2.60047 > 172.22.0.3.8472 | 外層:兩個 worker 在 podman1 上的 IP;dst port 8472 = Flannel VXLAN |
OTV, flags [I] (0x08) | tcpdump 預設把 UDP 8472 顯示為 OTV(同 port、格式相容);要強制顯示 VXLAN 加 -T vxlan |
instance 1 | 即 VNI 1(與 ip -d link show flannel.1 的 vxlan id 1 一致) |
內層 IP 10.244.0.153 > 10.244.1.22: ICMP echo request | 原始 Pod-to-Pod ICMP,被 VXLAN 包在裡面 |
8. Packet flow 圖
這節重點:把前面三節學到的 interface / route / fdb / encap 串成一張圖。
worker-1 (Podman container, 172.22.0.2) worker-2 (Podman container, 172.22.0.3)
+-------------------------------+ +-------------------------------+
| Pod A netns | | Pod B netns |
| +------+ | | +------+ |
| | eth0 | 10.244.0.153/32 | | 10.244.1.22/32| eth0 | |
| | mtu | 1500 | | 1500| mtu | |
| +--+---+ | | +---+--+ |
| | (veth pair) | | (veth pair) | |
| +--+--------------+ | | +--------------+--+ |
| | cali58f769715fe | ← Calico | | Calico → | caliYYYYYYYYY | |
| | proxy_arp=1 | CNI | | CNI | proxy_arp=1 | |
| +-------+---------+ 建立 | | 建立 +---------+------+ |
| | | | | |
| [host route, 在 node netns] | | [host route, 在 node netns] |
| 10.244.1.0/25 via 10.244.1.0 | | 10.244.0.128/25 via ... |
| dev flannel.1 onlink | | dev flannel.1 onlink |
| | | | | |
| +-------v-----+ | | +-----v-------+ |
| | flannel.1 | VXLAN encap | | VXLAN decap | flannel.1 | |
| | id 1 | (in-kernel) | | (in-kernel) | id 1 | |
| | 10.244.0.128| MTU 1450 | | MTU 1450 | 10.244.1.0 | |
| +-------+-----+ | | +-----+-------+ |
| | | | | |
| +-------v-----+ UDP 8472 | | UDP 8472 +--v-------+ |
| | eth0 | src 172.22.0.2 | dst 172.22.0.3 | eth0 | |
| | (veth) | | | | (veth) | |
| +-------+-----+ | | +----+-----+ |
+----------|--------------------+ +----------------------|-------+
| |
| +------------------------------+ |
+--------->| host bridge: podman1 |<---------------+
| (Podman network "tk8s") |
| subnet 172.22.0.0/24 |
+------------------------------+
(L2 forwarding;這裡看到的是 underlay UDP 封包)各步驟責任歸屬:
- Calico CNI:建立 Pod 的 veth pair、設定 host-side
caliXXXX、加上proxy_arp=1、寫<pod-ip>/32 dev caliXXXX scope linkhost route - Flannel daemon(
canal/kube-flannelcontainer):管理flannel.1的存在、寫入跨 node 的 host route 與 VXLAN fdb / neigh - Linux kernel:實際做 VXLAN encap/decap、ARP 與 routing;fdb / neigh 查表
- host 的 podman1 bridge:純 L2 forwarding,把 underlay UDP 在 nodes 之間搬
MTU mismatch 小提醒
- Pod eth0 MTU = 1500(Calico CNI 預設)
- flannel.1 MTU = 1450(VXLAN header 50 bytes)
- podman1 bridge MTU = 1500
意思是:Pod 直接送一個 1500 byte 的 packet,到了 flannel.1 因 MTU 較小,要嘛被分片、要嘛觸發 ICMP「Fragmentation Needed」回去做 PMTUD。本 demo 用 ping(小 packet)看不到問題;做大量 TCP 流量時會用 PMTUD 收斂到 1450。如要徹底避免這個 mismatch,可以在 Calico install spec 顯式把 mtu 設成 1450。
9. 常見驗證 checklist
這節重點:ping 不通時按以下順序排查(依 Flannel troubleshooting 文件)。
| # | 檢查項 | 指令 | 健康時應看到 |
|---|---|---|---|
| 1 | node 互通 | sudo podman exec tk8s-worker1 ping -c 2 172.22.0.3 | 0% packet loss |
| 2 | podCIDR 已設 | kubectl get node tk8s-worker1 -o jsonpath='{.spec.podCIDR}' | 10.244.0.128/25 |
| 3 | flannel subnet | 見 9.3 | FLANNEL_SUBNET 與 podCIDR 一致 |
| 4 | VXLAN checksum offload bug | 見 9.4 | (只在懷疑時才下) |
9.1 node 互通
sudo podman exec tk8s-worker1 ping -c 2 172.22.0.3PING 172.22.0.3 (172.22.0.3) 56(84) bytes of data.
64 bytes from 172.22.0.3: icmp_seq=1 ttl=64 time=0.308 ms
64 bytes from 172.22.0.3: icmp_seq=2 ttl=64 time=0.236 ms
--- 172.22.0.3 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1032ms9.2 podCIDR 是否設置
kubectl get node tk8s-worker1 -o jsonpath='{.spec.podCIDR}'; echo10.244.0.128/259.3 flannel 寫好的 subnet
/run/flannel/subnet.env在 canal pod 的kube-flannelcontainer 內,不在 node container 的 host 檔案系統。
CANAL_POD=$(kubectl get pod -n kube-system -l k8s-app=canal \
--field-selector spec.nodeName=tk8s-worker1 \
-o jsonpath='{.items[0].metadata.name}')
kubectl exec -n kube-system $CANAL_POD -c kube-flannel -- cat /run/flannel/subnet.envFLANNEL_NETWORK=10.244.0.0/16
FLANNEL_SUBNET=10.244.0.129/25 ← 與 podCIDR 10.244.0.128/25 對應(差別在 .128 vs .129:podCIDR 用 network address,flannel 用第一個 host IP)
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true9.4 VXLAN checksum offload(只在懷疑時才下)
抓得到 underlay UDP 8472 卻看不到 inner packet 時,可能是 VXLAN checksum offload bug:
sudo podman exec tk8s-worker1 ethtool -K flannel.1 tx-checksum-ip-generic offActual changes:
tx-checksum-ip-generic: off
tx-tcp-segmentation: off [not requested]
tx-tcp-ecn-segmentation: off [not requested]
tx-tcp-mangleid-segmentation: off [not requested]
tx-tcp6-segmentation: off [not requested]本 cluster 無此問題;上面只是示範指令在這個環境跑得起來。如不需要,請用
... on改回。
10. 收尾
kubectl delete -f docs/manifests/cni-demo.yamldeployment.apps "cni-demo" deleted from default namespaceAppendix A. 指令速查表
依「想看什麼」反查:
| 想看什麼 | 在哪裡跑 | 指令 |
|---|---|---|
| Pod IP / MTU / MAC | kubectl exec | ip a show eth0 |
| Pod default route | kubectl exec | ip route |
| Pod-to-Pod 連通 | kubectl exec | ping <pod B IP> |
| node 上所有 interface | node container | ip -br a |
| flannel.1 的 VXLAN 參數 | node container | ip -d link show flannel.1 |
| Pod A 對應的 host-side veth | node container | ip link | grep -A1 ^<iflink>: |
| node 的 routing 表 | node container | ip route |
| 對端 flannel.1 IP→MAC | node container | ip neigh show dev flannel.1 |
| 對端 MAC→underlay IP | node container | bridge fdb show dev flannel.1 |
| Felix iptables 痕跡 | node container | iptables -t filter -L | grep cali |
| underlay VXLAN 封包 | host | tcpdump -i podman1 -nn 'udp port 8472' |
| flannel 的 subnet.env | canal pod (kube-flannel container) | cat /run/flannel/subnet.env |
| Podman bridge / network | host | sudo podman network inspect tk8s |
| node container 列表 | host | sudo podman ps |
Appendix B. 術語表
B.1 Linux kernel networking
network namespace(netns) kernel 提供的隔離機制;每個 netns 有自己獨立的一套 interface、routing table、iptables、socket。每個 Pod 跑在自己的 netns 裡,看不到 host 與其他 Pod 的網路狀態。Node 本身(不論是真機還是 Podman container)也是一個 netns。
veth pair(virtual Ethernet pair)
kernel 提供的成對 virtual interface,行為像「一條虛擬網線」:從一端送進去的封包會從另一端原樣出現。常用法是把一端放在 Pod netns、另一端放在 host netns,當作 Pod 與 host 之間的連線。Calico CNI 對每個 Pod 建一條:host 端 = caliXXXXXXXXXX(host netns)、pod 端 = eth0(Pod netns)。
bridge(Linux bridge)
kernel 內建的 software switch,把多個 interface 聚成一個 L2 廣播域、做 MAC learning 與 forwarding。本文裡 host 上的 podman1 就是一個 bridge,連著三個 node container 的 veth。
MAC address L2 layer 的 6-byte 位址;同一個 L2 廣播域內用 ARP 由 IP 查到 MAC。
ARP(Address Resolution Protocol)/ neighbor table
IPv4 用 ARP 廣播「誰有這個 IP?」來解析 IP→MAC;kernel 把結果快取在 neighbor table,用 ip neigh 看。
proxy ARP
Host 收到 ARP request 時,雖然該 IP 不是自己也回應「我來」,引導對方把封包送過來、再由自己 forward。Calico 在每條 host-side veth 上設 /proc/sys/net/ipv4/conf/<veth>/proxy_arp = 1,所以 Pod 對 dummy gateway 169.254.1.1 發 ARP 永遠收到 host MAC,封包就被導進 host netns 處理。
routing table / next-hop
kernel 決定「封包該從哪個 interface 出、下一跳給誰」的查表結構;用 ip route 看。Next-hop 是命中該條後送往的下一個 IP:對端在同一鏈路時 = 目的 IP;跨 layer-3 時 = gateway。
onlink(route flag)
強制 kernel 把 next-hop 視為「同一鏈路」、不再去 ARP 解析它。Flannel 寫的跨 node 路由 10.244.1.0/25 via 10.244.1.0 dev flannel.1 onlink 用這個 flag——對端 flannel.1 的 IP 雖然不在自己 subnet 內,但因為走 VXLAN,下一跳由 fdb / neigh 表處理而非 ARP。
TTL(Time To Live)
IPv4 header 的 8-bit 計數器,每經過一個 router 減 1,到 0 就丟掉並回 ICMP Time Exceeded。送出端通常從 64 起算;本文 ping 看到 ttl=62 表示中間經過兩跳(兩端 flannel.1 各一次)。
MTU(Maximum Transmission Unit)/ PMTUD Interface 一次能送的最大 L3 封包大小。乙太網典型 1500 byte;VXLAN 因要加 50 byte header(IP 20 + UDP 8 + VXLAN 8 + 內層 Ethernet 14),inner MTU 通常 = underlay MTU − 50。當 sender 送了超過 path 上某段 MTU 的封包,DF flag 為 1 時 router 會丟封包並回 ICMP “Fragmentation Needed”,sender 據此調整——這個過程叫 Path MTU Discovery (PMTUD)。
B.2 Encapsulation 與 overlay 網路
encapsulation(encap)/ decapsulation(decap) encap 就是「把一個完整的封包當成 payload,包進另一個封包裡」,外層 header 的 src/dst 是傳輸路徑用的位址,內層才是原始封包;decap 就是反向取出內層。VXLAN 是其中一種 encap:把整個 L2 frame 包進 UDP。
underlay network / overlay network Underlay 是底下「真的能 routable / switchable 的網路」;overlay 是邏輯上跑在 underlay 上的虛擬網路。本文中:
- underlay = host bridge
podman1+ 三個 node 的172.22.0.0/24 - overlay =
10.244.0.0/16的 Pod IP 空間,靠 VXLAN 在 underlay 上重建
Pod 不知道 overlay 是「假的」;underlay 不知道 Pod 的存在。中間靠 flannel.1 做翻譯。
VXLAN(Virtual Extensible LAN)
RFC 7348 定義的 overlay 協定,把 L2 frame 包進 UDP(標準 port 4789;Flannel 為了相容歷史用 8472)。Linux kernel 支援,建一條 flannel.1 這種 VXLAN device 即可:對它送的封包會自動 encap、收到的會自動 decap。
VNI(VXLAN Network Identifier) 24-bit ID,類似 VLAN tag 但容量大得多;不同 VNI 的封包互相不可見。Flannel 預設 VNI = 1。
fdb(forwarding database)
Bridge / VXLAN device 的 MAC→出口對應表。一般 bridge 是 MAC→bridge port;VXLAN device 是 MAC→underlay IP(kernel 知道要把這個 MAC 的封包 encap 後送去哪個 underlay endpoint)。本文 bridge fdb show dev flannel.1 看到的 dst 172.22.0.3 就是「對端 flannel.1 的 MAC 對應到對端 node 的 underlay IP」。
B.3 Kubernetes / CNI 元件
CNI(Container Network Interface)
CNI spec:kubelet 每次起 / 停一個 Pod 時,按 spec 呼叫指定的 binary(如 /opt/cni/bin/calico),由它負責建 veth、配 IP、寫 host route。
IPAM(IP Address Management) 配 Pod IP 的子系統。CNI plugin 可以自己做 IPAM,也可以呼叫獨立 IPAM plugin。Calico 在 Canal 模式中自己做 IPAM。
podCIDR
Node 物件 .spec.podCIDR,給每個 node 預先切好的 Pod IP 段。本 cluster 是 cluster pod CIDR 10.244.0.0/16 → 每 node 切 /25。
NetworkPolicy K8s API 物件,宣告「哪些 Pod 可以連到哪些 Pod / port」。執行靠 CNI 端的 controller,例如 Calico Felix 把它翻成 iptables / eBPF rules。本文不示範。
Calico Felix Calico 的 per-node agent,負責 program iptables / eBPF rules(執行 NetworkPolicy)以及維護 endpoint state。
Flannel daemon(flanneld)
Per-node agent,watch K8s API 收集各 node 的 podCIDR、flannel.alpha.coreos.com/* annotation,然後在本機建 flannel.1 VXLAN device、寫 host route、寫 VXLAN fdb / neigh entry。
Calico CNI plugin
被 kubelet 呼叫的 binary(不是常駐 daemon),負責 Pod 啟動時建 veth pair、配 Pod IP、寫 host-side <pod-ip>/32 dev caliXXXX 路由、設 proxy_arp。
Canal
把 Calico(policy)+ Flannel(networking)兩個 project 的 manifest 包成一份 install。Calico 在這個組合下跑 policy-only mode(CALICO_NETWORKING_BACKEND=none),所以 Calico 的 BIRD(BGP daemon)不啟用、改由 Flannel 的 VXLAN 負責跨 node 連線。
BGP(Border Gateway Protocol) Internet 的 routing protocol,Calico 預設用它讓各 node 互相 announce 自己的 podCIDR、不需要 overlay encap。Canal 模式下 BGP 停用、改用 Flannel VXLAN,所以本文看不到 BGP 相關痕跡。
References
技術細節皆引自下列官方來源(依 CLAUDE.md 規範要求):
- Kubernetes networking model:https://kubernetes.io/docs/concepts/cluster-administration/networking/
- Canal(Calico + Flannel):https://docs.tigera.io/calico/latest/getting-started/kubernetes/flannel/install-for-flannel
- Calico architecture(Felix 等元件職責):https://docs.tigera.io/calico/latest/reference/architecture/overview
- Flannel backends(VXLAN VNI、UDP 8472):https://github.com/flannel-io/flannel/blob/master/Documentation/backends.md
- Flannel troubleshooting:https://github.com/flannel-io/flannel/blob/master/Documentation/troubleshooting.md
- Calico CNI dataplane source(169.254.1.1 gateway、proxy_arp):https://github.com/projectcalico/calico/blob/master/cni-plugin/pkg/dataplane/linux/dataplane_linux.go