iOS中Network Extension安全科學Tunnel應用(Swift5)

zlyBear發表於2019-04-23

本文簡要介紹iOS中Network Extension在提供安全科學(雙s)使用G高階搜尋、Y視訊等App簡單應用,同時使用了第三方庫NEKit提供路由規則支援。

demo程式碼已適配swift5,點選GitHub連結檢視。

demo執行需要有開發者賬號,修改bundle id,在自己的開發者賬號進行註冊。

在建立應用之前我們需要安裝NEProviderTargetTemplates.pkg,在xcode10.12之後蘋果在xcode中刪除了這個檔案,為什麼?可能和中國區被下架的那些VXN一樣的原因吧。好在我們還可以從老版本的xcode中提取這個檔案,連結在此點選下載 ,提取碼:18ek,要低調,安裝好後重啟xcode。

建立工程

接下來我們開始建立工程,首先和建立普通App一樣建立一個Project

建立Project

建立好後我們在當前Project中建立一個Target

iOS中Network Extension安全科學Tunnel應用(Swift5)

選擇Network Extension,Next

iOS中Network Extension安全科學Tunnel應用(Swift5)

語言選擇swift,因為NEKit是swift寫的,並且對oc支援不是很好,所以這裡就用swift來寫了。

iOS中Network Extension安全科學Tunnel應用(Swift5)

建立好後,目錄下會出現一個新的資料夾

iOS中Network Extension安全科學Tunnel應用(Swift5)

有關代理的程式碼在PacketTunnelProvider.swift中編寫。

在工程建立完畢後,需要在Target的Capabilities中開啟Network Extensions和Personal VXN功能,注意專案本身主Target及PacketTunnel都需要開啟這兩項功能。

iOS中Network Extension安全科學Tunnel應用(Swift5)

好了,到此工程就建立好了。

NEKit整合

由於專案中使用的NEKit這個第三方庫只支援Carthage進行整合管理,所以demo使用的整合工具也是Carthage,沒有用過的可以自行Google,安裝使用難度不高,一看即會。

但是注意在第三方庫編譯的時候需要使用NEKit提供的編譯方式,直接carthage update專案無法執行。

carthage update --no-use-binaries --platform mac,ios

第三方庫編譯好後會在Carthage目錄下Build/iOS中生成.framework檔案,我們需要把這些framework新增到專案中去,下圖的兩個位置都要需要新增,與Net無關的包在packetTunnel中可以不需要新增。

iOS中Network Extension安全科學Tunnel應用(Swift5)

iOS中Network Extension安全科學Tunnel應用(Swift5)

至此相關環境配置就已經搞定了,下面開始看程式碼如何建立VXN連結

建立VXN Manager

首先需要建立一個NETunnelProviderManager

fileprivate func createProviderManager() -> NETunnelProviderManager {
    let manager = NETunnelProviderManager()
    let conf = NETunnelProviderProtocol()
    conf.serverAddress = "BearFree"
    manager.protocolConfiguration = conf
    manager.localizedDescription = "BearFree"
    return manager
}
複製程式碼

將manager儲存至系統中

manager.saveToPreferences{
    error in
        if error != nil{print(error);return;}
        //Todo
}
複製程式碼

執行save方法後會跳轉系統VXN選單,新增我們建立的VPN

iOS中Network Extension安全科學Tunnel應用(Swift5)

此時如果save方法呼叫多次,會出現VPN 1 VPN 2等多個描述檔案 ,因此,蘋果也要求,在建立前應讀取當前的managers

NETunnelProviderManager.loadAllFromPreferencesWithCompletionHandler{ 
    (managers, error) in
    guard let managers = managers else{return}
    let manager: NETunnelProviderManager
    if managers.count > 0 {
        manager = managers[0]
    }else{
        manager = self.createProviderManager()
    }
    // Todo
    // manager.saveToPreferences.......
}
複製程式碼

配置Network Extension

開啟extension中的模板檔案,對應Swift版本與父類修改語法。主要需要以下兩個函式控制VPN狀態

//啟動VPN時呼叫
func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void)

//停止VPN時呼叫
func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void)
複製程式碼

此時我們僅僅需要測試VPN連線是否能正常建立。因此我們只需要在startTunnelWithOptions中呼叫setTunnelNetworkSettings 方法即可。當completionHandler()執行後VPN連線就會顯示在手機上了。

override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
    let ipv4Settings = NEIPv4Settings(addresses: ["192.169.89.1"], subnetMasks: ["255.255.255.0"])
    let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "8.8.8.8")
    networkSettings.mtu = 1500
    networkSettings.iPv4Settings = ipv4Settings
    setTunnelNetworkSettings(networkSettings) {
        error in
        guard error == nil else {
            completionHandler(error)
            return
        }
            completionHandler(nil)
    }
}
複製程式碼

開啟VXN

對剛剛建立的manager呼叫startVPNTunnel方法即可

try manager.connection.startVPNTunnel(options: [:])
複製程式碼

如果在建立VXN saveToPreferences後立刻執行startVPNTunnel,此時可能會出現Domin="null",說明系統並未準備好,所以啟動程式碼放至loadAndCreatePrividerManager方法的返回block中

func connect(){
    self.loadAndCreatePrividerManager { (manager) in
        guard let manager = manager else{return}
        do{
            try manager.connection.startVPNTunnel(options: [:])
        }catch let err{
            print(err)
        }
    }
}
複製程式碼

具體的實現邏輯可以檢視demo中vpnManager.swift檔案。

連線成功後manager.connection.status會發生改變,所以我們需要監聽status值,從而得知目前的vpn連線狀態,用於更新UI顯示。

func addVPNStatusObserver() {
    guard !observerAdded else{
        return
    }
    loadProviderManager { [unowned self] (manager) -> Void in
        if let manager = manager {
            self.observerAdded = true
            NotificationCenter.default.addObserver(forName: NSNotification.Name.NEVPNStatusDidChange, object: manager.connection, queue: OperationQueue.main, using: { [unowned self] (notification) -> Void in
                self.updateVPNStatus(manager)
                })
            }
        }
}
複製程式碼

VXN配置(IP、埠等)

manager通過protocolConfiguration的屬性向Network Extension傳遞配置資訊,在vpnManager中實現如下配置方法,並在啟動vpn前呼叫即可

fileprivate func setRulerConfig(_ manager:NETunnelProviderManager){
    var conf = [String:AnyObject]()
    conf["ss_address"] = ip_address as AnyObject?
    conf["ss_port"] = port as AnyObject?
    conf["ss_method"] = algorithm as AnyObject? // 大寫 沒有橫槓 看Extension中的列舉類設定 否則引發fatal error
    conf["ss_password"] = password as AnyObject?
    conf["ymal_conf"] = getRuleConf() as AnyObject?
    let orignConf = manager.protocolConfiguration as! NETunnelProviderProtocol
    orignConf.providerConfiguration = conf
    manager.protocolConfiguration = orignConf
    print(ip_address,port,algorithm,password)
}
複製程式碼

在Extension中

public var protocolConfiguration: NEVPNProtocol { get }    
複製程式碼

存入了上面寫入的配置資訊,我們可以直接讀取。

關於網路狀態的改變

4G與wifi切換,這裡確實有點坑,在裡面繞了很久,一開始除錯發現vxn是通的,但是過段時間就不能用了,刪了重灌也不行,無意中改了一個extension中的ip又好用了,換了網路環境又不能用了,百思不得其解,debug了半天也沒找到問題,下面先說下怎麼如何監聽網路環境變化:

NEProvider 中存在屬性public var defaultPath: NWPath? { get } 表明當前系統的網路請求路徑。我們可以通過KVO監聽defaultPath的改變。當defaultPath與之前狀態發生改變,且defaultPath.status == .Satisfied時,我們可以認定系統網路進行了切換。此時我們需要重啟VPN及重啟代理伺服器。

重連VPN方法非常簡單,只需要再次呼叫StartVPN函式即可。

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if keyPath == "defaultPath" {
        if self.defaultPath?.status == .satisfied && self.defaultPath != lastPath{
            if(lastPath == nil){
                lastPath = self.defaultPath
            }else{
                NSLog("received network change notifcation")
                DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
                // 延遲1s確保系統就緒
                    guard let strongSelf = self else { return }
                    strongSelf.startTunnel(options: nil){ _ in }
                }
            }
        }else{
            lastPath = defaultPath
        }
    }
}
複製程式碼

那如何在這個Network Extension中debug呢?我們用正常的debug方式,嘗試在當前檔案中加入斷點,發現毫無反應,所以Tunnel的除錯有著單獨的除錯方式,首先我們需要將主程式在裝置上執行,然後通過Xcode->Debug->Attach To Process (有個列表載入過程,需要稍等一下)選擇你的Tunnel名進行debug,debug過程中如果不手動關閉tunnel的除錯模式,會出現vxn連結後秒斷的情況,需要注意一下。下面再來說我是如何解決上面所說的切換網路時出現的問題,在系統設定中找到我們的App,將網路訪問許可權調整為WLAN與蜂窩移動網,至此再切換網路就沒有之前的問題了,為了App在剛啟動時就可以獲取網路許可權,我加了一個無用的網路請求,你也可以用自己的方式來獲取許可權。

iOS中Network Extension安全科學Tunnel應用(Swift5)

demoUI

UI層我直接使用了Eureka這個第三方框架,它是XLForm的swift版本,也是通過carthage整合,想了解的也可以看下demo。

The End

好了,我碰到的問題應該都講清楚了,有問題可以和我交流,如果有不對的地方希望指正,3Q!

參考文件:

NEKit

初探iOS Network Extension

iOS開發實戰-NetworkExtension

相關文章