lib/stun.ex

defmodule STUN do
  @moduledoc """
  STUN Client implementation module
  """

  require Record
  Record.extract_all(from_lib: "stun/include/stun.hrl")

  Record.defrecord(
    :stun,
    :stun,
    Record.extract(:stun, from_lib: "stun/include/stun.hrl")
  )

  @google_default_stun_server_name ~c"stun.l.google.com"

  @doc """
  Perform a STUN request against Google STUN server, and retrieve router WAN IP address and mapped port.

  It's than not obvious if you can use the outbound opened port from a different address / port (hole punching).

  You have to verify if you have

   - `full-cone` NAT [Good],
   - `address-restricted-cone` NAT [Good] (you must send the packet to the addr of the peer you want to receive from),
   - `port-restricted-cone` [Good] (you must send a packet to the addr AND port of the peer you want to receive from),
   - `simmetric` NAT (meaning you can't use hole punching).
  """
  @spec get_wan_public_ip_addr_port(
          local_net_ip_addr :: :inet.ip4_address(),
          local_port :: 0..65535
        ) :: {wan_public_ip_addr :: :inet.ip4_address(), wan_external_port :: 0..65535}

  def get_wan_public_ip_addr_port(local_net_ip_addr \\ Utils.local_net_ip_addr(), local_port \\ 0) do
    {:ok, sock} = :gen_udp.open(local_port, [:binary, {:ip, local_net_ip_addr}, {:active, false}])
    {:ok, {_ip, _port}} = :inet.sockname(sock)
    binding_req = :stun_codec.encode(bind_req())
    # https://gist.github.com/zziuni/3741933
    # stun.l.google.com -> 74.125.128.127
    :gen_udp.send(sock, google_stun_server_ip_addr(), 19302, binding_req)
    {:ok, {_, _, resp}} = :gen_udp.recv(sock, 0)
    {:ok, resp_dec} = :stun_codec.decode(resp, :datagram)
    stun("XOR-MAPPED-ADDRESS": wan_public_ip_addr_port) = resp_dec
    :gen_udp.close(sock)
    wan_public_ip_addr_port
  end

  defp bind_req(),
    do: stun(method: 0x1, class: :request, trid: 41_809_861_624_941_132_369_239_212_033)

  defp google_stun_server_ip_addr() do
    {:ok, {_, _, _, :inet, 4, [addr | _rest]}} =
      :inet_res.gethostbyname(@google_default_stun_server_name)

    addr
  end
end