前言 苹果提供的 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(包隧道提供者)
介绍
Implement a VPN client for a packet-oriented, custom VPN protocol.
为面向包的自定义VPN协议,实现VPN客户端
Overview
概述
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私有中继
Packet Tunnel Provider
The principal class for a packet tunnel provider app extension.
packet tunnel provider App 扩展 的主体类
An abstract base class shared by NEPacketTunnelProvider and NEAppProxyProvider.
一个由 NEPacketTunnelProvider 和 NEAppProxyProvider 共享的抽象基类
An abstract base class for all NetworkExtension providers. NEPacketTunnelNetworkSettings
所有 NetworkExtension 提供商 的抽象基类
NEPacketTunnelNetworkSettings
The configuration for a packet tunnel provider’s virtual interface.
包隧道提供商的虚拟配置接口
The configuration for a tunnel provider’s virtual interface.
VPN
核心技术
创建 Network Extension 步骤
在已经创建好的项目中新建 Target:
创建完成后,TARGET
下会增加一个名为 Tunnel
的扩展,左侧项目中系统会为我们自动生成对应的模板:
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(@"=====被唤醒时调用====="); }
配置 Network Extension 信息
勾选 Autimatically manage signing
主 Target
和 VPN扩展
均需要打开 Person VPN
和 Network Extensions
两个权限,开启后会新增两个 .extitlements
结尾的配置文件
实现步骤
实现步骤
在项目中导入 #import <NetworkExtension/NetworkExtension.h>
初始化并配置 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
初始化 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; }
启动 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]; } }]; }
关闭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]; } }]; }
监听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; } }
在 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; }
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 中: