エミュレータがビルドサーバーに到達できず、ホストのブラウザーでは問題なく開けるとき、役に立つ問いは VPN が動いているかどうかではない。ゲストに欠けているのは二つの仕組みのどちらか、という問いだ。仮想デバイスはエミュレータの NAT の背後で独自のネットワークスタックを動かしており、そのリゾルバー一覧は起動時に取られたコピーなので、あとから接続したトンネルはゲストの問い合わせ先を変えずにホスト側の経路だけを変える。この二つの層を理解すれば、この問題のほぼすべての変種を説明できる。

ゲストは固定 NAT の背後の別スタック

QEMU のユーザーモードネットワークは、ゲストを 10.0.2.15 に、ゲートウェイを 10.0.2.2 に、DNS サービスを 10.0.2.3 から 10.0.2.6 に置く。ゲストの外向きトラフィックはホストのカーネルで転送されるのではなく、ホスト側のソケットに変換される。ゲスト接続ごとに一つずつ生成され、それらのソケットはホストのアドレス空間から出ていく。だからゲストの接続は生成された瞬間のホストのルーティング判断を引き継ぎ、ゲストのルートテーブルにあなたのトンネルを指す項目は存在しない。

同じ仕組みがゲストに有用な抜け道を与える。10.0.2.2 はホストのループバックインターフェースの別名なので、ノート PC の 127.0.0.1 で待ち受けているサービスは、そのアドレスでゲストから到達できる。クライアントがループバックだけで公開しているものはこの経路で届き、これが後述のプロキシ構成の土台になる。

ゲストにできないのは、ホストのネットワークを自分の名前空間であるかのように観察することだ。ブリッジも、共有のルートテーブルも、継承されたリゾルバーもなく、あるのはパケットを受け取る NAT サービスと、仮想デバイスの起動時にランチャーが取ったスナップショットだけである。

DNS はスナップショットであり、生きた状態ではない

起動時にエミュレータはホストのリゾルバー一覧を読み、最大四つのサーバーをゲストに渡す。それらは 10.0.2.3 から 10.0.2.6 のアドレスで公開される。その時点でトンネルが落ちていれば、スナップショットにはノート PC が使っていた公開リゾルバーが入り、ゲストは生涯それに問い続ける。公開名は解決し、内部名は失敗し、症状はすべて VPN を指すが、実際に古い値はトンネルが上がる数分前に取得されたリゾルバー一覧なのである。

したがって順序の修正は俗説ではない。先にトンネルを接続し、そのあとエミュレータを再起動する。スナップショットの復元よりコールドブートを選ぶこと。クイックブートイメージは以前保存したリゾルバーとルートの状態で戻ることがある。ゲストが現在何を使っているかを見たいときは、ホストではなくデバイスに尋ねる:

adb shell dumpsys connectivity | grep -i -A2 dns
adb shell ping -c 1 build.internal.example

エミュレータは起動時に明示的なリゾルバーも受け付ける。ゲストがどのサーバーを使うべきか正確に分かっているときは便利で、そのサーバーが公開サーバーなら危険だ:

emulator -avd Pixel_API_34 -dns-server 203.0.113.53

公開リゾルバーは内部ゾーンのコピーを持たないので、一つに固定すると順序の問題が恒久的な障害に変わる。プロファイルがプライベート DNS を配るなら、トンネルに任せてゲストをコールドブートし、アドレスを直書きしないこと。

ルーティングは接続時にホストに従う

ゲストの接続はホストのソケットになるため、ルーティングの判断はソケットが作られたときに行われる。要求の前にトンネルが上がっていれば、要求はプロファイルのルートに従ってトンネルから出ていく。選択したプレフィックスだけを運ぶスプリットトンネルのプロファイルは、あなたのプライベートサブネットを平気で外に残すので、ホストへの到達性はゲストについての仮説であって結果ではない。既存のゲスト接続は作られたときの経路を保つ。だから再接続したクライアントがエミュレータ内で止まったダウンロードをいつも直すとは限らない。

検証はゲストの統計ではなく、ホストのインターフェースで行う:

sudo tcpdump -ni tun0 'host 203.0.113.10'

ゲストの要求がトンネルのインターフェースに現れればルーティングは機能しており、問題はその上にある。物理インターフェースに現れるならプロファイルがそのプレフィックスを覆っておらず、ゲスト側でどれだけ設定し直しても変わらない。

クライアントがループバックプロキシだけを提供する場合

一部の商用クライアントはシステムトンネルをまったく作らない。127.0.0.1 で HTTP または SOCKS のリスナーを公開し、アプリがそれを使うことを期待する。ゲストはそのソケットを直接見ることはできないが、エミュレータに使わせることはできる。拡張コントロール(Settings、次に Proxy、手動モード、ホスト 127.0.0.1 とクライアントが表示するポート)か、起動時だ:

emulator -avd Pixel_API_34 -http-proxy http://127.0.0.1:8080

ポートはクライアントの画面から写すこと。推測したポートは静かに失敗し、壊れたトンネルのように見える。これが何を覆うかを覚えておこう。HTTP プロキシ設定はプロキシを意識するアプリには尊重されるが、生のソケットは完全に無視する。ワークロードが自前で接続を開くネイティブバイナリなら、プロキシ経路はトンネルと等価ではなく、比較は明示的に行う必要がある。

結果はゲストの内側から証明する

同じセッションで二つのテストを行い、解決とルーティングを切り分ける。数値アドレスは応答するのに名前が失敗するなら DNS スナップショットの問題で、修正は順序か明示的なリゾルバーだ。両方失敗するならルーティングかポリシーで、次の確認はアプリが独自のネットワーク制限を持つワークプロファイル内で動いていないかである。

最後に、どのゲストで試しているかを知っておく。非常に古いエミュレータイメージは、ゲスト側 VPN クライアントが必要とする tun サポートなしで出荷されていたので、そこではどのプロファイルも動きようがなかった。現在のイメージには含まれている。現在のイメージで失敗するクライアントはクライアントかプロファイルの問題であり、ホストのトンネルを作り直しても結果は変わらない。

関連記事