cloud

家用 Kubernetes 的內部 DNS:從 router 到 CoreDNS 排查查詢迴圈

讓 GitLab、Registry 與 Vault 的內部名稱可以被 node 和 Pod 解析,並避免把 host 的 loopback resolver 交給 CoreDNS。

English繁中

GitLab 的網頁能開,不代表 Argo CD 能 clone repository;NUC 上能解析 Registry,也不代表 Pod 或 container runtime 走相同的 DNS 路徑。把家中服務分成公開瀏覽器入口與內部機器入口後,這幾個差異就不能再靠同一個「DNS 正常」概括。

我的內部 DNS 設定由 router 的 dnsmasq 管理,NUC 使用 router 作為 upstream,RKE2 裡的 CoreDNS 再替 Pods 解析名稱。這裡的分流使用不同的公開與內部名稱,不是讓同一個 hostname 在內外網回傳不同 IP 的 split-horizon 設定。

本文用以下網段示範,部署時需換成自己的固定租約或靜態位址。home.arpaRFC 8375指定給家用網路的特殊用途 domain。

用途範例
Router DNS192.168.50.1
NUC192.168.50.10
內部 Git/Registrygitlab.home.arparegistry.home.arpa
內部 Vaultvault.home.arpa

先畫出誰在問誰

一般使用 ClusterFirst 的 application Pod 先問 cluster DNS;CoreDNS 自己的 upstream 則來自它的設定與 kubelet 提供的 resolver。Container runtime 拉 image 通常走 node 端的解析路徑,不是 application Pod 的 DNS。

application Pod → CoreDNS → router dnsmasq → public upstream
                                └→ 直接回答 home.arpa 名稱
node/container runtime ───→ router dnsmasq

所以「host 成功、Pod 失敗」不應直接改 application。「image pull 失敗、Pod 內查詢成功」也不能排除 node DNS。先辨認出問題的是哪一個 client,才能選對觀察位置。

讓 router 成為內部名稱的來源

在 Asuswrt-Merlin 啟用 JFFS custom scripts/configs 後,將下列內容加入 /jffs/configs/dnsmasq.conf.add,保留原有設定:

local=/home.arpa/
host-record=gitlab.home.arpa,192.168.50.10
host-record=registry.home.arpa,192.168.50.10
host-record=vault.home.arpa,192.168.50.10

host-record 對這幾個確定名稱建立 records;local 則避免把該 zone 未知名稱的查詢轉送到 public upstream。這比把所有未知內部名稱也指到同一台主機更容易排錯。dnsmasq manual有兩項設定的完整語意;Merlin 的持久化路徑見 custom config files

安排 router DNS 可短暫中斷的時間後,在 router 上重載服務:

service restart_dnsmasq

接著從 LAN client 直接問 router,不先經過其他 DNS cache:

nslookup gitlab.home.arpa 192.168.50.1
nslookup registry.home.arpa 192.168.50.1
nslookup vault.home.arpa 192.168.50.1

三個名稱都應指向範例 NUC 位址。若這一步不成立,先處理 router config、租約與 DNS 服務,不要繼續改 CoreDNS。DHCP client 也必須取得 router DNS;自行使用 public DNS 或 DoH 的 client 不一定能查到這些名稱。

確認 NUC 實際使用哪個 resolver

在使用 NetworkManager 的 Linux node 上先查看狀態:

nmcli connection show --active
resolvectl status
readlink -f /etc/resolv.conf
cat /etc/resolv.conf
cat /run/systemd/resolve/resolv.conf

若正確的 connection 名稱是 LAN,可把 IPv4 DNS 指向 router。以下只適用於由 NetworkManager 管理的 connection;若使用 systemd-networkd 或其他 backend,請修改該主機對應的設定來源:

sudo nmcli connection modify "LAN" \
  ipv4.ignore-auto-dns yes \
  ipv4.dns "192.168.50.1"

IPv6 DHCP/RA 也可能提供另一個 DNS;若有使用 IPv6,須一併確認它能解析內部 zone,不能只修 IPv4。套用 connection 變更可能中斷 SSH,應先保留 console 或其他維護路徑,再用適合該主機的方式重新啟用 connection。

重新檢查 resolvectl status,並同時測試 system resolver 和一般程式的名稱解析:

resolvectl query gitlab.home.arpa
getent hosts gitlab.home.arpa
getent hosts registry.home.arpa

resolvectl 成功只證明 systemd-resolved 的查詢路徑可用。還要看 kubelet 使用的 resolver file,因為 host 的查詢 API 與 Pod 取得的 /etc/resolv.conf 並非同一件事。

為什麼 127.0.0.53 會成為陷阱

Host 上的 127.0.0.53 是 systemd-resolved 的 local stub。對 host 程式而言,它可以是合法的 DNS 入口;但在一般 Pod 的 network namespace 裡,loopback 指向 Pod 自己,不是 host。

如果 kubelet 把含有這個 loopback nameserver 的檔案交給 CoreDNS,而 Corefile 又使用 forward . /etc/resolv.conf,就可能形成 forwarding loop。看到 plugin/loop: Loop ... detected 時,應檢查這條 forwarding 關係,而不是把 loop plugin 刪掉。該 plugin 只偵測部分啟動時的簡單迴圈,沒有報錯也不等於整個 DNS 拓撲正確。CoreDNS loop 文件列出了原因與限制。

修正前,先確認非 stub 檔案真的包含可從 node/Pod 網路到達的 router DNS,且 router 不會把同樣的查詢再送回 CoreDNS。常見路徑是 /run/systemd/resolve/resolv.conf,不能只憑檔名就假設其內容正確。

明確指定 RKE2 給 kubelet 的 resolver file

原本的主機 runbook 採用調整 /etc/resolv.conf symlink 的方法。若希望保留 host 的 stub 設定,可以改在 RKE2 層指定 kubelet 的 resolver 來源;兩種方法不需要一起做。

在每個相關 node 的 /etc/rancher/rke2/config.yaml 合併以下 key,保留其他設定:

resolv-conf: /run/systemd/resolve/resolv.conf

這是 RKE2 的設定 key,不是直接貼入 kubelet config 的 resolvConf 欄位。可對照 RKE2 agent configuration reference。如果該檔案列出多個來自 VPN 或不同介面的 DNS,它不一定能保留原本的 per-domain routing;此時應提供一份由設定管理維護、只包含合適 upstream 的 resolver file,再讓 RKE2 指向它。

Node service 重新啟動應安排維護時段,依 node 角色選 rke2-serverrke2-agent,逐台操作。單節點 control plane 尤其要預期 API 暫時不可用。設定生效後,確認實際 CoreDNS Deployment 名稱,再重建受影響的 Pods:

kubectl -n kube-system get deployments
kubectl -n kube-system rollout restart deployment/<coredns-deployment>
kubectl -n kube-system rollout status deployment/<coredns-deployment>

只 restart CoreDNS 而沒有先修正它取得的 upstream,新 Pod 還是會拿到同一個錯誤設定。

用新的測試 Pod 分開驗證 cluster、內部與公開名稱

下列測試會建立短生命週期 Pod;先確認 cluster 能取得這個測試 image,避免把 image pull 問題誤認成測試內的 DNS 失敗:

kubectl run dns-check --rm -it --restart=Never --image=busybox:1.36 -- sh

在測試 Pod 內執行:

cat /etc/resolv.conf
nslookup kubernetes.default.svc.cluster.local
nslookup gitlab.home.arpa
nslookup registry.home.arpa
nslookup example.com
exit

Cluster Service、內部名稱與 public 名稱都要查。只測 example.com,可能漏掉 router 的內部 zone;只測 GitLab,又可能漏掉 CoreDNS 自己的 Kubernetes service discovery。測試使用預設 cluster.local,若 cluster domain 不同應隨之調整。

觀察結果下一個檢查位置
直接問 router 就失敗dnsmasq records、DNS service、router 可達性
Router 成功,node 失敗Node 的 DNS 設定、cache、VPN routing
Node 成功,Pod 內部名稱失敗CoreDNS upstream、node-local resolver、網路規則
CoreDNS 出現 loop errorForward 目標是否指向自己或繞回自己
DNS 成功,Registry 仍無法拉取TLS、port、container runtime trust 與登入憑證

DNS 只回答「去哪裡」,不授予 Git 或 Registry 的存取權。改用內部 hostname 後,TLS certificate、SSH known hosts 和 registry credential 的 hostname 也要對應。公開入口可繼續走 Cloudflare Tunnel;VPN 則需同時提供路由與內部 DNS,不能只建立網路連線就期待名稱自然可用。