Files
RustProxy/docs/VHOSTS.md
T
2026-10-13 22:54:52 +02:00

483 lines
33 KiB
Markdown

> 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
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`, `source`, `backends`, `load_balancing`,
`routes`, optional `acl`, `headers` and `security_headers`. `source` defaults to Reverse Proxy
for older documents; it can instead reference an independently configured Web Service. See
[WEB_SERVER.md](WEB_SERVER.md). 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.
## Internal error routing
The main vhost `source` may define `error_fallback`, and each Custom location may do the same. A fallback references the `id` of an enabled `internal` Custom location in the same vhost. Supported statuses are `403`, `404`, `500`, `502`, `503` and `504`; omitted status lists default to `404`. Internal targets are not reachable by ordinary path matching.
Configured upstream/Web Server failures are intercepted before final response/cache publication. The original request method, URI/query and context are internally dispatched to the target, which then follows normal vhost/location processing. Validation rejects fallback cycles and runtime dispatch is depth-limited. See [Custom Locations](CUSTOM_LOCATIONS.md) for the active internal-fallback behavior.
## Per-backend health monitoring (0.29.7)
Each backend has an independent `health_check_enabled` switch (default `true`).
Setting it to `false` excludes that member from both active health probes and
passive health ejection, **without** disabling the member for a named location.
This is useful for an internal 404 handler configured with `pool_eligible: false`
and a location pinned through `backend_id`: the 404 handler never receives
ordinary balanced traffic and does not need health monitoring.
A backend may also define `health_check` with the same fields as
`load_balancing.health_check` to use its own TCP/HTTP/off probe policy, timing,
path, Host and status ranges. Omitting it inherits the vhost policy. A custom
HTTP health check without `host` inherits the vhost Host default even when the
vhost's main health check is TCP. When monitoring is disabled, the backend is
assumed available; connection errors during real requests are still counted and
logged, so monitoring off is not a guarantee of backend reachability.
## 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.
`www_redirect` defaults to false. When enabled, `www_redirect_target` selects an exact DNS hostname from `frontend.hosts` as canonical (default: the first hostname for backwards compatibility). RustProxy also claims its direct companion (`example.com` <-> `www.example.com`) on the same listeners and returns `308 Permanent Redirect` from that companion to the first hostname while preserving path/query. Wildcards and IP literals cannot be the canonical hostname for this feature. The companion also participates in vhost certificate assignment, but the selected certificate must still contain both DNS names in its SANs. When `force_ssl` is also enabled, an HTTP companion request redirects directly to the HTTPS canonical URL instead of creating two redirect hops.
`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`, match fields, `enabled`, `source`, nullable `cache`,
optional `acl_profile_id`, inline `acl`, `country_access`, `method_policy`, `headers` and `backend_id`. `source` selects Reverse Proxy or a reusable Web Service;
`backend_id` is valid only for Reverse Proxy. An empty array leaves the main vhost source as the fallback. 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 method policy, reusable ACL profiles, vhost ACL/country policy and route method/profile/inline
ACL/country policies are cumulative before pool selection/DNS/cache. A vhost and each route can
attach a reusable `acl_profile_id`; profiles may contain an IP ACL plus a country block/allow-only
rule. Inline controls remain independent and cannot be weakened by a profile. Method-policy denial
returns 405; allow-list mode also emits `Allow`. 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 |
| `pool_recycle_requests` | 25000 | 0 (off) or 1000..10000000 |
| `pool_recycle_seconds` | 3600 | 0 (off) or 60..604800 |
| `http1_max_buffer_bytes` | 131072 | 8192..1048576 |
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. `pool_recycle_requests` and `pool_recycle_seconds` bound the lifetime
of each backend Hyper client generation. Recycling swaps only that backend client/pool;
it does not reload the vhost, purge cache or intentionally abort active exchanges. The
request counter is per backend client generation, not per TCP connection. Setting either
recycle value to `0` disables that trigger independently. `http1_max_buffer_bytes` bounds
Hyper's adaptive upstream HTTP/1 read buffer per connection; it does not apply to HTTP/2.
Compatible backend clients are retained across unrelated runtime snapshot rebuilds, so
generation age, request count and rotation totals remain continuous. A changed backend
origin, address policy, keepalive settings or connect timeout creates a fresh client
generation state instead of reusing an incompatible pool.
Disabling reuse sets the idle pool limit to zero and disables keepalive probes. 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.
Backend upstream `protocol` has four modes:
| UI mode | Serialized value | Backend behavior |
|---|---|---|
| **HTTP/1.1** | `http1` | HTTP/1.1 only over HTTPS or HTTP; default for new backends |
| **Auto** | `auto` | HTTPS: prefer H2 and negotiate H1 fallback with ALPN before sending a request; HTTP: H1 only, without H2C probing |
| **HTTP/2 compatible** | `http2` | **Unchanged 0.32.2 behavior**: HTTPS offers H2/H1 via ALPN; plaintext HTTP uses H2C prior knowledge (no H1 downgrade on HTTP) |
| **H2 only** | `h2` | HTTPS: require H2 via ALPN; reject H1-only peers. HTTP: prior-knowledge H2C, no fallback |
**0.32.2 -> 0.32.3 compatibility:** saved `"protocol": "http2"` retains its existing
HTTPS ALPN fallback and cleartext H2C behavior. No conversion, migration or
administrator action is needed to preserve the old selection. Choose the new
`"protocol": "h2"` only when H2 must be enforced. The editor describes the
transport for the selected backend URL; a descriptive notice next to compatible
HTTPS `http2` is informational, not a migration warning.
HTTPS Auto negotiation selects the wire protocol **before sending the request**.
RustProxy does not replay an ambiguous failed POST/PATCH/DELETE through H1, and
does not interpret backend 4xx/5xx responses as a reason to switch protocols.
Existing pool reuse, keep-alive, client generation rotation and the independent
HTTP/1.1 WebSocket Upgrade / H2 Extended CONNECT paths are retained.
HTTP health probes continue to use HTTP/1.1 independently of ordinary requests;
H2C-only peers can use TCP probes. A health probe that needs a virtual-host
name must configure `health_check.host` separately; `preserve_host` affects
visitor requests, not independent health probes.
**Since 0.32.4:** all four upstream modes allow `preserve_host: true`.
With HTTP/2, `:authority` follows the selected HTTP Host instead of the backend
IP; with HTTP/1.1 it remains the regular Host. TCP/DNS always target the
configured backend URL and still enforce the address and local-listener
policies; `tls_server_name` independently controls HTTPS SNI and certificate
identity, not the visitor Host. HTTPS backend certificate verification defaults
to OFF when `tls_verify` is omitted. Turn on verification and configure
`tls_server_name` / `tls_ca_pem` for a private DNS certificate; a valid IP-SAN
certificate can instead authenticate an IP backend without a DNS name.
The form's HTTP/1.1 troubleshooting preset is optional; it is not required
for Auto/H2, private origins or Host preservation.
## 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; otherwise the first exact vhost DNS hostname is used |
| `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. When `health_check.host` is empty, RustProxy automatically uses the first exact DNS hostname configured on the vhost so an IP-address backend can still route the probe through its name-based virtual host. If no exact DNS hostname exists, the existing backend-host fallback remains. Set `health_check.host` only when the probe must use a different Host value. 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.
The **Compression** tab inherits the enclosing/global policy by default. It can enable/disable dynamic compression and independently inherit/enable/disable zstd, Brotli, gzip and deflate. Selecting **Disabled** hides and disables the codec selectors while preserving their saved values for a later Inherit/Enabled selection. Global **Settings -> Response** codec switches are the hard availability ceiling, so a vhost cannot activate a codec disabled globally; `min_bytes` also remains global. Client `q` values are honored; equal-quality codecs use `zstd -> br -> gzip -> deflate`, and an existing `Content-Encoding` is never recompressed.
## 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. The static/maintenance response can set an explicit HTTP status independently of its HTML body, so an operator can serve a maintenance document as `503 Service Unavailable` instead of returning `200`. 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`.
Multiple explicit hostnames can belong to a single vhost: `suba.local` and `subb.local` share the same routing, backends and configuration. Only the www/non-www companion of the selected canonical hostname redirects; other aliases continue serving traffic directly.