Ramon Snir Follow @ramonsnir

Elixir UDP Proxy Read comments

Long story short, I somehow go to be the main “DevOps” engineer for one of our high priority projects (doing far more Ops than Dev, on that project at least). Between all the Chef resources, routing tables and EC2 launch configurations, I needed access to the EC2-VPC private DNS server from outside. It doesn’t like communicating to strangers, and doesn’t regard the VPC routing tables. There are a thousands solutions online for this but I wondered: how hard is to proxy UDP? It’s just binary packets!

The full code from this blog post can be found on Github.

In essence, the proxy is a GenServer receiving messages from the client that need to be proxies to a dedicated upstream, and messages from upstreams to their matching clients.

The replies from the upstreams are as simple as using :gen_udp and looking up in some state maps:

defmodule UdpProxy.Server do
  use GenServer
  alias UdpProxy.Upstream

  ...

  def receive_data downstream, data do
    GenServer.cast downstream[:server_pid], {:receive, downstream, data}
  end

  def handle_cast {:receive, downstream, data}, state do
    :ok = :gen_udp.send state[:socket], downstream[:host],
                        downstream[:port], data
    {:noreply, state}
  end

  ...

end

Sending the messages to the designated upstream (if exists) is pretty simple, and uses the process messages from :gen_udp’s active mode:

...

def handle_info {:udp, _socket, ip, port, data}, state do
  map_key = {ip, port}
  server = self
  upstream = Map.get_lazy state[:map], map_key, fn ->
    downstream = %{server_pid: server,
                   host: ip,
                   port: port}
    {:ok, pid} = Upstream.start_link state[:upstream_host],
                                     state[:upstream_port], downstream
    %{pid: pid}
  end
  map = Map.put state[:map], map_key, upstream
  state = Map.put state, :map, map
  Upstream.send_data upstream[:pid], data
  {:noreply, state}
end

...

Similarly the upstream communicates both with the UdpProxy.Server and with its own connection via :gen_udp.

To make sure that stale connections don’t end up eating too much memory and sockets, I introduced a GC loop that cleans clients that didn’t send messages through the proxy for 5 seconds. Specifically for my usecase, extending the connection’s lifetime on replies was not needed. The loop is using Process.send_after self, ... and I’m definitely not sure that that’s the correct way to implement this.

This is actually one of the few times I had worked directly with OTP in Elixir, as usually most of my hobby work with Elixir is either for scripting or CLI applications, or through deep abstractions. I know that there’s a lot of error-detection missing around the Upstream processes, that aren’t supervised at all and won’t be replaced on failure, but it was definitely a fun two-hours exercise with Elixir.