网络扩展
概述:
网络扩展用来替代内核扩展,在苹果WWDC19的视频中有进行对比讲解,可自行前往观看了解两者的区别和变化。通过NetworkExtension
库可以定制和扩展系统的网络功能。可以在macOS, iOS, tvOS和visionOS平台上使用,但并不是每个方法都可以,具体可以根据API说明查看使用的平台,具体我们通过该库的如下功能:
- 更改系统的Wi-Fi配置
- 热点助手(Hotspot Helper)
- 使用内置的XXX协议及自定义的XXX协议,来创建和管理XXX配置的相关业务
- 网络过滤器(content filter)
- 使用内置的DNS协议或自定义的设备上的DNS代理创建和管理系统范围的DNS配置的相关业务
而我们平时用得最多的是XXX和content filter相关的功能,这也是接下来详细介绍的内容。为了节约篇幅,这是就不对怎么去创建添加网络扩展进行编写,不过有几点注意事项在这里陈述一下,免得踩坑。
-
在宿主程序和网络扩展程序的Capability中添加
App Groups
, 然后填上在苹果账号后台申请的名字一致就行。 -
在宿主程序和网络扩展程序的Capability中添加
Network Extensions
,这里不用进行勾选,因为勾选会和配置文件中的参数匹配不上,貌似是苹果在macOS上的一个bug,这是一个容易踩坑的地方,需要在xxxxx.entitlements文件中手动设置,下面会说到,不过需要用到的功能在苹果后台生成配置的文件选项要匹配。 -
在宿主程序的Capability中添加
System Extension
,该系统扩展的内容在上篇文章中已经做过说明。 -
在项目中找到xxxxx.entitlements文件打开,这里会展示出刚才添加的,不过上面遗留的问题在这里手动添加,点开Network Extensions选项添加需要用到的功能参数。
-
app-proxy-provider-systemextension
-
content-filter-provider-systemextension
-
packet-tunnel-provider-systemextension
-
dns-proxy-systemextension
-
dns-settings
-
-
在网络扩展的plist文件中添加
NetworkExtension
字段,类型设置为字典。然后添加NEMachServiceName
将在宿主程序中添加的App Groups
填上,再一个就是NEProviderClasses
这里设置网络扩展各个功能的入口类名。
这里我们项目中用到了这两个业务功能,因此就添加了这两条,这里强调一下,针对我们用到了网络扩展的多个功能是用一个系统扩展去加载,还是一个功能一个系统扩展区加载。其实在我的实验中,两者都可以,个人还是偏向于用一个系统扩展,这个可以根据各自的业务需求决定。接下来说下两者的优缺点:
-
所有网络扩展功能用一个系统扩展
- 便于管理启动和停止网络扩展程序
- 授权的时候用户不用进行多次确认操作
- 业务代码可以共用一套
- 不利点:如果多个功能的有业务启用则需要另外的判断操作
-
每个网络扩展功能对应一个系统扩展
- 各个功能点分开,业务启用比较灵活
- 不利点:需要对每个进行启停和回调函数的判断处理
- 不利点:授权需要多次操作
- 不利点:与宿主程序通信得单独建立XPC,通讯方面比较麻烦
内容过滤器
主要用于限制或控制网络上的特定类型的内容。这种技术可以根据用户、组织或网络的需要,选择性地禁止、允许或监视网络上的内容。它通常被用来过滤不良、非法或不适宜的内容,例如色情、暴力、恐怖主义、诈骗等,以保护用户、组织或网络的安全和隐私。它可以在个人计算机、企业网络、学校、图书馆、公共场所等各种环境中使用。许多组织和机构都使用内容过滤技术来确保网络的安全和合规性,对数据具有可读权限。目前市场比较火热的安全沙箱网络Mac端应该会用到该功能来控制数据的流转。
从上图中可以清楚的看到在宿主程序中用 NEFilterManager
设定其属性 providerConfiguration
来配置过滤规则,比如是Flow数据流还是IP数据包类型。再通过 saveToPreferencesWithCompletionHandler
来配置到系统中,然后设定 enabled
为YES,这样就开启了网络扩展过滤器。
objective-c[[NEFilterManager sharedManager] loadFromPreferencesWithCompletionHandler:^(NSError * _Nullable error) { if (error) { return; } if ([NEFilterManager sharedManager].providerConfiguration == nil) { NEFilterProviderConfiguration * providerConfiguration = [[NEFilterProviderConfiguration alloc] init]; if (@available(macOS 10.15, *)) { providerConfiguration.filterSockets = YES;//Flow数据流层 providerConfiguration.filterPackets = NO; //IP数据包层 } [NEFilterManager sharedManager].providerConfiguration = providerConfiguration; [NEFilterManager sharedManager].localizedDescription = @"XXXSDP"; } [[NEFilterManager sharedManager] setEnabled:YES]; [[NEFilterManager sharedManager] saveToPreferencesWithCompletionHandler:^(NSError * _Nullable error) { if (error) { return; } }]; }];
在网络扩展中运行中 NEFilterDataProvider
或者 NEFilterPacketProvider
的子类,并重写 startFilterWithCompletionHandler
、stopFilterWithReason
、stopFilterWithReason
、handleInboundDataFromFlow
、handleOutboundDataFromFlow
、handleInboundDataFromFlow
和 handleOutboundDataFromFlow
等方法,具体各个方法实现请看下面案例:
objective-c// 这个开始方法中会设置拦截数据流的一些规则,列举了几种拦截方法设置方法,也可以根据自己的业务需要进行设置,设置完成之后不要忘记实现block回调。 - (void)startFilterWithCompletionHandler:(void (^)(NSError *error))completionHandler { NSMutableArray *filterRules = [[NSMutableArray alloc] init]; // NWHostEndpoint *ipv4LocalHost = [NWHostEndpoint endpointWithHostname:@"127.0.0.1" port:@"0"]; // NENetworkRule *ipv4LocalNetworkRule = [[NENetworkRule alloc]initWithRemoteNetwork:ipv4LocalHost remotePrefix:0 localNetwork:ipv4LocalHost localPrefix:0 protocol:NENetworkRuleProtocolAny direction:NETrafficDirectionAny]; // NEFilterRule *ipv4LocalFilterRule = [[NEFilterRule alloc]initWithNetworkRule:ipv4LocalNetworkRule action:NEFilterActionFilterData]; // // NWHostEndpoint *ipv6LocalHost = [NWHostEndpoint endpointWithHostname:@"::1" port:@"0"]; // NENetworkRule *ipv6LocalNetworkRule = [[NENetworkRule alloc]initWithRemoteNetwork:ipv6LocalHost remotePrefix:0 localNetwork:ipv6LocalHost localPrefix:0 protocol:NENetworkRuleProtocolAny direction:NETrafficDirectionAny]; // NEFilterRule *ipv6LocalFilterRule = [[NEFilterRule alloc]initWithNetworkRule:ipv6LocalNetworkRule action:NEFilterActionFilterData]; NENetworkRule *normalNetworkRule = [[NENetworkRule alloc]initWithRemoteNetwork:nil remotePrefix:0 localNetwork:nil localPrefix:0 protocol:NENetworkRuleProtocolAny direction:NETrafficDirectionAny]; NEFilterRule *normalFilterRule = [[NEFilterRule alloc]initWithNetworkRule:normalNetworkRule action:NEFilterActionFilterData]; // NENetworkRule *hostNetworkRule = [[NENetworkRule alloc] initWithDestinationHost:[NWHostEndpoint endpointWithHostname:@"com" port:@"0"] protocol:NENetworkRuleProtocolAny]; // NEFilterRule *hostFilterRule = [[NEFilterRule alloc]initWithNetworkRule:hostNetworkRule action:NEFilterActionFilterData]; // [filterRules addObject:ipv4LocalFilterRule]; // [filterRules addObject:ipv6LocalFilterRule]; [filterRules addObject:normalFilterRule]; // [filterRules addObject:hostFilterRule]; NEFilterSettings *settings = [[NEFilterSettings alloc]initWithRules:filterRules defaultAction:NEFilterActionAllow]; // Allow all flows that do not match the filter rules. [self applySettings:settings completionHandler:^(NSError * _Nullable error) { if (error != nil) { }else{ if (completionHandler) { completionHandler(nil); //这个block回调必须写 } } }]; }
objective-c// 这个停止方法中可以根据自身业务做些停止相关的业务,不过过滤器启动之后一般就会一直运行着。 - (void)stopFilterWithReason:(NEProviderStopReason)reason completionHandler:(void (^)(void))completionHandler { // Add code to clean up filter resources. completionHandler(); }
objective-c//该方法中就能拿到数据流flow,可以根据业务的策略规则进行放行或者拒绝和需要裁决等处理情况 - (NEFilterNewFlowVerdict *)handleNewFlow:(NEFilterFlow *)flow { // Add code to determine if the flow should be dropped or not, downloading new rules if required. //这里可以转成NEFilterSocketFlow类型的数据流,以便我们获取到更多数据流中可用的信息 NEFilterSocketFlow *socketFlow = (NEFilterSocketFlow *)flow; NWHostEndpoint *remoteEndpoint = (NWHostEndpoint *)socketFlow.remoteEndpoint; if (!socketFlow || !remoteEndpoint) { return [NEFilterNewFlowVerdict allowVerdict];; } if ([remoteEndpoint.port isEqual:@"53"] || [remoteEndpoint.port isEqual:@"5353"] || socketFlow.socketFamily == PF_INET6) { return [NEFilterNewFlowVerdict allowVerdict]; } if (//各种策略条件) { return [NEFilterNewFlowVerdict allowVerdict]; }else{ return [NEFilterNewFlowVerdict dropVerdict]; } return [NEFilterNewFlowVerdict allowVerdict]; }
这四个方法平常一般不使用,当业务有需要的时候可以使用。例如我们在macOS 13以下系统中通过 NEFilterSocketFlow
的 remoteHostname
属性获取不到flow流的域名时,而业务策略中下发的是域名,此时我们没有办法去通过下发的域名有效的判断出数据流是否需要放行,这是就可以启动下面的方法对数据流进行进一步的解析获取到有用的信息。至于 remoteHostname
在11、12版本为空,目前没有找到苹果官方解释,那没有就只能另辟蹊径了,如果遇到类似问题可以私聊沟通。
objective-c- (NEFilterDataVerdict *)handleInboundDataFromFlow:(NEFilterFlow *)flow readBytesStartOffset:(NSUInteger)offset readBytes:(NSData *)readBytes { return [NEFilterDataVerdict allowVerdict]; } - (NEFilterDataVerdict *)handleOutboundDataFromFlow:(NEFilterFlow *)flow readBytesStartOffset:(NSUInteger)offset readBytes:(NSData *)readBytes { return [NEFilterDataVerdict allowVerdict]; } - (NEFilterDataVerdict *)handleInboundDataCompleteForFlow:(NEFilterFlow *)flow { return [NEFilterDataVerdict allowVerdict]; } - (NEFilterDataVerdict *)handleOutboundDataCompleteForFlow:(NEFilterFlow *)flow { return [NEFilterDataVerdict allowVerdict] }
接下来聊下开发过程中遇到的问题和常使用的方法。
-
通过
NEFilterSocketFlow
的sourceAppAuditToken
属性获取pid。当然也可以获取其他相关的信息,可以跟到库中查看API。objective-caudit_token_t auditToken = * (audit_token_t *) socketFlow.sourceAppAuditToken.bytes; pid_t pid = audit_token_to_pid(auditToken);
-
通过pid获取进程名
objective-cchar procname[MAX_PROCNAME_LEN] = {0}; proc_name(pid, procname, sizeof(procname));
-
通过pid获取进程路径,有点需要注意的是假如应用使用了
com.apple.WebKit.Networking
网络库作为网络请求,最终通过pid获取到的路径就是该库所在的路径而非开始调用的库,很常见的是使用Safari浏览器访问网页。这时如果策略通过Safari去判断进程的话就会出现误差,这里是需要提出来的。objective-cchar procPath[MAX_PROCNAME_LEN] = {0}; proc_pidpath(pid, procPath, sizeof(procPath));
-
在开发过程中遇到最多的还是域名方面的问题,例如使用Google浏览器时,在flow数据流中我们会获取到根域名,在我们平常看到的域名后面多一个点,其实这也是正常的域名,平常没有看到是浏览器帮我们处理了。这样如果不注意处理,直接拿去精确匹配策略就会因为这个点而匹配失败。
-
应用使用WebKit库和不使用的在获取Flow数据流存在区别,使用Webkit库苹果自己应该有专门的接口,而其他的则需要从Socket层去HOOK,如果需要进一步了解,可以通过不同网络内核的Safari浏览器和Firefox浏览器进行研究。
数据包隧道
为面向数据包的自定义 XXX 协议实现 XXX 客户端。虚拟专用网络 (XXX) 是网络隧道的一种形式,其中 XXX 客户端使用公共 Internet 创建与 XXX服务器的连接,然后通过该连接传递专用网络流量。 如果您想要构建一个实现面向数据包的自定义 XXX 协议的 XXX 客户端,请创建数据包隧道提供商应用程序扩展。主要有以下步骤:
-
启动应用程序扩展
-
在应用程序扩展中实例化数据包隧道提供子类
-
将数据包转发到XXX服务端
从上图中可以看到在宿主app中将使用 NETunnelProviderManager
类来创建和管理隧道提供商的 XXX 配置的对象。每个 NETunnelProviderManager
实例对应一个XXX配置,存储在网络扩展偏好设置中。并且每个XXX配置与创建它的App相关联,网络扩展偏好设置在应用中的视图仅限于被该应用创建的配置。常用的方法有 loadAllFromPreferencesWithCompletionHandler
以及父类 NEVPNManager
中的 removeFromPreferencesWithCompletionHandler
、saveToPreferencesWithCompletionHandler
和 loadFromPreferencesWithCompletionHandler
等方法。
objective-c[NETunnelProviderManager loadAllFromPreferencesWithCompletionHandler: ^(NSArray<NETunnelProviderManager *> * _Nullable managers, NSError * _Nullable error) { if (error) { return; } //判定是否存在已有配置 if (managers && managers.count > 0) { //移除所有配置只保留最后一个 for (NSUInteger index = 0; index < managers.count-1; index++) { NETunnelProviderManager* manager = managers[index]; [manager removeFromPreferencesWithCompletionHandler:^(NSError* _Nullable error){ if (error) { } }]; } //获取配置管理并保存 self.tunnelProviderManager = managers[managers.count-1]; } //重新配置并加载 NETunnelProviderProtocol *protocol = [[NETunnelProviderProtocol alloc]init]; protocol.providerBundleIdentifier = @"tunnelBundleId";//bundle id protocol.providerConfiguration = self.protocolConfiguration;//该字典在tunnel建立成功后会传递给NETunnelProviders对象,可用于传递过滤规则参数!此属性不能为空,否则saveToPreferencesWithCompletionHandler会失败! protocol.serverAddress = @"controllerIP";//XXX服务器ip,这里不关键可以随便设置,主要是为了辨识连接的服务器ip self.tunnelProviderManager.protocolConfiguration = protocol; self.tunnelProviderManager.enabled = YES; self.tunnelProviderManager.localizedDescription = @"XXXSDP";//这里设置隧道代理名称,可根据需求设置 //如果配置不存在或者加载成功则error为nil //必须saveToPreferencesWithCompletionHandler -> loadFromPreferencesWithCompletionHandler,否则NEVPNErrorConfigurationDisabled [self.tunnelProviderManager saveToPreferencesWithCompletionHandler:^(NSError * _Nullable error) { if (error) { return; } //保存成功后再次加载 [self.tunnelProviderManager loadFromPreferencesWithCompletionHandler:^(NSError * _Nullable error) { if (error) { return; } }]; }]; }];
启动XXX隧道服务 startVPNTunnelWithOptions
objective-cif (self.tunnelProviderManager.connection.status == NEVPNStatusConnected) { //已连接状态 return; }else{ NSError *error; NETunnelProviderProtocol* protol = (NETunnelProviderProtocol*)self.tunnelProviderManager.protocolConfiguration; [self.tunnelProviderManager.connection startVPNTunnelWithOptions:protol.providerConfiguration andReturnError:&error]; }
停止XXX隧道服务 stopVPNTunnel
objective-cif (self.tunnelProviderManager.connection.status == NEVPNStatusConnected) { //已连接状态 [self.tunnelProviderManager.connection stopVPNTunnel]; }else{ //未连接 return; }
在网络扩展中使用 NEPacketTunnelProvider
子类,重写startTunnelWithOptions
、stopTunnelWithReason
、handleAppMessage
、sleepWithCompletionHandler
和 wake
等方法。主要用到的方法 startTunnelWithOptions
,其他的几个方法根据自身需求进行相应的功能开发。
objective-c- (void)startTunnelWithOptions:(NSDictionary *)options completionHandler:(void (^)(NSError *))completionHandler { // Add code here to start the process of connecting the tunnel. NEPacketTunnelNetworkSettings *tunnelSettings = [[NEPacketTunnelNetworkSettings alloc]initWithTunnelRemoteAddress:@"127.0.0.1"]; tunnelSettings.MTU = @1440; tunnelSettings.IPv4Settings = [[NEIPv4Settings alloc]initWithAddresses:[NSArray arrayWithObjects:@"10.1.0.0",nil] subnetMasks:[NSArray arrayWithObjects: @"255.255.255.0", nil]]; // tunnelSettings.IPv4Settings.includedRoutes = @[[NEIPv4Route defaultRoute]]; NEIPv4Route *ipv4Route = [[NEIPv4Route alloc]initWithDestinationAddress:@"10.10.3.11" subnetMask:@"255.255.255.255"]; tunnelSettings.IPv4Settings.includedRoutes = @[ipv4Route]; // NEIPv4Route *ipv4exRoute = [[NEIPv4Route alloc]initWithDestinationAddress:@"10.1.0.0" subnetMask:@"255.255.255.255"]; // NEIPv4Route *ipv4exRoute1 = [[NEIPv4Route alloc]initWithDestinationAddress:@"10.20.22.187" subnetMask:@"255.255.255.255"]; // tunnelSettings.IPv4Settings.excludedRoutes = @[ipv4exRoute,ipv4exRoute1]; // tunnelSettings.IPv6Settings = [[NEIPv6Settings alloc]initWithAddresses:[NSArray arrayWithObjects:@"2001:db8::",nil] networkPrefixLengths:[NSArray arrayWithObjects:@128, nil]]; //// tunnelSettings.IPv6Settings.includedRoutes = @[[NEIPv6Route defaultRoute]]; // NEIPv6Route *ipv6Route = [[NEIPv6Route alloc]initWithDestinationAddress:@"fc00:1983::80d0:f08c:ea0c:f073" networkPrefixLength:@128]; // tunnelSettings.IPv6Settings.includedRoutes = @[ipv6Route]; tunnelSettings.DNSSettings = [[NEDNSSettings alloc]initWithServers:@[@"114.114.114.114",@"233.5.5.5"]]; tunnelSettings.DNSSettings.matchDomains = @[@""]; __weak PacketTunnelProvider *weakSelf = self; [self setTunnelNetworkSettings:tunnelSettings completionHandler:^(NSError * _Nullable error) { if(error){ completionHandler(error); return; }else{ completionHandler(nil); [weakSelf readPackets]; } }]; } //这里获取数量包可以使用NEPacket和NSData,NEPacket中含有protocolFamily、direction、metadata、data属性,可根据需求进行选择 -(void)readPackets{ __weak PacketTunnelProvider *weakSelf = self; [self.packetFlow readPacketObjectsWithCompletionHandler:^(NSArray<NEPacket *> * _Nonnull packets) { for (NEPacket * packet in packets) { //业务处理 } [weakSelf readPackets]; }]; // [self.packetFlow readPacketsWithCompletionHandler:^(NSArray<NSData *> * _Nonnull packets, NSArray<NSNumber *> * _Nonnull protocols) { // for (NSData * packet in packets) { //业务处理 // } // // // [weakSelf readPackets]; // }]; }
根据上面的实例代码需要注意的是 NEPacketTunnelNetworkSettings
的设置, 可以通过 IPv4Settings
和 IPv6Settings
对虚拟网卡IP进行设置。
通过 includedRoutes
设置需要拦截的ip,这里也可以设置全部拦截 defaultRoute
,根据业务需求进行设置。通过 netstat -nr -f inet | grep utun
可以查看配置在虚拟网卡上的路由IP,此处查找的是ipv4的IP,生成的虚拟网卡名称一般为 utun
开头。
通过 DNSSettings
设置本地DNS服务器IP到路由表中,可以拦截到DNS数据包,进行内网DNS域名解析。
上图中的本地DNS服务器配置可以在扩展启动的时候去获取,如果在扩展使用过程中改变DNS服务器IP,则需要通过监听 scutil --dns
的变化做相应的变化改变路由表IP的配置。
总结:
- 该文中没有具体的介绍如何一步一步去实现网络扩展的细节,也还有很多知识点没有介绍,如XPC通信、IP数据包四元组处理、DNS数据包处理、数据包代理和转发等等。这些内容因为业务的不同,处理方式可能存在差异,因此就没有加到本文中,不过如有需要可以私下沟通交流开发经验和问题处理。