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

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.

Link to this function

from(headers, opts \\ [])

View Source
@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.