astrid-runtime/astrid · error · std::io::Error::PermissionDenied

DNS resolved to an unauthorized private or local IP address

Error message

DNS resolved to an unauthorized private or local IP address

What it means

The capsule's SSRF-safe DNS resolver (SafeDnsResolver::resolve, used as reqwest's custom resolver) filtered every address the DNS lookup returned through the airlock and none were allowed: at least one resolved address was private/loopback/link-local, so the fetch is rejected with PermissionDenied and the `tripped` flag is set so the host emits a typed `airlock-rejected` error. This prevents sandboxed WASM guests from reaching internal networks, cloud metadata endpoints, or localhost.

Source

Thrown at crates/astrid-capsule/src/engine/wasm/host/http/ssrf.rs:89

                    // not-found as some other kind is a safe degradation
                    // (falls back to a connection error). Mirrors the `tripped`
                    // recovery channel.
                    if lookup_err_is_not_found(&e) {
                        dns_failed.store(true, Ordering::Relaxed);
                    }
                    return Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>);
                },
            };

            let (safe_addrs, saw_unsafe) = filter_safe_addrs(addrs, exempt);

            if safe_addrs.is_empty() {
                // All resolved addresses failed the airlock: a genuine SSRF
                // block. Mark `tripped` so the caller can emit the typed
                // `airlock-rejected` instead of a generic connection error.
                if saw_unsafe {
                    tripped.store(true, Ordering::Relaxed);
                    return Err(Box::new(std::io::Error::new(
                        std::io::ErrorKind::PermissionDenied,
                        "DNS resolved to an unauthorized private or local IP address",
                    ))
                        as Box<dyn std::error::Error + Send + Sync>);
                }
                // Resolved to an empty address set: an ordinary resolution miss,
                // not an airlock block — mark `dns_failed`, not `tripped`.
                dns_failed.store(true, Ordering::Relaxed);
                return Err(Box::new(std::io::Error::new(
                    std::io::ErrorKind::NotFound,
                    "host did not resolve to any address",
                ))
                    as Box<dyn std::error::Error + Send + Sync>);
            }

            let iter: reqwest::dns::Addrs = Box::new(safe_addrs.into_iter());
            Ok(iter)
        })

View on GitHub (pinned to affd8760f4)

Solutions

  1. Use the operator's exempt-host/allowlist mechanism (exempt_host at pre-flight) to sanction the specific local endpoint the guest must reach
  2. Have the guest call the public hostname instead of an internal IP or localhost name
  3. If the endpoint must be internal, run the request outside the capsule airlock in trusted host code rather than guest code
  4. Do not weaken filter_safe_addrs — treat this error as a signal that guest code is attempting an SSRF-style request

Example fix

// before: guest fetches a local dev server
let resp = http.get("http://localhost:9000/api").send()?; // airlock-rejected
// after: register the endpoint as the sanctioned exempt host, then fetch the same name
SafeDnsResolver::with_exempt_host("localhost:9000");
let resp = http.get("http://localhost:9000/api").send()?;
Defensive patterns

Strategy: validation

Validate before calling

// Before issuing the guest request, check the host will pass the airlock:
fn resolves_public(host: &str) -> bool {
    tokio::net::lookup_host((host, 0u16))
        .map(|addrs| addrs.all(|a| !is_private_or_local(a.ip())))
        .unwrap_or(false)
}

Type guard

fn is_public_addr(addr: &SocketAddr) -> bool {
    !(addr.ip().is_loopback()
        || addr.ip().is_private()
        || addr.ip().is_link_local()
        || addr.ip().is_unspecified())
}

Try / catch

match result {
    Err(e) if e.to_string().contains("unauthorized private or local IP") => {
        eprintln!("blocked by capsule airlock: host resolves to a private/local address");
        // check tripped flag / `airlock-rejected` typed signal, do not retry
    }
    other => ignore_or_propagate(other),
}

Prevention

When it happens

Trigger: A guest makes an HTTP request to a hostname whose DNS records point only to private/loopback/link-local addresses (e.g. 127.0.0.1, 10.x, 169.254.169.254) and that host is not the operator's exempt/allowlisted host; also when DNS rebinding makes a public name resolve to an internal IP.

Common situations: Guest code fetching http://localhost:8080 or an internal service name like http://metadata.google.internal; corporate DNS resolving internal-only names for the sandbox; a redirect to an internal host; testing against a locally-running server without registering it as the exempt host.

Understand the failure class

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/ff3cbca85d763247. Report an issue: GitHub.