【转】DNS 报文结构和个人 DNS 解析代码实现——解决 getaddrinfo() 阻塞问题

https://segmentfault.com/a/1190000009369381

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

本文地址: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 协议分析

[图片上传失败...(image-8073ec-1641892719015)]

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 字节

[图片上传失败...(image-593ae5-1641892719015)]

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

[图片上传失败...(image-ebe265-1641892719014)]

域名压缩显示

这一部分直接参考的是 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

[图片上传失败...(image-7833d7-1641892719014)]

分析工具

除了 Ethereal 之外,推荐的分析工具有:

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

[图片上传失败...(image-3ab20b-1641892719014)]

代码实现

代码实现在我用来研究 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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容