VpnService protect() かアプリ除外か: ルーティングを直すのはどちらか
トンネルを運ぶソケットを保護するか、アプリ全体を除外するか。Android VPN に必要な範囲を決める三つの条件。
トンネル自身のパケットをトンネルの外に保つ方法は二つある。トランスポートのソケットを保護するか、アプリ全体を VPN から外すかだ。両者は別の問題を解くもので、配線が楽だからと広い方を選ぶのが、アプリが気づかないままローカルネットワークへのアクセスと分割 DNS を失う道である。トンネルの外に残すべきものから始め、それを達成する最小の範囲を選ぼう。
protect() がトンネルから外すもの
VpnService.protect()はソケット単位の呼び出しである。トンネルを運ぶソケットかディスクリプタを渡すと、Android はそのソケットに印を付け、通信が VPN インターフェースではなく下位のネットワークを使うようにする。アプリが送るその他のものは、これまで通り通常の VPN ルートに従う。
その狭い範囲が要点だ。保護されたソケットは、HTTP クライアントが名前を解決する方法も、分析の呼び出しが使うインターフェースも、後から開かれる二つ目の接続の扱いも変えない。
範囲を決める三つの条件
第一の条件は、トンネルのトランスポートだけを外に出すのかどうかである。アプリの他の通信が引き続き VPN を通るべきなら、ソケット保護が正しい範囲だ。第二の条件は、ソケットの生成を自分で制御できるかどうかである。内部の奥深くで自前のソケットを開くライブラリは、フックを公開するか構築済みのソケットを受け取らない限り、protect()を呼ぶ場所を残さない。第三の条件は、アプリがローカルネットワークや captive portal に直接到達する必要があるかどうかで、これはトンネルを生かし続けるのとは別の要件だ。
アプリをaddDisallowedApplication()で除外するのは荒っぽい道具だ。アプリが作るすべての接続を VPN から外し、監視したかったものも含めてしまう。さらに一部の端末ではローカル検出の挙動も変わる。
第三の選択肢は、トランスポートを選んだネットワークに縛ることだ。bindProcessToNetwork()やソケット単位のネットワークハンドルを使う。端末が Wi-Fi と携帯回線を同時に持っていて、トンネルを特定の上り回線に乗せたいときに役立つ。
トランスポートを作っている間に呼ぶ
呼び出しはprotect()トランスポートを生成する同じブロックで、connect()やsend()より前に行う。呼び出しが false を返したらソケットを閉じ、セットアップを失敗させる。続行すると、パケットが自分のインターフェースに再び入りうるトランスポートを抱えることになる。
形は一箇所に収まるほど小さい。
DatagramSocket transport = new DatagramSocket();
if (!protect(transport)) {
transport.close();
throw new IOException("transport not protected");
}
transport.connect(serverAddress);ここで三つの細部が効いてくる。第一に、真偽値が得られる唯一の信号であるため、同意の取り消しと拒否された呼び出しを区別できるだけの文脈を付けて失敗を記録する。第二に、protect()が覆うのはソケットであり、ディスクリプタ番号ではない。ソケットを閉じ、後で同じ整数を別のソケットに再利用すると、新しいソケットは保護されない。第三に、ネイティブライブラリがトランスポートを開く場合、ネイティブコードが接続する前にディスクリプタを保護する。そうしないとルートがすでに選ばれているかもしれない。VpnService のリファレンスは、対象の API レベルが実際に提供するオーバーロードを確認する場所である。
幸せな経路だけでなく、遮断された経路を試す
トンネルを立ち上げた状態で検証する。二つの経路が違いうるのはその状態だけだからだ。保護されたトランスポートから一つ、protect()を呼んでいないコード経路からもう一つ要求を送り、遠端から何が見えるか比べる。保護された接続は下位のネットワークを送信元として届き、保護されていない接続はトンネル自身のアドレスを示してトンネルのバイトカウンタを動かすはずだ。端末上では、ssやnetstatが、何かが無線から出る前に同じ分離を示す。
取り消しは別に試す。同意の変更はソケットのコードに触れずにインターフェースを落とすからだ。VPN の同意を取り消すかサービスを停止し、トランスポートを作り直して、protect()が依然として true を返し、同意が再び与えられるとトランスポートが再接続することを確認する。
保護した要求がそれでもトンネルを辿るなら、保護の呼び出しはソケットがすでにルーティングされた後に起きている可能性が高い。もっと前に移して再試験する。
判断はトランスポートの設定のそばに書き残す。どのソケットを保護し、どのアプリ通信を除外し、保護が失敗したときにアプリが何をするか。この一行が、一つのテストが落ちたからといって後のリファクタリングが除外をアプリ全体に広げるのを止める。次の人がマニフェストを逆に読まずに意図を確認する手段にもなる。