Elixir UDP Proxy

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}

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



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}
    map = Map.put state[:map], map_key, upstream
    state = Map.put state, :map, map
    Upstream.send_data upstream[:pid], data
    {:noreply, state}


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.

Ramon Snir

Ramon Snir

True Generalist

© 2020