实际应用中发现一个问题,在某些国家/ 地区的某些 ISP 提供的网络中,程序在请求 DNS 以链接一些服务器的时候,有时候会由于 ISP 的 DNS 递归查询太慢,致使设备端认为 DNS 超时了,没法获取服务器 IP。html
给用户的解决方案是:请不要用 ISP 自动分配的 DNS server,改用 8.8.8.8 就解决了。git
可是让用户这么配置太麻烦、也太不友好了。因而我就思考:能不能本身实现 DNS 服务,当 ISP 的 DNS 请求超时或者失败的时候,就从内部直接向 8.8.8.8 请求 DNS 信息,能够不?github
若是要使用 gethostbyname()
和 getaddrinfo()
来解决这个问题的话,方案是将 /etc/resolve.conf
修改了。但这并非正确的办法,由于这种改法一来不许确,二来会影响系统其余 DNS 请求。可行的方案是:本身构建 DNS 请求,而且本身解析得到咱们须要的 IP 信息。数据库
本文地址:http://www.javashuo.com/article/p-wvrczbuh-r.htmlsegmentfault
DNS 这样一个在网络互联中算是一个比较简单的协议,实现我如此简单的需求,竟然没有哪一个参考资料可以覆盖我须要的知识点……服务器
我本身也进行了抓包,抓包的时候,建议不直接向权威的 DNS server 发送请求,而是向网关、路由器等提供 DNS 中继的服务器发,这样能够得到比下面最后一个参考资料更多的信息。网络
《用 TCP / IP 进行网际互联(第五版)——原理、协议与结构(第五版)》,Douglas E. Comer
《计算机网络(第5版)》,Andrew S. Tannenbaum, David J. Wetherall:男神塔能鲍姆教授!
DNS Protocol
DNS Reference Information:有各类 type 的说明
Domain Name System (DNS) Parameters:有各类参数的总集合
DNS Name Notation and Message Compression Technique
RFC-1035
对 DNS 报文的理解
DNS message解析:这篇文章也挺仔细地说明了 DNS 报文结构,图形控能够看
利用 WireShark 进行 DNS 协议分析异步
简要整理一些和本文相关的点:socket
DNS 的本质是发明了一种层次的、基于域的命名方案,而且用一个分布式数据库系统加以实现。DNS 的主要做用是将主机名映射成 IP 地址。tcp
DNS 解析的发起端通常是互联网 Server / Client 模型中的 client 端(如下称 client 端,指的就是发起 DNS 解析的一端),如今大部分的 C 语言 client 端都使用 getaddrinfo()
实现。之前通常用 gethostbyname()
由于一些缘由再也不推荐使用了,而且也只支持 IPv4。
DNS 解析中,DNS server 开放的端口应当是 53 端口。当 client 端做出请求时,server 返回的不只仅是 IP 信息,还包含于该域名相关联的资源记录。
仅仅从一个域名 URL 中,咱们不能区分这是一个域名仍是某个对象(主机)名。域名的总长度应小于等于 255 个字节
,域名的每一段则必须小于等于 63 字节
。
DNS 请求的格式和响应格式差很少,就不单独讲了。从 UDP 数据包的正文部分算起,DNS 报文的结构按顺序以下:
数据类型 | Ethereal 里的名字 | 说明 |
---|---|---|
uint16_t |
Transaction ID | 标识符。下文说明 |
uint16_t |
Flags | 参数。下文说明 |
uint16_t |
Questions | 询问列表的数目 |
uint16_t |
Answer RRs | (直接) 的回答数 |
uint16_t |
Authority RRs | 认证机构数目(仅响应包里有) |
uint16_t |
Additional RRs | 附加信息数目(仅响应包里有) |
variable | Queries | 请求数据的正文。请求包中只有这个。响应包也会附上本来的请求数据 |
variable | Answers | 响应数据的正文 |
variable | Authortative name servers | 域名管理机构数据 |
variable | Additional records | 附加信息数据 |
16 bits 的值,各部分按顺序以下(按顺序:位号、Ethereal 名称、说明):
Bit 14~11, Opcode:查询类型——请求和响应包都适用:
0
:普通查询(最经常使用的)1
:反向查询2
:服务器状态请求3
:通知4
:更新(貌似是用在 DDNS 的?)Bit 3~0, Reply code:响应状态码,以下(参见 Micrisoft 资料 的 “DNS update message flags field” 小节):
0
:OK1
:查询格式错误2
:服务器内部错误3
:名字不存在4
:这个错误码不支持5
:请求被拒绝6
:name 在不该当出现时出现(什么鬼)7
:RR 设置不存在8
:RR 设置应当存在可是却不存在(什么鬼)9
:服务器不具有改管理区的权限10
:name 不在管理区中每一条 RR 的格式以下:
数据类型 | Ethereal 里的名字 | 说明 |
---|---|---|
variable | Name | 资源的域名——其实前文已经出现了 |
uint16_t |
Type | 类型。下文说明 |
uint16_t |
Class | 大多数是 0x0001,表明 IN |
uint32_t |
Time to Live | TTL 秒数 |
uint16_t |
Data length | 当前 RR 剩余部分的长度 |
variable | RR 主数据 |
若是是请求数据的话,那么 TTL、Data Length 和 RR 主数据都不须要
Type
的大部分值在 RFC-1035 中定义,此外的一些在其余文档定义(好比 IPv6)。我会用到的有:
1
:“A
”,表示 IPv4 地址2
:“NS
”,域名服务器的名字28
:“AAAA
”,表示 IPv6 地址5
:“CNAME
”,规范名,常常会有一个 CNAME 跟着一票 A 和 AAAA这一部分直接参考的是 RFC-1035 的 “4.1.4. Message Compression”小节。
RR 中的 Name 字段,有三种表示方法(不是官方分类,而是本人本身分的):
好比表示 “www.google.com” 这样一个完整的域名,须要如下16个字节:
B0 | B1 | B2 | B3 | B4 | B5 | B6 | B7 | B8 | B9 | B10 | B11 | B12 | B13 | B14 | B15 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
\3 |
w |
w |
w |
\6 |
g |
o |
o |
g |
l |
e |
\3 |
c |
o |
m |
\0 |
注意这里并非把谷歌的 URL 使用简单的 char *
字符串复制上去,而是将每一段都分割开来。本例子中将域名分红了三段,分别是 www
, google
, com
。每一段开头都会有一个字节,表示后面跟着的那段域名的字节长度。最后当读到 \0
的时候,表示再也不有数据了(这里和 char *
的 \0
含义有一点不一样,虽然形式上是同样的)
前文咱们提到,域名的每一段,最长不能超过 63 个字节,所以在表示域名段长度的这个字节的最高两位
(0xC0
),必然是 0。这就引伸出了这里的第二种用法。
这种表示法中,至关于一个指针,指代 DNS 报文中的某一个域名段。在解析一段 RR 数据段时,须要判断域长度嘛,判断的逻辑是:
去掉最高两位
后剩下的6位,以及接下来的 8 位总共 14 位长的数据,指向 DNS 数据报文中的某一段域名(不必定是完整域名,参见第三种),能够算是指针吧。好比 0xC150,表示从 DNS 正文(UDP payload)的 offset = 0x0150
处所表示的域名。0x0150 是将 0xC150 最高两位清零获得的数字。
这就是上面两种的混合表示。好比说,咱们假设前文表示 www.google.com
的完整域名的数据段处于 DNS 报文偏移 0x20 处,那么有如下几种可能的用法:
0xC020
:天然就表示 www.google.com
了0xC024
:从完整域名的第二段开始,指代 google.com
0x016DC024
:其中 0x6d
就是字符 m
,于是 0x016D
单独指代字符串 m
;而第二段 0xC024
则指代 google.com
,所以整段表示 m.google.com
除了 Ethereal 以外,推荐的分析工具备:
代码实如今我用来研究 epoll()
的分支中,GitHub 工程在此,许可证为 LGPL。
实现逻辑上其实仍是挺简单的,照着上面提到的原理实现就行了。大部分的代码和本文无关,只须要看里面的 AMCDns.c / h 文件便可。
个人这些代码能够彻底代替阻塞的 getaddrinfo()
函数,甚至也能够集成到异步 I/O 库中。使用流程以下:
socket()
建立一个 UDP 套接字并 bind()
AMCDns_GetDefaultServer()
获取系统默认配置的 DNS 服务器AMCDns_SendRequest()
请求指定域名的 IP 信息AMCDns_RecvAndResolve()
获取摘要的或完整的响应。AMCDns_FreeResult()
清除 DNS 响应数据以免内存泄露close()
掉 socket