{"record":{"id":"86a50f55bd92d8d6","repo":"clockworklabs/SpacetimeDB","slug":"refusing-to-connect-to-private-or-special-purpose","errorCode":null,"errorMessage":"refusing to connect to private or special-purpose addresses","messagePattern":"refusing to connect to private or special-purpose addresses","errorType":"exception","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"crates/core/src/host/instance_env.rs","lineNumber":1145,"sourceCode":"/// If the user requests a timeout longer than this, we will clamp to this value.\n/// 180 seconds accommodates long-running LLM and AI API calls,\n/// which routinely take 30-120 seconds for complex requests.\nconst HTTP_MAX_TIMEOUT: Duration = Duration::from_secs(180);\nconst MODULE_HTTP_DISABLED_ERROR: &str = \"module outbound HTTP requests are disabled\";\nconst BLOCKED_HTTP_ADDRESS_ERROR: &str = \"refusing to connect to private or special-purpose addresses\";\n\nstruct FilteredDnsResolver;\n\nimpl reqwest::dns::Resolve for FilteredDnsResolver {\n    fn resolve(&self, name: reqwest::dns::Name) -> reqwest::dns::Resolving {\n        let host = name.as_str().to_owned();\n        Box::pin(async move {\n            let addrs = tokio::net::lookup_host((host.as_str(), 0)).await?;\n            let filtered_addrs: Vec<SocketAddr> = addrs.filter(|addr| !is_blocked_ip(addr.ip())).collect();\n\n            if filtered_addrs.is_empty() {\n                return Err(\n                    std::io::Error::new(std::io::ErrorKind::PermissionDenied, BLOCKED_HTTP_ADDRESS_ERROR).into(),\n                );\n            }\n\n            Ok(Box::new(filtered_addrs.into_iter()) as reqwest::dns::Addrs)\n        })\n    }\n}\n\nfn is_blocked_ip_literal(url: &reqwest::Url) -> bool {\n    match url.host() {\n        Some(url::Host::Ipv4(ip)) => is_blocked_ip(IpAddr::V4(ip)),\n        Some(url::Host::Ipv6(ip)) => is_blocked_ip(IpAddr::V6(ip)),\n        Some(url::Host::Domain(_)) | None => false,\n    }\n}\n\nfn is_blocked_ip(ip: IpAddr) -> bool {\n    match ip {","sourceCodeStart":1127,"sourceCodeEnd":1163,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/eddf9f5014579a50d4b67630e28b6e15cad9c4af/crates/core/src/host/instance_env.rs#L1127-L1163","documentation":"SpacetimeDB's host environment installs a hardened reqwest DNS resolver (FilteredDnsResolver) for HTTP requests made from reducers/procedures. It resolves the hostname, then filters out every address that falls in a private or special-purpose range (loopback, link-local, private nets, multicast, etc., per is_blocked_ip at crates/core/src/host/instance_env.rs:1053). If the resolver strips ALL resolved addresses, the request fails with io::ErrorKind::PermissionDenied and this message. It is an anti-SSRF guard: hosted module code must not reach the host's internal network.","triggerScenarios":"Calling http_request / a reqwest client from module code toward a host whose DNS resolves only to blocked ranges: http://localhost:PORT, http://127.0.0.1, http://10.x.x.x, http://192.168.x.x, http://172.16-31.x.x, http://169.254.169.254 (cloud metadata), ::1, fc00::/7, or a public-looking domain that DNS-rebinds to an internal IP. IP-literal URLs are checked up-front by is_blocked_ip_literal and fail before DNS is even consulted.","commonSituations":"Developing against a local API (localhost:8080) or an in-cluster/LAN service from a module running on a SpacetimeDB host; containerized setups where the target service only binds loopback; accidentally pointing a module at a cloud metadata endpoint; DNS records that changed to point at an internal address.","solutions":["Point the module at a publicly reachable endpoint (public DNS name resolving to a public IP) — internal addresses are blocked by design","Verify what the hostname actually resolves to (dig/host) from the machine running the SpacetimeDB host; if it resolves internally, fix DNS or move the service","If you control the host, expose the service on a routable address and, where the deployment supports it, allowlist it per the host's documented HTTP policy","Never attempt to bypass the filter (e.g. hex/decimal IP encodings); blocked ranges are also matched after DNS, so rebinding does not help"],"exampleFix":"// before (in module/reducer code)\nlet res = reqwest::get(\"http://localhost:9090/api\").await; // PermissionDenied: filtered resolver\n\n// after\nlet res = reqwest::get(\"https://api.example.com/endpoint\").await; // public address passes FilteredDnsResolver","handlingStrategy":"validation","validationCode":"// Before issuing the request from module/host code:\nuse std::net::{IpAddr, ToSocketAddrs};\nfn resolves_only_to_blocked(host: &str) -> bool {\n    let addrs: Vec<_> = (host, 0).to_socket_addrs().map(|it| it.collect()).unwrap_or_default();\n    !addrs.is_empty()\n        && addrs.iter().all(|a| a.ip().is_loopback() || a.ip().is_unspecified()\n            || matches!(a.ip(), IpAddr::V4(v4) if v4.is_private() || v4.is_link_local()))\n}\nif resolves_only_to_blocked(\"my-service.internal\") { return Err(anyhow::anyhow!(\"target would be SSRF-blocked\")); }","typeGuard":"fn is_publicly_routable(ip: IpAddr) -> bool {\n    !(ip.is_loopback() || ip.is_unspecified() || ip.is_multicast()\n        || matches!(ip, IpAddr::V4(v4) if v4.is_private() || v4.is_link_local() || v4.is_broadcast())\n        || matches!(ip, IpAddr::V6(v6) if (v6.segments()[0] & 0xfe00) == 0xfc00))\n}","tryCatchPattern":"match reqwest::get(url).await {\n    Err(e) if e.is_connect()\n        && e.to_string().contains(\"private or special-purpose\") => {\n            // blocked by SSRF filter: fix the target address, do not retry\n        }\n    other => other?,\n}","preventionTips":["Configure module HTTP targets as public DNS names, never localhost/LAN IPs","Validate target hostnames in config at deploy time by resolving them and rejecting private/special-purpose results","Remember DNS is re-checked after resolution: rebinding tricks still get filtered","Watch for PermissionDenied on the request as the telltale sign of the filter, not a network outage"],"tags":["network","security","ssrf","dns","rust","spacetimedb"],"backgroundTag":"ssrf-protection-blocked","analyzedSha":"eddf9f5014579a50d4b67630e28b6e15cad9c4af","analyzedAt":"2026-08-20T06:08:37.179Z","contentChangedAt":"2026-08-20T06:08:37.179Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}