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数据流大致做两件事。

  1. 从虚拟网卡读到IP包,加密后用UDP送到VPN服务器

  2. 从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技术,苹果也终于领会到这个痛点。

results matching ""

    No results matching ""