Давайте рассмотрим как создать собственное приложение, поддерживающее OpenVPN-протокол. Для тех, кто об этом слышит впервые ссылки на обзорные материалы, помимо Википедии, приведены ниже.
С чего начать?
Начнем с фреймворка OpenVPNAdapter — написан на Objective-C, ставится с помощью Pods, Carthage, SPM. Минимальная поддерживаемая версия ОС — 9.0.
После установки необходимо будет добавить Network Extensions для таргета основного приложения, в данном случае нам понадобится пока Packet tunnel опция.

Network Extension
Затем добавляем новый таргет — Network Extension.
Сгенерированный после этого класс PacketTunnelProvider приведем к следующему виду:
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) { } }
И снова код
Возвращаемся к основному приложению. Нам необходимо работать с NetworkExtension, предварительно импортировав его. Обращу внимание на классы NETunnelProviderManager, с помощью которого можно управлять VPN-соединением, и NETunnelProviderProtocol, задающий параметры новому соединению. Помимо передачи конфига OpenVPN, задаем возможность передать логин и пароль в случае необходимости.
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) } }) } }) } } }
В результате система запросит у пользователя разрешение на добавление новой конфигурации, для чего придется ввести пароль от девайса, после чего соединение появится в Настройках по соседству с другими.

Добавим возможность выключения VPN-соединения.
do { try providerManager?.connection.stopVPNTunnel() completion() } catch let error { print(error.localizedDescription) }
Можно также отключать соединение с помощью метода removeFromPreferences(completionHandler:), но это слишком радикально и предназначено для окончательного и бесповоротного сноса загруженных данных о соединении:)
Проверять статус подключения Вашего VPN в приложении можно с помощью статусов.
if providerManager.connection.status == .connected { defaults.set(true, forKey: "serverIsOn") }
Всего этих статусов 6.
@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 }
Данный код позволяет собрать приложение с минимальным требуемым функционалом. Сами конфиги OpenVPN-а лучше все же хранить в отдельном файле, обращаться к которому можно будет для чтения.
Полезные ссылки:
OpenVPNAdapter
Habr
Конфиги для теста
ссылка на оригинал статьи https://habr.com/ru/post/562060/
Добавить комментарий