2014年2月4日

ルートファイルシステムはいじってはいけない

数年前から稼働している我が家のバックアップ専用サーバは2TBのHDDを6台使っている。パーティションは三つのRAIDデバイスで
  • md0 - RAID1 /boot
  • md1 - RAID5 //varを含むLVM
  • md2 - RAID6 /var/backup バックアップデータ専用
と言う構成。このバックアップ専用サーバ構築の前に使っていたHDD×4のRAID5(メインのファイルシステム)+HDD×2のJBOD(バックアップ専用)という構成のメインの単一の共通サーバのメインのRAID5のHDD2台が同時に死に、辛うじてバックアップ専用のJBODから数十年分の家族の写真ファイルなどを救出できたという経験からバックアップの重要性を思い知らされたので作った。一日24時間、週7日間我が家の全てのPCとサーバのデータをバックアップしている。

最初は、このサーバは4台のHDDで作成され、後にHDDを2台追加した。md0はHDD×4のRAID1からHDD×6のRAID1に、md2はHDD×4のRAID5からRAID6に拡張されたが、md1はHDD×4のRAID5+スペアHDD×2と言う構成のままだった。
 
本業の仕事でRAIDの内部を扱うことになり、何となく我が家のバックアップサーバを眺めて、md1がRAID5のままのことに気がつき、深く考えもせずに残りの2台を組み込んだRAID6に拡張したほうが良いような気がして作業を始めてしまったが、これが大きな誤りだったことに後で気がつく。今回はこの顛末記。

何が誤りだったかと言うと
  • 不注意: mdadmの--growオプションを--backup-file=無しで走らせた。
  • そもそもの誤り: ルートファイルシステムはみだりに変更してはいけない。
と言うこと。

まず、--backup-file=について。

RAIDの構成を変更する(RAIDのレベルを上げたりHDDを増やす)ときは、もしクリティカルセクション実行時に異常が起きても回復できるように--backup-file=を使って回復のためのデータを保持しておくべきなのだが、これを怠った。そしてマーフィの法則が示すように、構成変更作業中に停電が起きた。もちろんサーバはUPSを使っていたが、停電は数時間に及び、電源の回復前にシャットダウンしてしまった。ちなみに、当地はシリコンバレーと言う土地柄にも関わらず、日本では考えられないぐらい停電が多い。例年、3~4回の停電はざらだ。多くは嵐で立ち木が倒れて電線を切断するためだが、今回の停電は穏やかな日の夜明け前に起こり、原因は何だだったのか知らない。

サーバを立ち上げようとしたが、「ルートファイルシステムが見つからない」と言うエラーでフリーズしてしまう。冷や汗が流れてくるが、幸いなことに本機はバックアップサーバだから、1週間ぐらい死んでいても実用上問題はない。と言うことで、落ち着き、缶ビールを開けて修復作業を始める。

FedoraのインスタレーションCDを使ってレスキューモードで回復を図るが、何度やってもmd1のクリティカルデータが壊れているのでリシェープできないと言う。クリティカルセクションのバックアップファイルを作っておかなかった報いだ。

仕方がないので回復は諦め、md1のパーティションを作り直すことにする。幸いなことに、我が家にはもう一台バックアップ専用のアプライアンスがあり、こちらにこのバックアップサーバのバックアップデータ以外はすべて定期的にバックアップしてある(つまり我が家のシステムは別の部屋に設置した別のハードウェアに二重にバックアップしている)から、ファイルシステムを作り直してバックアップから回復すれば良いだろう。

と、希望的に考え、粛々と回復作業を行うが、一向に回復しない。ここで行った試行錯誤から、徐々に失敗の理由が明らかになってきた。すなわち、
ルートファイルシステムは他のファイルシステムと違い、ブートローダと密接にリンクしている
と言うことだ。

だから、単にファイルシステムを回復しただけではブートローダに認識されない。ルートファイルシステムそのもの(このサーバではルートファイルシステムを含むLVMが存在するRAID)とブートローダのポインタがちゃんと整合していなければならないのだ。

さて、実際の回復の道のりを思い出すだけ書いてみる。

対象のホストは「shadow」と言う名前で、問題のルートファイルシステム(オリジナル)は
  • LVM上の/dev/mapper/vg_shadow-LogVol00
  • LVMは4台のドライブからなるRAID5の/dev/md1
  • RAID5の構成物理ドライブは
    /dev/sda2
    /dev/sdb2/dev/sdc2/dev/sdd2(アクティブ)
    /dev/sde2
    /dev/sdf2(スペア)
これをRAID6に変換しようと言うスケベ心を出したのが間違いだったので、元通りのRAID5を復活させる。 方針として、各種設定ファイルはオリジナルのままとして、RAIDやLVM、FSなどをオリジナルの設定ファイルに整合するように再構築する。

整合してなければならないことが確認できたのは以下の識別子。 

/boot/grub/grub.conf内のブートメニュー項目のデバイス名とUUID。
shadow# cat /boot/grub/grub.conf
...
title Fedora (2.6.34.9-69.fc13.i686.PAE)
        root (hd5,0)
        kernel /vmlinuz-2.6.34.9-69.fc13.i686.PAE ro root=/dev/mapper/vg_shadow-LogVol00 rd_MD_UUID=cc8c918f:5b720e12:5c2a14a8:2e918d19 rd_LVM_LV=vg_shadow/LogVol00 rd_NO_LUKS rd_NO_DM LANG=en_US.UTF-8 SYSFONT=latarcyrheb-sun16 KEYTABLE=us rhgb quiet
        initrd /initramfs-2.6.34.9-69.fc13.i686.PAE.img

...


●LVMを構築する時に使う/etc/lvm/archive/vg_shadow_00000.vgの物理ボリューム(PV)のUUID。
shadow# cat /etc/lvm/archive/vg_shadow_00000.vg
...
         physical_volumes {

                pv0 {
                        id = "rLojRP-Ms7b-VKlA-p7Cj-n1nL-0MAy-nLy4Ty"
                        device = "/dev/md1"     # Hint only

...

●実際に構築されたRAIDデバイスのRAIDレベル、メタデータバージョン、ディスク台数、UUID。
shadow# mdadm -Esv
ARRAY /dev/md/0 level=raid1 metadata=1.0 num-devices=6 UUID=ec0da04a:e6ae42d2:bc3eb03c:7373ea18 name=localhost.localdomain:0
   devices=/dev/sdd1,/dev/sdb1,/dev/sdf1,/dev/sde1,/dev/sdc1,/dev/sda1
ARRAY /dev/md/1 level=raid5 metadata=1.1 num-devices=4 UUID=cc8c918f:5b720e12:5c2a14a8:2e918d19 name=localhost.localdomain:1
   spares=2   devices=/dev/sdd2,/dev/sdb2,/dev/sdf2,/dev/sde2,/dev/sdc2,/dev/sda2
ARRAY /dev/md/2 level=raid6 metadata=1.2 num-devices=6 UUID=74c85cf7:e8d75a3f:6daa34d5:db1226e7 name=shadow.arkusa.com:2
   devices=/dev/sdd3,/dev/sdb3,/dev/sdf3,/dev/sde3,/dev/sdc3,/dev/sda3
shadow# blkid /dev/md1
/dev/md1: UUID="rLojRP-Ms7b-VKlA-p7Cj-n1nL-0MAy-nLy4Ty"

●そして最後にINITRAMFS内の/etc/mdadm.confのRAIDレベル、ディスク台数、UUID
shadow# mkdir /tmp/initrd
shadow# cd /tmp/initrd
shadow# cp /boot/initramfs-2.6.34.9-69.fc13.i686.PAE.img .
shadow# gunzip -c initramfs-2.6.34.9-69.fc13.i686.PAE.img | cpio -i
shadow# cat etc/mdadm.conf
ARRAY /dev/md1 level=raid5 num-devices=4 UUID=cc8c918f:5b720e12:5c2a14a8:2e918d19


ファイルシステム、あるいはそのファイルシステムが存在するLVMやRAIDを認識する識別子には以下の3種類がある。
  1. デバイスファイル名(例: /dev/md1
  2. ラベル(例: LABEL=/
  3. UUID
下に行くほどモダンで曖昧さが薄れる。

驚くべきことに、UUIDには少なくとも3種類の表現方法があるらしい。
  1. UUID="67947017-6b60-1dbe-8457-1880d35f9d0e" 「blkid」の出力など
  2. UUID="67947017:6b601dbe:84571880:d35f9d0e" 「mdadm -Esv」の出力など
  3. UUID="nGR8l1-6jaI-SNbg-ILSp-Oh8o-53bM-oXm45a" LVMの「blkid」の出力、/etc/lvm/archive/xxxx.vgなど
上記の三つは、同じオブジェクトのUUIDで、全て同じ値を示す。1と2はハイフンとコロンの桁区切りの違いだけのようだが、3はBase64風で何だかよく分からない。

なお、Universally Unique IDと言っても、RAIDの構成メンバの個々のUUIDとRAIDそのもののUUIDは全て同じ。例えば、/dev/md0/dev/sda1/dev/sdb1/dev/sdc1/dev/sdd1から成り立っていると、これらの5台のオブジェクトのUUIDは全て共通。

システムをFedoraのCDのレスキューモードで起動。ネットワークを使えるようにしておくと便利。レスキューするファイルシステムはマウントしないようにする。

まずRAIDを再構築。RAID5以上の作成には--createオプションを使う。以前にRAIDに使われていたディスクを使うときはシグネチャを検査して上書きの警告をしてくるが、「OK」を選択する。
shadow# mdadm --create /dev/md1 --level=5 --raid-devices=4 /dev/sda2 /dev/sdb2 /dev/sdc2 /dev/sdd2

このままではランダムなUUIDが与えられてしまう。
shadow# mdadm -Esv
ARRAY /dev/md1 level=raid5 num-devices=4 UUID=86f7ac38:284c3bb5:bfe78010:bc810f04

他の正常なRAIDデバイス名が「/dev/md/N」の形で表示されるのに「/dev/md1」であり、「metadata」の表示もない。
そこでちょっとトリックを使う。
一旦RAIDを停止し--metadataオプションを追加して作成し直し。
shadow# mdadm --stop /dev/md1
shadow# mdadm --create /dev/md1 --metadata=1.1 --level=5 --raid-devices=4 /dev/sda2 /dev/sdb2 /dev/sdc2 /dev/sdd2

もう一度RAIDを停止してUUIDをアップデートするオプションでアセンブルし直し。
shadow# mdadm --stop /dev/md1
shadow# mdadm --assemble /dev/md1 --update=uuid --uuid=cc8c918f:5b720e12:5c2a14a8:2e918d19 /dev/sda2 /dev/sdb2 /dev/sdc2 /dev/sdd2
shadow# mdadm -Esv

ARRAY /dev/md/1 level=raid5 metadata=1.1 num-devices=4 UUID=cc8c918f:5b720e12:5c2a14a8:2e918d19
    devices=/dev/sda2,/dev/sdd2,/dev/sdc2,/dev/sdb2 
shadow# blkid /dev/md1
/dev/md1: UUID="rLojRP-Ms7b-VKlA-p7Cj-n1nL-0MAy-nLy4Ty"

めでたくRAIDが正しいUUIDで復活した。

mdadmの--addオプションを使って、残りの2台のディスクをスペアとして参加させておく。スペアがあれば、アクティブなディスクのどれかが死んだときにすぐに代替ディスクとして組み込まれ、リシンクが実行される。リシンク完了までに他のアクティブディスクが死ななければデータは完璧に保存される。実は、RAID6はこのリシンクの作業をリアルタイムに分散して実行していると見ることもできる。
shadow# mdadm /dev/md1--add /dev/sde2 /dev/sdf2


次に、LVMの設定ファイルを使ってLVMを復活させる。1番目のvgrestoreでレイアウトを復活させ,2番目のvgchangeでLVMを起動する。
shadow# lvm vgcfgrestore --file vg_shadow_00000.vg vg_shadow
shadow# lvm vgchange -a y vg_shadow
  Restored volume group vg_shadow
  2 logical volume(s) in volume group "vg_shadow" now active
blkid /dev/md1
/dev/md1: UUID="rLojRP-Ms7b-VKlA-p7Cj-n1nL-0MAy-nLy4Ty" TYPE="LVM2_member"

このLVM設定ファイルにはルート(/)と/varの二つのLVが記述されていたので、それぞれが作成される。UUIDはランダムな値が設定されるが、これはどことも整合させる必要がないのでそのまま。
shadow# ls -l /dev/mapper
total 0
crw-rw----. 1 root root  10, 62 2014-02-04 07:41 control
brw-rw----. 1 root disk 253,  1 2014-02-04 08:27 vg_shadow-LogVol00
brw-rw----. 1 root disk 253,  0 2014-02-04 08:27 vg_shadow-LogVol01_var

ここまできたらほとんどできたも同然。それぞれのLVにmkfsでファイルシステムを作り、バックアップからファイルをリストアして、レスキューCDの/mnt/sysimageに仮マウントできることを確認したらリブート。

まあ、人間必ずチョンボがあるので、一発で完全回復はできないかもしれないが、完璧なバックアップさえあれば順序よく誤りを潰していけば必ず復活できる。

ちなみに小生が犯した最後のエラーは、バックアップに/var/runを含めていなかったこと。この頃のFedora13のディストロでは、/var/run内にサブディレクトリを作るデーモンのいくつかは、サブディレクトリをランタイムに作成せずインスタレーション時に作成することを仮定していたので、それらのデーモンが機動できなかった。これは、他のFedora13の/var/runをそっくりコピーして完治。バックアップファイルセットから/var/runの除外を消すことも実施して完璧な修復ができた。

教訓をもう一度。

ルートファイルシステムはブートローダと密接な関係があるので決して構成を変えてはいけない。

延べ3日の作業は高い代償だったが、RAIDとLVMの修復の経験を積んだので価値ありとする。

2014年1月7日

Raspberry Piにリアルタイムクロックをインストールする

Raspberry Pi(以下RPi)は、コストを抑えるためか、電池でバックアップされたリアルタイムクロックを実装していない。このため、インターネットに接続されていない、正確にはNTPサーバにアクセスできない環境では、時計が例の1970年の元旦にリセットしてしまい、具合が悪い。前回のドライブレコーダの記事に使おうとしているRPiは「時々」インターネットに繋がる可能性があるだけなので、何とかしなければならない。
 
 
Amazonで検索すると、うってつけの製品が見つかった。SunFounder DS3231という小さなPCボードで、I2CでRPi(やArduino)のI/Oポートに直接装着できる。単価$6という価格も嬉しい。さっそく買ってみた。
 
RPiでの使用方法はこちらのサイトに解説されており(右の製品写真もこのサイトから拝借した)、それに従えばちゃんと動くが、少々読みにくいので以下に読み下し解説を再掲載してみる。

ハードウェアの実装はとても簡単。写真のようにI/Oポートに向きを正しく差し込んでお終い。

ソフトウェアは、以下の二つのファイルを変更し、
  • /etc/init.d/hwclock.sh
  • /etc/modprobe.d/raspi-blacklist.con
以下のファイルを追加する。
  • /etc/cron.hourly/sync-hwclock
いちいちエディタで修正するのは面倒なので、パッチファイルを作ったので使ってください。
 
/tmp/ds3231にパッチファイルをダウンロードして、patchを実行。





root@pi:~# cd /
root@pi:/# patch --dry-run -p0 < /tmp/ds3231.patch
 問題のないことを確認してね
root@pi:/# patch -p0 < /tmp/ds3231.patch 
実は、最後の追加ファイル(/etc/cron.hourly/sync-hwclock)は原典には含まれていない。定期的にntptraceを実行して、NTPが生きているならリアルタイムクロックをNTPに同期させるごく簡単なスクリプト。
 

で、OSの設定を変更

ファイルにパッチを当てただけではまだ動かない。以下の手順を実行してOSの設定を変更しなければならない。 

まず、パッチを当てたhwclock.shをイネーブルしてリブート。
root@pi:~# update-rc.d hwclock.sh enable
root@pi:~# reboot

この段階で、/dev/rtc0という新しいデバイスファイルが出来上がっていることを確認。
確認できたら、今一度タイムゾーンの設定を確認。
root@pi:~# dpkg-reconfigure tzdata
 
もうリアルタイムクロックが使えるので、NTPで同期してある現在の時刻をリアルタイムクロックに書き込んで、もともとのダミーのリアルタイムクロックをディセーブルすると出来上がり。
root@pi:~# heclock -w
root@pi:~# update-rc.d fake-hwclock disable
root@pi:~# reboot

原典では以下も実行してNTPを完全に殺しているが、時々でもインターネットに繋がる可能性があるのなら、NTPで時刻を常に同期させる方がよいのでここでは実行しない。もしNTPを完全に殺すなら、上記で作成したcronファイルは削除してください。
root@pi:~# update-rc.d ntp disable
  
リアルタイムクロックの動作確認は、本当はネットワークを外してリブートしてOSの時計がちゃんと設定されていることを確認すべきだが、ハードウェアクロックの動作確認だけなら以下で十分。
root@pi:~# hwclock --show


簡単でしたね。








2013年12月2日

Raspberry PiにVFATをマウントする - いろいろ問題が…

Raspberry Pi(RPi)を車載して、低価格(~$50)のドライブレコーダ(車載カメラ+ストレージ)に記録された画像データを帰宅後ガラージュに停車したら自動的にWiFiで我が家のサーバに転送するプロジェクトを進めている。

セキュアなWiFiの接続(確実に自宅サーバだけに接続する)だのカメラのモード(撮影と転送)の切り替えだのカメラの電源のコントロールだのいろいろ問題はあるが、まずはRPiに接続したカメラのデータをWiFiでサーバに転送できるところから実装し始めたが、大まかには動作し始めたものの、カメラをRPiに接続するところからRPi特有と思われる問題いろいろが見つかった。

使用したドライブレコーダ

参考のために、使用したカメラ(ドライブレコーダ)と使用している動作モードを紹介する。
  • モデル: BlackView(当然のことながら中国製)
  • 解像度: 1280x720 30fps
  • 記録: MJPEG(motion JPEG) 約1GB/10min.
  • メディア: 32GB micro SD(最大5時間記録)
  • 電源・転送: USB2.0
  • GPS機能なし(よって時刻合わせは手動)
USBでPCに接続すると撮影モードからデータ転送モードになり、PCからはマスストレージに見える。最初に見つかった問題は、USBのデータ転送が約900MBを超えると必ずコケること。RPiだけでなくUbubtu、Win7などでも同じ症状を見せたのでこのカメラ特有の問題と思われる。これはファイルあたりの記録時間を5分(5、10、15分から選択、5分≒500MB)に制限することで回避。

ストレージクラスデバイスをマウントする

RPiの標準インスタレーションではマスストレージクラスのデバイスしか認識しない。iPhoneやディジタルカメラはイメージングクラスデバイスなので、USBレベルでは認識されても「/dev/sdX」としては認識されない。LinuxでこれらのイメージングデバイスをマウントするにはGVFSを使うが、ここで使ったカメラはストレージクラスなので取り敢えず問題なし。
 
ストレージクラスのカメラは、例えば「/dev/sda1」というブロックデバイスとしてして現れるので単純にマウントできるが、そのままでは自動的にはマウントされない。自動的にマウントするにはいろいろ方法があるようだが、成功したのはusbmountを使う方法。automountではできなかった。usbmountは/dev/sdXNデバイスを自動的に/media/usbMにマウントする(例えば/dev/sda1 on /media/usb0)が、N(パーティション番号)はともかく、XとMは再現性が保証されないので不確定性が残るが、マウント時に起動されるスクリプトが/var/run/usbmount/の下にデバイスのベンダとモデルから生成されたシンボリックリンク(この場合は「Syntek_USB_MSDC_1」)がマウントされたデバイスのルートを指すように作成するので、こちらでアクセスする限り不確定性は実用上問題ない。
 

タイムゾーンが違う!!

最悪にして最大の(同じこと!?)の問題は、RPiのカーネルに含まれるVFATFSがメディアのタイムスタンプをUTCと解釈してしまうこと。おかげでファイルの日付がタイムゾーンの分だけずれてしまう。VFATFSはtz=UTCのマウントオプションを受け付けるはずだが、実際には機能していないようだ。他にもいろいろやってみたが全て不発。複数のUSBマスストレージデバイスをRPiにマウントして同じ現象が発生し、他のLinux(例えばUbuntu)では発生しないので、RPiカーネル固有の問題だと思う。以下のカーネルバージョンでこの現象が確認されている。
  • 3.6.11+ #474 PREEMPT Thu Jun 13 17:14:42 BST 2013
  • 3.6.11+ #538 PREEMPT Fri Aug 30 20:42:08 BST 2013
  • 3.10.19+ #600 PREEMPT Sat Nov 16 20:34:43 GMT 2013
カーネルを自分でビルドするとかモジュールでパッチするとかはやりたくないので、アプリケーションレベルでの回避策を考える。

ローカルで回避

RPiにログインしてローカルでVFATFSのタイムゾーンの問題を回避するにはTZ環境変数を使う。例えば
$ ls -l /var/run/usbmount/Syntek_USB_MSDC_1
 とする代わりに
$ env TZ=UTC ls -l /var/run/usbmount/Syntek_USB_MSDC_1
 とする。ぎこちないが仕方ない。

リモートで回避

そもそもカメラのファイルシステムのタイムゾーンが間違っていると何故困るかと言うと、構築中のシステムではカメラの「RECxxxxx.AVI(xxxxxはシーケンス番号)」という名前のMJPEG録画ファイルをサーバに転送して、最終的に録画開始時刻を基にした「20131201_07:01:23.avi」のようなファイル名のMPEGファイルに変換して保存したいからだ。残念ながら

2013年9月10日

AndroidでCalDAVカレンダを使う

NOOKというAndroidのタブレットの環境を整備している。今回はカレンダ。

我が家のカレンダシステムはThunderbird/Lightningを基にしている。ぐぐるなどの外部サービスに生殺与奪権を与えるのは嫌なので、サーバは自前で、汎用性のあるCalDAVを使っている。CalDAVはWebDAVというHTTP1.1上のプロトコルの上で動作するプロトコル上のカレンダ同期専用プロトコル。WebDAVの上では、CalDAV以外にもXMarksのブラウザの同期プロトコルなどが走る。
 
色々調べた結果、Androidでは個々のカレンダクライアントは直接外部CalDAV(あるいは別のプロトコルの)サーバと交信するのではなく、Android内部の共通カレンダデータベースをアクセスするらしいことが分かった。そうなると、カレンダ(CalDAV)同期用クライアントとカレンダ表示クライアントが別になり、自由度が増す。

と言うことでまた色々調べたが、 CalDAV同期用クライアントで使えそうなICSSyncという有料アプリが見つかった。現在まだお試し期間中だが、必要にして十分な機能をソツなく実現している印象だ。CalDAVサーバとAndroid内部のデータベースを双方向に同期してくれる。

我が家内部の家族メンバ別のカレンダとMozillaの休日カレンダを設定し、同期が実現したのでこれでうまくいくはず、と思ったが、そうは問屋がおろさない。NOOKにデフォルトでインストールされているカレンダが同期したデータを安定に表示しない。そこで代替カレンダを探したのだが、Lightningのようなシンプルでスケジュール管理のできるカレンダはあまりない。結局、ぐぐるのカレンダがこちらの要求に一番合っているのでこれをインストール。
 
しかし、ここでまた伏兵。ぐぐるカレンダは予定の表示は問題なく動作するのだが、 新規予定を作成しようとしたり、既存の予定を編集しようとしたりすると「LOADING...」を表示して固まってしまう。と言っても、致命的ではなく、「戻る」の左矢印をタップすれば抜け出せるが、カレンダの予定が読み出し専用なことに変わりはない。まぁ、とりあえず新規作成と変更はThunderbird/Lightningで行い、ぐぐるがアプリをアップデートするまで待つことにする。

PPTPD VPNサーバを走らせる

最近ハマっているVPNプロジェクトの中でPPTPサーバを走らせてみたが、巷の情報と実際が食い違ったり、よそ様の情報では不明瞭だった事柄が見つかったので書いてみる。
 

その1:PPPD

LinuxのPPTPやIPSecのようなVPNプロトコルの実装は、プロトコル独自の通信レイヤと、複数の異なるVPNプロトコルに共通の仮想ネットワークインタフェースレイヤが明確に分離していて、この共通部分はPPPD(Point-to-Point Protocol)というデーモンで実現される。PPPDはPPTPD(サーバ側)/PPTP(クライアント側)やIPSecといった「プロトコルプロセッサ」から起動され、ユーザが直接起動することはない。
 
ところで、実際にPPTPDを走らせる前にいろいろなサイトで情報を事前チェックしてみると、PPTPDが立ち上がるとすぐにPPPDが起動され仮想ネットワークインタフェースとしてppp+が作成されるようなことが書いてあったにもかかわらず、実際にPPTPDを走らせてみるとPPPDは起動されずppp+も作成されない。さては何かまずい設定か、といろいろなサイトと/etc/pptpd.conf/etc/ppp/options.pptpdの比較をしてみるが、問題は見つからない。PPTPDを起動するときにstraceで追っかけるが、PPPDの起動に失敗した形跡はなく、むしろPPPDなんか起動するつもりがなさそうな形跡だけが目につく。しかも、PPTPDはPPPDなしでそ知らぬ顔でデーモンとしてふんぞり返っている。
 
こういうミステリアスな状態にすっかり混乱し、LinuxQuestions.orgに質問を投げたりもしてみたが有益な情報は得られなかった。
 
ふと思い立って、PPTPDだけが走りPPPDのない状態でクライアントから接続を試みたら、ビックリ仰天!他にも問題があってこの時点でのVPN接続は最終的には失敗するも、サーバ側では接続の試行中にPPPDが走り、ppp+もIPアドレスこそ与えられていないが作成されているではないか!
 
PPTPD(そしてIPSecも)では、サーバデーモン起動時にはPPPD(およびppp+)は起動されず、クライアントから接続要求があったときにオンデマンドで動的に起動・作成されることが分かった。
 

その2:パケットフィルタリング

これは完全に私のチョンボだが、PPTPで開放しなければならないのはGREプロトコル(47)とTCPのPPTPポート(1723)。UDPを使ったOpenVPNのつもりでUDPを開いていたら最初のLCPのハンドシェークで失敗していた。
 

その3:options.pptpd

PPPDに様々なオプションを渡すために、/etc/pptpd.confに「option /etc/ppp/options.pptpd」というコマンドを書くようになっており、そのコメントには「指定省略時には/etc/ppp/optionsが使われる」と書いてある。これは正しくない、あるいは少なくとも正確ではない。PPPDには、「option」で指定したオプションファイルに加え、常に/etc/ppp/optionsのオプションファイルが渡される/etc/ppp/optionsはPPPDの共通オプションファイルで、VPNのプロトコルやサーバかクライアントかに関わらず常にPPPDに渡されるから、内容は真に共通な最小限にするか、または/etc/ppp/optionsが存在しないほうがよい。インストール時のデフォルトの/etc/ppp/optionsの内容は「Lock」の一行のみ。
 

この、PPPDがプロトコルやクライアント・サーバを区別しない(と言うより区別できない)問題はip-up.localip-down.localにも共通するから、PPPDを使う複数のVPNプロトコルやクライアントとサーバを同時に走らせるシステムでは適切な対応が必要。具体的には、これらのスクリプトは以下のような引数で起動されるので、(必要なら)それらで区別する。
  • PPTP(クライアント): インタフェース名(ppp+) pty名(pts+) ボーレート(38400) ローカル(クライアント側)エンドポイントアドレス リモート(サーバ側)エンドポイントアドレス
  • PPTPD(サーバ): インタフェース名(ppp+) pty名(pts+) ボーレート(38400) ローカル(サーバ側)エンドポイントアドレス リモート(クライアント側)エンドポイントアドレス クライアントの物理IPアドレス
PPTPDは6番目の引数としてクライアントの物理IPアドレス(正確にはVPNが走る下位ネットワークのIPアドレス)が追加される。インタフェース名は動的に割り当てられるので、これをプロトコルやクライアント/サーバの識別に使ってはいけない。
 
IPsecはどうなるのかは解析していない。
 

その4:ppp+

上でPPPDはクライアントから接続要求があったときにオンデマンドで起動されると書いたが、これは最初に接続するクライアントのみ。PPTPDは複数のクライアントを受け付けるが、二番目以降のクライアントは最初に起動されたPPPDが面倒をみるので、新たなPPPDプロセスは起動されず、したがってppp+仮想ネットワークインタフェースも「Point-to-Point」の名に反し一つだけで複数のクライアントと接続する。P-t-Pと言うことでOpenVPNのtunインタフェースのイメージを抱いていたが、むしろtapに近い。なおクライアントのエンドポイントアドレスは各クライアント個別に/etc/pptpd.confの「remoteip」で指定されたレンジの先頭からわりあてらていくようだ。ifconfigで見たときのppp+には、最初に接続したのクライアントのエンドポイントアドレスだけがリモート側に表示される。

iPhoneをVPNでLANにつなぐ

このところVPNに少々ハマっている。

数年前から、まず日本に置いた録画サーバをファイルバックアップサーバとして使うときのセキュリティのためにOpenVPNを開通させており、次いで職場のWSと自宅のLANの間にやはりOpenVPNを開通させて重宝している。また、ラップトップから自宅LANへのOpenVPNも構築し、旅行先から自宅の全ネットワークソースを使えている。

最近、NOOKというAndroidのタブレットとAmazonのKindle Paperwhiteを二台づつ買った。Kindleの方は電子本リーダ専用ということで我が家のVPNとは直接関係ないが、二台のうち一台は日本のキンドルストアから日本の本を買うため専用ということで、アマゾンの国別サービス制限を回避するために外部VPNを使っている(こちらの記事参照)。NOOKの方は少なくとも一台は配偶者の仕事用ということで、仕事先から我が家のLANにアクセスできるようにOpenVPNを開通させた。

となると、完璧主義者の小生としては、残りのモバイル機器である家族3人が使っているiPhoneも何とかVPNで我が家のLANに接続させたくなる。実を言うと、接続できたからと言って何ができるようになると言うあてはない。とにかく、まず接続してみたいのだ。

と言うことで仕事が暇になったのをよいことにあれこれやってみた。

使ったのは
・Linuxサーバ:Fedora 13
・iPhone 4S:iOS 6.1.3
・ネットワーク:WiFi(3G/4Gはまだ試していない)

iPhoneはネイティブでPPTP、IPSec、L2TPのVPNをサポートしている(私にはIPSecとL2TPの区別がよくつかない)いるが、結論としてこれらのいづれもダメで、やはりiPhoneもOpenVPNを採用することにした。
 

PPTP

少々の紆余曲折はあったが、Linuxのファイアウォール上でPPTPDを動かし、外部のLinuxクライアントから暗号付きでVPNを構築する異に成功した。

ところがiPhoneで同じように開通させようとしてもエラーになるばかりで、結局暗号化をすべて禁止しないとダメだということが分かった。VPNの目的が中国のようにサイトのアクセス規制のある国から禁止されているサイトをアクセスするためで、VPNの先がパブリックのインターネットなら暗号化は不要だが、ここで目的としているようにプライベートなLANをアクセスするためには暗号化されていないのは論外と言うことで却下。
 

IPSec/L2TP

こちらのVPNの比較記事を読み、試すまでもなくやめた。iPhoneのネイティブサポートに対する期待はあったが、PPTPのような結末では時間の無駄と判断し、性能と安全性にすぐれ(我が家で)実績のあるOpenVPNを進めることにした。
 

OpenVPN

iTunesからOpenVPNのアプリをダウンロードしてインストールしたら、基本的な設定はAndroidの場合と変わるところはない。*.confファイルから*.ovpnファイルを作成するところも同じ(こちらのスクリプトを使う)。
 
*.ovpnファイルの取り込みはさらに簡単で、ファイルを(本文ではなく)添付したメイルをiPhoneに送ってやって、そのファイルを開くとOpenVPNのアプリが起動してインポートが始まる。

さあ、iPhoneからもOpenVPNが開通した。何をさせよう?

2013年8月23日

ファイルシステムを跨いでのファイルの移動にはmvを使ってはいけない

mv(1)は賢い。同じファイルシステム内のファイルの移動ならrename(2)を使ってディレクトリエントリだけを変更し、ファイルシステムを跨いだ移動ならまずcp(1)と同じ動作でファイルをコピーしてからrm(1)と同じ動作で元のファイルを削除する。便利だ。

しかし便利さに甘えてはいけない。

失敗例は、btrfsの一つのサブボリュームから別のサブボリュームにファイルを移動させようとしたとき。mvで移動を開始したがどうも遅い。簡単な計測をしてみると5MB/s程度の速度しか出ていない。SOHO向けのNASで、だ。

straceで調べてみると、ファイルをコピーする前に必ずrenameを実行して本来の移動だけですまないかどうか試して、ダメならコピーを実行する、と言うことを全てのファイルの移動に毎回行っている。一回にかかる時間は僅かかも知れないが、移動したいファイルは50万個ぐらいあるから無視できない。おまけに、コピーの実行には32KBという小さなバッファで毎回read(2)write(2)を呼んでいる。遅いわけだ。多分cpとかtar(1)で実際にファイルをコピーして元のファイルを削除した方が速かったと思う。

今回はbtrfsのサブボリューム間の移動だった。btrfsのサブボリュームはストレージプールは共有するが個別のファイルシステムとして動作する。だから、コピーによる移動は同じストレージプールにコピーして削除と言うバカ臭いことになる。

おまけにmvのコピーによるファイルの移動は、個々のファイルのアトリビュートは全ファイルをコピーし終わるまで確定しないようだ。

今回はストレージの空き容量容量が十分あったので全ファイルを一旦コピーしてからコピー元の全ファイルを削除と言う方法も取れたはずだが、空き容量に余裕がないときはファイルをスキャンしながら一つづつコピーしては削除と言う動作をするスクリプトでも書くか。