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伺服器才可以。
本文的程式碼,大家可以參考: