Documentation
¶
Overview ¶
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.
Index ¶
- Constants
- Variables
- func GenerateSelfSignedCert(hosts ...string) (tls.Certificate, error)
- type Client
- type ClientConfig
- type Config
- type Conn
- func (c *Conn) AcceptStream(ctx context.Context) (*Stream, error)
- func (c *Conn) AcceptUniStream(ctx context.Context) (*UniReceiveStream, error)
- func (c *Conn) Close() error
- func (c *Conn) ConnectionState() tls.ConnectionState
- func (c *Conn) Context() context.Context
- func (c *Conn) IsZeroRTT() bool
- func (c *Conn) OpenStream(ctx context.Context) (*Stream, error)
- func (c *Conn) OpenUniStream(ctx context.Context) (*UniStream, error)
- func (c *Conn) QUIC() *quicgo.Conn
- func (c *Conn) Recv() ([]byte, error)
- func (c *Conn) Send(frame []byte) error
- type Server
- type ServerConfig
- type Stream
- type UniReceiveStream
- type UniStream
Constants ¶
const ALPN = "zap/1"
ALPN is the ALPN protocol identifier negotiated for every ZAP-QUIC connection. Servers reject TLS handshakes that propose any other ALPN.
const MaxFrameSize = 10 * 1024 * 1024
MaxFrameSize bounds a single ZAP frame on a QUIC stream. Identical to the TCP transport's bound — 10 MiB — so the application layer sees the same envelope regardless of transport.
Variables ¶
var ( // ErrConnClosed is returned by Send/Recv/OpenStream after the // underlying QUIC connection has been closed (graceful or abrupt). ErrConnClosed = errors.New("zap/quic: connection closed") // ErrFrameTooLarge is returned when reading a frame whose // declared length exceeds MaxFrameSize. ErrFrameTooLarge = errors.New("zap/quic: frame too large") // ErrBadHandshake is returned when the peer's identity handshake // is malformed or empty. ErrBadHandshake = errors.New("zap/quic: invalid handshake") // ErrBadALPN is returned when the negotiated ALPN is not "zap/1". // quic-go enforces ALPN at the TLS layer; this is a belt-and-suspenders // post-handshake check. ErrBadALPN = errors.New("zap/quic: unexpected ALPN") )
Common errors.
var ErrServerClosed = errors.New("zap/quic: server closed")
ErrServerClosed is returned by Accept after Close has been called. It is the unwrap target for errors from a closed listener.
Functions ¶
func 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 ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
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 ¶
func NewClient(cfg ClientConfig) (*Client, error)
NewClient returns a Client that listens on a random local UDP port.
NewClient does NOT establish any QUIC connections; use Dial to connect to a specific peer.
func (*Client) Close ¶
Close releases the underlying UDP socket. In-flight Dial calls return errors; existing *Conn objects remain valid until their own Close.
func (*Client) Dial ¶
Dial establishes a QUIC connection to addr.
addr is an "ip:port" string resolvable as a UDP address.
On a second-and-later dial to the same peer, if a valid TLS 1.3 session ticket is cached, 0-RTT resumption is attempted (the returned Conn's IsZeroRTT() reports whether the server accepted it).
func (*Client) DialEarly ¶
DialEarly is identical to Dial but explicitly attempts a 0-RTT handshake. If no session ticket is cached for the peer, this behaves identically to Dial (a full 1-RTT handshake is performed).
type ClientConfig ¶
type ClientConfig struct {
// NodeID is this node's ZAP identity. Sent on the control stream
// during the post-handshake identity exchange. Required.
NodeID string
// TLS is the TLS 1.3 client config. Required.
//
// If MinVersion / CurvePreferences / NextProtos are not set, they
// are defaulted to:
//
// MinVersion = TLS 1.3
// CurvePreferences = [X25519MLKEM768, X25519]
// NextProtos = ["zap/1"]
//
// The caller is responsible for providing RootCAs or
// VerifyPeerCertificate. Test code may set InsecureSkipVerify; do
// not do this in production.
TLS *tls.Config
// QUIC, if non-nil, is the quic-go Config used for dialing.
QUIC *quicgo.Config
// SessionCache is the client-side TLS 1.3 session ticket cache.
// When non-nil, 0-RTT resumption is enabled for second-and-later
// dials to the same peer.
//
// If nil, the TLS config's own ClientSessionCache (defaulted to
// an in-memory LRU of 128 entries by applyZAPDefaults) is used.
SessionCache tls.ClientSessionCache
// Logger for transport events. Defaults to slog.Default().
Logger *slog.Logger
}
ClientConfig configures a QUIC dialer.
type Config ¶
type Config struct {
// QUIC is the underlying quic-go transport config. Optional;
// nil yields ZAP's defaults (30s idle timeout, 15s keepalive,
// 1024 max concurrent streams in each direction).
QUIC *quicgo.Config
// RejectEarlyData disables 0-RTT resumption on the server side.
// Set this to true if any RPC handler is non-idempotent — 0-RTT
// data is replayable by an active network adversary.
RejectEarlyData bool
}
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},
}
type Conn ¶
type Conn struct {
// PeerID is the ZAP node identity asserted by the peer in the
// identity handshake on the control stream.
PeerID string
// LocalAddr / RemoteAddr expose the underlying UDP 5-tuple at the
// time the connection was created. Because QUIC supports connection
// migration, RemoteAddr may differ from the address actually
// observed for the most recent packet — these fields are
// informational, not authoritative.
LocalAddr net.Addr
RemoteAddr net.Addr
// contains filtered or unexported fields
}
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 (*Conn) AcceptStream ¶
AcceptStream blocks until the peer opens a new bidirectional QUIC stream. Used by the server to serve per-RPC streams from the client.
func (*Conn) AcceptUniStream ¶
func (c *Conn) AcceptUniStream(ctx context.Context) (*UniReceiveStream, error)
AcceptUniStream blocks until the peer opens a unidirectional QUIC stream addressed to this side.
func (*Conn) Close ¶
Close initiates a graceful close of the QUIC connection.
Graceful close protocol:
- CloseWrite() on the control stream so the peer's Recv loop observes io.EOF after draining all in-flight frames.
- Wait up to drainTimeout for the peer's control stream to deliver its own EOF (the symmetric DRAIN signal).
- CloseWithError(0, "") on the underlying *quic.Conn, which emits a QUIC CONNECTION_CLOSE frame with error code 0.
This is the ALPN-coordinated DRAIN sequence the mission spec calls for: in-flight frames are flushed before connection close.
func (*Conn) ConnectionState ¶
func (c *Conn) ConnectionState() tls.ConnectionState
ConnectionState returns the TLS connection state of the underlying QUIC handshake. Notably, ConnectionState().CurveID reports the negotiated TLS NamedGroup — used by tests to assert that X25519MLKEM768 (0x11ec) is the actual negotiated KEM.
func (*Conn) Context ¶
Context returns the underlying connection's context, which is canceled when the connection terminates (either side).
func (*Conn) IsZeroRTT ¶
IsZeroRTT reports whether the QUIC handshake completed with 0-RTT early data accepted by the server. Only meaningful on the client side after a resumed connection.
func (*Conn) OpenStream ¶
OpenStream opens a new bidirectional QUIC stream for an independent request/response exchange. The returned Stream wraps QUIC-level flow control so the caller cannot overrun the peer.
The typical RPC pattern is:
s, err := conn.OpenStream(ctx)
if err != nil { return err }
defer s.Close()
s.WriteFrame(reqBytes) // sends one ZAP frame
respBytes, err := s.ReadFrame()
After Close, the stream ID is freed and may be reused.
func (*Conn) OpenUniStream ¶
OpenUniStream opens a unidirectional send stream, used for fire-and-forget subscription deliveries and broadcasts where the peer never replies.
func (*Conn) QUIC ¶
QUIC returns the underlying *quic.Conn. Exposed for advanced callers (e.g. tests that need to inspect ConnectionState or trigger a migration). Most consumers should not touch this.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
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 ¶
func Listen(cfg ServerConfig) (*Server, error)
Listen creates a new QUIC listener bound to cfg.Addr.
The returned Server is ready for Accept calls. Close releases the UDP socket and all in-flight connections.
func (*Server) Accept ¶
Accept waits for and returns the next ZAP-QUIC connection. The returned Conn has already completed the node-identity handshake on the control stream.
type ServerConfig ¶
type ServerConfig struct {
// NodeID is this node's ZAP identity. Sent on the control stream
// of every accepted connection. Required.
NodeID string
// Addr is the UDP address to listen on. Empty means ":0" (random
// kernel-assigned port). Resolved as a UDP address.
Addr string
// TLS is the TLS 1.3 server config. Required field
// Certificates (or GetCertificate) must be populated by the caller.
//
// If MinVersion / CurvePreferences / NextProtos are not set, they
// are defaulted to:
//
// MinVersion = TLS 1.3
// CurvePreferences = [X25519MLKEM768, X25519]
// NextProtos = ["zap/1"]
//
// Any caller-supplied NextProtos list will have "zap/1" prepended
// if missing.
TLS *tls.Config
// QUIC, if non-nil, is the quic-go Config. Defaults are picked
// for the common ZAP workload (multiplex up to 1024 streams,
// enable 0-RTT, allow connection migration).
QUIC *quicgo.Config
// RejectEarlyData, if true, disables 0-RTT resumption. Application
// handlers that are NOT idempotent should set this to eliminate
// the 0-RTT replay-attack surface.
RejectEarlyData bool
// Logger for transport events. Defaults to slog.Default().
Logger *slog.Logger
}
ServerConfig configures a QUIC listener.
type Stream ¶
type Stream struct {
// contains filtered or unexported fields
}
Stream is a bidirectional QUIC stream carrying ZAP frames.
func (*Stream) CloseWrite ¶
CloseWrite finishes the local write side. The peer will see io.EOF on subsequent ReadFrame calls after all queued data is delivered.
func (*Stream) WriteFrame ¶
WriteFrame writes one length-prefixed ZAP frame to the stream.
type UniReceiveStream ¶
type UniReceiveStream struct {
// contains filtered or unexported fields
}
UniReceiveStream is a unidirectional receive-only QUIC stream.
func (*UniReceiveStream) ReadFrame ¶
func (u *UniReceiveStream) ReadFrame() ([]byte, error)
ReadFrame reads the next ZAP frame from the stream.
type UniStream ¶
type UniStream struct {
// contains filtered or unexported fields
}
UniStream is a unidirectional send-only QUIC stream.
func (*UniStream) Close ¶
Close finishes the stream. The receiver sees io.EOF after the last frame is delivered.
func (*UniStream) WriteFrame ¶
WriteFrame writes one ZAP frame to the unidirectional stream.