Pong IP
Sep 25, 2018 22:49 · 529 words · 3 minute read
Lately I was in need to know my external IP for various reasons (AWS security groups?, RDS?, anyone?).
There are numerous services out there to discover your IP address, just try curl -4 ifconfig.co
or visit ifconfig.co for instance.
Anyway, I was in the mood to reactivate my Heroku account with some pet project and test it with GitLab CI/CD.
And since Elixir is simply just cool, and just because I can have my own IP discovery service, this was the perfect match.
Basically all I wanted was just a small Phoenix application that could read my remote IP header, and return it as text. This can also be achieved using just the Plug library, but using the core Phoenix library is also fine.
To create a new Phoenix project stripped down from database and Javascript stuff, I used:
λ mix phx.new pong_ip --no-brunch --no-ecto --no-html
Then I removed all the unnecessary libraries, keeping just the ones below in mix.exs
:
defp deps do
[
{:phoenix, "~> 1.3.4"},
{:cowboy, "~> 1.0"}
]
end
I also removed all files, code and configuration related to views and channels, since I did not need them at that time.
Next step was to create a controller and link its functions in the router.
I named the controller PongController
and placed it under lib/pong_ip_web/controllers
, with an empty pong/2
method:
defmodule PongIpWeb.PongController do
use PongIpWeb, :controller
def pong(conn, _params) do
end
end
With this I updated the router.ex
with the newly created controller:
defmodule PongIpWeb.Router do
use PongIpWeb, :router
pipeline :api do
plug :accepts, ["json"]
end
scope "/", PongIpWeb do
pipe_through :api
get "/", PongController, :pong
end
end
My first attempt to retrieve my IP was to return the remote_ip
attribute from the conn struct:
def pong(conn, _params) do
text conn, to_string(:inet.ntoa(conn.remote_ip))
end
Since the remote_ip
is stored as a tuple, I had to translate it to a string before sending the info back.
To do that, I found this neat ntoa/1 function from the :inet
module (Erlang).
Note that I also used to_string/1
because Erlang’s string is actually a charlist for Elixir.
Pretty simple, right? I just used the text
function from Phoenix.Controller to send back the IP as text.
Ship it and that’s it!
And then for each curl
call to my service a different IP was returned.
What was wrong this time? Of course! Heroku probably has load balancers and other routing stuff going on there.
After adding a few debug logs to verify the request headers, I noticed that Heroku was adding the x-forwared-for
header.
And that had my real IP address.
All I had to do was read this header first, and then fallback to the remote_ip
if absent.
The final code to get the IP is as below:
# controller
def pong(conn, _params) do
text conn, PongIp.Parser.remote_ip(conn)
end
# PongIp module
defmodule PongIp.Parser do
@spec remote_ip(Plug.Conn.t()) :: String.t()
def remote_ip(conn) do
case Plug.Conn.get_req_header(conn, "x-forwarded-for") do
[ip] -> ip
_ ->
to_string(:inet.ntoa(conn.remote_ip))
end
end
end
The source code of this small project can be found on my gitlab page here.
Note
During my research I found this nice project remote_ip, which is actually a Plug that overwrites the remote_ip
field properly.