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.
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.
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.
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
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.