mirror of
https://github.com/discourse/discourse.git
synced 2026-08-05 20:29:55 +08:00
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.
35 lines
965 B
Ruby
Vendored
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
|