DNS(Domain Name System)是域名系统的缩写,因此DNS的关键在于对请求的域名给予相应的IP地址解析响应。
域名是由一串用点分割的字符组成的Internet上计算机(组)的名称。域名的主要作用是便于记忆一组服务器的地址,并提供字符映射到IP的对应关系。
想要理解DNS,首先就需要熟悉DNS的查询过程。DNS的查询按一定顺序进行,那就是host文件->DNS缓存->DNS服务器。
当DNS接收到域名解析请求时,会首先去检查本机的host文件(一般Linux和Macos系统位于etc/hosts
,Windows系统位于C:\Windows\System32\drivers\etc\hosts
),host文件存储的是域名与IP的键值对。
当host文件没有解析结果时,下一个查找的位置就是DNS缓存,DNS解析结果的缓存时间会根据不同的系统而不同。
当上面两个都无法返回解析结果时,DNS就会向DNS服务器查询结果。
根据上面的不同查询方法,DNS也分为内部DNS查询和外部DNS查询,从host文件或DNS缓存中查询出结果就是内部DNS查询,反之就是外部DNS查询。
一个常见的面试题是,说一说从浏览器输入一个网址到返回页面这个过程中发生了哪些事。这里我们来重点看看这一过程中的DNS查找,之后的TCP连接不作详细讨论。
这个面试题答案第一步就是DNS查找,查找顺序如上所述,而当DNS开始外部查询时,又会发生什么事呢?
稍微了解一点DNS知识后就会知道,DNS服务器是一种树状架构,每个DNS服务器只负责自己zone内的域名解析,而这个树的根就是根DNS服务器。根DNS服务器用.代替,一般域名中是省略的,比如www.baidu.com
的域名全貌是www.baidu.com.
。
全世界有13台根DNS服务器,从a.root-servers.org到m.root-servers.org,可以在https://root-servers.org/ 这个网址看到分布的详细信息
根DNS服务器只负责一些权威DNS服务器的IP解析,比如.com
、.cn
、.org
等,所以让查询看上去挺费劲的,我明明问的是www.baidu.com
的IP地址,你却不告诉我,让我去找.com
权威DNS服务器。当然这样设计肯定是有原因的,DNS的特性就决定了架构一定是分布式的,也只有这样的树状架构才能满足海量的查询请求。
接下来就是递归查询的过程了,.com
权威DNS服务器也不知道IP地址,却可以告诉请求你应该去问baidu.com
的DNS服务器,这样流量就进入了百度的网络中。
整个过程如下图所示:
这里有一个问题,DNS一定是递归查询吗?这当然不是的,实际上DNS还有一种查询方式,就是迭代查询,其实与递归查询差不多,区别在于递归查询是本地DNS服务器来依次去请求根DNS服务器、权威DNS服务器以及其他DNS服务器,而迭代查询则是由客户端来完成的。本地DNS服务器返回结果给客户端后,就不再参与查询了,由客户端去依次请求根DNS服务器、权威DNS服务器以及其他服务器。
一般情况下,为了减少资源的消耗,网络中客户端与所属的本地DNS服务器查询方式通常为递归查询,本地DNS服务器与外部的公共DNS服务器间的查询方式为迭代查询。
与其他的计算机网络协议相似,DNS协议也有自己的报文格式。
DNS的请求和响应的基本单位是DNS报文。请求和响应的DNS报文结构是完全相同的,每个报文都由以下三段(Section)构成:
DNS头部类似与TCP和UDP协议,也定义了一系列与DNS请求或响应有关的字段,具体结构如下:
ID:由生成任何类型查询的程序分配的16位标识符。这个标识符被复制到相应的回复中,请求者可以使用它来匹配对未完成查询的回复。即:ID将由发起查询查找的DNS客户端生成,当响应到来时,可以使用ID将响应映射到查询
QR:0表示查询报文,1表示响应报文
OPCODE:一个4位字段,用于指定此消息中的查询类型。该值由查询的发起者设置并复制到响应中。例如:标准查询或反向查询
AA:权威答案,该位在响应中有效,也表示该服务器是DNS请求的权威服务器
TC:代表报文可截断
RD:表示建议域名服务器进行递归解析
RA:表示支持递归
Z:保留以备将来使用
RCODE:响应代码,4位字段设置为响应的一部分
RCODE | | REFERENCE |
| :—: | ---------------------------------------------------------- | ------------------------------------------- |
| 0 | 没有错误。 | [RFC1035] |
| 1 | Format error:格式错误,服务器不能理解请求的报文格式。 | [RFC1035] |
| 2 | Server failure:服务器失败,因为服务器的原因导致没办法处理这个请求。 | [RFC1035] |
| 3 | Name Error:名字错误,该值只对权威应答有意义,它表示请求的域名不存在。 | [RFC1035] |
| 4 | Not Implemented:未实现,域名服务器不支持该查询类型。 | [RFC1035] |
| 5 | Refused:拒绝服务,服务器由于设置的策略拒绝给出应答。比如,服务器不希望对个请求者给出应答时可以使用此响应码。 | [RFC1035]
QDCOUNT、ANCOUNT、NSCOUNT、ARCOUNT为无符号16bit整数,分别表示报文请求段、回答段、授权段以及附加段中记录数。
域名将表示为一系列标签。每个标签表示为一个八位字节长度字段,后跟该八位字节数。域名以根的空标签的零长度八位字节结束。例如:“example.com”,首先“example.com”由“example”和“com”两部分组成。然后“example”和“com”将分别被URL编码为“69 88 65 77 80 76 69”和“99 111 109”。这将被称为标签。标签前面将有一个整数字节,其中包含该段中的字节数,即:“example”编码成“7 69 88 65 77 80 76 69”。标签中的每个值都可以转换为单个八位字节值,即:(7) (69) (88):00000111 01000101 01011000…最终数据可以放在QNAME问题部分。
Answers、Additional和Authority部分都共享相同的资源记录格式:
关于TYPE,这里简单列举一些DNS记录类型:
至此,DNS的一些基础概念就简单总结完毕了,我们知道了DNS消息(查询和回答)的结构,就可以尝试实现一个自己的DNS服务器。
通常,DNS查询和应答将作为UDP消息传递,因为它们是轻量级的。来自DNS服务器的查询和应答将使用 DNS标头中的ID进行映射。
DNS请求也有可能使用TCP传输,当报文长度过长时,会用TCP重试
TCP将在DNS服务器之间消息传递时使用,例如:zone传输,其中DNS服务器的全部内容被复制到另一个 DNS服务器。
要构建DNS服务器,我们必须侦听特定端口以接收传入查询。默认情况下,DNS服务器在端口53上运行。
//Listen on UDP Port
addr := net.UDPAddr{
Port: 8090,
IP: net.ParseIP("127.0.0.1"),
}
u, _ := net.ListenUDP("udp", &addr)
当我们以UDP消息的形式接收数据时,我们必须从中解码DNS消息。由于DNS消息结构复杂,我们很难自己编写DNS编码和解码。我们可以改为使用现有的开源库,或者我们可以构建一个库来满足更多定制化的需求。
这里我们使用Google的packets
//Listen on UDP Port
addr := net.UDPAddr{
Port: 8090,
IP: net.ParseIP("127.0.0.1"),
}
u, _ := net.ListenUDP("udp", &addr)
// Wait to get request on that port
for {
tmp := make([]byte, 1024)
_, addr, _ := u.ReadFrom(tmp)
clientAddr := addr
packet := gopacket.NewPacket(tmp, layers.LayerTypeDNS, gopacket.Default)
dnsPacket := packet.Layer(layers.LayerTypeDNS)
tcp, _ := dnsPacket.(*layers.DNS)
serveDNS(u, clientAddr, tcp)
}
我们现在可以监听UDP端口并获取UDP消息并使用Google数据包将其解码为DNS消息。在实现serveDNS
功能之前,我们只需要很少的记录来服务DNS查询。
package main
import (
"fmt"
"net"
"github.com/google/gopacket"
layers "github.com/google/gopacket/layers"
)
var records map[string]string
func main() {
records = map[string]string{
"baidu.com": "223.143.166.121",
"github.com": "79.52.123.201",
}
//Listen on UDP Port
addr := net.UDPAddr{
Port: 8090,
IP: net.ParseIP("127.0.0.1"),
}
u, _ := net.ListenUDP("udp", &addr)
// Wait to get request on that port
for {
tmp := make([]byte, 1024)
_, addr, _ := u.ReadFrom(tmp)
clientAddr := addr
packet := gopacket.NewPacket(tmp, layers.LayerTypeDNS, gopacket.Default)
dnsPacket := packet.Layer(layers.LayerTypeDNS)
tcp, _ := dnsPacket.(*layers.DNS)
serveDNS(u, clientAddr, tcp)
}
}
func serveDNS(u *net.UDPConn, clientAddr net.Addr, request *layers.DNS) {
replyMess := request
var dnsAnswer layers.DNSResourceRecord
dnsAnswer.Type = layers.DNSTypeA
var ip string
var err error
var ok bool
ip, ok = records[string(request.Questions[0].Name)]
if !ok {
//Todo: Log no data present for the IP and handle:todo
}
a, _, _ := net.ParseCIDR(ip + "/24")
dnsAnswer.Type = layers.DNSTypeA
dnsAnswer.IP = a
dnsAnswer.Name = []byte(request.Questions[0].Name)
fmt.Println(request.Questions[0].Name)
dnsAnswer.Class = layers.DNSClassIN
replyMess.QR = true
replyMess.ANCount = 1
replyMess.OpCode = layers.DNSOpCodeNotify
replyMess.AA = true
replyMess.Answers = append(replyMess.Answers, dnsAnswer)
replyMess.ResponseCode = layers.DNSResponseCodeNoErr
buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{} // See SerializeOptions for more details.
err = replyMess.SerializeTo(buf, opts)
if err != nil {
panic(err)
}
u.WriteTo(buf.Bytes(), clientAddr)
}
这些记录在主函数的开始初始化。
并且主函数不断侦听端口8090,并提供查询DNS服务器功能。
该serveDNS
函数获取连接,客户端地址和查询请求作为参数。然后,该请求消息用于模拟响应消息,并且必须在响应变化的值也被修改。
然后,响应消息被序列化到UDP消息格式并写回,使用在接收作为UDP包中的DNS消息中获得的客户端地址的客户端。
运行此程序,我们应该能够解析主函数中指定的记录的DNS查询,即运行下面的命令应该从我们创建的DNS服务器带来的DNS响应。
dig github.com @localhost -p8090 +short
返回:
; <<>> DiG 9.10.6 <<>> github.com @localhost -p8090
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: NOTIFY, status: NOERROR, id: 16550
;; flags: qr aa rd ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;github.com. IN A
;; ANSWER SECTION:
github.com. 0 IN A 79.52.123.201
;; Query time: 0 msec
;; SERVER: 127.0.0.1#8090(127.0.0.1)
;; WHEN: Thu Aug 12 16:33:53 CST 2021
;; MSG SIZE rcvd: 65
可以优化serveDNS
功能,可以把整个流程做成一个库,这样我们就可以轻松的搭建DNS服务器了。