Подключайтесь к VPN программно в iOS 8

После выпуска бета-версии iOS 8 я обнаружил в своем комплекте инфраструктуру Network Extension, которая позволит разработчикам настраивать и подключаться к VPN-серверам программно и без установки какого-либо профиля.

Каркас содержит основной класс, называемый NEVPNManager. Этот класс также имеет 3 основных метода, которые позволяют мне сохранять, загружать или удалять настройки VPN. Я написал фрагмент кода в методе viewDidLoad следующим образом:

NEVPNManager *manager = [NEVPNManager sharedManager];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(vpnConnectionStatusChanged) name:NEVPNStatusDidChangeNotification object:nil];
[manager loadFromPreferencesWithCompletionHandler:^(NSError *error) {
    if(error) {
        NSLog(@"Load error: %@", error);
    }}];
NEVPNProtocolIPSec *p = [[NEVPNProtocolIPSec alloc] init];
p.username = @“[My username]”;
p.passwordReference = [KeyChainAccess loadDataForServiceNamed:@"VIT"];
p.serverAddress = @“[My Server Address]“;
p.authenticationMethod = NEVPNIKEAuthenticationMethodCertificate;
p.localIdentifier = @“[My Local identifier]”;
p.remoteIdentifier = @“[My Remote identifier]”;
p.useExtendedAuthentication = NO;
p.identityData = [My VPN certification private key];
p.disconnectOnSleep = NO;
[manager setProtocol:p];
[manager setOnDemandEnabled:NO];
[manager setLocalizedDescription:@"VIT VPN"];
NSArray *array = [NSArray new];
[manager setOnDemandRules: array];
NSLog(@"Connection desciption: %@", manager.localizedDescription);
NSLog(@"VPN status:  %i", manager.connection.status);
[manager saveToPreferencesWithCompletionHandler:^(NSError *error) {
   if(error) {
      NSLog(@"Save error: %@", error);
   }
}];

Я также поместил кнопку в моем представлении и установил для ее действия TouchUpInside следующий метод:

- (IBAction)buttonPressed:(id)sender {
   NSError *startError;
   [[NEVPNManager sharedManager].connection startVPNTunnelAndReturnError:&startError];
   if(startError) {
      NSLog(@"Start error: %@", startError.localizedDescription);
   }
}

Здесь есть две проблемы:

1) Когда я пытаюсь сохранить настройки, выдается следующее сообщение об ошибке: Ошибка сохранения: Ошибка Домен = Код NEVPNErrorDomain =4 "Операция не может быть завершена. (Ошибка NEVPNErrorDomain 4.)" Что это за ошибка? Как можно Я решаю эту проблему?

2) [[NEVPNManager sharedManager].connection startVPNTunnelAndReturnError: & startError]; Метод не возвращает никакой ошибки, когда я его вызываю, но состояние соединения меняется на "Отключено" на "Соединение" на мгновение, а затем возвращается в состояние "Отключено".

Любая помощь будет оценена:)

4 ответа

Решение

Проблема заключается в ошибке, которую вы получаете при сохранении:Save error: Error Domain=NEVPNErrorDomain Code=4

Если вы загляните в файл заголовка NEVPNManager.h, вы увидите, что код ошибки 4 - "NEVPNErrorConfigurationStale". Конфигурация устарела и должна быть загружена. Вам следует позвонить loadFromPreferencesWithCompletionHandler: и в обработчике завершения измените значения, которые вы хотите изменить, а затем вызовите saveToPreferencesWithCompletionHandler:, Пример в вашем вопросе - изменение конфигурации до завершения загрузки, поэтому вы получаете эту ошибку.

Больше похоже на это:

[manager loadFromPreferencesWithCompletionHandler:^(NSError *error) {
     // do config stuff
     [manager saveToPreferencesWithCompletionHandler:^(NSError *error) {
     }];
}];

Я опубликовал пост в блоге относительно этого поста. Это полное руководство по управлению VPN-подключением в iOS 8, которое можно найти здесь

Этот ответ будет полезен для тех, кто ищет решение с использованием инфраструктуры расширения сети.

Мое требование состояло в том, чтобы подключить / отключить VPN-сервер по протоколу IKEv2 (конечно, вы можете использовать это решение для IPSec, также изменив конфигурацию протокола vpnManager)

ПРИМЕЧАНИЕ. Если вы ищете протокол L2TP, при использовании расширения сети невозможно подключить VPN-сервер. См. https://forums.developer.apple.com/thread/29909

Вот мой рабочий код:

Объявите VPNManager Object и другие полезные вещи

var vpnManager = NEVPNManager.shared()
var isConnected = false

@IBOutlet weak var switchConntectionStatus: UISwitch!    
@IBOutlet weak var labelConntectionStatus: UILabel!

Добавьте наблюдателя в viewDidLoad для получения VPN Staus и сохраните vpnPassword в связке ключей, вы также можете хранить sharedSecret, для чего вам потребуется протокол IPSec.

override func viewDidLoad() {

    super.viewDidLoad()

    let keychain = KeychainSwift()
    keychain.set("*****", forKey: "vpnPassword")

    NotificationCenter.default.addObserver(self, selector: #selector(ViewController.VPNStatusDidChange(_:)), name: NSNotification.Name.NEVPNStatusDidChange, object: nil)

 }

Теперь в моем приложении был UISwitch для подключения / отключения VPN-сервера.

func switchClicked() {

    switchConntectionStatus.isOn = false

    if !isConnected {
        initVPNTunnelProviderManager()
    }
    else{
        vpnManager.removeFromPreferences(completionHandler: { (error) in

            if((error) != nil) {
                print("VPN Remove Preferences error: 1")
            }
            else {
                self.vpnManager.connection.stopVPNTunnel()
                self.labelConntectionStatus.text = "Disconnected"
                self.switchConntectionStatus.isOn = false
                self.isConnected = false
            }
        })
    }
}

После нажатия на коммутатор инициируйте VPN-туннель, используя приведенный ниже код.

func initVPNTunnelProviderManager(){

    self.vpnManager.loadFromPreferences { (error) -> Void in

        if((error) != nil) {
            print("VPN Preferences error: 1")
        }
        else {

            let p = NEVPNProtocolIKEv2()
// You can change Protocol and credentials as per your protocol i.e IPSec or IKEv2

            p.username = "*****"
            p.remoteIdentifier = "*****"
            p.serverAddress = "*****"

            let keychain = KeychainSwift()
            let data = keychain.getData("vpnPassword")

            p.passwordReference = data
            p.authenticationMethod = NEVPNIKEAuthenticationMethod.none

//          p.sharedSecretReference = KeychainAccess.getData("sharedSecret")! 
// Useful for when you have IPSec Protocol

            p.useExtendedAuthentication = true
            p.disconnectOnSleep = false

            self.vpnManager.protocolConfiguration = p
            self.vpnManager.isEnabled = true

            self.vpnManager.saveToPreferences(completionHandler: { (error) -> Void in
                if((error) != nil) {
                    print("VPN Preferences error: 2")
                }
                else {


                    self.vpnManager.loadFromPreferences(completionHandler: { (error) in

                        if((error) != nil) {

                            print("VPN Preferences error: 2")
                        }
                        else {

                            var startError: NSError?

                            do {
                                try self.vpnManager.connection.startVPNTunnel()
                            }
                            catch let error as NSError {
                                startError = error
                                print(startError)
                            }
                            catch {
                                print("Fatal Error")
                                fatalError()
                            }
                            if((startError) != nil) {
                                print("VPN Preferences error: 3")
                                let alertController = UIAlertController(title: "Oops..", message:
                                    "Something went wrong while connecting to the VPN. Please try again.", preferredStyle: UIAlertControllerStyle.alert)
                                alertController.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.default,handler: nil))

                                self.present(alertController, animated: true, completion: nil)
                                print(startError)
                            }
                            else {
                                self.VPNStatusDidChange(nil)
                                print("VPN started successfully..")
                            }

                        }

                    })

                }
            })
        }
    }
}

После успешного запуска VPN вы можете соответствующим образом изменить статус, например, вызвав VPNStatusDidChange

func VPNStatusDidChange(_ notification: Notification?) {

    print("VPN Status changed:")
    let status = self.vpnManager.connection.status
    switch status {
    case .connecting:
        print("Connecting...")
        self.labelConntectionStatus.text = "Connecting..."
        self.switchConntectionStatus.isOn = false
        self.isConnected = false

        break
    case .connected:
        print("Connected")
        self.labelConntectionStatus.text = "Connected"
        self.switchConntectionStatus.isOn = true
        self.isConnected = true
        break
    case .disconnecting:
        print("Disconnecting...")
        self.labelConntectionStatus.text = "Disconnecting..."
        self.switchConntectionStatus.isOn = false
        self.isConnected = false

        break
    case .disconnected:
        print("Disconnected")
        self.labelConntectionStatus.text = "Disconnected..."
        self.switchConntectionStatus.isOn = false
        self.isConnected = false

        break
    case .invalid:
        print("Invalid")
        self.labelConntectionStatus.text = "Invalid Connection"
        self.switchConntectionStatus.isOn = false
        self.isConnected = false

        break
    case .reasserting:
        print("Reasserting...")
        self.labelConntectionStatus.text = "Reasserting Connection"
        self.switchConntectionStatus.isOn = false
        self.isConnected = false

        break
    }
}

Я сослался отсюда:

/questions/6078191/oshibka-domena-nevpnerrordomain-code1-null-pri-podklyuchenii-k-vpn-serveru/6078204#6078204

https://forums.developer.apple.com/thread/25928

http://blog.moatazthenervous.com/create-a-vpn-connection-with-apple-swift/

Спасибо:)

Я несколько раз тестировал другие решения и заметил, что

Положив saveToPreferences в loadFromPreferences это не решение !!


Предположим, мы уже загрузили экземпляр менеджера (или создали новый без загрузки), вызов save внутри обработчика завершения загрузки был для меня случайным (иногда), и, имея мой возраст опыта отладки, казалось, что iOS просто нужно время (что-то обработать ?!).

Итак, ниже всегда работает, а не случайным образом:

      guard let manager = self.manager else { return }

DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
    // Change some settings.
    manager.isEnabled = true
    // Save changes directly (without another load).
    manager.saveToPreferences(completionHandler: { [weak self] (error) in
        if let error = error {
            manager.isEnabled = false;
            print("Failed to enable - \(error)")
            return
        }
        // Establishing tunnel really needs reload.
        manager.loadFromPreferences(completionHandler: { [weak self] (error) in
            if let error = error {
                print("Failed to reload - \(error)")
                return
            }
            let session = (manager.connection as! NETunnelProviderSession);
            session.startVPNTunnel()
        });
    });
}

Обратите внимание, что я удалил загрузку из начала, указанного выше, и загрузка требуется только для запуска, поэтому при использовании вышеуказанной логики загрузка требуется ТОЛЬКО, если у вас еще нет экземпляра (или если он был изменен).

Другие вопросы по тегам