v0.34.0_4

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-18 07:00:34 +02:00
1 parent f20c8da718
commit 5910e6e901
21 files changed
+365 -39

No files matched your search

+14
View File
@@ -5,6 +5,20 @@
## 0.34.0 ## 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, - 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. stored-entry and saved-body statistics outside the HOT cache's shared mutex.
Publish bounded domain/reason indexes and update counters atomically while Publish bounded domain/reason indexes and update counters atomically while
+9
View File
@@ -1,5 +1,14 @@
# RustProxy 0.34.0 # 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) ## DEV: cache counter contention and saturated analytics (source candidate)
Per-domain cache outcome/reason accounting now uses bounded immutable indexes Per-domain cache outcome/reason accounting now uses bounded immutable indexes
+20 -18
View File
@@ -2,11 +2,11 @@
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example 4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore 9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
46bab1d2c2d6e0a80d57a6b232b1ba4c220884b95cd45ddccbafce2aa160cddb AGENTS.md 46bab1d2c2d6e0a80d57a6b232b1ba4c220884b95cd45ddccbafce2aa160cddb AGENTS.md
03d0a320df729cc98a119f4296e3b9805aece0c8591fa6b90003df2b58f8608c CHANGELOG.md 10f70748d2c070e2b9923b5aa6572347c7977a88f8bb5d4abe532827914bade7 CHANGELOG.md
ca201e3cd3f9842eae0006d0b40a0a72bcd3d4da6e4e5ecaf200d1b9a1390db2 Cargo.lock ca201e3cd3f9842eae0006d0b40a0a72bcd3d4da6e4e5ecaf200d1b9a1390db2 Cargo.lock
fec5f1bae9a59ef1fabd27c5e45d31448dd8f811736eebfa8826b3806a7e3349 Cargo.toml fec5f1bae9a59ef1fabd27c5e45d31448dd8f811736eebfa8826b3806a7e3349 Cargo.toml
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile 25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
5533499655d41f1faaf666dfa630375cf6770dff37f6cd7ef5a4e5bbd58cb258 README.md 09234b68d69eb10047396baab4bf5811b37e954ad3f5eaacf90247265d4fbd52 README.md
aee9bbb367e061aeb79916e38d121e7767f6f9d0c16a2dcc4b28f091586a33b1 ROADMAP.md aee9bbb367e061aeb79916e38d121e7767f6f9d0c16a2dcc4b28f091586a33b1 ROADMAP.md
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
@@ -56,8 +56,9 @@ be53a58afbeddbd7deb4b5f6447f2dc74db0327839bd83407c20915df98dcfa0 docs/NOT_FOUND
e68d5a5cf0991de9308f78afb0264e02979f7463a5f395eb7a6e9ec2bd3a3d54 docs/PHP_FPM.md e68d5a5cf0991de9308f78afb0264e02979f7463a5f395eb7a6e9ec2bd3a3d54 docs/PHP_FPM.md
bc068a176056f5ea4d3b637071e644ed58a2140494c233ef22aa66e9700e3299 docs/PHP_FPM_ISOLATION.md bc068a176056f5ea4d3b637071e644ed58a2140494c233ef22aa66e9700e3299 docs/PHP_FPM_ISOLATION.md
ad6e3f003b820a4f45e30e39577d120a69d167c70910bda4891971759458c3dd docs/PROMETHEUS.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 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 deea89aed410c0619bdbdb12437edc7aff9dca2fec7fdc059e918c649fc36b51 docs/PROXY_HOTPATH_0.34.0.md
a7d6189b0e8cb854918456e0063101ee77bfb2419a9ba0430eb0e3f8678c0f09 docs/PROXY_RPS_0.34.0.md a7d6189b0e8cb854918456e0063101ee77bfb2419a9ba0430eb0e3f8678c0f09 docs/PROXY_RPS_0.34.0.md
887d1fdcc3b546b024afcd18b1716b8e3a74a3800012bf4bda7a122d2eabe233 docs/README.md 887d1fdcc3b546b024afcd18b1716b8e3a74a3800012bf4bda7a122d2eabe233 docs/README.md
@@ -133,7 +134,7 @@ a641bd35400c56f3b68b0a92e97b602d06090f03972ca8f01fa744e90dd97403 scripts/deploy
2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migration/migrate_npm.py 2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migration/migrate_npm.py
a459d95bbb0076f33ade8b273b813308167f9d8d45242b309a2d18320ca0093c scripts/migration/migrate_npm_api.py a459d95bbb0076f33ade8b273b813308167f9d8d45242b309a2d18320ca0093c scripts/migration/migrate_npm_api.py
31ccad3ec3c880119f3b7002f0c6652f8b43634edad8ab3763cb0e4497d0bb1f scripts/migration/migrate_npm_core.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 939287dadf7309689d29d2171757c2d80e91d898c3f9112454b943d08bd611fb scripts/performance/ab_1kb_dev.py
0380406a059ee9ec27473f8a46435f638f73a6dc729382ae1561c08dbf7ccb53 scripts/performance/benchmark.py 0380406a059ee9ec27473f8a46435f638f73a6dc729382ae1561c08dbf7ccb53 scripts/performance/benchmark.py
9ac220163256e08513597c397c016a48345b114d7876b439259cc310b36c83fc scripts/performance/checks_cache.py 9ac220163256e08513597c397c016a48345b114d7876b439259cc310b36c83fc scripts/performance/checks_cache.py
@@ -183,6 +184,7 @@ fa42ec0475e4398d647b3c094309b4332cd2b5649db223cae0b8e821d6b44a28 scripts/tests/
72185f59f31bf04a4e88d53fe2fca52509f3663cb5f40b1d3bc319a5f945ef23 scripts/tests/panel_html.mjs 72185f59f31bf04a4e88d53fe2fca52509f3663cb5f40b1d3bc319a5f945ef23 scripts/tests/panel_html.mjs
b0136544af717b75df2cb107900ea65c3f945fdd0040369dbb93919008fb954c scripts/tests/panel_html.py b0136544af717b75df2cb107900ea65c3f945fdd0040369dbb93919008fb954c scripts/tests/panel_html.py
8d1903e2b17b3d59cd0a5bec3e298e468b3d88ed2dda8b724239cc45d8353c59 scripts/tests/progressbar_0332_browser_test.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 2a6e75c05f3860acf65ea8132f0083904478b690145115ec017c120d70f26cca scripts/tests/proxy_hotpath_034_static_test.py
6fa2f22816996e989dad9dc7884adbf24f02f0ca6a6634b06e219eb08526c7fe scripts/tests/rps_hotpath_034_static_test.py 6fa2f22816996e989dad9dc7884adbf24f02f0ca6a6634b06e219eb08526c7fe scripts/tests/rps_hotpath_034_static_test.py
5be5cdc60324f4f0f6638640000b888a5babb0a9bbf29511c824a0ba38878c2a scripts/tests/sendfile_034_panel_browser_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 f55efa99245bbdbc98fe3202e52952d224ac3200612561580d52f6e251810049 scripts/tests/web_server_0330_ui_test.py
0dd1cef06a1c3e1546e2fb9b085bf369dfbed02107b9ab01b4fb4f3b894099c0 scripts/tests/web_server_0334_browser_test.py 0dd1cef06a1c3e1546e2fb9b085bf369dfbed02107b9ab01b4fb4f3b894099c0 scripts/tests/web_server_0334_browser_test.py
22e880e98c7b9d700c7a4c02b9b1d543e5dbd1f4ecdca42813a1083165b32a19 scripts/version.py 22e880e98c7b9d700c7a4c02b9b1d543e5dbd1f4ecdca42813a1083165b32a19 scripts/version.py
55485a513e8c4da2a58daa7abcb15b32ed408a5fc062cf23599fb83d16b90cfe src/acl.rs 9bf4e4a61824f82b773b4e8ffa0909b9cf0a42bfc14d695bd64da240cc634a61 src/acl.rs
63efab1e95d41134fc30cc57320c32810bbae9f81a640bdb24b1be2059e63d8a src/acl/dynamic.rs 63efab1e95d41134fc30cc57320c32810bbae9f81a640bdb24b1be2059e63d8a src/acl/dynamic.rs
4803e44de39b872a5b3b55f9ca9e81c066ba52cb8f6adb879fd57c5f94b93e89 src/acl/dynamic/tests.rs 4803e44de39b872a5b3b55f9ca9e81c066ba52cb8f6adb879fd57c5f94b93e89 src/acl/dynamic/tests.rs
a22e5ea6381a1eacafe650be0f5cd6e653773e31f6491a562fe0459a309fb7cc src/acl/tests.rs 5e77c7e8d9781f6a8ff3d3f09da4dbcd5277b241901ff17c6c8c2703f2abbcef src/acl/tests.rs
6f5d3137bf24451da05c1db9f0f9e2a451305db17b6bc2590815ad54a6511829 src/acme.rs 6f5d3137bf24451da05c1db9f0f9e2a451305db17b6bc2590815ad54a6511829 src/acme.rs
587097620b2f25ce7c6f59f907b47660184b095e3040c7615e74509a82e7cdd8 src/acme/jobs.rs 587097620b2f25ce7c6f59f907b47660184b095e3040c7615e74509a82e7cdd8 src/acme/jobs.rs
3f158f46e80b403b658b4e9b1e3710e38c70e47b2d476b8223d9c394ffeeba95 src/acme/model.rs 3f158f46e80b403b658b4e9b1e3710e38c70e47b2d476b8223d9c394ffeeba95 src/acme/model.rs
@@ -336,7 +338,7 @@ a9eeb93519aae2de7a1282296ced91d2eb8307cc966f8fbf6085af0fd21b0ad2 src/cache/cold
2eaba3261da2a547e9af91330319a3abd3a8ad8b5b44a337a84040720209017d src/cache/cold/startup.rs 2eaba3261da2a547e9af91330319a3abd3a8ad8b5b44a337a84040720209017d src/cache/cold/startup.rs
e7c7fd71727d542c2df3e03a4a1cba9d839c516963f66bbfde1c29ec81053500 src/cache/cold/worker.rs e7c7fd71727d542c2df3e03a4a1cba9d839c516963f66bbfde1c29ec81053500 src/cache/cold/worker.rs
d51fa4f320be56c572f767617428c3f3838543cb0eeb7d1f8ae791e1a1ce9a71 src/cache/collapse.rs d51fa4f320be56c572f767617428c3f3838543cb0eeb7d1f8ae791e1a1ce9a71 src/cache/collapse.rs
36febf4d27a2f5bbd72dab7f45d8b8dd64fb4f7b95060b9e0ebd5709d19749fa src/cache/dashboard.rs 563919c53d38e9b83505a5715469833569efb2a09e91a2b9a89c335187368dee src/cache/dashboard.rs
7eaf3947cfc89cc6ef55a64fef0b3fedf1d9a1c33322aba2b5170c0ea34a5f2c src/cache/fill.rs 7eaf3947cfc89cc6ef55a64fef0b3fedf1d9a1c33322aba2b5170c0ea34a5f2c src/cache/fill.rs
675d18afb53f25c56cba49bbb431fa39ecd9d7bdd192c7b39b0698225de7a2a2 src/cache/invalidation.rs 675d18afb53f25c56cba49bbb431fa39ecd9d7bdd192c7b39b0698225de7a2a2 src/cache/invalidation.rs
c1357a13658e1ed5158084d3a41ba9ea906512774a5863ce9c56cb508d3fa6c8 src/cache/invalidation/ban.rs c1357a13658e1ed5158084d3a41ba9ea906512774a5863ce9c56cb508d3fa6c8 src/cache/invalidation/ban.rs
@@ -359,7 +361,7 @@ e26dce02df9c043329ca164fed215d271781bce8fb03240f528331bf3df6b9d7 src/cache/poli
989df11e246123f760ef31689370f3500e0913fcc6b70d7ac0c2c95e2f412fb0 src/cache/policy/request.rs 989df11e246123f760ef31689370f3500e0913fcc6b70d7ac0c2c95e2f412fb0 src/cache/policy/request.rs
d6051d55168fe1db97980a54c22c708f4f325ba72bfc36f35a1851eb5dcc051d src/cache/policy/response.rs d6051d55168fe1db97980a54c22c708f4f325ba72bfc36f35a1851eb5dcc051d src/cache/policy/response.rs
7e2bcfbb2b57732cdc704d9df7a9298fb80e735e37984431bc936630e41e7f8a src/cache/range.rs 7e2bcfbb2b57732cdc704d9df7a9298fb80e735e37984431bc936630e41e7f8a src/cache/range.rs
6d7a4c22a7776a6538fb22d78806bec72efbe11aa5a26fb3ecf575ffd266ada0 src/cache/stats.rs cf2513043813421e10260cef63645fdb8c5249114f16d91eae9f919b0214242a src/cache/stats.rs
93bd3e9e5c1a045c784cee0e1fdb9e1c5d862ed70d5d91c54c5a769bca8fd949 src/cache/tests.rs 93bd3e9e5c1a045c784cee0e1fdb9e1c5d862ed70d5d91c54c5a769bca8fd949 src/cache/tests.rs
9183fa8e6086e92fb148fc278fd866d44e3dd988c848a071e32066e44a1cbf0a src/cache/tests/cold_tier_tests.rs 9183fa8e6086e92fb148fc278fd866d44e3dd988c848a071e32066e44a1cbf0a src/cache/tests/cold_tier_tests.rs
25ed7635bfd5e34de55b319bf163db9a0af26642664ce7685ebe1241bab52cac src/cache/tests/dashboard_tests.rs 25ed7635bfd5e34de55b319bf163db9a0af26642664ce7685ebe1241bab52cac src/cache/tests/dashboard_tests.rs
@@ -391,8 +393,8 @@ b45d6040810c93ae2acb04ef6b0a59c189751edf9008cd81a4521c9e04e7f07d src/docker.rs
4c2c988191a97f81568a8df64af772463d010844f1abf327871901cdb32d4336 src/docker/model.rs 4c2c988191a97f81568a8df64af772463d010844f1abf327871901cdb32d4336 src/docker/model.rs
5f4610c59b2d4ff6a205a2d9ae11bc84be689b2a68302459f546b3f43e53593c src/docker/reconcile.rs 5f4610c59b2d4ff6a205a2d9ae11bc84be689b2a68302459f546b3f43e53593c src/docker/reconcile.rs
a12c47df4b90b6fc36c978a42477e6ddffbbbf69aed7252101c2fd829709bd6e src/docker/tests.rs a12c47df4b90b6fc36c978a42477e6ddffbbbf69aed7252101c2fd829709bd6e src/docker/tests.rs
2a02ea2e010cde1b1fe99b2e6baa80ea344ea6b332dbb3c4be795b71541f64a7 src/forwarding.rs ed17a905bc249b4c36dfd07711a87bea9f385a01d16264f798e1bae932dc51a0 src/forwarding.rs
057e652f216a1478638cbb53b1c64ac88c7f413a65825caacbf893c5af004a63 src/forwarding/tests.rs b25fbb00de0e37ad5fa1a049839265e5f7e9f83b6214c40a95f57ea4807685bc src/forwarding/tests.rs
af7a4e91c32ffbe4701c8c06e48ba3f8bbbe55e8b9e877aecb30ccdf858e9e8f src/geoip.rs af7a4e91c32ffbe4701c8c06e48ba3f8bbbe55e8b9e877aecb30ccdf858e9e8f src/geoip.rs
80850f7acd1ed4c0048b065e9e81740869a8efbb0d9b5d2e83d8ec7fbc494703 src/geoip/mmdb.rs 80850f7acd1ed4c0048b065e9e81740869a8efbb0d9b5d2e83d8ec7fbc494703 src/geoip/mmdb.rs
06aa056d789742cc11c8826c338d985e6a6fb22da6c3530937eec38e35478034 src/geoip/mmdb_tests.rs 06aa056d789742cc11c8826c338d985e6a6fb22da6c3530937eec38e35478034 src/geoip/mmdb_tests.rs
@@ -440,25 +442,25 @@ e03f0d6b3f35dae6b98c394837aa0d17e4d73a8306767b4c607868bc00c30286 src/prometheus
fb2d8d45cd2e947f783eb981b37fa418c39fcb327a3af17a648fadea7f37f888 src/prometheus/web_php.rs fb2d8d45cd2e947f783eb981b37fa418c39fcb327a3af17a648fadea7f37f888 src/prometheus/web_php.rs
2c97367686b54989573ce7fb81b2c6dde72adb254df9522bc05ded74f1fbffb6 src/proxy/body.rs 2c97367686b54989573ce7fb81b2c6dde72adb254df9522bc05ded74f1fbffb6 src/proxy/body.rs
c9b76abd91f0dfe81e07073e89d35faa033c19e11fa88b223f075428542284f1 src/proxy/body/tests.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 aa49c9928bb91e3d5bae0d340d88396a0417b9edec6192d41e1403533319a897 src/proxy/cache_http_invalidation.rs
676c1b325fd8e55bcdd86451c7d1d8b8d347ca00e8da6b3e5b148a58c376a9b4 src/proxy/cache_plan.rs 676c1b325fd8e55bcdd86451c7d1d8b8d347ca00e8da6b3e5b148a58c376a9b4 src/proxy/cache_plan.rs
3404b03096862de1050088f62cdd811ea1ab61d191f902501173107f81f23b9b src/proxy/cache_plan/tests.rs 3404b03096862de1050088f62cdd811ea1ab61d191f902501173107f81f23b9b src/proxy/cache_plan/tests.rs
a8848cbdf85368548b003af2b43823c453866c472af4c7d2ff0ec7da14c44c8d src/proxy/error_fallback.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 cfa941b60b5d167ec9d8e2566ea3342b588d1a9078a6ca711ad6062ddf00fec9 src/proxy/forward/response.rs
7a5f43fe65e5a746eb84881da0bf784215081ead66d18faa7780533ad5df9c8c src/proxy/forward/route_cache.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 0a921474ef2b93eabdaf3888651cf29130eace5ccb1e0f5d6bc25e6aa69280be src/proxy/forward/success_prepare.rs
bb24da9c9ab986a9c4d4144d4d1be0f661561d4afc755f298b58bc679b182850 src/proxy/forward/upstream.rs bb24da9c9ab986a9c4d4144d4d1be0f661561d4afc755f298b58bc679b182850 src/proxy/forward/upstream.rs
11dccc239ac8a8e9ea06e884c2f2658f86b7807d14a59b8e052bc7dd75e86d63 src/proxy/forward/web_source.rs bb8c9117e05b0cb4bbfe029ef4d1a031fc755cdce255b15dad072afd1c845737 src/proxy/forward/web_source.rs
facde401ba5bcbafdcea5524f236cfae37eec11144ceeea879e83314c84bc97e src/proxy/forward_entry.rs 2b6ae70b84fd1eba0dba6d8d7e2db62227193dc5b77e4a6111ceab1b219800c2 src/proxy/forward_entry.rs
9b25f5e3ea4443a63ee6f45d12e1957dd316fe8e85325fc00ea416faaab1025a src/proxy/h2_graceful.rs 9b25f5e3ea4443a63ee6f45d12e1957dd316fe8e85325fc00ea416faaab1025a src/proxy/h2_graceful.rs
3e856a2e3e2236caebf4a7e7e8aba229a529cc552c1fb73e4b81c38784ecad26 src/proxy/headers.rs 31e925a408ba6273deb0793df5f5f82eeabbcaffb365cc0457ed9c67d9200f34 src/proxy/headers.rs
02542edb25302ab4778d5f84f900b546ed126be56e4bc9d3580703edea6f492c src/proxy/headers/tests.rs 013f0495127e351cf535a928526b56bd3a63202d8abd0fa59da5dfb8dc783f27 src/proxy/headers/tests.rs
3f25429418941eb72c3847125340db74684ad316eb278ecf658953026fd41f4f src/proxy/mod.rs 3f25429418941eb72c3847125340db74684ad316eb278ecf658953026fd41f4f src/proxy/mod.rs
55133ceace2e5262d9c54fa41bea099a27b817d1e09c1cd0e2493fd9cee4da38 src/proxy/redirect_helpers.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 d2df55b6ccfc7ceb8a3d2c49f664acd183448e3d932d5399d6692e87f79a094b src/proxy/request_id/tests.rs
e56c65c85e42ee4fe6adf3655f3b2d176798e37c648db6bfb8e1602300b723eb src/proxy/tests/authority_tests.rs e56c65c85e42ee4fe6adf3655f3b2d176798e37c648db6bfb8e1602300b723eb src/proxy/tests/authority_tests.rs
1aa9bbbfb2659facdaeaf185e11221b1c8cd5fe6e23c7c1dacd3aa55e3db4fcb src/proxy/tests/cache_fast_path_tests.rs 1aa9bbbfb2659facdaeaf185e11221b1c8cd5fe6e23c7c1dacd3aa55e3db4fcb src/proxy/tests/cache_fast_path_tests.rs
+15
View File
@@ -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 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 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**. 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.
+88
View File
@@ -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<str>` 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<HeaderName>` 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.
+7
View File
@@ -1,5 +1,12 @@
# Live performance and regression harness # 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. These scripts exercise actual RustProxy executables through loopback sockets.
They do not simulate Rust request handling. The Python server in `peer.py` is a 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, controllable test **upstream**, used to verify streaming, non-replayable writes,
+37
View File
@@ -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<HeaderName>' 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)')
+26
View File
@@ -166,6 +166,32 @@ pub fn validate_decoded_routing_path(path: &str) -> Result<()> {
Ok(()) 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<std::borrow::Cow<'_, str>> {
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. /// Routing/ACL use one decoded representation; upstream still receives the original path.
/// Reject encoded separators, percent (double decoding), controls, ambiguous dot segments, /// Reject encoded separators, percent (double decoding), controls, ambiguous dot segments,
/// matrix parameters and malformed UTF-8. Repeated literal separators are /// matrix parameters and malformed UTF-8. Repeated literal separators are
+17
View File
@@ -255,4 +255,21 @@ mod tests {
assert!(routing_path(path).is_err(), "{path}"); 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());
}
} }
+3 -3
View File
@@ -5,7 +5,7 @@ impl Cache {
.lock() .lock()
.unwrap_or_else(|e| e.into_inner()) .unwrap_or_else(|e| e.into_inner())
.clone(); .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 n = |value: &AtomicU64| value.load(Ordering::Relaxed);
let hits = n(&self.counters.hits); let hits = n(&self.counters.hits);
let misses = n(&self.counters.misses); let misses = n(&self.counters.misses);
@@ -243,7 +243,7 @@ impl Cache {
count count
} }
pub fn scope_view(&self, scope: &str) -> ScopeView { 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 { let mut view = ScopeView {
entries: 0, entries: 0,
resident_bytes: 0, resident_bytes: 0,
@@ -269,7 +269,7 @@ impl Cache {
self.domain_stats.record_reason(scope, domain, reason); self.domain_stats.record_reason(scope, domain, reason);
} }
pub fn dashboard(&self, scope: Option<&str>, offset: usize, limit: usize) -> Dashboard { 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 revision = store.revision;
let mut groups: BTreeMap<(String, String), DomainView> = self let mut groups: BTreeMap<(String, String), DomainView> = self
.domain_stats .domain_stats
+4
View File
@@ -58,6 +58,10 @@ impl DomainStats {
fn record_reason(&self, reason: &'static str) { fn record_reason(&self, reason: &'static str) {
// Cache OFF is the highest-volume case; avoid reason map lookup and // Cache OFF is the highest-volume case; avoid reason map lookup and
// any shared lock/allocator activity for its dashboard reason. // 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) { if let Some(index) = FAST_REASONS.iter().position(|label| *label == reason) {
self.fast_reasons[index].fetch_add(1, Ordering::Relaxed); self.fast_reasons[index].fetch_add(1, Ordering::Relaxed);
return; return;
+28 -5
View File
@@ -217,11 +217,34 @@ impl Compiled {
) -> Result<Context> { ) -> Result<Context> {
let peer_ip = canonical(peer.ip()); let peer_ip = canonical(peer.ip());
let trusted_peer = self.trusted(peer_ip); let trusted_peer = self.trusted(peer_ip);
let mut ips = if trusted_peer { if !trusted_peer {
chain(incoming).unwrap_or_default() let scheme = if tls { "https" } else { "http" };
} else { let authority_text = authority.to_str()?;
Vec::new() 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); ips.push(peer_ip);
// Discard everything left of the first untrusted hop walking from us. // Discard everything left of the first untrusted hop walking from us.
let first = ips.iter().rposition(|ip| !self.trusted(*ip)).unwrap_or(0); let first = ips.iter().rposition(|ip| !self.trusted(*ip)).unwrap_or(0);
+29
View File
@@ -200,4 +200,33 @@ mod tests {
.unwrap(); .unwrap();
assert_eq!(c.values["x-forwarded-for"], "203.0.113.8, 104.16.1.2"); 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::<IpAddr>().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");
}
} }
+2 -2
View File
@@ -172,9 +172,9 @@ fn response_context(
let authority = effective_host(request, tls).ok()?; let authority = effective_host(request, tls).ok()?;
let host = request_host(authority.to_str().ok()?).ok()?; let host = request_host(authority.to_str().ok()?).ok()?;
let frontend = snapshot.frontend(&host, local, tls).cloned(); 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() .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)) Some((frontend, route))
})(); })();
context.unwrap_or((None, None)) context.unwrap_or((None, None))
+3 -3
View File
@@ -42,7 +42,7 @@ if let Some(ticket) = &cache_ticket {
uri: uri.clone(), uri: uri.clone(),
public_suffix: public_suffix.clone(), public_suffix: public_suffix.clone(),
host: host.clone(), host: host.clone(),
path: path.clone(), path: path.as_ref().to_owned(),
scope: scope.to_owned(), scope: scope.to_owned(),
timeout: frontend timeout: frontend
.and_then(|frontend| frontend.proxy_timeouts.response_header_timeout()) .and_then(|frontend| frontend.proxy_timeouts.response_header_timeout())
@@ -79,7 +79,7 @@ if let Some(ticket) = &cache_ticket {
uri: uri.clone(), uri: uri.clone(),
public_suffix: public_suffix.clone(), public_suffix: public_suffix.clone(),
host: host.clone(), host: host.clone(),
path: path.clone(), path: path.as_ref().to_owned(),
scope: scope.to_owned(), scope: scope.to_owned(),
timeout: frontend timeout: frontend
.and_then(|frontend| frontend.proxy_timeouts.response_header_timeout()) .and_then(|frontend| frontend.proxy_timeouts.response_header_timeout())
@@ -240,7 +240,7 @@ if handshake.is_none()
uri: uri.clone(), uri: uri.clone(),
public_suffix: public_suffix.clone(), public_suffix: public_suffix.clone(),
host: host.clone(), host: host.clone(),
path: path.clone(), path: path.as_ref().to_owned(),
scope: scope.to_owned(), scope: scope.to_owned(),
timeout: frontend timeout: frontend
.and_then(|frontend| frontend.proxy_timeouts.response_header_timeout()) .and_then(|frontend| frontend.proxy_timeouts.response_header_timeout())
+1 -1
View File
@@ -170,7 +170,7 @@ if let (Some(ticket), Some(plan), Some(transformed)) =
uri: active_uri.clone(), uri: active_uri.clone(),
public_suffix: public_suffix.clone(), public_suffix: public_suffix.clone(),
host: host.clone(), host: host.clone(),
path: path.clone(), path: path.as_ref().to_owned(),
scope: scope.to_owned(), scope: scope.to_owned(),
timeout: frontend timeout: frontend
.and_then(|frontend| frontend.proxy_timeouts.response_header_timeout()) .and_then(|frontend| frontend.proxy_timeouts.response_header_timeout())
+1 -1
View File
@@ -7,7 +7,7 @@
} }
let service_path = match route.rewritten_path(&path) { let service_path = match route.rewritten_path(&path) {
Ok(Some(value)) => value, Ok(Some(value)) => value,
Ok(None) => path.clone(), Ok(None) => path.as_ref().to_owned(),
Err(_) => return error(StatusCode::BAD_REQUEST, Err(_) => return error(StatusCode::BAD_REQUEST,
"RustProxy: invalid Web Server rewrite path\n"), "RustProxy: invalid Web Server rewrite path\n"),
}; };
+12 -1
View File
@@ -101,7 +101,12 @@ async fn forward_request(
.extensions() .extensions()
.get::<InternalFallbackRoute>() .get::<InternalFallbackRoute>()
.cloned(); .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( return error(
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
"RustProxy: ambiguous request path\n", "RustProxy: ambiguous request path\n",
@@ -216,10 +221,16 @@ async fn forward_request(
cloudflare_country = crate::forwarding::cloudflare_country(request.headers(), peer.0); cloudflare_country = crate::forwarding::cloudflare_country(request.headers(), peer.0);
} }
} }
// `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::<Arc<Exchange>>() { if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
*exchange.client_ip.lock().unwrap_or_else(|e| e.into_inner()) = *exchange.client_ip.lock().unwrap_or_else(|e| e.into_inner()) =
Some(context.client_ip.to_string()); Some(context.client_ip.to_string());
} }
}
// GeoIP stays off the normal request path. Resolve lazily for any vhost or // 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. // location policy that actually needs a country code, or for log capture.
let capture_country = state.logs.capture_country(); let capture_country = state.logs.capture_country();
+12
View File
@@ -4,6 +4,17 @@ use http::{
}; };
/// Remove both standard hop-by-hop fields and fields named by Connection. /// Remove both standard hop-by-hop fields and fields named by Connection.
pub fn strip_hop_by_hop(headers: &mut HeaderMap) { pub fn strip_hop_by_hop(headers: &mut HeaderMap) {
// 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<HeaderName> = headers let nominated: Vec<HeaderName> = headers
.get_all(CONNECTION) .get_all(CONNECTION)
.iter() .iter()
@@ -14,6 +25,7 @@ pub fn strip_hop_by_hop(headers: &mut HeaderMap) {
for name in nominated { for name in nominated {
headers.remove(name); headers.remove(name);
} }
}
for name in [ for name in [
"connection", "connection",
"keep-alive", "keep-alive",
+30
View File
@@ -52,4 +52,34 @@ mod tests {
restore_admin_cookie(&mut h, &admin); restore_admin_cookie(&mut h, &admin);
assert_eq!(h.get("cookie").unwrap(), "a=1; b=2; rustproxy_session=secret"); 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");
}
} }
+7 -5
View File
@@ -13,12 +13,14 @@ fn request_id_hash(sequence: u64) -> String {
fn request_id_hex(digest: &[u8]) -> String { fn request_id_hex(digest: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef"; const HEX: &[u8; 16] = b"0123456789abcdef";
let mut encoded = String::with_capacity(digest.len() * 2); // Encode two ASCII bytes per digest byte in one allocation; avoid 64
for &byte in digest { // separate String::push bounds checks for the 32-byte SHA-256 digest.
encoded.push(HEX[(byte >> 4) as usize] as char); let mut encoded = vec![0u8; digest.len() * 2];
encoded.push(HEX[(byte & 0x0f) as usize] as char); 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)] #[cfg(test)]