Elixir UDP Proxy

Play this article

Long story short, I somehow got 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 thousands of solutions online for this but I wondered: how hard is it 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 proxied 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 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 use case, extending the connection’s lifetime on replies was not needed. The loop is using Process.send_after self, … and I’m not sure that that’s the correct way to implement this.

This is actually one of the few times I have worked directly with OTP in Elixir, as 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-hour exercise with Elixir.