v0.10.0
The release candidate for v1.0.0. A full-tree audit of v0.9.3 (five directions plus a three-way simplify pass) was applied in full, the residue was ruled on, and a handful of ideas from another Rust QUIC client were taken in. v1.0.0 is tagged from this same tree after a week on both hosts.
Added
- From v1.0.0, configuration keys, their defaults and the command line are stable within 1.x. A key is removed only after a deprecation warning in at least one minor release. Log line shapes and the library API are stated as outside the promise. The statement lives in
docs/configuration.mdunder "Version compatibility". volto --diagnosticsprints a support bundle an operator can paste into an issue: version, the parsed configuration with every password redacted, the effective limits and security settings, the startup warnings, the file-descriptor limits, the fournet.coreUDP buffer sysctls on Linux, and the kernel. It binds nothing and logs nothing.- Every production log line carries a permanent
log_id, eight characters that survive a rewording, so a runbook grep and the accounted set of log lines are keyed by identity rather than text. - The proxy's own addresses are not tunnel targets. RFC 9298 section 7 names "the UDP proxy's own addresses" beside localhost, link-local, multicast and broadcast, and the fifth class was reachable: a credentialed client could open a tunnel to this listener and nest a handshake inside it, one connection slot and one tunnel slot per level. An address this host holds now draws the same 403 with
destination_ip_prohibitedas a private one, judged by asking the kernel rather than by a list, onebindper allowed address per tunnel opened. Loopback stays underallow_private_networks, and behind a relay the relay's address is not one this host holds; both are stated in the documentation.
Fixed
- The authentication failure limit counted requests, not guesses. A request could carry about a thousand
Proxy-Authorizationvalues inside one field section and every one was tried, so a single handshake bought thousands of guesses against a documented five. A request carrying more than two credential values (one per header name, which is all the "accept both names" rule needs) is refused with 400, and every value tried is one failure. - The unanswered-packet budget is a connection's total, not a session's. Opening a new session to the same silent target restored the RFC 9298 section 7 budget, so the mitigation was a constant factor rather than a bound. A second counter on the connection, eight times the per-session budget, is spent at the same point; a session whose target answers gives back what that session spent, and nothing else does; a session that finds the total spent is reset with H3_REQUEST_CANCELLED rather than muted. The refund was added when the release candidate's own stress test found that without it a long-lived connection of short answered sessions, which is what DNS through the tunnel is, was reset by session count alone.
- Process exit is bounded by
shutdown_grace. The runtime was dropped implicitly, and tokio waits without limit for a blocking name lookup that has started, so a SIGTERM waited for the slowest in-flight lookup, which a client chooses, until systemd's SIGKILL. The runtime is now stopped with a deadline of the grace plus the close flush plus one second of slack. - A reload and a drain, or a reload and an accept, can no longer tear. One lock covers the shutdown latch check and the three configuration writes, so a SIGTERM racing a SIGHUP cannot reopen the listener the drain closed; the live configuration is written before the endpoint's; and a connection is accepted and snapshotted under the same lock, so it runs wholly on the old configuration or wholly on the new one.
- A near-64 KiB HEADERS or SETTINGS frame no longer retains its allocation for the life of the stream. The frame decoder handed out its payload with
split, which kept the backing block; per tunnel and per connection that was up to 64 KiB outside every stated budget, about 16 MiB per connection at the defaults. The payload now leaves with its allocation. - Refusing an oversized field section stops the receiving side before answering 431, as the other three refusal writers already did; a windowless peer could otherwise park the write for one idle timeout while unread stream data was held.
- A
:protocolvalue that is not an RFC 9110 token is a malformed request (H3_MESSAGE_ERROR) rather than a 501, and a port is digits only on both routes (+443is refused, RFC 3986port = *DIGIT). - Closing a peer's QPACK stream is H3_CLOSED_CRITICAL_STREAM, the same construction the control stream uses, with the same exemption for a close the peer announced.
- The trailing-dot rule for host names was written three times and one copy differed.
expected_sni = ["proxy.example.."]passed validation, was held in a form no ClientHello matches, and drew no warning. One rule now, and an empty label is refused at startup and on reload. - The read side of a UDP session grades a socket error like the send side, forgiving the per-packet errors a Linux probe showed a
recvcan report (EMSGSIZE,ENOBUFS,EPERM,EACCES) instead of ending the session. - The datagram receive buffer and the CRYPTO buffer are stated in the transport configuration at quinn's current values rather than inherited, so a quinn bump that moves either is reported by a test.
Changed
- The SNI gate's hot path copies the header and its sample rather than the whole packet, borrows a single CRYPTO fragment rather than copying it, borrows the name out of the ClientHello, refuses a packet too short for a packet number before deriving Initial keys, and with an empty list skips the lock entirely. No verdict moves; the shape matrix, the fuzz target, both interop halves and the local gate probe say so.
- Numeric
ascasts are denied crate-wide. Twenty-eight becameFromortry_from; the forty-two that index a table by a value already proven in range carry an argued allow each. - Release binaries are stripped by the build profile rather than by the release workflow, whose strip step turned out to be a no-op for the cross-built aarch64 asset: the v0.9.3 aarch64 archive still carried its symbol tables. Both archives are now stripped the same way.
- The rest of the simplify pass: the tunnel context holds its members without an inner
Arceach, the two tunnel entry points take a stream and a borrowed context, the resolve failure joins the status vocabulary, one place decides what a lapsed response leaves behind, and the three bind-time failures say which step failed.
Testing
- Tests prove the reset on Linux. The abortive close of a target after a client reset was proven on Linux in Docker, where the kernel does not send RST from FIN_WAIT_2 and macOS does.
- Targeted mutation over every function this release added or changed: 67 mutants, 58 caught, 8 unviable, the one survivor pinned. The one gap it found, that a lapsed 200 did not stop a UDP session from starting, has a test.
- Compile-time assertions between constants, the ledgers re-keyed, five gates that read the tree share one reader, three-second sleeps replaced by condition waits, a probe that asserted nothing deleted, a documentation completeness gate over
docs/, and a fourth macro spelling in the accounted-log-lines scanner.
Dependencies
- quinn-proto stays pinned to the upstream 0.11.x branch head
48455d3under[patch.crates-io], because no 0.11.x release carries the MTU and datagram fixes yet. v1.0.0 will ship with the same pin unless 0.11.18 appears first.
Upgrading: nothing to do. What changed on the wire is on refusal paths only: a request carrying more than two credential values, a :protocol that is not a token, a signed port, a target that is this host's own address, a connection that has spent its unanswered-packet budget, and a peer that closes its QPACK stream are refused where they were served or ignored before. A client that does none of these sees no difference.
Static musl builds for x86_64 and aarch64. Verify with SHA256SUMS.
Each archive carries the binary, LICENSE, README.md and the script/ and docs/ directories. Installing and updating is described in docs/deployment.md.
Full Changelog: v0.9.3...v0.10.0