DNS 报文结构和个人 DNS 解析代码实现——解决 getaddrinfo() 阻塞问题
实际应用中发现一个问题,在某些国家/ 地区的某些 ISP 提供的网络中,程序在请求 DNS 以连接一些服务器的时候,有时候会因为 ISP 的 DNS 递归查询太慢,导致设备端认为 DNS 超时了,无法获取服务器 IP。
给用户的解决方案是:请不要用 ISP 自动分配的 DNS server,改用 8.8.8.8 就解决了。
但是让用户这么配置太麻烦、也太不友好了。于是我就思考:能不能自己实现 DNS 服务,当 ISP 的 DNS 请求超时或者失败的时候,就从内部直接向 8.8.8.8 请求 DNS 信息,可以不?
如果要使用 gethostbyname()
和 getaddrinfo()
来解决这个问题的话,方案是修改 /etc/resolve.conf
里的内容。但这并不是正确的办法,因为这种改法一来不准确,二来会影响系统其他 DNS 请求。可行的方案是:自己构建 DNS 请求,并且自己解析获得我们需要的 IP 信息。
本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
原文发布于:https://segmentfault.com/a/1190000009369381,也是作者本人的专栏。
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 基本概念
简要整理一些和本文相关的点:
DNS 的本质是发明了一种层次的、基于域的命名方案,并且用一个分布式数据库系统加以实现。DNS 的主要作用是将主机名映射成 IP 地址。
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 里的名字 |
说明 |
---|---|---|
|
Transaction ID |
标识符。下文说明 |
|
Flags |
参数。下文说明 |
|
Questions |
询问列表的数目 |
|
Answer RRs |
(直接) 的回答数 |
|
Authority RRs |
认证机构数目(仅响应包里有) |
|
Additional RRs |
附加信息数目(仅响应包里有) |
variable |
Queries |
请求数据的正文。请求包中只有这个。响应包也会附上原本的请求数据 |
variable |
Answers |
响应数据的正文 |
variable |
Authortative name servers |
域名管理机构数据 |
variable |
Additional records |
附加信息数据 |
- Transaction ID:这是由 client 端指定的标识数据,DNS server 会将这个字段原样返回,client 端可以用来区分不同的 DNS 请求
- RR:Resource 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 |
资源的域名——其实前文已经出现了 |
|
Type |
类型。下文说明 |
|
Class |
大多数是 0x0001,代表 |
|
Time to Live |
TTL 秒数 |
|
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 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|