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()
Great post
Thanks
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.
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.