天天看點

netty系列之: 在netty中使用 tls 協定請求 DNS 伺服器

目錄

  • ​​簡介​​
  • ​​支援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伺服器才可以。

本文的代碼,大家可以參考: