Android VpnService が一部の通信だけを捕捉する理由
ルート、DNS エントリ、アプリごとの範囲が、どのパケットが tun デバイスに届くか、ローカル機器が消える理由、ビルダーが拒否するものを決めます。
VpnService そのものが通信を横取りするわけではありません。システムに tun インターフェースを要求し、そのインターフェースに一連のアドレスとルートを渡すと、その瞬間からカーネルは、それらのルートに一致するすべてのパケットを、アプリが読めるファイルディスクリプタへコピーします。アプリのコードはルートが選ばなかったパケットを決して見ないため、トンネルの範囲はプログラミングの問題になる前にルーティングテーブルの問題です。だからこそ同じサービスの実装が、ある端末ではすべてのフローを取り込み、別の端末では一部だけを取り込み、ルートを追加すると壊れて見えたトンネルが直ることがあるのです。得られるものは宣言したものにほかなりません、ビルダーが黙って拒否したものを除いて。
どのパケットが届くかをルートが決める
Builder.addRoute(prefix, prefixLength) は VPN ネットワークのルーティングテーブルにプレフィックスをインストールします。システムはそのネットワークを範囲内のアプリの既定とみなすため、そのエントリは物理ネットワークの既定ルートより先に参照され、一致するプレフィックスだけでフローを迂回させられます。したがって 0.0.0.0/0 へのルートは IPv4 を丸ごと取り込み、プリンタ、cast の宛先、ルーターの管理ページまで含みます。
物理的な既定エントリを削除せずにその範囲を得たいクライアントは、代わりに 0.0.0.0/1 と 128.0.0.0/1 の組を使います。より具体的な二つのルートは既定ルートに勝り、合わせてアドレス空間全体を覆い、端末自身の既定エントリは物理ネットワークに束縛されたままのソケットのためにテーブルに残ります。
アドレスと DNS エントリは同じビルダーから来て、別の問いに答えます。addAddress はプレフィックス長付きのアドレスをインターフェースに与え、addDnsServer は範囲内のアプリがインターフェース経由で問い合わせるリゾルバをインストールします。両方のファミリが一つのインターフェースに載れるため、IPv6 アドレスの隣に IPv4 だけのルート集合を置く構成は、正しく読めてなお漏れます。インターフェースがアドレスを持たないファミリをルートすると、カーネルにはそのパケットの送信元アドレスがなく、ディスクリプタに現れることはありません。
establish() が返すもの
establish() は ParcelFileDescriptor を返し、アプリが許可されていなければ null を返します。null はネットワークエラーではなく同意の状態で、VpnService.prepare() がすでに null を返しているか、ユーザーが呼び出し前にシステムダイアログを承認している必要があります。サービス実行中に同意が取り消されることがあり、インターフェースは読み取りループへのコールバックなしに足元で閉じます。
ディスクリプタは特に指定しなければ非ブロッキングなので、読み取りはスレッドを止める代わりにデータなしで即座に戻ります。ブロッキングソケット向けに書かれたループは一つのコアを焼き、空の読み取りを致命的とみなすループはアイドルの一秒でトンネルを引き裂きます。Builder.setBlocking(true) はその挙動を変え、代価は読み取りの内側で待つスレッドです。
ルートとアドレスの引数は LinkAddress の値として検証され、224.0.0.0/4 のようなマルチキャスト宛先は即座に拒否されます。ポイントツーポイントの tun インターフェースにはブロードキャストドメインもないため、それを介したマルチキャスト探索にはもう一つのルートではなく別の設計が必要です。確認すべき一覧はVpnService.Builderであり、呼び出しが無視されたと結論する前に見てください。
アプリごとの範囲は集合を狭めるのであり、トンネルを狭めるのではありません
API 21 以降、ビルダーは addAllowedApplication と addDisallowedApplication を受け付け、二つのリストは一つのビルダー内で排他的です。許可リストはそれらのパケットのためにトンネルを存在させ、他のすべては物理ネットワークを保ち、拒否リストは指定したパケット以外のすべてを捕らえ、クライアントが分割トンネリングとして提供するものになります。API 21 以前はプラットフォームの仕組みがまったくなく、アプリごとの挙動は第三者クライアントが自前のフォワーダ内で UID によって絞り込むことから来ていました。この区別はデバッグ時に重要で、一つのパケットの許可リストを持つアプリごとのトンネルはほとんど通信を生まず、それは正しい挙動であって壊れたルートではありません。
範囲がどうであれ、アプリが VPN サーバーへ開くソケットは、それが構築中のトンネルを通ってはいけません。VpnService.protect(socket) はそのソケットを下位のネットワークに結び付け、パケットが tun デバイスを飛ばすようにします。呼び出しを省くと、ハンドシェイクはそれを立ち上げるはずのインターフェースに飲み込まれます。
VpnService.setUnderlyingNetworks はどの物理ネットワークがトンネルを運ぶかをシステムに伝えて輪を閉じます。その値は VPN が他のアプリへ接続性をどう報告するかを決め、null や古い値は、端末の他の部分がオフラインとみなす接続済みトンネルの日常的な原因です。下位ネットワークが変わったときに更新し、起動時だけにとどめないでください。
チェックサムと MTU:正しいルートでも失敗する場所
パケットが届いた後、パケット水準の二つの細部が、何かが端から端まで動くかどうかを決めます。一つ目は送信元アドレスで、複数のインターフェースアドレスがあるとカーネルは最初のものを載せたパケットを送ることがあるため、ヘッダを書き換えるフォワーダはアドレスとそれを覆うチェックサムを直さなければなりません。
二つ目はチェックサムの範囲です。アプリが TCP や UDP のパケットを自前で組み立てる場合、チェックサムは送信元アドレス、宛先アドレス、プロトコル番号、トランスポート長を持つ疑似ヘッダを覆い、どれか一つを省くと届いて破棄されるパケットになります。その失敗は、決して来ない応答の後のタイムアウトのように見え、多くの人をルートへ戻します。setMtu はインターフェースの MTU を固定し、各読み取りはちょうど一つの完全な IP パケットを返すため、バイトストリーム向けに書いたバッファはパケットではなく解析を分断します。
層を切り分ける検証の一巡
範囲がどこで狂ったかを見つけるには、層を順に試します。端末に VPN ネットワークが実際に持つルートを尋ね、次に既知のアドレスへのフローがディスクリプタ上の読み取りとして現れるかを確認し、次にフォワーダが送るパケットが正しい形かを確認します。ルートの問題はディスクリプタに何も現さず、チェックサムの問題は届いて消えるパケットとして現れます。
何もディスクリプタに届かないなら、疑わしいのは読み取り側ではなくルート集合であり、プレフィックスを確認し、同じファミリのアドレスがあることを確認し、アプリが設定した範囲の内側にあることを確認してください。パケットはディスクリプタに届くのに何も戻らないなら、再びルートに触れる前に protect() と下位ネットワークの値を調べてください。遠い側が壊れたパケットを報告するなら、チェックサムと MTU が見るべき場所です。層ごとに一つの探りで、午後半日の当て推量が省けます。