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 クライアントについては説明しません。

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

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 つのみです。アプリが最後にこのメソッドを呼び出して以降、ユーザーが別のアプリを VPN サービスとして設定している可能性があるため、常に VpnService.prepare() を呼び出してください。詳細については、サービスのライフサイクルのセクションをご覧ください。

サービスを接続する

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

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

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()
システムがローカル TUN インターフェース アドレスとして割り当てるサブネット マスクとともに、IPv4 または IPv6 アドレスを少なくとも 1 つ追加します。通常、アプリはこの IP アドレスとサブネット マスクを、ハンドシェイク中に VPN ゲートウェイから受け取ります。
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() を呼び出すことで、パーセル ファイル記述子を閉じる(排除する必要はありません)。

常時接続 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 をサポートしていない場合は、(Android 8.1 以降で)SERVICE_META_DATA_SUPPORTS_ALWAYS_ON サービス メタデータを false に設定することで無効にできます。次のアプリ マニフェスト例は、メタデータ要素を追加する方法を示しています。

<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 サービスの設定方法と接続方法を示します。