手把手NetworkExtension: 1. 创建L2TP/I
自从苹果在WWDC 2015公布NetworkExtension以来, 已经出现了大量优秀的基于此的app,比如Surge, Sh****Rocket等, 并且网络上有大量的教程(笑)怎么用NetworkExtensions.
然而不幸的是,到了2017年配合Xcode 8.3和macOS 10.12, 跟着那些个教程一起食用的话, 绝对会被带进坑里 (大笑).
下面让我带你走进跟着网络教程配置遇到的坑们.
( 注:本系列教程中,如无特殊说明,开发环境是macOS 10.12.4 + Xcode 8.3 + Swift 3.1 )
0. 坑篇
0.0 官方文档太简单, 教程满天飞, 都用不了
哭啊...关于查找文档和教程的事儿, 真的是想死. 要么教程是东拼西凑摘抄拷贝的, 要么代码逻辑混乱不知所云, 要么代码太老, 完全跑不起来, 一头雾水
官方的教程等于没有, api文档写的真心渣...看过的人都说渣......不相信可以去瞅瞅
还有就是, 无论Google还是StackOverflow, 真心找不到可用的教程, 不得不一步步摸索出来
0.1 去开发者中心申请entitlement?
其实从2016年10月份开始, 苹果开发者已经不再需要单独申请NetworkExtension的权限了. 在开发者中心的app ID设置里面, 就可以在其中添加NetworkExtension权限. 如图:

当然,如果要申请Hotspot Helper权限, 还是要去官网申请-->地址点我
对此, 官方说明如图:

0.2 NEProviderTargetTemplates.pkg ?
苹果不知道什么原因,从macOS 10.12开始就把这个扩展包给去掉了. 并且官方论坛里给出的推荐是, 从10.11当中提取. 网络上也有很多教程也是这么说的.
燃鹅, 你会发现,安装之后在App Extensions列表里面又丑,而且安装的模板各种代码无法编译, 都是老的方法, 老的枚举名等, 让人抓狂. 那么安装了的该怎么办呢?
很简单, 去对应的$dir中删掉相关文件:
- 如果安装时是当前用户: $dir = ~/Library/Developer/Xcode/Templates/Project Templates/
- 如果是全部用户: $dir = /Library/Developer/Xcode/Templates/Project Templates/
到$dir目录下,删掉iOS和Mac下的Application Extension当中相关的文件.

那么, 到底该怎么创建对应的Extensions扩展呢? 不要着急, 本文会讲到.
0.3 其他坑等我想到再补充

好,进入正题, 手把手教你如何开发一个能连接到L2TP/IPSec VPN的demo
1. 配置
首先, 新建个项目, 语言选swift, 包名就叫com.lucifer.proxydump
吧.
参照 坑0.1 的内容, 配置好entitlement和provision, 在项目的Capabilities中打开NetworkExtensions, PersonalVPN和KeychainSharing( 因为建立vpn时的密码是要从KeyChain当中获取的, 详细情况后面会讲), 并且在KeychainSharing当中添加com.apple.managed.vpn.shared
好了, 开始开发.
2. 开始
2.1 新建Package Tunnel Protocol的 target
问题是, 在Xcode的App Target里面没有相关的Extension啊?
其实在苹果开发者论坛中, 2017年2月14日 官方新增了如下的参考(对, 就是这个帖子, 之前推荐要用10.11的pkg模板):

- 新建个
Spotlight Index Extension
target (就叫PacketTunnelProvider吧)

- 删除
IndexRequestHandler.{swift,h,m}

其实这里还漏了一步,就是在PacketTunnelProvider
的Target的Capabilities中也要开启PersonalVPN和NetworkExtensions这两个开关,否则相关的库引入不进来
-
新建个
NEPacketTunnelProvider
的子类, 就叫PacketTunnelProvider
好了. (记得新建完了, 在类中引入NetworkExtension
) -
修改当前Extension下的Info.plist文件的NSExtension:
- 设置
NSExtensionPointIdentifier
为com.apple.networkextension.packet-tunnel
- 替换
NSExtensionPrincipalClass
中的IndexRequestHandler
为刚刚我们新建的类 - 效果如图:
- 设置

2.2 配置PacketTunnelProvider
PacketTunnelProvider
的逻辑是网络通信处理的核心. 比如著名的协议sh******cks协议就在这里进行自定义处理, 如果你有自己的通信协议, 也可以在这里进行定制. 首先把startTunnel
和stopTunnel
给重写一下
import UIKit
import NetworkExtension
class PacketTunnelProvider: NEPacketTunnelProvider {
override func startTunnel(options: [String : NSObject]? = nil, completionHandler: @escaping (Error?) -> Void) {
completionHandler(nil)
}
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
}
}
毕竟只是为了建立普通的vpn, 所以不需要太配置PacketTunnelProvider
, 留着默认值就好, 本节内容不做详述, 后续的文章里会展开来讲.
2.3 添加主逻辑
在ViewController中央添加个SwitchButton作为VPN开关

下面,核心逻辑来了
添加 NEVpnManager
, 用来控制VPN的. 不过我们用的是IPSec验证方式, 而NetworkExtension
中设置密码操作一定要从KeyChain当中读取, 因此顺带要建立一些存取KeyChain数据的方法. 有关KeyChain相关说明请自行搜索.
这里引用一段其他博文里的内容(因为自己懒, 就直接拷贝来)
IPSec 协议里的密码以及预共享密钥都需要是一个 KeyChain 中密码的永久引用(persistent reference)。
如果用证书来作为 IKE 的认证方式,而且 Server 端用的是自签发证书,则需要手工将 CA 导入到 iOS 设备。目前 Apple 还没提供添加授信证书的方法。
相关代码如下
let serviceName = "let.us.try.vpn.in.ipsec" //随便自定义
let vpnPwdIdentifier = "vpnPassword" //keychain的密码存取key
let vpnPrivateKeyIdentifier = "sharedKey" //keychain中共享密钥存取key
func createManager(){
//先把共享密钥和密码保存到keychain, 后面会用到
createKeychainValue("vpn密码", vpnPwdIdentifier)//密码
createKeychainValue("IPSec共享密钥", vpnPrivateKeyIdentifier)//共享密钥
let manager = NEVPNManager.shared() //先用shared获取个单例
//载入vpn相关信息
manager.loadFromPreferences { (error) in
var conf: NEVPNProtocolIPSec? = manager.protocolConfiguration as? NEVPNProtocolIPSec
if conf == nil {
conf = NEVPNProtocolIPSec()
}
conf!.serverAddress = "10.200.11.108" //vpn服务器地址
conf!.username = "zach" //vpn账户
conf!.authenticationMethod = .sharedSecret //选择共享密钥方式
conf!.sharedSecretReference = self.searchKeychainCopyMatching(self.vpnPrivateKeyIdentifier) //从keychain中获取共享密钥
conf!.passwordReference = self.searchKeychainCopyMatching(self.vpnPwdIdentifier) //从keychain中获取密码
manager.protocolConfiguration = conf!;
manager.localizedDescription = "走你vpn";
manager.isEnabled = true //allow对话框后自动选中当前vpn
manager.saveToPreferences { (error) in
print("done: \(error.debugDescription)")
if error == nil {
self.vpnManager = manager
}
}
}
}
/**
* 存取keychain用到的dictionary
*/
func newSearchDictionary(_ identifier : String) -> NSMutableDictionary {
let searchDictionary = NSMutableDictionary()
let encodedIdentifier: Data = identifier.data(using: .utf8)!
searchDictionary.addEntries(from: [
kSecClass as NSString: kSecClassGenericPassword as NSString,
kSecAttrGeneric as NSString: encodedIdentifier,
kSecAttrAccount as NSString: encodedIdentifier,
kSecAttrService as NSString: serviceName
])
return searchDictionary
}
/**
* 搜索对应的keychain数据
*/
func searchKeychainCopyMatching(_ identifier : String) -> Data{
let searchDictionary = newSearchDictionary(identifier)
searchDictionary.addEntries(from: [
kSecMatchLimit as NSString: kSecMatchLimitOne as NSString,
kSecReturnPersistentRef as NSString: true
])
var result: CFTypeRef? = nil
SecItemCopyMatching(searchDictionary as CFMutableDictionary, &result)
return result as! Data
}
/**
* 创建对应的keychain数据
*/
func createKeychainValue(_ password: String, _ identifier: String) -> Bool{
let dictionary = newSearchDictionary(identifier)
var status: OSStatus = SecItemDelete(dictionary as CFMutableDictionary)
let passwordData: Data = password.data(using: .utf8)!
dictionary.setObject(passwordData, forKey: kSecValueData as NSString)
status = SecItemAdd(dictionary as CFDictionary, nil)
return status == errSecSuccess
}
内容很简单, 获取NEVPNManager
单例之后,调用manager.loadFromPreferences
方法载入一次它的配置信息, 这里为了简单写, 就每次都直接修改配置信息了. 注意: NEVPNManager不能直接初始化, 而是调用shared()方法获取单例.
需要说明的是conf!.authenticationMethod
有三个值:
public enum NEVPNIKEAuthenticationMethod : Int {
/*! @const NEVPNIKEAuthenticationMethodNone Do not authenticate with the IPSec server */
case none //不做认证
/*! @const NEVPNIKEAuthenticationMethodCertificate Use a certificate and private key as the authentication credential */
case certificate // 证书认证
/*! @const NEVPNIKEAuthenticationMethodSharedSecret Use a shared secret as the authentication credential */
case sharedSecret //共享密钥认证
}
而我们用的就是共享密钥认证. 如果要使用证书认证, 调用方法如下:
conf!.authenticationMethod = .certificate
try! conf!.identityData = Data(contentsOf: Bundle.main.url(forResource: "client", withExtension: "p12")!)
conf!.identityDataPassword = "p12文件密码"
其他内容主要看代码的注释就行了.
最后在viewDidLoad
方法中调用createManager
方法, 这样在打开软件的时候出现那个提示安装vpn的对话框了

点击allow, 认证完后可以看到下方"个人VPN"当中就会增加一条刚刚设置的"走你VPN"
(其实本来验证完之后, 那个开关一直处于关闭当中, 这里是后截图的, 所以不用在意这些细节)
然后回到主界面. 我们要开启vpn, 点一下中间的添加的开关, 就可以愉快的连接上vpn了(笑).
当然连不上, 因为我们还没给中间的UISwitch
添加逻辑, 科科
@IBAction func switchChangeAction(_ sender: UISwitch) {
if vpnManager.connection.status == .disconnected {
do {
try vpnManager.connection.startVPNTunnel()
} catch {
NSLog("start error: \(error.localizedDescription)")
}
}
else {
vpnManager.connection.stopVPNTunnel()
}
}
好了, 大功告成, 这回打开开关, 愉快的连接到我们的服务器吧
上图是连接后的状态, 如何确定我们真的连接上了呢, 一种是访问查ip网站后查看当前的公网ip. 由于我在内网架设的测试vpn, 因此公网ip都是一样的, 所以索性登录服务器查看一下连接用户情况就知道了
如图, 登录vpn之前:

连接vpn之后:
嗯, 大功告成!!!
最后, 还是得吐槽一下, NetworkExtension相关的教程, 网络上搜的不能全信, 很多要么用不了, 要么就是太老, 真的没法正常使用.
本文介绍的其实是PersonalVPN的功能, 跟NetworkExtension非常相关, 比如要学习开发例如Surge
或者Sh****Rocket
等等这些优秀的工具时, 这部分的知识算是第一步.
好, 迈向Surge
或者Sh****Rocket
的一百步(大笑)中的第一步顺利迈出去了, 接下来一段时间会继续说说这块相关的开发.
比心❤
P.S. 10.12.4配合Chrome57.0.2987.133 (64-bit)版本真心崩溃, 是 真的心和浏览器不断崩溃
P.S. 其实细心的人已经发现, 创建L2TP/IPSec 根本用不到PacketProtocolProvider, 哪怕不添加也没关系. 没哦! 毕竟用的是NEVPNManager
, 而不是NETunnelProtocolManager
. 其实NETunnelProtocolManager
是NEVPNManager
的子类, 在后续文章中会讲到这块的用法, 不要着急, 我不是富坚老贼(毕竟没有人家的才华), 不会停更
P.S. 第二篇已经更新-->手把手NetworkExtension: 2. 分析官方 Demo 源码之 NEPacketTunnelProvider使用部分
转载请告诉我, 并注明来源, 谢谢