When running a Sinatra application we will typically place an Nginx web proxy in front of the application. One drawback of this is obtaining the IP address of the connecting client when using the Ruby Rack method request.ip
. This method shown below:
def ip
remote_addrs = split_ip_addresses(get_header('REMOTE_ADDR'))
remote_addrs = reject_trusted_ip_addresses(remote_addrs)
return remote_addrs.first if remote_addrs.any?
forwarded_ips = split_ip_addresses(get_header('HTTP_X_FORWARDED_FOR'))
.map { |ip| strip_port(ip) }
return reject_trusted_ip_addresses(forwarded_ips).last || forwarded_ips.first || get_header("REMOTE_ADDR")
end
Can be found in the lib/rack/request.rb file. As you can see it makes use of trusted ip addresses to attempt to reject “trusted” upstream proxies, however the method that performs these checks is somewhat limited:
def trusted_proxy?(ip)
Rack::Request.ip_filter.call(ip)
end
The ip_filter is a lambda: lambda { |ip| 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 }
, so we are restricting our trusted range of addresses to just those used on private IP ranges. Not ideal in a more complex production environment.
The solution without coding up something to replace request.ip
? Well you can monkey patch the method. In Sinatra the Rack::Request
object is actually as you might expect Sinatra::Request
. Our approach to the money patch is this:
module Sinatra
class Request
def trusted_proxy?(ip)
super || upstream_proxy?(ip)
end
private
def upstream_proxy?(ip)
proxies = defined?(Settings) ? Array(Settings.upstream_proxies) : []
proxies.include? ip
end
end
end
We regularly make use of the Ruby Config gem for our application configuration and So we’re pulling the proxies
out of that configuration object if it exists. This means we can simply define our upstream proxy in our settings.local.yml
file like this:
---
upstream_proxies:
- 0.0.0.0
- 0.0.0.0
This approach avoids breaking the original method, but allows us to work with our real world infrastructure. Our Nginx proxy configuration therefore only needs a line like:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;