沒錯,請求DNS伺服器還可以使用UDP協定

2022-07-25 15:00:18

簡介

之前我們講到了如何在netty中構建client向DNS伺服器進行域名解析請求。使用的是最常見的TCP協定,也叫做Do53/TCP。

事實上除了TCP協定之外,DNS伺服器還接收UDP協定。這個協定叫做DNS-over-UDP/53,簡稱("Do53")。

本文將會一步一步帶領大家在netty中搭建使用UDP的DNS使用者端。

搭建netty使用者端

因為這裡使用的UDP協定,netty為UDP協定提供了專門的channel叫做NioDatagramChannel。EventLoopGroup還是可以使用常用的NioEventLoopGroup,這樣我們搭建netty使用者端的程式碼和常用的NIO UDP程式碼沒有太大的區別,如下所示:

EventLoopGroup group = new NioEventLoopGroup();
            Bootstrap b = new Bootstrap();
            b.group(group)
                    .channel(NioDatagramChannel.class)
                    .handler(new Do53UdpChannelInitializer());
            final Channel ch = b.bind(0).sync().channel();

這裡的EventLoopGroup使用的是NioEventLoopGroup,作為client端Bootstrap的group。

因為要使用UDP協定進行傳輸,所以這裡的channel使用的是NioDatagramChannel。

設定好channel之後,傳入我們自定義的handler,netty client就搭建完畢了。

因為是UDP,所以這裡沒有使用TCP中的connect方法,而是使用bind方法來獲得channel。

Do53UdpChannelInitializer中包含了netty提供的UDP DNS的編碼解碼器,還有自定義的訊息處理器,我們會在後面的章節中詳細進行介紹。

在netty中傳送DNS查詢請求

搭建好netty使用者端之後,接下來就是使用使用者端傳送DNS查詢訊息了。

先看具體的查詢程式碼:

int randomID = (int) (System.currentTimeMillis() / 1000);
            DnsQuery query = new DatagramDnsQuery(null, addr, randomID).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();
            }

查詢的邏輯是先構建UDP的DnsQuery請求包,然後將這請求包寫入到channel中,然後等待訊息處理完畢。

DnsQuery之前我們已經介紹過了,他是netty中所有DNS查詢的基礎類。

public interface DnsQuery extends DnsMessage 

DnsQuery的子類有兩個,分別是DatagramDnsQuery和DefaultDnsQuery。這兩個實現類一個表示UDP協定的查詢,一個表示TCP協定的查詢。

我們看下UDP協定的DatagramDnsQuery具體定義:

public class DatagramDnsQuery extends DefaultDnsQuery implements AddressedEnvelope<DatagramDnsQuery, InetSocketAddress> 

可以看到DatagramDnsQuery不僅僅繼承自DefaultDnsQuery,還實現了AddressedEnvelope介面。

AddressedEnvelope是netty中UDP包的定義,所以要想在netty中傳送基於UDP協定的封包,就必須實現AddressedEnvelope中定義的方法。

作為一個UDP封包,除了基本的DNS查詢中所需要的id和opCode之外,還需要提供兩個額外的地址,分別是sender和recipient:

    private final InetSocketAddress sender;
    private final InetSocketAddress recipient;

所以DatagramDnsQuery的建構函式可以接收4個引數:

    public DatagramDnsQuery(InetSocketAddress sender, InetSocketAddress recipient, int id, DnsOpCode opCode) {
        super(id, opCode);
        if (recipient == null && sender == null) {
            throw new NullPointerException("recipient and sender");
        } else {
            this.sender = sender;
            this.recipient = recipient;
        }
    }

這裡recipient和sender不能同時為空。

在上面的程式碼中,我們構建DatagramDnsQuery時,傳入了伺服器的InetSocketAddress:

final String dnsServer = "223.5.5.5";
        final int dnsPort = 53;
 InetSocketAddress addr = new InetSocketAddress(dnsServer, dnsPort);

並且隨機生成了一個ID。然後呼叫setRecord方法填充查詢的資料。

.setRecord(DnsSection.QUESTION,
                    new DefaultDnsQuestion(queryDomain, DnsRecordType.A));

DnsSection有4個,分別是:

    QUESTION,
    ANSWER,
    AUTHORITY,
    ADDITIONAL;

這裡是查詢操作,所以需要設定DnsSection.QUESTION。它的值是一個DnsQuestion:

public class DefaultDnsQuestion extends AbstractDnsRecord implements DnsQuestion 

在這個查詢中,我們傳入了要查詢的domain值:www.flydean.com,還有查詢的型別A:address,表示的是域名的IP地址。

DNS訊息的處理

在Do53UdpChannelInitializer中為pipline新增了netty提供的UDP編碼解碼器和自定義的訊息處理器:

class Do53UdpChannelInitializer extends ChannelInitializer<DatagramChannel> {
    @Override
    protected void initChannel(DatagramChannel ch) throws Exception {
        ChannelPipeline p = ch.pipeline();
        p.addLast(new DatagramDnsQueryEncoder())
                .addLast(new DatagramDnsResponseDecoder())
                .addLast(new Do53UdpChannelInboundHandler());
    }
}

DatagramDnsQueryEncoder負責將DnsQuery編碼成為DatagramPacket,從而可以在NioDatagramChannel中進行傳輸。

public class DatagramDnsQueryEncoder extends MessageToMessageEncoder<AddressedEnvelope<DnsQuery, InetSocketAddress>> {

DatagramDnsQueryEncoder繼承自MessageToMessageEncoder,要編碼的物件是AddressedEnvelope,也就是我們構建的DatagramDnsQuery。

看一下它裡面最核心的encode方法:

    protected void encode(ChannelHandlerContext ctx, AddressedEnvelope<DnsQuery, InetSocketAddress> in, List<Object> out) throws Exception {
        InetSocketAddress recipient = (InetSocketAddress)in.recipient();
        DnsQuery query = (DnsQuery)in.content();
        ByteBuf buf = this.allocateBuffer(ctx, in);
        boolean success = false;
        try {
            this.encoder.encode(query, buf);
            success = true;
        } finally {
            if (!success) {
                buf.release();
            }
        }
        out.add(new DatagramPacket(buf, recipient, (InetSocketAddress)null));
    }

基本思路就是從AddressedEnvelope中取出recipient和DnsQuery,然後呼叫encoder.encode方法將DnsQuery進行編碼,最後將這些資料封裝到DatagramPacket中。

這裡的encoder是一個DnsQueryEncoder範例,專門用來編碼DnsQuery物件。

DatagramDnsResponseDecoder負責將接受到的DatagramPacket物件解碼成為DnsResponse供後續的自定義程式讀取使用:

public class DatagramDnsResponseDecoder extends MessageToMessageDecoder<DatagramPacket> 

看一下它的decode方法:

    protected void decode(ChannelHandlerContext ctx, DatagramPacket packet, List<Object> out) throws Exception {
        try {
            out.add(this.decodeResponse(ctx, packet));
        } catch (IndexOutOfBoundsException var5) {
            throw new CorruptedFrameException("Unable to decode response", var5);
        }
    }

上面的decode方法實際上呼叫了DnsResponseDecoder的decode方法進行解碼操作。

最後就是自定義的Do53UdpChannelInboundHandler用來進行訊息的讀取和解析:

    private static void readMsg(DatagramDnsResponse msg) {
        if (msg.count(DnsSection.QUESTION) > 0) {
            DnsQuestion question = msg.recordAt(DnsSection.QUESTION, 0);
            log.info("question is :{}", question);
        }
        for (int i = 0, count = msg.count(DnsSection.ANSWER); i < count; i++) {
            DnsRecord record = msg.recordAt(DnsSection.ANSWER, i);
            if (record.type() == DnsRecordType.A) {
                //A記錄用來指定主機名或者域名對應的IP地址
                DnsRawRecord raw = (DnsRawRecord) record;
                System.out.println(NetUtil.bytesToIpAddress(ByteBufUtil.getBytes(raw.content())));
            }
        }
    }

自定義handler接受的是一個DatagramDnsResponse物件,處理邏輯也很簡單,首先讀取msg中的QUESTION,並列印出來。

然後讀取msg中的ANSWER欄位,如果ANSWER的型別是A address,那麼就呼叫NetUtil.bytesToIpAddress方法將其轉換成為IP地址輸出。

最後我們可能得到下面的輸出:

question is :DefaultDnsQuestion(www.flydean.com. IN A)
49.112.38.167

總結

以上就是在netty中使用UDP協定進行DNS查詢的詳細講解。

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

learn-netty4

更多內容請參考 http://www.flydean.com/55-netty-dns-over-udp/

最通俗的解讀,最深刻的乾貨,最簡潔的教學,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!