目錄
- 簡介
- 支援DoT的DNS伺服器
- 搭建支援DoT的netty用戶端
- TLS的用戶端請求
- 總結
簡介
在前面的文章中我們講過了如何在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伺服器才可以。
本文的代碼,大家可以參考: