v0.34.0_fina;

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-18 12:00:59 +02:00
1 parent 1ca019d849
commit c207e8a03f
69 files changed
+1496 -13838

No files matched your search

+26 -209
View File
@@ -1,223 +1,40 @@
# Transport contract
# HTTP transport, TLS, HTTP/2 and HTTP/3
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.
RustProxy distinguishes **frontend** client protocols from the **upstream** protocol used to contact an application. Selecting HTTP/3 on a listener does not require an HTTP/3 backend.
## Protocol matrix
## Frontend listeners
| 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 |
- **HTTP/1.1:** default plaintext HTTP and standard Upgrade-based WebSocket support where enabled.
- **HTTPS / HTTP/2:** TLS listeners may negotiate HTTP/2 and HTTP/1.1. Settings include H2 idle/connection-drain controls.
- **HTTP/3 (QUIC):** optional UDP transport on an enabled TLS frontend, using the listener certificate and supported H3 settings. The host firewall/NAT must pass UDP on the actual HTTPS port, not only TCP/443. Browsers can fall back to H2/H1 when H3 is unavailable.
- **H2C:** explicit cleartext HTTP/2 capability only where the listener or upstream policy enables it; it is not ordinary browser 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.
Transport settings attach to **named listeners**, and a Vhost must select the appropriate listener IDs. Addresses locked by CLI/systemd startup cannot be rewritten from the panel. Changing a listener bind or startup-owned TLS option may require a restart.
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.
## Backend protocol selection
Since 0.32.4, `preserve_host: true` is supported with `http1`, `auto`, `http2`
and strict `h2`. H1 carries the public Host; on H2 the outgoing `:authority`
follows that Host. The *network* destination and DNS/ACL policy stay pinned to
the configured backend URL. TLS SNI and certificate validation independently use
`tls_server_name` when supplied or the configured origin otherwise.
The client hostname also remains available via `X-Forwarded-Host`. HTTP health
probes are separate H1 requests and may need an explicit `health_check.host`.
| Backend mode | HTTPS origin | Plain HTTP origin |
|---|---|---|
| `http1` | HTTP/1.1 | HTTP/1.1 |
| `auto` | ALPN H2 or H1 | HTTP/1.1 |
| Legacy `http2` | Compatible ALPN H2/H1 | H2C |
| Strict `h2` | Requires H2 | Requires H2C |
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.
WebSockets normally use HTTP/1.1 Upgrade. HTTP/2/HTTP/3 client requests can still reach HTTP/1.1 backends when the selected bridge/path is supported; do not assume the frontend and origin protocol always match. HTTP/2 GOAWAY, H3 stream cancellation and retries are constrained to safe lifecycle cases. Use backend protocol telemetry and logs when diagnosing negotiation.
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.
## TLS and trust
## DNS and upstream TLS
Public HTTPS requires a correct certificate with hostname coverage. Administration TLS is separate. Backend `https://` encrypts traffic, but **upstream certificate verification is opt-in by default** in the current backend model; enable it and specify CA/name where needed. Private/loopback addresses and routing to a local RustProxy listener require explicit risk overrides.
Only explicit HTTP/HTTPS backend origins are accepted; no credentials, path, query
or fragment are allowed. Client requests cannot choose an arbitrary upstream URL.
## Timeouts, request bodies and streaming
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.
Idle/read/connect/response-header timeouts and backend connection pools are configurable. A response may stream after its headers have arrived; response-header metrics are not full-body duration. WebSockets and Streams have separate idle lifetimes and should be tested with the actual application. RustProxy rejects suspicious ambiguous request targets rather than normalizing them into potentially unsafe backend paths.
`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.
## Troubleshooting
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.
- **HTTPS works but H3 does not:** check UDP firewall/NAT, listener H3 enablement, TLS certificate, network middleboxes and actual browser negotiation.
- **502 / protocol error to backend:** verify origin supports selected `auto`, `http2`, `h2` or H2C mode and the correct TLS hostname/verification policy.
- **WebSocket disconnects:** enable websocket forwarding in the Vhost and ensure idle limits/proxy/backend are compatible.
- **Unexpected HTTP→HTTPS redirect:** inspect Vhost `force_ssl`, selected listeners and actual public scheme.
## 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.
Manual entries support two addressing modes: simple filenames are loaded from
`<data-dir>/certs`, while two absolute paths are read in place (for example Certbot
`/etc/letsencrypt/live/<name>/fullchain.pem` plus `privkey.pem`). Mixed relative/absolute
pairs are rejected. The API stores filenames/paths 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. For absolute external paths RustProxy canonicalizes the path again on every reload, intentionally following Certbot `live/` symlinks to the current `archive/` generation before opening the final regular file. 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.
External absolute-path files are not copied into RustProxy full backups. Their path strings remain in configuration; restore or mount the certificate material separately on the destination host before starting TLS. The `rustproxy` service identity must be able to traverse every parent directory and read both files. RustProxy accepts POSIX ACL read access and does not require an exact chmod value. External certificate/key files must not be group/world-writable, and private keys must not be world-readable. The configured and resolved parent chains must be root- or RustProxy-owned and not group/world-writable, except for root-owned sticky ancestors such as `/tmp`. The final file is opened with `O_NOFOLLOW` after path validation.
## 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 | up to 64 per backend origin by default for new vhosts (1..64 configurable), 30-second idle timeout; existing explicit values preserved |
| 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. They also bound each backend Hyper client
generation by request count/age and cap the upstream HTTP/1 adaptive read buffer. Client
generation recycling is local to the backend pool and does not publish a new vhost
snapshot or purge cache. Health probes are separate no-reuse HTTP/1.1 or raw TCP
operations; their concurrency cap is 16. Existing active exchanges are not intentionally
killed by disabling reuse, recycling a generation or draining a member. Config
publication still 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.
For listener configuration see [Configuration](CONFIGURATION.md); for safer deployment see [Security](SECURITY.md).