View Source RemoteIp.Options (remote_ip v1.2.0)

The keyword options given to RemoteIp.init/1 or RemoteIp.from/2.

You shouldn't need to use this module directly. Its functions are used internally by RemoteIp to process configurations and support MFA-style runtime options.

You may pass any of the following keyword arguments into the plug (they get passed to RemoteIp.init/1). You can also pass the same keywords directly to RemoteIp.from/2.

:headers

The :headers option should be a list of strings. These are the names of headers that contain forwarding information. The default is

["forwarded", "x-forwarded-for", "x-client-ip", "x-real-ip"]

Every request header whose name exactly matches one of these strings will be parsed for IP addresses, which are then used to determine the routing information and ultimately the original client IP. Note that Plug normalizes headers to lowercase, so this option should consist of lowercase names.

In production, you likely want this to be a singleton - a list of only one string. There are a couple reasons:

  1. You usually can't rely on servers to preserve the relative ordering of headers in the HTTP request. For example, the Cowboy server presently uses maps to represent headers, which don't preserve key order. The order in which we process IPs matters because we take that as the routing information for the request. So if you have multiple competing headers, the routing might be ambiguous, and you could get bad results.

  2. It could also be a security issue. Say you're only expecting one header like X-Forwarded-For, but configure multiple headers like ["x-forwarded-for", "x-real-ip"]. Then it'd be easy for a malicious user to just set an extra X-Real-Ip header and interfere with the IP parsing (again, due to the sensitive nature of header ordering).

We still allow multiple headers because:

  1. Users can get up & running faster if the default configuration recognizes all of the common headers.

  2. You shouldn't be relying that heavily on IP addresses for security. Even a single plain-text header has enough problems on its own that we can't guarantee its results are accurate. For more details, see the documentation for the algorithm.

  3. It's more general. Networking setups are often very idiosyncratic, and we want to give users the option to use multiple headers if that's what they need.

:parsers

The :parsers option should be a map from strings to modules. Each string should be a header name (lowercase), and each module should implement the RemoteIp.Parser behaviour. The default is

%{"forwarded" => RemoteIp.Parsers.Forwarded}

Headers with the given name are parsed using the given module. If a header is not found in this map, it will be parsed by RemoteIp.Parsers.Generic. So you can use this option to:

  • add a parser for your own custom header

  • specialize on the generic parsing of headers like "x-forwarded-for"

  • replace any of the default parsers with one of your own

The map you provide for this option is automatically merged into the default using Map.merge/2. That way, the stock parsers won't be overridden unless you explicitly provide your own replacement.

:proxies

The :proxies option should be a list of strings - either individual IPs or ranges in CIDR notation. The default is

[]

For the sake of efficiency, you should prefer CIDR notation where possible. So instead of listing out 256 different addresses for the 1.2.3.x block, you should say "1.2.3.0/24".

These proxies are skipped by the algorithm and are never considered the original client IP, unless specifically overruled by the :clients option.

In addition to the proxies listed here, note that the following reserved IP addresses are also skipped automatically, as they are presumed to be internal addresses that don't belong to the client:

  • IPv4 loopback: 127.0.0.0/8
  • IPv6 loopback: ::1/128
  • IPv4 private network: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
  • IPv6 unique local address: fc00::/7

:clients

The :clients option should be a list of strings - either individual IPs or ranges in CIDR notation. The default is

[]

For the sake of efficiency, you should prefer CIDR notation where possible. So instead of listing out 256 different addresses for the 1.2.3.x block, you should say "1.2.3.0/24".

These addresses are never considered to be proxies by the algorithm. For example, if you configure the :proxies option to include "1.2.3.0/24" and the :clients option to include "1.2.3.4", then every IP in the 1.2.3.x block would be considered a proxy except for 1.2.3.4.

This option can also be used on reserved IP addresses that would otherwise be skipped automatically. For example, if your routing works through a local network, you might actually consider addresses in the 10.x.x.x block to be clients. You could permit the entire block with "10.0.0.0/8", or even specific IPs in this range like "10.1.2.3".

Runtime options

Every option can also accept a tuple of three elements: {module, function, arguments} (MFA). These are passed to Kernel.apply/3 at runtime, allowing you to dynamically configure the plug, even though the Plug.Builder generally calls Plug.init/1 at compilation time.

The return value from an MFA should be the same as if you were passing the literal into that option. For instance, the :proxies MFA should return a list of IP/CIDR strings.

The MFAs you give are re-evaluated on each call to RemoteIp.call/2 or RemoteIp.from/2. So be careful not to do anything too expensive at runtime. For example, don't download a list of known proxies, or else it will be re-downloaded on every request. Consider caching the download instead, perhaps using a library like Cachex.

Examples

Basic usage

Suppose you know:

  • you are behind proxies in the 1.2.x.x block
  • the proxies use the X-Real-Ip header
  • but the IP 1.2.3.4 is actually a client, not one of the proxies

Then you could say:

defmodule MyApp do
  use Plug.Router

  plug RemoteIp,
    headers: ~w[x-real-ip],
    proxies: ~w[1.2.0.0/16],
    clients: ~w[1.2.3.4]

  plug :match
  plug :dispatch

  # get "/" do ...
end

The same options may also be passed into RemoteIp.from/2:

defmodule MySocket do
  use Phoenix.Socket

  @options [
    headers: ~w[x-real-ip],
    proxies: ~w[1.2.0.0/16],
    clients: ~w[1.2.3.4]
  ]

  def connect(params, socket, connect_info) do
    ip = RemoteIp.from(connect_info[:x_headers], @options)
    # ...
  end
end

Custom parser

Suppose your proxies are using a header with a special format. The name of the header is X-Special and the format looks like ip=127.0.0.1.

First, you'd implement a custom parser:

defmodule SpecialParser do
  @behaviour RemoteIp.Parser

  @impl RemoteIp.Parser
  def parse(header) do
    ip = String.replace_prefix(header, "ip=", "")
    case :inet.parse_strict_address(ip |> to_charlist()) do
      {:ok, parsed} -> [parsed]
      _ -> []
    end
  end
end

Then you would configure the plug with that parser. Make sure to also specify the :headers option so that the X-Special header actually gets passed to the parser.

defmodule SpecialApp do
  use Plug.Router

  plug RemoteIp,
    headers: ~w[x-special],
    parsers: %{"x-special" => SpecialParser}

  plug :match
  plug :dispatch

  # get "/" do ...
end

Using MFAs

Suppose you're deploying a release and you want to get the proxy IPs from an environment variable. Because the release is compiled ahead of time, you shouldn't do a System.get_env/1 inline - it'll just be the value of the environment variable circa compilation time (probably empty!).

defmodule CompiledApp do
  use Plug.Router

  # DON'T DO THIS: the value of the env var gets compiled into the release
  plug RemoteIp, proxies: System.get_env("PROXIES") |> String.split(",")

  plug :match
  plug :dispatch

  # get "/" do ...
end

Instead, you can use an MFA to look up the variable at runtime:

defmodule RuntimeApp do
  use Plug.Router

  plug RemoteIp, proxies: {__MODULE__, :proxies, []}

  def proxies do
    System.get_env("PROXIES") |> String.split(",", trim: true)
  end

  plug :match
  plug :dispatch

  # get "/" do ...
end

Summary

Functions

The default value for the given option.

Processes keyword options, delaying the evaluation of MFAs until unpack/1.

Evaluates options processed by pack/1, applying MFAs as needed.

Functions

The default value for the given option.

Processes keyword options, delaying the evaluation of MFAs until unpack/1.

Evaluates options processed by pack/1, applying MFAs as needed.