Skip to content

Simple Discovery Example

This example shows basic public IP discovery using STUN.

Basic Discovery

-module(simple_discovery).
-export([main/0]).

-include_lib("estun/include/estun.hrl").

main() ->
    %% Start application
    {ok, _} = application:ensure_all_started(estun),

    %% Add STUN servers
    {ok, _} = estun:add_server(#{
        host => "stun.l.google.com",
        port => 19302
    }, google),

    %% Discover public address
    case estun:discover(google) of
        {ok, #stun_addr{family = Family, address = IP, port = Port}} ->
            io:format("~n=== STUN Discovery Results ===~n"),
            io:format("Family: ~p~n", [Family]),
            io:format("Public IP: ~s~n", [format_ip(IP)]),
            io:format("Mapped Port: ~p~n", [Port]),
            {ok, {IP, Port}};
        {error, Reason} ->
            io:format("Discovery failed: ~p~n", [Reason]),
            {error, Reason}
    end.

format_ip({A, B, C, D}) ->
    io_lib:format("~p.~p.~p.~p", [A, B, C, D]);
format_ip({A, B, C, D, E, F, G, H}) ->
    io_lib:format("~.16B:~.16B:~.16B:~.16B:~.16B:~.16B:~.16B:~.16B",
                  [A, B, C, D, E, F, G, H]).

Multiple Servers for Reliability

-module(reliable_discovery).
-export([discover/0]).

-include_lib("estun/include/estun.hrl").

discover() ->
    application:ensure_all_started(estun),

    %% Add multiple servers
    Servers = [
        {google1, #{host => "stun.l.google.com", port => 19302}},
        {google2, #{host => "stun1.l.google.com", port => 19302}},
        {google3, #{host => "stun2.l.google.com", port => 19302}}
    ],

    lists:foreach(fun({Id, Config}) ->
        estun:add_server(Config, Id)
    end, Servers),

    %% Try servers until one succeeds
    try_servers([google1, google2, google3]).

try_servers([]) ->
    {error, all_servers_failed};
try_servers([Server | Rest]) ->
    case estun:discover(Server) of
        {ok, Addr} ->
            io:format("Discovered via ~p: ~p:~p~n", [
                Server,
                Addr#stun_addr.address,
                Addr#stun_addr.port
            ]),
            {ok, Addr};
        {error, _Reason} ->
            io:format("Server ~p failed, trying next...~n", [Server]),
            try_servers(Rest)
    end.

Periodic Discovery

-module(periodic_discovery).
-export([start/1, stop/1]).

-include_lib("estun/include/estun.hrl").

-record(state, {
    server_id,
    interval,
    last_addr,
    callback
}).

start(Opts) ->
    ServerId = maps:get(server, Opts, default),
    Interval = maps:get(interval, Opts, 60000),  %% 1 minute
    Callback = maps:get(callback, Opts, fun default_callback/2),

    spawn_link(fun() ->
        application:ensure_all_started(estun),
        loop(#state{
            server_id = ServerId,
            interval = Interval,
            last_addr = undefined,
            callback = Callback
        })
    end).

stop(Pid) ->
    Pid ! stop.

loop(#state{server_id = ServerId, interval = Interval,
            last_addr = LastAddr, callback = Callback} = State) ->
    case estun:discover(ServerId) of
        {ok, Addr} ->
            case Addr =:= LastAddr of
                true ->
                    ok;  %% No change
                false ->
                    Callback(address_changed, {LastAddr, Addr})
            end,
            NewState = State#state{last_addr = Addr};
        {error, Reason} ->
            Callback(error, Reason),
            NewState = State
    end,

    receive
        stop -> ok
    after Interval ->
        loop(NewState)
    end.

default_callback(address_changed, {undefined, New}) ->
    io:format("Initial address: ~p:~p~n", [
        New#stun_addr.address, New#stun_addr.port
    ]);
default_callback(address_changed, {Old, New}) ->
    io:format("Address changed!~n"),
    io:format("  Old: ~p:~p~n", [Old#stun_addr.address, Old#stun_addr.port]),
    io:format("  New: ~p:~p~n", [New#stun_addr.address, New#stun_addr.port]);
default_callback(error, Reason) ->
    io:format("Discovery error: ~p~n", [Reason]).

Usage

%% Basic
1> simple_discovery:main().
=== STUN Discovery Results ===
Family: ipv4
Public IP: 203.0.113.42
Mapped Port: 54321
{ok,{{203,0,113,42},54321}}

%% Reliable
2> reliable_discovery:discover().
Discovered via google1: {203,0,113,42}:54322
{ok,{stun_addr,ipv4,54322,{203,0,113,42}}}

%% Periodic
3> Pid = periodic_discovery:start(#{interval => 30000}).
Initial address: {203,0,113,42}:54323
<0.123.0>

%% Later if address changes...
Address changed!
  Old: {203,0,113,42}:54323
  New: {203,0,113,42}:54324

4> periodic_discovery:stop(Pid).
ok

IPv6 Discovery

discover_ipv6() ->
    application:ensure_all_started(estun),

    %% Add IPv6 server
    {ok, _} = estun:add_server(#{
        host => "stun.example.com",
        port => 3478,
        family => inet6
    }, ipv6_server),

    case estun:discover(ipv6_server) of
        {ok, #stun_addr{family = ipv6, address = IP, port = Port}} ->
            io:format("IPv6 address: ~p~n", [IP]),
            io:format("Port: ~p~n", [Port]);
        {error, Reason} ->
            io:format("IPv6 discovery failed: ~p~n", [Reason])
    end.