最低限の理解でDNSサーバーを実装する

Posted by hiroki.kanaの日常 on October 22, 2019
このエントリーをはてなブックマークに追加

はじめに

私は、雰囲気でDNSを使っていました。

最近雰囲気で使い続けることに限界を感じ、少しでも雰囲気で使ってる部分を解消するためにDNSサーバーを実装しました。とはいえ、DNSに関連するRFCを見ると、かなりのバリエーションがあり見れば見るほど私は雰囲気で使っていたと自覚するとともに、やはり私は雰囲気で使うことを抜けることはまだまだできないなと思うばかりでした。

まずはDNSメッセージのフォーマットを紹介し、最後に理解したDNSメッセージのフォーマットを元に作ったDNSサーバーのfemtoDNSを紹介します。

DNSメッセージフォーマット

RFC 1035 4.1 Format にDNSパケットのフォーマットが定義されています。 下記はパケットの構成を示した図の引用です。

    +---------------------+
    |        Header       |
    +---------------------+
    |       Question      | the question for the name server
    +---------------------+
    |        Answer       | RRs answering the question
    +---------------------+
    |      Authority      | RRs pointing toward an authority
    +---------------------+
    |      Additional     | RRs holding additional information
    +---------------------+

とりあえず正引きができれば良いという目的から、Header / Question / Answerのみに今回は注目しました。 かなり雑な列挙ですが、ひとまずこれがわかればなんとなくどうにかなります……。

Header セクション

Header セクションはDNSパケットには必ず存在し、パケットの内容についての情報が格納されています。

RFC 1035 4.1.1. Header section formatからHeader セクションの構成について引用した図こちらです。

    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                      ID                       |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |QR|   Opcode  |AA|TC|RD|RA|   Z    |   RCODE   |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    QDCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    ANCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    NSCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    ARCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

ID / QDCOUNT / ANCOUNT / NSCOUNT / ARCOUNTはそれぞれ16bit(2byte)で、IDの後方16bitにフラグ群が含まれています。 すべての項目について説明することが望ましいのですが、今回はレスポンスメッセージを作る際に意識する必要があった部分だけ列挙します。

  • ID
    • 問い合わせごとに付与されるID
    • リクエスト元はこのIDが送ったときと同一であるかを確認するので、レスポンスには同じリクエストと同じIDを付与する
  • QR
    • 0は問い合わせ、1はレスポンス
  • RCODE
    • レスポンスコード。正常な場合は0
  • QDCOUNT
    • Question セクションのエントリー数
  • ANCOUNT
    • Answer セクションのエントリー数

Question セクション

どのような内容の問い合わせであるかを格納するセクションです。 ヘッダ内のQDCOUNT には、このQestion セクションのエントリー数が保存されています。 このセクションは、その内容から多くの場合は1つなので、QDCOUNT も多くの場合は1です。

Question セクションはQNAME / QTYPE / QCLASS の3つから構成されています。

  • QNAME
    • ドメイン名の情報がラベル(FQDNを.で分割した文字列)という単位で分割されている
    • 1オクテット(8bit = 1byte)目にラベルの長さが定義され、ラベル本体と続く
    • 1オクテットの0がQNAMEの末尾のマーク
  • QTYPE
    • 問い合わせレコードのタイプ
    • Aレコードの場合は1
    • 詳細はこちら 3.2.2. TYPE values
  • QCLASS
    • 問い合わせレコードのクラス
    • 概ねはインターネットを示すINなので1

Answer セクション

問い合わせを受けて、正常に引けた場合の返答となるリソースレコード(RFC 1035 ではRRと書かれています)を格納するセクションです。 リソースレコードのフォーマットについてRFC 1035から引用した図はこちらです。

    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                                               |
    /                                               /
    /                      NAME                     /
    |                                               |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                      TYPE                     |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                     CLASS                     |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                      TTL                      |
    |                                               |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                   RDLENGTH                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
    /                     RDATA                     /
    /                                               /
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

それぞれのフィールドは下記のような意味があります。

  • NAME
    • レコードのドメイン名
    • フォーマットはQNAMEと同一
  • TYPE
    • レコードのタイプ
    • フォーマットはQTYPEと同一
  • CLASS
    • レコードのクラス
    • フォーマットはQCLASSと同一
  • TTL
    • レコードの有効期限(秒)
  • RDLENGTH
    • リソースデータの長さ
    • IPv4アドレスの場合は4オクテット
  • RDATA
    • 実際のレコードのデータ

femtoDNSの概要

ここまで私がDNSメッセージのフォーマットについてかなり端折って紹介しましたが、ここまでの理解でDNSサーバーを実装しました。 実際に常用できるところまで実装するとなると、かなり道のりが先になりそうだったので下記を目的としました。

  • dig コマンドで問い合わせができる
  • localhost の問い合わせに対して 127.0.0.1 Aレコードを返す
  • ファイルにIPアドレスとホスト名のマッピングを追記すると、その内容を元にAレコードを返す Pythonのsocketserverモジュールを利用してUDPサーバーを実装しました。

動作例

Python 3.7以上があれば動作させることができます。

$ git clone git@github.com:hirokikana/femtoDNS.git
$ cd femtoDNS
$ python3 start.py

この状態で、ポート番号9999でUDPサーバーが立ち上がります。 digを使って問い合わせしてみます。

$ dig @127.0.0.1 -p 9999 localhost

; <<>> DiG 9.10.6 <<>> @127.0.0.1 -p 9999 localhost
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 13985
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;localhost.			IN	A

;; ANSWER SECTION:
localhost.		255	IN	A	127.0.0.1

;; Query time: 2 msec
;; SERVER: 127.0.0.1#9999(127.0.0.1)
;; WHEN: Tue Oct 22 12:24:11 JST 2019
;; MSG SIZE  rcvd: 52

ANSWER SECTIONにリソースレコードが含まれる形でレスポンスがあったことが確認できます。

まとめ

今回の実装はRFC 1035に書かれてることのほんの一部しか実装していません。 しかし、普段利用しているdigを利用して、正しい形に見えるレスポンスが返ってくるとちゃんと実装できたという満足感があります。 最後に、socketserverモジュールは余計なことを気にせずTCP/UDPサーバーを実装できるので雑にサーバーを実装するときはおすすめです。

参考サイト

このエントリーをはてなブックマークに追加