fix(#123): add IP fallback dialing in safeDialContext
PR Ready Gate / clear-labels (pull_request) Successful in 2s
CI / test (pull_request) Successful in 15s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 47s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m49s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 2m15s
PR Ready Gate / clear-labels (pull_request) Successful in 2s
CI / test (pull_request) Successful in 15s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 47s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m49s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 2m15s
Previously safeDialContext only dialed the first resolved IP. If the connection failed, it returned an error without trying other IPs. Now it iterates all validated IPs and returns the first successful connection, or the last error if all fail. This matches the resilience behavior of a plain net.Dialer on multi-IP hostnames. Addresses review finding: safeDialContext only dials first resolved IP. All IPs are still validated before any dial attempt is made.
This commit is contained in:
+16
-4
@@ -135,12 +135,24 @@ func safeDialContext(ctx context.Context, network, addr string) (net.Conn, error
|
||||
return nil, fmt.Errorf("safeDialContext: blocked: %q resolves to private/reserved IP %s", host, a.IP)
|
||||
}
|
||||
}
|
||||
// Dial the first resolved IP directly to avoid a second lookup.
|
||||
// Try each resolved IP in order, returning the first successful connection.
|
||||
// Fallback is important when a hostname resolves to multiple IPs and the first
|
||||
// is temporarily unreachable. All IPs were already validated above, so dialing
|
||||
// any of them is safe.
|
||||
//
|
||||
// Timeout: 10s per the design (PLAN.md); the outer http.Client has a 30s
|
||||
// total timeout, but the dial itself needs an independent bound so a slow
|
||||
// TCP connect does not consume the full 30s without cancellation.
|
||||
// total timeout, but the per-dial timeout ensures a slow TCP connect on one IP
|
||||
// doesn't consume the budget needed to try others.
|
||||
d := &net.Dialer{Timeout: 10 * time.Second}
|
||||
return d.DialContext(ctx, network, net.JoinHostPort(addrs[0].IP.String(), port))
|
||||
var lastErr error
|
||||
for _, a := range addrs {
|
||||
conn, err := d.DialContext(ctx, network, net.JoinHostPort(a.IP.String(), port))
|
||||
if err == nil {
|
||||
return conn, nil
|
||||
}
|
||||
lastErr = err
|
||||
}
|
||||
return nil, fmt.Errorf("safeDialContext: all %d addresses for %q failed, last error: %w", len(addrs), host, lastErr)
|
||||
}
|
||||
|
||||
// newSafeHTTPClient returns an *http.Client with the SSRF-blocking safeDialContext
|
||||
|
||||
Reference in New Issue
Block a user