8.0 KiB
RustProxy 0.27.3 implementation note
Scope
0.27.3 aligns HTTP/3 HEAD cache handling with HTTP/2, reduces long-lived cache/rate-limit memory, excludes the rebuildable managed Python runtime from backups, promotes package repositories as the normal installation path, and includes maintenance fixes for restore ownership, startup listener overrides, strict WebSocket 101 framing, resilient update checks, scaled sidebar layout and automatic GeoIP maintenance. SQLite schema 8 and the COLD cache on-disk format remain unchanged; existing saved configurations remain compatible because the new geoip_updates object is defaulted when absent.
HTTP/3 HEAD cache parity
HTTP semantics do not make HEAD meaningfully different between HTTP/2 and HTTP/3. RustProxy already allows GET and HEAD in cache policy, but the forwarding path actively probed an initially unknown request-body stream only for GET.
The HTTP/3 receive body starts with an unknown end-of-stream state until it is polled. A body-less HEAD could therefore reach cache policy as body_empty=false and be bypassed with request_body. HTTP/2 happened to expose end-of-stream immediately for the same curl request.
0.27.3 runs the existing bounded empty_for_cache probe for both GET and HEAD. A genuinely body-bearing request is still bypassed, while a body-less H3 HEAD follows the same cache lookup path as H2.
COLD cache RAM
The COLD tier stores bodies on disk, but its old in-memory index retained a complete response HeaderMap, origin validators, a duplicate 32-byte cache key and a 64-character SHA-256 string for every object. With many small COLD objects, these metadata allocations could become a material RSS component even though body bytes were no longer resident.
0.27.3 keeps the fields needed for matching, invalidation, dashboard rows, LRU and freshness in the COLD index, but removes the persistent header/validator copies. On a real COLD read RustProxy reads and validates the existing metadata JSON, materializes headers/validators for that request, verifies the body digest and then promotes/serves the object as before. The in-memory digest is stored as 32 raw bytes and the map-owned cache key is no longer duplicated inside ColdEntry.
This trades one metadata-file read on a COLD hit for substantially lower long-lived metadata RAM. HOT behavior, COLD disk format, invalidation fields and response semantics are unchanged.
Rate-limit/IP retention
The previous limiter keyed every client bucket by (revision, vhost String, IP), duplicating the vhost allocation for each IP. Expired buckets were normally retained until the global 65,536-entry bound caused cleanup.
0.27.3 groups client-IP buckets below a single vhost key, stores the active revision once and removes buckets whose fixed window has expired. Cleanup runs opportunistically during requests and from the existing periodic runtime-maintenance loop, so idle traffic no longer leaves expired IP state resident indefinitely. Sparse client/vhost hash tables are shrunk after reclamation and the allocator trim follows the periodic sweep. Removing an expired bucket is behavior-equivalent to the existing next-request reset: the next request starts a new fixed window with count zero. The global client-bucket bound remains 65,536.
Compression RAM assessment
Downstream compression remains streaming through tower_http::compression::Compression. It does not collect the complete response body before encoding, and CompressionLevel::Fastest remains selected. The long-lived cache does not keep encoder state. Compression memory therefore scales primarily with the number and codec type of concurrently active compressed responses rather than the number of cached objects or historical requests.
No compression algorithm or negotiation behavior is changed in 0.27.3 because the existing implementation is already bounded by streaming response lifetime. Brotli and Zstandard can have larger per-stream codec state than gzip/deflate, so concurrency remains the main compression-RAM factor under high load.
Backup scope
acme-python is a managed/rebuildable Python virtual environment, not authoritative application state. Backups now skip both acme-python and the underscore spelling acme_python, while certificate/configuration/secret state remains covered by the existing backup logic.
Installation paths
The documented normal installation/update paths are:
- DEB/RPM packages:
https://rustproxy.linuxiarz.pl/ - OPNsense plugin:
https://rustproxy-opn.linuxiarz.pl/
The Debian 13+ source bootstrap remains available as an optional source/development path.
Managed ACME bootstrap diagnostics
The managed ACME runtime now distinguishes private-environment creation, pip preparation, Certbot package installation and final plugin validation failures instead of collapsing all of them into one generic setup error. The selected stage is also emitted as a warning when no distribution Certbot fallback is available.
On distributions enforcing PEP 668, the application-local virtualenv bootstrap first uses normal pip --target; if the distribution rejects that operation, RustProxy retries with --break-system-packages while still writing exclusively into the private bootstrap directory under the RustProxy data tree. This does not install or modify packages in the distribution Python environment.
Restore ownership and production data directory
The production CLI default is /var/lib/rustproxy; dev.sh explicitly sets RUST_PROXY_DATA_DIR to repository-local ./data. During restore, RustProxy records the UID/GID of an existing target data directory and recursively applies that ownership to the validated staging tree before the atomic rename. This prevents a root-run restore from converting service data to root:root; symlinks are ownership-adjusted with lchown and are not followed.
GeoIP background maintenance
Config.geoip_updates defaults to enabled with the sapics latest/checksum release URLs, 256 MiB maximum file size and a 24-hour interval. The worker starts after successful listener binding, attempts immediately, compares publisher SHA-256 files with local files and downloads only stale assets. Database payloads are streamed to private staging files with a transfer limit and incremental SHA-256, the staged pair is parsed before publication, and the in-memory GeoIP database is atomically replaced without a proxy restart. Failures are fail-soft and retry later. The worker and managed manual updater share .geoip-update.lock; that runtime lock is excluded from backups.
Listener, WebSocket and control-plane startup fixes
Explicit CLI/systemd HTTP/HTTPS binds now replace the corresponding canonical saved http/https bind rather than appending a second listener that can leave :80/:443 active. Strict WebSocket response validation accepts 101 with no Content-Length or exactly Content-Length: 0, while rejecting positive lengths and any Transfer-Encoding. The release-version checker starts only after all sockets bind and logs polling failures as warnings, keeping network/DNS availability outside the proxy startup success path.
Scaled administration sidebar
The desktop shell/sidebar height compensates for CSS zoom via 100vh / --ui-scale. At scales below 100%, the flex sidebar therefore still spans the visual viewport and its bottom runtime information remains at the bottom edge. Mobile rules continue to use automatic height.
Source-tool separation
0.27.3 also separates application-coupled scripts from auxiliary tooling without changing proxy behavior. scripts/runtime, scripts/build, scripts/deploy and scripts/migration contain code directly tied to application/build/deployment workflows. Compiler-independent checks, mocks, audit probes and the retired Python backup helper moved to tools/. OPNsense and RouterOS helpers moved to integrations/.
The shell entry points, build.rs, embedded ACME source path, source upgrader, deterministic source archive and release checks were updated together so the relocation is part of the same 0.27.3 development release.