DNS 請求報文詳解

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

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。假如不是一個位元組,那麼可能需要改變一下里面的數字70xff的值。

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個引數QTYPEQCLASS是固定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

複製程式碼

參考資料

docstore.mik.ua/orelly/netw…

www.comptechdoc.org/independent…

www.iprotocolsec.com/2012/01/13/…

相關文章