Android VPN实现原理介绍

虚拟专用网络(Virtual Private Network,VPN)能够在不使用专用物理连接的情况下,将一个虚拟的网络扩展到全网,因此所有连接到VPN中的设备可如同物理连接到同一私有网络中一样,发送并接收数据。如果个人设备使用VPN接入目标私有网络,这种方式也叫作远程访问VPN;当VPN用来连接两个远程网络的时候,被称为site-to-site VPN。

远程访问VPN可以将特定设备与一个静态IP连接,设备如同远程办公室中的一台电脑;但是对于移动设备来说,更常用的是可变网络连接和动态地址的配置方法。这样的配置通常被称为road warrior配置,而且是Android VPN中最常用的配置。

为了保证通过VPN传输数据的私密性,VPN一般会使用一个安全隧道协议以认证远程客户端并实现数据保密。由于VPN协议需要同时在多个网络层工作而且为了兼容不同的网络配置,常常需要进行多层封装,所以非常复杂。对于VPN的详细讨论超出了本书范围,但是在下面几节中会简单介绍主流的VPN协议,而且会主要专注于Android中可用的VPN类型。

PPTP

Point-to-Point Tunneling Protocol(PPTP)使用TCP的信道来建立连接,并使用Generic Routing Encapsulation(GRE)隧道协议来封装Point-to-Point Protocol(PPP)的数据包。支持的认证方法有密码认证协议(Password Authentication Protocol,PAP)、握手问题认证协议(Challenge-Handshake Authentication Protocol,CHAP)和微软扩展MS-CHAP v1/v2,以及EAP-TLS。其中当前仍然被认为安全的只有EAP-TLS。

PPP的载荷可以使用微软点对点加密(MPPE)协议进行加密,其中使用了RC4流加密方法。因为MPPE并不支持任何密文认证,所以无法防范位翻转(bit-flipping)攻击。除此之外,RC4加密近年来也出现过多次问题,大大减弱了MMPE和PPTP的安全性。

L2TP/IPSec

二层隧道协议(Layer 2 Tunneling Protocol,L2TP)类似于PPTP,工作在数据链路层(OSI模型中的第二层)。因为L2TP本身并不提供任何加密或者保密功能(依赖于隧道协议实现这些特性),L2TP VPN一般使用L2TP和IPSec协议套件的组合实现,由IPSec完成认证,进行机密性和完整性的保证。

在L2TP/IPSec的配置中,首先会使用IPSec建立一个安全信道,然后L2TP隧道将会在这个安全信道之上建立。L2TP的包会被封装到IPSec包中,因此保证了安全。IPSec的连接需要建立一个安全关联(Security Association,SA),这是密钥算法和模式、加密密钥和建立安全信道所需的其他参数的组合。

SA使用网络安全关联和密钥管理协议(ISAKMP)建立。ISAKMP不会定义一个特殊的密钥交换方法,而是使用人工指定的预先共享的密钥或者使用网络密钥交换(IKE和IKEv2)协议。IKE使用X.509证书进行对方身份的验证(与SSL类似),并使用Diffie-Hellman密钥交换创建一个共享密文,并使用其生成实际的会话加密密钥。

IPSec Xauth

IPSec扩展认证(Xauth)对IKE进行了扩展,包含了额外的用户认证交换。这样就允许使用一个已存在的用户数据库或者RADIUS架构来认证远端请求访问的客户端,并且能够集成双因素认证。

Mode-configuration(Modecfg)是另一个IPSec扩展,经常被用于远程访问场景。Modecfg可以让VPN服务端向客户端推送网络配置信息,比如私有IP地址和DNS服务器地址。如果同时使用Xauth和Modecfg,能够生成一个纯IPSec的VPN解决方案,不需要使用任何额外协议进行认证和隧道操作。

基于SSL的VPN

基于SSL的VPN使用SSL或者TLS(见第6章)建立安全连接和网络数据传输隧道。然而并不存在一个标准来定义基于SSL的VPN,所以为了建立安全信道并封装数据包,在不同实现中会使用不同的策略。

OpenVPN是一个很流行的开源VPN应用,使用SSL进行认证和密钥交换(同样支持预先配置的共享静态密钥),并使用定制的加密协议 对数据包进行加密和认证。OpenVPN使用多路SSL会话进行认证和密钥交换以及加密包的传输,而且仅仅使用单一的UDP(或者TCP)端口。多路协议为SSL在UDP上提供了一个可靠的传输层,但是基于UDP的加密数据隧道并没有一定的可靠性。可靠性通过隧道协议本身来提供,通常是TCP。

OpenVPN比IPSec的优势在于协议的简单并且可以完全在用户层实现。而IPSec需要内核层的支持和多个相互依赖的协议实现。此外,由于OpenVPN使用常见的TCP和UDP协议,而且利用一个单一的端口完成多路隧道,所以更容易穿过防火墙、NAT和代理。

接下来的几节将会探究Android内置的VPN支持和Android提供给想完成额外VPN解决方案的Android应用的API。本书将会展示Android VPN架构的主要组件并且介绍如何保护VPN的凭据。

legacy VPN

在Android 4.0之前,对于VPN的支持是内置在系统中的,无法进行扩展。对于新的VPN类型的支持只能通过系统更新来完成。为了将这种情况与基于应用的实现进行区分,我们称内置的VPN支持为legacy VPN。

早先的Android版本支持基于PPTP和L2TP/IPSec的VPN配置,在Android 4.0中加入了对于使用IPSec Xauth的“纯IPSec”VPN的支持。除此之外,Android 4.0提供了系统基类VpnService增加了对基于应用的VPN的支持。应用可以集成该基类实现一个新的VPN解决方案。

legacy VPN通过系统设置应用进行控制,而且只有设备所有者用户(主用户)才可以配置。图9-8中展示的是添加一个新IPSec legacy VPN配置的对话框。

legacy VPN的实现

legacy VPN的实现中包含内核驱动、原生守护进程、命令和系统服务。PPTP和L2TP隧道的底层实现使用了Android特有的PPP守护进程mtpd以及PPPoPNS和PPPoLAC(仅仅在Android内核中可用)内核驱动。

因为对于一个设备,legacy VPN仅仅支持单一的VPN连接,所以mtpd只能建立一个会话。IPSec VPN的实现使用了内置的IPSec内核支持和修改的racoon IKE密钥管理守护进程(Linux内核IPSec实现中的IPSec-Tools 工具包的一部分;racoon只支持IKEv1)。清单9-6中展示了如何在init.rc中定义这两个守护进程。

清单9-6:init.rc中racoon和mtpd的定义

守护进程racoonŒ和mtpd都创建控制套接字(),只能够被系统用户访问而且不会默认启动;两个守护进程都拥有vpn、net_admin(对应Linux的CAP_NET_ADMIN能力),并且都在额外用户组中添加了inet(Ž‘),允许创建套接字并控制网络接口设备。在mtpd守护进程中还添加了net_raw组(对应Linux的CAP_NET_RAW能力),可以创建GRE数据包(被PPTP使用)。

当系统设置应用启动VPN之后,Android启动racoon和mtpd守护进程并且通过本地套接字向它们发送控制命令,以建立配置的连接。这两个守护进程将会创建请求的VPN隧道,并使用收到的IP地址和网络掩码创建并配置隧道网络接口。其中,mtpd守护进程会完成接口配置,而racoon使用ip-up-vpn帮助命令打开隧道接口——通常为tun0。

为了将通信的参数回传给系统,VPN守护进程将会在/data/misc/vpn目录下写入一个state文件,如清单9-7所示。

清单9-7:VPN状态文件的内容

这个文件中包含了隧道接口的名称Œ,IP地址和掩码,配置的路由Ž,DNS服务器和搜索域名,每项占一行。

VPN守护进程开始运行后,系统会处理state文件,然后调用系统的ConnectivityService为新建立的VPN连接配置路由、DNS服务器,以及搜索域名。ConnectivityService会向netd守护进程的本地套接字中发送控制命令,随后以root权限运行的netd将会修改内核的包过滤和路由表。通过添加匹配应用UID的防火墙规则和路由表,所有设备所有者或特定配置启动的所有应用流量将会被通过VPN接口进行路由。(本章后面“多用户支持”一节中讨论了各应用路由规则和多用户支持。)

配置和凭据存储

每个由设置应用创建的VPN配置被称为VPN配置文件(profile),在本地硬盘加密存储。加密的过程由Android的凭据存储守护进程keystore完成,并且需要一个与设备相关的密钥。

VPN配置文件的所有属性以NUL字符(\0)分隔连接到一起组成序列化的单个配置字符串,作为二进制大对象类型存储到系统密钥库中。VPN配置文件的文件名为VPN前缀加上以毫秒为单位的当前时间(十六进制格式)。例如,清单9-8中显示了用户的密钥库目录,其中包含三个VPN配置文件(未显示时间戳)。

清单9-8:配置VPN之后keystore目录中的内容

三个VPN配置文件被保存在1000_VPN_144965b85a6、1000_VPN_ 145635c88c8‘和1000_VPN_14569512c80“文件中。1000_前缀代表了文件所有者用户,也就是system(UID 1000)。因为VPN配置文件的所有者是system,所以只有系统应用可以获取并解密配置文件的内容。

清单9-9中展示了三个VPN配置文件解密之后的明文内容(为了可读性,NUL字符使用竖分割线[|]代替)。

清单9-9:VPN配置文件的内容

配置文件中包含了VPN配置编辑对话框(参见图9-8)中的所有域,如果未指定则由一个空字符串代表。开头的5个域分别代表了VPN的名称、类型、网关主机、用户名和密码。在清单9-9中,第一个VPN配置文件Œ是针对一个使用预先共享密钥方式(type1)运作的L2TP/IPSec VPN;第二个配置文件对应的是PPTP VPN(type 0),而最后一个配置文件Ž针对的是使用证书和Xauth认证的IPSec VPN(type 4)。

除了用户名和密码,VPN配置文件中还包含了其他建立连接需要的凭据。在清单9-9中的第一个VPN配置Œ中,额外的凭据是预分享的建立IPSec安全连接所需的密钥(在本例中为PSK字符串)。而对于第三个配置Ž,额外的凭据是用户的私钥和证书。然而,正如在清单中看到的那样,密钥和证书并不会完全包含在配置文件中;配置文件中仅仅包含密钥和证书的别名(两者相同,均为vpnclient)。密钥和证书被保存在系统凭据库中,VPN配置中的别名仅仅充当一个标识符,用来获取密钥和证书。

获取凭据

原本使用PEM文件中密钥和证书的racoon守护进程,在Android中被修改为使用密钥库(keystore)的OpenSSL引擎。正如第7章中讨论过的,keystore引擎是系统凭据库的入口,如果设备支持,甚至可以使用硬件凭据库。当keystore引擎处理一个密钥别名的时候,不需要从密钥库提取出密钥就能够使用对应的私钥对认证包进行签名。

清单9-9中的VPN配置文件Ž中也包含CA证书的别名(cacert),该证书在验证服务器的证书时会被作为信任锚点。在运行时,系统从密钥库中获取到客户端证书(清单9-8中的Ž)和CA证书(清单9-8中的Œ),然后通过控制套接字将它们和其他连接参数一起传送给racoon。私钥blob(清单9-8中的)永远不会被直接发送给racoon守护进程,而仅仅发送其别名(vpnclient)。

NOTE

虽然设备上的私钥使用硬件密钥库(hardware-backed keystore)保存,但VPN配置中预共享的密钥和密码却不是。造成这种情况的原因是:在撰写本书时,Android不支持在硬件密钥库中导入对称密 钥,而仅仅支持非对称密钥(RSA、DSA和EC)。所以,使用预共享密钥的VPN的凭据被直接明文保存在VPN配置文件中,这样就导致当配置文件被解密 到内存中后,可被root用户提取出来。

始终在线的VPN

Android 4.2和之后的版本中支持始终在线的VPN配置:在与指定的VPN建立连接前会屏蔽应用发起的所有网络连接。这样就防止了应用使用非加密的信道,例如公开Wi-Fi来发送数据。

设置一个始终在线的VPN需要将VPN的网关设置为IP地址,并且明确指定DNS服务器的IP。这种明确的配置是为了保证DNS流量不会被发送给本地配置的DNS服务器,其在始终在线的VPN状态下是被屏蔽的。VPN配置文件选择对话框如图9-9所示。

用户选择的配置会以“其他VPN设置”的类型被加密保存到文件

LOCKDOWN_VPN中(清单9-8中的);该文件中仅包含了选择的配置的名字—本例中为144965b85a6。如果存在LOCKDOWN_ VPN文件,系统在启动的时候就会自动连接指定的VPN。如果下层网络连接发生了重连或者改变(比如切换了Wi-Fi热点),VPN也将会自动重启。

始终在线的VPN通过加入防火墙规则屏蔽了所有不经过VPN接口的所有数据包,保证了所有的流量都会通过VPN进行发送。系统使用LockdownVpnTracker类(始终在线VPN在Android源码中被称为lockdown VPN)来添加防火墙规则:监视VPN的状态,并向netd守护进程发送命令来动态调整当前的防火墙状态。而netd守护进程使用iptables工具修改内核的包过滤表。比如,当一个始终在线的L2TP/IPSec VPN以11.22.33.44的IP地址连接到了VPN服务器,并且以10.1.1.1的IP地址创建了一个隧道接口tun0,添加的防火墙规则(通过使用iptables列出,为了简洁省略了一些列)的情况可能如清单9-10所示。

清单9-10:始终在线的VPN防火墙规则

如清单9-10所示,所有来自和发往VPN网络的流量都被允许(Œ),隧道接口也同样(‘)。而且仅仅允许IPSec端口(500和4500)和L2TP的端口(1701)上的来自/发往VPN服务端的流量(Ž’)。其他所有收到的流量都会被丢弃,所有向外的流量都会被拒绝“。

基于应用的VPN

Android 4.0中增加了一个VpnService公开API ,允许第三方应用实现VPN解决方案,而且此应用不需要被植入系统镜像或者拥有系统权限。VpnService和相关的Builder类允许应用指定网络参数,比如接口IP地址和路由,随后系统使用这些参数创建并配置一个虚拟网络接口。应用将会接收到一个该网络接口对应的文件描述符,之后就可以通过写入或者读取该描述符进行网络隧道通信。每次读取都会获取一个出去的IP包,而且每次写入会注入一个进入的IP包。因为对于网络包有效的直接访问能够允许基于应用的VPN完成对网络包的注入和修改,所以这种VPN不能自动启动,而且总是需要用户操作。此外,当VPN成功连接,将会显示一个运行状态的通告。对基于应用的VPN连接的警告框如图9-10所示。

图9-10:基于应用的VPN连接警告框

VPN声明

基于应用的VPN创建了一个继承VpnService基类的服务,然后将其注册到应用manifest中,如清单9-11所示。

清单9-11:在应用manifest中注册一个VPN服务

服务中必须包含一个可以匹配android.net.VpnService动作的intent filter,以允许系统绑定并控制这个服务。除此之外,服务还要求绑定者拥有BIND_VPN_SERVICE系统签名权限Œ,保证只有系统应用才能够绑定。

VPN准备

为了在系统中注册一个新VPN连接,应用需要首先调用VpnService.prepare()方法获取运行所需要的权限,然后调用establish()方法创建一个网络隧道(在下一节中讨论)。其中,prepare()方法返回一个用于启动图9-10所示警告框的intent。这个对话框用来获取用户的许可并且保证在任何情况下,一个设备上只运行一个VPN连接。如果prepare()被调用的同时,存在另一个应用创建的VPN连接,那么该连接将会被中断。同时prepare()方法也会保存调用者应用的包名,如果没有被再次调用之前,只有该应用可以启动一个VPN连接;或者系统将这个VPN连接终止(例如VPN应用进程崩溃)。如果VPN连接因为任何情况被关闭,系统将会调用当前VPN应用的VpnService实现中的onRevoke()方法。

建立VPN连接

在VPN应用被准备完成并且获取到了执行所需要的权限之后即可启动VpnService组件,创建一个指向VPN网关的隧道,然后协商VPN连接需要的网络参数。接下来,VpnService使用协商的参数创建VpnService.Builder类,然后调用VpnService.establish()方法获取读写数据包用到的文件描述符。其中,establish()方法将会:首先检查调用者应用和得到当前建立VPN连接权限应用的UID是否匹配,保证两者相同。然后检查当前Android用户是否拥有建立VPN连接的权限,并且验证该服务的绑定需要BIND_VPN_SERVICE权限;如果服务中没有对这个权限进行要求,就会被认为是不安全的,并且抛出SecurityException异常。之后使用原生代码创建并配置一个隧道接口,然后设置路由和DNS服务器。

将VPN连接状态通知用户

建立VPN连接的最后一步是显示一个运行状态通知,告知用户网络流量通过VPN隧道进行发送。而且通过相关的控制对话框,用户可以对连接进行监视和控制。OpenVPN Android应用相关的对话框如图9-11所示。

该对话框是专用包com.android.vpndialogs的一部分,这个包是非系统用户管理基于应用的VPN连接唯一可用的包。这样保证了VPN连接只能够通过系统授权的UI进行启动和管理。

利用基于应用的VPN架构,应用可以自行实现网络隧道并且使用任何认证和加密方法。因为设备发送或接收的所有数据包都会通过VPN应用,所以VPN应用不仅可以用来建立网络隧道,而且可以用来进行流量记录、过滤和修改(比如屏蔽广告)。

NOTE

若想获取一个可用的、使用Android凭据库管理认证密钥和证书的、基于应用的VPN工具的实现,请查看OpenVPN for Android 的源码。该应用实现了一个完全兼容OpenVPN服务器的SSL VPN客户端。

多用户支持

正如上文提到的,在多用户设备上,只有设备所有者用户才能够控制legacy VPN。然而,在Android 4.2和之后版本中增加了多用户的支持,允许所有次级用户(除了在受限设置,即restricted profile的情况下,所有次级用户必须共享主用户的VPN连接)启动基于应用的VPN。虽然这样允许每个用户自行启动各自的VPN,但是只能同时启动一个基于应用的VPN,所有设备用户的流量都会通过当前激活的VPN,启动这个VPN的用户是谁并不会造成影响。在Android 4.4中引入了对于多用户VPN的完整支持:增加了per-user VPN,允许每个用户使用各自的VPN连接,因此实现了多用户之间的隔离。

Linux高级路由

Android使用了多个Linux内核的高级包过滤和路由特性来实现per-user VPN。这些特性(通过netfilter内核框架实现)包含了Linux的iptables工具的owner模块,能够使用数据包生成进程的UID、GID或者PID对数据包进行匹配。例如,清单9-12中的Œ命令创建了一个丢弃所有UID为1234的用户发送的数据包的包过滤规则。

清单9-12:使用iptables进行拥有者匹配和数据包标记

此外,netfilter的另外一个重要特性是能够为特定数据包打上特定的数字标签(mark)。例如,的规则将所有目标端口为80(通常是网站服务器)的数据包标记上0x1。之后这个标记可以被用来进行过滤和路由。比如,通过添加将标记的包发送给预定义的路由表(本例中为webŽ)的路由规则,将标记的包通过特定的接口进行发送。最后添加一条路由,将匹配web路由表的数据包发送到em3接口。

多用户VPN的实现

Android使用之前提到的包过滤和路由特性,将特定Android用户的所有应用产生的数据,全部发送给该用户启动的VPN应用所创建的隧道。当设备所有者用户启动VPN的情况下,所有受限用户无法自行启动VPN,而是必须共享所有者所创建的VPN连接。

这种隔离路由的方法通过系统层NetworkManagementService实现,并且提供了管理UID或UID粒度的包匹配和路由的API。NetworkManagementService通过向以root运行的原生netd守护进程发送命令来实现这些API,因此可以修改内核的包过滤和路由表。而netd通过调用iptables和ip用户态工具进行内核层包过滤和路由的配置。

之后我们通过一个例子讲解Android的per-user VPN路由,如清单9-13所示。主用户(用户ID为0)和次级用户(用户ID为10)各自启动了一个基于应用的VPN。主用户的隧道接口为tun0,次级用户的为tun1。设备中也包含了一个受限用户,其用户ID为13。清单9-13中显示了当这两个VPN都被连接成功之后的内核包过滤表状态(忽略了一些不重要的细节)。

清单9-13:两个不同的设备用户启动VPN之后的包匹配规则

向外的数据包将会首先被发送给st_mangle_OUTPUT链,其负责对数据包进行匹配和标记。不需要进行per-user路由(已经被标记上0x1Œ)和来自于legacy VPN(UID 1016,表示内置vpn用户,负责启动mtd和racoon守护进程)的数据包不被修改直接通过。

接下来,由UID在0和99999之间的进程(由主用户启动的应用进程,详见第4章)产生的数据包会被匹配,并被发送给st_mangle_tun0_ OUTPUT链Ž。而由受限用户(用户ID为13)启动的进程(UID在1300000~1399999之间)产生的数据包也会被发送到同一个链。因此,主用户和受限用户的流量会被以同样的方式进行处理。而由第一次级用户(用户ID为10,UID为1000000~1099999)产生的数据包会被发送给一个不同的链,st_mangle_ tun1_OUTPUT。目标链本身非常简单:st_mangle_tun0_OUTPUT首先会清除包原来的标记并标记上0x3c‘;st_mangle_tun1_OUTPUT会做相同的处理,但使用的是标签0x3d’。在数据包被标记之后,这些标记会被用于实现不同的路由规则,如清单9-14所示。

清单9-14:两个不同的设备用户启动VPN之后的路由规则

可以注意到,清单中的两条规则针对每个标记创建,而且关联不同的路由表。拥有0x3c标签的数据包会被发送给路由表60(十六进制0x3cŒ),而拥有0x3d标签的数据包会被发送给路由表61(十六进制0x3d)。路由表60将所有流量发送给主用户创建的tun0隧道接口Ž,而路由表61将所有流量发送给次级用户创建的tun1接口。

NOTE

虽然Android 4.4中引入的VPN路由方法提供了更多灵活性,而且允许隔离不同用户的流量,但是在撰写本书时,这个实现还有很多问题,特别是在不同接口之间进行切换的 场景下,比如移动网络和Wi-Fi之间的相互切换。这些问题在之后的Android版本中可能会被改进,可能会修改包过滤链与接口的关联方式,但是基本策略的实现方法应该会保持不变。

评论 在此处输入想要评论的文本。

标题和URL已复制