上一篇我已經解釋了DNS請求報文怎麼解析,不會的自己坐飛機(飛機入口)。這一篇主要從DNS伺服器的角度來解釋,如何自己建立響應報文返回給客戶端。
就這個命題,可以羅列出DNS伺服器在建立response
響應報文時需要解決的問題。
- dns資料包型別
Buffer
? - Node.js中Buffer如何建立?
- 正常情況我們操作的字串和數字等是否可以轉換為
Buffer
? Buffer
是否可以建立response
響應報文指定型別的引數值?response
響應報文與request
請求報文的異同?
說到這,你是不是已經察覺到。既然dns
請求和dns
響應都做了,那是不是自己動手寫一個dns代理伺服器也可以信手拈來呢。
答案是: Yes
。
那然我們繼續完成這最後一步,response
響應報文的建立。
DNS響應報文格式
response
響應報文和request
請求報文格式相同。不同的地方是引數的值不同。
response引數詳解
Header
報文頭Question
查詢的問題Answer
應答Authority
授權應答Additional
附加資訊
DNS format
+--+--+--+--+--+--+--+
| Header |
+--+--+--+--+--+--+--+
| Question |
+--+--+--+--+--+--+--+
| Answer |
+--+--+--+--+--+--+--+
| Authority |
+--+--+--+--+--+--+--+
| Additional |
+--+--+--+--+--+--+--+
複製程式碼
Header報文頭
屬性說明:
- 客戶端請求ID是為了保證收到DNS伺服器返回報文時能正確知道是哪一個請求的響應報文。所以一個完整的DNS請求和響應,裡面
request
和response
的ID
必須保持一致。 header.qr = 1
,表示響應報文header.ancount
,這個牽涉到應答記錄條目,所以要根據應答欄位Answer
計算。
var response = {};
var header = response.header = {};
header.id = request.header.id;//id相同,視為一個dns請求
header.qr = 1; //響應報文
header.opcode = 0;//標準查詢
header.rd = 1;
header.ra = 0;
header.z = 0;
header.rcode = 0;//沒有錯誤
header.qdcount = 1;
header.nscount = 0;
header.arcount = 0;
header.ancount = 1;//這裡answer為一個,所以設定為1.如果有多個answer那麼就要考慮多個answer
複製程式碼
Question 請求資料
將請求資料原樣返回。
var question = response.question = {};
question.qname = request.question.qname;
question.qtype = request.question.qtype;
question.qclass = request.question.qclass;
複製程式碼
Answer應答報文資料
這個部分的內容就是dns伺服器要返回的資料包。
RDDATA
為資料欄位。
name
為域名,長度不固定。
格式:
Answer format
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| NAME |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| TYPE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| CLASS |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| TTL |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| RDLENGTH |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| RDATA |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
複製程式碼
var answer = {};
answer.name = request.question.qname;
answer.type = 1;
answer.class = 1;
answer.ttl = ttl || 1;//報文有效跳數
answer.rdlength = 4;
answer.rdata = rdata;//資料記錄
複製程式碼
rdata
存放的是ip
地址,ip必須經過轉換客戶端才能識別:
var numify = function(ip) {
ip = ip.split('.').map(function(n) {
return parseInt(n, 10);
});
var result = 0;
var base = 1;
for (var i = ip.length-1; i >= 0; i--) {
result += ip[i]*base;
base *= 256;
}
return result;
};
複製程式碼
rdata
是4
位元組,ip
地址從.
處切開後是由4段數字組成,每段資料不會超過2^8 === 256
---一個位元組(8bit
),那rdata
的4個位元組剛好可以存放下一個ip
地址。
那現在的問題是怎麼把ip地址資料存進4個位元組裡面,而又要保證客戶端能夠識別。很簡單按位元組存,按位元組取就行了。4
位元組剛好是一個32bit
整數的長度。
所以上面計算result
的for(...)
迴圈就是把ip存進rdata
的一種方式。
其實你也可以使用以下方式計算result
:
result = ip[0]*(1<<24) + ip[1]*(1<<16) + ip[2]*(1<<8) + ip[3];
複製程式碼
Authority/Additional 資料
自己處理的請求沒有授權應答和附加資料。
Buffer型別響應報文
得到了想要的一切響應資料之後,下一步就是將這些資料轉換為客戶端可以解析的Buffer
型別。
那這一步的工作正好與request
請求報文解析的工作恰好相反。報上面的資料一一拼湊為response
響應報文格式資料。
Buffer長度確定
返回一段Buffer
報文,總得先建立一定長度的Buffer
。
根據欄位分析,除了Question.qname
欄位和Answer.name
欄位是長度不固定的,其它的欄位都是可以計算出來。
通過帶入資料可以得到需要建立的Buffer
的大小。
len = Header + Question + Answer
= 12 + (Question.qname.length+4) + (Answer.name.length + 14)
= 30 + Question.qname.length + Answer.name.length
複製程式碼
確定需要建立的Buffer
例項的長度為30 + Question.qname.length + Answer.name.length
後,就可以進行引數轉換了。
Buffer例項引數轉換
response
資料大概分為了3中類別:
- 普通完整位元組類別
- 需要按位拼接成一個位元組的類別
- 無符號整數類別
普通完整位元組類別
這種往往是最好處理的了,直接copy
過來就可以了。
使用buf.copy(target[, targetStart[, sourceStart[, sourceEnd]]])
函式進行拷貝.
例如拷貝header.id
:
header.id.copy(buf,0,0,2);
複製程式碼
通過這種方式即可將其它引數進行一一轉換。
需要按位拼接成一個位元組的類別
這種主要數針對Header
的第[3,4]
個位元組。應為這2個位元組的資料是按位的長度區分,現在需要拼湊成完整位元組。
首先需要確定的是位元組長度,以及預設值,然後確定位操作符。
1byte = 8bit
預設值為:0 = 0x00
操作符:
&: 不行,因為任何數&0 == 0
|: ok ,任何數 | 0 都等於這個數
複製程式碼
通過|
可以得到想要的結果:
buf[2] = 0x00 | header.qr << 7 | header.opcode << 3 | header.aa << 2 | header.tc << 1 | header.rd;
buf[3] = 0x00 | header.ra << 7 | header.z << 4 | header.rcode;
複製程式碼
無符號整數類別
假如你看過Buffer
的api或使用Buffer
建立過buf
無符號整數,那麼這個問題就可以很容易解決了。
buf.writeUInt16BE(value, offset[, noAssert])
和buf.writeUInt32BE(value, offset[, noAssert])
,一看就知道一個是建立16
位,一個是32
位。
buf.writeUInt16BE(header.ancount, 6);
buf.writeUInt32BE(answer.rdata, len-4);
複製程式碼
應用場景
除了Answer
資料的ttl
報文有效跳數和rdata
,需要真的從其它地方獲取過來。其它資料基本可以通過計算或從request
中得到。
封裝成函式的話,只需要傳入(request,ttl,rdata)
就可以了。
以下程式碼僅供參考:
var responseBuffer = function(response){
var buf = Buffer.alloc(30+response.question.qname.length +response.answer.name.length) ,
offset = response.question.qname.length;
response.header.id.copy(buf,0,0,2);
buf[2] = 0x00 | response.header.qr << 7 | response.header.opcode << 3 | response.header.aa << 2 | response.header.tc << 1 | response.header.rd;
buf[3] = 0x00 | response.header.ra << 7 | response.header.z << 4 | response.header.rcode;
buf.writeUInt16BE(response.header.qdcount, 4);
buf.writeUInt16BE(response.header.ancount, 6);
buf.writeUInt16BE(response.header.nscount, 8);
buf.writeUInt16BE(response.header.arcount, 10);
response.question.qname.copy(buf,12);
response.question.qtype.copy(buf,12+offset,0,2);
response.question.qclass.copy(buf,14+offset,0,2);
offset += 16;
response.answer.name.copy(buf,offset);
offset += response.answer.name.length;
buf.writeUInt16BE(response.answer.type , offset);
buf.writeUInt16BE(response.answer.class , offset+2);
buf.writeUInt32BE(response.answer.ttl , offset+4);
buf.writeUInt16BE(response.answer.rdlength , offset+8);
buf.writeUInt32BE(response.answer.rdata , offset+10);
return buf;
};
var response = function(request , ttl , rdata){
var response = {};
response.header = {};
response.question = {};
response.answer = resolve(request.question.qname , ttl , rdata);
response.header.id = request.header.id;
response.header.qr = 1;
response.header.opcode = 0;
response.header.aa = 0;
response.header.tc = 0;
response.header.rd = 1;
response.header.ra = 0;
response.header.z = 0;
response.header.rcode = 0;
response.header.qdcount = 1;
response.header.ancount = 1;
response.header.nscount = 0;
response.header.arcount = 0;
response.question.qname = request.question.qname;
response.question.qtype = request.question.qtype;
response.question.qclass = request.question.qclass;
return responseBuffer(response);
};
var resolve = function(qname , ttl , rdata){
var answer = {};
answer.name = qname;
answer.type = 1;
answer.class = 1;
answer.ttl = ttl;
answer.rdlength = 4;
answer.rdata = rdata;
return answer;
};
複製程式碼