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 方法来在设备上配置新的本地TUN 接口,用于 VPN 流量。
  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 不包含 API 来确认系统是否启动了您的 VPN 服务。但是,当您的应用标记其启动的任何服务实例时,您可以假定系统启动了用于始终开启 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()。 如果列表中包含一个或多个应用,则只有列表中的应用才能使用 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 服务。