続・攻性防壁

以前書いたエントリの続き。 管理しているサーバの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で回して観測中。今のところうまく行ってるみたい。 ちゃんと動作して気が向いたら公開します。