Compare commits

...
13 Commits
Author SHA1 Message Date
Mateusz Gruszczyński 51f22a72cc v0.35.3 2026-11-06 23:38:00 +01:00
Mateusz Gruszczyński f4606cce18 v0.35.2 2026-11-06 23:16:36 +01:00
gru 09414dfb09 Merge pull request 'Release/0.35.1' (#3) from release/0.35.1 into master
Reviewed-on: #3
2026-11-05 21:54:24 +01:00
gru eba9f89283 v0.35.1: performance, AGPL licensing and documentation overhaul
- Web Server reads static files inline; proxy middleware chain flattened
- About window with license summary and project links; AGPL-3.0-or-later
- Rewritten documentation: feature list, screenshots, performance guide
- Fix false spikes in per-Vhost request-rate charts
2026-11-05 18:29:56 +01:00
Mateusz Gruszczyński d3b492874b v0.35.0 2026-10-31 23:42:58 +01:00
Mateusz Gruszczyński 2c03fcf248 v0.34.16 2026-10-27 13:49:21 +01:00
Mateusz Gruszczyński f27fbf6a30 v0.34.16 2026-10-27 13:22:43 +01:00
Mateusz Gruszczyński e364392659 v0.34.15 2026-10-27 12:18:20 +01:00
Mateusz Gruszczyński 62b7574553 v0.34.14 2026-10-27 09:27:00 +01:00
Mateusz Gruszczyński 5c2e50ec06 0.34.13_cleaup_code 2026-10-26 14:14:41 +01:00
Mateusz Gruszczyński ec46a96a29 v0.34.13 2026-10-25 17:47:01 +01:00
Mateusz Gruszczyński 24406b9580 v0.24.13 2026-10-25 17:46:10 +01:00
Mateusz Gruszczyński ed7e3377c1 v0.24.13 2026-10-25 17:45:57 +01:00
283 changed files with 13503 additions and 3071 deletions

No files matched your search

+1
View File
@@ -9,3 +9,4 @@ __pycache__/
problems.txt
todo/
scripts/routeros-oci-build/.env
dist
+10 -3
View File
@@ -1,17 +1,24 @@
# RustProxy source and documentation maintenance
Current source version: **0.34.12**. The Cargo package and binary are named `rust-proxy`.
Current source version: **0.35.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.
Before editing, read [README.md](README.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.
## Keep documentation in sync
- Write product documentation in English. Prefer focused instructions: purpose, configuration, defaults, restrictions, verification and common failures. Keep change history technical but user/operator-facing, not a series of profiling notes.
- Update the guide, [docs index](docs/README.md), [changelog](CHANGELOG.md), embedded Reader registry (`src/admin/documentation/documents.rs`) and API schema/tests together when functionality changes.
- Update the guide, [feature list](docs/FEATURES.md), [docs index](docs/README.md), [changelog](CHANGELOG.md), embedded Reader registry (`src/admin/documentation/documents.rs`) and API schema/tests together when functionality changes.
- Reader Markdown is compiled into the binary using `include_str!`. Renaming/deleting a guide without updating the registry breaks compilation. Avoid broken relative links.
- Keep per-component optional defaults accurate (for example COLD off, Prometheus off, backend `tls_verify` off unless explicitly set, and optional Web Server `sendfile` off). Mark source-only or untested changes accordingly.
- Maintain existing `langs/*.json` packs; `build.rs` discovers available locales. Do not add credentials, databases, real certificates, environment secrets or build artifacts to source archives.
## Authentication provider extension contract
- Keep provider implementations isolated under `src/<provider>/mod.rs` and `tests.rs`; the request pipeline only sees the object-safe `auth_provider::Provider` trait and the single `CompiledFrontend.auth_provider` slot. Preserve existing Vhost JSON fields on upgrades.
- Prefer the shared `forward_auth::Adapter` transport for compatible HTTP verifiers, and reserve protected-host callback routes through `reserved_path` / `serve_reserved` only when required.
- Return fail-closed provider errors using `auth_provider::gateway_failure` and one `src/admin/web/error_auth_provider.html` template (with escaped provider names); never create a separate HTML file per vendor or expose backend diagnostics to clients. `AuthenticationResponse` prevents custom Vhost error pages from overriding auth errors/redirects, and `Identity` / `CookieSink` keep identities and cookies request-scoped.
- Keep the `authentication` provider/method selector in `auth_provider/choice.rs` separate from the shared OIDC integration transport. Extend the allowed provider/method combinations without duplicating OIDC protocol code. Accept legacy Vhost settings without a selector; when present, reject selectors inconsistent with enabled settings. Update the panel, mutual-exclusion validation, config compiler, OpenAPI, docs and tests together. Prove access/denial, redirects, header stripping, timeouts, and reserved routes with real verifiers before deployment.
## Engineering safeguards
- Keep proxy traffic independent of control-plane database/provider calls. Prefer asynchronous streaming, bounded queues, resource limits and explicit cancellation. Hold backend permits for the entire body or tunnel lifetime.
+79 -8
View File
@@ -1,12 +1,83 @@
# 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.12.
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 the latest release.
## 0.34.12 — cache diagnostics and optional chart axes (source candidate)
## 0.35.3 — Overview layout fixes
- **Standard Overview layout restored:** the quick-health tiles added in 0.35.2 (healthy backends, error share, requests rejected by limits, nearest certificate expiry) are now hidden by default and placed at the end of the Overview, so they no longer shift the standard block layout. Enable them in the layout editor when needed.
- **Fixed:** the "Saved default" status next to the history sample count was always visible and looked like a fresh save notification. It now appears only for an unsaved temporary choice or briefly after **Remember choice**.
- **Upgrade notes:** no configuration changes are required. If 0.35.2 already changed your Overview, use **Unlock layout → Reset layout** to restore the default arrangement, or hide the tiles in the layout editor.
## 0.35.2 — memory fix, health check options and Overview tiles
- **Memory use restored:** with a large HOT cache, 0.35.1 used about 25% more memory than 0.35.0 (for example 1.1–1.2 GiB instead of about 800 MiB with a 600 MiB cache). The default build is back to the 0.35.0 memory profile. The faster alternative allocator is available as an optional build feature for servers with spare RAM; the Performance guide explains the trade-off with measurements.
- **Health check port:** active TCP and HTTP health checks can probe a different port than the one receiving traffic, for example a status port 8079 for a backend serving on port 80. It can be set for a whole Vhost or per backend; host, TLS and address restrictions stay the same.
- **Expected response text:** HTTP health checks can require a text fragment in the response body, so a status page that answers 200 while reporting a degraded state marks the backend as down.
- **New Overview tiles:** healthy backends (with the names of failing ones), error share, requests rejected by limits and the nearest certificate expiry.
- **Certificate expiry metrics:** Prometheus now exports the expiry date and remaining days of every installed certificate, including manually uploaded ones, for alerting.
- **Safari:** the Overview no longer jumps when you scroll near the Backends table while live values refresh.
- **Fixed:** network interface traffic below 1 KiB/s is shown as a whole number of bytes instead of a long decimal.
- **Upgrade notes:** no configuration changes are required. Existing Overview layouts receive the new tiles at the end; hide or move them in the layout editor.
## 0.35.1 — performance, licensing and documentation
- **Faster Web Server:** static files are read directly by the request instead of being handed to a background thread pool. Small and large files are served up to 1.8× faster with lower latency and CPU use.
- **Faster reverse proxy and cache:** lighter request processing, header handling and traffic statistics collection. Proxied requests are about 3–7% faster and cache hits about 11% faster on the same hardware.
- **Vhost charts:** the per-Vhost request-rate chart no longer shows a false spike right after the page opens or when two refreshes arrive at the same moment.
- **Performance guide:** new documentation page with the benchmark method, a comparison with a similarly configured Nginx and production tuning advice.
- **About window:** redesigned with the project description, copyright, a GNU AGPL v3.0-or-later license summary and links to the project website, source code, built-in documentation and the full license text.
- **License:** RustProxy is published under the GNU Affero General Public License v3.0 or later. The license text is included in the built-in documentation.
- **Documentation:** rewritten project overview with a complete feature list, panel screenshots and links to [rustproxy.linuxiarz.pl](https://rustproxy.linuxiarz.pl/). Container, API access, post-quantum TLS and integration notes were merged into the related guides. The built-in Reader now displays tables and bold text.
- **Upgrade notes:** no configuration changes are required.
## 0.35.0 — encoded-slash proxy compatibility and release cleanup
- **Encoded slash compatibility:** `%2F` and `%2f` are accepted in request paths. RustProxy decodes them to `/` only for routing/security decisions and applies the same duplicate-slash normalization as literal `/`.
- **Preserve-mode forwarding:** when a location does not rewrite the path, the original encoded request target is retained for the upstream, including query strings. Applications that use encoded slashes inside path parameters therefore receive the same target the client sent.
- **Path hardening retained:** encoded `..` traversal after slash normalization, encoded backslash, `%`, `?`, `#`, `;`, controls, malformed percent escapes and invalid UTF-8 remain rejected.
- **Release gate cleanup:** source/tests are adjusted for the current Rust/Clippy gate without weakening runtime security checks.
## 0.34.16 — optional OIDC central login
- **Configuration size fix:** increased the total serialized configuration limit from 256 KiB to 2 MiB. The administration request limit, configuration file import, SQLite/history loading and NPM migration utility use the same expanded bound; the existing limits on Vhosts, backends and locations remain unchanged.
- **Vhost → Authentication:** separates **Identity provider** (Tinyauth, Authelia, authentik, Pocket ID, custom OIDC, HTTP Basic) from **Integration method** (Forward Auth, OIDC, HTTP Basic). Pocket ID/custom support OIDC; Tinyauth/Authelia/authentik support Forward Auth or OIDC. Only one method can be active per Vhost. An optional `{ "authentication": { "provider": "authelia", "method": "oidc" } }` selector is validated against the existing provider settings. Omitted selectors retain legacy `*.enabled` behavior, including OIDC without an explicitly named vendor. Existing passwords, application callbacks, sessions and stored secrets retain their original keys and behavior.
- **Cross-domain login:** a protected application can use an unrelated HTTPS issuer with OIDC Authorization Code, S256 PKCE, transaction-bound state and nonce, and a fixed callback on the application hostname. Optional internal backend-origin routing supports identity providers running behind this same RustProxy without a circular public proxy dependency. The login provider keeps its own cookies; RustProxy issues a separate encrypted, host-only application session cookie.
- **ID token verification:** issuer discovery and same-origin JWKS retrieval, RS256/ES256 signature verification, issuer/audience/authorized-party/expiry/nonce checks, bounded HTTP responses and fail-closed provider errors. No access tokens are forwarded to application backends. Application identity is injected from verified token claims as `Remote-Sub`, `Remote-Issuer`, `Remote-User`, and optional name, email and groups headers after stripping forged client values.
- **Session policy:** HTTPS-only, HttpOnly, SameSite=Lax cookies; an independent random session secret per Vhost; one-hour default lifetime; callback at `/_rustproxy/oidc/callback` and local logout at `/_rustproxy/oidc/logout`. Invalid logins, expired cookies and unavailable issuer responses never authorize upstream access.
- **Administration:** new OIDC fields in the Vhost panel, matching OpenAPI schema and English/Polish labels. All OIDC values are disabled by default. Source candidate: the Rust build and live OIDC compatibility checks have not been performed in the packaging environment.
## 0.34.15 — optional Authelia and authentik forward auth
- **Provider-neutral refactor:** Extracted `auth_provider::Provider` with unified selected-provider dispatch, verified identity/cookie handling and one `error_auth_provider.html` template. Removed three Tinyauth-only HTML template files; 502/504 titles still identify Tinyauth, Authelia or authentik and failure category. Shared `forward_auth` transport continues to be reused without embedding vendor-specific branching in the request pipeline.
- **Vhost → Authentication:** two additional, independently configurable choices: Authelia (`/api/authz/forward-auth`) and authentik (`/outpost.goauthentik.io/auth/nginx`), alongside unchanged Tinyauth and HTTP Basic Auth. Exactly one method can be active per Vhost; both new methods default off, with old configurations remaining compatible. Settings can be edited through the panel and vhosts configuration API.
- **Authelia:** bounded server-side verification, trusted original-request metadata, allowlisted identity headers and strict same-portal-origin login redirects. The Authelia portal and session cookies must be configured for each protected parent domain; a single portal URL cannot issue a valid browser session cookie for unrelated parent domains.
- **authentik:** supports forward auth (single application) with each protected host's reserved `/outpost.goauthentik.io` route automatically proxied to the configured outpost. The protected URL is passed to the outpost for SSO login; request cookies, identity headers and session-refresh `Set-Cookie` values are propagated with limits. Different parent domains can use one central authentik instance with distinct per-app proxy providers.
- **Fail closed:** verifier transport/invalid-response errors return 502; timeout returns 504; unauthenticated/denied requests never reach application origins. Provider identity is never accepted from incoming client headers. Cache sharing is disabled for protected requests; fail-closed auth responses are kept out of custom-error interception.
## 0.34.14 — Tinyauth gateway error pages
- **Tinyauth outage:** when RustProxy cannot reach the Tinyauth verifier (connect, DNS, TLS or transport error), it returns a built-in **502 Tinyauth connection error** HTML page rather than plain `502 Bad Gateway`. The page is local and self-contained; no failed authentication is bypassed and no backend address, certificate detail or transport error is revealed to the caller.
- **Tinyauth compatibility patch:** the Nginx verifier now receives one consistent auth-request header set. RustProxy removes ForwardAuth module markers (`X-Forwarded-Host`, `X-Forwarded-URI`) while preserving the verified client IP and original request URL. This resolves the `conflicting auth module headers` rejection introduced in Tinyauth 5.2.0 without disabling anti-spoofing checks. Added regression tests for conflicting module headers and forged client identity. Use the internal `/api/auth/nginx` endpoint.
- **Tinyauth timeout:** a verifier request that exceeds the configured deadline returns a distinct **504 Tinyauth timeout** page. Invalid verifier responses, unexpected HTTP statuses and oversized identity/response data return **502 Tinyauth verification error**. All retain no-store and the authenticated-request isolation marker, so Vhost/global error-page overrides do not mask Tinyauth's diagnostic category.
## 0.34.13 — Global security, section-based configuration API and panel UX
- **GeoIP:** on-demand data refresh moved from the isolated `POST /api/geoip/update` to `POST /api/config/geoip/update`, colocated with the revisioned GeoIP settings. This action does not change the config revision; the UI and RO/RW token allowlist use the new URL. Section paths now consistently use kebab-case, including `not-found-hosts` (the JSON property remains `not_found_hosts`). Old action/section paths are removed.
- **API:** all 23 supported configuration sections now have `GET /api/config/{section}` returning `{revision, section, value}` and `PUT /api/config/{section}` accepting `{expected_revision, value}`. PUT still returns the updated full `{revision, config}` envelope so clients can refresh all sections. Both methods use the same section registry. Unknown/removed sections are 404; stale writes return 409. Writes remain full section replacements, validated and committed atomically; there are no partial PATCH semantics.
- **Global security:** the canonical User-Agent blocklist is now `config.global_security.user_agent_blocking`. `PUT /api/config/global-security` accepts one `GlobalSecurity` object, including an explicit `user_agent_blocking`, and no longer requires a special two-object wrapper. The old independent `/api/config/user-agent-blocking` section is removed. Per-vhost User-Agent policies are unaffected.
- **Upgrade safety:** legacy top-level `config.user_agent_blocking` is accepted on full-document input, compiled into the nested policy and omitted from normalized saved configuration. A payload containing contradictory legacy and nested policies is rejected rather than silently enabling/disabling a rule. Raw unnormalized backup payloads retain the old value until migrated.
- **Grouped sections:** `logging` is now strictly `{logging, error_log}`; `response` is `{server_header, diagnostic_headers, compression}`. The old bare logging-only PUT shape is rejected to prevent partial group replacement. All other section PUT payloads retain their previous single-field value formats. RO API tokens may read individual sections; RW tokens may write them subject to existing restrictions.
- **Settings → Rules & headers → Global block / allow:** replaces the separate global User-Agent blocklist editor with a unified global country block/allow policy, the existing User-Agent patterns, IP/CIDR and allow-only dynamic DNS ACL, HTTP method allow/block rules, request Content-Type denylist, and an optional client-IP request-rate limit shared across public HTTP vhosts. Each control is disabled by default. Existing User-Agent settings are retained, not reset. Global and per-vhost restrictions are cumulative, and access checks run before upstream requests, redirects and cache invalidation; TCP/UDP Streams and the private admin listener are excluded.
- **Global security headers:** separate subtab for HSTS, nosniff, X-Frame-Options, Referrer-Policy, CSP and Permissions-Policy. Global values form a response baseline; explicit per-vhost header values override matching settings. HSTS is sent only over HTTPS. Test restrictive policies before production rollout.
- **Config/API:** additive `global_security` configuration object, backwards-compatible defaults, atomic `PUT /api/config/global-security` payload containing the complete nested `global_security` object (including `user_agent_blocking`). Country detection uses trusted client IP/GeoIP; global DNS ACL rules use the existing safe background resolver. English and Polish panel strings, OpenAPI and guides updated.
- **Panel layout:** separated static cookie-cache advice from response-header toggles, added space between transform numeric controls and minification checkboxes, and moved certificate access-permission guidance next to the generated command preview. The **Copy commands** button remains hidden until commands are generated.
## 0.34.12 — cache diagnostics and optional chart axes
- Origin-provided `Cache-Status`, `X-Cache`, `X-Cache-Hits`, `X-Cache-Reason`, `X-Cache-Resident-Age` and `X-Cache-Status` are retained on forwarded and cached responses; the HOT/COLD cache and conditional `304` refresh no longer erase these values from stored metadata. Cache-disabled and HIT-only display modes leave origin headers untouched.
- `Cache-Status` follows RFC 9211's ordered member model: preserve upstream members and append the RustProxy member if enabled. RustProxy reports `hit; ttl=...` for a local cache hit, `fwd=miss`/`fwd=bypass` for forwarded requests, and `fwd=stale; fwd-status=304` when revalidation used a `304`. The existing fixed reason goes into `detail="..."`; transformation metadata applies to RustProxy's member only. No non-standard `backend=` parameter is generated.
- Existing `X-Cache*` RustProxy diagnostics remain available for compatibility. To avoid deleting an upstream value on a naming conflict, displaced origin values are exposed separately as `X-Upstream-Cache*`, one field value for each original value. These aliases are non-standard; clients should prefer `Cache-Status` when available.
- Existing `X-Cache*` RustProxy diagnostics remain available so clients can refresh all sections. To avoid deleting an upstream value on a naming conflict, displaced origin values are exposed separately as `X-Upstream-Cache*`, one field value for each original value. These aliases are non-standard; clients should prefer `Cache-Status` when available.
- Added regression coverage for origin/proxy naming collisions, disable/HIT-only modes, HOT/COLD storage, RFC 9211 member order and metadata generated after revalidation.
- **Appearance → Charts:** optional account-scoped **Show X/Y scales** switch, off by default. On sampled line plots, HTML labels show the exact Y range used by the existing SVG renderer (including fixed percentage and adaptive Vhost/Docker ranges) and the X sample indices. Charts with separately normalized HTTP/WebSocket protocol lines identify each series' own Y maximum instead of inventing a common scale. Missing/compacted samples are not mislabeled as minutes; any clipped Vhost peak remains available in the existing legend. Disable the switch to restore original geometry. Donuts, gauges and categorical bars are unchanged.
- The new preference is included in API/Reader docs and both interface languages. Existing accounts retain their chart style and the new switch stays off until explicitly enabled.
@@ -25,7 +96,7 @@ Release history focused on **features, settings, default behavior, compatibility
- **Web Server:** generate document-root permission commands directly below its path, updating them for input, edits, resets and presets. Include parent traversal, read access to the served tree and default ACLs for new content without granting RustProxy write access. The permission walk does not follow child symlinks. PHP-FPM and systemd/MAC policies remain separately managed.
- **API and documentation:** add a session-authenticated, CSRF-protected certificate inspection endpoint with matching OpenAPI/access regressions; update English/Polish panel text and filesystem guidance. General administration tokens and certificate-only tokens cannot use the inspection endpoint. Package and lockfile are aligned to **0.34.10**; native Cargo and protocol acceptance are still required for this source candidate.
## 0.34.9 — experimental TLS post-quantum key exchange (source candidate)
## 0.34.9 — experimental TLS post-quantum key exchange
- **Dashboard source maintenance:** panel styles are organized into eight modules with consolidated responsive conditions and a deterministic build step. Login and the dashboard continue to load one fingerprinted stylesheet; rebuilding the binary applies changes. Source checks and browser regression tooling cover the module order and UI cascade. Native Rust validation remains required for this source candidate.
- **CSS build preflight fix:** ship local `scripts`/`build` package markers so unrelated installed Python packages cannot hide `build.panel_css` or the application scripts. This fixes `dev.sh` startup/repair, the CSS audit and packaging imports on affected environments; generated styles remain unchanged.
@@ -48,7 +119,7 @@ Release history focused on **features, settings, default behavior, compatibility
- Only client hard-reload `Cache-Control: no-cache`, `max-age=0` and `Pragma: no-cache` are ignored when considering a **fresh HIT**, retaining `Age` and hit counters. Kept the original `Cache-Control: no-store` bypass, `only-if-cached` 504 on cache MISS and no background origin refresh on HIT, and `min-fresh`/`max-stale`/positive `max-age` freshness constraints (including explicit `max-stale=0` during grace). Original request headers, Vary, access/privacy rules and Web Server handling remain intact.
- Source-level regression cases cover hard reload, fallback TTL, validator revalidation, no-store/private/Surrogate protections, zero-TTL stale-if-error, min-fresh/max-stale, and `only-if-cached` for proxy and Web Server. Native Rust compilation and runtime acceptance are required before a production release claim. This remains a Varnish-inspired policy subset, not a VCL clone.
## 0.34.6 — Docker live logs, neutral Publish switch and Vhost Content-Type rules (source candidate)
## 0.34.6 — Docker live logs, neutral Publish switch and Vhost Content-Type rules
- **Security / ordinary Vhosts:** optional per-vhost list of up to 64 blocked request media types (`multipart/form-data`, `application/json`, `image/*`). Matches case-insensitively, ignores MIME parameters, returns 403 before any proxy, Web Server/PHP-FPM, Custom location or cache response. Missing `Content-Type` is permitted; malformed/ambiguous headers are refused while filtering is enabled. Docker editor is unchanged. Default off for existing and new Vhosts.
- **Interface:** Vhosts → Security has a dedicated Content-Type subgroup with enable checkbox and multiline patterns. The Docker Publish toggle is smaller, unboxed and neutral with standard theme colors; instant save, rollback and live logs remain.
@@ -57,7 +128,7 @@ Release history focused on **features, settings, default behavior, compatibility
- **Open app URL:** use the selected listener's actual `binding.tls` and `binding.address` so HTTPS and nonstandard public ports are reflected in generated app links.
- **Release:** Cargo package and lockfile, embedded OpenAPI version, Polish/English UI hints and documentation aligned to 0.34.6. This is a source-only release candidate requiring native compilation and integration testing before deployment. Blocking `Content-Type` does not replace security updates for PHP or the application.
## 0.34.5 — Administration UX improvements (source candidate)
## 0.34.5 — Administration UX improvements
- **Docker vhosts traffic view:** added the ordinary Vhost status, cumulative requests/active/errors, EWMA latency, traffic/s and lifetime-rate chart in a collapsed-by-default disclosure below each published Docker vhost's configuration editor. One toolbar button expands/collapses all active publications; individual disclosure state survives live refreshes. Values use generated Vhost counters from the existing status stream. The browser chart begins collecting when the vhost is observed and resets on sign-out; backend/configuration behavior is unchanged.
@@ -80,7 +151,7 @@ Release history focused on **features, settings, default behavior, compatibility
- **Corner-style consistency:** additional Vhost dependent-field groups, cache/legend and metric-info controls, selected admin cards and the self-contained authenticated documentation page follow the saved Square/Subtle/Rounded/Extra corner preference. Circular status indicators/spinners remain circular by design; Original retains its legacy styling.
- Updated English and Polish labels, documentation and UI regression checks. This is a UI-only release; it does not change backend execution or configuration semantics. Native Rust builds and target-system acceptance remain required.
## 0.34.4 — Multi-profile ACLs, certificate override and mobile Docker details (source candidate)
## 0.34.4 — Multi-profile ACLs, certificate override and mobile Docker details
- ACL profile picker corners (dropdown, counter, rule cards, policy badges, selected chips and remove buttons) follow the dashboard corner-style setting (Square, Subtle, Rounded, Extra rounded); Original keeps its prior shape. ACL selection and rule evaluation are unchanged.
- Docker agent details now wrap certificate SHA-256 fingerprints on narrow screens.
@@ -90,7 +161,7 @@ Release history focused on **features, settings, default behavior, compatibility
- Docker publication editor shows the certificate/domain mismatch consent immediately below **Backend host IP**, with a small vertical gap.
- The source release needs native Rust compilation and target-system acceptance tests before production use.
## 0.34.3 — QUIC socket buffering, live HTTP/3 settings and cache outcome mix (source candidate)
## 0.34.3 — QUIC socket buffering, live HTTP/3 settings and cache outcome mix
- 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.
Generated
+20 -1
View File
@@ -1330,6 +1330,15 @@ dependencies = [
"windows-link",
]
[[package]]
name = "libmimalloc-sys"
version = "0.1.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9"
dependencies = [
"cc",
]
[[package]]
name = "libproc"
version = "0.14.11"
@@ -1427,6 +1436,15 @@ dependencies = [
"windows",
]
[[package]]
name = "mimalloc"
version = "0.1.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862"
dependencies = [
"libmimalloc-sys",
]
[[package]]
name = "mime"
version = "0.3.17"
@@ -1925,7 +1943,7 @@ dependencies = [
[[package]]
name = "rust-proxy"
version = "0.34.12"
version = "0.35.3"
dependencies = [
"anyhow",
"arc-swap",
@@ -1949,6 +1967,7 @@ dependencies = [
"ipnet",
"libc",
"metrics-process",
"mimalloc",
"quinn",
"rand 0.10.3",
"rcgen",
+11 -2
View File
@@ -1,9 +1,13 @@
[package]
name = "rust-proxy"
version = "0.34.12"
version = "0.35.3"
edition = "2021"
rust-version = "1.99"
rust-version = "1.99.1"
description = "RustProxy - independent reverse proxy with local administration"
authors = ["Mateusz Gruszczyński"]
license = "AGPL-3.0-or-later"
homepage = "https://rustproxy.linuxiarz.pl/"
repository = "https://git.linuxiarz.pl/gru/RustProxy"
publish = false
[dependencies]
@@ -13,6 +17,7 @@ ipnet = "2.11"
rpassword = "7.4"
zeroize = "1.8"
anyhow = "1.0"
mimalloc = { version = "0.1", default-features = false, optional = true }
libc = "0.2"
arc-swap = "1.7"
axum = { version = "0.8.9", default-features = false, features = ["http1", "http2", "json", "query", "tokio", "ws"] }
@@ -67,6 +72,10 @@ systemstat = "=0.2.7"
# Swagger assets are prepared by build.rs and embedded behind the admin guard.
# No adapter build script, implicit version fallback or Cargo-config dependency.
[features]
default = []
mimalloc = ["dep:mimalloc"]
[build-dependencies]
serde_json = "1.0"
+54 -110
View File
@@ -1,131 +1,75 @@
# RustProxy 0.34.12
# RustProxy
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.
**RustProxy** is a reverse proxy, web server, certificate manager and traffic monitor in a single service, managed from a built-in browser panel. It publishes many domains and backends without a separate configuration language: every setting is validated before it is applied and can be changed live from the panel or the API.
## Main capabilities
- Website and packages: [rustproxy.linuxiarz.pl](https://rustproxy.linuxiarz.pl/)
- Source code: [git.linuxiarz.pl/gru/RustProxy](https://git.linuxiarz.pl/gru/RustProxy)
- Documentation: [docs/README.md](docs/README.md) (also built into the panel at `/docs`)
- License: [GNU AGPL v3.0 or later](LICENSE)
| Area | Available features |
![RustProxy Overview dashboard](docs/images/overview.png)
## Highlights
| Area | Capabilities |
|---|---|
| Reverse proxy | HTTP/HTTPS origins, HTTP/1.1 and HTTP/2 upstream modes, health checks, load balancing, connection reuse, WebSockets, path-based routing, retries and error fallbacks |
| Frontends | HTTP, HTTPS, HTTP/2, optional HTTP/3 (QUIC), named listeners, domain and wildcard routing, redirects, TLS certificates |
| Web Server | Static content, downloads, directory/index policy, rewrite and `try_files`, selected `.htaccess` rules, optional PHP-FPM, application presets |
| Access | IP/CIDR and DNS/DDNS rules, country restrictions, reusable ACLs, rate limits, method rules, Basic Auth, Tinyauth, security headers |
| Delivery | Optional shared HOT/RAM and COLD/disk cache, cache invalidation, compression, response transformations, static-file optimizations |
| Integrations | Docker discovery via separate agents, TCP/UDP Streams, ACME certificate providers, Prometheus, Grafana/Nagios examples and administration API |
| Operations | Dashboard presets, traffic analytics, live/stored logs, configuration history, encrypted backups and certificate reloads |
| Reverse proxy | HTTP/HTTPS backends, HTTP/1.1, HTTP/2 and H2C upstreams, load balancing, health checks, connection reuse, WebSockets, custom locations, retries and error fallbacks |
| Frontends | HTTP, HTTPS, HTTP/2, HTTP/3 (QUIC), named listeners, exact and wildcard domains, redirects, optional post-quantum TLS key exchange |
| Web Server | Static sites and downloads, `try_files`, rewrites, `.htaccess` subset, byte ranges, precompressed files, PHP-FPM |
| Certificates | PEM import, ACME HTTP-01 and DNS-01 (Cloudflare, OVH, RFC 2136), automatic renewal, external Certbot files, hot reload |
| Access | IP/CIDR, DNS/DDNS and country rules, ACL profiles, rate limits, method and Content-Type policies, Basic Auth, Tinyauth, Authelia, authentik, OIDC |
| Delivery | Shared memory and disk cache, `Cache-Status`, purge/ban, gzip/Brotli/Zstandard compression, asset minification |
| Monitoring | Live dashboard, per-Vhost statistics, live and stored logs, Prometheus exporter, JSON metrics, Grafana and Nagios |
| Operations | Configuration history and restore, encrypted backups, Docker discovery, Nginx Proxy Manager import, REST API with OpenAPI |
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.12 — upstream cache diagnostics and RFC 9211
- **Cache and upstream headers:** retain origin-supplied `Cache-Status` and `X-Cache*` diagnostics on BYPASS, MISS and HOT/COLD HIT; no longer silently strip them when cache diagnostics are disabled or limited to HIT. Origin `Cache-Status` members remain in order; an enabled RustProxy `Cache-Status` member is appended with its own `hit`, `ttl`, `fwd` and `detail` parameters rather than overwriting earlier members. On collisions with the non-standard legacy `X-Cache*` headers, RustProxy keeps its existing names and preserves upstream values as `X-Upstream-Cache*`. These options affect only RustProxy's own diagnostic output. See [Cache](docs/CACHE.md) for examples and RFC 9211 semantics.
- **Appearance:** **Settings → Appearance → Charts → Show X/Y scales** adds optional, off-by-default numeric chart scales to live SVG plots. The existing lines, colors and dynamic Y limits (including adaptive Vhost/Docker rate scales) are unchanged; Y uses the exact plotted range and X shows sample numbers, not inferred minutes. The setting is account-scoped and reversible. See [Dashboard](docs/DASHBOARD.md).
## 0.34.11 — adaptive live request-rate charts
- **Vhosts and Docker Vhosts:** the lifetime request-rate sparkline now rescales with recent traffic. Isolated extreme peaks no longer flatten ordinary request activity; sustained traffic growth expands the scale and it shrinks again as traffic drops. Samples above the visible range are capped at the top of the plot, while the text legend retains the real recorded maximum. Hover the value to see the current scale range. Existing statistics collection, request counters and metrics APIs are unchanged. See [Vhosts](docs/VHOSTS.md) and [Docker discovery](docs/DOCKER_DISCOVERY.md).
## 0.34.10 — filesystem permissions and certificate names
- **Certificates → Existing files:** permission commands are generated from the entered external certificate/key paths and displayed **below both path fields**. The command block keeps the previous panel styling, with account selection and guidance under **Options and help**. Commands grant directory traversal and read access to the resolved files for the `rustproxy` service account. After the public certificate can be read, its DNS SAN names automatically fill the legacy SNI field; manual edits are preserved, and **Read names from certificate** retries after permissions are corrected. See [Certificates](docs/ACME_PROVIDERS.md).
- **Web Server:** the document-root field now has generated permission commands directly below it, updated as the path changes. The helper covers parent traversal and read/traversal of the served tree. Review and run the commands as an administrator on the server; the panel does not execute them. See [Web Server](docs/WEB_SERVER.md).
## 0.34.9 — experimental PQC (source candidate)
- **Settings → TLS policy:** opt-in experimental TLS 1.3 hybrid post-quantum key exchange (X25519MLKEM768), using aws-lc-rs only for enabled frontend TLS policies. Classic key-exchange groups remain enabled for client compatibility. Off by default; cipher-suite choices remain separate. HTTP/3 and per-vhost TLS overrides follow the selected policy. Details and rollback: [TLS PQC guide](docs/TLS_POST_QUANTUM.md) (also in the authenticated panel Reader).
## 0.34.8
- **Docker vhosts:** automatic hostname uses the container name, without image or port suffix; prior auto-generated names are updated while custom names and aliases are preserved. Docker Agent **1.0.1**.
## 0.34.7
- **Settings → Cache:** Varnish-inspired cache policy (on by default) ignores hard-reload `no-cache` / `max-age=0` / `Pragma: no-cache` for fresh HITs, without disabling `only-if-cached`, `no-store`, `max-stale` or `min-fresh`. Public origin `no-cache` responses can use an explicit fallback TTL; other responses retain revalidation, `stale-if-error`, Surrogate-Control vetoes and privacy protections. The behavior is explained by an **i** icon next to the setting. See [Cache](docs/CACHE.md) for examples and safety implications.
## 0.34.6
- **Docker vhosts:** **Live logs** beside **Open app** opens the existing HTTP access-log viewer, filtered to the selected generated Docker vhost and its aliases. These are RustProxy request logs, not container stdout/stderr. Open app honors HTTPS and nonstandard listener ports.
- **Publish switch:** a compact neutral on/off control in the existing panel theme, without a pill container or green fill. It saves immediately and rolls back on failure.
- **Ordinary Vhosts → Security:** optional request **Content-Type** denylist with exact MIME types and `type/*` wildcards. Matching requests receive **403 before PHP-FPM, Web Server, reverse proxy, custom locations or cache**. Docker vhost configuration remains unchanged.
## 0.34.5
- Administration UX refinements: clearer grouping for Web Server delivery and PHP-FPM controls, Stream backend and health-check settings, and Redirect/404 host identity versus frontend certificate options.
- Stream backend probe settings are hidden while health checks are off, and disabled Web Server fields now have a consistent muted appearance. Both UI languages are supported.
- UX follow-up: Vhost transform/cache overrides now place checkboxes directly above their textareas; inline Vhost IP rules live with ACL profiles under Security. Web Server preset hardening controls and refresh interval grids have clearer alignment, and System information keeps the statistics source without the separate process block.
- More controls (including the authenticated documentation/Swagger page) follow the saved corner style. Configuration keys, API payloads and proxy behavior are unchanged.
- Published Docker vhosts now expose collapsed, Vhost-style traffic statistics and a single toolbar button to expand or collapse the statistics for all published Docker services.
## 0.34.4
- Docker vhosts: SHA-256 certificate fingerprints wrap correctly in mobile agent details.
- Ordinary and Docker vhosts support up to eight ACL profiles with deterministic explicit deny priority and merged explicit allows, while keeping standalone ACL layers restrictive. Existing singular ACL configuration remains supported.
- Certificate-to-domain mismatch can be explicitly accepted per ordinary or Docker vhost (with a warning/confirmation); normal validation stays enforced without this override, and TLS clients may still reject the certificate.
- Editor layout: SSL certificates and TLS settings are separate; Vhost traffic/security, WebSockets, balancing and location access options are grouped by purpose. Docker certificate overrides appear under the backend IP choice, and common controls respect the dashboard corner style.
## 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 has an **optional, hidden-by-default cumulative cache-outcome bar** showing HIT, MISS, BYPASS and OTHER request counts and percentages **since service start**. Enable it in the dashboard layout editor when needed. This complements the existing cumulative HIT/MISS donut and separate recent cache trend chart. OTHER denotes HTTP requests without a cache decision. Percentages use all HTTP requests, unlike the HIT/MISS donut. The upper-right counter displays only the lifetime HTTP request count; its full meaning is available on hover or to assistive technology.
## 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.
- **Settings → Manual config** displays the active configuration storage mode, its path and the SQLite history location for file-backed mode.
- Documented optional systemd `--config /etc/rustproxy/config.json` deployment with file/directory ACLs, write access for atomic panel saves and reload instructions.
- Public Web Server errors reuse embedded application-style HTML templates (or a matching fallback for statuses without a dedicated page). Proxy-generated non-template errors use standard HTTP status text; neither leaks internal `RustProxy:` diagnostics. Origin error bodies and custom error pages remain configurable.
## 0.34.1
- An explicit `--config /absolute/path/config.json` (or `RUST_PROXY_CONFIG`) makes an existing JSON document the active configuration source. Panel saves and configuration-history restores write back to that file, with SQLite retaining revision/audit data. Out-of-band file changes require an explicit reload; concurrent edits are rejected.
- `rust-proxy reload` / `rust-proxy config reload` and the example systemd `ExecReload` perform configuration and certificate refresh without terminating the service; listener changes can still require a restart.
- Panel-managed, one-time-displayed administration API Bearer tokens with `ro`/`rw` permissions and mandatory source-IP/CIDR restrictions. Browser login remains available; certificate-only tokens are separate.
- Bulk assignment of Vhosts to a certificate; a single **Save and lock** control for unlocked Overview layout.
- Panel-managed Prometheus token issuance, rotation and revocation with one-time display. Existing environment-token configuration remains a compatibility fallback until a generated credential or revocation record replaces it.
The complete list is in [Feature list](docs/FEATURES.md); a visual tour is in [Screenshots](docs/SCREENSHOTS.md).
## Installation
Use the maintained packages where available:
| Platform | Method |
|---|---|
| Debian, Ubuntu, RHEL, Rocky, Alma, Fedora | DEB/RPM packages from [rustproxy.linuxiarz.pl](https://rustproxy.linuxiarz.pl/) |
| OPNsense | Plugin from [rustproxy-opn.linuxiarz.pl](https://rustproxy-opn.linuxiarz.pl/) |
| Debian 13+ from source | `install-debian13.sh` bootstrap script |
| Any Linux with systemd | `cargo build --release`, then `install.sh` |
| Docker / OCI | Included `Dockerfile` |
- Linux DEB/RPM: https://rustproxy.linuxiarz.pl/
- OPNsense: https://rustproxy-opn.linuxiarz.pl/
- Docker hosts (optional agent, Linux DEB/RPM): install `rustproxy-docker-agent` independently; see [Docker discovery](docs/DOCKER_DISCOVERY.md).
Quick start from source on Debian 13+:
For Debian 13+ source installation, manual compilation, managed updates or Docker, see [Installation and updates](docs/INSTALL.md).
```bash
wget -qO- https://git.linuxiarz.pl/gru/RustProxy/raw/branch/master/install-debian13.sh | bash
sudo -u rustproxy /opt/rustproxy/current/rust-proxy --data-dir /var/lib/rustproxy admin setup-token
ssh -L 9090:127.0.0.1:9090 operator@server # then open http://127.0.0.1:9090
```
Typical managed Linux defaults: HTTP on `:80`, HTTPS on `:443`, local administration at `http://127.0.0.1:9090`, private data in `/var/lib/rustproxy`. The initial public certificate is self-signed. **There is no default administrator password.** Initialize administration locally and assign trusted certificates before public use.
Default managed installation: HTTP on `:80`, HTTPS on `:443` with a self-signed certificate, administration on `http://127.0.0.1:9090`, data in `/var/lib/rustproxy`. There is **no default administrator password**; the first account is created with a one-time setup token. Details: [Installation and updates](docs/INSTALL.md).
Updated Linux service templates allow **read-only** access to static website
roots under `/home`, `/var/www` and `/srv` using Unix groups or ACLs without
per-directory systemd overrides. The service still runs unprivileged with
`ProtectSystem=strict`; external certificate paths have stricter trust/owner
checks. See [Linux filesystem access](docs/INSTALL.md#linux-debrpm-read-only-access-to-external-paths).
## First steps
## First configuration
1. Create the administrator and sign in.
2. Review **Settings → Listeners** and assign a trusted certificate (**Certificates**).
3. Add a **Vhost** with your domain and a backend URL, or a **Web Service** for static files.
4. Check health, access rules and routing on **Overview** and **Logs**.
5. Enable cache, compression, authentication or monitoring when needed.
1. Install and start the service, then initialize the administrator using the local CLI.
2. Open the panel and review **Settings → Listeners**, administration access and certificates.
3. Create a **Vhost** with a domain name and a reachable HTTP backend or **Web Service**.
4. Confirm frontend TLS, backend health, access permissions and routing.
5. Enable additional cache, compression, Docker or monitoring functions as needed; inspect **Overview**, **Logs** and **Cache** for results.
## Performance
## Guides
RustProxy streams requests and responses asynchronously on Tokio and Hyper. On one CPU core it serves roughly 15,000 proxied and 26,000 static requests per second with full statistics and logging enabled, and it is faster than a comparably configured Nginx for large proxied responses. Benchmark method, the full comparison and tuning advice are in [Performance](docs/PERFORMANCE.md).
- [Installation](docs/INSTALL.md) · [Settings](docs/CONFIGURATION.md) · [Vhosts](docs/VHOSTS.md) · [Web Server](docs/WEB_SERVER.md) · [PHP-FPM](docs/PHP_FPM.md)
- [Certificates](docs/ACME_PROVIDERS.md) · [Cache](docs/CACHE.md) · [Response delivery](docs/RESPONSE_DELIVERY.md) · [Docker discovery](docs/DOCKER_DISCOVERY.md) · [Monitoring](docs/MONITORING.md)
- [Security](docs/SECURITY.md) · [API](docs/API.md) · [Troubleshooting](docs/TROUBLESHOOTING.md)
- [Changelog](CHANGELOG.md) · [Roadmap](ROADMAP.md)
## Building
The authenticated **`/docs`** page includes the guides and a separate interactive **OpenAPI/Swagger** tab.
```bash
./dev.sh check # cargo check for all targets
./dev.sh test # complete test suite
./dev.sh release # optimized binary in target/release/rust-proxy
```
## Dashboard source maintenance
Rust stable (see `rust-version` in `Cargo.toml`), a C compiler and Python 3.10+ are required. The build embeds the panel, the documentation and Swagger UI, so the binary has no runtime web dependencies.
The panel stylesheet is maintained in eight ordered modules under
[`src/admin/web/css`](src/admin/web/css/README.md). The build embeds one compact,
fingerprinted stylesheet for login and the dashboard. See the module guide for
ownership, cascade rules, duplicate checks and browser regression commands.
Optional memory allocator: `cargo build --release --features mimalloc` handles heavy HOT cache turnover faster but uses about 25% more memory; see [Performance](docs/PERFORMANCE.md#memory-allocator).
## Status of this source archive
## License
Version **0.34.12** is provided as a **source-only release candidate**. It retains the experimental PQC implementation introduced in 0.34.9, whose dependencies were provisionally locked using published crate metadata. Native build and protocol acceptance remain required. Run `cargo metadata --locked`, `cargo check --locked --all-targets`, `cargo test --locked --all-targets` and protocol compatibility checks on your target system before production deployment. See [Installation](docs/INSTALL.md) and [Roadmap](ROADMAP.md).
Copyright © 2026 Mateusz Gruszczyński.
RustProxy is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. It is distributed in the hope that it will be useful, but **without any warranty**. If you run a modified version as a network service, you must offer its users the corresponding source code. See [LICENSE](LICENSE) for the full text.
-8
View File
@@ -1,8 +0,0 @@
# RustProxy roadmap
This file records **work still requiring acceptance or planned future consideration**, rather than repeating completed release history. Implemented functionality is described in the [documentation](docs/README.md), and operator-visible changes by release are in the [changelog](CHANGELOG.md).
## Chart axes acceptance follow-up
The opt-in X/Y sample-scale display is implemented in **0.34.12**, with current SVG ranges preserved and no elapsed-time inference. Before production acceptance, review live screenshots at small screen widths, all themes, dense/custom card layouts, charts with empty data and compacted rate histories; check the toggle with per-account stored preferences and the full Rust service. The original axis-free appearance remains the default.
+277 -248
View File
@@ -1,16 +1,16 @@
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
26115eda9fc03552f26f900865df1126ae252ddff39fe65a81e68650edc1372b AGENTS.md
9571c19750663ce2f764d15cb8c752fb092f009058264ef134776bf105173556 CHANGELOG.md
2b52395199055d2dbc09ecff16bf0ad238cbc15ac41d5022a6f94e65883a2e53 Cargo.lock
bdea624ad5b7e6f7cb749c0398af6846615bf641d59348bb092406383ca2a94c Cargo.toml
ba8bc979322a494cfce59398085259d059142d6a789bbe73ed7992d143123f06 .gitignore
5142c2151f434a42b925a6def77b726abf0c10e8404f8db08c999ca1cad43386 AGENTS.md
c2c23ce76282075cc49bf1609fe693591c900a5a120ff3e209e00f86a97c0141 CHANGELOG.md
77a69eb3906a27fa42d4bcb60e9595e86fc23ad74d72b96f2710527da49f5225 Cargo.lock
5d49aef294682f80d98cc2cc50d3908a8e5684151d38ee5dfbd04cd6cd95f9e0 Cargo.toml
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
1151dada0b9f3e85a2437f3eb7b7f3a2a40b56a14daca525fb593cf0be8a344f README.md
55d9e5a155bd2e164e81c949620518e2b86d5792165a3b571b964d2386c6cb8c ROADMAP.md
0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0 LICENSE
82d1dee2fc68158f0ea73259151461e64c8b18d807cd1fecfb1ed99dc30cf5f4 README.md
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
ac07b3bdd46c787989e93f55ebd6ea3d7f5b150ef36f2422e2b58df3f6b7543a build.rs
2c6a0a5cb088399e2cc912073069eb334ec6615eee31734d2f0c161f1415fb6a deploy/README.md
c0df6b46bf795d49d00fcd9e2a9247aed0d3a99b6cae1dc5ab6614e8b3d30009 build.rs
95ececaba550a4bcfd6754073877ba0633373b1e1451521b9ac905a51e8ca6eb deploy/README.md
13d514de5042a230cacf8c10abe1ef183c94ac799a89b8e2c111152ac7c69b48 deploy/rustproxy.service
c111f5b20484707b80efb7dd9176acbaa27b63ac78db497261ac234821e1fade dev.sh
d213e774a9cd07bff0c79324722bd05c0aca18cc033d50b2c0558cdb72972bca docker-agent/README.md
@@ -20,66 +20,79 @@ e9f18b358a740f1fe16e369ed16ee37581b8ce4c94ddeb34a1a37b89a49b5a06 docker-agent/c
5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example
ae660c2a90f193167b166977277abfc86a25e043ab4def4c273b2a13d5a51b8b docker-agent/docker.go
9e9f3f17f38f9b0730f598f65988ce8ef562d2182ef6778fc7292b990e9cf22f docker-agent/go.mod
3657c195ee6ff9c3c9f1e0d99a09b1ec2091edc61173098ef03db9b8dd7483fa docker-agent/install.sh
7b70bd43a7f39bae8968decf4cd76af80fe0d08bf1bc53b894d89ead80581d56 docker-agent/install.sh
b7d0466f70b541a705d234fc8a88b99b6197fdd5e682d3a288a78c76764358cf docker-agent/main.go
d660f46f7ef8c539dd3f5486a31d5a1254b9737be8a278c7f9352c254558f8ac docker-agent/main_test.go
351bcd0321c823694e12d578f96dc68ceb929979a70146d12af5394f639a486f docker-agent/model.go
e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/network.go
634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service
5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go
5664af2afde7f6c88d44e02aa8a9cd719a6019db15b570510185e4114c83799e docs/ACCESS.md
dd186c91244be2d1dde47626d5022c2b24ce7a6834e0f08b1b401cec71017b4f docs/ACME_PROVIDERS.md
989293ade34917b0ab3718da3b6c84e897b45cd6fe22345757f151e1cbd510fd docs/API.md
2362ab34020d4e0dcd70e280e9539ef7764b63212856ee08a3a55906c48f6421 docs/API_ACCESS.md
60d108fd168064e4d6980ed7499f3219017f9878c743697e77f2efc592e4d629 docs/ACCESS.md
a1b6b8675d3b8321160406bc716f1355356e1e3dd056d4785be1d06a99313f31 docs/API.md
728b15b9825f547f34c4121219784798dd046fbbdd72ece8906d0cd77e8efc64 docs/BACKUP.md
d71c36578d944f3fc64dc317937a8d0ca2360a6a1a15de18cb63223c8d6d0de7 docs/CACHE.md
f24767c2f9d4db4db5005b700d03323dd20f0a1d180ae939be4fe54a126aaf32 docs/CONFIGURATION.md
80bcc666736b10f6202386d0342e8d6feeba858d77aea310bf3ec6de01c2bfb3 docs/CONTAINER.md
dc670d912a6a946160c2f8528c9bb710ec38ebe406dcb7dcaf18fb2653f667b6 docs/CUSTOM_LOCATIONS.md
e350c5081085b79e579d1500fa07feee94a709adaf6dfde7b3d9af124a78d184 docs/DASHBOARD.md
4fb55a549535f37467f14536cbf0db7be027ca8a019c2919a384af4f23655300 docs/CACHE.md
32599d67a11fd0ebbc612e82b9f602789f4ed4103cda482c6870d8741100c8b4 docs/CERTIFICATES.md
02bb383b5c3bce22616c3cc3db30a9469806dc16decb509973e84c60aa30fc5b docs/CONFIGURATION.md
fec29ce846d6705eb45819d85d382f28e28a146b265feab2e4c7cce2a35e5883 docs/CUSTOM_LOCATIONS.md
6e05af7bf62a41a3a5a71e8f0c036dc611408d04f7130c07b585317aaf59ef0b docs/DASHBOARD.md
11a08bf2110bbeabc924eb049a30177a216af5607ccdaf8be9178e751f78a2c4 docs/DOCKER_DISCOVERY.md
c891ee5272e2e47e1a8f97133a413e2330844103d543fb5cbcc4d0724d59a1a5 docs/INSTALL.md
37001cbf8ed248ca05ec40f12dd9876d33f6e0c97dcb21eb2e6f53c3d97a60bb docs/INTEGRATIONS.md
cebf36532f7366acdcf8808d958b86b5f67246043677ee3dba9474a9b81970e0 docs/FEATURES.md
3939c14e83ef6856a421f358a3f7fce3d50ea375bc1b9f0419f27a3eed658f6f docs/INSTALL.md
694cb7b15c5bbdc94cecd15dc516c05dcb3467849322cdd125b872d36dee4699 docs/LOGS.md
888cdc8c56e10b8a7615012e192e085bf2899d0ec9b3ddfb5731e7de6ab5bcde docs/MIGRATION.md
27ba3ed337963d9cd05ba382666eed9c414f58c52e5897b4c1c45603f412b456 docs/MONITORING.md
0e9ae9509a78d00951d2c7c3f13eaf518d1f3448a29b048f5933e1f5f8440275 docs/MIGRATION.md
0972248b7062b37e61edcc34dab5fe73ee9bb50195a0aa475c72efa0dd311aa9 docs/MONITORING.md
2a363d1e5dca0168ddbe58498641381e8281089f2e7b15ea5159e5b9eb6dae01 docs/PERFORMANCE.md
d5894c07c68ec4230987767198434abee13b0e525842fea21bcbe752fa82742b docs/PHP_FPM.md
81b14192310c59ae020dada4910b1984303ce2cbc8b4dfde933abcde7bf5dd51 docs/README.md
9095d0df62a4548459ca739c97fcca37d8902cee1ae04b7f9da57aaea7749a9a docs/README.md
490a6dbe870f2b1fd4ec7816768dde6c86fa7957fc9de2d2f075e123c9222222 docs/RESPONSE_DELIVERY.md
e2b36152c13d65621a367c69dd400118dae1e4426a4d2b2139d2f63fa30442d6 docs/SECURITY.md
70e8c8f3352629f5600537058b1997739d9e465747cf3051da8a2cc779c3b728 docs/SCREENSHOTS.md
3a48184b36d225757fc5e2d22c629f89bfe5d4007ff087f0be0ce02a5fb04c28 docs/SECURITY.md
bbef60ff1771da9f48af812987766e1e479408873ae2f079b13f1d5c62ecc6ff docs/STREAMS.md
6508819f181e2a935ffa117451915aa5568a31e2890c57fa10e04543db82f3e7 docs/TLS_POST_QUANTUM.md
43a2c6230f95f81bf4efd1575a4f7cdc8b02244df38e19b1ec9c3cd736d3afe6 docs/TRANSPORT.md
8542c4a5c9d5dfe474459099546ae590b4c4d3f363f0bba9721c6107643c2d3a docs/TROUBLESHOOTING.md
e14c02f2c95daf5779d1aa5300751162b63fdd968832a1f0c2f191fa00ace6dc docs/VHOSTS.md
25fd98fd7257bb371472ac32cb8d508f5ee3d785a6feb01fc45cc80a3d3afc54 docs/WEB_SERVER.md
8cfcda59060106151f13e93ca25c1280b4390433b3fbe2921a6eb3c13bf9766d docs/TRANSPORT.md
be6e8af62a7bf958b94ec15e9e3263263e511d1d33abae4e45d114833fa52b07 docs/TROUBLESHOOTING.md
b4abb86942f665230904fad570c1487bc173aa84633801e2999f2bfa197de4a7 docs/VHOSTS.md
e67928a9d044e71b681c3e52f9dd8b151d6b72f965d16d5da8b0eadde98b70e9 docs/WEB_SERVER.md
3769f97c74b1a4a2f14d82a6f9c30fa3ee48892f423bf82dbd2ec1bd9280e45a docs/images/about.png
a85a7b9d8cac74fb1b231471d313e6587ccfc2e8abd2885a79067704176c724b docs/images/api.png
1ec8e47b6c10a905d8ce63a060be3f51efa3bf2c12529456a8c3d3a0653db95f docs/images/cache.png
02b4ec5bc7e39aeb8f8928a015779b431ee12c4c16733954631de0e6699388f6 docs/images/certificates.png
40d0a4d28c3938612c93425a308c895dea0659f50c661849ec5bde606a141a97 docs/images/documentation.png
6e7f634d6c0cfe8f9fbeba129d745fff00cb8bc6f388de10c4c158ae4a854a17 docs/images/login.png
1f921d98aa16da3a43bdcaa46ade68eb921b1d88d95a8fa87f0b51b6f2a8db9c docs/images/logs.png
74c23173c2299f8ee150fd8fe160ffec0cbfb5f63b89d8debb1789fa4526fb19 docs/images/overview.png
585f7f450bb7dbbf32ce146edbee6555f849903d1abbcf665a75b5fb12ef36b8 docs/images/settings.png
fcfc9f1b932e243e2133061aa996d8ca6f8fede830190865b771482f4ab01ca5 docs/images/traffic.png
d50eff63d8c11af616a794d24967ca16a2ad827d9c3ec47299d578419b3c449f docs/images/vhost-editor.png
bb977580c5d93bd7f2eb931c795ae7f4b8676f316d0fbd1552f78e2757dd79b1 docs/images/vhosts.png
28549d423d6976181d9b4ff9e13626ccee3b4c44db36d442eb6b3e20e9f90d6a docs/images/web-server.png
5502e8ce44276213803b66c58e3a4d804a1354f5ed5d83f47ee805e8c44ab088 examples/acl-headers.json
2431dd88a2d9ad2cf3cd4b8fcdfa602563ab1013c612d8917fddbe7febfb2dc3 examples/cache-dashboard.json
e31ac05a99615e4ea6c4c9a8b2e090968583fe3959f838a600b5740cb170f8c5 examples/config.json
39cad993003930febf98460311d185a278e7a9508b6b591299d9339d0f0c51cf examples/empty.json
695cc7621aa403148fcd0868f351b6e9ccc076fe35610e779af4a72f94d7d5f4 examples/http3-vhost.json
b26833cb4251f9390a44192ecc827ff7f052fd19016a5ccf7498c94503ab4ce3 examples/mock-vhost.json
a6b835dc07defc12b8f0e48491d5f413843c8be2ba4501d7c28140a2822b034f examples/oidc-vhost.json
134d4a59727b27accb3141d4cee26a4a74157d236de4996bfd682e639462b2b5 examples/tinyauth-vhost.json
5a8f3a8a054abc89c6e78896b9fe2f903212199a24dc3d2d2bcbd0696a31bcde examples/transport.json
d242de9614fbb52933b4accd32f4715ca94107abe4a3b1d7e088362b07a6ba60 examples/vhosts-balanced.json
af2bdb1343a8141069e94ccd2c7f92574ad848437e2f9fdb5c7576a9a18ef4a5 examples/vhosts-private.json
1b8974af79a5026e0648ceab58a32f233e1760e21e7465a0d46e9c41fb09964f grafana/README.md
ea8aaf0257a5ab75153ee2fd383a4d5c2ac725c47042f9770949451636d442f1 grafana/rustproxy-overview.json
a05cbbf101b288bf9cf83fedfc5017925a8b360a78f313d31c1a70f15dc1dd22 install-debian13.sh
35e4f3acc652ac23a9442600c4c76173ae34966592031b3e942b62c249fc869e grafana/README.md
0d389faa4f64f095e9ec62cbae329658682b96f5e4d02ead79c7d2892ab89fa8 grafana/rustproxy-overview.json
1a041c505669625a7f0f8011484cf5b6d2ec69a7de859801312e4fcf4c8d3555 install-debian13.sh
909e9ab94617ace74a425f15084bf85f3086cc52ba5b28f2b441ff800abc6ef9 install.sh
42f9f1d18c021fc7734b5bd020844bf678725a0e539e2fcbe05ec676f6239eae integrations/README.md
2d4c859aae5ba357a6d30024c8487ae8f59f01b192711f13f9c2f8fee0f48b50 integrations/opnsense-acme/README.md
c73b814a42bb4ff703441dbd177ee625055eeb115f9b5bb0c2150aede573a159 integrations/opnsense-acme/opnsense-rustproxy-cert-deploy.sh
de6550ab8f48d556d752c252212c38ba65491fd224f972c10fbe552f91ed7520 integrations/opnsense-acme/opnsense-rustproxy-cert-deploy.sh
45be151cd85fc2ef2f1fdc3003ace63381af262e73af8a685ab48cb697cc0de1 integrations/routeros-oci-build/.env.routeros.example
11aa102de1089ecc375f20c4bd5420fcfd3b2ef23fe0c8bdf04953fcfaf40ef0 integrations/routeros-oci-build/Dockerfile.routeros
7c7a3e51f43b43e1c962a4b0bb591f99c1f06977bf77f23719f7f867dfe7eb8e integrations/routeros-oci-build/Dockerfile.routeros.dockerignore
cb01fdef286f38b442dd9252c23c4f8e44eb72e9ecdd99adb926e8e9f6318bb1 integrations/routeros-oci-build/README.md
96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh
4c7416b85ddbf66f8434735dfa6452c8b368f496a120861a2024c5775858ad0e integrations/routeros-oci-build/README.md
3f092a9ab80221eddd67be4d1a776acbc6e04c1eb6011b588280c28e83b2582f integrations/routeros-oci-build/build-routeros-oci.sh
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh
13189fa546c3488dddd8efc80c47f6da5241e5fc28b79cbfe795414ea05c7a4f langs/README.md
4de6c67c2ba4f7b6806d5747ec6402b058eb1f2050eeb031dcb61abb21e8288b langs/en_US.json
43dfc96953ef84a0d9fb04df728e3dea6ff70e6f8c8e7b4f0bf6de61f6fbc4a7 langs/pl_pl.json
a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py
582dcbe08575602b83610e316ee3d856d391f87d23f6d91dca7361ed58c470f4 langs/en_US.json
2bdd09e7734a65c828c268e89a85a1e6071be54cdf1b378a67406a1fa6dbf0b7 langs/pl_pl.json
9664543e4d03422788a65e42f968c1f026c2235cb5be6fc3e0ad318997077538 make_zip.py
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
5c62da30fa9db594a4d656c77ae676d3c388072b6a85f91170ec7d911d7cde43 migrations/0004_admin_preferences.sql
@@ -99,115 +112,116 @@ ee50f38c037765fab742e888d4b9fc4d874c31c785c6064d4eca3d2c4d24d944 nagios/check_r
7458b2d74495e6fb1c1edeea4d5a793705476431dadb60627187111f91441e13 rust-toolchain.toml
fd1c34599f725ad5f7c3c0cc89e08b11917baef02c83af054ac8528b2e01dd8e scripts/README.md
1d63e9cea61a744f1bf2085cbb7c3d8755b3eda1569c8532574dd4fb6a840678 scripts/__init__.py
1843422d648101bb675d8bfcdb109ef350bfda273999ad7395d5f6ceb923eaa1 scripts/audit_css_usage.py
292c65ad2a7f0ca13da4a00a836df17c3192728dc02034b75a6a70eb6b5eb25c scripts/audit_css_usage.py
5fc5c7f274aa5a6c07970f7b2e548acabae3dc2a24d598308fd768a8d9de2510 scripts/build/__init__.py
c3066f40f152a2655f88314fd9cf784eddbfa6c3238a494da13d972ef9ad930c scripts/build/panel_css.py
216abccbb8b0b77b28de96382bb5790d1ce831699746d3fb06ef13a6dd1f5d21 scripts/build/swagger_assets.py
5e98d6c30047512825f5dc1c42709b82f0f6342fc0db8346a67ae85ffeb40c90 scripts/build/swagger_assets.py
e4d15aa6aa864d04b9b30aca33826b833632af02ca643f8b23e219a5ea1298dd scripts/deploy/deploy.py
618603239b000bbe4af36a6a6ee268758af632c761fc53fc6b7f44a4a818bdf1 scripts/deploy/deploy_core.py
8c7da13e95bcddd2bd1b4f1b22ade1c5ac1ecf54448601daa8416905d4239751 scripts/deploy/deploy_runtime.py
a641bd35400c56f3b68b0a92e97b602d06090f03972ca8f01fa744e90dd97403 scripts/deploy/upgrade.py
992c9a806464fa10902de5be102f1609469986331fc0ae223a5b5f8507153574 scripts/deploy/deploy_runtime.py
a59587ce8ad41b1bbb431356b58398f9c8ff7cb96a613a91752e907a70e10c2e scripts/deploy/upgrade.py
2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migration/migrate_npm.py
a459d95bbb0076f33ade8b273b813308167f9d8d45242b309a2d18320ca0093c scripts/migration/migrate_npm_api.py
31ccad3ec3c880119f3b7002f0c6652f8b43634edad8ab3763cb0e4497d0bb1f scripts/migration/migrate_npm_core.py
f040ab366fb138a48debf764c71538c2471235353e2426316264c5d3c2d59762 scripts/preflight.py
4c905a387f210c30fbf12a7738676fd39819bc96bc4a272da1db5f18a2d3013e scripts/migration/migrate_npm_core.py
2ae780e30c1212ffe134f1785b2767a4a6591d5f74baef6c2f47cc8df35afbdb scripts/preflight.py
3d6a0503afc25beab79ddc86864bcbb15248ded7368d33aa64321b8cdff59933 scripts/resolve_dependencies_0312.sh
20124173452b4514bad8850c1637569a9a191882478451e549bd16b432605fc1 scripts/runtime/acme_provider.py
d759315babd9971b354d6f702d4c861eb1cd5c41b1e2e0f1a94ad4d2c2db49a3 scripts/runtime/acme_provider.py
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/runtime/acme_rfc2136.py
174c8059625e3315a3d9fefe74c17f78ed850ff25ee15bfdabc148107dd344eb scripts/start_stream_backends.sh
91c883cfdce94c8c22c98749f611317f538a112d29be4f3fb1fa460b8f29ceaa scripts/start_stream_backends.sh
046b4282550f5cb4e53906665c91c7442be70fc68fd0743466fd832582f7c979 scripts/test_stream_tcp.py
f0b48ec0cc4411755cc0b4128eebce710375afa99344cb76198cf31074f8eb63 scripts/test_stream_udp.py
22e880e98c7b9d700c7a4c02b9b1d543e5dbd1f4ecdca42813a1083165b32a19 scripts/version.py
5130529d8f55ba9d89c29235a7eac133a80c9e9b2591267b1d082207568abbeb src/acl.rs
adab6c8cd7c3f826e300effbd7d10c1bf12ee49253b3e5ad445e4ac04900f5dc src/acl/dynamic/mod.rs
78efae6c43732c0f5527d467bf07d1f890da361925416c89b58bc449d852fc2b src/acl.rs
1d55ecb2e104be1d3b19623a1380584724c106ed234d2e79fd2a2fef1d891ee3 src/acl/dynamic/mod.rs
1eee2ea2c8dd6068dd0bbb058a06d26436f14767d8efbc2010bbdb4e3e523672 src/acl/dynamic/tests.rs
75120cd3115aea6fc40665086617eab9816f08f556cef8db370806195c7641bf src/acl/tests.rs
50d9a0009cdcee6dd34a6215755d98f38ea18fa39b9faf070d1a7649eeac6aae src/acl/tests.rs
6189029ba493982acc38c9dc1f47fa1f0c933dec5ecfb95b60f8be83d33b5aa3 src/acme.rs
b9eff08fddf9b8787aa04e72288ea2ca0033965e4563d060ca51f267e4963918 src/acme/jobs.rs
f7f737aa28d2d209f4f20a221885e3bb3596dfc8e3c8c9a22ba53bb975c52df8 src/acme/model.rs
40cd44f2b0d43ba45471ef5a012b3737ca3e16e33f6404b4f13b9cd9bd57f0ac src/acme/providers/mod.rs
b77026ad33be84706112881dde840e0fe4694b150dd3087d8e106737d07dd464 src/acme/providers/mod.rs
d1dcb7bdfb7cfdc9c3b88cfb6908201c58532bc888744a13f45853079f214659 src/acme/providers/tests.rs
c51fd51177a476be6462ca980e9bc13f0d0b41538f520118b80a685130f0dbf0 src/acme/runtime_bootstrap.rs
5a684eff5f3539c7bb8f66c3b83e7fb62527c52c61da727079d3897e0943f952 src/acme/runtime_env.rs
2f02b08d88ad3c1fc9827e5bd3ef463effba633834d9c05e9c0d6d2a0e8869c5 src/acme/runtime_versions.rs
cfc13a293c126bb511745859669719a14c72dd7ff6be197bc9e0d0e560deebbd src/acme/tests.rs
b28a08ea996befe10949676f40fab5f1036a274c7d0ab1175cf5c43c59849dc3 src/admin/access.rs
bcf131abdae425a85b5a54ade51346f623de1e10fa08e18f7c10f1fd2dd0aec1 src/admin/api_token_access.rs
3afefdc6640d35bd55f20981e842647573f1ba80248680f99ba79eec9a7c4ce5 src/admin/api_token_access.rs
5f5b150bb8f51f57462e519650093354ae7b40fcb80c4380fd3391a33f4b30f2 src/admin/api_tokens.rs
34e851f725e8605e3c5d9946e56ac035996f3bd9d608169cba3408060d229c9b src/admin/certificate_api.rs
4a8b6406a44508b8ba102fef0d8309e871dbe0cbdbee1e6c8721baaca0c1aee1 src/admin/certificate_inspection_tests.rs
fe1934fe1ed611277a55485f187a4f62e1fbc3cd196676e64eb1deee4a4c0323 src/admin/certificates.rs
aefbef9839f9914ba05ea62bceff1463da3042b52ee851181ee39d4fbf7b91c3 src/admin/certificates.rs
521ac794501570cfa2e8e50c3a5cec2ca08ee4dbce8ac9c0e0310c09612f1062 src/admin/config_reload.rs
159c3970d25fa29acf2483fc9422b003e5d63884713f62baa8375e4a4020730e src/admin/dashboard_presets.rs
b7b0d3f0df7683b031136c05b314e206f7864e70514fe25165e41fd7a9382854 src/admin/config_sections.rs
b1119d6e713fd3af967f0058b2af9ae5c3ccb9d0be66454872e1f6cbd1a7d552 src/admin/dashboard_presets.rs
aae1ca0e70d987830bd63948d431f54302083148177c1b551565be12df7fe771 src/admin/documentation.rs
da28dd57087df138913b784b71dca11d36c68100f2f100f452fbf4e4d7982433 src/admin/documentation/documents.rs
6ca14ae276c0e683903cfe326f6733cf33ea05f3abbefd5a22671ea160a58ea6 src/admin/documentation/tests.rs
928ba7aa54ea742877cf267ee7ede798f95fb75839f21492063ba53e5e989882 src/admin/documentation/documents.rs
b9c2952e3fcfca540d0ad64f2d3acc5c1f5240e707f4f23ef4d452f46fcb520f src/admin/documentation/tests.rs
1ce779b84aa8f47bdb3c34f83593c1f2f06d2235f0319847096cfcd035434168 src/admin/file_log_search.rs
ab18acf6fb4ac86fb22635b9219a12dac76d24952c8f15538486267a9b4b7855 src/admin/file_logs.rs
059218501907355b22bc84e7e0c41ec72565af283f5e97170ce7707ea5874009 src/admin/file_logs.rs
d58a2ed83319176de4e66790ed292447c9497ae031464ec55cfe27174acb639a src/admin/file_logs_io.rs
282352eda7957477e7da44f16b90984b26a4a67a8755833bc0a6b34eaf93e3af src/admin/file_logs_query.rs
f66ff718f82a326f80808b0a5088bd05a48625be802e5ba305fff3c344b59948 src/admin/logging_api.rs
1ba0520c6348246cbb35b60e596e99dc600137c0696de86508cf06587bd86563 src/admin/mod.rs
5d00cd59bb9ce85fe0d2762757645592eec5a1bef803e0692edda687dd7966a8 src/admin/openapi.json
7cfd6593ac9bcea3d1b9e61bbcb7ed98d65ed3190f54f94c1e104541f4fbb4f5 src/admin/operations.rs
77b8c05645969834cc8a58e249a7b60a57d6c47ad1c5cd92ffa5e4a9b5a8e73c src/admin/file_logs_query.rs
444d7715406ffd2305a72decf7fdf633aa4c2752cd881c8fa3b95ed971d79820 src/admin/logging_api.rs
243f4a9c3fa99781e19c3952354d2632c0ce65e1dafac729567cbe3c177f94cb src/admin/mod.rs
61fd5eb6bb4f3cc19151ae96dc5c028e4845479ce1e2bb2691a41094dcd212c9 src/admin/openapi.json
1f4ec45d146e00ef664fae37517f8fcb5455d47daec496c02b2fc15ddec49131 src/admin/operations.rs
257d93a29d0bda96462bcf695aba8d0ec0f9412c379e066ec46ae8ee034e3a53 src/admin/panel_assets.rs
f5127855c2994d052678865df36d703b74f4a0afc633daf7718b10da18d26e37 src/admin/panel_content.rs
b3c3c2d33c150eb42ce210c6074e5eff6a8af9391118012e413bc9323d3ae50c src/admin/preferences.rs
35a7f99c7448c90b8637f4b3a7becca138f1f378fe5dc3f87122151525bb20f9 src/admin/preferences_columns.rs
334095b28a7f9e523dfa820a3499d93e209a5bad23b776f6d21b80bc82aa2942 src/admin/preferences_tests.rs
b31595d9522962ae16dff83dd8bc063d31fb027cbf357e9953285d2e622daa26 src/admin/preferences_tests.rs
62ecbae5dee00aeeca3eb806e2605372471dfab971cde8f66f11e711df8fcea1 src/admin/public/mod.rs
7a00e4ab943f74d2b1bdcbfab7f9cd3f584e9f59536b4aca6c4c31f5f5508d12 src/admin/public/tests.rs
6930b4f4b45257772b321b20a9becdf9bc4610274852948ba38c2d0cdf02dfaf src/admin/refresh_preferences.rs
6d104cd21ff86c55e80b9a5039be6b61f0d82c7ba9e35748515d73e9249441b3 src/admin/router.rs
c8454f3092145c5fefdc8d93951ae46f192a6eb8139dbe288284b3a1c803b192 src/admin/router.rs
e135431cb078ddb483165581123c64af8f00ea67acb3a395ec3e53c85d0d0d45 src/admin/session.rs
09cc50a86960897371730cc1abed83f4f640c86b680ec7ff6d6b84c7e96a3c0b src/admin/status.rs
7aeb784a3ef58795c8fe26467bb4e1a00e61420911f798a584ab09c12f73a754 src/admin/status.rs
2d5b4c28fd0bd48b06890fa8ec44a2296de948c1f88eb2909f4b495710d4e13e src/admin/status_live.rs
8315c856cd50f1072593ecb30c274d42d82f078b5fb93e7c68a8857cd81b0841 src/admin/status_system.rs
cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs
6548710af7070f2e73e09c2cce1054d85edbccd4e3d042e5cc3951303ce31d9f src/admin/tests.rs
3bcd09729f3718d9ea88afae10c9e37b127d742babedd46e20ffd05eb31885e3 src/admin/tests.rs
c18c369124368f58524652565102817ed69fd25b222b0c5e737ef7632b45dcfb src/admin/web/acl-picker.js
48fcd818c522fe80f2c7b2e218f87de6a7772ffd0a24e93502780b03ec78e335 src/admin/web/acme.js
86abf50f39862d821cd2e43ca425671aa85289272915132c5a9a5dd1fc4182e2 src/admin/web/api-tokens.js
5a9842a3f3ac5f6c769413496ea5ea1d019446d051cdde252784b1a1cc46996f src/admin/web/app/backend-dashboard.js
aed3aea4eef0011c6b51d39736782e7d1ec52662e422f40e0c7b9ddfc19d0378 src/admin/web/app/certificate-inspection.js
1a4d40a86067493e75b47f2d735d50fa4d161344ce145d4e502b0225144ed45a src/admin/web/app/chart-scales.js
d6c5448db12de34a7bb4924411b44d09c7144bc3e659bd3c8a7c0ebf1938da1c src/admin/web/app/config-history.js
fece68287c2c58b813b9313fdad1ef6617c55230b5c5322d8ba7b9fda32455b2 src/admin/web/app/core.js
e5b39b5592ba81292571033f26934ac0cd42bcdca7395e91094973cecb3eeaf8 src/admin/web/app/config-history.js
0248fee14bbab0b3d1f308f650061d7a348a8ffe00a46a56e297131f815aae10 src/admin/web/app/core.js
622d34e1616fee9a480d208192fc5a84d62d84ef4233d6a7d95e2bfb8cef415b src/admin/web/app/dashboard-inline-layout.js
851a4337127f332db91f6954f689363ee227fcddde2dda82e55f645940d0ce3b src/admin/web/app/dashboard-layout-editor.js
e4238322a985d3e8b7f2de94b7af31e1c718775590b2c7b3428b878cd43337b7 src/admin/web/app/dashboard-layout-editor.js
a184c50e1ca6bb5a14954e31b15f09ff9978fb939064ca724851bbb4e661dc56 src/admin/web/app/dashboard-presets.js
25d89adb3b6e995d7c51d9b315e167949d5d8d4403162bf1fbe8e98dfb392dee src/admin/web/app/overview-extra-metrics.js
47eaf9486099468b3d686bdca5c6b8d59aa5f5661baa03b3698320cf618db4e2 src/admin/web/app/overview-extra-metrics.js
5e62cccf6a9507fe208e9f41bbd08453c3d8c6a6b83c56d93a2bb22550ea6e9a src/admin/web/app/overview-masonry.js
b52456b41451f156edf5f58492119c22726f2cf40cee938527748997e8ee7412 src/admin/web/app/overview-samples.js
561f750cacd4d4fcf7d1d1d9167ba296c0b43d7e81f2bff0ab1a070e1b8682d6 src/admin/web/app/overview-samples.js
db9012546cae8c58fc2bcbee71c9a95956a13a6db9e6d126c6a3770d45a4e4d2 src/admin/web/app/overview-wide-metrics.js
717820edf68b51af47b94ce18367635da99b36ce7331f44a0ec9f06e6f59ca4b src/admin/web/app/path-permissions.js
bfd9fafb3112197e8f875dbf87a9a66c81aef0fe83687601a84a66f5e13f69bc src/admin/web/app/path-permissions.js
46c7cd36397375eb7e8c230c8aa97b1dbea0a63800beb977efe1ec67a14b237e src/admin/web/app/preferences.js
8e75b0915edfbadefc4388701431541f4d35fd5d717363297b63740b3afd34d2 src/admin/web/app/rate-sampling.js
43f4dddd1c3ea7fa42eeb9d7b655a8db48b5ddcc6c68213e08dfc2fcfbe8b17d src/admin/web/app/refresh.js
9e2b12d34ab1fb29716645720577ad49b16c03b46c482a03695e8f645d170e79 src/admin/web/app/rate-sampling.js
ac24300022bb98de2ad5c03a7859a2d80b14420e7a72db49ad649f52d92566a9 src/admin/web/app/refresh.js
7888eaea26c5cecc097ad276b73ae6c63def1db896053def6852ddf9d6739906 src/admin/web/app/runtime.js
31201586925e6f4792071d7d03fb05dcc4a69a03c02a4e85eb0746f45af68074 src/admin/web/app/session-resume.js
fa00bce2651308a4e0b1303cc0146be17ed4794b67b2f0cebfaf861558d8c78c src/admin/web/app/settings.js
c7a18238ac426de4d1cf84a395ee4dc66410d60a57a3822695dc10adc668d956 src/admin/web/app/settings.js
b7ead49224a4ec67181d3f24f9d9634bd6d76756f826a4d5d993300637d1d8f3 src/admin/web/app/stream-dashboard.js
7de070cc8cdde45d7e0932259299e341e25b32c9ffbd87e4fcce934d0f090de4 src/admin/web/app/web-overview.js
596205feca534302699120b84848cb0f3e9ef2ad7ba24eac18d5751532a40194 src/admin/web/cache-panel.js
e9788a9d1e0bce8775f612a5c842afea8a0dcf01301acdef4b69ff6a52d2ef45 src/admin/web/certificate-api.js
9d000e98497b3387641500d5273d23eda45d532c714f22e187572928da152a97 src/admin/web/css/00-tokens.css
b86b530c86ff7996f41644dcff7c26145f1fa8bf0d9476dc34cdb73a67188a9a src/admin/web/css/10-base.css
4aae1657e8f5faa755cab0399769e6179364148f80243e742fc7a5e6e237b3e6 src/admin/web/css/20-shell.css
707d08b7b8505c7346d5a98f3a8058f10577ddac7b9e6a5a9894285bde9e300a src/admin/web/css/30-overview.css
ce5ffb911d7e4cd2317ca047534921132b6f93efd70a26673f0c428e55320a4d src/admin/web/css/40-editors.css
5af1a00c7d348979fa62d64e14d4e9a4c65c097e5ac96f956c3d87cb1ad71e17 src/admin/web/css/50-settings.css
d019d9741951c9d652fab9e520693e0dd3f00b395e32169e5c7ca3ccaf04132c src/admin/web/css/60-monitoring.css
6afa10207938787013f5a4731910630b19bec4a22682a07c3e67707d17cfd97e src/admin/web/css/70-responsive.css
f2a2b0476cc55d8dadb4fd52c2c589d64785ad400ccc6cac5a2d10cfca729928 src/admin/web/css/README.md
a09f060b47771c56e9b9e57f1b896a32a2f87f1fb33401ac76932a91d247c67e src/admin/web/css/00-tokens.css
77b8b748f31ef383c64ba5ca1740e9b6073f0de1e77c2c8c62d0f38bb52dd296 src/admin/web/css/10-base.css
d3f19407ae97b3f6cca015877d369f4e3dc86d8ea7de08e430c85468fa033f5e src/admin/web/css/20-shell.css
98133ee0b23288dc8d0ed2fa0a1375f9145acce108d215a63c76ea96bbdf8e11 src/admin/web/css/30-overview.css
dd82b5e3ead91c20bb08cf8d71bbba31ea8e3d108c417913c63409f3e34da7e1 src/admin/web/css/40-editors.css
6af9fcb439823ec63abe2c1dd86c9ecba17c233262fd8ab80d321d64ae2758ab src/admin/web/css/50-settings.css
f8b515c63080c71ee647cfd8c36628271e8357b31cd84f00cf591e9b752b44fe src/admin/web/css/60-monitoring.css
c7c387e21e7bb148689e16f5fda6cd47c870043260dec059bbe66e09ac86fdd9 src/admin/web/css/70-responsive.css
8009afdb04d5170687816730cc930a22fead1fe0248835ca7e8de81d6907395d src/admin/web/css/README.md
8241bd9489dfed082863948829a44a49bf0a8e9cd63c55809655109e393e0918 src/admin/web/css/manifest.json
560a8e047182fbcee08aa4f8d999400a8a5ddca0b6a4640efd46f6b5c34fde91 src/admin/web/dashboard-defaults.json
14213011cfb6ac334c8711b76bbdde97588a7b7ba4eb2592a1e387f877d6cbe0 src/admin/web/dashboard-widgets.json
ee51ab0912e0ade6a3e1c2ea6f533bb2f65a6d3e6ec9087e25b9f0df009c19b2 src/admin/web/dashboard-defaults.json
20d65b22900d0de2f71dbafee2fe9a237e0a76515b386022b94c75d50bdfd926 src/admin/web/dashboard-widgets.json
50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html
448a4e00e78ca368f9307cc84f87e893e5225dc87fdeadfbb227fd4b1faf1702 src/admin/web/docker.js
1d5f8fbf81394a6a8c3c2eff78b48408ef7790c0613905c2c7f584f1548862c6 src/admin/web/docs.css
fec062a5959450b1711948adf3189ceb37edca6fbead2e5bfc03b7ce397c70b1 src/admin/web/docs.css
aeffd403fa1823df7d9fa4c48280b4033b2d15e4cb9f66608caff89290de0a52 src/admin/web/docs.html
24b6d5676c2e0cd3862851a395c753dfd7e6fb022f7c69267c1e02b728f1d06f src/admin/web/docs.js
fd8506a91f769b57b62d9d8e389fb67764599354bf702958a913be79f779707b src/admin/web/docs.js
c52c430b9a6e584012e6014709e1e16ae7e775b2a5aedff8eb767fa867c1cbdd src/admin/web/drafts.js
007c24189ad7b497d4da024fc52e6c51393f91ee135f525a356e1f14882b7632 src/admin/web/error_400.html
232b766f278ae4424705af351910677fcd7ac9a20bb8320d9a12b763c91219a2 src/admin/web/error_401.html
@@ -221,30 +235,31 @@ f2cb26eec86b6c03151072f847153df8aea612d840ca8b76367f41b9605bce64 src/admin/web/
ff72fc4c6a11907a16234c4b3c7d7f7bcf82eb72868dc3fbf654b04aff806fc8 src/admin/web/error_502.html
386b9dc3aecebe7727f7cf7bbd2a02ab036a3abf01315975111a51559128df73 src/admin/web/error_503.html
e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/error_504.html
003d382cc8dd69ad7237679632c04a9389366bb71dd7dca8ee64ee06614aa9a5 src/admin/web/error_auth_provider.html
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.js
b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js
38e651a95a1c347f6086fff6553d2e27ac4f1d096561cbef716fef6108c881f3 src/admin/web/index.html
3f0525ca1d4e088ccd4988b8a117b26a8cf8c12213b24c37081d41b68d2b0ff2 src/admin/web/host404.js
64c9c8f4ce6821dbf84cd1aa0912d1b5f47421f592bebf2fdc2bff341e293dde src/admin/web/index.html
7915a06cebb3dfa2c51833c326586920691ba4431b1f984429b039333f9c2a93 src/admin/web/integrations.js
7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json
cbd41678eadd994dc5252628ee8ef9551fa9f7d8694ca5abd206240d9a96ad30 src/admin/web/login.html
c2e5f554810b8e34fb47361fb90a01073a2e332ee26d82e030956fad3ec1f83e src/admin/web/login.js
734aeac4855f0913a84a82d7b5c17b37de4504b50a8701edeb775d6926929bce src/admin/web/management.js
4481a592fcce9a2aa1db5e4a25619e19fd459c39039b86c0549dfc427eec1b45 src/admin/web/partials/overview-charts.html
a8afc6e70950bc70b98725eb049d73cff35dc3a3ee659229a15a8e08d3eb4f12 src/admin/web/partials/overview-metrics.html
69296af507dbc8c211800f4841c3d7ad6a2490aad2e93ecb297246b7470e3be3 src/admin/web/partials/overview-metrics.html
3928afce2ed6d5f2d3e29475dd29e4c3bd3c6df180ad5119beaff0c7a58e3b3b src/admin/web/prometheus-token.js
90d9a0836662135b79fb6f183c4f3eff60f8a64392d6c5795ccf1d8ef6862263 src/admin/web/redirects.js
3ee4508cdda0b0b0e0f880ecac4b085368098b46240d24f2e6efed21c96bd5e9 src/admin/web/settings.js
2e891f34cad2a2950a47e529e15ed9dd41beedec71280b8035b19cfb91a5d317 src/admin/web/settings.js
8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 src/admin/web/status_200.html
1ea78a6e5b5d813febf2359ecec7fde42dc1278531053e91428f4d72b8a1d62b src/admin/web/streams.js
d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js
c7e0ca45c19498d40ef6f4122de23f0e73094e905d39f798942f70d15b9cf86e src/admin/web/transform-panel.js
a55f45dad5b540def419f8487161fb506b7e52744c76a5e661c7a5fa0a98a426 src/admin/web/vhosts/core.js
d88d89c8071d2458aef278498a5a7863fb45c374fca801b0b3a9de402c455b4c src/admin/web/vhosts/editor.js
8338ce13b5e3e3f421437631124cb691aeb5b9e17409d256b4b9bcac457b3c59 src/admin/web/vhosts/health.js
87917b0388090f2b27ec6460be56fea96dc09b42735319e0a91330748848e288 src/admin/web/vhosts/core.js
9bb38cdb8c0f49144d6058e007e375fb3b2e47359988029fe28addc6a900d0ca src/admin/web/vhosts/editor.js
3776f72d7051af86ac5fbdd4581040dd88a424c0c6cde789301b032b159094d8 src/admin/web/vhosts/health.js
b3239e67901f1796d7a974ade0796f2e3590ff8064628a5629b78c7f6ce2f32e src/admin/web/vhosts/protocol.js
8c3da49973c62333559b0e3588c02beb0b425c025eac5cfa82a1bb968f70d9dd src/admin/web/vhosts/rates.js
03f99423df8b32aed674478afa24f6b366fb3128d9bab56218dcef71456bedc4 src/admin/web/vhosts/render.js
903df941cfd04bd2e34178b5b72911f5c0a6be9aa593ce544c685aacbd757da2 src/admin/web/vhosts/render.js
4dfdeb7c73c2775eacaca65fd8ef1dc80b275421b15522d319d338f5b96288cc src/admin/web/web-server-profiles.js
dafce3a59a6007e59d7a4018fce96a14cd8e8cedb3ce77f02ee229a280c122d8 src/admin/web/web-server.js
2d40c2cb32d7365df0c9cd6c8cb6f0483c7a799786c648f4836a0d9740c6c5ef src/auth/files/mod.rs
@@ -252,119 +267,129 @@ b2f948bcc3dbd24952c7b1b02d41d06757f49f0c38a78caf51f8f8a687d8480d src/auth/files
50ef648351271d78310e00caa6d246e0009171ed036a3e4e1144737df8c064c2 src/auth/mod.rs
25b9f78a7eee277902d375266bd0f023b79794bcdf646214fc161c5e6dab72d1 src/auth/store.rs
745226cabb007819370fc6aa680995a03a8a13fa057dacfab0fcc21d0c43bbe4 src/auth/tests.rs
a4c14df927c22a7fa46023f25cfe48d3aa3c245bec97fce834999d9bcc8d75e1 src/auth_provider/choice.rs
514e99e0d3da21abd633d53872234bc4d7d0178583ab291e53cf4b1802fd5337 src/auth_provider/errors.rs
d3d9b756f8dd0444ce6937376f27fa2eae7eb4b9304a2ff039d272c929592bdb src/auth_provider/mod.rs
72a6e245cef078585bc39932033c05cd866ee518e9cb4e7b89bca87ce941e971 src/auth_provider/testing.rs
59e1af728770d28265198844b3dd5740328bcb1df96d170aae575d3d97ac42d8 src/authelia/mod.rs
36f87348cf1fbe557f6e94296d30e5b36ae4817b6a18071767d96ea21e43f498 src/authelia/tests.rs
0137e66fb7127f1ce023b1c6cfe3fac3fb1e8be17f1de722f8ef4e66d2594a13 src/authentik/mod.rs
d4a388f0897e50ad26e1d8ea527eccdf656557abb4b88fe714cc2b1969d8715e src/authentik/tests.rs
32a25e53ac9542424debeaf1b637155bd4e60abd8692df082729d81232eb90b3 src/backup.rs
76aa2afe9e7302be39b3512f30d0448d9134300f8ae930d64ada40cf3ad4a8a3 src/backup/archive.rs
5e8b1d7cf8280bec54521cec9e4a8cb9cdbed5deaca07e3a79c43d2336f22a80 src/backup/archive.rs
15213c87d4393739328ebf0eacfcb45f864ac12d7bd7509f750a241032f6c7e9 src/backup/ownership.rs
37f44cf3ecd71c7e817c76462c4c7de7ca6ecd68c0544f6429a861cdcde1fbdd src/backup/snapshot.rs
ece7a5eda4b57bae8f5d819a309cb411b95b1845f5746ac162dee51456365857 src/backup/tests.rs
f5afae218f3a6f91bc2f1f5a90094630ce5b7ca574de61807b5a317b13937ec2 src/backup/validation.rs
6edb5aabd210ff2d121e435815b5501c9eccbd7a8ba230c843c6335a3e24f94b src/backup/snapshot.rs
859cec0bcd604f205d7d8e14d1c810e33d97fd7cf00ddcc30877bcea7c5fc2b5 src/backup/tests.rs
5293e1b06f82654929805b53b9092a82b7503a54b34da3e882a999c5f14ee579 src/backup/validation.rs
eec564a7ef886d18ab75289025a47d62bb702f07d63f92d2f83b7afeecebbb15 src/balance.rs
8f7975b488fad31f8d83ffedf8a5b6774b86b59290ce0711e779bf2496d5503f src/balance/health.rs
344631248c71164187497a10d177d8eaa3e2f4a08111520d2536ec89ae55e1e0 src/balance/lease_runtime.rs
ebe2b95f8fc5904850a88207f0690449ced49b28dc14ce5608b706659e338fca src/balance/model.rs
a3d052a928f73ea430480637b867ae874d447e24b1ac913a96a068a4dd74ff63 src/balance/pool_core.rs
9158a2bbb303c53bd3d2c779d9eb6e4470d028c4f28f8e329788e9c8c830f843 src/balance/health.rs
458733ad4ec56d58112de6ffa3539ca7e9fa8311048a4e120da96b5f899d5dae src/balance/lease_runtime.rs
bd693ea1d15d8da2abc08c6b5b50d42b693d9d4e9344205e9ad7ae779c19387e src/balance/model.rs
ced69832cb32e7dbd0203d74f5a9c60c2cdc682729f4c5e2c01e7c082e7adc24 src/balance/pool_core.rs
477132dd381e23f1be197c32ff58a0925f41d3e6aa63fe77c10973cefb2b76f6 src/balance/pool_selection.rs
d789bf40cf9433e2fd16409d112210fe93154588b1ac9bddc9c7f78e1db14426 src/balance/pool_view.rs
3e75a3cba5f1cd1af996ab71b6ad50f6aa05ac9cd97ee3cf88fce7c6b7271fbe src/balance/tests.rs
16a22aa72f311d1fea6d8da03c3ea1590bc1f248b21876354eecc0da0fd93308 src/basic_auth/mod.rs
a361c341932ffd186c0b21356c91ab1fe8043f5020d40798266d15ae896a114e src/balance/tests.rs
ae06d238b1451328c751a511476ef39c012f3a397393f939ca02f96ecd0119a6 src/basic_auth/mod.rs
9a8858b7ff461a4d961ba701ca3ed6eaec15c3f7f05eef8e0e30edc63617605a src/basic_auth/tests.rs
76b7db53f38c95e128fee503381efd3fdcc13340c124ef277faf0fe5c1c25c39 src/cache/cache_regression_tests.rs
e35648537d5ac0bfb604997200d1be25652ee4e8c49bf8b720b68df087bd0249 src/cache/cache_regression_tests.rs
7def03ace07dc38a4806277d75b6ce673d33ddaaf1c4855fa0a3372be1cf9489 src/cache/cold.rs
b75ff512a1c6344483e00d2c67578ceea6b5472c279da0246f3622daa68d7e16 src/cache/cold/cleanup.rs
b0ac49535454f50349b5d808c9ebf3a2506c94e63a47dbf802c47fdaacf9ed61 src/cache/cold/cold_tier_tests.rs
5fde9e9f77277d557372e4614264512804192bb51f3d2eae785988c17b2e7253 src/cache/cold/cold_tier_tests.rs
3c3897980c31a69d96f9295689d1ae19f2190da758c69ee7a0541f4171288bb8 src/cache/cold/demotion.rs
e80f84449412c4ae7bc753672a554160519bf1f186b5707252c3286cbe13f02e src/cache/cold/startup.rs
17b2a157b03f43f49ed05f1f3391aef025a453b748056c67229cb3ceaa53bb7a src/cache/cold/startup.rs
83095bc93132f306c537b31ba929669fedac99af3c4f133d0533b5ed7ac4e6ed src/cache/cold/worker.rs
a36a4a255a0fd2c38449bbc462b9c0e21488eae108c70326ab2a0e4c7cee0f97 src/cache/collapse.rs
237b535362af6fca4fc2a51decd1125243ce3068a840f95f9564fdfda8a187e0 src/cache/collapse.rs
346fdb40d85627b583a01fa17c65e45c822dc7216198cdeadbb3d07904286fa3 src/cache/dashboard.rs
54785b64ab4a7b0382b84b4a313d1996c4b6986d69f1e598293f29122afbad33 src/cache/dashboard_tests.rs
8b5a8c04a8ee115497635d2faf08eab5ca4db4225c6c70110675685346f9c0e5 src/cache/fill.rs
be3d80cd340edb47755190a668e2f2a2776940665f95ae7110893b706eb94faa src/cache/dashboard_tests.rs
5b0a3dec613fa22a38237618ddaeeca9df5c79acb499bfd78a31d9e7ce104d61 src/cache/fill.rs
675d18afb53f25c56cba49bbb431fa39ecd9d7bdd192c7b39b0698225de7a2a2 src/cache/invalidation.rs
c1357a13658e1ed5158084d3a41ba9ea906512774a5863ce9c56cb508d3fa6c8 src/cache/invalidation/ban.rs
bbd9f372648d89b5e14d5b58029582d37f66a89752caf2c14eb1cbf8f24bb7a0 src/cache/invalidation/http_invalidation_tests.rs
6223eaea3f061b111330cc26b28d85b9b239bef30328025f2ef337291e22a445 src/cache/invalidation/invalidate.rs
9bae8be41d1e6b30cda51feff99fd77738940fa975bdd4ece7da75d936f07b47 src/cache/lifecycle.rs
1f3c731b57e98b76654446068167f3f5d1aad88259633ea871efdf0d790a6ebc src/cache/lifecycle.rs
bd831807ca4720b3a41a6ea1d9a6c0ed2d935225e2dacbd9313be064f2e4a20e src/cache/lookup.rs
13ecc0fec5f9ea4c555bc23e7ad0aaec2ba10032e8f5fd2729675316edb28487 src/cache/lookup/hot.rs
9b9a9e82e162699437798ee016e6dd87673ba8157935ac22b7bf6f5df551f841 src/cache/lookup/hot.rs
4148106b9baf70e41eeba24233b1edbe8dd6f927c11f2c7647e89fbbfb60555e src/cache/lookup/hot_hit_034_tests.rs
e9c6ab0c2be9f96573f826e6a1b9e4c2b8b526f779e72e95a6463ae07e45ab55 src/cache/lookup/insert.rs
6943a1856da7aaffc9af335d0af7bc88cbb7a8ba9f67a83c91e4500ad431ce56 src/cache/lookup/tiered.rs
18b9614fdeb7d9916c9f7b394bdc7f78737e5a7c0249763a47d8dd88bd4d52b8 src/cache/lookup/tiered.rs
639d5be6258699ec7e9f260ec33bd2c366203b6655bf0e98f30517c2e12ceeeb src/cache/mod.rs
0c8b0d550e94d4474dcdf991aa0340228dc5f2f904f39350e6e666a16f16fe87 src/cache/model.rs
c409174a9aaf20c69dc7b329108d1c26b5672a7341236cf639480df48800d262 src/cache/model/cache.rs
44b1da391c0ff51ffba6ae43ddfbadc171c240ec74c5bde630d3c5993b6ac15b src/cache/model/cache.rs
0ec4f6d0adea1ed80a3ca17c68c9c85502491da2904d9d690b4169ab9b025b21 src/cache/model/cold_metadata.rs
9f3143d99ee7e11abdb625cd8deff0af3ede8af716ea99b754e92803d1150bf7 src/cache/model/entry.rs
d4409e5137bf29bdbfc11f4ec3444e3ae408d6057304b29f50d885affb441d9b src/cache/model/entry.rs
ff1c3a1a3bc0d26218dcc943ef951c2a212c180bbf6e4bf1c542ffbf12a9f150 src/cache/model/http_invalidation.rs
81938ee9d96891a6e1c81ac1117c67bd4ce4a1530684eab84521b3de1a503dff src/cache/model/maintenance.rs
19aaf66abe2ad30b973131a020b7ca8ec6d0646a51df5c604c5f8914db898416 src/cache/model/settings.rs
395a82091ae520b78fa606553a0ec590ffb14540b83234cef5bbdcd2e57d39d5 src/cache/model/views.rs
ee9508498d420a726abee1d12ca21fd5117c1908ff53c2ce29f01de4380f26ac src/cache/model/maintenance.rs
f3859df02872b6a99b7aee38c34d7b4c48966e4e32df827844507e44d68bd6bb src/cache/model/settings.rs
14f3f8ae5252a2dc13fcf9556523ffb945b77ee92a5fb4939cd27e113fc61660 src/cache/model/views.rs
e26dce02df9c043329ca164fed215d271781bce8fb03240f528331bf3df6b9d7 src/cache/policy.rs
896b1869f94973b30a0de0faa62a2c89fbc8991b02de0e5988ab4ce1f77b392d src/cache/policy/request.rs
9dd790a35dbc15df8465664ed9fff296fe22847371d667aefe320504ab952b2b src/cache/policy/response.rs
e59efd322f518ebd7b5b154115188f382c2cf7882ca0a401736ba5d685981ea5 src/cache/policy/scope_tests.rs
4179c2c1367a9c8ea8432bd5815b7e7afbca820d7589742b3ad4775c0cc8e0bc src/cache/range.rs
70c255e15f2b8c030988a2263b0e07b5c16e7e26770c72becbcec6da4550fcd9 src/cache/policy/request.rs
6ae7a79886dbee537d15830c4647f59f158a2b31425d1c0f06218590c261c968 src/cache/policy/response.rs
12a6d5d9d6711e956391b4b952e8149658b8bf64cfcc0a4c59c281f2630a6aac src/cache/policy/scope_tests.rs
c3a4300f7cdb6b2b7c16fbc489d49ba2451331bb4a55a2d9f618c684fd61eecb src/cache/range.rs
e1d3f0512d6290b0ac4998f47398a0a7c404db249c5bbdbc4ad4767071013a72 src/cache/stats.rs
8a300f5dad44815401eb23f4f5a157893761852ba4f3005041e0eb3f16892ce3 src/cache/tests.rs
8d9532ea1f30ec431195698a9aba82f2d31083d4b4ee98cb8d9eb47b2b01c7b9 src/cache/tickets.rs
b7c063ed4c8ca96714e1dc5d279f38fc521da2132faad7bd4f40311352a71e7c src/cache/v022_cache_tests.rs
452ff2f162f70c3e4d9259de4b13a7751874742c6922c5e0e0196eeadee4ccaa src/cache/varnish_compatible_tests.rs
5a8022f6f4721f9fabdc715e01127d9299d47ee9981a79d26df1981448487d20 src/compression/mod.rs
2bb1ea392dc18553a03a6ae1c53f52712ae86571ea31b1bcd93c8fdec2cb60ec src/compression/tests.rs
98af70ada3b14f3ecc94c9c76b028c1b0de1ffd43d9836a0749de763f64d5402 src/cache/tickets.rs
a1132b980833b1b4ce2bba9aa681ff255a0e2d61faaa65a316860da503ea1e27 src/cache/v022_cache_tests.rs
4ba0a2e1c21a79cf68bf34cca1f2c2a3d7b20aef38f3b9f510743e60bc1d425f src/cache/varnish_compatible_tests.rs
1a2eb6684d9ea4a7e90970d936c75f7c8bbec46a4181fdf273be9bdac8eec847 src/compression/mod.rs
8de82d9188c9c337690c6ec4afdad9fff20651c358e34602a93d8bb8c3d2489a src/compression/tests.rs
e695079333ebdc2a6e7b3337cfabef171b3a0a01da0597b77feff70d417f833e src/config.rs
9f6272bd6d1f403cf6324b12472fd5c7d911a387139b57105ba1819e48ea0a54 src/config/backend.rs
f9a400d66f37056a0a2d8c77fb44c2b3db9fceba3511e4a5b1e4b98890d27b13 src/config/compile.rs
d727e5ebd6c6184943d5f773c6b5244954832139c744851c1357614880f09c6d src/config/backend.rs
367b46f2b57143db17044378b1f5a55bab1eae3ad6ba40f388896995d1b2a13d src/config/compile.rs
fbea01fea1213d98aea812c1b78d5db289e09200b98ae514d742235cd3fa70e1 src/config/internal_routing.rs
0d9f62fe4600b230e200f41652effc22ac7c1b649fbe63505ed6ea34c644feea src/config/model.rs
cc75698be2828000e88f2c482bd26f2fa4050e3432bec1b5a581dca17fef4a37 src/config/model.rs
75839458ddbc7480ec4492a95eeb67b617ba7d62f7b67e7a767a3453b74dbcbc src/config/route_policy_tests.rs
40e9483d7b2450f55d29b46c408727f4e331b85ecda44fbd7b96bd9a09f02655 src/config/routing.rs
736b46108c72220b8142f4f2ab128876b6bd82e0d09891578abdce6aa03c82f3 src/config/routing.rs
dd9a6ad8f58e0cdd1ae74f3f972fee0ac14383cd8b8f518403b9a36c169f35bd src/config/snapshot.rs
e943525706c51846adbac23d4a4f4b1a22bd20103b3004b7de1cd1e5e270ca50 src/config/tests.rs
477cddaa1e1c0b9b605a2f550971a04cf30287519cba70cfbb2e1860e3a93ff2 src/config/tests.rs
f35de0ee1516ece6bbfd2b2a7db7d1e0f5048b0ec47cf2a3765dffb799abfd93 src/config/transport_tests.rs
eb04a8922da26e45c9b6005c0cdf1b1ddf2da3c3b680650b22a1d1f16d17a4b2 src/db.rs
e0f46d885ed81ce306a110560cdd2c217dadde054986d89fcec835ebf6939d65 src/db.rs
c290d0cb02dfa5d2c60ecabac1cfb20c8884e3d14cca1944906c6d8e677f1784 src/db/api_tokens.rs
892ec0e5985700cca399c861197f6c9d2f1c823d8d95b88abac9d27ae15a069a src/db/certificate_api.rs
5491dd7bb9b3fceac97c4dea63aba6d5c7abcb0fbd06e36ae986e00c79bc07d0 src/db/dashboard_presets.rs
2ff744223934685439374572b83fd639bb3eabdbac168bea4a6501a6e9ce0ee0 src/db/history.rs
c05a02618d476e4670b4f9930555f199cac776cdf09edb8fcc1a675695b8f99f src/db/tests.rs
dada314e1709e960eb2e225b4d10c3cd12606c62c4bc2c0220aec1ba133e4ade src/demo.rs
5808205f03401e3f2c7f5c826d5de76e83e4b6f94c64ad3e50f505f985f6d26e src/diagnostics.rs
daf8f4dd4bc4dc4d1c69fa583165f88fbc299eee9c10edba75609424b0539ab9 src/diagnostics.rs
045593a42a6e4f632e5052c655788f0843faa313fc929265b89509f6dfff91ed src/docker.rs
93a8a3f6ba12761ddcbd19f22f32fb2c0e6948b660fb774ddd7ebdba9b6562a5 src/docker/client.rs
f69087a82cf353e9a92fc97ee96510dbe0382efa1c5e53821d16c455a3eb95a4 src/docker/model.rs
810415a436d842b79fa72392cc18977ad9c368d233ce43e135a60119722c9e77 src/docker/reconcile.rs
9a68e0d5bd98541314f1ab5a4c2e344e9837f315bd39ea3988ffab6a0f0425ab src/docker/client.rs
8feba6d9eb7fde798cbe8a9e339d6a5346cfef039980fce46727386717635eec src/docker/model.rs
f095e5e3c564df409874019c647f7cec2156bf5a845b5b84864879fd07cf80ce src/docker/reconcile.rs
2e8d4841665cc1d29e6262a1789bb64ced1cecd0036681cc1ef7b5d5bfe22065 src/docker/tests.rs
e36db1ed3188ffe54df7fc4ef76c4b453f23683bb7b8cdf601046e2bd50e0403 src/forwarding.rs
08d2e08a798abb944a53570f0bdaa08f1266b1107677ceda070091af4c2fc121 src/forwarding/peer_formatting.rs
0b6831d03841a1dd2d6ea73d8268d0b3fda678c6d9332447e269f1e45c60c0d3 src/forward_auth/mod.rs
6c25951cf69e972c47cdf815f22a5062916b2b768e39d55a26a5fc9e625a2344 src/forward_auth/tests.rs
5d32cf9177a33d6fb50452541805b10ae1b0833e709d833f5bd1fc6907193ffb src/forwarding.rs
b266c0a464bb0c6b581e423d08304a0121f126e8e42ef3a8469cadad42810fdf src/forwarding/peer_formatting.rs
b821e2efd2939cb13d01e402532f33feba6682acb6a7a3223be086c1cd171820 src/forwarding/tests.rs
5cef023035309caa5ca2035a11a77cd485f72092ab1c7670398d415460f84bc2 src/geoip.rs
80850f7acd1ed4c0048b065e9e81740869a8efbb0d9b5d2e83d8ec7fbc494703 src/geoip/mmdb.rs
06aa056d789742cc11c8826c338d985e6a6fb22da6c3530937eec38e35478034 src/geoip/mmdb_tests.rs
d3dc58628683472ef4d205fa5c42f0b7ef83cf90846146d96941cd3b36a40766 src/geoip/tests.rs
70091bdf6f38f03b6df8c8c4407c300677e57f293130122503f936500963d37c src/geoip/update.rs
c1b49bef28d87712e4c46138e70b52c4bcfb1a00196579031fac043d0b4aaa7b src/geoip/update_http.rs
9a912dad0b8d1e10410fd95d0da3684656092ef85cf8b23eda4ffe7fb92921d8 src/geoip/update_tests.rs
0541ad50b7bde1fd958fca59d2a4f9a0fde197bcaef68e6bf892ffdeaac46be2 src/geoip.rs
e3646c044604bbc169aeba549d973d37b8ac52f88bbab4a0ef74c6ea69681eaa src/geoip/mmdb.rs
1e8777b295e6a2de15b55548e8c4c3a4166b1c5af18d4d33bf067d8bece1b590 src/geoip/mmdb_tests.rs
cfc75486e69c310fa6e7bf0448ed39a20cdad9dd8afe75e01592fc7c76a380db src/geoip/tests.rs
3376ea3d018faa5cb8a69da2f1d5b7cdae4bb4b8f28f373f8a0a2996410e1bb7 src/geoip/update.rs
86a16add6757a377264ae6afe071039851d5996e80baca9d4882b71889d7c2d2 src/geoip/update_http.rs
8c72a10f07f82e2c0864551a3c95e261129c23cf6072f902d063564b6ea6770f src/geoip/update_tests.rs
61a2b01db9010f9debfb38b43a26bf588438a5e470dff938e4e54f9d7ba46732 src/headers/mod.rs
e4f07bfb7eb75b6897ef812870caff158679a3046d64582428abe8c6df490dbe src/headers/tests.rs
1b291ef7b06647ce820111cc802874394e3600b4b2c86ee9a1fbf7eee6cb864a src/http3.rs
0cc484bf0cc59a55a13e0e70435833a9a7a22ad7dfb5b9f86e059cf70a80cc2d src/http3/body.rs
3e9a19c221fa7e63d097c185470b17313da7c200a73ce50f6c069b4dddd37a96 src/headers/tests.rs
027d414f51e910ff20fbf3a924ab6b136178858f27819a046a922b1d8f349cd0 src/http3.rs
2554fc93a555e1f39d12d8c172097a4a4571d79db2b6b871a5a1bad0e8dd2778 src/http3/body.rs
741972222414c280104acbcd8f8071bed9cbfcfa2f503c487c11c56f553f53cd src/http3/diagnostics.rs
49ce4452ef7973cafa2e4071e2e14be0e16000e931a6501e4dc613652e49e230 src/http3/identity.rs
0b41072f6bcf28725c80f471584cd55d4900424ce5d55ac7f6909813788c63fd src/http3/settings.rs
5c550075c7b0fe687ad02095c1cad57b46b579fe21dc5371525b6c77db459886 src/http3/tests.rs
68fb5ab6a889995a66ad12cb6acf88c5d04f0aca572e2ae15f1a9cd171a5f24b src/lib.rs
ad0fc77b4065e4e59e97db60d77970c7336386e43b779198abcc727245f8c791 src/lib.rs
e8399753a6ff49b7de19edb0a268bf871c9979a518354160cf07b4669051fcbb src/logging.rs
e4be3ad758c3d93ce52dd3b1ab1a87810e5226339400ed997607a5b746354751 src/logging/error_runtime.rs
b2ed730006fdc080cf7eafc6d20bb89661c639631c89642d110e0c1319e431b5 src/logging/format.rs
e51524d45ddf54574bd33d71dd4ab18abbb642390fb6b6dda2a2c6e5439fe9a2 src/logging/model.rs
e9795eaa3e743c09f5ab0e18e6d359bc97a3aee55510c74923532d64df90c873 src/logging/format.rs
66c4596e644d9dd539d468575e2de6459bac2624fb1e1064cdae61a21c2e0240 src/logging/model.rs
bf898b318feb9bd3dd2c335b6b1bad172da63fd5cefbff3e267421ce37c076fb src/logging/runtime.rs
a9e453c94fc37e37d9f1f461494fffd5f88e13e15f6cd4158464dee4fafb14bd src/logging/tests.rs
0e9993aa6679381d91f3a22068b30e1611439d7fe50900f49a4a73ffdfbdb3b1 src/main.rs
3d6ea5508bc9609c6ce99a3a1d3fec2fafb95a2a8de579a80262b73c6eed3a63 src/logging/tests.rs
cdcb3806669207c17279da959bbfcfacf18cc192f24e862c0afa0c389a41631a src/main.rs
87e69b46c548b861e9d90d9d2e42dfc2630891f4583800b1942f9065d25e29cd src/main/admin_runtime.rs
224da3127b5f846a8932fb7f4b6e23d0dd590dc0170506267381d66042c64666 src/main/bootstrap.rs
5a98d0e249338e73aa57ae2e352dc16db2f79c2d042c1544ce1c2022da76a31c src/main/proxy_runtime.rs
b240033715323e7cb7b9bcaf92edd481f7089b4da6bab25005ca6733da8756dc src/main/shutdown/mod.rs
0a6578a0f4023b9f8d73043bf942dc5e096ef9afb8c0d2f131c14f65d08ac01d src/main/shutdown/tests.rs
ca01c97cc52a50797306193a546e4379fe3ce9401b0c936c756b881180315ab0 src/main/tasks.rs
ff491043180085800b786b77e6bd3c400b60051012482918440cd5dc6493d1e7 src/main/tasks.rs
a719c51aebcba0f16f8810c37618201954e79d6806da085ee6c5e4697a05b521 src/metrics.rs
9c7ed78345abc33d260f8d5cd52339bf8de748c08b53eaa2bdea8a32a0ea2a20 src/metrics/global.rs
31154052757c2bb975b8ce8cad67f3fad7b5132b615a73d5b0d7bf1b5311722b src/metrics/rate_sampling.rs
@@ -372,57 +397,60 @@ f27f1f8bed769883d1a7ae6a48f89f7712394dfab65d05702b0f944f0a45bd8e src/metrics/vh
ee401f943bd8b2706c33419ed69102269c08dcb4ceba6f4be2ac0861115874a7 src/metrics/vhost/tests.rs
9c96c3e4edb20dd0c36b5c373f1925dd60368e75853002206523072d418d5417 src/not_found/mod.rs
fa2fc6c6987a8356a9da92cade10db62426d8863e9cef3c0d4324c45780c5b33 src/not_found/tests.rs
d32d757ace8753eccb842c24b7af77061141478d10fe0acdfb05263d19e9a64c src/pages/mod.rs
9d3f4db663016033c4ba0d86e30367bd5a1ab79c8bc121265d410698104fa19c src/pages/tests.rs
c19998732c2d65b464f43d17a58644db5bcf7b4a6dd4c31a65a091c57692a5bb src/oidc/mod.rs
c4dbe0f8ded237d45b6a3977bdc7544296e4408996815cae9e28af028419d0b7 src/oidc/tests.rs
992f49379e44361d98088d8f8ca1013d62846a54a5a35c089a5e0e1a9488e08d src/oidc/token.rs
eb95c68c8639ce3ae658eea3334795cdb5e129fb2ba10955f7dffe06d8909b57 src/pages/mod.rs
86008e469c84a7242b7d4947e2116694ababa2cb26120f246d00923a7c019720 src/pages/tests.rs
5650a3f70162abd97021e0a51a92cac2c4dd39004426ef99dff36cbb0741aeac src/prometheus.rs
192a780d5874afc158d62a7dbe1687d734635afe9342fff526b8a2f6db4e5633 src/prometheus/core.rs
6c97eb9c593a7802422100e8130921e63b6b9b1b0387b6860af9f49581cd50b3 src/prometheus/core.rs
2321ee667569803dfe6c92c0e98adba0ac8fcdc6a3eacd5857233f1acae93daf src/prometheus/docker_misc.rs
cc909edf740a091f9057912f555dd888b644fa1cb49647fe0e2e1f5dba8ff5af src/prometheus/json.rs
f9aab3297fcf647295849f77cbf99197f4dd27efd6817e54c03451e04972430c src/prometheus/model.rs
2ea975c96fb45bfeeda95583e909abb26328a16e411fa46a1e31b2a359f7e711 src/prometheus/tests.rs
f8a9541b568ade946dfc0b3353548a773a0126f705b537815ce0523439b07ef4 src/prometheus/tests.rs
028e1dd9f2ec2afaf699a6b21c2d4b0d96e76f11f5e4ed009c233dadf0f3be51 src/prometheus/traffic.rs
e03f0d6b3f35dae6b98c394837aa0d17e4d73a8306767b4c607868bc00c30286 src/prometheus/vhosts_backends.rs
cdbfa0e7f49200dc70ea6df8f0594399ee3e0c5fc3d279cc336e2d83d0f8a535 src/prometheus/vhosts_backends.rs
f1b0254f53b1a4336bd8170ad441ba072b49a114e497b985cb20699b4e375044 src/prometheus/web_php.rs
569eda61453affd053fbe24fa59652289cb12d22b77e64caa38325bbea48f7df src/proxy/body/mod.rs
57a71c9c6e4e0deba5d05963b9870af8f4c4d8dbd321ed4e52cee88746c997f0 src/proxy/body/tests.rs
31d70f8be178f9213159bb030af448f37cf32ddb40e3142068a3b335df8749f7 src/proxy/cache_helpers.rs
7e3837d2f6ba7d07ca2cbabda57b315f5a7f4d75a13e5eddf6861a150d0d2ed6 src/proxy/cache_http_invalidation.rs
d134b0eb617f82f9971b3f3461379ea37c57bd0079bf54748d0c434b2ef1b475 src/proxy/cache_http_invalidation_tests.rs
a03b1735daa8fa8e40cf5bd9c6e200d211baf19c30f62522d4237ed56642bce7 src/proxy/cache_plan/mod.rs
3404b03096862de1050088f62cdd811ea1ab61d191f902501173107f81f23b9b src/proxy/cache_plan/tests.rs
a0b9e2e9ab0bc89c7b64bd19365198234b1c5c13f455ea47be122a5660639b17 src/proxy/error_fallback.rs
3efd124f2c27ac2d5b6c6c0ab6a5b91b7b18ce2f3bf097a00fca5475b05e36c0 src/proxy/forward/cache_fast_path_tests.rs
f7903200756dc6393392560656b23364f0d444d6357d264713b17c63fd8b636b src/proxy/forward/cache_lookup.rs
315cdf2ec1479433aabee00f616e8c0a9ebe4697bd4928885433b80e52a706cd src/proxy/body/mod.rs
5beeac12ca7f041bdb9b74b2dbc9781f5d5524fcf51294b8b6ad88284754ef20 src/proxy/body/tests.rs
1ecfebeff9a6b933368095b84762e2816fae8a99339e0b854e67e9e95a937e3f src/proxy/cache_helpers.rs
2d39552aa01ce3d048560944975243d0a58ae762d838e547f5fa65007d871dc4 src/proxy/cache_http_invalidation.rs
a86b10ce0707b51e925e11ea14f7f40fa108a196166f62cf40007a97f9e17203 src/proxy/cache_http_invalidation_tests.rs
df239d37b008691d54e257626009684310e6e3712e777094b8450579d56f732f src/proxy/cache_plan/mod.rs
811a00b55b24638b4bf8a6bea9ed7c0ffe2a758f76bfe63a95e1d31bce1c2c9c src/proxy/cache_plan/tests.rs
d6bd4aafab7b1371c31b3d22ddf949a08ea942489649ca12c7615b3b2d5a109a src/proxy/error_fallback.rs
d0d64fd4dd89efd4a9fe089ff4551815cbeaf5b883ddcff803f1b13b5e918dc2 src/proxy/forward/cache_fast_path_tests.rs
7611390a02341d5f9fd223626e57e0f2f129ab45e03fb1bc105be68d46b86a9a src/proxy/forward/cache_lookup.rs
287012c81144d8f15af4a148ce3ca911d431bc59adff44e697567561aa372bb3 src/proxy/forward/h3_navigation_tests.rs
a6580dc359d1f377a48ccc1a902056dc5c2ef86c89180b9b0995068d9817ae8a src/proxy/forward/response.rs
1a7322aadbe90e2a6940a27882c13e274b1c92f14de3ebad717054a9614d4ba8 src/proxy/forward/route_cache.rs
0e6e288fb8a0575aaa4e3bcb5c47ed5412a584386a2b00fb25db8428d652cbf3 src/proxy/forward/success_finish.rs
744daef016d4a6c821ab12eeedaa2f29e2de72e2ac9dae16481d4f43476cb713 src/proxy/forward/success_prepare.rs
e9ac9be64f6f83c4ca2cd5d299649f24dd16197712083c6dc10e55c662c65b80 src/proxy/forward/upstream.rs
690ff6a6860a7ca8ecd502657a68e5012b7f176036d6d7033dabc514735dc2f5 src/proxy/forward/web_source.rs
3340583d395fcc859cf3421f4e33cf8347915ee4c468f7d0e4abebce8156fdca src/proxy/forward_entry.rs
728576f7a42292386eca9cf450779364da87f94dc0031ba46c6f4d5302755949 src/proxy/forward/response.rs
0ba6b62e8560d6f7b99ca7e387f058412fd44456f5874a8258c1c24595c435f4 src/proxy/forward/route_cache.rs
28aecd57891ef67a618a0f8e4df0aee95d0cb6090facf37e531cd190331d06e7 src/proxy/forward/success_finish.rs
ccc54869ffbcf1edae9ddb96f6dc2191739f8804ee0318b7a6844def6c4693e5 src/proxy/forward/success_prepare.rs
c965dba1d2639b338f3f3cfc17f760786a30abab708dfb7920ea3bc94d4d734b src/proxy/forward/upstream.rs
5d72200f3b5308215b7a3d7b1f910932eaa4712f9e2954afdfbe2f252e2a2d3c src/proxy/forward/web_source.rs
0b03eed32b00d709e5887c179e49f7bdf0926ac19d2055af511333262039ed19 src/proxy/forward_entry.rs
9b25f5e3ea4443a63ee6f45d12e1957dd316fe8e85325fc00ea416faaab1025a src/proxy/h2_graceful.rs
684ea1e2a7591a17e3c633576fc66e2a30d6636738fbce61dab5af44f2c51a67 src/proxy/headers/authority_tests.rs
66efc52d64d34667ba7419f8a2da89d2efb1af1f426684f4fff8caccfdf19996 src/proxy/headers/mod.rs
0f9314d1380febe436def36b5f381731e97cbac90625637575f38f3d72eaaace src/proxy/headers/tests.rs
c04451ac8abebf4662ccc3ee4610d5342ad5382da2098f7af5db274d079770b6 src/proxy/identity.rs
30057732c35c369740a176f7da285e35a4157571a04328f6c6f1494d4ec8550b src/proxy/headers/authority_tests.rs
e87437ccf80e50e0159d204dfdd1f8dc0739b6e375be192fce0af749810258cf src/proxy/headers/mod.rs
af9b044761a56e8a6dc57f9d760b289ff4a6bf37db11dbfcd3ec4da5b8f426b6 src/proxy/headers/tests.rs
2bc63579975387c92a9459a9677b4a49e76df382b1ee19de12c8341a3ead57e8 src/proxy/identity.rs
a08ee061e146cb7a08311106f73a04468c89f77b6ad05a2c623621a2ae9faf2a src/proxy/mod.rs
5559efb5984e8da34235e2e5cb34cd6865a34ede20e159bc9f04fc9190a5c09f src/proxy/redirect_helpers.rs
92806ce1802da9b6771ece474d91a3458eba3f6ed19b0817b897e828a02e7e32 src/proxy/redirect_helpers.rs
de1763b89d98607e20f0f6e3c9b546483927cbe5450efc2e5634fe691fd52ad6 src/proxy/request_id/mod.rs
b9a2307c6aa7f16613d935508a61574dfee964020b1b5be86293df2d684615d8 src/proxy/request_id/tests.rs
580fefdc7dfb27da73f86f4d2067d46a1f856d2f398b0598d37a743e880d3dac src/proxy/types.rs
37b20f962563babd3602d21ed6326946635281c0b0d97ca59f4e33de170ae93f src/proxy/upstream_helpers.rs
56a921ee4f30da154176998a383bcb5a40d3c7784a04829c59b8e6536728e2d4 src/proxy/web_cache_revalidation/mod.rs
fdc589ed3968915558a98a11c7cadc36145b5b0b0dca9393613391990d138fc3 src/proxy/web_cache_revalidation/tests.rs
78bbc1263bcd600527bf71acdf5af92bf186267a9084b076d1f75ad6492df659 src/proxy/request_id/tests.rs
9d7cc19e78f50c9b96e8ada076e12ba96d7a19a51975a13e554c4463e624ee92 src/proxy/types.rs
e44430c54038a7dc26f327b5d7085e965241da2ffef42c1526ebfc5168981f72 src/proxy/upstream_helpers.rs
c3887e344249ce492862c365896731127b7b6fc51bbfda274d7e7aad3a70be0c src/proxy/web_cache_revalidation/mod.rs
5522456cc85948228250feac92308d53b130c4a7af98d24f669aca4dfe6999b6 src/proxy/web_cache_revalidation/tests.rs
3d591201595a3b1e928ded20a8736477a47e5acc3bcbde158f7c17547ce155ba src/proxy/websocket.rs
f8972bedb1a487fac86177774cd98cf2619fb25af97e57cfac2853db5cbf91a9 src/proxy/websocket/handshake.rs
aa58bacc35458ac615680e774a235191f80abf0db978b68095571fce94c2b502 src/proxy/websocket/handshake.rs
c02d42b5c7eeedbc54c58e98691f7d8e39f8e9be63a48f6d6b4ad6b8ea431b7e src/proxy/websocket/http2_tests.rs
faedf6cc2bd4c42f342a0aa188b634d6b8d280ca0e121032977eadaecb88e9d7 src/proxy/websocket/http3_tests.rs
7e9c4d4c207badabf90630223964bb2ad641f06d4149843cab751f192b8b4adf src/proxy/websocket/tests.rs
a44421714aab37f7ceda19e3ec2512c645d2a3ce5dcb3da5bc7a0d87a442d6a8 src/proxy/websocket/tunnel.rs
999cbf85cb6e917db529e30ba4a5eed1cc9e95a054da7b3039c730bb80061449 src/proxy/websocket/validation.rs
80d0f744f7ad9f0d1edeb0d5c23b13cb40431f6b6f4d10d534ed34d4198e3dfd src/rate_limit/mod.rs
2fcfa9706eb0708519d5181debaf3c85a86e401a8898df72f9eaf0ab72c9df85 src/rate_limit/tests.rs
c85c9f05dac65cde6878bef3ef7258eb9de79ca14c26ca2492e0e13839c40fc3 src/rate_limit/tests.rs
e53c40b85792d4804a432e3c97dc13af8e29304bb0d250ee9ab9018bde522eeb src/redirect/mod.rs
811c83ae9fd0f5a13d35b5443cd37843f82b9d600bee4bb136ba9b87732a5347 src/redirect/tests.rs
244d72b3dab36ec505c96d26a967420714fd3807268820f680fb3dcea0e88c0f src/runtime.rs
@@ -430,49 +458,49 @@ e53c40b85792d4804a432e3c97dc13af8e29304bb0d250ee9ab9018bde522eeb src/redirect/m
2d610dc36f746215ad3bc1d860ee624a9233886d73a019fd243e51901503fc53 src/runtime/constructors.rs
eb2b0eeeb617cad704732d87acb667530144836e26edb0189929b1f25f35c6b5 src/runtime/docker.rs
18f7460e3738386e070f66932cd77240001255447a769d6e19a40573254a1473 src/runtime/external_config.rs
a3b65ac6133979b2905b878e5634537994816abb79480af219a17896c6dc4e33 src/runtime/tests.rs
d7a65497d1faa2f09a09c0a0ef77b444990cfb5ab379c85f6c27c1f9c0c143cc src/runtime/types.rs
5999d7e91ee4231da88c5e9c6d8a12ea9f92d20d6dcdb24319f9f90891d7253b src/runtime/tests.rs
bab3044f4ad8957b65476a0c307bb8a364825b91d2100233b7bd6efa52e771be src/runtime/types.rs
11e99eb425887ad12b73d949f4b497fabc6897fee8fc7647fcb3766269c2fdc0 src/runtime/updates.rs
e3dbd60953db5a976985dd796e4268b41a0f3df59764c4664015bd41e2578058 src/secret_store/mod.rs
6dca8da072a3bd610704a391bb6e25c9042971f4ebc60edb47665ce1d74cbc58 src/secret_store/tests.rs
98854bfeb35cc5266210b1c33dffb36b8441e27e61d90ded8b681430696d156f src/security/mod.rs
b0c4a46b40b09794fd8a037afca380ee3b95941bf75a0e1102a4e520a4bd8be3 src/secret_store/tests.rs
53bc7b1fcc1eab8860ebafccd0c9ed2aadf75363323891a4a4a22114c323b22a src/security/mod.rs
0e5fc2aff856bf6fd959cba12b5ddd1e3b3cbeb0fbaadde906c811cdfafdaae7 src/security/tests.rs
0e8589f56e3229dec556b427b87c2cc5a5a9ffaa9ea84fc2522dc72d3239027d src/server.rs
ad4cfd4a7d6f331a8ce3bf17a6c461dba788bb41994f667033c6963df3b96332 src/server/http.rs
00dbce1ff1ea08774bcfd79d900d71704256e06ab0cc46c379c3d107c99ba50e src/server/http1_connection.rs
1cb8260c74de89c3e13bc225da32cd86fe883acd3c720d0cb2fc44c0c55e940c src/server/listeners.rs
a64bc0d4352ec8d97895978a98b6b66e0459842d1ec410a1d123d5d29d9ef058 src/server/sendfile.rs
4cb5afb53c824f5c87abaa65ba5dc85e937255c4a3dd14af16df9af7e01afb10 src/server/http.rs
533342fb3bf8b52743d3a7102ffe345be9f86d553b16a1ec322e468b44b0a7eb src/server/http1_connection.rs
22fca56396333ee7a5031da35b15c3258e93aea4ead6af733fcbb1716e21654e src/server/listeners.rs
1c4b84a4aa9f120ed3b7669a145ef51e90d825c559d40088a2b4074bd84a4912 src/server/sendfile.rs
5f6a59c6085c1250a93a70020661d1e8174d3b600b494d16f8eaf7941ee3bb99 src/server/serve/mod.rs
048a05a46108f50bef7da50b0c03b752d76e586b411de8ca3f55313bbdb08aee src/server/serve/tests.rs
669ea1041c103cb952f7a516363c8fe7baf51622f721aabdd57015e0e0279530 src/server/tls_runtime.rs
d0af5ee91ff7f0315d347b3fd41d60b41e9dd095e0d015ecd99f17c4f3a923f7 src/settings.rs
298673b6407b0ceda6ebc2ad4f4d787688cb0a5f144164f74895c745bcc53aa0 src/settings/public_origin.rs
a4fdf54f87bcc96b31e8a0ea400398a70011a044bf2ff3de78a0489b767f9de3 src/settings/tests.rs
c03472e8d937bb6f851593f4e0d347dec6b66e85e577c87a09eee0acd990bca1 src/settings.rs
b2e2924047019b1ada59c58a4060476151c99be12c0b4092f2cfd201c0756255 src/settings/public_origin.rs
c599d1cdca84ac18da7e7cdf6e2d1556d4df770481a0607edb96f6068cc051e2 src/settings/tests.rs
2ad7888b263e1dd9f0bd588dc35590c9eb232f8fbacd10ea7554cb55b4dbd25b src/stream.rs
c7771539fbb94544947e958e410c46b73f9647011e56b1b229ce7035fe21851f src/stream/health.rs
c02e85af45590866f20933561f85ea9b4f2ead62c2ce41db75113632670a7b5f src/stream/model.rs
5b74acf26aad3b73a10501866b716de5dace753453d5f09c4f22c414e5ad880c src/stream/pool.rs
4b9cb1135c53f4a159bf2fdc1b0c7900653d7651612568f38c12f06e70b8ca72 src/stream/model.rs
42aee5900c3c8697c80220c77a6af0435358e3b1fdaf1a246784aca4f62edaaa src/stream/pool.rs
6ec42b8647c95a0c2025462e0be29c71481ce1c3b1f4d4193926471f1cf06be0 src/stream/runtime.rs
9346cbd71e006e41a7188f28eb86f9b00bac822e4cb31c76c8d6d2ec5d4c4711 src/stream/tests.rs
63924d279f5fdfde24d2e0663518a123dd673cc6ed9ce52233c4b6851bfb1efd src/system_stats/linux/mod.rs
d9123c623e2a97d09e41edaa29993b4b2c8cba39a3abdf2b48a205aa9a9c2939 src/system_stats/linux/tests.rs
2c7da96822d605193f0208df70de670612259fd8c762c998ae828848cb210e11 src/system_stats/mod.rs
e0676801259fb09a73825d959b89630bfa0b6f1fd1efd3006af29f60537a2fdf src/system_stats/mod.rs
050aa831b1012c4aaade63863f433f01bfacf5d3f95787e57975572116a30ab2 src/system_stats/tests.rs
e7ed064a1706de9a7345e41fb8b6288b4467c12d1653e8afba6dc392c7a3a36a src/tinyauth/mod.rs
0e1d87554db55e724f30c41ec4a5c80bdb8fc9c8cba8700ac509864d4abf8871 src/tinyauth/tests.rs
6b955b13ef848e90dae1ec2906176336a775e9511f3688779203d922ff7d23a8 src/tinyauth/mod.rs
3d40febfbaf38ca2725630eed6b33106e353086081ea811107e4ec9d24f7de11 src/tinyauth/tests.rs
df98becda799b6bf4ff5e2fec7d0e0a486e6b7eca6f9f5ad49ffdafd49a4f498 src/tls.rs
2af2164ecdf2b7fd9eef7c6e358dec053dfac9aedaa501dbb75df742d18638b2 src/tls/acme_tests.rs
7425ee6c839f85d792e9251c6b9039e72fb49a5820a2266f751e6f4967a5871c src/tls/certificates.rs
b59a62d723c8683ebd7bb2d2d777d9451064af20b254b98a8c4b9eedfd74bd86 src/tls/external_paths.rs
8d14216afdae33b9dffeb5145d92b8c6fd224c3f8f2baca0cf007b70541daa86 src/tls/external_paths_tests.rs
7b4c0f58b3a1e1791bb3c682a001b78bfdbbdafdbc143db8063b3f22dc168616 src/tls/server_config.rs
7c0c06c3009c726043168ae47254a84642dbabbbce1b2e718544414312ca3e9b src/tls/tests.rs
d625e2e3f669484432ffc0bf7c1aba94aa5bb709dc4d080daa3bfa8004dee9f2 src/tls/types.rs
a535c8526b38de2990da7781e158b9ea70ef8c1f6fae0aac5ce6e9da21d3a309 src/traffic/mod.rs
dc8a83fae47da0d86e6ce0a329cb5274233264df56eebdb56b9bf37cb7be2bb4 src/traffic/tests.rs
65fcac840f8b5f1508213e3f374aa318b450b0f9f90ba827c5ea935b9c60bee7 src/transform/apply.rs
34a06c2dc635e80690961ef61fe84110f9578f55adf89f92a04668783ccb0a98 src/transform/compile.rs
5a16b628af31fced425903a9d4b6ee83e1fe06ba023d574db16467baefe00ad3 src/transform/css.rs
93e0576ac9077e645a72d164bad34cd75fe6032dd0516284d08bddd2fbda4b7c src/tls/certificates.rs
68cc35b3cae43b0321cf667a613310a4b9ec5d8994461d6f3543b1852771bf08 src/tls/external_paths.rs
937b95bb8621ab98d7cad3d223e4dee305350fbd0149b7527396d3bdcc28a68b src/tls/external_paths_tests.rs
f02bb2775e662b2d75810f631ad3da4e0b56a64ae72b36c0edeae37e979b0fb5 src/tls/server_config.rs
94b70c52fdc7bd6e32fba9eed9124d030c692246f6990d61e01b891765275add src/tls/tests.rs
ce51a3280331d67c8406d2c5bc156ac0b492afa58e1f328bcda76db0720a6a9d src/tls/types.rs
a897c48e3528565fef8d4e32f0c3c078adfb68655c80e1af011f44a8bcfac6f6 src/traffic/mod.rs
bd62b4ecdcf831d183c7720766a11703c8612d614e3abf38d2bff28df34710fd src/traffic/tests.rs
e373c7d2630dd209c7a7b7c00c20f80030d83c9e388968656d7323dbd2a11b46 src/transform/apply.rs
ae30a984fa39d5c45a676effbc880c923078ba8120adff1bff10e108350b31b5 src/transform/compile.rs
ffc0740462e5566a96912ab3089f18fc28f1c093ca65eec860fc6ea801fd907d src/transform/css.rs
2b9d5c11d4c02b4a3ad3b9bb1a18c63693f245b854084f5eba16595df47b47f8 src/transform/html/mod.rs
aa99b0d3029ef92c78371cb90e11f2026cce890d1be8e10ebd7fa9bc5cb13287 src/transform/html/tests.rs
047acd7209c1db19709801abbe3ee1076c1fc33f0fe80cc5d9b60669d5f1bae4 src/transform/js/mod.rs
@@ -480,55 +508,56 @@ cf5195ab6bacccae0082da7b3f1bd0382b73fdeb3607f4ae569ca6daab9d10d0 src/transform/
b0d20cf6784bf31457ea5a853e2fa5d85f6452395a78a538392703d97e687a11 src/transform/json/mod.rs
216e24e29ea87be4a124f95f898e155f127fd959d7d5d9b30f8cd0b2b5018f30 src/transform/json/tests.rs
d13fa462d6df516a4cfaee9f4e184044c5b6345e311aeb3d9058efafe54e4c5e src/transform/mod.rs
954b86300c3ea47281acb2f43b0771e838f83ac09eb55e882e42de26aee3128f src/transform/model.rs
1a56c6af6d48d52e2a73036b9bfa3cf16b5fa5b057593821e1229269b7ea4c08 src/transform/model.rs
4b52257e1a3643910b5e7043852d7218230c0021c31215d90d0f8e384f2ae233 src/transform/svg/mod.rs
0f53c64f442390ee20a59087a10f8f9ffb58053263e9aa848ddabfae1a6f3f51 src/transform/svg/tests.rs
3c99de898e4c516a805fec1bc77007a211550b7d7a563933a81ae635d235a2e6 src/transform/tests.rs
753047a1a731596f4c87351873c48f37b39f54e730795ff7d6895c3e2f0d56fb src/transform/tests.rs
8d38f2f7d4ca4dd453cd0e2337614ec704fbd0a36d3e2374cda43f05dfcad663 src/update_check/mod.rs
c42d000d1d576721af3ffe930589388693001b8113554a7658f6dcbc665b5d1b src/update_check/tests.rs
b7ee3629a8fde0972bc1822b764f5fa2c813a760dd5dfb7f1ce643a7db8f9f75 src/upstream.rs
70e6cdf339da86183b4cd07567a4bb99135dccea5b0fb4a6c37c4098ec4762cd src/upstream.rs
fe9d4deb34cea2408ac5a1030315989f29a6a7d078a46ac1b59d12aa07f67878 src/upstream/client.rs
eb513923f8951c8bf30bad3cbac77d34f5ebbc7af3ba69a08a25bae834f8a016 src/upstream/client_tests.rs
8c88d928b823e4b6ba6d852f74664c770d8b3fce32bc76c583dc1f672fe33f14 src/upstream/protocol_tests.rs
80e9952b6a4b2241aff8ee259d286f0381b3c974032adc2c276764d14878fc67 src/upstream/client_tests.rs
56b1e4242f8153391cd9a01fbb3017a2dbda9d02e83da6bbfb39ed44854534b5 src/upstream/protocol_tests.rs
ae880b40197454ea3fbf4d2113984dc372098b2e54b97e87c83cc319de253377 src/upstream/tests.rs
e68599c376d190ae7bdeb154cb3e5a7151feabeb0d688360327dff06574d985d src/user_agent/mod.rs
6d7cb73de7d8c6f3780736480f5f81ef08813f4626fd5ed494aca0552cf214a1 src/user_agent/tests.rs
5c42f1befec267293b6f03eada118a11c90adfb4b32e0134d7f0033c7453c7cc src/user_agent/mod.rs
696a2783a24f7f3d69fb820dc3a772ec49ea46ee017087952a8253363a9b7374 src/user_agent/tests.rs
d61c427c1e9cf457f1232b822d73e846e9f329cfbe61215e7ca061d53762155d src/vhost.rs
54b0c1d2f04e5c654123c3422645eb5a0a9609141cc4a1c6addca2d028392772 src/vhost/error_fallback.rs
9d358be766f767d9e1a42f585edcf7fc1cba8f22e5eaac294a83920425ec03c7 src/vhost/model.rs
e003b935d91791f20d80c59bb46f9bb0820694dcea5d95e65364b53b016284ef src/vhost/redirect.rs
dd6e121445402a2ac8744f0e4f23d879b4356f47c1bb00737bd15ba459661978 src/vhost/tests.rs
c95cc8f4549460917633a12ccb352a00a83c0f004b23c0b9ddecba42eee3ea17 src/vhost/validation.rs
846707452ccf10962f1763be66b8f56c050b0398b48968015fb799dac289c0d6 src/vhost/error_fallback.rs
a8c5ec5095178a0656a001b0d7100e240d1f32b052ea60df37f8681269638ab3 src/vhost/model.rs
fc8af7342a3b147921e9040116af5dfe680b3df910db2416ada18198bbeea7df src/vhost/redirect.rs
2651d7e3e3fadf3de74a75eafe2bbad8d02939551ccc0504c1a092a0b33b0db5 src/vhost/tests.rs
190b2bbdc7ef5181ae4dbe1659f6f0c7ead5d09936b109d0c5be7ea2e7edd681 src/vhost/validation.rs
8d1eff4b24f01d137414ef5419f75dd281879228adb70409645b11b3c78c20e9 src/web_server/config.rs
9744e8c29ae72a4d9fbc9a76166a97f8292c4c312295ea40d1cb699e03d80a9d src/web_server/config/compile.rs
daeb5e05e4129764102b3f04e62b2dde38a8c7739bb475a13bc8c8f6d5844da1 src/web_server/config/model.rs
5ed79fa5b9c53f95ca200d89a9ba927dda816b61fbdf3b852bd3c9670e3d2d4f src/web_server/config/model.rs
a863ad1337fbc353c0f42caea4ac8b3644792e2aa08c37f992b7882b4b8dc6d5 src/web_server/config/validation.rs
86c744216d7132d8294a6e90ba9be5fd871646006313d1c62506d40c81db9b12 src/web_server/fastcgi.rs
002071ef603f1d928a56226b011c76ac5ea2bc55521a0a3fd317c0ced0056c21 src/web_server/fastcgi/protocol.rs
e954308d94848471f6a9d70a50cd2181d17b9c41b2705a3e6c6c0e882a03733b src/web_server/fastcgi/response.rs
b98666353e4af560ab8423d4772d9ad26451dabb1c6d60f5faf547116a2bcd24 src/web_server/filesystem.rs
54aa2dc94df97faabd4859a9dafe6a7cb1ba3337bfef5fa51a4432097a985d88 src/web_server/fastcgi/protocol.rs
fbaf972f9dd2842ec7df2878f0105c9ef4a877817f037f2332c4d5baabf2ead9 src/web_server/fastcgi/response.rs
92d2ab4d7ecb766bbc705f8eba94c1ca33347ffb84b36e0ad0eccfed5915473f src/web_server/filesystem.rs
d4fec3682532b0f2d11a0aefcd024f69fb623a39922ddb6c528a2d2de265347d src/web_server/htaccess.rs
66b846cce61345ea6d182db03e4f8eddc5d4a3cbe87f2046523310f1ea13a5a3 src/web_server/htaccess/model.rs
9c57b7c556984f79404d4afeb9841d0280a198b94c42d887bc6d96b7748a0e67 src/web_server/htaccess/parse.rs
66260d159986d41eab0dfde2cb9f0713fc1cc209c2fb47e9023bd24fd37919e8 src/web_server/htaccess/rewrite.rs
b4646f705e532f682d6152b6827cc7f4f915766ebcb4af89eba9950965673772 src/web_server/htaccess/model.rs
a484113f2c1fea4e98a437acd8a28be6490d7e612f160a8728ceeaadd363f8ee src/web_server/htaccess/parse.rs
6e58e2674940bb0284035856fe9299a0555fee4c566de75e174deb373c3b164f src/web_server/htaccess/rewrite.rs
8ca74d9cc4470045301f70eb4a9fbabd0dbfe74434429148d2230df9f996c06c src/web_server/htaccess/tests.rs
b4a75519d9c7bb4f176126a284ea5da68fe512674200765da0984b245e4278ba src/web_server/metrics.rs
029bc2b277b2fd7528d5c9f5637453ce6d1d3ec2df7e441f6844cf55edef6b5b src/web_server/mod.rs
67ab8bef567452f123c4b9b46a942b39c93f1f765c128edca7ee808ff1c5e304 src/web_server/request.rs
7fe32af33b4d8e079ad724665aa1cfb62f6e98b9406b77168feaac55dac36137 src/web_server/metrics.rs
0f74d1fad9bc20a5f2ea1753381459c072cd8068cb13e7dfbc74e33eb3ace593 src/web_server/mod.rs
e9ba6e09ba2c8f8a33410c219d5ea29214cc53e279a43b17c110d24f17a5a71d src/web_server/request.rs
d98c80663847d6d83073df074ebff65952755415574708abcc153bd8d3adfaa9 src/web_server/response.rs
c69502a28eec224eeded7cad93159aae3e7c136721a19f4542d5f682e055f61b src/web_server/response/file.rs
cbeb022847e64d30e9d63aac92bee00d29281b3602e1a0589f3c978cbc7c2238 src/web_server/response/file_body/mod.rs
4e91d09fa086d388e384d14b347eff8c74c010915ca7c84171e074872323e341 src/web_server/response/file_body/tests.rs
c63a5935bf8e101f1813e029dd62da7d073eb598453ee98aafe9ffa6f85157c2 src/web_server/response/headers.rs
f826d8816bdb9edab966e10991e45335625ef7d7376169cf6fd1e0e5d01bf868 src/web_server/routing.rs
400e1c8aa2fe857d96036a64499fbc3de44c1e3a53811c5b25101fb843359792 src/web_server/security.rs
b38a1c9e648e3606a534ebdf74f6315863a326c6de1ca8edc433abe9c1fed7b4 src/web_server/response/file.rs
4733bb1824dd0a12b8804fb8d00791c3f9fda4c09b41503d5d8079d9607df058 src/web_server/response/file_body/mod.rs
e5f5fbe702f66bfcbcbc1d1f3cd3d89bdbb929bb86aed94868a2a06a4b9e59e3 src/web_server/response/file_body/tests.rs
7da903c1a69be0ae6b85772b3208c518679efa8ed9d3e98adb694483518156b6 src/web_server/response/headers.rs
2665d4eac9a6430d68224874784cb65d9b9d424353af77735caf044401d08cb4 src/web_server/routing.rs
9e584eb4455a18641c2c89f9e9725745c93f32877abf0f0f2fbea9e3d5ef38c3 src/web_server/security.rs
b791762b4ab7a43c3be19df84e523faf3dfb4df055f66d02178f93edb60d2204 src/web_server/serve.rs
df888adf893d0b6b99348d19fa07bd9c2b8fc0ce7f411d06be7411afb4311fe0 src/web_server/tests.rs
714418c86ab241020a561fcf662a8fe324a6a70cc2fd3a313c56d7939c14b9e8 src/web_server/tests.rs
f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json
e24a0f84f1bfc46964df865ac387d651f7b384d781cbc8f3a9a5b0b60f3d69ca tests/vhost_auth_selection.js
eca68a07c2b1e45fdb8f512294f5a432db3ae5a6aec8fb200b47f155cd9a31d6 tools/README.md
7c30c280d0b0a8bf855eeffab583ca34dc161255f7ef6124148aa979c17c26d3 tools/dev/memory_load.py
d1418f8e574a7390fd45a2aa879bdf5720b773162d65e295ab0350506aa5220a tools/dev/mock_auth.py
2d3ff92300b09ce9cecbbaaa9309b4d33160fa4b2291c9952b784e8cfb599ab5 tools/dev/mock_server.py
c711154a9df425a56607bd88f03815ffb408213a31a2b66ed1c1c9a6301a2fc6 tools/legacy/backup.py
89d2b4e60a2f4cb93e8943b74b1b5ca84374b01fa74ea07bec07bdff315ac0e9 update.sh
5421082e24f3794356331157e30b428f3221a0d74c0b5d060bd4d3989e0bcf3a update.sh
56c702111022e1ca2c8f6b72c635c749b05936ffe6387bdb0a202691614cbc96 upgrade.sh
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT"
DIST="$ROOT/dist"
CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$ROOT/target}"
export CARGO_TARGET_DIR
fail() {
printf 'error: %s\n' "$*" >&2
exit 1
}
need() {
command -v "$1" >/dev/null 2>&1 || fail "$1 is required"
}
need cargo
need rustc
need python3
need sha256sum
python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3,10) else 1)' \
|| fail 'Python 3.10+ is required'
[[ -f Cargo.toml ]] || fail 'run this script from the RustProxy project root'
[[ -f Cargo.lock ]] || fail 'Cargo.lock is required for a reproducible release'
[[ -f scripts/preflight.py ]] || fail 'scripts/preflight.py is missing'
VERSION="$(python3 scripts/version.py)"
HOST="$(rustc -vV | sed -n 's/^host: //p')"
[[ -n "$VERSION" ]] || fail 'could not determine project version'
[[ -n "$HOST" ]] || fail 'could not determine Rust host target'
printf 'RustProxy %s release build for %s\n' "$VERSION" "$HOST"
python3 scripts/preflight.py
cargo fmt --all -- --check
cargo test --locked --all-targets
cargo clippy --locked --all-targets -- -D warnings
rm -rf "$CARGO_TARGET_DIR/release"
cargo build --locked --release
BIN="$CARGO_TARGET_DIR/release/rust-proxy"
[[ -x "$BIN" ]] || fail "release binary was not created: $BIN"
ACTUAL_VERSION="$($BIN --version)"
printf 'Built: %s\n' "$ACTUAL_VERSION"
rm -rf "$DIST"
mkdir -p "$DIST"
ARTIFACT="rust-proxy-${VERSION}-${HOST}"
cp "$BIN" "$DIST/$ARTIFACT"
chmod 0755 "$DIST/$ARTIFACT"
sha256sum "$DIST/$ARTIFACT" > "$DIST/$ARTIFACT.sha256"
SOURCE_ZIP="$DIST/RustProxy-${VERSION}-source.zip"
python3 make_zip.py "$SOURCE_ZIP"
sha256sum "$SOURCE_ZIP" > "$SOURCE_ZIP.sha256"
printf '\nRelease ready:\n'
printf ' binary: %s\n' "$DIST/$ARTIFACT"
printf ' checksum: %s\n' "$DIST/$ARTIFACT.sha256"
printf ' source zip: %s\n' "$SOURCE_ZIP"
printf ' checksum: %s\n' "$SOURCE_ZIP.sha256"
+14 -3
View File
@@ -8,7 +8,10 @@ fn panel_styles(
println!("cargo:rerun-if-env-changed=RUST_PROXY_PYTHON");
let helper = root.join("scripts/build/panel_css.py");
println!("cargo:rerun-if-changed={}", helper.display());
println!("cargo:rerun-if-changed={}", root.join("src/admin/web/css").display());
println!(
"cargo:rerun-if-changed={}",
root.join("src/admin/web/css").display()
);
let interpreters = match env::var_os("RUST_PROXY_PYTHON") {
Some(value) if !value.is_empty() => vec![value],
_ => vec!["python3".into(), "python".into()],
@@ -67,9 +70,17 @@ fn swagger_assets(
_ => vec!["python3".into(), "python".into()],
};
for interpreter in interpreters {
match Command::new(&interpreter).arg(&helper).arg("--out-dir").arg(out).status() {
match Command::new(&interpreter)
.arg(&helper)
.arg("--out-dir")
.arg(out)
.status()
{
Ok(status) if status.success() => return Ok(()),
Ok(_) => return Err("Swagger UI asset preparation failed; see the diagnostic above and docs/API_ACCESS.md".into()),
Ok(_) => return Err(
"Swagger UI asset preparation failed; see the diagnostic above and docs/INSTALL.md"
.into(),
),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(error) => return Err(error.into()),
}
+1 -1
View File
@@ -1,7 +1,7 @@
# Example Linux service
`rustproxy.service` is an operator-reviewed deployment example, not an installed or
tested service. With RustProxy 0.34.2, the example includes `ExecReload` so `systemctl reload rustproxy` uses the authenticated CLI soft reload. If using `RUST_PROXY_CONFIG`, configure it in the service EnvironmentFile so both `ExecStart` and `ExecReload` share the same file source. It runs the compiled executable as a dedicated `rustproxy` account,
tested service. The example includes `ExecReload` so `systemctl reload rustproxy` uses the authenticated CLI soft reload. If using `RUST_PROXY_CONFIG`, configure it in the service EnvironmentFile so both `ExecStart` and `ExecReload` share the same file source. It runs the compiled executable as a dedicated `rustproxy` account,
uses the public administration profile shown below and grants only the capability needed
for low-port binding. It does not run the proxy as root or grant Docker socket access.
+1 -2
View File
@@ -28,8 +28,7 @@ install -d -o rustproxy-docker-agent -g docker -m 0750 /var/lib/rustproxy-docker
if [[ ! -f /etc/rustproxy/docker-agent.env ]]; then
install -m 0600 docker-agent.env.example /etc/rustproxy/docker-agent.env
fi
# The Go CLI is the single implementation for both manual and packaged
# provisioning; it preserves existing credentials, certificate and key.
# Provisioning is delegated to the Go CLI, which keeps existing credentials.
/usr/local/bin/rustproxy-docker-agent init
install -m 0644 rustproxy-docker-agent.service /etc/systemd/system/rustproxy-docker-agent.service
systemctl daemon-reload
+68 -4
View File
@@ -2,6 +2,11 @@
Access control can be configured globally, on a Vhost and on selected Custom locations. Policies are **cumulative**: a more specific allow rule does not cancel a denial at a broader level. Changes should be tested with both an allowed and a denied client.
## Global block / allow policy
**Settings → Rules & headers → Global block / allow** applies to public HTTP traffic across all vhosts, redirects and not-found hosts before upstream access. In addition to the existing User-Agent deny patterns, use the global country block/allow list (ISO two-letter codes), client IP/CIDR and allow-only DNS ACL, HTTP method policy, request Content-Type denylist and shared per-IP rate limit. All are disabled by default except previously configured User-Agent blocking. An explicit IP allow does **not** bypass the country policy: both must permit a request. Missing GeoIP country is allowed for country block mode and denied for country allow mode.
Global restrictions add to per-vhost and path rules. Rate-limit requests are counted for each client IP **across** vhosts. Prefer a conservative shared ceiling to avoid blocking shared NAT clients. A global method denial returns 405, the content-type/ACL/UA policy returns 403, and the shared rate limiter returns 429 with `Retry-After`. Admin and TCP/UDP stream listeners are outside this HTTP scope. For global response headers, use the adjacent **Security headers** subtab; see [Security](SECURITY.md).
## IP, country and DDNS rules
- **IP/CIDR ACL:** allow or deny IPv4/IPv6 networks. Use reusable ACL profiles for settings shared by several domains. Both ordinary and Docker Vhosts use a searchable multi-select with selected badges and a preview of IP/CIDR rules, country restrictions and the default policy; the selection changes presentation only, not ACL evaluation. A Vhost can select up to eight profiles (`acl_profile_ids`, or one legacy `acl_profile_id`); Docker assignments can override or inherit the agent's profile selection.
@@ -13,7 +18,7 @@ Access control can be configured globally, on a Vhost and on selected Custom loc
Default deny plus explicit allow exceptions is usually safer for private admin applications. Apply rules to a staging client before enforcing them on a live business-critical service.
In the **Vhost → Security** editor, Traffic policy groups reusable ACL profiles and the separate **inline Vhost IP/CIDR access list** together, followed by HTTP methods, request Content-Type filtering, per-IP rates, country policy, country-rate and User-Agent-rate controls. The inline list is not a second version of an assigned profile: both layers remain independently enforced; an allow in one does not cancel a deny in the other. The **Vhost → Authentication** tab is for Basic Auth and Tinyauth only. HSTS, security headers and User-Agent blocking are separate Security groups. **Custom locations → Access** separates its ACL profile, HTTP method, country and inline IP/CIDR fields. These are only layout changes; restriction scopes still combine as described above.
In the **Vhost → Security** editor, Traffic policy groups reusable ACL profiles and the separate **inline Vhost IP/CIDR access list** together, followed by HTTP methods, request Content-Type filtering, per-IP rates, country policy, country-rate and User-Agent-rate controls. The inline list is not a second version of an assigned profile: both layers remain independently enforced; an allow in one does not cancel a deny in the other. The **Vhost → Authentication** tab first selects an identity provider, then a compatible integration method (Forward Auth, OIDC or Basic). A Vhost still uses only one active method; existing JSON settings are retained for compatibility. HSTS, security headers and User-Agent blocking are separate Security groups. **Custom locations → Access** separates its ACL profile, HTTP method, country and inline IP/CIDR fields. These are only layout changes; restriction scopes still combine as described above.
## Content-Type request denylist
@@ -23,11 +28,70 @@ Ordinary Vhosts can reject selected MIME types under **Vhosts → Security → R
Method policies allow, block or leave methods unrestricted. A denied method returns `405`, while typical ACL/country denials return `403`. Per-client rate limits can configure request count, window and burst with optional country overrides; exceeding the limit returns `429` with `Retry-After`. Limits are application controls, not a replacement for upstream DDoS protection.
## Basic Auth and Tinyauth
## Identity providers and authentication integration methods
Choose **Identity provider → Integration method** in **Vhost → Authentication**. Tinyauth, Authelia and authentik support both **Forward Auth** and **OIDC** when the installed identity service has the relevant endpoint enabled. Pocket ID and other generic OpenID Connect issuers use **OIDC** only. Built-in HTTP Basic remains available. Forward Auth uses the vendor-specific settings object and transport, while OIDC uses the shared `oidc` settings and an exact application callback. The protected app does not need to implement OIDC.
New Vhosts store an explicit `authentication` choice; old Vhosts with only `tinyauth.enabled`, `authelia.enabled`, `authentik.enabled`, `basic_auth.enabled` or `oidc.enabled` still load. An older OIDC configuration without a named provider appears as **Other OIDC provider**, retaining its issuer and client settings. The selected pair must agree with the enabled settings (e.g. `authentication: {provider: authelia, method: oidc}` requires `oidc.enabled: true` and `authelia.enabled: false`). Conflicting choices are rejected, not silently downgraded to an unprotected Vhost.
### Existing integrations
- **Basic Auth:** protect selected Vhosts with configured credentials. Use HTTPS so credentials are not sent over plaintext.
- **Tinyauth:** add a verification URL and an HTTPS login URL. RustProxy makes a bounded, body-free authorization request and can pass specifically allowed identity headers upstream. An origin URL can use the service's conventional `/api/auth/nginx` path. Tinyauth has its own timeout and private-backend policy.
- The two authentication integrations should not be enabled simultaneously for the same Vhost. Protected responses bypass unsafe shared caching. Confirm that login redirections and WebSockets behave as expected.
- **Tinyauth:** add a verification URL and an HTTPS login URL. RustProxy makes a bounded, body-free authorization request and can pass specifically allowed identity headers upstream. An origin URL automatically uses `/api/auth/nginx` (or set that path explicitly). This Nginx auth-request integration sends `X-Original-URL` and `X-Original-Method`, with a validated `X-Forwarded-For` and `X-Real-IP` for Tinyauth IP ACLs. It does **not** send `X-Forwarded-Host` or `X-Forwarded-URI` to the verifier, avoiding the `conflicting auth module headers` rejection introduced in Tinyauth 5.2.0 while keeping compatibility with earlier versions that implement the Nginx endpoint. Tinyauth has its own timeout and private-backend policy.
- **Provider-neutral gateway error pages:** One embedded `src/admin/web/error_auth_provider.html` template renders a self-contained HTML page for **Tinyauth, Authelia, authentik, and future providers**, using only the safe provider display name and the failure category. It replaces the three vendor-specific `error_tinyauth_*.html` source files while preserving the familiar Tinyauth response titles and statuses. For example: `502 Tinyauth connection error` if its HTTP client cannot reach Tinyauth (DNS/TCP/TLS/transport), `504 Tinyauth timeout` if the auth deadline expires, or `502 Tinyauth verification error` for unexpected verifier status or invalid/oversized data. The page always retains the HTTP error status, `Content-Type: text/html; charset=utf-8`, `Cache-Control: private, no-store` and `X-Content-Type-Options: nosniff`. Backend URLs, protocol errors and identity data are not shown to visitors. All of these cases **deny access** to the protected origin. HTTP `401`/`403` and configured login redirects remain unchanged. These auth-specific pages take precedence over custom Vhost/general HTTP error pages so the failure remains distinguishable. The generic failure marker also protects Basic Auth denials and provider login redirects from being replaced by Vhost pages. HTTPS-verifier transport errors are classified as connection failures, truncated/oversized/invalid verifier responses as verification failures, and verification deadlines as timeouts.
- Point the verifier URL **directly** at the private Tinyauth service (for example `http://tinyauth:3000/api/auth/nginx`), rather than routing it through a second public proxy that might inject conflicting headers. Do not disable Tinyauth's header-spoofing protections. Confirm that Tinyauth trusts only the actual RustProxy address/network when enabling Tinyauth IP-based access controls (`TINYAUTH_AUTH_TRUSTEDPROXIES`).
- **Authelia:** select **Authelia → Forward Auth** under **Vhost → Authentication**. Enter the internal service origin (for example `http://authelia:9091`) and the public HTTPS login portal (for example `https://auth.example.com`); RustProxy calls `/api/authz/forward-auth`. It forwards trusted `X-Forwarded-*`, `X-Original-URL` and `X-Original-Method` metadata, verifies the provider result, returns provider-approved `302/303` login redirects only to the configured portal origin, and forwards configured `Remote-*` identity headers after stripping client-forged values. A `200` verifier response alone grants access. `401`/`403` deny; transport failures return 502 and timeouts 504. The Authelia `session.cookies` domains and `authelia_url` are defined in Authelia itself: protecting **unrelated registered domains** generally requires a login URL/cookie configuration within each domain, not one cookie from a single unrelated site.
- **authentik:** select **authentik → Forward Auth** and enter the private outpost origin (for example `http://authentik:9000`), not an `/if/flow` login URL. RustProxy calls `/outpost.goauthentik.io/auth/nginx`, forwards verified `X-Authentik-*` identity headers, and responds to a `401` with a same-origin login start at `/outpost.goauthentik.io/start?rd=...`. RustProxy reserves `/outpost.goauthentik.io` (and descendants) on the protected application hostname and forwards them to the configured outpost **without checking authentication again**, after ordinary global/Vhost IP/country/rate rules. No manual Custom Location is needed for that path. In authentik create a separate **Proxy Provider → Forward auth (single application)** for each protected external application URL, assign it to the serving outpost, then enable that Vhost's integration. Multiple unrelated domains can share the same authentik installation; they do **not** share a browser cookie across domains.
- **OIDC (opt-in):** choose **Authelia → OIDC**, **authentik → OIDC**, **Pocket ID → OIDC** or **Other OIDC provider → OIDC** for the protected Vhost. This switches to the shared OIDC implementation instead of the vendor's Forward Auth adapter. Provide the exact HTTPS **Issuer**, Client ID and confidential Client Secret, a strong unique Session Secret (`openssl rand -hex 32`), and `https://YOUR-EXACT-APP-HOST/_rustproxy/oidc/callback` as the **Redirect URL**. Register that exact callback as an allowed redirect URI in the identity provider. The issuer can be `https://auth.example.com.cc` while the protected hostname is `app.linuxiarz.pl`; browser cookies do not cross between them. The issuer's discovery `/.well-known/openid-configuration`, authorization, token and JWKS endpoints must resolve from RustProxy and use the issuer's HTTPS origin with trusted TLS. Use split DNS or the private-address opt-in if the issuer resolves to internal addresses; do not disable HTTPS certificate validation. OIDC currently requires a confidential client using `client_secret_basic`, authorization-code flow, and an RS256 or ES256 signing key. Public PKCE-only clients are not supported.
- **OIDC session and identity:** RustProxy generates short-lived transaction state with S256 PKCE and nonce; requires a signed ID token with exact issuer, audience, validity and nonce; and encrypts a separate `__Host-` session cookie for the application host (default 3600 seconds, max 86400). The session is **not continuously revalidated with the issuer**; removing a user at the issuer does not invalidate an already-issued RustProxy session before its expiry or local logout (`/_rustproxy/oidc/logout`). Successful login injects `Remote-Sub`, `Remote-Issuer`, `Remote-User` and optional `Remote-Email`, `Remote-Name`, `Remote-Groups`; use `(Remote-Issuer, Remote-Sub)` for stable account identity and do not auto-link accounts solely by email or username. The backend must not be directly accessible from untrusted clients. Non-GET/HEAD requests without a valid session receive 401, and authentication endpoint failures return 502/504. The provider URL must not itself be protected by this Vhost's OIDC integration. For Authelia, enabling its Forward Auth portal alone is insufficient: also configure Authelia as an OIDC issuer under `identity_providers.oidc`, with at least one signing key, and register a confidential client for every callback URL (or separate clients per application). OIDC does not require any special support from the application being proxied, only from the remote identity provider.
- **Adding another provider:** Register the vendor and supported methods in `auth_provider::choice::IdentityProvider` and in the Vhost editor. OIDC-compatible vendors reuse the shared `oidc` integration and may need no new Rust transport adapter. New Forward Auth adapters belong in `src/<provider>/mod.rs` with tests and may reuse `forward_auth::Adapter`; their compiled implementation implements the object-safe `auth_provider::Provider` trait. Runtime request handling continues through one `CompiledFrontend.auth_provider` slot and provider-neutral `error_auth_provider.html`. Preserve legacy Vhost configuration keys; add new providers without copying OIDC protocol code or HTML error templates.
- **Per-provider security:** The verifier/outpost connection uses TLS certificate verification for HTTPS with optional additional CA, explicit private-backend opt-in and the existing safe-address/local-listener restrictions. Only allowlisted response identity fields and bounded session cookies reach applications/browsers. Inaccessible, malformed or timed-out authorization services **deny** origin access. Both Authelia and authentik are disabled by default; old Tinyauth settings are retained when inactive. Only one integration method can be enabled for one Vhost. Protected responses bypass shared caching. Confirm both allowed/denied requests, logins, sessions, redirects and WebSockets with the actual provider before rollout.
### Minimal Vhost configuration examples (inside a Vhost object)
Authelia with Forward Auth:
```json
"authentication": {"provider": "authelia", "method": "forward_auth"},
"authelia": {
"enabled": true,
"url": "http://authelia.internal:9091",
"login_url": "https://auth.example.com",
"allow_private": true
}
```
authentik with Forward Auth (same URL for the outpost check and reserved public path):
```json
"authentication": {"provider": "authentik", "method": "forward_auth"},
"authentik": {
"enabled": true,
"url": "http://authentik.internal:9000",
"allow_private": true
}
```
For central login from an unrelated application domain, select **Authelia → OIDC** and use the following Vhost fragment (also register this callback with the issuer):
```json
"authentication": {"provider": "authelia", "method": "oidc"},
"oidc": {
"enabled": true,
"issuer_url": "https://auth.example.com.cc",
"backend_url": "http://10.0.0.20:9091",
"redirect_url": "https://app.linuxiarz.pl/_rustproxy/oidc/callback",
"client_id": "app-client-id",
"client_secret": "CHANGE_ME_CLIENT_SECRET",
"session_secret": "CHANGE_ME_RANDOM_SECRET_AT_LEAST_32_BYTES",
"scopes": "openid profile email",
"allow_private": true
}
```
The `client_secret` and `session_secret` are illustrative placeholders and must be replaced with generated values. Disable other authentication settings for this Vhost. Set `basic_auth.enabled`, `tinyauth.enabled`, and the other provider's `enabled` to `false` for these examples. These are fragments, not whole Vhost documents. Protect the public provider portal with a separate Vhost **without** forward auth to avoid recursive redirects.
Do not use a User-Agent blocklist as the primary access control: HTTP clients can forge their User-Agent freely.
+86 -3
View File
@@ -16,7 +16,8 @@ The current bundled `src/admin/openapi.json` declares these operations (among ot
| `POST /api/config/reload` | Revalidate and hot-reload manually changed `--config` JSON; revision-aware. |
| `GET/POST /api/api-tokens` · `DELETE /api/api-tokens/{id}` | List/generate/revoke general RO/RW API tokens (browser-admin-only). |
| `GET/POST/DELETE /api/prometheus-token` | Inspect/generate/rotate/revoke a standalone Prometheus scrape token (browser-admin-only). |
| `PUT /api/config/{section}` | Update one supported configuration section. |
| `GET /api/config/{section}` · `PUT /api/config/{section}` | Read or atomically replace one of 23 supported configuration sections. |
| `POST /api/config/geoip/update` | Run a GeoIP download/check/reload immediately using already-saved GeoIP settings; does not change revision. |
| `GET /api/config/history` · `POST /api/config/history/restore` | Review/restore retained configuration snapshots. |
| `GET /api/status` · `GET /api/backends` | Operational status and backend observations. |
| `GET /api/cache` · `POST /api/cache/purge` · `POST /api/cache/invalidate` | Cache observations and authenticated invalidation. |
@@ -36,13 +37,95 @@ Some paths have additional DELETE/PATCH operations, job-status resources, query
Certificate inspection accepts `{ "cert_file": "filename.pem" }` for a managed file or an absolute external path and returns `{ "cert_file": "...", "covered_hosts": ["example.com"] }`. It applies the existing certificate-file trust and size checks and reads no private key. It makes no configuration or permission changes. This POST requires the signed-in administrator's normal Origin, request-marker and CSRF checks; general RO/RW API tokens and certificate-only credentials cannot call it.
## Access and authentication
The administration listener is HTTP on loopback (`127.0.0.1:9090`) by default and is separate from the public Vhost listeners. No administrator password is bundled: complete first-time setup with the local CLI (`admin setup-token` or `admin init`). For remote access prefer an SSH tunnel; for direct access enable administration HTTPS with a trusted certificate and restrict the network before binding outside loopback.
| Credential | Scope |
|---|---|
| Browser session | Full administration. HttpOnly, SameSite=Strict cookie (Secure on HTTPS), absolute lifetime 7–30 days. Writes require matching Origin, `X-CSRF-Token` and `X-RustProxy-Request: 1`. |
| General API token (`ro` / `rw`) | Created in **Settings → Security → API tokens**. Displayed once, stored as a hash, bound to a mandatory IP/CIDR allowlist. Sent as `Authorization: Bearer <token>`. `ro` reads resources and sections; `rw` also writes configuration sections and selected operations. Cannot manage authentication, backups, tokens or certificate credentials. |
| Certificate API key | Key ID + Bearer token limited to specific certificate IDs and actions (reload by default, deploy opt-in). |
| Prometheus token | Scrape-only credential for the separate exporter; see [Monitoring](MONITORING.md). |
IP checks use the transport peer, never forwarded headers. Outside loopback, Bearer credentials require administration HTTPS.
### `/docs` Reader and Swagger
The authenticated `/docs` page has two tabs: **Reader** with the guides embedded at build time, and **API** with the bundled OpenAPI schema rendered by Swagger UI. All assets are served locally; no CDN is used. Swagger uses the current session, executes read calls directly and asks for confirmation before any write.
### Publishing the panel behind a reverse proxy
This is a special-risk topology. Configure a fixed HTTPS public origin, an allowed hostname and a trusted forwarding boundary. Ordinary public Vhosts never forward administration cookies to an upstream. Re-test CSRF/Origin validation and cookie security after any topology change.
## Authentication and safe writes
- Ordinary API calls require an administrator browser/session cookie on the admin listener, or a scoped general API Bearer token for explicitly allowlisted API methods/paths. Logging in also provides CSRF context. Writes require correct Host/Origin, `X-CSRF-Token` and `X-RustProxy-Request: 1` as enforced by the server.
- Ordinary API calls require an administrator browser/session cookie on the admin listener, or a scoped general API Bearer token for explicitly allowlisted API methods/paths. Browser-session writes require the matching Origin, `X-CSRF-Token` and `X-RustProxy-Request: 1`, in addition to Host validation. Scoped Bearer-token requests are exempt from browser CSRF requirements; the server enforces the token permission, source IP allowlist and HTTPS for non-loopback access instead.
- The Swagger **API** tab uses the signed-in session. Read calls can be executed; mutations require explicit confirmation. It cannot bypass server-side authorization.
- The serialized configuration limit is 2 MiB across all sections; JSON request bodies allow a bounded 64 KiB formatting overhead. Saves over the limit are rejected without applying partial changes.
- Configuration writes are revisioned (`expected_revision`) and rejected if based on a stale save. `GET /api/config` returns `{revision, config, storage}`; `storage` is runtime metadata and must not be copied inside the editable `config` object or included as a write parameter. PUT and history-restore responses remain `{revision, config}`. Prefer section updates over replacing the entire document when practical.
- Each supported section has the same **GET** and **PUT** value shape. `GET /api/config/cache` returns `{revision, section: "cache", value: {…}}`. `PUT /api/config/cache` accepts `{"expected_revision": 12, "value": {…}}`, using the **entire** section value from a fresh GET after modification. The response is the full updated `{revision, config}` envelope (not a section view). Even changes to different sections must use the latest global revision: concurrent stale writes return 409. This is replacement, not JSON Merge Patch.
- Section boundaries: `logging` uses `{"logging": {…}, "error_log": {…}}`; `response` uses `{"server_header": {…}, "diagnostic_headers": {…}, "compression": {…}}`; `global-security` uses a **single** `GlobalSecurity` object including `user_agent_blocking`. For this section, omitting `user_agent_blocking` is an invalid PUT rather than silently clearing the denylist. Independent `user-agent-blocking` and `error_log` section endpoints do not exist. The prior logging-only `PUT /api/config/logging` payload is no longer accepted: read the full logging section and send the paired value.
- The section names use kebab-case (e.g. `not-found-hosts`, `docker-agents`, `global-security`) even when the matching configuration JSON key is snake_case (`not_found_hosts`, `docker_agents`, `global_security`). GeoIP configuration lives at `GET/PUT /api/config/geoip` (serialized from `config.geoip_updates`). After saving, `POST /api/config/geoip/update` triggers a source refresh. The old standalone `/api/geoip/update` no longer exists. This action changes loaded database files, **not** the config revision. A read-only API token cannot execute it; a read/write token can.
- Full-document config imports remain backward compatible with older root-level `user_agent_blocking`; it is migrated into `global_security.user_agent_blocking` when validated. If both positions contain conflicting policies, the import is rejected. Canonical GET and saved config use the nested field. Per-vhost `user_agent_blocking` is unchanged. RO General API tokens may GET sections, RW tokens may PUT sections; the same existing authentication, Host/Origin, revision and audit checks apply.
- `/docs`, API schemas, configuration, logs, certificates and most observations are **not anonymous**. Keep the listener restricted and use HTTPS or a tunnel for remote access.
### Available configuration sections
The section name in the URL and its corresponding JSON member in a full configuration are:
| Section path (`/api/config/…`) | JSON member(s) in `config` |
|---|---|
| `listeners` | `listeners` |
| `docker-agents` | `docker_agents` |
| `http2` | `http2` |
| `pages` | `pages` |
| `cache` | `cache` |
| `transform` | `transform` |
| `forwarding` | `forwarding` |
| `headers` | `headers` |
| `prometheus` | `prometheus` |
| `geoip` | `geoip_updates` |
| `administration` | `administration` |
| `tls` | `tls` |
| `vhosts` | `vhosts` |
| `web-services` | `web_services` |
| `php-fpm-profiles` | `php_fpm_profiles` |
| `redirects` | `redirects` |
| `not-found-hosts` | `not_found_hosts` |
| `streams` | `streams` |
| `acl-profiles` | `acl_profiles` |
| `host-policies` | `host_policies` |
| `response` | `server_header`, `diagnostic_headers`, `compression` |
| `logging` | `logging`, `error_log` |
| `global-security` | `global_security`, including `user_agent_blocking` |
Every GET returns a `value` object or array whose shape can be sent unchanged to the matching PUT. For the three grouped sections the `value` contains exactly the named members; for the other sections it is the serialized value of the matching configuration member. Saving does not alter unrelated sections. An ordinary Vhost may configure mutually exclusive `oidc`, `authelia` and `authentik` objects alongside existing `tinyauth`/`basic_auth` settings; every optional provider defaults to disabled. OIDC uses `/_rustproxy/oidc/callback` on an exact Vhost HTTPS hostname and requires issuer/client/session secrets. See [Access](ACCESS.md).
### Complete section-update example
```http
GET /api/config/global-security
```
```json
{
"revision": 7,
"section": "global-security",
"value": {
"user_agent_blocking": {"enabled": true, "patterns": ["bad-bot"]},
"country_access": {"mode": "off", "countries": []},
"acl": null,
"method_policy": {"mode": "off", "methods": []},
"content_type_policy": {"enabled": false, "blocked": []},
"rate_limit": {"enabled": false, "requests": 100, "window_seconds": 60, "burst": 20},
"security_headers": {"hsts": {"enabled": false, "max_age_seconds": 31536000, "include_subdomains": false, "preload": false}, "nosniff": false, "frame_options": "preserve", "referrer_policy": null, "content_security_policy": null, "permissions_policy": null}
}
}
```
Use the actual **complete** `value` returned from GET as the PUT `value` after editing it; the example is illustrative, not a substitute for fetching the active defaults. Then send `PUT /api/config/global-security` with `{"expected_revision":7,"value":{...}}`. A successful PUT returns the complete new configuration envelope; HTTP **409** requires re-reading the section and applying the intended changes to the newer revision. GET never writes. The UI and API use the same validation and transaction path.
## Local CLI alternatives
The included `rust-proxy` program supports configuration and backup operations without turning browser sessions into automation tokens:
@@ -68,4 +151,4 @@ Authorization: Bearer <secret>
X-RustProxy-Certificate-Key: <key-id>
```
The credential can be used only for allowed certificate reload/deploy operations, not configuration, users, metrics or secret management. Non-loopback use requires the appropriate administration HTTPS security boundary. Rotate broad or legacy credentials. See [API access](API_ACCESS.md) and [Certificates](ACME_PROVIDERS.md).
The credential can be used only for allowed certificate reload/deploy operations, not configuration, users, metrics or secret management. Non-loopback use requires the appropriate administration HTTPS security boundary. Rotate broad or legacy credentials. See [Certificates](CERTIFICATES.md).
-35
View File
@@ -1,35 +0,0 @@
# Administration authentication and API access
## Default exposure and setup
The administration listener uses **HTTP loopback (`127.0.0.1:9090`) by default** and is separate from normal Vhost listeners. No administrator password is bundled. Use the local CLI `admin setup-token` or `admin init` to complete first-time setup. For access from another workstation, prefer an SSH tunnel; for direct remote operation, enable **administration HTTPS**, configure a trusted certificate and narrow network access before binding outside loopback.
## Session security
Administrator sessions use an HttpOnly, SameSite=Strict cookie, with Secure on HTTPS. Expiry is bounded by the configured absolute lifetime (7–30 days, default 30). Logout, password changes and revoke-all terminate sessions. API mutations are validated for Host/Origin, CSRF token and request marker; a cookie copied into automation is not an approved general-purpose API token.
Unauthenticated endpoints intentionally expose only minimal setup/sign-in and health behavior. Configuration, operational status, dashboard assets, file logs, the API specification and the documentation Reader require authentication. Admin responses use no-store and security headers, and the browser does not expose the full application state before sign-in.
## `/docs` Reader and Swagger
The authenticated documentation page has two tabs: **Reader**, embedding the registered Markdown guides at compile time, and **API**, displaying the bundled OpenAPI schema. Both are local assets; no runtime CDN access is required for viewing the reference. A document edit affects the shipped reader **only after rebuilding** RustProxy.
Swagger uses the current signed-in session, not a new broad credential. It requires explicit confirmation for write operations. Treat the OpenAPI schema as a request guide and RustProxy's field/authorization validation as the final authority.
## Publishing the panel behind a reverse proxy
This is a **special-risk topology**, not the default. Configure a fixed HTTPS public origin/allowed hostname and a trusted forwarding boundary. RustProxy has an explicit narrowly constrained local-listener exception, but a normal public Vhost must not pass administration cookies to an ordinary upstream. Forwarded Host/Proto values are trusted only when the direct peer satisfies the configured trust rules. Test CSRF/Origin validation and cookie security after a topology change.
## General administration API tokens (0.34.1 DEV)
Settings → **Security → API tokens (RO/RW)** generates a one-time-displayed opaque Bearer credential, with `ro` (read) or `rw` (read/write) permission and a **mandatory allowlist of client IPs or CIDRs**. The token is stored only as a hash; save it securely at creation or generate a replacement. Deleting a token revokes it immediately. The existing browser session workflow is unchanged; this is not a browser login/password substitute.
Send `Authorization: Bearer <token>` to the administration listener over HTTPS outside loopback. IP checks use the transport peer, not spoofable forwarded headers. Only the narrowly allowlisted `/api/*` resource routes are usable: RO is read-only, RW adds selected configuration and operational operations. Tokens cannot access authentication, backups, documentation, other token creation/revocation, certificate API management, or unrestricted administrator endpoints. The separate certificate-only API credentials work independently. The **Settings → Administration** page includes a direct link to token management.
## Certificate-only credentials
Certificate API Key ID + Bearer credentials are limited by certificate ID and by allowed action (reload by default; deploy opt-in). They do not grant login or administrative configuration access. Revocation/rotation are administrator-only actions. Keep tokens out of logs and pass them over HTTPS outside loopback.
## Offline build assets
Swagger JavaScript/stylesheets are prepared by the build and embedded behind authentication. If compilation fails during Swagger asset preparation, inspect `build.rs` and `scripts/build/swagger_assets.py` and ensure the required checked/downloaded asset inputs are available. Do not replace these authenticated assets with an unaudited public CDN workaround.
+6 -5
View File
@@ -2,6 +2,9 @@
RustProxy provides an optional shared HTTP response cache with a bounded in-memory **HOT** tier and optional persistent-disk **COLD** tier. Both the global cache and a Vhost's effective policy must permit caching. **COLD storage is disabled by default**, and newly created Vhosts do not automatically cache dynamic content.
### Cache controls in Settings
The static-extension cookie-bypass warning sits with its extension list, separate from **Cache response headers** options. Only enable this optimization for files whose bytes do not depend on the user's session. `Cookie` can then be omitted from the shared cache key for the configured extensions; responses with `Set-Cookie`, `private`, `no-store` or `Vary: Cookie` remain uncacheable.
## Enable caching safely
1. Open **Settings → Cache**, enable the global cache and choose memory limits. Enable COLD only if you want durable disk storage and have enough private disk capacity.
@@ -10,8 +13,7 @@ RustProxy provides an optional shared HTTP response cache with a bounded in-memo
4. Optionally enable HIT-only diagnostics. Send repeated identical requests using `curl`, then inspect **Cache → Domains / Stored objects** for `MISS → HIT`.
5. Review **Varnish-compatible cache policy**, enabled by default, then check TTL, revalidation and invalidation after updating the origin's content or configuration.
## Varnish-compatible cache policy (0.34.7)
## Varnish-compatible cache policy
**Settings → Cache → Varnish-compatible cache policy** (`cache.varnish_compatible`, **enabled by default**) avoids needless cache replacement on browser hard reloads. On a fresh object it ignores only the client's `Cache-Control: no-cache`, `max-age=0` and `Pragma: no-cache` for cache **lookup/freshness**. It does not remove or modify these request headers when fetching from the origin, and it does not alter cache identity or request conditionals.
Set **Default grace (seconds)** in Settings → Cache (default **60**, range 0..604800). This affects only eligible objects in compatibility mode and never overwrites explicit origin `stale-while-revalidate`, including `=0`. Use the adjacent **i** information icon for the short operator explanation (PL/EN). Disabling the option restores the original strict client reload semantics. Changing modes invalidates currently stored representations across HOT/COLD so they are refilled under one consistent policy; no process restart is needed. Existing JSON configurations with no field inherit `true`. The setting also applies to eligible resources of the embedded Web Server.
@@ -80,8 +82,7 @@ done
Look for cache reason labels including `request_cookie`, `request_authorization`, `response_cache_control`, `route_disabled` and `cache_hit` when diagnostic headers are enabled. A `MISS` alone does not prove the object was admitted to HOT/COLD.
## Cache response headers and upstream diagnostics (0.34.12)
## Cache response headers and upstream diagnostics
**RFC 9211:** `Cache-Status` is a structured list of cache hops, ordered from the origin-side cache toward the client. RustProxy **does not replace or suppress** an upstream `Cache-Status`. When **Settings → Cache → Expose Cache-Status** is enabled and RustProxy makes a cache decision, it appends its own `RustProxy` member to the existing list; multiple HTTP header fields are an equivalent wire representation of the same list. There is no `backend=hit/miss` parameter: the upstream's own members already describe its cache decisions.
For example, with an upstream HIT and a RustProxy HIT:
@@ -130,7 +131,7 @@ The initial request can show `RustProxy; fwd=miss`; an eligible repeat should sh
Authenticated Cache page/API operations can purge selected entries, domains or larger scopes. The optional public HTTP `PURGE`/`BAN` interface is **disabled by default**. Enable it only by listing trusted client IPs/CIDRs under **Settings → Cache → HTTP BAN / PURGE IP allowlist**; operations are additionally subject to Vhost binding and normal access controls. `PURGE` addresses an exact URL; `BAN` applies the supported bounded matching rules. Never allow untrusted Internet clients to purge all tenants' objects.
In 0.34.0, changing operational Vhost settings is intended to preserve compatible HOT/COLD objects; content-affecting changes invalidate affected scopes and COLD carries an additional fingerprint against stale resurrection. **This new behavior still needs release-level verification**, so validate cache after configuration edits and during upgrades. Earlier persistent cache entries may need refilling following format/compatibility transitions.
Changing operational Vhost settings is intended to preserve compatible HOT/COLD objects; content-affecting changes invalidate affected scopes and COLD carries an additional fingerprint against stale resurrection. **This new behavior still needs release-level verification**, so validate cache after configuration edits and during upgrades. Earlier persistent cache entries may need refilling following format/compatibility transitions.
## Troubleshooting
@@ -11,6 +11,9 @@ ancestor even though it can host a Web Server document root. See
[Installation: external paths](INSTALL.md#linux-debrpm-read-only-access-to-external-paths)
for scoped ACLs and Certbot renewal handling.
### Certificate file permission command preview
The permissions explanation now appears next to generated read/traversal ACL commands, rather than alongside the path inputs. The **Copy commands** action is offered only after a valid path selection produces a command preview. These commands grant read access to the certificate/private key and traversal of parent directories; they do not grant write permission.
## Supported certificate methods
| Mode | Requirements |
@@ -33,7 +36,7 @@ HTTP-01 cannot create wildcard certificates; use DNS-01. A staging certificate i
5. Wait for the job result; inspect errors under Certificates and the service logs.
6. Assign the issued/imported certificate to the intended Vhost/listener and verify a real TLS handshake with the public hostname.
In 0.34.1 DEV, expand a certificate in the Certificates inventory and use **Assign Vhosts** to check/uncheck multiple Vhosts before applying one revisioned Vhost-section save. This updates Vhost certificate mappings, not the PEM files themselves. Verify frontend listener and certificate hostname coverage separately.
Expand a certificate in the Certificates inventory and use **Assign Vhosts** to check/uncheck multiple Vhosts before applying one revisioned Vhost-section save. This updates Vhost certificate mappings, not the PEM files themselves. Verify frontend listener and certificate hostname coverage separately.
The reserved HTTP-01 challenge path is available only during eligible issuance/renewal work. It is not a generic public file server and does not bypass authentication for unrelated URLs.
+24 -2
View File
@@ -2,10 +2,12 @@
Most settings are managed in the authenticated panel. Save changes after validation; use **Configuration history** to compare or restore an earlier revision. Some listeners are process-owned and require restart to change. Configuration updates are not a replacement for encrypted full backups.
> Cache compatibility: **Settings → Cache → Varnish-compatible cache policy** (`cache.varnish_compatible`) defaults to `true` in 0.34.7, also for configs without the field. Turning it off restores legacy client freshness handling. Implicit grace defaults to **60 seconds** (`cache.varnish_grace_seconds`, editable in Settings → Cache). See [HTTP cache](CACHE.md#varnish-compatible-cache-policy-0347) for semantics and safety exceptions.
> Cache compatibility: **Settings → Cache → Varnish-compatible cache policy** (`cache.varnish_compatible`) defaults to `true`, also for configurations without the field. Turning it off restores legacy client freshness handling. Implicit grace defaults to **60 seconds** (`cache.varnish_grace_seconds`, editable in Settings → Cache). See [HTTP cache](CACHE.md#varnish-compatible-cache-policy) for semantics and safety exceptions.
## Configuration storage: SQLite or JSON file
The serialized configuration has a 2 MiB limit across all sections, including Vhosts. The administration API accepts compact configuration updates and bounded JSON formatting overhead; individual Vhost and backend limits still apply.
**Settings → Manual config** shows the active storage mode and path, sourced from the read-only `storage` member of the authenticated `GET /api/config` response (`revision`, `config`, `storage`). With no `--config`, the authoritative configuration is in `<data-dir>/config.sqlite3` (normally `/var/lib/rustproxy/config.sqlite3`). With `--config`, that JSON file is authoritative, while SQLite still stores revision history, accounts and audit records. Switching modes requires a service restart; the panel does not change the startup argument.
### Optional JSON configuration source
@@ -62,9 +64,29 @@ ACLs support IP/CIDR and **allow-only** DNS/DDNS hostname rules, plus reusable p
Global response headers, per-Vhost rules and Web Service-specific headers are distinct scopes. Security and proxy-protocol headers may be protected from rewriting. Compression supports eligible response encodings; availability is still constrained by global limits and the requesting client.
### Global HTTP block / allow and security headers
Under **Settings → Rules & headers**, choose **Global block / allow** to configure rules applying to every public HTTP vhost, redirect and 404 host. Independent controls cover **country block/allow**, global **User-Agent deny patterns**, **IP/CIDR and allow-only DNS ACL**, **HTTP method allow/block**, **request Content-Type denylist** and a **shared per-client-IP rate limit**. Disabled policies are neutral. If country allow mode is selected, an unknown GeoIP country is denied; block mode permits unknown countries. Global IP and country checks are cumulative, as are global rules and more restrictive per-vhost policies. Verify the trusted client-IP and GeoIP configuration before enabling country or rate controls. The global rate limit uses the same bucket for a given client across all public vhosts. It does not affect TCP/UDP streams or the separate administration listener.
The adjacent **Security headers** subtab applies a global response baseline: **HSTS** (HTTPS only), **nosniff**, **X-Frame-Options**, **Referrer-Policy**, **Content-Security-Policy** and **Permissions-Policy**. Explicit per-vhost security-header values take precedence; absence of a vhost value does not cancel a global header. Test HSTS and CSP against all sites before enabling. **Global custom headers** remains a distinct subtab for arbitrary request/response header rules.
Global User-Agent blocking is nested at `config.global_security.user_agent_blocking`; it is no longer a separate root-level setting. Example (only the relevant fragment):
```json
{
"global_security": {
"user_agent_blocking": {"enabled": true, "patterns": ["bad-bot"]},
"country_access": {"mode": "off", "countries": []}
}
}
```
`GET /api/config/global-security` returns `{revision, section, value}` where `value` is the entire `global_security` object. To update it, send `PUT /api/config/global-security` with `{ "expected_revision": <current-revision>, "value": <complete-global_security-object> }`; the User-Agent member is **required** in the section PUT. Both Global block / allow and Security headers forms save this complete object, preserving unchanged controls from the current revision. Validation and persistence are atomic. The formerly separate `PUT /api/config/user-agent-blocking` is no longer supported.
On upgrade, older JSON, database rows, backups and revision history containing a root-level `user_agent_blocking` are migrated at validation/compilation to the nested policy. If both old and new keys are supplied with differing values, validation fails instead of choosing one. Old configurations without `global_security` retain disabled defaults for all newly added policies. See [Administration API](API.md), [Access](ACCESS.md) and [Security](SECURITY.md).
## Cache and transforms
Caching is opt-in. HOT holds bounded memory entries; COLD is an optional disk tier (off by default). Freshness follows eligible HTTP policy and `Vary`; private/no-store responses and protected requests are not shared. Configuration saves can selectively invalidate affected cached responses; 0.34.0 behavior needs production validation. HTTP BAN/PURGE is **disabled by default** and uses an explicit source-IP allowlist. See [Cache](CACHE.md).
Caching is opt-in. HOT holds bounded memory entries; COLD is an optional disk tier (off by default). Freshness follows eligible HTTP policy and `Vary`; private/no-store responses and protected requests are not shared. Configuration saves selectively invalidate only the affected cached responses. HTTP BAN/PURGE is **disabled by default** and uses an explicit source-IP allowlist. See [Cache](CACHE.md).
Response transformations are optional and apply only to configured eligible MIME types and scopes. See [Response delivery](RESPONSE_DELIVERY.md) for codec defaults, transformation limits and header precedence. Content rewriting can affect validators, compression and cache entries; test actual representations after configuration changes.
-38
View File
@@ -1,38 +0,0 @@
# Docker / OCI deployment
The included `Dockerfile` compiles RustProxy with a **Rust 1.99** build image and runs it on Debian as an unprivileged `rustproxy` account (UID 10001). It includes Python, Certbot and DNS-plugin packages for certificate handling. The main container does **not** need the Docker socket.
## Build and start
Run from the repository root:
```bash
docker build -t rustproxy:0.34.2 .
docker volume create rustproxy-data
docker run -d --name rustproxy \
-p 8080:8080 \
-v rustproxy-data:/var/lib/rustproxy \
rustproxy:0.34.2
```
In the image, the default HTTP frontend binds `0.0.0.0:8080`. The admin listener defaults to `127.0.0.1:9090` **inside the container**. Simply publishing container port 9090 does not make that loopback listener reachable from a remote host. Initialize the administrator from a local container shell:
```bash
docker exec rustproxy rust-proxy admin setup-token
```
Provide a secure, private path for first-time panel access. If remote administration is required, configure its own HTTPS mode, certificate and an intentional non-loopback listener before mapping the port publicly; restrict it with firewall policy. Never expose plain HTTP administration as a convenience.
## Persistent data and ports
- Mount `/var/lib/rustproxy` persistently for SQLite, private keys, certificate profiles and optional COLD cache. Back it up securely and keep volume ownership correct.
- Map the chosen HTTP and HTTPS TCP ports. If enabling HTTP/3, publish the corresponding **UDP** port too, and allow it through the host firewall.
- If you need low-numbered **container** ports, use host-port forwarding or deliberate capability settings; the image runs without root privileges.
- Store file access logs under a writable location/volume if enabled. The built-in on-disk access-log sink is not necessarily on by default.
- Managed ACME material in the persisted volume survives container replacement. DNS credentials and external mounts must be protected and carried over separately.
For Docker-based application discovery, install the **separate Docker agent** on each target host and connect it over the supported restricted interface. See [Docker discovery](DOCKER_DISCOVERY.md).
## Upgrade and validate
Build a reviewed new image, preserve the volume, make an encrypted application backup and deploy with a maintenance window. Verify configuration compatibility, listener reachability, frontend certificates, backup/restore and live traffic. The supplied 0.34.2 development source candidate is not itself a production-tested OCI image.
+1 -1
View File
@@ -11,7 +11,7 @@ Custom locations add exceptions to a Vhost's normal source. They are **not** req
| `regex` / `regex_insensitive` | Case-sensitive or insensitive regular expression | Tested in configured order after priority-prefix rules |
| `prefix` | Ordinary longest matching path prefix | Fallback among matching rules |
Ambiguous and unsafe encoded path forms are rejected rather than treated as alternative spellings. Avoid overlapping regex rules when order matters; test paths with and without trailing `/`. A Vhost's main source handles paths without a matching custom location.
Encoded `/` separators (`%2F` / `%2f`) are normalized to real `/` only for location matching and security policy. Other ambiguous or unsafe encoded path forms remain rejected. In `preserve` rewrite mode the original encoded request target is forwarded unchanged to the upstream, so `/a%2Fb?x=1` is matched as `/a/b` but reaches the backend as `/a%2Fb?x=1`. Rewritten modes generate a new normalized target instead. Avoid overlapping regex rules when order matters; test paths with and without trailing `/`.
## Actions
+14 -1
View File
@@ -14,7 +14,20 @@ page has its own stylesheet.
The Overview contains live counters, charts and optional backend, cache, PHP, Docker and Stream cards. Under **Settings → Appearance → Dashboard layout** or the Overview editor, configure visibility, order, card width, density and the number of history samples. A hidden card stops consuming its optional browser-side visualization work but does not turn off its underlying proxy feature.
In 0.34.1, the unlocked Overview layout exposes a single **Save and lock** action (plus Cancel), removing the redundant lock-and-save button.
Optional quick-health tiles (since 0.35.2, hidden by default; enable them in the layout editor or **Settings → Appearance → Dashboard layout**):
| Tile | Shows |
|---|---|
| **Healthy backends** | Monitored backend members passing health checks as `up / total`, with the names of failing members. |
| **Error share** | Percentage of HTTP requests that ended with an error during the last sampling interval. |
| **Rejected by limits / s** | Requests and connections refused by `--max-connections` / `--max-inflight`; sustained values mean the limits are too low for the load. |
| **Nearest certificate expiry** | Days until the earliest installed certificate expires; the bar is full at 90 days or more. |
Enabled tiles are added at the end of the Overview, so the standard block layout does not change.
The **Remember choice** control next to the history sample count shows a status only when the current choice is temporary (not saved) or briefly after saving.
The unlocked Overview layout exposes a single **Save and lock** action (plus Cancel), removing the redundant lock-and-save button.
**Dashboard Presets** supports up to **16 named layouts per administrator**. Save, load, rename, overwrite and delete presets; a factory layout can be restored. Presets preserve the dashboard arrangement, not the live traffic values. Interface theme, scale and other display settings are also account-scoped and do not alter proxy routing or rewrite shared configuration.
+106
View File
@@ -0,0 +1,106 @@
# Feature list
RustProxy is a single service that combines a reverse proxy, a static/PHP web server, certificate management, an HTTP cache and traffic monitoring, all managed from a browser panel or an API. Optional features are disabled by default; enable only what you need.
![Overview dashboard](images/overview.png)
## Reverse proxy
| Feature | Details |
|---|---|
| Vhosts | Exact and wildcard hostnames, aliases, several listeners per Vhost, per-Vhost HTTP/2 and HTTP/3 switches |
| Backends | HTTP and HTTPS origins, HTTP/1.1, automatic ALPN (H2/H1), strict HTTP/2 and H2C, optional upstream certificate verification with custom CA and server name |
| Load balancing | Least connections (default), round robin, least traffic, adaptive and IP hash; member weights, backup and draining members, sticky cookie or IP affinity |
| Health checks | Active TCP or HTTP probes with thresholds, optional separate probe port, expected response text and live health status per member |
| Connection reuse | Bounded keep-alive pools with idle and age limits, pool recycling and protocol telemetry |
| Custom locations | Prefix, exact and regex paths, path rewriting, per-location backends, Web Services, local responses, redirects and internal error fallbacks |
| WebSockets | HTTP/1.1 Upgrade, RFC 8441 (HTTP/2) and RFC 9220 (HTTP/3) Extended CONNECT |
| Forwarding headers | `X-Forwarded-*`, `Forwarded`, `X-Real-IP`, trusted proxy ranges, Cloudflare client IP and country |
| Redirects and pages | Redirect hosts, canonical host and HTTPS redirects, 404 hosts, custom error pages and direct-IP pages |
| TCP/UDP Streams | Layer-4 forwarding with backend pools, health checks, session limits and idle timeouts |
## Frontends and TLS
- HTTP/1.1, HTTPS with HTTP/2, optional HTTP/3 (QUIC) with `Alt-Svc` management and configurable UDP buffers.
- Named listeners with per-listener protocols; Vhosts bind to listener IDs.
- TLS 1.2/1.3 policy with cipher-suite allowlist and per-Vhost overrides.
- Optional hybrid post-quantum key exchange (X25519MLKEM768).
- SNI certificate selection, hot certificate reload without restart.
## Web Server
- Static sites and downloads served from reusable **Web Services** shared by several Vhosts.
- Index files, optional directory listing, MIME mapping and attachment extensions.
- `try_files`, regex rewrites and a safe subset of Apache `.htaccess` rules.
- Conditional requests (`ETag`, `Last-Modified`), byte ranges, precompressed `.br`/`.gz` sidecars and optional Linux `sendfile`.
- Symlink policy, attack-probe blocking and header hardening presets.
- PHP through external PHP-FPM with shared profiles, per-application pools and resource limits.
- Presets for single-page apps, WordPress and Roundcube.
![Web Server editor](images/web-server.png)
## Certificates
- Self-signed certificates, PEM import and externally managed certificate paths (for example Certbot).
- Managed ACME with HTTP-01 and DNS-01 (Cloudflare, OVH, RFC 2136), automatic renewal and job history.
- Certificate inventory with expiry, coverage check and bulk assignment to Vhosts.
- Restricted certificate API keys for deploy/reload from external systems such as OPNsense.
- Prometheus metrics for the expiry date and remaining days of every installed certificate, ready for alerting.
![Certificates](images/certificates.png)
## Access and security
- IP/CIDR and DNS/DDNS allow/deny rules, reusable ACL profiles (up to eight per Vhost).
- Country allow/deny lists with automatic GeoIP database updates.
- Request rate limiting per Vhost and globally, with burst and `Retry-After`.
- HTTP method policy, request Content-Type denylist and User-Agent blocking.
- Security headers: HSTS, CSP, X-Frame-Options, Referrer-Policy, Permissions-Policy, `nosniff`.
- HTTP Basic authentication with Argon2 hashes and brute-force limits.
- Forward Auth with Tinyauth, Authelia and authentik; OIDC (Authorization Code + PKCE) with Authelia, authentik, Pocket ID and compatible providers.
- Fail-closed authentication errors, identity header stripping and private/no-store responses for authenticated content.
- Protection against private/loopback/control-plane upstream destinations unless explicitly allowed.
## Cache and delivery
- Shared HTTP cache with a memory tier (HOT) and an optional disk tier (COLD).
- RFC 9111 freshness, validators and `Vary`; stale-while-revalidate, stale-if-error and request coalescing.
- Varnish-compatible policy option, forced caching rules and per-location cache switches.
- RFC 9211 `Cache-Status` and optional `X-Cache` diagnostics.
- Purge and ban from the panel, the API or HTTP PURGE/BAN from allowlisted addresses.
- Live compression with gzip, Brotli, Zstandard and deflate.
- Optional minification of JavaScript, CSS, HTML, SVG and JSON with memory budgets.
![Cache](images/cache.png)
## Monitoring and logs
- Overview dashboard with requests per second, bandwidth, status codes, error share, cache hit ratio, protocol mix, system load, backend health, requests rejected by limits and nearest certificate expiry.
- Customizable dashboard layouts and saved presets.
- Per-Vhost, per-backend, per-Web-Service and per-Stream statistics.
- Live request log in the browser and in the terminal (`--livelogs`).
- Stored JSON access logs with search syntax, per-Vhost log files and an error log.
- Prometheus exporter with token/CIDR protection, JSON metrics and a compact summary endpoint.
- Grafana dashboard and Nagios checks included.
![Live logs](images/logs.png)
## Operations
- Configuration stored in SQLite or in a JSON file, with validation before activation and hot reload.
- Revision history with diff and one-click restore.
- Encrypted full backups and validated offline restore; encrypted Secret Store for credentials.
- Docker discovery through a separate agent: publish containers as Vhosts with one switch.
- Nginx Proxy Manager import with a dry-run plan.
- Managed install/update scripts with rollback, DEB/RPM packages, Docker image and an OPNsense plugin.
- Panel in English and Polish, light and dark themes, adjustable density and corner style.
## Administration and API
- Panel served on a separate, loopback-only listener by default; optional HTTPS for remote administration.
- Single administrator with Argon2 password, session revocation and CSRF/Origin protection.
- Complete REST API described by a bundled OpenAPI schema with Swagger UI at `/docs`.
- Scoped `ro`/`rw` API tokens bound to IP allowlists.
- Command-line tools for setup, configuration, certificates, backups and live logs.
See the [screenshots](SCREENSHOTS.md) for a visual tour and the [documentation index](README.md) for detailed guides.
+29 -10
View File
@@ -11,7 +11,7 @@ Use maintained packages whenever possible. For custom installations, use the scr
| OPNsense plugin | https://rustproxy-opn.linuxiarz.pl/ |
| Debian 13+ source bootstrap | `install-debian13.sh` from the project repository |
| Manual Linux/systemd install | Compile the application; use `install.sh` and `update.sh` |
| OCI container | Included `Dockerfile`; see [Container](CONTAINER.md) |
| OCI container | Included `Dockerfile`; see [Docker / OCI container](#docker--oci-container) |
Package/plugin instructions and maintenance policies are maintained separately from the source archive. The optional Docker agent is packaged independently of RustProxy's main service for **Linux amd64/x86_64 only**, not FreeBSD pkg. On a Docker host, the agent can also be installed manually from `docker-agent/install.sh`; the installer and packages preserve existing tokens and TLS keys. See [Docker discovery](DOCKER_DISCOVERY.md) and [agent operations](../docker-agent/README.md).
@@ -31,7 +31,7 @@ The script installs dependencies, builds with `cargo build --release --locked` a
## Build from the source
The current `Cargo.toml` declares **Rust 1.99 minimum**, with a `stable` toolchain in `rust-toolchain.toml`. Use Cargo, a C compiler/linker, Bash and Python 3 (Python 3.11+ is advisable for project checks). Run as a normal build user:
The minimum Rust version is declared in `Cargo.toml` (`rust-version`); `rust-toolchain.toml` selects the `stable` channel. Use Cargo, a C compiler/linker, Bash and Python 3 (Python 3.11+ is advisable for project checks). Run as a normal build user:
```bash
bash dev.sh doctor
@@ -89,8 +89,7 @@ Then visit `http://127.0.0.1:9090` on your workstation. For a directly reachable
## Linux DEB/RPM: read-only access to external paths
New Linux packages (package-builder filesystem-access policy, revision 5) and
the maintained manual service template use `ProtectHome=read-only` rather than
Linux packages and the maintained manual service template use `ProtectHome=read-only` rather than
`ProtectHome=true`. This makes document roots under `/home`, `/var/www` and
`/srv` readable if the service account has **ordinary Unix group or ACL
permissions**. `ProtectSystem=strict` remains enabled: the service cannot
@@ -98,7 +97,7 @@ modify these files merely because a group or ACL permits it. No systemd
drop-in or extra `ReadOnlyPaths=` entry is needed to **read** site content or
an external certificate. This also applies when paths are stored in the panel.
In **0.34.10**, the panel generates Linux permission commands from the actual
The panel generates Linux permission commands from the actual
paths you enter: below the two external certificate/key fields, and directly
below a Web Server's document root. The commands target the standard
`rustproxy` account and resolve symlink targets when run. Review them and run
@@ -160,7 +159,7 @@ After granting certificate read access, return to the form and use
**Read names from certificate**. The editor reads the public certificate's
DNS SAN entries into the legacy SNI field; this does not read the private key
or save the certificate. Manual name edits are preserved by background reads.
See [Certificates](ACME_PROVIDERS.md#permission-commands-and-automatic-sni-names).
See [Certificates](CERTIFICATES.md#permission-commands-and-automatic-sni-names).
To diagnose unresolved errors, run `systemctl cat rustproxy` and
`systemctl show rustproxy -p ProtectHome -p ProtectSystem -p User -p Group`,
@@ -170,7 +169,7 @@ check parent permissions with `namei -l /path/to/file`, and examine
unit and is **not** automatically removed by an upgrade. On SELinux/AppArmor
hosts, additional mandatory access-control policy may be required; POSIX ACLs
cannot bypass it. See [Web Server](WEB_SERVER.md) and
[Certificates](ACME_PROVIDERS.md).
[Certificates](CERTIFICATES.md).
**Write access is intentionally different.** An app that needs to upload or
modify files under an arbitrary document root needs a separately planned
@@ -233,6 +232,28 @@ This is **optional**. Without `--config`, RustProxy keeps its active configurati
For the alternate environment-variable method, set `RUST_PROXY_CONFIG=/etc/rustproxy/config.json` in the service's protected EnvironmentFile. Do not set **both** unrelated config paths. See [Configuration](CONFIGURATION.md) and the [deployment example](../deploy/README.md).
## Docker / OCI container
The included `Dockerfile` compiles RustProxy in a Rust build image and runs it on Debian as the unprivileged `rustproxy` account (UID 10001). The image contains Python, Certbot and DNS plugins for certificate automation. The main container does **not** need the Docker socket.
```bash
docker build -t rustproxy:latest .
docker volume create rustproxy-data
docker run -d --name rustproxy \
-p 8080:8080 \
-v rustproxy-data:/var/lib/rustproxy \
rustproxy:latest
docker exec rustproxy rust-proxy admin setup-token
```
Inside the image the HTTP frontend binds `0.0.0.0:8080` and administration binds `127.0.0.1:9090`, so publishing port 9090 alone does not expose the panel. For remote administration, enable administration HTTPS with a certificate and an explicit non-loopback listener first, and restrict access with firewall rules.
- Mount `/var/lib/rustproxy` persistently: it holds SQLite, private keys, certificate profiles and the optional COLD cache.
- Publish the HTTP/HTTPS TCP ports you use, and the matching **UDP** port when HTTP/3 is enabled.
- The image runs without root; map low host ports with port forwarding or deliberate capabilities.
- To discover containers on other hosts, install the separate [Docker agent](DOCKER_DISCOVERY.md).
- Upgrade by building a new image, keeping the volume and taking an encrypted backup first.
## Managed update
Test the new build first. Then execute:
@@ -246,10 +267,8 @@ Managed updates preserve the saved configuration, create a private backup, switc
## ACME prerequisites
The managed certificate service can create a private Python/Certbot environment in the data directory; a usable system Certbot may be used as fallback. DNS-01 providers need credentials and appropriate permissions. HTTP-01 needs incoming public TCP `80`. On systems without an available Python/Certbot runtime, PEM import and external certificate references can still be used. See [Certificates](ACME_PROVIDERS.md).
The managed certificate service can create a private Python/Certbot environment in the data directory; a usable system Certbot may be used as fallback. DNS-01 providers need credentials and appropriate permissions. HTTP-01 needs incoming public TCP `80`. On systems without an available Python/Certbot runtime, PEM import and external certificate references can still be used. See [Certificates](CERTIFICATES.md).
## After installation
Verify `systemctl status rustproxy` and `journalctl -u rustproxy` (managed systemd), the panel login, frontend certificates, first Vhost, real backend response, access policies, optional QUIC, current logs and a full backup/restore test. For error diagnosis, consult [Troubleshooting](TROUBLESHOOTING.md).
**Release verification:** RustProxy 0.34.2 is a source package; complete a Rust 1.99+ build and target-platform HTTP/systemd/security acceptance tests before Internet-facing production use.
-21
View File
@@ -1,21 +0,0 @@
# Integrations overview
RustProxy offers built-in integrations and optional helper components. Each has its own permissions and network requirements; enabling an integration does not automatically configure its external service.
| Integration | Purpose | Setup and limits |
|---|---|---|
| **Docker agent** | Discover running Docker services and publish generated Vhosts | Install separate agent on Docker host; use Bearer token and TLS certificate pin. [Guide](DOCKER_DISCOVERY.md). |
| **Tinyauth** | Delegate authentication for selected Vhosts | Configure verifier URL/login URL, allowed headers and HTTPS. [Access](ACCESS.md). |
| **ACME / Certbot** | Issue/renew HTTPS certificates | HTTP-01 or DNS-01 via Cloudflare, OVH or RFC2136; protect credentials. [Certificates](ACME_PROVIDERS.md). |
| **Prometheus** | Scrape operational metrics | Separate endpoint/token/CIDR rules. [Monitoring](MONITORING.md). |
| **Grafana** | Visualize Prometheus results | Example dashboard under `grafana/README.md`. |
| **Nagios** | External service checks | Sample checks under `nagios/README.md`; configure authorized network/API access. |
| **Nginx Proxy Manager** | Import existing NPM proxy hosts | Included Python dry-run migration, without importing all NPM settings or private certificates. [Migration](MIGRATION.md). |
| **OPNsense certificate deploy** | Deploy certificates from OPNsense ACME plugin | Review `integrations/opnsense-acme/README.md` and use scoped certificate credentials. |
| **RouterOS OCI packaging** | Build an image suited to RouterOS-container environments | Separate experimental packaging under `integrations/routeros-oci-build/README.md`; test the host architecture/network constraints. |
## Rules of thumb
Use API credentials with the smallest allowed action/scope and terminate TLS on sensitive network links. The Docker socket must remain confined to the Docker agent host, not the main proxy. Certificate deploy/reload tokens are **not** general administrator API tokens. For imported NPM services, revisit backend TLS verification, frontend certificate assignment, access rules and special Nginx directives manually.
External compatibility depends on the versions and settings of those external products; this source archive cannot establish acceptance against a live provider or CA.
+3 -3
View File
@@ -1,6 +1,6 @@
# Upgrades and migration
This guide covers changes that need operator action when moving to RustProxy 0.34.0 and the included Nginx Proxy Manager importer. For ordinary package upgrades, first follow [Installation and updates](INSTALL.md).
This guide covers upgrades that need operator action and the included Nginx Proxy Manager importer. For ordinary package upgrades, first follow [Installation and updates](INSTALL.md).
## Before changing versions
@@ -16,8 +16,8 @@ A managed install uses `update.sh`; an unmanaged source-tree install must follow
| Change area | What to review |
|---|---|
| **0.34.0 cache** | Operational Vhost saves aim to retain compatible HOT/COLD cache. Content-affecting changes invalidate a scope; older disk cache objects can require refilling. Validate actual behavior before rollout. |
| **0.34.0 Web Server** | `sendfile=auto` is an optional Linux cleartext HTTP/1.1 path; default is `off`. No existing service needs to enable it. |
| **Cache** | Operational Vhost saves aim to retain compatible HOT/COLD cache. Content-affecting changes invalidate a scope; older disk cache objects can require refilling. Validate actual behavior before rollout. |
| **Web Server** | `sendfile=auto` is an optional Linux cleartext HTTP/1.1 path; default is `off`. No existing service needs to enable it. |
| **0.32.x origin protocols** | Legacy backend `http2` remains compatible on TLS (H2/H1) and H2C for plain HTTP. New strict `h2` must be set explicitly. |
| **0.33.x Overview** | Dashboard presets/layouts are per administrator, independent from Vhost configuration; existing preferences should be retained and new widgets may remain hidden. |
| **Certificate API grants** | Older broad/reload-only credentials can exist after additive migration; replace them with explicit certificate-ID allowlists and least privilege. |
+9 -1
View File
@@ -24,7 +24,7 @@ Overview allows account-specific card layout and history controls; see [Dashboar
The exporter is **disabled by default** and uses a separate HTTP listener, normally `127.0.0.1:9191`, at `/metrics`. It is not protected by the admin browser cookie; it has its **own** client-network and bearer-token policy. In **Settings → Prometheus** enable the endpoint and configure:
In 0.34.1, use the separate **Prometheus token** controls in Settings to generate, rotate or revoke a 32+ character Bearer credential. Its clear value appears only once; copy it directly into your monitoring secrets store. The database keeps only its hash, and rotation/revocation takes effect without reprinting the previous secret. The older environment-variable credential remains supported as a fallback *only until* a generated credential or explicit revocation entry exists. Setting a new token does not reconfigure the exporter listener or its enable switch.
Use the separate **Prometheus token** controls in Settings to generate, rotate or revoke a 32+ character Bearer credential. Its clear value appears only once; copy it directly into your monitoring secrets store. The database keeps only its hash, and rotation/revocation takes effect without reprinting the previous secret. The older environment-variable credential remains supported as a fallback *only until* a generated credential or explicit revocation entry exists. Setting a new token does not reconfigure the exporter listener or its enable switch.
- `listen`, `path`, `allowed_cidrs` and an optional environment-variable **name** `bearer_token_env` (the secret value is supplied separately to the service).
@@ -44,6 +44,14 @@ scrape_configs:
- targets: ['127.0.0.1:9191']
```
Certificate expiry is exported for every installed certificate, including manually uploaded ones: `rust_proxy_certificate_not_after_timestamp_seconds{certificate="…"}` and `rust_proxy_certificate_days_remaining{certificate="…"}` (negative after expiry). Values are read from the certificate files and refreshed at most every five minutes. Example alert:
```yaml
- alert: RustProxyCertificateExpiresSoon
expr: rust_proxy_certificate_days_remaining < 14
for: 1h
```
When using a bearer token, configure Prometheus `authorization` or `bearer_token_file` securely rather than embedding a clear token in an exported dashboard. Review `grafana/README.md` and `grafana/rustproxy-overview.json` for chart examples; `nagios/README.md` contains external check scripts.
## JSON metrics API for external dashboards
+100
View File
@@ -0,0 +1,100 @@
# Performance
RustProxy runs on Tokio and Hyper with an asynchronous, streaming request path. This guide describes how performance is measured, where RustProxy stands compared with a similarly configured Nginx, and which settings matter in production.
## What is measured
Results are only comparable when both servers do the same work. A stock Nginx installation does very little per request, while RustProxy always records request statistics, traffic analytics and access logs and adds forwarding headers. The comparison therefore uses an Nginx configuration with a similar feature set:
| Feature | Nginx | RustProxy |
|---|---|---|
| Access log | `log_format` with timing fields, 64 KiB buffer | Built-in access log and live statistics (default) |
| Forwarding headers | `Host`, `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Real-IP` | `Forwarded`, `X-Forwarded-*`, `X-Real-IP`, `X-Request-Id` (default) |
| Response headers | `X-Request-ID`, `X-Content-Type-Options` | Request ID and default security headers |
| Upstream keep-alive | `keepalive 64`, HTTP/1.1 | Connection pool (default) |
| Cache | `proxy_cache` with `proxy_cache_lock` | HOT memory cache with request collapsing |
| Compression | `gzip on` | gzip/Brotli/Zstandard negotiation (default) |
| Static files | `root`, `etag on` | Web Service with validators (default) |
## Method
- Machine: 2 vCPU virtual machine, Linux 6.18, loopback networking.
- Each server is pinned to one CPU core (`worker_processes 1` for Nginx, `taskset` for RustProxy). The load generator and the origin share the second core.
- Load: `wrk -t1 -c50 -d8s`, HTTP/1.1 keep-alive, two or three runs per scenario, average reported.
- Origin: Nginx serving static files with `Cache-Control: public, max-age=600`.
- Nginx 1.24 and RustProxy release builds (`lto = "thin"`, `codegen-units = 1`).
## Results
Requests per second on one CPU core (higher is better):
| Scenario | Nginx | RustProxy 0.35.0 | RustProxy 0.35.2 | 0.35.2 vs 0.35.0 |
|---|---:|---:|---:|---:|
| Reverse proxy, 1 KiB response | 61,400 | 14,700 | 15,100 | +3% |
| Reverse proxy, 100 KiB response | 10,600 | 14,500 | 15,500 | +7% |
| Cache hit, 1 KiB response | 53,300 | 22,500 | 25,000 | +11% |
| Web Server, 1 KiB file | 85,000 | 15,200 | 26,000 | +71% |
| Web Server, 100 KiB file | 25,900 | 9,200 | 16,900 | +84% |
Reading the results:
- **Large proxied responses:** RustProxy streams the body directly between connections and is faster than Nginx in its default buffering mode, which spools larger upstream responses to temporary files.
- **Small requests:** Nginx remains ahead, because its fixed per-request cost is lower. In RustProxy this cost is dominated by per-request accounting (live statistics, traffic analytics, request IDs and logs) and by the asynchronous runtime. For most deployments it is far below the capacity of the backends behind the proxy.
- **Static files:** since 0.35.1 RustProxy reads files directly on the request task instead of a background thread pool, which nearly doubles static throughput.
- **Why small requests and cache hits trail Nginx:** each proxied request crosses the loopback network twice (client and backend), and on a VM about half of the CPU time goes to the kernel for those writes and the wake-ups of the other side. Most of the remaining time is RustProxy's per-request work that Nginx does not do in the same way: request IDs, live per-Vhost statistics, traffic analytics and structured access-log entries, plus the asynchronous runtime. A cache hit saves the backend hop but still pays for this accounting, so its gap is similar. No single function dominates the profile; closing the gap further would require architectural changes rather than tuning.
- **CPU per request:** at the 1 KiB proxy scenario RustProxy uses about 70 µs of CPU time per request on the test machine, Nginx about 17 µs. A single core therefore sustains well over 10,000 proxied requests per second.
Throughput scales with additional CPU cores; RustProxy uses all available cores automatically.
## Memory allocator
Release builds use the operating system's allocator (glibc on most Linux distributions). An alternative allocator, [mimalloc](https://github.com/microsoft/mimalloc), can be compiled in as an optional feature:
```bash
cargo build --release --features mimalloc
```
It is off by default because of how it behaves with a large HOT cache. Measured on the test machine with a 300 MiB HOT cache under constant turnover (random objects of 1–260 KiB, continuous evictions):
| Build | Process memory (RSS) | Throughput under cache turnover |
|---|---:|---:|
| System allocator (default) | 614 MiB | 5,700 req/s |
| mimalloc | 760 MiB (+24%) | 20,300 req/s (3.5×) |
| mimalloc with `MIMALLOC_PURGE_DELAY=0` | 720 MiB (+17%) | 18,600 req/s |
In the standard scenarios from the results table (proxy, cache hits, static files) the two allocators performed within measurement noise. On a production server with a 600 MiB HOT cache the same difference showed as about 800 MiB versus 1.1–1.2 GiB of process memory.
Choose mimalloc only when all of these apply:
- the HOT cache holds many objects that are replaced constantly (high eviction rate on the **Cache** page);
- throughput under that turnover matters more than memory;
- the server has at least 30% RAM headroom above the configured HOT cache size.
Otherwise keep the default. The system allocator returns memory from evicted cache entries to the operating system right away, so process memory stays close to the configured cache size.
## Production tuning
| Setting | Recommendation |
|---|---|
| Cache | Enable the HOT cache for cacheable content; hits avoid the backend completely. |
| Compression | Keep it enabled for text content. Use precompressed `.br`/`.gz` files for static sites. |
| Upstream protocol | Prefer HTTP/1.1 keep-alive or HTTP/2 to backends; avoid backends that close every connection. |
| Health checks | Use intervals of several seconds; frequent probes consume capacity on large installations. |
| Logging | Keep stored log files on fast local storage and use retention limits on busy servers. |
| Limits | `--max-connections` (default 512) and `--max-inflight` (default 256) cap concurrency. Raise them on busy servers with enough memory. |
| `sendfile` | Optional for large cleartext static files on Linux. On the test machine it gave no measurable gain, so keep `off` unless your own measurements show otherwise. |
| Memory allocator | Keep the default system allocator. See [Memory allocator](#memory-allocator) for when the optional `mimalloc` build is worth its extra memory. |
| TLS | TLS 1.3 with session resumption keeps handshake cost low; HTTP/2 or HTTP/3 reduce connection setup for browsers. |
## Reproducing the test
1. Start an origin (any static HTTP server) with a 1 KiB and a 100 KiB file.
2. Configure RustProxy with one Vhost pointing to the origin and one Web Service serving the same directory.
3. Configure Nginx with the equivalent features from the table above.
4. Pin each server to the same number of cores and run `wrk` from a different core:
```bash
taskset -c 1 wrk -t1 -c50 -d8s -H "Host: proxy.example" http://127.0.0.1:8080/1k.txt
```
Always compare matched configurations on the same machine, and repeat each run. Results on physical hardware and real networks differ from loopback measurements.
+52 -29
View File
@@ -1,44 +1,67 @@
# RustProxy documentation
Practical guides for **RustProxy 0.34.12**. 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.
Guides for operating RustProxy: installation, publishing services, security, certificates, monitoring and automation. The same pages are available in the administration panel under **`/docs → Reader`**, next to the interactive **API** reference.
- Project website and packages: [rustproxy.linuxiarz.pl](https://rustproxy.linuxiarz.pl/)
- Source code: [git.linuxiarz.pl/gru/RustProxy](https://git.linuxiarz.pl/gru/RustProxy)
- License: [GNU AGPL v3.0 or later](../LICENSE)
## Overview
- [Feature list](FEATURES.md) — everything RustProxy can do, grouped by area.
- [Screenshots](SCREENSHOTS.md) — a tour of the administration panel.
- [Performance](PERFORMANCE.md) — benchmark method, comparison with Nginx and tuning.
## Getting started
- [Installation and updates](INSTALL.md) — packages, build, first administrator, optional JSON/systemd permissions and updates.
- [Container deployment](CONTAINER.md) — image, persistent state, ports and isolation.
- [Settings and configuration](CONFIGURATION.md) — active SQLite/file storage source and which changes need restart.
- [Troubleshooting](TROUBLESHOOTING.md) — symptom-based checks and release validation.
- [Installation and updates](INSTALL.md) — packages, source build, Docker image, first administrator, systemd and updates.
- [Configuration](CONFIGURATION.md) — SQLite or JSON configuration source, configuration map and changes that need a restart.
- [Troubleshooting](TROUBLESHOOTING.md) — symptom-based checks and a rollout checklist.
## Publishing services
- [Vhosts and backends](VHOSTS.md) — domains, listener bindings, request Content-Type filtering, balancing, health and redirects.
- [Vhosts and backends](VHOSTS.md) — domains, listeners, backends, balancing, health checks and redirects.
- [Custom locations](CUSTOM_LOCATIONS.md) — path routing, rewrites, local responses and error fallbacks.
- [Access and authentication](ACCESS.md) — ACL, country/DDNS, Basic Auth, Tinyauth and rate limits.
- [Static Web Server](WEB_SERVER.md) — document roots, generated filesystem permission commands, downloads, rules, built-in error pages and file delivery.
- [PHP-FPM](PHP_FPM.md) — PHP service profiles, security and application examples.
- [Cache](CACHE.md) — HOT/COLD storage, privacy, TTL, RFC 9211 `Cache-Status`, origin diagnostic headers and invalidation.
- [Response delivery](RESPONSE_DELIVERY.md) — compression, static transformations and response headers.
- [HTTP transport](TRANSPORT.md) — HTTPS/H2/H3, WebSockets, origin protocols and timeouts.
- [Experimental TLS post-quantum key exchange](TLS_POST_QUANTUM.md) — opt-in X25519MLKEM768, compatibility, rollback and validation.
- [TCP/UDP Streams](STREAMS.md) — non-HTTP forwarding and its security boundaries.
- [Web Server](WEB_SERVER.md) — static sites, downloads, `try_files`, rewrites, `.htaccess` subset and error pages.
- [PHP-FPM](PHP_FPM.md) — PHP profiles, pool isolation and application examples.
- [HTTP transport and TLS](TRANSPORT.md) — HTTPS, HTTP/2, HTTP/3, WebSockets, backend protocols and post-quantum key exchange.
- [TCP/UDP Streams](STREAMS.md) — non-HTTP forwarding.
The Vhosts and Docker discovery guides include the shared adaptive request-rate chart scale introduced in 0.34.11. Version 0.34.12 preserves that adaptive range and adds a disabled-by-default X/Y scale display option under **Settings → Appearance → Charts**. See [Dashboard](DASHBOARD.md).
## Security and access
## Integrations and operations
- [Access and authentication](ACCESS.md) — ACLs, countries, rate limits, Basic Auth, Forward Auth and OIDC.
- [Certificates](CERTIFICATES.md) — PEM import, ACME HTTP-01/DNS-01, external Certbot files and automatic renewal.
- [Security model](SECURITY.md) — trust boundaries and deployment recommendations.
- [Certificates and automatic renewal](ACME_PROVIDERS.md) — PEM, automatic SNI names, path permission commands, 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, generated Vhosts and their per-service live HTTP logs.
- [Other integrations](INTEGRATIONS.md) — Tinyauth, NPM migration, OPNsense and monitoring tools.
- [Dashboard](DASHBOARD.md) — metrics panels, HIT/MISS/BYPASS/OTHER request mix, presets, configuration history and applying stylesheet updates.
- [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.
- [Migration](MIGRATION.md) — managed upgrades and Nginx Proxy Manager import.
- [Security](SECURITY.md) — important trust boundaries and limitations.
- [Administration API](API.md) and [API access](API_ACCESS.md) — endpoints, sessions and restricted certificate credentials.
## Performance and delivery
## Release information
- [Cache](CACHE.md) — HOT (memory) and COLD (disk) cache, freshness, `Cache-Status` and invalidation.
- [Response delivery](RESPONSE_DELIVERY.md) — compression, transformations and response headers.
[Changelog](../CHANGELOG.md) tracks user/operator-visible technical changes; [Roadmap](../ROADMAP.md) separates current verification work from future ideas. Historical low-level investigations, audits and benchmarks are not part of the live Reader. Keep them separately if you need an audit trail.
## Operations
When changing functionality, update the appropriate guide, this index, the registered Reader documents and any OpenAPI/schema references together. Describe actual code behavior, not unimplemented plans.
- [Dashboard](DASHBOARD.md) — Overview cards, layouts, presets and configuration history.
- [Monitoring](MONITORING.md) — Prometheus exporter, JSON metrics, Grafana and Nagios.
- [Logs](LOGS.md) — live, stored and error logs.
- [Backup and recovery](BACKUP.md) — encrypted backups, restore and the Secret Store.
- [Migration](MIGRATION.md) — upgrades and Nginx Proxy Manager import.
- [Docker discovery](DOCKER_DISCOVERY.md) — Docker agent and generated Vhosts.
- [Administration API](API.md) — authentication, tokens, endpoints and configuration sections.
## Integrations
| Integration | Purpose | Guide |
|---|---|---|
| Docker agent | Discover containers on Docker hosts and publish them as Vhosts | [Docker discovery](DOCKER_DISCOVERY.md) |
| Tinyauth, Authelia, authentik | Forward Auth for selected Vhosts | [Access](ACCESS.md) |
| OIDC (Authelia, authentik, Pocket ID, others) | Central login across unrelated domains | [Access](ACCESS.md) |
| ACME / Certbot | Certificates via HTTP-01 or DNS-01 (Cloudflare, OVH, RFC 2136) | [Certificates](CERTIFICATES.md) |
| Prometheus and Grafana | Metrics scraping and a ready dashboard | [Monitoring](MONITORING.md) |
| Nagios | Service checks for API, backends, latency and queues | [Monitoring](MONITORING.md) |
| Nginx Proxy Manager | Import of existing proxy hosts | [Migration](MIGRATION.md) |
| OPNsense | Certificate deployment from the os-acme-client plugin | `integrations/opnsense-acme/README.md` |
| MikroTik RouterOS | OCI image build for RouterOS containers | `integrations/routeros-oci-build/README.md` |
## Release notes
See the [changelog](../CHANGELOG.md).
+73
View File
@@ -0,0 +1,73 @@
# Screenshots
A tour of the RustProxy administration panel. The panel runs on the separate administration listener (by default `http://127.0.0.1:9090`) and is available in English and Polish, with light and dark themes.
## Sign-in
Local administrator sign-in. The first account is created with a one-time setup token; there is no default password.
![Sign-in](images/login.png)
## Overview dashboard
Live requests per second, bandwidth, status codes, cache hit ratio, protocol mix, system resources and backend health. Cards can be rearranged, resized, hidden and saved as presets.
![Overview dashboard](images/overview.png)
## Vhosts
Every published domain with its listeners, backends, health, request rate and security options. The editor groups frontend, backends, balancing, security, cache and custom locations.
![Vhost list](images/vhosts.png)
![Vhost editor](images/vhost-editor.png)
## Web Server
Reusable Web Services for static sites and PHP applications, with presets, delivery options and generated filesystem permission commands.
![Web Server](images/web-server.png)
## Certificates
Certificate inventory with expiry, covered hostnames, assigned Vhosts and managed ACME renewal.
![Certificates](images/certificates.png)
## Traffic
Per-Vhost traffic analytics: top paths, clients, user agents, countries, methods and status codes.
![Traffic analytics](images/traffic.png)
## Cache
HOT and COLD cache usage, hit ratio, stored objects and purge/ban tools.
![Cache](images/cache.png)
## Logs
Live and stored request logs with filters, search syntax and record details.
![Logs](images/logs.png)
## Settings
Listeners, TLS policy, security, logging, cache, compression, monitoring, administration and appearance.
![Settings](images/settings.png)
## Documentation and API
The built-in Reader with all guides, and an interactive OpenAPI reference powered by Swagger UI. Both work offline and use the administrator session.
![Documentation Reader](images/documentation.png)
![API reference](images/api.png)
## About
Version, author, license summary and project links.
![About](images/about.png)
+7 -1
View File
@@ -17,6 +17,12 @@ RustProxy provides several security controls, but it is not a complete Web Appli
- Honor forwarding identity headers only from correctly configured trusted peers. A client can forge arbitrary `X-Forwarded-*` and User-Agent headers.
- Restrict Basic Auth to HTTPS and protect Tinyauth endpoints. Authentication traffic should never be stored in public shared cache.
### Global HTTP security policy
**Settings → Rules & headers → Global block / allow** is a shared first gate for public HTTP listeners. Countries, User-Agents, IP/CIDR (including allow-only DNS), methods, declared request Content-Types and an optional shared rate limit can be configured there. These rules run before vhost routing, redirects and cache invalidation and do not replace Basic Auth, Tinyauth, application controls or OS firewalling. The country and IP decisions are both required to permit a request; a country deny cannot be bypassed by an IP allow and vice versa. Country mode **allow** is fail-closed for an unknown country; **block** mode is fail-open for unknown GeoIP data. Resolve identity from a trusted source and keep GeoIP updates healthy.
The **Security headers** subtab controls an HTTPS HSTS policy and optional global nosniff, frame protection, referrer, CSP and permissions policies. They are a common baseline; per-vhost values can set matching headers, but an unset field does not remove a global value. Rejecting only declared Content-Type is not a WAF, and changing HSTS/CSP may break sites. Changes do not apply to TCP/UDP Streams or the private administration listener.
## Content and cache privacy
- Shared cache must refuse private/no-store content and unsafe authenticated representations. Test behavior with cookies, `Authorization`, `Set-Cookie`, `Vary` and conditional requests.
@@ -39,4 +45,4 @@ RustProxy provides several security controls, but it is not a complete Web Appli
## Release and operational validation
The provided 0.34.0 source includes changes without completed native compilation, all-protocol regression or production performance measurement. Do **not** treat source-code inspection as proof of security or performance. Before using it at an Internet edge, test on the target OS, run the automated suite, verify TLS/H2/H3/WebSocket handling, access and cache privacy, backup/restore, logging and failure recovery. Keep monitored off-host backups and a rollback plan.
Before exposing a new version at an Internet edge, test it on the target OS with your own configuration: TLS/H2/H3/WebSocket handling, access rules and cache privacy, backup/restore, logging and failure recovery. Keep monitored off-host backups and a rollback plan.
-51
View File
@@ -1,51 +0,0 @@
# Experimental post-quantum TLS key exchange (PQC)
## What it does
Post-quantum cryptography (PQC) aims to protect key exchange from attackers with future quantum computing capabilities. **X25519MLKEM768** combines a conventional X25519 ephemeral key agreement with post-quantum **ML-KEM-768** in one TLS 1.3 *hybrid key-exchange group*. Both contributions protect the derived traffic secret. This is **not a TLS cipher suite**: AES-GCM or ChaCha20-Poly1305 and ordinary X.509 RSA/ECDSA certificates remain in use. It does not change certificate signatures to post-quantum algorithms, and does not make all parts of TLS quantum-safe.
When enabled, RustProxy uses the `aws-lc-rs` cryptography provider for selected incoming TLS policies, offering X25519MLKEM768 first and classical key-exchange groups as fallback. When disabled it keeps the existing `ring` TLS provider. The selected key exchange depends on client support and negotiation; turning the switch on does not imply every connection uses PQC. It does **not** change outbound/backend, update-check, certificate-automation or other HTTP client TLS settings.
For implementation details see [rustls defaults and PQC](https://rustls.dev/docs/rustls/manual/_05_defaults/index.html) and [X25519MLKEM768 rustls API](https://docs.rs/rustls/0.23.45/rustls/crypto/aws_lc_rs/kx_group/static.X25519MLKEM768.html).
## Enable
1. Make a configuration backup; ensure that **TLS 1.3** is enabled.
2. Open **Settings → TLS policy** and enable **Experimental post-quantum key exchange (PQC)**. Keep your current **cipher suite** list unless you have a specific reason to change it. The KEX group is enabled by the switch, **not** by adding X25519MLKEM768 to the cipher-suite textarea.
3. Select **Validate, save and apply**. The TLS status reports the configured key-exchange groups, not the group used by each individual connection.
4. Test HTTPS, HTTP/1.1, HTTP/2 and (if enabled) HTTP/3 on staging with a client that supports hybrid KEX, and with older clients that use classical KEX.
`post_quantum` is stored on the global policy (the default is false):
```json
{"tls": {"policy": {"tls12": true, "tls13": true, "cipher_suites": [], "post_quantum": true}}}
```
This fragment illustrates the `tls` field; do not replace your full configuration with it.
### Per-vhost behavior
A Vhost **inherits** the global TLS policy by default. If it has an explicit `tls_policy` override, the override may disable PQC, but it cannot enable PQC while the global setting is disabled. Existing overrides that do not contain `post_quantum` are treated as `false`, even after you enable PQC globally. HTTP/3 Vhosts require their override, if any, to match the global TLS policy exactly, including PQC. Update or remove conflicting overrides before enabling PQC on H3.
The administration TLS listener also uses the global policy when administration HTTPS is enabled. Open connections keep their negotiated keys; only **new handshakes** use the updated policy.
## Test negotiated group
Requires **OpenSSL 3.5+** with `X25519MLKEM768` support:
```bash
openssl s_client -connect example.com:443 -servername example.com \
-tls1_3 -groups X25519MLKEM768 -brief
```
Check for `Protocol version: TLSv1.3` and `Negotiated TLS1.3 group: X25519MLKEM768` (format depends on OpenSSL). Compare with a classic-only client using `-groups X25519` and test TLS 1.2 if it is enabled. For HTTP/3, also use an actual QUIC/H3 client and test with UDP blocked to verify fallback to H2/H1; `openssl s_client` does not test QUIC.
If your TLS connection ends at a CDN, load balancer or TLS-terminating proxy, the test shows **that intermediary**, not RustProxy. To check RustProxy, connect to its listener directly while preserving SNI, and verify its actual certificate.
## Caveats and rollback
- Hybrid handshakes are larger and can hit MTU/middlebox limits, especially over QUIC. Measure connection errors, handshake latency, memory and CPU under expected load. Some legacy clients and devices may reject hybrid negotiation.
- Enabling the switch changes the incoming TLS cryptographic provider to `aws-lc-rs`; some uncommon certificates, algorithms or hardware platforms may behave differently. `aws-lc-rs` requires a native C/C++ build toolchain; CMake may be needed for some build modes (non-FIPS commonly does not require it). The `ring` provider remains used where the switch is off and for existing outbound integrations.
- A non-empty cipher-suite allowlist must contain suites supported by the selected provider; invalid combinations are rejected. PQC requires TLS 1.3.
- For immediate rollback, **uncheck PQC in Settings → TLS policy and save**, and if necessary remove per-Vhost overrides that require PQC. Confirm new connections negotiate classic X25519. Existing sessions cannot renegotiate in place; close/reconnect them to retest. Keep the prior binary and backups available for a full rollback.
- **Release verification:** the PQC implementation inherited from 0.34.9 still requires native Cargo build, TLS handshake, QUIC interoperability and production load acceptance. Verify the provisionally updated `Cargo.lock` with `cargo metadata --locked` (and refresh it with Cargo if needed), then run `cargo check --all-targets`, `cargo test --all-targets`, `cargo build --release`, and protocol tests before deployment. See the release changelog and source verification notes.
+23 -5
View File
@@ -18,8 +18,7 @@ Transport settings attach to **named listeners**, and a Vhost must select the ap
- **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)
### QUIC UDP socket buffers
The HTTP/3 settings of each TLS listener include:
@@ -41,7 +40,6 @@ 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 |
@@ -55,10 +53,30 @@ WebSockets normally use HTTP/1.1 Upgrade. HTTP/2/HTTP/3 client requests can stil
## TLS and trust
**Experimental (0.34.9):** Settings → TLS policy has a disabled-by-default **Post-quantum key exchange (PQC)** switch for hybrid TLS 1.3 X25519MLKEM768. This is a TLS key-exchange group, not a cipher suite. See [Post-quantum TLS](TLS_POST_QUANTUM.md) for prerequisites, fallback, HTTP/3 and rollback.
Public HTTPS requires a correct certificate with hostname coverage. Administration TLS is separate. Backend `https://` encrypts traffic, but **upstream certificate verification is opt-in by default** in the current backend model; enable it and specify CA/name where needed. Private/loopback addresses and routing to a local RustProxy listener require explicit risk overrides.
## Post-quantum TLS key exchange (optional)
**Settings → TLS policy → Post-quantum key exchange** enables the hybrid TLS 1.3 group **X25519MLKEM768** (X25519 combined with ML-KEM-768) for incoming TLS. It is a key-exchange group, not a cipher suite: certificates and AES-GCM/ChaCha20-Poly1305 ciphers stay unchanged, and classic groups remain available as a fallback. The setting is off by default and requires TLS 1.3.
When enabled, incoming TLS uses the `aws-lc-rs` provider; otherwise `ring` is used. Outbound connections (backends, ACME, update checks) are not affected. The setting is stored as `tls.policy.post_quantum`:
```json
{"tls": {"policy": {"tls12": true, "tls13": true, "cipher_suites": [], "post_quantum": true}}}
```
- Vhosts inherit the global policy. A per-Vhost `tls_policy` override can disable PQC but cannot enable it when the global switch is off. HTTP/3 Vhosts need an override that matches the global policy exactly.
- Only new handshakes use the updated policy; open connections keep their keys.
- Hybrid handshakes are larger and may hit MTU or middlebox limits, especially over QUIC; some legacy clients reject them.
- Roll back by unchecking the switch and saving.
Verify the negotiated group with OpenSSL 3.5 or newer, directly against the RustProxy listener (not a CDN in front of it):
```bash
openssl s_client -connect example.com:443 -servername example.com \
-tls1_3 -groups X25519MLKEM768 -brief
```
## Timeouts, request bodies and streaming
Idle/read/connect/response-header timeouts and backend connection pools are configurable. A response may stream after its headers have arrived; response-header metrics are not full-body duration. WebSockets and Streams have separate idle lifetimes and should be tested with the actual application. RustProxy rejects suspicious ambiguous request targets rather than normalizing them into potentially unsafe backend paths.
+23 -14
View File
@@ -10,7 +10,13 @@ Start with a reproducible request, its hostname/path and the exact listener. Com
| 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. |
| Ordinary `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. A locally generated ordinary 502 has the generic **Bad gateway** page. |
| `502 Tinyauth connection error` | The authentication backend cannot be reached. Check Tinyauth process, DNS, verifier URL/port, network firewall, TLS/certificate, private backend allowlist and the RustProxy service journal. Do not route the verification URL back through the protected Vhost. |
| `502 Tinyauth verification error` | Tinyauth returned an unexpected status/body or identity headers were too large. Verify `/api/auth/nginx`, proxy header injection and the installed Tinyauth version; check the server journal for details. |
| `504 Tinyauth timeout` | The Tinyauth verification deadline was reached. Check service load/latency and `connect_timeout_seconds` / `timeout_seconds` (Vhost Authentication tab). |
| `502 <provider> connection error` / `502 <provider> verification error` / `504 <provider> timeout` | All delegated providers use the same `error_auth_provider.html` template. The name identifies the failing provider; the status identifies network, response-validation, or timeout failure. Check the internal URL, DNS/TLS, allowlist, deadline and RustProxy journal; origin access stays denied. |
| Authelia redirects repeatedly | Check `login_url` HTTPS origin, `session.cookies` domains/`authelia_url` mapping, real proxy forwarding headers and original application host. Unrelated registered domains require matching per-domain cookie/portal settings. |
| authentik redirects repeatedly or `/outpost.goauthentik.io` is 404 | Check its proxy provider is **Forward auth (single application)** for the exact external application URL, assigned to the correct outpost. Use the outpost internal origin in the Vhost; RustProxy reserves this path on each protected host. Ensure the outpost can reach authentik and browser accepts HTTPS cookies. |
| 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. |
| Cache always BYPASS or MISS | Global + Vhost enablement, cookies/login, origin `Cache-Control`, `Set-Cookie`, `Vary`, TTL, size and exclusion rules. |
@@ -48,8 +54,7 @@ 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)
## HTTP/3 throughput and CPU diagnostics
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.
@@ -63,21 +68,25 @@ Do not assume that low H3 throughput is a CPU bottleneck from the overall VM per
```
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.
4. 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).
5. Run `ss -uapm | grep -A1 rust-proxy` during a transfer; check `skmem rb`, `tb` and whether dropped-datagram `d` increments. RustProxy 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).
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
## Release rollout checklist
The latest source optimizations were **not all compiled or performance-tested** in the evidence supplied with this release. Before deploying:
1. Verify HTTP/1.1, HTTPS, HTTP/2, HTTP/3 (if enabled), WebSockets and multi-backend failover against a real origin.
2. Test private origin constraints, ACL/authentication, rate limits and protected cache content.
3. Test Web Server static files, PHP-FPM, cache revalidation/invalidation and `sendfile=auto` if enabled.
4. Exercise certificate renewal/reload and a backup **restore** on staging.
5. Compare performance only with matched configurations and repeatable traffic; see [Performance](PERFORMANCE.md).
1. Build using the declared Rust toolchain and run the project's available regression suite on the target platform.
2. Verify HTTP/1.1, HTTPS, HTTP/2, HTTP/3 (if enabled), WebSockets and multi-backend failover with a real origin.
3. Test private origin constraints, ACL/auth/CSRF, rate limits and protected cache content.
4. Test Web Server static files, PHP-FPM, cache revalidation/invalidation and optional `sendfile=auto` only if enabled.
5. Exercise certificate renewal/reload and backup **restore** on isolated staging.
6. Compare performance only under matched configurations with controlled repeatable traffic; do **not** label 0.34.0 as faster merely because its source reduces internal work.
For persistent problems, use the feature-specific guide in [Documentation](README.md) and keep a redacted error example with the version and request ID.
For persistent problems, use the feature-specific guide in [Documentation](README.md) and preserve a redacted error example with environment/version and request ID.
## OIDC central login
- **Login does not start / 502:** verify that the HTTPS Issuer URL is the exact `issuer` from `/.well-known/openid-configuration`, including any path or trailing slash. RustProxy must resolve that address and connect to it with a trusted certificate, or you must configure an explicit internal backend origin for server-side OIDC requests. Discovery, authorization, token, UserInfo and JWKS endpoints must have the same HTTPS origin as the issuer.
- **Redirect URI mismatch:** register the exact `https://APP-HOST/_rustproxy/oidc/callback` value with the identity provider. The callback hostname must be explicitly listed in the protected Vhost hosts and be accessed over HTTPS. Don't protect the IdP's own login Vhost using this OIDC setting.
- **Invalid code, nonce or signature:** ensure that the provider issues an `id_token` using RS256 or ES256 and that a confidential client uses `client_secret_basic` with authorization code and PKCE S256. Repeated callback requests or overwritten browser transaction cookies are refused. A single browser login flow per protected host avoids cookie conflicts.
- **No identity on the backend:** trust only requests through RustProxy. A verified login injects `Remote-Sub`, `Remote-Issuer`, `Remote-User`, and optional profile headers; the application must explicitly consume them. An OIDC session cookie on one app/domain is not a login cookie for another.
- **User removed at the provider but session still works:** local application sessions are independent and expire after the configured `session_ttl_seconds` (one hour by default); visit `/_rustproxy/oidc/logout` on the application origin to clear the local session. RustProxy does not implement back-channel logout or token revocation.
+3 -1
View File
@@ -58,13 +58,15 @@ Sample documents in `examples/vhosts-private.json` and `examples/vhosts-balanced
A backend's `preserve_host` option can retain the frontend authority when appropriate. HTTPS encryption alone does not guarantee verified backend identity: `tls_verify` is **false by default**. Enable verification and configure trusted CA/name for sensitive connections. Private, loopback and RustProxy-local listener targets require explicit security exceptions; do not point a public route to the admin listener. Read [Security](SECURITY.md).
Connection reuse is bounded by configured idle-pool/recycle limits. 0.34.0 changes the default H1 upstream idle pool for newly configured Vhosts; previously saved explicit values remain. Configure timeouts to suit expected request and upload behavior; a slow origin may need more than the default response-header timeout.
Connection reuse is bounded by configured idle-pool/recycle limits. The default H1 upstream idle pool applies to newly configured Vhosts; previously saved explicit values remain. Configure timeouts to suit expected request and upload behavior; a slow origin may need more than the default response-header timeout.
## Load balancing and health
Backend pools support scheduling/weighting, standby members, draining and optional session affinity. Check the **Backends** status view for member health, recently observed HTTP response code, upstream protocol and resource usage. A reported status from normal traffic is not necessarily the result of a health probe.
- Health monitoring may use TCP or HTTP checks with a path, Host, interval and thresholds. Avoid write endpoints; use a lightweight `/health` or equivalent.
- **Health check port** (`health_check.port`) probes a different port than the one receiving traffic, like `check port` in HAProxy. Example: a backend serving traffic on `http://10.0.0.5:80` with a status endpoint on port `8079` uses `"health_check": {"mode": "http", "path": "/status", "port": 8079}`. The probe keeps the backend host, scheme, TLS settings and address restrictions; only the port changes. Leave it empty to probe the traffic port. The port can be set for the whole pool or per backend.
- **Expected response text** (`health_check.expect_body`) makes an HTTP probe fail unless the first 64 KiB of the response body contain the given text, like `http-check expect string` in HAProxy. Use it for status pages that answer `200` even when the application is degraded, for example `"expect_body": "\"status\":\"ok\""`. Failed matches appear as `http_body` in backend health details.
- Each member can override the pool health check or disable active/passive monitoring with `health_check_enabled=false`. Disabling checks treats the member as available, not proven healthy.
- New monitored pools can temporarily have no eligible healthy member until rise checks succeed. A failed/incorrect check can produce `503` even if the process is reachable.
- `backup` members act as fallbacks; `drain` prevents new normal selection while letting existing work complete within normal lifetimes. Explicitly pinned backends need careful policy review.
+3 -4
View File
@@ -46,9 +46,8 @@ For PHP applications, the separate **PHP-FPM pool user** needs its own access to
| **Symlinks** | `deny` by default; `inside_root` is an explicit less restrictive policy. |
| **Hardening** | Optional checks for common sensitive/attack paths and removal of selected server-identifying headers; not a full WAF. |
### Static delivery (`sendfile`) — 0.34.0
In **Web Server → Edit service → Delivery**, `sendfile` supports `off` (default) and `auto`. On **Linux**, eligible full static **GET** responses of at least **64 KiB** over **unencrypted HTTP/1.1** may use a kernel file-transfer path. TLS, HTTP/2, HTTP/3, ranged or transformed content, compressed responses and active object-cache responses keep the normal streaming path. This transport choice should not invalidate otherwise compatible cached objects. It is an **unverified source candidate** in the provided archive: test it on the actual server before use and do not assume a measured speedup.
### Static delivery (`sendfile`)
In **Web Server → Edit service → Delivery**, `sendfile` supports `off` (default) and `auto`. On **Linux**, eligible full static **GET** responses of at least **64 KiB** over **unencrypted HTTP/1.1** may use a kernel file-transfer path. TLS, HTTP/2, HTTP/3, ranged or transformed content, compressed responses and active object-cache responses keep the normal streaming path. This transport choice should not invalidate otherwise compatible cached objects. Measure on your own hardware before enabling it; see [Performance](PERFORMANCE.md).
## Rewrites and application presets
@@ -66,7 +65,7 @@ In **Web Server → Edit → Delivery**, static file delivery, dynamic compressi
A Web Service may enable PHP via a shared PHP-FPM profile or its own configuration. PHP is executed by the FPM service, not RustProxy, and must not be exposed as a raw Internet port. Use dedicated pools for different applications. See [PHP-FPM](PHP_FPM.md).
Missing static files return **HTTP 404 Not Found**, unreadable/blocked paths **403 Forbidden**, unsupported static methods **405 Method Not Allowed** (with `Allow: GET, HEAD`), and internal failures **500 Internal Server Error**. The built-in Web Server now uses the existing **application-style HTML error pages** for statuses with a bundled template (including 400, 403, 404, 500 and PHP-FPM gateway errors). Other generated error statuses, such as 405, use the shared styled HTML fallback. Neither internal filesystem paths nor `RustProxy: Web Server ...` diagnostics are included in the public body. Details for 5xx filesystem failures remain in server logs.
Missing static files return **HTTP 404 Not Found**, unreadable/blocked paths **403 Forbidden**, unsupported static methods **405 Method Not Allowed** (with `Allow: GET, HEAD`), and internal failures **500 Internal Server Error**. The built-in Web Server now uses the existing **application-style HTML error pages** for statuses with a bundled template (including 400, 403, 404, 500 and PHP-FPM gateway errors). Other generated error statuses, such as 405, use the shared styled HTML fallback. The regular built-in 502 is styled with RustProxy branding; auth-specific Tinyauth failure pages have their own wording and are served before any origin application is accessed. Neither internal filesystem paths nor `RustProxy: Web Server ...` diagnostics are included in the public body. Details for 5xx filesystem failures remain in server logs.
These generated HTML responses retain the actual HTTP status, `Content-Type: text/html; charset=utf-8`, `X-Content-Type-Options: nosniff` and `Cache-Control: no-store`; `HEAD` preserves the matching `Content-Length` but sends no body. Explicit application/origin responses are still passed through without forced replacement, subject to the Vhost error interception options below.
Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

+22
View File
@@ -0,0 +1,22 @@
{
"id": "sample-oidc-app",
"frontend": {
"hosts": ["app.example.net"],
"listener_ids": ["https"]
},
"oidc": {
"enabled": true,
"issuer_url": "https://auth.example.com",
"backend_url": "http://192.0.2.21:9091",
"client_id": "CHANGE_ME_OIDC_CLIENT_ID",
"client_secret": "CHANGE_ME_CONFIDENTIAL_OIDC_CLIENT_SECRET",
"session_secret": "CHANGE_ME_RANDOM_64_OR_MORE_CHARACTERS_SECRET",
"redirect_url": "https://app.example.net/_rustproxy/oidc/callback",
"scopes": "openid profile email",
"allow_private": true,
"session_ttl_seconds": 3600
},
"backends": [
{"id": "app", "url": "http://192.0.2.20:8000", "allow_private": true}
]
}
+2 -2
View File
@@ -12,7 +12,7 @@ latency, backend health/EWMA latency/active work, traffic, probe age and upstrea
variables **Docker agent** and **Docker service** filter only this section, which keeps
multi-agent installations readable without changing the ordinary vhost panels.
The **0.33.0 Web Server** row adds header-ready request rates, status-class and
The **Web Server** row adds header-ready request rates, status-class and
static/PHP/other donut breakdowns, error percentage, response-header latency,
payload byte rates, and downstream compression by codec. **PHP-FPM / FastCGI**
adds request activity, RustProxy PHP permit capacity, and errors grouped by
@@ -29,7 +29,7 @@ classes, cache outcomes, capture-state gauges and configured-route traffic. Raw
are intentionally absent from Prometheus labels.
Import the JSON in Grafana and select a Prometheus datasource when prompted. The
dashboard expects RustProxy 0.33.0 for its newest Web Server metric names
dashboard expects a RustProxy version that exports the Web Server metric names
(and also retains the older panels); a scrape interval around 15s is suitable.
Collectors can be disabled in RustProxy settings; panels for disabled collectors
will simply show no series.
File diff suppressed because it is too large. Load diff
+5 -12
View File
@@ -106,18 +106,14 @@ run_builder() {
run_builder git clone --depth 1 "$REPO_URL" "$work/RustProxy"
# The builder, not root, owns the toolchain. Never update a system-wide
# apt-managed compiler or change the user's default rustup toolchain.
# rustup is needed even when an older system-installed cargo exists.
# The unprivileged builder owns its rustup toolchain; system compilers are left untouched.
if ! run_builder bash -c 'command -v rustup >/dev/null 2>&1'; then
echo "Installing rustup for build user: $builder"
run_builder env RUSTUP_INIT_SKIP_PATH_CHECK=yes bash -c \
'set -o pipefail; wget -qO- --https-only --secure-protocol=TLSv1_2 https://sh.rustup.rs | sh -s -- -y --profile minimal --no-modify-path --default-toolchain none'
fi
# Read the actual requirements from the checked-out sources, not a hardcoded
# Rust version. The project may inherit rust-version from [workspace.package].
# The channel from rust-toolchain.toml (or legacy rust-toolchain) takes precedence.
# Read the required Rust version and channel from the checked-out sources.
rust_settings=$(python3 - "$work/RustProxy" <<'PY_RUST'
import pathlib
import re
@@ -175,8 +171,7 @@ if [[ -z "$installed_rust" ]] || ! rust_version_at_least "$installed_rust" "$req
run_builder rustup update "$build_toolchain"
;;
*)
# A fixed toolchain must be respected. Do not silently override a pin
# that is older than the manifest's minimum supported Rust version.
# Never silently override a pinned toolchain older than rust-version.
if [[ -n "$installed_rust" ]]; then
echo "Pinned toolchain '$build_toolchain' is older than rust-version $required_rust; fix rust-toolchain.toml in the repository." >&2
exit 1
@@ -193,8 +188,7 @@ fi
run_builder rustup run "$build_toolchain" cargo --version
echo "Building with Rust $installed_rust ($build_toolchain); required >= $required_rust."
# Keep source builds reliable on small Debian VPSes. The repository release
# profile may use LTO/codegen settings that can make rustc exceed RAM limits.
# Lighter release settings keep builds within RAM limits on small VPSes.
BUILD_JOBS="${RUSTPROXY_BUILD_JOBS:-1}"
if ! [[ "$BUILD_JOBS" =~ ^[1-9][0-9]*$ ]]; then
echo "RUSTPROXY_BUILD_JOBS must be a positive integer." >&2
@@ -230,8 +224,7 @@ if [[ "$MODE" == install ]]; then
printf 'Administration is loopback-only by default. Use an SSH tunnel or enable Administration HTTPS before remote exposure.\n'
fi
else
# update.sh reads the saved installation record and therefore preserves the
# existing data directory, HTTP/TLS/admin binds and service account.
# update.sh preserves the saved data directory, binds and service account.
bash update.sh \
--binary target/release/rust-proxy \
--prefix "$PREFIX"
@@ -1,8 +1,7 @@
#!/bin/sh
set -eu
# Deploy an externally issued OPNsense/os-acme-client certificate to RustProxy 0.23.x.
# Configuration is kept separately so the API token does not need to live in this script.
# Deploy an OPNsense os-acme-client certificate to RustProxy; settings live in a separate file.
CONFIG_FILE="${RUSTPROXY_ACME_CONFIG:-/usr/local/etc/rustproxy-acme.conf}"
@@ -62,10 +61,7 @@ KEY_PUB=$(
"$OPENSSL" x509 -in "$CERT_FILE" -checkend 0 -noout >/dev/null 2>&1 \
|| fail "Certificate is already expired"
# Always reconcile with RustProxy. The deploy endpoint is idempotent: an identical
# RustProxy-owned generation is a server-side no-op, while a missing certificate id
# is created and a changed manual certificate is rotated atomically. This avoids
# trusting stale local state when the RustProxy configuration was changed separately.
# The deploy endpoint is idempotent, so always reconcile instead of trusting local state.
umask 077
TMP_JSON=$(mktemp "${TMPDIR:-/tmp}/rustproxy-cert.XXXXXX") || fail "mktemp failed"
@@ -73,8 +69,7 @@ TMP_CURL=$(mktemp "${TMPDIR:-/tmp}/rustproxy-curl.XXXXXX") || { rm -f "$TMP_JSON
TMP_RESPONSE=$(mktemp "${TMPDIR:-/tmp}/rustproxy-response.XXXXXX") || { rm -f "$TMP_JSON" "$TMP_CURL"; fail "mktemp failed"; }
trap 'rm -f "$TMP_JSON" "$TMP_CURL" "$TMP_RESPONSE"' EXIT HUP INT TERM
# PEM is line-oriented Base64 plus BEGIN/END markers. Escape it as a JSON string
# without needing jq/python on OPNsense.
# Escape PEM as a JSON string without jq/python.
pem_json() {
awk '{
gsub(/\\/, "\\\\");
@@ -114,8 +109,7 @@ if [ -n "${RUSTPROXY_CACERT:-}" ]; then
printf 'cacert = "%s"\n' "$RUSTPROXY_CACERT" >> "$TMP_CURL"
fi
# Optional curl --resolve equivalent, e.g. rustproxy.lan:8443:10.10.10.20.
# This lets the URL use the certificate DNS name while traffic goes to a fixed LAN IP.
# Optional curl --resolve value, e.g. rustproxy.lan:8443:10.10.10.20.
if [ -n "${RUSTPROXY_RESOLVE:-}" ]; then
printf 'resolve = "%s"\n' "$RUSTPROXY_RESOLVE" >> "$TMP_CURL"
fi
+2 -2
View File
@@ -77,10 +77,10 @@ ENV_FILE=integrations/routeros-oci-build/.env \
bash integrations/routeros-oci-build/build-routeros-oci.sh push
```
With RustProxy version `0.25.0` and the defaults this publishes:
For example, with RustProxy version `0.35.3` and the defaults this publishes:
```text
zot.example.net/network/rustproxy:0.25.0
zot.example.net/network/rustproxy:0.35.3
zot.example.net/network/rustproxy:latest
zot.example.net/network/rustproxy:routeros
```
@@ -1,9 +1,7 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# RustProxy multi-arch OCI builder for RouterOS.
# Rust compilation runs on BUILDPLATFORM. Target binaries are cross-compiled,
# so rustc/cargo do not run through QEMU for arm64 builds.
# RustProxy multi-arch OCI builder for RouterOS (cross-compiled, no QEMU rustc).
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -74,8 +72,7 @@ VERSION="$(awk '
' "$PROJECT_ROOT/Cargo.toml")"
[[ -n "$VERSION" ]] || { echo "Cannot determine package version from Cargo.toml" >&2; exit 1; }
# Registry/image. Preferred format: REGISTRY + IMAGE_NAME.
# OCI_IMAGE is kept as a compatible shorthand.
# Image: REGISTRY + IMAGE_NAME (or OCI_IMAGE shorthand).
if [[ -n "${OCI_IMAGE:-}" ]]; then
IMAGE_REPO="${OCI_IMAGE%/}"
REGISTRY="${OCI_REGISTRY:-${IMAGE_REPO%%/*}}"
+96 -7
View File
@@ -18,6 +18,44 @@
"streamBackendHealthSection": "Health checks",
"fileLogsSortAscending": "Sort ascending",
"fileLogsSortDescending": "Sort descending",
"globalBlockAllow": "Global block / allow",
"globalSecurityHeaders": "Security headers",
"globalPolicyScope": "PUBLIC HTTP LISTENERS",
"globalPolicyHelp": "Rules are evaluated for public HTTP traffic before connecting to the upstream, including redirects and cache invalidation. Global and per-vhost rules are cumulative; disabled controls leave existing behavior unchanged. DNS-based country rules need GeoIP data.",
"globalCountryTitle": "Country access",
"globalCountryHelp": "Block listed countries or allow only listed countries (ISO 3166-1 alpha-2). Unknown countries are denied in allow mode. Configure GeoIP updates under Settings.",
"globalCountryMode": "Country policy",
"globalCountryList": "Country codes (one per line or comma separated)",
"globalUaTitle": "User-Agent blocklist",
"globalUaEnabled": "Enable global User-Agent blocking",
"globalIpTitle": "IP / CIDR and dynamic DNS access",
"globalIpHelp": "Optional allow/deny rules for client IPs, subnets and allow-only DNS names. Deny wins. The default action applies when no rule matches.",
"globalIpEnabled": "Enable IP / CIDR access rules",
"globalIpDefault": "Default action",
"globalIpRules": "Rules (allow or deny, followed by IP/CIDR; DNS is allow-only)",
"globalMethodTitle": "HTTP methods",
"globalMethodHelp": "Block selected methods or allow only those listed. Applies before redirects and cache invalidation; denied requests receive 405.",
"globalMethodMode": "Method policy",
"globalMethodList": "Methods (comma, space or newline separated)",
"globalContentTypeTitle": "Request Content-Type",
"globalContentTypeHelp": "Reject declared MIME types (exact or type/*) with 403. This is a header rule, not body inspection.",
"globalContentTypeEnabled": "Enable Content-Type denylist",
"globalContentTypeList": "Blocked MIME types (one per line)",
"globalRateTitle": "Shared rate limit",
"globalRateHelp": "Optional per-client-IP limit shared by all public vhosts. Runs in addition to per-vhost limits; use conservative values to avoid blocking legitimate shared clients.",
"globalRateEnabled": "Enable global request rate limit",
"globalRateRequests": "Requests per window",
"globalRateWindow": "Window (seconds)",
"globalRateBurst": "Burst allowance",
"globalPolicySave": "Apply global block / allow rules",
"globalHeadersSecurityHelp": "Global response-header baseline for public HTTP vhosts, redirects and 404 hosts. Explicit per-vhost values override matching global headers. HSTS is sent only over HTTPS; test CSP carefully before rollout.",
"globalSecurityHeadersSave": "Apply security headers",
"globalCountriesRequired": "Enter at least one country code or turn off the country policy.",
"globalCountriesInvalid": "Use two-letter country codes, for example PL or DE.",
"globalMethodsRequired": "Enter at least one HTTP method or turn off the method policy.",
"globalContentTypesRequired": "Enter at least one MIME type or disable Content-Type blocking.",
"cacheHeadersSection": "Cache response headers",
"transformOperationsTitle": "Transformations",
"policyRulesTitle": "Rules & headers",
"vhUaRateTitle": "User-Agent rate limit overrides",
"vhUaRatePattern": "User-Agent pattern",
@@ -419,6 +457,9 @@
"vhHttp": "HTTP status + response time",
"vhHealthPath": "HTTP check path",
"vhHealthHost": "HTTP Host override (optional)",
"vhHealthPort": "Health check port (optional)",
"vhHealthPortPlaceholder": "Backend port",
"vhHealthExpectBody": "Expected text in HTTP response (optional)",
"vhInterval": "Interval (ms)",
"vhTimeout": "Timeout (ms)",
"vhSlow": "Slow threshold (ms; 0 = off)",
@@ -426,7 +467,7 @@
"vhRise": "Successes before recovery",
"vhStatusMin": "Minimum HTTP status",
"vhStatusMax": "Maximum HTTP status",
"vhHealthNote": "HTTP probes use HTTP/1.1 and measure time to headers, not body duration. When Host override is blank, RustProxy automatically uses the first exact DNS hostname of the vhost; if none exists it falls back to the backend host. Use TCP for h2c-only services. New backends wait for successful probes. Active health is independent from passive health.",
"vhHealthNote": "HTTP probes use HTTP/1.1 and measure time to headers, not body duration. When Host override is blank, RustProxy automatically uses the first exact DNS hostname of the vhost; if none exists it falls back to the backend host. Use TCP for h2c-only services. New backends wait for successful probes. Active health is independent from passive health. Health check port probes a different port than traffic (for example a status port 8079 for a backend on port 80); host, TLS and address restrictions stay the same. Expected text marks the backend as down unless the first 64 KiB of the HTTP probe response contain it.",
"vhPassiveHealth": "Passive Backend Health",
"vhPassiveEnabled": "Allow normal request failures to mark a backend unhealthy",
"vhPassiveHelp": "Off by default. Errors are still logged and counted, but normal user traffic cannot change backend health until this is explicitly enabled.",
@@ -605,7 +646,6 @@
"certSelectHint": "Select an imported certificate. Uploads and rotation are in the Certificates tab. Assignments are validated against vhost names.",
"vhTinyauth": "Authentication",
"vhTabAuth": "Authentication",
"taEnable": "Enable Tinyauth protection",
"taUrl": "Tinyauth URL / verification endpoint",
"taLogin": "Public login URL",
"taRedirectForbidden": "Accept same-origin provider redirects for 403",
@@ -820,9 +860,31 @@
"aclNetwork": "IP/CIDR or DNS name",
"aclAction": "Action",
"aclAdd": "Add IP rule",
"authProvider": "Identity provider",
"authMethod": "Integration method",
"authForwardAuth": "Forward Auth",
"authPocketId": "Pocket ID",
"authCustomOidc": "Other OIDC provider",
"authMode": "Authentication",
"authNone": "None",
"authTinyauth": "Tinyauth",
"authAuthelia": "Authelia",
"authAuthentik": "authentik",
"authOidc": "OIDC (central login)",
"oidcIssuer": "OIDC issuer (HTTPS)",
"oidcBackend": "OIDC internal service URL (optional)",
"oidcRedirect": "Application callback URL (HTTPS)",
"oidcClient": "Client ID",
"oidcSecret": "Client Secret",
"oidcSession": "RustProxy session secret (at least 32 characters)",
"oidcScopes": "OIDC scopes",
"oidcTtl": "Session lifetime (seconds)",
"oidcHelp": "Cross-domain central login using OIDC Authorization Code + PKCE. Register the exact callback URL with your provider. Requires trusted HTTPS, a session secret and a confidential client (client_secret_basic). Supported ID token signatures: RS256/ES256. Discovery endpoints must use the issuer origin. Application sessions are separate; RustProxy injects verified Remote-User, Remote-Sub and optional attributes.",
"forwardAuthUrl": "Forward-auth service URL (internal)",
"forwardAuthLogin": "Authelia public login URL (HTTPS)",
"forwardAuthAdvanced": "Advanced provider settings",
"autheliaHelp": "Calls /api/authz/forward-auth. Select an internal verifier origin and a trusted public login URL. Protected apps can be on separate domains if Authelia is configured with matching session cookies and authentication URLs.",
"authentikHelp": "Calls /outpost.goauthentik.io/auth/nginx; RustProxy forwards the /outpost.goauthentik.io path to the same outpost, without requiring separate Custom Locations. Use an authentik Forward auth (single application) provider for each protected app, including apps on different domains.",
"authBasic": "HTTP Basic Auth",
"basicRealm": "Realm (login prompt)",
"basicRequireTls": "Require HTTPS for Basic Auth",
@@ -883,6 +945,7 @@
"docsWrites": "Enable write operations",
"docsWriteConfirm": "This sends a real change to RustProxy. Continue?",
"docsExpired": "Session ended. Sign in again.",
"docsImage": "Screenshot",
"docsError": "Documentation could not be loaded.",
"docsAccount": "Signed in as",
"docsReadOnly": "Read-only execution by default. SSE streams and login/setup are not executed here.",
@@ -1459,7 +1522,7 @@
"vhBackendHeader": "Expose selected backend (X-RustProxy-Backend)",
"vhBackendTimingHeader": "Expose backend response time (X-RustProxy-Backend-Time)",
"vhBackendDiagnosticsHelp": "Opt-in response headers. Backend time measures origin request-to-response-headers latency; responses served without contacting an origin do not report a backend.",
"authModeHelp": "Choose one authentication provider. The IP access list below is independent and can be combined with either provider.",
"authModeHelp": "Select an identity provider and a supported integration method. Forward Auth checks sessions via the provider; OIDC uses central login with a callback on the protected application. Existing Vhost integrations remain supported.",
"taAdvanced": "Advanced Tinyauth settings",
"notfound": "404 hosts",
"notfoundTitle": "404 hosts, under control.",
@@ -2191,11 +2254,21 @@
"about": "About",
"aboutTitle": "About RustProxy",
"aboutAuthor": "Author",
"aboutLicenseText": "Full license text",
"aboutDocumentation": "Documentation and API reference",
"aboutLicensePointWarranty": "The software is provided without any warranty.",
"aboutLicensePointNetwork": "If you run a modified version as a network service, its users must be able to obtain the modified source code.",
"aboutLicensePointShare": "Distributed copies and modified versions must stay under the same license and include their source code.",
"aboutLicensePointUse": "Use it for any purpose, including commercial deployments.",
"aboutLicenseTitle": "License summary",
"aboutSpdx": "SPDX identifier",
"aboutCopyright": "Copyright",
"aboutTagline": "Reverse proxy, web server, certificate manager and traffic monitor in a single service with a built-in administration panel.",
"aboutLicense": "License",
"aboutLicenseValue": "GNU AGPL v3",
"aboutOpenSourceText": "RustProxy is an open-source project. Its source code is publicly available for inspection, development and community contribution.",
"aboutRepository": "Official source repository",
"aboutPackagesRepository": "Official package repository",
"aboutLicenseValue": "GNU AGPL v3.0 or later",
"aboutOpenSourceText": "RustProxy is free and open-source software. You may use, study, modify and redistribute it under the terms of the GNU Affero General Public License, version 3 or any later version.",
"aboutRepository": "Source code",
"aboutPackagesRepository": "Website and packages",
"aboutPythonRuntime": "Certificate Python runtime",
"runtimePython": "Python",
"runtimePip": "pip",
@@ -2699,6 +2772,22 @@
"overviewExtraCacheMissRateDetail": "Rate of cache misses.",
"overviewExtraBackendActive": "In-flight backend requests",
"overviewExtraBackendActiveDetail": "Current HTTP upstream requests across backends.",
"overviewExtraBackendsHealthy": "Healthy backends",
"overviewExtraBackendsHealthyDescription": "Monitored backend members that currently pass health checks, with the names of failing members.",
"overviewExtraBackendsNone": "No monitored backends",
"overviewExtraBackendsDown": "Down",
"overviewExtraBackendsAllUp": "All monitored backends are up",
"overviewExtraErrorShare": "Error share",
"overviewExtraErrorShareDescription": "Share of HTTP requests that ended with an error in the last sampling interval.",
"overviewExtraErrorShareDetail": "Errors / requests",
"overviewExtraRejectedRate": "Rejected by limits / s",
"overviewExtraRejectedRateDescription": "Requests and connections rejected by concurrency limits. Sustained values suggest raising max-connections or max-inflight.",
"overviewExtraRejectedDetail": "Total requests / connections",
"overviewExtraCertExpiry": "Nearest certificate expiry",
"overviewExtraCertExpiryDescription": "Days until the earliest installed certificate expires; the bar is full at 90 days or more.",
"overviewExtraCertNone": "No certificates installed",
"overviewExtraCertCount": "certificates",
"overviewExtraDaysUnit": "days",
"overviewWideProtocolRates": "HTTP/1.1 · HTTP/2 · HTTP/3 traffic",
"overviewWideProtocolRatesDetail": "Requests per second by frontend protocol.",
"overviewWideCpu": "System and process CPU",
+96 -7
View File
@@ -18,6 +18,44 @@
"streamBackendHealthSection": "Kontrola dostępności",
"fileLogsSortAscending": "Sortuj rosnąco",
"fileLogsSortDescending": "Sortuj malejąco",
"globalBlockAllow": "Globalne blokady / zezwolenia",
"globalSecurityHeaders": "Nagłówki bezpieczeństwa",
"globalPolicyScope": "PUBLICZNE LISTENERY HTTP",
"globalPolicyHelp": "Reguły dotyczą publicznego ruchu HTTP i są sprawdzane przed połączeniem z backendem, także przy przekierowaniach i unieważnianiu cache. Reguły globalne i per-vhost działają łącznie. Wyłączone opcje nie zmieniają dotychczasowego działania. Reguły krajów wymagają danych GeoIP.",
"globalCountryTitle": "Dostęp według kraju",
"globalCountryHelp": "Blokuj wskazane kraje albo zezwalaj tylko na nie (dwuliterowe kody ISO 3166-1 alpha-2). W trybie zezwalania nierozpoznany kraj jest blokowany. Aktualizacje GeoIP ustawisz w Settings.",
"globalCountryMode": "Polityka krajów",
"globalCountryList": "Kody krajów (każdy w nowym wierszu lub po przecinku)",
"globalUaTitle": "Blokada User-Agent",
"globalUaEnabled": "Włącz globalną blokadę User-Agent",
"globalIpTitle": "Dostęp według IP / CIDR / DNS",
"globalIpHelp": "Opcjonalne reguły allow/deny dla IP, podsieci i nazw DNS (DNS tylko allow). Blokada ma pierwszeństwo. Akcja domyślna obowiązuje, gdy nic nie pasuje.",
"globalIpEnabled": "Włącz reguły IP / CIDR",
"globalIpDefault": "Akcja domyślna",
"globalIpRules": "Reguły (allow lub deny oraz IP/CIDR; DNS tylko allow)",
"globalMethodTitle": "Metody HTTP",
"globalMethodHelp": "Blokuj wybrane metody albo zezwalaj tylko na wskazane. Działa również dla przekierowań i unieważniania cache; odmowa zwraca 405.",
"globalMethodMode": "Polityka metod",
"globalMethodList": "Metody HTTP (oddzielone przecinkami, spacjami lub nową linią)",
"globalContentTypeTitle": "Content-Type żądania",
"globalContentTypeHelp": "Blokuj zadeklarowane typy MIME (dokładnie lub typ/*) kodem 403. Sprawdzany jest nagłówek, nie zawartość żądania.",
"globalContentTypeEnabled": "Włącz blokadę Content-Type",
"globalContentTypeList": "Blokowane typy MIME (każdy w nowym wierszu)",
"globalRateTitle": "Wspólny limit żądań",
"globalRateHelp": "Opcjonalny limit na adres IP klienta, wspólny dla wszystkich publicznych vhostów. Działa obok limitów per-vhost. Zbyt niski limit może blokować klientów korzystających z jednego adresu.",
"globalRateEnabled": "Włącz globalny limit żądań",
"globalRateRequests": "Żądania na przedział",
"globalRateWindow": "Przedział (sekundy)",
"globalRateBurst": "Dodatkowe żądania (burst)",
"globalPolicySave": "Zastosuj globalne blokady / zezwolenia",
"globalHeadersSecurityHelp": "Globalne nagłówki dla odpowiedzi publicznych vhostów HTTP, przekierowań i stron 404. Jawnie ustawione nagłówki vhosta mają pierwszeństwo. HSTS jest wysyłane tylko przez HTTPS; przed wdrożeniem sprawdź działanie CSP.",
"globalSecurityHeadersSave": "Zastosuj nagłówki bezpieczeństwa",
"globalCountriesRequired": "Wpisz przynajmniej jeden kod kraju lub wyłącz politykę krajów.",
"globalCountriesInvalid": "Użyj dwuliterowego kodu kraju, np. PL lub DE.",
"globalMethodsRequired": "Wpisz przynajmniej jedną metodę HTTP lub wyłącz politykę metod.",
"globalContentTypesRequired": "Wpisz przynajmniej jeden typ MIME lub wyłącz blokadę Content-Type.",
"cacheHeadersSection": "Nagłówki odpowiedzi cache",
"transformOperationsTitle": "Operacje transformacji",
"policyRulesTitle": "Reguły i nagłówki",
"vhUaRateTitle": "Limity dla User-Agent",
"vhUaRatePattern": "Wzorzec User-Agent",
@@ -419,6 +457,9 @@
"vhHttp": "Status HTTP + czas odpowiedzi",
"vhHealthPath": "Ścieżka testu HTTP",
"vhHealthHost": "Własny Host testu HTTP (opcjonalny)",
"vhHealthPort": "Port testu zdrowia (opcjonalny)",
"vhHealthPortPlaceholder": "Port backendu",
"vhHealthExpectBody": "Oczekiwany tekst w odpowiedzi HTTP (opcjonalny)",
"vhInterval": "Interwał (ms)",
"vhTimeout": "Limit czasu (ms)",
"vhSlow": "Próg wolnej odpowiedzi (ms; 0 = wył.)",
@@ -426,7 +467,7 @@
"vhRise": "Sukcesy przed przywróceniem",
"vhStatusMin": "Minimalny status HTTP",
"vhStatusMax": "Maksymalny status HTTP",
"vhHealthNote": "Sondy HTTP używają HTTP/1.1 i mierzą czas do nagłówków, nie czas całego body. Gdy pole Host jest puste, RustProxy automatycznie używa pierwszej dokładnej domeny DNS vhosta; jeśli jej nie ma, używa hosta backendu. Dla usług tylko h2c użyj TCP. Nowe backendy czekają na udaną sondę. Active health jest niezależny od passive health.",
"vhHealthNote": "Sondy HTTP używają HTTP/1.1 i mierzą czas do nagłówków, nie czas całego body. Gdy pole Host jest puste, RustProxy automatycznie używa pierwszej dokładnej domeny DNS vhosta; jeśli jej nie ma, używa hosta backendu. Dla usług tylko h2c użyj TCP. Nowe backendy czekają na udaną sondę. Active health jest niezależny od passive health. Port testu zdrowia pozwala sprawdzać inny port niż ruch (np. port statusu 8079 dla backendu na porcie 80); host, TLS i ograniczenia adresów pozostają te same. Oczekiwany tekst oznacza backend jako niedostępny, jeśli pierwsze 64 KiB odpowiedzi testu HTTP go nie zawiera.",
"vhPassiveHealth": "Pasywne zdrowie backendu",
"vhPassiveEnabled": "Pozwól, aby błędy zwykłych requestów oznaczały backend jako niedostępny",
"vhPassiveHelp": "Domyślnie wyłączone. Błędy nadal są logowane i zliczane, ale zwykły ruch użytkowników nie zmienia stanu backendu bez świadomego włączenia.",
@@ -605,7 +646,6 @@
"certSelectHint": "Wybierz zaimportowany certyfikat. Import i rotacja są w zakładce Certyfikaty. Przypisania są sprawdzane względem nazw vhosta.",
"vhTinyauth": "Uwierzytelnianie",
"vhTabAuth": "Uwierzytelnianie",
"taEnable": "Włącz ochronę Tinyauth",
"taUrl": "URL Tinyauth / punkt weryfikacji",
"taLogin": "Publiczny URL logowania",
"taRedirectForbidden": "Akceptuj przekierowanie Tinyauth dla 403 (ten sam origin)",
@@ -820,9 +860,31 @@
"aclNetwork": "Adres IP/CIDR lub nazwa DNS",
"aclAction": "Akcja",
"aclAdd": "Dodaj regułę IP",
"authProvider": "Dostawca tożsamości",
"authMethod": "Metoda integracji",
"authForwardAuth": "Forward Auth",
"authPocketId": "Pocket ID",
"authCustomOidc": "Inny dostawca OIDC",
"authMode": "Uwierzytelnianie",
"authNone": "Brak",
"authTinyauth": "Tinyauth",
"authAuthelia": "Authelia",
"authAuthentik": "authentik",
"authOidc": "OIDC (logowanie centralne)",
"oidcIssuer": "Wystawca OIDC (Issuer HTTPS)",
"oidcBackend": "Adres wewnętrzny usługi OIDC (opcjonalny)",
"oidcRedirect": "Adres callbacku na domenie aplikacji (HTTPS)",
"oidcClient": "Client ID",
"oidcSecret": "Client Secret",
"oidcSession": "Sekret sesji RustProxy (min. 32 znaki)",
"oidcScopes": "Zakresy OIDC (scopes)",
"oidcTtl": "Czas sesji (sekundy)",
"oidcHelp": "Centralne logowanie między domenami przez OIDC Authorization Code + PKCE. Zarejestruj dokładnie ten adres callbacku u dostawcy. Wymaga zaufanego HTTPS, sekretu sesji i klienta poufnego (client_secret_basic). Obsługiwane podpisy ID tokenu: RS256/ES256. Wszystkie adresy discovery muszą używać originu Issuer. Sesja aplikacji jest osobna; RustProxy przekazuje zweryfikowane Remote-User, Remote-Sub i opcjonalne atrybuty.",
"forwardAuthUrl": "Adres usługi autoryzacji (wewnętrzny)",
"forwardAuthLogin": "Publiczny adres logowania Authelia (HTTPS)",
"forwardAuthAdvanced": "Zaawansowane ustawienia dostawcy",
"autheliaHelp": "Weryfikacja przez /api/authz/forward-auth. Podaj wewnętrzny adres Authelii i publiczny adres logowania HTTPS. Dla różnych domen skonfiguruj odpowiednie cookies i adresy portali w Authelii.",
"authentikHelp": "Weryfikacja przez /outpost.goauthentik.io/auth/nginx. RustProxy automatycznie przekazuje ścieżkę /outpost.goauthentik.io do outpostu, bez dodatkowych lokalizacji. Wybierz Forward auth (single application) dla każdej aplikacji, również na innych domenach.",
"authBasic": "HTTP Basic Auth",
"basicRealm": "Realm (nazwa logowania)",
"basicRequireTls": "Wymagaj HTTPS dla Basic Auth",
@@ -883,6 +945,7 @@
"docsWrites": "Włącz operacje zapisu",
"docsWriteConfirm": "Ta operacja naprawdę zmieni RustProxy. Kontynuować?",
"docsExpired": "Sesja wygasła. Zaloguj się ponownie.",
"docsImage": "Zrzut ekranu",
"docsError": "Nie udało się wczytać dokumentacji.",
"docsAccount": "Zalogowano jako",
"docsReadOnly": "Domyślnie wykonywane są tylko odczyty. Strumienie SSE oraz logowanie i setup nie są tu uruchamiane.",
@@ -1459,7 +1522,7 @@
"vhBackendHeader": "Udostępniaj wybrany backend (X-RustProxy-Backend)",
"vhBackendTimingHeader": "Udostępniaj czas odpowiedzi backendu (X-RustProxy-Backend-Time)",
"vhBackendDiagnosticsHelp": "Opcjonalne nagłówki odpowiedzi. Czas backendu oznacza czas od wysłania żądania do otrzymania nagłówków odpowiedzi; odpowiedzi obsłużone bez kontaktu z originem nie raportują backendu.",
"authModeHelp": "Wybierz jedną metodę uwierzytelniania. Lista dostępu IP poniżej działa niezależnie i może być używana razem z dowolną metodą.",
"authModeHelp": "Wybierz dostawcę tożsamości i obsługiwaną metodę integracji. Forward Auth sprawdza sesję u dostawcy, a OIDC zapewnia logowanie centralne z callbackiem na domenie chronionej aplikacji. Stare konfiguracje Vhostów pozostają obsługiwane.",
"taAdvanced": "Zaawansowane ustawienia Tinyauth",
"notfound": "Hosty 404",
"notfoundTitle": "Hosty 404 pod kontrolą.",
@@ -2191,11 +2254,21 @@
"about": "O projekcie",
"aboutTitle": "O RustProxy",
"aboutAuthor": "Autor",
"aboutLicenseText": "Pełny tekst licencji",
"aboutDocumentation": "Dokumentacja i dokumentacja API",
"aboutLicensePointWarranty": "Oprogramowanie jest dostarczane bez jakiejkolwiek gwarancji.",
"aboutLicensePointNetwork": "Jeśli udostępniasz zmodyfikowaną wersję jako usługę sieciową, jej użytkownicy muszą mieć dostęp do zmodyfikowanego kodu źródłowego.",
"aboutLicensePointShare": "Rozpowszechniane kopie i wersje zmodyfikowane muszą pozostać na tej samej licencji i zawierać kod źródłowy.",
"aboutLicensePointUse": "Możesz go używać w dowolnym celu, także komercyjnie.",
"aboutLicenseTitle": "Licencja w skrócie",
"aboutSpdx": "Identyfikator SPDX",
"aboutCopyright": "Prawa autorskie",
"aboutTagline": "Reverse proxy, serwer WWW, menedżer certyfikatów i monitor ruchu w jednej usłudze z wbudowanym panelem administracyjnym.",
"aboutLicense": "Licencja",
"aboutLicenseValue": "GNU AGPL v3",
"aboutOpenSourceText": "RustProxy jest projektem open source. Kod źródłowy jest publicznie dostępny do wglądu, rozwoju i współtworzenia.",
"aboutRepository": "Oficjalne repozytorium źródłowe",
"aboutPackagesRepository": "Oficjalne repozytorium pakietów",
"aboutLicenseValue": "GNU AGPL v3.0 lub nowsza",
"aboutOpenSourceText": "RustProxy jest wolnym oprogramowaniem o otwartym kodzie. Możesz go używać, analizować, modyfikować i rozpowszechniać na warunkach licencji GNU Affero General Public License w wersji 3 lub dowolnej późniejszej.",
"aboutRepository": "Kod źródłowy",
"aboutPackagesRepository": "Strona i pakiety",
"aboutPythonRuntime": "Środowisko Python certyfikatów",
"runtimePython": "Python",
"runtimePip": "pip",
@@ -2699,6 +2772,22 @@
"overviewExtraCacheMissRateDetail": "Tempo nietrafień w cache.",
"overviewExtraBackendActive": "Żądania do backendów",
"overviewExtraBackendActiveDetail": "Suma aktualnych żądań do backendów HTTP.",
"overviewExtraBackendsHealthy": "Sprawne backendy",
"overviewExtraBackendsHealthyDescription": "Monitorowane backendy, które obecnie przechodzą testy zdrowia, wraz z nazwami niedziałających.",
"overviewExtraBackendsNone": "Brak monitorowanych backendów",
"overviewExtraBackendsDown": "Niedostępne",
"overviewExtraBackendsAllUp": "Wszystkie monitorowane backendy działają",
"overviewExtraErrorShare": "Udział błędów",
"overviewExtraErrorShareDescription": "Odsetek żądań HTTP zakończonych błędem w ostatnim interwale próbkowania.",
"overviewExtraErrorShareDetail": "Błędy / żądania",
"overviewExtraRejectedRate": "Odrzucone przez limity / s",
"overviewExtraRejectedRateDescription": "Żądania i połączenia odrzucone przez limity współbieżności. Stałe wartości oznaczają, że warto podnieść max-connections lub max-inflight.",
"overviewExtraRejectedDetail": "Łącznie żądania / połączenia",
"overviewExtraCertExpiry": "Najbliższe wygaśnięcie certyfikatu",
"overviewExtraCertExpiryDescription": "Dni do wygaśnięcia najwcześniej kończącego się certyfikatu; pasek jest pełny przy 90 dniach lub więcej.",
"overviewExtraCertNone": "Brak zainstalowanych certyfikatów",
"overviewExtraCertCount": "certyfikatów",
"overviewExtraDaysUnit": "dni",
"overviewWideProtocolRates": "Ruch HTTP/1.1 · HTTP/2 · HTTP/3",
"overviewWideProtocolRatesDetail": "Tempo żądań według protokołu frontendu.",
"overviewWideCpu": "CPU systemu i procesu",
+2 -4
View File
@@ -15,7 +15,7 @@ ROOT = Path(__file__).resolve().parent
MANAGED = [
'src', 'tests', 'scripts', 'tools', 'integrations', 'nagios', 'docker-agent', 'migrations', 'examples', 'docs', 'langs', 'deploy', 'grafana',
'.github', '.cargo', 'Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'UPDATE_CHECK.toml',
'dev.sh', 'upgrade.sh', 'install.sh', 'install-debian13.sh', 'update.sh', 'README.md', 'ROADMAP.md',
'dev.sh', 'upgrade.sh', 'install.sh', 'install-debian13.sh', 'update.sh', 'README.md', 'LICENSE',
'CHANGELOG.md', 'AGENTS.md', '.env.example', '.gitignore', '.dockerignore',
'Dockerfile', 'make_zip.py',
]
@@ -39,9 +39,7 @@ def files() -> list[Path]:
rel = file.relative_to(ROOT)
if any(part in EXCLUDED for part in rel.parts) or file.suffix in ('.pyc', '.pyo', '.zip'):
continue
# Never ship local environment files from recursively managed trees.
# Explicit templates such as .env.example remain safe because they are
# named in MANAGED and do not have the exact basename `.env`.
# Never ship local .env files; templates such as .env.example are listed explicitly.
if rel.name == '.env':
continue
if rel.as_posix() == 'docker-agent/docker-agent':
+1 -3
View File
@@ -99,9 +99,7 @@ def structural_inventory(paths: list[Path]) -> dict:
families = [tuple(sorted({family for selector in entry['selectors']
for family in VENDOR_SELECTOR.findall(selector)}))
for entry in entries]
# A browser can reject an entire comma-separated selector list when it
# contains another engine's pseudo-element. Keep one body per compatible
# family, while still rejecting copies within the same family.
# Vendor pseudo-element families legitimately repeat one rule body.
vendor_only = (any(families) and all(len(family) <= 1 for family in families)
and len(families) == len(set(families)))
repeated_bodies.append({'scope': list(scope), 'rules': entries,
+1 -1
View File
@@ -33,7 +33,7 @@ ASSETS = {
LICENSE_MEMBERS = ("LICENSE", "NOTICE", "dist/swagger-ui-bundle.js.LICENSE.txt")
HELP = ("Install Python 3.10+ and allow HTTPS access to github.com/codeload.github.com, "
"or set SWAGGER_UI_DOWNLOAD_URL=file:///absolute/path/swagger-ui-5.33.1.zip "
"to a reviewed copy of the same tag archive. See docs/API_ACCESS.md.")
"to a reviewed copy of the same tag archive. See docs/INSTALL.md.")
class AssetError(ValueError):
+4 -10
View File
@@ -211,9 +211,7 @@ def admin_health_endpoint(record: dict) -> tuple[str, bool, str]:
if isinstance(values, list):
allowed_hosts = [str(value).strip() for value in values if str(value).strip()]
except (OSError, sqlite3.Error, ValueError, TypeError, json.JSONDecodeError):
# Old releases and damaged/uninitialized databases used the startup HTTP
# listener. The real startup diagnostics remain authoritative if this
# compatibility read cannot resolve persisted settings.
# Fall back to the startup listener for old or uninitialized databases.
pass
host, port = (raw[1:].split(']:') if raw.startswith('[') else raw.rsplit(':', 1))
@@ -234,9 +232,7 @@ def healthy(record: dict, version: str, attempts: int = 30) -> bool:
host, port = (raw[1:].split(']:') if raw.startswith('[') else raw.rsplit(':', 1))
for _ in range(attempts):
if tls:
# Local deployment health is process identity/liveness, not PKI validation.
# The configured admin certificate may intentionally be self-signed or for
# a DNS name while we connect to the local socket.
# Local liveness probe only; the admin certificate may be self-signed.
conn = http.client.HTTPSConnection(host, int(port), timeout=2, context=ssl._create_unverified_context())
else:
conn = http.client.HTTPConnection(host, int(port), timeout=2)
@@ -362,14 +358,12 @@ def update(record: dict, binary: Path, sha: str, start: bool, refresh_geoip: boo
# Repair missing or stale installer-owned shell setup on upgrades.
install_shell_path(prefix)
check_shell_path(prefix)
# Repair stale root:root/0700 metadata before staging/network work and before
# stopping the currently healthy service.
# Repair stale ownership before stopping the running service.
ensure_service_data_access(data, account)
release, version = stage(binary, prefix, record['user'], sha)
geoip_names: list[str] = []
if refresh_geoip:
# Keep the same lock for checksum/download staging and publication. The
# runtime worker uses this file too, so manual upgrades cannot race it.
# Shared lock with the runtime worker prevents concurrent upgrades.
geoip_lock_fd = acquire_geoip_update_lock(data, account.pw_uid, account.pw_gid)
geoip_stage, geoip_names = prepare_geoip_update(data, prefix)
active = subprocess.run(['systemctl', 'is-active', '--quiet', SERVICE], check=False, stdout=subprocess.DEVNULL).returncode == 0
+2 -7
View File
@@ -13,7 +13,7 @@ from scripts.version import project_version
MANAGED = ["build.rs", "langs", "deploy", "debian", "grafana", "make_zip.py", "src", "tests", "scripts", "tools", "integrations", "nagios", "docker-agent", "migrations", "examples", "docs", ".github", ".cargo",
"Cargo.toml", "Cargo.lock", "rust-toolchain.toml", "dev.sh", "upgrade.sh", "install.sh", "update.sh",
"README.md", "ROADMAP.md", "CHANGELOG.md", "AGENTS.md", "SHA256SUMS",
"README.md", "LICENSE", "CHANGELOG.md", "AGENTS.md", "SHA256SUMS",
".env.example", ".gitignore"]
@@ -72,12 +72,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None
elif new.is_file():
installed.append(name)
shutil.copy2(new, old)
# Old renewal observations were diagnostic scheduler state only;
# certificates, accounts and renewal profiles live elsewhere. 0.19.1
# has no runtime JSON importer. Validate the obsolete sidecar now,
# write the completed source-upgrade manifest, then remove the file
# as the final fallible operation. SQLite observations are rebuilt by
# the normal renewal scheduler. No second legacy copy is retained.
# Obsolete renewal sidecar: validate, record the manifest, then remove it last.
runtime_data = (data_dir or (target / "data")).resolve()
certs_dir = runtime_data / "certs"
if certs_dir.is_symlink():
+1 -1
View File
@@ -29,7 +29,7 @@ import urllib.error
import urllib.parse
import urllib.request
MAX_CONFIG_BYTES = 262_144
MAX_CONFIG_BYTES = 2_097_152
class MigrationError(RuntimeError):
+1 -1
View File
@@ -132,7 +132,7 @@ def check(root: Path, repair_known: bool = False) -> list[str]:
flat = module.parent.with_suffix(".rs")
if flat.is_file():
raise ValueError(f"Conflicting Rust modules: {flat.relative_to(root)} and {module.relative_to(root)}")
for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/config/internal_routing.rs", "src/proxy/websocket.rs", "src/proxy/error_fallback.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression/mod.rs", "src/diagnostics.rs", "src/traffic/mod.rs", "src/system_stats/mod.rs", "src/system_stats/linux/mod.rs", "src/vhost.rs", "src/vhost/error_fallback.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security/mod.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/rates.js", "src/admin/web/vhosts/health.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/app/core.js", "src/admin/web/app/session-resume.js", "src/admin/web/app/preferences.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/admin/web/drafts.js", "src/pages/mod.rs", "tools/dev/mock_server.py", "tools/dev/mock_auth.py", "src/forwarding.rs", "src/tinyauth/mod.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "CHANGELOG.md", "ROADMAP.md", "scripts/runtime/acme_provider.py", "src/basic_auth/mod.rs", "scripts/deploy/deploy.py", "scripts/deploy/deploy_core.py", "scripts/deploy/deploy_runtime.py", "scripts/migration/migrate_npm.py", "scripts/migration/migrate_npm_core.py", "scripts/migration/migrate_npm_api.py", "docs/MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/build/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/MONITORING.md", "docs/CUSTOM_LOCATIONS.md", "grafana/rustproxy-overview.json"]:
for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/config/internal_routing.rs", "src/proxy/websocket.rs", "src/proxy/error_fallback.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression/mod.rs", "src/diagnostics.rs", "src/traffic/mod.rs", "src/system_stats/mod.rs", "src/system_stats/linux/mod.rs", "src/vhost.rs", "src/vhost/error_fallback.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security/mod.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/rates.js", "src/admin/web/vhosts/health.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/app/core.js", "src/admin/web/app/session-resume.js", "src/admin/web/app/preferences.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/admin/web/drafts.js", "src/pages/mod.rs", "tools/dev/mock_server.py", "tools/dev/mock_auth.py", "src/forwarding.rs", "src/tinyauth/mod.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "CHANGELOG.md", "LICENSE", "docs/FEATURES.md", "docs/PERFORMANCE.md", "scripts/runtime/acme_provider.py", "src/basic_auth/mod.rs", "scripts/deploy/deploy.py", "scripts/deploy/deploy_core.py", "scripts/deploy/deploy_runtime.py", "scripts/migration/migrate_npm.py", "scripts/migration/migrate_npm_core.py", "scripts/migration/migrate_npm_api.py", "docs/MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/build/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/INSTALL.md", "docs/MONITORING.md", "docs/CUSTOM_LOCATIONS.md", "grafana/rustproxy-overview.json"]:
if not (root / path).is_file():
raise ValueError(f"Incomplete RustProxy 0.34.2 source tree: missing {path}")
for path in ("scripts/__init__.py", "scripts/build/__init__.py", "scripts/build/panel_css.py", "src/admin/web/css/manifest.json"):
+7 -15
View File
@@ -39,8 +39,7 @@ PROVIDER_FIELDS = {
def credential(value: object) -> str:
# All supported API keys are ASCII tokens. Reject whitespace/newlines and
# INI metacharacters; provider input must never add options or sections.
# API keys are ASCII tokens; reject anything that could inject INI content.
if not isinstance(value, str) or not re.fullmatch(r"[A-Za-z0-9_./+\-=]{8,4096}", value):
raise AcmeError("acme.invalid_api_credential")
return value
@@ -196,8 +195,7 @@ def certbot_command(value: dict, work: Path, credential_file: Path, command_pref
def read_generated(work: Path, name: str) -> bytes:
# Certbot live entries are symlinks into its private archive. Resolve only
# within that archive, then open without following a second/final symlink.
# Resolve live symlinks only inside Certbot's archive.
archive = (work / "config" / "archive").resolve(strict=True)
path = (work / "config" / "live" / "rustproxy" / name).resolve(strict=True)
if not path.is_relative_to(archive):
@@ -220,8 +218,7 @@ def issue(raw: dict, certificate_directory: Path, executable: str | None = None)
private_directory(certificate_directory)
root = certificate_directory / "acme"
private_directory(root)
# Each identity/environment has isolated CA accounts, DNS credentials and
# Certbot lineages. Both profile IDs and directory paths are operator-owned.
# Each profile has isolated CA accounts, credentials and lineages.
suffix = "" if value["provider"] == "rfc2136" else "-" + value["provider"]
work = root / (value["environment"] + "-" + value["certificate_id"] + suffix)
private_directory(work)
@@ -236,9 +233,7 @@ def issue(raw: dict, certificate_directory: Path, executable: str | None = None)
raise AcmeError("acme.profile_busy") from None
for folder in ["config", "work", "logs"]:
private_directory(work / folder)
# DNS credentials are supplied by RustProxy's encrypted Secret Store.
# Certbot needs a file path, but the plaintext file exists only while the
# provider command runs and is deleted before this worker returns.
# Secret Store credentials exist as a plaintext file only while Certbot runs.
credential_file = work / (value["provider"] + ".ini")
if value["provider"] == "http01":
webroot = certificate_directory / "acme-http01"
@@ -265,8 +260,7 @@ def issue(raw: dict, certificate_directory: Path, executable: str | None = None)
credential_file.unlink(missing_ok=True)
cert = read_generated(work, "fullchain.pem")
key = read_generated(work, "privkey.pem")
# Immutable copies allow validation/publication without changing the
# active certificate during CA work or a concurrent configuration edit.
# Work on immutable copies so the active certificate never changes mid-run.
stem = "acme-" + uuid.uuid4().hex
cert_path = certificate_directory / (stem + ".pem")
key_path = certificate_directory / (stem + ".key")
@@ -392,8 +386,7 @@ def renew(raw: dict, certificate_directory: Path, executable: str | None = None)
if not isinstance(names, list) or not 1 <= len(names) <= 16:
raise AcmeError("acme.invalid_domains")
names = [domain(n, wildcard=value["provider"] != "http01") for n in names]
# The renewal config persists, but provider credentials do not. Recreate
# the exact path referenced by Certbot only for this invocation.
# Credentials are recreated only for the duration of this invocation.
read_private(work / "config" / "renewal" / "rustproxy.conf", 65536)
credential_file = work / (value["provider"] + ".ini")
if value["provider"] != "http01":
@@ -436,8 +429,7 @@ def renew(raw: dict, certificate_directory: Path, executable: str | None = None)
return output
if dates["not_after_unix"] < old_dates["not_after_unix"]:
raise AcmeError("acme.renewal_older_certificate")
# Content-addressed immutable staging makes a retry after a publication
# failure idempotent (no additional orphan PEMs each time).
# Content-addressed staging keeps retries idempotent.
stem = "acme-" + hashlib.sha256(cert + b"\0" + key).hexdigest()[:48]
paths = [(certificate_directory / (stem + ".pem"), cert), (certificate_directory / (stem + ".key"), key)]
if len(list(certificate_directory.glob("acme-*.key"))) >= 1024 and not paths[1][0].exists():
-1
View File
@@ -1,7 +1,6 @@
#!/usr/bin/env bash
# Start two TCP and two UDP mock backends for RustProxy Stream tests.
# Usage: ./start_stream_backends.sh [bind-host]
# Defaults to loopback for safety. Use 0.0.0.0 only on a controlled test network.
set -euo pipefail
DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
+49 -21
View File
@@ -58,13 +58,17 @@ impl Acl {
ensure!(rule.network.len() <= 254, "ACL network is too long");
let target = if let Ok(network) = parse_network(&rule.network) {
if let IpNet::V6(net) = network {
ensure!(net.addr().to_ipv4_mapped().is_none(),
"use IPv4 CIDR instead of IPv4-mapped IPv6 in ACL rules");
ensure!(
net.addr().to_ipv4_mapped().is_none(),
"use IPv4 CIDR instead of IPv4-mapped IPv6 in ACL rules"
);
}
RuleMatch::Ip(network)
} else {
ensure!(rule.action == Action::Allow,
"dynamic DNS ACL rules only support allow; use a static IP/CIDR for deny");
ensure!(
rule.action == Action::Allow,
"dynamic DNS ACL rules only support allow; use a static IP/CIDR for deny"
);
RuleMatch::Dns(dynamic::hostname(&rule.network)?)
};
rules.push((target, rule.action));
@@ -94,9 +98,16 @@ pub fn allows_profiles_with_country(
let mut explicit_allow = false;
let mut defaults_allow = true;
for profile in profiles {
let ip_action = profile.acl.as_ref().and_then(|acl| acl.explicit_action(peer));
let ip_action = profile
.acl
.as_ref()
.and_then(|acl| acl.explicit_action(peer));
let country_listed = country.is_some_and(|code| {
profile.countries.countries.iter().any(|value| value.eq_ignore_ascii_case(code))
profile
.countries
.countries
.iter()
.any(|value| value.eq_ignore_ascii_case(code))
});
if ip_action == Some(Action::Deny)
|| (profile.countries.mode == crate::rate_limit::CountryMode::Block && country_listed)
@@ -106,20 +117,32 @@ pub fn allows_profiles_with_country(
explicit_allow |= ip_action == Some(Action::Allow)
|| (profile.countries.mode == crate::rate_limit::CountryMode::Allow && country_listed);
defaults_allow &= profile.countries.mode != crate::rate_limit::CountryMode::Allow
&& profile.acl.as_ref().is_none_or(|acl| acl.default == Action::Allow);
&& profile
.acl
.as_ref()
.is_none_or(|acl| acl.default == Action::Allow);
}
explicit_allow || defaults_allow
}
pub fn profile_ids<'a>(legacy: Option<&'a str>, ids: &'a [String]) -> impl Iterator<Item = &'a str> {
pub fn profile_ids<'a>(
legacy: Option<&'a str>,
ids: &'a [String],
) -> impl Iterator<Item = &'a str> {
legacy.into_iter().chain(ids.iter().map(String::as_str))
}
pub fn validate_profile_ids(legacy: Option<&str>, ids: &[String]) -> Result<()> {
ensure!(ids.len() + (if legacy.is_some() { 1 } else { 0 }) <= 8, "at most 8 ACL profiles may be assigned");
ensure!(
ids.len() + (if legacy.is_some() { 1 } else { 0 }) <= 8,
"at most 8 ACL profiles may be assigned"
);
let mut seen = std::collections::HashSet::new();
for id in profile_ids(legacy, ids) {
ensure!(crate::config::valid_id(id) && seen.insert(id), "invalid or duplicate ACL profile id");
ensure!(
crate::config::valid_id(id) && seen.insert(id),
"invalid or duplicate ACL profile id"
);
}
Ok(())
}
@@ -181,9 +204,7 @@ pub fn allows_with_country(
&& (ip_action == Some(Action::Allow)
|| acl.is_none_or(|value| value.default == Action::Allow))
}
crate::rate_limit::CountryMode::Allow => {
ip_action == Some(Action::Allow) || country_listed
}
crate::rate_limit::CountryMode::Allow => ip_action == Some(Action::Allow) || country_listed,
}
}
pub fn validate_decoded_routing_path(path: &str) -> Result<()> {
@@ -220,7 +241,8 @@ pub fn routing_path_view(raw: &str) -> Result<std::borrow::Cow<'_, str>> {
);
}
ensure!(
!raw.split('/').any(|segment| segment == "." || segment == ".."),
!raw.split('/')
.any(|segment| segment == "." || segment == ".."),
"ambiguous path segments"
);
Ok(std::borrow::Cow::Borrowed(raw))
@@ -242,13 +264,19 @@ pub fn routing_path(raw: &str) -> Result<String> {
let high = digit(bytes[index + 1]).context("invalid path escape")?;
let low = digit(bytes[index + 2]).context("invalid path escape")?;
let decoded = (high << 4) | low;
ensure!(
decoded >= 32
&& decoded != 127
&& !matches!(decoded, b'/' | b'\\' | b'%' | b'?' | b'#' | b';'),
"ambiguous encoded path character"
);
out.push(decoded);
if decoded == b'/' {
if out.last().copied() != Some(b'/') {
out.push(b'/');
}
} else {
ensure!(
decoded >= 32
&& decoded != 127
&& !matches!(decoded, b'\\' | b'%' | b'?' | b'#' | b';'),
"ambiguous encoded path character"
);
out.push(decoded);
}
index += 3;
} else {
ensure!(
+75 -27
View File
@@ -37,15 +37,34 @@ pub(crate) fn matches(name: &str, ip: IpAddr) -> bool {
pub(crate) fn hostname(value: &str) -> Result<String> {
let candidate = value.strip_suffix('.').unwrap_or(value);
ensure!(candidate.len() <= 253 && candidate.contains('.'), "ACL DNS name requires a fully qualified hostname");
ensure!(candidate.is_ascii() && !candidate.contains('/') && !candidate.contains(':'), "invalid ACL DNS name");
ensure!(
candidate.len() <= 253 && candidate.contains('.'),
"ACL DNS name requires a fully qualified hostname"
);
ensure!(
candidate.is_ascii() && !candidate.contains('/') && !candidate.contains(':'),
"invalid ACL DNS name"
);
let labels: Vec<_> = candidate.split('.').collect();
ensure!(labels.last().is_some_and(|part| part.chars().any(|ch| ch.is_ascii_alphabetic())), "invalid ACL DNS name suffix");
ensure!(
labels
.last()
.is_some_and(|part| part.chars().any(|ch| ch.is_ascii_alphabetic())),
"invalid ACL DNS name suffix"
);
for label in labels {
ensure!(!label.is_empty() && label.len() <= 63, "invalid ACL DNS label length");
ensure!(label.as_bytes()[0].is_ascii_alphanumeric()
&& label.as_bytes()[label.len() - 1].is_ascii_alphanumeric()
&& label.bytes().all(|byte| byte.is_ascii_alphanumeric() || byte == b'-'), "invalid ACL DNS label");
ensure!(
!label.is_empty() && label.len() <= 63,
"invalid ACL DNS label length"
);
ensure!(
label.as_bytes()[0].is_ascii_alphanumeric()
&& label.as_bytes()[label.len() - 1].is_ascii_alphanumeric()
&& label
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-'),
"invalid ACL DNS label"
);
}
Ok(candidate.to_ascii_lowercase())
}
@@ -55,26 +74,42 @@ pub(crate) fn configured_hostnames(config: &Config) -> Result<Vec<String>> {
let mut inspect = |acl: Option<&crate::acl::Acl>| -> Result<()> {
if let Some(acl) = acl {
for rule in &acl.rules {
if rule.network.parse::<ipnet::IpNet>().is_ok() || rule.network.parse::<IpAddr>().is_ok() {
if rule.network.parse::<ipnet::IpNet>().is_ok()
|| rule.network.parse::<IpAddr>().is_ok()
{
continue;
}
let name = hostname(&rule.network)?;
ensure!(rule.action == crate::acl::Action::Allow,
"dynamic DNS ACL rules only support allow; use static IP/CIDR for deny");
ensure!(
rule.action == crate::acl::Action::Allow,
"dynamic DNS ACL rules only support allow; use static IP/CIDR for deny"
);
names.insert(name);
ensure!(names.len() <= MAX_NAMES, "too many distinct dynamic DNS ACL names (max 128)");
ensure!(
names.len() <= MAX_NAMES,
"too many distinct dynamic DNS ACL names (max 128)"
);
}
}
Ok(())
};
for policy in &config.host_policies { inspect(Some(&policy.acl))?; }
for profile in &config.acl_profiles { inspect(profile.acl.as_ref())?; }
inspect(config.global_security.acl.as_ref())?;
for policy in &config.host_policies {
inspect(Some(&policy.acl))?;
}
for profile in &config.acl_profiles {
inspect(profile.acl.as_ref())?;
}
for vhost in &config.vhosts {
inspect(vhost.acl.as_ref())?;
for route in &vhost.routes { inspect(route.acl.as_ref())?; }
for route in &vhost.routes {
inspect(route.acl.as_ref())?;
}
}
for agent in &config.docker_agents.agents {
for assignment in &agent.assignments { inspect(assignment.acl.as_ref())?; }
for assignment in &agent.assignments {
inspect(assignment.acl.as_ref())?;
}
}
Ok(names.into_iter().collect())
}
@@ -84,7 +119,8 @@ async fn resolve(name: String) -> (String, Option<Addresses>) {
let answer = tokio::time::timeout(
Duration::from_secs(LOOKUP_SECONDS),
tokio::net::lookup_host((lookup_name.as_str(), 0u16)),
).await;
)
.await;
let mut ips = match answer {
Ok(Ok(sockets)) => sockets.map(|socket| socket.ip()).collect::<Vec<_>>(),
_ => {
@@ -94,21 +130,29 @@ async fn resolve(name: String) -> (String, Option<Addresses>) {
};
ips.sort_unstable();
ips.dedup();
if ips.is_empty() || ips.len() > MAX_ADDRESSES || ips.iter().any(|ip| {
matches!(ip, IpAddr::V6(v6) if v6.to_ipv4_mapped().is_some())
}) {
if ips.is_empty()
|| ips.len() > MAX_ADDRESSES
|| ips
.iter()
.any(|ip| matches!(ip, IpAddr::V6(v6) if v6.to_ipv4_mapped().is_some()))
{
tracing::warn!(hostname = %name, "dynamic ACL DNS returned an invalid or excessive address set");
return (name, None);
}
(name, Some(Addresses { ips, valid_until: Instant::now() + Duration::from_secs(MAX_AGE_SECONDS) }))
(
name,
Some(Addresses {
ips,
valid_until: Instant::now() + Duration::from_secs(MAX_AGE_SECONDS),
}),
)
}
pub(crate) fn config_changed() { CHANGED.notify_one(); }
pub(crate) fn config_changed() {
CHANGED.notify_one();
}
pub(crate) async fn poll_loop(
app: Weak<crate::runtime::App>,
shutdown: CancellationToken,
) {
pub(crate) async fn poll_loop(app: Weak<crate::runtime::App>, shutdown: CancellationToken) {
let mut interval = tokio::time::interval(Duration::from_secs(REFRESH_SECONDS));
interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
loop {
@@ -117,9 +161,13 @@ pub(crate) async fn poll_loop(
_ = interval.tick() => {},
_ = CHANGED.notified() => {},
}
let Some(state) = app.upgrade() else { break; };
let Some(state) = app.upgrade() else {
break;
};
let snapshot = state.config.load();
let Ok(names) = configured_hostnames(&snapshot.envelope.config) else { continue; };
let Ok(names) = configured_hostnames(&snapshot.envelope.config) else {
continue;
};
drop(snapshot);
let results: HashMap<_, _> = tokio::select! {
_ = shutdown.cancelled() => break,
+21 -3
View File
@@ -207,13 +207,29 @@ mod tests {
routing_path("/static/js//events.js").unwrap(),
"/static/js/events.js"
);
for (path, expected) in [
("/x%2Fy", "/x/y"),
("/x%2fy", "/x/y"),
("/x%2F%2Fy", "/x/y"),
("/x//y", "/x/y"),
("/%2Ffoo", "/foo"),
] {
assert_eq!(routing_path(path).unwrap(), expected, "{path}");
}
for path in [
"/%2e%2e/secret",
"/a/%2E%2E/b",
"/a%2fb",
"/x%2F..%2Fy",
"/x%2F%2E%2E%2Fy",
"/a%5cb",
"/%5Cadmin",
"/%25admin",
"/%3Fadmin",
"/%23admin",
"/%3Badmin",
"/bad%",
"/bad%2",
"/%00",
] {
assert!(routing_path(path).is_err(), "{path}");
}
@@ -239,10 +255,12 @@ mod tests {
"/static/js/events.js"
);
assert_eq!(routing_path("//admin///users").unwrap(), "/admin/users");
assert_eq!(routing_path("/%2fadmin").unwrap(), "/admin");
for path in [
"/api/../admin",
"/api/%2e%2e/admin",
"/%2fadmin",
"/api%2F..%2Fadmin",
"/api%2F%2E%2E%2Fadmin",
"/%5cadmin",
"/%2561dmin",
"/%00",
@@ -349,7 +367,7 @@ mod tests {
for peer in ["10.1.2.3", "203.0.113.5"] {
let peer = peer.parse().unwrap();
assert_eq!(
allows_profiles_with_country(&[single.clone()], peer, country),
allows_profiles_with_country(std::slice::from_ref(&single), peer, country),
allows_with_country(single.acl.as_ref(), &single.countries, peer, country),
);
}
+12 -9
View File
@@ -35,17 +35,20 @@ async fn execute(
.map_err(|_| "acme.validation_worker_failed".to_owned())?
}
pub async fn challenge(
axum::extract::State(state): axum::extract::State<Arc<App>>,
request: axum::extract::Request,
next: axum::middleware::Next,
) -> axum::response::Response {
const CHALLENGE_PREFIX: &str = "/.well-known/acme-challenge/";
pub fn is_challenge_path(path: &str) -> bool {
path.starts_with(CHALLENGE_PREFIX)
}
pub async fn challenge(state: Arc<App>, request: axum::extract::Request) -> axum::response::Response {
use axum::{body::Body, response::IntoResponse};
use tokio::io::AsyncReadExt;
const PREFIX: &str = "/.well-known/acme-challenge/";
let Some(token) = request.uri().path().strip_prefix(PREFIX) else {
return next.run(request).await;
};
let token = request
.uri()
.path()
.strip_prefix(CHALLENGE_PREFIX)
.unwrap_or_default();
let denied = || {
(
StatusCode::NOT_FOUND,
+9 -9
View File
@@ -21,17 +21,17 @@ fn general_api_token_allowed(method: &Method, path: &str, permission: &str) -> b
| "/api/metrics/json"
| "/api/metrics/summary"
| "/api/cache/bans"
);
)
|| (method == Method::GET
&& path
.strip_prefix("/api/config/")
.is_some_and(|section| ConfigSection::from_name(section).is_some()));
let write = permission == "rw"
&& ((method == Method::PUT
&& (path == "/api/config"
|| path.strip_prefix("/api/config/").is_some_and(|part| {
!part.is_empty()
&& part != "history"
&& part != "reload"
&& part != "storage"
&& !part.contains('/')
})))
|| path
.strip_prefix("/api/config/")
.is_some_and(|section| ConfigSection::from_name(section).is_some())))
|| (method == Method::POST
&& matches!(
path,
@@ -39,7 +39,7 @@ fn general_api_token_allowed(method: &Method, path: &str, permission: &str) -> b
| "/api/config/history/restore"
| "/api/cache/purge"
| "/api/cache/invalidate"
| "/api/geoip/update"
| "/api/config/geoip/update"
| "/api/docker/refresh"
| "/api/certificates/reload"
))
+2 -2
View File
@@ -194,7 +194,7 @@ async fn certificate_deploy(
)
.await
.map_err(auth::internal)?;
return Ok(Json(json!({
Ok(Json(json!({
"deployed": true,
"operation": "metadata-updated",
"created": false,
@@ -207,7 +207,7 @@ async fn certificate_deploy(
"public_tls": true,
"http3_new_handshakes": state.config.load().quic_tls.is_some(),
"administration_restart_required": false
})));
})))
}
crate::tls::ExternalPairDeployment::Staged {
cert_file,
+288
View File
@@ -0,0 +1,288 @@
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ConfigSection {
Listeners,
DockerAgents,
Http2,
Pages,
Cache,
Transform,
Forwarding,
Headers,
Prometheus,
Geoip,
Administration,
Tls,
Vhosts,
WebServices,
PhpFpmProfiles,
Redirects,
NotFoundHosts,
Streams,
AclProfiles,
HostPolicies,
Response,
Logging,
GlobalSecurity,
}
impl ConfigSection {
fn from_name(name: &str) -> Option<Self> {
match name {
"listeners" => Some(Self::Listeners),
"docker-agents" => Some(Self::DockerAgents),
"http2" => Some(Self::Http2),
"pages" => Some(Self::Pages),
"cache" => Some(Self::Cache),
"transform" => Some(Self::Transform),
"forwarding" => Some(Self::Forwarding),
"headers" => Some(Self::Headers),
"prometheus" => Some(Self::Prometheus),
"geoip" => Some(Self::Geoip),
"administration" => Some(Self::Administration),
"tls" => Some(Self::Tls),
"vhosts" => Some(Self::Vhosts),
"web-services" => Some(Self::WebServices),
"php-fpm-profiles" => Some(Self::PhpFpmProfiles),
"redirects" => Some(Self::Redirects),
"not-found-hosts" => Some(Self::NotFoundHosts),
"streams" => Some(Self::Streams),
"acl-profiles" => Some(Self::AclProfiles),
"host-policies" => Some(Self::HostPolicies),
"response" => Some(Self::Response),
"logging" => Some(Self::Logging),
"global-security" => Some(Self::GlobalSecurity),
_ => None,
}
}
fn read(self, config: &crate::config::Config) -> Value {
match self {
Self::Listeners => json!(&config.listeners),
Self::DockerAgents => json!(&config.docker_agents),
Self::Http2 => json!(&config.http2),
Self::Pages => json!(&config.pages),
Self::Cache => json!(&config.cache),
Self::Transform => json!(&config.transform),
Self::Forwarding => json!(&config.forwarding),
Self::Headers => json!(&config.headers),
Self::Prometheus => json!(&config.prometheus),
Self::Geoip => json!(&config.geoip_updates),
Self::Administration => json!(&config.administration),
Self::Tls => json!(&config.tls),
Self::Vhosts => json!(&config.vhosts),
Self::WebServices => json!(&config.web_services),
Self::PhpFpmProfiles => json!(&config.php_fpm_profiles),
Self::Redirects => json!(&config.redirects),
Self::NotFoundHosts => json!(&config.not_found_hosts),
Self::Streams => json!(&config.streams),
Self::AclProfiles => json!(&config.acl_profiles),
Self::HostPolicies => json!(&config.host_policies),
Self::Response => json!({
"server_header": &config.server_header,
"diagnostic_headers": &config.diagnostic_headers,
"compression": &config.compression,
}),
Self::Logging => json!({
"logging": &config.logging,
"error_log": &config.error_log,
}),
Self::GlobalSecurity => json!(&config.global_security),
}
}
fn replace(
self,
config: &mut crate::config::Config,
name: &str,
value: Value,
) -> Result<(), ApiError> {
match self {
Self::Listeners => {
config.listeners =
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
}
Self::DockerAgents => {
config.docker_agents =
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
}
Self::Http2 => {
config.http2 =
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
}
Self::Pages => {
config.pages =
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
}
Self::Cache => {
config.cache =
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
}
Self::Transform => {
config.transform =
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
}
Self::Forwarding => {
config.forwarding =
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
}
Self::Headers => {
config.headers =
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
}
Self::Prometheus => {
config.prometheus =
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
}
Self::Geoip => {
config.geoip_updates =
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
}
Self::Administration => {
config.administration =
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
}
Self::Tls => {
config.tls =
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
}
Self::Vhosts => {
config.vhosts =
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
}
Self::WebServices => {
config.web_services =
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
}
Self::PhpFpmProfiles => {
config.php_fpm_profiles =
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
}
Self::Redirects => {
config.redirects =
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
}
Self::NotFoundHosts => {
config.not_found_hosts =
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
}
Self::Streams => {
config.streams =
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
}
Self::AclProfiles => {
config.acl_profiles =
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
}
Self::HostPolicies => {
config.host_policies =
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
}
Self::Response => {
let part: ResponseSection =
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?;
config.server_header = part.server_header;
config.diagnostic_headers = part.diagnostic_headers;
config.compression = part.compression;
}
Self::Logging => {
let part: LoggingSection =
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?;
config.logging = part.logging;
config.error_log = part.error_log;
}
Self::GlobalSecurity => {
let part: crate::config::GlobalSecurity =
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?;
if part.user_agent_blocking.is_none() {
return Err(ApiError(
StatusCode::BAD_REQUEST,
"global-security requires user_agent_blocking in its complete section value".into(),
));
}
config.global_security = part;
}
}
Ok(())
}
}
fn requested_config_section(name: &str) -> Result<ConfigSection, ApiError> {
ConfigSection::from_name(name).ok_or_else(|| {
ApiError(
StatusCode::NOT_FOUND,
"unknown configuration section".into(),
)
})
}
async fn configuration_section(
State(state): State<Arc<App>>,
Path(section): Path<String>,
) -> Result<Json<Value>, ApiError> {
let selected = requested_config_section(&section)?;
let snapshot = state.config.load();
Ok(Json(json!({
"revision": snapshot.envelope.revision,
"section": section,
"value": selected.read(&snapshot.envelope.config),
})))
}
async fn update_section(
State(state): State<Arc<App>>,
Extension(session): Extension<Session>,
Path(section): Path<String>,
Json(input): Json<SectionUpdate>,
) -> Result<Json<crate::config::Envelope>, ApiError> {
let selected = requested_config_section(&section)?;
let current = state.config.load_full();
if current.envelope.revision != input.expected_revision {
return Err(ApiError(
StatusCode::CONFLICT,
"configuration changed; reload before saving".into(),
));
}
let mut config = current.envelope.config.clone();
selected.replace(&mut config, &section, input.value)?;
state
.update_as(
Update {
expected_revision: input.expected_revision,
config,
},
session.username,
)
.await
.map(Json)
}
async fn geoip_update(
State(state): State<Arc<App>>,
Extension(session): Extension<Session>,
) -> Result<Json<crate::geoip::UpdateResult>, ApiError> {
let result = crate::geoip::update_now(&state).await.map_err(|error| {
tracing::warn!(error = %format!("{error:#}"), "manual GeoIP update failed");
ApiError(
StatusCode::BAD_GATEWAY,
format!("GeoIP update failed: {error:#}"),
)
})?;
state
.store
.audit(
&session.username,
"geoip.update",
&format!(
"outcome={}; files={}",
result.outcome,
if result.updated_files.is_empty() {
"none".to_string()
} else {
result.updated_files.join(",")
}
),
)
.await
.map_err(auth::internal)?;
Ok(Json(result))
}
+6 -4
View File
@@ -62,10 +62,12 @@ async fn dashboard_presets(
tracing::warn!("skipped invalid saved Overview preset");
continue;
};
let mut prefs = AdminPreferences::default();
prefs.dashboard_widgets = layout.dashboard_widgets;
prefs.dashboard_hidden_widgets = layout.dashboard_hidden_widgets;
prefs.dashboard_widget_sizes = layout.dashboard_widget_sizes;
let mut prefs = AdminPreferences {
dashboard_widgets: layout.dashboard_widgets,
dashboard_hidden_widgets: layout.dashboard_hidden_widgets,
dashboard_widget_sizes: layout.dashboard_widget_sizes,
..Default::default()
};
reconcile_dashboard_widgets(&mut prefs);
layout.dashboard_widgets = prefs.dashboard_widgets;
layout.dashboard_hidden_widgets = prefs.dashboard_hidden_widgets;
+5 -6
View File
@@ -11,10 +11,11 @@ macro_rules! reader_doc {
pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
reader_doc!("README.md", "project"),
reader_doc!("CHANGELOG.md", "project"),
reader_doc!("ROADMAP.md", "project"),
reader_doc!("LICENSE", "project"),
reader_doc!("docs/README.md", "guides"),
reader_doc!("docs/FEATURES.md", "guides"),
reader_doc!("docs/SCREENSHOTS.md", "guides"),
reader_doc!("docs/INSTALL.md", "guides"),
reader_doc!("docs/CONTAINER.md", "guides"),
reader_doc!("docs/CONFIGURATION.md", "guides"),
reader_doc!("docs/VHOSTS.md", "guides"),
reader_doc!("docs/CUSTOM_LOCATIONS.md", "guides"),
@@ -24,11 +25,9 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
reader_doc!("docs/CACHE.md", "guides"),
reader_doc!("docs/RESPONSE_DELIVERY.md", "guides"),
reader_doc!("docs/TRANSPORT.md", "guides"),
reader_doc!("docs/TLS_POST_QUANTUM.md", "guides"),
reader_doc!("docs/STREAMS.md", "guides"),
reader_doc!("docs/ACME_PROVIDERS.md", "guides"),
reader_doc!("docs/CERTIFICATES.md", "guides"),
reader_doc!("docs/DOCKER_DISCOVERY.md", "guides"),
reader_doc!("docs/INTEGRATIONS.md", "guides"),
reader_doc!("docs/DASHBOARD.md", "guides"),
reader_doc!("docs/MONITORING.md", "guides"),
reader_doc!("docs/LOGS.md", "guides"),
@@ -36,6 +35,6 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
reader_doc!("docs/MIGRATION.md", "guides"),
reader_doc!("docs/SECURITY.md", "guides"),
reader_doc!("docs/API.md", "guides"),
reader_doc!("docs/API_ACCESS.md", "guides"),
reader_doc!("docs/PERFORMANCE.md", "guides"),
reader_doc!("docs/TROUBLESHOOTING.md", "guides"),
];
+1 -1
View File
@@ -12,7 +12,7 @@ mod tests {
assert!(documents.iter().any(|doc| doc["path"] == "docs/PHP_FPM.md"));
assert!(documents
.iter()
.any(|doc| doc["path"] == "docs/TLS_POST_QUANTUM.md"));
.any(|doc| doc["path"] == "docs/FEATURES.md"));
assert!(documents.iter().all(|doc| {
doc["path"].as_str().is_some_and(|value| !value.is_empty())
&& doc["title"].as_str().is_some_and(|value| !value.is_empty())
+41 -22
View File
@@ -95,32 +95,37 @@ fn configured_file_log_sources(config: &crate::config::Config) -> Vec<FileLogSou
.ok()
.is_some_and(|meta| meta.file_type().is_dir() && !meta.file_type().is_symlink());
if directory_safe {
let entries = newest_log_files(std::fs::read_dir(&directory)
.ok()
.into_iter()
.flat_map(|iter| iter.filter_map(|entry| entry.ok()))
.filter_map(|entry| {
let path = entry.path();
let name = entry.file_name().to_string_lossy().into_owned();
let plain = name.ends_with(".jsonl");
let rotated = name.contains(".jsonl.") && name.ends_with(".gz");
if (!plain && !rotated) || name.len() > 180 {
return None;
}
let file_type = entry.file_type().ok()?;
if !file_type.is_file() || file_type.is_symlink() {
return None;
}
let meta = entry.metadata().ok()?;
Some((name, path, file_modified_ms(&meta)))
}), 256);
let entries = newest_log_files(
std::fs::read_dir(&directory)
.ok()
.into_iter()
.flat_map(|iter| iter.filter_map(|entry| entry.ok()))
.filter_map(|entry| {
let path = entry.path();
let name = entry.file_name().to_string_lossy().into_owned();
let plain = name.ends_with(".jsonl");
let rotated = name.contains(".jsonl.") && name.ends_with(".gz");
if (!plain && !rotated) || name.len() > 180 {
return None;
}
let file_type = entry.file_type().ok()?;
if !file_type.is_file() || file_type.is_symlink() {
return None;
}
let meta = entry.metadata().ok()?;
Some((name, path, file_modified_ms(&meta)))
}),
256,
);
for (name, path, _) in entries {
let id = if name.ends_with(".jsonl") {
format!("vhost:{}", name.trim_end_matches(".jsonl"))
} else {
format!("vhost-gz:{name}")
};
let vhost_name = name.split_once(".jsonl").map_or(name.as_str(), |(prefix, _)| prefix);
let vhost_name = name
.split_once(".jsonl")
.map_or(name.as_str(), |(prefix, _)| prefix);
let group_id = format!("vhost:{vhost_name}");
sources.push(source_from_path(
id,
@@ -144,7 +149,14 @@ fn configured_file_log_sources(config: &crate::config::Config) -> Vec<FileLogSou
config.logging.file.enabled,
"JSONL",
));
sources.extend(rotated_gzip_sources(&access_path, "access", "Access log", "access", config.logging.file.enabled, "JSONL"));
sources.extend(rotated_gzip_sources(
&access_path,
"access",
"Access log",
"access",
config.logging.file.enabled,
"JSONL",
));
}
let error_format = match config.error_log.format {
crate::logging::ErrorLogFormat::Json => "JSONL",
@@ -160,7 +172,14 @@ fn configured_file_log_sources(config: &crate::config::Config) -> Vec<FileLogSou
config.error_log.enabled,
error_format,
));
sources.extend(rotated_gzip_sources(&error_path, "error", "Error log", "error", config.error_log.enabled, error_format));
sources.extend(rotated_gzip_sources(
&error_path,
"error",
"Error log",
"error",
config.error_log.enabled,
error_format,
));
sources
}
fn file_log_sources_json(sources: &[FileLogSource]) -> Value {
+2 -2
View File
@@ -182,7 +182,7 @@ fn validate_file_log_query(query: &FileLogQuery) -> Result<(), ApiError> {
.is_some_and(|value| value == 0 || value > FILE_LOG_MAX_AGGREGATE_RESULTS)
|| query
.aggregate_min_count
.is_some_and(|value| value < 2 || value > FILE_LOG_SCAN_LINES)
.is_some_and(|value| !(2..=FILE_LOG_SCAN_LINES).contains(&value))
|| query.offset.unwrap_or(0) > FILE_LOG_MAX_OFFSET
|| query
.limit
@@ -233,7 +233,7 @@ async fn file_logs_query(
));
}
} else {
sources.sort_by(|a, b| b.modified_ms.cmp(&a.modified_ms));
sources.sort_by_key(|source| std::cmp::Reverse(source.modified_ms));
if sources.len() > FILE_LOG_MAX_SOURCES {
truncated = true;
sources.truncate(FILE_LOG_MAX_SOURCES);
-135
View File
@@ -1,138 +1,3 @@
async fn update_section(
State(state): State<Arc<App>>,
Extension(session): Extension<Session>,
Path(section): Path<String>,
Json(input): Json<SectionUpdate>,
) -> Result<Json<crate::config::Envelope>, ApiError> {
let current = state.config.load_full();
if current.envelope.revision != input.expected_revision {
return Err(ApiError(
StatusCode::CONFLICT,
"configuration changed; reload before saving".into(),
));
}
let mut config = current.envelope.config.clone();
let value = input.value;
match section.as_str() {
"listeners" => {
config.listeners =
serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?
}
"docker-agents" => {
config.docker_agents =
serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?
}
"http2" => {
config.http2 =
serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?
}
"response" => {
let part: ResponseSection =
serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?;
config.server_header = part.server_header;
config.diagnostic_headers = part.diagnostic_headers;
config.compression = part.compression;
}
"pages" => {
config.pages =
serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?
}
"cache" => {
config.cache =
serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?
}
"transform" => {
config.transform =
serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?
}
"logging" => {
if value.get("logging").is_some() || value.get("error_log").is_some() {
let part: LoggingSection =
serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?;
config.logging = part.logging;
config.error_log = part.error_log;
} else {
config.logging =
serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?;
}
}
"forwarding" => {
config.forwarding =
serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?
}
"headers" => {
config.headers =
serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?
}
"prometheus" => {
config.prometheus =
serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?
}
"geoip" => {
config.geoip_updates =
serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?
}
"administration" => {
config.administration =
serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?
}
"tls" => {
config.tls = serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?
}
"vhosts" => {
config.vhosts =
serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?
}
"web-services" => {
config.web_services =
serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?
}
"php-fpm-profiles" => {
config.php_fpm_profiles =
serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?
}
"redirects" => {
config.redirects =
serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?
}
"not_found_hosts" => {
config.not_found_hosts =
serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?
}
"streams" => {
config.streams =
serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?
}
"acl-profiles" => {
config.acl_profiles =
serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?
}
"host-policies" => {
config.host_policies =
serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?
}
"user-agent-blocking" => {
config.user_agent_blocking =
serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?
}
_ => {
return Err(ApiError(
StatusCode::NOT_FOUND,
"unknown configuration section".into(),
))
}
}
state
.update_as(
Update {
expected_revision: input.expected_revision,
config,
},
session.username,
)
.await
.map(Json)
}
fn logs_payload(state: &App) -> Value {
json!({
"entries": state.logs.recent_limit(crate::logging::LIVE_WINDOW),
+1
View File
@@ -5,6 +5,7 @@ include!("session.rs");
include!("preferences.rs");
include!("dashboard_presets.rs");
include!("status.rs");
include!("config_sections.rs");
include!("logging_api.rs");
include!("config_reload.rs");
include!("api_tokens.rs");
+555 -70
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "RustProxy Administration API",
"version": "0.34.12",
"version": "0.35.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. The API 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": [
@@ -439,6 +439,55 @@
"id"
]
},
"AuthenticationChoice": {
"type": "object",
"description": "Optional explicit provider and integration method. Absent means infer the existing active Basic/Tinyauth/Authelia/authentik/OIDC configuration for backward compatibility. The selected method must match the one enabled legacy settings object; unknown, inconsistent or unsupported pairs are rejected.",
"additionalProperties": false,
"required": ["provider", "method"],
"properties": {
"provider": {"type": "string", "enum": ["basic", "tinyauth", "authelia", "authentik", "pocket_id", "custom"]},
"method": {"type": "string", "enum": ["basic", "forward_auth", "oidc"], "description": "Basic supports basic only; Tinyauth/Authelia/authentik support forward_auth or oidc; Pocket ID/custom support oidc only."}
}
},
"OidcConfig": {
"type": "object",
"description": "Opt-in OpenID Connect Authorization Code integration method for an identity provider selected by authentication.provider. Uses issuer discovery, S256 PKCE, nonce/state cookies, JWKS RS256/ES256 ID token verification and encrypted host-only application sessions. Existing standalone oidc.enabled configurations remain supported.",
"additionalProperties": false,
"properties": {
"enabled": {"type": "boolean", "default": false},
"issuer_url": {"type": "string", "format": "uri", "description": "Exact HTTPS OIDC issuer; /.well-known/openid-configuration is discovered at this URL"},
"backend_url": {"type": "string", "format": "uri", "description": "Optional internal HTTP(S) origin for server-side discovery/token/JWKS/UserInfo requests; public issuer and JWT validation remain unchanged"},
"redirect_url": {"type": "string", "format": "uri", "description": "HTTPS URL for an exact hostname in this Vhost and path /_rustproxy/oidc/callback"},
"client_id": {"type": "string", "maxLength": 256},
"client_secret": {"type": "string", "maxLength": 2048, "description": "Confidential OIDC client secret; stored in Vhost configuration"},
"session_secret": {"type": "string", "minLength": 32, "maxLength": 4096, "description": "High-entropy secret for encrypted session and temporary state cookies"},
"scopes": {"type": "string", "default": "openid profile email"},
"allow_private": {"type": "boolean", "default": false},
"address_allowlist": {"type": "array", "items": {"type": "string"}},
"tls_ca_pem": {"type": "string", "nullable": true},
"connect_timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 30, "default": 5},
"timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 60, "default": 10},
"session_ttl_seconds": {"type": "integer", "minimum": 60, "maximum": 86400, "default": 3600}
}
},
"ForwardAuthConfig": {
"type": "object",
"description": "Per-Vhost Authelia or authentik verification. Disabled by default. All other settings are optional and use RustProxy defaults; when enabled, url must identify an internal HTTP(S) origin (or expected verifier path). Authelia additionally requires a public HTTPS login_url. External addresses retain normal safe upstream validation, with TLS verification always enabled for HTTPS.",
"additionalProperties": false,
"properties": {
"enabled": {"type": "boolean", "default": false},
"url": {"type": "string", "maxLength": 2048, "description": "Internal service/outpost origin or the provider's verifier endpoint"},
"login_url": {"type": "string", "maxLength": 2048, "description": "Required public HTTPS portal URL for Authelia; ignored for authentik"},
"allow_private": {"type": "boolean", "default": false},
"address_allowlist": {"type": "array", "maxItems": 32, "items": {"type": "string"}},
"tls_ca_pem": {"type": "string", "nullable": true},
"connect_timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 30, "default": 5},
"timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 60, "default": 10},
"request_headers": {"type": "array", "maxItems": 32, "items": {"type": "string", "maxLength": 128}, "description": "Only cookie, authorization, user-agent, accept, accept-language and x-api-key are allowed"},
"response_headers": {"type": "array", "maxItems": 32, "items": {"type": "string", "maxLength": 128}, "description": "Allowed Remote-*, X-Auth-Request-* and X-Authentik-* identity headers; total injected identity data capped at 8 KiB"},
"redirect_forbidden": {"type": "boolean", "default": false, "description": "Authelia only: accept trusted portal Location on explicit 403 if provided"}
}
},
"Vhost": {
"type": "object",
"properties": {
@@ -534,6 +583,16 @@
"properties": {},
"additionalProperties": true
},
"authelia": {
"$ref": "#/components/schemas/ForwardAuthConfig",
"description": "Authelia ForwardAuth. Requires a public HTTPS login_url and /api/authz/forward-auth verifier. Disabled by default; mutually exclusive with basic_auth, tinyauth and authentik."
},
"authentik": {
"$ref": "#/components/schemas/ForwardAuthConfig",
"description": "authentik single-application forward auth. The URL is the internal outpost origin; RustProxy proxies its reserved path on the protected host. Disabled by default; mutually exclusive with basic_auth, tinyauth and authelia."
},
"oidc": {"$ref": "#/components/schemas/OidcConfig"},
"authentication": {"$ref": "#/components/schemas/AuthenticationChoice", "description": "Optional provider/method selector. Set to null or omit for legacy Vhost data. It does not replace the existing provider-specific settings objects."},
"cache": {
"type": "object",
"properties": {
@@ -1025,11 +1084,22 @@
},
"description": "Named PHP-FPM configurations reusable by Web Services."
},
"global_security": {
"$ref": "#/components/schemas/GlobalSecurity"
},
"user_agent_blocking": {
"$ref": "#/components/schemas/UserAgentBlocking"
"deprecated": true,
"writeOnly": true,
"description": "Migration-only input for full configuration imports from older installations. The live GET response and canonical saved configuration use global_security.user_agent_blocking instead. Conflicting legacy and nested policies are rejected.",
"allOf": [
{
"$ref": "#/components/schemas/UserAgentBlocking"
}
]
}
},
"additionalProperties": true
"additionalProperties": false,
"description": "The canonical global User-Agent policy lives at global_security.user_agent_blocking; the old root-level key is accepted only as a migration input, not returned by this API."
},
"Envelope": {
"type": "object",
@@ -1797,10 +1867,10 @@
"expected_revision": {
"type": "integer",
"format": "int64",
"description": "Revision returned by the latest GET /api/config."
"description": "Current revision from GET /api/config or GET /api/config/{section}."
},
"value": {
"description": "Complete value of the selected configuration section. This is a section replacement, not a merge patch."
"description": "Complete value of the selected configuration section. This is a section replacement, not a merge patch. Read GET /api/config/{section} first, then return its complete `value` with edits. Even an empty array is a full replacement of that collection."
}
},
"additionalProperties": false,
@@ -3364,6 +3434,19 @@
"nullable": true,
"description": "Optional HTTP Host override. When null for an HTTP vhost probe, RustProxy uses the first exact vhost DNS hostname; if none exists it falls back to the backend host."
},
"port": {
"type": "integer",
"minimum": 1,
"maximum": 65535,
"nullable": true,
"description": "Optional port for active probes. When null or equal to the backend port, probes use the backend address; otherwise TCP and HTTP probes connect to the same host on this port with the same TLS settings and address restrictions."
},
"expect_body": {
"type": "string",
"nullable": true,
"maxLength": 256,
"description": "Optional text that must appear in the first 64 KiB of an HTTP probe response body. A response with an accepted status but without this text counts as a failed probe (result http_body)."
},
"interval_ms": {
"type": "integer",
"minimum": 250,
@@ -3696,6 +3779,10 @@
"extra_h3_share",
"extra_cache_miss_rate",
"extra_backend_active",
"extra_backends_healthy",
"extra_error_share",
"extra_rejected_rate",
"extra_cert_expiry",
"wide_protocol_rates",
"wide_cpu",
"wide_memory",
@@ -3777,6 +3864,10 @@
"extra_h3_share",
"extra_cache_miss_rate",
"extra_backend_active",
"extra_backends_healthy",
"extra_error_share",
"extra_rejected_rate",
"extra_cert_expiry",
"wide_protocol_rates",
"wide_cpu",
"wide_memory",
@@ -3865,6 +3956,10 @@
"extra_h3_share",
"extra_cache_miss_rate",
"extra_backend_active",
"extra_backends_healthy",
"extra_error_share",
"extra_rejected_rate",
"extra_cert_expiry",
"wide_protocol_rates",
"wide_cpu",
"wide_memory",
@@ -3934,6 +4029,10 @@
"extra_h3_share",
"extra_cache_miss_rate",
"extra_backend_active",
"extra_backends_healthy",
"extra_error_share",
"extra_rejected_rate",
"extra_cert_expiry",
"wide_protocol_rates",
"wide_cpu",
"wide_memory",
@@ -7585,6 +7684,135 @@
}
},
"additionalProperties": false
},
"GlobalSecurity": {
"type": "object",
"description": "One atomic global HTTP security configuration for all public HTTP hosts. Includes User-Agent blocking, country/IP restrictions, methods, request Content-Type, shared rate limiting and response security headers. Individual policies default off. Old top-level user_agent_blocking in an imported full config is migrated at validation time; conflicting old and new values are rejected.",
"additionalProperties": false,
"properties": {
"country_access": {
"$ref": "#/components/schemas/CountryAccess"
},
"acl": {
"allOf": [
{
"$ref": "#/components/schemas/Acl"
}
],
"nullable": true,
"description": "Optional global client IP/CIDR and allow-only DNS ACL; null disables this layer."
},
"method_policy": {
"$ref": "#/components/schemas/MethodPolicy"
},
"content_type_policy": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"default": false
},
"blocked": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 64,
"description": "Blocked MIME types, e.g. application/json, image/*."
}
}
},
"rate_limit": {
"$ref": "#/components/schemas/VhostRateLimit"
},
"security_headers": {
"type": "object",
"additionalProperties": false,
"properties": {
"hsts": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"default": false
},
"max_age_seconds": {
"type": "integer",
"minimum": 0,
"maximum": 63072000,
"default": 31536000
},
"include_subdomains": {
"type": "boolean",
"default": false
},
"preload": {
"type": "boolean",
"default": false
}
}
},
"nosniff": {
"type": "boolean",
"default": false
},
"frame_options": {
"type": "string",
"enum": [
"preserve",
"deny",
"sameorigin"
],
"default": "preserve"
},
"referrer_policy": {
"type": "string",
"nullable": true,
"maxLength": 8192
},
"content_security_policy": {
"type": "string",
"nullable": true,
"maxLength": 8192
},
"permissions_policy": {
"type": "string",
"nullable": true,
"maxLength": 8192
}
}
},
"user_agent_blocking": {
"$ref": "#/components/schemas/UserAgentBlocking"
}
},
"required": [
"user_agent_blocking"
]
},
"ConfigSectionRead": {
"type": "object",
"description": "Read one whole configuration section with the same value format accepted by PUT. It does not contain storage metadata or unrelated sections.",
"additionalProperties": false,
"properties": {
"revision": {
"type": "integer",
"format": "int64"
},
"section": {
"type": "string"
},
"value": {
"description": "Serialized complete value for the requested section. The format is identical to PUT /api/config/{section}. See x-rustproxy-section-fields on the operation for section-to-Config mapping."
}
},
"required": [
"revision",
"section",
"value"
]
}
}
},
@@ -9322,7 +9550,7 @@
}
}
},
"description": "Maximum body 16 KiB. Staging by default. Production requires explicit consent. Provider fields depend on the selected provider; see docs/ACME_PROVIDERS.md. Poll GET /api/acme.",
"description": "Maximum body 16 KiB. Staging by default. Production requires explicit consent. Provider fields depend on the selected provider; see docs/CERTIFICATES.md. Poll GET /api/acme.",
"parameters": [
{
"name": "Origin",
@@ -9813,66 +10041,6 @@
]
}
},
"/api/geoip/update": {
"post": {
"summary": "Check, download and hot-reload GeoIP country databases now",
"description": "Download the configured direct GeoIP source(s), optionally verify publisher SHA-256 values, validate the selected CSV/MMDB format, atomically publish changed files and hot-reload future GeoIP lookups.",
"tags": [
"Settings"
],
"operationId": "post_api_geoip_update",
"responses": {
"200": {
"description": "GeoIP check completed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GeoIpUpdateResult"
}
}
}
},
"401": {
"description": "Authentication required or revoked",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Host, origin or CSRF validation failed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"502": {
"description": "Publisher check, download, validation or reload failed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"security": [
{
"AdminSession": []
},
{
"GeneralApiBearer": []
}
]
}
},
"/api/cache/purge": {
"post": {
"summary": "Purge all cache entries or one vhost",
@@ -10644,9 +10812,19 @@
}
}
}
},
"404": {
"description": "Unknown or removed configuration section",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"description": "Preferred UI/control-plane save path. Replaces only the selected section while preserving every other section from the current revision. This is not a merge patch. Cache identity is independent from the global configuration revision: logging, listeners, TLS inventory, metrics and administration saves do not purge cached objects. Changes which can alter origin requests or cached responses (cache/transform/response/forwarding/vhosts) rotate the cache compatibility identity and purge incompatible hot/cold objects. Supported section names include docker-agents, vhosts, redirects, not_found_hosts, streams, listeners, http2, tls, pages, cache, transform, logging, forwarding, headers, prometheus, geoip, administration, acl-profiles, host-policies and response.",
"description": "Replaces the named section in one revision-checked, validated, durable configuration update; unrelated sections are preserved. This is a full section replacement, not a merge/patch. Use GET /api/config/{section} to read the exact value shape before PUT. logging requires {logging, error_log}; response requires {server_header, diagnostic_headers, compression}; global-security requires one GlobalSecurity object containing user_agent_blocking. The legacy standalone user-agent-blocking section and standalone error_log are not writable. Global revision conflicts return 409, even for writes to different sections; retry after fetching a new revision. PUT returns the complete updated {revision, config} envelope so the UI can refresh all sections. Origin-dependent cache policy changes still invalidate incompatible cached objects; unrelated logging/listener/administration updates do not purge the cache. See x-rustproxy-section-fields for canonical JSON field mapping.",
"parameters": [
{
"name": "section",
@@ -10671,11 +10849,13 @@
"tls",
"vhosts",
"redirects",
"not_found_hosts",
"not-found-hosts",
"streams",
"acl-profiles",
"host-policies",
"user-agent-blocking"
"global-security",
"web-services",
"php-fpm-profiles"
]
},
"description": "Configuration section to replace."
@@ -10705,6 +10885,9 @@
{
"AdminSession": [],
"CsrfToken": []
},
{
"GeneralApiBearer": []
}
],
"requestBody": {
@@ -10716,6 +10899,248 @@
}
}
}
},
"x-rustproxy-section-fields": {
"listeners": [
"listeners"
],
"docker-agents": [
"docker_agents"
],
"http2": [
"http2"
],
"pages": [
"pages"
],
"cache": [
"cache"
],
"transform": [
"transform"
],
"forwarding": [
"forwarding"
],
"headers": [
"headers"
],
"prometheus": [
"prometheus"
],
"geoip": [
"geoip_updates"
],
"administration": [
"administration"
],
"tls": [
"tls"
],
"vhosts": [
"vhosts"
],
"web-services": [
"web_services"
],
"php-fpm-profiles": [
"php_fpm_profiles"
],
"redirects": [
"redirects"
],
"not-found-hosts": [
"not_found_hosts"
],
"streams": [
"streams"
],
"acl-profiles": [
"acl_profiles"
],
"host-policies": [
"host_policies"
],
"response": [
"server_header",
"diagnostic_headers",
"compression"
],
"logging": [
"logging",
"error_log"
],
"global-security": [
"global_security"
]
}
},
"get": {
"summary": "Read one revisioned configuration section",
"description": "Returns {revision, section, value}. The value is the complete canonical section payload accepted by PUT /api/config/{section}, not a partial patch. logging groups logging + error_log, response groups the three response controls, and global-security contains user_agent_blocking and all other global security rules. No storage paths are returned by this endpoint. See x-rustproxy-section-fields for the exact Config property mapping of all supported names.",
"tags": [
"Configuration"
],
"operationId": "get_api_config_section",
"parameters": [
{
"name": "section",
"in": "path",
"required": true,
"schema": {
"type": "string",
"enum": [
"listeners",
"docker-agents",
"response",
"pages",
"cache",
"transform",
"logging",
"forwarding",
"headers",
"prometheus",
"geoip",
"administration",
"http2",
"tls",
"vhosts",
"redirects",
"not-found-hosts",
"streams",
"acl-profiles",
"host-policies",
"global-security",
"web-services",
"php-fpm-profiles"
]
},
"description": "Exactly one supported configuration section."
}
],
"responses": {
"200": {
"description": "Section value and current global revision",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConfigSectionRead"
}
}
}
},
"401": {
"description": "Authentication required",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden by Host or token policy",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Unknown or removed configuration section",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"security": [
{
"AdminSession": []
},
{
"GeneralApiBearer": []
}
],
"x-rustproxy-section-fields": {
"listeners": [
"listeners"
],
"docker-agents": [
"docker_agents"
],
"http2": [
"http2"
],
"pages": [
"pages"
],
"cache": [
"cache"
],
"transform": [
"transform"
],
"forwarding": [
"forwarding"
],
"headers": [
"headers"
],
"prometheus": [
"prometheus"
],
"geoip": [
"geoip_updates"
],
"administration": [
"administration"
],
"tls": [
"tls"
],
"vhosts": [
"vhosts"
],
"web-services": [
"web_services"
],
"php-fpm-profiles": [
"php_fpm_profiles"
],
"redirects": [
"redirects"
],
"not-found-hosts": [
"not_found_hosts"
],
"streams": [
"streams"
],
"acl-profiles": [
"acl_profiles"
],
"host-policies": [
"host_policies"
],
"response": [
"server_header",
"diagnostic_headers",
"compression"
],
"logging": [
"logging",
"error_log"
],
"global-security": [
"global_security"
]
}
}
},
@@ -13905,7 +14330,67 @@
}
]
}
},
"/api/config/geoip/update": {
"post": {
"summary": "Update GeoIP data using saved geoip configuration now",
"description": "Manually start a GeoIP data refresh using the already-saved settings at GET/PUT /api/config/geoip. This is an operational POST, not a configuration PUT and it does not increment the configuration revision. Downloads configured GeoIP sources, verifies optional SHA-256 checksums, checks CSV/MMDB, atomically publishes changed databases and hot-reloads future lookups. Save any edited GeoIP settings before calling this action. The response is GeoIpUpdateResult; a failed external fetch returns 502 and existing usable data remains untouched.",
"tags": [
"Configuration"
],
"operationId": "post_api_config_geoip_update",
"responses": {
"200": {
"description": "GeoIP check completed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GeoIpUpdateResult"
}
}
}
},
"401": {
"description": "Authentication required or revoked",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Host, origin or CSRF validation failed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"502": {
"description": "Publisher check, download, validation or reload failed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"security": [
{
"AdminSession": []
},
{
"GeneralApiBearer": []
}
]
}
}
},
"tags": []
}
}
-31
View File
@@ -1,34 +1,3 @@
async fn geoip_update(
State(state): State<Arc<App>>,
Extension(session): Extension<Session>,
) -> Result<Json<crate::geoip::UpdateResult>, ApiError> {
let result = crate::geoip::update_now(&state).await.map_err(|error| {
tracing::warn!(error = %format!("{error:#}"), "manual GeoIP update failed");
ApiError(
StatusCode::BAD_GATEWAY,
format!("GeoIP update failed: {error:#}"),
)
})?;
state
.store
.audit(
&session.username,
"geoip.update",
&format!(
"outcome={}; files={}",
result.outcome,
if result.updated_files.is_empty() {
"none".to_string()
} else {
result.updated_files.join(",")
}
),
)
.await
.map_err(auth::internal)?;
Ok(Json(result))
}
async fn transform_status(State(state): State<Arc<App>>) -> Json<Value> {
let snapshot = state.config.load_full();
let global = &snapshot.envelope.config.transform;
+23 -17
View File
@@ -86,18 +86,20 @@ fn corner_style_defaults_and_rejects_unknown_presets() {
#[test]
fn old_widget_preferences_preserve_order_opt_in_and_survive_unknown_ids() {
let mut prefs = AdminPreferences::default();
prefs.dashboard_widgets = vec![
"errors".into(),
"from_future_release".into(),
"requests".into(),
"errors".into(),
];
prefs.dashboard_hidden_widgets = vec![
"errors".into(),
"from_future_release".into(),
"errors".into(),
];
let mut prefs = AdminPreferences {
dashboard_widgets: vec![
"errors".into(),
"from_future_release".into(),
"requests".into(),
"errors".into(),
],
dashboard_hidden_widgets: vec![
"errors".into(),
"from_future_release".into(),
"errors".into(),
],
..Default::default()
};
reconcile_dashboard_widgets(&mut prefs);
assert_eq!(prefs.dashboard_widgets[0], "errors");
assert_eq!(prefs.dashboard_widgets[1], "requests");
@@ -210,9 +212,11 @@ fn saved_dashboard_order_and_visibility_survive_new_factory_preset() {
#[test]
fn explicit_visible_stream_opt_in_is_not_reset_when_normalizing_saved_order() {
let mut prefs = AdminPreferences::default();
prefs.dashboard_widgets = default_admin_dashboard_widgets();
prefs.dashboard_hidden_widgets.clear();
let mut prefs = AdminPreferences {
dashboard_widgets: default_admin_dashboard_widgets(),
dashboard_hidden_widgets: Vec::new(),
..Default::default()
};
reconcile_dashboard_widgets(&mut prefs);
assert!(prefs.dashboard_hidden_widgets.is_empty());
assert!(prefs.dashboard_widgets.contains(&"cache_outcomes".into()));
@@ -224,8 +228,10 @@ fn explicit_visible_stream_opt_in_is_not_reset_when_normalizing_saved_order() {
fn dashboard_width_preferences_preserve_legacy_and_allow_custom_grids() {
let old: AdminPreferences = serde_json::from_str(r#"{"theme":"dark"}"#).unwrap();
assert!(old.dashboard_widget_sizes.is_none());
let mut prefs = AdminPreferences::default();
prefs.dashboard_widget_sizes = Some(std::collections::BTreeMap::new());
let mut prefs = AdminPreferences {
dashboard_widget_sizes: Some(std::collections::BTreeMap::new()),
..Default::default()
};
assert!(validate_admin_preferences(&prefs).is_ok());
for (id, span) in [
("http_protocol", 4),
+6 -3
View File
@@ -235,7 +235,6 @@ pub fn router(state: Arc<App>) -> Router {
.route("/api/traffic", get(traffic_status))
.route("/api/cache", get(cache_status))
.route("/api/transform", get(transform_status))
.route("/api/geoip/update", post(geoip_update))
.route("/api/cache/purge", post(cache_purge))
.route(
"/api/cache/invalidate",
@@ -260,7 +259,11 @@ pub fn router(state: Arc<App>) -> Router {
"/api/config/history/restore",
post(restore_config_history).layer(DefaultBodyLimit::max(4096)),
)
.route("/api/config/{section}", put(update_section))
.route("/api/config/geoip/update", post(geoip_update))
.route(
"/api/config/{section}",
get(configuration_section).put(update_section),
)
.route("/api/logs", get(logs))
.route("/api/live-logs", get(live_logs_stream))
.route("/api/file-logs", get(file_logs))
@@ -282,7 +285,7 @@ pub fn router(state: Arc<App>) -> Router {
.route("/api/metrics/summary", get(metrics_summary))
.fallback(|| async { reject(StatusCode::NOT_FOUND, "not_found") })
.layer(DefaultBodyLimit::max(
crate::config::MAX_CONFIG_BYTES + 1024,
crate::config::MAX_CONFIG_BYTES + 65_536,
))
.layer(middleware::from_fn_with_state(state.clone(), guard))
.layer(middleware::from_fn_with_state(state.clone(), observe_admin))
+2
View File
@@ -41,6 +41,8 @@ fn status_payload(state: &App) -> Value {
"security_headers",
"http3",
"tinyauth",
"authelia_forward_auth",
"authentik_forward_auth",
"trusted_forwarding",
"named_listeners",
"stream_proxy",
+338 -9
View File
@@ -461,6 +461,7 @@ mod tests {
let (router, store, _dir, secret, _) = fixture().await;
let old = request("/api/status")
.header("authorization", format!("Bearer {}", "a".repeat(64)))
.extension(PeerAddr("127.0.0.1:45000".parse().unwrap()))
.body(Body::empty())
.unwrap();
assert_eq!(
@@ -651,6 +652,231 @@ mod tests {
store.close().await;
}
#[test]
fn config_section_registry_read_and_replace_are_symmetric() {
let names = [
"listeners",
"docker-agents",
"http2",
"pages",
"cache",
"transform",
"forwarding",
"headers",
"prometheus",
"geoip",
"administration",
"tls",
"vhosts",
"web-services",
"php-fpm-profiles",
"redirects",
"not-found-hosts",
"streams",
"acl-profiles",
"host-policies",
"response",
"logging",
"global-security",
];
let mut original = Config::default();
original.global_security.compile().unwrap();
let expected = serde_json::to_value(&original).unwrap();
for name in names {
let section = ConfigSection::from_name(name).expect(name);
let value = section.read(&original);
let mut updated = original.clone();
section.replace(&mut updated, name, value.clone()).unwrap();
assert_eq!(serde_json::to_value(&updated).unwrap(), expected, "{name}");
assert_eq!(section.read(&updated), value, "{name}");
}
for name in [
"user-agent-blocking",
"error_log",
"history",
"storage",
"unknown",
"tls/nested",
] {
assert!(ConfigSection::from_name(name).is_none(), "{name}");
}
}
#[tokio::test]
async fn section_get_matches_put_payload_and_rejects_unknown_names() {
let (router, store, _dir, secret, _csrf) = fixture().await;
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
for name in [
"listeners",
"docker-agents",
"http2",
"pages",
"cache",
"transform",
"forwarding",
"headers",
"prometheus",
"geoip",
"administration",
"tls",
"vhosts",
"web-services",
"php-fpm-profiles",
"redirects",
"not-found-hosts",
"streams",
"acl-profiles",
"host-policies",
"response",
"logging",
"global-security",
] {
let response = router
.clone()
.oneshot(
request(&format!("/api/config/{name}"))
.header("cookie", &cookie)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK, "{name}");
let data = json_body(response).await;
assert_eq!(data["section"], name);
assert_eq!(data["revision"], 0);
assert!(!data["value"].is_null(), "{name}");
if name == "logging" {
assert!(data["value"].get("logging").is_some());
assert!(data["value"].get("error_log").is_some());
}
if name == "response" {
assert_eq!(data["value"].as_object().unwrap().len(), 3);
}
if name == "global-security" {
assert!(data["value"]["user_agent_blocking"].is_object());
}
}
let unknown = router
.clone()
.oneshot(
request("/api/config/user-agent-blocking")
.header("cookie", &cookie)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(unknown.status(), StatusCode::NOT_FOUND);
store.close().await;
}
#[test]
fn geoip_update_is_scoped_to_settings_for_general_api_tokens() {
assert!(general_api_token_allowed(
&Method::GET,
"/api/config/geoip",
"ro"
));
assert!(general_api_token_allowed(
&Method::PUT,
"/api/config/geoip",
"rw"
));
assert!(!general_api_token_allowed(
&Method::PUT,
"/api/config/geoip",
"ro"
));
assert!(general_api_token_allowed(
&Method::POST,
"/api/config/geoip/update",
"rw"
));
assert!(!general_api_token_allowed(
&Method::POST,
"/api/config/geoip/update",
"ro"
));
assert!(!general_api_token_allowed(
&Method::POST,
"/api/geoip/update",
"rw"
));
assert!(!general_api_token_allowed(
&Method::GET,
"/api/config/geoip/update",
"rw"
));
}
#[tokio::test]
async fn geoip_action_new_path_and_old_path_route_contract() {
let (router, store, _dir, secret, _csrf) = fixture().await;
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
for (path, expected) in [
("/api/config/geoip/update", StatusCode::METHOD_NOT_ALLOWED),
("/api/geoip/update", StatusCode::NOT_FOUND),
] {
let response = router
.clone()
.oneshot(
request(path)
.header("cookie", &cookie)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), expected, "{path}");
}
store.close().await;
}
#[tokio::test]
async fn vhosts_section_accepts_large_config_and_persists_it() {
let config = config::demo_config("127.0.0.1:9000".parse().unwrap());
let (router, store, _dir, secret, csrf) = fixture_with_config(config.clone()).await;
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
let mut vhosts = config.vhosts;
vhosts[0].routes = (0..5)
.map(|index| crate::vhost::PathRoute {
id: format!("large-{index}"),
path_prefix: format!("/large-{index}"),
response: Some(crate::vhost::LocationResponse {
body: "x".repeat(60_000),
..Default::default()
}),
..Default::default()
})
.collect();
let body = serde_json::to_vec(&json!({"expected_revision": 0, "value": vhosts})).unwrap();
assert!(body.len() > 262_144);
let response = router
.clone()
.oneshot(
request("/api/config/vhosts")
.method("PUT")
.header("content-type", "application/json")
.header("cookie", &cookie)
.header("origin", "http://127.0.0.1:9090")
.header("x-rustproxy-request", "1")
.header("x-csrf-token", &csrf)
.body(Body::from(body))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(json_body(response).await["revision"], 1);
let saved = store.load().await.unwrap();
assert_eq!(saved.revision, 1);
assert_eq!(saved.config.vhosts[0].routes.len(), 5);
let history = store.configuration_revision(1).await.unwrap().unwrap();
assert_eq!(history.config.vhosts[0].routes.len(), 5);
store.close().await;
}
#[tokio::test]
async fn section_updates_are_revisioned_and_do_not_require_full_config_payloads() {
let (router, store, _dir, secret, csrf) = fixture().await;
@@ -658,7 +884,7 @@ mod tests {
let logging = Config::default().logging;
let body = serde_json::to_vec(&json!({
"expected_revision": 0,
"value": logging,
"value": {"logging": logging, "error_log": Config::default().error_log},
}))
.unwrap();
let response = router
@@ -720,9 +946,10 @@ mod tests {
.header("x-rustproxy-request", "1")
.header("x-csrf-token", &csrf)
.body(Body::from(
serde_json::to_vec(
&json!({"expected_revision":0,"value":Config::default().logging}),
)
serde_json::to_vec(&json!({"expected_revision":0,"value":{
"logging":Config::default().logging,
"error_log":Config::default().error_log
}}))
.unwrap(),
))
.unwrap(),
@@ -750,6 +977,95 @@ mod tests {
store.close().await;
}
#[tokio::test]
async fn global_security_section_saves_rules_and_existing_ua_atomically() {
let (router, _store, _dir, secret, csrf) = fixture().await;
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
let value = serde_json::json!({
"country_access": {"mode":"block", "countries":["ru"]},
"method_policy": {"mode":"block", "methods":["TRACE"]},
"security_headers": {"nosniff":true},
"user_agent_blocking": {"enabled":true, "patterns":["bad-bot"]}
});
let body = serde_json::to_vec(&json!({"expected_revision":0, "value":value})).unwrap();
let response = router
.clone()
.oneshot(
request("/api/config/global-security")
.method("PUT")
.header("content-type", "application/json")
.header("cookie", &cookie)
.header("origin", "http://127.0.0.1:9090")
.header("x-rustproxy-request", "1")
.header("x-csrf-token", &csrf)
.body(Body::from(body))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let json = json_body(response).await;
assert_eq!(json["revision"], 1);
assert_eq!(
json["config"]["global_security"]["country_access"]["countries"][0],
"RU"
);
assert_eq!(
json["config"]["global_security"]["security_headers"]["nosniff"],
true
);
assert_eq!(
json["config"]["global_security"]["user_agent_blocking"]["patterns"][0],
"bad-bot"
);
assert!(json["config"].get("user_agent_blocking").is_none());
// A partial section update must not silently reset the existing UA policy.
let body = serde_json::to_vec(&json!({
"expected_revision":1,
"value": {"method_policy":{"mode":"off","methods":[]}}
}))
.unwrap();
let response = router
.clone()
.oneshot(
request("/api/config/global-security")
.method("PUT")
.header("content-type", "application/json")
.header("cookie", &cookie)
.header("origin", "http://127.0.0.1:9090")
.header("x-rustproxy-request", "1")
.header("x-csrf-token", &csrf)
.body(Body::from(body))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
// GET returns the same section shape that PUT accepts.
let read = router
.clone()
.oneshot(
request("/api/config/global-security")
.header("cookie", &cookie)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(read.status(), StatusCode::OK);
let view = json_body(read).await;
assert_eq!(view["revision"], 1);
assert_eq!(view["section"], "global-security");
assert_eq!(
view["value"]["user_agent_blocking"]["patterns"][0],
"bad-bot"
);
assert!(view.get("config").is_none());
assert!(view["value"].get("global_security").is_none());
}
#[tokio::test]
async fn logging_section_persists_error_log_settings_atomically() {
let (router, store, dir, secret, csrf) = fixture().await;
@@ -1329,6 +1645,7 @@ mod tests {
.method("POST")
.header("authorization", format!("Bearer {token}"))
.header("content-type", "application/json")
.extension(PeerAddr("127.0.0.1:45000".parse().unwrap()))
.body(Body::from("{}"))
.unwrap(),
)
@@ -1347,7 +1664,11 @@ mod tests {
)
.await
.unwrap();
assert_eq!(forbidden_scope.status(), StatusCode::UNAUTHORIZED);
assert_eq!(forbidden_scope.status(), StatusCode::FORBIDDEN);
assert_eq!(
json_body(forbidden_scope).await,
json!({"error":"certificate_api.invalid_route"})
);
let revoked = router
.clone()
@@ -1488,7 +1809,11 @@ mod tests {
)
.await
.unwrap();
assert_eq!(denied.status(), StatusCode::UNAUTHORIZED);
assert_eq!(denied.status(), StatusCode::FORBIDDEN);
assert_eq!(
json_body(denied).await,
json!({"error":"certificate_api.invalid_route"})
);
let no_csrf = router
.clone()
.oneshot(
@@ -2295,7 +2620,7 @@ mod tests {
("/api/acme/issue", "POST"),
("/api/acme/renew", "POST"),
("/api/docker/refresh", "POST"),
("/api/geoip/update", "POST"),
("/api/config/geoip/update", "POST"),
("/api/cache/purge", "POST"),
("/api/cache/invalidate", "POST"),
("/api/cache/bans", "GET"),
@@ -2458,6 +2783,7 @@ mod tests {
request(&path)
.header("cookie", cookie)
.header("authorization", format!("Bearer {secret}"))
.extension(PeerAddr("127.0.0.1:45000".parse().unwrap()))
.body(Body::empty())
.unwrap(),
)
@@ -3637,7 +3963,7 @@ mod tests {
)
.await
.unwrap();
assert_eq!(old_route.status(), StatusCode::METHOD_NOT_ALLOWED);
assert_eq!(old_route.status(), StatusCode::NOT_FOUND);
store.close().await;
}
@@ -3787,7 +4113,10 @@ mod tests {
.await
.unwrap();
assert_eq!(summary.status(), StatusCode::OK);
assert_eq!(summary.headers()["cache-control"], "no-store");
assert_eq!(
summary.headers()["cache-control"],
"private, no-store, no-cache, no-transform"
);
let summary = json_body(summary).await;
assert!(summary.get("prometheus").is_none());
assert_eq!(
+1 -2
View File
@@ -21,8 +21,7 @@ function syncConfigHistoryRestoreButton() {
button.hidden = configHistorySelectedRevision === null;
button.disabled = busy || !envelope || !Number.isSafeInteger(revision) || revision >= Number(envelope.revision);
}
// Compare leaf fields, not serialized object/array blobs. Object IDs are used
// for stable array paths so reordering vhosts does not become a huge fake diff.
// Leaf-level diff keyed by object IDs, so reordering is not a change.
function configDiff(before, after, path = "$", output = [], depth = 0) {
if (output.length >= 200 || Object.is(before, after)) return output;
if (depth >= 24) {
+82 -7
View File
@@ -112,7 +112,7 @@ function appConfirm(message, options = {}) {
}
function apiArea(path) {
const section = path.match(/^\/api\/config\/([^/?]+)/)?.[1];
if (section) return ({ "listeners": "settings", "http2": "settings", "docker-agents": "dockerVhosts", "response": "settings", "pages": "settings", "cache": "cache", "transform": "settings", "logging": "settings", "geoip": "settings", "forwarding": "settings", "headers": "settings", "prometheus": "settings", "administration": "administration", "tls": "tls", "vhosts": "vhosts", "redirects": "redirects", "not_found_hosts": "notfound", "streams": "streams", "acl-profiles": "aclProfiles", "host-policies": "security", "user-agent-blocking": "security" })[section] || "configuration";
if (section) return ({ "listeners": "settings", "http2": "settings", "docker-agents": "dockerVhosts", "response": "settings", "pages": "settings", "cache": "cache", "transform": "settings", "logging": "settings", "geoip": "settings", "forwarding": "settings", "headers": "settings", "prometheus": "settings", "administration": "administration", "tls": "tls", "vhosts": "vhosts", "redirects": "redirects", "not-found-hosts": "notfound", "streams": "streams", "acl-profiles": "aclProfiles", "host-policies": "security", "global-security": "settings" })[section] || "configuration";
if (path === "/api/config") return "configuration";
if (path.startsWith("/api/traffic")) return "traffic";
if (path.startsWith("/api/cache")) return "cache";
@@ -165,7 +165,7 @@ function setPanel(name, navigate = true) {
if (name === "filelogs") void window.rustProxyFileLogs?.refresh();
if (name === "tls") void refreshCertificateInventory().then(() => { if (authenticated && currentPanel === "tls") renderCertificates(); });
}
function bytes(value) { if (value < 1024) return `${value} B`; for (const unit of ["KiB", "MiB", "GiB", "TiB"]) { value /= 1024; if (value < 1024 || unit === "TiB") return `${value.toFixed(value < 10 ? 1 : 0)} ${unit}`; } }
function bytes(value) { if (value < 1024) return `${Math.round(value)} B`; for (const unit of ["KiB", "MiB", "GiB", "TiB"]) { value /= 1024; if (value < 1024 || unit === "TiB") return `${value.toFixed(value < 10 ? 1 : 0)} ${unit}`; } }
function uptime(value) {
let seconds = Math.max(0, Math.floor(Number(value) || 0));
if (seconds < 60) return `${seconds}s`;
@@ -232,7 +232,23 @@ window.rustProxyOverviewSamples.onChange(() => {
}
redrawOverviewHistoryCharts();
});
function renderStatus(s, systemSample = false) {
// Safari has no CSS scroll anchoring: keep the element under the viewport centre in place
// when a live refresh changes the height of content above it.
const nativeScrollAnchoring = typeof CSS !== "undefined" && CSS.supports?.("overflow-anchor", "auto");
function preserveScrollAnchor(render) {
if (nativeScrollAnchoring || window.scrollY <= 0) return render();
const probe = document.elementFromPoint(window.innerWidth / 2, window.innerHeight / 2);
const anchor = probe?.closest?.("article, tr, .mini-live-chart, .network-interface-row") || probe;
const before = anchor?.isConnected ? anchor.getBoundingClientRect().top : null;
const result = render();
if (before !== null && anchor.isConnected) {
const shift = anchor.getBoundingClientRect().top - before;
if (Math.abs(shift) >= 1) window.scrollBy(0, shift);
}
return result;
}
function renderStatus(s, systemSample = false) { return preserveScrollAnchor(() => renderStatusNow(s, systemSample)); }
function renderStatusNow(s, systemSample = false) {
const firstStatus = latestStatus === null;
latestStatus = {
...(latestStatus || {}), ...s,
@@ -386,9 +402,6 @@ function renderConfig() {
$("host-policy-list").replaceChildren(...(config.host_policies || []).map((policy, index) => { const box = el("div", undefined, "entity"); box.append(el("span", policy.hosts.join(", "), "entity-title"), el("p", `${t("aclDefault")}: ${t(policy.acl.default || "deny")}\n${rulesText(policy.acl)}`, "entity-details")); const buttons = el("div", undefined, "entity-actions"); buttons.append(action(t("edit"), () => { $("policy-hosts").value = policy.hosts.join(", "); $("policy-default").value = policy.acl.default || "deny"; $("policy-rules").value = rulesText(policy.acl); const editor = $("security-policy-editor"); if (editor) { editor.open = true; editor.scrollIntoView({ block: "nearest" }); } }), action(t("remove"), async () => { if (await appConfirm(t("confirmDelete"), { danger: true })) { const next = structuredClone(config); next.host_policies.splice(index, 1); await saveSection("host-policies", next.host_policies); } })); box.append(buttons); return box; }));
if (!config.host_policies?.length) $("host-policy-list").append(el("p", t("noPolicies"), "empty"));
if ($("security-policy-count")) $("security-policy-count").textContent = String((config.host_policies || []).length);
const uaPolicy = config.user_agent_blocking || {};
if ($("global-ua-block-enabled")) $("global-ua-block-enabled").checked = uaPolicy.enabled === true;
if ($("global-ua-block-patterns")) $("global-ua-block-patterns").value = (uaPolicy.patterns || []).join("\n");
}
async function saveRequest(path, payload) { if (busy) { toast(t("busy"), true); return false; } if (!authenticated || !envelope) return false; busy = true; const epoch = authEpoch; try { const value = await api(path, { method: "PUT", body: JSON.stringify(payload) }); if (!authenticated || epoch !== authEpoch) return false; envelope = value; if (path === "/api/config" || path === "/api/config/tls") await refreshCertificateInventory(); if (!authenticated || epoch !== authEpoch || !envelope) return false; renderConfig(); toast(t("saved")); await refresh(true); return true; } catch (e) { toast(e.message, true); if (e.status === 401) lock(); return false; } finally { busy = false; } }
async function save(config) { return saveRequest("/api/config", { expected_revision: envelope?.revision, config }); }
@@ -514,7 +527,69 @@ document.addEventListener("click", event => { if (dashboardSidebar?.classList.co
document.addEventListener("keydown", event => { if (event.key === "Escape" && dashboardSidebar?.classList.contains("mobile-nav-open")) { setMobileMenu(false); mobileMenuToggle?.focus(); } });
window.addEventListener("popstate", () => { setPanel(panelFromLocation(), false); setMobileMenu(false); });
$("host-policy-form").addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; if (!envelope || busy) return; try { await withFormSubmitState(form, event.submitter, async () => { const hosts = [...new Set($("policy-hosts").value.split(",").map(h => h.trim().toLowerCase()).filter(Boolean))]; const acl = { default: $("policy-default").value, rules: parseRules($("policy-rules").value, $("policy-rules")) }; const next = structuredClone(envelope.config); next.host_policies = (next.host_policies || []).map(p => ({ ...p, hosts: p.hosts.filter(h => !hosts.includes(h)) })).filter(p => p.hosts.length); next.host_policies.push({ hosts, acl }); if (await saveSection("host-policies", next.host_policies)) { form.reset(); if ($("security-policy-editor")) $("security-policy-editor").open = false; } }); } catch (e) { toast(e.message, true); } });
$("global-ua-block-form")?.addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; if (!envelope || busy) return; try { await withFormSubmitState(form, event.submitter, async () => { const patterns = [...new Set($("global-ua-block-patterns").value.split(/\r?\n/).map(value => value.trim()).filter(Boolean))]; await saveSection("user-agent-blocking", { enabled: $("global-ua-block-enabled").checked, patterns }); }); } catch (e) { toast(e.message, true); } });
// Global HTTP controls are a single atomic API section. A header-only save preserves
// the current block rules; a block-only save preserves the current security headers.
const globalLines = id => [...new Set($(id).value.split(/[\n,]+/).map(v => v.trim()).filter(Boolean))];
const globalPatternLines = id => [...new Set($(id).value.split(/\r?\n/).map(v => v.trim()).filter(Boolean))];
function globalSecurityPayload(security, ua) {
return { ...security, user_agent_blocking: ua };
}
$("global-security-form")?.addEventListener("submit", async event => {
event.preventDefault(); const form = event.currentTarget; if (!envelope || busy) return;
try {
await withFormSubmitState(form, event.submitter, async () => {
const previous = envelope.config.global_security || {}, rate = previous.rate_limit || {};
const countryMode = $("global-country-mode").value;
const countries = globalLines("global-country-list").map(v => v.toUpperCase());
if (countryMode !== "off" && !countries.length) throw fieldError("global-country-list", t("globalCountriesRequired"));
if (countries.some(v => !/^[A-Z]{2}$/.test(v))) throw fieldError("global-country-list", t("globalCountriesInvalid"));
const methodMode = $("global-method-mode").value;
const methods = [...new Set($("global-method-list").value.split(/[\s,]+/).map(v => v.trim().toUpperCase()).filter(Boolean))];
if (methodMode !== "off" && !methods.length) throw fieldError("global-method-list", t("globalMethodsRequired"));
const contentTypes = globalLines("global-content-type-list").map(v => v.toLowerCase());
if ($("global-content-type-enabled").checked && !contentTypes.length) throw fieldError("global-content-type-list", t("globalContentTypesRequired"));
const acl = $("global-ip-enabled").checked ? { default: $("global-ip-default").value, rules: parseRules($("global-ip-rules").value, $("global-ip-rules")) } : null;
const ua = { enabled: $("global-ua-block-enabled").checked, patterns: globalPatternLines("global-ua-block-patterns") };
const security = {
...previous,
country_access: { mode: countryMode, countries },
acl,
method_policy: { mode: methodMode, methods },
content_type_policy: { enabled: $("global-content-type-enabled").checked, blocked: contentTypes },
rate_limit: { ...rate, enabled: $("global-rate-enabled").checked,
requests: Number($("global-rate-requests").value),
window_seconds: Number($("global-rate-window").value),
burst: Number($("global-rate-burst").value) },
};
if (await saveSection("global-security", globalSecurityPayload(security, ua))) {
setSettingsDirty("global-security-form", false); renderSettings();
}
});
} catch (e) { toast(e.message, true); }
});
$("global-security-headers-form")?.addEventListener("submit", async event => {
event.preventDefault(); const form = event.currentTarget; if (!envelope || busy) return;
try {
await withFormSubmitState(form, event.submitter, async () => {
const previous = envelope.config.global_security || {};
const optional = id => $(id).value.trim() || null;
const security = { ...previous, security_headers: {
hsts: { enabled: $("global-hsts-enabled").checked,
max_age_seconds: Number($("global-hsts-age").value),
include_subdomains: $("global-hsts-enabled").checked && $("global-hsts-sub").checked,
preload: $("global-hsts-enabled").checked && $("global-hsts-preload").checked },
nosniff: $("global-nosniff").checked, frame_options: $("global-frame").value,
referrer_policy: optional("global-referrer"),
content_security_policy: optional("global-csp"),
permissions_policy: optional("global-permissions")
}};
if (await saveSection("global-security", globalSecurityPayload(security, previous.user_agent_blocking || { enabled: false, patterns: [] }))) {
setSettingsDirty("global-security-headers-form", false); renderSettings();
}
});
} catch (e) { toast(e.message, true); }
});
$("password-form").addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; try { await withFormSubmitState(form, event.submitter, async () => { if ($("new-password").value !== $("new-password-confirm").value) throw fieldError("new-password-confirm", t("passwordMismatch")); await api("/api/auth/password", { method: "POST", body: JSON.stringify({ current_password: $("current-password").value, new_password: $("new-password").value }) }); lock(); toast(t("passwordChanged")); }); } catch (e) { toast(e.message, true); } });
$("revoke-sessions").addEventListener("click", async () => { if (!(await appConfirm(t("confirmRevoke"), { danger: true }))) return; try { await api("/api/auth/logout-all", { method: "POST" }); lock(); toast(t("locked")); } catch (e) { toast(e.message, true); } });
$("security-audit-open").addEventListener("click", () => { const dialog = $("audit-dialog"); if (!dialog.open) dialog.showModal(); void loadAudit(); });
+1 -3
View File
@@ -144,9 +144,7 @@ window.rustProxyDashboardLayout = (() => {
widen.addEventListener("click", () => resize(id, 1));
const widthValue = el("span", `${width}/12`, "overview-layout-width-value");
widthValue.title = t("overviewLayoutWidth");
// The visual editor intentionally keeps uniformly sized previews.
// Group sizing separately from ordering/hiding so both sets wrap
// inside a narrow dialog rather than spilling into adjacent tiles.
// Size and order controls are grouped separately so they wrap in narrow dialogs.
const sizeActions = el("span", undefined, "overview-layout-size-actions");
sizeActions.append(narrow, widthValue, widen);
const orderActions = el("span", undefined, "overview-layout-order-actions");
+39 -2
View File
@@ -8,10 +8,12 @@ window.rustProxyExtraOverview = (() => {
"extra_response_latency", "extra_backend_unhealthy",
"extra_total_rps", "extra_active_exchanges", "extra_h1_share", "extra_h3_share",
"extra_cache_miss_rate", "extra_backend_active",
"extra_backends_healthy", "extra_error_share", "extra_rejected_rate", "extra_cert_expiry",
];
const bounded = new Set([
"extra_process_fds", "extra_cache_memory", "extra_cold_queue_memory", "extra_cold_storage",
"extra_h1_share", "extra_h2_share", "extra_h3_share",
"extra_backends_healthy", "extra_cert_expiry",
]);
const histories = new Map(), lastCounter = new Map(), lastValue = new Map();
const lastUnits = new Map(), lastSegments = new Map();
@@ -98,11 +100,36 @@ window.rustProxyExtraOverview = (() => {
unit: "count", detail: ""
};
}
if (id === "extra_backends_healthy") {
const members = Array.isArray(status.backend_pools)
? status.backend_pools.flatMap(pool => (pool.members || []).map(member => ({ pool: pool.id, ...member }))) : null;
if (!members) return { value: null, unit: "count", detail: "" };
const monitored = members.filter(member => member.health_check_enabled !== false && member.health_mode !== "off");
const down = monitored.filter(member => member.status === "down" || member.healthy === false);
const up = monitored.length - down.length;
return {
value: monitored.length ? `${fmt(up)} / ${fmt(monitored.length)}` : null, unit: "text",
meterPercent: monitored.length ? up / monitored.length * 100 : null,
detail: !monitored.length ? t("overviewExtraBackendsNone")
: down.length ? `${t("overviewExtraBackendsDown")}: ${down.slice(0, 3).map(member => `${member.pool}/${member.id}`).join(", ")}${down.length > 3 ? " …" : ""}`
: t("overviewExtraBackendsAllUp"),
};
}
if (id === "extra_cert_expiry") {
const inventory = typeof certificateInventory !== "undefined" && certificateInventory instanceof Map ? [...certificateInventory.values()] : [];
const dated = inventory.filter(cert => Number.isFinite(Number(cert?.days_remaining)))
.sort((a, b) => Number(a.days_remaining) - Number(b.days_remaining));
if (!dated.length) return { value: null, unit: "days", detail: t("overviewExtraCertNone") };
const nearest = dated[0], days = Number(nearest.days_remaining);
const name = (Array.isArray(nearest.covered_hosts) && nearest.covered_hosts[0]) || (Array.isArray(nearest.hosts) && nearest.hosts[0]) || nearest.id;
return { value: days, unit: "days", meterPercent: Math.max(0, Math.min(100, days / 90 * 100)), detail: `${name} · ${fmt(dated.length)} ${t("overviewExtraCertCount")}` };
}
if (id === "extra_cache_memory") return ramBudget(c.resident_bytes, c.hot_limit_bytes);
if (id === "extra_cold_queue_memory") return ramBudget(c.cold_queue_bytes, c.cold_queue_limit_bytes);
if (id === "extra_cold_storage") return ramBudget(c.cold_bytes, c.cold_limit_bytes);
if (!["extra_tls_error_rate", "extra_request_error_rate", "extra_cache_hit_rate", "extra_h2_share",
"extra_total_rps", "extra_h1_share", "extra_h3_share", "extra_cache_miss_rate"].includes(id))
"extra_total_rps", "extra_h1_share", "extra_h3_share", "extra_cache_miss_rate",
"extra_error_share", "extra_rejected_rate"].includes(id))
return { value: null, unit: "count", detail: "" };
const fields = ({
extra_tls_error_rate: [m.tls_handshake_errors],
@@ -113,8 +140,10 @@ window.rustProxyExtraOverview = (() => {
extra_h3_share: [m.http3_requests, m.requests],
extra_total_rps: [m.requests],
extra_cache_miss_rate: [c.misses],
extra_error_share: [m.errors, m.requests],
extra_rejected_rate: [m.rejected_requests, m.rejected_connections],
})[id].map(positive);
const unit = ["extra_cache_hit_rate", "extra_h2_share", "extra_h1_share", "extra_h3_share"].includes(id)
const unit = ["extra_cache_hit_rate", "extra_h2_share", "extra_h1_share", "extra_h3_share", "extra_error_share"].includes(id)
? "percent" : "rate";
if (fields.some(v => v === null)) {
lastCounter.delete(id);
@@ -124,6 +153,12 @@ window.rustProxyExtraOverview = (() => {
const result = elapsedDelta(id, fields, now);
if (!result) return { value: null, unit, detail: "", slices: null };
const { deltas, seconds } = result;
if (id === "extra_rejected_rate") return {
value: (deltas[0] + deltas[1]) / seconds, unit, detail: `${t("overviewExtraRejectedDetail")}: ${fmt(fields[0])} / ${fmt(fields[1])}`
};
if (id === "extra_error_share") return {
value: deltas[1] > 0 ? Math.min(100, deltas[0] / deltas[1] * 100) : 0, unit, detail: `${t("overviewExtraErrorShareDetail")}: ${fmt(deltas[0])} / ${fmt(deltas[1])}`
};
if (unit === "rate") return { value: deltas[0] / seconds, unit, detail: "" };
const sum = id === "extra_cache_hit_rate" ? deltas[0] + deltas[1] : deltas[1];
return {
@@ -137,6 +172,8 @@ window.rustProxyExtraOverview = (() => {
if (unit === "bytes") return fmtBytes(value);
if (unit === "ms") return `${fmt(value, 1)} ms`;
if (unit === "rate") return `${fmt(value, 1)}/s`;
if (unit === "days") return `${fmt(value)} ${t("overviewExtraDaysUnit")}`;
if (unit === "text") return String(value);
return fmt(value);
}
function points(id, history, unit) {
+13 -4
View File
@@ -9,18 +9,27 @@ window.rustProxyOverviewSamples = (() => {
} catch { /* Private browsing or storage policy: temporary selection still works. */ }
let limit = saved;
const callbacks = new Set();
let confirmationTimer = null;
const getLimit = () => limit;
const trim = array => {
if (Array.isArray(array) && array.length > limit) array.splice(0, array.length - limit);
return array;
};
function sync(messageKey = limit === saved ? "overviewSamplesSaved" : "overviewSamplesTemporary") {
// The status is shown only for a temporary choice, a storage problem or briefly after saving.
function sync(messageKey = limit === saved ? null : "overviewSamplesTemporary", confirmation = false) {
const select = document.getElementById("overview-live-samples");
if (select) select.value = String(limit);
const state = document.getElementById("overview-samples-state");
if (state) {
state.dataset.i18n = messageKey;
state.textContent = t(messageKey);
clearTimeout(confirmationTimer);
if (messageKey) {
state.dataset.i18n = messageKey;
state.textContent = t(messageKey);
state.style.visibility = "visible";
} else {
state.style.visibility = "hidden";
}
if (confirmation) confirmationTimer = setTimeout(() => { state.style.visibility = "hidden"; }, 2500);
}
const save = document.getElementById("overview-samples-save");
if (save) save.disabled = limit === saved;
@@ -43,7 +52,7 @@ window.rustProxyOverviewSamples = (() => {
const changed = count !== limit;
limit = count;
if (changed) for (const callback of callbacks) callback(limit);
sync();
if (persist) sync("overviewSamplesSaved", true); else sync();
return true;
}
function init() {
+2 -1
View File
@@ -108,6 +108,7 @@
output.textContent = target.hidden ? "" : commands({ ...options, user: account.value });
output.hidden = !output.textContent;
description.hidden = output.hidden;
copy.hidden = !output.textContent;
copy.disabled = !output.textContent;
empty.hidden = !!output.textContent;
empty.textContent = t(options.kind === "web" ? "pathPermissionsWebPrompt" : "pathPermissionsCertificatePrompt");
@@ -133,7 +134,7 @@
const empty = el("p", undefined, "fine");
details.append(summary, label, help);
actions.append(copy, details);
target.replaceChildren(description, output, empty, actions);
target.replaceChildren(output, description, empty, actions);
view = { target, options, description, summary, help, accountLabel, account, output, empty, copy };
views.set(target, view);
account.addEventListener("input", () => update(view));
+5 -3
View File
@@ -1,6 +1,4 @@
// One continuity policy for all browser-side counter-derived rates.
// Background tabs cannot supply a valid instantaneous rate for an interval
// spanning the pause. Keep the plotted history and rebase on the next sample.
// Shared rate sampling: rebase after background-tab pauses instead of plotting a false spike.
const rustProxyRateSampling = (() => {
let generation = 0;
const pause = () => { generation += 1; };
@@ -19,6 +17,10 @@ const rustProxyRateSampling = (() => {
at - previous.at <= Math.max(10000, seconds * 3000));
}
function difference(previous, values, at, intervalSeconds = 3) {
if (previous && previous.generation === generation && Number.isFinite(at) &&
Number.isFinite(previous.at) && at >= previous.at && at - previous.at < 250) {
return { next: previous, deltas: null, seconds: null, rapid: true };
}
const next = { ...stamp(at), values: [...values] };
if (!continuous(previous, at, intervalSeconds) ||
values.some((value, i) => !Number.isFinite(value) || value < 0 ||
+1 -1
View File
@@ -182,7 +182,7 @@ const rustProxyRefresh = (() => {
const value = await api("/api/system-info", { signal });
if (!isCurrent()) return;
latestStatus = { ...(latestStatus || {}), ...value };
if (latestStatus.metrics) renderDiagnostics(latestStatus, false, true);
if (latestStatus.metrics) preserveScrollAnchor(() => renderDiagnostics(latestStatus, false, true));
window.rustProxySettings?.render();
}
});
+55
View File
@@ -76,6 +76,61 @@ function renderSettings() {
$("error-log-status").textContent = errorSink ? `${t("loggingQueue")}: ${fmt(errorSink.queue_dropped)} ${t("loggingDropped")} / ${fmt(errorSink.written)} ${t("loggingSent")} / ${fmt(errorSink.errors)} ${t("errors")} · ${t("loggingCapacity")}: ${fmt(errorSink.queue_capacity)}` : "";
if (!settingsDirty.has("forwarding-form")) { $("trusted-proxies").value = (config.forwarding?.trusted_proxies || []).join("\n"); $("trusted-proto").checked = !!config.forwarding?.trust_forwarded_proto; }
if (!settingsDirty.has("headers-form")) { for (const direction of ["request", "response"]) renderHeaderRules(direction, config.headers?.[direction] || []); }
const global = config.global_security || {};
if (!settingsDirty.has("global-security-form")) {
const country = global.country_access || {}, methods = global.method_policy || {}, content = global.content_type_policy || {}, rate = global.rate_limit || {};
const ua = global.user_agent_blocking || {}, acl = global.acl;
$("global-country-mode").value = country.mode || "off";
$("global-country-list").value = (country.countries || []).join("\n");
$("global-ua-block-enabled").checked = ua.enabled === true;
$("global-ua-block-patterns").value = (ua.patterns || []).join("\n");
$("global-ip-enabled").checked = !!acl;
$("global-ip-default").value = acl?.default || "allow";
$("global-ip-rules").value = rulesText(acl);
$("global-method-mode").value = methods.mode || "off";
$("global-method-list").value = (methods.methods || []).join("\n");
$("global-content-type-enabled").checked = content.enabled === true;
$("global-content-type-list").value = (content.blocked || []).join("\n");
$("global-rate-enabled").checked = rate.enabled === true;
$("global-rate-requests").value = rate.requests ?? 120;
$("global-rate-window").value = rate.window_seconds ?? 60;
$("global-rate-burst").value = rate.burst ?? 30;
syncGlobalSecurityControls();
}
if (!settingsDirty.has("global-security-headers-form")) {
const headers = global.security_headers || {}, hsts = headers.hsts || {};
$("global-hsts-enabled").checked = hsts.enabled === true;
$("global-hsts-age").value = hsts.max_age_seconds ?? 31536000;
$("global-hsts-sub").checked = hsts.include_subdomains === true;
$("global-hsts-preload").checked = hsts.preload === true;
$("global-nosniff").checked = headers.nosniff === true;
$("global-frame").value = headers.frame_options || "preserve";
$("global-referrer").value = headers.referrer_policy || "";
$("global-csp").value = headers.content_security_policy || "";
$("global-permissions").value = headers.permissions_policy || "";
syncGlobalSecurityControls();
}
}
function syncGlobalSecurityControls() {
for (const [toggle, children] of [
["global-country-mode", ["global-country-list"]],
["global-ua-block-enabled", ["global-ua-block-patterns"]],
["global-ip-enabled", ["global-ip-default", "global-ip-rules"]],
["global-method-mode", ["global-method-list"]],
["global-content-type-enabled", ["global-content-type-list"]],
["global-rate-enabled", ["global-rate-requests", "global-rate-window", "global-rate-burst"]],
["global-hsts-enabled", ["global-hsts-age", "global-hsts-sub", "global-hsts-preload"]]
]) {
const source = $(toggle);
const enabled = source.type === "checkbox" ? source.checked : source.value !== "off";
for (const id of children) $(id).disabled = !enabled;
}
}
for (const id of ["global-country-mode", "global-ua-block-enabled", "global-ip-enabled", "global-method-mode", "global-content-type-enabled", "global-rate-enabled", "global-hsts-enabled"]) {
$(id)?.addEventListener("change", syncGlobalSecurityControls);
}
for (const id of ["global-security-form", "global-security-headers-form"]) {
for (const type of ["input", "change"]) $(id)?.addEventListener(type, () => setSettingsDirty(id));
}
function effectiveCacheMemory() {
const d = latestStatus?.system || {}, host = Number(d.memory_total_bytes), cgroup = Number(d.cgroup_memory_max_bytes);
+1 -3
View File
@@ -1,6 +1,4 @@
/* Theme palettes and appearance tokens. Runtime preference variables remain writable by JavaScript.
* Order is defined in manifest.json; responsive rules are centralized in 70-responsive.css.
*/
/* Theme palettes and appearance tokens */
:root {
color-scheme: dark;
+57 -7
View File
@@ -1,6 +1,4 @@
/* Shared reset, typography, controls, forms, tables, cards and overlay primitives.
* Order is defined in manifest.json; responsive rules are centralized in 70-responsive.css.
*/
/* Shared reset, typography, controls, forms, tables, cards and overlay primitives */
* {
box-sizing: border-box;
@@ -1304,8 +1302,9 @@ body[data-density=compact] .page-footer {
}
.about-dialog {
width: calc(min(560px, 100vw - 24px));
overflow: hidden;
width: calc(min(640px, 100vw - 24px));
max-height: calc(100vh - 24px);
overflow: auto;
}
.about-dialog .window-heading button {
@@ -1370,6 +1369,38 @@ body[data-density=compact] .page-footer {
line-height: 1.65;
}
.about-license {
display: grid;
gap: 8px;
padding: 12px 14px;
border: 1px solid var(--line);
background: var(--panel);
}
.about-license h3 {
margin: 0;
}
.about-license p {
margin: 0;
line-height: 1.6;
}
.about-license ul {
display: grid;
gap: 4px;
margin: 0;
padding-inline-start: 18px;
color: var(--muted);
line-height: 1.5;
}
.about-links {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 6px;
}
.about-repository {
justify-content: space-between;
gap: 12px;
@@ -1381,7 +1412,9 @@ body[data-density=compact] .page-footer {
.about-repository span:last-child {
color: var(--muted);
font: 10px var(--mono);
overflow-wrap: anywhere;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
html[data-table-density=compact] :is(table th, table td) {
@@ -1479,7 +1512,6 @@ html[data-table-density=ultra] :is(table th, table td) {
align-items: center;
}
/* Keep generated ACL instructions in the original metrics-example code style. */
.path-permissions {
min-width: 0;
}
@@ -1504,4 +1536,22 @@ html[data-table-density=ultra] :is(table th, table td) {
display: grid;
gap: 6px;
max-width: 260px;
}
#certificate-external-acl-example.path-permissions {
margin-top: 20px;
border-top: 1px solid var(--line);
padding-top: 15px;
}
.path-permissions-commands {
margin-bottom: 0;
}
.path-permissions-commands+.path-permissions-description {
margin: 7px 0 0;
}
.path-permissions-actions {
margin-top: 10px;
}
+12 -4
View File
@@ -1,6 +1,4 @@
/* Application startup, authentication, navigation, page chrome and draft dock.
* Order is defined in manifest.json; responsive rules are centralized in 70-responsive.css.
*/
/* Application startup, authentication, navigation, page chrome and draft dock */
html.app-loading .shell {
visibility: hidden;
@@ -500,6 +498,16 @@ main {
font-size: 12px;
}
.auth-provider-options {
display: flex;
flex-wrap: wrap;
gap: 7px 18px;
}
.auth-provider-options>.vh-check {
flex: 1 1 250px;
}
.auth-advanced {
border-top: 1px solid var(--line);
padding-top: 4px;
@@ -770,4 +778,4 @@ body[data-density=compact] .page-intro .lead {
to {
visibility: visible;
}
}
}
+16 -5
View File
@@ -1,6 +1,4 @@
/* Overview metrics, charts, dashboard density, inline layout controls and presets.
* Order is defined in manifest.json; responsive rules are centralized in 70-responsive.css.
*/
/* Overview metrics, charts, dashboard density, inline layout controls and presets */
html[data-theme=ultra_dark] :is(.card, .stat, dialog) {
box-shadow: 0 6px 24px rgba(0, 0, 0, .18);
@@ -76,8 +74,7 @@ html[data-theme=cream] :is(.card, .stat, dialog) {
stroke-dasharray: var(--chart-grid-dash, 2 5);
}
/* Optional X/Y scale readouts do not change SVG coordinates or chart ranges.
* Labels are HTML, not distorted SVG text under preserveAspectRatio="none". */
/* Axis labels are HTML overlays, not stretched SVG text. */
.rp-chart-axis-host {
position: relative;
display: flow-root;
@@ -2122,3 +2119,17 @@ html[data-dashboard-density] #panel-overview .stat :is(.stat-chart-legend, .over
color: var(--muted);
font: 400 10px var(--mono);
}
/* Live values stay on one line so refreshes do not change card heights. */
.mini-live-chart-head> :last-child,
.network-interface-row-head .network-interface-head-actions {
flex: 0 0 auto;
white-space: nowrap;
}
.mini-live-chart-head>span:first-child {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
+1 -3
View File
@@ -1,6 +1,4 @@
/* Shared service editors: Vhosts, backends, Web Server, Streams, routing and ACL pickers.
* Order is defined in manifest.json; responsive rules are centralized in 70-responsive.css.
*/
/* Shared service editors: Vhosts, backends, Web Server, Streams, routing and ACL pickers */
.host-chip-editor.field-invalid,
.listener-choices.field-invalid {
Loaded 100 of 283 files, more files were not shown because too many files have changed in this diff. Show more