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

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

geom 管理領域を(無理やり)消す

gmirror とか glabel の管理情報は、該当するオブジェクトの最終セクタに書いてあります。

なので、grom_mirror.so を読み込まない状態で、dd とかを使って最終セクタをつぶすと、 そのオブジェクトは gmirror への参加をしなくなったり、glabel データがなくなったりします。 その時に使うコマンドについての注意点。

ダメな例。

$ su dd if=/dev/null of=/dev/ada0 bs=512 seek=hogehoge

大丈夫な例。

$ su dd if=/dev/zero of=/dev/ada0 bs=512 seek=hogehoge

おわかりいただけただろうか。 入力インターフェースとして /dev/null を使うと、こいつはサイズが 0 なのでシークできなくて、 最終セクタを消さないまま成功扱いになります。

これで消したつもりになって kldload geom_mirror とかやると、意図しない結果になります。 というかなりました。

今まで /dev/null/dev/zero の違いはあまり意識していませんでしたが、 こういうところで差がでるのですね。うーんぐぬぬ

ポート 33060 ってなんぞ??

netstat を眺めてたら覚えのないポートがあいてたので調べてみた。 なんか知らんけど MySQL の Extended Interface とかいうものらしい。 なんぞ??

という訳で、 my.cnf に以下の行を加えて再起動。

mysqlx=OFF

これで消えました。なんだったんだ…??

今回は本当にメモ書きだけ。

ezjail の終了に 10 秒かかる問題を直した

なんか、いつ頃からか。 ある ezjail の仮想マシンを終了するときに 10 秒待たされることに気づいたのでした。

そう、前回の記事と同時に発症してたのです。まぁ、終了時にしか stop しなければ……と思ってましたが、やっぱり不便なのです。 そこでいろいろ調べた記録の片割れです。

その仮想マシンの中では tomcat さんが動いていたので、試しに起動自体をしないようにしてみると、問題なし。 一方、手動で tomcat さんを終了すると 10 秒待つことになる。なるほど、こいつが原因だ。

という訳で、ログを眺めてみると

WARNING [main] org.apache.tomcat.util.net.Acceptor.stop The acceptor thread [ajp-nio-127.0.0.1-8009-Acceptor] did not stop cleanly
INFO [main] org.apache.coyote.AbstractProtocol.destroy Destroying ProtocolHandler ["ajp-nio-127.0.0.1-8009"]

うーん、こいつっぽい。

この Acceptor とやらの実装を眺めてみると、どうやら 10 秒でタイムアウトさせているようだ。 こいつっぽいな。

ていうか非同期のソケットとかにはして……なさそうだな?と思って終了に関する実装を眺めてみると、 どうやら unlockAccept() メソッドを呼ぶことで「自分自身のソケットに connect を行うことで accept から戻らせる」 ということをやっている様子。ま、マジすか……

という訳で、ファイアウォールAJP のポートを開ける際に、自分自身からの接続も許可してみると、 無事に 1 秒もかからず正常終了するようになりました、とさ。

マジかー、そういう終了シーケンスは初めて見た気がするな……いや、私の見識が狭いだけで、実は定石なん?

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

MAP-E の設定を永続化した話

ここまでのあらすじ。

FreeBSD 13.1-R で MAP-E 対応が降りてきたので、いろいろ試して仮組みながら動くようになったのがこの辺。 13.1-R で MAP-E ルータを作ってみる - ほっしーの技術ネタ備忘録

で、その後いろいろ不具合を直した話がこっち。 使い物になる NAT with pf を作る - ほっしーの技術ネタ備忘録

でっ、だいぶ安定して動いてくれてるので、そろそろ再起動後に自動設定されるようにしようかと。というのが今回のお話。

/etc/rc.conf

まぁ、上の記事である通りの設定をそのまま rc.conf の形式で書くだけなので特に面白みはなく。

# em0 にこっち側の終端 IPv6 アドレスを割り振る
ifconfig_em0_alias0="inet6 240b:11:d4e0:2000:6a:49d4:e000:2000 prefixlen 64"

# MAP-E の設定をする
gateway_enable="YES"
gif_interfaces="gif0"
gifconfig_gif0="inet6 240b:11:d4e0:2000:6a:49d4:e000:2000 2404:9200:225:100::64"
ifconfig_gif0="inet 106.73.212.224/15 106.73.212.224"

# pf の設定
pf_enable="YES"
pf_rules="/etc/pf.conf"
pflog_enable="YES"

今どきは rc とかいろいろなスクリプトがやたら高機能なので、特に変なことを書く必要はなかったので面白くない。

/etc/pf.conf

あと pf.conf も改良版で一旦確定。

# /etc/pf.conf
scrub all fragment reassemble
scrub all reassemble tcp
nat log on em0 from 192.168.XXX.0/24 to any -> 106.73.212.224 map-e-portset 4/8/32
pass all
pass out log route-to gif0 from 106.73.212.224
pass in log reply-to (em0 192.168.XXX.1) from 106.73.212.224

落ち穂拾い

あとは ipfw のルールに ipencap を追加するのを忘れない。

いずれ ipfw から完全に移行しておきたい気持ちはあるけれども面倒。 パケットフィルタが 2 つ動いてるのはやっぱり邪魔だし保守の手間が継続ダメージになるので。

特に面白みはないね。

認証エラーログを見て ipfw で ban するやつ

せっかく(10年くらい前に)作ったのでここで公開しておく。

ログを監視して IP アドレスごとバンするやつ

こういうスクリプトを root で走らせておく。

#!/bin/sh

detect() {
        ip=`echo "$1" | egrep -o -e "\[([[:digit:]]+\.){3}[[:digit:]]+\]" | sed -e "s/[][]//g"`
        if [ ! -z "$ip" ]; then
                /sbin/ipfw add 8`date +%H` deny all from $ip to any 2>&1 | /usr/bin/sed 's/^/add ipfw rule: /' | /usr/bin/logger -p auth.info -t ${0##*/} -s
        fi
}

exit_me() {
        kill `jobs -p`
        exit 1
}

PID=$$
trap "exit_me" TERM INT

tail -n 0 -F /var/log/maillog | (trap "kill $PID" TERM INT && while read line; do
       if echo "$line" | egrep -e 'Password mismatch ' > /dev/null 2>&1; then
               detect "$line"
       fi
        if echo "$line" | egrep -e 'SASL LOGIN authentication failed:' > /dev/null 2>&1; then
                detect "$line"
        fi
done) &

wait $!
exit_me

内容を要約すると、 tail -f /var/log/maillog を走らせっぱなしにして、 何か出てくるたびに grep でフィルタして、認証エラーから IP アドレスを抽出したら ipfw のルールにつっこむ。

でっ、その時に 800 番台の番号を振っておく。800 + 現在時刻の hour の値。 例えば、10:34 に流れてきたログから抽出した IP アドレスなら 810 って。 800 番台にしたのは、うちの ipfw ルールが偶然あいてたから。別に何でもいいんだけど。

マッチングルールはまぁいろいろお好みで。 他のコードは、あとで daemon にする時にシグナル周りをいい感じに何とかするためのヤツ。

ほとぼりが冷めたころに ban を解除するやつ

で、あとはこういうスクリプトを cron で毎時 30 分に回す。

#!/bin/sh

export LC_ALL=C
id=`printf "8%02d" \`expr \( \\\`date +%H\\\` + 20 \) % 24\``
echo "delete ipfw rule: $id" | /usr/bin/logger -p auth.info -t ${0##*/}
/sbin/ipfw show | grep "^00$id" | /usr/bin/logger -p auth.info -t ${0##*/}
/sbin/ipfw delete $id > /dev/null 2>&1

20:30 に実行されたら、816 番のルールを全部消す。 だいたい 3 時間くらいで解除する感じかな?別にそんなに厳密にする必要もないので、 書きやすい感じで書いただけ。この辺の数字もお好みで。

最初のスクリプトdaemon にする

/usr/local/etc/rc.d/watch_mx とか適当な名前で下のスクリプトを置いとく。 パスとかは適宜直して使う。

#!/bin/sh
#

# PROVIDE: watch_mx
# REQUIRE: ezjail

. /etc/rc.subr

name=watch_mx
rcvar=watch_mx_enable
command=/usr/sbin/daemon

load_rc_config $name

: ${watch_mx_enable:="NO"}

pidfile=${watch_mx_pidfile:="/var/run/watch_mx.pid"}
command_args="-cfr -P ${pidfile} -t ${name} /usr/local/bin/${name}.sh"

run_rc_command "$1"

こっからメモ書き(ポエム)

/usr/sbin/daemon って初めて使ったけど案外便利ね。

基本的なノリとしては

$ sudo daemon -cfr -P /var/run/watch_mx.pid -t watch_mx sh ~/bin/watch_mx.sh

こんな感じで起動すると、 sh 以降のコマンドを子プロセスとして起動して、 死んでたら再起動したり、シグナルを中継したりしてくれるみたい。

実際、 tail の方に SIGKILL とか送ると勝手に再起動してくれる。 sh の方に送ってもちゃんと再起動してもらうために、 パイプの接続先は子プロセスでも trap してから while read とかしてる。 あんま見かけない書き方だけど、なんか動いてるから多分大丈夫(?)。

LISTEN から accept して fork するだけの、サーバプログラムの共通になりがちなコードの 親プロセス部分の実装が inetd だとするなら、daemon の実実装部分の管理をするだけの 親プロセス部分が daemon といったところか?

ただ、PGID の親が daemon さんになってしまうせいで、 kill -$$ がちゃんと動かないのはハマった。 子プロセスからみた自 PID、 $$ と同じ PGID は存在しないので。 おかげでシグナルトラップをちゃんと動かすのに大変手間取った…… 解決策は

$ kill `jobs -p`

こんな感じ。

素直に perl とかで書けばよかった気がしないでもない。 なんでもかんでも root で動かすとセキュリティホールになるしね。 いずれ作り直したいところではある。

2022-09-29 注:IPv6 対応がバグっているまま10年以上運用していたらしい。まぁ別に害がある訳でもないか……

ssh での接続がなんか遅い問題の対策

なんか知らんけど、前々からさくら VPS にある FreeBSD マシンへ SSH するときに限って、 なんか接続に時間がかかる。

ssh って打ってから公開鍵のパスフレーズを聞かれるまでに体感で 5 秒くらい待たされる。 でも2回目以降は他の場所にあるマシンと同じくらいのリズムでつながるようになる。

……あ~~~~~~~~これ絶対 DNS とか名前引きとかそういうアレだよー……タイムアウトしたりそれがキャッシュされたり忘れたりする挙動、これ絶対 DNS とかそういうやつだよ……

みたいなことを思いながら調べた時の記録。

まずは接続するときに ssh -vvv で接続する。 遅いときは最初にあるサーバからのあいさつが遅い。 つまり返事するまでにサーバが何かしてる。

次。 /etc/ssh/sshd_configLogLevel DEBUG3 にする。 過去のトラウマがフラッシュバックしつつ $ sudo /etc/rc.d/sshd restart & を叩く。 嫌な予感がする時は、最後におまじないのごとく & をつけるのがポイント。こいつがあれば大体救われる。

XXX XX 03:44:32 host sshd[19640]: debug3: Trying to reverse map address 240b:11:xxxx:xxxx:xxxx:xxxx:xxxx:xxxxx.
XXX XX 03:44:35 host sshd[19640]: Connection from <snip>

ほらー!なにこのタイムスタンプの不自然な間は!!! どうみてもクライアントの IPv6 アドレスを逆引きしようとしてタイムアウトしてんじゃん。

$ drill -x 240b:11:xxxx:xxxx:xxxx:xxxx:xxxx:xxxxx
;; Query time: 4267 msec

ほらぁー……

という訳で、原因が分かったので対策します。

今回は /etc/rc.conflocal_unbound_enable="YES" こういうことして、 unbound をローカル用のリゾルバにしてたので、そこに固定のレコードを入れることに。

/etc/unbound/unbound.confserver セクションに以下の行を追加。

local-zone: "0.0.0.2.0.e.4.d.1.1.0.0.b.0.4.2.ip6.arpa." static

そして

$ sudo /etc/rc.d/local_unbound restart
$ drill -x 240b:11:xxxx:xxxx:xxxx:xxxx:xxxx:xxxxx
;; ->>HEADER<<- opcode: QUERY, rcode: NXDOMAIN, id: 23003
;; Query time: 1 msec

ほい。この IP アドレスは誰も返事を返しません。あきらめてください。

これで ssh での接続が格段に速くなりました。やったね。

……これでいいのか?