Connect to VPN programmatically in iOS 8
Asked Answered
K

3

26

Since the release of iOS 8 beta, I found a Network Extension framework in its bundle which is going to let developers configure and connect to VPN servers programmatically and without any profile installation.

The framework contains a major class called NEVPNManager. This class also has 3 main methods that let me save, load or remove VPN preferences. I’ve written a piece of code in viewDidLoad method as following:

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

I also placed a button in my view and set its TouchUpInside action to the method below:

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

There are two problems here:

1) When I try to save the preferences, the following error will be thrown: 
Save error: Error Domain=NEVPNErrorDomain Code=4 "The operation couldn’t be completed. (NEVPNErrorDomain error 4.)”
What is this error? How can I solve this issue?

2) [[NEVPNManager sharedManager].connection startVPNTunnelAndReturnError:&startError]; method doesn’t return any error when I call it but the connection status changes from Disconnected to Connecting for just a moment and then it gets back to Disconnected state.

Any help will be appreciated :)

Keel answered 26/7, 2014 at 10:57 Comment(5)
weird, when I am running a copy of your code on a device with beta 4 I'm getting a nil returned from [NEVPNManager sharedManager];Gesner
Ah, I had not added the "Personal VPN" entitlement to my App ID or entitlements file. To do so, go to "Capabilities" under the projectGesner
How are you retrieving the password from the keychain? I've stored the password for my account in the keychain, and I have tried retrieving the persistent reference (which the docs imply are necessary) into the protocol.passwordReference field but it still prompts me for a password when I go to connect to the VPN service (once I enter it, I connect, so everything else seems good).Gesner
Use of the NEVPNManager class requires the com.apple.developer.networking.vpn.api entitlement. You can get this entitlement for your app by enabling the "Personal VPN" capability for your app in Xcode.Whitewood
why not connect free vpn ip address objective cKnown
B
27

The problem is the error you are getting when saving: Save error: Error Domain=NEVPNErrorDomain Code=4

If you look in the NEVPNManager.h header file, you will see that error code 4 is "NEVPNErrorConfigurationStale". The configuration is stale and needs to be loaded. You should call loadFromPreferencesWithCompletionHandler: and in the completion handler modify the values you want to modify, and then call saveToPreferencesWithCompletionHandler:. The example in your question is modifying the configuration before the loading is completed, which is why you are getting this error.

More like this:

[manager loadFromPreferencesWithCompletionHandler:^(NSError *error) {
     // do config stuff
     [manager saveToPreferencesWithCompletionHandler:^(NSError *error) {
     }];
}];
Bernardinebernardo answered 29/7, 2014 at 4:16 Comment(9)
It works OK! One more thing is that, this functionality works only and real devices not simulator.Keel
By the way: I'll blog about this tonight and I'll mention you man ;)Keel
Have you tried this on beta5? I have similar code that works up to beta 4, and on beta 5 I got an error: Domain=NEConfigurationErrorDomain Code=2 "Missing name" UserInfo=0x170078940 {NSLocalizedDescription=Missing name}Gouda
Do you have any idea about the "Missing name" error?Keel
same Missing Name error in xcode 6-beta 6 ! Any solution?Fanatic
The missing name issue showed up to me, when I actually forgot to set a username.Adham
I get a 'Missing identity' error when trying to save the config in preferences. Any solution? (working with iOS 8.2)Mayolamayon
When you use the authentication mode Certificate it needs this certificate set in the protocol as an NSData object prot.authenticationMethod = NEVPNIKEAuthenticationMethod.Certificate; prot.identityData = certificate; prot.identityDataPassword = self._PKCS12CertificatePassword (swift code)Adham
@Bernardinebernardo then if i did as you said in your answer now getting this error : #47551206 can you please help me ?Cabriolet
C
10

This answer will be helpful for those who are looking for solution using Network Extension framework.

My requirement was to connect/disconnect VPN server with IKEv2 Protocol (of course you can use this solution for IPSec also by changing vpnManager protocolConfiguration)

NOTE : If you are looking for L2TP Protocol, Using Network extension its not possible to connect VPN server. Refer : https://forums.developer.apple.com/thread/29909

Here is my working code snippet :

Declare VPNManager Object and other useful things

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

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

Add observer in viewDidLoad for getting VPN Staus and store vpnPassword in Keychain, you can store sharedSecret too which will you need IPSec protocol.

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)

 }

Now in my app was having UISwitch to connect/disconnect VPN server.

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

After clicking on switch initiate VPN tunnel using below code.

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

                        }

                    })

                }
            })
        }
    }
}

Once VPN started successfully you can change the status accordingly i.e. by calling 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
    }
}

I've referred from here :

https://mcmap.net/q/536813/-quot-error-domain-nevpnerrordomain-code-1-quot-null-quot-quot-while-connecting-vpn-server

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

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

Thank you :)

Cabriolet answered 30/11, 2017 at 10:41 Comment(4)
i have followed your steps , but i am getting "Unexpected Error" alert while connecting to vpn, my ikev2 server uses .crt and i already have it installed on my device , manually added configuration works good , only personal vpn with same configuration getting errorPushed
Installed .crt can work with both added configuration and Personal vpn ? or we have to do something else for Personal vpn to use certificate?Pushed
KeychainSwift not works here it just return the String as Data you need a keychain ref blog.moatazthenervous.com/create-a-key-chain-for-apples-vpnMystify
@KooroshGhorbani i am facing the same issue used this class it is getting crashed while saving preferences .Abhor
C
0

I tested other solutions multiple times and noticed that

Putting saveToPreferences in loadFromPreferences is NOT Enough!!


Consider we already loaded a manager instance (or created new without loading), calling save inside of completion-handler of load was for me working randomly (sometimes), and having my ages of debugging experience, it seemed like iOS just needed time (to process something?!).

So queueing for later always works instead of randomly, like:

guard let manager = self.manager else { return }

manager.loadFromPreferences(completionHandler: { _ in
    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()
            });
        });
    }
});

Note that I put loading around above's main-thread, but just to be sure, and loading is ONLY required if you don't have an instance already (or if it was changed).

Calebcaledonia answered 9/9, 2021 at 22:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.