DNS 報文結構和我的 DNS 解析代碼實現——解決 getaddrinfo() 阻塞問題

實際應用中發現一個問題,在某些國家/ 地區的某些 ISP 提供的網絡中,程序在請求 DNS 以鏈接一些服務器的時候,有時候會由於 ISP 的 DNS 遞歸查詢太慢,致使設備端認爲 DNS 超時了,沒法獲取服務器 IP。html

給用戶的解決方案是:請不要用 ISP 自動分配的 DNS server,改用 8.8.8.8 就解決了。git

可是讓用戶這麼配置太麻煩、也太不友好了。因而我就思考:能不能本身實現 DNS 服務,當 ISP 的 DNS 請求超時或者失敗的時候,就從內部直接向 8.8.8.8 請求 DNS 信息,能夠不?github

若是要使用 gethostbyname()getaddrinfo() 來解決這個問題的話,方案是將 /etc/resolve.conf 修改了。但這並非正確的辦法,由於這種改法一來不許確,二來會影響系統其餘 DNS 請求。可行的方案是:本身構建 DNS 請求,而且本身解析得到咱們須要的 IP 信息。數據庫

本文地址:http://www.javashuo.com/article/p-wvrczbuh-r.htmlsegmentfault

Reference

DNS 這樣一個在網絡互聯中算是一個比較簡單的協議,實現我如此簡單的需求,竟然沒有哪一個參考資料可以覆蓋我須要的知識點……服務器

我本身也進行了抓包,抓包的時候,建議不直接向權威的 DNS server 發送請求,而是向網關、路由器等提供 DNS 中繼的服務器發,這樣能夠得到比下面最後一個參考資料更多的信息。網絡

《用 TCP / IP 進行網際互聯(第五版)——原理、協議與結構(第五版)》,Douglas E. Comer
《計算機網絡(第5版)》,Andrew S. Tannenbaum, David J. Wetherall:男神塔能鮑姆教授!
DNS Protocol
DNS Reference Information:有各類 type 的說明
Domain Name System (DNS) Parameters:有各類參數的總集合
DNS Name Notation and Message Compression Technique
RFC-1035
對 DNS 報文的理解
DNS message解析:這篇文章也挺仔細地說明了 DNS 報文結構,圖形控能夠看
利用 WireShark 進行 DNS 協議分析異步

DNS 基本概念

簡要整理一些和本文相關的點:socket

DNS 的本質是發明了一種層次的、基於域的命名方案,而且用一個分佈式數據庫系統加以實現。DNS 的主要做用是將主機名映射成 IP 地址。tcp

DNS 解析的發起端通常是互聯網 Server / Client 模型中的 client 端(如下稱 client 端,指的就是發起 DNS 解析的一端),如今大部分的 C 語言 client 端都使用 getaddrinfo() 實現。之前通常用 gethostbyname() 由於一些緣由再也不推薦使用了,而且也只支持 IPv4。

DNS 解析中,DNS server 開放的端口應當是 53 端口。當 client 端做出請求時,server 返回的不只僅是 IP 信息,還包含於該域名相關聯的資源記錄。

僅僅從一個域名 URL 中,咱們不能區分這是一個域名仍是某個對象(主機)名。域名的總長度應小於等於 255 個字節,域名的每一段則必須小於等於 63 字節

DNS 報文格式

DNS 請求的格式和響應格式差很少,就不單獨講了。從 UDP 數據包的正文部分算起,DNS 報文的結構按順序以下:

數據類型 Ethereal 裏的名字 說明
uint16_t Transaction ID 標識符。下文說明
uint16_t Flags 參數。下文說明
uint16_t Questions 詢問列表的數目
uint16_t Answer RRs (直接) 的回答數
uint16_t Authority RRs 認證機構數目(僅響應包裏有)
uint16_t Additional RRs 附加信息數目(僅響應包裏有)
variable Queries 請求數據的正文。請求包中只有這個。響應包也會附上本來的請求數據
variable Answers 響應數據的正文
variable Authortative name servers 域名管理機構數據
variable Additional records 附加信息數據
  • Transaction ID:這是由 client 端指定的標識數據,DNS server 會將這個字段原樣返回,client 端能夠用來區分不一樣的 DNS 請求
  • RRResource Record 的縮寫

Flags

16 bits 的值,各部分按順序以下(按順序:位號、Ethereal 名稱、說明):

  • Bit 15,Response:0 表示查詢,1 表示響應(query / response)
  • Bit 14~11, Opcode:查詢類型——請求和響應包都適用:

    • 0:普通查詢(最經常使用的)
    • 1:反向查詢
    • 2:服務器狀態請求
    • 3:通知
    • 4:更新(貌似是用在 DDNS 的?)
  • Bit 10, Authoritative:用於響應包,判斷服務器是否一個認證的域服務器
  • Bit 9, Truncated:報文是否被截斷了。收發包都用
  • Bit 8, Recursion desired:收發包都用,表示是否須要用遞歸。做爲 client 端,最好置 1,要否則 DNS 不執行遞歸查詢,將有不少數據沒能查到
  • Bit 7, Recursion available:響應包用,表示服務器是否有能力使用遞歸查詢
  • Bit 6:這個數據段,Ethereal 說是保留位,而書中表示數據是不是鑑別的——求確認
  • Bit 5, Answer authenticated:數據是否被服務器鑑定過(貌似抓到的包裏都是 0)
  • Bit 4, Reserved
  • Bit 3~0, Reply code:響應狀態碼,以下(參見 Micrisoft 資料 的 「DNS update message flags field」 小節):

    • 0:OK
    • 1:查詢格式錯誤
    • 2:服務器內部錯誤
    • 3:名字不存在
    • 4:這個錯誤碼不支持
    • 5:請求被拒絕
    • 6:name 在不該當出現時出現(什麼鬼)
    • 7:RR 設置不存在
    • 8:RR 設置應當存在可是卻不存在(什麼鬼)
    • 9:服務器不具有改管理區的權限
    • 10:name 不在管理區中

資源記錄(RR)的格式

每一條 RR 的格式以下:

數據類型 Ethereal 裏的名字 說明
variable Name 資源的域名——其實前文已經出現了
uint16_t Type 類型。下文說明
uint16_t Class 大多數是 0x0001,表明 IN
uint32_t Time to Live TTL 秒數
uint16_t Data length 當前 RR 剩餘部分的長度
variable RR 主數據

若是是請求數據的話,那麼 TTL、Data Length 和 RR 主數據都不須要

Type 的大部分值在 RFC-1035 中定義,此外的一些在其餘文檔定義(好比 IPv6)。我會用到的有:

  • 1:「A」,表示 IPv4 地址
  • 2:「NS」,域名服務器的名字
  • 28:「AAAA」,表示 IPv6 地址
  • 5:「CNAME」,規範名,常常會有一個 CNAME 跟着一票 A 和 AAAA

域名壓縮顯示

這一部分直接參考的是 RFC-1035 的 「4.1.4. Message Compression」小節。

RR 中的 Name 字段,有三種表示方法(不是官方分類,而是本人本身分的):

完整域名錶示

好比表示 「www.google.com」 這樣一個完整的域名,須要如下16個字節:

B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 B10 B11 B12 B13 B14 B15
\3 w w w \6 g o o g l e \3 c o m \0

注意這裏並非把谷歌的 URL 使用簡單的 char * 字符串複製上去,而是將每一段都分割開來。本例子中將域名分紅了三段,分別是 www, google, com。每一段開頭都會有一個字節,表示後面跟着的那段域名的字節長度。最後當讀到 \0 的時候,表示再也不有數據了(這裏和 char *\0 含義有一點不一樣,雖然形式上是同樣的)

標號表示

前文咱們提到,域名的每一段,最長不能超過 63 個字節,所以在表示域名段長度的這個字節的最高兩0xC0),必然是 0。這就引伸出了這裏的第二種用法。

這種表示法中,至關於一個指針,指代 DNS 報文中的某一個域名段。在解析一段 RR 數據段時,須要判斷域長度嘛,判斷的邏輯是:

  • 若是最高兩位是 00,則表示上面第一種
  • 若是最高兩位是 11,則表示這是一個壓縮表示法。這一個字節去掉最高兩位後剩下的6位,以及接下來的 8 位總共 14 位長的數據,指向 DNS 數據報文中的某一段域名(不必定是完整域名,參見第三種),能夠算是指針吧。

好比 0xC150,表示從 DNS 正文(UDP payload)的 offset = 0x0150 處所表示的域名。0x0150 是將 0xC150 最高兩位清零獲得的數字。

混合表示

這就是上面兩種的混合表示。好比說,咱們假設前文表示 www.google.com 的完整域名的數據段處於 DNS 報文偏移 0x20 處,那麼有如下幾種可能的用法:

  • 0xC020:天然就表示 www.google.com
  • 0xC024:從完整域名的第二段開始,指代 google.com
  • 0x016DC024:其中 0x6d 就是字符 m,於是 0x016D單獨指代字符串 m;而第二段 0xC024 則指代 google.com,所以整段表示 m.google.com

分析工具

除了 Ethereal 以外,推薦的分析工具備:

代碼實現

代碼實如今我用來研究 epoll() 的分支中,GitHub 工程在此,許可證爲 LGPL。
實現邏輯上其實仍是挺簡單的,照着上面提到的原理實現就行了。大部分的代碼和本文無關,只須要看裏面的 AMCDns.c / h 文件便可。

個人這些代碼能夠徹底代替阻塞的 getaddrinfo() 函數,甚至也能夠集成到異步 I/O 庫中。使用流程以下:

  1. 調用 socket() 建立一個 UDP 套接字並 bind()
  2. 調用 AMCDns_GetDefaultServer() 獲取系統默認配置的 DNS 服務器
  3. 若是不使用系統默認的 DNS 服務器,則須要使用 struct addrinfo 類型來指定。
  4. 調用 AMCDns_SendRequest() 請求指定域名的 IP 信息
  5. 調用 AMCDns_RecvAndResolve() 獲取摘要的或完整的響應。
  6. 調用 AMCDns_FreeResult() 清除 DNS 響應數據以免內存泄露
  7. close() 掉 socket
相關文章
相關標籤/搜索