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