□PowerDNSのBackendをつくってみる
□PowerDNSのBackendをつくってみる
PowerDNSといえば、シェアこそ少ないものの、かなり大規模なシステムで使われているDNSです。(MyDNS.JPでも使っています)
さまざまなSQLデータベースと直結しているので、SQLができる人ならDNS情報の管理がしやすいという利点があります。
また、データベースと連携して地理的ロードバランス(Global Server Load Balancing:GSLB、Geo-IPとかGeoLocation-IPとかいったりしますね。)ができる、ということでも有名かもしれません。
GSLBの実装としてGeoBackendというバックエンド処理があるのですが、これ自体は期待するほど使えません。
(期待してがっかりした本人が言うのですから間違いない!?)
GeoBackendは特定のというか一つのドメインでのみGSLBができ、実際にIPv4アドレスの地理情報を提供しているところの情報を使い、あらかじめ指定した国のみ、そのIPからの問い合わせには、指定した国別コード(アルファベット2文字)をつけたCNAMEを返す、というものです。
例:
emacs /etc/powerdns/pdns.conf
----------
launch=geo,gpgsql
:
:
#
# GeoBackend Setting
#
query-cache-ttl=0
cache-ttl=0
geo-zone=geo.domain.com
geo-soa-values=ns0.domain.com,hostmaster@domain.com
geo-ns-records=ns0.domain.com,ns1.domain.com
geo-ip-map-zonefile=/usr/src/zz.countries.nerd.dk.rbldnsd
geo-maps=/etc/powerdns/geo-maps
----------
emacs /etc/powerdns/geo-maps/geo.domain.com
----------
$RECORD www
$ORIGIN geo.domain.com.
# default
0 other
# 127.0.1.136 -> 392
392 japan
# 127.0.2.190 -> 702
702 singapore
# 127.0.2.16 -> 528
528 netherlands
# 127.0.3.72 -> 840
840 usa
----------
とすると、この設定をしたPowerDNSに「www.geo.domain.com」を問い合わせると、問い合わせをしたマシンのIPv4アドレスの所在地により、日本から(と/usr/src/zz.countries.nerd.dk.rbldnsdに定義されている)なら
japan.www.geo.domain.com
がCNAMEとして返るので、別途本来のPowerDNSに「japan.www.geo.domain.com」のAレコードを指定しておけば、それが問い合わせ元に返る、ということになります。
上記の場合には「launch=geo,gpgsql」としているので、このgeobackendでCNAMEまで判明したら、gpgsqlbackend:つまりPostgreSQLの中の情報を検索する、ということになります。
これだけなら「おおっ、すごい」と思うのですが、「geo-zone=geo.domain.com」と書いてしまっているので、このドメインでしかGSLBができません。また「/usr/src/zz.countries.nerd.dk.rbldnsd」自体が国別でしか情報を返してくれないので、そこまで細かい設定はしなくていいんだけど、ということにもなります。
だってMyDNS.JPは日本とシンガポールとオランダとアメリカにしかサーバーがないんだもん。
ではPowerDNSをつかって複数のドメインでGSLBを実現するにはどうするか?
これはもうGeoBackendを改造するか、GeoBackendとは別のバックエンドを作るしかありません。
で、そのバックエンドのサンプルをPHPで作ってしまいました。
最初はC++でバックエンドそのものを作るか?それともCでソケット通信でもしてやり取りするようにするか?とか思っていたのですが、あれこれしながらドキュメントを読んでいると「pipebackendで納得いかない場合には自分でバックエンドを作れ」みたいな事が書いてありました。
→ http://doc.powerdns.com/html/backend-writers-guide.html
で、サンプルのPerlスクリプトを見ながら、PHPで同じようなものを書いてみました。
「PerlからPHPにしただけかい!!」といわれると…まぁこれはそうなんですが、実際にはこれにさらに味付けをすることで、MyDNS.JPで大陸別のGSLBを標準実装する予定ですので、もうちょっとお待ちください。
#!/usr/bin/php <? // ------------------------------------------------------------ // // GeoLocBackend // // Ver.01.00 2014.06.20 思い立って少し作ってみる // // Program: // Takeshi Kaburagi/JS1FVG disco-v8 @ 4x4.jp // // Usage: // // Edit /etc/powerdns/pdns.conf // // ---------- // launch=pipe,gpgsql // pipe-command=/etc/powerdns/pipebackend.php // pipebackend-abi-version=1 // ---------- // // /etc/init.d/pdns-server restart // // http://doc.powerdns.com/html/backends-detail.html#pipebackend // // 1) HELO 1が飛んできたらOKを返すとPIPEBackend処理が開始 // 2) PowerDNSから標準入力で上記URLにあるクエリが飛んでくる。 // →デバッグモードをON(=1)にして出力するとわかるが、しつこくバラバラにしていろいろなタイプのクエリが飛んでくる // 3) 必要に応じて処理をして返したいデータを標準出力に出して、ENDを最後に送信して終わり // // ※launchに書いた順番でDNS情報が検索される、pipe(この処理)で情報が見つからないと、gpgsqlに探しに行く、ということ // ※pipebackend-abi-versionによってHELO 1の数字が変わる、と同時に送られてくるクエリのフォーマットも増えるし、返し方も変わるので注意 // ------------------------------------------------------------ ?> <? // ---------------------------------------------------------------------- // Init Routine // ---------------------------------------------------------------------- // 環境変数を設定する // タイムゾーン //date_default_timezone_set('Asia/Tokyo'); date_default_timezone_set(@date_default_timezone_get()); // 内部文字コード mb_internal_encoding('UTF-8'); // 出力文字コード mb_http_output('UTF-8'); // デバッグ時のログファイル名 $LOG_FILE = '/var/tmp/pipebackend.log'; // 動作モード $MODE = 0; // デバッグモード $DEBUG_MODE = 1; ?> <? // ---------------------------------------------------------------------- // Sub Routine // ---------------------------------------------------------------------- ?> <? // -------------------- // LOGGING // -------------------- function log_write($LOG_STR) { global $LOG; global $LOG_FILE; // ログファイルポインタが開かれていないなら if (!$LOG) { $LOG = fopen($LOG_FILE, 'a'); } // ログファイルポインタが開かれているなら if ($LOG) { // ログファイルを排他的ロック if (flock($LOG, LOCK_EX)) { // ログファイルの一番最後までシーク fseek($LOG, 0, SEEK_END); // ログファイルに書き出し fprintf($LOG, $LOG_STR); // ログファイルをアンロック flock($LOG, LOCK_UN); } } } ?> <? // -------------------- // メイン処理 // -------------------- while(($QUERY = fgetcsv(STDIN, 2048, "\t")) !== FALSE) { // デバッグモードがON(=1)なら if ($DEBUG_MODE == 1) { log_write("----------------"."\n".'['.date("Y/m/d H:i:s").']'."\n"."PARAM COUNT = ".count($QUERY)."\n"); // クエリを全て書き出し foreach ($QUERY as $VALUE) { log_write($VALUE."\n"); } } // 接続状態なら if ($MODE == 1) { // パラメータが6個ないなら if (count($QUERY) < 6) { // ネゴシエーション完了、通常動作モード(=1)に $MODE = 1; log_write("MODE = ".$MODE."\n"); fprintf(STDOUT, "LOG\tPowerDNS sent unparseable line!?"."\n"); fprintf(STDOUT, "FAIL"."\n"); continue; } // クエリがAかCNAMEかANYの場合にのみ、CNAMEが返せる if (count($QUERY) == 6 && $QUERY[0] == "Q" && ($QUERY[3] == "A" || $QUERY[3] == "CNAME" || $QUERY[3] == "ANY") && $QUERY[1] == "www.example.com") { log_write("DATA = ".$QUERY[1]." -> "."geolocip.".$QUERY[1]."\n"); fprintf(STDOUT, "DATA"."\t".$QUERY[1]."\t".$QUERY[2]."\t"."CNAME"."\t"."300"."\t"."-1"."\t"."geolocip.".$QUERY[1]."\n"); } // ここに、受けたクエリ別にPowerDNSに返したいデータを作る処理を書いていけばよい // このBackendでできることはこれで終わり fprintf(STDOUT, "END"."\n"); } // 接続状態でないなら else if ($MODE == 0) { // ネゴシエーション文字列なら if (count($QUERY) == 2 && $QUERY[0] == "HELO" && $QUERY[1] == "1") { // ネゴシエーション完了、通常動作モード(=1)に $MODE = 1; log_write("MODE = ".$MODE."\n"); fprintf(STDOUT, "OK\tPIPE backend firing up"."\n"); } } } fclose($LOG); ?>