Download Latest Version 2.2.1 source code.zip (2.3 MB)
Email in envelope

Get an email when there's a new version of Sōzu

Home / 2.2.1
Name Modified Size InfoDownloads / Week
Parent folder
SHA256SUMS 2026-08-28 1.2 kB
SHA256SUMS.pem 2026-08-28 3.2 kB
SHA256SUMS.sig 2026-08-28 96 Bytes
sozu-2.2.1-aarch64-unknown-linux-gnu-crypto-aws-lc-rs.tar.gz 2026-08-28 5.6 MB
sozu-2.2.1-aarch64-unknown-linux-gnu-crypto-openssl.tar.gz 2026-08-28 4.7 MB
sozu-2.2.1-aarch64-unknown-linux-gnu-crypto-ring.tar.gz 2026-08-28 5.0 MB
sozu-2.2.1-aarch64-unknown-linux-gnu-fips.tar.gz 2026-08-28 5.7 MB
sozu-2.2.1-x86_64-unknown-linux-gnu-crypto-aws-lc-rs.tar.gz 2026-08-28 6.1 MB
sozu-2.2.1-x86_64-unknown-linux-gnu-crypto-openssl.tar.gz 2026-08-28 5.2 MB
sozu-2.2.1-x86_64-unknown-linux-gnu-crypto-ring.tar.gz 2026-08-28 5.5 MB
sozu-2.2.1-x86_64-unknown-linux-gnu-fips.tar.gz 2026-08-28 6.3 MB
sozu-2.2.1-x86_64-unknown-linux-musl-crypto-aws-lc-rs.tar.gz 2026-08-28 6.2 MB
sozu-2.2.1-x86_64-unknown-linux-musl-crypto-ring.tar.gz 2026-08-28 5.5 MB
2.2.1 source code.tar.gz 2026-08-28 2.1 MB
2.2.1 source code.zip 2026-08-28 2.3 MB
README.md 2026-08-28 9.9 kB
Totals: 16 Items   60.1 MB 0

Patch release: the main process validates listeners and HTTP/HTTPS frontends before committing them to its state and rolls back unanimously rejected or unacknowledged fan-outs, the router rejects malformed frontend hostnames instead of panicking workers, and a full dependency refresh re-enables TLS 1.2 + ECDSA under crypto-openssl.

🔐 Security

  • fix(command): redact TLS certificate and private-key material from Debug output. Certificate payloads, runtime certified keys and resolvers, HTTP(S) listeners and listener patches, HTTP frontend configuration, top-level Request/RequestType values, retained command tasks, and generated frontend requests now use bounded Debug summaries without exposing PEM contents, certificate DER, custom answer bodies, header values, string-valued command payloads, or other raw fields carried by those types through nested requests, retained state, or log statements that use Debug formatting. Direct log and failure projections now also replace certificate names, HTTP frontend keys, route-miss hosts/paths/custom methods, rewrites, cluster IDs, custom-answer keys, runtime SNI/ALPN/authority/certificate-SAN values, certificate fingerprints, and certificate-query domains and results with bounded addresses, kinds, counts, and byte lengths. StateError is bounded at its Display/Debug boundary before worker and audit sinks, and retained-task logs no longer delegate to concrete task payloads. Wire, Serde, JSON, raw retained state keys, raw StateError, RouterError, ListenerError, and RetrieveClusterError::SniAuthorityMismatch variant fields, RequestHttpFrontend::Display, and certificate-query response payloads remain unchanged.

🐛 Fixed

  • fix(router): reject a malformed frontend hostname instead of panicking the worker. TrieNode::insert asserted that its recursive insert never reports InsertResult::Failed, but the route-table grammar rejects a whole class of hostnames the two cheap guards above the assert let through: a host ending in / with no openable regex segment (example.com/), a regex segment that is not .-anchored (abc/[0-9]+/.example.com), a segment that is not a valid regex (/[/.example.com), and an empty label (.example.com). Those hostnames arrive from the control plane, so an AddHttpFrontend carrying one panicked every worker the main process fanned the request out to — and panicked them again on each restart state replay. add_tree_rule now surfaces the rejection as RouterError::AddRoute, so the worker answers Failure and stays up. Three sibling holes in the same input class are closed with it: convert_regex_domain_rule indexed one byte past the end of a hostname whose last segment is followed by a bare trailing . (/a/.) — a release panic reached through the unconditional DomainRule parse, upstream of the trie; hostnames are now bounded to MAX_HOSTNAME_LENGTH (4096 bytes) before any parse, since the trie recurses once per label (a ~100k-label hostname aborted the worker with an uncatchable stack overflow) and a /-segment compiles a control-plane-supplied regex whose compilation time grows with the pattern; and CertificateResolver::add_certificate now validates every certificate name against the same trie grammar up front — with the insert no longer panicking, a discarded InsertResult::Failed would have registered the certificate while the SNI trie never learned the name, silently failing every handshake for it. With workers answering Failure instead of dying, the unanimous-rejection rollback from #1301 also becomes reachable for this class on the live fan-out path, so the main process's ConfigState is reverted and no phantom route persists. Two replay holes remain open and are tracked as follow-ups: the LoadState path has no rollback at all (a poisoned saved state re-injects the entry and SaveState re-persists it), and the rollback is skipped when the scatter times out — a single unpatched worker panicking during a rolling upgrade commits the malformed entry permanently.

  • fix(command): validate a listener configuration before committing it to the main-process state (#1301). An HTTPS listener whose configuration the worker cannot build (an unusable TLS version/cipher set, or an unparseable answer template) was recorded in the main process's ConfigState before the worker rejected it, reserving the address; a corrected reload was then refused with StateError::Exists and never reached the workers, so the listener stayed down until an explicit RemoveListener or a restart. The main process now validates every Add{Http,Https,Tcp,Udp}Listener the way the worker builds it — reusing the worker's own construction check, before ConfigState is mutated and fanned out — so an invalid listener never reserves its address and a corrected reload applies cleanly. Invalid listeners in the static config or a loaded state file are likewise skipped without reserving their address. The StateError::Exists message now also points at the remedy (remove it first, or apply the corresponding update, instead of re-adding). As defense-in-depth, when a committed change is rejected by every worker it was fanned out to, the main process now reverts its own ConfigState with the inverse request (listener and HTTP/HTTPS-frontend adds), so its authoritative state — the one replayed into restarted and upgraded workers — never permanently holds an add the whole fleet refused.

  • fix(command): validate HTTP/HTTPS frontends before committing them to the main-process state (#1313). ConfigState has no route-grammar check — add_http_frontend only reparses the address and maps the position — so an Add{Http,Https}Frontend whose hostname every worker's router refuses was still recorded by the main process. That state is authoritative: SaveState re-serialised the malformed entry and every replay re-injected it, and the replay path (LoadState, static-config load) has no rollback at all, so the entry survived a unanimous worker rejection and poisoned each subsequent restart. The main process now runs the worker's own insertion path — the same Router::add_http_front, against a disposable empty router — before ConfigState is mutated, on all three apply paths: a live Add{Http,Https}Frontend is answered with a failure and never fanned out, and a malformed entry in a saved state file or the static configuration is skipped without entering the state, so it can never be re-persisted. Skipped entries (frontends and, as before, unbuildable listeners) are now reported at warn! instead of debug! — an entry silently dropped from the state the operator saved must be visible at the default log level — and a state load that skipped any entry says how many. Rejection messages report byte lengths only, never the operator-supplied hostname.

  • fix(command): roll back a fanned-out change when the scatter times out with zero worker acknowledgements (#1314). The unanimous- rejection rollback from #1301 required at least one worker to answer Failure, so it was skipped whenever the fan-out timed out instead — and a worker that panics answers nothing at all: it emits no synthetic Failure and no expected-response decrement, so the task can only end through its timeout. One unpatched worker panicking on a malformed frontend during a rolling upgrade therefore left the malformed entry committed in the main process's ConfigState forever, with SaveState re-persisting it, even though the operator was already told the command had failed. The revert now fires on either trigger — every scattered worker rejected the change, or the scatter timed out — as long as no worker acknowledged it; an entry at least one worker applied is still never reverted. Residual: a slow-but-healthy worker whose Ok arrives after the deadline is invisible to the finished task, so it keeps the frontend until its next restart or state replay while the main process reverts — a bounded divergence that self-heals on replay and matches the failure already reported to the client.

  • fix(command): report a fan-out timeout as a failure, not a success. handle_finishing_task passed a hard-coded timed_out = false into every task completion handler, so a command whose worker fan-out timed out was reported to the operator as Successfully applied request to all workers and its audit line mislabeled the FanoutStatus/result. The real timed_out flag is now forwarded, so a timed-out command correctly returns a failure (and the #1301 rollback safety-net's timeout guard now engages as intended).

🔄 Changed

  • chore: refresh every workspace dependency requirement to its latest published version (#1317). Cargo.toml requirements move to explicit ^x.y.z form: async-trait ^0.1.92, flate2 ^1.1.10, futures ^0.3.34, http-body-util ^0.1.5, log ^0.4.34, rustls-openssl ^0.4.0, and tui-big-text ^0.8.9, with both lockfiles regenerated (cargo audit reports no advisory; the transitive h2 0.4.19 closing RUSTSEC-2026-0258 had already landed on main). rustls-openssl 0.4.0 fixes the TLS 1.2 + ECDSA handshake regression that 0.3.x carried, so test_tls_1_2_ecdsa runs under crypto-openssl again and that CI cell is back to full parity with the other providers.
Source: README.md, updated 2026-08-28