openssl/openssl
QUIC
Active contributors: Hugo Landau, Tomáš Kasal, Matt Caswell, Neil Horman, Tomas Mraz
Purpose
libssl ships an in-tree QUIC v1 (RFC 9000–9002) implementation. Both client and server are supported (server stack added in 3.5). The TLS handshake reuses the existing TLS 1.3 state machine in ssl/statem/ via a custom record method that delivers TLS messages as QUIC CRYPTO frames; the rest of the protocol — packet protection, congestion control, loss recovery, stream multiplexing, flow control, connection migration, retry, version negotiation — lives entirely in ssl/quic/.
OpenSSL's QUIC has a deliberately narrow interface to the application:
- The application supplies a UDP socket (or a
BIO_s_datagram/ custom datagram BIO). - It uses
SSL_*APIs to drive streams, handshake, and connection lifecycle. - Internal threads can optionally be enabled to drive the protocol while the application is idle.
Directory layout
ssl/quic/
├── quic_impl.c 168 KB -- the SSL_* API surface for QUIC
├── quic_channel.c 140 KB -- per-connection state machine (handshake -> idle -> closing)
├── quic_txp.c 111 KB -- transmission processor (assembles outbound packets)
├── quic_ackm.c 59 KB -- ACK manager: loss detection, RTT estimation, ack-eliciting tracking
├── quic_record_rx.c 50 KB -- AEAD-protected packet receive/decrypt path
├── quic_rx_depack.c 50 KB -- post-decrypt frame demultiplexer
├── quic_record_tx.c 30 KB -- AEAD-protected packet transmit path
├── quic_record_shared.c, quic_record_util.c
├── quic_port.c 62 KB -- the listening "port" object (multiplexes connections to one UDP socket)
├── quic_stream_map.c 26 KB -- the per-connection stream table
├── quic_demux.c 14 KB -- incoming-packet demultiplexer (matches DCID -> connection)
├── quic_lcidm.c 16 KB -- Local Connection ID manager
├── quic_rcidm.c 20 KB -- Remote Connection ID manager
├── quic_srtm.c 16 KB -- Stateless Reset Token manager
├── quic_srt_gen.c
├── quic_fc.c 10 KB -- per-stream + per-connection flow control
├── quic_cfq.c 9 KB -- the "control-frame queue" of frames awaiting transmission
├── quic_fifd.c 10 KB -- frame-in-flight database
├── quic_sf_list.c, quic_sstream.c, quic_rstream.c -- send/receive stream byte buffers
├── quic_wire.c, quic_wire_pkt.c -- wire-format encode/decode for frames and packets
├── quic_tls.c, quic_tls_api.c -- the QUIC<->TLS bridge (the OSSL_RECORD_METHOD plumbing)
├── quic_thread_assist.c, quic_reactor.c, quic_reactor_wait_ctx.c -- the optional internal-thread reactor
├── quic_engine.c, quic_engine_local.h -- the top-level engine that owns ports
├── quic_obj.c, quic_obj_local.h -- the SSL-object polymorphism (TLS-SSL, QUIC-CONN, QUIC-STREAM, QUIC-LISTENER, QUIC-DOMAIN)
├── quic_method.c, quic_local.h, quic_types.c
├── quic_statm.c -- statistics
├── quic_trace.c, qlog.c, qlog_event_helpers.c, json_enc.c -- qlog tracing (RFC-style JSON event logging)
├── quic_tserver.c -- a tiny test server harness
├── cc_newreno.c -- the NewReno congestion controller
├── uint_set.c -- a sparse-set helper
└── build.info
ssl/rio/
├── poll_builder.c, poll_immediate.c, poll_method.h, poll_builder.h
└── rio_notifier.cssl/rio/ ("Reactor I/O") is the asynchronous polling layer used by the QUIC engine to wait on multiple sockets/timers. It abstracts select/poll/epoll/kqueue so the QUIC reactor can be portable.
SSL object polymorphism
OpenSSL extended the SSL type to be a polymorphic handle. Today the same SSL * may be one of:
| Kind | Created by | What it is |
|---|---|---|
TLS-SSL |
SSL_new(ctx) with a TLS method |
A TLS 1.x or DTLS connection. |
QUIC-CONN |
SSL_new(ctx) with OSSL_QUIC_client_method() or OSSL_QUIC_server_method() |
A QUIC connection. |
QUIC-STREAM |
SSL_new_stream(qconn, flags) |
A bidirectional or unidirectional QUIC stream within a connection. |
QUIC-LISTENER |
SSL_new_listener(ctx) |
A QUIC listener bound to a UDP socket (server side). |
QUIC-DOMAIN |
SSL_new_domain(ctx) |
A "domain" grouping multiple listeners under a single event loop. |
quic_obj.c and quic_obj_local.h implement the dispatch. Most SSL_* calls work on any kind of object where they make sense (e.g. SSL_read, SSL_write); a smaller set requires a specific kind (SSL_set_blocking_mode, SSL_get_stream_id, SSL_accept_stream).
Lifecycle (client)
sequenceDiagram
participant App
participant QC as QUIC-CONN (quic_impl.c, quic_channel.c)
participant TXP as quic_txp.c
participant AKM as quic_ackm.c
participant TLS as TLS state machine (statem_clnt.c)
participant UDP as BIO_s_datagram
App->>QC: SSL_new(ctx, OSSL_QUIC_client_method())
App->>QC: SSL_set_initial_peer_addr(...) ; SSL_set1_host(...)
App->>QC: SSL_connect()
QC->>TLS: drive handshake; TLS messages are QUIC CRYPTO frames
QC->>TXP: queue Initial / Handshake / 1-RTT packets
TXP->>UDP: BIO_write_ex(datagram)
UDP-->>QC: BIO_read_ex(datagram)
QC->>AKM: register sent packets; handle ACK frames
QC->>TLS: deliver received CRYPTO frames
TLS-->>QC: keying material via OSSL_RECORD_METHOD callbacks
QC->>QC: install Initial/Handshake/1-RTT keys
QC-->>App: handshake complete
App->>QC: SSL_new_stream / SSL_write / SSL_readThe same pattern holds server-side, with the listener accepting a connection that is delivered to the application via SSL_accept_connection.
QUIC-TLS bridge
ssl/quic/quic_tls.c and quic_tls_api.c implement a custom OSSL_RECORD_METHOD that the TLS state machine uses. Instead of TLS records, the method packages handshake messages into QUIC CRYPTO frames at the appropriate encryption level (Initial, Handshake, 1-RTT). The state machine itself doesn't know it's running over QUIC — it just calls into its record method. Keys are derived in TLS 1.3, exported via tls13_export_keying_material, and installed into quic_record_rx.c / quic_record_tx.c for packet protection.
The encryption levels:
| QUIC encryption level | Keys derived from | Used for |
|---|---|---|
| Initial | A salt + ConnectionID (RFC 9001 §5.2) | First Initial packet (CRYPTO with ClientHello). |
| Handshake | TLS 1.3 handshake_secret | Encrypts CRYPTO with EE/Cert/CertVerify/Finished. |
| 1-RTT | TLS 1.3 application_secret | Encrypts streams and most control frames. |
| 0-RTT | TLS 1.3 early_secret | Optional; not yet enabled in OpenSSL's default config. |
Packet processing pipeline
quic_demux.creads a UDP datagram viaBIO_recv_ex. It parses the QUIC short/long header to extract the Destination Connection ID (DCID).quic_demux.clooks the DCID up inquic_lcidm.cto find the owning connection (or the listener for a new connection).quic_record_rx.cdecrypts the packet using the key and PN cipher for the relevant encryption level.quic_rx_depack.cwalks the now-plaintext payload and dispatches each frame: STREAM, ACK, MAX_DATA, MAX_STREAM_DATA, NEW_CONNECTION_ID, RETIRE_CONNECTION_ID, HANDSHAKE_DONE, CONNECTION_CLOSE, CRYPTO, NEW_TOKEN, PING, PADDING, RESET_STREAM, STOP_SENDING, PATH_CHALLENGE, PATH_RESPONSE, ….- STREAM frames go to
quic_rstream.c's receive buffer. - CRYPTO frames go to the TLS bridge.
- ACK frames go to
quic_ackm.cto update RTT and confirm in-flight packets. - Stream control frames update flow-control state (
quic_fc.c).
The send side mirrors this: quic_txp.c collects pending frames from various queues and the in-flight DB, builds packets at the right encryption level, and writes them via the configured datagram BIO.
Loss recovery and congestion control
ssl/quic/quic_ackm.c is the largest QUIC file outside quic_impl.c because RFC 9002 (loss detection / congestion control) is intricate:
- Sent-packet history (
quic_fifd.c). - RTT samples and smoothed RTT.
- Loss thresholds (kPacketThreshold, kTimeThreshold).
- ACK frequency hints.
- ECN handling (basic).
cc_newreno.c is the default congestion controller (a NewReno variant). The CC interface (include/internal/quic_cc.h) is pluggable so future CCAs (BBR, CUBIC) can drop in.
Stream model
Each connection has a stream map (quic_stream_map.c) keyed by stream id. Streams are bidirectional or unidirectional, client- or server-initiated. Each stream has:
- A receive buffer (
quic_rstream.c) reassembled from STREAM frames using the segmented-byte-buffer (quic_sf_list.c). - A send buffer (
quic_sstream.c). - Flow control state.
SSL_*operations are implemented as forwards from the parentQUIC-CONNSSL *to the per-streamSSL *.
SSL_read / SSL_write on a QUIC-STREAM are stream byte-stream operations, not record-bounded.
Connection IDs and stateless reset
QUIC supports connection migration: a client may change its source address mid-connection without breaking the connection, because routing is by Destination Connection ID rather than 4-tuple. The local CIDs (quic_lcidm.c) and remote CIDs (quic_rcidm.c) are independent rotating sets. Stateless reset (quic_srtm.c, quic_srt_gen.c) lets a server that loses connection state respond with an unforgeable reset token rather than ignoring the packet.
qlog
ssl/quic/qlog.c and qlog_event_helpers.c emit qlog events (the IETF JSON tracing format for QUIC) to a file. Enable via SSL_QLOG_DIR=/tmp/qlog env var or programmatic API. quic_trace.c is an additional simpler text trace.
Threading
QUIC needs background work: timers fire even when the application isn't calling into it. Two modes:
- Application-driven: the application periodically calls
SSL_handle_events(ssl)(or anySSL_*operation that drives the reactor). No hidden threads. - Thread assisted: enable
SSL_VALUE_QUIC_INTERNAL_THREADS_ENABLEand OpenSSL spawns a helper thread that runs the reactor. The application can do blocking I/O elsewhere.
The reactor itself (quic_reactor.c) runs over the RIO poll abstraction.
Listener / domain
Server-side, multiple connections share one UDP socket. The QUIC-LISTENER owns the socket and the QUIC-PORT (quic_port.c) demuxes incoming packets to existing connections or accepts new ones. A QUIC-DOMAIN aggregates multiple listeners under a single event loop, useful for servers that want to terminate on multiple addresses in one process.
Configuration
SSL_set_initial_peer_addr/SSL_set1_host— required client-side knobs.SSL_set_blocking_mode— toggle blocking semantics.SSL_set1_qlog_dir— qlog destination.SSL_set_default_stream_mode— letSSL_read/SSL_writeon the connection handle implicit default streams.
Many configuration knobs are exposed via the generic SSL_set/get_value_uint(ssl, class, id, &v) API. See include/openssl/quic.h and doc/man3/SSL_get_value_uint.pod.
Integration points
- TLS state machine is reused unchanged. The bridge is the QUIC
OSSL_RECORD_METHOD. - EVP provides every cryptographic primitive (AES-GCM/ChaCha20-Poly1305 for AEAD, HKDF for key derivation).
- BIO is the application-supplied datagram socket (or
BIO_s_dgram_pairfor tests). - QUIC-GROUP / QUIC-SIGALG are not separate from TLS-GROUP / TLS-SIGALG — QUIC's TLS 1.3 sub-handshake reuses the TLS provider capabilities.
Entry points for modification
- New congestion controller: implement
OSSL_CC_METHOD(include/internal/quic_cc.h), register it; consider gating viaSSL_set_value_uint. - New transport parameter: extend wire encode/decode in
quic_wire.c, the channel state, and the TLS bridge that ferries it via the TLS extension. - New frame type: add a wire decoder/encoder in
quic_wire.c, a dispatcher branch inquic_rx_depack.c, a producer inquic_txp.c. - Server-side knobs:
quic_port.cand the listener glue inquic_impl.c.
Documentation
README-QUIC.md— top-level user guide.doc/designs/quic-design/*.md— extensive design notes.doc/man7/ossl-guide-quic-introduction.podand the rest of theossl-guide-quic-*series.doc/man3/OSSL_QUIC_client_method.pod,SSL_new_stream.pod,SSL_new_listener.pod,SSL_handle_events.pod, …
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.