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 解决方案。系统界面还会让使用设备的用户知道 VPN 连接处于活跃状态。Android 会针对 VPN 连接显示以下界面组件:
- 在 VPN 应用首次变为活跃状态之前,系统会显示一个连接请求对话框。该对话框会提示使用设备的用户确认他们信任 VPN 并接受请求。
- VPN 设置界面(“设置”>“网络和互联网”>“VPN”)会显示用户接受了连接请求的 VPN 应用。有一个按钮可用于配置系统选项或取消保存 VPN。
- 连接处于活动状态时,“快捷设置”托盘会显示一个信息面板。点按该标签会显示一个对话框,其中包含更多信息和指向“设置”的链接。
- 状态栏包含一个 VPN(钥匙)图标以表示有效连接。
您的应用还需要提供界面,以便设备使用者可以配置服务的选项。例如,您的解决方案可能需要捕获账号身份验证设置。应用应该显示以下界面:
- 用于手动启动和停止连接的控件。始终开启的 VPN 可以在需要时连接,但允许用户在首次使用 VPN 时配置连接。
- 服务处于活动状态时发出的不可关闭通知。通知可以显示连接状态或提供更多信息(例如网络统计信息)。 点按该通知会将您的应用调入前台。在服务变为非活跃状态后移除通知。
VPN 服务
您的应用会将用户(或工作资料)的系统网络连接到 VPN 网关。每个用户(或工作资料)都可以运行不同的 VPN 应用。您可以创建 VPN 服务,供系统用来启动和停止 VPN,以及跟踪连接状态。您的 VPN 服务继承自 VpnService。
该服务还充当 VPN 网关连接及其本地设备接口的容器。您的服务实例会调用 VpnService.Builder 方法来建立新的本地接口。
VpnService 如何将 Android 网络连接到 VPN 网关您的应用会传输以下数据,用于将设备连接到 VPN 网关:
- 从本地接口的文件描述符中读取传出 IP 数据包,对其进行加密,然后将其发送到 VPN 网关。
- 将传入的数据包(从 VPN 网关接收并解密)写入本地接口的文件描述符。
每个用户或工作资料只能有一项活动的服务。启动新服务会自动停止现有服务。
添加服务
如需向应用添加 VPN 服务,请创建继承自 VpnService 的 Android 服务。在应用清单文件中声明 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方法,在设备上为 VPN 流量配置新的本地 TUN 接口。 - 调用
VpnService.Builder.establish(),以便系统建立本地 TUN 接口并开始通过该接口路由流量。
VPN 网关通常会在握手期间为本地 TUN 接口建议设置。您的应用会调用 VpnService.Builder 方法来配置服务,如以下示例所示:
// 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()
“按应用 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 已开启。
应用通过将 intent 传递给 startService() 来启动 VPN 服务。如需了解详情,请参阅启动服务。
系统通过调用 onStartCommand() 在后台启动服务。不过,Android 在 8.0(API 级别 26)或更高版本中对后台应用施加了限制。如果您支持这些 API 级别,则需要通过调用 Service.startForeground() 将服务转换到前台。如需了解详情,请参阅在前台运行服务。
停止服务
设备的使用者可以使用应用界面来停止服务。请停止服务,而不仅仅是关闭连接。当设备用户在“设置”应用的 VPN 界面中执行以下操作时,系统也会停止活动连接:
- 断开或忘记了 VPN 应用
- 为活动连接关闭始终开启的 VPN
系统会调用服务的 onRevoke() 方法,但此调用可能不会在主线程上发生。当系统调用此方法时,备用网络接口已在路由流量。您可以安全地处置以下资源:
- 通过调用
DatagramSocket.close()关闭通往 VPN 网关的受保护隧道套接字。 - 通过调用
ParcelFileDescriptor.close()关闭 Parcel 文件描述符(您无需耗尽它)。
始终开启的 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,您可以通过将 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 时,系统会停用“设置”中的选项界面控件。
按应用开启的 VPN
VPN 应用可以过滤哪些安装式应用可以通过 VPN 连接发送流量。您可以创建允许列表或禁止列表,但不能同时创建这两者。如果您不创建允许或禁止列表,系统会通过 VPN 发送所有网络流量。
您的 VPN 应用必须先设置列表,然后建立连接。如果您需要更改列表,请建立新的 VPN 连接。将应用添加到列表时,该应用必须已安装在设备上。
// 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()
允许的应用
如需将应用添加到许可名单中,请调用 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 服务。