□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);
?>