tinydnsの設定

djbdns のもう一方の主軸ソフトウェア tinydns は、正確な意味での DNSサーバで、リカーシブでない (non-recursive) 問い合わせに責任を持って答える。「責任を持って」というのは抽象的な意味ではなく、 RFC1035 でいうところの Authoritative Name Server (オーソリタティブ・ネームサーバ:権威付きネームサーバ、「DNSコンテンツサーバ」とも訳される) を意味し、権限を与えられているドメインの情報については常に AA (Authoritative Answer) ビット付きのレスポンスを返す。 tinydnsRD ビットの立ったドメインリクエストメッセージにも答えるが、その場合でも再帰解決努力はしないし、権限外のドメインに関する問い合わせには返事をしない。

ここは、前章dnscache とセットで考えており、dnscache と同一のマシン上で動かすことを想定している。 dnscache の設定手順と重複する部分は少々はしょっているので、疑問があれば dnscache での説明を参照していただくといいかもしれない。

ユーザの作成

DNSサーバの実行ユーザを作成する。 dns グループと、ログユーザ dnslog(Systemd起動にする場合は不要) については dnscache の設定の際に構築済み。ここにおいても、ユーザ名や UID 、ホームディレクトリは下記の通りである必要はなく、後述する tinydns-conf の引数をそれに合わせれば問題ない。ホームディレクトリは実際には作らせず (-M オプション)、シェルも偽シェルにする。

root# useradd -u 802 -g dns -M -d /var/dns -s /sbin/nologin tinydns

運用ディレクトリ構造の作成

tinydns-conf コマンドを使って運用ディレクトリ構造を作成する。 1番目と 2番目の引数は、先ほど作成した実行ユーザと、ログユーザの名前。 3番目は運用ディレクトリの基底パスだが、 /var/tinydns でなければならないという決まりはない。最後の引数は tinydns がリッスンする IPアドレス(省略不可)。リッスンアドレスは、 /var/tinydns/env/IP ファイルを書き換えれば、あとで変更することも可能。

root# tinydns-conf tinydns dnslog /var/tinydns 127.0.0.1

tinydns は基底ディレクトリである /var/tinydns (/var/tinydns/env/ROOT ファイルで定義されている) へ chroot した状態で動作する。以降、この /var/tinydns$ROOT と表す。

ロギング定義の修正

$ROOT/log/run を修正し、最大 3Mバイトを目処にログをローテーションし 10世代を保存していく設定にする。サイズと世代数を省略したままの時の multilog の規定動作は、 99999バイトの 10世代ローテーション。Systemd起動にする場合はこのファイルは使われないので修正不要。

exec setuidgid dnslog multilog t s3000000 n10 ./main

tinydnsの稼働

まだ肝心のレコードがないが、とりあえずここで稼働させてしまう。

init/Upstart駆動の場合

daemontools の監視する /service ディレクトリ下へシンボリックリンクを張ることで 5秒以内に稼働状態となる。

root# ln -s /var/tinydns /service
root# svstat /service/tinydns

2行目は稼働状態の確認。`/service/tinydns: up (pid ...' と表示されれば無事稼働中。

Systemd制御とする場合

サービスユニットファイル /etc/systemd/system/tinydns.service [root:root 644] を作成する。

[Unit]
Description=A DNS contents server
Requires=network.target
After=network.target
 
[Service]
Restart=always
EnvironmentFile=/var/tinydns/tinydns.conf
PIDFile=/var/run/tinydns.pid
LimitDATA=300000
ExecStart=/usr/local/bin/tinydns
 
[Install]
WantedBy=multi-user.target

LimitDATA パラメータは、ulimit -d つまり Data Segment Size。tinydns-conf によって生成される run ファイル内の規定値である 'softlimit -d300000' を反映させたもの。

EnvironmentFile パラメータで参照している $ROOT/tinydns.conf ファイルは、tinydns プログラムへ環境変数として渡すべき値をエクスポートするためのファイルで、daemontools 利用時にはないもの。新規に作成する。

ROOT=/var/tinydns/root
UID=tinydns
GID=dns
IP=127.0.0.1

稼働させる。

root# systemctl daemon-reload
root# systemctl start tinydns.service
root# systemctl enable tinydns.service
root# systemctl status tinydns.service

ログは標準出力を経て journalに記録される。

root# journalctl -u tinydns -xe

DNSレコードの作成

tinydns は cdb フォーマットのバイナリファイル $ROOT/root/data.cdb から随時レコードデータを読み取る。そして、このファイルを作成するには、まずテキストファイル $ROOT/root/data に内容を書き、それを tinydns-data ユーティリティを使って data.cdb に変換するという手順を踏む。 data.cdb の変更は即座に認識されるので、更新の度に tinydns を再起動する必要はない。

data ファイルは、手作業で書いてもいいが、 tinydns運用ディレクトリ構造を作成した際に $ROOT/root/ 下にインストールされる add-ns, add-host といったユーティリティ (シェルスクリプト) を使う方法もある。使い方:

./add-{host|alias|mx|ns|childns} domain_or_host IP

ただし、これらのユーティリティは一長一短で、

[短所]
o ごく一部の定型パターンしか作れず、融通が利かない
[長所]
o ホスト名や IPアドレスのコンフリクトが起こりそうになると、エラーを発し、data ファイルには手が加わらない
o data ファイルの一時コピーを作って変更を加えてからそれをリネームするという動きをするので、 dataファイルを台無しにするおそれがない
o レコードの種類に応じて最適な(常識的な) TTL を添えてくれる

というわけで、ユーティリティで下地を作ってから手で修正するのが最も妥当と言える。下の表は、 data ファイルの主な書式と、その定型がどのユーティリティで作れるかを示している。

tinydns-dataの主な書式

書式 発生するレコード ユーティリティ 説明
.domain:x.x.x.x:nshost:TTL SOA + NS + nshost のA add-ns  
.domain::nshost:TTL SOA + NS add-ns
+編集
上記のIP部を省略した形。複数のSOA宣言文で同じネームサーバIPが登場しそうになる場合に、ふたつ目以降ではこのようにIP部を省かないと、そのAレコードが重複登録されてしまう。つまり、ひとつのtinydnsで複数のドメインを管理する場合や、逆引き用SOA宣言に使用する
&domain:x.x.x.x:nshost:TTL NS + nshost のA add-childns サブドメインを他の DNS に委譲する時などに使用
=host:x.x.x.x:TTL A + PTR add-host  
+host:x.x.x.x:TTL A add-alias CNAME はできるだけ使わずこれを使用すること
^host:x.x.x.x:TTL PTR 編集  
Chost:x.x.x.x:TTL CNAME 編集 なるべく使用しないこと
@domain:x.x.x.x:mxhost:D:TTL MX + mxhost のA add-mx D 部は距離(優先度)で 0, 5, 10 など。 :D 以降は省略可能

上記全てにおいて :TTL 以降は省略することもでき、その場合には tinydns によって常識的な TTL が自動決定される。なお、 add-ns 及び add-mx で作成されたデータ行の nshost 及び mxhost 部は、a とか b といった適当な英字一文字の相対ホスト名になってしまうので、そこも後で ns1.hoge.cxm などといった Aレコードに相応しい名前に手直ししたいところだ。

[Note] BIND と違って、FQDN を表す場合でも頭にドットは要らない。

dataファイル記述例

こういうサーバ群のあるローカルネットワークを想定してみる;

そうすると、 $ROOT/root/data ファイルはこんな具合。通常、ローカルネットワーク用DNS に MXレコードは不要だが、例として入れてある。

.hoge.cxm:192.168.1.1:ns1.hoge.cxm:259200
.1.168.192.in-addr.arpa::ns1.hoge.cxm:259200
.hoge.cxm:192.168.1.2:ns2.hoge.cxm:259200
.1.168.192.in-addr.arpa::ns2.hoge.cxm:259200
@hoge.cxm:192.168.1.3:mail.hoge.cxm:10:86400
@1.168.192.in-addr.arpa::mail.hoge.cxm:10:86400
=dog.hoge.cxm:192.168.1.1:86400
=wolf.hoge.cxm:192.168.1.2:86400
=monkey.hoge.cxm:192.168.1.3:86400
+www.hoge.cxm:192.168.1.2:86400

上記 2行目と 4行目はそれぞれ、逆引き用の SOA を宣言しているのだが、第2パラメータがカラになっている点がポイント。例えば、もしも 2行目を

.1.168.192.in-addr.arpa:192.168.1.1:ns1.hoge.cxm:259200

と書いてしまうと、「ns1.hoge.cxm のアドレスは 192.168.1.1 である」 という Aレコードが 1行目と 2行目で二度生成されてしまう。逆引き用 MX を示す 6行目にも同じことが言える。

data ファイルの用意ができたら、$ROOT/rootcd した上で、

root# make

を実行する。これによって、 tinydns が実際に読み込む data.cdb ファイルが出力される。

DNSサーバ単体の動作テスト

dig や、djbdns 付属の dnsq コマンドを使って問い合わせをしてみる。まだ /etc/resolv.conf に書いてないので、サーバを指定しなくてはならない。 dig なら;

hoge$ dig @127.0.0.1 www.hoge.cxm
hoge$ dig @127.0.0.1 ns hoge.cxm

dnsq では;

hoge$ dnsq a www.hoge.cxm 127.0.0.1
hoge$ dnsq ns hoge.cxm 127.0.0.1

DNSキャッシュ経由での動作テスト

DNSキャッシュに対して既にさんざん問い合わせテストしていた場合には、キャッシュを一旦クリアするために dnscache を再起動する;

daemontools制御の時:

root# svc -t /service/dnscache

Systemd制御なら:

root# systemctl restart dnscache

dig を使った問い合わせテスト;

hoge$ dig @192.168.1.1 www.hoge.cxm
hoge$ dig @192.168.1.1 ns hoge.cxm
hoge$ dig @192.168.1.1 www.yahoo.co.jp

dnsqr を使った問い合わせテスト;

hoge$ env DNSCACHEIP=192.168.1.1 dnsqr www.hoge.cxm
hoge$ env DNSCACHEIP=192.168.1.1 dnsqr ns hoge.cxm
hoge$ env DNSCACHEIP=192.168.1.1 dnsqr www.google.co.jp

晴れて解答が得られたら、そろそろ /etc/resolv.conf に書いてもいい頃合いだ;

search hoge.cxm
nameserver 192.168.1.1

そうしたらもう、 digdnsqr も `@192...' や `env DNSCACHEIP=...' 抜きでクエリが成功するはず。

内部 DNSサーバをプライマリとセカンダリのふたつ立て、それらを resolv.conf に書いた場合、 ひとつめから回答がないと次の DNSキャッシュに問い合わせが行われるわけだが、 Linux では、デフォルトのタイムアウトは 5秒。しかし、重要なネットワークサービスを提供するサーバ (DNS的にはクライアント) では、万が一プライマリDNSキャッシュが死んだ際に、これでは長すぎて、サーバサービスの滞りが大きいかもしれない。タイムアウトを指定するには、resolv.conf
options timeout:2
といった行を追加しておく。なお、基本中の基本だが、「回答がない」とは、本当に一切の返答がなかった時であり、 NXDOMAIN (「そんなホストやドメインないよ」) も回答のひとつなので、それは切り替わりのきっかけにはならない。

使わなくなった tinydns サービスを停めておくには [daemontools利用時]

supervise (daemontools) は、サービスディレクトリに down という名前のファイルがあるのを発見すると、そのサービスは起動させない。

root# touch /service/tinydns/down
root# touch /service/tinydns/log/down
root# svc -d -x /service/tinydns

また tinydns を使いたくなったら、down ファイルふたつを削除してから、`svc -u /service/tinydns' するかマシンを再起動すればいい。

tinydnsにおけるゾーン転送

プライマリDNSサーバとセカンダリがともに tinydns であれば、BIND のような込み入ったゾーン転送メカニズムは必要ない。 ただ単に $ROOT/root/data.cdb ファイル自体を同期させてやれば、即、データが反映されるからだ。考え方としては、プライマリDNSサーバ上で make を実行した時に、セカンダリサーバへも data.cdb のコピーが出来上がればいいわけで、これは、プライマリ側の $ROOT/root/Makefile を少し工夫するだけで簡単に実現できる。セカンダリが BIND など他の DNSサーバソフトウェアの場合は、axfrdns を使う。

Makefile修正例 (単体テキストファイルはこちら)

TINYROOT  =  /var/tinydns/root
SYNCUSER  =  dnssync
SSH      := /usr/bin/ssh -l $(SYNCUSER) -i /home/$(SYNCUSER)/.ssh/id_rsa
SECONDARY =  wolf.hoge.cxm
 
.PHONY: sync
 
sync: data.cdb
	-/usr/bin/rsync -t --rsh="$(SSH)" $(TINYROOT)/data \
	  $(SYNCUSER)@$(SECONDARY):$(TINYROOT)/ && \
	$(SSH) $(SECONDARY) "cd $(TINYROOT) && make"
 
data.cdb: data
	/usr/local/bin/tinydns-data

ここでは、data.cdb そのものを転送するのではなく、元テキストである data ファイルを転送して、相手先で make する形態を採った。「ssh をシェルに使った rsync」 と相まって、様々なメリットが得られる。

※ 相手が BIND など他の DNSサーバソフトウェアである時のために、 djbdns パッケージにはゾーントランスファサーバ axfrdns と、クライアント axfrdns-get が付属している。しかし筆者はまだ使う機会がなく未研究。

※ make は非常に面白いツールだ。プログラマにだけ使わせておくのはもったいない。上記の Makefile の内容が理解できない人は、損はないので `info make' してみるべし。 Co-op さんが日本語訳も公開している。

公開鍵を利用したパスワードなし転送(tinydnsどうし)

最後に挙げた公開鍵利用によるパスワードなしの転送を、なるべく安全に実現するひとつの方法。説明のため、プライマリDNS を動かしているマシンを dog、セカンダリDNSマシンを wolf とする。

  1. dog, wolf の両方で、転送専用のユーザ及びグループ (仮に dnssync とする) を作る。ホームディレクトリは通例通りに /home/dnssync に作らせ、シェルも殺さない。
    root# useradd -u 803 dnssync
    root# passwd dnssync
  2. 両方のマシンの dnssync のホームディレクトリに .ssh/ ディレクトリを作成。
    dnssync$ mkdir .ssh && chmod 700 .ssh
  3. プライマリサーバ上で dnssync 所有の暗号鍵ペアを作成。その際、パスフレーズは入力しない。詳細に渡る説明は sshd のページを参照のこと。
  4. 上記で作った公開鍵のほうをセカンダリサーバの ~dnssync/.ssh/ へコピーし、 authorized_keys ファイルに書き込む。
    dnssync@wolf$ cd .ssh
    dnssync@wolf$ cat id_rsa.pub >>authorized_keys
    dnssync@wolf$ chmod 644 authorized_keys
    dnssync@wolf$ rm id_rsa.pub
  5. 両サーバの /var/tinydns/root/ のパーミションを変更する。 /var/tinydns ディレクトリには元々 SETGID ビットが立っているので、それを活用し、グループdnssync に変える。セッティングを変更する必要があるのは以下の 2エントリのみ;
    /var/tinydns/root/    (root:dnssync 2775)
                    data  (root:dnssync 0664)
    要は、
    root# chgrp dnssync /var/tinydns/root /var/tinydns/root/data
    root# chmod g+w /var/tinydns/root /var/tinydns/root/data
  6. 実際に転送を行ってみる。
    dnssync@dog$ cd /var/tinydns/root
    dnssync@dog$ touch data   (実際にデータを編集したのなら要らない)
    dnssync@dog$ make

こうしておけば、プライマリでの makednssync ユーザで行っても root で行っても問題なし。なお且つ、上に紹介した Makefile なら、セカンダリへの転送と相手先での make は必ず dnssync ユーザ権限で行われる。

axfrdnsの設定(セカンダリがBINDなどの場合)

axfrdns は、セカンダリDNSサーバからのAXFRプロトコルによるDNSコンテンツの吸い上げ要求に答え、tinydns のゾーンデータを提供するデーモン。Systemd で管理する方法を示す。データを要求できる相手を限定するために、ucspi-tcptcpserver も必要だ。

ユーザの作成

tinydns と同一のユーザ/グループを使用する。

運用ディレクトリ構造の作成

axfrdns-conf コマンドを使って運用ディレクトリ構造を作成する。 1番目と 2番目の引数は、実行ユーザと、ログユーザの名前。 3番目は運用ディレクトリの基底パス。4番目に、渡すデータを採ってくるために tinydns の運用ディレクトリを指定。最後の引数は axfrdns がリッスンする IPアドレス。実のところ、Systemd 管理の場合、ログユーザは使われないし、3番目の axfrdns 運用ディレクトリ以外は、あとで環境変数ファイルで渡すので、あくまでも便宜的なもの。こういった具合にスクリプト化しておくと使い回せて楽だ。ゾーントランスファーサーバーは TCP で待ち受ける。 dnscache とポートがぶつかるので、その場合はエイリアスインターフェース等を使って別のIPアドレスを用意しておく必要がある。

#!/bin/sh
DNSROOT=/var/axfrdns
TINYROOT=/var/tinydns
LISTENIP=192.168.1.1
DNSUID=tinydns
DNSLOGUID=dnslog
DNSHARDLIMIT=500000
 
/usr/local/bin/axfrdns-conf $DNSUID $DNSLOGUID $DNSROOT $TINYROOT $LISTENIP
  
#perl -pi -e 's%\s(\./main)$% s3000000 n10 $1%;' ${DNSROOT}/log/run
 
exit 0

tcpserverルールの作成

ゾーンデータを要求できる相手を限定する機能は、tcpserver プログラムに依存している。セカンダリDNSのIPが 1.2.3.4 だとすると、次のような /var/axfrdns/tcp テキストファイルを用意する。

# sample line:  1.2.3.4:allow,AXFR="heaven.af.mil/3.2.1.in-addr.arpa"
1.2.3.4:allow
:deny

もし tinydns で複数のドメインやサブドメインを管理していてそのうち一部のみを同期許可したければ、1行目のコメントで示したように、アクション(allow)の後に AXFR変数をセットすればよい。複数指定の場合は"/"で連ねる。AXFR変数が設定されていない場合は全ドメインデータが対象となる。続いて、cdbファイルに変換してやる。axfrdns-conf で運用ディレクトリを配備した際に、Makefile が用意されるのだが、少し調整が必要だ。ルールの更新を root 権限で行う時(普通はそうだろう)、/usr/local/bin は root のPATH変数に載っていないディストリビューションが多いので、tcprules をフルパスに直してやる必要がある。diffで示すとこう。

--- Makefile.org	2019-09-06 17:20:52.383936823 +0900
+++ Makefile	2019-09-06 17:19:54.243620388 +0900
@@ -1,2 +1,2 @@
 tcp.cdb: tcp
-	tcprules tcp.cdb tcp.tmp < tcp
+	/usr/local/bin/tcprules tcp.cdb tcp.tmp < tcp

そうしておけば今後は

root# cd /var/axfrdns
root# make

で cdb が更新できる。

axfrdnsの稼働

Systemd制御の場合

サービスユニットファイル /etc/systemd/system/axfrdns.service [root:root 644] を作成する。

[Unit]
Description=A DNS zone-transfer server
Requires=network.target
After=network.target
 
[Service]
Restart=always
EnvironmentFile=/var/axfrdns/axfrdns.conf
PIDFile=/var/run/axfrdns.pid
LimitDATA=500000
ExecStart=/usr/local/bin/tcpserver -vDRHl0 -x /var/axfrdns/tcp.cdb -- ${IP} 53 /usr/local/bin/axfrdns
 
[Install]
WantedBy=multi-user.target

LimitDATA パラメータは、ulimit -d つまり Data Segment Size。axfrdns-conf によって生成される run ファイル内の 'softlimit -dxxxxx' の代わりとなるもの。

EnvironmentFile パラメータで参照している axfrdns.conf ファイルは、axfrdns プログラムへ環境変数として渡すべき値をエクスポートするためのファイルで、新規に作成する。

ROOT=/var/tinydns/root
UID=tinydns
GID=dns
IP=192.168.1.1

稼働させる。

root# systemctl daemon-reload
root# systemctl start axfrdns.service
root# systemctl enable axfrdns.service
root# systemctl status axfrdns.service

ログは標準出力を経て journalに記録される。

root# journalctl -u axfrdns -xe

axfrdnsの動作テスト

dig や、djbdns 付属の axfr-get コマンドを使って問い合わせをしてみる。その前に axfrdns への問い合わせ許可範囲を一時的に緩めてやらなければならない。tcp ファイルを例えばこのように書き換えて make

# sample line:  1.2.3.4:allow,AXFR="heaven.af.mil/3.2.1.in-addr.arpa"
1.2.3.4:allow
192.168.1.:allow
:deny

dig を使う;

hoge$ dig hoge.cxm @192.168.1.1 axfr
hoge$ dig 1.192.168.in-addr.arpa @192.168.1.1 axfr

axfr-get を使う;

hoge$ cd /tmp
hoge$ tcpclient 192.168.1.1 53 axfr-get hoge.cxm tinydata tinydata.tmp
hoge$ tcpclient 192.168.1.1 53 axfr-get 1.168.192.in-addr.arpa tinyptr tinyptr.tmp

djbdns付属ユーティリティ

dignslookup の代替品として使ったり、 DNSサーバやキャッシュをデバグするツール群について、簡単に触れておく。

dnsip FQDN [FQDN ...]
リカーシブ・クエリを行い、FQDN のアドレスを得る。環境変数 $DNSCACHEIP にセットすれば問い合わせ先の指定も可能。 FQDN は複数与えることもでき、その場合には 1行にひとつずつ出力される。
dnsip UDN [UDN ...]
`wolf' のような部分ホスト名を (可能ならば) FQDN へ変換してからリカーシブ・クエリを行い、FQDN のアドレスを得る。問い合わせ先の指定は環境変数 $DNSCACHEIP によって可能。 UDN を複数与えると 1行ひとつずつ表示。
dnsname x.x.x.x [x.x.x.x ...]
逆引きのリカーシブ・クエリを行い、x.x.x.x の FQDN を得る。問い合わせ先の指定は環境変数 $DNSCACHEIP によって可能。 x.x.x.x を複数与えると 1行ひとつずつ表示。
dnsfilter
stdin から 1行ずつ読み取り、行頭に IPアドレスらしきものがあればリカーシブ・クエリを行い、元の IPアドレスの後に `=FQDN ' を挿入した形にして stdout へ出力する。問い合わせ先の指定は環境変数 $DNSCACHEIP によって可能。
dnsmx FQDN
リカーシブ・クエリを行い、FQDN の MXレコードを得る。問い合わせ先の指定は環境変数 $DNSCACHEIP によって可能。
dnstxt FQDN
リカーシブ・クエリを行い、FQDN の TXTレコードを得る。問い合わせ先の指定は環境変数 $DNSCACHEIP によって可能。
dnsq TYPE FQDN SERVER
非リカーシブ・クエリを SERVER に行い、FQDNTYPE レコードをデバグ様式で表示する。
dnsqr TYPE FQDN
リカーシブ・クエリを行い、FQDNTYPE レコードをデバグ様式で表示する。問い合わせ先の指定は環境変数 $DNSCACHEIP によって可能。
dnstrace TYPE FQDN ROOT [ROOT ...]
FQDNTYPE レコードに対して影響のある全ての DNSサーバを ROOT ルートサーバから辿り、デバグ様式で表示する。遅いサーバや、返答のないサーバ、委譲の異常なサーバ、異常な DNSパケットに行き当たれば、そのことも表示される。基本的には DNSキャッシュが通常行うイタレーティブ・クエリ (反復クエリ) のアルゴリズムを採るが、可能な限りのあらゆる経路を試すので、出力は膨大な量となり、トレースには少なくとも数分かかる。なるべく読みやすくするには、出力を dnstracesort へパイプ渡した上でファイルへリダイレクトするといいらしい。
axfr-get DOMAIN OUTFILE OUTFILE.TMP
DOMAIN に関してゾーン転送プロトコルで受け取った入力を OUTFILE.TMP に書き出し、成功すればそれを OUTFILE にリネームする。書き出されるのは tinydns のデータ形式。処理はあくまでもディスクリプタからの入出力で、自分でネットワーク接続する機能はないので tcpclient と併用する。axfrdnsの動作テスト に例。