Rack アプリでクライアントのIPアドレスを取得する

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 を直接書き換えてしまっても構わないと思うけど、縁起物なので元の定義を使うようにしてみた。