DNS与终端研发

遇到的问题是什么

在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实现。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • “DNS”这个单字,想必那些经常使用计算机,尤其是经常手动配置IP和DNS服务器的人是非常熟悉的。也许其中的大部...
    Ulrich蚊子阅读 2,528评论 0 6
  • 转:https://jocent.me/2017/06/18/dns-protocol-principle.htm...
    鹏_c719阅读 3,205评论 0 2
  • DNS 是什么 DNS (Domain Name System), 也叫网域名称系统,是互联网的一项服务。它实质上...
    interstellar多米诺阅读 2,721评论 0 6
  • 每当夜幕来临的时候,关上灯,窗外隐约的光透了进来,给孤独寂寞的我带来了些许温暖。看那地上的影子都嘲笑你的孤...
    破碎的水晶球阅读 238评论 0 1
  • 常用命令 基本使用方式 初始化仓库通常克隆的远程仓库直接便是初始化好的状态; 另一种方式是在本地初始化然后关联远程...
    heitufei阅读 263评论 0 0