Files
RustProxy/docs/V0.24.5_AUDIT.md
T
2026-10-03 00:27:09 +01:00

6.6 KiB

RustProxy 0.24.5 specification audit

Scope

0.24.5 fixes HTTP/3 request-body framing when an H3 request has no declared Content-Length, carries no DATA, and is forwarded to an HTTP/1.1 upstream. Before the fix, the H3 receive body remains length-unknown until its stream end is polled; Hyper can therefore serialize body-capable HTTP/1.1 methods such as DELETE with Transfer-Encoding: chunked. Some upstream servers reject chunked request bodies even when the logical body is empty.

Source/spec matrix

Requirement Source contract Status
Preserve HTTP method and URI normalization changes only the request body; method/URI rewriting remains in the existing proxy path implemented in source
Detect an actually empty H3 body for H3 -> H1 with no declared/exact length, normalize_h3_h1_unknown_body() polls until EOF or the first semantically significant frame implemented in source
Represent empty H3 body as zero length EOF replaces the streaming wrapper with Body::empty(), giving Hyper an exact zero-length size hint implemented in source
Preserve non-empty bodies the first non-empty DATA/trailer frame is wrapped in ReplayBody; the remaining tail stays streaming implemented in source
Do not change H2 upstream framing normalization exits before polling unless the selected upstream version is HTTP/1.1 implemented in source
Do not preconsume declared bodies requests carrying Content-Length are left unchanged implemented in source
Avoid DELETE-only/Proxmox-only behavior normalization applies generically to body-capable H3 methods forwarded to H1; GET/HEAD/CONNECT retain Hyper's existing implicit-empty behavior implemented in source

Regression coverage added

src/proxy/mod.rs contains focused async tests for:

  • empty length-unknown H3 POST, PUT, PATCH and DELETE to HTTP/1.1 becoming exact zero-length bodies;
  • non-empty length-unknown H3 DELETE preserving its payload through ReplayBody;
  • H3 request bodies remaining untouched for HTTP/2 upstreams;
  • H1/H2 downstream request bodies remaining untouched by the H3-specific normalization;
  • declared-length H3 DELETE bodies not being preconsumed.

Runtime acceptance

Compiler/type/runtime acceptance remains open in this source workspace because the Rust toolchain is unavailable. On the test host, run ./dev.sh check and ./dev.sh test, then verify the full H1/H2/H3 method matrix. For the original Proxmox case, compare /var/log/pveproxy/access.log and a raw/mock HTTP/1.1 upstream. The H3 bodyless DELETE must arrive as DELETE with the same URI and without chunked request framing; a zero-length representation such as Content-Length: 0 is expected for HTTP/1.1.

Cache RAM hardening included in the same 0.24.5 source release

The cache keeps the existing HOT RAM -> COLD disk lifecycle. The changes are admission/accounting safeguards rather than a COLD format redesign:

  • cache.max_bytes is a hard published-HOT charge based on the body backing allocation retained by Bytes, directly owned entry metadata and a conservative charge for HOT hash/LRU/variant indexes. hot_logical_bytes remains the visible payload total; hot_allocated_bytes/resident_bytes are the charged allocation view, not a duplicate RSS measurement.
  • response fill uses a Vec<u8> whose actual capacity() is reserved before growth and transferred zero-copy into Bytes; max_object_bytes is checked before every chunk append;
  • active fills reserve from transient_max_bytes and compare HOT + active fill against HOT capacity plus a small allocation-rounding headroom (about 1/64 HOT, capped at 1 MiB and further bounded for small caches); LRU entries are removed before further fill allocation where possible;
  • one admission can remove at most hot_eviction_max_objects; if sufficient space cannot be made safely, the response continues uncached;
  • ordinary HOT -> COLD demotion removes the entry from HOT and moves the same Entry into ColdDraft. The payload is not cloned. A transient lease follows the draft until persistence completes; the persisted ColdEntry has paths/metadata and no Bytes body;
  • the COLD writer queue remains count-bounded and is additionally capped by cold_queue_max_bytes; COLD reads/promotions and revalidation reads reserve transient bytes before reading full bodies;
  • global cache.enabled=false cannot be overridden by a vhost enabled=true;
  • HOT/object settings no longer have the old artificial 1 GiB UI/source ceiling. Safety comes from the configured byte budgets rather than an arbitrary maximum.

Default memory values are 256 MiB HOT, 16 MiB maximum object, 64 MiB transient and 32 MiB COLD queue, with 4096 HOT entries and a 64-object LRU admission bound. The administration panel can calculate conservative/recommended/performance settings from the lower of detected host RAM and a finite cgroup memory maximum. For example, the recommended profile on an effective 2 GiB VM selects approximately 400 MiB HOT, 96 MiB transient, 16 MiB maximum object, 48 MiB COLD queue, 6400 entries and a 64-object LRU admission limit. Presets are disabled below 256 MiB effective RAM. The panel also checks the configured budgets against current host/cgroup memory headroom and warns without blocking manual configuration. COLD disk capacity is intentionally not changed by RAM presets.

Monitoring exposes HOT objects, HOT logical bytes, HOT allocated bytes, HOT limit, transient current/limit, COLD queue current/limit, COLD read/write activity, evicted/expired/rejected counts and existing process RSS/RssAnon/VmData/VmSize diagnostics. Cache RAM gauges and process RSS are overlapping views and must not be summed.

Cache runtime acceptance still required

The source-level/offline gates verify configuration, UI/API wiring and static contracts. Because this workspace has no Rust toolchain, compile/type/runtime acceptance must still be performed on the test host. In addition to ./dev.sh check and ./dev.sh test, exercise at least:

  • HOT 8 MiB under sustained traffic and verify HOT allocated stays bounded and RSS plateaus;
  • HOT 2 GiB with a deliberately high preset and verify admission remains bounded;
  • cold_after_seconds=5, verify HOT allocated falls after demotion and transient/COLD queue bytes return to zero after writes drain;
  • an object larger than max_object_bytes, verify the client receives the full streamed response while the cache fill aborts before full buffering;
  • concurrent COLD hits/writes under a small transient limit, verify memory-pressure rejection/pass-through rather than unbounded RAM growth;
  • all four global/vhost enabled combinations, especially global OFF + vhost ON => effective OFF.