DNS基礎知識以及golang實現的簡單DNS服務器

最近實習作的DNS相關知識,所以在這裏記錄一下,也算是一個階段總結git

DNS基礎

DNS(Domain Name System)是域名系統的縮寫,所以DNS的關鍵在於對請求的域名給予相應的IP地址解析響應。github

域名是由一串用點分割的字符組成的Internet上計算機(組)的名稱。域名的主要做用是便於記憶一組服務器的地址,並提供字符映射到IP的對應關係。面試

DNS的查詢過程

想要理解DNS,首先就須要熟悉DNS的查詢過程。DNS的查詢按必定順序進行,那就是host文件->DNS緩存->DNS服務器。shell

當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查詢。markdown

遞歸查詢

一個常見的面試題是,說一說從瀏覽器輸入一個網址到返回頁面這個過程當中發生了哪些事。這裏咱們來重點看看這一過程當中的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,能夠在root-servers.org/ 這個網址看到分佈的詳細信息

根DNS服務器只負責一些權威DNS服務器的IP解析,好比.com.cn.org等,因此讓查詢看上去挺費勁的,我明明問的是www.baidu.com的IP地址,你卻不告訴我,讓我去找.com權威DNS服務器。固然這樣設計確定是有緣由的,DNS的特性就決定了架構必定是分佈式的,也只有這樣的樹狀架構才能知足海量的查詢請求。

接下來就是遞歸查詢的過程了,.com權威DNS服務器也不知道IP地址,卻能夠告訴請求你應該去問baidu.com的DNS服務器,這樣流量就進入了百度的網絡中。

整個過程以下圖所示:

1_20lOJctutX1PTdWzYUbbZQ.png

迭代查詢

這裏有一個問題,DNS必定是遞歸查詢嗎?這固然不是的,實際上DNS還有一種查詢方式,就是迭代查詢,其實與遞歸查詢差很少,區別在於遞歸查詢是本地DNS服務器來依次去請求根DNS服務器、權威DNS服務器以及其餘DNS服務器,而迭代查詢則是由客戶端來完成的。本地DNS服務器返回結果給客戶端後,就再也不參與查詢了,由客戶端去依次請求根DNS服務器、權威DNS服務器以及其餘服務器。

通常狀況下,爲了減小資源的消耗,網絡中客戶端與所屬的本地DNS服務器查詢方式一般爲遞歸查詢,本地DNS服務器與外部的公共DNS服務器間的查詢方式爲迭代查詢。

DNS消息報文

與其餘的計算機網絡協議類似,DNS協議也有本身的報文格式。

DNS的請求和響應的基本單位是DNS報文。請求和響應的DNS報文結構是徹底相同的,每一個報文都由如下三段(Section)構成:

1_viPXyk-o9vlctIm0GtusfA.jpeg

Header

DNS頭部相似與TCP和UDP協議,也定義了一系列與DNS請求或響應有關的字段,具體結構以下:

0_dyNveLl0EzJyZwJu.png

  • 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

0_1Hy35bpijWoFGcbE.png

  • 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部分都共享相同的資源記錄格式:

0_mrpbXLdT3u61RgM4.png

  • 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服務器了。

以上代碼我在github中作了進一步更新,後續計劃慢慢補充。

相關文章
相關標籤/搜索