clockworklabs/SpacetimeDB · error · io::Error

refusing to connect to private or special-purpose addresses

Error message

refusing to connect to private or special-purpose addresses

What it means

SpacetimeDB's host applies SSRF protection to every outgoing HTTP(S) request a database module makes. URLs with an IP-literal host are checked directly, and hostnames resolve through FilteredDnsResolver, which drops every blocked address - RFC 6890 special-purpose ranges including 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, documentation/benchmarking ranges, and their IPv6 analogues. If no allowed address remains, the request fails with PermissionDenied and this message.

Source

Thrown at crates/core/src/host/instance_env.rs:1036

///
/// If the user requests a timeout longer than this, we will clamp to this value.
/// 180 seconds accommodates long-running LLM and AI API calls,
/// which routinely take 30-120 seconds for complex requests.
const HTTP_MAX_TIMEOUT: Duration = Duration::from_secs(180);
const BLOCKED_HTTP_ADDRESS_ERROR: &str = "refusing to connect to private or special-purpose addresses";

struct FilteredDnsResolver;

impl reqwest::dns::Resolve for FilteredDnsResolver {
    fn resolve(&self, name: reqwest::dns::Name) -> reqwest::dns::Resolving {
        let host = name.as_str().to_owned();
        Box::pin(async move {
            let addrs = tokio::net::lookup_host((host.as_str(), 0)).await?;
            let filtered_addrs: Vec<SocketAddr> = addrs.filter(|addr| !is_blocked_ip(addr.ip())).collect();

            if filtered_addrs.is_empty() {
                return Err(
                    std::io::Error::new(std::io::ErrorKind::PermissionDenied, BLOCKED_HTTP_ADDRESS_ERROR).into(),
                );
            }

            Ok(Box::new(filtered_addrs.into_iter()) as reqwest::dns::Addrs)
        })
    }
}

fn is_blocked_ip_literal(url: &reqwest::Url) -> bool {
    match url.host() {
        Some(url::Host::Ipv4(ip)) => is_blocked_ip(IpAddr::V4(ip)),
        Some(url::Host::Ipv6(ip)) => is_blocked_ip(IpAddr::V6(ip)),
        Some(url::Host::Domain(_)) | None => false,
    }
}

fn is_blocked_ip(ip: IpAddr) -> bool {
    match ip {

View on GitHub (pinned to 524b4487d9)

Solutions

  1. Point the module at a public, externally reachable address instead.
  2. If internal egress is genuinely required, route through a public proxy/gateway you control that performs its own authentication.
  3. For local test builds only, the host can be compiled with the allow_loopback_http_for_tests feature to unblock loopback - never enable this in production.

Example fix

// before: dev URL shipped into a hosted module
let url = "http://localhost:8080/api";

// after: public endpoint reachable from the host's egress
let url = "https://api.example.com/v1";
Defensive patterns

Strategy: validation

Validate before calling

use std::net::IpAddr;

fn is_private_or_special(ip: IpAddr) -> bool {
    match ip {
        IpAddr::V4(v) => v.is_loopback() || v.is_private() || v.is_link_local() || v.is_unspecified(),
        IpAddr::V6(v) => v.is_loopback() || v.is_unspecified() || (v.segments()[0] & 0xfe00) == 0xfc00,
    }
}

// Resolve the target host first and fail fast with your own message if every
// address is blocked, instead of discovering it inside the module call.

Try / catch

match module_http_request(url).await {
    Err(e) if e.to_string().contains("private or special-purpose addresses") => {
        // Configuration error, not transient: surface it to the operator and point
        // the module at a public endpoint or an authenticated proxy.
    }
    r => r,
}

Prevention

When it happens

Trigger: A module HTTP client requesting http://localhost:PORT, http://127.0.0.1, http://169.254.169.254/ (cloud metadata), any 10.x / 172.16-31.x / 192.168.x address, or a DNS name that resolves only to private or loopback IPs (e.g. an internal cluster service name).

Common situations: A module calling its own SpacetimeDB node or a sibling internal service during development; webhooks pointing at internal hostnames; localhost dev URLs accidentally shipped to a hosted deployment.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16). Data as JSON: /api/errors/86a50f55bd92d8d6. Report an issue: GitHub.