2010/03/27

clvm/gfs障害からの回復

前回はUbuntu 8.04 x86_64とDebian Lenny x86の組み合せでGFSを動かそうとしましたが、次のようなエラーがでたままでした。 しかしVMWare上では問題なく動き、前回は途中からVMWare上での様子をブログにまとめました。

/etc/init.d/cman start時のエラーメッセージ

Starting cluster manager:
 Loading kernel modules: done
 Mounting config filesystem: done
 Starting cluster configuration system: done
 Joining cluster:cman_tool: Cannot open connection to cman, is it running ?
 done
 Starting daemons: groupd fenced dlm_controld gfs_controld
 Joining fence domain:fence_tool: can't communicate with fenced -1
 done
 Starting Quorum Disk daemon: done

メッセージからも分かるように次のような状態でした。

  • aisexecは稼働中
  • ccsdは稼働中
  • /var/run/cman_admin, /var/run/cman_clientはnetstat -naで表示されず閉じている
  • fencedは未稼働

GFS周りの復旧手順をいろいろみていたのですが、fencedが起動せず、cman_toolも動かないので八方塞がりの状態でした。

そこでaisexecを停止してからcmanを起動したところ、クラスターメンバーは互いに通信できないものの、スタンドアローンな状態では起動し、cman_tool statusに反応があるようになりました。

Ubuntuではこんな手順。

$ sudo /etc/init.d/openais stop
$ sudo /etc/init.d/cman restart

debian lennyではこんな感じ。

$ sudo pkill aisexec
$ sudo /etc/init.d/cman restart

その結果のcman_tool statusの出力。

両方で/etc/init.d/cman startを実行した後のcman_tool statusの出力結果

Version: 6.1.0
Config Version: 4
Cluster Name: gwlogcl
Cluster Id: 13734
Cluster Member: Yes
Cluster Generation: 12604
Membership state: Cluster-Member
Nodes: 1
Expected votes: 1
Total votes: 1
Quorum: 1  
Active subsystems: 7
Flags: 2node Dirty 
Ports Bound: 0  
Node name: athlon
Node ID: 2
Multicast addresses: 239.192.53.219 
Node addresses: 192.168.1.210 

とりあえずこの状態で、cman_tool leave, cman_tool kill -n $(uname -n)を試してみる。 そこでcmanのリスタート。

"cman_tool leave"等はマニュアルにはサブシステム(clvm, gfs)が稼働している間は絶対に実行しないように書かれているので注意すること。

$ sudo /etc/init.d/cman restart

続いてaisexecの起動。 Ubuntuでは起動スクリプトを使います。

$ sudo /etc/init.d/openais start

Debianでは手動でaisexecを起動します。aisexecは自動的にforkするので、sudoに-bオプションは不要です。

$ sudo /usr/sbin/aisexec

この状態でcman_tool statusを実行した結果がこれ。

両ノードでのaisexec再起動後のcman_tool statusの出力

Version: 6.1.0
Config Version: 4
Cluster Name: gwlogcl
Cluster Id: 13734
Cluster Member: Yes
Cluster Generation: 12612
Membership state: Cluster-Member
Nodes: 2
Expected votes: 1
Total votes: 1
Quorum: 1  
Active subsystems: 7
Flags: 2node Dirty 
Ports Bound: 0  
Node name: athlon
Node ID: 2
Multicast addresses: 239.192.53.219 
Node addresses: 192.168.1.210 

大丈夫そうなので、両方のノードでclvmを起動してみます。

$ sudo /etc/init.d/clvm start

ここまで来ると問題なくgfs2_mkfsを実行する事ができて、同時に両方のノードからマウントすることができるようになります。

ポイントはaisexecを停止した状態から/etc/init.d/cmanを叩くところのようです。 これがどれくらい汎用性があるのかわかりませんが、とりあえずGFSを使うことができるようになりました。

とりあえず気は済んだのでGFSはこれでおしまい。 最終的にはUbuntu 10.04 LTSを待つか、beta2が出たくらいで移行してocfs2を使う事になると思います。

2010/03/26

OCFS2がだめでもGFSがあるじゃないか

さて、 前回はDRBDは順調だったものの、ocfs2の構成には失敗しました。そこでさっそくGFSを試してみようと思います。

しかし今回も作業の途中で回復の難しい状況に陥ってしまったため、VMWare上に環境を作って、その中で作業を行ないました。

ocfs2のアンインストール

DRBDの構成はそのままで良いので、/etc/fstabも編集していないですし、サービスを停止してパッケージを削除します。

$ sudo /etc/init.d/ocfs2 stop
$ sudo apt-get remove ocfs2-tools
$ sudo dpkg --purge ocfs2-tools
$ sudo apt-get autoremove

変更されている/etc/ocfs2/cluster.confは削除されませんが、後で使うかもしれないので、そのままにしておきました。

GFSの導入

Debian lennyへのパッケージ導入

ドキュメントに従ってdrbd.confを少しだけ変更します。

/etc/drbd.confファイルの修正したnetエントリ全体

...
  net {
    cram-hmac-alg sha1;
    shared-secret "FooFunFactory";
    allow-two-primaries; 
    after-sb-0pri discard-zero-changes;
    after-sb-1pri discard-secondary;
    after-sb-2pri disconnect;
  }
...

両方のサーバで変更したdrbd.confの内容を反映させます。

$ sudo /sbin/drbdadm adjust r0

/etc/init.d/drbdスクリプトを使うと次のようになり、 これは内部でdrbdadm adjust allを呼び出しています。

$ sudo /etc/init.d/drbd reload

DRBDのステータスを確認しておきます。

$ sudo /etc/init.d/drbd status
GIT-hash: dd7985327f146f33b86d4bff5ca8c94234ce840e build by root@vmgfsl02, 2010-03-24 00:43:08
m:res  cs         ro               ds                 p  mounted  fstype
0:r0   Connected  Primary/Primary  UpToDate/UpToDate  C

続いてgfs関連のパッケージを導入します。

$ sudo apt-get install gfs-tools clvm redhat-cluster-modules-2.6-686
Ubuntu 8.04にパッケージを導入する

drbd.confはdebianと同じように修正します。

$ sudo /sbin/drbdadm adjust r0

導入するパッケージはほとんど同じですが、カーネルモジュールの指定は不要なのでgfs-toolsとclvmだけです。

$ sudo apt-get install gfs-tools clvm

clvmの稼働

まずclvmdを起動する前に/etc/init.d/cmanスクリプトの起動に成功しなければなりません。 そのためには/etc/cluster/cluster.confファイルを配置します。 しかしディレクトリからして存在していないので、手動でファイルを配置します。

/etc/cluster/cluster.confファイルの配置
$ sudo mkdir /etc/cluster
$ sudo vi /etc/cluster/cluster.conf
<?xml version="1.0" encoding="UTF-8" ?>
<cluster name="gwlogcl" config_version="1">
 <dlm plock_ownership="1" plock_rate_limit="0"/>
  <cman two_node="1" expected_votes="1">
   </cman>
   <clusternodes>
     <clusternode name="vmgfsl01" votes="1" nodeid="1">
      <fence>
       <method name="single">
        <device name="human" ipaddr="10.0.0.2"/>
      </method>
     </fence>
    </clusternode>
    <clusternode name="vmgfsl02" votes="1" nodeid="2">
     <fence>
      <method name="single">
        <device name="human" ipaddr="10.0.0.3"/>
      </method>
     </fence>
   </clusternode>
  </clusternodes>
  <fence_devices>
  <fence_device name="human" agent="fence_manual"/> 
 </fence_devices>
</cluster>

これは samba.orgのドキュメントにあるcluster.confをコピーして、clusterのname属性を適当な名前に、clusternodeのname属性2個所を実際のホスト名に変更して、deviceのipaddrを対応するIPアドレスに変更したものです。

ファイルの準備が終れば、両方のノードでcmanコマンドを実行します。

$ sudo /etc/init.d/cman start

これが正常に起動すればclvmのプロセスをスタートします。

$ sudo /etc/init.d/clvm start

ここまでが無事に終るとGFSを使うために必要なクラスターの準備ができたことになります。

物理ボリューム(PV)の作成

既に/dev/drbd0は作成済みで同期が取れている状態なので、どちらかでPVを作成します。

$ sudo /sbin/pvcreate /dev/drbd0

ここで$ sudo pvdisplayを実行すると重複した定義を発見したというエラーと作成した領域が表示されるはずです。

Ubuntuでのlocking_typeの変更

/etc/lvm/lvm.confを修正し、locking_type = 3にする指示はwww.drbd.jpのドキュメントにありましたが、これはmanによればコンパイル時に"--with-cluster=internal"のオプション付きでないと意味がないようです。 そこでapt-get source lvm2でconfigureの引数を調べたところ"...=shared"となっていたので、locking_type = 2を試すことにしました。

/etc/lvm/lvm.confファイルのlocking_type = 2に変更した個所

--- lvm.conf.orig	2010-03-25 18:14:00.000000000 +0900
+++ lvm.conf	2010-03-25 18:14:28.000000000 +0900
@@ -235,7 +235,7 @@
     # if LVM2 commands get run concurrently).
     # Type 2 uses the external shared library locking_library.
     # Type 3 uses built-in clustered locking.
-    locking_type = 1
+    ##locking_type = 1
 
     # If using external locking (type 2) and initialisation fails,
     # with this set to 1 an attempt will be made to use the built-in
@@ -265,9 +265,9 @@
     # Enable these three for cluster LVM when clvmd is running.
     # Remember to remove the "locking_type = 1" above.
     #
-    #   locking_library = "liblvm2clusterlock.so"
-    #   locking_type = 2
-    #   library_dir = "/lib/lvm2"
+    locking_library = "liblvm2clusterlock.so"
+    locking_type = 2
+    library_dir = "/lib/lvm2"
 
     # The external locking library to load if locking_type is set to 2.
     #   locking_library = "liblvm2clusterlock.so"
Debianでのlocking_typeの変更

debianのlvm2パッケージを確認するとコンパイル時のオプションは"--with-cluster=none"となっています。 これではうまく使えないのでlvm2パッケージを再コンパイルします。

まずビルドに必要な関連パッケージを導入します。

$ sudo apt-get build-dep lvm2

次にパッケージのソースファイルを展開して修正します。

$ apt-get source lvm2
$ cd lvm2-2.02.39
$ vi debian/rules

debian/rulesファイルの変更点

--- debian/rules.orig	2010-03-26 13:04:42.000000000 +0900
+++ debian/rules	2010-03-26 12:51:56.000000000 +0900
@@ -62,6 +62,7 @@
 	./configure CFLAGS="$(CFLAGS)" \
 		$(CONFIGURE_FLAGS) \
 		--with-optimisation="" \
+		--with-cluster=shared \
 		--with-clvmd=cman \
 		--enable-readline
 	touch $@

ここでパッケージの再作成を行ないます。

$ dpkg-buildpackage -rfakeroot
$ cd ..
$ sudo dpkg -i clvm_2.02.39-7_amd64.deb lvm2_2.02.39-7_amd64.deb

パッケージを導入してから、Ubuntuの時と同じように/etc/lvm/lvm.confファイルを編集します。

/etc/lvm/lvm.confでのfilter行の修正

ここでlvm.confのfilter行を修正して、$ sudo pvdisplayコマンドが期待通りに動くようにしたいと思います。

ここではLMVに認識させるPVの名前を/dev/drdb0にします。 方法についてはdrbdのドキュメントの DRBDリソースを物理ボリュームとして構成するがまとまっているようです。

LVMをDRBD以外に使わないのであればドキュメントにあるように[ "a/drbd.*/", "r/.*/" ]のようなルールがシンプルになると思います。

他のPVとの競合を避けるのであれば、drbdを構成するデバイス名を"r/sdb.*/"のように書き、最後に... "a/drbd.*/", "a/.*/" ]を配置します。 この手前までにPVとして間違って認識された名前を外すように"r"で始まるルールを追加しました。

最終的に次のようなルールで落ち着きました。

Ubuntu用の/etc/lvm/lvm.confファイル

filter = [ "r/sdb.*/", "r!/dev/disk/.*!", "a/drbd.*/", "a/.*/" ]

もしdebian lennyであれば、さらに真ん中に"r!/dev/block/.*!"ぐらいのルール追加が必要になるはずです。

Node addresses: 127.0.1.1の修正

"clvmd: cluster is not running. Aborting."というメッセージが表示されてしまい、clvmが起動しなかったのですが、/etc/init.d/clvmスクリプトをみると"cman_tool status"を実行した戻り値(Return Code == RC)が0でないからのようです。

両方のノードで$ sudo cman_tool statusを実行すると奇妙な事がわかりました。

192.168.1.210側での出力例

Version: 6.1.0
Config Version: 3
Cluster Name: gwlogcl
Cluster Id: 13734
Cluster Member: Yes
Cluster Generation: 0
Membership state: Cluster-Member
Nodes: 1
Expected votes: 1
Total votes: 1
Quorum: 1  
Active subsystems: 7
Flags: 2node Dirty 
Ports Bound: 0  
Node name: vmgfsl02
Node ID: 2
Multicast addresses: 239.192.53.219 
Node addresses: 127.0.1.1

この中にある"Nodes: 1"は他のノードを見つけられていないからですが、最後のNode addresses: 127.0.1.1は他のノードからアクセスできないアドレスなので、どうもこれが関係していそうです。

Debian系ではインストール時に入力したホスト名は127.0.1.1に紐付けられることが多いと思います。 /etc/hostsに記述がある事は覚えていたので、ここを修正しました。

/etc/hostsファイル

127.0.0.1 localhost
10.0.0.3 vmgfsl02

ここで両方のノードでcmanを再起動します。

$ sudo /etc/init.d/cman restart

両方のノードが無事に起動して、念のため$ sudo cman_tool statusでNodes: 2に増えていることを確認。 続いて両方のノードでclvmを起動します。

$ sudo /etc/init.d/clvm restart

ファイルシステムの作成

いよいよgfsを作成しますが、まだPVだけしか作成していませんでしたが、気にせずgfsを作成します。 これは片側だけで実行することにします。

$ sudo gfs2_mkfs -t gwlogcl:drbd0 -p lock_dlm -j 2 /dev/drbd0

とりあえず、VMWare上では無事に動いたようです。 そのため設定ファイルがとりあえず動くものであることは分かりました。

VMWare上ではUbuntuとDebian lenny間で共有ファイルシステムが動いたものの

しかし当初のubuntuとdebian lennyの組み合せで試していた方はcmanが起動しない、おかしな状況になってしまい修復が難しい状況になってしまいました。

現在のエラーメッセージ

Starting cluster manager:
 Loading kernel modules: done
 Mounting config filesystem: done
 Starting cluster configuration system: done
 Joining cluster:cman_tool: Cannot open connection to cman, is it running ?
 done
 Starting daemons: groupd fenced dlm_controld gfs_controld
 Joining fence domain:fence_tool: can't communicate with fenced -1
 done
 Starting Quorum Disk daemon: done

一度はclvmdまでちゃんと動いて、両方からpvが認識できたんですけどね。 どういうタイミングかよくわからないなまま、パッケージを入れ直しても戻らないようになってしまいました。

いずれにしてもgfs/clvmはquorumの考え方があるので基本的には起動時にクラスタメンバー全体が稼働しているのが前提だと思います。 いろいろ考慮するとocfs2がお手軽に思えます。

今回の目的はログファイルを別のマシンからも取り出せるようにしたいという事なので、read-onlyでマウントできればそれでも良いんですよね。 GFSはNFSやらSambaやらの常に全ノードが稼働してクラスタによる冗長性が必要なストレージバックエンドとして使うのが良いんだろうと思います。

この目的なら単純にext3でフォーマットして必要な時だけ$ sudo mount -o ro ...でマウントすれば良いんだと思いますけどね。

2010/03/24

Ubuntu 8.04 LTSでDRBD+OCFS2を試してみる

いろいろ管理しているサーバーのログを手元のワークステーションにコピーする仕組みを考えている中で、DRBDを試す事にしました。

これはDRBDがベストだろうという目論見ではなく、keepalivedやLVSを試していた時の流れで、いつか試そうと考えていたからです。

そこでメインマシンのUbuntu 8.04 LTSとalixのDebian lennyの間で、DRBDを試す事にしました。 ファイルシステムレベルでの同期を取るために、ocfs2を試しています。

最終的にはocfs2のバージョンの問題で同期がうまくとれない、ということがわかりました。 そのためこの構成ではブロックデバイスレベルの同期に問題はありませんが、ファイルシテムを共有するという目的については、最終的に失敗しました。

Ubuntu 8.04 LTSへのDRBDの導入

適当なパッケージがないなぁと思っていたところ 技評のUbuntu関連の記事の中に、DRBDが9.10からサポートされるようになり、8.04 LTSを含む 各バージョン用のパッケージがPPAに準備されている事を知りました。

さっそく sources.list.d/drbd.listファイルを作成しました。

/etc/apt/sources.list.d/drbd.listファイル

deb http://ppa.launchpad.net/ubuntu-ha/drbd/ubuntu hardy main 
deb-src http://ppa.launchpad.net/ubuntu-ha/drbd/ubuntu hardy main
apt-keyスクリプトの使い方

これをベースに導入しようとapt-getを使ったのですが、エラーが表示されてしまいました。

$ sudo apt-get update

表示されたメッセージ

...
取得:18 http://dl.google.com stable Release [2540B]
取得:19 http://dl.google.com stable/non-free Packages [1025B]
178kB を 2min4s で取得しました (1433B/s)
パッケージリストを読み込んでいます... 完了
W: GPG error: http://ppa.launchpad.net hardy Release: 公開鍵を利用できないため、以下の署名は検証できませんでした: NO_PUBKEY 8F63FC3CB64F0AFA
W: これらの問題を解決するためには apt-get update を実行する必要があるかもしれません

もちろん"apt-get update"で問題が発生したので、実行する必要があるのはapt-keyコマンドです。 とりあえず keyserver.ubuntu.com から該当するキーファイルを取ってくることにしました。

$ sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 8F63FC3CB64F0AFA

コマンド実行時のメッセージ

Executing: gpg --ignore-time-conflict --no-options --no-default-keyring --secret-keyring /etc/apt/secring.gpg --trustdb-name /etc/apt/trustdb.gpg --keyring /etc/apt/trusted.gpg --keyserver keyserver.ubuntu.com --recv-keys 8F63FC3CB64F0AFA
gpg: 鍵B64F0AFAをhkpからサーバーkeyserver.ubuntu.comに要求
gpg: 鍵B64F0AFA: 公開鍵“Launchpad ubuntu-ha”を読み込みました
gpg: 処理数の合計: 1
gpg:               読込み: 1  (RSA: 1)

結果としては問題なかったのですが、時々keyserver.ubuntu.comでキーファイルがみつからない場合がある事です。 また90年代のpgpの頃からキーサーバといえば、MITのサーバですが、最近の動向を調べてみるとキーサーバ間の同期が取れていない状況が続いているようです。

OpenPGPキーサーバについて

OpenPGPはRFC4880としてまとまっているスタンダードを指します。 使っているgnupg(gpg)コマンドはOpenPGPの実装という関係にあります。

日本語で読める情報は OpenPKSDのサイトがまとまっているようです。少し古い情報で止まっているようですけどね。 先ほどのキーサーバ間の情報の食い違いなどは、「次世代 OpenPGP Public Keyserver (OpenPKSD) 平成14年度報告」の中でOpenPKSDが必要な背景の一つとして記述があります。

例えば先ほどの鍵ファイル"8F63FC3CB64F0AFA"は、pgp.mit.eduには登録されていますが、openpksd.orgなど他のサーバには、だいたいないようです。

DRBDパッケージの導入続き

途中で止まってしまったapt-get updateを再度、実行します。 続けて、drbd8-utilsパッケージと推奨パッケージに含まれていたdebconf-utils、共有FSを構築するためにocfs2-toolsを導入します。

$ sudo apt-get update
$ sudo apt-get install drbd8-utils debconf-utils ocfs2-tools

alixのdebian lenny側でも同様に、ただ"drbd8-utils"と"ocfs2-tools"をインストールすることにします。

$ sudo apt-get install drbd8-utils ocfs2-tools

Ubuntuでは必要なかったのですが、カーネルモジュールが自動的に入らなかったので、現在使っているカーネル(-686)に応じたモジュールも導入します。 どういうモジュールが存在するのかは、$ apt-cache search drbd8-modulesで確認できます。

$ sudo apt-get install drbd8-modules-2.6-686
ファイルシステムのセットアップ

パッケージは入ったので、これを動かしていきます。 そもそもの目的はalix側のログをWSなUbuntu側にも保存したいということでした。 とはいえ、DRBDはブロックデバイス(/dev/sd*, /dev/hd*, etc...)のミラーリングツールなので、デバイスを準備しないといけません。

WS側はLVM2が動いているので、ブロックデバイスを増やす事は問題なさそうです。 alix側のCFカードは単一のパーティションですから、USBメモリを追加することにしました。

まずはUbuntu 8.04側にLVMを準備します。 VGはtmpvgでLV名は任意ですが、ゲートウェイにしているサーバのloglvという意味で"gwloglv.drbd"としました。

$ sudo /sbin/lvcreate -L 4GB -n gwloglv.drbd tmpvg

コマンド成功時のメッセージ

  Logical volume "gwloglv.drbd" created

次はFATでフォーマットされているUSBメモリをalixに差して、/dev/sdb1として認識されているところからの作業です。 パーティションIDを変更する必要はなさそうですが、まぎらわしいので83に変更しておきます。

sudo /sbin/fdisk /dev/sdb
Command (m for help): p

Disk /dev/sdb: 4009 MB, 4009754624 bytes
32 heads, 32 sectors/track, 7648 cylinders
Units = cylinders of 1024 * 512 = 524288 bytes
Disk identifier: 0x55ccabed

   Device Boot      Start         End      Blocks   Id  System
/dev/sdb1               1        7648     3915760    c  W95 FAT32 (LBA)

Command (m for help): t
Selected partition 1
Hex code (type L to list codes): 83
Changed system type of partition 1 to 83 (Linux)

Command (m for help): p

Disk /dev/sdb: 4009 MB, 4009754624 bytes
32 heads, 32 sectors/track, 7648 cylinders
Units = cylinders of 1024 * 512 = 524288 bytes
Disk identifier: 0x55ccabed

   Device Boot      Start         End      Blocks   Id  System
/dev/sdb1               1        7648     3915760   83  Linux

Command (m for help): w
The partition table has been altered!

Calling ioctl() to re-read partition table.

WARNING: If you have created or modified any DOS 6.x
partitions, please see the fdisk manual page for additional
information.
Syncing disks.

DRBDを動かす

drbd.confファイルの作成

とりあえず/etc/drbd.confファイルを作成することにします。 デフォルトで配置されているファイルの中身は、ちょっと修正するには大変なボリュームなので名前を変えておきます。

$ sudo mv /etc/drbd.conf /etc/drbd.conf.original

またファイルを作成する前に両方のマシンでuname -nコマンドを実行して、ホスト名を確認しておきます。

192.168.1.1でのuname -n実行結果

gw

192.168.1.210でのuname -n実行結果

athlon

この結果を踏まえると、drbd.confファイルは次のようになります。

/etc/drbd.confファイル

global {
    dialog-refresh 0;
}

common {
  syncer {
    rate 10M;
  }
}

resource r0 {
  protocol C;

  startup { 
    become-primary-on both;
  }

  net {
    cram-hmac-alg sha1;
    shared-secret "FooFunFactory";
    allow-two-primaries; 
  }

  on gw {
    device     /dev/drbd0;
    disk       "/dev/disk/by-id/usb-SanDisk_Cruzer_Colors+_01180b20332200445284-0:0-part1";
    address    192.168.1.1:7791;
    meta-disk  internal;
  }
  on athlon {
    device    /dev/drbd0;
    disk      /dev/disk/by-id/dm-name-tmpvg-gwloglv.drbd;
    address   192.168.1.210:7791;
    meta-disk internal;
  }
}
ブロックデバイスの初期化

両方のマシンに/etc/drbd.confファイルを配置してから、次のコマンドをそれぞれ実行します。

$ sudo /sbin/drbdadm create-md r0
カーネルモジュールの起動

両方のサーバで、drbdプロセスを起動します。

/etc/init.d/drbd start

ここで次のようなメッセージが表示されます。

0: State change failed: (-2) Refusing to be Primary without at least one UpToDate disk
Command '/sbin/drbdsetup 0 primary' terminated with exit code 17

初期設定が出きていないだけのようなので、両方でdrbdプロセスを起動してから次のコマンドを実行します。

$ sudo /sbin/drbdadm -- -o primary r0

また最初に/etc/init.d/drbdを起動した側では、内部で/sbin/drbdadm wait-con-intコマンドが実行された段階で" To abort waiting enter 'yes' [ 10]:"というプロンプトが表示されます。 反対側のノードが起動するまで待ちますか、というメッセージですが、相手方がすぐに起動しないのであれば'yes'を入力します。

ここで気になったのは、本来はサーバのバックグラウンドで動くはずのスクリプトがユーザの入力待ちで止まるという違和感です。 深夜にマシントラブルでサーバファーム全体がダウン、その後片系だけ復旧したもののサーバがユーザの入力待ちで起動しない、そんなシナリオはあり得ないことではありません。

このスクリプトは大丈夫なのかなぁ。 まだ構成が終っていないから、どちらもPrimaryではないけれど、ちゃんと動くのかなぁ…。 ソースコードをみるとno_ttyな場合は/dev/consoleにattachするようになっていて、この場合はどうやってもユーザに入力させたいみたいです。

まぁ先に進めてから考える事にしましょう。 ここでステータスを確認しておきます。

$ sudo /etc/init.d/drbd status
drbd driver loaded OK; device status:
version: 8.3.2 (api:88/proto:86-90)
GIT-hash: dd7985327f146f33b86d4bff5ca8c94234ce840e build by root@athlon, 2010-03-24 00:43:08
m:res  cs         ro               ds                 p  mounted  fstype
0:r0   Connected  Primary/Primary  UpToDate/UpToDate  C

両方がプライマリで最新であるような状態を示す"Primary/Primary UpToDate/UpToDate"のメッセージを確認しておきます。

ocfs2を動かす

いよいよocfs2の操作に向います。

まずはプロセスの起動まで

まずは設定ファイルを両方のマシンで同じものを準備します。 UbuntuではDebian lenny側にあった/etc/ocfs2ディレクトリが準備されていなかったので、手動で作成しました。

/etc/ocfs2/cluster.confファイル

node:
        name = gw
        cluster = ocfs2
        number = 0
        ip_address = 192.168.1.1
        ip_port = 7891

node:
        name = athlon
        cluster = ocfs2
        number = 1
        ip_address = 192.168.1.210
        ip_port = 7891

cluster:
        name = ocfs2
        node_count = 2

ファイルを配置してからプロセスを起動します。 まずデフォルトでは無効にされているので、/etc/default/o2cbファイルを編集します。 これをしないと/etc/init.d/o2cbスクリプトが仕事をしてくれません。

/etc/default/o2cbファイルの変更個所

...
O2CB_ENABLED=true
...

これでスクリプトを実行すると、メッセージが出力されます。

$ sudo /etc/init.d/o2cb start

起動に成功した時のメッセージ


Loading filesystem "configfs": OK
Mounting configfs filesystem at /sys/kernel/config: OK
Loading stack plugin "o2cb": OK
Loading filesystem "ocfs2_dlmfs": OK
Creating directory '/dlm': OK
Mounting ocfs2_dlmfs filesystem at /dlm: OK
Setting cluster stack "o2cb": OK
Starting O2CB cluster ocfs2: OK
ocfs2ファイルシステムの作成

これはext3ファイルシステムを作る場合などとオプションが違うだけで、だいたい同じような操作になります。 まずはパラメータを考えずにデフォルトの状態で作成します。

既に/dev/drbd0は同期されているため、どちらか一方で次のようなコマンドを実行します。

sudo /sbin/mkfs.ocfs2 /dev/drbd0
いよいよファイルシステムのマウント

いろいろアプリケーションやらサービスやらが存在する環境になってくると、どうやって管理しようかディレクトリ構造から悩む事になります。 今回はログファイルを配置する場所を /app/logs/$(uname -n)/ocfs2 としました。

そこで両方のマシンで次のようなコマンドを実行します。

$ sudo mkdir -p /app/logs/$(uname -n)/ocfs2
$ sudo mount /dev/drbd0 /app/logs/$(uname -n)/ocfs2

ここで最終的にはエラーになってしまいました。

出力されたエラーメッセージ

mount.ocfs2: Transport endpoint is not connected while mounting /dev/drbd0 on /app/logs/athlon/ocfs2. Check 'dmesg' for more information on this error.

dmesgで確認したエラーメッセージ

[845863.471758] (10794,0):o2net_check_handshake:1197 node athlon (num 1) at 192.168.1.210:7891 advertised net protocol version 8 but 11 is required, disconnecting

どうやら原因を調べると、32bit版と64bit版のocfs2では 同期を取るためのプロトコルのバージョンが違うことによるものとのこと。

一次情報がないけど本当なのかな…。いやいやそんなはずはない。 カーネルのコードをみれば 原因はバージョンの違いでしょう。 定数"O2NET_PROTOCOL_VERSION"は32bit, 64bitとは関係ないところで定義されていますからね。 カーネルのバージョンを上げないと、どうしようもなさそうです。

それでも同時にマウントしなければ、交互にマウントはできてファイルができていることは確認できました。

負荷をかけたりはしていませんが、とりあえずocfs2が簡単に導入できることは分かりました。 ブロックデバイスレベルでの同期と、ファイルシステムレベルでの同期を考えなければいけない事がわかったのは収穫かな。

構成をもう一度考え直さなければ…。 他のalixマシンを中心に32bit版の共有ファイルシステムサーバを作るのがいいのかなぁ…。

2010/03/21

もしLED電球の価格が毎年5%づつ下がったら…

一番最初に書いておくべきだと思うのですが、この手の「〜にするべきか」という話題は観点によって答えが変化します。 世の中の流行という精神的圧力やLED電球・電球型蛍光灯の製造・環境コストなどは考慮していないので、こういう考え方もできるという視点で読んでください。

家電量販店ではLED電球を売り出しています。 広告をみると10年間や5年間ベースで、買い替える必要もなく経済的だと宣伝しています。

ここに引っ越して2年近く経ちますが、機会のある度に電球型蛍光灯に切り替えて、古い白熱電球が残っているのは1,2個所でしょうか。それもほとんど使っていないところだけです。

そこで次はLED電球なのかなと思ったのですが、いろいろ調べてみると私のようなアパート住まいには、この流行りに乗るのはまだ早いのかもしれないと考るようになりました。

ランニングコストを試算するための仮定

広告などを参考に価格、電気代や寿命について、次のような想定をしました。

  • 白熱電球
    • 本体価格:100 (円/1個)
    • 電気代:20 (円/日)
    • 寿命:1,000 (時間)
  • 電球型蛍光灯
    • 本体価格:1,000 (円/1個)
    • 電気代:4 (円/日)
    • 寿命:6,000 (時間)
  • LED電球
    • 本体価格:4,000 (円/1個)
    • 電気代:2 (円/日)
    • 寿命:40,000 (時間)

電気代を詳しくみてみると、1日で12時間使った場合と24時間付けっ放しで使った場合の1年間の電気代は次のようになります。

  • 1年間の電気代 - 12時間
    • 白熱電球:3600円
    • 電球型蛍光灯:720円
    • LED電球:360円
  • 1年間の電気代 - 24時間
    • 白熱電球:7200円
    • 電球型蛍光灯:1440円
    • LED電球:720円

この差をどう評価するのか難しいですが、電球の値段でみると、少なくとも白熱電球から電球型蛍光灯には変えた方がお得なようです。

24時間使い続ける場合でも電気代だけをみてLED電球を選択するのは難しそうです。 とりあえず5年後ぐらいのLED電球の価格を想定して、グラフを描いてみました。

もし毎年LED電球の値段が5%づつ下落したら…

ランニングコストが安くても、4000円もするLED電球の投資を回収する期間はどれくらいになるのでしょう。

それにまだ生産が始まったばかりの製品ですから、年々コストは下がっていくはずです。 もっと後になってから購入した方がお得かもしれません。

1日に12時間使う場合

仮に1日に12時間使った場合のランニングコストをグラフにしてみました。 これには寿命が経過した製品を新規に購入するコストも含まれています。

白熱電球や電球型蛍光灯の価格変化はなく、LED電球は毎年5%ずつ価格が下がります。 とはいえ長寿命の製品ですから9年後になって、やっと約2,500円で更新することになります。

1日12時間使った場合のランニングコスト

「5年後」の線は、いまは電球型蛍光灯を使って5年後に価格の下がったLED電球に乗り換えたと想定した場合のグラフです。毎年、価格が5%づつ下がる仮定なので、5年後のLED電球の価格は3,095円になります。 コストへの反映は翌年の6年後ですが、電球型蛍光灯を更新するよりも経済的になるのは3年後です。

5年後に、さらに3年後を見越してLED電球に投資するかどうか、判断できるでしょうか…。

もし24時間使う場合

付けっ放しであれば、ランニングコストも上がりますが、回転が早まるため投資を回収する期間は短くなります。 こういう場合は早めにLED電球に切り替えた方がお得でしょう。

1日24時間使った場合のランニングコスト

これぐらいなら、まぁLED電球でも良いかもしれません。 もっと値段が下がると思えば、1,2年は様子をみても差はあまりなさそうです。

1日に6時間使う場合

しかし1日6時間使用した場合にはどうなるでしょう。

1日6時間使った場合のランニングコスト

だんだんとLED電球と蛍光灯との差が狭くなってきました。6時間程度であれば10年以上使用しても差額は2,000円程度です。

後でまとめで述べるような不慮の事故を考慮するとLED電球に投資するのは危険かもしれません。

1日に2時間使う場合

これを1日2時間程度使った場合にすると、次のようになります。

1日2時間使った場合のランニングコスト

ここまで使用頻度が低いと、LED電球にする意味はないですね…。 しかし特筆するべきは白熱電球の圧倒的なコストパフォーマンスの悪さです。

まとめ

グラフから判断できたことは、使用時間が短くても電球型蛍光灯にするのがベストな選択といえそうなこと。 それはとにかく白熱電球のパフォーマンスが悪すぎることに起因しています。

白熱電球の単価は安いですが、次に切れた時には少なくとも電球型蛍光灯に変えた方がいいでしょう。

それにケースバイケースですが、電球型蛍光灯を途中で新品に交換する事になったとしても、少なくとも2,3回交換しても、元は十分に取れそうだということです。

それ以上に壊すのであれば白熱電球を使い続ける理由になりそうですが、おそらく別の何かを見直した方がよいのではないでしょうか。

考慮点

長時間 and 長期間使う場合、LED電球に切り替える事でコストを削減することができそうです。 しかしアパート住まいで長期間は住まないとか、色を変えたいとったニーズの変化も考慮に入れる必要があるでしょう。

もし途中で一回でもLED電球を新品に交換してしまうと、電球型蛍光灯を時々壊しながら更新し続けた方が経済的になってしまいます。

子供が何かをぶつけて壊してしまうかもしれませんし、長期使用できるか自信がないなら、とりあえず電球型蛍光灯にするのが良さそうです。 一軒家でなければまだLED電球は時期が早いのかもしれません。

2010/03/19

Anakia:#set()の挙動について

このブログ記事や最近作成したIronPythonについての文書はAnakiaで生成するようにしています。 最近ではJSPとの比較でVelocityが語られる事はむしろ少なくなってきたのかもしれませんが、テンプレート言語として習得の容易さやスピードの面で優れていると思います。

ただ、しばらく使ってみてAnakiaで作成する原稿からは事前に設定された、主にXML文書のリソース、にアクセスできるだけで柔軟性に欠ける部分があると感じるようになりました。 つまり動的にマクロの内部から任意のXMLファイルへ、あるいはネットワーク越しにRSSなどのXML形式の外部データソースにアクセスする事はできません。

これはシンプルにするための割り切りなのかもしれませんが、プロセス上のフレームワークの作り手とドキュメントを作成するライターの役割分担を行なうことを意識しているようにも思えます。

いずれにしても本当に不満であれば、AnakiaTask.java辺りを書き換えれば簡単に拡張できそうです。 例えば$dateで参照可能なjava.util.Dateオブジェクトは次のようなコードで実装されています。

org/apache/anakia/AnakiaTask.javaより抜粋

...
context.put ("date", new java.util.Date() );
...

本題:Velocity #set()マクロの挙動について

さてさて、本題ですが、*.vslマクロファイルを書いている時に気がついたコーディングパターンがあります。 それは#setマクロでgetAttributeValue()メソッドを使った時に変数が初期化されないという点です。 結局のところ次のように空文字列で初期化をする1行が必要になりました。

最終的に作成したマクロ

#macro (find_nextpage $value)##
  #set($name = "")
  #set($name = $root.getChild("next").getAttributeValue("name"))##
  ...

このgetAttributeValue()メソッドを呼び出す前に$name変数を空文字列で初期化しているところが問題です。 こうしないと該当するAttributeが存在しない場合に、前のループでセットした$nameの値がそのまま維持されてしまうところです。 $nameの値はnullでもなく変化しません。

velocity.propertiesの設定でvelocimacro.context.localscope=falseとしているために、全ての変数が大域変数と同じ扱いになるため以前の呼び出しで設定された$nameの値を覚えているのは、しかたのないことです。 しかし代入しているのに、左辺値が変化しないのはちょっと感覚とずれているように感じます。

左辺値が変化しないのはgetAttributeValue()に限らず、#set()マクロの基本的な挙動のようです。 コードをざっとみたところleftReferenceがcontextから削除されているので、未定義なのかと思いきや以前の値が復活しているという少し謎な動きです。

getAttributeValue()を少し追ってみる

私がコーディングしたコードではgetChildren("name")のようなメソッドは、getName()で事前にタグが"name"だと判定しているので戻り値があると分かっていて呼ぶ場合がほとんどでした。 その反面getAttributeValue()はオプション扱いのattributeの存在を確認するために呼ぶ場合がほとんどだったため、戻り値が空文字列になると仮定して書いたコードは、とってもバギーになってしまいました。

そこでgetAttributeValue()周りのコードをシンプルにできないか少しコードを追ってみました。

もともとgetAttributeValue()メソッドはAnakiaが準備しているものではなく、 org.jdom.Elementで定義されているjdom由来のメソッドです。 この処理は次のように定義されています。

引数が1つの場合(org/jdom/Element.javaより抜粋)

    public String getAttributeValue(final String name) {
        return getAttributeValue(name, Namespace.NO_NAMESPACE);
    }

さらにNamespaceが指定されている場合に呼ばれるメソッドは次のようになります。

引数が2つでNamespaceを取る場合(org/jdom/Element.javaより抜粋)

    public String getAttributeValue(final String name, final Namespace ns) {
        return getAttributeValue(name, ns, null);
    }

最終的には次のように該当するattributeがない場合には、defに該当するnullが返されます。

該当するattributeがない場合に第3引数が返される

    public String getAttributeValue(final String name, final Namespace ns, final String def) {
        final Attribute attribute = (Attribute) attributes.get(name, ns);
        if (attribute == null) {
            return def;
        }

        return attribute.getValue();
    }

しかし.getAttributeValue("name", "", "")をようなマクロを作成しても呼ばれるのはGetAttributeValue("name")のままで、defにはnullが入ってしまうため回避する方法はなさそうです。

別のメソッドを呼び出す方法がないか考えましたが、良い方法は思い付きません。 最初の例を別の方法で書き直すなら、次のような方法でしょうか。

別の解決策

  #if($root.getChild("next").getAttributeValue("name"))##
    #set($name = $root.getChild("next").getAttributeValue("name"))##
  #else##
    #set($name = "")##
  #end##

いずれにしても良い方法ではないので、繰り返し呼ぶマクロ内部では変数の初期化とgetAttributeValue()の呼び出しは対で必要そうです。

2010/03/12

blueprint cssとIE8の組み合せで気づいたこと

問題点について

IronPythonについて、いろいろまとめた文書を作成したのでWebサイトにアップロードしつつ確認していました。 そこでLinuxやMacOS XのFirefoxやChrome, Safariで問題のなかったページが、IE8でみた時には画面が真っ白になって何も表示されない現象に遭遇しました。

互換モードにしたところ、文字化けしつつも画面に文字が表示されるようになりました。その後、少しずつファイルを編集していきBlueprint CSSとの関連で行なった設定が原因だと分かりました。

原因と対応

文字化けするページ正常に表示されるページの差分は次の通りです。

--- index.html	2010-03-12 20:24:02.000000000 +0900
+++ index_ie8.html	2010-03-12 20:21:19.000000000 +0900
@@ -18,7 +18,7 @@
   <link rel="stylesheet" href="/css/blueprint/print.css" type="text/css" media="print" />
   <!--[if lt IE 8]>
     <link rel="stylesheet" href="css/blueprint/ie.css" type="text/css" media="screen, projection" />
-  <![endif]-->
+  <![endif] -->
   <link rel="stylesheet" href="/css/default.css" type="text/css" media="screen, projection" />

違いは空白一つ分だったわけですが、これが問題となるべきなのかどうかは微妙です。 この部分はConditional Commentsと呼ばれる記法で、W3Cが作成しているCSSやXHTML、HTMLの仕様ではなく、Microsoft IE5以上が独自に定義している機能だからです。

HTMLの規約からみればコメント文の内側に独自の命令が追記されているようにみえて、その観点からは前後に空白があっても問題がなさそうです。

しかしConditional Comments側からみれば、<![endif]-->という固定文字列の命令を追加したのであって、あいだに空白が入るなど想定外だという見方もできます。 まず先にあったコメント文のルールと自らが独自に実装させたい機能の整合性を取ったはずなんですけどね。

まとめ

いずれにしてもWebブラウザはソースコードに問題があっても最大限の情報を描画できるように作るべきで、標準的なルールに従っているコードを描画できないのはバグと呼ばれてもしかたがないと思います。

ただこの記述はIE8未満のブラウザへの対応のために必要なもので、もはやMSもIE8への乗り換えを推進している状況では問題になった行を削除してIE8未満では見れないサイトになってもしょうがないのかもしれないと思っています。 まぁIE8未満へのケアのためで将来的には不要になるから含めていますが、これがIE8以上対応のワークアラウンドだったら叫んでいたでしょうね。

2010/03/07

hostapdをdaemontoolsで管理する

いままでプロトタイプ状態で動かしていたhostapdですが、ちゃんと安定して動かすためのサーバにmini-PCIカード(XG-601)の引っ越しをしました。 IPv6やらNIC周りの設定は/etc/network/interfacesのpost-up辺りに書けば良いんですけどね。 稼働し続けるサーバプロセスを動かすための仕組みは、一度起動して終りというわけにはいかないのが難しいところです。

アクセスのたびに起動すれば良い場合にはinetdやxinetdが使えるのですが、今回はずっと起動し続けるdaemonなのでrc.local辺りで一回起動して終りになってしまいそうです。 ALIXに入れたdebian lennyには、いずれも準備されていなかったのでdjbさん作成のdaemontoolsを入れてみました。

「DAEMON Tools」というとWindows用のISOイメージのマウントツールが検索でかなりヒットしますが、qmailで有名なdjbのツールの方が思い浮ぶのは歳を取り過ぎたのでしょうか。

daemontoolsを選んだ理由

hostapdは常に起動し続けるタイプのサーバプロセスなので、inetdやxinetdでは対応する事ができません。 inetdやxinetdはアクセスの都度プロセスを起動して処理が終ったら消えていくプロセスだけで、あまり一般的ではないですがsendmailやhttpdに対応できる応用範囲の広い仕組みです。

他にinetd, xinetで対応できないサーバプロセスは、OSとして必須のものを除くとfreeradius、ntpdぐらいでしょうか。 freeradiusやntpdはパッケージが起動スクリプトを/etc/rc.d/に配置してくれるので便利ですが、監視はされていないので異常停止した場合には手動で起動するまではサービスが提供できない時間帯(outage)が発生することになります。

気になるのであとで、freeradiusもdaemontoolsの

daemontoolsの情報源

本家のdaemontoolsページがまとまっていて、日本語だと本家の翻訳などはありますが、実用的な解説サイトはざっとみたところ見つかりませんでした。 かといってここで包括的な説明をしていくのは難しいので、hostapd + debian lennyを対象にしていきます。

hostapdをdaemontoolsで動かしていく

daemontoolsの導入

とりあえずdaemontoolsを導入します。 daemontools-runは/etc/inittabなども編集して、導入直後から管理プロセスを起動してくれます。

$ sudo apt-get install daemontools daemontools-run
別ディレクトリへの設定ファイルの配置

daemontoolsは導入した直後から/etc/serviceディレクトリをトップディレクトリとして既に稼働しています。 まずは/usr/local/serviceディレクトリを作成して、その中にhostapdディレクトリとrunという名前の起動用スクリプトを配置します。

$ sudo mkdir -p /usr/local/service/hostapd
$ sudo vi /usr/local/service/hostapd/run
$ sudo chmod +x /usr/local/service/hostapd/run

runファイル内容

#!/bin/bash

exec /usr/local/sbin/hostapd /usr/local/etc/hostapd.conf
/etc/serviceディレクトリへの配置

準備が終ったら、daemontoolsが監視している/etc/servicesディレクトリにシンボリックリンクを貼る事でhostapdを起動させます。 もし事前にhostapdが起動しているようであれば、killコマンドなどで停止させてください。

$ sudo ln -s /usr/local/service/hostapd /etc/service

ファイルを作成すると、5秒以内にhostapdプロセスが起動するはずです。 ひと呼吸置いてからプロセスが起動しているか確認しましょう。

$ ps auxww| egrep 'supervise|hostapd'

コマンド実行後の出力結果

root      1798  0.0  0.1   1492   276 ?        S    12:48   0:00 readproctitle service errors: ...ed to set Short Slot Time option in kernel driver?Could not set preamble for kernel driver?Failed to remove interface (ifidx=6).??starting?Configuration file: /usr/local/etc/hostapd.conf?Using interface wlan0 with hwaddr 00:60:b3:xx:xx:xx and ssid 'XXXXXXXX'?starting?Configuration file: /usr/local/etc/hostapd.conf?Using interface wlan0 with hwaddr 00:60:b3:xx:xx:xx and ssid 'XXXXXXXX'?starting?
root      2317  0.0  0.1   1504   328 ?        S    16:17   0:00 supervise hostapd
root      2895  0.0  0.6   4200  1764 ?        S    16:18   0:00 /usr/local/sbin/hostapd /usr/local/etc/hostapd.conf
...

/usr/local/serviceディレクトリを作成した理由は、次のセクションにまとめました。

稼働中のsuperviseプロセスを停止する際の注意点

最初は/etc/serviceディレクトリの直下にサブディレクトリを作成していたのですが、プロセスを停止する段階になって困ってしまいました。 本家のFAQには次のような記述があります。

http://cr.yp.to/daemontools/faq/create.html#remove

How do I remove a service? I want to eliminate /service/telnetd.
Answer:

     cd /service/telnetd
     rm /service/telnetd
     svc -dx . log

debian lennyでは"/service"は"/etc/service"に、"telnetd"は今回"hostapd"になるわけですが、直接"telnetd"(あるいは"hostapd")ディレクトリを作成しているとこの手順を実施する事はできません。 そしてどうやら代替手段もなく、手動でsuperviseプロセスをkillするしかなさそうです。

そんなわけで"/etc/service"直下はシンボリックリンクだけを集めるようにしましょう。

さいごに

djbさんが作成したツール群は非常に素晴しいのですが、作り手の個性が強烈に伝わってくる仕上りになっています。 良くも悪くも従来の概念の延長線上の斜め上ぐらいにあるので、使い手が、自身が対応できないために、使いづらく感じるのは事実だと思います。 まぁ機能が限定されすぎていて現状に合っていないとか、いろいろ全うな不満はあるとは思いますけどね。

それでも、しばらく前にコードの改変を許すようになったので、不満があるなら改造して使い易くするのは使い手の責任になっていますね。 うーんちょっとコードを変更するぐらいじゃだめだし、これと同等に安定した成果物を作れる自信はないです。

ちなみにqmailを題材にdjbさんが書かれた論文「Some thoughts on security after ten years of qmail 1.0」ではどのようにセキュアな(サーバ)アプリを作成するかが解説されています。

閑話休題:djbがパブリックドメインを宣言したというけれど

原文は本家のサイト「Placing documents into the public domain」にあります。

まぁ日本でパブリックドメインという概念は著作権上の財産権の放棄ぐらいでしか実現できなさそうだから、勝手にコードを取り込んだとしても名前を明記するなどの著作者人格権の尊重はしないといけないんだろうなぁ。

2010/03/06

セキュリティを考慮したOpenVPNの設定変更

ALIXのブロードバンドルータで動かしているOpenVPNについては、本家のHOWTOの中にセキュリティについての記述があります。

この中により強固なセキュリティを確保するために、暗号化アルゴリズムとしてAES-256-CBCも選択できるという記述がありました。 そこまでは必要ないとしても、よりセキュアな環境を作るために少し変更を加えていこうと思います。

テスト環境

今回は次のような環境で、図左の端末からPHS通信カード(AX420S)を使いOpenVPN経由で家のサーバに接続します。

OpenVPN Network Graph

tls-authを併用する

HOWTOには通常の認証に加えて追加の共通鍵を追加する事で,DoS攻撃への耐性を高めるなどいくつかの利点があると説明されています。 予防的なものですから、これを無神経に配ってもそれほど脅威ではないでしょうね。 低コストでそれなりの効果は期待できそうなので、導入することにします。

まずは有効にするためにサーバとクライアントで共有する鍵ファイルta.keyを生成します。

$ cd /etc/openvpn
$ sudo openvpn --genkey --secret ta.key

このファイルと同じものをクライアントとなるWindows XPにも転送します。 server.confやclient.confの先頭の方に次のように設定を加えていきます。

/etc/openvpn/server.confファイル

...
tls-server
tls-auth ta.key 0
...

C:\Program Files\OpenVPN\config\client.confファイル

...
tls-client
tls-auth ta.key 1
...

サーバ側をリスタート(/etc/init.d/openvpn restart)してから、クライアントから接続します。 Windows XP側の"View Log"でログを確認すると無事に認識されたようです。

client.logファイルから抜粋

...
Sat Mar 06 16:00:05 2010 WARNING: this configuration may cache passwords in memory -- use the auth-nocache option to prevent this
Sat Mar 06 16:00:05 2010 Control Channel Authentication: using 'ta.key' as a OpenVPN static key file
Sat Mar 06 16:00:05 2010 Outgoing Control Channel Authentication: Using 160 bit message hash 'SHA1' for HMAC authentication
...

udpの使用

TCPはパケットの再送などを上位レイヤで実現できるので、使う事ができる環境なら「UDPよりもTCP」という認識があるかもしれません。ただUDPはオーバヘッドが少ないですし、VPNの用途では望ましいのかもしれません。

HOWTOではproto udpを使う事でDoS攻撃に対してはUDPの方がより望ましい耐性を備えていると説明されています。

プロセス実行ユーザの変更

root以外のユーザでプロセスを実行する方法として、実行後に"nobody"などroot以外のユーザに権限を降格させる方法と、そもそもプロセスの起動自体をroot以外で行なう方法、最後にchroot環境が説明されています。

お勧め:実行後に権限を降格させる方法

よりお手軽な方法はapacheなどと同様にuser, group行でroot以外のユーザを指定する方法です。 当初から採用している方法はこれで、最低限これぐらいは必要でしょう。

Linux限定:最初からroot以外のユーザでプロセスを起動する方法

もう一方はプロセスの起動自体をroot以外のユーザにする方法です。 OpenVPNが使用するポートは特権ポートではないですから原理的には問題ないですが、 Linux環境のみで有効なこの方法は少しばかり複雑です。

セキュリティを高めるためにあまり複雑な方法を採用するのは、よくよく考慮する事が必要です。 この方法は悪くはないですし潜在的な脅威が高まるとは思えませんが、設定ファイルやTLS鍵ファイルの管理など、考慮するべき点は他にもあり、あまり報われない気がします。

プロセスがハイジャックされた場合を考えれば、より汎用的な次の方法がお勧めです。

chroot環境の利用

HOWTOで説明しているchroot環境は、プロセスが起動後に初期設定を終えてから特定のディレクトリに引きこもる方法です。 初期設定は終っているので、下記に挙げた例外を除けばserver.confファイルにchroot行を追加するだけです。

もしプロセスがハイジャックされても、そこから起動されたプログラムがアクセスできる範囲は、chrootで指定したディレクトリ以下に限定されるため、より安全になるはずです。

これもあまりにも簡単なのでuser, group行と合せてchrootも使うことにしました。

$ sudo mkdir /var/jail/openvpn

/etc/openvpn/server.confの最後に追加

...
## chroot after initialization
chroot /var/jail/openvpn

HOWTOでは動的に読み込まれる、crl-verify行に指定しているファイルとclient-config-dir行に指定しているディレクトリはchrootしたディレクトリをトップディレクトリとした場所に配置するよう書いていますが、これを使用していなければディレクトリを作成する以外の準備作業は不要です。

chrootに指定するディレクトリは任意なので/etc/openvpn以下にしたい場合もあると思いますが、openvpnプロセスがcoredumpした場合を考えると十分な余裕のあるファイルシステムにディレクトリに配置する事をお勧めします。

共通鍵は256bit程度、公開鍵は2048bitを使う

より長いサイズの鍵を使いましょうという話しがHOWTOにありますが特に説明は不要と思われます。

CAのプライベート鍵ファイルの管理

親CA、中間CAのプライベート鍵ファイル(demoCA/private/cakey.pem)の管理はサーバに配置しないようにして分離しておきましょう。 HOWTOにはフロッピーディスクなどに格納してネットワークアクセスを難しくするようアドバイスされています。 日本で無難な対応はMOやUSBメモリなどにdemoCAディレクトリ一式を配置して必要なタイミングでマウントするのが最善でしょう。

少し脱線して鍵の管理について…

まぁ個人レベルでそこまでする必要はないと思いますけどね。 少なくとも鍵ファイルを使うサーバ(Consumer)とdemoCAディレクトリ(Provider)を共存させるのはやめましょう。 企業でプライベートなCAを運用するのであれば、こういったメディアの管理に加えて部屋への物理的な入室制限も考えなければいけないところです。

まぁそれでも抜け道はいろいろ発生するんですよね。 より良い対応は手順を守らせるフレームワークの策定と、その手順を現実的に対応可能な範囲で四半期毎といった間隔で改訂していくことでしょう。

二人以上が共謀しないと漏洩しない仕組みは、割と低コストで実現可能な範囲に収まります。 物理的な鍵の管理と、論理的なパスワードを別々の人間が管理するだけで実現できますからね。 一般的な日本の会社だと責任の範囲だけ決めて、そういう仕組みが機能するようにはならないのかなぁ…。

さいごに

OpenVPNはお手軽にインターネット経由でVPNを実現可能にする仕組みで、RSA鍵の有効期限を短く設定するぐらいの考慮があれば比較的安全なインフラが構築できるでしょう。 問題はパフォーマンスですが

2010/03/05

MTU設定時の注意点

以前、 IPsecを試していた時にIPのフラグメントが原因と思われる通信障害が発生しました。 どうやらAlixで作成したブロードバンドルータのMTU設定がまずかったようです。

現状でも参考にした資料を全部読めていないのですが、いまの理解でまとめておこうと思います。

問題となった現象について

エラーメッセージ

VMWare上でTeamを組んで2つのサブネットをlinuxをルータにして接続してみました。 MTUをいろいろ変更している最中にsyslog経由で記録されたログは次のようになっています。

linuxでのメッセージ

...
Mar  3 10:05:40 lipsec01 kernel: [ 1778.358003] pmtu discovery on SA AH/09dd8188/0a000001
Mar  3 10:07:40 lipsec01 kernel: [ 1898.356663] pmtu discovery on SA AH/09dd8188/0a000001
...

netbsdでのメッセージ

...
wm0: discarding oversize frame (len=1502)
wm0: discarding oversize frame (len=1502)
...

どちらもMTUが適切に設定されていない事を示唆していますが、この時点ではまだその点にちゃんと気がついていませんでした。

設定を確認

実際に問題が発生したAlixブロードバンドルータを中心にしたネットワークは次のようになっていました。

Alixを中心にしたHomeLANの概要

192.168.1.0/24と192.168.10.0/24のサブネットのゲートウェイになるeth1, eth2のMTU値が1454になっていました。 MTU値1454はBフレッツで接続する際にNTTから指定されている値で、PPPデバイスに対しては正しいのですが、家の中で各サブネットに接続しているPCはMTU値1500が設定されているため、サブネット単位で不整合が発生しています。

対応策

サブネット単位でちゃんとMTUを揃えて対応は完了。

通常はこれで気になる事もなかったのですが、どういうわけかIPsecでAH+ESPヘッダを付与したところで顕著に影響が出てしまったようです。 IPsecはヘッダが付与される分パケットが長くなりがちですが、どうしてこれでIPsec以外の通信では問題にならなかったんだろう。

ああ、外部に出ていく時にはPPPのMTU値1454に合わせるから問題がなくて、家の中ではそもそも大きなファイルの転送をしなかったとかかな。いや、そんなことはない。

Path MTU Discoveryについて

今回の原因はMTUの不整合でした。 それとは別にMTU関連についていろいろ調べていく中で、alixを通した時にPath MTU(PMTU) Discovery(PMTUD)がちゃんと動いているのか確認する事にしまいた。

サブネット単位でMTU値を同じ値に設定した後に、ICMPパケットをtcpdumpを使って眺めてみました。 外部へ接続する時にはppp0に設定されているMTU値1454に合わせるよう、PMTUDが動いている様子がわかります。

$ sudo tcpdump -p -i eth0 icmp -vv -n

tcpdump出力ログ

...
21:26:29.868484 IP (tos 0xc0, ttl 64, id 18704, offset 0, flags [none], proto ICMP (1), length 576) 192.168.1.1 > 192.168.1.xxx: ICMP 210.xxx.yyy.zzz unreachable - need to frag (mtu 1454), length 556
	IP (tos 0x0, ttl 64, id 19526, offset 0, flags [DF], proto TCP (6), length 1499) 192.168.1.xxx.59612 > 210.xxx.yyy.zzz.80: P 3195263263:3195264710(1447) ack 810452849 win 90 <nop,nop,timestamp 89692924 674447780>[|icmp]
...

今回の反省

サブネット単位でMTUが揃っていなかったのは痛いミスでした。

今回の調査の中でPMTUDについても勉強しなおしてAlixの設定に反映させました。 まぁAlixでルータを作った時にはPMTUの事を考えずにICMPパケットを全て落していましたからね。

現在のiptables設定

現在ではいろいろな経緯で変更を加えて state モジュールをUDPとICMPにも適用した状態になっています。 またネットワーク内部を探査されないようにecho-requestとredirectについては明示的にDROPする設定にしています。

現在のiptables設定スクリプト

...
## [inppp] default rules
## - disallow some icmp inbound messages
iptables -A inppp -p icmp --icmp-type echo-request -j logdrop
iptables -A inppp -p icmp --icmp-type redirect -j logdrop
## - allow already used connections
iptables -A inppp -m state --state ESTABLISHED,RELATED -p tcp -j ACCEPT
iptables -A inppp -m state --state ESTABLISHED,RELATED -p udp -j logaccept
iptables -A inppp -m state --state ESTABLISHED,RELATED -p icmp -j logaccept

## [outppp] default rules
iptables -A outppp -m state --state ESTABLISHED,RELATED -p tcp -j ACCEPT
iptables -A outppp -m state --state ESTABLISHED,RELATED -p udp -j logaccept
iptables -A outppp -m state --state ESTABLISHED,RELATED -p icmp -j logaccept
iptables -A outppp -m state --state NEW -p tcp -j ACCEPT
iptables -A outppp -m state --state NEW -p udp -j logaccept
iptables -A outppp -m state --state NEW -p icmp -j logaccept
...

意図せず侵入を許したパケットは通過してしまうので、outboundに"ESTABLISHED,RELATED"を設定するのは少し問題なのかもしれません。 外部からのSSH接続などinboundを追加するタイミングで個別にoutboundを設定するよう変更するかな。

icmpは到達不可をちゃんと伝えるには必要そうだけど利便性の問題でたぶん必須ではないのだと思います。

2010/03/01

NetBSD 5.0.1によるIPsec環境の構築

VMWare Workstation 6.5を使って、NetBSD 5.0.1同士でTeamを作成して、2つのVMの間でIPsec環境を構築することにしました。

これはDebian lennyで同じことをした時にMTUを片側で少し減らした場合、PMTUが発生し、端末がハングしたようにみえる現象がNetBSDと連携した時にどうなるか確認したかったからです。

とりあえずNetBSD 5.0.1同士でのIPsec環境を構築したので、その時の差分の手順をメモしておきます。

はじめに

作業手順は大きく分けて次のような流れになっています。

ipsec.confファイルはipsec-tools.confと読み替えれば、手順もファイルの内容もLinuxの時と同じものがそのまま使えます。 そのため今回はNetBSD固有の部分で、特に気になった設定周りについてだけメモをまとめていきます。

NetBSDカーネルの再ビルド

5.0.1ではIPSECが標準で有効になっていないため、これを有効にしたカーネルを作成します。 手順自体は通常のようにGENERICファイルをコピーして編集しますが、その際に次のオプションを有効にしてください。

名前を変更したGENERICファイル編集個所

...
options IPSEC
options IPSEC_ESP
...
IPsec用のNICの追加

VMWare Workstationの機能でネットワークセグメントを一つ追加しています。 今回はwm1に対して"10.0.0."1と"10.0.0.2"のIPアドレスを割り当て、その2点間でIPsecが有効になるように設定を行ないました。

システムの設定

NetBSD固有の設定は/etc/rc.confに集中しています。

/etc/rc.confファイルの追加内容

sshd=YES
ipsec=YES
racoon=YES

念のためにipsec.confとraccon.confファイルの内容もコピーしておきます。

server2側の/etc/ipsec.confファイル

#!/usr/sbin/setkey -f
#
flush;
spdflush;

## out rule (local -> remote)
## IPv4
spdadd 10.0.0.2 10.0.0.1 any -P out ipsec
           esp/transport//require
           ah/transport//require;

## in rule (remote -> local)
## IPv4
spdadd 10.0.0.1 10.0.0.2 any -P in ipsec
           esp/transport//require
           ah/transport//require;

ipsec.confファイルはもう片側では、IPアドレスを入れ替えています。 実際には"out", "in"を入れ替えた方が楽でしょう。

server2側の/etc/racoon/racoon.confファイル

path certificate "/etc/racoon/certs";

remote 10.0.0.1 {
        exchange_mode main;
        my_identifier asn1dn;
        peers_identifier asn1dn;

        certificate_type x509 "newcert.pem" "newkey.pem";
        peers_certfile x509 "server1.newcert.pem";
        verify_cert on;
        proposal {
                encryption_algorithm aes;
                hash_algorithm sha1;
                authentication_method rsasig;
                dh_group modp1024;
        }
        generate_policy on;
        passive off;
}

sainfo anonymous {
        pfs_group modp768;
        encryption_algorithm rijndael, 3des;
        authentication_algorithm hmac_sha1, hmac_md5;
        compression_algorithm deflate;
}

racoon.confファイルは、もう片側では10.0.0.1を10.0.0.2に、server1.netcert.pemはserver2.newcert.pemに変更しています。

さいごに

システムを再起動すると2つのIPアドレスの間では無事にIPsecによる通信が始まりました。 最初はracoonを有効にせずに再起動し、後からracoonを起動したせいか、linuxの時と同じようにphase2でtimeoutが発生しました。

当初発生したエラー抜粋

Mar  1 13:21:00 server2 racoon: ERROR: phase1 negotiation failed due to time up. e60ee11ca211d3b2:0000000000000000 
Mar  1 13:21:03 server2 racoon: ERROR: phase2 negotiation failed due to time up waiting for phase1. ESP 10.0.0.1[500]->10.0.0.2[500] 

やはり同じように両方で"racoonの停止"→"setkeyのクリア"を行なった後に、"racoonの起動"、"setkey -f ipsec.confによる設定の反映"を行なうと無事に通信が始まりました。

やはりMTUをいじってもちゃんと動きますね。まぁ当然か。 Linuxの新しいkernelでは直ってるのかなぁ…。