Skip to content

antony@notes:~/kubernetes$ cat "CNI-運作原理-理解-Container-Network-Interface-如何讓跨主機的-Pod-通訊.md"

CNI 運作原理:理解 Container Network Interface 如何讓跨主機的 Pod 通訊

2026-05-08· kubernetes

CNI 運作原理:理解 Container Network Interface 如何讓跨主機的 Pod 通訊

TL;DR

當 worker-1 上的 Pod A ping worker-2 上的 Pod B:

  1. Pod 內 eth0/32 IP、169.254.1.1 link-local default gateway)→ 透過 Calico CNI 建的 veth pair 出到 host netns
  2. Host netns 的 routing 表把對端 podCIDR 指向 flannel.1(VXLAN device,VNI 1)
  3. Linux kernelip neigh + bridge fdb 查到對端 underlay IP,做 VXLAN encap(UDP port 8472)
  4. 封包經 node container 的 eth0 → host bridge podman1 → 對端 node 的 eth0 → 對端 flannel.1 做 VXLAN decap → veth → Pod B eth0

Calico 負責建 veth、設 proxy_arpFlannel 負責跨 node 的 host route 與 VXLAN fdb;kernel 負責真正的 encap/decap。

不熟悉 veth pair / encap / overlay / fdb / VXLAN 等術語的讀者,建議先翻 Appendix B 術語表 一次再讀正文。


目錄


目標

在一個 1 master + 2 workers 的 Kubernetes cluster(用 Podman 建立、運作方式類似 kind),CNI 採用 Canal 的情境下,回答一個具體問題:

當 worker-1 上的 Pod A ping worker-2 上的 Pod B,封包到底走了哪些 interface、被誰 encap、又被誰 decap?

讀完本文後,應能在自己的 cluster 上用 kubectlpodman execiptcpdump 等工具,把整條 datapath 親眼看一遍。

適合對象:已熟悉 Pod / Deployment / Service 的使用者,但對 overlay network、VXLAN、CNI plugin chain 仍是黑盒子。

驗證環境:cluster tk8s(v1.35.0、container runtime containerd 2.2.0、node image quay.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 wide
NAME                 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}'; echo
NAME          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 衝突
2Pod-to-Pod 通訊不經 NATPod A 看到自己的 IP,跟 Pod B 看到 Pod A 的 IP,必須一致
3Node agent(kubelet 等)能直連該 node 上的 Podhost 和 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 CNInode 上的 /opt/cni/bin/calico binary
Overlay backend(跨 node 封裝)Flannel VXLANcanal pod 的 kube-flannel container
NetworkPolicy 執行Calico Felixcanal 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 tk8s
NETWORK 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 bridge podman1
  • 三個 node 都掛在 podman1 上、IP 落在 172.22.0.0/24
  • node 內 eth0 = 接到 podman1 的 veth;node 的 INTERNAL-IP 就是它在 bridge 上的 IP

4. 部署 demo workload

這節重點:用 podAntiAffinity 強制兩個 nicolaka/netshoot Pod 各落到一台 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 wide
deployment.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 B

Pod 名稱與 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_IP
2: 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

為什麼會這樣?

  • /32 mask: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.1 MTU 1450 → § 8 MTU mismatch 小提醒

6. 第二層觀察:node container 內看到什麼

這節重點:node netns 上有三類 interface(eth0flannel.1caliXXXX veth);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 a
lo                  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.1
5: 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 與 caliXXXX hash 每個 Pod 都不同;29 / cali58f769715fe 只是這次的值。 三類 interface 角色一覽:

Interface角色
eth0underlay 進出口;連到 host bridge podman1
flannel.1VXLAN device(VNI 1、dstport 8472、IP 為 podCIDR 首 IP)
caliXXXX每個本機 Pod 一條 veth(host 端)

6.2 Routing 表

sudo podman exec tk8s-worker1 ip route
default 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) 對外 defaultdefault via 172.22.0.254 dev eth0node 啟動時 DHCP/Podman
(b) 本機 Pod /3210.244.0.153 dev cali58f769715feCalico CNI 每建一個 Pod 寫一條
(c) 跨 node Pod CIDR10.244.1.0/25 via 10.244.1.0 dev flannel.1 onlinkFlannel daemon
(d) host underlay172.22.0.0/24 dev eth0kernel 自動

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-plane

kernel 對 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 | head
cali-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 dispatcher

7. 第三層觀察:抓真正的 VXLAN 封包

這節重點:在 host 對 podman1 bridge 抓 UDP 8472,能同時看到外層 underlay 與內層 Pod-to-Pod ICMP。

本 cluster 的 node image 沒裝 tcpdumpsudo 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_IP
listening 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.1vxlan 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 link host route
  • Flannel daemoncanal/kube-flannel container):管理 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 文件)。

#檢查項指令健康時應看到
1node 互通sudo podman exec tk8s-worker1 ping -c 2 172.22.0.30% packet loss
2podCIDR 已設kubectl get node tk8s-worker1 -o jsonpath='{.spec.podCIDR}'10.244.0.128/25
3flannel subnet見 9.3FLANNEL_SUBNET 與 podCIDR 一致
4VXLAN checksum offload bug見 9.4(只在懷疑時才下)

9.1 node 互通

sudo podman exec tk8s-worker1 ping -c 2 172.22.0.3
PING 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 1032ms

9.2 podCIDR 是否設置

kubectl get node tk8s-worker1 -o jsonpath='{.spec.podCIDR}'; echo
10.244.0.128/25

9.3 flannel 寫好的 subnet

/run/flannel/subnet.env 在 canal pod 的 kube-flannel container 內,不在 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.env
FLANNEL_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=true

9.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 off
Actual 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.yaml
deployment.apps "cni-demo" deleted from default namespace

Appendix A. 指令速查表

依「想看什麼」反查:

想看什麼在哪裡跑指令
Pod IP / MTU / MACkubectl execip a show eth0
Pod default routekubectl execip route
Pod-to-Pod 連通kubectl execping <pod B IP>
node 上所有 interfacenode containerip -br a
flannel.1 的 VXLAN 參數node containerip -d link show flannel.1
Pod A 對應的 host-side vethnode containerip link | grep -A1 ^<iflink>:
node 的 routing 表node containerip route
對端 flannel.1 IP→MACnode containerip neigh show dev flannel.1
對端 MAC→underlay IPnode containerbridge fdb show dev flannel.1
Felix iptables 痕跡node containeriptables -t filter -L | grep cali
underlay VXLAN 封包hosttcpdump -i podman1 -nn 'udp port 8472'
flannel 的 subnet.envcanal pod (kube-flannel container)cat /run/flannel/subnet.env
Podman bridge / networkhostsudo podman network inspect tk8s
node container 列表hostsudo 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 規範要求):