%%%------------------------------------------------------------------- %%% @doc macula-net facade. %%% %%% Sovereign IPv6 substrate for the macula realm. Crypto-derived %%% addresses, multi-realm identity, no central allocation authority. %%% See PLAN_MACULA_NET.md for the full spec. %%% %%% This module orchestrates the slices: %%% %%% %%% %%% == Lifecycle == %%% %%% ``` %%% {ok, _} = macula_net:start(#{ %%% realm_pubkey => <<...32 bytes...>>, %%% identity_pubkey => <<...32 bytes...>>, %%% tun_name => <<"macula0">>, %%% tun_mtu => 1280, %%% quic_port => 4400, %%% peers => [ %%% #{station => <<"beam02">>, %%% host => "192.168.1.12", port => 4400, %%% addresses => [<<...16 bytes...>>]} %%% ] %%% }). %%% ''' %%% @end %%%------------------------------------------------------------------- -module(macula_net). -export([ start/1, stop/0, derive_address/2, format_address/1, own_address/0 ]). -export_type([ node_config/0, peer_spec/0 ]). -type node_config() :: #{ realm_pubkey := <<_:256>>, identity_pubkey := <<_:256>>, tun_name := binary(), tun_mtu := 1280..65535, quic_port := inet:port_number(), peers := [peer_spec()] }. -type peer_spec() :: #{ station := binary(), host := binary() | string(), port := inet:port_number(), addresses := [<<_:128>>] }. -define(OWN_ADDR, macula_net_own_address). %% ============================================================================= %% Lifecycle %% ============================================================================= %% @doc Bring up macula-net for this node with the supplied config. -spec start(node_config()) -> {ok, pid()} | {error, term()}. start(#{realm_pubkey := Realm, identity_pubkey := Identity, tun_name := TunName, tun_mtu := Mtu, quic_port := QuicPort, peers := Peers}) -> OwnAddr = macula_address:derive(Realm, Identity), persistent_term:put(?OWN_ADDR, OwnAddr), %% 1. QUIC transport listener. {ok, _Tr} = ensure_started(macula_net_transport_quic, fun() -> macula_net_transport_quic:start_link(#{port => QuicPort}) end), %% 2. TUN device (skipped if no CAP_NET_ADMIN; demo will surface). TunHandle = open_tun(TunName, Mtu), %% 3. Ingress: which addresses are local + how to write to TUN. LocalAddrs = [OwnAddr], TunWriter = make_tun_writer(TunHandle), ok = macula_deliver_packet:configure(#{ local_addresses => LocalAddrs, tun_writer => TunWriter }), %% 4. Wire the transport's inbound handler to deliver_packet. %% Phase 3.5 promoted the handler to arity-2 (Cbor, StreamRef); %% station-only nodes ignore the StreamRef. Host stations replace %% this wiring with macula_host_attach_controller:handle/2 — see %% PLAN_MACULA_NET_PHASE3_5_TRANSPORT_SEAM.md. ok = macula_net_transport_quic:set_handler( fun(Cbor, _StreamRef) -> macula_deliver_packet:handle_envelope(Cbor) end), %% 5. Connect outbound to each peer + populate egress route table. SendFun = fun(StationId, Cbor) -> macula_net_transport_quic:send(StationId, Cbor) end, Stations = lists:flatmap( fun(#{station := S, host := H, port := P, addresses := As}) -> _ = macula_net_transport_quic:connect(S, H, P), %% best effort [#{address => A, station => S, send => SendFun} || A <- As] end, Peers), ok = macula_route_packet:configure(#{own_address => OwnAddr, stations => Stations}), %% 6. Start TUN reader (if we have a TUN). Each packet -> route_packet:dispatch. start_tun_reader(TunHandle), {ok, self()}. %% @doc Bring everything down. -spec stop() -> ok. stop() -> catch macula_net_transport_quic:stop(), persistent_term:erase(?OWN_ADDR), ok. %% ============================================================================= %% Convenience helpers %% ============================================================================= %% @doc Derive a macula-net IPv6 address. See {@link macula_address:derive/2}. -spec derive_address(<<_:256>>, <<_:256>>) -> <<_:128>>. derive_address(RealmPk, IdentityPk) -> macula_address:derive(RealmPk, IdentityPk). %% @doc Format a 16-byte IPv6 binary as RFC 5952 lowercase text. -spec format_address(<<_:128>>) -> binary(). format_address(Addr) -> macula_address:format(Addr). %% @doc Return THIS node's macula-net address (set by {@link start/1}). -spec own_address() -> <<_:128>> | undefined. own_address() -> persistent_term:get(?OWN_ADDR, undefined). %% ============================================================================= %% Internals %% ============================================================================= ensure_started(Name, StartFun) -> decide_start(whereis(Name), StartFun). decide_start(undefined, StartFun) -> pid_or_already(StartFun()); decide_start(Pid, _StartFun) -> {ok, Pid}. pid_or_already({ok, Pid}) -> {ok, Pid}; pid_or_already({error, {already_started, Pid}}) -> {ok, Pid}; pid_or_already(Other) -> Other. %% Try to open the TUN device. If it fails (no CAP_NET_ADMIN, no %% /dev/net/tun, wrong platform), log and continue — the demo runner %% will surface the missing device. open_tun(Name, Mtu) -> open_tun_result(macula_tun:open(Name, Mtu), Name, Mtu). open_tun_result({ok, Handle}, Name, Mtu) -> error_logger:info_msg("macula-net: TUN ~s up (mtu ~p)", [Name, Mtu]), Handle; open_tun_result({error, Reason}, _Name, _Mtu) -> error_logger:warning_msg( "macula-net: TUN open failed: ~p — running without TUN", [Reason]), skipped. make_tun_writer(skipped) -> fun(_Pkt) -> {error, no_tun} end; make_tun_writer(Handle) -> fun(Pkt) -> macula_tun:write(Handle, Pkt) end. start_tun_reader(skipped) -> ok; start_tun_reader(Handle) -> {ok, ReaderPid} = macula_tun_reader_proxy:start_link(#{ dispatch_fn => fun macula_route_packet:dispatch/1 }), ok = macula_tun:start_reader(Handle, ReaderPid).