Packages

macula

0.11.3
7.1.0 7.0.0 6.0.0 5.2.2 5.2.1 5.2.0 5.1.0 5.0.0 4.8.0 4.7.1 4.7.0 4.6.0 4.5.0 4.4.10 4.4.9 4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.1 4.3.0 4.2.9 4.2.8 4.2.7 4.2.6 4.2.5 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.1 4.1.0 4.0.0 3.16.0 3.15.3 3.15.2 3.15.1 3.14.0 3.13.0 3.12.1 3.12.0 3.11.1 3.11.0 3.10.3 3.10.2 3.10.1 3.9.0 3.8.0 3.7.0 3.5.0 3.4.0 3.3.0 3.2.0 3.1.0 3.0.0 2.1.1 2.1.0 2.0.0 1.5.2 1.5.1 1.4.30 1.4.29 1.4.28 1.4.27 1.4.26 1.4.25 1.4.24 1.4.23 1.4.22 1.4.21 1.4.20 1.4.19 1.4.18 1.4.17 1.4.16 1.4.15 1.4.14 1.4.13 1.4.11 1.4.10 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.1 1.3.0 1.2.0 1.1.0 1.0.10 1.0.9 1.0.8 1.0.7 1.0.6 1.0.5 1.0.4 1.0.3 1.0.2 1.0.1 1.0.0 0.48.6 0.48.5 0.48.4 0.48.3 0.48.2 0.48.1 0.48.0 0.47.1 0.47.0 0.46.3 0.46.1 0.46.0 0.45.3 0.45.2 0.45.1 0.45.0 0.44.2 0.44.1 0.44.0 0.43.3 0.43.2 0.43.1 0.43.0 0.42.9 0.42.8 0.42.7 0.42.6 0.42.5 0.42.4 0.42.3 0.42.2 0.42.1 0.42.0 0.41.1 0.41.0 0.40.1 0.40.0 0.39.9 0.39.8 0.39.7 0.39.6 0.39.5 0.39.4 0.39.3 0.39.2 0.39.1 0.39.0 0.38.8 0.38.7 0.38.6 0.38.5 0.38.4 0.38.3 0.38.2 0.38.1 0.38.0 0.37.7 0.37.6 0.37.5 0.37.4 0.37.3 0.37.2 0.37.1 0.37.0 0.36.6 0.36.5 0.36.4 0.36.3 0.36.2 0.36.1 0.36.0 0.35.4 0.35.3 0.35.2 0.35.1 0.35.0 0.34.1 0.34.0 0.33.1 0.33.0 0.32.5 0.32.4 0.32.3 0.32.2 0.32.1 0.32.0 0.31.9 0.31.8 0.31.7 0.31.6 0.31.5 0.31.4 0.31.3 0.31.2 0.31.1 0.31.0 0.30.10 0.30.9 0.30.8 0.30.7 0.30.6 0.30.5 0.30.4 0.30.3 0.30.2 0.30.1 0.30.0 0.29.0 0.28.3 0.28.2 0.28.1 0.28.0 0.27.1 0.27.0 0.26.1 0.26.0 0.25.6 0.25.5 0.25.4 0.25.3 0.25.2 0.25.1 0.25.0 0.24.6 0.24.5 0.24.4 0.24.3 0.24.2 0.24.1 0.24.0 0.23.3 0.23.2 0.23.1 0.23.0 0.22.12 0.22.11 0.22.10 0.22.9 0.22.8 0.22.7 0.22.6 0.22.5 0.22.4 0.22.3 0.22.2 0.22.1 0.22.0 0.21.7 0.21.6 0.21.5 0.21.4 0.21.2 0.21.1 0.21.0 0.20.25 0.20.24 0.20.23 0.20.22 0.20.21 0.20.20 0.20.19 0.20.18 0.20.17 0.20.16 0.20.15 0.20.14 0.20.13 0.20.12 0.20.11 0.20.10 0.20.9 0.20.8 0.20.7 0.20.6 0.20.5 0.20.3 0.20.2 0.20.1 0.20.0 0.19.2 0.19.1 0.19.0 0.18.1 0.18.0 0.17.4 0.17.3 0.17.2 0.17.1 0.17.0 0.16.6 0.16.5 0.16.4 0.16.3 0.16.2 0.16.1 0.16.0 0.15.1 0.15.0 0.14.3 0.14.2 0.14.1 0.14.0 0.12.6 0.12.5 0.12.3 0.11.3 0.10.2 0.10.1 0.10.0 0.9.2 0.9.1 0.9.0 0.8.25 0.8.24 0.8.23 0.8.22 0.8.21 0.8.20 0.8.19 0.8.18 0.8.17 0.8.16 0.8.15 0.8.14 0.8.13 0.8.12 0.8.11 0.8.10 0.8.9 0.8.8 0.8.7 0.8.6 0.8.5 0.8.4 0.8.3 0.8.2 0.8.1 0.8.0 0.7.30 0.7.29 0.7.28 0.7.27 0.7.26 0.7.25 0.7.24 0.7.23 0.7.22 0.7.21 0.7.20 0.7.19 0.7.18 0.7.17 0.7.16 0.7.15 0.7.14 0.7.13 0.7.12 0.7.11 0.7.10 0.7.9 0.7.8 0.7.7 0.7.6 0.7.5 0.7.4 0.7.3 0.7.2 0.7.1 0.7.0 0.6.7 0.6.6 0.6.5 0.6.4 0.6.3 0.6.2 0.6.1 0.6.0 0.5.0 0.4.4 0.4.3 0.4.2 0.4.1 0.4.0 0.3.4 0.3.3 0.3.2 0.3.1

Macula HTTP/3 Mesh SDK — connect, subscribe, publish, call, advertise

Current section

Files

Jump to
macula src macula_nat_system macula_nat_connector.erl
Raw

src/macula_nat_system/macula_nat_connector.erl

%%%-------------------------------------------------------------------
%%% @doc
%%% NAT-aware Peer Connector.
%%%
%%% Provides intelligent connection establishment using the optimal
%%% strategy based on NAT profiles. Integrates:
%%% - NAT coordinator for strategy determination
%%% - Hole punch executor for direct connections
%%% - Relay fallback when direct fails
%%%
%%% Connection Strategy Order:
%%% 1. Direct - If target can receive unsolicited (Full Cone NAT)
%%% 2. Hole Punch - If NAT profiles indicate feasibility
%%% 3. Relay - Fallback via gateway
%%%
%%% Usage: connect(TargetNodeId, Opts) to establish connection.
%%%
%%% @end
%%%-------------------------------------------------------------------
-module(macula_nat_connector).
-include_lib("kernel/include/logger.hrl").
%% API
-export([
connect/2,
connect/3,
disconnect/1
]).
-define(DEFAULT_TIMEOUT_MS, 10000). % 10 seconds total timeout
-define(HOLE_PUNCH_TIMEOUT_MS, 5000). % 5 seconds for hole punch
-define(DIRECT_TIMEOUT_MS, 3000). % 3 seconds for direct connect
%%%===================================================================
%%% Types
%%%===================================================================
-type connect_opts() :: #{
timeout => timeout(),
skip_hole_punch => boolean(),
relay_endpoint => binary()
}.
-type connect_result() ::
{ok, quicer:connection_handle(), direct | hole_punch | relay} |
{error, term()}.
-export_type([connect_opts/0, connect_result/0]).
%%%===================================================================
%%% API
%%%===================================================================
%% @doc Connect to a peer using optimal strategy.
%% Automatically determines best approach based on NAT profiles.
-spec connect(binary(), binary()) -> connect_result().
connect(LocalNodeId, TargetNodeId) ->
connect(LocalNodeId, TargetNodeId, #{}).
%% @doc Connect with options.
-spec connect(binary(), binary(), connect_opts()) -> connect_result().
connect(LocalNodeId, TargetNodeId, Opts) ->
Timeout = maps:get(timeout, Opts, ?DEFAULT_TIMEOUT_MS),
?LOG_INFO("Initiating NAT-aware connection: ~s -> ~s", [LocalNodeId, TargetNodeId]),
%% Get connection strategy from coordinator
case get_connection_strategy(LocalNodeId, TargetNodeId) of
{ok, Strategy, SessionId} ->
execute_strategy(Strategy, LocalNodeId, TargetNodeId, SessionId, Opts, Timeout);
{error, Reason} ->
?LOG_WARNING("Failed to get connection strategy: ~p, trying relay", [Reason]),
try_relay_connection(TargetNodeId, Opts)
end.
%% @doc Disconnect from a peer.
-spec disconnect(quicer:connection_handle()) -> ok.
disconnect(Conn) ->
catch quicer:close_connection(Conn),
ok.
%%%===================================================================
%%% Internal functions - Strategy Selection
%%%===================================================================
%% @private
%% @doc Get connection strategy from NAT coordinator.
-spec get_connection_strategy(binary(), binary()) ->
{ok, macula_nat_coordinator:connection_strategy(), binary()} | {error, term()}.
get_connection_strategy(LocalNodeId, TargetNodeId) ->
case whereis(macula_nat_coordinator) of
undefined ->
%% Coordinator not running - default to hole_punch (optimistic)
{ok, hole_punch, generate_session_id()};
_Pid ->
macula_nat_coordinator:request_connection(LocalNodeId, TargetNodeId)
end.
%% @private
%% @doc Execute the determined connection strategy.
-spec execute_strategy(macula_nat_coordinator:connection_strategy(),
binary(), binary(), binary(), connect_opts(), timeout()) ->
connect_result().
execute_strategy(direct, _LocalNodeId, TargetNodeId, _SessionId, Opts, _Timeout) ->
?LOG_DEBUG("Trying direct connection to ~s", [TargetNodeId]),
try_direct_connection(TargetNodeId, Opts);
execute_strategy(hole_punch, LocalNodeId, TargetNodeId, SessionId, Opts, _Timeout) ->
?LOG_DEBUG("Trying hole punch to ~s (session ~s)", [TargetNodeId, SessionId]),
case try_hole_punch(LocalNodeId, TargetNodeId, SessionId, Opts) of
{ok, Conn} ->
report_result(SessionId, LocalNodeId, success),
{ok, Conn, hole_punch};
{error, Reason} ->
report_result(SessionId, LocalNodeId, failure),
?LOG_DEBUG("Hole punch failed: ~p, falling back to relay", [Reason]),
try_relay_connection(TargetNodeId, Opts)
end;
execute_strategy(relay, _LocalNodeId, TargetNodeId, _SessionId, Opts, _Timeout) ->
?LOG_DEBUG("Using relay for ~s (NAT profile requires it)", [TargetNodeId]),
try_relay_connection(TargetNodeId, Opts).
%%%===================================================================
%%% Internal functions - Connection Attempts
%%%===================================================================
%% @private
%% @doc Try direct QUIC connection to target.
-spec try_direct_connection(binary(), connect_opts()) -> connect_result().
try_direct_connection(TargetNodeId, _Opts) ->
%% Look up target's advertised endpoint from DHT
case lookup_peer_endpoint(TargetNodeId) of
{ok, Host, Port} ->
case attempt_quic_connect(Host, Port, ?DIRECT_TIMEOUT_MS) of
{ok, Conn} ->
{ok, Conn, direct};
{error, Reason} ->
{error, {direct_failed, Reason}}
end;
{error, not_found} ->
{error, endpoint_not_found}
end.
%% @private
%% @doc Try hole punch connection.
-spec try_hole_punch(binary(), binary(), binary(), connect_opts()) ->
{ok, quicer:connection_handle()} | {error, term()}.
try_hole_punch(_LocalNodeId, TargetNodeId, SessionId, Opts) ->
case get_hole_punch_target(TargetNodeId) of
{ok, Host, Ports} ->
PunchOpts = build_hole_punch_opts(Host, Ports, SessionId),
maybe_execute_hole_punch(TargetNodeId, PunchOpts, Opts);
{error, Reason} ->
{error, {hole_punch_target_not_found, Reason}}
end.
build_hole_punch_opts(Host, Ports, SessionId) ->
#{
target_host => Host,
target_ports => Ports,
session_id => SessionId,
role => initiator
}.
maybe_execute_hole_punch(_TargetNodeId, _PunchOpts, #{skip_hole_punch := true}) ->
{error, hole_punch_skipped};
maybe_execute_hole_punch(TargetNodeId, PunchOpts, _Opts) ->
macula_hole_punch:execute(TargetNodeId, PunchOpts, ?HOLE_PUNCH_TIMEOUT_MS).
%% @private
%% @doc Try relay connection via gateway.
-spec try_relay_connection(binary(), connect_opts()) -> connect_result().
try_relay_connection(TargetNodeId, Opts) ->
RelayEndpoint = get_relay_endpoint(Opts),
do_relay_connection(RelayEndpoint, TargetNodeId).
get_relay_endpoint(#{relay_endpoint := Endpoint}) ->
Endpoint;
get_relay_endpoint(_Opts) ->
find_relay_endpoint().
do_relay_connection({ok, Host, Port}, TargetNodeId) ->
connect_and_setup_relay(attempt_quic_connect(Host, Port, ?DIRECT_TIMEOUT_MS), TargetNodeId);
do_relay_connection({error, Reason}, _TargetNodeId) ->
{error, {no_relay_available, Reason}}.
connect_and_setup_relay({ok, Conn}, TargetNodeId) ->
setup_relay_or_close(setup_relay_tunnel(Conn, TargetNodeId), Conn);
connect_and_setup_relay({error, Reason}, _TargetNodeId) ->
{error, {relay_connect_failed, Reason}}.
setup_relay_or_close(ok, Conn) ->
{ok, Conn, relay};
setup_relay_or_close({error, Reason}, Conn) ->
quicer:close_connection(Conn),
{error, {relay_setup_failed, Reason}}.
%%%===================================================================
%%% Internal functions - Helpers
%%%===================================================================
%% @private
%% @doc Look up peer's advertised endpoint from DHT.
-spec lookup_peer_endpoint(binary()) ->
{ok, binary() | string(), inet:port_number()} | {error, not_found}.
lookup_peer_endpoint(NodeId) ->
case whereis(macula_routing_server) of
undefined ->
{error, not_found};
Pid ->
%% Build DHT key for peer endpoint
Key = crypto:hash(sha256, <<"peer.endpoint.", NodeId/binary>>),
case macula_routing_server:find_value(Pid, Key, 20) of
{ok, #{<<"host">> := Host, <<"port">> := Port}} ->
{ok, Host, Port};
_ ->
{error, not_found}
end
end.
%% @private
%% @doc Get hole punch target info (reflexive address and predicted ports).
-spec get_hole_punch_target(binary()) ->
{ok, binary() | string(), [inet:port_number()]} | {error, term()}.
get_hole_punch_target(NodeId) ->
case whereis(macula_nat_cache) of
undefined ->
{error, nat_cache_unavailable};
_Pid ->
case macula_nat_cache:get_from_dht(NodeId) of
{ok, Profile} ->
%% Extract reflexive address and predict ports
case maps:get(reflexive_address, Profile, undefined) of
{IP, Port} ->
Ports = predict_target_ports(Profile, Port),
Host = format_ip(IP),
{ok, Host, Ports};
undefined ->
{error, no_reflexive_address}
end;
not_found ->
{error, nat_profile_not_found};
{error, Reason} ->
{error, Reason}
end
end.
%% @private
%% @doc Predict target ports based on NAT profile using port predictor.
-spec predict_target_ports(macula_nat_cache:nat_profile(), inet:port_number()) ->
[inet:port_number()].
predict_target_ports(Profile, Port) ->
NodeId = maps:get(node_id, Profile, <<>>),
AllocationPolicy = maps:get(allocation_policy, Profile, unknown),
%% Use the port predictor for intelligent prediction
case whereis(macula_port_predictor) of
undefined ->
%% Fallback to simple prediction if predictor not running
simple_predict_ports(AllocationPolicy, Port, Profile);
_Pid ->
Prediction = macula_port_predictor:predict(NodeId, AllocationPolicy,
#{base_port => Port, count => 5}),
maps:get(ports, Prediction, [Port])
end.
%% @private
%% @doc Simple port prediction fallback.
-spec simple_predict_ports(atom(), inet:port_number(), map()) -> [inet:port_number()].
simple_predict_ports(pp, Port, _Profile) ->
%% Port preservation - use same port
[Port];
simple_predict_ports(pc, Port, #{port_delta := Delta}) when is_integer(Delta) ->
%% Port contiguity - try sequential range
[Port, Port + Delta, Port + (2 * Delta), Port - Delta];
simple_predict_ports(_, Port, _Profile) ->
%% Random or unknown - try range around known port
[Port, Port + 1, Port - 1, Port + 2, Port - 2].
%% @private
%% @doc Format IP address as string.
-spec format_ip(inet:ip_address()) -> string().
format_ip({A, B, C, D}) ->
lists:flatten(io_lib:format("~B.~B.~B.~B", [A, B, C, D]));
format_ip(IP) when is_tuple(IP), tuple_size(IP) == 8 ->
inet:ntoa(IP).
%% @private
%% @doc Find available relay endpoint.
-spec find_relay_endpoint() ->
{ok, binary() | string(), inet:port_number()} | {error, term()}.
find_relay_endpoint() ->
case whereis(macula_routing_server) of
undefined ->
{error, routing_unavailable};
Pid ->
%% Look for relay service in DHT
Key = crypto:hash(sha256, <<"service.relay">>),
case macula_routing_server:find_value(Pid, Key, 20) of
{ok, #{<<"host">> := Host, <<"port">> := Port}} ->
{ok, Host, Port};
_ ->
%% Fallback to bootstrap gateway
{error, no_relay_found}
end
end.
%% @private
%% @doc Attempt QUIC connection.
-spec attempt_quic_connect(binary() | string(), inet:port_number(), timeout()) ->
{ok, quicer:connection_handle()} | {error, term()}.
attempt_quic_connect(Host, Port, Timeout) when is_binary(Host) ->
attempt_quic_connect(binary_to_list(Host), Port, Timeout);
attempt_quic_connect(Host, Port, Timeout) when is_list(Host) ->
ConnOpts = quic_connection_opts(),
quicer:connect(Host, Port, ConnOpts, Timeout).
quic_connection_opts() ->
[
{alpn, ["macula"]},
{verify, none},
{idle_timeout_ms, 60000},
{keep_alive_interval_ms, 20000}
].
%% @private
%% @doc Setup relay tunnel to target.
-spec setup_relay_tunnel(quicer:connection_handle(), binary()) -> ok | {error, term()}.
setup_relay_tunnel(Conn, TargetNodeId) ->
send_relay_request_on_stream(quicer:start_stream(Conn, []), TargetNodeId).
send_relay_request_on_stream({ok, Stream}, TargetNodeId) ->
RelayRequest = #{type => relay_request, target_node_id => TargetNodeId},
send_relay_request(quicer:send(Stream, msgpack:pack(RelayRequest)));
send_relay_request_on_stream(Error, _TargetNodeId) ->
Error.
send_relay_request({ok, _}) -> ok;
send_relay_request(Error) -> Error.
%% @private
%% @doc Report hole punch result to coordinator.
-spec report_result(binary(), binary(), success | failure) -> ok.
report_result(SessionId, ReporterId, Result) ->
case whereis(macula_nat_coordinator) of
undefined -> ok;
_Pid -> macula_nat_coordinator:report_result(SessionId, ReporterId, Result)
end.
%% @private
%% @doc Generate unique session ID.
-spec generate_session_id() -> binary().
generate_session_id() ->
base64:encode(crypto:strong_rand_bytes(16)).