Android で VPN 接続をコードから確認する方法
Android には VPN 接続状態を返す getter がありません。ネットワーク機能を問い合わせ、トランスポートの変化を監視し、アプリの同意・端末の状態・サーバー側の証明を分けて扱います。
Android に VPN が接続されているかを尋ねることはできません。VpnService には接続状態を返す getter がなく、ステータスバーの鍵アイコンはシステムが描画するもので、アプリが読めるものではありません。できるのは、接続スタックにもっと狭い問いを投げかけることです。このアプリの通信はどのネットワークを使っているのか、そのネットワークは VPN トランスポートを備えているのか。本稿では、誤検知を避けられる順序でチェックをたどります。端末でのシェルコマンドから、アプリ内で動かし続けるコールバックまでです。
アプリのコードに触れる前にシェルで端末を確認する
まず基準となる状態を取ります。adb shell dumpsys connectivity はシステムが把握しているすべてのネットワークを出力し、各ブロックにはトランスポートが列挙されるため、VPN ネットワークはその一覧に VPN を伴って現れます。何より先にデフォルトネットワークのブロックを読み、そのうえで同じトランスポートを持つ他のネットワークを探します。
二つの出力は別の問いに答えます。VPN ネットワークが存在するかどうかと、アプリがそれを使っているかどうかは同じではありません。仕事用プロファイルや第二のユーザーがある端末では、あなたのプロセスが直接インターネットと通信していながら、両方が同時に真になり得ます。ソケットの挙動を決めるのはアクティブネットワークの行です。
アプリが使っているネットワークを問い合わせる
コードでは機能テストは四行です。マニフェストで ACCESS_NETWORK_STATE を要求し、アクティブネットワークの機能を VPN トランスポートと比較します。
ConnectivityManager cm = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);
Network active = cm.getActiveNetwork();
NetworkCapabilities caps = cm.getNetworkCapabilities(active);
boolean vpnInUse = caps != null
&& caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN);
答えは意図的に狭いものです。アプリのデフォルトネットワークが今 VPN かどうか。どの VPN か、それが自分のものか、トンネルが終端まで届いているかは何も語りません。true は「通信が何らかのトンネルを通って出ていく」と解釈し、自前の記録は別に保ちます。三つの意味を黙って同時に持つ真偽値は、ユーザーが VPN 経由で閲覧している最中に設定画面が VPN をオフと表示してしまう原因になります。
逆に、VPN ネットワークが存在するかどうかを知りたい場合は、cm.getAllNetworks() を反復して各機能セットを検査します。二つの答えは、他のアプリを対象にするが自分のアプリは対象にしないアプリ別 VPN が動いている端末で食い違います。その食い違いが、明らかにオンの VPN をアプリが見られないという不具合報告のほとんど源です。
ポーリングではなく変化を監視する
タイマーによるポーリングはこの問いには適した形ではありません。状態の変化はイベントとして届くからです。VPN トランスポートに一致するリクエストを登録し、一致するネットワークが現れたり消えたり変化したりしたときにフレームワークに知らせてもらいます。
NetworkRequest request = new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.build();
cm.registerNetworkCallback(request, callback);
VPN ネットワークが NET_CAPABILITY_NOT_VPN を持つことは決してないため、明示的な削除は意図を文書化します。それがなくてもリクエストは一致しますが、次にコードを読む人はそれを自分で突き止めなければなりません。
登録は観測を必要とするスコープに属し、通常は画面かフォアグラウンドサービスです。onStop や onDestroy で解除し、すでに消えたアクティビティにコールバックを付けたままにしないでください。可視のコンポーネントを持たないアプリは常駐オブザーバの宿主としては不適切であり、忘れたコールバックは構成変更の後にクラッシュするものです。
三つの問いを切り離しておく
この領域の混乱のほとんどは、三つの異なるチェックを一つとして扱うことから生じます。それぞれ別の API を使い、一つが真でも他は偽であり得ます。
| Question | How you answer it | What a yes means |
|---|---|---|
| Did the user grant VPN consent to my app? | VpnService.prepare() returns null | Consent exists; the tunnel may still be down |
| Is the device sending default traffic through a VPN? | TRANSPORT_VPN on the active network | Something is tunneling, not necessarily your app |
| Did this request arrive through the VPN? | Source address seen by the server | Proof for that connection, nothing wider |
三行目は人々がクライアント側で答えようとするものですが、そこでは答えられません。VPN クライアントに専用のアドレスプールを与え、サーバー側で送信元アドレスを読みます。セッション途中でユーザーがトンネルを切り替えても残る唯一の証拠です。
誤検知を排除する検証順序
まだ出回っている古いサンプルコードは getNetworkInfo(ConnectivityManager.TYPE_VPN) を呼びます。この呼び出しは非推奨で、いくつかのリリースでは VPN が接続中でも null を返しました。コピーされた断片が動作する端末で壊れて見えるのはそのためです。機能チェックはその罠を避け、Android 5.0 以降で同じように振る舞います。今も呼び出しているコードベースを保守しているなら、フォールバックで包むのではなく呼び出し自体を置き換えてください。誤った答えを生んだのはそのフォールバックだからです。
一つの近道には警告が必要です。外部サービスが報告する公開アドレスと VPN の出口アドレスを比較することは、経路について何かを証明しますが、チェックのたびに第三者へのリクエストが増え、そのサービスが稼働し続けることに依存し、キャッシュされたり遮断されたりし得ます。手動の診断にとどめ、アプリ内の状態チェックには決して使わないでください。
この順序でチェックを実行し、ユーザーの報告と食い違う最初のもので止めます。シェルのトランスポート一覧、アクティブネットワークの機能、トンネルを切り替えながらのコールバックイベント、そしてサーバー側の送信元アドレスです。四つすべてが一致してもアプリが誤動作するなら、不具合は Android のネットワーク認識ではなく、あなた自身の状態機械にあります。