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

Vvk
  • 4,031
  • 29
  • 51
  • 1
    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]; – Rembrandt Q. Einstein Jul 29 '14 at 19:50
  • 1
    Ah, I had not added the "Personal VPN" entitlement to my App ID or entitlements file. To do so, go to "Capabilities" under the project – Rembrandt Q. Einstein Jul 29 '14 at 21:33
  • 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). – Rembrandt Q. Einstein Aug 04 '14 at 17:31
  • 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. – Vadiraj Purohit May 06 '16 at 13:05
  • why not connect free vpn ip address objective c – Ramani Hitesh Oct 26 '18 at 07:24

3 Answers3

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) {
     }];
}];
quellish
  • 21,123
  • 4
  • 76
  • 83
  • 1
    It works OK! One more thing is that, this functionality works only and real devices not simulator. – Mohammad M. Ramezanpour Aug 02 '14 at 07:03
  • By the way: I'll blog about this tonight and I'll mention you man ;) – Mohammad M. Ramezanpour Aug 02 '14 at 07:04
  • 1
    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} – lstipakov Aug 08 '14 at 16:41
  • Do you have any idea about the "Missing name" error? – Mohammad M. Ramezanpour Aug 20 '14 at 06:56
  • same Missing Name error in xcode 6-beta 6 ! Any solution? – Vish Aug 28 '14 at 09:09
  • The missing name issue showed up to me, when I actually forgot to set a username. – Forke Mar 30 '15 at 13:27
  • I get a 'Missing identity' error when trying to save the config in preferences. Any solution? (working with iOS 8.2) – user591410 Mar 31 '15 at 14:22
  • 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) – Forke May 29 '15 at 11:21
  • @quellish then if i did as you said in your answer now getting this error : https://stackoverflow.com/questions/47550706/error-domain-nevpnerrordomain-code-1-null-while-connecting-vpn-server can you please help me ? – Shrikant K Nov 30 '17 at 07:26
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://stackoverflow.com/a/47569982/3931796

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

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

Thank you :)

Shrikant K
  • 1,988
  • 2
  • 23
  • 34
  • 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 error – Vishal_iOS_Developer Aug 10 '18 at 07:03
  • Installed .crt can work with both added configuration and Personal vpn ? or we have to do something else for Personal vpn to use certificate? – Vishal_iOS_Developer Aug 10 '18 at 07:05
  • KeychainSwift not works here it just return the String as Data you need a keychain ref http://blog.moatazthenervous.com/create-a-key-chain-for-apples-vpn/ – Koorosh Ghorbani Dec 10 '18 at 20:58
  • @KooroshGhorbani i am facing the same issue used this class it is getting crashed while saving preferences . – jarvis12 Mar 18 '19 at 11:59
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).

Top-Master
  • 7,611
  • 5
  • 39
  • 71