在iOS 8中以编程方式连接VPN

26

自从iOS 8 beta发布以来,我在其bundle中发现了一个名为Network Extension的框架,该框架将使开发人员能够编程方式配置和连接到VPN服务器,而无需安装任何配置文件。

该框架包含一个重要的类NEVPNManager。该类还有三个主要方法,让我可以保存、加载或删除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)当我尝试保存偏好设置时,会出现以下错误:保存错误:Error Domain=NEVPNErrorDomain Code=4 "The operation couldn’t be completed. (NEVPNErrorDomain error 4.)” 这是什么错误?我该如何解决这个问题?
2)当我调用[[NEVPNManager sharedManager].connection startVPNTunnelAndReturnError:&startError];方法时,并没有返回任何错误,但连接状态从未连接变为连接中只持续了一瞬间,然后又回到了未连接状态。
非常感谢您的帮助 :)

1
奇怪的是,当我在使用beta 4版本的设备上运行您的代码副本时,从[NEVPNManager sharedManager]返回了一个nil。 - Rembrandt Q. Einstein
1
啊,我没有在我的应用程序ID或权限文件中添加“个人VPN”权限。要这样做,请转到项目下的“功能”。 - Rembrandt Q. Einstein
你是如何从钥匙串中检索密码的?我已经将我的账户密码存储在钥匙串中,并尝试检索持久引用(文档暗示必要)到protocol.passwordReference字段,但当我尝试连接VPN服务时它仍然提示我输入密码(一旦我输入密码,我就可以连接,所以其他方面看起来都很好)。 - Rembrandt Q. Einstein
使用NEVPNManager类需要com.apple.developer.networking.vpn.api授权。您可以通过在Xcode中为您的应用启用“个人VPN”功能来获取此授权。 - Abc.Xyz
为什么不连接免费的 VPN IP 地址 Objective C? - Ramani Hitesh
3个回答

27

问题是在保存时出现的错误:Save error: Error Domain=NEVPNErrorDomain Code=4

如果查看 NEVPNManager.h 标头文件,您会发现错误代码 4 是 "NEVPNErrorConfigurationStale"。配置过期并需要加载。 您应该调用 loadFromPreferencesWithCompletionHandler:,并在完成处理程序中修改要修改的值,然后调用 saveToPreferencesWithCompletionHandler:。您在问题中的示例是在完成加载之前修改配置,这就是为什么会出现此错误。

更多类似内容:

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

1
它运行正常!还有一件事是,该功能仅在真实设备上工作,而不是模拟器。 - Mohammad M. Ramezanpour
1
你在beta5上试过这个吗?我有类似的代码,在beta4上可以运行,但在beta5上出现了错误:Domain=NEConfigurationErrorDomain Code=2 "Missing name" UserInfo=0x170078940 {NSLocalizedDescription=Missing name}。 - lstipakov
在Xcode 6-beta 6中出现了相同的Missing Name错误!有什么解决方法吗? - Vish
当我尝试保存首选项中的配置时,出现“缺少标识”的错误。有什么解决办法吗?(使用iOS 8.2) - user591410
当您使用证书身份验证模式时,需要将此证书设置为NSData对象并添加到协议中。prot.authenticationMethod = NEVPNIKEAuthenticationMethod.Certificate; prot.identityData = certificate; prot.identityDataPassword = self._PKCS12CertificatePassword(Swift代码) - Forke
显示剩余4条评论

10

这篇回答会帮助那些正在寻找使用 Network Extension 框架解决问题的人。

我的需求是使用 IKEv2 协议连接/断开 VPN 服务器(当然,您可以通过更改 vpnManager protocolConfiguration 来使用 IPSec 解决此问题)。

注意:如果您正在寻找 L2TP 协议,则无法使用 Network Extension 连接 VPN 服务器。参考:https://forums.developer.apple.com/thread/29909

以下是我正在使用的代码片段:

声明 VPNManager 对象和其他有用的东西。

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

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

在viewDidLoad中添加观察者以获取VPN状态,并将vpnPassword存储在Keychain中,您还可以存储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
    }
}
我从这里参考:https://stackoverflow.com/a/47569982/3931796https://forums.developer.apple.com/thread/25928http://blog.moatazthenervous.com/create-a-vpn-connection-with-apple-swift/。谢谢 :)

我按照您的步骤进行操作,但在连接 VPN 时出现“意外错误”警报,我的 IKEv2 服务器使用 .crt 文件,并且我已经将其安装在设备上,手动添加配置可以正常工作,只有相同配置的个人 VPN 出现错误。 - Vishal_iOS_Developer
已安装的 .crt 文件可以与添加的配置和个人 VPN 一起使用吗?还是我们需要做其他事情才能让个人 VPN 使用证书? - Vishal_iOS_Developer
KeychainSwift 在这里不起作用,它只返回字符串作为数据,你需要一个钥匙链参考。请查看此链接:http://blog.moatazthenervous.com/create-a-key-chain-for-apples-vpn/。 - Koorosh Ghorbani
@KooroshGhorbani 我也遇到了同样的问题,使用这个类时在保存偏好设置时崩溃了。 - jarvis12

0

我多次测试了其他解决方案,并注意到:

loadFromPreferences中放置saveToPreferences是不够的!


考虑我们已经加载了一个管理器实例(或创建了一个新的但未加载),在加载完成时调用保存并不能有效地工作,对于我来说有时候能工作,但是根据我的多年调试经验,这似乎是因为需要时间(处理某些事情?!)。

因此,稍后排队总是比随机工作更好,例如:

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

请注意,我将加载放在上面的主线程周围,但只是为了确保,只有在您没有实例或者实例已更改时才需要加载。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接