ログに異常は見えません。インターフェースは稼働し、リゾルバーは VPN の DNS サーバーを報告し、ディスク上のファイルにもそれが並んでいます。それでも内部ホスト名はローカルルーター経由で解決されます。この三つの記述は矛盾していません。ファイル、リンクごとの状態、検索の経路は、たまたま同じ名前を共有している別のオブジェクトだからです。以下では、systemd-resolved を実行するホストでクエリが実際にたどる経路を説明します。

resolv.conf という名の三つのオブジェクト

resolved を実行するホストでは、/etc/resolv.conf は通常 /run/systemd/resolve/stub-resolv.conf へのシンボリックリンクです。そのリンク先にはローカルの stub リスナーのアドレス 127.0.0.53 と、resolved が計算した検索ドメインが入っています。これは状態の表示であり、状態そのものではありません。

状態は resolved のリンクごとの設定にあり、だからこそ resolvectl status はファイルと食い違っても正しいことがあります。各リンクは独自のサーバーと独自のルーティングドメインを持ち、クエリが届くと stub がそのデータを参照します。systemd-resolved のマニュアル は生成されるファイルとリンクごとのモデルを説明しています。表示と状態の分離こそ覚えておく価値のある部分です。シンボリックリンクのリンク先を変えても変わるのは従来のクライアントが読む内容であり、リゾルバーが知っている内容ではありません。

三つ目のオブジェクト /run/systemd/resolve/resolv.conf は、すべてのリンクを一つのサーバー一覧に平坦化します。そのファイルを参照するプログラムは、クエリを一覧のサーバーへ直接送って stub を飛ばすため、リンクごとのポリシーはもう適用されません。したがって同じマシン上の二つのプログラムが同じ名前に違う答えを返すことがあり、どちらも壊れてはいません。

NSS を通る一つの検索を追う

glibc は名前の答え手を決めるために /etc/nsswitch.conf を読みます。resolve を含む hosts 行は問い合わせを nss-resolve に回し、それが D-Bus 経由で resolved に問い合わせてリンクごとの答えを受け取ります。files dns だけの行は、/etc/resolv.conf が持つアドレスへクエリを送ります。

この一行がブラウザーと dig の食い違いのほとんどを説明します。dig は resolv.conf を直接読み、NSS には一切触れないため、アプリケーションが使う経路ではなく stub や上位サーバーを試します。getent hosts internal.example は NSS を通り、その答えを dig の答えと比べることが最初の有用な測定です。

独自のリゾルバーを同梱するアプリケーションや、固定のエンドポイントへ HTTPS 上の DNS で話すアプリケーションは両方の経路を迂回します。resolv.conf にも resolved の状態にも、それらを制約するものはありません。

ファイルの所有者と参照先を確認する

何かを変える前にリンクの連鎖を読みます。どのファイルが正なのかは所有権が決めるからです。以下のコマンドはどれも状態を読むだけです。

stat -c '%N' /etc/resolv.conf
resolvectl status --no-pager
grep '^hosts' /etc/nsswitch.conf
ls -l /run/systemd/resolve/

最初の行はリンクとその参照先をまとめて出力します。VPN スクリプトが所有する通常のファイルなら、リゾルバーはその書き込みを入力として扱いません。状態の出力にはリンクごとのサーバーと、各クエリの行き先を決めるルーティングドメインが並びます。nsswitch の行は stub をそもそも参照するかを示すので、ファイルに stub アドレスがあってもそこに resolve がなければ、クエリはローカルのリスナーに届く一方で分割を管理するものが何もないことになります。最後の一覧も重要です。stub-resolv.conf のないホストはここで説明した構成を動かしていません。

連鎖が別のパッケージの管理するファイルで終わるなら、VPN クライアントとそのパッケージが調整すべき二人の所有者です。リンクを置き換えると一方の作業を捨て、もう一方の作業をそのまま残すことになります。

リンクの所有者の側で直す

VPN 接続は自分のサーバーとドメインを自分のリンクに登録し、切断時にそれを取り下げるべきです。NetworkManager の resolved 連携もクライアント独自のヘルパーもそう動きます。resolv.conf を書き換えるだけのスクリプトはそうではありません。

すでに存在するリンクでは、実行時の設定が仕組みを示します。resolvectl dns tun0 10.40.0.53 はサーバーを結びつけ、resolvectl domain tun0 '~corp.example' はルーティングドメインを結びつけます。チルダが要の文字です。チルダはその接尾辞のクエリをこのリンクに閉じ込め、ほかのすべてをそのままにします。一方、チルダのない同じ値は検索ドメインになり、分割は消えてしまいます。これらのアドレスは実験用の例であり、永続的な設定ではありません。resolvectl query host.corp.example で確認し、どのリンクが答えたかを読みます。

登録されたサーバーにも経路が要ります。クエリが正しいリンクに届いてタイムアウトするなら、原因はリゾルバーの所有権ではなく、トンネル経由の到達性です。

機能している状態とは

健全な結果は三つの表示すべてで一致します。リンクごとのサーバーが VPN の配った内容と合い、ルーティングドメインが内部の接尾辞だけを覆い、重要なアプリケーションが内部名を解決しつつ、公開名はなおローカルの経路をたどります。クエリが一つ成功することは、その一致より弱い証拠です。

そのあと切断して、両方のクエリを繰り返します。切断後もドメインを登録したままにするクライアントは、数時間後に内部の名前解決で失敗するマシン を作ります。トンネルは開いておらず、明らかな原因もありません。

関連記事