此篇文章不会详述iOS中VPN开发的各种流程,只是博主自己在做 Personal VPN 开发时的遇到的一些坑,如果需要详细流程可自行查询官方文档。
Apple提供了 NetworkExtension 框架,让开发者可以在iOS、Mac os中进行VPN开发。iOS中的VPN开发分为 个人VPN 和 非个人VPN 开发。个人VPN开发比较简单,可以直接使用系统提供的IPSec、IKEv2协议来进行VPN连接。而在iOS9之后,Apple开放了新的api,可以让开发者开发自己的私密协议的VPN,参考地址: https://developer.apple.com/reference/networkextension。
开发前提
虽然此篇文章不讲解流程,但开发VPN的前提还是值得一说的。开发VPN必须得有 开发者账号 ,并且在对应 App IDs 配置项中需要勾选下图所示选项,再创建对应的 Provisioning Profiles 文件。
注意VPN开发只能在真机进行测试。
开发流程
Personal VPN 通过 NEVPNManager 类来进行开发,NEVPNManager 用于创建和管理VPN配置并控制最终的VPN隧道连接。参考地址:https://developer.apple.com/reference/networkextension/nevpnmanager 。
获取VPN管理者单利对象
let vpnManager = NEVPNManager.shared()
创建并配置IPSec协议
let vpnProtocol = NEVPNProtocolIPSec() vpnProtocol.serverAddress = "vpn服务器地址" vpnProtocol.username = "vpn用户名" vpnProtocol.passwordReference = "vpn密码 需要从keychain中引用" // 预共享密钥认证 vpnProtocol.authenticationMethod = .sharedSecret // 认证方式 (证书 和 预共享密钥) vpnProtocol.sharedSecretReference = "预共享密钥 需要从keychain中引用" // 证书认证 // vpnProtocol.authenticationMethod = .certificate // vpnProtocol.identityData = try? Data(contentsOf: URL(fileURLWithPath: Bundle.main.path(forResource: "client", ofType: "p12")!)) // vpnProtocol.identityDataPassword = "证书导入密钥" vpnProtocol.useExtendedAuthentication = true vpnProtocol.disconnectOnSleep = true // 睡眠是否断开vpn连接
VPN常用配置信息
vpnManager.protocolConfiguration = vpnProtocol // 设置VPN协议配置 vpnManager.localizedDescription = "风速加速器" // VPN本地名称 vpnManager.isEnabled = true // 激活VPN // vpnManager.localIdentifier = "本地标识" // vpnManager.remoteIdentifier = "远程标识" // let rules = [NEOnDemandRule]() // 配置按需连接的规则 // vpnManager.onDemandRules = rules // 设置按需连接规则 // vpnManager.isOnDemandEnabled = true // 按需连接默认开关
保存VPN配置信息到系统偏好设置
vpnManager.saveToPreferences(completionHandler: { (error) in if let error = error { print(error.localizedDescription) return } // VPN配置保存成功 })
从系统偏好设置加载VPN配置信息
vpnManager.saveToPreferences(completionHandler: { (error) in if let error = error { print(error.localizedDescription) return } // 加载配置成功 }
从系统偏好设置移除VPN配置信息
vpnManager.removeFromPreferences { (error) in if let error = error { print(error.localizedDescription) return } print("移除配置成功") }
启动VPN
do { try vpnManager.connection.startVPNTunnel() } catch { print("启动失败") }
断开VPN
vpnManager.connection.stopVPNTunnel()
监听VPN系统配置信息和VPN连接状态改变
NotificationCenter.default.addObserver(self, selector: #selector(vpnConfigurationDidChanged(notification:)), name: .NEVPNConfigurationChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(vpnStatusDidChanged(notification:)), name: .NEVPNStatusDidChange, object: nil)
/// vpn配置改变监听 func vpnConfigurationDidChanged(notification: Notification) { } /// vpn状态改变监听 func vpnStatusDidChanged(notification: Notification) { }
完整代码
JFVPNManager.swift
import UIKit import NetworkExtension enum JFVPNStatus: Int { case invalid = 0 // 无效连接 case disconnected = 1 // 已经断开 case connecting = 2 // 正在连接 case connected = 3 // 已经连接 case reasserting = 4 // 重试断言 case disconnecting = 5 // 正在断开 } protocol JFVPNManagerDelegate: NSObjectProtocol { /// vpn连接状态改变 /// /// - Parameters: /// - manager: vpn管理者 /// - status: vpn状态 func vpnConnectionStatusDidChanged(manager: JFVPNManager, status: JFVPNStatus) } class JFVPNManager: NSObject { static let shared = JFVPNManager() private var vpnManager = NEVPNManager.shared() weak var delegate: JFVPNManagerDelegate? override init() { super.init() // 监听vpn配置和连接状态 NotificationCenter.default.addObserver(self, selector: #selector(vpnConfigurationDidChanged(notification:)), name: .NEVPNConfigurationChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(vpnStatusDidChanged(notification:)), name: .NEVPNStatusDidChange, object: nil) } /// vpn配置改变监听 func vpnConfigurationDidChanged(notification: Notification) { print("vpn本地配置信息改变") connect() } /// vpn状态改变监听 func vpnStatusDidChanged(notification: Notification) { let status = JFVPNStatus(rawValue: vpnManager.connection.status.rawValue)! delegate?.vpnConnectionStatusDidChanged(manager: self, status: status) } /// 连接vpn fileprivate func connect() { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(1 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)) { self.vpnManager.loadFromPreferences { (error) in if let error = error { print(error.localizedDescription) self.delegate?.vpnConnectionStatusDidChanged(manager: self, status: .invalid) return } do { try self.vpnManager.connection.startVPNTunnel() print("开启vpn隧道成功") } catch { print("开启vpn隧道失败") self.delegate?.vpnConnectionStatusDidChanged(manager: self, status: .disconnected) } } } } /// vpn是否已经连接 var isConnected: Bool { get { return vpnManager.connection.status == .connected } } /// vpn是否正在连接 var isConnecting: Bool { get { return vpnManager.connection.status == .connecting } } /// 预加载vpn配置信息 func preLoadPreferences() { vpnManager.loadFromPreferences { (error) in if let error = error { print(error.localizedDescription) return } print("预加载vpn配置信息成功") } } /// 配置VPN系统偏好设置并连接VPN /// /// - Parameters: /// - ip: ip地址 /// - userName: 用户名 /// - password: 密码 /// - psk: 共享密钥 func connect(ip: String, userName: String, password: String, psk: String) { print("开始连接vpn ip=\(ip) userName=\(userName) password=\(password) psk=\(psk)") // 保存vpn密码和密钥到钥匙串 JFKeychain.createKeychainValue(password, forIdentifier: "VPN_PASSWORD") JFKeychain.createKeychainValue(psk, forIdentifier: "VPN_PSK") vpnManager.loadFromPreferences { (error) in if let error = error { print(error.localizedDescription) self.delegate?.vpnConnectionStatusDidChanged(manager: self, status: .invalid) return } let vpnProtocolIKEv2 = NEVPNProtocolIKEv2() vpnProtocolIKEv2.serverAddress = ip vpnProtocolIKEv2.username = userName vpnProtocolIKEv2.passwordReference = JFKeychain.searchCopy(matching: "VPN_PASSWORD") vpnProtocolIKEv2.authenticationMethod = .sharedSecret vpnProtocolIKEv2.sharedSecretReference = JFKeychain.searchCopy(matching: "VPN_PSK") vpnProtocolIKEv2.remoteIdentifier = ip vpnProtocolIKEv2.useExtendedAuthentication = true vpnProtocolIKEv2.disconnectOnSleep = true self.vpnManager.protocolConfiguration = vpnProtocolIKEv2 self.vpnManager.localizedDescription = "风速加速器" self.vpnManager.isEnabled = true self.vpnManager.saveToPreferences(completionHandler: { (error) in if let error = error { // 第一次保存的时候会提示用户授权,用户拒绝则会保存失败 print(error.localizedDescription) self.delegate?.vpnConnectionStatusDidChanged(manager: self, status: .invalid) return } print("保存配置成功") // 第一次保存配置成功没有回调 则手动调用一次(测试机型 iPhone6 iOS10.1.1) if !UserDefaults.standard.bool(forKey: "FIST_CONNECT_VPN") { UserDefaults.standard.set(true, forKey: "FIST_CONNECT_VPN") self.connect() } }) } } /// 断开VPN连接 func disconnect() { vpnManager.connection.stopVPNTunnel() } /// 移除VPN系统偏好设置 func remove() { vpnManager.removeFromPreferences { (error) in if let error = error { print(error.localizedDescription) return } print("移除配置成功") } } }
JFKeychain.m
#import "JFKeychain.h" @implementation JFKeychain static NSString * const serviceName = @"com.fengsu123.WindSpeedVPN.vpn_config"; + (NSData *)searchKeychainCopyMatching:(NSString *)identifier { NSMutableDictionary *searchDictionary = [self newSearchDictionary:identifier]; [searchDictionary setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit]; [searchDictionary setObject:@YES forKey:(__bridge id)kSecReturnPersistentRef]; CFTypeRef result = NULL; SecItemCopyMatching((__bridge CFDictionaryRef)searchDictionary, &result); return (__bridge_transfer NSData *)result; } + (BOOL)createKeychainValue:(NSString *)password forIdentifier:(NSString *)identifier { NSMutableDictionary *dictionary = [self newSearchDictionary:identifier]; OSStatus status = SecItemDelete((__bridge CFDictionaryRef)dictionary); NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding]; [dictionary setObject:passwordData forKey:(__bridge id)kSecValueData]; status = SecItemAdd((__bridge CFDictionaryRef)dictionary, NULL); if (status == errSecSuccess) { return YES; } return NO; } + (NSMutableDictionary *)newSearchDictionary:(NSString *)identifier { NSMutableDictionary *searchDictionary = [[NSMutableDictionary alloc] init]; [searchDictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass]; NSData *encodedIdentifier = [identifier dataUsingEncoding:NSUTF8StringEncoding]; [searchDictionary setObject:encodedIdentifier forKey:(__bridge id)kSecAttrGeneric]; [searchDictionary setObject:encodedIdentifier forKey:(__bridge id)kSecAttrAccount]; [searchDictionary setObject:serviceName forKey:(__bridge id)kSecAttrService]; return searchDictionary; } @end
JFHomeViewController.swift
VPN调用
// 连接VPN JFVPNManager.shared.delegate = self JFVPNManager.shared.connect(ip: ipAddress, userName: vpnAccount, password: vpnPassword, psk: "myPSKkey")
VPN状态改变回调
// MARK: - JFVPNManagerDelegate extension JFHomeViewController: JFVPNManagerDelegate { func vpnConnectionStatusDidChanged(manager: JFVPNManager, status: JFVPNStatus) { switch status { case .invalid: print("连接无效") case .connecting: print("正在连接") case .connected: print("已经连接") case .disconnecting: print("正在断开") case .disconnected: print("已经断开") case .reasserting: print("正在断言") } } }
注意事项:
1.第一次保存配置会给系统安装一个配置文件,并且用户需要手动授权。
2.每次启动VPN都需要去加载系统的配置信息。
3.IPSec协议里的密码和预共享密钥都需要是一个Keychain中密码的永久引用,即 kSecReturnPersistentRef,一定不要直接赋值。(证书密码不需要)
4.如果用证书来作为 IKE 的认证方式,而且 Server 端用的是自签发证书,则需要手工将 CA 导入到 iOS 设备。目前 Apple 还没提供添加授信证书的方法。
5.ENVPNManager 和系统配置文件中的信息不同步,可以监听 NEVPNConfigurationChange 通知。
6.connection 的 status 不支持 kvo,所以需要监听 NEVPNStatusDidChange 通知随时获取VPN连接状态改变。
常见错误:
1.与 VPN 服务器协议失败。(IPSec)
可能是psk密钥错误,也可能是 localIdentifier 和 remoteIdentifier 设置错误。
2.VPN 服务器并未响应。(IPSec)
可能是服务器地址填写错误,也可能是服务器的问题。
3.未提供任何 VPN 共享密钥。(IPSec)
共享密钥认证属性设置错误,也可能是没有使用 Keychain 永久引用。
4.连接没有任何反应。(IKEv2)
可能是没有设置 remoteIdentifier 。
欢迎大神填坑。。。。
网络切换坑:
如果我们需要连接VPN后,切换网络VPN也能断线重连或者不掉线,可以使用IKEv2协议。IPSec协议默认情况下,切换网络会断开VPN。如果配置了按需连接的rule,可以实现切换网络断线重连,但是我们只能从app里关闭VPN了,无法从系统VPN设置里关闭VPN,因为关闭了也会断线重连,除非手动关闭按需连接。