DNS の反復的な名前解決の仕組みとフルリゾルバの実装

ドメイン名の階層

DNS ではドメイン名に対する情報を管理します.

ドメイン名には階層があり、ドットで区切られたラベルの列の接尾辞がより上位の名前です. 最上位のドメイン名は "." です.

通常利用されているホスト名では、最上位の "." が省略されています. たとえば、"example.com" の省略をやめると "example.com." です. "example.com." は "com." から見て下位のドメイン名です. また、"example.com." と "com." は "." から見て下位のドメイン名です.

DNS ゾーン、DNS 権威サーバ、委任

DNS ゾーンとは、ドメイン名を頂点とする名前情報の管理単位で、より下位のドメイン名を管理します. この頂点のドメイン名をゾーン頂点(zone apex)と言います. DNS 権威サーバはゾーンの名前情報を管理します. 最上位のゾーン頂点は "." で、 このゾーンをルートゾーンといいます.

権威サーバはより下位のドメイン名のすべてを直接管理しているとは限らず、間接的な管理を行なっている場合があります. このとき、権威サーバは名前に対する情報を直接返す代わりに、下位のドメイン名を頂点とするゾーンの権威サーバの情報を返します. このような操作を委任と言い、その情報を委任情報と言います.

委任情報には、下位のドメイン名の権威サーバのドメイン名の情報を示すタイプNS のリソースレコード 1と、 権威サーバのアドレスの情報を示すタイプ A (IPv4アドレス) または AAAA (IPv6アドレス)のリソースレコードが含まれます. この、権威サーバのアドレスの情報を示すレコードを、グルーレコードと言います.

下位のドメイン名の権威サーバは、下位のドメイン名を持つ場合と、そうではないドメイン名を持つ場合があります. 下位のドメイン名を持つ場合には、グルーレコードが委任情報に含まれますが、 そうではないドメイン名を持つ場合には、グルーレコードは利用できません.

DNS の反復的な名前解決とフルリゾルバ

DNS で名前を解決するには、目的の'ドメイン名'と'タイプ'を指定し、次のような問い合わせの繰り返しの操作が必要です.2 この操作を反復的名前解決(iterative resolution)と言います.

  1. '問い合わせ先'をルートゾーンの権威サーバ、'ドメイン名'を目的のドメイン名のトップレベル("com.", "net.", "jp." 等)に設定して開始する
  2. 'ドメイン名'が目的のドメイン名なら、目的のタイプを問い合わせて終了.
  3. 'ドメイン名'とタイプA の問い合わせを行なう
  4. 問い合わせの結果、
    1. 委任情報が返らない場合、'ドメイン名'をより下位のドメイン名へと設定して、繰り返す. 2へ
    2. 委任情報が返った場合、'問い合わせ先'を委任先の権威サーバ3、 'ドメイン名'をより下位のドメイン名へと設定して、繰り返す. 2へ

DNS のフルリゾルバはこの反復的名前解決を行なう機能に加えて、問い合わせの結果のキャッシュを保持しているため、 直接は反復的な名前解決を行なうことができないクライアントのスタブリゾルバからの要求に対して、解決結果を提供することができます.

フルリゾルバの実装

Haskell で PoC として実装した反復的名前解決4とキャッシュ5 を組み合わせてフルリゾルバを実装しました. 6

次のプログラムは反復的名前解決うち、反復的に最終的な委任情報を得る部分を単純化したものです:

iterative :: Delegation  {- 初期値はルートゾーン -}
          -> [Name]      {- 上位から下位へのドメインリスト ex. ["com.", "example.com.", "www.example.com."] -}
          -> DNSQuery Delegation
iterative di0 []        = return di0
iterative di0 (name:ns) =
  step di0 >>=
  maybe
  (iterative di0 ns)  {- 委任情報が返らない無い場合は同じ委任情報を使う -}
  (\di -> iterative di ns)
  where
    step :: Delegation -> DNSQuery (Maybe Delegation)
    step di = do
      aa <- selectAuthAddr di
      msg <- queryAuth aa name A
      getDelegation name msg

{- 委任情報から権威サーバのアドレスを選ぶ.
   グルーレコードが利用できない場合は名前解決を再帰する -}
selectAuthAddr :: Delegation -> DNSQuery IP
{- 権威サーバから問い合わせ結果を得る -}
queryAuth :: IP -> Domain -> TYPE -> DNSQuery DNSMessage
{- 問い合わせ結果から委任情報を取り出す -}
getDelegation :: Domain -> DNSMessage -> QNSQuery (Maybe Delegation)

DNSメッセージのエンコード、デコード、およびスタブリゾルバには開発中のdnsextライブラリ群7 を利用しています.

キャッシュは優先度付きキューのライブラリであるpsqueues を利用して実現しています. キャッシュの実装の詳細な説明については https://khibino.hatenadiary.jp/entry/2023/03/20/105555#dns-full-resolver を参照してください.

フルリゾルバのサーバ機能は、次の 3種類のスレッドを連結することで実現しました.

  • 問い合わせDNSメッセージの受信とデコード
  • キャッシュ付きの反復的名前解決のワーカー
  • 返答DNSメッセージのエンコードと送信

Haskell の並行プログラミングの機能によって、フルリゾルバのサーバ機能を簡潔に実現することができました.


  1. DNS のリソースレコードについては別記事 https://khibino.hatenadiary.jp/entry/2023/03/20/105555#dns-rr を参照
  2. QNAME Minimisation Examples https://datatracker.ietf.org/doc/html/rfc9156#section-4
  3. 委任先の権威サーバが解決中の下位ドメイン名でない場合には、グルーレコードが利用できないため、同様の繰り返しで委任先の権威サーバのドメイン名の解決が必要
  4. https://github.com/khibino/dns-resolver/blob/tag/cache-server/src/DNSC/Iterative.hs
  5. https://github.com/khibino/dns-resolver/blob/tag/cache-server/src/DNSC/Cache.hs
  6. https://github.com/khibino/dns-resolver/blob/tag/cache-server/src/DNSC/Server.hs
  7. フルリゾルバの PoC 実装当時はdnsライブラリ を利用していましたが dnsext へと移行しました. またフルリゾルバのリポジトリを dnsext 下へ移動しました https://github.com/kazu-yamamoto/dnsext/tree/main/dnsext-full-resolver