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
方法,用于创建新的本地接口。
您的应用会传输以下数据,用于将设备连接到 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 网关时,您需要按以下顺序完成以下步骤:
- 调用
VpnService.prepare()
来请求权限(在 )。 - 调用
VpnService.protect()
以保留应用的隧道套接字 在系统 VPN 外部访问,并避免循环连接。 - 调用
DatagramSocket.connect()
以连接应用的隧道 连接到 VPN 网关。 - 调用
VpnService.Builder
方法以配置新的本地 TUN 接口中的 来连接 VPN 流量 - 调用
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 网关关闭受保护的隧道套接字:
DatagramSocket.close()
。 - 通过调用以下代码关闭 parcel 文件描述符(您无需排空它)
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 之前警告用户他们没有互联网连接 连接。“设置”应用会提示设备的使用者继续操作,或 取消。
由于系统(而不是人)会启动和停止始终开启的连接, 您需要调整应用的行为和界面:
- 停用因系统和设置而断开连接的所有界面 应用控制连接。
- 在每次应用启动之间保存任何配置,并配置与 最新设置。由于系统会按需启动您的应用, 使用设备时,用户可能并不总是想配置连接。
您还可以使用托管配置来配置 连接。借助托管配置,IT 管理员可远程配置您的 VPN。
检测始终开启的 VPN
Android 不提供用于确认系统是否已启动 VPN 的 API 服务。但是,当应用标记其启动的任何服务实例时,您可以假定 系统为始终开启的 VPN 启动了未标记的服务。示例如下:
- 创建
Intent
实例以启动 VPN 服务。 - 通过在 Intent 中放置 extra 来标记该 VPN 服务。
- 在服务的
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 服务。