DNS
DNS【域名系統:(英文:Domain Name System,縮寫:DNS)】是網際網路的一項服務。 它作為將域名和IP地址相互對映的一個分散式資料庫,能夠使人更方便地訪問網際網路。 DNS使用TCP和UDP埠53。
白話版
就是客戶端(例如:瀏覽器)傳入的網站域名,到DNS列表中找到對應的ip返回給客戶端,然後客戶端根據ip就可以找到對應的伺服器,就可以向伺服器傳送請求了。
說的在直接點:DNS目的就是把對應伺服器IP給客戶端。最後客戶端與伺服器通訊就沒DNS什麼事了。
DNS 報文格式
DNS報文格式,不論是請求報文,還是DNS伺服器返回的應答報文,都使用統一的格式。
Header
報文頭Question
查詢的問題Answer
應答Authority
授權應答Additional
附加資訊
DNS format
+--+--+--+--+--+--+--+
| Header |
+--+--+--+--+--+--+--+
| Question |
+--+--+--+--+--+--+--+
| Answer |
+--+--+--+--+--+--+--+
| Authority |
+--+--+--+--+--+--+--+
| Additional |
+--+--+--+--+--+--+--+
複製程式碼
Header 報文頭
ID
:2
個位元組(16bit
),標識欄位,客戶端會解析伺服器返回的DNS應答報文,獲取ID
值與請求報文設定的ID
值做比較,如果相同,則認為是同一個DNS會話。FLAGS
:2
個位元組(16bit
)的標誌欄位。包含以下屬性:QR
:0
表示查詢報文,1
表示響應報文;opcode
: 通常值為0
(標準查詢),其他值為1
(反向查詢)和2
(伺服器狀態請求),[3,15]
保留值;AA
: 表示授權回答(authoritative answer)-- 這個位元位在應答的時候才有意義,指出給出應答的伺服器是查詢域名的授權解析伺服器;TC
: 表示可截斷的(truncated)--用來指出報文比允許的長度還要長,導致被截斷;RD
: 表示期望遞迴(Recursion Desired) -- 這個位元位被請求設定,應答的時候使用的相同的值返回。如果設定了RD,就建議域名伺服器進行遞迴解析,遞迴查詢的支援是可選的;RA
: 表示支援遞迴(Recursion Available) -- 這個位元位在應答中設定或取消,用來代表伺服器是否支援遞迴查詢;Z
: 保留值,暫未使用;RCODE
: 應答碼(Response code) - 這4個位元位在應答報文中設定,代表的含義如下:0
: 沒有錯誤。1
: 報文格式錯誤(Format error) - 伺服器不能理解請求的報文;2
: 伺服器失敗(Server failure) - 因為伺服器的原因導致沒辦法處理這個請求;3
: 名字錯誤(Name Error) - 只有對授權域名解析伺服器有意義,指出解析的域名不存在;4
: 沒有實現(Not Implemented) - 域名伺服器不支援查詢型別;5
: 拒絕(Refused) - 伺服器由於設定的策略拒絕給出應答.比如,伺服器不希望對某些請求者給出應答,或者伺服器不希望進行某些操作(比如區域傳送zone transfer);[6,15]
: 保留值,暫未使用。
QDCOUNT
: 無符號16bit
整數表示報文請求段中的問題記錄數。ANCOUNT
: 無符號16bit
整數表示報文回答段中的回答記錄數。NSCOUNT
: 無符號16bit
整數表示報文授權段中的授權記錄數。ARCOUNT
: 無符號16bit
整數表示報文附加段中的附加記錄數。
Header format
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ID |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR| opcode |AA|TC|RD|RA| Z | RCODE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QDCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ANCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| NSCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ARCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
複製程式碼
Question 查詢欄位
QNAME
無符號8bit
為單位長度不限表示查詢名(廣泛的說就是:域名).QTYPE
無符號16bit
整數表示查詢的協議型別.QCLASS
無符號16bit
整數表示查詢的類,比如,IN
代表Internet.
Question format
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ... |
| QNAME |
| ... |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QTYPE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QCLASS |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
複製程式碼
Answer/Authority/Additional
這3個欄位的格式都是一樣的。
NAME
資源記錄包含的域名.TYPE
表示DNS
協議的型別.CLASS
表示RDA他的類.TTL
4位元組無符號整數表示資源記錄可以快取的時間。0代表只能被傳輸,但是不能被快取。RDLENGTH
2個位元組無符號整數表示RDA他的長度RDATA
不定長字串來表示記錄,格式根TYPE和CLASS有關。比如,TYPE是A,CLASS 是 IN,那麼RDATA就是一個4個位元組的ARPA網路地址。
Answer/Authority/Additional format
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| NAME |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| TYPE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| CLASS |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| TTL |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| RDLENGTH |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| RDATA |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
複製程式碼
DNS請求報文解析
光說不做假把式。那如何對DNS請求報文進行解析呢。 先來看一下一個DNS請求報文:
6dca 0100 0001 0000 0000 0000 0377 7777
0561 7070 6c65 0363 6f6d 0000 0100 01
複製程式碼
這是一個Buffer
例項,看完後是不是一臉懵B,別緊張,先看解析後console.log
大概的樣子,是不是世界瞬間變美好了。
下面是一個請求查詢www.apple.com
網站ip的DNS請求報文。
//Header
ID: <Buffer 6d ca>
FLAG: QR: 0 opcode: 0 AA: 0 TC: 0 RD: 1
RA: 0 zero: 0 recode: 0
QDCOUNT: <Buffer 00 01> ANCOUNT: <Buffer 00 00> NSCOUNT: <Buffer 00 00> ARCOUNT: <Buffer 00 00>
//QUESTION
QNAME: <Buffer 03 77 77 77 05 61 70 70 6c 65 03 63 6f 6d 00> QTYPE: <Buffer 00 01> QCLASS: <Buffer 00 01>
QUESTION STRING: www.apple.com
複製程式碼
請求報文解析分為2個小塊:
Header
報文頭解析QUESTION
查詢問題解析
Header 報文頭解析
對Header部分進行解析。
先確定一下每個欄位的大小:
ID: 2 位元組
QR: 1 bit
opcode: 4bit
AA: 1bit
TC: 1bit
RD: 1bit
RA: 1bit
Z : 3bit
RCODE: 4bit
QDCOUNT: 2 位元組
ANCOUNT: 2 位元組
NSCOUNT: 2 位元組
ARCOUNT: 2 位元組
複製程式碼
共12個位元組。
假如我們拋開第[3,4]
個位元組,其實很容易就可以把header解析,但是單位為bit
的就需要對buffer
例項的值進行位運算操作了。
所以以下引數的值可以直接從buffer
中獲取:
var header = {};
header.id = buf.slice(0,2);
header.qdcount = buf.slice(4,6);
header.ancount = buf.slice(6,8);
header.nscount = buf.slice(8,10);
header.arcount = buf.slice(10, 12);
複製程式碼
難點就是如何獲取第[3,4]
的值,首先需要把buffer
例項對應的位元組轉成2
進位制字串然後轉換為數值,然後按引數的長度計算最後的結果。
第一步,將buffer
轉換為2進位制字串然後轉換為數值(假設dns報文是buf
):
//對第3個位元組轉成`2`進位制字串然後轉換為數值
var b = buf.slice(2,3).toString('binary', 0, 1).charCodeAt(0);
複製程式碼
第2步,進行資料切割:
首先需要理解下面這個函式,功能無非就是提取從offset
開始,長度為length
數字位,通過位運算轉換為Integer
型別的數然後返回。
說直白一點,就是把你需要的那一段2進位制資料轉換為Integer
型別,並返回。
var bitSlice = function(b, offset, length) {
return (b >>> (7-(offset+length-1))) & ~(0xff << length);
};
複製程式碼
注意這裡因為只考慮一個位元組 ===
8bit
,所以可以寫成(7-(offset+length-1))
和0xff << length
。假如不是一個位元組,那麼可能需要改變一下里面的數字7
和0xff
的值。
demo走起:
'use strict';
var buf = Buffer.from([0x2d]);
var b = buf.toString('binary' , 0,1).charCodeAt(0);
console.log(bitSlice(b , 0, 1));//0
console.log(bitSlice(b , 1, 1));//0
console.log(bitSlice(b , 2, 1));//1
console.log(bitSlice(b , 3, 1));//0
console.log(bitSlice(b , 4, 1));//1
console.log(bitSlice(b , 5, 1));//1
console.log(bitSlice(b , 6, 1));//0
console.log(bitSlice(b , 7, 1));//1
console.log(bitSlice(b , 5, 3));//5 === 0000 0101
/**
* 16進位制:0x2d
* 10進位制:45
* 2進位制: 0010 1101
*
* (45,0,1):45>>>7 & ~(0xff<<1)
* 45>>>7 = 0000 0000
* (0xff<<1) = 0000 0000 0000 0000 0000 0001 1111 1110 510
* ~(0xff<<1) = 1111 1111 1111 1111 1111 1110 0000 0001 -511 = -((0xff<<1)+1)
*
* 0000 0000 0000 0000 0000 0000 0000 0000 === 45>>>7
* & 1111 1111 1111 1111 1111 1110 0000 0001 === ~(0xff<<1)
* ----------------------------------------
* 0000 0000 0000 0000 0000 0000 0000 0000 = 0
*
* (45,2,1):45>>>5 & ~(0xff<<1)
* 45>>>5 = 0000 0001
* (0xff<<1) = 0000 0000 0000 0000 0000 0001 1111 1110 510
* ~(0xff<<1) = 1111 1111 1111 1111 1111 1110 0000 0001 -511 = -((0xff<<1)+1)
*
* 0000 0000 0000 0000 0000 0000 0000 0001 === 45>>>5
* & 1111 1111 1111 1111 1111 1110 0000 0001 === ~(0xff<<1)
* ----------------------------------------
* 0000 0000 0000 0000 0000 0000 0000 0001 = 1
*/
複製程式碼
理解了上面的函式的作用之後就可以真正的使用這個函式取DNS報文Header的第[3,4]
位元組中的值。
信手拈來:
//第3個位元組
var b = buf.slice(2,3).toString('binary', 0, 1).charCodeAt(0);
header.qr = bitSlice(b,0,1);
header.opcode = bitSlice(b,1,4);
header.aa = bitSlice(b,5,1);
header.tc = bitSlice(b,6,1);
header.rd = bitSlice(b,7,1);
//第4個位元組
b = buf.slice(3,4).toString('binary', 0, 1).charCodeAt(0);
header.ra = bitSlice(b,0,1);
header.z = bitSlice(b,1,3);
header.rcode = bitSlice(b,4,4);
複製程式碼
QUESTION 查詢欄位解析
主要包括了查詢域名,協議型別及類別。
這3個引數QTYPE
和QCLASS
是固定2
位元組,QNAME
是不固定的。
所以取資料的時候需要注意,因為QUESTION
資訊是跟隨在Header
之後,所以要從第12
個位元組往後取:
var question = {};
question.qname = buf.slice(12, buf.length-4);
question.qtype = buf.slice(buf.length-4, buf.length-2);
question.qclass = buf.slice(buf.length-2, buf.length);
複製程式碼
qname
使用的是len+data
混合編碼,以0x00
結尾。每個字串都以長度開始,然後後面接內容。qname
長度必須以8
位元組為單位。
例如www.apple.com
(注意:中間的.
是解析的時候自己新增上去的),它的buffer
例項表示為:
03 77 77 77 05 61 70 70 6c 65 03 63 6f 6d 00
//約等於
3www5apple3com
複製程式碼
也就是第一位表示的是長度,後面跟隨相同長度的資料,依此類推。
var domainify = function(qname) {
var parts = [];
for (var i = 0; i < qname.length && qname[i];) {
var len = qname[i] , offset = i+1;//獲取每一塊域名長度
parts.push(qname.slice(offset,offset+len).toString());//獲取每一塊域名
i = offset+len;
}
return parts.join('.');//拼湊成完整域名
};
複製程式碼
qtype
協議型別. 檢視詳情
協議型別對應的列表:
值 | 協議型別 | 描述 |
---|---|---|
1 | A | IPv4地址 |
2 | NS | 名字伺服器 |
5 | CNAME | 規範名稱定義主機的正式名字的別名 |
6 | SOA | 開始授權標記一個區的開始 |
11 | WKS | 熟知服務定義主機提供的網路服務 |
12 | PTR | 指標把IP地址轉化為域名 |
13 | HINFO | 主機資訊給出主機使用的硬體和作業系統的表述 |
15 | MX | 郵件交換把郵件改變路由送到郵件伺服器 |
28 | AAAA | IPv6地址 |
252 | AXFR | 傳送整個區的請求 |
255 | ANY | 對所有記錄的請求 |
qclass
通常為1,指Internet資料.
應用場景--dns請求代理
將以下程式碼儲存為.js
檔案,然後使用Node.js
執行,使用相同區域網內的機器配置DNS到這臺機器即可。
以下程式碼僅供參考:
'use strict';
const dgram = require('dgram');
const dns = require('dns');
const fs = require('fs');
const server = dgram.createSocket('udp4');
var bitSlice = function(b, offset, length) {
return (b >>> (7-(offset+length-1))) & ~(0xff << length);
};
var domainify = function(qname) {
var parts = [];
for (var i = 0; i < qname.length && qname[i];) {
var length = qname[i];
var offset = i+1;
parts.push(qname.slice(offset,offset+length).toString());
i = offset+length;
}
return parts.join('.');
};
var parse = function(buf) {
var header = {};
var question = {};
var b = buf.slice(2,3).toString('binary', 0, 1).charCodeAt(0);
console.log('b:',b,buf.slice(2,3));
header.id = buf.slice(0,2);
header.qr = bitSlice(b,0,1);
header.opcode = bitSlice(b,1,4);
header.aa = bitSlice(b,5,1);
header.tc = bitSlice(b,6,1);
header.rd = bitSlice(b,7,1);
b = buf.slice(3,4).toString('binary', 0, 1).charCodeAt(0);
header.ra = bitSlice(b,0,1);
header.z = bitSlice(b,1,3);
header.rcode = bitSlice(b,4,4);
header.qdcount = buf.slice(4,6);
header.ancount = buf.slice(6,8);
header.nscount = buf.slice(8,10);
header.arcount = buf.slice(10, 12);
question.qname = buf.slice(12, buf.length-4);
question.qtype = buf.slice(buf.length-4, buf.length-2);
question.qclass = buf.slice(buf.length-2, buf.length);
return {header:header, question:question};
};
server.on('error' , (err)=>{
console.log(`server error: ${err.stack}`);
});
server.on('message' , (msg , rinfo)=>{
//fs.writeFile('dns.json' ,msg, {flag:'w',endcoding:'utf-8'} ,(err)=>{
// console.log(err);
//});
var query = parse(msg);
console.log('標識ID: ' ,query.header.id);
console.log('標識FLAG: ' , 'QR: ',query.header.qr , 'opcode: ',query.header.opcode , 'AA: ',query.header.aa , 'TC: ',query.header.tc,'RD: ',query.header.rd);
console.log('RA: ',query.header.ra , 'zero: ',query.header.z , 'recode: ',query.header.rcode);
console.log('QDCOUNT: ',query.header.qdcount , 'ANCOUNT: ' , query.header.ancount, 'NSCOUNT: ' , query.header.nscount,'ARCOUNT: ',query.header.arcount);
console.log('QNAME: ',query.question.qname , 'QTYPE: ', query.question.qtype ,'QCLASS: ' , query.question.qclass);
console.log('QUESTION STRING: ' ,domainify(query.question.qname));
server.close();
});
server.on('listening' , ()=>{
var address = server.address();
console.log(`server listening ${address.address}:${address.port}`);
});
server.bind({port:53,address:'8.8.8.8'});//address需要指定到你要用於進行代理的機器ip
複製程式碼