这东西没什么好说的,对照协议写就完了。
这东西没什么好说的,对照协议写就完了。
talk is cheap,show your my code!
DNS包总体结构
参考RFC1035,我们可以知道一个DNS请求/响应由下面几部分构成。
+---------------------+
| Header |
+---------------------+
| Question | the question for the name server
+---------------------+
| Answer | RRs answering the question
+---------------------+
| Authority | RRs pointing toward an authority
+---------------------+
| Additional | RRs holding additional information
+---------------------+
其中,我们做的服务器非常简单,只考虑Header
,Qustion
和Answer
这三个部分。
Header数据段
Header
部分是一定有的,长度固定为12个字节;其余4部分可能有也可能没有,并且长度也不一定,这个在Header
部分中有指明。Header
的结构如下:
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ID |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR| Opcode |AA|TC|RD|RA| Z | RCODE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QDCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ANCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| NSCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ARCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
ID
:占16位。该值由发出DNS请求的程序生成,DNS服务器在响应时会使用该ID,这样便于请求程序区分不同的DNS响应。QR
:占1位。指示该消息是请求还是响应。0表示请求;1表示响应。OPCODE
:占4位。指示请求的类型,有请求发起者设定,响应消息中复用该值。0表示标准查询;1表示反转查询;2表示服务器状态查询。3~15目前保留,以备将来使用。AA
(Authoritative Answer,权威应答):占1位。表示响应的服务器是否是权威DNS服务器。只在响应消息中有效。TC
(TrunCation,截断):占1位。指示消息是否因为传输大小限制而被截断。RD
(Recursion Desired,期望递归):占1位。该值在请求消息中被设置,响应消息复用该值。如果被设置,表示希望服务器递归查询。但服务器不一定支持递归查询。RA
(Recursion Available,递归可用性):占1位。该值在响应消息中被设置或被清除,以表明服务器是否支持递归查询。Z
:占3位。保留备用。RCODE
(Response code):占4位。该值在响应消息中被设置。取值及含义如下:
0
:No error condition,没有错误条件;
1
:Format error,请求格式有误,服务器无法解析请求;
2
:Server failure,服务器出错。
3
:Name Error,只在权威DNS服务器的响应中有意义,表示请求中的域名不存在。
4
:Not Implemented,服务器不支持该请求类型。
5
:Refused,服务器拒绝执行请求操作。
6~15
:保留备用。QDCOUNT
:占16位(无符号)。指明Question部分的包含的实体数量。ANCOUNT
:占16位(无符号)。指明Answer部分的包含的RR(Resource Record)数量。NSCOUNT
:占16位(无符号)。指明Authority部分的包含的RR(Resource Record)数量。ARCOUNT
:占16位(无符号)。指明Additional部分的包含的RR(Resource Record)数量。
Question数据段
Question表示一个查询请求(不完全是,抓包发现服务器的响应也经常带有这个部分),数量由QDCOUNT
指定,格式如下所示:
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| |
/ QNAME /
/ /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QTYPE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QCLASS |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
QNAME
:字节数不定,以0x00作为结束符。表示查询的主机名。注意:众所周知,主机名被"."号分割成了多段标签。在QNAME中,每段标签前面加一个数字,表示接下来标签的长度。比如:api.sina.com.cn表示成QNAME时,会在"api"前面加上一个字节0x03,"sina"前面加上一个字节0x04,"com"前面加上一个字节0x03,而"cn"前面加上一个字节0x02;
QTYPE
:占2个字节。表示RR(Resource Record)类型;
QCLASS
:占2个字节。表示RR分类。
Answer数据段
Answer
表示服务器对Query
的响应,数量由ANCOUNT
指定,格式如下所示:
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| |
/ /
/ NAME /
| |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| TYPE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| CLASS |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| TTL |
| |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| RDLENGTH |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
/ RDATA /
/ /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
NAME
:长度不定,可能是真正的数据,也有可能是指针(其值表示的是真正的数据在整个数据中的字节索引数),还有可能是二者的混合(以指针结尾)。若是真正的数据,会以0x00结尾;若是指针,指针占2个字节,第一个字节的高2位为11。经过抓包发现,大部分服务都是将Question数据段原封不动返回,这里直接用指针实现,在这里,我们为了实现简单,也采用同样的方法。TYPE
:占2个字节。表示RR的类型,如A、CNAME、NS等,对于本次程序固定写1CLASS
:占2个字节。表示RR的分类,对于本次程序固定写1TTL
:占4个字节。表示RR生命周期,即RR缓存时长,单位是秒RDLENGTH
:占2个字节。指定RDATA字段的字节数RDATA
:即之前介绍的value,含义与TYPE有关,对于本次程序就是IP地址的int表示
基础bit位操作
我们知道,网络字节序是大端模式,所以到x86架构
的本地需要做一些转换。对此,我们需要一些工具类:
package com.example.demo.dns;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class BitUtils {
/**
* 从网络字节序的data中读取指定位(bit)
* @param data 网络字节序数组
* @param offset 偏移(bit)
* @param cnt 读取数量(bit)
* @return
*/
public static int readBits(byte[] data, int offset, int cnt){
int ans = 0 , end = offset+cnt;
for(int i = offset; i < end ;++i){
int remain = 7 - (i&7);
ans = (ans<<1) + ((data[i>>>3] & (1 << remain) ) >>> remain) ;
}
return ans;
}
/**
* 将指定数据以网络字节序写入到byte数组
* @param n 数据
* @param cnt 数量(bit) 必须是8的倍数且不能超过32
* @param bos byte数组
*/
public static void writeBits(int n, int cnt, ByteArrayOutputStream bos){
if((cnt&7) !=0 || cnt >32) throw new IllegalArgumentException("what hell you doing!");
int len = cnt>>>3;
byte[] buff = new byte[len];
for(int i = len - 1; i >= 0 ;--i){
buff[i] = (byte)(n & 511);
n = n >>>8;
}
try {
bos.write(buff);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 将指定整数按位反转
* @param n 指定整数
* @param cnt 多少位
* @return 结果
* <pre>
* 001 ====> 100
* reverseBit(1,3) => 4
* </pre>
*/
public static int reverseBit(int n,int cnt){
int half = cnt >>> 1;
for(int i = 0 ; i < half ; ++i){
int j = cnt - 1 - i;
int l = (n & (1 << i)) >>> i ;
int r = (n & (1 << j)) >>> j ;
if(l == 1) n |= 1 << j;
else n &= ~(1 << j);
if(r == 1) n |= 1 << i;
else n &= ~(1 << i);
}
return n;
}
}
Header数据段解析
对照报文格式,使用工具类,慢慢解析即可:
package com.example.demo.dns;
import java.io.ByteArrayOutputStream;
public class DNSHeader {
// ID:占16位。该值由发出DNS请求的程序生成,DNS服务器在响应时会使用该ID,这样便于请求程序区分不同的DNS响应。
int id;
// QR:占1位。指示该消息是请求还是响应。0表示请求;1表示响应。
int query;
// OPCODE:占4位。指示请求的类型,有请求发起者设定,响应消息中复用该值。0表示标准查询;1表示反转查询;2表示服务器状态查询。3~15目前保留,以备将来使用。
int opcode;
// AA(Authoritative Answer,权威应答):占1位。表示响应的服务器是否是权威DNS服务器。只在响应消息中有效。
int aa;
// TC(TrunCation,截断):占1位。指示消息是否因为传输大小限制而被截断。
int tc;
// RD(Recursion Desired,期望递归):占1位。该值在请求消息中被设置,响应消息复用该值。如果被设置,表示希望服务器递归查询。但服务器不一定支持递归查询。
int rd;
// RA(Recursion Available,递归可用性):占1位。该值在响应消息中被设置或被清除,以表明服务器是否支持递归查询。
int ra;
// Z:占3位。保留备用。
int z;
// RCODE(Response code):占4位。该值在响应消息中被设置。取值及含义如下:
// 0:No error condition,没有错误条件;
// 1:Format error,请求格式有误,服务器无法解析请求;
// 2:Server failure,服务器出错。
// 3:Name Error,只在权威DNS服务器的响应中有意义,表示请求中的域名不存在。
// 4:Not Implemented,服务器不支持该请求类型。
// 5:Refused,服务器拒绝执行请求操作。
// 6~15:保留备用。
int rCode;
// QDCOUNT:占16位(无符号)。指明Question部分的包含的实体数量。
int qdCount;
// ANCOUNT:占16位(无符号)。指明Answer部分的包含的RR(Resource Record)数量。
int anCount;
// NSCOUNT:占16位(无符号)。指明Authority部分的包含的RR(Resource Record)数量。
int nsCount;
// ARCOUNT:占16位(无符号)。指明Additional部分的包含的RR(Resource Record)数量。
int arCount;
public byte[] getBytes(){
ByteArrayOutputStream bos = new ByteArrayOutputStream();
BitUtils.writeBits(id,16,bos);
BitUtils.writeBits(BitUtils.reverseBit(query + (BitUtils.reverseBit(opcode,4) << 1) + (aa << 5) + (tc << 6) + (rd << 7) + (ra << 8)
+ (BitUtils.reverseBit(z,3) << 9) + (BitUtils.reverseBit(rCode,4) << 12),16),16,bos );
BitUtils.writeBits(qdCount,16,bos);
BitUtils.writeBits(anCount,16,bos);
BitUtils.writeBits(nsCount,16,bos);
BitUtils.writeBits(arCount,16,bos);
return bos.toByteArray();
}
public DNSHeader(byte[] data,int offset){
this.id = BitUtils.readBits(data,offset*8 + 0,16);
this.query = BitUtils.readBits(data,offset*8 + 16,1);
this.opcode = BitUtils.readBits(data,offset*8 + 17,4);
this.aa = BitUtils.readBits(data,offset*8 + 21,1);
this.tc = BitUtils.readBits(data,offset*8 + 22,1);
this.rd = BitUtils.readBits(data,offset*8 + 23,1);
this.ra = BitUtils.readBits(data,offset*8 + 24,1);
this.z = BitUtils.readBits(data,offset*8 + 25,3);
this.rCode = BitUtils.readBits(data,offset*8 + 28,4);
this.qdCount = BitUtils.readBits(data,offset*8 + 32,16);
this.anCount = BitUtils.readBits(data,offset*8 + 48,16);
this.nsCount = BitUtils.readBits(data,offset*8 + 64,16);
this.arCount = BitUtils.readBits(data,offset*8 + 80,16);
}
public DNSHeader(byte[] data){
this(data,0);
}
}
Question数据段解析
对于Question
数据段,前面我们说到,这个数据段后面会用于做Answer
的指针,所以我们需要额外记录下长度,便于处理:
package com.example.demo.dns;
public class DNSQuestion {
private String name ;
private int type;
private int clazz;
private int length = 0;
public DNSQuestion(byte[] data, int offset){
int len;
len = data[offset +length++];
while(len != 0){
if(name == null) name = "";
else name += ".";
for(int i = 0 ; i < len ; ++i){
name +=Character.toString((char) data[offset +length++]);
}
len = data[offset +length++];
}
this.type = BitUtils.readBits(data,offset*8+length*8,16);
this.clazz = BitUtils.readBits(data,offset*8+length*8+16,16);
length +=4;
}
}
Answer数据段解析
对于这个数据段,除了IP
,其他直接写死:
package com.example.demo.dns;
import java.io.ByteArrayOutputStream;
public class DNSAnswer {
// 这里用指针
int name;
int type;
int clazz;
int ttl;
int length;
int data;
public DNSAnswer(int ip){
this.name = (0xc0 << 8) + 0x0c;
this.type = 0x01;
this.clazz = 1;
this.ttl = 240;
this.length = 4;
this.data = ip;
}
public byte[] getBytes(){
ByteArrayOutputStream bos = new ByteArrayOutputStream();
BitUtils.writeBits(name,16,bos);
BitUtils.writeBits(type,16,bos);
BitUtils.writeBits(clazz,16,bos);
BitUtils.writeBits(ttl,32,bos);
BitUtils.writeBits(length,16,bos);
BitUtils.writeBits(data,32,bos);
return bos.toByteArray();
}
}
运行测试
对于所有的请求,我们都返回192.168.128.8
package com.example.demo.dns;
import java.io.ByteArrayOutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.HashMap;
public class DNSServer implements Runnable{
public static final int IP = (192<<24) + (168<<16) + (128<<8) + (8);
private HashMap<String,String> mapping;
public DNSServer(HashMap<String, String> mapping) {
this.mapping = mapping;
}
@Override
public void run() {
try(
DatagramSocket server = new DatagramSocket(53);
) {
while(true){
byte[] data = new byte[1024];
DatagramPacket packet = new DatagramPacket(data,data.length);
server.receive(packet);
System.out.println("===");
System.out.println(packet.getAddress().getHostAddress());
DNSHeader header = new DNSHeader(data);
System.out.println(header);
DNSQuestion question = new DNSQuestion(data, 12);
System.out.println(question);
DNSAnswer answer = new DNSAnswer(IP);
InetAddress address = packet.getAddress();
int port = packet.getPort();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
header.query = 1;
header.qdCount = 1;
header.anCount = 1;
bos.write(header.getBytes());
for(int i = 0 ; i < question.getLength() ; ++i) bos.write(data[12+i]);
bos.write(answer.getBytes());
byte[] bytes = bos.toByteArray();
DatagramPacket packet2 = new DatagramPacket(bytes, bytes.length, address, port);
server.send(packet2);
System.out.println("===");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
1 条评论
Web服务器获得客户端DNS配置信息 – Combination的博客 · 2021年8月2日 下午10:32
[…] 在这里,我的测试服务器IP地址是106.55.235.158,使用上次写的简易DNS服务器,并使该DNS服务器强制返回106.55.235.158(无论什么样的查询,都返回这个IP地址)。 […]
评论已关闭。