システム構築・運用

このエントリーを含むブックマーク 2008年03月19日

昨年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 通信を利用しているかたは、 たとえフレッツ・ドットネットサービスを利用していなくても、 フレッツ・ドットネットを解約すべきではないので、 ご注意のほどを!



hiroaki_sengoku at 15:32|この記事のURLComments(0)TrackBack(0)
このエントリーを含むブックマーク 2008年01月31日

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 以降を使って 送信していることが分かる。



hiroaki_sengoku at 08:55|この記事のURLComments(0)TrackBack(0)
このエントリーを含むブックマーク 2007年10月12日

極めて稀とはいえ、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 へ書き込み、 すなわち蹴りが入れられるので、 蹴り代行スクリプトによってウォッチドッグ・タイマーに蹴りが入れられる。



hiroaki_sengoku at 07:19|この記事のURLComments(0)TrackBack(1)
このエントリーを含むブックマーク 2007年09月18日

ディスクレス (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) を利用して、 ディスクレスサーバを作ってみた。
(あいかわらず) 前フリが長いが (^^;)、ここからが本題である。

続きを読む

hiroaki_sengoku at 06:53|この記事のURLComments(0)TrackBack(1)
このエントリーを含むブックマーク 2007年08月21日

私の自宅サイト 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 の場合)。



hiroaki_sengoku at 06:53|この記事のURLComments(0)TrackBack(0)
このエントリーを含むブックマーク 2007年08月16日

自前のブラックリストを用いて迷惑メール (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 をインストールしていない、 というセキュリティ上の理由もある) ので、 IP::Country::Fast の perl スクリプトを C 言語で書き直してみた。 ほとんど 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 にこのコードを組み込んだ。



hiroaki_sengoku at 07:42|この記事のURLComments(0)TrackBack(1)
このエントリーを含むブックマーク 2007年08月03日

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 も そういう仕様だったと記憶している。



hiroaki_sengoku at 06:29|この記事のURLComments(1)TrackBack(0)
このエントリーを含むブックマーク 2007年07月04日

ssh の ssh-agent は便利であるが、 ForwardAgent 機能を使う場合は注意が必要である。

あるマシン (ここでは senri とする) で、ssh-agent を実行すると、 ssh-agent は ssh 認証のための UNIX ドメイン ソケットを作成し、 そのパス名を環境変数「SSH_AUTH_SOCK」に設定する (正確に言えば環境変数を設定するのはシェルの組み込みコマンドである eval)。

senri:/home/sengoku % eval `ssh-agent`
Agent pid 14610
senri:/home/sengoku % ssh-add
Enter passphrase for /home/sengoku/.ssh/id_rsa:
Identity added: /home/sengoku/.ssh/id_rsa (/home/sengoku/.ssh/id_rsa)

続いて、ssh-add を実行し、 このソケットを通して ssh 秘密鍵を ssh-agent に登録する。 すると ssh (あるいは scp など) を passphrase を入力することなく使用できる。

senri:/home/sengoku % ssh mail1.v6.klab.org
Last login: Tue Jul  3 18:22:13 2007 from ishibashi.klab.org
mail1:/home/sengoku %

もしマシン senri が、きちんと管理されていて、 かつ root 権限を持っている人が自分一人しかいないのであれば、 SSH_AUTH_SOCK ソケットを他者に使われる心配はないので、 ssh-agent を走らせっぱなしにしておくことができて、 ssh を常に passphrase 無しで使うことができて便利である。

さらに ssh の ForwardAgent 機能を使うと、 senri 以外のマシンでも senri 上の ssh-agent を利用できる。

senri:/home/sengoku % ssh -A mail1.v6.klab.org
Last login: Wed Jul  4 07:13:46 2007 from senri.v6.gcd.org
mail1:/home/sengoku % echo $SSH_AUTH_SOCK
/tmp/ssh-dHyPx12011/agent.12011
mail1:/home/sengoku % ls -la /tmp/ssh-dHyPx12011
total 0
drwx------    2 sengoku  sengoku        80 Jul  4 07:15 .
drwxrwxrwt    8 root     root          368 Jul  4 07:15 ..
srwxr-xr-x    1 sengoku  sengoku         0 Jul  4 07:15 agent.12011
mail1:/home/sengoku % ssh mail2 hostname
mail2.klab.org
mail1:/home/sengoku %

つまりマシン mail1 上の sshd (ssh サーバ) が、 UNIX ドメイン ソケット /tmp/ssh-dHyPx12011/agent.12011 を作成し、 そのパス名を環境変数「SSH_AUTH_SOCK」に設定した上でログイン シェルを実行する。 ここで ssh を実行すると、このソケットを経由して senri の SSH_AUTH_SOCK ソケットへつながる (「ポート」ではないがポートフォワードと同様) ので、 passphrase 入力を求められない。

さらに、mail1 から別のマシンへ ssh でログインするときも、 同様に ForwardAgent を使うことができるので、 ForwardAgent の連鎖が続く限り ログインした全てのマシンで ssh を passphrase 無しで使うことができる。

以上のように、ForwardAgent は大変便利な機能であるが、 ログインした先のマシンの root 権限を持っている人が自分以外にもいる場合は、 注意が必要である。 例えば上記の例で言うと、 mail1 の root 権限を持っている人は、 UNIX ドメイン ソケット /tmp/ssh-dHyPx12011/agent.12011 へアクセスできる。 つまり、その人は senri 上で動いている私の ssh-agent すなわち私の ssh 秘密鍵を (passphrase 無しで) 利用することができてしまう。

もちろん、そんな悪いことをするヤツを root にしてはいけないのであるが、 ForwardAgent の連鎖を続けすぎると、 ログインしたマシンの中には 100% 信用できるとは限らない人が root 権限を持っている場合もあるだろう。 仮に全てのマシンの root 全員が 100% 信用できたとしても、 常に最悪のケースを考えるのが「セキュリティ」の基本である。 passphrase 無しで ssh を使うという利便性を優先したいのであれば、 せめて悪用されたときに即座に気づく仕掛けを組み込んでおきたいものである。

といっても、不正な利用と正当な利用とを区別することは一般に困難である。 悪用しようとする人がそこそこ注意深ければ、 正当な利用と判別しにくくなるような工夫は可能であろう。 そこで、正当な利用であろうが悪用であろうが、 ssh-agent へのアクセスがあったときは常に知らせる方法について考えてみる。

ssh-agent が利用ログを出力するように変更

ssh-agent.c に次のような変更を加えて、 ssh-agent がアクセスされたときは syslog へ出力するようにしてみる。

--- ssh-agent.c.org	2007-02-28 19:19:58.000000000 +0900
+++ ssh-agent.c	2007-06-28 22:41:19.000000000 +0900
@@ -734,7 +734,7 @@
 		return;
 	}
 
-	debug("type %d", type);
+	logit("type %d", type);
 	switch (type) {
 	case SSH_AGENTC_LOCK:
 	case SSH_AGENTC_UNLOCK:

この変更で ssh-agent へのアクセスがあったときは syslog へ出力が行なわれる。 あとは syslog 側で、 このログ出力をユーザ (つまり私) へ知らせる方法を考えればよい。

私の場合、 認証関係のログは携帯電話へメールで届くように syslog-ng を設定してある。 ssh-agent を利用していないのにケータイ メールが届くようなことがあれば、 ssh-agent が悪用された可能性を考えて調査することになるだろう。

ssh が ForwardAgent 利用ログを出力するように変更

ssh-agent にログを出力させる方法だと、 ローカルホストから ssh-agent を利用する場合にもログ出力を行なってしまう。 もしローカルホストの root 権限を持っている人が自分一人しかいないのであれば、 ログ出力は ForwardAgent 経由の場合に限定してよい。 リスクが全く無いケースのログ出力はできるだけ抑制したほうが、 危険度が高いケースを際立たせることができるので、より望ましいだろう。

clientloop.c と ssh.c に次のような変更を加えて、 ssh がリモートから ForwardAgent 要求を受取ったときに ログ出力するようにしてみる。

--- clientloop.c.org	2007-02-25 18:36:49.000000000 +0900
+++ clientloop.c	2007-07-03 07:02:39.000000000 +0900
@@ -1763,6 +1763,8 @@
 		error("Warning: this is probably a break-in attempt by a malicious server.");
 		return NULL;
 	}
+	logit("sshd %.100s@%.64s requested agent forwarding.",
+	      options.user, host);
 	sock = ssh_get_authentication_socket();
 	if (sock < 0)
 		return NULL;
--- ssh.c.org	2007-01-05 14:30:17.000000000 +0900
+++ ssh.c	2007-07-03 07:15:38.000000000 +0900
@@ -630,7 +630,8 @@
 	channel_set_af(options.address_family);
 
 	/* reinit */
-	log_init(av[0], options.log_level, SYSLOG_FACILITY_USER, 1);
+	log_init(av[0], options.log_level, SYSLOG_FACILITY_AUTH,
+		 (options.log_level > SYSLOG_LEVEL_VERBOSE));
 
 	seed_rng();
 

このような変更を加えると、 ssh でログインした先 (上記の例で言うと mail1) で ssh を使ったとき、 あるいはさらにそこから別のマシンへ ssh でログインした先で ssh を使うなどして、 大元のマシン (上記の例で言うと senri) 上の ssh-agent へアクセスしたときに、 syslog への出力が行なわれる。

ログ出力の際、 「sshd sengoku@mail1.v6.klab.org requested agent forwarding.」などと、 ForwardAgent 要求を行なったのがどのサーバか示せる点も、 ssh-agent に変更を加える方法と比べてこの方法が優れている点と言える (ForwardAgent 連鎖していても、残念ながら一番手前のサーバしか示せないが)。



hiroaki_sengoku at 07:45|この記事のURLComments(0)TrackBack(0)
このエントリーを含むブックマーク 2007年04月16日

よく知られているように、 Windows の「ファイルとフォルダの共有」を、 リモート (例えば社外に持ち出したノートPC) からアクセスする には、 アクセス元マシンの TCP 139番ポートあるいは TCP 445番ポートを、 アクセス先サーバの同番ポートへポートフォワードすればよい。 ポートフォワードの方法としては、 ssh の -L オプション を使ってもよいし、 perl POEstone などを組合わせてフォワードする仕掛けを構築してもよいし、 VPN-Warpを使ってもよい。 要は「local:445」への接続が、 そのまま「remote:445」へ中継されるようにできればよい (以下、アクセス元マシンのホスト名を「local」、 アクセス先サーバのホスト名を「remote」とする)。

アクセス元マシン local の 139番ポートないし 445番ポートが 未使用なら話はここで終わりだが、 local が Windows マシンだったりすると話が少しややこしい。 local が Windows マシンだと普通は 139番ポートも 445番ポートも使用済である。 139番ポートも 445番ポートも、 フォルダを他のマシンのユーザへ共有公開するためのポートであるにもかかわらず、 共有公開しない場合でもこのポートを Windows が使用する (listen する) のは、 Windows の不可解な仕様の一つ。

139番ポートは、 以下に引用するように 「NetBIOS over TCP/IP を無効にする」設定で空けることが可能なので、 「Microsoft Loopback Adapter」などをインストールして 139番ポートが未使用なインタフェースアドレスを確保すればよい。

Windows XP 等で、「ネットワーク接続」の各アダプタのプロパティの中の、 「インターネット プロトコル (TCP/IP)」のプロパティにて、 「詳細設定」を選択すると、 WINS タブの中に、この「NetBIOS over TCP/IP を無効にする」という設定があります。
この設定 (以下、「NBT を無効にする」と略記) はどういう意味でしょうか? 実は、「このアダプタにおいて『NetBIOS 付 SMB』サービスを行なわない」 という意味です。 「NetBIOS over TCP/IP」という表現で 「SMBサービス」を指すのは分かりにくいと思うのですが、 それはサテオキこの設定を行なうと、 「NetBIOS 付 SMB」すなわち 137, 138番ポートと TCP 139番ポートを 使用しなくなります (つまり listen しなくなる)。

Windows XP ならば以上のような方法で、 リモートのファイル/フォルダを共有できた。

しかしながら、 Windows VISTA の場合、そうは問屋がおろさない。 local が Windows VISTA なマシンである場合、 まず 445番ポートへアクセスしに行ってしまう。 445番ポートは local マシンの「ファイルとフォルダの共有」のため、 (たとえ共有公開設定を行なわなかったとしても) Windows VISTA が使用済であり、 かつインタフェースアドレスを指定せずに listen しているので、 「Microsoft Loopback Adapter」などをインストールしたとしても、 445番ポートを空けることができない。

また、VISTA でなくても 139番ポートをポートフォワードして NetBIOS 付 SMB セッションを リモートサーバへ張る方法は、 実はあまり適切ではない。 Windows が「NetBIOS 付」つまり 137番ポートを使った NetBIOS 名の解決を 試みるからだ。 137番ポートはポートフォワードしていないため、 当然この NetBIOS 名の解決は失敗するのだが、 タイムアウトするまで待たされる。

では、どうすればいいか?

NetBIOS 名の解決を抑止するには、 Direct Hosting of SMB (TCP 445番ポート) を使うのが一番である。 通信相手を Windows 2000 以降に限定して構わなければ (そろそろ Windows 98 などは切り捨ててもいいのではないだろうか?)、 全てのネットワークアダプタにおいて、 「NetBIOS over TCP/IP を無効にする」を設定しておくことにより、 137 〜 139番ポートを使った NetBIOS 通信に煩わされることがなくなる。

「NetBIOS over TCP/IP を無効にする」には、 各ネットワークアダプタのプロパティの中の、 「インターネット プロトコル (TCP/IP)」のプロパティにて、 「詳細設定」を選択すると、 WINS タブの中に、 この「NetBIOS over TCP/IP を無効にする」という設定があるので、 このラジオボタンを選択する。
やれやれ、これでアドホックな名前解決とは縁を切れる、と思ったら Windows VISTA では LLMNR (Link-local Multicast Name Resolution) なる 新しいプロトコルが導入されてしまった。 果たして LLMNR を無効にすることは可能なのだろうか? アドホックな名前解決の有用性は認めるにしても、 それを抑制する方法が存在しないのは問題なのではないだろうか。

そして 445番ポートをポートフォワードする。 前述したように、 local マシンの 445番ポートは Windows が使用済なので、 local マシンとは「別のマシン」を用意することが必要になる。 幸い、最近は仮想マシンが普及しつつあるので、 ノートPC などでも local マシンとは別のマシンを同居させることが 現実的になってきた。 もちろん、ポートフォワードのためだけに仮想マシンを走らすのは、 いかにも牛刀なので、 仮想マシンを走らせるニーズがなければあまり好ましい方法ではないだろう。 ポートフォワードだけを行なう仮想ネットワークドライバがあればいいのだが、 そういうものは果たしてあるのだろうか? ご存じの方は教えて頂けると幸いである。

幸い私の場合、以前から Windows 上で coLinux を走らせているので、 この仮想マシン上の Linux を使って 445番ポートをポートフォワードできる。 具体的な方法については長くなってしまうので、
続きは次回に...



hiroaki_sengoku at 08:02|この記事のURLComments(1)TrackBack(1)
このエントリーを含むブックマーク 2006年11月30日

DNS逆引きできないメールサーバからのメールを拒否するサイトが増えはじめ、 その弊害を指摘する声が上がっているようだ。

reject_unknown_clientは迷惑メール対策としておすすめではない」から引用:

迷惑メール対策として reject_unknown_client を紹介しているページは 多数みつかるのだが、 その弊害の大きさにまで言及しているページはほとんどないことがわかる。 これはあまりよくない傾向だ。そこで、ある程度説明をまとめておくことにする。

逆に言うと、弊害をきちんと理解していれば、 DNS逆引きの結果を迷惑メール判定に用いることは受信側の判断だろう。 DNS逆引きができないホストからメールを送らざるを得ないサイトにとっては 釈然としないところもあるとは思うが、 受け取らない権利があるのもインターネットである。
DNS逆引き設定の無いメールサーバからのメールをspam扱い」から引用:

うーん、自宅は固定IPアドレスにしているので、 追加で月額1050円也を払えば逆引きぐらい設定してもらえるのだが、 きっかけがどうにも癪だなぁ。
元はといえばSPAMMERが悪いのだけれど、 拒否するサーバ側もちょっと乱暴な気がする。
...
悪貨が良貨を駆逐してはいかんよなぁ。
SPAM地獄に陥っているmail Server管理者の気持ちはわかるんだけど...。

「癪だなぁ」という気持ちはとてもよく理解できるのであるが、 「悪貨が良貨を駆逐」というのは...? 「悪貨」は SPAMMER を指すとして、 「良貨」は何を指すのだろう? 逆引きできないメールサーバって「良貨」なのだろうか? 他者と通信するホストは、 基本的には DNS 逆引きできたほうがいいと思うのだが...

性善説が通用した牧歌的なインターネットの黎明期ならいざ知らず、 通信相手はまず疑ってかからなければ手痛い目に会うのが (それがいいことかどうかはさておき) 現代のインターネットである。 通信相手の素性は可能な限り収拾しておきたいし、 できれば ssh や SSL認証のような認証を行なって、 通信相手が間違いなく意図した通りの相手であることを確認したい。

しかし残念ながらメール配送は、いまだ認証無しの通信が大多数を占める。 通信は相手あってのものだし、 特にメールは不特定多数からの通信を受付けなくては機能しない。 一方的に、 「認証無しの接続は受付けません」と主張したところで得るものはあまり無い。 だから完全な認証は棚上げせざるを得ないが、 不十分であっても通信相手の素性が分かる方法は総動員したいところだ。 そして、素性を調べる手段として DNS逆引きは、ある程度の合理性を持つ。
逆引き判定」から引用:

spam をたくさん送ってくる中国韓国あたりは そもそも逆引きを設定する習慣があまりないようで、 このチェックによってこれらの国からの spam を多数撃墜できるのは事実である。 しかしそれは逆引きを設定する習慣がない国と spam を送ってくる国がたまたま重なっているだけにすぎない。 spam を送ってくるホストはとうぜんまともな管理はされていないだろうが、 まともに管理されているホストに逆引きがないというのもふつうに存在する (何をもってまともとするかの定義も曖昧だが)。

「まともに管理されているホストに逆引きがないというのもふつうに存在する」 のは事実だと思うが、 「まともに管理している」ということを通信相手に伝える努力は すべきではないだろうか? 通信はお互いの協力があって初めて成り立つものであるから、 spammer と区別してもらうための努力もせずに、 spammer と一緒にするなと叫んでいるだけでは解決にならない。 もちろん、spammer と区別してもらう方法が DNS逆引きの設定だけであると 主張するつもりはない。 送信側と受信側、双方にとって最も合理的な判別法を選択していくべきであろう。

もし、逆引きを設定することは、 送信側にとってコストがかかる (例えば「追加で月額1050円也を払う」) ので採用したくない、 とお考えのかたがいたら、 そもそもなぜ迷惑メールが蔓延しているのか考えてみて頂きたい。

つまり、迷惑メールは際限無く増大しているのに、 なぜダイレクトメール (つまり宣伝目的で送られる郵便物) はそれほど増えないのか。 言うまでもなく郵便物は送信するのにコストがかかるからだ。 送信側と受信側のコスト負担がアンバランスだったことこそが、 迷惑メールがここまで社会問題化した最大の理由である。 送信側に応分の負担を求めること、 すなわち送信側に身の潔白 (つまり、まともに管理しているということ) を証明する コストを支払わせることこそが根本的な解決策となるのだと思う。



hiroaki_sengoku at 07:24|この記事のURLComments(5)TrackBack(0)
このエントリーを含むブックマーク 2006年11月11日

ケータイのキャリアを WILLCOM に変えたついでに、 自宅のバックアップ回線も WILLCOM に変更した (メインの回線は Bフレッツ)。 マルチパック割引につられた (^^;) ため。 ケータイとの抱き合わせ割引でなくても フレッツより安い らしい。 普段はほとんど使わない (死活確認パケットを飛ばすだけ) バックアップ回線なので、 1円でも安いほうが助かる。

そして MNP が巷を賑わす昨今、なぜ MNP 対象外の WILLCOM かというと、 W-ZERO3[es] が 使いたかったから (^.^) であるが、 eメールの送受信と PHSへの通話が全て 定額料金に含まれるというのもうれしい。 おかげで一ヶ月の電話代が半額になった。

ウィルコムADSLサービスは、 アッカ・ネットワークスの ADSL サービスを使っていて、 アッカでは市販モデムを利用できるサービスは行なっていないという。 つまりアッカが(ウィルコムユーザ向けに) レンタルするモデムを使えということ。 それまで使っていたモデム (買い取り) が無駄になってしまうし、 このレンタルモデムはモデムといいつつルータ機能まで含んでいるので、 バックアップ回線として使いにくい機器だと困るなぁと躊躇したのも事実だが、 月額費用が 2000円ほど安くなる (モデムが買い取り可能ならもっと安くなるのに...) という誘惑には勝てず、乗り換えてしまった。

私が何のためにバックアップ回線を契約しているかというと、 メイン回線が落ちたときにも、 外部から自宅のサーバへログイン可能とするためである。 だから IP アドレスが固定割当てでないのであればダイナミックDNS などの 仕掛けを併用して、 外部からアクセスする際の IP アドレスが常に調べられなければならない。 反面、内部から外部へのアクセス (つまりいわゆる一般的なインターネットの利用法) には全くといっていいほど使用しないわけで、 普通の ADSLサービスの利用方法とは大きく異なる。 レンタルルータ込みのサービスだと、 提供側が想定する「標準的な」利用方法を「押し付けられる」リスクがあるわけで、 なるべくなら利用したくない、という思いがあった。 まあ、後述するようにそれは杞憂だったのであるが。

届いたレンタルモデム WARPSTAR Aterm WD735GV をいろいろいじっていると、 PPPoE パケットをスルーして、 ルータ機能を使わないことも可能であることがわかり一安心。 PPPoE の認証のとき必要となるログインID とパスワードも、 レンタルモデムから無事読み出すことができた。 というわけでしばらく (数日ほど) モデムとしてのみ使っていたのであるが、 レンタルモデムにルータ機能がついているのなら それを使いたくなるのが技術者のサガだろう。

ただし、外部からログイン可能、という条件だけは譲れない。 バックアップ回線としての唯一の存在意義だからだ。 それまで使っていたルータは、ダイナミックDNS サービスに対応していたので、 gcd.iobb.net を DNS で引けばルータの WAN 側の IP アドレスを 取得することができた。 今回のレンタルモデムには、少なくともマニュアルには、 WAN 側の IP アドレスを取得する方法は書かれていないし、 「クイック設定Web」インタフェースを見ても WAN 側の IP アドレスを取得できるページは見当たらない (「通信情報ログ」以外は)。

もちろん、このレンタルモデムを経由して外部にアクセスすれば、 WAN 側の IP アドレスが送信元アドレスとなるパケットが飛ぶので、 それをもとにダイナミックDNS に登録してくれるサービスがあれば いいのであるが、 そういったダイナミックDNS サービスを見つけるより、 WAN 側の IP アドレスを取得する方法を見つけるほうが早かった。

さてどうしたものか、と思って ダイナミックDNSサービス iobb.net のプロトコルを調べるためにダンプしておいた Aterm WD735GV との通信内容を眺めていると、 Aterm WD735GV が送信したマルチキャスト パケットを見つけた:

17:13:16.645897 IP (tos 0x0, ttl   4, id 2784, offset 0, flags [DF], length: 298) 192.168.1.251.1900 > 239.255.255.250.1900: [udp sum ok] UDP, length: 270
	0x0000:  4500 012a 0ae0 4000 0411 b845 c0a8 01fb  E..*..@....E....
	0x0010:  efff fffa 076c 076c 0116 484b 4e4f 5449  .....l.l..HKNOTI
	0x0020:  4659 202a 2048 5454 502f 312e 310d 0a48  FY.*.HTTP/1.1..H
	0x0030:  4f53 543a 2032 3339 2e32 3535 2e32 3535  OST:.239.255.255
	0x0040:  2e32 3530 3a31 3930 300d 0a4e 543a 2075  .250:1900..NT:.u
	0x0050:  706e 703a 726f 6f74 6465 7669 6365 0d0a  pnp:rootdevice..
	0x0060:  4e54 533a 2073 7364 703a 616c 6976 650d  NTS:.ssdp:alive.
	0x0070:  0a55 534e 3a20 7575 6964 3aXX XXXX XXXX  .USN:.uuid:XXXXX
	0x0080:  XXXX XXXX XXXX XXXX XXXX XXXX 3a3a 7570  XXXXXXXXXXXX::up
	0x0090:  6e70 3a72 6f6f 7464 6576 6963 650d 0a43  np:rootdevice..C
	0x00a0:  4143 4845 2d43 4f4e 5452 4f4c 3a20 6d61  ACHE-CONTROL:.ma
	0x00b0:  782d 6167 653d 3132 300d 0a4c 6f63 6174  x-age=120..Locat
	0x00c0:  696f 6e3a 2068 7474 703a 2f2f 3139 322e  ion:.http://192.
	0x00d0:  3136 382e 312e 3235 313a 3238 3639 2f75  168.1.251:2869/u
	0x00e0:  706e 702f 726f 6f74 6465 7669 6365 2e78  pnp/rootdevice.x
	0x00f0:  6d6c 0d0a 5345 5256 4552 3a20 4947 442d  ml..SERVER:.IGD-
	0x0100:  4854 5450 2f31 2e31 2055 506e 502f 312e  HTTP/1.1.UPnP/1.
	0x0110:  3020 5550 6e50 2d44 6576 6963 652d 486f  0.UPnP-Device-Ho
	0x0120:  7374 2f31 2e30 0d0a 0d0a                 st/1.0....

こんな、家庭用の安物ルータ (しかもモデムと称している) でさえ UPnP (Universal Plug and Play) をサポートしているような時代になったとは... シリアルケーブルでつないだ端末でルータ設定をしていたころが懐かしい... という感慨はサテオキ、 まずは 読みやすいように整形してみる (Universally Unique Identifier の部分は伏せ字)。

NOTIFY * HTTP/1.1
HOST: 239.255.255.250:1900
NT: upnp:rootdevice
NTS: ssdp:alive
USN: uuid:XXXXXXXXXXXXXXXXX::upnp:rootdevice
CACHE-CONTROL: max-age=120
Location: http://192.168.1.251:2869/upnp/rootdevice.xml
SERVER: IGD-HTTP/1.1 UPnP/1.0 UPnP-Device-Host/1.0

機器の詳細は、http://192.168.1.251:2869/upnp/rootdevice.xml を見よ、 と言っているのでアクセスしてみると、 レスポンス中に次のような記載がある:

<service>
<serviceType>urn:schemas-upnp-org:service:WANPPPConnection:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANPPPConn1</serviceId>
<controlURL>/upnp/control/WANPPPConn1</controlURL>
<eventSubURL>/upnp/event/WANPPPConn1</eventSubURL>
<SCPDURL>/upnp/WANPPPConn1.xml</SCPDURL>
</service>

実は UPnP を使うのはこれが初めてだったりする (^^;) のだが、 WANPPPConn1 という名称からしておそらくこれが「接続先1」を意味するのだろう。 サービスの詳細は、SCPDURL に書いてある URL を見ればよいのだろうと、 http://192.168.1.251:2869/upnp/WANPPPConn1.xml にアクセスしてみると、 GetExternalIPAddress というメソッドがあることが分かる:

 <action>
  <name>GetExternalIPAddress</name>
  <argumentList>
   <argument>
    <name>NewExternalIPAddress</name>
    <direction>out</direction>
    <relatedStateVariable>ExternalIPAddress</relatedStateVariable>
   </argument>
  </argumentList>
 </action>

試しに呼び出してみる:

#!/usr/bin/perl
use SOAP::Lite;
my $soap = SOAP::Lite
    ->ns('urn:schemas-upnp-org:service:WANPPPConnection:1')
    ->proxy('http://192.168.1.251:2869/upnp/control/WANPPPConn1');
my $som = $soap->GetExternalIPAddress();
my $ip = $som->valueof('//GetExternalIPAddressResponse/NewExternalIPAddress');
print "$ip\n";

するとあっさり Aterm WD735GV の WAN 側 IP アドレスを取得できてしまった。

use SOAP::Lite;」の部分を、 「use SOAP::Lite +trace => debug;」に変更すると、 http リクエストとレスポンスの内容を見ることができる。 これを真似して http リクエストを手で打ってみる (やはり、どんなプロトコルでも一度は手で打ってみないと... ^^;) と、こんな感じ:

% telnet 192.168.1.251 2869
Trying 192.168.1.251...
Connected to 192.168.1.251.
Escape character is '^]'.
POST /upnp/control/WANPPPConn1 HTTP/1.1
Host: 192.168.1.251:2869
Accept: text/xml
Accept: multipart/*
Accept: application/soap
Content-Length: 503
Content-Type: text/xml; charset=utf-8
SOAPAction: "urn:schemas-upnp-org:service:WANPPPConnection:1#GetExternalIPAddress"

<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope
 xmlns:namesp1="urn:schemas-upnp-org:service:WANPPPConnection:1"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
 xmlns:xsd="http://www.w3.org/2001/XMLSchema"
 soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
 xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
 <namesp1:GetExternalIPAddress xsi:nil="true" />
</soap:Body>
</soap:Envelope>

HTTP/1.1 200 OK
CONTENT-LENGTH: 423
CONTENT-TYPE: text/xml; charset="utf-8"
SERVER: IGD-HTTP/1.1 UPnP/1.0 UPnP-Device-Host/1.0
EXT:

<?xml version="1.0"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
	<SOAP-ENV:Body>
		<m:GetExternalIPAddressResponse xmlns:m="urn:schemas-upnp-org:service:WANPPPConnection:1">
			<NewExternalIPAddress>222.147.27.89</NewExternalIPAddress>
		</m:GetExternalIPAddressResponse>
	</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

たかがルータの IP アドレスを取得するために、 これだけ沢山のデータをやり取りするのもどうかと思うが...



hiroaki_sengoku at 08:23|この記事のURLComments(2)TrackBack(0)
このエントリーを含むブックマーク 2006年11月04日

ダイナミックDNS というと RFC 2136 で定められている 「Dynamic Updates in the Domain Name System (DNS UPDATE)」がまず思い浮かぶ。 自宅のバックアップ回線 (ADSL) 用として使っている無線LANルータ WN-G54/R2 は、 「アイ・オーが提供する無料ダイナミックDNSサービス iobb.net 対応」と 書いてあるので、 てっきり DNS UPDATE 方式なのだと思っていた。

ADSL 回線を、NTT東日本の フレッツ・ADSL から、 ウィルコムADSLサービス に切り替える (バックアップ回線なのでコスト最優先) にあたって、 以前使っていた ADSL モデムはアッカ回線に適合しないようなので、 素直にウィルコムのレンタルモデムを使うことにした。 ところが、このレンタルモデム「Aterm WD735GV」には、 ルータ機能もついている。

ウィルコム曰く、 「ウィルコムADSLサービスのレンタルモデムは、 あらかじめお客さまのログインIDとパスワード等が設定されています」、 ということだそうで、 ログインIDとパスワードは教えてもらえない。 教えてもらわないことにはレンタルモデム以外のルータを使えないのだが、 サポートに電話して教えてもらうのも面倒だったので、 このレンタルモデムの「クイック設定Web」の「接続先設定」画面にて、 ログインIDとパスワードを変更せずに「設定」ボタンを押してみたら、 パスワードが平文で POST されたので、 リクエストボディを観察するだけでパスワードを知ることができた。

レンタルモデム WD735GV の「PPPoEブリッジ」機能を使えば、 WD735GV を単なる ADSL モデムとして使うこともできるし、 実際まずは ADSL モデムだけ入れ替えて ルータは元の WN-G54/R2 のまま使っているのだが、 ADSL モデムにルータ機能があるのなら それを使いたくなるのが人情というものであろう。 機器の数を減らせばそれだけ故障する確率も減らすことができる。

しかし、PPPoE 接続を WD735GV に行なわせると、 WN-G54/R2 のダイナミックDNS 機能が利用できなくなってしまう。 iobb.net 以外のダイナミックDNSサービスに乗り換えれば済む話なのであるが、 せっかく iobb.net に対応した機器を使っているのに iobb.net を使わないのは モッタイナイと思ってしまったので、 とりあえず iobb.net に対して RFC 2136 で定められている DNS UPDATE パケットを 飛ばしてみた... が何の反応もない。

無闇矢鱈にテストパケットを飛ばしていては怒られそう (^^;) なので、 ADSLモデム (つまり Aterm WD735GV) と無線LANルータ (WN-G54/R2) との間に Linux ブリッジをはさんで iobb.net への通信 (つまり PPPoE セッション) を tcpdump でダンプしてみた。 すると...

# brctl addbr br0
# brctl addif br0 eth0
# brctl addif br0 eth2
# ifconfig br0 up
# ifconfig eth0 up
# ifconfig eth2 up
# tcpdump -i br0 -s 1500 -vvv -X
tcpdump: WARNING: br0: no IPv4 address assigned
tcpdump: listening on br0, link-type EN10MB (Ethernet), capture size 1500 bytes
	...
17:13:20.954864 PPPoE  [ses 0x1e10] IP (tos 0x0, ttl 150, id 2, offset 0, flags [none], length: 228) 60.38.65.147.31602 > XXX.XXX.XXX.XXX.http: . [tcp sum ok] 1:189(188) ack 1 win 5656
	0x0000:  1100 1e10 00e6 0021 4500 00e4 0002 0000  .......!E.......
	0x0010:  9606 b91e 3c26 4193 XXXX XXXX 7b72 0050  ....<&A.XXXX{r.P
	0x0020:  0361 fb73 903f 4510 5010 1618 0ce3 0000  .a.s.?E.P.......
	0x0030:  4745 5420 2f63 6769 2d62 696e 2fXX XXXX  GET./cgi-bin/XXX
	0x0040:  XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX  XXXXXXXXXXXXXXXX
	0x0050:  XXXX XXXX 2e63 6769 3f68 6f73 743d 6763  XXXX.cgi?host=gc
	0x0060:  642e 696f 6262 2e6e 6574 266d 7969 703d  d.iobb.net&myip=
	0x0070:  3630 2e33 382e 3635 2e31 3437 2048 5454  60.38.65.147.HTT
	0x0080:  502f 312e 300d 0a48 6f73 743a 20XX XXXX  P/1.0..Host:.XXX
	0x0090:  XXXX XX2e 696f 6262 2e6e 6574 0d0a 5573  XXX.iobb.net..Us
	0x00a0:  6572 2d41 6765 6e74 3a20 574e 4735 3452  er-Agent:.WNG54R
	0x00b0:  322f 312e 320d 0a41 7574 686f 7269 7a61  2/1.2..Authoriza
	0x00c0:  7469 6f6e 3a20 4261 7369 6320 XXXX XXXX  tion:.Basic.XXXX
	0x00d0:  XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX  XXXXXXXXXXXXXXXX
	0x00e0:  XXXX XXXX XXXX XXXX 0d0a 0d0a            XXXXXXXX....

何のことはない、IP アドレスを登録するための Web ページを アクセスしているだけだった。 登録するホスト名と IP アドレスを、 それぞれ「host=」「myip=」パラメータで CGI へ渡し、 認証は Basic 認証を使っている (上記ダンプ中、URL の一部および認証文字列は伏せ字にした)。

したがって curl コマンドなどを使って、 ダイナミックDNSサービスに ホスト名と IP アドレスを登録することができる。

curl --interface eth0:1 --user-agent 'WNG54R2/1.2' --user 'XXXXXXXXXXXX:XXXXXXXX' 'http://XXXXXX.iobb.net/cgi-bin/XXXXXXXXXXXXXXXXXXXXXXX.cgi?host=gcd.iobb.net&myip=60.38.65.147'


hiroaki_sengoku at 07:25|この記事のURLComments(0)TrackBack(0)
このエントリーを含むブックマーク 2006年10月16日

迷惑メール (UCE, UBE, スパム メールなどとも呼ばれる) が沢山届いて困る、 というのはメールアドレスを公開している人にとって共通の悩みのタネであるはず。 SpamAssasin などの メールフィルタを使っている、というかたも多いだろう。

しかしながら、メールの内容を見て迷惑メールか否かを判断するフィルタでは、 迷惑メール送信者とのイタチごっこは避けられない。 送信者は迷惑メールと判断されないよう、 どんどん内容を巧妙に変化させていくからだ。 例えばフィルタリングアルゴリズムとしてよく使われる ベイジアン フィルタ(Bayesian Filter) は、 ワード サラダ (Word Salad) を食わされることによって精度が落ちてしまう。

したがって 迷惑メール送信者とのイタチごっこに終止符を打つには、 迷惑メール送信者が変更できない情報にのみ依存した 迷惑メール判定方法を用いるしかない。

迷惑メール送信者が変更できない情報とは、つまり 送信元 IP アドレスである。 そんなものはプロバイダ (ISP, Internet Service Provider) を 変更すれば変えられる、 と言われればその通りなのであるが、 単に別のアドレスに変えられるというだけであって、 望み通りのアドレスに変更できるわけではない。

例えば私が使っているメールサーバの IP アドレスは 60.32.85.220 であるが、 送信元 IP アドレスとしてこのアドレスを使える人は特定少数の人のみであり、 この IP アドレスから迷惑メールが送信されるような事態は、 まず起きないと言えるし、 万一「特定少数」のうちの誰かが迷惑メールを送るようなことがあれば、 直ちに止めさせることができる。

この IP アドレスからは迷惑メールを送信させない、というポリシーでもって 運用されている IP アドレスは数多い。 きちんと運用されているサイトの多くが同様のポリシーで運用されていることだろう。 だからそのような、「きちんと管理された」IP アドレスから送信された メールのみ受付け、 迷惑メールの送信元となる可能性がある IP アドレスからのメールを拒否すればよい。

つまり IP25B (Inbound Port 25 Blocking) である。 既に IP25B を実施しているプロバイダ もあるようであるが、 インターネットにおける通信は end-to-end を基本にすべきであって、 通信インフラを提供する側であるプロバイダが、 他のプロバイダと連係して IP25B を実施することについては違和感を覚えなくもない。

もちろん沢山ある IP アドレスの全てについて、 きちんと管理されているか否かを調べ上げることは困難であるが、 迷惑メールを送ってくる IP アドレスというのは実はそんなに多くはない。 私のサイト宛に迷惑メールを送ってきた IP アドレスを、 私は数年間にわたって記録しているが、 大多数は、ダイアルアップと思しき IP アドレスであり、 しかも、いくつかの国に集中している。

ここでいう「ダイアルアップ IP アドレス」とは、 個人ユーザがプロバイダと契約してインターネットに接続するときに、 プロバイダから割当てられる IP アドレスを指す。 「ダイアル」して接続する場合だけでなく、ADSL 等による接続も含む。 多くの場合、接続が切れるたびに異なる IP アドレスが割当てられるが、 固定的な IP アドレスが割当てられている場合もある。

このような IP アドレスは、 逆引きしても、「P061198164231.ppp.prin.ne.jp」といったような プロバイダ名しか分からないような「匿名」ホスト名しか得られないか、 あるいはそもそも逆引きが設定されていないケースが大半である。 ちなみに「P061198164231.ppp.prin.ne.jp」というホスト名は、 数字の部分を三桁づつ区切ると「061」「198」「164」「231」になり、 このホスト名に対応する IP アドレス「61.198.164.231」から 規則的に名付けられたホスト名であることが分かる。

多くのプロバイダにおいて、 「ダイアルアップ IP アドレス」には、 このような規則的に命名されたホスト名がつけられているようだ。 逆に言うと、 逆引きして規則的なホスト名が得られたときに、 その IP アドレスを「ダイアルアップ IP アドレス」と見なすことは、 ある程度の合理性を持つ。

「規則的なホスト名」=「ダイアルアップ」という認識が広まるにつれ、 ダイアルアップ以外の IP アドレスには、 規則的でないホスト名をつける風潮が強まるだろう。 実際、 私が知っているあるデータセンタは、顧客が使用する IP アドレスに、 規則的ではないホスト名をつけるよう推奨している。

したがって、メールの送信元 IP アドレスを逆引きしたときに、 規則的なホスト名が得られたら、 そのメールは高い確率で迷惑メールと予測できる。 他の迷惑メール判別方法と組合わせ、 送信元が規則的なホスト名であることを必須条件とすれば、 排除するメールを迷惑メールのみに限定する (つまり false positive の可能性をほぼゼロにする) ことが可能となる。

一方、逆引きしたとき規則的でないホスト名が得られたら、 そのメールは十中八九、マトモなメールである。 仮に迷惑メールだったのなら、 ホスト名を元にその管理主体がどんな組織であるかある程度想像つくだろうから、 迷惑メール対策が期待できそうなら連絡すればよいし、 その組織からのメールを受け取る必要がなければ、 以後受け取りを拒否すればよい。

したがって問題となるのは逆引きができない場合である。 逆引きを設定していないような管理不十分なサイトからのメールは受け取らない、 と言ってしまえるのなら楽なのであるが、 残念ながら逆引きを設定していないサイトは数多い。 その全てからのメールを拒否してしまうと、 必要なメールまで失いかねない。

そこで役に立つのが自前のブラックリスト/ホワイトリストである。 今まで受け取ったメールを集計して、 迷惑メールしか送ってこない送信元 IP アドレスはブラックリストに載せ、 必要なメールを送ってくる送信元 IP アドレスはホワイトリストに載せる。 そして、ブラックリストに載っている IP アドレスから送られてきたメールは 「ダイアルアップ IP アドレス」に準じる扱いにし、 ホワイトリストに載っている IP アドレスから送られてきたメールは、 迷惑メール判定をスキップして無条件に受け取るようにする。

今まで受け取ったメールを全て保存していれば、 ブラックリスト/ホワイトリストを作るのは容易だろう。 が、マトモなメールならいざ知らず、 迷惑メールまでも全て保存している人は稀かも知れない。 迷惑メールを受け取ったら、 内容を保存するかどうかはともかく、 送信元 IP アドレスだけは記録するようにしておきたい。

とりあえずお手軽にブラックリストを使って、 逆引きできない送信元 IP アドレスを分別してみたいというかたのために、 私が個人的に使用しているブラックリストを公開する。 送信元 IP アドレスが A.B.C.D であるとき、 D.C.B.A.rbl.gcd.org の TXT レコードが引けたなら、 その IP アドレスはブラックリストに載っている。 例えば IP アドレス 127.0.0.2 がブラックリストに載っているか調べるには、

% host -t txt 2.0.0.127.rbl.gcd.org
2.0.0.127.rbl.gcd.org text "Listed 127.0.0.2"

などと DNS 問合わせを行なえばよい。 "Listed 127.0.0.2" などの結果が返ってくれば ブラックリストに載っていることを意味し、 NXDOMAIN つまりレコードが存在しないという結果が返ってくれば ブラックリストに載っていないことを意味する。 私はこのブラックリストを利用した迷惑メール判別フィルタを用いることによって、 私のメールボックスに届く迷惑メールのほとんど全てを排除できている。

前述したように、 このブラックリストはあくまで送信元 IP アドレスが逆引きできないときに限り 利用することを想定している。 逆引き可能な IP アドレスに対してこのブラックリストを利用した場合に 得られる結果は無意味であるので注意して欲しい。 また、もちろん利用は at your own risk でお願いしたい。 このブラックリストに対するご意見は歓迎するが、 このブラックリストを利用したことによって、 あるいは利用できないことによって発生するいかなる損害に対しても 私は責任を負わない。 この条件に同意して頂けるかたにのみ、 このブラックリストの利用を許諾する。



hiroaki_sengoku at 06:48|この記事のURLComments(2)TrackBack(1)
このエントリーを含むブックマーク 2006年07月10日

前回は、tinydns を IP アドレス指定せずに起動したいケースについて述べた。 前々回で述べたように、 djbdns はネームサーバ tinydns と、 キャッシュサーバ dnscache が完全に分離した独立のプログラムになっていることが 大きな特徴であるが、 tinydns を IP アドレス指定せずに起動しようとすると、 同じポート番号を使う dnscache を起動することができなくなってしまう。

そもそも論で言えば、 ネームサーバとキャッシュサーバは全く異なるサービスを行なうサーバなのだから、 同じポート番号を使うこと自体が間違っているのである。 もしキャッシュサーバが 53番以外のポートを使うのであれば、 tinydns も dnscache も IP アドレスを指定せずに同じマシンで起動できるし、 これ以上シンプルな解決策はないであろう。

ならば dnscache を 53番とは異なるポートで起動しよう。 ただし、dnscache は 53番ポートを bind(2) するように ハードコーディングされているので、 若干パッチをあてる必要がある。

--- dnscache.c.org	Mon Feb 12 06:11:45 2001
+++ dnscache.c	Sun Jul  9 06:43:23 2006
@@ -390,6 +390,7 @@
 {
   char *x;
   unsigned long cachesize;
+  unsigned long port;
 
   x = env_get("IP");
   if (!x)
@@ -397,16 +398,19 @@
   if (!ip4_scan(x,myipincoming))
     strerr_die3x(111,FATAL,"unable to parse IP address ",x);
 
+  x = env_get("PORT");
+  if (x) scan_ulong(x,&port); else port = 53;
+
   udp53 = socket_udp();
   if (udp53 == -1)
     strerr_die2sys(111,FATAL,"unable to create UDP socket: ");
-  if (socket_bind4_reuse(udp53,myipincoming,53) == -1)
+  if (socket_bind4_reuse(udp53,myipincoming,port) == -1)
     strerr_die2sys(111,FATAL,"unable to bind UDP socket: ");
 
   tcp53 = socket_tcp();
   if (tcp53 == -1)
     strerr_die2sys(111,FATAL,"unable to create TCP socket: ");
-  if (socket_bind4_reuse(tcp53,myipincoming,53) == -1)
+  if (socket_bind4_reuse(tcp53,myipincoming,port) == -1)
     strerr_die2sys(111,FATAL,"unable to bind TCP socket: ");
 
   droproot(FATAL);

すなわち、環境変数「PORT」にポート番号を設定した場合は、 53番ポート番号の代わりに設定されたポート番号を使えるようにする。 例えば 2053番ポートで dnscache を起動する。 もちろんリゾルバは、こんな事情はお構い無しに 53番ポートへ問合わせるので、 それを 2053番ポートへ振り向ける (リダイレクトさせる) 必要がある。

Linux には、 パケット フィルタリング フレームワーク があって、 ネットワーク アドレス変換を自在に行なうことができる。 だから、53番ポートへのアクセスを 2053番ポートへリダイレクトする、 などということは iptables コマンドを実行するだけで手軽に実現できる。

例えば、dnscache と同じマシン上のリゾルバから 53番ポートへの接続を、 2053番ポートへリダイレクトするなら、

iptables -t nat -A OUTPUT -j REDIRECT -p udp --dport 53 --to-port 2053 \
	-s 127.0.0.1 -d 127.0.0.1

などと iptables を実行するだけである。 「-p udp --dport 53」の部分で、 UDP 53番ポートへのアクセスが対象であることを指定し、 「--to-port 2053」の部分で、 リダイレクト先が 2053番ポートであることを指定している。

また、「-s 127.0.0.1 -d 127.0.0.1」の部分が、 接続元 (つまりリゾルバ) と接続先 (つまり dnscache) の IP アドレスの指定である。 接続元が LAN 内の他のマシンの場合も許可するなら、 この部分を例えば「-s 192.168.1.0/24 -d 192.168.1.1」に変更し、 「OUTPUT」の代わりに「PREROUTING」チェインを指定して iptables を実行すればよい (後述)。

さて、これだけだと実は問題がある。 dnscache は当然のことながら自ドメインのホスト名を解決するには 同じマシン上の tinydns へアクセスする必要があるのだが、 上記のように iptables を実行すると、 この dnscache から tinydns へのアクセスまでリダイレクトしてしまう。 すなわち dnscache が localhost の 53番ポートへ、 自ドメインに関する問合わせを行なうと、 それが 2053番ポートへリダイレクトされ、 結果 dnscache (つまり自分自身) に届いてしまう。 これでは自ドメインのホスト名の解決ができない。

したがって、dnscache からのアクセスの場合だけは、 リダイレクトが行なわれないようにしなければならない。 これを実現するには、 iptables のパラメータに「-m owner ! --uid-owner Gdnscache」 を追加すればよい。 これは、アクセス元のユーザが「Gdnscache」ならばリダイレクトを行なわない、 という指定であり、 Gdnscache は dnscache を実行しているユーザID である。

以上をまとめると、 次のような sh スクリプトになる。 これをマシンのブート時に実行すればよい。

#!/bin/sh
PATH=/sbin:/usr/sbin:/bin:/usr/bin
nsredirect="-t nat -j REDIRECT --dport 53 --to-port 2053"
iptables -p udp $nsredirect -A OUTPUT -s 127.0.0.1 -d 127.0.0.1 \
	-m owner ! --uid-owner Gdnscache
iptables -p tcp $nsredirect -A OUTPUT -s 127.0.0.1 -d 127.0.0.1
if [ -n "$INNER_NETWORK" -a -n "$INNER_DNSCACHE" ]; then
	nsredirect="$nsredirect -A PREROUTING \
		-s $INNER_NETWORK/$INNER_NETMASK -d $INNER_DNSCACHE"
	iptables -p udp $nsredirect
	iptables -p tcp $nsredirect
fi

$INNER_NETWORK」と「$INNER_NETMASK」に、 LAN のネットワークアドレス (例えば 192.168.1.0) とサブネットマスクを それぞれ指定する。 また、「$INNER_DNSCACHE」は キャッシュサーバ (つまりこのマシン) の IPアドレスである。

なお、Linux のパケット フィルタリングでは、
接続元が同一ホストの場合は OUTPUT チェインが使われるので、
-s 127.0.0.1」を指定するときは「-A OUTPUT」を指定して OUTPUT チェインへ追加し、
接続元が LAN 内の他のマシンの場合は PREROUTING チェインが使われるので、
-s $INNER_NETWORK/$INNER_NETMASK」を指定するときは 「-A PREROUTING」を指定して PREROUTING チェインへ追加している。



hiroaki_sengoku at 06:43|この記事のURLComments(0)TrackBack(1)
このエントリーを含むブックマーク 2006年07月09日

前回は djbdns の特徴について述べた。 djbdns の大きな特徴は、 キャッシュサーバ (dnscache) とネームサーバ (tinydns) が分離している、 という点であり、 ネームサーバだけを不特定多数に対して公開することにより、 BIND など他のネームサーバがかかえている潜在的リスクを完全に回避している。

dnscache と tinydns を走らせるための、 固定的な IP が確保できるのであれば話は簡単なのであるが、 複数の (固定的な) IP アドレスを使いにくいマシン環境もある。

例えばノートPC は、移動中など、スタンドアロンで動かすこともあり、 この場合、IP アドレスは 127.0.0.1 の一つだけである。 LAN に接続したり、あるいはダイアルアップ接続すると、 もう一つ IP アドレスを持つことになるが、 この IP アドレスは固定ではなく、 接続のたびに動的に割当てられることが多いだろう。 仮に毎回同じ IP アドレスが割当てられるのだとしても、 スタンドアロン状態の時は、 その IP アドレスが利用できないという点で、 固定的な IP アドレスが利用できるとは言いがたい。

Linux 等では alias IP アドレスを設定することによって 固定的に IP を割当てておくこともできる。 例えば loopback インタフェース lo に対して

onohara:/root # ifconfig lo:0 127.0.0.2

などと別の IP アドレスを追加することができる。 マシン内部で利用するサービスならこれでもいいが、 外部に対して公開するサービスではこの方法は使えない (ノートPC で外部にサービスしてどうするん?という突っ込みは言わないお約束 ;)。

ノートPC 以外でも、 例えば小規模サイト (家庭LAN など) における ゲートウェイマシンなどにおいても、 昨今だとインターネットへの接続は PPPoE を使うことが多いだろう。 グローバル IP アドレスは接続時のみ割当てられる。 サイト外に公開するネームサーバは、 このグローバル IP アドレスで動かすのが自然であるが、 PPPoE 接続が切れている場合にどうするか考えておく必要がある。

ノートPC にしろゲートウェイマシンにしろ、 外部に対して接続しているときのみ割当てられる IP アドレスを利用しようとすると、 接続時のみ tinydns を起動する、 という方法を採らざるを得ない。 たとえば接続が成功したときに実行されるスクリプト (/etc/ppp/ip-up など) で tinydns を起動し、 接続が切れたときに実行されるスクリプト (/etc/ppp/ip-down など) で tinydns を終了させる、 という方法が一般的だろう。

しかしながらこの方法だと、 接続が切れているときは tinydns も動いていないわけで、 サイト内に対して別途 tinydns を立ち上げる必要がある。 また、 接続のたびにサーバプログラムを起動するということは、 それだけトラブルの可能性が高まるということであり、 できればもっとシンプルにしたい。 対外サービスを行なうサーバプログラムには、 常に安定して動いていてほしいわけで、 起動・停止を繰り返す運用よりは、 ずーっと立ち上げっぱなしの方が好ましいだろう。

UNIX の場合、 動的に IP アドレスを割当てられたときに、 サーバプログラムで IP アドレスを bind(2) しなおさないで済む方法というと 一つしかない。 つまり IP アドレスを指定せず (INADDR_ANY) に bind(2) する方法である。

senri:/root # netstat -nap
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address      Foreign Address    State      PID/Program name
	...
udp        0      0 :::53              :::*                          1240/tinydns
tcp        0      0 0.0.0.0:53         0.0.0.0:*          LISTEN     1242/tcpserver
	...

「Local Address」に「:::53」とか「0.0.0.0:53」と出ているのが、 IP アドレスを指定せずに走らせているサーバプログラムである。 このようなサーバプログラムは、 マシンに割当てられているどの IPアドレスに対する接続も受け付けることができる。 つまり新たに動的に IPアドレスが追加されても、 サーバプログラムを立ち上げ直す必要がない。

だから tinydns は IPアドレスを指定せずに、 つまり環境変数 IP に 0.0.0.0 (INADDR_ANY) を設定して立ち上げることが望ましい。

ところが、このようにしてしまうと、当然のことながら 同じ 53番ポートを使う dnscache を同じマシンで立ち上げることが できなくなってしまう。

そもそも、キャッシュサーバ (dnscache) とネームサーバ (tinydns) では 目的が違うのにもかかわらず、 同じポート番号 53番を使うのが問題なのである。 最初から別のポート番号を割当てておけば、 このような問題で悩まなかったものを... RFC 883 が悪い〜
と言っても始まらない (^^;) ので、 なんとか tinydns と dnscache を IP アドレスを指定せずに立ち上げる方法を 考えてみたい。

続きは次回に...



hiroaki_sengoku at 07:10|この記事のURLComments(0)TrackBack(0)
このエントリーを含むブックマーク 2006年07月08日

djbdns は、 とてもシンプルなネームサーバなので、 動作が予測しやすいという特徴を持っている。 複雑なプログラムにありがちな、 謎な現象とも無縁。 ネームサーバというと BINDが有名だが、 BIND はとても巨大なプログラムで、 特に高負荷時に理解しがたい挙動をする。 私は 3年半ほど前に BIND の異常動作 (高負荷環境下だとサービス起動に異常に時間がかかる) で徹夜する羽目に陥って以来、 BIND を捨てて djbdns を使うようになった。

djbdns の大きな特徴の一つに、 キャッシュサーバ (dnscache) とネームサーバ (tinydns) が分離している、 という点がある。 キャッシュサーバ、すなわちリゾルバからの問合わせを受けて、 他のネームサーバに再帰的に問合わせを行なうサーバは、 不特定多数からの問合わせを受付けるべきではない。 なぜなら、キャッシュサーバはその仕組み上、 攻撃に対して脆弱であり、 不特定多数に対してオープンにするのであれば、 嘘のレコードを覚え込まされてしまう 恐怖に怯え続けなければならない。

一方、ネームサーバは本来的に不特定多数に対してサービスすべきものである。 他ドメインからの問合わせ (主に他ドメインのキャッシュサーバからの問合わせ) に対して答えなければ役目を果たせない。 つまり、

  • キャッシュサーバは自ドメインのユーザからの問合わせに答えるサーバ
  • ネームサーバは他ドメインからの問合わせに答えるサーバ

であって、全く異なる役目のサーバである。 両機能を (BIND のように) 一つのプログラムで実現することは 大変なリスクをともなう。

私も djbdns に乗り換える前は BIND を使っていたのだが、 キャッシュサーバとネームサーバを分離し、 セキュリティを向上させるために様々な工夫をした。 この辺りの顛末は、 日経Linux に書いた連載の中でも紹介している:

2000年 5月号 第2回 「ネーム・サーバ (前編)」 pp143-148 (HTML 版)
2000年 6月号 第3回 「ネーム・サーバ (後編)」 pp133-140 (HTML 版)

djbdns だと、この連載で書いたような工夫をしなくても、 同等以上のセキュリティをデフォルトで実現できるので、 とても楽である。

キャッシュサーバとネームサーバそれぞれを別のマシンで動かすのであれば、 素直に djbdns を動かすだけで完璧なのであるが、 一台のマシンで両者のサーバを動かそうとすると、 少々工夫が必要である。 もし、そのマシンが複数の NIC (ネットワーク インタフェース カード) を 持っていて、 各 NIC に固定的に IP アドレスを割当てているなら、 話は簡単である。 異なる NIC それぞれに、 キャッシュサーバないしネームサーバをそれぞれ立ち上げればよい。 ところが IP アドレスが固定でない場合は少しやっかいである。

続きは次回に...



hiroaki_sengoku at 08:34|この記事のURLComments(0)TrackBack(0)