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
- 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).
- 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).
- Narrow or remove the over-broad black-list entry that accidentally covers the destination.
- 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
- Keep the IP/segment black-list as narrow as the actual threat model requires and review it when tool endpoints change.
- Use PRIVATE_ENDPOINT_ALLOW_LIST for sanctioned internal endpoints instead of weakening the black-list.
- Log the resolved IP on every OutboundPolicyError to spot accidental blocklist coverage.
- Add unit tests for each configured tool endpoint against the current policy before deploying config changes.
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
- Outbound address is not globally routable
- 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/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)