quic
Package quic implements the QUIC transport for the ZAP messaging substrate.
import "github.com/luxfi/zap/quic"Package quic implements the QUIC transport for the ZAP messaging substrate.
QUIC offers, compared to ZAP's existing TCP+TLS transport:
- Stream multiplexing: N concurrent RPCs over one connection without head-of-line blocking.
- Connection migration: the connection survives client-IP changes (Wi-Fi-to-LTE, NAT rebinding, etc.) because the QUIC connection ID is independent of the 5-tuple.
- 0-RTT resumption: cached session tickets let the second handshake to a peer complete without a round trip before app data flows.
- TLS 1.3 with the X25519MLKEM768 hybrid post-quantum key exchange (IANA NamedGroup 0x11ec) baked in.
Wire format
One ZAP message is one length-prefixed Cap'n Proto frame on a QUIC stream. The frame format is identical to the TCP transport:
[4-byte little-endian length][ZAP message bytes]
Two stream patterns are used:
- Bidirectional streams carry one request/response exchange. After the response is written the server side closes its half of the stream and the client closes its half on Recv, freeing the stream ID. This maps onto Node.Call.
- Unidirectional streams from the server carry one-way notifications and subscription deliveries. The receiver routes each frame through the same handler dispatch as the TCP path.
The control stream (the first bidirectional stream opened by the dialer) carries the 64-byte ZAP node-identity handshake exchanged before any RPC.
Cryptography
The default TLS 1.3 configuration prefers X25519MLKEM768. That is the IANA-registered hybrid: the shared secret is HKDF-Extract of X25519_ss concatenated with ML-KEM-768 ciphertext-derived ss. The Go runtime performs the combination internally — this package does not roll its own KEM combiner. See ../papers/pq-hybrid-kem/main.pdf.
The server certificate signature is still classical (ECDSA or RSA); post-quantum signature schemes are not yet wired into Go's stdlib TLS. The threat model that motivates X25519MLKEM768 is harvest-now / decrypt-later against the confidentiality of the key-exchange, not forgery against a trusted-today certificate authority.
0-RTT replay
QUIC permits 0-RTT application data, which can be replayed by a network adversary. The server defaults to accepting 0-RTT data, but application handlers MUST treat 0-RTT-carried RPCs as either idempotent or rejected. Use ServerConfig.RejectEarlyData to force a full 1-RTT handshake for every connection.
Functions
GenerateSelfSignedCert
func GenerateSelfSignedCert(hosts ...string) (tls.Certificate, error)GenerateSelfSignedCert generates a short-lived self-signed ECDSA-P256 certificate suitable for testing. NOT for production use: production deployments must supply real certificates via tls.Config.Certificates or GetCertificate, signed by a trusted CA.
Exposed as a public helper because tests in other packages may want to spin up a real ZAP-QUIC server.
Types
Client
Client is a QUIC dialer with optional connection migration and 0-RTT support.
One Client may be shared across many goroutines and used to Dial multiple peers concurrently. The Client owns one *quic.Transport which is bound to a single UDP socket — every Dial uses the same local socket, so a connection migration of the local side will affect all connections owned by this Client (this matches QUIC semantics).
func NewClient(cfg ClientConfig) (*Client, error)func (c *Client) Close() errorfunc (c *Client) Dial(ctx context.Context, addr string) (*Conn, error)func (c *Client) DialEarly(ctx context.Context, addr string) (*Conn, error)func (c *Client) LocalAddr() net.Addrfunc (c *Client) Transport() *quicgo.TransportClientConfig
ClientConfig configures a QUIC dialer.
Config
Config is the user-facing knob that the zap parent package's NodeConfig.QUICConfig accepts. It's a thin wrapper so callers don't have to import quic-go just to flip RejectEarlyData; they can supply nil for QUIC if defaults are fine.
Typical use:
cfg := zap.NodeConfig{ NodeID: "node-a", Port: 9999, TLS: tlsCfg, Transport: zap.TransportQUIC, QUICConfig: &quic.Config{RejectEarlyData: true}, }
Conn
Conn is a multiplexed ZAP connection over QUIC.
Conn is the transport-level connection abstraction shared between the dialer (client.go) and the listener (server.go). It exposes the same shape as ZAP's TCP *Conn (Send, Recv, Close, peer identity) and adds stream-level primitives that the underlying TCP transport cannot efficiently express.
Conn is safe for concurrent use. Send serializes onto the control stream; concurrent callers may instead OpenStream to get independent per-RPC streams.
func (c *Conn) AcceptStream(ctx context.Context) (*Stream, error)func (c *Conn) AcceptUniStream(ctx context.Context) (*UniReceiveStream, error)func (c *Conn) Close() errorfunc (c *Conn) ConnectionState() tls.ConnectionStatefunc (c *Conn) Context() context.Contextfunc (c *Conn) IsZeroRTT() boolfunc (c *Conn) OpenStream(ctx context.Context) (*Stream, error)func (c *Conn) OpenUniStream(ctx context.Context) (*UniStream, error)func (c *Conn) QUIC() *quicgo.Connfunc (c *Conn) Recv() ([]byte, error)func (c *Conn) Send(frame []byte) errorServer
Server is a QUIC listener that yields *Conn instances ready for ZAP-frame exchange.
Server is the QUIC analogue of net.Listener. The outer ZAP Node drives the accept loop; this type does not spawn its own goroutines.
func Listen(cfg ServerConfig) (*Server, error)func (s *Server) Accept(ctx context.Context) (*Conn, error)func (s *Server) Addr() net.Addrfunc (s *Server) Close() errorServerConfig
ServerConfig configures a QUIC listener.
Stream
Stream is a bidirectional QUIC stream carrying ZAP frames.
func (s *Stream) Close() errorfunc (s *Stream) CloseWrite() errorfunc (s *Stream) ReadFrame() ([]byte, error)func (s *Stream) WriteFrame(frame []byte) errorUniReceiveStream
UniReceiveStream is a unidirectional receive-only QUIC stream.
func (u *UniReceiveStream) ReadFrame() ([]byte, error)UniStream
UniStream is a unidirectional send-only QUIC stream.
func (u *UniStream) Close() errorfunc (u *UniStream) WriteFrame(frame []byte) error