Pong IP

Sep 25, 2018 22:49 · 529 words · 3 minute read elixir phoenix erlang remote_ip tech

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.