macula_quic (macula v10.25.0)
View SourceMacula QUIC transport — Quinn-based Rust NIF.
Provides QUIC listener, connection, and stream operations backed by Quinn (Rust). Listeners bind to specific IP addresses, enabling per-identity IPv6 binding for virtual relay identities.
Active-mode messages delivered to owning process: {quic, Data, StreamRef, Flags} — stream data {quic, new_conn, ConnRef, Info} — new connection accepted {quic, new_stream, StreamRef, Props} — new stream accepted {quic, connected, Tag, ConnRef} a dial from async_connect/4 connected {quic, connect_failed, Tag, Reason} a dial from async_connect/4 failed {quic, peer_send_shutdown, StreamRef, undefined} {quic, stream_closed, StreamRef, Flags} Flags is {reset, ErrorCode} when the read failed because the peer called reset_stream/2 on their send side (a deliberate, peer-visible abort) — none for every other read failure (connection loss, zero-RTT rejection, ...). {quic, shutdown, Handle, Reason} {quic, send_ready, StreamRef, undefined} The stream takes data again after async_send/2 answered {error, busy} to this process; also sent when the stream stops taking data, so the retry sees the failure. {quic, send_failed, StreamRef, Reason} A write on the stream failed; later sends return the error. Handle it as a closed stream.
Summary
Functions
Accept stream with options and timeout (for macula_dist).
Start accepting connections on a listener. Delivers {quic, new_conn, ConnRef, Info} to the calling process.
Start accepting streams on a connection. Delivers {quic, new_stream, StreamRef, #{conn => ConnRef}} to the owning process.
Start a dial and return at once, instead of waiting as connect/4 does. The calling process owns the dial and later receives {quic, connected, Tag, ConnRef} or {quic, connect_failed, Tag, Reason}, where Tag is dial_tag(Dial). The dial ends early when cancel_connect/1 is called or when its owner exits. Opts and Timeout are those of connect/4; options that cannot be used return {error, Reason} at once.
Start opening a bidirectional stream and return at once, instead of waiting as open_stream/1 does. The calling process owns the open and later receives {quic, stream_opened, Tag, StreamRef}, and owns that stream, or {quic, stream_open_failed, Tag, Reason}, where Tag is stream_open_tag(Opening). The open waits for as long as the peer allows no further stream, and ends early when cancel_open_stream/1 is called or when its owner exits. On a closed connection it returns {error, already_closed} at once.
Queue data on a stream and return at once.
Async shutdown connection.
Async shutdown stream. Code now genuinely reaches the wire via reset_stream/2 — previously a stub that silently discarded both Flag and Code and always did a graceful close_stream/1. Flag is unused (reserved; no caller has ever needed it, kept for signature compatibility).
End a dial. Afterwards no result for it is in, or will reach, the caller's mailbox: a result sent before the cancel is taken out, and the connection it carried is closed. Call it from the dial's owner.
End a stream open. Afterwards no result for it is in, or will reach, the caller's mailbox: a result sent before the cancel is taken out, and the stream it carried is reset. Call it from the open's owner.
Generic close — tries stream, then connection, then listener.
Close a connection.
Close a listener. Once it returns the owner receives no further {quic, new_conn, ...} from it: a connection whose handshake completes after the close is closed instead.
Close a stream's sending side gracefully, and return at once.
Connect to a remote QUIC server. Host is a hostname or IP-string; validation depends on verify / verify_pubkey opts.
Transfer ownership of a handle to another process. Works with both stream and connection handles. Returns once no message for the handle is on its way to the former owner: from then on every data message and event of a stream, and every new_stream notice of a connection, goes to Pid.
The reference in this dial's result message.
Generate a self-signed X.509 cert from an Ed25519 keypair. Returns {ok, {CertPem, KeyPem}} as PEM-encoded binaries suitable for handing to macula_quic:listen/3 via cert / key opts (after writing to disk). The cert wraps the identity's macula pubkey; no CA chain required. Used by station listeners running pubkey-anchored peering.
Get connection stats. NOT IMPLEMENTED — answers {error, not_implemented}.
Hand off a stream to another process (for macula_dist).
Complete TLS handshake. With Quinn, handshake completes during accept — this is a no-op for compat.
Listen on a port or {Address, Port} tuple.
Listen on a specific bind address and port. BindAddr is a binary: "0.0.0.0", "192.168.1.1", "2600:3c0e::100", etc.
Path MTU as discovered by Quinn's DPLPMTUD on this connection. Returns {ok, Bytes} once the path MTU has been established; {error, no_path_mtu} early in the connection lifecycle (before the first probe lands) or if the peer disabled datagrams. Phase 4.2.
Open a new bidirectional stream, owned by the calling process.
Open stream with options map (for macula_dist).
Get remote address of a connection.
Abruptly reset a stream's send side with ErrorCode — a QUIC RESET_STREAM frame, genuinely peer-visible at the transport level: the peer's RecvStream::read fails with {quic, stream_closed, PeerStream, {reset, ErrorCode}} instead of the clean EOF close_stream/1 produces. Returns at once: data queued on the stream is dropped, and a send/2 waiting for its write returns {error, reset}. ErrorCode must fit a QUIC VarInt (< 2^62); out-of-range values answer {error, error_code_out_of_range}.
Send data on a stream, waiting in the calling process until the data is written or the write fails.
Set active mode on a stream handle.
The reference in this open's result message.
Types
Functions
Accept stream with options and timeout (for macula_dist).
Start accepting connections on a listener. Delivers {quic, new_conn, ConnRef, Info} to the calling process.
Start accepting streams on a connection. Delivers {quic, new_stream, StreamRef, #{conn => ConnRef}} to the owning process.
-spec async_connect(Host, inet:port_number(), list(), timeout()) -> {ok, dial()} | {error, term()} when Host :: binary() | string().
Start a dial and return at once, instead of waiting as connect/4 does. The calling process owns the dial and later receives {quic, connected, Tag, ConnRef} or {quic, connect_failed, Tag, Reason}, where Tag is dial_tag(Dial). The dial ends early when cancel_connect/1 is called or when its owner exits. Opts and Timeout are those of connect/4; options that cannot be used return {error, Reason} at once.
-spec async_open_stream(reference()) -> {ok, stream_opening()} | {error, term()}.
Start opening a bidirectional stream and return at once, instead of waiting as open_stream/1 does. The calling process owns the open and later receives {quic, stream_opened, Tag, StreamRef}, and owns that stream, or {quic, stream_open_failed, Tag, Reason}, where Tag is stream_open_tag(Opening). The open waits for as long as the peer allows no further stream, and ends early when cancel_open_stream/1 is called or when its owner exits. On a closed connection it returns {error, already_closed} at once.
Queue data on a stream and return at once.
Returns ok when the data is queued for the stream's writer task. When the stream already has 1 MiB queued, queues nothing and returns {error, busy}; the calling process later gets one {quic, send_ready, Stream, undefined} message, meaning it may retry. That message also comes when the stream's writes stop meanwhile, and the retry then returns the reason. Returns {error, already_closed} after close_stream/1 or reset_stream/2, and the reason once the stream's writes have failed.
When a write fails, the stream's owner gets one {quic, send_failed, Stream, Reason} message.
Async shutdown connection.
Async shutdown stream. Code now genuinely reaches the wire via reset_stream/2 — previously a stub that silently discarded both Flag and Code and always did a graceful close_stream/1. Flag is unused (reserved; no caller has ever needed it, kept for signature compatibility).
-spec cancel_connect(dial()) -> ok.
End a dial. Afterwards no result for it is in, or will reach, the caller's mailbox: a result sent before the cancel is taken out, and the connection it carried is closed. Call it from the dial's owner.
-spec cancel_open_stream(stream_opening()) -> ok.
End a stream open. Afterwards no result for it is in, or will reach, the caller's mailbox: a result sent before the cancel is taken out, and the stream it carried is reset. Call it from the open's owner.
-spec close(reference()) -> ok.
Generic close — tries stream, then connection, then listener.
-spec close_connection(reference()) -> ok.
Close a connection.
-spec close_listener(reference()) -> ok.
Close a listener. Once it returns the owner receives no further {quic, new_conn, ...} from it: a connection whose handshake completes after the close is closed instead.
-spec close_stream(reference()) -> ok.
Close a stream's sending side gracefully, and return at once.
Data queued before the close is still written, and then a QUIC FIN ends the stream: the peer's RecvStream::read resolves {ok, none}. When that data cannot be written within the linger bound, the stream is reset with application error code 1, which means the stream closed and its unwritten data was dropped after the linger bound. The bound is the macula application env quic_close_linger_ms, 30000 by default, read when close_stream/1 is called. For an immediate, peer-visible abort see reset_stream/2.
-spec connect(Host, inet:port_number(), list(), timeout()) -> {ok, reference()} | {error, term()} when Host :: binary() | string().
Connect to a remote QUIC server. Host is a hostname or IP-string; validation depends on verify / verify_pubkey opts.
Trust modes (most to least authenticated):
{verify_pubkey, Pin}— pin the leaf cert's Ed25519 SPKI toPin(32 bytes). No CA chain. Overridesverify.{verify, webpki}— webpki roots + hostname check (Let's Encrypt-anchored station certs). THE DEFAULT since 5.0.0; before that the default wasnone.{verify, none}— skip all server-cert verification. Development / self-signed labs only; a network MITM can impersonate the peer. Must now be opted into explicitly, and every such dial logs a warning.
The calling process waits for the result, and the dial ends if that process exits while it waits. Use async_connect/4 to wait elsewhere.
Transfer ownership of a handle to another process. Works with both stream and connection handles. Returns once no message for the handle is on its way to the former owner: from then on every data message and event of a stream, and every new_stream notice of a connection, goes to Pid.
The reference in this dial's result message.
-spec generate_self_signed_cert(Pubkey :: binary(), Privkey :: binary(), Sans :: [binary() | string()]) -> {ok, {CertPem :: binary(), KeyPem :: binary()}} | {error, term()}.
Generate a self-signed X.509 cert from an Ed25519 keypair. Returns {ok, {CertPem, KeyPem}} as PEM-encoded binaries suitable for handing to macula_quic:listen/3 via cert / key opts (after writing to disk). The cert wraps the identity's macula pubkey; no CA chain required. Used by station listeners running pubkey-anchored peering.
Get connection stats. NOT IMPLEMENTED — answers {error, not_implemented}.
⚠ This used to answer {ok, [{S, 0} || S <- Stats]} — plausible, well-formed, permanently zero — and excuse itself with "zeroed values are harmless (dist_util only uses these for liveness signals)". That is precisely the use a hardcoded zero destroys. A counter that always reads zero makes "nothing is moving" indistinguishable from "nobody implemented the counter", so any liveness check built on it is green forever and its author cannot tell.
That is not hypothetical. On 2026-08-13 station-it-milan received every packet sent to it, answered none for thirty hours, and every signal derived from the BEAM read healthy. Anyone reaching for a send-side counter to catch that would have found this one, and it would have lied. Failing loudly is the only honest answer until the NIF surfaces the real thing.
Quinn HAS the numbers: quinn::Connection::stats() carries udp_tx{datagrams,bytes}, udp_rx{...} and path{rtt,lost_packets,black_holes_detected}, and nif_max_datagram_size already calls stats() and discards all but path.current_mtu. Surfacing the rest is an extension of a working function — see macula-station plans/PLAN_WIRE_LIVENESS_TRIPWIRE.md commit 5.
The sole consumer, the getstat callback in macula_dist, already has an {error, _} -> {ok, 0, 0, 0} branch, so this changes no behaviour there. It changes what the next caller is told.
Hand off a stream to another process (for macula_dist).
Complete TLS handshake. With Quinn, handshake completes during accept — this is a no-op for compat.
-spec listen(inet:port_number() | {string() | binary(), inet:port_number()}, list()) -> {ok, reference()} | {error, term()}.
Listen on a port or {Address, Port} tuple.
-spec listen(binary() | string(), inet:port_number(), list()) -> {ok, reference()} | {error, term()}.
Listen on a specific bind address and port. BindAddr is a binary: "0.0.0.0", "192.168.1.1", "2600:3c0e::100", etc.
stream_receive_window and receive_window are the credit, in bytes, a peer gets on one stream and across all of a connection's streams before this side reads: 16 MiB and 64 MiB unless set. A value that is not a positive integer returns {error, {invalid_receive_window, Value}}.
-spec max_datagram_size(reference()) -> {ok, pos_integer()} | {error, term()}.
Path MTU as discovered by Quinn's DPLPMTUD on this connection. Returns {ok, Bytes} once the path MTU has been established; {error, no_path_mtu} early in the connection lifecycle (before the first probe lands) or if the peer disabled datagrams. Phase 4.2.
Open a new bidirectional stream, owned by the calling process.
The calling process waits until the peer allows another stream or the connection ends, for as long as that takes, and the open ends if that process exits while it waits. Use async_open_stream/1 to wait elsewhere.
Open stream with options map (for macula_dist).
-spec peername(reference()) -> {ok, {string(), inet:port_number()}} | {error, term()}.
Get remote address of a connection.
-spec reset_stream(reference(), non_neg_integer()) -> ok | {error, term()}.
Abruptly reset a stream's send side with ErrorCode — a QUIC RESET_STREAM frame, genuinely peer-visible at the transport level: the peer's RecvStream::read fails with {quic, stream_closed, PeerStream, {reset, ErrorCode}} instead of the clean EOF close_stream/1 produces. Returns at once: data queued on the stream is dropped, and a send/2 waiting for its write returns {error, reset}. ErrorCode must fit a QUIC VarInt (< 2^62); out-of-range values answer {error, error_code_out_of_range}.
Send data on a stream, waiting in the calling process until the data is written or the write fails.
A stream's writes run in a writer task on the QUIC runtime; the calling process waits in a receive, not in a NIF. It waits for as long as the peer withholds flow-control credit. For a bounded wait, use async_send/2 with its busy result and send_ready message, or reset_stream/2.
Returns ok; {error, already_closed} after close_stream/1 or reset_stream/2; {error, reset} when reset_stream/2 dropped the data; {error, closed} when the stream ended without writing it; or the reason the stream's writes failed.
Set active mode on a stream handle.
-spec stream_open_tag(stream_opening()) -> reference().
The reference in this open's result message.