NSURLProtocol — DNS劫持的解决方案

作者: __Mr_Xie__ | 来源:发表于2019-07-31 16:36 被阅读45次

    前言

    研究 NSURLProtocol 初衷是为了动态改变网络请求的 DNS 服务器的 IP,事实上我已经实现了,所以和大家分享一下。
    demo地址

    小插曲

    • DNSPOD相关
    DNSPOD简介

    我们知道要要把项目中请求的接口替换成成 IP 其实很简单,URL 是字符串,域名替换 IP,无非就是一个字符串替换而已,的确这块其实没有什么技术含量,而且现在像阿里云(没开源)七牛云(开源),等一些比较大的平台在这方面也都有了比较成熟的解决方案,一个 SDK,传个普通的 URL 进去就会返回一个域名被替换成 IPURL 出来,也比较好用,这里要说一下 IP 地址的来源,如何拿到一个域名所对应的 IP 呢?这里就是需要用到另一个服务 — HTTPDNS,国内比较有名的就是 DNSPOD,包括阿里,七牛等也是使用他们的 DNS 服务来解析。

    DNSPOD 会给我们提供一个接口,我们使用 HTTP 请求的方式去请求这个接口,参数带上我们的域名,他们就会把域名对应的 IP 列表返回,接口如下:

    /*
    这个请求URL的结构是固定的,119.29.29.29是DNSPOD固定的服务器地址;
    ttl参数的意思是返回结果是否带ttl是个BOOL;
    dn:需要解析的域名;
    id:就是我们在dnspod上注册时候他给我们的一个KEY;
    */
    
    http://119.29.29.29/d?ttl=1&dn=www.baidu.com&id=KEY
    
    • DNSPOD运用

    如果想要在一个已经比较完善的 APP 中加入 DNS 防劫持的话,就必须拿到所有网络请求的控制权,这篇文章中我主要使用是 NSURLProtocol + Runtime hook 方式来处理这些东西的。

    NSURLProtocol 属于 iOS 黑魔法的一种,是属于 Foundation 框架里的 URL Loading System 的一部分,它是一个抽象类,不能去实例化它,只能子类化 NSURLProtocol,然后使用的时候注册子类。


    NSURLProtocol 可以拦截任何从 AppURL Loading System 系统中发出的请求,包括如下:
    • ftp://
      File Transfer Protocol
    • http://
      Hypertext Transfer Protocol
    • https://
      Hypertext Transfer Protocol with encryption
    • file:///
      Local file URLs
    • data://
      Data URLs

    注:如果你的请求不在以上列表中就不能进行拦截了,比如 WKWebviewAVPlayer(比较特殊,虽然请求也是 http/https 但是就是不走这套系统)等,其实对于正常来说光用 NSURLProtocol 已经足够了。

    了解几个概念

    • DNS解析

    现在假如我们访问一个网站 www.baidu.com 从按下回车到百度页面显示到终端会经历如下几个步骤:

    1. 计算机会向我们的 运营商 (移动电信联通等)发出打开 www.baidu.com 的请求。
    2. 运营商收到请求后会到自己的DNS服务器中找 www.baidu.com 这个域名所对应的服务器的 IP 地址(也就是百度的服务器的 IP 地址),这里比如是 180.149.132.47
    3. 运营商用第二步得到的 IP 地址去找到百度的服务器请求得到数据后返回给我们。

    其中第二步就是我们所说的 DNS解析过程,域名和 IP 地址的关系其实就是我们的身份证号和姓名的关系,都是来标记一个人或者是一个网站的,只是 IP 地址或身份证号只是一串没有意义的数字,辨识度低,又不好记,所以就会在 IP 上加上一个域名以便区分,或是做的更加个性化,但是如果真的要来准确的区分还是要靠身份证号码或者是 IP 的,所以 DNS 解析就应运而生了。

    • DNS劫持

    DNS 劫持,是指在 DNS 解析过程中拦截域名解析的请求,然后做一些自己的处理,比如返回假的 IP 地址或者什么都不做使请求失去响应,其效果就是对特定的网络不能反应或访问的是假网址。根本原因就是以下两点:

    1. 恶意攻击,拦截运营商的解析过程,把自己的非法东西嵌入其中。
    2. 运营商为了利益或者一些其他的因素,允许一些第三方在自己的链接里打打广告之类的。
    • DNS劫持的解决方案

    了解了 DNS 劫持的相关资料后我们就知道了,防止 NDS 劫持就要从第二步入手,因为 DNS 解析过程是运营商来操作的,我们不能去干涉他们,不然我们也就成了劫持者了,所以我们要做的就是在我们请求之前对我们的请求链接做一些修改,将我们原本的请求链接 www.baidu.com 修改为 180.149.132.47,然后请求出去,这样的话就运营商在拿到我们的请求后发现我们直接用的就是 IP 地址就会直接给我们放行,而不会去走他自己 DNS 解析了,也就是说我们把运营商要做的事情自己先做好了。不走他的 DNS 解析也就不会存在 DNS 被劫持的问题,从根本是解决了。

    NSURLProtocol可以实现的功能

    • 重定向网络请求(可以解决之前电信的 DNS 域名劫持问题)
    • 缓存
    • 自定义 Response(过滤敏感信息)
    • 全局网络请求设置
    • HTTP Mocking

    NSURLProtocol的子类需要实现的方法

    // 这个方法返回一个布尔值告诉系统该请求是否需要处理,返回Yes才能进行后续处理。
    + (BOOL)canInitWithRequest:(NSURLRequest *)request;
    
    // 如果想对某个请求添加请求头或者返回新的请求时,可以在这个方法里自定义然后返回,一般情况下直接返回参数里的NSURLRequest实例即可。
    + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
    
    // 这个方法能够判断当拦截URL相同时是否使用缓存数据
    + (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b;
    
    // 开始请求
    - (void)startLoading;
    
    // 取消请求
    - (void)stopLoading;
    
    方法的大致执行流程

    使用

    demo地址

    1. 子类化
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface XWCustomURLProtocol : NSURLProtocol
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    2. 注册
    [NSURLProtocol registerClass:[XWCustomURLProtocol class]];
    

    NSURLConnection 发起请求的时候,会让所有已注册的 XWCustomURLProtocol 来“审批”这个请求

    注意: 如果是基于 NSURLSession 进行的请求,注册的时候需要注册到 NSURLSessionConfiguration 中,如下:

    NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSArray *protocolArray = @[[XWCustomURLProtocol class]];
    configuration.protocolClasses = protocolArray;
    NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    NSURLSessionTask *task = [session dataTaskWithRequest:request];
    [task resume];
    
    3. 注册必须实现的五个方法
    • 这个方法返回一个布尔值告诉系统该请求是否需要处理,返回 Yes 才能进行后续处理。
    + (BOOL)canInitWithRequest:(NSURLRequest *)request {
        NSString * scheme = [[request.URL scheme] lowercaseString];
        
        // 看看是否已经处理过了,防止无限循环 根据业务来截取
        if ([NSURLProtocol propertyForKey: URLProtocolHandledKey inRequest:request]) {
            return NO;
        }
        
        // TODO - 这里是自己需要处理的逻辑,这里就简单举个例子处理一下
        if ([scheme isEqual:@"http"] || [scheme isEqual:@"https"]) {
            return YES;
        }
        
        return NO;
    }
    
    • 如果想对某个请求添加请求头或者返回新的请求时,可以在这个方法里自定义然后返回,一般情况下直接返回参数里的 NSURLRequest 实例即可。
    + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
        return request;
    }
    
    • 这个方法能够判断当拦截 URL 相同时是否使用缓存数据
    + (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
        return [super requestIsCacheEquivalent:a toRequest:b];
    }
    
    • 开始请求。可以在这里修改请求信息,重定向,DNS 解析,返回自定义的测试数据等。以下代码实现的是:修改 DNS 解析的服务器地址,用新的 DNS 解析的服务器地址去做 DNS 解析。
    - (void)startLoading {
        NSLog(@"***监听接口:%@", self.request.URL.absoluteString);
        
        NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
        //标示该request已经处理过了,防止无限循环
        [NSURLProtocol setProperty:@(YES) forKey:URLProtocolHandledKey inRequest:mutableReqeust];
        
        // dns解析
        NSMutableURLRequest *request = [self.class replaceHostInRequset:mutableReqeust];
        
        // 使用NSURLSession继续把request发送出去
        NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
        NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
        self.session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:mainQueue];
        NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request];
        [task resume];
    }
    
    + (NSMutableURLRequest *)replaceHostInRequset:(NSMutableURLRequest *)request {
        NSLog(@"%s", __func__);
        if ([request.URL host].length == 0) {
            return request;
        }
        
        NSString *originUrlString = [request.URL absoluteString];
        NSString *originHostString = [request.URL host];
        NSRange hostRange = [originUrlString rangeOfString:originHostString];
        if (hostRange.location == NSNotFound) {
            return request;
        }
        
        // 用HappyDNS 替换host
        NSMutableArray *array = [NSMutableArray array];
        // 第一dns解析为114,第二解析才是系统dns
        [array addObject:[[QNResolver alloc] initWithAddress:@"114.114.115.115"]];
        [array addObject:[QNResolver systemResolver]];
        QNDnsManager *dnsManager = [[QNDnsManager alloc] init:array networkInfo:[QNNetworkInfo normal]];
        NSArray *queryArray = [dnsManager query:originHostString];
        if (queryArray && queryArray.count > 0) {
            NSString *ip = queryArray[0];
            if (ip && ip.length) {
                // 替换host
                NSString *urlString = [originUrlString stringByReplacingCharactersInRange:hostRange withString:ip];
                NSURL *url = [NSURL URLWithString:urlString];
                request.URL = url;
                
                [request setValue:originHostString forHTTPHeaderField:@"Host"];
            }
        }
        
        return request;
    }
    

    其中 URLProtocolHandledKey 为:

    static NSString * const URLProtocolHandledKey = @"URLProtocolHandledKey";
    
    • 取消请求。
    - (void)stopLoading {
        [self.session invalidateAndCancel];
        self.session = nil;
    }
    
    4. 拦截之后的处理过程

    需要注意的是 XWCustomURLProtocol 的父类中有一个 client 属性,如下:

    /*!
        @abstract Returns the NSURLProtocolClient of the receiver. 
        @result The NSURLProtocolClient of the receiver.  
    */
    @property (nullable, readonly, retain) id <NSURLProtocolClient> client;
    

    遵守代理 NSURLProtocolClient,需要实现的方法如下:

    - (void)URLProtocol:(NSURLProtocol *)protocol wasRedirectedToRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse;
    
    - (void)URLProtocol:(NSURLProtocol *)protocol cachedResponseIsValid:(NSCachedURLResponse *)cachedResponse;
    
    - (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy;
    
    - (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data;
    
    - (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol;
    
    - (void)URLProtocol:(NSURLProtocol *)protocol didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
    
    - (void)URLProtocol:(NSURLProtocol *)protocol didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
    

    对于需要处理的 NSURLSession,可以在 NSURLSessionDelegate 中进行操作:

    - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
        [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
        completionHandler(NSURLSessionResponseAllow);
    }
    
    - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
        // 打印返回数据
        NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        if (dataStr) {
            NSLog(@"***截取数据***: %@", dataStr);
        }
        [self.client URLProtocol:self didLoadData:data];
    }
    
    - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
        if (error) {
            [self.client URLProtocol:self didFailWithError:error];
        } else {
            [self.client URLProtocolDidFinishLoading:self];
        }
    }
    

    Author

    如果你有什么建议,可以关注我的公众号:iOS开发者进阶,直接留言,留言必回。

    参考

    NSURLProtocol
    NSURLProtocol 详解
    IOS下三种DNS解析方式分析(LocalDns)
    可能是最全的iOS端HttpDns集成方案
    NSURLProtocol -- DNS劫持和Web资源本地化

    相关文章

      网友评论

        本文标题:NSURLProtocol — DNS劫持的解决方案

        本文链接:https://www.haomeiwen.com/subject/srcgrctx.html