これはなに
自宅 Kubernetes に worker node を1台追加したときのメモ。
既存の Kubernetes node は server network 側、追加した worker は別 network 側にあった。
Tailscale の subnet router を使って site-to-site networking 的につなぎ、routing table を設定した。
node は Ready になる。RKE2 agent も起動する。API server にもつながる。
しかし、Pod から DNS が引けなかった…。
nslookup kubernetes.default.svc.cluster.local 10.96.0.10 ;; connection timed out; no servers could be reached
最終的には、route でも Tailscale ACL でも kube-proxy でもなく、通信経路は正しく設定できているが、flannel.1 の checksum offload に問題があることが判明した。
sudo ethtool -K flannel.1 tx-checksum-ip-generic off
これで直った。
ただし、そこにたどり着くまでにかなり遠回りした。せっかくなので、どうトラブルシュートしたかをまとめておく。
構成
ざっくりこういう構成。
Network topology with example IP ranges
router-01
LAN gateway: 192.0.2.1/24
server gateway: 198.51.100.1/24
interconnect gw: 203.0.113.1/24
└── switch-01
├── server / Kubernetes underlay segment
│ network: 198.51.100.0/24
│ pod CIDR: 10.244.0.0/16
│ service CIDR: 10.96.0.0/12
│
│ └── hypervisor-a / Proxmox
│ ├── cp-01 198.51.100.11 podCIDR 10.244.0.0/24
│ ├── cp-02 198.51.100.12 podCIDR 10.244.1.0/24
│ ├── cp-03 198.51.100.13 podCIDR 10.244.3.0/24
│ ├── worker-01 198.51.100.21 podCIDR 10.244.4.0/24
│ ├── worker-02 198.51.100.22 podCIDR 10.244.5.0/24
│ ├── worker-03 198.51.100.23 podCIDR 10.244.6.0/24
│ └── site-router-01
│ home-side: 192.0.2.15
│ server-side: 198.51.100.30
│ Tailscale IP: 100.64.10.15
│
└──── 2.5GbE hub
└── edge-host-01 / VMware
└── edge-worker-01
home-side: 192.0.2.14
Tailscale IP: 100.64.10.14
podCIDR: 10.244.2.0/24
Kubernetes 側は RKE2。CNI は Canal なので、Pod-to-Pod networking は Flannel VXLAN、NetworkPolicy まわりは Calico という構成になる。
edge-worker-01 は 192.0.2.1/24 側にいるので、198.51.100.0/28 や 198.51.100.16/28 へ出るには site-router-01 を next hop にする。
ip route replace 198.51.100.0/28 via 192.0.2.15 dev ens32 src 192.0.2.14 ip route replace 198.51.100.16/28 via 192.0.2.15 dev ens32 src 192.0.2.14
反対側、つまり server network 側の node から 192.0.2.14 へ戻る route も必要になる。
ip route replace 192.0.2.14/32 via 198.51.100.253
ここまでで host level の通信は動く。edge-worker-01 は cluster に join するし、kubectl get nodes でも Ready になる。
しかし Pod から DNS は引けない。
まず route と ACL を疑う
最初に疑ったのは Tailscale の subnet route と ACL。
今回の構成では Tailscale の subnet router で site-to-site networking をしていた。
Tailscale の subnet router は、tailnet と物理 subnet の間に置く gateway として使うもので、Tailscale を直接入れていない機器や network にも到達できるようになる。
基本的な考え方はこのあたり。
https://tailscale.com/docs/features/subnet-routers https://tailscale.com/docs/features/site-to-site
この時点では、「Kubernetes の制御通信は通るけど Pod DNS は通らない」という現象を見て、Tailscale policy file の書き方が悪いのでは、と思っていた。
なので ACL を整理した。Kubernetes control plane と Flannel VXLAN を同じものとして扱わないようにした。
/* K8S flannel VXLAN overlay */
{
"action": "accept",
"src": ["ipset:k8s-vxlan-home-client-01s"],
"proto": "udp",
"dst": ["ipset:k8s-vxlan-native-peers:8472"]
},
{
"action": "accept",
"src": ["ipset:k8s-vxlan-native-peers"],
"proto": "udp",
"dst": ["ipset:k8s-vxlan-home-client-01s:8472"]
}
control plane 側は別にする。
/* K8S control plane / kubelet */
{
"action": "accept",
"src": ["edge-worker-01-home"],
"dst": ["ipset:k8s-control-plane-native:6443,9345"]
},
{
"action": "accept",
"src": ["ipset:k8s-control-plane-native"],
"dst": ["edge-worker-01-home:10250"]
}
わかりやすくなったのでこの整理自体は良かったが、これで直ったわけではない。
Service ではなく Pod-to-Pod networking を見る
確認用 Pod を edge-worker-01 に固定して、Service VIP と CoreDNS Pod IP の両方を直接引いてみる。 Service VIP だけを見ると kube-proxy や Service 周りの問題と混ざるので、CoreDNS の Pod IP も指定する。これで、Service ClusterIP の問題なのか、Pod-to-Pod の overlay network の問題なのかを分けられる。
POD="edge-worker-dnscheck-$(date +%s)"
NS="debug-jobs"
NODE="edge-worker-01"
printf '%s\n' \
'apiVersion: v1' \
'kind: Pod' \
'metadata:' \
" name: ${POD}" \
" namespace: ${NS}" \
'spec:' \
" nodeName: ${NODE}" \
' restartPolicy: Never' \
' containers:' \
' - name: dnscheck' \
' image: busybox:1.36.1' \
' command:' \
' - sh' \
' - -c' \
' - |' \
' set -x' \
' date -u' \
' cat /etc/resolv.conf' \
' nslookup kubernetes.default.svc.cluster.local 10.96.0.10' \
' nslookup kubernetes.default.svc.cluster.local 10.244.0.11' \
' nslookup kubernetes.default.svc.cluster.local 10.244.3.37' \
' nslookup example.com 10.96.0.10' \
| kubectl apply -f -
for i in $(seq 1 60); do
PHASE="$(
kubectl -n "${NS}" get pod "${POD}" \
-o jsonpath='{.status.phase}' 2>/dev/null || true
)"
echo "phase=${PHASE}"
if [ "${PHASE}" = "Succeeded" ] || [ "${PHASE}" = "Failed" ]; then
break
fi
sleep 1
done
kubectl -n "${NS}" get pod "${POD}" -o wide
kubectl -n "${NS}" logs "${POD}" --timestamps
kubectl -n "${NS}" delete pod "${POD}" --wait=false
10.96.0.10 は CoreDNS の Service ClusterIP。10.244.0.11 と 10.244.3.37 は CoreDNS の実 Pod IP。
結果は全部 timeout。
Service ClusterIP だけ落ちるなら kube-proxy を見る。しかし CoreDNS Pod IP を直接指定しても落ちるので、Service proxy ではなく Pod-to-Pod networking の問題と見てよい。
RKE2 の要件にも、Flannel VXLAN を使う場合は node 間で UDP/8472 が通る必要がある、とある。
https://docs.rke2.io/install/requirements
つまり edge-worker-01 と cp-01/02 の間で、Flannel VXLAN の outer packet が通っているかを見る必要がある。
flannel がどの IP を使っているか
kubectl get nodes の annotation で、Flannel の public-ip と podCIDR を見る。
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.annotations.flannel\.alpha\.coreos\.com/public-ip}{"\t"}{.spec.podCIDR}{"\n"}{end}'
整理するとこうだった。
cp-01 flannel public-ip=198.51.100.11 podCIDR=10.244.0.0/24 cp-03 flannel public-ip=198.51.100.13 podCIDR=10.244.3.0/24 edge-worker-01 flannel public-ip=192.0.2.14 podCIDR=10.244.2.0/24
次に、control plane 側で edge-worker-01 の PodCIDR がどこへ向いているかを見る。
ip route | grep '10.244.2.0/24' sudo bridge fdb show dev flannel.1 | grep '192\.0\.2\.14'
10.244.2.0/24 via 10.244.2.0 dev flannel.1 onlink 02:00:4d:d0:35:00 dst 192.0.2.14 self permanent
つまり、10.244.2.0/24 宛の traffic は flannel.1 に入り、VXLAN の outer destination として 192.0.2.14 が使われる。
raw UDP は通る
次に UDP/8472 自体が通るかを確認した。
master 側から edge-worker に向けて短い UDP packet を投げたり、その逆を試した。tcpdump で site-router-01 の ens18 / ens20 と、master 側の ens18 を見る。
raw UDP は通った。戻り方向も通った。
これは重要で、少なくとも次の仮説はかなり弱くなった。
- route が全面的におかしい
site-router-01の forwarding が死んでいる- Proxmox bridge が単純に UDP/8472 を落としている
- INPUT chain で UDP/8472 が単純に落ちている
- Tailscale ACL で落ちている
ところが、Pod DNS はまだ落ちる。
この時点で「UDP/8472 が通るなら Flannel VXLAN も通るはず」と考えたくなるが、そうではなかった。raw UDP と Flannel が生成する正規の VXLAN frame は違う。
raw UDP test は tcpdump ではこう見えた。
OTV, flags [.] (0x76), overlay 7892065, instance 7220592
一方、Flannel VXLAN はこう見える。
OTV, flags [I] (0x08), overlay 0, instance 1
tcpdump が OTV と表示しているのはさておき、見るべきは flags [I] と instance 1 のほう。
本物の VXLAN packet を見る
edge-worker-01 上で tcpdump する。
sudo tcpdump -tttt -vvv -nni ens32 \ 'udp dst port 8472 and src host 192.0.2.14 and (dst host 198.51.100.11 or dst host 198.51.100.13)'
Pod から CoreDNS に問い合わせると、こういう packet が出る。
192.0.2.14.64625 > 198.51.100.11.8472: [bad udp cksum ...] OTV, flags [I] (0x08), overlay 0, instance 1 IP 10.244.2.58 > 10.244.0.11.53: A? kubernetes.default.svc.cluster.local.
198.51.100.13 / 10.244.3.37 宛も同じように出る。
site-router-01 の ens18 でも見える。ens20 でも見える。つまり、edge-worker から出た正規 VXLAN packet は subnet router を通過している。
ところが master 側では、同じ条件で 0 packets だった。
sudo tcpdump -tttt -vvv -nni ens18 \ 'udp dst port 8472 and src host 192.0.2.14 and dst host 198.51.100.11'
0 packets captured
raw UDP は見えるのに、Flannel の正規 VXLAN は見えない。
この時点でだいぶ嫌な予感がしてくる。route ではない。ACL でもない。packet の種類によって挙動が違う。
ログに出ている bad udp cksum を疑うことにした。
bad udp cksum と checksum offload
送信側の tcpdump で bad udp cksum が出るのは、それだけならよくある。NIC の checksum offload が有効な場合、tcpdump は checksum がまだ完成していない packet を見ることがある。
しかし今回は、edge-worker だけではなく、site-router-01 の ens18 / ens20 でも bad udp cksum が見えていた。raw UDP は通るが、正規の Flannel VXLAN だけが落ちる。しかも DNS は完全に timeout する。
このへんは自分だけで見ていたら、たぶん route と firewall を延々といじっていた気がする。AI に「raw UDP は通るが正規 VXLAN だけ落ちる」という形で投げたら、checksum/offload の線がかなり濃いという返しになった。正直ここでようやく flannel.1 側を疑う発想になった。
Flannel の troubleshooting にもこの回避策がある。
https://github.com/flannel-io/flannel/blob/master/Documentation/troubleshooting.md
VMware と Flannel の組み合わせで flannel.1 の checksum offload を切る話も出てくる。
https://community.replicated.com/t/flannel-with-vmware/1396
直す
edge-worker-01 でこれを実行する。
sudo ethtool -k flannel.1 | grep -E 'tx-checksum|tx-checksum-ip-generic|tcp-segmentation|generic-segmentation' sudo ethtool -K flannel.1 tx-checksum-ip-generic off sudo ethtool -k flannel.1 | grep -E 'tx-checksum|tx-checksum-ip-generic|tcp-segmentation|generic-segmentation'
実行前はこう。
tx-checksumming: on tx-checksum-ip-generic: on
実行後はこう。
Actual changes: tx-checksum-ip-generic: off tx-tcp-segmentation: off [not requested] ... tx-checksumming: off tx-checksum-ip-generic: off
その後、同じ DNS check を再実行する。
nslookup kubernetes.default.svc.cluster.local 10.96.0.10 nslookup kubernetes.default.svc.cluster.local 10.244.0.11 nslookup kubernetes.default.svc.cluster.local 10.244.3.37 nslookup artifact-bucket.example.com 10.96.0.10
今度は全部通った。
Server: 10.96.0.10 Name: kubernetes.default.svc.cluster.local Address: 10.96.0.1 Server: 10.244.0.11 Name: kubernetes.default.svc.cluster.local Address: 10.96.0.1 Server: 10.244.3.37 Name: kubernetes.default.svc.cluster.local Address: 10.96.0.1
外部名も引ける。
artifact-bucket.example.com canonical name = object-storage.example.net
phase=Succeeded になった。これで今回の DNS 問題は直った。
永続化
flannel.1 は Canal/flannel によって作られる interface なので、単に boot 時に一回叩けばOKとは限らない。interface 作成時にも適用されるようにしておく。
edge-worker-01 に script を置く。
sudo tee /usr/local/sbin/disable-flannel1-tx-checksum.sh >/dev/null <<'SCRIPT' #!/bin/sh set -eu if ip link show flannel.1 >/dev/null 2>&1; then /usr/sbin/ethtool -K flannel.1 tx-checksum-ip-generic off fi SCRIPT sudo chmod +x /usr/local/sbin/disable-flannel1-tx-checksum.sh
udev rule。
sudo tee /etc/udev/rules.d/90-flannel1-tx-checksum.rules >/dev/null <<'RULE' SUBSYSTEM=="net", ACTION=="add|change|move", KERNEL=="flannel.1", RUN+="/usr/local/sbin/disable-flannel1-tx-checksum.sh" RULE sudo udevadm control --reload
保険で systemd service も置く。
sudo tee /etc/systemd/system/disable-flannel1-tx-checksum.service >/dev/null <<'UNIT' [Unit] Description=Disable tx-checksum-ip-generic on flannel.1 After=network-online.target rke2-agent.service Wants=network-online.target [Service] Type=oneshot ExecStart=/bin/sh -c 'for i in $(seq 1 60); do if ip link show flannel.1 >/dev/null 2>&1; then exec /usr/local/sbin/disable-flannel1-tx-checksum.sh; fi; sleep 1; done; exit 1' RemainAfterExit=yes [Install] WantedBy=multi-user.target UNIT sudo systemctl daemon-reload sudo systemctl enable --now disable-flannel1-tx-checksum.service
確認。
sudo ethtool -k flannel.1 | grep -E 'tx-checksumming|tx-checksum-ip-generic'
期待値。
tx-checksumming: off tx-checksum-ip-generic: off
わかったこと
今回の問題は、最初に見えていたより layer が多かった。
- Tailscale subnet router による site-to-site routing
- Linux の routing table / policy routing
- RKE2 agent の join
- Flannel VXLAN の UDP/8472
- Calico の iptables/nftables rule
flannel.1の checksum offload- VMware / Proxmox / 物理 network
このうち、最初に疑ったのは Tailscale ACL と route だった。確かにそこも重要だが、最終的な DNS timeout の直接原因はそこではなかった。
決め手になったのは、10.96.0.10 だけでなく CoreDNS Pod IP へ直接 timeout すること、raw UDP は通ること、正規の Flannel VXLAN だけ bad udp cksum 付きで落ちること、そして flannel.1 の tx-checksum-ip-generic を off にすると即座に DNS が通ることだった。
AI との壁打ちは、この手の layer 分けにはかなり便利だった。GPT 5.5 xhigh と 5.5 pro thinkingを使った。観測結果を貼っていくと「その仮説ならこの tcpdump とは矛盾する」という形で鬼ロジカルシンキング力で適切な方向にガイドしてくれた。