Android の chroot で VPN を使う:トンネルは動くのにアプリが無視する理由
chroot が変えるのはパスの解決だけで、カーネル、ネットワーク名前空間、Android の VPN 登録は変わらない。そこから二つの結果が生まれ、どちらも人を驚かせる。
スマートフォンで chroot の下に Linux ディストリビューションを動かし、その中に VPN クライアントを入れると、シェルはトンネルが完成したと告げる。ところがブラウザに切り替えると、何も起きなかったかのように通信はモバイルデータから出ていく。あるいは逆に、クライアントが Android が実際に使っている名前空間へ既定ルートを追加したせいで、スマートフォンの全アプリが接続を失う。この二つの症状は同じ欠けている部品から来ており、どちらも VPN クライアントの不具合ではない。
シェルは接続済みと言い、アプリは同意しない
最初の結果がよくあるものだ。chroot から開いたユーザー空間のトンネルはハンドシェイクを終え、統計を表示し、同じシェルが生む通信を運ぶことができる。一方で Android のアプリはスマートフォンの既定ネットワークを使い続ける。何も横取りしていないのは、VPN があると Android に伝えたものが何もないからだ。
二つ目の結果は危険なものだ。chroot が Android のネットワーク名前空間を共有していると、既定でそうなるが、十分な権限を持つプロセスはスマートフォン全体に影響するルートと TUN インターフェースを追加できる。すると、意図していなかったアプリも含めてすべてのアプリでトンネルが動き、スマートフォンの接続はライフサイクル管理も接続表示も常時接続ポリシーも持たないデーモンに依存することになる。
どちらになったかは一つのコマンドで分かる。chroot の中の ip route と、外の Android シェルから同じコマンドを比べる。出力が同じなら chroot はホストの名前空間にあり、行ったネットワーク変更はスマートフォン全体に及ぶ。出力が違えば、ランチャーが専用の名前空間を作っており、そのトンネルは構造上 Android から見えない。
chroot が変えたもの、変えなかったもの
| Object | Effect of a chroot |
|---|---|
| Pathname resolution | Changed: lookups start at the new root |
| Kernel and drivers | Unchanged: the phone's kernel, its TUN driver, its limits |
| Network namespace | Unchanged unless the launcher creates one |
| Android VPN registration | Unchanged: no app consented, no VPN is selected |
chroot システムコールがするのは一つだけだ。パス解決に使うディレクトリを変える。man ページはこれがセキュリティ境界ではないと明言し、インターフェースやルートや VPN の状態には何も触れていない。Linux コンテナに期待するもの、つまり専用のネットワークスタック、フィルタされたファイルシステム、リソース制限は、ランチャーが使ったかどうか分からない別の仕組みから来る。
だから chroot が VPN を隔離するという主張は、たいてい間違った問いに答えている。問うべきは、そのプロセスがどの名前空間で動き、どの SELinux ドメインに閉じ込められているかであり、どちらもどう起動されたかの性質であって、chroot という言葉の性質ではない。
TUN に必要なのは三つで、デバイスノードはその一つにすぎない
従来のユーザー空間トンネルには、開ける TUN デバイス、そのデバイスへのカーネル対応、インターフェースとルートを設定する権限が要る。これらは別々の要件であり、/dev の下にパス名を作っても、それだけではどれも満たさない。
ls -l /dev/net/tun
ip tuntap add dev tun0 mode tun
Android のカーネルは通常 TUN 対応を組み込んでいる。プラットフォーム自身の VPN サービスが使うからだ。だがそれは、あなたのプロセスが使ってよいという意味ではない。インターフェース設定にはネットワーク管理の権能が必要で、デバイスノードに到達できるかは SELinux が決める。拒否は原因の分からない権限エラーに見え、答えは監査ログにある。
dmesg | grep avc
logcat -d | grep "avc: denied"
root でも解決しない。SELinux は uid 0 のプロセスも他と同じように制約するので、root シェルから始めた chroot でも、見えているデバイスを拒否されることがある。古いインストーラを動かすために強制を切るのは、トンネルのはるか先まで影響するスマートフォン全体の判断であり、古いバイナリの修正ではない。
Android のアプリがトンネルを無視する理由
Android が支えるモデルは、プラットフォームの VPN サービスに登録し、利用者に一度だけ同意を求め、自分のトンネルのファイル記述子を受け取る VPN アプリだ。その後プラットフォームが通信をその記述子へ流し、利用者や管理者が選んだアプリ範囲を適用する。だから同じ仕組みがアプリ単位でもプロファイル単位でも、常時接続や VPN なしのブロック設定と組み合わせても使える。Android の VPN 開発ガイドがそのハンドシェイクを説明しており、要点はこれが登録であってルーティングの小技ではないことだ。
chroot の中のデーモンは決して登録しない。同意のダイアログも、ステータスバーの鍵アイコンも、アプリ単位の包含や除外のリストも、常時接続ポリシーへの参加もない。だからアプリは既定ネットワークを使い続け、トンネルはシェル自身が送るものだけを運ぶ。
逆方向も助けにならない。ホストの名前空間にルートを押し込むと、プラットフォームがローカルに保つ想定だった通信も含めてすべてを捕まえ、しかもアプリ単位の制御も、利用者が見たり取り消したりする手段もない。chroot のデーモンが一つのアプリのためだけに通信を流す中間設定は存在しない。
バイナリ互換性は別の壁
ネットワーク側が興味深くても、実行ファイルには独自の要件がある。CPU アーキテクチャ、動的ローダ、C ライブラリ、そして期待するカーネル機能だ。Android は glibc ではなく bionic を使い、デスクトップ向けにビルドされた古いバイナリは端末にもう存在しないライブラリを必要とすることがある。
ライブラリのファイル名を新しい版にリンクしても ABI 互換性は生まれず、十年前のパッケージから取り出した暗号ライブラリは保守された部品ではない。計画がそれに依存しているなら、その計画は端末でネットワーク権限を持つ保守されないコードを動かすことだ。
Android の文脈で証明し、後片付けをする
本当に重要なことを、重要な側から試す。特定のアプリを開き、特定のプライベートな宛先に到達し、どのリゾルバが答えたかを確認し、それからトンネルを切断して文書化された挙動を確かめる。chroot の中の端末の結果は、その端末の実行文脈についての証拠にすぎない。
実験中にホストのルートを変えたなら元に戻し、何を変えたか記録する。そのうえでスマートフォンに常時接続 VPN が設定されていないか確認する。ルールを残した古い実験は、数か月後にプラットフォームの不具合のように見えることがある。
実際の遠隔アクセスが必要なら、サービスの正規クライアントか承認されたゲートウェイ設計を使う。問う価値があるのは、root のインストーラを動かせるかではなく、正規の実装が、端末に所有者のいない特権変更を残さずに、必要なアプリを意図した境界の向こうへ運べるかどうかだ。