App extension實戰 - Personal VPN 連線並捕獲packet

小東邪發表於2018-06-23

本例需求 : 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. 背景

我們先來簡單瞭解一下國內牆的原理

network1

network2

2. 定義

蘋果提供的VPN Extension可以幫助我們配置VPN通道,自定義和擴充核心網路功能。

蘋果官方文件中定義如下

5-VPN Extension

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中。

1-NewTarget

在彈出列表中選擇Packet Tunnel Provider,注意在最新的Xcode中該模板已經被取消,需要我們手動下載,安裝好後重啟Xcode即可,這裡提供下載連結連結:

https://pan.baidu.com/s/1V3xIljdRedmqGyp5QSwbTA
密碼: ne8x
複製程式碼

2-extensionList

選擇好後我們可以看到我們專案中的變化如下:

4-target

我們的專案中增加了對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中開啟,所以我們需要先登入蘋果開發者中心

8-selectAccount

9-selectProfile

  • 需要手動為主工程的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結尾的配置檔案。

12-XcodeConfig

4. 實現步驟

4-1 匯入必需框架

在專案中匯入NetworkExtension.framework框架

13-exportNE

之後在主控制器匯入#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的標識,如下圖。

14-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];
    }];
}
    
複製程式碼

以下為我讀到的包

read packet

另外如需Debug app extension target可以通過Xcode如下方法

Debug

Debug info

  • 停止方法較為簡單,不再敘述,詳細可在Demo中查閱

總結:此Demo為利用蘋果官方提供的VPN Extension在一個專案中通過新建Target來完成一個簡單建立VPN的過程。其中涉及到App extension的知識可在本文所附的另一篇文章中查閱,主要涉及App extension在專案中的配置,以及建立VPN連線時一些引數的設定及方法的呼叫時機,以及成功建立連線後可攔截手機中的網路Packet。

相關文章