简介
在后面的文章中咱们讲过了如何在 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