From 8b2aaaf55fbaba3023a3e95da17ceb61b8a0bd79 Mon Sep 17 00:00:00 2001 From: Dax Date: Sat, 17 Oct 2026 11:44:25 -0400 Subject: [PATCH] fix(console): use private inference IP handoff (#56427) --- infra/console.ts | 7 ++- packages/console/app/src/lib/inference-ip.ts | 20 ++++++++ .../console/app/src/lib/inference-proxy.ts | 5 +- packages/console/app/test/inferenceIp.test.ts | 50 +++++++++++++++++++ 4 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 packages/console/app/src/lib/inference-ip.ts create mode 100644 packages/console/app/test/inferenceIp.test.ts diff --git a/infra/console.ts b/infra/console.ts index 79d5e077cfac..a3ead0e182e4 100644 --- a/infra/console.ts +++ b/infra/console.ts @@ -64,7 +64,12 @@ new sst.cloudflare.x.SolidStart("Console", { if (migrationDomain) args.bindings = $resolve(args.bindings).apply((bindings) => [ ...(bindings ?? []), - { type: "service", name: "INFERENCE", service: `opencode-inference-${$app.stage}` }, + { + type: "service", + name: "INFERENCE", + service: `opencode-inference-${$app.stage}`, + entrypoint: "LegacyIngress", + }, ]) }, }, diff --git a/packages/console/app/src/lib/inference-ip.ts b/packages/console/app/src/lib/inference-ip.ts new file mode 100644 index 000000000000..76b403fab09c --- /dev/null +++ b/packages/console/app/src/lib/inference-ip.ts @@ -0,0 +1,20 @@ +// Only direct Cloudflare requests can supply visitor-IP evidence to the private inference binding. +// Worker subrequests can derive CF-Connecting-IP from script-controlled X-Real-IP. +export function inferenceHeaders(request: Request & { readonly cf?: unknown }) { + const ip = + request.cf !== undefined && !["cf-worker", "cf-ew-via", "x-real-ip"].some((name) => request.headers.has(name)) + ? request.headers.get("cf-connecting-ip") + : null + const headers = new Headers(request.headers) + for (const name of [ + "x-real-ip", + "x-forwarded-for", + "forwarded", + "cf-connecting-ip", + "cf-connecting-ipv6", + "x-zen-ip", + ]) + headers.delete(name) + if (ip) headers.set("x-real-ip", ip) + return headers +} diff --git a/packages/console/app/src/lib/inference-proxy.ts b/packages/console/app/src/lib/inference-proxy.ts index b0a2ec0cb99e..c823641ee7fa 100644 --- a/packages/console/app/src/lib/inference-proxy.ts +++ b/packages/console/app/src/lib/inference-proxy.ts @@ -1,4 +1,5 @@ import { Resource } from "@opencode/console-resource" +import { inferenceHeaders } from "./inference-ip" const paths: Record = { "POST /zen/v1/chat/completions": "/openai/v1/chat/completions", @@ -36,14 +37,12 @@ export async function proxyInference(request: Request) { destination.search = url.search destination.hash = "" - const forwarded = new Request(destination, request) + const forwarded = new Request(destination, new Request(request, { headers: inferenceHeaders(request) })) for (const name of ["host", "content-length"]) forwarded.headers.delete(name) // Keyless requests must reach new inference without credentials; `public` is not an API key there. const anonymous = !key || key === "public" if (anonymous) for (const name of ["authorization", "x-api-key", "x-goog-api-key"]) forwarded.headers.delete(name) if (!anonymous) forwarded.headers.set("authorization", `Bearer ${key}`) - const ip = request.headers.get("cf-connecting-ip") - if (ip) forwarded.headers.set("x-real-ip", ip) const requestID = request.headers.get("x-opencode-request-id") ?? request.headers.get("x-opencode-request") if (requestID) forwarded.headers.set("x-opencode-request-id", requestID) diff --git a/packages/console/app/test/inferenceIp.test.ts b/packages/console/app/test/inferenceIp.test.ts new file mode 100644 index 000000000000..0fd070abc174 --- /dev/null +++ b/packages/console/app/test/inferenceIp.test.ts @@ -0,0 +1,50 @@ +import { expect, test } from "bun:test" +import { inferenceHeaders } from "../src/lib/inference-ip" + +test.each(["203.0.113.10", "2001:db8::10"])("forwards only the direct edge address: %s", (ip) => { + const request = Object.assign( + new Request("https://opencode.ai/zen/v1/models", { + headers: { + "cf-connecting-ip": ip, + "cf-connecting-ipv6": "2001:db8::bad", + "x-forwarded-for": "198.51.100.1", + forwarded: "for=198.51.100.1", + "x-zen-ip": "198.51.100.1", + authorization: "Bearer test-only", + }, + }), + { cf: { colo: "TEST" } }, + ) + expect([...inferenceHeaders(request)]).toEqual([ + ["authorization", "Bearer test-only"], + ["x-real-ip", ip], + ]) +}) + +test.each(["cf-worker", "cf-ew-via", "x-real-ip"])("rejects relay evidence: %s", (name) => { + const request = Object.assign( + new Request("https://opencode.ai/zen/v1/models", { + headers: { "cf-connecting-ip": "203.0.113.10", [name]: "spoofed" }, + }), + { cf: { colo: "TEST" } }, + ) + expect(inferenceHeaders(request).has("x-real-ip")).toBe(false) + expect(inferenceHeaders(request).has("cf-connecting-ip")).toBe(false) +}) + +test("missing runtime metadata cannot turn headers into trusted evidence", () => { + const request = new Request("http://origin/zen/v1/models", { + headers: { "cf-connecting-ip": "203.0.113.10" }, + }) + expect([...inferenceHeaders(request)]).toEqual([]) +}) + +test("missing edge IP strips claims rather than retaining a fallback", () => { + const request = Object.assign( + new Request("https://opencode.ai/zen/v1/models", { + headers: { "x-forwarded-for": "198.51.100.1", "cf-connecting-ipv6": "2001:db8::1" }, + }), + { cf: { colo: "TEST" } }, + ) + expect([...inferenceHeaders(request)]).toEqual([]) +})