iOS - NetworkExtension 建立隧道(OpenXXX)

注意:由于簡大叔對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'
end
end
end
end
target 'TargetTunnel' do
use_frameworks!
pod 'OpenVPNAdapter', :git => 'https://github.com/ss-abramchuk/OpenVPNAdapter.git', :tag => '0.4.0'
end

1、創建Target


創建Target.png

選擇Network.png

2、創建完成后需要將主項目和子項目的Bundle Identifier進行替換,這里填寫的是開發者中心創建好的Bundle Identifier,填寫完成后父子項目添加相應的Capability,如下圖


添加Capability.png

代碼實現 - OC 版

  • 首先是子項目,也就是新創建的Target

PacketTunnelProvider.h

//
//  PacketTunnelProvider.h
//  TargetTunnel
//
//
//

@import NetworkExtension;
@import OpenVPNAdapter;

NS_ASSUME_NONNULL_BEGIN

@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;

@end

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;

@end

@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;
    }
}

@end

NEPacketTunnelFlow+NEPacketTunnelFlow_Extension.h

//
//  NEPacketTunnelFlow+NEPacketTunnelFlow_Extension.h
//  PacketTunnel
//
#import <NetworkExtension/NetworkExtension.h>

@interface NEPacketTunnelFlow ()<OpenVPNAdapterPacketFlow>

@end
  • 下一步是父項目,分為初始化,建立連接,斷開連接,監控狀態
  1. 初始化,將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
<ca>
-----BEGIN CERTIFICATE-----
密鑰
-----END CERTIFICATE-----
</ca>
<cert>
密鑰
-----END CERTIFICATE-----
</cert>
<key>
-----BEGIN PRIVATE KEY-----
密鑰
-----END PRIVATE KEY-----
</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!");

                }];
            }
        }];
 
    }];
}
  1. 建立隧道,開始連接
-(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!");
            }
        }
    }];
}
  1. 斷開連接
-(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,如下圖


添加groups

添加通信,這里只是舉了一個例子,也可以自行百度(多Target之間goup通信)

#pragma mark - NSUserDefaults,進行通信

// 獲取
- (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() {
        do {
            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

        do {
            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的,在運行時先刪除對應的,項目結構如下:

項目結構

到此基本就完成了,這里提供一些參考連接以供使用,都是干貨

參考連接1
參考連接2
參考連接3
參考連接4
參考連接5

我自己使用的是OC,但是在子Target中,用了Swift。具體為什么用這個,打個啞謎,你們自己試一試就知道了。

哦了,就這么多,有問題請指出。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容