Rack アプリで、クライアントのIPアドレスを取得する方法を調べてみたのでメモ。
どうやら Rack::Request#ip
を使えばいいらしいので Rack アプリはこんな感じで。
[config.ru]
class HogeApp def call(env) req = Rack::Request.new(env) [200, {}, [ "req.ip=#{req.ip}\n", "REMOTE_ADDR=#{env["REMOTE_ADDR"]}\n", "HTTP_X_FORWARDED_FOR=#{env["HTTP_X_FORWARDED_FOR"]}\n", ]] end end run HogeApp.new
普通に接続
これを 192.0.2.11
サーバーで次のようにして起動して、
rackup -o 0.0.0.0
192.0.2.1
のクライアントからアクセスしてみる。
こんな感じになってる。
[client] 192.0.2.1 ↓ 192.0.2.11 [app]
% curl http://192.0.2.11:9292 req.ip=192.0.2.1 REMOTE_ADDR=192.0.2.1 HTTP_X_FORWARDED_FOR=
ちゃんとクライアントのIPアドレスが取得できた。
REMOTE_ADDR
と同じ値になってる。
リバースプロキシ経由で接続
同じサーバーの Apache の設定に次のように書いて、
ProxyPass /hoge/ http://127.0.0.1:9292/
Apache 経由で接続してみる。
[client] 192.0.2.1 ↓ 192.0.2.11 [proxy] 127.0.0.1 ↓ 127.0.0.1 [app]
% curl http://192.0.2.11/hoge/ req.ip=192.0.2.1 REMOTE_ADDR=127.0.0.1 HTTP_X_FORWARDED_FOR=192.0.2.1
やっぱりちゃんとクライアントのIPアドレスを取得できる。
REMOTE_ADDR
はアプリに直接繋いでいる Apache のIPアドレスになるので 127.0.0.1
だけど、Apache はプロキシ時に X-Forwarded-For
ヘッダにクライアントのIPアドレスを設定するので、その値を使用している。
プロキシが多段になっている場合
別の 192.0.2.10
サーバーにもう1個 Apache をリバースプロキシとして立てる。
ProxyPass /hoge/ http://192.0.2.11/hoge/
こんな感じ。
[client] 192.0.2.1 ↓ 192.0.2.10 [proxy] 192.0.2.10 ↓ 192.0.2.11 [proxy] 127.0.0.1 ↓ 127.0.0.1 [app]
% curl http://192.0.2.10/hoge/ req.ip=192.0.2.10 REMOTE_ADDR=127.0.0.1 HTTP_X_FORWARDED_FOR=192.0.2.1, 192.0.2.10
X-Forwarded-For
ヘッダには二つ値が入っている。
Rack::Request#ip
はクライアントのIPアドレスではなくて1個めのプロキシサーバーのIPアドレスになってしまった。
Rack は X-Forwarded-For
ヘッダの最後の値をクライアントのIPアドレスとして使用するようになっているためである。
ところで、ここでプロキシサーバーをプライベートネットワークに置いて同じことをやってみる。
こんな感じ。
[client] 192.168.0.1 ↓ 192.168.0.10 [proxy] 192.168.0.10 ↓ 192.168.0.11 [proxy] 127.0.0.1 ↓ 127.0.0.1 [app]
% curl http://192.168.0.10/hoge/ req.ip=192.168.0.1 REMOTE_ADDR=127.0.0.1 HTTP_X_FORWARDED_FOR=192.168.0.1, 192.168.0.10
こんどはちゃんとクライアントのIPアドレスが取得できた。
Rack が X-Forwarded-For
から値を選択する際、どうやらプライベートアドレスは信頼できるプロキシとみなして除外するようになっているらしい。
つまり「X-Forwarded-For
の最後の値」ではなくて「X-Forwarded-For
の中の信頼できないIPアドレスの一番後ろにあるもの」を使用するという動きになっている。
なお、今回のようにプライベートアドレスを除外すると X-Forwarded-For
の値が空になる場合は、最初の値が使用される。
信頼できるIPアドレス
とは言っても、プライベートなIPアドレスが本当にすべて信頼できるものとは限らないし、グローバルなIPアドレスでも信頼できるプロキシなので除外したいこともあるかもしれない。
IPアドレスのフィルタは Rack::Request.ip_filter
に Proc を設定することで制御できる。
デフォルトはこんな風になっていて、ローカルアドレスとプライベートアドレスとUNIXドメインソケットで真を返すようになっている。
self.ip_filter = lambda { |ip| /\A127\.0\.0\.1\Z|\A(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|\A::1\Z|\Afd[0-9a-f]{2}:.+|\Alocalhost\Z|\Aunix\Z|\Aunix:/i.match?(ip) }
上の例で、192.0.2.10
を信頼できるプロキシとしたい場合は、その条件を追加すればいい。
orig_ip_filter = Rack::Request.ip_filter Rack::Request.ip_filter = ->(ip){ return true if orig_ip_filter.call(ip) ip == '192.0.2.10' } class HogeApp 〜以下略〜
% curl http://192.0.2.10/hoge/ req.ip=192.0.2.1 REMOTE_ADDR=127.0.0.1 HTTP_X_FORWARDED_FOR=192.0.2.1, 192.0.2.10
ちゃんとクライアントの 192.0.2.1
が取れるようになった。
逆にプライベートだけど 192.168.0.10
は信頼できないという場合はこんな感じで。
orig_ip_filter = Rack::Request.ip_filter Rack::Request.ip_filter = ->(ip){ return false if ip == '192.168.0.10' orig_ip_filter.call(ip) } class HogeApp 〜以下略〜
% curl http://192.168.0.10/hoge/ req.ip=192.168.0.10 REMOTE_ADDR=127.0.0.1 HTTP_X_FORWARDED_FOR=192.168.0.1, 192.168.0.10
192.168.0.10
がクライアントとして返されるようになる。
もちろん Rack::Request.ip_filter
を直接書き換えてしまっても構わないと思うけど、縁起物なので元の定義を使うようにしてみた。