DNS基础知识以及golang实现的简单DNS服务器

DNS基础

DNS(Domain Name System)是域名系统的缩写,因此DNS的关键在于对请求的域名给予相应的IP地址解析响应。

域名是由一串用点分割的字符组成的Internet上计算机(组)的名称。域名的主要作用是便于记忆一组服务器的地址,并提供字符映射到IP的对应关系。

DNS的查询过程

想要理解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基础知识以及golang实现的简单DNS服务器_第1张图片

迭代查询

这里有一个问题,DNS一定是递归查询吗?这当然不是的,实际上DNS还有一种查询方式,就是迭代查询,其实与递归查询差不多,区别在于递归查询是本地DNS服务器来依次去请求根DNS服务器、权威DNS服务器以及其他DNS服务器,而迭代查询则是由客户端来完成的。本地DNS服务器返回结果给客户端后,就不再参与查询了,由客户端去依次请求根DNS服务器、权威DNS服务器以及其他服务器。

一般情况下,为了减少资源的消耗,网络中客户端与所属的本地DNS服务器查询方式通常为递归查询,本地DNS服务器与外部的公共DNS服务器间的查询方式为迭代查询。

DNS消息报文

与其他的计算机网络协议相似,DNS协议也有自己的报文格式。

DNS的请求和响应的基本单位是DNS报文。请求和响应的DNS报文结构是完全相同的,每个报文都由以下三段(Section)构成:

DNS基础知识以及golang实现的简单DNS服务器_第2张图片

Header

DNS头部类似与TCP和UDP协议,也定义了一系列与DNS请求或响应有关的字段,具体结构如下:

DNS基础知识以及golang实现的简单DNS服务器_第3张图片

  • 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]

QDCOUNTANCOUNTNSCOUNTARCOUNT为无符号16bit整数,分别表示报文请求段、回答段、授权段以及附加段中记录数。

Question

DNS基础知识以及golang实现的简单DNS服务器_第4张图片

  • QNAME:该字段包含我们希望解析的域名。

域名将表示为一系列标签。每个标签表示为一个八位字节长度字段,后跟该八位字节数。域名以根的空标签的零长度八位字节结束。例如:“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问题部分。

  • QTYPE:这个2bit值指定了Query的类型
  • QCLASS:指定Query的类别,主要是IN(internet)

DNS Answers、Additional和Authority

Answers、Additional和Authority部分都共享相同的资源记录格式:

DNS基础知识以及golang实现的简单DNS服务器_第5张图片

  • TYPE:指定查询的类型。例如:标准或反向查询
  • TTL:这个32位字段表示资源记录(答案)可以被缓存的时间量,零值表示不应缓存资源记录。
  • RDLENGTH:Response Data Length,16位字段表示Response Data字段中八位字节的长度。
  • RDATA:描述资源的可变长度八位字节串。此信息的格式根据资源记录的TYPE和CLASS而有所不同。例如,如果TYPE是A(IPv4)并且CLASS是IN,RDATA字段是一个4个八位字节的ARPA Internet地址。

关于TYPE,这里简单列举一些DNS记录类型:

  • A记录:记录域名对应的IPv4地址
  • AAAA记录:记录域名对应的IPv6地址
  • CNAME记录:将域名记录指向另一个域名记录
  • NS记录:指定域名由哪个DNS服务器来解析
  • PTR记录:A记录的反向解析,将IP映射到对应的域名
  • MX记录:将域名记录指向邮件服务器
  • TXT记录:某个主机名或域名的说明
  • SOA记录:指定多个NS记录中的主服务器
  • SRV记录:服务器资源记录

至此,DNS的一些基础概念就简单总结完毕了,我们知道了DNS消息(查询和回答)的结构,就可以尝试实现一个自己的DNS服务器。

Go实现的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服务器了。

你可能感兴趣的:(golang,服务器,golang,网络)