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
Applicationenvironment, 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
endBut 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 ...
endYou 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
endConfiguration
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: trueand 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 21msNote 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.