mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix(agent): recognize Tailscale CGNAT (100.64.0.0/10) as local for Ollama timeouts
`is_local_endpoint()` leaned on `ipaddress.is_private`, which classifies RFC-1918 ranges and link-local as private but deliberately excludes the RFC 6598 CGNAT block (100.64.0.0/10) — the range Tailscale uses for its mesh IPs. As a result, Ollama reached over Tailscale (e.g. `http://100.77.243.5:11434`) was treated as remote and missed the automatic stream-read / stale-stream timeout bumps, so cold model load plus long prefill would trip the 300 s watchdog before the first token. Add a module-level `_TAILSCALE_CGNAT = ipaddress.IPv4Network("100.64.0.0/10")` (built once) and extend `is_local_endpoint()` to match the block both via the parsed-`IPv4Address` path and the existing bare-string fallback (for symmetry with the 10/172/192 checks). Also hoist the previously function-local `import ipaddress` to module scope now that it's used by the constant. Extend `TestIsLocalEndpoint` with a CGNAT positive set (lower bound, representative host, MagicDNS anchor, upper bound) and a near-miss negative set (just below 100.64.0.0, just above 100.127.255.255, well outside the block, and first-octet-wrong).
This commit is contained in:
parent
44a16c5d9d
commit
6513138f26
2 changed files with 47 additions and 4 deletions
|
|
@ -4,6 +4,7 @@ Pure utility functions with no AIAgent dependency. Used by ContextCompressor
|
||||||
and run_agent.py for pre-flight context checks.
|
and run_agent.py for pre-flight context checks.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import ipaddress
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
@ -51,6 +52,13 @@ _OLLAMA_TAG_PATTERN = re.compile(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Tailscale's CGNAT range (RFC 6598). `ipaddress.is_private` excludes this
|
||||||
|
# block, so without an explicit check Ollama reached over Tailscale (e.g.
|
||||||
|
# `http://100.77.243.5:11434`) wouldn't be treated as local and its stream
|
||||||
|
# read / stale timeouts wouldn't get auto-bumped. Built once at import time.
|
||||||
|
_TAILSCALE_CGNAT = ipaddress.IPv4Network("100.64.0.0/10")
|
||||||
|
|
||||||
|
|
||||||
def _strip_provider_prefix(model: str) -> str:
|
def _strip_provider_prefix(model: str) -> str:
|
||||||
"""Strip a recognised provider prefix from a model string.
|
"""Strip a recognised provider prefix from a model string.
|
||||||
|
|
||||||
|
|
@ -283,7 +291,15 @@ def _is_known_provider_base_url(base_url: str) -> bool:
|
||||||
|
|
||||||
|
|
||||||
def is_local_endpoint(base_url: str) -> bool:
|
def is_local_endpoint(base_url: str) -> bool:
|
||||||
"""Return True if base_url points to a local machine (localhost / RFC-1918 / WSL)."""
|
"""Return True if base_url points to a local machine.
|
||||||
|
|
||||||
|
Recognises loopback (``localhost``, ``127.0.0.0/8``, ``::1``),
|
||||||
|
container-internal DNS names (``host.docker.internal`` et al.),
|
||||||
|
RFC-1918 private ranges (``10/8``, ``172.16/12``, ``192.168/16``),
|
||||||
|
link-local, and Tailscale CGNAT (``100.64.0.0/10``). Tailscale CGNAT
|
||||||
|
is included so remote-but-trusted Ollama boxes reached over a
|
||||||
|
Tailscale mesh get the same timeout auto-bumps as localhost Ollama.
|
||||||
|
"""
|
||||||
normalized = _normalize_base_url(base_url)
|
normalized = _normalize_base_url(base_url)
|
||||||
if not normalized:
|
if not normalized:
|
||||||
return False
|
return False
|
||||||
|
|
@ -298,14 +314,17 @@ def is_local_endpoint(base_url: str) -> bool:
|
||||||
# Docker / Podman / Lima internal DNS names (e.g. host.docker.internal)
|
# Docker / Podman / Lima internal DNS names (e.g. host.docker.internal)
|
||||||
if any(host.endswith(suffix) for suffix in _CONTAINER_LOCAL_SUFFIXES):
|
if any(host.endswith(suffix) for suffix in _CONTAINER_LOCAL_SUFFIXES):
|
||||||
return True
|
return True
|
||||||
# RFC-1918 private ranges and link-local
|
# RFC-1918 private ranges, link-local, and Tailscale CGNAT
|
||||||
import ipaddress
|
|
||||||
try:
|
try:
|
||||||
addr = ipaddress.ip_address(host)
|
addr = ipaddress.ip_address(host)
|
||||||
return addr.is_private or addr.is_loopback or addr.is_link_local
|
if addr.is_private or addr.is_loopback or addr.is_link_local:
|
||||||
|
return True
|
||||||
|
if isinstance(addr, ipaddress.IPv4Address) and addr in _TAILSCALE_CGNAT:
|
||||||
|
return True
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
# Bare IP that looks like a private range (e.g. 172.26.x.x for WSL)
|
# Bare IP that looks like a private range (e.g. 172.26.x.x for WSL)
|
||||||
|
# or Tailscale CGNAT (100.64.x.x–100.127.x.x).
|
||||||
parts = host.split(".")
|
parts = host.split(".")
|
||||||
if len(parts) == 4:
|
if len(parts) == 4:
|
||||||
try:
|
try:
|
||||||
|
|
@ -316,6 +335,8 @@ def is_local_endpoint(base_url: str) -> bool:
|
||||||
return True
|
return True
|
||||||
if first == 192 and second == 168:
|
if first == 192 and second == 168:
|
||||||
return True
|
return True
|
||||||
|
if first == 100 and 64 <= second <= 127:
|
||||||
|
return True
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
return False
|
return False
|
||||||
|
|
|
||||||
|
|
@ -106,3 +106,25 @@ class TestIsLocalEndpoint:
|
||||||
])
|
])
|
||||||
def test_remote_endpoints(self, url):
|
def test_remote_endpoints(self, url):
|
||||||
assert is_local_endpoint(url) is False
|
assert is_local_endpoint(url) is False
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("url", [
|
||||||
|
"http://100.64.0.0:11434", # lower bound of CGNAT block
|
||||||
|
"http://100.64.0.1:11434/v1", # lower bound +1
|
||||||
|
"http://100.77.243.5:11434", # representative Tailscale host
|
||||||
|
"https://100.100.100.100:443", # Tailscale MagicDNS anchor
|
||||||
|
"https://100.127.255.254:443", # upper bound -1
|
||||||
|
"http://100.127.255.255:11434", # upper bound of CGNAT block
|
||||||
|
])
|
||||||
|
def test_tailscale_cgnat_is_local(self, url):
|
||||||
|
"""Tailscale 100.64.0.0/10 should be treated as local for timeout bumps."""
|
||||||
|
assert is_local_endpoint(url) is True
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("url", [
|
||||||
|
"http://100.63.255.255:11434", # just below CGNAT block
|
||||||
|
"http://100.128.0.1:11434", # just above CGNAT block
|
||||||
|
"http://100.200.0.1:11434", # well outside CGNAT
|
||||||
|
"http://99.64.0.1:11434", # first octet wrong
|
||||||
|
])
|
||||||
|
def test_near_but_not_cgnat_is_remote(self, url):
|
||||||
|
"""Hosts adjacent to but outside 100.64.0.0/10 must not match."""
|
||||||
|
assert is_local_endpoint(url) is False
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue