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