仙石浩明の日記

システム構築・運用

2010年2月25日

本ブログのサーバを海外レンタルサーバ Linode を使って冗長化・地域分散してみた

前回前々回に書いたように、 このブログ 「仙石浩明の日記」 (および 「仙石浩明CTO の日記」) は、 私の自宅にある PC サーバで動かしている。 2台の PC からなる冗長構成になっていて、 10年以上の安定稼働実績がある (両サーバ共に停止したことは皆無) が、 回線が (家庭用の) フレッツ光ネクストなので、 インターネットとの接続が切れてしまう可能性は皆無ではない。 実際、 Bフレッツからフレッツ光ネクストへ切り替えたときは 40分間ほど切れてしまった。 一応バックアップ回線として ADSL 回線も契約しているが、 こちらは固定 IP アドレスを割当てていないので WWW サービス用としては使いにくい。

短時間とはいえブログが見えないようなことがあると、 たまたまそのタイミングで (検索エンジン等で見つけて) 訪れた人が、 ブログが既に閉鎖してしまったものと勘違いしてしまうかもしれない。 ここは一つ自宅以外の場所にもサーバを立てて、 回線断の影響を受けないようにしたいところ。 海外のサーバなら地域分散もできてなおよい。

道楽で運営している自宅サーバにそこまで可用性を求めるのは、 やりすぎの感もあるが、 近頃流行りの VPS (Virtual Private Server, 仮想専有サーバ) は、 月額 $20 (初期費用無し) 程度で利用できるらしい。 $20 なら試しに使ってみるのも悪くない。

サービス
プラン名
メモリ
ディスク
帯域
追加額/単位
月額
年額
データセンタの
場所
ServerAxis Xen
VS000.5G-0025.0GP
512MB
25GB
0GB
$.05/GB
$15
$180
Chicago, IL
Linode
Xen 360
360MB
16GB
200GB
$10/100GB
$19.95
$215.46
Fremont, CA
Dallas, TX ...
VPSLink
Xen VPS Link-3
256MB
10GB
300GB
$.50/GB
$19.95
$201.12
Seattle, WA
New York, NY
slicehost
Xen VPS 256 slice
256MB
10GB
150GB
$.30/GB
$20
$216
Dallas, TX
St. Louis, MO

VPSLink 以外は最安のプランで比較した。 VPSLink には、 メモリ 64MB で月額 $7.95 の Link-1 プラン、 メモリ 128MB で月額 $13.95 の Link-2 プランもあるが、 256MB 未満のメモリではできることも限られてしまうので比較対象から外した。

月額基本料だけを比較すると、 ServerAxis が格安に見えるが、 これは帯域課金が含まれていないためで、 例えば 100GB (送受信の合計バイト数) 使うと $5 加算されて $20 と他社並になる。

データセンタの場所 (というかネットワーク上での位置) によって遅延時間が変わってくるので、 どこのデータセンタを使っているかも重要。 例えば同じ Linode でもデータセンタによって、 日本からアクセスしたときの RTT (Round Trip Time, 往復にかかる時間, ミリ秒) が倍以上変わってくる。 それぞれ 10回ずつ ping を打って、 RTT の最小/平均/最大を比較してみる:

データセンタの場所ホスト名 最小平均最大
Fremont, Californiafremont1.linode.com 121.341132.299146.939
Dallas, Texasdallas1.linode.com 171.049173.152174.905
Atlanta, Georgiaatlanta1.linode.com 183.184185.938190.174
Newark, New Jerseynewark1.linode.com 200.629203.053204.755
London, Englandlondon1.linode.com 269.324273.499276.822

米国西海岸だと RTT は 130msec 程度で済むが、 東海岸は 200msec を超えてしまい、 英国はもっと遠い。 西海岸にデータセンタがあるのは Linode と VPSLink だが、 Linode がデータセンタの情報を詳細に公開しているのに対し、 VPSLink は、 Spry Hosting のシアトルのデータセンタを使用している、 ということ以上の情報を公開していない (しかもこのページは 2006年8月の内容のまま) のが少し気になる。

サポートしている Linux ディストリビューションは、 各社だいたい共通で、 CentOS, Debian, Gentoo, Ubuntu の各バージョンが利用できるが、 VPSLink Ubuntu Plan のページに Ubuntu 9.10 Karmic が書かれていない点も、 ちょっと気になった (ちゃんとポリシーを持って 9.10 をサポートしないのならいいのだが、 どうもそうではなく単に運用の手を抜いているだけのような気配がする)。 Web 界隈での評判も Linode のほうがよさげだったので、 Linode を使ってみることに決めた。

サポートしているディストリビューションのバージョンが古くてもアップグレードできるのだろうが、 帯域で課金されるので最初から新しいバージョンをインストールできる方が好ましい。

サービス CentOSDebianGentooUbuntu Fedoraその他
ServerAxis 5.45.02008.09.10- Mandriva SUSE
Linode 5.35.02008.09.1011 Arch SUSE Slack
VPSLink 55O9.0411 Arch Slack
slicehost 5.45.02008.09.1012 Arch RedHat

おそらく各社ともカーネルも入れ替えることができるのだと思うが、 Linode 以外は使っていないので未確認。 Linode の場合は pv_ops (paravirtualization) を有効にしておけば任意のカーネルを利用できる。 したがって (帯域課金さえ気にしなければ ;-) 任意のディストリビューションの利用 (あるいは完全なカスタマイズ) が可能 (私はまだそこまではやっていない)。

なお、VPS サービスは探すといろいろあるようだ。 前述した 4社と比べると公開している情報が不十分 (特に Virtuozzo/OpenVZ な VPS は実際のパフォーマンスを予想しにくく、 実地に使ってみないとなんとも...) なので私は比較検討対象から外したが、 ダメ元で使ってみるのも面白いかもしれない:

サービス
プラン名
メモリ
ディスク
帯域
追加額/単位
月額
年額
データセンタの
場所
SplitServ
Xen 1024
1024MB
15GB
200GBs
$8/400GB
$14.95
$149.50
Los Angeles, CA
Kansas City, MO
WebKeepers
Virtuozzo ライト
512MB
50GB
?1280円
11760円
?
PhotonVPS
Xen VPS WARP 1
512MB
35GB
500GB
?
$16.95
$203.40
Los Angeles, CA
KnownHost
Virtuozzo VS2
512MB
30GB
750GB
$40/100GB
$35
$400
Los Angeles, CA
Dallas, TX
BurstNet
VPS PACKAGE #1
512MB
20GB
1000GB
$25/200GB
$5.95
$71.40
Scranton, PA
VPS NOC
OpenVZ Bronze
512 MB
20 GB
400 GB
?
$12.95
$155.40
Kansas City, MO
CoreNetworks
CoreMR
512MB
12GB
500GB
?
$19.95
$239.40
?
prgmr.com
Xen VPS
512MB
12GB
80GB
?
$12
$115.20
San Jose, CA
HostCadet
VPS Micro
512MB
10GB
400GB
?
$9.99
$119.88
Colorado Springs, CO
NY NOC OpenVZ
Super VPS #1
512MB
10GB
1000GB
?
$10.00
$120
Chicago, IL
North Bergen, NJ
ARP Networks
KVM/QEMU
512MB
10GB
100GB
?
$15
$180
Los Angeles, CA
VirtuallyDedicated
Xen VX384
384MB
24GB
300GB
?
$10.99
$131.88
Chicago, IL
Scranton, PA
GrokThis.net
Xen VPS
320MB
16GB
160GB
?
$20
$200
Philadelphia, PA
DMEHosting
OpenVZ VPS1
256MB
25GB
1000GB
$10/200GB
$5.95
$71.40
Denver, CO
Chicago, IL
Quantact
OpenVZ VS1
256MB
15GB
300GB
$1/GB
$14.99
$179.88
Santa Rosa, CA
RackspaceCloud
Xen CloudServers
256MB
10GB
0GB
$.08-$.22/GB
$10.95
$131.40
Dallas, TX
XENnode
XEN 256
256MB
10GB
200GB
?
$14.99
$143.99
Dallas, TX
Datarealm Xen
PowerVPS
256MB
10GB
1Mbps
Unmetered
$19.95
$215.52
?
HostVirtual Xen
XV0 Server
256MB
10GB
250GB
?
$14.95
$179.40
San Jose, CA
RackUnlimited
Xen RVPS-Starter
256MB
10GB
100GB
?
$8
$96
Asheville, NC
QuickWeb
Xen Lite
256MB
5GB
240GB
?
$17.95
$215.40
San Jose, CA
QuickVPS
Xen プラン1
256MB
10GB
50GB
?
2300円
21600円
日本国内
QuillHost
OpenVZ Standard
256MB
8GB
150GB
?
$9.99
$119.88
Washington, DC
Charlotte, NC

と、いうわけで、 Linode 360 を契約してみた。

More...
Filed under: システム構築・運用 — hiroaki_sengoku @ 08:58
2010年2月10日

CTO日記も livedoorブログから WordPress へ引越しました (URL は変更なし)

「仙石浩明の日記」 に続いて、 「仙石浩明CTO の日記」 も先週末に livedoorブログから自宅サーバへ引っ越した (つまりネームサーバの設定を変更して切替。有料プランの解約はこれから)。 もともと両ブログは相互にリンクを張って密接に連係していたので、 引越を機会に両者を統合した。

統合といっても両ブログは微妙(?)に読者層が異なると思われるし、 何よりページの体裁が大きく変わってしまっては読者の方々を戸惑わせてしまうので、 CTO日記を 「仙石浩明の日記」 の一カテゴリという位置付けにして、 かつページの体裁は WordPress のテーマを切り替えることによって、 どちらのブログもあまり大きな変化がないようにしている。

「仙石浩明CTO の日記」 http://sengoku.blog.klab.org/ をアクセスすると、 次のような PHP スクリプトを走らせた上で、 WordPress を呼び出す (末尾の require 文):

<?php
$new = NULL;
if ($_SERVER['REQUEST_URI'] == "/") {
    $new = "/blog/category/cto/";
} elseif ($_SERVER['REQUEST_URI'] == "/feed/") {
    $new = "/blog/category/cto/feed/";
} elseif (preg_match('@^/\d+/\d+/\d+/@',
		     $_SERVER['REQUEST_URI'], $matches)) {
    $new = $_SERVER['REQUEST_URI'];
...(中略)...
}
if ($new) {
    ...(中略)...
    $ORIG_SERVER_NAME = $_SERVER['SERVER_NAME'];
    $host = "www.gcd.org";
    $_SERVER['SERVER_NAME'] = $host;
    $_SERVER['REQUEST_URI'] = $new;
    $_SERVER['SCRIPT_NAME'] = $new;
    $_SERVER['PHP_SELF'] = $new;
    $abspath = "/usr/local/www/wordpress/";
    $themepath = "${abspath}wp-content/themes/sengoku_cto/";
    define('WP_USE_THEMES', true);
    define('TEMPLATEPATH', $themepath);
    define('STYLESHEETPATH', $themepath);
    require("${abspath}wp-blog-header.php");
...(中略)...
}
?>

つまり http://sengoku.blog.klab.org/ へのアクセスは、 パス名に 「/category/cto/」 を追加することによって、 CTO日記カテゴリへのアクセスに変換する。

ページの体裁については、 「wp-content/themes/sengoku_cto/」 ディレクトリが、 CTO日記のテーマフォルダで、 二つの PHP 定数 TEMPLATEPATH と STYLESHEETPATH をこのディレクトリへ設定することによって、 テーマの切り替えを行なっている。

テーマフォルダの中にあるテーマ関数ファイル 「functions.php」 は、 WordPress の初期化中に読み込まれるので、 ここに PHP スクリプトを書いておくことによって WordPress の挙動を変更することができる。 例えばブログのタイトルを 「仙石浩明CTO の日記」 に変更するには、 以下のスクリプトを functions.php に追加しておけばよい:

function option_blogname_cto() {
    return '仙石浩明CTO の日記';
}
add_filter('pre_option_blogname', 'option_blogname_cto');

つまり、 pre_option_blogname フックに、 option_blogname_cto フィルタを登録する。

WordPress では、 ブログのタイトルなど各種オプションの設定値 (DB に格納している) を、 get_option($setting) 関数を呼び出すことで参照している。 例えばタイトルは get_option('blogname') を呼び出すことで得られ、 URL は get_option('home') で得られる。

get_option($setting) 関数は wp-includes/functions.php で定義されていて、 以下のようにフィルタフック pre_option_* が定義されている:

function get_option( $setting, $default = false ) {
    global $wpdb;

    // Allow plugins to short-circuit options.
    $pre = apply_filters('pre_option_' . $setting, false);
    if ( false !== $pre )
	return $pre;
    ...(中略)...
}

つまり、 「pre_option_設定名」 というフックに登録されたフィルタが値を返すなら、 get_option はオプションの設定値ではなくフィルタが返した値を返すようになる。 前述の例なら、 「pre_option_blogname」 フックに登録された 「option_blogname_cto」 フィルタが 「仙石浩明CTO の日記」 という値を返すので、 get_option('blogname') も 「仙石浩明CTO の日記」 という値を返すようになり、 結果としてブログのタイトルを変更できる、というわけ。

ただし、 前述したように CTO日記は 「仙石浩明の日記」 の一カテゴリという位置付けなので、 ブログのタイトルを変更しただけだと、 ブログ 「仙石浩明CTO の日記」 の 「仙石浩明CTO の日記」 カテゴリということで、 ページのタイトル等が 「仙石浩明CTO の日記 » 仙石浩明CTO の日記」 という冗長なものになってしまう。 そこで、 以下のようなスクリプトを 「functions.php」 に追加して、 タイトルとカテゴリ名が同じときはカテゴリ名が表示されないようにする:

function single_cat_title_cto($category_name) {
    $name = get_option('blogname');
    if ($category_name == $name) return "";
    return $category_name;
}
add_filter('single_cat_title', 'single_cat_title_cto');

single_cat_title フックは、 wp-includes/general-template.php で定義されていて、 ページのタイトルなどに表示されるカテゴリ名を変更することができる。

以上で、 「仙石浩明の日記」 の一カテゴリを 「仙石浩明CTO の日記」 の体裁で見せることができるようになる。 しかし、 元が 「仙石浩明の日記」 であるだけに、 リンク先が全て 「仙石浩明の日記」 のページになってしまう。 例えば、 「仙石浩明CTO の日記」 のトップページの一番下に、 「古い投稿 »」 というリンクがあるが、 このリンク先が https://www.gcd.org/blog/page/2/ になってしまい、 たどると 「仙石浩明の日記」 のトップページの 2ページ目へ遷移してしまう。

また、 本文中 (あるいはサイドバー) に現れるリンクも、 DB のデータは 「仙石浩明の日記」 のパーマリンクを用いているので、 たとえそれが 「仙石浩明CTO の日記」 カテゴリに含まれていても、 そのリンクをたどると 「仙石浩明の日記」 の記事として表示されてしまう。

そこで、 遷移先も 「仙石浩明CTO の日記」 として表示したいリンクを、 フィルタで書き換えることにした。 つまり DB のデータは 「仙石浩明の日記」 へのリンクのままで、 ブラウザに送信する前に都度書き換える。

対象となるリンクは、 記事本文中だけでなく、 前述したページナビ 「古い投稿」 「新しい投稿」 や、 サイドバー (「人気記事」 や 「最近の投稿」) にも現れる。 ページ丸ごと (つまり HTTP レスポンス丸ごと) HTML を書き換えられるフックがあるとよかったのだが、 残念ながらそういうフックは定義されていないようだ。 以下のフックそれぞれについてリンクを書き換えればよさげ:

フィルタフック フィルタが変更できる対象, 第2引数, ...
the_content 記事本文 HTML
the_category 記事の末尾に表示されるカテゴリーリストの HTML,
$separator, $parents
get_pagenum_link ページ末尾に表示されるページナビ 「古い投稿」 「新しい投稿」 の URL
post_link 記事の URL (パーマリンク), $post, $leavename
widget_text サイドバーに表示されるテキストウィジェットの HTML, $instance
wp_list_categories サイドバーに表示されるカテゴリーのリストの HTML
category_feed_link カテゴリーの RSSフィードの URL, $feed

書き換え対象のリンクを決めるために、 まず CTO日記カテゴリに属す記事の ID を取得する:

function setup_cto_id() {
    global $wpdb;
    global $is_cto_id;
    $result = $wpdb->get_results("SELECT object_id FROM wp_term_relationships WHERE term_taxonomy_id=17", ARRAY_N);
    foreach ($result as $row) {
	$is_cto_id[$row[0]] = 1;
    }
}

あるカテゴリに属す記事ID のデータを取得する関数など、 WordPress に含まれているんじゃないかと探してみたのだが、 見つからなかったので DB に問合わせて取得するようにしてみた。 毎回 DB アクセスが発生してしまうが、 キャッシュとかはアクセス数が増えてから考える (^^;)。 「term_taxonomy_id=17」 が CTO日記のカテゴリ (決め打ち ^^;)。 CTO日記カテゴリに属す記事は、 配列 $is_cto_id[記事ID] に 1 を代入しておく。

次に URL を書き換えるスクリプト:

function replace_URL_cto($matches) {
    global $is_cto_id;
    global $ORIG_SERVER_NAME;
    if (!is_array($is_cto_id)) {
	setup_cto_id();
    }
    if (is_null($matches[2])) {
	if ($matches[1] == "category/cto/") return "http://$ORIG_SERVER_NAME/";
	return "http://$ORIG_SERVER_NAME/$matches[1]";
    } elseif ($is_cto_id[$matches[2]]) {
	return "http://$ORIG_SERVER_NAME/$matches[1]";
    }
    return $matches[0];
}

function convert_URLs_cto($text) {
    $textarr = preg_split("/(<.*>)/U", $text, -1, PREG_SPLIT_DELIM_CAPTURE);
    $stop = count($textarr);
    for ($i = 0; $i < $stop; $i++) {
	$content = $textarr[$i];
	if (strlen($content) > 0) {
	    $content = preg_replace_callback(
		    '@https://www.gcd.org/blog/(\d+/\d+/(\d+)|category/cto/)@',
		    'replace_URL_cto', $content);
	}
	$output .= $content;
    }
    return $output;
}

顔文字を画像に変換して表示するフィルタ wptexturize (wp-includes/formatting.php で定義されている) を参考にさせてもらった。 preg_replace_callback() を使って書き換え対象リンクを探し、 replace_URL_cto で記事ID がCTO日記カテゴリに属す ($is_cto_id[$matches[2]] が TRUE) 場合のみ書き換える。

最後に、 この書き換えフィルタ replace_URL_cto を前述したフィルタフックに追加:

add_filter('the_content', 'convert_URLs_cto');
add_filter('the_category', 'convert_URLs_cto');
add_filter('get_pagenum_link', 'convert_URLs_cto');
add_filter('post_link', 'convert_URLs_cto');
add_filter('widget_text', 'convert_URLs_cto');
add_filter('wp_list_categories', 'convert_URLs_cto', 12000);
add_filter('category_feed_link', 'convert_URLs_cto');

wp_list_categories にフィルタを追加すると Category Order プラグインと衝突するので、 優先順位を下げて Category Order プラグインの後で実行されるようにしている。

また、 the_category, post_link, widget_text, category_feed_link 各フックは、 2つ以上の引数を持つが、 第1引数 (書き換え対象の HTML あるいは URL) のみ使用するので引数の数 (add_filter の第3引数) を省略している。

Filed under: システム構築・運用,プログラミングと開発環境 — hiroaki_sengoku @ 07:46
2010年1月28日

本ブログを livedoorブログから WordPress へ引越しました (URL は変更なし)

このブログ 「仙石浩明の日記」 は、 今まで livedoorブログの有料プラン (月額 315円) を利用していた。 比較的自由にデザインをカスタマイズできること、 URL のドメインを自由に設定できることから、 2006年3月9日にこのブログを開設して以来、 今まで 4年近く使い続けてきた。 しかし自前サーバでない不便さ、隔靴掻痒感はいかんともしがたく、 乗り換えを検討したことは何度もあって、 そのたびに不便さと乗り換えの面倒くささを天秤にかけて踏み切れずにいた。

例えば、 HTML文法エラーを修正できない問題点を見つけたときや、 ブログのサイドバーにある 「livedoor Reader」 「RSS」 「livedoor Blog」などのバナーを非表示にしていたら、 ライブドアから警告メールが来て、 やむなくバナー表示を復活させたときなど。 なかでも、 昨年ブログ管理画面 (有料プラン) がリニューアルされたときは、 とても大きなフラストレーションを感じた。 ライブドアはよかれと思ってリニューアルしているのだろうが、 JavaScript を多用した新管理画面は、 私にとっては使いにくいことこのうえないし、 生ログ取得スクリプトも使えなくなった。

そして、 今年に入って 「ブログ共通ヘッダー」 (最上端に表示される livedoor Blogのロゴ, 所属カテゴリ, テキスト広告) が勝手に表示されるようになったのが最後の背中のひと押しとなった。 これを非表示にしたければ PRO プラン (月額 315円) ではなく ADVANCE プラン (月額 840円) に変更しろという趣旨 (要は値上げ) なのだと思うが、 何の連絡もなく画面の一番目立つ最上端に、 醜悪なヘッダー (ADVANCE プランへの移行を促すためにわざと醜悪にしている? *_*) を勝手に挿入するデリカシーの無さに驚き呆れ、 自前サーバへの引越を決めた。

common header

ブラウザ画面最上端 ↑ の 「ブログ共通ヘッダー」 (所属カテゴリ 「IT > プログラミング 」 と 「受験にまつわるエピソードを教えてください」 という広告? 表示) は、 ライブドアのサーバが HTML 中に自動挿入する以下の JavaScript (settings/header.js) によって body 要素へ追加 (appendChild) されている。

(function () {
    var hd = document.createElement('div');
    var str = '';
    str += '<div id="header2" style="z-index:10001"><table cellspacing="0" class="blog-common-header" id="header">';
	...(中略)...
    hd.innerHTML = str;
    document.body.appendChild(hd);
	...(後略)...

ユーザがカスタマイズ可能な範囲の外であるため、 この JavaScript の実行そのものを止めることは無理っぽいが、 追加された子要素を削除する JavaScript を実行して消すことは可能。 例えば以下のようなコードを HTML ファイルのどこかに入れておけばよい。

<script type="text/javascript" src="http://www.gcd.org/sengoku/docs/livedoor.js"></script>

livedoor.js は、 body 要素の子要素 「header2」 を取り除く (removeChild) だけの JavaScript:

(function () {
    var element = document.getElementById("header2");
    element.removeChild(element.childNodes.item(0));
})();

正月早々 「ブログ共通ヘッダー」 が勝手に表示されるようになったことに気付き、 直ちにこの対策を行なって非表示にしたのだが、 以前バナーを非表示にしたときライブドアから警告メールが来たことを考えれば、 今回も警告メールが来るのは時間の問題だろうと、 引越を急いだ次第。

私はブログを書くときも HTML で書いていて、 あまり CMS 的な機能は必要としないので、 トラックバックとコメントを受付けるだけの簡単な Web アプリを書こうと思っていたのだが、 試しにインストールしてみた WordPress が思いの外シンプルな作りで、 ちょっといじっているうちに簡単にカスタマイズできてしまったので、 これを使うことにした。

livedoorブログでエクスポートしたデータを (Movable Type / TypePad のデータとして) WordPress でインポートしたら、 (pre 要素なのに) 行頭の空白文字が削除されてしまって往生したが、 大した量でもなかったので手作業で修正してしまった。 もう少し量が多かったら真面目に PHP コードを追ったのだが...

こんなに簡単にできるなら、 もっと早く WordPress へ移行すればよかった。 既存のテーマを見よう見まねでいじっただけなのだが、 以前のデザインをほとんどそのまま踏襲することができた (むしろ余計な div 要素を排除できたので、よりシンプルになった)。

livedoorブログと WordPress では URL (パーマリンク, permalink) の形式が異なる。 例えば、 あるエントリの URL は、 livedoorブログ (旧URL) と WordPress (新URL) で次のようになる:

旧: livedoorブログ新: WordPress
ベースURL http://blog.gcd.org http://www.gcd.org/blog
エントリ /archives/51168556.html /2008/04/154/
月別表示 /archives/2008-04.html /2008/04/
カテゴリ /archives/cat_50026701.html /category/enlighten/
RSS /index.rdf /feed/rdf/

一見して、 WordPress のパーマリンクの方が分かりやすい。 正確に言えば、 WordPress のパーマリンクの形式は任意に設定できて、 livedoorブログ (Movable Type ベース) の形式に合わせることも可能だが、 エントリID (上記の例だと 51168556 と 154) が異なるので形式を合わせること自体にはあまり意味はない。

livedoorブログを利用していた時の URL がそのまま使えるように、 旧URL を新URL へ変換する以下の PHP スクリプトを書いた (switch 文で URL ごとにずらずら case を並べただけ)。

<?php
$new = NULL;
if ($_SERVER['REQUEST_URI'] == "/") {
    $new = "/blog/";
} elseif ($_SERVER['REQUEST_URI'] == "/atom.xml") {
    $new = "/blog/feed/atom/";
} elseif ($_SERVER['REQUEST_URI'] == "/index.rdf") {
    $new = "/blog/feed/rdf/";
} elseif (preg_match('/^\/archives\/(20\d\d)-(\d\d)\.html$/',
		     $_SERVER['REQUEST_URI'], $matches)) {
    $year = $matches[1];
    $mon = $matches[2];
    $new = "/blog/$year/$mon/";
} elseif (preg_match('/^\/archives\/cat_(\d+)\.html$/',
		     $_SERVER['REQUEST_URI'], $matches)) {
    $id = $matches[1];
    $new = "/blog/category/";
    switch ($id) {
    case 50026701: $new .= "enlighten/";   break;
    case 50026704: $new .= "business/";    break;
    case 50026703: $new .= "engineer/";    break;
    case 50026699: $new .= "system/";      break;
    case 50035671: $new .= "hardware/";    break;
    case 50026700: $new .= "programming/"; break;
    case 50021362: $new .= "stone/";       break;
    case 50035209: $new .= "la-fonera/";   break;
    case 50041534: $new .= "hawaii/";      break;
    case 50045637: $new .= "hongkong/";    break;
    case 50026702: $new .= "others/";      break;
    default: $new = NULL;
    }
} elseif (preg_match('/^\/archives\/(\d+)\.html$/',
		     $_SERVER['REQUEST_URI'], $matches)) {
    $id = $matches[1];
    $new = "/blog/";
    switch ($id) {
    case 50071073: $new .= "2006/03/8/"; break;
	...(中略)...
    case 51168556: $new .= "2008/04/154/"; break;
	...(中略)...
    case 51552583: $new .= "2009/12/184/"; break;
    case 51555097: $new .= "2010/01/185/"; break;
    default: $new = NULL;
    }
}
if ($new) {
    $host = "www.gcd.org";
    $_SERVER['SERVER_NAME'] = $host;
    $_SERVER['REQUEST_URI'] = $new;
    $_SERVER['SCRIPT_NAME'] = $new;
    $_SERVER['PHP_SELF'] = $new;
    define('WP_USE_THEMES', true);
    require('/usr/local/www/wordpress/wp-blog-header.php');
} else {
    header("HTTP/1.1 301 Moved Permanently");
    header("Location: https://www.gcd.org/blog/");
    exit();
}
?>

http://blog.gcd.org/* にアクセスすると、 ↑ この PHP スクリプトが実行される。

これに加え、 逆方向の変換、 つまり新URL から旧URL への変換も必要。 例えば 「はてなブックマーク」 へ頂いたコメント (例えば前述したエントリに対するコメント一覧) は旧URL に紐づいているので、 各エントリに旧URL を登録しておく必要がある。 WordPress には カスタムフィールド という機能があって、ひとつのエントリに複数の 「キー」 と、 各キーにその値を登録することができる。 そこで沢山の 「ブックマーク」 を頂いたエントリには、 「hatena_b」 というキーで旧URL を登録しておくことにした。 すると、以下のような PHP スクリプトでブックマーク数 を表示できる。

<?php
  $hatena_b = get_post_meta($post->ID, "hatena_b", true);
  if ($hatena_b)
    echo ' <a href="http://b.hatena.ne.jp/entry/' . $hatena_b
       . '"><img style="border:0;" src="http://b.hatena.ne.jp/entry/image/'
       . $hatena_b . '" alt="" /></a>';
?>

最後に、 ネームサーバの設定変更を行なって、 blog.gcd.org レコード (TTL = 3時間) の値を 「CNAME blog-01.livedoor.jp」 から 「CNAME www.gcd.org」 へ変更した。 1/26 19:35 に変更を行なったところ、 19:46 には最初のアクセスが www.gcd.org へ届き、 20時台には大半のアクセスが www.gcd.org へ届くようになった。 20時台にライブドア側へ届いた blog.gcd.org へのアクセスはわずかに 7件、 その後は 1 時間〜数時間ごとに、ぱらぱらアクセスがあるという状況が続いている。 旧レコードを保持しているキャッシュネームサーバが残っていて、 そのネームサーバを使ってるブラウザからのアクセスのみが、 ライブドア側へ届いているといった感じ。

More...
Filed under: システム構築・運用 — hiroaki_sengoku @ 08:26
2009年11月12日

自宅回線を 100Mbps から 200Mbps へ、帯域を倍にしてみた ~ NGN が自宅にやってきた

私の自宅がある神奈川でも、 先月 13日から 「フレッツ 光ネクスト ファミリー・ハイスピードタイプ」 の申込受付が始まったので、 早速申し込んでみた。 今まで使っていたのは 「Bフレッツ ニューファミリータイプ」 だから、 下りの帯域が 100Mbps から倍の 200Mbps に増える。 月額利用料は変わらない。 工事費が 5827円かかるが、 キャンペーン期間中につき月額料金が 3ヶ月間無料になるので悪くない。

10/16日に NTT東日本 0120-116116 に電話して申し込んだところ、 回線切替工事まで最短で 10営業日かかり、 工事の予約状況によると最速で 11/1(日) の工事になるらしい。 続いて NTT コミュニケーションズ 0120-506-506 に電話して、 200Mbps に変更したときプロバイダ (OCN) 側の変更が必要か確認。 個人向けサービス (OCN 光 with フレッツ) は元々 200Mbps に対応しているから契約変更は必要無しとのことだったが、 あいにく私が契約しているのは法人向けサービスであるところの OCN 光アクセス IP8/IP16「Bフレッツ」プラン である。 この場合、 OCN 光アクセス IP8/IP16「フレッツ 光ネクスト」プラン への契約変更が必要となる。

NTT コミュニケーションズの担当者の話によると、 認証サーバの設定を変更するらしい。 PPPoE のユーザ名とパスワードはそのまま同じものを使っていい (新しいユーザ名も付与されるので新旧どちらのユーザ名も使える) が、 認証サーバ側の設定を変更すると旧回線 (つまり Bフレッツ) からの認証は拒否されるらしい。 すなわち認証サーバの設定変更後は、 新回線 (つまり フレッツ 光ネクスト) からでないと認証が通らない。 したがって、 回線が切り替わる前に PPPoE セッションが切れると、 旧回線のままでは再接続できなくなってしまう。 さらに、 設定変更は平日の未明 0:00 ~ 5:00 の間にしか実施しないらしい。

ということはつまり、 11/1(日) に回線工事を行なうには、 10/30(金) の未明に認証サーバの設定変更を行なうことになる。 10/30 未明から 11/1 の回線工事まで丸二日間、 もし PPPoE セッションが何らかの原因によって切れてしまうと、 工事完了後まで二度と再接続できなくなる。 最長丸二日間切れたままというのはあまりに不便なので、 11/2(月) の未明に認証サーバの設定変更を行ない、 同日午前 9:00-12:00 に回線切替工事を行なうことにした。 これなら PPPoE セッションが切れても数時間の切断で済む。

More...
Filed under: IPv6,システム構築・運用 — hiroaki_sengoku @ 10:00
2009年10月8日

__sync_bool_compare_and_swap_4 とは何か? ~ glibc をビルドする場合は、 gcc の –with-arch=i686 configure オプションを使ってはいけない

glibc-2.10.1 をビルドしようとしたら、 「__sync_bool_compare_and_swap_4 が定義されていない」 というエラーが出た:

senri:/usr/local/src/glibc-2.10.1.i386 % ../glibc-2.10.1/configure
	...
senri:/usr/local/src/glibc-2.10.1.i386 % make
	...
/usr/local/src/glibc-2.10.1.i386/libc_pic.os: In function `__libc_fork':
/usr/local/src/glibc-2.10.1/posix/../nptl/sysdeps/unix/sysv/linux/i386/../fork.c:79: undefined reference to `__sync_bool_compare_and_swap_4'
/usr/local/src/glibc-2.10.1.i386/libc_pic.os: In function `__nscd_drop_map_ref':
/usr/local/src/glibc-2.10.1/nscd/nscd-client.h:320: undefined reference to `__sync_fetch_and_add_4'
	...
/usr/local/src/glibc-2.10.1.i386/libc_pic.os: In function `*__GI___libc_freeres':
/usr/local/src/glibc-2.10.1/malloc/set-freeres.c:39: undefined reference to `__sync_bool_compare_and_swap_4'
collect2: ld returned 1 exit status
make[1]: *** [/usr/local/src/glibc-2.10.1.i386/libc.so] Error 1
make[1]: Leaving directory `/usr/local/src/glibc-2.10.1'
make: *** [all] Error 2

__sync_bool_compare_and_swap_4 は gcc の組み込み関数なので、 関数が未定義であることを示す 「undefined reference to ...」 というエラーメッセージは、 誤解を招く不親切なメッセージだと思う。

__sync_bool_compare_and_swap_4(mem, oldval, newval) は、 mem が指し示すメモリの値 (4バイト分) が oldval であれば newval に変更する、 という操作をアトミックに行なう組み込み関数。 アトミック (不可分) 操作とは、 操作の途中が存在してはいけない操作のことで、 この例なら比較 (メモリの値が oldval か?) と代入 (newval に変更) が必ず 「いっぺん」 に行なわれ、 「比較だけ行なったけどまだ代入は行なわれていない」 という状態が存在しないことを意味する。

アトミックに行なうためには、 当然ながら CPU でその操作をサポートしている必要がある (複数個の命令の列で実現しようとすると、 命令列の半ばを実行中の状態が必ず存在してしまう) わけだが、 残念ながら Intel 386 プロセッサでは、 この compare_and_swap (CMPXCHG 命令) をサポートしておらず、 サポートするのは Intel 486 以降の CPU である。 テストプログラムを書いて試してみる:

#include <stdio.h>

int main() {
    int mem[1], oldval, newval;
    oldval=0;
    newval=1;
    mem[0] = 0;
    __sync_bool_compare_and_swap(mem, oldval, newval);
    printf("mem[0]=%d\n", mem[0]);
    return 0;
}

見ての通り、 mem[0] の値を oldval の値 (0) と比較し、 一致していたら newval の値 (1) を代入し、 mem[0] の値を表示するだけのプログラムである。

関数名が 「__sync_bool_compare_and_swap」 であって、 後ろに 「_4」 がついていないことに注意。 gcc が引数の型 (この例では int) を見て、 その型のビット長を後ろにつけてくれる (この例では int 型は 4 バイトなので 「_4」 をつけてくれる)。

gcc では 「-march=タイプ」 オプションを指定することによって CPU タイプを指定できる。 -march オプションを指定しなかったり (この場合は全 CPU でサポートされる組み込み関数のみ利用できる)、 あるいは -march=i386 を指定したりすると、 コンパイル時にエラーになる:

% gcc -Wall test.c
/tmp/cc4eNX6L.o: In function `main':
test.c:(.text+0x3b): undefined reference to `__sync_bool_compare_and_swap_4'
collect2: ld returned 1 exit status
% gcc -Wall -march=i386 test.c
/tmp/cc6chtFj.o: In function `main':
test.c:(.text+0x36): undefined reference to `__sync_bool_compare_and_swap_4'
collect2: ld returned 1 exit status
% gcc -Wall -march=i486 test.c
% ./a.out
mem[0]=1

いまさら i486 というのもアレなので、 今なら i686 を指定するのがよさげ。 私の手元にはいまだ PentiumIII マシンがあるものの、 PentiumIII より古いマシンはない (昨年 ML115 と SC440 を買ったとき PentiumII マシンを引退させた) ので、 pentium3 を指定すれば SSE (Streaming SIMD Extensions) が利用できるようになるが、 glibc をビルドするときに必要かというと、 たぶん必要ない。

というわけでエラーの原因は分かったが、 では glibc をビルドするときは、 どうすればいいだろうか?

とりあえず google で検索してみたら、 gcc の configure オプションに 「--with-arch=i686」 を指定して gcc をビルドする必要がある、 と書いてあるページが見つかった。

--with-arch オプションは、 -march のデフォルトを設定するための configure オプションである。 つまり 「--with-arch=i686」 を指定して gcc を再インストールすると、 gcc に -march オプションをつけなくてもデフォルトが i686 になる。 なるほど確かにそうすれば、 glibc 側で何も変更せずに __sync_bool_compare_and_swap_4 関数が使えるようになりそうである。

いまどき i686 以前の CPU 用のコードが必要になりそうなケースは滅多にないだろうから、 -march オプションのデフォルトを i686 にするのも悪い選択ではないように思えた。 gcc をビルドし直すのは面倒だなーと思いつつも、 ついでに gcc のバージョンを上げておこうと gcc-4.3.4 をダウンロードしてきて 「--with-arch=i686」 付でビルドしてみた。

ところが!

More...
Filed under: システム構築・運用,プログラミングと開発環境 — hiroaki_sengoku @ 09:39
2009年9月14日

文字化けしていなくても MySQL 内の文字コードが正しくない場合がある

MySQL 5 からテーブルごとに文字列のエンコーディングを指定できるようになった (「そんなことは知ってるYO!」という人も多いと思うので、 そういう人は「これからが本題」 の部分まで読み飛ばして欲しい)。 例えばテーブルを作るときに、

mysql> CREATE DATABASE test;
Query OK, 1 row affected (0.05 sec)

mysql> USE test
Database changed
mysql> CREATE TABLE user ( name VARCHAR(255) ) CHARSET=utf8;
Query OK, 0 rows affected (0.05 sec)

などと 「CHARSET=utf8」 を指定すれば、 文字列を UTF-8 エンコーディングで格納する。 「CHARSET」 すなわち 「文字集合」 と、 エンコーディング (文字符号化) は本来別の概念であるが、 MySQL の場合は両者をまとめて CHARSET ないし character_set と呼んでいるので、 ここではそれを踏襲してキャラクタセットと呼ぶことにする。 MySQL のシステム変数のうちキャラクタセットに関連するものは、 以下のように沢山ある。

mysql> SHOW VARIABLES LIKE 'character\_set\_%';
+--------------------------+--------+
| Variable_name            | Value  |
+--------------------------+--------+
| character_set_client     | latin1 |
| character_set_connection | latin1 |
| character_set_database   | latin1 |
| character_set_filesystem | binary |
| character_set_results    | latin1 |
| character_set_server     | latin1 |
| character_set_system     | utf8   |
+--------------------------+--------+
7 rows in set (0.00 sec)

たくさんあってややこしいが、 重要なのは 「character_set_client」 と 「character_set_connection」 「character_set_results」 で、 この3変数はクライアントがクエリを送信し、 クエリ結果を受信するときのキャラクタセットを設定する。 charset コマンド (あるいは SET NAMES) を使うと、 クライアント側のキャラクタセットに関係するこの3変数を一度に変更できるので、 特に必要がなければこの3変数は常に同じキャラクタセット、 すなわちクライアント側で送受信するキャラクタセットに一致させておくとよい (PHP スクリプトから MySQL をアクセスするときは、 mysql_set_charset() を使ってクライアント側のキャラクタセットを設定する)。

mysql> CHARSET utf8
Charset changed
mysql> SHOW VARIABLES LIKE 'character\_set\_%';
+--------------------------+--------+
| Variable_name            | Value  |
+--------------------------+--------+
| character_set_client     | utf8   |
| character_set_connection | utf8   |
| character_set_database   | latin1 |
| character_set_filesystem | binary |
| character_set_results    | utf8   |
| character_set_server     | latin1 |
| character_set_system     | utf8   |
+--------------------------+--------+
7 rows in set (0.00 sec)

mysql> INSERT INTO user VALUES ('仙石 浩明');
Query OK, 1 row affected (0.00 sec)

mysql> SELECT name, HEX(name) FROM user;
+-----------------+--------------------------------+
| name            | HEX(name)                      |
+-----------------+--------------------------------+
| 仙石 浩明      | E4BB99E79FB3E38080E6B5A9E6988E |
+-----------------+--------------------------------+
1 row in set (0.00 sec)

このように 「select HEX()」 で確認すると、 文字列が正しく UTF-8 エンコーディングで格納されていることが確認できる。

蛇足だが、mysql クライアントが GNU readline を使っている場合は、 ~/inputrc などで 「set convert-meta Off」 を設定しておく。 デフォルトの readline では convert-meta が On なので、 キャラクタの最上位ビット (MSB) を 0 にしてしまう。 つまり UTF-8 (および EUC-JP や Shift_JIS) などの 8bit キャラクタセットだと、 MSB が 1 である文字が正しく送信されない。 例えば 「あ」 (UTF-8 で E3 81 82) を入力しようとしても、 MSB が落ちた 「c^A^B」 (63 01 02) が送られてしまう。

「character_set_server」 が latin1 になっていて気持ち悪いかも知れないが、 このシステム変数は新しく database を作るときのデフォルトを設定するものなので、 (latin1 な database は金輪際作らないというのでも無い限り) 変更する必要はない。

latin1 になっているもう片方の変数 「character_set_database」 は、 デフォルト database に合わせて (つまり USE コマンドを発行するごとに) サーバがこの変数を変更するので、 これもユーザが変更する必要はない。

前置きが長くなったが、これからが本題

UTF-8 なテーブルを読み書きする際は、 「charset utf8」 コマンドを送信してクライアント側のキャラクタセットを UTF-8 に設定すればよいのであるが、 デフォルトが latin1 であるクライアントも多い。 PHP などから MySQL サーバにアクセスする場合なども、 (PHP のビルド方法にも依存するが) デフォルトは latin1 になっている (PHP の場合 mysql_client_encoding() で確認できる)。 このようなクライアントをデフォルトの latin1 のままで使うとどうなるだろうか?

More...
Filed under: システム構築・運用 — hiroaki_sengoku @ 08:19
2009年1月8日

wpa_supplicant のバグ: TLS拡張 (TLS Extensions) 対応の OpenSSL と使うと、WPA EAP-TLS が常に失敗する

あけましておめでとうございます。 今年もよろしくお願いします。

自宅の無線LAN を WPA2-EAP (WPA2 エンタープライズ) にして 1ヶ月になるが、 すこぶる快適。 アクセスポイントがエンタープライズモードに対応していなければいけないのと、 RADIUS サーバが必要である上、 EAP-TLS を用いる場合は SSL クライアント証明書を発行する 認証局 (CA) を作る必要まであるので、 自宅LAN での導入にはややハードルが高いが、 いったん導入してしまえば WPA/WPA2 パーソナルより手間がかからない。

WPA/WPA2 パーソナルだと、 秘密のパスワードを全ての無線LAN 端末で共有するので、 端末の台数が増えてくるとどんどん漏洩のリスクが高まる。

自宅LAN でそんなに端末があるのか? という突っ込みが入りそうだが、 格安 NetBook や、 無線LAN 機能付スマートフォンなどを買っていると、 自宅LAN とはいえ、 「エンタープライズ」 的なニーズが出てくる。

WPA2 エンタープライズで認証方式として EAP-TLS を使うと、 無線LAN 端末にはそれぞれ個別の証明書をインポートしておくだけでよく、 万一その端末を紛失しても、 その端末の証明書を無効にするだけで済む。

EAP-TLS には対応機器/OS が多いというメリットもある。 無線LAN 端末として、 Windows VISTA, Windows XP, Ubuntu 8.04 LTS などを使ってみたが、 いずれもあっけないくらい簡単に接続できてしまった。 証明書さえインポートしておけば (Windows ならダブルクリックだけ)、 あとはパスワードを入力しなくてもいいぶん WPA/WPA2 パーソナルより設定が簡単。

既存の OS (Linux の場合はディストリビューション) で無線接続できるようになったので、 次は私が独自に構築した GNU/Linux (いわば my distribution) 上で WPA2 EAP-TLS 接続を試みた。

まず wpa_supplicant 0.5.11 をダウンロードしてコンパイル。 続いて設定ファイルである wpa_supplicant.conf を書く。

network={
	ssid="XXXXXXXXXX"
	key_mgmt=WPA-EAP
	eap=TLS
	identity="olevia"
	ca_cert="/usr/local/ssl/certs/GCD_Root_CA.pem"
	client_cert="/usr/local/ssl/certs/olevia.pem"
	private_key="/usr/local/ssl/private/olevia.pem"
	private_key_passwd=""
}

「ca_cert=」 「client_cert=」 「private_key=」 にそれぞれ CA の公開鍵、端末の公開鍵、端末の秘密鍵のファイル名を指定している。 で、wpa_supplicant を実行。

# wpa_supplicant -i eth1 -c /etc/wpa_supplicant.conf -d -D wext
Initializing interface 'eth1' conf '/etc/wpa_supplicant.conf' driver 'wext' ctrl_interface 'N/A' bridge 'N/A'
Configuration file '/etc/wpa_supplicant.conf' -> '/etc/wpa_supplicant.conf'
Reading configuration file '/etc/wpa_supplicant.conf'
Priority group 0
   id=0 ssid='XXXXXXXXXX'
Initializing interface (2) 'eth1'
EAPOL: SUPP_PAE entering state DISCONNECTED
EAPOL: KEY_RX entering state NO_KEY_RECEIVE
EAPOL: SUPP_BE entering state INITIALIZE
EAP: EAP entering state DISABLED
	...(中略)...

wpa_supplicant はデバッグモード (-d オプション) にすると大量のログを出力するので動作を追いにくいが、 wpa_supplicant のプログラムは状態遷移機械 (オートマトン) として動作するよう書かれているので、 「EAP: EAP entering state」 の部分を追っていくとよい。 「DISABLED」 と出力されているのが、 その時点におけるオートマトンの状態。

状態遷移機械 (オートマトン) と言ってしまうと、 コンピュータ/プログラムは全てオートマトンなのであるが (^^;)、 プログラミングの方法として状態遷移機械 (state machine) と言うときは、 各状態に名前を付けて、 各状態ごとの動作を (switch 文や if ... else if ... 文などで) 分けて書く方法を指す。

状態には INITIALIZE, DISABLED, IDLE, RECEIVED, GET_METHOD, METHOD, SEND_RESPONSE, DISCARD, IDENTITY, NOTIFICATION, RETRANSMIT, SUCCESS, FAILURE の 13状態がある。 もちろん 「SUCCESS」 が受理状態。 SUCCESS 状態は、 アクセスポイントが接続を許可した状態を意味する。

ところが、ログを追っていくと、

EAP: EAP entering state RECEIVED
EAP: Received EAP-Success
EAP: EAP entering state FAILURE
CTRL-EVENT-EAP-FAILURE EAP authentication failed

FAILURE 状態 (非受理状態) に遷移している (*_*)。当然、接続できず。
その直前に 「Received EAP-Success」 と出ているにもかかわらず!

アクセスポイント側 (正確に言うと RADIUS サーバ) のログを調べてみると、 ちゃんと接続を許可している (EAP-Success を送っているのだから当然だが)。 一体これはどうしたことか?

私がコンパイルした wpa_supplicant に問題があるのかと思って、 この wpa_supplicant を、 既に接続できることが確認済みの Ubuntu 上にコピーして実行してみる。 「Received EAP-Success」 をログの中から探してみると、

EAP: EAP entering state RECEIVED
EAP: Received EAP-Success
EAP: EAP entering state SUCCESS
CTRL-EVENT-EAP-SUCCESS EAP authentication completed successfully

ちゃんと SUCCESS 状態に遷移しているし、接続もできた。
同一バイナリなのに、異なる環境 (ディストリビューション) だと、 どうして結果が異なるのか?

どのような可能性が考えられるだろうか? 腕に覚えがあるかたは、 この続きを見ずに (といっても、既にタイトルでネタバレしてしまっているが ^^;) 原因を推測してみてはいかがだろうか?

More...
Filed under: システム構築・運用 — hiroaki_sengoku @ 08:58
2008年10月27日

initramfs シェル環境 (initramfs shell environment) でジョブ制御する方法 (aka “can’t access tty; job control turned off” を消す方法)

GNU/Linux OS のブート時に、init(8) を経由せずにシェル (/bin/sh) を実行すると、 このシェル上ではジョブ制御 (job control) が行なえない。 つまりこのシェル環境は制御端末 (controlling tty) に成れない。 これがどんなに不便かというと、 自動的に止まらないプログラム (例えばオプション無しで ping を実行したときなど) を止める方法が無いわけで、 いったんそういうプログラムを動かしてしまったら最後、 CTRL-ALT-DEL で reboot させる他なくなってしまう。

そもそも、なぜ init(8) を起動する前に /bin/sh を実行したいかというと、 ミニルート (initramfs) 上で 作業を行ないたいから。 initramfs の init (これは init(8) ではなくシェル・スクリプト) の中で、 BusyBox の /bin/sh (/bin/ash) を exec する (つまり PID=1) ことによって、 initramfs 上での作業を可能にする。

init(8) は、 GNU/Linux OS の全てのプロセスの親プロセスだが、 その万物の親すら生まれていない創世記以前に作業を行なえるメリットは数多い。 例えば、ルート・ファイル・システム (root file system) すらマウントしていない段階なので、 マウント後 (つまり init(8) 起動後) には実行不可能な操作 (xfs_repair などのファイルシステム修復操作とか) を行なうことができる。 しかもこのシェルはプロセスID が 1番なので、 このシェル環境上で root file system を「/」にマウントし、 続いて init(8) を exec すれば、 そのまま GNU/Linux OS を起動することができる。

root file system のメンテナンス等は、 別の起動ディスク (CD-ROM や USB メモリ等) からブートして行なうのが一般的だが、 CD-ROM ドライブや USB メモリを準備したり、 あるいは CD-ROM や USB メモリの抜き差しが必要になったりと、 なにかと面倒である。 メンテナンス用の起動パーティションを root file system とは別に用意する、 という方法も考えられるが、 メンテナンス専用のパーティションを維持管理するのが面倒くさい (普段使わないものほど陳腐化して、いざというとき役に立たない)。
initramfs だとハードディスクすら不要 (例えば PXE ブート) でメンテナンスが可能になるし、 普段 GNU/Linux OS 起動用として使ってる initramfs が、 そのまま非常用のメンテナンス環境になるため、 陳腐化する心配がない。
実は「突然死したハードディスクを復旧させる『お手軽パック』」は、 initramfs そのものだったりする。 しかも「復旧」用として作った initramfs というわけではなく、 私が普段 GNU/Linux OS を ブートするときに使っている initramfs と 全く同じものである (だからこそ ハードディスクの突然死問題 が勃発した直後にリリースできた)。

というわけで、 いいことづくめの initramfs シェル環境 (initramfs shell environment) なのだが、 「復旧お手軽パック」実行例 にもあるように、 init(8) 以前の段階で /bin/sh を実行すると

/bin/sh: can't access tty; job control turned off
#

と表示してジョブ制御がオフになってしまう。 つまりこのシェル環境では、 プログラムを実行中に control-C (^C) を押しても止める (正確にいうと SIGINT シグナルを送る) ことができない。

ジョブ制御 (^C などでシグナルを送ること) ができない状態に陥って 初めて沸き起こる制御端末 (controlling tty) に対する感謝の念なのであるが、 initramfs が役目を終えて init(8) が起動して (GNU/Linux OS がブートして) しまうと、 喉元過ぎればなんとやらで 「can't access tty」をなんとかしようという意欲は雲散霧消し、 そのままになっていた。

制御端末になれない initramfs シェル環境に対して何十回目かの悪態をついた後、 ようやく対策を立てるべく原因を調べてみることにした。

Linuxカーネル(ドライバ)のソースを読んでみたところ、 以下の端末デバイスは制御端末に成れないのです。 興味がある人は、ソース、 drivers/char/tty_io.cのtty_open()を見てみてください。

* /dev/console -- カーネルの起動時の端末。
* /dev/tty0 -- tty1~の「Linux Virtual Terminal」のうち、現在表示している物を示す。
* /dev/tty -- 現在使っている端末を示す。
* PTYのマスター側
Linux:制御端末 から引用

initramfs シェル環境で使っている端末は /dev/console だから制御端末になれない。 だから BusyBox には /dev/console という仮想的な端末ではなく、 本物のデバイスを探すための cttyhack というプログラムが付属している。 /bin/sh を実行する代わりに cttyhack /bin/sh を実行すれば ジョブ制御ができると BusyBox のマニュアルには書いてある。

...という解説は上に引用したページをはじめ、 WWW 上のあちらこちらのページで見かけるし、 私としても当然そんなことは先刻承知で、

/bin/sh: can't access tty; job control turned off
# tty
/dev/console
# cttyhack sh
sh: can't access tty; job control turned off
# tty
/dev/tty1
#

などと、確かに cttyhack の働きにより /dev/console ではなく /dev/tty1 を使うようになったものの、 相変わらず「can't access tty」エラーが出ているので困っているわけである。 cttyhack を使っているのにジョブ制御できないわけで、 cttyhack-- と思っていた。

前置きが長くなったが、ここからが本題である。

More...
Filed under: システム構築・運用 — hiroaki_sengoku @ 07:38
2008年3月19日

フレッツ・ドットネットを解約したら、フレッツ網 router へ ping6 できなくなった!

昨年12月26日に、 NTT東日本から 「BフレッツにおけるIPv6映像視聴等機能の標準装備について」 というお知らせが来た。 3月3日より、 「Bフレッツ」に IPv6 映像視聴等機能を標準装備するので、 フレッツ・ドット・ネットの契約が不要になるとのこと。

現在、IPv6映像視聴等機能は「フレッツ・ドットネット」にて 提供しておりますが、 平成20年3月3日(月)以降は、 ブロードバンド映像サービスのみ をご利用の場合は、 「フレッツ・ドットネット」のご契約が不要になります。 これにより解約を希望されるお客さまにつきましては、 平成20年3月3日(月) より※3受付を開始いたします。  なお、ブロードバンド映像サービス以外で、 「FdNネーム」「FdNディスク」「FdNディスクビューセレクト」「FdNナンバー」等 「フレッツ・ドットネット」サービス※4をご利用の際には、 引き続き「フレッツ・ドットネット」のご契約が必要となりますのでご注意ください。

私は IPv6 機能のためだけに「フレッツ・ドットネット」を契約していて、 「FdNネーム」「FdNディスク」「FdNディスクビューセレクト」「FdNナンバー」等の サービスを利用したことはない (「ブロードバンド映像サービス」も利用していない) ので、 フレッツ・スクウェアトップから、 サービス申込受付を選んで、「フレッツ・ドットネット」を解約した。

ところが!

フレッツ網側の v6 ルータが ping に反応しなくなった。

senri % ping6 -n router.flets.gcd.org
PING router.flets.gcd.org(2001:c90:XXXX:XXXX:2d0:2bff:fe30:b91a) 56 data bytes
From 2001:c90:XXXX:XXXX:2d0:2bff:fe30:b91a icmp_seq=1 Destination unreachable: Administratively prohibited
From 2001:c90:XXXX:XXXX:2d0:2bff:fe30:b91a icmp_seq=2 Destination unreachable: Administratively prohibited

--- router.flets.gcd.org ping statistics ---
2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 1000ms

ちなみに、 この v6 ルータからの router advertisement は正常に流れてきている:

senri # tcpdump -i eth1 -vvv ip6
tcpdump: listening on eth1, link-type EN10MB (Ethernet), capture size 96 bytes
	...
13:02:02.904899 fe80::2d0:2bff:fe30:b91a > ff02::1: icmp6: router advertisement(chlim=64, pref=medium, router_ltime=1800, reachable_time=0, retrans_time=0)(src lladdr: 00:d0:2b:30:b9:1a)(mtu: mtu=1500)[ndp opt] [class 0xe0] (len 64, hlim 255)

v6 ルータ越えの通信が全て禁止されてしまったらしく、 フレッツ網に接続している他サイトとの IPv6 通信も同様に禁止されて (Administratively prohibited) しまった。 例外は、フレッツ・スクウェアv6 へのアクセス:

senri % ping6 -n flets-v6.jp
PING flets-v6.jp(2001:c90:ff:1::1) 56 data bytes
64 bytes from 2001:c90:ff:1::1: icmp_seq=1 ttl=52 time=5.05 ms
64 bytes from 2001:c90:ff:1::1: icmp_seq=2 ttl=52 time=4.55 ms

--- flets-v6.jp ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 4.554/4.803/5.053/0.258 ms

おそらく「ブロードバンド映像サービス」を提供するサーバへの IPv6 通信も 許可されているのだろう。 つまり、 「BフレッツにIPv6映像視聴等機能を標準装備」 という NTT東日本の発表のココロは、 IPv6 映像サービスへの IPv6 通信*のみ*許可するということであって、 それ以外の IPv6 通信は対象外ということのようだ。

「フレッツ・ドットネット」サービスの概要には、 「フレッツ・ドットネットサービス機能一覧」として、

・FdNネームが1つ利用できます。
・FdNディスク(100MB)で、ファイル共有が利用できます。
・FdNディスク(100MB)には、最大10のグループメンバーを登録できます。
※NTT東日本が無料で提供する専用ソフトウェア「FLET'S.Netメッセンジャーにより、 ファイル転送やビデオチャットが利用できます。

が列挙されているのみであって、 IPv6 通信の許可/不許可について言及していないのは、 とてもミスリーディングな記述だと思う。

慌てて再度フレッツ・ドットネット契約を (フレッツ・スクウェアで) 申込むと、 10分ほどで再び IPv6 通信ができるようになった:

senri % ping6 -n router.flets.gcd.org
PING router.flets.gcd.org(2001:c90:XXXX:XXXX:2d0:2bff:fe30:b91a) 56 data bytes
64 bytes from 2001:c90:XXXX:XXXX:2d0:2bff:fe30:b91a: icmp_seq=1 ttl=64 time=3.11 ms
64 bytes from 2001:c90:XXXX:XXXX:2d0:2bff:fe30:b91a: icmp_seq=2 ttl=64 time=0.920 ms

--- router.flets.gcd.org ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 0.920/2.019/3.118/1.099 ms

フレッツ網を経由した IPv6 通信を利用しているかたは、 たとえフレッツ・ドットネットサービスを利用していなくても、 フレッツ・ドットネットを解約すべきではないので、 ご注意のほどを!

Filed under: IPv6,システム構築・運用 — hiroaki_sengoku @ 15:32
2008年1月31日

リモートの p0f (passive fingerprinting) の結果を参照してスパム対策を行なう

p0f は、 通信相手の OS を受動的に特定するツールで、 迷惑メール送信などの スパム行為を行なう「敵」を知る手段として有用である。 例えば、もし (あくまで仮定の話だが) 受信するメールのほとんどすべてが Linux や FreeBSD などの UNIX 系サーバから送信されるメールであって、 Windows マシンから送られてきたメールのほとんどすべてが迷惑メールであったなら、 Windows マシンからのメールを排除するという対策は合理的なものとなるだろう。

もちろん、Windows を使ってマトモなメールを送ってくるケースもあるだろうから、 Windows から送られたメールを全て排除するのは現実的ではないが、 p0f での判定結果と、 その他の手段 (例えば送信元 IP アドレス) での判定結果を組合わせて 迷惑メールであるか否かの判断を行なえば、 より精度の高い迷惑メール排除が可能になる。

ところが、 p0f は通信相手から送られてくる IP パケットを元に、 通信相手の OS を特定するツールであるから、 間にファイアウォールや NAT (IPアドレス・ポート変換) を行なう機器があると、 通信相手ではなくファイアウォールや NAT について調べてしまう。 だから、メールを受信するサーバがファイアウォールの内側にある場合は、 意味ある結果が得られないし、 外側にある場合だとメールを受信するサーバとは別の場所 (つまりファイアウォールの内側) で スパム判定を行ないたくなるものだろう。 例えばメールサーバは DMZ 上にあるが、 迷惑メール判定は LAN 内のマシンで行ないたい場合など。

私の個人サイト GCD は、 b フレッツに PPPoE 接続している。 p0f は調べる通信のインタフェース名を -i オプションで指定する必要があるが、 (1) PPPoE だからインタフェース名 (ppp0~) が変わることがある。 また、 PPPoE を行なうゲートウェイマシンは二台ある (冗長構成) ので、 (2) アクティブ側で p0f を実行しないと意味がない。 さらに、 メールサーバは (メールボックスを一ヶ所にまとめたかったので) 一台だけであり、 (3) 異なるサーバ上 (アクティブ側のゲートウェイ) で動いている p0f の結果を メールサーバから参照しなければならない。

以上 (1) ~ (3) の 3点を満たすための構成を考えてみた。

まず (1) と (2) は、pppd の ip-up スクリプトから p0f を実行すればよい。 例えば、ip-up で

command=$0
interface=$1
	...
case $command in
    *ip-up)
	p0f -i $interface -Q /var/run/p0f-sock \
	    'port 25 and (not src net 192.168.0.0/16)' \
	    -u stone -d -t -o /var/log/p0f.log
	;;
    *ip-down)
	killall p0f
	;;
esac

などと p0f を起動し、ip-down で p0f を終了させる。 これでアクティブ側のゲートウェイ上でのみ p0f が動く。

p0f による判定結果は、 p0f の -Q オプションで指定した UNIX ドメイン・ソケット (上記の例では、 /var/run/p0f-sock) を介して 問合わせることができるが、 UNIX ドメイン・ソケットなので当然のことながら 別のマシンからは問合わせることができない。 そこで stone に転送させる:

stone /var/run/p0f-sock 12345 &

アクティブ側のゲートウェイは、 仮想ルータの IP アドレス 192.168.1.1 を持っているので、 「192.168.1.1:12345」へアクセスすれば、 それを stone が /var/run/p0f-sock へ中継してくれるので、 (3) p0f の結果を参照できる。

p0f の結果を参照するサンプルプログラムとして、 p0f には perl で書かれた p0fq.pl と、 C で書かれた p0fq.c が付属しているが、 あいにくどちらも UNIX ドメイン・ソケットにしか対応していない (当たり前)。 ちょっといじってリモート上の p0f へ (stone 経由で) アクセスできるようにしてみる。

p0fq.pl へのパッチ:

--- test/p0fq.pl.org	2006-08-21 23:11:10.000000000 +0900
+++ test/p0fq.pl	2008-01-31 08:00:14.652880068 +0900
@@ -30,8 +30,14 @@
                  $src->intip(), $dst->intip(), $ARGV[2], $ARGV[4]);
 
 # Open the connection to p0f
-my $sock = new IO::Socket::UNIX (Peer => $ARGV[0],
+my $sock;
+if ($ARGV[0] =~ /^[\-\w]+:\d+$/) {
+    $sock = new IO::Socket::INET (PeerAddr => $ARGV[0],
                                  Type => SOCK_STREAM);
+} else {
+    $sock = new IO::Socket::UNIX (Peer => $ARGV[0],
+				  Type => SOCK_STREAM);
+}
 die "Could not create socket: $!\n" unless $sock;
 
 # Ask p0f

「IO::Socket::UNIX」を「IO::Socket::INET」に変更するだけで済む。

p0fq.c へのパッチ:

--- test/p0fq.c.org	2006-08-21 21:29:49.000000000 +0900
+++ test/p0fq.c	2008-01-31 08:05:55.499326450 +0900
@@ -16,6 +16,7 @@
 
 #include <sys/types.h>
 #include <sys/socket.h>
+#include <netdb.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <unistd.h>
@@ -40,6 +41,7 @@
   struct p0f_response r;
   _u32 s,d,sp,dp;
   _s32 sock;
+  char *str;
   
   if (argc != 6) {
     debug("Usage: %s p0f_socket src_ip src_port dst_ip dst_port\n",
@@ -55,12 +57,37 @@
   if (!sp || !dp || s == INADDR_NONE || d == INADDR_NONE)
     fatal("Bad IP/port values.\n");
 
+  if ((str=strchr(argv[1], ':'))) {
+    struct addrinfo *ai = NULL;
+    struct addrinfo hint;
+    int err;
+    *str++ = '\0';
+    hint.ai_flags = 0;
+    hint.ai_family = AF_INET;
+    hint.ai_socktype = SOCK_STREAM;
+    hint.ai_protocol = IPPROTO_TCP;
+    hint.ai_addrlen = 0;
+    hint.ai_addr = NULL;
+    hint.ai_canonname = NULL;
+    hint.ai_next = NULL;
+    err = getaddrinfo(argv[1], str, &hint, &ai);
+    if (err) {
+      if (err == EAI_SYSTEM) pfatal("getaddrinfo");
+      else fatal("getaddrinfo(%s,%s): %s\n",
+		 argv[1], str, gai_strerror(err));
+    }
+    memcpy(&x, ai->ai_addr, ai->ai_addrlen);
+    sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
+    freeaddrinfo(ai);
+    if (sock < 0) pfatal("socket");
+  } else {
   sock = socket(PF_UNIX,SOCK_STREAM,0);
   if (sock < 0) pfatal("socket");
 
   memset(&x,0,sizeof(x));
   x.sun_family=AF_UNIX;
   strncpy(x.sun_path,argv[1],63);
+  }
 
   if (connect(sock,(struct sockaddr*)&x,sizeof(x)))  pfatal(argv[1]);
 

getaddrinfo を呼び出すための準備に行数を費やしているので 複雑に見えるかも知れないが、 本質は プロトコル・ファミリ (protocol family) を AF_UNIX から AF_INET に変更しただけである。

(p0f を実行しているマシンとは異なるマシン上で) p0fq を実行してみる:

% p0fq 192.168.1.1:12345 81.36.137.136 2943 60.32.85.220 25
Genre    : Windows
Details  : 2000 SP4, XP SP1+
Distance : 21 hops
Link     : pppoe (DSL)

上記は、 メール送信元 (81.36.137.136 のポート 2943番) が GCD の MX (60.32.85.220 のポート 25番) へメールを送ってきた通信の p0f による判定結果。 メールサーバで、 メールヘッダにメール送信元のポート番号も出力するようにしておけば、 メールを受信するユーザが自前のメール振り分けプログラム (procmail など) を使って p0f の判定結果を参照できる点がミソ。

81.36.137.136 はスペインのプロバイダの IP アドレスらしいが、 逆引きしてみると 136.Red-81-36-137.dynamicIP.rima-tde.net となるので 動的に割当てられているアドレスなのだろう。 これは p0f の結果に pppoe (DSL) と出ていることと符合する。 そして Windows 2000 SP4 か Windows XP SP1 以降を使って 送信していることが分かる。

Filed under: システム構築・運用 — hiroaki_sengoku @ 08:55
2007年10月12日

ハードウェア・ウォッチドッグ・タイマー iTCO_wdt のススメ

極めて稀とはいえ、Linux もハングすることはある。 ハードウェア自体には何ら異常はなく、 リセットスイッチを押したら正常に再起動してしまって、 何が問題だったか分からずじまい、という経験は誰にでもあるのではなかろうか。 原因不明のハングが全く無くなるのが理想ではあるのだが、 ハングして止ったままになるよりは、 自動的にリセットがかかって再起動してくれたほうがいい、という場合もあるだろう。

もちろんハードウェア障害が原因でハングしてしまった場合は、 リセットスイッチを押しても解決にはならない。 再起動を試みることにより、障害がより致命的になる可能性もあるので、 ウォッチドッグ・タイマーを設定する際は、 「止ったまま」と「自動再起動」とどちらがマシか天秤にかける必要がある。

そんなとき、 ウォッチドッグ・タイマー (watchdog timer) が便利。 一定時間 (例えば 30秒) 放置すると、 システムを自動的にリセットするタイマーである。 この自動リセットを回避するには、 定期的に (30秒以内に) タイマーを元に戻す (以下、番犬 (watchdog) に「蹴りを入れる」と略記) 必要があるわけで、 システムが正常に動作している時は 定期的に「蹴り」を入れ続けるようなプログラムを走らせておく。 で、 カーネルがハングしたなどの理由によって 「蹴り」を入れるプログラムが動かなくなると、 自動的にシステム・リセットがかかって ハング状態を脱出できる、という仕掛け。

ソフトウェアにどんなトラブルが起きても確実に再起動を行なわせるには、 ハードウェアで物理的にリセット スイッチを押す ハードウェアを用いるのが一番であるが、 まずはお手軽にソフトウェア版を利用してみることにした。
仙石浩明の日記: ウォッチドッグ タイマ から引用

わざわざハードウェア・ウォッチドッグ・タイマーを買ってきて 組み込むのは大変と思ったので、 上に引用した日記 (2006年5月) で書いたように ソフトウェア版ウォッチドッグ (softdog.ko モジュール) を使っていたのだが、 実はインテル・チップセットであれば大抵の PC に標準で ハードウェア・ウォッチドッグ・タイマーがついていた (何たる不覚 orz)。

Intel TCO Timer/Watchdog
Hardware driver for the intel TCO timer based watchdog devices. These drivers are included in the Intel 82801 I/O Controller Hub family (from ICH0 up to ICH8) and in the Intel 6300ESB controller hub.
linux/drivers/char/watchdog/Kconfig から引用

つまり Intel の ICH には最初から ハードウェア・ウォッチドッグ・タイマーがついていたようである。 最近の Linux カーネルには、 このウォッチドッグ・タイマーのドライバが含まれているので早速使ってみた。 というか、 Linux 2.6.22.9 を使っていたら、 このドライバ・モジュール iTCO_wdt が自動的に読み込まれていた (^^;) ので、 このウォッチドッグ・タイマーの存在に気づいた、という次第。 /dev/watchdog に何か書込んでみるだけで (例えば「echo @ > /dev/watchdog」を実行)、 タイマーがスタートした (/dev/watchdog が存在しない場合は、 「mknod /dev/watchdog c 10 130」を実行する)。

そして 30秒後、勝手にリセットがかかった (めでたしめでたし)。

ウォッチドッグ タイマというと、 普通は 60秒くらいに設定しておくものだとは思うが、 自宅サーバの場合、一時間くらいハング状態が続いてもそんなに困らない ;) のと、 あまりタイマの間隔が短すぎると、 不用意に再起動してしまう恐れもあるので、 3600秒 (一時間) に設定している。 つまり一時間以内にタイマをリセットしないと、 自動再起動が行なわれる。
仙石浩明の日記: ウォッチドッグ タイマ から引用

じゃ、iTCO_wdt.ko モジュールでも同様に heartbeat=3600 を指定すればいいのかな と思っていたら、 heartbeat は最大 613 秒までしか設定できない (TCO v2 の場合) ようである。 わずか 10分足らずでは不用意に再起動してしまう恐れ大。 そこで、 監視プログラムが直接 /dev/watchdog に「蹴り」を入れる代わりに、 監視プログラムは /var/run/watchdog に「蹴り」を入れることにして、 /var/run/watchdog を監視して /dev/watchdog に「蹴り」を入れる 「蹴り代行」デーモンを走らせておくことにした。

つまり、監視プログラムは 20分に一度 /var/run/watchdog に「蹴り」を入れるだけで、 あとは「蹴り代行」デーモンが 5秒に一度、 /dev/watchdog に「蹴り」を入れ続けてくれる。 だからウォッチドッグ・タイマーのドライバの設定は、 デフォルト (30秒) のままで済むし、 また「蹴り代行」デーモンの設定次第で、 20分といわずもっと長い余裕を持たせることも可能。

/service/watchdog/run

#!/usr/bin/perl
use strict;
use warnings;
$| = 1;
my $watchdog_uid = getpwnam("adsl_check");
my $watchdog_gid = getgrnam("watchdog");
my $watchdog_file = "/var/run/watchdog";
my $watchdog_dev = "/dev/watchdog";
print "start\n";
if (! -f $watchdog_file) {
    if (!open(WATCHDOG, ">$watchdog_file")) {
	print "can't create $watchdog_file exiting...\n";
	exit 1;
    }
    close(WATCHDOG);
    chown $watchdog_uid, $watchdog_gid, $watchdog_file;
}
($(, $)) = ($watchdog_gid, $watchdog_gid);
($<, $>) = ($watchdog_uid, $watchdog_uid);
while (-z $watchdog_file) {
    sleep 5;
}
print "confirmed $watchdog_file\n";
truncate($watchdog_file, 0);
if (!open(WATCHDOG, ">$watchdog_dev")) {
    print "can't open $watchdog_dev exiting...\n";
    exit 1;
}
select(WATCHDOG);
$| = 1;
select(STDOUT);
for (my $i=0; $i < 240; $i++) {
    print WATCHDOG "\@\n";
    sleep 5;
}
print WATCHDOG "\@\n";
close(WATCHDOG);
print "exiting...\n";
exit 0;

「/service/watchdog/run」というパス名からも分かる通り、 このスクリプトは daemontools 配下で動かしている。 このスクリプトは、 20分間 (5 秒 * 240) /dev/watchdog に蹴りを入れ続けると終了する。 そして daemontools がこのスクリプトを再起動すると、 /var/run/watchdog の存在を確認した上で再び蹴りを入れ続ける。 つまり、 20 分間以上 /var/run/watchdog に蹴りが入れられないと、 この「蹴り代行」スクリプトは止ってしまい、 /dev/watchdog への蹴りも止ってしまう。

ここでなぜ 20分毎にこのスクリプトを終了するようにしているかというと、 daemontools の動きも監視対象に含めたいから。 つまり、システムの負荷が高くなり過ぎて daemontools による再起動に時間がかかるような事態になっても、 /dev/watchdog への蹴りが止る。

まとめると、 /var/run/watchdog への蹴りが止ったり、 あるいは daemontools による再起動が滞ったりすると、 /dev/watchdog への蹴りも止ってしまって、 ウォッチドッグ・タイマーが時間切れになり、 ハードウェア的に自動リセットがかかる、という仕掛け。

私は他のマシンから

ssh server "echo '@' > /var/run/watchdog"

などと ssh でアクセスするよう cron に設定している。 ssh が成功すれば /var/run/watchdog へ書き込み、 すなわち蹴りが入れられるので、 蹴り代行スクリプトによってウォッチドッグ・タイマーに蹴りが入れられる。

Filed under: システム構築・運用,ハードウェアの認識と制御 — hiroaki_sengoku @ 07:19
2007年9月18日

NFS と AUFS (Another Unionfs) を使って、ディスクレス (diskless) サーバ群からなる低コスト・高可用な大規模負荷分散システムを構築する

ディスクレス (diskless) サーバを多数運用しようとしたときネックとなるのが、 NAS (Network Attached Storage) サーバの性能。 多数のディスクレスサーバを賄え、かつ高信頼な NAS サーバとなると、 どうしても高価なものになりがちであり、 NAS サーバ本体の価格もさることながら、 ディスクが壊れたときの交換体制などの保守運用費用も高くつく。

それでも、多数のハードディスク内蔵サーバ (つまり一般的なサーバ) を 運用して各サーバのディスクを日々交換し続ける (運用台数が多くなると、 毎週のようにどこかのディスクが壊れると言っても過言ではない) よりは、 ディスクを一ヶ所の NAS にまとめたほうがまだ安い、 というわけで NAS/SAN へのシフトは今後も進むだろう。 そもそも CPU やメモリなどとハードディスクとでは、 故障率のケタが違うのだから、 両者の台数を同じように増やせば破綻するのは当たり前。 要は、滅多に故障しないものは増やしてもいいが、 普通に故障するものは増やしてはいけない。 サーバの部品で故障する確率が桁違いに高いのはハードディスクだから、 大規模負荷分散環境においてディスクレス化は論理的必然だろう。

ハードディスクの故障率が高いのは可動部品が多いから、 というわけでハードディスクをフラッシュメモリで 置き換えようとする傾向もあるようだ。 確かに高価な NAS サーバを導入するよりは、 各サーバにフラッシュメモリを搭載する方が安上がりである可能性もある (比較的低容量であれば)。 しかしながら、 以下に述べるように NAS サーバを普通の PC サーバで実現できてしまえば、 ディスクレス化のほうが安いのは当たり前である。 サーバの台数が多くなればなるほど、 各サーバにディスク/フラッシュメモリを必要としない ディスクレス方式の方が有利になる。

とはいえ、 PC サーバの価格に慣れてしまうと、 超高価な専用サーバの世界にはもう戻れない。 そこで、 どうすればサーバ群のディスクレス化を、 低コストで行なうことができるか考えてみる。 そもそもなぜ NAS サーバが高価かと言えば、 高パフォーマンス性と高信頼性を兼ね備えようとするから。 多数のディスクレスサーバにストレージサービスを提供するのだから 高パフォーマンス性は譲れない。 となると犠牲にしてよいのは高信頼性ということになる。

信頼できなくてもよい、 つまり時々ディスクが壊れて、 書込んだはずのデータが失われても良いなら、 そこそこ高性能な PC サーバで用が足りてしまうだろう。 壊れた場合に備えて冗長化しておけば、 高信頼ではないものの、無停止性は達成できる。 もちろんマスターサーバが壊れてスレーブサーバに切り替われば、 マスターサーバにしか書込めなかったデータは失われる。

そんな NAS サーバは使えない、と言われてしまいそうであるが (^^;)、 ディスクに書込むデータで消えては困るデータとは何だろうか? 例えば Web サーバなどでは、 永続化が必要なデータは DB サーバへ書込むのが普通で、 それ以外のデータは消えては困るとは必ずしも言えないのではないか? というか消えては困るデータは DB サーバへ書けばいいのである。

さらに考えを一歩進めて、 ディスクに書込むデータは消えてもよい、 と割りきってしまうことができれば、 NAS サーバにデータを書く必要性すらなくなってしまう。 つまり NAS サーバのディスクを読み込み専用 (Read Only 以下 RO と略記) でマウントし、 書き込みはローカルな読み書き可能な (Read Write 以下 RW と略記) RAM ディスクに対して行なう。 NAS サーバは RO だから 内容が同じ NAS サーバを複数台用意して負荷分散させれば、 高パフォーマンスと故障時のフェールオーバを同時に達成できてしまう。 NAS サーバのクラスタリングが難しいのはデータを書込もうとするからであって、 書込む必要がなければ話は一気に単純になる。

もちろん全く何も書込めない NAS サーバというのはナンセンスだろう。 ここで「書く必要がない」と言っているのは、 アプリケーション実行中にアプリケーションの動作に同期して (つまり動作結果を) 「書く」必要性である。 アプリケーションとは非同期な書込み、 例えば何らかのコンテンツを配信する Web サーバを考えたとき、 あらかじめ大量の「コンテンツ」を NAS サーバへ事前に保存しておく場合や、 あるいは「コンテンツ」を定期的に更新する場合は、 (アプリケーションの動作結果とは無関係な書き込みなので) Web アプリケーションが NAS へ書込む必要はない。
むしろ、 大量の「コンテンツ」を NAS サーバに集中することは、 コンテンツ更新が素早く行なえるというメリットとなる。 多数の Web サーバそれぞれにハードディスクを内蔵して 同じコンテンツをコピーしていては、 コンテンツの更新頻度が上がってくると 全 Web サーバの内容を同期させるのが難しくなってくるからだ。

ここで重要なのは、 上記「RO NAS サーバ + RW ローカル RAM ディスク」が、 ディスクレスサーバ上で動くソフトウェア (例えば Web アプリケーションやミドルウェア) から見ると、 普通の RW ローカルディスク (つまり普通に書込み可能なハードディスク) に見えなければならないという点である。 もしソフトウェア側で特別な対応が必要だと、 ソフトウェアの改修コストがかかってしまう。 ハードウェアのコストを下げようとして ソフトウェアのコストが上がってしまっては本末転倒である。 ディスク上のデータが RO な NAS サーバから読み込まれたものであり、 ディスクへ書込んだデータが、実は RAM ディスクに書込んだだけで、 再起動によって消えてしまうものであったとしても、 ソフトウェアから見れば、 普通の RW ハードディスクディスクのように 振る舞わなければならないのである。

このように、 複数のディスク (RO NAS と RW RAM ディスク) を重ねて 一つのディスクとして見せる仕掛けを、 重ね合わせ可能な統合ファイルシステム (Stackable Unification File System) と呼ぶ。 Linux 2.6.20 以降の場合、 二種類の統合ファイルシステムが利用可能である。 後発の Aufs (Another Unionfs) を利用して、 ディスクレスサーバを作ってみた。
(あいかわらず) 前フリが長いが (^^;)、ここからが本題である。

More...
Filed under: システム構築・運用,プログラミングと開発環境 — hiroaki_sengoku @ 06:53
2007年8月21日

SPF (Sender Policy Framework) チェックをパスしてしまう迷惑 (スパム) メールが増えている

私の自宅サイト GCD (と勤務先の KLab) では、 qmail にパッチをあてて、 外部から届くメールのヘッダに SPF (Sender Policy Framework) チェックの結果を挿入するようにしている。 例えば以下のような感じ:

Received-SPF: pass (senri.gcd.org: SPF record at thelobstershoppe.com designates 89.215.246.95 as permitted sender)
Message-ID: <34f301c7df7d$2e65ece5$5ff6d759@unknown.interbgc.com>
From: "Sales Department" <sales@thelobstershoppe.com>

「Received-SPF: pass」というのは、 このメール (実は迷惑メール) の送信者のドメイン thelobstershoppe.com では、 メールを送信する「正規」の IP アドレスの集合 (SPF レコード) を公表していて、 その中にこのメールの送信元 IP アドレス 89.215.246.95 が、 含まれていることを意味する。

つまり thelobstershoppe.com ドメイン管理者の公認 IP アドレスから 迷惑メールが送信されてきたわけで、 (1) ドメイン管理者の意図に反して不正なメール送信が行なわれた (つまり管理に不備がある) か、 (2) ドメイン管理者に意図に沿って迷惑メール送信が行なわれた (つまりドメインの管理者がスパマー)、 ということになる。

ドメイン管理者の意図がどちらであるかは実際に話を聞いてみない限りは 厳密には判別不能であろうが、 仮に (1) であったとしても、 不正なメール送信を許すような管理体制のドメインからのメールは、 なるべく受取りたくないものである。

「Received-SPF: pass」な迷惑メールが送られてきたら、 基本的にはそのドメインからのメールは今後受取らないように、 送信者メールアドレスのドメインのブラックリストを更新してきた。 ところが、最近、「Received-SPF: pass」な迷惑メールを受取る機会が 妙に増えたような気がする。 いったいどんな SPF レコードなのかと調べてみると...

% foreach dom (thelobstershoppe.com scottjanos.com bode-research.com \
ambiguouslyblack.com onairrewards.com mailtong.org artoffurnitureworkshop.com)
foreach? host -t txt $dom
foreach? end
thelobstershoppe.com descriptive text "v=spf1 +all"
scottjanos.com descriptive text "v=spf1 +all"
bode-research.com descriptive text "v=spf1 +all"
ambiguouslyblack.com descriptive text "v=spf1 +all"
onairrewards.com descriptive text "v=spf1 +all"
mailtong.org descriptive text "v=spf1 mx ip4:125.187.32.0/16 ~all"
artoffurnitureworkshop.com descriptive text "v=spf1 +all"

なんと、「Received-SPF: pass」な迷惑メールのドメインの大半が、 「+all」を指定していた。 つまり、 全ての IP アドレスがメール送信 IP アドレスとして「正規」なものである、 と主張しているわけである。 こんな主張は SPF の主旨から考えると全くナンセンスであるから、 対抗措置をとらねばなるまい。

SPF レコードに「+all」を含むドメインは、 自動的に迷惑メール送信元と判断するようにしてしまおうかと (一瞬 ;-) 考えたのであるが、 設定ミス等で「+all」を指定してしまっているケースもあるかもしれない。 そこで、

diff -u spf.c.org spf.c
--- spf.c.org	2007-08-20 16:09:13.000000000 +0900
+++ spf.c	2007-08-20 16:11:43.000000000 +0900
@@ -543,7 +543,7 @@
   unsigned int filldomain  : 1;
   int defresult            : 4;
 } mechanisms[] = {
-  { "all",      0,          0,0,0,0,SPF_OK   }
+  { "all",      0,          0,0,0,0,-1   }
 , { "include",  spf_include,1,0,1,0,0        }
 , { "a",        spf_a,      1,1,1,1,0        }
 , { "mx",       spf_mx,     1,1,1,1,0        }
@@ -784,6 +784,10 @@
 					q = spfmech(spf.s + begin, 0, 0, domain->s);
 			}
 
+			if (q == -1) {
+					if (prefix == SPF_OK) q = SPF_UNKNOWN;
+					else q = SPF_OK;
+			}
 			if (q == SPF_OK) q = prefix;
 
 			switch(q) {

という修正を行なってみた (qmail-1.03 + qmail-spf-rc5.patch に対するパッチ)。 これで、IP アドレスが「+all」にマッチした場合は、 「SPF_OK」の代わりに「SPF_UNKNOWN」を返すようになるので、 ヘッダには「Received-SPF: neutral」が挿入される。

例えば、haywardins.com の SPF レコードは、「v=spf1 +all」であるが、 以下のように「Received-SPF: neutral」が挿入される。

Received: from unknown (HELO 6.124.18.84.in-addr.arpa) (84.18.124.6)
  by senri.gcd.org with SMTP; 20 Aug 2007 10:22:30 +0000
X-Country: RU 84.18.124.6
Received-SPF: neutral (senri.gcd.org: 84.18.124.6 is neither permitted nor denied by SPF record at haywardins.com)
Message-ID: <456801c7e313$07cb3524$067c1254@6.124.18.84.in-addr.arpa>
- o -

上記 qmail-spf-rc5.patch もそうだが、 SPF 実装の多くが、 デフォルトで Trusted Forwarder ホワイトリストを参照する設定を推奨している。 例えば qmail-spf-rc5.patch の場合であれば、

spfrules
You can specify a line with local rules.
Local rules means: Rules that are executed before the real SPF rules for a domain would fail (fail, softfail, neutral).
They are also executed for domains that don't publish SPF entries.
I suggest adding  include:spf.trusted-forwarder.org.
You can also add mechanisms to trust known mail servers like backup MX servers, though I suggest that you should at least also use tcprules (to modify SPFBEHAVIOR).

などと、 「/var/qmail/control/spfrules」に、 「include:spf.trusted-forwarder.org」を追加することを勧めている。 つまり、spf.trusted-forwarder.org の SPF レコードを問合わせよ、 ということだが、実際に問合わせてみると、

% host -t txt spf.trusted-forwarder.org
spf.trusted-forwarder.org descriptive text "v=spf1 exists:%{ir}.wl.trusted-forwarder.org exists:%{p}.wl.trusted-forwarder.org"

というレスポンスが返ってくる。 つまり「%{ir}.trusted-forwarder.org」か「%{p}.wl.trusted-forwarder.org」の どちらかが存在していれば、 ホワイトリストに登録されていることを意味する。 すなわちその IP アドレスは「あらゆる」送信者アドレスのメールを送ることができる 「信頼されたフォワーダ」ということになる。 ここで「%{ir}」は IP アドレスを逆順にした文字列、 「%{p}」は IP アドレスを逆引きして得られるホスト名である。

どんな送信者アドレスのメールも送れる「万能」の IP アドレスというのも、 タイガイにしてほしいと思うが、 The Trusted Forwarder SPF Global Whitelist によれば、 SPF 導入初期のためのものだったようだ。 2004年ごろは、ほとんどのサイトが SPF レコードを公表していなかったわけで、 「信頼されたフォワーダ」のホワイトリストを保持しておく理由があったのだろう (いまいちその必要性がピンとこないが)。

同ページによれば、 このホワイトリストは既に更新されておらず、 早晩リストの内容が削除されるようだ。 「*.wl.trusted-forwarder.org」への無用な問合わせを避けるためにも、 「/var/qmail/control/spfrules」から 「include:spf.trusted-forwarder.org」の記述を削除すべきだろう (qmail の場合)。

Filed under: システム構築・運用 — hiroaki_sengoku @ 06:53
2007年8月16日

CPAN の IP::Country を C で書き直して MTA に組み込み、メールのヘッダに国コードを挿入するようにしてみた

自前のブラックリストを用いて迷惑メール (spam, UBE) を排除する方法について、 「迷惑メール送信者とのイタチごっこを終わらせるために (1)」で説明した。 メールの送信元 IP アドレスが DNS で逆引きできない場合に、 その IP アドレスがブラックリストに載っているか否かを調べ、 もし載っているならその IP アドレスを 「ダイアルアップ IP アドレス」に準じる扱いにする、 という方法である。

迷惑メールを送ってきた実績 (?) がある IP アドレスブロックであれば、 ためらうこと無くブラックリストに入れてしまえるのであるが、 初めてメールを送ってきた IP アドレスブロックを、 逆引きできないという理由だけでダイアルアップ IP アドレス扱いするのは、 少々乱暴だろう。 そこで、 接続元 IP アドレスが属する国のコードをメールヘッダに挿入する仕掛けを MTA (Message Transfer Agent, メールサーバ) に作り込んでみた。例えば、

Received: from unknown (HELO unknown.interbgc.com) (89.215.246.95)
  by senri.gcd.org with SMTP; 15 Aug 2007 20:46:15 +0000
X-Country: BG 89.215.246.95
Received-SPF: pass (senri.gcd.org: SPF record at thelobstershoppe.com designates 89.215.246.95 as permitted sender)
Message-ID: <34f301c7df7d$2e65ece5$5ff6d759@unknown.interbgc.com>

といった感じで、「X-Country: 」フィールドが挿入される。 「89.215.246.95」がこのメールを送ってきたマシンの IP アドレスであり、 その前の「BG」が、 この IP アドレスが属する国 (この例ではブルガリア) の ISO 3166 コード である。

ブルガリアに知り合いがおらず、 かつこのメールがメーリングリスト宛でなく個人アドレス宛であるならば、 MUA (Message User Agent, メーラー) の設定で、 このメールを迷惑メールとして排除することが可能だろう。 あるいは逆に、 「X-Country: JP」である場合は、 迷惑メール判定の結果にかかわらず排除しないという設定にして、 必要なメールを誤って排除するのを防止することもできるだろう (日本語の迷惑メールも、大半は海外の IP アドレスから送信されている)。

IP アドレスから国コード (ISO 3166 コード) を調べるサービスはいろいろあるが、 メールを受信するたびに外部のサイトへ通信するのはあまり感心しない。 ネットワークないし外部のサイトの状況の影響を受けてしまうし、 あるいは逆に大量のメールを一時に受信したときなど、 そのサイトに迷惑をかけてしまう恐れもある。 集中して問合わせを行なってしまった、などの理由で濫用と判断され、 サービスの提供が受けられなくなってしまう可能性もある。

したがって、IP アドレスから国コードを検索するためのデータベースを 自前で持つことが望ましい。 例えば CPAN には、 IP アドレスから国コードを検索するモジュール 「IP::Countryが 登録されている。 このモジュールをインストールすると、 「/usr/lib/perl5/site_perl/5.8.8/IP/Country/Fast」ディレクトリに、 「ip.gif」と「cc.gif」というファイルがインストールされる。

% ls -l /usr/lib/perl5/site_perl/5.8.8/IP/Country/Fast
total 256
-r--r--r-- 1 perl perl    681 Feb  2  2007 cc.gif
-r--r--r-- 1 perl perl 252766 Feb  2  2007 ip.gif

「ip.gif」が、IP アドレスから国番号を検索するためのデータベースであり、 「cc.gif」が、国番号から国コード (ISO 3166 コード) への変換テーブルである。

メールを受信するたびにメールサーバで perl スクリプトを実行するのは、 メールサーバの負荷などの観点からあまり望ましくない (私のサイトではメールサーバを chroot 環境で動かしていて、 その chroot 環境には perl をインストールしていない、 というセキュリティ上の理由もある) ので、 ほとんど perl スクリプトをそのまま C に置き換えただけなので、 説明は不要だろう。 inet_ntocc 関数に限ると、 C 版のほうが perl 版より簡潔に書けてしまっている点が興味深い。 コメントは、IP/Country/Fast.pm スクリプトのコメントをそのまま入れてある。

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

#ifndef DBDIR
#define DBDIR "/usr/lib/perl5/site_perl/5.8.8/IP/Country/Fast"
#endif
#define CC_MAX	256	/* # of countries */

int inet_ntocc(u_char *ip_db, u_long inet_n) {
/*
  FORMATTING OF EACH NODE IN $ip_db
  bit0 - true if this is a country code, false if this
         is a jump to the next node
  
  country codes:
    bit1 - true if the country code is stored in bits 2-7
           of this byte, false if the country code is
           stored in bits 0-7 of the next byte
    bits 2-7 or bits 0-7 of next byte contain country code
  
  jumps:
    bytes 0-3 jump distance (only first byte used if
           distance < 64)
*/
    u_long mask = (1 << 31);
    const u_long bit0 = 0x80;
    const u_long bit1 = 0x40;
    int pos = 4;
    u_char byte_zero = ip_db[pos];
    /* loop through bits of IP address */
    while (mask) {
	if (inet_n & mask) {
	    /* bit[$i] is set [binary one]
	       - jump to next node
	       (start of child[1] node) */
	    if (byte_zero & bit1) {
		pos = pos + 1 + (byte_zero ^ bit1);
	    } else {
		pos = pos + 3 + ((ip_db[pos] << 8 | ip_db[pos+1]) << 8
				 | ip_db[pos+2]);
	    }
	} else {
	    if (byte_zero & bit1) {
		pos = pos + 1;
	    } else {
		pos = pos + 3;
	    }
	}
	/*
	  all terminal nodes of the tree start with zeroth bit 
	  set to zero. the first bit can then be used to indicate
	  whether we're using the first or second byte to store the
	  country code */
	byte_zero = ip_db[pos];
	if (byte_zero & bit0) {
	    if (byte_zero & bit1) {
		/* unpopular country code - stored in second byte */
		return ip_db[pos+1];
	    } else {
		/* popular country code - stored in bits 2-7
		   (we already know that bit 1 is not set, so
		   just need to unset bit 1) */
		return byte_zero ^ bit0;
	    }
	}
	mask = (mask >> 1);
    }
    return -1;
}

u_char *getdb(char *file, int *fdp, int *lenp) {
    char path[PATH_MAX+1];
    int fd;
    struct stat st;
    int length;
    u_char *db;
    snprintf(path, PATH_MAX, "%s/%s", DBDIR, file);
    path[PATH_MAX] = '\0';
    fd = open(path, O_RDONLY);
    if (fd < 0) {
	fprintf(stderr, "Can't open: %s err=%d\n", path, errno);
	exit(1);
    }
    if (fdp) *fdp = fd;
    if (fstat(fd, &st) < 0) {
	fprintf(stderr, "Can't stat: %s fd=%d err=%d\n", path, fd, errno);
	exit(1);
    }
    length = st.st_size;
    if (lenp) *lenp = length;
    db = (u_char*)mmap((void*)0, length, PROT_READ, MAP_SHARED, fd, 0);
    if (db == MAP_FAILED) {
	fprintf(stderr, "Can't map: %s fd=%d len=%d err=%d\n",
		path, fd, length, errno);
	exit(1);
    }
    return db;
}

int main(int argc, char *argv[]) {
    int i;
    u_char *ip_db = getdb("ip.gif", NULL, NULL);
    const char *_cc = 
	"USDEGBNL--FREUBEITESCACHRUSEAUAT"
	"PLCZIEFIJPDKNOUAZANGILROGRCNPTHU"
	"INTRIQSGHKCYIRLTNZKRLUBGAEARBRSI"
	"IDCLSKTWSAMYTHYUMXLVCOPHLBPKKZGH"
	"EETZKWKERSBDHRDZEGVECMPEECLIPRGE"
	"ISMEAPBYMGMDAOCIMTSOPAZWVNBANEBH"
	"PSJOCDMZAZMAUZDOBJAMUGSLCGGNZMMU"
	"TJMCANMWJMGIMKCRBMLRBOUYGTBWLKGP"
	"ALGAMQKGTTNASVLYMNMRAFNPSNKHBBQA"
	"CUGLBNOMMOPYSYPGNISMMLSCDJSZLSBF"
	"CFGUNCVGHNFJPFTDLAYEFOBISDGQRWKY"
	"**BSSRADGMCVGDKNETERRETNTMTGYTMV"
	"VIHTKMSTGWAGVABZBTNRTOFKKIVUMPWS"
	"MMAWSBJEGFAIAQIOGYNFLCPWCKDMAXFM"
	"TVNUAS--------------------------"
	"--------------------------------";
#ifdef CHECKCC
    int cc_fd;
    int cc_len;
    int cc_num;
    u_char *cc_db = getdb("cc.gif", &cc_fd, &cc_len);
    char cc[CC_MAX * 2 + 1];
    cc_num = cc_len / 3;
    if (cc_num < 0 || CC_MAX <= cc_num) {
	fprintf(stderr, "Can't happen: irregular CC DB cc_num=%d\n", cc_num);
	exit(1);
    }
    for (i=0; i < CC_MAX; i++) {
	cc[i*2] = '-';
	cc[i*2+1] = '-';
    }
    cc[i*2] = '\0';
    for (i=0; i < cc_num; i++) {
	u_char c = cc_db[i*3];
	cc[c*2] = cc_db[i*3+1];
	cc[c*2+1] = cc_db[i*3+2];
    }
    munmap(cc_db, cc_len);
    close(cc_fd);
    if (strcmp(cc, _cc) != 0) {
	for (i=0; i < CC_MAX; i+=16) {
	    int j;
	    printf("\"");
	    for (j=0; j < 16; j++) {
		printf("%c%c", cc[(i+j)*2], cc[(i+j)*2+1]);
	    }
	    printf("\"\n");
	}
    }
#else
    const char *cc = _cc;
#endif
    for (i=1; i < argc; i++) {
	u_long in = ntohl(inet_addr(argv[i]));
	int c = inet_ntocc(ip_db, in);
	if (c < 0) {
	    printf("UNKNOWN %s\n", argv[i]);
	} else {
	    printf("%c%c %s\n", cc[c*2], cc[c*2+1], argv[i]);
	}
    }
    return 0;
}

国コードへの変換テーブル「cc.gif」は、 変更頻度もさほど高くないだろうと思われたので、 プログラム中に固定文字列として定義している。 コンパイル時に「-DCHECKCC」を指定することにより、 cc.gif と内蔵の変換テーブルが一致するかチェックできる。

あとは、このコードを MTA に組み込むだけ。 私のサイトでは qmail を 使っているので、 qmail-smtpd.c にこのコードを組み込んだ。

Filed under: システム構築・運用,プログラミングと開発環境 — hiroaki_sengoku @ 07:42
2007年8月3日

ダイナミックDNSサービスを始めてみた

GCDバックアップ回線は、 (費用節約のため ^^;) 固定IPアドレス契約をしていない。 また出先でノートPC を使うときなども IPアドレスは固定でないわけで、 そういったときにもホスト名を持たせられる ダイナミックDNSサービス が便利。

ダイナミックDNSサービスというと、 DynDNS.org, freedns.afraid.org, ZoneEdit.com, No-IP.com, ieServer.Net, ddo.jp などが有名であるが、 いろいろ実験したいときなど、 自前のダイナミックDNSサービスがあると何かと便利なので立ち上げてみた。

大抵のダイナミックDNSサービスは、濫用防止の仕掛けがあって、 アドレス更新頻度が高いとすぐサービス利用を拒否されてしまう。 実験の時など意図せず何度も更新へ行ってしまうこともあり得るわけで、 そのたびに利用を拒否されては実験が滞ってしまうし、 そもそも人様のサーバを実験につきあわせてしまっては申し訳ない。

http://ddns.gcd.jp/register に アクセスして、 ホスト名とパスワード (と captcha) を入力して、 「register」ボタンを押すと、 「ホスト名.gcd.jp」という FQDN を DNS に登録する。

そして、 http://ddns.gcd.jp/update

ユーザ名:登録したホスト名
パスワード:登録したパスワード

でアクセスすると、 アクセス元のグローバル IP アドレスが登録される。 アクセス元と異なる IP アドレスを登録したい時は、 http://ddns.gcd.jp/update?ip=127.0.0.1 などと「ip」をパラメータとして指定すればよい。 もちろんこのサービスは実験目的で立ち上げたものであり、 安定運用を保証するものではない。 予告無くサービスを停止あるいは制限を加えることを 了承いただけるかたにのみ利用を許諾する。

GnuDIP など、 ダイナミックDNS サービスを実現するソフトウェアはいくつかあるようであるが、 Web アプリケーションとして行なうべき事は極めて単純 (ホスト名登録と IPアドレス更新のページだけ) なので、 ざくっと php で書いてみた(わずか 250行)。

ネームサーバは MyDNS を使用している。 MyDNS というのは MySQL (あるいは PostgreSQL) のレコード (一例を以下に示す) を そのままゾーンレコードとして扱えるネームサーバ。 つまり php スクリプトから MySQL データベースを更新するだけで ダイナミックDNS サービスを実現できる。

mysql> select * from rr;
+-----+------+-------------+-------+----------------+-----+-------+
| id  | zone | name        | type  | data           | aux | ttl   |
+-----+------+-------------+-------+----------------+-----+-------+
|   1 |    1 |             | NS    | ns.gcd.jp.     |   0 | 14400 |
|   2 |    1 |             | A     | 60.32.85.216   |   0 | 14400 |
|   3 |    1 |             | MX    | mx.gcd.org.    |  10 | 14400 |
|   4 |    1 | *           | MX    | mx.gcd.org.    |  10 | 14400 |
|   5 |    1 | ns          | A     | 60.32.85.217   |   0 | 14400 |
|   6 |    1 | www         | CNAME | gcd.jp.        |   0 | 14400 |
|   7 |    1 | ddns        | CNAME | gcd.jp.        |   0 | 14400 |
    (中略)
| 106 |    1 | senri       | A     | 60.32.85.220   |   0 |    50 |
| 107 |    1 | asao        | A     | 60.32.85.221   |   0 |    50 |
    (中略)
+-----+------+-------------+-------+----------------+-----+-------+

MyDNS は listen するポートを、 「listen = 127.0.0.1:53」などといった形式で指定するが、 IP アドレスとして 0.0.0.0 を指定する (つまりインタフェースを指定せずに listen させる) ことができないので、 以下のようなパッチをあてて使っている (2行コメントアウトしただけ)。

--- src/mydns/listen.c.org	2006-01-19 05:46:47.000000000 +0900
+++ src/mydns/listen.c	2007-08-02 13:46:33.000000000 +0900
@@ -81,8 +81,8 @@
 	if (family == AF_INET)
 	{
 		memcpy(&addr4, address, sizeof(struct in_addr));
-		if (addr4.s_addr == INADDR_ANY)
-			return;
+/*		if (addr4.s_addr == INADDR_ANY)
+			return;		*/
 	}
 #if HAVE_IPV6
 	else if (family == AF_INET6)

PPPoE などでインターネットへ接続するサーバ (つまりルータ兼用サーバ) で ネームサーバを走らせようとする場合、 インタフェースを指定せず (INADDR_ANY) bind する ほうが何かと都合がよい。 にもかかわらず、 ネームサーバがインタフェース毎に bind しようとするのは何故なのだろうか。 久しく使っていないが、 BIND も そういう仕様だったと記憶している。

Filed under: システム構築・運用 — hiroaki_sengoku @ 06:29
« Newer PostsOlder Posts »