You are currently viewing Simple VPN app in IOS (Swift5)

Simple VPN app in IOS (Swift5)

  • Post author:
  • Post category:iOS / swift
  • Post comments:8 Comments
  • Post last modified:December 10, 2021
  • Reading time:5 mins read

Creating a VPN app in iOS using swift is pretty simple.

First you will need KeychainService

//
// KeychainService.swift
// Androvaid VPN
//
// Created by Shaikh Wasi Sadman on 18/11/21.
// Copyright (c) 2021 Androvaid. All rights reserved.
//


import Foundation
import UIKit
import Security

// Identifiers
let serviceIdentifier = "MySerivice"
let userAccount = "authenticatedUser"
let accessGroup = "MySerivice"

// Arguments for the keychain queries
var kSecAttrAccessGroupSwift = NSString(format: kSecClass)

let kSecClassValue = kSecClass as CFString
let kSecAttrAccountValue = kSecAttrAccount as CFString
let kSecValueDataValue = kSecValueData as CFString
let kSecClassGenericPasswordValue = kSecClassGenericPassword as CFString
let kSecAttrServiceValue = kSecAttrService as CFString
let kSecMatchLimitValue = kSecMatchLimit as CFString
let kSecReturnDataValue = kSecReturnData as CFString
let kSecMatchLimitOneValue = kSecMatchLimitOne as CFString
let kSecAttrGenericValue = kSecAttrGeneric as CFString
let kSecAttrAccessibleValue = kSecAttrAccessible as CFString

class KeychainService: NSObject {
    func save(key:String, value:String) {
        let keyData: Data = key.data(using: String.Encoding(rawValue: String.Encoding.utf8.rawValue), allowLossyConversion: false)!
        let valueData: Data = value.data(using: String.Encoding(rawValue: String.Encoding.utf8.rawValue), allowLossyConversion: false)!
        
        let keychainQuery = NSMutableDictionary();
        keychainQuery[kSecClassValue as! NSCopying] = kSecClassGenericPasswordValue
        keychainQuery[kSecAttrGenericValue as! NSCopying] = keyData
        keychainQuery[kSecAttrAccountValue as! NSCopying] = keyData
        keychainQuery[kSecAttrServiceValue as! NSCopying] = "VPN"
        keychainQuery[kSecAttrAccessibleValue as! NSCopying] = kSecAttrAccessibleAlwaysThisDeviceOnly
        keychainQuery[kSecValueData as! NSCopying] = valueData;
        // Delete any existing items
        SecItemDelete(keychainQuery as CFDictionary)
        SecItemAdd(keychainQuery as CFDictionary, nil)
    }
    
    func load(key: String)->Data {
        
        let keyData: Data = key.data(using: String.Encoding(rawValue: String.Encoding.utf8.rawValue), allowLossyConversion: false)!
        let keychainQuery = NSMutableDictionary();
        keychainQuery[kSecClassValue as! NSCopying] = kSecClassGenericPasswordValue
        keychainQuery[kSecAttrGenericValue as! NSCopying] = keyData
        keychainQuery[kSecAttrAccountValue as! NSCopying] = keyData
        keychainQuery[kSecAttrServiceValue as! NSCopying] = "VPN"
        keychainQuery[kSecAttrAccessibleValue as! NSCopying] = kSecAttrAccessibleAlwaysThisDeviceOnly
        keychainQuery[kSecMatchLimit] = kSecMatchLimitOne
        keychainQuery[kSecReturnPersistentRef] = kCFBooleanTrue
        
        var result: AnyObject?
        let status = withUnsafeMutablePointer(to: &result) { SecItemCopyMatching(keychainQuery, UnsafeMutablePointer($0)) }
        
        if status == errSecSuccess {
            if let data = result as! NSData? {
                if let value = NSString(data: data as Data, encoding: String.Encoding.utf8.rawValue) {
                }
                return data as Data;
            }
        }
        return "".data(using: .utf8)!;
    }
}

Then use VPNManager class and you can easily make one.

//
// VPNManager.swift
// Androvaid VPN
//
// Created by Shaikh Wasi Sadman on 30/11/21.
// Copyright (c) 2021 Androvaid. All rights reserved.
//

import Foundation
import NetworkExtension


protocol VpnDelegate: AnyObject {
    func setStatus(status: Status)
    func checkNES(status: NEVPNStatus)
}

class VPNManager {
    
    let vpnManager = NEVPNManager.shared();
    var server_address : String
    weak var vpnDelegate: VpnDelegate?
    
    
    /// The static field that controls the access to the singleton instance.
    ///
    /// This implementation let you extend the Singleton class while keeping
    /// just one instance of each subclass around.
    static var shared: VPNManager = {
        let instance = VPNManager(serverAddress: "Enter your server address")
        // ... configure the instance
        // ...
        return instance
    }()
    
    private init(serverAddress address : String) {
        self.server_address = address
    }
    
    private var vpnLoadHandler: (Error?) -> Void { return
        { (error:Error?) in
            if ((error) != nil) {
                self.vpnDelegate?.setStatus(status: Status.errorConnecting)
                print("vpnLoadHandler Could not load VPN Configurations : \(String(describing: error))")
                return;
            }
            
            let p = NEVPNProtocolIKEv2()
            //        p.username = "androvaid"
            p.serverAddress = self.server_address /// Required
            p.authenticationMethod = NEVPNIKEAuthenticationMethod.sharedSecret
            
            let kcs = KeychainService();
            kcs.save(key: "SHARED", value: "password") // Enter your password
            p.sharedSecretReference = kcs.load(key: "SHARED") // Required type is Data
            p.useExtendedAuthentication = false
            p.disconnectOnSleep = false
            p.remoteIdentifier = self.server_address /// Required
            
            p.ikeSecurityAssociationParameters.diffieHellmanGroup = NEVPNIKEv2DiffieHellmanGroup.group2
            p.ikeSecurityAssociationParameters.encryptionAlgorithm = NEVPNIKEv2EncryptionAlgorithm.algorithmAES128
            p.ikeSecurityAssociationParameters.integrityAlgorithm = NEVPNIKEv2IntegrityAlgorithm.SHA96
            p.ikeSecurityAssociationParameters.lifetimeMinutes = 1140
            
            p.childSecurityAssociationParameters.diffieHellmanGroup = NEVPNIKEv2DiffieHellmanGroup.group2
            p.childSecurityAssociationParameters.encryptionAlgorithm = NEVPNIKEv2EncryptionAlgorithm.algorithmAES128
            p.childSecurityAssociationParameters.integrityAlgorithm = NEVPNIKEv2IntegrityAlgorithm.SHA96
            p.childSecurityAssociationParameters.lifetimeMinutes = 1140
            
            self.vpnManager.protocolConfiguration = p
            
            self.vpnManager.localizedDescription = "Androvaid VPN" // The VPN Name saved in your setting
            self.vpnManager.isOnDemandEnabled = false
            
            /// You can set rules for On Demand Connection
            let rule1 : NEOnDemandRule = NEOnDemandRule()
            rule1.setValuesForKeys(["Action" : NEOnDemandRuleAction.connect, "InterfaceTypeMatch" : NEOnDemandRuleInterfaceType.wiFi])
            
            let rule2 : NEOnDemandRule = NEOnDemandRule()
            rule2.setValuesForKeys(["Action" : NEOnDemandRuleAction.connect, "InterfaceTypeMatch" : NEOnDemandRuleInterfaceType.cellular])
            
            let connectRule = NEOnDemandRuleConnect()
            connectRule.interfaceTypeMatch = .any
            self.vpnManager.onDemandRules = [connectRule]
            
            
            
            //        do {
            //
            //
            //        let rule1 : NEOnDemandRuleConnect = NEOnDemandRuleConnect()
            //
            //        rule1.setValue(NEOnDemandRuleAction.connect, forKeyPath: "Action")
            //        //rule1.setValue(NEOnDemandRuleAction.connect, forKey: "Action")
            //        //rule1.setValue(NEOnDemandRuleAction.connect, forKeyPath:)
            //        //rule1.setValue(NEOnDemandRuleAction.connect, forUndefinedKey: "Action")
            //
            //        //rule1.setValuesForKeys(["Action" : NEOnDemandRuleAction.connect, "InterfaceTypeMatch" : NEOnDemandRuleInterfaceType.wiFi])
            //        rule1.setValue(NEOnDemandRuleInterfaceType.wiFi, forKeyPath: "InterfaceTypeMatch")
            //
            //
            //        let rule2 : NEOnDemandRule = NEOnDemandRule()
            //        rule2.setValue(NEOnDemandRuleAction.connect, forKeyPath: "action")
            //        rule2.setValue(NEOnDemandRuleInterfaceType.cellular, forKeyPath: "InterfaceTypeMatch")
            //
            //        self.vpnManager.protocolConfiguration = p
            //        self.vpnManager.onDemandRules = [rule2, rule1]
            //
            //        } catch {
            //            Log.d(message:  "Error info: \(error)")
            //        }
            //
            self.vpnManager.isEnabled = true
            self.vpnManager.saveToPreferences(completionHandler: self.vpnSaveHandler)
        }
        
    }
    
    private var vpnSaveHandler: (Error?) -> Void { return
        { (error:Error?) in
            if (error != nil) {
                print("vpnSaveHandler Could not save VPN Configurations \(String(describing: error?.localizedDescription))")
                return
            } else {
                do {
                    print("vpnSaveHandler do")
                    try self.vpnManager.connection.startVPNTunnel()
                    
                } catch let error {
                    
                    print("Error starting VPN Connection \(error.localizedDescription)");
                }
            }
        }
        //self.vpnlock = false
    }
    
    public func connectVPN() {
        //For no known reason the process of saving/loading the VPN configurations fails.On the 2nd time it works
        print("connectVPN")
        self.vpnManager.loadFromPreferences(completionHandler: self.vpnLoadHandler)
        NotificationCenter.default.addObserver(forName: NSNotification.Name.NEVPNStatusDidChange, object: nil , queue: nil) {
            notification in
            let nevpnconn = notification.object as! NEVPNConnection
            let status = nevpnconn.status
            self.vpnDelegate?.checkNES(status: status)
        }
    }
    
    public func disconnectVPN() ->Void {
        self.vpnManager.loadFromPreferences { error in
            assert(error == nil, "Failed to load preferences: \(error!.localizedDescription)")
            self.vpnManager.connection.stopVPNTunnel()
            NotificationCenter.default.addObserver(forName: NSNotification.Name.NEVPNStatusDidChange, object: nil , queue: nil) {
                notification in
                let nevpnconn = notification.object as! NEVPNConnection
                let status = nevpnconn.status
                self.vpnDelegate?.checkNES(status: status)
            }
        }
    }
}

 

You can use it like below

Initialize VPNManger => var vpn = VPNManager.shared

Connect VPN =>  self.vpn.connectVPN();

Disconnect VPN => self.vpn.disconnectVPN()

 

 

This Post Has 8 Comments

  1. wasisadman

    Great post

    1. Sadman

      Thanks

  2. GregoireG

    When copy-pasting the code, it doesn’t accept “func setStatus(status: Status)” as Status is unrecognized. Did you mean OSStatus or NEVPNStatus? errorConnecting is then not recognized too.

    1. Sadman

      As you can see… it’s a delegate func or listener func or method what ever you are comfortable to say… it takes the status of any kind of error regarding the process. vpn status is checked by the chrckNES delegate

      please let me know if you have any confusion.

  3. Kumar

    Could not load VPN Configurations : Optional(Error Domain=NEVPNErrorDomain Code=5 “permission denied” UserInfo={NSLocalizedDescription=permission denied})

  4. Kumar

    how can i connect different server ?

  5. Sadman

    Hello kumar,

    This is a fundamental implementation for VPN. There are so many things to consider in regard to the server and the application.

    In this scenario, if the server accepts ** sharedSecret** and a password, you can easily connect with your VPN server. Normally it’s a bit had to find that kind of server but if you can create one for your own then it will be easy to test.

    You can use strongswan for that which is an open-source project.

    https://github.com/strongswan/strongswan

  6. jack

    Dear Sir, very good sharing,
    and there is necessary to set the KeychainService? Can I skip it? thanks.

Leave a Reply