仙石浩明の日記

2007年12月10日

La Fonera+ を買ってシリアル・ケーブルをつないでみた hatena_b

FON から無料キャンペーンのお知らせが来たので、 土曜日に秋葉原の九十九電機へ行ってラ・フォネラ+を ?台買ってきた。

La Fonera+

一台 1000円。 以前から La Fonera+ に興味はあったのだが、 3800円はチト高いなぁと思って手を出さずにいたので、 この機会に是非入手すべく、朝早く (といっても 10:00 ごろだが) から店頭へ。

ツクモ パソコン本店には開店前から行列ができている。 本店の整然とした並びとは対照的に、 DOS/V パソコン館では La Fonera+ の棚の前に無秩序に人が群がっている。 私は大阪人なので、並ぶのはニガテである。 なので DOS/V パソコン館の前で開店を待つことにする。 ちなみに、 ツクモケース王国や 12号店は開店時間が 11:00 なので誰も並んでいない。

10:30 開店の合図と同時に La Fonera+ 目指して飛びかかる客数人 (^^;)。 みるみる間に棚の上の La Fonera+ が減っていく。 無事入手して足早に秋葉原を後にする。

帰宅してすぐ、La Fonera+ 裏面のゴム足をはがす。 う、トルクス (TORX は登録商標なので、ヘックスローブ Hexlobe とも呼ばれる) ねじだ。 La Fonera は普通の「+」ネジだったのに... 慌てて近所のホームセンターに走る。 ねじのサイズは T-9H のようだ。 1サイズ小さい T-8H でも使えた。

La Fonera+ 内部

La Fonera にはヒートシンクがあったのに、La Fonera+ には無い。 ↑ 基板下辺に、JP1 と書かれたジャンパ・ピンが 4本立っている。 基板裏を見ると、一番右は明らかに GND である。 真ん中二本は信号線のようなので、 UP-12C (USB携帯電話メモリー転送ケーブル) の線を、 JP1 左から NC(つながない)、白(Rx)、緑(Tx)、黒(GND) の順につないでみると、 無事 RedBoot の出力を PC 側で見ることができた。

google で検索してみると、 UP-12C の緑が Rx で、白が Tx と書いてある説明が多いが、 これは緑を (デバイスの) Rx につなぐという意味で書いた (オリジナルの) 記述が、 「緑=Rx」と誤解されて広まったためではないだろうか? デバイス (La Fonera+) の Rx (受信入力) につなぐのは、 UP-12C の Tx 信号 (送信) 線だから、 「緑=Tx」「白=Rx」と書くべきだと思う。 逆に、ジャンパ・ピンの説明をするなら、 緑をつなぐピンが Rx ということになる。

つまり、JP1 左から (La Fonera+ 側から見て) Vcc, Tx, Rx, GND だと思われる。 ちなみに UP-12C の赤は電源なので接続してはいけない。 赤をショートさせると、 USB の電源供給能力一杯の電流が流れ、 UP-12C 本体が過熱する (でも壊れなかった。経験者談 ^^;)。

La Fonera+ にシリアル・ケーブルをつなぐ

RedBoot は boot script を自動的に実行しようとするので、 2秒以内に ^C を入力すると、 RedBoot のコマンドプロンプトが表示される:

senri %        cu -l ttyUSB1
Connected.
+Ethernet eth0: MAC address 00:18:84:xx:xx:xx
IP: 192.168.1.1/255.255.255.0, Gateway: 0.0.0.0
Default server: 192.168.1.254

RedBoot(tm) bootstrap and debug environment [ROMRAM]
OpenWrt certified release, version 1.1 - built 22:32:28, May  7 2007

Copyright (C) 2000, 2001, 2002, 2003, 2004 Red Hat, Inc.

Board: FON 2201 
RAM: 0x80000000-0x81000000, [0x80040290-0x80fe1000] available
FLASH: 0xa8000000 - 0xa87f0000, 128 blocks of 0x00010000 bytes each.
== Executing boot script in 2.000 seconds - enter ^C to abort
^C
RedBoot> 

コマンド一覧を表示させてみる:

RedBoot> help
Manage aliases kept in FLASH memory
   alias name [value]
Set/Query the system console baud rate
   baudrate [-b <rate>]
Move Atheros Board Data information
   bdmove 
Restore Atheros Board Data information
   bdrestore 
Manage machine caches
   cache [ON | OFF]
Display/switch console channel
   channel [-1|<channel number>]
Compute a 32bit checksum [POSIX algorithm] for a range of memory
   cksum -b <location> -l <length>
Display (hex dump) a range of memory
   dump -b <location> [-l <length>] [-s] [-1|2|4]
Execute an image
   exec [-b <argv addr>] [-c "kernel command line"] [-w <timeout>]
        [<entry point>]
Manage FLASH images
   fis {cmds}
Manage configuration kept in FLASH memory
   fconfig [-i] [-l] [-n] [-f] [-d] | [-d] nickname [value]
Get Key number
   get_key 
Get Mac addresses
   get_mac 
Get Serial number
   get_serial 
Execute code at a location
   go [-w <timeout>] [-c] [-n] [entry]
Help about help?
   help [<topic>]
Display command history
   history 
Set/change IP addresses
   ip_address [-l <local_ip_address>[/<mask_len>]] [-h <server_address>]
Load a file
   load [-r] [-v] [-d] [-h <host>] [-p <TCP port>][-m <varies>] [-c <channel_number>] 
        [-b <base_address>] <file_name>
Compare two blocks of memory
   mcmp -s <location> -d <location> -l <length> [-1|-2|-4]
Copy memory from one address to another
   mcopy -s <location> -d <location> -l <length> [-1|-2|-4]
Fill a block of memory with a pattern
   mfill -b <location> -l <length> -p <pattern> [-1|-2|-4]
Network connectivity test
   ping [-v] [-n <count>] [-l <length>] [-t <timeout>] [-r <rate>]
        [-i <IP_addr>] -h <IP_addr>
Reset the system
   reset 
Set values
   set 
Display RedBoot version information
   version 
Display (hex dump) a range of memory
   x -b <location> [-l <length>] [-s] [-1|2|4]

La Fonera の RedBoot と比べると、 bdshow コマンド (Display Atheros Board Data information) が削除され、 get_key, get_mac, get_serial, set コマンドが追加されているが、 機能的には大差ないようだ。

フラッシュ・メモリの内容は、こんな感じ:

RedBoot> fis list
Name              FLASH addr  Mem addr    Length      Entry point
RedBoot           0xA8000000  0x80040400  0x00030000  0xA8000000
loader            0xA8030000  0x80100000  0x00010000  0x80100000
image             0xA8040000  0x80040400  0x00230004  0x80040400
image2            0xA8660000  0xA8660000  0x00140000  0x80040400
FIS directory     0xA87E0000  0xA87E0000  0x0000F000  0x00000000
RedBoot config    0xA87EF000  0xA87EF000  0x00001000  0x00000000

「vmlinux.bin.l7」や「rootfs」の区画があった La Fonera とは様変わりしている。 サイズからいって、「loader」は単なるイニシャル・ローダであろう。 「image」「image2」はカーネル・イメージ? 0xA8270004 ~ 0xA8660000 の 4M Byte 弱 (0x3EFFFC = 4128764) が空いているが、 おそらくここにルート・イメージがあるのではないかと思われる。 ルート・イメージにしては少し小さすぎる気がするが、 おいおい解析していく予定。

設定 (fconfig) は次のようになっていた:

Run script at boot: true
Boot script:
.. fis load -b 0x80100000 loader
..  go 0x80100000
Boot script timeout (1000ms resolution): 2
Use BOOTP for network configuration: false
Gateway IP address:
Local IP address: 192.168.1.1
Local IP address mask: 255.255.255.0
Default server IP address: 192.168.1.254
Console baud rate: 9600
GDB connection port: 9000
Force console for special debug messages: false
Network debug at boot time: false

なぜ RedBoot から直接 Linux を起動せず、 「loader」を起動しているのか謎であるが、 変則的な fis list と関係があるのかも知れない。 また、La Fonera と異なり、 La Fonera+ ではシリアル・コンソールが有効になっていない。 すなわちカーネルの起動メッセージが出力されず、 ブート後にログインすることもできない。

- o -

さて、一段落ついたところで、 「La Fonera+ の登録」をしておこうかと思い、 同梱されていたマニュアルにしたがって http://registerlafonera.fon.com にアクセスしたところ、

EN
You are not connected through your “MyPlace” signal. Please choose the “MyPlace” signal in your “Wireless Network Connection” window and click connect. See image.

日本語
あなたはMy Placeでの接続をなさっておりません。 'ワイアレスネットワークコ ネクションより 'My Place'を選択して接続し直してください。画像をご覧になってください。

と表示されてしまった。 今まで何台かの La Fonera を登録してきたが、 スムーズに登録できたためしがない。 FON の主旨は面白いのに、 こういう細かいところのツメが甘いのは、とてもモッタイナイ。 ついでに言うと、 あいかわらず日本語が微妙なのはなんとかならないものか。

「あなたはMy Placeでの接続をなさっておりません」などと 決めつけられてしまったが、 もちろん決してそんなことはない。 WPA パスフレーズとして S/N を入力して接続に成功したのだから、 確かに目の前の La Fonera+ の My Place 経由で インターネットに接続しているはずである。

さて、どうしたものか、と思いつつ、試しに FON_AP のほうを試してみたら、 あっさり登録できてしまった。 なんだかなぁ... これでは以前の La Fonera と同じ登録方法ではないか。 FON_AP 経由でルータ登録を行なうこの方法では、 第三者が勝手に他人の La Fonera+ を登録できてしまう。

Filed under: La Fonera — hiroaki_sengoku @ 07:28
2007年12月8日

チロルチョコ きなこもち 100コ入 & 特大だきまくら

妻が、 10円チロルチョコ きなこもち を買ってきた。 なぜか今年はあまり出回っていないらしい。

チロルチョコ きなこもち 100個入

10コ入袋 x 10コ入 <T10キナコモチフクロ> と 100コ入 <T1チロルキナコモチ>。
しめて 200個。馬鹿だねぇ...

10コ入 袋:

チロルチョコ きなこもち 10コ入 袋

100コ入 箱:

チロルチョコ きなこもち 100コ入 箱

小さすぎてバーコードが入れられないため、 コンビニでは 10円チロルチョコは売られていない (代りに 20円バージョンが売られている)、 と妻が自説を力説している。

チロルチョコ きなこもち

2005年のキャンペーンでもらった、チロルチョコ「きなこもち特大だきまくら」:

チロルチョコ「きなこもち特大だきまくら」 チロルチョコ「きなこもち特大だきまくら」当選
Filed under: その他 — hiroaki_sengoku @ 18:45
2007年11月22日

x86_64 Linux でメモリ・デバッグ・ツール Valgrind を使う場合の注意点 hatena_b

次のようなプログラム test.c について考える:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>

struct test {
    int32_t len;
    int8_t buf[16];
};

int main(int argc, char *argv[]) {
    struct test *p = malloc(sizeof(struct test));
    int8_t buf[16];
    p->len = sizeof(p->buf);
    bzero(p->buf, p->len);
    printf("0x%lX-0x%lX => 0x%lX\n",
           (long)p->buf, (long)p->buf+p->len-1, (long)buf);
    bcopy(p->buf, buf, p->len);
    free(p);
    return 0;
}

malloc(3) で確保した領域のうち、 16 byte を bcopy(3) でコピーするだけの極めて単純なプログラムであり、 特に問題はないように見える。

ところが memory debugging tool Valgrind を使って検証してみると、 x86_64 Linux だと次のようなエラーが出てしまう。

sag16:/home/sengoku/tmp % cc -O -Wall test.c
sag16:/home/sengoku/tmp % valgrind ./a.out
==19008== Memcheck, a memory error detector.
==19008== Copyright (C) 2002-2006, and GNU GPL'd, by Julian Seward et al.
==19008== Using LibVEX rev 1658, a library for dynamic binary translation.
==19008== Copyright (C) 2004-2006, and GNU GPL'd, by OpenWorks LLP.
==19008== Using valgrind-3.2.1-Debian, a dynamic binary instrumentation framework.
==19008== Copyright (C) 2000-2006, and GNU GPL'd, by Julian Seward et al.
==19008== For more details, rerun with: -v
==19008==
0x4D5C034-0x4D5C043 => 0x7FF000750
==19008== Invalid read of size 8
==19008==    at 0x4B9326B: (within /lib/libc-2.3.6.so)
==19008==    by 0x4B92C06: bcopy (in /lib/libc-2.3.6.so)
==19008==    by 0x4005BD: main (in /home/sengoku/tmp/a.out)
==19008==  Address 0x4D5C040 is 16 bytes inside a block of size 20 alloc'd
==19008==    at 0x4A1B858: malloc (vg_replace_malloc.c:149)
==19008==    by 0x400574: main (in /home/sengoku/tmp/a.out)
==19008==
==19008== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 8 from 1)
==19008== malloc/free: in use at exit: 0 bytes in 0 blocks.
==19008== malloc/free: 1 allocs, 1 frees, 20 bytes allocated.
==19008== For counts of detected errors, rerun with: -v
==19008== All heap blocks were freed -- no leaks are possible.

「Invalid read of size 8」、 すなわちアクセスすべきではないメモリを、 64bit (8 byte) 読み込み命令で読んだというエラー。

test.c で読み込みを行なう可能性があるところと言えば、 「bcopy(p->buf, buf, p->len);」の部分だけであり、 その範囲は printf で表示しているように、 0x4D5C034 番地から 0x4D5C043 番地までの 16 byte である。

ところが、Valgrind 曰く:

Address 0x4D5C040 is 16 bytes inside a block of size 20 alloc'd

ちょっと英語の意味が取りにくい (私の英語力が低いだけ? ^^;) が、 つまり「malloc で確保した 20 byte の領域のうち、 先頭から数えて 16 byte 目 (先頭は 0 byte 目と数える) が 0x4D5C040 番地であり、 この番地に対してメモリ読み込みが行なわれた」 という意味である (「16 byte 目」なら 「16 bytes」でなくて「16th byte」のような...?)。

すなわち、 「20 byte の領域のうち 16 byte 目」というのは残り 4 byte であり、 あと 4 byte コピーすればいいのにもかかわらず、 64bit 読み込み命令を使って 8 byte いっぺんに読んでしまっているから、 malloc で確保した領域の外をアクセスしてしまう、というわけ。

結果として 4 byte 無駄に読んでしまっている (実はコピー開始位置も 4 byte 前から行なうので、計 8 byte 余計に読み込んでいる) わけだが、 CPU にとって一番高速にコピーできる単位が (64bit 境界に合わせた) 64bit 読み書きだから、 bcopy の実装がこのようになっているのだろう。

より正確に言えば、 bcopy は 16 byte 以上のコピーを行なう場合は コピー開始位置手前の 64bit 境界 (alignment) の番地から 64bit ずつコピーし、 16 byte 未満の場合は byte 単位でコピーする。 test.c では、 コピー開始位置 p->buf が (直前のメンバが int32_t なので) 64bit 境界に一致しておらず、 しかもコピーする byte 数 p->len が 16 byte (= 64bit の倍数) なので、 16 byte 以上のコピーかつコピー終了位置も 64bit 境界に一致していない、 というのがミソである。

したがって 32bit な x86 Linux の場合であれば 32bit 単位でコピーを行なうので、 test.c ではこのようなエラーは起きない。 もちろん、64bit な x86_64 Linux で Valgrind がエラーを出すからといって、 bcopy の x86_64 における実装に問題がある、というわけではない。 Valgrind は、 あくまでバグの「可能性」を指摘するだけであって、 malloc で確保した領域の外へのアクセスでも、 それが意図的なものであれば (メモリ保護違反などでない限り) 何の問題もない。

分かってみれば単純な話なのであるが、 Valgrind のメッセージ「16 bytes inside a block」の意味が把握できなかった私は、 glibc の bcopy のソースを読んで 64bit 単位でコピーを行なっていることを知り、 4 byte の領域外読み込みが行なわれることを理解して初めて、 Valgrind のメッセージの意味が分かったという、 本末転倒な体験をした (^^;)。

ちなみに、 もちろん最初から上記のようなテストプログラムを Valgrind で チェックしようと思ったわけではなく、 「struct test」構造体は実際には次のような SockAddr 構造体であり、 saDup 関数にて malloc した SockAddr 構造体を doconnect 関数で bcopy する処理になっていて、 元ネタは拙作 stone である。

typedef struct {
    socklen_t len;
    struct sockaddr addr;
} SockAddr;
#define SockAddrBaseSize        ((int)&((SockAddr*)NULL)->addr)
...

SockAddr *saDup(struct sockaddr *sa, socklen_t salen) {
    SockAddr *ret = malloc(SockAddrBaseSize + salen);
...

int doconnect(Pair *p1, struct sockaddr *sa, socklen_t salen) {
    struct sockaddr_storage ss;
    struct sockaddr *dst = (struct sockaddr*)&ss;        /* destination */
...
    bcopy(sa, dst, salen);
...

stone ML にて、 Valgrind で検証したらエラーが出た、という報告を頂いて (_O_) 以上のような調査を行なった次第。 bcopy に与えた引数に問題はなく、 どうしてこれが 「Invalid read of size 8」 エラーを引き起こすのか謎だった。 結果的には stone には問題はなく、 修正の必要もないことが判明したわけであるが、 今まで使っていなかった Valgrind を使ってみるいいきっかけになった。 実を言うと 64bit Linux を (プログラミングのレベルで) 使ったのも、 今回が初めてだったりする (^^;)。

Filed under: stone 開発日記,プログラミングと開発環境 — hiroaki_sengoku @ 20:36
2007年11月21日

La Fonera ファームウェア vmlinux.bin.l7 の拡張子「L7」とは? hatena_b

FON ソーシャルルータ La Fonera のファームウェアは Linux であり、 RedBoot ブートローダ から起動される。 デフォルト状態の La Fonera では、 以下のように RedBoot がフラッシュメモリから Linux カーネルをロードして 自動起動する設定になっている。

== Executing boot script in 1.000 seconds - enter ^C to abort
RedBoot> fis load -l vmlinux.bin.l7
Image loaded from 0x80041000-0x801ba000
RedBoot> exec
Now booting linux kernel:
 Base address 0x80030000 Entry 0x80041000
 Cmdline : 
(以下略)

fis (Flash Image System) は、 フラッシュメモリの読み書きを行なうためのコマンドである。

fis load -l vmlinux.bin.l7」を実行することにより、 フラッシュメモリ内の「vmlinux.bin.l7」と名前をつけられた区画の内容を RAM へコピー (つまり load) する。

ところが、 fis load コマンドのマニュアルには、 「-l」オプションの記述がない。

Synopsis
fis load [-b load address] [-c ] [-d ] [name]

にもかかわらず、 La Fonera の RedBoot で fis コマンドのヘルプを表示させると、 「-l」オプションが指定できることが分かる。

RedBoot> fis help
*** invalid 'fis' command: unrecognized command
Usage:
  fis create -b <mem_base> -l <image_length> [-s <data_length>]
      [-f <flash_addr>] [-e <entry_point>] [-r <ram_addr>] [-n] <name>
  fis delete name
  fis erase -f <flash_addr> -l <length>
  fis free
  fis init [-f]
  fis list [-c] [-d]
  fis load [-d] [-l] [-b <memory_load_address>] [-c] name    ← コレ
  fis write -f <flash_addr> -b <mem_base> -l <image_length>

一体この「-l」オプションとは何なのか?

というか、 そもそも「vmlinux.bin.l7」という名前の見慣れない拡張子「L7」とは何なのか?
(L7 というと Layer 7 くらいしか思いつかない... ^^;)

というわけで調べてみた。

「vmlinux.bin.l7」の内容と、 「fis load -l」コマンドによって 0x80041000 番地にロードされた内容を見比べると、 「vmlinux.bin.l7」はデータ圧縮を行なった形式であるように見える。 おそらく「-l」オプションは、 フラッシュメモリ上の圧縮データを展開して RAM へロードするための オプションなのだろう。 圧縮データを展開するオプションとして「fis load」コマンドには、 すでに「-d」(gzip 圧縮データを展開) があるにもかかわらず、 何故わざわざ gzip 以外の圧縮形式を使っているのか謎であるが、 おそらくはより圧縮率の高い圧縮形式を使いたかったのだろう。

gzip より圧縮率が高い圧縮形式で「7」といえば 「7-Zip」、 という (安直な ^^;) 連想をもとに、 とりあえず手元にあった lzma コマンドで展開を試みてみる。

% lzma d vmlinux.bin.l7 vmlinux.bin

LZMA 4.43 Copyright (c) 1999-2006 Igor Pavlov  2006-06-04

ありゃ、あっさり展開できてしまった。 ちょっと拍子抜け。

展開した vmlinux.bin を TFTP サーバに置いて、 La Fonera に読み込ませて起動してみる:

RedBoot> load -r -b 0x80041000 vmlinux.bin
Using default protocol (TFTP)
Raw file loaded 0x80041000-0x8029aa37, assumed entry at 0x80041000
RedBoot> exec
Now booting linux kernel:
 Base address 0x80030000 Entry 0x80041000
 Cmdline :
(以下略)

これで、TFTP サーバに置いた任意のカーネルを La Fonera で起動することが できるようになった。 フラッシュメモリに書込む必要がないので手軽にカーネルの入れ替えができる。

ちなみに、 「vmlinux.bin」は RAM 上にコピーして即実行 (RedBoot の exec コマンド) 可能であることから、 raw binary 形式 (つまりオブジェクトファイルから、 シンボル情報やリロケーション情報を取り除いたもの) であることが分かる。 つまり、「vmlinux.bin」の拡張子「bin」は「raw binary」の意味なのだろう。 vmlinux から raw binary 形式のファイル vmlinux.bin を得るには、 次のように objcopy コマンドを実行すればよい。

% mips-linux-uclibc-objcopy -O binary vmlinux vmlinux.bin
Filed under: La Fonera — hiroaki_sengoku @ 06:49
2007年10月28日

ぎゅうぎゅう (箱に入った三匹の小猫)

妻のリクエストにより掲載:

3 cats in a box

どうしてこんな狭い箱に無理矢理入りたがるのか、 猫に興味がない私には全く理解できない。

箱はペットボトル6本入り段ボール箱を妻が半分に切ったもの。 三匹が入るにはかなり無理がある。
全体図:

3 cats in a box
Filed under: その他 — hiroaki_sengoku @ 10:55
2007年10月15日

ビデオキャプチャ・カード GV-MVP/RX2W を使って Linux 2.6.22.10 でテレビ録画 hatena_b

Linux でテレビ録画を行なう方法は、 多くの Web ページで紹介されているが、 ビデオキャプチャ・カードによっては、 Linux カーネルのバージョンが変わると一筋縄にはいかなかったりするので、 現時点での Linux カーネル安定版の最新バージョン 2.6.22.10 で、 I-O DATA 製 ハードウェア MPEG-2 エンコーダ搭載TVキャプチャボード GV-MVP/RX2W (2006年7月5日生産終了) を使う方法をメモ (2.6.24.4 で使う方法)。

といっても多くの方々の努力により、 GV-MVP/RX2 は標準のカーネルのままで (多少の不具合はあるにせよ) テレビ視聴が可能になりつつあるので、 Linux 2.6.22.10 の時点で必要なパッチは、 以下の三ヵ所のみ (モノラル音声で構わなければ saa7115 に対するパッチのみ)。 なお、 IVTV プロジェクト で 最新ドライバが公開されているが、 少なくとも GV-MVP/RX2W を使う限りにおいては 既に Linux 2.6.22.10 カーネルに取り込まれているドライバだけで問題がないので、 標準のカーネルのドライバのみを使っている。

まず、SAA7115 Video Decoder ドライバ・モジュール saa7115.ko に対するパッチ:

diff -ru linux-2.6.22.10.org/drivers/media/video/saa7115.c linux-2.6.22.10/drivers/media/video/saa7115.c
--- linux-2.6.22.10.org/drivers/media/video/saa7115.c        2007-08-23 08:23:54.000000000 +0900
+++ linux-2.6.22.10/drivers/media/video/saa7115.c        2007-10-14 06:37:09.000000000 +0900
@@ -1543,7 +1543,8 @@
                 saa711x_writeregs(client, saa7113_init);
                 break;
         default:
-                state->crystal_freq = SAA7115_FREQ_32_11_MHZ;
+                state->ucgc = 1;
+                state->cgcdiv = 0x04;
                 saa711x_writeregs(client, saa7115_init_auto_input);
         }
         saa711x_writeregs(client, saa7115_init_misc);

CGC (Clock Generation Circuit) の設定。 SAA7115 は 32.11MHz か 24.576MHz の水晶発振子を利用できて、 GV-MVP/RX2 では後者なのであるが、 このパッチをあてておかないと 32.11MHz の設定になってしまって、 録画したものを再生すると音声が早回しになってしまう。

次に、オーディオ・チップ・ドライバ・モジュール tvaudio.ko に対するパッチ:

diff -ru linux-2.6.22.10.org/drivers/media/video/tvaudio.c linux-2.6.22.10/drivers/media/video/tvaudio.c
--- linux-2.6.22.10.org/drivers/media/video/tvaudio.c        2007-08-23 08:23:54.000000000 +0900
+++ linux-2.6.22.10/drivers/media/video/tvaudio.c        2007-10-14 06:38:26.000000000 +0900
@@ -528,6 +528,31 @@
                 chip_write(chip,TDA985x_C6,c6);
 }
 
+static int gvmvprx_getmode(struct CHIPSTATE *chip) {
+        return VIDEO_SOUND_MONO;
+}
+
+extern int tda9887_tuner_init(struct i2c_client *c);
+static void gvmvprx_setmode(struct CHIPSTATE *chip, int mode) {
+        u8 data[3] = { 0x00, 0x00, 0x04 };
+
+        switch (mode) {
+                case VIDEO_SOUND_MONO:
+                case VIDEO_SOUND_LANG1:
+                        break;
+                case VIDEO_SOUND_STEREO:
+                        data[1] = 0x01;
+                        break;
+                case VIDEO_SOUND_LANG2:
+                        data[1] = 0x02;
+                        break;
+        }
+
+        if (i2c_master_send(&chip->c, data, sizeof(data)) != sizeof(data)) {
+                v4l_err(&chip->c, "%s: I/O error setting audmode\n", chip->c.name);
+        }
+}
+
 
 /* ---------------------------------------------------------------------- */
 /* audio chip descriptions - defines+functions for tda9873h               */
@@ -1307,17 +1332,17 @@
                 .checkmode  = generic_checkmode,
         },
         {
-                .name       = "tda9850",
+                .name       = "tda9850 (GV-MVP/RX)",
                 .id         = I2C_DRIVERID_TDA9850,
                 .insmodopt  = &tda9850,
                 .addr_lo    = I2C_ADDR_TDA985x_L >> 1,
                 .addr_hi    = I2C_ADDR_TDA985x_H >> 1,
                 .registers  = 11,
 
-                .getmode    = tda985x_getmode,
-                .setmode    = tda985x_setmode,
+                .getmode    = gvmvprx_getmode,
+                .setmode    = gvmvprx_setmode,
 
-                .init       = { 8, { TDA9850_C4, 0x08, 0x08, TDA985x_STEREO, 0x07, 0x10, 0x10, 0x03 } }
+                .init       = { 3, { 0x00, 0x01, 0x04 } }
         },
         {
                 .name       = "tda9855",

ステレオ/モノラル/音声多重(二ヶ国語放送) 音声モードの変更。 GV-MVP/RX2W は他のチューナと異なり、 設定変更の際 3 バイトのデータを送信する必要があるらしい。

tda9887 を deemphasis->off (このチップは deemphasis すると音声を強制でモノラルにしてしまう。 tda9887.c の NTSC-M-J の項では deemphasis を default で OFF にすべきなのではないでしょうか。 素人考えかな。)
ivtv-0.2.0-rc3j-paken.051002-bilingul.patch をベースに MPX 操作 (レジスタ0に3バイト書き込むほう) すればうちのカードでは音声多重に対応できるようです。 レジスタ 0 に 1 バイト書き込むほうのMPX操作では うちのカードでは動作しませんでした。 カードのリビジョンとかの関係かもしれません。
ぱ研「LinuxでITVC16-STVLP」 の kazz 氏コメント (2007-03-10) から引用

DeEmphasis (DEM) モードを設定すると常にモノラル音声になってしまう。 kazz 氏が言及しているように default で DEM を off にしておけば 済むような気がするし、 そもそもなぜ DEM がデフォルトで ON になっているのか疑問だったので、 以下のようなパッチを tuner チップ・ドライバ・モジュール tuner.ko の tda9887.c にあてて、 NTSC-M-JP の場合は DeEmphasis を off にしてみた。

diff -ru linux-2.6.22.10.org/drivers/media/video/tda9887.c linux-2.6.22.10/drivers/media/video/tda9887.c
--- linux-2.6.22.10.org/drivers/media/video/tda9887.c        2007-08-23 08:23:54.000000000 +0900
+++ linux-2.6.22.10/drivers/media/video/tda9887.c        2007-10-14 11:59:50.000000000 +0900
@@ -214,8 +214,7 @@
                 .name  = "NTSC-M-JP",
                 .b     = ( cNegativeFmTV  |
                            cQSS           ),
-                .c     = ( cDeemphasisON  |
-                           cDeemphasis50  |
+                .c     = ( cDeemphasisOFF |
                            cTopDefault),
                 .e     = ( cGating_36     |
                            cAudioIF_4_5   |

「cDeemphasisON | cDeemphasis50」の部分を「cDeemphasisOFF」に変更しただけの 安易なパッチ (^^;) であるが、 とりあえずこれでステレオ/音声多重での録画が可能になっている。

以上のように tvaudio と tuner (tda9887.c) の両ドライバ・モジュールに パッチをあてることにより、 ステレオ/音声多重で録画できる。 もちろん v4l2-ctl コマンドで音声モードを変更することもできるが、 録画は常に「ステレオ/音声多重」で行なっておいて、 再生時に音声モードを切替えたほうが便利。

私は録画 perl スクリプトを自作して使っているが、 この perl スクリプトで使用している Video::ivtv & Video::Frequencies モジュールには若干問題がある。 すなわち setFrequency する際にチューナ形式を指定し忘れているので、 正しく TV チャンネルの変更が行なえない。 私は以下のような修正パッチをあてて使用している。

diff -u Video-ivtv-0.13/ivtv.xs.org Video-ivtv-0.13/ivtv.xs
--- Video-ivtv-0.13/ivtv.xs.org        2004-06-14 06:07:40.000000000 +0900
+++ Video-ivtv-0.13/ivtv.xs        2007-10-08 10:03:57.000000000 +0900
@@ -132,6 +132,7 @@
                 }
   CODE:
     vf.tuner = tuner;
+    vf.type = V4L2_OUTPUT_TYPE_ANALOG;
     if (ioctl(fd, VIDIOC_G_FREQUENCY, &vf) < 0)
     {
       RETVAL = -1;
@@ -158,6 +159,7 @@
     }
   CODE:
     vf.tuner = tuner;
+    vf.type = V4L2_OUTPUT_TYPE_ANALOG;
     vf.frequency = freq;
     if (ioctl(fd, VIDIOC_S_FREQUENCY, &vf) < 0)
     {

この録画スクリプトは 予約録画 (-t オプション) もサポートしている他、 tv -P 1234 などと実行すると、 TV サーバとして利用することもできる。 つまり LAN 内の任意のマシンで、 VLC media player を使って http://senri:1234/?c=1 などとチャンネル指定付で tv サーバへ接続し、 TV を視聴できる。

(ビデオキャプチャ・カード GV-MVP/RX2W を使って Linux 2.6.24.4 でテレビ録画)

Filed under: ハードウェアの認識と制御 — hiroaki_sengoku @ 07:14
2007年10月12日

ハードウェア・ウォッチドッグ・タイマー iTCO_wdt のススメ hatena_b

極めて稀とはいえ、Linux もハングすることはある。 ハードウェア自体には何ら異常はなく、 リセットスイッチを押したら正常に再起動してしまって、 何が問題だったか分からずじまい、という経験は誰にでもあるのではなかろうか。 原因不明のハングが全く無くなるのが理想ではあるのだが、 ハングして止ったままになるよりは、 自動的にリセットがかかって再起動してくれたほうがいい、という場合もあるだろう。

もちろんハードウェア障害が原因でハングしてしまった場合は、 リセットスイッチを押しても解決にはならない。 再起動を試みることにより、障害がより致命的になる可能性もあるので、 ウォッチドッグ・タイマーを設定する際は、 「止ったまま」と「自動再起動」とどちらがマシか天秤にかける必要がある。

そんなとき、 ウォッチドッグ・タイマー (watchdog timer) が便利。 一定時間 (例えば 30秒) 放置すると、 システムを自動的にリセットするタイマーである。 この自動リセットを回避するには、 定期的に (30秒以内に) タイマーを元に戻す (以下、番犬 (watchdog) に「蹴りを入れる」と略記) 必要があるわけで、 システムが正常に動作している時は 定期的に「蹴り」を入れ続けるようなプログラムを走らせておく。 で、 カーネルがハングしたなどの理由によって 「蹴り」を入れるプログラムが動かなくなると、 自動的にシステム・リセットがかかって ハング状態を脱出できる、という仕掛け。

ソフトウェアにどんなトラブルが起きても確実に再起動を行なわせるには、 ハードウェアで物理的にリセット スイッチを押す ハードウェアを用いるのが一番であるが、 まずはお手軽にソフトウェア版を利用してみることにした。
仙石浩明の日記: ウォッチドッグ タイマ から引用

わざわざハードウェア・ウォッチドッグ・タイマーを買ってきて 組み込むのは大変と思ったので、 上に引用した日記 (2006年5月) で書いたように ソフトウェア版ウォッチドッグ (softdog.ko モジュール) を使っていたのだが、 実はインテル・チップセットであれば大抵の PC に標準で ハードウェア・ウォッチドッグ・タイマーがついていた (何たる不覚 orz)。

Intel TCO Timer/Watchdog
Hardware driver for the intel TCO timer based watchdog devices. These drivers are included in the Intel 82801 I/O Controller Hub family (from ICH0 up to ICH8) and in the Intel 6300ESB controller hub.
linux/drivers/char/watchdog/Kconfig から引用

つまり Intel の ICH には最初から ハードウェア・ウォッチドッグ・タイマーがついていたようである。 最近の Linux カーネルには、 このウォッチドッグ・タイマーのドライバが含まれているので早速使ってみた。 というか、 Linux 2.6.22.9 を使っていたら、 このドライバ・モジュール iTCO_wdt が自動的に読み込まれていた (^^;) ので、 このウォッチドッグ・タイマーの存在に気づいた、という次第。 /dev/watchdog に何か書込んでみるだけで (例えば「echo @ > /dev/watchdog」を実行)、 タイマーがスタートした (/dev/watchdog が存在しない場合は、 「mknod /dev/watchdog c 10 130」を実行する)。

そして 30秒後、勝手にリセットがかかった (めでたしめでたし)。

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

じゃ、iTCO_wdt.ko モジュールでも同様に heartbeat=3600 を指定すればいいのかな と思っていたら、 heartbeat は最大 613 秒までしか設定できない (TCO v2 の場合) ようである。 わずか 10分足らずでは不用意に再起動してしまう恐れ大。 そこで、 監視プログラムが直接 /dev/watchdog に「蹴り」を入れる代わりに、 監視プログラムは /var/run/watchdog に「蹴り」を入れることにして、 /var/run/watchdog を監視して /dev/watchdog に「蹴り」を入れる 「蹴り代行」デーモンを走らせておくことにした。

つまり、監視プログラムは 20分に一度 /var/run/watchdog に「蹴り」を入れるだけで、 あとは「蹴り代行」デーモンが 5秒に一度、 /dev/watchdog に「蹴り」を入れ続けてくれる。 だからウォッチドッグ・タイマーのドライバの設定は、 デフォルト (30秒) のままで済むし、 また「蹴り代行」デーモンの設定次第で、 20分といわずもっと長い余裕を持たせることも可能。

/service/watchdog/run

#!/usr/bin/perl
use strict;
use warnings;
$| = 1;
my $watchdog_uid = getpwnam("adsl_check");
my $watchdog_gid = getgrnam("watchdog");
my $watchdog_file = "/var/run/watchdog";
my $watchdog_dev = "/dev/watchdog";
print "start\n";
if (! -f $watchdog_file) {
    if (!open(WATCHDOG, ">$watchdog_file")) {
        print "can't create $watchdog_file exiting...\n";
        exit 1;
    }
    close(WATCHDOG);
    chown $watchdog_uid, $watchdog_gid, $watchdog_file;
}
($(, $)) = ($watchdog_gid, $watchdog_gid);
($<, $>) = ($watchdog_uid, $watchdog_uid);
while (-z $watchdog_file) {
    sleep 5;
}
print "confirmed $watchdog_file\n";
truncate($watchdog_file, 0);
if (!open(WATCHDOG, ">$watchdog_dev")) {
    print "can't open $watchdog_dev exiting...\n";
    exit 1;
}
select(WATCHDOG);
$| = 1;
select(STDOUT);
for (my $i=0; $i < 240; $i++) {
    print WATCHDOG "\@\n";
    sleep 5;
}
print WATCHDOG "\@\n";
close(WATCHDOG);
print "exiting...\n";
exit 0;

「/service/watchdog/run」というパス名からも分かる通り、 このスクリプトは daemontools 配下で動かしている。 このスクリプトは、 20分間 (5 秒 * 240) /dev/watchdog に蹴りを入れ続けると終了する。 そして daemontools がこのスクリプトを再起動すると、 /var/run/watchdog の存在を確認した上で再び蹴りを入れ続ける。 つまり、 20 分間以上 /var/run/watchdog に蹴りが入れられないと、 この「蹴り代行」スクリプトは止ってしまい、 /dev/watchdog への蹴りも止ってしまう。

ここでなぜ 20分毎にこのスクリプトを終了するようにしているかというと、 daemontools の動きも監視対象に含めたいから。 つまり、システムの負荷が高くなり過ぎて daemontools による再起動に時間がかかるような事態になっても、 /dev/watchdog への蹴りが止る。

まとめると、 /var/run/watchdog への蹴りが止ったり、 あるいは daemontools による再起動が滞ったりすると、 /dev/watchdog への蹴りも止ってしまって、 ウォッチドッグ・タイマーが時間切れになり、 ハードウェア的に自動リセットがかかる、という仕掛け。

私は他のマシンから

ssh server "echo '@' > /var/run/watchdog"

などと ssh でアクセスするよう cron に設定している。 ssh が成功すれば /var/run/watchdog へ書き込み、 すなわち蹴りが入れられるので、 蹴り代行スクリプトによってウォッチドッグ・タイマーに蹴りが入れられる。

Filed under: システム構築・運用,ハードウェアの認識と制御 — hiroaki_sengoku @ 07:19
2007年9月29日

10行でできる高精度ハードウェア自動認識 (initramfs の init を busybox だけで書く) hatena_b

これまでLinuxのハードウェア自動認識と言えば、 /sys/bus/pci/devices 以下と、 /lib/modules/`uname -r`/modules.pcimap を照らし合わせて 解析していくのが定石でした。 USBにも対応しようとすると、もう一つ大変です。
しかしこれからの常識は、 /sys/bus/*/devices/*/modalias と
/lib/modules/`uname -r`/modules.alias です。
古橋貞之の日記「20行できる高精度ハードウェア自動認識」から引用

すばらしい。 確かに modules.alias を使う方が、 簡単かつ確実に必要なモジュールを読み込むことができそう。 さっそくこの方法を使って initramfs の init スクリプトを書き直してみた。

tmp=/tmp/dev2mod
echo 'dev2mod(){ while read dev; do case $dev in' > $tmp
sort -r /lib/modules/`uname -r`/modules.alias \
| sed -n 's/^alias  *\([^ ]*\)  *\(.*\)/\1)modprobe \2;;/p' >> $tmp
echo 'esac; done; }' >> $tmp
. $tmp
rm $tmp
cat /sys/bus/*/devices/*/modalias | dev2mod

わずかに 8行 (^^)
(9/30追記: modules.alias を逆順ソートしておく必要があることが判明、sort -r を追加)。

シェルスクリプト版はRuby版と比べて40倍くらい遅いので注意。
同ページ(古橋貞之の日記)から続けて引用

sh スクリプトの名誉のために言っておくと、 私が書いた上記 sh スクリプトだと、 古橋さんの Ruby 版と比べて 4倍くらいの遅さで済んでいる。

% time ./dev2mod
ide_cd
intel_agp
intelfb
uhci_hcd
...(中略)...
libusual
usbcore
0.252u 0.012s 0:00.77 33.7%        0+0k 0+0io 0pf+0w
% time ./detect_kmod.rb
["ivtv", "snd_intel8x0", "intelfb", "libusual", "ftdi_sio", "usbhid", "uhci_hcd", "ehci_hcd", "usbcore", "via_velocity", "eepro100", "e100", "3c59x", "psmouse", "ide_cd", "i2c_i801", "hw_random", "intel_agp"]
0.072u 0.008s 0:00.18 38.8%        0+0k 0+0io 0pf+0w

ちなみに古橋さんのスクリプトは、 modules.alias の各行それぞれに対し、 マッチするデバイスが /sys/bus/*/devices/*/modalias に存在すれば、 そのモジュールを読み込む処理になっている。
しかしながら、これだと一つのデバイスに対し、 複数のモジュールが読み込まれてしまうことになるのではないだろうか?

古橋さんが同日追記されているように、 複数のモジュールが読み込まれること自体は簡単に修正可能で、 むしろモジュールの読み込み順が modules.alias に載っている順になることのほうが問題。 この問題点を解決するため、 /sys/bus/*/devices/*/modalias の各行それぞれに対し、 マッチするモジュールを modules.alias から見つける修正版が追記された。 さすが古橋さん、すばやい。
9/30追記

例えば古橋さんのスクリプトだと、 私の手元のマシンでは e100 と eepro100 の両方のモジュールが読み込まれてしまう。 つまり、

% cat /sys/bus/pci/devices/0000:01:08.0/modalias
pci:v00008086d00001050sv0000107Bsd00004043bc02sc00i00

が、modules.alias の次の二つの行にマッチするため、 このようなことが起こる。

alias pci:v00008086d00001050sv*sd*bc*sc*i* eepro100
alias pci:v00008086d00001050sv*sd*bc02sc00i* e100

modules.alias を検索する際は、 マッチする行が見つかった時点で以降の行はスキップしないと、 この例のように複数のモジュール読み込みが起きる恐れがある。 マッチした以降の行を読み飛ばすには、 私が書いた上記 sh スクリプトのように、 /sys/bus/*/devices/*/modalias の各行それぞれに対し、 マッチするモジュールを一つだけ modules.alias から見つけて読み込む処理のほうが、 簡単に書けるのではないかと思うがどうだろうか。

とはいえ、実際の NIC は Intel Pro 10/100 だったりする (^^;) ので、 読み込むべきモジュールは e100 であるような気もする。 もし e100 が正しいモジュールであるのなら、 modules.alias における eepro100 のパターンが適切ではないということになるのかも。
9/30追記
「*」を多く含むパターンは「後で」マッチさせたほうが、 より適切なモジュールを選択できると考えられるため、 modules.alias を逆順ソートしておくことにした。 これにより、eepro100 ではなく、e100 を読み込むようになった。
9/30さらに追記

参考までに initramfs の /init スクリプト全体を添付しておく:

More...
2007年9月28日

chroot されたディレクトリから脱出してみる hatena_b

要約すれば、 「chrootなんて簡単に抜けられるからセキュリティ目的で使っても意味ないよ。」 ってことね。そうだったのか。

そうだったのか orz

Note that this call does not change the current working directory, so that `.' can be outside the tree rooted at `/'. In particular, the super-user can escape from a `chroot jail' by doing `mkdir foo; chroot foo; cd ..'.
chroot(2) から引用

chroot するときは、そのディレクトリへ chdir しておくのが常識と 思っていたので気づいていなかった。 つまり、 故意にカレントディレクトリを chroot 外へもっていけば、 chroot されたディレクトリから抜け出せてしまう、ということ。

より正確に言えば、 chroot されたディレクトリの中で、 さらに chroot すれば、 その「親」chroot ディレクトリを抜け出せてしまう。 chroot がネストしないことを利用したテクニック、ということか。 逆に言えば、 chroot(2) 実行時にカレントディレクトリを chroot ディレクトリ下へ 強制的に移動させるか、 あるいは chroot がネストするようにすれば回避可能?

mkdir foo; chroot foo; cd ..

確かに本質はこの短いコードで言い尽くされているが、 こーいうのを見ると実地に試さずにはおれないので、コードを書いてみた。

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <errno.h>
#define BUFMAX 256

int main(int argc, char *argv[]) {
    char buf[BUFMAX+1];
    sprintf(buf, "escape.%d", getpid());
    if (chdir("/") < 0) {
        printf("Can't chdir \"/\" errno=%d\n", errno);
        return 1;
    }
    if (mkdir(buf, 0755) < 0) {
        printf("Can't mkdir \"%s\" errno=%d\n", buf, errno);
        return 1;
    }
    if (chroot(buf) < 0) {
        printf("Can't chroot \"%s\" errno=%d\n", buf, errno);
        return 1;
    }
    if (rmdir(buf) < 0) {
        printf("Can't rmdir \"%s\" errno=%d\n", buf, errno);
        return 1;
    }
    if (!getcwd(buf, BUFMAX)) {
        printf("Can't getcwd errno=%d\n", errno);
        return 1;
    }
    printf("Now escaping from chrooted %s\n", buf);
    do {
        if (chdir("..") < 0) {
            printf("Can't chdir \"..\" cwd=%s errno=%d\n", buf, errno);
            return 1;
        }
        if (!getcwd(buf, BUFMAX)) {
            printf("Can't getcwd errno=%d\n", errno);
            return 1;
        }
    } while (buf[1] != '\0' && buf[0] == '/');
    if (chroot(".") < 0) {
        printf("Can't chroot \".\" errno=%d\n", errno);
        return 1;
    }
    argv++;
    execv(argv[0], argv);
    printf("Can't exec %s err=%d\n", argv[0], errno);
    return 0;
}

chdir / して、mkdir foo して、chroot foo して (rmdir foo して)、 その後に chdir .. でディレクトリ階層を上がれば抜け出せる。 言葉で書けば簡単だが、実際のコードを書こうとすると、 もう少し考えるべきことがあった。

すなわち、 抜け出した後、プログラムを終了してしまっては元の木阿弥であるので、 /bin/sh などを exec すべきであるし、 「本物」の / 下の /bin/sh をちゃんと実行するには、 「本物」の / へ chroot しなおす必要もある。 ここで注意すべきなのは、 「/」ディレクトリは元の chrooted なディレクトリのままという点だろう。 つまり chroot / してしまうと、 元の chrooted なディレクトリへ chroot してしまう (つまり何も変わらない)。

だから「/」を使わずに、 「chdir ..」で一段ずつディレクトリ階層を上っていって 「本物」の / にたどり着かねばならない。 上記コード中の while ループが「一段ずつ上っていく」処理である。 「本物」の / にたどりついたら chroot . する (くどいようだがここで chroot / してはいけない)。

試しに脱出してみる:

ikeda:/ # chroot /tmp/chroot /bin/sh
# ls -laR /
/:
drwxr-xr-x    3 0        0              29 Sep 28 17:05 .
drwxr-xr-x    3 0        0              29 Sep 28 17:05 ..
drwxr-xr-x    2 0        0              38 Sep 28 16:21 bin
-rwxr-xr-x    1 0        0         2111689 Sep 28 17:02 escape

/bin:
drwxr-xr-x    2 0        0              38 Sep 28 16:21 .
drwxr-xr-x    3 0        0              29 Sep 28 17:05 ..
-rwxr-xr-x    1 0        0         1392832 Sep  1 11:24 busybox
lrwxrwxrwx    1 0        0               7 Sep 28 16:21 ls -> busybox
lrwxrwxrwx    1 0        0               7 Sep 28 16:21 sh -> busybox
# ./escape /bin/sh
Now escaping from chrooted /tmp/chroot
sh-3.00# ls -la /tmp/chroot
total 2068
drwxr-xr-x 3 root root      29 Sep 28 17:05 .
drwxrwxrwt 8 root root    4096 Sep 28 17:05 ..
drwxr-xr-x 2 root root      38 Sep 28 16:21 bin
-rwxr-xr-x 1 root root 2111689 Sep 28 17:02 escape
sh-3.00#
More...
Filed under: プログラミングと開発環境 — hiroaki_sengoku @ 17:19
2007年9月27日

Linux でシンボリックリンクのタイムスタンプを変更する (utimensat システムコールを使って) hatena_b

Linux 2.6.22 以前は、 シンボリックリンク (symbolic link) のタイムスタンプ (time stamp) を 変更することが出来なかった。 Linux (に限らず unix のほとんど全て) では、 シンボリックリンクも普通のファイルと同様、 アクセス日時と更新日時のタイムスタンプを保持している。 BSD な unix などでは、 lutimes システムコールを使ってシンボリックリンクのタイムスタンプを変更できる。

ところが、Linux には lutimes システムコールが存在しない。 したがってシンボリックリンクのタイムスタンプは、 そのシンボリックリンクを作成した時刻のままである。 tar などでアーカイブからリストアする場合や、 rsync などでディレクトリをコピーする場合などでも、 シンボリックリンクだけは元のタイムスタンプが復元されず、 復元した時刻のシンボリックリンクが作成されるので、 不便なことこのうえない。

Linux 2.6.22 になって、 utimensat システムコールが新設された。

Ulrich Drepper (glibc のメンテナ) は、 futimesat インターフェイスでは機能が足りていないということを理由に、 utimensat システムコールを提案しました。
futimesat は、inode のアクセス・変更時間を設定するためのシステムコールです。 struct timeval をパラメータとして受け取るため、 マイクロ秒単位で設定します。 一方、inode の各種情報を取得する stat というシステムコールでは、 ナノ秒単位で取得できるようになっています。 つまり、そもそも設定できない精度で情報を取得できるような仕組みに なっているわけです。
この問題を解決するために、 パラメータとして struct timespec (ナノ秒単位) を利用できる utimensat というシステムコールを用意することになりました。 このシステムコールは Linux カーネル 2.6.22-rc1 でマージされました。

この記事では、 ナノ秒単位で設定できることばかり強調しているが (私の感覚だとマイクロ秒がナノ秒になっても、あまり嬉しくない ;-)、 utimensat で機能拡張されたのはそれだけではない。

int utimensat(unsigned int dfd, char *filename, struct timespec *t, int flags);

引数が「struct timespec」になってナノ秒単位で設定できるようになったわけだが、 「utimensat」という名称から推測される通り、 他の *at (末尾に「at」がつく) システムコールと同様、 引数として与えたパス名 (char *filename) の扱いを指定できる。 /usr/include/fcntl.h には次のように書いてある。

#ifdef __USE_ATFILE
# define AT_FDCWD                -100        /* Special value used to indicate
                                           the *at functions should use the
                                           current working directory. */
# define AT_SYMLINK_NOFOLLOW        0x100        /* Do not follow symbolic links.  */
# define AT_REMOVEDIR                0x200        /* Remove directory instead of
                                           unlinking file.  */
# define AT_SYMLINK_FOLLOW        0x400        /* Follow symbolic links.  */
# define AT_EACCESS                0x200        /* Test access permitted for
                                           effective IDs, not real IDs.  */
#endif

utimensat の第四引数 int flags に「AT_SYMLINK_NOFOLLOW」を与えれば、 シンボリックリンクを「たどらず」に、 シンボリックリンクそのものに対して、 タイムスタンプの変更を行なうことができそうである (ちなみに futimesat は第三引数までしかなく int flags を指定できない)。

さっそく実験してみる:

int utimensat(int dfd, char *filename,
              struct timespec *utimes, int flags) {
    register unsigned int ret;
    asm volatile (
        "movl %1, %%eax\n\t"
        "call *%%gs:%P2\n\t"
        : "=a" (ret)
        : "i" (320), "i" (16),
          "b" (dfd), "c" (filename), "d" (utimes), "S" (flags)
        : "memory", "cc");
    return (long)ret;
}

手元の Linux マシンの glibc では、 utimensat を呼び出せるようにはなっていなかったので、 glibc のソースを参考にしながら utimensat システムコールを呼び出す関数を書いてみた。 コード中「320」などとハードコーディング (^^;) している数値は、 linux/include/asm-i386/unistd.h 中の、

#define __NR_utimensat                320

を意味している。 これで utimensat システムコールをユーザ空間から呼び出せるようになった。 もちろん、utimensat をサポートしている glibc であれば、 このような関数をデッチあげるまでもなく、 そのまま普通に utimensat を呼び出せばよい。

さっそく使ってみる:

#include <stdio.h>
#include <stdlib.h>
#define __USE_ATFILE
#include <fcntl.h>
#undef __USE_MISC
#include <sys/stat.h>

int main(int argc, char *argv[]) {
    int i;
    for (i=1; i < argc; i++) {
        struct stat lst, st;
        if (lstat(argv[i], &lst)) {
            printf("Can't find: %s\nUsage: %s <file>...\n", argv[i], argv[0]);
            exit(1);
        }
        if (S_ISLNK(lst.st_mode) && !stat(argv[i], &st)) {
            struct timespec ts[2];
            ts[0].tv_sec = st.st_atime;
            ts[0].tv_nsec = st.st_atimensec;
            ts[1].tv_sec = st.st_mtime;
            ts[1].tv_nsec = st.st_mtimensec;
            if (utimensat(AT_FDCWD, argv[i], ts, AT_SYMLINK_NOFOLLOW) < 0) {
                printf("Failed to utimensat %s %ld.%09ld\n",
                       argv[i], ts[1].tv_sec, ts[1].tv_nsec);
            }
        }
    }
    return 0;
}

シンボリックリンクのタイムスタンプを、 シンボリックリンク先のファイル (or ディレクトリ、その他) のタイムスタンプと 一致させるプログラムである。 上記ソースプログラム (utimensat 関数と main 関数) を ltouch.c という名前で保存し、 「gcc -o ltouch ltouch.c」などとコンパイルした後、
「ltouch シンボリックリンクのパス名」などと実行する。

senri # ls -lt /usr/i486-linuxaout/lib/
total 20000
lrwxrwxrwx 1 root    root      16 Dec 22  2005 libPEX5.so.1 -> libPEX5.so.1.1.0*
lrwxrwxrwx 1 root    root      14 Dec 22  2005 libPEX5.so.6 -> libPEX5.so.6.0*
lrwxrwxrwx 1 root    root      15 Dec 22  2005 libX11.so.3 -> libX11.so.3.1.0*
lrwxrwxrwx 1 root    root      13 Dec 22  2005 libX11.so.6 -> libX11.so.6.0*
lrwxrwxrwx 1 root    root      13 Dec 22  2005 libXIE.so.6 -> libXIE.so.6.0*
lrwxrwxrwx 1 root    root      15 Dec 22  2005 libXaw.so.3 -> libXaw.so.3.1.0*
lrwxrwxrwx 1 root    root      13 Dec 22  2005 libXaw.so.6 -> libXaw.so.6.0*
lrwxrwxrwx 1 root    root      15 Dec 22  2005 libXpm.so.3 -> libXpm.so.3.3.0*
lrwxrwxrwx 1 root    root      13 Dec 22  2005 libXpm.so.4 -> libXpm.so.4.3*
lrwxrwxrwx 1 root    root      14 Dec 22  2005 libXt.so.3 -> libXt.so.3.1.0*
lrwxrwxrwx 1 root    root      12 Dec 22  2005 libXt.so.6 -> libXt.so.6.0*
lrwxrwxrwx 1 root    root      18 Dec 22  2005 libcurses.so.0 -> libcurses.so.0.1.2*
lrwxrwxrwx 1 root    root      15 Dec 22  2005 libdb.so.1 -> libdb.so.1.85.1*
lrwxrwxrwx 1 root    root      10 Dec 22  2005 libdbm.sa -> libgdbm.sa
lrwxrwxrwx 1 root    root      16 Dec 22  2005 libdosemu -> libdosemu-0.60.1
lrwxrwxrwx 1 root    root      14 Dec 22  2005 libe2fs.so.1 -> libe2fs.so.1.0*
lrwxrwxrwx 1 root    root      13 Dec 22  2005 libe2p.so.1 -> libe2p.so.1.0*
lrwxrwxrwx 1 root    root      12 Dec 22  2005 libet.so.1 -> libet.so.1.0*
lrwxrwxrwx 1 root    root      12 Dec 22  2005 libgr.so.1 -> libgr.so.1.3*
lrwxrwxrwx 1 root    root      12 Dec 22  2005 libss.so.1 -> libss.so.1.0*
lrwxrwxrwx 1 root    root      14 Dec 22  2005 libtcl.so.3 -> libtcl.so.3.1j*
lrwxrwxrwx 1 root    root      13 Dec 22  2005 libtk.so.3 -> libtk.so.3.1j*
lrwxrwxrwx 1 root    root      16 Dec 22  2005 libvga.so.1 -> libvga.so.1.0.11*
        ...(後略)...

古いディレクトリだと、 このように同じタイムスタンプのシンボリックリンクばかり並んでしまって、 見にくいことこの上ない (おそらく 2005年12月22日に、ハードディスクを入れ替えたのだろう) のであるが、 「ltouch *」を実行すると、

senri # ltouch /usr/i486-linuxaout/lib/*
senri # ls -lt /usr/i486-linuxaout/lib/
total 20000
lrwxrwxrwx 1 root    root      14 Sep 11  1995 libPEX5.so.6 -> libPEX5.so.6.0*
-r-xr-xr-x 1 root    root  234500 Sep 11  1995 libPEX5.so.6.0*
lrwxrwxrwx 1 root    root      13 Sep 11  1995 libXIE.so.6 -> libXIE.so.6.0*
-r-xr-xr-x 1 root    root   58372 Sep 11  1995 libXIE.so.6.0*
lrwxrwxrwx 1 root    root      13 Sep 11  1995 libXaw.so.6 -> libXaw.so.6.0*
-r-xr-xr-x 1 root    root  209924 Sep 11  1995 libXaw.so.6.0*
lrwxrwxrwx 1 root    root      12 Sep 11  1995 libXt.so.6 -> libXt.so.6.0*
-r-xr-xr-x 1 root    root  320516 Sep 11  1995 libXt.so.6.0*
lrwxrwxrwx 1 root    root      13 Sep 11  1995 libX11.so.6 -> libX11.so.6.0*
-r-xr-xr-x 1 root    root  529412 Sep 11  1995 libX11.so.6.0*
lrwxrwxrwx 1 root    root      16 Jul 29  1995 libdosemu -> libdosemu-0.60.1
-rw-r--r-- 1 root    root  630973 Jul 29  1995 libdosemu-0.60.1
-rw-r--r-- 1 root    root   28912 Mar 19  1995 libdes.a
lrwxrwxrwx 1 root    root      14 Feb 28  1995 libe2fs.so.1 -> libe2fs.so.1.0*
-rwxr-xr-x 1 root    root   84035 Feb 28  1995 libe2fs.so.1.0*
lrwxrwxrwx 1 root    root      13 Feb 28  1995 libe2p.so.1 -> libe2p.so.1.0*
-rwxr-xr-x 1 root    root   51633 Feb 28  1995 libe2p.so.1.0*
lrwxrwxrwx 1 root    root      12 Feb 28  1995 libet.so.1 -> libet.so.1.0*
-rwxr-xr-x 1 root    root   56437 Feb 28  1995 libet.so.1.0*
lrwxrwxrwx 1 root    root      12 Feb 28  1995 libss.so.1 -> libss.so.1.0*
-rwxr-xr-x 1 root    root   63306 Feb 28  1995 libss.so.1.0*
lrwxrwxrwx 1 root    root      18 Feb 19  1995 libcurses.so.0 -> libcurses.so.0.1.2*
-rwxr-xr-x 1 root    root   49152 Feb 19  1995 libcurses.so.0.1.2*
        ...(後略)...

このように、 シンボリックリンクとリンク先ファイルが同じタイムスタンプになるので、 「ls -lt」などと更新日時でソートすれば、 リンクとリンク先が隣り合わせになって見やすくなる。

Filed under: プログラミングと開発環境 — hiroaki_sengoku @ 09:22
2007年9月18日

NFS と AUFS (Another Unionfs) を使って、ディスクレス (diskless) サーバ群からなる低コスト・高可用な大規模負荷分散システムを構築する hatena_b

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

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

initramfs (initrd) の init を busybox だけで書いてみた hatena_b

linux をブートさせる際、 さまざまな PC に対応させようとすると、 多くのデバイスドライバをカーネルに組み込んでおかねばならない。 つまりルートファイルシステム (root file system, 以下 rootfs と略記) をマウントするまでは、 その rootfs 上にインストールしたモジュール群 (/lib/modules/ の下に置いた *.ko ファイル群) を読めないからだ。 rootfs さえマウントできてしまえば、 あとはいくらでもモジュールを必要に応じて読み込むことができるようになる。 だから、さまざまなハードウェアへの対応といっても、 重要なのは rootfs をマウントするまで、である。

しかしながら、 rootfs をマウントするまでの辛抱といっても、 rootfs をマウントするにはハードディスクを認識しなければならないし、 それには ATA ドライバやら SCSI ドライバやら、 果ては AHCI ドライバなどが、 ハードウェアに応じて必要になる。

個人で管理する PC の全てのハードウェアに対応させるだけなら、 全てのドライバをあらかじめカーネルに読み込んでおくのもアリだろう。 しかし汎用的なディストリビューションなど、 (カーネル再構築を行なわずに) 多くのハードウェアに対応する必要がある場合は、 必要になるかも知れない全てのドライバを、 あらかじめカーネルに読み込んでおくなどということは非現実的である。

ちなみに私は、1993年頃から linux を使っているが、 いまだに当時インストールした slackware を使用し続けている。 もちろん kernel や libc をはじめとして、 ほぼ全てのソフトウェアをアップデートしてしまっているし、 しかも起動スクリプトをはじめとして、 あらゆる設定を好き勝手に書き換えてしまっているので、 インストールしてから 10年以上たった今となっては、 元の slackware の痕跡は全くといっていいほど残っていない。 もはや、私独自のディストリビューションと呼んでしまっても差し支えないだろう。 私が個人的に管理しているマシンには、 全てこの「my distribution」をインストールしている。 そんなわけで、私は「普通の」ディストリビューションを使ったことがない。 initrd がディストリビューションの「常識」となってからも、 私は initrd は使わずに、 自分が管理する PC のハードウェアに合わせてカーネルを再構築して使ってきた。

そこで linux では、 initrd (Initial RAM Disk) という仕掛けが使われてきた。 すなわちハードディスクを rootfs としてマウントする前に、 一時的にマウントする「ミニ」ルート (mini root) である。 このミニルートには、 ハードディスク (あるいは 1CD Linux の場合であれば CD だし、 ネットワークブートする場合であれば NFS サーバ) を rootfs としてマウントするのに必要となる可能性がある モジュール群一式を置いておき、 ハードウェアに応じて必要なモジュールをミニルートから読む。 そして、ハードディスクをマウントして、 / (ルート) をミニルートからハードディスクへ切り替える。

ただ、この initrd は少々扱いが面倒くさい。 initrd は RAM ディスクという「本物の」ブロックデバイスなので、 「本物の」ファイルシステム (例えば ext2) で mkfs しなければならない。 initrd にモジュールを追加しようとすれば、 initrd イメージを (losetup コマンドを使って) ループバックデバイス経由でマウントして内容を書き換えなければならないし、 たくさんのモジュールを追加した結果、 もしファイルシステムが一杯にでもなったりしたら、 initrd イメージのサイズを大きくして mkfs からやり直しである。

メンドクサイだけでなく、 RAM ディスク自体が非効率なものであるようで、 ファイルからブロックデバイスを作る方法としては、 すでに「semi-obsolete」とまで言われてしまっているようである:

Another reason ramdisks are semi-obsolete is that the introduction of loopback devices offered a more flexible and convenient way to create synthetic block devices, now from files instead of from chunks of memory. See losetup (8) for details.
linux/Documentation/filesystems/ramfs-rootfs-initramfs.txt から引用

というわけで、initrd に代わる仕掛けとして、 linux kernel 2.6 からは initramfs と呼ばれる仕掛けが導入された。 すなわち RAM ディスクというブロックデバイスを用いるのではなく、 RAM 上に直接ファイルシステムを作る ramfs を用いた「ミニルート」である。 私自身は今まで initrd を使っていなかったのであるが、 cpio アーカイブを作るだけでいいというのは、 とても手軽であるように思えたし、 カーネルにどんどんドライバを組み込んで肥大化させるよりは、 initramfs を使う方がヨサゲである (もちろん、どんどんドライバを組み込めば、 initramfs が肥大化するのだが、 カーネルが肥大化するデメリットとは比較にならない) ように感じてきたので、 宗旨替えすることにした。

initrd initramfs
イメージ ファイルシステム (ext2など + gzip) アーカイブ (cpio + gzip)
実装 ブロックデバイス (RAM ディスク) ファイルシステム
実行 /linuxrc /init
rootfs
マウント
適当なディレクトリへマウントして
pivot_root
/ へマウント (switch_root)
init 起動 /linuxrc 終了後、カーネルが起動 /init が exec /sbin/init する

ブートパラメータとして「initrd=」を与えると、 ブートローダがイメージをメモリ上に読み込んでカーネルに渡す。 するとカーネルはそのイメージがファイルシステムなのか、 cpio アーカイブなのか調べる。 もしファイルの magic number が cpio であれば、 ramfs としてマウントする。 そして /init が実行可能ならば、 initramfs として扱い、 /init を起動する。

以上の条件が一つでも成立しない場合、 すなわち cpio アーカイブでない場合や、 /init が実行できない (/init が存在しない) 場合は、 initrd 扱いになるので注意が必要である。 すなわち RAM ディスクとしてマウントしようとするので、 カーネルに RAM ディスクドライバが組み込まれていなかったり、
「root=/dev/ram0」カーネルパラメータを指定していなかったりすると、 kernel panic を起こす。

実は、initramfs として起動できるようになるまで、 かなりハマってしまった。 まず、cpio アーカイブを作るところで、いきなりハマった。

(cd /usr/src/initramfs/; find . | cpio -o -H newc ) | gzip > initrd.gz

などとしてアーカイブを作ればいいだけの話なのであるが、 このコマンドラインを /bin/csh 上で実行したために、 アーカイブの先頭にゴミが入ってしまった。 つまり、

senri:/ % (cd /usr/src/initramfs/; find . | cpio -o -H newc ) | cpio -tv | head
cpio: Malformed number 0000000.
cpio: Malformed number 000000.
cpio: Malformed number 00000.
cpio: Malformed number 0000.
cpio: Malformed number 000.
cpio: Malformed number 00.
cpio: Malformed number 0.
cpio: Malformed number .
cpio: warning: skipped 56 bytes of junk
drwxr-xr-x  13 root     root            0 Aug 27 17:56 .
drwxr-sr-x   2 root     root            0 Aug 25 10:00 bin
-rwxr-xr-x   1 root     root      1392832 Aug 25 18:36 bin/busybox
lrwxrwxrwx   1 root     root            7 Aug 25 10:09 bin/addgroup -> busybox
lrwxrwxrwx   1 root     root            7 Aug 25 10:09 bin/adduser -> busybox
lrwxrwxrwx   1 root     root            7 Aug 25 10:09 bin/ash -> busybox
lrwxrwxrwx   1 root     root            7 Aug 25 10:09 bin/cat -> busybox
lrwxrwxrwx   1 root     root            7 Aug 25 10:09 bin/catv -> busybox
lrwxrwxrwx   1 root     root            7 Aug 25 10:09 bin/chattr -> busybox
lrwxrwxrwx   1 root     root            7 Aug 25 10:09 bin/chgrp -> busybox

何が起きているかお分かりだろうか? お恥ずかしながら、アーカイブにゴミが混入していると気づくまで、 何度も kernel を panic させてしまった。 シェルのカスタマイズをやりすぎるとロクなことにならない、 という典型例なのかも (^^;)。

senri:/ % (cd /usr/src/initramfs/; echo test) | od -t a
0000000 esc   E   m   A   c   S   c   d  sp   /   u   s   r   /   s   r
0000020   c   /   i   n   i   t   r   a   m   f   s  nl esc   E   m   A
0000040   c   S   c   d  sp   /   u   s   r   /   s   r   c   /   i   n
0000060   i   t   r   a   m   f   s  nl   t   e   s   t  nl
0000075
senri:/ % alias cd
set back="$cwd";chdir !*;if(!* =~ "..")set cwd="$back:h";chdir "$cwd";setProm
senri:/ % alias setProm
set prompt="${HOST}:${cwd} $prompt_tail_char "

この alias 設定は、 もうかれこれ 10年以上使い続けてきた設定。 こんな形で悪さをするとは... orz

ようやくマトモなアーカイブを作れたと思ったら、 今度は以下のような Kernel panic が起きた:

Unpacking initramfs... done
Freeing initrd memory: 1412k freed
...(中略)...
No filesystem could mount root, tried:
Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(8,1)

「Unpacking initramfs... done」と出ているのだから、 cpio アーカイブはちゃんと展開できて ramfs としてマウントできているはず。 なぜに「Unable to mount root fs」なのか、 と思っていたのだが、 これは initramfs に「/init」が無いためだった (エラーメッセージが不親切杉!)。 initrd みたいなものだろうと思って、 起動スクリプトを「/linuxrc」というファイル名で作っていたのが敗因。

「/init」がないと initrd 扱いになってしまい、 RAM ディスクを mount しようとしていたが、 RAM ディスクドライバが組み込まれていなかったのでマウントできない、 というのが、このエラーメッセージの主旨だったようだ (素直に /init が見つからない、って言ってくれればいいのに...)。 「/linuxrc」を「/init」へファイル名変更してみると、 あっさり initramfs 上での起動に成功した。

% ls -lt /boot/*-2.6.20.*
-rw-r--r-- 1 root root 1643881 Sep  3 07:55 /boot/initz-2.6.20.18
-rw-r--r-- 1 root root 1193392 Sep  1 23:10 /boot/linuz-2.6.20.18
-rw-r--r-- 1 root root 2017936 May 12 19:31 /boot/linuz-2.6.20.11

2.6.20.11 を make したときは、 rootfs のマウントに必要なドライバを全てカーネルに詰め込んでいたのに対し、 2.6.20.18 では、 マウントに必要なドライバは極力 initramfs に入れた。 これにより linuz 単体のサイズは半分近くに減っている。 linuz (カーネル) + initz (initramfs) の合計サイズは 2.6.20.11 に比べ増えてしまっているが、 これは busybox だけで 1.3MB ほどあるため。 とはいえ、 initramfs 内では busybox よりも lib/modules 以下のサイズの方が倍ほど大きいので、 非効率というほどでもない。

senri:/usr/src/initramfs % ls -l bin/busybox
-rwxr-xr-x 1 root root 1392832 Sep  1 11:24 bin/busybox
senri:/usr/src/initramfs % du --max-depth=2 --byte
1397502        ./bin
4740        ./sbin
6204        ./usr/bin
4334        ./usr/sbin
8226        ./usr/share
22860        ./usr
4096        ./dev/pts
4096        ./dev/shm
12338        ./dev
4108        ./etc
4096        ./mnt
2422679        ./lib/modules
2426775        ./lib
4096        ./proc
4096        ./tmp
4096        ./var
4096        ./sys
3895272        .

じゃ、いよいよハードウェアの自動認識をして、 適切なモジュールのみを組み込むようにしてみようかと思って、 いろいろ探してみたのだが、 どうしたことか適当なスクリプトが見当たらない。 1CD Linux の /linuxrc をいろいろ読んでみたのだが、 いまいちパッとするものがない。 デバイスの ID などがゴリゴリ書いてあるものが大半で、 どれもアドホックすぎるように思えたのである。

かといって、ハードウェアの認識を udev などに行なわせる、 というのは牛刀過ぎるように思えた。 なんたって init を起動する前のブートストラップなのである。 目的は rootfs をマウントするだけなのであるから、 あまりに汎用的な仕掛けは、いかがなものかと思うのである。

というわけで、 busybox だけでハードウェアの自動認識 & モジュール読み込みを実現することを 目標にしてみた。 前フリが長くなった (長すぎ!) が、ようやくここからが本題である。

More...
2007年8月24日

Advanced/W-ZERO3[es] アドエスを非常用の起動ディスクにしてみる hatena_b

ノートPC を持ち歩いていると、たまに起動ディスクが欲しくなる。 カーネルや起動スクリプト、あるいはブートローダなどをいじっていて、 起動しなくなることが (しばしば ^^;) あるからだ。 ハードディスクから起動しなくなってしまうと、 起動ディスクが最後の手段となる (昔のノートPC では、 分解してハードディスクを取り出して内容を修正する、 という手段を使ったこともあったが、 最近のノートだと「ハードディスク保護」の仕掛けがあるようなので難しいだろう)。

昔は起動ディスクと言えばフロッピーディスクだった。 slackware の救急用ディスクを常備していた人も多いだろう。 そして今でも最後の最後の手段としてフロッピーディスクは万能である (実はつい最近、昔の VAIO C1 (無印) をいじっていたら起動しなくなって、 フロッピーディスクのお世話になってしまった)。 最近だとフロッピーディスクドライブがついているノート PC は、 ほとんど皆無だろう。 大抵のノートPC は、 USB 接続のフロッピーディスクドライブを使って起動できるとは思うが、 非常用起動ディスクのためだけにドライブを持ち歩くのもあまり現実的ではない。

フロッピーディスクの次に確実なブート手段というと、 CD/DVD ROM だろう。 CD からブートできない PC は、 さすがに最近ではほとんど見かけなくなった。 前述の VAIO C1 も CD からもブートできるのだが、 ドライブが悪いのか CD が悪いのか分からないが、 読み取りエラーが多発してブートできなかった。 滅多に使わないドライブだと光学系が経年劣化してしまうのか、 いつのまにか使えなくなってしまうので注意が必要である。

CD/DVD ドライブが内蔵のノートであれば、 Knoppix などの 1CD Linux を持ち歩けばよい。 12cm CD を常に持ち運ぶのが面倒という場合は、 名刺サイズの CD-R もある。 職場や自宅に何枚か 1CD Linux を常備している人も多いだろう。

私が普段持ち歩いているノートPC は、 レッツノート CF-R6MWVAJP (CF-W2FW6AXR から乗り換えた) なので CD/DVD ドライブが外付けである。 長期の旅行などでなければドライブを持ち歩くことはない。 なので、CF-W2 から CF-R6 へ乗り換えたのを機に、 非常用起動ディスクとして USB フラッシュメモリを使うようになった。 CD に比べコンパクトな上に起動時間も早いし、 (ライトワンスな CD-R と比べて) 内容の変更も手軽に行なえるので便利である。

しかし便利になると、もっと便利さを求めたくなるのがサガというもので、 USB メモリを常に持ち歩くのが面倒になってきた。 できたら常に持ち歩いているケータイを起動ディスクにできないだろうか?

幸い、私が使っているケータイ Advanced/W-ZERO3[es] (アドエス) は、 WM5torage を使うと、 アドエスを USB 大容量記憶デバイス (USB Mass Storage device) として使うことができるようなので、 syslinux を使って、 アドエスにブートローダを書込んでみると、 あっさり起動ディスクとして使えてしまった。 アドエスには 2GB の microSD を入れてあるので、 その片隅に 1CD Linux を入れておいても容量的には大して問題にならない。 常に持ち歩いているケータイをそのまま非常用の起動ディスクとして使えるので、 とても便利である。

- o -

と、これだけでは内容が無い (^^;) ので、 1CD Linux を USB メモリに入れて使う方法の解説などを... USB メモリへの書込むためのスクリプトを用意している 1CD Linux もあるようだが、 以下に説明するように 1CD Linux の起動の仕組みさえ理解していれば、 必要なファイル (基本的には、 カーネルと initrd と圧縮ディスクイメージの 3 つ) を USB メモリへコピーするだけの作業である。

むしろ CD に書込んで (仮想マシンを使えば書込まずに済むが、 面倒であることにはかわり無い) 1CD Linux をブートさせ、 書込みスクリプトを実行するよりは、 1CD Linux の ISO イメージから必要なファイルを取り出してコピーする方が、 手軽と言えるのではないだろうか? (もちろん万人向けの方法ではないが)

1CD Linux の起動は、おおむね次のようなステップを経る:

  1. BIOS が CD メディア上のブートローダを実行
  2. ブートローダが起動メニュー等を表示
    ユーザがブートパラメータを必要に応じて編集
  3. ブートローダがカーネル (vmlinuz など) と initrd を読み込む
    CD からの読み込みは、BIOS 経由
  4. ブートローダがカーネルを起動
  5. カーネルが initrd を RAM ディスクに展開し、ルートとしてマウント
  6. カーネルが RAM ディスク上の /linuxrc を実行
  7. /linuxrc がブロックデバイスをスキャン
    圧縮ディスクイメージ (/KNOPPIX/KNOPPIX など) が存在する ブロックデバイスを探す
  8. /linuxrc が見つけたブロックデバイスをマウント
  9. /linuxrc がマウントしたファイルシステム上の圧縮ディスクイメージをマウント
  10. /linuxrc の実行が終了すると、 通常の起動プロセス (つまり init の実行) が始まる

1CD Linux を USB メモリに入れるには、 上記ステップ 1. と 8. において、CD ではなく USB メモリが対象となるようにすればよい。 ステップ 1. は、BIOS による CD メディアの認識であり、 ステップ 8. は、カーネルによる CD メディアの認識である。 同じ CD メディアではあるが、認識を行なうプログラムが異なるので、 ステップ 7. で認識可能な全てのブロックデバイスをスキャンして、 自メディアを見つける必要がある。

なお、Puppy Linux の場合は、 ブートパラメータとして PMEDIA= を指定することにより、 スキャンするデバイスの種類を限定できる。 USB メモリに限定したいときは、「PMEDIA=usbflash」を指定すればよい。

スキャンしたデバイスが、 自メディアか否かを確認する方法は様々であるが、 おおむね特定のファイルが存在するか否かで判断している。 例えば、以下のような感じ:

1CD Linux確認方法
Knoppix /KNOPPIX/KNOPPIX knoppix_dir=, knoppix_name= で変更可能
Damn Small Linux /KNOPPIX/KNOPPIX knoppix_dir=, knoppix_name= で変更可能
INSERT /INSERT/INSERT insert_dir=, insert_name= で変更可能
Puppy Linux /pup_216.sfs 「216」の部分はバージョン番号
SLAX /livecd.sgn

ほとんどの 1CD Linux で、 圧縮ディスクイメージ (KNOPPIX, INSERT, pup_216.sfs など) の存在を 調べることによって自メディアを探し出しているが、 SLAX (Linux Live system) のように、 チェック用ファイル /livecd.sgn を CD に置いているケースもある。 /livecd.sgn は 243 バイトほどのファイルで、 以下のような内容である:

All available discs and CDROMs are mounted during the boot process.
When done, linuxrc script is looking for this livecd.sgn file.
It tells linuxrc where to mount the live filesystem from.

Don't delete this file, else your LiveCD won't work.

「Don't delete this file, else your LiveCD won't work.」と書いてある通り、 このファイルが存在しないと、 /linuxrc が SLAX のメディアを見つけられなくなってしまう。

これらの特定のファイルが存在するメディアであれば、 それがブートローダを読み込んだメディアと異なるメディアであっても、 構わず圧縮ディスクイメージをマウントして Linux が起動する。 だから例えば PXE でネットワーク経由 (tftp) で Knoppix のカーネルと initrd を読み込んで、 Knoppix CD メディアから起動させる、といったようなこともできる。 つまり、 カーネルが起動するまで認識できないデバイス (BIOS が USB 接続された CD/DVD ドライブを認識できない場合など) から起動させることが可能になる。

どのように自メディアを確認しているかは initrd の中の /linuxrc を読めば分かるのだが、 サイズ削減のためにカーネルの機能を絞っている場合があるので注意が必要である。 例えばINSERT v1.3.9b の場合、 カーネルが VFAT ファイルシステムをサポートしているにもかかわらず、 「Codepage 437」(VFAT ファイルシステムのデフォルト) をサポートしていないので、 VFAT ファイルシステムをマウントしようとしても失敗してしまう。 つまり圧縮ディスクイメージを VFAT ファイルシステム上に置くことができない。 /linuxrc 中には、

 case "$fs" in vfat)
  # We REALLY need this for INSERT on DOS-filesystems
  shortname="shortname=winnt"
  [ -n "$options" ] && options="$options,$shortname" || options="-o $shortname"
 ;;
 esac
 mount -t $fs $options $1 $2 >/dev/null 2>&1 && return 0

「INSERT on DOS-filesystems」という記述があるにもかかわらず、 カーネルに「Codepage 437」機能を組み込んでいないのはバグだと思われる。 このバグを回避するため、INSERT を USB メモリに入れるのであれば、 (1) USB メモリ全体を (FAT ではなく) ext2 でフォーマットするか、あるいは (2) USB メモリに複数のパーティションを作って、 圧縮ディスクイメージ INSERT を置くパーティションは ext2 でフォーマットする、 などとして カーネルがマウントできるファイルシステム (つまり ext2) 上に、 圧縮ディスクイメージを置く必要がある。 (1) の場合は、ブートローダとして extlinux を使えばよい。 ただし PC によっては、 FAT 以外でフォーマットした USB メモリではブートできない場合もあるので、 (2) のほうがお勧め。

さて、 ステップ 1. の BIOS によるブートローダの実行であるが、 大半の 1CD Linux では CD のブートローダとして isolinux が使われている。 isolinux というのは syslinux に含まれる、 CD-ROM 用のブートローダ。 syslinux には以下のブートローダが含まれる:

ブートローダブートメディア
syslinuxMS-DOS/Windows FAT ファイルシステム
pxelinuxPXE ネットワークブート
isolinuxISO9660 CD-ROM
extlinuxLinux ext2/ext3 ファイルシステム

各ブートローダは、 設定ファイル (isolinux の場合であれば isolinux.cfg) の書式が共通なので、 ブートメディアを変更しても、 設定ファイルはほとんど修正する必要がない。 1CD Linux を USB メモリ (FAT ファイルシステム) に入れる場合であれば、 設定ファイルのファイル名を isolinux.cfg から syslinux.cfg に変更するだけでよい。

1CD Linux から必要なファイルを USB メモリへコピーし、 syslinux コマンドを使ってブートローダを USB メモリへ書込めば、 Linux ブート可能な USB メモリを作ることができる。

参考までに、 私が SLAX 日本語版 を アドエスに入れた例を紹介する。 まず以下のファイルを SLAX 日本語版 CD-ROM からアドエスの microSD へ (WM5torage を activate した状態で) コピーした (もちろん memtest は、使わないならコピーする必要はないし、 起動画面の画像やヘルプ画面も、表示する必要がなければ不要)。

SLAX CD-ROM アドエス microSD
/isolinux.cfg /boot/syslinux/syslinux.cfg syslinux 設定ファイル
/boot/memtest /boot/syslinux/memtest memtest プログラム
/boot/vmlinuz /boot/syslinux/slax/vmlinuz カーネル
/boot/initrd.gz /boot/syslinux/slax/initrd initrd
/boot/splash.cfg /boot/syslinux/slax/splash.cfg SLAX 起動画面
/boot/splash.lss /boot/syslinux/slax/splash.lss SLAX 起動画面の画像
/boot/splash.txt /boot/syslinux/slax/splash.txt F1 を押したときのヘルプ画面
/boot/splash2.txt /boot/syslinux/slax/splash2.txt F2 を押したときのヘルプ画面
/livecd.sgn /livecd.sgn SLAX チェック用ファイル
/base/* /base/* SLAX のベースモジュール
/modules/* /modules/* SLAX のモジュール

syslinux 3.35 から、 設定ファイルをメディアのルートディレクトリだけでなく、 /boot/syslinux ないし /syslinux にも 置くことができるようになった。 microSD のルートディレクトリにたくさんのファイルを置くと混乱の元なので、 /boot/syslinux に置くことにした。 この場合、設定ファイルに相対パス名を書くときは、 設定ファイルを置いたディレクトリからの相対パスとなる。

SLAX CD-ROM の isolinux.cfg は、 カーネルのパス名などを相対パス名で指定しているので適宜修正する。 修正ついでということで、 SLAX 関係のファイルを /boot/syslinux/slax にまとめることにした。 こうすることにより SLAX 以外の 1CD Linux を同居させる際に混乱せずに済む (Knoppix, Damn Small Linux, Puppy Linux, SLAX を全て一本の USB メモリに 入れることも可能)。

また、SLAX 起動画面 splash.cfg は、 ファイル中に起動画面の画像 (四葉のクローバーの絵) splash.lss へのパスを指定しているので、 これも適宜修正する。

ブートローダの書き込みは、 Windows マシンで以下のように実行:

syslinux -ma -d /boot D:

「-ma」は、 「D:」ドライブ (USB フラッシュメモリのドライブ名) に MBR (マスターブートレコード) を書込むための「-m」オプションと、 そのパーティションをアクティブにするための「-a」オプションの指定。 Linux 上で syslinux コマンドを使ってブートローダを書込む場合は、 「-m」オプションが指定できないので、 別途 (dd コマンド等で) MBR を書込む必要がある。

「-d /boot」はブートローダ本体である「ldlinux.sys」を 書込むディレクトリの指定。 どこでもいいのであるが、まあ /boot あたりが無難だろう。 このコマンドを実行すると、 「/boot/ldlinux.sys」というファイルが作られる。

このブートローダは、設定ファイル syslinux.cfg を、 (/, /boot/syslinux, /syslinux の中から) 自動的に探してくれるので、 syslinux.cfg を編集したとしてもブートローダを再度書込む必要はない。 だから、別の 1CD Linux をこの USB メモリに同居させようとする場合は、 カーネルと initrd と圧縮ディスクイメージを適当なディレクトリへコピーし、 syslinux.cfg に必要な変更を加えるだけでよい。

Filed under: ハードウェアの認識と制御 — hiroaki_sengoku @ 07:24
2007年8月21日

SPF (Sender Policy Framework) チェックをパスしてしまう迷惑 (スパム) メールが増えている hatena_b

私の自宅サイト GCD (と勤務先の KLab) では、 qmail にパッチをあてて、 外部から届くメールのヘッダに SPF (Sender Policy Framework) チェックの結果を挿入するようにしている。 例えば以下のような感じ:

Received-SPF: pass (senri.gcd.org: SPF record at thelobstershoppe.com designates 89.215.246.95 as permitted sender)
Message-ID: <34f301c7df7d$2e65ece5$5ff6d759@unknown.interbgc.com>
From: "Sales Department" <sales@thelobstershoppe.com>

「Received-SPF: pass」というのは、 このメール (実は迷惑メール) の送信者のドメイン thelobstershoppe.com では、 メールを送信する「正規」の IP アドレスの集合 (SPF レコード) を公表していて、 その中にこのメールの送信元 IP アドレス 89.215.246.95 が、 含まれていることを意味する。

つまり thelobstershoppe.com ドメイン管理者の公認 IP アドレスから 迷惑メールが送信されてきたわけで、 (1) ドメイン管理者の意図に反して不正なメール送信が行なわれた (つまり管理に不備がある) か、 (2) ドメイン管理者に意図に沿って迷惑メール送信が行なわれた (つまりドメインの管理者がスパマー)、 ということになる。

ドメイン管理者の意図がどちらであるかは実際に話を聞いてみない限りは 厳密には判別不能であろうが、 仮に (1) であったとしても、 不正なメール送信を許すような管理体制のドメインからのメールは、 なるべく受取りたくないものである。

「Received-SPF: pass」な迷惑メールが送られてきたら、 基本的にはそのドメインからのメールは今後受取らないように、 送信者メールアドレスのドメインのブラックリストを更新してきた。 ところが、最近、「Received-SPF: pass」な迷惑メールを受取る機会が 妙に増えたような気がする。 いったいどんな SPF レコードなのかと調べてみると...

% foreach dom (thelobstershoppe.com scottjanos.com bode-research.com \
ambiguouslyblack.com onairrewards.com mailtong.org artoffurnitureworkshop.com)
foreach? host -t txt $dom
foreach? end
thelobstershoppe.com descriptive text "v=spf1 +all"
scottjanos.com descriptive text "v=spf1 +all"
bode-research.com descriptive text "v=spf1 +all"
ambiguouslyblack.com descriptive text "v=spf1 +all"
onairrewards.com descriptive text "v=spf1 +all"
mailtong.org descriptive text "v=spf1 mx ip4:125.187.32.0/16 ~all"
artoffurnitureworkshop.com descriptive text "v=spf1 +all"

なんと、「Received-SPF: pass」な迷惑メールのドメインの大半が、 「+all」を指定していた。 つまり、 全ての IP アドレスがメール送信 IP アドレスとして「正規」なものである、 と主張しているわけである。 こんな主張は SPF の主旨から考えると全くナンセンスであるから、 対抗措置をとらねばなるまい。

SPF レコードに「+all」を含むドメインは、 自動的に迷惑メール送信元と判断するようにしてしまおうかと (一瞬 ;-) 考えたのであるが、 設定ミス等で「+all」を指定してしまっているケースもあるかもしれない。 そこで、

diff -u spf.c.org spf.c
--- spf.c.org        2007-08-20 16:09:13.000000000 +0900
+++ spf.c        2007-08-20 16:11:43.000000000 +0900
@@ -543,7 +543,7 @@
   unsigned int filldomain  : 1;
   int defresult            : 4;
 } mechanisms[] = {
-  { "all",      0,          0,0,0,0,SPF_OK   }
+  { "all",      0,          0,0,0,0,-1   }
 , { "include",  spf_include,1,0,1,0,0        }
 , { "a",        spf_a,      1,1,1,1,0        }
 , { "mx",       spf_mx,     1,1,1,1,0        }
@@ -784,6 +784,10 @@
                                         q = spfmech(spf.s + begin, 0, 0, domain->s);
                         }
 
+                        if (q == -1) {
+                                        if (prefix == SPF_OK) q = SPF_UNKNOWN;
+                                        else q = SPF_OK;
+                        }
                         if (q == SPF_OK) q = prefix;
 
                         switch(q) {

という修正を行なってみた (qmail-1.03 + qmail-spf-rc5.patch に対するパッチ)。 これで、IP アドレスが「+all」にマッチした場合は、 「SPF_OK」の代わりに「SPF_UNKNOWN」を返すようになるので、 ヘッダには「Received-SPF: neutral」が挿入される。

例えば、haywardins.com の SPF レコードは、「v=spf1 +all」であるが、 以下のように「Received-SPF: neutral」が挿入される。

Received: from unknown (HELO 6.124.18.84.in-addr.arpa) (84.18.124.6)
  by senri.gcd.org with SMTP; 20 Aug 2007 10:22:30 +0000
X-Country: RU 84.18.124.6
Received-SPF: neutral (senri.gcd.org: 84.18.124.6 is neither permitted nor denied by SPF record at haywardins.com)
Message-ID: <456801c7e313$07cb3524$067c1254@6.124.18.84.in-addr.arpa>
- o -

上記 qmail-spf-rc5.patch もそうだが、 SPF 実装の多くが、 デフォルトで Trusted Forwarder ホワイトリストを参照する設定を推奨している。 例えば qmail-spf-rc5.patch の場合であれば、

spfrules
You can specify a line with local rules.
Local rules means: Rules that are executed before the real SPF rules for a domain would fail (fail, softfail, neutral).
They are also executed for domains that don't publish SPF entries.
I suggest adding  include:spf.trusted-forwarder.org.
You can also add mechanisms to trust known mail servers like backup MX servers, though I suggest that you should at least also use tcprules (to modify SPFBEHAVIOR).

などと、 「/var/qmail/control/spfrules」に、 「include:spf.trusted-forwarder.org」を追加することを勧めている。 つまり、spf.trusted-forwarder.org の SPF レコードを問合わせよ、 ということだが、実際に問合わせてみると、

% host -t txt spf.trusted-forwarder.org
spf.trusted-forwarder.org descriptive text "v=spf1 exists:%{ir}.wl.trusted-forwarder.org exists:%{p}.wl.trusted-forwarder.org"

というレスポンスが返ってくる。 つまり「%{ir}.trusted-forwarder.org」か「%{p}.wl.trusted-forwarder.org」の どちらかが存在していれば、 ホワイトリストに登録されていることを意味する。 すなわちその IP アドレスは「あらゆる」送信者アドレスのメールを送ることができる 「信頼されたフォワーダ」ということになる。 ここで「%{ir}」は IP アドレスを逆順にした文字列、 「%{p}」は IP アドレスを逆引きして得られるホスト名である。

どんな送信者アドレスのメールも送れる「万能」の IP アドレスというのも、 タイガイにしてほしいと思うが、 The Trusted Forwarder SPF Global Whitelist によれば、 SPF 導入初期のためのものだったようだ。 2004年ごろは、ほとんどのサイトが SPF レコードを公表していなかったわけで、 「信頼されたフォワーダ」のホワイトリストを保持しておく理由があったのだろう (いまいちその必要性がピンとこないが)。

同ページによれば、 このホワイトリストは既に更新されておらず、 早晩リストの内容が削除されるようだ。 「*.wl.trusted-forwarder.org」への無用な問合わせを避けるためにも、 「/var/qmail/control/spfrules」から 「include:spf.trusted-forwarder.org」の記述を削除すべきだろう (qmail の場合)。

Filed under: システム構築・運用 — hiroaki_sengoku @ 06:53
2007年8月16日

CPAN の IP::Country を C で書き直して MTA に組み込み、メールのヘッダに国コードを挿入するようにしてみた hatena_b

自前のブラックリストを用いて迷惑メール (spam, UBE) を排除する方法について、 「迷惑メール送信者とのイタチごっこを終わらせるために (1)」で説明した。 メールの送信元 IP アドレスが DNS で逆引きできない場合に、 その IP アドレスがブラックリストに載っているか否かを調べ、 もし載っているならその IP アドレスを 「ダイアルアップ IP アドレス」に準じる扱いにする、 という方法である。

迷惑メールを送ってきた実績 (?) がある IP アドレスブロックであれば、 ためらうこと無くブラックリストに入れてしまえるのであるが、 初めてメールを送ってきた IP アドレスブロックを、 逆引きできないという理由だけでダイアルアップ IP アドレス扱いするのは、 少々乱暴だろう。 そこで、 接続元 IP アドレスが属する国のコードをメールヘッダに挿入する仕掛けを MTA (Message Transfer Agent, メールサーバ) に作り込んでみた。例えば、

Received: from unknown (HELO unknown.interbgc.com) (89.215.246.95)
  by senri.gcd.org with SMTP; 15 Aug 2007 20:46:15 +0000
X-Country: BG 89.215.246.95
Received-SPF: pass (senri.gcd.org: SPF record at thelobstershoppe.com designates 89.215.246.95 as permitted sender)
Message-ID: <34f301c7df7d$2e65ece5$5ff6d759@unknown.interbgc.com>

といった感じで、「X-Country: 」フィールドが挿入される。 「89.215.246.95」がこのメールを送ってきたマシンの IP アドレスであり、 その前の「BG」が、 この IP アドレスが属する国 (この例ではブルガリア) の ISO 3166 コード である。

ブルガリアに知り合いがおらず、 かつこのメールがメーリングリスト宛でなく個人アドレス宛であるならば、 MUA (Message User Agent, メーラー) の設定で、 このメールを迷惑メールとして排除することが可能だろう。 あるいは逆に、 「X-Country: JP」である場合は、 迷惑メール判定の結果にかかわらず排除しないという設定にして、 必要なメールを誤って排除するのを防止することもできるだろう (日本語の迷惑メールも、大半は海外の IP アドレスから送信されている)。

IP アドレスから国コード (ISO 3166 コード) を調べるサービスはいろいろあるが、 メールを受信するたびに外部のサイトへ通信するのはあまり感心しない。 ネットワークないし外部のサイトの状況の影響を受けてしまうし、 あるいは逆に大量のメールを一時に受信したときなど、 そのサイトに迷惑をかけてしまう恐れもある。 集中して問合わせを行なってしまった、などの理由で濫用と判断され、 サービスの提供が受けられなくなってしまう可能性もある。

したがって、IP アドレスから国コードを検索するためのデータベースを 自前で持つことが望ましい。 例えば CPAN には、 IP アドレスから国コードを検索するモジュール 「IP::Countryが 登録されている。 このモジュールをインストールすると、 「/usr/lib/perl5/site_perl/5.8.8/IP/Country/Fast」ディレクトリに、 「ip.gif」と「cc.gif」というファイルがインストールされる。

% ls -l /usr/lib/perl5/site_perl/5.8.8/IP/Country/Fast
total 256
-r--r--r-- 1 perl perl    681 Feb  2  2007 cc.gif
-r--r--r-- 1 perl perl 252766 Feb  2  2007 ip.gif

「ip.gif」が、IP アドレスから国番号を検索するためのデータベースであり、 「cc.gif」が、国番号から国コード (ISO 3166 コード) への変換テーブルである。

メールを受信するたびにメールサーバで perl スクリプトを実行するのは、 メールサーバの負荷などの観点からあまり望ましくない (私のサイトではメールサーバを chroot 環境で動かしていて、 その chroot 環境には perl をインストールしていない、 というセキュリティ上の理由もある) ので、 ほとんど perl スクリプトをそのまま C に置き換えただけなので、 説明は不要だろう。 inet_ntocc 関数に限ると、 C 版のほうが perl 版より簡潔に書けてしまっている点が興味深い。 コメントは、IP/Country/Fast.pm スクリプトのコメントをそのまま入れてある。

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

#ifndef DBDIR
#define DBDIR "/usr/lib/perl5/site_perl/5.8.8/IP/Country/Fast"
#endif
#define CC_MAX        256        /* # of countries */

int inet_ntocc(u_char *ip_db, u_long inet_n) {
/*
  FORMATTING OF EACH NODE IN $ip_db
  bit0 - true if this is a country code, false if this
         is a jump to the next node
  
  country codes:
    bit1 - true if the country code is stored in bits 2-7
           of this byte, false if the country code is
           stored in bits 0-7 of the next byte
    bits 2-7 or bits 0-7 of next byte contain country code
  
  jumps:
    bytes 0-3 jump distance (only first byte used if
           distance < 64)
*/
    u_long mask = (1 << 31);
    const u_long bit0 = 0x80;
    const u_long bit1 = 0x40;
    int pos = 4;
    u_char byte_zero = ip_db[pos];
    /* loop through bits of IP address */
    while (mask) {
        if (inet_n & mask) {
            /* bit[$i] is set [binary one]
               - jump to next node
               (start of child[1] node) */
            if (byte_zero & bit1) {
                pos = pos + 1 + (byte_zero ^ bit1);
            } else {
                pos = pos + 3 + ((ip_db[pos] << 8 | ip_db[pos+1]) << 8
                                 | ip_db[pos+2]);
            }
        } else {
            if (byte_zero & bit1) {
                pos = pos + 1;
            } else {
                pos = pos + 3;
            }
        }
        /*
          all terminal nodes of the tree start with zeroth bit 
          set to zero. the first bit can then be used to indicate
          whether we're using the first or second byte to store the
          country code */
        byte_zero = ip_db[pos];
        if (byte_zero & bit0) {
            if (byte_zero & bit1) {
                /* unpopular country code - stored in second byte */
                return ip_db[pos+1];
            } else {
                /* popular country code - stored in bits 2-7
                   (we already know that bit 1 is not set, so
                   just need to unset bit 1) */
                return byte_zero ^ bit0;
            }
        }
        mask = (mask >> 1);
    }
    return -1;
}

u_char *getdb(char *file, int *fdp, int *lenp) {
    char path[PATH_MAX+1];
    int fd;
    struct stat st;
    int length;
    u_char *db;
    snprintf(path, PATH_MAX, "%s/%s", DBDIR, file);
    path[PATH_MAX] = '\0';
    fd = open(path, O_RDONLY);
    if (fd < 0) {
        fprintf(stderr, "Can't open: %s err=%d\n", path, errno);
        exit(1);
    }
    if (fdp) *fdp = fd;
    if (fstat(fd, &st) < 0) {
        fprintf(stderr, "Can't stat: %s fd=%d err=%d\n", path, fd, errno);
        exit(1);
    }
    length = st.st_size;
    if (lenp) *lenp = length;
    db = (u_char*)mmap((void*)0, length, PROT_READ, MAP_SHARED, fd, 0);
    if (db == MAP_FAILED) {
        fprintf(stderr, "Can't map: %s fd=%d len=%d err=%d\n",
                path, fd, length, errno);
        exit(1);
    }
    return db;
}

int main(int argc, char *argv[]) {
    int i;
    u_char *ip_db = getdb("ip.gif", NULL, NULL);
    const char *_cc = 
        "USDEGBNL--FREUBEITESCACHRUSEAUAT"
        "PLCZIEFIJPDKNOUAZANGILROGRCNPTHU"
        "INTRIQSGHKCYIRLTNZKRLUBGAEARBRSI"
        "IDCLSKTWSAMYTHYUMXLVCOPHLBPKKZGH"
        "EETZKWKERSBDHRDZEGVECMPEECLIPRGE"
        "ISMEAPBYMGMDAOCIMTSOPAZWVNBANEBH"
        "PSJOCDMZAZMAUZDOBJAMUGSLCGGNZMMU"
        "TJMCANMWJMGIMKCRBMLRBOUYGTBWLKGP"
        "ALGAMQKGTTNASVLYMNMRAFNPSNKHBBQA"
        "CUGLBNOMMOPYSYPGNISMMLSCDJSZLSBF"
        "CFGUNCVGHNFJPFTDLAYEFOBISDGQRWKY"
        "**BSSRADGMCVGDKNETERRETNTMTGYTMV"
        "VIHTKMSTGWAGVABZBTNRTOFKKIVUMPWS"
        "MMAWSBJEGFAIAQIOGYNFLCPWCKDMAXFM"
        "TVNUAS--------------------------"
        "--------------------------------";
#ifdef CHECKCC
    int cc_fd;
    int cc_len;
    int cc_num;
    u_char *cc_db = getdb("cc.gif", &cc_fd, &cc_len);
    char cc[CC_MAX * 2 + 1];
    cc_num = cc_len / 3;
    if (cc_num < 0 || CC_MAX <= cc_num) {
        fprintf(stderr, "Can't happen: irregular CC DB cc_num=%d\n", cc_num);
        exit(1);
    }
    for (i=0; i < CC_MAX; i++) {
        cc[i*2] = '-';
        cc[i*2+1] = '-';
    }
    cc[i*2] = '\0';
    for (i=0; i < cc_num; i++) {
        u_char c = cc_db[i*3];
        cc[c*2] = cc_db[i*3+1];
        cc[c*2+1] = cc_db[i*3+2];
    }
    munmap(cc_db, cc_len);
    close(cc_fd);
    if (strcmp(cc, _cc) != 0) {
        for (i=0; i < CC_MAX; i+=16) {
            int j;
            printf("\"");
            for (j=0; j < 16; j++) {
                printf("%c%c", cc[(i+j)*2], cc[(i+j)*2+1]);
            }
            printf("\"\n");
        }
    }
#else
    const char *cc = _cc;
#endif
    for (i=1; i < argc; i++) {
        u_long in = ntohl(inet_addr(argv[i]));
        int c = inet_ntocc(ip_db, in);
        if (c < 0) {
            printf("UNKNOWN %s\n", argv[i]);
        } else {
            printf("%c%c %s\n", cc[c*2], cc[c*2+1], argv[i]);
        }
    }
    return 0;
}

国コードへの変換テーブル「cc.gif」は、 変更頻度もさほど高くないだろうと思われたので、 プログラム中に固定文字列として定義している。 コンパイル時に「-DCHECKCC」を指定することにより、 cc.gif と内蔵の変換テーブルが一致するかチェックできる。

あとは、このコードを MTA に組み込むだけ。 私のサイトでは qmail を 使っているので、 qmail-smtpd.c にこのコードを組み込んだ。

Filed under: システム構築・運用,プログラミングと開発環境 — hiroaki_sengoku @ 07:42
« Newer PostsOlder Posts »