端末を再起動し、常時接続が有効で、「VPN なしの接続をブロック」もオンなのに、トンネルが一向に上がらない。設定は何も間違っていません。ブロックは役目を果たしており、クライアント自身がエンドポイントへ到達しようとする試みもその対象です。エンドポイントは、トンネルが存在する前に、他のすべてのアプリが通信を許されないなかで通信を許されたクライアントによって解決されなければなりません。どの構成要素がそれを担い、どの順序で行うのかを理解すれば、この謎は設定の問題に変わります。

VPN ビルダーが実際に求めているもの

Android の VPN アプリは、プラットフォームにサーバーアドレスを渡しません。ローカルのインターフェースを構築します。トンネル用のアドレスとルート、その内部でアプリが使うべきリゾルバーを指定し、その後 establish() がファイルディスクリプターを返し、アプリはそれを生の IP パケットとして読み書きします。リモートのエンドポイントはそのビルダーには一切現れません。

これが構造上の要点です。プラットフォームは tun インターフェースを作り、そこにポリシーを適用します。アプリはユーザー空間でトランスポートを実装するので、エンドポイント名の解決も暗号化された接続の開始もアプリ自身が行います。したがってホスト名のエンドポイントが可能なのは、プラットフォームが名前をサポートしているからではなく、プラットフォームがエンドポイントを一度も見ないからです。

ビルダーのフィールドでアドレスを受け取るのは一つだけ、DNS サーバーです。これは数値のリゾルバーアドレスでなければならず、通常は事業者がトンネル経由で問い合わせたいものです。このフィールドは、トンネルが上がった後にアプリがクエリをどこへ送るかを表します。クライアントがどうやってエンドポイントを見つけるかは何も語っておらず、この二つを同じ設定として扱うところから混乱が始まります。

DNS の問いは二つある

第一の問いは、トンネルが上がった後、アプリはクエリをどこへ送るのかです。答えはトンネル内側のリゾルバーアドレスで、tun インターフェース経由で到達でき、構造上かならず数値です。

第二の問いは、そのトンネルが存在する前に、クライアントは自分のエンドポイントをどう見つけるのか、です。この検索はトンネルの外、下位のネットワーク上で、クライアントのプロセス内で行われます。名前を使えますが、VPN なしで端末が到達できるリゾルバーを使わなければなりません。

StageWho resolvesPathFailure looks like
Endpoint lookupThe VPN appUnderlying network, protected socketNever connects; no tunnel interface traffic
Peer authenticationClient and serverTunnel transportConnects then drops; certificate or key error
Private name lookupApps via the tunnelResolver inside the tunTunnel up, internal sites do not resolve

三つの段階、三つの異なる症状。「接続できません」という一つのエラーメッセージはそれらを一つに潰してしまい、そのために人々は、まだ参照される前の段階で起きている問題を直そうとして DNS サーバーのフィールドを編集してしまうのです。

ブートストラップが行き詰まる理由

常時接続でブロックが有効ということは、VPN の内側にない通信が破棄されるということで、VPN を所有するアプリ自身の通信も含まれます。脱出口は明示的です。アプリはソケットに対して protect() を呼び、これがソケットを下位のネットワークに結び付けるので、トンネルを迂回して外へ出られます。どの VPN クライアントも自分のトランスポートのためにこれをしなければ、サーバーに到達できません。

ここで、エンドポイント名を知る唯一のリゾルバーがトンネルの内側にある配備を考えます。クライアントは名前を解決するためにトンネルを必要とし、トンネルを作るために名前を必要とします。これは真正の循環依存であり、クライアントの設定では解決できません。バグではなく設計の誤りです。

タイミングは同じ依存をより穏やかな形で露呈させます。起動時、最初のロック解除前には、端末にまだ使えるネットワークがないことがあります。キャプティブポータルではネットワークはありますが、ポータル以外をすべて遮断します。Wi-Fi からモバイルへの切り替えでは、保護されたソケットの下位ネットワークが変わるので、クライアントはそれに気づいて再解決しなければなりません。どれもブートストラップの出来事であり、どれも午前 3 時に発見するのではなく、意図的にテストする価値があります。

答えを固定することが解決にならない理由

誘惑される回避策は、名前を一度解決して得られたアドレスを数値フィールドに貼り付ける、あるいはカスタムビルドに埋め込むことです。動いているように見え、名前をスナップショットに変えてしまいます。事業者がエンドポイントを移すと、移行、anycast の変更、新しいリージョンのアドレスといった理由でクライアントは古いほうへかけ続け、UI のどこにも理由は説明されません。

DNS レコードに TTL があるのは、まさに答えが変わりうるからです。キャッシュはその寿命に基づいて定義され、永遠ではありません。答えを固定することは、名前をアドレスより優れたものにしている仕組みを捨てながら、名前を使った運用コストは抱え続けることになります。

固定は二つ目の悪い習慣とも一緒に現れがちです。接続の失敗の仕方が変わったからといって、識別の検証を緩めることです。サーバー識別の検証はブートストラップの問題の一部ではなく、弱めると DNS の不便を傍受のリスクと交換することになります。

再起動に耐える設計

耐久性のある構成は地味です。保護ソケットを正しく実装し、エンドポイントを下位ネットワークで解決するクライアントを使い、公開で解決できる、またはトンネル前に到達できるリゾルバー経由で解決できるエンドポイント名を与えます。ブロックは有効にしておきます。DNS を動かすために丸ごと無効化すると、その配備が存在する理由の性質を失います。

エンドポイントを非公開に保つ必要がある場合は、プラットフォームが使える明示的なブートストラップ経路を用意します。トンネルなしで到達できるリゾルバー、または事業者が維持する管理接続です。そしてその例外は狭く保ち、ユーザーのトグルではなくポリシーが所有するものにします。Android の VpnService のドキュメントは、保護とライフサイクルについてプラットフォームが何を保証するかの参照であり、VpnService.Builder のリファレンスは、DNS フィールドが数値アドレスを受け取ることを確認しています。

そのうえで再起動後の新しい接続で、三つの段階を順に検証します。エンドポイント名が解決すること、トンネルが確立してピアが認証されること、非公開の名前がトンネルのリゾルバー経由で解決すること。第一段階が失敗すれば、後のどれも動きません。修正はブートストラップ経路に属し、リゾルバーのフィールドでは決してありません。

  • エンドポイントの解決はアプリ内で、トンネルの外で、保護されたソケット上で行われる。
  • ビルダーの DNS フィールドはトンネル内のアプリのためのもので、クライアント自身のエンドポイント用ではない。
  • トンネル経由でしか到達できないリゾルバーは、遅い起動ではなく行き詰まりである。
  • 再起動後にエンドポイント、認証、非公開の名前解決を別々にテストする。

関連記事