上一篇我已经解释了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 附加信息
1
2
3
4
5
6
7
8
9
10
11
12
13
DNS format

+--+--+--+--+--+--+--+
| Header |
+--+--+--+--+--+--+--+
| Question |
+--+--+--+--+--+--+--+
| Answer |
+--+--+--+--+--+--+--+
| Authority |
+--+--+--+--+--+--+--+
| Additional |
+--+--+--+--+--+--+--+

Header报文头

属性说明:

  • 客户端请求ID是为了保证收到DNS服务器返回报文时能正确知道是哪一个请求的响应报文。所以一个完整的DNS请求和响应,里面requestresponseID
    必须保持一致。
  • header.qr = 1,表示响应报文
  • header.ancount,这个牵涉到应答记录条目,所以要根据应答字段Answer计算。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 请求数据

将请求数据原样返回。

1
2
3
4
var question = response.question = {};
question.qname = request.question.qname;
question.qtype = request.question.qtype;
question.qclass = request.question.qclass;

Answer应答报文数据

这个部分的内容就是dns服务器要返回的数据报。

RDDATA为数据字段。

name为域名,长度不固定。

格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Answer format

0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| NAME |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| TYPE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| CLASS |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| TTL |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| RDLENGTH |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| RDATA |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
1
2
3
4
5
6
7
8
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必须经过转换客户端才能识别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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:

1
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的大小。

1
2
3
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:

1
header.id.copy(buf,0,0,2);

通过这种方式即可将其它参数进行一一转换。

需要按位拼接成一个字节的类别

这种主要数针对Header的第[3,4]个字节。应为这2个字节的数据是按位的长度区分,现在需要拼凑成完整字节。

首先需要确定的是字节长度,以及默认值,然后确定位操作符。

1byte = 8bit

默认值为:0 = 0x00

操作符:

1
2
&: 不行,因为任何数&0 == 0
|: ok ,任何数 | 0 都等于这个数

通过|可以得到想要的结果:

1
2
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位。

1
2
buf.writeUInt16BE(header.ancount, 6);
buf.writeUInt32BE(answer.rdata, len-4);

应用场景

除了Answer数据的ttl报文有效跳数和rdata,需要真的从其它地方获取过来。其它数据基本可以通过计算或从request中得到。

封装成函数的话,只需要传入(request,ttl,rdata)就可以了。

以下代码仅供参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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;
};

上一篇: DNS 请求报文详解

参考资料

https://github.com/mafintosh/dnsjack