[Разработка под iOS, Swift] Делаем OpenVPN клиент для iOS

Автор Сообщение
news_bot ®

Стаж: 6 лет 3 месяца
Сообщений: 27286

Создавать темы news_bot ® написал(а)
10-Июн-2021 04:44

Привет всем!
Давайте рассмотрим как создать собственное приложение, поддерживающее 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
Конфиги для теста
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_razrabotka_pod_ios (Разработка под iOS), #_swift, #_swift, #_vpn, #_openvpn, #_ios, #_razrabotka_pod_ios (
Разработка под iOS
)
, #_swift
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 11-Май 09:56
Часовой пояс: UTC + 5