ChenYilong / ChenYilong/iOSBlog
iOS 防 DNS 污染方案调研(二)--- SNI 业务场景
- Dominant language
- Objective-C
- Stars
- 1.1k
- Forks
- 184
- PR merge metrics
- No merged PRs in 30d
Description
# iOS 防 DNS 污染方案调研--- SNI 业务场景
对应的GitHub仓库镜像地址在[这里](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/DNS污染方案调研/iOS防DNS污染方案调研---SNI业务场景.md) ,欢迎提PR进行修改。
## 概述
SNI(单IP多HTTPS证书)场景下,iOS上层网络库 `NSURLConnection/NSURLSession` 没有提供接口进行 `SNI 字段` 配置,因此需要 Socket 层级的底层网络库例如 `CFNetwork`,来实现 `IP 直连网络请求`适配方案。而基于 CFNetwork 的解决方案需要开发者考虑数据的收发、重定向、解码、缓存等问题(CFNetwork是非常底层的网络实现)。
针对 SNI 场景的方案, Socket 层级的底层网络库,大致有两种:
- 基于 CFNetWork ,hook 证书校验步骤。
- 基于原生支持设置 SNI 字段的更底层的库,比如 libcurl。
下面将目前面临的一些挑战,以及应对策略介绍一下:
### 支持 Post 请求
使用 NSURLProtocol 拦截 NSURLSession 请求丢失 body,故有以下几种解决方法:
方案如下:
1. 换用 NSURLConnection
2. 将 body 放进 Header 中
3. 使用 HTTPBodyStream 获取 body,并赋值到 body 中
4. 换用 Get 请求,不使用 Post 请求。
对方案做以下分析
- 换用 NSURLConnection ,不多说了,与 NSURLSession 相比终究会被淘汰,不作考虑。
- body放header的方法,2M以下没问题,超过2M会导致请求延迟,超过 10M 就直接 Request timeout。而且无法解决 Body 为二进制数据的问题,因为Header里都是文本数据。
- 换用 Get 请求,不使用 Post 请求。这个也是可行的,但是毕竟对请求方式有限制,终究还是要解决 Post 请求所存在的问题。如果是基于旧项目做修改,则侵入性太大。这种方案适合新的项目。
- 另一种方法是我们下面主要要讲的,使用 HTTPBodyStream 获取 body,并赋值到 body 中,具体的代码如下,可以解决上面提到的问题:
```Objective-C
//
// NSURLRequest+CYLNSURLProtocolExtension.h
//
//
// Created by ElonChan on 28/07/2017.
// Copyright © 2017 ChenYilong. All rights reserved.
//
#import
@interface NSURLRequest (CYLNSURLProtocolExtension)
- (NSURLRequest *)cyl_getPostRequestIncludeBody;
@end
//
// NSURLRequest+CYLNSURLProtocolExtension.h
//
//
// Created by ElonChan on 28/07/2017.
// Copyright © 2017 ChenYilong. All rights reserved.
//
#import "NSURLRequest+CYLNSURLProtocolExtension.h"
@implementation NSURLRequest (CYLNSURLProtocolExtension)
- (NSURLRequest *)cyl_getPostRequestIncludeBody {
return [[self cyl_getMutablePostRequestIncludeBody] copy];
}
- (NSMutableURLRequest *)cyl_getMutablePostRequestIncludeBody {
NSMutableURLRequest * req = [self mutableCopy];
if ([self.HTTPMethod isEqualToString:@"POST"]) {
if (!self.HTTPBody) {
NSInteger maxLength = 1024;
uint8_t d[maxLength];
NSInputStream *stream = self.HTTPBodyStream;
NSMutableData *data = [[NSMutableData alloc] init];
[stream open];
BOOL endOfStreamReached = NO;
//不能用 [stream hasBytesAvailable]) 判断,处理图片文件的时候这里的[stream hasBytesAvailable]会始终返回YES,导致在while里面死循环。
while (!endOfStreamReached) {
NSInteger bytesRead = [stream read:d maxLength:maxLength];
if (bytesRead == 0) { //文件读取到最后
endOfStreamReached = YES;
} else if (bytesRead == -1) { //文件读取错误
endOfStreamReached = YES;
} else if (stream.streamError == nil) {
[data appendBytes:(void *)d length:bytesRead];
}
}
req.HTTPBody = [data copy];
[stream close];
}
}
return req;
}
@end
```
使用方法:
在用于拦截请求的 `NSURLProtocol` 的子类中实现方法 `+canonicalRequestForRequest:` 并处理 `request` 对象:
```Objective-C
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
return [request cyl_getPostRequestIncludeBody];
}
```
下面介绍下相关方法的作用:
```Objective-C
//NSURLProtocol.h
/*!
@method canInitWithRequest:
@abstract This method determines whether this protocol can handle
the given request.
@discussion A concrete subclass should inspect the given request and
determine whether or not the implementation can perform a load with
that request. This is an abstract method. Sublasses must provide an
implementation.
@param request A request to inspect.
@result YES if the protocol can handle the given request, NO if not.
*/
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
/*!
@method canonicalRequestForRequest:
@abstract This method returns a canonical version of the given
request.
@discussion It is up to each concrete protocol implementation to
define what "canonical" means. However, a protocol should
guarantee that the same input request always yields the same
canonical form. Special consideration should be given when
implementing this method since the canonical form of a request is
used to look up objects in the URL cache, a process which performs
equality checks between NSURLRequest objects.
This is an abstract method; sublasses must provide an
implementation.
@param request A request to make canonical.
@result The canonical form of the given request.
*/
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
```
翻译下:
```Objective-C
//NSURLProtocol.h
/*!
* @method:创建NSURLProtocol实例,NSURLProtocol注册之后,所有的NSURLConnection都会通过这个方法检查是否持有该Http请求。
@parma :
@return: YES:持有该Http请求NO:不持有该Http请求
*/
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
/*!
* @method: NSURLProtocol抽象类必须要实现。通常情况下这里有一个最低的标准:即输入输出请求满足最基本的协议规范一致。因此这里简单的做法可以直接返回。一般情况下我们是不会去更改这个请求的。如果你想更改,比如给这个request添加一个title,组合成一个新的http请求。
@parma: 本地HttpRequest请求:request
@return:直接转发
*/
+ (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest *)request
```
简单说:
- `+[NSURLProtocol canInitWithRequest:]` 负责筛选哪些网络请求需要被拦截
- `+[NSURLProtocol canonicalRequestForRequest:]` 负责对需要拦截的网络请求`NSURLRequest` 进行重新构造。
这里有一个注意点:`+[NSURLProtocol canonicalRequestForRequest:]` 的执行条件是 `+[NSURLProtocol canInitWithRequest:]` 返回值为 `YES`。
注意在拦截 `NSURLSession` 请求时,需要将用于拦截请求的 NSURLProtocol 的子类添加到 `NSURLSessionConfiguration` 中,用法如下:
```Objective-C
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSArray *protocolArray = @[ [CYLURLProtocol class] ];
configuration.protocolClasses = protocolArray;
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
```
## 换用其他提供了SNI字段配置接口的更底层网络库
如果使用第三方网络库:curl, 中有一个 `-resolve` 方法可以实现使用指定 ip 访问 https 网站,iOS 中集成 curl 库,参考 [curl文档](https://curl.haxx.se/libcurl/c/CURLOPT_RESOLVE.html) ;
另外有一点也可以注意下,它也是支持 IPv6 环境的,只需要你在 build 时添加上 `--enable-ipv6` 即可。
curl 支持指定 SNI 字段,设置 SNI 时我们需要构造的参数形如: `{HTTPS域名}:443:{IP地址}`
假设你要访问. www.example.org ,若IP为 127.0.0.1 ,那么通过这个方式来调用来设置 SNI 即可:
> curl *** --resolve 'www.example.org:443:127.0.0.1'
### iOS CURL 库
使用[libcurl](https://curl.haxx.se/libcurl/c/) 来解决,libcurl / cURL 至少 7.18.1 (2008年3月30日) 在 SNI 支持下编译一个 SSL/TLS 工具包,`curl` 中有一个 `--resolve` 方法可以实现使用指定ip访问https网站。
在iOS实现中,代码如下
```Objective-C
//{HTTPS域名}:443:{IP地址}
NSString *curlHost = ...;
_hosts_list = curl_slist_append(_hosts_list, curlHost.UTF8String);
curl_easy_setopt(_curl, CURLOPT_RESOLVE, _hosts_list);
```
其中 `curlHost` 形如:
`{HTTPS域名}:443:{IP地址}`
`_hosts_list` 是结构体类型`hosts_list`,可以设置多个IP与Host之间的映射关系。`curl_easy_setopt`方法中传入`CURLOPT_RESOLVE` 将该映射设置到 HTTPS 请求中。
这样就可以达到设置SNI的目的。
我在这里写了一个 Demo:[CYLCURLNetworking](https://github.com/ChenYilong/CYLCURLNetworking),里面包含了编译好的支持 IPv6 的 libcurl 包,演示了下如何通过curl来进行类似NSURLSession。
## 走过的弯路
## 误以为 iOS11 新 API 可以直接拦截 DNS 解析过程
参考:[NEDNSProxyProvider:DNS based on HTTP supported in iOS11](https://github.com/ChenYilong/iOS11AdaptationTips/issues/12)
## 参考链接:
- [Apple - Communicating with HTTP Servers](https://developer.apple.com/library/content/documentation/Networking/Conceptual/CFNetwork/CFHTTPTasks/CFHTTPTasks.html?spm=5176.doc30143.2.3.5016q8)
- [Apple - HTTPS Server Trust Evaluation - Server Name Failures ](https://developer.apple.com/library/content/technotes/tn2232/_index.html?spm=5176.doc30143.2.4.5016q8#//apple_ref/doc/uid/DTS40012884-CH1-SECSERVERNAME)
- [Apple - HTTPS Server Trust Evaluation - Trusting One Specific Certificate ](https://developer.apple.com/library/content/technotes/tn2232/_index.html?spm=5176.doc30143.2.5.5016q8#//apple_ref/doc/uid/DTS40012884-CH1-SECCUSTOMCERT)
- [《HTTPDNS > 最佳实践 > HTTPS(含SNI)业务场景“IP直连”方案说明
HTTPS(含SNI)业务场景“IP直连”方案说明》]( https://help.aliyun.com/document_detail/30143.html?spm=5176.doc30141.6.591.A8B1d3 )
- [《在 curl 中使用指定 ip 来进行请求 https》]( https://blog.mozcp.com/curl-request-https-specify-ip/ )
- [支持SNI与WebView的 alicloud-ios-demo](https://github.com/Dave1991/alicloud-ios-demo)
- [《SNI: 实现多域名虚拟主机的SSL/TLS认证》]( https://shansing.com/read/355/ )
## 补充说明
文中提到的几个概念:
概念 | 解释 | 举例
-------------|-------------|-------------
host | 可以是 IP 也可以是 FQDN。 | www.xxx.com 或 1.1.1.1
FQDN | fully qualified domain name,由主机名和域名两部分组成 | www.xxx.com
域名 | 域名分为全称和简称,全称就是FQDN、简称就是 FQDN 不包括主机名的部分 | 比如:xxx.com ,也就是www.xxx.com 这个 FQDN 中,www 是主机名,xxx.com 是域名。
文中部分提到的域名,如果没有特殊说明均指的是 FQDN。
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.