Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 6 additions & 10 deletions src/lib/provider-outbound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 34 additions & 0 deletions tests/provider-outbound.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading