<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Vpn on Tarragon</title><link>https://tarrragon.github.io/blog/tags/vpn/</link><description>Recent content in Vpn on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 14 Aug 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/vpn/index.xml" rel="self" type="application/rss+xml"/><item><title>Tailscale 深入：tailnet、MagicDNS、直連與 DERP 中繼</title><link>https://tarrragon.github.io/blog/linux/tools/remote/tailscale-tailnet-and-relay/</link><pubDate>Wed, 08 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/linux/tools/remote/tailscale-tailnet-and-relay/</guid><description>&lt;p>Tailscale 承擔的是可達性：給每台加入的裝置一個私網內固定的位址，讓它們互相找得到、跟各自當下的公網 IP 與所在網路無關。&lt;a href="../connection-and-sync-tools/">遠端連線與同步工具選型&lt;/a> 的網路層段已交代它在選型上的定位（要省事的 mesh VPN、對照自建 WireGuard）；這篇往下講它怎麼運作與配置——tailnet 的位址模型、用主機名連線、headless 機器怎麼加入、以及最容易困惑的「連得到但走中繼、延遲偏高」是怎麼回事。&lt;/p>
&lt;h2 id="tailnet每台裝置一個私網固定位址">tailnet：每台裝置一個私網固定位址&lt;/h2>
&lt;p>加入同一個帳號的裝置組成一個 tailnet（你的私有網路）。Tailscale 給每台裝置分配一個 &lt;code>100.x.y.z&lt;/code> 的私網位址（CGNAT 保留段），這個位址在裝置的整個生命週期內固定、跟它當下接的是家用 Wi-Fi、行動網路還是咖啡廳網路無關。裝置實際的網路位置變了，由它自己向 Tailscale 的協調伺服器回報、連線在底層自動重建，上層看到的 tailnet 位址不變。&lt;/p>
&lt;p>這把「定位」跟「公網 IP」解耦：家裡 IP 重撥、手機切網路、機器躲在電信業者級 NAT（CGNAT，多戶共用一個公網 IP、對外開 port 這條路直接消失）後面，裝置之間用 tailnet 位址一樣連得到。tailnet 位址穩定這個性質，也是為什麼走 tailnet 的 TCP 連線有機會撐過一次實體換網（見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/tcp-connection-roaming/" data-link-title="TCP Connection Roaming（連線與漫遊）" data-link-desc="遠端連線一換網路（Wi-Fi 切行動網路、休眠喚醒、換 IP）就斷、想知道為什麼 SSH 扛不住而 mosh 撐得住時回來讀">TCP 連線與漫遊&lt;/a> 的邊界段）。&lt;/p>
&lt;h2 id="magicdns用主機名而非-ip">MagicDNS：用主機名而非 IP&lt;/h2>
&lt;p>記 &lt;code>100.x.y.z&lt;/code> 不如記名字。MagicDNS 讓你用裝置的主機名連線——&lt;code>ssh user@my-vm&lt;/code> 而不是 &lt;code>ssh user@100.68.144.88&lt;/code>。主機名比位址更該當作連線座標寫進設定：位址雖然在 tailnet 內固定，但重裝、換帳號都可能拿到新位址，主機名則跟著機器的身分走。要把連線座標記進版控或腳本時，用主機名、留位址當備援。&lt;/p>
&lt;p>這個能力的代價要一起知道：開啟後（&lt;code>accept-dns&lt;/code>）Tailscale 會把 &lt;code>100.100.100.100&lt;/code> 這個跑在本機的 resolver 設成系統第一順位，tailnet 以外的域名由它轉發給上游，於是它進入每一次名字查詢的路徑。它短暫失聯時，整台機器的域名解析會跟著失效——症狀是直連 IP 通、任何用域名的操作卻回 &lt;code>Could not resolve host&lt;/code>。定位方式與 &lt;code>--accept-dns=false&lt;/code> 的取捨見 &lt;a href="https://tarrragon.github.io/blog/work-log/vpn_local_dns_proxy_amplifies_outage/" data-link-title="本機 DNS proxy 插在第一順位時，VPN 的短暫失聯會放大成全機解析故障" data-link-desc="git / curl 回 Could not resolve host 而直連 IP 通、或同一個域名時好時壞時，用來定位是整條網路斷了、還是解析路徑上多了一個會單獨失效的本機 DNS proxy">本機 DNS proxy 插在第一順位時的放大效應&lt;/a>。&lt;/p>
&lt;h2 id="headless-機器怎麼加入-tailnet">headless 機器怎麼加入 tailnet&lt;/h2>
&lt;p>沒有瀏覽器的機器（VM、伺服器）用 &lt;code>tailscale up&lt;/code> 加入，它會印一個授權 URL、在任何已登入該帳號的裝置上開這個 URL 核准即上線：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">sudo systemctl &lt;span class="nb">enable&lt;/span> --now tailscaled &lt;span class="c1"># 啟動 daemon&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">sudo tailscale up --hostname&lt;span class="o">=&lt;/span>my-vm &lt;span class="c1"># 印出授權 URL&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># → To authenticate, visit: https://login.tailscale.com/a/...&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在手機或桌機的瀏覽器開那個 URL、用你的 Tailscale 帳號授權這個節點，機器就加入 tailnet、拿到位址。&lt;code>--hostname&lt;/code> 指定它在 tailnet 裡的主機名。手機端則裝 Tailscale app、登入同一帳號、打開開關即加入——之後手機用 tailnet 位址就找得到那台 VM，跟手機接哪個網路無關。&lt;/p>
&lt;h2 id="直連-vs-derp-中繼連得到但延遲高是怎麼回事">直連 vs DERP 中繼：連得到但延遲高是怎麼回事&lt;/h2>
&lt;p>兩台 tailnet 裝置之間，Tailscale 會盡量建&lt;strong>直連&lt;/strong>（peer-to-peer、封包直接走）；建不起來時退回 &lt;strong>DERP 中繼&lt;/strong>（封包繞經 Tailscale 的中繼伺服器）。中繼一定通（只要兩端都連得上某個 DERP 伺服器），但封包多繞一段、延遲被放大。判讀在 &lt;code>tailscale status&lt;/code>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">100.68.144.88 my-vm ... active; direct 203.0.113.5:41641 # 直連
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">100.68.144.88 my-vm ... active; relay &amp;#34;hkg&amp;#34; # 走香港 DERP 中繼&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>直連建不起來的典型原因是 NAT 穿透失敗：兩端的 NAT 類型不配合（對稱型 NAT——每次對外連線都換一個 port、讓對端無法預測該往哪打，或某些虛擬機的 NAT 網路模式）時，打洞（hole punching，兩端同時往對方猜測的位址送封包、在 NAT 上鑿出臨時通道）建不起 peer-to-peer 路徑、只能退中繼。一個實測到的例子：一台跑在筆電上的虛擬機（NAT 網路模式），從同一台筆電連它、tailscale 卻走跨國 DERP 中繼、延遲幾十毫秒——因為虛擬機的 NAT 讓直連打洞失敗。功能完全可用（能連、能傳），只是延遲被中繼繞路放大。&lt;/p>
&lt;p>&lt;strong>看得到、&lt;code>ping&lt;/code> 通但延遲偏高且標 &lt;code>relay&lt;/code>&lt;/strong>，就是 NAT 穿透失敗退中繼——功能可用只是慢、不必急著修，除非延遲影響到逐鍵互動的體感。要降延遲得從 NAT 類型下手：換 VM 的網路模式（bridged 讓 VM 直接掛在實體網段、比 NAT 模式容易建直連）、在路由器開 UPnP（讓裝置自動請求對外 port 映射、打洞更容易成功）、或用 Tailscale 的 exit node（指定一台裝置當所有對外流量的統一出口）與 subnet router（讓一台裝置把它所在的整個子網橋進 tailnet）換一條路徑。&lt;/p></description><content:encoded><![CDATA[<p>Tailscale 承擔的是可達性：給每台加入的裝置一個私網內固定的位址，讓它們互相找得到、跟各自當下的公網 IP 與所在網路無關。<a href="../connection-and-sync-tools/">遠端連線與同步工具選型</a> 的網路層段已交代它在選型上的定位（要省事的 mesh VPN、對照自建 WireGuard）；這篇往下講它怎麼運作與配置——tailnet 的位址模型、用主機名連線、headless 機器怎麼加入、以及最容易困惑的「連得到但走中繼、延遲偏高」是怎麼回事。</p>
<h2 id="tailnet每台裝置一個私網固定位址">tailnet：每台裝置一個私網固定位址</h2>
<p>加入同一個帳號的裝置組成一個 tailnet（你的私有網路）。Tailscale 給每台裝置分配一個 <code>100.x.y.z</code> 的私網位址（CGNAT 保留段），這個位址在裝置的整個生命週期內固定、跟它當下接的是家用 Wi-Fi、行動網路還是咖啡廳網路無關。裝置實際的網路位置變了，由它自己向 Tailscale 的協調伺服器回報、連線在底層自動重建，上層看到的 tailnet 位址不變。</p>
<p>這把「定位」跟「公網 IP」解耦：家裡 IP 重撥、手機切網路、機器躲在電信業者級 NAT（CGNAT，多戶共用一個公網 IP、對外開 port 這條路直接消失）後面，裝置之間用 tailnet 位址一樣連得到。tailnet 位址穩定這個性質，也是為什麼走 tailnet 的 TCP 連線有機會撐過一次實體換網（見 <a href="/blog/linux/dotfile/knowledge-cards/tcp-connection-roaming/" data-link-title="TCP Connection Roaming（連線與漫遊）" data-link-desc="遠端連線一換網路（Wi-Fi 切行動網路、休眠喚醒、換 IP）就斷、想知道為什麼 SSH 扛不住而 mosh 撐得住時回來讀">TCP 連線與漫遊</a> 的邊界段）。</p>
<h2 id="magicdns用主機名而非-ip">MagicDNS：用主機名而非 IP</h2>
<p>記 <code>100.x.y.z</code> 不如記名字。MagicDNS 讓你用裝置的主機名連線——<code>ssh user@my-vm</code> 而不是 <code>ssh user@100.68.144.88</code>。主機名比位址更該當作連線座標寫進設定：位址雖然在 tailnet 內固定，但重裝、換帳號都可能拿到新位址，主機名則跟著機器的身分走。要把連線座標記進版控或腳本時，用主機名、留位址當備援。</p>
<p>這個能力的代價要一起知道：開啟後（<code>accept-dns</code>）Tailscale 會把 <code>100.100.100.100</code> 這個跑在本機的 resolver 設成系統第一順位，tailnet 以外的域名由它轉發給上游，於是它進入每一次名字查詢的路徑。它短暫失聯時，整台機器的域名解析會跟著失效——症狀是直連 IP 通、任何用域名的操作卻回 <code>Could not resolve host</code>。定位方式與 <code>--accept-dns=false</code> 的取捨見 <a href="/blog/work-log/vpn_local_dns_proxy_amplifies_outage/" data-link-title="本機 DNS proxy 插在第一順位時，VPN 的短暫失聯會放大成全機解析故障" data-link-desc="git / curl 回 Could not resolve host 而直連 IP 通、或同一個域名時好時壞時，用來定位是整條網路斷了、還是解析路徑上多了一個會單獨失效的本機 DNS proxy">本機 DNS proxy 插在第一順位時的放大效應</a>。</p>
<h2 id="headless-機器怎麼加入-tailnet">headless 機器怎麼加入 tailnet</h2>
<p>沒有瀏覽器的機器（VM、伺服器）用 <code>tailscale up</code> 加入，它會印一個授權 URL、在任何已登入該帳號的裝置上開這個 URL 核准即上線：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo systemctl <span class="nb">enable</span> --now tailscaled     <span class="c1"># 啟動 daemon</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo tailscale up --hostname<span class="o">=</span>my-vm         <span class="c1"># 印出授權 URL</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># → To authenticate, visit: https://login.tailscale.com/a/...</span></span></span></code></pre></div><p>在手機或桌機的瀏覽器開那個 URL、用你的 Tailscale 帳號授權這個節點，機器就加入 tailnet、拿到位址。<code>--hostname</code> 指定它在 tailnet 裡的主機名。手機端則裝 Tailscale app、登入同一帳號、打開開關即加入——之後手機用 tailnet 位址就找得到那台 VM，跟手機接哪個網路無關。</p>
<h2 id="直連-vs-derp-中繼連得到但延遲高是怎麼回事">直連 vs DERP 中繼：連得到但延遲高是怎麼回事</h2>
<p>兩台 tailnet 裝置之間，Tailscale 會盡量建<strong>直連</strong>（peer-to-peer、封包直接走）；建不起來時退回 <strong>DERP 中繼</strong>（封包繞經 Tailscale 的中繼伺服器）。中繼一定通（只要兩端都連得上某個 DERP 伺服器），但封包多繞一段、延遲被放大。判讀在 <code>tailscale status</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">100.68.144.88  my-vm  ...  active; direct 203.0.113.5:41641   # 直連
</span></span><span class="line"><span class="ln">2</span><span class="cl">100.68.144.88  my-vm  ...  active; relay &#34;hkg&#34;                # 走香港 DERP 中繼</span></span></code></pre></div><p>直連建不起來的典型原因是 NAT 穿透失敗：兩端的 NAT 類型不配合（對稱型 NAT——每次對外連線都換一個 port、讓對端無法預測該往哪打，或某些虛擬機的 NAT 網路模式）時，打洞（hole punching，兩端同時往對方猜測的位址送封包、在 NAT 上鑿出臨時通道）建不起 peer-to-peer 路徑、只能退中繼。一個實測到的例子：一台跑在筆電上的虛擬機（NAT 網路模式），從同一台筆電連它、tailscale 卻走跨國 DERP 中繼、延遲幾十毫秒——因為虛擬機的 NAT 讓直連打洞失敗。功能完全可用（能連、能傳），只是延遲被中繼繞路放大。</p>
<p><strong>看得到、<code>ping</code> 通但延遲偏高且標 <code>relay</code></strong>，就是 NAT 穿透失敗退中繼——功能可用只是慢、不必急著修，除非延遲影響到逐鍵互動的體感。要降延遲得從 NAT 類型下手：換 VM 的網路模式（bridged 讓 VM 直接掛在實體網段、比 NAT 模式容易建直連）、在路由器開 UPnP（讓裝置自動請求對外 port 映射、打洞更容易成功）、或用 Tailscale 的 exit node（指定一台裝置當所有對外流量的統一出口）與 subnet router（讓一台裝置把它所在的整個子網橋進 tailnet）換一條路徑。</p>
<h2 id="tailscale-status-判讀">tailscale status 判讀</h2>
<p><code>tailscale status</code> 是這一層的權威狀態，除錯先看它：</p>
<ul>
<li><strong>對方不在清單</strong> → 登入 / 帳號問題（是不是同一個 tailnet、有沒有上線）。</li>
<li><strong>標 <code>offline</code></strong> → 那台裝置的 tailscaled 沒跑或斷網。</li>
<li><strong>看得到、標 <code>relay</code></strong> → 上線且可達、只是走中繼（延遲議題、非連不通）。</li>
<li><strong>看得到、標 <code>direct</code></strong> → 直連、最佳狀態。</li>
</ul>
<p>從手機連 tailnet 位址回「connection timed out」時，逾時指向可達性層而非服務層——問題多半在手機的 Tailscale 沒連上、那個私網位址對手機根本不存在，而不在伺服器的 sshd。判別方法是分清「逾時 vs 被拒」，見 <a href="/blog/linux/dotfile/knowledge-cards/connection-refused-vs-timeout/" data-link-title="Connection Refused vs. Timeout（連線被拒與逾時）" data-link-desc="遠端連不上、要判斷是封包到不了還是服務拒絕、決定往網路層還是服務層除錯時回來讀">連線逾時 vs 連線被拒</a>。</p>
<h2 id="跟連線層疊加關掉公網入口">跟連線層疊加、關掉公網入口</h2>
<p>Tailscale 是網路層、跟連線層（SSH / mosh）是疊加關係：先有可達性、上面才談連線手感。這個疊加還帶來一個安全性質——服務可以只綁 tailnet 介面、公網防火牆全關：SSH / ttyd 這類只在私網位址上聽，對外沒有任何開放 port，公網入口縮到零。比「開公網 port 再堆 fail2ban」的維護成本低得多。連得到私網位址本身就代表通過了 tailnet 認證，定位與授權在這一層合一。</p>
<h2 id="取捨與邊界">取捨與邊界</h2>
<p>省事是有代價的，採用前把三個取捨算進去：</p>
<ul>
<li><strong>可達性外包給第三方 control plane</strong>：tailnet 的協調伺服器（新連線建立、金鑰交換）與 DERP 中繼都由 Tailscale 這家公司營運。連線本身是端對端加密、公司看不到內容，但「裝置怎麼找到彼此」這層依賴它的服務——公司關門、服務中斷、政策變動都在 blast radius 內。要完全自主、不依賴外部協調，用自建 WireGuard（代價是手動管金鑰與 endpoint）。</li>
<li><strong>免費方案有規模上限</strong>：個人免費方案有裝置數與使用者數上限（會隨官方政策調整，採用前查當前數字）。個人一台 VM 加幾支裝置遠在上限內，但把它當團隊或機群方案前要確認額度。</li>
<li><strong>接管 DNS 換來主機名、也換來一個新的單點</strong>：<code>accept-dns</code> 開著時，<code>100.100.100.100</code> 進入每一次名字查詢的路徑，它短暫失聯會讓整台機器解析不了任何域名。取捨與定位方式見上面 MagicDNS 段。</li>
<li><strong>tailnet 是一張扁平信任網</strong>：預設「連得到私網位址 = 已通過 tailnet 認證」把定位與授權合一，方便，但也意味著任何加入 tailnet 或被入侵的裝置都在信任邊界內。要細分權限得配 ACL（限制哪台連得到哪台），別把「只綁 tailnet 介面」當成不必再做存取控制。</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>選型層（Tailscale vs 自建 WireGuard、跟連線 / 同步工具的關係）：<a href="../connection-and-sync-tools/">遠端連線與同步工具選型</a></li>
<li>在遠端 agent 工作機上實際用它打通手機到 VM（含 DERP 中繼實例）：<a href="../agent-workstation-vm-handson/">遠端 agent 工作機實作記錄</a> 的 Step 3</li>
<li>機器連不到的完整分流（網路層 / 服務層 / 機器沒起）：<a href="../../../debug/machine-unreachable/">機器連不到或起不來</a></li>
<li>相關術語卡：<a href="/blog/linux/dotfile/knowledge-cards/tcp-connection-roaming/" data-link-title="TCP Connection Roaming（連線與漫遊）" data-link-desc="遠端連線一換網路（Wi-Fi 切行動網路、休眠喚醒、換 IP）就斷、想知道為什麼 SSH 扛不住而 mosh 撐得住時回來讀">TCP 連線與漫遊</a>、<a href="/blog/linux/dotfile/knowledge-cards/connection-refused-vs-timeout/" data-link-title="Connection Refused vs. Timeout（連線被拒與逾時）" data-link-desc="遠端連不上、要判斷是封包到不了還是服務拒絕、決定往網路層還是服務層除錯時回來讀">連線逾時 vs 連線被拒</a></li>
</ul>
]]></content:encoded></item><item><title>本機 DNS proxy 插在第一順位時，VPN 的短暫失聯會放大成全機解析故障</title><link>https://tarrragon.github.io/blog/work-log/vpn_local_dns_proxy_amplifies_outage/</link><pubDate>Fri, 14 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/work-log/vpn_local_dns_proxy_amplifies_outage/</guid><description>&lt;h2 id="情境的形狀">情境的形狀&lt;/h2>
&lt;p>症狀組合是這樣：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">git push → fatal: Could not resolve host: dev-tea.example.com
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">ping 8.8.8.8 → 通
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">curl https://&amp;lt;伺服器 IP&amp;gt;/ → 通
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">git push（幾分鐘後重試） → 成功&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>用域名的操作一律失敗、用位址的操作一律成功，而且過一陣子自行恢復。這個組合把診斷推向「網路壞了又自己修好」，而網路層的證據從頭到尾都是通的。&lt;/p>
&lt;p>射程是這樣的機器：開發機上裝著會接管 DNS 的常駐程式。mesh VPN 客戶端（Tailscale 的 MagicDNS）、公司 VPN 的 split DNS 客戶端（把內網域名導向公司內部 DNS、其餘照常走外部的那種設定）、本機跑的 &lt;code>dnsmasq&lt;/code> 或廣告過濾（Pi-hole / AdGuard Home）、Linux 上的 &lt;code>systemd-resolved&lt;/code> 都算。它們的共同點是在本機開一個 resolver，並把自己設成系統的解析入口。&lt;/p>
&lt;p>本文的指令與輸出在 macOS 實機驗證過。Linux 與 Windows 側的對應動作依各自工具的文件寫出、標在對應段落，未經實機驗證。&lt;/p>
&lt;h2 id="症狀落在解析層可達性層已經被排除">症狀落在解析層，可達性層已經被排除&lt;/h2>
&lt;p>連線失敗有三層症狀，各自指向不同的方向：解析失敗（名字還沒變成位址）、連線逾時（封包送不到）、連線被拒（送到了、對方拒絕）。三者的分層判別見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/connection-refused-vs-timeout/#%e5%88%a4%e8%ae%80%e8%a8%8a%e8%99%9f" data-link-title="Connection Refused vs. Timeout（連線被拒與逾時）" data-link-desc="遠端連不上、要判斷是封包到不了還是服務拒絕、決定往網路層還是服務層除錯時回來讀">連線逾時 vs 連線被拒&lt;/a>。&lt;/p>
&lt;p>&lt;code>Could not resolve host&lt;/code> 是最前面那一層，封包還沒送出去。而 &lt;code>ping 8.8.8.8&lt;/code> 與直連伺服器 IP 都通，代表鏈路、路由、對外可達性都成立。這兩個證據合起來把範圍收斂到單一問題：&lt;strong>這台機器把名字換成位址的那條路徑，出了什麼事&lt;/strong>。&lt;/p>
&lt;h2 id="macos-的解析設定權威在-scutil不在-etcresolvconf">macOS 的解析設定權威在 scutil，不在 /etc/resolv.conf&lt;/h2>
&lt;p>Linux 上的第一個檢查是 &lt;code>/etc/resolv.conf&lt;/code> 裡有沒有可用的 &lt;code>nameserver&lt;/code>。這個動作搬到 macOS 會讀到一份不決定任何事的檔案，該檔自己就印著這件事：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">$ cat /etc/resolv.conf
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">#
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"># macOS Notice
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">#
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"># This file is not consulted for DNS hostname resolution, address
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"># resolution, or the DNS query routing mechanism used by most
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"># processes on this system.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">#
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"># To view the DNS configuration used by this system, use:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"># scutil --dns
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">#
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"># ...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">#
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"># This file is automatically generated.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">#
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">nameserver 168.95.192.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">nameserver 168.95.1.1&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>它仍然存在、仍然有內容、&lt;code>cat&lt;/code> 出來看起來完全正常，所以讀到「有 nameserver」就宣告 DNS 設定沒問題，是在一份不決定路由的檔案上結案。它的內容通常鏡射系統設定，但決定 &lt;code>git&lt;/code> 與 &lt;code>curl&lt;/code> 走哪條路的是 &lt;code>scutil --dns&lt;/code> 那份：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">$ scutil --dns
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">DNS configuration
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">resolver #1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> nameserver[0] : 168.95.192.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> nameserver[1] : 168.95.1.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> if_index : 11 (en0)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>實際輸出比這長得多，後面還跟著數個處理 &lt;code>local&lt;/code> 與 mDNS 的 resolver 區塊。上面這份是 VPN 停用後的狀態，第一順位是 Wi-Fi 從 DHCP 拿到的電信業者 DNS。事故當下的同一個指令，第一順位是 &lt;code>100.100.100.100&lt;/code>，也就是 Tailscale 跑在本機的 resolver 位址。&lt;/p></description><content:encoded><![CDATA[<h2 id="情境的形狀">情境的形狀</h2>
<p>症狀組合是這樣：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">git push                  → fatal: Could not resolve host: dev-tea.example.com
</span></span><span class="line"><span class="ln">2</span><span class="cl">ping 8.8.8.8              → 通
</span></span><span class="line"><span class="ln">3</span><span class="cl">curl https://&lt;伺服器 IP&gt;/  → 通
</span></span><span class="line"><span class="ln">4</span><span class="cl">git push（幾分鐘後重試）    → 成功</span></span></code></pre></div><p>用域名的操作一律失敗、用位址的操作一律成功，而且過一陣子自行恢復。這個組合把診斷推向「網路壞了又自己修好」，而網路層的證據從頭到尾都是通的。</p>
<p>射程是這樣的機器：開發機上裝著會接管 DNS 的常駐程式。mesh VPN 客戶端（Tailscale 的 MagicDNS）、公司 VPN 的 split DNS 客戶端（把內網域名導向公司內部 DNS、其餘照常走外部的那種設定）、本機跑的 <code>dnsmasq</code> 或廣告過濾（Pi-hole / AdGuard Home）、Linux 上的 <code>systemd-resolved</code> 都算。它們的共同點是在本機開一個 resolver，並把自己設成系統的解析入口。</p>
<p>本文的指令與輸出在 macOS 實機驗證過。Linux 與 Windows 側的對應動作依各自工具的文件寫出、標在對應段落，未經實機驗證。</p>
<h2 id="症狀落在解析層可達性層已經被排除">症狀落在解析層，可達性層已經被排除</h2>
<p>連線失敗有三層症狀，各自指向不同的方向：解析失敗（名字還沒變成位址）、連線逾時（封包送不到）、連線被拒（送到了、對方拒絕）。三者的分層判別見 <a href="/blog/linux/dotfile/knowledge-cards/connection-refused-vs-timeout/#%e5%88%a4%e8%ae%80%e8%a8%8a%e8%99%9f" data-link-title="Connection Refused vs. Timeout（連線被拒與逾時）" data-link-desc="遠端連不上、要判斷是封包到不了還是服務拒絕、決定往網路層還是服務層除錯時回來讀">連線逾時 vs 連線被拒</a>。</p>
<p><code>Could not resolve host</code> 是最前面那一層，封包還沒送出去。而 <code>ping 8.8.8.8</code> 與直連伺服器 IP 都通，代表鏈路、路由、對外可達性都成立。這兩個證據合起來把範圍收斂到單一問題：<strong>這台機器把名字換成位址的那條路徑，出了什麼事</strong>。</p>
<h2 id="macos-的解析設定權威在-scutil不在-etcresolvconf">macOS 的解析設定權威在 scutil，不在 /etc/resolv.conf</h2>
<p>Linux 上的第一個檢查是 <code>/etc/resolv.conf</code> 裡有沒有可用的 <code>nameserver</code>。這個動作搬到 macOS 會讀到一份不決定任何事的檔案，該檔自己就印著這件事：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">$ cat /etc/resolv.conf
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">#
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"># macOS Notice
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">#
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"># This file is not consulted for DNS hostname resolution, address
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"># resolution, or the DNS query routing mechanism used by most
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"># processes on this system.
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">#
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"># To view the DNS configuration used by this system, use:
</span></span><span class="line"><span class="ln">10</span><span class="cl">#   scutil --dns
</span></span><span class="line"><span class="ln">11</span><span class="cl">#
</span></span><span class="line"><span class="ln">12</span><span class="cl"># ...
</span></span><span class="line"><span class="ln">13</span><span class="cl">#
</span></span><span class="line"><span class="ln">14</span><span class="cl"># This file is automatically generated.
</span></span><span class="line"><span class="ln">15</span><span class="cl">#
</span></span><span class="line"><span class="ln">16</span><span class="cl">nameserver 168.95.192.1
</span></span><span class="line"><span class="ln">17</span><span class="cl">nameserver 168.95.1.1</span></span></code></pre></div><p>它仍然存在、仍然有內容、<code>cat</code> 出來看起來完全正常，所以讀到「有 nameserver」就宣告 DNS 設定沒問題，是在一份不決定路由的檔案上結案。它的內容通常鏡射系統設定，但決定 <code>git</code> 與 <code>curl</code> 走哪條路的是 <code>scutil --dns</code> 那份：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">$ scutil --dns
</span></span><span class="line"><span class="ln">2</span><span class="cl">DNS configuration
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl">resolver #1
</span></span><span class="line"><span class="ln">5</span><span class="cl">  nameserver[0] : 168.95.192.1
</span></span><span class="line"><span class="ln">6</span><span class="cl">  nameserver[1] : 168.95.1.1
</span></span><span class="line"><span class="ln">7</span><span class="cl">  if_index : 11 (en0)</span></span></code></pre></div><p>實際輸出比這長得多，後面還跟著數個處理 <code>local</code> 與 mDNS 的 resolver 區塊。上面這份是 VPN 停用後的狀態，第一順位是 Wi-Fi 從 DHCP 拿到的電信業者 DNS。事故當下的同一個指令，第一順位是 <code>100.100.100.100</code>，也就是 Tailscale 跑在本機的 resolver 位址。</p>
<p>三件事要一起從這份輸出讀出來，它們決定了後面整條推論成不成立：</p>
<ul>
<li><strong>第一順位是誰</strong>。<code>100.100.100.100</code>（Tailscale）、<code>127.0.0.53</code>（systemd-resolved）、<code>127.0.0.1</code> 這類本機或保留位址出現在這裡，就代表有一個本機 proxy 站在路徑上。</li>
<li><strong>有沒有獨立於它的第二條路</strong>。<code>resolver #1</code> 底下多一行 <code>nameserver[1]</code> 未必是退路——接管 DNS 的元件常常整組替換這份清單，兩行可能是同一個 proxy 的 v4 與 v6 位址。要看的是第二順位屬不屬於另一個元件。即使是，<code>man 5 resolver</code> 描述的順序嘗試是<strong>逾時才換下一台</strong>，所以退路的表現是每次查詢都先卡掉一個逾時、全機變慢，而不是透明接手。</li>
<li><strong>失敗的名字歸哪個 resolver 管</strong>。帶 <code>domain :</code> 的 resolver 只服務該網域。內網域名解析不出來時，要看的是那條 scoped resolver，而不是 <code>resolver #1</code>——公司 VPN 的 split DNS 客戶端多半只加 scoped resolver、不動預設那條。</li>
</ul>
<p>Linux 上的對應動作是看 <code>/etc/resolv.conf</code> 指向哪裡與 <code>resolvectl status</code> 的 <code>resolv.conf mode</code>：值是 <code>stub</code> 就代表查詢先進 <code>systemd-resolved</code> 的本機 stub，各介面實際用的上游列在下面的 Link 區塊。Windows 上是 <code>Get-DnsClientServerAddress</code>。WSL2 是雙層——WSL 內的 <code>/etc/resolv.conf</code> 指向 Windows 虛擬網卡，真正的 proxy 在 Windows 那一側，處置也要在那一側做。這是預設形態，新版的 DNS tunneling 與 mirrored 網路模式下 nameserver 不長這樣，先確認自己在哪一種。</p>
<h2 id="tailscale-的-resolver-進入了每一次名字查詢的路徑">Tailscale 的 resolver 進入了每一次名字查詢的路徑</h2>
<p><code>100.100.100.100</code> 在事故當下的設定裡是一個全量 resolver：tailnet（該帳號底下所有裝置組成的那張私網）內部的名字自己回答，其餘的<strong>轉發</strong>給上游 DNS。上游預設是這台機器原本用的那組，tailnet 若設了全域 nameserver 就改送給它。這個設計讓 <code>ssh my-vm</code> 這種寫法能成立（MagicDNS 的正常用途見 <a href="/blog/linux/tools/remote/tailscale-tailnet-and-relay/" data-link-title="Tailscale 深入：tailnet、MagicDNS、直連與 DERP 中繼" data-link-desc="要用 Tailscale 讓散在不同網路的機器互連、遇到連得到但延遲高、或想讀懂 tailscale status 的 relay / direct 標示時回來讀">Tailscale 深入：tailnet、MagicDNS、直連與 DERP 中繼</a>），代價是它處理的遠不只 tailnet 主機名，<code>github.com</code> 這種跟 tailnet 毫無關係的域名同樣要經過它。</p>
<p>「全量」是當次設定的結果，不是這個產品的性質。同一個 Tailscale 也可以只接管 tailnet 網域、其餘照走系統上游，那種設定下它根本不在 <code>github.com</code> 的路徑上。前一節那三個檢查就是在確認「它這次確實在必經路徑上」。</p>
<p>於是可用性的算式變了。原本「解析得出來」需要的是：Wi-Fi 通、上游 DNS 活著。插進一個本機 proxy 之後，多一個條件——這個 proxy 此刻運作正常。三個條件串聯，整體可用性是三者的下限。</p>
<p>放大效應就從這裡來。一次 Wi-Fi 瞬斷對純網路操作只是幾個封包重傳（事故當下 ping 對外仍然通，延遲從 10ms 級跳到 363ms），對這條解析路徑卻可能是整段中斷，而中斷期間，<strong>這台機器所有沒被快取住的域名都解析不出來</strong>。一個局部、短暫、原本只造成抖動的事件，被路徑上的位置放大成全機範圍的功能中斷。</p>
<p>至於這個 proxy 為什麼會跟著抖，當下沒有取得它的 log，所以機制未經驗證。手上的證據（對外 ping 通、延遲跳動、間歇恢復）至少支持兩種解釋：它與協調伺服器的連線在丟包下不穩、或者它轉發給上游的查詢在高延遲下逾時，而系統原本的兩台 nameserver 冗餘被它換成了單一入口、失去重試的餘裕。分流測試能證明斷點落在它身上，證明不了它為什麼斷——兩者要分開講。</p>
<h2 id="分流測試對同一個名字繞過它再問一次">分流測試：對同一個名字，繞過它再問一次</h2>
<p>定位方法是把「解析壞了」拆成「路徑上哪一段壞了」，做法是繞過可疑的那一段、直接問下一段。兩個前提決定這個測試有沒有分辨力，先講清楚：</p>
<p><strong>測試的名字要用當下失敗的那一個。</strong> 內網域名、tailnet 主機名這類只有特定 resolver 答得出來的名字，換一台 resolver 問必然拿不到答案——那是設計如此，不是故障。反過來，拿 <code>github.com</code> 去測而故障只發生在內網域名，測試會回報一切正常，因為壞掉的那條路根本沒被走到。</p>
<p><strong>第二條要問的是那個 proxy 自己的上游。</strong> 轉發型 resolver 把查詢送給上游，上游死掉會表現成它自己死掉。問一台無關的公共 resolver 只能證明「不是全網不可達」，分不出「proxy 壞了」與「proxy 的上游壞了」，而這兩者的處置相反。上游位址從 <code>tailscale dns status</code>、或第一順位被接管之前的 DHCP 設定取得。</p>
<p>診斷時不要加 <code>+short</code>。實測 <code>+short</code> 對 NXDOMAIN 與 SERVFAIL 都印空字串、都 exit 0，兩者的下一步卻完全不同：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">dig +time<span class="o">=</span><span class="m">2</span> +tries<span class="o">=</span><span class="m">1</span> @&lt;proxy 位址&gt;     &lt;失敗的那個名字&gt;
</span></span><span class="line"><span class="ln">2</span><span class="cl">dig +time<span class="o">=</span><span class="m">2</span> +tries<span class="o">=</span><span class="m">1</span> @&lt;proxy 的上游&gt;   &lt;失敗的那個名字&gt;</span></span></code></pre></div><p>要讀的是 <code>;; -&gt;&gt;HEADER&lt;&lt;-</code> 那行的 <code>status:</code>，配著下一行的 <code>ANSWER:</code> 計數看：</p>
<ul>
<li><strong><code>NOERROR</code> 且 <code>ANSWER</code> 大於 0</strong>：答出來了。</li>
<li><strong><code>NOERROR</code> 但 <code>ANSWER: 0</code></strong>（NODATA）：名字存在、這個型別沒有記錄，常見於只有 AAAA 或 CNAME 鏈的名字。「查不到」最常長這個樣子而非 NXDOMAIN，而 <code>+short</code> 對它一樣印空。</li>
<li><strong><code>NXDOMAIN</code></strong>：這個名字不存在。</li>
<li><strong><code>SERVFAIL</code></strong>：這台 resolver 試了但失敗，多半往它的上游查；DNSSEC 驗證失敗也回這個，那條線跟上游無關。</li>
<li><strong><code>REFUSED</code></strong>：它拒絕服務這個 client，多半是 ACL。</li>
<li><strong>完全沒回應</strong>：<code>connection timed out</code>。</li>
</ul>
<p>兩條的組合這樣讀：</p>
<table>
  <thead>
      <tr>
          <th>proxy 的回答</th>
          <th>它的上游的回答</th>
          <th>判讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>有位址</td>
          <td>有位址</td>
          <td>解析路徑正常，往程式端查（見下一節的 <code>dig</code> 陷阱）</td>
      </tr>
      <tr>
          <td>逾時 / SERVFAIL / REFUSED</td>
          <td>有位址</td>
          <td>斷點在 proxy。上游有回應、封包送得出去</td>
      </tr>
      <tr>
          <td>逾時 / SERVFAIL</td>
          <td>逾時 / SERVFAIL</td>
          <td>上游或可達性層，往 <a href="/blog/linux/debug/machine-unreachable/" data-link-title="機器連不到或起不來" data-link-desc="遠端機器突然 SSH 連不上、虛擬機開不了機、或懷疑磁碟滿引發連鎖故障時，從主機側與網路層的權威狀態往下定位是哪一環斷了">機器連不到或起不來</a> 走</td>
      </tr>
      <tr>
          <td>有位址</td>
          <td>逾時 / NXDOMAIN</td>
          <td>proxy 健康。這個名字本來就只有它答得出來</td>
      </tr>
      <tr>
          <td>NXDOMAIN</td>
          <td>有位址</td>
          <td>proxy 主動擋掉這個名字，是設定而非故障</td>
      </tr>
      <tr>
          <td>NXDOMAIN</td>
          <td>NXDOMAIN</td>
          <td>名字不存在，跟路徑無關</td>
      </tr>
  </tbody>
</table>
<p>「proxy 答得出來、上游答不出來」那一列容易被誤讀成故障，它是 split DNS 與 MagicDNS 正常運作的樣子——內網域名只有公司 DNS 答得出來、tailnet 主機名只有 <code>100.100.100.100</code> 答得出來。同一列也涵蓋另一種情形：對外 53 埠被封（旅館、公共 Wi-Fi、企業內網），而 proxy 走 DoH 或走 VPN 隧道轉發、不受影響。</p>
<p>「兩條都失敗」那一列要先排除封鎖。部分網路會封鎖或劫持對外的 53 埠，讓「直接問任意上游」在該網路上本來就走不通，兩條 <code>dig</code> 一起失敗而 proxy 無辜。判別方式是換一個網路（手機熱點）再跑同一組指令。</p>
<p>「proxy 回 NXDOMAIN、上游回得出位址」那一列是廣告過濾與企業 DNS 封鎖政策的正常樣子——Pi-hole 擋廣告網域就是回 NXDOMAIN。判成故障會讓人去修一個正在照設定運作的元件；真正該問的是這個名字該不該在阻擋清單上。</p>
<p><strong>兩邊都回位址、但位址不同</strong>這個組合刻意沒有進表。公網名字經過 CDN 與 GeoDNS，本來就會依 resolver 所在位置回不同的節點，兩台 resolver 給出不同答案是常態、不構成任何訊號。只有在測<strong>內網名字</strong>時，位址不同才值得當 split-horizon 讀（同一個名字在內外網指向不同機器），而那時該檢查的是自己連的是哪一邊。</p>
<h2 id="間歇性故障要問幾次才算數">間歇性故障要問幾次才算數</h2>
<p>單次成功排除不了問題，而該問幾次取決於故障有多間歇。結案要求的是「連續 n 次都成功」，而它在故障仍然存在時發生的機率是 <code>p^n</code>（<code>p</code> 是單次成功率）。要把這個誤判機率壓到 <code>α</code> 以下，取樣次數是：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">n ≥ ln α / ln p        取整往上</span></span></code></pre></div><p>事故當下觀測到的 <code>p</code> 大約三分之一（連問三次中一次，樣本只有三次、這是觀測值而非推導值）。代進去，<code>α</code> 取 0.5% 時 <code>n</code> 是 5。但這個數字對輕度間歇完全不夠——<code>p</code> 是 0.9 時，同樣的 <code>α</code> 要問 29 次（<code>0.9^28</code> 是 5.2%、仍高於門檻），用五次的話有將近六成機率誤判成已經修好。取樣次數要跟著觀測到的成功率重算，不是一個固定值。</p>
<p>公式吃一個前提：各次取樣互相獨立。間歇性故障通常是時間相關的（壞一陣、好一陣），背靠背連跑五次可能整批都落在同一個好的區間裡，那批樣本的獨立性最差。取樣要拉開間隔，跨過故障起伏的時間尺度：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">for</span> i in <span class="k">$(</span>seq 5<span class="k">)</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  dig +short +time<span class="o">=</span><span class="m">2</span> +tries<span class="o">=</span><span class="m">1</span> @&lt;proxy 位址&gt; &lt;失敗的那個名字&gt; <span class="p">|</span> rg -c <span class="s1">&#39;^[0-9]+\.&#39;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  sleep <span class="m">20</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><p>計次要過濾出位址行。<code>+short</code> 對只有 CNAME 的回答會印出一行網域名（實測 <code>dig +short ipv6.google.com A</code> 印 <code>ipv6.l.google.com.</code>），直接數行數會把它算成成功。另外要知道這樣量到的失敗率會比程式看到的低：<code>+tries=1</code> 關掉了重試，而系統的 resolver 會自己重試幾次。</p>
<h2 id="dig-問到的答案不等於程式問到的答案">dig 問到的答案，不等於程式問到的答案</h2>
<p><code>dig</code> 走的是它自己的路徑。man page 寫明：未指定 <code>@</code> 時它讀 <code>/etc/resolv.conf</code> 取伺服器位址，然後直接對該位址發送查詢。而 <code>git</code>、<code>curl</code>、<code>ping</code> 走的是 <code>getaddrinfo</code>，也就是系統的解析函式，在 macOS 上經過 <code>scutil</code> 那份設定與 <code>mDNSResponder</code> 的快取。</p>
<p>分岔有兩個來源，要分開查。一個是<strong>問了不同的伺服器</strong>：<code>/etc/resolv.conf</code> 由系統生成、通常鏡射 <code>scutil</code> 的第一順位，但接管 DNS 的元件寫入這兩處的時機未必同步，兩邊不一致時 <code>dig</code> 跟程式打的就是不同的 resolver。當下比對一次就知道，<code>cat /etc/resolv.conf</code> 的 <code>nameserver</code> 跟 <code>scutil --dns</code> 的 <code>resolver #1</code> 對不對得上。另一個是<strong>繞過了中間那幾層</strong>：即使問的是同一台，<code>dig</code> 仍然不經過 <code>mDNSResponder</code> 的快取（包含把失敗結果記下來的負快取）、不讀 <code>/etc/hosts</code>、也不套用 <code>scutil</code> 那份多 resolver 的排序與網域比對。</p>
<p>實務後果是：<code>dig</code> 拿到位址、<code>git push</code> 仍然回 <code>Could not resolve host</code>，這個組合完全可能同時成立，而它不代表 <code>git</code> 有問題。</p>
<p>要重現程式實際看到的結果，改用走系統解析路徑的工具。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">dscacheutil -q host -a name github.com   <span class="c1"># macOS</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">getent hosts github.com                  <span class="c1"># Linux</span></span></span></code></pre></div><p>有位址回來就代表系統解析路徑通了（<code>dscacheutil</code> 印在 <code>ip_address</code> 欄，<code>getent</code> 直接印位址與名字）。這條檢查跟 <code>dig</code> 互補。<code>dig</code> 定位是哪個 resolver 壞了，<code>dscacheutil</code> / <code>getent</code> 確認程式那條路徑會不會通。</p>
<h2 id="處置與它的代價">處置與它的代價</h2>
<p>處置的通用形式是把本機 proxy 從路徑上移開，讓系統回到原本的上游 resolver。做法隨元件而異：Tailscale 交還 DNS 控制權、<code>systemd-resolved</code> 停掉服務並還原 <code>/etc/resolv.conf</code> 的符號連結、本機 <code>dnsmasq</code> 或廣告過濾則是停掉該服務後把介面的 DNS 設定改回 DHCP 取得的位址。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">tailscale <span class="nb">set</span> --accept-dns<span class="o">=</span><span class="nb">false</span>   <span class="c1"># Tailscale：交還系統預設 resolver</span></span></span></code></pre></div><p><strong>這個動作的前提是分流測試指向 proxy 本身。</strong> 上游已經死掉時移開 proxy 只會讓系統回落到那個死掉的上游，解析照樣壞、還一併失去該元件提供的能力，比動作前更糟。判讀表裡兩條 <code>dig</code> 都失敗的那一列就是這種情形，那裡的下一步是換上游或往可達性層查。</p>
<p>代價隨元件而異，而且差距很大。Tailscale 是 tailnet 主機名失效，<code>ssh my-vm</code> 要改回 tailnet 位址；廣告過濾是規則一併停用；<strong>公司 VPN 的 split DNS 客戶端則是整組內網域名解析不出來</strong>——內網名字本來就只有公司 DNS 答得出來，把本機 resolver 移開等於把那條路拆掉。內網域名故障時該診斷的是那條 scoped resolver，而不是把整個解析入口換掉。</p>
<p>重啟該元件是並列的另一條路，它保留原有能力、代價是恢復時間取決於它自己重連的速度，而且根因沒有被排除、同樣的抖動會再發生。臨時要趕工就移開它，想留著能力就重啟並記下發生頻率。</p>
<p><strong>沒有權限動它的情況要另外算。</strong> 受 MDM 管控的 VPN 客戶端與企業 DNS 過濾 agent 停不掉、也改不動介面設定，而這兩類正好都在本文射程內。此時剩下的動作是繞過與升級：用位址直連、把急用的名字暫時寫進 <code>/etc/hosts</code>、重連該客戶端，然後帶著兩條 <code>dig</code> 的輸出往 IT 報修。分流測試在這裡的產物是可交付的證據——它把「網路怪怪的」換成「這台 resolver 對這個名字回 SERVFAIL，同一時間它的上游回得出來」。</p>
<p>任一種處置之後，再讀一次權威設定（macOS 的 <code>scutil --dns</code>），確認第一順位換成了誰。同一個指令同時是處置前的診斷與處置後的驗收。</p>
<h2 id="判準">判準</h2>
<p><strong>一個元件被裝在某項功能的必經路徑上時，它的可用性就是那項功能的可用性上限。</strong> 診斷據此展開：症狀是「整個功能壞了」，要問的是「路徑上哪一段壞了」，方法是繞過可疑的那一段、對同一個問題直接問下一段。</p>
<p>「必經」要先確認，它是四個條件的合取，缺一個這條上限就不成立：那個元件是唯一的入口（沒有次順位可退）、它沒有網域限定（不是只管某個網域的 scoped resolver）、查詢沒有被快取接走、以及出問題的程式確實走系統的解析函式（瀏覽器的 DoH、直讀設定檔的靜態連結程式、容器內的行程都不走）。前兩條在讀權威設定那節有對應的檢查（是不是唯一一條、有沒有網域限定）。後兩條要靠比對：<code>dig @&lt;proxy&gt;</code> 直接對伺服器發問、完全不碰系統快取，而 <code>dscacheutil</code> / <code>getent</code> 走的是程式那條含快取的路。同一個名字兩邊問一次，結果一致就代表快取沒有插手、而且那個程式確實走系統解析函式；結果分岔就是這兩條其中之一沒有成立。</p>
<p>配套的第二條：<strong>間歇性故障用重複取樣判定，取樣次數跟著觀測到的成功率算。</strong> 成功率越接近一，要問的次數越多，而那正是最容易誤判成「已經好了」的情形。</p>
<h2 id="這條判準的邊界">這條判準的邊界</h2>
<p>同一個形狀出現在各種接管 DNS 的元件上：公司 VPN 的 split DNS 客戶端、Linux 的 <code>systemd-resolved</code>（<code>127.0.0.53</code>）、本機跑的 <code>dnsmasq</code> 或廣告過濾、企業裝的 DNS 過濾 agent。判別方式一致，先讀權威設定，確認第一順位是本機位址、而且滿足上面那四個條件。</p>
<p>第一順位落在<strong>另一台機器</strong>時，是同一個形狀的變體，單點仍然存在、只是它不在本機。架在路由器或 NAS 上的 Pi-hole、公司內網 DNS 都算，WSL2 也是——WSL 內看到的 <code>nameserver 172.x.x.1</code> 指向 Windows 虛擬網卡，真正接管 DNS 的元件在 Windows 那一側。分流測試同樣適用，差別在定位到它之後，處置要去那台機器上做；在 WSL2 內手改 <code>/etc/resolv.conf</code> 會在重啟後被自動生成蓋掉。</p>
<p>容器內的解析屬於另一個範圍。掛在自訂網路的 Docker 容器，其內嵌 resolver（<code>127.0.0.11</code>）只出現在容器自己的 <code>resolv.conf</code> 裡，宿主的 <code>scutil --dns</code> 看不到它，症狀也限縮在容器內。要查的是容器裡的 <code>cat /etc/resolv.conf</code> 與容器執行環境自己的 DNS 設定，本文的宿主層檢查在這裡全部會回報正常。</p>
<p><code>/etc/hosts</code> 的錯誤條目落在整張判讀表之外。它讓名字解析成一個錯的位址，症狀是連得到卻連錯，後續表現為逾時或被拒。之所以要在這裡列出，是因為 <code>dig</code> 對它沒有偵測力：<code>dig</code> 直接對 DNS 伺服器發查詢、不讀 <code>hosts</code>，兩條 <code>dig</code> 都會回來正確的位址。要看見 <code>hosts</code> 的影響，用走系統解析路徑的檢查（<code>dscacheutil</code> / <code>getent</code>），或直接讀 <code>cat /etc/hosts</code>。</p>
<p><strong>判準往 DNS 之外泛化時，吃兩個前提</strong>：下一段要能從發問端直接定址，而且要能用相同語意回答同一個問題。HTTP proxy 滿足（清掉 <code>http_proxy</code> 直打 origin）、連線池滿足（用新 client 繞過 pgbouncer 直連 DB）。service mesh 的 sidecar 兩個都不滿足——出向流量在同一個 network namespace 被攔截，發問端沒有「繞過去問下一段」這個動作可做。DNS 之所以是個好例子，正是因為任一台遞迴 resolver 都答得出公網名字，這兩個前提罕見地同時成立；而內網域名連 DNS 自己都不滿足第二條，這也是前面要求「用失敗的那個名字測」的理由。</p>
<p>事故本身自行恢復，這篇留下的因此是分流方法。故障自行恢復時，能重複使用的產物是「下次幾條指令內定位到哪一段」這個能力。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>完整的連不上分流（可達性層、服務層、Linux 的 <code>resolv.conf</code> 缺 nameserver、磁碟滿這類共同根因）：<a href="/blog/linux/debug/machine-unreachable/" data-link-title="機器連不到或起不來" data-link-desc="遠端機器突然 SSH 連不上、虛擬機開不了機、或懷疑磁碟滿引發連鎖故障時，從主機側與網路層的權威狀態往下定位是哪一環斷了">機器連不到或起不來</a></li>
<li>解析失敗 / 逾時 / 被拒三種症狀的分層判別：<a href="/blog/linux/dotfile/knowledge-cards/connection-refused-vs-timeout/#%e5%88%a4%e8%ae%80%e8%a8%8a%e8%99%9f" data-link-title="Connection Refused vs. Timeout（連線被拒與逾時）" data-link-desc="遠端連不上、要判斷是封包到不了還是服務拒絕、決定往網路層還是服務層除錯時回來讀">連線逾時 vs 連線被拒</a></li>
<li>MagicDNS 在正常狀態下換來什麼（也就是關掉 <code>accept-dns</code> 之後失去的能力）、以及 tailnet 的位址模型：<a href="/blog/linux/tools/remote/tailscale-tailnet-and-relay/" data-link-title="Tailscale 深入：tailnet、MagicDNS、直連與 DERP 中繼" data-link-desc="要用 Tailscale 讓散在不同網路的機器互連、遇到連得到但延遲高、或想讀懂 tailscale status 的 relay / direct 標示時回來讀">Tailscale 深入：tailnet、MagicDNS、直連與 DERP 中繼</a></li>
<li>「讀權威狀態而非表象」的一般紀律，本文的 <code>/etc/resolv.conf</code> 陷阱是它的一個實例：<a href="/blog/linux/debug/diagnosis-read-authoritative-state/" data-link-title="診斷心法：讀權威狀態，不靠肉眼猜表象" data-link-desc="Linux 上一個現象看起來像 A 卻可能是 B、想建立一套先讀權威狀態再下判斷的除錯紀律、避免看畫面就猜而猜錯時回來讀">診斷心法</a></li>
</ul>
]]></content:encoded></item></channel></rss>