Fall back across resolved IPs in Gitlab::HTTP (Dependency Proxy 599)
Following up on my comment in this issue. I know the plan leans toward the Ruby 3.4 upgrade for native Happy Eyeballs, so this isn't meant to pre-empt that. I'm putting it up as a tested, SSRF-safe option in case a fix is useful before the upgrade lands, and because it shows one thing concretely: a Ruby upgrade alone won't fix this while UrlBlocker pins a single IP. Happy to defer or close if the upgrade is the path.
The bug
Gitlab::HTTP resolves a host to all its IPs but only connects to the first one (UrlBlocker#ip_address takes address_info.first, and Net::HTTP#connect opens one socket to it). If that IP is unreachable, typically a black-holed IPv6 under dual stack, the connect hangs until Net::OpenTimeout with no fallback, and the Dependency Proxy turns that into a 599 (HeadManifestService and RequestTokenService rescue Timeout::Error and return 599). Because the IP is pre-resolved and pinned for DNS-rebinding protection, Ruby 3.4's native Happy Eyeballs wouldn't kick in either; it only races when given a hostname or multiple addresses.
The change
connect tries the remaining resolved IPs before giving up. Kept conservative:
- SSRF-safe. Candidate IPs are exposed only after
validate_local_requesthas validated the whole resolved set; the allow-list short-circuits return before that, so they expose nothing. An allowlisted host can't leak a co-resolved private or loopback IP, and there's a regression test for it. - No re-resolution. Fallback reuses the
address_infofrom validation time, so there's no DNS-rebinding gap. - Narrow trigger. Only unreachable failures (
Net::OpenTimeout,ETIMEDOUT,EHOSTUNREACH,ENETUNREACH) fall back; a refused or reset connection still surfaces. - Sequential failover, not RFC 8305 racing. Each unreachable IP costs up to one
open_timeout. Racing could be a follow-up, or moot once 3.4 lands.
This touches UrlBlocker, so it should get AppSec review. The SSRF behaviour is pinned by the regression test.
How to validate
cd gems/gitlab-http
bundle exec rspec spec/gitlab/http_v2/ip_fallback_spec.rbFour examples: fallback past an unreachable first IP; every IP unreachable raises; a refused connection surfaces without falling back; an allowlisted host does not fall back to a co-resolved loopback IP.
Related to #560575.