美文网首页
DNS与终端研发

DNS与终端研发

作者: 于天佐 | 来源:发表于2020-01-20 12:48 被阅读0次

    遇到的问题是什么

    在App发布后,线上往往会有少量访问服务端的异常,这些异常有很多都是由于DNS解析出问题导致的。
    具体问题可能是:
    1、目标URL无法访问。(域名无法解析成为IP地址,DNS服务器无返回)
    2、访问到错误的IP地址。(DNS将域名解析成了错误的IP地址)
    3、webview打开H5可能访问到了钓鱼网站。(DNS恶意解析)
    最终的结果都是导致终端访问服务端时发生错误,从而影响程序正确,降低用户体验。

    一个例子是:


    某一域名被本地DNS服务屏蔽

    这是某一个局域网内,通过本地网络提供的DNS服务解析youtube.com的结果。从结果上看,本地DNS主动阉割了youtube.com,这就是一个典型的DNS劫持,后果是你找不到主机IP,最后导致访问服务失败。

    DNS

    概念

    Domain Name Server 域名系统。它来提供域名解析服务,完成域名-->IP的转换。
    为什么要有域名到IP地址的转化?因为IP地址是标识机器身份的符号,而域名是供人类方便记忆的符号。在程序层面,自然需要做人类可以读懂的符号到机器可以读懂的符号的转换。

    域名

    域名层级是一个层级的树形结构,其语法是:域名由一或多个部分组成,这些部分通常连接在一起,并由点分隔,例如zh.wikipedia.org。最右边的一个标签是顶级域名,例如zh.wikipedia.org的顶级域名是org。一个域名的层次结构,从右侧到左侧隔一个点依次下降一层。每个标签可以包含1到63个八字节。域名的结尾有时候还有一点,这是保留给根节点的,书写时通常省略,在查询时由软件内部补上。


    域名的树形结构

    域名服务器的分级

    • 根域名服务器:全球13组根域名服务器以英文字母A到M依序命名,域名格式为“字母.root-servers.net”。主根服务器位于美国,其余12组为辅根服务器(9组在美国,2组在欧洲,1组在日本)。
      根域名服务器在中国大陆有F、I、K、L根镜像服务器。
    • 顶级域名服务器:提供com、net、org、cn等的域名解析服务
    • 域名分销商提供的具体域名服务器

    通过DNS查询IP的过程

    客户端直接访问的DNS服务器会将客户端发来的域名请求的域名逐级分解,然后分别进行域名查询,将最终结果返回给客户端。这个过程被称为递归查询。


    递归查询的过程

    这是本地用dig baidu.com +trace命令进行递归查询模拟的返回结果:

    ; <<>> DiG 9.10.6 <<>> baidu.com +trace
    ;; global options: +cmd
    .           188300  IN  NS  k.root-servers.net.
    .           188300  IN  NS  g.root-servers.net.
    .           188300  IN  NS  a.root-servers.net.
    .           188300  IN  NS  j.root-servers.net.
    .           188300  IN  NS  i.root-servers.net.
    .           188300  IN  NS  b.root-servers.net.
    .           188300  IN  NS  f.root-servers.net.
    .           188300  IN  NS  e.root-servers.net.
    .           188300  IN  NS  d.root-servers.net.
    .           188300  IN  NS  l.root-servers.net.
    .           188300  IN  NS  m.root-servers.net.
    .           188300  IN  NS  h.root-servers.net.
    .           188300  IN  NS  c.root-servers.net.
    .           464643  IN  RRSIG   NS 8 0 518400 20200201050000 20200119040000 33853 . zmM/gCiOlLmdrcx1+Ae8f4vXVmEtCAXXPhHJqMb961AXYWvZuEn3BWPM Tna3OX1y2igyKyCGE5fgYMz7y3XGxwpmPIP2xD9XswGsrzBhqsyCq+kg Is2+iTIy2vTfPnsmLCx/id/H6Sn9XzAFwt/omepqOMQQdt/TsRDZUrV9 5X1LuL0ulI/Dm2wu8lart4Zv8RnGNsbABoVzs9KFwUwqItP5QDa6thja SbLwqOhV0tY0zyZ45lXfDWCvTmVRvyZ2NcamONxWDzTEutf2X9uGayjq Yd+bA0ebXTRv3nkEJet82QbGP9xdPvIapeJ2vQosPYdXFkqpAp5FP3Q7 Mu85hQ==
    ;; Received 733 bytes from 192.168.4.251#53(192.168.4.251) in 5 ms
    //以上进行第一次查询,查询根域名服务
    com.            172800  IN  NS  b.gtld-servers.net.
    com.            172800  IN  NS  c.gtld-servers.net.
    com.            172800  IN  NS  g.gtld-servers.net.
    com.            172800  IN  NS  e.gtld-servers.net.
    com.            172800  IN  NS  h.gtld-servers.net.
    com.            172800  IN  NS  i.gtld-servers.net.
    com.            172800  IN  NS  d.gtld-servers.net.
    com.            172800  IN  NS  a.gtld-servers.net.
    com.            172800  IN  NS  f.gtld-servers.net.
    com.            172800  IN  NS  k.gtld-servers.net.
    com.            172800  IN  NS  j.gtld-servers.net.
    com.            172800  IN  NS  m.gtld-servers.net.
    com.            172800  IN  NS  l.gtld-servers.net.
    com.            86400   IN  DS  30909 8 2 E2D3C916F6DEEAC73294E8268FB5885044A833FC5459588F4A9184CF C41A5766
    com.            86400   IN  RRSIG   DS 8 1 86400 20200201170000 20200119160000 33853 . qdsPvzT8MgPoeSr27CAMKX+UluFKCx5FAHPAHwyR2KC8w99QXlVRYiNw p0Pm3PanfSXek2cCkPjxnOwBmnB1mKQsOOZszRNdl4Q2Tv/h8kNabL3n efP2yNShgiys0phAl+X7gEK4OXbfb3ffJX0/GdhuxEBV2CCcBHcoN7kH N/speLKVJElZxVoIrqxdi3foddrTMGwyeugGpSi3JCICsMEfOQ8dSWQO q0bdEWHdVw5UhV/rC1jBmvWhk1bWlSSai+tkBY1A08xdLrladHLeUBD5 9ucrVbsmOI1jGnWtWiO2ogegJlQXV/6QcnSFqvL6q7+sTkIq0iUVFAG7 A/fyjQ==
    ;; Received 1169 bytes from 192.112.36.4#53(g.root-servers.net) in 109 ms
    //以上进行第二次查询,查询顶级域名服务
    baidu.com.      172800  IN  NS  ns2.baidu.com.
    baidu.com.      172800  IN  NS  ns3.baidu.com.
    baidu.com.      172800  IN  NS  ns4.baidu.com.
    baidu.com.      172800  IN  NS  ns1.baidu.com.
    baidu.com.      172800  IN  NS  ns7.baidu.com.
    CK0POJMG874LJREF7EFN8430QVIT8BSM.com. 86400 IN NSEC3 1 1 0 - CK0Q1GIN43N1ARRC9OSM6QPQR81H5M9A  NS SOA RRSIG DNSKEY NSEC3PARAM
    CK0POJMG874LJREF7EFN8430QVIT8BSM.com. 86400 IN RRSIG NSEC3 8 2 86400 20200126055014 20200119044014 56311 com. NcJcIRRw7pXwPdxkhDo/FJiXqzuNXVWc3cjoFHtkMyhCCt7JCPk5d7rK iKj5KOAtJ5fq/5UnNb3FTUrrd5YQgK1fkCCG9E1vZ7626YD0N9eVAcRM M75NPBo7IBJoS8Ko8ekQttNC9DfVOfQTHUhEPNbDZ4lCDUeyYLh2JvPB xOzRtQ4AfM2Fycu2/QgS4isGR/ktIqGz63pCPQpGrXoDyw==
    HPVUNU64MJQUM37BM3VJ6O2UBJCHOS00.com. 86400 IN NSEC3 1 1 0 - HPVVN3Q5E5GOQP2QFE2LEM4SVB9C0SJ6  NS DS RRSIG
    HPVUNU64MJQUM37BM3VJ6O2UBJCHOS00.com. 86400 IN RRSIG NSEC3 8 2 86400 20200125052224 20200118041224 56311 com. yiowTXPeqTvrbxmMzD3eRl4CpkcHFdZaP67jv7GteB5AjJ4WbDaiSxux 9jO5b7sZou/l4CPkXF4aXyDrJxJYMmWtN3+FRSpkImrmi7zIELq+9BFl pqb7nDXh3EkU5VJWDhoJGESqOXVyIHSBNtg8kLkP4opV3JzWn8am4Iik E7Vl5a5j/c3xiIgNF7Wan/M5gkizsjaGxH5e2rME8r6ytQ==
    ;; Received 757 bytes from 192.54.112.30#53(h.gtld-servers.net) in 178 ms
    //以上进行第三次查询,根据某一台顶级域名服务地址查询baidu.com所在的域名查询服务器
    baidu.com.      600 IN  A   220.181.38.148
    baidu.com.      600 IN  A   39.156.69.79
    baidu.com.      86400   IN  NS  ns4.baidu.com.
    baidu.com.      86400   IN  NS  ns7.baidu.com.
    baidu.com.      86400   IN  NS  ns3.baidu.com.
    baidu.com.      86400   IN  NS  dns.baidu.com.
    baidu.com.      86400   IN  NS  ns2.baidu.com.
    ;; Received 240 bytes from 14.215.178.80#53(ns4.baidu.com) in 44 ms
    //以上进行第四次查询,在为baidu.com提供查询服务的服务器查询到最终结果
    

    以上可以看做是我们通常访问服务之前,域名服务器的工作过程,在任何一环出现问题,我们的本地程序最终都会取得错误的或者取不到IP地址,最终导致程序访问服务失败。

    终端开发如何解决DNS解析失败问题

    客户端目前最有效的解决方案是用httpdns替代本地DNS解析,即信任大厂提供的httpdns服务,这种信任,确实比新人小运营商提供的DNS解析服务要可靠。

    HTTPDNS

    总的来说,HttpDNS 作为移动互联网时代 DNS 优化的一个通用解决方案,主要解决了以下几类问题:
    LocalDNS 劫持/故障
    LocalDNS 调度不准确
    这段话来自腾讯云提供的httpDNS文档,它的原理很简单,即用http协议来进行hostname-->IP的查询过程。http库在如今的移动端开发中都已经是标配的基础设施,这样的设计可以很方便的让开发者接入这项服务。
    具体的接入有详细的文档说明:https://cloud.tencent.com/document/product/379/17655

    另一种方案

    httpDNS的方案可以很好的解决DNS故障以及劫持等问题,但是它并不免费。
    这篇文章重点要介绍的就是下面这种免费也相对可靠的方案。

    实现DNS解析查询

    通过DNS的征求意见稿(https://tools.ietf.org/rfc/rfc1035.txt),我们完全可以实现协议的收发来自实现一个DNS服务。

    前提

    • 前提条件:国内以及全球多家公司提供了免费的public DNS服务,如114dns的公共DNS服务114.114.114.114,腾讯云的公共DNS服务119.29.29.29,google的公有DNS服务8.8.8.8等。
    • 以上的DNS服务速度和稳定性均足以替代本地DNS,如果作为本地DNS解析失败的降级方案非常合适。
    PING 119.29.29.29 (119.29.29.29): 56 data bytes
    64 bytes from 119.29.29.29: icmp_seq=0 ttl=35 time=12.597 ms
    64 bytes from 119.29.29.29: icmp_seq=1 ttl=35 time=10.896 ms
    64 bytes from 119.29.29.29: icmp_seq=2 ttl=35 time=12.571 ms
    64 bytes from 119.29.29.29: icmp_seq=3 ttl=35 time=16.304 ms
    64 bytes from 119.29.29.29: icmp_seq=4 ttl=35 time=12.271 ms
    64 bytes from 119.29.29.29: icmp_seq=5 ttl=35 time=12.572 ms
    64 bytes from 119.29.29.29: icmp_seq=6 ttl=35 time=11.376 ms
    64 bytes from 119.29.29.29: icmp_seq=7 ttl=35 time=11.761 ms
    64 bytes from 119.29.29.29: icmp_seq=8 ttl=35 time=12.495 ms
    

    我们可以看到腾讯提供的免费公共DNS服务的反应时间是很快的,比本地DNS服务的ping值平均只高7-8ms。结合上层的域名解析结果缓存策略来看,高出的时间完全可以被忽略。

    实现

    DNS服务通常都用UDP来做传输协议,服务端口为53。我们只需要构建好查询的数据,通过UDP协议发送,然后分析返回的数据,即可完成DNS的查询过程。

    构建查询数据

    先来分析一下查询数据的构成。

    • 传输ID(第1、2字节)


      传输顺序号

      查询数据的开始两个字节是传输ID,用它来做查询和结果的匹配。

    • flags(第3、4字节)


      查询flags

      我们只需要关注用到的两个bit位,即第一字节的最高位,它代表是一个查询还是一个响应;另一个是第一字节的最低位,我们用1来告知DNS服务器,我们要求它做递归查询,即我们要拿到的是最终的IP地址结果。

    • 资源数(第5-12字节)


      资源数

      这8个字节代表4个数字,分别是查询的问题数、相应资源数、权威结果数、附加数据数。他们表示了相应顺序的具体协议数据数量,即从13字节开始,有一个query数据。我们这里只需要关注questions数即可,因为这是一个查询协议数据,只带有查询数据。

    • 查询数据中的名字数据


      查询数据中的名字数据

      这是从第13自己开始的查询数据,查询数据中的第一段数据是查询的域名名字字段。这里的数据构成是将域名以点分割开,每一段成为label,数据如下:
      label长度+label+label长度+label+...+0
      以上图为例,第13字节是0x04,代表后面4字节为第一label(stun),后面第二红框0x0a,代表后面10字节为第二label(freesitch),第三红框0x03代表后面3字节为第三label(org),第四红框0,代表整个名字结束。

    • 查询数据的最后4字节


      查询type
      查询class

      查询的type(2字节),为1,代表我们期望查询的是A记录,即IPV4地址。
      查询的class(2字节),为1,代表internet,不必关心。

    以上完整的构建了查询DNS的查询数据。下面是关键代码片段:

    #pragma pack(1)
    struct DnsHeader
    {
        std::int16_t trans_id;
        std::int8_t flags[2];
        std::int16_t questions;
        std::int16_t answer_rrs;
        std::int16_t authority_rrs;
        std::int16_t additional_rrs;
    };
    #pragma pack()
    
        ///construct simple dns query request
        std::string dns_query(std::string const &str_host)
        {
            std::string request;
            const int buff_len = 256;
            char buff[buff_len] = {0};
            int index = 0;
    
            ::srand(::time(nullptr));
            trans_id = rand();
    
            stringxa strx_host(str_host);
            strx_host.trim();
            std::vector<stringxa> hosts;
            strx_host.split_string(".", hosts);
    
            ///header
            DnsHeader header = {0};
            header.trans_id = htons((std::int16_t)trans_id);
    
            //Do query recursively
            header.flags[0] = 1;
            header.flags[1] = 0;//flags  0 0000 0 1 0000 0000
    
            header.questions = htons(1);
    
            header.answer_rrs = htons(0);
    
            header.authority_rrs = htons(0);
    
            header.additional_rrs = htons(0);
    
            memcpy(buff, &header, sizeof(header));
            index += sizeof(header);
            ///header end
    
            ///queries
            for (const auto &host : hosts)
            {
                if (host.size() > 0x3f) //00xx xxxx domain label MUST NOT > 0x3f(0011 1111)
                {
                    return "";
                }
    
                buff[index++] = host.size();
                for (auto c : host)
                {
                    buff[index++] = c;
                }
            }
            index++; //end with a ZERO
    
            buff[index++] = 0;
            buff[index++] = 1; //type A --> 0x0001
    
            buff[index++] = 0;
            buff[index++] = 1; //class IN --> 0x0001
    
            ///queries end
    
            request.assign(buff, index > buff_len ? buff_len : index);
            return request;
        }
    

    分析回复数据

    • 前4字节


      回复数据前4字节

      前两字节跟查询一样,是用来匹配是哪一个查询对应的回复数据。
      接下来的两个字节同样是flags,只不过作为查询数据,我们关心第一字节的最高位(1代表是回复数据),第一字节的最低位(要求递归查询,返回查询传来的数据位),第二字节的最高位(代表DNS服务器是否支持递归查询),第二字节低四位(回复数据的状态码,0为成功)。我们用上述的几个关键bit位,就可以判断回复数据是否是我们需要的,是否需要进一步进行分析。

    • 资源数(8字节)


      回复资源数

      这里的8字节同查询数据一致,这里的场景是表示1个查询数据块,2个回复数据块,6个权威服务数据块,9个附加数据块。查询数据块是原封不动将查询的数据转发回来,这里我们只关心接下来的两个回复数据块。

    • 回复数据中的名字数据


      回复数据中的名字数据

      我们来看回复的数据(即answer数据块,多个answer数据一个接一个的顺序排布),首先同查询数据一样,是名字数据。我们看到,这里的名字数据和查询数据中的名字数据明显不同,因为这段数据在前面的查询数据块中出现过,所以这里采取了一个压缩的方式来指明回复数据快中的名字信息。这里用两个字节来指明名字数据相对数据起始位置的便宜。当第一字节的高两位为11时,代表名字信息为一个指针,当一个名字数据是指针时,它固定占用2字节来指向前面出现过的数据。16个bit位,前两个为11,后14个来用作指针偏移量。
      The pointer takes the form of a two octet sequence:
      +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
      | 1 1 |   OFFSET                        |
      +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
      正因为第一个字节用了高两位作为是否为指针的标识,所以第一个字节作为label长度时,只能用到后6位,即最大为0x3f(63),这也是为什么URL中的每一级域名不能超过63个字节。

    • 回复中的IP地址数据


      IPV4地址数据

      最终,我们关心的只有IPV4地址的具体数据,在一个answer数据中,当type为1(即A记录,IPV4)时,最后的4字节数据即为IPV4的地址信息,也就是我们发起DNS请求所要获取的真正数据。

    以上是整个查询和回复数据的构成,回复数据的解析部分代码比较长,可以参见具体实现:https://github.com/yutianzuo/android-nativesocket/blob/master/nativesock/src/main/cpp/netutils/dns.h

    最后

    这里的实现是DNS协议规定的一小部分,只能覆盖到我们遇到的问题的解决。如果想要完整的DNS协议解析,那么开源实现将会是最终的解决方案:https://github.com/c-ares/c-ares,它也是被chrome、curl等所采用的DNS实现。

    相关文章

      网友评论

          本文标题:DNS与终端研发

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