Voyons comment créer votre propre application prenant en charge le protocole OpenVPN. Pour ceux qui entendent parler de cela pour la première fois, des liens vers des documents de révision, en plus de Wikipédia, sont fournis ci-dessous.
Où commencer?
Commençons par le framework OpenVPNAdapter - écrit en Objective-C, installé à l'aide de Pods, Carthage, SPM. La version minimale du système d'exploitation prise en charge est 9.0.
Après l'installation, il sera nécessaire d'ajouter des extensions réseau pour la cible de l'application principale, dans ce cas nous aurons besoin de l'option Tunnel de paquets pour le moment.

Extension de réseau
Ensuite, nous ajoutons une nouvelle cible - Network Extension.
La classe PacketTunnelProvider générée après cela sera convertie sous la forme suivante :
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) {
}
}
Et encore le code
Nous revenons à l'application principale. Nous devons travailler avec NetworkExtension après l'avoir importé. Permettez-moi d' attirer votre attention sur les classes NETunnelProviderManager , avec lesquelles vous pouvez gérer la connexion VPN, et le NETunnelProviderProtocol , qui définit les paramètres de la nouvelle connexion. En plus du transfert de la configuration OpenVPN, nous définissons la possibilité de transférer le login et le mot de passe si nécessaire.
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)
}
})
}
})
}
}
}
En conséquence, le système demandera à l'utilisateur la permission d'ajouter une nouvelle configuration, pour laquelle vous devrez entrer le mot de passe de l'appareil, après quoi la connexion apparaîtra dans Paramètres à côté des autres.

Ajoutons la possibilité de désactiver la connexion VPN.
do {
try providerManager?.connection.stopVPNTunnel()
completion()
} catch let error {
print(error.localizedDescription)
}
Vous pouvez également déconnecter la connexion en utilisant la méthode removeFromPreferences (completionHandler :) , mais celle-ci est trop radicale et est destinée à la démolition définitive et irréversible des données de connexion téléchargées :) Vous pouvez
vérifier l'état de connexion de votre VPN dans l'application à l'aide de status .
if providerManager.connection.status == .connected {
defaults.set(true, forKey: "serverIsOn")
}
Il y a 6 de ces statuts.
@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
}
Ce code vous permet de créer une application avec le minimum de fonctionnalités requises. Il est préférable de stocker les configurations OpenVPN elles-mêmes dans un fichier séparé, accessible en lecture.
Liens utiles :
OpenVPNAdapter
Habr
Test Configs