This commit is contained in:
gru committed 2026-10-03 00:27:09 +01:00
commit 5e23e9af02
240 files changed
+99957

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
.git
.github
.upgrade-backup
target
node_modules
data
*.zip
*.sqlite3
*.sqlite3-*
*.pem
*.key
*.p12
*.pfx
.env
+8
View File
@@ -0,0 +1,8 @@
# Optional. Copy to .env. dev.sh sources this as a shell file; use trusted content only.
RUST_PROXY_DATA_DIR=./data
RUST_PROXY_PROXY_LISTEN=127.0.0.1:8080
RUST_PROXY_ADMIN_LISTEN=127.0.0.1:9090
RUST_PROXY_DEMO_LISTEN=127.0.0.1:9000
# 1 starts a separate local demo backend and seeds a NEW database only.
RUST_PROXY_DEMO=1
RUST_LOG=rust_proxy=info,warn
+11
View File
@@ -0,0 +1,11 @@
/target/
/data/
/.env
*.sqlite*
*.db*
*.log
__pycache__/
*.zip
problems.txt
todo/
scripts/routeros-oci-build/.env
+78
View File
@@ -0,0 +1,78 @@
# RustProxy implementation rules
Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes.
The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for
compatibility. Current source version: 0.24.5.
## Delivery
Maintain English documentation and Markdown files. Keep translations in discovered langs/*.json packs, with en_US as fallback. Never hardcode a two-language switch. Preserve the existing logo.
CHANGELOG.md uses version headings with no release dates. Patch increments may be
bundled; substantial optional integrations raise the minor version. 0.7.0 adds managed renewal, server deployment helpers, Basic Auth and visual locations. The reported compression threshold type mismatch is removed without narrowing. Cloudflare Tunnel and Docker discovery are deferred.
Deliver a complete archive with dev.sh, tests and an updated verification report.
Do not include databases, credentials, real certificates/keys, target directories,
node_modules or fabricated Cargo.lock content. Preserve an actually resolved
lockfile when a compiler-enabled environment can produce it.
## Engineering
Keep proxy traffic independent from control-plane database queries and providers.
Use async I/O, streaming bodies, bounded DNS work, bounded queues and explicit
cancellation. Hold request and connection permits through full body/tunnel lifetimes.
Run ACLs before opening OR reusing an upstream connection. Resolve only configured
backend origins; validate every DNS answer and use the validated addresses directly.
Backend TLS certificate verification defaults OFF when `tls_verify` is omitted for vhost
backends. Administrators may explicitly enable chain/identity verification per HTTPS backend.
This compatibility default is limited to application backends: never widen it to Tinyauth or
other verifier clients. Disabling verification must not weaken backend address/DNS/loop/control-
plane protections.
Preserve the explicit auth module path and upgrade regression coverage. Replace
managed source directories instead of leaving obsolete modules during upgrades.
Keep administrator intent, provider observations and live connection state separate.
Hot changes must publish validated, immutable snapshots after a successful revisioned
SQLite write. Invalid updates leave the old snapshot active.
## Verification
Run `./dev.sh check` and `./dev.sh test` when Rust is available. Run the SQL and upgrade
scripts even when it is not. UI tests use a simulated API; the Python protocol-peer
self-test is not a test of RustProxy. Record these distinctions accurately.
Do not mark a stage accepted or production-ready on the basis of static inspection.
Review real protocol behavior, restored data and resource bounds before expanding
scope. Do not expand provider work ahead of the core real-runtime acceptance gate. Keep that gate explicitly open until Rust runs.
## Vhost invariants
Use the accepted socket's actual local address and TLS state for frontend selection;
never infer a private binding or sticky identity from forwarding headers. Vhosts receive
frontend sockets only through `frontend.listener_ids` referencing `listeners.endpoints`.
Keep hostname ownership unambiguous. Socket/ALPN changes require restart, with actual
and desired plans visible.
Keep health observations separate from desired configuration. Preserve observations
only for compatible members; re-enabled/changed origins require new health checks.
Keep backend leases through request/response/tunnel completion. Do not replay failed
writes to another backend. Cookies are routing affinity, never authentication. Health
probes must use the same origin/DNS policy, bounded concurrency and cancellation.
Retain HTTP/1.1 in TLS HTTP/2 negotiation; do not implement fallback by replaying traffic.
## Vhost configuration invariants
The pre-production route/inline-listener compatibility importer has been removed.
Persist only the current vhost document and named listener references; old documents
must fail validation instead of being silently converted. Keep path-pinned origins
distinct from balanced pools. Do not silently widen listener bindings.
ACME secrets belong in private credentials files, not SQLite, stdout, argv or API.
HTTP-01 only serves bounded tokens for domains in the active job. No CA call on boot.
The exporter checks actual peer CIDRs and is disabled by default; secret configuration
stores a variable name, never a token. Keep metric labels bounded by configuration.
## Administration access regression gate (0.8.1)
No new public admin routes without updating the exact method/path allowlist and
access tests. Operational assets and the OpenAPI schema are private. Keep the
public login document/bundle separate from the dashboard. Documentation uses
the existing cookie session/CSRF, not a second auth mechanism. Preserve the pinned
Swagger build configuration and keep external validators/CDNs disabled.
+795
View File
@@ -0,0 +1,795 @@
# Changelog
## 0.24.5
- Normalize actually empty, length-unknown HTTP/3 request bodies before HTTP/1.1 upstream forwarding so Hyper emits a zero-length request instead of chunked framing for methods such as DELETE.
- Keep non-empty HTTP/3 bodies streaming by replaying the inspected first frame, leave declared `Content-Length` requests untouched, and do not alter HTTP/2 upstream framing.
- Add focused regression coverage for empty/non-empty H3 DELETE and generic empty H3 methods forwarded to HTTP/1.1.
- Keep the source package consistent with its existing release contracts by dropping the stale unreferenced `src/system_stats/bsd.rs` file and updating the offline container check to the already-shipped Debian 13/Trixie base images.
- Harden cache RAM admission without changing the HOT -> COLD storage model: HOT now has a 256 MiB allocation-aware default budget, fills reserve memory before buffer growth, and a small bounded allocator headroom replaces max-object-sized overshoot.
- Add a separate 64 MiB transient RAM budget plus a 32 MiB byte ceiling for the COLD writer queue; demoted bodies stay charged until persistence drops the `ColdDraft`, while persisted `ColdEntry` values retain only disk paths/metadata.
- Preserve body backing-allocation accounting across fill, revalidation and COLD promotion, expose HOT logical vs allocated bytes and the HOT/transient/COLD-queue limits, and add memory-pressure/LRU-admission counters.
- Make global cache disable authoritative over per-vhost enablement, remove the artificial 1 GiB HOT/object configuration ceiling, and hot-apply reduced HOT capacity with bounded LRU demotion/eviction.
- Add Settings memory sizing based on effective host/cgroup RAM with conservative, recommended and performance presets, a live recommended profile, host/cgroup headroom warnings, and controls for transient RAM, COLD queue RAM and bounded LRU admission. Automatic presets are disabled below 256 MiB effective RAM.
- Update release identity to 0.24.5.
## 0.24.4
- Disable Linux Transparent Huge Pages for the RustProxy process with `prctl(PR_SET_THP_DISABLE)` before the Tokio runtime is created, avoiding a host-wide THP policy change while preventing 2 MiB anonymous huge pages from amplifying allocator RSS retention.
- Treat the process-local THP operation as best-effort: unsupported/rejected kernels emit a startup warning and RustProxy continues to run.
- Update release identity to 0.24.4.
## 0.24.3
- Add a per-certificate reload operation for manual/file-backed certificate IDs and authorize it through the scoped Certificate API as well as the normal administrator session.
- Allow multiple Certificate API tokens (bounded to 32), operator comments, individual revocation and revoke-all while continuing to persist token hashes only.
- Replace the overview Core State block with a live connection/activity chart, move uptime into the Process block below process start, add RX/TX-style upload/download detail under Payload transferred, and persist per-browser hidden network-interface charts.
- Add `MALLOC_ARENA_MAX=2` to managed Linux/systemd service units to reduce glibc arena growth and long-running RSS retention while retaining the environment-file override.
- Remove an identity-body copy from the transformation pipeline, remove `Vec<char>` plus final output-copy allocations from the JavaScript/CSS minifiers, release successful-transform temporary buffers earlier, and use a throttled `malloc_trim(0)` after large Linux/glibc transforms.
- Make HTTP/2 lifecycle tuning listener-scoped like HTTP/3: keep H2/H2C and H3 enablement in **Settings -> Listeners**, expose per-listener H2 GOAWAY/lifetime limits under **HTTP connections -> HTTP/2**, retain per-listener QUIC limits under **HTTP/3**, and link listener rows to the matching tuning page. Existing top-level `http2` values remain a compatibility fallback for listeners without explicit H2 settings.
- Rename **Proxy process** to **Process** and dynamically hide unsupported process diagnostics and history charts instead of showing unknown values.
- Remove the stale, unreferenced legacy `src/system_stats/bsd.rs` parser file from the source package; shared library collectors remain the only BSD statistics implementation.
- Update release identity to 0.24.3.
## 0.24.2
- Version-only release bump from 0.24.1; no functional, configuration or protocol changes.
## 0.24.1
- Restore system network interface discovery and RX/TX counters after the shared-statistics refactor: use the cross-platform `sysinfo::Networks` API as the primary network inventory/counter source, merge `systemstat` as a library-level supplement/fallback for missing or partial inventory, preserve per-interface rate calculation, and stop silently truncating the interface list.
- Fix the compiler error in portable uptime collection by using the `systemstat` duration API correctly, and remove the now-unused `Future` import in the H2 server module.
- Remove RustProxy's artificial hostname-count ceilings from certificate SNI bindings, local certificate generation/import validation, ACME domain validation, vhost/redirect/404 hostnames, administration allowed hosts, host ACL policies and Docker aliases. Input validity and bounded control-plane/resource limits remain enforced.
- Replace per-BSD command/parsing statistics code with shared library collectors: `systemstat` for host metrics, `sysinfo::Networks` for network inventory/counters, and `metrics-process` for process metrics, using the documented dummy process fallback on targets without a native collector.
- Preserve Linux `/proc` and cgroup-specific details as additive compatibility metrics while using the common library API for portable host/process values.
- Add portable swap, system uptime, process virtual memory/limit, descriptor limit and process start-time diagnostics where the libraries support them.
- Add authenticated runtime metadata for platform, statistics source, resolved data directory and exact SQLite database path.
- Make local CLI data-directory discovery understand both `/var/lib/rustproxy` and the BSD/OPNsense `/var/db/rustproxy` convention.
- Move Logging into **Settings -> Logging** while retaining legacy `/logging` navigation compatibility.
- Update release identity to 0.24.1.
## 0.24.0
- Replace abrupt downstream H2 idle termination with a GOAWAY-based graceful shutdown that rechecks current activity and waits for accepted streams.
- Add independent H2 idle timeout, maximum connection lifetime and drain timeout settings; lifetime may be disabled with `0`.
- Bound H2 draining so stalled streams cannot keep a retiring connection open indefinitely.
- Keep H2 Extended CONNECT/WebSocket streams under the same active-stream accounting and graceful drain path.
- Extract system statistics into platform backends while preserving Linux metric names, units and behavior; add native FreeBSD/OpenBSD/NetBSD sampling where available and explicit nulls otherwise.
- Add runtime platform to Proxy Process and Prometheus platform info.
- Add Settings UI/API/OpenAPI/language-pack support plus H2 lifecycle/configuration regressions.
- Reorganize **Settings -> HTTP connections** into HTTP/2 and HTTP/3 sub-tabs; move listener-scoped H3/QUIC enablement and limits out of the Listeners editor while preserving restart semantics and the existing listener configuration model.
- Update release identity to 0.24.0.
## 0.23.3
- Make external certificate deployment convergent: create missing manual certificate ids, rotate existing manual ids, no-op identical RustProxy-owned generations, and keep managed ACME ids protected from external ownership.
- Make the OPNsense os-acme-client hook reconcile on every run so RustProxy remains the source of truth and deleted/drifted external certificate entries self-heal.
- Add per-vhost **Enable WebSocket legacy support** for older appliance/embedded WebSocket implementations.
- Force legacy-mode WebSocket upstream handshakes to HTTP/1.1 and force the WebSocket `Host` to the configured backend authority and align `Origin` to the matching backend scheme + authority.
- Tolerate legacy `101 Switching Protocols` response framing while keeping `Sec-WebSocket-Accept` and subprotocol validation strict.
- Add UI/OpenAPI/translations, documentation and regression contracts for the compatibility mode.
- Update package/source/UI identity and release verification contracts to 0.23.3.
## 0.23.2
- Add selectable 60/120/300/600-sample history windows to the Cache and Transformation live charts, with bounded per-workspace browser persistence.
- Fix `rust-proxy --livelogs` on HTTPS administration listeners by detecting the HTTP 308 redirect and reconnecting over TLS instead of looping.
- Pin local CLI HTTPS to leaf certificates present in the private `data-dir/certs` directory and reuse the HTTP/HTTPS-aware client for config and certificate CLI API calls.
- Update package/source/UI identity and release verification contracts to 0.23.2.
## 0.23.1
- Add default-collapsed, full-width live activity charts to the Cache and Transform workspaces.
- Plot Cache HIT/MISS/BYPASS rates for the selected scope using deltas from the existing cache runtime counters.
- Plot Transform applied/cache-HIT/skipped/error rates for the selected scope using the existing transform metrics endpoint.
- Reuse the administration dashboard SVG/grid/legend visual language, keep the existing 3-second live refresh, and add English/Polish language-pack labels.
- Update package/source/UI identity and release verification contracts to 0.23.1.
## 0.23.0
- Bound inactive inbound HTTP/2 connections to five minutes so idle TLS/H2 sessions cannot accumulate indefinitely toward the process connection limit; export `rust_proxy_http2_active_connections` for direct diagnosis.
- Account hot-cache response-body backing allocation capacity rather than only visible body length, preventing `BytesMut` growth slack from bypassing the configured RAM budget and under-reporting resident cache memory.
- Fix the `--livelogs` stdin reader to send owned `String` values from the match body rather than a pattern guard, resolving Rust `E0507` during binary compilation.
- Add a scoped external Certificate API credential managed from a new **Settings → Certificate API** tab. RustProxy stores only the token hash plus a non-secret Key ID; the cleartext token is returned only on rotation.
- Allow the scoped Key ID + Bearer Token only on `POST /api/certificates/deploy` and `POST /api/certificates/reload`, require HTTPS for non-loopback token use, track last use, support immediate revoke, and retain normal administrator-session/CSRF access as an alternative.
- Keep production certificate operations available directly from the installed binary (`rust-proxy tls deploy` / `rust-proxy tls reload`) without requiring `dev.sh`.
- Add `rust-proxy --livelogs`: an interactive varnishlog-style all-vhost live access stream with structured filters (`host`, `vhost`, `backend`, `route`, `client`, `method`, `transport`, `cache`, status classes, path prefix, errors and slow requests), pause/resume, per-request JSON inspection and detailed error/WebSocket/H3/cache fields.
- Back live logs with a bounded non-blocking broadcast stream and authenticated SSE endpoint; slow consumers receive an explicit `gap` event with the dropped-entry count instead of silent loss.
- Split the large **Settings → Cache** form into **General**, **Resilience**, **Cold cache**, and **Static & diagnostics** sub-tabs while retaining one atomic cache-policy save.
- Add SQLite schema v5 for the scoped certificate credential, OpenAPI/security schemes, UI translations, audit events and 0.23.0 regression/release documentation.
## 0.22.3
- Add downstream HTTP/1.1 `Upgrade: h2c` on listeners that explicitly enable cleartext HTTP/2, preserving the existing prior-knowledge path and rejecting malformed/body-bearing upgrade attempts before protocol switch.
- Reconstruct the accepted H1 upgrade request as HTTP/2 stream 1 and hand subsequent framing/streams to the existing Hyper H2 server path; include raw-TCP interoperability regression source.
- Add authenticated `POST /api/certificates/reload` and `tls reload` to re-read configured public certificate/key files and hot-publish a fresh TLS/H2/H3 snapshot without changing the desired configuration revision.
- Add `tls deploy --id ... --cert ... --key ...` and authenticated `POST /api/certificates/deploy` for externally issued local certificates such as Certbot. The CLI performs bounded local reads while the running service owns pair/SAN validation, immutable generation staging, revision-CAS publication and rollback on failure, avoiding root-owned staged PEMs when Certbot hooks run as root.
- Keep all vhosts assigned to one manual certificate id synchronized during external rotation; reject RustProxy-managed ACME ids to avoid dual renewal ownership and report the administration-TLS restart caveat explicitly.
- Add Certificates-panel reload UX, OpenAPI contracts, audit events, translations and 0.22.3 audit/test documentation.
## 0.22.2
- Fix the 0.22.2 backend diagnostics build failure by replacing the oversized recursive `serde_json::json!` member payload with explicit `serde_json::Map` construction; the administration JSON schema is unchanged.
- Add bounded replay-safe backend failover retry for body-free GET/HEAD/OPTIONS requests, including retry policy controls, passive-health integration, logs and per-backend retry counters.
- Correct ordinary explicit-HTTP/2 upstream requests and add a real Hyper h2c prior-knowledge regression path.
- Add per-backend request-concurrency limits with bounded capacity waiting, `503` rejection and capacity/queue-timeout metrics. This limits full proxy exchanges, not TCP socket count.
- Add detailed upstream failure classes, last failure detail/timestamp, and backend health ejection/recovery timestamps to runtime diagnostics and Prometheus.
- Expand the backend administration table with concurrency capacity, last failure and last recovery columns.
- Parse public X.509 leaf metadata for issuer, SANs, validity, remaining days and expiry warning state, and surface ACME renewal status/last attempt/error in certificate inventory.
- Extend OpenAPI, language packs, source regressions, audit/TODO and release documentation for the expanded 0.22.2 scope.
## 0.22.1
- Vhost UX: replaced separate WebSocket/H2 RFC 8441/H3 RFC 9220 checkboxes with one transport selector; H2/H3 modes keep HTTP/1.1 Upgrade as an explicit fallback and unavailable transports are disabled from listener capabilities.
- UI polish: replaced the overview TLS listener tile with compact HTTP protocol and WebSocket protocol mini charts; Target and Cache key/tags cells remain copyable with full values.
- Keep the overview metrics in one shared stats grid so the WebSocket/H2/H3/TLS row uses exactly the same card geometry as the primary metrics, without a transport-specific dashboard layout. Make Cache **Target** and **Cache key / tags** cells copy their full untruncated value on click/keyboard activation.
- Polish the 0.22.1 administration UI: move **Active tunnels** into the WebSocket dashboard card, flatten the H1/H2/H3 tunnel breakdown to match the standard transport-card styling, and align Cache modal close actions with the vhost modal header pattern.
- Fix Rust ownership in tunnel registration by releasing the client-IP mutex guard before moving the shared exchange into the active tunnel, and remove an unnecessary mutable H2 upstream reader binding.
- Move selective cache PURGE/BAN and the active BAN list into dedicated Cache modals, keep their launch actions beside **Purge selected scope**, and use null-safe inline operation feedback consistent with login errors.
- Add opt-in RFC 8441 WebSocket Extended CONNECT for HTTP/2, with per-vhost controls beside RFC 9220, listener-scoped SETTINGS advertisement, H2-to-H1 bridging, explicit H2 upstream forwarding, cancellation/idle timeout and transport-specific metrics.
- Add opt-in cleartext HTTP/2 prior-knowledge (`h2c`) listeners. Legacy HTTP/1.1 `Upgrade: h2c` remains deliberately unsupported because the current Hyper integration cannot safely reconstruct the required HTTP/2 stream 1 bootstrap.
- Add stable active-tunnel IDs, authenticated tunnel inventory/kill API, operator-supplied close reasons, audit records, RX/TX/duration metadata and an administration modal.
- Consolidate active WebSocket/tunnel visibility on the main dashboard as total plus H1 / H2 RFC 8441 / H3 RFC 9220 counters while retaining ordinary H2/H3 request statistics separately.
- Preserve `Accept-Ranges` from origins and synthesize it only for RustProxy-generated cached range responses; transformed/compressed representations continue to remove incompatible range advertising.
- Update source/package/UI/OpenAPI/Debian/Grafana/tooling identity to 0.22.1.
## 0.22.0
- Fix a Rust ownership regression in the upstream request path: retain a cloned origin URI for proactive refresh after the request URI is assigned.
- Add per-cache-key request collapsing for foreground MISS/fill traffic with bounded wait, configurable timeout fallback, cancellation-safe leader release and collapse metrics.
- Add single `Range: bytes=...` handling from complete cached objects, including `206`, `416`, `Content-Range`, `Accept-Ranges` and `If-Range`; multipart/unsupported ranges remain origin pass-through and are not cached as separate objects.
- Extend stale-if-error with configurable transport failure classes and an explicit 5xx status allowlist while preserving the previous default behavior.
- Add bounded proactive cache refresh with pre-expiry scheduling, deterministic sampling, concurrency limits and reuse of the existing validator revalidation path.
- Add selective cache PURGE by exact URL, prefix, tag, regex or cache key, including query/Vary handling, RAM + Cold Cache invalidation and a publication barrier preventing pre-PURGE fills from republishing stale data.
- Add lazy BAN rules with lookup-time enforcement, bounded rule count/TTL, incremental garbage collection, rule deletion and metrics.
- Add authenticated administration API and Cache panel controls for PURGE/BAN, broad-operation confirmation, per-object Purge URL actions, BAN listing/removal and audit entries.
- Extend cache diagnostics, OpenAPI, translations and regressions for the 0.22 cache behavior.
- Update package/source/UI/OpenAPI/Debian/Grafana/tooling identity to 0.22.0.
- Preserve the resolved dependency graph; compiler/live acceptance remains an explicit release-host gate when Cargo/Rust are unavailable in the packaging workspace.
## 0.21.0
- Fix browser refresh on the dedicated **Transform** route by serving the dashboard document at `/transform` and covering it in the panel-route regression.
- Align the Cache **Transformed** marker with the dashboard geometry by replacing the capsule outline with the standard badge border and a 3px corner radius.
- Give the Live traffic **Transformed** series a dedicated violet chart token in dark/light themes so it remains distinct from HTTP/2 green.
- Add a dedicated Transform administration workspace with global/per-vhost runtime counters for applied/skipped/error transformations, transformed cache HITs, byte savings, reduction, transform time, kind breakdown and bounded skip reasons.
- Add a transformed-response series to **Live traffic** (fresh transformations plus transformed cache HITs) and render **Core state** uptime in compact human-readable units.
- Rename the proxy-owned backend timing diagnostic to `X-RustProxy-Backend-Time`; the legacy lookalike `X-Rust-Proxy-Backend-Time` is stripped but is no longer emitted.
- Fix Docker-vhost Passive Backend Health checkbox sizing so enabling the expanded editor cannot make fields overlap.
- Remove the unused mutable `origin` binding from the background revalidation path.
- Replace the monolithic `src/cache.rs` with the responsibility-based `src/cache/` module (`model`, lifecycle, cold storage, dashboard, tickets, lookup, policy, fill and tests) and update source/package gates for the new layout.
- Fix the cache split so method groups are emitted from module-scope `include!` files wrapped in their own `impl Cache` blocks; this avoids the Rust compiler error caused by `include!` directly in impl-item position.
- Persist transformed-representation metadata through hot/Cold Cache, mark transformed objects in the Cache dashboard, and append `transformed` to standardized RFC 9211 `Cache-Status` diagnostics; origin `X-Cache-Status` lookalikes are stripped rather than emitted.
- Shard Cold Cache storage and add bounded asynchronous garbage collection plus selective manual cleanup.
- Add RFC 5861 stale windows, request `max-stale`, age-aware request `max-age=N`, and correct `must-revalidate` / `proxy-revalidate` behavior while preserving `s-maxage` priority.
- Harden transformed cache fills so transformed Content-Length is authoritative, incomplete streams cannot publish partial objects, and canonical transformed identity representations do not fragment on origin `Vary: Accept-Encoding`.
- Add the static-response transformation pipeline before cache/client compression, with strict `no-transform`, separate origin/representation validators and JS/CSS plus optional conservative HTML/SVG and parsed JSON transforms.
- Keep route cache overrides tri-state and add effective route/cache/transform diagnostics.
- Make Passive Backend Health explicit opt-in (default OFF) for ordinary and Docker vhosts, independent from active TCP/HTTP health, with per-error-class policy, separate counters and persisted Docker assignment settings.
- Extend administration UI/OpenAPI and regression coverage for Cold Cache cleanup, transform settings, passive health and cache semantics.
- Keep release/package/UI/OpenAPI/Debian/Grafana/tooling identity at 0.21.0.
- Close the 0.21.0 feature scope for release packaging. Parser-backed JS/CSS minifiers are deferred; compiler/live acceptance still has to be executed on a Rust-enabled release host.
## 0.20.0
- Rebuild **Certificates** into Installed / Renewal / Generate tabs using the same accessible keyboard tab pattern as Settings.
- Keep certificate issuance, PEM import and existing-file registration inline in the Generate tab; successful imports and edits return to Installed.
- Add explicit ACME operation state cards and include the exact private Certbot log directory in failed issuance/renewal messages.
- Expose per-managed-certificate Certbot log directories from the authenticated ACME status endpoint so persisted renewal failures remain actionable after refresh/restart.
- Rework renewal entries into readable cards with provider/environment, last check, next check, expiry and grouped actions.
- Update release/package/UI/OpenAPI/Debian/Grafana/tooling identity to 0.20.0.
## 0.19.4
- Align the Docker-vhost Aliases editor with the standard 38px Docker form controls and remove the inherited `host-chip-field` label margin that created an extra visual line below `Aliases`.
- Preserve backend traffic display semantics: idle renders as `0.0 KiB/s`, and positive rates below 0.1 KiB/s render as `<0.1 KiB/s` in both backend rows and aggregate vhost traffic.
- Add a presentation filter to **Cache -> Domains and effectiveness** with All / Cache enabled / Cache disabled choices, using each vhost's resolved cache settings while leaving aggregate statistics unchanged.
- Update release/package/UI/OpenAPI/Debian/Grafana/tooling identity to 0.19.4.
## 0.19.3
- Fix the compiler error in the release checker body collector by converting Hyper/http-body-util boxed collection errors explicitly into `anyhow::Error` instead of calling `anyhow::Context` on an unsupported boxed error type.
- Simplify the dashboard footer to API latency and dropped-log count, and add a fail-soft built-in release checker using embedded `UPDATE_CHECK.toml` defaults. The checker reads the official Cargo manifest immediately and then hourly, exposes a newer-version link in the footer, and never performs automatic upgrades.
- Allow runtime release-check overrides through `--update-repository-url`, `--update-version-url` and `--update-check-interval-secs` (plus matching `RUST_PROXY_*` environment variables); interval `0` disables checks.
- Fix the remaining oversized Live Logs column checkboxes by excluding checkbox/radio inputs from both broad `.log-tools input` text-field sizing blocks that were overriding the shared 15px checkbox rules.
- Add an account-scoped light/dark administration theme stored in the existing SQLite `admin_preferences` document; dark remains the default and the last authenticated choice is cached locally only to avoid login/dashboard first-paint theme flicker.
- Refactor theme-sensitive UI colors into shared CSS variables and keep checkbox/radio controls out of generic text-input sizing/density rules.
- Rework the Live Logs column picker into a compact scrollbar-free list with standard-size checkboxes; short viewports use two columns instead of an internal scroll area.
- Keep expired-session re-authentication on the exact panel and Settings sub-tab, use the minimal public login document without flashing the authenticated shell, and show embedded login errors inline instead of as global toasts.
- Canonicalize the legacy `/configuration` shortcut to `/settings?tab=manual-config` and keep later Settings tab switches in the URL so F5 restores the exact page.
- Add one shared saving-state helper for settings/security/certificate forms: disable the active submit button, show `Saving…`, restore its translated label, and prevent duplicate submissions while a save is in flight.
- Highlight and focus the actual invalid control for local validation failures in Logging JSON, named listeners, Docker-agent listener selection, TLS policy, ACL profiles, manual JSON and certificate forms.
- Preserve selected certificate/key files after a failed certificate import/save so the administrator can correct the problem without reselecting both files.
- Mark Settings tabs with a subtle unsaved-change dot, including Manual config, and keep dirty values from being overwritten by live config refreshes.
- Update release/package/UI/OpenAPI/Debian/Grafana/tooling identity to 0.19.3.
## 0.19.2
- Restore the vhost local-listener risk control for administration backends when the administration socket is startup-locked or otherwise reported only by live status; treat `localhost`, `127.0.0.0/8` and `::1` as equivalent loopback forms for the same administration port.
- Make **Discard changes** reload the persisted vhost being edited instead of replacing the editor with a new blank/default vhost.
- Add descriptive HTTP reason phrases to the default-response status selectors and headings.
- Add a temporary, continuously updated 4096-entry Live-log buffer that resets when the log window closes.
- Align inline checkbox labels and compact Docker-vhost alias controls.
- Stop backend traffic EWMA tails on an idle sampling window, show idle consistently as `0.0 KiB/s`, and render positive sub-0.1 KiB/s traffic as `<0.1 KiB/s` instead of a rounded zero.
- Make the Live Logs column picker substantially smaller with subtle 11px checkboxes; let the menu escape the log-dialog clipping boundary and open upward on short viewports.
- Default newly created vhosts to the standard HTTP `:80` listener when available; HTTPS listeners are selected explicitly.
- Left-align the Settings -> Docker agents save action to match the other settings forms.
- Suppress dashboard/login first-paint flicker and preserve the active Settings sub-tab across F5, including a re-login redirect back to `/settings?tab=...` and `/docker-vhosts`.
- Replace native browser confirmations with application-styled confirmation dialogs and add panel-area context to API error messages.
- Present login failures as an inline styled error below the form, including invalid-credential responses.
- Install a login-shell PATH profile for the managed `current` release directory on first install; updates check and warn about the profile without changing it.
- Enforce `no-store` and suppress cache diagnostics/validators for local placeholder/static/default pages.
- Allow vhost-ID renames with duplicate protection, confirmation and an explicit runtime-identity warning.
- Update source, UI, OpenAPI, regressions and package identity to 0.19.2.
## 0.19.1
- Fix Default responses section saves by passing the normal `{expected_revision, value}` payload to `/api/config/pages` and add server-side `pages` section handling/regression coverage.
- Add frontend scheme to the primary cache hash so otherwise identical HTTP and HTTPS requests cannot reuse one object.
- Retain 4096 completed access-log entries while keeping the 256-entry live window; add one-shot on-demand history loading and document the API limit.
- Render backend traffic with KiB/s as the minimum unit, simplify the Live Logs column picker, align generic checkboxes and move Docker-vhost Advanced switches beside Backend protocol on desktop; remove the redundant alias help line.
- Remove stale simulated dashboard screenshots, retain only two explicitly labelled legacy references and add a documentation index for historical/version-labelled material.
- Restore the integration-test sources and independent Argon2id fixture required by release/source checks so compiler-enabled test builds no longer reference files missing from the supplied source archive.
- Update source, UI, OpenAPI, regressions and package identity to 0.19.1.
## 0.19.0
- Persist per-administrator Live Logs column visibility in SQLite schema v4 without changing the revisioned proxy configuration; JSON export remains complete regardless of visible columns.
- Keep the Live Logs column selector visually consistent with the existing compact `check vh-check` controls instead of rendering oversized standalone checkbox widgets.
- Add authenticated `GET/PUT /api/preferences` endpoints for dashboard-only account preferences, protected by the existing session/CSRF control-plane guards.
- Remove the duplicate version badge from the top-right dashboard toolbar while keeping the footer release identity.
- Retain the full disabled-verification legacy X.509 fix and expanded upstream connector diagnostics introduced during the 0.18.8 follow-up.
- Update release/package identity to 0.19.0.
## 0.18.8
- Make backend `tls_verify=false` a complete certificate-authentication bypass instead of delegating TLS CertificateVerify to webpki. This restores compatibility with legacy appliance certificates such as X.509 v1 while keeping TLS encryption and all backend address/DNS/loop protections in force; `tls_verify=true` is unchanged.
- Expand upstream connection diagnostics with the full nested Hyper/rustls/io error chain, backend/TLS context, 2 KiB UTF-8-safe detail storage and warning-level journal entries for connect failures.
- Replace Docker-vhost alias comma input with hostname-style chips for add/remove/paste workflows.
- Add per-Docker-vhost HTTP-to-HTTPS redirect, WebSocket and cache controls; new compatible assignments default HTTP-to-HTTPS redirect on while legacy assignments retain their prior redirect-off behavior until explicitly saved.
- Align Docker-vhost boolean controls with the application's standard checkbox classes and keep checkbox inputs from inheriting full-width text-input sizing.
- Add a Docker-agent default ACL-profile selector. Generated Docker vhosts inherit it unless the application selects its own ACL profile; existing per-app inline ACL configuration is unchanged.
- Remove the compression-codec negotiation explainer from the Overview dashboard.
- Update release/package identity to 0.18.8.
## 0.18.7
- Collapse **Response time history** by default on the Overview dashboard while keeping live sampling active.
- Restyle per-interface RX/TX histories to use the same chart geometry and visual treatment as the aggregate RX/TX history, without nested panel backgrounds.
- Align expanded Docker-vhost routing and ACL controls with uniform field blocks; Preserve Host and per-app ACL switches now occupy the same label/control rhythm as neighboring inputs, and ACL selects no longer stretch to textarea height.
- Update release/package identity to 0.18.7.
## 0.18.6
- Add bounded per-interface RX/TX diagnostics from `/proc/net/dev` to the administration status payload and render separate live interface histories without adding per-interface Prometheus labels.
- Strengthen compression negotiation regressions with explicit `q=` preference cases for gzip, Brotli, Zstd and deflate; clarify that dashboard counters track the codec actually selected per response.
- Align Docker-agent settings inputs/actions, keep Enabled/status/remove controls on one stable row, and add spacing between listener selection and the Add Docker agent action.
- Update release/package identity to 0.18.6.
## 0.18.5
- Make compression accounting follow an explicit RustProxy-applied codec marker instead of re-reading `Content-Encoding`; add independent gzip/br/zstd/deflate counter regression coverage.
- Add an explicit HTTP/1.1 request counter and render total + HTTP/1.1 + HTTP/2 + HTTP/3 request-rate series on the Overview chart.
- Add an Open app action to published Docker vhosts, using the assigned frontend listener and expanded proxied hostname.
- Label Docker-generated pools in the Backends view with their primary proxied domain.
- Remove the global Open proxy shortcut from the page header.
- Make Compact density materially tighter than Dense across cards, controls, tables, Docker rows and charts.
- Default vhost/Docker proxy timeout controls to explicit 10/120/60/60-second connect/header/read/send limits instead of blank inherited values.
- Replace free-form Docker-agent listener-ID entry with the configured named-listener selector; move Enabled left of the centered connection-status badge.
- Update release/package identity to 0.18.5.
## 0.18.4
- Default backend health checks to TCP for newly created or otherwise defaulted ordinary and Docker vhosts; explicit existing HTTP/off health modes are preserved.
- Replace the dense Docker-vhost service editor with summary-first rows. Publish state, service identity, backend runtime health and Hide/Show stay visible; routing, ACL, timeouts and health tuning are expanded only for the selected service.
- Collapse secondary Docker-agent inventory metadata behind Agent details while keeping host, service count, last observation and base domain immediately visible.
- Rework Settings -> Security into focused account/session and host-policy cards with on-demand password and policy editors.
- Move Administration audit into a dedicated modal and stop fetching audit entries merely by opening the Security settings tab.
- Update release/package identity and EN/PL UI copy to 0.18.4.
## 0.18.3
- Add Docker-vhost search plus persistent per-browser Hide/Show Hidden view controls, and allow each Docker agent to select a configured frontend certificate inherited by its generated vhosts.
- Keep Docker inventory polling driven by each agent's configured 5..3600 second interval; the 5-second dashboard refresh reads cached observations only.
- Move redirect HTTP/2 and HTTP/3 toggles below listener selection and keep Cache `Updated` + `Live` together at the right side of the toolbar.
- Remove the unused `config::yes` helper warning.
- Add an Open-in-browser action beside each vhost Enabled state; wildcard-only hosts open the wildcard base domain, while mixed host lists prefer the first non-wildcard name.
- Restore the documented HTTP/1.1 backend default for newly created ordinary and Docker vhosts: preserve the public `Host`; existing explicit backend settings are not rewritten.
- Write HTTP/1 upstream header names in conventional title case for compatibility with legacy/custom origins that incorrectly treat header names as case-sensitive; HTTP/2 is unaffected.
- Add bounded `upstream_error` diagnostics to access logs/live logs so TCP/TLS/HTTP-client failures that previously appeared only as 503 with no upstream status can be distinguished.
- Restrict local/error response selectors to supported error statuses; application-style 404 hosts offer only bundled 401/403/404/429/503/504 templates, including a new dedicated 404 template.
- Make Docker Publish/Unpublish an immediate per-service configuration action, add per-service Apply, and replace unconditional UI rerendering with an operator-controlled Live view that reads only RustProxy's in-memory state.
- Add per-agent Docker inventory polling intervals (5..3600 seconds), while manual refresh still polls immediately.
- Add reusable ACL-profile plus inline IP/CIDR ACL controls per Docker application, and protect referenced profiles from deletion.
- Add per-vhost proxy connect/response-header/read/send timeout controls for ordinary and Docker-backed vhosts.
- Print the Docker-agent bearer token after installation alongside the certificate fingerprint.
- Improve 404 Hosts response UX with supported error-status selectors, application-style built-in status pages, cache-diagnostic suppression and protocol toggles beside listener selection.
- Add optional per-vhost JSONL access-log files under a configurable directory while retaining the combined-file mode, Logstash and Loki sinks.
- Lock HTTP/HTTPS/admin listener addresses that are supplied explicitly by systemd/CLI/environment so the control plane cannot remove, disable or rewrite process-owned binds; listeners not supplied at startup remain editable.
- Remove the separate remote-admin risk-acknowledgement requirement; plain remote administration now requires only `--allow-remote-admin`, while legacy `--accept-remote-admin-risk` is accepted as a hidden no-op so older systemd units still start.
- Tighten Compact/Dense dashboard density, rename the Shared RAM cache surface to **Cache**, expose redirect targets directly on cards, and keep 404 Hosts aligned with the application error-code set.
- Clarify that TLS Policy is inherited globally and show currently effective cipher suites alongside all available rustls suites.
- Treat ACME-issued certificates as managed/read-only in the custom-file editor, require certificates to be unassigned (including the administration panel/default binding) before deletion, and default new ACME issuance to the production CA environment.
- Make classic HTTP/1.1 Upgrade the default WebSocket path; RFC 9220 Extended CONNECT over HTTP/3 is now an explicit experimental per-vhost opt-in and is advertised only on QUIC listeners with an opted-in vhost.
- Split Docker discovery/vhost observability into dedicated `rust_proxy_docker_vhost_*` and `rust_proxy_docker_backend_*` Prometheus metric families; Docker pools no longer appear in ordinary vhost/backend series.
- Add a separate live **Docker vhosts** Prometheus collector group, enabled by default, with configured agent/assignment/domain gauges plus Docker vhost traffic/status/duration and full backend health/probe/runtime metrics.
- Extend Prometheus/OpenAPI/UI documentation and regression coverage for the Docker metric boundary.
- Extend the bundled Grafana operations dashboard with a dedicated Docker-vhost section and per-agent/service filters using only the separate Docker metric families.
- Fix Docker discovery compilation against the locked HTTP/body stack by mapping limited-body collection errors explicitly and logging `ApiError` via its `Debug` representation.
## 0.18.2
- Secure Docker-agent transport by default with a persistent self-signed ECDSA certificate and SHA-256 certificate pinning in RustProxy; public-CA HTTPS remains available without a pin and plain HTTP is compatibility-only.
- Add certificate lifecycle helpers, hardened systemd state storage, installer fingerprint output and TLS regression tests for the standalone Docker agent.
- Enable per-Docker-vhost HTTP/TCP backend health checks using the same bounded backend-pool health machinery as ordinary vhosts, with live checking/up/down, latency, probe, request, active and error diagnostics in Docker vhosts.
- Rework Docker-agent and Docker-vhost UX around connection security, last observation, detected addresses, route controls and collapsible health-check tuning while retaining per-agent domains, aliases and per-service IP selection.
## 0.18.1
- Extend the Docker agent inventory with all usable host addresses (interfaces, routed address, public lookup and optional advertised address) and preserve every published binding per service.
- Add per-service Docker backend-IP selection, per-agent default backend IP, arbitrary FQDNs and multiple aliases while retaining short-name expansion below each agent's own base domain.
- Support many Docker agents with independent domains such as `docker1.lab.local` and `docker2.lab.local`, plus operator-controlled custom domains.
- Remove all special `X-Rust-Proxy-Revision` response-header behavior; the name is no longer inserted, stripped, protected or accepted as a legacy diagnostic setting.
- Keep cache-Live placement, production password recovery and Settings navigation improvements from 0.18.0.
## 0.18.0
- Add a standalone Go Docker discovery agent with a hardened systemd unit, token authentication, published TCP-port inventory and host/public-IP detection.
- Add per-agent Docker configuration, in-memory observations and dynamic Docker vhosts with operator-controlled service/subdomain assignments.
- Add Docker vhost/admin UI and OpenAPI endpoints for cached observations and explicit refresh.
- Move the cache Live toggle to the action-toolbar edge.
- Remove configurable `X-Rust-Proxy-Revision` response exposure and always strip the header.
- Use the installed `rust-proxy admin reset-password` command in production recovery guidance.
- Update EN/PL strings, documentation, release checks and package identity to 0.18.0.
## 0.17.0
- Add Debian packaging assets, a hardened systemd unit, `rustproxy` service user/state/log directories, three-day maximum-gzip log rotation, local `.deb` build tooling and signed APT-repository generation suitable for `rustproxy.linuxiarz.pl`.
- Expand Prometheus coverage across all current bounded process/OS diagnostics plus GeoIP, certificate renewal, backend health-probe detail, stream, logging and frontend inventory metrics; add explicit token-free scraping for trusted actual-peer CIDRs.
- Add a Grafana operations dashboard under `grafana/`.
- Add dedicated listener/certificate-aware local-response / 404 hosts with built-in placeholder, empty/status-only and custom-HTML modes.
- Keep the default `RustProxy is running` placeholder uncacheable and intentionally free of optional proxy/cache/backend/diagnostic/security/compression headers.
- Reuse vhost-style hostname chips for Redirect source domains.
- Update OpenAPI, EN/PL UI, release checks and package identity to 0.17.0.
## 0.16.1
- Remove internal configuration revision numbers from successful save feedback in dashboard editors/manual configuration while retaining revision-based CAS and stale-write protection internally.
- Update source, UI, OpenAPI, release tooling and packaging identity to 0.16.1.
## 0.16.0
- Decouple Custom locations from the main vhost backend fallback. Unmatched paths now use the main pool even when custom rules exist; untouched legacy synthetic root routes are normalized automatically.
- Fix same-process public administration login by allowing `rustproxy_session` across only a guarded HTTPS self-admin backend that directly targets the configured administration listener and whose public host is explicitly allowed.
- Make RFC 9220 bootstrap wait for the backend HTTP/1.1 upgrade before downstream H3 success, and extend access logging with transport/WebSocket phase/error/reset/upstream status diagnostics.
- Add cache transient-memory gauges and procfs RSS/data/swap breakdowns to the dashboard and Prometheus. Existing allocator trimming/release behavior is unchanged; no new transient-memory throttle is introduced.
- Reorganize the Vhost editor around clearer backend cards, Authentication subcards and compact protocol controls. New vhosts select standard HTTP :80 plus HTTPS :443 together when available; Redirects reuse the same listener selector; disabled vhost cache hides its subordinate controls and new vhost drafts default cache to off.
- Show the local-listener risk acceptance only when the entered backend URL directly targets the configured administration listener. Add protected opt-in `X-RustProxy-Backend` and `X-RustProxy-Backend-Time` response diagnostics for selected member identity and origin response-header latency.
- Bump source, UI, OpenAPI and packaging identity to 0.16.0.
## 0.15.1
- Fix Custom locations EN/PL translation loading by keeping all UI strings inside the language-pack `translations` map and add a regression check that rejects stray top-level translation keys.
- Make visual header rules easier to edit with numbered cards, clear field grouping, empty-state guidance and localized help.
- Add a live per-location behavior preview for matching, action, rewrite, cache, ACL and header rules, and reorganize the inline documentation with common examples.
- Add one live three-series HIT/MISS/BYPASS activity chart to the RAM cache card and CPU/RSS history charts to the Proxy process card.
- Extend offline source/release checks for the new location UX and dashboard charts; package identity is 0.15.1.
## 0.15.0
- Expand vhost Custom locations with Nginx-style exact/prefix/`^~`/regex/regex-insensitive matching, upstream path preserve/strip/replace/regex rewrites and backend-free custom responses.
- Replace the location header JSON-only workflow with a visual request/response header builder, keep Advanced JSON as an escape hatch, add in-panel documentation and reorganize the location editor into compact functional sections.
- Add `nagios/` API checks for administration response time, unavailable/checking backends, backend latency and dropped logging/cold-cache work, with reusable protected session cookies and Nagios performance data.
- Move per-vhost `Server-Timing` and verified Cloudflare client-IP diagnostics into the main frontend/details configuration.
- Add compact dashboard history charts for RAM-cache residency, network RX/TX and mean response latency, plus administration API RTT and low-priority queue/drop information in the footer.
- Add independent cold-cache age policy controls: demote by time since last access, by total hot-tier residency, by LRU pressure and on graceful shutdown.
- Add verified effective client/source IP to live logs and logging field mappings; early responses still record at least the transport peer.
- Remove the runtime legacy renewal-observation JSON importer. Stopped source upgrades remove the obsolete sidecar as the final upgrade step without importing it or retaining a second legacy copy; SQLite remains the only live renewal-observation store.
- Normalize repeated literal path separators for routing/ACL decisions, matching Nginx `merge_slashes` behavior, while preserving the original request target to the backend when no rewrite is configured. URLs such as `/static/js//events.js?v=...` are therefore accepted without weakening encoded-separator/dot-segment protections.
- Bump source, panel, OpenAPI, upgrade tooling and package identity to 0.15.0 and extend offline regression contracts for the new controls.
## 0.14.0
- Fix administration reverse-proxy Host/Origin handling: explicit DNS allowlist entries describe the external authority instead of the private admin socket port; direct IP/localhost access remains bound to the actual admin port.
- Trust forwarded administration host/scheme only from loopback or configured trusted proxy peers, use the external scheme for Origin/CSRF checks, and set/clear Secure session cookies consistently behind HTTPS termination.
- Implement RFC 9220 WebSocket over HTTP/3 Extended CONNECT, including `SETTINGS_ENABLE_CONNECT_PROTOCOL`, `:protocol=websocket` validation, HTTP/1.1 backend Upgrade translation, downstream 200 bootstrap, bidirectional H3 DATA transport and FIN/reset cancellation handling.
- Replace the separate `h3`/`h3-quinn` pair with `hpx-h3 2.5.19` and its integrated Quinn feature to expose the RFC 9220 protocol extension through the HTTP/3 stack.
- Rename dashboard backend, health, security, error-page and recommended-preset labels in English and Polish.
- Update release/static checks, HTTP/3 documentation and package identity. The source contains no `certificates.json` certificate metadata path.
## 0.13.2
- Fix authenticated dashboard refresh/back-forward session restoration so valid cookies are honored on every panel document route instead of briefly rendering the login shell.
- Make managed software updates force a bounded hot-to-cold cache handoff whenever the cold tier is enabled, independent of the ordinary `cold_on_shutdown` preference, and extend the managed systemd stop budget.
- Persist non-secret automatic-certificate-renewal observations in SQLite schema v3, preload them before administration listeners are exposed, and import the older `certs/renewals.json` sidecar once for upgrade continuity.
- Move the raw full-document editor from primary navigation into **Settings -> Manual config** while retaining `/configuration` as a compatibility browser route.
- Update OpenAPI, EN/PL help, cache/renewal/API documentation, migration/backup checks and release identity for 0.13.2.
## 0.13.1
- Make administrator browser sessions configurable from 7 to 30 days (30 days by default), replacing the previous short browser lifetime/idle behavior while preserving explicit logout/password/revoke invalidation.
- Refresh authenticated cookies only to the remaining absolute server-side expiry so session persistence cannot become an unbounded sliding session.
- Add per-backend `accept_local_listener_risk` with a dashboard confirmation and validation path for deliberate local-listener targets such as `127.0.0.1:9090`; `allow_private` and special-address protections remain independent.
- Update OpenAPI, transport/security/admin-session documentation, EN/PL strings and release regressions for 0.13.1.
## 0.13.0
- Add first-class redirect hosts with dedicated dashboard navigation, named listeners/certificates, exact/prefix and scheme matching, URL/host/path targets, 301/302/303/307/308, path/query policies and redirect cache controls.
- Resolve redirect traffic locally before proxy routes while preserving listener ownership and global host ACLs.
- Change omitted backend `tls_verify` to default false and update UI/API/documentation accordingly.
- Compact TLS certificate cards: show DNS SAN coverage as the same policy-style badges used on vhosts, collapse bindings/files/assigned-vhost details, and inspect older/manual public PEM files when persisted coverage metadata is missing.
- Extend OpenAPI, English/Polish translations, regressions and release tooling for 0.13.0.
## 0.12.1
- Add revisioned per-section configuration PUT endpoints and move ordinary dashboard forms to them, retaining full-document updates only where multiple sections must publish atomically.
- Decouple cache compatibility from the SQLite configuration revision so logging, administration, TLS inventory, metrics and other unrelated saves do not purge hot/cold objects.
- Add explicit dashboard subpage routes plus History API navigation so browser refresh preserves the selected panel.
- Add independent cold-tier policies for idle objects, LRU evictions and graceful shutdown, reduce the cold-write body queue, flush remaining hot objects before process exit, and trim released allocator memory on glibc/Linux.
- Expose certificate DNS SubjectAltName coverage separately from active SNI bindings for uploaded and ACME-managed certificates.
- Preserve secure-by-default backend TLS verification when omitted; keep the explicit debug-origin verification bypass and improve corrective validation/help text.
- Extend regression/OpenAPI/release checks and bump source/package identity to 0.12.1.
## 0.12.0
- Move raw TCP/UDP stream management from Settings to a dedicated **Streams** main-navigation page directly below Vhosts.
- Add vhost-style searchable stream cards and a modal editor with Details, Listener & upstream, and Limits tabs plus an explicit **Add TCP/UDP listener** action.
- Add fixed-cardinality runtime telemetry per active stream: connections/sessions, completed sessions, active sessions, rejects, errors, RX/TX bytes and UDP packet counters.
- Add per-stream live RX/TX rate charts in the dashboard while keeping desired stream changes visibly pending until the process restarts and the listener is actually active.
- Change named HTTP/HTTPS listener bind-address editing from a textarea to chip-style addresses matching the Vhost hostname interaction, preserving existing multi-address validation.
- Keep stream forwarding as raw Layer 4 with no HTTP vhost, ACL, cache, headers, compression or authentication envelope.
- Extend authenticated-asset and offline release/source regressions for the new Streams page and telemetry contracts; bump Cargo, panel, OpenAPI and upgrade/package identity to 0.12.0.
- Extend the Debian 13+ one-line bootstrap with `auto`/`install`/`update` modes; managed updates build the new source as an unprivileged user and delegate to the existing backup/health-check/rollback update path while preserving saved listeners and private data.
## 0.11.5
- Accept percent-encoded spaces (`%20`) in otherwise unambiguous request paths while preserving rejection of encoded separators, double-decoding hazards, controls and dot-segment ambiguity.
- Avoid the `h3-quinn 0.0.10` `stop_sending()` panic path when an HTTP/3 request body is dropped while a receive read is pending.
- Add raw layer-4 TCP/UDP stream hosts on dedicated listeners with bounded TCP connections / UDP client sessions and restart-bound configuration in the admin panel.
- Add a Debian 13+ source bootstrap that installs build dependencies and Rust, clones/builds the repository and installs the systemd service with the explicitly requested public admin profile.
- Match Varnish built-in request-method behavior for the shared cache: `GET`/`HEAD` participate in lookup while successful POST/PUT/PATCH/DELETE/OPTIONS/TRACE requests no longer trigger an implicit global purge.
- Replace the conservative per-request-header/backend-member cache identity with a Varnish-style Host + URL primary identity, isolated by RustProxy vhost/config scope, and use response `Vary` as the secondary representation selector.
- Share cached objects across load-balanced members and across GET/HEAD, reducing avoidable MISSes while retaining Cookie/Authorization pass behavior and response privacy guards.
- Preserve `X-Cache-Resident-Age` across backend 304 revalidation and hot/cold tier movement. Cold metadata format 2 stores variant identity and resident age explicitly.
- Add cache regressions for Vary variants/schema changes, Host/URL hashing, explicit ports, HEAD eligibility and resident-age continuity; update release/package/API/UI metadata to 0.11.5.
## 0.11.4
- Fix Force SSL frontend validation and messaging: require an actual non-TLS listener for the incoming redirect, prefer HTTPS port 443 when multiple TLS listeners are selected, and reject only ambiguous non-standard HTTPS target ports.
- Add a visual reusable ACL-profile rule editor with add/remove/action/CIDR rows. Keep bulk paste/import as an advanced path, normalize invisible Unicode whitespace and report malformed rules with their source line.
- When a vhost disables cache, strip and suppress RustProxy `X-Cache`, `X-Cache-Hits`, `Cache-Status`, `X-Cache-Reason` and `X-Cache-Resident-Age` diagnostics for that response.
- Render backend pools belonging to disabled vhosts as muted `Disabled` rows in the main dashboard instead of classifying them as unavailable.
- Add source/offline regression coverage and bump release/package/UI/OpenAPI/update metadata to 0.11.4. Rust compilation/runtime acceptance remains open in this packaging environment.
## 0.11.3
- Add per-vhost control over forwarding the effective client IP to the application. Disabling it removes `Forwarded`, `X-Forwarded-For` and `X-Real-IP` only on the backend request; ACLs, GeoIP, rate limiting and Tinyauth continue to use the real client identity.
- Add a compact Vhost search/filter for vhost IDs, hostnames/wildcards and backend IDs/URLs.
- Fix the invalid `quinn = 0.11.22` manifest requirement to the resolved `0.11.12` and stop `dev.sh` from rewriting a valid checked-in `Cargo.lock`.
- Fix JSONL access logging under the hardened systemd unit by provisioning `/var/log/rustproxy` as a writable service log directory while keeping `ProtectSystem=strict`.
- Add source/offline regression coverage and bump release/package/UI/OpenAPI/update metadata to 0.11.3. Rust compilation/runtime acceptance remains open in this packaging environment.
## 0.11.2
- Fix administration HTTPS access negotiated over HTTP/2. The admin guard now derives the effective authority from H2/H3 `:authority` (via the request URI) when no legacy `Host` header is present.
- Preserve strict HTTP/1.x Host handling and reject conflicting HTTP/2 `Host` versus `:authority` values to avoid ambiguous authority validation.
- Add per-vhost Force SSL beside frontend selection. Cleartext requests receive a non-cacheable HTTP 308 to the same hostname/path/query on the vhost's unique selected TLS port; ambiguous multi-port TLS selections are rejected.
- Map NPM `ssl_forced` (and migrator `--force-https`) to RustProxy `force_ssl`, retaining HTTP+HTTPS listener bindings when available instead of importing forced hosts as TLS-only.
- Add unit/integration regression coverage for HTTP/2 administration authority, `/healthz`, Force SSL redirect construction/validation and NPM `ssl_forced`; bump release/package/UI/OpenAPI/update metadata to 0.11.2. Rust compilation/runtime acceptance remains open in this packaging environment.
## 0.11.1
- Fix release update health checks when the administration panel uses persisted HTTPS or a persisted bind different from the install-time CLI address. The check now resolves the active admin listener/scheme from `config.sqlite3` and honors the admin Host allowlist.
- Add a standard-library-only Nginx Proxy Manager SQLite migrator. It reads `proxy_host`, creates RustProxy vhosts/backends, preserves wildcard names/WebSocket/enabled/H2 intent, uses the RustProxy default certificate for imported TLS hosts unless a named certificate is selected, and keeps unsupported NPM policy/custom-config items as explicit review warnings.
- Make NPM migration dry-run by default, emit `npm-migration-plan.json`, detect hostname conflicts, split source records above RustProxy's 16-host limit and use revisioned authenticated API writes only with explicit `--apply`.
- Add offline regression coverage and bump release/package/UI/OpenAPI/update metadata to 0.11.1. Rust compilation/runtime acceptance remains open in this packaging environment.
## 0.11.0
- Add reusable ACL Profiles with optional IP/CIDR ACL and country allow/block rules. Vhost profile, inline vhost ACL and route ACL enforcement remains cumulative.
- Add an ACL Profiles navigation page and per-vhost profile selection without removing individual vhost policy controls.
- Add chip-style hostname editing and validated leading `*.` wildcard domains. Exact hosts win before the longest matching wildcard on the same listener.
- Add compact browser-session request-rate history charts to vhost cards, with progressive compaction of older samples.
- Add the built-in HTTP 401 page and classify normal WebSocket tunnel EOF/reset/abort/broken-pipe termination after a successful 101 upgrade as `websocket_closed` rather than a body/proxy error.
- Redirect plaintext requests received on the HTTPS administration socket to HTTPS while keeping application content unavailable over plaintext. Permit valid DNS Host values when the HTTPS admin hostname allowlist is empty; explicit allowlists remain strict.
- Extend named listeners to up to 16 explicit local addresses under one protocol policy. Multiple-address configurations reject unspecified `0.0.0.0`/`::` binds.
- Align the Logging field-builder dialog with the Vhost configuration modal and reposition the Configure fields action next to the format preset.
- Make source ZIP generation deterministic and verify byte-identical archives in release smoke tests.
- Bump Cargo, panel, OpenAPI, installer/update metadata and package identity to 0.11.0. Rust compilation/runtime acceptance remains open in this packaging environment.
## 0.10.0
- Add an optional persistent cold cache tier under the private data directory. Hot LRU/idle objects demote through a bounded asynchronous queue; fresh cold hits are SHA-256 checked and promoted back into the bounded RAM tier.
- Add hot/cold object, byte, promotion, demotion, cold-hit, eviction/error and queue-drop observability to the Cache API, panel and Prometheus output.
- Add cold-tier path, byte/object budgets and idle demotion settings; purge/config publication invalidates both tiers and startup rebuilds the same-revision cold index.
- Document browser hard-reload revalidation behavior and add translations for the exact `request_no_cache`, `request_max_age`, `request_min_fresh` and `request_pragma_no_cache` reasons.
- Add structured-log Standard, Nginx-style, ECS-style and Minimal presets plus a visual field picker/renamer with JSON preview; preserve advanced raw JSON mappings.
- Reuse the shared application card component for Logging sinks and remove release-specific fieldset border styling.
- Add baseline multi-stage Docker/OCI packaging with an unprivileged runtime user and persistent data volume. Docker discovery remains deferred.
- Bump Cargo, panel, OpenAPI, installer/update metadata and package identity to 0.10.0. Rust compilation/runtime acceptance remains open in this packaging environment.
## 0.9.0
- Redesign the vhost list as readable cards with listener/protocol/TLS/cache identity and live pool health, request, active, error, latency and traffic metrics.
- Add bounded asynchronous structured access-log sinks for JSONL files, Logstash TCP and Loki HTTP(S), with configurable JSON field mapping/static fields and sink delivery/drop/error counters.
- Expand cache object inspection with body bytes, resident bytes and metadata overhead; add optional `X-Cache-Resident-Age`.
- Add administration HTTPS with either the application-managed default certificate or a selected inventory certificate. Persisted non-loopback administration binds are accepted when HTTPS is enabled.
- Keep loopback HTTP as the default and retain the explicit legacy risk gate for plain remote HTTP. Secure session cookies are marked `Secure` on HTTPS administration.
- Add global TLS 1.2/1.3 and rustls cipher-suite policy plus per-vhost TCP TLS overrides. HTTP/3 vhosts must use the global TLS 1.3 policy.
- Add dedicated Logging navigation and full-width Settings tabs with new Administration and TLS policy pages.
- Complete roadmap items for remote HTTPS administration and `X-Cache-Resident-Age`; bump release identity, OpenAPI and packaging metadata to 0.9.0.
- Rust compilation/runtime acceptance remains unverified in this packaging environment.
## 0.8.1
- Enable the shared RAM cache by default.
- Replace blanket conditional-request bypass with local conditional HITs for fresh `If-None-Match` / `If-Modified-Since` requests.
- Add validator-based backend revalidation: request `no-cache`, freshness constraints and legacy `Pragma: no-cache` can revalidate a stored representation; backend 304 refreshes metadata and reuses the cached body.
- Keep request `no-store` as a hard bypass. Allow response `Cache-Control: no-cache` with ETag/Last-Modified to be stored only for mandatory validation; `no-store` and `private` still forbid shared storage.
- Retain expired validator-bearing objects for a bounded one-hour revalidation keep window without stale delivery.
- Raise the configurable object ceiling from 8 MiB to 1 GiB, still bounded by the global 1 GiB resident budget.
- Grow fill buffers incrementally and serialize very large fills through the existing fill semaphore to avoid preallocating the configured object ceiling.
- Exclude cache-control/Pragma and conditional/precondition headers from representation-key hashing because they govern reuse rather than representation identity.
- Add cache diagnostics and regressions for conditional HIT, revalidation, no-cache/no-store semantics and the 1 GiB configuration ceiling.
- Bump Cargo, panel, OpenAPI, installer/update metadata and package identity to 0.8.1.
## 0.8.0
- Global static-cache defaults now include common image, stylesheet/script, font, media and download extensions, with request Cookie ignored for matching assets by default. The global and per-vhost controls are visible in the cache UI; `Set-Cookie`, `private`/`no-store` and `Vary: Cookie` still prevent shared caching.
- HTTP/2 and HTTP/3 are per-vhost frontend opt-outs next to listener selection. Controls are enabled only when a selected named listener supports the protocol; TLS H2 ALPN is narrowed by SNI per vhost.
- Settings -> Listeners uses normal page scrolling instead of an extra nested scrollbar.
- Add per-vhost request throttling with graphical request/window/burst controls, country-specific limits and country block/allow-only policies. Return built-in HTML 429/403 responses while preserving custom error-page overrides.
- Download the PDDL `sapics/ip-location-db` user-country IPv4/IPv6 data during first installation over HTTPS and verify publisher SHA-256 checksums; expose load status without exposing client data.
- Verify the installed GeoIP database against current publisher SHA-256 files on every managed update and download only stale/missing assets before downtime.
- Make managed updates regenerate the systemd unit and restore it together with code/private data on failure; preserve and report the original failure reason and add a development-only `--reset-config` recovery path for intentionally incompatible saved vhost/settings documents.
- Add an opt-in per-vhost Cloudflare client-IP mode that accepts `CF-Connecting-IP` only when the direct peer belongs to Cloudflare's published edge CIDRs.
- Add Server-Timing response diagnostics. Disabled per-vhost H3 removes `Alt-Svc` and rejects HTTP/3 requests for that vhost.
- Move vhost listener binding exclusively to named listener selection. Listener addresses are configured under Settings; remove the pre-production inline/global listener compatibility importer and reject the old vhost format instead of migrating it.
- Make health-check mode `off` fully disable active probes and passive health classification instead of quietly learning health from production requests.
- Add a new-origin debugging preset for HTTPS-by-IP origins: default pool/private-origin/HTTP1/preserve-host enabled and backend TLS verification disabled for newly created panel backends, with explicit warnings about the certificate-verification risk and mismatched settings.
- Keep per-vhost inherit/override controls for static-cookie cache behavior while the global defaults cover common static extensions. Cookie is ignored only for the cache key/request bypass decision.
- Fix cache `Age` calculation to retain sub-second response delay and apply RFC 9111 corrected received age before adding response delay.
- Bump Cargo, panel, OpenAPI, installer/update metadata and package identity to 0.8.0.
- Rust compilation/runtime acceptance remains unverified in this packaging environment.
## 0.7.3
- Generate the one-time administrator setup token from the release CLI even before
the first server start; retain the rule that it is invalid after setup completes.
- Add explicit remote-panel opt-in for `0.0.0.0`/`::`, requiring both
`--allow-remote-admin`; keep plain-HTTP risk visible.
- Make Linux/systemd install defaults listen on HTTP 80 and HTTPS 443; add
`--no-https` for explicit HTTP-only installation.
- Add deployment and admin-host regressions and bump release/package/UI/schema
identifiers to 0.7.3.
- Hotfix backend HTTPS-by-IP with optional `tls_server_name`: TLS SNI and hostname
verification may use a DNS name while the validated TCP target stays pinned to
the configured backend URL.
- Add an explicit per-backend `tls_verify` switch, secure by default. Administrators
may turn verification off for an HTTPS origin without weakening DNS/address/self-loop
policy. Add matching 503/504 HTML gateway pages while preserving custom-page overrides.
- Rust compilation/runtime acceptance remains unverified in this packaging environment.
## 0.7.2
- Publish a distinct patch release; keep both previous 0.7.1 deliveries historical.
- Remove the build.rs panic on absent SWAGGER_UI_DOWNLOAD_URL. The asset preparer
defaults to pinned Swagger UI 5.32.11 independently of Cargo config discovery.
- Remove the independently downloading utoipa-swagger-ui adapter and embed the
prepared JS/CSS directly, for debug and release. Existing admin guards stay in place.
- Prepare assets with the existing Python 3.10+ tool requirement, now also needed
for direct Cargo builds. Support absolute local tag ZIPs, optional archive SHA-256,
bounded exact-member reads, validated download caching and retained license notices.
- Resolve build input paths from CARGO_MANIFEST_DIR, track local sources and outputs,
and report missing tools/assets explicitly instead of silently changing UI versions.
- Add executable synthetic-archive regression tests and protected license notices.
- Update release identifiers, packaging/update metadata and migration/build notes.
- Rust compilation and actual Swagger rendering remain unverified in this environment.
## 0.7.1
- Add visual request/response header editing to each custom vhost location, with
set/append/remove, validation, isolation and synchronized optional JSON.
- Split anonymous sign-in from the operational document and module/language bundles.
Remove the login tagline; preserve the original brand and compact square style.
- Reduce public health to `status` and auth discovery to `setup_required` only.
Make the public allowlist exact by method and path; private/unknown requests are
denied before body/query extraction, including method mismatches and aliases.
- Keep Origin/Host/CSRF/session guards; reject duplicate CSRF/marker headers and
hide login/setup JSON parser details. Clear UI data across tabs and on revocation.
- Add authenticated `/docs` and OpenAPI with pinned, build-embedded Swagger UI,
same-session authorization, read-only execution by default and CSRF-confirmed writes.
- Replace updater reliance on a public version with local MainPID/executable checks.
- Add source contracts, browser script regressions, actual-router Rust tests and a
read-only anonymous-access probe. Rust/build/vendor-renderer acceptance not completed.
## 0.7.0
- Renew managed ACME profiles automatically with shared worker bounds, jitter,
backoff, private observations and validated CAS hot deployment; expose opt-out,
check-now and dry-run controls. Keep previous certificates active on failure.
- Add install.sh/update.sh for Linux/systemd, root-owned release switching,
stopped full-data backups and code/data rollback on failed startup/version checks.
- Add per-vhost Basic Auth with Argon2id-only storage, optional verified upstream
credentials and cumulative graphical IP ACLs. Keep protected content out of cache.
- Add WebSocket toggles and graphical custom path locations, including explicit
exclusion of dedicated path origins from normal pool/sticky selections.
- Replace the sample welcome page with neutral information, make the protocol
fixture opt-in, unify square modal and Settings-tab styling, preserve branding.
- Add renewal/deployment helper tests and Rust/browser regressions. Rust compilation,
live CA renewal and real systemd deployment remain unverified in this environment.
## 0.6.0
- Replace separate legacy routes with vhosts and a one-way, validated compatibility importer; preserve path-pinned origins, ACLs and existing listener scope.
- Add HTTP-01 webroot domain validation, Cloudflare DNS-01 and OVH DNS-01 alongside RFC2136 using private Certbot workers.
- Unify certificate creation/import in a provider-picker modal; record optional issuer metadata in the library and keep secrets outside exported configuration.
- Add an independently configurable Prometheus endpoint with actual-peer CIDRs, optional environment-sourced bearer token, bounded scrapes and collector switches.
- Export application/admin/listener/vhost/backend/cache/system data and full-exchange vhost histograms with bounded configured labels.
- Split Settings into functional switching cards; preserve dirty values, keyboard access and translation discovery; remove Security and legacy routing sidebar entries.
- Add offline provider/browser regression tests and Rust tests for migration, ACME challenge isolation and scrape authorization. Rust compilation/runtime and actual CA/provider integrations remain unverified.
- Preserve the original brand assets. Automatic certificate renewal, Cloudflare Tunnel and Docker discovery remain deferred.
## 0.5.0
- Add a compact dedicated cache panel with vhost/domain counters, object pagination,
TTL/Age/hits, memory/fill diagnostics and authenticated selective purge.
- Fix reserved administrator-cookie ordering and empty terminating DATA frames;
emit fixed cache decision reasons without exposing request secrets.
- Replace special X-Mock-Origin hiding with the explicit global/vhost revision
switch; ordinary response rules still handle arbitrary removable headers.
- Default HTTP/3 on TLS bindings, retain explicit null opt-outs, and explain
overlapping sockets with ownership and concrete protocol/limit differences.
- Return 503 for upstream connection/unavailability, 502 for invalid responses
and 504 for header timeouts; generated errors use no-store.
- Add basic asynchronous Certbot DNS-01/RFC2136 issuance, staging-first consent,
private TSIG/account profiles, output validation and explicit library import.
No automatic renewal scheduler or real CA validation is claimed.
- Add offline helper and UI checks plus Rust regression source. Rust/Cargo remain
unavailable in the packaging environment; compilation/runtime unverified.
## 0.4.0
- Rebuild standard Forwarded and X-Forwarded metadata at an explicit trusted-proxy boundary; use verified client IP for ACL/affinity, never for destination routing.
- Add per-vhost Tinyauth subrequests, identity header allowlists, safe same-login-origin redirects and fail-closed checks before origin/static/cache/WebSocket use.
- Move certificate upload into an independent inventory; assign certificates by ID per vhost and expose metadata-only inventory reads. ACME is not implemented.
- Add named multiple-IP listeners with descriptive scope, opt-in legacy routes, reusable vhost references and actual/desired plans.
- Add Quinn/h3-based HTTP/3 UDP transport, TLS 1.3/SNI, Alt-Svc, per-endpoint connection and per-connection stream/idle limits, shared proxy policies and bounded shutdown. Preserve TCP HTTP/2 plus HTTP/1.1.
- Hide request-ID/revision headers by default; add individual overrides and optional mock-origin stripping. Apply effective Server policy across all response paths, including extra sockets.
- Partition cache by actual frontend IP/port and safely probe unknown-length empty requests; preserve nonempty body frames instead of discarding them.
- Raise toast above active dialogs without making it inert under the modal; retain the logo, compact UI and discovered language packs.
- Add a loopback-only fake-auth development peer and Rust integration sources for forwarding, auth, extra-socket cache, listener ownership and real QUIC/TCP fallback.
- Execute frontend/helper/static checks documented in TEST_REPORT.md. Rust/Cargo are unavailable: no passing compilation, real proxy run or full RFC-conformance claim.
## 0.3.0
- Split the large status JSON macro into small objects and an explicit feature map;
preserve response fields without increasing Rust's macro recursion limit.
- Add optional per-vhost certificate inventory selection and PEM file upload.
Validate key/certificate pairing and names, write private immutable files, and
activate only through the existing revisioned configuration update.
- Add per-vhost cache overrides and selective invalidation, including stale
in-flight fill cancellation while leaving other host entries intact.
- Add administrator-authored, bounded global/vhost error pages, static vhost and
missing-path responses, direct-IP and unknown-host responses. Retain ACL checks,
HEAD handling and authentication challenges; never execute HTML in the admin UI.
- Move access logs into a separate modal/docked viewer with fixed filters,
vhost identity, pause, scoped search/status/method/cache controls and JSON export.
- Add a compact vhost list, tabbed dialog, live health/latency summaries and visual
per-host response/request headers; keep the original logo and favicon.
- Add density preferences and a Settings shortcut to Security.
- Add a Python-only mock origin with random data, cache/no-store/private/cookie
paths, compression-size payloads, controllable delay/health and error statuses.
- Add Rust regression source for pages, cache scoping, TLS staging and API guards.
- Rust compilation/runtime acceptance remains unverified: toolchain unavailable.
Executed checks and their exact limitations are recorded in TEST_REPORT.md.
## 0.2.0
- Replace SizeAbove(u16) with a custom u64 predicate; add >64 KiB threshold regressions.
- Add IP/port/TLS-bound virtual hosts with frontend(s), backend pool and path policies.
- Enforce real accepted destination matching; isolate legacy routes from extra sockets.
- Add weighted round-robin, least-connections, least-traffic, adaptive and IP hashing.
- Add bounded TCP/HTTP health checks, separate passive failures, recovery and slow start.
- Add connection keepalive controls, backup members, drain and opaque cookie/IP affinity.
- Preserve compatible observations/tokens across hot edits; recheck changed/re-enabled members.
- Keep leases through full uploads/responses/tunnels; never replay failed requests.
- Add per-vhost TLS-only HSTS, nosniff, frame/referrer/CSP/permissions headers.
- Retain HTTP/1.1 ALPN with HTTP/2 for TLS frontends and HTTPS origins.
- Separate cache identities by selected member; bypass cookie-affine pools.
- Add compact vhost editor, atomic legacy migration, backend observations/API/metrics/log IDs.
- Extend discovered English/Polish translations; preserve original logo and favicon.
- Add Rust regression source and a real-binary vhost/keepalive/health acceptance suite.
- Execute 63 simulated-API UI, 24 SQLite, 21 upgrade, 4 language-mirror, 10 protocol-peer
and 7 backend-fixture checks. None is a passing Rust build or runtime test.
- Rust/Cargo remain unavailable in the packaging environment; compilation/runtime
acceptance and a resolved Cargo.lock remain mandatory before deployment.
## 0.1.0
Development source advancing directly from 0.0.6. Cloudflare is deferred.
### Added
- Persisted public listener addresses/ports, 80/443 presets, startup defaults and restart status.
- Bounded streaming shared RAM cache, TTL/Age, LRU, privacy/Vary protections, per-route bypass, optional HIT/MISS headers and audited purge.
- Streaming gzip, Brotli, Zstandard and deflate controls, final Server policy and a global client-response header editor.
- Compact dashboard with system/process/cache diagnostics, log filters and export.
- Compile-time language discovery, JSON packs, metadata/RTL/fallback, and preservation of extra packs during upgrade.
- Example unprivileged low-port service and cache/settings real-binary integration test source.
### Changed
- Allow ordinary remote-origin 80/443 while guarding local proxy/admin listener targets.
- Keep original logo/favicon; reduce panel spacing and improve data density.
- Invalidate cache and older fills on config changes, explicit purge and successful unsafe methods.
- Expand source/SQL/upgrade/language/frontend checks; actual execution scope is in docs/TEST_REPORT.md.
### Verification boundary
Rust/Cargo and real Rust integration were unavailable. This is a source candidate,
not a successfully compiled or production-certified binary. No lockfile is fabricated.
## 0.0.6
Cumulative development source release. The planned 0.0.3, 0.0.4 and 0.0.5 scopes
are included here, not represented as separately published archives.
### Fixed
- Explicitly select `src/auth/mod.rs` to prevent the reported flat/folder module
conflict when obsolete 0.0.1 source remains in an existing checkout.
- Detect mixed source layouts and preserve the known legacy auth file; refuse to
delete unrecognized local changes.
- Support interactive username input for `admin init`.
- Replace managed source directories with rollback on copy failure rather than
encouraging overlay extraction.
### Added
- DNS and HTTPS manual backends with per-backend pools, bounded DNS workers,
validated DNS answers, optional CIDR allowlists and additional PEM trust anchors.
- rustls public TLS listener, persistent development default certificate, exact SNI
bindings, local generate/import commands and explicit hot reload.
- Public HTTP/2 over TLS ALPN and HTTP/2/auto backend protocol options.
- Validated HTTP/1.1 WebSocket handshakes and bounded bidirectional tunnels,
including TLS on either side, connection leases and a separate idle timeout.
- Transport metrics, WebSocket lifetime logging, TLS editor and advanced backend
controls in the English/Polish panel.
- Backup of configured/default certificate files alongside the SQLite snapshot.
- Upgrade regression tests, transport configuration tests, real protocol test
scripts and CI steps. Test execution limits are recorded in TEST_REPORT.md.
### Changed
- Public TLS is enabled by default by `dev.sh`; direct binary startup keeps it optional.
- Listener ports are protected across HTTP, HTTPS and administration.
- All Markdown documentation is maintained in English.
- The roadmap identifies the combined transport release and upcoming Docker stages.
## 0.0.2
- Introduced RustProxy naming, administrator setup, Argon2id passwords, sessions,
CSRF checks, login throttling, password recovery and credential revocation.
- Added host/route ACLs, generic request/response header policies, administration
audit, SSE dashboard updates and security controls in the bilingual panel.
- Added an additive SQLite migration, offline validation and online database backup.
- Rust build/runtime verification was unavailable for the source package.
## 0.0.1
- Initial manual-backend HTTP/1.1 proxy, streaming and keep-alive.
- SQLite configuration, dynamic host/alias/path routing and local token control plane.
- English/Polish interface, bounded access log, counters, CLI, demo backend and dev.sh.
- Rust build/runtime verification was unavailable for the source package.
Generated
+2994
View File
File diff suppressed because it is too large. Load diff
+77
View File
@@ -0,0 +1,77 @@
[package]
name = "rust-proxy"
version = "0.24.5"
edition = "2021"
rust-version = "1.88"
description = "RustProxy - independent reverse proxy with local administration"
publish = false
[dependencies]
argon2 = { version = "0.5.3", features = ["std"] }
sha2 = "0.10"
ipnet = "2.11"
rpassword = "7.4"
zeroize = "1.8"
anyhow = "1.0"
libc = "0.2"
arc-swap = "1.7"
axum = { version = "0.8", default-features = false, features = ["http1", "http2", "json", "query", "tokio", "ws"] }
bytes = "1.10"
clap = { version = "4.5", features = ["derive", "env"] }
futures-util = "0.3"
fs2 = "0.4"
http = "1.3"
http-body = "1.0"
http-body-util = "0.1"
hyper = { version = "1.6", features = ["client", "server", "http1", "http2"] }
hyper-util = { version = "0.1", features = ["client-legacy", "server-auto", "http1", "http2", "tokio"] }
rand = "0.8"
regex = "1.11"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio", "sqlite"] }
subtle = "2.6"
tokio = { version = "1.44", features = ["macros", "rt-multi-thread", "net", "signal", "time", "sync", "fs", "io-util", "process"] }
tokio-util = { version = "0.7", features = ["rt"] }
tower = { version = "0.5", features = ["util"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
url = "2.5"
base64 = "0.22"
sha1 = "0.10"
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"] }
hyper-rustls = { version = "0.27", default-features = false, features = ["ring", "http1", "http2", "tls12"] }
rustls-pemfile = "2.2"
webpki-roots = "1.0"
rcgen = { version = "0.13.2", default-features = false, features = ["crypto", "pem", "ring"] }
httpdate = "1.0"
tower-http = { version = "0.6.8", default-features = false, features = ["compression-gzip", "compression-br", "compression-zstd", "compression-deflate"] }
flate2 = "1.1"
brotli = "8"
zstd = "0.14"
h3 = { package = "hpx-h3", version = "=2.5.19", features = ["quinn"] }
quinn = { version = "0.11.12", default-features = false, features = ["runtime-tokio", "rustls-ring"] }
metrics-process = { version = "=2.4.3", default-features = false, features = ["dummy"] }
sysinfo = { version = "=0.38.4", default-features = false, features = ["network"] }
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly", target_os = "macos", target_os = "windows"))'.dependencies]
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.
[build-dependencies]
serde_json = "1.0"
[dev-dependencies]
tempfile = "3.19"
[profile.release]
lto = "thin"
codegen-units = 1
strip = "debuginfo"
+24
View File
@@ -0,0 +1,24 @@
# syntax=docker/dockerfile:1
FROM rust:1.88-trixie AS build
WORKDIR /build
COPY . .
RUN if [ ! -f Cargo.lock ]; then cargo generate-lockfile; fi \
&& cargo build --locked --release
FROM debian:trixie-slim AS runtime
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates python3 certbot openssl \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd --system --gid 10001 rustproxy \
&& useradd --system --uid 10001 --gid rustproxy --home /var/lib/rustproxy --shell /usr/sbin/nologin rustproxy \
&& install -d -o rustproxy -g rustproxy -m 0700 /var/lib/rustproxy \
&& install -d -o rustproxy -g rustproxy -m 0750 /var/log/rustproxy
COPY --from=build /build/target/release/rust-proxy /usr/local/bin/rust-proxy
USER rustproxy:rustproxy
ENV RUST_PROXY_DATA_DIR=/var/lib/rustproxy \
RUST_PROXY_PROXY_LISTEN=0.0.0.0:8080 \
RUST_PROXY_ADMIN_LISTEN=127.0.0.1:9090
VOLUME ["/var/lib/rustproxy"]
EXPOSE 8080 8443 9090
ENTRYPOINT ["/usr/local/bin/rust-proxy"]
CMD ["serve"]
+802
View File
@@ -0,0 +1,802 @@
# RustProxy 0.24.5
Independent Rust reverse proxy with a dashboard, SQLite configuration,
streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhosts and backend pools.
**0.24.5 release source.**
## Layer-4 streams
Raw TCP/UDP forwarding has its own **Streams** page in the primary navigation, alongside **Vhosts**, **Redirects** and **404 hosts**. Streams use the same card/modal workflow as vhosts, but each stream owns a dedicated Layer-4 listener and one upstream and intentionally bypasses HTTP ACL/cache/header/compression logic. The page exposes live per-stream connections/sessions, rejects, errors, transferred bytes and RX/TX rate charts. Listener changes require a restart. See [TCP/UDP streams](docs/STREAMS.md).
Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost hostnames, including multi-address listeners.
## What changed in 0.24.5
- Fix HTTP/3 requests with an actually empty, length-unknown body when forwarding to an HTTP/1.1 upstream. RustProxy now observes the H3 body end before handing the request to Hyper so methods such as `DELETE`, `POST`, `PUT` and `PATCH` are represented as a known zero-length body instead of being emitted as `Transfer-Encoding: chunked`.
- Preserve streaming semantics for non-empty HTTP/3 request bodies: the first inspected frame is replayed and the remaining body continues streaming; declared `Content-Length` bodies and HTTP/2 upstreams are left unchanged.
- Add focused regressions for bodyless HTTP/3 `DELETE`, non-empty `DELETE`, declared-length requests, HTTP/2 upstreams and the generic bodyless method set.
- Harden the existing HOT -> COLD cache model with allocation-aware HOT admission, bounded transient fill/tier-transfer RAM, a byte-bounded COLD writer queue and incremental `max_object_bytes` enforcement before buffer growth.
- Track HOT logical and allocated/charged bytes separately, preserve body allocation accounting through revalidation/COLD promotion, make global cache disable authoritative over vhost overrides, and remove the old artificial 1 GiB HOT/object ceiling.
- Add host/cgroup-aware conservative/recommended/performance cache presets in Settings. The panel shows the recommended values for detected effective RAM and warns about high cache share, low current headroom, an oversized COLD queue or a maximum object that cannot fit in transient RAM.
- Keep the source package consistent with its existing release contracts by dropping the stale unreferenced `src/system_stats/bsd.rs` file and updating the offline container check to the already-shipped Debian 13/Trixie base images.
- Update package/source/UI/OpenAPI/release identity to 0.24.5.
## What changed in 0.24.4
- On Linux, opt the RustProxy process out of Transparent Huge Pages with `prctl(PR_SET_THP_DISABLE)` before Tokio creates worker threads. This keeps the memory policy process-local and avoids requiring a host-wide `/sys/kernel/mm/transparent_hugepage/enabled` change.
- Keep the existing managed `MALLOC_ARENA_MAX=2`, transform allocation reductions and bounded `malloc_trim(0)` paths from 0.24.3.
- Update package/source/UI/OpenAPI/release identity to 0.24.4.
## What changed in 0.24.3
- Reduce long-running Linux/glibc RSS retention by shipping `MALLOC_ARENA_MAX=2` in the managed systemd service while keeping `/etc/rustproxy/environment` available as the administrator override.
- Reduce transform-path temporary allocations: identity bodies are borrowed instead of copied during decode, JavaScript/CSS no longer build a full `Vec<char>` or copy the completed output buffer again, successful transforms release origin/decode buffers early, and large glibc transform churn gets a throttled `malloc_trim(0)`.
- Keep HTTP/2 and HTTP/3 transport enablement together in **Settings -> Listeners** and make both connection-tuning pages listener-scoped. **Settings -> HTTP connections -> HTTP/2** now edits lifecycle/GOAWAY limits per H2/h2c listener, while **HTTP/3** edits per-listener QUIC limits; listener rows link directly to the matching tuning page. Legacy top-level `http2` values remain a read-time fallback for older configurations.
- Rename the dashboard **Proxy process** card to **Process** and hide unsupported process metrics/charts instead of rendering `unknown`/`N/A` placeholders for platform-specific fields.
- Add targeted hot reload for manual/file-backed certificates with `POST /api/certificates/{certificate_id}/reload`; the operation keeps the configuration revision stable and is available to the scoped Certificate API credential as well as administrator sessions.
- Expand the scoped Certificate API to up to 32 independently revocable tokens, each with a non-secret operator comment; cleartext tokens remain one-time responses and only hashes are persisted.
- Replace the dashboard **Core State** table with a live connection/activity chart, move **Uptime** into **Process** directly below process start, split **Payload transferred** into upload/download counters, and let operators hide individual network-interface charts in browser-local preferences.
- Update package/source/UI/OpenAPI/release identity to 0.24.3.
## What changed in 0.24.2
- Version-only release bump from 0.24.1 to 0.24.2; no runtime or configuration behavior changes.
## What changed in 0.24.1
- Restore network interface/RX/TX diagnostics after the statistics-library refactor. Network inventory and byte counters now use `sysinfo::Networks` as the primary API and merge in `systemstat` as a library-level supplement/fallback, including targets where the primary API has no inventory; the implementation does not shell out to `netstat`/`sysctl` or reintroduce per-OS network parsers.
- Fix the portable system-uptime build error by using the duration API exposed by `systemstat` and remove the stale H2 `Future` import warning.
- Remove application-level item-count ceilings for certificate hostnames/SAN requests and the hostname lists that feed certificate selection (vhosts, redirects, 404 hosts, administration allowed hosts, host policies and Docker aliases). DNS syntax/length validation, certificate/key size bounds, global configuration size and other resource-safety limits remain in place.
- Replace the hand-written BSD statistics backend with shared library APIs: `systemstat` for portable host metrics, `sysinfo::Networks` for network inventory/counters, and `metrics-process` for process metrics. `systemstat` remains the library-level network supplement/fallback where `sysinfo` has no native inventory or returns only a partial view. Unsupported values remain null instead of being synthesized.
- Preserve Linux `/proc` and cgroup fields as additive Linux-only details so existing Linux names, units and semantics remain compatible.
- Expose additional portable diagnostics when available: system swap/uptime and process virtual memory, descriptor limit, start time, RSS and thread count.
- Add authenticated runtime metadata to `/api/status`: platform, statistics source, resolved data directory and database path. The UI does not guess installation paths.
- Make local CLI data-directory discovery platform-aware: Linux convention `/var/lib/rustproxy`, BSD/OPNsense convention `/var/db/rustproxy`, while an explicit `--data-dir` always wins.
- Move Logging from the primary navigation into **Settings -> Logging** and keep `/logging` as a compatibility redirect/canonical route.
- Update package/source/UI/OpenAPI/release identity to 0.24.1.
This workspace does not provide a Rust toolchain by default. Compiler/type/runtime acceptance and resolver-generated `Cargo.lock` for the new statistics dependencies must be completed on a compiler-enabled release host. See [the 0.24.1 specification audit](docs/V0.24.1_AUDIT.md) and [verification report](docs/TEST_REPORT.md).
## What changed in 0.24.0
- Rework downstream HTTP/2 connection retirement into graceful `GOAWAY -> drain -> TCP close`: idle shutdown now checks active streams and recent frame traffic before retiring a connection, and a bounded drain timeout prevents indefinite hangs.
- Add independent global HTTP/2 `idle_timeout_seconds`, `max_connection_lifetime_seconds` (`0` disables it) and `drain_timeout_seconds` settings, exposed through the Settings UI and section API. Lifetime expiry uses the same graceful path rather than an abrupt socket close.
- Keep RFC 8441 WebSocket-over-H2 streams inside the active-stream drain accounting, so existing tunnels may finish while new H2 streams are refused after GOAWAY.
- Extract system/process sampling into `system_stats` platform backends. Linux keeps the existing `/proc`/cgroup field names, units and semantics; FreeBSD/OpenBSD/NetBSD use native `sysctl`/system interfaces where available and leave unsupported metrics null.
- Show the runtime platform in **Proxy Process** and export `rust_proxy_platform_info{platform=...}`.
- Group protocol tuning under **Settings -> HTTP connections**, with HTTP/2 and HTTP/3 sub-tabs. H3/QUIC remains listener-scoped and restart-required, but its enablement and limits are edited from the HTTP/3 sub-tab instead of the listener editor.
- Update package/source/UI/OpenAPI/release identity to 0.24.0 and add H2 lifecycle/configuration regressions.
This workspace does not provide `cargo`, `rustc` or `rustfmt`, so compiler/type/runtime acceptance remains a release-host gate. See [the 0.24.0 specification audit](docs/V0.24.0_AUDIT.md) and [verification report](docs/TEST_REPORT.md).
## What changed in 0.23.3
- Add per-vhost **Enable WebSocket legacy support** compatibility mode for older appliances and embedded WebSocket servers such as iLO-class management controllers.
- In legacy mode, force the upstream WebSocket hop to HTTP/1.1, force the legacy WebSocket `Host` to the configured backend authority, align `Origin` to that backend scheme + authority, and tolerate legacy `101 Switching Protocols` framing while retaining strict `Sec-WebSocket-Accept` and subprotocol validation.
- Keep the compatibility behavior scoped only to WebSocket handshakes; ordinary HTTP traffic and vhosts without the option are unchanged.
- Add dashboard/OpenAPI/language-pack controls, source regressions and release verification for the new compatibility switch.
- Update source/package/UI identity and release verification contracts to 0.23.3.
Rust compiler/type/runtime acceptance remains a release-host gate in this workspace when `cargo`, `rustc` or `rustfmt` are unavailable. See [the 0.23.3 specification audit](docs/V0.23.3_AUDIT.md) and [verification report](docs/TEST_REPORT.md).
## What changed in 0.23.2
- Add a selectable live-chart history window for Cache and Transformation: 60, 120, 300 or 600 samples (about 3, 6, 15 or 30 minutes at the existing 3-second refresh). Each workspace remembers its choice in browser local storage and remains bounded.
- Fix `rust-proxy --livelogs` when administration TLS is enabled. The CLI now probes the local administration listener, switches to HTTPS after the expected HTTP 308 redirect, and pins the presented TLS leaf certificate to certificates stored in the private RustProxy data directory.
- Reuse the same HTTP/HTTPS-aware local administration client for CLI configuration and certificate API calls, so local commands no longer fail solely because the administration listener is HTTPS.
- Update source/package/UI identity and release verification contracts to 0.23.2.
Rust compiler/type/runtime acceptance remains a release-host gate in this workspace when `cargo`, `rustc` or `rustfmt` are unavailable. See [the 0.23.2 specification audit](docs/V0.23.2_AUDIT.md) and [verification report](docs/TEST_REPORT.md).
## What changed in 0.23.1
- Add a collapsible, full-width **live Cache activity** chart to the Cache workspace. It samples the selected scope and plots HIT, MISS and BYPASS rates while preserving the existing 3-second live refresh.
- Add a matching collapsible, full-width **live Transformation activity** chart. It plots transformed responses, transformed cache HITs, skipped/fail-open responses and errors for the selected scope.
- Keep both charts collapsed by default, retain their in-memory history while the workspace stays active, and reset chart history when the selected vhost scope changes.
- Update source/package/UI identity and release verification contracts to 0.23.1.
## What changed in 0.23.0
- Add a scoped **Certificate API** credential configured from **Settings → Certificate API**. External automation uses `X-RustProxy-Certificate-Key` plus `Authorization: Bearer ...`; only deploy/reload are authorized, the token is stored only as a hash, and non-loopback use requires HTTPS.
- Keep production local certificate rotation independent of development tooling: call `rust-proxy tls deploy --id ID --cert fullchain.pem --key privkey.pem` or `rust-proxy tls reload` directly from a Certbot deploy hook or administrator shell.
- Add `rust-proxy --livelogs`, an interactive all-vhost request stream modeled after varnishlog but using structured commands instead of regex expressions. Filters include host/vhost/backend/route/client/method/transport/cache/status/path, errors and slow requests; `show ID` prints the complete stored event.
- Feed live logs through a bounded non-blocking broadcast/SSE path. If a viewer cannot keep up, RustProxy emits an explicit dropped-entry count rather than silently pretending the stream is complete.
- Split **Settings → Cache** into four sub-tabs: **General**, **Resilience**, **Cold cache**, and **Static & diagnostics**. The form still saves one cache policy atomically.
- Add SQLite schema v5, OpenAPI security definitions, audit/status metadata, translations and regression coverage for the new control-plane capabilities.
## What changed in 0.22.3
- Add standards-oriented downstream HTTP/1.1 `Upgrade: h2c` on cleartext listeners that already enable `h2c`, while preserving the existing HTTP/2 prior-knowledge path. The initiating body-free H1 request is reconstructed as HTTP/2 stream 1 after `101 Switching Protocols`; malformed upgrades are rejected before switching.
- Add authenticated certificate hot reload through `POST /api/certificates/reload` and `rust-proxy tls reload`. Configured public certificate/key files are re-read and validated without changing the desired configuration revision; new public TLS/H2/H3 handshakes use the refreshed snapshot while existing sessions continue.
- Add external certificate deployment for local issuers such as system Certbot: `rust-proxy tls deploy --id ID --cert fullchain.pem --key privkey.pem`. The CLI resolves/checks local Certbot files and calls authenticated loopback `POST /api/certificates/deploy`; the running RustProxy service validates the pair/SANs, creates immutable generation files with its own filesystem identity and convergently creates a missing manual certificate id or atomically rotates an existing one used by any number of vhosts; identical RustProxy-owned material is a no-op.
- Reject external deployment into RustProxy-managed ACME certificate ids to avoid dual renewal ownership, roll back staged files when publication fails, and explicitly report that startup-fixed administration TLS needs a process restart when it selects the changed certificate.
- Extend the Certificates workspace with an explicit **Reload certificate files** action plus OpenAPI, audit logging, translations, source regressions and the 0.22.3 verification/audit documentation.
Rust compiler/type/runtime acceptance remains a release-host gate in this workspace because `cargo`, `rustc` and `rustfmt` are unavailable. See [the 0.22.3 specification audit](docs/V0.22.3_AUDIT.md) and [verification report](docs/TEST_REPORT.md).
## What changed in 0.22.2
- Add replay-safe bounded backend failover retry for empty `GET`, `HEAD` and `OPTIONS` exchanges, with per-vhost retry policy, passive-health integration, logs and Prometheus counters.
- Send ordinary requests to explicit `protocol=http2` backends as HTTP/2 and retain a real Hyper h2c prior-knowledge upstream regression.
- Add per-backend `max_concurrent_requests` with a bounded capacity wait (`queue_timeout_ms`), immediate/timeout `503` rejection, dashboard controls and rejection/queue-timeout metrics. A TCP socket-count limit remains deliberately separate and is not claimed by this setting.
- Add detailed upstream failure classification and last-failure diagnostics for DNS, TLS, connect, response-header timeout, read/send timeout and I/O failures, plus health ejection/recovery timestamps.
- Extend the backend dashboard table with configured concurrency, last failure/reason and last recovery while retaining health, request/error, traffic and latency data in one operational view.
- Parse public leaf-certificate metadata for issuer, SANs, validity window, days remaining and expiry state; surface renewal status/last attempt/error in the certificate inventory without reading private keys.
- Update OpenAPI, translations, metrics documentation, regression sources and the 0.22.2 audit/TODO for the expanded release scope.
Rust compiler/type/runtime acceptance remains a release-host gate in this workspace because `cargo`, `rustc` and `rustfmt` are unavailable. See [the 0.22.2 specification audit](docs/V0.22.2_AUDIT.md) and [verification report](docs/TEST_REPORT.md).
## What changed in 0.22.1
- Use one shared overview stats grid for primary and transport/WebSocket metrics, so both visual rows have identical card sizing and spacing. Cache **Target** and **Cache key / tags** cells copy their full untruncated value on click or keyboard activation.
- Move selective **Purge / Ban** and **Active BANs** into dedicated Cache modals next to **Purge selected scope**; operation results use null-safe inline feedback styled like the login form.
- Add opt-in HTTP/2 RFC 8441 WebSocket Extended CONNECT beside the existing RFC 9220 per-vhost option. The listener advertises Extended CONNECT only when an opted-in vhost is bound to that H2 endpoint.
- Support RFC 8441 H2 clients through RustProxy to an HTTP/1.1 WebSocket upstream bridge or an explicitly configured HTTP/2 upstream. Lifecycle handling includes shutdown cancellation, idle timeout and byte counters.
- Add explicit cleartext HTTP/2 **h2c prior-knowledge** listeners. HTTP/1.1 `Upgrade: h2c` is intentionally not exposed because the current Hyper integration cannot safely reconstruct the required HTTP/2 stream 1 bootstrap.
- Add stable active-tunnel IDs, inventory (`client`, `host`, `upstream`, protocol, duration, RX/TX), authenticated kill with operator reason, and `tunnel.kill` audit records.
- Consolidate WebSocket visibility on the main dashboard as total active plus H1 / H2 RFC 8441 / H3 RFC 9220, while ordinary H2/H3 request counters remain separate.
- Retain the full 0.22.0 cache feature set: single-flight fills, cached single-range 206, selective PURGE/BAN, configurable stale-if-error and proactive refresh.
- Update release/package/UI/OpenAPI/Debian/Grafana/tooling identity to 0.22.1.
The source package includes Rust regression definitions and Rust-independent release checks. This workspace does not contain `cargo`, `rustc` or `rustfmt`, so compiler/type/runtime acceptance remains a required release-host gate. See [the 0.22.1 specification audit](docs/V0.22.1_AUDIT.md) and [verification report](docs/TEST_REPORT.md).
## What changed in 0.21.0
- Shard Cold Cache objects by the first two SHA-256 hex characters, add bounded asynchronous 90% -> 80% garbage collection, and expose selective asynchronous cleanup in the Cache panel.
- Implement RFC 5861 `stale-while-revalidate` / `stale-if-error`, request `max-stale`, age-aware request `max-age=N`, and correct shared-cache `must-revalidate` / `proxy-revalidate` semantics while retaining `s-maxage` precedence.
- Add a formal decode -> transform -> canonical cache -> encode stage for JS/CSS plus optional HTML/SVG/JSON transformations, with strict origin `no-transform`, separate origin/representation validators, transformed Content-Length, canonical `Accept-Encoding` handling and safe incomplete-fill behavior.
- Keep route cache overrides tri-state (`inherit` / enabled / disabled) and report effective vhost/route cache and transform decisions in access diagnostics.
- Make Passive Backend Health opt-in and disabled by default for ordinary and Docker-generated vhosts; active TCP/HTTP health remains independent and ordinary traffic errors remain observable even when they do not affect backend health.
- Add global Settings, vhost and Docker-discovery controls for transformation, plus OpenAPI/admin coverage and expanded cache/transform/health regressions.
- Add a dedicated **Transform** monitoring workspace with global/per-vhost runtime effectiveness counters, mark transformed objects in Cache, and expose transformed representations through the standardized `Cache-Status` diagnostic.
- Tighten Docker-vhost desktop layout so the transform switch stays with the other primary switches and Passive Backend Health fields remain readable when enabled, including checkbox sizing in the expanded editor.
- Add a transformed-response series to **Live traffic** and show **Core state** uptime in compact minute/hour/day/year form.
- Keep `/transform` refreshable as a first-class dashboard route; use a square-aligned Cache transformed badge and a dedicated violet color for the transformed live-traffic series.
- Rename the backend timing diagnostic to `X-RustProxy-Backend-Time`, while stripping the former `X-Rust-Proxy-Backend-Time` spelling as an untrusted upstream lookalike.
- Split the former monolithic `src/cache.rs` into a responsibility-based `src/cache/` module and remove the unused mutable background-revalidation binding.
- Keep package/source/UI/OpenAPI/Debian/Grafana/tooling identity at 0.21.0.
The 0.21.0 feature scope is closed for release packaging. The conservative JS/CSS transformers remain supported as-is for this release; parser-backed replacements are deferred. This workspace still cannot execute the Rust compiler/runtime gate, so a release host must run that gate and live protocol acceptance before deployment. See [the 0.21.0 specification audit](docs/V0.21.0_AUDIT.md) and [verification report](docs/TEST_REPORT.md).
## What changed in 0.20.0
- Rebuild **Certificates** as a three-tab workspace: **Installed**, **Renewal** and **Generate**, matching the Settings tab interaction and responsive layout.
- Move certificate generation/import/existing-file controls into the Generate tab so the workflow remains visible instead of opening a separate modal.
- Add clear ACME operation states and show the exact private Certbot `logs/` directory when issuance or renewal fails; persisted renewal failures also expose the corresponding profile log path.
- Improve renewal cards with provider/environment context, structured last/next/expiry fields and clearer action grouping.
- Update release/package/UI/OpenAPI/Debian/Grafana/tooling identity to 0.20.0.
## What changed in 0.19.4
- Align the Docker-vhost Aliases chip editor with neighboring 38px controls and remove the inherited extra label margin that made the field look like it started on a new line.
- Keep backend traffic formatting explicit: idle is `0.0 KiB/s`, while any positive rate below 0.1 KiB/s is rendered as `<0.1 KiB/s` in backend rows and aggregate vhost traffic.
- Add an All / Cache enabled / Cache disabled selector to **Cache -> Domains and effectiveness**. Filtering affects only the domain table; aggregate cache statistics remain scoped by the selected vhost and are not changed by the presentation filter.
- Update release/package/UI/OpenAPI/Debian/Grafana/tooling identity to 0.19.4.
## What changed in 0.19.3
- Fix the update-checker body collection error conversion so compiler-enabled Rust builds do not require `anyhow::Context` on Hyper/http-body-util's boxed body error.
- Add a dark/light administration theme selector in Settings. The preference is account-scoped in SQLite, defaults to the existing dark theme, and uses a small local cache only to prevent first-paint theme flicker before the authenticated preference is loaded.
- Consolidate theme-sensitive CSS around shared surface/text/border variables while preserving the RustProxy accent palette.
- Make the Live Logs column chooser a compact, scrollbar-free standard-checkbox list; on short screens it switches to two columns rather than adding internal scrollbars.
- Fix the remaining Live Logs checkbox inflation caused by broad `.log-tools input` text-field rules; checkbox/radio controls are now excluded from both sizing blocks.
- Simplify the dashboard footer to API latency + dropped-log count and add a fail-soft hourly release check against the official repository Cargo manifest; a newer version is linked in the footer without performing any automatic upgrade.
- Re-authenticate expired sessions through the minimal login page while preserving the exact panel and Settings sub-tab; embedded login errors are inline and the authenticated shell no longer flashes during the redirect.
- Canonicalize `/configuration` to `/settings?tab=manual-config` and persist every Settings tab in the URL for reliable F5/re-login restoration.
- Use a shared form-saving state that disables the active submit action, shows `Saving…`, restores translated labels and blocks duplicate saves.
- Highlight/focus the actual invalid field for local validation problems in Logging JSON, listeners, Docker agents, TLS policy, ACL profiles, Manual config and certificate forms.
- Keep selected certificate/key files after failed import/save attempts.
- Show a subtle unsaved-change marker on the affected Settings tab and preserve dirty Manual config text across live refreshes.
- Update package, UI, OpenAPI, release checks and tooling identity to 0.19.3.
## What changed in 0.19.2
- Restore the local-listener risk checkbox for administration backends even when the administration listen socket is startup-owned/locked, including loopback equivalence between `localhost`, `127.0.0.0/8` and `::1` on the administration port.
- Make the vhost **Discard changes** action restore the persisted vhost instead of resetting the editor to a blank/default vhost.
- Use descriptive HTTP status labels in Settings -> Default responses, matching the 404-host editor.
- Let Live logs temporarily expand from the 256-entry live window to a continuously updated 4096-entry client buffer until the log window closes.
- Align inline checkbox/text controls and compact the Docker-vhost Aliases editor.
- Render idle backend traffic as `0.0 KiB/s`; positive sub-0.1 KiB/s activity renders as `<0.1 KiB/s` instead of a misleading zero.
- Replace native browser confirmation dialogs with RustProxy-styled confirmation modals and prefix API failures with the panel area that failed.
- Style sign-in failures inline under the login form, including `Incorrect username or password.` / its selected-language equivalent.
- Add `/opt/rustproxy/current` (or the selected prefix) to new login-shell `PATH` through `/etc/profile.d/rustproxy-path.sh` on install; managed update only verifies and warns about that profile.
- Keep placeholder/static/default pages strictly non-cacheable and suppress cache diagnostics/validators on those locally generated responses.
- Allow an existing vhost ID to be renamed with collision checks and an explicit warning about the new metric/log/cache identity and reset live health/traffic history.
- Update package, UI, OpenAPI, release checks and tooling identity to 0.19.2.
## What changed in 0.19.1
- Fix **Settings -> Default responses** saves by sending the standard section-update `value` payload and accepting the `pages` section in the administration API.
- Include the frontend HTTP/HTTPS scheme in the shared-cache primary key so scheme-dependent origin responses cannot cross protocols.
- Keep the live log stream at 256 rows while retaining up to 4096 completed requests for a one-shot **Load history** action; filters are applied before the requested history limit.
- Make backend traffic rates use KiB/s as the smallest display unit, using fractional KiB/s for small traffic instead of large raw B/s values.
- Polish Live Logs column selection, vertically align generic checkbox labels and place Docker-vhost Advanced switches beside Backend protocol on wide layouts; the alias editor no longer adds a redundant help line.
- Reduce stale simulated UI screenshots to two explicitly labelled legacy references and add a documentation index that identifies historical/version-labelled material.
- Restore the integration-test sources and independent Argon2id fixture required by the source/release checks so the 0.19.1 archive is self-contained for compiler-enabled `cargo test`.
- Update source, UI, API schema, release checks and package identity to 0.19.1.
## What changed in 0.19.0
- Harden managed updates against systemd `200/CHDIR` failures by repairing the managed data-root owner/mode and verifying that the saved service account can traverse it before the running service is stopped; inaccessible parent directories now fail early with an actionable error.
- Persist per-administrator **Live access logs** column visibility in SQLite. The preference changes only the dashboard table; filtered JSON export still contains every field present in each log entry.
- Render the Live Logs column picker with the same compact checkbox controls used elsewhere in the dashboard.
- Make backend `tls_verify=false` fully bypass X.509 certificate authentication/parsing callbacks used by webpki, restoring compatibility with legacy appliance certificates that fail with `UnsupportedCertVersion` while keeping TLS encryption and address/DNS/loop protections.
- Keep expanded upstream connect diagnostics with the nested Hyper/rustls/io error chain in Live Logs and the service journal.
- Remove the duplicate RustProxy version label from the top-right toolbar; the release version remains visible in the footer.
- Add SQLite schema v4 for administrator view preferences and update the administration API/release package identity to 0.19.0.
## What changed in 0.18.8
- Expand failed-upstream diagnostics so access/live logs preserve the nested Hyper/rustls/io error chain plus origin, effective TLS request origin, certificate-verification mode and elapsed connection time; connect failures are also emitted to the service journal at warning level, and Live access logs show the detail in a dedicated Upstream error column.
Follow-up fixes keep the four Docker-vhost per-app switches in one compact row and add regression coverage that generated Docker vhosts inherit the global `Server` response-header policy when they do not define an override.
- Replace the Docker-vhost alias CSV input with the same chip-style add/remove interaction used by hostname editors.
- Add per-Docker-vhost HTTP-to-HTTPS redirect, WebSocket and cache switches. New Docker assignments default the redirect on when the selected agent listeners can support it; existing pre-0.18.8 assignments keep their previous redirect-off behavior until saved with the new switch.
- Add an agent-level default ACL-profile selector next to the frontend certificate selector. Per-app ACL-profile selections still override the agent default, and inline per-app ACL rules remain unchanged.
- Remove the compression-codec explanatory sentence from the Overview dashboard.
- Update source, UI, API schema, release checks and package identity to 0.18.8.
## What changed in 0.18.7
- Keep **Response time history** collapsed by default while continuing to collect and update its live series.
- Make per-interface RX/TX history use the same 60 px plotting geometry, grid and RX/TX line treatment as aggregate network history, without nested background cards.
- Normalize expanded Docker-vhost routing and ACL field layout so text/select/checkbox controls share a stable label/control baseline and ACL selects retain normal control height.
- Update source, UI, release checks and package identity to 0.18.7.
## What changed in 0.18.6
- Add per-interface Linux network diagnostics from `/proc/net/dev`, with separate RX/TX history rows for every non-loopback interface in **Network and responses** while keeping Prometheus label cardinality unchanged.
- Strengthen compression telemetry verification with explicit quality-weight negotiation cases for gzip, Brotli, Zstd and deflate, and clarify in the UI that the counters represent the codec actually selected for each response.
- Polish **Settings -> Docker agents** field alignment/status controls and add a clear visual gap after listener selection before **Add Docker agent**.
- Update source, UI, EN/PL language packs, release checks and package identity to 0.18.6.
## What changed in 0.18.5
- Fix compression telemetry accounting by attaching an internal marker for the codec RustProxy actually applied; gzip, Brotli, Zstd and deflate now have independent counter regression coverage.
- Add an HTTP/1.1 request counter and show total/H1.1/H2/H3 request rates as separate colored series on the Overview chart.
- Add **Open app** to published Docker vhosts and show the primary Docker proxied domain beside generated pool IDs in **Backends**.
- Remove the page-level **Open proxy** shortcut.
- Make **Compact** density substantially tighter than **Dense** across dashboard cards, controls, tables and Docker/vhost rows.
- Give ordinary and Docker vhosts explicit proxy timeout defaults (10 s connect, 120 s response headers, 60 s read and 60 s send) so new/defaulted editors never start blank.
- Replace the Docker-agent Listener IDs text field with the shared named-listener selector, and keep Enabled immediately to the left of the centered connection-status badge.
- Update source, UI, EN/PL language packs, release checks and package identity to 0.18.5.
## What changed in 0.18.4
- Default new/missing backend health-check configuration to **TCP** instead of HTTP. Existing explicitly configured HTTP/off checks remain unchanged.
- Rework **Docker vhosts** into a summary-first view: each service keeps Publish, runtime health and visibility actions visible while route, ACL, timeout and health-check configuration stays collapsed until that service is edited. Agent diagnostics are collapsed behind **Agent details**.
- Rework **Settings -> Security** around compact account/session and host-policy sections, with password and policy editors collapsed until needed.
- Move **Administration audit** out of the Security page into a dedicated modal and load audit rows only when that modal is opened or refreshed.
- Retain the 0.18.3 HTTP/1 upstream title-case compatibility behavior and upstream-error diagnostics used for legacy/custom origin troubleshooting.
- Update source, UI, EN/PL language packs, release checks and package identity to 0.18.4.
## What changed in 0.18.3
- Add configurable per-vhost access-log files (`logging.file.per_vhost`) with a dedicated directory and `_unmatched.jsonl` fallback while preserving the combined JSONL mode.
- Treat explicitly supplied systemd/CLI/environment HTTP, HTTPS and administration binds as startup-owned: their addresses cannot be changed/removed in Settings, while non-startup listeners remain editable. Remote plain HTTP administration needs `--allow-remote-admin` but no second risk-acknowledgement flag.
- Make Compact density tighter, rename the cache settings surface to **Cache**, display Redirect targets on cards, and let 404 Hosts choose any final HTTP status from 200 to 599.
- Show the effective global TLS cipher list in Settings, mark ACME certificates as managed/read-only, require unassignment before certificate deletion (including administration/default usage), and default new ACME requests to Production.
- Keep WebSocket support on the nginx-compatible HTTP/1.1 Upgrade path by default. RFC 9220 WebSocket over HTTP/3 remains available only as an explicit experimental vhost option.
- Separate Docker discovery/vhost observability from ordinary vhost/backend Prometheus families. Docker assignments now export dedicated `rust_proxy_docker_vhost_*` and `rust_proxy_docker_backend_*` series instead of appearing under `rust_proxy_vhost_*` / `rust_proxy_backend_*`.
- Add an independent **Docker vhosts** collector switch under **Settings -> Prometheus -> Metric groups**, enabled by default and live-applied like the other collector switches.
- Export bounded Docker agent/assignment/domain inventory plus Docker-vhost traffic/status/duration and the full backend readiness/health/request/error/byte/latency/EWMA/failure/probe observations.
- Add regression coverage that verifies Docker pools never leak into ordinary vhost/backend Prometheus families.
- Update OpenAPI, Prometheus documentation, release checks and package identity to 0.18.3.
## What changed in 0.18.2
- Add optional Docker host discovery through a standalone Go agent in `docker-agent/`, with Bearer-token inventory access, systemd packaging helpers, multi-address host discovery and complete published-TCP binding discovery. The agent serves HTTPS by default with a persistent self-signed ECDSA certificate generated on first start.
- Pin the Docker agent certificate by SHA-256 in RustProxy instead of accepting arbitrary self-signed certificates. Public-CA agents remain supported without a pin; plaintext HTTP is an explicit compatibility mode only.
- Add **Settings -> Docker agents** and a main **Docker vhosts** page. Multiple agents each keep their own base domain (for example `docker1.lab.local` / `docker2.lab.local`), while each service can use a short name, an arbitrary FQDN and multiple aliases.
- Allow every Docker vhost to select a concrete detected Docker-host IP; wildcard bindings follow the selected IPv4/IPv6 address, while explicit Docker bindings retain their own host port. Agent-level default target IP remains optional.
- Reconcile enabled Docker assignments into ephemeral RustProxy vhosts without creating configuration revisions. Changed Docker published bindings/IP observations update routes on the next poll.
- Give Docker vhosts the same backend health engine used by ordinary vhosts. Per service, HTTP/TCP/off checks, path/Host/status/interval/timeout/rise/fall controls and live up/down/checking, latency, last-result, request/error/active diagnostics are available directly on the Docker vhosts page.
- Move the cache **Live** toggle to the right side of the cache action toolbar.
- Remove all dedicated `X-Rust-Proxy-Revision` response-header behavior. RustProxy no longer creates, strips, protects or migrates that header; if an upstream/operator provides it, it is treated like an ordinary response header. Internal configuration revision CAS remains unchanged.
- Replace production password-recovery guidance with `rust-proxy admin reset-password` and reorganize the Settings navigation for Docker-agent configuration.
- Update source, UI, OpenAPI, release tooling and package identity to 0.18.2.
## What changed in 0.17.0
- Add a complete `debian/` packaging workflow for a native `rustproxy` package: hardened systemd service, dedicated `rustproxy` user, `/var/lib/rustproxy` and `/var/log/rustproxy`, daily three-day `gzip -9` log rotation, package/repository build scripts and repository layout suitable for `https://rustproxy.linuxiarz.pl/`.
- Expand the Prometheus exporter to cover every field in the current bounded OS/process diagnostics view, plus GeoIP, certificate renewal, backend probe/EWMA/passive-failure, redirect/404-host, stream and structured-log diagnostics. Add an explicit trusted-CIDR option that can allow scraping without a bearer token while always using the real transport peer.
- Ship `grafana/rustproxy-overview.json` with traffic, vhost/backend, cache, memory, protocol, stream, logging, inventory and certificate panels.
- Add a dedicated **404 hosts** editor. A local-response host owns domains/listeners/certificates like a redirect/vhost but never opens an upstream; it can return the built-in placeholder, an empty status response or bounded custom HTML.
- Make the built-in `RustProxy is running` placeholder strictly `no-store` and strip optional RustProxy server, cache, backend, diagnostic, security and compression headers from that minimal response.
- Reuse the vhost-style hostname chip editor for Redirect source domains and keep the 0.16.x UX improvements intact.
- Update source, UI, OpenAPI, release tooling and package identity to 0.17.0.
## What changed in 0.16.1
- Keep the 0.16.0 proxy, cache, administration, H3 and editor behavior unchanged while polishing save feedback: successful configuration saves no longer expose the internal configuration revision number in modal/toast feedback. Revision CAS remains fully active internally for stale-write protection and is still available in operational status/diagnostics.
- Update source, UI, OpenAPI, release tooling and packaging identity to 0.16.1.
## What changed in 0.16.0
- Separate the main vhost backend from **Custom locations**. Custom locations are now overrides only; when none matches, routing always falls back to the vhost pool. An untouched legacy synthetic `root /` route is normalized away, while a deliberately customized `/` location remains a valid override.
- Fix publishing the administration panel through an explicitly configured RustProxy vhost. The reserved `rustproxy_session` cookie is still stripped for ordinary upstreams, but is preserved for a tightly guarded HTTPS self-admin route that directly targets the configured admin listener, has `accept_local_listener_risk=true`, and uses a hostname listed in `administration.allowed_hosts`.
- Harden RFC 9220 WebSocket setup: RustProxy now completes the backend HTTP/1.1 `OnUpgrade` before returning downstream HTTP/3 success, avoiding a `200` followed by an immediate H3 reset when backend takeover fails. Access logs gain HTTP version/transport, WebSocket phase/error, H3 reset and upstream-status fields.
- Split memory diagnostics into process RSS categories and RustProxy cache allocations. The panel now shows total RSS, anonymous/file/shared RSS, data/swap plus hot cache and transient fill/cold-queue/cold-I/O bytes. Existing allocator page release (`malloc_trim` on supported glibc/Linux paths) is retained; 0.16.0 does not add a new cache-memory throttle.
- Refine the Vhost/Redirect editor UX: new vhosts select standard HTTP `:80` and HTTPS `:443` listeners together when available, H2/H3 switches sit side by side, Redirects reuse the exact same listener selector, Authentication separates the IP access list from provider settings, TinyAuth advanced fields stay collapsed, and disabled vhost cache hides all subordinate options. New vhost drafts explicitly start with cache disabled.
- Rework backend cards and add opt-in origin diagnostics: `X-RustProxy-Backend` exposes the selected member ID and `X-RustProxy-Backend-Time` exposes origin response-header latency. Both headers are proxy-owned/protected and are omitted when a response is served without contacting an origin. The local-listener risk control appears only when the entered backend URL actually targets the configured RustProxy administration listener.
## What changed in 0.15.1
- Fix EN/PL language-pack placement for the 0.15 custom-location controls so documentation, match modes, rewrite controls and the visual header-rule builder render translated text instead of raw keys.
- Rework the visual header-rule editor into numbered rule cards with clearer operation/name/value fields, empty-state guidance and protected-header help.
- Add a live effective-behavior preview to every Custom location showing matching, proxy/local response, rewrite, cache, ACL and request/response header-rule counts before saving.
- Restructure the in-panel Custom locations documentation into matching, handling/policy and common-example sections.
- Add a combined live HIT/MISS/BYPASS activity chart based on per-sample counter deltas, while retaining the RAM-residency history.
- Add live process CPU and RSS charts to balance the dashboard process diagnostics card.
## What changed in 0.15.0
- **Custom locations** now support prefix, priority-prefix (`^~`), exact, case-sensitive regex and case-insensitive regex matching. Proxy locations can preserve, strip, replace or regex-rewrite the upstream path; local locations can return a configured status/body/content type without opening a backend connection.
- Location request/response header rules now have a visual builder plus inline documentation; the Advanced JSON representation remains available for copy/paste and automation. See [Custom locations](docs/CUSTOM_LOCATIONS.md).
- Add `nagios/` checks for admin API RTT, backend availability, backend latency and dropped-work counters. See [Nagios / Icinga checks](nagios/README.md).
- Move `Server-Timing` and verified Cloudflare client-IP diagnostics to the main vhost frontend/details page.
- Add live dashboard histories for RAM cache, network RX/TX and mean response latency. The compact footer also exposes measured administration API RTT and cold/log drop counters.
- Split cold-cache demotion into explicit idle age and maximum hot-tier residency age, while retaining independent LRU and shutdown policies.
- Live logs now include the effective source IP after trusted-proxy/verified Cloudflare processing; requests rejected before that processing still record the transport peer.
- Remove the runtime legacy renewal-observation JSON importer. Stopped source upgrades remove the obsolete observation sidecar as the final upgrade step without importing it or retaining a second legacy copy; the application itself uses SQLite only.
- Repeated literal slashes are normalized for route/ACL matching but preserved toward the backend unless a rewrite is selected, so requests such as `/static/js//events.js?v=bf9b5489` are accepted without allowing encoded separator or traversal ambiguity.
## What changed in 0.14.0
- Fix administration behind a trusted reverse proxy: the optional DNS allowlist now validates the browser-facing authority without forcing the internal admin port, while direct IP/`localhost` access remains tied to the real administration port and stays available. Trusted `X-Forwarded-Host`/`X-Forwarded-Proto` are used for same-origin checks and Secure session cookies.
- Implement RFC 9220 WebSocket bootstrapping on HTTP/3 with Extended CONNECT. RustProxy advertises `SETTINGS_ENABLE_CONNECT_PROTOCOL`, validates `:protocol=websocket`, translates the bootstrap to the backend HTTP/1.1 Upgrade handshake, returns downstream `200`, and bridges WebSocket bytes over H3 DATA with FIN/reset handling. Generic CONNECT and unsupported Extended CONNECT protocols remain rejected.
- Switch the HTTP/3 integration to `hpx-h3 2.5.19` with its Quinn transport feature so the protocol layer can represent RFC 9220 Extended CONNECT without a local parser bypass.
- Rename dashboard labels: Backend pools -> Backends, debug-origin preset -> recommended preset, health/restore tab -> Backend health, per-host response security -> Security, and Pages -> Error pages (with corresponding Polish labels).
- Bump source, UI, OpenAPI, release tooling and documentation to 0.14.0. No `certificates.json` metadata store is present; certificate state remains in the current SQLite/PEM paths, while the separate historical `certs/renewals.json` importer is retained only for renewal-observation migration.
## What changed in 0.13.2
- Fix direct dashboard refresh/session restoration: authenticated panel routes now validate the existing cookie before selecting the login or application shell, and restored browser-history login pages re-check the server session instead of showing a stale logged-out view.
- Preserve cache across managed software updates when the cold tier is enabled. RustProxy 0.13.2+ recognizes the updater's one-shot marker and forces a bounded hot-to-cold handoff even when ordinary `cold_on_shutdown` is disabled; RAM-only cache still cannot survive a process exit when cold storage is disabled.
- Persist non-secret automatic-certificate-renewal observations in SQLite and preload them before the administration listener starts. Existing `certs/renewals.json` observations are imported once, so **Check now** is no longer required after a restart just to repopulate last/next/expiry/result data.
- Move the raw full-document editor out of primary navigation into **Settings -> Manual config**. The old `/configuration` browser route remains as a compatibility alias that opens that Settings block.
- Add schema-v3 migration/release regressions, update OpenAPI/UI/docs and bump the source/package identity to 0.13.2.
## What changed in 0.13.1
- Extend administrator browser sessions from the old short 8-hour/30-minute-idle behavior to a configurable absolute **7–30 day** lifetime (default 30 days). The setting is live, session cookies are refreshed only up to the server-side absolute expiry, and password/revoke/logout invalidation is unchanged.
- Add an explicit per-backend `accept_local_listener_risk` switch. Together with `allow_private=true` where required, it can intentionally proxy to a local RustProxy listener such as `127.0.0.1:9090`; the default remains blocked to prevent loops/control-plane exposure. The Vhost editor requires an explicit risk confirmation.
- Update OpenAPI, English/Polish UI, security/transport documentation, regression contracts and release identity for 0.13.1.
## What changed in 0.13.0
- Add first-class **Redirects** beside Vhosts/Streams. Redirect hosts use named listeners and certificate assignments, support exact/prefix and HTTP/HTTPS matching, URL/host/path targets, 301/302/303/307/308, path append/strip/replace, query preserve/drop and explicit redirect cache lifetime.
- Redirects are resolved locally before proxy routing, never open an upstream connection, retain global host ACL enforcement and reject overlapping hostname/listener ownership with vhosts or other redirect hosts.
- Backend HTTPS certificate verification now defaults to **off** when `tls_verify` is omitted. Administrators can explicitly enable chain/identity verification per backend; Tinyauth and other verifier clients keep their independent secure defaults.
- TLS certificate cards now show DNS SAN coverage as compact vhost-style badges, keep verbose bindings/file/assignment data collapsed by default, and live-inspect older/manual public PEM files when `covered_hosts` metadata is missing.
- Update OpenAPI, English/Polish UI, regression contracts, documentation and release identity to 0.13.0.
## What changed in 0.12.1
- Split ordinary dashboard saves into revisioned `PUT /api/config/{section}` endpoints while retaining full-document PUT for intentional multi-section transactions. Cache invalidation is decoupled from the global configuration revision so unrelated control-plane saves no longer discard hot/cold objects.
- Add explicit browser routes for dashboard subpages and History API synchronization so refreshing `/vhosts`, `/cache`, `/tls` and the other primary panels returns the same guarded application shell instead of the landing page.
- Extend cold-cache policy with separate idle, LRU-pressure and graceful-shutdown persistence switches, reduce queued body retention, flush hot objects directly to cold storage during graceful shutdown, and release allocator pages after large demotions on glibc/Linux.
- Show certificate DNS SubjectAltName coverage separately from active SNI bindings for uploaded and ACME-managed certificates.
- In 0.12.1 backend TLS verification still defaulted on when `tls_verify` was omitted; 0.13.0 intentionally changes that default to off. The debug-origin preset already wrote an explicit `tls_verify=false`.
- Update OpenAPI, regressions, release tooling and source identity to 0.12.1.
## What changed in 0.12.0
- Promote TCP/UDP streams from Settings to the main navigation directly below Vhosts, with searchable stream cards and the same modal editing workflow used by vhosts.
- Add fixed-cardinality per-stream runtime telemetry for active connections/sessions, accepted/completed sessions, rejects, errors, bytes and UDP packets. The dashboard derives live RX/TX rates from those backend counters and keeps compact lifetime charts for the open panel session.
- Make restart-bound state explicit: desired stream configuration is shown separately from the active startup listeners, and changed/new/removed stream listeners remain marked pending until RustProxy restarts.
- Restyle named HTTP/HTTPS listener bind-address editing as address chips, matching the vhost hostname editor while preserving the existing 1–16 address validation and wildcard restrictions.
- Keep raw TCP/UDP forwarding outside HTTP ACL/cache/header/compression handling and retain bounded TCP connections / UDP client sessions.
- Bump the source release, panel, OpenAPI, upgrade tooling and package identity to 0.12.0 and extend offline regression checks for the Streams navigation/UI/telemetry contracts.
## What changed in 0.11.5
- Rework shared-cache identity around Varnish built-in semantics: the primary object hash is vhost scope + normalized Host authority + exact URL, while response `Vary` selects representation variants. Unrelated request headers, frontend scheme and the selected load-balanced backend member no longer fragment the cache.
- Treat `GET` and `HEAD` as the cacheable methods. A cacheable HEAD shares the GET object and a HEAD miss fetches/stores the GET representation; other methods pass to the backend without implicitly clearing unrelated cached objects.
- Fix `X-Cache-Resident-Age` continuity. Resident time now survives successful 304 revalidation and hot/cold demotion/promotion instead of resetting to zero when metadata is refreshed or a cold object returns to RAM.
- Bump cold-cache metadata to format 2 for the Vary and resident-age fields, add variant-schema safety checks, and extend cache regressions for Vary selection, key sharing, HEAD eligibility and resident age.
## What changed in 0.11.4
- Make Force SSL validation match redirect behavior: a selected plain HTTP listener is required, port 443 is preferred when several TLS ports are present, and ambiguous non-standard TLS targets get a clearer error.
- Replace the reusable ACL-profile free-form rule list with a compact visual rule builder plus an optional bulk import. Bulk parsing normalizes invisible Unicode spacing and reports the exact bad line instead of a generic syntax error.
- Suppress RustProxy cache diagnostic response headers entirely when the effective vhost cache is disabled, while keeping internal cache counters.
- In the dashboard backend-pool table, disabled vhosts are rendered muted and explicitly labeled Disabled instead of looking like failed backends.
## What changed in 0.11.3
- Add a per-vhost **Forward client IP to backend** switch. It defaults ON; when disabled, application requests omit `Forwarded`, `X-Forwarded-For` and `X-Real-IP` while RustProxy, ACLs, rate limits, GeoIP and Tinyauth still use the authenticated client address.
- Add a compact Vhost search box that filters by vhost ID, hostname/wildcard, backend ID and backend URL without changing configuration.
- Fix the release manifest to use the resolved `quinn 0.11.12` already present in `Cargo.lock`, and stop `dev.sh` from regenerating a valid application lockfile on every run.
- Make the managed systemd service writable at `/var/log/rustproxy` so the JSONL file sink works with `ProtectSystem=strict`; the service now creates/manages the log directory through systemd.
## What changed in 0.11.2
- Fix administration HTTPS over HTTP/2: the control-plane Host guard now accepts the protocol-native `:authority` value exposed by Hyper through the request URI instead of requiring a legacy `Host` field that browsers commonly omit on H2.
- Keep HTTP/1.x strict single-Host handling and reject absolute-form targets; on H2/H3, reject conflicting `Host` and `:authority` values rather than silently choosing one.
- Add regression tests for H2 authority handling and the real administration `/healthz` route so HTTPS administration works without forcing HTTP/1.1.
- Add per-vhost **Force SSL** directly below frontend selection. Plain HTTP receives a `308` to the same hostname/path/query on the selected HTTPS port; TLS listeners for a forced vhost must use one unambiguous port.
- Map Nginx Proxy Manager `ssl_forced` to RustProxy `force_ssl` during SQLite migration, retaining both HTTP and HTTPS listener bindings so the redirect actually works.
## What changed in 0.11.1
- Fix managed-update health checks for administration HTTPS and persisted administration bind changes. The deployment checker now follows `administration.listen`, `administration.tls` and the configured Host allowlist instead of always probing the install-time address over plain HTTP.
- Add `scripts/migrate_npm.py`, a read-first Nginx Proxy Manager SQLite migrator for proxy-host names/backends. It preserves WebSocket/enabled/H2 intent, maps NPM TLS hosts to RustProxy TLS using the application default certificate, reports unsupported NPM features for manual review, writes a JSON plan and requires explicit `--apply` before changing RustProxy.
- Add offline regression coverage for the NPM migration planner and persisted HTTPS administration health-target resolution.
## What changed in 0.11.0
- Add reusable ACL Profiles with country allow/block policy and IP/CIDR rules. Profiles can be attached to vhosts while existing per-vhost and route ACLs remain available and are enforced cumulatively.
- Improve hostname editing with chip-style input and leading `*.` wildcard domains. Exact hostnames take precedence, then the longest matching wildcard, so exact and wildcard vhosts can coexist on the same listener.
- Add compact per-vhost request-rate charts that retain the current browser panel session by progressively compacting older samples.
- Add the missing built-in HTTP 401 page and stop benign WebSocket EOF/reset/abort/broken-pipe closes after a successful upgrade from being reported as proxy body errors.
- Administration HTTPS now redirects plaintext requests on the HTTPS admin socket to the same authority/path over HTTPS. With no explicit admin hostname allowlist, valid DNS Host values are accepted; a configured allowlist remains strict.
- Named frontend listeners can bind the same policy to up to 16 explicit local addresses. Multi-address listeners reject wildcard `0.0.0.0`/`::` binds.
- Restyle the structured-log field builder to match the Vhost configuration modal and place its Configure fields action beside the preset selector.
- Source archives are now reproducible: sorted entries, deterministic timestamps/permissions and `SOURCE_DATE_EPOCH` support are verified by the release smoke test.
## What changed in 0.10.0
- Cache now has two bounded tiers: hot RAM plus optional persistent cold disk storage. LRU/idle hot objects are demoted asynchronously; fresh cold hits are integrity-checked and promoted back to RAM.
- Cold cache settings expose the relative data path, disk/object limits and idle demotion delay. The Cache panel reports hot/cold objects, RAM/disk bytes, promotions, demotions, cold hits and errors.
- Cache diagnostics now distinguish browser hard-reload revalidation (`request_no_cache`, `request_max_age`, `request_pragma_no_cache`) from true cache misses. Normal browser cache can satisfy navigation without sending a request to RustProxy; use repeated `curl` GETs for proxy-level HIT testing.
- Logging format configuration adds Standard, Nginx-style, ECS-style and Minimal presets plus a graphical field picker/renamer with JSON preview. Advanced raw JSON mapping remains available.
- Logging sink cards reuse the shared square application card styling instead of release-specific rounded fieldset CSS.
- A baseline multi-stage Docker/OCI image is included. It runs as an unprivileged user, persists `/var/lib/rustproxy` and keeps administration loopback by default. Docker discovery uses the separate `docker-agent/`; the RustProxy container itself never needs the Docker socket.
## What changed in 0.9.0
- Vhosts use readable cards with listener/protocol/TLS/cache identity and live backend health, request, active, error, latency and traffic metrics.
- Structured access logging can write configurable JSON/JSONL to one combined file or separate files per vhost, plus Logstash TCP and Loki HTTP(S), from a bounded asynchronous queue; sink delivery/drop/error counters are visible in the panel.
- Cache stored-object inspection reports body bytes, resident bytes and metadata overhead, with optional `X-Cache-Resident-Age`.
- Settings adds full-width Administration and TLS policy pages. The panel can run on HTTPS using the application-managed certificate or a selected certificate; a non-loopback bind is allowed when HTTPS is enabled.
- TLS 1.2/1.3 and rustls cipher suites are configurable globally and per vhost for TCP TLS. HTTP/3 remains tied to the global TLS 1.3 policy.
- Logging is a dedicated navigation page; Settings tabs fill the available width.
- Roadmap items completed here include remote HTTPS administration and `X-Cache-Resident-Age`.
## What changed in 0.8.1
- Cache is enabled by default.
- Browser validators no longer cause blanket BYPASS: fresh `If-None-Match` / `If-Modified-Since` requests can be answered as local conditional HITs, including `304 Not Modified`.
- Request `no-cache`, `max-age` freshness constraints, `min-fresh` and legacy `Pragma: no-cache` use validator-based backend revalidation when a safe stored validator exists; backend `304` refreshes metadata and reuses the cached body.
- Request `no-store` remains a hard BYPASS. Response `Cache-Control: no-cache` may be stored when it has a usable validator, but is validated before every reuse; response `no-store`/`private` remain uncacheable.
- `cache.max_object_bytes` now permits values up to 1 GiB (still bounded by `cache.max_bytes`). Large fills grow incrementally and consume more fill permits instead of preallocating the configured object ceiling.
- Cache diagnostics add `cache_conditional_hit`, `cache_revalidated`, `request_no_cache`, `request_no_store` and related revalidation reasons.
- Release identity, tests, panel limits and cache documentation are updated to 0.8.1.
## What changed in 0.8.0
- Per-vhost rate limiting plus country-specific limits and country block/allow-only rules, with built-in 429/403 pages.
- GeoIP country data is downloaded on first install from `sapics/ip-location-db` and verified against publisher SHA-256 files. Each managed update compares local files with the current publisher checksums and downloads only stale/missing assets.
- Optional Cloudflare real-client-IP forwarding per vhost, trusted only from Cloudflare edge CIDRs.
- Per-vhost HTTP/2 and HTTP/3 opt-outs live next to named frontend selection. Listener IP/ports and protocol capabilities stay in Settings; H2 ALPN is narrowed by SNI for vhosts that disable it.
- Health-check mode `off` now disables both active probes and passive health classification.
- New backend rows use the HTTPS-by-IP debugging defaults requested for preserved Host routing: default pool ON, private origin ON, HTTP/1.1, preserve Host ON and backend TLS verification OFF. The panel warns about this insecure TLS choice and about incomplete/mismatched debug settings. Existing backend values are not silently rewritten.
- Static-cookie cache boost is now visible globally and per vhost. The global defaults include common images, CSS/JS/WASM, fonts, audio/video and static downloads; matching assets ignore request Cookie while `Set-Cookie`, `private`/`no-store` and `Vary: Cookie` still forbid shared storage.
- Cache `Age` now follows corrected received age plus response delay without rounding sub-second delay up to a full second.
- Cargo, panel, OpenAPI, packaging and source-update metadata are 0.8.0.
The 0.7.2 Swagger build fix remains in place: the pinned UI assets are prepared by
this project and embedded at build time without a mandatory Cargo environment setting.
The [0.7.1 source audit](docs/SECURITY_AUDIT.md) remains historical and is not a new
audit or proof of live security in 0.8.0.
## Existing 0.7.0 foundation
Managed certificates imported into the library renew automatically unless disabled.
Initial issuance/import is still explicit and staging-first; imported PEM alone is
not auto-managed. Existing 0.6 issuer metadata enables renewal by default, so review
managed certificates before upgrade. Nothing silently switches from staging to the
production CA. See [renewal and recovery](docs/RENEWAL.md).
Read [installation and update](docs/INSTALL.md), [host access and locations](docs/VHOST_ACCESS.md),
[migration](docs/MIGRATION.md), [NPM migration](docs/NPM_MIGRATION.md), [ACME providers](docs/ACME_PROVIDERS.md),
[Prometheus](docs/PROMETHEUS.md), [cache diagnosis](docs/CACHE_DIAGNOSTICS.md),
[HTTP/3 scope](docs/HTTP3.md), [TCP/UDP streams](docs/STREAMS.md) and [container deployment](docs/CONTAINER.md). Cloudflare Tunnel remains deferred. Docker discovery is available through the standalone agent; see [Docker discovery](docs/DOCKER_DISCOVERY.md).
Examples are disposable full configurations: applying one replaces, not merges,
an existing document.
## Requirements and first build
### Debian 13+ one-line install/update
The bootstrap installs the compiler/build dependencies and Rust, clones this repository and
builds the locked release. With no argument it runs in `auto` mode: a host without a managed
RustProxy installation is installed, while an existing `/opt/rustproxy/install.json` triggers
the safe `update.sh` path with backup, health check and rollback. First install starts HTTP on
`0.0.0.0:80`, HTTPS on `0.0.0.0:443` and the administration panel on plain HTTP
`0.0.0.0:9090` with the explicit `--allow-remote-admin` gate. Updates preserve the saved listener and
service settings. Restrict TCP/9090 with a firewall/private network.
```bash
# automatic: install on a new host, update an existing managed installation
wget -qO- https://git.linuxiarz.pl/gru/RustProxy/raw/branch/master/install-debian13.sh | bash
# explicit update
wget -qO- https://git.linuxiarz.pl/gru/RustProxy/raw/branch/master/install-debian13.sh | bash -s -- update
```
Manual build prerequisites used by that bootstrap:
```bash
apt install -y build-essential gcc git ca-certificates python3 pkg-config wget sudo python3-certbot-dns-rfc2136 python3-certbot-dns-cloudflare
wget -qO- --https-only --secure-protocol=TLSv1_2 https://sh.rustup.rs | sh
```
Rust stable (declared minimum 1.88), Cargo, C compiler/linker, Bash and Python 3.10+.
Linux is the primary deployment target;
macOS/WSL2 need normal native build tools. Detailed OS diagnostics currently use
Linux procfs and cgroup v2. Unsupported measurements remain null/N/A.
The first build needs access to the Rust toolchain and crate registry. `dev.sh`
never installs system software or runs sudo. Rustls handles runtime TLS; zstd's
native dependency still needs a C build toolchain.
The Debian bootstrap intentionally builds with one Cargo job, LTO disabled and
`codegen-units=8`. This avoids `rustc` being killed by the OOM killer on small VPSes
while leaving the repository's normal release profile unchanged. For a manual
low-memory build use:
```bash
CARGO_BUILD_JOBS=1 CARGO_PROFILE_RELEASE_LTO=false \
CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 cargo build --release --locked
```
The 0.22.1 source archive preserves the resolved `Cargo.lock` supplied with the source tree and
updates only this package's own version entry. This packaging environment has no Cargo, so the
dependency graph was not re-resolved here. On the compiler-enabled deployment host, run the
locked check/test/release gates below and retain the verified lockfile. `dev.sh` can generate a
lockfile only when one is genuinely absent; it does not replace normal locked verification.
```bash
cd RustProxy
bash dev.sh doctor
bash dev.sh check
bash dev.sh test
bash dev.sh
```
| Service | Development default |
|---|---|
| Default HTTP | `http://127.0.0.1:8080` |
| Default HTTPS | `https://localhost:8443` |
| Administration (default) | `http://127.0.0.1:9090`; HTTPS/remote bind is opt-in in Settings |
| Optional protocol fixture | `127.0.0.1:9000`, only with `RUST_PROXY_DEMO=1` |
In a second terminal, print the one-time setup capability and use the browser,
or initialize an administrator locally:
```bash
bash dev.sh admin setup-token
# Alternative: interactive account creation, no default credentials.
bash dev.sh admin init
```
There is no default username/password. Normal startup serves a neutral informational
page for an unconfigured address and does not seed sample vhosts. The self-signed
bootstrap certificate is retained under `data/certs` and is not publicly trusted.
Assign intended certificates explicitly. Public frontend TLS does not implicitly expose the separate administration listener. Administration remains loopback HTTP by default; enable its own HTTPS setting before choosing a remote bind. Existing databases are never reset by this default.
Use the protocol fixture only explicitly when running the documented test suites;
`RUST_PROXY_DEMO=1 bash dev.sh` seeds a new disposable database. The separate
`bash dev.sh mock` helper remains available for cache/backend testing.
For a system service, build as an ordinary user and follow the complete procedure
in [INSTALL.md](docs/INSTALL.md):
```bash
bash dev.sh release
bash install.sh --plan --binary target/release/rust-proxy
sudo bash install.sh --binary target/release/rust-proxy
# A subsequent release:
sudo bash update.sh --binary target/release/rust-proxy
```
Copy `.env.example` to `.env` for startup defaults; it is trusted shell code.
`RUST_PROXY_DEMO=0` disables the demo. `RUST_PROXY_TLS=0` removes the startup TLS
default used when bootstrapping listeners.
The release checker reads its compile-time defaults from repository-root `UPDATE_CHECK.toml`,
which is embedded in the final binary. It checks the configured `Cargo.toml` URL immediately
on startup and then every 3600 seconds by default. A failed request is ignored until the next
interval; it never blocks startup or proxy traffic and it never installs updates. Operators may
override the embedded values with global CLI options or equivalent environment variables:
```text
--update-repository-url RUST_PROXY_UPDATE_REPOSITORY_URL
--update-version-url RUST_PROXY_UPDATE_VERSION_URL
--update-check-interval-secs RUST_PROXY_UPDATE_CHECK_INTERVAL_SECS
```
Set the interval to `0` to disable automatic checks. Debian/systemd installs may place the
environment overrides in `/etc/rustproxy/environment`; source/development runs may use the
normal process environment.
Persisted frontend sockets live only in
`listeners.endpoints` and are edited under Settings -> Listeners. Listener address/TLS/H2/H3
capability changes need a process restart; vhost H2/H3 switches only narrow those
capabilities and routing/pool policies otherwise use validated hot publication.
## Private vhosts and backend pools
Use **Settings -> Listeners** to configure frontend sockets, then use **Vhosts** to
select those named listeners and configure backend members. `examples/vhosts-private.json`
binds its named listener only to `127.0.0.2` and uses two
explicit loopback origins. Replace these with addresses assigned to your machine
and your real services. `examples/vhosts-balanced.json` shows an opt-in public
80/443 deployment; do not apply it unintentionally.
A private vhost is selected by hostname **and the accepted socket's actual local
IP, port and TLS state**. Sending its Host/X-Forwarded-* values to a different IP
does not select it. A shared wildcard socket may accept TCP at multiple addresses,
but each vhost keeps its original routing constraints. IP binding is not a firewall
or authentication: NAT/another proxy can still forward traffic to that private IP.
Keep network restrictions and application authentication.
New health-checked members remain in `checking` until the configured successes
make them eligible. No healthy member means 503; a healthy backup may be selected.
HTTP checks measure time to response headers, not whole-body throughput. Affinity
cookies bypass shared cache; they contain only an opaque routing token and are not
login sessions. Existing cookie sessions may finish on a draining healthy member.
The keepalive controls are connection reuse, **not Keepalived/VRRP**.
Save pool/routing/security changes live. New/removed sockets and TLS ALPN changes
need a process restart; active vs desired endpoints are visible in Settings/API.
Old top-level routes are converted once into vhosts during validated startup/import.
There is no separate legacy routing UI or dispatcher; path-to-origin ownership is
retained. Read the migration guide before updating an existing database. See [VHOSTS.md](docs/VHOSTS.md)
for algorithms, defaults, bounds, caveats and the complete configuration contract.
## Standard ports without running as root
Settings -> Listeners accepts multiple named endpoints and socket addresses such as `0.0.0.0:80`,
`0.0.0.0:443`, `[::]:443` and custom ports. Presets change only the port: they do
not silently replace a loopback interface with a public one. The admin listener is independent of these controls and stays loopback by default. In Settings -> Administration, enabling HTTPS permits a persisted non-loopback bind using either the application-managed certificate or a selected certificate. Plain remote HTTP remains available only with the explicit `--allow-remote-admin` startup flag.
Linux low-port binding can require `CAP_NET_BIND_SERVICE`. Use a dedicated service
account and the example in [deploy/README.md](deploy/README.md), not a root-run
application. Port conflicts and OS permissions cannot be proved by merely saving
configuration; bind failures are reported on restart. See
[settings, defaults and rollback](docs/SETTINGS.md).
## Cache and response controls
The shared cache is enabled by default. Normal fresh `200 GET` and cacheable HEAD
requests use `s-maxage`, `max-age` or `Expires`; a configurable default TTL applies only
to explicit `public` responses without an origin freshness lifetime and defaults to
zero. GET/HEAD share the same frontend scheme + Host + exact URL object, response `Vary` selects
representation variants, and unrelated request headers or the selected backend-pool
member do not fragment that object. Since 0.19.1 HTTP and HTTPS use distinct primary
cache identities so scheme-dependent origin responses cannot cross protocols. Non-GET/HEAD methods pass without globally
purging unrelated cached objects.
Cookies (except the configured static-asset exception), authorization, ranges,
`If-Match`/`If-Unmodified-Since`, private/no-store responses, Set-Cookie, `Vary: *`,
streaming events and failed/incomplete bodies are not reused. `If-None-Match` and
`If-Modified-Since` can be answered from a fresh cache entry as local conditional HITs.
Request `no-cache`/freshness constraints revalidate a stored validator instead of
blanket-bypassing the cache. Response `no-cache` with a usable validator may be retained,
but must validate before every reuse. Request/response `no-store` remains a hard exclusion.
Default cache memory limits are 256 MiB HOT allocated/charged data, 16 MiB per object, 64 MiB transient RAM, a 32 MiB COLD writer queue, 4096 HOT entries and eight fill permits. The panel also offers memory-aware conservative/recommended/performance presets derived from the lower of detected host RAM and a finite cgroup memory limit; automatic presets are disabled below 256 MiB effective RAM. `max_object_bytes` remains an independent per-object ceiling and cannot exceed the global HOT budget. If it is configured above the transient budget, the panel warns that objects near that size will pass through uncached because a fill cannot reserve enough bounded transient RAM. Optional cold cache defaults to disabled; when
enabled it uses a bounded disk path under the private data directory and asynchronously
demotes LRU/idle hot objects. Cold objects survive process restart for the same
configuration revision and are promoted to RAM on a fresh hit. Large fill buffers grow
incrementally and consume multiple permits. Actual process RSS also includes in-flight
responses, compression buffers and runtime allocations. HIT/MISS/BYPASS, hit count and
standardized RFC 9211 `Cache-Status` diagnostics are independently optional; required Age remains on cache hits. Optional `X-Cache-Resident-Age` tracks RustProxy residency and now survives revalidation/tier movement.
Purge is authenticated, CSRF-protected, audited and invalidates both tiers plus older
fills. See [cache behavior and limits](docs/CACHE.md) and
[cache diagnostics](docs/CACHE_DIAGNOSTICS.md).
Response compression is streamed and negotiated with Accept-Encoding; enabled
algorithms are gzip, Brotli, Zstandard and deflate. Privacy-sensitive, ranged,
upgraded, pre-encoded and no-transform responses are excluded. Strong ETags are
weakened after transformation, and obsolete content digests are removed. Server
policy is applied after generic response rules. Protected framing/identity headers
cannot be edited through generic rules.
With the separate demo enabled, `/cache-demo` returns an explicitly public 30-second
object. Enable cache and X-Cache in Settings, then issue this twice without cookies:
```bash
curl -i -H 'Host: localhost' http://127.0.0.1:8080/cache-demo
```
The first eligible request is MISS, then HIT while fresh. The origin timestamp
stays unchanged on HIT. Unlike proxy/system measurements, this endpoint is explicitly
demo content. The reserved admin cookie is removed before eligibility; application cookies still
trigger privacy bypass. Use anonymous curl requests to check cache behavior.
## Adding a language
Create `langs/de_DE.json` with metadata and string translations, then rebuild and
restart. No JavaScript language switch or registry edits are needed:
```json
{
"meta": {
"locale": "de-DE", "name": "German", "native_name": "Deutsch", "direction": "ltr"
},
"translations": { "settings": "Einstellungen", "overview": "Ubersicht" }
}
```
Missing keys fall back to `en_US.json`. Translation strings are rendered as text,
not HTML; locale and direction metadata also control formatting/RTL. The two
bundled languages are English and Polish. New packs are compile-time assets, not
untrusted runtime plugins. See [langs/README.md](langs/README.md).
## Updating an installed source tree
Read [MIGRATION.md](docs/MIGRATION.md). Back up actual data/certificates, stop the
old server and extract this archive into a separate, non-nested directory. From
the new source directory:
```bash
python3 scripts/backup.py --data-dir /actual/RustProxy/data /private/backup-before-0.13.2
bash upgrade.sh /actual/RustProxy --stopped
# Also pass --data-dir /actual/private/data when it is external.
cd /actual/RustProxy
bash dev.sh test
bash dev.sh
```
`.env`, data, unmanaged files and previous managed sources are preserved. Extra
language JSON files survive the update; shipped files replace their old names,
whose previous bytes remain in the backup. Unknown custom source edits are not
merged. Do not overlay individual source files onto an old tree.
RustProxy 0.13.2 advances SQLite to schema version 3 by adding the non-secret
`certificate_renewals` observation table; configuration-document fields still use defaults
when omitted. Once a 0.13.2 database/document is saved, an older binary may reject the
newer schema or unknown fields. Roll back
using the pre-upgrade data backup as well as the old source, not just the binary.
## Documentation and checks
[documentation index](docs/README.md), [API](docs/API.md), [architecture](docs/ARCHITECTURE.md),
[security](docs/SECURITY.md), [transport](docs/TRANSPORT.md),
[vhosts and pools](docs/VHOSTS.md), [settings](docs/SETTINGS.md), [cache](docs/CACHE.md),
[roadmap](ROADMAP.md), [changelog](CHANGELOG.md), [verification](docs/TEST_REPORT.md).
```bash
# Rust-independent checks:
python3 scripts/check_source.py
python3 scripts/release_smoke.py
# Runs those checks first, then the Rust suite when Cargo is available:
bash dev.sh test
# Source-only package, with no Git checkout required:
python3 make_zip.py /tmp/RustProxy-0.22.1.zip
```
## New management and test-origin guide
See [MANAGEMENT.md](docs/MANAGEMENT.md) for the certificate staging workflow,
page/status behavior and logs; [MOCK_SERVER.md](docs/MOCK_SERVER.md) provides a
ready-to-use cache/health test setup. The example `examples/mock-vhost.json` is a
complete minimal document for a fresh test installation, not a merge patch.
## Authentication and mock development
In **Vhosts -> Authentication**, enable Tinyauth and supply its base URL (or the
complete `/api/auth/nginx` endpoint) and public login URL. Private verifiers need
the explicit private-network permission; optionally constrain allowed networks.
Default identity headers are Remote-User and Remote-Email. No embedded provider
password or disabled TLS verification is required. Do not protect the login host
with itself. See INTEGRATIONS_040.md for exact status/redirect behavior and limits.
```bash
# Separate terminals, after building the real proxy on a toolchain-enabled host:
bash dev.sh mock --port 9001 --name origin-a --ttl 30
bash dev.sh mock-auth --port 9300
```
The fake-auth server accepts the intentionally insecure cookie `mock_auth=allow`;
it is a loopback-only development peer, never an authentication product.
### Docker vhosts operational controls
Docker vhosts now publish/unpublish immediately per service. Each Docker agent has a configurable inventory polling interval, while the optional Live view refreshes cached runtime state only. Docker-backed applications can use ACL profiles/inline ACLs and per-vhost proxy connect/header/read/send timeouts, matching the ordinary vhost proxy controls.
+658
View File
@@ -0,0 +1,658 @@
# RustProxy roadmap
## 0.24.5
Current direction: 0.24.4 -> 0.24.5
- [x] Resolve unknown-length, actually empty HTTP/3 bodies before HTTP/1.1 upstream framing.
- [x] Preserve streaming for non-empty H3 bodies and declared `Content-Length` requests.
- [x] Add source-level regression coverage for DELETE and other body-capable methods.
- [x] Bound HOT by allocation-aware admission, enforce `max_object_bytes` during fill, and add a separately bounded transient/COLD-queue memory model.
- [x] Preserve HOT -> COLD ownership semantics while charging demoted payloads until persistence drops them; expose logical/allocated/transient telemetry.
- [x] Make global cache disable authoritative over vhost overrides and remove the artificial 1 GiB HOT/object ceiling.
- [x] Add host/cgroup-aware conservative/recommended/performance cache presets and non-blocking memory/headroom warnings in Settings.
- [x] Update source/package/UI/OpenAPI identity and offline release checks to 0.24.5.
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, the H1/H2/H3 method matrix, and cache RSS/HOT/COLD stress acceptance on the release host.
## 0.24.4
Current direction: 0.24.3 -> 0.24.4
- [x] Make the Linux THP mitigation process-local with `PR_SET_THP_DISABLE` before Tokio worker creation.
- [x] Preserve the existing glibc arena limit and allocator-trim behavior.
- [x] Add source/release regression coverage for THP opt-out ordering and release identity.
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, release build and a sustained production RSS comparison with host THP returned to its normal policy.
## 0.24.3
Current direction: 0.24.2 -> 0.24.3
- [x] Reduce Linux/glibc arena retention by adding `MALLOC_ARENA_MAX=2` to managed systemd units.
- [x] Remove redundant identity-body and JavaScript/CSS transformation allocations; release successful-transform temporaries earlier and trim large glibc transform churn at a bounded cadence.
- [x] Move HTTP/3 enablement to listener editing while keeping HTTP/3 tuning under HTTP connections.
- [x] Rename Proxy Process to Process and hide unsupported platform diagnostics dynamically.
- [x] Remove the stale unreferenced per-BSD parser source from the release archive.
- [x] Update source/package/UI/OpenAPI identity and Rust-independent release gates to 0.24.3.
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, release build and sustained cache/transform RSS acceptance on the release host.
## 0.24.2
Current direction: 0.24.1 -> 0.24.2
- [x] Bump package/source/UI/OpenAPI release identity to 0.24.2 without functional changes.
## 0.24.1
Current direction: 0.24.0 -> 0.24.1
- [x] Replace the duplicated BSD statistics implementation with shared cross-platform library APIs.
- [x] Preserve Linux-specific `/proc`/cgroup diagnostics as additive compatibility fields.
- [x] Populate additional portable system/process metrics exposed by the selected libraries where available.
- [x] Expose low-risk runtime metadata through the authenticated status API, including the resolved data/database path.
- [x] Make CLI installed-data discovery use Linux and BSD/OPNsense conventions without hardcoding the path in the browser.
- [x] Move Logging into Settings and retain legacy `/logging` navigation compatibility.
- [x] Fix the release-host compiler error in portable uptime sampling and remove the stale H2 import warning.
- [x] Remove artificial hostname-count ceilings from certificate bindings/issuance and hostname-list configuration while retaining syntax, payload and resource-safety bounds.
- [x] Update source/package/UI/OpenAPI identity and source-side regression contracts to 0.24.1.
- [ ] Generate and commit a resolver-produced `Cargo.lock`, then run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on the release host.
- [ ] Run native FreeBSD/OpenBSD/NetBSD diagnostics acceptance and final H2/H3/WebSocket/proxy-statistics regression.
See [the 0.24.1 specification audit](docs/V0.24.1_AUDIT.md).
## 0.24.0
Current direction: 0.23.3 -> 0.24.0
- [x] Graceful H2 idle retirement with activity/stream recheck, GOAWAY and bounded drain.
- [x] Independent H2 maximum connection lifetime and drain timeout settings.
- [x] Settings API/UI/OpenAPI contracts for all three H2 lifecycle limits.
- [x] Extract system statistics into platform backends without changing Linux metric semantics.
- [x] Add FreeBSD/OpenBSD/NetBSD native best-effort sampling and explicit unsupported/null metrics.
- [x] Surface platform in Proxy Process and Prometheus metadata.
- [x] Update source/package/UI identity and source-side regression contracts to 0.24.0.
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on the release host.
See [the 0.24.0 specification audit](docs/V0.24.0_AUDIT.md).
## 0.23.3
Current direction: 0.23.2 -> 0.23.3
- [x] Add opt-in per-vhost legacy WebSocket compatibility without changing ordinary HTTP behavior.
- [x] Force legacy-mode upstream WebSocket handshakes to HTTP/1.1 and force legacy WebSocket `Host` to the configured backend authority and align `Origin` with it.
- [x] Tolerate old `101` response framing while keeping the WebSocket accept proof and subprotocol validation strict.
- [x] Add dashboard/OpenAPI/language-pack controls and source regressions.
- [x] Update package/source identity and Rust-independent release gates to 0.23.3.
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, release build and live iLO4/legacy WebSocket acceptance on the release host.
See [the 0.23.3 specification audit](docs/V0.23.3_AUDIT.md).
## 0.23.2
Current direction: 0.23.1 -> 0.23.2
- [x] Let Cache and Transformation live charts retain 60, 120, 300 or 600 samples without changing the 3-second polling cadence.
- [x] Persist each chart history-window choice locally in the browser and keep history memory bounded.
- [x] Fix local CLI administration transport detection so HTTPS administration no longer traps `--livelogs` in a 308 reconnect loop.
- [x] Pin local CLI HTTPS to certificates from the private RustProxy certificate directory and reuse the transport for config/TLS CLI calls.
- [x] Update package/source identity and Rust-independent release gates to 0.23.2.
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, release build and live HTTPS `--livelogs` acceptance on the release host.
See [the 0.23.2 specification audit](docs/V0.23.2_AUDIT.md).
## 0.23.1
Current direction: 0.23.0 -> 0.23.1
- [x] Add default-collapsed, full-width live activity charts to the Cache and Transform workspaces.
- [x] Plot selected-scope Cache HIT/MISS/BYPASS rates from runtime counters without adding proxy-path work.
- [x] Plot selected-scope Transform applied/cache-HIT/skipped/error rates from the existing runtime metrics endpoint.
- [x] Reuse the dashboard SVG/chart styling, live 3-second refresh and language-pack model.
- [x] Update package/source identity and Rust-independent release gates to 0.23.1.
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, release build and browser acceptance with sustained Cache/Transform traffic on the release host.
See [the 0.23.1 specification audit](docs/V0.23.1_AUDIT.md).
## 0.23.0
Current direction: 0.22.3 -> 0.23.0
- [x] Add scoped external certificate API authentication with Key ID + Bearer Token, hash-only storage, rotation/revoke/status and last-used metadata.
- [x] Restrict the external credential to certificate deploy/reload and require HTTPS outside loopback.
- [x] Add Settings → Certificate API UX and preserve direct production binary commands for local deploy/reload.
- [x] Add `rust-proxy --livelogs` with a bounded live stream, interactive structured filters, detailed request inspection and explicit lag reporting.
- [x] Split Settings → Cache into General, Resilience, Cold cache and Static & diagnostics sub-tabs without changing the atomic cache settings document.
- [ ] Run compiler-enabled `rust-proxy`/Cargo check, tests and release build plus live token-auth certificate rotation and sustained high-rate live-log acceptance on the release host.
- [ ] Upstream mTLS/client certificate remains deliberately deferred.
See [the 0.23.0 specification audit](docs/V0.23.0_AUDIT.md).
## 0.22.3
Current direction: 0.22.2 -> 0.22.3
- [x] Add HTTP/1.1 `Upgrade: h2c` on opt-in cleartext H2 listeners while preserving prior-knowledge h2c.
- [x] Validate Upgrade/HTTP2-Settings and reject malformed or body-bearing bootstrap requests before `101`.
- [x] Reconstruct the initiating H1 request as HTTP/2 stream 1 and retain the normal Hyper H2 path for later streams.
- [x] Add dedicated certificate hot reload without mutating desired configuration or revision.
- [x] Add external certificate deployment for Certbot/manual issuers with immutable generation staging and atomic multi-vhost switch by certificate id.
- [x] Add API/CLI/panel support plus audit/OpenAPI/documentation for certificate reload/deploy.
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, release build and live H1 -> h2c Upgrade -> H2 plus TLS/H2/H3 certificate-rotation acceptance on the release host.
- [ ] Upstream mTLS/client certificate remains deliberately deferred.
See [the 0.22.3 specification audit](docs/V0.22.3_AUDIT.md).
## 0.22.2
Current direction: 0.22.1 -> 0.22.2
- [x] Add replay-safe failover retry with bounded policy and observability.
- [x] Correct explicit HTTP/2 upstream request versioning and retain h2c prior-knowledge regression coverage.
- [x] Add per-backend maximum concurrent proxy exchanges with bounded queue timeout / 503 behavior and metrics.
- [x] Add detailed upstream failure classification plus last failure and health ejection/recovery timestamps.
- [x] Consolidate backend operations data in the existing dashboard table: health, active/limit, requests/errors, latency, traffic, last failure and last recovery.
- [x] Parse and expose public-certificate issuer/SAN/validity/remaining-days/expiry status plus renewal status/last attempt/error.
- [ ] Add a distinct maximum TCP connection/socket limit per backend; request concurrency is implemented but is intentionally not presented as a socket limit.
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test` and live H1/H2/H3 plus H2 -> proxy -> H2 acceptance on the release host.
See [the 0.22.2 specification audit](docs/V0.22.2_AUDIT.md).
## 0.22.1
Current direction: 0.22.0 -> 0.22.1
- [x] Move selective PURGE/BAN and active BAN management into dedicated Cache modals with inline operation feedback.
- [x] Add per-vhost RFC 8441 HTTP/2 WebSocket Extended CONNECT beside RFC 9220.
- [x] Bridge H2 RFC 8441 WebSockets to H1 upstreams and preserve Extended CONNECT to explicit H2 upstreams.
- [x] Add opt-in cleartext HTTP/2 prior-knowledge (`h2c`) listeners.
- [x] Add stable active-tunnel inventory, per-tunnel kill, operator reason and audit logging.
- [x] Split WebSocket tunnel statistics by H1 / H2 RFC 8441 / H3 RFC 9220 on the main dashboard and Prometheus.
- [x] Preserve the 0.22.0 cache feature set and `Accept-Ranges` policy.
- [x] Update 0.22.1 source/package identity and Rust-independent release gates.
- [ ] Run the compiler-enabled `./dev.sh check` / `./dev.sh test` gate plus live H1/H2/H3/h2c/RFC8441 acceptance on the release host.
See [the 0.22.1 specification audit](docs/V0.22.1_AUDIT.md).
## 0.21.0
Current direction: 0.20.0 -> 0.21.0
- [x] Shard Cold Cache storage and add bounded asynchronous GC plus selective cleanup.
- [x] Add RFC 5861 stale windows, request `max-stale`, correct request `max-age=N`, and correct revalidation-directive semantics.
- [x] Add the extensible decode -> transform -> canonical cache -> encode stage with transformed lengths/validators, strict `no-transform`, HEAD/fill safety and canonical `Vary: Accept-Encoding` handling.
- [x] Add global, per-vhost and Docker-discovery transformation controls and administration UI/API coverage.
- [x] Make Passive Backend Health explicit opt-in for ordinary and Docker vhosts while preserving independent active health checks and unconditional observability.
- [x] Keep route cache override tri-state and expose effective route/cache/transform diagnostics.
- [x] Close 0.21.0 with the current conservative JS/CSS transformers; defer parser-backed replacements to a later development cycle.
- [ ] Release-host gate: run Rust 1.88+ fmt/check/test/clippy/release-build and live H1/H2/H3/cache/compression/restart acceptance before deployment.
- [x] Finish release polish: backend timing header rename, human-readable uptime, transformed Live traffic series, Docker Passive Backend Health layout fix, warning cleanup and cache module split.
- [x] Keep package/source identity and OpenAPI at 0.21.0 and package the completed feature scope as the 0.21.0 release source.
See [the 0.21.0 specification audit](docs/V0.21.0_AUDIT.md).
## 0.20.0
Current direction: 0.19.4 -> 0.20.0
- [x] Split Certificates into Installed / Renewal / Generate tabs.
- [x] Move generation/import into the tabbed workspace and keep existing certificate actions compatible.
- [x] Improve ACME/renewal status readability and display exact Certbot log locations on failures.
- [x] Update package/source identity, OpenAPI and release checks to 0.20.0.
## 0.19.4
Current direction: 0.19.3 -> 0.19.4
- [x] Normalize Docker-vhost Aliases field geometry with neighboring inputs and remove the extra inherited label spacing.
- [x] Retain `0.0 KiB/s` idle and `<0.1 KiB/s` positive low-rate backend traffic formatting with release regression coverage.
- [x] Add Cache domain-table filtering by resolved cache-enabled state without changing aggregate cache statistics.
- [x] Update package/source identity, OpenAPI and release checks to 0.19.4.
## 0.19.3
Current direction: 0.19.2 -> 0.19.3
- [x] Preserve the exact panel and Settings tab through session expiry/re-login without shell flicker.
- [x] Canonicalize `/configuration` into persistent Settings tab routing.
- [x] Add shared save-button busy state and field-focused validation feedback.
- [x] Preserve certificate import file selections after failed saves.
- [x] Add per-Settings-tab unsaved-change indicators, including Manual config.
- [x] Add persisted dark/light administrator theme selection and consolidate theme-sensitive CSS tokens.
- [x] Keep Live Logs column selection compact with standard checkboxes and no internal scrollbars.
- [x] Add a fail-soft embedded-config release checker with hourly default cadence and CLI/environment overrides.
- [x] Keep the release checker compiler-compatible by mapping boxed HTTP body collection failures explicitly into `anyhow::Error`.
- [x] Reduce the footer to API latency/dropped logs and fix the last broad Live Logs checkbox sizing override.
- [x] Update package/source identity, OpenAPI and release checks to 0.19.3.
## 0.19.2
Current direction: 0.19.1 -> 0.19.2
- [x] Restore administration-backend local-listener risk detection for startup-locked listeners and loopback aliases; make vhost discard restore the persisted draft baseline.
- [x] Use descriptive status names for default responses.
- [x] Add an on-demand 4096-entry live log buffer that lasts until the log window closes.
- [x] Align inline checkboxes and reduce Docker-vhost alias editor height.
- [x] Render idle backend traffic as `0.0 KiB/s` and positive sub-0.1 KiB/s traffic as `<0.1 KiB/s`.
- [x] Remove cache diagnostics/validators from locally generated placeholder/default pages and enforce `no-store`.
- [x] Allow safe vhost-ID rename with collision protection and operator warning.
- [x] Update package/source identity, OpenAPI and release checks to 0.19.2.
## 0.19.1
Current direction: 0.19.0 -> 0.19.1
- [x] Repair Default responses section serialization/server routing.
- [x] Isolate cache objects by frontend scheme.
- [x] Keep a 256-entry live log window with 4096 retained entries available on demand.
- [x] Polish backend traffic units, checkbox/column UI and Docker-vhost Advanced layout/alias spacing.
- [x] Remove stale screenshots and index historical/version-labelled documentation.
- [x] Restore source-package test files/fixtures required by the release gates.
- [x] Update package/source identity, OpenAPI and release checks to 0.19.1.
## 0.19.0
Current direction: 0.18.8 -> 0.19.0
- [x] Persist per-admin Live Logs column visibility in SQLite without affecting JSON export or proxy configuration revisions.
- [x] Remove the duplicate top-right version badge.
- [x] Carry the legacy X.509 `tls_verify=false` compatibility fix and expanded upstream diagnostics into the release.
- [x] Add schema-v4 migration, administration API coverage and update package/source identity to 0.19.0.
## 0.18.8
Current direction: 0.18.7 -> 0.18.8
- [x] Use hostname-style chips for Docker-vhost aliases.
- [x] Add per-app HTTP-to-HTTPS redirect, WebSocket and cache switches for Docker vhosts.
- [x] Add an agent-level default ACL-profile selector without removing per-app ACL overrides/inline rules.
- [x] Remove the compression-codec explainer from Overview.
- [x] Update package/source identity, API schema and release checks to 0.18.8.
## 0.18.7
Current direction: 0.18.6 -> 0.18.7
- [x] Keep Response time history collapsed by default without stopping its live samples.
- [x] Make per-interface RX/TX charts visually consistent with the aggregate network history.
- [x] Fix expanded Docker-vhost route/ACL field alignment from the 0.18.6 UI.
- [x] Update package/source identity and release checks to 0.18.7.
## 0.18.6
Current direction: 0.18.5 -> 0.18.6
- [x] Add network-throughput diagnostics split by non-loopback host interface.
- [x] Re-audit compression counters and add q-weighted negotiation coverage for all four supported codecs.
- [x] Polish Docker-agent settings alignment and spacing around listener selection / Add agent.
- [x] Update package/source identity and release checks to 0.18.6.
## 0.18.5
Current direction: 0.18.4 -> 0.18.5
- [x] Make compression counters track the codec actually applied by RustProxy and add independent per-codec regression coverage.
- [x] Split the Overview request-rate chart into total, HTTP/1.1, HTTP/2 and HTTP/3 series.
- [x] Add a proxied-app launcher to Docker vhosts and show their domains beside generated backend-pool IDs.
- [x] Remove the global Open proxy shortcut.
- [x] Make Compact density clearly tighter than Dense.
- [x] Give proxy timeout controls explicit safe defaults instead of blank inherited values.
- [x] Replace Docker-agent listener CSV entry with the named-listener picker and tighten the agent status-row UX.
- [x] Update package/source identity and release checks to 0.18.5.
## 0.18.4
Current direction: 0.18.3 -> 0.18.4
- [x] Make TCP the default backend health-check mode without rewriting existing explicit HTTP/off configuration.
- [x] Make Docker vhosts summary-first so large container inventories do not render every routing/ACL/timeout/health control at once.
- [x] Simplify Settings -> Security with progressive disclosure for credential and host-policy editing.
- [x] Move Administration audit to a dedicated on-demand modal.
- [x] Update package/source identity and release checks to 0.18.4.
## 0.18.3
Current direction: 0.18.2 -> 0.18.3
- [x] Export Docker vhosts/backends under dedicated Prometheus metric families rather than ordinary vhost/backend families.
- [x] Add a separate live Docker-vhosts metric-group switch in Prometheus settings.
- [x] Cover Docker vhost traffic/duration and backend health/latency/probe/runtime observations with bounded labels.
- [x] Add regression checks for ordinary-vs-Docker metric isolation.
- [x] Add configurable separate access-log files per vhost and preserve bounded asynchronous sink behavior.
- [x] Lock startup-owned HTTP/HTTPS/admin binds while keeping non-startup listeners editable.
- [x] Polish compact UI, 404 response statuses, redirect summaries, global TLS cipher visibility and managed-certificate lifecycle UX.
- [x] Default WebSocket proxying to HTTP/1.1 Upgrade and make RFC 9220 HTTP/3 Extended CONNECT an explicit experimental vhost opt-in.
## 0.18.2
Current direction: 0.18.1 -> 0.18.2
- [x] Serve the standalone Docker agent over HTTPS by default using a persistent self-signed ECDSA certificate.
- [x] Add SHA-256 certificate pinning for self-signed Docker agents while retaining normal public-CA validation when no pin is configured.
- [x] Add per-service Docker backend health-check settings backed by the normal bounded backend health machinery.
- [x] Show Docker backend health/runtime diagnostics and agent TLS/observation state in the Docker vhosts UX.
## 0.18.1
Current direction: 0.18.0 -> 0.18.1
- [x] Add a standalone, token-authenticated Docker discovery agent that reports only running containers with non-loopback published TCP ports.
- [x] Add multi-agent endpoint/token/base-domain/listener/default-IP settings and keep observed Docker inventory separate from desired persisted assignments.
- [x] Detect multiple usable Docker-host addresses and preserve every public Docker bind for each discovered service.
- [x] Add a Docker vhosts panel with short names, arbitrary FQDNs, aliases and per-service backend-IP selection; reconcile assignments into dynamic ephemeral vhosts without revision writes.
- [x] Move the cache Live toggle to the right edge of the cache action toolbar.
- [x] Remove all dedicated `X-Rust-Proxy-Revision` handling; it is no longer generated, stripped, protected or migrated.
- [x] Replace production password recovery help with the installed `rust-proxy admin reset-password` command.
## 0.17.0
Current direction: 0.16.1 -> 0.17.0
- [x] Add Debian/systemd/logrotate packaging and local APT-repository generation.
- [x] Export all currently implemented bounded process/OS diagnostics and extend safe Prometheus coverage for backend, GeoIP, certificate renewal, stream and logging state.
- [x] Add explicit trusted-CIDR Prometheus scraping without a token and preserve actual-peer-only ACL evaluation.
- [x] Ship a Grafana operations dashboard.
- [x] Add dedicated certificate/listener-aware local-response / 404 hosts and a dashboard editor.
- [x] Keep the built-in welcome placeholder `no-store` and strip optional response metadata.
- [x] Match Redirect source-domain editing UX to Vhost hostname chips.
## 0.16.1
Current direction: 0.16.0 -> 0.16.1
- [x] Keep revision CAS as an internal concurrency mechanism but stop showing revision numbers in successful configuration-save feedback.
- [x] Preserve all 0.16.0 proxy/cache/H3/admin/UX behavior and bump release identity to 0.16.1.
## 0.16.0
- [x] Keep the main vhost backend as an internal fallback separate from Custom locations, with legacy synthetic-root normalization.
- [x] Preserve the reserved administration session cookie only for an explicitly allowed, TLS-protected self-admin reverse-proxy route; ordinary upstreams remain isolated.
- [x] Complete backend WebSocket takeover before HTTP/3 Extended CONNECT success and add phase/error/reset/upstream diagnostics to access logs.
- [x] Split process-memory and cache-memory reporting into RSS categories plus hot/transient fill/cold-queue/cold-I/O gauges while retaining the existing allocator-release paths.
## 0.15.1
Current direction: 0.15.0 -> 0.15.1
- [x] Repair EN/PL Custom locations/help translations and reject misplaced language-pack keys in release checks.
- [x] Rework visual header-rule editing and add a live effective-behavior preview to each location.
- [x] Expand in-panel location documentation with structured sections and common examples.
- [x] Add combined HIT/MISS/BYPASS activity and Proxy process CPU/RSS charts.
## 0.15.0
Current direction: 0.14.0 -> 0.15.0
- [x] Add Nginx-style Custom location matching, rewrite and local-response controls with inline documentation and a visual header-rule builder.
- [x] Add reusable Nagios/Icinga API checks for control-plane latency, backend availability/latency and dropped-work counters.
- [x] Move per-vhost timing/Cloudflare diagnostics to the main frontend configuration and add source IP to live logs.
- [x] Add dashboard history charts for RAM cache, network throughput and response latency plus compact API/footer diagnostics.
- [x] Add separate idle-age and total-hot-residency cold-cache policies.
- [x] Remove the runtime renewal-observation JSON importer and quarantine obsolete state only during a stopped source upgrade.
- [x] Accept repeated literal path separators by normalizing them for routing/ACL decisions while preserving the original backend target.
## 0.14.0
Current direction: 0.13.2 -> 0.14.0
- [x] Accept explicitly allowlisted browser-facing administration DNS authorities behind a trusted reverse proxy without requiring the internal admin port.
- [x] Keep direct IP/localhost administration access on the configured admin port and derive same-origin/cookie security from trusted external proxy metadata.
- [x] Implement RFC 9220 WebSocket Extended CONNECT over HTTP/3 without a side-channel transport or HTTP/1.1 bootstrap fields on the client-facing H3 request.
- [x] Move the H3 stack to `hpx-h3 2.5.19` with integrated Quinn transport and advertise Extended CONNECT support.
- [x] Rename the requested dashboard sections/actions in EN/PL and update release contracts/documentation.
## 0.13.2
Current direction: 0.13.1 -> 0.13.2
- [x] Honor a valid administrator session on direct panel refresh and revalidate stale browser-history login documents.
- [x] Force hot-cache handoff to enabled cold storage during managed updates and provide a longer controlled-stop budget.
- [x] Store automatic-renewal observations in SQLite schema v3 and restore them before the administration listener starts, with one-time legacy JSON import.
- [x] Move the raw configuration document editor under **Settings -> Manual config** while preserving `/configuration` as a compatibility alias.
- [x] Update documentation, OpenAPI, language help, offline regressions and release identity.
## 0.13.1
Current direction: 0.13.0 -> 0.13.1
- [x] Extend administrator browser sessions to a configurable absolute 7–30 day lifetime, default 30 days.
- [x] Keep password change, logout and revoke-all as immediate session invalidation boundaries.
- [x] Allow deliberate local-listener backends, including port 9090, only behind explicit per-backend risk acceptance while retaining private/special-address policy.
- [x] Add panel confirmation, OpenAPI fields, EN/PL strings, documentation and regressions.
## 0.13.0
Current direction: 0.12.1 -> 0.13.0
- [x] Add first-class redirect hosts and dashboard navigation.
- [x] Support scheme/path matching, URL/host/path targets, 301/302/303/307/308, path/query and redirect-cache controls.
- [x] Share named listeners and certificate inventory with redirects while rejecting conflicting frontend ownership.
- [x] Default backend HTTPS `tls_verify` to false when omitted; keep explicit opt-in verification.
- [x] Compact certificate inventory cards, render DNS SANs as vhost-style badges and inspect legacy/manual public PEM files when stored coverage metadata is absent.
- [x] Update OpenAPI, docs, translations, regressions and release identity.
## 0.12.1
Current direction: 0.12.0 -> 0.12.1
- [x] Split ordinary configuration writes into revisioned section endpoints and retain atomic full-document publication for intentional multi-section changes.
- [x] Stop unrelated control-plane saves from rotating cache compatibility identity.
- [x] Preserve dashboard subpage routing across browser refresh/back-forward navigation.
- [x] Add independently selectable cold-cache idle/LRU/shutdown persistence policies.
- [x] Flush hot cache objects to cold storage during graceful shutdown and release RAM after bulk demotion where the allocator supports it.
- [x] Expose certificate DNS SubjectAltName coverage separately from configured SNI bindings.
- [x] Improve HTTPS backend TLS validation guidance while preserving secure-by-default omitted configuration and the explicit debug preset.
- [x] Update OpenAPI, regression checks and release identity to 0.12.1.
- [ ] Compile and execute the Rust test suite on a Rust 1.88+ staging host.
## 0.12.0
Current direction: 0.11.5 -> 0.12.0
- [x] Move Layer-4 TCP/UDP stream management out of Settings into a dedicated main-navigation page directly below Vhosts.
- [x] Give Streams the vhost-style searchable card + modal workflow with explicit active-versus-desired restart state.
- [x] Add fixed-cardinality per-stream connection/session, reject, error, byte and UDP packet telemetry without database work on the data path.
- [x] Add browser-session RX/TX rate charts derived from monotonic backend stream counters.
- [x] Restyle named listener bind addresses with the same chip interaction used for vhost hostnames.
- [x] Extend offline source/release regression gates for Streams navigation, authenticated assets, UI contracts and L4 metrics source invariants.
- [x] Stop successful unsafe methods from globally purging otherwise unrelated cached GET objects.
- [x] Use Host + URL as the logical primary cache identity and response `Vary` for representation variants instead of hashing unrelated request headers/backend members.
- [x] Share cache objects across backend-pool members and GET/HEAD requests.
- [x] Preserve resident age through 304 revalidation and hot/cold movement; persist the additional cold metadata in format 2.
- [x] Add variant-schema safety and cache regression coverage for the new behavior.
- [x] Make Force SSL require a real HTTP ingress and choose port 443 deterministically when several TLS ports are selected.
- [x] Add a graphical ACL-profile rule editor and resilient bulk-rule import with exact line errors.
- [x] Hide RustProxy cache diagnostics when a vhost explicitly disables cache.
- [x] Distinguish disabled vhost pools from unhealthy backends in the dashboard.
- [x] Add per-vhost client-IP forwarding control without weakening RustProxy ACL/GeoIP/rate-limit/Tinyauth identity.
- [x] Add a compact vhost search/filter for large installations.
- [x] Keep the checked-in Cargo lockfile stable and correct the Quinn manifest version to the resolved release.
- [x] Make the standard JSONL log directory writable under the hardened systemd service.
- [x] Accept HTTP/2/H3 `:authority` on the HTTPS administration panel without requiring a duplicate legacy Host header.
- [x] Reject ambiguous requests where legacy Host conflicts with protocol-native authority, while preserving strict HTTP/1.x origin-server semantics.
- [x] Add administration H2 authority and `/healthz` regression tests.
- [x] Add per-vhost Force SSL with an HTTP 308 redirect to the configured TLS frontend while preserving path/query.
- [x] Preserve NPM `ssl_forced` in the SQLite migrator as RustProxy Force SSL with usable HTTP+HTTPS listener bindings.
- [x] Fix managed update/rollback health checks for persisted administration HTTPS and changed admin bind/Host policy.
- [x] Add a dry-run-first SQLite migration tool for Nginx Proxy Manager proxy-host names and backends, with generic RustProxy TLS certificate fallback and manual-review warnings for unsupported NPM features.
- [x] Add offline migration-planner and HTTPS health-target regression coverage.
- [x] Add reusable ACL Profiles with country allow/block and IP/CIDR policy, attachable per vhost while retaining cumulative inline vhost and route ACL controls.
- [x] Add chip-style hostname editing and leading `*.` wildcard domains with exact-host and longest-wildcard precedence.
- [x] Add compact browser-session request-rate charts to vhost cards with progressive lifetime compaction.
- [x] Add the built-in HTTP 401 error page and treat benign post-upgrade WebSocket closes as normal tunnel completion.
- [x] Redirect plaintext requests arriving on the HTTPS administration socket and accept valid DNS Host values when no explicit admin hostname allowlist is configured.
- [x] Allow one named frontend listener policy to bind up to 16 explicit local addresses while forbidding wildcard binds in multi-address mode.
- [x] Align the Logging format builder with the Vhost configuration modal and reposition its field-configuration action.
- [x] Make source ZIP generation deterministic and verify reproducibility in release smoke tests.
- [x] Add bounded hot RAM and optional persistent cold disk cache tiers with asynchronous demotion/promotion, restart recovery, integrity metadata and separate hot/cold statistics.
- [x] Add cold-tier settings for path, disk/object budgets and idle demotion time while preserving global hot RAM limits.
- [x] Add structured-log format presets plus a visual field selector/renamer with JSON preview and advanced raw mapping fallback.
- [x] Normalize Logging sink cards to the shared application card styling instead of release-specific fieldset CSS.
- [x] Add baseline multi-stage Docker/OCI packaging with an unprivileged runtime user and persistent data volume.
- [x] Document browser hard-reload cache revalidation semantics and expose the exact request revalidation reasons in the cache inspector.
- [x] Redesign vhost cards around listener/protocol/certificate/cache identity plus live pool health, requests, active exchanges, errors, latency and traffic rate.
- [x] Add bounded structured JSON access-log sinks for JSONL files, Logstash TCP and Loki HTTP(S), with configurable field mapping/static fields and sink counters.
- [x] Expand cache object inspection with body bytes, resident bytes, overhead and aggregate stored-object memory counters.
- [x] Add optional `X-Cache-Resident-Age` and expose resident age for stored HITs.
- [x] Add administration HTTPS using the application-managed default certificate or a selected certificate, with restart-aware persisted bind address and hostname allow list.
- [x] Permit non-loopback administration binding when HTTPS is enabled; keep plain remote HTTP behind the legacy explicit risk gate.
- [x] Add global TLS protocol/cipher policy plus per-vhost TLS 1.2/1.3 and cipher overrides for TCP TLS. HTTP/3 vhosts inherit the global TLS 1.3 policy.
- [x] Make Settings tabs fill the available width and add dedicated Administration/TLS policy pages plus a separate Logging navigation page.
- [x] Enable shared cache by default while keeping explicit vhost/route disable controls.
- [x] Implement fresh conditional HIT for `If-None-Match` / `If-Modified-Since`.
- [x] Implement backend validator revalidation and cached-body reuse on backend 304.
- [x] Treat request/response `no-cache` as validation semantics rather than no-store; preserve `no-store` as hard exclusion.
- [x] Retain stale validators for bounded revalidation without stale serving.
- [x] Raise maximum object configuration to 1 GiB and remove eager object-sized fill allocation.
- [x] Add diagnostics, real-binary smoke scenarios, unit regressions and 0.8.1 release documentation.
- [x] Per-vhost H2/H3 frontend opt-outs next to named-listener selection, capability-gated by Settings listeners.
- [x] Global static-cookie cache defaults for common assets, visible global/per-vhost controls and response-side privacy guards.
- [x] Listener editor uses page scrolling without a nested scrollbar.
- [x] Per-vhost fixed-window request rate limiting with burst allowance.
- [x] Per-country rate overrides and block/allow-only country access rules backed by verified install-time GeoIP data.
- [x] Cloudflare client IP mode gated by direct-peer Cloudflare CIDRs.
- [x] Per-vhost H2/H3 frontend opt-outs and Server-Timing diagnostics.
- [x] Named listener selection in vhosts; listening IP/ports managed in Settings.
- [x] Health-check `off` disables active and passive classification.
- [x] HTTPS-by-IP backend debugging defaults and warnings.
- [x] Global static-cookie cache defaults plus per-vhost inheritance/overrides with response privacy guards.
- [x] Corrected cache Age calculation.
- [x] Updated deployment regressions, docs and 0.8.0 release identity.
- [x] Release CLI can generate the one-time setup token before first server start.
- [x] Explicit two-flag risk gate for wildcard remote administration binding.
- [x] Numeric Host validation for the wildcard panel; DNS hostnames remain rejected.
- [x] Linux/systemd install defaults enable 80/443; explicit `--no-https` opt-out.
- [x] Updated deployment regressions, docs and 0.7.3 release identity.
- [ ] Build/check/test the Rust binary and exercise remote-panel/systemd behavior live.
- [x] Explicit release identity, without overwriting the 0.7.1 artifact.
- [x] Swagger asset default independent of environment presence/Cargo config lookup.
- [x] Embedded assets prepared by our build, without an adapter's old-version fallback.
- [x] Offline local ZIP, bounded member validation, optional SHA-256 and safe caching.
- [x] Executable build-helper regressions and updated build/migration instructions.
- [ ] Build/check/test real Rust and render the actual upstream Swagger distribution.
- [x] Per-location visual proxy-request and client-response header rules.
- [x] Minimal anonymous login document and filtered extensible language bundle.
- [x] Exact default-deny admin route/method policy including private assets and docs.
- [x] Protected OpenAPI/Swagger, cookie session and CSRF-confirmed mutations.
- [x] Generic public discovery/errors and cross-tab UI revocation cleanup.
- [x] Source audit, access probe and regression tests with explicit execution limits.
- [ ] Compile and run router/proxy tests, inspect real Swagger rendering, and run the
anonymous probe against the built release before production acceptance.
- [x] Scheduled renewal for HTTP-01, Cloudflare, OVH and RFC2136 managed profiles.
- [x] Validated revisioned hot certificate publication with unchanged/failure safety.
- [x] Per-certificate opt-out, manual check, dry run, next/last/expiry observations.
- [x] Separate install/update scripts; isolated service, full data backup and rollback.
- [x] Vhost Basic Auth with bounded Argon2id checks and optional credential forwarding.
- [x] Per-vhost WebSocket toggle and graphical cumulative CIDR ACL editing.
- [x] Visual prefix locations with pool exclusion for dedicated origins.
- [x] Neutral welcome page, opt-in protocol fixture, shared square modal/Settings style.
- [x] Independent proxy and local administrator control plane; no Docker dependency.
- [x] SQLite documents, revision CAS, immutable snapshots, audit and private backups.
- [x] Accounts, bounded sessions, setup capability, origin/CSRF protections.
- [x] Host/path routes, aliases, explicit private-origin permission and CIDR ACLs.
- [x] Streaming bodies and full-exchange limits; bounded logs, SSE and Prometheus.
- [x] Public TLS/SNI, verified HTTPS upstreams, guarded DNS, HTTP/2 and WebSocket source.
- [x] Whole-source upgrade, legacy auth-module repair and rollback on copy failure.
- [x] Persist public HTTP/HTTPS bind addresses and ports, including 80/443 presets.
- [x] Distinguish active listeners, desired listeners and startup defaults; show restart pending.
- [x] Allow remote origins on ordinary 80/443 without allowing local listener loops.
- [x] RAM cache with TTL/Age, entry/object/byte limits, LRU eviction and route bypass.
- [x] Bounded streaming fills; do not publish partial, failed, oversized or trailer bodies.
- [x] Privacy exclusions, conservative Vary identity and ACL-before-cache ordering.
- [x] Optional X-Cache, X-Cache-Hits and Cache-Status; counters and authenticated purge.
- [x] Invalidate old fill generations on purge, config publication and successful unsafe methods.
- [x] Streaming gzip, Brotli, Zstandard and deflate negotiation with privacy/no-transform safeguards.
- [x] Preserve/hide/custom Server policy; client-response and upstream-request header editors.
- [x] Denser responsive dashboard; original logo retained.
- [x] OS/process CPU, RAM/RSS, load, descriptors, threads, network, cgroup and cache diagnostics.
- [x] Compile-time language discovery from langs/*.json; metadata, fallback and RTL.
- [x] Extra language files survive managed source updates.
- [x] Log filtering by host/path/cache outcome and bounded JSON export.
- [x] Unit and real-binary integration test source; 49 simulated-API frontend checks.
- [x] Example unprivileged systemd service with low-port capability.
- [x] Remove reported compression threshold type mismatch without narrowing or panic conversion.
- [x] Explicit frontend IP/port/TLS ownership inside a vhost; actual-destination isolation.
- [x] Multiple backend members, weights, backup and drain modes.
- [x] Weighted round-robin, least active exchanges, least traffic, adaptive and IP hash.
- [x] Connection keepalive/reuse limits and timers, not a Keepalived/VRRP integration.
- [x] Bounded TCP/HTTP health checks, slow-response exclusion, fall/rise and recovery.
- [x] Opaque cookie/IP affinity; compatible edit continuity, safe cache bypass.
- [x] Full-exchange backend leases; no automatic replay of failed writes.
- [x] Per-vhost response/security headers, TLS-only HSTS and explicit preload policy.
- [x] HTTP/2 TLS preference retains HTTP/1.1 for clients and HTTPS origins.
- [x] Vhost editor, atomic legacy migration, health/traffic/latency dashboard and API metrics.
- [x] All new user-facing text in discovered language packs; logo unchanged.
- [x] Rust regression and real-binary acceptance test source; 63 simulated-API UI checks.
- [x] Complete source update, examples, migration documentation and evidence report.
- [x] Split oversized status JSON expansion instead of masking it with a larger recursion limit.
- [x] Optional per-vhost certificate upload/selection with bounded private staging and validated SNI mappings.
- [x] Independent vhost cache behavior, inherited defaults and shared resident/object ceilings.
- [x] **Selective cache invalidation by vhost**, promoted from Cache evolution; invalidate pending fills too.
- [x] Compact manager list and tabbed vhost dialog, visual header rules and live health/latency.
- [x] Dashboard density presets and top alignment, without redesigning the logo.
- [x] Separate modal/docked live logs with persistent controls, vhost filters, pause and scoped export.
- [x] Global/vhost error pages, maintenance/static response and direct-IP/unknown-host behavior.
- [x] Security shortcut within Settings; new labels remain in discoverable language packs.
- [x] Python-only random-data mock origin for cache, health, delay, compression and error-page development.
- [x] Regression test source for new runtime policies; executed frontend/helper checks in TEST_REPORT.md.
- [x] Canonical Forwarded/X-Forwarded-For/X-Real-IP with explicit bounded proxy trust; keep destination-IP routing independent.
- [x] Per-vhost Tinyauth auth subrequest, sanitized identity headers, restricted login redirects and fail-closed handling before cache/static/upgrades.
- [x] Separate certificate inventory tab with upload, per-vhost selection and metadata-only read API; leave ACME as future work.
- [x] Named multiple-IP listeners, descriptive scope, reusable vhost references and explicit legacy-route permission.
- [x] HTTP/3 UDP adapter using Quinn/h3 with TLS 1.3, same SNI/policy pipeline, Alt-Svc, requested limits and bounded drain; retain TCP H2/H1.
- [x] Request ID/revision off by default, independent per-vhost controls, optional mock-origin removal and effective per-vhost Server policy.
- [x] Actual frontend socket in cache identity; streaming-safe empty request detection for H2/H3.
- [x] Toast above active modal backdrop, including browser top-layer/inertness handling.
- [x] Python fake-auth peer plus Rust regression sources for auth, headers, extra-frontend cache, listeners and real QUIC/TCP.
- [x] Dedicated bounded cache panel: resident objects/bytes, per-domain effectiveness, TTL/Age/hits, reasons and vhost filters.
- [x] Remove the reserved administrator cookie before cache eligibility; preserve application-cookie privacy.
- [x] Handle empty terminating DATA frames and expose fixed X-Cache-Reason / Cache-Status detail diagnostics.
- [x] Replace special mock-origin hiding with explicit revision controls; keep general header rules.
- [x] Default HTTP/3 for TLS while persisting explicit opt-out and TCP fallbacks.
- [x] Diagnose overlapping sockets with addresses, ownership, protocol/limit differences and remedies.
- [x] Classify unavailable/invalid/timed-out upstreams as 503/502/504 respectively.
- [x] Basic Let's Encrypt DNS-01/RFC2136 job and library-import workflow through Certbot; private TSIG/account state and staging-first consent.
- [x] Offline adapter tests, browser fixtures and Rust regression sources; unchanged brand assets.
- [x] Vhosts-only runtime/UI and one-way legacy import preserving path/backend and IP ownership.
- [x] HTTP-01 active-job/domain-scoped challenge serving with private webroot storage.
- [x] Certbot Cloudflare and OVH API DNS-01 adapters alongside RFC2136.
- [x] Unified provider/import dialog, masked credentials, explicit consent and issuer metadata.
- [x] Dedicated opt-in Prometheus endpoint, scrape CIDRs and environment-token authentication.
- [x] Application/admin/listener/vhost/backend/cache/system collectors and exchange histograms.
- [x] Functional Settings tabs with preserved dirty fields, keyboard navigation and no Security sidebar.
- [x] Offline provider regressions, simulated-API UI checks and unexecuted Rust acceptance tests.
- [ ] Validate real HTTP-01, Cloudflare/OVH/RFC2136 DNS updates, CA issuance, cancellation cleanup and certificate hot-deployment.
- [x] Add the managed renewal scheduler and audited validated deployment source.
- [ ] Verify real renewal/publication and add external expiry alerts, revocation/account lifecycle.
- [ ] Resolve and retain Cargo.lock; run check, test and all real-binary suites.
- [ ] Verify real Tinyauth integration, revocation, trusted forwarding and independent HTTP/3 client interoperability.
- [ ] Run an independent RFC 9114/QUIC conformance suite, malformed-stream/load tests and actual TLS/UDP production deployment checks.
- [ ] Execute vhost isolation/weighted balancing/health/recovery/keepalive/affinity acceptance against a real Rust binary.
- [ ] Verify compression output with independent Brotli/Zstandard decoders.
- [ ] Run real TLS/HTTP2/WebSocket/cache concurrency, cancellation and graceful-drain tests.
- [ ] Benchmark HIT/MISS/streaming RSS and CPU under slow clients and origin failures.
- [ ] Fuzz malformed HTTP/cache directives and language-pack validation.
- [ ] Exercise real non-root 80/443 bindings and systemd service confinement.
- [ ] Verify native browser HTTP/CSP/cookie enforcement (navigation blocked in this environment).
- [ ] Validate the dedicated scrape endpoint/token with a real Prometheus server and production network policy.
- [x] X-Cache-Resident-Age - implemented in 0.9.0
## Optional module follow-ups
### Docker discovery
Implemented in 0.18.1 through the standalone `docker-agent/` provider boundary.
Future work can add provider-side filters, mTLS and DNS-provider automation; the core remains useful without Docker and never opens the Docker socket itself.
### Cache evolution
Conditional revalidation is implemented. Stale-while-revalidate/stale-if-error, request collapsing,
tag-based invalidation and richer per-route policy remain open. Persistent hot/cold storage is implemented in 0.10.0.
Selective invalidation by vhost is implemented in 0.3.0; the domain/object inspector in 0.5.0; tags are not. No VCL engine,
distributed cache, ESI or Varnish-protocol compatibility is implemented.
### Operations
Container/OCI packaging is implemented in 0.10.0. Certificate alerting/revocation and account lifecycle, reproducible releases,
performance baselines and cross-platform diagnostics remain open. Remote HTTPS administration is implemented in 0.9.0;
loopback remains the default and plain remote HTTP remains explicitly risk-gated.
+237
View File
@@ -0,0 +1,237 @@
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
9cd6d2c6e3d9acaec82000530d28b7e2a24c39313c6d96c1b15ffd13a7984bde .gitignore
6e27536e3111c9ffceb406bd4b0cf756909fff1e51c8d682ebe734539bd22bc8 AGENTS.md
53127d46ca032af306e9ded8dc2be1afc5a47f07cd4e6b1eff0a6409a5efe3bd CHANGELOG.md
1c54d0333fd6dbbff27ebe6b814c955cf9ab0e776f60496615fa70ebd3bda0a3 Cargo.lock
aed16132478f472abc172dae8c544eefdaab1f88f9d7b6bb00dc7e7934338eab Cargo.toml
d6d22ba63f4bc3d16a13012ecaabff8a156be57c824d2f20f66976502fb27d1f Dockerfile
96916966502e9e64b4f774a6644dd045764babab2e44e8eec9d5dfad933035ee README.md
0eacc532549167c6c371b76769aeb8b359597c99a0aa1f4560fa76750b1516bb ROADMAP.md
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
a55fafd4c9fc17f8af591407e7f86d222ce06658c64dcffda92acbf420f969f1 build.rs
de73fa3783786d4e66ab1e5ed9c4e44e1201a298e223c692c604cd50e30f7ff2 debian/README.md
53f5d02d09d8a7616b0f9954af9a6dbb26bea60b1823da614dda411911f89c23 debian/build-package.sh
0763a4bd8b429f7922b23a7276ff694f3a751b83fb127f6ad7ec654e5e507ecd debian/build-repository.sh
7d169934839fa7a48b888da5cf801ae6f2bb533715de1da10e4bf3c2bd1b5f4d debian/changelog
c90457b19b1b2f7998d3db59a656d215eb0fab23769197405960457b66d95cf7 debian/control
dff2ecb9c14623416981f444bc6df1c4e2bf55d330260f36a312d8d42245400f debian/rules
c537d54fd92a9b97c5f6e6ec63bc1276cda324513e2946b341b2193f63b62e6a debian/rustproxy.environment
f14b43b9ff4c8d767aa0476b28d0c66bcabf18d20986bc897e4a826695184f37 debian/rustproxy.logrotate
d1c483406fb53674ed8db5f30de599b5a91fa1c6b28242eb70a64d6a736430bb debian/rustproxy.postinst
143de32f4eb4d9e833a24476c88bedce2761fe6bc4b20a7986f1b884a2db23e6 debian/rustproxy.service
8e3884153448d76cb5027296ce50a1b614854d9732ba88a4ad3f6b25a3810753 debian/rustproxy.tmpfiles
e8e18df40bcd69d6aa404282679186d9b8256ac63a5b4e80d7f081e8a0095a2e debian/source/format
57d737d73b828bf763b210c8ddea39d7a63224f8d509a518d0b876ba2e6a805a deploy/README.md
50329bd9d6980fb1c90a5cb29966b19a257905791d1d07d185d6bd4e3b0bcb1a deploy/rustproxy.service
8c321c96974e79f725470d38b3aa3780d5b8c432a5c89a6f8d36bd575014657f dev.sh
5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md
5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example
9e9f3f17f38f9b0730f598f65988ce8ef562d2182ef6778fc7292b990e9cf22f docker-agent/go.mod
9d77c90767fa929ee8338734d72ce83284d3d965673270f62cb2b1613d587a33 docker-agent/install.sh
8aeb74c89c914712c14b6e5958bd3242ad298c96d9be6d3163f9380c3847f9ba docker-agent/main.go
3d4051a1f805d145df3877476e4b87773e74be8e123045065955a5133afdc68c docker-agent/main_test.go
634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service
0dbacca69143bb1e38d74270e24562a6749e387b26ef869643e876df6e0b1edb docs/ACME_PROVIDERS.md
888bbf7812e7283202f303680145c99b3f1a7a52fb3923fc3de1a6cbcfb72d00 docs/ACME_RFC2136.md
30822eed02d9a5fbc73ee6cfa1348c67c719665c026462c84e4106bd2a0e6b68 docs/API.md
da6b26d4bc6adec43c5c546eae42d92a5145095b9084bde652818892acd44f0a docs/API_ACCESS.md
98ad4df87b6620c0418c8c39fa6e8e1f8be88576edde2945f9be6e5dae7acb78 docs/ARCHITECTURE.md
9ad82a202abb308e9bf54fdf1a657104e0e1b5d93d05f0b1fb36248ba25a3993 docs/CACHE.md
0450813e3c875371d75cc41835f6362abb5300cd4bdf545f3011d9df8101170c docs/CACHE_DIAGNOSTICS.md
db5534427ff0f004940fd6960906d3eaa75347a110e6322827c51c5d63f20adb docs/CONTAINER.md
c822cd1d6a055d5ddec06d4f6077f0b13837dfb25ede1ec1f4e9a52e182db946 docs/CUSTOM_LOCATIONS.md
7db65a96f696aff1d7f74e2041cfc6ecdf437b1412ece90cee26da1ceff4cf7e docs/DOCKER_DISCOVERY.md
061e1ccee4f3e70126a27ba8bdbac10e895fdb4e6e4607c5c6a6ab1d164baf02 docs/HTTP3.md
7c3281b5e15101c434cbe62250f161adae050bf49826dbb93cf0ce5371affbb4 docs/INSTALL.md
0a124bd3eafaa9c6b3bc2b811fdc3e6295a6a2978766c37cc0735c27f4cf6d59 docs/INTEGRATIONS_040.md
2baaa1d34979c33caead9b271a7d235c673acb4e188c94917d975c25c179c242 docs/LIVE_LOGS.md
4e1504d1a343b94c2252545cbd5248af79f158c4566754dfe11f3c15a19ec0da docs/MANAGEMENT.md
8b7d8106f19543f5e8f801e9f7a35513eb0d02769528b1175943e914297843aa docs/MIGRATION.md
5f090a190000c6badab2a74e5d4968e70c34de43b59c1452f5b05503b5c5c18a docs/MOCK_AUTH.md
1f85fac0e06e432ef6f744f0b5f9c1b7a27a1e9e1ad30dc1a218eab96b1caffe docs/MOCK_SERVER.md
e7516646dfb107d6240370f4e7460b1189f2eb21f398740381d5d102847bbfe6 docs/NOT_FOUND_HOSTS.md
3da047768f32064c5ef0ad7a4c73c4b14930fe7317c54f5650f426af9f484593 docs/NPM_MIGRATION.md
8538816f6b64780ca4f652d189c5d2c00e40b0c8fcd1ad77ae2e11d39058dce6 docs/PROMETHEUS.md
8ecc6a0ac347c66e668255402ca4c46ad5dcb44bd463c86ab23843f9cc8d6e27 docs/README.md
6e46a137b9c88dac8cb9e3fdca877430092392de9913d746278dbff8652460c2 docs/REDIRECTS.md
0dfd0d431c5aedc67abac1ce5de040ffa790ec9578fd7d6f91a987c333d73358 docs/REFERENCES.md
ce5afb4f531bb5fb4c453deb49f643f2a563f32f7908c995e5bc8e0d7de995b7 docs/RENEWAL.md
c6fa646500c0acac18bff6349cbb1d821680a849c8648b4eaa64f2a4305f9b9d docs/SECURITY.md
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
fce52cc908fb86cab16072c0297b6cdaf4857e84185783bc96240d9a6bc321fd docs/SETTINGS.md
3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md
a6c7acde02c37b0f1f3f074bc1a1d9b899391a76ba719a16f0a2d57cfa6f1dda docs/TEST_REPORT.md
67de3ed624b92bc8d8ba7655cd55dde95d2a909f772e3e42535fd80459ab4c5a docs/TRANSFORM.md
b02957b708174c9def830ced3c70c943a47af7a33467ef858255d04ab92c4af0 docs/TRANSPORT.md
96a6816915d526f00e261846131ee3f942ef332af996cf0f73c79ec1ba9f9c7a docs/V0.21.0_AUDIT.md
0c1c3ba6e8724d85ec54e254e70bbf05e063db025280ef4af866b774fb43f76b docs/V0.22.0_AUDIT.md
3f811c457bf6be662969fbac32c3cfcc49b13c2f00d0125e4f2eeab768f59f0e docs/V0.22.1_AUDIT.md
36c2fbab9ec1276715f7d4a7a4aa49725b2fec5e1ba58d97d00a87b259257ea7 docs/V0.22.2_AUDIT.md
579547e18982bb1a527417aaa8bf88b40d02d2e68dc71215b9815d1134a4f865 docs/V0.22.3_AUDIT.md
0fd3d7bad0b3b437cddf40f2dd184b7a1f62f0340a319eb6f6d966960b666e46 docs/V0.23.0_AUDIT.md
ab7d48c2a359928da653263e69b4739cfff849a03fa1270491cb1ce553989788 docs/V0.23.1_AUDIT.md
9cb1b028be2a4a9cf105fb14617ac5776d131814a5e92b497ff816929aa3db41 docs/V0.23.2_AUDIT.md
e4d47b50fb0ce33d26817fc0afcc2f05ac53bc9b62c7e50e61b9fa9734e817b0 docs/V0.23.3_AUDIT.md
984092eeb3899ed696f94059d88f7834703b151682d5c8af9fabacc7a823ea0b docs/V0.24.0_AUDIT.md
5782adf99a292783149882e844ad07b49d4ac0ea478e8393758cff849d48c13d docs/V0.24.1_AUDIT.md
230005cbd56fed4bc832940c0a49cc14f50231650308724df105b0506bb34210 docs/V0.24.3_AUDIT.md
b891db1453cd7cf6b10f79879f4edaf7c83f14e91e4e95826919d40cb5dca2d4 docs/V0.24.4_AUDIT.md
8d6519b00e01e5cd41ee6772941fdc7e9a70fe29626df71cefced6eefe6a94d0 docs/V0.24.5_AUDIT.md
bd7959e325685085507a20b22254c7024ceb9396b6314257694bd764ec096ba5 docs/VHOSTS.md
e32472cea5ef3cd25c058e55df8adfcc1d7bf03e0d12dba69d2f2b9b94fe04b7 docs/VHOST_ACCESS.md
c336f998027330c58d5afbbbb3a3a682fd44ae3997643882fe342633822f8bb3 docs/screenshots/README.md
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
134d4a59727b27accb3141d4cee26a4a74157d236de4996bfd682e639462b2b5 examples/tinyauth-vhost.json
5a8f3a8a054abc89c6e78896b9fe2f903212199a24dc3d2d2bcbd0696a31bcde examples/transport.json
17e7971fa9d9b141019fba3135d87cb1aa0cbc55d56b0718f879f880b615da26 examples/vhosts-balanced.json
e06905b6363103b1bcee1a09b9e02292033f114d88f3edbea22ddab53e9e5071 examples/vhosts-private.json
529b47c6d4ea44d2a25d1cf9561cb5ee1e05f50a568c6dba5889741bcf936434 grafana/README.md
1f43923617da0a0b667a5c9bd98457c78024a786701f56a07be5f24f5eec8ad8 grafana/rustproxy-overview.json
f45e5cc3d0c253534978d4b752b0667e2a7906ccf5a22106400e0f6b70b00d23 install-debian13.sh
858b39fa6886b71d20690bf369e76580922364b3cc7f0cf6ba9b040df9cd76e3 install.sh
29cada0936762f26495064d1524a668262ed729b21f20aebddc8b6e5cfea363b langs/README.md
2e3e5dc918d2838e91310b78922d8d84cbf6c9e8dcd3a0deedf6dd5045f07423 langs/en_US.json
9e1ed1d4f3daa0461ef3d3e137dca66c30141858943c0461a1c80b092a11d5f8 langs/pl_pl.json
0b9cd9ec07cb31ab0d0c75f08fe1ffee04dca099e90513017835f095d347012a make_zip.py
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
5c62da30fa9db594a4d656c77ae676d3c388072b6a85f91170ec7d911d7cde43 migrations/0004_admin_preferences.sql
57dc28982f416874d5d00d1d438a18846d1ce35cb940c4bcf67fd9a8bc57339c migrations/0005_certificate_api.sql
faa7e50922bf180c363f7627fbcfe8079831e36282220d17481cf69bc64b57f1 migrations/0006_certificate_api_credentials.sql
478220552ffeea3de00e2866d427d9f87f4cfc4ae5232e5c3a774e62425b1e0c nagios/README.md
91233910aa4dab07ae917fef93d018a7a0beaab697f9a0bb889ac613e595cca4 nagios/check_rustproxy.py
a42e01b341725c9da0928edf40c8c3b9c814ef90592f208c52b1f2c56a2ad383 nagios/check_rustproxy_api
ee50f38c037765fab742e888d4b9fc4d874c31c785c6064d4eca3d2c4d24d944 nagios/check_rustproxy_backends
1465396cf1358067e9fe48b3d61f28fd4ac65c577b67c502950ce6b42ecbe3b3 nagios/check_rustproxy_latency
6e036f736a2b40feb78eec17bf34871771524190f283591d1c89e48e6d2271e6 nagios/check_rustproxy_queues
7458b2d74495e6fb1c1edeea4d5a793705476431dadb60627187111f91441e13 rust-toolchain.toml
4c0a0a2131c5144ed97228db89ad0b78f08b93dc24b373bd0dc49ca137ad8324 scripts/acme_provider.py
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/acme_rfc2136.py
ea2c670aa1fc2c145fb3666711642d526c2adb01537fc3b6398664e33136f0e4 scripts/admin_audit.py
761aadaffbfb08c32afca28af7e5379f933712563b00dddab7261a9505cb7d5b scripts/backup.py
7155159c6a76448bec8134900c8022c79c1047d0814cb7ab227d2eb331c2a53f scripts/check_source.py
15b3e90be1d89a9b25643bca3226ee4cf912bc2e9bba62bd281ea7a789552943 scripts/deploy.py
6facb2d65bfab234f09855097d12da8f72669a38ebceb4b1cbd03ab42b8d2469 scripts/langs_check.py
60504dd426195fca88d77e075d607c04540dbe8aae2cd806077b3818e0e0cf08 scripts/migrate_npm.py
d1418f8e574a7390fd45a2aa879bdf5720b773162d65e295ab0350506aa5220a scripts/mock_auth.py
2d3ff92300b09ce9cecbbaaa9309b4d33160fa4b2291c9952b784e8cfb599ab5 scripts/mock_server.py
2d4c859aae5ba357a6d30024c8487ae8f59f01b192711f13f9c2f8fee0f48b50 scripts/os-acme-client/README.md
c73b814a42bb4ff703441dbd177ee625055eeb115f9b5bb0c2150aede573a159 scripts/os-acme-client/opnsense-rustproxy-cert-deploy.sh
635ec74ae1be91145e42d4e4580e2015df5b54177a17f04c42b1ee14075b9190 scripts/preflight.py
ae0834cfcc518f4bc5ce1203a703fd5ebcbe71cd02463fdcaf6d776a1241cb2e scripts/release_smoke.py
0f2deebabf637b890f38eb0317e3b26be75d3c169e154f86caf1df82f939325e scripts/routeros-oci-build/.env
45be151cd85fc2ef2f1fdc3003ace63381af262e73af8a685ab48cb697cc0de1 scripts/routeros-oci-build/.env.routeros.example
3b48cff8506e5a6ae7d3876f47721f5de7d63e2e02264a78cf5b6ddd52d9fbc6 scripts/routeros-oci-build/Dockerfile.routeros
7c7a3e51f43b43e1c962a4b0bb591f99c1f06977bf77f23719f7f867dfe7eb8e scripts/routeros-oci-build/Dockerfile.routeros.dockerignore
e6fc807f43f290315c753d0d0d2b9eb081c4dd16690f8c35edef91863d08efe3 scripts/routeros-oci-build/README.md
d53b2ae7315ac143bd17d6327611a39e7c23666408bb95e39da2ca8517322ccb scripts/routeros-oci-build/build-routeros-oci.sh
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a scripts/routeros-oci-build/docker-entrypoint.sh
02ba853fbfcd0d722b1ba121016ebfbe5354dcf7710c708e3110c2ea9fa7e1a5 scripts/swagger_assets.py
102b23de19a88a8e60de392fb83aefd3dfb33e90cc97ed1690d9f3d4a33d8866 scripts/test_compression.py
61e6af8cbca6894e32c0b93f945087d40100e4ca4112249306ea7bbeea8ccf3a scripts/upgrade.py
081aad397b2b9485e20b684dc11682d3a16464c5b4f110cd461f8518eb74ad99 src/acl.rs
2d1ffc1c64e76c647abd3134719fb561b1f27d584c92d867011d8de7dd7f6662 src/acme.rs
fdba60fa997aa18cb0a7ef50e286a8b6fd877d14fe829d4b0bcb5c7c269a5ae6 src/admin/documentation.rs
44d4d26ac04cd9fb15f73ff0dffb8d7fc76062fda2ab4c3eb4e6c7172085877f src/admin/mod.rs
6a78b88b4bb20457bb72b4d01157d596fa498432f916de37fc4ee9a38fa8267e src/admin/openapi.json
b4fa393b780b2676b5d72e0f90cd7290292f424b8062d4b5983e722d6ca72c32 src/admin/public.rs
23c55f23deeec844a9abc2289440c9e0583aaeb72416fc5f3632a43a864c0efd src/admin/web/acme.js
cab34b79502b15c8c5b6e6b7ca0d0b7389179e75906053c8c1daa2ac29b343da src/admin/web/app.js
5412033412495e84e62415413dce8a65dc01db72dfbf56e50253c9d7cbfd37e2 src/admin/web/cache-panel.js
50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html
3b84a7a08f9f68e2c78fed4523b594c31a251addc5480a69c5f6949908230539 src/admin/web/docker.js
4b8a6c0e4c3870432354c88e6248655faa6072cc1f8d0c6906aa970174ec245d src/admin/web/docs.css
c8e509c22696af1fbb423f748dae4906c1584c5da8d114da0bb297c87ec1dbc7 src/admin/web/docs.html
41b641dfa703afd655cd49861f0e714b073b8deb6dbea396cae1b763012f50d5 src/admin/web/docs.js
232b766f278ae4424705af351910677fcd7ac9a20bb8320d9a12b763c91219a2 src/admin/web/error_401.html
16293363c7848471d866092fec5e83023df087aa824989e87450cee7b7ca1263 src/admin/web/error_403.html
f7d0955ab18c15e04369204e789f341d2e832f927c1ff20a3b2b9e35e7898f00 src/admin/web/error_404.html
7dc3634e3d508176545936f77db9deb3dc51f0abb71985e1950aa91b81b6f44f src/admin/web/error_429.html
386b9dc3aecebe7727f7cf7bbd2a02ab036a3abf01315975111a51559128df73 src/admin/web/error_503.html
e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/error_504.html
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
dee3b113907f77138cdf919efdfb097e45b889cff3893bfc6f02ed998643bbae src/admin/web/host404.js
4768f187f04b41e6423bd83e4f596e084eb1c1ac6592c422d5f2e034fb4fea96 src/admin/web/index.html
24db88842707f764064657712d3660c6ac129fbfb47d188c0903925cf8244ce5 src/admin/web/integrations.js
cdab0c11a28deebdf401c0453fbc76b675e9e643722106fb20e812bb9648ccde src/admin/web/login-keys.json
a253d202dc9a147f2add34c64e0f35c38fdc7f667f9908933ce5f959b1a92a2e src/admin/web/login.html
4c38f9398f008b7d27d7100c951465fcecb24d51a1b8973f66e1365d208bbb99 src/admin/web/login.js
e41619e5cb0ba01a3d287748f28eb38bbe7641f9bedade252d286c18e619c400 src/admin/web/management.js
22318f39f7bb1ae98e65737d3c2ad9e63065b8e94ad59bed560c0e80d3a46949 src/admin/web/redirects.js
dafc8b023f334091c7b5a04920038bce46ea3ef4b068e1f88cffe49d44eaeeee src/admin/web/settings.js
1a30affd6a717095a1763998a5850a79ae0f86b7b694806f2aba2b880735dd1a src/admin/web/streams.js
a330875bda5dab02b15a5d0587d24e93380f4d694841d31e89d2d1f294542f82 src/admin/web/style.css
a6f8edf5cd4d5b3485b5ea687f82e3a3b57b5be40c136026e7ad00b5e180891d src/admin/web/transform-panel.js
3b92b83803a309809b0321ba6575dbbb4fa8a133235a2de1fecdba0b3f553103 src/admin/web/vhosts.js
1e5c6c6157f8b66d90751a0480cb90c6fdfa229330fc4c3f3504bc0c689c3d60 src/auth/files.rs
606f5491f94da007881a679051677a4609881d3151ac753de02556e0e76e1ce8 src/auth/mod.rs
1ceb57d97cbff655f185e126d98b8dc7884c7aac865e1f70e3c6d199685c13a7 src/auth/store.rs
168f55f75ad6ebd80f98c3376f5885cdc8d313f8b03f4e93066a28cf7b8c8b81 src/balance.rs
e2bf9d4f8a8c8c7f9a8df96872a657b9706d05f3f3a83721fe912ca0af37b29a src/basic_auth.rs
075b65ac7278fe11481855e844b8ff0654162e8b3e4194e8e3fbc1c0a1d05767 src/cache/cold.rs
1f7073115229bbfb1dd7ea487e1cd97e5815cb1767e068fda7bcb2c8c4b866c0 src/cache/collapse.rs
bdfbbebaeaac5301d775c9b2c9f1ea6505b667d6adeea5c4af542e4e0f61c502 src/cache/dashboard.rs
bbd6de8568fea79cc4a11d33b905af75713e609c8f37ec943c82f0d180e66dcc src/cache/fill.rs
9110e0662022da6a0518bfbf93872143e8ecafb0f541c0d30a0288ad09fc5883 src/cache/invalidation.rs
5cf902ffa805f4c5296fe006dc4d0a3c7e03ff758a68480ba0a4b6e4af9e5fb3 src/cache/lifecycle.rs
8c451cf8008bdb9db19cbd6b45d1352f9c3790a1cd3a6b2341b21789fea9ffb2 src/cache/lookup.rs
58c03fde09e5eab40968679793b364f4f1cf5fd832bd79ebcdc92bb23871ad82 src/cache/mod.rs
61c95d48cc6a3b1d8619030ce34c2a3c340c09af50f3e05840d8f69564d348fc src/cache/model.rs
cae9d35be0e31b5f942e3d3b20e7496581ec6491d7974509ece4cf540271b1f5 src/cache/policy.rs
7e2bcfbb2b57732cdc704d9df7a9298fb80e735e37984431bc936630e41e7f8a src/cache/range.rs
09633b26d089b5a7944262b1d175c17c99de3d1704a03aac5344b4a29f3f279c src/cache/tests.rs
9874abc56a3f38bd06f663965588b709d2a8d29e2317dd3dd898c5aaf9460209 src/cache/tickets.rs
5d1788e6da0669546729e7bea56c64f3b989d399793e67d34fdbf427a6e0c65b src/compression.rs
09279a1f5e6e97ca6f856081bc1e710ae0b57b708bc6a62157741a5ace2fdfbb src/config.rs
d465a0a7fd36bbd6a59cb6e551291530c16697865583cb038bc72ff0673d3240 src/db.rs
19ab6c3ae37009c544291cb0b65c1bb9c721e168f95c1ed3fa88155aca7463ad src/demo.rs
f93f0b1fdb894b29a271fc6f88ccc0afbd5c847ba3297cad3709858e17d05292 src/diagnostics.rs
d7c1093b62dd37479dbefd1bc9f18d2fbc0ee5d4ebe155f7a7bbaccd058b1fd3 src/docker.rs
43f09add98bfd3b2333486979e163bd31f3716c7517dc99c47dc2f4e72c8951a src/forwarding.rs
eff4484fd9630009471f824c57c5c74f3d3c5378f9e46a3bdda03ede37daec82 src/geoip.rs
185a7874d2ba45e6705fb69703fb99c96eef2a8134f43343e2b20f105f1a1be3 src/headers.rs
2f6dd8c74fe1d22a8fa4ec5a21d0d334a667ede1b11629541f4802252765b21b src/http3.rs
a6630962453c7e68a409aac45da51093148b9b3c8d044ab13897a8c9e476ae4d src/lib.rs
df14373ce3f9c5740d7501fdd4cb42d8b7b07b23ad86f2b69b5e534f0e0f01eb src/logging.rs
9125df29c30aac3812d8f6e8b8ee92c151ffc0ce344f334a5cbc81cd3ac3e1ef src/main.rs
7022ec2e991e9906e30b06f5bca58f96b9d7e09cc92a27202b5196d54e341b75 src/metrics.rs
64494e5746912cfcb10612ea4e0b336004b1f738bd7999be7e58fc96abadb6e1 src/not_found.rs
e4b1f61c12437cc840c36eb7ab7b69b03f310bca3f89288ade9992d9dd1b7e2c src/pages.rs
f355d84c16cef89c43785fdc02181d6ad234fbc9c834367843822ff970088d00 src/prometheus.rs
3097b53fee3cfa4bc0316b2aa85aa46e054da4463ce4baf5e771ade957952fb0 src/proxy/body.rs
69edf65d8dea5b6e942e682c96a67f072bd2196c9c4901a68f8ab8d8d4464598 src/proxy/headers.rs
808d8d61635f10012788129f361e52084cf4e7cdcb0d7df986d1e1836476ff11 src/proxy/mod.rs
64665d778a90a22e93af7a17be037255d59fff3220d8924737da9be965acae69 src/proxy/websocket.rs
6b24aa692ed3cc7d20b561c90239e26e203f8e44f64d4bb45b8b63ba05906d4f src/rate_limit.rs
acdca1bf861f95b85f08dbbcf69e8a2090335cab4d542f2509383bcc67a0e699 src/redirect.rs
d6be7744938707714f4e948fc62dd17119f93a068ffcde7537ac51aba945d32e src/runtime.rs
c403b63a4192e9549b8a82f29e6966fddd2130be1a9a86a895e7692168de98ea src/security.rs
d35fbad33476b465d456dde8984daa4e52e34b9d546ab9e48dffcc40c74a4669 src/server.rs
a80e8fc2990e62b8fd28426c8e1d213c9761577462179e96637d646ad3d87518 src/settings.rs
d6f7d8c8e4155c931f037543fde258d0ef6354f7c829b6239e097fde69f07031 src/stream.rs
5663d7bacf5699cf607c5bc1440f6f7650c46d0929a3e9b74e20ac8997a8ca4e src/system_stats/linux.rs
5dada498502763f3183c0c3758d1f6c5139366936fef4be585a018c4746a2c7b src/system_stats/mod.rs
a026c13cc4757cb01bfd4595ac6b57a4d837482e40cd399e8d086a54700b2b97 src/tinyauth.rs
898553df5b2b416c4f0d9b7dc7706d9db5f79b9d12753354a1a1a7e45d48d159 src/tls.rs
f188257276292ae3ff683d1cc9162dd679728ef13247ba65cd498f00be489112 src/transform/css.rs
8b74590ef49d228c0c5959752215b5a8d376014436650f6b4ec647333930d713 src/transform/html.rs
388f602176f152ef29a7c3ecd3ab328d6534b6d1eaae6e7c35a959b432232413 src/transform/js.rs
d51b1ada3cdfce44fccdff1e56ded76baff092c97572a27dc73174881ff37c25 src/transform/json.rs
d3120af52cf47ef07a18d10ffa4dcbe558f2ae38c1d738de8d471f7c6f4ec358 src/transform/mod.rs
6eb6e5b49b73ff8da615ca0fd1d871b87f2f1dc352b0587935ff0b602cecbcf7 src/transform/svg.rs
f255ed094a1cedf86f767bc062b1c589c5940a4f024274ca8238d2e83d0ede6c src/update_check.rs
77cabbcd9730e01d19e1147ede94272f6100d97281f7892d2b1bc0279c780f63 src/upstream.rs
ea3ec51c0ffac30670cca8580374cbd1f67a792dfab2e6ece46aaf22c70636b1 src/vhost.rs
f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json
1725c10331745dc04a07675784d385260da7b14567b8dba3cbae5e891eb5dadf tests/http2.rs
fd8655c72f35f83586af20a9f04468528cf2cfea96a6329cb582972ed8a6548f tests/http3.rs
eecf680f001f2110dde4eceaccf2f922859a16c946a644f8854e8de4d3771f7c tests/policies.rs
728999110a6d8cacb62bc0399bc333fa23a66526ee23f563a3c93bf00e5fad19 tests/transport_config.rs
77ee069a5c0f69c49d2688eff3c9aa0f3e39fa02042cfe348e410a23b8f3e116 update.sh
af688fd4e67e54a8a2ff243fdfe916a20ceae6251baef207a4e974de2149f308 upgrade.sh
+6
View File
@@ -0,0 +1,6 @@
# Embedded defaults for the built-in RustProxy release checker.
# The file is compiled into the rust-proxy binary. Runtime overrides are available
# through CLI flags / environment variables documented in README.md.
repository_url = "https://git.linuxiarz.pl/gru/RustProxy"
version_url = "https://git.linuxiarz.pl/gru/RustProxy/raw/branch/master/Cargo.toml"
interval_seconds = 3600
File diff suppressed because it is too large. Load diff
+135
View File
@@ -0,0 +1,135 @@
//! Build extensible language packs and pinned, authenticated Swagger UI assets.
use serde_json::{json, Value};
use std::{collections::HashSet, env, fs, path::PathBuf, process::Command};
// Prepare the actual embedded bytes here, not in a dependency's earlier build.
// Cargo config discovery is deliberately irrelevant to the pinned default.
fn swagger_assets(
root: &std::path::Path,
out: &std::path::Path,
) -> Result<(), Box<dyn std::error::Error>> {
for name in [
"SWAGGER_UI_DOWNLOAD_URL",
"SWAGGER_UI_ARCHIVE_SHA256",
"RUST_PROXY_PYTHON",
"CARGO_NET_OFFLINE",
] {
println!("cargo:rerun-if-env-changed={name}");
}
let helper = root.join("scripts/swagger_assets.py");
println!("cargo:rerun-if-changed={}", helper.display());
let interpreters = match env::var_os("RUST_PROXY_PYTHON") {
Some(value) if !value.is_empty() => vec![value],
_ => vec!["python3".into(), "python".into()],
};
for interpreter in interpreters {
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()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(error) => return Err(error.into()),
}
}
Err("Python 3.10+ is required for build assets; install python3 or set RUST_PROXY_PYTHON to its executable".into())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let root = PathBuf::from(
env::var_os("CARGO_MANIFEST_DIR").ok_or("Cargo did not supply CARGO_MANIFEST_DIR")?,
);
let out = PathBuf::from(env::var_os("OUT_DIR").ok_or("Cargo did not supply OUT_DIR")?);
swagger_assets(&root, &out)?;
let langs = root.join("langs");
println!("cargo:rerun-if-changed={}", langs.display());
let mut paths: Vec<_> = fs::read_dir(&langs)
.expect("langs directory is required")
.map(|entry| entry.expect("read language entry").path())
.filter(|path| path.extension().is_some_and(|ext| ext == "json"))
.collect();
paths.sort();
assert!(
!paths.is_empty() && paths.len() <= 128,
"expected 1..128 language packs"
);
let mut languages = Vec::new();
let mut locales = HashSet::new();
for path in paths {
println!("cargo:rerun-if-changed={}", path.display());
let id = path
.file_stem()
.unwrap()
.to_str()
.expect("language filename must be UTF-8");
assert!(
id.len() <= 64
&& id
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-'),
"invalid language filename: {}",
path.display()
);
let content = fs::read(&path).expect("read language JSON");
assert!(content.len() <= 262_144, "language pack exceeds 256 KiB");
let pack: Value =
serde_json::from_slice(&content).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
let meta = pack["meta"]
.as_object()
.expect("language meta object is required");
for key in ["locale", "name", "native_name", "direction"] {
assert!(
meta.get(key)
.and_then(Value::as_str)
.is_some_and(|v| !v.is_empty() && v.len() <= 128),
"{id}: missing/invalid {key}"
);
}
assert!(
matches!(meta["direction"].as_str(), Some("ltr" | "rtl")),
"{id}: invalid direction"
);
let locale = meta["locale"].as_str().unwrap();
assert!(
locale.split('-').all(|part| !part.is_empty()
&& part.len() <= 8
&& part.bytes().all(|b| b.is_ascii_alphanumeric())),
"{id}: use a BCP-47 style locale"
);
assert!(
locales.insert(locale.to_ascii_lowercase()),
"duplicate locale {locale}"
);
let translations = pack["translations"]
.as_object()
.expect("translations object is required");
assert!(
translations.len() <= 4096
&& translations.iter().all(|(k, v)| !k.is_empty()
&& k.len() <= 128
&& v.as_str().is_some_and(|s| s.len() <= 16_384)),
"{id}: translations must be bounded strings"
);
languages.push(json!({"id":id,"meta":meta,"translations":translations}));
}
assert!(
languages.iter().any(|p| p["id"] == "en_US"),
"en_US.json is the required fallback pack"
);
let login_path = root.join("src/admin/web/login-keys.json");
println!("cargo:rerun-if-changed={}", login_path.display());
let login_keys: HashSet<String> =
serde_json::from_slice(&fs::read(login_path).expect("read public translation keys"))
.expect("parse public translation keys");
let mut login_languages = languages.clone();
for language in &mut login_languages {
language["translations"]
.as_object_mut()
.expect("translations object")
.retain(|key, _| login_keys.contains(key));
}
let login_bundle =
serde_json::to_vec(&json!({"default":"en_US","languages":login_languages})).unwrap();
fs::write(out.join("login-langs.json"), login_bundle).expect("write login translations");
let bundle = serde_json::to_vec(&json!({"default":"en_US","languages":languages})).unwrap();
assert!(bundle.len() <= 8_388_608, "language bundle exceeds 8 MiB");
fs::write(out.join("langs.json"), bundle).expect("write language bundle");
Ok(())
}
+60
View File
@@ -0,0 +1,60 @@
# Debian packaging
This directory builds the `rustproxy` binary package for RustProxy 0.24.5.
The package installs:
- `/usr/bin/rust-proxy`
- `/etc/rustproxy/environment` (conffile)
- `rustproxy.service`
- logrotate policy for `/var/log/rustproxy`
- Grafana dashboard under `/usr/share/rustproxy/grafana/`
The service runs as the unprivileged `rustproxy` system user. `CAP_NET_BIND_SERVICE`
is the only ambient/bounding capability, allowing ports 80/443 without root. The
administration listener is loopback-only by default. systemd and `postinst` both
ensure `/var/lib/rustproxy` and `/var/log/rustproxy` exist with mode `0750`.
## Build the `.deb`
Install build prerequisites (including Rust 1.88+), then from the source tree:
```bash
./debian/build-package.sh
```
The script uses the committed `Cargo.lock`, runs `cargo fetch --locked`, invokes
`dpkg-buildpackage -us -uc -b`, and copies generated package artifacts to `dist/`.
If dependencies are pre-fetched/vendored, set `CARGO_NET_OFFLINE=true` for an
offline Cargo build.
## Repository for rustproxy.linuxiarz.pl
Install repository tooling:
```bash
apt install dpkg-dev apt-utils gzip gnupg
```
Build the repository:
```bash
RUSTPROXY_REPO_GPG_KEY='<fingerprint-or-key-id>' ./debian/build-repository.sh
```
Publish the generated `repository/` directory at the web root of
`https://rustproxy.linuxiarz.pl/`. Export the public repository key separately and
install it on clients as `/usr/share/keyrings/rustproxy.gpg`, then use:
```text
deb [signed-by=/usr/share/keyrings/rustproxy.gpg] https://rustproxy.linuxiarz.pl stable main
```
The repository script emits `Packages`, maximum-gzip `Packages.gz`, `Release`, and,
when a GPG key is configured, `Release.gpg` plus `InRelease`.
## Logs
Structured file logging defaults to `/var/log/rustproxy/access.jsonl` when enabled
in RustProxy. Logrotate runs daily, keeps at most three days, compresses immediately
with `gzip -9`, and uses `copytruncate` because RustProxy owns the open log file.
Systemd journal output remains independent of this JSONL rotation.
Vendored Executable
+26
View File
@@ -0,0 +1,26 @@
#!/bin/sh
set -eu
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
cd "$ROOT"
for tool in cargo rustc dpkg-buildpackage; do
command -v "$tool" >/dev/null 2>&1 || { echo "Missing build tool: $tool" >&2; exit 2; }
done
RUSTC_VERSION=$(rustc --version | awk '{print $2}')
if ! dpkg --compare-versions "$RUSTC_VERSION" ge 1.88; then
echo "RustProxy requires rustc >= 1.88 (found $RUSTC_VERSION)" >&2
exit 2
fi
# Fetch against Cargo.lock before the Debian build. Use CARGO_NET_OFFLINE=true
# when the registry/cache has already been prepared or dependencies are vendored.
cargo fetch --locked
dpkg-buildpackage -us -uc -b
mkdir -p dist
find .. -maxdepth 1 -type f \( -name 'rustproxy_0.24.5_*.deb' -o -name 'rustproxy_0.24.5_*.changes' -o -name 'rustproxy_0.24.5_*.buildinfo' \) \
-exec cp -f {} dist/ \;
echo "Packages copied to $ROOT/dist"
Vendored Executable
+44
View File
@@ -0,0 +1,44 @@
#!/bin/sh
set -eu
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
REPO=${1:-"$ROOT/repository"}
DEBS=${2:-"$ROOT/dist"}
ARCH=${RUSTPROXY_REPO_ARCH:-$(dpkg --print-architecture)}
CODENAME=${RUSTPROXY_REPO_CODENAME:-stable}
COMPONENT=${RUSTPROXY_REPO_COMPONENT:-main}
for tool in dpkg-scanpackages apt-ftparchive gzip; do
command -v "$tool" >/dev/null 2>&1 || { echo "Missing repository tool: $tool" >&2; exit 2; }
done
POOL="$REPO/pool/$COMPONENT/r/rustproxy"
BINARY="$REPO/dists/$CODENAME/$COMPONENT/binary-$ARCH"
mkdir -p "$POOL" "$BINARY"
find "$DEBS" -maxdepth 1 -type f -name 'rustproxy_*.deb' -exec cp -f {} "$POOL/" \;
cd "$REPO"
dpkg-scanpackages --arch "$ARCH" "pool/$COMPONENT" /dev/null > "dists/$CODENAME/$COMPONENT/binary-$ARCH/Packages"
gzip -9 -n -c "dists/$CODENAME/$COMPONENT/binary-$ARCH/Packages" > "dists/$CODENAME/$COMPONENT/binary-$ARCH/Packages.gz"
apt-ftparchive \
-o APT::FTPArchive::Release::Origin=RustProxy \
-o APT::FTPArchive::Release::Label=RustProxy \
-o APT::FTPArchive::Release::Suite="$CODENAME" \
-o APT::FTPArchive::Release::Codename="$CODENAME" \
-o APT::FTPArchive::Release::Architectures="$ARCH" \
-o APT::FTPArchive::Release::Components="$COMPONENT" \
release "dists/$CODENAME" > "dists/$CODENAME/Release"
if [ -n "${RUSTPROXY_REPO_GPG_KEY:-}" ]; then
command -v gpg >/dev/null 2>&1 || { echo "RUSTPROXY_REPO_GPG_KEY is set but gpg is missing" >&2; exit 2; }
gpg --batch --yes --local-user "$RUSTPROXY_REPO_GPG_KEY" --armor --detach-sign \
--output "dists/$CODENAME/Release.gpg" "dists/$CODENAME/Release"
gpg --batch --yes --local-user "$RUSTPROXY_REPO_GPG_KEY" --clearsign \
--output "dists/$CODENAME/InRelease" "dists/$CODENAME/Release"
else
rm -f "dists/$CODENAME/Release.gpg" "dists/$CODENAME/InRelease"
echo "WARNING: repository is unsigned. Set RUSTPROXY_REPO_GPG_KEY for production." >&2
fi
echo "Repository ready: $REPO"
echo "Publish this directory as https://rustproxy.linuxiarz.pl/"
+197
View File
@@ -0,0 +1,197 @@
rustproxy (0.24.5) stable; urgency=medium
* Fix empty HTTP/3 request-body forwarding to HTTP/1.1 upstreams, including DELETE.
* Preserve streaming for non-empty H3 request bodies and existing H1/H2 behavior.
-- RustProxy Release <release@localhost> Fri, 02 Oct 2026 23:27:00 +0200
rustproxy (0.24.4) stable; urgency=medium
* Disable Transparent Huge Pages locally for the RustProxy Linux process before Tokio startup.
* Keep host THP policy unchanged and retain existing glibc arena/trim tuning.
-- RustProxy Release <release@localhost> Fri, 02 Oct 2026 15:20:00 +0200
rustproxy (0.24.3) stable; urgency=medium
* Reduce glibc allocator retention and transformation-path temporary allocations.
* Move HTTP/3 enablement to Listeners while retaining dedicated QUIC tuning.
* Hide unsupported process diagnostics dynamically in the dashboard.
-- RustProxy Release <release@localhost> Fri, 02 Oct 2026 09:45:00 +0200
rustproxy (0.24.2) stable; urgency=medium
* Version-only release bump; no functional changes from 0.24.1.
-- RustProxy Release <release@localhost> Thu, 01 Oct 2026 22:11:00 +0200
rustproxy (0.24.1) stable; urgency=medium
* Use shared cross-platform statistics APIs and expose more portable metrics.
* Add authenticated runtime path metadata and platform-aware installed-data discovery.
* Move Logging into Settings while retaining legacy navigation compatibility.
-- RustProxy Release <release@localhost> Thu, 01 Oct 2026 21:00:00 +0200
rustproxy (0.24.0) stable; urgency=medium
* Graceful HTTP/2 GOAWAY/drain shutdown with independent idle, lifetime and drain limits.
* Extract system statistics backends and add BSD platform support plus process platform reporting.
-- RustProxy Release <release@localhost> Thu, 01 Oct 2026 18:46:00 +0200
rustproxy (0.23.3) stable; urgency=medium
* Add opt-in per-vhost legacy WebSocket compatibility for older appliances.
* Align legacy WebSocket Origin/Host and tolerate legacy 101 response framing.
-- RustProxy Maintainers <noreply@localhost> Thu, 01 Oct 2026 00:00:00 +0000
rustproxy (0.23.2) stable; urgency=medium
* Add configurable live-chart history windows for Cache and Transformation.
* Fix HTTPS administration transport handling for --livelogs and local CLI API calls.
-- RustProxy Maintainers <root@localhost> Thu, 01 Oct 2026 11:36:19 +0200
rustproxy (0.23.1) stable; urgency=medium
* Add collapsible full-width live Cache and Transform activity charts.
* Update package and administration UI identity to 0.23.1.
-- RustProxy Maintainers <root@localhost> Thu, 01 Oct 2026 11:11:00 +0200
rustproxy (0.23.0) stable; urgency=medium
* Add HTTP/1.1 Upgrade: h2c and external certificate hot-deploy/reload support.
-- RustProxy Maintainers <root@localhost> Thu, 01 Oct 2026 08:30:00 +0200
rustproxy (0.22.2) stable; urgency=medium
* Add replay-safe backend failover retry with bounded policy and metrics.
* Correct explicit HTTP/2 upstream request handling and add H2 regression coverage.
* Add vhost retry controls to the administration UI and OpenAPI schema.
-- RustProxy Maintainers <root@localhost> Thu, 01 Oct 2026 07:10:00 +0200
rustproxy (0.22.1) stable; urgency=medium
* Add HTTP/2 RFC 8441 WebSocket transport and opt-in h2c prior-knowledge listeners.
* Add active tunnel inventory/kill administration with audit and H1/H2/H3 metrics.
* Move selective PURGE/BAN workflows into modal UI and fix null-safe operation feedback.
-- RustProxy Maintainers <root@localhost> Wed, 30 Sep 2026 22:45:00 +0200
rustproxy (0.22.0) stable; urgency=medium
* Add cache request collapsing, single-range HTTP 206 reuse, selective PURGE/BAN,
configurable stale-if-error classes and proactive cache refresh.
* Extend the authenticated cache API, dashboard, metrics and regression coverage.
-- RustProxy Maintainers <root@localhost> Wed, 30 Sep 2026 21:00:00 +0200
rustproxy (0.21.0) stable; urgency=medium
* Add sharded Cold Cache with bounded asynchronous garbage collection.
* Implement RFC 5861 stale handling and request max-stale semantics.
* Add per-vhost static-response transformations before cache/compression.
-- RustProxy Maintainers <root@localhost> Tue, 29 Sep 2026 21:25:00 +0200
rustproxy (0.20.0) stable; urgency=medium
* Split Certificates into Installed, Renewal and Generate tabs.
* Improve ACME renewal status and failed-operation messages.
* Show exact private Certbot log directories for certificate failures.
-- RustProxy Maintainers <root@localhost> Tue, 29 Sep 2026 12:14:00 +0200
rustproxy (0.19.4) stable; urgency=medium
* Align Docker-vhost Aliases with neighboring form controls.
* Preserve sub-0.1 KiB/s backend traffic rendering.
* Add cache-domain filtering by effective cache state.
-- RustProxy Maintainers <root@localhost> Tue, 29 Sep 2026 11:30:00 +0200
rustproxy (0.19.3) stable; urgency=medium
* Preserve exact Settings location across re-login and browser refresh.
* Add shared saving state, field-focused validation and unsaved-tab markers.
* Keep certificate import file selections after failed saves.
-- RustProxy Maintainers <root@localhost> Tue, 29 Sep 2026 09:32:00 +0200
rustproxy (0.19.2) stable; urgency=medium
* Improve default-response status labels and Live-log buffering.
* Polish checkbox/alias layout and backend rate formatting.
* Keep local pages non-cacheable and allow guarded vhost-ID renames.
-- RustProxy Maintainers <root@localhost> Mon, 28 Sep 2026 23:26:00 +0200
rustproxy (0.19.1) stable; urgency=medium
* Fix Default responses saves and isolate cache objects by frontend scheme.
* Retain on-demand Live Logs history and polish traffic/checkbox/Docker Advanced UI.
* Remove stale screenshots and update documentation/release contracts.
-- RustProxy Maintainers <root@localhost> Mon, 28 Sep 2026 22:14:00 +0200
rustproxy (0.19.0) stable; urgency=medium
* Persist per-admin Live Logs column visibility in SQLite while keeping full JSON exports.
* Fix disabled backend TLS verification for legacy X.509 certificates and retain detailed upstream diagnostics.
* Remove the duplicate version label from the top-right dashboard toolbar.
-- RustProxy Maintainers <root@localhost> Mon, 28 Sep 2026 14:55:00 +0200
rustproxy (0.18.8) stable; urgency=medium
* Add Docker-vhost alias chips plus redirect, WebSocket and cache controls.
* Add a Docker-agent default ACL profile and remove the Overview codec explainer.
-- RustProxy Maintainers <root@localhost> Mon, 28 Sep 2026 12:43:00 +0200
rustproxy (0.18.7) stable; urgency=medium
* Polish Overview network charts and collapse response-time history by default.
* Align expanded Docker-vhost routing and ACL controls.
-- RustProxy Maintainers <root@localhost> Mon, 28 Sep 2026 12:05:00 +0200
rustproxy (0.18.6) stable; urgency=medium
* Fix compression/protocol telemetry and improve Docker/backend dashboard UX.
* Remove the Open proxy shortcut and make Compact density materially tighter.
-- RustProxy Maintainers <root@localhost> Mon, 28 Sep 2026 10:36:37 +0200
rustproxy (0.18.4) stable; urgency=medium
* Default backend health checks to TCP and streamline Docker vhost/Security UX.
* Move administration audit to an on-demand modal.
-- RustProxy Maintainers <root@localhost> Mon, 28 Sep 2026 09:45:00 +0200
rustproxy (0.18.3) stable; urgency=medium
* Separate Docker vhost/backend Prometheus metric families and collector group.
-- RustProxy Maintainers <root@localhost> Sun, 27 Sep 2026 17:45:00 +0200
rustproxy (0.18.2) stable; urgency=medium
* Add optional Docker-agent discovery and dynamic Docker vhost assignments.
* Remove revision-header exposure and refresh dashboard/settings UX.
-- RustProxy package maintainer <packages@rustproxy.linuxiarz.pl> Sun, 27 Sep 2026 15:06:00 +0200
rustproxy (0.17.0) stable; urgency=medium
* Package RustProxy 0.17.0 with a hardened systemd service, log rotation,
Prometheus/Grafana assets and local-response host support.
-- RustProxy package maintainer <packages@rustproxy.linuxiarz.pl> Sat, 27 Sep 2026 11:35:00 +0200
+16
View File
@@ -0,0 +1,16 @@
Source: rustproxy
Section: net
Priority: optional
Maintainer: RustProxy package maintainer <packages@rustproxy.linuxiarz.pl>
Build-Depends: debhelper-compat (= 13), cargo, rustc (>= 1.88), pkg-config, libsqlite3-dev
Standards-Version: 4.7.0
Rules-Requires-Root: no
Homepage: https://rustproxy.linuxiarz.pl
Package: rustproxy
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}, adduser, ca-certificates, logrotate, gzip
Description: independent Rust reverse proxy with local administration
RustProxy is an independent reverse proxy with HTTP/1.1, HTTP/2, HTTP/3,
WebSocket, TLS, caching, redirects, local response hosts, TCP/UDP streams,
diagnostics and a local administration dashboard.
Vendored Executable
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/make -f
export DEB_BUILD_MAINT_OPTIONS = hardening=+all
export CARGO_TERM_COLOR = never
%:
dh $@
override_dh_auto_configure:
# Cargo is the authoritative build system; no generated configure step.
override_dh_auto_build:
cargo build --release --locked
override_dh_auto_test:
ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
cargo test --locked
endif
override_dh_auto_install:
install -D -m0755 target/release/rust-proxy debian/rustproxy/usr/bin/rust-proxy
install -D -m0640 debian/rustproxy.environment debian/rustproxy/etc/rustproxy/environment
install -D -m0644 grafana/rustproxy-overview.json debian/rustproxy/usr/share/rustproxy/grafana/rustproxy-overview.json
install -D -m0644 grafana/README.md debian/rustproxy/usr/share/doc/rustproxy/grafana/README.md
+7
View File
@@ -0,0 +1,7 @@
# RustProxy Debian package defaults.
# systemctl restart rustproxy after changing listener environment values.
RUST_LOG=rust_proxy=info,warn
RUST_PROXY_DATA_DIR=/var/lib/rustproxy
RUST_PROXY_ADMIN_LISTEN=127.0.0.1:9090
RUST_PROXY_PROXY_LISTEN=0.0.0.0:80
RUST_PROXY_TLS_LISTEN=0.0.0.0:443
+15
View File
@@ -0,0 +1,15 @@
/var/log/rustproxy/*.jsonl /var/log/rustproxy/*.log /var/log/rustproxy/vhosts/*.jsonl {
daily
rotate 3
maxage 3
missingok
notifempty
compress
compresscmd /usr/bin/gzip
compressext .gz
compressoptions -9
dateext
dateformat -%Y%m%d
copytruncate
su rustproxy rustproxy
}
Vendored Executable
+14
View File
@@ -0,0 +1,14 @@
#!/bin/sh
set -e
if ! getent group rustproxy >/dev/null 2>&1; then
addgroup --system rustproxy >/dev/null
fi
if ! getent passwd rustproxy >/dev/null 2>&1; then
adduser --system --ingroup rustproxy --home /var/lib/rustproxy --no-create-home \
--disabled-password --shell /usr/sbin/nologin rustproxy >/dev/null
fi
install -d -o rustproxy -g rustproxy -m 0750 /var/lib/rustproxy /var/log/rustproxy
#DEBHELPER#
exit 0
+55
View File
@@ -0,0 +1,55 @@
[Unit]
Description=RustProxy reverse proxy
Documentation=https://rustproxy.linuxiarz.pl
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=rustproxy
Group=rustproxy
WorkingDirectory=/var/lib/rustproxy
Environment=MALLOC_ARENA_MAX=2
EnvironmentFile=-/etc/rustproxy/environment
ExecStart=/usr/bin/rust-proxy serve
Restart=on-failure
RestartSec=3s
KillSignal=SIGINT
KillMode=control-group
TimeoutStopSec=120s
UMask=0027
# Bind 80/443 without running the service as root.
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
# Runtime filesystem policy. Diagnostics intentionally retain normal /proc access.
StateDirectory=rustproxy
StateDirectoryMode=0750
LogsDirectory=rustproxy
LogsDirectoryMode=0750
RuntimeDirectory=rustproxy
RuntimeDirectoryMode=0750
ReadWritePaths=/var/lib/rustproxy /var/log/rustproxy
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
LockPersonality=true
RestrictRealtime=true
RestrictSUIDSGID=true
RestrictNamespaces=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
SystemCallArchitectures=native
# High enough for many frontend/upstream sockets without removing all limits.
LimitNOFILE=65536
TasksMax=4096
[Install]
WantedBy=multi-user.target
+2
View File
@@ -0,0 +1,2 @@
d /var/lib/rustproxy 0750 rustproxy rustproxy -
d /var/log/rustproxy 0750 rustproxy rustproxy -
+1
View File
@@ -0,0 +1 @@
3.0 (native)
+47
View File
@@ -0,0 +1,47 @@
# Example Linux service
`rustproxy.service` is an operator-reviewed deployment example, not an installed or
tested service. 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.
Before installation, run the real Rust acceptance suite, build a release executable,
create the unprivileged account/group through your normal system administration
process and install the executable at `/opt/rustproxy/rust-proxy`. The service
manager creates `/var/lib/rustproxy` as private writable state; the actual private
data directory is `/var/lib/rustproxy`. `LogsDirectory=rustproxy` provisions
`/var/log/rustproxy` for the JSONL access-log sink while the rest of the filesystem
remains protected by `ProtectSystem=strict`. The service enables 80/443 startup
defaults, does not start a demo backend and uses a self-signed fallback certificate
until trusted certificates are imported. Firewall, DNS and trust configuration are
separate operator responsibilities.
Existing SQLite listener overrides take precedence over these startup defaults.
Review Settings before moving an old database into this service. Public addresses
are deliberately explicit in the example. Customize the unit for other paths,
addresses, limits or a service-account convention. Never point it at a user-owned
source directory containing writable executables or real public web content.
After starting through your service manager, run account/certificate CLI operations
as the same service account with `--data-dir /var/lib/rustproxy`. The example
uses `ProtectHome=true`, so certificate files intended for runtime use must be in
the data directory, not in a home directory. Use an SSH tunnel to access the local
admin port remotely. This reference unit deliberately binds administration to `0.0.0.0:9090` with
`--allow-remote-admin`. It is plain HTTP and must be limited by firewall/private-network
policy. The normal installer remains loopback-safe unless that flag is explicitly supplied. For a systemd drop-in, reset `ExecStart`
before replacing it:
```ini
[Service]
ExecStart=
ExecStart=/opt/rustproxy/current/rust-proxy --data-dir /var/lib/rustproxy --admin-listen 0.0.0.0:9090 serve --proxy-listen 0.0.0.0:80 --tls-listen 0.0.0.0:443 --allow-remote-admin
ReadWritePaths=/var/log/rustproxy
```
Saving HTTP/TLS or TCP/UDP stream listener changes in the UI does not restart this service. Restart explicitly
only after checking permission, free ports and certificate readiness. A bad saved
bind can prevent startup; preserve a pre-change database backup and consult
`docs/MIGRATION.md` for recovery. Verify the unit on the target system with
`systemd-analyze verify` and exercise actual non-root binding before accepting a
production deployment. This packaging environment did not run systemd or bind
privileged ports through this unit.
+40
View File
@@ -0,0 +1,40 @@
# Reference for install.sh defaults; the installer generates paths from its arguments.
[Unit]
Description=RustProxy reverse proxy
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=rustproxy
Group=rustproxy
WorkingDirectory=/var/lib/rustproxy
UMask=0077
Environment=RUST_LOG=rust_proxy=info,warn
Environment=MALLOC_ARENA_MAX=2
Environment=PATH=/opt/rustproxy-certbot/bin:/usr/local/bin:/usr/bin:/bin
EnvironmentFile=-/etc/rustproxy/environment
ExecStart=/opt/rustproxy/current/rust-proxy --data-dir /var/lib/rustproxy --admin-listen 0.0.0.0:9090 serve --proxy-listen 0.0.0.0:80 --tls-listen 0.0.0.0:443 --allow-remote-admin
Restart=on-failure
RestartSec=3
KillSignal=SIGINT
KillMode=control-group
TimeoutStopSec=120
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
LogsDirectory=rustproxy
LogsDirectoryMode=0770
ReadWritePaths=/var/lib/rustproxy /var/log/rustproxy
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
LockPersonality=true
[Install]
WantedBy=multi-user.target
Executable
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT"
umask 077
if [[ -f .env ]]; then
# This is a local shell configuration file. Do not use untrusted .env files.
set -a
# shellcheck disable=SC1091
source .env
set +a
fi
if ! command -v cargo >/dev/null 2>&1 && [[ -x "$HOME/.cargo/bin/cargo" ]]; then
export PATH="$HOME/.cargo/bin:$PATH"
fi
mode="${1:-run}"
if [[ $# -gt 0 ]]; then shift; fi
case "$mode" in
help|-h|--help)
cat <<'HELP'
RustProxy 0.24.5
./dev.sh Build and start proxy + local panel
./dev.sh run [serve flags] Same, with extra server options
./dev.sh admin setup-token Generate/print a one-time setup capability before first login
./dev.sh admin init --username NAME
Create the sole administrator (hidden password prompt)
./dev.sh admin reset-password
Recover the password and revoke all existing sessions
./dev.sh admin logout-all Revoke sessions without changing the password
./dev.sh admin status Show administrator setup status
./dev.sh tls generate --name app --hosts app.localhost,alias.localhost
Create a named self-signed certificate (no overwrite)
./dev.sh tls import --name app --cert fullchain.pem --key privkey.pem
Validate and import a PEM pair (private key mode 600)
./dev.sh tls deploy --id app --cert /etc/letsencrypt/live/app/fullchain.pem --key /etc/letsencrypt/live/app/privkey.pem
Deploy an externally issued pair to an existing manual certificate id
./dev.sh tls reload Re-read and hot-apply configured certificate files without changing config revision
./dev.sh config reload Revalidate the current runtime configuration
./dev.sh repair Preserve known legacy files; no compilation required
./dev.sh doctor Check source layout and show available build tools
./dev.sh token Compatibility alias for admin setup-token
./dev.sh config get Export the running desired config as JSON
./dev.sh config apply FILE Validate, persist and hot-apply a JSON document
./dev.sh config validate FILE
Validate only; works without a running server
./dev.sh backup DIRECTORY Back up SQLite and configured certificates using Python 3
./dev.sh mock [--port 9001] Start a random-data HTTP origin (Python only; Ctrl+C to stop)
./dev.sh mock-auth [--port 9300]
Start a LOOPBACK-ONLY fake Tinyauth peer (never production)
./dev.sh acme --doctor Check local Certbot availability without network access
./dev.sh acme --cert-dir DIR Read provider request JSON from stdin; stage PEM files
./dev.sh test Run offline release checks and all Rust tests
./dev.sh build Build the development binary only
./dev.sh release Build an optimized binary (target/release/rust-proxy)
./dev.sh fmt Format Rust sources with rustfmt
./dev.sh check Cargo check for all targets
Defaults: HTTP 127.0.0.1:8080, HTTPS 127.0.0.1:8443, panel 127.0.0.1:9090.
The optional diagnostic origin uses 127.0.0.1:9000 and is OFF by default.
Set RUST_PROXY_TLS=0 to disable the TLS listener. Default certificate is self-signed.
Set RUST_PROXY_DEMO=1 to explicitly start a diagnostic origin and seed a NEW database.
Configuration options: .env.example, README.md, docs/API.md, ./dev.sh run --help.
HELP
exit 0 ;;
run|admin|token|config|tls|repair|doctor|backup|test|build|release|fmt|check|mock|mock-auth|acme) ;;
*) printf 'Unknown command: %s. Use ./dev.sh help\n' "$mode" >&2; exit 2 ;;
esac
if [[ "$mode" == "backup" ]]; then
exec python3 scripts/backup.py --data-dir "${RUST_PROXY_DATA_DIR:-$ROOT/data}" "$@"
fi
if ! command -v python3 >/dev/null 2>&1; then
printf 'Python 3.10+ is required for source preflight and upgrade safety checks.\n' >&2
exit 127
fi
python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3, 10) else "Python 3.10+ is required.")'
if [[ "$mode" == "doctor" ]]; then
python3 scripts/preflight.py
printf 'Source layout: OK\n'
for tool in cargo rustc cc python3; do
printf '%s: ' "$tool"
command -v "$tool" || printf 'NOT FOUND\n'
done
exit 0
fi
if [[ "$mode" == "acme" ]]; then exec python3 scripts/acme_provider.py "$@"; fi
if [[ "$mode" == "mock-auth" ]]; then exec python3 scripts/mock_auth.py "$@"; fi
if [[ "$mode" == "mock" ]]; then exec python3 scripts/mock_server.py "$@"; fi
python3 scripts/preflight.py --repair-known
if [[ "$mode" == "repair" ]]; then printf 'Source preflight: OK\n'; exit 0; fi
if [[ "$mode" == "test" ]]; then
python3 scripts/check_source.py
python3 scripts/release_smoke.py
fi
if ! command -v cargo >/dev/null 2>&1; then
cat >&2 <<'MISSING'
Rust/Cargo is not installed or not in PATH.
Install the stable Rust toolchain using https://rustup.rs, then open a new terminal.
Linux also needs a C compiler/linker (for example: build-essential or gcc).
macOS: install the Xcode command line tools. Windows: use WSL2 + Bash.
This script does not install system software or run sudo automatically.
MISSING
exit 127
fi
export RUST_PROXY_DATA_DIR="${RUST_PROXY_DATA_DIR:-$ROOT/data}"
export RUST_PROXY_PROXY_LISTEN="${RUST_PROXY_PROXY_LISTEN:-127.0.0.1:8080}"
export RUST_PROXY_ADMIN_LISTEN="${RUST_PROXY_ADMIN_LISTEN:-127.0.0.1:9090}"
if [[ "${RUST_PROXY_TLS:-1}" == "1" ]]; then
export RUST_PROXY_TLS_LISTEN="${RUST_PROXY_TLS_LISTEN:-127.0.0.1:8443}"
else
unset RUST_PROXY_TLS_LISTEN
fi
export RUST_PROXY_DEMO_LISTEN="${RUST_PROXY_DEMO_LISTEN:-127.0.0.1:9000}"
export RUST_LOG="${RUST_LOG:-rust_proxy=info,warn}"
export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$ROOT/target}"
if [[ "$CARGO_TARGET_DIR" != /* ]]; then export CARGO_TARGET_DIR="$ROOT/$CARGO_TARGET_DIR"; fi
if [[ "$mode" == "fmt" ]]; then exec cargo fmt --all "$@"; fi
if [[ ! -f Cargo.lock ]]; then
printf 'Resolving dependencies once; creating Cargo.lock (keep it for future builds).\n' >&2
cargo generate-lockfile
fi
case "$mode" in
check) exec cargo check --locked --all-targets "$@" ;;
release) exec cargo build --locked --release "$@" ;;
esac
printf 'Building RustProxy 0.24.5...\n' >&2
cargo build --locked
BIN="$CARGO_TARGET_DIR/debug/rust-proxy"
case "$mode" in
build) printf 'Binary: %s\n' "$BIN" ;;
token) exec "$BIN" token "$@" ;;
admin) exec "$BIN" admin "$@" ;;
config) exec "$BIN" config "$@" ;;
tls) exec "$BIN" tls "$@" ;;
test)
cargo test --locked --all-targets ;;
run)
args=(serve)
if [[ "${RUST_PROXY_DEMO:-0}" == "1" ]]; then args+=(--demo); fi
exec "$BIN" "${args[@]}" "$@" ;;
esac
+46
View File
@@ -0,0 +1,46 @@
# RustProxy Docker Agent
Small read-only discovery service for Docker hosts. It talks to the local Docker Engine Unix socket, reports running containers with non-loopback published TCP ports, preserves every host binding for each service, and exposes the inventory to RustProxy through a Bearer-token protected HTTPS endpoint.
The inventory also reports multiple usable host addresses: active host interface addresses, the primary routed address, an optional public-IP lookup result, and the optional advertised/preferred address. Loopback/link-local and obvious Docker-generated bridge/veth interfaces are omitted from the host-address list.
## TLS by default
HTTPS is enabled by default. On first start the agent creates a persistent ECDSA P-256 self-signed certificate under `/var/lib/rustproxy-docker-agent/` and reuses it across restarts. The private key is written with mode `0600`.
The installer prints the certificate SHA-256 fingerprint. Configure that fingerprint in **RustProxy -> Settings -> Docker agents**. RustProxy then pins the exact agent certificate while still verifying the TLS handshake signatures. A changed/replaced certificate is rejected until the configured fingerprint is updated.
Show the current fingerprint again with:
```bash
sudo rustproxy-docker-agent --print-fingerprint
```
If you replace the generated certificate with one signed by a normal public CA, the fingerprint field in RustProxy may be left empty so standard CA/hostname validation is used. Plain HTTP is available only when `RUSTPROXY_DOCKER_AGENT_TLS=false`; it is not the default.
## Quick install on systemd
```bash
sudo ./install.sh
```
The installer runs tests, builds the binary, creates the service account, writes a random token on first install, installs the hardened unit, starts it and prints the TLS fingerprint.
## Build manually
```bash
go test ./...
go build -trimpath -ldflags='-s -w' -o rustproxy-docker-agent .
```
## systemd
1. Create a dedicated `rustproxy-docker-agent` system user and grant it read access to the Docker socket (commonly by membership in the `docker` group). Docker socket access is privileged; isolate this account and the host firewall.
2. Copy the binary to `/usr/local/bin/rustproxy-docker-agent`.
3. Copy `docker-agent.env.example` to `/etc/rustproxy/docker-agent.env`, generate a long random token and bind only to an address reachable by RustProxy.
4. Ensure `/var/lib/rustproxy-docker-agent` is writable only by the service identity. The shipped systemd unit creates it through `StateDirectory=`.
5. Copy `rustproxy-docker-agent.service` to `/etc/systemd/system/`, then run `systemctl daemon-reload && systemctl enable --now rustproxy-docker-agent`.
The inventory endpoint is `GET /v1/inventory` with `Authorization: Bearer <token>`. `/healthz` does not expose Docker inventory. Both are served over HTTPS by default.
`RUSTPROXY_DOCKER_ADVERTISE_IP` marks a preferred address but does not suppress automatic discovery of the host's other addresses. `RUSTPROXY_DOCKER_PUBLIC_IP_URL` adds the externally observed address when enabled. RustProxy can choose a concrete reported address independently for every Docker-backed vhost.
+10
View File
@@ -0,0 +1,10 @@
RUSTPROXY_DOCKER_AGENT_LISTEN=0.0.0.0:9876
RUSTPROXY_DOCKER_AGENT_TOKEN=replace-with-at-least-24-random-characters
# HTTPS is enabled by default. The agent creates and reuses a self-signed certificate.
RUSTPROXY_DOCKER_AGENT_TLS=true
RUSTPROXY_DOCKER_AGENT_CERT_FILE=/var/lib/rustproxy-docker-agent/agent.crt
RUSTPROXY_DOCKER_AGENT_KEY_FILE=/var/lib/rustproxy-docker-agent/agent.key
# Optional preferred address. Other usable host addresses are still auto-detected and reported.
RUSTPROXY_DOCKER_ADVERTISE_IP=
# Optional external/public address lookup. Set empty to disable.
RUSTPROXY_DOCKER_PUBLIC_IP_URL=https://api.ipify.org
+3
View File
@@ -0,0 +1,3 @@
module rustproxy/docker-agent
go 1.22
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ ${EUID:-$(id -u)} -ne 0 ]]; then
echo "Run as root: sudo ./install.sh" >&2
exit 2
fi
command -v go >/dev/null 2>&1 || { echo "Go is required to build the agent." >&2; exit 2; }
command -v systemctl >/dev/null 2>&1 || { echo "systemd is required." >&2; exit 2; }
getent group docker >/dev/null 2>&1 || { echo "Docker group not found; install Docker first." >&2; exit 2; }
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
cd "$ROOT"
go test ./...
go build -trimpath -ldflags='-s -w' -o "$TMP/rustproxy-docker-agent" .
if ! id rustproxy-docker-agent >/dev/null 2>&1; then
useradd --system --no-create-home --shell /usr/sbin/nologin --gid docker rustproxy-docker-agent
else
usermod -a -G docker rustproxy-docker-agent
fi
install -m 0755 "$TMP/rustproxy-docker-agent" /usr/local/bin/rustproxy-docker-agent
install -d -m 0750 /etc/rustproxy
install -d -o rustproxy-docker-agent -g docker -m 0750 /var/lib/rustproxy-docker-agent
if [[ ! -f /etc/rustproxy/docker-agent.env ]]; then
install -m 0640 docker-agent.env.example /etc/rustproxy/docker-agent.env
TOKEN=$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')
sed -i "s/replace-with-at-least-24-random-characters/$TOKEN/" /etc/rustproxy/docker-agent.env
echo "Created /etc/rustproxy/docker-agent.env with a random token."
fi
install -m 0644 rustproxy-docker-agent.service /etc/systemd/system/rustproxy-docker-agent.service
systemctl daemon-reload
systemctl enable --now rustproxy-docker-agent.service
systemctl --no-pager --full status rustproxy-docker-agent.service || true
CERT_FILE=/var/lib/rustproxy-docker-agent/agent.crt
for _ in $(seq 1 30); do
[[ -s "$CERT_FILE" ]] && break
sleep 0.1
done
if [[ -s "$CERT_FILE" ]]; then
FINGERPRINT=$(/usr/local/bin/rustproxy-docker-agent --cert-file "$CERT_FILE" --print-fingerprint)
printf '\nAgent certificate SHA-256:\n%s\n' "$FINGERPRINT"
else
echo "Agent certificate was not created; inspect journalctl -u rustproxy-docker-agent." >&2
fi
TOKEN=$(sed -n 's/^RUSTPROXY_DOCKER_AGENT_TOKEN=//p' /etc/rustproxy/docker-agent.env | head -n1)
if [[ -n "$TOKEN" ]]; then
printf '\nBearer token:\n%s\n' "$TOKEN"
else
echo "Bearer token is missing from /etc/rustproxy/docker-agent.env." >&2
fi
echo "Installed. Configure RustProxy -> Settings -> Docker agents with the https:// endpoint, bearer token, and certificate fingerprint printed above."
+701
View File
@@ -0,0 +1,701 @@
package main
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"encoding/json"
"encoding/pem"
"errors"
"flag"
"fmt"
"io"
"log"
"math/big"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"syscall"
"time"
)
const version = "0.24.5"
type config struct {
Listen string
Token string
DockerSock string
AdvertiseIP string
PublicIPURL string
Hostname string
TLS bool
CertFile string
KeyFile string
Fingerprint string
}
type dockerPort struct {
IP string `json:"IP"`
PrivatePort uint16 `json:"PrivatePort"`
PublicPort uint16 `json:"PublicPort"`
Type string `json:"Type"`
}
type dockerContainer struct {
ID string `json:"Id"`
Names []string `json:"Names"`
Image string `json:"Image"`
Labels map[string]string `json:"Labels"`
Ports []dockerPort `json:"Ports"`
}
type binding struct {
BindIP string `json:"bind_ip"`
PublicPort uint16 `json:"public_port"`
}
type service struct {
Key string `json:"key"`
TechnicalName string `json:"technical_name"`
ContainerID string `json:"container_id"`
ContainerName string `json:"container_name"`
Image string `json:"image"`
PrivatePort uint16 `json:"private_port"`
Protocol string `json:"protocol"`
Bindings []binding `json:"bindings"`
}
type hostAddress struct {
IP string `json:"ip"`
Source string `json:"source"`
Interface string `json:"interface,omitempty"`
Scope string `json:"scope"`
}
type inventory struct {
AgentVersion string `json:"agent_version"`
Hostname string `json:"hostname"`
PublicIP string `json:"public_ip"`
PublicIPSource string `json:"public_ip_source"`
CertificateSHA256 string `json:"certificate_sha256"`
Addresses []hostAddress `json:"addresses"`
ObservedAt string `json:"observed_at"`
Services []service `json:"services"`
}
type unixRoundTripper struct{ sock string }
func (u unixRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
transport := &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
var d net.Dialer
return d.DialContext(ctx, "unix", u.sock)
},
DisableKeepAlives: false,
}
clone := req.Clone(req.Context())
clone.URL.Scheme = "http"
clone.URL.Host = "docker"
return transport.RoundTrip(clone)
}
func dockerContainers(ctx context.Context, sock string) ([]dockerContainer, error) {
client := &http.Client{Transport: unixRoundTripper{sock: sock}, Timeout: 4 * time.Second}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://docker/containers/json?all=0", nil)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("docker API: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
return nil, fmt.Errorf("docker API returned %s: %s", resp.Status, strings.TrimSpace(string(body)))
}
var items []dockerContainer
dec := json.NewDecoder(io.LimitReader(resp.Body, 4<<20))
if err := dec.Decode(&items); err != nil {
return nil, fmt.Errorf("decode Docker response: %w", err)
}
return items, nil
}
func cleanName(names []string) string {
if len(names) == 0 {
return "container"
}
name := strings.TrimPrefix(names[0], "/")
if name == "" {
return "container"
}
return name
}
func dnsLabel(value string) string {
value = strings.ToLower(value)
var b strings.Builder
dash := false
for _, r := range value {
ok := (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9')
if ok {
b.WriteRune(r)
dash = false
} else if b.Len() > 0 && !dash {
b.WriteByte('-')
dash = true
}
}
out := strings.Trim(b.String(), "-")
if out == "" {
out = "app"
}
if len(out) > 48 {
out = strings.Trim(out[:48], "-")
}
return out
}
func serviceIdentity(c dockerContainer, p dockerPort) (key, technical string) {
name := cleanName(c.Names)
project := c.Labels["com.docker.compose.project"]
composeService := c.Labels["com.docker.compose.service"]
replica := c.Labels["com.docker.compose.container-number"]
suffix := strconv.Itoa(int(p.PrivatePort))
if project != "" && composeService != "" {
if replica == "" {
replica = name
}
key = fmt.Sprintf("compose:%s/%s/%s:%d/%s", project, composeService, replica, p.PrivatePort, p.Type)
technicalBase := project + "-" + composeService
if replica != "1" {
technicalBase += "-" + replica
}
technical = dnsLabel(technicalBase + "-" + suffix)
} else {
key = fmt.Sprintf("container:%s:%d/%s", name, p.PrivatePort, p.Type)
technical = dnsLabel(name + "-" + suffix)
}
return key, technical
}
func validIP(raw string) string {
ip := net.ParseIP(strings.TrimSpace(raw))
if ip == nil || ip.IsUnspecified() || ip.IsLoopback() || ip.IsMulticast() || ip.IsLinkLocalUnicast() {
return ""
}
return ip.String()
}
func servicesFrom(containers []dockerContainer) []service {
byKey := make(map[string]*service)
bindingSeen := make(map[string]map[string]struct{})
for _, c := range containers {
name := cleanName(c.Names)
for _, p := range c.Ports {
if p.PublicPort == 0 || p.Type != "tcp" {
continue
}
if bind := net.ParseIP(strings.TrimSpace(p.IP)); bind != nil && bind.IsLoopback() {
continue
}
key, technical := serviceIdentity(c, p)
item := byKey[key]
if item == nil {
id := c.ID
if len(id) > 12 {
id = id[:12]
}
item = &service{
Key: key, TechnicalName: technical, ContainerID: id, ContainerName: name,
Image: c.Image, PrivatePort: p.PrivatePort, Protocol: p.Type,
}
byKey[key] = item
bindingSeen[key] = make(map[string]struct{})
}
bindIP := strings.TrimSpace(p.IP)
dedupe := bindIP + "|" + strconv.Itoa(int(p.PublicPort))
if _, ok := bindingSeen[key][dedupe]; ok {
continue
}
bindingSeen[key][dedupe] = struct{}{}
item.Bindings = append(item.Bindings, binding{BindIP: bindIP, PublicPort: p.PublicPort})
}
}
out := make([]service, 0, len(byKey))
for _, item := range byKey {
sort.Slice(item.Bindings, func(i, j int) bool {
left := net.ParseIP(strings.TrimSpace(item.Bindings[i].BindIP))
right := net.ParseIP(strings.TrimSpace(item.Bindings[j].BindIP))
leftWildcard := left == nil || left.IsUnspecified()
rightWildcard := right == nil || right.IsUnspecified()
if leftWildcard != rightWildcard {
return !leftWildcard
}
if item.Bindings[i].BindIP != item.Bindings[j].BindIP {
return item.Bindings[i].BindIP < item.Bindings[j].BindIP
}
return item.Bindings[i].PublicPort < item.Bindings[j].PublicPort
})
out = append(out, *item)
}
sort.Slice(out, func(i, j int) bool { return out[i].Key < out[j].Key })
return out
}
func publicIPFromURL(ctx context.Context, rawURL string) string {
if rawURL == "" {
return ""
}
client := &http.Client{Timeout: 2 * time.Second}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
if err != nil {
return ""
}
resp, err := client.Do(req)
if err != nil {
return ""
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return ""
}
body, err := io.ReadAll(io.LimitReader(resp.Body, 128))
if err != nil {
return ""
}
return validIP(string(body))
}
func routeIP() string {
conn, err := net.DialTimeout("udp", "1.1.1.1:53", time.Second)
if err != nil {
return ""
}
defer conn.Close()
if addr, ok := conn.LocalAddr().(*net.UDPAddr); ok {
return validIP(addr.IP.String())
}
return ""
}
func ipScope(ip net.IP) string {
switch {
case ip.IsPrivate():
return "private"
case ip.IsLinkLocalUnicast():
return "link_local"
default:
return "public"
}
}
func skipInterface(name string) bool {
name = strings.ToLower(strings.TrimSpace(name))
return name == "docker0" || strings.HasPrefix(name, "docker") || strings.HasPrefix(name, "veth") || strings.HasPrefix(name, "br-") || strings.HasPrefix(name, "virbr")
}
func localHostAddresses() []hostAddress {
interfaces, err := net.Interfaces()
if err != nil {
return nil
}
out := make([]hostAddress, 0)
for _, iface := range interfaces {
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 || skipInterface(iface.Name) {
continue
}
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
raw := addr.String()
if host, _, err := net.ParseCIDR(raw); err == nil {
raw = host.String()
} else if host, _, err := net.SplitHostPort(raw); err == nil {
raw = host
}
clean := validIP(raw)
if clean == "" {
continue
}
ip := net.ParseIP(clean)
out = append(out, hostAddress{IP: clean, Source: "interface", Interface: iface.Name, Scope: ipScope(ip)})
}
}
sort.Slice(out, func(i, j int) bool {
if out[i].Interface != out[j].Interface {
return out[i].Interface < out[j].Interface
}
return out[i].IP < out[j].IP
})
return out
}
type ipDetector struct {
mu sync.Mutex
publicIP string
expires time.Time
}
func (d *ipDetector) public(ctx context.Context, rawURL string) string {
if rawURL == "" {
return ""
}
d.mu.Lock()
defer d.mu.Unlock()
if time.Now().Before(d.expires) {
return d.publicIP
}
d.publicIP = publicIPFromURL(ctx, rawURL)
d.expires = time.Now().Add(5 * time.Minute)
if d.publicIP == "" {
d.expires = time.Now().Add(30 * time.Second)
}
return d.publicIP
}
func mergeHostAddress(out *[]hostAddress, index map[string]int, candidate hostAddress) {
candidate.IP = validIP(candidate.IP)
if candidate.IP == "" {
return
}
if candidate.Scope == "" {
candidate.Scope = ipScope(net.ParseIP(candidate.IP))
}
if pos, ok := index[candidate.IP]; ok {
existing := &(*out)[pos]
if candidate.Interface != "" && existing.Interface == "" {
existing.Interface = candidate.Interface
}
if candidate.Source != "" && !strings.Contains(existing.Source, candidate.Source) {
if existing.Source == "" {
existing.Source = candidate.Source
} else {
existing.Source += "," + candidate.Source
}
}
return
}
index[candidate.IP] = len(*out)
*out = append(*out, candidate)
}
func detectedAddresses(ctx context.Context, cfg config, detector *ipDetector) ([]hostAddress, string, string) {
out := make([]hostAddress, 0, 8)
index := make(map[string]int)
for _, item := range localHostAddresses() {
mergeHostAddress(&out, index, item)
}
routed := routeIP()
if routed != "" {
mergeHostAddress(&out, index, hostAddress{IP: routed, Source: "route"})
}
public := detector.public(ctx, cfg.PublicIPURL)
if public != "" {
mergeHostAddress(&out, index, hostAddress{IP: public, Source: "public_api"})
}
advertised := validIP(cfg.AdvertiseIP)
if advertised != "" {
mergeHostAddress(&out, index, hostAddress{IP: advertised, Source: "override"})
}
switch {
case advertised != "":
return out, advertised, "override"
case routed != "":
return out, routed, "route"
case public != "":
return out, public, "public_api"
case len(out) > 0:
return out, out[0].IP, out[0].Source
default:
return out, "", "unavailable"
}
}
func certificateFingerprintDER(der []byte) string {
sum := sha256.Sum256(der)
return hex.EncodeToString(sum[:])
}
func certificateFingerprint(certFile string) (string, error) {
raw, err := os.ReadFile(certFile)
if err != nil {
return "", err
}
block, _ := pem.Decode(raw)
if block == nil || block.Type != "CERTIFICATE" {
return "", fmt.Errorf("%s does not contain a PEM certificate", certFile)
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return "", fmt.Errorf("parse certificate: %w", err)
}
return certificateFingerprintDER(cert.Raw), nil
}
func atomicWrite(path string, data []byte, mode os.FileMode) error {
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0750); err != nil {
return err
}
tmp, err := os.CreateTemp(dir, ".rustproxy-agent-*")
if err != nil {
return err
}
tmpName := tmp.Name()
defer os.Remove(tmpName)
if err := tmp.Chmod(mode); err != nil {
tmp.Close()
return err
}
if _, err := tmp.Write(data); err != nil {
tmp.Close()
return err
}
if err := tmp.Sync(); err != nil {
tmp.Close()
return err
}
if err := tmp.Close(); err != nil {
return err
}
return os.Rename(tmpName, path)
}
func ensureSelfSignedCertificate(certFile, keyFile, hostname string) (string, bool, error) {
certInfo, certErr := os.Stat(certFile)
keyInfo, keyErr := os.Stat(keyFile)
if certErr == nil && keyErr == nil {
if certInfo.IsDir() || keyInfo.IsDir() {
return "", false, errors.New("TLS certificate/key path points to a directory")
}
if _, err := tls.LoadX509KeyPair(certFile, keyFile); err != nil {
return "", false, fmt.Errorf("load TLS certificate/key: %w", err)
}
fp, err := certificateFingerprint(certFile)
return fp, false, err
}
if (certErr == nil) != (keyErr == nil) {
return "", false, errors.New("only one TLS certificate/key file exists; refusing to replace the other")
}
if certErr != nil && !os.IsNotExist(certErr) {
return "", false, certErr
}
if keyErr != nil && !os.IsNotExist(keyErr) {
return "", false, keyErr
}
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return "", false, fmt.Errorf("generate TLS key: %w", err)
}
serialLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serial, err := rand.Int(rand.Reader, serialLimit)
if err != nil {
return "", false, fmt.Errorf("generate TLS serial: %w", err)
}
now := time.Now().UTC()
template := &x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{CommonName: hostname, Organization: []string{"RustProxy Docker Agent"}},
NotBefore: now.Add(-5 * time.Minute),
NotAfter: now.AddDate(10, 0, 0),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
if hostname != "" {
if ip := net.ParseIP(hostname); ip != nil {
template.IPAddresses = append(template.IPAddresses, ip)
} else {
template.DNSNames = append(template.DNSNames, hostname)
}
}
der, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey)
if err != nil {
return "", false, fmt.Errorf("create TLS certificate: %w", err)
}
keyDER, err := x509.MarshalPKCS8PrivateKey(privateKey)
if err != nil {
return "", false, fmt.Errorf("marshal TLS key: %w", err)
}
if err := atomicWrite(keyFile, pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}), 0600); err != nil {
return "", false, fmt.Errorf("write TLS key: %w", err)
}
if err := atomicWrite(certFile, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0644); err != nil {
_ = os.Remove(keyFile)
return "", false, fmt.Errorf("write TLS certificate: %w", err)
}
return certificateFingerprintDER(der), true, nil
}
func tokenOK(got, expected string) bool {
if expected == "" || len(got) != len(expected) {
return false
}
return subtle.ConstantTimeCompare([]byte(got), []byte(expected)) == 1
}
func handler(cfg config) http.Handler {
mux := http.NewServeMux()
detector := &ipDetector{}
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
if cfg.Fingerprint != "" {
w.Header().Set("X-RustProxy-Agent-Certificate-SHA256", cfg.Fingerprint)
}
_, _ = io.WriteString(w, `{"status":"ok"}`)
})
mux.HandleFunc("/v1/inventory", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
auth := strings.TrimSpace(r.Header.Get("Authorization"))
const prefix = "Bearer "
if !strings.HasPrefix(auth, prefix) || !tokenOK(strings.TrimSpace(strings.TrimPrefix(auth, prefix)), cfg.Token) {
w.Header().Set("WWW-Authenticate", "Bearer")
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
containers, err := dockerContainers(r.Context(), cfg.DockerSock)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
addresses, primaryIP, primarySource := detectedAddresses(r.Context(), cfg, detector)
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
if cfg.Fingerprint != "" {
w.Header().Set("X-RustProxy-Agent-Certificate-SHA256", cfg.Fingerprint)
}
_ = json.NewEncoder(w).Encode(inventory{
AgentVersion: version, Hostname: cfg.Hostname, PublicIP: primaryIP, PublicIPSource: primarySource, CertificateSHA256: cfg.Fingerprint,
Addresses: addresses, ObservedAt: time.Now().UTC().Format(time.RFC3339), Services: servicesFrom(containers),
})
})
return mux
}
func envOr(name, fallback string) string {
if value := strings.TrimSpace(os.Getenv(name)); value != "" {
return value
}
return fallback
}
func envBool(name string, fallback bool) bool {
raw := strings.TrimSpace(os.Getenv(name))
if raw == "" {
return fallback
}
value, err := strconv.ParseBool(raw)
if err != nil {
return fallback
}
return value
}
func main() {
hostname, _ := os.Hostname()
cfg := config{}
flag.StringVar(&cfg.Listen, "listen", envOr("RUSTPROXY_DOCKER_AGENT_LISTEN", "127.0.0.1:9876"), "listen address")
flag.StringVar(&cfg.Token, "token", envOr("RUSTPROXY_DOCKER_AGENT_TOKEN", ""), "Bearer token (prefer environment variable)")
flag.StringVar(&cfg.DockerSock, "docker-socket", envOr("RUSTPROXY_DOCKER_SOCKET", "/var/run/docker.sock"), "Docker Engine unix socket")
flag.StringVar(&cfg.AdvertiseIP, "advertise-ip", envOr("RUSTPROXY_DOCKER_ADVERTISE_IP", ""), "optional preferred host IP; all usable host addresses are still reported")
flag.StringVar(&cfg.PublicIPURL, "public-ip-url", envOr("RUSTPROXY_DOCKER_PUBLIC_IP_URL", "https://api.ipify.org"), "public IP lookup URL; empty disables")
flag.StringVar(&cfg.Hostname, "hostname", envOr("RUSTPROXY_DOCKER_HOSTNAME", hostname), "reported host name")
flag.BoolVar(&cfg.TLS, "tls", envBool("RUSTPROXY_DOCKER_AGENT_TLS", true), "serve HTTPS; enabled by default")
flag.StringVar(&cfg.CertFile, "cert-file", envOr("RUSTPROXY_DOCKER_AGENT_CERT_FILE", "/var/lib/rustproxy-docker-agent/agent.crt"), "TLS certificate file")
flag.StringVar(&cfg.KeyFile, "key-file", envOr("RUSTPROXY_DOCKER_AGENT_KEY_FILE", "/var/lib/rustproxy-docker-agent/agent.key"), "TLS private key file")
initCert := flag.Bool("init-cert", false, "create the self-signed TLS certificate if missing and exit")
printFingerprint := flag.Bool("print-fingerprint", false, "print SHA-256 certificate fingerprint and exit")
flag.Parse()
if *initCert {
fingerprint, created, err := ensureSelfSignedCertificate(cfg.CertFile, cfg.KeyFile, cfg.Hostname)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", fingerprint)
if created {
log.Printf("created self-signed certificate %s", cfg.CertFile)
}
return
}
if *printFingerprint {
fingerprint, err := certificateFingerprint(cfg.CertFile)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", fingerprint)
return
}
if len(cfg.Token) < 24 {
log.Fatal("RUSTPROXY_DOCKER_AGENT_TOKEN must contain at least 24 characters")
}
if _, err := os.Stat(cfg.DockerSock); err != nil {
log.Fatalf("Docker socket unavailable: %v", err)
}
if _, _, err := net.SplitHostPort(cfg.Listen); err != nil {
log.Fatalf("invalid listen address: %v", err)
}
if cfg.TLS {
fingerprint, created, err := ensureSelfSignedCertificate(cfg.CertFile, cfg.KeyFile, cfg.Hostname)
if err != nil {
log.Fatalf("TLS certificate: %v", err)
}
cfg.Fingerprint = fingerprint
if created {
log.Printf("generated self-signed TLS certificate; SHA-256 %s", fingerprint)
}
}
server := &http.Server{Addr: cfg.Listen, Handler: handler(cfg), ReadHeaderTimeout: 3 * time.Second, IdleTimeout: 30 * time.Second, MaxHeaderBytes: 16 << 10}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
<-ctx.Done()
shutdown, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = server.Shutdown(shutdown)
}()
scheme := "http"
if cfg.TLS {
scheme = "https"
}
log.Printf("RustProxy Docker Agent %s listening on %s://%s", version, scheme, cfg.Listen)
if cfg.Fingerprint != "" {
log.Printf("TLS certificate SHA-256: %s", cfg.Fingerprint)
}
var err error
if cfg.TLS {
err = server.ListenAndServeTLS(cfg.CertFile, cfg.KeyFile)
} else {
err = server.ListenAndServe()
}
if err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}
+245
View File
@@ -0,0 +1,245 @@
package main
import (
"context"
"crypto/tls"
"encoding/json"
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"sync/atomic"
"testing"
"time"
)
func fakeDocker(t *testing.T, payload string) string {
t.Helper()
dir := t.TempDir()
sock := filepath.Join(dir, "docker.sock")
ln, err := net.Listen("unix", sock)
if err != nil {
t.Fatal(err)
}
srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/containers/json" {
t.Fatalf("unexpected path %s", r.URL.Path)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(payload))
})}
go srv.Serve(ln)
t.Cleanup(func() { _ = srv.Close(); _ = os.Remove(sock) })
return sock
}
func TestServicesFromUsesStableComposeIdentity(t *testing.T) {
items := []dockerContainer{{ID: strings.Repeat("a", 64), Names: []string{"/web-1"}, Image: "demo", Labels: map[string]string{"com.docker.compose.project": "lab", "com.docker.compose.service": "web", "com.docker.compose.container-number": "1"}, Ports: []dockerPort{{IP: "0.0.0.0", PrivatePort: 8080, PublicPort: 18080, Type: "tcp"}, {PrivatePort: 53, PublicPort: 1053, Type: "udp"}}}}
got := servicesFrom(items)
if len(got) != 1 {
t.Fatalf("got %d services", len(got))
}
if got[0].Key != "compose:lab/web/1:8080/tcp" || got[0].TechnicalName != "lab-web-8080" {
t.Fatalf("unexpected service %#v", got[0])
}
if len(got[0].Bindings) != 1 || got[0].Bindings[0].PublicPort != 18080 {
t.Fatalf("unexpected bindings %#v", got[0].Bindings)
}
}
func TestServicesFromKeepsComposeReplicasAndAllBindings(t *testing.T) {
labels := func(number string) map[string]string {
return map[string]string{"com.docker.compose.project": "lab", "com.docker.compose.service": "web", "com.docker.compose.container-number": number}
}
items := []dockerContainer{
{ID: "a", Names: []string{"/lab-web-1"}, Labels: labels("1"), Ports: []dockerPort{{IP: "0.0.0.0", PrivatePort: 8080, PublicPort: 18080, Type: "tcp"}, {IP: "::", PrivatePort: 8080, PublicPort: 18080, Type: "tcp"}, {IP: "10.10.0.21", PrivatePort: 8080, PublicPort: 18081, Type: "tcp"}}},
{ID: "b", Names: []string{"/lab-web-2"}, Labels: labels("2"), Ports: []dockerPort{{IP: "0.0.0.0", PrivatePort: 8080, PublicPort: 18082, Type: "tcp"}}},
}
got := servicesFrom(items)
if len(got) != 2 {
t.Fatalf("expected two replicas, got %#v", got)
}
if got[0].Key != "compose:lab/web/1:8080/tcp" || got[1].Key != "compose:lab/web/2:8080/tcp" {
t.Fatalf("unexpected replica keys %#v", got)
}
if len(got[0].Bindings) != 3 {
t.Fatalf("expected all published bindings, got %#v", got[0].Bindings)
}
if got[0].Bindings[0].BindIP != "10.10.0.21" || got[0].Bindings[0].PublicPort != 18081 {
t.Fatalf("concrete binding should be retained, got %#v", got[0].Bindings)
}
}
func TestServicesFromSkipsLoopbackOnlyBindings(t *testing.T) {
items := []dockerContainer{{ID: "abc", Names: []string{"/app"}, Labels: map[string]string{}, Ports: []dockerPort{
{IP: "127.0.0.1", PrivatePort: 8080, PublicPort: 18080, Type: "tcp"},
{IP: "::1", PrivatePort: 8080, PublicPort: 18081, Type: "tcp"},
{IP: "0.0.0.0", PrivatePort: 8082, PublicPort: 18082, Type: "tcp"},
}}}
got := servicesFrom(items)
if len(got) != 1 || len(got[0].Bindings) != 1 || got[0].Bindings[0].PublicPort != 18082 {
t.Fatalf("unexpected public services %#v", got)
}
}
func TestIPDetectorCachesPublicLookup(t *testing.T) {
var calls atomic.Int32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls.Add(1)
_, _ = w.Write([]byte("203.0.113.44"))
}))
defer srv.Close()
detector := &ipDetector{}
for range 2 {
ip := detector.public(context.Background(), srv.URL)
if ip != "203.0.113.44" {
t.Fatalf("unexpected detection %s", ip)
}
}
if calls.Load() != 1 {
t.Fatalf("expected one public lookup, got %d", calls.Load())
}
}
func TestMergeHostAddressKeepsInterfaceAndSources(t *testing.T) {
var got []hostAddress
index := map[string]int{}
mergeHostAddress(&got, index, hostAddress{IP: "10.20.30.40", Source: "interface", Interface: "eno1", Scope: "private"})
mergeHostAddress(&got, index, hostAddress{IP: "10.20.30.40", Source: "route"})
if len(got) != 1 || got[0].Interface != "eno1" || got[0].Source != "interface,route" {
t.Fatalf("unexpected merged address %#v", got)
}
}
func TestInventoryRequiresTokenAndReportsMultipleAddressModel(t *testing.T) {
sock := fakeDocker(t, `[{"Id":"1234567890abcdef","Names":["/app1"],"Image":"example/app","Labels":{},"Ports":[{"IP":"0.0.0.0","PrivatePort":8080,"PublicPort":18080,"Type":"tcp"}]}]`)
cfg := config{Token: strings.Repeat("x", 24), DockerSock: sock, AdvertiseIP: "203.0.113.10", PublicIPURL: "", Hostname: "docker-a"}
srv := httptest.NewServer(handler(cfg))
defer srv.Close()
res, err := http.Get(srv.URL + "/v1/inventory")
if err != nil {
t.Fatal(err)
}
if res.StatusCode != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", res.StatusCode)
}
_ = res.Body.Close()
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, srv.URL+"/v1/inventory", nil)
req.Header.Set("Authorization", "Bearer "+cfg.Token)
client := &http.Client{Timeout: time.Second}
res, err = client.Do(req)
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", res.StatusCode)
}
var got inventory
if err := json.NewDecoder(res.Body).Decode(&got); err != nil {
t.Fatal(err)
}
if got.PublicIP != "203.0.113.10" || got.PublicIPSource != "override" || len(got.Services) != 1 {
t.Fatalf("unexpected inventory %#v", got)
}
found := false
for _, address := range got.Addresses {
if address.IP == "203.0.113.10" && strings.Contains(address.Source, "override") {
found = true
}
}
if !found {
t.Fatalf("advertised address missing from %#v", got.Addresses)
}
}
func TestSkipInterfaceFiltersDockerGeneratedLinksButKeepsHostBridges(t *testing.T) {
for _, name := range []string{"docker0", "docker_gwbridge", "veth1234", "br-123456", "virbr0"} {
if !skipInterface(name) {
t.Fatalf("expected %s to be filtered", name)
}
}
for _, name := range []string{"eno1", "eth0", "br0", "bond0", "wg0"} {
if skipInterface(name) {
t.Fatalf("expected %s to remain", name)
}
}
}
func TestSelfSignedCertificateIsCreatedOnceAndFingerprintStable(t *testing.T) {
dir := t.TempDir()
certFile := filepath.Join(dir, "agent.crt")
keyFile := filepath.Join(dir, "agent.key")
first, created, err := ensureSelfSignedCertificate(certFile, keyFile, "docker-a.lab.local")
if err != nil {
t.Fatal(err)
}
if !created || len(first) != 64 {
t.Fatalf("unexpected initial certificate result created=%v fingerprint=%q", created, first)
}
second, createdAgain, err := ensureSelfSignedCertificate(certFile, keyFile, "docker-a.lab.local")
if err != nil {
t.Fatal(err)
}
if createdAgain || second != first {
t.Fatalf("certificate should be stable: first=%q second=%q created=%v", first, second, createdAgain)
}
if mode := fileMode(t, keyFile); mode.Perm() != 0600 {
t.Fatalf("private key mode = %o, want 600", mode.Perm())
}
if mode := fileMode(t, certFile); mode.Perm() != 0644 {
t.Fatalf("certificate mode = %o, want 644", mode.Perm())
}
}
func fileMode(t *testing.T, path string) os.FileMode {
t.Helper()
info, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
return info.Mode()
}
func TestTLSInventoryReportsServingCertificateFingerprint(t *testing.T) {
sock := fakeDocker(t, `[{"Id":"abc","Names":["/app"],"Image":"demo","Labels":{},"Ports":[{"IP":"0.0.0.0","PrivatePort":8080,"PublicPort":18080,"Type":"tcp"}]}]`)
dir := t.TempDir()
certFile := filepath.Join(dir, "agent.crt")
keyFile := filepath.Join(dir, "agent.key")
fingerprint, _, err := ensureSelfSignedCertificate(certFile, keyFile, "localhost")
if err != nil {
t.Fatal(err)
}
cfg := config{Token: strings.Repeat("z", 24), DockerSock: sock, Hostname: "docker-tls", TLS: true, CertFile: certFile, KeyFile: keyFile, Fingerprint: fingerprint}
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
server := &http.Server{Handler: handler(cfg)}
go func() { _ = server.ServeTLS(listener, certFile, keyFile) }()
t.Cleanup(func() { _ = server.Close() })
transport := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} // test-only: verify the endpoint is actually TLS; RustProxy pins it in production.
client := &http.Client{Transport: transport, Timeout: time.Second}
req, _ := http.NewRequest(http.MethodGet, "https://"+listener.Addr().String()+"/v1/inventory", nil)
req.Header.Set("Authorization", "Bearer "+cfg.Token)
res, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
if got := res.Header.Get("X-RustProxy-Agent-Certificate-SHA256"); got != fingerprint {
t.Fatalf("certificate header = %q, want %q", got, fingerprint)
}
var got inventory
if err := json.NewDecoder(res.Body).Decode(&got); err != nil {
t.Fatal(err)
}
if got.CertificateSHA256 != fingerprint {
t.Fatalf("inventory fingerprint = %q, want %q", got.CertificateSHA256, fingerprint)
}
}
@@ -0,0 +1,35 @@
[Unit]
Description=RustProxy Docker discovery agent
After=docker.service network-online.target
Wants=network-online.target
Requires=docker.service
[Service]
Type=simple
User=rustproxy-docker-agent
Group=docker
EnvironmentFile=/etc/rustproxy/docker-agent.env
StateDirectory=rustproxy-docker-agent
StateDirectoryMode=0750
UMask=0027
ExecStart=/usr/local/bin/rustproxy-docker-agent
Restart=on-failure
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
ProtectClock=true
ProtectKernelLogs=true
RestrictSUIDSGID=true
RestrictNamespaces=true
LockPersonality=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
SystemCallArchitectures=native
[Install]
WantedBy=multi-user.target
+128
View File
@@ -0,0 +1,128 @@
> 0.7 update: configured managed certificates now have automatic renewal and
> validated hot deployment. Initial issuance/import remains explicit. Read
> [RENEWAL.md](RENEWAL.md) for defaults, retained profiles and opt-out.
# Certificate providers (0.6.0)
Source implementation; real provider/DNS/CA issuance and Rust runtime have not been
verified in the packaging environment. Offline tests substitute Certbot, so they
cannot establish that a DNS token, zone policy or CA deployment works.
## Workflow
Open Certificates -> Add certificate. Choose one provider or import mode, enter
the domains and metadata, then request issuance. Production is selected by default
in the panel and still requires the separate explicit production confirmation. Staging
remains available for testing and its certificates are not browser-trusted.
After a successful job, import its validated pair into the library, then assign the
certificate to a vhost. No CA operation occurs on startup or merely opening a form.
| Selection | Mechanism and required configuration |
|---|---|
| HTTP-01 / domain validation | Certbot webroot; domain names and a publicly reachable plaintext HTTP listener |
| Cloudflare | DNS-01; scoped API token; `certbot-dns-cloudflare` installed |
| OVH | DNS-01; `ovh-eu` or `ovh-ca`, application key/secret, consumer key; `certbot-dns-ovh` installed |
| RFC2136 | DNS-01; server/port, TSIG name/secret/algorithm; `certbot-dns-rfc2136` installed |
| PEM import | Upload fullchain/certificate and corresponding private key, then validate |
| Existing server files | Advanced: reference a PEM pair already in the certificate directory |
Python 3 and Certbot must be executable by the RustProxy service user. Install
plugins in that SAME Certbot environment. The application does not install packages
or execute sudo. Check the CLI installation with `bash dev.sh acme --doctor` and
`certbot plugins`; this does not validate provider credentials or DNS reachability.
DNS uses the API/plugin appropriate to the selected provider, not a Cloudflare
Tunnel integration or a hand-written replacement for ACME.
## HTTP-01 requirements and scope
Public TCP port **80** for every requested name must reach an active plaintext
RustProxy listener. NAT from public 80 to a private/custom HTTP port is allowed;
setting only port 8080 in public DNS is not sufficient. Check all published A/AAAA
addresses and multi-node routing. TLS-only listeners are not enough. There is no
TLS-ALPN-01 mode. This implementation accepts DNS names, not IP certificates.
Wildcard certificates require one of the DNS-01 providers, not HTTP-01.
During a running HTTP-01 job, RustProxy serves only
`/.well-known/acme-challenge/<token>` for that job's exact normalized hostnames.
Only GET/HEAD and bounded regular files from `certs/acme-http01` are accepted;
queries, traversal, unrelated names, HTTPS requests and inactive jobs cannot read
these files. The reserved path precedes application auth/cache/static pages so a
CA can validate ownership without logging in. This exception does not bypass
application authentication for any other path. Responses are no-store.
A CDN or upstream proxy must forward this path and the correct Host without
rewriting it. There is no automatic forwarding configuration at your DNS/CDN vendor.
Expired challenge jobs do not leave an open generic file server.
## DNS credentials and least privilege
Cloudflare: use an API token with `Zone:DNS:Edit` restricted to the zones used for
issuance. The adapter intentionally does not accept the account-wide Global API Key.
OVH: use a consumer key allowing `GET /domain/zone/` and GET/PUT/POST/DELETE for
`/domain/zone/<YOUR_ZONE>/*`. Repeat only for required zones, not the whole account.
Choose the region matching the account. Consult the official plugin docs if API
policies change. A domain registered at OVH but DNS hosted elsewhere must use the
actual authoritative DNS provider; registration alone is not sufficient.
RFC2136: retain the TSIG-limited update policy described in ACME_RFC2136.md. DNS
provider propagation delay is configurable. The helper rejects malformed tokens,
unknown/cross-provider fields and INI injection. It does not test your DNS policy
in advance. Wildcard DNS-01 issuance does not add wildcard HOST routing: vhost
hostnames remain exact matches.
## API input
Common fields: `provider`, `certificate_id`, `domains` (one or more DNS names), `email`,
`environment` (`staging`/`production`), `agree_tos`, `confirm_production`. RustProxy
does not impose its own domain-count ceiling; the selected CA/provider may still
apply its external certificate/SAN policy.
DNS providers accept `propagation_seconds`.
Provider-specific fields:
- `http01`: none; wildcard domains are rejected.
- `cloudflare`: `api_token`.
- `ovh`: `endpoint`, `application_key`, `application_secret`, `consumer_key`.
- `rfc2136`: `dns_server`, `dns_port`, `key_name`, `key_secret`, `key_algorithm`, `sign_query`.
`POST /api/acme/issue` is authenticated and CSRF-protected; one job runs at a time.
`GET /api/acme` is safe status metadata only. Job status is process-local; account
and credential files persist. A completed certificate has optional issuer metadata
with provider/environment, not a copy of request credentials.
## Secret storage, renewal and limitations
Provider secrets are sent to the private worker on stdin, not command arguments.
They are not stored in exported JSON, SQLite, localStorage or audit records.
Files are private (0600 credentials, 0700 directories); these permissions do NOT
encrypt them. The service user/root can read them. Certbot account/lineage state
and provider credential files persist for managed renewal under
`certs/acme/<environment>-<id>-<provider>`; RFC2136 keeps its old path without a
provider suffix. Issued PEM outputs use new immutable names before library import.
Back up these private profiles separately while issuance is idle: the standard
SQLite/configured-PEM backup does not include every account/credential profile.
Do not put real credentials into shell history or source-control examples.
Restrict the admin interface and service environment. Rotate exposed API credentials.
Configured managed certificates in 0.7 renew through the shared RustProxy worker;
validated changed material is published automatically while failed checks preserve
the active generation. Use the certificate's toggle to disable scheduled renewal.
PEM-only imports remain manual. A separate system Certbot timer may own a **different,
manual RustProxy certificate id** and publish it with `tls deploy --id ...`; it must not
also own a RustProxy-managed ACME profile. There is no external expiry alert, revocation
or certificate garbage collector. Cancellation
kills the worker group; interrupted DNS cleanup can leave TXT records an operator
must remove. See [renewal and recovery](RENEWAL.md) and test CA staging first.
## Primary references
- HTTP-01/DNS-01 constraints: https://letsencrypt.org/docs/challenge-types/
- Certbot webroot and operational use: https://eff-certbot.readthedocs.io/en/latest/using.html
- Cloudflare plugin/API scope: https://certbot-dns-cloudflare.readthedocs.io/en/stable/
- OVH plugin/API scopes: https://certbot-dns-ovh.readthedocs.io/en/latest/
- RFC2136 plugin: https://certbot-dns-rfc2136.readthedocs.io/en/stable/
These document upstream requirements, not evidence that this RustProxy build has
passed live provider or CA acceptance.
+157
View File
@@ -0,0 +1,157 @@
# Basic Let's Encrypt / RFC2136 integration (0.5.0)
For the multi-provider UI, HTTP-01, Cloudflare and OVH, see ACME_PROVIDERS.md.
Managed automatic renewal added in 0.7 is documented in RENEWAL.md.
The RFC2136 fields and private account paths below remain supported.
## Scope and prerequisites
This is an integrated control-plane adapter for **Certbot + certbot-dns-rfc2136**,
not a newly written ACME or DNS-update protocol client. The plugin creates and
cleans up DNS-01 TXT records through TSIG-signed RFC2136 updates. It must be
installed separately. No real CA issuance or authoritative-DNS update has been
performed in the packaging environment.
Rust compilation, process integration and actual certificate deployment remain
acceptance work (TEST_REPORT.md).
Use Linux/Unix with Python 3.10+, a current compatible Certbot/plugin installation,
and `python3` and `certbot` available in the RustProxy service's PATH. Check:
```bash
bash dev.sh acme --doctor
certbot --version
certbot plugins --authenticators
```
The doctor command reports executable discovery and the required plugin's name;
it does NOT verify the plugin, DNS permissions or network connectivity. Nothing
is installed automatically, and RustProxy never invokes sudo. A service hardening
policy must allow child processes, the cert directory, DNS updates/queries to the
chosen server and outbound HTTPS to the CA. Do not run the proxy as root just to
make Certbot available. Use a private, service-owned cert directory (0700).
The operator must control publicly valid DNS names and an authoritative DNS
server that permits the configured TSIG identity to change **only the necessary
_acme-challenge TXT names**. Do not grant zone-wide update authority merely for
certificates. A private update-server IP is acceptable when the resulting TXT
records are visible to the public validation resolvers. Split DNS/views, CAA,
replication delays and delegation must be checked on the operator's DNS setup.
DNS-01 does not require opening the application's HTTP listener for a challenge.
## Panel workflow
Open **Certificates > Let's Encrypt / RFC2136**. Enter a certificate library ID,
contact email, comma-separated DNS names (including optional `*.example.org`),
DNS update server IP/port, TSIG name, base64 secret, algorithm and propagation
wait. ASCII/punycode DNS names only; up to 16 names. SHA256 is the default TSIG
algorithm; SHA384/SHA512 are also allowed. SHA1/MD5 are not exposed. A TSIG key
name may be a single label such as `keyname.`. Optional signed SOA queries support
DNS servers/views that require them. Port defaults to 53; propagation defaults
to 60 seconds (allowed 10..900).
Start with **staging**. Its certificates are not publicly trusted. Production
requires a separate explicit confirmation, in addition to agreement with the
Let's Encrypt subscriber agreement. The CA directory is fixed for each mode;
arbitrary directory URLs, shell hooks and arbitrary extra Certbot options are
not accepted by this interface.
Requesting starts one bounded asynchronous job, not a proxy-path operation. Refresh
shows running, failed, or ready. At ready, the Rust control plane has checked PEM
parsing, matching key and every requested DNS name before exposing only metadata.
Click **Add issued certificate to library**, then select its ID in the vhost SSL
tab and save. Replacing an existing ID requires confirmation and retains its
legacy SNI host assignments; the whole current configuration is revalidated with
revision CAS. Failed issuance/validation/import leaves active configuration alone.
Certificates are loaded for new handshakes; changing listener protocols still
requires restart. Existing connections retain their negotiated certificate.
## Renewal and operator responsibilities
Configured RFC2136 managed profiles participate in the 0.7 renewal scheduler.
Validated changed certificate material is published automatically; there is a
per-certificate opt-out, explicit Check now and Dry run. Initial issuance/import
remains explicit. The old 0.5 helper alone was issuance-only; do not confuse
retained files or an unrelated OS Certbot timer with RustProxy publication.
See RENEWAL.md for prerequisites, timing, rollback boundaries and legacy profiles.
No external expiry notification, revocation UI or account manager is included.
Account state and TSIG credentials are kept privately under:
```text
data/certs/acme/<staging-or-production>-<certificate-id>/
rfc2136.ini # SECRET, mode 0600
profile.json # non-secret parameters, mode 0600
config/ # Certbot account, renewal, live and archive data
work/
logs/ # private Certbot diagnostics
```
The location follows the configured certificate directory, not always `data/certs`.
There is one job per RustProxy process and a filesystem lock per profile. Bounds:
16 KiB request, 128 profiles, 128 staged ACME pairs and 128 KiB per PEM file. The
provider timeout is 1200 seconds plus propagation wait; the Rust worker has a
2160-second outer deadline and stops its process group on cancellation/shutdown.
Normal TXT cleanup belongs to the plugin. A hard kill, machine crash or DNS error
can leave challenge TXT records: inspect DNS and clean only the failed challenge
values, not unrelated records. Only the latest job status lives in memory; restart
loses that status, not the private Certbot files.
TSIG is supplied through stdin to the helper, never argv, SQLite, exports or job
responses. The browser clears the secret field after submission and does not put
it in local/session storage. Private credentials/account files are deliberately
retained for renewal; protect them and private logs as secrets. Keep unused staged
pairs under operator control; active files are never silently deleted.
The existing `dev.sh backup` includes SQLite and configured PEM files, **not the
ACME account/profile directory**. Back up `certs/acme` separately while no issuance
is active, preserving ownership/modes and encryption appropriate to private keys.
Source upgrade leaves the data directory alone. Do not ship profiles in source ZIPs.
## API and local helper
`GET /api/acme` returns the latest sanitized job, provider name and
`automatic_renewal: false`. `POST /api/acme/issue` requires the normal authenticated
session, Origin/CSRF protections and an explicit bounded JSON request:
```json
{
"certificate_id": "example",
"email": "operator@example.org",
"domains": ["example.org", "*.example.org"],
"environment": "staging",
"agree_tos": true,
"confirm_production": false,
"dns_server": "192.0.2.53",
"dns_port": 53,
"key_name": "rustproxy.example.org.",
"key_secret": "REPLACE_WITH_YOUR_PRIVATE_BASE64_TSIG_SECRET",
"key_algorithm": "HMAC-SHA256",
"sign_query": false,
"propagation_seconds": 60
}
```
This documentation example is intentionally not an executable credential. A
successful job submission returns 202; busy returns 429. Invalid input, consent
and provider failures use fixed `acme.*` diagnostics rather than returning
provider stderr or credentials. Look in the private profile logs for DNS/plugin
errors. A 202 response means accepted, not issued.
Advanced local staging without the running API:
```bash
# request.json must be private (0600), with real operator-owned parameters.
bash dev.sh acme --cert-dir /absolute/private/cert-directory < request.json
```
This CLI only stages files and prints metadata. Unlike the integrated API, it
cannot run Rust's key/SAN validation on its own and never activates a certificate.
Import through the normal validated library/config workflow. Do not post secret
JSON into shell arguments, source examples, tickets or logs.
## Primary references
- Plugin: https://certbot-dns-rfc2136.readthedocs.io/en/stable/
- Certbot CLI: https://eff-certbot.readthedocs.io/en/stable/man/certbot.html
- Staging: https://letsencrypt.org/docs/staging-environment/
- Challenge background: https://letsencrypt.org/docs/challenge-types/
+549
View File
@@ -0,0 +1,549 @@
# Administration API
The API is served on the separate administration listener, normally
`http://127.0.0.1:9090`. It is never mounted on reverse-proxy HTTP/TLS listeners.
Loopback HTTP is the default. Settings can enable administration HTTPS, choose a
certificate and persist a non-loopback bind; plain remote HTTP requires the explicit
startup `--allow-remote-admin` gate. Responses carry no-store, CSP and other browser
protection headers.
## Browser documentation and anonymous surface
Open `/docs` **on the administration listener** after signing in. The OpenAPI JSON,
JavaScript, styles and Swagger vendor assets all require the same session. Writes
are opt-in and CSRF-protected. See [API_ACCESS.md](API_ACCESS.md).
Only GET/HEAD `/`, the panel routes `/overview`, `/vhosts`, `/streams`,
`/acl-profiles`, `/configuration`, `/tls`, `/cache`, `/logging`, `/settings`,
`/assets/login.js`, `/assets/login-langs.json`, `/assets/style.css`, `/favicon.svg`,
`/healthz`, `/api/auth/status`, and POST `/api/auth/setup`, `/api/auth/login` are
anonymous. Panel routes serve the same guarded shell as `/`, so a browser refresh
preserves the selected subpage instead of falling back to the landing page. Root
optionally authenticates and selects login or operational HTML. `/healthz` is exactly `{"status":"ok"}`.
Unknown/private paths and method mismatches return a generic 401 without a session.
Only top-level `/docs` and `/docs/` navigations redirect to `/?next=docs`; API/schema
fetches never redirect to login HTML. Other invalid Host/Origin checks can return 403.
## Authentication
| Method / path | Input or result |
|---|---|
| `GET /api/auth/status` | Public `setup_required` boolean only; no version or operational state |
| `POST /api/auth/setup` | One-time `{token, username, password}`; creates no session |
| `POST /api/auth/login` | `{username, password}`; sets session cookie and returns session data |
| `GET /api/auth/session` | Username, CSRF token, absolute expiry and idle timeout |
| `POST /api/auth/logout` | Revoke the current session |
| `POST /api/auth/logout-all` | Revoke all administrator sessions |
| `POST /api/auth/password` | `{current_password, new_password}`; changes password and revokes sessions |
Mutations, including setup/login, require `Origin` equal to the active administration scheme plus `{Host}` and
`X-RustProxy-Request: 1`. Authenticated mutations also need `X-CSRF-Token` from
session data. Send JSON as `application/json`. A permanent bearer access token
is not supported; the old setup capability does not authorize API reads/writes.
The `rustproxy_session` cookie is HttpOnly, SameSite=Strict and Path=/ with the configured
7–30 day absolute lifetime (default 30 days); server-side revocation still applies. HTTPS
administration adds the Secure attribute; behind a trusted HTTPS-terminating reverse proxy
the external forwarded scheme controls that flag. A protected SSH/VPN tunnel remains valid,
while direct remote administration should use HTTPS plus firewall/private-network policy.
Direct IP/localhost Host values must use the actual administration port. A DNS name explicitly
listed in `administration.allowed_hosts` represents the browser-facing authority and may use
the trusted reverse proxy's external port. Foreign Origin/cross-site requests are rejected.
There is no permissive CORS mode.
Auth request bodies are limited to 4096 bytes.
Typical errors include 401 for missing/invalid credentials, 403 for origin/CSRF
rejection, 409 for already-completed setup, 422 for password policy and 429 for
throttling/busy workers. Error objects usually contain an `error` code/string;
framework extraction failures are not guaranteed to have that JSON shape.
## Configuration and observations
| Method / path | Result |
|---|---|
| `GET /healthz` | Public process health/version; not a backend health probe |
| `GET /api/config` | `{revision, config}` |
| `PUT /api/config` | Input `{expected_revision, config}`; returns the applied envelope; retained for atomic multi-section changes |
| `PUT /api/config/{section}` | Input `{expected_revision, value}`; replaces one validated section and returns the applied envelope |
| `GET /api/backends` | Authenticated per-vhost backend observations and active/desired frontend plans |
| `GET /api/status` | Uptime, active revision, configured counts, listener addresses, feature flags, aggregate metrics, active/desired TCP/UDP streams with per-stream L4 counters, active/desired administration state, TLS policy/cipher inventory and structured-log sink counters |
`status.streams` contains `active`, `desired`, `restart_required` and a `metrics` map keyed by startup-active stream ID. Stream counters include active/total/completed sessions, rejects, errors, RX/TX bytes and UDP packet counts; the dashboard derives rates from byte-counter deltas.
| `GET /api/logs` | Recent bounded access entries, ring capacity and contention-drop count |
| `GET /api/audit` | Last 100 audit entries; database retention is 10,000 |
| `GET /api/events` | SSE `snapshot` events containing status and logs |
| `GET /metrics` | Prometheus text exposition |
| `GET /api/metrics` | Alias of `/metrics` |
Except for health and authentication/bootstrap routes, these endpoints require a
session. These administrative previews are not anonymous. The separate configurable
exporter uses its own CIDRs and optional bearer token; see the scrape section below.
SSE has eight viewer slots, generates a snapshot approximately every two seconds
and rechecks session validity. It does not queue an unlimited per-viewer event log
or guarantee replay. The browser falls back to polling. An active dashboard renews
idle activity, not the absolute session deadline.
Configuration updates return 409 for a stale revision, 422 for validation failure
and 429 when the eight-operation update admission bound is reached. Persistence and
snapshot publication finish before a successful response. Disconnecting the caller
after the detached writer starts does not cancel an accepted write.
The document limit is 256 KiB, with an additional 1024-byte request-envelope allowance.
For ordinary Settings saves prefer `PUT /api/config/{section}`. Supported sections are
`listeners`, `response`, `cache`, `logging`, `forwarding`, `headers`, `prometheus`,
`administration`, `tls`, `vhosts`, `redirects`, `not_found_hosts`, `streams`, `acl-profiles` and `host-policies`.
Each section write still uses revision CAS, full validation and atomic persistence.
Only a change to the derived cache-compatibility identity purges hot/cold cache;
unrelated section saves do not.
## Configuration fields
The canonical document uses `vhosts`, `redirects`, `listeners`, `administration`, `tls`, `logging`,
`prometheus`, `cache`, `compression`, `server_header`, `headers`, `host_policies` and other defaulted
settings. Unknown fields are rejected. `backends`/`routes` at the top level are
input-only legacy migration fields, omitted from saved canonical documents.
```json
{
"vhosts": [{
"id": "application",
"frontend": {
"hosts": ["app.localhost", "alias.localhost"],
"listeners": [{"address":"127.0.0.1:8080", "tls":false, "http2":false}]
},
"backends": [{"id":"application", "url":"http://127.0.0.1:9000", "allow_private":true}],
"routes": [{"id":"root", "path_prefix":"/"}]
}]
}
```
The backend service must already exist. Routes without `backend_id` select from
the vhost pool; an explicit ID pins that path to a member of the same pool.
`vhosts[].force_ssl=true` redirects matching cleartext requests with HTTP 308 to the
same hostname/path/query. Force SSL requires both a selected plain HTTP listener and a
selected TLS listener. Port 443 is preferred when several TLS ports are selected;
otherwise multiple non-standard TLS ports are rejected as an ambiguous redirect target.
`vhosts[].forward_client_ip` defaults to true. Set it to false only when an application
must see RustProxy's transport peer instead of the visitor: backend requests then omit
`Forwarded`, `X-Forwarded-For` and `X-Real-IP`, while `X-Forwarded-Host/Proto/Port/URI`
remain and RustProxy/Tinyauth still use the real client address. See examples and
VHOSTS.md. HTTPS upstream certificate verification is disabled when `tls_verify` is omitted. Set `tls_verify: true` to validate the origin certificate chain and identity. For verified IP-based origins, use a DNS `tls_server_name` present in the
certificate and provide `tls_ca_pem` when the issuer is private. Disabling verification
retains TLS encryption and all backend address/DNS policy checks. Certificate metadata references
files under `<data-dir>/certs`; it does not create a vhost or expose a listener.
First-class redirect hosts live under `redirects`; see `REDIRECTS.md` for matching, target, status, path/query and cache behavior.
ACL rules have `network` CIDR and `action` (`allow`/`deny`). Omitted ACL means no
restriction in that scope; an explicit empty ACL defaults to deny. Matching deny
wins. Host and route decisions are ANDed. Identity is the socket peer, not an
untrusted forwarded header. There are up to 128 rules per ACL.
Headers run global, then vhost (where applicable), then route rules. `set` replaces, `append` adds a
separate value and `remove` accepts no value. Transport, WebSocket handshake and
client-identity headers are protected. Up to 32 rules per direction/scope, 128-byte
names and 8192-byte values are allowed. Response rules and matching vhost security policy also cover locally generated
proxy errors/denials. Multiple Set-Cookie fields remain separate.
Routing matches exact normalized hostnames, then Nginx-style location precedence: exact path,
longest priority prefix (`^~`), first matching regex in declaration order, then longest ordinary
prefix at a segment boundary (`/api` matches `/api/x`, not `/apix`). Case-insensitive regex is
also supported. Repeated literal slashes are merged for route/ACL selection, while an unrevised
backend request retains the original path/query. Locations can explicitly strip/replace/regex-
rewrite the upstream path or return a local response. Ambiguous encoded separators and traversal
forms remain rejected. Vhost pools support load balancing as described in VHOSTS.md; locations
may pin traffic to one member. Wildcard hostname matching is supported only in the documented
leading-label form; hostname regex remains unsupported.
## Metrics and logs
Metric names use `rust_proxy_` and bounded aggregate labels. Transport counters cover
HTTP/2 requests/connections, TLS handshakes/errors and WebSocket tunnel totals,
active tunnels, errors, frame bytes and closed-tunnel duration sum/count. Ordinary
HTTP body bytes and WebSocket frame bytes are separate measurements.
Access entries are emitted at exchange completion or tunnel closure, not only when
headers arrive. WebSocket exchanges retain status 101 and a `websocket_closed`
outcome when cleanly released. Errors/cancellation affect the outcome. No query
values, request bodies, cookies, password material or WebSocket payloads are logged
by the access logger. Error diagnostics use stderr tracing. The in-memory access ring retains up to 4096
completed entries; live SSE/UI snapshots use the newest 256 by default. The dashboard can
temporarily expand an open Live Logs window to the retained 4096-entry buffer and keep
merging new live snapshots until that window closes. This is not a durable lossless log archive.
## 0.1.0 cache, listener and diagnostics additions
| Method / path | Contract |
|---|---|
| GET /assets/langs.json | Public compiled language bundle: default ID, discovered language metadata/translations |
| GET /api/cache | Authenticated cache settings, metrics and bounded object metadata; never returns cached bodies |
| POST /api/cache/purge | Full/global or vhost-scoped purge with the normal mutation protections and audit |
| POST /api/cache/invalidate | Selective authenticated PURGE/BAN by exact URL, prefix, tag, safe regex or cache key; optional host/vhost scope |
| GET /api/cache/bans | List active in-memory BAN rules |
| DELETE /api/cache/bans/{id} | Remove one BAN rule with audit |
| POST /api/cache/cold/cleanup | Start an authenticated/CSRF-protected asynchronous selective Cold Cache cleanup job |
| GET /api/cache/cold/cleanup/{id} | Read status/result of an authenticated selective cleanup job |
Configuration includes defaulted `listeners`, `cache`, `transform`, `compression`, `server_header`
objects and optional per-route `cache` (`false` bypasses, null/omitted inherits).
The complete documented field contracts are in SETTINGS.md and CACHE.md. Listener
changes persist immediately but require restart; all other settings here are hot
published. Settings forms use revisioned section writes where one settings area can
be changed independently; full-document PUT remains for changes that must publish
multiple sections atomically.
Status and SSE snapshots include `cache`, `system`, and `listeners` with active,
desired, startup-default addresses and `restart_required`. Metrics include four
compression counters; log entries include `cache` (HIT/MISS/BYPASS, or `-` before
cache classification). OS measurements are optional/null and sampled off the
request path. Unsupported is not represented as zero. Prometheus adds fixed-name
cache/resource series; absent OS measurements are omitted. No arbitrary URL/cache-key
labels are introduced and the metrics endpoint remains authenticated.
The language bundle is generated by build.rs from langs/*.json and embedded into the
binary; uploading a new file requires recompilation/restart. It exposes public
translations only, not filesystem paths, administrator data or credentials.
## 0.2.0 vhosts and backend state
`vhosts` defaults to an empty array. All models, enum values, defaults and bounds
are documented in VHOSTS.md, with complete examples under `examples/vhosts-*.json`.
Use the same revisioned PUT endpoint; there is no unauthenticated health-control
endpoint or separate unsynchronized frontend/backend write API.
`GET /api/backends` returns `{pools, active_frontends, desired_frontends}`. Pool state
includes `id`, `algorithm`, `ready`, `health_mode`, `sticky_mode`, `keepalive`,
`members`. Members expose ID/origin/weight, status, healthy flag, active exchanges,
request/error/byte totals, EWMA byte rate/header latency, last check/result, failure
and success counters, transition count and last upstream status. It does not expose
affinity tokens. Member status is up/down/checking/draining/disabled. In health-off
mode, eligibility is not proof that the origin is live. Draining describes operator
intent; inspect `healthy` as well. `ready` reflects new-assignment availability;
a pool containing only draining members may still serve existing cookie sessions.
Status contains `backend_pools` and `vhosts`; listener state additionally includes
`active_endpoints`, `desired_endpoints`. Each endpoint reports its address, TLS and
H2/H3 transport capabilities. `restart_required` covers socket and listener-capability
changes. Listener capabilities are configured only under `listeners.endpoints`. A vhost
then narrows them with `http2_enabled` and `http3_enabled`; both default true. For TLS,
SNI selects either H2+HTTP/1.1 or HTTP/1.1-only ALPN for the matched vhost, so disabling
H2 on one hostname does not disable it for another hostname sharing the listener.
Metrics add `rust_proxy_backend_*` per bounded vhost/member labels. Access entries
add nullable `backend` (`vhost/member` for a forwarded pool exchange). Cache hits
have no actual upstream member request and do not increase its request counters.
The `/assets/vhosts.js` frontend uses the same API/CSRF/revision
boundaries; the static script itself, like other frontend assets, is publicly readable.
## 0.3.0 certificate upload and scoped operations
`POST /api/certificates/upload` accepts JSON
`{"cert_pem":"PEM chain","key_pem":"PEM private key","hosts":["app.example"]}`.
It requires the same session, Origin and CSRF guards as configuration updates.
The request limit is 300 KiB; each PEM value is at most 128 KiB. At most two
blocking upload workers run, and the directory holds at most 128 upload-prefixed
pairs. This endpoint is staging only, not a configuration mutation.
Success: `{"certificate":{"id":"upload-...","covered_hosts":["app.example"],
"hosts":[],"cert_file":"upload-....pem","key_file":"upload-....key"},"staged":true}`.
`covered_hosts` contains the normalized DNS names extracted from the leaf certificate
SubjectAltName extension; `hosts` remains the configured active SNI bindings. No key or
certificate PEM is returned. Add the metadata to `config.tls.certificates` and its ID
to a vhost's `certificate_id`, then save `tls`/`vhosts` as needed. The panel keeps a
combined full-document Save only when staged certificate metadata and vhost assignment
must be published atomically. Unknown certificate references, mismatched keys,
uncovered DNS names and conflicting same-name selections fail validation.
There is no ACME enrollment, expiry/chain-trust guarantee or remote PEM retrieval.
`GET /api/cache` adds `vhosts: [{id, usage: {entries, resident_bytes}, settings}]`.
`POST /api/cache/purge?vhost=app` purges only that configured vhost and invalidates
its pending fills. Missing/unknown IDs return 404, not an accidental global purge.
Omitting the query purges all entries. Both actions are audited and require CSRF;
an empty body is accepted. Configuration updates purge only when cache compatibility
changes; unrelated section saves retain both hot and cold objects.
`POST /api/cache/cold/cleanup` accepts a JSON selection with optional
`unused_for_seconds`, optional `oldest` (maximum count) and `hits_zero`. At least one
criterion is required. It returns HTTP 202 with a `ColdCleanupJob`; poll
`GET /api/cache/cold/cleanup/{id}` until `status` is `done`. The result contains
`removed_entries` and `freed_bytes`. This is intentionally separate from full/scope purge
but reuses the same internal Cold Cache candidate/removal machinery as automatic GC.
### 0.22.1 selective cache invalidation
`POST /api/cache/invalidate` accepts an `InvalidationRequest`:
```json
{
"operation": "purge",
"selector": "exact",
"value": "/assets/app.js?v=42",
"host": "app.example",
"vhost": "application",
"ttl_seconds": null,
"confirm_wide": false
}
```
`operation` is `purge` or `ban`. `selector` is `exact`, `prefix`, `tag`, `regex` or
`cache_key`. `host` and `vhost` are optional independent scope restrictions. BAN accepts
`ttl_seconds` (1..604800, default 3600) and the runtime keeps at most 1024 active rules.
Every regex operation and prefix `/` requires `confirm_wide=true`; the browser panel asks
for a danger confirmation before sending it. Regex input is length-bounded and compiled
with Rust's non-backtracking `regex` engine plus a compiled-size limit.
The response includes `result` with operation/selector/value/host/vhost,
`affected_entries`, optional `ban_id`, `elapsed_ms` and `status`, plus current cache metrics
and active BAN rules. Selective PURGE removes RAM and Cold Cache matches, removes the full
`Vary` family under a matched primary key and prevents fills that started before the PURGE
from publishing afterward. BAN creation does not synchronously scan the store: matching
objects created before the rule are rejected on lookup and removed incrementally by cache
maintenance; fresh objects created after the rule may be stored. BAN rules are runtime
state and are not persisted across process restart.
`GET /api/cache/bans` returns the active rule views. `DELETE /api/cache/bans/{id}` removes
one rule. All invalidation mutations use the standard administrator session, same-origin
request marker and CSRF protections; requested operator/scope information is written to the
existing audit log.
The `transform` configuration section is replaceable through the normal revision-CAS
`PUT /api/config/transform` endpoint. Vhost objects carry a `transform` override and
Docker assignments carry `transform_enabled`; both default to disabled per vhost.
`GET /api/transform` returns in-memory transformation observability for the administration
Transform workspace: totals and per-vhost counters for examined/applied/skipped/error
responses, transformed cache HITs, original/transformed/saved bytes, accumulated transform
time, applied counts by content kind and bounded skip/fail-open reasons. These counters are
runtime diagnostics and reset when the RustProxy process restarts.
`GET /api/logs` accepts optional `vhost`, `q`, exact numeric `status`, `cache` and
`limit` parameters. Limit clamps to 1..4096 and defaults to 256; search length is at most
256 bytes. Filters run over the retained ring before the limit is applied. `cache` accepts
HIT, MISS, BYPASS or `-`. Entries now include nullable `vhost` independently of `backend`:
cached and static responses can be attributed without claiming that an origin was contacted.
SSE still sends the newest 256 entries per global snapshot. The Live Logs UI can seed a
temporary 4096-entry buffer on demand and then merge subsequent live snapshots into it until
the window closes. This is not a durable log stream or a per-tenant authorization boundary.
The status endpoint keeps existing field names, adds feature flags for new
capabilities, and assembles small values rather than one deeply expanded macro.
The management script `/assets/management.js` is a public static asset; APIs remain
protected. Pages and per-vhost cache fields use the standard configuration endpoint.
## 0.4.0 certificate inventory, forwarding, auth and listeners
`GET /api/certificates` requires the existing admin session and returns revision,
metadata-only certificates including `covered_hosts`, active `hosts`, `assigned_vhosts`,
file names and the per-entry default flag, plus `acme:false`. Inventory reads only the
public certificate PEM on a blocking worker and derives current DNS SubjectAltName values,
so older/manual entries and in-place renewals do not depend on stale persisted coverage
metadata. Persisted `covered_hosts` is only a fallback when the public PEM cannot be read.
It never reads a private key for this enrichment and never returns PEM contents or private-key bytes. Existing upload remains a
CSRF-protected staging operation. The inventory UI saves its returned metadata in
`config.tls.certificates`; vhost assignment is a later independent configuration
save. Every configuration mutation retains revision CAS and validation.
Vhost additions: `tinyauth`, `server_header`, `diagnostic_headers`,
`backend_header`, `backend_timing_header`, and
`frontend.listener_ids`. Global additions: `forwarding`,
`diagnostic_headers`, and `listeners.endpoints`. Each effective
TLS binding can carry `http3` with the five fields described in HTTP3.md. These
are full-config fields, not independent unaudited mutation endpoints. Unknown or
invalid references, trust ranges, URLs, limits and protected header names fail
validation before publication.
Status contains named_endpoints and active/desired endpoint transport metadata,
H3 connection counters and feature flags. Feature flags describe included source
capabilities, not evidence of successful build/conformance testing. Existing
Prometheus metrics add rust_proxy_http3_connections_total,
rust_proxy_http3_active_connections and rust_proxy_http3_rejected_connections_total
and rust_proxy_http3_requests_total. See source `metrics.rs` for exact HELP/type
contracts and INTEGRATIONS_040.md for configuration examples.
## 0.22.3 certificate hot reload and external deployment
`POST /api/certificates/reload` is an authenticated, Origin/CSRF-protected control-plane
operation. It re-reads every certificate/key file referenced by the current desired
configuration, validates a fresh immutable routing/TLS snapshot and publishes it at the
**same configuration revision**. Invalid or mismatched material returns an error and the
previous snapshot stays active. The response contains `reloaded`, `revision`, `public_tls`,
`http3_new_handshakes` and `administration_restart_required`. New public H1/H2 TLS and HTTP/3
handshakes use the refreshed certificates; established connections are not renegotiated.
Administration TLS is startup-fixed and therefore reports when a process restart is needed.
`POST /api/certificates/{certificate_id}/reload` is the targeted variant for a manual/file-backed certificate (`issuer == null`). It validates the requested configured ID and atomically republishes the immutable TLS snapshot at the same configuration revision. It is intended for external renewers that replace the files already referenced by RustProxy. Application-managed ACME certificate IDs are rejected with `409` because their renewal pipeline owns those files. The endpoint accepts either the normal administrator session + CSRF flow or a scoped Certificate API token.
`POST /api/certificates/deploy` accepts
`{"certificate_id":"shared-cert","cert_pem":"...","key_pem":"..."}` under the same
authenticated Origin/CSRF boundary. It is a convergent upsert for externally owned certificate
ids: a missing id creates a manual certificate entry, an existing manual id is rotated in place,
and an identical RustProxy-owned generation is a no-op. The pair is validated **inside the running
RustProxy process** and changed material is staged under immutable generation-specific files before
a revision-CAS configuration publish. This is important for root-run Certbot hooks when RustProxy
itself runs as an unprivileged service account: final generations are created with the service's
filesystem identity rather than `root:root`. Managed ACME ids are rejected, and failed
configuration publication removes any newly staged generation.
For a certificate owned by a local external issuer such as system Certbot, prefer the local
CLI deploy hook, which calls that loopback API after bounded local file checks:
```bash
rust-proxy tls deploy --id shared-cert \
--cert /etc/letsencrypt/live/example/fullchain.pem \
--key /etc/letsencrypt/live/example/privkey.pem
```
The command resolves Certbot's local `live/` symlinks, checks bounded regular files/private-key permissions, then sends the pair to `POST /api/certificates/deploy`. The running service validates the certificate/key and DNS SANs, stages generation-specific files inside RustProxy's private certificate directory, and creates or switches the **manual** certificate id through the normal revision-CAS configuration path. A single id may already be selected by multiple vhosts, so those vhosts switch together
without individual edits. RustProxy-managed ACME entries (`issuer != null`) are rejected to
avoid two renewal owners for the same lineage. The existing `/api/certificates/upload` remains
the generic PEM staging endpoint; private certificate paths are never accepted from a remote
request.
## 0.23.0 scoped Certificate API credentials
External automation no longer needs a full administrator browser session. From **Settings -> Certificate API**, an administrator can create multiple dedicated credentials (up to 32), each consisting of a Key ID, Bearer token and optional non-secret comment. Each Key ID can be rotated independently. The clear token is returned only by the create/rotation response; SQLite stores only its SHA-256 hash plus metadata/comment. Every credential has exactly two scopes: certificate deploy and certificate reload.
API clients send both headers:
```text
Authorization: Bearer <token>
X-RustProxy-Certificate-Key: <key-id>
```
A Certificate API credential is accepted only by `POST /api/certificates/deploy`, `POST /api/certificates/reload`, and `POST /api/certificates/{certificate_id}/reload`; it cannot read configuration, logs, metrics or any other administration endpoint. Non-loopback use requires the configured HTTPS administration surface. The normal administrator session + Origin/request-marker + CSRF path remains valid for the same operations. Credential management remains administrator-only: `GET /api/certificate-api` lists metadata, `PUT /api/certificate-api` creates/rotates one Key ID, `DELETE /api/certificate-api/{key_id}` revokes one token, and `DELETE /api/certificate-api` revokes all tokens.
Example external deploy:
```bash
curl --fail --show-error \
-H 'Authorization: Bearer TOKEN' \
-H 'X-RustProxy-Certificate-Key: certbot-main' \
-H 'Content-Type: application/json' \
--data-binary @deploy.json \
https://proxy-admin.example/api/certificates/deploy
```
For an on-host Certbot hook, a token is optional: invoke the installed binary directly instead:
```bash
rust-proxy tls deploy --id shared-cert \
--cert /etc/letsencrypt/live/example/fullchain.pem \
--key /etc/letsencrypt/live/example/privkey.pem
```
`dev.sh` is development tooling and is not required for production certificate rotation.
## 0.5.0 cache inspector and certificate jobs
`GET /api/cache` additionally returns `dashboard: {domains, objects,
matching_objects, offset, limit, statistics_revision}`. Domain rows flatten
hits/misses/bypasses/stored/saved_body_bytes/reasons beside vhost/domain and
resident counts. Object rows contain vhost/domain/path/body_bytes/resident_bytes/
hits/age_seconds/remaining_ttl_seconds, never bodies, request headers or queries.
`vhost=ID` filters, `vhost=` selects legacy only; omitted selects all. Limit 1..200
(default 50), offset <=16384. Unknown vhost ->404. The same scope semantics apply
to POST /api/cache/purge. Existing global metrics remain process-lifetime values.
The historical 0.5 `GET /api/acme` response did not support renewal; see the
current 0.7 response below.
`POST /api/acme/issue` accepts at most 16 KiB of JSON with certificate_id, email,
domains, environment, agree_tos, confirm_production, dns_server, dns_port, key_name,
key_secret, key_algorithm, sign_query and propagation_seconds. See
ACME_RFC2136.md for exact constraints. It returns 202 `{job}` only after admission;
one process-wide job, 429 when busy. Job fields: id, certificate_id, status,
environment, message, certificate. The ready certificate is immutable PEM metadata,
not private bytes. Import is a separate normal config CAS write. Both endpoints
are behind the administrator session; POST retains Origin/CSRF and audit.
LogEntry adds `cache_reason`, a fixed string token. Debug response headers add
X-Cache-Reason when X-Cache is enabled, and standardized RFC 9211 `Cache-Status` detail
when enabled. Transformed cached representations add the `transformed` Cache-Status
extension parameter. `X-Cache-Status` is stripped if supplied by an origin but is not
emitted by RustProxy.
`diagnostic_headers.hide_mock_origin` is removed and old documents containing it are rejected on
export; `request_id` is the only diagnostic-header switch. No revision response-header behavior remains. TLS listener
http3 omission is default-on; explicit null is persisted off. Legacy listeners use
https_http3 with identical default/null behavior.
## 0.6 provider and scrape additions
`POST /api/acme/issue` accepts `provider`: `http01`, `cloudflare`, `ovh` or
`rfc2136` (omitted retains RFC2136 compatibility). Common fields include
`certificate_id`, `domains`, `email`, `environment`, `agree_tos` and
`confirm_production`. DNS providers also accept `propagation_seconds`.
Cross-provider/unknown fields are rejected. Provider-specific fields, secret
handling and request examples are in ACME_PROVIDERS.md. `GET /api/acme` returns
only public job status and validated certificate metadata, not input credentials.
Issuer metadata on certificates is optional `{ "provider": "cloudflare",
"environment": "staging" }`. Do not confuse metadata with CA trust validation.
`config.prometheus` controls the new dedicated HTTP scrape endpoint, which uses
actual-peer CIDRs and optional bearer authentication, not administrator cookies.
The exact configured GET path only is accepted (query strings rejected). Disabled
or changed/restart-pending settings return 404; denied peers 403; invalid tokens
401; non-GET 405; exhausted scrape capacity 429. No administrative actions are
available there. Admin `/api/metrics` and `/metrics` remain session-protected
previews on the admin listener, even when the dedicated endpoint is disabled.
See PROMETHEUS.md for the full settings and metric contract. Docker discovery is an independent collector group (`include_docker_vhosts`) and exports only dedicated `rust_proxy_docker_vhost_*` / `rust_proxy_docker_backend_*` families; ordinary vhost/backend families exclude dynamic Docker routes.
## 0.7 renewal and host access
`GET /api/acme` now returns `{job, providers, automatic_renewal:true,
interval_seconds:43200, renewals}`. `renewals` is keyed by certificate ID and
contains last_checked, next_check, last_renewed, not_after (Unix seconds), failures,
status and a public message token. No credential/account data is returned.
`GET /api/certificates` also includes renewal observations. Statuses are observations,
not a promise of a successful future CA operation.
`POST /api/acme/renew` accepts only `{certificate_id, dry_run:false}`. It uses an
existing managed lineage and returns 202 `{job}` after admission, or 404 unknown ID,
409 unmanaged/invalid profile or disabled prerequisites, 429 worker busy. Actual
worker/CA failures arrive in job state. No force-renew flag is offered. Dry runs
never deploy their test certificate and never postpone a due real renewal.
The usual session, Origin/CSRF protections and audit apply.
A certificate's `issuer` supports `auto_renew` (default true for saved managed
metadata, explicit false opts out). Toggle through ordinary revisioned config;
no provider credentials need resubmitting. Old immutable generations are retained.
`POST /api/basic-auth/hash` accepts `{username, password}` with at most 4096 bytes
of request JSON. It requires an administrator session and the same mutation/CSRF
guards, returns `{username, password_hash}`, and never saves the plaintext password.
Passwords are 12..128 UTF-8 bytes with no control characters. Usernames are 1..128
bytes, no colon/control characters. Hashing has two shared blocking-worker slots;
no available slot returns 429. Invalid input returns 400. This is NOT a public
login endpoint and does not create a browser session for a vhost.
A vhost accepts `websockets` and `basic_auth` with enabled, realm, require_tls,
forward_credentials and users[{username,password_hash}]. Basic and Tinyauth cannot
be enabled simultaneously. Hash import accepts only the bounded Argon2id policy
created by the hash endpoint, not arbitrary-cost PHC strings or htpasswd formats.
Missing `websockets` retains the previous true behavior; the new-host UI selects
false. Basic TLS defaults true and forwarding defaults false.
Backend pool members have `pool_eligible` (default true). False excludes that
member from default load balancing and sticky selection without blocking an
explicit location `backend_id`. Visual custom locations edit existing vhost
`routes`: ID, path_prefix, backend_id, enabled, cache, ACL and advanced headers.
This is prefix routing, not regex, rewrite or nginx configuration evaluation.
See [host access](VHOST_ACCESS.md) for cumulative ACL and authentication semantics.
## 0.18 Docker discovery additions
| Method / path | Contract |
|---|---|
| `GET /api/docker` | Authenticated cached in-memory Docker-agent observations; no agent poll and no configuration write |
| `POST /api/docker/refresh` | Authenticated + Origin/CSRF immediate poll of enabled agents followed by ephemeral route reconciliation; no configuration revision write |
| `PUT /api/config/docker-agents` | Revision-CAS replacement of desired Docker-agent settings, per-agent domains and service hostname/alias/IP assignments |
`config.docker_agents` stores desired agent endpoint/token/base-domain/IP override/listener references, a per-agent 5..3600 second inventory poll interval, and assignments. Inventory data returned by agents is not persisted in the configuration document. The normal configuration API remains authoritative for desired state; the Docker observation API is operational state only. Docker assignments may also carry an ACL profile, inline ACL, health-check settings and per-vhost proxy timeouts. Publishing/unpublishing is therefore an ordinary CAS configuration update and does not require polling the agent.
## HTTP/2 lifecycle settings
As of 0.24.3 the preferred configuration is per listener: `listeners.endpoints[].binding.http2_settings` contains `idle_timeout_seconds`, `max_connection_lifetime_seconds` and `drain_timeout_seconds` for that TLS H2 or cleartext H2C endpoint. Update these values by revision-CAS replacement of the `listeners` section (`PUT /api/config/listeners`), the same ownership model used by per-listener HTTP/3 limits. `max_connection_lifetime_seconds: 0` disables the absolute lifetime limit. Idle and lifetime expiry both use graceful GOAWAY/drain shutdown.
The top-level `http2` object introduced in 0.24.0 and `PUT /api/config/http2` remain accepted for backward compatibility. They are a fallback only for listeners that do not yet have explicit `binding.http2_settings`; the structured 0.24.3 UI writes listener-scoped values.
The runtime system diagnostics object includes `platform`, supplied by the shared system-statistics backend (`Linux`, `FreeBSD`, `OpenBSD`, `NetBSD`, or the generic/unsupported fallback). The browser only renders that value; it does not detect the operating system itself.
+231
View File
@@ -0,0 +1,231 @@
# Administration API access - 0.24.5
## Sign-in boundary
The administration listener remains separate from reverse-proxy listeners and uses
loopback HTTP by default. Settings -> Administration can enable HTTPS, select either
the application-managed default certificate or an inventory certificate, persist a
new bind address and allow explicit DNS hostnames. A persisted non-loopback bind is
accepted when HTTPS is enabled. The legacy startup flags remain only for deliberately
plain remote HTTP. Public vhost `/docs` paths are not captured by the admin listener.
When the dashboard is published through a trusted reverse proxy, an explicit
`administration.allowed_hosts` DNS entry is matched against the external authority rather
than the private admin socket port. `X-Forwarded-Host` is trusted only from loopback or a
configured trusted proxy; the external `X-Forwarded-Proto` additionally requires the
forwarded-proto trust setting except for loopback. Origin/CSRF checks and Secure cookies use
that external origin. Direct IP/localhost access still requires the real administration port.
### Publishing the panel through RustProxy itself
0.16.0 supports the explicitly opted-in same-process topology where a public HTTPS vhost proxies to the local administration listener. The reserved `rustproxy_session` cookie is passed through only when all of these conditions are true: the browser-facing request is TLS, its DNS hostname is explicitly present in `administration.allowed_hosts`, the selected backend directly addresses the configured administration socket by IP, and that backend has `accept_local_listener_risk=true`. The control-plane route is forced out of shared cache.
This is a narrow exception to the normal isolation rule. For every ordinary backend RustProxy still removes `rustproxy_session` before forwarding requests and strips an upstream attempt to set that cookie in responses. DNS backend names are not accepted for the special self-admin cookie path. This prevents a normal application backend from reading or replacing the administration session.
The anonymous page contains only the sign-in/setup form, logo, shared stylesheet,
a dedicated login script, and filtered language strings. It does not fetch the
configuration, status, metrics, logs, certificate inventory, dashboard modules or
the full translation bundle. Adding a new language JSON still discovers that
language at build time; login strings use the same fallback rules.
Exact anonymous methods/paths:
| Method | Paths | Content |
|---|---|---|
| GET/HEAD | `/`, `/overview`, `/vhosts`, `/redirects`, `/streams`, `/acl-profiles`, `/configuration`, `/tls`, `/cache`, `/logging`, `/settings` | Minimal login when no valid cookie is present; the same URL returns the authenticated application shell when the supplied session is valid. `/configuration` is a compatibility alias for Settings -> Manual config. |
| GET/HEAD | `/assets/login.js`, `/assets/login-langs.json`, `/assets/style.css`, `/favicon.svg` | Login assets; no runtime state |
| GET/HEAD | `/healthz` | `{"status":"ok"}` only |
| GET/HEAD | `/api/auth/status` | `{"setup_required":false}` or `true` only |
| POST | `/api/auth/login`, `/api/auth/setup` | Credential/capability validation, exact Origin and request marker required |
The document allowlist above is intentionally session-aware: a browser refresh on a protected
panel path first validates the cookie server-side instead of rendering a false logged-out state.
The login document also rechecks `/api/auth/session` when restored from browser history, preventing
a stale back/forward-cache copy from contradicting a still-valid session. Every other admin
path/method, including full assets, `/api/metrics`, `/metrics`, unknown endpoints and the OpenAPI
schema, requires an active database session.
Missing/invalid/expired cookies return `401 {"error":"auth.required"}` before
query/body extraction. Invalid Host/Origin may return 403 first. Only GET/HEAD
`/docs` and `/docs/` navigate to login through a fixed `303 /?next=docs` redirect;
API/spec/assets never return login HTML instead of a status error.
Administrator browser sessions have a configurable absolute lifetime of **7–30 days**
(default 30) through `administration.session_days`. The server stores only a hash of the
session secret, keeps the cookie HttpOnly/SameSite=Strict (and Secure on HTTPS), and
never extends a session past its creation-time absolute expiry. The cookie is refreshed
with only the remaining lifetime so an upgraded still-valid short session can adopt the
new configured bound. Logout, password change and revoke-all still invalidate server-side
sessions immediately.
Responses are no-store. Headers include CSP, nosniff, frame denial, no-referrer and
restricted camera/microphone/geolocation permissions. Error responses do not expose
request-body excerpts for malformed anonymous login/setup input. Anonymous responses contain no application version; fingerprint/revision
response headers are stripped on the admin surface. Authenticated status and the
protected schema intentionally retain the current version. The application name/logo and the single setup-needed bit are intentional
public information, not claims of anonymity or software fingerprint prevention.
Authenticated `GET /api/status` also returns a small `runtime` object with the platform,
statistics adapter name, resolved data directory and SQLite database path. This is used by
the Settings UI so Linux, BSD/OPNsense and custom installations show their actual runtime
paths instead of browser-side guesses. The object is not available anonymously and does
not expose credentials, environment values, certificate contents or arbitrary filesystem
listings.
## Swagger UI
`/docs/openapi.json` describes the local endpoints and common request schemas; it
contains no live configuration, actual credentials, generated keys, user accounts,
cache entries or listener addresses. Extensible schemas summarize configuration;
the Rust validator remains authoritative for the full configuration model.
Documentation uses the same HttpOnly `rustproxy_session` cookie as the panel.
Do not paste session cookies into the schema or store them in browser storage.
GET execution is enabled by default; write execution requires a checkbox and an
explicit confirmation for each request. The script validates the current session,
allows only documented same-origin operations, and sends the current session's
`X-CSRF-Token` plus `X-RustProxy-Request: 1`. The browser supplies Origin. The server
performs the real authorization checks regardless of any browser options.
Login/setup operations and indefinite SSE streams are documented but not executed
by Swagger UI. Native logout/password writes can invalidate the browser session; reload or
sign in again afterward. Ordinary API mutations still require the administrator session,
Origin/request marker and CSRF token.
0.23.0 adds one deliberately narrow exception for certificate automation. An operator can
create one or more **Certificate API Key ID + Bearer token** credentials in Settings -> Certificate API. Each credential may carry an operator comment and authorizes only:
- `POST /api/certificates/deploy`
- `POST /api/certificates/reload`
- `POST /api/certificates/{certificate_id}/reload`
Both `Authorization: Bearer <token>` and `X-RustProxy-Certificate-Key: <key-id>` are required.
The clear token is returned only on creation/rotation; the database stores its SHA-256 hash. Up to 32 Key IDs can coexist, and each can be revoked independently or all at once. The credential does not authorize configuration, logs, metrics, users or any other endpoint. Non-loopback use is accepted only on the configured HTTPS administration surface. Revocation from Settings invalidates that credential immediately.
Local host automation does not need this token. The installed binary can run
`rust-proxy tls deploy ...` or `rust-proxy tls reload` directly; it obtains short-lived local
CLI authority from the local database and calls the guarded loopback administration API.
`dev.sh` is a development helper and is not required in production.
The external validator, query-supplied schema/server configuration and persisted
Swagger authorization are disabled. Runtime JS/CSS are served from the same admin
origin behind its guard. Only the two necessary vendor asset filenames and a plain-text license notice
(`/docs/licenses.txt`) are routed; all require an administrator session.
Swagger's inline layout styles have a document-scoped CSP exception; scripts stay
self-only with no unsafe-eval allowance. The ordinary dashboard CSP is unchanged.
Logout broadcasts an in-memory notification between tabs. The panel drops its
operational document on logout/revocation; documentation clears its schema,
responses and write opt-in. Docs recheck sessions on actions, visibility and every
30 seconds while visible. Browser history restoration revalidates; this is not a
promise to erase browser/OS screenshots or data intentionally downloaded by an admin.
## First build / offline assets (fixed in 0.7.2)
No `SWAGGER_UI_DOWNLOAD_URL` environment variable or `.cargo/config.toml` is required.
The default is Swagger UI **5.32.11**, selected by `scripts/swagger_assets.py` and
called from this application's `build.rs`. The former `utoipa-swagger-ui` adapter
is removed: there is no separate dependency build silently choosing its older
historical browser default. Direct Cargo builds using `--manifest-path` use the
project's absolute manifest directory, not the caller's working directory.
Python **3.10+ is now also required for direct Cargo builds**, as it already was
for `dev.sh`. `build.rs` tries `python3`, then `python` if that executable is absent.
Use `RUST_PROXY_PYTHON=/absolute/path/to/python3` to select an interpreter explicitly.
The helper uses Python's verified HTTPS transport and standard-library ZIP reader;
`curl` is no longer a Swagger build requirement. Missing tools/network/assets give
an actionable build error rather than an absent-environment-variable panic.
Normal build (no Swagger environment setup):
```bash
cd RustProxy
bash dev.sh check
bash dev.sh test
bash dev.sh release
```
The first build still needs the normal Cargo registry and the pinned upstream
archive. Swagger JS/CSS and license notices are embedded in **both debug and
release binaries**, served behind the existing admin guard. No runtime download,
CDN, public asset endpoint or alternate authentication mechanism was introduced.
An offline or GitHub-restricted build can use a reviewed local copy of the same
GitHub **tag archive**, with its original `swagger-ui-5.32.11/` directory structure:
```bash
export SWAGGER_UI_DOWNLOAD_URL=file:///srv/build-assets/swagger-ui-5.32.11.zip
# Optional: require the archive digest independently verified by your build team.
# export SWAGGER_UI_ARCHIVE_SHA256=<64-hex-digit-verified-digest>
bash dev.sh check
bash dev.sh test
bash dev.sh release
```
Unset/empty source values use the default pin. Different remote URLs and old tag
archives are rejected. Local file URLs support escaped spaces and must be absolute.
`SWAGGER_UI_ARCHIVE_SHA256`, when set, checks the entire archive before publication.
No upstream checksum is fabricated: HTTPS provenance, the archive's declared tag
and its layout are not themselves a cryptographic authenticity guarantee for an
operator-supplied ZIP. Independently review/pin local inputs.
The helper only reads exact, bounded members; it does not extract arbitrary ZIP
paths. It retains licenses/notices, validates package name/version, rejects duplicate
required members, symlinks and oversized assets, caches only validated downloads in
Cargo's output directory, and writes outputs atomically. Failed preparation blocks
compilation instead of embedding stale assets. A `source.json` receipt records
archive and output SHA-256 values in `OUT_DIR/swagger-ui/`.
Cargo tracks changes to the helper, source/digest/interpreter environment variables,
local or cached ZIP and generated outputs. Repeated identical preparation preserves
output timestamps. `CARGO_NET_OFFLINE=true` forbids a new helper download; ordinary
Cargo dependencies must also be cached for a fully offline build. A local archive
is the recommended explicit setup for `cargo --offline`; Cargo CLI offline flags
are not assumed to propagate automatically into subprocess environments.
0.24.1 adds cross-platform statistics dependencies. This packaging workspace has no Cargo toolchain, so it does not fabricate or hand-edit a resolver result. Generate and preserve `Cargo.lock` with Cargo on the compiler-enabled release host, then run `cargo check --locked` / the normal `dev.sh` gates before deployment.
Build-helper tests use synthetic ZIPs; they do not claim to render Swagger. This
environment lacks Rust/Cargo and cannot fetch the upstream assets. Compilation,
real embedding/rendering and live access-control acceptance remain to be run on a
build server. Browser tests still label their simulated API/Swagger renderer.
## Focused live regression probe
After running the built server:
```bash
python3 scripts/admin_audit.py --url http://127.0.0.1:9090
```
The script sends anonymous read-only loopback probes, follows no redirects, ignores
proxy environment variables, prints no response bodies, and fails on unexpected
public content/statuses. It does not send credentials or mutate configuration. It
is also called by the real-binary security integration suite. Rust router tests
add write/method/duplicate-cookie/CSRF checks. A passing probe is a regression gate,
not a complete pentest, dependency scan or guarantee against all vulnerabilities.
## Custom location headers
Vhosts -> edit -> Custom locations -> Location headers. Add rules independently
for proxy requests (to backend) and responses (to client). `set` replaces, `append`
adds another field value, `remove` has no value. Maximum 32 rules per direction.
The existing JSON editor is synchronized and remains optional. Invalid JSON,
newlines, unknown operations and protected names cannot silently save an old draft.
Rules apply global -> vhost -> matching location. Proxy transport, forwarding,
verified authentication identity, configured Server/security/diagnostic policies
remain system-managed and can override or forbid a conflicting custom rule.
Values are literal, not Nginx variables; changing Host/forwarded identity still uses
the designated backend/forwarding controls. Headers for sibling locations remain
unchanged. No configuration schema migration is needed for existing header rules.
## References
- OWASP REST Security Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html
- OWASP Session Management Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
- Swagger cookie authentication: https://swagger.io/docs/specification/v3_0/authentication/cookie-authentication/
- Swagger UI configuration: https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/
- Cargo configuration discovery: https://doc.rust-lang.org/cargo/reference/config.html
- Cargo build scripts: https://doc.rust-lang.org/cargo/reference/build-scripts.html
- Pinned browser release: https://github.com/swagger-api/swagger-ui/tree/v5.32.11
+214
View File
@@ -0,0 +1,214 @@
# Architecture
## Control plane and forwarding plane
SQLite stores administrator intent, including vhosts/paths, policies, backend definitions
and certificate metadata. `config::compile` validates this document and builds an
immutable Snapshot with routing tables, compiled ACL/header policies, per-backend
clients and an optional rustls ServerConfig. ArcSwap publishes the active snapshot.
No public request consults SQLite or a provider to choose its route.
A configuration mutation obtains a bounded admission slot and builds a candidate on
a blocking worker. The slot remains held if the HTTP caller is cancelled while the
worker is running. A serialized detached writer checks the expected revision,
commits the desired document in SQLite and swaps the active snapshot with no await
between commit completion and publication. Invalid candidates leave the old state
active. A crash between persistence and publication is recovered by loading the
committed document at startup; this is not one cross-memory/SQLite transaction.
## Module responsibilities
| Module | Responsibility |
|---|---|
| `server` | Bounded listeners, TLS acceptance, HTTP/1.1/HTTP/2 selection, connection leases and shutdown |
| `proxy` | Host/authority routing, ACL sequencing, forwarding and streaming instrumentation |
| `proxy::websocket` | Handshake validation, upgrade tasks and bounded bidirectional byte transport |
| `upstream` | Per-backend pools, guarded resolver, connection deadlines and verified HTTPS clients |
| `tls` | Local PEM files, generation/import, validation, SNI and loaded certificate snapshots |
| `config` | Desired models, normalization, validation and compiled snapshots |
| `vhost` | Host-owned frontend/member models, actual-address selection and listener planning |
| `balance` | Full-exchange leases, health/recovery, weighted scheduling, connection/affinity settings |
| `security` | Compiled per-vhost response protection, TLS-only HSTS |
| `acl`, `headers` | Generic access and header policies, no application-specific exceptions |
| `auth`, `admin` | Administrator lifecycle, local API, browser sessions, CSRF, SSE and static UI |
| `db` | SQLite schema, migrations, desired-state persistence and revision checks |
| `metrics`, `logging` | Aggregate instrumentation, bounded access ring and trace output |
| `transform` | Bounded decode/transform stage for eligible static identity representations before cache/compression |
| `docker` | Desired Docker-agent settings, in-memory observations and ephemeral vhost reconciliation |
| `runtime` | Shared process state, snapshot updates, admission limits and tunnel tracking |
| `demo` | A separate opt-in demonstration backend with its own listener |
Docker discovery is an optional provider boundary implemented through the standalone
`docker-agent/`; the RustProxy process never opens the Docker socket. Cloudflare Tunnel
remains a future module. Certbot-based HTTP-01/Cloudflare DNS/OVH DNS/RFC2136 issuance and managed renewal are optional
control-plane adapters. Provider work stays outside proxy request paths and
publishes certificates only through validated configuration snapshots.
## Request lifetime
A listener owns a shared TCP connection lease. Each admitted request owns an
in-flight permit and an Exchange, which remains alive through streamed request and
response bodies. HTTP/2 connections share the public TCP cap with HTTP/1.1 and TLS.
Upgraded WebSocket tasks retain both the Exchange and TCP lease after Hyper stops
managing the HTTP connection, preventing upgrades from escaping limits or counters.
Routing captures one snapshot. Socket peer identity, host and route ACLs are checked
before DNS or upstream connection reuse. Forwarding uses a URI built from a stored
backend origin; the public client never supplies an arbitrary destination. Header
rules cannot change the identity already used by ACL. Ordinary bodies pass through
without full aggregation. For an explicitly transform-enabled vhost, eligible bounded
static responses use the logical pipeline `origin -> decode supported Content-Encoding
-> transform -> cache identity representation -> downstream compression`. Origin
`no-transform` is authoritative; changed bytes receive a RustProxy representation ETag
while origin validators are retained separately for revalidation. WebSocket data uses
fixed-size copy buffers.
New requests on existing keep-alive/HTTP/2 connections load the current snapshot.
Existing bodies and tunnels retain the old snapshot/backend. Each accepted TLS
connection captures the current certificate configuration before its handshake.
Certificate validation and file reads are never performed inside the SNI resolver.
## Startup and shutdown
Startup opens/migrates SQLite, loads existing intent, prepares required certificates,
compiles the snapshot and binds the requested listeners. The demo only seeds a new
database. A corrupt configuration is not automatically reset. After the runtime is published,
the optional Docker poll loop contacts enabled agents; failures remain observations and do not reset desired configuration or block ordinary proxy service.
Shutdown cancels listeners and tracked tunnels, drains admitted HTTP connections
within the configured grace period, drains accepted configuration writers and
closes SQLite. Bounded runtime shutdown avoids waiting forever for cancelled OS DNS
workers. This does not turn a blocking getaddrinfo call into a killable operation.
## Deliberate development tradeoffs
The panel remains loopback HTTP. All active configuration changes currently rebuild
backend pools and certificate state, even if unrelated fields changed. Compatible health/counter/affinity observations are retained independently; idle
connection and certificate reuse optimization still requires measurements. Backend loop protection checks local listener addresses by default while allowing remote origins on any valid port. A per-backend `accept_local_listener_risk` opt-in can deliberately allow a local RustProxy listener (for example port 9090) without bypassing private/special-address policy. DNS special-range restrictions
are basic safeguards rather than an independently audited network policy engine.
No performance target has been established or measured for this package.
## 0.1.0 response pipeline and resources
Observe pins one immutable snapshot for the exchange. Host/route ACLs precede the
cache and all upstream I/O. Eligible GET lookup uses effective request headers
and a revisioned representation key. On a miss the origin response is validated
before and after global/route header rules; a bounded tee records complete bodies
while still yielding frames immediately. No request-path SQLite/provider lookup is
introduced. Server policy and optional streaming compression apply after cache
lookup/fill wrapping. The outer MeteredBody counts delivered encoded payload frames.
Cache publication uses a short mutex section and LRU/byte/entry bounds; fill buffers
have eight semaphore slots. Purge increments a generation, preventing older in-flight
fills from publishing. Config commit invalidates cache before snapshot publication,
without awaiting between those operations. New listener intent is persisted but
active sockets remain unchanged until restart; original CLI defaults are retained
separately so clearing an override has accurate desired-state reporting.
`cache`, `transform`, `compression`, `settings`, `diagnostics`, and `system_stats` are dedicated modules. `diagnostics` owns periodic sampling/export while `system_stats` adapts the shared `systemstat` host API, `sysinfo::Networks` network API, and one `metrics-process` process API. Network inventory/counters use `sysinfo` as the primary source and merge `systemstat` as a secondary library source, so missing/partial primary inventory is supplemented without shell commands; no BSD/Linux shell-command network parser is maintained. `metrics-process` uses its documented dummy fallback where no native process collector exists, so unsupported fields remain absent without adding another platform parser. Linux adds its compatibility-only `/proc`/cgroup fields after the common sample. The
sampler runs blocking OS/library reads off the async request path roughly every two
seconds and stops through the process cancellation token. Language discovery runs
in build.rs, not in browser hardcoded PL/EN logic. The result is an embedded public
JSON asset; existing logo resources remain intact.
## Vhost pool publication and observations
The immutable routing snapshot contains actual-address-constrained routes, compiled
frontends, pools and both TLS ALPN configurations. The process separately retains
its actually bound listener plan. New routing uses desired bindings immediately;
new sockets/ALPN take effect only at restart. This can temporarily deny moved hosts,
but never silently broadens them to still-active old bindings.
Each pool owns immutable member definitions and clients. Small synchronized health,
scheduling and traffic observations live separately. Compatible publication reuses
observations/tokens by vhost/member/origin/health identity; changed or re-enabled
members are quarantined. Request leases retain the old member/client until both
body directions or the upgraded tunnel finish. Changes do not replay requests.
A weak-App, cancellation-aware supervisor schedules a rotating, bounded window of
probes outside the data path. DNS and configured-origin policies are shared with
normal forwarding; HTTP checks use a dedicated connection and discard the body.
Traffic sampling and status snapshots operate on these observations, not SQLite.
Forwarding order is actual frontend selection -> host/vhost/path ACL -> eligible
member selection -> member-isolated cache lookup -> guarded upstream request.
Cookie affinity bypasses cache. Cache hits release the selection without recording
an actual upstream request. Response protection is applied after cache selection,
and per-vhost HSTS is removed on plaintext, including synthetic responses.
## Integrations in 0.4.0
`forwarding` compiles the explicit trust boundary and reconstructs canonical
metadata. `tinyauth` owns immutable verifier configuration and an outbound client;
it authorizes before the existing origin/cache path. `http3` adapts bounded Quinn
streams to the same Axum router and response Body without a separate cache/auth
implementation. Named listener IDs resolve to actual bindings while compiling
vhosts; accepted destination addresses still control route visibility.
TLS and QUIC use the same validated certificate resolver. Active endpoint limits
remain fixed until restart; new QUIC handshakes can use the latest certificate
snapshot without changing Retry token keys. Graceful shutdown signals H3 GOAWAY
and bounds drain before endpoint close. Runtime conformance remains to be tested.
## 0.5.0 cache inspection and certificate jobs
Cache decision counters and entry metadata live in the bounded cache store. A
separate authenticated paginated read API returns metadata, not bodies, queries or
request headers. Expiry cleanup also runs during inspection; generation barriers
still prevent old in-flight fills from republishing after purge/config changes.
The ACME manager is outside proxy request paths: one job semaphore, a tracked
child-process group, JSON over stdin, sanitized status and private Certbot/RFC2136
profiles. Only validated immutable certificate metadata can enter the existing
library through explicit revision CAS publication. See ACME_RFC2136.md for the
required external tools and unverified integration boundary; RENEWAL.md documents
the 0.7 scheduler and automated publication.
## 0.6 boundaries
`legacy` is an input-only migration module, not a traffic dispatcher. It emits the
same canonical vhost model used by the UI. Validated startup migration uses one
CAS write; failure before validation completion leaves the stored JSON untouched.
`acme` manages one private worker job; `scripts/acme_provider.py` adapts Certbot
webroot/Cloudflare/OVH/RFC2136. Only the running HTTP-01 job's names and token files
are exposed by the reserved challenge middleware. Provider files remain private,
not part of SQLite or generic configuration exports. Issuance/import are separate.
`prometheus` owns a dedicated read-only HTTP router, actual-peer CIDRs, environment
bearer token and bounded scrape work. It does not share administrator cookies.
Instrumentation is process-local; per-vhost counters reuse stable-ID observations
across configuration changes. Domain cache gauges intentionally reset with revision.
Admin request timing measures response headers, while proxy/vhost timing measures
complete body/tunnel lifetime. Neither renderer queries a provider or SQLite to
handle public proxy requests.
## 0.7 renewal, access and deployment boundaries
A shutdown-tracked weak-reference scheduler selects due configured managed
certificates. It shares the issuance semaphore and per-profile advisory lock.
Provider work remains outside request paths. Publication validates PEM/key/names
and the complete current configuration, then uses the same SQLite CAS/snapshot
path. Only changed generations publish; failures do not touch the active snapshot.
TCP and QUIC handshakes consult current TLS state. Certificate publication also
uses the existing cache-generation invalidation. See RENEWAL.md for timing/limits.
Basic Auth is a separate vhost gate after cumulative IP ACLs and before static
responses, cache and upstream selection. Argon2id verification is bounded by a
shared semaphore held by blocking workers and an IP failure table. Hashes, not
passwords, are persisted. A verified Authorization header is optional and reapplied
after custom request headers; unverified identity headers are removed. Protected
responses bypass the shared cache and authentication failures cannot become custom
200 responses. WebSocket admission uses the same gates before opening a tunnel.
`pool_eligible=false` distinguishes a location-pinned backend from ordinary pool
membership. The GUI edits the same prefix route model as the configuration API;
there is no second nginx-style parser or unvalidated location execution path.
Server deployment uses root-owned immutable release directories, an atomic current
symlink, a non-root systemd service and private service-owned data. A stopped-state
backup precedes update publication. Failed health/version checks attempt to restore
both old code and data; backups and failed-generation state remain for recovery.
Live systemd/database recovery acceptance is not replaced by helper mocks.
+290
View File
@@ -0,0 +1,290 @@
# Shared hot/cold cache
See [CACHE_DIAGNOSTICS.md](CACHE_DIAGNOSTICS.md) for practical HIT/MISS/BYPASS diagnosis.
## Scope
RustProxy implements a bounded reverse-proxy cache with a hot RAM tier and an optional
persistent cold disk tier. Version 0.11.5 aligned request-method handling and most logical
hash behavior with Varnish built-in semantics. Since 0.19.1 the logical primary identity
includes the frontend scheme as well as Host + exact request URL, so `http://host/path`
and `https://host/path` cannot reuse the same stored response. RustProxy still isolates
different vhost/config scopes internally and uses response `Vary` to select representation
variants.
This is not a VCL implementation. Generic Varnish grace, ESI, public unauthenticated
BAN/PURGE and distributed cache state are not implemented. RustProxy 0.22.1 adds bounded
foreground request collapsing, authenticated selective PURGE/BAN and optional proactive
refresh while keeping directive-controlled stale delivery through RFC 5861 and request
`max-stale`; it is not an unconditional availability/grace layer. RustProxy also keeps its
validator-based handling of response `no-cache` instead of reproducing Varnish hit-for-miss
behavior exactly.
## Settings
| Field | Default | Bounds / meaning |
|---|---|---|
| enabled | true | Global master switch; vhosts may disable cache but cannot re-enable a globally disabled cache |
| max_bytes | 268435456 | Hard HOT allocation budget (body backing allocation + charged entry metadata); minimum 64 KiB, no artificial high ceiling |
| max_object_bytes | 16777216 | Maximum logical body accepted by one fill; minimum 1 KiB and at most `max_bytes` |
| max_entries | 4096 | 1..16384 hot objects |
| transient_max_bytes | 67108864 | Hard RAM budget for fill buffers and complete bodies moving between HOT/COLD |
| cold_queue_max_bytes | 33554432 | Byte ceiling for complete demoted objects waiting for the COLD writer; at most `transient_max_bytes` |
| hot_eviction_max_objects | 64 | 1..4096 LRU removals attempted for one HOT admission before the new object passes uncached |
| cold_enabled | false | Enable persistent disk tier; never implied by hot-cache enable |
| cold_path | cache/cold | Relative path under the private RustProxy data directory |
| cold_max_bytes | 4294967296 | 64 KiB..1 TiB disk budget |
| cold_max_entries | 50000 | 1..1000000 cold objects |
| cold_gc_interval_seconds | 60 | 5..86400; interval between asynchronous pressure-GC checks |
| cold_gc_max_delete | 256 | 1..10000; maximum objects removed by one automatic GC pass |
| cold_after_seconds | 300 | 5..604800; idle hot objects are eligible after this delay |
| cold_on_idle | true | Move idle eligible hot objects to cold storage |
| cold_max_hot_seconds | 3600 | 5..2592000; total RAM-residency age used when `cold_on_age=true` |
| cold_on_age | false | Demote objects after the maximum hot-tier age even if they remain frequently accessed |
| cold_on_lru | true | Move objects displaced by hot-tier LRU pressure to cold storage |
| cold_on_shutdown | true | Best-effort direct hot-to-cold flush during ordinary graceful shutdown/restart |
| default_ttl_seconds | 0 | Only explicit public responses without origin freshness |
| max_ttl_seconds | 86400 | 1..604800; maximum configured retention/freshness cap |
| bypass_query | false | Bypass requests containing a query when enabled |
| bypass_prefixes | [] | Up to 64 normalized absolute path prefixes |
| static_ignore_cookies | true | For configured static extensions only, ignore request Cookie for eligibility/keying |
| expose_status | false | Emit `X-Cache: HIT`, `MISS` or `BYPASS` |
| expose_hits | false | Emit per-object `X-Cache-Hits` |
| expose_cache_status | false | Emit standardized `Cache-Status` (RFC 9211) |
| expose_resident_age | false | Emit `X-Cache-Resident-Age` on reused stored responses |
| collapse_enabled | true | Collapse concurrent foreground fills for the same primary cache key |
| collapse_wait_timeout_ms | 1500 | 10..60000 ms follower wait before fallback |
| collapse_timeout_fallback | origin | `origin` or `stale_if_available` after follower timeout |
| stale_if_error_timeout | true | Allow RFC 5861 stale fallback for timeout failures |
| stale_if_error_connection | true | Allow stale fallback for connection failures |
| stale_if_error_dns | true | Allow stale fallback for DNS failures |
| stale_if_error_tls | true | Allow stale fallback for TLS failures |
| stale_if_error_statuses | [500,502,503,504] | Up to 32 unique eligible HTTP 5xx statuses |
| proactive_refresh_enabled | false | Schedule sampled cached objects for validator refresh before expiry |
| proactive_refresh_before_expiry_seconds | 15 | 1..604800 seconds before expiry |
| proactive_refresh_percent | 10 | 0..100 deterministic percentage of primary keys |
| proactive_refresh_max_inflight | 4 | 1..64 concurrent proactive refreshes |
The hot tier enforces `max_bytes` and `max_entries` before publication. HOT accounting uses the retained body allocation (the fill buffer capacity transferred into `Bytes`) plus directly owned entry metadata and a conservative per-entry charge for the HOT hash/LRU/variant indexes, while `hot_logical_bytes` reports only visible body lengths. Rust container/allocator APIs do not expose every bucket/node allocation exactly, so `hot_allocated_bytes` is intentionally a conservative charged estimate rather than a promise that it equals process RSS. In-progress fills reserve a separate bounded transient budget before growing their buffers. When HOT pressure would exceed the budget plus a small allocator-rounding headroom (roughly 1/64 of HOT, capped at 1 MiB), RustProxy removes LRU objects first; if admission still cannot be made safe within `hot_eviction_max_objects`, the response continues normally without being cached. `max_object_bytes` is checked incrementally before every body-buffer growth, so an oversized response is not fully accumulated before rejection.
When cold storage is enabled,
`cold_on_lru`, `cold_on_idle` and `cold_on_age` independently choose which hot objects are demoted.
`cold_after_seconds` measures time since the last access. `cold_max_hot_seconds` measures total
hot-tier residency, so it can move a frequently accessed object to disk after a configured age.
The administration UI exposes these as separate policies to avoid conflating "old" with "unused".
Demotions use an asynchronous queue bounded by both object count and `cold_queue_max_bytes`. A demoted `Entry` is removed from HOT accounting and moved, not copied, into a `ColdDraft`; its `Bytes` payload remains charged to `transient_max_bytes` until persistence completes and the draft is dropped. The persisted `ColdEntry` contains paths/metadata rather than the body allocation. This prevents the COLD writer queue from becoming a second unbounded RAM cache. If an HTTP response is already using a clone of the same `Bytes`, that last transport-owned reference can keep the backing allocation alive until the response finishes; the cache-owned HOT/COLD reference itself is released after persistence. Process RSS is therefore monitored separately from cache accounting. Supported
glibc/Linux builds ask the allocator to release unused pages after bulk demotion. During
ordinary graceful shutdown, `cold_on_shutdown` performs a best-effort direct flush of remaining hot
objects before the store is closed. A **managed software update** (`update.sh` / `scripts/deploy.py`)
uses a private one-shot marker and, on RustProxy 0.13.2+, forces this hot-to-cold handoff whenever
the cold tier is enabled even if `cold_on_shutdown=false`. The updater gives the process a larger
stop budget and removes the marker before backing up state. If the cold tier is disabled there is
nowhere persistent to hand the hot objects, so RAM-only cache is still lost on process exit. The
body is stored as a raw `.body` file and metadata as JSON with a SHA-256 body digest. A fresh cold hit is integrity-checked and promoted to
RAM. Cold format 3, introduced in 0.21.0, stores objects below the first two hexadecimal
characters of the SHA-256 cache key, for example `cache/cold/7f/<hash>.body` and
`cache/cold/7f/<hash>.json`. Shard directories are created lazily. The previous flat
Cold Cache layout is deliberately not migrated; legacy flat objects are removed and can
be refilled from origin. Format 3 metadata also carries RFC 5861 windows, revalidation
flags, origin validators, last access and hit count.
Automatic Cold Cache GC is asynchronous. When either byte or entry usage reaches about
90% of its configured limit, it selects old/rarely used candidates and cleans toward
about 80%, removing at most `cold_gc_max_delete` objects in one pass. Metadata is
snapshotted in small batches and disk deletion is queued instead of holding request-path
state across a full-cache sort/delete loop.
## Varnish-style identity and variants
The logical primary cache identity is:
- a stable cache-compatibility revision derived only from settings that can change
cached representation/origin semantics, plus vhost scope (internal isolation),
- normalized frontend scheme (`http`/`https`),
- normalized original Host authority, including an explicit port,
- exact request path and query.
Selected backend-pool member, route/backend IDs and unrelated request headers are not
part of the representation hash. This means two healthy members of the same pool can
serve/fill the same public cache object, while HTTP and HTTPS remain isolated. Headers
such as User-Agent, X-Forwarded-For or browser client hints no longer create unnecessary
variants by themselves.
The response `Vary` header provides the secondary representation identity. For example,
`Vary: Accept-Language` stores independent language variants under the same scheme + Host + URL
primary hash. `Vary: *`, malformed Vary fields or an excessive Vary field set are not
stored. If an origin changes the Vary schema for a URL, incompatible old variants are
removed so an obsolete no-Vary object cannot shadow a newer Vary representation (or
vice versa).
Cookie and Authorization requests still pass by default, matching Varnish built-in
safety behavior. RustProxy's static-cookie optimization can ignore Cookie only for the
configured static file extensions; response `Set-Cookie`, `private`, `no-store` and
`Vary: Cookie` still prevent unsafe shared reuse.
## Request methods and invalidation
GET and HEAD participate in lookup. They share one representation identity. On a
cacheable HEAD miss RustProxy fetches the backend representation as GET, may populate
the shared object, and strips the response body before delivering the original HEAD
request.
POST, PUT, PATCH, DELETE, OPTIONS and TRACE pass to the backend and are not stored.
They do **not** implicitly purge the whole cache. This fixes the pre-0.11.5 behavior
where any successful unsafe request could erase unrelated static objects and destroy
the hit ratio.
Cache invalidation remains explicit:
- authenticated full purge can clear all objects or one vhost scope,
- selective PURGE supports exact URL, URL prefix, `Cache-Tag`, safe regex and primary cache key, with optional host and vhost restrictions,
- exact/prefix matching uses the stored request target including query; a match removes the whole `Vary` family under that primary cache key,
- selective PURGE operates on RAM and Cold Cache and advances a publication generation so no fill that started before the operation can publish afterward, even if the selector depended on response metadata such as `Cache-Tag`,
- BAN records a bounded TTL rule without synchronously scanning the entire store; objects created before a matching active BAN are rejected during lookup/insertion and are removed later by bounded GC,
- new fills created after a BAN rule are allowed, so BAN invalidates the old representation set rather than permanently blocking a URL,
- regex rules and prefix `/` are treated as broad operations and require explicit confirmation,
- only cache-compatibility changes (cache policy, configured header/origin semantics or vhosts) advance the normal cache generation/revision; unrelated logging/TLS/admin/metrics/response-presentation saves retain objects.
There is no public unauthenticated PURGE/BAN endpoint. If an application changes a GET
resource after POST/PUT/PATCH/DELETE, use origin freshness/validators or the authenticated
administration invalidation path; RustProxy does not guess the affected URLs.
## Eligibility and freshness
Hard request bypasses include Cookie (outside the static exception), Authorization,
Upgrade, `If-Match`, `If-Unmodified-Since`, nonempty request bodies, configured path/query
exclusions and request `Cache-Control: no-store`.
A single `Range: bytes=...` request is special rather than a normal bypass. When a complete
fresh cached representation is available, RustProxy evaluates `If-Range` and can return
`206 Partial Content` directly with `Content-Range`, `Accept-Ranges`, the correct sliced
`Content-Length`, validators and normal cache `Age`; an unsatisfied range returns `416`
with `Content-Range: bytes */<length>`. A mismatching `If-Range` returns the complete cached
representation. If origin access is needed, RustProxy forwards the Range request as an
identity, non-transformed stream. Multiple/multipart, duplicate or otherwise unsupported
Range forms are passed through to origin and are never stored as separate range objects.
A fresh cached representation can satisfy `If-None-Match`/`If-Modified-Since` locally.
Request `no-cache`, `max-age=0` and legacy `Pragma: no-cache` require validation when a
safe stored validator exists. `max-age=N` and `min-fresh=N` are freshness constraints:
they reuse the cached response when its current Age/remaining freshness satisfies the
client limit and otherwise drive normal revalidation/fetch behavior. Request `max-stale`
can explicitly allow a retained stale response: bare `max-stale` accepts any retained
staleness, while `max-stale=N` bounds it. Freshness expiry does not itself physically
delete an object; normal RAM LRU/Cold Cache GC may still evict it. `must-revalidate` and
`proxy-revalidate` override client stale permission. With a safe validator, an origin
304 refreshes metadata and reuses the stored body.
Responses with `Set-Cookie`, `private`, `no-store`, `Content-Range`, trailers,
SSE/gRPC, `Vary: *`, invalid freshness metadata or unusable size/lifetime are not
stored. Response `Cache-Control: no-cache` may be retained only with ETag or
Last-Modified and must validate before every reuse. Both origin and transformed response
policy must remain eligible.
`s-maxage` takes precedence over `max-age`, then Expires/Date. `Age` uses the RFC 9111
current-age model and includes origin age; it is not a measure of how long the object has
been in RustProxy. Cache metadata keeps normal TTL, `stale-while-revalidate`,
`stale-if-error`, `must-revalidate` and `proxy-revalidate` separately. A representation
is therefore **fresh**, **stale-but-servable** only under an applicable directive, or
**expired/unusable**.
Inside `stale-while-revalidate=N`, RustProxy may return stale immediately and starts one
deduplicated background refresh for that cache identity. Inside `stale-if-error=N`, stale
is used only after an eligible origin failure. Version 0.22 makes timeout, connection, DNS
and TLS classes independently configurable and uses an explicit configurable 5xx status
allowlist; defaults preserve the previous behavior (`500`, `502`, `503`, `504` plus all four
transport classes). After TTL plus the corresponding stale window, that mode is unavailable.
`must-revalidate` and `proxy-revalidate` still permit storage, but prohibit all stale delivery
after expiry, including RFC 5861 and request `max-stale`.
Optional proactive refresh schedules a deterministic percentage of cache keys before normal
expiry. It reuses the same validator-based refresh logic and background-revalidation guard,
keeps only one schedule per key, and enforces a global concurrent-refresh bound. A newly
completed fill can schedule this work immediately, so sampled objects do not require another
client request to start the pre-expiry timer. The feature is disabled by default.
## Resident age
`X-Cache-Resident-Age` is the RustProxy-specific whole-second clock for how long the
stored representation has remained in this cache. In 0.11.5 that clock is independent
from the HTTP freshness `Age` clock and survives:
- ordinary hot-cache HITs,
- successful backend 304 revalidation,
- hot-to-cold demotion,
- cold-to-hot promotion.
A value of `0` is valid only during the first sub-second of residency. Revalidation or
tier movement must no longer reset a long-lived object to zero.
## Streaming and capacity
RustProxy tees response frames while delivering them; it does not wait for the complete
origin body before sending the first bytes. Only a complete successful body is
published. Oversize, body error, trailer and cancelled-fill paths discard the candidate.
Large buffers grow incrementally and consume multiple bounded fill permits.
The configured resident byte limit accounts for body bytes, estimated header storage
and fixed per-entry overhead; it is not total process RSS. Active HIT references,
in-flight origin responses, compression buffers and runtime allocations also use memory.
With `collapse_enabled=true`, the first foreground MISS for a primary cache key owns the
fill while concurrent followers wait for publication. The fill guard is held through the
actual response-body tee, so followers cannot wake merely because origin headers arrived.
Leader error/cancellation releases waiters. A follower that wakes to a different `Vary`
representation re-enters the single-flight decision instead of immediately stampeding the
origin. After `collapse_wait_timeout_ms`, the configured fallback is either an independent
origin request or an eligible stale response. SWR/background revalidation keeps its separate
existing deduplication guard.
## Diagnostics and API
`GET /api/cache` reports settings, HIT/MISS/BYPASS, hot/cold counts and bytes,
promotions/demotions, errors, fill pressure, object views and per-domain reason counters.
Hit ratio is `hits / (hits + misses)` and intentionally excludes BYPASS traffic.
`POST /api/cache/purge` requires the existing administrator session, same-origin marker
and CSRF token. `?vhost=<id>` purges only that vhost scope; omitting it purges all cache
objects. Purge is audited.
`POST /api/cache/invalidate` performs authenticated selective `purge` or `ban` using
`exact`, `prefix`, `tag`, `regex` or `cache_key`. Optional `host` and `vhost` restrict scope;
BAN accepts a bounded TTL. Regex and prefix `/` require `confirm_wide=true`. The result
reports the selector/scope, affected entry count, elapsed time, status and BAN id when one
was created. `GET /api/cache/bans` lists active rules and `DELETE /api/cache/bans/{id}`
removes one. These operations use the normal administration audit path. The Cache panel
exposes the same controls plus per-object **Purge URL**.
`POST /api/cache/cold/cleanup` starts a selective asynchronous Cold Cache cleanup using
the same candidate/removal machinery as automatic GC. Criteria can select entries unused
for a duration, cap removal to the oldest N objects, and/or require `hits == 0`.
`GET /api/cache/cold/cleanup/{id}` reports job status, removed object count and freed
bytes. Purge-all and purge-vhost remain the full-clear mechanisms; selective invalidation
is deliberately separate.
`X-Cache`, `X-Cache-Hits`, `Cache-Status`, `X-Cache-Reason` and
`X-Cache-Resident-Age` are optional diagnostics. `Cache-Status` is the standardized
RFC 9211 field name; RustProxy does not rename it to the legacy/custom `X-Cache-Status`.
HTTP/2 and HTTP/3 serialize field names in lowercase, so `cache-status:` on the wire is
normal. When the cached representation was produced by the transform pipeline,
RustProxy appends the extension parameter `transformed`, for example:
```http
Cache-Status: RustProxy; hit; ttl=556488; detail="cache_hit"; transformed
```
Origin lookalike values (`Cache-Status`, `X-Cache-Status` and the other RustProxy
cache diagnostics) are removed before RustProxy emits its own diagnostics. HTTP `Age`
remains present on reused cached responses because it is protocol freshness metadata,
not a debug header. The Cache dashboard also marks transformed objects explicitly.
## Pool behavior
Backend selection and health policy still run before an origin fetch. The selected pool
member no longer fragments the cache identity in 0.11.5, so a fresh public object can be
reused regardless of which healthy member would have been selected for a MISS. A pool
with no eligible member still returns 503 before RustProxy can use the cache; this cache
is not a stale-availability layer.
Cookie affinity and protected/authenticated traffic keep their existing privacy bypass
rules. Per-vhost response/security policy also applies to HIT responses.
+187
View File
@@ -0,0 +1,187 @@
# Cache diagnostics and behavior in 0.22.3
This describes implemented source, not a verified Rust runtime. See
[TEST_REPORT.md](TEST_REPORT.md).
## 0.19.1 cache identity safety
The frontend scheme is part of the primary cache identity. `http://host/path` and
`https://host/path` therefore cannot share an object, preventing responses that depend
on `X-Forwarded-Proto` or equivalent origin logic from crossing schemes.
## 0.11.5 cache changes
The cache follows the Varnish built-in model for the behaviors that most directly
affect hit ratio, with the 0.19.1 scheme-isolation difference noted above:
- GET and HEAD use the cache; other ordinary methods pass without clearing unrelated objects.
- The logical primary identity is frontend scheme + normalized Host authority + exact URL inside the current vhost/config scope.
- Selected backend-pool member and unrelated request headers do not fragment the primary object.
- Response `Vary` chooses representation variants under that object.
- GET and HEAD share the same stored representation; a cacheable HEAD miss can fill it from a backend GET.
- `X-Cache-Resident-Age` survives 304 revalidation and hot/cold tier movement.
RustProxy still deliberately bypasses Cookie and Authorization requests by default,
matching Varnish built-in safety behavior. The configured static-extension optimization
can ignore Cookie only for selected static paths, subject to response privacy guards.
## Why objects used to disappear after POST/PUT/etc.
Before 0.11.5 a successful non-safe request could call a global cache purge. A periodic
POST, API heartbeat or form submission could therefore erase every cached static object
even though the request was unrelated to those URLs. In 0.11.5 POST/PUT/PATCH/DELETE,
OPTIONS and TRACE pass through and do not implicitly purge the shared cache.
Only configuration changes that alter cache/origin compatibility invalidate incompatible cache
state; unrelated control-plane saves keep reusable hot/cold objects. Explicit administrator purge
still invalidates the selected cache state. Use the purge action when the application really needs
explicit invalidation.
## `Age` versus `X-Cache-Resident-Age`
These are different clocks:
- `Age` is HTTP freshness age. It can already be thousands of seconds when RustProxy
receives an object because the origin/CDN `Date`/`Age` contributes to it.
- `X-Cache-Resident-Age` is RustProxy-specific and counts whole seconds that the stored
representation has remained resident in this cache.
In 0.11.5 resident age is preserved across a successful backend 304 and across
hot-to-cold/cold-to-hot movement. An immediate first HIT can legitimately show `0`
because less than one full second has elapsed; a long-lived object should then keep
increasing rather than repeatedly resetting to zero.
## Practical HIT test
Enable cache plus `X-Cache`, `X-Cache-Hits` and `X-Cache-Resident-Age`, then make
repeated identical requests that read the complete body:
```bash
for n in 1 2 3; do
curl -q -sS -D - -o /dev/null \
-H 'Host: app.localhost' \
http://127.0.0.2:8888/cache
sleep 2
done
```
For an eligible response, expect MISS then HIT/HIT. `X-Cache-Resident-Age` should grow
between later HITs. A browser is a poorer diagnostic because its own cache may satisfy
a navigation without sending any request to RustProxy.
HEAD is now eligible too. It shares the GET representation, but GET is still the best
basic test because it proves that the body was completely consumed and published.
## Vary test
A response with:
```text
Vary: Accept-Language
```
stores language variants under one primary scheme + Host + URL object identity. Two English
requests can HIT each other; a Polish request must not receive the English body. If the
origin later changes between no `Vary` and `Vary: Accept-Language`, incompatible older
variants are removed rather than used as a fallback.
`Vary: *` is not cacheable.
## Cookie/static behavior
`request_cookie` is a BYPASS by default. With `static_ignore_cookies=true`, configured
static extensions such as `.css`, `.js`, fonts and images ignore the request Cookie for
cache eligibility/keying. This is intentionally limited to static paths. A response
with `Set-Cookie`, `Cache-Control: private/no-store`, or `Vary: Cookie` still cannot be
shared through that optimization.
This setting is useful for raising the asset hit ratio for logged-in users without
turning dynamic personalized pages into shared objects.
## Conditional requests and reloads
Fresh cached objects can satisfy `If-None-Match`/`If-Modified-Since` locally. Request
`Cache-Control: no-cache`, `max-age`, `min-fresh` and legacy `Pragma: no-cache` select
validation instead of creating a separate cache key. If a stored validator exists and
the backend returns 304, RustProxy refreshes metadata and reuses the body.
Hard browser reloads commonly add revalidation directives, so they may produce
`cache_revalidated` instead of a plain `cache_hit`. This is different from a cache miss.
## Reason tokens
When cache diagnostics are enabled, these fixed reason tokens explain decisions:
| Token | Meaning |
|---|---|
| `cache_disabled` | Effective cache is disabled. |
| `route_disabled` | Selected route explicitly disables cache. |
| `request_method` | Method is not GET/HEAD. It passes and does not purge the cache. |
| `request_cookie`, `request_authorization` | Private request state bypasses shared cache. Static Cookie exception may apply. |
| `request_no_store` | Client forbids use/storage of shared cache. |
| `request_no_cache`, `request_max_age`, `request_min_fresh`, `request_pragma_no_cache` | Client requests validation. |
| `request_body`, `request_range`, `request_precondition` | Request is outside reusable shared-object semantics. |
| `query_excluded`, `path_excluded` | Configured exclusion matched. |
| `tinyauth`, `sticky_cookie` | Authentication/affinity policy deliberately bypasses shared cache. |
| `response_set_cookie`, `response_cache_control` | Origin response is not safely shareable. |
| `response_vary`, `response_ttl_size_or_metadata` | Vary/freshness/size/metadata prevents storage. |
| `response_headers_policy` | Transformed client-response policy is not cacheable. |
| `cache_miss`, `cache_hit` | No reusable variant was found, or a variant was reused. |
| `cache_conditional_hit` | Fresh cache metadata satisfied the client validator locally. |
| `cache_revalidated` | Backend 304 validated the stored body and it was reused. |
| `cache_revalidated_uncached` | Validation succeeded but refreshed policy no longer permits storage. |
A MISS only means lookup found no reusable object. The later origin response can still
fail storage because of privacy headers, size, incomplete streaming, trailers or a
changed policy.
## Memory accounting in 0.16.0
The process **RSS total** is an operating-system measurement and is intentionally shown separately from RustProxy's own cache accounting. Do not add the rows together: they overlap. The process block reports:
- `RSS total` (`VmRSS`) - all resident pages currently attributed to the process;
- `RSS anonymous` (`RssAnon`) - anonymous resident pages, which typically include heap/stacks and other private allocations;
- `RSS file-backed / shared` (`RssFile` / `RssShmem`) - resident mapped file/shared-memory pages;
- `data address space / swap` (`VmData` / `VmSwap`) - address-space and swapped-page context, not extra resident bytes.
The cache block separately reports memory RustProxy can attribute to the cache subsystem:
- `hot logical` - visible cached body bytes (`body.len()`);
- `hot allocated` - the hard-budget charge for retained backing allocations plus entry metadata (`resident_bytes` remains a compatibility alias);
- `transient` - bounded cache allocations temporarily outside the hot store;
- `fill` - active response buffers being assembled before publication;
- `cold queue` - complete bodies waiting for cold-tier persistence;
- `cold I/O` - bodies currently being written to or read from the cold tier.
This explains why, for example, a process RSS around 630 MiB can coexist with only about 100 MiB of reported hot cache: the old `Pamięć hot` card was never a whole-process memory counter. TLS/QUIC/Hyper/Tokio allocations, thread stacks, configuration/routing state, active request bodies and allocator-retained pages are also part of RSS but are not falsely assigned to a RustProxy subsystem when the source cannot measure them directly.
The existing Linux/glibc allocator-release path remains in place: large cache demotion/purge paths call `malloc_trim(0)` through `release_allocator_memory()`. Since 0.24.5, HOT allocation, active fills and bodies moving to/from COLD are explicitly budgeted. COLD queue admission is byte-bounded as well as count-bounded. RSS is still a process-wide view containing the same cache memory plus TLS/QUIC/Hyper/Tokio, stacks and allocator retention, so cache gauges must not be added to RSS. Long-running acceptance should verify that HOT stays within `hot_limit_bytes`, transient stays within `transient_limit_bytes`, demotion drains transient bytes, and process RSS reaches a practical plateau under representative traffic.
## Hot and cold tiers
HOT RAM is bounded by allocated/charged `max_bytes` and `max_entries`; `hot_logical_bytes` separately exposes payload length. Fill and COLD handoff/read memory are bounded by `transient_max_bytes`, and queued demotions also obey `cold_queue_max_bytes`. Optional cold storage demotes LRU
or sufficiently idle hot objects asynchronously. Cold hits verify the body digest and
promote the object back to RAM. Version 0.11.5 uses cold metadata format 2 to persist
Vary identity and resident age; old-format metadata is not reused.
Cold is not required for normal hot-cache stability. Disabling cold must not cause hot
objects to disappear on a timer. With cold disabled, hot objects leave RAM only because
of expiry, hot capacity/LRU pressure, explicit purge, configuration generation changes,
or replacement of the same logical representation.
## Cache panel and API
The Cache panel shows hot/cold object counts, RAM/disk usage, HIT/MISS/BYPASS, hit ratio,
fill pressure, evictions/expiry, per-domain decisions and stored objects. Live polling
refreshes the view; it does not itself purge cache objects.
`GET /api/cache?offset=0&limit=50` returns aggregate and object data. Add `vhost=ID` to
filter. `POST /api/cache/purge?vhost=ID` invalidates one vhost; omitting the query purges
all cache objects. Mutations remain authenticated, CSRF-protected and audited.
Hit ratio is `HIT / (HIT + MISS)`. BYPASS requests are excluded, so a large number of
cookie/authenticated requests can reduce backend offload without mathematically lowering
that displayed ratio. For the highest safe reusable-object ratio, give static assets
long explicit public TTLs, keep their URLs stable/immutable, leave the static-cookie
optimization enabled when appropriate, and avoid unnecessary `Vary` fields.
+43
View File
@@ -0,0 +1,43 @@
# Container / OCI deployment
RustProxy ships a baseline multi-stage `Dockerfile`. It compiles the locked Rust
release and runs it as the unprivileged `rustproxy` user (UID 10001). The core image does
not mount the Docker socket. Docker discovery uses the separate `docker-agent/` process on
each Docker host, so the proxy container never needs Docker-socket access. See
[Docker discovery](DOCKER_DISCOVERY.md).
Build locally with a normal OCI builder:
```bash
docker build -t rustproxy:0.22.3 .
```
Persist `/var/lib/rustproxy`; it contains the SQLite database, certificates and the
optional cold-cache tier. The default container proxy listener is `0.0.0.0:8080`.
Administration remains `127.0.0.1:9090` inside the container and is intentionally
not remotely reachable by merely publishing port 9090.
```bash
docker volume create rustproxy-data
docker run --rm -d --name rustproxy \
-p 8080:8080 \
-v rustproxy-data:/var/lib/rustproxy \
rustproxy:0.22.3
docker exec rustproxy rust-proxy admin setup-token
```
To expose administration, first enable administration HTTPS in the persisted
RustProxy configuration and choose a certificate, then bind the administration
listener to a non-loopback address. Publish the admin port only after HTTPS is
active. Plain remote HTTP keeps the legacy explicit risk gate.
The runtime image includes Python, Certbot and OpenSSL for the built-in HTTP-01
certificate helper. Provider-specific Certbot DNS plugins are not installed in the
baseline image; extend the image with the required plugin package before using a DNS
provider.
For access-log files, prefer a writable path under `/var/lib/rustproxy` or mount a
separate writable log volume. The built-in JSON file sink is disabled by default.
Low ports such as 80/443 require an explicit container capability or host port
mapping; the image itself does not run as root.
+96
View File
@@ -0,0 +1,96 @@
# Custom locations
RustProxy 0.22.3 supports the common location-level controls that are typically used for application reverse proxies. The UI intentionally exposes a safe subset instead of accepting arbitrary Nginx configuration text.
## Routing model in 0.16.0
The vhost's main backend pool is an internal fallback and is **not** one of the Custom locations. Custom locations are optional overrides only. Request routing is therefore:
```text
exact custom location
-> priority/regex/prefix custom location according to the documented precedence
-> main vhost backend pool
```
An empty Custom locations list is valid. Adding `/test`, a regex, a local response or a dedicated-backend location cannot disable `/`, `/abc` or other unmatched paths. Older saved documents that contain the former untouched synthetic `root` prefix `/` entry are normalized automatically. If `/` was deliberately customized with a backend, cache/ACL/header rule, rewrite or local response, it remains a real Custom location and intentionally overrides the main fallback.
## Dashboard editor
The visual editor is the primary workflow. Every location card now shows a live **Effective behavior preview** at the bottom with the match mode/pattern, proxy or local-response action, selected backend, rewrite behavior, cache policy, ACL composition and request/response header-rule counts. The preview is calculated from the unsaved form state so an administrator can verify intent before saving.
Request and response header rules use numbered visual cards with localized **Operation / Header name / Header value** controls. `Set`, `Append` and `Remove` are supported. The Advanced JSON view remains available for automation/copy-paste, but is no longer required for normal editing. The documentation button in the location tab explains precedence, rewrites, local responses, policy composition and common examples.
## Match modes
Each location has an ID, a pattern and one match mode:
| RustProxy mode | Nginx-style equivalent | Behaviour |
| --- | --- | --- |
| `prefix` | `location /path/` | Longest matching path prefix. A matching regex may still win. |
| `prefix_priority` | `location ^~ /path/` | Longest prefix and, when selected, skips regex locations. |
| `exact` | `location = /path` | Exact normalized path only. Exact matches have highest priority. |
| `regex` | `location ~ pattern` | Case-sensitive Rust regular expression. Regex locations are tested in configuration order. |
| `regex_insensitive` | `location ~* pattern` | Case-insensitive Rust regular expression. Regex locations are tested in configuration order. |
Routing and ACL checks collapse repeated literal slashes, matching Nginx's usual `merge_slashes on` behaviour. For example `/static/js//events.js` is routed as `/static/js/events.js`. Unless a rewrite is configured, the original request target is still sent to the backend.
Encoded separators (`%2f`, `%5c`), encoded `%`, dot segments and other ambiguous path forms are still rejected instead of being silently normalized.
## Actions
A location can either proxy to a backend or return a local response.
### Proxy
The location can use the vhost pool or pin traffic to one backend. A dedicated backend can be created from the location editor. Cache policy can inherit from the vhost or be explicitly enabled/disabled for the location.
Supported path transformations:
- **Preserve** — send the original client path unchanged.
- **Strip prefix** — remove the matched prefix before proxying.
- **Replace prefix** — replace the matched prefix with another normalized path prefix.
- **Regex rewrite** — available for regex locations; Rust regex captures can be referenced as `$1` or `${1}` in the replacement.
The query string is preserved. Rewrites change only the upstream path; cache identity remains based on the public client URL.
### Local response
`Return custom response` is the equivalent of a simple Nginx `return`: choose an HTTP status from 200 through 599, a Content-Type and a response body. RustProxy does not open a backend connection for this action. `HEAD`, `204` and `304` responses do not send a body.
For a redirect, use a 3xx status and add a `Location` response header in the visual header configurator.
## Request and response headers
Every location has a visual header configurator and an Advanced JSON view. Both edit the same data.
Supported operations are:
- `set` — replace the header with the configured value;
- `append` — append another header value;
- `remove` — remove the header.
Example JSON:
```json
{
"request": [
{"op": "set", "name": "X-App-Route", "value": "api"}
],
"response": [
{"op": "set", "name": "Cache-Control", "value": "no-store"},
{"op": "remove", "name": "X-Legacy-Header"}
]
}
```
Hop-by-hop/framing headers, forwarding identity headers, RustProxy request IDs and WebSocket handshake headers are protected and cannot be overwritten by custom rules. This prevents a location rule from changing transport framing or spoofing the verified client identity.
## Access control
A location can define its own IPv4/IPv6 CIDR ACL. The location ACL is additive: it cannot override a deny enforced by the host policy, reusable ACL profile or vhost ACL.
Authentication, vhost rate limits, country policy, security headers, compression, custom error pages and WebSocket policy still apply through the normal vhost pipeline.
## What is intentionally not an Nginx parser
Custom locations do **not** execute arbitrary Nginx directives or configuration snippets. RustProxy does not currently implement location-level `try_files`, filesystem aliases/roots, embedded scripting, arbitrary `if` expressions or arbitrary module directives. Use the explicit UI controls and the documented JSON schema instead; unknown fields are rejected on save.
+147
View File
@@ -0,0 +1,147 @@
# Docker discovery and dynamic vhosts
RustProxy 0.22.3 includes an optional standalone Docker discovery agent. The RustProxy core never opens the Docker socket. Run `docker-agent/rustproxy-docker-agent` on every Docker host that should be discovered; RustProxy polls each agent over HTTPS by default using that agent's Bearer token and certificate pin.
## State model
- **Desired state** is stored in the normal RustProxy SQLite configuration: agent endpoint/token, per-agent base domain, optional default backend IP, listener IDs, optional frontend certificate and service-to-vhost assignments.
- **Observed state** stays in memory: Docker host addresses, container metadata and every non-loopback published TCP binding.
- A successful poll reconciles enabled assignments into ephemeral vhosts. Polling does not write a new configuration revision.
- If a published host port, binding or detected host address changes, the next poll rebuilds the effective route.
- If a service disappears, its dynamic vhost disappears until the service is observed again.
- Docker discovery is optional; ordinary vhosts and all other proxy functions remain independent of Docker.
The agent returns only running containers with Docker-published TCP ports. Bindings limited to loopback (`127.0.0.0/8` or `::1`) are excluded. Multiple bindings for the same container port are retained instead of being collapsed to one address/port pair.
## Install an agent on a Docker host
The agent is written in Go and uses only the standard library.
```bash
cd docker-agent
sudo ./install.sh
```
The installer runs the Go tests, builds one binary, creates the `rustproxy-docker-agent` system user, generates a random token on first install, starts `rustproxy-docker-agent.service` and prints the persistent self-signed certificate SHA-256 fingerprint.
Manual build:
```bash
cd docker-agent
go test ./...
go build -trimpath -ldflags='-s -w' -o rustproxy-docker-agent .
```
Agent configuration is read from `/etc/rustproxy/docker-agent.env`:
```text
RUSTPROXY_DOCKER_AGENT_LISTEN=0.0.0.0:9876
RUSTPROXY_DOCKER_AGENT_TOKEN=<long-random-token>
RUSTPROXY_DOCKER_AGENT_TLS=true
RUSTPROXY_DOCKER_AGENT_CERT_FILE=/var/lib/rustproxy-docker-agent/agent.crt
RUSTPROXY_DOCKER_AGENT_KEY_FILE=/var/lib/rustproxy-docker-agent/agent.key
RUSTPROXY_DOCKER_ADVERTISE_IP=
RUSTPROXY_DOCKER_PUBLIC_IP_URL=https://api.ipify.org
```
`RUSTPROXY_DOCKER_ADVERTISE_IP` is only a preferred address hint; it does not hide the other host addresses. The inventory includes usable addresses found on active host network interfaces, the primary routed address, the optional public-IP lookup result and the advertised address. Loopback/link-local and obvious Docker-generated bridge/veth interfaces are omitted; explicit Docker bind addresses are still preserved with each service. Each host-address entry includes its source and, where available, interface name/scope.
The inventory endpoint is `GET /v1/inventory` and requires `Authorization: Bearer <token>`. `GET /healthz` exposes no inventory. HTTPS is enabled by default. The agent creates an ECDSA P-256 self-signed certificate once and keeps it in `/var/lib/rustproxy-docker-agent/`; the key is mode `0600`.
`install.sh` prints both the generated/reused bearer token and the SHA-256 certificate fingerprint. Copy the SHA-256 fingerprint printed by `install.sh` into the agent settings in RustProxy. You can print it again with `sudo rustproxy-docker-agent --print-fingerprint`. With a fingerprint configured, RustProxy accepts only that exact certificate and still checks the TLS handshake signatures. If the agent uses a certificate issued by a normal public CA, leave the fingerprint empty to use standard CA/hostname verification. Plain HTTP is retained only for explicit compatibility setups and exposes the Bearer token to the network.
Docker-socket access is privileged. Restrict access to the agent port to the RustProxy host even when TLS is enabled.
## Configure multiple Docker hosts
Open **Settings -> Docker agents** and create one entry for every Docker host. Each agent also has its own inventory **poll interval** (5..3600 seconds, 30 seconds by default); manual refresh bypasses the interval. Agents are independent and may use different domains, for example:
```text
docker-a -> docker1.lab.local
docker-b -> docker2.lab.local
docker-c -> apps.example.net
```
For each agent configure:
- stable agent ID and display name;
- agent endpoint, normally `https://10.10.0.21:9876`;
- the Bearer token from that Docker host;
- the SHA-256 certificate fingerprint printed by the default self-signed agent installer, or an empty fingerprint only for a public-CA certificate;
- that agent's base domain;
- optional default backend IP;
- one or more RustProxy listener IDs;
- an optional configured frontend certificate to apply to every generated vhost for that agent;
- an optional default ACL profile inherited by generated vhosts unless a service selects its own profile.
RustProxy polls each enabled agent according to its configured interval (30 seconds by default, checked by the scheduler in bounded steps). The 5-second Live-view refresh reads only cached RustProxy memory and does not force a Docker request. **Docker vhosts** shows the latest cached observation for every configured agent.
## Publish a discovered service
Use the search field in **Docker vhosts** to filter by agent, service, hostname or alias. Individual service rows can be hidden from the normal view; **Show Hidden** beside Docker agents settings reveals them so they can be restored. This is a per-browser view preference and does not disable or unpublish the vhost.
For each discovered service you can change the mapping directly in **Docker vhosts**:
- **Hostname**: a short name such as `app1`, or a complete FQDN such as `portal.example.com`;
- **Aliases**: zero or more additional short names/FQDNs, edited with the same add/remove chips as normal vhost hostnames;
- **Backend host IP**: Auto/default or one concrete address detected on the Docker host;
- **Backend protocol**: HTTP or HTTPS;
- **Preserve Host**: forward the requested hostname to the container when enabled; new HTTP/1.1 assignments default to enabled, while an existing explicit disabled value is preserved;
- **Redirect HTTP to HTTPS**: per-app Force SSL switch. New assignments default on when the agent has a usable plain-HTTP + TLS listener pair; assignments saved before 0.18.8 retain their previous redirect-off behavior until explicitly saved with the new switch;
- **WebSockets**: enable or disable classic HTTP/1.1 Upgrade forwarding for this application;
- **Cache**: enable or disable normal RustProxy caching for this generated vhost;
- **Transform**: opt this generated vhost into the global static-response transform engine; it is off by default and still requires global transformation to be enabled;
- **Backend health check**: HTTP/TCP/off active-probe mode, path/Host override, interval, timeout, slow threshold, rise/fall counters and accepted HTTP status range.
- **Passive Backend Health**: independent opt-in policy, OFF by default, with separate connect/timeout/I/O/HTTP-5xx/slow-response switches and optional passive rise/fall overrides.
- **Access control**: an optional reusable ACL profile plus an optional inline IP/CIDR ACL for this application. A per-app profile overrides the agent default profile; inline rules remain cumulative with the effective profile.
- **Proxy timeouts**: optional connect, response-header, read-idle and send-idle timeouts; blank values inherit RustProxy defaults.
Publish/Unpublish is applied immediately. Editing a published service uses the per-service **Apply changes** action; there is no page-wide assignment save step. The optional **Live view** switch refreshes only cached RustProxy observations/health state and does not contact the Docker agent.
A short name is expanded below that agent's base domain. With base domain `docker1.lab.local`, `app1` becomes `app1.docker1.lab.local`. A full FQDN is not rewritten. Aliases use the same rule, so `app-old` becomes `app-old.docker1.lab.local`, while `legacy.example.com` remains unchanged.
This makes renaming and aliasing a Docker-backed vhost a normal RustProxy configuration change; the container/service identity remains the stable link to discovery.
## Multiple Docker-host IPs and bindings
The agent reports all usable addresses it can see on the Docker host. This is intended for hosts that have separate public, management and LAN addresses.
RustProxy also keeps every published Docker binding. For example, one container port may be published as:
```text
10.21.0.15:18080
192.168.50.15:28080
```
Selecting `10.21.0.15` for the Docker vhost uses port `18080`; selecting `192.168.50.15` uses `28080`. For Docker wildcard bindings (`0.0.0.0` / `::`), RustProxy uses the selected host address with the wildcard binding's published port.
Backend selection order is:
1. the Docker vhost's explicit `target_ip`;
2. the agent's optional `default_target_ip`;
3. a concrete Docker bind address;
4. the agent's preferred/primary detected address when compatible with the binding;
5. another compatible detected host address.
An explicit target that is not compatible with the service's Docker bindings does not silently fall back to another IP; that dynamic vhost is not materialized until the selected address/binding is available.
The generated backend is pinned to the selected IP through an address allowlist and uses the matching Docker-published host port. Cache, Transform, WebSockets and HTTP-to-HTTPS redirect are materialized from the per-app switches. If the Docker agent has a frontend certificate selected, that certificate ID is copied to every generated vhost and participates in the normal TLS/SNI certificate validation and ownership rules. The effective ACL profile is the per-app profile when selected, otherwise the agent default profile. Active backend health checks default to TCP. Passive Backend Health is materialized from the Docker assignment separately and defaults to OFF, so discovery refresh/restart does not silently re-enable production-traffic quarantine. The Docker vhosts page exposes the same passive policy as ordinary vhosts. Normal 5xx/timeouts/connect/I/O/slow-response observations remain visible in counters while passive health is OFF. Active checks can be switched between HTTP/TCP/off per service without changing the passive policy.
## DNS
DNS for Docker vhost names must resolve to the RustProxy frontend/listener that should receive the HTTP(S) request. For a per-agent base domain this is normally a wildcard record, for example:
```text
*.docker1.lab.local -> <RustProxy frontend IP>
*.docker2.lab.local -> <RustProxy frontend IP>
```
Custom FQDNs and aliases likewise need DNS records pointing at the relevant RustProxy frontend. The Docker-host addresses detected by the agent are backend routing candidates; they are not automatically published to DNS.
RustProxy does not change DNS records itself in 0.22.3.
## Prometheus metrics
Docker discovery has its own Prometheus collector switch, `prometheus.include_docker_vhosts`, enabled by default. It is intentionally separate from `include_vhosts` and `include_backends`. Dynamic Docker routes never appear under ordinary `rust_proxy_vhost_*` or `rust_proxy_backend_*` families.
The Docker collector exports bounded configured labels only: agent ID, generated Docker-vhost ID, assigned service key, configured domains/aliases and backend ID. It includes `rust_proxy_docker_vhost_*` traffic/status/duration metrics and `rust_proxy_docker_backend_*` readiness, health, active work, requests/errors/bytes, current/EWMA latency, failure/success counters, health transitions and last-probe observations. No token, agent endpoint, backend URL or arbitrary request Host value is exported.
+157
View File
@@ -0,0 +1,157 @@
# HTTP/3 transport (0.22.3)
**Uncompiled development source in this environment.** This release adds an actual
QUIC/HTTP/3 adapter, not only an Alt-Svc header. It does not claim independent full-RFC
conformance, interoperability or production performance until the acceptance tests run.
## Stack and configuration
The adapter delegates QUIC transport/TLS to Quinn 0.11.12 and HTTP/3/QPACK plus the
Quinn adapter to `hpx-h3 2.5.19` with its `quinn` feature. The crate is imported as `h3`
in source. These are Cargo dependencies, not bundled binaries.
Rustls provides TLS 1.3 for QUIC; SNI certificates come from the existing inventory.
HTTP/1.1 and optional HTTP/2 remain on TCP at the same port. HTTP/3 is on UDP.
There is no HTTP/3 upstream client in this revision: origins still use HTTP/1.1 or HTTP/2.
Each vhost has `http3_enabled` (default true). When false, RustProxy removes
`Alt-Svc` from responses for that vhost and rejects HTTP/3 requests for it with 421,
while HTTP/1.1 and any permitted HTTP/2 traffic on the selected TLS listener keep
working. The UDP listener is still a listener-level resource; the vhost switch narrows
use of that shared capability rather than hot-unbinding the UDP socket.
HTTP/2 follows the same UI model through `http2_enabled` (default true). A vhost may
only opt into H2 when one of its selected named listeners enables H2. For TLS, RustProxy
reads ClientHello/SNI before finishing the handshake and selects H2+HTTP/1.1 or
HTTP/1.1-only ALPN for that vhost. The listener remains the capability ceiling.
Since 0.5.0, an omitted `http3` enables QUIC with defaults on TLS bindings.
Explicit `http3: null` disables it and is preserved when serialized. An object
enables it with chosen limits. Plain HTTP defaults to no QUIC. In 0.24.3 the
**Enable HTTP/3 over QUIC (UDP)** switch is edited with the TLS endpoint under
**Settings -> Listeners**. The listener row links to **Settings -> HTTP connections
-> HTTP/3**, which contains only advertisement, connection, stream and idle-timeout
tuning for listeners where H3 is enabled. Both enablement and tuning remain
restart-required. Configure this only on named `listeners.endpoints[].binding`:
```json
{
"address": "127.0.0.1:8443",
"tls": true,
"http2": true,
"http3": {
"alt_svc_max_age": 3600,
"max_concurrent_connections": 512,
"max_concurrent_bidistream": 100,
"max_concurrent_unistream": 100,
"max_idle_timeout": 10
}
}
```
| Field | Meaning and accepted limits |
|---|---|
| alt_svc_max_age | Advertisement lifetime, seconds, 0..604800 |
| max_concurrent_connections | Established/pending accepted connection slots per endpoint, 1..4096 |
| max_concurrent_bidistream | Peer bidirectional streams per connection, 1..1024 |
| max_concurrent_unistream | Peer unidirectional streams per connection, 3..1024 |
| max_idle_timeout | QUIC idle timeout, seconds, 0..86400; zero means no local QUIC idle timeout |
Three unidirectional streams are required for the peer control and QPACK streams.
These stream limits are NOT global to all listeners. The existing global in-flight
request and application body/header deadlines still apply. Setting QUIC idle to zero
does not disable application deadlines, and the peer can negotiate its own idle timeout.
All changes to socket/ALPN/QUIC transport parameters require restart.
## Transport behavior and safety
- Bind UDP before announcing readiness; failure to bind is a startup failure.
- Advertise `Alt-Svc: h3=":port"; ma=N` only on an ACTUAL active TLS+H3 endpoint.
Origin Alt-Svc is stripped so it cannot advertise the origin or a disabled service.
- UDP address validation/Retry, bounded pending handshakes, a 10-second handshake
deadline, per-endpoint permits, stream flow control and bounded application concurrency.
- 32 KiB decoded HTTP field section limit; request content-length checks and rejection
of forbidden connection/framing headers; streamed bodies and trailers.
- Shared routing pipeline: real local socket -> validated authority -> trusted client
address -> ACL -> Tinyauth -> cache -> origin -> response/security/Server policies.
- 0-RTT is disabled to avoid replaying writes; no retry to another origin or transport.
- Endpoint active migration is disabled. Missing real local destination data on a
wildcard QUIC connection fails closed rather than substituting a forwarded IP.
- Configuration reload changes SNI certificates for new QUIC handshakes without
changing active socket limits. Graceful shutdown requests GOAWAY then bounded drain.
- Counters: http3_requests, http3_connections, http3_active_connections and
http3_rejected_connections in status/Prometheus; H3 request count also in dashboard.
A connection receives a maximum aggregate 8 MiB QUIC window and 256 KiB per-stream
window; these limits are not process memory guarantees. Many simultaneous connections
can use substantial memory. Size connection/stream limits to the machine and measure.
No active path migration, 0-RTT, server push, WebTransport or HTTP datagrams are enabled.
RFC 9220 WebSocket Extended CONNECT is experimental and disabled by default per vhost.
RustProxy advertises SETTINGS_ENABLE_CONNECT_PROTOCOL on a QUIC listener only when at least
one vhost bound to that listener explicitly enables `websocket_rfc9220`; request routing
still rejects RFC 9220 for vhosts that did not opt in. Generic CONNECT and other Extended
CONNECT protocols remain unsupported and are answered with 501. The downstream H3 request must
not carry HTTP/1.1-only `Connection`, `Upgrade`, `Sec-WebSocket-Key` or
`Sec-WebSocket-Accept` fields. RustProxy creates a private RFC 6455 key for the HTTP/1.1
backend handshake; a valid backend `101` becomes downstream `200`, after which H3 DATA
bytes are the WebSocket byte stream. Client FIN half-closes the backend write side;
backend EOF finishes the H3 send side; abnormal/forced termination uses
`H3_REQUEST_CANCELLED`. No side TCP listener or protocol bypass is used.
### WebSocket bootstrap ordering and diagnostics in 0.16.0
For RFC 9220, a validated backend `101` is no longer enough to send the client-facing H3 success immediately. RustProxy first awaits Hyper's backend `OnUpgrade` and obtains the actual upgraded stream. Only then is downstream `200` returned and the H3 DATA bridge started. If backend takeover is cancelled or fails, the client receives a normal 503/502 response instead of `200` followed by an immediate `H3_REQUEST_CANCELLED` reset.
Access logging can now distinguish this path with `http_version`, `transport`, `websocket_bootstrap`, `websocket_phase`, `websocket_error`, `h3_reset_code` and `upstream_status`. This makes Safari/iOS interoperability failures distinguishable from backend Socket.IO responses and from H3 tunnel resets. Query-string logging remains subject to the normal privacy policy; do not use access logging to capture session secrets.
The advertised port is the real listener port. Behind port-rewriting NAT, use a
matching externally reachable UDP/TCP port or do not enable this advertisement; there
is no separate external-port override. Open UDP as well as TCP in your firewall/load
balancer. Scope labels do not perform those operations. Clients lacking QUIC or blocked
UDP can still connect using TCP; no guarantee is made about browser protocol preference.
## Verification gate
```bash
bash dev.sh check
cargo test --locked --test http3 -- --nocapture
cargo test --locked --test integrations --test listeners
bash dev.sh test
```
`tests/http3.rs` creates a real loopback UDP endpoint, a trusted generated test
certificate, a real origin and a Quinn+h3 client. It checks MISS/HIT, body preservation,
Server/diagnostic policy, connection capacity and ALPN HTTP/2/HTTP/1.1 compatibility.
The same integration test explicitly enables `websocket_rfc9220`, then opens an RFC 9220 Extended CONNECT stream and translates it
to a real HTTP/1.1 backend Upgrade, sends a masked WebSocket frame as H3 DATA, half-closes
the client send side and verifies byte-transparent DATA return plus tunnel metrics. Unit
regressions separately reject HTTP/1.1-only proof/framing headers on the H3 side.
It is provided but was NOT run in the packaging environment. The same-stack client
is a regression test, not independent interoperability evidence.
After applying examples/http3-vhost.json on a disposable test instance and starting
`bash dev.sh mock --port 9001`, use an independently built curl with HTTP3 support:
```bash
curl --version
curl --http3-only --cacert data/certs/default.pem -D - https://localhost:8443/cache
curl --http3-only --cacert data/certs/default.pem -D - https://localhost:8443/cache
curl --http2 --cacert data/certs/default.pem -D - https://localhost:8443/cache
curl --http1.1 --cacert data/certs/default.pem -D - https://localhost:8443/cache
```
Do not substitute `--http3` fallback or `-k` for strict protocol/certificate validation.
Also test independent clients, malformed-frame handling, stream exhaustion, slow
uploads/downloads, IPv6 wildcard destination metadata, cancellation, graceful drain,
certificate rotation, and memory/CPU at configured maxima before exposure.
No full RFC conformance or load-test result is asserted by this source package.
## Primary references
- HTTP/3 RFC 9114: https://www.rfc-editor.org/rfc/rfc9114.html
- QUIC RFC 9000: https://www.rfc-editor.org/rfc/rfc9000.html
- QPACK RFC 9204: https://www.rfc-editor.org/rfc/rfc9204.html
- Quinn: https://docs.rs/quinn/latest/quinn/
- RFC 8441 Extended CONNECT: https://www.rfc-editor.org/rfc/rfc8441.html
- RFC 9220 WebSocket over HTTP/3: https://www.rfc-editor.org/rfc/rfc9220.html
- hpx-h3 2.5.19: https://docs.rs/hpx-h3/2.5.19/h3/
+291
View File
@@ -0,0 +1,291 @@
# Installing and updating RustProxy 0.24.5
The scripts prepare a Linux/systemd installation. This source archive is **not a
verified production build**: Rust/Cargo, real CA operations, live systemd startup
and rollback against a real Rust database have not been exercised in the packaging
environment. Perform the acceptance steps below on an isolated server first.
## Debian 13+ one-line source install/update
```bash
# auto: first install or managed update
wget -qO- https://git.linuxiarz.pl/gru/RustProxy/raw/branch/master/install-debian13.sh | bash
# force the update path
wget -qO- https://git.linuxiarz.pl/gru/RustProxy/raw/branch/master/install-debian13.sh | bash -s -- update
```
The bootstrap installs build dependencies, installs Rust with rustup for an unprivileged build user,
clones `https://git.linuxiarz.pl/gru/RustProxy` and builds with `cargo build --release --locked`.
Its default `auto` mode calls `install.sh` only when no managed installation exists. When
`/opt/rustproxy/install.json` exists it calls `update.sh`, which uses the saved installation record,
backs up private state, switches the release, verifies health/version and rolls code/data/unit back
on failure. First install intentionally starts the admin listener on `0.0.0.0:9090` over plain HTTP
using `--allow-remote-admin`; updates preserve the saved admin/listener
settings. Firewall/private-network restriction is therefore required. The normal `install.sh`
default remains loopback-only.
Equivalent prerequisite commands are:
```bash
apt install -y build-essential gcc git ca-certificates python3 pkg-config wget sudo python3-certbot-dns-rfc2136 python3-certbot-dns-cloudflare
wget -qO- --https-only --secure-protocol=TLSv1_2 https://sh.rustup.rs | sh
```
## Build as a normal user
Requirements: stable Rust (minimum declared 1.88), Cargo, a C compiler/linker,
Python 3.10+, Bash. Linux/systemd deployment also requires `systemctl`, `runuser`
and a usable non-login service account (created by the installer when missing).
The static TOML checker additionally needs Python 3.11+. No script downloads an
executable or installs a compiler as root.
```bash
cd RustProxy
bash dev.sh doctor
bash dev.sh check
bash dev.sh test
bash dev.sh release
```
The first build resolves and creates `Cargo.lock`; retain the real lockfile and
binary hash for repeatable subsequent builds. There is no fabricated lockfile or
precompiled binary in the archive. Set `--binary` explicitly when using a custom
`CARGO_TARGET_DIR`. Review and trust the source and built binary before installation.
## First installation
The normal first install also downloads `user-country-ipv4.csv` and
`user-country-ipv6.csv` from the `sapics/ip-location-db` latest release over HTTPS and
verifies each file against the publisher SHA-256 release before writing it privately to
`DATA_DIR/geoip`. This enables country access/rate rules. Use `--skip-geoip` only when
you intentionally do not want the database; allow-only country policies then fail
closed for unresolved clients. Updates preserve the installed database.
```bash
bash install.sh --plan --binary target/release/rust-proxy
sudo bash install.sh --binary target/release/rust-proxy
```
Defaults: HTTP `0.0.0.0:80`, HTTPS `0.0.0.0:443`, administration
`127.0.0.1:9090`, code `/opt/rustproxy`, all private state `/var/lib/rustproxy`,
service account `rustproxy`. The initial page is a small informational response; no demonstration
backend or sample application is started. The first database has no configured
vhosts. Existing, nonempty data is refused rather than reset.
The first install also writes `/etc/profile.d/rustproxy-path.sh`, which prepends the
managed `PREFIX/current` directory (normally `/opt/rustproxy/current`) to `PATH` for
new login shells. Open a new login shell, or source that profile explicitly, before
expecting `rust-proxy` to resolve without its absolute path.
HTTPS is enabled by default and starts with a **self-signed certificate**, not a
publicly trusted certificate. For an explicit HTTP-only installation use:
```bash
sudo bash install.sh --binary target/release/rust-proxy --no-https
```
Assign the intended certificate in the panel before exposing applications. Existing
HTTP/3 defaults enable QUIC on TLS listeners: permit the corresponding UDP port
when desired, or explicitly disable H3 in Listeners. Review firewall rules yourself;
the installer does not change them. Public frontend TLS and administration TLS are
independent settings.
Generate the one-time setup capability directly from the installed release CLI:
```bash
sudo -u rustproxy /opt/rustproxy/current/rust-proxy \
--data-dir /var/lib/rustproxy admin setup-token
```
This works before the first server start. After an administrator exists, setup
tokens remain invalid. You can alternatively initialize the account interactively:
```bash
sudo -u rustproxy /opt/rustproxy/current/rust-proxy \
--data-dir /var/lib/rustproxy admin init
```
For direct remote administration, first sign in on the default loopback listener and
open **Settings -> Administration**. Enable HTTPS, choose the application-managed
certificate or a certificate from the inventory, set the bind address (for example
`0.0.0.0:9090`) and optionally add DNS names accepted by Host/Origin validation.
Save and restart RustProxy. The persisted remote bind is accepted only while
administration TLS is enabled. Keep the listener behind a firewall/private network;
the certificate must be trusted by the browser if you want a clean HTTPS connection.
An SSH tunnel remains available without exposing the listener directly:
```bash
ssh -L 9090:127.0.0.1:9090 operator@your-server
```
Open `http://127.0.0.1:9090` on that workstation while the administration listener
is still using its default HTTP mode. No default administrator/password is added.
Plain-HTTP remote administration is possible with the explicit allow flag:
```bash
sudo bash install.sh --binary target/release/rust-proxy \
--admin 0.0.0.0:9090 --allow-remote-admin
```
That plain-HTTP path has no transport confidentiality and should be limited to a trusted
private network. Prefer the built-in administration HTTPS mode for a direct remote
listener.
Other installation switches: `--prefix`, `--data-dir`, `--user`, `--http`, `--https`,
`--no-https`, `--admin`, `--allow-remote-admin`, `--sha256` (expected binary digest), `--no-start`. Paths must be dedicated,
absolute, separate, non-nested and without spaces or symlink components.
`--plan` reads the supplied binary but changes nothing; it does not prove port
availability, compiler success or systemd readiness.
### Startup-owned listeners
When HTTP, HTTPS or administration listen addresses are supplied explicitly by systemd, CLI
arguments or the corresponding `RUST_PROXY_*_LISTEN` environment variables, RustProxy treats
those exact binds as process-owned. Settings shows them as locked and configuration updates
cannot remove, disable or rewrite them. Listeners not supplied at process start remain normal
editable desired-state entries.
## ACME prerequisites
Automatic renewal runs inside RustProxy, not a separately installed cron timer.
Install `certbot`, the DNS plugins you use (`certbot-dns-cloudflare`,
`certbot-dns-ovh`, `certbot-dns-rfc2136`), Python and OpenSSL on the server. HTTP-01
uses Certbot's webroot authenticator, not a competing standalone web server.
Certbot and its plugins must be in the **same Python environment**. An optional
root-owned virtual environment at `/opt/rustproxy-certbot` is on the service PATH;
otherwise use `/usr/local/bin` or `/usr/bin`. Avoid an executable under an
administrator's home directory: the unit uses `ProtectHome=true`.
Check the effective service account's environment rather than only your shell:
```bash
sudo -u rustproxy env PATH=/opt/rustproxy-certbot/bin:/usr/local/bin:/usr/bin:/bin \
python3 scripts/acme_provider.py --doctor
```
The provider scripts are embedded in the binary; the source file above is just a
preflight command. The service owns its complete private `certs/acme` tree. Do not
run a second Certbot renewal timer over those same profiles. See [renewal](RENEWAL.md).
### Using a separate system Certbot
If another service already owns a certificate through the system Certbot, keep that lineage
outside RustProxy's managed ACME profiles and create/use a **manual** RustProxy certificate id.
A Certbot deploy hook can publish every successful renewal to all vhosts that already select
that id:
```bash
/usr/bin/rust-proxy tls deploy --id shared-cert \
--cert /etc/letsencrypt/live/example/fullchain.pem \
--key /etc/letsencrypt/live/example/privkey.pem
```
The command reads the Certbot files locally and sends them to the authenticated loopback `POST /api/certificates/deploy` endpoint. The running RustProxy service writes the private generation with its own filesystem identity and hot-publishes it for new public TLS/H2/H3 handshakes. No per-vhost edit is needed. If another
local process instead replaces the exact files already referenced by RustProxy, run
`/usr/bin/rust-proxy tls reload` (or authenticated `POST /api/certificates/reload`) afterward. If the replacement belongs to one configured manual certificate ID, automation may instead call `POST /api/certificates/{certificate_id}/reload`; the same endpoint accepts a scoped Certificate API Key ID + Bearer token and does not change the configuration revision.
Administration TLS is startup-fixed and may still require a service restart.
Optional environment overrides go in a root-owned `/etc/rustproxy/environment`
(mode 0600); create the directory when needed. Restart after changing service
environment values. Provider secrets remain in private profile files, not this
file or the exported configuration. A systemd environment file is not a shell script.
## Update an installation created by install.sh
Build and test the new release as a normal user first, then:
```bash
bash update.sh --plan --binary target/release/rust-proxy
sudo bash update.sh --binary target/release/rust-proxy
```
Before stopping the service, the updater fetches the current publisher SHA-256
files for the `user-country` GeoIP IPv4/IPv6 assets. Matching local files are left
untouched; stale or missing assets are staged, verified and published only after
the private-data backup exists. Use `--skip-geoip` only when intentionally accepting
a stale/missing country database (for example during an upstream outage).
Use the original `--prefix` for a non-default installation. Other startup/account/
data settings are read from root-owned `install.json`; update does not silently
replace them with command-line defaults. Change intentional listener settings in
RustProxy, or maintain the unit separately when needed.
Update mode checks that `/etc/profile.d/rustproxy-path.sh` still matches the saved
installation prefix, but deliberately does not rewrite it. A missing or changed file
produces an explicit warning before service downtime; repair the profile separately
if direct `rust-proxy` shell invocation is required.
The updater verifies the supplied binary hash and runs only its `--version` as the
service account. Immediately before stopping a RustProxy 0.14.0+ service it creates a
private one-shot managed-update marker. During graceful shutdown the process forces hot
objects into the enabled cold tier even when ordinary `cold_on_shutdown=false`; the marker
is removed before backup. The managed unit allows up to 120 seconds for controlled stop.
If cold storage is disabled, RAM-only cache cannot survive the process exit. The updater
then stages an immutable release, stops the service and backs up the **entire** data directory. This includes SQLite and sidecars, certificates, account
keys, provider credentials, Certbot renewal configs and internal relative symlinks.
Code changes use an atomic `current` symlink switch. It then starts the new process
and requires systemd activity, minimal local `/healthz` readiness, and the actual
MainPID executable matching the staged, locally version-checked binary. The local
health probe follows the persisted administration bind and HTTP/HTTPS mode (including
the configured Host allowlist), rather than assuming the install-time plain-HTTP
address. Version is no longer exposed by the anonymous health endpoint.
The managed systemd unit is regenerated from the saved installation record and
`daemon-reload` is run before startup. The previous unit is included in the backup
and restored on rollback. Keep custom service policy outside this managed unit or
reapply it after an update.
A failed start/health check restores the previous release, private data **and**
managed systemd unit. The reported error now includes the original update failure;
recent service status/journal output is emitted before rollback when available.
Failed-generation data is retained alongside the original data directory; the
backup is retained under `/opt/rustproxy/backups`. A failed rollback health check is
reported explicitly. Protect these directories: they contain private keys and
credentials. They are never automatically pruned. Plan enough free space for the
full backup, restored data, retained failed state and old/new executable releases.
During development, if an older saved vhost/settings document is intentionally
incompatible with the current format, use:
```bash
sudo bash update.sh --reset-config --binary target/release/rust-proxy
```
This happens only after the backup is created. It deletes the saved desired
configuration row so the new process creates current listener defaults; administrator
account data, certificates, ACME/provider files and other private state are retained.
Do not use this flag when the old vhost/settings configuration must be preserved.
After a failed update, **do not run `install.sh`**: rollback leaves the previous
installation installed. Fix the reported cause and rerun `update.sh` (optionally
with `--reset-config` for this development-only incompatible-config case).
An update interrupts traffic; zero-downtime deployment is not implemented. `--no-start`
leaves the service stopped and cannot certify a healthy installation. Backup failure
leaves the old code/data in place and attempts to restart a previously running service.
Keep an independent off-host backup; local rollback is not a disaster-recovery plan.
## Existing source-tree installations from earlier archives
Do not point a first installer at your old live data. To keep the current source-
tree deployment, stop RustProxy and use the retained source updater:
```bash
bash /path/to/new/RustProxy/update.sh --source-tree /path/to/old/RustProxy --stopped
```
This mode keeps the old `upgrade.sh` contract and does not install a systemd service
or build a binary. Back up **all** private data and ACME profiles separately before
starting it. See [migration](MIGRATION.md). To adopt the new service layout, make an
explicit offline migration with ownership, absolute Certbot paths and port settings
reviewed; do not merely move a Certbot account tree to another location.
## Server acceptance
Check actual non-root HTTP/TLS/QUIC binds, certificate assignment, a real renewal
validation (`Dry run`), backend failures, live traffic, Basic Auth over TLS, ACLs,
WebSocket enable/disable and location isolation. Verify the backup can be restored
on another isolated instance. Run `systemctl status rustproxy` and
`journalctl -u rustproxy` after installation/update. Only then decide on rollout.
+161
View File
@@ -0,0 +1,161 @@
# RustProxy 0.4.0 - forwarding, Tinyauth, listeners and certificate library
This is source implementation, not a verified binary. Read TEST_REPORT.md before deployment.
The admin panel remains local; public frontend TLS does not make the admin listener remote-safe.
## Forwarded identity and the trust boundary
Every origin request now gets canonical `X-Forwarded-For`, `Forwarded`, `X-Real-IP`,
`X-Forwarded-Proto`, `X-Forwarded-Host`, `X-Forwarded-Port` and `X-Forwarded-URI`.
Without configured trusted proxies, incoming proxy headers are discarded. The socket
peer IP is used, so a client cannot supply an arbitrary address in X-Forwarded-For.
Both header formats are emitted; RFC 7239 IPv6 nodes are quoted and bracketed.
There is no ephemeral client port in the representation fingerprint.
When RustProxy is behind a real reverse proxy, Settings -> Forwarding accepts explicit
IPv4/IPv6 CIDRs (max 32). Do not enter application client networks as trusted proxies.
The last hop must sanitize incoming forwarding headers. `0.0.0.0/0` and `::/0` are rejected.
With a trusted socket peer, X-Forwarded-For is preferred; Forwarded's `for=` chain is the
fallback. The chain is walked right-to-left and anything before the nearest untrusted
hop is discarded. Malformed or oversized chains fall back to the actual peer.
`X-Forwarded-Proto` is honored only with the separate `trust_forwarded_proto` opt-in.
Authority/Host always comes from validated Host or HTTP/2/3 `:authority`, not X-Forwarded-Host.
ACLs and IP affinity use this centrally resolved client address. Listener selection
continues to use the REAL local destination socket and actual TLS state.
```json
"forwarding": {
"trusted_proxies": ["10.87.7.10/32"],
"trust_forwarded_proto": true
}
```
That address is illustrative, not a default. Leave the list empty for direct clients.
RFC 7239: https://www.rfc-editor.org/rfc/rfc7239.html
## Tinyauth per vhost
Open Vhosts -> Edit -> Authentication. Enable protection, enter the verification
server and the public login URL, allow a private verifier explicitly if needed,
and save. An origin URL automatically appends `/api/auth/nginx`; an explicit URL
path remains unchanged. No public `/tinyauth` proxy route needs to be created.
The following matches the supplied nginx-style setup:
```json
"tinyauth": {
"enabled": true,
"url": "http://10.87.7.99:3000/api/auth/nginx",
"login_url": "https://auth.linuxiarz.pl/login",
"allow_private": true,
"address_allowlist": ["10.87.7.99/32"],
"connect_timeout_seconds": 5,
"timeout_seconds": 10,
"request_headers": ["cookie", "authorization", "x-api-key", "user-agent", "accept"],
"response_headers": ["remote-user", "remote-email"],
"redirect_forbidden": true
}
```
Verification is an internal body-free GET, carrying canonical forwarding headers,
X-Original-URL and X-Original-Method. Original method, path and query remain available
to Tinyauth, while the upload body is sent only to the application after authorization.
The RustProxy admin session cookie is never passed to the verifier or application.
Only 2xx approves access. Selected Remote-* / safe X-* response headers become trusted
identity headers. Incoming Remote-* and configured identity headers are removed,
including after administrator request-header rules. Missing identity fields are not
filled from client values. Transport/framing/forwarding fields cannot be selected
as identity headers. Each direction supports at most 32 names; approved identity values are bounded
to 8 KiB in total.
401 redirects to a validated X-Tinyauth-Location on the configured login origin,
or constructs `login_url?redirect_uri=<percent-encoded original URL>`.
403 stays 403 unless the provider returns an allowed same-origin redirect and the
403-redirect option is enabled. Cross-origin locations are rejected. Verifier errors
fail closed (502/504), not open. Auth denial/error is not replaced by custom error pages.
Public login URLs require HTTPS; HTTP is allowed only for loopback test servers.
Additional CA PEM can extend trust for Tinyauth HTTPS verification; there is no
verify-off switch for the authentication verifier. The per-backend `tls_verify` option
does not apply to Tinyauth.
Configure Tinyauth itself to trust the ACTUAL RustProxy source IP, not 0.0.0.0/0.
Authorization runs before origin connection, static pages, WebSocket upgrade to the
origin and every cache lookup. Authenticated vhosts always BYPASS the internal shared
cache and emit `Cache-Control: private, no-store`, even when their cache toggle is on.
This intentionally also disables privacy-sensitive compression through the existing
predicate. Login/session validity is NOT cached. Revocation is checked on each new
HTTP request or WebSocket handshake; an already upgraded socket is not periodically
reauthenticated. There is no automatic replay of POST or failed upgrades.
Do not enable this protection on the Tinyauth login vhost itself (login loop).
Provider reference: https://tinyauth.app/docs/guides/nginx-proxy-manager/
Header reference: https://tinyauth.app/docs/reference/headers/
## Named listeners
Settings -> Listeners owns every proxy socket. Each endpoint has a stable ID, display
name, scope label, enabled state and a binding with numeric IP:port, TLS, HTTP/2 and
optional HTTP/3 parameters. Scope labels do not install firewall rules.
Vhosts reference endpoints only through `frontend.listener_ids`. Inline frontend
listener definitions and the old global listener-pair compatibility fields are not
accepted in 0.8.0. A named endpoint can be shared by multiple vhosts; same-socket
transport settings must agree. Disabled endpoints create no socket and grant no vhost
binding. Unknown/duplicate IDs and admin-port conflicts are rejected.
Listener transport changes require restart. The panel shows desired and actually bound
endpoints separately. A wildcard bind never widens a vhost's own destination-IP match.
## Certificate library
Certificates is an independent sidebar section. Import a PEM certificate/full chain
and PEM private key, give the entry an ID, specify domain names to validate the pair,
and save it to the inventory. In Vhost -> SSL certificates, select the saved entry.
Import does not add public SNI names by itself; active vhost assignment supplies names.
The advanced filename editor remains for CLI-managed existing pairs and direct SNI maps.
The library shows assignment and default status. ACME/Let's Encrypt and expiry renewal
are not implemented; the inventory/API boundary is ready for that future work.
Uploads: at most 128 KiB per file, key matching/name validation, immutable randomly
named private files, limited parallel imports and mode 600 on Unix. Config stores
filenames and IDs, never PEM private key bytes. File inputs are cleared after import.
Failure between staging and configuration publication may leave unused private files;
they are not automatically activated or deleted. Removing inventory metadata does not
delete key files. Back up the cert directory together with SQLite.
Existing referenced/default entries cannot be removed in the UI without unassigning them.
Certificate changes affect new handshakes; existing sessions continue normally.
The resolver is shared by listeners: one DNS name cannot use different certificates
on different destination IPs. Conflicts are rejected rather than silently chosen.
HTTP-only startup also prepares the private certificate directory, but creates no TLS
socket unless configured. No existing certificates are overwritten.
## Response policies and cache on extra frontends
Global and per-vhost `server_header` accept `preserve`, `hide`, `custom` plus `value`.
Omitted/null vhost policy inherits the global choice. Final policy applies to origin
responses, HITs, local pages and errors for inline/named TCP/TLS/H3 listeners.
Generic header rules run before this dedicated Server policy.
As of 0.18.8, `diagnostic_headers` exposes only the request-ID switch:
```json
"diagnostic_headers": {
"request_id": false
}
```
There is no revision response-header switch or special `X-Rust-Proxy-Revision` handling. If an upstream or ordinary response-header rule supplies that name, it behaves like any other non-protected response header.
Request IDs remain available internally/upstream and in logs. X-Mock-Origin is preserved and can
be removed with ordinary global or per-vhost response-header rules. An explicit vhost diagnostic
object overrides the request-ID choice; null inherits.
Cache keys now include the actual frontend IP:port and stable canonical forwarding
metadata. Empty HTTP/2/HTTP/3 GET bodies without an exact length are checked by peeking
at most eight frames, skipping empty DATA frames; nonempty data/trailers are
replayed to the origin without buffering the full body.
A stream with data/trailers bypasses cache. Reusing the same socket with a different
client source PORT no longer changes the forwarded representation.
Client IP still affects the conservative all-header fingerprint, so different clients
can have separate cache entries. Auth, cookies, no-store, private, upgrades and partial
bodies still bypass caching. Cookies created by a login or the sticky-session feature
are not a cache bug. Close/consume the entire response before expecting a stored HIT.
+49
View File
@@ -0,0 +1,49 @@
# Interactive live logs
RustProxy provides an operator CLI that follows structured request/error events from every HTTP vhost:
```bash
rust-proxy --livelogs
```
The command connects only to the loopback administration listener and creates a bounded local CLI session from the private RustProxy database. When `./data/config.sqlite3` is absent, the bare command discovers common installed layouts: `/var/lib/rustproxy` is preferred on Linux/non-BSD systems and `/var/db/rustproxy` on BSD/OPNsense-style installations, with the alternate convention checked as a fallback. An explicit `--data-dir` always wins and custom installations should pass the same path used by the service. `--admin-listen` can select a non-default loopback port.
## Administration HTTP/HTTPS transport
The CLI first probes the loopback administration `/healthz` endpoint over HTTP. An HTTP-only administration listener stays on HTTP; the `308 Permanent Redirect` emitted by a TLS-enabled administration listener switches the CLI to HTTPS before a CLI session cookie is sent. HTTPS is pinned to leaf certificates present in the private `<data-dir>/certs` directory, so a configured self-signed administration certificate does not require a system trust-store installation.
This fixes the previous reconnect loop where `--livelogs` treated the TLS listener's expected HTTP-to-HTTPS `308` response as a stream failure. The same local transport selection is used by CLI config and certificate API operations.
The live path is separate from configured file/syslog sinks. Request processing publishes into a bounded Tokio broadcast channel without waiting for viewers. A slow viewer receives an explicit `gap` event with the number of entries lost from that viewer's live queue. The CLI also consults the retained history after connecting/reconnecting and de-duplicates entries by request id.
## Interactive commands
No regex language is required. Type commands while the stream is running:
```text
host api.example.com
vhost api
backend origin-a
route users
client 192.0.2.10
method GET
transport h2
cache MISS
status 5xx
path /api/
errors on
slow 250
detail on
filters
show 12345
pause
resume
reset
clear
quit
```
Use `*` to clear a scalar filter, for example `backend *`, and `slow off` / `errors off` for boolean filters. `show ID` prints the complete retained structured event for that request. `detail on` expands the continuously printed view with route/upstream/cache/transform/WebSocket/H3/byte metadata.
The server revalidates the streaming session every 10 seconds rather than writing session state for every request. Password changes, session revocation or expiry therefore terminate an existing stream within that bounded interval.
+134
View File
@@ -0,0 +1,134 @@
# Vhost management (updated for 0.4.0)
This guide describes implemented source behavior. Read TEST_REPORT.md before
acceptance: the packaging environment did not compile or run the Rust backend.
## Compact operations
Vhosts opens a compact list with domains, bound sockets, backend count and live
health/latency. Edit opens a modal with Details, Backends, Health, SSL, Cache,
Security, Pages, Authentication and Advanced tabs. Existing source/ACL JSON remains available.
The structure is inspired by proxy-host managers, not a claim of full feature
parity with nginx-proxy-manager. Density is configured in Settings and stored only
as a browser preference. Logo and favicon have not been redesigned.
## Optional certificate upload
In Certificates, upload certificate/full chain PEM and an unencrypted private-key
PEM. The pair is cryptographically validated and written to private immutable files;
the form then saves inventory metadata through revision CAS. No vhost or DNS name
is required just to import a certificate. In a separate vhost save, choose its ID
in SSL; validation checks that it covers the host's configured DNS names.
The file inputs clear after the request. Failed/stale inventory saves can leave
unbound staged files; no key bytes are returned by the API or stored in the config.
Clear/inherit uses the global SNI/default behavior.
Limits: 128 KiB per file, 300 KiB JSON request, up to 16 supplied DNS names, two concurrent
upload workers, 64 configured certificate inventory entries and 128 staged/upload
pairs on disk. Canceled uploads remain staged; no automatic deletion of potentially
active keys is attempted. New uploads eventually stop at that limit. Perform
manual cleanup only after backup and confirmation that neither the saved
configuration nor the serving snapshot references those files, preferably stopped.
The configured-certificate backup does not collect unused staging files.
Certificates must cover the configured DNS names. This does not prove browser trust,
chain completeness, revocation status or remaining lifetime. ACME-issued certificates
are shown as managed/read-only inventory entries; they are renewed through the ACME
workflow rather than edited as uploaded custom PEM files. New issuance selects the
production CA environment by default but still requires explicit production confirmation;
staging remains available for testing. A certificate can be deleted only after it has
been unassigned and saved everywhere, including vhosts, redirects, 404 hosts, the
default TLS certificate and the administration panel. Encrypted-key password prompts
are not supported. The single SNI resolver rejects different selected certificates for
one DNS name on different IP bindings. Socket changes still require restart; changing
a certificate for an existing listener uses hot snapshot publication after Save.
Requests already in flight are not interrupted.
## Cache and response headers
Cache selectors distinguish inheritance, on and off. Blank numeric fields inherit.
TTL/object limits, query/path exclusions and optional diagnostics are configurable
per host. The memory/entry ceilings are shared globally; one host cannot increase
its object ceiling above the global limit. Explicit per-vhost enable is permitted
when the global cache is explicitly disabled. Cookie-affine pools and path cache=false still
bypass. Purge on a host clears its entries and invalidates its in-progress fills,
without rewriting configuration or evicting other hosts. Changing configuration
still performs a full invalidation.
Security includes visual rules for headers sent to the backend and to the client,
and per-vhost HSTS/nosniff/frame/referrer/CSP/permissions policies. Advanced JSON
edits update the visual rows. Global, vhost and path header rules run in order;
existing final Server/security/protocol safeguards retain precedence. In
particular, a hidden Server cannot be restored by an ordinary header rule. An
explicit per-vhost Server override can replace the inherited global Server policy.
## Custom pages
Global Settings / Pages supplies error defaults, direct-IP and unknown-host
responses. Vhost / Pages supplies an optional static response, a missing-path
fallback and error overrides. Modes are inheritance, built-in congratulations,
empty and administrator HTML (typed or loaded from an HTML file).
Supported error keys: 400, 401, 403, 404, 408, 413, 429, 500, 502, 503 and 504.
Keep the original status by leaving status unset; an explicit status is allowed
from 200, 204, 400, 401, 403, 404, 408, 410, 413, 429, 500, 502, 503 or 504. Only empty
mode permits 204. HTML is at most 32 KiB UTF-8 per page and the complete serialized
configuration remains bounded to 256 KiB. No CSS/image asset hosting or directory
serving is provided; use self-contained HTML or an upstream for multi-file sites.
The built-in congratulations text is English; replace it with your own HTML for
other languages. RustProxy-generated 503 and 504 failures use matching built-in dark
HTML pages when no administrator override is configured; vhost/global custom error
pages still take precedence. An origin's own 503/504 response is not rewritten unless
a custom error override is configured. Admin interface translations still come from
language packs.
Request behavior:
1. Validate authority/path and real local binding; check applicable host/path ACLs
and enabled Tinyauth authentication before serving any content.
2. Use the explicit direct-IP page for a literal IPv4/IPv6 authority, if enabled.
3. Otherwise use the matching vhost's static response, if configured.
4. If no path is selected, use that vhost's missing-path fallback; if there is no
matching vhost, use the global unknown-host fallback. Otherwise keep normal 404.
5. Forward normally when a route exists, then replace a configured error with its
vhost override or global default. Apply the normal response/security policy.
Static/fallback endpoints accept GET/HEAD; other methods get 405 with Allow.
HEAD has the selected response headers and no body. Default status for a static
vhost/direct-IP page is 200; a missing-path/unknown-host fallback retains 404 unless
explicitly changed. Error responses retain their original status by default.
Already-authored pages are not rewritten a second time by error rules. Replacement
drops old encoding, length, cookies and other representation metadata, while
preserving authentication challenges, Retry-After and Allow. Pages use no-store.
ACL denial always prevents upstream access, even when its 403 page is configured
to return 200. A success status for an authentication/server error can mislead
clients and monitoring. Prefer preserving original statuses. Direct-IP HTTPS
must still finish TLS; this HTTP feature cannot bypass certificate validation.
HTML is not previewed in the admin DOM. It is trusted administrator content and
may execute scripts when served to visitors. Never import untrusted HTML.
## Separate live-log window
Use Live logs in the toolbar or a host's Logs button. Choose modal or docked mode;
filters remain above a scrollable table. Filters combine vhost, method, status
family, cache/error outcome and text (host, path, method, vhost, backend). Pause
freezes the displayed bounded snapshot, not collection. Resume uses the latest
snapshot. Export contains exactly the selected rows. Closing/reopening keeps
filters; signing out clears viewer state.
The source is a 256-entry in-memory access ring refreshed through SSE/polling.
It is not an unbounded tail, durable file, guaranteed replay or audit store. The
vhost field comes from the actual selected frontend and remains available for
cache/static responses, without fabricating a backend request. Pool health data
are live observations, not settings; disabled checks are not proof of liveness.
## Additional 0.4.0 controls
The Authentication tab enables Tinyauth. Security includes inherited/per-vhost
Server and diagnostic header settings. Details selects named listeners shared
with other vhosts; inline listeners are not part of the current configuration model. Certificates are managed in
the separate inventory rather than uploading keys from each vhost editor.
The toast moves into the active dialog/top layer so its message and action remain
visible and interactive above the backdrop. See INTEGRATIONS_040.md for contracts.
+444
View File
@@ -0,0 +1,444 @@
# 0.7.3 -> 0.8.0 deployment changes
## 0.15.1 -> 0.16.0
There is no SQLite schema migration. Existing vhosts, backends, cache files, administrator accounts and sessions are retained.
Custom locations are now interpreted only as overrides. An untouched legacy synthetic `root` prefix `/` location with no custom backend/cache/ACL/header/rewrite/local-response behavior is normalized away and replaced by the internal main-vhost fallback. A deliberately customized `/` location remains a real override. No manual edit is required for the normal legacy `root` entry.
Publishing the administration listener through a vhost remains explicit-risk functionality. For reserved session-cookie passthrough the public request must be HTTPS, the external hostname must be listed in `administration.allowed_hosts`, the backend must directly address the configured administration listener by IP, and that backend must have `accept_local_listener_risk=true`. Ordinary upstreams continue to have `rustproxy_session` removed in both directions.
Access-log JSON may contain the additional fields `http_version`, `transport`, `websocket_bootstrap`, `websocket_phase`, `websocket_error`, `h3_reset_code` and `upstream_status`. Cache/system metrics add memory-breakdown gauges; existing consumers should ignore unknown fields as usual.
## 0.15.0 -> 0.15.1
There is no schema or configuration-format migration. This patch changes only administration UI language-pack placement, Custom locations editing/help/preview and dashboard visualizations. Existing vhosts, locations, cache settings and SQLite data remain unchanged.
## 0.14.0 -> 0.15.0
There is no new SQLite schema version in 0.15.0. Renewal observations remain in the existing
schema-v3 `certificate_renewals` table. The application no longer contains a runtime JSON importer.
When `upgrade.sh` / `scripts/upgrade.py` is used while RustProxy is stopped, an obsolete
`<data-dir>/certs/renewals.json` observation sidecar is validated as a regular file and removed as
the final source-upgrade step. It is not imported and no second legacy copy is retained. If the
source-upgrade transaction fails before that final removal, the old sidecar remains untouched.
If a site replaces source manually instead of using the stopped upgrader, the old sidecar is simply
ignored. Back it up and remove it. An empty renewal-observation table is safe: the ordinary scheduler
reconstructs non-secret status/timestamps on its first 5–10 minute startup check, while certificates,
keys, ACME account material and renewal profiles remain unchanged.
## 0.13.1 -> 0.13.2
Back up `config.sqlite3` and the configured certificate files before updating. On first
0.13.2 open, RustProxy applies the additive SQLite schema-v3 migration and creates
`certificate_renewals`; existing configuration, administrator accounts, sessions, audit
records and revisions are retained. The table stores only non-secret renewal observation
state (timestamps/status/message). If it is empty and the older private
`certs/renewals.json` exists, validated observations are imported once and the sidecar is
removed only after the database write succeeds. That one-time runtime compatibility path existed in
0.13.2/0.14.0 and is removed in 0.15.0; use the 0.15.0 stopped upgrader described above instead.
The managed `update.sh` path for 0.13.2+ also marks the stop as a software update so an
enabled cold tier receives remaining hot cache objects even when ordinary
`cold_on_shutdown=false`. This is a best-effort persistent handoff bounded by the managed
stop timeout; no cache persistence is possible when cold storage is disabled.
The raw JSON editor moved to **Settings -> Manual config**. `/configuration` remains a
browser compatibility alias, so old bookmarks do not require migration.
## 0.8.0 -> 0.8.1 cache behavior
The source default for `cache.enabled` changes to `true`. Existing saved configurations
that explicitly contain `enabled: false` remain disabled; the new default applies to new
or missing cache settings and is not a silent rewrite of an administrator choice.
`cache.max_object_bytes` may now be configured up to 1 GiB when `cache.max_bytes` is at
least as large. Browser conditional requests/no-cache refreshes now use conditional HIT
or backend revalidation instead of blanket BYPASS; `no-store` remains a hard exclusion.
- Existing vhosts deserialize new traffic-policy fields with conservative/default-off
behavior except `http2_enabled` and `http3_enabled`, which default true to preserve prior protocol behavior.
- The pre-production inline/global listener format is intentionally no longer readable.
Recreate development listener configuration under `listeners.endpoints` and bind each
vhost with `frontend.listener_ids`. No automatic vhost migration is performed.
- Existing backend TLS settings are preserved. Only newly created panel backend rows use
the 0.13.0 default `tls_verify:false`; enable verification explicitly for origins that require certificate identity checks.
- First installs fetch verified GeoIP country files. Existing installations upgraded with
`update.sh` keep their data directory; install the two verified files under `data/geoip`
before enabling country rules if the installation predates 0.8.0.
No database schema migration is introduced. The release changes deployment defaults
and control-plane startup behavior:
- `admin setup-token` can create the one-time setup token before first server start.
- New managed systemd installations default to `0.0.0.0:80` and `0.0.0.0:443`.
Existing installations keep the saved service unit during `update.sh`; update does
not silently add port 443.
- Wildcard administration is opt-in only. Direct server startup requires
`--admin-listen 0.0.0.0:9090 serve --allow-remote-admin`.
The installer uses the equivalent `--admin` and `--allow-remote-admin` options.
This is plain HTTP; keep it behind a firewall/private network or use a tunnel.
Back up private state before updating and run the real Rust check/test suite on the
target build host.
---
# 0.7.1 -> 0.7.2 build fix
This is a distinct source release. It removes the absent-SWAGGER-environment panic;
it does not migrate the database, alter vhosts or reset any account/certificate.
Back up private data as usual and update the complete source tree, not just build.rs.
The patch changes Cargo dependencies and adds a required build helper, so copying
only the Rust file would leave an inconsistent package.
The existing `dev.sh` Python 3.10+ requirement now also applies to direct Cargo
builds. No `.cargo/config.toml` or manual `SWAGGER_UI_DOWNLOAD_URL` export is required.
The default pinned tag is fetched on the first build; a reviewed local tag archive
remains supported for restricted build hosts. See [API_ACCESS.md](API_ACCESS.md).
No default to an older Swagger distribution is introduced.
Use the existing stopped-source `update.sh --source-tree` workflow or build a new
binary and use the managed-service updater as documented in [INSTALL.md](INSTALL.md).
`dev.sh` backs up an older Cargo.lock before regenerating it; do not fake the new
lockfile by replacing its root version text. A valid new lockfile is not included.
Rebuild/restart after successful tests; the old process cannot load embedded changes.
There is no need to clear application data or certificates, or to run `cargo clean`
just to remove this panic. Test the real admin session and `/docs` after building.
---
# 0.7.0 -> 0.7.1 access changes
No database/configuration schema migration is introduced by this patch. Back up
existing data/certificates and use the existing install/update instructions.
The reported release is source-only; compile and test before installing a binary.
Operational JS and full translation packs are now session-protected. Old tabs
should be reloaded after upgrading. Automation reading `/api/auth/status.version`
or `/healthz.version` must use authenticated `/api/status` instead. Dedicated
Prometheus scraping retains its own CIDR/token policy and remains independent of
browser sessions. No reverse-proxy vhost auth policy is relaxed by this patch.
In 0.7.1 the Swagger pin depended on `.cargo/config.toml`. That requirement is
removed in 0.7.2 (see the build notes above). Source-tree updates still back up and
replace that managed directory; retain any unrelated local Cargo settings from
your private backup. Read [API_ACCESS.md](API_ACCESS.md) for current build inputs.
Run `python3 scripts/admin_audit.py --url http://127.0.0.1:9090` after deployment.
It prints only status checks, no response bodies or credentials.
---
# Updating, backup and recovery
## 0.7.0: renewal and service deployment
See [INSTALL.md](INSTALL.md) for separate installer/updater commands and complete
code-plus-data rollback. Older source deployments retain `update.sh --source-tree`.
Back up the entire private state, including Certbot profiles, before any update.
Do not overlay a new binary onto a live, untested database without a recovery plan.
Managed 0.6 certificate issuer metadata defaults to automatic renewal in 0.7.
The scheduler starts after a 5-10 minute delay and reuses existing consented
profiles. Imported PEM without issuer metadata is not scheduled. Review auto-renew
flags, service PATH, DNS permissions and HTTP-01 reachability before starting.
The new process does not implicitly switch staging certificates to production.
Missing WebSocket flags preserve existing enabled behavior. New UI-authored vhosts
default to disabled. Existing backend members remain in the normal pool; new
dedicated location origins default to `pool_eligible:false`. Basic Auth is opt-in,
requires actual TLS by default and combines with (rather than bypasses) IP ACLs.
Normal dev/server startup no longer starts the demonstration origin. Existing
sample vhosts are not silently deleted: remove or replace them deliberately, or
explicitly enable the protocol fixture in an isolated old test installation. New
databases show a neutral informational landing page with no sample backend.
New fields are not a promise of backward parser compatibility. Downgrade by
restoring both old code and the pre-upgrade private-data snapshot. No verified Rust
build is supplied; run check/test and server acceptance before rollout.
## 0.6.0: historical vhost migration note
0.6.0 originally shipped a one-way importer from the pre-vhost development format.
That importer is removed in 0.8.0. Current builds accept only `vhosts`, named
`listeners.endpoints` and `frontend.listener_ids`. Development databases containing
top-level route/backend documents or inline frontend listeners must be recreated or
manually rewritten before starting 0.8.0. No automatic compatibility conversion runs.
The dedicated Prometheus listener remains opt-in/disabled by default. Settings ->
Prometheus controls it. A restart is required for enable/bind/token-variable changes.
Certificate provider metadata remains additive and secrets remain outside exported
configuration.
## 0.5.0 behavior changes
Back up the database and configured PEM pairs before upgrading. The managed
updater replaces the full source tree, not live data. Build and test on staging.
No verified binary or fabricated Cargo.lock is supplied.
TLS bindings with omitted `http3` now enable UDP with defaults; older versions
omitted disabled options when serializing, so review ALL TLS bindings. To retain
TCP-only operation, explicitly save `http3: null` on the named TLS listener before
restarting the new process. Shared/wildcard
IP-port bindings must agree on all protocol flags and H3 limits. A UDP bind failure
is not silently ignored. HTTP/1.1 and optional HTTP/2 remain on TCP.
`diagnostic_headers.hide_mock_origin` was removed and is no longer accepted or
exported; use explicit response-header rules if needed. X-Mock-Origin is a normal upstream field; remove it with ordinary
response-header rules when desired. Request ID remains off by default. The removed 0.17 `diagnostic_headers.revision` field is no longer part of the configuration schema. Custom Server values
are NOT automatically rewritten to the new binary version.
Cache contents are process-local. Reserved admin-cookie removal now occurs before
cache eligibility; application cookies still bypass. Domain counters reset on
configuration publication. See CACHE_DIAGNOSTICS.md for explaining BYPASS.
ACME/RFC2136 is optional and adds external Python/Certbot/plugin requirements.
No DNS update or CA request happens simply by upgrading. Issuance is explicit,
staging-first and does not activate a certificate without import. Existing
`dev.sh backup` does not include the private `certs/acme` account/TSIG profiles;
back those up separately when issuance is idle. No automatic renewal is included.
See ACME_RFC2136.md for limitations and restoration responsibilities.
## Reported auth module compilation failure
0.0.1 used `src/auth.rs`. 0.0.2 moved the implementation into `src/auth/mod.rs` and
`src/auth/store.rs`. Overlay extraction can retain both layouts and cause Rust
E0761. Missing `Store::session_at`, `admin_record`, `end_session` and
`revoke_all_sessions` messages can follow because the implementation module did not
load. The 0.0.2 archive itself did not contain both auth layouts.
0.4.0 retains an explicit `#[path = "auth/mod.rs"]` declaration, including for direct
Cargo invocations. Preflight also detects mixed source trees. It recognizes the
exact original auth file by SHA-256 and moves it to
`.upgrade-backup/legacy-auth/auth.rs.0.0.1`. Unknown custom source is left untouched
and the development script stops with instructions. Never delete custom files just
to silence a compiler error.
```bash
bash dev.sh doctor
bash dev.sh repair
```
These commands do not require Rust. Repair is deliberately limited; a safe whole
source-tree update is preferable to extracting individual files over a checkout.
## Recommended source update
Extract the new archive beside, not inside, the installed project. First back up
the existing database using the new backup script. The destination must not exist:
```bash
unzip RustProxy-0.4.0.zip -d /home/mg/rustproxy-040-staging
cd /home/mg/rustproxy-040-staging/RustProxy
python3 scripts/backup.py --data-dir /home/mg/RustProxy/data \
/home/mg/RustProxy-backup-before-0.4.0
```
A missing database means there is no database to back up. Do not initialize an empty
replacement accidentally. For data stored elsewhere, use the actual directory.
Stop RustProxy using its terminal or service manager. Then:
```bash
bash upgrade.sh /home/mg/RustProxy --stopped
# External data directory: also pass --data-dir /actual/private/data
cd /home/mg/RustProxy
bash dev.sh test
bash dev.sh
```
The updater checks a process lock on POSIX, requires the explicit `--stopped`
acknowledgement and rejects nested source/target paths and managed symlinks.
It preserves `.env`, `data`, `.git`, `target` and unmanaged files. Source, scripts,
tests, examples and documentation are replaced as whole managed paths. Old managed
files remain in `.upgrade-backup/before-0.4.0-<identifier>`. An interrupted copy
exception triggers source rollback. Abrupt power loss is not a transactional
filesystem update; inspect the backup and source tree before restarting.
The updater does not evaluate the old `.env` or discover arbitrary external data
paths. Pass `--data-dir` when necessary, and stop every process yourself before
changing source. It does not perform a database migration or backup automatically.
Custom source edits are backed up, not merged.
## Database compatibility
RustProxy 0.13.2 advances the SQLite schema from version 2 to **version 3** with one
additive `certificate_renewals` table for non-secret ACME observation state. The existing
0.0.1-to-0.0.2 auth migration still runs when needed. Configuration-document additions
remain defaulted/non-destructive. Existing accounts, policies, revisions and configuration
remain intact. Starting with 0.15.0 the application reads renewal observations only from this table;
historical JSON observation state is handled, when present, by the stopped source-upgrade remove
described above rather than by the running service.
The generated default certificate is created on first TLS-enabled startup, in
`<data-dir>/certs`. Its key is never overwritten by startup. A partial or invalid
pair causes a clear startup error; it is not silently discarded. Demo settings
never overwrite an existing configuration, including an empty existing one.
## Backup contents and restore
`./dev.sh backup NEW_DIRECTORY` uses SQLite's online backup API, so committed WAL
content is included. The result contains a private `config.sqlite3`, `backup.json`
with hashes/revision metadata and any configured/default certificate files under
`certs/`. Unreferenced staged certificate files are not included. Do not rotate or
manually replace certificate files while taking a backup. This is a consistent
SQLite snapshot plus file copies, not one cross-filesystem transaction.
Backups contain credential hashes, session records and private keys. Keep them
private, encrypted at rest as appropriate, and outside public web directories.
They do not contain `.env`, source edits or a generated dependency lockfile; preserve
those separately. Existing backup destinations and certificate symlinks are refused.
To restore, stop the server, verify backup hashes and keep the current data directory
as a separate recovery copy. Restore into a **new private directory**, including
`config.sqlite3` and `certs/` together, then point `RUST_PROXY_DATA_DIR` to it.
Use directory mode 700 and file mode 600. Do not copy stale `-wal` or `-shm` files
from another database beside the restored snapshot. Start the matching version and
run `./dev.sh admin logout-all` or reset the password to revoke restored sessions.
Verify routes, TLS identities and administration before resuming traffic.
## Downgrade
Older versions reject unknown configuration fields such as `tls`, `protocol` or
`tls_ca_pem`. Restoring old source alone is therefore not a safe downgrade after
saving new configuration. Restore the matching pre-upgrade database and environment
from a verified backup, with the server stopped. Do not edit password hashes or
schema versions manually.
## 0.0.6 -> 0.1.0 document and source changes
No new SQL schema version is introduced. Old documents default to inherited
listeners, disabled cache/compression and preserve-Server behavior. New optional
route cache policy does not change existing route behavior. Existing account,
revision, TLS and ACL state is retained.
Managed paths now include build.rs, langs, deploy and make_zip.py. Extra *.json
language packs from the installed langs directory are copied into the new tree
when the new distribution does not contain the same filename. Shipped packs are
updated, with old bytes preserved in the source backup. Invalid custom packs can
correctly block a build; review them with scripts/langs_check.py. No symlinked
language pack is followed. The updater never copies a database from the package.
The old lockfile is preserved in the managed source backup and a new dependency
graph is generated by Cargo on first build. No 0.1.0 resolved Cargo.lock is included
because Cargo was unavailable here. Keep the actual newly resolved file afterward.
Saving any 0.1.0 document serializes its new fields. Older binaries use strict
unknown-field rejection, so rolling back only the source/binary may fail startup.
Restore the pre-upgrade database/certificates into a new private directory and keep
the newer data separately. Do not directly edit live SQLite to remove fields.
A saved invalid listener address or missing low-port permission can prevent
startup. The online config CLI cannot repair an offline server. Keep a pre-change
backup and use the documented stopped-server restore path; remove persisted
listener overrides through Settings while the service is still running before
changing deployment assumptions. The cache itself is RAM only and is not backed up.
## 0.1.0 -> 0.2.0: compression, vhosts and pool behavior
The `SizeAbove` u16/u64 mismatch is fixed by replacing that predicate with a custom
u64 implementation. Do not use `try_into().unwrap()` or truncate the setting to
work around the old source: configured thresholds above 65535 must remain valid.
This source change removes that call, but is not a successful compiler result;
run `bash dev.sh check` and `bash dev.sh test` on a Rust-enabled staging machine.
SQL schema version stays 2; accounts, ACLs, certificates and revisions are retained.
New `vhosts` defaults empty; named TLS listeners default to H2/H3 capability when those fields are omitted. Legacy routes
continue on the global listeners and are never silently migrated or exposed on
additional IPs. The editor's explicit migration action converts one unambiguous
route group in a single revision and keeps the migrated route group's existing health behavior.
New vhosts default to TCP health checks and remain unavailable until
`rise` checks succeed. Plan the correct path/status/timeouts before migration.
Health/counter/affinity observations are ephemeral. They survive compatible edits,
not restarts; new/changed/re-enabled origins require new checks. Cache and idle
upstream pools are not carried across config publication. Admitted exchanges finish
with their old snapshot and are not replayed on a different backend.
HTTPS origins previously configured `http2` now retain HTTP/1.1 ALPN compatibility.
Plaintext `http2` is still strict h2c. Per-listener IP/port/TLS/HTTP2 changes require
restart. During an address move, hot routing can deny the previous destination
before the new socket is bound. Plan a maintenance window, verify assigned IPs,
privileges/certificates and retain an offline-restorable backup.
After saving any 0.2.0 document, 0.1.0 can reject new fields even without a vhost,
because `https_http2` is serialized. Downgrade requires matching pre-0.2.0 data,
source and environment. Never remove fields by editing a live SQLite database.
The archive contains no fabricated lockfile, executable, database or private key.
Generate and retain the real dependency graph before acceptance/deployment.
## 0.2.0 -> 0.3.0
The reported `json_internal!` recursion error is addressed by splitting
`status_payload` into small JSON values and inserting feature flags through a map.
Do not overlay only this function: this release also adds the `query` Axum feature,
`src/pages.rs`, `management.js`, new configuration fields and matching UI assets.
The earlier u64 compression predicate remains in place. Compilation has not been
executed in the packaging environment; this is not a successful-build assertion.
Existing database documents deserialize with empty/default `pages`, vhost `cache`
and `certificate_id` fields. No SQL schema migration is added. Per-vhost certificate
selection rewrites the effective SNI map in a snapshot, not the files. Upload is a
staging action: Save must publish a validated configuration before traffic uses it.
Canceling the editor leaves an unbound pair, not an active certificate change.
Use the complete backup/whole-source update procedure above. The existing backup
copies configured keys and fallback files. Unbound upload staging files are not in
the configuration and therefore are not part of that configured-certificate backup.
Retain them separately only when needed. Never delete a filename still referenced
by the configuration or currently serving snapshot; stop the process before manual
cleanup and preserve a backup of the full private certificate directory.
New defaults do not activate custom pages or vhost cache overrides. An explicitly
disabled global cache can still be overridden by one vhost; the global byte/entry/object
limits still bound it. Current releases rotate cache compatibility only for configuration
changes that can alter cached/origin semantics; unrelated control-plane publication preserves
reusable objects. The dedicated per-vhost Purge action remains selective.
Downgrade requires restoring the earlier database/configuration together with its
matching source: 0.2.0 rejects the new fields. Do not merely relabel a new document.
The source archive contains no fabricated lockfile. Resolve and retain Cargo.lock
on the first compiler-enabled build and run all tests before restarting production.
## 0.3.0 -> 0.4.0
Use the full-source update above, not an overlay of a few Rust files. This revision
adds Quinn/h3 dependencies, new modules and a new embedded integrations.js asset.
No successful compiler run or dependency resolution is claimed. Generate and retain
a real Cargo.lock, then run check and all tests before acceptance.
SQL schema stays 2. Proxy sockets are persisted only in `listeners.endpoints`; a new
database is seeded from startup CLI listener addresses. Existing development documents
using the removed listener format are rejected rather than migrated. Tinyauth and
vhost Server policy continue to use their normal defaults. New diagnostic defaults remove x-request-id from client responses unless it is enabled explicitly where needed. Access logs retain request IDs. The old revision diagnostic field/header behavior was removed in 0.18.2. Forwarding headers are
rebuilt from actual peers unless known proxy CIDRs are explicitly configured.
Reference named listener IDs per vhost instead of duplicating bindings. There is no
inline/global compatibility conversion path in 0.8.0. Scope names have no firewall meaning. Changed sockets, ALPN or H3 limits
require restart; opening UDP is necessary for H3. Saving intent does not prove
that an OS bind or UDP firewall rule will succeed.
The certificate directory/fallback is prepared even with HTTP-only startup, so
library uploads can be staged before enabling TLS. The generated fallback is
self-signed development material and does not automatically open an HTTPS socket.
Import the PEM pair in Certificates, save inventory, then select its ID in SSL.
Unbound staged files are not automatically backed up; preserve private files needed
for rollback and never remove any currently referenced certificate/key.
Tinyauth-protected vhosts bypass shared cache regardless of cache toggles. Failures
deny access instead of serving cached/static content. Configure its verifier URL,
public login URL and explicit private-origin permission before enabling it on an
existing service. Test login, refusal, logout/revocation and WebSockets on staging.
The examples are complete replacement documents for disposable test databases,
not merge patches. Saving a 0.4 document serializes new fields; older binaries may
reject them. Downgrade requires matching pre-upgrade database, certificates,
environment and source, with the process stopped. The source backup alone is not
a database/configuration rollback.
+37
View File
@@ -0,0 +1,37 @@
# Fake Tinyauth protocol peer - local development only
This is NOT authentication. Any client can mint the allow cookie. The fixture binds
only to 127.0.0.1 and must never protect a production application.
```bash
bash dev.sh mock --port 9001 --name origin-a --ttl 30
# Separate terminal:
bash dev.sh mock-auth --port 9300
```
In a disposable vhost's Authentication tab, set:
- verifier: http://127.0.0.1:9300 (adds /api/auth/nginx)
- login: http://127.0.0.1:9300/login
- explicitly allow private verifier IPs
- forwarded identities: remote-user and remote-email
Use your actual frontend address/hostname. Example for a local plain-HTTP vhost:
```bash
# Denied -> 302 to configured login, no application request:
curl -i -H 'Host: app.localhost' http://127.0.0.2:8181/inspect
# Allowed -> app receives mock-user / mock@example.test; cache remains BYPASS:
curl -i -H 'Host: app.localhost' -H 'Cookie: mock_auth=allow' http://127.0.0.2:8181/inspect
# Force authentication failure even with an allow cookie:
curl -X POST -H 'Content-Type: application/json' -d '{"status":503}' http://127.0.0.1:9300/control
# Simulate slow verifier; choose less than or greater than the vhost total timeout:
curl -X POST -H 'Content-Type: application/json' -d '{"delay_ms":12000}' http://127.0.0.1:9300/control
# Restore default deny/allow behavior:
curl -X POST -H 'Content-Type: application/json' -d '{"reset":true}' http://127.0.0.1:9300/control
curl http://127.0.0.1:9300/state
```
/control replaces status/delay/redirect behavior (omitted values reset to defaults).
Optional location sets X-Tinyauth-Location for redirect-policy tests. /state exposes
only the last 16 metadata records, not Authorization, cookies or query strings.
Real Rust-router auth tests are in tests/integrations.rs.
+116
View File
@@ -0,0 +1,116 @@
# Python-only mock origin
Run independently of Cargo/Rust, in a separate terminal:
```bash
bash dev.sh mock --port 9001 --name origin-a --ttl 30
```
This runs until Ctrl+C. A second origin for balancing uses another terminal:
```bash
bash dev.sh mock --port 9002 --name origin-b --ttl 30
```
The helper binds 127.0.0.1 by default. It accepts IPv4 addresses only and refuses
non-loopback bindings without --allow-remote. Do not expose it publicly:
/control deliberately has no authentication. It has 32 connection workers,
15-second socket timeout, a 2 MiB maximum response and a 10-second delay ceiling.
It is a development fixture, not a secure production website.
## Configure a test vhost
After building and starting RustProxy, create a vhost with domain mock.localhost,
frontend 127.0.0.1:8080, backend http://127.0.0.1:9001, private backend permission,
HTTP health path /health, and Cache enabled with X-Cache diagnostics. Leave sticky
cookies off to test shared cache. Wait for its backend to become healthy.
`examples/mock-vhost.json` is a complete ready-to-apply document for a disposable
installation. It explicitly enables the per-vhost cache (the 0.8.1 global default is
also enabled), adds a custom 503 page, and serves the congratulations page on direct-IP access. Applying
it replaces all existing routes/vhosts; it is not a merge patch. Review first:
```bash
bash dev.sh config validate examples/mock-vhost.json
bash dev.sh config apply examples/mock-vhost.json
```
Do not replace your real configuration for this demonstration. Use the panel to
add equivalent settings to an existing project instead.
## Verify cache, not just the origin
Requests must go through the proxy with the right Host:
```bash
curl -si -H 'Host: mock.localhost' http://127.0.0.1:8080/cache
curl -si -H 'Host: mock.localhost' http://127.0.0.1:8080/cache
```
After the first complete response, expect MISS then HIT with an identical nonce
while fresh. Origin JSON always generates a new nonce and timestamp; a cache hit
therefore makes reuse visible. Health probes can advance the sequence too, so
compare nonce/body, not an assumption that sequence increases by exactly one.
GET is the clearest fill test because it consumes the complete body. HEAD is also
cacheable in 0.11.5 and shares the GET representation. Only response `Vary` fields
create request-header representation variants; unrelated request headers and the
selected backend-pool member no longer fragment the same Host + URL object.
Use the host's Purge action and request again to expect a new MISS. Waiting past
TTL also refreshes. To compare with a non-cacheable route:
```bash
curl -si -H 'Host: mock.localhost' http://127.0.0.1:8080/nocache
curl -si -H 'Host: mock.localhost' http://127.0.0.1:8080/nocache
```
Bodies should change and must not become HIT. A diagnostic MISS means lookup was
attempted, not that the response was stored: no-store/private/Set-Cookie may reject
storage after origin headers arrive. BYPASS indicates a request-side exclusion.
## Endpoints
| Path | Behavior |
|---|---|
| / | Small HTML page with links |
| /cache?ttl=30 | Random JSON; public max-age; Vary: Accept-Language |
| /nocache | Random JSON; no-store |
| /private | Private JSON; cannot enter shared cache |
| /cookie | Sets a cookie; must not enter shared cache |
| /large?size=131072 | Compressible bytes; public freshness; useful above 65535 bytes |
| /slow?ms=800 | Delayed no-store response |
| /health | 200/503 according to controlled health; delay is also applied |
| /state | Current health, delay, sequence and origin ID |
| /status/401, /status/403, /status/404, /status/503 | Origin error responses for page tests; 401 includes a challenge |
| POST /control | JSON with healthy:boolean and/or delay_ms:0..10000 |
Compression needs enabled proxy compression and an appropriate Accept-Encoding.
The origin itself does not compress. Inspect both response headers and decoded body;
not all curl builds support Brotli or Zstandard. Example:
```bash
curl -si --compressed -H 'Host: mock.localhost' \
'http://127.0.0.1:8080/large?size=131072'
```
## Failover, latency and recovery
Control the origin directly, not through the proxy, especially after it is marked
down. Configure the health path /health (the default root page always returns 200).
```bash
curl -s -H 'Content-Type: application/json' -d '{"healthy":false}' http://127.0.0.1:9001/control
curl -s -H 'Content-Type: application/json' -d '{"healthy":true,"delay_ms":1200}' http://127.0.0.1:9001/control
curl -s -H 'Content-Type: application/json' -d '{"healthy":true,"delay_ms":0}' http://127.0.0.1:9001/control
```
Watch health/RTT in the vhost list/editor and dashboard. Fall/rise thresholds and
probe interval delay transitions intentionally. Add origin-b as a second backend
for failover and balancing tests. Random origin data alone does not prove that the
Rust scheduler, compression or cache is correct; run the Rust acceptance tests.
## Verification scope
The mock server is a development fixture, not a proxy test. Exercise its endpoints
directly with curl when needed, then run the RustProxy suite separately with
`bash dev.sh test` on a host that has Cargo.
+24
View File
@@ -0,0 +1,24 @@
# Local-response / 404 hosts
RustProxy 0.22.3 has a dedicated **404 hosts** page for domains that should be
owned by RustProxy but must not contact a backend.
Each entry has the same frontend model as vhosts/redirects:
- one or more hostnames,
- named listener selection,
- optional TLS certificate,
- independent HTTP/2 and HTTP/3 switches.
The response can be:
- the built-in `RustProxy is running` placeholder,
- an application-style built-in status page for 401, 403, 404, 429, 503 or 504,
- an empty/status-only response,
- bounded custom HTML.
The status is selected rather than typed manually. Application-style pages expose only the bundled 401/403/404/429/503/504 templates. Placeholder, empty and custom-HTML modes expose the supported application error set: 400/401/403/404/408/413/429/500/502/503/504. HTTP/2 and HTTP/3 toggles are shown beside the listener controls because they apply to the selected frontend listeners. Host/listener ownership is
validated against vhosts and redirect hosts, so two active configuration objects
cannot claim the same frontend binding.
All 404-host responses remain local and never open an upstream connection. They are not stored in RustProxy cache. Before the response leaves RustProxy, cache policy/validator headers and RustProxy `X-Cache*` diagnostics are removed, so 404 hosts do not advertise cache behavior on the wire. The built-in `RustProxy is running` placeholder also intentionally omits optional RustProxy server, diagnostic, security, compression and backend headers.
+115
View File
@@ -0,0 +1,115 @@
# Nginx Proxy Manager migration
RustProxy ships `scripts/migrate_npm.py` for a one-time, dry-run-first migration of
Nginx Proxy Manager (NPM) proxy hosts from its SQLite database.
The tool reads NPM only. It does not modify the NPM database and it never imports
Let's Encrypt/private-key material. It logs in to the RustProxy administration API,
reads the current revision, merges generated vhosts into the existing configuration,
and writes only when `--apply` is supplied.
## What is migrated
For each NPM `proxy_host` row:
- `domain_names` -> RustProxy vhost hostnames, including leading `*.` wildcards;
- `forward_scheme`, `forward_host`, `forward_port` -> one RustProxy backend;
- `allow_websocket_upgrade` -> vhost WebSocket policy;
- `enabled` -> vhost enabled state;
- `http2_support` -> vhost HTTP/2 policy;
- NPM hosts with a certificate/forced SSL -> RustProxy TLS listener. Certificate
files are not copied: the application-managed RustProxy default certificate is
used unless `--certificate-id` names an existing RustProxy inventory certificate;
- `ssl_forced` -> RustProxy `force_ssl=true`. When HTTP and HTTPS listeners exist,
both are attached so cleartext requests can receive RustProxy's `308` redirect to
HTTPS. If no TLS listener exists, an SSL-forced NPM host is skipped instead of being
imported with weaker semantics.
Migrated backends use `allow_private=true` because NPM commonly proxies to LAN or
container origins. The migration tool follows RustProxy 0.22.1 and imports backend `tls_verify=false` unless you later enable verification explicitly.
Use `--npm-compatible-backend-tls` when an NPM HTTPS origin depends on
the common Nginx behavior of not verifying the upstream certificate; review that
exception after migration.
The following are reported for manual review rather than guessed: NPM access lists,
advanced Nginx snippets, custom locations, NPM cache settings and `block_exploits`.
Existing RustProxy hostname ownership is never silently replaced; conflicting NPM
rows are skipped and shown in the report.
## SQLite location
The standard NPM SQLite path inside the container is `/data/database.sqlite`. On a
Docker host with a bind mount, pass the corresponding host path. For a named volume,
`docker volume inspect` shows its mountpoint.
Read the live mounted database rather than copying only `database.sqlite` from a
running WAL-mode instance; copying without its WAL file can omit recent changes.
## Dry run
Set the password in the environment or omit it and use the interactive prompt:
```bash
export RUSTPROXY_USERNAME=admin
export RUSTPROXY_PASSWORD='...'
python3 scripts/migrate_npm.py \
--database /path/to/npm/data/database.sqlite \
--rustproxy https://rustproxy.example.net:9090
```
The default run is read-only. It creates `npm-migration-plan.json` with mode `0600`
and prints every imported/skipped NPM row plus manual-review warnings.
If the administration panel intentionally uses the application self-signed
certificate and the script connects directly to it, add:
```bash
--insecure-admin-tls
```
Prefer `--ca-file /path/to/ca.pem` or a hostname with a trusted certificate where
available.
By default the first enabled HTTP listener and first enabled TLS listener are used.
For `ssl_forced` hosts both are selected when available. `--force-https` applies the
same Force SSL policy to every imported host. Select explicit listener IDs when needed:
```bash
--http-listener public-http --https-listener public-https
```
## Apply
After reviewing `npm-migration-plan.json`:
```bash
python3 scripts/migrate_npm.py \
--database /path/to/npm/data/database.sqlite \
--rustproxy https://rustproxy.example.net:9090 \
--apply
```
The write uses the revision returned by `GET /api/config`; a concurrent RustProxy
configuration change causes the API compare-and-swap to fail rather than overwrite a
newer revision.
## Running from a helper container
If the Docker host does not have Python 3, mount the NPM data read-only and mount the
RustProxy source/script into a temporary Python container. For example, from the
RustProxy directory:
```bash
docker run --rm -it \
--network host \
-v /path/to/npm/data:/npm:ro \
-v "$PWD/scripts/migrate_npm.py:/migrate_npm.py:ro" \
python:3.13-alpine \
python /migrate_npm.py \
--database /npm/database.sqlite \
--rustproxy https://127.0.0.1:9090 \
--insecure-admin-tls
```
Use `--apply` only after the dry-run plan is reviewed.
+147
View File
@@ -0,0 +1,147 @@
# Dedicated Prometheus endpoint (0.22.3)
RustProxy exposes Prometheus text exposition 0.0.4 on a dedicated read-only
listener. It is independent from administrator browser sessions and does not
export request paths, query strings, cookies, credentials, backend URLs or ACME
account secrets.
## Configuration
```json
{
"prometheus": {
"enabled": false,
"listen": "127.0.0.1:9191",
"path": "/metrics",
"allowed_cidrs": ["127.0.0.0/8", "::1/128"],
"bearer_token_env": null,
"allow_trusted_network_without_token": false,
"include_system": true,
"include_vhosts": true,
"include_backends": true,
"include_docker_vhosts": true,
"include_cache_domains": true,
"include_streams": true,
"include_logging": true
}
}
```
The listener is plain HTTP. Keep it on loopback, a private monitoring network or
behind a protected tunnel. `allowed_cidrs` is always evaluated against the actual
transport peer (`PeerAddr`); `Forwarded` and `X-Forwarded-For` never grant metrics
access.
### Bearer token
Set `bearer_token_env` to an environment-variable name such as
`RUST_PROXY_METRICS_TOKEN`. RustProxy stores only the variable name. The value must
contain 32..4096 printable non-whitespace ASCII characters and must be present at
startup.
### Trusted network without a token
`allow_trusted_network_without_token=true` explicitly allows a peer already matched
by `allowed_cidrs` to scrape without a bearer token. This can be used for a private
Prometheus VLAN. An explicit IPv4/IPv6 `/0` is rejected in this mode. Keep the CIDR
list narrow; enabling this option is an authentication decision, not a forwarded-IP
feature.
A non-loopback exporter requires either a bearer-token variable or this explicit
trusted-network bypass. Changing enabled/listen/token-variable requires restart;
path, CIDRs, collector switches and the trusted-network bypass apply live.
Only `GET` on the exact configured path without a query string is accepted. A
wrong path is `404`, wrong method `405`, disallowed peer `403`, invalid/missing
required bearer `401`, and exporter concurrency exhaustion `429`.
## Coverage audit in 0.19.1
The exporter represents all scalar fields currently sampled by `diagnostics::View` when available. The per-interface `network_interfaces` list added in 0.18.6 is intentionally dashboard-only so kernel-created interface names cannot create an uncontrolled Prometheus label set. RustProxy also exports the bounded operational data already available from traffic metrics, cache, vhosts, backend pools, streams, logging, GeoIP and ACME renewal observations.
| Area | Exported diagnostics |
|---|---|
| Global traffic | requests/completions/active/rejects/errors, status classes, request/response bytes, exchange lifetime, accepted/active/rejected connections |
| Protocols | HTTP/1.1, HTTP/2 and HTTP/3 requests, H2/H3 connections/rejects, TLS connections/handshake failures, WebSocket tunnels/bytes/errors/lifetime, compression counters |
| Admin | request/status counters and time to control-plane response headers |
| Process/host | diagnostic source, runtime platform info, sampled timestamp, system/process CPU, logical CPUs, RAM total/used, system swap total/used, system uptime, RSS total/anon/file/shmem, virtual memory/limit, VmData, process swap, FDs/limit, threads, process start time, load 1/5/15, network totals/rates, process disk I/O, cgroup memory current/max |
| GeoIP | loaded state plus IPv4/IPv6 range counts |
| Cache | hot entries/bytes, global hit ratio and counters, cold entries/bytes and movement/error counters, fill bytes, cold queue bytes/items, active cold read/write bytes, combined transient bytes, configured-domain hot/cold occupancy, hit ratio and decision data |
| Vhosts | desired enable/domain/path/cache settings, per-vhost traffic/status/error/byte counters and cumulative exchange-duration histogram |
| Redirect/404 hosts | desired enable state, domain/rule counts and local-response status |
| Backends | pool readiness/policy info, bounded backend status, health/drain/backup, active work and configured request-concurrency/queue limits, requests/errors/bytes, weight, separate active/passive failure state, active/passive successes and health-failure totals, unconditional HTTP-5xx/timeout/connect/I/O/slow-response observation counters, DNS/TLS/header-timeout/read-timeout/send-timeout diagnostics, capacity rejection/queue-timeout counters, plus `passive_http_5xx_total`, `passive_timeouts_total`, `passive_connect_errors_total`, `passive_io_errors_total` and `passive_slow_total` aliases, health transitions, last status, current/EWMA latency, last-probe/failure/ejection/recovery timestamps, traffic rate, and retry attempt/success totals for ordinary vhosts |
| Docker vhosts | separate `rust_proxy_docker_vhost_*` and `rust_proxy_docker_backend_*` families: configured agents/assignments/domains, vhost traffic/status/duration plus backend readiness, health, requests/errors/bytes, latency/EWMA, separate active/passive failure state, unconditional observation counters and matching passive-health aliases, transitions and probe results |
| Streams | desired enable state, active/session/reject/error counters, bytes/packets and max connections |
| Logging | structured sink queue capacity, sent/error/drop counters plus the global live-log contention drop counter |
| TLS/ACME | configured certificate count, auto-renew flag, bounded issuer metadata, latest ACME job state, renewal check/expiry timestamps, failure count and status |
| Inventory | listeners/protocol flags, vhost/redirect/404/stream/ACL counts, request capacity and available permits, config revision and restart-required state |
Values unavailable on the running OS are omitted rather than fabricated. Series
labels are bounded by configured IDs/domains, finite states and fixed protocol/status
families. Per-request URLs and arbitrary Host input are never labels.
This is complete for **currently implemented bounded diagnostic views**. It does
not intentionally expose secrets, raw configuration documents, access-log entries
or unbounded per-request values merely to make them visible to Prometheus.
## Prometheus examples
Loopback/private CIDR scrape without a token when the explicit trusted option is
used:
```yaml
scrape_configs:
- job_name: rustproxy
scrape_interval: 15s
metrics_path: /metrics
static_configs:
- targets: ['127.0.0.1:9191']
```
With bearer authentication:
```yaml
authorization:
type: Bearer
credentials_file: /etc/prometheus/secrets/rustproxy-token
```
Vhost rate:
```promql
sum by (vhost) (rate(rust_proxy_vhost_requests_total[5m]))
```
Vhost p95 exchange lifetime:
```promql
histogram_quantile(0.95,
sum by (vhost, le) (rate(rust_proxy_vhost_exchange_duration_seconds_bucket[5m])))
```
Docker-vhost request rate (kept separate from ordinary vhosts):
```promql
sum by (agent, vhost, service) (rate(rust_proxy_docker_vhost_requests_total[5m]))
```
Docker backend availability and latency:
```promql
rust_proxy_docker_backend_up
rust_proxy_docker_backend_ewma_latency_seconds
```
Certificate expiry window:
```promql
rust_proxy_certificate_renewal_not_after_timestamp_seconds - time()
```
The supplied dashboard is `grafana/rustproxy-overview.json`; Debian packages also
install it under `/usr/share/rustproxy/grafana/`. Its dedicated **Docker vhosts** row
uses only the separate Docker metric families and provides Docker-agent/service
filters so multi-agent installations can be inspected independently from ordinary
vhosts/backends.
Reference: https://prometheus.io/docs/instrumenting/exposition_formats/
+46
View File
@@ -0,0 +1,46 @@
# Documentation index
This index separates current operational documentation from historical/version-labelled material. A version in an older document title describes the milestone that introduced or last audited that topic; it is not the current RustProxy release identity.
## Current operational entry points
- [INSTALL.md](INSTALL.md) - installation and managed updates.
- [SETTINGS.md](SETTINGS.md) - listeners, response defaults and administration settings.
- [API.md](API.md) and [API_ACCESS.md](API_ACCESS.md) - administration API and access controls.
- [VHOSTS.md](VHOSTS.md), [VHOST_ACCESS.md](VHOST_ACCESS.md), [CUSTOM_LOCATIONS.md](CUSTOM_LOCATIONS.md) - HTTP routing and access behavior.
- [DOCKER_DISCOVERY.md](DOCKER_DISCOVERY.md) - Docker discovery agent and generated vhosts.
- [CACHE.md](CACHE.md) and [CACHE_DIAGNOSTICS.md](CACHE_DIAGNOSTICS.md) - cache semantics and diagnostics.
- [PROMETHEUS.md](PROMETHEUS.md), [HTTP3.md](HTTP3.md), [STREAMS.md](STREAMS.md) - monitoring and transport features.
- [SECURITY.md](SECURITY.md) - current security model and limitations.
- [TEST_REPORT.md](TEST_REPORT.md) - release verification status and explicit environment limitations.
- [0.24.5 specification audit](V0.24.5_AUDIT.md) - HTTP/3 empty-request-body normalization for HTTP/1.1 upstreams and DELETE regression matrix.
- [0.24.4 specification audit](V0.24.4_AUDIT.md) - process-local Linux THP mitigation and memory-retention verification matrix.
- [0.24.3 specification audit](V0.24.3_AUDIT.md) - memory-retention, H3 settings placement and dynamic diagnostics source-vs-spec matrix.
- [0.24.1 specification audit](V0.24.1_AUDIT.md) - current shared-statistics/runtime-metadata/Settings navigation source-vs-spec matrix.
- [0.24.0 specification audit](V0.24.0_AUDIT.md) - historical H2 graceful-shutdown/system-statistics source-vs-spec matrix.
- [0.23.3 specification audit](V0.23.3_AUDIT.md) - historical legacy WebSocket compatibility source-vs-spec matrix.
- [0.23.2 specification audit](V0.23.2_AUDIT.md) - adjustable live-chart history and HTTPS local-CLI/live-log source-vs-spec matrix.
- [0.23.1 specification audit](V0.23.1_AUDIT.md) - historical initial Cache/Transform live-chart source-vs-spec matrix.
- [0.23.0 specification audit](V0.23.0_AUDIT.md) - historical certificate API/live-logs/cache-UX source-vs-spec matrix.
- [Interactive live logs](LIVE_LOGS.md) - `rust-proxy --livelogs`, structured filters, detail view, bounded stream/gap semantics.
- [0.22.3 specification audit](V0.22.3_AUDIT.md) - historical h2c Upgrade/external-certificate audit.
- [0.22.2 specification audit](V0.22.2_AUDIT.md) - historical retry/H2/concurrency source-vs-spec matrix.
- [0.22.1 specification audit](V0.22.1_AUDIT.md) - historical UI/RFC8441/h2c/tunnel source-vs-spec matrix.
- [0.21.0 specification audit](V0.21.0_AUDIT.md) - historical 0.21 source-vs-spec matrix.
## Historical or version-labelled context
These files are retained because they document migrations, original feature contracts or past audits, but their version labels and conclusions must not be treated as a fresh 0.24.1 audit:
| File | Historical marker | Use |
|---|---|---|
| `INTEGRATIONS_040.md` | 0.4.0 | Original integration/forwarding milestone. |
| `MANAGEMENT.md` | updated for 0.4.0 | Early vhost-management behavior and terminology. |
| `SECURITY_AUDIT.md` | 0.7.1 | Historical limited administration-exposure audit; not a current security certification. |
| `MIGRATION.md` | 0.7.3 -> 0.8.0 | Version-specific deployment migration notes. |
| `ACME_RFC2136.md` | 0.5.0 | Original RFC2136 integration notes; current renewal behavior is also documented in `RENEWAL.md`. |
| `TRANSPORT.md` | early transport contract | Background contract; current HTTP/3-specific behavior is in `HTTP3.md`. |
## Screenshots
See [screenshots/README.md](screenshots/README.md). Only two explicitly labelled legacy simulated references remain; stale simulated UI captures were removed in 0.19.1.
+66
View File
@@ -0,0 +1,66 @@
# Redirect hosts
RustProxy 0.13.0 adds first-class redirect hosts on the **Redirects** dashboard page. A redirect host owns one or more source hostnames and named HTTP/HTTPS listeners and answers locally without opening a backend connection.
Each host contains ordered redirect rules. The longest matching path wins. Rules can match `http`, `https` or either scheme and can use exact or path-prefix matching. Targets can be:
- `url`: an absolute `http://` or `https://` URL,
- `host`: a hostname/authority with the target scheme set to preserve, HTTP or HTTPS,
- `path`: an absolute path on the same request authority.
Supported status codes are 301, 302, 303, 307 and 308. Use 307/308 when the client must preserve the HTTP method. Path handling can append the complete source path, strip the matched prefix before appending, or replace the path entirely. Source query parameters can be preserved or dropped. `cache_seconds=0` emits `Cache-Control: no-store`; a positive value emits a bounded public `max-age`.
Redirect hosts use the same named listener inventory and certificate library as vhosts. The dashboard uses the same listener-selection component for both editors. A new redirect preselects the standard enabled HTTP :80 and HTTPS :443 listeners together when available, while existing bindings are preserved. The same hostname cannot overlap a vhost or another redirect host on the same socket. Global host ACL policy still runs before a redirect response. Redirect rules never make DNS/TCP/TLS connections to the target; the client follows the resulting `Location` header.
Configuration lives under `redirects` and normal dashboard saves use `PUT /api/config/redirects`.
## Example
```json
{
"redirects": [
{
"id": "legacy-site",
"name": "Legacy site redirects",
"frontend": {
"hosts": ["old.example.com"],
"listener_ids": ["http", "https"]
},
"certificate_id": "old-example",
"rules": [
{
"id": "application",
"source_scheme": "any",
"match_type": "prefix",
"path_prefix": "/app",
"target_type": "host",
"target": "app.example.com",
"target_scheme": "https",
"path_mode": "strip_prefix",
"query_mode": "preserve",
"status_code": 308,
"cache_seconds": 300
},
{
"id": "fallback",
"match_type": "prefix",
"path_prefix": "/",
"target_type": "url",
"target": "https://www.example.com/",
"path_mode": "replace",
"query_mode": "drop",
"status_code": 302,
"cache_seconds": 0
}
]
}
]
}
```
Rules are evaluated by longest path first; exact matching wins over prefix matching at the same path. A disabled redirect host reserves its hostname/listener ownership just like a disabled vhost, so another frontend cannot silently take over that traffic.
## Hostname editor
The Redirect source-domain field uses the same chip editor as Vhosts: add one hostname/wildcard at a time, remove individual chips, and keep listener selection in the shared frontend control.
+65
View File
@@ -0,0 +1,65 @@
# Implementation references
Primary specifications and library documentation consulted while implementing the
transport code. These references are not evidence that this source package was built
or tested successfully against a resolved dependency graph.
## HTTP and WebSocket
- HTTP semantics and intermediary header handling: https://www.rfc-editor.org/rfc/rfc9110
- HTTP/1.1 messaging: https://www.rfc-editor.org/rfc/rfc9112
- HTTP/2: https://www.rfc-editor.org/rfc/rfc9113
- WebSocket handshake and framing: https://www.rfc-editor.org/rfc/rfc6455
- Extended CONNECT bootstrap semantics: https://www.rfc-editor.org/rfc/rfc8441
- WebSocket over HTTP/3: https://www.rfc-editor.org/rfc/rfc9220
- hpx-h3 2.5.19: https://docs.rs/hpx-h3/2.5.19/h3/
- Hyper HTTP/2 server builder: https://docs.rs/hyper/latest/hyper/server/conn/http2/struct.Builder.html
- Hyper upgrades: https://docs.rs/hyper/latest/hyper/upgrade/index.html
- hyper-util legacy client: https://docs.rs/hyper-util/latest/hyper_util/client/legacy/struct.Client.html
- Resolver interface: https://docs.rs/hyper-util/latest/hyper_util/client/legacy/connect/dns/index.html
- Tokio bounded-buffer bidirectional copying: https://docs.rs/tokio/latest/tokio/io/fn.copy_bidirectional.html
## TLS
- HTTPS connector builder: https://docs.rs/hyper-rustls/latest/hyper_rustls/struct.HttpsConnectorBuilder.html
- rustls signing key/certificate validation: https://docs.rs/rustls/latest/rustls/sign/struct.CertifiedKey.html
- rustls server certificate resolver: https://docs.rs/rustls/latest/rustls/server/trait.ResolvesServerCert.html
- SNI certificate bindings: https://docs.rs/rustls/latest/rustls/server/struct.ResolvesServerCertUsingSni.html
- Development certificate generation: https://docs.rs/rcgen/0.13.2/rcgen/fn.generate_simple_self_signed.html
## Persistence and security
- SQLx SQLite connection model: https://docs.rs/sqlx/latest/sqlx/struct.SqliteConnection.html
- SQLite online backup API: https://www.sqlite.org/backup.html
- OWASP SSRF guidance: https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html
- OWASP password storage: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
- Prometheus instrumentation practices: https://prometheus.io/docs/practices/instrumentation/
The next Docker stages must use actual published port bindings and reconciliation,
not infer exposure from EXPOSE alone. Start with the official engine networking and
events documentation, and record the API versions actually tested in that release.
## Cache and compression
- HTTP caching semantics and Age/freshness: https://www.rfc-editor.org/rfc/rfc9111
- Cache-Status response field: https://www.rfc-editor.org/rfc/rfc9211
- tower-http streaming compression: https://docs.rs/tower-http/latest/tower_http/compression/
- Compression implementation API: https://docs.rs/crate/tower-http/latest/source/src/compression/service.rs
- Compression predicates: https://docs.rs/tower-http/latest/tower_http/compression/predicate/
References describe upstream APIs/specifications, not successful compilation of the
unresolved 0.2.0 dependency graph. Consult the exact Cargo.lock when generated.
## Vhosts, keepalive and negotiation
- Client pool timers, idle limits and HTTP/2 pings: https://docs.rs/hyper-util/latest/hyper_util/client/legacy/struct.Builder.html
- DNS Name parsing interface: https://docs.rs/hyper-util/latest/hyper_util/client/legacy/connect/dns/struct.Name.html
- TLS ALPN server configuration: https://docs.rs/rustls/latest/rustls/server/struct.ServerConfig.html
- HTTP strict transport security: https://www.rfc-editor.org/rfc/rfc6797
The library documentation is reference material, not proof that a particular future
resolved graph compiles. The reported tower-http 0.6.11 `SizeAbove::new(u16)` call
has been removed entirely; the custom predicate uses the generic Predicate API.
- Generic compression predicate API: https://docs.rs/tower-http/latest/tower_http/compression/trait.Predicate.html
+118
View File
@@ -0,0 +1,118 @@
# Automatic certificate renewal
## Scope and defaults
RustProxy 0.7.0 renews **configured managed certificates** whose `issuer` metadata
identifies an existing HTTP-01, Cloudflare, OVH or RFC2136 Certbot profile. Issuance
and initial library import are still explicit, staging-first operations. Importing
a PEM pair alone does not create a managed lineage or authorize CA requests.
For managed certificates, `issuer.auto_renew` defaults to `true`, including saved
0.6 metadata lacking that field. Explicit `false` opts out. Therefore upgrading
an already configured managed certificate can enable scheduled CA checks; review
this before starting the new binary. Missing profiles fail closed with an error;
there is no silent recreation, provider switch or production issuance.
Certificates shows an independent enable/disable action, Check now and Dry run,
last check, next check, expiry and result. Expiry is populated after a worker check;
it is not a pre-existing inventory of every manually imported certificate. Renewal
is not a configurable new ACME account lifecycle or revocation feature.
## Schedule and recovery
Saved renewal observations are loaded from SQLite **before administration listeners are
exposed**, so last check, next check, expiry and result are available immediately after a
restart without pressing **Check now**. RustProxy then waits 5-10 minutes before the first
automatic CA check, avoiding a renewal burst during startup. Successful checks are scheduled
approximately every 12 hours with up to 30 minutes of additional jitter. Only one issue/renew
worker runs at once. Failures back off from one to six hours; a missing/invalid profile is
checked again after one hour. Observation rows contain non-secret status/timestamps only and
are stored only in `config.sqlite3`. RustProxy 0.15.0 no longer reads or imports historical JSON
observation sidecars at runtime. When SQLite has no observation row, the normal scheduler safely
reconstructs status on its next automatic check; this does not replace or regenerate certificate,
key, account or renewal-profile material. A manual dry run does not postpone a real due check. The
service must be running; this is not an independent timer that works while RustProxy is stopped.
The helper runs `certbot renew --cert-name rustproxy` in the original isolated
lineage, reusing saved authenticator/account/credential settings. It does not use
`--force-renewal`, change the CA or run directory hooks. Do not add shell hooks
to the private Certbot renewal configuration: hooks already saved by an operator
are still part of Certbot's trusted configuration. Certbot decides when the
certificate needs renewal. A successful exit can mean nothing was due: actual
certificate and key bytes are compared with the currently installed generation.
For changed material, the helper verifies validity times using OpenSSL and creates
private immutable, content-addressed staging files. Rust validates the certificate/
key match and requested domain coverage, then validates a complete new snapshot
including current vhost assignments. Only a successful revisioned SQLite write
publishes that snapshot. New TCP and QUIC TLS handshakes use the new certificate;
existing connections are not restarted. A simultaneous manual replacement/removal
or automatic-renewal disable wins over an older automatic worker result.
Worker failure, invalid PEM/SANs, expiry regression or configuration conflict leaves
the previously active generation intact. If Certbot succeeded but publication did
not, the next check compares files again and can retry the same staged generation.
Unchanged checks do not write a new configuration revision. Changed certificate publications
use the ordinary revisioned configuration path, but certificate inventory changes are not part
of cache compatibility and therefore do not discard otherwise compatible hot/cold objects.
Private profile/file checks, single-worker and per-profile locks, bounded subprocess
timeouts and process-group cancellation constrain work. Original certificate files
and Certbot archives are retained, not automatically deleted. Immutable renewal
staging is limited to 1024 keys; initial issuance staging remains limited to 128.
Plan monitored disk space and controlled offline cleanup after verified backups;
there is no automatic certificate garbage collector or external expiry alert yet.
## Provider-specific prerequisites
HTTP-01 still requires public TCP port 80 to reach an active plaintext RustProxy
listener for every name. The reserved challenge handler exposes only bounded tokens
for the current worker's domains, before application auth/ACL/cache. It is not a
public file browser. Wildcard certificates use DNS-01, not HTTP-01.
Cloudflare, OVH and RFC2136 need their original DNS permissions, credentials and
propagation settings. Secrets are stored in private files, not returned by the
renew endpoint or included in desired configuration. They are not encrypted at
rest. The helper, Certbot, plugins and OpenSSL must be usable by the service user.
Keep absolute profile paths stable; moving the Certbot directory requires explicit
migration of its stored paths. A system-wide timer should not also manage these
RustProxy-isolated profiles.
For a certificate intentionally owned by a separate system Certbot instead of this managed
renewal subsystem, use a manual certificate id and a Certbot deploy hook that runs, for example:
```bash
/usr/bin/rust-proxy tls deploy --id shared-cert \
--cert /etc/letsencrypt/live/example/fullchain.pem \
--key /etc/letsencrypt/live/example/privkey.pem
```
The command resolves Certbot's `live/` symlinks locally, validates and stages a new pair, then
publishes the existing id through the normal revisioned configuration path. Do not point this
workflow at a certificate carrying RustProxy `issuer` metadata; one lineage must have one owner.
Staging certificates are not trusted by browsers. A Dry run checks renewal
validation but does not deploy the staging result. Only an explicit production
issuance creates a production profile; renewal never upgrades staging implicitly.
## API
All routes below require the existing local administrator session and CSRF checks.
- `GET /api/acme`: current job, provider names, nominal interval and renewal states.
- `POST /api/acme/renew`: `{"certificate_id":"site","dry_run":false}`; returns 202
and a job. Only identity/options, no API credential resubmission.
- Certificate opt-out is a revisioned config update setting
`tls.certificates[].issuer.auto_renew` to `false`.
`--dry-run` is **not** a claim of live CA validation in this release's test report:
packaging tests used a simulated Certbot. Run a real staging/production-lineage
validation on your server. Full runtime scheduler/publication acceptance is pending.
## Primary references
Certbot renewal behavior and flags:
https://eff-certbot.readthedocs.io/en/stable/using.html#renewing-certificates
https://eff-certbot.readthedocs.io/en/stable/man/certbot.html
Challenge types: https://letsencrypt.org/docs/challenge-types/
+207
View File
@@ -0,0 +1,207 @@
# Security model and limitations
This is a development source release with no executed Rust integration results in
the packaging environment. Do not expose it as a production security boundary until
the actual build, protocol tests, deployment review and an appropriate security
assessment have passed.
## Administration
Administration binds to loopback HTTP by default. Settings can enable a dedicated
administration HTTPS listener, choose the application-managed certificate or a
certificate from the inventory, persist a non-loopback bind and allow explicit DNS
Host/Origin names. Remote persisted binding is accepted only with administration TLS.
An explicit startup `--allow-remote-admin` can expose plain HTTP on a non-loopback address;
that path should remain behind a trusted firewall/private network boundary.
Passwords use salted Argon2id; password work has admission limits and runs off the
network I/O path. Setup requires local filesystem authority or the one-time random
capability. No default administrator/password is created.
Sessions have HttpOnly/SameSite=Strict cookies, idle and absolute expiries and
credential-generation revocation. Mutations require scheme-aware Host/Origin checks,
the request marker and a CSRF token. Local CLI reset changes the hash and invalidates
all existing sessions without editing SQLite manually. Administration HTTPS marks the
session cookie Secure; HTTP cannot. The admin certificate and bind changes are
restart-required. A dedicated metrics credential is separate from browser sessions.
The data directory and SQLite files are private. The local OS user with data-directory
access is a recovery authority and can create short-lived CLI sessions. Protect that
account, filesystem and backup copies; browser authentication does not defend against
a compromised process owner. `.env` is trusted shell code, never untrusted input.
## Backend and routing boundary
Only configured HTTP/HTTPS origins may be dialed. Absolute-form client targets and
CONNECT cannot turn the proxy into an unrestricted forward proxy. ACL is evaluated
before new TCP/TLS connections and before reuse of an existing upstream connection.
Host and route restrictions combine; route allow cannot override host deny.
Forwarded headers are rebuilt from socket identity by default. An explicit
trusted-proxy CIDR policy can accept a bounded existing chain and derive the client
IP for ACL/affinity; see INTEGRATIONS_040.md. Routing still uses the actual local
destination, never any forwarded address or host.
Private/loopback destinations require explicit approval. DNS answers pass the same
policy and optional per-backend CIDR restriction; an unsafe answer fails the entire
resolution. Validated results are used without a second DNS lookup. Special-address
blocks include common metadata/link-local targets and normalize mapped IPv6.
Listener self-target restrictions prevent simple loops and control-plane access by
default. A privileged administrator can deliberately set `accept_local_listener_risk=true`
on one backend; private/loopback destinations additionally require `allow_private=true`.
This is intended for consciously accepted local-listener use (including port 9090) and
can create a request loop or expose administration through a public vhost. It does not
relax metadata/link-local/special-address filtering or CIDR restrictions. Keep network
egress and frontend ACLs as defense in depth.
No backend `Location` redirect is followed internally. HTTPS transport encryption remains
available, but backend trust/hostname verification is off by default. Set `tls_verify: true`
for origins where certificate-chain and hostname authentication is required. With
verification disabled, a network attacker able to intercept that route may impersonate
the origin. Address/DNS restrictions still apply; the local-listener exception exists only
when explicitly accepted as above. Operator-supplied backend configuration is privileged
configuration, not a field that an unauthenticated caller can edit.
## TLS and tunnels
Private key files are size-bounded, regular files, owner-only on POSIX and opened
without following final-component symlinks. Named files cannot escape the certificate
directory by using path separators. Keep the entire directory tree private as well.
Keys and certificate name coverage are validated before publication. Generated
self-signed certificates are development material, not automatic public trust.
Existing TLS sessions/tunnels are not revoked by changing a certificate or ACL.
Policies apply to new requests and handshakes. Restart/stop the relevant process for
immediate termination until a scoped administrative close operation is implemented.
WebSocket payloads remain opaque byte streams. The proxy validates upgrade metadata,
not application-level authorization or message semantics. Frame/message limits on a
backend remain the backend's responsibility. Proxy buffers, tunnel/connection counts
and inactivity timers bound proxy-side resource usage.
## Information and resource controls
Access logging does not collect credential headers, query values, cookies, bodies
or WebSocket payloads. Hostnames and paths are logged; never put secrets in them. Do not enable third-party verbose tracing or application logging without
reviewing their output separately. Access rings, viewer slots, auth work, updates,
DNS workers and request/connection admission are bounded. A slow log viewer does not
create an unbounded delivery queue.
Backups include TLS private keys and credential/session state. Restore matching
SQLite and certificate data together, then revoke restored sessions. Never commit
`.env`, the data directory, backups or generated certificates to a public repository.
## Not a completed security feature
Docker socket access controls, service-account metrics access,
2FA, roles, provider secret storage, certificate
expiry automation, a full special-address registry policy and independent fuzz/load
assessment remain roadmap work. Feature availability is not evidence of a completed
security audit or a performance guarantee.
## Cache, response transforms and ports (0.1.0)
Cache is enabled by default in 0.8.1 and ACLs precede lookup. Cookie (outside the
configured static-asset exception), Authorization, Range, unsafe preconditions and
request/response `no-store` remain hard shared-cache exclusions. Conditional validators
can be satisfied from a fresh object or revalidated against the origin. Response
`no-cache` with a usable validator may be retained only for mandatory validation before reuse. Origin privacy is
checked before custom header removal, and generated request IDs are not cached. Bounded
fill permits, object/entry/byte limits and generation checks protect cancellation,
update and purge paths. Purge is authenticated, CSRF-protected and audited, not a
public PURGE/BAN capability.
Compression is opt-in, with authenticated/private/no-transform exclusions. Response
rules cannot overwrite protected framing and identity fields. Required Age remains
on cache hits even when debug headers are disabled. Server hiding is cosmetic and
must not be considered an access-control or vulnerability mitigation substitute.
Standard public 80/443 ports are supported without globally forbidding remote
origins using those ports. Local listener targets remain guarded. Review SETTINGS.md
for interface-enumeration and restart semantics. A Linux service can grant only
CAP_NET_BIND_SERVICE to a dedicated unprivileged account; the application does not
acquire capabilities itself. Public listener controls do not implicitly expose the separate admin
API. The provided systemd unit is an unexecuted example, not an audit result.
Language packs are trusted build inputs with size/type/metadata bounds. The browser
renders strings as text and falls back per key. Translations are not executable
plugins, and no directory browsing or runtime arbitrary file loading is exposed.
## 0.2.0 frontend and pool boundaries
An explicit vhost binding is checked using the accepted stream's real local IP,
port and TLS state. Host, SNI and forwarded headers cannot assert a different local
address. A shared wildcard TCP listener does not grant every vhost access on every
accepted IP. Legacy routing does not inherit additional frontend sockets. Conflicting
hostname ownership is rejected, including disabled declarations.
Binding a private IP is not an authorization mechanism or firewall. NAT, port
forwarding or another proxy can make that destination reachable. Configure network
restrictions and application authentication as well; no trusted-proxy identity or
Keepalived/VRRP service is introduced. New unavailable IP bindings fail startup.
Health probes share literal/DNS/allowlist/loop protections, including actual old
listeners and desired new listeners, and cannot follow redirects to arbitrary
origins. They are bounded and do not use user cookies or credentials. Probes can
still hit an operator-selected GET path; select a side-effect-free health endpoint.
Affinity uses opaque random tokens but is not authentication, CSRF protection or
session replication. Tokens survive compatible edits, not process restarts. Cookie
mode bypasses cache; the dedicated cookie is stripped before origin forwarding and
cannot be overridden by an origin Set-Cookie. No failed request, especially a write,
is replayed on another member to obtain a successful response.
Dedicated per-vhost security headers also cover matching-host denials/errors and
cache hits. HSTS is removed on plaintext; includeSubDomains/preload remain explicit
opt-ins. Preload directives do not enroll a domain in a browser list. Arbitrary CSP
and Permissions-Policy values are encoding-validated, not semantically audited.
Old configurations retain origin headers unless an explicit policy overrides them.
## Certificate uploads and custom pages (0.3.0)
Certificate upload is an authenticated local-control-plane mutation protected by
Origin and CSRF validation, body/per-file bounds and two-worker admission. PEM pairs
are parsed, key-matched and checked against supplied DNS names. Private immutable
files use generated simple names, mode 600 and the existing no-symlink/directory
lock protections. Public API responses/config/audit contain metadata, not key PEM.
JavaScript clears file controls after use, but does not claim secure erasure of
browser memory. Only configuration publication activates an inventory selection.
No new listener or public certificate-upload endpoint is created.
Custom HTML is fully trusted administrator-authored content served on the chosen
public vhost. It may contain executable HTML/scripts there; it is never previewed
or inserted as HTML in the admin DOM. Do not upload untrusted markup. The fixed
welcome page interpolates only the software version, not Host, URL or user input.
HTML is bounded to 32 KiB per page and by the overall 256 KiB configuration limit.
Responses use no-store; old representation encoding/length/cookies are discarded
when replacing an origin error. Authentication challenges, Allow and Retry-After
are retained, then administrator response/security rules are applied.
ACLs run before static/fallback/direct-IP pages. Even a custom status 200 for a
blocked request never authorizes an upstream fetch. Such overrides can confuse
clients and monitoring; retaining the original 401/403/5xx status is recommended.
Direct-IP HTTPS still needs a completed TLS handshake and a suitable certificate;
an HTTP welcome page cannot remove a TLS certificate-name error.
## Integration boundaries in 0.4.0
Tinyauth runs after host/path ACLs and before any cached/static/origin response or
WebSocket upgrade. Incoming identity headers are stripped, including after generic
header rules, and only configured fields from a successful verifier are copied.
The verifier gets no request body or admin session cookie. Its DNS/private-network
and TLS checks match the origin safety boundary. Authentication failure cannot be
turned into a 200 by custom pages. Denials and protected responses are no-store;
there is no auth-result or protected-content shared cache. Existing upgraded
sessions are not reauthenticated continuously.
Only configured login-origin redirects are accepted. Do not expose the fake-auth
helper or treat its test cookie as an actual login. H3 uses the same routing,
auth, request limits and response policies; 0-RTT is disabled. Transport resource
limits do not replace rate limiting, firewalls or independent security/conformance
testing. H3 is uncompiled/unexecuted in this packaging environment.
Diagnostic response fields are hidden by default, but origin secrets still need
explicit response rules. In 0.5.0 the dedicated controls expose request ID and
revision independently; the obsolete hide_mock_origin field is not accepted. X-Mock-Origin can be
removed using ordinary response-header rules. Scope labels on listeners never
enforce network restrictions. See ACME_RFC2136.md for private TSIG/account storage
and the separate backup requirement for manual DNS-01 issuance.
+78
View File
@@ -0,0 +1,78 @@
# Limited administration exposure audit - 0.7.1
## Scope and evidence
Reviewed the supplied 0.7.0 source and this patch's admin router/middleware,
authentication endpoints, sessions, API assets, dashboard login/logout, Swagger
integration, proxy header rule precedence and deployment readiness consumer.
This is a manual source review plus static/browser-script regression tests. It
is **not** a penetration test of the user's installed binary or a complete audit
of proxy/TLS/QUIC/ACME/operating-system/dependency security. No Rust compiler was
available; runtime enforcement and complete vendor rendering remain unverified.
The reported unauthenticated operational JSON disclosure was not reproduced on
an installed server. In the input source, `/api/config`, status, logs, metrics,
backend/cache and certificate endpoints already passed through a session guard.
The concrete findings were public version/mode metadata, unnecessarily public
operational HTML/JS/full language assets, broad path-only method exceptions and
incomplete browser-state minimization. Do not infer that a user's older build,
reverse-proxy front or a different exposed endpoint had the same behavior.
## Findings and source changes
| Observation | 0.7.1 change | Verification limit |
|---|---|---|
| `/healthz` disclosed version; auth discovery disclosed version/mode/password policy | Health reduced to one status field; auth discovery to setup-required boolean | Source/contract tests; actual router tests supplied, not run |
| Full operational HTML/modules/language bundle could be fetched anonymously | Root selects minimal login or authenticated document; full modules/bundle are private | Login fetch/DOM checks use mocked fetch, not real HTTP |
| Public route exemptions were not a strict method/path matrix | Explicit allowlist; private and unknown requests denied before JSON/query parsing | Six additional async Rust test functions supplied, unexecuted |
| Anonymous JSON rejection could include extractor input detail | Login/setup map parser failures to a fixed message, preserving 400/413/415 status semantics | Source and unexecuted router tests |
| Mutation marker/CSRF duplicate values were not explicitly rejected | Require one exact Origin, marker and session-bound CSRF value | Existing guards retained; added unexecuted duplicate-header tests |
| Stale DOM/form values can outlive a visible logout screen | Scrub inputs/state, notify other tabs, reload root; docs clear schema/results on session change | Chromium script tests with simulated location/storage |
| New API docs would otherwise create another disclosure surface | Protect document, schema and every necessary asset with existing guard; no wildcard asset route | Route/schema contracts; vendor-renderer acceptance outstanding |
| Interactive docs can carry credentials into requests | Fixed same-origin schema, per-action session verification, no external validator/persisted auth, writes opt-in + CSRF | Custom interceptor tests use a vendor stub; server guard is the authority |
| Updater used public version in `/healthz` | Locally verify systemd MainPID executable against staged release after minimal readiness | Temporary-file and simulated-systemd tests; real deployment not run |
## Deliberate public minimum
Anonymous login assets, favicon/shared CSS, setup-needed boolean, minimal health
status, and guarded login/one-time setup POSTs remain. These are necessary for
sign-in, initial administration and local readiness. Static CSS may contain UI
selectors; it contains no runtime configuration or secrets. Invalid Host/Origin
still returns generic 403 instead of bypassing the boundary.
Login/setup POST success is allowed to create/return session information **only
after credential/capability validation**. No API Bearer token or setup token was
added as an alternative authority for operational reads. Setup remains one-time,
local-capability-based; password checking/rate limiting/session invalidation were
not weakened or replaced by JavaScript-only checks.
## Separate Prometheus listener
Admin `/metrics` and `/api/metrics` require the browser/admin session. The optional
standalone Prometheus listener remains independent, disabled by default, with
configured source CIDRs and its existing bearer-token policy. Non-loopback export
requires a token by the existing Rust validation. A loopback exporter can be
intentionally configured without a browser login. Do not treat it as an overlooked
admin API exception, and do not place it on an untrusted network without reviewing
its token, CIDRs and protected transport. This patch does not verify its runtime.
## Outstanding acceptance and operational caveats
- Build and execute `dev.sh check` / `dev.sh test`; run `admin_audit.py` against
the actual upgraded service. Compare 401/no-store responses without cookies.
- Open real `/docs` in a normal browser; verify rendering, actual cookie+Origin+CSRF,
logout, CSP console output and failure after revocation. Vendor assets were not
downloaded/rendered here, and build dependency compatibility is unverified.
- Test real install/update restart/rollback; local MainPID and `/proc` verification
now fail closed when the expected executable cannot be established.
- Retain loopback-only administration and use a protected tunnel. Cookie/session
security does not protect a compromised local account, malicious browser extension,
an administrator sharing exported configuration, or every possible web attack.
- No RBAC/read-only administrator role, general API token scopes, dependency CVE
scan, fuzzing or proof of vulnerability absence is delivered by this small patch.
- Review a real resolved Cargo.lock and the pinned Swagger artifact before shipping
binaries. Source review is not a substitute for release acceptance.
For the exact exposure matrix and test/build commands, read [API_ACCESS.md](API_ACCESS.md).
The review uses OWASP REST/session guidance as a checklist, not a certification:
https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html
+346
View File
@@ -0,0 +1,346 @@
# Listener and response settings
## Configuration and update semantics
GET `/api/config` returns a revisioned envelope. PUT the entire document with
`expected_revision`. Defaults apply when fields are omitted. Unknown fields are
rejected; failed validation/persistence leaves the old snapshot active. Existing
backend/route/TLS/ACL data must be retained when changing settings. The panel does
this and preserves dirty settings forms across status/language refreshes.
```json
{
"listeners": {
"endpoints": [
{"id":"http", "name":"HTTP", "scope":"public", "enabled":true,
"binding":{"address":"0.0.0.0:80", "tls":false, "http2":false, "http3":null}},
{"id":"https", "name":"HTTPS", "scope":"public", "enabled":true,
"binding":{"address":"0.0.0.0:443", "tls":true, "http2":true,
"http2_settings":{"idle_timeout_seconds":300, "max_connection_lifetime_seconds":0, "drain_timeout_seconds":10}}}
]
},
"server_header": {"mode":"custom", "value":"RustProxy/v0.20.0"},
"compression": {"gzip":true, "br":true, "zstd":true, "deflate":false, "min_bytes":1024},
"headers": {
"request": [{"op":"set", "name":"x-proxy-name", "value":"RustProxy"}],
"response": [
{"op":"remove", "name":"x-powered-by"},
{"op":"set", "name":"x-served-by", "value":"RustProxy"}
]
}
}
```
This is a settings fragment, not a replacement for the existing routing/vhost document.
A complete opt-in example is in `examples/cache-dashboard.json`.
The dashboard exposes the complete raw JSON editor under **Settings -> Manual config**.
The historical `/configuration` browser route is retained only as a compatibility alias and
opens that Settings block; it is no longer a primary-navigation item. Prefer the structured
section editors for routine changes so unrelated sections are not republished.
## Administration HTTPS
`administration` is separate from public proxy listeners. It defaults to the startup
loopback address and plain HTTP. Persisted address/certificate changes are validated
immediately but take effect after process restart. A non-loopback persisted bind is
accepted only when `administration.tls` is true. Plain remote HTTP remains available only when process startup explicitly allows it with
`--allow-remote-admin`.
```json
{
"administration": {
"listen": "0.0.0.0:9090",
"tls": true,
"certificate_id": "admin-example",
"allowed_hosts": ["admin.example.com"],
"session_days": 30
}
}
```
`certificate_id: null` uses `data/certs/default.pem` and `default.key`, generated by
the application. A selected inventory certificate is loaded from the same private
certificate directory. `session_days` controls the absolute browser-session lifetime
and must be 7..30 (default 30). It applies without restart; changing listener/TLS/certificate
settings still requires restart. Session cookies are HttpOnly/SameSite=Strict and receive
the `Secure` attribute on HTTPS. When HTTPS is enabled, accidental plain HTTP received on the same administration
port receives a permanent redirect to the same authority/path over `https://`; the
plaintext path contains no administration handlers. An empty `allowed_hosts` accepts
valid DNS Host values on the configured HTTPS port as well as valid numeric destination
addresses. Once `allowed_hosts` contains entries it becomes a strict DNS allowlist for
browser-facing DNS authorities. An explicitly listed hostname may arrive from a trusted
reverse proxy on an external port such as 443 even when the private admin socket is 9090.
Direct IP/`localhost` access is still accepted only on the actual configured admin port.
For HTTPS termination in front of a plain local admin socket, the trusted proxy must pass
its external Host and scheme; `X-Forwarded-Host` is accepted only from loopback/configured
trusted proxy peers and `X-Forwarded-Proto` additionally follows the forwarded-proto trust
policy. Origin/CSRF comparison and the session cookie Secure flag use that external origin.
Use a firewall and normal network policy even with HTTPS.
## Structured access logging
The **Settings -> Logging** tab controls optional external sinks while the existing bounded
live ring remains available to the panel. Sink I/O is performed by a bounded background
queue; request processing only performs a non-blocking enqueue. Supported destinations:
- JSONL file append, either one combined file such as `/var/log/rustproxy/access.jsonl` or
one file per resolved vhost under a configurable directory (default `/var/log/rustproxy/vhosts`).
Unmatched traffic uses `_unmatched.jsonl`. The managed systemd service provisions
`/var/log/rustproxy` for the service account and keeps that directory writable under
`ProtectSystem=strict`. Other custom paths still need matching service permissions.
- Logstash TCP using `host:port` (IP or DNS). Each event is one JSON line.
- Loki HTTP(S) push. Optional bearer credentials are read from an environment variable
named by `bearer_token_env`; the secret itself is not stored in configuration.
`logging.format.fields` maps output field names to standard event fields; an empty map
emits the complete standard event. The panel provides Standard, Nginx-style, ECS-style
and Minimal presets plus a visual field selector/renamer with JSON preview. Advanced raw
mapping remains available for aliases not representable by the simple builder.
`static_fields` adds constant string fields. Loki labels are separately configurable.
`/api/status.logging.sinks` reports queue capacity, dropped events, successfully delivered
events and sink errors.
## Hot and cold cache tiers
`cache.max_bytes` and `cache.max_entries` bound the HOT RAM tier. `max_bytes` is charged from retained body backing allocation plus entry metadata rather than only payload length. `cache.transient_max_bytes` separately bounds in-progress fills and full bodies moving between HOT and COLD; `cache.cold_queue_max_bytes` additionally caps queued demotions in bytes, and `cache.hot_eviction_max_objects` bounds one LRU admission attempt. The source defaults are 256 MiB HOT, 16 MiB maximum object, 64 MiB transient and 32 MiB COLD queue. The Settings panel calculates conservative/recommended/performance presets from the lower of host RAM and a finite cgroup memory limit without changing the COLD disk budget. It also warns when HOT + transient is a large fraction of effective RAM, when the additional configured cache budget is large compared with current host/cgroup headroom, when the COLD queue exceeds transient RAM, or when the object ceiling cannot fit in transient RAM. Automatic presets are disabled below 256 MiB effective RAM. On a 2 GiB effective-memory VM the recommended profile selects approximately 400 MiB HOT, 96 MiB transient, 16 MiB maximum object, 48 MiB COLD queue, 6400 HOT entries and a 64-object LRU admission limit. The optional persistent
cold tier is controlled by `cold_enabled`, `cold_path`, `cold_max_bytes`,
`cold_max_entries`, `cold_gc_interval_seconds`, `cold_gc_max_delete`,
`cold_after_seconds`, `cold_on_lru`, `cold_on_idle` and `cold_on_shutdown`.
`cold_gc_interval_seconds` defaults to 60 and `cold_gc_max_delete` to 256. Pressure GC
starts at roughly 90% of the configured byte/entry budget and cleans toward roughly 80%
without deleting more than the configured per-pass cap. `cold_path` is relative to the RustProxy data directory and cannot
contain parent traversal or an absolute path. The two demotion switches independently
select LRU-pressure and idle objects; shutdown flush is a best-effort direct write during
a graceful restart. Demotions use a small bounded queue and release hot accounting before
disk persistence so cold storage does not retain an unnecessary second body buffer in RAM.
Cold metadata is readable JSON and bodies are raw `.body` files; reads are size/SHA-256
checked before promotion to RAM. Since 0.21.0 objects are sharded by the first two SHA-256
hex characters (`cache/cold/00/...` through `ff/...`), with shard directories created on
first write. The previous flat Cold Cache is intentionally not migrated. Purge operations
clear both in-memory indexes and schedule disk cleanup; selective cleanup uses the same
GC/eviction path and runs asynchronously.
Version 0.22 adds foreground fill collapsing (`collapse_enabled`,
`collapse_wait_timeout_ms`, `collapse_timeout_fallback`), detailed stale-if-error policy
(`stale_if_error_timeout`, `stale_if_error_connection`, `stale_if_error_dns`,
`stale_if_error_tls`, `stale_if_error_statuses`) and opt-in proactive refresh
(`proactive_refresh_enabled`, `proactive_refresh_before_expiry_seconds`,
`proactive_refresh_percent`, `proactive_refresh_max_inflight`). Defaults preserve the
previous stale-if-error behavior; proactive refresh is disabled by default. Selective
PURGE/BAN is an operational API/panel action rather than persisted configuration. See
[CACHE.md](CACHE.md) for request-collapse, Range and invalidation semantics.
## Static-response transformation
`transform` is a formal identity-response stage between the upstream response and shared
cache storage. `transform.enabled` is the global kill switch, but every vhost must also
set its own `transform.enabled=true`; vhosts stay disabled by default even when the global
engine is enabled. Global defaults enable JavaScript and CSS transformation and leave
HTML, SVG and JSON off. `extensions` controls extension-based detection and
`max_body_bytes` (1 KiB..32 MiB, 2 MiB default) bounds aggregation/decoding.
The pipeline is `origin -> decode supported Content-Encoding -> transform -> cache
identity representation -> client compression`. JavaScript/CSS can be enabled normally;
HTML minification, SVG optimization and JSON compaction are independent optional formats.
Recognition uses safe configured extensions and known Content-Type values. Binary/image
formats outside those explicit transform types are not transformed.
Origin `Cache-Control: no-transform` always disables transformation. When bytes change,
RustProxy removes representation-specific length/digest/encoding metadata and creates a
new strong `rustproxy-*` ETag for the transformed representation. The original origin
ETag/Last-Modified are kept separately and used for origin revalidation, so a proxy ETag
is never sent upstream as though it validated the original object. The transformed
identity body is what enters cache, preventing repeated minification on every HIT.
## Global TLS protocol and cipher policy
`tls.policy` is the global baseline inherited by every TLS host. It controls TLS 1.2,
TLS 1.3 and optional rustls cipher-suite names. An empty `cipher_suites` list uses the
ring provider defaults. Settings shows both the currently effective cipher suites and
the complete selectable suite inventory. At least one protocol version and
one compatible effective suite are required. This policy is also used by administration
HTTPS. HTTP/3 requires TLS 1.3 and a TLS 1.3 suite. Vhosts can narrow TCP TLS with
`tls_policy`; when a vhost has HTTP/3 enabled on a selected H3-capable listener it must
match the global policy because QUIC TLS is shared per UDP listener. Available suite names are exposed in authenticated status for
the Settings editor.
## Listeners: saved intent, not hot rebinding
When an HTTP, HTTPS or administration address was explicitly supplied by systemd/CLI
(or the corresponding service environment variable), that exact startup bind is a
process contract: the panel shows it as locked and configuration updates cannot remove,
disable or change it. If an address was not explicitly supplied at startup, listener
entries remain freely addable/removable/editable subject to normal validation.
`listeners.endpoints` is the only persisted frontend socket configuration. Each
endpoint has a stable id/name, an informational scope label, an enabled flag and one
primary binding containing IP:port, TLS, HTTP/2 plus its optional lifecycle settings, and optional HTTP/3 settings. Optional
`additional_addresses` reuse that same transport policy on up to 15 further addresses.
When more than one address is configured every address must be explicit; `0.0.0.0` and
`::` are rejected so a multi-IP selection cannot silently mean every interface. IP/port
and transport capability belong here, not inside a vhost. IPv6 uses `[address]:port`.
A vhost references endpoints through `frontend.listener_ids`. Listener changes are
restart-required because they alter TCP/UDP sockets or TLS ALPN capability. The
listener editor owns the HTTP/2/H2C and HTTP/3 enable/disable switches. An H2/H2C row
links to **HTTP connections -> HTTP/2** for idle, absolute-lifetime and graceful-drain
limits; an enabled H3 row links to **HTTP connections -> HTTP/3** for advertisement,
connection, stream and idle-timeout limits. Both tuning pages are listener-scoped and
do not own protocol enablement. Vhost H2/H3 switches only narrow the protocols already
enabled by the selected endpoints; they never create a protocol that Settings disabled.
The administration listener is independent and cannot share a frontend port.
Bindings must be valid local unicast/wildcard addresses; conflicting overlapping
endpoints are rejected rather than silently widened.
## Server and other client headers
`server_header.mode` is `preserve` (default), `hide`, or `custom`. Custom values are
nonempty, bounded to 256 bytes and validated against header injection. Preserve
means retain the result of origin/global/route rules; it does not add a missing
header. Hide/custom take final precedence, including proxy-generated errors.
Global and route `headers.response` apply to client responses; `headers.request`
apply only upstream. Each scope/direction supports up to 32 ordered set/append/remove
rules. Remove has no value. Global rules precede route rules. Multiple Set-Cookie
values remain distinct. Admin-session cookie names are never forwarded or set by
public proxy rules. Transport, framing and trusted forwarding/identity fields are
protected. Cache diagnostic fields are owned by the cache controls, not generic rules.
Global response rules also cover generated errors; route-specific rules apply only
after an origin route response exists. Client policy changes use hot configuration
publication and invalidate the RAM cache. In-flight exchanges retain their old
snapshot; no mixed-version configuration is applied midway through a response.
## Compression
All four flags default false. `min_bytes` defaults to 1024 (valid 32..1048576).
The library's default small-body/content-type predicate is also retained; actual
minimum for a known-size body is at least that default. Unknown-length text bodies
can be streamed without pre-buffering merely to discover their size.
Accept-Encoding quality negotiation is delegated to tower-http. Text, JSON,
JavaScript, XML, SVG and wasm are eligible; already encoded, SSE, range, upgrade,
HEAD/204/304 and privacy-sensitive responses are excluded. Cookies/authorization
bypass even when an origin marks a response public. Origin no-transform/privacy
exclusions cannot be defeated by deleting a response header in the editor. Cached
identity bodies can be streamed through compression on HIT; origin encodings are
passed through without recompression.
Vary: Accept-Encoding is added by compression; content length is removed for the
transformed body. Strong ETags are weakened, and identity-body content digests and
Accept-Ranges are removed. Encoders use their fastest preset; there is no panel
compression-quality slider in this release. Real binary interoperability tests
are still an acceptance requirement, not a result of this packaging session.
## Diagnostics and units
Samples run off the traffic path roughly every two seconds. CPU and network rates
need two successful samples. Host CPU is aggregate; process CPU uses 100% per busy
logical CPU and can exceed 100%. Neither is normalized to a container CPU quota.
Host memory is MemTotal minus MemAvailable. RSS/FDs/threads and process I/O come
from procfs. Network rates sum non-loopback interfaces in the process network
namespace, not just RustProxy traffic. Load average is OS scope, not latency.
The process's cgroup v2 memory files are read at the standard mount location when
available. Linux keeps the existing `/proc`/cgroup metric names, units and behavior.
FreeBSD/OpenBSD/NetBSD use native system interfaces where the same metric is available;
unknown or unsupported values remain null rather than being fabricated. Snapshot timestamps and sample age are shown;
cache counters and proxy request metrics are separate from OS resource metrics.
The dashboard history is browser-local and resets on reload/sign-out. No time-series
database, disk utilization chart or Windows/macOS OS sampler is implemented.
## HTTP/2 compatibility and per-vhost settings
HTTP/2 lifecycle settings are listener-scoped under `listeners.endpoints[].binding.http2_settings`:
- `idle_timeout_seconds` (default 300) retires only a truly unused H2 connection; active streams and current frame I/O postpone it.
- `max_connection_lifetime_seconds` (default 0) is an independent absolute lifetime; `0` disables this limit.
- `drain_timeout_seconds` (default 10) bounds how long accepted streams may finish after retirement begins.
Idle and lifetime retirement both send GOAWAY, stop accepting new streams, drain already accepted streams and then close TCP. RFC 8441 WebSocket/H2 Extended CONNECT streams are counted as active until their response body/tunnel ends. The same settings apply to TLS H2 and cleartext H2C on that named listener.
For backward compatibility, the top-level `http2` object is still accepted as the fallback for an H2/H2C listener that has no explicit `binding.http2_settings`. The structured UI writes explicit per-listener settings; operators should treat the top-level object and `PUT /api/config/http2` as legacy compatibility surfaces rather than the preferred configuration path.
A named TLS listener with `binding.http2: true` advertises H2 capability and always
retains HTTP/1.1 fallback. `binding.http2: false` makes that listener HTTP/1.1-only.
Cleartext H2 uses `binding.h2c: true`. Changing listener transport capability or H2 lifecycle settings requires restart.
A vhost can additionally set `http2_enabled: false`. RustProxy reads TLS ClientHello
and SNI before committing to ALPN, so a vhost that disables H2 gets HTTP/1.1-only
ALPN even while another hostname on the same listener keeps H2. `http3_enabled` is
the equivalent per-vhost H3 opt-out. The panel enables these switches only when a
selected listener provides the protocol. No failed application request is replayed
to downgrade.
Vhosts have their own backend pool, health/keepalive/affinity controls, ACL and
security/custom response headers. See VHOSTS.md. Global Server policy still takes
final precedence. There is no per-vhost compression algorithm override.
The reported `SizeAbove(u16)` compilation error is removed by a custom u64 predicate,
not a cast or panic-producing conversion. Compression thresholds retain the full
32..1048576-byte setting range. Known body length/Content-Length is compared exactly;
unknown streaming lengths are eligible without buffering the full body. Actual
compression execution still requires the compiler-enabled acceptance suite.
## Management in 0.3.0
Settings contains a direct Security shortcut (accounts, sessions and host ACLs)
and a vhost shortcut (SSL, headers, cache and pages). Compact is the default density;
Dense and Comfortable change spacing/control sizes without browser zoom or logo
redesign. The preference is local to the browser, not a global proxy setting.
Global Pages configures error overrides, direct-IP and unknown-host responses.
See MANAGEMENT.md for inheritance, allowed statuses and security implications.
Access logs have moved out of the dashboard into a separate modal/docked window.
Filters stay above the scrollable rows; counts and exports use the same selection.
## Named listeners, forwarding and diagnostics (0.4.0)
As of 0.8.0, the vhost editor no longer asks for a free-form listener IP. Configure
listener IP/port/protocol once in this Settings section, then select named endpoints in
the vhost. The pre-production inline/global listener format is no longer accepted. This separates
socket intent from host routing and avoids duplicating listening IP configuration across
vhosts.
`listeners.endpoints` is the only proxy-listener configuration. Each record has id,
name, scope, enabled and a binding with address, tls, http2, optional http2_settings, h2c and optional http3. Scope
is only a label. A vhost uses `frontend.listener_ids`; every reference must exist.
Conflicting protocols on shared sockets are rejected.
Changes to effective transport plans need restart, including H2 lifecycle and H3 limits. The UI
shows saved named entries and actual bound transports separately.
`forwarding` controls trusted CIDRs and forwarded-proto acceptance.
`diagnostic_headers` exposes only `request_id` (false initially). Vhosts can override the diagnostic
policy and Server policy; absent/null means inherit. Runtime/cache/static/error
responses pass through the same final header policy.
See INTEGRATIONS_040.md and HTTP3.md for complete JSON, limits and security details.
## 0.5.0 changes (override earlier baseline examples)
A named TLS listener with omitted http3 uses default H3 limits. Explicit null is
persisted as disabled. Overlapping named sockets must match protocols AND H3 limits;
the error identifies owners and remedies. A changed binding requires restart, including
UDP protocol/limit changes. Check UDP port availability before upgrading.
The diagnostic control is `request_id` (false by default). The removed hide_mock_origin field is not accepted; use normal response-header removal
for X-Mock-Origin. Existing custom Server values remain unchanged.
Cache decision/object inspection is under Cache, separate from Overview. Basic
manual certificate issuance is under Certificates; TSIG is never a configuration
field. See CACHE_DIAGNOSTICS.md and ACME_RFC2136.md for exact scopes and limits.
## 0.6 navigation and exporter
The Settings page now has eight switching cards: Listeners, Response, Cache,
Headers, Forwarding, Pages, Prometheus and Security. Unsaved values remain in
place while switching; switching alone never writes configuration. Security is
not a separate sidebar item. Use arrow keys/Home/End in the tab bar.
The Prometheus card controls enabled, bind IP/port, URL path, client CIDRs,
bearer-token environment variable name and optional collectors. Only the variable
name is exported; supply the actual secret to the service separately. See
PROMETHEUS.md for immediate revocation, restart rules and scrape configuration.
+61
View File
@@ -0,0 +1,61 @@
# TCP/UDP streams
RustProxy 0.13.0 exposes raw Layer-4 forwarding on the main **Streams** page, alongside **Vhosts** and **Redirects**. A stream combines one dedicated TCP or UDP listener with one upstream. Stream traffic does **not** enter the HTTP pipeline: no HTTP vhost routing, ACL, cache, headers, compression, Tinyauth or TLS termination is applied.
## Dashboard workflow
Use **Streams > New stream** or **Add TCP/UDP listener**. The modal mirrors the Vhost workflow and contains Details, Listener & upstream, and Limits tabs. Existing streams are shown as searchable cards with live runtime counters and RX/TX rate charts.
A saved listener change is desired state only until RustProxy restarts. The page keeps the active startup listener visible and marks changed/new/removed stream configuration as pending restart.
The HTTP/HTTPS listener editor remains under **Settings > Listeners**. Its bind addresses use the same chip-style interaction as vhost hostnames. Those named HTTP/HTTPS listeners are separate from stream listeners.
## Configuration document
```json
{
"streams": [
{
"id": "ssh",
"name": "SSH relay",
"enabled": true,
"protocol": "tcp",
"listen": "0.0.0.0:2222",
"upstream": "10.0.0.20:22",
"connect_timeout_seconds": 5,
"idle_timeout_seconds": 300,
"max_connections": 512
},
{
"id": "dns-udp",
"name": "DNS UDP",
"enabled": true,
"protocol": "udp",
"listen": "0.0.0.0:5353",
"upstream": "10.0.0.53:53",
"connect_timeout_seconds": 5,
"idle_timeout_seconds": 30,
"max_connections": 2048
}
]
}
```
TCP uses a bidirectional byte relay with a connect timeout, inactivity timeout and connection limit. UDP creates a bounded per-client session mapped to one connected upstream socket, expires it after inactivity and limits concurrent sessions.
## Runtime telemetry
The administration status snapshot exposes fixed-cardinality metrics for active startup streams. The Streams page displays:
- active TCP connections or UDP client sessions;
- accepted connections/sessions and completed sessions;
- capacity rejects and relay/upstream errors;
- bytes received from clients and returned from upstreams;
- UDP packet counters;
- live RX/TX rates derived from monotonic backend byte counters.
The chart history exists only in the current browser panel session. Backend counters are process-lifetime counters and reset on process restart.
## Listener safety
RustProxy validates collisions before publishing configuration. TCP stream listeners may not overlap HTTP/TLS, administration or Prometheus TCP listeners. UDP stream listeners may not overlap HTTP/3 UDP listeners. TCP and UDP may intentionally use the same numeric port because they are different transports. Direct loops into another RustProxy listener of the same transport are rejected.
+52
View File
@@ -0,0 +1,52 @@
# Verification report - RustProxy 0.24.5
## Scope
RustProxy 0.24.5 is a protocol-framing and cache-memory maintenance release over 0.24.4. It keeps the 0.24.4 process-local THP mitigation, fixes the H3 -> H1 boundary for actually empty request bodies whose length is initially unknown, and hardens the existing HOT RAM -> COLD disk cache lifecycle without changing the COLD storage format.
## Source contracts reviewed
- `ReceiveBody` in `src/http3.rs` reports end-of-stream only after the H3 DATA side reaches EOF and trailers are resolved; absence of `Content-Length` alone is not treated as proof of an empty body.
- `normalize_h3_h1_unknown_body()` runs only for HTTP/3 requests selected for an HTTP/1.1 upstream and only when no declared/exact body length is already available.
- EOF replaces the request body with `Body::empty()`. Non-empty first frames are replayed through `ReplayBody` and the remaining request body stays streaming.
- HTTP/2 upstreams, non-H3 downstreams and declared `Content-Length` requests are not pre-read by this normalization.
- The change is generic to body-capable methods; there is no Proxmox-specific or DELETE-to-H2 fallback.
- Published HOT objects are bounded by `max_bytes` using body backing capacity plus directly owned metadata and a conservative HOT-index charge; logical payload bytes are reported separately.
- Fill buffers reserve a bounded transient budget before growth, enforce `max_object_bytes` per chunk, and make HOT LRU room before further cache-buffer allocation. Failure leaves the response streaming uncached.
- HOT -> COLD demotion moves the `Entry` rather than cloning the payload. The `Bytes` allocation remains charged to transient RAM while queued/writing and disappears from the persisted `ColdEntry` after the write completes.
- The COLD writer queue is bounded by both object count and bytes; full COLD reads/promotions/revalidation reads reserve transient RAM before loading a body.
- Global `cache.enabled=false` is authoritative over vhost overrides. HOT/object values no longer have the old artificial 1 GiB configuration ceiling.
- Settings exposes conservative/recommended/performance presets derived from effective host/cgroup memory, plus non-blocking high-budget/headroom and inconsistent transient/object/queue warnings. A 2 GiB effective-memory VM gets approximately 400 MiB HOT / 96 MiB transient / 16 MiB max object / 48 MiB COLD queue under the recommended profile.
- Release identity is 0.24.5 across Cargo/package/UI/OpenAPI/release helpers. The existing dependency graph is unchanged; only the root `rust-proxy` package version changes.
## Verification status in this workspace
| Check | Status |
|---|---|
| `python3 scripts/preflight.py` | PASS |
| `python3 scripts/langs_check.py` | PASS |
| Browser JavaScript `node --check` | PASS |
| Shell `bash -n` | PASS |
| Python syntax / bytecode compile | PASS |
| Docker agent `go test ./...` / `go vet ./...` / `go build ./...` | PASS |
| Existing `Cargo.lock` dependency graph | PRESERVED; root package version updated to 0.24.5 |
| `python3 scripts/check_source.py` | PASS |
| `python3 scripts/release_smoke.py` | PASS |
| `./dev.sh fmt` / `cargo fmt --all` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace |
| `./dev.sh check` / `cargo check --locked --all-targets` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace |
| `./dev.sh test` Rust phase | BLOCKED: Rust/Cargo toolchain unavailable in this workspace |
| H1/H2/H3 live method matrix | test-host acceptance gate |
| Proxmox DELETE H3 access-log verification | test-host acceptance gate |
| HOT 8 MiB sustained-load/RSS acceptance | test-host acceptance gate |
| HOT 2 GiB bounded-admission acceptance | test-host acceptance gate |
| 5 s HOT -> COLD demotion/RSS/transient drain | test-host acceptance gate |
| Oversized-object streaming without full cache buffering | source regression added; runtime acceptance gate remains |
| Concurrent COLD read/write under small transient budget | test-host acceptance gate |
## Protocol acceptance target
Run GET/POST/PUT/PATCH/DELETE over H1, H2 and H3. For DELETE, test both an empty body and a body-bearing request (use a mock upstream if the chosen Proxmox endpoint does not accept a DELETE body). The original H3 empty DELETE must no longer produce `501`; H1/H2 behavior must remain unchanged; Proxmox must receive `DELETE` and the same URI; and a raw HTTP/1.1 upstream must show non-chunked zero-length framing for the empty H3 case.
For cache acceptance, test a small 8 MiB HOT tier under sustained traffic, a deliberately large 2 GiB HOT tier, `cold_after_seconds=5`, a response above `max_object_bytes`, and concurrent COLD promotion/demotion under a small transient limit. Published `hot_allocated_bytes` must remain within the HOT limit; transient/queue usage must drain after COLD persistence; large responses must keep streaming after their cache candidate is rejected; and RSS must plateau rather than track total traffic volume.
See [V0.24.5_AUDIT.md](V0.24.5_AUDIT.md) for the source/spec matrix and remaining runtime gates.
+41
View File
@@ -0,0 +1,41 @@
# Static-response transformation
RustProxy 0.22.3 retains static transformation between origin decoding and shared-cache
storage. A transformed identity representation is cached once and downstream compression
is selected later for each client. Per-vhost and Docker-discovery transform opt-in remain
disabled by default even when the global engine is available.
## Administration dashboard
The main **Transform** workspace reads `GET /api/transform` and exposes in-memory runtime
statistics globally or per vhost:
- responses examined, transformed, skipped/fail-open and failed;
- transformed cache HITs (served without running the transformer again);
- original, transformed and saved bytes before client compression;
- average transform time;
- applied transforms by JavaScript/CSS/HTML/SVG/JSON;
- bounded skip/fail-open reason counters;
- a per-vhost effectiveness table.
Counters reset on process restart. They are diagnostic telemetry, not persisted cache
metadata. The Cache workspace separately marks stored transformed objects.
## Cache diagnostics
When `cache.expose_cache_status=true`, transformed cached representations are identified
with the RFC 9211 field plus an extension parameter:
```http
Cache-Status: RustProxy; hit; ttl=556488; detail="cache_hit"; transformed
```
The field remains `Cache-Status`, not `X-Cache-Status`. HTTP/2 and HTTP/3 normalize field
names to lowercase on the wire. Origin-provided `Cache-Status` and `X-Cache-Status` values
are removed before RustProxy emits its own diagnostics.
## Release status
The dashboard/telemetry does not change the existing v0.22.3 release gate. JavaScript and
CSS still require mature parser-backed minifiers, and the complete Rust compiler/runtime
acceptance gate must pass before release acceptance.
+211
View File
@@ -0,0 +1,211 @@
# Transport contract
This document describes the implementation contract of 0.22.3. It is not a record
of successful Rust execution; see TEST_REPORT.md for verification status.
## Protocol matrix
| Connection | Implemented mode |
|---|---|
| Client to cleartext listener | HTTP/1.1 plus opt-in HTTP/2 prior knowledge and HTTP/1.1 `Upgrade: h2c`; WebSocket Upgrade on H1 and RFC 8441 Extended CONNECT on H2 when enabled per vhost |
| Client to TLS listener | HTTP/1.1 or HTTP/2 through ALPN; WebSocket uses H1 Upgrade by default or opt-in RFC 8441 Extended CONNECT on H2 |
| Client to HTTP/3 TLS listener | HTTP/3 over QUIC; RFC 9220 WebSocket Extended CONNECT only when a bound vhost explicitly opts in |
| Proxy to HTTP backend, `http1` | HTTP/1.1 |
| Proxy to HTTPS backend, `http1` | TLS with HTTP/1.1 |
| Proxy to HTTP backend, `http2` | HTTP/2 prior knowledge; the backend must support it |
| Proxy to HTTPS backend, `http2` | Prefer HTTP/2 through ALPN, retain HTTP/1.1 |
| Proxy to HTTPS backend, `auto` | ALPN HTTP/2 or HTTP/1.1 |
| Proxy to HTTP backend, `auto` | HTTP/1.1; no automatic h2c upgrade |
| Proxy to a WebSocket backend | HTTP/1.1 Upgrade over HTTP or HTTPS |
Cleartext downstream HTTP/2 is an explicit listener opt-in. The same listener accepts both
the HTTP/2 prior-knowledge preface and a standards-oriented body-free HTTP/1.1 `Upgrade: h2c`
bootstrap. Upgrade requests must include `Connection: Upgrade, HTTP2-Settings`, exactly one
`Upgrade: h2c` and one valid base64url `HTTP2-Settings` field. RustProxy sends `101`, waits for
the client's normal HTTP/2 connection preface and initial SETTINGS frame, then reconstructs the
accepted HTTP/1.1 request as HTTP/2 stream 1 before handing later streams to Hyper's ordinary H2
state machine. Body-bearing or malformed upgrade requests are rejected before `101`. HTTP/1.1
remains available on the same cleartext listener.
Enabling WebSocket on a vhost keeps the HTTP/1.1 Upgrade path by default. `websocket_rfc8441`
and `websocket_rfc9220` are independent per-vhost opt-ins for HTTP/2 and HTTP/3 Extended
CONNECT with `:protocol=websocket`. HTTP/2 listeners advertise ENABLE_CONNECT_PROTOCOL when
serving H2; QUIC advertises it when required by the HTTP/3 transport. Routing still rejects
Extended CONNECT on vhosts that did not opt in. Ordinary CONNECT and other Extended CONNECT
protocols remain rejected because RustProxy is not an open forward proxy. RFC 8441 may bridge
to an HTTP/1.1 WebSocket backend or use a backend explicitly configured for HTTP/2.
HTTP/2 and `auto` upstream configuration require `preserve_host: false` in this
release. The target URI, pool key and HTTP/2 authority use the configured backend.
Use HTTP/1.1 for a backend that requires the original Host. The original hostname
is also available through the proxy-generated `X-Forwarded-Host` field.
For ordinary proxy traffic, an explicit `protocol=http2` backend is forwarded with an
HTTP/2 request version. Cleartext `http://` uses h2 prior knowledge. HTTPS keeps the
existing ALPN policy and offers HTTP/2 with HTTP/1.1 compatibility; RustProxy does not
replay a failed TLS request merely to downgrade protocols.
Balanced routes may perform bounded failover retry only for body-free `GET`, `HEAD` and
`OPTIONS`. Defaults allow one retry on connection failure, response-header timeout or
`502/503/504`, always selecting another currently healthy backend. Path-pinned routes,
WebSocket handshakes and write/body-bearing requests are never automatically replayed.
## DNS and upstream TLS
Only explicit HTTP/HTTPS backend origins are accepted; no credentials, path, query
or fragment are allowed. Client requests cannot choose an arbitrary upstream URL.
Each new DNS connection uses a guarded resolver. Every returned IPv4/IPv6 address
must pass the configured address policy, with IPv4-mapped IPv6 normalized for checks.
A mixed safe/unsafe response is rejected as a whole. Up to 32 answers and 16 OS
resolver workers are allowed. Validated answers go directly to the HTTP connector:
there is no second unvalidated DNS lookup before dialing. Existing pooled sockets
remain bound to their original peer until closed; they do not silently move when
DNS changes. New snapshots get new connection pools.
`allow_private` is explicit approval for eligible private/loopback addresses, not a
universal SSRF bypass. `address_allowlist` further restricts eligible addresses by
CIDR. Include both address families when a hostname legitimately resolves to both.
Link-local/metadata, multicast, unspecified and certain special ranges remain
blocked. Remote origins may use any valid backend port, including 9090. A target that
resolves to an actual/local RustProxy proxy, TLS or administration listener remains
blocked by default, including validated DNS answers. An administrator may intentionally
override only that self-target guard per backend with `accept_local_listener_risk: true`;
loopback/private targets still require `allow_private: true`. This opt-in can create a
proxy loop or expose the administration plane, so keep it disabled unless that route is
deliberate and separately access-controlled. For example, an intentional local backend
`http://127.0.0.1:9090` requires both `allow_private: true` and
`accept_local_listener_risk: true`; this only permits the connection and does not bypass
the administration listener's own Host/Origin/authentication policy. The special-range
policy is a basic safeguard, not a substitute for network egress controls or an independent
security audit.
HTTPS upstream transport remains encrypted, but certificate-chain/hostname verification
is **off by default**. Set `tls_verify: true` to authenticate the backend identity. An
optional `tls_ca_pem` adds up to 16 trust anchors within 64 KiB per backend when
verification is enabled. The configured `tls_server_name`, when present, determines TLS
identity while DNS/TCP still targets the configured URL. Upstream redirects are returned
to the client, not followed internally.
## Public certificates
`dev.sh` enables `127.0.0.1:8443` by default. Direct binary startup enables TLS only
when `serve --tls-listen ADDRESS` or the corresponding environment setting is used.
Changing listener addresses or enabling a new listener requires a restart.
`data/certs/default.pem` and `default.key` are created once when needed. Generated
certificates are self-signed development material; they do not provide public trust.
Explicit bindings use exact hostname names and already-present certificate/key files.
The API stores filenames and names, never private key bytes. CLI generate/import
refuses to overwrite named files. Input size, regular-file status, private-key
permissions, matching keys, chain parsing and name coverage are validated.
```bash
rust-proxy tls generate --name app --hosts app.localhost,alias.localhost
rust-proxy tls import --name app-v2 --cert fullchain.pem --key private.key
rust-proxy tls deploy --id shared-cert \
--cert /etc/letsencrypt/live/example/fullchain.pem \
--key /etc/letsencrypt/live/example/privkey.pem
rust-proxy tls reload
```
Select these files in the panel or `tls.certificates` configuration. Set
`tls.default_certificate` to a binding ID to change the fallback. Unknown/no SNI
receives that fallback, not a default HTTP route. Unknown Host still returns 404.
Wildcard/SAN coverage and certificate expiry/issuer/renewal status are exposed by the certificate inventory; certificate ownership remains explicit per certificate id.
`tls deploy` is intended for an external issuer such as a system Certbot. Certbot's
`live/*.pem` symlinks are resolved locally; the final files are bounded regular files and
the private key must not be group/world readable. The local CLI performs bounded file/permission checks and sends the PEM pair through the authenticated loopback deploy API. The running RustProxy process treats the request as a convergent upsert: it creates a missing manual `certificate_id`, rotates changed material through immutable generation-specific files, and returns a no-op for an identical RustProxy-owned generation. The same id may be assigned to many vhosts, which all switch together when the pair changes. RustProxy-managed ACME certificate ids are deliberately rejected by this command so one lineage never has two renewal owners.
`POST /api/certificates/reload` and `rust-proxy tls reload` re-read all currently configured
certificate/key files and publish a fresh public TLS snapshot without changing the desired
configuration or its revision. This supports an operator that replaces the already configured
files by another safe local mechanism. Validation and file reads happen on bounded control-plane
workers; invalid material leaves the previous snapshot active. New public TLS/H2 and HTTP/3
handshakes use the refreshed snapshot, while established sessions/tunnels are not renegotiated.
The administration TLS listener remains startup-fixed and reports that a service restart is
required when its selected certificate changed.
For an externally managed/manual certificate whose configured files were replaced in place, `POST /api/certificates/{certificate_id}/reload` provides the same revision-stable hot-publication contract while targeting that certificate ID. The TLS resolver is immutable, so RustProxy validates and republishes a complete snapshot atomically; this avoids partially updated SNI state. Managed ACME certificate IDs are not accepted by this manual reload path.
## WebSocket behavior
The default WebSocket path validates an HTTP/1.1 GET upgrade, version 13, one valid key,
connection upgrade tokens and a body-free handshake. RFC 8441 over HTTP/2 and RFC 9220 over
HTTP/3 must be enabled explicitly for the selected vhost and use Extended CONNECT rather
than H1-only Upgrade/Connection/Key headers. When RFC 8441/9220 is bridged to an HTTP/1.1
backend, RustProxy performs and validates the backend RFC 6455 handshake before exposing a
successful downstream tunnel. Direct HTTP/2 upstream RFC 8441 validates the 2xx response and
selected subprotocol. Protected handshake fields cannot be replaced by generic custom-header
rules. Unsupported/malformed upgrades fail; unsolicited upstream 101 responses become a
gateway error.
After an accepted handshake the proxy copies bytes in both directions using bounded
buffers and backpressure. It does not aggregate messages, interpret application
payloads, log frames or hardcode a particular WebSocket application. Fragmentation,
control frames and negotiated extensions remain the peers' responsibility.
The demo echo server separately caps messages/frames at 64 KiB.
Request and TCP-connection leases survive Hyper's upgrade and are held until the
tunnel closes. An independent idle timeout observes traffic in either direction.
Shutdown cancels tunnels; routing/ACL changes affect new handshakes, not existing tunnels.
The administration API and dashboard expose active tunnels with a stable exchange ID, client,
host, upstream, transport, duration and RX/TX counters. An administrator can cancel one tunnel;
the action and supplied reason are written to the audit log.
## HTTP/2 connection retirement
Downstream H2 idle retirement is activity-aware: RustProxy tracks transport I/O and accepted streams, rechecks that a connection is actually unused, then asks Hyper for graceful shutdown. Hyper sends GOAWAY and refuses newer streams while accepted streams continue. Each H2/H2C listener uses `binding.http2_settings.max_connection_lifetime_seconds` for the independent absolute lifetime (zero disables it) and `binding.http2_settings.drain_timeout_seconds` for the maximum drain before TCP is forcibly dropped. Older configurations without per-listener values inherit the legacy top-level `http2` settings. RFC 8441 Extended CONNECT/WebSocket streams remain active for this accounting until their tunnel body ends.
## Resource and timeout defaults
| Resource | Default / bound |
|---|---|
| Shared public TCP connections, HTTP + TLS | 512; configurable up to 4096 |
| Active request/body/tunnel exchanges | 256; configurable up to 4096 |
| Concurrent HTTP/2 streams per public connection | 64 |
| HTTP/2 header list / send buffer | 32 KiB / 64 KiB |
| HTTP/2 stream / connection receive window | 65,535 bytes / 1 MiB |
| HTTP/2 idle / max lifetime / drain | 300 s / disabled (0) / 10 s |
| Public HTTP/1 header timeout | 10 seconds |
| TLS accept / upstream connect including TLS | 10 seconds |
| DNS lookup caller timeout / OS workers | 5 seconds / 16 |
| Backend response-header deadline | 120 seconds, including upload until headers |
| HTTP body idle timeout | 60 seconds |
| WebSocket idle timeout / upgrade completion | 300 seconds / 10 seconds |
| Idle pooled upstream connections | 4 per backend origin, 30-second idle timeout |
| Configuration writers / log-viewer streams | 8 / 8 |
CLI serve flags expose connection/in-flight counts and HTTP body, WebSocket idle,
upstream and shutdown timeouts. OS DNS calls cannot be forcibly cancelled; their
worker permits remain occupied until completion. Runtime shutdown stops waiting
for blocking workers after an additional bounded timeout.
Request and response bodies stream without collection, including trailers. Body-bearing/write requests are not transparently replayed. Replay-safe body-free
GET/HEAD/OPTIONS may use the bounded backend retry policy described above. Bytes counted at the body layer are not
acknowledged network bytes. WebSocket counters measure raw frame bytes separately
from HTTP body counters and exclude TLS/TCP overhead.
## Vhost sockets and keepalive in 0.22.3
Public connections across global and vhost endpoints share the same connection
budget. Per-vhost routing uses the actual accepted local IP/port/TLS state, including
when an OS wildcard socket is shared. See VHOSTS.md for overlap rules and restrictions.
Changing sockets or their ALPN policy needs restart. Certificate refresh remains hot.
TLS `http2: true` advertises h2 followed by http/1.1; false advertises HTTP/1.1 only.
An HTTP/1.1-only client, including one without ALPN, can connect when h2 is enabled.
HTTPS origins configured `http2` also negotiate h2 or HTTP/1.1. This is one TLS
negotiation, not a failed-request retry or TLS-verification downgrade. Plaintext
h2c remains explicit and strict: when `h2c: true`, a cleartext listener accepts both HTTP/2
prior knowledge and validated HTTP/1.1 `Upgrade: h2c`. The Upgrade bootstrap is body-free,
requires `HTTP2-Settings`, and becomes HTTP/2 stream 1 after `101`. Optional downstream HTTP/3
remains a separate QUIC listener capability (see HTTP3.md).
Pool keepalive settings configure idle connection reuse, idle timeout/limit, TCP
keepalive and HTTP/2 active-connection pings. Health probes are separate no-reuse
HTTP/1.1 or raw TCP operations; their concurrency cap is 16. Existing active exchanges
are not killed by disabling reuse or draining a member. Config publication rebuilds
client pools while preserving compatible observations and existing exchange leases.
The full contract and bounds are in VHOSTS.md. Keepalived/VRRP is not involved.
+77
View File
@@ -0,0 +1,77 @@
# RustProxy 0.21.0 specification audit
## Status vocabulary
- **SOURCE-FIXED**: a concrete source defect found during this audit was changed and regression coverage was added. Rust execution is still pending in this workspace.
- **SOURCE-AUDITED**: the required implementation and source-level regression coverage are present; live/runtime acceptance is still pending where applicable.
- **DEFERRED**: intentionally moved out of the closed 0.21.0 feature scope.
- **RUNTIME OPEN**: source work is present, but the required compiler/live test cannot be completed in this workspace.
## Main TODO audit
| # | Requirement | Status | Audit result |
|---:|---|---|---|
| 1 | Transformed response stored in cache | SOURCE-FIXED | `Plan::restrict()` uses transformed `Content-Length`; transformed bytes are stored and reused. |
| 2 | Full request `max-stale` semantics | SOURCE-FIXED | Freshness and retention are separated; bounded/unbounded `max-stale` and revalidation flags have regressions. |
| 3 | Request `max-age=N` | SOURCE-FIXED | Only `max-age=0` forces validation unconditionally; other values are checked against cached age. |
| 4 | HEAD + transform + cache | SOURCE-FIXED / RUNTIME OPEN | Cacheable HEAD fills through internal GET; cache-disabled empty HEAD keeps origin representation headers. H1/H2/H3 live acceptance remains open. |
| 5 | Incomplete body / HEAD cache fill | SOURCE-FIXED | Dropped/error/trailer-incomplete fills are rejected and counted; a later complete fill can succeed. |
| 6 | Route cache tri-state | SOURCE-AUDITED | `Option<bool>` preserves inherit/true/false through model and UI; effective routing diagnostics are logged. |
| 7 | Parser-based JavaScript minifier | **DEFERRED** | 0.21.0 keeps the conservative scanner; a parser-backed replacement is deferred rather than changing the resolved dependency graph during release closure. |
| 8 | Parser-based CSS minifier | **DEFERRED** | 0.21.0 keeps the conservative scanner; a parser-backed replacement is deferred rather than changing the resolved dependency graph during release closure. |
| 9 | HTML minification | SOURCE-AUDITED | Optional/default-off conservative transform preserves whitespace and raw-text elements and removes only safe comments. |
| 10 | SVG optimization | SOURCE-AUDITED | Optional/default-off conservative comment cleanup preserves text/tspan whitespace and CDATA; it is not documented as a full optimizer. |
| 11 | JSON compacting | SOURCE-AUDITED | JSON is parsed and compact-serialized; malformed JSON fails open; transformed length/ETag/cache behavior is covered. |
| 12 | Formal transform pipeline | SOURCE-AUDITED | Proxy flow is origin -> decode -> transform -> canonical cache representation -> client encoding. |
| 13 | Content-Encoding | SOURCE-FIXED / RUNTIME OPEN | gzip/br/zstd decoding is implemented and transformed identity responses remove the `Accept-Encoding` Vary dimension only after canonicalization. Live all-codec acceptance remains open. |
| 14 | ETag / Last-Modified validators | SOURCE-FIXED | Origin validators are retained separately; client conditionals use the representation ETag; origin revalidation uses origin validators. |
| 15 | `Cache-Control: no-transform` | SOURCE-AUDITED | Origin policy is captured before response rules; final rules may add protection; cache storage remains allowed and origin representation/validators are retained. |
| 16 | `stale-if-error` | SOURCE-AUDITED | Separate metadata/window and fallback paths for origin errors/timeouts are present; revalidation flags block stale. |
| 17 | `stale-while-revalidate` | SOURCE-AUDITED | Immediate stale delivery, bounded background refresh and exact-variant single-flight are implemented. |
| 18 | `must-revalidate` | SOURCE-AUDITED | Storage remains allowed; stale serving through SWR/SIE/max-stale is blocked. |
| 19 | `proxy-revalidate` | SOURCE-AUDITED | Shared-cache stale behavior mirrors `must-revalidate`. |
| 20 | Cache metadata | SOURCE-AUDITED | TTL/stale windows/revalidation flags/origin validators persist in RAM and Cold Cache metadata. |
| 21 | Explicit cache object states | SOURCE-AUDITED | `Fresh`, `StaleButServable`, and `Expired` are explicit decisions rather than a single expiry comparison. |
| 22 | `s-maxage` precedence | SOURCE-AUDITED | Shared-cache parsing keeps `s-maxage` ahead of `max-age` and regression coverage exists. |
| 23 | `immutable` | SOURCE-FIXED | Duplicate/argument forms no longer invalidate Cache-Control; freshness is not extended and explicit client validation still works. |
| 24 | Cold Cache sharding | SOURCE-AUDITED | `00..ff` SHA-256 sharding, lazy directories, startup scan and no flat-cache migration are implemented. |
| 25 | Cold Cache GC | SOURCE-AUDITED | Async bounded GC uses configured interval, high/low watermarks and shared eviction accounting. |
| 26 | Cold read vs GC race | SOURCE-FIXED | Reads are pinned with entry generation; an old read guard cannot unpin a replacement using the same cache key. |
| 27 | Manual selective cleanup | SOURCE-AUDITED | Async cleanup reuses GC/eviction selection and reports removed objects/freed bytes. |
| 28 | Global transform settings | SOURCE-AUDITED | Global engine/type/extensions/body-limit settings exist. |
| 29 | Per-vhost transform opt-in | SOURCE-AUDITED | Per-vhost transform remains disabled by default even when the global engine is enabled. |
| 30 | Docker transform opt-in | SOURCE-AUDITED | Docker assignments have separate default-off transform configuration propagated to runtime vhosts. |
| 31 | Cache reasons | SOURCE-FIXED | `fill_incomplete`, `transform_length_mismatch`, `transform_error` and other bounded reasons are recorded. |
| 32 | Routing / transform diagnostics | SOURCE-FIXED | Access logs carry vhost, route, effective cache/transform state; the admin UI now has a dedicated Transform runtime dashboard, and transformed cache objects are marked in both Cache UI and `Cache-Status`. |
| 33 | Transform + cache integration | SOURCE-FIXED / RUNTIME OPEN | Source regression now exercises MISS -> transform -> store -> HIT for JS/CSS/HTML/SVG/JSON with ETag/length/body checks. Rust execution remains open. |
| 34 | Transform + compression | SOURCE-AUDITED / RUNTIME OPEN | Canonical identity cache representation and codec negotiation exist; live identity/gzip/br/zstd end-to-end acceptance remains open. |
| 35 | `Vary` | SOURCE-FIXED | Canonicalized identity drops only `Accept-Encoding`; custom Vary dimensions are retained and exact-variant SWR locking is preserved. |
| 36 | Query string | SOURCE-FIXED | `?v=1`, `?v=2` and `?F` remain distinct cache keys; `bypass_query` is a request policy and transform does not rewrite the key. |
| 37 | Conditional requests | SOURCE-FIXED / RUNTIME OPEN | Representation ETag vs origin validator behavior is covered at source-test level; live integration remains open. |
| 38 | Restart | SOURCE-FIXED / RUNTIME OPEN | Cold restart regression preserves transformed ETag, origin validators, stale metadata and `no-transform` original representation. |
| 39 | Rust compiler quality gate | **RUNTIME OPEN** | `cargo`, `rustc` and `rustfmt` are unavailable here; fmt/check/test/clippy/release build cannot be claimed. |
| 40 | Remaining tests | PARTIAL | Source/package/SQLite/upgrade/JS/JSON/OpenAPI/shell and Docker-agent gates can run here; Rust-backed suites remain open. |
| 41 | Packaging | SOURCE-AUDITED / RUNTIME OPEN | 0.21.0 source packaging is deterministic and the feature scope is closed; compiler/live release-host verification remains open. |
## Passive Backend Health addendum
The later Passive Backend Health specification is implemented as an opt-in policy:
- ordinary request failures remain observable when passive health is disabled;
- `passive_health.enabled` defaults to `false` for normal and Docker-generated vhosts;
- active TCP/HTTP health remains independent;
- connect/timeout/I/O/HTTP-5xx/slow-response classes have individual switches;
- passive `fall`/`rise` can override active thresholds;
- active and passive counters/state are separate;
- current unhealthy source/reason hands off correctly when one health domain recovers while the other remains unhealthy;
- Docker assignments persist and reapply the policy across discovery rebuilds;
- OpenAPI documents passive health for both Docker assignments and ordinary vhost `load_balancing`;
- Prometheus keeps unconditional observation counters and exposes passive-health aliases without making observation itself a health decision.
## Deferred follow-up and release-host verification
- Parser-backed JavaScript and CSS minifiers are deferred to a later development cycle; 0.21.0 intentionally retains the conservative implementations and the existing resolved dependency graph.
- The release host must run the complete Rust 1.88+ gate: `cargo fmt --check`, `cargo check --all-targets`, `cargo test --all-targets`, `cargo clippy --all-targets -- -D warnings`, and `cargo build --release`.
- The compiled binary must pass the required live H1/H2/H3/cache/compression/restart acceptance tests before deployment.
The 0.21.0 source is feature-complete and packaged as release source; runtime acceptance remains explicitly external to this toolchain-less workspace.
+106
View File
@@ -0,0 +1,106 @@
# RustProxy 0.22.0 specification audit
This audit maps the 0.22 cache work directly to the requested scope. The implementation status below is based on the current source tree. Rust unit/integration tests are present, but this packaging workspace has no `cargo`, `rustc` or `rustfmt`; compiler/runtime acceptance therefore remains open until the release-host gate runs.
## P0 - request collapsing / cache stampede protection
| Requirement | Status | Implementation |
|---|---|---|
| Per-cache-key lock / single-flight | IMPLEMENTED | `src/cache/collapse.rs` tracks one foreground fill flight per primary cache key. |
| First request fills, followers wait | IMPLEMENTED | Leader owns `FillLeaderGuard`; followers await the retained completion state. |
| Wait timeout | IMPLEMENTED | `cache.collapse_wait_timeout_ms`, validated to 10..60000 ms. |
| Controlled timeout fallback | IMPLEMENTED | `cache.collapse_timeout_fallback`: origin or stale-if-available. |
| Release on error/cancel | IMPLEMENTED | Guard `Drop` removes the flight and publishes completion even after purge cancellation. |
| SWR integration | IMPLEMENTED | Foreground collapsing is separate from and compatible with the existing background-revalidation dedupe. |
| Metrics | IMPLEMENTED | Collapsed fills, timeouts and current waiters are exposed in cache metrics. |
| Concurrent MISS/variant regressions | SOURCE-TESTED | Rust tests cover wait-until-publish, timeout/leader drop, cancelled-follower metric cleanup and retry after a different `Vary` representation wins the first fill. |
## P1 - Range / HTTP 206
| Requirement | Status | Implementation |
|---|---|---|
| `Range: bytes=...` | IMPLEMENTED | One byte range, including bounded, open-ended and suffix forms. |
| `206 Partial Content` | IMPLEMENTED | Local cache slicing returns 206 with exact body length. |
| `Content-Range` / `Accept-Ranges` | IMPLEMENTED | Emitted for satisfiable cached ranges; unsatisfied ranges return 416 with `bytes */len`. |
| `If-Range` | IMPLEMENTED | Strong ETag and HTTP-date handling; weak validators do not match. |
| One range without full response buffering | IMPLEMENTED | Cache HIT slices the already-complete cached object; origin Range responses remain streamed. |
| Multipart decision | DEFINED | Multipart/multiple/unsupported Range forms are passed through to origin and are not synthesized or cached as separate range objects. |
| Compression/transform policy | DEFINED | Origin Range path requests identity encoding and skips transformation/client compression; local range is cut from the canonical complete cached representation. |
| Cache HIT + Range without origin | IMPLEMENTED | Complete cached representation serves the requested single range directly. |
| Preserve validators/Age | IMPLEMENTED | Cached range response retains cache validators and cache Age; only range framing/length/status is rewritten. |
| Revalidation + Range | IMPLEMENTED | A stale complete object revalidated by origin 304 is then evaluated again for 206/416/If-Range behavior. |
| Range regressions | SOURCE-TESTED | Rust tests cover 206, suffix/open-ended ranges, 416, validators, `If-Range`, duplicate fields and multipart rejection. |
## P2 - extended stale-if-error policy
| Requirement | Status | Implementation |
|---|---|---|
| Timeout / connection / DNS / TLS classes | IMPLEMENTED | Independent booleans select eligible transport failure classes. |
| Optional HTTP status list | IMPLEMENTED | `stale_if_error_statuses`, max 32 unique 5xx values; default `500,502,503,504`. |
| Backward-compatible default | IMPLEMENTED | Default transport classes remain enabled and the previous 5xx set is preserved. |
| Regression coverage | SOURCE-TESTED | Policy test covers class/status inclusion and exclusion; existing RFC 5861 window tests remain. |
## P2 - proactive cache refresh
| Requirement | Status | Implementation |
|---|---|---|
| Threshold before expiry | IMPLEMENTED | `proactive_refresh_before_expiry_seconds`. |
| Limit fraction without traffic | IMPLEMENTED | Deterministic `proactive_refresh_percent` sampling, default disabled. |
| Reuse existing revalidation | IMPLEMENTED | Reuses validator-based `refresh_cache_once` and background-revalidation guard. |
| Avalanche protection | IMPLEMENTED | Per-key schedule dedupe plus `proactive_refresh_max_inflight`; a just-filled object can schedule refresh without another client request. |
| Regression coverage | SOURCE-TESTED | Guard test covers schedule dedupe and global concurrency limit. |
## P1 - selective Cache PURGE / BAN
### PURGE
| Requirement | Status | Implementation |
|---|---|---|
| Exact URL | IMPLEMENTED | Exact path/query or absolute URL selector. |
| Host + URL | IMPLEMENTED | Optional host/authority filter combines with exact/prefix/pattern selection. |
| URL prefix | IMPLEMENTED | Prefix matches the actual request target, including query text. |
| Cache key | IMPLEMENTED | Exact 64-hex primary cache key selector. |
| Cache tag | IMPLEMENTED | `Cache-Tag` values are parsed/stored and can be purged by tag. |
| Regex/pattern | IMPLEMENTED | Rust `regex` engine with 512-character input bound and 1 MiB compiled-size bound; every regex purge/BAN requires explicit broad-operation confirmation. |
| RAM + persistent/cold | IMPLEMENTED | Matching hot and Cold Cache objects are removed; cold file deletion is queued. |
| Query + real cache identity | IMPLEMENTED | Stored target and primary cache key are both available to invalidation. |
| `Vary` variants | IMPLEMENTED | Matching one representation removes the whole primary-key variant family. |
| Pre-PURGE fill race | IMPLEMENTED | Selective PURGE advances the publication generation, so any fill started before the operation cannot publish afterward; matching single-flight waiters are also released. |
### BAN
| Requirement | Status | Implementation |
|---|---|---|
| Rule without immediate full scan | IMPLEMENTED | BAN creation only records a rule. |
| Lookup enforcement | IMPLEMENTED | Hot/cold lookup rejects objects whose creation predates a matching active BAN. |
| GC | IMPLEMENTED | Lifecycle maintenance removes banned objects incrementally with a bounded batch. |
| Remove one rule | IMPLEMENTED | Authenticated `DELETE /api/cache/bans/{id}` and Cache-panel action. |
| Rule count/TTL bound | IMPLEMENTED | Maximum 1024 active rules; TTL is bounded to 1..604800 seconds. |
| New objects after BAN | IMPLEMENTED | BAN intentionally targets representations that existed before the rule; a fresh post-BAN fill may be stored. |
### API, panel and observability
| Requirement | Status | Implementation |
|---|---|---|
| Dedicated admin endpoint | IMPLEMENTED | `POST /api/cache/invalidate`. |
| PURGE/BAN type | IMPLEMENTED | `operation: purge|ban`. |
| exact/prefix/tag/regex/cache_key | IMPLEMENTED | Typed selector in API/OpenAPI and UI. |
| Vhost selector | IMPLEMENTED | Optional validated `vhost`; optional host filter is separate. |
| Explicit broad warning | IMPLEMENTED | Regex and prefix `/` require `confirm_wide=true`; UI shows a danger confirmation. |
| Result data | IMPLEMENTED | Operation, selector, scope inputs, affected count, optional BAN id, elapsed milliseconds and status. |
| Audit log | IMPLEMENTED | Operator and requested operation/scope/value are written through the existing administrative audit store. |
| Metrics | IMPLEMENTED | PURGE counts/entries, BAN create/remove/matches/active count plus collapsing/proactive metrics. |
| `Cache -> Purge / Ban` | IMPLEMENTED | Dedicated controls and active-BAN table in the Cache panel. |
| Per-object `Purge URL` | IMPLEMENTED | Cache object table exposes a targeted purge action. |
## Release gate
The 0.22.0 source scope is implemented and covered by Rust-independent package checks plus source-level Rust regression definitions. The following gate remains mandatory before treating the build as runtime-accepted:
```bash
bash dev.sh doctor
bash dev.sh check
bash dev.sh test
```
That gate must run on a host with the Rust toolchain and must be followed by the project's live H1/H2/H3/cache/compression/restart acceptance checks.
+43
View File
@@ -0,0 +1,43 @@
# RustProxy 0.22.1 specification audit
## Scope
This audit covers the 0.22.1 delta over 0.22.0: Cache administration UI polish, HTTP/2 RFC 8441 WebSocket, cleartext HTTP/2 prior knowledge, and identifiable/killable active tunnels. The 0.22.0 cache audit remains historical evidence for the retained cache implementation.
## Requirement matrix
| Requirement | Source implementation | Regression / verification |
|---|---|---|
| PURGE/BAN form in a modal beside **Purge selected scope** | `src/admin/web/index.html`, `cache-panel.js` | JS syntax + source-release checks |
| Active BAN list in a separate modal | `src/admin/web/index.html`, `cache-panel.js` | JS syntax + source-release checks |
| BAN/PURGE messages cannot dereference a missing status node | null-safe `operationMessage(...)` | source-release marker + UI syntax checks |
| RFC 8441 `CONNECT` + `:protocol=websocket` | `proxy/websocket.rs`, `proxy/mod.rs` | `tests/http2.rs` + module tests |
| RFC 8441 per-vhost opt-in beside RFC 9220 | `vhost.rs`, `config.rs`, `vhosts.js` | config/unit source regressions |
| Listener advertises `SETTINGS_ENABLE_CONNECT_PROTOCOL` only where required | `config::Snapshot::websocket_rfc8441_on`, `server.rs` | source/config regression; Hyper builder contract |
| H2 client -> H1 upstream WebSocket | H2 request DATA <-> upgraded H1 byte bridge in `bridge_h2_to_h1` | source regression + compiler/live release-host gate |
| H2 client -> explicit H2 upstream | preserve H2 CONNECT/Protocol and meter request/response bodies | source regression + compiler/live release-host gate |
| WebSocket cancellation / idle timeout | `TunnelSession` cancellation + H2 idle watcher + existing H1/H3 timeout loops | source review; live release-host gate |
| h2c explicit enable/disable | `Listener.h2c`, Settings UI and validation | `transport_config` / vhost tests |
| h2c prior knowledge | HTTP/2 connection-preface detection in `server.rs` | server prefix unit test + live release-host gate |
| HTTP/1.1 `Upgrade: h2c` | deliberately deferred; no fake `101` path is shipped | docs + source-release guard |
| Stable active tunnel ID | request/exchange ID stored in `TunnelSession` | source review/API contract |
| List client/host/upstream/protocol/duration/RX/TX | `TunnelInfo`, `GET /api/tunnels`, modal | OpenAPI + source-release checks |
| Kill one tunnel | `POST /api/tunnels/{id}/kill` cancellation | OpenAPI + source-release checks |
| Audit close reason | `tunnel.kill` audit with operator reason | admin source regression |
| Main dashboard H1/H2/H3 tunnel stats | metrics + overview card | JS/source checks + Prometheus contract |
| Overview card geometry is shared across both visual rows | one `stats-grid` containing primary + WebSocket/H2/H3/TLS cards | source-release check |
| Full Cache Target / Cache key + tags remain retrievable when cells ellipsize | keyboard/click copy cells in `cache-panel.js`, full value in `title` | JS/source-release check |
## Protocol notes
RFC 8441 Extended CONNECT is connection-scoped at the HTTP/2 SETTINGS level. RustProxy therefore advertises the capability on a listener connection only when at least one bound vhost enables RFC 8441; request routing still enforces the selected vhost's own flag.
`h2c` in 0.22.1 means HTTP/2 prior knowledge. The legacy HTTP/1.1 `Upgrade: h2c` bootstrap is not accepted because a correct implementation must carry the initiating H1 request into HTTP/2 stream 1 after the `101` response. The current Hyper integration does not expose a safe bootstrap path for that operation, so the release does not claim partial support.
## Accept-Ranges policy
RustProxy does not globally inject `Accept-Ranges: bytes` into ordinary responses. It preserves the origin header where representation semantics remain unchanged and synthesizes `Accept-Ranges: bytes` for a cached single-range response handled by RustProxy. Transform/compression paths remove incompatible range advertising.
## Release gate
The packaging workspace has no Rust toolchain. JavaScript/JSON/language/source/package checks can be executed here, but `cargo check --locked`, `cargo test --locked`, Clippy, release build and live H2/h2c/RFC8441 interoperability remain mandatory on a Rust-enabled release host before deployment.
+43
View File
@@ -0,0 +1,43 @@
# RustProxy 0.22.2 specification audit
## Scope
This audit covers the 0.22.2 delta over 0.22.1: replay-safe backend failover retry, explicit HTTP/2 upstream request handling, bounded per-backend request concurrency, richer upstream diagnostics, an expanded backend operations view, and public X.509 expiry metadata. The retained 0.22.1 RFC 8441/h2c-listener/tunnel work and 0.22.0 cache work remain historical evidence for retained functionality.
## Requirement matrix
| Requirement | Source implementation | Regression / verification |
|---|---|---|
| Retry only replay-safe methods | `proxy/mod.rs::retry_safe_method` permits GET/HEAD/OPTIONS only | proxy source regression; compiler gate open here |
| Never replay body-bearing writes | retry requires an exact empty request body; POST/PUT/PATCH fail the method gate | source regression + release checks |
| Retry another backend | `Pool::select_retry_wait(..., excluded)` excludes attempted IDs | `failover_selection_never_reuses_an_attempted_backend` |
| Bounded retry policy | `RetryPolicy.max_retries` validates 0..3; bounded 5xx list | balance policy regressions |
| Explicit H2 upstream | explicit `protocol=http2` uses HTTP/2 request versioning | proxy regression + real Hyper h2c upstream test source |
| Per-backend request concurrency | `PoolMember.max_concurrent_requests`, atomic full-exchange lease acquisition | `backend_concurrency_limit_queues_then_rejects_and_recovers` |
| Bounded capacity queue / 503 | `queue_timeout_ms` validates 0..60000; async selection waits in bounded intervals and returns `SelectionFailure::Capacity` | capacity timeout regression |
| Reject vs queue-timeout observability | `capacity_rejections_total` always counts final capacity rejection; `queue_timeouts_total` only counts an elapsed non-zero queue | `zero_capacity_queue_timeout_rejects_without_counting_a_queue_timeout` |
| No false TCP connection claim | limit is documented and named as concurrent proxy requests/exchanges, not `max_connections` | TODO/roadmap explicitly keep socket-count limit open |
| Failure classification | DNS, TLS, connect, response-header timeout, read/send timeout and generic I/O flow through `Lease::error_detail` | source contract checks + detailed failure regression |
| Last failure | backend view exposes class, bounded detail and timestamp | `detailed_backend_failures_are_exposed_without_losing_aggregate_counters` |
| Health ejection/recovery timestamps | health transitions store last ejected/recovered timestamps | source contract checks |
| Backend operational view | dashboard combines status, active/limit, requests/errors, latency, traffic, last failure and last recovery | JS/language/source checks |
| X.509 validity metadata | public leaf PEM parser extracts SAN, issuer, notBefore/notAfter and days remaining without reading keys | X.509 time + generated-certificate regression sources |
| Expiry warning | inventory reports `ok`, `warning` (<=30 days), `expired`, or `unknown` | source/OpenAPI/UI checks |
| Renewal status in certificate view | inventory flattens current renewal status, last check and last failure message | OpenAPI/UI/source checks |
| Full downstream H2 -> RustProxy -> upstream H2 interoperability | implementation path present | **OPEN:** live/compiler-enabled E2E required |
## Capacity semantics
`max_concurrent_requests` is a full proxy-exchange permit. The permit remains held for the response body/tunnel lifetime through the existing backend lease, preventing a streaming response from releasing capacity after headers only. A value of `0` preserves the historical unlimited behavior. `queue_timeout_ms=0` rejects immediately when no eligible backend has capacity; a positive value performs a bounded wait and then returns HTTP 503.
Pool readiness remains a health/availability signal rather than a transient-capacity signal. Backup members can receive traffic when eligible primary members are saturated. Sticky affinity is honored only when the matched backend has capacity; otherwise normal eligible selection can move the request to another backend.
A physical TCP connection/socket cap is deliberately not claimed in 0.22.2 because Hyper connection pooling requires a distinct transport-level mechanism.
## Certificate safety
Certificate inventory parsing opens only the configured public certificate PEM. Private keys are not opened by the metadata path. The parser is deliberately narrow: it extracts the leaf issuer, RFC 5280 validity values and DNS SAN coverage while rustls remains responsible for certificate/key validation in serving paths.
## Release gate
This packaging workspace has no `cargo`, `rustc` or `rustfmt`, and outbound DNS is unavailable, so Rust parsing/type checking, compilation, linking and execution cannot be claimed here. Rust-independent JavaScript/JSON/language/source/package/Go gates are run locally. A Rust-enabled release host must still run `./dev.sh check`, `./dev.sh test`, release build and live H1/H2/H3/H2-to-H2 acceptance before deployment.
+49
View File
@@ -0,0 +1,49 @@
# RustProxy 0.22.3 specification audit
## Scope
This audit covers the 0.22.3 delta over 0.22.2: downstream HTTP/1.1 `Upgrade: h2c`, dedicated certificate hot reload, and deployment of certificate/key pairs owned by an external local issuer such as system Certbot. Upstream mTLS/client-certificate support is intentionally deferred.
## Requirement matrix
| Requirement | Source implementation | Regression / verification |
|---|---|---|
| Preserve h2c prior knowledge | cleartext listeners still sniff the HTTP/2 connection preface before entering HTTP/1 | existing prior-knowledge regression plus source/release checks |
| Accept HTTP/1.1 `Upgrade: h2c` only when enabled | H1 service checks the listener `h2c` capability before returning `101` | `malformed_h2c_upgrade_is_rejected_before_switching_protocols` and source checks |
| Validate the Upgrade request | requires HTTP/1.1, `Connection: Upgrade, HTTP2-Settings`, exactly one `Upgrade: h2c`, one bounded base64url settings payload, Host/authority, no request body | `h2c_upgrade_validation_rejects_missing_settings_and_bodies` |
| Reconstruct H1 request as H2 stream 1 | bounded HPACK bootstrap encodes pseudo/header fields and injects a stream-1 HEADERS frame after client preface + initial SETTINGS | `http1_h2c_upgrade_bootstraps_request_as_http2_stream_one` source regression |
| Keep later streams on normal H2 engine | upgraded I/O is handed to the same Hyper HTTP/2 server builder used by native H2 | source contract review; runtime gate open here |
| Bound malformed bootstrap traffic | initial preface/SETTINGS/header block have explicit size/shape checks; invalid input closes/rejects rather than entering routing | source checks + malformed request test source |
| Dedicated certificate reload API | authenticated `POST /api/certificates/reload` invokes `App::reload_certificates` | admin regression + OpenAPI/source checks |
| Reload does not mutate desired config/revision | recompile uses the existing revision and only swaps the immutable effective snapshot | `certificate_reload_keeps_configuration_revision_stable` |
| Failed reload preserves active snapshot | compilation completes before `ArcSwap` publication; an error returns without `store` | source review; release-host negative runtime acceptance required |
| Public H1/H2/H3 new handshakes use refreshed cert | public TCP TLS reads the current snapshot per ClientHello; QUIC config is rebuilt in the published snapshot | source review; live release-host acceptance required |
| Existing sessions are not interrupted | reload swaps configuration state only; it does not cancel public connection/tunnel tokens | source review |
| External Certbot/manual pair can be deployed | `tls deploy --id ... --cert ... --key ...` reads local PEMs and calls authenticated `POST /api/certificates/deploy`; the service stages a generation and publishes the existing manual certificate id | CLI/API/source checks + external pair staging regression |
| Certbot `live/*.pem` symlinks work | local CLI canonicalizes the supplied path once, then opens the resolved regular file with bounded reads | source checks; live Certbot-path acceptance recommended |
| Private key safety | external key is size-bounded, must be a regular file and on Unix may not grant group/world permissions; staged files use private creation semantics | source review + existing TLS file safety contracts |
| Pair/SAN validation before publication | `stage_external_pair` calls the existing certificate/key parser and extracts DNS SANs | `external_pair_stages_generation_and_rolls_back_cleanly` |
| Atomic multi-vhost switch | existing certificate id keeps all vhost assignments; only its generation filenames/coverage change in one revision-CAS config update | source/API review |
| No dual owner with built-in ACME | `tls deploy` rejects entries carrying RustProxy `issuer` metadata | source contract checks |
| Roll back staged files if publication fails | failed config/API publication invokes `remove_external_pair` for the staged generation | source review + helper regression |
| Administration TLS caveat is explicit | reload/deploy report that startup-fixed administration TLS needs restart when it selects the affected inventory certificate | API/CLI/UI/OpenAPI checks |
| Upstream mTLS | intentionally not part of 0.22.3 | deferred by product decision |
## h2c bootstrap semantics
The Upgrade path is enabled only on a cleartext listener that already opts into `h2c`. It does not make ordinary HTTP listeners opportunistically negotiate HTTP/2. A valid H1 request receives `101 Switching Protocols`; the client then sends the normal HTTP/2 connection preface and its initial non-ACK SETTINGS frame. RustProxy inserts a synthetic body-free HEADERS frame for stream 1 and lets Hyper own subsequent H2 framing, flow control, stream lifecycle and graceful shutdown.
The bootstrap deliberately rejects H1 request bodies. This avoids replaying or buffering a body across the protocol switch and keeps the transition bounded. Prior-knowledge h2c remains unchanged.
## External certificate ownership model
There are two supported external workflows:
1. **Generation deploy (recommended for Certbot):** a local deploy hook calls `tls deploy`. The CLI reads Certbot's current `live/` targets and sends the bounded PEM pair to authenticated loopback `POST /api/certificates/deploy`; the running service validates and creates a new immutable pair in its private certificate directory, then switches an existing manual certificate id through the normal revisioned configuration API. Every vhost already assigned to that id moves together.
2. **In-place file replacement:** an operator replaces files already referenced by the desired configuration through another safe local mechanism, then calls `POST /api/certificates/reload` or `tls reload`. The desired configuration and revision do not change.
RustProxy-managed ACME ids are excluded from external deploy so the same certificate lineage cannot have two renewal owners. The API does not accept arbitrary host filesystem paths from a remote request.
## Release gate
This packaging workspace has no `cargo`, `rustc` or `rustfmt`. Rust parsing/type checking, compilation, linking and Rust test execution cannot be claimed here. Rust-independent JavaScript/JSON/language/source/package/Go gates are run locally. A Rust-enabled release host must still run `bash dev.sh doctor`, `bash dev.sh check`, `bash dev.sh test`, a release build, the raw H1 -> h2c Upgrade -> H2 stream-1 test, native h2c prior-knowledge, TLS/H2/H3 certificate hot reload, invalid-certificate rollback and normal H1/H2/H3 acceptance before deployment.
+48
View File
@@ -0,0 +1,48 @@
# RustProxy 0.23.0 specification audit
## Scope
0.23.0 builds on 0.22.3 and adds three control-plane/operations changes: scoped external certificate automation credentials, interactive all-vhost live logs, and a reorganized cache settings UX. Upstream mTLS/client-certificate support remains intentionally deferred.
## External certificate automation
- The administrator can create/rotate/revoke one scoped certificate credential from **Settings → Certificate API**.
- The credential is represented by a non-secret Key ID plus a Bearer Token. Only the token hash is persisted in SQLite schema v5; the cleartext token is returned only by the rotate operation.
- Token authentication is accepted only for `POST /api/certificates/deploy` and `POST /api/certificates/reload`.
- Token authentication does not grant `/api/config`, logs, metrics, certificate inventory, ACME or other administration access.
- Non-loopback token requests require HTTPS. Loopback HTTP remains available for local Certbot hooks.
- Normal administrator session + CSRF authentication continues to work on deploy/reload.
- Direct production CLI operations are `rust-proxy tls deploy ...` and `rust-proxy tls reload`; `dev.sh` is not required.
- Rotation, revoke, deploy and reload operations are audited. Credential status exposes Key ID, creation time and last-used time but never the hash/token.
## Live logs
- `rust-proxy --livelogs` creates a bounded local CLI session and opens an authenticated SSE stream from the running daemon.
- Request handling publishes complete access-log records to a bounded `tokio::sync::broadcast` channel with no await/block in the proxy path.
- The CLI supports structured interactive filters without regex syntax: host, vhost, backend, route, client, method, transport, cache result, exact/status class, path prefix, errors-only and minimum duration.
- `pause`, `resume`, `reset`, `filters`, `clear`, `detail` and `show <id>` provide interactive control and full JSON inspection of recent displayed entries.
- The detailed view includes upstream errors/status, cache reason/effective state, transform effective state, WebSocket bootstrap/phase/error, H3 reset code and byte counters.
- A lagging receiver gets an explicit SSE `gap` event with the number of entries discarded by the bounded channel.
- Session validity is periodically rechecked by the stream endpoint; logout/password-generation changes terminate access.
## Cache settings UX
The existing cache settings IDs and one atomic `cache-form` submission remain unchanged. The UI only groups them into four persistent DOM sub-pages:
1. General
2. Resilience
3. Cold cache
4. Static & diagnostics
No cache setting was removed or split into an independently saved document.
## Production memory hardening
- Inbound HTTP/2 connections now have a five-minute I/O inactivity ceiling. This prevents idle TLS/H2 sessions from accumulating indefinitely toward the process-wide connection semaphore while preserving active streams and normal connection reuse.
- Runtime metrics expose `http2_active_connections`, and Prometheus exports `rust_proxy_http2_active_connections`; the overview HTTP/2 tile shows the live count.
- Hot-cache accounting uses the response body backing allocation capacity before `BytesMut::freeze()`, not only the visible body length, so geometric buffer slack cannot silently bypass `cache.max_bytes`.
- Existing cold-cache demotion/purge allocator trimming remains in place on glibc Linux.
## Open release-host gate
The source includes Rust regression definitions for certificate-token scoping/hash-only persistence and structured live-log filtering/SSE parsing. A compiler-enabled release host must still run the normal `dev.sh check`, `dev.sh test`, release build, live external-token certificate deploy/reload and sustained live-log load acceptance before production deployment.
+31
View File
@@ -0,0 +1,31 @@
# RustProxy 0.23.1 specification audit
## Scope
0.23.1 is a patch release on top of 0.23.0. It adds live operational charts to the existing Cache and Transform workspaces without changing proxy-path behavior, configuration schemas or administration API payloads. The 0.23.0 Certificate API, Bearer-token scoping, `--livelogs` and four-page Cache settings contracts remain unchanged.
## Cache live activity chart
- The Cache workspace contains one full-width `<details>` chart and leaves it collapsed by default.
- The chart uses the existing authenticated `/api/cache` refresh path and the selected vhost scope; it does not add a new endpoint or proxy-path query.
- HIT, MISS and BYPASS cumulative counters are converted in the browser to per-second rates from consecutive samples.
- The chart keeps at most 60 in-memory samples, reuses the dashboard SVG grid/line styling and resets history when the selected scope changes or the administration session is reset.
- The existing 3-second Live toggle remains the refresh cadence; disabling Live stops automatic chart updates together with the rest of the Cache workspace.
## Transform live activity chart
- The Transform workspace contains one matching full-width `<details>` chart and leaves it collapsed by default.
- The chart uses the existing authenticated `/api/transform` payload and selected vhost scope.
- Applied transformations, transformed cache HITs, skipped/fail-open responses and errors are converted from cumulative counters to per-second rates between samples.
- The chart keeps at most 60 in-memory samples, uses the existing dashboard chart visual language and resets history on scope/session changes.
- No transformation body, request body, cache object body or unbounded request-derived label is loaded into the chart.
## Retained 0.23.0 contracts
- Certificate API credentials remain Key ID + Bearer Token, hash-only at rest and restricted to deploy/reload.
- `rust-proxy --livelogs` remains a bounded structured all-vhost live stream with explicit lag reporting.
- Cache settings remain one atomic document split visually into General, Resilience, Cold cache and Static & diagnostics pages.
## Open release-host gate
Rust-independent source, language, JavaScript, packaging and release-smoke checks are part of this source delivery. A compiler-enabled release host must still run `./dev.sh check`, `./dev.sh test`, a release build and live browser acceptance with sustained Cache/Transform traffic before production deployment.
+27
View File
@@ -0,0 +1,27 @@
# RustProxy 0.23.2 specification audit
## Scope
0.23.2 is a patch release on top of 0.23.1. It extends the Cache/Transformation live charts with an operator-selectable bounded history window and fixes local CLI access when the administration listener uses HTTPS. No proxy-path, SQLite schema or administration API payload change is introduced.
## Cache / Transformation chart history
- Both live charts remain default-collapsed and full-width when expanded.
- Existing cumulative counters are still converted to per-second rates with the existing 3-second refresh.
- Each workspace offers 60, 120, 300 and 600 retained samples, corresponding to roughly 3, 6, 15 and 30 minutes.
- The selected limit is stored in browser local storage separately for Cache and Transformation.
- Reducing the limit trims the in-memory arrays immediately; increasing it grows only with future samples. The maximum remains bounded at 600 samples per series.
- Scope/session resets still clear the corresponding chart history.
## Local CLI / live-log HTTPS
- The root cause of the 0.23.0/0.23.1 `rust-proxy --livelogs` reconnect loop is the CLI's previous hard-coded `http://{admin}/api/live-logs` URI. A TLS-enabled administration socket intentionally returns HTTP 308 on plaintext requests, and the CLI treated that expected redirect as a stream failure.
- The CLI now probes the loopback `/healthz` endpoint before creating authenticated API requests. HTTP success selects `http`; the administration listener's 308 with an HTTPS `Location` selects `https`.
- The probe carries no CLI session cookie. After scheme selection, the live SSE stream and retained-history API use the same selected transport.
- HTTPS certificate verification is pinned to leaf certificates read from the private `<data-dir>/certs` directory. This supports the app-managed self-signed default certificate without disabling TLS verification globally or relying on public DNS/SAN routing to reach the loopback listener.
- The same local client is used by `config get/reload/apply` and `tls reload/deploy`, eliminating the same hard-coded-HTTP failure mode there.
- The local administration address remains required to be loopback.
## Verification status
Rust-independent source, JavaScript/JSON, shell, Go-agent and release-smoke checks are run in the packaging workspace. `cargo`, `rustc` and `rustfmt` are unavailable here, so compiler/type tests, a live TLS handshake against the built binary and sustained browser/live-log runtime acceptance remain release-host gates.
+25
View File
@@ -0,0 +1,25 @@
# RustProxy 0.23.3 specification audit
## Scope
0.23.3 is a compatibility patch on top of 0.23.2. It adds an opt-in, per-vhost legacy WebSocket mode intended for older appliance and embedded WebSocket servers. Cache/Transformation history controls and HTTPS-aware local CLI/live logs from 0.23.2 are retained unchanged. No SQLite schema change is introduced.
## Legacy WebSocket mode
- `websocket_legacy_support` defaults to false and is valid only when `websockets` is enabled.
- The vhost editor exposes **Enable WebSocket legacy support** and persists the field in the normal vhost document/API.
- The switch is compiled into the immutable per-vhost frontend snapshot; there is no global compatibility downgrade.
- For WebSocket exchanges only, legacy mode forces the upstream hop to classic HTTP/1.1, including when the normal backend protocol setting prefers HTTP/2. HTTP/2 or HTTP/3 clients are bridged to the HTTP/1.1 upstream path.
- Before the upstream handshake, the WebSocket upstream `Host` is forced to the configured backend authority and `Origin` is rewritten to `<upstream-scheme>://<backend-authority>`. This intentionally overrides Preserve Host for the legacy WebSocket handshake only, so old same-origin checks see the appliance address they expect.
- Legacy upstream `101 Switching Protocols` validation tolerates historical syntax differences: missing `Connection: Upgrade`, repeated `Upgrade: websocket`, transfer-encoding metadata and zero Content-Length. Non-zero Content-Length is still rejected.
- The `Sec-WebSocket-Accept` proof remains exact and any selected `Sec-WebSocket-Protocol` must still have been offered by the client.
- Legacy response framing headers are removed before the downstream response.
- Ordinary HTTP requests and WebSocket vhosts without the switch use the existing strict path unchanged.
## iLO4 evidence / intended acceptance
The supplied browser HAR shows repeated attempts to `wss://ilo.lab.danio298.com/wss/ircport` with a standard RFC 6455 request (`Upgrade: websocket`, `Connection: Upgrade`, `Origin: https://ilo.lab.danio298.com`) and no HTTP response (`status: 0`). That is consistent with an upstream handshake being closed or rejected before a browser-visible status is returned. The new mode specifically addresses the two proxy-sensitive compatibility points identified in source review: upstream Origin/Host mismatch and strict rejection of legacy `101` response framing. Live iLO4 acceptance remains a release-host/runtime gate.
## Verification status
Rust-independent source, JavaScript/JSON, language-pack, shell, Go-agent and release-smoke checks are run in the packaging workspace. `cargo`, `rustc` and `rustfmt` are unavailable here, so compiler/type tests, release binary build and live iLO4/legacy-WebSocket runtime acceptance remain release-host gates.
+40
View File
@@ -0,0 +1,40 @@
# RustProxy 0.24.0 specification audit
## Scope
0.24.0 changes downstream HTTP/2 connection retirement and extracts runtime system statistics into platform backends. Existing H1/H2/H3 routing, RFC 8441 WebSocket support, legacy WebSocket compatibility (including iLO4-class origins), cache and proxy behavior are retained unless stated below.
## HTTP/2 lifecycle
- `http2.idle_timeout_seconds` defaults to 300 seconds and remains independent from maximum lifetime.
- `http2.max_connection_lifetime_seconds` defaults to `0`, which disables the absolute lifetime limit.
- `http2.drain_timeout_seconds` defaults to 10 seconds and bounds graceful draining after shutdown begins.
- Idle qualification checks both recent H2 transport activity and the active stream count immediately before retirement.
- Idle or lifetime retirement calls Hyper graceful shutdown, which emits HTTP/2 GOAWAY, refuses newer streams and continues polling already accepted streams.
- Active response bodies, including RFC 8441 Extended CONNECT/WebSocket bodies, hold a stream lease until end-of-stream/error/drop.
- After accepted streams finish, the H2 connection future completes and the TCP transport is dropped normally. A drain timeout forces transport closure if a stream does not finish.
- Process shutdown uses the same GOAWAY/drain path; its drain is additionally capped by the process grace period.
Source regressions cover idle GOAWAY, active-stream idle suppression, lifetime GOAWAY plus forced drain timeout, and successful active-stream completion during lifetime drain.
## Settings organization
The former top-level **HTTP/2 connections** Settings tab is now **HTTP connections** with HTTP/2 and HTTP/3 sub-tabs. HTTP/2 lifecycle settings still save through the live `http2` section. HTTP/3 remains stored per TLS listener and requires restart, but its QUIC enablement, advertisement lifetime, connection/stream limits and idle timeout are edited from the HTTP/3 sub-tab. The Listeners editor retains bind/TLS/H2/h2c controls and preserves each listener's existing H3 configuration when unrelated listener fields are saved.
Legacy `/settings?tab=http2` links are normalized to `/settings?tab=connections`.
## System statistics/platform runtime
`system_stats` is the shared sampling module. Linux retains the existing `linux_procfs` source and the same names, units and `/proc`/cgroup semantics for CPU, process CPU, memory, RSS breakdown, FDs, threads, load, network, process I/O and cgroup memory.
FreeBSD, OpenBSD and NetBSD use the BSD backend and native OS mechanisms where available: `sysctl`, `getrusage`, `ps` and `netstat`. A metric that cannot be obtained with the same meaning is left `None`; values are not synthesized. Other Unix targets report the platform and leave unavailable system metrics unsupported/null.
The status payload now includes `platform`; Proxy Process displays it. Prometheus also exposes `rust_proxy_platform_info{platform="..."} 1`.
## Retained WebSocket compatibility contracts
The 0.23.3 legacy WebSocket mode remains opt-in per vhost. It continues to use HTTP/1.1 upstream, align Host and Origin with the configured backend, tolerate the documented legacy 101 framing, and keep Sec-WebSocket-Accept/subprotocol proof strict. This retained path is separate from RFC 8441 H2 WebSocket Extended CONNECT, whose accepted stream participates in the new H2 drain accounting.
## Release verification
Source-side Python/OpenAPI/language/JavaScript/shell/release-package checks are run in this workspace. The workspace does not provide `cargo`, `rustc` or `rustfmt`, so `cargo fmt --all`, `cargo check --locked --all-targets`, `cargo test --locked --all-targets`, release build acceptance, and native BSD compiler/runtime execution remain release-host gates. No passing Rust compiler result is claimed here.
+48
View File
@@ -0,0 +1,48 @@
# RustProxy 0.24.1 specification audit
## Scope
0.24.1 is a maintenance release over 0.24.0. It keeps the existing HTTP/2 graceful shutdown and HTTP/3 behavior, replaces duplicated BSD system-statistics collection with shared library APIs, restores network interface/RX/TX diagnostics through a dedicated cross-platform network API plus library fallback, exposes resolved runtime installation metadata to authenticated administrators, moves Logging into Settings, fixes compiler issues reported from a Rust-enabled release host, and removes artificial hostname-count ceilings from certificate/routing hostname lists.
## Shared system statistics API
| Requirement | Source implementation | Status |
|---|---|---|
| Avoid separate metric implementations per BSD family | `src/system_stats/mod.rs` collects portable host values through `systemstat`, network inventory/counters through `sysinfo::Networks` merged with a `systemstat` library supplement/fallback, and process values through `metrics-process`; the old `src/system_stats/bsd.rs` command/parser backend is removed | implemented in source |
| Linux compatibility | `src/system_stats/linux.rs::augment` keeps `/proc/stat`, `/proc/meminfo`, `/proc/self/status`, `/proc/self/io` and cgroup-v2 details after common sampling | implemented in source; compiler/runtime gate open |
| FreeBSD | `systemstat` host API + `sysinfo::Networks` + `metrics-process`; network inventory/counters are supplemented by `systemstat` when useful | source-integrated; native build/runtime gate open |
| OpenBSD | `systemstat` host API plus available `metrics-process` values; `sysinfo` network inventory may be unavailable so the shared adapter uses the merged `systemstat` source | source-integrated; native gate open |
| NetBSD | `systemstat` host API + `sysinfo::Networks` + shared `metrics-process` dummy fallback; unavailable process values remain null | source-integrated; native gate open |
| DragonFly BSD | `systemstat` host API plus shared `metrics-process` dummy fallback; network inventory merges `sysinfo` when available with the `systemstat` source | source-integrated; native gate open |
| macOS / Windows | same common host API and the process API on their supported targets | source-integrated; native gate open |
| Do not invent unsupported values | all portable fields are `Option`; failed/unsupported library calls leave values `None` | implemented in source |
The portable view now includes CPU, logical CPU count, RAM, swap, system uptime, load average, network totals/rates/per-interface values, process CPU, RSS, virtual memory, descriptor count/limit, process start time and thread count where the selected libraries provide them. Linux-only detailed RSS categories, process data/swap, process I/O and cgroup limits remain explicit additive fields rather than guessed equivalents on BSD.
The release-host `systemstat` compile failure in uptime conversion is fixed by using the duration's `as_secs()` API. The stale `std::future::Future` import in `src/server.rs` is also removed.
### Network inventory regression fix
The first 0.24.1 library adapter built the interface list only from `systemstat::network_stats()` successes. A backend error for a discovered interface therefore removed that interface entirely, which could yield `0 interfaces` and null aggregate RX/TX on both Linux and BSD installations. The corrected adapter uses `sysinfo::Networks::new_with_refreshed_list()` as the primary interface/counter API, supplements it with `systemstat`, merges counters by interface name, filters only loopback (`lo`, `loN`), and never shells out. This also preserves interfaces when either library returns only a partial inventory. The old 64-interface truncation is removed so discovered non-loopback interfaces are not silently hidden. Rate calculation remains based on deltas between the existing low-frequency snapshots.
## Hostname/certificate cardinality
RustProxy no longer imposes fixed item-count ceilings on certificate SNI hostname bindings, generated/imported certificate hostname checks, ACME domain requests, vhost/redirect/404 hostname lists, administration allowed hosts, host ACL hostname lists or Docker aliases. These lists remain constrained by DNS syntax and length rules plus the existing bounded control-plane inputs/global configuration size. Certificate PEM/key size limits, certificate-chain bounds and abandoned-upload storage bounds are deliberately retained as resource-safety controls. External ACME CAs/providers may enforce their own certificate/SAN quotas.
## Runtime installation information
`/api/status` now includes a `runtime` object with `platform`, `statistics_source`, `data_directory` and `database_path`. It is not added to the anonymous route allowlist; the existing administrator guard protects the status endpoint. These values describe installation/runtime state and contain no credentials or certificate/private-key contents.
The browser displays the returned values and performs no path/platform detection. For local CLI operations, an explicit `--data-dir` always wins. When using the default local path without a database, installed-data discovery checks platform conventions: `/var/lib/rustproxy` first on Linux/non-BSD and `/var/db/rustproxy` first on BSD, then the alternate convention.
## Settings navigation
The standalone Logging primary-navigation panel is removed. Its existing form is mounted as **Settings -> Logging**. `/logging` remains accepted as a legacy URL and is canonicalized to `/settings?tab=logging`; logging persistence and sink behavior are unchanged.
## Retained 0.24.0 transport behavior
The 0.24.0 H2 connection state/stream lease tracking, idle recheck, GOAWAY, bounded drain, maximum connection lifetime and accepted-stream completion path are unchanged by this patch. HTTP/3/QUIC remains under **Settings -> HTTP connections -> HTTP/3** and retains listener-scoped restart semantics.
## Open release gates
This workspace has no `cargo`, `rustc` or `rustfmt`. A Rust-enabled release-host attempt exposed the uptime API mismatch and unused import; both are fixed in source, but this workspace cannot rerun compilation to prove that no later compiler error remains. A release host must generate/preserve the resolver-produced lockfile and rerun `cargo fmt --all`, `cargo check --locked --all-targets`, `cargo test --locked --all-targets`, a release build, native BSD sampling tests and the final H2/H3/WebSocket/proxy-statistics regression before production deployment.
+56
View File
@@ -0,0 +1,56 @@
# RustProxy 0.24.3 specification audit
## Scope
0.24.3 is a focused runtime/UI maintenance release over 0.24.2. It reduces long-running Linux/glibc RSS retention in the transform-heavy path, keeps HTTP/2/H2C and HTTP/3 enablement in the listener editor while making both tuning pages listener-scoped, makes OS diagnostics render only values actually supplied by the current platform, expands external certificate automation, and improves overview/network observability.
## Memory-retention changes
| Requirement | Source implementation | Status |
|---|---|---|
| Bound glibc arena proliferation for managed systemd installs | `deploy/rustproxy.service`, `debian/rustproxy.service` and `scripts/deploy.py::service_unit` set `MALLOC_ARENA_MAX=2`; `/etc/rustproxy/environment` remains the administrator override | implemented in source |
| Avoid a duplicate identity transform buffer | `src/transform/mod.rs::decode` returns `Cow<[u8]>`; identity input is borrowed and compressed input owns only the decoded buffer | implemented in source |
| Reduce JS/CSS minifier peak allocations | `src/transform/js.rs` and `src/transform/css.rs` iterate with `Peekable<Chars>` rather than collecting `Vec<char>`, and return the completed `String` allocation with `into_bytes()` | implemented in source |
| Release temporary transform buffers earlier | successful changed transforms drop the decoded/collected origin buffers before final response construction | implemented in source |
| Give large glibc transform churn a bounded RSS-release opportunity | transformed responses that release at least 256 KiB can call `malloc_trim(0)` no more than once per 30 seconds on Linux GNU targets | implemented in source; sustained-RSS acceptance remains open |
| Keep non-glibc targets portable | allocator-specific tuning/trim code is guarded for Linux GNU; other targets keep the portable allocation path | implemented in source |
`MALLOC_ARENA_MAX=2` is an allocator-tuning default, not an application memory limit. It is useful for this multi-threaded long-running proxy because it limits how many glibc malloc arenas can retain freed pages. It does not replace cache limits, cgroup limits or sustained-load testing.
The cache already calls `malloc_trim(0)` after bulk demotion/purge paths. 0.24.3 does not change cache policy semantics: in particular `cold_on_age` remains an explicit operator choice and defaults to false. This release reduces allocator/transformation retention without silently changing cache freshness or tiering behavior.
## HTTP/2 and HTTP/3 settings placement
Both downstream protocol families now use the named-listener ownership model. `binding.http2` / `binding.h2c` enable HTTP/2, `binding.http2_settings` stores its lifecycle limits, and `binding.http3` is either `null` (disabled) or an object containing QUIC limits. In 0.24.3:
- **Settings -> Listeners** owns the H2, H2C and **Enable HTTP/3 over QUIC (UDP)** switches.
- An H2/H2C listener links to **Settings -> HTTP connections -> HTTP/2**; an enabled H3 listener links to the HTTP/3 page.
- The HTTP/2 page edits idle, absolute-lifetime and graceful-drain limits independently for every H2/H2C listener. The HTTP/3 page continues to edit advertisement, connection, bidirectional/unidirectional stream and idle-timeout limits per enabled TLS listener.
- The legacy top-level `http2` object remains a fallback when an older listener has no explicit `http2_settings`, so existing configurations keep their effective behavior until saved through the new UI.
- Listener/H2/H3 tuning changes remain restart-required. Each tuning page is blocked while the listener form has unsaved changes, preventing stale copies of the listener section from overwriting each other.
## Dynamic diagnostics
The dashboard card is named **Process**. Optional process/system fields are backed by nullable statistics. The browser hides rows and history charts when a field is unavailable on the current platform instead of substituting `unknown`, `N/A` or a synthetic zero. Linux continues to expose its `/proc`/cgroup details; other supported systems show only values supplied by the shared collectors.
The same rule is applied to optional system CPU/RAM/swap/load/uptime and network/latency chart rows where the data source can be absent. This is a presentation change only; the authenticated status API continues to represent unsupported metrics as `null`.
## Certificate automation extensions
| Requirement | Source implementation | Status |
|---|---|---|
| Reload one manual/file-backed certificate after an external file replacement | `POST /api/certificates/{certificate_id}/reload` calls `App::reload_certificate`; ACME-managed IDs are rejected and the configuration revision stays unchanged | implemented in source |
| Permit the same reload through Certificate API authorization | `certificate_api_operation` recognizes the validated per-ID reload path and preserves the external HTTPS/loopback restriction | implemented in source |
| Support multiple independently managed automation tokens | SQLite schema v6 introduces `certificate_api_credentials`, bounded to 32 rows, with Key ID, hash, comment and usage metadata | implemented in source |
| Individual and bulk revocation | `DELETE /api/certificate-api/{key_id}` revokes one; `DELETE /api/certificate-api` revokes all | implemented in source |
| Never persist cleartext automation tokens | PUT returns the clear token once; only SHA-256 hashes and non-secret metadata/comments are stored | implemented in source |
## Dashboard extensions
- **Core State** is replaced by a same-footprint live connection chart for open connections and active exchanges, with backend count retained as chart context. **Uptime** moves to **Process**, directly below process start.
- **Payload transferred** keeps the aggregate value and adds compact download (`bytes_out`) / upload (`bytes_in`) counters.
- Each network-interface chart has a **Hide** action. Hidden interface names are stored only in browser `localStorage` (`rp-hidden-network-interfaces`) and can be restored from the dashboard; no server configuration is changed.
## Open release gates
This workspace does not provide `cargo`, `rustc` or `rustfmt`. Static source, language-pack, JavaScript, shell, Python, deployment/archive and Go-agent gates can run here, but a Rust-enabled release host must still run formatting, `cargo check --locked --all-targets`, `cargo test --locked --all-targets`, a release build, live H2/H3/WebSocket regression and a sustained 10-12 hour cache/transform RSS comparison before production promotion.
+21
View File
@@ -0,0 +1,21 @@
# RustProxy 0.24.4 specification audit
## Scope
0.24.4 is a focused Linux memory-retention maintenance release over 0.24.3. Production diagnostics showed that a large share of anonymous RSS could be backed by 2 MiB Transparent Huge Pages while the live RustProxy cache residency was much smaller. A host-wide switch from THP `always` to `madvise` removed those huge-page mappings after restart. 0.24.4 internalizes that mitigation for RustProxy only.
## Source/spec matrix
| Requirement | Source contract | Status |
|---|---|---|
| Do not require a host-wide THP policy change | `src/main.rs` calls `prctl(PR_SET_THP_DISABLE, 1, 0, 0, 0)` on Linux | implemented in source |
| Apply the policy before runtime worker heaps are created | `disable_transparent_huge_pages()` runs before `tokio::runtime::Builder::new_multi_thread()` | implemented in source |
| Keep non-Linux platforms unchanged | the THP call is guarded by `cfg(target_os = "linux")`; other platforms use a no-op helper | implemented in source |
| Avoid turning kernel capability into a startup dependency | a failed `prctl` emits a warning and startup continues | implemented in source |
| Retain the 0.24.3 glibc mitigation | managed systemd units still set `MALLOC_ARENA_MAX=2`; existing bounded `malloc_trim(0)` paths remain | retained |
## Runtime acceptance
Source inspection cannot establish steady-state RSS. On a Linux release host, return `/sys/kernel/mm/transparent_hugepage/enabled` to the host's normal policy, restart the 0.24.4 binary, and verify `/proc/<pid>/status` reports `THP_enabled: 0`. Under representative sustained traffic, `/proc/<pid>/smaps_rollup` should keep `AnonHugePages` at zero or negligible levels without changing the global THP setting. Record RSS, anonymous RSS, cache resident/transient bytes and request rate for the same 10-12 hour acceptance window used by 0.24.3.
Compiler/type/runtime acceptance remains open in this source workspace because the Rust toolchain is unavailable.
+59
View File
@@ -0,0 +1,59 @@
# RustProxy 0.24.5 specification audit
## Scope
0.24.5 fixes HTTP/3 request-body framing when an H3 request has no declared `Content-Length`, carries no DATA, and is forwarded to an HTTP/1.1 upstream. Before the fix, the H3 receive body remains length-unknown until its stream end is polled; Hyper can therefore serialize body-capable HTTP/1.1 methods such as `DELETE` with `Transfer-Encoding: chunked`. Some upstream servers reject chunked request bodies even when the logical body is empty.
## Source/spec matrix
| Requirement | Source contract | Status |
|---|---|---|
| Preserve HTTP method and URI | normalization changes only the request body; method/URI rewriting remains in the existing proxy path | implemented in source |
| Detect an actually empty H3 body | for H3 -> H1 with no declared/exact length, `normalize_h3_h1_unknown_body()` polls until EOF or the first semantically significant frame | implemented in source |
| Represent empty H3 body as zero length | EOF replaces the streaming wrapper with `Body::empty()`, giving Hyper an exact zero-length size hint | implemented in source |
| Preserve non-empty bodies | the first non-empty DATA/trailer frame is wrapped in `ReplayBody`; the remaining tail stays streaming | implemented in source |
| Do not change H2 upstream framing | normalization exits before polling unless the selected upstream version is HTTP/1.1 | implemented in source |
| Do not preconsume declared bodies | requests carrying `Content-Length` are left unchanged | implemented in source |
| Avoid DELETE-only/Proxmox-only behavior | normalization applies generically to body-capable H3 methods forwarded to H1; GET/HEAD/CONNECT retain Hyper's existing implicit-empty behavior | implemented in source |
## Regression coverage added
`src/proxy/mod.rs` contains focused async tests for:
- empty length-unknown H3 `POST`, `PUT`, `PATCH` and `DELETE` to HTTP/1.1 becoming exact zero-length bodies;
- non-empty length-unknown H3 `DELETE` preserving its payload through `ReplayBody`;
- H3 request bodies remaining untouched for HTTP/2 upstreams;
- H1/H2 downstream request bodies remaining untouched by the H3-specific normalization;
- declared-length H3 `DELETE` bodies not being preconsumed.
## Runtime acceptance
Compiler/type/runtime acceptance remains open in this source workspace because the Rust toolchain is unavailable. On the test host, run `./dev.sh check` and `./dev.sh test`, then verify the full H1/H2/H3 method matrix. For the original Proxmox case, compare `/var/log/pveproxy/access.log` and a raw/mock HTTP/1.1 upstream. The H3 bodyless DELETE must arrive as `DELETE` with the same URI and without chunked request framing; a zero-length representation such as `Content-Length: 0` is expected for HTTP/1.1.
## Cache RAM hardening included in the same 0.24.5 source release
The cache keeps the existing HOT RAM -> COLD disk lifecycle. The changes are admission/accounting safeguards rather than a COLD format redesign:
- `cache.max_bytes` is a hard published-HOT charge based on the body backing allocation retained by `Bytes`, directly owned entry metadata and a conservative charge for HOT hash/LRU/variant indexes. `hot_logical_bytes` remains the visible payload total; `hot_allocated_bytes`/`resident_bytes` are the charged allocation view, not a duplicate RSS measurement.
- response fill uses a `Vec<u8>` whose actual `capacity()` is reserved before growth and transferred zero-copy into `Bytes`; `max_object_bytes` is checked before every chunk append;
- active fills reserve from `transient_max_bytes` and compare HOT + active fill against HOT capacity plus a small allocation-rounding headroom (about 1/64 HOT, capped at 1 MiB and further bounded for small caches); LRU entries are removed before further fill allocation where possible;
- one admission can remove at most `hot_eviction_max_objects`; if sufficient space cannot be made safely, the response continues uncached;
- ordinary HOT -> COLD demotion removes the entry from HOT and moves the same `Entry` into `ColdDraft`. The payload is not cloned. A transient lease follows the draft until persistence completes; the persisted `ColdEntry` has paths/metadata and no `Bytes` body;
- the COLD writer queue remains count-bounded and is additionally capped by `cold_queue_max_bytes`; COLD reads/promotions and revalidation reads reserve transient bytes before reading full bodies;
- global `cache.enabled=false` cannot be overridden by a vhost `enabled=true`;
- HOT/object settings no longer have the old artificial 1 GiB UI/source ceiling. Safety comes from the configured byte budgets rather than an arbitrary maximum.
Default memory values are 256 MiB HOT, 16 MiB maximum object, 64 MiB transient and 32 MiB COLD queue, with 4096 HOT entries and a 64-object LRU admission bound. The administration panel can calculate conservative/recommended/performance settings from the lower of detected host RAM and a finite cgroup memory maximum. For example, the recommended profile on an effective 2 GiB VM selects approximately 400 MiB HOT, 96 MiB transient, 16 MiB maximum object, 48 MiB COLD queue, 6400 entries and a 64-object LRU admission limit. Presets are disabled below 256 MiB effective RAM. The panel also checks the configured budgets against current host/cgroup memory headroom and warns without blocking manual configuration. COLD disk capacity is intentionally not changed by RAM presets.
Monitoring exposes HOT objects, HOT logical bytes, HOT allocated bytes, HOT limit, transient current/limit, COLD queue current/limit, COLD read/write activity, evicted/expired/rejected counts and existing process RSS/RssAnon/VmData/VmSize diagnostics. Cache RAM gauges and process RSS are overlapping views and must not be summed.
### Cache runtime acceptance still required
The source-level/offline gates verify configuration, UI/API wiring and static contracts. Because this workspace has no Rust toolchain, compile/type/runtime acceptance must still be performed on the test host. In addition to `./dev.sh check` and `./dev.sh test`, exercise at least:
- HOT 8 MiB under sustained traffic and verify HOT allocated stays bounded and RSS plateaus;
- HOT 2 GiB with a deliberately high preset and verify admission remains bounded;
- `cold_after_seconds=5`, verify HOT allocated falls after demotion and transient/COLD queue bytes return to zero after writes drain;
- an object larger than `max_object_bytes`, verify the client receives the full streamed response while the cache fill aborts before full buffering;
- concurrent COLD hits/writes under a small transient limit, verify memory-pressure rejection/pass-through rather than unbounded RAM growth;
- all four global/vhost enabled combinations, especially global OFF + vhost ON => effective OFF.
+404
View File
@@ -0,0 +1,404 @@
> 0.7 adds graphical locations, Basic Auth, cumulative IP ACL editing and a
> WebSocket toggle. See [host access and locations](VHOST_ACCESS.md).
# Virtual hosts, frontend bindings and backend pools - 0.2.0
This is the source implementation contract. Rust compilation and runtime acceptance
were not performed in the packaging environment; see TEST_REPORT.md.
## One vhost, one backend pool
A `vhosts` entry owns an `id`, `enabled`, `frontend`, `backends`, `load_balancing`,
`routes`, optional `acl`, `headers` and `security_headers`. The pre-production top-level
routing and inline-listener formats are no longer accepted. Unknown JSON fields are
rejected. The examples are complete documents, not JSON merge patches:
- `examples/vhosts-private.json`: explicit loopback IP, two existing HTTP origins,
adaptive balancing, HTTP health checks, keepalive, no cache or cookie affinity.
- `examples/vhosts-balanced.json`: explicitly public HTTP 80 and TLS 443, cookie
affinity and a deliberately short HSTS value; requires a trusted, imported `app`
certificate/key and real origins. It does not create those services or files.
In the examples, both origins need `/health` to return a quick 2xx response. Adjust
that path/status range for the real service. New pools start quarantined until
`rise` successful checks; an unavailable or misconfigured check deliberately causes
503 instead of sending traffic to an unverified member. `mode: off` opts out.
The editor includes frontends, members, balancing, health, connection reuse, sticky
sessions and response protection. Custom locations have graphical match/action/rewrite/ACL/header
controls plus synchronized Advanced JSON for automation. Unsaved drafts survive translation/SSE refreshes. A stale draft must be
reloaded before saving: the panel does not silently merge competing revisions.
Existing vhost IDs can be renamed in the dashboard as long as the new ID is valid and
unique. The rename keeps the configured frontend hostnames, routes and backend IDs, but it
intentionally starts a new vhost runtime identity for metrics/log attribution and current
health/traffic history. Because the vhost configuration participates in cache compatibility,
a rename also rotates that compatibility identity and invalidates incompatible cached objects.
The editor warns before applying the rename.
## Frontend selection and private IP isolation
`frontend.hosts` is a normalized hostname-pattern list. Exact hostnames and a single
leading wildcard such as `*.example.com` are supported; regex and mid-label wildcards
are not. Routing tries the exact hostname first, then the longest matching wildcard.
This means `api.example.com` can have its own vhost while `*.example.com` handles other
subdomains on the same listener without stealing the exact route.
`frontend.listener_ids` references named `listeners.endpoints` configured under
Settings. A listener owns its IP:port, TLS flag and H2/H3 transport capabilities;
a vhost never stores a listening address directly. For a new dashboard vhost, the
editor preselects both an enabled HTTP listener on port 80 and an enabled TLS listener
on port 443 when they exist (public-scope listeners are preferred). Existing vhost
bindings are never replaced by that convenience default.
`force_ssl` defaults to false. When enabled, any cleartext request that matches this
vhost receives HTTP `308 Permanent Redirect` to the same normalized hostname and
original path/query. At least one selected plain HTTP listener and one selected TLS
listener are required so there is both an ingress to redirect and an HTTPS target. If
several TLS ports are selected, port 443 is preferred; otherwise the selected TLS target
port must be unique.
`forward_client_ip` defaults to true. Normal vhosts therefore receive the sanitized
`Forwarded`, `X-Forwarded-For` and `X-Real-IP` client identity generated by RustProxy.
Set it to false for applications whose own trusted-proxy/bypass logic must see the
RustProxy TCP peer instead. Only the backend application request loses those three
client-IP headers; ACLs, GeoIP, rate limiting and Tinyauth still use the effective client
address, and origin metadata such as `X-Forwarded-Proto`, `X-Forwarded-Host`,
`X-Forwarded-Port` and `X-Forwarded-URI` remains available to the backend.
`websockets` enables WebSocket handling for the vhost. The dashboard defaults new WebSocket
vhosts to the nginx-compatible HTTP/1.1 Upgrade transport. `websocket_legacy_support: true` is
a separate per-vhost compatibility switch for older appliances and embedded servers. It affects
only WebSocket handshakes: RustProxy forces an HTTP/1.1 upstream WebSocket hop, uses the configured backend authority as upstream `Host`,
rewrites `Origin` to the matching upstream scheme + backend authority, and tolerates legacy `101 Switching Protocols` response
framing while keeping the `Sec-WebSocket-Accept` proof and offered subprotocol checks strict.
Ordinary HTTP requests are unchanged. `websocket_rfc8441: true` and `websocket_rfc9220: true` are
separate opt-ins for HTTP/2 and HTTP/3 Extended CONNECT and are valid only when WebSocket
support is enabled. They are presented together in the vhost editor.
RFC 8441 requires an H2-capable selected listener (TLS ALPN or cleartext h2c prior knowledge).
Because the RFC 9220 advertisement is QUIC-connection scoped, one opted-in vhost causes the
shared QUIC listener to advertise Extended CONNECT, while request routing still rejects RFC
9220 on non-opted-in vhosts.
`http2_enabled` and `http3_enabled` are per-vhost opt-outs (both default true). The
panel presents the two switches together and enables each only when at least one
selected listener provides that protocol. For TLS H2, ClientHello/SNI selects H2+HTTP/1.1 or HTTP/1.1-only ALPN for
the matched vhost. H3 remains a shared UDP listener capability; disabling it on a
vhost removes Alt-Svc and rejects H3 for that host.
Each vhost can optionally set `tls_policy` with `tls12`, `tls13` and a list of rustls
cipher-suite names. Omitting it inherits `tls.policy` from Settings. The override is
selected from ClientHello/SNI before the TCP TLS handshake, so a vhost can for example
run TLS 1.2 only while another frontend continues to use the global TLS 1.2/1.3 policy.
At least one protocol version and a compatible effective cipher suite are required.
Because HTTP/3 shares QUIC TLS on the UDP listener and requires TLS 1.3, a vhost with
HTTP/3 enabled must use the same TLS policy as the global setting. Disable HTTP/3 on
that vhost before using a differing per-vhost TLS policy.
Routing checks Host/HTTP authority, actual accepted local IP, local port and actual
TLS state. Neither `Forwarded` nor `X-Forwarded-*` can create frontend access. A
request for a private hostname on a different local IP is not routed to that host.
Unknown or unmatched hosts return 404.
Bindings must be local addresses assigned to the OS. The process does not add IPs,
manage routes, configure firewall rules, acquire virtual IPs or run Keepalived/VRRP.
A missing IP, occupied port or permission error aborts startup without wildcard
fallback. Low ports may need deployment capabilities; see `../deploy/README.md`.
Identical or same-family wildcard sockets are consolidated only when the named
listener transport capabilities agree. Vhost routing restrictions are not
consolidated: a wildcard TCP listener can accept on a public IP while still rejecting
a private vhost there. Per-vhost H2 is narrowed by SNI and therefore does not require
a second TCP socket. IPv4/IPv6 wildcard overlap is rejected; use explicit addresses.
Native IPv4 binds are required instead of mapped IPv6 literals. The administration
port is reserved even on a different IP.
IP isolation is not client authentication. A firewall, router, NAT or preceding
proxy can forward external clients to the private destination IP; RustProxy sees
that local destination and the connecting peer. Use network ACLs, firewall rules
and application authentication too. Trusted forwarding headers have a separate explicit CIDR policy (see
INTEGRATIONS_040.md); PROXY protocol is not implemented. Interface changes require config
reload/restart to refresh the upstream loop-guard's local-address enumeration.
The same literal hostname pattern may occur only where its listener bindings remain
unambiguous. Exact and wildcard patterns are distinct ownership keys, so an exact host
and its parent wildcard may coexist. Disabled vhosts reserve their patterns and are
still validated, but create no pool or additional active listener.
## Paths, policies and migration
`routes` contains per-vhost paths: `id`, `path_prefix`, `enabled`, nullable `cache`,
optional `acl`, `headers` and `backend_id`. An empty array becomes one enabled root route. Longest
segment-boundary prefix wins. Origins receive the original path/query; prefixes
are not stripped. IDs must use ASCII letters, digits, hyphens and underscores.
Host-policy, reusable ACL-profile, vhost and route ACLs are ANDed before pool
selection/DNS/cache. A vhost can attach one reusable `acl_profile_id`; the profile may
contain an IP ACL plus a country block/allow-only rule. Inline vhost/location controls
remain independent and cannot be weakened by the profile. Their client identity is the
validated effective peer address, not an untrusted forwarding header.
There is no legacy routing UI/runtime or automatic importer in 0.8.0. Development
databases using the old top-level routes or inline frontend listeners must be reset
or migrated explicitly before startup. An omitted path backend ID selects from the
balanced pool; a specified ID must belong to the same vhost and does not fail over to a
different application's member. Imported health checks start OFF. Ambiguous names,
unreferenced old backends and invalid/oversized documents stop migration before
saving. See MIGRATION.md before updating an existing installation.
## Backend members and connection reuse
Each member has `id`, HTTP(S) origin `url`, `weight` (1..256), `enabled` (true),
`backup` (false), `drain` (false), `allow_private` (false),
`accept_local_listener_risk` (false), `preserve_host` (true), `protocol` (`http1`),
`address_allowlist`, optional `tls_server_name`, `tls_verify` (false) and optional
`tls_ca_pem`. `tls_server_name` is an HTTPS-only DNS hostname
used for SNI and, while verification is enabled, certificate hostname verification;
DNS/TCP still targets the configured `url`. `tls_verify` defaults to false. HTTPS connections remain encrypted, but backend certificate authentication is skipped until an administrator explicitly sets `tls_verify: true`; this includes compatibility with legacy certificate encodings that modern webpki validation rejects. Address/DNS policy remains enforced; local-listener self-targeting has only the explicit risk exception described below. `tls_ca_pem` is ignored while verification is disabled.
Backend IDs are unique inside a vhost. URLs cannot contain credentials, paths,
queries or fragments.
Every DNS answer and every configured literal must pass the same upstream policy
for health probes and forwarding. Active, pending and prior-snapshot frontend
addresses and local administration are guarded against self-proxying by default.
For an intentional local target such as `http://127.0.0.1:9090`, enable both
`allow_private: true` and `accept_local_listener_risk: true`. The latter is a narrow,
explicit acceptance of loop/control-plane exposure risk; metadata/link-local and other
special-address blocks are not bypassed. In the dashboard this risk checkbox is hidden
unless the entered backend URL directly targets an administration listener reported by
persisted configuration or live status. Startup-owned/locked administration sockets are
therefore handled too. On the same administration port, loopback spellings such as
`localhost`, `127.0.0.1` (and the rest of `127.0.0.0/8`) and `::1` are treated as
equivalent for showing the explicit risk control, so it is not presented as a normal
backend option.
A vhost can independently enable two proxy-owned response diagnostics:
- `backend_header: true` -> `X-RustProxy-Backend: <backend-id>`,
- `backend_timing_header: true` -> `X-RustProxy-Backend-Time: <milliseconds>ms`.
The timing value covers the origin request through receipt of the backend response
headers; it is not full response-body duration. Both proxy-owned names are protected
from custom header rules and any upstream values are removed before RustProxy optionally
writes its own value. The former `X-Rust-Proxy-Backend-Time` spelling is also stripped
as a legacy lookalike but is never emitted. Responses served without contacting an
origin do not expose a selected backend or stale origin timing.
`timing_headers` remains the separate opt-in `Server-Timing` diagnostic.
`keepalive` controls **connection pooling**, not an HA service:
| Field | Default | Accepted range |
|---|---|---|
| `enabled` | true | Boolean |
| `idle_timeout_seconds` | 30 | 1..3600 |
| `max_idle_per_host` | 4 | 1..64 |
| `tcp_keepalive_seconds` | 30 | 1..3600 |
These configure HTTP idle reuse and TCP keepalive; HTTP/2 also uses pings for active
connections, with a 10-second ping timeout and no pinging solely to keep idle
connections alive. Disabling reuse sets the idle pool limit to zero and disables
those keepalive probes. It does not abort already active requests/tunnels. The idle
limit is not a separate per-backend active-request cap; global connection/exchange
bounds still apply. HTTP health probes use separate no-reuse HTTP/1.1 connections.
`http2` over HTTPS prefers h2 but keeps HTTP/1.1 ALPN compatibility. Over plaintext,
`http2` remains explicit prior-knowledge h2c with no retry-based downgrade. `auto`
over HTTP is HTTP/1.1. `http2`/`auto` require `preserve_host: false` to keep origin
URI/authority/TLS identity consistent. HTTPS backend certificate verification is disabled when `tls_verify` is omitted. Enable `tls_verify: true` when origin identity must be validated; for IP-based origins use a DNS `tls_server_name` present in the certificate and provide `tls_ca_pem` when the issuer is private.
## Scheduling
| `algorithm` | Decision |
|---|---|
| `round_robin` | Smooth weighted round-robin |
| `least_connections` (default) | Lowest `(active exchanges + 1) / effective weight` |
| `least_traffic` | Lowest recent EWMA payload rate plus active-exchange allowance, divided by effective weight |
| `adaptive` | Combines active exchanges, EWMA time-to-headers and payload rate, divided by effective weight |
| `ip_hash` | Weighted rendezvous hash of vhost/member and canonical actual peer IP |
An active exchange spans upload, response body and upgraded tunnel, not just the
wait for response headers. Slow-start gradually increases effective weight after
initial success/recovery (`slow_start_seconds`: 10, allowed 0..300; 0 disables it).
IP hashing uses configured weights instead of slow-start to keep assignments stable.
These are explicit heuristics, not workload predictions or benchmark-backed claims.
No remote host CPU measurement is used by the scheduler.
Only enabled, healthy, non-draining primary members receive new assignments. If
none exists, eligible backups are used. All unavailable means 503 with Retry-After;
there is no fallback into another vhost. Balanced routes may retry a body-free `GET`,
`HEAD` or `OPTIONS` on another healthy member according to `load_balancing.retry`:
| Retry field | Default | Accepted range / meaning |
|---|---|---|
| `max_retries` | 1 | 0..3; zero disables failover retry |
| `retry_connect_errors` | true | Retry connection establishment failures |
| `retry_timeouts` | true | Retry the response-header timeout |
| `retry_statuses` | `[502,503,504]` | Up to 8 HTTP 5xx statuses |
A backend already attempted in the same exchange is excluded from later attempts.
Path-pinned routes and WebSocket handshakes are not retried. POST/PUT/PATCH and any
request with a body are never automatically replayed. Failed attempts still feed the
normal passive-health policy and retry counters/logs; successful failover updates sticky
affinity to the backend that actually answered.
## Health checks and recovery
| Field | Default | Accepted range / meaning |
|---|---|---|
| `mode` | `tcp` | `off`, `tcp`, `http` |
| `path` | `/` | Leading slash, up to 1024 bytes; no authority/fragment |
| `host` | null | Optional validated HTTP Host override |
| `interval_ms` | 5000 | 250..300000 |
| `timeout_ms` | 2000 | 50..60000; no greater than interval |
| `slow_response_ms` | 1500 | 0 disables; otherwise no greater than timeout |
| `fall` / `rise` | 3 / 2 | 1..20 consecutive failed/successful checks |
| `expected_status_min` / `max` | 200 / 399 | Inclusive, ordered HTTP status range within 100..599 |
TCP checks verify that an approved origin address accepts a connection. HTTP checks
perform GET on the configured path, without redirects, cookies or authorization.
They use HTTP/1.1, including verified TLS when appropriate, and measure time to
headers, not the full body. For an h2c-only service, use TCP checks. Slow probes
count as active failures. Active TCP/HTTP health is independent from `passive_health`.
`passive_health.enabled` defaults to `false`. With that default, normal user traffic
never changes the shared backend healthy/unhealthy state: HTTP 5xx, connection errors,
timeouts, response-body I/O failures and slow responses are still counted/logged but
cannot quarantine the member. When explicitly enabled, the administrator can choose
`count_connect_errors`, `count_timeouts`, `count_io_errors`, `count_http_5xx` and
`count_slow_responses`, with optional passive `fall`/`rise` overrides. Missing overrides
inherit the active health thresholds. Active and passive streaks are separate, so a
successful user request cannot erase an active-probe failure streak and an active probe
cannot erase a passive failure streak. `mode: off` disables only active probes; an
explicitly enabled passive policy may still quarantine a backend.
One cancellation-aware supervisor schedules at most 16 probes, one per member,
with a rotating window for fairness. The tick is 250 ms; configured intervals are
best-effort under resource pressure, not precise service-level guarantees. DNS
workers have their own bounded slots. Probe bodies are not collected or cached.
## Sticky sessions and drain
`sticky.mode` is `off`, `cookie` or `ip_hash`. IP mode overrides the normal algorithm
using the actual peer IP. Users behind one NAT/proxy share that identity.
Cookie mode uses a random 32-character opaque member token, not a backend URL.
The default name is `RP_AFFINITY_<vhost-id>`; custom names accept 1..96 ASCII letters,
digits, underscores or hyphens and cannot use the admin session name. TTL defaults
to 3600 seconds (60..604800), with Path=/, HttpOnly, SameSite=Lax and Secure on TLS.
`__Host-`/`__Secure-` names require every frontend to use TLS. Cookies are host-only,
so aliases have independent browser cookies. They do not authenticate users.
Cookie-affine requests always bypass shared cache. The dedicated routing cookie
is stripped before forwarding; origins cannot override it via Set-Cookie. Unknown,
duplicate or unhealthy-target tokens are ignored/reassigned. Existing valid cookie
sessions may continue on a healthy draining/backup member; new sessions do not
choose a draining member. Disabling a member prevents all new assignments,
including existing cookie sessions, while already admitted exchanges finish.
Affinity tokens, health and traffic observations survive compatible hot edits.
A changed origin/health policy or re-enabled member is checked afresh. Process
restart resets tokens/counters; clients can be reassigned. Session data itself is
never replicated: applications needing durable sessions require a shared store.
## Response protection
`security_headers` supports `hsts` (`enabled`, `max_age_seconds`,
`include_subdomains`, `preload`), `nosniff`, `frame_options` (`preserve`, `deny`,
`sameorigin`), `referrer_policy`, `content_security_policy` and `permissions_policy`.
Header values are validated against splitting and bounded to 8192 bytes.
HSTS is configured only for TLS responses. For a matched vhost, plaintext responses
remove Strict-Transport-Security even if an origin/custom rule adds it. On TLS,
disabled HSTS means no forced replacement of an origin value; use a custom remove
rule to suppress that value. Enabled HSTS overrides generic rules. `max_age_seconds`
accepts 0..63072000 (default31536000); setting zero is available for explicit expiry.
Preload is opt-in and requires enabled HSTS, includeSubDomains and at least one year.
This only emits the directive; it does not register a hostname in a preload list.
Start with a short lifetime and verify every affected hostname before increasing it.
Request rules run global -> vhost -> path, then protected identity fields are
regenerated. Response rules run global -> vhost -> path, followed by dedicated
security/Server policy and optional compression. Security headers cover cache hits
and matched-host synthetic errors/ACL denials too. Protected transport fields
remain uneditable. `false`, `preserve` or null security settings otherwise leave
origin values unchanged. CSP and Permissions-Policy must suit the application;
RustProxy validates header encoding, not the semantic correctness of those policies.
## Live state, cache and limits
The dashboard defaults a newly created vhost to `cache.enabled: false`. When the
effective cache state is disabled, the editor hides the remaining per-vhost cache
controls; enabling cache (or explicitly choosing inheritance from an enabled global
cache) reveals them. This is a UI default for new drafts, not a migration that rewrites
existing vhost cache settings.
`GET /api/backends` requires an admin session and returns `pools`,
`active_frontends`, `desired_frontends`. `/api/status` and SSE contain `backend_pools`
and complete active/desired endpoint plans. The dashboard renders per-member state,
active exchanges, request/error totals, EWMA header latency and payload traffic.
Prometheus adds bounded vhost/member labels; access logs include `backend` as
`vhost/member` for actual pool forwarding (null when no upstream exchange is made).
Bytes are observed payload, not NIC throughput; response counters include the
client-delivered encoded representation. Header latency includes connection/setup and any request upload; it is not full
exchange time. Active probes use body-free requests for an independent measurement.
Health mode off is labeled as checks off, not proof of a probe passing. Statistics
are observations, not persisted configuration. They reset on process restart.
Cookie affinity bypasses cache entirely. A pool with no eligible member does not invent
an unselected cached fallback. Config changes invalidate incompatible cache state.
RFC 5861/request `max-stale` behavior is owned by the shared cache and does not alter
backend member selection; request coalescing remains separate from stale-while-revalidate
deduplication.
Limits: 64 vhosts; hostname lists are not capped by an application-level item count;
listener entries remain capped at 16 per host, with 32 members and 64 paths per host;
128 backend definitions and 256 paths across all vhosts;
128 additional declared frontend entries plus two globals; 256 KiB serialized
configuration. Existing request/connection/DNS/queue bounds remain in force.
Every successful config publication rebuilds upstream connection pools. Compatible
member observations/tokens and in-flight exchanges survive, but idle sockets are
not retained across publication. Listener/address/ALPN changes are saved immediately
and require restart; routing changes apply to existing bound sockets immediately.
When moving a host away from an old IP, it can become unavailable until restart
rather than being served on an address no longer permitted by desired policy.
## Per-vhost operations in 0.3.0
The compact list opens a tabbed editor rather than displaying a full form beside
the list. Health summaries update on the list and editor without replacing unsaved
form controls. Header rules have a visual request/response editor; Advanced JSON
remains available and stays synchronized with those rows.
The SSL tab selects an existing certificate ID or uploads a chain/private-key
pair into private staging. Only Save activates the selection. A shared global SNI
resolver cannot serve different certificates for the same DNS name by destination
IP, so conflicting selections are rejected. Browser upload is local-admin only;
there is no ACME, encrypted-key password prompt, certificate download or automated
cleanup. Details and limits: MANAGEMENT.md.
Cache overrides inherit field by field and use one shared memory budget. The list
and Cache tab offer selective purge. An explicitly disabled global cache can still be
overridden by an explicitly enabled vhost. Path exclusions and sticky-cookie safeguards
remain in force. See CACHE.md.
The **Transform** tab is a separate vhost opt-in. `transform.enabled` defaults to false
for every vhost even when the global transform engine is enabled. JavaScript, CSS, HTML,
SVG and JSON can inherit or override global format switches, and the extension list may
be replaced per vhost. Eligible origin responses are transformed before shared-cache
storage and before downstream compression. Origin `Cache-Control: no-transform` always
wins and cannot be overridden from the vhost.
Pages offers a static/maintenance response, a fallback for unassigned paths, and
error overrides. Vhosts still retain their configured backend pool; static mode
avoids forwarding but is not a backend-less static-file server. Host/path ACLs run
first. Explicit error-status overrides change presentation, not authorization.
Empty/inherit means use the global error policy; to disable one inherited error
page specifically, configure an explicit replacement rather than expecting null
to override inheritance. See MANAGEMENT.md for exact precedence.
### Backend Host compatibility
New backends use the backend URL host for the upstream `Host` header by default, matching nginx `proxy_pass` behavior. Enable **Preserve client Host** only when the application intentionally routes on the public hostname. This is especially important for embedded/lighttpd-style administration panels that may close or reject requests carrying an unknown Host. Existing explicit settings are preserved during upgrades.
When an upstream request fails before an HTTP response is received, access logs include a bounded `upstream_error` field in addition to `upstream_status`.
+121
View File
@@ -0,0 +1,121 @@
# Vhost access and visual locations
## 0.8.0 edge traffic controls
The Security/Traffic policy section now includes per-client-IP request limits, burst,
window length, country-specific limit overrides, country blocking or allow-only mode,
per-vhost HTTP/3 enable/disable, optional `Server-Timing`, and a Cloudflare real-client-IP
switch. Country rules use the install-time GeoIP country database. Allow-only mode
fails closed when a country cannot be resolved; block mode only blocks resolved matches.
Exceeded limits return 429 plus `Retry-After`; country policy denial returns 403. Both
use built-in HTML pages unless a configured custom error page replaces them.
Cloudflare mode trusts `CF-Connecting-IP` only when the **direct peer** is inside the
compiled Cloudflare edge CIDRs. An arbitrary client sending that header directly does
not become trusted. The verified address becomes the effective client IP for ACLs,
rate limiting, country lookup and forwarded metadata.
Vhost listener editing is now selection-only. Listener addresses/IPs and transport
settings live under Settings > Listeners; a vhost selects one or more named endpoints.
Existing inline/legacy bindings are promoted to named endpoints when the vhost is saved.
New backend rows use the HTTPS-by-IP debugging defaults: include in the default LB pool,
allow private origin, HTTP/1.1, preserve client Host and backend TLS verification OFF.
The panel shows the risk and asks before saving an HTTPS-by-IP backend that does not
match the complete preset. Existing backend values are not rewritten automatically.
Health mode `off` disables active probes only. Passive Backend Health is a separate
opt-in policy and defaults to OFF. With the default policy, normal request failures are
still counted but never mark a backend healthy/unhealthy; an administrator may explicitly
enable passive health even while active probe mode remains `off`.
## WebSockets
`vhosts[].websockets` controls HTTP/1.1 Upgrade forwarding for that host. Existing
configuration omitting it retains the previous enabled behavior. New hosts created
in the panel default to disabled. Disabled valid upgrades return 403 without opening
an upstream connection; normal HTTP continues. Basic Auth/Tinyauth and ACLs still
apply to an allowed handshake. This does not implement WebSockets over HTTP/3,
WebTransport or generalized CONNECT, and does not terminate already open tunnels
when toggled; it governs subsequent handshakes. After a successful `101`, normal TCP
EOF/reset/abort/broken-pipe closure is recorded as tunnel closure rather than an invalid
proxy request; handshake/protocol errors remain errors.
## Basic Auth and ACL
The Authentication tab selects None, Tinyauth or HTTP Basic. Basic and Tinyauth
cannot both be enabled. The Basic editor manages users, realm, TLS requirement
and whether verified `Authorization: Basic ...` is passed to the backend.
Only bounded Argon2id password hashes are persisted. New/changed passwords are
sent to `POST /api/basic-auth/hash` through the protected administrator API; no
password is stored in browser storage, desired configuration, audit or access logs.
An empty password while editing keeps the saved hash. New passwords use the existing
12-128 byte administrator password policy; Basic credentials cannot contain control
characters. Usernames are case-sensitive, up to 128 UTF-8 bytes, without colons or
control characters. This is not an htpasswd/APR1/bcrypt importer.
TLS is required by default and is checked from the accepted connection, not
`X-Forwarded-Proto`. Plaintext HTTP is allowed only by an explicit host option; avoid
that outside a deliberately protected TLS-termination network. HTTP Basic encodes,
not encrypts, the username/password. Keep the frontend and any credential-carrying
upstream hop protected. Administrator access defaults to loopback; direct remote access is supported when Administration HTTPS is enabled and the bind/hostname policy is configured.
Successful credentials are removed before forwarding by default. Selecting forward
passes the original **verified** Basic Authorization header, not a password from
configuration or an unverified client value. The final enforcement step runs after
custom request-header rules, which cannot reintroduce removed credentials.
`Remote-User`/`Remote-Email` are stripped in Basic mode; the separate Tinyauth mode
continues to use verified identity headers from that service.
Global host policies, reusable ACL profiles, vhost ACLs and location ACLs remain
cumulative: access requires all applicable policies **and** successful authentication.
An ACL profile can be defined once under **ACL profiles**, combine default allow/deny
IPv4/IPv6 CIDR rules with country block/allow-only policy, then be attached to several
vhosts. Per-vhost rules remain available and are evaluated in addition to the profile.
Within an IP ACL, deny takes precedence. This is not an Nginx `satisfy any` mode: a
permitted IP cannot skip another applicable policy or a password. The graphical inline
ACL editor still supports default allow/deny and IPv4/IPv6 CIDR rules.
Trusted-proxy settings control which peer can contribute the effective client IP;
untrusted forwarding headers do not grant network access.
Authentication runs before static pages, cache, origin connections and upgrades.
401 responses include a Basic challenge and `private, no-store`; custom pages or
header rules cannot convert these failures into successful access. Protected vhosts
bypass shared response caching. A process-wide limit allows two Argon2 workers;
failed-password accounting is bounded to 4096 IPs, with ten failures per minute
blocking that address for the rest of the window. Busy hashing returns 503; a
blocked address returns 429 with Retry-After. This is abuse resistance, not MFA or
a replacement for perimeter rate limiting, especially for many clients behind NAT.
## Custom locations
The **Custom locations** tab supports ordinary prefix, priority prefix (`^~`), exact (`=`),
case-sensitive regex (`~`) and case-insensitive regex (`~*`) matching. Exact matches win first;
a selected priority prefix skips regex evaluation; regexes are tested in declaration order; the
longest ordinary prefix is the fallback. Exact paths preserve a meaningful trailing slash.
A proxy location can use the normal pool, pin one backend, inherit/override cache behavior and
apply an additional ACL. Path handling can preserve the public URL, strip a prefix, replace a
prefix or perform a capture-based regex rewrite. Query parameters are preserved. Repeated literal
slashes are normalized only for routing/ACL decisions; without an explicit rewrite the backend
receives the original target, including `//`. Encoded separators, percent double-decoding and dot
segments remain rejected.
A location can alternatively **Return custom response** with a 200–599 status, Content-Type and
body without opening a backend connection. Route/vhost ACL and authentication still apply. A 3xx
return plus a visual `Location` response-header rule is the supported simple redirect pattern.
Request/response headers have a graphical `set`/`append`/`remove` builder, inline documentation
and a synchronized **Advanced JSON** view. Protected framing, forwarding-identity, request-ID and
WebSocket handshake headers cannot be overridden. The full routes JSON remains available as an
automation/escape hatch, but invalid/unknown fields are rejected rather than silently ignored.
RustProxy intentionally implements an explicit safe subset, not arbitrary Nginx configuration:
location-level `try_files`, filesystem `root`/`alias`, embedded scripting, arbitrary `if` and
module directives are not accepted. See [Custom locations](CUSTOM_LOCATIONS.md) for examples and
the exact supported subset.
Reference: HTTP Basic security considerations, RFC 7617:
https://www.rfc-editor.org/rfc/rfc7617.html#section-4
+8
View File
@@ -0,0 +1,8 @@
# UI preview policy
Only two legacy simulated previews are retained as narrow visual references. They were generated with injected HTML/CSS/JS and simulated API/fetch/storage, so they are **not current Rust runtime evidence** and may differ from the 0.22.3 UI. Runtime verification status belongs in [../TEST_REPORT.md](../TEST_REPORT.md).
- `login.png`: anonymous minimal Polish login document.
- `location-headers.png`: request/response rules for an individual location.
RustProxy 0.19.1 removes the other old simulated screenshots so stale layouts are not presented as current product state.
+236
View File
@@ -0,0 +1,236 @@
{
"host_policies": [
{
"hosts": [
"localhost",
"127.0.0.1",
"demo.localhost"
],
"acl": {
"default": "deny",
"rules": [
{
"network": "127.0.0.0/8",
"action": "allow"
},
{
"network": "::1/128",
"action": "allow"
}
]
}
}
],
"headers": {
"request": [
{
"op": "set",
"name": "x-rustproxy-example",
"value": "global"
}
],
"response": [
{
"op": "remove",
"name": "server"
},
{
"op": "set",
"name": "x-content-type-options",
"value": "nosniff"
}
]
},
"vhosts": [
{
"id": "example-1",
"frontend": {
"hosts": [
"localhost"
],
"listener_ids": [
"example-1-http"
]
},
"backends": [
{
"id": "demo",
"url": "http://127.0.0.1:9000",
"allow_private": true,
"preserve_host": true
}
],
"load_balancing": {
"health_check": {
"mode": "off"
}
},
"routes": [
{
"id": "demo",
"path_prefix": "/",
"backend_id": "demo",
"enabled": true,
"headers": {
"request": [
{
"op": "set",
"name": "x-rustproxy-example",
"value": "route"
}
],
"response": [
{
"op": "set",
"name": "x-rustproxy-demo",
"value": "enabled"
}
]
}
},
{
"id": "blocked-admin",
"path_prefix": "/admin",
"backend_id": "demo",
"enabled": true,
"acl": {
"default": "deny",
"rules": []
}
}
]
},
{
"id": "example-2",
"frontend": {
"hosts": [
"127.0.0.1"
],
"listener_ids": [
"example-1-http"
]
},
"backends": [
{
"id": "demo",
"url": "http://127.0.0.1:9000",
"allow_private": true,
"preserve_host": true
}
],
"load_balancing": {
"health_check": {
"mode": "off"
}
},
"routes": [
{
"id": "demo",
"path_prefix": "/",
"backend_id": "demo",
"enabled": true,
"headers": {
"request": [
{
"op": "set",
"name": "x-rustproxy-example",
"value": "route"
}
],
"response": [
{
"op": "set",
"name": "x-rustproxy-demo",
"value": "enabled"
}
]
}
},
{
"id": "blocked-admin",
"path_prefix": "/admin",
"backend_id": "demo",
"enabled": true,
"acl": {
"default": "deny",
"rules": []
}
}
]
},
{
"id": "example-3",
"frontend": {
"hosts": [
"demo.localhost"
],
"listener_ids": [
"example-1-http"
]
},
"backends": [
{
"id": "demo",
"url": "http://127.0.0.1:9000",
"allow_private": true,
"preserve_host": true
}
],
"load_balancing": {
"health_check": {
"mode": "off"
}
},
"routes": [
{
"id": "demo",
"path_prefix": "/",
"backend_id": "demo",
"enabled": true,
"headers": {
"request": [
{
"op": "set",
"name": "x-rustproxy-example",
"value": "route"
}
],
"response": [
{
"op": "set",
"name": "x-rustproxy-demo",
"value": "enabled"
}
]
}
},
{
"id": "blocked-admin",
"path_prefix": "/admin",
"backend_id": "demo",
"enabled": true,
"acl": {
"default": "deny",
"rules": []
}
}
]
}
],
"listeners": {
"endpoints": [
{
"id": "example-1-http",
"name": "example-1 HTTP",
"scope": "internal",
"enabled": true,
"binding": {
"address": "127.0.0.1:8080",
"tls": false,
"http2": false,
"http3": null
}
}
]
}
}
+159
View File
@@ -0,0 +1,159 @@
{
"listeners": {
"endpoints": [
{
"id": "example-1-http",
"name": "example-1 HTTP",
"scope": "internal",
"enabled": true,
"binding": {
"address": "127.0.0.1:8080",
"tls": false,
"http2": false,
"http3": null
}
}
]
},
"server_header": {
"mode": "custom",
"value": "RustProxy/v0.5.0"
},
"compression": {
"gzip": true,
"br": true,
"zstd": true,
"deflate": false,
"min_bytes": 1024
},
"cache": {
"enabled": true,
"max_bytes": 268435456,
"max_object_bytes": 16777216,
"max_entries": 4096,
"transient_max_bytes": 67108864,
"cold_queue_max_bytes": 33554432,
"hot_eviction_max_objects": 64,
"default_ttl_seconds": 0,
"max_ttl_seconds": 86400,
"bypass_query": false,
"bypass_prefixes": [
"/private"
],
"expose_status": true,
"expose_hits": true,
"expose_cache_status": true
},
"headers": {
"request": [],
"response": [
{
"op": "remove",
"name": "x-powered-by"
},
{
"op": "set",
"name": "x-served-by",
"value": "RustProxy"
}
]
},
"vhosts": [
{
"id": "example-1",
"frontend": {
"hosts": [
"localhost"
],
"listener_ids": [
"example-1-http"
]
},
"backends": [
{
"id": "demo",
"url": "http://127.0.0.1:9000",
"allow_private": true,
"preserve_host": true
}
],
"load_balancing": {
"health_check": {
"mode": "off"
}
},
"routes": [
{
"id": "demo",
"path_prefix": "/",
"backend_id": "demo",
"enabled": true
}
]
},
{
"id": "example-2",
"frontend": {
"hosts": [
"127.0.0.1"
],
"listener_ids": [
"example-1-http"
]
},
"backends": [
{
"id": "demo",
"url": "http://127.0.0.1:9000",
"allow_private": true,
"preserve_host": true
}
],
"load_balancing": {
"health_check": {
"mode": "off"
}
},
"routes": [
{
"id": "demo",
"path_prefix": "/",
"backend_id": "demo",
"enabled": true
}
]
},
{
"id": "example-3",
"frontend": {
"hosts": [
"demo.localhost"
],
"listener_ids": [
"example-1-http"
]
},
"backends": [
{
"id": "demo",
"url": "http://127.0.0.1:9000",
"allow_private": true,
"preserve_host": true
}
],
"load_balancing": {
"health_check": {
"mode": "off"
}
},
"routes": [
{
"id": "demo",
"path_prefix": "/",
"backend_id": "demo",
"enabled": true
}
]
}
]
}
+52
View File
@@ -0,0 +1,52 @@
{
"vhosts": [
{
"id": "demo",
"frontend": {
"hosts": [
"localhost",
"127.0.0.1",
"demo.localhost"
],
"listener_ids": [
"demo-http"
]
},
"backends": [
{
"id": "demo",
"url": "http://127.0.0.1:9000",
"allow_private": true,
"preserve_host": true
}
],
"load_balancing": {
"health_check": {
"mode": "off"
}
},
"routes": [
{
"id": "root",
"path_prefix": "/"
}
]
}
],
"listeners": {
"endpoints": [
{
"id": "demo-http",
"name": "demo HTTP",
"scope": "internal",
"enabled": true,
"binding": {
"address": "127.0.0.1:8080",
"tls": false,
"http2": false,
"http3": null
}
}
]
}
}
+3
View File
@@ -0,0 +1,3 @@
{
"vhosts": []
}
+61
View File
@@ -0,0 +1,61 @@
{
"listeners": {
"endpoints": [
{
"id": "local-https",
"name": "Local TLS / HTTP3",
"scope": "internal",
"enabled": true,
"binding": {
"address": "127.0.0.1:8443",
"tls": true,
"http2": true,
"http3": {
"alt_svc_max_age": 3600,
"max_concurrent_connections": 512,
"max_concurrent_bidistream": 100,
"max_concurrent_unistream": 100,
"max_idle_timeout": 10
}
}
}
]
},
"vhosts": [
{
"id": "h3-demo",
"frontend": {
"hosts": [
"localhost"
],
"listener_ids": [
"local-https"
]
},
"backends": [
{
"id": "origin-a",
"url": "http://127.0.0.1:9001",
"allow_private": true
}
],
"load_balancing": {
"health_check": {
"mode": "off"
}
},
"cache": {
"enabled": true,
"expose_status": true,
"expose_hits": true
},
"server_header": {
"mode": "custom",
"value": "RustProxy/v0.5.0"
},
"diagnostic_headers": {
"request_id": false
}
}
]
}
+92
View File
@@ -0,0 +1,92 @@
{
"cache": {
"enabled": false
},
"compression": {
"gzip": true,
"br": true,
"min_bytes": 1024
},
"pages": {
"direct_ip": {
"kind": "welcome",
"status": 200
}
},
"vhosts": [
{
"id": "mock",
"frontend": {
"hosts": [
"mock.localhost"
],
"listener_ids": [
"mock-http"
]
},
"backends": [
{
"id": "origin-a",
"url": "http://127.0.0.1:9001",
"allow_private": true,
"preserve_host": true
}
],
"load_balancing": {
"health_check": {
"mode": "http",
"path": "/health",
"interval_ms": 1000,
"timeout_ms": 800,
"slow_response_ms": 600,
"fall": 2,
"rise": 1
},
"slow_start_seconds": 0
},
"cache": {
"enabled": true,
"expose_status": true,
"expose_hits": true,
"expose_cache_status": true,
"max_ttl_seconds": 300,
"default_ttl_seconds": 30
},
"headers": {
"response": [
{
"op": "set",
"name": "x-site",
"value": "mock"
}
]
},
"security_headers": {
"nosniff": true
},
"pages": {
"errors": {
"503": {
"kind": "html",
"html": "<!doctype html><meta charset=\"utf-8\"><title>Maintenance</title><h1>Origin unavailable</h1><p>Please try again soon.</p>"
}
}
}
}
],
"listeners": {
"endpoints": [
{
"id": "mock-http",
"name": "mock HTTP",
"scope": "internal",
"enabled": true,
"binding": {
"address": "127.0.0.1:8080",
"tls": false,
"http2": false
}
}
]
}
}
+69
View File
@@ -0,0 +1,69 @@
{
"listeners": {
"endpoints": [
{
"id": "private-http",
"name": "Private applications",
"scope": "internal",
"enabled": true,
"binding": {
"address": "127.0.0.2:8181",
"tls": false,
"http2": false
}
}
]
},
"vhosts": [
{
"id": "tinyauth-app",
"frontend": {
"hosts": [
"app.localhost"
],
"listener_ids": [
"private-http"
]
},
"backends": [
{
"id": "origin-a",
"url": "http://127.0.0.1:9001",
"allow_private": true
}
],
"load_balancing": {
"health_check": {
"mode": "off"
}
},
"cache": {
"enabled": true,
"expose_status": true,
"expose_hits": true
},
"server_header": {
"mode": "custom",
"value": "RustProxy/v0.5.0"
},
"diagnostic_headers": {
"request_id": false
},
"tinyauth": {
"enabled": true,
"url": "http://10.87.7.99:3000",
"login_url": "https://auth.linuxiarz.pl/login",
"allow_private": true,
"address_allowlist": [
"10.87.7.99/32"
],
"connect_timeout_seconds": 5,
"timeout_seconds": 10,
"response_headers": [
"remote-user",
"remote-email"
]
}
}
]
}
+69
View File
@@ -0,0 +1,69 @@
{
"tls": {
"certificates": [
{
"id": "app",
"hosts": [
"app.localhost"
],
"cert_file": "app.pem",
"key_file": "app.key"
}
]
},
"vhosts": [
{
"id": "example-1",
"frontend": {
"hosts": [
"app.localhost"
],
"listener_ids": [
"example-1-http"
]
},
"backends": [
{
"id": "demo-dns",
"url": "http://localhost:9000",
"protocol": "http1",
"allow_private": true,
"preserve_host": true,
"address_allowlist": [
"127.0.0.0/8",
"::1/128"
]
}
],
"load_balancing": {
"health_check": {
"mode": "off"
}
},
"routes": [
{
"id": "app",
"path_prefix": "/",
"backend_id": "demo-dns",
"enabled": true
}
]
}
],
"listeners": {
"endpoints": [
{
"id": "example-1-http",
"name": "example-1 HTTP",
"scope": "internal",
"enabled": true,
"binding": {
"address": "127.0.0.1:8080",
"tls": false,
"http2": false,
"http3": null
}
}
]
}
}
+138
View File
@@ -0,0 +1,138 @@
{
"listeners": {
"endpoints": [
{
"id": "public-app-http",
"name": "public-app HTTP",
"scope": "internal",
"enabled": true,
"binding": {
"address": "0.0.0.0:80",
"tls": false,
"http2": false
}
},
{
"id": "public-app-https",
"name": "public-app HTTPS",
"scope": "internal",
"enabled": true,
"binding": {
"address": "0.0.0.0:443",
"tls": true,
"http2": true
}
}
]
},
"vhosts": [
{
"id": "public-app",
"enabled": true,
"frontend": {
"hosts": [
"app.example.test"
],
"listener_ids": [
"public-app-http",
"public-app-https"
]
},
"backends": [
{
"id": "app-a",
"url": "http://127.0.0.1:9001",
"weight": 3,
"enabled": true,
"allow_private": true,
"preserve_host": true,
"protocol": "http1",
"address_allowlist": [
"127.0.0.1/32"
]
},
{
"id": "app-b",
"url": "http://127.0.0.1:9002",
"weight": 1,
"enabled": true,
"allow_private": true,
"preserve_host": true,
"protocol": "http1",
"address_allowlist": [
"127.0.0.1/32"
]
}
],
"load_balancing": {
"algorithm": "adaptive",
"slow_start_seconds": 10,
"health_check": {
"mode": "http",
"path": "/health",
"interval_ms": 5000,
"timeout_ms": 2000,
"slow_response_ms": 1500,
"fall": 3,
"rise": 2,
"expected_status_min": 200,
"expected_status_max": 299
},
"keepalive": {
"enabled": true,
"idle_timeout_seconds": 30,
"max_idle_per_host": 4,
"tcp_keepalive_seconds": 30
},
"sticky": {
"mode": "cookie",
"ttl_seconds": 3600
}
},
"security_headers": {
"hsts": {
"enabled": true,
"max_age_seconds": 300,
"include_subdomains": false,
"preload": false
},
"nosniff": true,
"frame_options": "sameorigin",
"referrer_policy": "strict-origin-when-cross-origin"
},
"headers": {
"response": [
{
"op": "remove",
"name": "x-powered-by"
},
{
"op": "set",
"name": "x-served-by",
"value": "RustProxy"
}
]
},
"routes": [
{
"id": "root",
"path_prefix": "/",
"cache": false
}
]
}
],
"tls": {
"default_certificate": "app",
"certificates": [
{
"id": "app",
"hosts": [
"app.example.test"
],
"cert_file": "app.pem",
"key_file": "app.key"
}
]
}
}
+110
View File
@@ -0,0 +1,110 @@
{
"listeners": {
"endpoints": [
{
"id": "private-app-http",
"name": "private-app HTTP",
"scope": "internal",
"enabled": true,
"binding": {
"address": "127.0.0.2:8080",
"tls": false,
"http2": false
}
}
]
},
"vhosts": [
{
"id": "private-app",
"enabled": true,
"frontend": {
"hosts": [
"private.localhost"
],
"listener_ids": [
"private-app-http"
]
},
"backends": [
{
"id": "app-a",
"url": "http://127.0.0.1:9001",
"weight": 3,
"enabled": true,
"allow_private": true,
"preserve_host": true,
"protocol": "http1",
"address_allowlist": [
"127.0.0.1/32"
]
},
{
"id": "app-b",
"url": "http://127.0.0.1:9002",
"weight": 1,
"enabled": true,
"allow_private": true,
"preserve_host": true,
"protocol": "http1",
"address_allowlist": [
"127.0.0.1/32"
]
}
],
"load_balancing": {
"algorithm": "adaptive",
"slow_start_seconds": 10,
"health_check": {
"mode": "http",
"path": "/health",
"interval_ms": 5000,
"timeout_ms": 2000,
"slow_response_ms": 1500,
"fall": 3,
"rise": 2,
"expected_status_min": 200,
"expected_status_max": 299
},
"keepalive": {
"enabled": true,
"idle_timeout_seconds": 30,
"max_idle_per_host": 4,
"tcp_keepalive_seconds": 30
},
"sticky": {
"mode": "off",
"ttl_seconds": 3600
}
},
"security_headers": {
"hsts": {
"enabled": false
},
"nosniff": true,
"frame_options": "sameorigin",
"referrer_policy": "strict-origin-when-cross-origin"
},
"headers": {
"response": [
{
"op": "remove",
"name": "x-powered-by"
},
{
"op": "set",
"name": "x-served-by",
"value": "RustProxy"
}
]
},
"routes": [
{
"id": "root",
"path_prefix": "/",
"cache": false
}
]
}
]
}
+34
View File
@@ -0,0 +1,34 @@
# Grafana dashboard
`rustproxy-overview.json` is a provisioning/import dashboard for the dedicated
RustProxy Prometheus exporter. It covers global traffic, status classes, vhost
latency, backend health/latency, a dedicated Docker-vhost section, hot/cold/transient
cache memory, Linux process and cgroup diagnostics, HTTP/2/HTTP/3/TLS/WebSocket
counters, Layer-4 streams, structured-log sink health and configuration inventory.
The **Docker vhosts** row uses only the dedicated `rust_proxy_docker_*` metric
families and includes configured/enabled counts, request/error rates, p95 exchange
latency, backend health/EWMA latency/active work, traffic and probe age. Dashboard
variables **Docker agent** and **Docker service** filter only this section, which keeps
multi-agent installations readable without changing the ordinary vhost panels.
Import the JSON in Grafana and select a Prometheus datasource when prompted. The
dashboard expects RustProxy 0.18.3 metric names and a scrape interval around 15s.
Collectors can be disabled in RustProxy settings; panels for disabled collectors
will simply show no series.
Recommended scrape job:
```yaml
scrape_configs:
- job_name: rustproxy
scrape_interval: 15s
metrics_path: /metrics
static_configs:
- targets: ['rustproxy-host:9191']
```
When the exporter is protected with a bearer token, configure Prometheus
`authorization.credentials_file`. For a private monitoring VLAN, RustProxy can
instead explicitly allow actual peers from configured trusted CIDRs to scrape
without a token. Forwarded client-IP headers never satisfy that trust check.
File diff suppressed because it is too large. Load diff
+138
View File
@@ -0,0 +1,138 @@
#!/usr/bin/env bash
# Debian 13+ one-line bootstrap for first install or safe source update.
# Builds as an unprivileged user, then delegates host changes to install.sh/update.sh.
set -euo pipefail
REPO_URL="${RUSTPROXY_REPO_URL:-https://git.linuxiarz.pl/gru/RustProxy.git}"
ADMIN_LISTEN="${RUSTPROXY_ADMIN_LISTEN:-0.0.0.0:9090}"
PREFIX="${RUSTPROXY_PREFIX:-/opt/rustproxy}"
MODE="${1:-auto}"
case "$MODE" in
auto|install|update) ;;
--install) MODE=install ;;
--update) MODE=update ;;
-h|--help)
cat <<'USAGE'
Usage: install-debian13.sh [auto|install|update]
auto Install when RustProxy is absent; update when /opt/rustproxy/install.json exists (default)
install Require a fresh installation
update Require an existing managed installation and perform a safe update
Environment:
RUSTPROXY_REPO_URL Git repository URL
RUSTPROXY_ADMIN_LISTEN Admin bind used only on first install (default 0.0.0.0:9090)
RUSTPROXY_PREFIX Managed install prefix (default /opt/rustproxy)
RUSTPROXY_BUILD_JOBS Cargo jobs for low-memory builds (default 1)
USAGE
exit 0
;;
*)
echo "Unknown mode: $MODE (expected auto, install or update)." >&2
exit 2
;;
esac
if [[ ${EUID:-$(id -u)} -ne 0 ]]; then
echo "Run this bootstrap as root (for example: wget -qO- ... | bash)." >&2
exit 1
fi
. /etc/os-release
if [[ ${ID:-} != debian ]] || [[ ${VERSION_ID%%.*} -lt 13 ]]; then
echo "This bootstrap supports Debian 13+. Detected: ${PRETTY_NAME:-unknown}." >&2
exit 1
fi
install_record="$PREFIX/install.json"
if [[ "$MODE" == auto ]]; then
if [[ -f "$install_record" ]]; then
MODE=update
elif [[ -e "$PREFIX/current" || -e /etc/systemd/system/rustproxy.service ]]; then
echo "RustProxy artifacts exist but $install_record is missing; refusing automatic overwrite." >&2
echo "Recover the managed install record or use the project migration/recovery procedure." >&2
exit 1
else
MODE=install
fi
fi
if [[ "$MODE" == update && ! -f "$install_record" ]]; then
echo "Update requested, but managed installation record is missing: $install_record" >&2
exit 1
fi
if [[ "$MODE" == install && -e "$install_record" ]]; then
echo "Install requested, but RustProxy is already installed. Use update mode." >&2
exit 1
fi
echo "RustProxy bootstrap mode: $MODE"
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y \
build-essential gcc git ca-certificates python3 pkg-config wget sudo \
python3-certbot-dns-rfc2136 python3-certbot-dns-cloudflare
created_builder=0
if [[ -n ${SUDO_USER:-} && ${SUDO_USER} != root ]] && id "${SUDO_USER}" >/dev/null 2>&1; then
builder=${SUDO_USER}
builder_home=$(getent passwd "$builder" | cut -d: -f6)
else
builder=rustproxy-build
builder_home=/var/tmp/rustproxy-build
if ! id "$builder" >/dev/null 2>&1; then
useradd --create-home --home-dir "$builder_home" --shell /bin/bash "$builder"
created_builder=1
fi
fi
work=$(mktemp -d /var/tmp/rustproxy-source.XXXXXX)
chown "$builder":"$(id -gn "$builder")" "$work"
cleanup() {
rm -rf "$work"
if [[ $created_builder -eq 1 ]]; then
userdel -r "$builder" >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT
run_builder() {
runuser -u "$builder" -- env HOME="$builder_home" PATH="$builder_home/.cargo/bin:/usr/local/bin:/usr/bin:/bin" "$@"
}
if ! run_builder bash -lc 'command -v cargo >/dev/null 2>&1'; then
run_builder bash -lc "wget -qO- --https-only --secure-protocol=TLSv1_2 https://sh.rustup.rs | sh -s -- -y --profile minimal"
fi
run_builder git clone --depth 1 "$REPO_URL" "$work/RustProxy"
# Keep source builds reliable on small Debian VPSes. The repository release
# profile may use LTO/codegen settings that can make rustc exceed RAM limits.
BUILD_JOBS="${RUSTPROXY_BUILD_JOBS:-1}"
if ! [[ "$BUILD_JOBS" =~ ^[1-9][0-9]*$ ]]; then
echo "RUSTPROXY_BUILD_JOBS must be a positive integer." >&2
exit 1
fi
run_builder bash -lc "cd '$work/RustProxy' && CARGO_BUILD_JOBS='$BUILD_JOBS' CARGO_PROFILE_RELEASE_LTO=false CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 cargo build --release --locked"
cd "$work/RustProxy"
if [[ "$MODE" == install ]]; then
bash install.sh \
--binary target/release/rust-proxy \
--prefix "$PREFIX" \
--admin "$ADMIN_LISTEN" \
--allow-remote-admin \
printf '\nRustProxy installed. Admin panel: http://%s\n' "$ADMIN_LISTEN"
printf 'WARNING: remote administration uses plain HTTP. Restrict TCP/9090 with a firewall/private network.\n'
else
# update.sh reads the saved installation record and therefore preserves the
# existing data directory, HTTP/TLS/admin binds and service account.
bash update.sh \
--binary target/release/rust-proxy \
--prefix "$PREFIX"
printf '\nRustProxy updated successfully. Existing configuration and data were preserved.\n'
fi
Executable
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
# Install an already built binary; never download or compile code as root.
set -euo pipefail
ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
exec python3 "$ROOT/scripts/deploy.py" install "$@"
+40
View File
@@ -0,0 +1,40 @@
# Language packs
Add `de_DE.json` (or another safe unique filename) here and rebuild with Cargo.
`build.rs` discovers all `*.json` files; there is no frontend language allowlist.
The binary embeds the bundle at `/assets/langs.json`, which is accessible before
login. No runtime network dependency or external translation service is required.
```json
{
"meta": {
"locale": "de-DE",
"name": "German",
"native_name": "Deutsch",
"direction": "ltr"
},
"translations": {
"overview": "Ubersicht",
"settings": "Einstellungen"
}
}
```
Use UTF-8 and a BCP-47 locale accepted by browser Intl. Filenames may use letters,
numbers, `_` and `-`. `en_US.json` is the required fallback. `pl_pl.json` preserves
the requested filename; its metadata locale is `pl-PL`. Missing keys fall back to
English; an explicitly empty string is retained. Unknown keys are permitted for
future extensions. `rtl` enables right-to-left layout. HTML is never evaluated:
translations and metadata labels are rendered using textContent.
The saved selection is matched by ID or locale. Browser language preferences are
used when no saved language matches. Short legacy selections such as `pl` are
matched through metadata, not language-specific code. Adding or removing a file
triggers a Cargo rebuild. Duplicate locales, invalid metadata, non-string values
and oversized packs cause a build error. Limits: 128 packs, 256 KiB per pack,
4096 keys per pack, 8 MiB total bundle.
Run `python3 scripts/langs_check.py` before a build. The checker also validates
that every static UI translation key has an English fallback. Extra JSON packs survive the managed updater when the new release does not contain
the same filename. Keep a separate backup too; shipped filenames are replaced and
the previous bytes are retained in the source backup.
Loaded 100 of 240 files, more files were not shown because too many files have changed in this diff. Show more