diff --git a/gitea/client.go b/gitea/client.go index c265569..6639753 100644 --- a/gitea/client.go +++ b/gitea/client.go @@ -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