v0.24.5
This commit is contained in:
commit
5e23e9af02
240 files changed
+99957
No files matched your search
@@ -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.
|
||||
@@ -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
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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/
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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/
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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`.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user