Files
reverse-proxy/docs/architecture/proxy.md
glm-5.1 7efc142406 Expand architecture: multi-site Phase 1, multi-domain TLS, fix review issues
Promote multi-site support from Phase 2 to Phase 1 (ADR-010): the proxy
must support git.alk.dev and alk.dev from initial release. Add multi-domain
TLS configuration (ADR-011): acme_domains array replaces acme_domain string,
single SAN certificate via rustls-acme.

Key changes:
- ADR-010: Multi-site in Phase 1 — avoids config format migration later
- ADR-011: Multi-domain TLS — single SAN cert, acme_domains Vec<String>
- ADR-002: Updated rationale for multi-site (one upstream per domain)
- overview.md: Phase 1 now includes multi-site, alk.dev pass-through,
  dual licensing (MIT OR Apache-2.0), real IP removed
- config.md: acme_domain → acme_domains, TOML example shows both sites,
  validation adds unique host check, real IP replaced with 203.0.113.10
- tls.md: Multi-domain SNI section moved from Future to current, manual
  mode uses ResolvesServerCert for SNI mapping, TOML header fixed
- proxy.md: Updated for multi-site, removed single-domain language
- operations.md: RFC 5737 documentation IPs, clarified rate limit eviction
  semantics (distinct scan interval vs eviction age)
- open-questions.md: OQ-05 resolved (single bind_addr sufficient), new
  OQ-07 (per-site TLS overrides)

Review fixes:
- acme_domains (plural) consistently used across all docs and diagram
- ADR-011 clearly scopes acme_domain as previous design
- Inline decision rationale extracted: tls.md hot-reload → ADR-004 ref,
  config.md static/dynamic → ADR-008 ref
- TOML section headers consistent (server.tls)
2026-06-11 08:50:03 +00:00

171 lines
6.5 KiB
Markdown

---
status: draft
last_updated: 2026-06-11
---
# Proxy Handler
## What It Is
The proxy handler is the core component that receives an incoming HTTP request
on the TLS-terminated connection, applies middleware (rate limiting, header
injection, body size limits), and forwards it to the upstream service.
## Why It Exists
This component replaces nginx's `proxy_pass` directive. For our use case —
one upstream per domain across multiple domains, no load balancing, no HTTP/2
proxying — a custom handler is simpler and more maintainable than a
general-purpose proxy library (ADR-002, ADR-010).
## Architecture
```
Incoming HTTPS request
┌─────────────────┐
│ axum Router │
│ (Host-based) │─── /health → 200 OK
│ │
│ match Host │
│ header on │
│ incoming req │
└───────┬─────────┘
┌─────────────────┐
│ Rate Limiting │ ← tower middleware layer
│ Middleware │
└───────┬─────────┘
┌─────────────────┐
│ Proxy Header │ ← custom middleware / handler
│ Injection │
│ │
│ X-Real-IP │ ← connect_info remote_addr
│ X-Forwarded-For │ ← append to existing or set
│ X-Forwarded-Proto │ ← "https" (or "http" on port 80)
│ Host │ ← original host header (already set)
└───────┬─────────┘
┌─────────────────┐
│ Body Size Limit │ ← DefaultBodyLimit(100 MB)
│ Middleware │
└───────┬─────────┘
┌─────────────────┐
│ Reverse Proxy │ ← hyper Client request forwarding
│ Handler │
│ │
│ 1. Build upstream│
│ URI from │
│ original req │
│ 2. Forward req │
│ to upstream │
│ 3. Stream │
│ response back │
└─────────────────┘
```
## Request Flow
### 1. Host-Based Routing
The axum router uses a `Host` extractor to match incoming requests to site
definitions from `DynamicConfig`. Each site definition maps a hostname to an
upstream address.
Where `host_based_proxy` reads the `Host` header, looks up the site in
`DynamicConfig.sites`, and either proxies to the upstream or returns 404.
### 2. Proxy Header Injection
Headers are injected before forwarding. The handler reads connection metadata
from axum's `ConnectInfo` and the original request:
| Header | Value Source | Notes |
|--------|-------------|-------|
| `Host` | Original request `Host` header | Already present; preserved as-is |
| `X-Real-IP` | `ConnectInfo<SocketAddr>` remote IP | Set to client's IP address |
| `X-Forwarded-For` | Client IP, appended if header exists | Comma-separated list of proxies |
| `X-Forwarded-Proto` | Determined by listener | `https` on port 443, `http` on port 80 |
The `X-Forwarded-For` handling must append the client IP to any existing value
(rather than replacing it), to support chained proxies correctly.
### 3. Request Forwarding
The proxy handler constructs a new request to the upstream:
1. Build the upstream URI using the site's `upstream_scheme` and `upstream`
address, preserving the original path and query string
2. Copy the request method, headers, and body from the original
3. Inject proxy headers (X-Real-IP, X-Forwarded-For, X-Forwarded-Proto)
4. Send the request via a shared hyper Client instance
5. Stream the response back to the client
The hyper Client is created once at startup and shared via axum's `State`. It
must be configured with:
- Connection pooling (hyper default behavior)
- Connect timeout: 5 seconds
- Request timeout: 60 seconds
- No redirect following (proxies should not follow redirects)
### 4. Error Handling
| Upstream Condition | Response | Notes |
|-------------------|----------|-------|
| Upstream reachable | Stream response as-is | Headers, status, body all forwarded |
| Upstream unreachable | 502 Bad Gateway | Logged at `warn` level |
| Upstream timeout | 504 Gateway Timeout | Logged at `warn` level |
| Request body too large | 413 Payload Too Large | From `DefaultBodyLimit` middleware |
| Rate limit exceeded | 429 Too Many Requests | Logged at `info` level |
| Unknown Host header | 404 Not Found | No matching site definition |
### 5. HTTP → HTTPS Redirect
A separate HTTP listener on port 80 handles redirect. It reads the `Host`
header from the incoming request and returns a 301 Permanent Redirect to the
HTTPS equivalent URL (preserving the path and query string).
This listener runs on the same bind address as the TLS listener but on port 80.
## Upstream Connection
The upstream connection scheme defaults to `http://` since the proxy and backend
services typically run on the same host (e.g., `127.0.0.1:3000`). The
`upstream_scheme` field in each site's configuration allows specifying `https://`
for upstreams that require TLS (e.g., separate hosts or secure internal services).
For the initial deployment, upstream connections use plain HTTP (e.g.,
`git.alk.dev``127.0.0.1:3000`, `alk.dev``127.0.0.1:8080`) since TLS
between the proxy and backend services on loopback is unnecessary.
## Body Size Limit
axum's `DefaultBodyLimit` layer sets the maximum request body size. For
compatibility with Gitea's push operations (large pack files), this defaults
to 100 MB. In Phase 1, the body limit is a global setting; Phase 2 may add
per-site body limits.
## Design Decisions
All design decisions are documented as ADRs in [decisions/](decisions/).
| ADR | Decision | Summary |
|-----|----------|---------|
| [002](decisions/002-custom-proxy-handler.md) | Custom proxy handler | One upstream per domain — simpler than a general proxy library |
| [007](decisions/007-custom-log-format.md) | Custom structured log format | key=value pairs with RATE_LIMIT prefix for fail2ban |
| [010](decisions/010-multi-site-phase1.md) | Multi-site in Phase 1 | Multiple domains from initial release |
## Open Questions
Open questions are tracked in [open-questions.md](open-questions.md). Key
questions affecting this document:
- **OQ-06**: Should upstream timeouts be configurable per-site? (open — Phase 1
uses global defaults of 5s connect, 60s request)