いつの頃からか、手動で ezjail-admin stop
とするとエラーがでる。
どうやら Device is busy
とのこと。 mount | grep /mnt/jails
とかやってみると、ルートを umount
できてない様子。
まぁ、どうせシャットダウン時しかやらなければいいし……とか思って放置してたけど、 それはそれで地味に面倒な気がしてきたので、いい加減まじめに調査した時の記録。
問題が起きる jail はいくつかあるけれども、共通点としてはイメージを 1 ファイルに詰めて
md
でマウントするタイプ(ファイルベース?とか man
にはある)だと問題が起きる様子。
いろいろネットを捜索してみた感じ、だいたいみんな fuser
とか fstat
を使って使用中のファイルを調べてるみたい。
ファイルを開いているプロセスを殺すと umount
できるとか。
だがしかし!今回は誰もルートをマウントしていない!!!なんなら ps
で見ても誰もいない!!
一週間くらい、あーでもないこーでもないと調べまわってたところ、どうやらソケットが残ってる場合も
なぜかルートを umount
できなくなるらしい。あー?うーん??
という訳で、jail の中で netstat -an
とか叩いてみると、なんかコネクション一杯あるなぁ……という訳で、
tcpdrop
を叩いてから ezjail-admin stop
すると安定して正常終了するようになりました。
まぁ、これで解決としてもいいのでは?という気もしたけれども、偶然暇があったので終了スクリプトに組み込もうと…… 思ったところからが地獄の始まりだった気がします。
tcpdrop はホストで実行する必要がある
生きてるコネクションをリストアップする tcpdrop -al
は jail の中でも実行できますが、
それを皆殺しにするにはホストで実行する必要があります。
どうやら、カーネルとの通信は sysctl
を使っているようで、
net.inet.tcp.drop
に殺したいコネクションのエンドポイントを送る形らしい。
そして、カーネルのソースを読むと、どうやらこのリーフは通常の CTLFLAG_RW
になっている(
/usr/src/sys/netinet/tcp_subr.c
あたり )ので、jail の中からは WRITE アクセスは拒否される模様。
気になったのでその辺のカーネルソースを眺めてたら、 /usr/src/sys/kern/kern_priv.c
あたりから、
priv_check_cred_pre()
に飛んで jail の判定を入れているらしい。
/usr/src/sys/kern/kern_jail.c
の prison_priv_check()
では、jail 内部だった場合にホワイトリスト以外は
全て default
でひっかけて EPERM
を返すという強烈な仕様。
うーん、この辺りいじりたくないなぁ……という気持ちになったので諦め。
jail の poststop に仕掛けてみるパターン
jail.conf
には、 poststop
という項目があるので、ここで tcpdrop
すれば良さそう。
jail
コマンドのソースコードを読むと、 prestop (host) -> stop (in jail) -> poststop (host)
での実行になるようだ。
jail に割り当ててる IP を使って grep
すれば良さそう。
そしてみなさまご存じの通り、 /usr/local/etc/ezjail
にある設定ファイルは、環境変数の設定を行うだけで、
/etc/rc.d/jail
でテンポラリの /var/run/jail.NAME.conf
に変換されているので、そのまま設定できそう。
export jail_NAME_exec_poststop0="tcpdrop -al | grep -Fw -e 192.168.0.10 -e 2001:db8::10 | sh"
こんな感じで ezjail の設定として追記しておくと、 /var/run/jail.NAME.conf
では
exec.poststop += "tcpdrop -al | grep -Fw -e 192.168.0.10 -e 2001:db8::10 | sh"
となって、無事に安定して正常終了するようになりました。わーい。
すべての ezjail 設定に書くの面倒だな…?
いや、別にこれでも特に困りはしないんですけどね。
ezjail を何匹も飼ってると、同じことを書くのがだんだん面倒というか、 同じじゃなくて IP アドレスだけ差し替えるのが面倒。
という訳で、 jail.conf
の仕様を眺めてると、なんと変数展開に対応しているらしい。そしてできたのがこれ。
export jail_exec_poststop0="sh -c 'tcpdrop -al | grep -Fw -e \\\${0%/*} -e \\\${1%/*} | sh' \${ip4.addr} \${ip6.addr}"
/etc/rc.conf
に 1 回書いておくだけ。こうすると、各 ezjail を動かすたびに /var/run/jail.NAME.conf
では
exec.poststop += "sh -c 'tcpdrop -al | grep -Fw -e \${0%/*} -e \${1%/*} | sh' ${ip4.addr} ${ip6.addr}"
と変換されて、 jail
コマンドを実行するときに後ろの IP アドレスが変数展開される。
最終的には、ホストで
tcpdrop -al | grep -Fw -e 192.168.0.10 -e 2001:db8::10 | sh
みたいなコマンドが実行されて、無事に TCP コネクションが皆殺しになる。これならいいかな。
IPv6 がない jail でエラーになる&あと同じ IP アドレスが巻き添えになる
ぇー……ダメじゃん……
prestop
ならまだ jail は生きてるから、ホスト側で jexec tcpdrop -al
して sh
に流し込めばいいかな。
tcpdrop -al
の出力がそのまま実行できるスクリプトになってるのは助かった。
……しかし、 /etc/rc.d/jail
が生成する jail.conf
はデフォルトで persist
がついてないので、
中のプロセスが全員死ぬと jail の箱ごと消えるのでした。
でも中のプロセスが殺されたコネクションを再作成してくれると、また umount
できなくなる罠が再発する……
という訳で確実にプロセスを皆殺しにした上で、 tcpdrop -al
を実行する必要がある。はてさて……
そしてしばらく試行錯誤してできたのがこちら。
jail_exec_prestop0="jexec \${name} /bin/sh -c '/bin/sh /etc/rc.shutdown jail >&2 ; kill -TERM -1 >&2 ; sleep 20 >&2 & tcpdrop -al' | sh | logger -t tcpdrop/\${name} -p daemon.notice"
同じく /etc/rc.conf
に書くだけ。前のやつは消すのを忘れずに。
先頭の /etc/rc.shutdown jail
はデフォルトで exec.stop
にあった奴をそのまま持ってきただけ。
その後、 kill -1
で jail の中を掃除します。ただ、これは sh
の内部コマンドになっているので、
これを実行している /bin/sh -c
で始まってるプロセスは jail の中で生き続けます。
でっ、 sleep
コマンドを &
つけて起動することでしばらく jail を生存させ続けて、
その間に jexec
内部で tcpdrop -al
を実行した出力「のみ」を stdout
に送り込む。
他のコマンドはすべて stderr
につながないと sh
が実行できないので注意。
唯一の生存者、 sleep
も、 jail
コマンドの exec.poststop
の後で強制的に kill -TERM
してくれるので、
デフォルトのタイムアウト、10 秒より長ければまぁ何でも変わりはないはず。
ただし、パイプ工事は上手くやらないと sleep
が終わるまで jexec
が終了してくれなかったりするので、
なかなか微妙なバランスで動いてるっぽい。うへぇ……
最期の logger
はおまけ。せっかくなので syslog
にコネクションの撃墜記録でも流しておこうかと(デバッグに役立つので)。