Membuat klien OpenVPN untuk iOS

Halo semua!

Mari kita lihat cara membuat aplikasi Anda sendiri yang mendukung protokol OpenVPN. Bagi mereka yang mendengar tentang ini untuk pertama kalinya, tautan ke materi ulasan, selain Wikipedia, diberikan di bawah ini.



Di mana untuk memulai?



Mari kita mulai dengan kerangka kerja OpenVPNAdapter - ditulis dalam Objective-C, diinstal menggunakan Pods, Carthage, SPM. Versi OS minimum yang didukung adalah 9.0.

Setelah instalasi, perlu menambahkan Ekstensi Jaringan untuk target aplikasi utama, dalam hal ini kita memerlukan opsi terowongan Paket untuk saat ini.



gambar



Ekstensi jaringan



Kemudian kami menambahkan target baru - Ekstensi Jaringan.

Kelas PacketTunnelProvider yang dihasilkan setelah ini akan dikonversi ke bentuk berikut:



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

}

      
      







Dan lagi kodenya



Kami kembali ke aplikasi utama. Kita perlu bekerja dengan NetworkExtension setelah mengimpornya. Biarkan saya menarik perhatian Anda ke kelas NETunnelProviderManager , yang dengannya Anda dapat mengelola koneksi VPN, dan NETunnelProviderProtocol , yang menetapkan parameter untuk koneksi baru. Selain mentransfer konfigurasi OpenVPN, kami mengatur kemampuan untuk mentransfer login dan kata sandi jika perlu.



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

      
      







Akibatnya, sistem akan meminta izin kepada pengguna untuk menambahkan konfigurasi baru, di mana Anda harus memasukkan kata sandi dari perangkat, setelah itu koneksi akan muncul di Pengaturan di sebelah yang lain.



gambar



Mari tambahkan kemampuan untuk mematikan koneksi VPN.



do {
            try providerManager?.connection.stopVPNTunnel()
            completion()
        } catch let error {
            print(error.localizedDescription)
        }

      
      







Anda juga dapat memutuskan koneksi menggunakan metode removeFromPreferences (completionHandler :) , tetapi ini terlalu radikal dan dimaksudkan untuk pembongkaran terakhir dan tidak dapat diubah dari data koneksi yang diunduh :) Anda dapat



memeriksa status koneksi VPN Anda di aplikasi menggunakan status .



if providerManager.connection.status == .connected {
      defaults.set(true, forKey: "serverIsOn")
}

      
      







Ada 6 status ini.



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

      
      







Kode ini memungkinkan Anda membangun aplikasi dengan fungsionalitas minimum yang diperlukan. Lebih baik menyimpan sendiri konfigurasi OpenVPN dalam file terpisah, yang dapat diakses untuk dibaca.



Tautan yang berguna:

OpenVPNAdapter

Habr Test Configs




All Articles