From c97552a04cbaa0afffb67139c49477432a6b2ee0 Mon Sep 17 00:00:00 2001 From: openperf <16864032@qq.com> Date: Sun, 3 May 2026 02:03:04 +0800 Subject: [PATCH] fix(agents): broaden local-baseUrl detection to 127/8 and RFC 6598 CGNAT --- .../run/llm-idle-timeout.test.ts | 27 +++++++++++++++++++ .../run/llm-idle-timeout.ts | 18 ++++++++++--- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/agents/pi-embedded-runner/run/llm-idle-timeout.test.ts b/src/agents/pi-embedded-runner/run/llm-idle-timeout.test.ts index 230897e4267..41b5e2f121b 100644 --- a/src/agents/pi-embedded-runner/run/llm-idle-timeout.test.ts +++ b/src/agents/pi-embedded-runner/run/llm-idle-timeout.test.ts @@ -89,16 +89,43 @@ describe("resolveLlmIdleTimeoutMs", () => { it.each([ "http://localhost:11434", "http://127.0.0.1:11434", + "http://127.0.0.2:11434", + "http://127.255.255.254:11434", "http://0.0.0.0:11434", "http://[::1]:11434", "http://my-rig.local:11434", "http://10.0.0.5:11434", "http://172.16.5.10:11434", + "http://172.31.99.1:11434", "http://192.168.1.20:11434", + "http://100.64.0.5:11434", + "http://100.127.255.254:11434", ])("disables the default idle watchdog for local provider baseUrl %s", (baseUrl) => { expect(resolveLlmIdleTimeoutMs({ model: { baseUrl } })).toBe(0); }); + it.each([ + "http://172.32.0.1:11434", + "http://192.169.1.1:11434", + "http://100.63.255.254:11434", + "http://100.128.0.1:11434", + ])("keeps the default idle watchdog for non-private IPv4 baseUrl %s", (baseUrl) => { + expect(resolveLlmIdleTimeoutMs({ model: { baseUrl } })).toBe(DEFAULT_LLM_IDLE_TIMEOUT_MS); + }); + + it.each([ + "http://10.0.0.5evil:11434", + "http://127.0.0.1foo:11434", + "http://192.168.1.20attacker.com:11434", + "http://10.0.0.5.evil.com:11434", + "http://1.2.3.4.5:11434", + ])( + "keeps the default idle watchdog for numeric-looking hostnames that are not IPv4 literals (%s)", + (baseUrl) => { + expect(resolveLlmIdleTimeoutMs({ model: { baseUrl } })).toBe(DEFAULT_LLM_IDLE_TIMEOUT_MS); + }, + ); + it("keeps the default idle watchdog for remote provider baseUrls", () => { expect(resolveLlmIdleTimeoutMs({ model: { baseUrl: "https://api.openai.com/v1" } })).toBe( DEFAULT_LLM_IDLE_TIMEOUT_MS, diff --git a/src/agents/pi-embedded-runner/run/llm-idle-timeout.ts b/src/agents/pi-embedded-runner/run/llm-idle-timeout.ts index cbc1e66ee08..18d1c8ddf19 100644 --- a/src/agents/pi-embedded-runner/run/llm-idle-timeout.ts +++ b/src/agents/pi-embedded-runner/run/llm-idle-timeout.ts @@ -33,7 +33,6 @@ function isLocalProviderBaseUrl(baseUrl: string): boolean { } if ( host === "localhost" || - host === "127.0.0.1" || host === "0.0.0.0" || host === "::1" || host === "::ffff:7f00:1" || @@ -42,13 +41,26 @@ function isLocalProviderBaseUrl(baseUrl: string): boolean { ) { return true; } + // Require a strict IPv4 literal before parsing; `Number.parseInt` is + // permissive and would otherwise let `10.0.0.5evil` parse to [10,0,0,5] + // and disable the watchdog for a non-IP hostname. + if (!/^\d+\.\d+\.\d+\.\d+$/.test(host)) { + return false; + } const octets = host.split(".").map((part) => Number.parseInt(part, 10)); - if (octets.length !== 4 || octets.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) { + if (octets.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) { return false; } const [a, b] = octets; + // RFC 5735 loopback (127/8 — full range, not just .0.1; container/sandbox + // setups commonly bind 127.0.0.2+), RFC 1918 private IPv4, and RFC 6598 + // shared CGNAT (100.64/10 — used by Tailscale and similar mesh VPNs). return ( - a === 10 || (a === 172 && b !== undefined && b >= 16 && b <= 31) || (a === 192 && b === 168) + a === 127 || + a === 10 || + (a === 172 && b !== undefined && b >= 16 && b <= 31) || + (a === 192 && b === 168) || + (a === 100 && b !== undefined && b >= 64 && b <= 127) ); }