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

Kate

Administrator
Команда форума
Привет всем!
Давайте рассмотрим как создать собственное приложение, поддерживающее 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:mad: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/
 
Сверху