システム構築・運用

このエントリーを含むブックマーク 2009年01月08日

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

続きを読む

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

続きを読む

hiroaki_sengoku at 07:38|この記事のURLComments(0)TrackBack(0)
このエントリーを含むブックマーク 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(1)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(2)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)
このエントリーを含むブックマーク 2006年06月29日

jabber.jp と Google Talk との相互接続開始」で jabber.jp の紹介をしたのが原因というわけでもないのだろうが、 一時的に jabber サーバが過負荷になってサービス不能状態に陥ってしまった。 この jabber.jp は KLab のサーバマシン群の中では低性能なマシンを使っていたので、 この機会にと、もう少しマシなマシンへ引越しさせた。 が、負荷の問題というよりは、サーバプログラム自体に、 なにかのタイミングでハングするバグがあるようだ。 他のソフトウェアへの乗り換えも含めて対策を検討中であるが、 とりあえずハングしていないかの死活確認は必須だろう。

まず CPAN から Net::Jabber をインストール。 「Google Talk を流れるデータを見る」に、
「Net::Jabberは,インストールしようと思ってもmake testでひっかかってしまってうまくインストールできません.」と書いてあったのが 気になりながら (^^;) 作業していると...

cpan> install Net::Jabber(改行)
	...
---- Unsatisfied dependencies detected during [R/RE/REATMON/Net-XMPP-1.0.tar.gz] -----
    XML::Stream
Shall I follow them and prepend them to the queue
of modules we are processing right now? [yes] (改行)
Running make test
	...
t/buildxml......ok
t/load..........ok
t/parse_node....ok
t/parse_tree....ok
t/tcpip.........ok
t/tcpip2ssl.....ok 2/3

ありゃ、止まってしまった。 XML-Stream-1.22/t/tcpip2ssl.t を見ると、

my $status = $stream->Connect(hostname=>"obelisk.net",
                              port=>5223,
                              namespace=>"jabber:client",
                              connectiontype=>"tcpip",
                              ssl=>1,
                              timeout=>10);

などと書いてある。 つまり、jabber サーバである「obelisk.net」を相手に 接続テストを行なおうとして止まってしまっている。 おそらく obelisk.net が相手にしてくれないのだろう (5222番ポートの方は相手をしてくれるようだ)。 とりあえず obelisk.net の代わりに手元の jabber サーバを指定して、 テストを成功させ、無事インストール完了。

とりあえず、さくっと死活確認スクリプトを書いてみる:

#!/usr/bin/perl
@RCPTS = ('jabber-error@gcd.org',);
use Net::Jabber qw(Client);

&ckjabber("jabber.gcd.org", 5222, "test", "XXXXXXXX", "ckjabber.$$");
&ckjabber("jabber.jp", 5222, "test", "YYYYYYYY", "ckjabber.$$");

sub ckjabber {
    my ($server, $port, $username, $password, $resource) = @_;
    my $client = new Net::Jabber::Client();
    my $connected = 0;
    my $authenticated = 0;
    my $recv_subject;
    my $recv_body;
    my $subject = "check $server";
    my $body = "This is a test message from $resource.";
    my $onConnect = sub { $connected++; };
    my $onAuth = sub {
	$authenticated++;
	$client->MessageSend(to => "$username\@$server/$resource",
			     subject=> $subject, body => $body, );
    };
    my $onMessage = sub {
	my ($sid, $message) = @_;
	$recv_subject = $message->GetSubject();
	$recv_body = $message->GetBody();
	$client->Disconnect();
    };
    $client->Connect(hostname => $server);
    $client->SetCallBacks(onconnect => $onConnect, onauth => $onAuth,
			  message => $onMessage, );
    $client->Execute(username => $username, password => $password,
		     resource => $resource,
		     hostname => $server, port => $port,
		     register => 1,
		     connectsleep => 0, connectattempts => 1, );
    my $error;
    if (! $connected) {
	$error = "can't connect to $server";
    } elsif (! $authenticated) {
	$error = "authentication failure";
    } elsif ($recv_subject ne $subject || $recv_body ne $body) {
	$error = "get '$recv_subject' & '$recv_body'";
    }
    if ($error) {
	print STDERR "$server error\n$error\n";
	&mail("$server error", localtime(time) . "\n$error\n", @RCPTS);
    }
}

sub mail {
    my ($subject, $body, @rcpts) = @_;
    open(INJECT,"|/var/qmail/bin/qmail-inject " . join(" ", @rcpts)) || die;
    print INJECT "From: root\nTo: ", join(", ", @rcpts), "\n",
	"Subject: $subject\n\n$body";
    close(INJECT);
    if ($? << 8) {
	print STDERR "Fail to send mail to ", join(", ", @rcpts), "\n";
	exit 1;
    }
}

自分自身にメッセージを送ってみて、 同じ文面が返ってくるか確認するだけの単純なテストスクリプトだが、 jabber サーバがハングしたら検知してメールを送ってくれるだろう。 とりあえず 5 分に一度実行するように cron に仕掛けた。



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

DNS問合わせというと、 ネームサーバの UDP/IP の 53番ポートへ問合わせるのが一般的であり、 TCP/IP の 53番ポートはゾーン転送のみに使われることが多い。 しかし RFC (RFC1035 および RFC2136) 上は TCP も、UDP と同様、 通常の問合わせにも利用することができることになっているし、 多くのネームサーバでそのような実装になっている。

手近なネームサーバが利用できない環境 (某ホテルの無線LAN 環境など ;) で、 やむを得ず遠方のネームサーバを利用する際などに、 TCP を使って問合わせをすると、 ポートフォワードすることもできて便利である。

拙著「ネーム・サーバ (前編)」に書いたようにネームサーバには二種類あって、 ここで言うネームサーバは、 「/etc/resolv.conf に登録できるネーム・サーバー」のことである。 つまり、リゾルバから問い合わせがあると, 他のネーム・サーバーに問い合わせて結果を返してくれるタイプである。 一般にはキャッシュ ネーム サーバと呼ばれることが多いようだ。
ちなみにネームサーバには他に、 「答えを知っている問い合わせに対しては答えてくれるけれど, 知らない場合は,どこどこへ聞けと冷たく言い放つタイプ」がある。
また、キャッシュ ネーム サーバの一形態として、 他のキャッシュ ネーム サーバへ問合わせを転送する フォワード専用型キャッシュネームサーバもある。

広く普及しているネームサーバ実装の一つである djbdns には、 UDP 問合わせに失敗したり、 あるいは 512バイト以上の問合わせを行なう際は、 TCP にて問合わせを行なう機能があるが、 最初から TCP しか使わない (キャッシュ) ネームサーバがあると便利だろう。 すなわち、 遠方のネームサーバへ TCP で問合わせを転送する、 フォワード専用型サーバである。

フォワード専用型 キャッシュ ┌→ルート (root) ネームサーバ
リゾルバ──→キャッシュネームサーバ──→ ネームサーバ─┼→ネームサーバ
UDP TCP  ├→ネームサーバ
 └→

といった感じで TCP専用サーバを用いる。 この TCP専用サーバの作り方は簡単で、 djbdns の場合ならば次のようなパッチをあてるだけである。

dns_transmit.c で定義されている dns_transmit_start() 関数の末尾部分で、
    if (len + 16 > 512) return firsttcp(d);
    return firstudp(d);
となっている部分を、
    /* if (len + 16 > 512) */ return firsttcp(d);
    return firstudp(d);
で置き換える。

このようなパッチをあてた dnscache プログラムを、 環境変数 FORWARDONLY をセットした状態で呼び出せば、 「servers/@」に指定したキャッシュネームサーバへ TCP で問合わせを転送する、 フォワード専用型キャッシュネームサーバになる。



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

一週間ほど休暇で自宅を留守にしていたら、 運悪くちょうど半ばあたりで自宅の Linux サーバ (二台ある GCD ゲートウェイのうちの片方) がハングしてしまった。 きっかけは毎晩ハードディスの内容をバックアップするために動かしている rsync だったので、 メモリ不足 ? かなにかの原因でカーネルのバグを引き当ててしまったのか?

安定版でないカーネルを使っていて再びハングする危険もあるので、 早急にバージョンアップが必要なのだが、 その前に新しいカーネルで GV-MVP/RX2W を動かせるようにしないと... (^^;)
# ぱ研を参考にさせてもらっています (_O_)

GCD のゲートウェイは VRRP で二重化してあるので、 片方がハングしても問題無いのではあるが、 手動リセットするまでハングしっぱなしというのでは、 留守の期間が長いと二台ともハングする可能性を否定できない。
# UUCP スプールなど二重化されていないものもあるので、 全く問題が無いというわけでもない

そこでハングしてもなるべく自動的に復帰できるように ウォッチドッグ タイマ (Watchdog Timer) を仕掛けることにした。 つまり一定期間内にタイマがリセットされなければ、 ハングしたと見なして問答無用でカーネルを落とす仕掛けである。

ソフトウェアにどんなトラブルが起きても確実に再起動を行なわせるには、 ハードウェアで物理的にリセット スイッチを押すハードウェアを用いるのが 一番であるが、まずはお手軽にソフトウェア版を利用してみることにした。 ソフトウェアによるウォッチドッグ タイマの場合、 カーネル パニックが起きると当然機能しなくなるわけだから、 パニック時は自動的に再起動するように設定しておく必要がある。 rc.local 等で、

echo 60 > /proc/sys/kernel/panic
echo 1 > /proc/sys/kernel/panic_on_oops

を行なっておけば、パニック時は 60秒で再起動するようになる。

次に、ソフトウェア版ウォッチドッグ モジュール (softdog) をインストールする。 softdog.ko を /lib/modules/current 以下に置き、 /etc/modprobe.conf に以下の行を追加するだけ。

alias char-major-10-130 softdog
options softdog soft_margin=3600

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

後は適当な方法で、 /dev/watchdog に適当な文字 (ただし「V」を除く) を書込むだけである。 例えば、定期的に実行される sh スクリプトに、

echo "@" > /dev/watchdog

という行を追加しておくだけでよい。

Linux カーネルのドキュメント: linux/Documentation/watchdog/watchdog.txt には、

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, const char *argv[])
{
	int fd=open("/dev/watchdog",O_WRONLY);
	if(fd==-1)
	{
		perror("watchdog");
		exit(1);
	}
	while(1)
	{
		write(fd,"\0",1);
		fsync(fd);
		sleep(10);
	}
}

というサンプルが載っているが、タイマをリセットするプログラムを 動かしっぱなしにする方法だと、 ハングの仕方によってはこのプログラムが動き続ける (つまり自動再起動が行なわれない) 危険がある。 私の自宅サーバの場合は、たとえカーネルが生きていても、 リモートから ssh ログイン出来ない状態なら使い物にならないわけで、 外部から ssh ログイン出来る場合のみタイマのリセットが行なわれるようにした。 つまり、外部から

ssh server "echo @ > /dev/watchdog"

に相当することを定期的に (一時間以内の間隔で) 行なえばよい。 この場合、/dev/watchdog に文字を出力する都度、 /dev/watchdog をクローズすることになり、

SoftDog: Unexpected close, not stopping watchdog!

というメッセージが syslog に出力されるので、 果たしてきちんとタイマのリセットが行なわれているのか不安になるが、 softdog モジュールのソースを見ると、 このメッセージが出力される場合は常に 「softdog_keepalive()」呼び出しが行なわれているので問題ない。



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

GCD LAN内で Real Player コンテンツを再生しようとしたら、 GCDゲートウェイで、外向きポート554番をブロックしていたので、 とりあえず特定の接続先だけ解除する。

これでコンテンツを見られると、思って再生ボタンを押すと、 いきなりゲートウェイがネットワークから見えなくなった。 NICごと死んだのか、IP, IPv6 ともに反応なし。 二つあるNICの両方が無反応なので、 原因はドライバより上のレベルにありそう。 あいにくゲートウェイにはディスプレイカードを入れていないので、 シリアル(RS-232C)経由でログインしてみると、 iptables のログが延々と出力されている。 大半が、root ネームサーバのポート53番から。 つまり DNS 問い合わせに対する応答パケット。 ということは、PPPoE が生きている。これはマズイ。

機能しなくなったゲートウェイが PPPoE をつかんだままでは、 MASTER に昇格したもう片方のゲートウェイが PPPoE できない。 正確に言えば、PPPoE 自体はできるのだがプロバイダがどちらの ゲートウェイへパケットを送ってくれるかは運次第。 事実、ほとんどのパケットは機能しなくなったゲートウェイ側に 届いたので、GCD からインターネットへの通信が事実上不可能になった。

冗長構成を組むとき最も難しいのが、いかにして中途半端な死に方を 回避するかだが、死んだゲートウェイに PPPoE を止めさせるのも、 なかなか難しそうだ。

  1. PPPoE している機能不全状態のゲートウェイには頼らず、
  2. PPPoE 先のプロバイダ側には手が出せない、

と仮定すると、どうすればいいだろうか。 機能不全状態のゲートウェイのNIC を LAN から切り離す仕掛けを 組み込むとかだろうか? (どうやって? ^^;)



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

CTO日記のほうで 「VPN-Warp入門編」を始めた。 VPN-Warpというのは、port forwarder で、「relay agent」プログラムを動かしているマシンへ、「リレーサーバ」経由で TCP トンネルを掘ることができる。 つまり、イントラの中だろうが固定IPがついてない NAT の中だろうが、ダイアルアップするノートPCだろうが、インターネットへアクセスすることさえできるならば、relay agent を動かすだけで外部からアクセスできるトンネルが設置可能。

ssh の -R オプションと、-L オプションを組み合わせて同様のことが実現可能であるが、大きな違いは、ssh とちがってアカウントを作る必要がないこと。SSLクライアント証明書を発行するだけなので、異なるセキュリティポリシーのトンネルを何本でも手軽に設置できる。実際、KLab のインフラの各所で活用している。



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

ネットが切れる不安という日記を見つけた。

引っ越し先には光ファイバーが入っていないのだ(汗) 明日、ADSLが大丈夫かどうか確認して、なるべく間隔があかないように手続きをしないと。 今の時代、一日でもネットに接続できないと、なんだか不安になってしまう。

私も 6年前に引越したのだが、 ネットが切れる不安におそわれた。 なんとか間隔があかないように苦労したのを覚えている。 顛末は日経Linuxの連載の枕 (PDF版) に書いた。

引っ越し作業中も社宅でサーバーを動かし続ける必要があるわけですが,部屋中に電話線やらEthernetケーブルが張り巡らせてある状態で引っ越し作業ができるはずはないので,サーバー,ルーター,TA(Terminal Adapter),電話等のネットワーク関係を部屋の片隅の半畳程度の部分に再構築し,作業中絶対にこの部分には近付かないよう*3 引っ越し業者の方にお願いすることにしました

当時はまだ ADSL のサービスが始まっていなかったので、OCN エコノミーを利用していた。だから TA。



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

今朝未明、bフレッツのメンテナンス工事があって、 GCDの対外線が切れた。 切れたこと自体は事前に NTT からアナウンスがあったことだし 問題はないのだが、

bフレッツが切れたことにより PPPoE を行っている pppd が終了すると、 /etc/ppp/adsl-lost が実行される。 GCD の adsl-lost は次のようになっている:

#!/bin/sh
PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/sbin:/usr/local/bin
. /etc/rc.d/hostname
MASTER_FILE="/var/run/keepalived_vrrp.MASTER"
if [ -f $MASTER_FILE -a "$1" = "/etc/ppp/pppoe.conf" ]; then
    rm -f $MASTER_FILE
    ERROR_FILE="/var/run/keepalived_vrrp.ERROR"
    echo 2 >> $ERROR_FILE
    /command/svc -t /service/keepalived/
fi

つまり、/var/run/keepalived_vrrp.MASTER を削除したうえで keepalived を再起動する。 すると、keepalived は BACKUP 状態に移行する。 すると、

/etc/rc.d/vrrp_notify INSTANCE VI BACKUP

実行されて、vrrp_notify が adsl-stop を呼び出す。

そして、adsl-stop が adsl-connect が強制終了する。 こうして完全な BACKUP 状態になるはずだった。

ところが

今朝起きて見てみると、 ゲートウェイが両方とも BACKUP 状態になっていた。(*_*)

片方のゲートウェイは PPPoE がつながっていて、 インターネットに正常に接続しているのだが、 あいにく BACKUP 状態なので、 GCD LAN のデフォルトゲートウェイアドレスである 192.168.1.1 を 持っておらず、 GCD LAN の他のマシンからインターネットへ接続できない状態に 陥っていた。

どうしてこんな事態になってしまったのか考えるより、 まずは復旧が優先ということで、 PPPoE がつながっているのに BACKUP 状態となっている 中途半端なゲートウェイ上で adsl-connect プロセスを殺し、 keepalived を再起動させて、 完全な BACKUP 状態へ移行させた。

すると、もう片方のゲートウェイが自動的に MASTER へ昇格して、 無事正常な状態に戻った。

その後、原因を調べていると、 前述したように adsl-stop が adsl-connect を強制終了させることが できずに adsl-connect が走り続けていたことが原因と判明。 つまり、adsl-connect が走り続けたために、 BACKUP 状態であるにもかかわらず PPPoE のリトライを行っていた。 そして bフレッツのメンテナンス工事の終了にともなって PPPoE が成功、 BACKUP 状態なのに PPPoE でつながっている、という状態が発生した。 この状態では、正常にインターネットにつながっているために、 MASTER に昇格することはなく、BACKUP 状態が継続する。

一方、もう片方のゲートウェイは、MASTER に昇格しようとして PPPoE を試みても失敗してしまう。 つまり MASTER へ昇格できない。 したがって、両方のゲートウェイが BACKUP 状態のままになる。

というわけで問題は adsl-stop が adsl-connect を 終了させることができない点にありそうだと推測。 あらためて adsl-stop を読みなおすと...

    # Kill pppd, which should in turn kill pppoe
    if [ -r "$PPPD_PIDFILE" ] ; then
        PPPD_PID=`cat "$PPPD_PIDFILE"`
        $LOGGER -p daemon.notice "Killing pppd"
        echo "Killing pppd ($PPPD_PID)"
        kill $PPPD_PID > /dev/null 2>&1 || exit 1
    fi
    ....
    # Kill adsl-connect
    $LOGGER -p daemon.notice "Killing adsl-connect"
    echo "Killing adsl-connect ($PID)"
    kill $PID > /dev/null 2>&1

以前このスクリプトを読んだときは見落としていたのだが、 「kill $PPPD_PID」の後に「|| exit 1」がついている! なんだこれは!?

つまり、pppd プロセスが動いていなければ exit してしまうので、 adsl-connect を終了させないではないか! 誰だ、こんな頭悪いコードを書いた奴は。

adsl-connect は pppd が失敗すると、 リトライするまえに 5秒 sleep する。 つまりこの 5秒間は pppd プロセスが無い。 だから、運悪く sleep 中に adsl-stop を実行すると、 adsl-connect が強制終了されずに残ってしまう。

速攻で adsl-stop から「|| exit 1」の部分を削除した。 これで安心 (だといいな)。



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

GCDバックアップ回線用のフレッツADSL では、 バックアップ回線ということで固定IPアドレスにはしていない。 どのくらい安定してつながっているか過去 10日間の統計を取ってみた。

毎分 ssh のテストログイン 14400回(=10*24*60)の結果:

エラー回数 112回
IPアドレスの切換回数 114回
IPアドレス平均持続時間 123.6分
持続時間の標準偏差(σ) 201.2分
持続時間の中央値 39分
最短持続時間 1分
最長持続時間 1385分

思ったより IPアドレスの切換が激しい。 一時間も持続しないIPアドレスが半数以上にのぼる。 10時間以上安定していたのは 4例だけ。



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

IPv6ブリッジ機能付きルーター (ブルータ) を使えば、 NAT 内の LAN でそのまま IPv6 を使えるわけで、 IPv6 の敷居は一気に下がる。

つまり、ほとんどの OS がすでに IPv6 をサポートしているので、 ルータが IPv6 を素通し (ブリッジ) しさえすれば、 LAN 内で IPv6 を意識せずに使える。

今まで通り IP を (NAT 経由したプライベートアドレスで) 使いながら、IPv6 のオイシイところ (end-to-end 通信ができる) だけを ツマミ食いできるわけで、 今度こそ (^^;) IPv6 が普及するのではないかと...

Linux でブルータを実現するには、 まずブリッジ機能をカーネルに組み込んでおく。

# brctl show br0
bridge name     bridge id               STP enabled     interfaces
br0             8000.00022Axxxxxx       no              eth0
                                                        eth1

このままだと IP までブリッジしてしまうので、 IP に対してはルータとして機能し、 IPv6 に対してだけブリッジさせるために、ebtables を使う。

# ebtables -t broute -L
Bridge table: broute

Bridge chain: BROUTING, entries: 3, policy: DROP      …… (3)
-j mark --set-mark 0x0 --mark-target CONTINUE         …… (0)
-p IPv6 -i eth0 -j mark --set-mark 0x600              …… (1)
-p IPv6 -i eth1 -j mark --set-mark 0x601              …… (2)

上記設定の意味は、

(0) まず全パケットに対し 0x0 のマークをつける。
    で、
(1) eth0 から入ってきた (LAN内から外へ出ていく)
    IPv6 パケットは、0x600 のマークをつけてブリッジする。
(2) 逆に eth1 から入ってきた (外から入ってきた)
    IPv6 パケットは、0x601 のマークをつけてブリッジする。
(3) それ以外 (IP パケットや PPPoE パケット) はブリッジしない (DROP)
    つまり IP に対してはルータとして機能する

さらに ip6tables を使ってフィルタリングする。

ip6tables -P FORWARD DROP                                       …… (*)
ip6tables -I FORWARD -j incoming -m mark --mark 0x601/0xffff    …… (2)
ip6tables -I FORWARD -j ACCEPT -m mark --mark 0x600/0xffff      …… (1)
(1) 0x600 のマークがついているパケットは、素通し (ACCEPT) する。
(2) 0x601 のマークがついているパケットは、incoming チェインへ渡す。

incoming チェインで ACCEPT されなかったパケットは、
(*) FORWARD チェインのポリシーに従って、DROP される。

ebtables でマーク付して ip6tables でそのマークに基づいて フィルタリングするという、 まわりくどい方法をとっているのは、 ebtables はあくまで L2 のフィルタなので IPv6 アドレスに基づいたフィルタリングができず、 逆に ip6tables は L3 のフィルタなので eth0/eth1 はまとめてブリッジインタフェース br0 としてしか見えず、 どちらの物理インタフェースから入ってきたパケットか 判別できないため。

ちなみに iptables だと、 L3 フィルタといいながら physdev モジュールがあるので、 ブリッジのどちら側から入ってきたか判別できたりする。 ip6tables の機能拡張に期待したいところ。



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

バックボーンには導入が進むものの、 一般ユーザには一向に普及する兆しのない IPv6。 可能性があるとすれば、安価なブルータの普及が鍵だろうと思う。

ADSL や FTTH によって安価にインターネットへ常時接続できるようになり、 また一家に複数の PC を持つことが普通になってルータも普及してきた。 このルータに、IPv6 ブリッジ機能が標準で搭載されるようになれば、 IPv6 への移行は格段に容易になるに違いない。 つまりユーザが意識しなくても NAT の内側の LAN 上の PC で IPv6 が使えるようになる。

実際、NTT東日本のフレッツドットネット対応を謳って、 IPv6 ブリッジ機能を持つ安価なルータが多数販売されている。

IPv6対応(フレッツドットネット対応=IPv6ブリッジ機能付き) ルータ一覧

IPv6 が IP に比べて優れた点があるとは思わないが、 IP の「やり直し」という意味はあるだろう。 IP アドレスの割当問題しかり、 無防備な PC の氾濫しかり。

IP が提案された当時、現在のような普及状況は誰も予測していなかったわけで、 今、やり直すことができるのなら、もっと効率的なアドレス割当ができるだろう。

IP スタックが OS に標準搭載されるのが一般的になり始めた頃、 現在のようなウィルス蔓延の可能性を、 アプリケーション開発者の大半が意識できていなかったわけで、 今、やり直すことができるのなら、IP 通信を受付けることの危険性を 認識した上での開発ができるだろう。



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

OS って何だろう。

言うまでもなく OS といえば、 コンピュータのリソースを管理するものであり、 またハードウェアの仕様の差を吸収して、 異なるハードウェア上で同一のプログラムを 走らせることができるようにするソフトウェアである。

ただ、OS といっても、 ほとんどカーネルと呼んだほうがいいような場合もあるし、 ミドルウェアの機能をまで含んで、 アプリケーションの操作性を統一するようなものまである。 さらには Windows における Internet Explorer のように、 従来はアプリケーションと思われていたものまで OS としてしまう場合すらある。

わたしが初めて使った OS (と呼べるかどうかは微妙だが) は、 CP/M だった。自作 PC (今風に言う自作 PC ではなく、 回路の設計から半田付けまですべて自分で作る PC である。 CPUなどはもちろん LSI を使うが、 ほかは汎用 TTL でロジックをくみ上げているので、 今風に言うチップセットは存在しない) 用の BIOS を スクラッチから開発して、市販されていた CP/M を移植した。

低機能な OS をハードウェアレベルから くみ上げるところから始めたということもあって、 OS というとわたしはカーネルに近いレベルを想定してしまう。一方、

システム管理コラム集 実践してみてわかったことシリーズその1

などを読むと、 あらためて人によって OS という言葉から思い浮かべるものが 異なるものだと実感する。 私にとっては Linux の各ディストリビューションごとの差異など あまり気にしなかったりするのだが、 最近の人にとっては、ディストリビューションが違うと、 異なる OS ととらえることもあるのだろう。

ちなみに、私の自宅サーバは、 10年以上前にインストールした Slackware をベースにしている。 ベースにしているといっても、 カーネルや libc をはじめとして ほとんどすべてのプログラムをアップデートしているので、 元のディストリビューションの痕跡は皆無といっていい。 自作ディストリビューションと呼べるかもしれない。 これを私が管理する全ての Linux マシンにコピーして使っている。

会社 (KLab(株)) のサーバは、私の直轄のチームが管理していて、 こちらはさすがに自作ディストリビューションというわけにはいかず、Debian をベースにしているが、 コアの部分は KLab 独自の拡張を加えている。 つまり誰が作っても同じようになる部分は 既存のディストリビューションを使うことによって「手を抜き」、 コアの部分は独自に開発して運用コストを下げたり、 耐故障性能を向上させたり、パフォーマンスを改善したりしている。



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

VRRP のマルチキャスト (VRRP Advert) を取りこぼすのは、 NIC が悪いのかも、 と思って別NIC を使用するように変更してみたのだが、 相変わらずスタンバイが勝手にアクティブに昇格したがる。

アクティブ側から見ると

Mar 22 12:58:36 asao Keepalived_vrrp: VRRP_Instance(VI)
                     Received lower prio advert, forcing new election
Mar 22 12:58:36 asao Keepalived_vrrp: VRRP_Instance(VI)
                     Sending gratuitous ARPs on eth1 for 192.168.0.254

スタンバイ側から見ると

Mar 22 12:58:30 senri Keepalived_vrrp: VRRP_Instance(VI)
                      Transition to MASTER STATE
Mar 22 12:58:31 senri Keepalived_vrrp: VRRP_Instance(VI)
                      Entering MASTER STATE
Mar 22 12:58:31 senri Keepalived_vrrp: VRRP_Instance(VI)
                      setting protocol VIPs.
Mar 22 12:58:31 senri Keepalived_vrrp: VRRP_Instance(VI)
                      Sending gratuitous ARPs on eth1 for 192.168.0.254
Mar 22 12:58:31 senri Keepalived_vrrp:
                      Netlink reflector reports IP 192.168.0.254 added

Mar 22 12:58:36 senri Keepalived_vrrp: VRRP_Instance(VI)
                      Received higher prio advert
Mar 22 12:58:36 senri Keepalived_vrrp: VRRP_Instance(VI)
                      Entering BACKUP STATE
Mar 22 12:58:36 senri Keepalived_vrrp: VRRP_Instance(VI)
                      removing protocol VIPs.
Mar 22 12:58:36 senri Keepalived_vrrp:
                      Netlink reflector reports IP 192.168.0.254 removed

tcpdump で確認すると、 VRRP Advert パケットはスタンバイ側に届いているのだが、 keepalived がそれを読めないらしい。 勝手にアクティブになるくせに、 5秒程度で Received higher prio advert して おとなしくスタンバイに戻る。

アクティブになったときに、 (自分がゲートウェイになったつもりで) 本当のゲートウェイへのルーティングを削除してしまうと、 外部への通信が途絶してしまうので、 PPPoE が成功したときのみルーティングを変更するようにした。 これで、勝手にアクティブになったとしても PPPoE が失敗する限りは 実質的に何も変わらないようになった。

keepalived を単なる死活確認にしか使っていない (しかも死んでないのに死んだと誤判断することあり) わけで、 モッタイナイお化けがでそうだ (^^;)。



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

sshによるバックアップ回線の監視を仕掛けて一日、 タイムアウト(監視プログラムには 10秒のalarmを仕掛けている) エラーが発生。

なぜかと思って調べてみると、 同一IPアドレスからのsshアクセスが多い、 という理由でブロックされてしまっていた (^^;)。 ssh攻撃が多い昨今、GCDのサーバには、 次のようなフィルタが仕掛けてある:

iptables -N block_ssh 2>/dev/null || iptables -F block_ssh
iptables -N ssh 2>/dev/null || iptables -F ssh
iptables -A block_ssh -j LOG --log-prefix block_ssh: \
	-m recent --name block_ssh --set
iptables -A block_ssh -j DROP
iptables -A ssh -j REJECT -p tcp --syn \
	-m recent --update --seconds 600 --name block_ssh
iptables -A ssh -j block_ssh -p tcp --syn \
	-m recent --rcheck --seconds 60 --hitcount 5 --name syn_ssh
iptables -A ssh -p tcp --syn \
	-m recent --set --name syn_ssh
iptables -A ssh -j ACCEPT

# この設定は http://d.hatena.ne.jp/hirose31/20050920/1127208718
# を参考にさせて頂きました (_O_)

つまり 1分間に 5回以上、同じIPアドレスから ssh接続があると、 10分間ブロックする。 監視元IPアドレスが、いったんブロックされると、 毎分接続を試みるわけだから、ずっとブロックされたままになる。 あわてて、監視元IPアドレスをブロック対象からはずした:

iptables -I ssh -j ACCEPT -s 60.32.85.216/29


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

GCD は、bフレッツでインターネットに接続している。 普通に PPPoE しているほか、 フレッツ・ドット・ネットを使っているので、 ネイティブ IPv6 でも外部から (といっても、フレッツ・ドット・ネットを使っているサイト限定だが) 接続できるので、 PPPoE がこけた (設定変更をミスったときなど) ときでも リモートから復旧させることができる。

普通はこれで十分なのだが、さらに念のためということで、 フレッツADSL でもインターネットへ接続して バックアップ回線としている。 bフレッツ側が全滅しても、 ADSL経由でログインできるようにしてある (もちろん、それぞれ別系統のハブに接続してあるし、 ログイン先のサーバは複数ある)。

このバックアップ回線は滅多に使わないので、 いつのまにか不通になっていても気づかないかもしれない、 ということで、さくっと (設定込みで 30分くらい) 監視プログラムを仕掛けた。 単に bフレッツ→インターネット→フレッツADSL 経由で ssh ログインするだけのプログラムを cron で定期的に走らせるだけだが、 不通になっていることに気づかない、という事態は避けられるだろう。



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

GCDの二台のゲートウェイのうちの片方 (senri.gcd.org) の 1000baseT NIC (eth0) が時々マルチキャストをとりこぼす。 二台のゲートウェイは、 VRRP で アクティブ・スタンバイ構成になっているのだが、 senri がスタンバイのときに、 もう片方 (asao.gcd.org) からの VRRP Advert をとりこぼしてしまい、 勝手に アクティブへ昇格しようとする。 一日〜数日に一度くらい、このとりこぼしは起きるようだ。 アクティブになっても、 OCN へ PPPoE しに行って失敗して (asao が PPPoE しているから) スタンバイへ戻るので、まあ実害はないのだが、 あまり気持ちのよいものではない。

eth0 で VRRP するのをやめて、 eth1 (100baseT NIC) で VRRP するようにしてみた。 eth1 は PPPoE (と、フレッツ・ドット・ネットのネイティブ IPv6) 専用で、 IPv4 アドレスは割り当てていないのだが、 マルチキャストなので VRRP Advert は受け取れるようだ。

keepalived.conf はこんな感じ:

vrrp_instance VI {
    state MASTER
    interface eth1
    garp_master_delay 10
    virtual_router_id 33
    priority 230
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass XXXX
    }
    virtual_ipaddress {
        192.168.0.254/24 dev eth1
    }
    preempt_delay 300
    notify "/etc/rc.d/vrrp_notify"
}

アクティブの時の asao:

asao.gcd.org:/ # ip addr show dev eth1
4: eth1: <BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast qlen 1000
  link/ether 00:a0:cc:xx:xx:xx brd ff:ff:ff:ff:ff:ff
  inet 192.168.0.254/24 scope global eth1
  inet6 2001:c90:1448:200a:2a0:ccff:fexx:xxxx/64 scope global dynamic
    valid_lft 2591983sec preferred_lft 604783sec
  inet6 fe80::2a0:ccff:fexx:xxxx/64 scope link
    valid_lft forever preferred_lft forever


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

セキュリティって、 分かっている人にとっては、 条件反射というか無意識のうちに意識してしまうというか、 あまりに自然な感覚だけに、 分かっていない人にどう説明するのがいいのか、とても難しい。



hiroaki_sengoku at 10:37|この記事のURLComments(0)TrackBack(0)