From 7b68eb1acbd58980f989852a2d4fc240c4901374 Mon Sep 17 00:00:00 2001 From: James Shawn Carnley Date: Thu, 2 Apr 2026 13:46:10 -0400 Subject: [PATCH] Enhance local provider URL detection for IPv6 and loopback ranges --- src/services/api/providerConfig.local.test.ts | 11 +++++++++++ src/services/api/providerConfig.ts | 18 ++++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/services/api/providerConfig.local.test.ts b/src/services/api/providerConfig.local.test.ts index 0513434b..9adf0d57 100644 --- a/src/services/api/providerConfig.local.test.ts +++ b/src/services/api/providerConfig.local.test.ts @@ -6,6 +6,10 @@ test('treats localhost endpoints as local', () => { expect(isLocalProviderUrl('http://localhost:11434/v1')).toBe(true) expect(isLocalProviderUrl('http://127.0.0.1:11434/v1')).toBe(true) expect(isLocalProviderUrl('http://0.0.0.0:11434/v1')).toBe(true) + // Full 127.0.0.0/8 loopback range should be treated as local + expect(isLocalProviderUrl('http://127.0.0.2:11434/v1')).toBe(true) + expect(isLocalProviderUrl('http://127.1.2.3:11434/v1')).toBe(true) + expect(isLocalProviderUrl('http://127.255.255.255:11434/v1')).toBe(true) }) test('treats private IPv4 endpoints as local', () => { @@ -18,7 +22,14 @@ test('treats .local hostnames as local', () => { expect(isLocalProviderUrl('http://ollama.local:11434/v1')).toBe(true) }) +test('treats private IPv6 endpoints as local', () => { + expect(isLocalProviderUrl('http://[fd00::1]:11434/v1')).toBe(true) + expect(isLocalProviderUrl('http://[fe80::1]:11434/v1')).toBe(true) + expect(isLocalProviderUrl('http://[::1]:11434/v1')).toBe(true) +}) + test('treats public hosts as remote', () => { expect(isLocalProviderUrl('http://203.0.113.1:11434/v1')).toBe(false) expect(isLocalProviderUrl('https://example.com/v1')).toBe(false) + expect(isLocalProviderUrl('http://[2001:4860:4860::8888]:11434/v1')).toBe(false) }) diff --git a/src/services/api/providerConfig.ts b/src/services/api/providerConfig.ts index 90deeb1b..5097f142 100644 --- a/src/services/api/providerConfig.ts +++ b/src/services/api/providerConfig.ts @@ -210,7 +210,19 @@ function isCodexAlias(model: string): boolean { export function isLocalProviderUrl(baseUrl: string | undefined): boolean { if (!baseUrl) return false try { - const hostname = new URL(baseUrl).hostname.toLowerCase() + let hostname = new URL(baseUrl).hostname.toLowerCase() + + // Strip IPv6 brackets added by the URL parser (e.g. "[::1]" -> "::1") + if (hostname.startsWith('[') && hostname.endsWith(']')) { + hostname = hostname.slice(1, -1) + } + + // Strip RFC6874 IPv6 zone identifiers (e.g. "fe80::1%25en0" -> "fe80::1") + const zoneIdIndex = hostname.indexOf('%25') + if (zoneIdIndex !== -1) { + hostname = hostname.slice(0, zoneIdIndex) + } + if (LOCALHOST_HOSTNAMES.has(hostname) || hostname === '0.0.0.0') { return true } @@ -220,7 +232,9 @@ export function isLocalProviderUrl(baseUrl: string | undefined): boolean { const ipVersion = isIP(hostname) if (ipVersion === 4) { - return isPrivateIpv4Address(hostname) + // Treat the full 127.0.0.0/8 loopback range as local + const firstOctet = Number.parseInt(hostname.split('.', 1)[0] ?? '', 10) + return firstOctet === 127 || isPrivateIpv4Address(hostname) } if (ipVersion === 6) { return isPrivateIpv6Address(hostname)