仙石浩明の日記

システム構築・運用

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

(続きを読む...)
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
2007年7月4日

ssh-agent & ForwardAgent を、より安全にしてみる

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 連鎖していても、残念ながら一番手前のサーバしか示せないが)。

Filed under: システム構築・運用 — hiroaki_sengoku @ 07:45
2007年4月16日

Windows「ファイルとフォルダの共有」をリモートな Windows VISTA マシンからアクセスする方法 (1)

よく知られているように、 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番ポートをポートフォワードできる。 具体的な方法については長くなってしまうので、
続きは次回に...

Filed under: システム構築・運用 — hiroaki_sengoku @ 08:02
2007年3月19日

オープンソース版 VPN-Warp リレー サーバ (Perl POE を使って実装)

Perl の非同期I/Oモジュール POE を使って VPN-Warp relayagent を書いてみました」に 続いて、 同じく POE を使って VPN-Warp リレー サーバも書いてみました。 これで、オープンソースだけを使って VPN-Warp を実現することができます。

今までも、 BIGLOBE の VPN ワープのページから証明書を取得すれば、 月額 525円で VPN-Warp を試してみることはできたわけですが、 ちょっと試してみたい場合など、 有料であることがネックである感は否めませんでした。 特に、 常日頃からオープンソースを使いこなしている方々だと、 ちょっと使ってみたいだけなのにお金を払うのはねぇ、 と思ってしまうのではないでしょうか。 かくいう私も、 無料「お試し版」のサービスやソフトウェアに慣れきってしまっているので、 試しに使ってみようとする場合に、 それが有料だったりすると、 いきなり億劫になってしまう今日このごろだったりします (^^;)。

というわけで、 オープンソース版 VPN-Warp です。 使い方はあまりフレンドリーではありませんが、 なんたって全て公開してしまっているので、 興味あるかたは、 とことんいじってみてはいかがでしょうか。

relayagent.pl と同様、 今回公開する relayserver.pl も SSL 暗号化/復号の機能を含んでいません。 したがってリレー サーバへの https アクセスを stone などを 通して SSL 復号する必要があります。 例えば stone を

stone -z cert=cert.pem -z key=priv.pem \
      localhost:12345 443/ssl &

などと実行しておき、 relayserver.pl を

relayserver.pl 12345

と実行します。これだけで 443番ポートはリレー サーバとして利用できます。 つまり、relayagent とブラウザからの https 接続を受付けると、 リレー サーバが両セッションを中継し 「ブラウザ → リレー サーバ → relayagent → Webサーバ」 という経路で通信できます。

                      リレー            イントラ         イントラ
ブラウザ ─────→ サーバ ←──── relayagent──→ Webサーバ
            https     443番ポート                        80番ポート

見かけは極めて tiny ですが、 通信プロトコルは本物(?)の VPN-Warp と互換性があるので、 「VPN-Warp relayagent フリー ダウンロード」から ダウンロードできる VPN-Warp relayagent を使うこともできます。

そもそも論で言えば、 リレー サーバの役目は単にデータを右から左へ渡すだけなので、 以下に示すようにその中核の部分は極めてシンプルです。 しかしながら、もちろんこれは KLab(株) で運用しているリレー サーバが単純であることを意味しません。 機能がシンプルでも、大量の同時接続 & 大量データを受付ける耐高負荷性能や、 機器の一部に故障が起きてもサービスが影響を受けない高可用性を実現するために、 様々な工夫を盛り込んでいます。

では、relayserver.pl の中身を順に見ていきましょう。

#!/usr/bin/perl
use POE qw(Component::Server::TCP Filter::Stream);
my $Port = shift;
my $PollID;
my $PollHeap;
my $PollBuf;
my $PollHeader;
my %SID;
my %Heap;
my %Buf;
my $NextSID = 0;

POE::Component::Server::TCP->new
    (
     Port => $Port,
     ClientInput => sub {
	 my ($heap, $input, $id) = @_[HEAP, ARG0, ARG1];
	 if (defined $PollID && $id == $PollID) {
	     $PollHeap = $heap;
	     $PollBuf .= $input;
	     &doPoll;
	 } elsif (defined $SID{$id}) {
	     my $sid = $SID{$id};
	     $Heap{$sid} = $heap;
	     $Buf{$sid} .= $input;
	     &doSession($sid);
	 } elsif ($input =~ m@^GET /KLAB/poll @) {
	     if (defined $PollID) {
		 $heap->{client}->
		     put("HTTP/1.1 503 Service Unavailable\r\n\r\n");
		 $heap->{client}->shutdown_output;
		 return;
	     }
	     $PollID = $id;
	     $PollHeap = $heap;
	     $PollBuf = $input;
	     &doPoll;
	 } else {
	     $SID{$id} = $NextSID;
	     $NextSID = ($NextSID + 1) & 0xFFFF;
	     my $sid = $SID{$id};
	     $Heap{$sid} = $heap;
	     $Buf{$sid} = $input;
	     &doSession($sid);
	 }
     },
     ClientDisconnected => sub {
	 my $heap = $_[HEAP];
	 my $id = $heap->{client}->ID;
	 if (defined $PollID && $id == $PollID) {
	     undef $PollHeap;
	     undef $PollBuf;
	     undef $PollHeader;
	     undef $PollID;
	 } elsif (defined $SID{$id}) {
	     my $sid = $SID{$id};
	     undef $SID{$id};
	     undef $Heap{$sid};
	     undef $Buf{$sid};
	 }
     },
     ClientFilter => POE::Filter::Stream->new(),
    );
POE::Kernel->run;

わずか 70行にも満たないコードですが、 リレー サーバの中核の部分は、ほとんどこれで全てです。 いかに POE (Perl Object Environment) の 記述性が高いか分かりますね。

私は常日頃から プログラマの生産性は、ピンとキリでは 3桁の違いがある と主張しています。 この主張をもう少し詳しく言うと、 その 3桁のうち、プログラマの腕に純粋に依存する部分は 2桁ほどの違いで、 残り 1桁ぶんは解決すべき問題に応じていかに最適な道具を使うかの違い、 ということになります。 最適な道具を使いこなせるもの腕のうち、 ということもできますね。

上記 70行にも満たないコードですが、 実は命令文としてみると、 わずかに 2 つの命令文であることが分かります。 すなわち、

POE::Component::Server::TCP->new(...中略...);
POE::Kernel->run;

ですね。実質「POE::Component::Server::TCP->new(...中略...);」 だけと言ってもいいでしょう。 この命令文は、

POE::Component::Server::TCP->new
    (
     Port => $Port,
     ClientInput => sub {
	 ... クライアントから受信したデータの処理 ...
     },
     ClientDisconnected => sub {
         ... クライアントとの接続が切れたときの処理 ...
     },
     ClientFilter => POE::Filter::Stream->new(),
    );

という構造になっています。 つまり、クライアントからデータが送られて来たときに呼び出されるルーチンと、 クライアントとの接続が切れたときに呼び出されるルーチンを指定しておけば、 あとは POE がうまくやってくれる、というわけです。簡単でしょう?

リレー サーバにとって「クライアント」というと、 ブラウザか relayagent になります。 クライアントからの TCP/IPセッション一本一本に対して POE が ID を割り振っていて、 この ID を見ればどの TCP/IPセッションで送られて来たデータか分かります。

Perl の非同期I/Oモジュール POE を使って VPN-Warp relayagent を書いてみました」で 解説したように、 クライアントから送られて来た最初のデータが 「GET /KLAB/poll 」で始まっていれば、 そのクライアントは relayagent ですから、 以下のようにその ID ($id) を $PollID に代入しておきます。

	 elsif ($input =~ m@^GET /KLAB/poll @) {
	     if (defined $PollID) {
		 $heap->{client}->
		     put("HTTP/1.1 503 Service Unavailable\r\n\r\n");
		 $heap->{client}->shutdown_output;
		 return;
	     }
	     $PollID = $id;
	     $PollHeap = $heap;
	     $PollBuf = $input;
	     &doPoll;
	 }

同じ TCP/IPセッション (つまり $id == $PollID) で 続いて送られてきたデータは、 以下の部分で処理されます。

	 if (defined $PollID && $id == $PollID) {
	     $PollHeap = $heap;
	     $PollBuf .= $input;
	     &doPoll;
	 }

いずれの場合も、受信したデータはいったん $PollBuf に蓄えた上で、 「doPoll」ルーチンを呼び出します。

一方、ブラウザから送られてきたデータの場合は、 以下のようにセッションID ($SID{$id}) を順に割当てていきます。 「セッション」という単語が何度も出てきてややこしいのですが、 $id が POE が各 TCP/IPセッションに割当てた ID で、 各 TCP/IPセッションそれぞれに、 リレー サーバが 16bit の番号を割当てたのが VPN-Warp で言うところのセッションID ($sid = $SID{$id}) です。

	 else {
	     $SID{$id} = $NextSID;
	     $NextSID = ($NextSID + 1) & 0xFFFF;
	     my $sid = $SID{$id};
	     $Heap{$sid} = $heap;
	     $Buf{$sid} = $input;
	     &doSession($sid);
	 }

同じ TCP/IPセッション (つまりセッションID $sid が $SID{$id}) を通して 続いて送られてきたデータは、 以下の部分で処理されます。

	 elsif (defined $SID{$id}) {
	     my $sid = $SID{$id};
	     $Heap{$sid} = $heap;
	     $Buf{$sid} .= $input;
	     &doSession($sid);
	 }

いずれの場合も、受信したデータはいったん $Buf{$sid} に蓄えた上で、 「doSession」ルーチンを呼び出します。

つまり、relayagent から受信したデータは doPoll ルーチンで、 ブラウザから受信したデータは doSession ルーチンで、 それぞれ処理する、というわけです。 以下の図に示すように、 リレー サーバの役割は、 relayagent から受信した (ブロック化された) データを、 (ブロックを開梱しつつ) ブラウザへ送信し、 またブラウザから受信したデータを、 ブロック化して relayagent へ送ることですから、 doPoll および doSession が何をするためのルーチンか予想できますよね?

VPN-Warp セッション

まず doPoll を見ていきましょう。

sub doPoll {
    do {
	if (! defined $PollHeader) {
	    if ($PollBuf =~ /\r\n\r\n/) {
		$PollHeader = `;
		$PollBuf = ';
		$PollHeap->{client}->put("HTTP/1.1 200 OK\r\n\r\n");
	    }
	}
	return unless defined $PollHeader;

リクエストヘッダを全て読み込んでいない場合 (つまり $PollBuff に空行 \r\n\r\n が含まれていない場合) は、ここで終わりです。
$PollBuf に受信データが追加されて、ふたたび doPoll が呼ばれるまで待ちます。

リクエストヘッダを全て読み込んだ場合は、 $PollBuf からリクエストヘッダ部分を削除した上で、 次に進みます。

	my ($sid, $len, $data) = unpack("nna*", $PollBuf);
	return unless defined $sid && defined $len && $len ne "";

ブロック全体を読み込めていない場合は、ここで終わりです。 $PollBuf に受信データが追加されて、ふたたび doPoll が呼ばれるまで待ちます。 「ブロック」というのは VPN-Warp 用語で、 relayagent とリレーサーバとの通信は、 基本的にこの「ブロック」を単位にして行ないます。 ブロックは次のような可変長のデータです。

    ┌───┬───┬───┬───┬───┬─≪─┬───┐
    │セッションID│ データ長  │  可変長データ   │
    └───┴───┴───┴───┴───┴─≫─┴───┘
          2バイト         2バイト      「データ長」バイト

「セッションID」および「データ長」は、ビッグエンディアンです。 つまり上位バイトが先に来ます。 したがって、上記コードによって $sid, $len, $data にそれぞれ 「セッションID」「データ長」「可変長データ」が代入されます。

なお、データ長が 0 ないし負数の場合は、 「可変長データ」の部分は 0 バイトになります。 このような「可変長データ」がないブロックは、 コントロール用のブロックで、 EOF や Error などのイベントを伝えます。

	if ($len > 32767) {
	    $len -= 65536;
	    $PollBuf = $data;
	    if ($len == -1) {
		&doShutdown($sid);
	    }
	}

$len == -1 のときは、Error を伝えるコントロール ブロックなので、 「doShutdown」ルーチンを呼び出しています。

	elsif ($len > 0) {
	    return unless defined $data && length($data) >= $len;
	    ($data, $PollBuf) = unpack "a${len}a*", $data;
	    if (defined $Heap{$sid}) {
		$Heap{$sid}->{client}->put($data);
	    }
	}

$len > 0 のときは、 $sid で示されるブラウザに対して $data を送信します。 $len == 0 のときは、 EOF を伝えるコントロール ブロックなので、 「doShutdown」ルーチンを呼び出しています。

	else {	# len == 0
	    $PollBuf = $data;
	    &doShutdown($sid);
	}
    } while ($PollBuf ne "");
}

以上を、$PollBuf が空になるまで続けます。

doShutdown はブラウザとの TCP/IPセッションを shutdown するためのルーチンです。

sub doShutdown {
    my ($sid) = @_;
    if (defined $Heap{$sid}) {
	$Heap{$sid}->{client}->shutdown_input;
    }
}

次に doSession です。

sub doSession {
    my ($sid) = @_;
    if (defined $PollHeap) {
	my $req = $Buf{$sid};
	$Buf{$sid} = "";
	for my $block (unpack "(a2048)*", $req) {
	    $PollHeap->{client}->
		put(pack("nna*", $sid, length($block), $block));
	}
    }
}

ブラウザから送られてきたデータを、 2048 バイトずつ区切って「セッションID」「データ長」を 前につけることによってブロック化して、 relayagent に送信しています。

オリジナルの VPN-Warp を使ったことがある方は既にお気付きかも知れませんが、 上記 relayserver.pl は説明を簡単にするために機能をいくつか省いています。 例えば、オリジナルのリレー サーバは、 接続する際はクライアント認証が必須で、 同じクライアント証明書を提示した relayagent とブラウザを 結び付ける機能があるのですが、 上記 relayserver.pl はクライアント認証を行なわないので、 任意のブラウザから接続可能ですし、 同時接続が可能な relayagent は一つだけです。

腕に覚えのあるかたは、 オリジナルの VPN-Warp と同等の機能を実現するには どのような修正を加えればよいか、 考えてみてはいかがでしょうか? そして、 こういうことを考えることが好きなかた、 「いっしょにDSASつくりませんか?

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

Perl の非同期I/Oモジュール POE を使って VPN-Warp relayagent を書いてみました

多数の TCP/IP セッションを同時に維持する必要性などから、 非同期I/O が最近流行りのようです。 何をいまさら、という気もするのですが、 いわゆる「最新技術」の多くが 30年前の技術の焼き直しに過ぎない今日このごろなので、 非同期I/O 技術が「再発見」されるのも、 「歴史は繰り返す」の一環なのでしょう。 スレッドが当たり前の時代になってからコンピュータ技術を学んだ人にとっては、 (古めかしい) 非同期I/O が新鮮に映るのかも知れず、 なんだか「ファッションのリバイバル」に似ていますね。

Perl で非同期I/O 処理を手軽に行なうための枠組みとして、 POE: Perl Object Environment というものが あるようです。 POE を使うと、 あたかもスレッドを使っているような手軽さでプログラミングできます。 試しに VPN-Warp の relayagent を POE を使って書いてみました。 オリジナルの relayagent は C 言語で記述した 4000 行を超える プログラムなのですが、 Perl だと 200 行以下で一通り動くものが書けてしまいました (もちろん C 版の機能を全て実装したわけではありません)。

POE を触るのは今回が初めてだったので、 マニュアルをいちいち参照しながら書いたのですが、 なにせわずか 200 行ですから、 開発はデバッグ込みで 1 日かかりませんでした。 改めて Perl の記述性の良さと開発効率の高さに感動したのですが、 これだけ簡潔に書けてしまうと、 relayagent の機能を解説するときの教材としても使えそうです。

というわけで、 今までブラックボックスだった relayagent の中身の解説を試みたいと思います。 これから POE を使ってみようとする人の参考にもなれば幸いです。

VPN-Warp の relayagent とは、 以下の図のようにリレーサーバと Webサーバの両方へ接続して、 リレーサーバから受取ったリクエストを Webサーバへ中継するプログラムです。 http リクエストを受取ってサービスを行なうのですから、 サーバの一種と言えますが、 外部から接続を受付けるわけではなく、 リレーサーバと Webサーバの両方に対してクライアントとして振る舞う点が ユニークと言えるでしょう。

                      リレー            イントラ         イントラ
ブラウザ ─────→ サーバ ←──── relayagent──→ Webサーバ
            https     443番ポート                        80番ポート

http リクエストを受取って Webサーバへ中継するプログラムというと、 proxy サーバを思い浮かべるかも知れません。 proxy サーバはその名の通り、 ブラウザに対してはサーバとして振る舞います:

                                        proxy            イントラ
ブラウザ ──────────────→ サーバ────→ Webサーバ
                                        8080番ポート     80番ポート

proxy サーバが、ブラウザからの接続を受付けて、 それを Webサーバに中継するのに対し、 relayagent は自身では接続を受付けずに中継する、 という違いがお分かりでしょうか? relayagent は接続を受ける必要がないため、 ファイアウォールの内側など、 外部からアクセスできない場所で使うことが可能になっています。

なお、C 版の relayagent はリレーサーバに対して https で接続するのですが、 Perl 版 relayagent (以下 relayagent.pl) は、 説明の都合上 SSL 暗号化の機能を含んでいません。 実際に使うときは、 stone などで SSL 暗号化して リレーサーバに接続する必要があります。

         リレー                         イントラ         イントラ
         サーバ ←──── stone ←── relayagent──→ Webサーバ
         443番ポート       SSL化        Perl 版          80番ポート

例えば stone を

stone -q pfx=relay,5000005.pfx \
      -q passfile=relay,5000005-pass.txt \
      warp.klab.org:443/ssl localhost:12345 &

などと実行しておき、 relayagent.pl はリレーサーバに接続する代わりに、 localhost の 12345 番に接続します。

では、relayagent.pl を順に見ていきましょう。

#!/usr/bin/perl
use POE qw(Component::Client::TCP Filter::Stream);
my $IdleTimerMax = 6;	# 60 sec
&help unless @ARGV == 2;
&help unless shift =~ m/^(\w+):(\d+)$/;
my ($RelayHost, $RelayPort) = ($1, $2);
&help unless shift =~ m/^(\w+):(\d+)$/;
my ($WebHost, $WebPort) = ($1, $2);
my %WebHeap;
my $PollBuf;
my $PollHeap;
my $PollHeader;
my $IdleTimer;
my $DisconectTime = 0;

$RelayHost, $RelayPort は、 リレーサーバのホスト名とポート番号ですが、
前述したように stone 経由でリレーサーバにつなぐために、
$RelayHost = "localhost", $RelayPort = 12345 などとなります。また、 $WebHost, $WebPort は、 中継先となる (イントラの) Webサーバのホスト名とポート番号です。

続いて、リレーサーバへ接続する (直接の接続先は SSL 化を行なう stone ですが、 煩雑になるので以下 「リレーサーバ」 と略記します) ためのコードです:

POE::Component::Client::TCP->new
    ( RemoteAddress => $RelayHost,
      RemotePort    => $RelayPort,
      Connected     => sub {
	  $PollHeap = $_[HEAP];
	  undef $PollHeader;
	  $PollBuf = "";
	  $IdleTimer = $IdleTimerMax;
	  $PollHeap->{server}->
	      put("GET /KLAB/poll HTTP/1.1\r\nX-Ver: realyagent.pl 0.01\r\n\r\n");
      },
      ServerInput   => sub {
	  $PollHeap = $_[HEAP];
	  $PollBuf .= $_[ARG0];
	  &doPoll;
      },
      Filter        => POE::Filter::Stream->new(),
      Disconnected  => \&reconnectPoll,
    );

POE では、非同期に動く処理を、 処理ごとに分けて書くことができます。 各処理のことを「POEセッション」と呼びます。

上記は、リレーサーバへ接続する POEセッションの生成です。 接続先ホストおよびポートを、 それぞれ $RelayHost と $RelayPort に設定しています。

「Connected => sub {」から始まる部分が、 接続に成功したときに実行するコードです。 細かいところはさておき、 接続したら以下のリクエストをリレーサーバに送る、 という点は読み取れるのではないでしょうか。

GET /KLAB/poll HTTP/1.1
X-Ver: realyagent.pl 0.01

同様に、 「ServerInput => sub {」から始まる部分が、 通信相手 (リレーサーバ) からデータを受信したときに実行するコードです。 受信したデータは、 いったん変数 $PollBuf に溜めておいて、 続いて呼び出す doPoll の中で処理を行ないます。

以上からお分かりのように、 リレーサーバへデータを送るときは、
「$PollHeap->{server}->put(送るべきデータ);」を実行し、 リレーサーバからデータが送られてきた時は、 doPoll で受取ります。 とても見通しが良いですね。

各 POEセッションは、スレッドと同様、同一メモリ空間を共有しているので、 他の POEセッションが変更した変数の値を参照できます。 したがってどの POEセッションでもリレーサーバへデータを送ることができますし、 リレーサーバから受信したデータはどの POEセッションでも読むことができます。

続いて、もう一つ POEセッションを作ります。

POE::Session->create
    ( inline_states =>
      { _start => sub {
	  $_[KERNEL]->delay( tick => 10 );
        },
        tick => sub {
	    if ($IdleTimer > 0) {
		if (--$IdleTimer <= 0) {
		    &sendControl(0, -2);	# keep alive
		}
	    }
            $_[KERNEL]->delay( tick => 10 );
        },
      },
    );
$poe_kernel->run;
exit;

この POEセッションは 10秒に一回、 「tick => sub {」から始まる部分を実行します。 見ての通り、$IdleTimer の値を減らしていって、 0 になったら sendControl を実行します。 $IdleTimer は最初 6 ($IdleTimerMax) に設定されるので、 1 分ごとに sendControl を実行する、という意味ですね。

以上 2つの POEセッションは作成しただけで、まだ走り出していません。
その次の「$poe_kernel->run;」が各 POEセッションを走らせるための呼び出しです。 このルーチンは全ての POEセッションが終了するまで返ってきません。

さて、relayagent はリレーサーバとの接続を常時維持していますが、 無通信時間が続くと (通信経路中にあるファイアウォールなどに) 切られてしまう恐れがあるので、 keep alive ブロックを送信しています。 通信が行なわれていない時間を測るためのカウンタが $IdleTimer というわけです。

通信が行なわれない限り $IdleTimer は減り続け、 1 分経過すると sendControl(0, -2) を呼び出して keep alive ブロックを送信します。 sendControl はこんな感じ:

sub sendControl {
    my ($id, $control) = @_;
    $control += 65536 if $control < 0;
    $IdleTimer = $IdleTimerMax;
    if (defined $PollHeap && $PollHeap->{connected}) {
	$PollHeap->{server}->put(pack("nn", $id, $control));
    }
}

既に説明したように「$PollHeap->{server}->put(データ)」は、 リレーサーバにデータを送る呼び出しですから、 「pack("nn", 0, 65534)」が keep alive ブロックであることが分かります。

「ブロック」というのは VPN-Warp 用語でして、 relayagent とリレーサーバとの通信は、 基本的にこの「ブロック」を単位にして行ないます。 ブロックは次のような可変長のデータです。

    ┌───┬───┬───┬───┬───┬─≪─┬───┐
    │セッションID│ データ長  │  可変長データ   │
    └───┴───┴───┴───┴───┴─≫─┴───┘
          2バイト         2バイト      「データ長」バイト

「セッションID」および「データ長」は、ビッグエンディアンです。 つまり上位バイトが先に来ます。 データ長が 0 ないし負数の場合は、 「可変長データ」の部分は 0 バイトになります。

データ長が 0 ないし負数であるブロックは、 コントロール用のブロックで、 以下の意味を持っています:

データ長意味内容
0EOFWebセッションの終了を要求
-1ErrorWebセッションの異常終了を要求
-2Keep Alive無通信状態が続いたときに送信
-3X OFFWebセッションのデータ送信の一時停止を要求
-4X ONWebセッションのデータ送信の再開を要求

ブラウザ送ったリクエストを Webサーバに届け、 Webサーバのレスポンスをブラウザに返す一連の通信のことを、 ここでは「Webセッション」と呼ぶことにします。 つまり、 VPN-Warp が提供する仮想的な通信路 (トンネル) 上のセッションです。

VPN-Warp セッション

ブラウザがリレーサーバと通信するときの TCP/IPセッションと、 relayagent と Webサーバが通信するときの TCP/IPセッションを対応づけるのが、 セッションID です。 「セッション」という言葉が何度も出てきてややこしいですが、 「セッションID」の「セッション」は、 「Webセッション」の意味です。

リレーサーバと relayagent との間は、 複数の Webセッションを一本の TCP/IPセッションに相乗りさせるので、 そのとき各 Webセッションがこんがらないようにするために ブロックにはセッションID がつけられている、というわけです。

では、次はいよいよ relayagent の中核ルーチンである doPoll です:

sub doPoll {
    do {
	if (! defined $PollHeader) {
	    if ($PollBuf =~ /\r\n\r\n/) {
		$PollHeader = $`;
		$PollBuf = $';
	    }
	}
	return unless defined $PollHeader;
	my ($id, $len, $data) = unpack("nna*", $PollBuf);
	return unless defined $id && defined $len && $len ne "";
	if ($len > 32767) {
	    $len -= 65536;
	    $PollBuf = $data;
	    if ($len == -1) {
		&closeWeb($id);
	    }
	} elsif ($len > 0) {
	    return unless defined $data && length($data) >= $len;
	    ($data, $PollBuf) = unpack "a${len}a*", $data;
	    &reqWeb($id, $data);
	} else {	# len == 0
	    $PollBuf = $data;
	    &closeWeb($id);
	}
    } while ($PollBuf);
}

前述したように、relayagent はリレーサーバに接続したとき、 まず
「GET /KLAB/poll HTTP/1.1」から始まるリクエストヘッダを送ります。 するとリレーサーバは、 次のようなレスポンスを返します:

HTTP/1.1 200 OK
X-Customer: nusers=5&type=1&expire=1169696110&digest=3f6977eceb8c2c43e28e6026b08ba900

そしてこの後 (doPoll において「defined $PollHeader」が真のとき)、 リレーサーバと relayagent は、 前述したブロックを送受信することになります。

「my ($id, $len, $data) = unpack("nna*", $PollBuf);」の部分が、
リレーサーバから受信したブロックを、
「セッションID ($id)」 「データ長 ($len)」 「可変長データ ($data)」 に分解している処理ですね。 続いてブロックの処理が行なわれますが、 コントロールブロックに関する処理は割愛して、 可変長データが付いているブロックの処理を見ていきましょう。 ここで受信した可変長データは、 ブラウザが送信した http リクエストを 2048バイトごとに分割したものです。

つまりリレーサーバは、 ブラウザから https リクエストを受取るたびに「セッションID」を割り振ります。 そして、リクエストをブロックに分割して relayagent へ送信し、 逆に relayagent から受取ったブロックを 同じセッションID ごとに連結して、 http レスポンスとしてブラウザへ送信します。

したがって、 relayagent はリレーサーバから受取ったブロックを 同じセッションID ごとに連結して Webサーバへ中継し、 そのレスポンスをブロックに分割してリレーサーバへ送信すればよいことになります。

同じセッションID ごとに連結して Webサーバへ送信する処理が、 reqWeb です:

sub reqWeb {
    my ($id, $req) = @_;
    if (defined $WebHeap{$id} && $WebHeap{$id}->{connected}) {
	$WebHeap{$id}->{server}->put($req);
    } else {
	POE::Component::Client::TCP->new
	    ( RemoteAddress => $WebHost,
	      RemotePort    => $WebPort,
	      Connected     => sub {
		  $WebHeap{$id} = $_[HEAP];
		  $WebHeap{$id}->{server}->put($req);
	      },
	      ServerInput   => sub {
		  $WebHeap{$id} = $_[HEAP];
		  &sendRes($id, $_[ARG0]);
	      },
	      Filter        => POE::Filter::Stream->new(),
	      Disconnected  => sub {
		  &sendControl($id, 0);
	      },
	    );
    }
}

「POE::Component::Client::TCP->new」によって、 Webサーバと通信するための POEセッションを生成しています。 この reqWeb を実行しているのは、 リレーサーバとの通信を受け持つ POEセッションでしたが、 この POEセッションが新たに POEセッションを生成している点に注意してください。

新しく生成した POEセッションは、Webサーバと接続したとき (Connected)、
「$WebHeap{$id}->{server}->put($req);」を実行して リクエスト ($req) を Webサーバに送信します。 そして Webサーバからレスポンスを受信したとき (ServerInput)、 sendRes を実行します。

sub sendRes {
    my ($id, $res) = @_;
    $IdleTimer = $IdleTimerMax;
    if (defined $PollHeap && $PollHeap->{connected}) {
	for my $block (unpack "(a2048)*", $res) {
	    $PollHeap->{server}->
		put(pack("nna*", $id, length($block), $block));
	}
    }
}

sendRes は Webサーバからのレスポンス ($res) を 2048バイトごとに分割し、 セッションID ($id) とデータ長 (length($block)) を付加した ブロックとしてリレーサーバに送信します。

以上をまとめたのが、relayagent スクリプト です。 ここで解説した機能の他、 http リクエストヘッダの Host: フィールドを書き換える機能も追加しています。

C 版の relayagent に比べると、 http レスポンスの書き換え機能や、 http 以外のプロトコルを通す機能などがない点や、 高負荷時の性能の検証が充分行なえていない点など、 そのまま実運用に使用するには難しい点もありそうですが、 少なくとも プロトタイピングなどの目的 (あるいは教育などの目的) ならば 充分使えそうです。

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

La Fonera (FON ソーシャルルータ) で VPN-Warp を使う

La Fonera (FON ソーシャルルータ) って知っていますか?

FONは世界最大のWiFiコミュニティです。 誰もが「世界中どこからでもインターネットに無料で接続したい!」という 望みを持っているはずです。 そのようなメンバーが助け合ってWiFiを広めて行こう!ということを コンセプトに私たちは活動しています。 元々簡単なアイディアで始まったFONコミュニティ。 メンバーが作るWiFiインフラを用いて、 WiFiを世界中のどこからでも楽しめるようにしましょう!。
参加は簡単!FON取り扱い店でLa Foneraを購入して接続してスタートするだけ!

La Fonera は、FON のアクセスポイントであると同時に、 普通の (プライベートな) 無線LAN アクセスポイントとしても利用できます。 自宅などで無線LAN アクセスポイントを設置している人は多いと思いますが、 おそらくそのまま La Fonera で置き換えることが可能でしょう。

実際、私はそれまで使ってた 無線LAN ルータ WN-G54/R2 を La Fonera で置き換えてしまいました。 La Fonera が提供する二つのアクセスポイントのうち、 プライベート側を使えば、 自宅LAN に普通にアクセスできて、 いままで使っていた無線LAN ルータに比べて全く遜色ありません。 もちろん WPA2 (Wi-Fi Protected Access 2) 暗号化方式が使えます。

より正確に言うと、 La Fonera のプライベート側アクセスポイント (MyPlace) は、 自宅LAN とは異なるセグメントになります。 有線LAN と無線LAN 相互で自由に通信するためには、 多少の設定変更 (/etc/firewall.user に 2行ほど追加) が必要になります。

私の場合 WPA に対応していない古い無線LAN 端末 (Windows98マシン^^;) も 持っていて、 自宅の無線LAN を WEP から WPA2 に変更した (自宅LAN といえど、WEP を使うのはちょっと抵抗がありますよね?) 時点で、 お蔵入りにしていました。 La Fonera が提供するもう一つのアクセスポイント (パブリック側) を使えば、 直接自宅LAN へはアクセスできないもののインターネットへは接続できるし、 インターネット経由で自宅LAN に戻ってくる (もちろんインターネットからアクセス可能な部分に限定されますが) ことも可能なので、 La Fonera の導入によって古い無線LAN 端末も再び利用できるようになった、 というオマケがつきました。

さて、 この La Fonera で VPN-Warp が利用可能になったらどうなるでしょうか? 無線LAN ルータが VPN ゲートウェイの機能も持つわけです。 つまりインターネットに接続できる環境ならどこからでも、 自宅LAN へ手軽にアクセスできるようになります。

ここで La Fonera は固定 IP アドレスを持つ必要がないばかりか、 そもそもグローバルアドレスを持つ必要性すらない、 という点がミソです。 必要なことは、La Fonera からインターネットへ接続可能、ということだけです。 La Fonera をどこに設置しようと、 その設置した LAN に外部からアクセスすることができます。

以下は、La Fonera で VPN-Warp を使うようにするための手順です。 現状は多少の Linux の知識が必要となってしまうのですが、 要は relayagent プログラムを La Fonera にインストールするだけのことなので、 適切なインストーラさえ作ればいくらでも簡単な手順になることでしょう。 なので、難しそうだとあきらめるのではなく、 関心がある方はご連絡頂ければと思います。

まず La Fonera に ssh あるいはシリアルコンソールで ログインすることが必要となります。 おそらくこれが最大の難関でしょう (^^;)。

senri:/home/sengoku % ssh fonera
root@fonera's password:


BusyBox v1.1.3 (2006.11.21-19:49+0000) Built-in shell (ash)
Enter 'help' for a list of built-in commands.

 _______  _______  _______
|   ____||       ||   _   |
|   ____||   -   ||  | |  |
|   |    |_______||__| |__|
|___|

 Fonera Firmware (Version 0.7.1 rev 2) -------------
  *
  * Based on OpenWrt - http://openwrt.org
  * Powered by FON - http://www.fon.com
 ---------------------------------------------------
root@OpenWrt:~#

ログインさえ可能なら、あとはさほど難しくはありません。 まずネットワーク経由で La Fonera に relayagent を インストールするための準備をします。 具体的には、 以下のように /etc/ipkg.conf に「src gcd http://www.gcd.org/fonera」 を追加し、 「ipkg update」 を実行します。

root@OpenWrt:~# echo "src gcd http://www.gcd.org/fonera" >> /etc/ipkg.conf
root@OpenWrt:~# ipkg update
Downloading http://download.fon.com/release/fonera/0.7/packages/Packages
Updated list of available packages in /usr/lib/ipkg/lists/fon
Downloading http://www.gcd.org/fonera/Packages
Updated list of available packages in /usr/lib/ipkg/lists/gcd
Done.
root@OpenWrt:~#

http://www.gcd.org/fonera というのは、 私が最近始めた La Fonera 用の ipkg feed です。 つまり、上記のような設定をしておくと、 La Fonera にいろいろなソフトウェアを ネットワーク経由でインストールすることができるようになります。 もちろん ;-) VPN-Warp の relayagent も、 ここからインストールできます。 インストールに必要なコマンドは、 「ipkg install relayagent」だけです。 このコマンドを打ち込むだけで、 relayagent の実行に必要な OpenSSL ライブラリ等が 自動的にインストールされます。

root@OpenWrt:~# ipkg install relayagent
Installing relayagent (1.0.7) to root...
Downloading http://www.gcd.org/fonera/relayagent_1.0.7_mips.ipk
Installing libopenssl (0.9.8d-1) to root...
Downloading http://www.gcd.org/fonera/libopenssl_0.9.8d-1_mips.ipk
Installing zlib (1.2.3-3) to root...
Downloading http://www.gcd.org/fonera/zlib_1.2.3-3_mips.ipk
Configuring libopenssl
Configuring relayagent
Configuring zlib
Done.
root@OpenWrt:~#

次に、VPN-Warp の証明書をインストールします。 BIGLOBE の VPN ワープのページ などから入手した証明書とそのパスワードを記したファイルを、 La Fonera の /etc/warp ディレクトリへコピーしてください。 以下の実行例では relay,5000005.pfx が証明書のファイル、 relay,5000005-pass.txt がパスワードを記したファイルです。 5000005 というのは私が使用している証明書の番号なので、 実際に取得した証明書の番号で読み替えてください。

senri:/home/sengoku % echo "パスワード" > relay,5000005-pass.txt
senri:/home/sengoku % scp -p relay,5000005.pfx relay,5000005-pass.txt fonera:/etc/warp/
root@fonera's password:
relay,5000005.pfx                             100% 4856     4.7KB/s   00:00
relay,5000005-pass.txt                        100%    9     0.0KB/s   00:00

次が最後の難関で、 VPN-Warp の設定ファイルを作成します。 /etc/warp/relayagent.cfg.sample に設定ファイルのサンプルがあるので、 これを /etc/warp/relayagent.cfg へコピーして、 vi エディタで編集します。

root@OpenWrt:~# cd /etc/warp/
root@OpenWrt:/etc/warp# ls -l
-rw-------    1 root     root            9 Jan  9 04:09 relay,5000005-pass.txt
-rw-------    1 root     root         4856 Jan  9 04:09 relay,5000005.pfx
-rw-r--r--    1 root     root         3290 Jan  9 07:47 relayagent.cfg.sample
root@OpenWrt:/etc/warp# cp relayagent.cfg.sample relayagent.cfg
root@OpenWrt:/etc/warp# vi relayagent.cfg

設定ファイル relayagent.cfg の中に、以下のような部分があるので、
「relay,0000000」の数字の部分を実際に取得した証明書
(私の場合は「relay,5000005」) の番号で置き換えます。

#--------------------------------------------------------------------
#
# -x  PFX 形式 クライアント証明書を指定
# -X  同パスワードファイルを指定
#
#--------------------------------------------------------------------

-x "/etc/warp/relay,0000000.pfx"
-X "/etc/warp/relay,0000000-pass.txt"

以上で設定ファイルの作成が完了しました。 あとは La Fonera を再起動する (裏面のリセットスイッチを押す) だけです。 再起動する代わりに、起動スクリプトを実行することによって relayagent を起動することもできます。

root@OpenWrt:~# /etc/init.d/N50relayagent start
root@OpenWrt:~#

では、ブラウザで https://warp.klab.org へアクセスしてみましょう。 La Fonera にインストールした証明書と同じ証明書、あるいは その子証明書を使ってアクセスしてください。 La Fonera の Web 設定ページが表示されたら成功です。

Web 設定ページが表示されるのは、 先ほど作成した設定ファイル /etc/warp/relayagent.cfg に、

#--------------------------------------------------------------------
#
# <relay サーバ名:ポート番号>  <転送先ホスト名:ポート番号>
#
# ※-p オプション指定時は下記の意味となる
#
# <プロキシサーバ名:ポート番号> <転送先ホスト名:ポート番号>
#
#--------------------------------------------------------------------

warp.klab.org:443 localhost:80

と書いてあるからです。 「localhost:80」だから La Fonera 内蔵の WWW サーバ (つまり設定ページ) ですね。 「localhost:80」の部分を、 自宅LAN 内の適当なサーバの「アドレス:ポート」で置き換えれば、 そのサーバにアクセスできますし、 通常の VPN-Warp と全く同様に、 任意のサーバに接続するように設定することもできます。

Filed under: La Fonera,システム構築・運用 — hiroaki_sengoku @ 07:02
2006年11月30日

迷惑メール送信者とのイタチごっこを終わらせるために (2)

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円也を払う」) ので採用したくない、 とお考えのかたがいたら、 そもそもなぜ迷惑メールが蔓延しているのか考えてみて頂きたい。

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

Filed under: システム構築・運用 — hiroaki_sengoku @ 07:24
2006年11月11日

ADSLモデム Aterm WD735GV の WAN 側 IP アドレスを取得する方法

ケータイのキャリアを 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 アドレスを取得するために、 これだけ沢山のデータをやり取りするのもどうかと思うが...

Filed under: システム構築・運用,ハードウェアの認識と制御 — hiroaki_sengoku @ 08:23
2006年11月4日

ダイナミックDNSサービス iobb.net

ダイナミック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'
Filed under: システム構築・運用 — hiroaki_sengoku @ 07:25
2006年10月16日

迷惑メール送信者とのイタチごっこを終わらせるために (1)

迷惑メール (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 でお願いしたい。 このブラックリストに対するご意見は歓迎するが、 このブラックリストを利用したことによって、 あるいは利用できないことによって発生するいかなる損害に対しても 私は責任を負わない。 この条件に同意して頂けるかたにのみ、 このブラックリストの利用を許諾する。

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