您还未登录! 登录 | 注册 | 帮助  

您的位置: 首页 > 软件测试管理 > 配置管理 > 正文

iOS VPN开发的配置和管理

发表于:2017-01-09 作者:网络转载 来源:

  最近对VPN的开发来了兴致,就花些时间研究研究。由于在VPN方面苹果并没有给出官方文档,我觉得有必要把自己研究出来的东西分享出来,方便大家的学习和交流。
  VPN的开发主要依赖于苹果在iOS 8推出的NetworkExtension框架。
  准备工作
  1.由于VPN无法在模拟器上进行调试,因此,需要一台系统是iOS 8以上的真机。
  2.Xcode版本必须是6.0及以上。
  3.由于测试无法在模拟器上进行,因此你需要在苹果开发者中心注册开发者账户。
  配置
  如果你还没有provisioning profiles,那么你需要在苹果开发者中心创建一个。
  首先,你需要登录开发者账户,然后点击“Certificate, IDs & Profiles”:
  选择“Identifiers”->“App IDs”,选择你的app所对应的id,并将“Personal VPN”改为选中状态。
  然后,下载provisioning profile取代原先旧的provisioning profile。
  代码
  回到xcode中并创建一个“single view application project”。然后,在屏幕的中央位置放置一个UIButton并连接到对应的控制器中。
  我将要做的就是在控制器的viewDidLoad:方法中当按钮被点击时应用程序可以连接到VPN服务器上。在这之前,你需要了解它是如何工作的。如果你对NetworkExtension框架足够了解,那么去开发会更容易。
  NetworkExtension.framework
  这个框架允许每个符合条件的app能够访问系统的偏好设置,但是限制在应用自己的沙盒中。这就是说你不能访问别的应用的沙盒。
  首先,被保存的偏好信息会被操作系统加载以确保应用程序能够去访问。一旦它们被加载了,那么你就能够去进行修改。偏好信息发生改动后,它们需要得到保存,未被保存的偏好信息将不会被应用。如果你不再需要你的app的偏好信息,你也可以移除。因此,要创建VPN配置我们需要做以下几件事:
  · 加载APP的偏好信息
  · 修改偏好信息
  · 保存偏好信息
  注:即使你还没有设置任何的配置你也需要去加载应用程序的偏好信息。
  NetworkExtension包含三个主要的类:
  · NEVPNManager
  · NEVPNProtocol
  · NEVPNConnection
  NEVPNManager是这个框架中最重要的类。它主要负责加载、保存、移除偏好信息。实际上,所有的VPN任务都不得不通过这个类来实现。
  创建VPN连接
  通过NEVPNManager的单例方法获取该类的实例:
  NEVPNManager *manager = [NEVPNManager sharedManager];
  当NEVPNManager的实例变量被初始化后,系统的偏好信息就通过loadFromPreferencesWithCompletionHandler:方法加载:
  [manager loadFromPreferencesWithCompletionHandler:^(NSError *error) {
  // Put your codes here...
  }];
  在上面这个方法中包含一个block,这个block会在加载进程完成时触发。block包含一个NSError类型的参数,如果加载成功,那么这个参数将会为空,否则,它将会是非空的。
  当加载进程完成后,就该去设置VPN连接了。iOS 8支持两种协议:IPSec和IKEv2.这是苹果第一次在它们自己的操作系统中支持IKEv2协议。这个协议被几乎所有的主流操作系统所支持,包括安卓、Windows phone、Linux等。除了这两个协议外,苹果也支持你自己创建的协议。这个特性对于那些自己实现协议的人来说非常重要。以下我将以IPSec协议为例来进行设置:
NEVPNProtocolIPSec *p = [[NEVPNProtocolIPSec alloc] init];
p.username = @"[Your username]";
p.passwordReference = [VPN user password from keychain];
p.serverAddress = @"[Your server address]";
p.authenticationMethod = NEVPNIKEAuthenticationMethodSharedSecret;
p.sharedSecretReference = [VPN server shared secret from keychain];
p.localIdentifier = @"[VPN local identifier]";
p.remoteIdentifier = @"[VPN remote identifier]";
p.useExtendedAuthentication = YES;
p.disconnectOnSleep = NO;
  上面代码的第一行我创建了一个NEVPNProtocolIPSec实例。这个类继承自NEVPNProtocol类。NEVPNProtocol是一个虚基类来让你去创建自己的协议。然后,在第二行第三行设置用户名和密码。注意,密码是和钥匙串关联的,因此,你需要先在钥匙串中储存你的密码然后获取它。第四行是我们的服务器地址。服务器地址可以是IP地址,主机名或者是URL。再下面一行是验证方法。iOS 8支持三种验证方法:
  · NEVPNIKEAuthenticationMethodNone:不需要与服务器进行验证
  · NEVPNIKEAuthenticationMethodCertificate:使用证书和私钥作为验证凭证
  · NEVPNIKEAuthenticationMethodSharedSecret:使用共享的秘钥作为验证凭证
  再往下一行是共享秘钥的引用。由于它引用自钥匙串,因此要从钥匙串获取共享的秘钥。如果你你准备使用证书而不是共享秘钥的方式,那就没必要设置sharedSecretReference属性,但是你需要设置identityData属性。Identity data是PKCS12数据类型的VPN验证实体。这个属性的值必须是PKCS12的NSData类型的:
  p.identityData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"clientCert" ofType:@"p12"]];
  下面两行是本地和远程的标识符属性。这两个字符串用来标识本地和远程的验证的IPSec的终止。再往下的属性叫做useExtendedAuthetication。这是一个布尔值用来指示是否使用额外的验证方法。这个验证方式是作为IKE验证方法的所额外添加的,它用来验证IKE会话的终止。最后一个属性是disconnectOnSleep,这个布尔值用来指示当设备进入睡眠状态时,VPN连接是否断开。

  设置完协议后,接下来要做的就是分配协议给VPN管理者:
  [manager setProtocol:p];
  IPSec和IKEv2协议都有一个特性叫做按需(On-demand)。这个特性能够在用户尝试连接网络的时候自动连接。如果你不设置,那么当设备进入睡眠状态时,建立起来的VPN连接会自动断开来达到省电的目的。为了避免这个问题,我们需要做相应的设置。首先,你需要做将onDemandEnabled设为YES:
  [[NEVPNManager sharedManager] setOnDemandEnabled:YES];
  除此之外,你还需要告诉操作系统何时你希望on-demand是有用的。因此,你需要在配置中添加一些策略。这些策略叫做“按需策略”。按需策略是告诉操作系统VPN建立的是按需连接的所有性质的集合。onDemandRules属性接收策略数组,因而,你能给VPN的配置设置多种策略。比如,你可以设置一种策略来告诉操作系统当用户想要打开Apple.com时建立VPN连接,否则,VPN连接将不会建立。有一件事是你可能想要去做的,那就是当应用程序建立网络连接的时候激活VPN连接。因此,所有的iOS网络通信将会通过你的VPN服务器进行转换。为了达到这样的效果,你需要使用到NEOnDemandRuleConnect类。在NetworkExtension框架中,苹果提供了一些有用的按需策略的模板。NEOnDemandRuleConnect类是其中的一个模板。它会告诉操作系统当iOS需要连接到网络时建立VPN连接,因而,只要用户需要用到网络就会连接到你的VPN服务器。以下是使用NEOnDemandRuleConnect模板的例子:
  [[NEVPNManager sharedManager] setOnDemandEnabled:YES];
  NSMutableArray *rules = [[NSMutableArray alloc] init];
  NEOnDemandRuleConnect *connectRule = [NEOnDemandRuleConnect new];
  [rules addObject:connectRule];
  [[NEVPNManager sharedManager] setOnDemandRules:array];
  最后一个需要去设置的是VPN偏好信息的描述信息:
  [manager setLocalizedDescription:@"[You VPN configuration name]"];
  设置完后还没有结束,因为我们还没有将其保存。为了能保存配置信息需要调用aveToPreferencesWithCompletionHandler: method::
  [manager saveToPreferencesWithCompletionHandler:^(NSError *error) {
  if(error) {
  NSLog(@"Save error: %@", error);
  }
  else {
  NSLog(@"Saved!");
  }
  }];
  连接VPN connection
  设置完偏好信息后,就可以去连接它了。NEVPNManager有一个属性叫做connection。这个属性是NEVPNConnection类。它保存了负责VPN连接的信息。为了能够与VPN服务器相连接,需要调用NEVPNConnection类的startVPNTunnelAndReturnError:方法:
  - (IBAction)buttonPressed:(id)sender {
  NSError *startError;
  [[NEVPNManager sharedManager].connection startVPNTunnelAndReturnError:&startError];
  if(startError) {
  NSLog(@"Start error: %@", startError.localizedDescription);
  } else {
  NSLog(@"Connection established!");
  }
  }
  在你的设备上运行程序你会看到当按钮被按下时新的连接会被创建然后与服务器连接。并且,你也可以通过调用stopVPNTunnel方法断开与VPN服务器的连接。
  以下代码来自gist:
- (void)viewDidLoad
{
[super viewDidLoad];
// init VPN manager
self.vpnManager = [NEVPNManager sharedManager];
// load config from perference
[_vpnManager loadFromPreferencesWithCompletionHandler:^(NSError *error) {
if (error) {
NSLog(@"Load config failed [%@]", error.localizedDescription);
return;
}
NEVPNProtocolIPSec *p = _vpnManager.protocol;
if (p) {
// Protocol exists.
// If you don't want to edit it, just return here.
} else {
// create a new one.
p = [[NEVPNProtocolIPSec alloc] init];
}
// config IPSec protocol
p.username = @"[Your username]";
p.serverAddress = @"[Your server address]";;
// Get password persistent reference from keychain
// If password doesn't exist in keychain, should create it beforehand.
// [self createKeychainValue:@"your_password" forIdentifier:@"VPN_PASSWORD"];
p.passwordReference = [self searchKeychainCopyMatching:@"VPN_PASSWORD"];
// PSK
p.authenticationMethod = NEVPNIKEAuthenticationMethodSharedSecret;
// [self createKeychainValue:@"your_psk" forIdentifier:@"PSK"];
p.sharedSecretReference = [self searchKeychainCopyMatching:@"PSK"];
/*
// certificate
p.identityData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"client" ofType:@"p12"]];
p.identityDataPassword = @"[Your certificate import password]";
*/
p.localIdentifier = @"[VPN local identifier]";
p.remoteIdentifier = @"[VPN remote identifier]";
p.useExtendedAuthentication = YES;
p.disconnectOnSleep = NO;
_vpnManager.protocol = p;
_vpnManager.localizedDescription = @"IPSec Demo";
[_vpnManager saveToPreferencesWithCompletionHandler:^(NSError *error) {
if (error) {
NSLog(@"Save config failed [%@]", error.localizedDescription);
}
}];
}];
}
- (IBAction)startVPNConnection:(id)sender {
//[[VodManager sharedManager] installVPNProfile];
NSError *startError;
[_vpnManager.connection startVPNTunnelAndReturnError:&startError];
if (startError) {
NSLog(@"Start VPN failed: [%@]", startError.localizedDescription);
}
}
#pragma mark - KeyChain
static NSString * const serviceName = @"im.zorro.ipsec_demo.vpn_config";
- (NSMutableDictionary *)newSearchDictionary:(NSString *)identifier {
NSMutableDictionary *searchDictionary = [[NSMutableDictionary alloc] init];
[searchDictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
NSData *encodedIdentifier = [identifier dataUsingEncoding:NSUTF8StringEncoding];
[searchDictionary setObject:encodedIdentifier forKey:(__bridge id)kSecAttrGeneric];
[searchDictionary setObject:encodedIdentifier forKey:(__bridge id)kSecAttrAccount];
[searchDictionary setObject:serviceName forKey:(__bridge id)kSecAttrService];
return searchDictionary;
}
- (NSData *)searchKeychainCopyMatching:(NSString *)identifier {
NSMutableDictionary *searchDictionary = [self newSearchDictionary:identifier];
// Add search attributes
[searchDictionary setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
// Add search return types
// Must be persistent ref !!!!
[searchDictionary setObject:@YES forKey:(__bridge id)kSecReturnPersistentRef];
CFTypeRef result = NULL;
SecItemCopyMatching((__bridge CFDictionaryRef)searchDictionary, &result);
return (__bridge_transfer NSData *)result;
}
- (BOOL)createKeychainValue:(NSString *)password forIdentifier:(NSString *)identifier {
NSMutableDictionary *dictionary = [self newSearchDictionary:identifier];
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)dictionary);
NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
[dictionary setObject:passwordData forKey:(__bridge id)kSecValueData];
status = SecItemAdd((__bridge CFDictionaryRef)dictionary, NULL);
if (status == errSecSuccess) {
return YES;
}
return NO;
}
  以上。