Domain Name System
What is Dns
简言之就是将Domain和ip相对应,类似一个电话簿
如果看toy implement, 我这里用py写了一个很简单的。
How it works
首先明确一下,整体的逻辑可以抽象程一个CS模型,但是这其中涉及到多个服务器, 最后client看到的就是一个req->response
下述四个服务器其实就可以看作一个dns解析流程,具体步骤可以看Ref里cloudfare的文章,里面一共写了8个步骤
Dns解析器(dns resolver)
dns解析器,是一个服务器,一般是由ISP提供,也可以自己搭建,这个服务器会缓存一些dns解析结果,这样可以加快dns解析的速度,这个服务器会接受client的dns请求,然后去查询缓存,如果没有,就会去查询其他的dns服务器,最后将结果返回给client
root name server
dns解析器缓存未命中的时候,会去查询root name server,这个服务器是一个全球分布式的服务器,这个服务器会返回给dns解析器,下一步应该去查询哪个顶级域名服务器, CN国内解析其实都打到了这些的镜像
TLD(top-level-domain) name server
这个服务器会返回给dns解析器,下一步应该去查询哪个权威域名服务器
authoritative name server
这个服务器会返回给dns解析器,查询的结果
DNS记录类型
理论上讲dns记录可以看成一个map:
- key: 域名
- value: 域名关联值
除了ip地址之外,他其实还可以是别名或者文本
- A ,主机 IP 地址;
- AAAA ,主机 IPv6 地址;
- ALIAS ,自动解析的别名( alias );
- CNAME ,别名的权威名称( canonical name );
- MX ,邮件交换服务器( Mail eXchange );
- NS ,域名服务器( name server );
- TXT ,描述文本;
A类型
使用dig,如果没有指定类型,就是A类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
➜ ~ dig baidu.com
; <<>> DiG 9.16.44-Debian <<>> baidu.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 39207
;; flags: qr rd ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0
;; WARNING: recursion requested but not available
;; QUESTION SECTION:
;baidu.com. IN A
;; ANSWER SECTION:
baidu.com. 0 IN A 110.242.68.66
baidu.com. 0 IN A 39.156.66.10
;; Query time: 10 msec
;; SERVER: 192.168.208.1#53(192.168.208.1)
;; WHEN: Wed Oct 04 11:19:30 CST 2023
;; MSG SIZE rcvd: 68
AAAA记录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
➜ ~ dig baidu.com AAAA
; <<>> DiG 9.16.44-Debian <<>> baidu.com AAAA
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 49072
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;baidu.com. IN AAAA
;; Query time: 20 msec
;; SERVER: 192.168.208.1#53(192.168.208.1)
;; WHEN: Wed Oct 04 11:21:49 CST 2023
;; MSG SIZE rcvd: 38
DNS报文格式
DNS一般用UDP( TCP 亦可),端口号是 53
DNS报文格式分两中,请求和应答,两者结构大体相似:
- 头部( header ),描述报文类型,以及其下 4 个小节的情况;
- 问题节( question ),保存查询问题;
- 答案节( answer ),保存问题答案,也就是查询结果;
- 授权信息节( authority ),保存授权信息;
- 附加信息节( additional ),保存附加信息;
头部
头部是固定的,共 12 字节, 其他节不固定,记录数可多可少,数目保存在头部中
头部有6个字段,每个16位
- 标识符( identifier ),用于标识一个查询或应答过程,一个 16 位的 ID ,在应答中原样返回,以此匹配请求和应答;
- 标志( flags ),主要是一些标志位;
- 问题数( question count ),描述问题节的数目;
- 答案数( answer count ),描述答案节的数目;
- 授权信息数( authority count ),描述授权信息节的数目, 仅respone包;
- 附加信息数( additional count ),描述附加信息节的数目,仅response包;
标志中的16位具体划分:
- QR( query or response ),标识查询或应答报文,1bit
- 0表示查询报文,1表示应答报文;
- OPCODE( operation code ),标识报文类型,4bit
- 0表示标准查询;
- 1表示反向查询;
- 2表示服务器状态请求;
- 3-15保留;
- AA( authoritative answer ),标识授权应答,1bit
- 0表示非授权应答;
- 1表示授权应答,当前查询结果是由域名的权威服务器给出的;
- TC( truncation ),标识可截断的,使用 UDP 时,如果应答超过 512 字节,只返回前 512 个字节,1bit
- 0表示报文未截断;
- 1表示报文被截断;
- RD( recursion desired ),标识期望递归,如果服务器支持递归查询,就会在应答中设置该位,以告知客户端,1bit
- 0表示不期望,如果服务器没有授权回答,它就返回一个能够处理该查询的服务器列表给客户端,由客户端自己进行 迭代查询;
- 1表示期望,服务器必须处理这个请求:如果服务器没有授权回答,它必须替客户端请求其他 DNS 服务器,这也是所谓的 递归查询;
- RA( recursion available ),标识可用递归,1bit
- 0表示不可用;
- 1表示可用;
- 保留位( reserved ),保留字段;3bit
- 响应码( response code ),标识应答状态,4bit
- 0表示无差错;
- 1表示格式错误;
- 2表示服务器错误;
- 3表示名字错误;
- 4表示域名服务器不支持该查询;
- 5表示拒绝;
- 6-15保留;
问题节
问题节支持保存多条问题记录,记录条数则保存在 DNS 头部中的问题记录数字段。这意味着,DNS 协议单个请求能够同时查询多个域名,虽然通常只查询一个。
每个问题记录包含三个字段:
- 域名( domain name ),用于保存查询的域名,长度不固定,由具体域名决定,可以看成一个len(name) + name的结构,其中len是一个char大小,所以这个最大能做的数据也就255字节
查询类型( query type ),用于描述查询类型;
查询类型 名称代码 描述 1 A IPv4地址 2 NS 名称服务器 5 CNAME 规范名称 15 MX 电子邮件交互 16 TXT 文本信息 28 AAAA IPv6地址 - class 类( query class ),用于描述查询类别,通常为1,表示 TCP/IP 互联网地址;
domain name长度不固定,结构是一个len + data的结构,其中len是一个char大小,按照.划分再保存baidu.com就会分开保存,分成len(baidu) + baidu和len(com) + com + \0。这三个部分也是待查询部分
另外这个因为要考虑域名压缩,所以一般每个.分的部分的len的头两位其实是一个标志位,如果是11,后6 + 剩下的8bit就表示这个部分是一个指针,指向另一个部分。
应答节
查询的response,域名查询结果作为资源记录,保存在答案以及其后两节中
答案节、授权信息节和附加信息节均由一条或多条资源记录组成,这些数目保存在头部对应字段中
答案section的资源记录格式:
- 域名( domain name ),用于保存查询的域名,这个长度不固定,可以压缩;
- 类型( type ),用于描述资源记录类型;
- 类( class ),用于描述资源记录类别;
- 生存时间( time to live ),用于描述资源记录的生存时间(s);
- 数据长度( data length ),用于描述资源记录数据长度;
- 数据( data ),用于保存资源记录数据;
如果查询类型是 A 记录,那查询结果就是一个 IP 地址,保存于资源记录中的数据字段,而数据长度字段值为 4 ,因为 IP 地址的长度为 32 位,折合 4 字节。
递归dns解析器和迭代dns解析器
递归dns解析器和迭代dns解析器,是两种不同的dns解析器,递归dns解析器,会一直向下查询,直到查询到结果,然后返回给client,而迭代dns解析器,会一直向下查询,直到查询到权威域名服务器,然后返回给client,client再去查询权威域名服务器,直到查询到结果
在迭代DNS查询中,每个DNS查询都直接向客户端反馈另一个需要查询的DNS服务器地址,然后客户端继续查询DNS服务器,直到其中一个回复了给定域的正确IP地址为止。在递归DNS查询中,DNS服务器会向客户端提供所需的IP地址,而不是提供另一个DNS服务器的地址。递归查询的过程是:客户端向DNS服务器发送一个查询请求,DNS服务器要么返回所需的IP地址,要么返回一个错误,指出它无法解析该域名
换句话说,客户端在递归DNS查询中进行某种形式的委任。它告诉DNS解析器,“嘿,我需要这个域的IP地址,请查清它,在没有查到它不用回复我。”而在迭代查询中,客户端告诉DNS解析器:“嘿,我需要这个域的IP地址。请在查找过程中告诉我下一个DNS服务器的地址,以便我自己查找。”
通常递归的查找要快一点(高速缓存)。递归DNS服务器将对执行的每个查询的最终答案进行高速缓存,并将该最终答案保存一定的时间。如果另一个客户端发出相同的查询,递归DNS服务器将直接返回缓存的答案,而不是再次执行查询。这样,递归DNS服务器可以减少对其他DNS服务器的查询,从而减少网络流量并提高性能。
Linux上看DNS缓存服务器(resolver)
1
2
3
4
5
➜ ~ cat /etc/resolv.conf
# This file was automatically generated by WSL. To stop automatic generation of this file, add the following entry to /etc/wsl.conf:
# [network]
# generateResolvConf = false
nameserver 192.168.208.1
当然递归dns解析也是有缺点的,可以后续关注一下dns放大攻击和dns缓存中毒
Attention
遇到过比较尴尬的有几点
如果你用getaddrinfo,这个接口是阻塞的,会卡住你调用的线程一段时间,而且这个在操作系统里面是无缓的。大部分的实现就是开一个thread pool去做dns解析(libuv)。而libevent自己实现了协议,和io复用一个逻辑,但是遇到C++可能有生存周期的问题,这也是比较尴尬的另一点。
我遇到过dns解析,从发起解析到解析报错结束,这中间可能过了几分钟,这种情况下,其实dns解析应该考虑一个超时处理
如果客户端,尤其是给移动设备之类的用的客户端,考虑做app内的dns缓存的话,其实是有一定风险的,一个可能的场景就是开了VPN->关闭VPN再连接。这种属于很特殊的需求了,如果trigger一个用了解析结果的连接的重连我觉得代价有点大。这种我觉得要考虑给客户一个API,chrome也是这么做的,他可以自己consider这个调用的时候要不要用dns缓存
换挂新机器,因为之前的TTL,旧服务器立马关会G
dns解析在某些实现上可能存在立即回调的,比如/etc/host styles的文件里存在解析映射的时候,比如libevent,这种他可能理解触发callback且返回了