/** * Trusted-proxy resolver + effective-transport decision (saga vr7kgx.9.2, D2). * * Pure, injectable function: `(peer, headers, trustedProxyConfig, pinnedPublicHost, …)` * → `{ scheme, host, remote }` (or a `{ rejected }` refusal). No I/O, no globals, no * `Request`/`Bun.Server` dependency — the caller extracts the peer address and the * forwarded header values and passes them in, so `.5` can test the full spoofing matrix * deterministically. * * Security invariants (locked in `vr7kgx.9` D2/D3): * - When `trustedProxy` is undefined, forwarded headers are ignored entirely and the * result is byte-identical to the loopback/direct path today. * - When `trustedProxy` is set, forwarded headers are honored **only** when the request's * direct peer matches `upstream` (`loopback` = 127.0.0.1/::1, or an exact IP literal). * From any other peer, forwarded headers are ignored and the request is treated as * direct. * - `X-Forwarded-Host` (or the configured `headerHost`) is **cross-checked** against the * pinned `publicHost` and the request is rejected on mismatch — it is never *used* to * derive `expectedHost`. The pinned `publicHost` is always the effective host. * - `X-Forwarded-For` is never consumed (informational only; the peer address is the trust * anchor). * - The effective scheme is `https` iff the trusted peer's forwarded scheme is `https` * (case-insensitive); otherwise `http`. */ /** Loopback bind hosts (no remote transport exposed). `undefined` matches the default bind. */ const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost']); /** True when `webHost` is a pure loopback bind (or the default unset bind). */ export function isLoopbackBind(webHost: string | undefined): boolean { return webHost === undefined || LOOPBACK_HOSTS.has(webHost); } function isLoopbackIp(ip: string): boolean { return ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1'; } /** * Trusted reverse-proxy configuration. Opt-in: when undefined, no `X-Forwarded-*` header * is honored and the daemon behaves exactly as loopback mode. `upstream` is the only * trusted peer for forwarded headers (`loopback` = 127.0.0.1/::1, or an explicit IP * literal). `headerScheme`/`headerHost` default to `X-Forwarded-Proto`/`X-Forwarded-Host` * and are configurable for deployments that rename them. */ export interface TrustedProxyConfig { upstream: 'loopback' | string; headerScheme?: string; headerHost?: string; } /** The request's direct peer — the trust anchor for forwarded headers. */ export type ResolverPeer = | { ip: string } | { socketPath: string }; /** Forwarded header values already extracted by the caller (using the configured names). */ export interface ForwardedHeaders { scheme?: string; host?: string; } /** A usable effective transport: the caller treats this as the request's scheme + expectedHost. */ export interface EffectiveTransport { scheme: 'http' | 'https'; host: string; remote: boolean; } /** Refusal: a trusted peer forwarded a host that does not match the pinned `publicHost`. */ export interface RejectedTransport { rejected: true; reason: 'forwarded-host-mismatch'; } export type ResolveResult = EffectiveTransport | RejectedTransport; export interface ResolveEffectiveTransportArgs { /** Direct peer of the request (the trust anchor). */ peer: ResolverPeer; /** Forwarded header values (extracted by the caller using the configured header names). */ headers: ForwardedHeaders; /** Trusted-proxy config from options; undefined = disabled (pure loopback/direct). */ trustedProxy?: TrustedProxyConfig; /** Pinned public `host:port` from options; the effective host in any remote path. */ pinnedPublicHost?: string; /** Daemon's bound loopback port (the effective host in pure-loopback mode is `127.0.0.1:`). */ loopbackPort: number; /** * Caller-computed: true when the daemon binds a non-loopback interface directly (no proxy). * The resolver's job is the proxy decision; the direct-remote decision is the caller's based * on `webHost`. When true and no proxy header is honored, the effective transport is the * pinned `publicHost` over plaintext `http`. */ directRemote?: boolean; } function peerMatchesUpstream(peer: ResolverPeer, upstream: string): boolean { if ('socketPath' in peer) return false; // Unix-socket peer anchor is a fast-follow (D2/Q2). if (upstream === 'loopback') return isLoopbackIp(peer.ip); return peer.ip === upstream; } /** * Resolve the effective transport for a request. Pure: same inputs → same outputs, no I/O. * * Returns `{ rejected: true, reason: 'forwarded-host-mismatch' }` when a trusted peer * forwards a `host` header that does not equal the pinned `publicHost`; the caller turns * this into a 400. In that case `scheme`/`host`/`remote` are not meaningful. */ export function resolveEffectiveTransport(args: ResolveEffectiveTransportArgs): ResolveResult { const { peer, headers, trustedProxy, pinnedPublicHost, loopbackPort, directRemote } = args; const loopbackHost = `127.0.0.1:${loopbackPort}`; // No trusted proxy: ignore forwarded headers entirely (D2). Pure-loopback is // byte-identical to today; direct non-loopback bind serves plaintext over the pinned host. if (trustedProxy === undefined) { if (directRemote) { return { scheme: 'http', host: pinnedPublicHost ?? loopbackHost, remote: true }; } return { scheme: 'http', host: loopbackHost, remote: false }; } // Trusted proxy configured: only honor forwarded headers from the trusted upstream peer. if (!peerMatchesUpstream(peer, trustedProxy.upstream)) { // Non-trusted peer: ignore forwarded headers, treat as direct/loopback (D2). Under a // remote configuration the implicit loopback origin is NOT added (D3), so a // state-changing request from a non-trusted peer is rejected at origin validation // rather than silently falling back to loopback trust. if (directRemote) { return { scheme: 'http', host: pinnedPublicHost ?? loopbackHost, remote: true }; } return { scheme: 'http', host: loopbackHost, remote: false }; } // Trusted peer: read the forwarded scheme. `https` (case-insensitive) → https, else http. const scheme: 'http' | 'https' = headers.scheme !== undefined && headers.scheme.trim().toLowerCase() === 'https' ? 'https' : 'http'; // The effective host is ALWAYS the pinned publicHost — never derived from a client header. const host = pinnedPublicHost ?? loopbackHost; // Cross-check: a forwarded host present and not equal to the pinned publicHost is a // spoofing attempt (or a misconfigured proxy); reject rather than re-trust (D2/D3). const forwardedHost = headers.host; if ( forwardedHost !== undefined && forwardedHost.length > 0 && forwardedHost !== host ) { return { rejected: true, reason: 'forwarded-host-mismatch' }; } return { scheme, host, remote: true }; }