この記事は mruby Advent Calendar 2016 の13日目の記事です。
本エントリでは matsumotory さんが作成された、mrubyでメールの制御を行うことができる pmilter を使ってSMTPのDDoSを軽減するソフトウェアを作ってみたのでその紹介をします。
pmilterとは?
pmilterはProgrammable Mail Filterの略で、SMTPサーバ(送信や受信)とmilterプロトコルで通信し、SMTPサーバの送受信の振る舞いをRubyでコントロールできるサーバソフトウェアです。
インストールや設定はシンプルでpmilterのバイナリを配置して通常のmilterのようにSMTPサーバに設定するだけです。
### postfixの設定
smtpd_milters = unix:/pmilter/pmilter.sock
そして、milterプロトコルの各フェーズで任意のmrubyスクリプトをフックします。
### pmilterの設定
: (snip)
[handler]
# connection info filter handler
mruby_connect_handler = "handler/connect.rb"
# SMTP HELO command filter handler
mruby_helo_handler = "handler/helo.rb"
: (snip)
ngx_mrubyやmod_mrubyを利用したことのある方ならよりイメージしやすいと思います。
詳しくは以下をご参照ください。
pmilterのセットアップ
筆者はCentOS7で動作確認を行いました。セットアップについては以下のエントリをご参照ください。
pmilterでメールのDDoSを軽減する
smtp-dos-detector
今回、takumakume/smtp-dos-detector というソフトウェアを作りました。 これは、一定間隔にSMTP接続できる回数を制限したり、送信できるメール通数を制御したりできます。
pmilterのビルド
pmilterの “src/mruby/build_config.rb” でpmilterで実行するmruby scriptで使うmgemの設定を行うところがあります。今回は以下のmgemを利用しますので設定してpmilterを再ビルドします。
- matsumotory/mruby-mutex
- matsumotory/mruby-localmemcache
# git diff src/mruby/build_config.rb
diff --git a/src/mruby/build_config.rb b/src/mruby/build_config.rb
index bc9e69e..c157a37 100644
--- a/src/mruby/build_config.rb
+++ b/src/mruby/build_config.rb
@@ -18,6 +18,8 @@ MRuby::Build.new do |conf|
# conf.gem 'examples/mrbgems/c_and_ruby_extension_example'
# conf.gem :github => 'masuidrive/mrbgems-example', :checksum_hash => '76518e8aecd131d047378448ac8055fa29d974a9'
# conf.gem :git => 'git@github.com:masuidrive/mrbgems-example.git', :branch => 'master', :options => '-v'
+ conf.gem :github => 'matsumotory/mruby-mutex'
+ conf.gem :github => 'matsumotory/mruby-localmemcache'
# include the default GEMs
conf.gembox 'default'
再ビルド
make mruby
make
smtp-dos-detectorの使い方
今回は、IPアドレス単位で一定間隔にSMTP接続をできる回数を制限してみます。
- pmilter.conf に以下のように設定する。
[handler]
# connection info filter hhandler/connect.rb"
mruby_connect_handler = "handler/smtp-dos-detector/src/smtp_dos_detector.rb"
- handler/smtp-dos-detector/src/smtp_dos_detector.rb のように smtp-dos-detector のmruby scriptを配置します。以下のようになっていると思います。
: (snip)
target = Pmilter::Session.new.client_ipaddr
config = {
:counter_key => target,
:threshold_time => 10,
:threshold_counter => 5,
:expire_time => 30,
:behind_counter => -10,
}
timeout = global_mutex.try_lock_loop(50000) do
dos = DosDetector.new config
data = dos.analyze
p "smtp-dos-detector: analyze: #{data}"
begin
if dos.detect?
p "smtp-dos-detector: detect: #{data}"
Pmilter.status = Pmilter::SMFIS_REJECT
end
rescue => e
raise "smtp-dos-detector: fail: #{e}"
ensure
global_mutex.unlock
end
end
p "smtp-dos-detector: get timeout mutex lock, #{data}" if timeout
- 以下に、更に詳しい説明をしていきます。
target = Pmilter::Session.new.client_ipaddr
これは、pmilterからSMTP接続を行ったクライアントのIPアドレスを取得しています。 今回は、IPアドレス単位で一定間隔にSMTP接続をできる回数を制限するためです。
config = {
:counter_key => target,
:threshold_time => 10,
:threshold_counter => 5,
:expire_time => 30,
:behind_counter => -10,
}
:counter_key には、pmilterから取得したsmtp接続をしてきたクライアントのIPアドレスが入ります。 この設定の例では「“target"からのSMTP接続は10秒間(threshold_time)に5回(threshold_counter)まで可能で、それ以降の10回(behind_counter)の接続は30秒(expire_time)間rejectします。」となります。
if dos.detect?
p "smtp-dos-detector: detect: #{data}"
Pmilter.status = Pmilter::SMFIS_REJECT
end
ここでは、条件にマッチしてDDoSと判定した接続に対してREJECTを返しています。
smtp-dos-detectorの動作
実際にメールを送信した場合にどのような挙動になるのかについて説明します。
テストのため以下のコマンドを実行してローカルからメールを送信します。
echo "test" | mail -s "test" user@hoge.local
メールを送信したときのpmilterの出力結果です。
# 1回目の接続:許可 (10秒以内)
"smtp-dos-detector: analyze: {:time_diff=>0, :counter=>0, :counter_key=>\"127.0.0.1\"}"
# 2回目の接続:許可 (10秒以内)
"smtp-dos-detector: analyze: {:time_diff=>2, :counter=>1, :counter_key=>\"127.0.0.1\"}"
# 3回目の接続:許可 (10秒以内)
"smtp-dos-detector: analyze: {:time_diff=>4, :counter=>2, :counter_key=>\"127.0.0.1\"}"
# 4回目の接続:許可 (10秒以内)
"smtp-dos-detector: analyze: {:time_diff=>4, :counter=>3, :counter_key=>\"127.0.0.1\"}"
# 5回目の接続:許可 (10秒以内)
"smtp-dos-detector: analyze: {:time_diff=>6, :counter=>4, :counter_key=>\"127.0.0.1\"}"
# 6回目の接続:拒否 (10秒以内)
"smtp-dos-detector: analyze: {:time_diff=>7, :counter=>5, :counter_key=>\"127.0.0.1\"}"
"smtp-dos-detector: detect: {:time_diff=>7, :counter=>5, :counter_key=>\"127.0.0.1\"}"
6回目の接続時にpostfixに出力されるログ
Dec 12 14:14:43 pmilter sendmail[5143]: uBCEEhjZ005143: to=postmaster, delay=00:00:00, xdelay=00:00:00, mailer=relay, pri=32273, relay=[127.0.0.1], dsn=5.0.0, stat=Service unavailable
“Service unavailable” を返している事が分かる。
# 7回目の接続:拒否 (30秒以内)
"smtp-dos-detector: analyze: {:time_diff=>0, :counter=>-9, :counter_key=>\"127.0.0.1\"}"
"smtp-dos-detector: detect: {:time_diff=>0, :counter=>-9, :counter_key=>\"127.0.0.1\"}"
:
# 15回目の接続:拒否 (30秒以内)
"smtp-dos-detector: analyze: {:time_diff=>8, :counter=>-1, :counter_key=>\"127.0.0.1\"}"
"smtp-dos-detector: detect: {:time_diff=>8, :counter=>-1, :counter_key=>\"127.0.0.1\"}"
# 16回目の接続:許可
"smtp-dos-detector: analyze: {:time_diff=>9, :counter=>0, :counter_key=>\"127.0.0.1\"}"
# 17回目の接続:許可
"smtp-dos-detector: analyze: {:time_diff=>10, :counter=>1, :counter_key=>\"127.0.0.1\"}"
:
:
このように、大量にアクセスされた場合に適切なリターンコードでアクセスを拒否することができます。 また、smtp_dos_detector.rbのフックポイントを変えて、targetを変えることでToやFromをベースとした制御などの応用ができます。