注意:由于简大叔对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'
target 'TargetTunnel' do
use_frameworks!
pod 'OpenVPNAdapter', :git => '
https://github.com/ss-abramchuk/OpenVPNAdapter.git'
, :tag => '0.4.0'
1、创建Target
@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; NS_ASSUME_NONNULL_ENDPacketTunnelProvider.m
// PacketTunnelProvider.m // TargetTunnel #import "PacketTunnelProvider.h" #include "NEPacketTunnelFlow+NEPacketTunnelFlow_Extension.h" @interface PacketTunnelProvider ()<OpenVPNAdapterDelegate> // 这个先放下,一会讲,主要用到和父项目进行通信 @property (strong,nonatomic) NSUserDefaults *userDefaults; @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;NEPacketTunnelFlow+NEPacketTunnelFlow_Extension.h
// NEPacketTunnelFlow+NEPacketTunnelFlow_Extension.h // PacketTunnel #import <NetworkExtension/NetworkExtension.h> @interface NEPacketTunnelFlow ()<OpenVPNAdapterPacketFlow>
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
-----BEGIN CERTIFICATE-----
-----END CERTIFICATE-----
-----END CERTIFICATE-----
</cert>
-----BEGIN PRIVATE KEY-----
-----END PRIVATE 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,如下图
- (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() {
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
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的,在运行时先删除对应的,项目结构如下: