手把手教你在netty中使用TCP協(xié)議請求DNS服務(wù)器

簡介

DNS的全稱domain name system,既然是一個系統(tǒng)就有客戶端和服務(wù)器之分。一般情況來說我們并不需要感知這個DNS客戶端的存在,因為我們在瀏覽器訪問某個域名的時候,瀏覽器作為客戶端已經(jīng)實(shí)現(xiàn)了這個工作损拢。

但是有時候我們沒有使用瀏覽器,比如在netty環(huán)境中婚肆,如何構(gòu)建一個DNS請求呢驳阎?

DNS傳輸協(xié)議簡介

在RFC的規(guī)范中,DNS傳輸協(xié)議有很多種,如下所示:

  • DNS-over-UDP/53簡稱"Do53",是使用UDP進(jìn)行DNS查詢傳輸?shù)膮f(xié)議郑藏。
  • DNS-over-TCP/53簡稱"Do53/TCP",是使用TCP進(jìn)行DNS查詢傳輸?shù)膮f(xié)議衡查。
  • DNSCrypt,對DNS傳輸協(xié)議進(jìn)行加密的方法。
  • DNS-over-TLS簡稱"DoT",使用TLS進(jìn)行DNS協(xié)議傳輸必盖。
  • DNS-over-HTTPS簡稱"DoH",使用HTTPS進(jìn)行DNS協(xié)議傳輸拌牲。
  • DNS-over-TOR,使用VPN或者tunnels連接DNS俱饿。

這些協(xié)議都有對應(yīng)的實(shí)現(xiàn)方式,我們先來看下Do53/TCP塌忽,也就是使用TCP進(jìn)行DNS協(xié)議傳輸拍埠。

DNS的IP地址

先來考慮一下如何在netty中使用Do53/TCP協(xié)議,進(jìn)行DNS查詢土居。

因為DNS是客戶端和服務(wù)器的模式枣购,我們需要做的是構(gòu)建一個DNS客戶端,向已知的DNS服務(wù)器端進(jìn)行查詢擦耀。

已知的DNS服務(wù)器地址有哪些呢棉圈?

除了13個root DNS IP地址以外,還出現(xiàn)了很多免費(fèi)的公共DNS服務(wù)器地址,比如我們常用的阿里DNS,同時提供了IPv4/IPv6 DNS和DoT/DoH服務(wù)眷蜓。

IPv4: 
223.5.5.5

223.6.6.6

IPv6: 
2400:3200::1

2400:3200:baba::1

DoH 地址: 
https://dns.alidns.com/dns-query

DoT 地址: 
dns.alidns.com

再比如百度DNS分瘾,提供了一組IPv4和IPv6的地址:

IPv4: 
180.76.76.76

IPv6: 
2400:da00::6666

還有114DNS:

114.114.114.114
114.114.115.115

當(dāng)然還有很多其他的公共免費(fèi)DNS,這里我選擇使用阿里的IPv4:223.5.5.5為例吁系。

有了IP地址德召,我們還需要指定netty的連接端口號,這里默認(rèn)的是53汽纤。

然后就是我們要查詢的域名了氏捞,這里以www.flydean.com為例。

你也可以使用你系統(tǒng)中配置的DNS解析地址冒版,以mac為例液茎,可以通過nslookup進(jìn)行查看本地的DNS地址:

nslookup  www.flydean.com
Server:     8.8.8.8
Address:    8.8.8.8#53

Non-authoritative answer:
www.flydean.com canonical name = flydean.com.
Name:   flydean.com
Address: 47.107.98.187

Do53/TCP在netty中的使用

有了DNS Server的IP地址,接下來我們需要做的就是搭建netty client辞嗡,然后向DNS server端發(fā)送DNS查詢消息捆等。

搭建DNS netty client

因為我們進(jìn)行的是TCP連接,所以可以借助于netty中的NIO操作來實(shí)現(xiàn)续室,也就是說我們需要使用NioEventLoopGroup和NioSocketChannel來搭建netty客戶端:

 final String dnsServer = "223.5.5.5";
        final int dnsPort = 53;

EventLoopGroup group = new NioEventLoopGroup();
            Bootstrap b = new Bootstrap();
            b.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new Do53ChannelInitializer());

            final Channel ch = b.connect(dnsServer, dnsPort).sync().channel();

netty中的NIO Socket底層使用的就是TCP協(xié)議栋烤,所以我們只需要像常用的netty客戶端服務(wù)一樣構(gòu)建客戶端即可。

然后調(diào)用Bootstrap的connect方法連接到DNS服務(wù)器挺狰,就建立好了channel連接明郭。

這里我們在handler中傳入了自定義的Do53ChannelInitializer,我們知道handler的作用是對消息進(jìn)行編碼丰泊、解碼和對消息進(jìn)行讀取薯定。因為目前我們并不知道客戶端查詢的消息格式,所以Do53ChannelInitializer的實(shí)現(xiàn)我們在后面再進(jìn)行詳細(xì)講解瞳购。

發(fā)送DNS查詢消息

netty提供了DNS消息的封裝话侄,所有的DNS消息,包括查詢和響應(yīng)都是DnsMessage的子類。

每個DnsMessage都有一個唯一標(biāo)記的ID年堆,還有代表這個message類型的DnsOpCode吞杭。

對于DNS來說,opCode有下面這幾種:

    public static final DnsOpCode QUERY = new DnsOpCode(0, "QUERY");
    public static final DnsOpCode IQUERY = new DnsOpCode(1, "IQUERY");
    public static final DnsOpCode STATUS = new DnsOpCode(2, "STATUS");
    public static final DnsOpCode NOTIFY = new DnsOpCode(4, "NOTIFY");
    public static final DnsOpCode UPDATE = new DnsOpCode(5, "UPDATE");

因為每個DnsMessage都可能包含4個sections,每個section都以DnsSection來表示变丧。因為有4個section芽狗,所以在DnsSection定義了4個section類型:

    QUESTION,
    ANSWER,
    AUTHORITY,
    ADDITIONAL;

每個section里面又包含了多個DnsRecord, DnsRecord代表的就是Resource record,簡稱為RR,RR中有一個CLASS字段痒蓬,下面是DnsRecord中CLASS字段的定義:

    int CLASS_IN = 1;
    int CLASS_CSNET = 2;
    int CLASS_CHAOS = 3;
    int CLASS_HESIOD = 4;
    int CLASS_NONE = 254;
    int CLASS_ANY = 255;

DnsMessage是DNS消息的統(tǒng)一表示童擎,對于查詢來說,netty中提供了一個專門的查詢類叫做DefaultDnsQuery谊却。

先來看下DefaultDnsQuery的定義和構(gòu)造函數(shù):

public class DefaultDnsQuery extends AbstractDnsMessage implements DnsQuery {

        public DefaultDnsQuery(int id) {
        super(id);
    }

    public DefaultDnsQuery(int id, DnsOpCode opCode) {
        super(id, opCode);
    }

DefaultDnsQuery的構(gòu)造函數(shù)需要傳入id和opCode柔昼。

我們可以這樣定義一個DNS查詢:

int randomID = (int) (System.currentTimeMillis() / 1000);
            DnsQuery query = new DefaultDnsQuery(randomID, DnsOpCode.QUERY)

既然是QEURY,那么還需要設(shè)置4個sections中的查詢section:

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

這里調(diào)用的是setRecord方法向section中插入RR數(shù)據(jù)。

這里的RR數(shù)據(jù)使用的是DefaultDnsQuestion炎辨。DefaultDnsQuestion的構(gòu)造函數(shù)有兩個捕透,一個是要查詢的domain name,這里就是"www.flydean.com",另外一個參數(shù)是dns記錄的類型碴萧。

dns記錄的類型有很多種乙嘀,在netty中有一個專門的類DnsRecordType表示,DnsRecordType中定義了很多個類型,如下所示:

public class DnsRecordType implements Comparable<DnsRecordType> {
    public static final DnsRecordType A = new DnsRecordType(1, "A");
    public static final DnsRecordType NS = new DnsRecordType(2, "NS");
    public static final DnsRecordType CNAME = new DnsRecordType(5, "CNAME");
    public static final DnsRecordType SOA = new DnsRecordType(6, "SOA");
    public static final DnsRecordType PTR = new DnsRecordType(12, "PTR");
    public static final DnsRecordType MX = new DnsRecordType(15, "MX");
    public static final DnsRecordType TXT = new DnsRecordType(16, "TXT");
    ...

因為類型比較多破喻,我們挑選幾個常用的進(jìn)行講解虎谢。

  • A類型,是address的縮寫曹质,用來指定主機(jī)名或者域名對應(yīng)的ip地址.
  • NS類型婴噩,是name server的縮寫,是域名服務(wù)器記錄羽德,用來指定域名由哪個DNS服務(wù)器來進(jìn)行解析几莽。
  • MX類型,是mail exchanger的縮寫,是一個郵件交換記錄宅静,用來根據(jù)郵箱的后綴來定位郵件服務(wù)器章蚣。
  • CNAME類型,是canonical name的縮寫姨夹,可以將多個名字映射到同一個主機(jī).
  • TXT類型纤垂,用來表示主機(jī)或者域名的說明信息。

以上幾個是我們經(jīng)常會用到的dns record類型磷账。

這里我們選擇使用A峭沦,用來查詢域名對應(yīng)的主機(jī)IP地址。

構(gòu)建好query之后够颠,我們就可以使用netty client發(fā)送query指令到dns服務(wù)器了熙侍,具體的代碼如下:

            DnsQuery query = new DefaultDnsQuery(randomID, DnsOpCode.QUERY)
                    .setRecord(DnsSection.QUESTION, new DefaultDnsQuestion(queryDomain, DnsRecordType.A));
            ch.writeAndFlush(query).sync();

DNS查詢的消息處理

DNS的查詢消息我們已經(jīng)發(fā)送出去了,接下來就是對消息的處理和解析了履磨。

還記得我們自定義的Do53ChannelInitializer嗎蛉抓?看一下它的實(shí)現(xiàn):

class Do53ChannelInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) {
        ChannelPipeline p = ch.pipeline();
        p.addLast(new TcpDnsQueryEncoder())
                .addLast(new TcpDnsResponseDecoder())
                .addLast(new Do53ChannelInboundHandler());
    }
}

我們向pipline中添加了兩個netty自帶的編碼解碼器TcpDnsQueryEncoder和TcpDnsResponseDecoder,還有一個自定義用來做消息解析的Do53ChannelInboundHandler剃诅。

因為我們向channel中寫入的是DnsQuery,所以需要一個encoder將DnsQuery編碼為ByteBuf,這里使用的是netty提供的TcpDnsQueryEncoder:

public final class TcpDnsQueryEncoder extends MessageToByteEncoder<DnsQuery> 

TcpDnsQueryEncoder繼承自MessageToByteEncoder巷送,表示將DnsQuery編碼為ByteBuf。

看下他的encode方法:

    protected void encode(ChannelHandlerContext ctx, DnsQuery msg, ByteBuf out) throws Exception {
        out.writerIndex(out.writerIndex() + 2);
        this.encoder.encode(msg, out);
        out.setShort(0, out.readableBytes() - 2);
    }

可以看到TcpDnsQueryEncoder在msg編碼之前存儲了msg的長度信息矛辕,所以是一個基于長度的對象編碼器笑跛。

這里的encoder是一個DnsQueryEncoder對象。

看一下它的encoder方法:

    void encode(DnsQuery query, ByteBuf out) throws Exception {
        encodeHeader(query, out);
        this.encodeQuestions(query, out);
        this.encodeRecords(query, DnsSection.ADDITIONAL, out);
    }

DnsQueryEncoder會依次編碼header聊品、questions和records飞蹂。

完成編碼之后,我們還需要從DNS server的返回中decode出DnsResponse翻屈,這里使用的是netty自帶的TcpDnsResponseDecoder:

public final class TcpDnsResponseDecoder extends LengthFieldBasedFrameDecoder

TcpDnsResponseDecoder繼承自LengthFieldBasedFrameDecoder陈哑,表示數(shù)據(jù)是以字段長度來進(jìn)行分割的,這和我們剛剛將的encoder的格式類似伸眶。

來看下他的decode方法:

    protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
        ByteBuf frame = (ByteBuf)super.decode(ctx, in);
        if (frame == null) {
            return null;
        } else {
            DnsResponse var4;
            try {
                var4 = this.responseDecoder.decode(ctx.channel().remoteAddress(), ctx.channel().localAddress(), frame.slice());
            } finally {
                frame.release();
            }
            return var4;
        }
    }

decode方法先調(diào)用LengthFieldBasedFrameDecoder的decode方法將要解碼的內(nèi)容提取出來惊窖,然后調(diào)用responseDecoder的decode方法,最終返回DnsResponse厘贼。

這里的responseDecoder是一個DnsResponseDecoder界酒。具體decoder的細(xì)節(jié)這里就不過多闡述了。感興趣的同學(xué)可以自行查閱代碼文檔嘴秸。

最后毁欣,我們得到了DnsResponse對象。

接下來就是自定義的InboundHandler對消息進(jìn)行解析了:

class Do53ChannelInboundHandler extends SimpleChannelInboundHandler<DefaultDnsResponse> 

在它的channelRead0方法中岳掐,我們調(diào)用了readMsg方法對消息進(jìn)行處理:

    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);
            //A記錄用來指定主機(jī)名或者域名對應(yīng)的IP地址
            if (record.type() == DnsRecordType.A) {
                DnsRawRecord raw = (DnsRawRecord) record;
                log.info("ip address is: {}",NetUtil.bytesToIpAddress(ByteBufUtil.getBytes(raw.content())));
            }
            i++;
        }
    }

DefaultDnsResponse是DnsResponse的一個實(shí)現(xiàn)凭疮,首先判斷msg中的QUESTION個數(shù)是否大于零。

如果大于零岩四,則打印出question的信息哭尝。

然后再解析出msg中的ANSWER并打印出來。

最后剖煌,我們可能得到這樣的輸出:

INFO  c.f.dnstcp.Do53ChannelInboundHandler - question is :DefaultDnsQuestion(www.flydean.com. IN A)
INFO  c.f.dnstcp.Do53ChannelInboundHandler - ip address is: 47.107.98.187

總結(jié)

以上就是使用netty創(chuàng)建DNS client進(jìn)行TCP查詢的講解材鹦。

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

learn-netty4

更多內(nèi)容請參考 http://www.flydean.com/54-netty-dns-over-tcp/

最通俗的解讀耕姊,最深刻的干貨桶唐,最簡潔的教程,眾多你不

歡迎關(guān)注我的公眾號:「程序那些事」,懂技術(shù)茉兰,更懂你尤泽!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子坯约,更是在濱河造成了極大的恐慌熊咽,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件闹丐,死亡現(xiàn)場離奇詭異横殴,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)卿拴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進(jìn)店門衫仑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人堕花,你說我怎么就攤上這事文狱。” “怎么了缘挽?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵瞄崇,是天一觀的道長。 經(jīng)常有香客問我到踏,道長杠袱,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任窝稿,我火速辦了婚禮楣富,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘伴榔。我一直安慰自己纹蝴,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布踪少。 她就那樣靜靜地躺著塘安,像睡著了一般。 火紅的嫁衣襯著肌膚如雪援奢。 梳的紋絲不亂的頭發(fā)上兼犯,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天,我揣著相機(jī)與錄音集漾,去河邊找鬼切黔。 笑死,一個胖子當(dāng)著我的面吹牛具篇,可吹牛的內(nèi)容都是我干的纬霞。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼驱显,長吁一口氣:“原來是場噩夢啊……” “哼诗芜!你這毒婦竟也來了瞳抓?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤伏恐,失蹤者是張志新(化名)和其女友劉穎孩哑,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體脐湾,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡臭笆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年叙淌,在試婚紗的時候發(fā)現(xiàn)自己被綠了秤掌。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡鹰霍,死狀恐怖闻鉴,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情茂洒,我是刑警寧澤孟岛,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站督勺,受9級特大地震影響渠羞,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜智哀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一次询、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧瓷叫,春花似錦屯吊、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至次氨,卻和暖如春蔽介,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背煮寡。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工虹蓄, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人洲押。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓武花,卻偏偏與公主長得像,于是被迫代替她去往敵國和親杈帐。 傳聞我的和親對象是個殘疾皇子体箕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內(nèi)容