mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 13:10:43 +00:00
fix(agents): cover IPv6 ULA and link-local in local-baseUrl detector
This commit is contained in:
@@ -100,6 +100,17 @@ describe("resolveLlmIdleTimeoutMs", () => {
|
||||
"http://192.168.1.20:11434",
|
||||
"http://100.64.0.5:11434",
|
||||
"http://100.127.255.254:11434",
|
||||
// RFC 4193 IPv6 unique local (Tailscale IPv6 mesh fd7a:115c:a1e0::/48
|
||||
// falls inside fc00::/7).
|
||||
"http://[fc00::1]:11434",
|
||||
"http://[fd00::1]:11434",
|
||||
"http://[fd7a:115c:a1e0::dead:beef]:11434",
|
||||
"http://[fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff]:11434",
|
||||
// RFC 4291 IPv6 link-local.
|
||||
"http://[fe80::1]:11434",
|
||||
"http://[fe9a::1]:11434",
|
||||
"http://[feab:cd::1]:11434",
|
||||
"http://[febf::1]:11434",
|
||||
])("disables the default idle watchdog for local provider baseUrl %s", (baseUrl) => {
|
||||
expect(resolveLlmIdleTimeoutMs({ model: { baseUrl } })).toBe(0);
|
||||
});
|
||||
@@ -113,6 +124,25 @@ describe("resolveLlmIdleTimeoutMs", () => {
|
||||
expect(resolveLlmIdleTimeoutMs({ model: { baseUrl } })).toBe(DEFAULT_LLM_IDLE_TIMEOUT_MS);
|
||||
});
|
||||
|
||||
it.each([
|
||||
// Just outside fc00::/7 (fe.. and 00fc::/16 are not unique-local).
|
||||
"http://[fec0::1]:11434",
|
||||
"http://[fbff::1]:11434",
|
||||
// Just outside fe80::/10 (fec0:: was deprecated site-local, fe7f:: not LL).
|
||||
"http://[fe7f::1]:11434",
|
||||
// Public IPv6.
|
||||
"http://[2001:db8::1]:11434",
|
||||
// Abbreviated `fc::1` expands to 00fc:0:0:...:1, first byte is 0x00, not
|
||||
// 0xfc — outside fc00::/7. Strict first-hextet match keeps this remote.
|
||||
"http://[fc::1]:11434",
|
||||
// IPv4-mapped IPv6 outside loopback (private RFC 1918 in mapped form is
|
||||
// intentionally not matched, mirroring the SSRF policy helper).
|
||||
"http://[::ffff:10.0.0.5]:11434",
|
||||
"http://[::ffff:192.168.1.20]:11434",
|
||||
])("keeps the default idle watchdog for non-private IPv6 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",
|
||||
|
||||
@@ -20,6 +20,24 @@ const MAX_SAFE_TIMEOUT_MS = 2_147_000_000;
|
||||
* (Ollama, LM Studio, llama.cpp) legitimately stay silent for many minutes
|
||||
* during prompt evaluation and thinking, so the network-silence-as-hang
|
||||
* heuristic that motivates the default idle watchdog does not apply.
|
||||
*
|
||||
* Coverage scope:
|
||||
* - IPv4 loopback (RFC 5735, full 127/8), RFC 1918 private, RFC 6598 shared
|
||||
* CGNAT (100.64/10 — Tailscale/Headscale IPv4 mesh), `0.0.0.0`, `localhost`,
|
||||
* and `*.local` mDNS (RFC 6762).
|
||||
* - IPv6 loopback `::1`, IPv6 unique local `fc00::/7` (RFC 4193 — Tailscale's
|
||||
* IPv6 mesh `fd7a:115c:a1e0::/48` falls in this range), and IPv6 link-local
|
||||
* `fe80::/10` (RFC 4291).
|
||||
* - IPv4-mapped IPv6 covers loopback only (`::ffff:127.0.0.1`,
|
||||
* `::ffff:7f00:1`); private IPv4 in mapped form is intentionally not
|
||||
* matched, mirroring the SSRF-policy helper in
|
||||
* `src/cron/isolated-agent/model-preflight.runtime.ts`.
|
||||
* - DNS-resolved local aliases (e.g. an `/etc/hosts` entry mapping a custom
|
||||
* hostname to a private IP) are not detected: classification keys on
|
||||
* `URL.hostname` so resolution would have to happen here, and adding
|
||||
* sync/async DNS to the watchdog hot path is disproportionate. Affected
|
||||
* users can use the IP directly or set
|
||||
* `models.providers.<id>.timeoutSeconds` explicitly.
|
||||
*/
|
||||
function isLocalProviderBaseUrl(baseUrl: string): boolean {
|
||||
let host: string;
|
||||
@@ -41,6 +59,15 @@ function isLocalProviderBaseUrl(baseUrl: string): boolean {
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// IPv6 unique local (RFC 4193, fc00::/7) and link-local (RFC 4291,
|
||||
// fe80::/10). The full first hextet is required so an abbreviated `fc::1`
|
||||
// (which expands to `00fc:0:0:...` and is therefore not in fc00::/7)
|
||||
// correctly stays on the cloud path. The first regex requires four hex
|
||||
// digits then a colon; a zone identifier such as `fe80::1%eth0` is fine
|
||||
// because the prefix still matches at the start.
|
||||
if (/^f[cd][0-9a-f]{2}:/.test(host) || /^fe[89ab][0-9a-f]:/.test(host)) {
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user