Dec 14th, 2016

check external ip and geoip with ngx_mruby

この記事はmruby Advent Calendar 2016の14日目の記事です。 前日は@takumakumeさんのpmilterでmrubyを用いてメールのDDoSを軽減するでした。

スマフォなどでもグローバルIPが割り振られる事はなくなった現在で、external ipを知りたくなる場合は多いものです。
IPを確認するという行為はよくあることで、下記のようにたくさんサービスがありますが、これを自作しようとするものです。
簡単ですね。

http://ifconfig.io/
http://ifconfig.co/
http://ifconfig.me/
http://httpbin.org/
https://myexternalip.com
http://inet-ip.info
http://www.ugtop.com/spill.shtml

構築するにあたり色々と面倒なRackを挟むようなものではなく、より簡単に作れて軽量なものを作りたいので、組み込み型のmrubyとそれをnginxで使えるようした@matsumotory氏作のngx_mrubyを使います。

ビルド準備

構築環境は例によってdebian jessieを使います。
ngix_mrubyはビルドする必要がありますので、必要なライブラリをインストールすることから始めます。

$ sudo apt-get -qqy install git curl wget make gcc libc-dev libc6-dev ruby ruby2.1 ruby2.1-dev rake bison libcurl4-openssl-dev libssl-dev libhiredis-dev libmarkdown2-dev libcap-dev libcgroup-dev libpcre3 libpcre3-dev

ビルドするディレクトリを$HOME以下に作成する事とし、githubからngx_mrubyのソースをダウンロードし解凍します。

$ curl -fsSLo ngx_mruby.tgz https://github.com/matsumoto-r/ngx_mruby/tarball/master
$ mkdir ngx_mruby && tar -zxf ngx_mruby.tgz -C ngx_mruby --strip-components 1
$ cd ngx_mruby

mrubyライブラリ組み込みとビルド

ここでおもむろにビルド開始しても良いのですが、すこし変更点があります。デフォルトでは@mattn_jpさんのmruby-jsonが組み込まれますが、JSONを出力する際にインデントを伴って整形してくれる機能(pretty_printというようですが)がIIJのmruby-iijsonの方に存在するので、こちらに変更します。

--- a/build_config.rb
+++ b/build_config.rb
@@ -18,7 +18,7 @@ MRuby::Build.new('host') do |conf|
   conf.gem :github => 'iij/mruby-process'
   conf.gem :github => 'iij/mruby-pack'
   conf.gem :github => 'iij/mruby-socket'
-  conf.gem :github => 'mattn/mruby-json'
+  conf.gem :github => 'iij/mruby-iijson'
   conf.gem :github => 'mattn/mruby-onig-regexp'
   conf.gem :github => 'matsumoto-r/mruby-redis'
   conf.gem :github => 'matsumoto-r/mruby-vedis'

また既存のDebianのNginxに合わせるためnginxビルドのための環境変数を下記のように設定します。

$ export NGINX_CONFIG_OPT_ENV="--prefix=/etc/nginx \
--sbin-path=/usr/sbin/nginx \
--conf-path=/etc/nginx/nginx.conf \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--pid-path=/var/run/nginx.pid \
--lock-path=/var/run/nginx.lock \
--http-client-body-temp-path=/var/cache/nginx/client_temp \
--http-proxy-temp-path=/var/cache/nginx/proxy_temp \
--http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \
--http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \
--http-scgi-temp-path=/var/cache/nginx/scgi_temp \
--user=www-data --group=www-data \
--with-http_ssl_module \
--with-http_realip_module \
--with-http_addition_module \
--with-http_sub_module \
--with-http_flv_module \
--with-http_mp4_module \
--with-http_gunzip_module \
--with-http_gzip_static_module \
--with-http_random_index_module \
--with-http_secure_link_module \
--with-http_stub_status_module \
--with-http_auth_request_module \
--with-threads \
--with-stream \
--with-stream_ssl_module \
--with-http_slice_module \
--with-file-aio \
--with-http_v2_module"

設定が終われば、build.shを実行します。

$ bash build.sh

問題なく終了すれば、(現時点で最新1.11.7!nginxの)ソースがビルドされ、mrubyが組み込まれたものがbuild/nginx-[NGINX_VERSION]/objsにできています。 インストールします。

$ cd build/nginx-1.11.7
$ sudo make install

インストールは終わったら、nginxの状況を確認します。

$ sudo -i
# nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: [emerg] mkdir() "/var/cache/nginx/client_temp" failed (2: No such file or directory)
nginx: configuration file /etc/nginx/nginx.conf test failed

私の環境では上のようなエラーが表示されましたが、原因はなぜかディレクトリが作成できていないようです。

# mkdir -p /var/cache/nginx/client_temp
# nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

できました。念のためnginx -Vでmrubyが組み込まれてビルドしているこを確認してください。

# nginx -V

nginxの設定とmrubyでipを返すコード

さてnginx.confの設定ですが、詳細な設定方法は公式のドキュメントに譲らせていただいて、nginx.confを下記のように書き換えます。

worker_processes  1;
events { }

http {
  server {
    listen       80;

    location / {
      mruby_content_handler_code '
        Nginx.echo("hello")
      ';
    }
  }
}

nginxの起動スクリプトなども用意する必要はありますが、こちらも省略させてもらって下記のように起動します。 defaultで/etc/nginx/nginx.confが設定されるので、-c以降は本来不要ですが・・・。

# nginx -c /etc/nginx/nginx.conf

起動できていれば、curlで以下のようにしてmrubyを確認することができます。

# curl localhost
hello

ようやくexternal ipの取得のコードを組み込みますが、以下のようにすれば良いだけです。簡単ですね。

--- a/nginx.conf
+++ b/nginx.conf
@@ -7,7 +7,8 @@ http {

     location / {
       mruby_content_handler_code '
-        Nginx.echo("hello")
+        req = Nginx::Request.new
+        Nginx.echo(req.var.remote_addr)
       ';
     }
   }

編集が終わればnginxをリロードし、外部からcurlでアクセスすればIPが帰ってきます。

# nginx -s reload

これだけでは芸が無いのでJSONで返すようにしてみます。

--- a/nginx.conf
+++ b/nginx.conf
@@ -8,7 +8,9 @@ http {
     location / {
       mruby_content_handler_code '
         req = Nginx::Request.new
-        Nginx.echo(req.var.remote_addr)
+        hash = {}
+        hash["origin"] = req.var.remote_addr
+        Nginx.echo JSON.generate(hash, {pretty_print: true, indent_with: 2})
       ';
     }
   }

するとこうなります。mruby-iijsonの機能であるpretty_printの指定を有効にしてインデント幅を指定することで綺麗に整形されます。

# curl localhost
{
  "origin": "127.0.0.1"
}

GeoIP組み込み

少し欲をだしてIP情報のGEOIP情報も同時に出してみます。 まずはGeoIPのライブラリが必要ですので、インストールするのですが debian ではGeoIPのデータベースが3種類あり、以下のようになっています。

geoip-database - IP lookup command line tools that use the GeoIP library (country database)
geoip-database-extra - IP lookup command line tools that use the GeoIP library (ASN/city database)
geoip-database-contrib - GeoLite binary database (downloader)

このうち一番情報量が多いのが geoip-database-contrib なのでこちらを採用します。contrib を有効にする必要がありますので、/etc/apt/sources.listの情報を変更します。

--- a/sources.list
+++ b/sources.list
@@ -1,9 +1,9 @@
-deb http://httpredir.debian.org/debian jessie main
-deb-src http://httpredir.debian.org/debian/ jessie main
+deb http://httpredir.debian.org/debian jessie main contrib
+deb-src http://httpredir.debian.org/debian/ jessie main contrib

-deb http://security.debian.org/ jessie/updates main
-deb-src http://security.debian.org/ jessie/updates main
+deb http://security.debian.org/ jessie/updates main contrib
+deb-src http://security.debian.org/ jessie/updates main contrib

 # jessie-updates, previously known as 'volatile'
-deb http://httpredir.debian.org/debian/ jessie-updates main
-deb-src http://httpredir.debian.org/debian/ jessie-updates main
+deb http://httpredir.debian.org/debian/ jessie-updates main contrib
+deb-src http://httpredir.debian.org/debian/ jessie-updates main contrib

編集が終われば以下のようにしてインストールします。

$ sudo apt update && sudo apt install geoip-bin libgeoip-dev geoip-database-contrib

インストールが終わったら、ngx_mrubyをビルドしたディレクトリに戻り nginxビルドのための環境変数を下記のように設定します。 前回からは最後の1行を追加しただけです。

$ export NGINX_CONFIG_OPT_ENV="--prefix=/etc/nginx \
--sbin-path=/usr/sbin/nginx \
--conf-path=/etc/nginx/nginx.conf \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--pid-path=/var/run/nginx.pid \
--lock-path=/var/run/nginx.lock \
--http-client-body-temp-path=/var/cache/nginx/client_temp \
--http-proxy-temp-path=/var/cache/nginx/proxy_temp \
--http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \
--http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \
--http-scgi-temp-path=/var/cache/nginx/scgi_temp \
--user=www-data --group=www-data \
--with-http_ssl_module \
--with-http_realip_module \
--with-http_addition_module \
--with-http_sub_module \
--with-http_flv_module \
--with-http_mp4_module \
--with-http_gunzip_module \
--with-http_gzip_static_module \
--with-http_random_index_module \
--with-http_secure_link_module \
--with-http_stub_status_module \
--with-http_auth_request_module \
--with-threads \
--with-stream \
--with-stream_ssl_module \
--with-http_slice_module \
--with-file-aio \
--with-http_v2_module \
--with-http_geoip_module "

次は build_config.rbのGeoIPのモジュールの組み込みでコメントを外します。

--- a/build_config.rb
+++ b/build_config.rb
@@ -45,7 +45,7 @@ MRuby::Build.new('host') do |conf|
   #conf.gem :github => 'mattn/mruby-mysql'

   # have GeoIPCity.dat
-  # conf.gem :github => 'matsumoto-r/mruby-geoip'
+  conf.gem :github => 'matsumoto-r/mruby-geoip'

   # Linux only for ngx_mruby
   # conf.gem :github => 'matsumoto-r/mruby-capability'

修正が終わったら先のビルドで使ったディレクトリを消去してから、再度ビルドを行います。

$ rm -rf build && bash build.sh

ビルドが成功している事を確認して、インストールするのですが、現在動いているnginxはkillしてから行います。

$ cd build/nginx-1.11.7/
$ sudo kill $(pgrep nginx | head -n 1)
$ make install

次にnginx.confの修正ですが、ソースを見るとgeoip_orgが無いようなのでそれ以外について出力してみます。

--- a/nginx.conf
+++ b/nginx.conf
@@ -7,10 +7,25 @@ http {

     location / {
       mruby_content_handler_code '
+        geoip = GeoIP.new "/usr/share/GeoIP/GeoIPCity.dat"
         req = Nginx::Request.new
-        Nginx.echo(req.var.remote_addr)
+        ip = req.var.remote_addr
+
         hash = {}
-        hash["origin"] = req.var.remote_addr
+        hash["origin"] = ip
+        begin
+          geoip.record_by_name(ip)
+        rescue => err
+          hash["error"] = err
+        else
+          hash["country-code"] = geoip.country_code
+          hash["city"]         = geoip.city if geoip.city != "N/A"
+          hash["region"]       = geoip.region if geoip.region != "N/A"
+          hash["region-name"]  = geoip.region_name if geoip.region_name != "N/A"
+          hash["timezone"]     = geoip.time_zone if geoip.time_zone != "N/A"
+          hash["loc"]          = "#{geoip.latitude.round(4)}:#{geoip.longitude.round(4)}"
+        end
+
         Nginx.echo JSON.generate(hash, {pretty_print: true, indent_with: 2})
       ';
     }

これでnginxを再起動して、外からアクセスすると次のような感じで表示されると思います。(サーバ内部からアクセスするとエラーとなりますので、外からアクセスしてください)

$ curl [SERVER-IP]
{
  "origin": "xxx.xx.xxx.xxx",
  "country-code": "JP",
  "city": "Tokyo",
  "region": "40",
  "region-name": "Tokyo",
  "timezone": "Asia/Tokyo",
  "loc": "35.xxxx:139.xxxx"
}

まとめ

ちょっと細かく書きすぎたせいで予定していた分の3割もかけていないのですが、mrubyのソースをインラインで書いているので、修正するたびにnginxのリロードが必要となってますが、別ファイルとするとnginxのリロードは必要とせず、編集すればすぐ反映されるという点もお手軽に試せて良い感じです。今回はそこまでできませんでしたが、難しくはありませんので、試してもらえればと思います。

参考文献

@hsbtさんのnginx 実践入門 9 章の "Lua による nginx の拡張" を ngx_mruby を用いて実現したサンプルコード