続・攻性防壁
以前書いたエントリの続き。 管理しているサーバの1つが、同一IPから1000回以上のアタックを受けていたので、今度こそ自動的に拒否リストに追加する仕組みを考えてみる。 rubyで実装するとして、デーモンとして常駐するとかhosts.allowから呼び出すとかも考えたけど、あんま激しく呼び出されるとそれ自体が負荷をかけてしまう(ruby自体がけっこう重たいし)ということで、ヌルくcronから呼び出すことにする。
と言うわけで、要件はこんな感じ。
- 一定時間ごとにcronで起動
- /var/log/messagesを解析してアタックを行っているIPアドレスを抽出し、deny.listに追加する
- 回数が閾値以下の場合は追加しない(自分で間違ってアクセスしたとたんにBANとかは避けたい)
- ターンオーバーして圧縮されたログも解析できるようにする
- IPアドレスは重複して登録しない、またできればdeny.listは毎回ソートする
- ホワイトリストに登録されているIPアドレスは登録しない
半日くらいで書けた。名前はギブスンにあやかってIcewallとしてみた。すごくなんかと被ってそうです。 以下、工夫したこととか。
せっかくなのでoptparse使ってみた。 実は初めて。PerlでGetopt::Longとか使った朧気な記憶が。
opt = OptionParser.new
opt.on('-d', '--deny=DENY_ADDRESSES', String, 'specify IP addresses to deny.') {|var| @denyaddr << var }
opt.on('-q', '--quiet', 'quiet mode.') { @quiet = true }
opt.parse!(ARGV)
とかやると、–helpを付けて起動すればそれっぽいヘルプを表示してくれる。 でもなんか複数のパラメタをちゃんと取れてない気がする。
IPアドレスの正規表現は、一発でマッチするように
str.scan(/(\d|[01]?\d\d|2[0-4]\d|25[0-5])\.(\d|[01]?\d\d|2[0-4]\d|25[0-5])\.(\d|[01]?\d\d|2[0-4]\d|25[0-5])\.(\d|[01]?\d\d|2[0-4]\d|25[0-5])/)
としてみたけど、なんか誤動作する(^$でくくれば一致するんだけど)。あれこれ悩んだけど面倒になったので、
str.scan(/\b(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3}\b)/).select{|a| a.all?{|b| (0..255).include?(b.to_i)}}.map{|a| a.join('.')}
という超強引な方法で解決。ほ、ほら全部正規表現で書くより短いよ! ソートはこんな感じ。これは昔Perlでさんざんやった覚えがある。
addresses.sort_by{|a| a.split(".").map {|c| c.to_i}.pack("C4")}
その他、最初はパターンをコマンドラインに書いてたけど、やってみたらcronをいくつも書くのがめんどくさかったので、YAMLでレシピを書くようにしてみた。これなら新しい種類のアタックがあっても対応しやすい。 圧縮ログについては、bzip2ライブラリがよく分からなかったので、標準入力を受け付けるようにしてbzcatとパイプで繋げた。結果的にこの方が便利だけど若干敗北感。 APIをかっこよくしたくてActionHelperあたりのソースを読んだけど、それほど難しいことをしてるわけでもなく分かりやすかった。 あと、クラス書くときにrspec使ってみたりした。
さっそくcronで回して観測中。今のところうまく行ってるみたい。 ちゃんと動作して気が向いたら公開します。