First Chapter
第一章先转载下某个童鞋的文章,后续再调整。
iOS 9 Network Extension VPN API 编程指南 - 如何使用NEPacketTunnelProvider和NWUDPSession 字数3035 阅读10484 评论88 喜欢38 本文详细记录了开发iOS 9 VPN app的过程。我们使用了苹果最新的API框架,Network Extension,以及在iOS 9之后才出现的新组件NEPacketTunnelProvider和NWUDPSession。这些新接口让我们制作出私密协议的VPN产品,苹果官方称之为Enterprise VPN。这在iOS 9之前是不可能做到的。
通过本文,你可以了解network extension具体如何使用,以及开发VPN的实战经验。
背景 在iOS 9之前开发私密协议的VPN是不可能的任务,苹果从未公开过iOS版本的虚拟网卡tun设备,也没有相应的API。只有在越狱的iphone上才能编译诸如openvpn的非标准VPN代码,但是那种产品无法通过苹果的review,也绝不可能在app store上架,让普通人使用。
不过2015年苹果开发者大会上,苹果首次提到将公布一组全新的开发框架,开发者从此可以在iOS 9以及osx 10.11上使用系统自带的虚拟网卡vtun0。
WWDC 2015, session 717 https://developer.apple.com/videos/play/wwdc2015/717/
它就是network extension api。
其实,network extension最早出现在iOS 8,不过那个版本不支持虚拟网卡,只能简单调用iOS系统自带的IPSec和IKEv2协议的VPN。
在iOS 9中,开发者可以用NETunnelProvider扩展核心网络层,从而实现非标准化的私密VPN技术。代表产品有:极速VPN,surge, shadow socks
最重要的两个类是:
NETunnelProviderManager
NEPacketTunnelProvider
尽管苹果的文档写的还凑合,开发网络扩展的难度远远超过一般的app开发。
接下去我们一步一步说明怎么干。
问苹果拿官方权限,network extension entitlement 在开始动手前,先写封邮件发到这个地址
[email protected] 问苹果要开发network extension的权限。
没有这个权限,就算辛辛苦苦搞懂怎么编程,也会看到无限循环的NSError。
苹果收到邮件后,会自动回复一张表格,里面非常仔细地询问你公司,团队,产品,具体如何使用网络接口的情况。
大概内容是这样的,这个截屏只展现表格的一小部分。
Network Extension 权限申请表单 填好表格后回给苹果。
我们在等待2周之后才得到Apple发出的entitlement。
确认信长这个样子。
安装Xcode网络扩展模版插件 有了权限后,还需要安装XCode网络扩展模版插件。
没几个人听过吧~~
前提条件是,osx一定先升级到10.11,就可以在这儿找到苹果官方的插件
/System/Library/Frameworks/NetworkExtension.framework/Resources/NEProviderTargetTemplate.pkg 安装这个包。然后打开Xcode,在Application Extension分类中就可以看到网络扩展模版了。
Packet Tunnel Provider就是VPN要用的组件。
新建一组provisioning profile 装好网络扩展模版后,还需要新建一组provisioning profile。
登录到苹果开发者网站,新建iOS provisioning profile。流程和原来一样,就是到这一步,我们看到Entitlement下拉列表里多出一条Network Extension iOS条目。选它。
记得,开发版和生产版本的provisioning profile都需要重建,让它们支持网络扩展权限。
在项目里添加一个App扩展 为了做iOS 9的VPN app,项目里至少包含两个Target,也就是两种组件。
第一种组件就是大家都懂的UIApplication,处理界面,消息,数据等编程工作都在这个Target内完成。
第二种组件对很多iOS开发老手来说也是新的,很少用到,那就是App扩展。
App扩展其实就是一种插件,它和UIApplication是分开打包的,App扩展可以供其他应用使用,例如一套新键盘插件。
好奇心强的童鞋,可以看看苹果有关app扩展的官方文档。
https://developer.apple.com/library/ios/documentation/General/Conceptual/ExtensibilityPG/index.html#//apple_ref/doc/uid/TP40014214-CH20-SW1 做VPN也需要app扩展,它就是NEPacketTunnelProvider。
打开我们的项目,然后新建一个扩展。
File > New > Target > iOS, Application Extension > Packet Tunnel Provider Xcode自动生成一套代码框架,同时自动完成app扩展和app之间的关联。
XCode自动创建网络扩展框架和代码依赖关联 接下去的任务,就是写NEPacketTunnelProvider。
NETunnelProviderManager,控制VPN的开关 wow~ 要coding了。
现在,你已经明白了app和app扩展是分开打包的组件。
我们在app中,通过NETunnelProviderManager来控制VPN。
而在app扩展中,用NEPacketTunnelProvider来实现VPN的IO。
每个NETunnelProviderManager实例对应一个具体的VPN设置。先看看啥叫VPN设置。
如上图,屏幕上边有一个iOS 9的enterprise VPN,下边还有一个iOS 8的personal ipsec vpn。
我们要开发的NETunnelProviderManager就对应那个enterprise vpn。
NETunnelProviderManager和VPN设置是一一对应关系。如果app有两个VPN设置,我们通过代码就能得到两个NETunnelProviderManager实例。
我们要通过代码,对NETunnelProviderManager做四种操作
新建一个VPN设置
启动VPN
查询VPN状态
关闭VPN 先说新建VPN设置
你可能在市场上看到很多VPN服务商,给你一组ip地址,用户名,密码之类的,然后你辛辛苦苦手工配置后才发现,连也连不上。
要用NETunnelProviderManager自动设置VPN,先调用这个方法
- (void)loadAllFromPreferencesWithCompletionHandler:(void (^) (NSArraymanagers, NSError error))completionHandlerapp 刚安装时,肯定没有VPN profile,因此第一次调用这个方法,managers数组是空的。
这时就这么干。
NETunnelProviderManager *manager = [[NETunnelProviderManager alloc] init];
NETunnelProviderProtocol *protocol = [[NETunnelProviderProtocol alloc] init];
protocol.providerBundleIdentifier = @"com.tigervpns.hideme.ios.HideMe.HideMeTunnelProvider";// bundle ID of tunnel provider
protocol.providerConfiguration = @{@"key": @"value"};
protocol.serverAddress = @“server”;// VPN server address
manager.protocolConfiguration = protocol;
[manager saveToPreferencesWithCompletionHandler:^(NSError * _Nullable error) { … } 看到吗?
我们init一个新的NETunnelProviderManager,给它配一个NETunnelProviderProtocol。这个protocol里边,最最重要的属性就是我们接下去要做的网络扩展的bundle ID,这样NETunnelProviderManager才能知道控制哪个网络扩展。
protocol还可以带其他属性,如服务器地址,用户名,密码,等等。
最后用saveToPreferencesWithCompletionHandler保存该配置。
于是,iphone上就多了一个VPN设置。
启动VPN
有了NETunnelProviderManager实例后,我们可以启动它。
NETunnelProviderSession session = (NETunnelProviderSession) manager.connection;
NSDictionary *options = @{@“key” : @“value”};// Send additional options to the tunnel provider
NSError *err;
[session startTunnelWithOptions:options andReturnError:&err]; NETunnelProviderSession代表VPN连接,每个VPN配置只有一个session对象。
查询VPN状态
想看看某个VPN配置目前啥状态,先拿到它对应的NETunnelProviderManager,然后通过属性
manager.connection.status 就可以知道当前VPN状态。
NEVPNStatusConnected 已连接
NEVPNStatusDisconnected 断开
NEVPNStatusConnecting 正在连接
NEVPNStatusDisconnecting 正在断线
NEVPNStatusInvalid 无效状态,配置有错
NEVPNStatusReasserting 暂时无法获得确切状态 NEPacketTunnelProvider,真正的VPN核心代码 现在进入网络扩展了,NEPacketTunnelProvider才是VPN真正核心代码。
NEPacketTunnelProvider是个base class,我们要继承它,然后在子类里填入具体实现。
以下两个方法是一定要实现的,对应VPN的启动和断线逻辑:
(void)startTunnelWithOptions:(NSDictionary )options completionHandler:(void (^)(NSError ))completionHandler
(void)stopTunnelWithReason:(NEProviderStopReason)reason completionHandler:(void (^)(void))completionHandler 当app里的NETunnelProviderManager对象调用startTunnelWithOptions时,控制流程就跳到app extension里的startTunnelWithOptions方法。iOS会自动加载app extension。
startTunnelWithOptions有两个参数。
options是个key-value结构,从app里传过来,具体有啥信息完全由开发者自己定义。
completionHandler是个objective-c的block,由iOS系统提供。我们需要把这个block存放到某个变量里,待VPN启动完成时主动调用这个block。
stopTunnelWithReason也有两个参数。
reason表示VPN被关闭的理由。iOS预先定义了一组NEProviderStopReason常数,但实际应用时,reason基本不用。
completionHandler的用法同startTunnelWithOptions第二个参数一样,不再重复。
下面我们以极速VPN(hideme vpn)为例,聊聊VPN数据流到底怎么做。本文只谈UDP。
VPN数据流大致做两件事。
从虚拟网卡读到IP包,加密后用UDP送到VPN服务器
从UDP读取VPN服务器返回的加密IP包,解密后,送到虚拟网卡 以上两件事是同时进行的。只要VPN开着,数据流永不停止。
iOS提供了event-driven的异步通信方式,让以上两个数据流同时进行。
我们把这个数据流叫做IO loop。
那VPN连接时发生什么呢?也就是IO loop启动前需要做啥呢?
第一,VPN客户端需要知道VPN服务器的IP地址和端口,才能进行连线认证。
NWHostEndpoint *peer = [NWHostEndpoint endpointWithHostname:_ip port:_port]; 第二,我们要通过UDP发出认证请求。
熟悉socket的童鞋们,直接写个send或sendto就行了,但是,为了利用network extension提供的event driven IO性能,我们用它自带的类。
NWUDPSession *udpSession = [self createUDPSessionToEndpoint:peer fromEndpoint:nil]]; 其中,self是我们自己的NEPacketTunnelProvider对象。
有了udpSession,就可以用writeDatagram发送UDP消息。
[udpSession writeDatagram:packet completionHandler:^(NSError * _Nullable error) {….}]; 第三,我们要处理UDP的回复。
用C来写就是read或者recv,对不对?
但我们反复提到network extension自带的event driven IO,所以应该这样写
[udpSessionsetReadHandler:(void (^)(NSArraydatagrams, NSError error))handler maxDatagrams:(NSUInteger)maxDatagrams] 要写一个handler,当UDP收到数据时,系统会自动调用这个handler。注意是“自动调用”,这就是event-driven IO的核心,我们只需要告诉系统,从UDP接收到数据包是做哪些事情,而不用时刻检查UDP有没有数据传送过来。
第四,配置虚拟网卡vtun0。
假如认证完成,VPN服务器告诉客户端这些信息
虚拟网卡地址: 10.0.1.100
network mask: 255.255.255.0
DNS:8.8.8.8和8.8.4.4 我们要调用setTunnelNetworkSettings方法,告诉iOS配置虚拟网卡。
NSArray *addresses = @[“10.0.1.100];
NSArray *subnetMasks = @[@"255.255.255.0"];
NSArray*dnsServers = @[“8.8.8.8”, “8.8.4.4”];
NEPacketTunnelNetworkSettings *settings = [[NEPacketTunnelNetworkSettings alloc] initWithTunnelRemoteAddress:[vpn_server_public_ip_address]];
settings.IPv4Settings = [[NEIPv4Settings alloc] initWithAddresses:addresses subnetMasks:subnetMasks];
NEIPv4Route *defaultRoute = [NEIPv4Route defaultRoute];
NEIPv4Route *localRoute = [[NEIPv4Route alloc] initWithDestinationAddress:@"10.0.0.0" subnetMask:@"255.255.255.0"];
settings.IPv4Settings.includedRoutes = @[defaultRoute, localRoute];
settings.IPv4Settings.excludedRoutes = @[];
settings.MTU = [NSNumber numberWithInt:1500];
[self setTunnelNetworkSettings:settings completionHandler:^(NSError * _Nullable error) {
if (error) {
NSLog(@"setTunnelNetworkSettings error: %@", error);
} else {
NSError *err;
pendingCompletionHandler(err);
}]; 代码中pendingCompletionHandler就是startTunnelWithOptions的第二个参数。
一切顺利的话,这段代码执行完毕时,你会看到iOS状态栏显示了这个标记。
接着,IO loop就开始工作了。
前面,已经提到过IO loop在两个方向的数据流。
NEPacketTunnelProvider有个属性叫packetFlow,它的类是NEPacketTunnelFlow
用这个对象的readPacketsWithCompletionHandler方法可以从虚拟网卡读取IP包。
反方向,就用writePackets:withProtocols:方法把IP包写入虚拟网卡。
至于怎么加密VPN数据,是不是压缩,采用哪种格式打包,加多少meta信息,等等,本篇就不展开了。
总结 在所有的流行平台上,iOS是最后支持自定义VPN协议的。Android早在2012年就允许开发者完全控制VPN模块。不过iOS 9还是干的挺漂亮,苹果的代码结构充满苹果的味道:简单,清爽。
network extension有着非凡的用途,通过它,我们可以让app截获所有其他app发出的网络数据包,随后用我们喜欢的方式进行处理。
VPN只是用途之一。
传统VPN技术已经不适合中国。要想更加舒服地上网,要靠自定义的VPN技术,苹果也终于领会到这个痛点。