ルーティングテーブルだけでは届かなかった DNS: Tailscale subnet router 越しに Kubernetes worker node を足した話

これはなに

自宅 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.1110.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-01cp-01/02 の間で、Flannel VXLAN の outer packet が通っているかを見る必要がある。

flannel がどの IP を使っているか

kubectl get nodes の annotation で、Flannel の public-ippodCIDR を見る。

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-01ens18 / 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-01ens18 でも見える。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-01ens18 / 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.1tx-checksum-ip-generic を off にすると即座に DNS が通ることだった。

AI との壁打ちは、この手の layer 分けにはかなり便利だった。GPT 5.5 xhigh と 5.5 pro thinkingを使った。観測結果を貼っていくと「その仮説ならこの tcpdump とは矛盾する」という形で鬼ロジカルシンキング力で適切な方向にガイドしてくれた。