From b92bb611c5088cec6153cac1c21494bcd7aeaa32 Mon Sep 17 00:00:00 2001 From: Ingwannu Date: Tue, 18 Aug 2026 14:12:01 +0000 Subject: [PATCH] fix(outbound): keep fake-IP opt-in off NO_PROXY routes --- src/lib/provider-outbound.ts | 16 ++++++---------- tests/provider-outbound.test.ts | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/lib/provider-outbound.ts b/src/lib/provider-outbound.ts index ab8b1ceed7..f067aa927d 100644 --- a/src/lib/provider-outbound.ts +++ b/src/lib/provider-outbound.ts @@ -149,16 +149,12 @@ async function providerOutboundRequest( context: "provider URL", allowPrivateNetwork: allowPrivate, // Clash/Surge/Mihomo fake-IP DNS (198.18.0.0/15) answers are admitted only - // when an outbound proxy is configured: the hostname then rides the proxy as - // an ordinary CONNECT instead of failing as a private destination or being - // pin-connected to the fake-IP (credit #1748). Without a proxy, benchmark - // answers keep rejecting. Image/Lab fetch never passes this flag. - // Known corner: the opt-in arms on the GLOBAL proxy config, not per-host. If - // NO_PROXY excludes this host, Bun bypasses the proxy and direct-connects to - // the benchmark answer — non-routable space typically intercepted by the - // local fake-IP TUN, so not an SSRF widening, but the CONNECT claim does not - // hold for NO_PROXY-excluded hosts. - allowBenchmarkAddresses: proxyConfigured, + // when this exact host will use the configured outbound proxy: the hostname + // then rides the proxy as an ordinary CONNECT instead of failing as a private + // destination or being pin-connected to the fake-IP (credit #1748). A NO_PROXY + // match is a direct route, so it keeps the benchmark answer rejected. Image/Lab + // fetch never passes this flag. + allowBenchmarkAddresses: proxyConfigured && !noProxyMatches(parsed), }); } catch (error) { const dnsResolutionFailed = error instanceof DestinationDnsResolutionError diff --git a/tests/provider-outbound.test.ts b/tests/provider-outbound.test.ts index 9ac18438ba..f43cb3587f 100644 --- a/tests/provider-outbound.test.ts +++ b/tests/provider-outbound.test.ts @@ -170,6 +170,40 @@ describe("provider outbound GET transport", () => { expect(resolveOptions).toEqual([{ allowBenchmarkAddresses: false }]); }); + test("NO_PROXY-matched hosts do not receive the fake-IP benchmark exception", async () => { + const proxyUrl = "http://127.0.0.1:9"; + process.env.HTTPS_PROXY = proxyUrl; + process.env.https_proxy = proxyUrl; + process.env.NO_PROXY = "www.packyapi.com"; + process.env.no_proxy = "www.packyapi.com"; + const originalFetch = globalThis.fetch; + const fetchMock = mock(async () => new Response("unexpected", { status: 500 })) as typeof fetch; + globalThis.fetch = fetchMock; + try { + const { providerOutboundGet } = await import("../src/lib/provider-outbound"); + const resolveOptions: { allowBenchmarkAddresses?: boolean }[] = []; + const { dependencies, captured } = directDependencies(new Response(null, { status: 500 })); + dependencies.resolveAddresses = mock(async (_url: string, options?: { allowBenchmarkAddresses?: boolean }) => { + resolveOptions.push({ allowBenchmarkAddresses: options?.allowBenchmarkAddresses }); + throw new Error("provider URL hostname www.packyapi.com resolves to benchmark address (198.18.56.214)"); + }) as ProviderOutboundDependencies["resolveAddresses"]; + + await expect(providerOutboundGet( + "packy", + { baseUrl: "https://www.packyapi.com/v1" }, + "https://www.packyapi.com/v1/models", + {}, + dependencies, + )).rejects.toThrow(/benchmark address/); + + expect(resolveOptions).toEqual([{ allowBenchmarkAddresses: false }]); + expect(fetchMock).not.toHaveBeenCalled(); + expect(captured.address).toBeUndefined(); + } finally { + globalThis.fetch = originalFetch; + } + }); + test("built-in ollama admits loopback discovery without an explicit allowPrivateNetwork flag (#758)", async () => { for (const key of proxyKeys) delete process.env[key]; const { providerOutboundGet } = await import("../src/lib/provider-outbound");