为iOS和OSX开发VPN的九九八十一难

2835 查看

故事的起因

2015年苹果在开发者大会上,首次提到iOS 9和OSX 10.11将公开底层网络接口给第三方developer,这组新的接口叫做Network Extension API。简单地讲,苹果将在iOS和OSX里植入虚拟网卡,允许第三方app控制虚拟网卡并修改系统路由表。

在此之前的8年里,苹果从未公开过这组API。在没有jailbreak的iOS上,只有三家公司曾做到过控制系统路由表:Cisco, Juniper,以及iOS 6.0之后的OpenVPN社区。他们能做,因为他们和Apple签了某种协议,从而获得苹果定制的entitlement。

现在,苹果决定开放这个权限给全世界。

于是技术狂悄悄动手了。ss在7月中开始coding全新的proxy代码,可惜8月底git repo被清空了。那时基于新接口的ss还没完工。

为苹果免费debug的艰难历程

我和ss几乎同时开始做这块。不过,我的打法是开发全新VPN协议,这和ss走sock5代理是两条路。

要调用Network Extension,先要问苹果拿一个特殊的entitlement。否则只能得到无止尽的NSError。拿这个许可大约花费两周时间。

我们很早就在Android上开发了新的VPN协议并投入使用。

原以为拿到许可之后,简单地把C代码搬到XCode里,再加个OC或Swift的UI层就搞定了。后来的经历证明苹果的坑很多,官方文档和stackoverflow对这块技术也接近空白,一切全靠试错。

第一个坑,NWTCPConnection不work

开篇提到的苹果开发者大会Session 717,当时Tommy Pauly演示了一个swift写的vpn demo,用的是NWTCPConnection。三个月后苹果放出来的sample code也用了NWTCPConnection。

But, 实测下来,NWTCPConnection根本不work,connect一直timeout,不知道Tommy Pauly当时怎么做的。

Workaround还是有的,就是用C开TCP socket,然后用BSD的kqueue自己写异步IO。

第二个坑,NWUDPSession每个包最多只能传送1472字节

这很容易理解,大部分WiFi环境下iOS网卡MTU配置成1500,减掉IP header共20字节,再减掉UDP header共8个字节,剩下数据部分1472个字节。

但是iOS的行为非常古怪。

之前我们在Windows, OSX,Linux和Android上获得的经验是这样的,系统IP层会自动分解超大的UDP数据,这个过程叫fragment,送到另一端时IP层又自动合成出原始UDP包,这个过程叫defragment。整个过程对应用层app是透明的。所以在其他几个操作系统上用UDP传送超过MTU的数据不会有问题。

iOS呢?实测下来,iOS不会自动fragment/defragment,也不会把超过MTU的包整个扔掉,而是传送1472字节给应用层。

于是从network extension拿到的只是部分数据,当然无法跑通VPN。这问题花了几周时间才搞清楚。

强烈要求苹果改改iOS TCP/IP内核代码,起码也要和OSX一样吧。

第三个坑,iOS Provision Profile带来的问题

终于,iOS上搞定了,自己用的很开心。

交给苹果review,等到D疼,终于approve了。从app store下载安装,居然不能用!

dev版本是可以用的,production版本不能用,问题在哪里?

先连夜赶工,VPN核心模块切换到老代码,然后加个switch,默认关闭。

接下去,找始作俑者,也就是开发Network Extension的那位大神。

非常不幸的是,苹果在安全保密方面极端严格。通过这个渠道去询问技术问题,绝对无可奉告。

上论坛试试吧。

最终我们得到大神回复,删掉本机所有profile,然后新建一组profile,重新build,试试!

我们照做了,搞定!

不过坦率承认,这招我们时常用,在此贴之前,不管怎么弄profile都没用。苹果微调了啥呢?

更艰难的Mac之旅

有了iOS的code base,port到Mac岂不是秒杀?

Yes and No.

在UI层,大部分工作就是把UI改成NS,比如把UIView改成NSView。这要多谢Cocoa和Cocoa Touch结构基本相同。

但底层的app extension里还是有细节上的差异。要注意,Xcode不会显示app extension里的NSLog,得这样看。

又瞎忙了N天,看起来差不多了。在自己Mac上跑的甚好。

事后证明,还需要被折磨6周,才能最终把它推到Mac app store。

这次,还是provision profile的问题。见图。

原来,调用Network Extension要求先取得苹果的许可,然后可以新建一个provision profile使用network extension扩展entitlement,其中包括三种权限,分别是:

  • HotspotHelper,允许app控制wifi连接,这是wifi万能钥匙这种app必须的功能

  • AppProxy,让第三方app在应用层过滤其他app的网络通信,适合做内容过滤和监控

  • PacketTunnelProvider,这是VPN相关的,在IP层拿到虚拟网卡和路由表控制权

但是,OSX不支持HotspotHelper,所以,Xcode打包项目后做validation时报错了。

解决方案呢?

先依靠自己的力量,找遍所有相关资料,一个一个试,无解。

然后,去Apple开发者论坛发帖,无解。

最后几经周折,在确信这问题99%出在苹果的前提下,硬着头皮找到苹果Core OS团队的人。

嗯,进了他们的ticket system,也算为OSX和Network Extension做点贡献了。

然后,等。

顺便说下,OSX里有个pluginkit命令,开发app extension时要用到。

列出所有plugin

pluginkit -m -A

强制安装plugin

pluginkit -a <filename>

强制删除指定的plugin

pluginkit -r <filename>

在开发阶段,Xcode有时不能正确安装或刷新app extension,此时就要手工操作了。

完工

后来,Apple fix了这个问题。幸好,不需要升级XCode。

我们有幸成为全球第一个,也是唯一一个遇到这个问题的人。

这是Mac版成品的样子。

这是iOS版。

整个开发过程零零碎碎历经几个月。我们总体感觉是,苹果开放的Network Extension接口没有达到最成熟的阶段,可能的原因是苹果开发流程很封闭而且环节很多,涉及Apple Developer Program里各种权限,涉及XCode,还有就是code,任何一环出错都会造成产品无法上线。

而且我们认为,苹果自己的Core Network部门并没有像第三方developer一样从头到尾把流程跑一遍,才会出现各种细节问题。

Anyway,we did it。

现在,我们需要更多人来测试这个产品。当时iOS那个profile引发的bug至今没有得到苹果的官方解释。同时,在Mac上,我们也发现pluginkit不是每次都能自动加载app extension。

任何新发现的问题我们都会及时和苹果Core Network团队沟通,相信大部分都是他们的错。

如果你有iOS 9.0和OSX 10.11以上的设备,正好需要完全免费的VPN,请试用这两个app。

iOS: https://itunes.apple.com/us/app/hideme-free-vpn-proxy-unlimited/id879905781?ls=1&mt=8

Mac: https://itunes.apple.com/us/app/hideme-free-vpn-proxy-unlimited/id1084098222?ls=1&mt=12