仙石浩明の日記

2010年3月8日

tinydns のゾーンを MyDNS へ反映させるスクリプトを書いてみた

MyDNS というのは MySQL (あるいは PostgreSQL) のレコードを、 そのままゾーンレコードとして扱えるネームサーバ。 私は MyDNS を使って実験的にダイナミックDNSサービスを (もちろん無償で) 提供している。 実験と言いつつ、 サービス開始以来 2年以上安定的に継続できているし、 先月から海外レンタルサーバを使って地域分散したので、 仮に私の自宅の回線が切れてもネームサーバが見えなくなることはない。

gcd.jp のマスタネームサーバである ns.gcd.jp と、 スレーヴネームサーバである ns2.gcd.jp (海外サーバ fremont.gcd.org の別名。 名前の通りカルフォルニア州フリーモントにある) とは、 MySQL のレプリケーションによってゾーンデータを同期させている。 したがってマスタ側での変更が即座にスレーヴ側に伝わる。

ちなみに、 ネームサーバ間の同期でよく利用されるゾーン転送 (AXFR) は、 ゾーンデータを丸ごと転送するので (ゾーンが大きくなってくると) 同期頻度を高くすることが難しく、 ダイナミックDNSサービスにはあまり向いていない。 MySQL のレプリケーションでネームサーバ間の同期が行なえてしまう MyDNS は、 ダイナミックDNSサービス向きと言える。

ところが gcd.org では (ダイナミックではない) 普通のネームサーバ ns1.gcd.org も運用していて、 これは tinydns を利用している。 せっかく海外にサーバ fremont.gcd.org を借りたのだから、 tinydns で管理しているゾーンも fremont.gcd.org で引けるようにしたいが、 fremont に複数 IP アドレスを付与するのはお金がかかる (月額 $1 追加) ので、 fremont で MyDNS と tinydns の両方を走らせるわけにもいかない。

私が借りてるレンタルサーバ (正確に言うと VPS) Linode は、 もともと DNS サービス (マスタ/スレーヴどちらでも、ドメインいくつでも) を無料で利用できるので、 DNS のためだけに 1IP 追加する気にはちょっとなれない。

もちろん、 tinydns を止めてしまって全てのゾーンを MyDNS へ移行してしまうという解決策も無くはないのだが、 MyDNS に全面的に依存してしまうのは恐い気もする。 できれば tinydns 側はいじらずに、 tinydns のゾーンデータを MyDNS 側へ反映させる仕掛けを作りたい。

というわけで、 tinydns のゾーンデータを読み込み、 MyDNS のゾーンデータの形式で MySQL へレコードを書込む perl スクリプト tinydns2mydns を書いてみた (CVSリポジトリ)。

なお MyDNS には、 mydnsimport という外部からゾーンデータを取り込むツールが付属していて、 tinydns のゾーンデータもインポートできるのだが、 以下の欠陥があってゾーン転送目的には使えない:

  1. ゾーンの全レコードをいったん削除した上でインポートを行なう実装になっている。
  2. SRV レコードをインポートできない。

1. tinydns 側で新しいレコードを追加したとき、 対応するレコードのみを DB に INSERT できる実装になっているかと思いきや、 ソースを確認してみると、 いったん全部 DELETE した後 tinydns のゾーンデータを 1レコードずつ INSERT する実装になっていた。

つまり、 短い時間とはいえ、 ゾーンにレコードが存在しないタイミングが存在する。 したがって、 その瞬間に届いたクエリに対して非存在 (NXDOMAIN) を回答してしまう。 mydnsimport のソースをいじって、 DELETE してから INSERT 完了まで読み込みをロックするようにすれば、 NXDOMAIN を返すこと自体は回避可能だが、 ネームサーバとしての性能を大いに損ねてしまう。

2. tinydns には A, AAAA, NS, CNAME, PTR, MX, TXT 以外の任意のタイプのレコードを定義する方法として、 タイプを数値で指定する汎用記法がある:

:fqdn:n:rdata:ttl:timestamp:lo

Generic record for fqdn. tinydns-data creates a record of type n for fqdn showing rdata. n must be an integer between 1 and 65535; it must not be 2 (NS), 5 (CNAME), 6 (SOA), 12 (PTR), 15 (MX), or 252 (AXFR). The proper format of rdata depends on n. You may use octal \nnn codes to include arbitrary bytes inside rdata.

この記法を利用して、 例えば以下のように SRV レコードを定義できる。

:_jabber._tcp.jabber.jp:33:\000\012\000d\024\225\006jabber\002jp\000:300

つまり fqdn が 「_jabber._tcp.jabber.jp」、 n が 「33」 で SRV の意味。 rdata はタイプによってデータの意味が変わってくるが、 SRV の場合は次のような構造になる:

┌─┬─┬─┬─┬─┬─┬─┬─≫┬─┬─┬─┬─≫─┬─┬─┐
│優先度│ 重み │ポート│長│ホスト名│長│ドメイン名……│0│
└─┴─┴─┴─┴─┴─┴─┴─≪┴─┴─┴─┴─≪─┴─┴─┘

つまり先頭から 2バイトずつ 3つの数値 (ネットワークバイトオーダ) を表わし、 複数個の文字列 (先頭に 1バイトの文字列長) が続いて最後に 0。 前述の SRV レコードの場合であれば、 優先度は 「\000\012」 つまり 8進数で 000 012 だから 10進数だと 10 になる。 以下同様に重みが 100 で、 ポート番号が 5269 で、 続く文字列が 「jabber」 と 「jp」 でホスト名 jabber.jp を意味する。

 優先度  重み  ポート ホスト名
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│00 0A│00 64│14 95│06│j│a│b│b│e│r│02│j│p│00│
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
  10   100   5269      6 文字         2 文字

ゾーンファイルの書式で書けば次のようなレコードになる:

_jabber._tcp.jabber.jp.        300        IN        SRV        10 100 5269 jabber.jp.

つまり、 ドメイン jabber.jp における TCP/IP の jabber サービスは、 ホスト jabber.jp の 5269 番ポートにアクセスすればよい、 ということを表わす。 同じサービスを提供するホスト・ポートが複数ある場合、 優先度の数値が (アクセス可能なものの中で) 最小のものに接続することが求められる (MUST)。 同じ優先度のホスト・ポートが複数ある場合は、 重みの比率に応じてアクセスを振り分けるべき (SHOULD)。

tinydns2mydns スクリプトにおいて SRV レコードを変換する部分は次のようになっている:

    # :fqdn:n:rdata:ttl:timestamp:lo
    elsif ($top eq ':') {
        my ($fqdn, $n, $rdata, $ttl, $timestamp, $lo) = @_;
        my $domain = &get_domain($fqdn);
        next unless defined $domain;
        my $name = &get_host($fqdn, $domain);
        if ($n == 33) {
            $rdata =~ s/\\(\d\d\d)/sprintf("%c", oct($1))/ge;
            my ($priority, $weight, $port, @target)
                = unpack("nnn(C/a)*", $rdata);
            &record($sth_insert, $sth_update,
                    $Zones{$domain}, $name,
                    'SRV', "$weight $port " . join(".", @target),
                    $priority, $ttl, $lo);
        }
    }

眺めるだけで処理が見えてくる見通しのよいスクリプトだと思う (自画自賛 ;)。 $n が 33 のとき SRV レコードだから 続く $rdata を読み込んで、 8進数表記 \nnn をバイトデータへ変換し、 「優先度」 「重み」 「ポート」 および複数個の文字列に分解する:

$rdata =~ s/\\(\d\d\d)/sprintf("%c", oct($1))/ge;
my ($priority, $weight, $port, @target) = unpack("nnn(C/a)*", $rdata);

C で書けば何十行にもおよぶであろうこの処理をわずか 2行で書けてしまう perl はスゴイと改めて思う。 あとは分解したデータを MyDNS のレコードの形式に組み立てるだけ:

$Zones{$domain}, $name, "$weight $port " . join(".", @target), $priority, $ttl

mydnsimport のソースが C で 2000行近くもある (MyDNS ライブラリ群のソースを除く) のに対し、 tinydns2mydns はわずか 353行で書けてしまい、 しかも上記 mydnsimport にある欠陥を解決できている。 現状では tinydns の汎用記法のうち SRV しか対応していないが、 スクリプトの見通しがよいため他タイプに対応させるのも容易だろう。

tinydns2mydns スクリプトが DB に書込んだレコードは以下のようになる:

mysql> select * from rr where name='_jabber._tcp';
+-----+------+--------------+------+---------------------+-----+-----+
| id  | zone | name         | type | data                | aux | ttl |
+-----+------+--------------+------+---------------------+-----+-----+
| 817 |   13 | _jabber._tcp | SRV  | 100 5269 jabber.jp. |  10 | 300 |
+-----+------+--------------+------+---------------------+-----+-----+
1 row in set (0.00 sec)

外部のサイトから jabber.jp の SRV レコードを問合わせてみると、 fremont も正しいレコードを返すことが確認できる:

% host -t srv _jabber._tcp.jabber.jp fremont.gcd.org
Using domain server:
Name: fremont.gcd.org
Address: 74.207.241.21#53
Aliases: 

_jabber._tcp.jabber.jp has SRV record 10 100 5269 jabber.jp.

% host -t srv _jabber._tcp.jabber.jp
_jabber._tcp.jabber.jp has SRV record 10 100 5269 jabber.jp.
Filed under: プログラミングと開発環境 — hiroaki_sengoku @ 09:25

No Comments »

No comments yet.

RSS feed for comments on this post.

Leave a comment