VPN

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

概要

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

Android には、レガシー VPN と呼ばれることもある組み込みの(PPTP および L2TP/IPSec)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 つのみです。アプリが最後にメソッドを呼び出してから、ユーザーが別のアプリを VPN サービスとして設定している可能性があるため、常に VpnService.prepare() を呼び出します。詳細については、サービスのライフサイクルのセクションをご覧ください。

サービスを接続する

サービスが実行されたら、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 メソッドを呼び出してサービスを構成します。

// 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()

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

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

常時接続 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 に設定することで、オプトアウトできます(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 接続を確立します。アプリをリストに追加するには、アプリがデバイスにインストールされている必要があります。

// 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()

許可されているアプリ

アプリを許可リストに追加するには、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 サービスの設定方法と接続方法を示します。