iflytek/astron-agent · error · OutboundPolicyError

Outbound hostname is blocked

Error message

Outbound hostname is blocked

What it means

OutboundPolicy.validate_url checks the URL's hostname against the configured domain blacklist before DNS resolution. If the normalized hostname equals a blacklisted rule or is a subdomain of one (matching on label boundaries, including wildcard '*.rule' entries), it raises OutboundPolicyError('Outbound hostname is blocked'). This is the first line of the SSRF/egress defense for tool HTTP calls.

Solutions

  1. Check DOMAIN_BLACK_LIST (and any '*.domain' wildcard entries) in the link-service environment for the hostname from the tool URL
  2. If the domain is safe and required, remove or narrow the blacklist entry, or re-register the tool against an allowed domain
  3. Verify hostname normalization (case, trailing dot, IDNA) is not causing an unintended match

Example fix

// before (env)
DOMAIN_BLACK_LIST=internal.example.com
// after (allow a required subdomain)
DOMAIN_BLACK_LIST=other-internal.example.com
Defensive patterns

Strategy: validation

Validate before calling

def domain_allowed(host, blocked_domains):
    h = host.lower().rstrip('.')
    return not any(h == d or h.endswith('.' + d) for d in blocked_domains)

Try / catch

try:
    result = await run.do_call(span)
except CallThirdApiException as e:
    if 'hostname is blocked' in str(e.err):
        raise DomainBlockedError(e.err) from e
    raise

Prevention

When it happens

Trigger: Calling create_socket_factory or HttpRun._validate_destination with a URL whose host matches an entry in DOMAIN_BLACK_LIST (env), including any subdomain — e.g. blacklist 'evil.com' blocks 'api.evil.com' too.

Common situations: Ops blacklisted a domain that a tool legitimately needs; a suffix collision (e.g. blacklist contains 'notreal.com' and tool uses 'real.com' is NOT matched, but 'x.notreal.com' would be); tool was registered to a domain later added to the blacklist.

Related errors


AI-assisted analysis of iflytek/astron-agent@5e758547a8 (2026-09-12). Data as JSON: /api/errors/d549c7cbcb65d0a6. Report an issue: GitHub.

Appendix: source

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

            const.IP_WHITE_LIST_KEY,
        )
        blocked_domains = _parse_domains(os.getenv(const.DOMAIN_BLACK_LIST_KEY, ""))
        allowed_private_endpoints = _parse_private_endpoints(
            os.getenv(const.PRIVATE_ENDPOINT_ALLOW_LIST_KEY, "")
        )
        return cls(
            blocked_networks,
            allowed_literal_networks,
            blocked_domains,
            allowed_private_endpoints,
        )

    def validate_url(self, url: str) -> SplitResult:
        """Validate URL syntax and any literal destination before DNS resolution."""
        parsed = _parse_http_url(url)
        normalized_host = _normalize_hostname(parsed.hostname or "")
        if self.is_domain_blocked(normalized_host):
            raise OutboundPolicyError("Outbound hostname is blocked")

        literal = _parse_ip(normalized_host)
        if literal is not None:
            self.validate_address(
                literal,
                allow_private_endpoint=self.is_private_endpoint_allowed(parsed),
                allow_literal_exception=True,
            )
        return parsed

    def is_private_endpoint_allowed(self, parsed: SplitResult) -> bool:
        """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

View on GitHub (pinned to 5e758547a8)