NetworkExtension之NEVPNManager开发笔记

/ 1

此篇文章不会详述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开发只能在真机进行测试。

qq20161126-02x

开发流程

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,因为关闭了也会断线重连,除非手动关闭按需连接。