VPN

Android 提供了 API,供开发者创建虚拟专用网络 (VPN) 解决方案。阅读本指南后,您将了解如何为 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

系统会调用您的服务的 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。

检测始终开启

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 服务。