View Source RemoteIp (remote_ip v1.2.0)
A plug to rewrite the Plug.Conn
's remote_ip
based on forwarding headers.
Generic comma-separated headers like X-Forwarded-For
, X-Real-Ip
, and
X-Client-Ip
are all recognized, as well as the RFC
7239 Forwarded
header. IPs are
processed last-to-first to prevent IP spoofing. Read more in the
documentation for the algorithm.
This plug is highly configurable, giving you the power to adapt it to your particular networking infrastructure:
IPs can come from any header(s) you want. You can even implement your own custom parser if you're using a special format.
You can configure the IPs of known proxies & clients so that you never get the wrong results.
All options are configurable at runtime, so you can deploy a single release but still customize it using environment variables, the
Application
environment, or any other arbitrary mechanism.Still not getting the right IP? You can recompile the plug with debugging enabled to generate logs, and even fine-tune the verbosity by selecting which events to track.
Usage
This plug should be early in your pipeline, or else the remote_ip
might not
get rewritten before your route's logic executes.
In Phoenix, this might mean plugging RemoteIp
into your endpoint before the router:
defmodule MyApp.Endpoint do
use Phoenix.Endpoint, otp_app: :my_app
plug RemoteIp
# plug ...
# plug ...
plug MyApp.Router
end
But if you only want to rewrite IPs in a narrower part of your app, you could of course put it in an individual pipeline of your router.
In an ordinary Plug.Router
, you should make sure RemoteIp
comes before
the :match
/:dispatch
plugs:
defmodule MyApp do
use Plug.Router
plug RemoteIp
plug :match
plug :dispatch
# get "/" do ...
end
You can also use RemoteIp.from/2
to determine an IP from a list of headers.
This is useful outside of the plug pipeline, where you may not have access to
the Plug.Conn
. For example, you might only be getting the x_headers
from
Phoenix.Socket
:
defmodule MySocket do
use Phoenix.Socket
def connect(params, socket, connect_info) do
ip = RemoteIp.from(connect_info[:x_headers])
# ...
end
end
Configuration
Options may be passed as a keyword list via RemoteIp.init/1
or directly
into RemoteIp.from/2
. At a high level, the following options are available:
:headers
- a list of header names to consider:parsers
- a map from header names to custom parser modules:clients
- a list of known client IPs, either plain or in CIDR notation:proxies
- a list of known proxy IPs, either plain or in CIDR notation
You can specify any option using a tuple of {module, function_name, arguments}
, which will be called dynamically at runtime to get the
equivalent value.
For more details about these options, see RemoteIp.Options
.
Troubleshooting
Getting the right configuration can be tricky. Requests might come in with unexpected headers, or maybe you didn't account for certain proxies, or any number of other issues.
Luckily, you can debug RemoteIp.call/2
and RemoteIp.from/2
by updating
your Config
file:
config :remote_ip, debug: true
and recompiling the :remote_ip
dependency:
$ mix deps.clean --build remote_ip
$ mix deps.compile
Then it will generate log messages showing how the IP gets computed. For more
details about these messages, as well advanced usage, see
RemoteIp.Debugger
.
Metadata
When you use this plug, RemoteIp.call/2
will populate the Logger
metadata
under the key :remote_ip
. This will be the string representation of the
final value of the Plug.Conn
's remote_ip
. Even if no client was found in
the headers, we still set the metadata to the original IP.
You can use this in your logs by updating your Config
file:
config :logger,
message: "$metadata[$level] $message\n",
metadata: [:remote_ip]
Then your logs will look something like this:
[info] Running ExampleWeb.Endpoint with cowboy 2.8.0 at 0.0.0.0:4000 (http)
[info] Access ExampleWeb.Endpoint at http://localhost:4000
remote_ip=1.2.3.4 [info] GET /
remote_ip=1.2.3.4 [debug] Processing with ExampleWeb.PageController.index/2
Parameters: %{}
Pipelines: [:browser]
remote_ip=1.2.3.4 [info] Sent 200 in 21ms
Note that metadata will not be set by RemoteIp.from/2
.
Summary
Functions
The Plug.call/2
callback.
Extracts the remote IP from a list of headers.
The Plug.init/1
callback.
Functions
The Plug.call/2
callback.
Rewrites the Plug.Conn
's remote_ip
based on its forwarding headers. Each
call will re-evaluate all runtime options. See RemoteIp.Options
for
details.
@spec from( Plug.Conn.headers(), keyword() ) :: :inet.ip_address() | nil
Extracts the remote IP from a list of headers.
In cases where you don't have access to a full Plug.Conn
struct, you can
use this function to process the remote IP from a list of key-value pairs
representing the headers.
You may specify the same options as if you were using the plug. Runtime
options are evaluated each time you call this function. See
RemoteIp.Options
for details.
If no client IP can be found in the given headers, this function will return
nil
.
Examples
iex> RemoteIp.from([{"x-forwarded-for", "1.2.3.4"}])
{1, 2, 3, 4}
iex> [{"x-foo", "1.2.3.4"}, {"x-bar", "2.3.4.5"}]
...> |> RemoteIp.from(headers: ~w[x-foo])
{1, 2, 3, 4}
iex> [{"x-foo", "1.2.3.4"}, {"x-bar", "2.3.4.5"}]
...> |> RemoteIp.from(headers: ~w[x-bar])
{2, 3, 4, 5}
iex> [{"x-foo", "1.2.3.4"}, {"x-bar", "2.3.4.5"}]
...> |> RemoteIp.from(headers: ~w[x-baz])
nil
The Plug.init/1
callback.
This accepts the keyword options described by RemoteIp.Options
. Because
plug initialization typically happens at compile time, we make sure not to
evaluate runtime options until call/2
.