From 5ac93bea70792362cb193a6f7d361fa032870962 Mon Sep 17 00:00:00 2001 From: claw Date: Thu, 14 May 2026 01:44:32 -0700 Subject: [PATCH] fix(#123): add IP fallback dialing in safeDialContext 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. --- gitea/client.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) 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