Skip to content

Releases

colibri 0.14.1

Choose a tag to compare

@c4milo c4milo released this 06 Nov 04:51

colibri 0.14.1 fixes colibri.Server over h3. It pins stdx 51c91698, chapulin v0.3.0 (4da5b220) and Rotor v0.9.0 (5a5d7d72).

Fixes

  • colibri.Server read the ECN codepoint of each datagram but did not turn ECN on in its endpoint, so the first datagram marked ECT(0) or ECT(1) stopped the server on an assertion (RFC 9000 §13.4, decision 68). Server.init now turns it on, and a test sends an ECT(0) Initial.
  • On Linux arm64, the colibri tests stopped in Thread.spawn: the test server lived in thread-local storage, and glibc puts that block on each thread's stack. The test server is now a plain global.

Checked

tools/ci.sh passed on 393dd94f, with Zig 0.16.0 on macOS arm64, without the TLA+ models and trace checks, which the nightly job runs. The colibri tests passed on Ubuntu 26.04 arm64, 32 of 32.

colibri 0.14.0

Choose a tag to compare

@c4milo c4milo released this 06 Nov 03:08

colibri 0.14.0 adds colibri, a module that binds the sockets and runs the loop: a program gives colibri.Server a host, a port, a PEM identity and a server.router table, and calls run. It serves h11 and h2 over TCP and h3 over QUIC on the UDP socket of the same port, on Rotor's own loop. The client keeps a response's headers and trailers whole, and an h3 client no longer stops on a status that RFC 9110 does not name. It pins stdx 51c91698, chapulin 965668d2 and Rotor 4f818b7.

Breaking changes

  • client.Response.wanted, Response.values, Response.values_len and client.WantedHeader are gone (decision 152). A response holds its header section in Response.headers, a client.Headers, and its trailer section in Response.trailers, a client.Trailers, each over a buffer the program gives. A program reads a value with response.headers.get("content-type") and every line with iterator(). An empty buffer drops its section, and a section that does not fit ends the exchange too_large.
  • Rotor is no longer a lazy package: the library's colibri module imports it, so a project that depends on colibri fetches Rotor too.
  • An h3 request whose stream ended with its head now reports request.end with that head, and no empty body follows (decision 116 as amended). A program that reads h3 events itself reads request.end; server.router already did.

What's new

  • colibri.Server (decision 150, design §8 step 37):
    • init(config) reads the identity, given as PEM text in colibri.Identity { chain, key }, and binds. The program reads its own files and may wipe the key text after init.
    • port() returns the bound port, so a program may ask for port 0.
    • run(router, context) serves on one Rotor loop on the calling thread until shutdown(), which any thread may call.
    • deinit() closes what init opened and wipes the private key and the cookie key.
    • With an identity, the server opens UDP on the same port for h3, with UDP_GRO and UDP_SEGMENT on Linux, and its TLS connections advertise h3 there (Alt-Svc).
    • With no source of randomness given, it draws from the operating system's entropy.
    • examples/colibri_server.zig serves a route table on 127.0.0.1 port 0.
  • An h3 client reads a response's trailers, and h11 and h2 clients read them too, into Response.trailers.

Fixes

  • An h3 client converted a server's status to an enum of RFC 9110's codes, so a 429 (RFC 6585) or a 599 stopped it on an assertion. It now compares the number.
  • h11 kept a chunked message's trailer section into the next message, so a message without chunked coding that followed one with trailers reported them again, in the client and the server. h11 now clears it at each head.

Speed

  • h3: a request whose stream ended with its head takes one event, not three (design §8 step 29d.15), and a field section that inserts nothing into the QPACK table takes no encoder stream room (step 29d.13).

Checked

tools/ci.sh passed on 8ae5faf1, with Zig 0.16.0 on macOS arm64. As the owner ruled on 2026-11-05, the run left out the TLA+ models and the seven trace checks, which a nightly job will run.

colibri 0.13.0

Choose a tag to compare

@c4milo c4milo released this 05 Nov 15:22

colibri 0.13.0 lets a QUIC connection resume its congestion state by RFC 9959's Careful Resume, and adds RFC 9002's NewReno as a third congestion controller. It pins stdx 51c91698, which carries stdx's Zstandard encoder, so a program that links colibri can encode zstd with the same stdx. The h3 server spends 15% fewer instructions a request. It pins chapulin 965668d2.

Breaking changes

  • stdx's platform.Cpu gains avx2 and vaes. A program that writes a platform.Cpu literal, for example in a tls.Cpu, must name both. A program that passes the value probe() returns changes nothing.
  • The QUIC structs grow: quic.Connection from 208,304 to 208,456 bytes, and server.Endpoint at its default capacity from 52,433,296 to 52,448,528 bytes. docs/performance.md lists each size.

What's new

  • Careful Resume (RFC 9959, decision 149). A server or a client gives a ResumeProvider with take, put and drop, and a connection starts from the window and round-trip time saved for its peer. It runs under CUBIC, NewReno and BBRv3 (Appendix C). ResumeState is a versioned structure of 32 bytes, and ResumeBounds limits what a connection takes. examples/server_resume.zig shows it.
  • NewReno (RFC 9002 §7, decision 133 as amended) is a third value of quic.CongestionController. BBRv3 stays the default.
  • The bulk run of bench/run.sh measures NewReno, and can keep each colibri connection's qlog.

Planned

These are ruled and documented, not built:

  • Decision 150: serve, which binds, reads its files and runs the loop through a std.Io.
  • Decision 151: JSON in server through stdx's json module.
  • Decision 152: client.Headers and client.Trailers replace WantedHeader, wanted and values, and a client can send trailers. This breaks dependents' code in a later release.

Speed

The judge counted the h3 server's instructions over steps 29d.9 to 29d.12 (design §8):

  • h3-many fell from 42,280 to 35,552 instructions a request.
  • Each step won on h3-many, and no h2 input lost.

Checked

tools/ci.sh passed all 39 sections on a62c4eb9, with Zig 0.16.0 on macOS arm64.

colibri 0.12.0

Choose a tag to compare

@c4milo c4milo released this 04 Nov 03:54

colibri 0.12.0 makes the server prefer the CNSA 2.0 algorithms: ML-KEM-1024, AES-256-GCM and ML-DSA-87 (#115). It gives every server identity one shape, and RSA-PSS returns in that shape. The h3 server spends 38% fewer instructions a request. It pins chapulin 965668d2 and stdx 5fd03aa.

Breaking changes

  • A server identity has one shape for every key (decision 147). tls.Key, tls.Identity, tls.SignatureScheme and tls.pem.key replace tls.P256Key and the names for each algorithm. tls.Server.identities takes a list of tls.Identity, at most one for each scheme. server.StartError adds IdentityRepeated, IdentityUnfilled and IdentityUnavailable.
  • client.ConnectionPool.cancel takes now_ns (decision 146). The instant starts the drop of an h11 response that the caller left.
  • Each TLS session is larger by about 300 bytes: tls.record.Server grows from 49,160 to 49,464 bytes, tls.quic.Server from 100,736 to 101,080, tls.record.Client from 49,800 to 50,104, and tls.quic.Client from 101,304 to 101,608. server.Endpoint at its default capacity grows from 52,422,080 to 52,433,296 bytes. docs/performance.md lists each size.

What's new

  • The server selects ML-KEM-1024 when a client lists it, and sends a HelloRetryRequest for it when the client sent only an X25519MLKEM768 share.
  • The server prefers TLS_AES_256_GCM_SHA384 when a client lists it.
  • The server signs with ML-DSA-87 (draft-ietf-tls-mldsa-05, RFC 9881) when a client lists it, and tls.pem.key reads an ML-DSA-87 key.
  • tls.pem.key reads an RSA key in PKCS #8, and the server signs with rsa_pss_rsae_sha256 when a client offers neither ML-DSA-87 nor ECDSA P-256. chapulin signs once with the key when it loads, so a key that does not match the leaf is refused there.
  • client.Timeouts bounds the drop of an h11 response that the caller left: 256 KiB and 50 ms, and no drop when the response closes the connection (decision 146).
  • tools/tls_openssl.sh checks each choice with OpenSSL's s_client over TCP and QUIC.

Fixes

  • chapulin 965668d2 wipes the stack below each private call of its 64-bit RSA signer and each x86-64 AES-GCM seal and open.

Speed

The judge counted the h3 server's instructions over steps 29d.3 to 29d.8 (design §8):

  • h3-many fell from 67,680 to 42,011 instructions a request.
  • Each step won on h3-many and h3-bbr-many, and no h2 input lost.

Checked

GitHub's main workflow passed on 39b8cd6d (run 41763427588), and tools/ci.sh passed all 39 sections on it.

colibri 0.11.0: BBRv3, PEM files and DNSSEC checks

Choose a tag to compare

@c4milo c4milo released this 01 Nov 14:13

colibri 0.11.0 runs BBRv3 on each QUIC connection by default, and CUBIC stays a choice. It reads the PEM files an operator holds, checks DNSSEC signatures in a new dnssec module, and sends more request content from the program's memory with no copy. It pins chapulin f59c7e3f and stdx 5fd03aa.

Breaking changes

  • Each module's constants are grouped by who sets them (decision 137): protocol for what the RFCs fix, limits for the bounds colibri chose, policy for the other values colibri chose, and one file for each algorithm. quic.constants.frame_ack becomes quic.constants.protocol.frame_ack, and the same holds for every module.
  • A QUIC connection runs BBRv3 (draft-ietf-ccwg-bbr-06) by default, where 0.10.0 ran CUBIC (decision 143). server.Config, client.Config and quic.Connection's options take .congestion_controller = .cubic to keep CUBIC. Under BBRv3 a connection draws its probe waits from the source init takes (decision 136).
  • A program with its own tls_provider.Provider must fill Negotiated.group (decision 140) and give writable_len and encrypt_parts (decision 139).
  • A response whose Content-Length is larger than its body ends too_large at its head (decision 124 as amended). h11 closes the connection, and h2 and h3 reset the stream. Before, colibri read and dropped the whole content.
  • Each session and connection is larger: tls.record.Client grows from 40,968 to 49,800 bytes, quic.Connection from 176,888 to 208,304 bytes, and server.Endpoint at its default capacity from 51,310,368 to 52,422,080 bytes. tls.constants.limits.anchors_max is 256. docs/performance.md lists each size.

What's new

  • quic.CongestionController selects BBRv3 or CUBIC. BBRv3 keeps its model of the path from delivery rate samples, moves through Startup, Drain, ProbeBW and ProbeRTT, and undoes a spurious loss episode. spec/lean proves its arithmetic, and spec/tla/bbr checks its states.
  • tls.pem reads a certificate chain, a P-256 key and a CA bundle from PEM (RFC 7468, decision 141). tls.P256Key holds the key.
  • The new dnssec module checks DNSSEC signatures and DS digests through chapulin: dnssec.verify, dnssec.Hash, dnssec.Algorithm and dnssec.DigestType (decision 142).
  • tls_provider.Negotiated.group and tls.quic.Client.negotiated() report the key-exchange group beside the cipher suite (decision 140).
  • quic.PeerAddress.scope_id carries an IPv6 address's interface, so a pool dials a link-local address (RFC 9000 §9).
  • h2.Connection.write_data_header writes a DATA frame's header for data the caller writes after it (decision 138).

Fixes

  • A cancel during a handshake that then failed left the pool's attempt open, so the next request ended refused with no connection opened (#100).

Speed

The judge's bulk run downloads a file of 2 MiB over h3 through a bottleneck of 20 Mbit/s and 25 ms (design §8 steps 13k and 29c.2):

  • At 1% loss from one connection, BBRv3 gives 14.5 to 14.8 Mbit/s, and CUBIC 1.7. From four connections, BBRv3 gives 17.1 Mbit/s, and CUBIC 8.1.
  • With no loss and four connections, a small GET's p99 beside the download is 186 ms under BBRv3, and 509 to 718 ms under CUBIC.
  • When the two share the bottleneck with no loss, CUBIC takes about 0.82 of it. #123 tracks CUBIC's goodput at 1% loss.
  • The h11 client sends request content from the program's memory, and the h2 client seals each DATA frame from two slices over TLS and writes its data after the header in cleartext (decision 139).

Checked

GitHub's main workflow passed on 83c922b1 (run 41306132768), and tools/ci.sh passed all 38 sections on it.

colibri 0.10.0: CUBIC, HyStart++ and h3 under loss

Choose a tag to compare

@c4milo c4milo released this 30 Oct 10:48

colibri 0.10.0 makes h3 hold up on paths with delay and loss. QUIC now runs CUBIC with HyStart++ in place of NewReno, carries up to 16 STREAM frames in a packet, and holds 100 h3 requests open. The server's router matches a path that ends in "/" with its own route, and h2 indexes a response's field lines in HPACK. It pins chapulin 1f3a99cb and stdx 5fd03aa, as 0.9.0 did.

Breaking changes

  • quic.constants.congestion_loss_reduction_divisor is gone, because CUBIC scales the window to 0.7 on a congestion event, not to half (decision 133). quic.constants.cubic and quic.constants.hystart hold the new constants.
  • server.constants.quic_requests_max is 100, where it was 32 (decision 130). A server's QUIC connection holds more streams, so server.Endpoint at its default capacity grows from 40,987,680 to 51,310,368 bytes, and quic.Connection from 141,912 to 176,888 bytes. docs/performance.md lists each size.

What's new

  • The congestion controller is CUBIC (RFC 9438, decision 133), in integer arithmetic, so one seed still replays on every host. Its first slow start is HyStart++ (RFC 9406, decision 135), which ends slow start when the round trip grows, before a loss does. spec/lean proves the cube root, the target's bounds and the HyStart++ stages, and two TLA+ models check the window and the stages in every order.
  • A packet carries up to 16 STREAM frames (decision 131), so many small responses share one packet. h3-many takes 0.685 of 0.9.0's instructions a request.
  • A server holds 100 h3 requests open and sends MAX_STREAMS each time a stream closes (decision 130). h3 body meters wait on data credit alone.
  • A route whose pattern ends in "/" matches a path that ends in "/" (decision 122 as amended). A parameter does not match the empty last segment, and the wildcard does not match a path that ends in "/".
  • h2 writes a response's field lines with incremental indexing in HPACK, so a repeated line is one byte (decision 132).
  • quic.data_credit_owed says whether a connection waits on the peer's data credit, which the server's body meters read.

Fixes

  • A server dropped every packet of a connection whose client started its first key update before the server had opened any of its 1-RTT packets, until the idle timeout. RFC 9001 §6.1 allows that update once the client confirms the handshake. quinn's client did it after a lost handshake datagram.

Speed

Each figure is colibri's p99 over h3 divided by its p99 over h2 with TLS, on the arm64 judge with tc netem on the loopback device (design §8 step 13i). Below 1, h3 answers the slowest requests sooner. The 0.9.0 tree's figure follows where it differs:

  • far (25 ms each way): 0.55 to 0.64 at 1,000 to 4,000 req/s.
  • lossy (25 ms each way, 1% loss): 0.70 to 0.76, where it was 1.32 to 1.34 at 4,000 req/s. colibri's p99 over h3 is about 108 ms there, and quic-go's chi's about 107 ms.
  • mobile (50 ms each way with jitter, 2% loss): 0.62 to 0.82 at 1,000 and 2,000 req/s, where it was 4.46 to 5.02 at 2,000. At 4,000 no h3 server reaches the rate: colibri reaches about 3,240 req/s with every request answered, and chi about 3,270.
  • h3-many takes 0.685 of the instructions a request it took before 16 STREAM frames shared a packet.

Checked

GitHub's main workflow passed on a23be1b2 (run 41012227601).

colibri 0.9.0: routes, queries, one set of HTTP names, and TLS alerts

Choose a tag to compare

@c4milo c4milo released this 29 Oct 07:51

colibri 0.9.0 gives the server's router a program's functions and middleware, gives a request its query, names each HTTP concept once in the server and the client, and lets a TLS record session send a fatal alert the program names. The client's connection pool opens TCP connections on demand. It pins chapulin 1f3a99cb and stdx 5fd03aa.

Breaking changes

  • One name for each HTTP concept in server and client (decision 126):
    • server.Body is RequestBody, server.Content is ResponseBody, and the client's Request.content is body.
    • Fields, Field and FieldSection are Headers and Header, and fields is headers. Trailers holds headers: Headers, Wanted is WantedHeader, and NeverIndexed.fields is headers.
    • The server's done and cancelled events are one finished event with an outcome, on both sides. RequestId names a request on both sides.
    • Versions is Protocols, Deadlines and Deadline are Timeouts and Timeout, and the close reason deadline is timeout.
    • client.Channel is client.ConnectionPool, and ChannelOf is ConnectionPoolOf. client.Https is HttpsRecord, client.Values is Destination, and set_values is set_destination. client.Options folds into Config: memory_bytes(config) and init(memory, config, destination).
    • Names say bytes, not octets: StreamOctets is StreamBytes. Received is ReceiveResult, and accept(Security) takes tls: bool.
    • Decision 126 holds the whole table.
  • The client's connection pool opens TCP connections on demand and holds its connections and requests in memory the program places (decision 125). Each event and call about a connection names a ConnectionHandle, and a send event names a TCP connection that owes bytes. The counts say requests: Config.requests, constants.requests_max and pool_requests_max.
  • A request's query is apart from its path (decision 127): Request.path holds no query, and Request.query holds the raw query or null. http.QueryParameters reads its name=value pairs, from the WHATWG URL Standard. The router percent-decodes each path segment and takes %. A segment that encodes /, \ or NUL gets not_found.
  • server.router(Context, table) replaces router(Value, table) (decision 127). The table is .{ .error_handler, .middleware, .routes }. A route holds a function, which takes typed path and query structs, or a struct with on_request, on_body, on_writable, on_finished and a State. Router.Of(options) sizes the router's pool, and handle(context, endpoint, event) drives it. New names: server.ResponseWriter, server.Handled and server.middleware.Next. match gives a Route tag.
  • A route table's walk backs up past a leaf that does not serve the method (decision 128), so a table such as /v1/charges/search beside /v1/charges/:charge compiles, and route_hidden is gone. A 405's Allow lists the methods of every leaf the path reaches, so its value can grow. A table that names more than server.constants.route_methods_max (10) methods does not compile.
  • chapulin 50d7d17b answers a KeyUpdate with its next record of data. tls_provider.Content.new_session_ticket and .key_update are .without_data, and h2's and h11's Decrypted.owes_handshake is gone. More than 16 KeyUpdates in a run close the connection: OpenError.KeyUpdates, h2.Limit.key_updates and server.Limit.key_updates are new, and tls.constants.key_update_replies_max is removed. A program with its own tls_provider follows the new contract.
  • stdx's platform.Cpu holds sha256, sha512, sha3, ssse3 and sse4_1, with no default. A program that writes a platform.Cpu literal states them. One that calls platform.probe() changes nothing.

What's new

  • tls.record.Client.fail and tls.record.Server.fail send the fatal alert the program names, for an error it found itself, such as a record longer than RFC 9846 §5.2 allows. They take the error alerts of RFC 9846 §6.2 alone, on a connected session, and fail the session. A refused call returns a tls.record.FailError and leaves the session as it was (#114).
  • Middleware may wrap a body's reader and writer: State, wrap_body, wrap_response and finish_response.
  • Each route names its own parameters, so two routes may name one position differently (decision 129).
  • On arm64, a session runs the SHA instructions where the probe has them and the timing is stated as data-independent.

Fixes

  • chapulin 1f3a99cb sends record_overflow before it decrypts a record longer than RFC 9846 §5.1 or §5.4 allows, or longer than the record_size_limit this side sent (RFC 8449 §4). Each TLS session over TCP grows 8 bytes.
  • A server sends record_size_limit only to a client that offered it (chapulin a6703587, RFC 9846 §4.3). Clients on rustls refused it before.
  • h11's write_response returns FramingInvalid for a bad Content-Length, where it panicked.

Speed

No API change. Against the competitors' run of 2026-10-20, per request or connection, kernel and user together, on the arm64 judge:

  • A TLS handshake costs about half: h11-tls-one, h2-tls-one and h3-one fall 48%, 47% and 59%.
  • h2-many falls 7%, h11-many 11%, and h11-tls-many 20%.
  • The router matches a path in one pass: h2-routes-many, which routes each request over the GitHub API's GET routes, falls 4% (ratio 0.960 in two judge runs).

Checked

GitHub's main workflow passed on bf4fe21f (run 40828900211).

colibri 0.8.0: the server's router, client.Request, h3 deadlines, and the KeyUpdate read fix

Choose a tag to compare

@c4milo c4milo released this 21 Oct 02:20

colibri 0.8.0 gives the server a router and one endpoint for TCP and QUIC, and gives the client client.Request and client.Response. Each module now exports its names from its root alone. It pins chapulin e6e3c1e7 and stdx 5da4397.

Breaking changes

  • Each module exports the names a program calls from its root, and no file (decision 115). A type named after its file has its own name: write h2.Connection for h2.connection.Connection, and the same for http.Field, core.Reader, qpack.Encoder, quic.PeerAddress and the others. A method that only a module's own files call is private.
  • server.Endpoint holds every TCP and QUIC connection, and a program answers each request by its Id (decision 119). server.Config replaces EndpointConfig and takes the TLS identity once. Connection, QuicConnection and QuicConfig leave the root. Endpoint.init returns StartError, with error.DeadlineInvalid for deadlines a connection refuses.
  • versions replaces cleartext on the server and the client, and limits holds the bounds that named h2 (decision 117). A server that allows both versions reads h2's preface in cleartext (RFC 9113 §3.3).
  • server.Limits holds peer_resets_max and resets_sent_max, and server.constants.quic_peer_reset_rate_max is removed. A server refuses a body rate that an honest peer cannot meet over TLS or h2 (decision 110).
  • client.Request and client.Response replace client.HttpExchange (decision 124). client.Config replaces ChannelConfig and takes one tls.Client value. The channel's types lose their prefix: Values, Input, Received, Event, Open and Phase. Connection and QuicConnection leave the root.
  • Channel.start_quic draws a QUIC connection's IDs and grease itself, so QuicStart and QuicStartError leave the root. RequestError names only what Channel.request returns.
  • client.Coding gains zstd and br, so a switch over it must name both.
  • tls.Cpu holds the program's probe and a tls.Timing that the program states (chapulin 0.2.0, decision 97 as amended). tls.quic.version is private.

What's new

  • server.router: literal paths, parameters, a trailing wildcard, and HEAD answered by a route's GET. A table that hides a route does not compile (decision 122).
  • The server runs its deadlines and rate limits over h3 too.
  • The client decodes zstd and br from decoder pools the program places. It sends an idempotent request again once after an unanswered close, and holds a long h11 head over TLS. client.ChannelOf sizes a channel.
  • A tls.Client value can take no tickets, with takes_tickets = false.
  • After a KeyUpdate that asks for an answer, the client and the server read on, and send the answer before their next record of application data (RFC 9846 §4.7.3).

Checked

GitHub's main workflow passed on 7496cdc (run 39183420881).

colibri 0.7.0: QUIC version 2, the program's CPU probe, and the server's deadlines

Choose a tag to compare

@c4milo c4milo released this 30 Sep 17:07

colibri 0.7.0 speaks QUIC version 2 beside version 1, takes the program's probe of its CPU for TLS, and ends slow and silent peers at the server. It pins chapulin 900ce67 and stdx fa53aa4.

Breaking changes

  • tls.Client and tls.Server values take cpu, the platform.Cpu that stdx's platform.probe() returns, with no default. A session runs the AES instructions only when aes_clmul is yes, and ChaCha20 alone otherwise (decision 97). Depend on stdx at the commit colibri pins, with the options colibri gives it (.target and .release), so both reach one platform.Cpu. colibri's package does not export platform.
  • server enforces deadlines: first request, idle, head, body, send, h2's SETTINGS acknowledgment and drain. Each is a named limit in its configuration that a caller may change or turn off (decision 110).

What's new

  • QUIC version 2 (RFC 9369) with compatible version negotiation (RFC 9368). A client lists version 2 and switches when its server answers in it. A server switches every client that lists it, unless its switch_to is null. A client offered a ticket starts in the ticket's version (decisions 108 and 111).
  • h2 ends a connection whose peer opens and resets more than 100 streams a second (CVE-2023-44487).
  • One chapulin object per architecture runs the AES instructions or ChaCha20, as the probe says. ChaCha20 runs four blocks at a time on SSE2 and NEON. chapulin 900ce67 seals a 16 KiB AES-256-GCM record in about 5.5 µs on an Apple M1 Pro, where 044a49c took 27 to 36 µs (chapulin's own figures).
  • bench/: the judge and its workflow (design §8 step 13a).
  • h11: CONNECT and Upgrade fixes.

Checked

tools/ci.sh passed every section on macOS arm64, with 2505 of 2505 tests in Debug and ReleaseSafe.

colibri 0.6.0: AES-256-GCM first on the AES instructions, and a client's own suite order

Choose a tag to compare

@c4milo c4milo released this 29 Sep 20:45

colibri 0.6.0 runs AES-256-GCM first where the CPU has the AES instructions, as a TLS client and as a server, and lets a client choose the suites it offers. It pins chapulin at 10a5bc8, whose default order this is, and stdx at c134f2c. qlog writes each record's tokens many at a call, and the client and the server send a failed record's alert before they close.

Breaking

  • qlog: TextWriter is now qlog's batch writer, which gathers a record's tokens and hands them to stdx's JSON writer many at a call (81707f2). It keeps json.TextWriter's calls, but written() returns an error union, because it writes what is still gathered. The records are the same octets.

To move from 0.5.0, handle the error qlog.TextWriter.written() may return.

Changed

  • tls: on a target with the AES instructions, both roles prefer TLS_AES_256_GCM_SHA384, then TLS_AES_128_GCM_SHA256, then TLS_CHACHA20_POLY1305_SHA256, where 0.5.0 put ChaCha20 first (b3880da, chapulin's decision 80). AES-256-GCM comes first for CNSA 2.0. Against Go's server a client still runs AES-128-GCM, because Go chooses among the AES suites by its own order once the client lists one first. A target without the AES instructions holds ChaCha20 alone, as before. A program that wants another order sets tls.values.Server.cipher_suites, or the new client field below.

Added

  • tls: values.Client.cipher_suites, the suites a client offers, most preferred first, as RFC 9846 Appendix B.4 codepoints. The ClientHello offers exactly this list, and an empty one keeps chapulin's order. An object without AES-GCM refuses a list with SuitesUnavailable, and chapulin refuses a suite named twice when the session starts (b3880da).
  • tls.quic: version, the QUIC version every session and Retry protects packets for: version 1 (RFC 9000 §15).

Dependencies

  • chapulin is pinned at 10a5bc8, where 0.5.0 pinned 0adcf33. It brings the default order above, a client's own order, and a version argument on each QUIC packet call, which colibri fills in.
  • stdx is pinned at c134f2c, where 0.5.0 pinned 8c2a3b8 (a5bfb50). Its DEFLATE encoder, which the server's content codings run, writes smaller output, and runs level 1 at 0.91 and 0.93 of its old speed (stdx's decision 36). Its brotli command loop runs without Zig's runtime checks, and colibri never runs brotli.

Fixed

  • client and server: after a record failed, should_close said to close before send wrote the bad_record_mac alert the connection owes (RFC 9846 §5.2), so a caller that asked before its next send closed without it. Each now waits for the alert (4ed2108). The interop scripts check it with a record no key sealed (628449b, #74).

Checked against

CI ran tools/ci.sh on 830af4c, on Ubuntu 24.04 with an AMD EPYC 7763, and every section passed:

  • TLS against Go 1.27.1: colibri's server selected TLS_AES_256_GCM_SHA384 (suite=0x1302), and its client ran TLS_AES_128_GCM_SHA256 (suite=0x1301), the suite Go's server chose. On 0.5.0 both ran ChaCha20 (0x1303).
  • 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, with gzip content codings in both directions and the scripts' forged-record checks.
  • 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,337 tests in Debug and in ReleaseSafe. The tests also passed on Linux and macOS arm64.

The HTTP Garden and the QUIC Interop Runner ran on b5a7c79, which adds documentation alone to 830af4c. The Garden fed its 72 streams to colibri's server and to 35 other origins, and compared every one. In the runner, colibri passed every case against quic-go, ngtcp2, neqo, quinn and itself, as the server and as the client, but two:

  • chacha20, wherever colibri was the client. The case requires the client to offer ChaCha20 alone, and the test endpoint still offered its object's order, now AES-256-GCM first. 0af377e on main gives the endpoint a chacha20 option.
  • handshakeloss with quinn's client, which also failed at 9796e95 and passed at bb9263e.

The runner ran again at 0af377e, main with that fix and the QUIC version 2 work that follows this release, and colibri passed every case, chacha20 and quinn's handshakeloss included.

quic-go runs 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.6.0