VPN

Android 为开发者提供了创建虚拟专用网络 (VPN) 解决方案的 API。阅读本指南后,您将了解如何为 Android 设备开发和测试您自己的 VPN 客户端。

概述

VPN 允许不在网络上的设备安全地访问网络。

Android 包含一个内置的 (PPTP 和 L2TP/IPSec) VPN 客户端,有时称为传统 VPN。Android 4.0 (API 级别 14) 引入了 API,以便应用开发者可以提供他们自己的 VPN 解决方案。您将 VPN 解决方案打包到人们安装到设备上的应用中。开发者通常出于以下原因之一构建 VPN 应用

  • 提供内置客户端不支持的 VPN 协议。
  • 帮助用户无需复杂配置即可连接到 VPN 服务。

本指南的其余部分介绍了如何开发 VPN 应用(包括始终在线每个应用 VPN),不涵盖内置 VPN 客户端。

用户体验

Android 提供了一个用户界面 (UI) 来帮助用户配置、启动和停止您的 VPN 解决方案。系统 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 网关
Block-architecture diagram showing how VpnService creates a local TUN
         interface in system networking.

您的应用传输以下数据以将设备连接到 VPN 网关

  • 从本地接口的文件描述符读取传出的 IP 数据包,对其进行加密,然后发送到 VPN 网关。
  • 将传入的数据包(从 VPN 网关接收并解密)写入本地接口的文件描述符。

每个用户或配置文件只有一个活动服务。启动新服务会自动停止现有服务。

添加服务

要向您的应用添加 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 服务。始终调用 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()
至少添加一个 IPv4 或 IPv6 地址以及系统分配为本地 TUN 接口地址的子网掩码。您的应用通常在握手期间从 VPN 网关接收 IP 地址和子网掩码。
addRoute()
如果希望系统通过 VPN 接口发送流量,则至少添加一条路由。路由按目标地址进行筛选。要接受所有流量,请设置一个开放路由,例如 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 应用
  • 关闭活动连接的始终保持 VPN 连接选项

系统会调用服务的 onRevoke() 方法,但此调用可能不会发生在主线程上。当系统调用此方法时,备用网络接口已开始路由流量。您可以安全地释放以下资源

始终保持 VPN 连接

Android 可以在设备启动时启动 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。

检测始终保持 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 连接。添加应用到列表时,该应用必须已安装在设备上。

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()。如果列表包含一个或多个应用,则只有列表中的应用使用 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 服务。