From 5910e6e9018d6a690244fd299e130020c08a93d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 18 Oct 2026 07:00:34 +0200 Subject: [PATCH] v0.34.0_4 --- CHANGELOG.md | 14 ++++ README.md | 9 +++ SHA256SUMS | 38 ++++----- docs/PROXY_1KB_COUNTERS_DEV_0.34.0.md | 15 ++++ docs/PROXY_1KB_OPT3_DEV_0.34.0.md | 88 +++++++++++++++++++++ scripts/performance/README.md | 7 ++ scripts/tests/proxy_1kb_opt3_static_test.py | 37 +++++++++ src/acl.rs | 26 ++++++ src/acl/tests.rs | 17 ++++ src/cache/dashboard.rs | 6 +- src/cache/stats.rs | 4 + src/forwarding.rs | 33 ++++++-- src/forwarding/tests.rs | 29 +++++++ src/proxy/cache_helpers.rs | 4 +- src/proxy/forward/cache_lookup.rs | 6 +- src/proxy/forward/success_finish.rs | 2 +- src/proxy/forward/web_source.rs | 2 +- src/proxy/forward_entry.rs | 19 ++++- src/proxy/headers.rs | 30 ++++--- src/proxy/headers/tests.rs | 30 +++++++ src/proxy/request_id.rs | 12 +-- 21 files changed, 377 insertions(+), 51 deletions(-) create mode 100644 docs/PROXY_1KB_OPT3_DEV_0.34.0.md create mode 100755 scripts/tests/proxy_1kb_opt3_static_test.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2794e19..4d6a6d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ ## 0.34.0 +- DEV 1 KiB throughput follow-up (OPT3): keep normalized safe routing + paths borrowed from an independently retained URI instead of allocating + during observation and forwarding; use the original strict percent/decode + validator for noncanonical paths. Build untrusted-peer Forwarded headers + without intermediate per-hop vectors, avoid rewriting an unchanged client + address into per-request log state, fast-path plain `Connection: keep-alive`, + encode request-ID hex in a single buffer and update the cache-disabled + reason counter directly. ACL, trusted-proxy validation, IPv6 formatting, + body streaming, analytics, metrics and defaults remain unchanged. +- Add regressions for path validator equivalence, multi-valued hop-by-hop + fields and untrusted IPv6 forwarding. User-supplied OPT2-FIX1 binary passed + 89 targeted live checks. **This newest source has not been compiled or + RPS-tested**; see `docs/PROXY_1KB_OPT3_DEV_0.34.0.md`. + - DEV 1 KiB accounting follow-up: move per-domain HIT/MISS/BYPASS, reason, stored-entry and saved-body statistics outside the HOT cache's shared mutex. Publish bounded domain/reason indexes and update counters atomically while diff --git a/README.md b/README.md index 8a009d3..a3fda30 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,14 @@ # RustProxy 0.34.0 +## DEV: small-response proxy hot path, OPT3 source candidate + +Seven compatible optimizations reduce path copies and work constructing +forwarded headers, log IPs, hop-by-hop headers, request IDs and cache-off +reason totals. HTTP/1.1 proxy with a 1,040-byte response remains the first +benchmark priority; local Web Server is secondary. No version or policy changes. +The **new code is not compiled or RPS-verified** here. See +[`docs/PROXY_1KB_OPT3_DEV_0.34.0.md`](docs/PROXY_1KB_OPT3_DEV_0.34.0.md). + ## DEV: cache counter contention and saturated analytics (source candidate) Per-domain cache outcome/reason accounting now uses bounded immutable indexes diff --git a/SHA256SUMS b/SHA256SUMS index f70ce15..dc449e9 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -2,11 +2,11 @@ 4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example 9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore 46bab1d2c2d6e0a80d57a6b232b1ba4c220884b95cd45ddccbafce2aa160cddb AGENTS.md -03d0a320df729cc98a119f4296e3b9805aece0c8591fa6b90003df2b58f8608c CHANGELOG.md +10f70748d2c070e2b9923b5aa6572347c7977a88f8bb5d4abe532827914bade7 CHANGELOG.md ca201e3cd3f9842eae0006d0b40a0a72bcd3d4da6e4e5ecaf200d1b9a1390db2 Cargo.lock fec5f1bae9a59ef1fabd27c5e45d31448dd8f811736eebfa8826b3806a7e3349 Cargo.toml 25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile -5533499655d41f1faaf666dfa630375cf6770dff37f6cd7ef5a4e5bbd58cb258 README.md +09234b68d69eb10047396baab4bf5811b37e954ad3f5eaacf90247265d4fbd52 README.md aee9bbb367e061aeb79916e38d121e7767f6f9d0c16a2dcc4b28f091586a33b1 ROADMAP.md da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs @@ -56,8 +56,9 @@ be53a58afbeddbd7deb4b5f6447f2dc74db0327839bd83407c20915df98dcfa0 docs/NOT_FOUND e68d5a5cf0991de9308f78afb0264e02979f7463a5f395eb7a6e9ec2bd3a3d54 docs/PHP_FPM.md bc068a176056f5ea4d3b637071e644ed58a2140494c233ef22aa66e9700e3299 docs/PHP_FPM_ISOLATION.md ad6e3f003b820a4f45e30e39577d120a69d167c70910bda4891971759458c3dd docs/PROMETHEUS.md -9a19e57e30b2d2818322fcd5b490380281014e2ed43279c2251f09acbfd23130 docs/PROXY_1KB_COUNTERS_DEV_0.34.0.md +401ab1bb2c7f3961742195d81f5daf5c9cf81609a4b175bea1d10d2a4b727082 docs/PROXY_1KB_COUNTERS_DEV_0.34.0.md f13ee0005d57d2576ccc970745f17069096df4afa23d712ea0f748da85433f7d docs/PROXY_1KB_HOTPATH_DEV_0.34.0.md +c9bbdd1cd8df02b2a19016b7bbeee1cf55917614ac672f1f94e0a05db7d309ef docs/PROXY_1KB_OPT3_DEV_0.34.0.md deea89aed410c0619bdbdb12437edc7aff9dca2fec7fdc059e918c649fc36b51 docs/PROXY_HOTPATH_0.34.0.md a7d6189b0e8cb854918456e0063101ee77bfb2419a9ba0430eb0e3f8678c0f09 docs/PROXY_RPS_0.34.0.md 887d1fdcc3b546b024afcd18b1716b8e3a74a3800012bf4bda7a122d2eabe233 docs/README.md @@ -133,7 +134,7 @@ a641bd35400c56f3b68b0a92e97b602d06090f03972ca8f01fa744e90dd97403 scripts/deploy 2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migration/migrate_npm.py a459d95bbb0076f33ade8b273b813308167f9d8d45242b309a2d18320ca0093c scripts/migration/migrate_npm_api.py 31ccad3ec3c880119f3b7002f0c6652f8b43634edad8ab3763cb0e4497d0bb1f scripts/migration/migrate_npm_core.py -2f5206ddea7a0816924986bfe663734f9fee7771fb25c04e0374642329b06c35 scripts/performance/README.md +bf934d59052988a528f3b7e7e3957b5a1ff1cf17db0d543e165cf76600e2137a scripts/performance/README.md 939287dadf7309689d29d2171757c2d80e91d898c3f9112454b943d08bd611fb scripts/performance/ab_1kb_dev.py 0380406a059ee9ec27473f8a46435f638f73a6dc729382ae1561c08dbf7ccb53 scripts/performance/benchmark.py 9ac220163256e08513597c397c016a48345b114d7876b439259cc310b36c83fc scripts/performance/checks_cache.py @@ -183,6 +184,7 @@ fa42ec0475e4398d647b3c094309b4332cd2b5649db223cae0b8e821d6b44a28 scripts/tests/ 72185f59f31bf04a4e88d53fe2fca52509f3663cb5f40b1d3bc319a5f945ef23 scripts/tests/panel_html.mjs b0136544af717b75df2cb107900ea65c3f945fdd0040369dbb93919008fb954c scripts/tests/panel_html.py 8d1903e2b17b3d59cd0a5bec3e298e468b3d88ed2dda8b724239cc45d8353c59 scripts/tests/progressbar_0332_browser_test.py +e07652d8a9d170325779009e6d4a826a4d3cfc57515d73ada5f8717dd7a2aca6 scripts/tests/proxy_1kb_opt3_static_test.py 2a6e75c05f3860acf65ea8132f0083904478b690145115ec017c120d70f26cca scripts/tests/proxy_hotpath_034_static_test.py 6fa2f22816996e989dad9dc7884adbf24f02f0ca6a6634b06e219eb08526c7fe scripts/tests/rps_hotpath_034_static_test.py 5be5cdc60324f4f0f6638640000b888a5babb0a9bbf29511c824a0ba38878c2a scripts/tests/sendfile_034_panel_browser_test.py @@ -198,10 +200,10 @@ f0a2e3daa0a49aac54723cb5a281360e49a97c076281372c5cca2a85e096c02c scripts/tests/ f55efa99245bbdbc98fe3202e52952d224ac3200612561580d52f6e251810049 scripts/tests/web_server_0330_ui_test.py 0dd1cef06a1c3e1546e2fb9b085bf369dfbed02107b9ab01b4fb4f3b894099c0 scripts/tests/web_server_0334_browser_test.py 22e880e98c7b9d700c7a4c02b9b1d543e5dbd1f4ecdca42813a1083165b32a19 scripts/version.py -55485a513e8c4da2a58daa7abcb15b32ed408a5fc062cf23599fb83d16b90cfe src/acl.rs +9bf4e4a61824f82b773b4e8ffa0909b9cf0a42bfc14d695bd64da240cc634a61 src/acl.rs 63efab1e95d41134fc30cc57320c32810bbae9f81a640bdb24b1be2059e63d8a src/acl/dynamic.rs 4803e44de39b872a5b3b55f9ca9e81c066ba52cb8f6adb879fd57c5f94b93e89 src/acl/dynamic/tests.rs -a22e5ea6381a1eacafe650be0f5cd6e653773e31f6491a562fe0459a309fb7cc src/acl/tests.rs +5e77c7e8d9781f6a8ff3d3f09da4dbcd5277b241901ff17c6c8c2703f2abbcef src/acl/tests.rs 6f5d3137bf24451da05c1db9f0f9e2a451305db17b6bc2590815ad54a6511829 src/acme.rs 587097620b2f25ce7c6f59f907b47660184b095e3040c7615e74509a82e7cdd8 src/acme/jobs.rs 3f158f46e80b403b658b4e9b1e3710e38c70e47b2d476b8223d9c394ffeeba95 src/acme/model.rs @@ -336,7 +338,7 @@ a9eeb93519aae2de7a1282296ced91d2eb8307cc966f8fbf6085af0fd21b0ad2 src/cache/cold 2eaba3261da2a547e9af91330319a3abd3a8ad8b5b44a337a84040720209017d src/cache/cold/startup.rs e7c7fd71727d542c2df3e03a4a1cba9d839c516963f66bbfde1c29ec81053500 src/cache/cold/worker.rs d51fa4f320be56c572f767617428c3f3838543cb0eeb7d1f8ae791e1a1ce9a71 src/cache/collapse.rs -36febf4d27a2f5bbd72dab7f45d8b8dd64fb4f7b95060b9e0ebd5709d19749fa src/cache/dashboard.rs +563919c53d38e9b83505a5715469833569efb2a09e91a2b9a89c335187368dee src/cache/dashboard.rs 7eaf3947cfc89cc6ef55a64fef0b3fedf1d9a1c33322aba2b5170c0ea34a5f2c src/cache/fill.rs 675d18afb53f25c56cba49bbb431fa39ecd9d7bdd192c7b39b0698225de7a2a2 src/cache/invalidation.rs c1357a13658e1ed5158084d3a41ba9ea906512774a5863ce9c56cb508d3fa6c8 src/cache/invalidation/ban.rs @@ -359,7 +361,7 @@ e26dce02df9c043329ca164fed215d271781bce8fb03240f528331bf3df6b9d7 src/cache/poli 989df11e246123f760ef31689370f3500e0913fcc6b70d7ac0c2c95e2f412fb0 src/cache/policy/request.rs d6051d55168fe1db97980a54c22c708f4f325ba72bfc36f35a1851eb5dcc051d src/cache/policy/response.rs 7e2bcfbb2b57732cdc704d9df7a9298fb80e735e37984431bc936630e41e7f8a src/cache/range.rs -6d7a4c22a7776a6538fb22d78806bec72efbe11aa5a26fb3ecf575ffd266ada0 src/cache/stats.rs +cf2513043813421e10260cef63645fdb8c5249114f16d91eae9f919b0214242a src/cache/stats.rs 93bd3e9e5c1a045c784cee0e1fdb9e1c5d862ed70d5d91c54c5a769bca8fd949 src/cache/tests.rs 9183fa8e6086e92fb148fc278fd866d44e3dd988c848a071e32066e44a1cbf0a src/cache/tests/cold_tier_tests.rs 25ed7635bfd5e34de55b319bf163db9a0af26642664ce7685ebe1241bab52cac src/cache/tests/dashboard_tests.rs @@ -391,8 +393,8 @@ b45d6040810c93ae2acb04ef6b0a59c189751edf9008cd81a4521c9e04e7f07d src/docker.rs 4c2c988191a97f81568a8df64af772463d010844f1abf327871901cdb32d4336 src/docker/model.rs 5f4610c59b2d4ff6a205a2d9ae11bc84be689b2a68302459f546b3f43e53593c src/docker/reconcile.rs a12c47df4b90b6fc36c978a42477e6ddffbbbf69aed7252101c2fd829709bd6e src/docker/tests.rs -2a02ea2e010cde1b1fe99b2e6baa80ea344ea6b332dbb3c4be795b71541f64a7 src/forwarding.rs -057e652f216a1478638cbb53b1c64ac88c7f413a65825caacbf893c5af004a63 src/forwarding/tests.rs +ed17a905bc249b4c36dfd07711a87bea9f385a01d16264f798e1bae932dc51a0 src/forwarding.rs +b25fbb00de0e37ad5fa1a049839265e5f7e9f83b6214c40a95f57ea4807685bc src/forwarding/tests.rs af7a4e91c32ffbe4701c8c06e48ba3f8bbbe55e8b9e877aecb30ccdf858e9e8f src/geoip.rs 80850f7acd1ed4c0048b065e9e81740869a8efbb0d9b5d2e83d8ec7fbc494703 src/geoip/mmdb.rs 06aa056d789742cc11c8826c338d985e6a6fb22da6c3530937eec38e35478034 src/geoip/mmdb_tests.rs @@ -440,25 +442,25 @@ e03f0d6b3f35dae6b98c394837aa0d17e4d73a8306767b4c607868bc00c30286 src/prometheus fb2d8d45cd2e947f783eb981b37fa418c39fcb327a3af17a648fadea7f37f888 src/prometheus/web_php.rs 2c97367686b54989573ce7fb81b2c6dde72adb254df9522bc05ded74f1fbffb6 src/proxy/body.rs c9b76abd91f0dfe81e07073e89d35faa033c19e11fa88b223f075428542284f1 src/proxy/body/tests.rs -b88d1d7a6837a2a470e5863f1f3247878daee62791fc3d8e72baeb66469bdfbf src/proxy/cache_helpers.rs +818dcfefa1c40d6a722dacf53c7f45c53141240f1c1913cfa651e19dd2718526 src/proxy/cache_helpers.rs aa49c9928bb91e3d5bae0d340d88396a0417b9edec6192d41e1403533319a897 src/proxy/cache_http_invalidation.rs 676c1b325fd8e55bcdd86451c7d1d8b8d347ca00e8da6b3e5b148a58c376a9b4 src/proxy/cache_plan.rs 3404b03096862de1050088f62cdd811ea1ab61d191f902501173107f81f23b9b src/proxy/cache_plan/tests.rs a8848cbdf85368548b003af2b43823c453866c472af4c7d2ff0ec7da14c44c8d src/proxy/error_fallback.rs -bf2e6bbd64fb45fceed54af0bff1c1e00da22b79c64dd8f5c23d2af0dbab0b7d src/proxy/forward/cache_lookup.rs +5670cd3557d862660596d8f9163a510aab8651206b5e61ae0f735a1a8a3c4313 src/proxy/forward/cache_lookup.rs cfa941b60b5d167ec9d8e2566ea3342b588d1a9078a6ca711ad6062ddf00fec9 src/proxy/forward/response.rs 7a5f43fe65e5a746eb84881da0bf784215081ead66d18faa7780533ad5df9c8c src/proxy/forward/route_cache.rs -96c97928986ca89c2d6d415d5b107b9cc7228410bae93a2c08c24974324f4d8d src/proxy/forward/success_finish.rs +8b7d9802616fde19a5be3ae2d2e4703e066f203e6a60a6bd781d3a2c15388348 src/proxy/forward/success_finish.rs 0a921474ef2b93eabdaf3888651cf29130eace5ccb1e0f5d6bc25e6aa69280be src/proxy/forward/success_prepare.rs bb24da9c9ab986a9c4d4144d4d1be0f661561d4afc755f298b58bc679b182850 src/proxy/forward/upstream.rs -11dccc239ac8a8e9ea06e884c2f2658f86b7807d14a59b8e052bc7dd75e86d63 src/proxy/forward/web_source.rs -facde401ba5bcbafdcea5524f236cfae37eec11144ceeea879e83314c84bc97e src/proxy/forward_entry.rs +bb8c9117e05b0cb4bbfe029ef4d1a031fc755cdce255b15dad072afd1c845737 src/proxy/forward/web_source.rs +2b6ae70b84fd1eba0dba6d8d7e2db62227193dc5b77e4a6111ceab1b219800c2 src/proxy/forward_entry.rs 9b25f5e3ea4443a63ee6f45d12e1957dd316fe8e85325fc00ea416faaab1025a src/proxy/h2_graceful.rs -3e856a2e3e2236caebf4a7e7e8aba229a529cc552c1fb73e4b81c38784ecad26 src/proxy/headers.rs -02542edb25302ab4778d5f84f900b546ed126be56e4bc9d3580703edea6f492c src/proxy/headers/tests.rs +31e925a408ba6273deb0793df5f5f82eeabbcaffb365cc0457ed9c67d9200f34 src/proxy/headers.rs +013f0495127e351cf535a928526b56bd3a63202d8abd0fa59da5dfb8dc783f27 src/proxy/headers/tests.rs 3f25429418941eb72c3847125340db74684ad316eb278ecf658953026fd41f4f src/proxy/mod.rs 55133ceace2e5262d9c54fa41bea099a27b817d1e09c1cd0e2493fd9cee4da38 src/proxy/redirect_helpers.rs -96da894a86c05f8f467bf83cc23e56627ed3f9f490ffc936872d70126c8ecb41 src/proxy/request_id.rs +01b3027cfe92512f71be056a1ae26749b3ad3417d43388f1c05dcf5884d83966 src/proxy/request_id.rs d2df55b6ccfc7ceb8a3d2c49f664acd183448e3d932d5399d6692e87f79a094b src/proxy/request_id/tests.rs e56c65c85e42ee4fe6adf3655f3b2d176798e37c648db6bfb8e1602300b723eb src/proxy/tests/authority_tests.rs 1aa9bbbfb2659facdaeaf185e11221b1c8cd5fe6e23c7c1dacd3aa55e3db4fcb src/proxy/tests/cache_fast_path_tests.rs diff --git a/docs/PROXY_1KB_COUNTERS_DEV_0.34.0.md b/docs/PROXY_1KB_COUNTERS_DEV_0.34.0.md index 5ef580a..ba6f1e5 100644 --- a/docs/PROXY_1KB_COUNTERS_DEV_0.34.0.md +++ b/docs/PROXY_1KB_COUNTERS_DEV_0.34.0.md @@ -130,3 +130,18 @@ backend connects and all log/traffic drop counters. Test concurrent global purges and per-vhost purges during traffic and verify the stats reset behavior and cache HIT/MISS/BYPASS/reason distribution. Run long soaks and both cold/HOT cache tests. The 100k RPS / nginx 80% goal is still **OPEN**. + +## Build fix after first compiler feedback (OPT2-FIX1) + +The first user build of OPT2 failed with six Rust `E0753` diagnostics because +`src/cache/stats.rs` starts with `//!` *inner module* documentation but is +injected via `include!("stats.rs")` after other items in `src/cache/mod.rs`. +The six documentation lines are now ordinary `//` comments; there is no +behavior change. Three unnecessary `mut` bindings in +`src/cache/dashboard.rs` were also removed (warnings only). Version remains +0.34.0; no performance or feature semantics have changed in this fix. + +The compiler-enabled host should rerun `./dev.sh check`, +`CARGO_INCREMENTAL=0 ./dev.sh test` and an optimized release build, then +performance benchmarks. This editing environment still lacks `cargo` and +`rustc`, so the fix has been statically checked, **not compiled** here. diff --git a/docs/PROXY_1KB_OPT3_DEV_0.34.0.md b/docs/PROXY_1KB_OPT3_DEV_0.34.0.md new file mode 100644 index 0000000..9c58672 --- /dev/null +++ b/docs/PROXY_1KB_OPT3_DEV_0.34.0.md @@ -0,0 +1,88 @@ +# RustProxy 0.34.0 DEV — 1,040-byte response, OPT3 + +## Scope + +Primary: uncached reverse proxy, HTTP/1.1 keep-alive, 1,040-byte body, +no compression, ACL/security/logging/analytics/counters fully operational. +Goal: ~100,000 RPS and >=80% of nginx on the **same isolated host**. +Secondary: local Web Server/static files, measured independently. +**Version remains exactly 0.34.0**; existing configuration and defaults are +unchanged; no functions, checks or logs are disabled. + +## New code optimizations (relative to OPT2-FIX1) + +1. `src/acl.rs` / `src/proxy/cache_helpers.rs`: borrow ordinary validated + routing paths using `Cow` during immutable route selection. Calls + with escaped (`%xx`) or repeated separators still use the original strict + allocator/decoder. Invalid control bytes, dot segments, UTF-8 and ambiguous + encodings remain rejected; the original wire path is not rewritten. +2. `src/proxy/forward_entry.rs` plus cache/Web Service call sites: retain the + original `http::Uri` value so normal paths can remain borrowed during the + entire mutable forwarding/rewrite operation. Cache-refresh tasks that need + ownership still allocate explicitly and hold their own `String`. +3. `src/forwarding.rs`: a **direct, untrusted** TCP peer now obtains the same + seven forwarding headers without allocating hop-chain `Vec` structures. + Trust chains and the configured `trust_forwarded_proto` logic use the + unchanged full parser. IPv4/IPv6 RFC 7239 formatting and true listener + port/scheme are preserved; untrusted spoofed values are still ignored. +4. `src/proxy/forward_entry.rs`: when effective client IP is identical to + the transport peer, reuse the IP stored by observation. No redundant + `IpAddr::to_string()` and no per-exchange mutex write. Different/trusted or + IPv4-mapped addresses are still stored in canonical form. +5. `src/proxy/headers.rs`: plain single-valued `Connection: keep-alive` + avoids the temporary `Vec` while all standard hop-by-hop fields + are removed; comma-separated, repeated and complex Connection fields keep + the original removal parser to prevent nominated-header leaks. +6. `src/proxy/request_id.rs`: preserve SHA-256, CSPRNG, per-process sequence, + unpredictable 64-char hexadecimal IDs and wire headers, but write the hex + digits into one preallocated byte buffer before UTF-8 verification. +7. `src/cache/stats.rs`: select the most frequent `cache_disabled` reason + counter directly. The same exact atomic totals, reason labels, 4096-domain + limit and reset behavior are retained. + +The changes avoid repeated work in common cases; no numerical throughput +benefit has been measured for this new source. In particular, no cache, log, +metrics, retry or auth feature was switched off for a synthetic benchmark. + +## Evidence and known limits + +- Provided newly compiled *OPT2-FIX1* executable SHA-256: + `6037e98d059672b716f2e1411976bf305cac927901a6f6662d8b703829ff8196`. +- PASS **89** HTTP proxy/policy/cache black-box assertions, zero failures, + testing that supplied executable, **not** the new OPT3 code. +- Source checks, migration/upgrade checks and ZIP checks are separate + static/offline verification; they are not equivalent to cargo/test/live. +- A single interrupted diagnostic A/B sample at c=64, 3 s, same idle=64, + gave older binary 33,752 RPS and OPT2-FIX1 binary 33,556 RPS, zero failed + requests; do **not** infer a true performance difference from one sample. +- NOT RUN here: `cargo check`, `cargo test`, rebuilt OPT3 integration tests, + post-build A/B RPS, long soaking, real TLS/H2/H3 compatibility and 80%-of-nginx + acceptance. No Rust toolchain is available in this editing container. +- Resource/latency trade-offs (including log/analytics drops, RSS/FDs) require + the same measured and pinned server/peer/generator CPU sets for before/after. + +## Rebuild and measure + +From the unpacked `RustProxy/` directory: + +```bash +./dev.sh check +CARGO_INCREMENTAL=0 ./dev.sh test +bash dev.sh +python3 scripts/performance/regression.py \ + --binary new=/ABSOLUTE/PATH/NEW/rust-proxy --out /tmp/rustproxy-opt3-http +python3 scripts/performance/ab_1kb_dev.py \ + --binary before=/ABSOLUTE/PATH/OPT2FIX1/rust-proxy \ + --binary after=/ABSOLUTE/PATH/OPT3/rust-proxy \ + --nginx /usr/sbin/nginx --ab /usr/bin/ab \ + --out /tmp/rustproxy-opt3-1kb --concurrency 16,64 \ + --duration 5 --repeats 3 --workers 4 --max-idle 64 +``` + +For an acceptance-grade evaluation, run the `scripts/performance/benchmark.py` +paired before/after/nginx test with separate pinned CPU sets, a verified origin +and 30-second runs repeated 7 times. Report median and p95/p99 latency, +errors, CPU, RSS, file descriptors, dropped log/analytics events, and all HTTP +status/body hashes. Retest TLS/H2/H3/WebSocket, auth, cache semantics and +connection/backpressure limits. Compare the **rebuilt** OPT3 binary against +OPT2-FIX1 at exactly the same listener/backend settings and compiler profile. diff --git a/scripts/performance/README.md b/scripts/performance/README.md index 148a9f3..1090d21 100644 --- a/scripts/performance/README.md +++ b/scripts/performance/README.md @@ -1,5 +1,12 @@ # Live performance and regression harness +## DEV OPT3 / 1 KiB response: uncompiled source + +`docs/PROXY_1KB_OPT3_DEV_0.34.0.md` describes 7 optimized hot-path sites +and how to benchmark their **newly rebuilt** executable. Baseline +`rust-proxy(2)` passed 89 real proxy/policy/cache checks. This runtime result +is not evidence that uncompiled OPT3 changes work, nor a measured OPT3 RPS gain. + These scripts exercise actual RustProxy executables through loopback sockets. They do not simulate Rust request handling. The Python server in `peer.py` is a controllable test **upstream**, used to verify streaming, non-replayable writes, diff --git a/scripts/tests/proxy_1kb_opt3_static_test.py b/scripts/tests/proxy_1kb_opt3_static_test.py new file mode 100755 index 0000000..d105bbb --- /dev/null +++ b/scripts/tests/proxy_1kb_opt3_static_test.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +"""Source-only regression guards for the OPT3 patch (NOT a Rust compile).""" +from pathlib import Path +root = Path(__file__).resolve().parents[2] + +def read(name): + return (root / name).read_text(encoding='utf-8') + +assert 'version = "0.34.0"' in read('Cargo.toml') +acl = read('src/acl.rs') +assert "pub fn routing_path_view(raw: &str)" in acl +assert "return routing_path(raw).map(std::borrow::Cow::Owned)" in acl +assert 'raw.split' in acl and 'raw.len() <= 8192' in acl +forward = read('src/proxy/forward_entry.rs') +assert 'let routing_uri = request.uri().clone();' in forward +assert 'crate::acl::routing_path_view(routing_uri.path())' in forward +assert 'if context.client_ip != peer.0.ip()' in forward +route = read('src/proxy/cache_helpers.rs') +assert 'crate::acl::routing_path_view(request.uri().path())' in route +for name in ('src/proxy/forward/cache_lookup.rs', 'src/proxy/forward/success_finish.rs', 'src/proxy/forward/web_source.rs'): + assert 'path.clone()' not in read(name), name +assert 'if !trusted_peer' in read('src/forwarding.rs') +assert 'let mut ips = chain(incoming).unwrap_or_default();' in read('src/forwarding.rs') +headers = read('src/proxy/headers.rs') +assert 'let only_keep_alive' in headers +assert 'let nominated: Vec' in headers +assert 'headers.remove(name)' in headers +assert 'String::from_utf8(encoded)' in read('src/proxy/request_id.rs') +assert 'if reason == "cache_disabled"' in read('src/cache/stats.rs') +assert not read('src/cache/stats.rs').startswith('//!') # previous E0753 build issue +for name in ('src/acl/tests.rs', 'src/proxy/headers/tests.rs', 'src/forwarding/tests.rs'): + assert (root / name).exists() +for source in (root / 'src').rglob('*.rs'): + if '/tests/' in source.as_posix() or source.name == 'tests.rs': + continue + assert len(source.read_text(encoding='utf-8').splitlines()) <= 500, source +print('PASS OPT3 source guards (STATIC ONLY; not cargo check, live HTTP or throughput)') diff --git a/src/acl.rs b/src/acl.rs index 043abe9..cdb95e5 100644 --- a/src/acl.rs +++ b/src/acl.rs @@ -166,6 +166,32 @@ pub fn validate_decoded_routing_path(path: &str) -> Result<()> { Ok(()) } +/// Validate a routing path without allocating for the common already-normalized +/// form. The slow path delegates to `routing_path` to preserve percent decode, +/// UTF-8 validation, slash folding and ambiguity rejection exactly as before. +/// Callers must not use this value as the upstream URL: the raw URI is forwarded. +pub fn routing_path_view(raw: &str) -> Result> { + ensure!( + raw.starts_with('/') && raw.len() <= 8192, + "invalid path length or form" + ); + let bytes = raw.as_bytes(); + for (index, &byte) in bytes.iter().enumerate() { + if byte == b'%' || (byte == b'/' && index > 0 && bytes[index - 1] == b'/') { + return routing_path(raw).map(std::borrow::Cow::Owned); + } + ensure!( + byte > 32 && byte != 127 && !matches!(byte, b'\\' | b'?' | b'#' | b';'), + "invalid path character" + ); + } + ensure!( + !raw.split('/').any(|segment| segment == "." || segment == ".."), + "ambiguous path segments" + ); + Ok(std::borrow::Cow::Borrowed(raw)) +} + /// Routing/ACL use one decoded representation; upstream still receives the original path. /// Reject encoded separators, percent (double decoding), controls, ambiguous dot segments, /// matrix parameters and malformed UTF-8. Repeated literal separators are diff --git a/src/acl/tests.rs b/src/acl/tests.rs index cbe7f31..9bf87db 100644 --- a/src/acl/tests.rs +++ b/src/acl/tests.rs @@ -255,4 +255,21 @@ mod tests { assert!(routing_path(path).is_err(), "{path}"); } } + + #[test] + fn borrowed_route_path_matches_original_validator_for_safe_and_unsafe_targets() { + use std::borrow::Cow; + for value in ["/", "/a/b", "/static/app.js", "/café", "/admin/%61", "/a//b", "/%2e%2e/a", "/..", "/a;secret", "/x%2Fy", "/%00", "/a b", "/bad%", "/a\\b", "not-absolute"] { + let original = routing_path(value); + let borrowed = routing_path_view(value); + assert_eq!(original.is_ok(), borrowed.is_ok(), "{value}"); + if let (Ok(normalized), Ok(view)) = (original, borrowed) { + assert_eq!(normalized, view.as_ref(), "{value}"); + } + } + assert!(matches!(routing_path_view("/foo/bar").unwrap(), Cow::Borrowed(_))); + assert!(matches!(routing_path_view("/foo//bar").unwrap(), Cow::Owned(_))); + assert!(matches!(routing_path_view("/%61").unwrap(), Cow::Owned(_))); + assert!(routing_path_view(&format!("/{}", "x".repeat(8192))).is_err()); + } } diff --git a/src/cache/dashboard.rs b/src/cache/dashboard.rs index b5a15b8..57d620d 100644 --- a/src/cache/dashboard.rs +++ b/src/cache/dashboard.rs @@ -5,7 +5,7 @@ impl Cache { .lock() .unwrap_or_else(|e| e.into_inner()) .clone(); - let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner()); + let store = self.store.lock().unwrap_or_else(|e| e.into_inner()); let n = |value: &AtomicU64| value.load(Ordering::Relaxed); let hits = n(&self.counters.hits); let misses = n(&self.counters.misses); @@ -243,7 +243,7 @@ impl Cache { count } pub fn scope_view(&self, scope: &str) -> ScopeView { - let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner()); + let store = self.store.lock().unwrap_or_else(|e| e.into_inner()); let mut view = ScopeView { entries: 0, resident_bytes: 0, @@ -269,7 +269,7 @@ impl Cache { self.domain_stats.record_reason(scope, domain, reason); } pub fn dashboard(&self, scope: Option<&str>, offset: usize, limit: usize) -> Dashboard { - let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner()); + let store = self.store.lock().unwrap_or_else(|e| e.into_inner()); let revision = store.revision; let mut groups: BTreeMap<(String, String), DomainView> = self .domain_stats diff --git a/src/cache/stats.rs b/src/cache/stats.rs index b9493f9..7930c9a 100644 --- a/src/cache/stats.rs +++ b/src/cache/stats.rs @@ -58,6 +58,10 @@ impl DomainStats { fn record_reason(&self, reason: &'static str) { // Cache OFF is the highest-volume case; avoid reason map lookup and // any shared lock/allocator activity for its dashboard reason. + if reason == "cache_disabled" { + self.fast_reasons[0].fetch_add(1, Ordering::Relaxed); + return; + } if let Some(index) = FAST_REASONS.iter().position(|label| *label == reason) { self.fast_reasons[index].fetch_add(1, Ordering::Relaxed); return; diff --git a/src/forwarding.rs b/src/forwarding.rs index 6161a21..38077a6 100644 --- a/src/forwarding.rs +++ b/src/forwarding.rs @@ -217,11 +217,34 @@ impl Compiled { ) -> Result { let peer_ip = canonical(peer.ip()); let trusted_peer = self.trusted(peer_ip); - let mut ips = if trusted_peer { - chain(incoming).unwrap_or_default() - } else { - Vec::new() - }; + if !trusted_peer { + let scheme = if tls { "https" } else { "http" }; + let authority_text = authority.to_str()?; + crate::config::request_host(authority_text)?; + let target = uri.path_and_query().map(|v| v.as_str()).unwrap_or("/"); + let original_url = format!("{scheme}://{authority_text}{target}"); + let peer_text = peer_ip.to_string(); + let peer_header = HeaderValue::from_str(&peer_text)?; + let forwarded = format!( + "for={};by={};proto={scheme};host=\"{authority_text}\"", + node(peer_ip), node(canonical(local.ip())) + ); + let mut values = HeaderMap::with_capacity(7); + values.insert("forwarded", forwarded.parse()?); + values.insert("x-forwarded-for", peer_header.clone()); + values.insert("x-real-ip", peer_header); + values.insert("x-forwarded-host", authority.clone()); + values.insert("x-forwarded-proto", HeaderValue::from_static(scheme)); + values.insert("x-forwarded-port", local.port().to_string().parse()?); + values.insert("x-forwarded-uri", target.parse()?); + return Ok(Context { + client_ip: peer_ip, + scheme, + original_url, + values, + }); + } + let mut ips = chain(incoming).unwrap_or_default(); ips.push(peer_ip); // Discard everything left of the first untrusted hop walking from us. let first = ips.iter().rposition(|ip| !self.trusted(*ip)).unwrap_or(0); diff --git a/src/forwarding/tests.rs b/src/forwarding/tests.rs index 6e7bc03..88d64f4 100644 --- a/src/forwarding/tests.rs +++ b/src/forwarding/tests.rs @@ -200,4 +200,33 @@ mod tests { .unwrap(); assert_eq!(c.values["x-forwarded-for"], "203.0.113.8, 104.16.1.2"); } + + #[test] + fn untrusted_fast_path_preserves_ipv6_and_forwarding_metadata() { + let source = Settings { + trusted_proxies: vec!["10.0.0.0/8".into()], + trust_forwarded_proto: true, + }.compile().unwrap(); + let mut supplied = HeaderMap::new(); + supplied.insert("forwarded", "for=203.0.113.8".parse().unwrap()); + supplied.insert("x-forwarded-proto", "http".parse().unwrap()); + let context = source.context( + &supplied, + "[2001:db8::5]:5050".parse().unwrap(), + "[2001:db8::10]:8080".parse().unwrap(), + true, + &"app.example:443".parse().unwrap(), + &"/s?q=1".parse().unwrap(), + ).unwrap(); + assert_eq!(context.client_ip, "2001:db8::5".parse::().unwrap()); + assert_eq!(context.scheme, "https"); + assert_eq!(context.original_url, "https://app.example:443/s?q=1"); + assert_eq!(context.values["forwarded"], + "for=\"[2001:db8::5]\";by=\"[2001:db8::10]\";proto=https;host=\"app.example:443\""); + assert_eq!(context.values["x-forwarded-for"], "2001:db8::5"); + assert_eq!(context.values["x-real-ip"], "2001:db8::5"); + assert_eq!(context.values["x-forwarded-proto"], "https"); + assert_eq!(context.values["x-forwarded-port"], "8080"); + assert_eq!(context.values["x-forwarded-uri"], "/s?q=1"); + } } diff --git a/src/proxy/cache_helpers.rs b/src/proxy/cache_helpers.rs index 53d5505..e78dd65 100644 --- a/src/proxy/cache_helpers.rs +++ b/src/proxy/cache_helpers.rs @@ -172,9 +172,9 @@ fn response_context( let authority = effective_host(request, tls).ok()?; let host = request_host(authority.to_str().ok()?).ok()?; let frontend = snapshot.frontend(&host, local, tls).cloned(); - let route = crate::acl::routing_path(request.uri().path()) + let route = crate::acl::routing_path_view(request.uri().path()) .ok() - .and_then(|path| snapshot.select_on_arc(&host, &path, local, tls).cloned()); + .and_then(|path| snapshot.select_on_arc(&host, path.as_ref(), local, tls).cloned()); Some((frontend, route)) })(); context.unwrap_or((None, None)) diff --git a/src/proxy/forward/cache_lookup.rs b/src/proxy/forward/cache_lookup.rs index 869b130..65bb021 100644 --- a/src/proxy/forward/cache_lookup.rs +++ b/src/proxy/forward/cache_lookup.rs @@ -42,7 +42,7 @@ if let Some(ticket) = &cache_ticket { uri: uri.clone(), public_suffix: public_suffix.clone(), host: host.clone(), - path: path.clone(), + path: path.as_ref().to_owned(), scope: scope.to_owned(), timeout: frontend .and_then(|frontend| frontend.proxy_timeouts.response_header_timeout()) @@ -79,7 +79,7 @@ if let Some(ticket) = &cache_ticket { uri: uri.clone(), public_suffix: public_suffix.clone(), host: host.clone(), - path: path.clone(), + path: path.as_ref().to_owned(), scope: scope.to_owned(), timeout: frontend .and_then(|frontend| frontend.proxy_timeouts.response_header_timeout()) @@ -240,7 +240,7 @@ if handshake.is_none() uri: uri.clone(), public_suffix: public_suffix.clone(), host: host.clone(), - path: path.clone(), + path: path.as_ref().to_owned(), scope: scope.to_owned(), timeout: frontend .and_then(|frontend| frontend.proxy_timeouts.response_header_timeout()) diff --git a/src/proxy/forward/success_finish.rs b/src/proxy/forward/success_finish.rs index 8af09ee..ec2a981 100644 --- a/src/proxy/forward/success_finish.rs +++ b/src/proxy/forward/success_finish.rs @@ -170,7 +170,7 @@ if let (Some(ticket), Some(plan), Some(transformed)) = uri: active_uri.clone(), public_suffix: public_suffix.clone(), host: host.clone(), - path: path.clone(), + path: path.as_ref().to_owned(), scope: scope.to_owned(), timeout: frontend .and_then(|frontend| frontend.proxy_timeouts.response_header_timeout()) diff --git a/src/proxy/forward/web_source.rs b/src/proxy/forward/web_source.rs index ecc9e34..b7ec0f1 100644 --- a/src/proxy/forward/web_source.rs +++ b/src/proxy/forward/web_source.rs @@ -7,7 +7,7 @@ } let service_path = match route.rewritten_path(&path) { Ok(Some(value)) => value, - Ok(None) => path.clone(), + Ok(None) => path.as_ref().to_owned(), Err(_) => return error(StatusCode::BAD_REQUEST, "RustProxy: invalid Web Server rewrite path\n"), }; diff --git a/src/proxy/forward_entry.rs b/src/proxy/forward_entry.rs index 0fb0063..978d896 100644 --- a/src/proxy/forward_entry.rs +++ b/src/proxy/forward_entry.rs @@ -101,7 +101,12 @@ async fn forward_request( .extensions() .get::() .cloned(); - let Ok(path) = crate::acl::routing_path(request.uri().path()) else { + // Retain the original URI's shared backing bytes independently of `request`. + // A normalized, already-safe path can then be borrowed for the entire + // forwarding operation, even after the request URI/headers are rewritten. + // Encoded/slash-folded paths still allocate via the strict decoder. + let routing_uri = request.uri().clone(); + let Ok(path) = crate::acl::routing_path_view(routing_uri.path()) else { return error( StatusCode::BAD_REQUEST, "RustProxy: ambiguous request path\n", @@ -216,9 +221,15 @@ async fn forward_request( cloudflare_country = crate::forwarding::cloudflare_country(request.headers(), peer.0); } } - if let Some(exchange) = request.extensions().get::>() { - *exchange.client_ip.lock().unwrap_or_else(|e| e.into_inner()) = - Some(context.client_ip.to_string()); + // `observe` already stored the actual transport peer. Most requests + // don't use a trusted forwarding hop, so avoid formatting the same IP + // and taking the per-request mutex a second time. IPv4-mapped peers + // still take this path so their canonical form is correctly logged. + if context.client_ip != peer.0.ip() { + if let Some(exchange) = request.extensions().get::>() { + *exchange.client_ip.lock().unwrap_or_else(|e| e.into_inner()) = + Some(context.client_ip.to_string()); + } } // GeoIP stays off the normal request path. Resolve lazily for any vhost or // location policy that actually needs a country code, or for log capture. diff --git a/src/proxy/headers.rs b/src/proxy/headers.rs index 6ccc73a..0ff2d6d 100644 --- a/src/proxy/headers.rs +++ b/src/proxy/headers.rs @@ -4,15 +4,27 @@ use http::{ }; /// Remove both standard hop-by-hop fields and fields named by Connection. pub fn strip_hop_by_hop(headers: &mut HeaderMap) { - let nominated: Vec = headers - .get_all(CONNECTION) - .iter() - .filter_map(|v| v.to_str().ok()) - .flat_map(|v| v.split(',')) - .filter_map(|v| v.trim().parse().ok()) - .collect(); - for name in nominated { - headers.remove(name); + // The common HTTP/1.1 "Connection: keep-alive" case has no dynamic + // nominated headers to collect. Keep the general multi-value/token + // parser for all other requests (including maliciously repeated fields). + let only_keep_alive = { + let mut values = headers.get_all(CONNECTION).iter(); + values.next().is_some_and(|value| { + value.as_bytes().eq_ignore_ascii_case(b"keep-alive") + && values.next().is_none() + }) + }; + if !only_keep_alive { + let nominated: Vec = headers + .get_all(CONNECTION) + .iter() + .filter_map(|v| v.to_str().ok()) + .flat_map(|v| v.split(',')) + .filter_map(|v| v.trim().parse().ok()) + .collect(); + for name in nominated { + headers.remove(name); + } } for name in [ "connection", diff --git a/src/proxy/headers/tests.rs b/src/proxy/headers/tests.rs index cbede77..fe7c02d 100644 --- a/src/proxy/headers/tests.rs +++ b/src/proxy/headers/tests.rs @@ -52,4 +52,34 @@ mod tests { restore_admin_cookie(&mut h, &admin); assert_eq!(h.get("cookie").unwrap(), "a=1; b=2; rustproxy_session=secret"); } + + #[test] + fn common_keep_alive_does_not_strip_unrelated_headers() { + for value in ["keep-alive", "KEEP-ALIVE", "Keep-Alive"] { + let mut headers = HeaderMap::new(); + headers.insert("connection", value.parse().unwrap()); + headers.insert("keep-alive", "timeout=2".parse().unwrap()); + headers.insert("x-tenant", "alpha".parse().unwrap()); + headers.insert("authorization", "Bearer allowed".parse().unwrap()); + strip_hop_by_hop(&mut headers); + assert!(!headers.contains_key("connection")); + assert!(!headers.contains_key("keep-alive")); + assert_eq!(headers["x-tenant"], "alpha"); + assert_eq!(headers["authorization"], "Bearer allowed"); + } + } + #[test] + fn multiple_connection_fields_still_strip_all_nominated_headers() { + let mut headers = HeaderMap::new(); + headers.append("connection", "keep-alive".parse().unwrap()); + headers.append("connection", "x-hidden, upgrade".parse().unwrap()); + headers.insert("x-hidden", "sensitive".parse().unwrap()); + headers.insert("x-public", "allowed".parse().unwrap()); + headers.insert("upgrade", "websocket".parse().unwrap()); + strip_hop_by_hop(&mut headers); + assert!(!headers.contains_key("x-hidden")); + assert!(!headers.contains_key("connection")); + assert!(!headers.contains_key("upgrade")); + assert_eq!(headers["x-public"], "allowed"); + } } diff --git a/src/proxy/request_id.rs b/src/proxy/request_id.rs index 2d8f66d..75db0f0 100644 --- a/src/proxy/request_id.rs +++ b/src/proxy/request_id.rs @@ -13,12 +13,14 @@ fn request_id_hash(sequence: u64) -> String { fn request_id_hex(digest: &[u8]) -> String { const HEX: &[u8; 16] = b"0123456789abcdef"; - let mut encoded = String::with_capacity(digest.len() * 2); - for &byte in digest { - encoded.push(HEX[(byte >> 4) as usize] as char); - encoded.push(HEX[(byte & 0x0f) as usize] as char); + // Encode two ASCII bytes per digest byte in one allocation; avoid 64 + // separate String::push bounds checks for the 32-byte SHA-256 digest. + let mut encoded = vec![0u8; digest.len() * 2]; + for (i, &byte) in digest.iter().enumerate() { + encoded[2 * i] = HEX[(byte >> 4) as usize]; + encoded[2 * i + 1] = HEX[(byte & 0x0f) as usize]; } - encoded + String::from_utf8(encoded).expect("hex digits are always valid UTF-8") } #[cfg(test)]