ios OpenVPN 使用

发表:2018-11-12 11:43:04

实现方式

NetWorkExtension + OpenVPNAdapter.

NetWorkExtension主要帮助我们完成VPN的配置与获取配置信息。OpenVPNAdapter帮我们建立连接。

NetWorkExtension

NetWorkExtension App拓展。

创建NetWorkExtension

  1. 创建NetWorkExtension target。

  2. 开启相关权限。

  3. 配置VPN到手机。

NetWorkExtension Target创建
61C716C6-2116-405C-843D-C4ADA12F1D52.png
B9597FB0-4976-4704-95C8-88D631EAB7E7.png
11B9489C-91F6-4339-A8DC-74106AA03026.png
权限配置

权限配置需要在宿主app与app extension都配置完成。

D4159E5A-B09C-48AA-AA56-949C43338CD0.png

权限配置完毕后会在鉴权文件中显示。


8E5AA5B6-187E-4CE2-9540-177E333D2B83.png

如若涉及到宿主app与app extension之间的通讯,利用app groups。同样宿主app、app extension都需要打开并配置相同的app group


9D017D88-7D2F-475B-90F3-6B13A4E3B45D.png
配置VPN到手机

宿主app引入NetWorkExtension框架,通过NetWorkExtension框架下提供的API  将openVPN的配置信息配置到手机。

 func confingVPN()  {        //获取VPNManager        NETunnelProviderManager.loadAllFromPreferences { (managers, error) in            guard error == nilelse {                // Handle an occured error                print("loadAllFromPreferences error (String(describing: error))")                return            }                        self.providerManager = managers?.first ?? NETunnelProviderManager()                                    //配置VPNself.providerManager?.loadFromPreferences(completionHandler: { (error) in                guard error == nilelse {                    // Handle an occured error                    print("loadFromPreferences error")                    return                }                                // Assuming the app bundle contains a configuration file named 'client.ovpn' lets get its// Data representation                                guard                    let configurationFileURL = Bundle.main.url(forResource: "client2", withExtension: "ovpn"),                    let configurationFileContent = try? Data(contentsOf: configurationFileURL)                    else {                        fatalError()                }                                let tunnelProtocol = NETunnelProviderProtocol()                                // If the ovpn file doesn't contain server address you can use this property// to provide it. Or just set an empty string value because `serverAddress`// property must be set to a non-nil string in either case.                tunnelProtocol.serverAddress = "223.100.8.226 11194"// The most important field which MUST be the bundle ID of our custom network// extension target.                tunnelProtocol.providerBundleIdentifier = "app extension的bundle identifier "// Use `providerConfiguration` to save content of the ovpn file.                tunnelProtocol.providerConfiguration = ["ovpn": configurationFileContent]                                // Provide user credentials if needed. It is highly recommended to use// keychain to store a password.//                tunnelProtocol.username = "username"//                tunnelProtocol.passwordReference = Data()  // A persistent keychain reference to an item containing the password// Finish configuration by assigning tunnel protocol to `protocolConfiguration`// property of `providerManager` and by setting description.self.providerManager?.protocolConfiguration = tunnelProtocol                self.providerManager?.localizedDescription = "Fch OpenVPN Client"self.providerManager?.isEnabled = true// Save configuration in the Network Extension preferencesself.providerManager?.saveToPreferences(completionHandler: { (error) inif let error = error  {                        // Handle an occured error                        print("saveToPreferences error (String(describing: error)) ")                    }                })//                self.providerManager?.removeFromPreferences(completionHandler: { (error) in//                    //                })                                           })                    }            }

有几点很重要

  • tunnelProtocol.providerBundleIdentifier 为你创建的appextension的bundle Identifier. 宿主app会通过这个将我们的配置信息传递到appextension。

  • tunnelProtocol.providerConfiguration字典文件中key值会在app extension中作为获取配置文件的key使用,需要前后保持一致。

OpenVPNAdapter配置文件config.ovpn

配置过程会打开手机设置来完成,需要进行指纹验证。成功后会可手机设配置查看相关配置信息。


IMG_7901.PNG

IMG_7902.PNG
IMG_7903.PNG

完成配置后使用NetWorkExtension下的API控制VPN的开启。
开启后建立连接的过程会在app extension之中利用OpenVPNAdapter完成。

@objc func startVPN()  {                                self.providerManager?.loadFromPreferences(completionHandler: { (error) in            guard error == nil else {                // Handle an occured errorprint("loadFromPreferences error (String(describing: error))")                return            }                        do {                tryself.providerManager?.connection.startVPNTunnel()                                self .addVPNStatusObserver();                print("startVPNTunnel state (String(describing: self.providerManager?.connection.status))")                            } catch {                // Handle an occured errorprint("startVPNTunnel error (String(describing: error))")            }        })                                   }    

OpenVPNAdapter

集成

使用carthage将OpenVPNAdapter集成到自己项目当中。

建立连接

按照OpenVPNAdapter提供的代码即可。

enum PacketTunnelProviderError: Error {    case fatalError(message: String)}@available(iOSApplicationExtension 9.0, *)class PacketTunnelProvider: NEPacketTunnelProvider {            lazy var vpnAdapter: OpenVPNAdapter = {        let adapter = OpenVPNAdapter()        adapter.delegate = selfreturn adapter    }()        let vpnReachability = OpenVPNReachability()        var startHandler: ((Error?) -> Void)?    var stopHandler: (() -> Void)?        override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {                // There are many ways to provide OpenVPN settings to the tunnel provider. For instance,// you can use `options` argument of `startTunnel(options:completionHandler:)` method or get// settings from `protocolConfiguration.providerConfiguration` property of `NEPacketTunnelProvider`// class. Also you may provide just content of a ovpn file or use key:value pairs// that may be provided exclusively or in addition to file content.// In our case we need providerConfiguration dictionary to retrieve content// of the OpenVPN configuration file. Other options related to the tunnel// provider also can be stored there.        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//        configuration.settings = [//        ]//              configuration.keyDirection = 1;                // Apply OpenVPN configuration        let properties: OpenVPNProperties        do {            properties = try vpnAdapter.apply(configuration: configuration)        } catch {            completionHandler(error)            return        }                // Provide credentials if neededif !properties.autologin {            // If your VPN configuration requires user credentials you can provide them by// `protocolConfiguration.username` and `protocolConfiguration.passwordReference`// properties. It is recommended to use persistent keychain reference to a keychain// item containing the password.            guard let username: String = protocolConfiguration.username else {                fatalError()            }            // Retrieve a password from the keychain//            guard let password: String = ... {//                fatalError()//            }            let credentials = OpenVPNCredentials()            credentials.username = username//            credentials.password = passworddo {                try vpnAdapter.provide(credentials: credentials)            } 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 { [weakself] status in            guard status != .notReachable else { return }            self?.vpnAdapter.reconnect(afterTimeInterval: 5)        }                // Establish connection and wait for .connected event        startHandler = completionHandler        vpnAdapter.connect()    }        override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {        stopHandler = completionHandler                if vpnReachability.isTracking {            vpnReachability.stopTracking()        }                vpnAdapter.disconnect()    }    }@available(iOSApplicationExtension 9.0, *)extension PacketTunnelProvider: OpenVPNAdapterDelegate {        // 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 (OpenVPNAdapterPacketFlow?) -> Void) {        setTunnelNetworkSettings(networkSettings) { (error) in            completionHandler(error == nil ? self.packetFlow : nil)        }    }            // 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 = nilcase .disconnected:            guard let stopHandler = stopHandler else { return }                        if vpnReachability.isTracking {                vpnReachability.stopTracking()            }                        stopHandler()            self.stopHandler = nilcase .reconnecting:            reasserting = truedefault:            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 == trueelse {            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        print("handleLogMessage (logMessage)")        NSLog("handleLogMessage (logMessage)")    }    //    Printing description of logMessage://    "Transport Error: Transport error on '223.100.8.226: NETWORK_EOF_ERRORn"//    Printing description of error://    Error Domain=me.ss-abramchuk.openvpn-adapter.error-domain Code=26 "OpenVPN error occured" UserInfo={NSLocalizedFailureReason=General transport error, me.ss-abramchuk.openvpn-adapter.error-key.message=Transport error on '223.100.8.226: NETWORK_EOF_ERROR, me.ss-abramchuk.openvpn-adapter.error-key.fatal=false, NSLocalizedDescription=OpenVPN error occured}   }

整个过程大致为:通过我们在宿主app中设置的key获取到配置信息,利用OpenVPNAdapter读取配置信息,并建立连接。

建立成功后手机状态栏会显示出VPN的标志。


56B86A1C-870C-4E78-93CF-27E72152D277.png
配置文件

配置文件在OpenVPNAdapter中支持两种方式
健值对方式

remote 223.100.8.226 11194

标签方式

<ca></ca>

config文件配置基本两种方式

  • 用户名密码验证方式。

  • 证书验证方式。

根据实际情况大致配置如下

client#路由模式devtun #改为tcpprototcp#OpenVPN服务器的外网IP和端口remotexxx.xxx.x.xxxxxxxxresolv-retryinfinitenobindpersist-keypersist-tun#caca.crt#certtest1.crt#keytest1.keyns-cert-typeserver#tls-authta.key 1comp-lzoverb 3#密码认证相关#auth-user-pass

通常情况下这是一种比较标准常见的配置文件。但是在OpenVPNAdapter可能会存在问题。OpenVPNAdapter并不能完善的支持所有标签,导致我们在建立连接过程中出现很多问题。详细参考 常见错误。

参考资料
openVPN的客户端的client.ovpn配置.

常见错误

(Error) error = <variable not available>变量不支持。
.ovpn中tls-auth变量导致的OpenVPNAdapter并不支持健值对这种方法

tls-authta.key 1

改为

<tls-auth>-----BEGIN OpenVPN Static key V1-----···从你的ta.key中复制过来-----END OpenVPN Static key V1-----</tls-auth>

Error Domain=me.ss-abramchuk.openvpn-adapter.error-domain Code=67 "Failed to establish connection with OpenVPN server"建立连接失败
原因很多种,例如:

066D286D-3B51-49B4-A4BE-2AA9081A3E7E.png

ca 证书文件格式不正确。因为OpenVPNAdapter认为我们我们配置的ca ca.crt中ca.crt为我们的证书文件内容。但实际上它是一个证书文件的路径。所以我们也适用标签方式配置
<ca>....</ca>

同理

#caca.crt#certtest1.crt#keytest1.key#tls-authta.key 1

都使用标签方式进行配置。

"UNUSED OPTIONSn4 [resolv-retry] [infinite] n5 [nobind] n6 [persist-key] n7 [persist-tun] n10 [verb] [3] n11 [key-deriction] [1] nn"

317545E1-8DE2-4298-8FA1-867AC78544B4.png

有几个标签在没有用,应该是能够识别这些标签但是无法使用。
其中key-deriction比较重要,所以我们在代码中配置

configuration.keyDirection = 1;

"TCP recv EOFn"TCP EOF错误
Transport error on '223.100.8.226: NETWORK_EOF_ERROR

3937EB0C-EA6A-404A-92D6-935F8EAF4935.png

由于key-deriction无法使用导致。在代码中配置后解决。



作者:february29
链接:https://www.jianshu.com/p/66039ea97656
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。


相关文章