iOS - NetworkExtension 建立隧道(Open
注意:由于简大叔对XXX关键字过敏,所以本文均用XXX代替V皮N。
需要实现Personal-XXX功能是苹果开发者账号才有权限开启,所以第一步先去开发者中心创建证书,并添加权限(此步骤省略,自己百度)
本文章针对的是OpenXXX !!!
我们将使用OpenXXXAdapter,使用Cocoapods进行安装
pod 'OpenVPNAdapter', :git => 'https://github.com/ss-abramchuk/OpenVPNAdapter.git', :tag => '0.4.0'
Carthage安装
github "ss-abramchuk/OpenVPNAdapter"
多target时,Cocoapods的格式如下:
platform :ios, '10.0'
target 'OpenSSLOnce' do
use_frameworks!
pod 'AFNetworking','~> 4.0'
pod 'MJRefresh'
pod 'SVProgressHUD'
post_install do |installer_representation|
installer_representation.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'NO'
end
end
end
end
target 'TargetTunnel' do
use_frameworks!
pod 'OpenVPNAdapter', :git => 'https://github.com/ss-abramchuk/OpenVPNAdapter.git', :tag => '0.4.0'
end
1、创建Target
创建Target.png
选择Network.png
2、创建完成后需要将主项目和子项目的Bundle Identifier进行替换,这里填写的是开发者中心创建好的Bundle Identifier,填写完成后父子项目添加相应的Capability,如下图
添加Capability.png
代码实现 - OC 版
- 首先是子项目,也就是新创建的Target
PacketTunnelProvider.h
//
// PacketTunnelProvider.h
// TargetTunnel
//
//
//
@import NetworkExtension;
@import OpenVPNAdapter;
NS_ASSUME_NONNULL_BEGIN
@interface PacketTunnelProvider : NEPacketTunnelProvider
@property(nonatomic,strong) OpenVPNAdapter *vpnAdapter;
@property(nonatomic,strong) OpenVPNReachability *openVpnReach;
typedef void(^StartHandler)(NSError * _Nullable);
typedef void(^StopHandler)(void);
@property(nonatomic,copy) StartHandler __nullable startHandler;
@property(nonatomic,copy) StopHandler __nullable stopHandler;
@end
NS_ASSUME_NONNULL_END
PacketTunnelProvider.m
//
// PacketTunnelProvider.m
// TargetTunnel
//
#import "PacketTunnelProvider.h"
#include "NEPacketTunnelFlow+NEPacketTunnelFlow_Extension.h"
@interface PacketTunnelProvider ()<OpenVPNAdapterDelegate>
// 这个先放下,一会讲,主要用到和父项目进行通信
@property (strong,nonatomic) NSUserDefaults *userDefaults;
@end
@implementation PacketTunnelProvider
// 懒加载
-(OpenVPNAdapter*)vpnAdapter{
if(!_vpnAdapter){
_vpnAdapter = [[OpenVPNAdapter alloc] init];
_vpnAdapter.delegate = self;
}
return _vpnAdapter;
}
-(OpenVPNReachability*)openVpnReach{
if(!_openVpnReach){
_openVpnReach = [[OpenVPNReachability alloc] init];
}
return _openVpnReach;
}
-(void)startTunnelWithOptions:(NSDictionary<NSString *,NSObject *> *)options completionHandler:(void (^)(NSError * _Nullable))completionHandler
{
NETunnelProviderProtocol *proto = (NETunnelProviderProtocol*)self.protocolConfiguration;
if(!proto){
return;
}
NSDictionary<NSString *,id> *provider = proto.providerConfiguration;
NSData * fileContent = provider[@"ovpn"];
// NSString * str1 = [[NSString alloc] initWithData:fileContent encoding:NSUTF8StringEncoding];
OpenVPNConfiguration *openVpnConfiguration = [[OpenVPNConfiguration alloc] init];
openVpnConfiguration.keyDirection = 1;
openVpnConfiguration.fileContent = fileContent;
// If true, don't send client cert/key to peer.
openVpnConfiguration.disableClientCert = NO;
// 用户名和密码进行认证
// openVpnConfiguration.settings = @{@"username":@"",@"password":@""};
// 如果要在暂停或重新连接期间保持TUN接口处于活动状态,请取消对此行的注释
// openVpnConfiguration.tunPersist = YES;
NSError *error;
OpenVPNProperties *evaluation = [self.vpnAdapter applyConfiguration:openVpnConfiguration error:&error];
if(error){
completionHandler(error);
return;
}
// 配置用户名和密码
if (!evaluation.autologin)
{
OpenVPNCredentials *tials = [[OpenVPNCredentials alloc]init];
tials.username = [NSString stringWithFormat:@"%@",[options objectForKey:@"username"]];
tials.password = [NSString stringWithFormat:@"%@",[options objectForKey:@"password"]];
[self.vpnAdapter provideCredentials:tials error:&error];
if(error){
completionHandler(error);
return;
}
}
[self.openVpnReach startTrackingWithCallback:^(OpenVPNReachabilityStatus status) {
if(status==OpenVPNReachabilityStatusReachableViaWiFi){
[self.vpnAdapter reconnectAfterTimeInterval:5];
}
}];
//建立连接并等待。关联事件
self.startHandler = completionHandler;
[self.vpnAdapter connect];
}
-(void)stopTunnelWithReason:(NEProviderStopReason)reason completionHandler:(void (^)(void))completionHandler
{
self.stopHandler = completionHandler;
if ([self.openVpnReach isTracking]) {
// vpn被主动关闭
[self.openVpnReach stopTracking];
}
[self.vpnAdapter disconnect];
}
- (void)openVPNAdapter:(nonnull OpenVPNAdapter *)openVPNAdapter configureTunnelWithNetworkSettings:(nullable NEPacketTunnelNetworkSettings *)networkSettings completionHandler:(nonnull void (^)(NSError * _Nullable))completionHandler {
__weak __typeof(self) weak_self = self;
[self setTunnelNetworkSettings:networkSettings completionHandler:^(NSError * _Nullable error) {
if(!error){
completionHandler(weak_self.packetFlow);
}
}];
}
- (void)openVPNAdapter:(nonnull OpenVPNAdapter *)openVPNAdapter handleError:(nonnull NSError *)error {
BOOL isOpen = (BOOL)[error userInfo][OpenVPNAdapterErrorFatalKey];
NSLog(@"isOpen = %d ",isOpen);
if(isOpen){
if (self.openVpnReach.isTracking) {
[self.openVpnReach stopTracking];
}
if (error)
{
self.startHandler(error);
}
self.startHandler = nil;
}
}
- (void)openVPNAdapter:(nonnull OpenVPNAdapter *)openVPNAdapter handleEvent:(OpenVPNAdapterEvent)event message:(nullable NSString *)message {
switch (event) {
case OpenVPNAdapterEventConnected:
{
if(self.reasserting){
self.reasserting = false;
}
self.startHandler(nil);
self.startHandler = nil;
}
break;
case OpenVPNAdapterEventDisconnected:
{
if (self.openVpnReach.isTracking) {
[self.openVpnReach stopTracking];
}
self.stopHandler();
self.stopHandler = nil;
}
break;
case OpenVPNAdapterEventReconnecting:
self.reasserting = true;
break;
default:
break;
}
}
@end
NEPacketTunnelFlow+NEPacketTunnelFlow_Extension.h
//
// NEPacketTunnelFlow+NEPacketTunnelFlow_Extension.h
// PacketTunnel
//
#import <NetworkExtension/NetworkExtension.h>
@interface NEPacketTunnelFlow ()<OpenVPNAdapterPacketFlow>
@end
- 下一步是父项目,分为初始化,建立连接,断开连接,监控状态
- 初始化,将XXX的配置信息进行保存,这里传的data,大概是如下格式:
client
dev tun
proto tcp或者udp
remote ip地址 端口
resolv-retry infinite
nobind
persist-key
persist-tun
remote-cert-tls server
auth SHA512
cipher AES-256-CBC
ignore-unknown-option block-outside-dns
block-outside-dns
verb 3
<ca>
-----BEGIN CERTIFICATE-----
密钥
-----END CERTIFICATE-----
</ca>
<cert>
密钥
-----END CERTIFICATE-----
</cert>
<key>
-----BEGIN PRIVATE KEY-----
密钥
-----END PRIVATE KEY-----
</key>
<tls-crypt>
-----BEGIN OpenVPN Static key V1-----
密钥
-----END OpenVPN Static key V1-----
</tls-crypt>
保存vpn相关的数据
/// 保存vpn相关的数据
/// @param data 数据
-(void)saveVpn:(NSData *)data
{
//加载与调用应用程序关联的所有应用程序代理配置,这些配置以前已保存到网络扩展首选项中。
[NETunnelProviderManager loadAllFromPreferencesWithCompletionHandler:^(NSArray<NETunnelProviderManager *> * _Nullable managers, NSError * _Nullable error) {
if (error) {
SSLog(@"Load Error: %@", error.description);
}
NETunnelProviderManager *manager;
if (managers.count > 0) {
manager = managers[0];
}else {
manager = [[NETunnelProviderManager alloc] init];
manager.protocolConfiguration = [[NETunnelProviderProtocol alloc] init];
}
NETunnelProviderProtocol *tunel = [[NETunnelProviderProtocol alloc]init];
// 获取文件内容
tunel.providerConfiguration = @{@"ovpn": data};
// 项目的Identifier
tunel.providerBundleIdentifier = @"这里是子项目的BundleIdentifier";
// serverAddress:即在手机设置的vpn中显示的vpn地址(服务器显示)
tunel.serverAddress = @"openXXX";
// tunel.username = @"username";
// tunel.identityDataPassword = @"password";
// 设备进入睡眠,vpn断开连接
tunel.disconnectOnSleep = YES;
// 是否可以编辑
[manager setEnabled:YES];
// 协议配置
[manager setProtocolConfiguration:tunel];
// 包含vpn描述的字符串(类型显示)
manager.localizedDescription = @"openXXX";
// 保存信息
SSLWeakSelf(self);
[manager saveToPreferencesWithCompletionHandler:^(NSError *error) {
if(error) {
SSLog(@"Save error: %@", error);
}else {
weakself.providerManagers = manager;
SSLog(@"add success");
//加载与调用应用程序关联的所有应用程序代理配置,这些配置以前已保存到网络扩展首选项中。
[manager loadFromPreferencesWithCompletionHandler:^(NSError * _Nullable error) {
SSLog(@"loadFromPreferences!");
}];
}
}];
}];
}
- 建立隧道,开始连接
-(void)connect
{
// 连接
[self.providerManagers loadFromPreferencesWithCompletionHandler:^(NSError * _Nullable error) {
if(!error){
NSError *error = nil;
[self.providerManagers.connection startVPNTunnelWithOptions:nil andReturnError:&error];
if(error) {
SSLog(@"Start error: %@", error.localizedDescription);
}else{
SSLog(@"Connection established!");
}
}
}];
}
- 断开连接
-(void)disconnectAction
{
// 断开连接
[self.providerManagers loadFromPreferencesWithCompletionHandler:^(NSError * _Nullable error) {
[self.providerManagers.connection stopVPNTunnel];
}];
}
4.监控XXX的状态
// 添加通知 - 连接信息改变时进行通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onVpnStateChange:) name:NEVPNStatusDidChangeNotification object:nil];
// 通知的方法
-(void)onVpnStateChange:(NSNotification *)Notification {
NEVPNStatus status = self.providerManagers.connection.status;
switch (status) {
case NEVPNStatusInvalid:
{
SSLog(@"连接无效");
}
break;
case NEVPNStatusDisconnected:
{
SSLog(@"未连接");
}
break;
case NEVPNStatusConnecting:
{
SSLog(@"正在连接");
}
break;
case NEVPNStatusConnected:
{
SSLog(@"已连接");
}
break;
case NEVPNStatusDisconnecting:
{
SSLog(@"断开连接中...");
}
break;
case NEVPNStatusReasserting:
{
SSLog(@"重新连接...");
}
break;
default:
break;
}
}
下面说一下父子项目之间怎么进行通信,其实最基本的方法就是两个项目读取本地保存的文件,需要在开发者中心添加app groups,如下图
添加groups
添加通信,这里只是举了一个例子,也可以自行百度(多Target之间goup通信)
#pragma mark - NSUserDefaults,进行通信
// 获取
- (void)getRewardTimeFromMain{
self.userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"这里填对应的bundle Indentifier"];
NSString *timeStr = [self.userDefaults objectForKey:@"key"];
}
// 保存
- (void)rewardTimeToMain:(NSInteger)timeNum{
[self.userDefaults setObject:@"value" forKey:@"key"];
[self.userDefaults synchronize];
}
代码实现 - Swift 版
父项目写了一个类,管理vpn的创建等步骤,代码如下:
VPNManager.swift
//
// VPNManager.swift
// VPNClient
//
// Created by wl on 2021/3/15.
//
import Foundation
import NetworkExtension
class VPNManager {
static let shared = VPNManager()
var manager: NETunnelProviderManager?
func connect() {
guard self.manager != nil else {
return
}
self.loadPreferences()
}
func disconnect() {
self.manager?.connection.stopVPNTunnel()
}
//加载已保存的NETunnelProvider configurations
func loadManager() {
NETunnelProviderManager.loadAllFromPreferences { (managers, error) in
guard error == nil else {
return
}
if let manager = managers?.first {
self.manager = manager
} else {
//新建
self.manager = NETunnelProviderManager()
self.manager?.localizedDescription = "myVPN"
}
print("VPNManager 初始化完成")
}
}
//加载当前vpn配置
func loadPreferences() {
guard let manager = self.manager else {
return
}
self.manager?.loadFromPreferences { (error) in
guard error == nil else {
return
}
// 如果没有对应的配置,我们需要新建配置
if manager.protocolConfiguration == nil {
manager.protocolConfiguration = self.newConfiguration()
}
// 设置完isEnabled需要保存配置,启动当前配置
manager.isEnabled = true
manager.saveToPreferences { (error) in
guard error == nil else {
// 用户拒绝保存等情况,清空配置
manager.protocolConfiguration = nil
return
}
// 保存完成后我们需要重新加载配置,进行连接,
//https://stackoverflow.com/questions/47550706/error-domain-nevpnerrordomain-code-1-null-while-connecting-vpn-server
self.loadPreferencesAndStartTunnel()
}
}
}
func loadPreferencesAndStartTunnel() {
self.manager?.loadFromPreferences(completionHandler: { (error) in
guard error == nil else {
return
}
self.startTunnel()
})
}
private func startTunnel() {
do {
try self.manager?.connection.startVPNTunnel()
} catch {
print(error)
}
}
func newConfiguration() -> NETunnelProviderProtocol {
//加载ovpn文件
guard
let configurationFileURL = Bundle.main.url(forResource: "vpnclient", withExtension: "ovpn"),
let configurationFileContent = try? Data(contentsOf: configurationFileURL)
else {
fatalError()
}
let tunnelProtocol = NETunnelProviderProtocol()
tunnelProtocol.serverAddress = ""
//指定network extension 确保bundleIdentifier和network extension的id一致
tunnelProtocol.providerBundleIdentifier = "com.starpavilionlimited.freeouterspace.TargetTunnel"
tunnelProtocol.providerConfiguration = ["ovpn": configurationFileContent]
return tunnelProtocol
}
private init(){
}
}
子项目则是建立隧道用的,代码如下:
//
// PacketTunnelProvider.swift
// vpn-tunnel
//
import NetworkExtension
import UIKit
import OpenVPNAdapter
extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {}
class PacketTunnelProvider: NEPacketTunnelProvider {
lazy var vpnAdapter: OpenVPNAdapter = {
let adapter = OpenVPNAdapter()
adapter.delegate = self
return adapter
}()
let vpnReachability = OpenVPNReachability()
var startHandler: ((Error?) -> Void)?
var stopHandler: (() -> Void)?
override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
guard
let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration
else {
fatalError()
}
guard let ovpnFileContent: Data = providerConfiguration["ovpn"] as? Data else {
fatalError()
}
let configuration = OpenVPNConfiguration()
configuration.fileContent = ovpnFileContent
// Uncomment this line if you want to keep TUN interface active during pauses or reconnections
// configuration.tunPersist = true
do {
try vpnAdapter.apply(configuration: configuration)
} catch {
completionHandler(error)
return
}
// Checking reachability. In some cases after switching from cellular to
// WiFi the adapter still uses cellular data. Changing reachability forces
// reconnection so the adapter will use actual connection.
vpnReachability.startTracking { [weak self] status in
guard status == .reachableViaWiFi else { return }
self?.vpnAdapter.reconnect(afterTimeInterval: 5)
}
// Establish connection and wait for .connected event
startHandler = completionHandler
// cocoapos 倒入0.8版本就需要换方法了
// vpnAdapter.connect(using: packetFlow)
vpnAdapter.connect();
}
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
stopHandler = completionHandler
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
vpnAdapter.disconnect()
}
}
extension PacketTunnelProvider: OpenVPNAdapterDelegate {
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?, completionHandler: @escaping (OpenVPNAdapterPacketFlow?) -> Void) {
networkSettings?.dnsSettings?.matchDomains = [""]
setTunnelNetworkSettings(networkSettings) { error in
completionHandler(self.packetFlow);
}
}
// OpenVPNAdapter calls this delegate method to configure a VPN tunnel.
// `completionHandler` callback requires an object conforming to `OpenVPNAdapterPacketFlow`
// protocol if the tunnel is configured without errors. Otherwise send nil.
// `OpenVPNAdapterPacketFlow` method signatures are similar to `NEPacketTunnelFlow` so
// you can just extend that class to adopt `OpenVPNAdapterPacketFlow` protocol and
// send `self.packetFlow` to `completionHandler` callback.
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?, completionHandler: @escaping (Error?) -> Void) {
// In order to direct all DNS queries first to the VPN DNS servers before the primary DNS servers
// send empty string to NEDNSSettings.matchDomains
networkSettings?.dnsSettings?.matchDomains = [""]
// Set the network settings for the current tunneling session.
setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler)
}
// Process events returned by the OpenVPN library
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleEvent event: OpenVPNAdapterEvent, message: String?) {
switch event {
case .connected:
if reasserting {
reasserting = false
}
guard let startHandler = startHandler else { return }
startHandler(nil)
self.startHandler = nil
case .disconnected:
guard let stopHandler = stopHandler else { return }
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
stopHandler()
self.stopHandler = nil
case .reconnecting:
reasserting = true
default:
break
}
}
// Handle errors thrown by the OpenVPN library
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) {
// Handle only fatal errors
guard let fatal = (error as NSError).userInfo[OpenVPNAdapterErrorFatalKey] as? Bool, fatal == true else {
return
}
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
if let startHandler = startHandler {
startHandler(error)
self.startHandler = nil
} else {
cancelTunnelWithError(error)
}
}
// Use this method to process any log message returned by OpenVPN library.
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleLogMessage logMessage: String) {
// Handle log messages
}
}
控制器视图只有两个按钮,对应着下面代码中的connect和dissconnect,直接上代码:
//
// SwiftViewController.swift
//
import UIKit
import NetworkExtension
class SwiftViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
VPNManager.shared.loadManager()
NotificationCenter.default.addObserver(self, selector: #selector(statusChange), name: .NEVPNStatusDidChange, object: nil)
}
@objc func statusChange() {
guard let manager = VPNManager.shared.manager else {
return
}
switch manager.connection.status {
case .connected:
print("已连接")
case .connecting:
print("正在连接")
case .disconnected:
print("未连接")
case .disconnecting:
print("正在断开连接")
default:
print("其他状态")
}
}
@IBAction func dissconnect(_ sender: UIButton) {
VPNManager.shared.disconnect()
}
@IBAction func connect(_ sender: UIButton) {
VPNManager.shared.connect()
}
}
附上demo地址,有需要可以下载。
注意!!!需要自己在开发者中心申请Bundle Identifier,进行替换,项目中有OC和Swift的,在运行时先删除对应的,项目结构如下:
到此基本就完成了,这里提供一些参考连接以供使用,都是干货
我自己使用的是OC,但是在子Target中,用了Swift。具体为什么用这个,打个哑谜,你们自己试一试就知道了。
哦了,就这么多,有问题请指出。