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

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 地址、简单教程

代码实现

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

DNS 报文结构和个人 DNS 解析代码实现——解决 getaddrinfo() 阻塞问题相关推荐

  1. DNS报文格式及抓包解析

    报文结构 DNS的报文结构如下,其中黄色为基础部分,绿色为问题部分,蓝色为资源记录部分.资源记录部分只在响应包中出现. 基础部分 1.事务ID 16bit,DNS报文的标识,请求报文和对应的响应报文的 ...

  2. 如何突破DNS报文的512字节限制

    "DNS协议大家都应该很熟悉,最近有同学问到如何获得UDP承载的超过512字节的DNS报文,借此机会,我们一起了解下DNS协议与报文长度有关的一些细节." 本文将讨论的是DNS协议 ...

  3. 移动和云环境下的报文传输流程--理解DNS解析、CDN资源下发、公网传输流程、数据中心网络

    DNS解析 手机打开app时,首先需要解析网站的域名. 在手机运营商所在的互联网区域中,有一个本地DNS,手机向本地DNS请求解析DNS.如果本地DNS有缓存,则直接返回:如果本地DNS没有缓存,则递 ...

  4. 【RHCE】DNS域名解析服务器及正反向解析与主从同步简单配置

    目录 域名解析服务器的介绍 因特网的域名结构 域名服务器的类型划分 DNS域名解析的过程 搭建DNS服务器 实验 实验一:正向解析 [vim  /named/named.localhost] [roo ...

  5. 阿里DNS:用LibFuzzer照亮DNS代码的死角

    1 引言 2018年11月初,国内某云解析服务提供商出现大规模服务不可用故障,在业界引起了不小的震动,以下是官方的故障复盘公告: 技术复盘中很明确地说明了此次故障的起因:大量恶意请求报文攻击触发了软件 ...

  6. DNS服务详解及正向解析与反向解析

    现在想想,我们平时在浏览器上浏览网页时,一般是不是使用的域名去访问的呢,为什么要使用域名呢,使用域名的好处是什么呢,带着这些问题,深入思考才能真正的学好DNS. 要想了解DNS,就必须先知道域名这个概 ...

  7. https网络编程——HTTP协议的简介、HTTP报文结构和代理、HTTP加速解析方法(哈希加速、协议状态机)

    参考:HTTP协议的简介及其工作原理等 地址:https://qingmu.blog.csdn.net/article/details/108046553?spm=1001.2014.3001.550 ...

  8. DNS概述和DNS服务器部署(详细正向解析)

    文章目录 DNS概述和DNS服务器部署(正向解析) 一.简介 二.域名 2.1 域名层次结构 2.2 查询方式 2.3 DNS域名解析过程 三 .DNS服务器类型 四.DNS服务器部署 4.1.BIN ...

  9. DNS云学堂 | 快速定位DNS解析异常问题,牢记这四种DNS状态码

    DNS的状态码在进行故障排查的时候起着至关重要的作用.在DNS的维护中会经常遇到DNS解析异常问题,通过DNS的状态码可以初步判断DNS解析的异常问题.本期云学堂通过详解DNS状态码的定义,给出常见状 ...

  10. JAVA实现报文解析:协议的数据类型和完整的报文结构解析

    **JAVA实现GBT32960报文解析系列文章链接:** JAVA实现GBT32960报文解析(一):协议的数据类型和完整的报文结构解析 JAVA实现GBT32960报文解析(二):数据包结构解析源 ...

最新文章

  1. Markdown通用的常用语法说明
  2. 【转】python2与python3的主要区别
  3. ubuntu 安装pycharm
  4. python常用操作符_Python--3常用操作符
  5. QDoc C ++特定的配置变量
  6. 转载:Java编程风格与命名规范整理
  7. java 中方法里的参数_【Java基础】12、java中方法的参数传递机制
  8. 关于计算机工作的诗歌,提高计算机工作及上网效率的方法
  9. 如何在本机安装mysql_机器人之如何在本机安装MySQL,并配置电脑为数据库服务器...
  10. 图解cgroup架构中cgroup与css之间的多对多的关系
  11. 感受MapXTreme2004
  12. HBase学习笔记:HBase数据库介绍
  13. 我要偷偷的学Python,然后惊呆所有人(第九天)
  14. 字母异位词(anagram)的不同复杂度实现
  15. sun.net.ftp.FtpClient介绍
  16. 代码设置margintop_如何通过代码设置TextView的Margin参数?
  17. java1.8离线安装包_离线安装JDK1.8
  18. 堡垒机和防火墙有什么区别?
  19. jenkins邮件使用自定义变量
  20. 攻防世界-PWN-house of grey-Linux系统文件泄漏

热门文章

  1. 企业域名和企业邮箱究竟有什么关系?
  2. JavaWeb如何判断账户密码
  3. 杭州地铁四期尘埃落定和我预测差异,看地铁空白地,遥想地铁五期规划
  4. 我所理解的协方差矩阵
  5. 四川绵阳动感地带学生套餐资费备忘
  6. C语言:数组排序(选择法排序)
  7. 「ggplot2练习」画基因结构图
  8. 跨国面板数据(1960-2020)十三:GDP、人均GDP、国民收入、储蓄(excel、stata版)
  9. 蔡勒(Zeller)公式及其推导:快速将任意日期转换为星期数
  10. python文件同时读写_python可以同时对文件进行读写操作吗