JuliusBrussee/caveman · error

ssrf: DNS resolution failed for %q: %w

Error message

ssrf: DNS resolution failed for %q: %w

What it means

Pre-flight DNS failure in ssrf.ValidateURL: net.DefaultResolver.LookupIPAddr returned an error for the hostname. The original resolver error is wrapped with %w, so the cause (NXDOMAIN, timeout, SERVFAIL) is visible in the chain.

Source

Thrown at shared/platform/ssrf/ssrf.go:220

	// If host is an IP literal, check it directly without a DNS round-trip.
	if addr, err := netip.ParseAddr(host); err == nil {
		return checkAddr(addr, host, port, cfg)
	}

	// "localhost" is explicitly blocked regardless of what DNS says — unless a
	// self-hosted operator allowlisted it (resolution still runs, so every
	// resolved address is range-checked below like any other).
	if strings.EqualFold(host, "localhost") && !(!cfg.ManagedMode && isInAllowList(host, port, cfg.AllowList)) {
		return fmt.Errorf("ssrf: host %q is blocked (loopback)", host)
	}

	// Resolve ALL addresses the hostname currently maps to.  A hostname that
	// returns even one blocked address is rejected (defense-in-depth against
	// split-horizon / DNS rebinding scenarios where the pre-flight check and
	// the dial see different answers).
	addrs, err := net.DefaultResolver.LookupIPAddr(ctx, host)
	if err != nil {
		return fmt.Errorf("ssrf: DNS resolution failed for %q: %w", host, err)
	}
	if len(addrs) == 0 {
		return fmt.Errorf("ssrf: host %q resolved to no addresses", host)
	}

	for _, ia := range addrs {
		a, ok := netip.AddrFromSlice(ia.IP)
		if !ok {
			return fmt.Errorf("ssrf: could not parse resolved IP %v for host %q", ia.IP, host)
		}
		a = a.Unmap() // normalise ::ffff:x.x.x.x → x.x.x.x
		if err := checkAddr(a, host, port, cfg); err != nil {
			return err
		}
	}
	return nil
}

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. dig/host the name from the same container/host to confirm it resolves: `dig +short example.com`.
  2. Fix the typo or use the canonical hostname from the provider docs.
  3. If the resolver is the problem, repair container DNS (resolv.conf, cluster DNS) or add the host to a local zone — do not bypass the SSRF check.
  4. Retry once for transient resolver timeouts before surfacing the error to the user.
Defensive patterns

Strategy: retry

Validate before calling

if _, err := net.DefaultResolver.LookupIPAddr(ctx, u.Hostname()); err != nil {
    return fmt.Errorf("host does not resolve: %w", err) // fail before building the request
}

Type guard

func isDNSFailure(err error) bool {
    var dnsErr *net.DNSError
    return errors.As(err, &dnsErr)
}

Try / catch

err := ssrf.ValidateURL(ctx, raw, cfg)
if err != nil {
    var dnsErr *net.DNSError
    if errors.As(err, &dnsErr) && dnsErr.IsTimeout && !dnsErr.IsNotFound {
        // transient resolver issue: retry once
    } else if dnsErr != nil && dnsErr.IsNotFound {
        // permanent: bad hostname, surface to user
    }
}

Prevention

When it happens

Trigger: Calling ssrf.ValidateURL with a hostname that does not exist (NXDOMAIN), a resolver that times out, or a host with only a malformed/unresolvable record. IP-literal hosts skip this path entirely.

Common situations: Typo'd domains in webhook config; recently-expired or not-yet-propagated DNS records; broken /etc/resolv.conf or blocked DNS egress in containers; split-horizon DNS where the name only resolves inside another network.

Understand the failure class

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/5856e8b1cbf6ee63. Report an issue: GitHub.