注意:由于簡大叔對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
2、創建完成后需要將主項目和子項目的Bundle Identifier進行替換,這里填寫的是開發者中心創建好的Bundle Identifier,填寫完成后父子項目添加相應的Capability,如下圖
代碼實現 - 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,如下圖
添加通信,這里只是舉了一個例子,也可以自行百度(多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。具體為什么用這個,打個啞謎,你們自己試一試就知道了。
哦了,就這么多,有問題請指出。