Getting the client public IP address in Phoenix

By Jake Morrison in DevOps on Wed 16 May 2018

When your app is running behind a proxy like Nginx, then the request will look like it's coming from Nginx, i.e. the IP will be Similarly, If Nginx is behind a CDN, then all the requests will come from the IP of the CDN.

In order to log the request properly and make decisions like rate limiting, we need to get the IP address from a header set by the CDN.

As described in the Plug docs, we are expected to overwrite the remote_ip field in the Conn.

Following is a plug that reads the X-Forwarded-For HTTP header. You can do something similar with HAProxy’s PROXY protocol.

Add it to your app's Endpoint:

  plug MyApp.Plug.PublicIp
defmodule MyApp.Plug.PublicIp do
  @moduledoc "Get public IP address of request from x-forwarded-for header"
  @behaviour Plug
  @app :my_app

  def init(opts), do: opts

  def call(%{assigns: %{ip: _}} = conn, _opts), do: conn
  def call(conn, _opts) do
    process(conn, Plug.Conn.get_req_header(conn, "x-forwarded-for"))

  def process(conn, []) do
    Plug.Conn.assign(conn, :ip, to_string(:inet.ntoa(get_peer_ip(conn))))
  def process(conn, vals) do
    if Application.get_env(@app, :trust_x_forwarded_for, false) do ip_address = get_ip_address(conn, vals) # Rewrite standard remote_ip field with value from header
      # See
      conn = %{conn | remote_ip: ip_address}

      Plug.Conn.assign(conn, :ip, to_string(:inet.ntoa(ip_address)))
      Plug.Conn.assign(conn, :ip, to_string(:inet.ntoa(get_peer_ip(conn))))

  defp get_ip_address(conn, vals)
  defp get_ip_address(conn, []), do: get_peer_ip(conn)
  defp get_ip_address(conn, [val | _]) do
    # Split into multiple values
    comps = val
      |> String.split(~r{\s*,\s*}, trim: true)
      |> Enum.filter(&(&1 != "unknown"))          # Get rid of "unknown" values
      |>, ":"))))   # Split IP from port, if any
      |> Enum.filter(&(&1 != ""))                 # Filter out blanks
      |>           # Parse address into :inet.ip_address tuple
      |> Enum.filter(&(is_public_ip(&1)))         # Elminate internal IP addreses, e.g.

    case comps do
      [] -> get_peer_ip(conn)
      [comp | _] -> comp

  @spec get_peer_ip(Plug.Conn.t) :: :inet.ip_address
  defp get_peer_ip(conn) do
    {ip, _port} = conn.peer

  @spec parse_address(String.t) :: :inet.ip_address
  defp parse_address(ip) do
    case :inet.parse_ipv4strict_address(to_charlist(ip)) do
      {:ok, ip_address} -> ip_address
      {:error, :einval} -> :einval

  # Whether the input is a valid, public IP address
  @spec is_public_ip(:inet.ip_address | atom) :: boolean
  defp is_public_ip(ip_address) do
    case ip_address do
      {10, _, _, _}     -> false
      {192, 168, _, _}  -> false
      {172, second, _, _} when second >= 16 and second <= 31 -> false
      {127, 0, 0, _}    -> false
      {_, _, _, _}      -> true
      :einval           -> false