iflytek/astron-agent · error · OutboundPolicyError
Outbound address is not globally routable
Error message
Outbound address is not globally routable
What it means
OutboundPolicyError raised in validate_address as the final check: when neither the literal exception nor the private-endpoint exception applies, the resolved/canonical IP must be globally routable (ipaddress.is_global). Anything private (RFC1918, ULA, loopback, etc.) is rejected to prevent SSRF into internal infrastructure. IPv4-mapped IPv6 addresses are canonicalized to their IPv4 form first.
Solutions
- Add the exact endpoint to PRIVATE_ENDPOINT_ALLOW_LIST as scheme://host:port/path with no query string and no ';' in the path (format is strictly validated).
- Remove any query string or matrix-parameter (';') segments from the tool URL so the allow-list comparison in is_private_endpoint_allowed matches.
- If the service should be public, expose it on a globally routable address instead of an internal one.
- Verify the allow-list entry's scheme, normalized host, and default port (443/https, 80/http) exactly match the URL — the comparison is an exact tuple match.
Example fix
// before url = "http://internal-svc:8080/api?x=1" # query defeats allow-list // after url = "http://internal-svc:8080/api" # PRIVATE_ENDPOINT_ALLOW_LIST=http://internal-svc:8080/api
Defensive patterns
Strategy: validation
Validate before calling
from urllib.parse import urlsplit
def endpoint_matches_allow_list(url: str, allow_list: list[str]) -> bool:
p = urlsplit(url)
if p.query or ";" in p.path:
return False # query/matrix params disqualify allow-list matching
port = p.port or (443 if p.scheme == "https" else 80)
entry = f"{p.scheme}://{p.hostname}:{port}{p.path or '/'}"
normalized = [a.rstrip('/') for a in allow_list]
return entry.rstrip('/') in normalized
# verify before deploying: endpoint_matches_allow_list("http://internal-svc:8080/api", allow_list) Type guard
def is_plain_path(url: str) -> bool:
p = urlsplit(url)
return not p.query and ";" not in p.path Try / catch
try:
policy.validate_url(url)
except OutboundPolicyError as exc:
if "not globally routable" in str(exc):
logger.error("endpoint %s resolves to a private address and is not in PRIVATE_ENDPOINT_ALLOW_LIST", url)
raise
raise Prevention
- Every internal endpoint a tool may call must be registered verbatim in PRIVATE_ENDPOINT_ALLOW_LIST (no query, no ';', exact path).
- Keep tool URLs path-only when talking to allow-listed private endpoints; move parameters into the body.
- Remember allow-list matching normalizes the default port (443/https, 80/http) and lowercases/IDNA-normalizes the host.
- Test with the same DNS the service uses in production — container-internal names resolve to private IPs that require allow-listing.
When it happens
Trigger: validate_url or socket_factory resolving a hostname to a private/non-global address (10.x, 172.16-31.x, 192.168.x, fc00::/7, etc.) while allow_private_endpoint is False — i.e. the endpoint is not listed in PRIVATE_ENDPOINT_ALLOW_LIST, or the path/query contains ';' or a query string so is_private_endpoint_allowed returns False.
Common situations: Calling an internal microservice by hostname that resolves to an RFC1918 address without allow-listing it; allow-list entry mismatch caused by a query string or a semicolon/matrix parameter in the tool path; docker-compose service names resolving to container-internal IPs; IPv6 ULA (fd00::/8) addresses from modern DNS setups.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- Outbound address is blocked
- MODEL_URL_CHECK_FAILED
- Resolved remote resource address is invalid
- HTTPClientError
- Remote resource address is unsafe
AI-assisted analysis of iflytek/astron-agent@5e758547a8 (2026-09-12).
Data as JSON: /api/errors/835fd20659f31832.
Report an issue: GitHub.
Appendix: source
Thrown at core/plugin/link/infra/tool_exector/ssrf_guard.py:124
self,
address: IpAddress,
*,
allow_private_endpoint: bool,
allow_literal_exception: bool,
) -> None:
"""Validate the exact IP address that aiohttp is about to connect to."""
if _matches_any(address, self.blocked_networks):
raise OutboundPolicyError("Outbound address is blocked")
if allow_literal_exception and _matches_any(
address, self.allowed_literal_networks
):
return
if _is_never_connect_address(address):
raise OutboundPolicyError("Outbound address is unsafe")
if allow_private_endpoint:
return
if not _canonical_address(address).is_global:
raise OutboundPolicyError("Outbound address is not globally routable")
def is_domain_blocked(self, hostname: str) -> bool:
"""Match configured domains on label boundaries, including subdomains."""
for rule in self.blocked_domains:
if hostname == rule or hostname.endswith("." + rule):
return True
return False
def ensure_same_origin(base_url: str, candidate_url: str) -> None:
"""Reject path or authentication data that changes scheme, host, or port."""
if _origin(base_url) != _origin(candidate_url):
raise OutboundPolicyError("Tool path must not change the endpoint origin")
def create_socket_factory(
policy: OutboundPolicy,
target_url: str,View on GitHub (pinned to 5e758547a8)