Elixir UDP Proxy
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.