caddy

package
v0.7.21 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 21, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func InFluxPortRouteID

func InFluxPortRouteID(id string, port int) string

func InFluxSandboxRouteID

func InFluxSandboxRouteID(id string) string

func IngressCustomDomainHTTPRouteID

func IngressCustomDomainHTTPRouteID(sandboxID, hostname string) string

IngressCustomDomainHTTPRouteID is the stable @id for the per-custom- hostname HTTP route installed on the owner node. Mirrors the SNI variant's naming so reconcile / GC code can derive both from the same (sandboxID, hostname) pair. The hostname is fnv64-hashed so very long hostnames stay within Caddy's @id size budget and the slug avoids characters that would have to be escaped.

func IngressCustomDomainSNIRouteID

func IngressCustomDomainSNIRouteID(sandboxID, hostname string) string

IngressCustomDomainSNIRouteID is the stable route ID for the per-custom- hostname SNI passthrough route on non-owner ingress nodes (cluster mode, domain mode). The hostname is hashed so the ID stays within Caddy's route-ID size budget for very long custom hostnames and avoids embedding punctuation that would have to be escaped in the @id slug. The pair (sandboxID, hostname) yields a deterministic ID so the delta-driven ingress reconciler can recognize an existing route and avoid churn.

func IngressPortSNIRouteID

func IngressPortSNIRouteID(id string, port int) string

func IngressSandboxSNIRouteID

func IngressSandboxSNIRouteID(id string) string

func PortRouteID

func PortRouteID(id string, port int) string

func SandboxRouteID

func SandboxRouteID(id string) string

IDs exposed for the zombie GC to add to its keep-set.

func WakePortRouteID

func WakePortRouteID(id string, port int) string

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

func New

func New(cfg config.Config) *Client

func (*Client) DeleteCustomDomainHTTPRoute

func (c *Client) DeleteCustomDomainHTTPRoute(ctx context.Context, sandboxID, hostname string) error

DeleteCustomDomainHTTPRoute removes the per-hostname HTTP route. 404 is a no-op so the detach path is safe to retry and reconcile-driven GC can call it without first checking existence.

func (*Client) DeleteInFluxPortRoute

func (c *Client) DeleteInFluxPortRoute(ctx context.Context, id string, port int) error

DeleteInFluxPortRoute mirrors DeleteInFluxSandboxRoute for per-port URLs.

func (*Client) DeleteInFluxSandboxRoute

func (c *Client) DeleteInFluxSandboxRoute(ctx context.Context, id string) error

DeleteInFluxSandboxRoute drops the in-flux 503 route once the live route is back. 404 is treated as success.

func (*Client) DeletePortRoute

func (c *Client) DeletePortRoute(ctx context.Context, id string, port int) error

func (*Client) DeleteRouteByID

func (c *Client) DeleteRouteByID(ctx context.Context, routeID string) error

DeleteRouteByID is the zombie-GC entry point: the reconcile sweep finds an @id under apps/http or the tls-mux server that doesn't correspond to any sandbox row, and calls this to drop it. Wraps the same DELETE /id/<routeID> the typed helpers use, so 404 is still treated as success.

func (*Client) DeleteSandboxRoute

func (c *Client) DeleteSandboxRoute(ctx context.Context, id string) error

func (*Client) DeleteTCPRoute

func (c *Client) DeleteTCPRoute(ctx context.Context, hostPort int) error

DeleteTCPRoute removes the layer4 server holding hostPort. 404 is treated as success — the desired post-condition is "not present", and it isn't.

func (*Client) DeleteTCPServer

func (c *Client) DeleteTCPServer(ctx context.Context, serverID string) error

DeleteTCPServer drops a layer4 server by its name (e.g. tcp-port-37412). Used by reconcile's zombie GC; not tied to a specific sandbox/port pair so the caller doesn't have to know which exposure originally owned the port.

func (*Client) DeleteTLSSNIRoute

func (c *Client) DeleteTLSSNIRoute(ctx context.Context, id string, port int) error

DeleteTLSSNIRoute removes one SNI route by @id. 404 is treated as success for the same reason DeleteSandboxRoute does.

func (*Client) DeleteWakeHTTPPortRoute

func (c *Client) DeleteWakeHTTPPortRoute(ctx context.Context, id string, port int) error

DeleteWakeHTTPPortRoute removes the wake-aware route. 404 is a no-op.

func (*Client) Enabled

func (c *Client) Enabled() bool

func (*Client) EnsureLayer4

func (c *Client) EnsureLayer4(ctx context.Context, tlsListen, tlsFallback string) error

EnsureLayer4 idempotently bootstraps the layer4 app and (when tlsListen is non-empty) the shared SNI-mux server. Safe to call on every sandboxd start — the admin API treats a no-op POST as 200 and a PATCH on a missing key as 404, so we issue a PUT only when the path actually doesn't exist.

Without this bootstrap, the very first UpsertTCPRoute would fail because /config/apps/layer4 doesn't exist yet on a fresh Caddy.

When tlsListen is non-empty, tlsFallback must point at the local HTTPS listener that owned the same port before caddy-l4 took it over (the API site, the on-demand-TLS catch-all, etc.). caddy-l4 routes by SNI for sandbox subdomains and forwards the rest of the traffic — including ACME HTTP-01 cert validation that piggy-backs on :443 ALPN and any non-sandbox hostname — to the fallback. Empty tlsFallback with non-empty tlsListen is rejected; the service layer surfaces it as a config error at boot.

func (*Client) EnsureOnDemandTLS

func (c *Client) EnsureOnDemandTLS(ctx context.Context, askURL string, burst int, interval time.Duration) error

EnsureOnDemandTLS idempotently installs the on-demand TLS automation settings used by the custom-domains feature. Safe to call on every sandboxd start: the two writes target leaf paths and replace prior values at those paths only, leaving any pre-existing wildcard policy and other automation knobs untouched.

  • PUT /config/apps/tls/automation/on_demand → ask URL. Replacing the leaf on every boot makes handler URL changes take effect without a manual Caddy reload. The daemon-side ACME budget performs issuance throttling; the deployed Caddy schema rejects the older `rate_limit` field here.
  • POST /config/apps/tls/automation/policies (with a catch-all policy {on_demand: true, issuers: [acme]}) only when no on-demand policy is present yet. Discovered by checking pathExists on automation/policies and re-walking the list; we never insert a duplicate.

askURL must be reachable from Caddy — typically http://127.0.0.1:<api-port>/internal/tls-ask (the InternalIngressAddr listener mounts the handler via RegisterTLSAsk).

The on-demand policy MUST land at the END of the policies list so the existing wildcard policy (matching `*.$DOMAIN`) wins first; otherwise Caddy attempts per-host issuance for hostnames the wildcard already covers, burning the Let's Encrypt quota. POST appends, which gives us that ordering when policies already contains the wildcard.

func (*Client) L4TLSFallback

func (c *Client) L4TLSFallback() string

L4TLSFallback returns the address caddy-l4 forwards non-sandbox SNI to (the regular HTTPS Caddy site that handles the API, on-demand TLS, and the 404 catch-all). Meaningful only when L4TLSListen is non-empty.

func (*Client) L4TLSListen

func (c *Client) L4TLSListen() string

L4TLSListen returns the listen address configured for the shared TLS-SNI multiplexer. Empty means TLS-SNI exposure is disabled — the service layer should reject protocol="tls" requests and skip EnsureLayer4 of the mux.

func (*Client) Ping

func (c *Client) Ping(ctx context.Context) error

func (*Client) PortPublicURL

func (c *Client) PortPublicURL(id string, port int) string

func (*Client) PublicHost

func (c *Client) PublicHost() string

PublicHost returns the dial target for raw-TCP exposures. In domain mode the base domain is returned so TCP URLs are consistent with HTTP sandbox URLs; otherwise falls back to the configured publicHost IP.

func (*Client) SNIHost

func (c *Client) SNIHost(id string, port int) string

SNIHost is the per-sandbox subdomain caddy-l4 routes by for a TLS exposure. Returns empty in IP mode (no domain configured), which the service layer uses to detect "TLS not supported in this deployment" without sniffing.

func (*Client) SandboxPublicURL

func (c *Client) SandboxPublicURL(id string) string

func (*Client) Snapshot

func (c *Client) Snapshot(ctx context.Context) (Snapshot, error)

func (*Client) TCPPublicEndpoint

func (c *Client) TCPPublicEndpoint(hostPort int) string

TCPPublicEndpoint returns the URL clients dial for a raw TCP exposure allocated at hostPort. In domain mode the base domain is used as the dial target (e.g. tcp://sandbox.aerol.cloud:22534); otherwise falls back to publicHost.

func (*Client) TLSPublicEndpoint

func (c *Client) TLSPublicEndpoint(id string, port int, l4Listen string) string

TLSPublicEndpoint returns the dial target for a TLS-SNI multiplexed exposure. The host portion is the per-sandbox subdomain caddy-l4 uses for SNI matching (so the certificate the upstream presents must cover that name); the port is the layer4 listener address operators configured. Empty l4Listen means TLS-SNI mode is disabled.

func (*Client) UpsertCustomDomainHTTPRouteWithDial

func (c *Client) UpsertCustomDomainHTTPRouteWithDial(ctx context.Context, sandboxID, hostname, dial string, opts ...HTTPRouteOptions) error

UpsertCustomDomainHTTPRouteWithDial installs (or replaces) the per-hostname HTTP route for a single (sandbox, custom hostname) using an explicit upstream dial target. WASM sandboxes use this to point at the host HTTP mediator rather than containerIP:guestPort.

func (*Client) UpsertInFluxPortRoute

func (c *Client) UpsertInFluxPortRoute(ctx context.Context, id string, port int) error

UpsertInFluxPortRoute mirrors UpsertInFluxSandboxRoute for per-port URLs.

func (*Client) UpsertInFluxSandboxRoute

func (c *Client) UpsertInFluxSandboxRoute(ctx context.Context, id string) error

UpsertInFluxSandboxRoute installs an HTTP route for the sandbox's public hostname/path that responds with 503 Service Unavailable + Retry-After: 2. Used by the cluster-ingress reconciler when a placement is orphaned or when the owner's data-plane host hasn't gossiped yet — the alternative is the Caddy fallback 404, which clients can't tell apart from "sandbox does not exist". The route has its own @id namespace (suffix "-in-flux") so it can coexist with a live route during transitions; the reconciler is responsible for deleting the live route before installing the in-flux route, and vice versa.

In domain mode the L4 SNI mux falls through to the local HTTPS listener for any SNI it doesn't recognize, so an HTTP route registered for the sandbox hostname captures in-flux traffic without needing a separate L4 route.

func (*Client) UpsertPortRoute

func (c *Client) UpsertPortRoute(ctx context.Context, id, containerIP string, port int, opts ...HTTPRouteOptions) error

func (*Client) UpsertPortRouteToPeer

func (c *Client) UpsertPortRouteToPeer(ctx context.Context, id string, port int, peerHost string) error

UpsertPortRouteToPeer installs the IP/path-mode per-port route for a sandbox owned by another node. In domain mode this is handled by SNI pass-through routes in the layer4 mux.

func (*Client) UpsertPortRouteWithDial

func (c *Client) UpsertPortRouteWithDial(ctx context.Context, id string, guestPort int, dial string, opts ...HTTPRouteOptions) error

UpsertPortRouteWithDial installs a per-port HTTP route whose public hostname uses guestPort but dials an explicit upstream (used by WASM host-mediated listeners where the loopback port differs from the guest port).

func (*Client) UpsertPortRouteWithRetry

func (c *Client) UpsertPortRouteWithRetry(ctx context.Context, id, containerIP string, port int, tryDuration time.Duration, opts ...HTTPRouteOptions) error

UpsertPortRouteWithRetry is UpsertPortRoute plus a load_balancing.try_duration / try_interval window. Used by the HTTP-wake direct-bypass path (plans/warm-direct-route-bypass.md C5): when a warm sandbox container dies, the docker `die` event takes ~10ms to propagate before the route flips back to wake-aware. During that window Caddy may already be holding the direct route pointing at a now-dead IP. The retry window absorbs the gap by retrying the dial for tryDuration before failing — outside the window, Caddy returns 502 just like any direct-route service.

tryDuration must be > 0; tryDuration <= 0 means "behave like the no-retry UpsertPortRoute" and the caller should use that method instead. UpsertPortRoute is kept byte-for-byte identical so the non-serverless surface does not inherit retry semantics it never asked for.

func (*Client) UpsertSNIPassthroughRoute

func (c *Client) UpsertSNIPassthroughRoute(ctx context.Context, routeID, sniHost, peerHost string, peerPort int) error

UpsertSNIPassthroughRoute publishes a layer4 SNI route that does not terminate TLS. Non-owner ingress nodes use this to forward domain-mode sandbox hosts to the owner node's :443 mux, preserving the original ClientHello and letting the owner perform the normal local routing.

func (*Client) UpsertSandboxRoute

func (c *Client) UpsertSandboxRoute(ctx context.Context, id, containerIP string, toolboxPort int, customs []CustomHostnameRoute) error

UpsertSandboxRoute installs the HTTP ingress route for a sandbox owned by this node, plus one route per attached custom hostname. The default `sandbox-{id}` route matches ONLY `{id}.{domain}` (or the path-mode equivalent in IP mode) and dials the toolbox port — the custom-hostname routes are independent so each can dial a different in-container port without affecting the toolbox-served default URL.

customs is the operator-attached set of (hostname, target_port) pairs from the custom-domains feature. Each entry is installed as a separate route keyed by IngressCustomDomainHTTPRouteID(id, hostname), so the per-hostname dial target can change independently. Passing nil/empty preserves the legacy single-hostname behaviour. Custom-domain routes only take effect in domain mode; in IP mode the field is ignored (custom domains are rejected at the service layer in that deployment shape).

NOTE: UpsertSandboxRoute does NOT garbage-collect stale per-hostname routes — callers that detach a hostname must call DeleteCustomDomainHTTPRoute (or the reconciler must converge it).

func (*Client) UpsertSandboxRouteToPeer

func (c *Client) UpsertSandboxRouteToPeer(ctx context.Context, id, peerHost string, customHostnames []string) error

UpsertSandboxRouteToPeer installs the IP/path-mode ingress route for a sandbox owned by another node. Domain-mode clusters use caddy-l4 SNI pass-through instead, because the local HTTPS app sits behind the :443 layer4 mux and remote proxying would require dynamic upstream TLS SNI.

customHostnames is accepted for API parity with UpsertSandboxRoute, but only takes effect in domain mode; this peer-forwarding variant runs only in IP mode where the host matcher is unused.

func (*Client) UpsertTCPProxyRoute

func (c *Client) UpsertTCPProxyRoute(ctx context.Context, id string, port, hostPort int, peerHost string, peerPort int) error

UpsertTCPProxyRoute creates a raw-TCP ingress server bound to hostPort that forwards to another node's hostPort. This is the non-owner half of stable cluster TCP exposure: every node can accept tcp://cluster-host:hostPort, but only the owner forwards from hostPort to the container.

func (*Client) UpsertTCPRoute

func (c *Client) UpsertTCPRoute(ctx context.Context, id, containerIP string, port, hostPort int) error

UpsertTCPRoute creates (or replaces) the layer4 server bound to hostPort. One server per host-port allocation; POST is Caddy admin's "set or replace object" for a map-child path (PUT errors with 409 when the path already exists), so re-running with a different upstream IP after a sandbox restart is the right way to refresh routing without poking at routes/0.

func (*Client) UpsertTLSSNIRoute

func (c *Client) UpsertTLSSNIRoute(ctx context.Context, id, sniHost, containerIP string, port int) error

UpsertTLSSNIRoute publishes (or refreshes) one SNI route inside the shared tls-mux layer4 server. PATCH /id/<routeID> replaces the existing route in place without disturbing siblings; if the @id isn't there yet (404) we PUT at routes/0 so SNI matching tries it ahead of any future fallback.

The handler chain is [tls, proxy]: caddy-l4 terminates TLS using Caddy's own cert manager (which already holds the wildcard for *.$DOMAIN, issued once at startup via DNS-01), then proxies the now-plaintext bytes to the container on its native port. The container speaks raw TCP — no cert, no private key, nothing TLS-related lives inside the user's sandbox. The connection_policies entry is intentionally empty: Caddy picks the cert by SNI from the shared cert manager, so there is no per-route cert config.

Caller must have already called EnsureLayer4 with a non-empty tlsListen at least once; otherwise the routes/0 PUT will land on a missing server.

func (*Client) UpsertWakeHTTPPortRoute

func (c *Client) UpsertWakeHTTPPortRoute(ctx context.Context, id, ingressAddr string, port int) error

UpsertWakeHTTPPortRoute installs a wake-aware per-port HTTP route that dials the loopback ingress proxy at ingressAddr instead of the container. The route's @id is namespaced with a "-wake" suffix so reconcile can distinguish it from the legacy direct route — both must never exist at the same time for the same sandbox+port (see D5 in the plan).

The route rewrites the URI to /__ingress/http/{id}/{port}{path} before proxying so the ingress handler can parse the target sandbox+port out of the path without relying on host-header inspection.

func (*Client) UpsertWakeTCPRoute

func (c *Client) UpsertWakeTCPRoute(ctx context.Context, id string, port, hostPort int, wakeAddr string) error

UpsertWakeTCPRoute publishes a raw-TCP exposure in serverless mode. The public listener stays on hostPort, but Caddy forwards to sandboxd's loopback L4 wake listener and emits PROXY protocol v1 so sandboxd can map the connection back to this hostPort before waking the sandbox.

func (*Client) UpsertWakeTLSSNIRoute

func (c *Client) UpsertWakeTLSSNIRoute(ctx context.Context, id, sniHost, socketPath string, port int) error

UpsertWakeTLSSNIRoute publishes a TLS-SNI exposure in serverless mode. Caddy still terminates TLS at the edge, then proxies the plaintext stream to a sandboxd-owned Unix socket whose path identifies the sandbox/container port.

type CustomHostnameRoute

type CustomHostnameRoute struct {
	Hostname   string
	TargetPort int
	// MaskRequestHost is only set by service for user HTTP-port custom
	// domains. Toolbox-targeted domains keep the platform agent's Host intact.
	MaskRequestHost string
}

CustomHostnameRoute describes one operator-attached public hostname plus the in-container TCP port traffic should dial. TargetPort=0 means "use the toolbox port" (the pre-target-port default), so existing callers that only know hostnames can pass a zero-valued entry and preserve old behavior.

type HTTPRouteOptions

type HTTPRouteOptions struct {
	// MaskRequestHost, when non-empty, rewrites the upstream Host header to
	// this value (E2B network.maskRequestHost) so frameworks that validate the
	// Host (Vite, webpack-dev-server, Django) accept the request. Empty => no
	// headers block is emitted and the route JSON is byte-for-byte identical to
	// a route built without options, keeping the route-shape regression tests
	// valid. This is the DIRECT-route enforcement point; the serverless wake
	// path enforces the same mask in the loopback ingress proxy instead, since
	// that proxy overwrites Host on re-dial (see pkg/api/ingressproxy).
	MaskRequestHost string
}

HTTPRouteOptions tunes the reverse_proxy handler for a per-port HTTP route. Passed variadically so existing callers (and their regression tests) compile unchanged; only the mask-aware service path supplies a value.

type Snapshot

type Snapshot struct {
	// HTTPRouteIDs are the @ids of routes under apps/http that match our
	// "sandbox-..." prefix. Both the per-sandbox toolbox routes and the
	// per-port HTTP routes show up here.
	HTTPRouteIDs []string
	// L4TCPServerIDs are server names under apps/layer4 of the form
	// tcp-port-<hostPort>. Each maps 1:1 to a host-port allocation in the DB.
	L4TCPServerIDs []string
	// L4TLSRouteIDs are @ids of SNI routes inside the tls-mux server.
	L4TLSRouteIDs []string
}

Snapshot is the read side of reconcile's zombie-route detection. It walks the live Caddy config once and returns every entity whose name follows our conventions, so the service layer can compare against the DB and delete anything that has no matching row.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL