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_request has 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_info from 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.rb

Four 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.

Merge request reports

Loading
Loading