0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-05 20:29:55 +08:00
discourse/lib/final_destination/http.rb
David Taylor 6dfcbb6282
FEATURE: Support 'Happy Eyeballs' in FinalDestination::HTTP (#41680)
Ruby 3.4 shipped support for happy eyeballs in `Socket.tcp` and
`TCPSocket`. However, our `FinalDestination::HTTP` wrapper was
performing a DNS lookup and passing IP addresses one at a time when
opening the socket. That meant that we didn't benefit from the new Ruby
feature in most Discourse features.

This commit factors the strategy. Now, `FinalDestination::HTTP` encodes
the DNS result and passes it to the underlying implementation as a fake
hostname string. A patch to `Addrinfo` detects this fake hostname and
returns the given IPs instead of performing its own lookup.

For this Addrinfo patch to work, we also had to patch `TCPSocket` so
that it uses the ruby-based `Socket.tcp` rather than its native C
socket-opening code.

The result is that we now get the benefit of the native Ruby 'Happy
Eyeballs' support for concurrent ipv4 and ipv6 connections.

All this patching of low-level ruby classes is not ideal, but there is
no native way to control name resolution in `Net::HTTP` or its
dependencies.
2026-07-15 11:31:23 +01:00

35 lines
965 B
Ruby
Vendored

# frozen_string_literal: true
class FinalDestination::HTTP < Net::HTTP
# Ruby's Happy Eyeballs implementation will try every IP address at 250ms intervals.
# Limit the total to avoid DoS via a high-ip-count DNS response.
MAX_ADDRESSES_PER_FAMILY = 5
def connect
raise ArgumentError.new("address cannot be nil or empty") if @address.blank?
return super if @ipaddr
ips = FinalDestination::SSRFDetector.lookup_and_filter_ips(@address, timeout: @connect_timeout)
if proxy?
self.ipaddr = ips.first
return super
end
@final_destination_token = FinalDestination::Connector.encode(@address, capped_addresses(ips))
super
ensure
@final_destination_token = nil
end
def conn_address
@final_destination_token || super
end
private
def capped_addresses(ips)
ipv6, ipv4 = ips.partition { |ip| ip.include?(":") }
ipv6.first(MAX_ADDRESSES_PER_FAMILY) + ipv4.first(MAX_ADDRESSES_PER_FAMILY)
end
end