聽說你想寫個 DNS 服務器 - 數據包

大家好,我是微微笑的蝸牛,??。

今天將開始一個新的系列,動手寫 DNS 服務器,使用 Swift 實現。

我們知道,DNS 服務器是用來解析域名,返回對應的 IP 地址。它使用請求/應答模型,客戶端發送請求包,服務器解析后返回應答的數據包。

既然要自己動手寫 DNS 服務器,那我們就得先了解下 DNS 數據包的結構是什么樣的。

數據包格式

DNS 請求和應答的數據包格式是一樣的,包括包頭和包體數據兩部分。

先看看數據包的概覽圖:

它包括如下部分:

  • Header,頭部,長度為 12 字節
  • Question Section,查詢列表
  • Answer Section,記錄列表
  • Authority Section,權威服務器列表

包含 Name Server 信息,也就是 NS 記錄,用于逐級向下遞歸查詢。

NS 記錄表示域名服務器信息,只能返回域名。

舉個例子,以下是一條 NS 記錄:

com.   6285 IN NS g.gtld-servers.net.

它表明 g.gtld-servers.net 是一個 DNS 服務器,分管 com 域名。

  • Additional Section,附加信息列表

這是比較有用的一部分,比如可包含 Name Server 對應的 IP 信息,也就是 A 記錄。

A 代表 Address,表示域名與 IP 的映射關系。

舉個例子,以下是一條 A 記錄:

a.gtld-servers.net. 1517    IN  A   192.5.6.30

這就是 NS 對應的 A 記錄,包含了 IP 信息。當拿到 IP 后,我們可以繼續向該 DNS 服務器查詢。

Dig

接著我們使用 dig 命令,查看服務器返回的數據,看是否能跟上述結構對應上。noedns 表示按原始數據展示。

$ dig +noedns google.com

; <<>> DiG 9.10.6 <<>> +noedns google.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 7159
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 13, ADDITIONAL: 11

;; QUESTION SECTION:
;google.com.            IN  A

;; ANSWER SECTION:
google.com.     127 IN  A   142.250.204.78

;; AUTHORITY SECTION:
com.            6285    IN  NS  g.gtld-servers.net.
com.            6285    IN  NS  k.gtld-servers.net.
com.            6285    IN  NS  c.gtld-servers.net.
com.            6285    IN  NS  d.gtld-servers.net.
com.            6285    IN  NS  b.gtld-servers.net.
com.            6285    IN  NS  f.gtld-servers.net.
com.            6285    IN  NS  i.gtld-servers.net.
com.            6285    IN  NS  m.gtld-servers.net.
com.            6285    IN  NS  a.gtld-servers.net.
com.            6285    IN  NS  l.gtld-servers.net.
com.            6285    IN  NS  h.gtld-servers.net.
com.            6285    IN  NS  j.gtld-servers.net.
com.            6285    IN  NS  e.gtld-servers.net.

;; ADDITIONAL SECTION:
a.gtld-servers.net. 1517    IN  A   192.5.6.30
a.gtld-servers.net. 57320   IN  AAAA    2001:503:a83e::2:30
b.gtld-servers.net. 438 IN  A   192.33.14.30
b.gtld-servers.net. 32119   IN  AAAA    2001:503:231d::2:30
c.gtld-servers.net. 3318    IN  A   192.26.92.30
c.gtld-servers.net. 172527  IN  AAAA    2001:503:83eb::30
d.gtld-servers.net. 1877    IN  A   192.31.80.30
d.gtld-servers.net. 60201   IN  AAAA    2001:500:856e::30
e.gtld-servers.net. 438 IN  A   192.12.94.30
e.gtld-servers.net. 6198    IN  AAAA    2001:502:1ca1::30
f.gtld-servers.net. 2957    IN  A   192.35.51.30

;; Query time: 0 msec
;; SERVER: 172.26.9.10#53(172.26.9.10)
;; WHEN: Sun Jul 04 16:26:53 CST 2021
;; MSG SIZE  rcvd: 504

它包含了如下幾部分:

  • ->>HEADER<<-:表示包頭,后面的數據是包頭字段對應的值。

    // 操作碼
    opcode: QUERY, 
    
    // 狀態
    status: NOERROR, 
    
    id: 7159
    
    // 設置的標記,qr 表示 response;rd 表示 recursion desire;ra 表示 Recursion available
    flags: qr rd ra;
    
    // 查詢個數為 1 
    QUERY: 1, 
    
    // 記錄個數為 1
    ANSWER: 1, 
    
    // ns 個數為 13
    AUTHORITY: 13, 
    
    // 附加信息個數為 11
    ADDITIONAL: 11
    
  • 之后是 QUESTION SECTION,有一條查詢數據。IN 表示 ClassA 是記錄類型。

    google.com.         IN  A
    
  • ANSWER SECTION,返回了一條記錄。127TTL142.250.204.78 是 IP。

    google.com.     127 IN  A   142.250.204.78
    
  • AUTHORITY SECTION,有 13 條 NS 記錄。

  • ADDITIONAL SECTION,有 11 條 NS 對應的 A 記錄信息。

我們可以看出,這些數據是跟包的結構是吻合的。

DNS Header

再來看看 Header 部分,照例先上結構圖。

image

包頭總共 12 字節,包括如下部分:

  • ID,包標識,16 bits。查詢與應答包的 ID 一致。
  • QR,Query Response,1 bit。用于確定是查詢還是應答,0 為查詢,1 為應答。
  • OPCODE,Operation Code,4 bit。一般為 0。
  • AA,Authoritative Answer,1 bit。如果 DNS 服務器是權威的,則為 1。
  • TC,Truncated Message,消息是否截斷,1 bit。如果數據包長度大于 512 字節,則為 1。
  • RD,Recursion Desired,1 bit,是否期望遞歸查詢。
  • RA,Recursion Available,1 bit,服務器是否支持遞歸查詢。
  • Z,保留字段,3 bits。
  • RCODE,Response Code,4 bits,返回碼。
  • QDCOUNT,Question Count,16 bits,查詢數量。
  • ANCOUNT,Answer Count,16 bits,結果數量。
  • NSCOUNT,Name Server Count,16 bits,ns 的數量。
  • ARCOUNT,Additional Count,16 bits,附加信息數量。

DNS Question

接著我們再來看看查詢部分,可包含多條查詢信息。

查詢的結構如下圖所示:

image

它包括三部分:

  • domain:要查詢的域名,比如 google.com
  • type:記錄的類型,2 字節。
  • class:2 字節,一般為 1。

DNS Answer

同樣,應答部分可包含多條記錄。記錄結構如下圖所示:

image

它包括如下幾部分:

  • domain:要查詢的域名,比如 google.com
  • type:記錄類型,2 字節。
  • class:2 字節,一般為 1。
  • ttl:Time To Live,存活時間,4 字節。
  • data_len:數據長度,2 字節。
  • ip:ip 地址,4 字節。

Domain

在查詢和記錄的結構中都包括了域名字段,那么域名在數據包中是如何表示的呢?

1. 域名存儲結構

我們知道,域名是一串字符,以 "." 號分隔,比如 google.com

一般情況下,如果需存儲一段字符,我們會將各字符對應的 ASCII 寫入。但是在 DNS 包數據中,. 是不存儲的。

它的做法是:

  • 將域名以 . 分隔后,將各部分進行存儲。比如 google.com,只會存儲 googlecom 兩部分。

  • 在各部分之前還會加上數據長度,也就是 「數據長度+數據」 的格式。

舉個栗子,googlecom 的存儲分別如下:

image
  • 最后以 0x00 空字符結束。

2. 舉例說明

還是拿 google.com 舉例。以 . 分隔后,它會變成兩部分:googlecom

現在我們遵循「數據長度+數據」這個格式,來構造一下數據。

  • 先看 google,它的長度是 6,那么表示為 6+google
  • 再看 com,它的長度是 3,那么表示為 3+com
  • 最后加上結束符 0x00

完整數據表示如下:

image

3. 例外情況

但是存在一種例外情況,域名使用間接的方式來表示。

比如響應包中包含了查詢部分和記錄部分,它們都包含了域名字段。既然在查詢部分已經有域名數據了,那在記錄部分就不需使用重復的數據來表示。只需使用某種間接方式,沿用查詢部分的域名即可。

這也是一種減少包大小的方式。如果有很多相同的域名,那么只需存儲一份真正的域名即可,其余的都使用間接方式指向真正的域名。

如下所示,間接表示時,通過某種方式指向到原查詢部分的域名。

image

所以,只需要找到一種可以間接指向到真正域名的方式即可。

上面說到常規的域名表示為:「數據長度+數據」。而間接表示時,在數據長度上做了些手腳,引出了跳轉偏移的概念。

跳轉偏移是相對于整個數據包的偏移量。從數據長度中計算出偏移量后,再轉到偏移處重新讀取域名數據。

正常情況下,數據長度是一字節。為了兼容間接表示的情況,有這樣一個約定:如果長度的高兩位都是 1,則表示是間接方式。這時候,數據長度之后的一個字節也將表示長度,也就是兩個字節表示長度。

跳轉偏移量的計算方式是:去除第一個字節的高兩位,余下的 14 位數據則為偏移量。

如下圖所示:

image

用數學公式表示如下,使用異或的方式清除高 2 位。

// b1 是第一個字節,b2 是第二個字節
b1b2 ^ 0xC000

舉個栗子,比如頭兩個字節是 0xC00C

由于第一個字節 0xC0 → 11000000 高兩位是 1,那么接下來的一個字節 0x0C 也表示長度。

根據公式計算出偏移量:0xC00C ^ 0xC000 = 0x000C,接著去偏移量為 12 字節的地方去查找真實域名。

偏移部分如下圖所示:

image

其實上面這個栗子不是隨便舉的,是真實的數據。此時偏移量為 12,而 Header 的頭部剛好也是 12 字節。哈哈,是不是有點巧合?

由于頭部后面跟著的是查詢數據,而查詢結構的第一個字段就是域名。這樣就會恰好跳轉到查詢結構中的域名處,然后按正常情況讀取該域名。一切都剛剛好~

如下圖所示:

image

數據包抓取

在了解數據包的結構后,現在我們就來抓下 DNS 的數據包,解析出具體的數據。

這里,我們使用 netcat 監聽某端口的數據,然后執行 dig 命令,將查詢數據發到同樣的端口。

步驟如下:

  1. 打開終端,執行如下命令:
nc -u -l 1053 > query_packet.txt
  1. 新開另一個終端窗口,執行如下命令:
dig +retry=0 -p 1053 @127.0.0.1 +noedns google.com

這時,命令會執行失敗。

  1. 在 netcat 命令窗口,按住 CTRL+C 終止進程,這樣在 query_packet.txt 就有了查詢的數據。

  2. 在 netcat 命令窗口,執行如下命令。使用 query_packet 中的包發送請求,同時記錄響應數據。

nc -u 8.8.8.8 53 < query_packet.txt > response_packet.txt
  1. 在 netcat 命令窗口,按住 CTRL+C 終止進程。同樣響應數據會寫入 response_packet.txt 中。

這樣我們就得到了查詢數據和返回數據,接下來進行數據包的分析。

數據包分析

我們可使用 hexdump 查看包中的十六進制數據。

hexdump -C query_packet.txt

請求包

query_packet.txt 中十六進制數據如下:

00000000  19 59 01 20 00 01 00 00  00 00 00 00 06 67 6f 6f  |.Y. .........goo|
00000010  67 6c 65 03 63 6f 6d 00  00 01 00 01              |gle.com.....|
0000001c

我們按照包結構一點點的分析。

  1. 首先是包頭部分。

包頭有 12 個字節,將數據提取出來如下所示。注意:數據是大端字節序

19 59 01 20 00 01 00 00  00 00 00 00

根據包頭中數據的屬性歸類,將其分為三部分:

  • ID 部分:2 字節,0x1959
  • flag 部分:2 字節,0x0120
  • count 部分:8 字節,0x01000000

各字段的值就不一一分析了,如下所示:

image
  1. 包體部分

由于是請求包,只會有查詢部分,數據就是剩余部分,如下所示:

06 67 6f 6f 67 6c 65 03 63 6f 6d 00  00 01 00 01

上面我們提到,查詢數據的結構如下:

image

首先說下 domain 的解析。

正常情況下,domain 的數據格式是「數據長度+數據」。規則很簡單,解析過程如下所示:

  • 解析出一個字節的數據長度 len。
  • 讀取后面 len 個字節的數據。因為分隔符 . 不存儲,需手動加上。
  • 不斷循環上面的步驟,直至碰到結束符 0x00 退出循環。

參照上面的步驟,可解析出 domain 的值為:google.com

接著就是解析 type、class 的值,按照它們所占字節數逐個解析就好。

最后各部分的值,如下圖所示:

image

響應包

response_packet.txt 中十六進制數據如下:

00000000  19 59 81 80 00 01 00 01  00 00 00 00 06 67 6f 6f  |.Y...........goo|
00000010  67 6c 65 03 63 6f 6d 00  00 01 00 01 c0 0c 00 01  |gle.com.........|
00000020  00 01 00 00 00 1c 00 04  8e fa cc 4e              |...........N|
0000002c
  1. 包頭部分

跟請求包一樣的分析方式,就不再贅述了,直接給出結果吧。

image
  1. 包體部分

響應的數據比請求數據要多一些,因為除了待查詢數據,還包含了返回記錄。查詢數據跟請求包中的數據是一樣的,解析就不再重復說了。

這里主要說說返回記錄數據的解析,數據從 c0 開始。

我們先回顧下記錄的數據結構,看看下圖,第一個字段是域名。

image

再看看記錄部分的數據,如下所示:

// 記錄部分的數據
c0 0c 00 01 00 01 00 00 00 1c 00 04  8e fa cc 4e

我們可以發現,第一個字節 0xc0 是以 0x11 開頭的,說明 domain 是采用了間接的獲取方式,這時候前兩個字節 0xc00c 表示長度。

按照前面提到過的公式,可計算出偏移量為 12:

0xC00C ^ 0xC000 = 0x000C

而包頭大小剛好是 12 字節,這時候就會跳轉到包體開始的位置,也就是查詢數據部分,以正常方式讀取域名。

間接域名指向可看下圖中標紅部分:

image

在域名解析完成后,接下來是 type、class、ttl、data_len、ip 數據的解析。處理起來比較簡單,按照它們各自所占字節數解析就好,各部分的值參照上圖。

代碼實現

完整代碼實現可查看:https://github.com/silan-liu/dns-server

工程結構分為如下幾部分:

  • BytePacketBuffer,主要是對數據的操作,比如讀取數據、移動指針等。
  • DNSHeader,包頭數據的結構定義及數據解析。
  • DNSQuestion,查詢數據的結構定義及數據解析。
  • DNSRecord,記錄數據的結構定義及數據解析。
  • DNSPacket,數據包的結構定義及數據解析。
  • main,讀取本地的響應包數據,解析出數據包。

代碼實現就不詳細進行分析了,相信聰明的你可以輕易看懂~

總結

這篇文章主要介紹了 DNS 數據包的結構,各個部分的字段定義,布局信息以及如何進行相關數據的解析。

下一節將講述如何準備響應數據包,主要是關于數據包的寫入操作。

感謝閱讀,希望能給你帶來一點點收獲~

參考資料

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容