Skip to content

NAT Type Detection Example

This example shows how to detect NAT type and make connectivity decisions.

NAT Analyzer Module

-module(nat_analyzer).
-export([analyze/0, analyze/1, get_recommendation/0]).

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

%% Quick analysis using default server
analyze() ->
    application:ensure_all_started(estun),
    estun:add_server(#{host => "stun.l.google.com", port => 19302}, default),
    analyze(default).

%% Analyze using specific server
analyze(ServerId) ->
    io:format("~n╔════════════════════════════════════════╗~n"),
    io:format("║         NAT Type Analysis              ║~n"),
    io:format("╚════════════════════════════════════════╝~n~n"),

    case estun:discover_nat(ServerId) of
        {ok, Behavior} ->
            print_results(Behavior),
            print_nat_type(Behavior),
            print_recommendation(Behavior),
            {ok, Behavior};
        {error, Reason} ->
            io:format("Analysis failed: ~p~n", [Reason]),
            io:format("~nNote: Full NAT analysis requires RFC 5780 server support.~n"),
            io:format("Falling back to basic discovery...~n~n"),
            basic_analysis(ServerId)
    end.

basic_analysis(ServerId) ->
    case estun:discover(ServerId) of
        {ok, Addr} ->
            io:format("Public Address: ~p:~p~n", [
                Addr#stun_addr.address,
                Addr#stun_addr.port
            ]),
            io:format("~nBasic discovery successful.~n"),
            io:format("For full NAT analysis, use an RFC 5780 compliant server.~n"),
            {ok, Addr};
        {error, Reason} ->
            io:format("Basic discovery also failed: ~p~n", [Reason]),
            {error, Reason}
    end.

print_results(#nat_behavior{} = B) ->
    io:format("┌─────────────────────────────────────────┐~n"),
    io:format("│ Discovery Results                       │~n"),
    io:format("├─────────────────────────────────────────┤~n"),

    %% Public Address
    case B#nat_behavior.mapped_address of
        #stun_addr{address = IP, port = Port} ->
            io:format("│ Public Address: ~s~n", [
                format_addr(IP, Port)
            ]);
        _ ->
            io:format("│ Public Address: Unknown~n")
    end,

    %% NAT Present
    io:format("│ Behind NAT: ~s~n", [
        format_bool(B#nat_behavior.nat_present)
    ]),

    %% Mapping Behavior
    io:format("│ Mapping: ~s~n", [
        format_behavior(B#nat_behavior.mapping_behavior)
    ]),

    %% Filtering Behavior
    io:format("│ Filtering: ~s~n", [
        format_behavior(B#nat_behavior.filtering_behavior)
    ]),

    %% Hairpin Support
    io:format("│ Hairpin: ~s~n", [
        format_bool(B#nat_behavior.hairpin_supported)
    ]),

    io:format("└─────────────────────────────────────────┘~n~n").

print_nat_type(#nat_behavior{mapping_behavior = M, filtering_behavior = F}) ->
    Type = classify_nat(M, F),
    io:format("┌─────────────────────────────────────────┐~n"),
    io:format("│ NAT Type: ~-29s │~n", [Type]),
    io:format("└─────────────────────────────────────────┘~n~n").

print_recommendation(#nat_behavior{mapping_behavior = M}) ->
    io:format("┌─────────────────────────────────────────┐~n"),
    io:format("│ Recommendation                          │~n"),
    io:format("├─────────────────────────────────────────┤~n"),

    case M of
        endpoint_independent ->
            io:format("│ ✓ Hole punching: HIGH success rate     │~n"),
            io:format("│ ✓ Direct P2P: Recommended              │~n"),
            io:format("│ ○ TURN relay: Not needed               │~n");
        address_dependent ->
            io:format("│ ○ Hole punching: MEDIUM success rate   │~n"),
            io:format("│ ○ Direct P2P: Possible with timing     │~n"),
            io:format("│ ○ TURN relay: Recommended as fallback  │~n");
        address_port_dependent ->
            io:format("│ ✗ Hole punching: LOW success rate      │~n"),
            io:format("│ ✗ Direct P2P: Difficult                │~n"),
            io:format("│ ✓ TURN relay: Strongly recommended     │~n");
        unknown ->
            io:format("│ ? Hole punching: Unknown               │~n"),
            io:format("│ ? Try direct connection first          │~n"),
            io:format("│ ? Have TURN relay ready as fallback    │~n")
    end,

    io:format("└─────────────────────────────────────────┘~n~n").

get_recommendation() ->
    case analyze() of
        {ok, #nat_behavior{mapping_behavior = M}} ->
            case M of
                endpoint_independent ->
                    {direct, "Use direct hole punching"};
                address_dependent ->
                    {direct_with_fallback, "Try direct, have TURN ready"};
                address_port_dependent ->
                    {relay, "Use TURN relay"};
                unknown ->
                    {try_direct, "Try direct first, fallback to TURN"}
            end;
        {ok, _} ->
            {try_direct, "Basic discovery only, try direct"};
        {error, _} ->
            {unknown, "Could not determine NAT type"}
    end.

%% Helpers

classify_nat(endpoint_independent, endpoint_independent) ->
    "Full Cone NAT";
classify_nat(endpoint_independent, address_dependent) ->
    "Restricted Cone NAT";
classify_nat(endpoint_independent, address_port_dependent) ->
    "Port Restricted Cone NAT";
classify_nat(address_dependent, _) ->
    "Address Dependent NAT";
classify_nat(address_port_dependent, _) ->
    "Symmetric NAT";
classify_nat(_, _) ->
    "Unknown".

format_addr({A,B,C,D}, Port) ->
    io_lib:format("~p.~p.~p.~p:~p", [A,B,C,D,Port]);
format_addr(IP, Port) when tuple_size(IP) == 8 ->
    io_lib:format("~p:~p", [IP, Port]).

format_bool(true) -> "Yes";
format_bool(false) -> "No";
format_bool(unknown) -> "Unknown";
format_bool(_) -> "Unknown".

format_behavior(endpoint_independent) -> "Endpoint Independent";
format_behavior(address_dependent) -> "Address Dependent";
format_behavior(address_port_dependent) -> "Address+Port Dependent";
format_behavior(unknown) -> "Unknown";
format_behavior(_) -> "Unknown".

Usage

1> nat_analyzer:analyze().

╔════════════════════════════════════════╗
         NAT Type Analysis              
╚════════════════════════════════════════╝

┌─────────────────────────────────────────┐
 Discovery Results                       
├─────────────────────────────────────────┤
 Public Address: 203.0.113.42:54321
 Behind NAT: Yes
 Mapping: Endpoint Independent
 Filtering: Address+Port Dependent
 Hairpin: Unknown
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
 NAT Type: Port Restricted Cone NAT      
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
 Recommendation                          
├─────────────────────────────────────────┤
  Hole punching: HIGH success rate     
  Direct P2P: Recommended              
  TURN relay: Not needed               
└─────────────────────────────────────────┘

{ok,{nat_behavior,...}}

Programmatic Recommendations

2> nat_analyzer:get_recommendation().
{direct, "Use direct hole punching"}

%% Use in application logic
case nat_analyzer:get_recommendation() of
    {direct, _} ->
        attempt_hole_punch();
    {direct_with_fallback, _} ->
        case attempt_hole_punch() of
            ok -> ok;
            _ -> use_turn_relay()
        end;
    {relay, _} ->
        use_turn_relay();
    _ ->
        %% Unknown - try direct first
        case attempt_hole_punch() of
            ok -> ok;
            _ -> use_turn_relay()
        end
end.

Comparing Multiple Locations

-module(multi_location_test).
-export([test/0]).

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

    %% Test from different perspectives
    Servers = [
        {google, #{host => "stun.l.google.com", port => 19302}},
        {google2, #{host => "stun1.l.google.com", port => 19302}}
    ],

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

    Results = lists:map(fun({Id, _}) ->
        case estun:discover(Id) of
            {ok, Addr} ->
                {Id, {ok, Addr#stun_addr.address, Addr#stun_addr.port}};
            Error ->
                {Id, Error}
        end
    end, Servers),

    io:format("~n=== Multi-Server Results ===~n"),
    lists:foreach(fun({Id, Result}) ->
        case Result of
            {ok, IP, Port} ->
                io:format("~p: ~p:~p~n", [Id, IP, Port]);
            {error, Reason} ->
                io:format("~p: ERROR - ~p~n", [Id, Reason])
        end
    end, Results),

    %% Check consistency
    Addrs = [Addr || {_, {ok, Addr, _}} <- Results],
    case lists:usort(Addrs) of
        [_SingleAddr] ->
            io:format("~n✓ Consistent mapping (Endpoint Independent)~n");
        _ ->
            io:format("~n✗ Inconsistent mapping (may indicate Symmetric NAT)~n")
    end.