iOS开发之常用技术点

ios OpenVPN 使用

2018-09-04  本文已影响703人  february29

实现方式

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
//                    
//                })
               
                
            })
            
        }
        
    }

有几点很重要

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无法使用导致。在代码中配置后解决。

上一篇下一篇

猜你喜欢

热点阅读