macula_dist_tunnel (macula v13.4.0)

View Source

A distribution tunnel that names its peer.

The macula-dist and macula-dist-relay carriers forward a tunnel's bytes between two nodes, and forwarding is what they are for: a relay in the middle, or the stations of the pool path. A connection over them proves only that the far end holds the key of the certificate it presented, which binds that key to no node_id at all, so distribution's cookie handshake can be relayed by whatever sits between. That is why those dials refuse to start without MACULA_DIST_UNIDENTIFIED_PEER=accept.

D29's answer, and this module: the two nodes hold a TLS 1.3 session between them inside the tunnel, and run the connection handshake end to end inside that session. The accepting node takes the station role, with its TLS key, certificate and TLS-key binding; the dialling node sends CONNECT with its CONNECT key, against the node_id it meant to reach. The carrier sees ciphertext, and is never asked to be honest about who is at the far end.

The handshake is macula_handshake, unchanged and shared with the peering connections: the same frames, the same checks, the same refusals. What differs is where the leaf comes from. On a peering connection it is the leaf of the QUIC connection; here it is the leaf of the session inside the tunnel, which is the only one the two nodes hold between them.

Summary

Functions

Accept the tunnel offered on Stream, in the station role, and return it once the node at the far end has signed for the node_id it claims.

Dial through Stream and return the tunnel, once the node at the far end has proved it is expected_node_id. The refusals are the handshake's own, by name.

Write on the tunnel. The bytes travel inside its session and nowhere else.

Types

accept_options/0

-type accept_options() ::
          #{profile := profile(),
            identity := macula_node_keys:node_key(),
            issuer := pid(),
            cert := file:filename_all(),
            key := file:filename_all(),
            puzzle := #{mode := macula_handshake:puzzle_mode()},
            capabilities => non_neg_integer(),
            timeout_ms => pos_integer()}.

dial_options/0

-type dial_options() ::
          #{profile := profile(),
            identity := macula_node_keys:node_key(),
            issuer := pid(),
            expected_node_id := <<_:256>>,
            capabilities => non_neg_integer(),
            timeout_ms => pos_integer()}.

profile/0

-type profile() :: macula_crypto_profile:profile().

tunnel/0

-type tunnel() :: #{session := ssl:sslsocket(), peer_node_id := <<_:256>>, peer := map()}.

Functions

accept(Stream, Options)

-spec accept(reference(), accept_options()) -> {ok, tunnel()} | {error, term()}.

Accept the tunnel offered on Stream, in the station role, and return it once the node at the far end has signed for the node_id it claims.

close(_)

-spec close(tunnel()) -> ok.

dial(Stream, Options)

-spec dial(reference(), dial_options()) -> {ok, tunnel()} | {error, term()}.

Dial through Stream and return the tunnel, once the node at the far end has proved it is expected_node_id. The refusals are the handshake's own, by name.

peer_node_id(_)

-spec peer_node_id(tunnel()) -> <<_:256>>.

recv(_, Length, Timeout)

-spec recv(tunnel(), non_neg_integer(), timeout()) -> {ok, binary()} | {error, term()}.

send(_, Data)

-spec send(tunnel(), iodata()) -> ok | {error, term()}.

Write on the tunnel. The bytes travel inside its session and nowhere else.

session(_)

-spec session(tunnel()) -> ssl:sslsocket().