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
方法来建立新的本地接口。
您的应用传输以下数据以将设备连接到 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 网关,您需要按以下顺序完成以下步骤
- 调用
VpnService.prepare()
以请求权限(在需要时)。 - 调用
VpnService.protect()
以将应用的隧道套接字保留在系统 VPN 外,并避免循环连接。 - 调用
DatagramSocket.connect()
以将应用的隧道套接字连接到 VPN 网关。 - 调用
VpnService.Builder
方法在设备上为 VPN 流量配置新的本地 TUN 接口。 - 调用
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()
方法,但此调用可能不会发生在主线程上。当系统调用此方法时,备用网络接口已开始路由流量。您可以安全地释放以下资源
- 通过调用
DatagramSocket.close()
关闭到 VPN 网关的受保护隧道套接字。 - 通过调用
ParcelFileDescriptor.close()
关闭数据包文件描述符(您无需将其清空)。
始终保持 VPN 连接
Android 可以在设备启动时启动 VPN 服务,并在设备开启期间保持运行状态。此功能称为始终保持 VPN 连接,在 Android 7.0(API 级别 24)或更高版本中可用。虽然 Android 会维护服务生命周期,但您的 VPN 服务负责 VPN 网关连接。始终保持 VPN 连接还可以阻止不使用 VPN 的连接。
用户体验
在 Android 8.0 或更高版本中,系统会显示以下对话框,以便设备用户了解始终保持 VPN 连接
- 当始终保持 VPN 连接断开或无法连接时,用户会看到一个不可取消的通知。点击通知会显示一个包含更多说明的对话框。当 VPN 重新连接或用户关闭始终保持 VPN 连接选项时,通知会消失。
- 始终保持 VPN 连接允许设备用户阻止任何不使用 VPN 的网络连接。开启此选项时,设置应用会警告用户在 VPN 连接之前没有互联网连接。设置应用会提示用户继续或取消。
由于系统(而非用户)会启动和停止始终保持连接,因此您需要调整应用的行为和用户界面
- 禁用任何断开连接的 UI,因为系统和设置应用会控制连接。
- 在每次应用启动之间保存任何配置,并使用最新的设置配置连接。由于系统会根据需要启动您的应用,因此设备用户可能并不总是希望配置连接。
您还可以使用 托管配置 配置连接。托管配置有助于 IT 管理员远程配置您的 VPN。
检测始终保持 VPN 连接
Android 不包含用于确认系统是否启动了您的 VPN 服务的 API。但是,当您的应用标记其启动的任何服务实例时,您可以假设系统为始终保持 VPN 连接启动了未标记的服务。以下是一个示例
- 创建一个
Intent
实例以启动 VPN 服务。 - 通过 在意图中添加额外数据 来标记 VPN 服务。
- 在服务的
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 服务。