DNS 报文结构和我的 DNS 解析代码实现——解决 getaddrinfo() 阻塞问题

实际应用中发现一个问题,在某些国家/ 地区的某些 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

Reference

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 协议分析异步

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 报文格式

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 附加信息数据
  • Transaction ID:这是由 client 端指定的标识数据,DNS server 会将这个字段原样返回,client 端能够用来区分不一样的 DNS 请求
  • RRResource Record 的缩写

Flags

16 bits 的值,各部分按顺序以下(按顺序:位号、Ethereal 名称、说明):

  • Bit 15,Response:0 表示查询,1 表示响应(query / response)
  • Bit 14~11, Opcode:查询类型——请求和响应包都适用:

    • 0:普通查询(最经常使用的)
    • 1:反向查询
    • 2:服务器状态请求
    • 3:通知
    • 4:更新(貌似是用在 DDNS 的?)
  • Bit 10, Authoritative:用于响应包,判断服务器是否一个认证的域服务器
  • Bit 9, Truncated:报文是否被截断了。收发包都用
  • Bit 8, Recursion desired:收发包都用,表示是否须要用递归。做为 client 端,最好置 1,要否则 DNS 不执行递归查询,将有不少数据没能查到
  • Bit 7, Recursion available:响应包用,表示服务器是否有能力使用递归查询
  • Bit 6:这个数据段,Ethereal 说是保留位,而书中表示数据是不是鉴别的——求确认
  • Bit 5, Answer authenticated:数据是否被服务器鉴定过(貌似抓到的包里都是 0)
  • Bit 4, Reserved
  • Bit 3~0, Reply code:响应状态码,以下(参见 Micrisoft 资料 的 “DNS update message flags field” 小节):

    • 0:OK
    • 1:查询格式错误
    • 2:服务器内部错误
    • 3:名字不存在
    • 4:这个错误码不支持
    • 5:请求被拒绝
    • 6:name 在不该当出现时出现(什么鬼)
    • 7:RR 设置不存在
    • 8:RR 设置应当存在可是却不存在(什么鬼)
    • 9:服务器不具有改管理区的权限
    • 10:name 不在管理区中

资源记录(RR)的格式

每一条 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 数据段时,须要判断域长度嘛,判断的逻辑是:

  • 若是最高两位是 00,则表示上面第一种
  • 若是最高两位是 11,则表示这是一个压缩表示法。这一个字节去掉最高两位后剩下的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 库中。使用流程以下:

  1. 调用 socket() 建立一个 UDP 套接字并 bind()
  2. 调用 AMCDns_GetDefaultServer() 获取系统默认配置的 DNS 服务器
  3. 若是不使用系统默认的 DNS 服务器,则须要使用 struct addrinfo 类型来指定。
  4. 调用 AMCDns_SendRequest() 请求指定域名的 IP 信息
  5. 调用 AMCDns_RecvAndResolve() 获取摘要的或完整的响应。
  6. 调用 AMCDns_FreeResult() 清除 DNS 响应数据以免内存泄露
  7. close() 掉 socket
相关文章
相关标签/搜索