这东西没什么好说的,对照协议写就完了。
这东西没什么好说的,对照协议写就完了。
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
    +---------------------+

其中,我们做的服务器非常简单,只考虑HeaderQustionAnswer 这三个部分。

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等,对于本次程序固定写1
  • CLASS:占2个字节。表示RR的分类,对于本次程序固定写1
  • TTL:占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地址)。 […]

评论已关闭。