Decompose architecture into 23 atomic tasks across 7 parallel generations
Task graph covers all Phase 1 concerns: config system, TLS termination, proxy handler, operations (rate limiting, logging, health check, admin socket, signals, shutdown, body size limit), deployment artifacts, and two review checkpoints. No circular dependencies. Critical path length of 7. Risk distribution: 3 high-risk (ACME, TLS listener setup, startup orchestration), 7 medium, 11 low, 2 trivial.
This commit is contained in:
61
tasks/proxy/error-responses.md
Normal file
61
tasks/proxy/error-responses.md
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
id: proxy/error-responses
|
||||
name: Implement proxy error responses with plain text bodies and correct status codes
|
||||
status: pending
|
||||
depends_on: [proxy/host-routing]
|
||||
scope: single
|
||||
risk: trivial
|
||||
impact: isolated
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the error response types for the proxy handler. All error responses use plain text bodies with no proxy version or identity information. No upstream error details are included.
|
||||
|
||||
### Error Response Table
|
||||
|
||||
| Upstream Condition | Response | Body |
|
||||
|-------------------|----------|------|
|
||||
| Upstream reachable | Stream response as-is | (upstream body) |
|
||||
| Upstream unreachable | 502 Bad Gateway | `Bad Gateway` |
|
||||
| Upstream timeout | 504 Gateway Timeout | `Gateway Timeout` |
|
||||
| Request body too large | 413 Payload Too Large | `Payload Too Large` |
|
||||
| Rate limit exceeded | 429 Too Many Requests | `Too Many Requests` |
|
||||
| Unknown Host header | 404 Not Found | `Not Found` |
|
||||
| Missing Host header | 400 Bad Request | `Bad Request` |
|
||||
|
||||
### Response Format
|
||||
|
||||
- Content-Type: `text/plain; charset=utf-8`
|
||||
- Body: Brief status text matching the HTTP status
|
||||
- No proxy version or identity information
|
||||
- No upstream error details leaked
|
||||
|
||||
### Logging
|
||||
|
||||
- 502 and 504 responses logged at `warn` level with structured fields
|
||||
- 429 responses logged at `info` level with RATE_LIMIT prefix
|
||||
- 404 and 400 responses not specially logged (normal routing)
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Error response type/enum covering all cases in the table
|
||||
- [ ] All error responses use `text/plain; charset=utf-8` Content-Type
|
||||
- [ ] Error bodies are brief status text with no version or identity info
|
||||
- [ ] 502 logged at `warn` level with host and upstream
|
||||
- [ ] 504 logged at `warn` level with host and upstream
|
||||
- [ ] 429 logged at `info` level with RATE_LIMIT prefix
|
||||
- [ ] Unit tests for each error response type
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/proxy.md — error handling section
|
||||
|
||||
## Notes
|
||||
|
||||
> This is a small but important task — correct error responses without information leakage are a security concern. Implementation agents should not add extra detail to error bodies.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
89
tasks/proxy/headers-and-forwarding.md
Normal file
89
tasks/proxy/headers-and-forwarding.md
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
id: proxy/headers-and-forwarding
|
||||
name: Implement proxy header injection, hop-by-hop removal, and request forwarding with hyper Client
|
||||
status: pending
|
||||
depends_on: [proxy/host-routing]
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the core reverse proxy logic: inject proxy headers, remove hop-by-hop headers, and forward requests to the upstream via a shared `hyper::Client`.
|
||||
|
||||
### Proxy Header Injection
|
||||
|
||||
The proxy is an **edge proxy** — it sits directly in front of the internet with no trusted proxies upstream. This means existing `X-Forwarded-For` headers from the client cannot be trusted.
|
||||
|
||||
| Header | Value Source | Behavior |
|
||||
|--------|-------------|----------|
|
||||
| `Host` | Original request `Host` header | Preserved as-is |
|
||||
| `X-Real-IP` | `ConnectInfo<SocketAddr>` remote IP | Set to client's IP address |
|
||||
| `X-Forwarded-For` | `ConnectInfo<SocketAddr>` remote IP | **Replaced**, not appended |
|
||||
| `X-Forwarded-Proto` | Determined by listener port | `https` for `https_port`, `http` for `http_port` |
|
||||
|
||||
### Hop-by-Hop Header Removal
|
||||
|
||||
Remove these headers before forwarding to upstream (RFC 2616 §13.5.1):
|
||||
- `Connection`, `Keep-Alive`, `Proxy-Authorization`, `Proxy-Authenticate`
|
||||
- `TE`, `Trailers`, `Transfer-Encoding`, `Upgrade`
|
||||
|
||||
Also remove these from upstream responses before sending to client.
|
||||
|
||||
### Request Forwarding
|
||||
|
||||
1. Build the upstream URI: `{upstream_scheme}://{upstream}{path}?{query}`
|
||||
2. Copy request method, headers (with proxy headers injected, hop-by-hop removed), and body
|
||||
3. Send via shared `hyper::Client` with per-site timeout overrides
|
||||
4. Stream response back to client (chunk-by-chunk, not buffered)
|
||||
5. Handle client disconnect (log at debug, close upstream connection)
|
||||
6. Handle upstream disconnect (send whatever was already sent, close connection)
|
||||
|
||||
### hyper Client Configuration
|
||||
|
||||
- Created once at startup, shared via axum State
|
||||
- HTTP/1.1 only for upstream connections
|
||||
- No redirect following (proxies should not follow redirects)
|
||||
- Connection pooling (hyper default behavior)
|
||||
- Per-site timeout overrides: `upstream_connect_timeout_secs` (default 5s), `upstream_request_timeout_secs` (default 60s)
|
||||
|
||||
### Upstream Scheme
|
||||
|
||||
Default is `http://`. When `upstream_scheme` is `"https"`, validate the upstream's TLS certificate using the system's native TLS root certificates. Certificate validation failures result in `502 Bad Gateway`.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `X-Real-IP` set from `ConnectInfo<SocketAddr>` remote IP
|
||||
- [ ] `X-Forwarded-For` **replaced** (not appended) with client IP
|
||||
- [ ] `X-Forwarded-Proto` set to `https` or `http` based on listener port
|
||||
- [ ] `Host` header preserved as-is
|
||||
- [ ] Hop-by-hop headers removed before forwarding to upstream
|
||||
- [ ] Hop-by-hop headers removed from upstream response before sending to client
|
||||
- [ ] No `Server` header added to responses
|
||||
- [ ] No `Via` header added in Phase 1
|
||||
- [ ] Request body streamed (not buffered) to upstream
|
||||
- [ ] Response body streamed (not buffered) to client
|
||||
- [ ] Client disconnect logged at debug level, upstream connection closed
|
||||
- [ ] Upstream disconnect: client receives whatever was already sent
|
||||
- [ ] Per-site timeout overrides applied to hyper Client requests
|
||||
- [ ] `upstream_scheme: "https"` validates upstream TLS certificate with system roots
|
||||
- [ ] Shared `hyper::Client` instance via axum State
|
||||
- [ ] Unit tests for header injection and removal
|
||||
- [ ] Integration test: proxy request to upstream, verify headers and response
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/proxy.md — header injection, request forwarding, error handling
|
||||
- docs/architecture/decisions/002-custom-proxy-handler.md — custom handler rationale
|
||||
- docs/architecture/decisions/017-upstream-connection-defaults.md — HTTP/1.1, no redirects
|
||||
- docs/architecture/decisions/021-x-forwarded-for-edge-proxy.md — edge proxy model
|
||||
|
||||
## Notes
|
||||
|
||||
> The `X-Forwarded-For: replace, don't append` behavior is critical. The proxy is the edge — there are no trusted proxies upstream. Existing `X-Forwarded-For` values from the client could be spoofed and must not be trusted.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
56
tasks/proxy/host-routing.md
Normal file
56
tasks/proxy/host-routing.md
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
id: proxy/host-routing
|
||||
name: Implement Host-based routing with global routing table from DynamicConfig
|
||||
status: pending
|
||||
depends_on: [config/dynamic-config]
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the host-based routing that matches incoming requests to site definitions. Sites are defined per-listener in TOML but collected into a single global routing table in `DynamicConfig`.
|
||||
|
||||
### Routing Logic
|
||||
|
||||
1. Check for `/health` path — if matched, return 200 OK with empty body (regardless of Host)
|
||||
2. Extract `Host` header from request
|
||||
3. If no `Host` header, return `400 Bad Request`
|
||||
4. Normalize `Host` to lowercase, strip port component (e.g., `git.alk.dev:443` → `git.alk.dev`)
|
||||
5. Look up normalized host in the global routing table
|
||||
6. If found, forward to the matching `SiteConfig`'s upstream
|
||||
7. If not found, return `404 Not Found`
|
||||
|
||||
### Global Routing Table
|
||||
|
||||
The routing table is a `HashMap<String, SiteConfig>` (or similar) in `DynamicConfig`, built by collecting all sites from all listeners. Hostnames must be unique — validation enforces this.
|
||||
|
||||
The routing table is part of `DynamicConfig` and is swapped atomically on config reload. This means a config reload can add, remove, or change site routing without restarting.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Host-based routing extracts `Host` header and normalizes to lowercase
|
||||
- [ ] Port component stripped from `Host` header before matching
|
||||
- [ ] `/health` path matches regardless of `Host` header, returns 200 OK
|
||||
- [ ] Missing `Host` header returns `400 Bad Request`
|
||||
- [ ] Unknown host returns `404 Not Found`
|
||||
- [ ] Global routing table built from all listeners' site definitions
|
||||
- [ ] Routing table updates atomically on config reload via ArcSwap
|
||||
- [ ] Case-insensitive host matching per RFC 7230 §2.7.3
|
||||
- [ ] Unit tests for host normalization (case, port stripping)
|
||||
- [ ] Unit tests for routing table lookup (match, no match)
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/proxy.md — Host-based routing section
|
||||
- docs/architecture/config.md — DynamicConfig, global routing table
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
Reference in New Issue
Block a user