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

  1. 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).
  2. Remove any query string or matrix-parameter (';') segments from the tool URL so the allow-list comparison in is_private_endpoint_allowed matches.
  3. If the service should be public, expose it on a globally routable address instead of an internal one.
  4. 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

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


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)