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
- Unwrap to the net.OpError: 'connection refused' means the target is down; 'i/o timeout'/'no route' means networking/firewall.
- Check the service is listening on the expected port (ss -ltnp / kubectl get endpoints).
- Fix egress rules or routing (especially IPv6) from the client environment.
- 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
- Read the wrapped net.OpError before retrying: refused = service down, timeout = network.
- Health-check upstreams and use circuit breakers rather than blind retries.
- After deploys, expect transient refusals while endpoints re-register — backoff, don't hammer.
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
- ssrf: scheme %q not permitted (managed mode requires https)
- ssrf: host %q is blocked (loopback)
- ssrf: DNS resolution failed for %q: %w
- ssrf: host %q resolved to no addresses
- ssrf: could not parse resolved IP %v for host %q
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/48a89bc9b08026f6.
Report an issue: GitHub.