写点什么

netty 系列之: 在 netty 中使用 tls 协议请求 DNS 服务器

作者:程序那些事
  • 2022 年 6 月 24 日
  • 本文字数:3153 字

    阅读完需:约 10 分钟

netty系列之:在netty中使用tls协议请求DNS服务器

简介

在前面的文章中我们讲过了如何在 netty 中构造客户端分别使用 tcp 和 udp 协议向 DNS 服务器请求消息。在请求的过程中并没有进行消息的加密,所以这种请求是不安全的。


那么有同学会问了,就是请求解析一个域名的 IP 地址而已,还需要安全通讯吗?


事实上,不加密的 DNS 查询消息是很危险的,如果你在访问一个重要的网站时候,DNS 查询消息被监听或者篡改,有可能你收到的查询返回 IP 地址并不是真实的地址,而是被篡改之后的地址,从而打开了钓鱼网站或者其他恶意的网站,从而造成了不必要的损失。


所以 DNS 查询也是需要保证安全的。


幸运的是在 DNS 的传输协议中特意指定了一种加密的传输协议叫做 DNS-over-TLS,简称("DoT")。


那么在 netty 中可以使用 DoT 来进行 DNS 服务查询吗?一起来看看吧。

支持 DoT 的 DNS 服务器

因为 DNS 中有很多传输协议规范,但并不是每个 DNS 服务器都支持所有的规范,所以我们在使用 DoT 之前需要找到一个能够支持 DoT 协议的 DNS 服务器。


这里我还是选择使用阿里 DNS 服务器:


223.5.5.5
复制代码


之前使用 TCP 和 UDP 协议的时候查询的 DNS 端口是 53,如果换成了 DoT,那么端口就需要变成 853。

搭建支持 DoT 的 netty 客户端

DoT 的底层还是 TCP 协议,也就是说 TLS over TCP,所以我们需要使用 NioEventLoopGroup 和 NioSocketChannel 来搭建 netty 客户端,如下所示:


EventLoopGroup group = new NioEventLoopGroup();            Bootstrap b = new Bootstrap();            b.group(group)                    .channel(NioSocketChannel.class)                    .handler(new DotChannelInitializer(sslContext, dnsServer, dnsPort));            final Channel ch = b.connect(dnsServer, dnsPort).sync().channel();
复制代码


这里选择的是 NioEventLoopGroup 和 NioSocketChannel。然后向 Bootstrap 中传入自定义的 DotChannelInitializer 即可。


DotChannelInitializer 中包含了自定义的 handler 和 netty 自带的 handler。


我们来看下 DotChannelInitializer 的定义和他的构造函数:


class DotChannelInitializer extends ChannelInitializer<SocketChannel> {
public DotChannelInitializer(SslContext sslContext, String dnsServer, int dnsPort) { this.sslContext = sslContext; this.dnsServer = dnsServer; this.dnsPort = dnsPort; }
复制代码


DotChannelInitializer 需要三个参数分别是 sslContext,dnsServer 和 dnsPort。


这三个参数都是在 sslContext 中使用的:


    protected void initChannel(SocketChannel ch) {        ChannelPipeline p = ch.pipeline();        p.addLast(sslContext.newHandler(ch.alloc(), dnsServer, dnsPort))                .addLast(new TcpDnsQueryEncoder())                .addLast(new TcpDnsResponseDecoder())                .addLast(new DotChannelInboundHandler());    }
复制代码


SslContext 主要用来进行 TLS 配置,下面是 SslContext 的定义:


SslProvider provider =                    SslProvider.isAlpnSupported(SslProvider.OPENSSL)? SslProvider.OPENSSL : SslProvider.JDK;            final SslContext sslContext = SslContextBuilder.forClient()                    .sslProvider(provider)                    .protocols("TLSv1.3", "TLSv1.2")                    .build();
复制代码


因为 SslProvider 有很多种,可以选择 openssl,也可以选择 JDK 自带的。


这里我们使用的 openssl,要想提供 openssl 的支持,我们还需要提供 openssl 的依赖包如下:


        <dependency>            <groupId>io.netty</groupId>            <artifactId>netty-tcnative</artifactId>            <version>2.0.51.Final</version>        </dependency>        <dependency>            <groupId>io.netty</groupId>            <artifactId>netty-tcnative-boringssl-static</artifactId>            <version>2.0.51.Final</version>        </dependency>
复制代码


有了 provider 之后,就可以调用 SslContextBuilder.forClient 方法来创建 SslContext。


这里我们指定 SSL 的 protocol 是"TLSv1.3"和"TLSv1.2"。


然后再调用 sslContext 的 newHandler 方法就创建好了支持 ssl 的 handler:


sslContext.newHandler(ch.alloc(), dnsServer, dnsPort)
复制代码


newHandler 还需要指定 dnsServer 和 dnsPort 信息。


处理完 ssl,接下来就是对 dns 查询和响应的编码解码器,这里使用的是 TcpDnsQueryEncoder 和 TcpDnsResponseDecoder。


TcpDnsQueryEncoder 和 TcpDnsResponseDecoder 在之前介绍使用 netty 搭建 tcp 客户端的时候就已经详细解说过了,这里就不再进行讲解了。


编码解码之后,就是自定义的消息处理器 DotChannelInboundHandler:


class DotChannelInboundHandler extends SimpleChannelInboundHandler<DefaultDnsResponse> 
复制代码


DotChannelInboundHandler 中定义了消息的具体处理方法:


    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);            if (record.type() == DnsRecordType.A) {                //A记录用来指定主机名或者域名对应的IP地址                DnsRawRecord raw = (DnsRawRecord) record;                log.info("ip address is: {}",NetUtil.bytesToIpAddress(ByteBufUtil.getBytes(raw.content())));            }            i++;        }    }
复制代码


读取的逻辑很简单,先从 DefaultDnsResponse 中读取 QUESTION,打印出来,然后再读取它的 ANSWER,因为这里是 A address,所以调用 NetUtil.bytesToIpAddress 方法将 ANSWER 转换为 ip 地址打印出来。


最后我们可能得到这样的输出:


INFO  c.f.dnsdot.DotChannelInboundHandler - question is :DefaultDnsQuestion(www.flydean.com. IN A)INFO  c.f.dnsdot.DotChannelInboundHandler - ip address is: 47.107.98.187
复制代码

TLS 的客户端请求

我们创建好 channel 之后,就需要向 DNS server 端发送查询请求了。因为是 DoT,那么和普通的 TCP 查询有什么区别呢?


答案是并没有什么区别,因为 TLS 的操作 SslHandler 我们已经在 handler 中添加了。所以这里的查询和普通查询没什么区别。


int randomID = (int) (System.currentTimeMillis() / 1000);            DnsQuery query = new DefaultDnsQuery(randomID, DnsOpCode.QUERY)                    .setRecord(DnsSection.QUESTION, new DefaultDnsQuestion(queryDomain, DnsRecordType.A));            ch.writeAndFlush(query).sync();            boolean result = ch.closeFuture().await(10, TimeUnit.SECONDS);            if (!result) {                log.error("DNS查询失败");                ch.close().sync();            }
复制代码


同样我们需要构建一个 DnsQuery,这里使用的是 DefaultDnsQuery,通过传入一个 randomID 和 opcode 即可。


因为是查询,所以这里的 opcode 是 DnsOpCode.QUERY。


然后需要向 QUESTION section 中添加一个 DefaultDnsQuestion,用来查询具体的域名和类型。


这里的 queryDomain 是 www.flydean.com,查询类型是 A,表示的是对域名进行 IP 解析。


最后将得到的 query,写入到 channel 中即可。

总结

这里我们使用 netty 构建了一个基于 TLS 的 DNS 查询客户端,除了添加 TLS handler 之外,其他操作和普通的 TCP 操作类似。但是要注意的是,要想客户端可以正常工作,我们需要请求支持 DoT 协议的 DNS 服务器才可以。


本文的代码,大家可以参考:


learn-netty4

发布于: 刚刚阅读数: 3
用户头像

关注公众号:程序那些事,更多精彩等着你! 2020.06.07 加入

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧,尽在公众号:程序那些事!

评论

发布
暂无评论
netty系列之:在netty中使用tls协议请求DNS服务器_Java_程序那些事_InfoQ写作社区