iflytek/astron-agent · error · OutboundPolicyError

Outbound address is blocked

Error message

Outbound address is blocked

What it means

OutboundPolicyError raised in OutboundPolicy.validate_address when the IP address about to be connected to matches one of the configured blocked_networks (loaded from the SEGMENT/IP black-list env vars). This is the SSRF guard's explicit deny-list check, including IPv4-mapped IPv6 candidates. It means the destination IP is forbidden by deployment policy, not that the network is unreachable.

Solutions

  1. Inspect SEGMENT_BLACK_LIST_KEY / IP_BLACK_LIST_KEY env values and check whether the target IP falls inside one of those CIDRs (python -c "import ipaddress;..." to test membership).
  2. If the endpoint is legitimately needed but private, add it to PRIVATE_ENDPOINT_ALLOW_LIST (exact scheme, host, port, plain path) so allow_private_endpoint bypasses, or add its exact IP to IP_WHITE_LIST_KEY when connecting by literal IP (allow_literal_exception short-circuits before unsafe checks).
  3. Narrow or remove the over-broad black-list entry that accidentally covers the destination.
  4. If the hostname unexpectedly resolves into a blocked range, fix DNS or point the tool at the correct public endpoint.

Example fix

# before
IP_BLACK_LIST=10.0.0.0/8   # tool endpoint 10.1.2.3 is now blocked
# after
IP_BLACK_LIST=10.0.0.0/8
PRIVATE_ENDPOINT_ALLOW_LIST=http://10.1.2.3:8080/api
Defensive patterns

Strategy: validation

Validate before calling

import ipaddress

def ip_is_blocked(host: str, blocked_cidrs: list[str]) -> bool:
    try:
        addr = ipaddress.ip_address(host)
    except ValueError:
        return False  # hostname; resolution-time check applies
    for cidr in blocked_cidrs:
        if addr in ipaddress.ip_network(cidr, strict=False):
            return True
    return False

# call before issuing the request: ip_is_blocked("10.1.2.3", os.getenv("IP_BLACK_LIST").split(","))

Type guard

def is_ip_literal(host: str) -> bool:
    try:
        ipaddress.ip_address(host)
        return True
    except ValueError:
        return False

Try / catch

from plugin.link.infra.tool_exector.ssrf_guard import OutboundPolicyError

try:
    policy.validate_url(url)
except OutboundPolicyError as exc:
    logger.warning("outbound blocked by policy: %s (url=%s)", exc, url)
    return None

Prevention

When it happens

Trigger: Calling validate_url (directly or via create_socket_factory) with a URL whose host is a literal IP inside a blocked CIDR, or whose hostname DNS-resolves (at socket_factory time) to an address in the blocklist; also triggered for IPv4-mapped IPv6 addresses whose embedded IPv4 falls in a blocked network.

Common situations: Env misconfig: an IP black-list range (e.g. 10.0.0.0/8) is broader than intended and now covers a legitimate tool endpoint; a tool's endpoint moved into a blacklisted range; DNS resolves a public name to an internal/blocked address (split-horizon DNS); tests that connect to localhost while 127.0.0.1/8 was blacklisted.

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/5ae8510183d5bff5. Report an issue: GitHub.

Appendix: source

Thrown at core/plugin/link/infra/tool_exector/ssrf_guard.py:114

        """Return whether deployment configuration authorizes this exact private endpoint."""
        # Private exceptions intentionally support only exact plain paths. Keeping
        # semicolons in ``SplitResult.path`` prevents matrix parameters (including
        # a trailing empty ``;``) from comparing equal to the configured path.
        return (
            ";" not in parsed.path
            and _endpoint(parsed) in self.allowed_private_endpoints
        )

    def validate_address(
        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

View on GitHub (pinned to 5e758547a8)