最近實習作的DNS相關知識,所以在這裏記錄一下,也算是一個階段總結git
DNS(Domain Name System)是域名系統的縮寫,所以DNS的關鍵在於對請求的域名給予相應的IP地址解析響應。github
域名是由一串用點分割的字符組成的Internet上計算機(組)的名稱。域名的主要做用是便於記憶一組服務器的地址,並提供字符映射到IP的對應關係。面試
想要理解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服務器,這樣流量就進入了百度的網絡中。
整個過程以下圖所示:
這裏有一個問題,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服務器了。
以上代碼我在github中作了進一步更新,後續計劃慢慢補充。