Skip to content

NAT Discovery

This guide explains how to use estun to discover your NAT type and behavior.

Understanding NAT Types

NAT devices are classified by two behaviors:

Mapping Behavior

How the NAT creates external mappings:

Type Description Hole Punching
Endpoint Independent Same external IP:port for all destinations Easy
Address Dependent Different mapping per destination IP Medium
Address+Port Dependent Different mapping per destination IP:port Hard

Filtering Behavior

Which incoming packets the NAT allows:

Type Description
Endpoint Independent Accepts from any source
Address Dependent Only from contacted IPs
Address+Port Dependent Only from contacted IP:port pairs

Classic NAT Types

The combination of behaviors creates classic NAT types:

NAT Type Mapping Filtering
Full Cone Endpoint Independent Endpoint Independent
Restricted Cone Endpoint Independent Address Dependent
Port Restricted Cone Endpoint Independent Address+Port Dependent
Symmetric Address+Port Dependent Address+Port Dependent

Discovering NAT Behavior

Server Requirements

NAT behavior discovery requires a STUN server that supports RFC 5780 (has alternate IP/port). Google's public servers don't support this. You may need to run your own server.

Basic Discovery

%% Add an RFC 5780 compatible server
{ok, _} = estun:add_server(#{
    host => "stun.example.com",  %% Must support RFC 5780
    port => 3478
}, rfc5780_server).

%% Discover NAT behavior
{ok, Behavior} = estun:discover_nat(rfc5780_server).

Interpreting Results

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

analyze_nat(ServerId) ->
    case estun:discover_nat(ServerId) of
        {ok, #nat_behavior{
            mapped_address = Addr,
            mapping_behavior = Mapping,
            filtering_behavior = Filtering,
            nat_present = NatPresent,
            hairpin_supported = Hairpin
        }} ->
            io:format("~n=== NAT Analysis ===~n"),
            io:format("Public Address: ~p:~p~n", [
                Addr#stun_addr.address,
                Addr#stun_addr.port
            ]),
            io:format("Behind NAT: ~p~n", [NatPresent]),
            io:format("Mapping Behavior: ~p~n", [Mapping]),
            io:format("Filtering Behavior: ~p~n", [Filtering]),
            io:format("Hairpin Support: ~p~n", [Hairpin]),

            %% Determine NAT type
            NatType = classify_nat(Mapping, Filtering),
            io:format("NAT Type: ~s~n", [NatType]),

            %% Hole punching feasibility
            Feasibility = assess_hole_punching(Mapping, Filtering),
            io:format("Hole Punching: ~s~n", [Feasibility]);

        {error, Reason} ->
            io:format("Discovery failed: ~p~n", [Reason])
    end.

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_port_dependent, _) ->
    "Symmetric NAT";
classify_nat(_, _) ->
    "Unknown".

assess_hole_punching(endpoint_independent, _) ->
    "High success probability";
assess_hole_punching(address_dependent, _) ->
    "Medium success probability";
assess_hole_punching(address_port_dependent, _) ->
    "Low success probability - consider TURN relay".

NAT Behavior Record

The #nat_behavior{} record contains:

-record(nat_behavior, {
    %% Your public address
    mapped_address :: #stun_addr{},

    %% How NAT creates mappings
    mapping_behavior :: endpoint_independent |
                        address_dependent |
                        address_port_dependent |
                        unknown,

    %% How NAT filters incoming packets
    filtering_behavior :: endpoint_independent |
                          address_dependent |
                          address_port_dependent |
                          unknown,

    %% Whether you're behind a NAT
    nat_present :: boolean() | unknown,

    %% Whether NAT supports hairpinning
    hairpin_supported :: boolean() | unknown,

    %% NAT binding lifetime in seconds
    binding_lifetime :: pos_integer() | unknown
}).

Checking if Behind NAT

is_behind_nat() ->
    application:ensure_all_started(estun),
    estun:add_server(#{host => "stun.l.google.com", port => 19302}),

    {ok, SocketRef} = estun:open_socket(),
    {ok, MappedAddr} = estun:bind_socket(SocketRef, default),

    %% Get local address from binding info
    {ok, Info} = estun:get_binding_info(SocketRef),
    estun:close_socket(SocketRef),

    %% Compare local IP with mapped IP
    %% Note: A full check would get the local socket address
    io:format("Public address: ~p:~p~n", [
        MappedAddr#stun_addr.address,
        MappedAddr#stun_addr.port
    ]),

    %% Check if address is private (RFC 1918)
    case MappedAddr#stun_addr.address of
        {10, _, _, _} -> false;
        {172, B, _, _} when B >= 16, B =< 31 -> false;
        {192, 168, _, _} -> false;
        _ ->
            io:format("Behind NAT (public IP differs from local)~n"),
            true
    end.

Practical Recommendations

Based on NAT Type

recommend_strategy(Mapping, Filtering) ->
    case {Mapping, Filtering} of
        {endpoint_independent, _} ->
            %% Best case - standard hole punching works
            #{
                strategy => direct_hole_punch,
                success_rate => high,
                notes => "Use simultaneous open technique"
            };

        {address_dependent, endpoint_independent} ->
            %% Good - need to send first
            #{
                strategy => direct_hole_punch,
                success_rate => medium,
                notes => "Initiator must send packets first"
            };

        {address_dependent, _} ->
            %% Moderate difficulty
            #{
                strategy => direct_hole_punch,
                success_rate => medium,
                notes => "May require multiple attempts"
            };

        {address_port_dependent, _} ->
            %% Symmetric NAT - hardest case
            #{
                strategy => turn_relay,
                success_rate => low_for_direct,
                notes => "Consider TURN relay for reliability"
            };

        _ ->
            #{
                strategy => unknown,
                success_rate => unknown,
                notes => "Could not determine NAT behavior"
            }
    end.

Complete Example

-module(nat_analysis).
-export([analyze/0]).

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

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

    %% Try multiple servers
    Servers = [
        #{host => "stun.l.google.com", port => 19302}
    ],

    lists:foreach(fun(Config) ->
        {ok, Id} = estun:add_server(Config),
        io:format("~nTesting server: ~p~n", [Config]),

        case estun:discover_nat(Id) of
            {ok, Behavior} ->
                print_behavior(Behavior);
            {error, Reason} ->
                io:format("  Error: ~p~n", [Reason])
        end
    end, Servers).

print_behavior(#nat_behavior{} = B) ->
    io:format("  Mapped Address: ~p:~p~n", [
        B#nat_behavior.mapped_address#stun_addr.address,
        B#nat_behavior.mapped_address#stun_addr.port
    ]),
    io:format("  NAT Present: ~p~n", [B#nat_behavior.nat_present]),
    io:format("  Mapping: ~p~n", [B#nat_behavior.mapping_behavior]),
    io:format("  Filtering: ~p~n", [B#nat_behavior.filtering_behavior]).