Hacer un cliente OpenVPN para iOS

¡Hola todos!

Echemos un vistazo a cómo crear su propia aplicación que admita el protocolo OpenVPN. Para aquellos que escuchan sobre esto por primera vez, a continuación se proporcionan enlaces para revisar materiales, además de Wikipedia.



¿Dónde empezar?



Comencemos con el marco OpenVPNAdapter, escrito en Objective-C, instalado usando Pods, Carthage, SPM. La versión mínima del sistema operativo compatible es 9.0.

Después de la instalación, será necesario agregar extensiones de red para el destino de la aplicación principal, en este caso necesitaremos la opción de túnel de paquetes por ahora.



imagen



Extensión de la red



Luego agregamos un nuevo objetivo: Extensión de red.

La clase PacketTunnelProvider generada después de esto se convertirá a la siguiente forma:



import NetworkExtension
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 ovpnContent = providerConfiguration["ovpn"] as? String else {
            fatalError()
        }

        let configuration = OpenVPNConfiguration()
        configuration.fileContent = ovpnContent.data(using: .utf8)
        configuration.settings = [:]

        configuration.tunPersist = true

        let evaluation: OpenVPNConfigurationEvaluation
        do {
            evaluation = try vpnAdapter.apply(configuration: configuration)
        } catch {
            completionHandler(error)
            return
        }

        if !evaluation.autologin {
            guard let username: String = protocolConfiguration.username else {
                fatalError()
            }

            guard let password: String = providerConfiguration["password"] as? String else {
                fatalError()
            }

            let credentials = OpenVPNCredentials()
            credentials.username = username
            credentials.password = password

            do {
                try vpnAdapter.provide(credentials: credentials)
            } catch {
                completionHandler(error)
                return
            }
        }

        vpnReachability.startTracking { [weak self] status in
            guard status == .reachableViaWiFi else { return }
            self?.vpnAdapter.reconnect(afterTimeInterval: 5)
        }

        startHandler = completionHandler
        vpnAdapter.connect(using: packetFlow)
    }

    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 (Error?) -> Void) {
        networkSettings?.dnsSettings?.matchDomains = [""]

        setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler)
    }

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

    func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) {
        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)
        }
    }

    func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleLogMessage logMessage: String) {
    }

}

      
      







Y de nuevo el codigo



Volvemos a la aplicación principal. Necesitamos trabajar con NetworkExtension después de importarlo. Permítanme llamar su atención sobre las clases NETunnelProviderManager , con las que puede administrar la conexión VPN, y NETunnelProviderProtocol , que establece los parámetros para la nueva conexión. Además de transferir la configuración de OpenVPN, configuramos la capacidad de transferir el nombre de usuario y la contraseña si es necesario.



var providerManager: NETunnelProviderManager!

    override func viewDidLoad() {
        super.viewDidLoad()
        loadProviderManager {
            self.configureVPN(serverAddress: "127.0.0.1", username: "", password: "")
        }
     }

    func loadProviderManager(completion:@escaping () -> Void) {
       NETunnelProviderManager.loadAllFromPreferences { (managers, error) in
           if error == nil {
               self.providerManager = managers?.first ?? NETunnelProviderManager()
               completion()
           }
       }
    }

    func configureVPN(serverAddress: String, username: String, password: String) {
      providerManager?.loadFromPreferences { error in
         if error == nil {
            let tunnelProtocol = NETunnelProviderProtocol()
            tunnelProtocol.username = username
            tunnelProtocol.serverAddress = serverAddress
            tunnelProtocol.providerBundleIdentifier = "com.myBundle.myApp" 
            tunnelProtocol.providerConfiguration = ["ovpn": configData, "username": username, "password": password]
            tunnelProtocol.disconnectOnSleep = false
            self.providerManager.protocolConfiguration = tunnelProtocol
            self.providerManager.localizedDescription = "Light VPN"
            self.providerManager.isEnabled = true
            self.providerManager.saveToPreferences(completionHandler: { (error) in
                  if error == nil  {
                     self.providerManager.loadFromPreferences(completionHandler: { (error) in
                         do {
                           try self.providerManager.connection.startVPNTunnel()
                         } catch let error {
                             print(error.localizedDescription)
                         }                                              
                     })
                  }
            })
          }
       }
    }

      
      







Como resultado, el sistema le pedirá permiso al usuario para agregar una nueva configuración, para lo cual deberá ingresar la contraseña del dispositivo, luego de lo cual la conexión aparecerá en Configuración junto a otras.



imagen



Agreguemos la capacidad de apagar la conexión VPN.



do {
            try providerManager?.connection.stopVPNTunnel()
            completion()
        } catch let error {
            print(error.localizedDescription)
        }

      
      







También puede desconectar la conexión usando el método removeFromPreferences (CompletionHandler :) , pero esto es demasiado radical y está destinado a la demolición final e irreversible de los datos de conexión descargados :) Puede



verificar el estado de la conexión de su VPN en la aplicación usando estados .



if providerManager.connection.status == .connected {
      defaults.set(true, forKey: "serverIsOn")
}

      
      







Hay 6 de estos estados.



@available(iOS 8.0, *)
public enum NEVPNStatus : Int {

    /** @const NEVPNStatusInvalid The VPN is not configured. */
    case invalid = 0

    /** @const NEVPNStatusDisconnected The VPN is disconnected. */
    case disconnected = 1

    /** @const NEVPNStatusConnecting The VPN is connecting. */
    case connecting = 2

    /** @const NEVPNStatusConnected The VPN is connected. */
    case connected = 3

    /** @const NEVPNStatusReasserting The VPN is reconnecting following loss of underlying network connectivity. */
    case reasserting = 4

    /** @const NEVPNStatusDisconnecting The VPN is disconnecting. */
    case disconnecting = 5
}

      
      







Este código le permite crear una aplicación con la funcionalidad mínima requerida. Es mejor almacenar las configuraciones de OpenVPN en un archivo separado, al que se puede acceder para leer.



Enlaces útiles:

OpenVPNAdapter

Habr

Test Configs



All Articles