Skip to content

colibri 0.5.0: the server over QUIC, and content codings

Choose a tag to compare

@c4milo c4milo released this 29 Sep 12:34
· 745 commits to main since this release

colibri 0.5.0 adds the server over QUIC and content codings. server.Endpoint holds QUIC connections behind the caller's UDP socket, and server.QuicConnection serves h3 with the same calls and events as the TCP Connection. A TCP connection over TLS advertises h3 with Alt-Svc when its configuration names a port. The server codes a response in gzip or deflate when its request accepts one and the caller marks the response codable, and the client offers both codings and decodes them. The client retires a QUIC connection near its idle timeout, and keeps one with an exchange outstanding open.

Breaking

  • server: respond(id, .{ .status, .fields, .end }) and write_body(id, .{ .octets, .end }) take the response's head and content as structs, server.Response and server.Content (3542f0f).
  • server: Event gains done, which says a response's memory is the caller's again (decision 103, 0fb5878). Protocol gains h3 (c533709).
  • h2: Event gains alt_svc, an ALTSVC frame a client read (RFC 7838 §4, 9796e95). Each write call asserts that the connection's preface went out first, as h2's contract always asked: call write_pending before writing a request or a response (e3126a9).
  • client: Outcome.malformed also covers content whose coding is corrupt or cut short (23c454b).
  • qlog: Log.init takes the CPU features stdx's writer uses, qlog.Features.detect() or .target() (6b7e03d).

To move from 0.4.0:

  • Pass respond and write_body a struct.
  • Handle done in a server's event switch, and release the response's memory there.
  • Handle alt_svc in an h2 event switch; only a client receives it.
  • Call h2.Connection.write_pending before writing any frame of your own.
  • Pass the CPU features to qlog.Log.init.

Added

  • server (design §8 step 17b):
    • Endpoint, or EndpointOf(connections_max, receive_capacity), routes each datagram by its first packet's Destination Connection ID (RFC 9000 §5.2) and starts a connection from a client's first Initial. It answers Version Negotiation and Retry itself. send writes each datagram it owes with the address it goes to, and ended hands back each connection that is over.
    • QuicConnection serves h3. write_body sends the caller's octets from where they are, and a request is done once the peer acknowledged every octet of its response.
    • EndpointConfig.logs, a log provider the endpoint asks for each connection's qlog log, and hands each log back once its connection is over (decision 102 as amended, 7cfa88c).
    • Config.h3_alternative names a UDP port that each connection over TLS advertises: an Alt-Svc line on each final h11 response, and one ALTSVC frame per h2 connection (RFC 7838 §3, §4).
  • Content codings (design §8 step 17e, decision 101):
    • server: Config.codings and Config.encoders, and the same in QuicConfig, name the codings a server applies, in its order of preference, and the pool of encoders, EncoderPool(count, level) or DefaultEncoderPool. A final response marked Response.codable goes out in the coding its request's Accept-Encoding accepts with the highest weight. It gains Content-Encoding and loses Content-Length, and a strong ETag becomes weak. A marked response Accept-Encoding could choose carries Vary: accept-encoding, coded or not. With every encoder taken, the response goes out uncoded.
    • client: Config.codings and Config.decoders, and the same in QuicConfig, name the codings a client offers and the pool of decoders, DecoderPool(count) or DefaultDecoderPool. The client offers them in Accept-Encoding, each after the first a tenth lighter, takes a decoder as it writes the request, decodes a response in a coding it offered into the exchange's body, and reports the coding in HttpExchange.coding. A response coded twice, in a coding it did not offer, or in a 206 goes on as it arrived.
    • server.Coding and client.Coding name the codings, http.content_coding reads Accept-Encoding's weights (RFC 9110 §12.4.2, §12.5.3), and h11.coding.reserve takes a decoder before a message's coding is known.
    • colibri's package exports stdx's codec, gzip, zlib, zstd and brotli, so a dependent codes request content with the stdx the library pins.
  • client (step 17g): a QUIC connection that has held no exchange until its idle deadline less a margin takes no new exchange and closes, and the channel's next exchange opens another (RFC 9114 §5.1). One holding an exchange sends a PING at that instant instead (RFC 9000 §10.1.2). The margin is one PTO, at least 1 s and at most half the timeout. The client also learns h3 from an ALTSVC frame over h2.
  • quic: connection_idle gives the idle deadline and the effective timeout, and owe_keep_alive has the next 1-RTT packet elicit an acknowledgment.
  • qlog: each packet event names the tuple it went to or came from, and quic:tuple_assigned numbers each new address (84cd3ad).
  • h2: the ALTSVC frame, written with Connection.write_alt_svc and read by a client.

Dependencies

  • stdx is pinned at 8c2a3b8, where 0.4.0 pinned eb0f0c7. chapulin's pin is unchanged.
    • Its JSON writer and reader take the CPU features their vector paths may use, which is why qlog.Log.init does (6b7e03d).
    • Its brotli decoder carries a fix, and its JSON and DEFLATE's arm64 path are faster (bb9263e).

Fixed

  • server: an h2 connection that read a request with the client's preface, over TLS in the flight of the client's Finished, answered it before its own SETTINGS, which RFC 9113 §3.4 requires first, and colibri's client failed the connection (e3126a9).
  • quic: a probe above the Initial level repeats its space's latest ACK frame, so a peer whose acknowledgments were all lost learns which packets arrived (RFC 9000 §13.2, 1a4bcda).
  • server: a client's close no longer ends a QUIC connection as a failure (87fff2f).

Checked against

CI ran tools/ci.sh on bb9263e, on Ubuntu 24.04, and every section passed:

  • h2spec 2.6.0: 144 passed in cleartext and over TLS, and 2 skipped by name, which test an RFC 7540 rule RFC 9113 dropped. h3spec 0.1.13: 49 examples, 0 failures.
  • The client and the server over h11 and h2 against Go 1.27.1, nghttpd 1.52.0, h2o 2.2.5, curl 7.88.1 and nghttp, in cleartext and over TLS.
  • Content codings over h11 and h2, in cleartext and over TLS: curl with --compressed and Go's client read the server's gzip, and the client decoded gzip from Go's server and h2o's, octet for octet.
  • The client's Channel fetched over h3 from aioquic 1.3.0 and quic-go 9d085cc, and over h2 from Go's server with no UDP.
  • aioquic in both directions over hq-interop and h3, QPACK against ls-qpack, the TLA+ models with the h2, h3 and client traces, the Lean proofs, and 2,325 tests in Debug and in ReleaseSafe. The tests also passed on Linux and macOS arm64.

The HTTP Garden fed its 72 streams to colibri's server and to 35 other origins on bb9263e, and compared every one.

The QUIC Interop Runner ran on bb9263e. colibri passed every case against quic-go, ngtcp2, neqo, quinn and itself, as the server and as the client, and quic-go ran no ecn case. Every qlog file colibri wrote, in 168 test cases, passed tools/qlog_check.py.

docs/design.md §8 records each check: what ran, on what machine, and what it printed.

Install

zig fetch --save git+https://github.com/c4milo/colibri#v0.5.0