簡(jiǎn)介
在前面的文章中我們講過(guò)了如何在netty中構(gòu)造客戶端分別使用tcp和udp協(xié)議向DNS服務(wù)器請(qǐng)求消息。在請(qǐng)求的過(guò)程中并沒(méi)有進(jìn)行消息的加密错妖,所以這種請(qǐng)求是不安全的。
那么有同學(xué)會(huì)問(wèn)了格粪,就是請(qǐng)求解析一個(gè)域名的IP地址而已舍哄,還需要安全通訊嗎?
事實(shí)上普舆,不加密的DNS查詢消息是很危險(xiǎn)的恬口,如果你在訪問(wèn)一個(gè)重要的網(wǎng)站時(shí)候,DNS查詢消息被監(jiān)聽(tīng)或者篡改沼侣,有可能你收到的查詢返回IP地址并不是真實(shí)的地址祖能,而是被篡改之后的地址,從而打開(kāi)了釣魚(yú)網(wǎng)站或者其他惡意的網(wǎng)站蛾洛,從而造成了不必要的損失养铸。
所以DNS查詢也是需要保證安全的。
幸運(yùn)的是在DNS的傳輸協(xié)議中特意指定了一種加密的傳輸協(xié)議叫做DNS-over-TLS轧膘,簡(jiǎn)稱(chēng)("DoT")钞螟。
那么在netty中可以使用DoT來(lái)進(jìn)行DNS服務(wù)查詢嗎?一起來(lái)看看吧谎碍。
支持DoT的DNS服務(wù)器
因?yàn)镈NS中有很多傳輸協(xié)議規(guī)范鳞滨,但并不是每個(gè)DNS服務(wù)器都支持所有的規(guī)范,所以我們?cè)谑褂肈oT之前需要找到一個(gè)能夠支持DoT協(xié)議的DNS服務(wù)器蟆淀。
這里我還是選擇使用阿里DNS服務(wù)器:
223.5.5.5
之前使用TCP和UDP協(xié)議的時(shí)候查詢的DNS端口是53太援,如果換成了DoT,那么端口就需要變成853扳碍。
搭建支持DoT的netty客戶端
DoT的底層還是TCP協(xié)議提岔,也就是說(shuō)TLS over TCP,所以我們需要使用NioEventLoopGroup和NioSocketChannel來(lái)搭建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。
我們來(lái)看下DotChannelInitializer的定義和他的構(gòu)造函數(shù):
class DotChannelInitializer extends ChannelInitializer<SocketChannel> {
public DotChannelInitializer(SslContext sslContext, String dnsServer, int dnsPort) {
this.sslContext = sslContext;
this.dnsServer = dnsServer;
this.dnsPort = dnsPort;
}
DotChannelInitializer需要三個(gè)參數(shù)分別是sslContext赛惩,dnsServer和dnsPort哀墓。
這三個(gè)參數(shù)都是在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主要用來(lái)進(jìn)行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();
因?yàn)镾slProvider有很多種喷兼,可以選擇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之后吠各,就可以調(diào)用SslContextBuilder.forClient方法來(lái)創(chuàng)建SslContext。
這里我們指定SSL的protocol是"TLSv1.3"和"TLSv1.2"勉抓。
然后再調(diào)用sslContext的newHandler方法就創(chuàng)建好了支持ssl的handler:
sslContext.newHandler(ch.alloc(), dnsServer, dnsPort)
newHandler還需要指定dnsServer和dnsPort信息贾漏。
處理完ssl,接下來(lái)就是對(duì)dns查詢和響應(yīng)的編碼解碼器藕筋,這里使用的是TcpDnsQueryEncoder和TcpDnsResponseDecoder纵散。
TcpDnsQueryEncoder和TcpDnsResponseDecoder在之前介紹使用netty搭建tcp客戶端的時(shí)候就已經(jīng)詳細(xì)解說(shuō)過(guò)了,這里就不再進(jìn)行講解了隐圾。
編碼解碼之后伍掀,就是自定義的消息處理器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記錄用來(lái)指定主機(jī)名或者域名對(duì)應(yīng)的IP地址
DnsRawRecord raw = (DnsRawRecord) record;
log.info("ip address is: {}",NetUtil.bytesToIpAddress(ByteBufUtil.getBytes(raw.content())));
}
i++;
}
}
讀取的邏輯很簡(jiǎn)單,先從DefaultDnsResponse中讀取QUESTION暇藏,打印出來(lái)蜜笤,然后再讀取它的ANSWER,因?yàn)檫@里是A address叨咖,所以調(diào)用NetUtil.bytesToIpAddress方法將ANSWER轉(zhuǎn)換為ip地址打印出來(lái)瘩例。
最后我們可能得到這樣的輸出:
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的客戶端請(qǐng)求
我們創(chuàng)建好channel之后啊胶,就需要向DNS server端發(fā)送查詢請(qǐng)求了甸各。因?yàn)槭荄oT,那么和普通的TCP查詢有什么區(qū)別呢焰坪?
答案是并沒(méi)有什么區(qū)別趣倾,因?yàn)門(mén)LS的操作SslHandler我們已經(jīng)在handler中添加了。所以這里的查詢和普通查詢沒(méi)什么區(qū)別某饰。
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();
}
同樣我們需要構(gòu)建一個(gè)DnsQuery儒恋,這里使用的是DefaultDnsQuery,通過(guò)傳入一個(gè)randomID和opcode即可黔漂。
因?yàn)槭遣樵兘刖。赃@里的opcode是DnsOpCode.QUERY。
然后需要向QUESTION section中添加一個(gè)DefaultDnsQuestion炬守,用來(lái)查詢具體的域名和類(lèi)型牧嫉。
這里的queryDomain是www.flydean.com,查詢類(lèi)型是A,表示的是對(duì)域名進(jìn)行IP解析。
最后將得到的query,寫(xiě)入到channel中即可酣藻。
總結(jié)
這里我們使用netty構(gòu)建了一個(gè)基于TLS的DNS查詢客戶端曹洽,除了添加TLS handler之外,其他操作和普通的TCP操作類(lèi)似辽剧。但是要注意的是送淆,要想客戶端可以正常工作,我們需要請(qǐng)求支持DoT協(xié)議的DNS服務(wù)器才可以怕轿。
本文的代碼偷崩,大家可以參考: