OC网络学习11:Network Extension探索

张建 lol

前言

苹果提供的 Network Extension 可以帮助我们配置VPN通道,自定义和拓展核心网络功能。

Network Extension 功能类型

Network Extension 一共提供了 App Proxy、Content Filter、Packet Tunnel、DNS Proxy、DNS Settings 五种功能。

  • App Proxy:Proxy TCP and UDP network connections

App 代理:代理TCP和UDP网络连接

  • Content Filter:Allow or deny network connections

内容过滤:允许或拒绝网络连接

  • Packet Tunnel:Tunnel IP packets using a tunneling network protocol

Packet隧道:可以实现本地连接并成功拦截IP数据包packets

  • DNS Proxy:Proxy DNS queries

DNS 代理:代理DNS查询

  • DNS Settings:Configure settings for encrypted DNS

DNS设置:配置加密DNS

Packet Tunnel Provider(包隧道提供者)

  1. 介绍

Implement a VPN client for a packet-oriented, custom VPN protocol.

为面向包的自定义VPN协议,实现VPN客户端

Overview

  1. 概述

A virtual private network (VPN) is a form of network tunnel where a VPN client uses the public Internet to create a connection to a VPN server and then passes private network traffic over that connection.

虚拟专用网络(VPN)是一种网络隧道形式,VPN客户端使用公共互联网去创建与VPN服务器的连接,然后通过该连接传递私有网络流量

If you want to build a VPN client that implements a packet-oriented,custom VPN protocol, create a packet tunnel provider app extension.

如果你想构建一个 实现面向包的的VPN客户端,自定义VPN协议,创建一个 包隧道提供者 App 扩展

When the system starts a VPN configuration that uses your packet tunnel provider, it performs the following steps:

当系统使用 包隧道提供者 来启动VPN配置时,将执行以下步骤:

Launches your app extension.

启动你的App扩展

Instantiates your packet tunnel provider subclass within that app extension.

在App扩展中实例化你的 包隧道提供者子类

Starts forwarding packets to your provider.

开始将数据包转发到你的 提供者

Your provider should open a tunnel to a VPN server and send those packets over that tunnel.

你的 提供者 应该打开通往VPN服务器的隧道,并通过该隧道发送这些数据包

Similarly, if your provider receives packets from the tunnel, it should pass them back to the system.

类似地,如果您的 提供者 从隧道接收到数据包,它应该将它们传回系统

Packet tunnel providers can run in destination IP mode or source-application mode.

包隧道提供者 能以 目的IP模式 或 源应用模式 运行

The latter is one form of per-app VPN (the other form is an App Proxy Provider).

后者是一种 per-app VPN 形式(另一种形式是 App Proxy Provider)

For detailed information about packet tunnel provider deployment options, see TN3134: Network Extension provider deployment.

关于 包隧道提供者 部署选项 的详细信息,请参见 TN3134: Network Extension provider deployment.

Note

请注意

When a VPN configuration is active, connections use the VPN instead of iCloud Private Relay.

当VPN配置处于激活状态时,连接将使用VPN而不是 iCloud私有中继

Network Extension providers also don’t use iCloud Private Relay.

Network Extension providers 也不使用 iCloud私有中继

  1. Packet Tunnel Provider
  • NEPacketTunnelProvider

The principal class for a packet tunnel provider app extension.

packet tunnel provider App 扩展 的主体类

  • NETunnelProvider

An abstract base class shared by NEPacketTunnelProvider and NEAppProxyProvider.

一个由 NEPacketTunnelProvider 和 NEAppProxyProvider 共享的抽象基类

  • NEProvider

An abstract base class for all NetworkExtension providers.
NEPacketTunnelNetworkSettings

所有 NetworkExtension 提供商 的抽象基类

  • NEPacketTunnelNetworkSettings

The configuration for a packet tunnel provider’s virtual interface.

包隧道提供商的虚拟配置接口

  • NETunnelNetworkSettings

The configuration for a tunnel provider’s virtual interface.

VPN

  1. 核心技术
  • 苹果提供了 Network Extension 可以帮助我们配置 VPN 通道。

  • 在iOS9中,开发者可以用 NETunnelProvider 扩展核心网络层,从而 实现非标准化的私密VPN 技术,最重要的两个类是 NETunnelProviderManagerNEPacketTunnelProvider

  1. 创建 Network Extension 步骤
  • 新建 Target

在已经创建好的项目中新建 Target:

创建完成后,TARGET 下会增加一个名为 Tunnel 的扩展,左侧项目中系统会为我们自动生成对应的模板:

  1. PacketTunnelProvide 模板中的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#pragma mark - 启动VPN时调用
/*
当一个新的通道被建立会会调用此函数。我们必须通过重写该类来完成建立连接。
@param : options - 在主控制调用开启VPN连接时传入的字典,可空。
@param : completionHandler - 在该方法彻底完成时必须调用这个Block。如果不能建立连接要将错误信息传给该block,如果成功建立连接则只需将nil传给该Block.
*/
- (void)startTunnelWithOptions:(NSDictionary *)options completionHandler:(void (^)(NSError *))completionHandler {
NSLog(@"=====启动VPN时调用 startTunnelWithOptions=====");
}

#pragma mark - 停止VPN时调用
- (void)stopTunnelWithReason:(NEProviderStopReason)reason completionHandler:(void (^)(void))completionHandler {
NSLog(@"=====停止VPN时调用 stopTunnelWithReason=====");
completionHandler();
}

#pragma mark - 处理App消息时调用
- (void)handleAppMessage:(NSData *)messageData completionHandler:(void (^)(NSData *))completionHandler {
// Add code here to handle the message.
NSLog(@"=====处理App消息时调用:=====\n%@",messageData);
}

#pragma mark - 准备休眠时调用
- (void)sleepWithCompletionHandler:(void (^)(void))completionHandler {
// Add code here to get ready to sleep.
NSLog(@"=====准备休眠时调用:=====");
completionHandler();
}

#pragma mark - 被唤醒时调用
- (void)wake {
// Add code here to wake up.
NSLog(@"=====被唤醒时调用=====");
}
  1. 配置 Network Extension 信息
  • 配置 Signing

勾选 Autimatically manage signing

  • 开启权限

TargetVPN扩展 均需要打开 Person VPNNetwork Extensions 两个权限,开启后会新增两个 .extitlements 结尾的配置文件

实现步骤

  1. 实现步骤
  • 导入必须框架头文件

在项目中导入 #import <NetworkExtension/NetworkExtension.h>

  1. 初始化并配置 NETunnleProviderManager 对象
  • 建立 VPN 连接前负责配置基本参数信息保存设置到系统中(一般 VPN App 都会在第一次打开App时授权并保存配置参数信息到系统的VPN设置中)

注:配置好的 NETunnleProviderManager 对象仅在系统中起展示作用(这里的配置并不是真正的生效),真正生效的设置在 Extension Target 中配置

  • NETunnleProviderManager 类继承了 NEVPNManager 大多数基本功能:

    • protocolConfiguration 属性只有 NETunnleProviderManager 类才能设置
    • connection 这个只读属性只能通过 NETunnleProviderManager 这个类设置
  • 每个 NETunnleProviderManager 实例对应一个 VPN 配置,存储在 Network Extension 偏好设置中,可以创建多个 NETunnleProviderManager 实例来管理 多个VPN配置

  • NETunnleProviderManager 创建的 VPN 配置归属 企业VPN 配置,NEVPNManager 创建的 VPN 归属于 个人VPN

  • 一次只能在系统启用一个 VPN 配置,如果同时在系统中激活 个人VPN企业VPN,优先使用 企业VPN

  1. 初始化 NETunnleProviderManager 的代码

在主Target下实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#pragma mark - 保存VPN相关数据
- (void)saveVpnData{
/*
此函数异步读取调用所有app创建且先前保存在本地的 NETunnelProvider 配置信息,并将它们在回调中以一个存放 NETunnelProvider 对象的数组的形式返回
*/
[NETunnelProviderManager loadAllFromPreferencesWithCompletionHandler:^(NSArray<NETunnelProviderManager *> * _Nullable managers, NSError * _Nullable error) {
if (error){
NSLog(@"LoadFromPreferences Error: %@", error.description);
}
// manager
NETunnelProviderManager * manager;
if (managers.count > 0) {
manager = managers[0];
}else {
manager = [[NETunnelProviderManager alloc] init];
manager.protocolConfiguration = [[NETunnelProviderProtocol alloc] init];
}

// 配置 manager
[self configTunnelManagerWithManager:manager];

// 调用此方法用来保存上述配置好的VPN到本机设备的VPN设置中
[self.tpManager saveToPreferencesWithCompletionHandler:^(NSError * _Nullable error) {
if (error){
NSLog(@"Save To Preferences Error:%@",error.description);
}else {
NSLog(@"Save To Preferences Success");
// 启动 VPN
[self startVPN];
}
}];
}];
}
// 配置 VPN 协议
- (void)configTunnelManagerWithManager:(NETunnelProviderManager *)manager{
/*
注:以下参数只是在系统设置中 VPN 设置里显示的参数,真正设置网络参数在 VPN Target 项目中重新设置。
*/
// 协议(配置好协议后,会自动跳转到 iPhone,当你输入好密码后,VPN会自动配置好相关信息并返回到App)
NETunnelProviderProtocol * proto = [[NETunnelProviderProtocol alloc] init];
// 子项目的identifier
proto.providerBundleIdentifier = @"com.jit.cn.MobileCertManagement.sdp.tunnel";

// 配置
// NSMutableDictionary * configDic = @{}.mutableCopy;
// [configDic setObject:@"5055" forKey:@"port"]; // 随机
// [configDic setObject:@"www.jit.com.cn" forKey:@"server"];
// [configDic setObject:@"10.10.10.8" forKey:@"ip"];
// [configDic setObject:@"255.255.255.0" forKey:@"subnet"];
// [configDic setObject:@"1400" forKey:@"mtu"];
// [configDic setObject:@"8.8.8.8,8.4.4.4" forKey:@"dns"];
// proto.providerConfiguration = configDic;

// 手机设置的vpn中显示的vpn地址(服务器)
proto.serverAddress = @"www.jit.com.cn";
// 设备进入睡眠,vpn断开连接
proto.disconnectOnSleep = NO;

// 协议配置
manager.protocolConfiguration = proto;
// 包含vpn描述的字符串(类型)
manager.localizedDescription = @"零信任";
// 是否可以编辑
manager.enabled = YES;

// 赋值
self.tpManager = manager;
}

  1. 启动 VPN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#pragma mark - 启动 VPN
- (void)startVPN{
// 此函数从调用者的VPN设置中加载当前VPN配置
[self.tpManager loadFromPreferencesWithCompletionHandler:^(NSError * _Nullable error) {
if (error) {
NSLog(@"LoadFromPreferences error:%@",error.description);
}else {
NSLog(@"LoadFromPreferences success");
// 启动VPN
NSError * connectError;
// 调用此函数会使用 NEVPNConnection 对当前VPN配置来建立VPN连接。返回YES表示成功。
[self.tpManager.connection startVPNTunnelAndReturnError:&connectError];
}
}];
}

  1. 关闭VPN连接
1
2
3
4
5
6
7
8
9
10
11
12
13
#pragma mark - 断开连接
- (void)disconnectVpn{
// 此函数从调用者的VPN设置中加载当前VPN配置
[self.tpManager loadFromPreferencesWithCompletionHandler:^(NSError * _Nullable error) {
if (error) {
NSLog(@"loadFromPreferences error:%@",error.localizedDescription);
}else {
// 调用此函数会关闭当前建立的VPN连接
[self.tpManager.connection stopVPNTunnel];
}
}];
}

  1. 监听VPN连接状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#pragma mark - 添加VPN连接通知
- (void)addVPNNotification{
// 添加通知 - 连接VPN时会发送通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onVpnStateChange:) name:NEVPNStatusDidChangeNotification object:nil];
}
// 通知的方法
- (void)onVpnStateChange:(NSNotification *)Notification {
// VPN连接的状态
NEVPNStatus status = self.tpManager.connection.status;
switch (status) {
case NEVPNStatusInvalid:{
NSLog(@"VPN连接无效");
}
break;
case NEVPNStatusDisconnected:{
NSLog(@"VPN未连接");
}
break;
case NEVPNStatusConnecting:{
NSLog(@"VPN正在连接...");
}
break;
case NEVPNStatusConnected: {
NSLog(@"VPN已连接");
}
break;
case NEVPNStatusDisconnecting:{
NSLog(@"VPN断开连接中...");
}
break;
case NEVPNStatusReasserting:{
NSLog(@"VPN重新连接...");
}
break;
default:
break;
}
}
  1. 在 PacketTunnelProvider 回调中完成剩余工作
  • 当我们在 主Target 中调用开启VPN连接后,会进入 VPN Target 项目的 PacketTunnelProvider 文件下述方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
当一个新的通道被建立会会调用此函数。我们必须通过重写该类来完成建立连接。
@param : options - 在主控制调用开启VPN连接时传入的字典,可空。
@param : completionHandler - 在该方法彻底完成时必须调用这个Block。如果不能建立连接要将错误信息传给该block,如果成功建立连接则只需将nil传给该Block.
*/
- (void)startTunnelWithOptions:(NSDictionary *)options completionHandler:(void (^)(NSError *))completionHandler {
NSLog(@"=====启动VPN时调用 startTunnelWithOptions=====");
// 建立VPN连接
[self setupVpnTunnelConnect:completionHandler];
}
#pragma mark - 建立 VPN 连接
// 建立 VPN 连接
- (void)setupVpnTunnelConnect:(void (^)(NSError *))completionHandler{

// 建立 VPN 网络配置
NEPacketTunnelNetworkSettings * settings = [self setupVpnTunnelNetworkConfig];

// 建立 VPN 连接
__weak __typeof(&*self)weakSelf = self;
[self setTunnelNetworkSettings:settings completionHandler:^(NSError * _Nullable error) {
if (error){
NSLog(@"=====建立 VPN 连接 Error:%@=====",error.description);
}else {
NSLog(@"=====建立 VPN 连接 Success=====");
// 启动并运行socks5隧道
[weakSelf startSockstTunnel];
}
completionHandler(error);
}];
}
  • 这里是真正设置 VPN tunnel 网络参数的地方,主 target 中设置的仅为系统 VPN 设置中的展示名称
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// VPN网络配置
- (NEPacketTunnelNetworkSettings *)setupVpnTunnelNetworkConfig{
// 我们要建立连接的远程服务器的地址(如果是域名,则需要解析成ip地址)
NSString * remoteAddress = @"172.16.21.222";

// 新建一个 NEPacketTunnelNetworkSettings 对象
NEPacketTunnelNetworkSettings * settings = [[NEPacketTunnelNetworkSettings alloc] initWithTunnelRemoteAddress:remoteAddress];

// 最大传输单元,即每个packet最大的容量
// settings.MTU = 1400;

/* ipv4 用于拦截流量/代理 */
// 虚拟ip
NSString * ip = @"10.10.10.8";
NSString * subNet = @"255.255.255.0";
NEIPv4Settings * ipv4Settings = [[NEIPv4Settings alloc] initWithAddresses:@[ip]
subnetMasks:@[subNet]];
/*
includedRoutes:即VPN tunnel需要拦截包的地址,如果全部拦截则设置[NEIPv4Route defaultRoute],也可以指定部分需要拦截的地址
*/
NEIPv4Route * route = [[NEIPv4Route alloc] initWithDestinationAddress:@"172.16.156.243"
subnetMask:@"255.255.255.255"];
ipv4Settings.includedRoutes = @[route];
// 配置ipv4代理
settings.IPv4Settings = ipv4Settings;

// DNS 解析器(由于 不处理 IP Package)
NSString * dns = @"8.8.8.8,8.4.4.4";
NEDNSSettings * dnsSettings = [[NEDNSSettings alloc] initWithServers:[dns componentsSeparatedByString:@","]];
settings.DNSSettings = dnsSettings;

return settings;
}
  • 域名转换ip
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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;
}
  • 在 VPN Tunnel 中持续读取 packet 包

因为我们已经成功建立了 VPN 连接,所以网络数据包我们可以在此回调中截取到,利用 NEPacketTunnelProvider 对象的 packetFlow 完成下面的方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
从流中读入可用的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("TVUVPNManager", "Read Packet - %s",[NSString stringWithFormat:@"%@",packet].UTF8String);
__typeof__(self) strongSelf = weakSelf;
// TODO ...

NSLog(@"XDX : read packet - %@",packet);
}
[weakSelf readPakcets];
}];
}

  • 查看打印结果

注:由于此时是处于另一个 Target 中,在手机中相当于另一条进程,因此我们无法直接在控制台看到任何打印的 log 信息,如果想要看到 log 信息,需要如下配置:
1、Debug -> Attach to Process by PID or Name…

2、启动台 -> 控制台 -> 设备(张建的 iPhone)-> tunnel(进程) :查看 NSLog 打印结果

总结

在 iOS 中:

  • 如果您 启用 此功能,您的所有流量都将 被 VPN 隧道加密本地流量 将通过您的 VPN

  • 如果它被 禁用,并且您正在浏览互联网,它可以通过本地 WiFi 或 VPN,具体取决于 VPN 提供的路由。

  • Post title:OC网络学习11:Network Extension探索
  • Post author:张建
  • Create time:2023-06-25 11:35:59
  • Post link:https://redefine.ohevan.com/2023/06/25/OC网络/OC网络学习11:Network Extension探索/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.