Android 10 以降では、/proc/net/tcp を開くアプリは socket テーブルの代わりにアクセス拒否を受けます。VPN インターフェースには関係ありません。パケットは今も届きます。トンネルはルーティングの判断であり、ファイルの権限ではないからです。変わったのは「どのアプリがこの接続を所有しているか」という、より狭い問いへの答えです。置き換えは ConnectivityManager.getConnectionOwnerUid、サポートされている接続単位の問い合わせであり、古い習慣と新しい API のどちらを選ぶかは、実際に持っている三つの仕事のどれかで決まります。パケットの受信、フローの帰属、使用量の合計です。

所有者の問いに答える二つの方法

API 29 より前は、特権プロセスや root がカーネルの socket テーブルを読み、四つの組を照合して所有 UID を知ることができました。これは端末全体の一覧を一つのファイルで提供しました。API 29 からはファイルシステムの見え方が制限され、サポートされる経路はプロトコル、ローカルアドレス、リモートアドレスを使った接続単位の問い合わせです。

QuestionLegacy socket tablesgetConnectionOwnerUid
Who owns this flow?Scan and match the tupleAsk once per tuple
Device-wide inventory?Yes, when readableNo, one flow at a time
Historical usage?NoNo; that is NetworkStatsManager
Works without root?No on modern releasesYes, for authorized callers

比較は「古い対新しい」ではありません。かつて一つの読めるファイルで満たしていた三つの異なる仕事が、今は三つの異なる契約を持つ三つのインターフェースで満たされるのです。

ローカルアドレスがアプリ側の端点

メソッドの署名はプロトコル、ローカル socket アドレス、リモート socket アドレスを取ります。「ローカル」とは接続におけるアプリ側のことであり、調べた瞬間にたまたまパケットの送信元になっている端点ではありません。リモートサーバーからアプリへ戻るパケットは、この定義に対して送信元と宛先が逆です。

ConnectivityManager cm = getSystemService(ConnectivityManager.class);
int uid = cm.getConnectionOwnerUid(
    OsConstants.IPPROTO_TCP, appLocalAddress, appRemoteAddress);

この向きの間違いが、正しく動くはずの問い合わせが誤った答え、あるいは何も返さない最もよくある理由です。呼び出しの前に両端を記録し、アプリが実際に開いた接続と組を照合してください。アドレスファミリーも重要です。IPv4 でマップされたアドレスとネイティブな IPv6 アドレスは異なる tuple であり、同じポート上の TCP と UDP のフローも同様です。

認可と不明な結果は契約の一部

問い合わせは権限を持つ呼び出し元に限られ、該当ユーザーのアクティブな VPN アプリも含まれます。無関係な監視アプリは別プロファイルの接続を列挙できると仮定できず、権限のない呼び出しは親切なエラーではなくセキュリティ例外を投げることがあります。両方の結果を明示的に扱ってください。

所有者がいないことはパッケージ名ではありません。パケットと問い合わせの間にフローが閉じたかもしれず、tuple が不完全かもしれず、呼び出し元に知る権限がないだけかもしれません。最初の成功した答えをキャッシュして後で再利用すると、新しい接続を古いアプリに帰属させることになり、不明と報告するより悪くなります。

帰属に失敗しても、トラフィックには判断が必要です。ポリシーが求める場所で遮断する、理解できる範囲だけ許可する、あるいは隙間を示す。そのフォールバックは設計に属し、コメントに属しません。

会計は別の問い

NetworkStatsManager は独自のアクセスルールで使用量を報告します。ある UID が一定期間にどれだけ転送したかは分かりますが、任意の生きたパケットの所有者は分かりません。ネットワーク機能はネットワークの性質を表し、socket の一覧ではありません。三つを混ぜると、一貫して見えてそうではないダッシュボードができます。

どのアプリがトンネルを使うかを決める単純な場合は、Android のサポートする許可・不許可アプリの一覧が、何もスクレイピングせずにポリシーを表現します。フロー単位の帰属は本当に必要な場合に取っておき、向き、権限、不明な状態をまとめて扱う保守された実装のための予算を確保してください。

仕事で道具を選び、端を試す

要件がトンネルの選択なら、プラットフォームの一覧を使ってください。ライブの帰属なら、所有者の問い合わせを使い、答えられない tuple はすべて不明として扱います。履歴の合計なら、stats API を使ってください。そのうえで、素朴な実装を壊す三つの端を試します。逆方向のパケット、問い合わせの前に閉じるフロー、アクティブな VPN ではない呼び出し元です。

これらいずれも、SELinux を無効にしたり、端末を root 化したり、カーネルのテーブルを世界中から読めるようにしたりする必要はありません。制約こそが要点です。プラットフォームは帰属を契約の背後に移し、正当に必要とするコンポーネントだけが答えを得られるようにしました。

関連記事