Files
RustProxy/docs/VHOSTS.md
T
2026-10-05 10:58:50 +02:00

27 KiB

0.7 adds graphical locations, Basic Auth, cumulative IP ACL editing and a WebSocket toggle. See host access and locations.

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. 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, 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. 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.

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.