v0.34.3
This commit is contained in:
1 parent
8c0de520d7
commit
8f41b39c26
32 files changed
+934
-128
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# RustProxy source and documentation maintenance
|
||||
|
||||
Current source version: **0.34.2**. The Cargo package and binary are named `rust-proxy`.
|
||||
Current source version: **0.34.3**. The Cargo package and binary are named `rust-proxy`.
|
||||
|
||||
Before editing, read [README.md](README.md), [ROADMAP.md](ROADMAP.md), the relevant [documentation guide](docs/README.md) and the actual code and tests. Historical release experiments are not current specifications. Use the bundled `src/admin/openapi.json` for API routes and payloads; the source implementation is authoritative for runtime behavior.
|
||||
|
||||
|
||||
+9
-1
@@ -6,7 +6,15 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
Release history focused on **features, settings, default behavior, compatibility and fixes**. Changes to internal code structure and preliminary performance experiments are intentionally omitted. Historical descriptions reflect their respective releases; consult the current documentation for the settings available in 0.34.2.
|
||||
Release history focused on **features, settings, default behavior, compatibility and fixes**. Changes to internal code structure and preliminary performance experiments are intentionally omitted. Historical descriptions reflect their respective releases; consult the current documentation for the settings available in 0.34.3.
|
||||
|
||||
## 0.34.3 — QUIC socket buffering, live HTTP/3 settings and cache outcome mix (source candidate)
|
||||
|
||||
- QUIC now opens the UDP listener socket with per-listener receive/send buffer targets (4 MiB each by default; `udp_receive_buffer_bytes`, `udp_send_buffer_bytes`; `0` uses OS defaults). Reports the effective buffer sizes and warns if kernel limits clamp them. Linux `SO_RCVBUF` / `SO_SNDBUF` accounting is accommodated, so observed `ss` values correspond to the configured effective target when permitted. UDP buffer modifications require restart, not silent hot application. Existing configs without these fields load the new defaults.
|
||||
- HTTP/3 `Alt-Svc` follows newly saved listener lifetime immediately on H1/H2 TLS responses for an already-bound H3 socket; disabling H3 publishes `Alt-Svc: clear`. New QUIC handshakes use saved stream/idle limits and reduced connection caps. Listener binding/TLS/H1-H2 changes, a new/disabled UDP listener or expanding startup capacity still need restart; UI restart indicator reflects this.
|
||||
- DEBUG-level bulk QUIC snapshots expose packet loss, congestion window, RTT, MTU and datagram syscall counters to investigate slow H3 and CPU usage, without assuming an unmeasured throughput improvement.
|
||||
- Overview adds a **recent HTTP cache outcomes** 100% stacked bar with HIT, MISS, BYPASS and OTHER (requests with no cache decision); retains the original lifetime HIT/MISS donut. Samples exclude pauses/resets and do not normalize invalid counter differences into misleading percentages. The widget is included in the dashboard registry and factory preset but **hidden by default**; administrators can enable it in the layout editor. Saved layout visibility is preserved. Both UI languages are supported.
|
||||
- Delivered as source for compilation and protocol acceptance on supported targets; no compiled Rust binary or measured post-patch benchmark is included in this candidate.
|
||||
|
||||
## 0.34.2 — configuration storage visibility and public HTTP error responses
|
||||
|
||||
|
||||
Generated
+2
-1
@@ -1881,7 +1881,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-proxy"
|
||||
version = "0.34.2"
|
||||
version = "0.34.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -1917,6 +1917,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sha1",
|
||||
"sha2",
|
||||
"socket2",
|
||||
"sqlx",
|
||||
"subtle",
|
||||
"sysinfo",
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rust-proxy"
|
||||
version = "0.34.2"
|
||||
version = "0.34.3"
|
||||
edition = "2021"
|
||||
rust-version = "1.99"
|
||||
description = "RustProxy - independent reverse proxy with local administration"
|
||||
@@ -56,6 +56,7 @@ zstd = "0.14"
|
||||
|
||||
h3 = { package = "hpx-h3", version = "=2.5.20", features = ["quinn"] }
|
||||
quinn = { version = "0.11.12", default-features = false, features = ["runtime-tokio", "rustls-ring"] }
|
||||
socket2 = "0.6.5"
|
||||
|
||||
metrics-process = { version = "=2.4.3", default-features = false, features = ["dummy"] }
|
||||
sysinfo = { version = "0.39", default-features = false, features = ["network", "system"] }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# RustProxy 0.34.2
|
||||
# RustProxy 0.34.3
|
||||
|
||||
RustProxy combines a reverse proxy, static/PHP web server, certificate management and traffic monitoring in one service with a browser administration panel. It supports several domains and backends without requiring a separate proxy configuration language.
|
||||
|
||||
@@ -16,6 +16,12 @@ RustProxy combines a reverse proxy, static/PHP web server, certificate managemen
|
||||
|
||||
Features and default settings vary by component; configure only the capabilities you need. The [documentation index](docs/README.md) covers each workflow, including prerequisites, security boundaries, troubleshooting and API access.
|
||||
|
||||
## 0.34.3
|
||||
|
||||
- HTTP/3 now explicitly requests **4 MiB effective UDP receive and send buffers** per QUIC listener (configurable as `udp_receive_buffer_bytes` and `udp_send_buffer_bytes` in Settings > HTTP/3). On Linux the kernel can cap them using `net.core.rmem_max` / `net.core.wmem_max`; RustProxy logs the effective values and a warning if the target is not met. Setting either value to `0` leaves the OS default. Changing UDP buffers requires service restart.
|
||||
- HTTP/3 Alt-Svc response headers follow saved policy immediately; turning off an already-bound H3 service emits `Alt-Svc: clear`. New QUIC handshakes pick up modified stream/idle limits without socket rebinding. Increasing the connection cap beyond the startup bound or changing the UDP endpoint still requires restart. QUIC bulk-transfer diagnostic snapshots are available at debug log level.
|
||||
- The Overview adds an **optional, hidden-by-default 100% horizontal cache-outcome bar** showing recent HIT, MISS, BYPASS and OTHER HTTP requests. Enable it in the dashboard layout editor when needed. This complements the existing lifetime HIT/MISS donut. OTHER denotes HTTP requests that did not produce a cache decision during the sample. The chart does not claim a cache hit-rate for all requests.
|
||||
|
||||
## 0.34.2
|
||||
|
||||
- Added read-only pull-based JSON monitoring at `/api/metrics/json` (admin session or General API token) and at the enabled Prometheus exporter path with `.json` suffix (same exporter bearer/CIDR policy). Includes Overview-style metrics and all existing Prometheus sample families without SSE. Compact precomputed dashboard metrics are available at `/api/metrics/summary`, under `summary` in the full JSON, and at the exporter path with `.summary.json` suffix. This includes readiness counts, RPS, transfer/second and system/cache percentages.
|
||||
@@ -62,4 +68,4 @@ The authenticated **`/docs`** page includes the guides and a separate interactiv
|
||||
|
||||
## Status of this source archive
|
||||
|
||||
Version **0.34.2** is delivered as a **source package**. This archive includes regression tests for configuration, public HTTP error handling and the JSON monitoring API, but native compilation, full Rust tests and target-system acceptance are required before production use. See [Installation](docs/INSTALL.md) and [Roadmap](ROADMAP.md).
|
||||
Version **0.34.3** is provided as a **source release candidate**. The archive includes the 0.34.2 tests plus HTTP/3 socket-buffer and live-settings regressions, and a browser chart calculation test. Native compilation, full Rust tests and target-VM throughput testing must pass before a production release can be declared. See [Installation](docs/INSTALL.md) and [Roadmap](ROADMAP.md).
|
||||
+30
-29
@@ -1,12 +1,12 @@
|
||||
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
|
||||
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
||||
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
|
||||
65441f53fe87a21b1b960ddd7b76a6346a7ab97d6c75ba71c04a1d28504a7ee0 AGENTS.md
|
||||
03a75f7f891aa9ed5e09aa2461510e4b021be397ca0dad7533fb95bd50d42aea CHANGELOG.md
|
||||
b8d1eceeb2e8aa506da7d5675307a3ca7f101bc7a78a8cc00bb3b441a4dd85a9 Cargo.lock
|
||||
55788c48310d81442f3a937de1a42e67cae7482f88ec4f1699b50e37f6f56acb Cargo.toml
|
||||
00e914cd6a8f8962d857b92bdfa172aa7ce0bdbcafcebe8c9c67f452ac8479f2 AGENTS.md
|
||||
58b2500a3ddf9893f48b4ef881a308229c74665fb160d6189383fd5fe8b848b6 CHANGELOG.md
|
||||
789f77a2ecb443f171197d002c0911779bf1a8d8f0c151a18a6a4ee54d8c97eb Cargo.lock
|
||||
b6cf2ee2017e6a0ad99f66a809713f031b7b5480a4c97d74e2ed2c07c5c4f210 Cargo.toml
|
||||
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
|
||||
ef845b5a97d7b62b3fe765a3fac5789f708109c50a5358a605b63b185b325322 README.md
|
||||
1ada0bf3146cd5f76b78a9ef21d03ade4bd5d08dbe2b9c89111aceda34a8ae1a README.md
|
||||
ae6d14ada6fde7d0b8514840b821678418cf5b1816c18434e3b7fb2ae27a0b21 ROADMAP.md
|
||||
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
|
||||
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
|
||||
@@ -33,10 +33,10 @@ b0eaa2cfe4f6e86582f67891e457591ae650d15145e025d85cab43e37c883ac8 docs/ACCESS.md
|
||||
2362ab34020d4e0dcd70e280e9539ef7764b63212856ee08a3a55906c48f6421 docs/API_ACCESS.md
|
||||
728b15b9825f547f34c4121219784798dd046fbbdd72ece8906d0cd77e8efc64 docs/BACKUP.md
|
||||
7e6a7b9421541c94181a712d17f6166a328e7e28c5a1d629e7d335c0ae5aa1de docs/CACHE.md
|
||||
ede69043675c78151bb199a393ae20041047571f7f4bc5e1a2c2c3b8d8c01727 docs/CONFIGURATION.md
|
||||
e6d9feabc7a4fcb9a56a2028accbab19ca44479ee67deeefd0ab37f29c45cf4a docs/CONFIGURATION.md
|
||||
80bcc666736b10f6202386d0342e8d6feeba858d77aea310bf3ec6de01c2bfb3 docs/CONTAINER.md
|
||||
dc670d912a6a946160c2f8528c9bb710ec38ebe406dcb7dcaf18fb2653f667b6 docs/CUSTOM_LOCATIONS.md
|
||||
990153083a982ba820f5b9f475a5fc657b3613d8f1af11c2d34f6483dc7449cf docs/DASHBOARD.md
|
||||
1f53baf1a2dbea19e155fc44394703eb3e0d4a698af7550bd0041fd95e127cd4 docs/DASHBOARD.md
|
||||
5a8d311c7ab36af9fb84964585bd974b92d8ea4979829b5735fb834ef10156ce docs/DOCKER_DISCOVERY.md
|
||||
dbae1a739874ac7b61e276c1ad6fd7c3465acc95afa4db3751224c8426ad6569 docs/INSTALL.md
|
||||
37001cbf8ed248ca05ec40f12dd9876d33f6e0c97dcb21eb2e6f53c3d97a60bb docs/INTEGRATIONS.md
|
||||
@@ -44,12 +44,12 @@ dbae1a739874ac7b61e276c1ad6fd7c3465acc95afa4db3751224c8426ad6569 docs/INSTALL.m
|
||||
888cdc8c56e10b8a7615012e192e085bf2899d0ec9b3ddfb5731e7de6ab5bcde docs/MIGRATION.md
|
||||
e4ffd659be1e6c445b582a76643a46408699ef1e929d3a4c6e741d8146ded031 docs/MONITORING.md
|
||||
d5894c07c68ec4230987767198434abee13b0e525842fea21bcbe752fa82742b docs/PHP_FPM.md
|
||||
1e8eada9d90fab1671cc137e2884656a13589c78cc11620b98b821eb5ebd1331 docs/README.md
|
||||
6cd363d5679d5da50069b545f9b3084fcf9838dc7be5d6cd108c356f2f94b905 docs/README.md
|
||||
7c47aed9c178e8c707b98d1b20ec985c1170f193de0c7b10b682e76da576e764 docs/RESPONSE_DELIVERY.md
|
||||
701023a4cdcac145dcb2bc5bb709afe79599a5d70cbda6fdfe19ab39ad703bfc docs/SECURITY.md
|
||||
519f9c45fe31ede5afdfb8c4e4ef05f17540843ac93fe8b05ff6c4fbea7bcf68 docs/STREAMS.md
|
||||
f2f8916e5782bc9d4deef3405c36eeddcc1c1dde5198f9b415407c0351bf364e docs/TRANSPORT.md
|
||||
f4c23ee07723a78e1e104991c99bac6be61d72b7b912141e69cffffddf7232fe docs/TROUBLESHOOTING.md
|
||||
890b3f014fff3aa88167c2e8d4159bab7753a521b8ab536c5f6473fa834e569f docs/TRANSPORT.md
|
||||
8542c4a5c9d5dfe474459099546ae590b4c4d3f363f0bba9721c6107643c2d3a docs/TROUBLESHOOTING.md
|
||||
23765b0d3e32766966e922aa79951b8bc94fc93c0ffa38824e9dfc7e6cd4a1aa docs/VHOSTS.md
|
||||
f80fe921f0e93f1833ade5740b102c548210519b3b792d386762a57b92ea54cc docs/WEB_SERVER.md
|
||||
5502e8ce44276213803b66c58e3a4d804a1354f5ed5d83f47ee805e8c44ab088 examples/acl-headers.json
|
||||
@@ -76,8 +76,8 @@ cb01fdef286f38b442dd9252c23c4f8e44eb72e9ecdd99adb926e8e9f6318bb1 integrations/r
|
||||
96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh
|
||||
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh
|
||||
13189fa546c3488dddd8efc80c47f6da5241e5fc28b79cbfe795414ea05c7a4f langs/README.md
|
||||
c928fa3466bb66ec312790a72fafa617cbe443577825410d2f7e3852f5ec32f8 langs/en_US.json
|
||||
1627109d6dace0c35cd89fecc832aaa7d046acda6aa02b116e3c2190e74a1d84 langs/pl_pl.json
|
||||
bf33b38fd6690f20a76ddeab0cdfa52772158437ae8678be2d8e666984da64c3 langs/en_US.json
|
||||
9c18537f67ecdfa110f13e8a0271566ff2e3ed46dcc9232d2c4cf2b2d7d56328 langs/pl_pl.json
|
||||
a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py
|
||||
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
|
||||
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
|
||||
@@ -142,20 +142,20 @@ c6b552cd5acd9e743c28b1a5564420c214c619fe891ab0561b7d4499aae8d6c0 src/admin/file
|
||||
c720a7cad6a15c57b1f2ae7b3e5fbf30a97c64afb9137915f37b23f4e576bdc5 src/admin/file_logs_query.rs
|
||||
4e15827a896c30e8aeba4677bc138695cce61c9feb847de0f3de9d66f542ff7a src/admin/logging_api.rs
|
||||
8d1cf7855e2b95798819af33a47a3209cf2438a6108c814cd291fe2ca89fa006 src/admin/mod.rs
|
||||
4ef9b39fdde727e44c1ab1b7907cf5dc4d477c26d4d98da54cd482a2dfa6d436 src/admin/openapi.json
|
||||
fe938fa4c606daf4dd0e501e97f7f143f0cc0fd83b33a87b43d20fe1c62ef431 src/admin/openapi.json
|
||||
d9120f7dc61d27621c5ac27ea49815dc7945c44d5b99d208da503818c198f649 src/admin/operations.rs
|
||||
30895e05c5d5d0a26be29f667e0f6d72f16a96a5c8a98e09cd11d8ef2d7b4763 src/admin/panel_assets.rs
|
||||
75bbedd54fdabb2b489642a642ee7c6533f69cdacb85e251763a56ba11174d5c src/admin/panel_content.rs
|
||||
92b3e05f04711d8001b56816405859631d9e917868e15be965c7e76ef4ddef99 src/admin/preferences.rs
|
||||
4a5cb4c3a3aa8b9a519020854506ce4be5e688a6c4298260322ae641dded7fe9 src/admin/preferences_columns.rs
|
||||
9a3f13e2477769ae680c03d63336880c3b7b7d5aba4a91e44390a976608b9b1d src/admin/preferences_tests.rs
|
||||
668cab06d8c8ddbeda2595bcb7aec236e0c56b22ca6adfe9f19c3d160de0fb39 src/admin/preferences_tests.rs
|
||||
4a7a2017ba57c9e7a8b29e73764d439f6d03653403909ef198ded8c48d152784 src/admin/public.rs
|
||||
7a00e4ab943f74d2b1bdcbfab7f9cd3f584e9f59536b4aca6c4c31f5f5508d12 src/admin/public/tests.rs
|
||||
9926827131254677a848c178c80850b3ca1993c05b4bf913687dc0c7992ad048 src/admin/refresh_preferences.rs
|
||||
48842cddf9a0e5b1da57988af824da96c81258f4c870b31641e0a45d1ed61ece src/admin/router.rs
|
||||
0ca8c45132b11c926967a0f78fc7e5e36c379a99597e13096ad453140c372cc2 src/admin/session.rs
|
||||
66111616ccb59a561003c723554c25ba35925afbef7f9b73e5479ca8f0b35b54 src/admin/status.rs
|
||||
4b8a0483ede651d08489a4d6e5bb616f06423094bbbefcee70b8755d65c04398 src/admin/status_live.rs
|
||||
698dbe70871e599c98e8c54c226bfeba00f2ba6e2f71a0da13b4b502d7721685 src/admin/status.rs
|
||||
6d3224349d08b1052ca66eb153aba513de53bcb666c6f57f00075509c6b6cc47 src/admin/status_live.rs
|
||||
c97881b17b7a42511868b0e48e2f25aab6fb5b613d538f5e1f1967508454e486 src/admin/status_system.rs
|
||||
cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs
|
||||
5e52252a8e886d8e28eed1d456cdfb7387ea6a193007043662889ff366f6d6f5 src/admin/tests.rs
|
||||
@@ -175,15 +175,15 @@ d997f62e8bd7ac9d4f16bb302800c98ad943afab663bb0b212ea26c35b58de11 src/admin/web/
|
||||
5bde5f0881320464eb133cd44de9eff5211e6816e79f7f7e9574b16784fab126 src/admin/web/app/preferences.js
|
||||
8e75b0915edfbadefc4388701431541f4d35fd5d717363297b63740b3afd34d2 src/admin/web/app/rate-sampling.js
|
||||
43f4dddd1c3ea7fa42eeb9d7b655a8db48b5ddcc6c68213e08dfc2fcfbe8b17d src/admin/web/app/refresh.js
|
||||
4d683c98d3adb7ae5a0a1bc9219ee8617868c9e06632ce389d2c6ed9c179b766 src/admin/web/app/runtime.js
|
||||
86cd1d5b2a6d12e329e9fbd92c825e53ab4aa6286ff7cf484610e81c4f44ba70 src/admin/web/app/runtime.js
|
||||
31201586925e6f4792071d7d03fb05dcc4a69a03c02a4e85eb0746f45af68074 src/admin/web/app/session-resume.js
|
||||
86ac42bd4a6a84bb137d5a3c3dc3d1f4e404a543447845e6ea6e6e8210e268ce src/admin/web/app/settings.js
|
||||
8386710827e5ccc9432fb46413368f94afc1016425404a6092b460903636837e src/admin/web/app/stream-dashboard.js
|
||||
7f89bfae66e2a08a05a112733864e908f036bb1ee3d8b5b9ff23783b870acd96 src/admin/web/app/web-overview.js
|
||||
849b2ca9d183bdf6cb1e04651cda52da55b8a631e11ab1d827a052c8436bbd76 src/admin/web/cache-panel.js
|
||||
02a9f5a61379e68e1723e64ef4c589500e9a993adcb02fe352f1cf9456c90c52 src/admin/web/certificate-api.js
|
||||
aec5755244242424e5091ff57c0258d2618588618fe1d52d36f081d03879f686 src/admin/web/dashboard-defaults.json
|
||||
797e2935a34b974cb0e07a88056c31e6948cc11385bf2a076d902a332882d0a0 src/admin/web/dashboard-widgets.json
|
||||
ec73368c1297d807fad53349c73fc3b94bc2f25a7043058057a8b38644f67167 src/admin/web/dashboard-defaults.json
|
||||
7da696305e693c29ecf0ff463a7966128018f17ee8d8bdada295bd8bf87e9b05 src/admin/web/dashboard-widgets.json
|
||||
50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html
|
||||
d01a2e48f344fe074c1e63ee0a9bbf7151dfe26140690418535197e13a2a16d7 src/admin/web/docker.js
|
||||
d4aef376cb44b2a13174dca1cb9383f64c04b0b93279f4f0b4805e76002548a1 src/admin/web/docs.css
|
||||
@@ -205,20 +205,20 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/
|
||||
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
|
||||
7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.js
|
||||
b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js
|
||||
1c72ceae6c39e29fe9472dc34fa82f9237c5c90e6bfa0d6992be450ac17a5848 src/admin/web/index.html
|
||||
03ad9572bb14c34059636d612cf6b876d6361ce601817cff674c57b715dc65dd src/admin/web/integrations.js
|
||||
72fe97fd22e4a1e14b7e8f20f3edd032eeddea53b539894168c6431302802a0c src/admin/web/index.html
|
||||
12c80c1dcc8c9423e991ec51c34ddbbd9136f9843bbd600e9e2cfdca2686eae1 src/admin/web/integrations.js
|
||||
7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json
|
||||
cbd41678eadd994dc5252628ee8ef9551fa9f7d8694ca5abd206240d9a96ad30 src/admin/web/login.html
|
||||
c2e5f554810b8e34fb47361fb90a01073a2e332ee26d82e030956fad3ec1f83e src/admin/web/login.js
|
||||
097fdb41aa1cd526c4d9bc65e502b05a62e664ef9fff8e69f706b93b0b124dc2 src/admin/web/management.js
|
||||
4481a592fcce9a2aa1db5e4a25619e19fd459c39039b86c0549dfc427eec1b45 src/admin/web/partials/overview-charts.html
|
||||
c1b831617bf3cd03dfa416b43791777c8e71f0711c78f9d467fd0f1930ca2a1e src/admin/web/partials/overview-metrics.html
|
||||
c8b6ea885bfc180e7e57e93b51634ea3b6aa1e3669ca41f162c66b1d0162982f src/admin/web/partials/overview-metrics.html
|
||||
3928afce2ed6d5f2d3e29475dd29e4c3bd3c6df180ad5119beaff0c7a58e3b3b src/admin/web/prometheus-token.js
|
||||
90d9a0836662135b79fb6f183c4f3eff60f8a64392d6c5795ccf1d8ef6862263 src/admin/web/redirects.js
|
||||
17784875e475d6df42964d7ecfe50ca82b74bad0e942342bc17537e727224796 src/admin/web/settings.js
|
||||
8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 src/admin/web/status_200.html
|
||||
c13cffe18a616fe5faa5ee945ab0d94d8d392ae8f32474c62fe41010e063ddb1 src/admin/web/streams.js
|
||||
d5147f3ff46a9e102b8c250eef0e59c241f2cb320518d945616148c5b8fb04ef src/admin/web/style.css
|
||||
027752715d9c3cbe7aebbe4c0cad2c9ec931f235778b894a4469c05616231839 src/admin/web/style.css
|
||||
d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js
|
||||
dfb4ded2fefb90fc1e2d33370cdef10b040a85cb43235ea9dbc1d73bef823a79 src/admin/web/transform-panel.js
|
||||
d816f2f70eef398c18091479320c36d07f9afe07278a250b840ebcdd7bc715f2 src/admin/web/vhosts/core.js
|
||||
@@ -324,12 +324,12 @@ c1b49bef28d87712e4c46138e70b52c4bcfb1a00196579031fac043d0b4aaa7b src/geoip/upda
|
||||
9a912dad0b8d1e10410fd95d0da3684656092ef85cf8b23eda4ffe7fb92921d8 src/geoip/update_tests.rs
|
||||
f6ebafafa82391f8cb3b5a09331f3762b6ca5a9fb64bafb5772cde3ab814dfcf src/headers.rs
|
||||
e4f07bfb7eb75b6897ef812870caff158679a3046d64582428abe8c6df490dbe src/headers/tests.rs
|
||||
9e21edc67a14ef8a2ce3dda0ed18784d0fd0be305973e5d216058b7ac802c6cb src/http3.rs
|
||||
652dbc91c7f36d6b4def44ecca22db8dabec518413121a52510ff92f0993b927 src/http3.rs
|
||||
eb57117938a447d9a6c2ec1806ebd9fecc640a68c88950a8b35fa9bad14d0dc6 src/http3/body.rs
|
||||
ed25da264a2d25b40c4fcddc97c626a3a43d4f3a9be6c6d31af58b9e4925c198 src/http3/diagnostics.rs
|
||||
37f54961d8f52b79a75fb2be31e81f4fdb1e66b06350345c4483d6197b8a0bbc src/http3/identity.rs
|
||||
f2a8c2a98f4ccfb708f9d75a48ec7b351232be9b8a4364b7e63ba14d28eead7c src/http3/settings.rs
|
||||
a31db7eac12b578b1eb5a74fae98bbe36312da95b7d7173135dbc41fc454da21 src/http3/tests.rs
|
||||
9d31c89ed7139d13f12f2031183f9542ee25e3f9d56303617c086747fe1487f6 src/http3/settings.rs
|
||||
8b7e840c93f08f49a5b15b2f594366930536070d76391db9fd2dd1e884ce88a8 src/http3/tests.rs
|
||||
68fb5ab6a889995a66ad12cb6acf88c5d04f0aca572e2ae15f1a9cd171a5f24b src/lib.rs
|
||||
2551189a3e5701371026c9abd2c5d44d37495f28fb67e220180421bfd315e3dd src/logging.rs
|
||||
21ddb01b8f9c7fab60840d18103ef8de4716ff0c890cc1525b472b69af7916ec src/logging/error_runtime.rs
|
||||
@@ -354,7 +354,7 @@ c915d30bbd7032d867e7916b4cd45f3bb7e7604d3f4558f4383c59394b4300c6 src/not_found/
|
||||
3567c2b65b09553e8aa46fb223dcab885b358fee9fc69cd0faf148059433b9e4 src/pages.rs
|
||||
da40e8ece87245efc49a0c4e750526b1d19fd97565856bd5caf641fcc1dfe18d src/pages/tests.rs
|
||||
63a84af77b6c3dd390aa9dfde71c527eec6e1a3c0a38c1f02c08055e525f250c src/prometheus.rs
|
||||
cc6fb14351b009a16d07ed0f412c2bcc68cd7e551421aea03d8d97ceec9bee40 src/prometheus/core.rs
|
||||
c53a6a29dbe1ef64f471054c3a4b49fd55d39ca938baac961fcaa5e06f5fbc74 src/prometheus/core.rs
|
||||
74822d8293fe1ce64b18d8363a67e9f0ed2ee9d181e2668e5ca6bd258f98285c src/prometheus/docker_misc.rs
|
||||
97926653a7c00e884784c2739a998d666abb3a891f1642bcf1ecc6c49d0efa62 src/prometheus/json.rs
|
||||
c34c3204bfb07a0de95250dfc1208979a5e701ccf5b4093cfbfd9b996ff3ba15 src/prometheus/model.rs
|
||||
@@ -364,7 +364,7 @@ e03f0d6b3f35dae6b98c394837aa0d17e4d73a8306767b4c607868bc00c30286 src/prometheus
|
||||
fb2d8d45cd2e947f783eb981b37fa418c39fcb327a3af17a648fadea7f37f888 src/prometheus/web_php.rs
|
||||
2c97367686b54989573ce7fb81b2c6dde72adb254df9522bc05ded74f1fbffb6 src/proxy/body.rs
|
||||
66b7f2cb47f2e70dc26b7b19eb8650872e34d3e44cf250d2effdb758295f8b8a src/proxy/body/tests.rs
|
||||
a1a78f580bef47e03f3bad826640183e3a68e511634157d8eba95e8b50548ac8 src/proxy/cache_helpers.rs
|
||||
37ce11759496277686eab55f1ac428e1c62baf43a33f28f41cbb1dd6385d4403 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
|
||||
@@ -390,7 +390,7 @@ b9a2307c6aa7f16613d935508a61574dfee964020b1b5be86293df2d684615d8 src/proxy/requ
|
||||
0f2eacadf1447db8aa1c1a78005aa262e0f747096bfe7e0a0e41a2cd31a0d17f src/proxy/tests/h3_navigation_tests.rs
|
||||
d134b0eb617f82f9971b3f3461379ea37c57bd0079bf54748d0c434b2ef1b475 src/proxy/tests/http_invalidation_tests.rs
|
||||
fecc1c7ffff740caa7168370e59234a17b9836a6dce6d08b770ee9648c22709b src/proxy/types.rs
|
||||
84713a5904f9687a72aafaff6f1dbfc997343a97a4f3031d498c6a4b01d7db93 src/proxy/upstream_helpers.rs
|
||||
e9cf8c1585441ce39dfc75f4e1cab7d0c3a8e4ea7b79b658911bc3ef4da2fa08 src/proxy/upstream_helpers.rs
|
||||
2d9142e3ae95895b5271b4009ab48236a3a2074ce51179c021193b35777560e4 src/proxy/web_cache_revalidation.rs
|
||||
d7b4b809b12ee89293df45e9a64b1c9981fb3f89deaab3762f4a3ad3992dd7b1 src/proxy/web_cache_revalidation/tests.rs
|
||||
69f775f6746b4c6d729dc10961dbef54168846f4ce9cf7fe453ab5a2b879c9b8 src/proxy/websocket.rs
|
||||
@@ -501,6 +501,7 @@ b35e00e4084b4de2125e9b6fff9699358b881277427bed8706fb2e8286e3cf28 src/web_server
|
||||
400e1c8aa2fe857d96036a64499fbc3de44c1e3a53811c5b25101fb843359792 src/web_server/security.rs
|
||||
c645740e5175e32a9ee743700e99a2f16b6fd894de5f734b84021b774dd747eb src/web_server/serve.rs
|
||||
dc3a18c14c3cc28519103e860b0f42a59d163db15df3f68b76778cdfa6786e46 src/web_server/tests.rs
|
||||
b1703af0b7749aa6ec59f4afa90198576925a721e4992e6214a2afcf3e57d86f tests/cache_outcomes.cjs
|
||||
f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json
|
||||
1725c10331745dc04a07675784d385260da7b14567b8dba3cbae5e891eb5dadf tests/http2.rs
|
||||
347a4ff9e028a1e999ad129590c355b5734cf928dd81e5099aea0c8aca8d5908 tests/http3.rs
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package main
|
||||
|
||||
var version = "0.34.2" // overridden by the package builder using -ldflags -X main.version
|
||||
var version = "1.0.0" // overridden by the package builder using -ldflags -X main.version
|
||||
|
||||
type config struct {
|
||||
Listen string
|
||||
|
||||
@@ -48,7 +48,7 @@ Read the dedicated guide before enabling advanced options: [Vhosts](VHOSTS.md),
|
||||
4. Set the source: HTTP reverse proxy or an existing Web Service. Test DNS, TLS and backend health.
|
||||
5. Add HTTP → HTTPS (`308`) or www/non-www redirection if needed. Check routing before turning on caching or restrictions.
|
||||
|
||||
Listener bindings set by startup CLI/systemd cannot be removed, disabled or modified through saved configuration. Saved settings may therefore be valid but not active until a service restart. Administration and public frontends use different security boundaries.
|
||||
Listener bindings set by startup CLI/systemd cannot be removed, disabled or modified through saved configuration. Saved settings may therefore be valid but not active until a service restart. For H3, the `Alt-Svc` lifetime and reductions in runtime transport limits update for new requests/connections without restart; changed UDP binds, protocol toggles and H3 connection-capacity increases still need restart. See [HTTP transport](TRANSPORT.md). Administration and public frontends use different security boundaries.
|
||||
|
||||
## Backend protocols
|
||||
|
||||
|
||||
@@ -26,6 +26,10 @@ Unfinished editors may be minimized into a draft dock; this is a UI convenience
|
||||
|
||||
## Interpreting charts
|
||||
|
||||
- The **Cache hit ratio** donut shows cumulative `HIT / (HIT + MISS)`: BYPASS and requests outside cache are deliberately excluded from its denominator.
|
||||
- The **Cache outcomes (recent)** compact horizontal 100% bar is **hidden by default**; enable it in **Settings → Appearance → Dashboard layout** or the Overview layout editor. It compares HIT, MISS, BYPASS and OTHER against all HTTP request counter increments in one contiguous sampling interval. BYPASS includes requests for which the cache path was considered but disabled/skipped (including globally disabled cache). OTHER means requests that generated no cache decision, e.g. early rejected or redirect paths. These are request counts, not response bytes. It shows no value until a second valid sample, after background-tab pauses, after counter resets, or when independently observed cache outcomes briefly exceed the requests delta. Revisit the next interval rather than trusting a forced 100% value.
|
||||
|
||||
|
||||
- Live views are process/sampling-based and can show no-data when an integration has not reported yet.
|
||||
- Per-backend protocol badges can distinguish configured HTTP mode from the protocol actually observed in connection traffic.
|
||||
- Response-header timing excludes the full streamed body duration. HTTP server/client percentages refer to the specified layer.
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
# RustProxy documentation
|
||||
|
||||
Practical guides for **RustProxy 0.34.2**. This index and the listed pages are also available in the authenticated panel at **`/docs → Reader`**. The adjacent **API** tab provides the embedded OpenAPI reference. Paths below are links to actual files in the source archive.
|
||||
Practical guides for **RustProxy 0.34.3**. This index and the listed pages are also available in the authenticated panel at **`/docs → Reader`**. The adjacent **API** tab provides the embedded OpenAPI reference. Paths below are links to actual files in the source archive.
|
||||
|
||||
## Getting started
|
||||
|
||||
@@ -26,7 +26,7 @@ Practical guides for **RustProxy 0.34.2**. This index and the listed pages are a
|
||||
- [Certificates and automatic renewal](ACME_PROVIDERS.md) — PEM, HTTP-01, DNS-01, Certbot and external certificates.
|
||||
- [Docker discovery](DOCKER_DISCOVERY.md) — optional Linux agent DEB/RPM or manual installation, TLS/token CLI, networking and generated Vhosts.
|
||||
- [Other integrations](INTEGRATIONS.md) — Tinyauth, NPM migration, OPNsense and monitoring tools.
|
||||
- [Dashboard](DASHBOARD.md) — metrics panels, presets and configuration history.
|
||||
- [Dashboard](DASHBOARD.md) — metrics panels, HIT/MISS/BYPASS/OTHER request mix, presets and configuration history.
|
||||
- [Monitoring](MONITORING.md) — Prometheus, full JSON and compact `summary` API, traffic, backend and cache observations.
|
||||
- [Logs](LOGS.md) — live logs, stored logs, error logs and searches.
|
||||
- [Backups and recovery](BACKUP.md) — encrypted backups, restore and Secret Store.
|
||||
|
||||
@@ -11,6 +11,37 @@ RustProxy distinguishes **frontend** client protocols from the **upstream** prot
|
||||
|
||||
Transport settings attach to **named listeners**, and a Vhost must select the appropriate listener IDs. Addresses locked by CLI/systemd startup cannot be rewritten from the panel. Changing a listener bind or startup-owned TLS option may require a restart.
|
||||
|
||||
### Saving HTTP/3 settings
|
||||
|
||||
- **Immediately after a successful panel save:** H2/H1 TLS responses use the saved `alt_svc_max_age` (`Alt-Svc`), provided a matching UDP listener was actually bound at startup. Origin `Alt-Svc` never passes through. The value `0` publishes `ma=0`.
|
||||
- **Disabling H3 on a Vhost or an existing listener:** new H1/H2 responses publish `Alt-Svc: clear` to remove the browser's cached alternative service. A browser may keep an established QUIC connection until it closes; `clear` does not forcibly terminate it.
|
||||
- **For new QUIC connections only:** updates to `max_concurrent_bidistream`, `max_concurrent_unistream`, `max_idle_timeout` and reductions to `max_concurrent_connections` are applied without restart. Existing QUIC connections keep their negotiated limits. If no existing UDP socket matches the saved listener address, no H3 is advertised or accepted on the old address.
|
||||
- **Restart needed:** UDP listener enable/disable, changing IP/port/TLS properties, UDP socket buffer sizes, changes to H1/H2 listener settings and increasing `max_concurrent_connections` above the startup bound. The live listener status continues to show `restart_required` for these cases. After a restart, the new connection cap can take effect.
|
||||
|
||||
|
||||
### QUIC UDP socket buffers (0.34.3)
|
||||
|
||||
The HTTP/3 settings of each TLS listener include:
|
||||
|
||||
| Parameter | Default | Accepted values | Takes effect |
|
||||
|---|---:|---|---|
|
||||
| `udp_receive_buffer_bytes` | 4194304 (4 MiB) | `0` (OS default) or 65536–16777216 | On listener restart |
|
||||
| `udp_send_buffer_bytes` | 4194304 (4 MiB) | `0` (OS default) or 65536–16777216 | On listener restart |
|
||||
|
||||
Use **Settings → HTTP/3** to edit these as whole MiB values (0–16); the JSON API stores bytes. No global `net.core.*_default` adjustment is necessary: RustProxy explicitly sets its own socket options. An OS limit may still clamp the request. Linux doubles `setsockopt(SO_RCVBUF / SO_SNDBUF)` values for bookkeeping; RustProxy targets the final effective size shown as `rb`/`tb` in `ss`.
|
||||
|
||||
For a 4 MiB target on Debian/Linux, if the default `net.core.rmem_max` or `net.core.wmem_max` prevents reaching the target, set adequate socket *maxima* after reviewing host memory constraints, for example:
|
||||
|
||||
```bash
|
||||
sudo sysctl -w net.core.rmem_max=8388608
|
||||
sudo sysctl -w net.core.wmem_max=8388608
|
||||
sudo systemctl restart rustproxy.service
|
||||
sudo ss -uapm | grep -A1 rust-proxy
|
||||
```
|
||||
|
||||
Read the `HTTP/3 UDP effective socket buffers` startup log message. `ss` should show approximately `rb4194304,tb4194304` (or larger if the OS adjusts values) when the requested size is granted. Changes to the OS maxima alone do not resize an existing socket; the configured buffer size is applied when a QUIC listener starts. High default/maxima for every program are not recommended as a substitute for explicit per-socket configuration. No specific throughput gain is guaranteed; compare H2/H3 at matched traffic before/after.
|
||||
|
||||
|
||||
## Backend protocol selection
|
||||
|
||||
| Backend mode | HTTPS origin | Plain HTTP origin |
|
||||
|
||||
@@ -9,6 +9,7 @@ Start with a reproducible request, its hostname/path and the exact listener. Com
|
||||
| Domain returns the wrong site or local 404 | DNS A/AAAA, selected listener ID, exact vs wildcard host precedence, Vhost enabled state, redirect/404 host ownership. |
|
||||
| HTTP works, HTTPS does not | Trusted certificate, hostname/SAN coverage, TLS listener, port 443 firewall, certificate assignment and whether restart was needed. |
|
||||
| HTTPS/H2 works but H3 fails | UDP firewall/NAT, H3 toggle on TLS listener, browser QUIC support and actual transport negotiation. |
|
||||
| H2 runs at link speed but H3 is slower | Compare the same origin and client with browser H3 vs H2, keeping TLS/cache/compression consistent. Inspect QUIC loss/cwnd/MTU and VM UDP offload, not only total CPU. |
|
||||
| `502` / `503` / `504` to origin | Backend IP/port, pool eligibility/health, private-address policy, upstream HTTPS verification, protocol `auto`/`h2`, timeout and container/PHP process availability. |
|
||||
| WebSocket handshake fails or disconnects | Vhost WebSockets flag, backend Upgrade support, TLS/frontend protocol path, WebSocket idle limits and auth redirect. |
|
||||
| `403` / `405` / `429` | Applicable IP/ACL/country policy, HTTP methods, rate limit, GeoIP availability, Basic/Tinyauth permissions. |
|
||||
@@ -47,6 +48,27 @@ rust-proxy --data-dir /var/lib/rustproxy config validate config.json
|
||||
|
||||
Use **Logs → Live Logs** or the `--livelogs` CLI mode to correlate request IDs, Vhost/route/backend and status. Inspect **Stored Logs** for previously saved JSONL and selected low-level protocol failures.
|
||||
|
||||
## HTTP/3 throughput and CPU diagnostics (0.34.3 source candidate)
|
||||
|
||||
Do not assume that low H3 throughput is a CPU bottleneck from the overall VM percentage alone. A 50% VM reading can mean one saturated vCPU on a 2-vCPU machine. Quinn uses UDP datagrams, per-packet QUIC/TLS, congestion control and OS/virtual NIC offloads; H1/H2 over TCP can behave differently under the same load. Quinn enables UDP GSO by default when the platform supports it, but some VM/network stacks fall back to per-datagram sends.
|
||||
|
||||
1. Run **four matched tests** against the same OpenSpeedTest/large streaming endpoint: direct backend H1, proxied HTTPS H2, proxied HTTPS H3 download, proxied H3 upload. Record client speed, per-process CPU, **per-thread** CPU, and whether the problem persists with a larger number of simultaneous clients.
|
||||
2. On Linux, inspect the NIC and guest kernel counters before and after an H3 run (replace `eth0` with the actual VM interface):
|
||||
|
||||
```bash
|
||||
ip -s link show dev eth0
|
||||
ethtool -k eth0 | grep -E 'generic-segmentation-offload|generic-receive-offload|udp-segmentation|rx-udp-gro-forwarding'
|
||||
nstat -az | grep -E 'Udp(InErrors|RcvbufErrors|SndbufErrors)|Ip(InDiscards|OutDiscards)'
|
||||
top -H -p "$(pidof rust-proxy)"
|
||||
```
|
||||
|
||||
3. If available, use `perf top -p "$(pidof rust-proxy)"` briefly during the benchmark, and inspect `%sys` / `%soft` in `mpstat -P ALL 1`. Check whether guest CPU has `aes` instruction support with `lscpu`.
|
||||
4. For the 0.34.3 source candidate, set `RUST_LOG=rust_proxy::http3=debug` in a **test** deployment and inspect `HTTP/3 bulk transfer transport snapshot` logs: `quic_lost_packets`, `quic_congestion_events`, `quic_rtt_ms`, `quic_cwnd_bytes`, `quic_path_mtu`, `quic_udp_tx_datagrams` and `quic_udp_tx_ios`. Those Quinn counters are **connection-wide cumulative snapshots**, not stream-specific deltas; compare differences for one isolated connection or across successive samples.
|
||||
|
||||
5. Run `ss -uapm | grep -A1 rust-proxy` during a transfer; check `skmem rb`, `tb` and whether dropped-datagram `d` increments. RustProxy 0.34.3 defaults to 4 MiB per direction and logs the actual SO_RCVBUF/SO_SNDBUF effective values on bind. If Linux clamps them, inspect `sysctl net.core.rmem_max net.core.wmem_max`; saving new socket buffer targets in the panel requires a listener restart. See [HTTP transport](TRANSPORT.md#quic-udp-socket-buffers-0343).
|
||||
|
||||
A rising loss/retransmission count or shrinking congestion window with low CPU suggests packet pacing, network path, UDP buffer/offload or VM issues. With negligible loss and one saturated worker thread, investigate QUIC encryption, UDP send/receive and application streaming costs using `perf` before changing window sizes. HTTP/3 overhead can also affect large file transfers, uploads and long-lived H3 streams; speedtest is only one way to expose it. No throughput improvement is claimed without a production-equivalent QUIC benchmark.
|
||||
|
||||
## Rollout gate for RustProxy 0.34.0
|
||||
|
||||
The latest source optimizations were **not all compiled or performance-tested** in the evidence supplied with this release. Before deploying:
|
||||
|
||||
+12
-3
@@ -571,7 +571,7 @@
|
||||
"h3_max_concurrent_unistream": "Unidirectional streams per connection",
|
||||
"h3_max_idle_timeout": "QUIC idle timeout (s; 0 = unlimited)",
|
||||
"h3Help": "HTTP/3 requires TLS. Keep TCP plus the same UDP port available; HTTP/1.1 and HTTP/2 remain. Zero disables the local QUIC idle timeout. Changes require restart.",
|
||||
"h3TuningHelp": "These limits apply only when HTTP/3 is enabled for this TLS listener. Changes require restart.",
|
||||
"h3TuningHelp": "Alt-Svc updates immediately; stream and idle limits apply to new QUIC connections. Changing UDP receive/send socket buffers (default 4 MiB each) or listener bindings needs a restart. On Linux, socket buffers may be capped by net.core.rmem_max / wmem_max.",
|
||||
"h3Requests": "HTTP/3 requests",
|
||||
"certImport": "Import certificate",
|
||||
"certImportSave": "Import and save in library",
|
||||
@@ -2767,11 +2767,20 @@
|
||||
"promTokenGenerate": "Generate / rotate token",
|
||||
"promTokenRevoke": "Revoke token",
|
||||
"promTokenConfigured": "Token configured",
|
||||
"promTokenMissing": "No generated token; scrapes require a legacy environment token or an explicit trusted-network bypass."
|
||||
"promTokenMissing": "No generated token; scrapes require a legacy environment token or an explicit trusted-network bypass.",
|
||||
"h3_udp_receive_buffer_mib": "UDP receive buffer (MiB; 0 = system default)",
|
||||
"h3_udp_send_buffer_mib": "UDP send buffer (MiB; 0 = system default)",
|
||||
"http3ApplyPolicy": "LIVE / RESTART",
|
||||
"cacheMixTitle": "Cache outcomes (recent)",
|
||||
"cacheMixHelp": "HIT/MISS/BYPASS and other HTTP requests in the last sample; non-cache routes count as OTHER.",
|
||||
"cacheMixOther": "OTHER",
|
||||
"cacheMixSample": "Last interval · {count} HTTP requests",
|
||||
"cacheMixWaiting": "Waiting for comparable traffic samples",
|
||||
"cacheMixAria": "Recent HTTP requests: {hit} HIT, {miss} MISS, {bypass} BYPASS, {other} OTHER"
|
||||
},
|
||||
"overviewLayoutNarrow": "Decrease card width",
|
||||
"overviewLayoutWiden": "Increase card width",
|
||||
"overviewLayoutWidth": "Card width in 12-column grid",
|
||||
"overviewLayoutInlineHint": "Layout unlocked: move any card anywhere; use − / + to resize (2, 3, 4, 6 or 12 columns). Save and lock or cancel to revert.",
|
||||
"dashboardPresetsConflict": "Preset already exists or was removed. Refresh and try again."
|
||||
}
|
||||
}
|
||||
+12
-3
@@ -571,7 +571,7 @@
|
||||
"h3_max_concurrent_unistream": "Strumienie jednokierunkowe na połączenie",
|
||||
"h3_max_idle_timeout": "Bezczynność QUIC (s; 0 = bez limitu)",
|
||||
"h3Help": "HTTP/3 wymaga TLS. Udostępnij TCP i UDP na tym samym porcie; HTTP/1.1 i HTTP/2 pozostają. Zero wyłącza lokalny timeout QUIC. Zmiana wymaga restartu.",
|
||||
"h3TuningHelp": "Te limity obowiązują tylko wtedy, gdy HTTP/3 jest włączone dla danego listenera TLS. Zmiana wymaga restartu.",
|
||||
"h3TuningHelp": "Alt-Svc działa natychmiast; limity strumieni i bezczynności dotyczą nowych połączeń QUIC. Zmiana buforów UDP (domyślnie po 4 MiB) lub adresów listenerów wymaga restartu. Linux może ograniczać rozmiar przez net.core.rmem_max / wmem_max.",
|
||||
"h3Requests": "Żądania HTTP/3",
|
||||
"certImport": "Importuj certyfikat",
|
||||
"certImportSave": "Importuj i zapisz w bibliotece",
|
||||
@@ -2767,11 +2767,20 @@
|
||||
"promTokenGenerate": "Generuj / odnów token",
|
||||
"promTokenRevoke": "Usuń token",
|
||||
"promTokenConfigured": "Token skonfigurowany",
|
||||
"promTokenMissing": "Brak wygenerowanego tokenu; potrzebny token ze starszej zmiennej środowiskowej lub jawny wyjątek dla zaufanej sieci."
|
||||
"promTokenMissing": "Brak wygenerowanego tokenu; potrzebny token ze starszej zmiennej środowiskowej lub jawny wyjątek dla zaufanej sieci.",
|
||||
"h3_udp_receive_buffer_mib": "Bufor odbiorczy UDP (MiB; 0 = domyślny systemowy)",
|
||||
"h3_udp_send_buffer_mib": "Bufor nadawczy UDP (MiB; 0 = domyślny systemowy)",
|
||||
"http3ApplyPolicy": "NA ŻYWO / RESTART",
|
||||
"cacheMixTitle": "Wyniki cache (ostatni pomiar)",
|
||||
"cacheMixHelp": "HIT/MISS/BYPASS i pozostały ruch HTTP w ostatnim pomiarze; trasy bez decyzji cache to INNE.",
|
||||
"cacheMixOther": "INNE",
|
||||
"cacheMixSample": "Ostatni przedział · {count} żądań HTTP",
|
||||
"cacheMixWaiting": "Oczekiwanie na porównywalne próbki ruchu",
|
||||
"cacheMixAria": "Ostatnie żądania HTTP: {hit} HIT, {miss} MISS, {bypass} BYPASS, {other} INNE"
|
||||
},
|
||||
"overviewLayoutNarrow": "Zmniejsz szerokość karty",
|
||||
"overviewLayoutWiden": "Zwiększ szerokość karty",
|
||||
"overviewLayoutWidth": "Szerokość karty w siatce 12 kolumn",
|
||||
"overviewLayoutInlineHint": "Układ odblokowany: przenoś dowolne karty i zmieniaj ich szerokość przyciskami − / + (2, 3, 4, 6 lub 12 kolumn). Zapisz lub anuluj zmiany.",
|
||||
"dashboardPresetsConflict": "Preset już istnieje lub został usunięty. Odśwież listę i spróbuj ponownie."
|
||||
}
|
||||
}
|
||||
+84
-4
@@ -2,8 +2,8 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.34.2",
|
||||
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust. Version 0.34.2 also permits explicitly scoped GeneralApiBearer tokens on a narrow route allowlist, with RO/RW and source CIDR checks; API-token management and Prometheus-token management are browser-session-only."
|
||||
"version": "0.34.3",
|
||||
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust. Version 0.34.3 also permits explicitly scoped GeneralApiBearer tokens on a narrow route allowlist, with RO/RW and source CIDR checks; API-token management and Prometheus-token management are browser-session-only."
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
@@ -691,8 +691,34 @@
|
||||
},
|
||||
"listeners": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": true
|
||||
"properties": {
|
||||
"endpoints": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"binding": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"http3": {
|
||||
"description": "HTTP/3/QUIC listener policy. null means disabled.",
|
||||
"nullable": true,
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Http3Settings"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": true,
|
||||
"description": "Named listener endpoints (binding.http3 uses Http3Settings); settings that change UDP binds/socket buffers need service restart."
|
||||
},
|
||||
"http2": {
|
||||
"description": "Legacy HTTP/2 lifecycle fallback for listeners that do not define binding.http2_settings. Prefer listener-scoped settings in config.listeners.",
|
||||
@@ -1161,6 +1187,7 @@
|
||||
"format": "int64"
|
||||
},
|
||||
"listeners": {
|
||||
"description": "Bound and configured HTTP listener endpoints. restart_required excludes hot HTTP/3 Alt-Svc and per-new-connection QUIC settings, but includes socket/port and startup capacity changes.",
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": true
|
||||
@@ -7352,6 +7379,59 @@
|
||||
"capacity"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Http3Settings": {
|
||||
"type": "object",
|
||||
"description": "Per-TLS-listener binding.http3 settings. Alt-Svc lifetime updates immediately; stream/idle limits apply to new QUIC handshakes. UDP buffers are applied to new listener sockets after service restart and can be OS-clamped. 0 disables an explicit socket buffer request.",
|
||||
"properties": {
|
||||
"alt_svc_max_age": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": 0,
|
||||
"maximum": 604800,
|
||||
"default": 3600
|
||||
},
|
||||
"max_concurrent_connections": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 4096,
|
||||
"default": 512
|
||||
},
|
||||
"max_concurrent_bidistream": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 1024,
|
||||
"default": 100
|
||||
},
|
||||
"max_concurrent_unistream": {
|
||||
"type": "integer",
|
||||
"minimum": 3,
|
||||
"maximum": 1024,
|
||||
"default": 100
|
||||
},
|
||||
"max_idle_timeout": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": 0,
|
||||
"maximum": 86400,
|
||||
"default": 10
|
||||
},
|
||||
"udp_receive_buffer_bytes": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 16777216,
|
||||
"default": 4194304,
|
||||
"description": "0 or 65536..16777216 bytes. Applies after restart."
|
||||
},
|
||||
"udp_send_buffer_bytes": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 16777216,
|
||||
"default": 4194304,
|
||||
"description": "0 or 65536..16777216 bytes. Applies after restart."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -31,6 +31,7 @@ fn overview_factory_preset_matches_registry_and_operator_visibility() {
|
||||
"extra_process_cpu",
|
||||
"stream_traffic",
|
||||
"wide_cache_activity",
|
||||
"cache_outcomes",
|
||||
"www_status",
|
||||
"www_requests",
|
||||
"www_top_services",
|
||||
@@ -189,6 +190,9 @@ fn saved_dashboard_order_and_visibility_survive_new_factory_preset() {
|
||||
assert!(prefs
|
||||
.dashboard_hidden_widgets
|
||||
.contains(&"www_status".to_owned()));
|
||||
assert!(prefs
|
||||
.dashboard_hidden_widgets
|
||||
.contains(&"cache_outcomes".to_owned()));
|
||||
assert!(validate_admin_preferences(&prefs).is_ok());
|
||||
}
|
||||
|
||||
@@ -199,6 +203,7 @@ fn explicit_visible_stream_opt_in_is_not_reset_when_normalizing_saved_order() {
|
||||
prefs.dashboard_hidden_widgets.clear();
|
||||
reconcile_dashboard_widgets(&mut prefs);
|
||||
assert!(prefs.dashboard_hidden_widgets.is_empty());
|
||||
assert!(prefs.dashboard_widgets.contains(&"cache_outcomes".into()));
|
||||
assert_eq!(prefs.dashboard_widgets, default_admin_dashboard_widgets());
|
||||
assert!(validate_admin_preferences(&prefs).is_ok());
|
||||
}
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ fn status_payload(state: &App) -> Value {
|
||||
"active_endpoints":state.active_listeners,
|
||||
"desired_endpoints":snapshot.listeners,
|
||||
"named_endpoints":snapshot.envelope.config.listeners.endpoints,
|
||||
"restart_required":snapshot.listeners != state.active_listeners
|
||||
"restart_required":crate::http3::listeners_require_restart(&state.active_listeners, &snapshot.listeners)
|
||||
});
|
||||
let mut features = serde_json::Map::new();
|
||||
for name in [
|
||||
|
||||
@@ -54,7 +54,7 @@ fn live_status_payload(state: &App) -> Value {
|
||||
"listeners": {
|
||||
"active_endpoints": state.active_listeners,
|
||||
"desired_endpoints": snapshot.listeners,
|
||||
"restart_required": snapshot.listeners != state.active_listeners
|
||||
"restart_required": crate::http3::listeners_require_restart(&state.active_listeners, &snapshot.listeners)
|
||||
},
|
||||
"metrics": state.metrics.view(),
|
||||
"web_server_metrics": web_server_metrics_payload(&snapshot),
|
||||
|
||||
@@ -174,6 +174,7 @@ $("reload-certificates").addEventListener("click", async () => {
|
||||
$("reset-certificate").addEventListener("click", () => { $("certificate-form").reset(); $("certificate-file-source").value = "managed"; syncCertificateFileSource(); });
|
||||
syncCertificateFileSource();
|
||||
|
||||
let cacheMixLastCounters = null, cacheMixLastShares = null, cacheMixLastTotal = 0;
|
||||
let systemSamples = [], connectionOpenSamples = [], connectionExchangeSamples = [], cacheMemorySamples = [], cacheTransientSamples = [], cacheFillSamples = [], cacheColdQueueSamples = [], cacheColdIoSamples = [], cacheColdStorageSamples = [], cacheHitSamples = [], cacheMissSamples = [], cacheBypassSamples = [], lastCacheCounters = null, lastCacheOutcomeRates = null,
|
||||
processCpuSamples = [], processRssSamples = [], networkRxSamples = [], networkTxSamples = [], responseLatencySamples = [], networkInterfaceSamples = new Map(), settingsDirty = new Set();
|
||||
const hiddenNetworkInterfaces = (() => {
|
||||
@@ -330,6 +331,34 @@ function renderNetworkInterfaceCharts(interfaces, appendSample) {
|
||||
target.replaceChildren(...children);
|
||||
}
|
||||
}
|
||||
|
||||
function cacheOutcomeMix(deltas) {
|
||||
if (!Array.isArray(deltas) || deltas.length !== 4 ||
|
||||
deltas.some(value => !Number.isFinite(value) || value < 0)) return null;
|
||||
const [requests, hits, misses, bypasses] = deltas;
|
||||
const cacheTotal = hits + misses + bypasses;
|
||||
if (requests <= 0 || cacheTotal > requests) return null;
|
||||
const counts = [hits, misses, bypasses, requests - cacheTotal];
|
||||
return { counts, percentages: counts.map(value => value / requests * 100), requests };
|
||||
}
|
||||
function renderCacheMix(mix) {
|
||||
const names = ["hit", "miss", "bypass", "other"];
|
||||
names.forEach((name, i) => {
|
||||
const pct = mix ? mix.percentages[i] : 0;
|
||||
$(`cache-mix-${name}`).style.width = `${pct}%`;
|
||||
$(`cache-mix-${name}-percent`).textContent = mix ? `${fmt(pct, 1)}%` : "-";
|
||||
});
|
||||
$("cache-mix-headline").textContent = mix ? `${fmt(mix.percentages[0], 1)}% HIT` : "-";
|
||||
$("cache-mix-caption").textContent = mix
|
||||
? t("cacheMixSample").replace("{count}", fmt(mix.requests))
|
||||
: t("cacheMixWaiting");
|
||||
$("cache-mix-bar").setAttribute("aria-label", mix
|
||||
? t("cacheMixAria").replace("{hit}", fmt(mix.counts[0]))
|
||||
.replace("{miss}", fmt(mix.counts[1]))
|
||||
.replace("{bypass}", fmt(mix.counts[2]))
|
||||
.replace("{other}", fmt(mix.counts[3]))
|
||||
: t("cacheMixWaiting"));
|
||||
}
|
||||
function renderDiagnostics(status, appendSample = true, systemSample = appendSample) {
|
||||
const d = status.system || {}, live = status.live_system || d, c = status.cache || {}, m = status.metrics || {};
|
||||
$("metric-h3").textContent = fmt(m.http3_requests);
|
||||
@@ -337,6 +366,14 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
|
||||
const cacheEvents = cacheHits !== null && cacheMisses !== null ? cacheHits + cacheMisses : 0;
|
||||
$("metric-cache-ratio").textContent = cacheEvents > 0 ? `${fmt(cacheHits / cacheEvents * 100, 1)}%` : "-";
|
||||
window.rustProxyExtraOverview?.donut?.("cache-ratio", cacheHits, cacheMisses);
|
||||
if (appendSample) {
|
||||
const counters = [m.requests, c.hits, c.misses, c.bypasses].map(value => value == null ? NaN : Number(value));
|
||||
const delta = rustProxyRateSampling.difference(cacheMixLastCounters, counters,
|
||||
performance.now(), rustProxyRateSampling.cadence("overview_seconds"));
|
||||
cacheMixLastCounters = counters.every(Number.isFinite) ? delta.next : null;
|
||||
cacheMixLastShares = delta.deltas ? cacheOutcomeMix(delta.deltas) : null;
|
||||
}
|
||||
renderCacheMix(cacheMixLastShares);
|
||||
if (systemSample) {
|
||||
if (d.process_rss_bytes != null) $("metric-rss").textContent = fmtBytes(d.process_rss_bytes);
|
||||
$("process-summary").hidden = d.process_threads == null;
|
||||
|
||||
@@ -1,43 +1,139 @@
|
||||
{
|
||||
"dashboard_widgets": [
|
||||
"requests", "active", "bandwidth", "errors", "memory", "websocket",
|
||||
"backend_traffic", "extra_response_latency", "http_protocol", "tls",
|
||||
"websocket_protocol", "backend_latency", "stream_overview", "stream_traffic",
|
||||
"stream_sessions", "stream_backend_health", "backend_health", "backend_activity",
|
||||
"backend_errors", "backend_retries", "http2", "backend_protocols",
|
||||
"backend_requests", "backend_capacity", "backend_http5xx", "backend_timeouts",
|
||||
"backend_network", "http3", "backend_queue", "extra_open_connections",
|
||||
"extra_process_cpu", "extra_process_rss", "extra_process_fds", "backend_rotations",
|
||||
"extra_websocket_active", "extra_tls_error_rate", "extra_request_error_rate",
|
||||
"extra_cache_hit_rate", "extra_backend_active", "extra_h2_share",
|
||||
"extra_cache_memory", "extra_backend_unhealthy", "extra_total_rps",
|
||||
"extra_active_exchanges", "extra_h1_share", "extra_h3_share",
|
||||
"extra_cache_miss_rate", "extra_cold_storage", "wide_protocol_rates",
|
||||
"wide_cpu", "wide_memory", "wide_cache_activity", "wide_connections",
|
||||
"wide_backend_health", "wide_latency", "cache", "traffic_chart",
|
||||
"system_chart", "connection_chart", "wide_transform_activity",
|
||||
"wide_compression_activity", "cache_diagnostics", "extra_cold_queue_memory",
|
||||
"process_diagnostics", "network_diagnostics", "backend_table",
|
||||
"www_status", "www_kind", "www_capacity", "www_methods", "www_error_rate",
|
||||
"www_php_requests", "www_requests", "www_latency", "www_failures",
|
||||
"www_traffic", "www_top_services", "www_status_trend"
|
||||
"requests",
|
||||
"active",
|
||||
"bandwidth",
|
||||
"errors",
|
||||
"memory",
|
||||
"websocket",
|
||||
"backend_traffic",
|
||||
"extra_response_latency",
|
||||
"http_protocol",
|
||||
"tls",
|
||||
"websocket_protocol",
|
||||
"backend_latency",
|
||||
"stream_overview",
|
||||
"stream_traffic",
|
||||
"stream_sessions",
|
||||
"stream_backend_health",
|
||||
"backend_health",
|
||||
"backend_activity",
|
||||
"backend_errors",
|
||||
"backend_retries",
|
||||
"http2",
|
||||
"backend_protocols",
|
||||
"backend_requests",
|
||||
"backend_capacity",
|
||||
"backend_http5xx",
|
||||
"backend_timeouts",
|
||||
"backend_network",
|
||||
"http3",
|
||||
"backend_queue",
|
||||
"extra_open_connections",
|
||||
"extra_process_cpu",
|
||||
"extra_process_rss",
|
||||
"extra_process_fds",
|
||||
"backend_rotations",
|
||||
"extra_websocket_active",
|
||||
"extra_tls_error_rate",
|
||||
"extra_request_error_rate",
|
||||
"extra_cache_hit_rate",
|
||||
"extra_backend_active",
|
||||
"extra_h2_share",
|
||||
"extra_cache_memory",
|
||||
"extra_backend_unhealthy",
|
||||
"extra_total_rps",
|
||||
"extra_active_exchanges",
|
||||
"extra_h1_share",
|
||||
"extra_h3_share",
|
||||
"extra_cache_miss_rate",
|
||||
"extra_cold_storage",
|
||||
"wide_protocol_rates",
|
||||
"wide_cpu",
|
||||
"wide_memory",
|
||||
"wide_cache_activity",
|
||||
"wide_connections",
|
||||
"wide_backend_health",
|
||||
"wide_latency",
|
||||
"cache",
|
||||
"cache_outcomes",
|
||||
"traffic_chart",
|
||||
"system_chart",
|
||||
"connection_chart",
|
||||
"wide_transform_activity",
|
||||
"wide_compression_activity",
|
||||
"cache_diagnostics",
|
||||
"extra_cold_queue_memory",
|
||||
"process_diagnostics",
|
||||
"network_diagnostics",
|
||||
"backend_table",
|
||||
"www_status",
|
||||
"www_kind",
|
||||
"www_capacity",
|
||||
"www_methods",
|
||||
"www_error_rate",
|
||||
"www_php_requests",
|
||||
"www_requests",
|
||||
"www_latency",
|
||||
"www_failures",
|
||||
"www_traffic",
|
||||
"www_top_services",
|
||||
"www_status_trend"
|
||||
],
|
||||
"dashboard_hidden_widgets": [
|
||||
"stream_overview", "stream_backend_health", "tls", "stream_sessions",
|
||||
"backend_health", "backend_activity", "backend_errors", "backend_protocols",
|
||||
"backend_requests", "backend_timeouts", "backend_network", "extra_process_cpu",
|
||||
"extra_process_rss", "extra_process_fds", "extra_websocket_active",
|
||||
"extra_tls_error_rate", "extra_request_error_rate", "extra_h2_share",
|
||||
"extra_backend_unhealthy", "extra_total_rps", "extra_h1_share",
|
||||
"extra_h3_share", "wide_memory", "wide_connections", "wide_backend_health",
|
||||
"wide_protocol_rates", "extra_active_exchanges", "extra_cache_miss_rate",
|
||||
"extra_cache_hit_rate", "backend_capacity", "stream_traffic", "backend_http5xx",
|
||||
"bandwidth", "backend_queue", "wide_cpu", "wide_latency", "www_status",
|
||||
"www_kind", "www_capacity", "www_methods", "www_error_rate",
|
||||
"www_php_requests", "www_requests", "www_latency", "www_failures",
|
||||
"www_traffic", "www_top_services", "www_status_trend",
|
||||
"extra_cold_queue_memory", "wide_cache_activity", "backend_rotations"
|
||||
"cache_outcomes",
|
||||
"stream_overview",
|
||||
"stream_backend_health",
|
||||
"tls",
|
||||
"stream_sessions",
|
||||
"backend_health",
|
||||
"backend_activity",
|
||||
"backend_errors",
|
||||
"backend_protocols",
|
||||
"backend_requests",
|
||||
"backend_timeouts",
|
||||
"backend_network",
|
||||
"extra_process_cpu",
|
||||
"extra_process_rss",
|
||||
"extra_process_fds",
|
||||
"extra_websocket_active",
|
||||
"extra_tls_error_rate",
|
||||
"extra_request_error_rate",
|
||||
"extra_h2_share",
|
||||
"extra_backend_unhealthy",
|
||||
"extra_total_rps",
|
||||
"extra_h1_share",
|
||||
"extra_h3_share",
|
||||
"wide_memory",
|
||||
"wide_connections",
|
||||
"wide_backend_health",
|
||||
"wide_protocol_rates",
|
||||
"extra_active_exchanges",
|
||||
"extra_cache_miss_rate",
|
||||
"extra_cache_hit_rate",
|
||||
"backend_capacity",
|
||||
"stream_traffic",
|
||||
"backend_http5xx",
|
||||
"bandwidth",
|
||||
"backend_queue",
|
||||
"wide_cpu",
|
||||
"wide_latency",
|
||||
"www_status",
|
||||
"www_kind",
|
||||
"www_capacity",
|
||||
"www_methods",
|
||||
"www_error_rate",
|
||||
"www_php_requests",
|
||||
"www_requests",
|
||||
"www_latency",
|
||||
"www_failures",
|
||||
"www_traffic",
|
||||
"www_top_services",
|
||||
"www_status_trend",
|
||||
"extra_cold_queue_memory",
|
||||
"wide_cache_activity",
|
||||
"backend_rotations"
|
||||
],
|
||||
"dashboard_density": "compact",
|
||||
"overview_samples": 600
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,17 @@
|
||||
"default_hidden": false,
|
||||
"display_type": "value"
|
||||
},
|
||||
{
|
||||
"id": "cache_outcomes",
|
||||
"label_key": "cacheMixTitle",
|
||||
"description_key": "cacheMixHelp",
|
||||
"element_selector": "[data-dashboard-widget=\"cache_outcomes\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "overview",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "memory",
|
||||
"label_key": "processMemory",
|
||||
@@ -857,4 +868,4 @@
|
||||
"default_hidden": true,
|
||||
"display_type": "chart"
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -2028,10 +2028,11 @@
|
||||
<form class="edit-form embedded-settings-form" id="http3-form">
|
||||
<div class="card-heading">
|
||||
<h3 data-i18n="http3Settings">HTTP/3 / QUIC</h3><span class="pill"
|
||||
data-i18n="restartLabel">RESTART</span>
|
||||
data-i18n="http3ApplyPolicy">LIVE / RESTART</span>
|
||||
</div>
|
||||
<p class="fine" data-i18n="http3SettingsHelp">HTTP/3 enablement is controlled in
|
||||
Listeners. Tune advertisement, connection and stream limits here.</p>
|
||||
Listeners. Tune live advertisement/stream limits and startup UDP socket
|
||||
buffers here.</p>
|
||||
<p class="pending-note" hidden="" id="http3-listeners-dirty"
|
||||
data-i18n="http3ListenersDirty">Save pending listener changes before editing
|
||||
HTTP/3 settings.</p>
|
||||
@@ -3053,7 +3054,8 @@
|
||||
role="tabpanel">
|
||||
<article class="card edit-form">
|
||||
<div class="card-heading">
|
||||
<div><span class="small-label" data-i18n="configStorageLabel">CONFIGURATION SOURCE</span>
|
||||
<div><span class="small-label" data-i18n="configStorageLabel">CONFIGURATION
|
||||
SOURCE</span>
|
||||
<h2 data-i18n="manualConfig">Manual config</h2>
|
||||
</div>
|
||||
<div class="manual-config-tools"><button class="quiet" data-i18n="configHistory"
|
||||
@@ -3062,7 +3064,8 @@
|
||||
JSON</button></div>
|
||||
</div>
|
||||
<div class="manual-config-source" role="status" aria-live="polite">
|
||||
<strong id="config-storage-mode" data-i18n="configStorageLoading">Checking storage...</strong>
|
||||
<strong id="config-storage-mode" data-i18n="configStorageLoading">Checking
|
||||
storage...</strong>
|
||||
<p id="config-storage-location" class="manual-config-path"></p>
|
||||
<p id="config-storage-description" class="fine"></p>
|
||||
<p id="config-storage-history" class="fine"></p>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
"use strict";
|
||||
// Shared controls for named listeners, HTTP/2/HTTP/3 and the certificate inventory.
|
||||
// All labels are resolved from discovered language packs.
|
||||
(() => {
|
||||
let uploadBusy = false;
|
||||
const label = (tag, key, cls) => { const node = el(tag, t(key), cls); node.dataset.i18n = key; return node; };
|
||||
@@ -10,7 +8,7 @@
|
||||
if (type === "select") for (const [value, key] of options.values || []) { const option = label("option", key); option.value = value; input.append(option); }
|
||||
else if (type !== "textarea") input.type = type;
|
||||
if (type === "checkbox") input.checked = !!value; else input.value = value ?? "";
|
||||
for (const key of ["min", "max", "maxLength", "required", "pattern", "rows"]) if (Object.hasOwn(options, key)) input[key] = options[key];
|
||||
for (const key of ["min", "max", "maxLength", "required", "pattern", "rows", "step"]) if (Object.hasOwn(options, key)) input[key] = options[key];
|
||||
if (type === "checkbox") wrapper.append(input, label("span", key)); else wrapper.append(label("span", key), input);
|
||||
parent.append(wrapper); return input;
|
||||
}
|
||||
@@ -87,15 +85,33 @@
|
||||
};
|
||||
return { read, sync };
|
||||
}
|
||||
const h3Defaults = { alt_svc_max_age: 3600, max_concurrent_connections: 512, max_concurrent_bidistream: 100, max_concurrent_unistream: 100, max_idle_timeout: 10 };
|
||||
const h3Defaults = { alt_svc_max_age: 3600, max_concurrent_connections: 512, max_concurrent_bidistream: 100, max_concurrent_unistream: 100, max_idle_timeout: 10, udp_receive_buffer_bytes: 4194304, udp_send_buffer_bytes: 4194304 };
|
||||
function http3SettingsControls(parent, binding) {
|
||||
const box = el("div", undefined, "h3-editor vh-wide"); parent.append(box);
|
||||
const grid = el("div", undefined, "vh-grid"); box.append(grid); const inputs = {};
|
||||
for (const [name, min, max] of [["alt_svc_max_age", 0, 604800], ["max_concurrent_connections", 1, 4096], ["max_concurrent_bidistream", 1, 1024], ["max_concurrent_unistream", 3, 1024], ["max_idle_timeout", 0, 86400]]) inputs[name] = field(grid, name, `h3_${name}`, binding.http3?.[name] ?? h3Defaults[name], "number", { min, max, required: true });
|
||||
for (const direction of ["receive", "send"]) {
|
||||
const name = `udp_${direction}_buffer_bytes`;
|
||||
const mib = (binding.http3?.[name] ?? h3Defaults[name]) / 1048576;
|
||||
inputs[name] = field(grid, name, `h3_udp_${direction}_buffer_mib`, mib, "number", { min: 0, max: 16, step: "any", required: true });
|
||||
}
|
||||
box.append(label("p", "h3TuningHelp", "fine"));
|
||||
const sync = (blocked = false) => { for (const input of Object.values(inputs)) input.disabled = blocked; };
|
||||
sync(false);
|
||||
const read = () => { const value = {}; for (const [name, input] of Object.entries(inputs)) { value[name] = Number(input.value); if (!Number.isSafeInteger(value[name])) throw fieldError(input, t("vhJsonInvalid")); } return value; };
|
||||
const read = () => {
|
||||
const value = {};
|
||||
for (const [name, input] of Object.entries(inputs)) {
|
||||
let number = Number(input.value);
|
||||
if (!input.value.trim() || !Number.isFinite(number)) throw fieldError(input, t("vhJsonInvalid"));
|
||||
if (name.startsWith("udp_") && name.endsWith("_buffer_bytes")) {
|
||||
number = Math.round(number * 1048576);
|
||||
if (number !== 0 && (number < 65536 || number > 16777216)) throw fieldError(input, t("vhJsonInvalid"));
|
||||
}
|
||||
if (!Number.isSafeInteger(number)) throw fieldError(input, t("vhJsonInvalid"));
|
||||
value[name] = number;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
return { read, sync };
|
||||
}
|
||||
function addRow(item = { id: `listener-${$("named-listener-rows").children.length + 1}`, name: "", scope: "internal", enabled: true, binding: { address: "127.0.0.1:8181", tls: false, http2: false, http2_settings: null, h2c: false, http3: null } }) {
|
||||
|
||||
@@ -31,6 +31,25 @@
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="stat stat-cache-outcomes" data-dashboard-widget="cache_outcomes">
|
||||
<span data-i18n="cacheMixTitle">Cache outcomes (recent)</span>
|
||||
<strong id="cache-mix-headline">-</strong>
|
||||
<div class="cache-mix-bar" id="cache-mix-bar" role="img" aria-label="Recent request cache outcomes">
|
||||
<span class="cache-mix-hit" id="cache-mix-hit" style="width:0%"></span>
|
||||
<span class="cache-mix-miss" id="cache-mix-miss" style="width:0%"></span>
|
||||
<span class="cache-mix-bypass" id="cache-mix-bypass" style="width:0%"></span>
|
||||
<span class="cache-mix-other" id="cache-mix-other" style="width:0%"></span>
|
||||
</div>
|
||||
<small class="cache-mix-legend">
|
||||
<span class="cache-mix-hit-key"><i></i>HIT <b id="cache-mix-hit-percent">-</b></span>
|
||||
<span class="cache-mix-miss-key"><i></i>MISS <b id="cache-mix-miss-percent">-</b></span>
|
||||
<span class="cache-mix-bypass-key"><i></i>BYPASS <b id="cache-mix-bypass-percent">-</b></span>
|
||||
<span class="cache-mix-other-key"><i></i><span data-i18n="cacheMixOther">OTHER</span> <b
|
||||
id="cache-mix-other-percent">-</b></span>
|
||||
</small>
|
||||
<small class="cache-mix-caption" id="cache-mix-caption" data-i18n="cacheMixWaiting">Waiting for request
|
||||
samples</small>
|
||||
</article>
|
||||
<article class="stat" data-dashboard-widget="memory"><span data-i18n="processMemory">Process
|
||||
memory</span><strong id="metric-rss">-</strong><small id="process-summary">-</small>
|
||||
</article>
|
||||
|
||||
@@ -12193,4 +12193,87 @@ html[data-dashboard-density] #panel-overview .dashboard-overview-grid>.card.pool
|
||||
|
||||
.certificate-bulk-list .check {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
#panel-overview .stat-cache-outcomes .cache-mix-bar {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
margin: 7px 0 9px;
|
||||
border-radius: var(--rp-progress-radius);
|
||||
background: var(--rp-progress-track);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#panel-overview .cache-mix-bar>span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#panel-overview .cache-mix-hit {
|
||||
background: var(--chart-accent);
|
||||
}
|
||||
|
||||
#panel-overview .cache-mix-miss {
|
||||
background: var(--chart-secondary);
|
||||
}
|
||||
|
||||
#panel-overview .cache-mix-bypass {
|
||||
background: var(--chart-tertiary);
|
||||
}
|
||||
|
||||
#panel-overview .cache-mix-other {
|
||||
background: var(--muted);
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
#panel-overview .cache-mix-legend {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 5px 8px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
#panel-overview .cache-mix-legend>span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#panel-overview .cache-mix-legend i {
|
||||
display: inline-block;
|
||||
flex: 0 0 7px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
#panel-overview .cache-mix-hit-key i {
|
||||
background: var(--chart-accent);
|
||||
}
|
||||
|
||||
#panel-overview .cache-mix-miss-key i {
|
||||
background: var(--chart-secondary);
|
||||
}
|
||||
|
||||
#panel-overview .cache-mix-bypass-key i {
|
||||
background: var(--chart-tertiary);
|
||||
}
|
||||
|
||||
#panel-overview .cache-mix-other-key i {
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
#panel-overview .cache-mix-legend b {
|
||||
margin-left: auto;
|
||||
color: var(--text);
|
||||
font: 500 10px var(--mono);
|
||||
}
|
||||
|
||||
#panel-overview .stat-cache-outcomes .cache-mix-caption {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: var(--muted);
|
||||
}
|
||||
+209
-12
@@ -17,7 +17,7 @@ use http_body_util::BodyExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
io,
|
||||
net::SocketAddr,
|
||||
net::{SocketAddr, UdpSocket},
|
||||
pin::Pin,
|
||||
sync::{atomic::Ordering, Arc},
|
||||
task::{Context, Poll},
|
||||
@@ -63,6 +63,66 @@ pub struct Bound {
|
||||
address: SocketAddr,
|
||||
settings: Settings,
|
||||
}
|
||||
// Set the actual QUIC UDP socket buffers, not only sysctl maxima. Linux
|
||||
// doubles SO_RCVBUF/SO_SNDBUF values returned by getsockopt(2), which `ss`
|
||||
// reports as rb/tb. Target the effective buffer size shown by `ss`.
|
||||
fn requested_socket_buffer_size(effective_bytes: u32) -> usize {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
(effective_bytes as usize / 2).max(1)
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
effective_bytes as usize
|
||||
}
|
||||
}
|
||||
|
||||
fn configure_udp_socket(socket: &UdpSocket, address: SocketAddr, settings: Settings) {
|
||||
let socket = socket2::SockRef::from(socket);
|
||||
for (direction, requested, result) in [
|
||||
(
|
||||
"receive",
|
||||
settings.udp_receive_buffer_bytes,
|
||||
if settings.udp_receive_buffer_bytes == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
socket.set_recv_buffer_size(requested_socket_buffer_size(
|
||||
settings.udp_receive_buffer_bytes,
|
||||
))
|
||||
},
|
||||
),
|
||||
(
|
||||
"send",
|
||||
settings.udp_send_buffer_bytes,
|
||||
if settings.udp_send_buffer_bytes == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
socket.set_send_buffer_size(requested_socket_buffer_size(
|
||||
settings.udp_send_buffer_bytes,
|
||||
))
|
||||
},
|
||||
),
|
||||
] {
|
||||
if let Err(error) = result {
|
||||
tracing::warn!(%address, direction, requested, %error,
|
||||
"HTTP/3 UDP buffer request failed; continuing with OS socket default");
|
||||
}
|
||||
}
|
||||
let receive = socket.recv_buffer_size().ok();
|
||||
let send = socket.send_buffer_size().ok();
|
||||
tracing::info!(%address, ?receive, ?send,
|
||||
"HTTP/3 UDP effective socket buffers (bytes)");
|
||||
for (direction, requested, actual) in [
|
||||
("receive", settings.udp_receive_buffer_bytes, receive),
|
||||
("send", settings.udp_send_buffer_bytes, send),
|
||||
] {
|
||||
if requested != 0 && actual.is_some_and(|size| size < requested as usize) {
|
||||
tracing::warn!(%address, direction, requested, ?actual,
|
||||
"HTTP/3 UDP socket buffer below configured target; check net.core.rmem_max and wmem_max on Linux");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bind(listener: Listener, state: &App) -> Result<Bound> {
|
||||
let settings = listener
|
||||
.http3
|
||||
@@ -82,8 +142,16 @@ pub fn bind(listener: Listener, state: &App) -> Result<Bound> {
|
||||
.max_incoming(settings.max_concurrent_connections as usize)
|
||||
.incoming_buffer_size(64 * 1024)
|
||||
.incoming_buffer_size_total(1024 * 1024);
|
||||
let endpoint = quinn::Endpoint::server(server_config.clone(), listener.address)
|
||||
let socket = UdpSocket::bind(listener.address)
|
||||
.with_context(|| format!("cannot bind HTTP/3 UDP {}", listener.address))?;
|
||||
configure_udp_socket(&socket, listener.address, settings);
|
||||
let endpoint = quinn::Endpoint::new(
|
||||
quinn::EndpointConfig::default(),
|
||||
Some(server_config.clone()),
|
||||
socket,
|
||||
Arc::new(quinn::TokioRuntime),
|
||||
)
|
||||
.with_context(|| format!("cannot initialize HTTP/3 UDP {}", listener.address))?;
|
||||
Ok(Bound {
|
||||
endpoint,
|
||||
server_config,
|
||||
@@ -91,13 +159,77 @@ pub fn bind(listener: Listener, state: &App) -> Result<Bound> {
|
||||
settings,
|
||||
})
|
||||
}
|
||||
/// Derive the advertised endpoint from ACTUALLY bound sockets, never saved intent
|
||||
/// or a backend Alt-Svc header. A pending H3 enable is not advertised before restart.
|
||||
/// An established UDP socket is immutable until restart. Use the committed
|
||||
/// *desired* transport policy for new QUIC connections on that same socket;
|
||||
/// never apply settings from a listener planned on a different IP/port.
|
||||
fn desired_for_bound(address: SocketAddr, tls: bool, desired: &[Listener]) -> Option<Settings> {
|
||||
desired
|
||||
.iter()
|
||||
.find(|listener| listener.address == address && listener.tls == tls)
|
||||
.and_then(|listener| listener.http3)
|
||||
}
|
||||
|
||||
/// Compare immutable listener properties. H3 limits other than an increase in
|
||||
/// the startup connection capacity are hot-applied for *new* connections.
|
||||
/// UDP socket creation/removal (and H1/H2 TLS/ALPN settings) still need restart.
|
||||
pub fn listeners_require_restart(active: &[Listener], desired: &[Listener]) -> bool {
|
||||
if active.len() != desired.len() {
|
||||
return true;
|
||||
}
|
||||
active.iter().any(|bound| {
|
||||
let Some(next) = desired.iter().find(|next| next.address == bound.address) else {
|
||||
return true;
|
||||
};
|
||||
if bound.tls != next.tls
|
||||
|| bound.http2 != next.http2
|
||||
|| bound.http2_settings != next.http2_settings
|
||||
|| bound.h2c != next.h2c
|
||||
{
|
||||
return true;
|
||||
}
|
||||
match (bound.http3, next.http3) {
|
||||
(None, None) => false,
|
||||
(Some(initial), Some(updated)) => {
|
||||
updated.max_concurrent_connections > initial.max_concurrent_connections
|
||||
|| updated.udp_receive_buffer_bytes != initial.udp_receive_buffer_bytes
|
||||
|| updated.udp_send_buffer_bytes != initial.udp_send_buffer_bytes
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Origin-supplied Alt-Svc is never trusted. An HTTP/3 opt-out emits `clear`
|
||||
/// on TLS responses while the existing QUIC socket is still bound, so clients
|
||||
/// are told to discard their cached alternative service immediately.
|
||||
pub fn withdraw_advertisement(
|
||||
headers: &mut HeaderMap,
|
||||
local: Option<SocketAddr>,
|
||||
tls: bool,
|
||||
active: &[Listener],
|
||||
) {
|
||||
headers.remove("alt-svc");
|
||||
if tls
|
||||
&& local.is_some_and(|local| {
|
||||
active
|
||||
.iter()
|
||||
.any(|binding| binding.matches(local, true) && binding.http3.is_some())
|
||||
})
|
||||
{
|
||||
headers.insert("alt-svc", HeaderValue::from_static("clear"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Advertise only an actually active H3 UDP listener, using the newest
|
||||
/// committed Alt-Svc lifetime. Disabling H3 for the listener or vhost publishes
|
||||
/// `Alt-Svc: clear` without waiting for a restart.
|
||||
pub fn advertise(
|
||||
headers: &mut HeaderMap,
|
||||
local: Option<SocketAddr>,
|
||||
tls: bool,
|
||||
active: &[Listener],
|
||||
desired: &[Listener],
|
||||
host_http3_enabled: bool,
|
||||
) {
|
||||
headers.remove("alt-svc");
|
||||
if !tls {
|
||||
@@ -106,11 +238,19 @@ pub fn advertise(
|
||||
let Some(local) = local else {
|
||||
return;
|
||||
};
|
||||
if let Some(settings) = active
|
||||
.iter()
|
||||
.find(|binding| binding.matches(local, true))
|
||||
.and_then(|binding| binding.http3)
|
||||
{
|
||||
let Some(bound) = active.iter().find(|binding| binding.matches(local, true)) else {
|
||||
return;
|
||||
};
|
||||
// A desired H3 enable on a socket with no UDP listener is pending restart.
|
||||
if bound.http3.is_none() {
|
||||
return;
|
||||
}
|
||||
let settings = desired_for_bound(bound.address, bound.tls, desired);
|
||||
if !host_http3_enabled || settings.is_none() {
|
||||
withdraw_advertisement(headers, Some(local), true, active);
|
||||
return;
|
||||
}
|
||||
if let Some(settings) = settings {
|
||||
let value = format!("h3=\":{}\"; ma={}", local.port(), settings.alt_svc_max_age);
|
||||
if let Ok(value) = HeaderValue::from_str(&value) {
|
||||
headers.insert("alt-svc", value);
|
||||
@@ -131,6 +271,29 @@ pub async fn serve(bound: Bound, router: Router, state: Arc<App>, grace: Duratio
|
||||
let Some(incoming)=incoming else {break;};
|
||||
let peer=incoming.remote_address();
|
||||
if !incoming.remote_address_validated() {let _=incoming.retry();continue;}
|
||||
// Listener setting saves are already published through ArcSwap.
|
||||
// Apply new limits to fresh QUIC handshakes on this bound UDP
|
||||
// address, without mutating established connections.
|
||||
let desired = {
|
||||
let snapshot = state.config.load();
|
||||
desired_for_bound(bound.address, true, &snapshot.listeners)
|
||||
};
|
||||
let Some(settings) = desired else { incoming.refuse(); continue; };
|
||||
// Startup permits and Quinn's incoming capacity cannot be grown
|
||||
// without restarting the UDP endpoint. A lower limit can be
|
||||
// enforced dynamically without releasing live connection leases.
|
||||
let open = bound.settings.max_concurrent_connections as usize - slots.available_permits();
|
||||
let capacity = (settings.max_concurrent_connections as usize)
|
||||
.min(bound.settings.max_concurrent_connections as usize);
|
||||
if open >= capacity {
|
||||
state.metrics.http3_rejected_connections.fetch_add(1,Ordering::Relaxed);
|
||||
state.metrics.rejected_connections.fetch_add(1,Ordering::Relaxed);
|
||||
state.logs.push_protocol_error(
|
||||
"http3_connection_limit_rejected", "HTTP/3 configured connection cap reached",
|
||||
peer, bound.address, "quic", "3", 0, None,
|
||||
);
|
||||
incoming.refuse();continue;
|
||||
}
|
||||
let Ok(permit)=slots.clone().try_acquire_owned() else {
|
||||
state.metrics.http3_rejected_connections.fetch_add(1,Ordering::Relaxed);
|
||||
state.metrics.rejected_connections.fetch_add(1,Ordering::Relaxed);
|
||||
@@ -141,9 +304,21 @@ pub async fn serve(bound: Bound, router: Router, state: Arc<App>, grace: Duratio
|
||||
incoming.refuse();continue;
|
||||
};
|
||||
let mut config=bound.server_config.clone();
|
||||
let transport = match settings.transport() {
|
||||
Ok(transport) => transport,
|
||||
Err(error) => {
|
||||
state.logs.push_protocol_error(
|
||||
"http3_transport_config_error", error,
|
||||
peer, bound.address, "quic", "3", 0, None,
|
||||
);
|
||||
incoming.refuse();continue;
|
||||
}
|
||||
};
|
||||
config.transport = Arc::new(transport);
|
||||
let Some(crypto)=state.config.load().quic_tls.clone() else {incoming.refuse();continue;};
|
||||
// Certificate changes apply to new handshakes without changing
|
||||
// active transport limits or regenerating the Retry token key.
|
||||
// Existing QUIC sessions keep their negotiated transport limits.
|
||||
// New sessions get current limits and certificate without
|
||||
// regenerating the Retry token key.
|
||||
config.crypto=crypto;
|
||||
let connecting=match incoming.accept_with(Arc::new(config)) {
|
||||
Ok(c)=>c,
|
||||
@@ -155,7 +330,7 @@ pub async fn serve(bound: Bound, router: Router, state: Arc<App>, grace: Duratio
|
||||
}
|
||||
};
|
||||
let router=router.clone();let state=state.clone();let address=bound.address;
|
||||
let limit=bound.settings.max_concurrent_bidistream as usize;
|
||||
let limit=settings.max_concurrent_bidistream as usize;
|
||||
tasks.spawn(async move {
|
||||
let connection=tokio::select! {
|
||||
_=state.shutdown.cancelled()=>return,
|
||||
@@ -251,6 +426,7 @@ async fn connection_loop(
|
||||
drop(resolver);continue;
|
||||
}
|
||||
let router=router.clone();let state=state.clone();let peer=connection.remote_address();let tls_sni=tls_sni.clone();
|
||||
let quic_diagnostics = connection.clone();
|
||||
requests.spawn(async move {
|
||||
let resolved=tokio::time::timeout(state.header_timeout,resolver.resolve_request()).await;
|
||||
let (mut request,stream)=match resolved {
|
||||
@@ -403,6 +579,27 @@ async fn connection_loop(
|
||||
} else {
|
||||
log_h3_response_complete(peer, &stream_id, response_status, sent_body_bytes);
|
||||
}
|
||||
// Connection-level cumulative counters (not per-stream
|
||||
// deltas). Useful for distinguishing QUIC loss/cwnd/MTU
|
||||
// limitations from application CPU when benchmarking.
|
||||
if sent_body_bytes >= 8 * 1024 * 1024 || expected.is_some_and(|n| n >= 8 * 1024 * 1024) {
|
||||
let stats = quic_diagnostics.stats();
|
||||
tracing::debug!(
|
||||
%peer, local=%local, stream_id=%stream_id,
|
||||
response_bytes=sent_body_bytes,
|
||||
request_content_length=?expected,
|
||||
quic_udp_tx_datagrams=stats.udp_tx.datagrams,
|
||||
quic_udp_tx_ios=stats.udp_tx.ios,
|
||||
quic_udp_rx_datagrams=stats.udp_rx.datagrams,
|
||||
quic_sent_packets=stats.path.sent_packets,
|
||||
quic_lost_packets=stats.path.lost_packets,
|
||||
quic_congestion_events=stats.path.congestion_events,
|
||||
quic_rtt_ms=stats.path.rtt.as_secs_f64() * 1000.0,
|
||||
quic_cwnd_bytes=stats.path.cwnd,
|
||||
quic_path_mtu=stats.path.current_mtu,
|
||||
"HTTP/3 bulk transfer transport snapshot"
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
Ok(None)=>break,
|
||||
|
||||
@@ -7,6 +7,10 @@ pub struct Settings {
|
||||
pub max_concurrent_unistream: u32,
|
||||
/// QUIC idle timeout in seconds; zero disables the transport idle timeout.
|
||||
pub max_idle_timeout: u64,
|
||||
/// Desired effective UDP socket receive buffer (bytes). 0 = OS default.
|
||||
pub udp_receive_buffer_bytes: u32,
|
||||
/// Desired effective UDP socket send buffer (bytes). 0 = OS default.
|
||||
pub udp_send_buffer_bytes: u32,
|
||||
}
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
@@ -16,6 +20,8 @@ impl Default for Settings {
|
||||
max_concurrent_bidistream: 100,
|
||||
max_concurrent_unistream: 100,
|
||||
max_idle_timeout: 10,
|
||||
udp_receive_buffer_bytes: 4 * 1024 * 1024,
|
||||
udp_send_buffer_bytes: 4 * 1024 * 1024,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,6 +44,15 @@ impl Settings {
|
||||
self.max_idle_timeout <= 86400,
|
||||
"HTTP/3 idle timeout must be 0..86400 seconds"
|
||||
);
|
||||
for (name, value) in [
|
||||
("receive", self.udp_receive_buffer_bytes),
|
||||
("send", self.udp_send_buffer_bytes),
|
||||
] {
|
||||
ensure!(
|
||||
value == 0 || (64 * 1024..=16 * 1024 * 1024).contains(&value),
|
||||
"HTTP/3 UDP {name} buffer must be 0 or 65536..16777216 bytes"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn transport(&self) -> Result<quinn::TransportConfig> {
|
||||
|
||||
+107
-9
@@ -41,9 +41,15 @@ mod tests {
|
||||
}
|
||||
.validate()
|
||||
.is_err());
|
||||
assert!(Settings { udp_receive_buffer_bytes: 1, ..Default::default() }
|
||||
.validate().is_err());
|
||||
assert!(Settings { udp_send_buffer_bytes: 16 * 1024 * 1024 + 1, ..Default::default() }
|
||||
.validate().is_err());
|
||||
assert!(Settings { udp_send_buffer_bytes: 0, ..Default::default() }
|
||||
.validate().is_ok());
|
||||
}
|
||||
#[test]
|
||||
fn alt_svc_only_for_actual_tls_udp_listener() {
|
||||
fn alt_svc_uses_latest_committed_lifetime_and_only_bound_udp() {
|
||||
let binding = Listener {
|
||||
address: "127.0.0.1:8443".parse().unwrap(),
|
||||
tls: true,
|
||||
@@ -52,19 +58,111 @@ mod tests {
|
||||
h2c: false,
|
||||
http3: Some(Settings::default()),
|
||||
};
|
||||
let active = [binding];
|
||||
let mut h = HeaderMap::new();
|
||||
h.insert("alt-svc", "h3=\":9999\"".parse().unwrap());
|
||||
advertise(&mut h, Some(binding.address), true, &[binding]);
|
||||
advertise(&mut h, Some(binding.address), true, &active, &active, true);
|
||||
assert_eq!(h["alt-svc"], "h3=\":8443\"; ma=3600");
|
||||
advertise(&mut h, Some(binding.address), false, &[binding]);
|
||||
|
||||
let mut updated = binding;
|
||||
updated.http3.as_mut().unwrap().alt_svc_max_age = 19;
|
||||
advertise(&mut h, Some(binding.address), true, &active, &[updated], true);
|
||||
assert_eq!(h["alt-svc"], "h3=\":8443\"; ma=19");
|
||||
|
||||
advertise(&mut h, Some(binding.address), false, &active, &[updated], true);
|
||||
assert!(!h.contains_key("alt-svc"));
|
||||
advertise(
|
||||
&mut h,
|
||||
Some("127.0.0.2:8443".parse().unwrap()),
|
||||
true,
|
||||
&[binding],
|
||||
);
|
||||
advertise(&mut h, Some("127.0.0.2:8443".parse().unwrap()), true, &active, &[updated], true);
|
||||
assert!(!h.contains_key("alt-svc"));
|
||||
|
||||
// An intended but not bound H3 socket MUST NOT be advertised.
|
||||
let mut previously_h2_only = binding;
|
||||
previously_h2_only.http3 = None;
|
||||
advertise(&mut h, Some(binding.address), true, &[previously_h2_only], &[updated], true);
|
||||
assert!(!h.contains_key("alt-svc"));
|
||||
}
|
||||
#[test]
|
||||
fn disabling_h3_withdraws_cached_advertisement() {
|
||||
let binding = Listener {
|
||||
address: "0.0.0.0:443".parse().unwrap(),
|
||||
tls: true,
|
||||
http2: true,
|
||||
http2_settings: None,
|
||||
h2c: false,
|
||||
http3: Some(Settings::default()),
|
||||
};
|
||||
let local = Some("192.0.2.2:443".parse().unwrap());
|
||||
let mut h = HeaderMap::new();
|
||||
// Vhost opt-out: immediate Alt-Svc: clear without affecting another host.
|
||||
advertise(&mut h, local, true, &[binding], &[binding], false);
|
||||
assert_eq!(h["alt-svc"], "clear");
|
||||
// Listener opt-out: listener removal requires restart but the header
|
||||
// is withdrawn immediately, while existing H3 sessions may survive.
|
||||
let mut disabled = binding;
|
||||
disabled.http3 = None;
|
||||
advertise(&mut h, local, true, &[binding], &[disabled], true);
|
||||
assert_eq!(h["alt-svc"], "clear");
|
||||
advertise(&mut h, local, true, &[binding], &[], true);
|
||||
assert_eq!(h["alt-svc"], "clear");
|
||||
withdraw_advertisement(&mut h, local, true, &[binding]);
|
||||
assert_eq!(h["alt-svc"], "clear");
|
||||
withdraw_advertisement(&mut h, local, false, &[binding]);
|
||||
assert!(!h.contains_key("alt-svc"));
|
||||
}
|
||||
#[test]
|
||||
fn h3_hot_changes_do_not_misreport_restart() {
|
||||
let active = Listener {
|
||||
address: "127.0.0.1:8443".parse().unwrap(),
|
||||
tls: true,
|
||||
http2: true,
|
||||
http2_settings: Some(crate::settings::Http2Connections::default()),
|
||||
h2c: false,
|
||||
http3: Some(Settings::default()),
|
||||
};
|
||||
assert!(!listeners_require_restart(&[active], &[active]));
|
||||
let mut next = active;
|
||||
let h3 = next.http3.as_mut().unwrap();
|
||||
h3.alt_svc_max_age = 0;
|
||||
h3.max_concurrent_connections = 40;
|
||||
h3.max_concurrent_bidistream = 20;
|
||||
h3.max_concurrent_unistream = 30;
|
||||
h3.max_idle_timeout = 90;
|
||||
assert!(!listeners_require_restart(&[active], &[next]));
|
||||
// A capacity increase above the startup socket/permit cap needs restart.
|
||||
next.http3.as_mut().unwrap().max_concurrent_connections = 513;
|
||||
assert!(listeners_require_restart(&[active], &[next]));
|
||||
next = active;
|
||||
next.http3.as_mut().unwrap().udp_receive_buffer_bytes = 8 * 1024 * 1024;
|
||||
assert!(listeners_require_restart(&[active], &[next]));
|
||||
next = active;
|
||||
next.http3.as_mut().unwrap().udp_send_buffer_bytes = 8 * 1024 * 1024;
|
||||
assert!(listeners_require_restart(&[active], &[next]));
|
||||
next = active;
|
||||
next.http3 = None;
|
||||
assert!(listeners_require_restart(&[active], &[next]));
|
||||
next = active;
|
||||
next.address = "127.0.0.1:9443".parse().unwrap();
|
||||
assert!(listeners_require_restart(&[active], &[next]));
|
||||
next = active;
|
||||
next.http2 = false;
|
||||
assert!(listeners_require_restart(&[active], &[next]));
|
||||
assert!(listeners_require_restart(&[active], &[]));
|
||||
}
|
||||
#[test]
|
||||
fn udp_buffers_keep_legacy_config_compatible() {
|
||||
let old = r#"{"alt_svc_max_age":3600,"max_concurrent_connections":512,"max_concurrent_bidistream":100,"max_concurrent_unistream":100,"max_idle_timeout":10}"#;
|
||||
let settings: Settings = serde_json::from_str(old).unwrap();
|
||||
assert_eq!(settings.udp_receive_buffer_bytes, 4 * 1024 * 1024);
|
||||
assert_eq!(settings.udp_send_buffer_bytes, 4 * 1024 * 1024);
|
||||
assert_eq!(serde_json::from_str::<Settings>(&serde_json::to_string(&settings).unwrap()).unwrap(), settings);
|
||||
}
|
||||
#[test]
|
||||
fn udp_socket_tuning_produces_readable_socket_options() {
|
||||
let socket = UdpSocket::bind("127.0.0.1:0").unwrap();
|
||||
let address = socket.local_addr().unwrap();
|
||||
configure_udp_socket(&socket, address, Settings::default());
|
||||
let socket = socket2::SockRef::from(&socket);
|
||||
assert!(socket.recv_buffer_size().unwrap() > 0);
|
||||
assert!(socket.send_buffer_size().unwrap() > 0);
|
||||
}
|
||||
#[test]
|
||||
fn client_cancellation_is_a_stream_event_not_an_http_400() {
|
||||
|
||||
@@ -172,10 +172,10 @@ fn render_core(app: &App, snapshot: &crate::config::Snapshot, out: &mut String,
|
||||
f.add(
|
||||
"rust_proxy_restart_required",
|
||||
"gauge",
|
||||
"Listener/exporter intent differs from startup state.",
|
||||
"Listener/exporter changes requiring a restart.",
|
||||
"",
|
||||
u8::from(
|
||||
snapshot.listeners != app.active_listeners
|
||||
crate::http3::listeners_require_restart(&app.active_listeners, &snapshot.listeners)
|
||||
|| settings.restart_differs(&app.active_prometheus),
|
||||
),
|
||||
);
|
||||
@@ -428,7 +428,6 @@ fn render_vhosts(snapshot: &crate::config::Snapshot, f: &mut Families) {
|
||||
counter.load(Ordering::Relaxed),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -291,11 +291,14 @@ fn finish_headers(
|
||||
HeaderValue::from_static("private, no-store"),
|
||||
);
|
||||
}
|
||||
if frontend.is_some_and(|frontend| !frontend.http3_enabled) {
|
||||
response.headers_mut().remove("alt-svc");
|
||||
} else {
|
||||
crate::http3::advertise(response.headers_mut(), local, tls, active);
|
||||
}
|
||||
crate::http3::advertise(
|
||||
response.headers_mut(),
|
||||
local,
|
||||
tls,
|
||||
active,
|
||||
&snapshot.listeners,
|
||||
!frontend.is_some_and(|frontend| !frontend.http3_enabled),
|
||||
);
|
||||
}
|
||||
fn apply_timing_headers(
|
||||
response: &mut Response,
|
||||
|
||||
@@ -316,14 +316,24 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
.get::<RedirectResponse>()
|
||||
.is_some_and(|redirect| !redirect.http3_enabled)
|
||||
{
|
||||
response.headers_mut().remove("alt-svc");
|
||||
crate::http3::withdraw_advertisement(
|
||||
response.headers_mut(),
|
||||
local,
|
||||
transport_tls,
|
||||
&state.active_listeners,
|
||||
);
|
||||
}
|
||||
if response
|
||||
.extensions()
|
||||
.get::<LocalHostResponse>()
|
||||
.is_some_and(|item| !item.http3_enabled)
|
||||
{
|
||||
response.headers_mut().remove("alt-svc");
|
||||
crate::http3::withdraw_advertisement(
|
||||
response.headers_mut(),
|
||||
local,
|
||||
transport_tls,
|
||||
&state.active_listeners,
|
||||
);
|
||||
}
|
||||
let global_compression = &snapshot.envelope.config.compression;
|
||||
// All scoped policies have the global codec ceiling. With no accepted
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"use strict";
|
||||
// Zero-dependency browser calculation regressions (run: node tests/cache_outcomes.cjs).
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const vm = require("node:vm");
|
||||
const root = path.join(__dirname, "..");
|
||||
const src = fs.readFileSync(path.join(root, "src/admin/web/app/runtime.js"), "utf8");
|
||||
const begin = src.indexOf("function cacheOutcomeMix(deltas) {");
|
||||
const end = src.indexOf("\nfunction renderCacheMix(mix) {", begin);
|
||||
assert.ok(begin >= 0 && end > begin, "production cacheOutcomeMix function exists");
|
||||
const mix = vm.runInNewContext(`${src.slice(begin, end)}; cacheOutcomeMix;`);
|
||||
const result = mix([10, 4, 2, 3]);
|
||||
assert.deepEqual(Array.from(result.counts), [4, 2, 3, 1]);
|
||||
assert.deepEqual(Array.from(result.percentages), [40, 20, 30, 10]);
|
||||
assert.equal(result.requests, 10);
|
||||
assert.deepEqual(Array.from(mix([12, 0, 0, 0]).counts), [0, 0, 0, 12]);
|
||||
assert.deepEqual(Array.from(mix([9, 0, 0, 9]).counts), [0, 0, 9, 0]);
|
||||
for (const invalid of [[0, 0, 0, 0], [1, 2, 0, 0], [10, -1, 0, 0], [NaN, 0, 0, 0], [], null]) {
|
||||
assert.equal(mix(invalid), null, `invalid sample ${invalid} is not charted`);
|
||||
}
|
||||
const widgets = JSON.parse(fs.readFileSync(path.join(root, "src/admin/web/dashboard-widgets.json"), "utf8"));
|
||||
const defaults = JSON.parse(fs.readFileSync(path.join(root, "src/admin/web/dashboard-defaults.json"), "utf8"));
|
||||
assert.equal(widgets.filter(widget => widget.id === "cache_outcomes").length, 1);
|
||||
assert.equal(defaults.dashboard_widgets.filter(id => id === "cache_outcomes").length, 1);
|
||||
assert.equal(widgets.find(widget => widget.id === "cache_outcomes").default_hidden, true, "cache outcomes is opt-in");
|
||||
assert.equal(defaults.dashboard_hidden_widgets.filter(id => id === "cache_outcomes").length, 1, "hidden in factory layout");
|
||||
const html = fs.readFileSync(path.join(root, "src/admin/web/partials/overview-metrics.html"), "utf8");
|
||||
assert.match(html, /data-dashboard-widget="cache_outcomes"/);
|
||||
for (const key of ["hit", "miss", "bypass", "other"]) {
|
||||
assert.match(html, new RegExp(`id="cache-mix-${key}"`));
|
||||
assert.match(html, new RegExp(`id="cache-mix-${key}-percent"`));
|
||||
}
|
||||
const strings = ["en_US", "pl_pl"].map(language =>
|
||||
JSON.parse(fs.readFileSync(path.join(root, `langs/${language}.json`), "utf8")).translations);
|
||||
for (const translation of strings) {
|
||||
for (const key of ["cacheMixTitle", "cacheMixHelp", "cacheMixOther", "cacheMixSample", "cacheMixWaiting", "cacheMixAria",
|
||||
"h3_udp_receive_buffer_mib", "h3_udp_send_buffer_mib", "http3ApplyPolicy"]) {
|
||||
assert.ok(translation[key], `translation missing: ${key}`);
|
||||
}
|
||||
}
|
||||
console.log("Cache outcomes, manifest and locale regression checks passed.");
|
||||
Reference in new issue
Block a user