JuliusBrussee/caveman · error

ssrf: dial validated addresses for %q: %w

Error message

ssrf: dial validated addresses for %q: %w

What it means

Dial-time failure inside the SSRF DialContext: every address that passed range validation was attempted and the underlying dial errored for each; the last error is wrapped with the hostname via %w. Note the SSRF checks themselves succeeded — this is a genuine connectivity failure (refused, unreachable, TLS-timeout at TCP layer) on already-validated addresses.

Source

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

		}
		checked := make([]netip.Addr, 0, len(addrs))
		for _, resolved := range addrs {
			resolved = resolved.WithZone("").Unmap()
			if err := checkAddr(resolved, host, port, cfg); err != nil {
				return nil, err
			}
			checked = append(checked, resolved)
		}

		var lastErr error
		for _, resolved := range checked {
			conn, err := dial(ctx, network, net.JoinHostPort(resolved.String(), port))
			if err == nil {
				return conn, nil
			}
			lastErr = err
		}
		return nil, fmt.Errorf("ssrf: dial validated addresses for %q: %w", host, lastErr)
	}
}

// NewHTTPClient returns an *http.Client whose Transport enforces the SSRF
// policy at dial time.  The caller may set additional fields (Timeout, etc.)
// on the returned client.
//
// Use this to create the gateway's upstream client so all outbound provider
// requests are guarded even against DNS-rebinding attacks.
func NewHTTPClient(cfg Config) *http.Client {
	t := http.DefaultTransport.(*http.Transport).Clone()
	// SSRF enforcement observes the address passed to DialContext. Go's
	// default transport may instead dial an HTTP(S)_PROXY address and leave the
	// proxy to connect to the request destination, which would move the guarded
	// boundary away from the host this client was built to protect. This package
	// has no destination-aware proxy contract, so protected clients are direct
	// by construction; callers that need a proxy must provide a separate,
	// explicitly validated client.

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Unwrap to the net.OpError: 'connection refused' means the target is down; 'i/o timeout'/'no route' means networking/firewall.
  2. Check the service is listening on the expected port (ss -ltnp / kubectl get endpoints).
  3. Fix egress rules or routing (especially IPv6) from the client environment.
  4. Retry with backoff for transient restarts; connection refused right after a deploy usually self-heals.

Example fix

// before
resp, err := client.Get(u) // opaque 'dial validated addresses' error

// after
resp, err := client.Get(u)
if err != nil {
    var oe *net.OpError
    if errors.As(err, &oe) {
        switch {
        case errors.Is(oe.Err, syscall.ECONNREFUSED): // service down
        case oe.Timeout():                             // network/firewall
        }
    }
}
Defensive patterns

Strategy: retry

Type guard

func isDialFailure(err error) bool {
    var oe *net.OpError
    return errors.As(err, &oe)
}

Try / catch

resp, err := client.Do(req)
if err != nil {
    var oe *net.OpError
    if errors.As(err, &oe) {
        if errors.Is(oe.Err, syscall.ECONNREFUSED) {
            // service down: retry with backoff, then circuit-break
        } else if oe.Timeout() {
            // network/firewall: check egress before retrying
        }
    }
}

Prevention

When it happens

Trigger: http.Client.Do on the guarded client where checkAddr passed for all resolved addresses but net.Dialer failed: connection refused (service down), no route/unreachable, firewall drop, or exhausted ephemeral ports.

Common situations: Upstream service stopped or crashed (connection refused); security-group/firewall blocking egress on the target port; pod restarted and old IP now unreachable; IPv6 address attempted first with broken v6 routing.

Related errors


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