Android NDK开发详解连接性之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 提供界面来帮助用户配置、启动和停止您的 VPN 解决方案。系统界面也会让设备的使用者知晓活动的 VPN 连接。Android 会显示以下适用于 VPN 连接的界面组件:

在 VPN 应用首次变为活动状态之前,系统会显示一个连接请求对话框。该对话框会提示设备的使用者确认他们信任 VPN 并接受请求。
VPN 设置屏幕(设置 > 网络和互联网 > VPN)会显示用户接受了连接请求的 VPN 应用。有一个用于配置系统选项或忘记 VPN 的按钮。
“快捷设置”栏显示当连接处于活动状态时的信息面板。点按标签会显示一个对话框,里面包含更多信息和指向“设置”的链接。
状态栏包含一个 VPN(钥匙)图标以表示有效连接。

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

用于手动启动和停止连接的控件。始终开启的 VPN 可以在需要时连接,但允许用户在首次使用 VPN 时配置连接。
服务处于活动状态时发出的不可关闭通知。通知可以显示连接状态或提供更多信息,例如网络统计信息。点按该通知会将您的应用调入前台。在服务变为非活动状态后移除通知。

VPN 服务

您的应用会将用户(或工作资料)的系统网络连接到 VPN 网关。每个用户(或工作资料)都可以运行不同的 VPN 应用。您需要创建 VPN 服务,供系统用来启动和停止您的 VPN,并跟踪连接状态。您的 VPN 服务继承自 VpnService。

该服务还充当 VPN 网关连接及其本地设备接口的容器。您的服务实例调用 VpnService.Builder 方法来建立新的本地接口。
图 1. VpnService 如何将 Android 网络连接到 VPN 网关
显示 VpnService 如何在系统网络中创建本地 TUN 接口的块架构图。

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

从本地接口的文件描述符读取传出的 IP 数据包,进行加密并发送到 VPN 网关。
将传入的数据包(从 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 方法来配置服务,如以下示例所示:
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 服务可以通过以下方式启动:

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

您的应用通过向 startService() 传递 Intent 来启动 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 部分。

停用始终开启的 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 连接。当您将应用添加到列表中时,该应用必须已安装在设备上。
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 服务。
在这里插入图片描述

本页面上的内容和代码示例受内容许可部分所述许可的限制。Java 和 OpenJDK 是 Oracle 和/或其关联公司的注册商标。

最后更新时间 (UTC):2019-12-30。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Android NDK开发是指利用NDK(Native Development Kit)将C/C++开发的代码编译成so库,然后通过JNI(Java Native Interface)让Java程序调用。在Android开发中,默认使用的是Android SDK进行Java语言的开发,而对于一些需要使用C/C++的高性能计算、底层操作或跨平台需求的场景,可以使用NDK进行开发。 在Android Studio中进行NDK开发相对于Eclipse来说更加方便,特别是在Android Studio 3.0及以上版本中,配置更加简化,并引入了CMake等工具,使得开发更加便捷。首先要进行NDK开发,需要配置环境,包括导入NDK、LLDB和CMake等工具。可以通过打开Android Studio的SDK Manager,选择SDK Tools,在其中选中相应的工具进行导入。 在项目的build.gradle文件中,可以配置一些NDK相关的参数,例如编译版本、ABI过滤器等。其中,可以通过externalNativeBuild配置CMake的相关设置,包括CMakeLists.txt文件的路径和版本号。此外,在sourceSets.main中还可以设置jniLibs.srcDirs,指定so库的位置。 在进行NDK开发时,可以在jni文件夹中编写C/C++代码,并通过JNI调用相关的函数。通过JNI接口,可以实现Java与C/C++之间的相互调用,从而实现跨语言的开发。 综上所述,Android NDK开发是指利用NDK将C/C++开发的代码编译成so库,并通过JNI实现与Java的相互调用。在Android Studio中进行NDK开发相对方便,可以通过配置环境和相应的参数来进行开发。<span class="em">1</span><span class="em">2</span><span class="em">3</span>
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

五一编程

程序之路有我与你同行

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值