Android で main テーブルに追加したルートは、どの一覧でも正しく見えるのにトラフィックを運ばない。理由は、main テーブルが複数ある選択肢のひとつにすぎず、最初に参照されるわけではないからだ。Android のネットワークサービスは、送信先アドレスには含まれない情報 — アプリケーションの uid、パケット上のマーク、到着したインターフェース — を使ってルーティングテーブルを選ぶポリシールールを構築し、送信先の検索はその後、それらのルールが選んだテーブルの内部で行われる。

固定された順序の三つの判断

外向きのパケットは三つの別々の判断を行う。第一に、ポリシールールがどのルーティングテーブルを適用するかを決める。第二に、そのテーブル内のルートが次のホップと送出インターフェースを決める。第三に、netfilter のフックがパケットを通すか、変更するか、リダイレクトするかを決める。

順序が重要なのは、各段階が自分の問いにしか答えられないからだ。パケットを受け入れるファイアウォールルールはテーブルを選ばず、どのルールも選ばないテーブルに追加したルートは無効のままだ。Android の VPN をめぐる混乱の多くは、三つのうち一つを直しても変化が見えないことから来る。故障は別の段階にあったのだ。

StageSelectorWhat it cannot decide
Policy ruleuid range, mark, incoming interfaceWhich route exists inside the table
Route lookupDestination prefix, longest matchWhether the packet is allowed out
Netfilter hookChain position, match conditionsWhich table was selected

マークは詰め込まれたフィールドであり、自分のラベルではない

Android は複数の事実を同じマーク値に詰め込む。どのネットワークが選ばれたか、アプリケーションが明示的にネットワークを選んだか、ソケットが VPN から保護されているか、権限に関するビットだ。AOSP の fwmark ヘッダーが現在のツリーにおけるその配置を定義しており、配置はリリース間で変わるプラットフォーム内部のものだ。

だからこそ Android では、マーク値全体を iptables -j MARK --set-mark で書き込むのは危険だ。タグを足すのではなく、システムがすでに行ったネットワーク選択を置き換えてしまう。その結果パケットは別のルールに一致し、どのビットを上書きしたかによって、VPN が動き始めたようにも、無関係なアプリが接続を失ったようにも見える。

どうしてもマークに触れるなら、まず現在の値を読み、自分が理解しているビットだけが変わるようにマスクを使う。

ip rule show
ip route show table all | head -40
ip route get 10.20.4.11

接続マークはパケットマークとは別のオブジェクトであり、どちらもルートではない。応答パケットでマークを復元することは、送出時にテーブルを選ぶこととは別の手順だ。だから片方向だけの修正は、開いたあとに固まる接続を生む。

ルールとテーブルの所有者は誰か

Android のネットワークサービスである netd が、ルールとネットワークごとのテーブルを作る。AOSP RouteController は、VPN、物理ネットワーク、uid 範囲のためにそれらのルールが組み立てられる場所であり、そのライフサイクルは設定ファイルではなくネットワークイベントに結び付いている。

そこから二つの帰結が生じる。第一に、ある端末で見つけた優先度やテーブル番号はビルド内部の詳細であり、インターフェースではない。別の Android バージョンで同じように見える番号がまったく別の意味を持つことがある。第二に、ネットワークが変わると状態は作り直されるので、今日 root シェルから挿入したルールは次の Wi-Fi 切り替えの後に消えているかもしれない。

この消失は、システムがルートを無視していると報告されがちだ。実際に起きたのは、テーブルを選んでいたルールがもう存在せず、テーブル内のルートがパケットから見て到達不能になったということだ。再接続後に動いていた設定が動かなくなったら、ルート一覧ではなくルール一覧を確認する。

フックの順序についての問いに普遍的な答えはない

iptables が VPN ルーティングより前に動くかという問いは、パケットの経路を名指ししない限り答えられない。ローカルで生成されたパケットは output フックを通り、そこでマークの変更が再ルーティング検査を引き起こす。これは二度目の検索であり、一度目を遡って編集するわけではない。テザリングのクライアントから届くパケットは prerouting の経路を通って代わりに転送され、それを見るチェーンは別のものだ。

つまり同じルールが、あるアプリのトラフィックには効き、テザリングのトラフィックには見えないことがある。ゼロのままのカウンターは、ルールが誤っているかではなく、パケットがどの経路を通ったかを教えてくれる。一致条件を変える前に、カウンターで経路を確定させる。

ここはポリシールーティングの誤りが見えにくくなる段階でもある。存在するが送信先を含まないテーブルへルーティングすると、ファイアウォールのドロップに見えるネットワーク到達不能になる。逆にファイアウォールのドロップは、ルーティングのブラックホールに見えるタイムアウトになる。

システムの内側で診断するか、手を出さない

許可された調査では、何かに触れる前に四つの事実を確定させる。アプリの uid とプロファイル、Android がそのアプリに選んだネットワーク、VPN がそのアプリをそもそも含むか、問題が両方のアドレスファミリーで起きるか。アプリ単位の除外は、ルーティングの不具合がまったくない VPN が動かないという報告の多くを説明する。

そのうえで実際に有効なルール、マスク、テーブルを読み、観測の窓の中にネットワーク切り替えを入れておく。再接続をまたいで挙動が変わるなら、見ているのは管理されたライフサイクルであり、欠けたルートではない。

通常の要件では、サポートされた制御を使う。アプリ単位の包含と除外 を VPN アプリケーションで、特定の経路が必要なものにはプラットフォーム自身のネットワーク選択を使う。特権的な統合が本当にルーティングの仕組みと協調しなければならないなら、サポートする正確な Android バージョンの実装を対象にし、それを所有するコンポーネントを通じてライフサイクルを維持する。プラットフォームのチェーンをフラッシュしたり優先度の高いルールを挿入したりすると、診断が二台目のネットワークマネージャーに変わり、端末はそれに応じて振る舞う。

関連記事