遇到的问题是什么
在App发布后,线上往往会有少量访问服务端的异常,这些异常有很多都是由于DNS解析出问题导致的。
具体问题可能是:
1、目标URL无法访问。(域名无法解析成为IP地址,DNS服务器无返回)
2、访问到错误的IP地址。(DNS将域名解析成了错误的IP地址)
3、webview打开H5可能访问到了钓鱼网站。(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字节)
我们只需要关注用到的两个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(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字节
前两字节跟查询一样,是用来匹配是哪一个查询对应的回复数据。
接下来的两个字节同样是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地址的具体数据,在一个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实现。