ios OpenVPN 使用

作者: february29 | 来源:发表于2018-09-04 17:31 被阅读703次

    实现方式

    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 == nil else {
                    // Handle an occured error
                    print("loadAllFromPreferences error \(String(describing: error))")
                    return
                }
                
                self.providerManager = managers?.first ?? NETunnelProviderManager()
                
                
                //配置VPN
                self.providerManager?.loadFromPreferences(completionHandler: { (error) in
                    guard error == nil else {
                        // 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 preferences
                    self.providerManager?.saveToPreferences(completionHandler: { (error) in
                        if 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 error
                    print("loadFromPreferences error \(String(describing: error))")
                    return
                }
                
                do {
                    try self.providerManager?.connection.startVPNTunnel()
                    
                    self .addVPNStatusObserver();
                    print("startVPNTunnel state \(String(describing: self.providerManager?.connection.status))")
                    
                } catch {
                    // Handle an occured error
                    print("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 = self
            
            return 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 needed
            if !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 = password
    
                do {
                    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 { [weak self] 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 = 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
            print("handleLogMessage \(logMessage)")
            NSLog("handleLogMessage \(logMessage)")
        }
    
        
    //    Printing description of logMessage:
    //    "Transport Error: Transport error on \'223.100.8.226: NETWORK_EOF_ERROR\n"
    //    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
    #路由模式
    dev tun 
    #改为tcp
    proto tcp
    #OpenVPN服务器的外网IP和端口
    remote xxx.xxx.x.xxx xxxxx
    resolv-retry infinite
    nobind
    persist-key
    persist-tun
    #ca ca.crt
    #cert test1.crt
    #key test1.key
    ns-cert-type server
    #tls-auth ta.key 1
    comp-lzo
    verb 3
    #密码认证相关
    #auth-user-pass
    
    

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

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

    常见错误

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

    tls-auth ta.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>
    

    同理

    #ca ca.crt 
    #cert test1.crt
    #key test1.key
    #tls-auth ta.key 1
    

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

    "UNUSED OPTIONS\n4 [resolv-retry] [infinite] \n5 [nobind] \n6 [persist-key] \n7 [persist-tun] \n10 [verb] [3] \n11 [key-deriction] [1] \n\n"

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

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

    configuration.keyDirection = 1;
    

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

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

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

    相关文章

      网友评论

      • rockyMJ:大神,我想知道这样方式的VPN连接,可以设置某些APP使用此VPN,某些APP不使用此VPN吗?
        february29:VPN连接是系统级的,通道建立成功后所有的网络通讯都通过这个通道。手机客户端所做的只是建立通道,无法设置某些app使用某些不使用(毕竟手机客户端并不知道你手机上存在哪些app)。后续系统是否会添加相应的功能就要看Apple的心情了。
        如果要实现过滤请求的功能倒是可以在服务端来做(具体工作量,难易程度不清楚)。

      本文标题:ios OpenVPN 使用

      本文链接:https://www.haomeiwen.com/subject/qdrklftx.html