乐趣区

关于java:手把手教你在netty中使用TCP协议请求DNS服务器

简介

DNS 的全称 domain name system,既然是一个零碎就有客户端和服务器之分。个别状况来说咱们并不需要感知这个 DNS 客户端的存在,因为咱们在浏览器拜访某个域名的时候,浏览器作为客户端曾经实现了这个工作。

然而有时候咱们没有应用浏览器,比方在 netty 环境中,如何构建一个 DNS 申请呢?

DNS 传输协定简介

在 RFC 的标准中,DNS 传输协定有很多种, 如下所示:

  • DNS-over-UDP/53 简称 ”Do53″, 是应用 UDP 进行 DNS 查问传输的协定。
  • DNS-over-TCP/53 简称 ”Do53/TCP”, 是应用 TCP 进行 DNS 查问传输的协定。
  • DNSCrypt, 对 DNS 传输协定进行加密的办法。
  • DNS-over-TLS 简称 ”DoT”, 应用 TLS 进行 DNS 协定传输。
  • DNS-over-HTTPS 简称 ”DoH”, 应用 HTTPS 进行 DNS 协定传输。
  • DNS-over-TOR, 应用 VPN 或者 tunnels 连贯 DNS。

这些协定都有对应的实现形式,咱们先来看下 Do53/TCP,也就是应用 TCP 进行 DNS 协定传输。

DNS 的 IP 地址

先来考虑一下如何在 netty 中应用 Do53/TCP 协定,进行 DNS 查问。

因为 DNS 是客户端和服务器的模式,咱们须要做的是构建一个 DNS 客户端,向已知的 DNS 服务器端进行查问。

已知的 DNS 服务器地址有哪些呢?

除了 13 个 root DNS IP 地址以外,还呈现了很多收费的公共 DNS 服务器地址, 比方咱们罕用的阿里 DNS, 同时提供了 IPv4/IPv6 DNS 和 DoT/DoH 服务。

IPv4: 
223.5.5.5

223.6.6.6

IPv6: 
2400:3200::1

2400:3200:baba::1

DoH 地址: 
https://dns.alidns.com/dns-query

DoT 地址: 
dns.alidns.com

再比方百度 DNS,提供了一组 IPv4 和 IPv6 的地址:

IPv4: 
180.76.76.76

IPv6: 
2400:da00::6666

还有 114DNS:

114.114.114.114
114.114.115.115

当然还有很多其余的公共收费 DNS,这里我抉择应用阿里的 IPv4:223.5.5.5 为例。

有了 IP 地址,咱们还须要指定 netty 的连贯端口号,这里默认的是 53。

而后就是咱们要查问的域名了,这里以 www.flydean.com 为例。

你也能够应用你零碎中配置的 DNS 解析地址,以 mac 为例,能够通过 nslookup 进行查看本地的 DNS 地址:

nslookup  www.flydean.com
Server:        8.8.8.8
Address:    8.8.8.8#53

Non-authoritative answer:
www.flydean.com    canonical name = flydean.com.
Name:    flydean.com
Address: 47.107.98.187

Do53/TCP 在 netty 中的应用

有了 DNS Server 的 IP 地址,接下来咱们须要做的就是搭建 netty client,而后向 DNS server 端发送 DNS 查问音讯。

搭建 DNS netty client

因为咱们进行的是 TCP 连贯,所以能够借助于 netty 中的 NIO 操作来实现,也就是说咱们须要应用 NioEventLoopGroup 和 NioSocketChannel 来搭建 netty 客户端:

 final String dnsServer = "223.5.5.5";
        final int dnsPort = 53;

EventLoopGroup group = new NioEventLoopGroup();
            Bootstrap b = new Bootstrap();
            b.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new Do53ChannelInitializer());

            final Channel ch = b.connect(dnsServer, dnsPort).sync().channel();

netty 中的 NIO Socket 底层应用的就是 TCP 协定,所以咱们只须要像罕用的 netty 客户端服务一样构建客户端即可。

而后调用 Bootstrap 的 connect 办法连贯到 DNS 服务器,就建设好了 channel 连贯。

这里咱们在 handler 中传入了自定义的 Do53ChannelInitializer,咱们晓得 handler 的作用是对音讯进行编码、解码和对音讯进行读取。因为目前咱们并不知道客户端查问的音讯格局,所以 Do53ChannelInitializer 的实现咱们在前面再进行具体解说。

发送 DNS 查问音讯

netty 提供了 DNS 音讯的封装,所有的 DNS 音讯,包含查问和响应都是 DnsMessage 的子类。

每个 DnsMessage 都有一个惟一标记的 ID,还有代表这个 message 类型的 DnsOpCode。

对于 DNS 来说,opCode 有上面这几种:

    public static final DnsOpCode QUERY = new DnsOpCode(0, "QUERY");
    public static final DnsOpCode IQUERY = new DnsOpCode(1, "IQUERY");
    public static final DnsOpCode STATUS = new DnsOpCode(2, "STATUS");
    public static final DnsOpCode NOTIFY = new DnsOpCode(4, "NOTIFY");
    public static final DnsOpCode UPDATE = new DnsOpCode(5, "UPDATE");

因为每个 DnsMessage 都可能蕴含 4 个 sections, 每个 section 都以 DnsSection 来示意。因为有 4 个 section,所以在 DnsSection 定义了 4 个 section 类型:

    QUESTION,
    ANSWER,
    AUTHORITY,
    ADDITIONAL;

每个 section 外面又蕴含了多个 DnsRecord, DnsRecord 代表的就是 Resource record, 简称为 RR,RR 中有一个 CLASS 字段,上面是 DnsRecord 中 CLASS 字段的定义:

    int CLASS_IN = 1;
    int CLASS_CSNET = 2;
    int CLASS_CHAOS = 3;
    int CLASS_HESIOD = 4;
    int CLASS_NONE = 254;
    int CLASS_ANY = 255;

DnsMessage 是 DNS 音讯的对立示意,对于查问来说,netty 中提供了一个专门的查问类叫做 DefaultDnsQuery。

先来看下 DefaultDnsQuery 的定义和构造函数:

public class DefaultDnsQuery extends AbstractDnsMessage implements DnsQuery {public DefaultDnsQuery(int id) {super(id);
    }

    public DefaultDnsQuery(int id, DnsOpCode opCode) {super(id, opCode);
    }

DefaultDnsQuery 的构造函数须要传入 id 和 opCode。

咱们能够这样定义一个 DNS 查问:

int randomID = (int) (System.currentTimeMillis() / 1000);
            DnsQuery query = new DefaultDnsQuery(randomID, DnsOpCode.QUERY)

既然是 QEURY, 那么还须要设置 4 个 sections 中的查问 section:

query.setRecord(DnsSection.QUESTION, new DefaultDnsQuestion(queryDomain, DnsRecordType.A));

这里调用的是 setRecord 办法向 section 中插入 RR 数据。

这里的 RR 数据应用的是 DefaultDnsQuestion。DefaultDnsQuestion 的构造函数有两个,一个是要查问的 domain name,这里就是 ”www.flydean.com”, 另外一个参数是 dns 记录的类型。

dns 记录的类型有很多种,在 netty 中有一个专门的类 DnsRecordType 示意,DnsRecordType 中定义了很多个类型,如下所示:

public class DnsRecordType implements Comparable<DnsRecordType> {public static final DnsRecordType A = new DnsRecordType(1, "A");
    public static final DnsRecordType NS = new DnsRecordType(2, "NS");
    public static final DnsRecordType CNAME = new DnsRecordType(5, "CNAME");
    public static final DnsRecordType SOA = new DnsRecordType(6, "SOA");
    public static final DnsRecordType PTR = new DnsRecordType(12, "PTR");
    public static final DnsRecordType MX = new DnsRecordType(15, "MX");
    public static final DnsRecordType TXT = new DnsRecordType(16, "TXT");
    ...

因为类型比拟多,咱们筛选几个罕用的进行解说。

  • A 类型,是 address 的缩写,用来指定主机名或者域名对应的 ip 地址.
  • NS 类型,是 name server 的缩写,是域名服务器记录,用来指定域名由哪个 DNS 服务器来进行解析。
  • MX 类型, 是 mail exchanger 的缩写,是一个邮件替换记录,用来依据邮箱的后缀来定位邮件服务器。
  • CNAME 类型,是 canonical name 的缩写,能够将多个名字映射到同一个主机.
  • TXT 类型,用来示意主机或者域名的阐明信息。

以上几个是咱们常常会用到的 dns record 类型。

这里咱们抉择应用 A,用来查问域名对应的主机 IP 地址。

构建好 query 之后,咱们就能够应用 netty client 发送 query 指令到 dns 服务器了,具体的代码如下:

            DnsQuery query = new DefaultDnsQuery(randomID, DnsOpCode.QUERY)
                    .setRecord(DnsSection.QUESTION, new DefaultDnsQuestion(queryDomain, DnsRecordType.A));
            ch.writeAndFlush(query).sync();

DNS 查问的音讯解决

DNS 的查问音讯咱们曾经发送进来了,接下来就是对音讯的解决和解析了。

还记得咱们自定义的 Do53ChannelInitializer 吗?看一下它的实现:

class Do53ChannelInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) {ChannelPipeline p = ch.pipeline();
        p.addLast(new TcpDnsQueryEncoder())
                .addLast(new TcpDnsResponseDecoder())
                .addLast(new Do53ChannelInboundHandler());
    }
}

咱们向 pipline 中增加了两个 netty 自带的编码解码器 TcpDnsQueryEncoder 和 TcpDnsResponseDecoder,还有一个自定义用来做音讯解析的 Do53ChannelInboundHandler。

因为咱们向 channel 中写入的是 DnsQuery, 所以须要一个 encoder 将 DnsQuery 编码为 ByteBuf, 这里应用的是 netty 提供的 TcpDnsQueryEncoder:

public final class TcpDnsQueryEncoder extends MessageToByteEncoder<DnsQuery> 

TcpDnsQueryEncoder 继承自 MessageToByteEncoder,示意将 DnsQuery 编码为 ByteBuf。

看下他的 encode 办法:

    protected void encode(ChannelHandlerContext ctx, DnsQuery msg, ByteBuf out) throws Exception {out.writerIndex(out.writerIndex() + 2);
        this.encoder.encode(msg, out);
        out.setShort(0, out.readableBytes() - 2);
    }

能够看到 TcpDnsQueryEncoder 在 msg 编码之前存储了 msg 的长度信息,所以是一个基于长度的对象编码器。

这里的 encoder 是一个 DnsQueryEncoder 对象。

看一下它的 encoder 办法:

    void encode(DnsQuery query, ByteBuf out) throws Exception {encodeHeader(query, out);
        this.encodeQuestions(query, out);
        this.encodeRecords(query, DnsSection.ADDITIONAL, out);
    }

DnsQueryEncoder 会顺次编码 header、questions 和 records。

实现编码之后,咱们还须要从 DNS server 的返回中 decode 出 DnsResponse,这里应用的是 netty 自带的 TcpDnsResponseDecoder:

public final class TcpDnsResponseDecoder extends LengthFieldBasedFrameDecoder

TcpDnsResponseDecoder 继承自 LengthFieldBasedFrameDecoder,示意数据是以字段长度来进行宰割的,这和咱们刚刚将的 encoder 的格局相似。

来看下他的 decode 办法:

    protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {ByteBuf frame = (ByteBuf)super.decode(ctx, in);
        if (frame == null) {return null;} else {
            DnsResponse var4;
            try {var4 = this.responseDecoder.decode(ctx.channel().remoteAddress(), ctx.channel().localAddress(), frame.slice());
            } finally {frame.release();
            }
            return var4;
        }
    }

decode 办法先调用 LengthFieldBasedFrameDecoder 的 decode 办法将要解码的内容提取进去,而后调用 responseDecoder 的 decode 办法,最终返回 DnsResponse。

这里的 responseDecoder 是一个 DnsResponseDecoder。具体 decoder 的细节这里就不过多论述了。感兴趣的同学能够自行查阅代码文档。

最初,咱们失去了 DnsResponse 对象。

接下来就是自定义的 InboundHandler 对音讯进行解析了:

class Do53ChannelInboundHandler extends SimpleChannelInboundHandler<DefaultDnsResponse> 

在它的 channelRead0 办法中,咱们调用了 readMsg 办法对音讯进行解决:

    private static void readMsg(DefaultDnsResponse msg) {if (msg.count(DnsSection.QUESTION) > 0) {DnsQuestion question = msg.recordAt(DnsSection.QUESTION, 0);
            log.info("question is :{}",question);
        }
        int i = 0, count = msg.count(DnsSection.ANSWER);
        while (i < count) {DnsRecord record = msg.recordAt(DnsSection.ANSWER, i);
            // A 记录用来指定主机名或者域名对应的 IP 地址
            if (record.type() == DnsRecordType.A) {DnsRawRecord raw = (DnsRawRecord) record;
                log.info("ip address is: {}",NetUtil.bytesToIpAddress(ByteBufUtil.getBytes(raw.content())));
            }
            i++;
        }
    }

DefaultDnsResponse 是 DnsResponse 的一个实现,首先判断 msg 中的 QUESTION 个数是否大于零。

如果大于零,则打印出 question 的信息。

而后再解析出 msg 中的 ANSWER 并打印进去。

最初,咱们可能失去这样的输入:

INFO  c.f.dnstcp.Do53ChannelInboundHandler - question is :DefaultDnsQuestion(www.flydean.com. IN A)
INFO  c.f.dnstcp.Do53ChannelInboundHandler - ip address is: 47.107.98.187

总结

以上就是应用 netty 创立 DNS client 进行 TCP 查问的解说。

本文的代码,大家能够参考:

learn-netty4

更多内容请参考 http://www.flydean.com/54-netty-dns-over-tcp/

最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不

欢送关注我的公众号:「程序那些事」, 懂技术,更懂你!

退出移动版