如何使用Netty中的DNS编解码器

536 阅读5分钟

上一篇文章讲解了DNS协议的基本原理。如果我们要实现一个自己的DNSServer又应该如何做呢?虽然DNS协议的编解码对于刚入门的同学不是很友好需要学习很多知识的前提下才能自己去解析DNS协议,但是我们可以用Netty提供的DNS编解码器来实现DNS请求处理。

1、Netty DNS Codec

这里我们使用两个核心的编解码器DatagramDnsQueryDecoder和DatagramDnsResponseEncoder来处理DNS记录的请求和响应,对于网络编程数据上行处理的过程叫解码,数据下行处理的过程叫编码。

下面我们用Netty来启动一个基于UDP协议53标准DNS端口的服务,看一下处理过程是不是很简单。其中DnsHandler为具体的DNS请求响应处理器。

NioEventLoopGroup group = new NioEventLoopGroup();
try {
    Bootstrap bootstrap = new Bootstrap();
    bootstrap.group(group).channel(NioDatagramChannel.class)
            .handler(new ChannelInitializer<NioDatagramChannel>() {
                @Override
                protected void initChannel(NioDatagramChannel nioDatagramChannel) throws Exception {
                    nioDatagramChannel.pipeline().addLast(new DatagramDnsQueryDecoder());
                    nioDatagramChannel.pipeline().addLast(new DatagramDnsResponseEncoder());
                    nioDatagramChannel.pipeline().addLast(new DnsHandler());
                }
            }).option(ChannelOption.SO_BROADCAST, true);

    ChannelFuture future = bootstrap.bind(53).syncUninterruptibly();
} catch (Exception e) {
    e.printStackTrace();
}

DnsHandler

public class DnsHandler extends SimpleChannelInboundHandler<DatagramDnsQuery> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, DatagramDnsQuery query) throws Exception {
            log.info("查询的域名:{},ID:{}",dnsQuestion.name(),query.id());
    }

}

问题1: 为何要使用SimpleChannelInboundHandler?

回答: 在Netty中其实你也可以不使用SimpleChannelInboundHandler,如果你使用了ChannelInboundHandlerAdapter也没关系并不是强制要求你必须使用SimpleChannelInboundHandler只要记住你使用完Buffer后释放掉这个Buffer即可让它回到内存池中即可,要不然你的Netty应用就会存在内存泄漏,通过观察RSS的变化是持续增长,这个增长的速度是由你的数据包大小来表现出的RSS增长快和慢。

问题2: 我知道Netty中的核心是pipiline那么我使用了SimpleChannelInboundHandler后续的Handler是还能使用这个Buffer

回答:当然不能因为因为你已经在SimpleChannelInboundHandler中自动释放了Buffer再使用就是属于非法操作,如果你需要使用多个Handler去处理那么你只要保证最后一个Handler释放掉这个Buffer即可,Netty框架使用本身就是灵活多变的根据你自己的喜好可以无限发挥它的优势。

我们看一下SimpleChannelInboundHandler 看到了吧channelRead是数据进入方法channelRead0才是你要实现的方法一定不能重写错了,在finally里面会依据标识来调用ReferenceCountUtil.release(msg)自动释放Buffer。

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    boolean release = true;
    try {
        if (acceptInboundMessage(msg)) {
            @SuppressWarnings("unchecked")
            I imsg = (I) msg;
            channelRead0(ctx, imsg);
        } else {
            release = false;
            ctx.fireChannelRead(msg);
        }
    } finally {
        if (autoRelease && release) {
            ReferenceCountUtil.release(msg);
        }
    }
}

2、Netty DNS响应处理

既然请求解析好了那么我们这时需要响应DNS记录了,首先记住2个点决定了DNS是否会应答成功.

  • 1. DNS是请求应答模式那么应答的ID要和请求的ID一致
  • 2. DnsRecordType 决定了DNS响应的类型一定要保证请求和响应的记录类型是一致的

下面我们就以响应A记录为参考内容其中A记录为IPV4记录我们将IPV4地址转成Bytes放到dnsRecord中即可

DefaultDnsQuestion dnsQuestion = query.recordAt(DnsSection.QUESTION);
DatagramDnsResponse response = new DatagramDnsResponse(query.recipient(), query.sender(), query.id());
response.setId(query.id());
response.addRecord(DnsSection.QUESTION, dnsQuestion);
ByteBuf buf=ctx.channel().alloc().buffer().writeBytes(dnsRecord);
DefaultDnsRawRecord queryAnswer = new DefaultDnsRawRecord(dnsQuestion.name(), DnsRecordType.A, 10, buf);
response.addRecord(DnsSection.ANSWER, queryAnswer);
ctx.writeAndFlush(response);

3、Netty DatagramDnsQueryDecoder 标注

public class DatagramDnsQueryDecoder extends MessageToMessageDecoder<DatagramPacket> {

    private final DnsRecordDecoder recordDecoder;

    /**
     * Creates a new decoder with {@linkplain DnsRecordDecoder#DEFAULT the default record decoder}.
     */
    public DatagramDnsQueryDecoder() {
        this(DnsRecordDecoder.DEFAULT);
    }

    /**
     * Creates a new decoder with the specified {@code recordDecoder}.
     */
    public DatagramDnsQueryDecoder(DnsRecordDecoder recordDecoder) {
        this.recordDecoder = checkNotNull(recordDecoder, "recordDecoder");
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, DatagramPacket packet, List<Object> out) throws Exception {
        final ByteBuf buf = packet.content();

        final DnsQuery query = newQuery(packet, buf);
        boolean success = false;
        try {
            final int questionCount = buf.readUnsignedShort();//DNS问题个数
            final int answerCount = buf.readUnsignedShort();//DNS应答个数
            final int authorityRecordCount = buf.readUnsignedShort();//授权记录个数
            final int additionalRecordCount = buf.readUnsignedShort();//额外记录个数

            //依次根据个数解析查询问题、应答、授权、额外记录
            decodeQuestions(query, buf, questionCount);
            decodeRecords(query, DnsSection.ANSWER, buf, answerCount);
            decodeRecords(query, DnsSection.AUTHORITY, buf, authorityRecordCount);
            decodeRecords(query, DnsSection.ADDITIONAL, buf, additionalRecordCount);

            out.add(query);
            success = true;
        } finally {
            if (!success) {
                query.release();
            }
        }
    }
   
    //QR(1位)+OPCODE(4位)+AA(1位)+TC(1位)+RD(1位)+RA(1位)+ZERO(3位)+RCODE(4位)
    //QR 查询应答标志,0表示这是查询报文,1表示这是应答报文
    //OPCODE 查询应答类型,0表示标准查询,1表示反向查询,2表示请求服务器状态。
    //AA 表示权威回答( authoritative answer ),意味着当前查询结果是由域名的权威服务器给出的,仅由应答报文使用.
    //TC 位表示截断( truncated ),使用 UDP 时,如果应答超过 512 字节,只返回前 512 个字节
    //RD 表示递归查询标志 ( recursion desired ),在请求中设置,并在应答中返回
    //RA 位表示可递归 ( recursion available ),如果服务器支持递归查询,就会在应答中设置该位,以告知客户端。仅由应答报文使用
    //ZERO 这三位未使用,固定为0
    //RCODE 表示返回码(reply code),用来返回应答状态,常用返回码:0表示无错误,2表示格式错误,3表示域名不存在。
    private static DnsQuery newQuery(DatagramPacket packet, ByteBuf buf) {
        final int id = buf.readUnsignedShort();//DNS请求ID

        final int flags = buf.readUnsignedShort();
        if (flags >> 15 == 1) {
            //Flag标识 0为查询报文,1为应答报文 这里是解析DNS请求处理不是响应所以抛出异常
            throw new CorruptedFrameException("not a query");
        }
        final DnsQuery query =
            new DatagramDnsQuery(
                packet.sender(),
                packet.recipient(),
                id,
                DnsOpCode.valueOf((byte) (flags >> 11 & 0xf)));
        
        query.setRecursionDesired((flags >> 8 & 1) == 1);
        query.setZ(flags >> 4 & 0x7);
        return query;
    }

    //根据问题个数解析问题
    private void decodeQuestions(DnsQuery query, ByteBuf buf, int questionCount) throws Exception {
        for (int i = questionCount; i > 0; i--) {
            query.addRecord(DnsSection.QUESTION, recordDecoder.decodeQuestion(buf));
        }
    }

    //根据记录个数解析
    private void decodeRecords(
        DnsQuery query, DnsSection section, ByteBuf buf, int count) throws Exception {
        for (int i = count; i > 0; i--) {
            final DnsRecord r = recordDecoder.decodeRecord(buf);
            if (r == null) {
                // Truncated response
                break;
            }

            query.addRecord(section, r);
        }
    }
}