使わなくなったパソコン,あなたの周りで眠っていませんか?
ここでは,FreeBSDのMFS_ROOTと呼ばれる機能を使って,書き込み禁止にしたフロッピーディスク1枚で動くファイアーウォールルータの作成方法をご紹介します。

Last-Modified: 1998/11/16 02:10

ObsoleteFreeBSDNetworking

用意するもの

CPU i386以上,メモリ4MB以上を搭載したパソコン
FreeBSDがサポートしている機種なら何でもいいです。今回はPC-9801DA (CPU:i386DX 16MHz; MEM:640+9216 KB)を用意しました。
ネットワークカード
必要なだけ用意してください。私はPCI EN-2298P-Tを2枚用意しました。
FreeBSDのカーネルソース
なるべく最新のものがよいでしょう。ここではFreeBSD(98) 2.2.1R-Releaseをベースにしています。(どこが最新だ……)
FreeBSDが動いているパソコン
カーネルを再コンパイルするのに必要になります。
フロッピーディスク
2HDのものを1枚。さすがに2DDでは無理です。

カーネルの再構築

まず,カーネルの再構築をします。カーネルの再構築の仕方がわからない場合には,FreeBSD ハンドブックなどを参照してください。

FreeBSDをファイアーウォールルータとして使用するには,GENERICカーネルに次のオプションを追加する必要があります。

options    IPFIREWALL
options    IPFIREWALL_VERBOSE
options    "IPFIREWALL_VERBOSE_LIMIT=100"

IPFIREWALLオプションを指定することで,パケットフィルタリングがサポートされるようになります。IPFIREWALL_VERBOSEは,パケットフィルタリングのログをとるためのオプションです。
最後のIPFIREWALL_VERBOSE_LIMIT=nは,ログをn回までしかとらないように制限するオプションです。アタックの多いサイトではこのオプションを指定しておくと,ログがあふれる心配がなくなります。

MFS (メモリファイルシステム=RAMディスク)をルートデバイスとして使用するためのオプションを指定します。

options    MFS
options    "MFS_ROOT=450"

MFSオプションで,MFSを有効にし,MFS_ROOT=nオプションで,nKB のメモリをルートデバイスとして確保するようにします。ただしここで指定した容量分だけカーネルのサイズは大きくなります。あまり欲張るとカーネルがフロッピーディスクに収まらなくなりますので,注意してください。2HC (1.2MB)のディスクをブートデバイスとして使用する場合,ここにあげた450KBがほぼ上限値となるでしょう。

カーネルの所在はとりあえず,/dev/fd0ということにしておきます。

config    kernel    root on fd0

gzipで圧縮したCrunched Binary (後述)を使用するため,仮想デバイスとしてgzipを指定します。圧縮しなくてもFDに入るように,気合いを入れてCrunched Binaryからコマンドを削除しまくれば,このデバイスは不要になるかもしれません。

pseudo-device    gzip

以上のオプションを追加し終わったら,後は,ひたすら不要なオプションやデバイスを削除しましょう。ATAPIやSCSI関連などは全く不要ですし,ファイアーウォールにNFSも感心しません。
とりあえず,できあがったカーネルがフロッピーディスクに入りきるサイズになるまで,徹底的にシェープアップしましょう。
参考までに私が作ったコンフィグレーションファイル(PC-9801用)が,こちらにあります。
この例では,カーネルサイズを抑えるため,ユーザーコンフィグレーション (boot: -c) は使用不可とし,2枚のネットワークカードPCI EN-2298P-TのIRQ, I/O を決めうちして,それぞれed0, ed1として使用するようにしています。(ptyの数をもう少し減らしてもいいかもしれません)

make depend;makeで満足できるカーネルができあがったら,make installはせず,とりあえず適当なところにコピーしておいてください。

Crunched Binaryの作成

次に必要となるコマンド類とライブラリを一つにまとめたCrunched Binaryを作成します。

まずは設定ファイルを作成します。適当なディレクトリを作って,その中に,下記のようなfdbsd.confとでも名付けた設定ファイルを作ります。

srcdirs /usr/src/bin
srcdirs /usr/src/sbin
srcdirs /usr/src/usr.bin
srcdirs /usr/src/usr.sbin
srcdirs /usr/src/libexec

# /bin
progs   kill ls mkdir cat hostname ln pwd
progs   rm sh ps test stty date sync cp

ln test [
ln sh -sh
ln sh -u

#/usr/bin
progs   passwd login netstat

# /sbin
progs   dmesg
progs   ifconfig init mount umount
progs   reboot route ipfw ping newfs

ln reboot halt
ln newfs mount_mfs

# /usr/sbin
progs   inetd routed syslogd sysctl
progs   kvm_mkdb dev_mkdb pwd_mkdb

#/usr/libexec
progs   getty telnetd

libs -lutil -ll
libs -lcrypt          # for login
libs -lkvm            # for ps, netstat
libs -lm              # for ps
libs -lipx            # for ifconfig
libs -ledit -ltermcap # for sh
libs -ltelnet         # for telnetd

簡単なファイルなので見ればわかると思いますが,srcdirsで始まる行にコマンドのソースがあるディレクトリを列挙し,progsで始まる行に実際のコマンド名,libsで始まる行にはコマンドをコンパイルする際に必要となるライブラリ名をgccに指定するのと同じ形式でそれぞれ記述します。また,lnで始まる行には,元々ハードリンクされているコマンドを記述します。

この例では,/sbin/dsetがありませんが,カーネルオプションで,USERCONFIGを指定している場合には,追加した方がよいでしょう。また,telnetdが不要な場合には,libs -ltelnetの行を削除してください。

設定ファイルの記述が終わったら,Crunched Binaryの作成にかかるのですが,その前に,loginとpasswdコマンドのMakefileを修正する必要があります。

  • /usr/src/usr.bin/login/Makefile
    • "-DSKEY"を削除
  • /usr/src/usr.bin/passwd/Makefile
    • "-DYP"を削除
    • "SRCS= ..." 行を"SRCS=local_passwd.c passwd.c pw_copy.c pw_util.c"に変更
    • "SRCS+= ..."行をコメントアウト

以上の作業を終えたら,Crunched Binaryの作成です。
さきほど,fdbsd.confを置いたディレクトリに戻って,

% crunchgen fdbsd.conf

と入力してください。設定ファイルに間違いがなければ,

Run "make -f fdbsd.mk objs exe" to build crunched binary.

というメッセージが表示されますので,素直に

% make -f fdbsd.mk objs exe

とします。
マシンパワーとコマンドの数にもよりますが,5〜30分程度で同じディレクトリにfdbsdという実行ファイルができあがりますので,これをgzipで圧縮して,Crunched Binaryの作成をひとまず完了します。

% gzip -9 fdbsd

/etcファイル群の編集

この節は,2.2.1R-Releaseをベースに記述しています。
2.2.2以降では,スクリプトの構成がかなり変わっていますので,参考程度にとどめてください。

いくらフロッピーディスク1枚で起動すると言っても,一応はFreeBSDですので,それなりの設定ファイルを整えてやらなければなりません。
次のファイルを/etcから適当なディレクトリにコピーして,修正していくことにします。

書き換え不要
services, protocols, gettytab, netstart
書き換えが必要
ttys, master.passwd, inetd.conf, syslog.conf, host.conf, resolv.conf, hosts, sysconfig, rc.firewall, rc, rc.local

上記のうち「書き換え不要」としているものについても,カーネルとCrunched Binaryのサイズ次第では書き換えた方がよいものもあります。特にservicesファイルにはよけいなエントリやコメントが多いので,1024以上のポートに関しては,ばっさりと切ってしまっても良いと思います。

ttys

ルータなんてそうそうログオンするものでもありませんので,console, ttyd0, ttyp0,ttyv0の4つのエントリーがあればよいでしょう。

console none                            unknown off secure
ttyd0   "/usr/libexec/getty std.9600"   unknown off secure
ttyp0   none                            network on  secure
ttyv0   "/usr/libexec/getty Pc"         cons25  on  secure

あまりほめられたことではありませんが,管理作業のため,rootでtelnetできるようにしてあります。気になる方はCrunched Binaryにsuを追加し,ttyp0のエントリから,"on secure"を削除してください。
(suを追加する場合には,/usr/src/usr.bin/su/Makefileに/usr/src/usr.bin/login/Makefileと同じ処置を施してやる必要があります。)

master.passwd

rootとnobodyのエントリーがあればよいでしょう。

シェルは/bin/shしかないので,忘れずに変更してください。ホームディレクトリは適当に設定してください。

root:XXXXX:0:0::0:0:Administrator:/:/bin/sh
nobody:*:65534:65534::0:0:Unprivileged user:/nonexistent:/nonexistent

(上記rootのパスワードはもちろんダミーです。実際には/etc/master.passwdの内容を使用すればよいでしょう。)

MFS_ROOTに十分な容量がある場合,master.passwdの編集が終了したら以下のようにしてパスワードdbファイルの作成を行います。

# /usr/sbin/pwd_mkdb -d ./ ./master.passwd

これにより,カレントディレクトリにpwd.db, spwd.dbという2つのファイルが生成されます。
今回の例のようにMFS_ROOTに十分な容量がない場合には,rcやrc.localの中で起動時にdbファイルを作成することも可能です。これについてはrcの項を参照してください。

inetd.conf

telnetdを呼び出すエントリ以外は不要です。コピー元のホストでTCP Wrapperを使用している場合は,tcpdの呼び出しを忘れずにはずしてください。

telnet  stream  tcp    nowait  root    /usr/libexec/telnetd    telnetd

syslog.conf

/varがMFSなので,普通の設定ではリブートしたらログが消えてしまいます。そこで今回は,ログを別のホストにとばすことにしました。hostsにsyslogdの動いているホストをloggerとして登録してください。また,ログが詳細すぎてわずらわしい場合には適当にログレベルを上げてください。
シリアル端末に流すのもおもしろいかもしれません。

*.err;kern.debug;auth.notice;mail.crit          /dev/console
*.debug                                         @logger

host.conf, resolv.conf, hosts

これらは解説不要でしょう。各自のサイトにあわせて設定してください。

sysconfig

rcスクリプト内で使用する変数の初期値を設定します。

ここでは,"Netconfig Section"のみ設定します。

hostname="myhost"
defaultdomainname="mydomain"
tcp_extensions="YES"
network_interfaces="ed0 ed1 lo0"
ifconfig_ed0="inet XXX.XXX.XXX.XXX  netmask MMM.MMM.MMM.MMM"
ifconfig_ed1="inet YYY.YYY.YYY.YYY  netmask NNN.NNN.NNN.NNN"
ifconfig_lo0="inet 127.0.0.1"
static_routes="ZZZ.ZZZ.ZZZ.ZZZ"
router="YES"
routerflags="-q"
gateway="YES"
firewall="YES"

ここで設定しない項目についてはすべて"NO"でよいでしょう。
ホスト名やドメイン名,IPアドレスなどは正しく設定してください。(私のところでは「記憶喪失」のルータが動いています。 :-P)

rc.firewall

パケットフィルタリングのポリシーを決定するためのファイルです。

デフォルトで用意されているフィルタリングルールは,"open", "client", "simple", "NONE"の4種類で,それぞれ,

open
すべての通信を許可
client
  1. 特定のホストと特定のネットワーク間の通信をすべて許可
  2. 特定のホストとすべてのネットワーク間のsmtp, dns, ntp接続を双方向で許可
  3. その他は拒否
simple
  1. 偽装IPの拒否
  2. 接続が確立している通信をすべて許可
  3. 外部からのsmtp, dns (ゾーン転送), http接続を許可
  4. 双方向のntp, dns接続を許可
  5. 内部からの接続要求をすべて許可
  6. その他は拒否
NONE
すべての通信を拒否(ルータとして機能しない)

の意味を持っています。

これらの中からルールを選択する場合は

firewall_type=

の行に使用したいルールを記入し,さらに"client"を選択した場合には

net=通信を許可するネットワークのネットワークアドレス
mask=通信を許可するネットワークのサブネットマスク
ip=通信を許可するホストのIPアドレス

の3つの変数を,
"simple"を選択した場合には

oif=外側インターフェースのデバイス名
onet=外側インターフェースのネットワークアドレス
omask=外側インターフェースのサブネットマスク
oip=外側インターフェースのIPアドレス

iif=内側インターフェースのデバイス名
inet=内側インターフェースのネットワークアドレス
imask=内側インターフェースのサブネットマスク
iip=内側インターフェースのIPアドレス

の8つの変数を設定します。

独自のルールを記述したい場合には,

firewall_type=custom

とでもして,スクリプトの末尾の"fi"の前に

elif [ "${firewall_type}" = "custom" ]; then
    /sbin/ipfw add パケットごとのポリシー
        :
        :
        :

とパケットごとのフィルタリングポリシーを列挙すればよいでしょう。
(ipfwの書式についてはman 8 ipfwで確認してください。)

なお,ここで誤った設定を行うと,全く通信できなくなったり,ファイアーウォールの意味をなさなくなったりします。TCP/IPについて十分理解している場合以外は,既存のルールを用いた方が無難でしょう。(通常は,"simple"ルールで十分実用に耐えます。)

rc

2HCでしかブートできない9801シリーズでは,はっきり言ってディスク容量との格闘になります。そこで私はrcを思い切って簡素化してしまいました。(恥ずかしいrcはこちらを参照。)
このスクリプトでは,先に説明したsysconfigもrc.firewallも使用しません。エラー処理もしていません。さらに,特殊な環境下で動かしているため,ルータなのにroutedすら動いていません。「いくら何でもやりすぎ」という声が聞こえてきそうです。

私の例は極端としても,rcはやはり書き換えてやる必要があります。

まず,先ほどCrunched Binaryを作成したときに入れなかったコマンドを呼び出している箇所は,ばっさり削除してしまいます。swapon, adjkerntz, chmod, ldconfig, cron, uname, fsck などを実行しているところですね。

ついでに,mountしているところも削除してしまいましょう。どうせマウントすべきファイルシステムはないのです。そのかわり,ここでは/varをMFSマウントすることにします。

mount_mfs -s 720 -c 1 -m 0 /dev/rfd0a /var
mkdir -p /var/db /var/log /var/run /var/spool/lock /var/tmp /var/etc
ln -s /var/tmp /tmp
cp /dev/null /var/log/messages
cp /dev/null /var/run/utmp
cp /dev/null /var/run/log

上の例では,/dev/rfd0aをダミーのファイルシステムとして/varに720KBのMFSをマウントし,その中にログファイル用のディレクトリを作成,さらに/var/tmpを/tmpへのシンボリックリンクとして設定しています。(メモリに余裕があるならもう少し多くても良いでしょう。)

さらに9801シリーズでは容量的にパスワードdbファイルを格納することができないので,上記設定の直後に

ln -s /etc/master.passwd /var/etc/master.passwd
/usr/sbin/pwd_mkdb -d /var/etc /var/etc/master.passwd
ln -s /var/etc/pwd.db /etc/pwd.db
ln -s /var/etc/spwd.db /etc/spwd.db

としてdbファイルを作成します。
ただし,このように/etcディレクトリの容量がない場合,passwdコマンドでパスワードの変更ができなくなります。(うまく/etcをすり替えられたら回避できるかもしれません。)

rc.local

これも説明は不用でしょう。お好みに応じて適当に編集してください。

ルートイメージの作成

先ほど作成したCrunched Binaryおよび/etcファイル群をルートイメージに書き込みます。

まず,下準備として,ルートイメージの元になるファイルを用意し,これをvnodeデバイスとしてマウントします。

# dd of=fs-image if=/dev/zero count=450 bs=1k
# awk 'BEGIN {printf "%c%c", 85, 170}' | dd of=fs-image obs=1 seek=510 conv=notrunc
# vnconfig -s labels -c /dev/rvn0 fs-image
# disklabel -Brw /dev/rvn0 minimum
# newfs -u 0 -t 0 -i 60000 -m 0 -T minimum -o space /dev/rvn0c
# mount /dev/vn0c /mnt

450KBのfs-imaeというファイルを作成し,/dev/vn0とアタッチして,ファイルシステムを作成,/mntにマウントしています。
/dev/*vn0*が見あたらない場合には,MAKEDEV vn0でデバイススペシャルファイルを作成してください。(もちろん今使っているカーネルがvnodeデバイスに対応している必要もあります。GENERICカーネルなら問題ありません。)

次にこのファイルに対してディレクトリとデバイススペシャルファイルの作成を行います。

# pushd /mnt
# mkdir -p etc dev var sbin bin usr/libexec usr/bin usr/sbin
# cd dev
# sh /dev/MAKEDEV std fd0 ttyd0 cuaa0
# mknod ttyv0 c 12 0
# mknod ttyp0 c 5 0
# mknod ptyp0 c 6 0
# popd

シリアルポートを使用しないなら,ttyd0, cuaa0 は不要でしょう。

/etcファイル群をコピーします。
なお,/etcファイル群は~/mfsrouter/etcにあらかじめコピーしてあるものとします。

# cp  -R -p ~/mfsrouter/etc/* /mnt/etc

ここまでできたら,先ほど作成したCrunched Binaryを書き込み,それぞれのコマンドにリンクを張ります。

# cp ~/mfsrouter/fdbsd.gz /mnt
# ln /mnt/fdbsd.gz /mnt/bin/kill
       :
       : (結合前のコマンドのあった位置へハードリンク)
       :

このリンクを張る作業,手でやってももちろんかまわないのですが,私は面倒くさかったので次のようなスクリプトを書きました。

#!/bin/sh
#
#
echo "#!/bin/sh" > $2
echo "# GENERATED BY mklinkscript" >> $2
echo -n "#  " >> $2
date >> $2
echo "#" >> $2
#
awk '/^progs[ \t]+.*/{for(i=2;i<=NF;i++) printf
    "find /bin /sbin /usr/bin /usr/sbin /usr/libexec -name %s -print\n",$i}'\
    $1 | sh | awk '{printf "ln $1/$2 $1%s\n",$0}' >> $2
echo 'ln $1/bin/test $1/bin/[' >> $2
echo 'ln $1/bin/sh $1/bin/-sh' >> $2
echo 'ln $1/bin/sh $1/bin/-u' >> $2
echo 'ln $1/sbin/reboot $1/sbin/halt' >> $2
echo 'ln $1/sbin/newfs $1/sbin/mount_mfs' >> $2
#
chmod u+x $2

作成したファイルがシェルスクリプトになるようにマジックナンバーを出力し,Crunched Binaryの設定ファイルを1行ずつ読んで,"progs"で始まっていれば,そこに記述されたコマンドの所在をfindで探し,結果を整形出力するだけのスクリプトです。lnで始まる行については良い方法が見つからなかったので,ハードコードしています。
あまりスマートな方法とはいえませんが,何とかなっているので良いことにしましょう。:-P

このスクリプトをmklinkscriptとでも名付け,

# ./mklinkscript ~/mfsrouter/crunch/fdbsd.conf ~/mfsrouter/mklink.sh

と,Crunched Binaryの設定ファイルへのパス,作成したいスクリプトへのパスを引数に実行すると,自動的にリンクを張るためのスクリプトが出力されます。 そこで,今度はマウントディレクトリのパス,Crunched Binaryのファイル名を引数にこのスクリプトを実行すると,リンクは完了という寸法です。

# ~/mfsrouter/mklink.sh /mnt fdbsd.gz

最後に,このルートイメージファイルをカーネルに焼き込みます。

カーネルへのルートイメージの焼き込みには,/usr/src/release/write_mfs_in_kernel.cをコンパイルして使用します。

# umount /mnt
# vnconfig -u /dev/rvn0
# gcc /usr/src/release/write_mfs_in_kernel.c -o ./write_mfs_in_kernel
# ./write_mfs_in_kernel kernel fs-image

なお,この作業は,一度ルートイメージを焼き込んだカーネルに対して行うと失敗します。必ずコンパイル直後のカーネルに対して行うようにしてください。

ブートディスクの作成

さていよいよ大詰めです。

フロッピーディスクをブート可能なようにフォーマットして,カーネルを書き込んでください。

# fdformat /dev/rfd0.1200
# disklabel -r -w -B /dev/rfd0.1200 fd1200
# newfs -c 1 -i 4096 -o space -m 0 /dev/rfd0.1200
# mount /dev/fd0 /mnt
# cp .kernel /mnt/kernel
# umount /mnt

ここではPC-9801シリーズ用に2HCのディスクを作成しました。

このディスクをターゲットとなるマシンにつっこみ電源を入れると,ファイアーウォールルータの出来上がりです。

実体のあるファイルシステムはマウントしていませんので,フロッピーディスクは書き込み禁止にできますし,ブートしてしまえばディスクを引っこ抜いてしまうことも可能です。

既知の問題

と,ここまで偉そうに解説してきたのですが,実はこの方法,以下のような問題を抱えています。

パスワード変更ができない

/etcファイル群の編集の節でも書きましたが,容量不足のためパスワードの変更ができなくなっています。

→ /sbin/initが/etc/rc以外をスタートアップスクリプトとして認識するよういじってやる (/usr/src/sbin/init/pathnames.h中の _PATH_RUNCOMの定義を書き換える)ことで,/etcを動的にマウントできそうです。(1998.10.28)

→ 鈴木善昭さんが作られたコンパイルキットによると,カーネル構築の際にMFS_ROOTの値を大きくとり,イメージを書き込んだカーネルそのものをkzipで圧縮すれば,/sbin/initを改造することなく,/etcの容量を確保できるそうです。
ちなみに,鈴木さんのコンパイルキットは,私がここで紹介しているものよりはるかに優れた物です。(1998.11.16)

ps, netstatなどが動かない

psやnetstatを実行すると"/kernel : no such file or directory"とエラーを出してしまいます。どうしましょ?

→ カーネルシンボリックテーブルが欠落するのが原因だそうです。鈴木善昭さんおよび川渕靖広さんから「picoBSDのソースを眺めてみては」との指摘をいただきました。また,鈴木さんからはnetstat, psのソースをいじることで解決可能との情報もいただています。(1998.10.27)

一度作ってしまった環境の変更が面倒くさい

せめて起動時に/etcで設定している内容程度はカスタマイズできるようにしたいとは思っているのですが……

どなたか,力を貸していただけませんか?

履歴

1998.10.21.
プロトタイプ (ルーティング,パケットフィルタリングのみ)
1998.10.22.
login可能バージョン, telnetd追加
1998.10.23.
pwd追加,ドキュメント作成

参考資料