VPN のキルスイッチがポート 53 を塞ぐと DNS が引けなくなる理由
キルスイッチは DNS を意図的に塞ぐことがある。トンネル内だけで引く方式と、ローカルリゾルバーを例外にする方式を比べ、どちらの層が実際に失敗したのかを見極めたい。
キルスイッチがトンネルを落とすと、DNS の経路も一緒に落ちる。そして残されたリゾルバーは、期待したものではないことがほとんどだ。この瞬間の扱い方は二つの設計で大きく異なる。トンネル内にない DNS パケットをすべて塞ぐか、あるいはリゾルバーを一つだけ例外として残すかだ。どちらも筋は通っている。ブラウザーのタブから見ると症状はほぼ同じなので、まず自分がどちらの設計を配備したのかを突き止める必要がある。
この区別を常に手元に置いているのは、直し方が場合によって正反対だからだ。一方の設計には広い例外が要り、もう一方には狭い例外が要る。当て推量で進めると、プライバシーか半日かのどちらかを失う。
同じ障害に対する二つの DNS 設計
設計 A はリゾルバーを、トンネルの向こう側にあるただのサービスの一つとして扱う。ファイアウォールの規則は UDP と TCP の 53 番をトンネルのインターフェース上だけに許し、クライアントに渡されるアドレスはトンネル内のリゾルバーだ。トンネルが落ちれば名前引きは止まるが、それは不具合ではなく意図した結果である。
設計 B はローカルネットワーク上にリゾルバーを到達可能なまま残す。通常は 192.168.1.1 のルーターか、127.0.0.53 の OS のスタブだ。名前解決はトンネルと一緒に死ぬべきではない、という理屈である。家庭内 LAN でルーターが家じゅうの名前を引くなら便利だ。しかしホテルや空港の Wi-Fi では、同じ例外が他人のサーバーを指すことになり、ウェブの通信が塞がれたままでも、打ち込んだ名前はすべて開示される。
境界線は、リゾルバーを誰が持っているかだ。ローカルのものを信頼するなら、例外は小さく正直な穴になる。信頼しないなら、トンネル内だけで引く設計だけが持ちこたえる。代償は、落ちたトンネルがすべての名前を持ち去ることだ。
VPN クライアント自身の名前引きが生き残る理由
クライアントはトンネルができる前にゲートウェイのホスト名を引かなければならない。だから厳格なブロックが、クライアントの起動経路まで覆ってはいけない。多くのクライアントはエンドポイントのアドレスをキャッシュするか、その一つのホストに限った起動例外を文書化して提供している。もし規則がその名前引きを塞ぐと、トンネルは二度と戻らず、ログには自分のサーバーに届かないクライアントが残る。これは DNS の欠陥ではなく、ポリシーの欠陥だ。
スプリット DNS も、これが顔を出す場所だ。クライアントは内部ゾーンをプライベートなリゾルバーに押しやり、それ以外はすべてシステムのリゾルバーに任せる。落ちたあと、公開名はまだ引けるのに内部名はタイムアウトする。プライベートゾーンに経路が残っていないからだ。利用者には「VPN がインターネットを壊した」に見え、リゾルバーのログには「決して届かなかった問い合わせ」に映る。
systemd-resolved の下なら、その分割を直接見られる。resolvectl statusはリンクごとの DNS サーバーとルーティングドメインを一覧にする。トンネルが落ちたあとも、内部ドメインはもう存在しないリンク上のサーバーを指し続ける。127.0.0.53 のスタブは問い合わせを今も受け取り、そのあと応答しない上流を待つ。だからこの失敗は即座に返らず、数秒かかるのだ。
暗号化 DNS はパケットフィルターを迂回しない
DNS over HTTPS と DNS over TLS は 443 と 853 を通る。だから 53 番を塞ぐ規則は、それらについて何も言っていない。ポートで「DNS」を塞ぐのは、ポートについてのポリシーであって、名前解決についてのポリシーではない。キルスイッチがトンネル外の通信をすべて塞ぐなら、DoH は偶然カバーされる。キャプティブポータルや公開リゾルバーに例外を空けたなら、カバーされない。
安く済むのはクライアント側の制御だ。Firefox はカナリア名use-application-dns.netが解決できないと自分の DoH リゾルバーを手放す。管理されたネットワークがこのドメインを NXDOMAIN として公開するのはそのためで、企業ポリシーでも無効に固定できる。Android Private DNS と Chrome のセキュア DNS には、それぞれ独自のスイッチと既定値がある。それらを切るほうが、リゾルバーのアドレスを追いかけるより確実だ。アドレスの一覧は変わるが、あなたの規則は変わらないからだ。
これは漏えいの話ではない場合もある。リゾルバーを固定したアプリでも、そこへ届くネットワーク経路は要る。暗号化が変えるのは誰が問い合わせを読めるかであって、パケットが機械の外に出られるかではない。だから端末全体のブロックは、その設定に触れずに無効化してしまう。
症状を層に当てはめる
報告の多くは一文で届き、その一文はたいてい層の名前をもう言っている。下の表は、規則に触る前に私がたどる順序だ。狙いは、問い合わせが塞がれたのか、誤って送られたのか、そもそも送られなかったのかを決めることにある。
| Symptom | Likely layer | First check |
|---|---|---|
| Every name fails at once, instantly | Resolver answering with a refusal | Query the resolver by address, not by name |
| Lookups hang for seconds, then time out | Packets dropped with no response | Watch port 53 on the physical link |
| Public names resolve, internal ones do not | Split-DNS routing domain | Per-link DNS servers on the tunnel |
| Names resolve, the page still fails | Not DNS: routing or MTU | Connect to the IP directly |
| Only one application fails | Pinned resolver or cached answer | Check that app's DNS setting |
最初の二行こそ、人が誤診しやすいところだ。静かな破棄と拒否は、ブラウザーではどちらも「DNS が壊れた」に見える。だがキルスイッチが仕事をしていると分かるのは片方だけだ。拒否は何かが答えた証拠であり、ブロックはあなたが思う場所にはない。
リゾルバーのポリシーを戻し、それから証明する
直すべきは症状ではなく設計だ。トンネル内だけで引くなら、トンネルのインターフェースで 53 番を許し、内部リゾルバーをトンネル設定と一緒に配布し、物理リンクには DNS の許可を一切残さない。wg-quick では、リゾルバーをインターフェース設定に書き込むのはDNS =の行だ。だからそれを省いたトンネルは古いリゾルバーをそのまま残し、すべての問い合わせが誤った経路に流れる。wg-quick のマニュアルが、その振る舞いをプラットフォームごとに記している。
次にこの順で確かめる。Linux ではresolvectl statusが有効なリンクを示し、dig +short example.comが公開名を試し、dig @10.8.0.1 internal.example.comが内部名を試し、sudo tcpdump -ni any port 53が、両方を実行している間にパケットがどのインターフェースを使うかを見せる。Windows ではGet-DnsClientServerAddress -AddressFamily IPv4とResolve-DnsName example.comが同じ二つの事実を与える。誰が設定されているか、そして誰が答えているかだ。
最後に否定テストで締める。トンネルを切断し、新しい公開名が失敗することを確かめ、内部名も失敗することを確かめ、そして53 番のパケットが物理アダプターから出ていないことを確かめる。キャッシュされた応答にしか効かないブロックは、まったく効いていない。
チケットを閉じる前のチェックリスト:
- 障害中に使われていたリゾルバーは、ポリシーが意図したものだ。
- クライアント自身のエンドポイント引きは、キャッシュか例外によって今も許されている。
- DoH と DoT は、ブロックに含まれているか、ポリシーで無効になっている。
- トンネルが消えたら、プライベートゾーンは開くのではなく閉じる方向に失敗する。