DNS 響應報文詳解

富途web開發團隊發表於2018-03-25

上一篇我已經解釋了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請求和響應,裡面requestresponseID 必須保持一致。
  • 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;
  };
複製程式碼

rdata4位元組,ip地址從.處切開後是由4段數字組成,每段資料不會超過2^8 === 256---一個位元組(8bit),那rdata的4個位元組剛好可以存放下一個ip地址。 那現在的問題是怎麼把ip地址資料存進4個位元組裡面,而又要保證客戶端能夠識別。很簡單按位元組存,按位元組取就行了。4位元組剛好是一個32bit整數的長度。

所以上面計算resultfor(...)迴圈就是把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;
  };
複製程式碼

參考資料

github.com/mafintosh/d…

相關文章