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 中继的服务器发,这样可以获得比下面最后一个参考资料更多的信息。


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 里的名字

说明

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 之外,推荐的分析工具有:

  • Wireshark:抓包工具
  • BIND:DNS 服务器,可以安装在你的开发环境上,用来观察和生成 DNS 响应。FTP 地址:ftp.isc.org/isc/bind9/简单教程

代码实现

代码实现在我用来研究 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

本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。

原文发布于:https://segmentfault.com/a/1190000009369381,也是作者本人的专栏。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Sorrower的专栏

内核必须懂(一): 用系统调用打印Hello, world!

1325
来自专栏腾讯云安全的专栏

[经验分享]——XSS 入门介绍

1883
来自专栏三杯水

服务稳定性及应用防攻击方案

日志收集推荐使用Elastic Stack协议栈,可以满足收集海量日志需求,而且便于后续分析、报表、报警操作

1263
来自专栏琯琯博客

awesome-php-cn软件资源

PHP 资源列表,内容包括:库、框架、模板、安全、代码分析、日志、第三方库、配置工具、Web 工具、书籍、电子书、经典博文等。 依赖管理 依赖和包管理库 Com...

3715
来自专栏owent

libatbus的几个藏得很深的bug

在写这篇文章前,我突然想到以前流行了一段时间的服务器面试题:当一个BUG只有几百万分之一的概率会出现,怎么办?这个问题在这个BUG里只是毛毛雨而已,因为这次的B...

773
来自专栏FreeBuf

Katana Framework武士刀操作指南

Katana Framework是用Python语言写的渗透框架,但我更觉得它像是一个工具箱,作者是俄罗斯人,该工具集成以下功能:SQL注入、NMAP系统扫描探...

1042
来自专栏友弟技术工作室

iptables系列五

iptables系列之layer7 ? 一块网卡多个IP,这张网卡上连接一个交换机,交换机上连接了多个不同网段的主机,如果设置网关,以及转发功能。不同网段主机可...

2805
来自专栏Android群英传

从 Linux 进程调度到 Android 线程管理

2372
来自专栏进击的程序猿

raft 系列解读(3) 之 代码实现最小规则followercandidateleader规则RequestVote RPCAppendEntries RPC

首先,其实raft如果你不去看理论正确性的证明,光实现的话,只要按照raft里面给出的原则写代码就ok!如果代码写出来不正确,只能是你自己实现的问题。囧

772
来自专栏JavaEdge

压测软件Jmeter使用实例(WIN7环境)百科我们为什么使用JmeterJmeter安装配置Sampler监听器(Listener)点击启动按钮,开启测试Jmeter自定义变量Redis的压测

3085

扫码关注云+社区