VPN

Android 为开发者提供了用于创建虚拟专用网 (VPN) 的 API 解决方案。阅读本指南后,您将了解如何开发和测试 自己的 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 解决方案。系统界面还会让用户使用设备 检测到有效 VPN 连接Android 会显示以下界面组件: VPN 连接:

  • 在 VPN 应用首次变为活动状态之前,系统会显示 连接请求对话框。该对话框会提示设备使用者 确认他们信任该 VPN 并接受请求。
  • “VPN 设置”屏幕(“设置”>“网络和互联网”>“VPN”)会显示 VPN 用户接受了关联请求的应用。系统提供了 或者取消保存 VPN。
  • 连接到网络时,“快捷设置”托盘会显示信息面板 活动状态。点按标签会显示一个对话框,其中包含更多信息和一个链接 前往“设置”。
  • 状态栏包含一个 VPN(钥匙)图标以表示有效连接。

您的应用还需要提供一个界面,以便设备使用者可以 配置服务的选项例如,您的解决方案可能需要 捕获账号身份验证设置。应用应该显示以下界面:

  • 用于手动启动和停止连接的控件。始终开启的 VPN 可以在需要时连接,但允许用户在第一次连接时 每次使用时都没有问题
  • 服务处于活动状态时发出的不可关闭通知。通知可以 显示连接状态或提供网络统计信息等更多信息。 点按该通知会将您的应用调入前台。移除 通知。

VPN 服务

您的应用为用户(或单位员工)连接系统网络 配置文件)连接到 VPN 网关。每位用户(或工作资料)都可以运行 另一个 VPN 应用您创建一项 VPN 服务,供系统用来启动和 停止 VPN 并跟踪连接状态。您的 VPN 服务继承自 VpnService

该服务还充当 VPN 网关连接的容器 本地设备接口您的服务实例调用 VpnService.Builder 方法,用于创建新的本地接口。

<ph type="x-smartling-placeholder">
</ph>
图 1.VpnService 如何连接 Android 连接到 VPN 网关
显示 VpnService 如何创建本地 TUN 的块架构图
         界面。

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

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

每个用户或工作资料只能有一项活动的服务。启动一项新服务 自动停止现有服务。

添加服务

如需向您的应用添加 VPN 服务,请创建一项继承自以下来源的 Android 服务: VpnService。在应用中声明 VPN 服务 清单文件中添加了以下内容:

  • 使用 BIND_VPN_SERVICE 保护服务 以便只有系统可以绑定到您的服务。
  • 使用 "android.net.VpnService" intent 过滤器通告服务,以便 以便系统找到您的服务

以下示例展示了如何在应用清单文件中声明服务:

<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()。如果使用设备的人 已经为应用授予了权限,则该方法会返回一个 activity intent。 您将使用此 Intent 来启动询问权限的系统 Activity。通过 系统会显示类似于其他权限对话框的对话框,如 摄像头或通讯录访问权限。如果您的应用已准备就绪,该方法会返回 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 的状态,以及任何活跃的 VPN 的状态 连接。更新应用的界面 (UI) 以让用户使用 任何变化

启动服务

您的 VPN 服务可以通过以下方式启动:

  • 您的应用启动服务,通常是因为用户点按了连接按钮。
  • 系统启动服务,因为始终开启的 VPN 已开启。

您的应用通过向 startService()。要了解详情,请阅读开始 服务

系统通过调用 onStartCommand()。不过,Android 对 8.0(API 级别 26)或更高版本中的后台应用。如果你支持这些元素 API 级别,您需要通过调用 Service.startForeground()。有关详情,请参阅运行 服务

停止服务

设备的使用者可以使用应用界面来停止服务。停止 而不是直接关闭连接系统还会使正在运行的 当用户在 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. 停用因系统和设置而断开连接的所有界面 应用控制连接。
  2. 在每次应用启动之间保存任何配置,并配置与 最新设置。由于系统会按需启动您的应用, 使用设备时,用户可能并不总是想配置连接。

您还可以使用托管配置来配置 连接。借助托管配置,IT 管理员可远程配置您的 VPN。

检测始终开启的 VPN

Android 不提供用于确认系统是否已启动 VPN 的 API 服务。但是,当应用标记其启动的任何服务实例时,您可以假定 系统为始终开启的 VPN 启动了未标记的服务。示例如下:

  1. 创建 Intent 实例以启动 VPN 服务。
  2. 通过在 Intent 中放置 extra 来标记该 VPN 服务。
  3. 在服务的 onStartCommand() 方法中,查找 (在 intent 参数的 extra 中标记)。

屏蔽的连接

设备的使用者(或 IT 管理员)可以强制所有流量使用 VPN。 系统会屏蔽所有不使用 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 时,系统会停用选项界面 控件。

按应用开启的 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,在以下情况下调用 VpnService.Builder.allowBypass() 建立 VPN 接口启用 VPN 服务。如果应用未将其进程或套接字绑定到特定的 则应用的网络流量会继续通过 VPN 传输。

当有人尝试连接到特定网络时,绑定到特定网络的应用 可屏蔽未通过 VPN 的流量。要通过特定区域 以及应用调用方法,例如 ConnectivityManager.bindProcessToNetwork()Network.bindSocket(),然后再连接套接字。

示例代码

Android 开源项目包含一个名为 ToyVPN 的示例应用。 此应用展示了如何设置和连接 VPN 服务。