ノート PC は VPN につながっていて、dig service.internal.example は 10.x のアドレスで応答し、curl は依然として Could not resolve host で終了します。トンネルが問題ではありません。dig のクエリは何の不平もなく運ばれています。違うのは、誰が問い合わせているかです。dig は自分で指定した 一台のサーバーに 一つのクエリを送り、curl は OS の検索チェーンを通ります。そのチェーンには、どのサーバーに問い合わせるかについて独自の規則があります。

同じ名前に対する 二つの検索

dig は独自の stub resolver です。/etc/resolv.conf を読み、ネームサーバーを選び、A または AAAA のクエリを 一つ送って応答を表示します。nsswitch は完全に飛ばし、systemd-resolved や NetworkManager がそのファイルの外に持っているリンク単位の状態を何も知りません。だから dig の応答が正しくても、アプリケーションが何を見るかは何も語りません。

アプリケーションは getaddrinfo を呼び、getaddrinfo はより長いチェーンをたどります。まず nsswitch のモジュール順、次にその名前を扱うモジュール、最後に現在のリンク設定から組み立てられた resolver です。systemd のマシンではクエリは 127.0.0.53 に送られ、systemd-resolved が tun インターフェースに結び付いたルーティングドメインに基づいてリンクごとの上流サーバーを選びます。

二つのコマンドが、推測せずにこれらの層を切り分けます:

getent ahosts service.internal.example
resolvectl query service.internal.example

getent は curl と同じ経路を使うので、dig が動いているのに getent が失敗すれば、障害はパケットのルーティングではなく検索ポリシーにあります。resolvectl query は応答を返したインターフェースと上流も表示するので、トンネルのリンクと物理リンクのどちらが判定を制したかが分かります。

アプリケーションが実際に使う resolver を読む

/etc/resolv.conf はほとんどのデスクトップで生成され、VPN クライアントがこれに関わる方法は三つあり、ネームサーバーを先頭に足す、ファイルを上書きする、自分のサーバーを NetworkManager に渡してデーモンに書き直させる、です。リンク単位の DNS が保たれるのは三つ目だけで、サーバーが一つのグローバルな一覧に平坦化されず、インターフェースに結び付いたままになるからです。

NetworkManager のシステムでは、結果は /etc/NetworkManager/NetworkManager.conf の backend に依存します:

[main]
dns=systemd-resolved

dns=default の場合、クライアントのサーバーは勝った順序で resolv.conf に混ぜられ、プライベートな検索がなお公開 resolver に送られることがあります。リンクを理解する backend、systemd-resolved または dnsmasq なら、トンネルのサーバーはトンネルに限定され、そのルーティングドメインに一致する名前だけがそこへ送られます。この限定こそが実務における split DNS の意味であり、クライアントが対応をうたうときに最も欠けている部分です。

何かを変える前に状態を読みます:

resolvectl status
resolvectl dns tun0

tun インターフェースに DNS サーバーと、プライベートなサフィックス用のルーティングドメインが並び、~internal.example と表示されていてほしいところです。リンクにサーバーがなければ、クライアントは何も渡していません。サーバーはあるのにドメインがない場合、検索はすべてそこへ行くか、まったく行かないかのどちらかで、クライアント次第であり、どちらも split DNS ではありません。

nsswitch が検索を早く終わらせる場所

標準的なデスクトップの行はこうなっています:

hosts: files mdns4_minimal [NOTFOUND=return] resolve [!UNAVAIL=return] dns myhostname

mdns4_minimal はマルチキャスト DNS を扱い、.local で終わるあらゆる名前に権利を主張します。api.infra.svc.cluster.local のような Kubernetes のサービスも .local で終わります。mDNS モジュールが NOTFOUND を報告すると、アクション [NOTFOUND=return] は dns モジュールへ進まずに検索全体を終わらせるので、名前が VPN の resolver に届くことはありません。dig はこのファイルを完全に迂回します。二つの道具の結論が食い違うのはそのためです。

これがこの問題で最も鋭い対比で、行き着く先は VPN より古い設定ファイルの 一語です。アクションを [NOTFOUND=continue] に変えれば、プリンターやローカル機器のために mDNS を残しつつ、DNS に名前を見せられます。行を hosts: files dns に置き換えても動きますが、より乱暴です。実際に使っているプリンターの .local 解決を失います。

一つのネットワークにしか存在しない名前

プライベートゾーンが権威を持つのは 一か所だけです。ip-10-0-4-31.eu-west-1.compute.internal のような AWS の名前は、そのリージョンの VPC resolver を通じて応答され、VPN の端点がリソースと同じリージョンにある間だけ通用します。フランクフルトの端点につないでアイルランドの compute.internal 名を問い合わせても、ローカルの設定では何も救えません。そのレコードを持つゾーンにはそこから到達できないからです。

検索ドメインは裏返しの姿を生みます。裸の service は resolver が適切と見なすサフィックスで補完され、VPN リンクがあるとローカルのドメインが会社のドメインに勝つことがあります。末尾にドットを付けて問い合わせれば推測を排除できます。getent ahosts service.internal.example. が通るのに短い名前が通らないなら、直すべきはトンネル側の検索ドメインであり、サーバーのアドレスではありません。

管理されたクライアントは、システムの resolver の上にもう 一層足します。Windows の VPN プロファイルは独自の名前解決ポリシー、アプリごとの規則、トンネルが落ちていても効く NRPT テーブルを持てます。独自の DNS キャッシュを持つアプリケーションは .NET や Java の実行環境でよく見られ、チェーンが直った後も古い失敗を長く保ちます。Apple のクライアントは split DNS を resolv.conf ではなく DNS 設定の一致ドメインで構成し、Chromium や Firefox は安全な DNS モードで公開名についてシステムの resolver を迂回することがあります。Microsoft の VPN 名前解決のリファレンスはそのポリシー層の Windows 側を説明しています。

どの層を直すかを決める

修正を順に試すのではなく、症状から層へと進みます。

SymptomWhat it meansCheck first
dig answers, getent failslookup chain, not the packet pathnsswitch line and resolvectl status
Timeout rather than NXDOMAINprivate resolver unreachable through the tunnelresolvectl dns tun0, route to that address
NXDOMAINan authority answered and does not know the namezone owner, endpoint region
FQDN works, short name failssearch suffix mismatchresolvectl domain on the tun link
getent works, app failsapp cache or its own resolverrestart the app, check secure DNS

二つの行がほとんどの重みを担います。タイムアウトは到達性の問題です。プライベートな resolver がトンネル経由で応答していないので、そのアドレスへの経路と、リンク上の DNS サーバーが思っているものかを確認します。NXDOMAIN は権威の問題です。何かが応答し、その名前を知らないということで、ゾーン、リージョン、あるいは本来このクエリを受け取るはずのない resolver を指します。

最後の行は、両方の半分が成功に見えるために見落とされます。短い名前が FQDN と違う解決をするのはサフィックスの不一致であり、トンネルをつなぎ直してもクライアントが渡すサフィックスは変わりません。

設定ファイルを編集する前に状態を記録しておけば、その編集が何かの役に立ったかを言えます。

resolvectl status > before.txt
getent ahosts service.internal.example >> before.txt

チェックはこの順で実行し、最初に失敗したところで止めます。getent と resolvectl query は一致しなければなりません。getent が失敗して dig が応答するなら、検索チェーンにとどまり、ルーティングには触れません。resolvectl status は tun インターフェースに DNS サーバーとルーティングドメインを示す必要があります。nsswitch はテスト中のサフィックスについて NOTFOUND で早く戻ってはいけません。名前は端点が接続しているネットワークで権威を持ち、末尾のドットで検証されている必要があります。アプリケーションは独自のキャッシュや安全な DNS の上書きを保持してはいけません。

失敗した最初の層だけを直し、それから同じコマンドをもう一度実行します。二つ同時に変えると、どちらが効いたのか言えなくなり、次の障害は調査をゼロからやり直すことになります。

関連記事