一、基础背景
1. DNS解析
- 1:计算机会向我们的运营商(移动、电信、联通等)发出打开的请求。
- 2:运营商收到请求后会到自己的DNS服务器中找这个域名所对应的服务器的IP地址(也就是百度的服务器的IP地址),这里比如是180.149.132.47。
- 3:运营商用第二步得到的IP地址去找到百度的服务器请求得到数据后返回给我们。
其中第二步就是我们所说的DNS解析过程,域名和IP地址的关系其实就是我们的身份证号和姓名的关系,都是来标记一个人或者是一个网站的,只是IP地址\身份证号只是一串没有意义的数字,辨识度低,又不好记,所以就会在IP上加上一个域名以便区分,或是做的更加个性化,但是如果真的要来准确的区分还是要靠身份证号码或者是IP的,所以DNS解析就应运而生了。
2. 什么是DNS劫持
DNS劫持,是指在DNS解析过程中拦截域名解析的请求,然后做一些自己的处理,比如返回假的IP地址或者什么都不做使请求失去响应,其效果就是对特定的网络不能反应或访问的是假网址。根本原因就是以下两点:
- 1:恶意攻击,拦截运营商的解析过程,把自己的非法东西嵌入其中。
- 2:运营商为了利益或者一些其他的因素,允许一些第三方在自己的链接里打打广告之类的。
4. 防止DNS劫持
5. IP直连
它具有多方面的优势:
- 防劫持,可以绕过运营商 LocalDNS 解析过程,避免域名劫持,提高网络访问成功率。
- 降低延迟,DNS 解析是一个相对耗时的工作,跳过这个过程可以降低一定的延迟。
- 精准调度,运营商解析返回的节点不一定是最优的,自己获取 IP 可以基于自己的策略来获取最精准的、最优的节点。
5. 获取IP
对于获取 IP,有两种方案:
- HTTPDNS
- 内置IP列表
可以在启动等阶段由服务端下发域名和 IP 的对应列表,客户端来进行缓存,发起网络请求的时候直接根据缓存 IP 来进行业务访问。
二、实际应用场景中的问题
实现 HTTP 协议下 IP 连接其实是很简单的,我们只需要通过 NSURLProtocol 来拦截网络请求,然后将符号条件的网络请求 URL 中的域名修改为 IP 就可以啦。
但是会有各种各样的问题:
1.http请求服务器无法判断请求访问的内容
解决:由于服务器是根据host字段来判断请求的服务,所以在发起网络请求时,用带ip的URL生成request后,手动将request中的host字段改回域名。这样服务器可以正确识别,运营商也会根据域名中的ip为我们路由。
//原始URL
NSURL *originalUrl =[NSURL
//根据原始URL获取 第三方解析出的ip
NSString *ip = [self getHostByUrlSyn:url];
//替换ip后的URL
NSURL *url = [ip replaceHostWithIp:ip];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
//将request的host字段改为原始URL的域名
[request setValue:originalUrl.host forHTTPHeaderField:@"host"];
2. POST请求这块也算是一个大坑
#pragma mark -
#pragma mark 处理POST请求相关POST 用HTTPBodyStream来处理BODY体
- (NSMutableURLRequest *)handlePostRequestBodyWithRequest:(NSMutableURLRequest *)request {
NSMutableURLRequest * req = [request mutableCopy];
if ([request.HTTPMethod isEqualToString:@"POST"]) {
if (!request.HTTPBody) {
uint8_t d[1024] = {0};
NSInputStream *stream = request.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];
}
}
req.HTTPBody = [data copy];
[stream close];
}
}
return req;
}
这样之后的req就是携带了body体的request啦,可以愉快地做post请求啦。
3.Https请求证书校验错误
分析:
发送HTTPS请求首先要进行SSL/TLS握手,握手过程大致如下:
- 客户端发起握手请求,携带随机数、支持算法列表等参数。
- 服务端收到请求,选择合适的算法,下发公钥证书和随机数。
- 客户端对服务端证书进行校验,并发送随机数信息,该信息使用公钥加密。
- 服务端通过私钥获取随机数信息。
- 双方根据以上交互的信息生成session ticket,用作该连接后续数据传输的加密密钥。
上述过程中,和HTTPDNS有关的是第3步,客户端需要验证服务端下发的证书,验证过程有以下两个要点:
- 客户端用本地保存的根证书解开证书链,确认服务端下发的证书是由可信任的机构颁发的。
- 客户端需要检查证书的domain域和扩展域,看是否包含本次请求的host。
如果上述两点都校验通过,就证明当前的服务端是可信任的,否则就是不可信任,应当中断当前连接。
当客户端使用HTTPDNS解析域名时,请求URL中的host会被替换成HTTPDNS解析出来的IP,所以在证书验证的第2步,会出现domain不匹配的情况,导致SSL/TLS握手不成功。
解决方案:只需在验证时,传入真实的 host 即可:
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
forDomain:(NSString *)domain
{
/*
* 创建证书校验策略
*/
NSMutableArray *policies = [NSMutableArray array];
if (domain) {
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
} else {
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
}
/*
* 绑定校验策略到服务端的证书上
*/
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
/*
* 评估当前serverTrust是否可信任,
* 官方建议在result = kSecTrustResultUnspecified 或 kSecTrustResultProceed
*
* 关于SecTrustResultType的详细信息请参考SecTrust.h
*/
SecTrustResultType result;
SecTrustEvaluate(serverTrust, &result);
return (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
}
/*
* NSURLSession
*/
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler
{
if (!challenge) {
return;
}
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
NSURLCredential *credential = nil;
/*
* 获取原始域名信息。
*/
NSString* host = [[self.request allHTTPHeaderFields] objectForKey:@"host"];
if (!host) {
host = self.request.URL.host;
}
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if ([self evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:host]) {
disposition = NSURLSessionAuthChallengeUseCredential;
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
// 对于其他的challenges直接使用默认的验证方案
completionHandler(disposition,credential);
}
4. webview中H5页面部分
HTTPDNS实施的主要难点与坑点都在H5页面上面,下面逐条记录下在实施webview的HTTPDNS时遇到的问题:由于web页面的请求并不是由客户端发起,我们无法在生成request的时候修改host。
解决:在这里我们使用NSURLProtocol来解决。
用一句话解释NSURLProtocol :NSURLProtocol就是一个苹果允许的中间人攻击。
NSURLProtocol可以劫持系统所有基于C socket的网络请求。
注意:WKWebView基于Webkit,并不走底层的C socket,所以NSURLProtocol拦截不了WKWebView中的请求。
具体步骤为:
注册NSURLProtocol子类 -> 使用NSURLProtocol子类拦截Webview请求 -> 使用NSURLSession重新发起请求 -> 将NSURLSession请求的响应内容返回给Webview
- NSURLProtocol子类的实现:
拦截哪些请求
- request的URL是ip的(ipv4、ipv6)
- 非白名单的请求
/**
* 是否拦截处理指定的请求
*
* @param request 指定的请求
*
* @return 返回YES表示要拦截处理,返回NO表示不拦截处理
*/
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
//DNS开关控制功能开启关闭
if (![[HLJHttpDNS shareInstance] isDNSConfigWorking]) {
return NO;
}
/* 防止无限循环,因为一个请求在被拦截处理过程中,也会发起一个请求,这样又会走到这里,如果不进行处理,就会造成无限循环 */
if ([NSURLProtocol propertyForKey:protocolKey inRequest:request]) {
return NO;
}
// 防止无限循环, 第三方解析会发出ip域名的请求,这里筛选
// 判断请求URL的Host是否Ipv4
if ([WebViewURLProtocol checkHostIp:request.URL.host]) {
return NO;
}
NSString *url = [request.URL.host mutableCopy];
//去掉Ipv6的大括号
url = [url stringByReplacingOccurrencesOfString:@"[" withString:@""];
url = [url stringByReplacingOccurrencesOfString:@"]" withString:@""];
// 判断请求URL的Host是否Ipv6
if ([WebViewURLProtocol checkHostIpv6:url]) {
return NO;
}
NSMutableURLRequest *mutableReq = [request mutableCopy];
//假设原始的请求头部没有host信息,只有使用IP替换后的请求才有
NSString *host = [mutableReq valueForHTTPHeaderField:@"host"];
if (!mutableReq && host) {
return NO;
}
return YES;
}
在拦截的部分,我们需要注意一点,因为我们向第三方解析域名的请求也是ip的。这里我们需要在拦截时对域名的host位进行判断,如果是ipv4、ipv6的域名,就不对其进行拦截。不然程序就会循环拦截重新发起后的请求,导致程序卡死。
我们项目中图片服务是走CDN的服务器,还有其他统计等第三方的服务等等。我们将这类第三方的域名加入了白名单,在请求时会跳过对白名单内域名的拦截。
- 拦截住的请求怎么修改
- 替换域名为解析后的ip
- 修改request的host
- 修改证书校验中的host
拦截请求后,我们在重新发起的请求中对request进行修改:替换域名为解析后的ip、修改request的host
- (void)startLoading {
NSMutableURLRequest *request = [self.request mutableCopy];
// 表示该请求已经被处理,防止无限循环
[NSURLProtocol setProperty:@(YES) forKey:protocolKey inRequest:request];
NSMutableURLRequest *mutableReq = [request mutableCopy];
NSString *originalUrl = mutableReq.URL.absoluteString;
NSURL *url = [NSURL URLWithString:originalUrl];
// 同步接口获取IP地址
NSString *ip = [[HLJHttpDNS shareInstance] getHostByNameSyn:url.absoluteString];
if (ip) {
// 通过HTTPDNS获取IP成功,进行URL替换和HOST头设置
NSRange hostFirstRange = [originalUrl rangeOfString:url.host];
if (NSNotFound != hostFirstRange.location) {
mutableReq.URL = [NSURL URLWithString:ip];
// 添加原始URL的host
[mutableReq setValue:url.host forHTTPHeaderField:@"host"];
// 添加originalUrl保存原始URL
[mutableReq addValue:originalUrl forHTTPHeaderField:@"originalUrl"];
}
}
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue currentQueue]];
NSURLSessionTask *task = [_session dataTaskWithRequest:mutableReq];
[task resume];
}
在NSURLProtocol中拦截了请求后,在重新发起NSURLSession代理方法中,我们将证书校验的Host重新改回域名,这样就会通过证书校验过程。
#pragma NSURLSessionTaskDelegate
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *_Nullable))completionHandler {
if (!challenge) {
return;
}
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
NSURLCredential *credential = nil;
/*
* 获取原始域名信息。
*/
NSString *host = [[self.request allHTTPHeaderFields] objectForKey:@"host"];
if (!host) {
host = self.request.URL.host;
}
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if ([self evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:host]) {
disposition = NSURLSessionAuthChallengeUseCredential;
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
// 对于其他的challenges直接使用默认的验证方案
completionHandler(disposition, credential);
}