-
Notifications
You must be signed in to change notification settings - Fork 190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
iOS 防 DNS 污染方案调研(二)--- SNI 业务场景 #12
Comments
//TODO: 测试原生NSURLSession请求,与 NSURLProtocol 拦截NSURLSession请求,两种方式之间的性能对比,可以直接基于NSURLSession的回调,回调前与回调后之差进行统计。相比于 WebView 更佳统计,WebView 用 NSURLProtocol 拦截后是黑盒,而NSURLSession能看到回调。基于测试,是制作成图。并对比body大小不同的性能差距。 对于拦截网络请求带来的编解码问题,需要手动编解码,比如 gzip 编码,需要在接收到response 后进行 gzip 解码,再传回。 |
上面使用 /**
* 开始加载,在该方法中,加载一个请求
*/
- (void)startLoading {
NSMutableURLRequest *request = [self.request mutableCopy];
[request cyl_handlePostRequestBody];
// 表示该请求已经被处理,防止无限循环
[NSURLProtocol setProperty:@(YES) forKey:CYL_NSURLPROTOCOL_REQUESTED_FLAG_KEY inRequest:request];
[self startRequest];
} 其中 //
// NSURLRequest+CYLNSURLProtocolExtension.h
//
//
// Created by ElonChan on 28/07/2017.
// Copyright © 2017 ChenYilong. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface NSMutableURLRequest (CYLNSURLProtocolExtension)
- (void)cyl_handlePostRequestBody;
@end
@implementation NSMutableURLRequest (CYLNSURLProtocolExtension)
- (void)cyl_handlePostRequestBody {
if ([self.HTTPMethod isEqualToString:@"POST"]) {
if (!self.HTTPBody) {
uint8_t d[1024] = {0};
NSInputStream *stream = self.HTTPBodyStream;
NSMutableData *data = [[NSMutableData alloc] init];
[stream open];
while ([stream hasBytesAvailable]) {
NSInteger len = [stream read:d maxLength:1024];
if (len > 0 && stream.streamError == nil) {
[data appendBytes:(void *)d length:len];
}
}
self.HTTPBody = [data copy];
[stream close];
}
}
}
@end 两种方法的实现效果一致,推荐在 |
下面测试下添加 SNI 策略后,与原生网络请求相比,对性能的影响。 测试数据如下: 正常wifi网络环境下,iPhone 6s plus - iOS9.3 系统: SNI 场景: IP 直连: IP 直连:
0.161788+0.156012+0.220671+0.149536+0.160505+0.158274+0.200679+0.164229+0.173180+0.171909+0.177469+0.195068+0.166481+0.165608+0.183510+0.183593+0.171261+0.176552+0.194354+0.172321+0.179747+0.166126+0.213673+0.173284+0.183772+0.154173+0.156247+0.159818+0.186316+0.203103=5.279259/30=.1759753
详细数据:
2017-08-25 14:53:30.155 executionTime(执行时间) = 0.161788
2017-08-25 14:53:31.800 executionTime(执行时间) = 0.156012
2017-08-25 14:53:33.127 executionTime(执行时间) = 0.220671
2017-08-25 14:53:34.342 executionTime(执行时间) = 0.149536
2017-08-25 14:53:36.323 executionTime(执行时间) = 0.160505
2017-08-25 14:53:38.031 executionTime(执行时间) = 0.158274
2017-08-25 14:53:39.254 executionTime(执行时间) = 0.200679
2017-08-25 14:53:40.709 executionTime(执行时间) = 0.164229
2017-08-25 14:53:42.867 executionTime(执行时间) = 0.173180
2017-08-25 14:53:45.073 executionTime(执行时间) = 0.171909
2017-08-25 14:53:46.673 executionTime(执行时间) = 0.177469
2017-08-25 14:53:48.289 executionTime(执行时间) = 0.195068
2017-08-25 14:53:49.404 executionTime(执行时间) = 0.166481
2017-08-25 14:53:50.663 executionTime(执行时间) = 0.165608
2017-08-25 14:53:51.849 executionTime(执行时间) = 0.183510
2017-08-25 14:53:53.428 executionTime(执行时间) = 0.183593
2017-08-25 14:53:55.366 executionTime(执行时间) = 0.171261
2017-08-25 14:53:57.672 executionTime(执行时间) = 0.176552
2017-08-25 14:53:58.749 executionTime(执行时间) = 0.194354
2017-08-25 14:54:00.337 executionTime(执行时间) = 0.172321
2017-08-25 14:54:02.393 executionTime(执行时间) = 0.179747
2017-08-25 14:54:04.311 executionTime(执行时间) = 0.166126
2017-08-25 14:54:05.910 executionTime(执行时间) = 0.213673
2017-08-25 14:54:07.224 executionTime(执行时间) = 0.173284
2017-08-25 14:54:10.001 executionTime(执行时间) = 0.183772
2017-08-25 14:54:11.850 executionTime(执行时间) = 0.154173
2017-08-25 14:54:14.316 executionTime(执行时间) = 0.156247
2017-08-25 14:54:16.927 executionTime(执行时间) = 0.159818
2017-08-25 14:54:18.834 executionTime(执行时间) = 0.186316
2017-08-25 14:54:21.505 executionTime(执行时间) = 0.203103
4.076027 非 IP 直连的方案: 非 IP 直连的方案:
0.152328+1.722805+0.160652+0.131074+0.155207+0.141891+0.134469+0.145446+0.198126+0.158850+0.138900+0.144652+0.137210+0.141466+0.135740+0.132003+0.142922+0.166419+0.153931+0.128710+0.150666+0.139668+0.136447+0.139566+0.141225+0.141087+0.153604+0.137701+0.140645+0.137058=5.940468/30=.1980156
2017-08-25 11:36:52.532 executionTime(执行时间) = 0.152328
2017-08-25 11:37:09.077 executionTime(执行时间) = 1.722805
2017-08-25 11:37:12.116 executionTime(执行时间) = 0.160652
2017-08-25 11:37:14.482 executionTime(执行时间) = 0.131074
2017-08-25 11:37:17.019 executionTime(执行时间) = 0.155207
2017-08-25 11:37:19.220 executionTime(执行时间) = 0.141891
2017-08-25 11:37:21.694 executionTime(执行时间) = 0.134469
2017-08-25 11:37:23.996 executionTime(执行时间) = 0.145446
2017-08-25 11:37:25.950 executionTime(执行时间) = 0.198126
2017-08-25 11:37:27.584 executionTime(执行时间) = 0.158850
2017-08-25 11:37:29.699 executionTime(执行时间) = 0.138900
2017-08-25 11:37:32.145 executionTime(执行时间) = 0.144652
2017-08-25 11:37:34.440 executionTime(执行时间) = 0.137210
2017-08-25 11:37:36.392 executionTime(执行时间) = 0.141466
2017-08-25 11:37:38.088 executionTime(执行时间) = 0.135740
2017-08-25 11:37:39.959 executionTime(执行时间) = 0.132003
2017-08-25 11:37:41.451 executionTime(执行时间) = 0.142922
2017-08-25 11:37:43.430 executionTime(执行时间) = 0.166419
2017-08-25 11:37:45.767 executionTime(执行时间) = 0.153931
2017-08-25 11:37:47.454 executionTime(执行时间) = 0.128710
2017-08-25 11:37:49.401 executionTime(执行时间) = 0.150666
2017-08-25 11:37:51.162 executionTime(执行时间) = 0.139668
2017-08-25 11:37:54.506 executionTime(执行时间) = 0.136447
2017-08-25 11:37:56.696 executionTime(执行时间) = 0.139566
2017-08-25 11:37:59.650 executionTime(执行时间) = 0.141225
2017-08-25 11:38:01.720 executionTime(执行时间) = 0.141087
2017-08-25 11:38:03.672 executionTime(执行时间) = 0.153604
2017-08-25 11:38:05.040 executionTime(执行时间) = 0.137701
2017-08-25 11:38:06.457 executionTime(执行时间) = 0.140645
2017-08-25 11:38:07.853 executionTime(执行时间) = 0.137058
2017-08-25 11:38:09.871 executionTime(执行时间) = 0.146094
2017-08-25 11:38:19.822 executionTime(执行时间) = 0.160912
2017-08-25 11:38:22.341 executionTime(执行时间) = 0.138762 测试结果显示两个性能差别并不大,SNI 解决方案平均0.1759753秒 原生网络请求:0.1980156秒。 注意:测试采用的是密集的网络请求方式,每次网络请求间隔较短,如果间隔时间很大,比如10分钟,由于 TCP 通道不会被复用,波动可能较大,达到5秒、6秒级别。测试的代码部分,不涉及 TCP 通道复用部分,应该避免该部分的干扰,故采用密集的网络请求方式。另外SNI 解决方案中,解析出的 IP 有 TTL 过期时间属性,如果 TTL 过期会走原生的网络请求部分,以上测试期间 IP 均未过期。 |
基于 CFNetWork 有性能瓶颈方案:
调研性能瓶颈的原因在使用 CFNetWork 实现了基本的 SNI 解决方案后,虽然问题解决了,但是遇到了性能瓶颈,对比 /one more thing/ 调研性能瓶颈的方法可以使用下面的方法,做一个简单的打点,将流开始和流结束记录下。 记录的数据如下:
#import <Foundation/Foundation.h>
@interface CYLRequestTimeMonitor : NSObject
+ (NSString *)requestBeginTimeKeyWithID:(NSUInteger)ID;
+ (NSString *)requestEndTimeKeyWithID:(NSUInteger)ID;
+ (NSString *)requestSpentTimeKeyWithID:(NSUInteger)ID;
+ (NSString *)getKey:(NSString *)key ID:(NSUInteger)ID;
+ (NSUInteger)timeFromKey:(NSString *)key;
+ (NSUInteger)frontRequetNumber;
+ (NSUInteger)changeToNextRequetNumber;
+ (void)setCurrentTimeForKey:(NSString *)key taskID:(NSUInteger)taskID time:(NSTimeInterval *)time;
+ (void)setTime:(NSUInteger)time key:(NSString *)key taskID:(NSUInteger)taskID;
+ (void)setBeginTimeForTaskID:(NSUInteger)taskID;
+ (void)setEndTimeForTaskID:(NSUInteger)taskID;
+ (void)setSpentTimeForKey:(NSString *)key endTime:(NSUInteger)endTime taskID:(NSUInteger)taskID;
@end #import "CYLRequestTimeMonitor.h"
@implementation CYLRequestTimeMonitor
static NSString *const CYLRequestFrontNumber = @"CYLRequestFrontNumber";
static NSString *const CYLRequestBeginTime = @"CYLRequestBeginTime";
static NSString *const CYLRequestEndTime = @"CYLRequestEndTime";
static NSString *const CYLRequestSpentTime = @"CYLRequestSpentTime";
+ (NSString *)requestBeginTimeKeyWithID:(NSUInteger)ID {
return [self getKey:CYLRequestBeginTime ID:ID];
}
+ (NSString *)requestEndTimeKeyWithID:(NSUInteger)ID {
return [self getKey:CYLRequestEndTime ID:ID];
}
+ (NSString *)requestSpentTimeKeyWithID:(NSUInteger)ID {
return [self getKey:CYLRequestSpentTime ID:ID];
}
+ (NSString *)getKey:(NSString *)key ID:(NSUInteger)ID {
NSString *timeKeyWithID = [NSString stringWithFormat:@"%@-%@", @(ID), key];
return timeKeyWithID;
}
+ (NSUInteger)timeFromKey:(NSString *)key {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSUInteger time = [defaults integerForKey:key];
return time ?: 0;
}
+ (NSUInteger)frontRequetNumber {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSUInteger frontNumber = [defaults integerForKey:CYLRequestFrontNumber];
return frontNumber ?: 0;
}
+ (NSUInteger)changeToNextRequetNumber {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSUInteger nextNumber = ([self frontRequetNumber]+ 1);
[defaults setInteger:nextNumber forKey:CYLRequestFrontNumber];
[defaults synchronize];
return nextNumber;
}
+ (void)setCurrentTimeForKey:(NSString *)key taskID:(NSUInteger)taskID time:(NSTimeInterval *)time {
NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970]*1000;
*time = currentTime;
[self setTime:currentTime key:key taskID:taskID];
}
+ (void)setTime:(NSUInteger)time key:(NSString *)key taskID:(NSUInteger)taskID {
NSString *keyWithID = [self getKey:key ID:taskID];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setInteger:time forKey:keyWithID];
[defaults synchronize];
}
+ (void)setBeginTimeForTaskID:(NSUInteger)taskID {
NSTimeInterval begin;
[self setCurrentTimeForKey:CYLRequestBeginTime taskID:taskID time:&begin];
}
+ (void)setEndTimeForTaskID:(NSUInteger)taskID {
NSTimeInterval endTime = 0;
[self setCurrentTimeForKey:CYLRequestEndTime taskID:taskID time:&endTime];
[self setSpentTimeForKey:CYLRequestSpentTime endTime:endTime taskID:taskID];
}
+ (void)setSpentTimeForKey:(NSString *)key endTime:(NSUInteger)endTime taskID:(NSUInteger)taskID {
NSString *beginTimeString = [self requestBeginTimeKeyWithID:taskID];
NSUInteger beginTime = [self timeFromKey:beginTimeString];
NSUInteger spentTime = endTime - beginTime;
[self setTime:spentTime key:CYLRequestSpentTime taskID:taskID];
}
@end
NSURLConnection 的打点位置如下: 这里普通的做法就是继承 NSURLProtocol 这个类写一个子类,然后在子类中实现NSURLConnectionDelegate 的那五个代理方法。
- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response
// 这个方法里可以做计时的开始
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
// 这里可以得到返回包的总大小
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
// 这里将每次的data累加起来,可以做加载进度圆环之类的
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
// 这里作为结束的时间
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
// 错误的收集 NSURLSession 类似。 然后在自定义CFNetwork的下面两个方法中打点:流开始和流结束,命名大致如: 发送相同的网络请求,然后通过对比两个的时间来观察性能。 瓶颈原因 |
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -canInitWithRequest: only defined for abstract class. Define -[CUSTOMEURLProtocol canInitWithRequest:]!' 闪退报这个错 |
你好,关于使用NSURLProtocol接管NSURLSession请求的bug ,可否这样处理。 把body通过objc_setAssociatedObject关联到request对象上。 这样在protocol中可以取到body。 |
为啥我们获取到的httpBodyStream也是空的? |
iOS 防 DNS 污染方案调研--- SNI 业务场景
对应的GitHub仓库镜像地址在这里 ,欢迎提PR进行修改。
概述
SNI(单IP多HTTPS证书)场景下,iOS上层网络库
NSURLConnection/NSURLSession
没有提供接口进行SNI 字段
配置,因此需要 Socket 层级的底层网络库例如CFNetwork
,来实现IP 直连网络请求
适配方案。而基于 CFNetwork 的解决方案需要开发者考虑数据的收发、重定向、解码、缓存等问题(CFNetwork是非常底层的网络实现)。针对 SNI 场景的方案, Socket 层级的底层网络库,大致有两种:
下面将目前面临的一些挑战,以及应对策略介绍一下:
支持 Post 请求
使用 NSURLProtocol 拦截 NSURLSession 请求丢失 body,故有以下几种解决方法:
方案如下:
对方案做以下分析
使用方法:
在用于拦截请求的
NSURLProtocol
的子类中实现方法+canonicalRequestForRequest:
并处理request
对象:下面介绍下相关方法的作用:
翻译下:
简单说:
+[NSURLProtocol canInitWithRequest:]
负责筛选哪些网络请求需要被拦截+[NSURLProtocol canonicalRequestForRequest:]
负责对需要拦截的网络请求NSURLRequest
进行重新构造。这里有一个注意点:
+[NSURLProtocol canonicalRequestForRequest:]
的执行条件是+[NSURLProtocol canInitWithRequest:]
返回值为YES
。注意在拦截
NSURLSession
请求时,需要将用于拦截请求的 NSURLProtocol 的子类添加到NSURLSessionConfiguration
中,用法如下:换用其他提供了SNI字段配置接口的更底层网络库
如果使用第三方网络库:curl, 中有一个
-resolve
方法可以实现使用指定 ip 访问 https 网站,iOS 中集成 curl 库,参考 curl文档 ;另外有一点也可以注意下,它也是支持 IPv6 环境的,只需要你在 build 时添加上
--enable-ipv6
即可。curl 支持指定 SNI 字段,设置 SNI 时我们需要构造的参数形如:
{HTTPS域名}:443:{IP地址}
假设你要访问. www.example.org ,若IP为 127.0.0.1 ,那么通过这个方式来调用来设置 SNI 即可:
iOS CURL 库
使用libcurl 来解决,libcurl / cURL 至少 7.18.1 (2008年3月30日) 在 SNI 支持下编译一个 SSL/TLS 工具包,
curl
中有一个--resolve
方法可以实现使用指定ip访问https网站。在iOS实现中,代码如下
其中
curlHost
形如:{HTTPS域名}:443:{IP地址}
_hosts_list
是结构体类型hosts_list
,可以设置多个IP与Host之间的映射关系。curl_easy_setopt
方法中传入CURLOPT_RESOLVE
将该映射设置到 HTTPS 请求中。这样就可以达到设置SNI的目的。
我在这里写了一个 Demo:CYLCURLNetworking,里面包含了编译好的支持 IPv6 的 libcurl 包,演示了下如何通过curl来进行类似NSURLSession。
走过的弯路
误以为 iOS11 新 API 可以直接拦截 DNS 解析过程
参考:NEDNSProxyProvider:DNS based on HTTP supported in iOS11
参考链接:
HTTPS(含SNI)业务场景“IP直连”方案说明》
补充说明
文中提到的几个概念:
文中部分提到的域名,如果没有特殊说明均指的是 FQDN。
The text was updated successfully, but these errors were encountered: