Делаем OpenVPN клиент для iOS

от автора

Привет всем!
Давайте рассмотрим как создать собственное приложение, поддерживающее OpenVPN-протокол. Для тех, кто об этом слышит впервые ссылки на обзорные материалы, помимо Википедии, приведены ниже.

С чего начать?

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

image

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

В результате система запросит у пользователя разрешение на добавление новой конфигурации, для чего придется ввести пароль от девайса, после чего соединение появится в Настройках по соседству с другими.

image

Добавим возможность выключения 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *