ほっしーの技術ネタ備忘録

技術ネタの備忘録です。基本的に私が忘れないためのものです。他の人の役にも立つといいなぁ。

ezjail の終了時に umount できない問題を直した

いつの頃からか、手動で 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.cprison_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 にコネクションの撃墜記録でも流しておこうかと(デバッグに役立つので)。