Documentation
¶
Overview ¶
Package ingressproxy implements the loopback HTTP listener that fronts wake-aware Caddy port routes. Caddy rewrites a request hitting {id}-{port}.{domain} to /__ingress/http/{id}/{port}{path} and forwards it to this listener; the handler then wakes the sandbox (if armed) and reverse-proxies to {containerIP}:{port}.
This listener MUST bind to loopback only. It carries no auth — Caddy is the trust boundary.
Index ¶
- Constants
- func RecordAskResult(result string)
- func RecordIssuanceCompleted(hostname string)
- func RecordIssuanceStart(hostname string)
- func RegisterRoutes(mux *http.ServeMux, d Deps)
- func RegisterTLSAsk(mux *http.ServeMux, h *TLSAskHandler)
- type ACMEBudgetReserver
- type CustomDomainResolver
- type Deps
- type IssuanceObserver
- type PortResolver
- type TLSAskDeps
- type TLSAskHandler
Constants ¶
const ( AskResultOK = "ok" // 200 — hostname recognised, Caddy may issue AskResultBaseDomain = "base_domain" // 200 — request is under SB_DOMAIN, served by wildcard policy AskResultUnknown = "unknown" // 403 — hostname not in FSM/store AskResultBadRequest = "bad_request" // 400 — missing/malformed query AskResultResolveError = "resolve_error" // 500 — resolver transient outage AskResultThrottled = "throttled" // 429 — daemon ACME budget exhausted )
Result labels for aerolvm_ask_requests_total. Bounded set — the expvar map key space cannot grow with traffic, only with code paths.
const PathPrefix = "/__ingress/http"
PathPrefix is the URL prefix Caddy rewrites wake-aware HTTP port requests to. The shape is fixed (id / port / remaining path); both the route helper in pkg/caddy and the handlers in this package share it as the single grep target.
const TLSAskPath = "/internal/tls-ask"
TLSAskPath is Caddy's on-demand TLS `ask` callback path. Caddy passes the requested hostname as the `domain` query parameter and decides whether to attempt issuance based on the HTTP status: 2xx → issue, anything else → refuse. The path is fixed so both the Caddy install-time config push and this handler share a single grep target.
Variables ¶
This section is empty.
Functions ¶
func RecordAskResult ¶ added in v0.3.1
func RecordAskResult(result string)
RecordAskResult bumps the per-result counter. Use the AskResult* constants — adding a freeform string would explode the label space.
func RecordIssuanceCompleted ¶ added in v0.3.1
func RecordIssuanceCompleted(hostname string)
RecordIssuanceCompleted records the issuance duration into the histogram and clears the held-seconds gauge. Idempotent — calling for an untracked host is a no-op.
func RecordIssuanceStart ¶ added in v0.3.1
func RecordIssuanceStart(hostname string)
RecordIssuanceStart marks the moment TLSAsk allowed a previously unseen hostname to attempt issuance. Safe to call repeatedly — the first call wins (subsequent are no-ops until Completed/Cancel).
func RegisterRoutes ¶
RegisterRoutes mounts the ingress proxy routes onto mux. The mux is expected to be served on a loopback-only listener (see pkg/api/server wiring in cmd/sandboxd/main.go).
func RegisterTLSAsk ¶ added in v0.3.1
func RegisterTLSAsk(mux *http.ServeMux, h *TLSAskHandler)
RegisterTLSAsk mounts the on-demand TLS `ask` handler on mux. Called only when SB_ENABLE_CUSTOM_DOMAINS=true. Kept separate from RegisterRoutes so the wake-proxy listener stays usable when custom domains are off (and so the caller controls construction of the TLSAskHandler, which the service layer needs to hold a reference to for negative-cache eviction).
Types ¶
type ACMEBudgetReserver ¶ added in v0.3.1
ACMEBudgetReserver is the daemon-wide brake on new ACME orders. A nil implementation is fine: the handler treats nil as "no brake" so the hot path stays one-branch when the feature is off. The handler calls Reserve only when the ask would actually allow issuance — base-domain fast-fail and negative-cache hits don't consume budget.
Returns (true, 0) to permit, (false, retryAfter) to refuse. The retryAfter is surfaced as the HTTP Retry-After header on the 429 response so Caddy backs off coherently with the budget window.
type CustomDomainResolver ¶ added in v0.3.1
type CustomDomainResolver interface {
ResolveCustomDomain(ctx context.Context, hostname string) (string, error)
}
CustomDomainResolver is the narrow lookup the ask handler needs. In PR #2 it is backed by store.ResolveCustomDomain (single PK lookup); PR #3 swaps in the local Raft FSM's hostname → sandbox map so the hot path stays fully in-process across the cluster. The interface intentionally returns store.ErrNotFound on miss so the handler treats both backends the same.
type Deps ¶
type Deps struct {
Resolver PortResolver
Logger *slog.Logger
MaxBufferBytes int64
UpstreamReadyTimeout time.Duration
MaxPendingPerSandbox int
MaxPendingGlobal int
MaxBufferBytesGlobal int64
}
Deps wires the ingress proxy to the rest of the daemon. Resolver is required; MaxBufferBytes caps how much request body is buffered while a cold-start wake is in flight; UpstreamReadyTimeout bounds the post-wake TCP-readiness probe (zero → defaultUpstreamReadyTimeout).
MaxPendingPerSandbox / MaxPendingGlobal / MaxBufferBytesGlobal cap how many cold-start requests can simultaneously hold the wake window and how much memory their buffered bodies may pin. Zero on any of them disables that specific cap (intended for tests; production wiring in cmd/sandboxd/main.go always sets all three).
type IssuanceObserver ¶ added in v0.3.1
type IssuanceObserver interface {
Started(hostname string)
}
IssuanceObserver lets the handler signal the issuance lifecycle without depending on the concrete tracker — kept minimal so tests can stub it without dragging in expvar state.
func DefaultIssuanceTracker ¶ added in v0.3.1
func DefaultIssuanceTracker() IssuanceObserver
DefaultIssuanceTracker returns the process-global tracker that publishes the aerolvm_acme_lock_held_seconds gauge. Wire this into TLSAskDeps.Tracker in production. Tests should build a private tracker via newIssuanceTrackerWithGauge so they don't share state.
type PortResolver ¶
type PortResolver interface {
WakeAwarePortTarget(ctx context.Context, id string, port int) (service.PortEndpoint, error)
TouchSandbox(ctx context.Context, id string) error
IsSandboxStarted(ctx context.Context, id string) (bool, error)
}
PortResolver is the narrow slice of *service.Service the ingress handler needs. Kept as an interface so the package is testable without standing up a real Service + Docker + store stack.
TouchSandbox bumps last_active_at so the lifecycle idle sweep does not stop a sandbox that is currently serving a long-lived request (WebSocket, SSE, long-poll). The handler calls it at request start AND on a 30s ticker for as long as the upstream proxy is running.
IsSandboxStarted is the fast preflight check used to bypass request-body buffering for warm sandboxes. Buffering exists so the body survives the cold-start wait (and to enforce MaxBufferBytes against unbounded uploads during wake), but it has no purpose when the upstream is already running — paying that cost on every warm request would cap normal POST uploads at MaxBufferBytes and add avoidable memory pressure under load. Returns store.ErrNotFound when no sandbox row matches id so the handler can surface 404 without a second store hit.
type TLSAskDeps ¶ added in v0.3.1
type TLSAskDeps struct {
Resolver CustomDomainResolver
BaseDomain string
NegCacheTTL time.Duration
NegCacheCap int
Logger *slog.Logger
// Budget is the daemon-wide ACME issuance brake (OV5A). Nil disables
// the brake — single-node deployments and tests that don't care
// about LE rate limits pass nil and Reserve is skipped entirely.
Budget ACMEBudgetReserver
// Tracker observes the issuance lifecycle for the
// aerolvm_acme_lock_held_seconds gauge. Nil disables tracking. Wired
// to the package-global tracker in production via the helpers in
// metrics.go; tests can substitute a stub.
Tracker IssuanceObserver
}
TLSAskDeps wires the ask handler. BaseDomain is the wildcard zone served out of cfg.Domain — when set, requests for the apex or any subdomain pass through with 200 as defense in depth (Caddy's wildcard policy should already match first, but a stray on-demand fallthrough must never trigger per-host issuance for a hostname covered by the wildcard).
NegCacheTTL and NegCacheCap bound the negative-cache footprint. Zero on either disables the cache (intended for tests).
type TLSAskHandler ¶ added in v0.3.1
type TLSAskHandler struct {
// contains filtered or unexported fields
}
TLSAskHandler answers Caddy's on-demand TLS `ask` callback for custom hostnames. Hot path: PK-style lookup with an LRU negative cache so an SNI flood from an internet-facing scanner does not turn into per-request store traffic.
func NewTLSAskHandler ¶ added in v0.3.1
func NewTLSAskHandler(d TLSAskDeps) *TLSAskHandler
NewTLSAskHandler builds a handler. Resolver and Logger are required; BaseDomain is recommended whenever cfg.Domain is set.
func (*TLSAskHandler) EvictNegativeCache ¶ added in v0.3.1
func (h *TLSAskHandler) EvictNegativeCache(host string)
EvictNegativeCache drops a hostname from the negative cache. The service layer calls this after AddCustomDomain succeeds so a legitimate add does not have to wait out the TTL before Caddy will issue a cert.
func (*TLSAskHandler) ServeHTTP ¶ added in v0.3.1
func (h *TLSAskHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP implements http.Handler. GET /internal/tls-ask?domain=<host>.