権威DNSサーバを作ろうと思い、オープンソースの権威DNSサーバとして有名なNLnetLabsのNSDのソースコードを読み込んだので、内容についてまとめました。
投稿日時点でのリリースバージョンは 4.3.7 が最新のため、こちらをリファレンス先とします。githubの該当コード行に飛べるように各所にリンクを貼っています。
また、最終的に作ったものは下記になります。
DNSプロトコルの基本的な処理を重点的に、他のところはさらっと流していきます。
main()からスタートとして芋づる式に見ていくのですが、非常に長くなるため4つのパートに分けて見ていきます。
Part1. start from main()
nsd.cのmain関数からスタートします。
さまざまなセットアップが行われていますが、流し読みしつつ、クエリを受け付けるためのソケットセットアップをどこかで行うんだろうなと想像しながら見ていきます。
ざっくりとした構造は下記の通りです。
main (nsd.c)
... various setups ...
server_main (server.c)
server_start_children # start the child processes that handle incoming queries
restart_child_servers
server_child
server_child (server.c) # Serve DNS requests
Part2. setup udp/tcp handler
ここでudpハンドラーとtcpハンドラーが登場します。
いずれの場合もソケット/コネクション経由でクエリを受け取り、クエリを処理してレスポンスを作成し、ソケット/コネクションから返してあげるというおおまかな流れが見て取れます。
server_child (server.c)
add_udp_handler
handle_udp # Handle incoming queries on the UDP server sockets.
nsd_recvmmsg
rcvd = recvfrom(...)
server_process_query_udp
query_process (query.c)
nsd_sendmmsg
sendto(...)
add_tcp_handler
handle_tcp_accept # Handle an incoming TCP connection.
perform_accept
handle_tls_reading # Handle incoming queries on a TLS over TCP connection.
received = SSL_read(...)
server_process_query
query_process (query.c)
handle_tls_writitng # Handle outgoing responses on a TLS over TCP connection.
handle_tcp_reading # Handle incoming queries on a TCP connection.
received = read(...)
server_process_query
query_process (query.c)
handle_tcp_writing # Handle outgoing responses on a TCP connection.
Part3. query process
クエリ処理部分を詳しく見ていきましょう。
前半部分で異常なDNSクエリを処理し、その後EDNSのパースやTSIGの処理を行っていますね。
後半のほうでクエリに対する回答作成が行われています。今回はanswer_queryのみピックアップします。
query_process (query.c)
check packet size -> drop
check QR bit -> drop
check OPCODE -> drop or query_error
perse the question section -> query_formerror
OPCODE == NOTIFY -> answer_notify
check QDCOUNT -> query_formerror
check ANCOUNT or NSCOUNT -> query_formerror
edns_parse_record (edns.c)
tsig_parse_rr (tsig.c)
process_tsig -> query_error
process_edns -> return BADVERS(16)
query_prepare_response # Update the flags, etc.
answer_chaos
answer_axfr_ixfr (axfr.c)
if qtype == ANY and refuse_any option and not tcp -> truncate
answer_query
Part4. create answer response
まずはコードの流れを見てみましょう。
answer_query (query.c)
answer_init (answer.c) # Reset answer rrset count
namedb_lookup (namedb.c)
domain_table_search
radname_find_less_equal (radtree.c)#radix tree (patricia tree)
rbtree_find_less_equal (rbtree.c) # red black tree
answer_lookup_zone
domain_find_zone (namedb.c) -> return REFUSE
acl_check_incoming (options.c) -> return REFUSE
If the current "closest encloser" does not exist, it will move from child to parent in order until it does exist.
responding to queries for DS RRs
see if the zone has expired (for secondary zones)
answer nodata for type DS query at the zone apex
domain_find_ns_rrsets (namedb.c) # Delegation check
if delegation doesn't exist -> answer_authoritative
process NSEC3
process DNAME and CNAME
process wildcard
some dnssec process...
if match -> answer_domain
if qtype == ANY -> return response to ANY type
if qtype == NSEC3 -> return response to NSEC3 type
if domain_find_rrset (qname, qtype) exists -> add rrset to (qname, qtype)
if domain_find_rrset (qname, CNAME) exists -> process CNAME and return response
if none of the above applies, answer_nodata
if needed, add ns rrset to authority section
if not match -> answer_nxdomain
if delegation exists -> answer_delegation
encode_answer
packet_encode_rrset
If the minimal response size is exceeded during this process, set the TC bit. -> TC_SET
長いですが、ここは細かく見ていきます。
namedb_lookupのところで、問い合わせているqnameに該当するものが存在するかどうかや、closest encloserについて調べているようです。1
Closest encloser: "The longest existing ancestor of a name." (Quoted from [RFC5155], Section 1.3)
An earlier definition is "The node in the zone's tree of existing domain names that has the most labels matching the query name (consecutively, counting from >the root label downward).
Each match is a 'label match' and the order of the labels is the same." (Quoted from [RFC4592], Section 3.3.1)
↓ JPRSによる日本語訳より
Closest encloser(最近接名):
"ある名前の実在する先祖(訳注: 親、親の親等)の中で最も長いもの"([RFC5155]のセクション1.3から引用)。
それ以前の定義は、"実在するドメイン名のゾーンの木構造内にあるノードで、問い合わせ名にラベルが(ルートラベルから下方に向けて連続して)最もマッチするもの。
ここで、マッチとは'ラベルが一致'し、かつラベルの順序が同じであることを意味する"([RFC4592]のセクション3.3.1から引用)。
つまり、下記のようなイメージでしょうか。
$ORIGIN = example.com.
... snip ...
bar NS ns.bar
ns.bar A 1.2.3.4
www A 5.6.7.8
query for www.foo.bar.example.com -> closest encloser is bar.example.com
query for www.example.com -> closest encloser is www.example.com
query for nxdomain0123.example.com -> closest encloser is example.com
query for hoge.net -> closest encloser is . (root)
後のDSクエリに対する処理やdelegationに関する処理のところでClosest encloserが使用されるようです。
続いてanswer_lookup_zoneの処理を見ていきましょう。2
answer_lookup_zone から戻ってきたら、作成した回答をバイナリ形式(Wire format)にエンコードします。
この際にでレスポンスサイズがオーバーしている場合は、ここで truncate されてTCビットがセットされます。
そして Part2 へと戻っていき、UDP/TCPハンドラーによってクエリ元へと回答されます。お疲れさまでした。
-
exact=1(True)となるのはclosest_encloser==qnameとなる場合だと読み取ったのですが、少し自信がないので分かる人がいればコメントいただけると嬉しいです。
ところでclosest encloserとは何でしょうか。RFC8499に語義の説明があります。 ↩ -
余談ですが、英語のコードリーディングの記事を読んでいると、関数/メソッドを呼ぶという説明のときに"call"の他に"invoke"という単語をよく見かけます。
まずクエリに対応するゾーンを探していますね。3 もし見当違いなクエリが届いてしまっている場合はここでREFUSE応答を返してしまいます。 ↩ -
クエリ構造体(struct query)のメンバにzoneが用意されており、そこに見つかったゾーンを入れるという実装をしていて、少し独特だなと感じました。
aclのチェックなどを行ったあと、Closest encloserの修正が行われ、DSクエリへの処理をし、ゾーン情報がexpireしていないか確認しています。
その後、まずdelegationを確認し、qnameにマッチするものがゾーンに存在するか確認し、さらにqtypeに合うものが存在するか確認し、それぞれに対応した回答の作成を行います。
フローとしては下記の通りになります。 ↩