macula_bridge (macula v13.2.2)

View Source

The legacy-application bridge: an unmodified TCP application (a database client, ssh, a web dashboard) reaches an unmodified TCP service across the mesh, one bidi stream per TCP connection.

The serving end (serve/5) advertises a bidi stream procedure and, for each stream a caller opens, connects to the real service and pumps bytes both ways. Who may connect is the procedure's auth policy, checked at STREAM_OPEN against the caller's verified identity ({realm_member_required, _, _} or {ucan_required, _}); a caller the policy refuses gets the stream refused, and its local connection is closed at once and logged by the refusal's name. serve/5 refuses to run without an explicit auth: open has to be asked for.

The listening end (listen/4) accepts TCP connections on a local port (127.0.0.1 by default) and opens one stream per connection.

Each connection is one macula_bridge_pump process: TCP reads go out as stream chunks under a credit window, so a slow reader holds the writer back; the stream is read one chunk at a time, so a peer that ignores the window trips the stream's own inbox bound; a TCP FIN travels in band and becomes a TCP write shutdown on the other side, which keeps crediting the answer it reads.

serve_with/3 and listen_with/2 are the two ends over any advertiser and opener (serve/5 and listen/4 pass the pool's).

Options (serve/5, serve_with/3, listen/4, listen_with/2):

  • auth (serve, required): the procedure's auth policy, as macula:advertise_stream/6 takes it.
  • stations (serve): the stations to advertise on.
  • ucan_token and dial_timeout_ms (listen): what each stream is opened with, as macula:call_stream/5 takes them.
  • port (listen, default 0: any free port) and ip (listen, default {127,0,0,1}).
  • window_bytes (default 1 MiB, 64 KiB to 8 MiB): the bytes a side lets the other have in flight towards it (credit is the receiver's to grant, so the two ends may differ). The serving end grants no more than one served session's share of its caller's budget for unread bytes (macula_stream_sessions:session_share/0, 1 MiB by default), and refuses to serve when that is under 64 KiB.
  • chunk_bytes (1 KiB to 1 MiB and at most half the window; by default 64 KiB or half the window, whichever is less): the most one socket read, and so one stream chunk, carries.
  • idle_ms (default infinity): no traffic either way for this long closes the connection.
  • write_timeout_ms (default infinity): a socket that takes no write for this long closes the connection.
  • connect_timeout_ms (serve, default 5 s): connecting to the service.

Summary

Functions

The stream handler that bridges each stream to Target: what serve/5 advertises, and what a test or an in-process advertisement (macula_stream_local) runs as it is. Its options are resolved here, once: the share of the caller budget it grants under is the one this node has now, whatever the budget becomes while it serves.

Listen on a local port and bridge each accepted connection to the bidi stream procedure Procedure in Realm, through Pool. The listener is linked to the caller.

As listen/4, opening each connection's stream with Open, run in that connection's own process (so the stream is owned by it) and given the call options: mode => bidi, owner, and ucan_token and dial_timeout_ms when Opts has them.

The port a listener accepts on (useful with port => 0).

Serve the TCP service at Target as the bidi stream procedure Procedure in Realm, through Pool. Opts must carry auth.

As serve/5, advertising with Advertise, which is given the handler and exactly the advertisement's options (auth, stations).

Stop a listener. Connections already bridged run on.

Types

opener/0

-type opener() :: fun((map()) -> {ok, pid()} | {error, term()}).

target/0

-type target() :: {inet:hostname() | inet:ip_address(), inet:port_number()}.

Functions

handle_call(Request, From, S)

handle_cast(Msg, S)

handle_info(Msg, S)

handler(Target, Opts)

-spec handler(target(), map()) -> fun((pid(), term()) -> ok).

The stream handler that bridges each stream to Target: what serve/5 advertises, and what a test or an in-process advertisement (macula_stream_local) runs as it is. Its options are resolved here, once: the share of the caller budget it grants under is the one this node has now, whatever the budget becomes while it serves.

init(_)

listen(Pool, Realm, Procedure, Opts)

-spec listen(macula:pool(), macula:realm(), macula:procedure(), map()) -> {ok, pid()} | {error, term()}.

Listen on a local port and bridge each accepted connection to the bidi stream procedure Procedure in Realm, through Pool. The listener is linked to the caller.

listen_with(Open, Opts)

-spec listen_with(opener(), map()) -> {ok, pid()} | {error, term()}.

As listen/4, opening each connection's stream with Open, run in that connection's own process (so the stream is owned by it) and given the call options: mode => bidi, owner, and ucan_token and dial_timeout_ms when Opts has them.

local_port(Listener)

-spec local_port(pid()) -> {ok, inet:port_number()}.

The port a listener accepts on (useful with port => 0).

serve(Pool, Realm, Procedure, Target, Opts)

-spec serve(macula:pool(), macula:realm(), macula:procedure(), target(), map()) -> ok | {error, term()}.

Serve the TCP service at Target as the bidi stream procedure Procedure in Realm, through Pool. Opts must carry auth.

serve_with(Advertise, Target, Opts)

-spec serve_with(fun((fun((pid(), term()) -> ok), map()) -> ok | {error, term()}), target(), map()) ->
                    ok | {error, term()}.

As serve/5, advertising with Advertise, which is given the handler and exactly the advertisement's options (auth, stations).

stop(Listener)

-spec stop(pid()) -> ok.

Stop a listener. Connections already bridged run on.

terminate(Reason, _)