iOS - NetworkExtension 建立隧道(OpenVpn)

注意:由于简大叔对XXX关键字过敏,所以本文均用XXX代替V皮N。
需要实现Personal-XXX功能是苹果开发者账号才有权限开启,所以第一步先去开发者中心创建证书,并添加权限(此步骤省略,自己百度)

本文章针对的是OpenXXX !!!

我们将使用OpenXXXAdapter,使用Cocoapods进行安装

pod 'OpenVPNAdapter', :git => ' https://github.com/ss-abramchuk/OpenVPNAdapter.git' , :tag => '0.4.0'

Carthage安装

github "ss-abramchuk/OpenVPNAdapter"

多target时,Cocoapods的格式如下:

platform :ios, '10.0'
target 'OpenSSLOnce' do
use_frameworks!
pod 'AFNetworking','~> 4.0'
pod 'MJRefresh'
pod 'SVProgressHUD'
post_install do |installer_representation|
installer_representation.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'NO'
target 'TargetTunnel' do
use_frameworks!
pod 'OpenVPNAdapter', :git => ' https://github.com/ss-abramchuk/OpenVPNAdapter.git' , :tag => '0.4.0'

1、创建Target

@interface PacketTunnelProvider : NEPacketTunnelProvider @property(nonatomic,strong) OpenVPNAdapter *vpnAdapter; @property(nonatomic,strong) OpenVPNReachability *openVpnReach; typedef void(^StartHandler)(NSError * _Nullable); typedef void(^StopHandler)(void); @property(nonatomic,copy) StartHandler __nullable startHandler; @property(nonatomic,copy) StopHandler __nullable stopHandler; NS_ASSUME_NONNULL_END

PacketTunnelProvider.m

// PacketTunnelProvider.m // TargetTunnel #import "PacketTunnelProvider.h" #include "NEPacketTunnelFlow+NEPacketTunnelFlow_Extension.h" @interface PacketTunnelProvider ()<OpenVPNAdapterDelegate> // 这个先放下,一会讲,主要用到和父项目进行通信 @property (strong,nonatomic) NSUserDefaults *userDefaults; @implementation PacketTunnelProvider // 懒加载 -(OpenVPNAdapter*)vpnAdapter{ if(!_vpnAdapter){ _vpnAdapter = [[OpenVPNAdapter alloc] init]; _vpnAdapter.delegate = self; return _vpnAdapter; -(OpenVPNReachability*)openVpnReach{ if(!_openVpnReach){ _openVpnReach = [[OpenVPNReachability alloc] init]; return _openVpnReach; -(void)startTunnelWithOptions:(NSDictionary<NSString *,NSObject *> *)options completionHandler:(void (^)(NSError * _Nullable))completionHandler NETunnelProviderProtocol *proto = (NETunnelProviderProtocol*)self.protocolConfiguration; if(!proto){ return; NSDictionary<NSString *,id> *provider = proto.providerConfiguration; NSData * fileContent = provider[@"ovpn"]; // NSString * str1 = [[NSString alloc] initWithData:fileContent encoding:NSUTF8StringEncoding]; OpenVPNConfiguration *openVpnConfiguration = [[OpenVPNConfiguration alloc] init]; openVpnConfiguration.keyDirection = 1; openVpnConfiguration.fileContent = fileContent; // If true, don't send client cert/key to peer. openVpnConfiguration.disableClientCert = NO; // 用户名和密码进行认证 // openVpnConfiguration.settings = @{@"username":@"",@"password":@""}; // 如果要在暂停或重新连接期间保持TUN接口处于活动状态,请取消对此行的注释 // openVpnConfiguration.tunPersist = YES; NSError *error; OpenVPNProperties *evaluation = [self.vpnAdapter applyConfiguration:openVpnConfiguration error:&error]; if(error){ completionHandler(error); return; // 配置用户名和密码 if (!evaluation.autologin) OpenVPNCredentials *tials = [[OpenVPNCredentials alloc]init]; tials.username = [NSString stringWithFormat:@"%@",[options objectForKey:@"username"]]; tials.password = [NSString stringWithFormat:@"%@",[options objectForKey:@"password"]]; [self.vpnAdapter provideCredentials:tials error:&error]; if(error){ completionHandler(error); return; [self.openVpnReach startTrackingWithCallback:^(OpenVPNReachabilityStatus status) { if(status==OpenVPNReachabilityStatusReachableViaWiFi){ [self.vpnAdapter reconnectAfterTimeInterval:5]; //建立连接并等待。关联事件 self.startHandler = completionHandler; [self.vpnAdapter connect]; -(void)stopTunnelWithReason:(NEProviderStopReason)reason completionHandler:(void (^)(void))completionHandler self.stopHandler = completionHandler; if ([self.openVpnReach isTracking]) { // vpn被主动关闭 [self.openVpnReach stopTracking]; [self.vpnAdapter disconnect]; - (void)openVPNAdapter:(nonnull OpenVPNAdapter *)openVPNAdapter configureTunnelWithNetworkSettings:(nullable NEPacketTunnelNetworkSettings *)networkSettings completionHandler:(nonnull void (^)(NSError * _Nullable))completionHandler { __weak __typeof(self) weak_self = self; [self setTunnelNetworkSettings:networkSettings completionHandler:^(NSError * _Nullable error) { if(!error){ completionHandler(weak_self.packetFlow); - (void)openVPNAdapter:(nonnull OpenVPNAdapter *)openVPNAdapter handleError:(nonnull NSError *)error { BOOL isOpen = (BOOL)[error userInfo][OpenVPNAdapterErrorFatalKey]; NSLog(@"isOpen = %d ",isOpen); if(isOpen){ if (self.openVpnReach.isTracking) { [self.openVpnReach stopTracking]; if (error) self.startHandler(error); self.startHandler = nil; - (void)openVPNAdapter:(nonnull OpenVPNAdapter *)openVPNAdapter handleEvent:(OpenVPNAdapterEvent)event message:(nullable NSString *)message { switch (event) { case OpenVPNAdapterEventConnected: if(self.reasserting){ self.reasserting = false; self.startHandler(nil); self.startHandler = nil; break; case OpenVPNAdapterEventDisconnected: if (self.openVpnReach.isTracking) { [self.openVpnReach stopTracking]; self.stopHandler(); self.stopHandler = nil; break; case OpenVPNAdapterEventReconnecting: self.reasserting = true; break; default: break;

NEPacketTunnelFlow+NEPacketTunnelFlow_Extension.h

// NEPacketTunnelFlow+NEPacketTunnelFlow_Extension.h // PacketTunnel #import <NetworkExtension/NetworkExtension.h> @interface NEPacketTunnelFlow ()<OpenVPNAdapterPacketFlow>
  • 下一步是父项目,分为初始化,建立连接,断开连接,监控状态
  • 初始化,将XXX的配置信息进行保存,这里传的data,大概是如下格式:
  • client
    dev tun
    proto tcp或者udp
    remote ip地址 端口
    resolv-retry infinite
    nobind
    persist-key
    persist-tun
    remote-cert-tls server
    auth SHA512
    cipher AES-256-CBC
    ignore-unknown-option block-outside-dns
    block-outside-dns
    verb 3
    -----BEGIN CERTIFICATE-----
    -----END CERTIFICATE-----
    -----END CERTIFICATE-----
    </cert>
    -----BEGIN PRIVATE KEY-----
    -----END PRIVATE KEY-----
    <tls-crypt>
    -----BEGIN OpenVPN Static key V1-----
    -----END OpenVPN Static key V1-----
    </tls-crypt>

    保存vpn相关的数据

    ///  保存vpn相关的数据
    /// @param data 数据
    -(void)saveVpn:(NSData *)data
        //加载与调用应用程序关联的所有应用程序代理配置,这些配置以前已保存到网络扩展首选项中。
        [NETunnelProviderManager loadAllFromPreferencesWithCompletionHandler:^(NSArray<NETunnelProviderManager *> * _Nullable managers, NSError * _Nullable error) {
            if (error) {
                SSLog(@"Load Error: %@", error.description);
            NETunnelProviderManager *manager;
            if (managers.count > 0) {
                manager = managers[0];
            }else {
                manager = [[NETunnelProviderManager alloc] init];
                manager.protocolConfiguration = [[NETunnelProviderProtocol alloc] init];
            NETunnelProviderProtocol *tunel = [[NETunnelProviderProtocol alloc]init];
            // 获取文件内容
            tunel.providerConfiguration = @{@"ovpn": data};
            // 项目的Identifier
            tunel.providerBundleIdentifier = @"这里是子项目的BundleIdentifier";
            // serverAddress:即在手机设置的vpn中显示的vpn地址(服务器显示)
            tunel.serverAddress = @"openXXX";
    //        tunel.username = @"username";
    //        tunel.identityDataPassword = @"password";
            // 设备进入睡眠,vpn断开连接
            tunel.disconnectOnSleep = YES;
            // 是否可以编辑
            [manager setEnabled:YES];
            // 协议配置
            [manager setProtocolConfiguration:tunel];
            // 包含vpn描述的字符串(类型显示)
            manager.localizedDescription = @"openXXX";
            // 保存信息
            SSLWeakSelf(self);
            [manager saveToPreferencesWithCompletionHandler:^(NSError *error) {
                if(error) {
                    SSLog(@"Save error: %@", error);
                }else {
                    weakself.providerManagers = manager;
                    SSLog(@"add success");
                    //加载与调用应用程序关联的所有应用程序代理配置,这些配置以前已保存到网络扩展首选项中。
                    [manager loadFromPreferencesWithCompletionHandler:^(NSError * _Nullable error) {
                        SSLog(@"loadFromPreferences!");
    
  • 建立隧道,开始连接
  • -(void)connect
        // 连接
        [self.providerManagers loadFromPreferencesWithCompletionHandler:^(NSError * _Nullable error) {
            if(!error){
                NSError *error = nil;
                [self.providerManagers.connection startVPNTunnelWithOptions:nil andReturnError:&error];
                if(error) {
                    SSLog(@"Start error: %@", error.localizedDescription);
                }else{
                    SSLog(@"Connection established!");
    
    -(void)disconnectAction
        // 断开连接
        [self.providerManagers loadFromPreferencesWithCompletionHandler:^(NSError * _Nullable error) {
            [self.providerManagers.connection stopVPNTunnel];
    

    4.监控XXX的状态

    // 添加通知 - 连接信息改变时进行通知
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onVpnStateChange:) name:NEVPNStatusDidChangeNotification object:nil];
    // 通知的方法
    -(void)onVpnStateChange:(NSNotification *)Notification {
        NEVPNStatus status = self.providerManagers.connection.status;
        switch (status) {
            case NEVPNStatusInvalid:
                SSLog(@"连接无效");
                break;
            case NEVPNStatusDisconnected:
                SSLog(@"未连接");
                break;
            case NEVPNStatusConnecting:
                SSLog(@"正在连接");
                break;
            case NEVPNStatusConnected:
                SSLog(@"已连接");
                break;
            case NEVPNStatusDisconnecting:
                SSLog(@"断开连接中...");
                break;
            case NEVPNStatusReasserting:
                SSLog(@"重新连接...");
                break;
            default:
                break;
    

    下面说一下父子项目之间怎么进行通信,其实最基本的方法就是两个项目读取本地保存的文件,需要在开发者中心添加app groups,如下图

    - (void)getRewardTimeFromMain{ self.userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"这里填对应的bundle Indentifier"]; NSString *timeStr = [self.userDefaults objectForKey:@"key"]; // 保存 - (void)rewardTimeToMain:(NSInteger)timeNum{ [self.userDefaults setObject:@"value" forKey:@"key"]; [self.userDefaults synchronize];

    代码实现 - Swift 版

    父项目写了一个类,管理vpn的创建等步骤,代码如下:

    VPNManager.swift
    //  VPNManager.swift
    //  VPNClient
    //  Created by wl on 2021/3/15.
    import Foundation
    import NetworkExtension
    class VPNManager {
        static let shared = VPNManager()
        var manager: NETunnelProviderManager?
        func connect() {
            guard self.manager != nil else {
                return
            self.loadPreferences()
        func disconnect() {
            self.manager?.connection.stopVPNTunnel()
        //加载已保存的NETunnelProvider configurations
        func loadManager() {
            NETunnelProviderManager.loadAllFromPreferences { (managers, error) in
                guard error == nil else {
                    return
                if let manager = managers?.first {
                    self.manager = manager
                } else {
                    self.manager = NETunnelProviderManager()
                    self.manager?.localizedDescription = "myVPN"
                print("VPNManager 初始化完成")
        //加载当前vpn配置
        func loadPreferences() {
            guard let manager = self.manager else {
                return
            self.manager?.loadFromPreferences { (error) in
                guard error == nil else {
                    return
                // 如果没有对应的配置,我们需要新建配置
                if manager.protocolConfiguration == nil {
                    manager.protocolConfiguration = self.newConfiguration()
                // 设置完isEnabled需要保存配置,启动当前配置
                manager.isEnabled = true
                manager.saveToPreferences { (error) in
                    guard error == nil else {
                        // 用户拒绝保存等情况,清空配置
                        manager.protocolConfiguration = nil
                        return
                    // 保存完成后我们需要重新加载配置,进行连接,
                    //https://stackoverflow.com/questions/47550706/error-domain-nevpnerrordomain-code-1-null-while-connecting-vpn-server
                    self.loadPreferencesAndStartTunnel()
        func loadPreferencesAndStartTunnel()  {
            self.manager?.loadFromPreferences(completionHandler: { (error) in
                guard error == nil else {
                    return
                self.startTunnel()
        private func startTunnel() {
                try self.manager?.connection.startVPNTunnel()
            } catch  {
                print(error)
        func newConfiguration() -> NETunnelProviderProtocol {
            //加载ovpn文件
            guard
                let configurationFileURL = Bundle.main.url(forResource: "vpnclient", withExtension: "ovpn"),
                let configurationFileContent = try? Data(contentsOf: configurationFileURL)
            else {
                fatalError()
            let tunnelProtocol = NETunnelProviderProtocol()
            tunnelProtocol.serverAddress = ""
            //指定network extension 确保bundleIdentifier和network extension的id一致
            tunnelProtocol.providerBundleIdentifier = "com.starpavilionlimited.freeouterspace.TargetTunnel"
            tunnelProtocol.providerConfiguration = ["ovpn": configurationFileContent]
            return tunnelProtocol
        private init(){
    

    子项目则是建立隧道用的,代码如下:

    // PacketTunnelProvider.swift // vpn-tunnel import NetworkExtension import UIKit import OpenVPNAdapter extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {} class PacketTunnelProvider: NEPacketTunnelProvider { lazy var vpnAdapter: OpenVPNAdapter = { let adapter = OpenVPNAdapter() adapter.delegate = self return adapter let vpnReachability = OpenVPNReachability() var startHandler: ((Error?) -> Void)? var stopHandler: (() -> Void)? override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) { guard let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol, let providerConfiguration = protocolConfiguration.providerConfiguration else { fatalError() guard let ovpnFileContent: Data = providerConfiguration["ovpn"] as? Data else { fatalError() let configuration = OpenVPNConfiguration() configuration.fileContent = ovpnFileContent // Uncomment this line if you want to keep TUN interface active during pauses or reconnections // configuration.tunPersist = true try vpnAdapter.apply(configuration: configuration) } catch { completionHandler(error) return // Checking reachability. In some cases after switching from cellular to // WiFi the adapter still uses cellular data. Changing reachability forces // reconnection so the adapter will use actual connection. vpnReachability.startTracking { [weak self] status in guard status == .reachableViaWiFi else { return } self?.vpnAdapter.reconnect(afterTimeInterval: 5) // Establish connection and wait for .connected event startHandler = completionHandler // cocoapos 倒入0.8版本就需要换方法了 // vpnAdapter.connect(using: packetFlow) vpnAdapter.connect(); override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { stopHandler = completionHandler if vpnReachability.isTracking { vpnReachability.stopTracking() vpnAdapter.disconnect() extension PacketTunnelProvider: OpenVPNAdapterDelegate { func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?, completionHandler: @escaping (OpenVPNAdapterPacketFlow?) -> Void) { networkSettings?.dnsSettings?.matchDomains = [""] setTunnelNetworkSettings(networkSettings) { error in completionHandler(self.packetFlow); // OpenVPNAdapter calls this delegate method to configure a VPN tunnel. // `completionHandler` callback requires an object conforming to `OpenVPNAdapterPacketFlow` // protocol if the tunnel is configured without errors. Otherwise send nil. // `OpenVPNAdapterPacketFlow` method signatures are similar to `NEPacketTunnelFlow` so // you can just extend that class to adopt `OpenVPNAdapterPacketFlow` protocol and // send `self.packetFlow` to `completionHandler` callback. func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?, completionHandler: @escaping (Error?) -> Void) { // In order to direct all DNS queries first to the VPN DNS servers before the primary DNS servers // send empty string to NEDNSSettings.matchDomains networkSettings?.dnsSettings?.matchDomains = [""] // Set the network settings for the current tunneling session. setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler) // Process events returned by the OpenVPN library func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleEvent event: OpenVPNAdapterEvent, message: String?) { switch event { case .connected: if reasserting { reasserting = false guard let startHandler = startHandler else { return } startHandler(nil) self.startHandler = nil case .disconnected: guard let stopHandler = stopHandler else { return } if vpnReachability.isTracking { vpnReachability.stopTracking() stopHandler() self.stopHandler = nil case .reconnecting: reasserting = true default: break // Handle errors thrown by the OpenVPN library func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) { // Handle only fatal errors guard let fatal = (error as NSError).userInfo[OpenVPNAdapterErrorFatalKey] as? Bool, fatal == true else { return if vpnReachability.isTracking { vpnReachability.stopTracking() if let startHandler = startHandler { startHandler(error) self.startHandler = nil } else { cancelTunnelWithError(error) // Use this method to process any log message returned by OpenVPN library. func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleLogMessage logMessage: String) { // Handle log messages

    控制器视图只有两个按钮,对应着下面代码中的connect和dissconnect,直接上代码:

    // SwiftViewController.swift import UIKit import NetworkExtension class SwiftViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() VPNManager.shared.loadManager() NotificationCenter.default.addObserver(self, selector: #selector(statusChange), name: .NEVPNStatusDidChange, object: nil) @objc func statusChange() { guard let manager = VPNManager.shared.manager else { return switch manager.connection.status { case .connected: print("已连接") case .connecting: print("正在连接") case .disconnected: print("未连接") case .disconnecting: print("正在断开连接") default: print("其他状态") @IBAction func dissconnect(_ sender: UIButton) { VPNManager.shared.disconnect() @IBAction func connect(_ sender: UIButton) { VPNManager.shared.connect()

    附上demo地址,有需要可以下载。
    注意!!!需要自己在开发者中心申请Bundle Identifier,进行替换,项目中有OC和Swift的,在运行时先删除对应的,项目结构如下: