How to get IP addresses from users?

I’d suggest simply using X-Forwarded-For. RemoteIp already deals with the complexities of parsing it and it’s the defacto standard.

Ok, I see. I’m no expert on this subject, but there seem to be many insecure ways to handle X-Forwarded-For, and Fly.io explicitly advises using Fly-Client-IP instead.

On the other hand, if X-Forwarded-For can be handled securely and if RemoteIp does this, why not? I’ll try it out.

Why is it so hard to pass the headers I want to? Is it for security reasons?

I’m a noob, and could only get it to work by passing the ip in a controller action. :peer_data doesn’t contain the right address and I don’t know how to get remote_ip any other way.

This is what I ended up with for now. It’s working, and I’ll only need the ip in a couple of liveviews so it’s ok to create some extra controllers, but it feels unnecessary.

defmodule MyWeb.Authentication.Controller do
  use MyWeb, :controller

  def login(conn, _params) do
    live_render(conn, MyWeb.Authentication.LoginLive,
      session: %{
        "ip" => conn.remote_ip
      }
    )
  end
end

The way I usually get things from conn to socket is by using the session.

Call this plug after the RemoteIp plug:

defmodule MyAppWeb.Plugs.AssignRemoteIp do
  @moduledoc false
  import Plug.Conn

  alias Plug.Conn

  def init(_opts), do: nil

  def call(%Conn{} = conn, _opts) do
    conn
    |> assign(:remote_ip, conn.remote_ip)
    |> put_session("remote_ip", conn.remote_ip)
  end
end

Then use this on_mount in your LiveView(s):

defmodule MyAppWeb.OnMounts.RemoteIp do
  @moduledoc false
  import Phoenix.LiveView.Utils

  def on_mount(:default, _params, session, socket) do
    socket
    |> assign_new(:remote_ip, fn -> Map.get(session, "remote_ip") end)
    |> then(&{:cont, &1})
  end
end

I can provide more details on implementing these if you need it.

2 Likes

Nice! Works great as soon as it’s after the :fetch_session plug.

I don’t understand this line: |> assign(:remote_ip, conn.remote_ip). Seems superfluous to me.

1 Like

I think it is. Try removing it, everything should work as expected I think.

The project I copied it from uses this remote_ip in conn assigns. If you don’t need it, remove it. :slight_smile:

2 Likes

Ah! I thought assign was a standard function in Elixir to assign a value to a Map, or something, so I thought it was just assigning a value to conn itself. But now I understand it is assigning a value to conn.assigns. I’m still learning …

Not sure if somebody mentioned here the same way but I know @Flo0807 has a blog post on this
How to Get User IP Addresses in Phoenix LiveView

3 Likes

For anyone who uses remote_ip, be careful.

For an “X-Forward-For” header with a value in the form of “my_real_ip, fly_io_proxy_ip” it was selecting fly’s IP! Which was a disaster, since I also had PlugAttack setup to ban sites scraping for WordPress. Oy. Luckily I caught this quickly.

So I then I figured, ok, I will pass in my app’s IP to remote_ip’s proxy options. But then that didn’t feel right because what if fly’s IP changes.

So then I tried passing in headers: ~w[fly-client-ip x-forwards-for] thinking that remote_ip would prioritize those headers based on my order. But no, it still pulled from x-forwards-for. So I hacked up my own… yuck. But seems to work.

defmodule MyAppWeb.Plugs.ClientIp do
  @moduledoc false
  @behaviour Plug

  alias RemoteIp.Parsers.Generic

  require Logger

  def init(opts), do: opts

  def call(conn, _opts) do
    remote_ip =
      try do
        get_remote_ip(conn)
      rescue
        _ -> conn.remote_ip
      end

    case :inet.ntoa(remote_ip) do
      {:error, _} ->
        conn

      ip ->
        Logger.metadata(remote_ip: to_string(ip))
        %{conn | remote_ip: remote_ip}
    end
  end

  defp get_remote_ip(conn) do
    client_ip = List.first(Plug.Conn.get_req_header(conn, "fly-client-ip"))

    if client_ip do
      client_ip |> Generic.parse() |> hd()
    else
      forwarded_for = List.first(Plug.Conn.get_req_header(conn, "x-forwarded-for"))

      if forwarded_for do
        forwarded_for
        |> String.split(",")
        |> Enum.map(&String.trim/1)
        |> List.first()
        |> Generic.parse()
        |> hd()
      else
        conn.remote_ip
      end
    end
  end
end

The remote_ip uses a specific algorithm to detect the IP address—details are available here: remote_ip/extras/algorithm.md at main · ajvondrak/remote_ip · GitHub

If it’s not working well in your case, you might consider opening a GitHub issue with relevant details so the maintainers can look into it.

This also reminds me that I used GitHub - modosc/cloudflare-rails: fix request.ip and request.remote_ip in rails when using cloudflare on Rails to handle such vendor-specific behavior.