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伺服器才可以。

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

learn-netty4