本例需求 : iOS建立簡單的VPN連線併成功攔截IP資料包pakcet.
建議:建議閱讀本文前先仔細閱讀並理解下App extension原理,有助於在專案中解決很多問題。App extension總結
注意:Personal-VPN功能是蘋果開發者賬號才有許可權開啟,所以如果沒有開發者賬號次Demo無法執行
本Demo中已經測試可以通過,只需要下載Demo並按照Demo中所說提供兩個合法的bundle id即可執行。
GitHub地址(附程式碼) : XDXVPNExtensionDemo
簡書地址 : XDXVPNExtensionDemo
部落格地址 : XDXVPNExtensionDemo
掘金地址 : XDXVPNExtensionDemo
一. App extension 定義及工作原理
1. App extension, 更多請了解App extension總結
-
定義:App Extension 可以讓開發者們擴充自定義的功能和內容到應用程式之外,並在使用者與其他應用程式或系統互動時提供給使用者。
-
蘋果定義了很多種app extension, 每一種不同功能的app extension 稱為extension point,其中我們要學習的VPN Extension就是一種extension point。
二. VPN extension 簡介
1. 背景
我們先來簡單瞭解一下國內牆的原理
2. 定義
蘋果提供的VPN Extension可以幫助我們配置VPN通道,自定義和擴充核心網路功能。
蘋果官方文件中定義如下
3.使用框架
NetworkExtension :包含在iOS和macOS中用於自定義和擴充核心網路功能的API集合。
其中我們要講的是NetworkExtension框架中的Personal VPN服務。
-
Personal VPN
NEVPNManager API提供給我們去建立和管理個人VPN的配置。它通常用來向使用者提供服務確保使用者安全的上網,例如使用公共場所的wifi。
4. 核心API
-
NETunnelProviderManager 和 NEPacketTunnelProvider
在 iOS 9 中,開發者可以用 NETunnelProvider 擴充套件核心網路層,從而實現非標準化的私密VPN技術。最重要的兩個類是 NETunnelProviderManager 和 NEPacketTunnelProvider。
二. VPN extension 建立步驟
1. 新建Target
在已經建立好的專案中新建Target
- Target : 指定了應用程式中構建product的設定資訊和檔案,即包含App extension point的程式碼集合。
Xcode會為每一種extension point提供一個模板,每種模板裡會提供特定的原始檔(原始檔中會包含一些示例程式碼)和設定資訊,build這個target將會生成一個指定的二進位制檔案被新增到app’s bundle中。
在彈出列表中選擇Packet Tunnel Provider,注意在最新的Xcode中該模板已經被取消,需要我們手動下載,安裝好後重啟Xcode即可,這裡提供下載連結連結:
https://pan.baidu.com/s/1V3xIljdRedmqGyp5QSwbTA
密碼: ne8x
複製程式碼
選擇好後我們可以看到我們專案中的變化如下:
我們的專案中增加了對Target的配置,左側專案工程檔案中增加了系統為我們自動生成的模板,注意如果在新建Target時選擇的是Ojective-C語言模板中有一處錯誤資訊,因為與我們要實現的並無關聯,模板中程式碼我們均不使用,忽略即可。
2. 刪除模板中多餘程式碼
由於我們這裡只演示簡單的建立連線的過程,所以模板中生成的很多程式碼使用不到,我們只需要留下如下程式碼。
- (void)startTunnelWithOptions:(NSDictionary *)options completionHandler:(void (^)(NSError *))completionHandler
{
}
- (void)stopTunnelWithReason:(NEProviderStopReason)reason completionHandler:(void (^)(void))completionHandler
{
// Add code here to start the process of stopping the tunnel
[self.connection cancel];
completionHandler();
}
複製程式碼
在這裡我們需要建立和斷開連線的回撥即可。此時Build通過。
3. 配置VPN所需資訊
3-1. 配置簽名資訊,開啟許可權
-
Personal-VPN功能是蘋果開發者賬號才有許可權開啟,所以如果沒有開發者賬號此Demo無法執行。
-
Bundle Identifier不可隨意寫,需要按照規範com.xxx.xxx形式來書寫,否則Build會出錯
-
因為vpn extension並不是一個獨立的app,所以建立target時字首必須為主app的字首再.extensionName.
主app Bundle Identifier : com.xdx.XDXRouterDemo.XDXVPNExtensionDemo
extension Bundle Identifier : com.xdx.XDXRouterDemo.XDXVPNExtensionDemo.XDXVPNTunnelDemo
複製程式碼
3-2 開啟許可權
- 因為personal vpn許可權較高,不能直接在Xcode中開啟,所以我們需要先登入蘋果開發者中心
-
需要手動為主工程的target和vpn的target建立父子App ID。建立APP ID的方法網上較多,不再說明。
-
在建立APP ID時開啟Service 中的 Network Extensions 和 Personal VPN兩個功能。對於已經建立好的APP ID需要在編輯中增加這兩個Service。
注意:主target和extension target 對應的app id中都需要開啟這兩個許可權。
- 建立好APP ID後需要繼續建立Profile檔案,如果是已經存在的app id中增加了這兩項服務後需要重新啟用配置檔案Profile。
3-3 在Xcode中配置VPN
- 在兩個Target中選擇正確的Profile。(前提是在3-2中正確配置了APP ID並開啟了對應兩項服務)由於我們只是測試,所以這裡只配置了開發證書.
- 在Capabilities中開啟Personal VPN 和 Network Extension(同時如圖勾選前三項),然後你的兩個Target專案檔案中會新增兩個.entitlements結尾的配置檔案。
4. 實現步驟
4-1 匯入必需框架
在專案中匯入NetworkExtension.framework框架
之後在主控制器匯入#import <NetworkExtension/NetworkExtension.h>
4-1 初始化並配置NETunnelProviderManager物件
- NETunnelProviderManager :配置並控制由Tunnel Provider app extension提供的VPN 連線。
可以理解為建立VPN連線前負責配置基本引數資訊儲存設定到系統(即一般vpn app中都會在第一次開啟時授權並儲存到系統的VPN設定中)
- 包含Packet Tunnel Provider extension的containing app使用NETunnelProviderManager去建立和管理使用自定義協議配置VPN。
注意:配置好的NETunnelProviderManager物件僅在系統設定中起展示作用,或者說這裡的設定並不真正生效,真正生效的設定在app extension target中,如果為了保持一致,如果僅僅測試,配置資訊可以隨便填寫(但target bundle id 必須為專案target真實的ID),如果為了與vpn真正配置保持一致,可填寫正確地址。
知識點,瞭解可跳過
知識點 1. 區別
NETunnelProviderManager類繼承了 NEVPNManager大多數基本的功能,主要的區別如下:
- protocolConfiguration 屬性只有 NETunnelProviderProtocol類才能設定
- connection這個只讀屬性只能通過 NETunnelProviderSession這個類設定。
每個NETunnelProviderManager例項對應一個VPN配置被儲存在 Network Extension偏好設定中。可以建立多個NETunnelProviderManager例項來管理多個VPN配置的。
使用NETunnelProviderManager建立的VPN配置被歸屬為常規企業VPN配置(而不是由NEVPNManager建立的個人VPN)。一次只能在系統啟用一個VPN配置。如果同時在系統中啟用個人VPN和企業VPN,企業VPN優先使用。
知識點 2. 將網路資料路由到VPN兩種方式
-
IP 地址
這是預設的路由方式。IP通道通過Packet Tunnel Provider extension被標記在VPN通道完全被建立。
-
By source application (Per-App VPN)
Per-App VPN 應用程式規則同時用於路由規則和VPN 需求規則。這與基於路由的IP地址形成鮮明的對比,當onDemandEnabled屬性被設定成true並且app匹配he Per-App VPN規則試圖通過網路進行通訊,VPN將自動開始。
4-2 初始化NETunnelProviderManager物件
-
設定NETunnelProviderManager所需的各個引數的值
在本例中利用各個引數的值均用模型實現,在主控制器直接呼叫以下即可
注意
- TunnelBundleId 需要傳入VPN Extension Target 的BundleID
- serverAddress:即在手機設定的VPN中顯示的VPN地址
- 其餘引數根據真實情況填寫,如果只是測試即可直接用以下引數(或設定其他引數也可)
- 以下引數只是在系統設定中VPN設定裡顯示的引數,真正設定網路引數在VPN Target專案中重新設定。
[model configureInfoWithTunnelBundleId:@"com.xxx.xxx.xxxx"
serverAddress:@"10.10.10.1"
serverPort:@"54345"
mtu:@"1400"
ip:@"10.8.0.2"
subnet:@"255.255.255.0"
dns:@"8.8.8.8,8.4.4.4"];
複製程式碼
而在封裝管理NETunnelProviderManager物件真正起作用的是如下程式碼
- (void)applyVpnConfiguration {
[NETunnelProviderManager loadAllFromPreferencesWithCompletionHandler:^(NSArray<NETunnelProviderManager *> * _Nullable managers, NSError * _Nullable error) {
if (managers.count > 0) {
self.vpnManager = managers[0];
log4cplus_error("XDXVPNManager", "The vpn already configured. We will use it.");
return;
}else {
log4cplus_error("XDXVPNManager", "The vpn config is NULL, we will config it later.");
}
[self.vpnManager loadFromPreferencesWithCompletionHandler:^(NSError * _Nullable error) {
if (error != 0) {
const char *errorInfo = [NSString stringWithFormat:@"%@",error].UTF8String;
log4cplus_error("XDXVPNManager", "applyVpnConfiguration loadFromPreferencesWithCompletionHandler Failed - %s !",errorInfo);
return;
}
NETunnelProviderProtocol *protocol = [[NETunnelProviderProtocol alloc] init];
protocol.providerBundleIdentifier = self.vpnConfigurationModel.tunnelBundleId;
NSMutableDictionary *configInfo = [NSMutableDictionary dictionary];
[configInfo safeSetObject:self.vpnConfigurationModel.serverPort forKey:@"port"];
[configInfo safeSetObject:self.vpnConfigurationModel.serverAddress forKey:@"server"];
[configInfo safeSetObject:self.vpnConfigurationModel.ip forKey:@"ip"];
[configInfo safeSetObject:self.vpnConfigurationModel.subnet forKey:@"subnet"];
[configInfo safeSetObject:self.vpnConfigurationModel.mtu forKey:@"mtu"];
[configInfo safeSetObject:self.vpnConfigurationModel.dns forKey:@"dns"];
protocol.providerConfiguration = configInfo;
protocol.serverAddress = self.vpnConfigurationModel.serverAddress;
self.vpnManager.protocolConfiguration = protocol;
self.vpnManager.localizedDescription = @"NEPacketTunnelVPNDemoConfig";
[self.vpnManager setEnabled:YES];
[self.vpnManager saveToPreferencesWithCompletionHandler:^(NSError * _Nullable error) {
if (error != 0) {
const char *errorInfo = [NSString stringWithFormat:@"%@",error].UTF8String;
log4cplus_error("XDXVPNManager", "applyVpnConfiguration saveToPreferencesWithCompletionHandler Failed - %s !",errorInfo);
}else {
[self applyVpnConfiguration];
log4cplus_info("XDXVPNManager", "Save vpn configuration successfully !");
}
}];
}];
}];
}
複製程式碼
以上程式碼通過NETunnelProviderManager的類方法+ (void)loadAllFromPreferencesWithCompletionHandler:(void (^)(NSArray<NETunnelProviderManager *> * __nullable managers, NSError * __nullable error))completionHandler
實現
- 如果是第一次配置VPN,VPN的設定未儲存在系統中,所以上述程式碼managers.count讀取結果應該為0,需要我們做如上設定,第一次設定成功後,VPN的配置資訊會儲存在設定資訊裡,所以以後無需重複設定,只需要讀取到就好。
- 注意,第一次配置時需要手機授權,如果拒絕則無法繼續進行。
此函式非同步讀取呼叫所有app建立且先前儲存在本地的NETunnelProvider配置資訊,並將它們在回撥中以一個存放NETunnelProvider物件的陣列的形式返回。
+ (void)loadAllFromPreferencesWithCompletionHandler:(void (^)(NSArray<NETunnelProviderManager *> * __nullable managers, NSError * __nullable error))completionHandler
該函式從呼叫者的VPN設定中載入當前VPN配置
- (void)loadFromPreferencesWithCompletionHandler:(void (^)(NSError * __nullable error))completionHandler;
複製程式碼
然後將存入模型的所有引數放入一個字典,並存入NETunnelProviderProtocol物件的providerConfiguration中,該字典在tunnel建立成功後會傳遞給NETunnelProviders物件。最後將配置好的NETunnelProviderProtocol物件賦值給NETunnelProviderManager物件的protocolConfiguration即可。
呼叫此方法用來儲存上述配置好的VPN到本機裝置的VPN設定中。
- (void)saveToPreferencesWithCompletionHandler:(nullable void (^)(NSError * __nullable error))completionHandler;
複製程式碼
呼叫此函式會使用NEVPNConnection物件當前VPN配置來建立VPN連線。返回YES表示成功。
[self.vpnManager.connection startVPNTunnelAndReturnError:&error];
- (BOOL)startVPNTunnelAndReturnError:(NSError **)error;
呼叫此函式會關閉當前建立的VPN連線。
[self.vpnManager.connection stopVPNTunnel];
- (void)stopVPNTunnel;
複製程式碼
4-3. 主控制器啟動VPN
-
在viewDidLoad中完成初始化操作
- 初始化模型即根據實際情況對各個網路引數賦值(注意TunnelBundleId為本專案VPN Extension的BundleID)
- 對XDXVPNManager進行初始化配置模型,設定代理。
- 註冊通知(NEVPNStatusDidChangeNotification),監聽VPN狀態變化
-
在
- (void)vpnDidChange:(NSNotification *)notification
方法中根據VPN的狀態動態改變按鈕狀態 -
點選按鈕實現開啟/關閉VPN連線
4-4.在PacketTunnelProvider回撥中完成剩餘工作
- 當我們在主控器呼叫開啟VPN連線後,會進入VPN Target專案中PacketTunnelProvider檔案的下述方法中
當一個新的通道被建立會會呼叫此函式。我們必須通過重寫該類來完成建立連線。
@param : options - 在主控制呼叫開啟VPN連線時傳入的字典,可空。
@param : completionHandler - 在該方法徹底完成時必須呼叫這個Block。如果不能建立連線要將錯誤資訊傳給該block,如果成功建立連線則只需將nil傳給該Block.
- (void)startTunnelWithOptions:(nullable NSDictionary<NSString *,NSObject *> *)options completionHandler:(void (^)(NSError * __nullable error))completionHandler
複製程式碼
注意:因為此時是處於另一個Target中,在手機相當於另一條程式,因此我們無法直接在控制檯看到任何我們列印的log資訊,這裡我使用log4cplus則可以在我們的終端中來截獲debug資訊。如果你的本機裝有log4cplus直接使用下面的命令即可獲取資訊,若未裝則可以在github裡直接搜尋log4cplus mac版直接執行,也可以在log4cplus mac版的控制檯裡過濾關鍵字得到debug資訊。
$ deviceconsole |grep XDXVPNManager
複製程式碼
#define XDX_NET_MTU 1400
#define XDX_NET_REMOTEADDRESS "192.168.3.14"
#define XDX_NET_SUBNETMASKS "255.255.255.255"
#define XDX_NET_DNS "223.5.5.5"
#define XDX_LOCAL_ADDRESS "127.0.0.1"
#define XDX_NET_TUNNEL_IPADDRESS "10.10.10.10"
- (void)startTunnelWithOptions:(NSDictionary *)options completionHandler:(void (^)(NSError *))completionHandler
{
log4cplus_info("XDXVPNManager", "XDXPacketTunnelManager - Start Tunel !");
NEPacketTunnelNetworkSettings *tunnelNetworkSettings = [[NEPacketTunnelNetworkSettings alloc] initWithTunnelRemoteAddress:@XDX_NET_REMOTEADDRESS];
tunnelNetworkSettings.MTU = [NSNumber numberWithInteger:XDX_NET_MTU];
tunnelNetworkSettings.IPv4Settings = [[NEIPv4Settings alloc] initWithAddresses:[NSArray arrayWithObjects:@XDX_NET_TUNNEL_IPADDRESS, nil] subnetMasks:[NSArray arrayWithObjects:@XDX_NET_SUBNETMASKS, nil]];
tunnelNetworkSettings.IPv4Settings.includedRoutes = @[[NEIPv4Route defaultRoute]];
NEIPv4Settings *excludeRoute = [[NEIPv4Settings alloc] initWithAddresses:[NSArray arrayWithObjects:@"10.10.10.11", nil] subnetMasks:[NSArray arrayWithObjects:@XDX_NET_SUBNETMASKS, nil]];
tunnelNetworkSettings.IPv4Settings.excludedRoutes = @[excludeRoute];
[self setTunnelNetworkSettings:tunnelNetworkSettings completionHandler:^(NSError * _Nullable error) {
if (error == nil) {
log4cplus_info("XDXVPNManager", "XDXPacketTunnelManager - Start Tunel Success !");
completionHandler(nil);
}else {
log4cplus_error("XDXVPNManager", "XDXPacketTunnelManager - Start Tunel Failed - %s !",error.debugDescription.UTF8String);
completionHandler(error);
return;
}
}];
[self.packetFlow readPacketsWithCompletionHandler:^(NSArray<NSData *> * _Nonnull packets, NSArray<NSNumber *> * _Nonnull protocols) {
log4cplus_debug("XDXVPNManager", "XDXPacketTunnelManager - Read Packet !");
[packets enumerateObjectsUsingBlock:^(NSData * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSString *packetStr = [NSString stringWithFormat:@"%@",obj];
log4cplus_debug("XDXVPNManager", "XDXPacketTunnelManager - Read Packet - %s !",packetStr.UTF8String);
}];
}];
}
複製程式碼
當你上述的每一步都正確配置,並在主控器中呼叫-startVPNTunnelAndReturnError方法時,緊接著會在vpn target中呼叫以上回撥,我們需要在此回撥中建立tunnel並配置我們需要的各種網路設定,最後在呼叫- (void)setTunnelNetworkSettings
回撥中呼叫completionHandler(nil);
來建立vpn連線。
注意,在設定網路引數成功後必須顯式呼叫completionHandler(nil)才能正常建立連線(官方API要求),如果設定網路引數出錯則將錯誤資訊傳入completionHandler(error),否則無法正常建立連線。
這裡是真正設定vpn tunnel 網路引數的地方,主控制器中設定的僅為系統vpn設定中的展示名稱
- 首先新建一個NEPacketTunnelNetworkSettings物件,此物件是用來設定並建立vpn tunnel初始化物件時提供的RemoteAddress即為我們要建立連線的遠端伺服器的地址,注意如果是域名需要手動解析為IP地址再傳入。下面提供了方法可轉換。
- MTU:最大傳輸單元,即每個packet最大的容量
- includedRoutes:即vpn tunnel需要攔截包的地址,如果全部攔截則設定[NEIPv4Route defaultRoute],也可以指定部分需要攔截的地址
- excludeRoute : 設定不攔截哪些包的地址,預設不會攔截tunnel本身地址發出去的包。
- 最後呼叫
- (void)setTunnelNetworkSettings
,在回撥若成功直接呼叫completionHandler(nil);
來建立vpn連線。
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
// 域名轉換IP
- (NSString *)queryIpWithDomain:(NSString *)domain {
struct hostent *hs;
struct sockaddr_in server;
if ((hs = gethostbyname([domain UTF8String])) != NULL) {
server.sin_addr = *((struct in_addr*)hs->h_addr_list[0]);
return [NSString stringWithUTF8String:inet_ntoa(server.sin_addr)];
}
return nil;
}
複製程式碼
在呼叫完setTunnelNetworkSettings後如果回撥返回error且我們按照上述所說完成completionHandler(nil);的配置後,vpn成功建立連線,此時我們的app狀態列裡也會相應出現vpn的標識,如下圖。
- 附加步驟:在VPN Tunndel 中持續讀取packet包 因為我們已經成功建立了vpn連線,所以網路資料包我們可以在此回撥中擷取到
利用NEPacketTunnelProvider物件的packetFlow 完成下面的方法即可。
從流中讀入可用的IP包
- (void)readPakcets {
__weak PacketTunnelProvider *weakSelf = self;
[self.packetFlow readPacketsWithCompletionHandler:^(NSArray<NSData *> * _Nonnull packets, NSArray<NSNumber *> * _Nonnull protocols) {
for (NSData *packet in packets) {
// log4cplus_debug("XDXVPNManager", "Read Packet - %s",[NSString stringWithFormat:@"%@",packet].UTF8String);
__typeof__(self) strongSelf = weakSelf;
// TODO ...
NSLog(@"XDX : read packet - %@",packet);
}
[weakSelf readPakcets];
}];
}
複製程式碼
以下為我讀到的包
另外如需Debug app extension target可以通過Xcode如下方法
- 停止方法較為簡單,不再敘述,詳細可在Demo中查閱