Total Pageviews

Saturday 19 March 2016

分享开发 iOS 和 Mac 全新 VPN 的艰难历程

故事的起因

2015 年苹果在开发者大会上,首次提到 iOS 9 和 OSX 10.11 将公开底层网络接口给第三方 developer ,这组新的接口叫做 Network Extension API 。简单地讲,苹果将在 iOS 和 OSX 里植入虚拟网卡,允许第三方 app 控制虚拟网卡并修改系统路由表。
Imgur
在此之前的 8 年里,苹果从未公开过这组 API 。在没有 jailbreak 的 iOS 上,只有三家公司曾做到过控制系统路由表: Cisco, Juniper ,以及 iOS 6.0 之后的 OpenVPN 社区。他们能做,因为他们和 Apple 签了某种协议,从而获得苹果定制的 entitlement 。
现在,苹果决定开放这个权限给全世界。
于是技术狂悄悄动手了。 ss 在 7 月中开始 coding 全新的 proxy 代码,可惜 8 月底 git repo 被清空了。那时基于新接口的 ss 还没完工。
Imgur

为苹果免费 debug 的艰难历程

我和 ss 几乎同时开始做这块。不过,我的打法是开发全新 VPN 协议,这和 ss 走 sock5 代理是两条路。
要调用 Network Extension ,先要问苹果拿一个特殊的 entitlement 。否则只能得到无止尽的 NSError 。拿这个许可大约花费两周时间。
Imgur
我们很早就在Android 上开发了新的 VPN 协议并投入使用。
原以为拿到许可之后,简单地把 C 代码搬到 XCode 里,再加个 OC 或 Swift 的 UI 层就搞定了。后来的经历证明苹果的坑很多,官方文档和 stackoverflow 对这块技术也接近空白,一切全靠试错。

第一个坑, NWTCPConnection 不 work

开篇提到的苹果开发者大会 Session 717 ,当时 Tommy Pauly 演示了一个 swift 写的 vpn demo ,用的是 NWTCPConnection 。三个月后苹果放出来的 sample code 也用了 NWTCPConnection 。
Imgur
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 ,默认关闭。
Imgur
接下去,找始作俑者,也就是开发 Network Extension 的那位大神。
Imgur
非常不幸的是,苹果在安全保密方面极端严格。通过这个渠道去询问技术问题,绝对无可奉告。
上论坛试试吧。
Imgur
最终我们得到大神回复,删掉本机所有 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 ,得这样看。
Imgur
又瞎忙了 N 天,看起来差不多了。在自己 Mac 上跑的甚好。
事后证明,还需要被折磨 6 周,才能最终把它推到 Mac app store 。
这次,还是 provision profile 的问题。见图。
Imgur
原来,调用 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 团队的人。
Imgur
嗯,进了他们的 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 版成品的样子。
Imgur
这是 iOS 版。
Imgur
整个开发过程零零碎碎历经几个月。我们总体感觉是,苹果开放的 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 :
原文:http://v2ex.com/t/264480