VPN

Android には、バーチャル プライベート ネットワーク(VPN)ソリューションを作成するための API がデベロッパーに用意されています。このガイドを読むと、Android 搭載デバイス用に独自の VPN クライアントを開発してテストする方法を理解できます。

概要

VPN を使用すると、物理的にネットワーク上にないデバイスがネットワークに安全にアクセスできます。

Android には、PPTP と L2TP/IPSec の組み込みの VPN クライアントが含まれています。これはレガシー VPN とも呼ばれます。Android 4.0(API レベル 14)では、アプリ デベロッパーが独自の VPN ソリューションを提供できるように、API が導入されました。ユーザーがデバイスにインストールするアプリに VPN ソリューションをパッケージ化します。通常、デベロッパーは次のいずれかの理由で VPN アプリを作成します。

  • 組み込みのクライアントがサポートしていない VPN プロトコルを提供する場合。
  • ユーザーが複雑な設定を行わずに VPN サービスに接続できるようにするため。

このガイドの残りの部分では、VPN アプリ(常時接続 VPN とアプリ別 VPN を含む)の開発方法について説明します。組み込みの VPN クライアントについては説明しません。

ユーザー エクスペリエンス

Android には、VPN ソリューションを構成、起動、停止できるようにするためのユーザー インターフェース(UI)が用意されています。また、システム UI は、デバイスを使用しているユーザーにアクティブな VPN 接続を認識させます。Android には、VPN 接続用に次の UI コンポーネントが表示されます。

  • VPN アプリが初めてアクティブになる前に、接続リクエスト ダイアログが表示されます。このダイアログでは、デバイスを使用しているユーザーに、VPN を信頼してリクエストを受け入れることの確認を求めます。
  • VPN 設定画面([設定] > [ネットワークとインターネット] > [VPN])には、ユーザーが接続リクエストを承認した VPN アプリが表示されます。システムオプションを設定したり VPN 接続を解除したりするためのボタンがあります
  • 接続がアクティブになると、クイック設定トレイに情報パネルが表示されます。ラベルをタップすると、詳細情報と設定へのリンクを含むダイアログが表示されます。
  • ステータスバーには、アクティブな接続を示す VPN(鍵)アイコンがあります。

また、デバイスを使用するユーザーがサービスのオプションを構成できる UI も提供する必要があります。たとえば、ソリューションでアカウント認証設定をキャプチャする必要がある場合があります。アプリは次の UI を表示する必要があります。

  • 接続の開始と停止を手動で行うためのコントロール。常時接続 VPN は必要に応じて接続できますが、ユーザーが初めて VPN を使用するときに接続を構成できます。
  • サービスがアクティブなときは非表示にできない通知。この通知では、接続ステータスを表示したり、ネットワーク統計情報などの詳細情報を表示したりできます。通知をタップするとアプリがフォアグラウンドに表示されます。サービスが非アクティブになったら、通知を削除します。

VPN サービス

アプリがユーザーのシステム ネットワーク(または仕事用プロファイル)を VPN ゲートウェイに接続します。ユーザー(または仕事用プロファイル)ごとに異なる VPN アプリを実行できます。VPN サービスを作成して、システムが VPN の開始と停止、接続ステータスのトラッキングに使用するようにします。VPN サービスは VpnService から継承されます。

このサービスは、VPN ゲートウェイ接続とそのローカル デバイス インターフェースのコンテナとしても機能します。サービス インスタンスは VpnService.Builder メソッドを呼び出して、新しいローカル インターフェースを確立します。

図 1.VpnService が Android ネットワークを VPN ゲートウェイに接続する方法
VpnService がシステム ネットワーキングでローカル TUN インターフェースを作成する方法を示すブロック アーキテクチャ図。

アプリは次のデータを転送して、デバイスを VPN ゲートウェイに接続します。

  • ローカル インターフェースのファイル記述子から送信 IP パケットを読み取り、暗号化して VPN ゲートウェイに送信します。
  • 受信パケット(VPN ゲートウェイから受信して復号)をローカル インターフェースのファイル記述子に書き込みます。

アクティブなサービスは、ユーザーまたはプロファイルごとに 1 つのみです。新しいサービスを開始すると、既存のサービスは自動的に停止します。

サービスを追加

アプリに VPN サービスを追加するには、VpnService を継承する Android サービスを作成します。アプリ マニフェスト ファイルで、以下を追加して VPN サービスを宣言します。

  • システムだけがサービスにバインドできるように、BIND_VPN_SERVICE 権限でサービスを保護します。
  • システムがサービスを検出できるように、"android.net.VpnService" インテント フィルタを使用してサービスをアドバタイズします。

次の例は、アプリ マニフェスト ファイルでサービスを宣言する方法を示しています。

<service android:name=".MyVpnService"
         android:permission="android.permission.BIND_VPN_SERVICE">
     <intent-filter>
         <action android:name="android.net.VpnService"/>
     </intent-filter>
</service>

アプリでサービスを宣言すると、システムは必要に応じてアプリの VPN サービスを自動的に開始および停止できます。たとえば、常時接続 VPN の実行時にサービスを制御するのはシステムです。

サービスを準備する

ユーザーの現在の VPN サービスにアプリを設定するには、VpnService.prepare() を呼び出します。デバイスを使用するユーザーがまだアプリに権限を付与していない場合、このメソッドはアクティビティ インテントを返します。このインテントを使用して、権限を求めるシステム アクティビティを開始します。カメラや連絡先へのアクセスなど、他の権限ダイアログに似たダイアログが表示されます。アプリがすでに準備されている場合、メソッドは null を返します。

同時に準備できる VPN サービスは 1 つのみです。常に VpnService.prepare() を呼び出してください。これは、アプリが最後にメソッドを呼び出した後に別のアプリが VPN サービスとして設定されている可能性があるためです。詳細については、サービスのライフサイクルをご覧ください。

サービスを接続する

サービスを実行すると、VPN ゲートウェイに接続する新しいローカル インターフェースを確立できます。権限をリクエストして VPN ゲートウェイへのサービスに接続するには、次の手順を次の順序で行う必要があります。

  1. 必要に応じて、VpnService.prepare() を呼び出して権限をリクエストします。
  2. VpnService.protect() を呼び出して、アプリのトンネル ソケットをシステム VPN の外部に保持し、循環接続を回避する。
  3. DatagramSocket.connect() を呼び出して、アプリのトンネル ソケットを VPN ゲートウェイに接続します。
  4. VpnService.Builder メソッドを呼び出して、VPN トラフィック用に新しいローカル TUN インターフェースをデバイスに構成します。
  5. VpnService.Builder.establish() を呼び出して、システムがローカル TUN インターフェースを確立し、そのインターフェースを介したトラフィックのルーティングを開始します。

VPN ゲートウェイは通常、ハンドシェイク中にローカル TUN インターフェースの設定を提案します。次のサンプルに示すように、アプリは VpnService.Builder メソッドを呼び出してサービスを構成します。

Kotlin

// Configure a new interface from our VpnService instance. This must be done
// from inside a VpnService.
val builder = Builder()

// Create a local TUN interface using predetermined addresses. In your app,
// you typically use values returned from the VPN gateway during handshaking.
val localTunnel = builder
        .addAddress("192.168.2.2", 24)
        .addRoute("0.0.0.0", 0)
        .addDnsServer("192.168.1.1")
        .establish()

Java

// Configure a new interface from our VpnService instance. This must be done
// from inside a VpnService.
VpnService.Builder builder = new VpnService.Builder();

// Create a local TUN interface using predetermined addresses. In your app,
// you typically use values returned from the VPN gateway during handshaking.
ParcelFileDescriptor localTunnel = builder
    .addAddress("192.168.2.2", 24)
    .addRoute("0.0.0.0", 0)
    .addDnsServer("192.168.1.1")
    .establish();

アプリごとの VPN セクションの例は、追加のオプションを含む IPv6 構成を示しています。新しいインターフェースを確立する前に、次の VpnService.Builder 値を追加する必要があります。

addAddress()
少なくとも 1 つの IPv4 アドレスまたは IPv6 アドレスを、システムがローカル TUN インターフェース アドレスとして割り当てるサブネット マスクとともに追加します。アプリは通常、ハンドシェイク中に VPN ゲートウェイから IP アドレスとサブネット マスクを受け取ります。
addRoute()
システムが VPN インターフェース経由でトラフィックを送信するようにする場合は、少なくとも 1 つのルートを追加します。ルートは宛先アドレスでフィルタします。すべてのトラフィックを受け入れるには、0.0.0.0/0::/0 などのオープンルートを設定します。

establish() メソッドは、ParcelFileDescriptor インスタンスを返します。アプリは、これを使用してインターフェースのバッファとの間でパケットの読み書きを行います。establish() メソッドは、アプリが準備できていない場合や、誰かが権限を取り消した場合、null を返します。

サービスのライフサイクル

アプリは、システムで選択された VPN とアクティブな接続のステータスを追跡する必要があります。アプリのユーザー インターフェース(UI)を更新して、デバイスを使用するユーザーが変更を認識できるようにします。

サービスの開始

VPN サービスは次の方法で開始できます。

  • アプリは通常、ユーザーが接続ボタンをタップするとサービスを開始します。
  • システムは、常時接続 VPN がオンになっているとサービスを開始します。

アプリは、インテントを startService() に渡して VPN サービスを開始します。詳細については、サービスの開始をご覧ください。

システムは、onStartCommand() を呼び出して、バックグラウンドでサービスを開始します。ただし、Android バージョン 8.0(API レベル 26)以降のバックグラウンド アプリには制限が適用されます。これらの API レベルをサポートしている場合は、Service.startForeground() を呼び出して、サービスをフォアグラウンドに移行する必要があります。詳細については、フォアグラウンドでサービスを実行するをご覧ください。

サービスの停止

デバイスを使用しているユーザーは、アプリの UI を使用してサービスを停止できます。接続を閉じるだけでなく、サービスを停止してください。また、デバイスを使用しているユーザーが設定アプリの VPN 画面で次の操作を行うと、アクティブな接続が停止します。

  • VPN アプリを切断または削除する
  • アクティブな接続の常時接続 VPN をオフにする

システムはサービスの onRevoke() メソッドを呼び出しますが、この呼び出しはメインスレッドでは発生しない場合があります。システムがこのメソッドを呼び出すと、代替ネットワーク インターフェースがすでにトラフィックをルーティングしています。次のリソースは安全に廃棄できます。

  • DatagramSocket.close() を呼び出して、VPN ゲートウェイへの保護されたトンネル ソケットを閉じます。
  • ParcelFileDescriptor.close() を呼び出して、Parcel ファイル記述子を閉じます(ドレインする必要はありません)。

常時接続 VPN

Android は、デバイスの起動時に VPN サービスを開始し、デバイスの電源が入っている間は VPN サービスを実行し続けることができます。この機能は常時接続 VPN と呼ばれ、Android 7.0(API レベル 24)以降で利用できます。サービスのライフサイクルは Android が管理しますが、VPN ゲートウェイ接続を担うのは VPN サービスです。常時接続 VPN は、VPN を使用しない接続もブロックできます。

ユーザー エクスペリエンス

Android 8.0 以降では、デバイスを使用しているユーザーに常時接続 VPN を認識させるため、次のダイアログが表示されます。

  • 常時接続 VPN 接続が切断された場合や接続できない場合は、閉じることができない通知がユーザーに表示されます。通知をタップすると、詳細を説明するダイアログが表示されます。VPN に再接続するか、常時接続 VPN オプションをオフにすると、通知は表示されなくなります。
  • 常時接続 VPN を使用すると、デバイスを使用しているユーザーは、VPN を使用しないネットワーク接続をブロックできます。このオプションを有効にすると、VPN に接続する前に、インターネットに接続していないことを設定アプリからユーザーに警告されます。設定アプリは、デバイスを使用しているユーザーに、続行またはキャンセルするよう求めます。

(ユーザーではなく)システムが常時接続の開始と停止を行うため、アプリの動作とユーザー インターフェースを適応させる必要があります。

  1. 接続はシステムと設定アプリが制御するため、接続を切断する UI をすべて無効にします。
  2. アプリの起動ごとに構成を保存し、最新の設定で接続を構成します。システムはアプリをオンデマンドで起動するため、デバイスを使用するユーザーは必ずしも接続を構成するとは限りません。

マネージド構成を使用して接続を構成することもできます。管理対象設定は、IT 管理者がリモートで VPN を設定するのに役立ちます。

常時接続を検出する

Android には、システムが VPN サービスを開始したかどうかを確認する API は含まれていません。ただし、起動したサービス インスタンスにアプリがフラグを立てた場合、常時接続 VPN のフラグなしサービスが開始されたと想定できます。次に例を示します。

  1. Intent インスタンスを作成して VPN サービスを開始します。
  2. インテントに拡張データを追加することで、VPN サービスにフラグを立てます。
  3. サービスの onStartCommand() メソッドで、intent 引数のエクストラでフラグを探します。

ブロックされた接続

デバイスを使用するユーザー(または IT 管理者)は、すべてのトラフィックに VPN の使用を強制できます。VPN を使用しないネットワーク トラフィックはすべてシステムがブロックします。デバイスを使用しているユーザーには、[設定] の VPN オプション パネルに [VPN 以外の接続をブロック] スイッチが表示されます。

常時接続を無効にする

アプリで現在常時接続 VPN をサポートできない場合は、SERVICE_META_DATA_SUPPORTS_ALWAYS_ON サービス メタデータを false に設定することで、VPN をオプトアウトできます(Android 8.1 以降)。次のアプリ マニフェストの例は、メタデータ要素を追加する方法を示しています。

<service android:name=".MyVpnService"
         android:permission="android.permission.BIND_VPN_SERVICE">
     <intent-filter>
         <action android:name="android.net.VpnService"/>
     </intent-filter>
     <meta-data android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
             android:value=false/>
</service>

アプリで常時接続 VPN をオプトアウトすると、[設定] の UI コントロールのオプションが無効になります。

アプリ別の VPN

VPN アプリは、VPN 接続を介したトラフィックの送信を許可するインストール済みのアプリをフィルタすることができます。許可リストまたは不許可リストのいずれかを作成できますが、両方は作成できません。許可リストまたは禁止リストを作成しない場合、すべてのネットワーク トラフィックが VPN 経由で送信されます。

VPN アプリは、接続を確立する前にリストを設定する必要があります。リストを変更する必要がある場合は、新しい VPN 接続を確立します。アプリをリストに追加する際は、デバイスにアプリをインストールする必要があります。

Kotlin

// The apps that will have access to the VPN.
val appPackages = arrayOf(
        "com.android.chrome",
        "com.google.android.youtube",
        "com.example.a.missing.app")

// Loop through the app packages in the array and confirm that the app is
// installed before adding the app to the allowed list.
val builder = Builder()
for (appPackage in appPackages) {
    try {
        packageManager.getPackageInfo(appPackage, 0)
        builder.addAllowedApplication(appPackage)
    } catch (e: PackageManager.NameNotFoundException) {
        // The app isn't installed.
    }
}

// Complete the VPN interface config.
val localTunnel = builder
        .addAddress("2001:db8::1", 64)
        .addRoute("::", 0)
        .establish()

Java

// The apps that will have access to the VPN.
String[] appPackages = {
    "com.android.chrome",
    "com.google.android.youtube",
    "com.example.a.missing.app"};

// Loop through the app packages in the array and confirm that the app is
// installed before adding the app to the allowed list.
VpnService.Builder builder = new VpnService.Builder();
PackageManager packageManager = getPackageManager();
for (String appPackage: appPackages) {
  try {
    packageManager.getPackageInfo(appPackage, 0);
    builder.addAllowedApplication(appPackage);
  } catch (PackageManager.NameNotFoundException e) {
    // The app isn't installed.
  }
}

// Complete the VPN interface config.
ParcelFileDescriptor localTunnel = builder
    .addAddress("2001:db8::1", 64)
    .addRoute("::", 0)
    .establish();

許可されているアプリ

許可リストにアプリを追加するには、VpnService.Builder.addAllowedApplication() を呼び出します。リストに 1 つ以上のアプリが含まれている場合、リスト内のアプリのみが VPN を使用します。リストにないすべてのアプリは、VPN が実行されていないかのようにシステム ネットワークを使用します。許可リストが空の場合、すべてのアプリが VPN を使用します。

禁止されているアプリ

許可されていないリストにアプリを追加するには、VpnService.Builder.addDisallowedApplication() を呼び出します。許可されていないアプリは、VPN が実行されていないかのようにシステム ネットワークを使用し、他のすべてのアプリは VPN を使用します。

VPN をバイパスする

VPN を使用すると、アプリは VPN をバイパスして独自のネットワークを選択できます。VPN をバイパスするには、VPN インターフェースの確立時に VpnService.Builder.allowBypass() を呼び出します。VPN サービスの開始後にこの値を変更することはできません。アプリがアプリのプロセスまたはソケットを特定のネットワークにバインドしない場合、アプリのネットワーク トラフィックは VPN を通過します。

VPN を経由しないトラフィックを誰かがブロックした場合、特定のネットワークにバインドするアプリは接続されません。特定のネットワーク経由でトラフィックを送信する場合、アプリはソケットを接続する前に ConnectivityManager.bindProcessToNetwork()Network.bindSocket() などのメソッドを呼び出します。

サンプルコード

Android オープンソース プロジェクトには、ToyVPN というサンプルアプリが含まれています。 このアプリは VPN サービスの設定方法と接続方法を示します。