域名系统(DNS)
域名系统(DNS)
引言
什么是域名?域名系统又是什么?
让我们先来看看百度百科对域名和域名系统给出的解释的一部分内容:
网域名称(英语:Domain Name,简称:Domain),简称域名、网域,是由一串用点分隔的字符组成的互联网上某一台计算机或计算机组的名称,用于在数据传输时标识计算机的电子方位。
由于IP地址具有不方便记忆并且不能显示地址组织的名称和性质等缺点,人们设计出了域名,并通过网域名称系统(DNS,Domain Name System)来将域名和IP地址相互映射,使人更方便地访问互联网,而不用去记住能够被机器直接读取的IP地址数串。域名系统(英文:Domain Name System,缩写:DNS)是互联网的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。DNS使用UDP端口53。当前,对于每一级域名长度的限制是63个字符,域名总长度则不能超过253个字符。
可以这么理解:域名可以方便大家记忆,DNS 目的是为了实现域名和主机地址之间的转换而存在的系统。
在部分情况下,域名不仅仅起到便于记忆的作用,服务端还会对其进行区分标记,如 a.example.com 和 b.example.com 可能是同一个 IP 的不同页面,由 Web 服务器进行展示,这时候直接访问 IP 地址就可能出现与期望不符的现象,所以我认为不能简单地说域名是代替 IP 地址进行访问。
DNS 解析的过程
在考虑如何完成这部分内容之前我参考了腾讯云社区中其他一些文章,也发现了一些问题(也可能是我的理解有问题),其中包括了腾讯认证的 IMWeb 前端团队这方面的文章,该团队的文章《DNS 解析》对 DNS 解析的整个过程进行了阐述,在这里我就简单通过对这篇文章的分析来讲述解析这一过程吧。
简单概括一下改文章中域名解析过程:
- 查找浏览器缓存
- 查找系统缓存
- 查找路由器缓存
- 查找ISP DNS 缓存
- 递归搜索
首先浏览器缓存是没有太大问题的,各家浏览器都有自己的缓存机制。
第二是系统缓存,确实系统会根据 TTL 缓存域名解析的结果,但文中所表述的 hosts 文件作为缓存是不正确的,在 Windows 操作系统下是由 DnsClient 服务负责实现的,在 Linux 系统下由 NSCD 类服务实现的。
第三路由器缓存,我本人对此表示疑惑(对不起,也许是我的路由器太弱了),虽然从道理上来说路由器确实可以劫持 DNS 请求进行解析缓存和返回,但是否真如此还得进一步讨论。
最后的查找 ISP DNS 缓存和递归搜索那就更值得怀疑了,首先我设置的 DNS 解析地址不一定是 ISP 所提供的地址,我大可选择一些公共 DNS ,谷歌、微软、腾讯等公司都对外提供公共 DNS 解析,并且并不是所有的 DNS 服务器都采用递归方式进行请求,迭代请求方式也是常见的。
这篇文章暂且分析到这里,在此对该团队该文章的严谨性提出质疑。
这里不得不再提的是 DNS 的迭代查询方式和递归查询方式,实际情景可能会较为复杂,甚至出现不同方式混合的情况,这里简单的进行如下不严谨的表述:
迭代解析方式中客户端依次访问不同级别的域名解析服务器进行查询
递归请求中由服务器完成解析直接返回给客户端(客户端仅发送一起解析请求即可完成解析)
主机记录和 TTL
在腾讯云 DNS 解析的常见问题文档中有了详细的陈述,这里直接引用:
要指向主机服务商提供的 IP 地址,选择类型 A;要指向一个域名,选择类型 CNAME。
A 记录:地址记录,用来指定域名的 IPv4 地址(例如 8.8.8.8),如果需要将域名指向一个 IP 地址(外网地址),就需要添加 A 记录。 CNAME 记录: 如果需要将域名指向另一个域名,再由另一个域名提供 IP 地址,就需要添加 CNAME 记录。
NS 记录:域名服务器记录,如果需要把子域名交给其他 DNS 服务商解析,就需要添加 NS 记录。
AAAA 记录:用来指定主机名(或域名)对应的 IPv6 地址(例如 ff06:0:0:0:0:0:0:c3)记录。
MX 记录:如果需要设置邮箱,让邮箱能收到邮件,就需要添加 MX 记录。
TXT 记录:如果希望对域名进行标识和说明,可以使用 TXT 记录,绝大多数的 TXT 记录是用来做 SPF 记录(反垃圾邮件)。
SRV 记录:SRV 记录用来标识某台服务器使用了某个服务,常见于微软系统的目录管理。主机记录处格式为:服务的名字.协议的类型。例如 _sip._tcp 。
隐、显性 URL 记录:将一个域名指向另外一个已经存在的站点,就需要添加 URL 记录。
其他和具体的解释参见下一部分内容
关于 TTL 的部分:
TTL 即 Time To Live,缓存的生存时间。指地方 DNS 缓存您域名记录信息的时间,缓存失效后会再次到 DNSPod 获取记录值。我们默认的600秒是最常用的。
DNS 请求
那么 DNS 请求是怎样的?
主要参考是 RFC 1034 Domain names - concepts and facilities 和 RFC 1035 Domain names - implementation and specification, RFC 1035 有更多的细节,1034 更为简洁。
该部分仅针对上述两个规范进行讨论,其他的 DNS 拓展(如 RFC 6891、1183、1706、2536、2539 等)规范不在该部分的分析范围内,后续可以进一步了解,对于在实例中出现的内容简单带过不作深入探讨。
约定
- 数据传输以 8 位构成的字节进行分割,每个单元内左侧为高位,如 1 0 1 0 1 0 1 0 表示十进制数 170
- 不区分大小写,但奇偶校验必须完全匹配
TPYE(类型)
类型 | 值 | 含义 |
---|---|---|
A | 1 | 主机地址 |
NS | 2 | NS 服务器 |
MD | 3 | 邮件目的地(已废弃 - 使用 MX) |
MF | 4 | 邮件转发器(已废弃 - 使用 MX) |
CNAME | 5 | 别名 |
SOA | 6 | 鉴权区域开始标志 |
MB | 7 | 邮箱域名(实验) |
MG | 8 | 邮件组成员(实验) |
MR | 9 | 邮件重命名域名(实验) |
NULL | 10 | 空 RR(实验) |
WKS | 11 | 服务描述 |
PTR | 12 | 域名指针 |
HINFO | 13 | 主机信息 |
MINFO | 14 | 邮箱或邮件列表信息 |
MX | 15 | 邮件交换 |
基本格式
包含:请求头(Header),请求(Question),回复(Answer),认证(Authority),附加(Additional)几个部分,在传输过程中,请求头必须存在,请求头内部表明了是否包括其他几个部分内容。
请求头格式
- ID 标识
- QR 0:查询 1:响应
- Opcode 0:标准查询 1:反向查询 2:状态请求 3-15:保留
- AA: 授权回答
- TC 报文截断
- RD 是否进行递归请求
- RA 是否支持递归
- Z 保留
- RCODE 应答码
- QDCOUNT 请求部分中的条目数。
- ANCOUNT 响应部分中的资源记录数。
- NSCOUNT 认证部分中名称服务器资源记录的数量。
- ARCOUNT 附加记录部分中的资源记录数量。
计数均为16位无符号整数
应答码:
值 | 应答 |
---|---|
0 | 无错误 |
1 | 格式错误,服务器无法解释查询 |
2 | 服务器故障 |
3 | 名称错误,针对权威 NS 查询中引用的域名不存在 |
4 | 未实现,不支持的查询类型 |
5 | 拒绝查询 |
6-15 | 保留 |
请求查询数据格式
- QNAME 请求名
- QTYPE 查询类型
- QCKASS 查询类,如IN
请求名:按 .
将域名进行分隔,将每一片的长度作为分隔符记录在该片之前,以 0 结尾,无需填充
如: cloud.tencent.com 分为 cloud tencent com 三部分,长度分别为 5,7,3,最终请求名为: \x05cloud\x07tencent\x03com
类:
类型 | 值 | 说明 |
---|---|---|
IN | 1 | the Internet |
CS | 2 | the CSNET class (已过时) |
CH | 3 | the CHAOS class |
HS | 4 | Hesiod |
响应、认证、附加数据格式
- NAME 名称
- TYPE 类型
- CLASS 类
- TTL 生存时间
- RDLENGTH 附加数据长度(字节)
- RDDATA 附加数据
数据压缩
附加字段(RDDATA)中对于重复的域名可以进行压缩,其格式为:
由于约定但记录不超过 63 字符,所以其可作为指针,纸箱请求开始后的第 x 个字节开始以 0 结尾的内容。
DNS 请求实例
脱离了实例只看结构很难了解,想要了解 DNS 请求的内容和过程还是需要一步一步进行尝试。
此处以 腾讯公共 DNS 119.29.29.29
作为 DNS 服务器,请求解析 im.qq.com
、web.tdh6.top
两个域名为例,使用 Python 3.10
编程支持,利用 socket 库进行请求。
构造请求
首先明确所需要进行请求的基本格式,Header 和 Question 是必须的,Answer 无法预知也无需在查询请求中包含,Authority 和 Additional 也无需指定内容。
查询请求头
根据请求头包含的内容进行构建:
1 | request_data = b"" # 初始化 |
除请求头外,仅包括 1 条 查询请求。
查询请求
查询请求包括三个部分:查询名称,查询类型,查询类,在此实例中查询类型为 1 A 记录
,查询类为1 the Internet 。
对需要查询的域名需要提前处理:
1 | fdm = "im.qq.com".split(".") # 以 . 分隔域名 |
处理后的域名为:
1 | b'\x02im\x02qq\x03com\x00' |
构造查询请求:
1 | request_data += dm # 域名 |
至此,请求构造完毕,完整请求内容:
1 | b'\x00\x01\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x02im\x02qq\x03com\x00\x00\x01\x00\x01' |
对请求进行 Base 64 URL 编码结果为
1 | b'AAEBAAABAAAAAAAAAmltAnFxA2NvbQAAAQAB' |
注:虽然此处没有 Base 64 编码后的请求之后会使用到
发送请求
通过 Socket 库建立 UDP 连接,发送请求内容并获取返回
1 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # udp |
接收到的请求为:
1 | b'\x00\x01\x81\x80\x00\x01\x00\x01\x00\x00\x00\x00\x02im\x02qq\x03com\x00\x00\x01\x00\x01\xc0\x0c\x00\x01\x00\x01\x00\x00\x00<\x00\x04\xa3\xb1\\\x12' |
解析请求
对获取到的数据进行解析处理,即可获得所需内容,虽可直接使用 DnsPython 库,但此处以简单的方式进行分析。
响应请求头
响应头的长度是固定(12 字节)的,对响应头进行提取并读取内容
1 | head = raw_res[:12] |
Rcode 为 0,表明请求成功,无错误产生,相应内容包括查询部分和响应(Answer)部分。
查询(Question 部分)
去除响应头后剩余的请求内容起始为 Question 查询内容,该结构如前所述,NAME 是边长的内容,以 0
结尾,之后的类型和类是固定长度,所以以 0 位分隔可提取出 RNAME,进而提取完成的 Question 部分。
查询部分和发送的内容完全一致,不再进行展示,提取后去除查询部分内容
1 | res = res[res.index(b'\00') + 4:] |
响应(Answer 部分)
根据响应结构对内容进行提取
1 | res_name = res[:res.index(b'\00')] # 提取RNAME |
对于 A 记录,其附加字段为解析的 IP,按位转为整型并使用 . 连接即可获得解析后的结果
1 | for j in res_rddata: |
1 | 163.177.92.18 (A) |
处理完成后剩余请求长度为 0。
改变请求解析 web.tdh6.top
这一没有 A 记录但有 CNAME 记录的域名,进行对比:
1 | b'\x00\x01\x81\x80\x00\x01\x00\x04\x00\x00\x00\x00\x03web\x04tdh6\x03top\x00\x00\x01\x00\x01\xc0\x0c\x00\x05\x00\x01\x00\x00\x02X\x00\x1c\x03web\x04tdh6\x03top\x03cdn\x05dnsv1\x03com\x00\xc0*\x00\x05\x00\x01\x00\x00\x02X\x00\x1f\x084kzi99cn\x06slt-dk\x05sched\x06tdnsv8\xc0A\xc0R\x00\x01\x00\x01\x00\x00\x00<\x00\x04v\xb4<\x14\xc0R\x00\x01\x00\x01\x00\x00\x00<\x00\x04\xb7Bg\x92' |
其 RCOUNT
值为 4
,表明有 4 条相应荷载,根据前述内容去除响应头和 Question 部分后进行分析:
由于附加字段中出现了 0xc0 开头的内容,表明使用了压缩方法,进行压缩,需要找到指针指向的内容,同时考虑到极端情况可能出现多层指针,故采用嵌套函数进行分析:
1 | def decom(datas, raw_res) -> bytes: |
响应为:
1 | web.tdh6.top.cdn.dnsv1.com (CNAME) |
有此处两个 IP 对应了同一个域名,分析响应的 RNAME 部分不难发现
DNS Queries over HTTPS (DoH)
安全 DNS
通过上述内容不难发现,传统的 DNS 查询和响应通过 UDP 或 TCP 发送,没有加密,因此存在被监视、篡改的风险,HTTPS 作为利用 TLS 的常用传输协议,利用其传输 DNS 请求是 DNS Queries over HTTPS,目前火绒、谷歌等浏览器已经支持该项功能,先对其进行简单的分析和实例展示。
进行 DoH 请求
在前述内容中对请求进行了 Base 64 编码,通过该编码可进行 DOH 请求。
腾讯云公共解析已经停止了对 DoH 的支持,但可以利用公共解析功能找到 DoH 地址,或采用公共 DNS
以 IBM 公共 DNS 9.9.9.9 为例,通过请求 https://9.9.9.9/dns-query?dns={Base 64 编码后的 DNS 请求}
即可进行 DOH 请求,例如上述例子的解析为:
https://9.9.9.9/dns-query?dns=AAEBAAABAAAAAAAAAmltAnFxA2NvbQAAAQAB
返回的内容以二进制的形式返回,内容为 DNS 解析的结果,通过相同的方式可对其进行分析,快速实现:
1 | import requests |
执行结果:
1 | im.qq.com. 600 IN A 203.205.254.62 |
补充的一点点 DoH 分析
DNS Queries over HTTPS,通过 HTTPS 来进行 DNS 传输,在编写此文时,推荐的规范为 RFC8484 DNS Queries over HTTPS (DoH),其前生目前共计 14 个版本,当前版本与 2018 年 10 月被提出,2019 年 1 月最后更改。
DNS Over HTTPS 是利用有 TLS 保护的 HTTP 传输进行标准 DNS 解析的过程,规范中指出,传输需使用 HTTP/2方式进行传输,其传输可利用 HTTP 提供的压缩、认证、缓存、重定向、代理等特性,每一次 DNS 请求对应一次独立的 HTTP 请求。在解析过程中,标准 DNS 请求进行 Base 64 url 编码(RFC4648)通过 GET 或 POST 方式向 DoH 服务器进行请求(DoH 服务器必须同时支持 GET 和 POST 方法),使用 POST 请求时,还需要在请求头中明确 Content-Type,DoH 请求的 Mime 为 application/dns-message
。
DNS over TLS
DNS over TLS 要早于 DoH 的使用,目前推荐的规范为 RFC 7858 和 RFC 8310,DoT 的提出面向的是 DNS 请求的传输层安全性,规范中规定了默认情况下 DoT 使用端口为 853 的TCP 连接进行数据传输,其不允许数据明文传输,需使用 TLS 加密通信。服务器和客户端之间不应每次请求后立即断开连接,应将其保留一段时间以供下一次查询使用,只要资源足够,应保持 TCP 长时间连接,并要求遵循 RFC7766 DNS Transport over TCP - Implementation Requirements 中所提出的最佳实践以避免资源枯竭和拒绝服务攻击。
在通过 TCP 方式进行 DNS 请求过程中,需要将请求体长度放置于数据的开始,Dns over TLS 也有同样的要求。
DoT 实例
1 | import ssl |
更多
Windows 的 DoH
微软已经推出了 DNS 加密功能,使用 Win 11 配置过网络的一定不会陌生,如下图所示就是在 Windows 11 操作系统配置 DNS 的界面,可以看到已经有了 “DNS 加密” 的选项,但是其默认支持的 DNS 非常少,后面将会进行阐述。
在 Windows 技术社区中由文章详细说明了这部分的配置方法,Windows Insiders gain new DNS over HTTPS controls,其有这样的表述:
To start with, we want to note that the registry key controls documented in our original DoH testing blog post are no longer applicable. As stated there, those instructions were time limited to the initial DoH test rollout. If you did ever set that key, please delete it then reboot your machine before proceeding with the rest of this blog post.
这里设计到目前通过百度以“Windows 如何配置 DoH”这样的关键词进行搜索时经常看到的一个方法:通过注册表进行配置,目前已经不再需要它,并且原文作者推荐将其删除。
自带支持的 DoH
如前所述,并不是所有的 DoH 都可以直接使用,当我们想使用自己的 DoH 的时候就不再是能简单的填入 IP,选择加密那么简单了。
通过在 PowerShell 或者 Windows 终端中执行 Get-DnsClientDohServerAddress
或 netsh dns show encryption
就可以查看目前已有的 DoH 配置。
Get-DnsClientDohServerAddress 输出:
1 | ServerAddress AllowFallbackToUdp AutoUpgrade DohTemplate |
由于众所周知的原因,系统自带的这些 DoH 在当前网络环境下并不是很方便使用,我们需要需要为其增加我们自定义的节点。
增加 DoH 配置
通过
1 | netsh dns add encryption server=<resolver-IP-address> dohtemplate=<resolver-DoH-template> autoupgrade=yes udpfallback=no |
或
1 | Add-DnsClientDohServerAddress -ServerAddress '<resolver-IP-address>' -DohTemplate '<resolver-DoH-template>' -AllowFallbackToUdp $False -AutoUpgrade $True |
可以添加 DoH 配置,以通过 NETSH 为例,相关说明如下图
进行配置:
1 | netsh dns add encryption server=1.12.34.56 dohtemplate="https://doh-{{授权ID}}-{{设备标志}}.doh.pub/dns-query" autoupgrade=no udpfallback=yes |
配置之后,在通过系统 GUI 进行设置,即可实现通过 DoH 进行 DNS 解析了。
在 Ubuntu 20.04 LTS 上搭建域名解析服务
Bind 9 域名解析
快速安装
1 | apt install bind9 -y |
新版安装
1 | add-apt-repository ppa:isc/bind-dev |
检查安装状态
1 | named -V |
- bind 9.17.10 版本后才提供 DoH 原生支持,更早版本将无法工作
- 若出现
systemd-resolved
服务占用了 53 端口则需要调整系统设置
配置文件/etc/systemd/resolved.conf
1 | [Resolve] |
重启 systemd-resolved
systemctl restart systemd-resolved
配置文件/etc/bind/named.conf.options
:
1 | tls dot-local-tls { |
- 证书路径
cert-file
私钥路径key-file
,注意权限 - 若未配置 allow-query 则会导致客户端无权限查询,表现在日志中对应查询提示
query failed (REFUSED)
- max-cache-size 20% 避免使用过多内存