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

No files matched your search

+214
View File
@@ -0,0 +1,214 @@
# Architecture
## Control plane and forwarding plane
SQLite stores administrator intent, including vhosts/paths, policies, backend definitions
and certificate metadata. `config::compile` validates this document and builds an
immutable Snapshot with routing tables, compiled ACL/header policies, per-backend
clients and an optional rustls ServerConfig. ArcSwap publishes the active snapshot.
No public request consults SQLite or a provider to choose its route.
A configuration mutation obtains a bounded admission slot and builds a candidate on
a blocking worker. The slot remains held if the HTTP caller is cancelled while the
worker is running. A serialized detached writer checks the expected revision,
commits the desired document in SQLite and swaps the active snapshot with no await
between commit completion and publication. Invalid candidates leave the old state
active. A crash between persistence and publication is recovered by loading the
committed document at startup; this is not one cross-memory/SQLite transaction.
## Module responsibilities
| Module | Responsibility |
|---|---|
| `server` | Bounded listeners, TLS acceptance, HTTP/1.1/HTTP/2 selection, connection leases and shutdown |
| `proxy` | Host/authority routing, ACL sequencing, forwarding and streaming instrumentation |
| `proxy::websocket` | Handshake validation, upgrade tasks and bounded bidirectional byte transport |
| `upstream` | Per-backend pools, guarded resolver, connection deadlines and verified HTTPS clients |
| `tls` | Local PEM files, generation/import, validation, SNI and loaded certificate snapshots |
| `config` | Desired models, normalization, validation and compiled snapshots |
| `vhost` | Host-owned frontend/member models, actual-address selection and listener planning |
| `balance` | Full-exchange leases, health/recovery, weighted scheduling, connection/affinity settings |
| `security` | Compiled per-vhost response protection, TLS-only HSTS |
| `acl`, `headers` | Generic access and header policies, no application-specific exceptions |
| `auth`, `admin` | Administrator lifecycle, local API, browser sessions, CSRF, SSE and static UI |
| `db` | SQLite schema, migrations, desired-state persistence and revision checks |
| `metrics`, `logging` | Aggregate instrumentation, bounded access ring and trace output |
| `transform` | Bounded decode/transform stage for eligible static identity representations before cache/compression |
| `docker` | Desired Docker-agent settings, in-memory observations and ephemeral vhost reconciliation |
| `runtime` | Shared process state, snapshot updates, admission limits and tunnel tracking |
| `demo` | A separate opt-in demonstration backend with its own listener |
Docker discovery is an optional provider boundary implemented through the standalone
`docker-agent/`; the RustProxy process never opens the Docker socket. Cloudflare Tunnel
remains a future module. Certbot-based HTTP-01/Cloudflare DNS/OVH DNS/RFC2136 issuance and managed renewal are optional
control-plane adapters. Provider work stays outside proxy request paths and
publishes certificates only through validated configuration snapshots.
## Request lifetime
A listener owns a shared TCP connection lease. Each admitted request owns an
in-flight permit and an Exchange, which remains alive through streamed request and
response bodies. HTTP/2 connections share the public TCP cap with HTTP/1.1 and TLS.
Upgraded WebSocket tasks retain both the Exchange and TCP lease after Hyper stops
managing the HTTP connection, preventing upgrades from escaping limits or counters.
Routing captures one snapshot. Socket peer identity, host and route ACLs are checked
before DNS or upstream connection reuse. Forwarding uses a URI built from a stored
backend origin; the public client never supplies an arbitrary destination. Header
rules cannot change the identity already used by ACL. Ordinary bodies pass through
without full aggregation. For an explicitly transform-enabled vhost, eligible bounded
static responses use the logical pipeline `origin -> decode supported Content-Encoding
-> transform -> cache identity representation -> downstream compression`. Origin
`no-transform` is authoritative; changed bytes receive a RustProxy representation ETag
while origin validators are retained separately for revalidation. WebSocket data uses
fixed-size copy buffers.
New requests on existing keep-alive/HTTP/2 connections load the current snapshot.
Existing bodies and tunnels retain the old snapshot/backend. Each accepted TLS
connection captures the current certificate configuration before its handshake.
Certificate validation and file reads are never performed inside the SNI resolver.
## Startup and shutdown
Startup opens/migrates SQLite, loads existing intent, prepares required certificates,
compiles the snapshot and binds the requested listeners. The demo only seeds a new
database. A corrupt configuration is not automatically reset. After the runtime is published,
the optional Docker poll loop contacts enabled agents; failures remain observations and do not reset desired configuration or block ordinary proxy service.
Shutdown cancels listeners and tracked tunnels, drains admitted HTTP connections
within the configured grace period, drains accepted configuration writers and
closes SQLite. Bounded runtime shutdown avoids waiting forever for cancelled OS DNS
workers. This does not turn a blocking getaddrinfo call into a killable operation.
## Deliberate development tradeoffs
The panel remains loopback HTTP. All active configuration changes currently rebuild
backend pools and certificate state, even if unrelated fields changed. Compatible health/counter/affinity observations are retained independently; idle
connection and certificate reuse optimization still requires measurements. Backend loop protection checks local listener addresses by default while allowing remote origins on any valid port. A per-backend `accept_local_listener_risk` opt-in can deliberately allow a local RustProxy listener (for example port 9090) without bypassing private/special-address policy. DNS special-range restrictions
are basic safeguards rather than an independently audited network policy engine.
No performance target has been established or measured for this package.
## 0.1.0 response pipeline and resources
Observe pins one immutable snapshot for the exchange. Host/route ACLs precede the
cache and all upstream I/O. Eligible GET lookup uses effective request headers
and a revisioned representation key. On a miss the origin response is validated
before and after global/route header rules; a bounded tee records complete bodies
while still yielding frames immediately. No request-path SQLite/provider lookup is
introduced. Server policy and optional streaming compression apply after cache
lookup/fill wrapping. The outer MeteredBody counts delivered encoded payload frames.
Cache publication uses a short mutex section and LRU/byte/entry bounds; fill buffers
have eight semaphore slots. Purge increments a generation, preventing older in-flight
fills from publishing. Config commit invalidates cache before snapshot publication,
without awaiting between those operations. New listener intent is persisted but
active sockets remain unchanged until restart; original CLI defaults are retained
separately so clearing an override has accurate desired-state reporting.
`cache`, `transform`, `compression`, `settings`, `diagnostics`, and `system_stats` are dedicated modules. `diagnostics` owns periodic sampling/export while `system_stats` adapts the shared `systemstat` host API, `sysinfo::Networks` network API, and one `metrics-process` process API. Network inventory/counters use `sysinfo` as the primary source and merge `systemstat` as a secondary library source, so missing/partial primary inventory is supplemented without shell commands; no BSD/Linux shell-command network parser is maintained. `metrics-process` uses its documented dummy fallback where no native process collector exists, so unsupported fields remain absent without adding another platform parser. Linux adds its compatibility-only `/proc`/cgroup fields after the common sample. The
sampler runs blocking OS/library reads off the async request path roughly every two
seconds and stops through the process cancellation token. Language discovery runs
in build.rs, not in browser hardcoded PL/EN logic. The result is an embedded public
JSON asset; existing logo resources remain intact.
## Vhost pool publication and observations
The immutable routing snapshot contains actual-address-constrained routes, compiled
frontends, pools and both TLS ALPN configurations. The process separately retains
its actually bound listener plan. New routing uses desired bindings immediately;
new sockets/ALPN take effect only at restart. This can temporarily deny moved hosts,
but never silently broadens them to still-active old bindings.
Each pool owns immutable member definitions and clients. Small synchronized health,
scheduling and traffic observations live separately. Compatible publication reuses
observations/tokens by vhost/member/origin/health identity; changed or re-enabled
members are quarantined. Request leases retain the old member/client until both
body directions or the upgraded tunnel finish. Changes do not replay requests.
A weak-App, cancellation-aware supervisor schedules a rotating, bounded window of
probes outside the data path. DNS and configured-origin policies are shared with
normal forwarding; HTTP checks use a dedicated connection and discard the body.
Traffic sampling and status snapshots operate on these observations, not SQLite.
Forwarding order is actual frontend selection -> host/vhost/path ACL -> eligible
member selection -> member-isolated cache lookup -> guarded upstream request.
Cookie affinity bypasses cache. Cache hits release the selection without recording
an actual upstream request. Response protection is applied after cache selection,
and per-vhost HSTS is removed on plaintext, including synthetic responses.
## Integrations in 0.4.0
`forwarding` compiles the explicit trust boundary and reconstructs canonical
metadata. `tinyauth` owns immutable verifier configuration and an outbound client;
it authorizes before the existing origin/cache path. `http3` adapts bounded Quinn
streams to the same Axum router and response Body without a separate cache/auth
implementation. Named listener IDs resolve to actual bindings while compiling
vhosts; accepted destination addresses still control route visibility.
TLS and QUIC use the same validated certificate resolver. Active endpoint limits
remain fixed until restart; new QUIC handshakes can use the latest certificate
snapshot without changing Retry token keys. Graceful shutdown signals H3 GOAWAY
and bounds drain before endpoint close. Runtime conformance remains to be tested.
## 0.5.0 cache inspection and certificate jobs
Cache decision counters and entry metadata live in the bounded cache store. A
separate authenticated paginated read API returns metadata, not bodies, queries or
request headers. Expiry cleanup also runs during inspection; generation barriers
still prevent old in-flight fills from republishing after purge/config changes.
The ACME manager is outside proxy request paths: one job semaphore, a tracked
child-process group, JSON over stdin, sanitized status and private Certbot/RFC2136
profiles. Only validated immutable certificate metadata can enter the existing
library through explicit revision CAS publication. See ACME_RFC2136.md for the
required external tools and unverified integration boundary; RENEWAL.md documents
the 0.7 scheduler and automated publication.
## 0.6 boundaries
`legacy` is an input-only migration module, not a traffic dispatcher. It emits the
same canonical vhost model used by the UI. Validated startup migration uses one
CAS write; failure before validation completion leaves the stored JSON untouched.
`acme` manages one private worker job; `scripts/acme_provider.py` adapts Certbot
webroot/Cloudflare/OVH/RFC2136. Only the running HTTP-01 job's names and token files
are exposed by the reserved challenge middleware. Provider files remain private,
not part of SQLite or generic configuration exports. Issuance/import are separate.
`prometheus` owns a dedicated read-only HTTP router, actual-peer CIDRs, environment
bearer token and bounded scrape work. It does not share administrator cookies.
Instrumentation is process-local; per-vhost counters reuse stable-ID observations
across configuration changes. Domain cache gauges intentionally reset with revision.
Admin request timing measures response headers, while proxy/vhost timing measures
complete body/tunnel lifetime. Neither renderer queries a provider or SQLite to
handle public proxy requests.
## 0.7 renewal, access and deployment boundaries
A shutdown-tracked weak-reference scheduler selects due configured managed
certificates. It shares the issuance semaphore and per-profile advisory lock.
Provider work remains outside request paths. Publication validates PEM/key/names
and the complete current configuration, then uses the same SQLite CAS/snapshot
path. Only changed generations publish; failures do not touch the active snapshot.
TCP and QUIC handshakes consult current TLS state. Certificate publication also
uses the existing cache-generation invalidation. See RENEWAL.md for timing/limits.
Basic Auth is a separate vhost gate after cumulative IP ACLs and before static
responses, cache and upstream selection. Argon2id verification is bounded by a
shared semaphore held by blocking workers and an IP failure table. Hashes, not
passwords, are persisted. A verified Authorization header is optional and reapplied
after custom request headers; unverified identity headers are removed. Protected
responses bypass the shared cache and authentication failures cannot become custom
200 responses. WebSocket admission uses the same gates before opening a tunnel.
`pool_eligible=false` distinguishes a location-pinned backend from ordinary pool
membership. The GUI edits the same prefix route model as the configuration API;
there is no second nginx-style parser or unvalidated location execution path.
Server deployment uses root-owned immutable release directories, an atomic current
symlink, a non-root systemd service and private service-owned data. A stopped-state
backup precedes update publication. Failed health/version checks attempt to restore
both old code and data; backups and failed-generation state remain for recovery.
Live systemd/database recovery acceptance is not replaced by helper mocks.