實際應用中發現一個問題,在某些國家/ 地區的某些 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
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 協議分析異步
簡要整理一些和本文相關的點: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 請求的格式和響應格式差很少,就不單獨講了。從 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 | 附加信息數據 |
16 bits 的值,各部分按順序以下(按順序:位號、Ethereal 名稱、說明):
Bit 14~11, Opcode:查詢類型——請求和響應包都適用:
0
:普通查詢(最經常使用的)1
:反向查詢2
:服務器狀態請求3
:通知4
:更新(貌似是用在 DDNS 的?)Bit 3~0, Reply code:響應狀態碼,以下(參見 Micrisoft 資料 的 「DNS update message flags field」 小節):
0
:OK1
:查詢格式錯誤2
:服務器內部錯誤3
:名字不存在4
:這個錯誤碼不支持5
:請求被拒絕6
:name 在不該當出現時出現(什麼鬼)7
:RR 設置不存在8
:RR 設置應當存在可是卻不存在(什麼鬼)9
:服務器不具有改管理區的權限10
:name 不在管理區中每一條 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 數據段時,須要判斷域長度嘛,判斷的邏輯是:
去掉最高兩位
後剩下的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 庫中。使用流程以下:
socket()
建立一個 UDP 套接字並 bind()
AMCDns_GetDefaultServer()
獲取系統默認配置的 DNS 服務器AMCDns_SendRequest()
請求指定域名的 IP 信息AMCDns_RecvAndResolve()
獲取摘要的或完整的響應。AMCDns_FreeResult()
清除 DNS 響應數據以免內存泄露close()
掉 socket