mirror of
https://github.com/discourse/discourse.git
synced 2026-08-06 13:08:40 +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.
56 lines
1.7 KiB
Ruby
Vendored
56 lines
1.7 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
describe FinalDestination::Connector do
|
|
describe "round-trip" do
|
|
it "recovers the addresses it encoded" do
|
|
token = described_class.encode("example.com", %w[1.2.3.4 2400:c800::1])
|
|
|
|
expect(described_class.token?(token)).to eq(true)
|
|
expect(described_class.addresses(token)).to eq(%w[1.2.3.4 2400:c800::1])
|
|
end
|
|
|
|
it "does not treat an ordinary hostname as a token" do
|
|
expect(described_class.token?("example.com")).to eq(false)
|
|
end
|
|
end
|
|
|
|
describe ".addresses_for_family" do
|
|
let(:ips) { %w[1.2.3.4 5.6.7.8 2400:c800::1] }
|
|
|
|
it "returns only IPv4 addresses for AF_INET" do
|
|
expect(described_class.addresses_for_family(ips, Socket::AF_INET)).to contain_exactly(
|
|
"1.2.3.4",
|
|
"5.6.7.8",
|
|
)
|
|
end
|
|
|
|
it "returns only IPv6 addresses for AF_INET6" do
|
|
expect(described_class.addresses_for_family(ips, Socket::AF_INET6)).to contain_exactly(
|
|
"2400:c800::1",
|
|
)
|
|
end
|
|
|
|
it "returns every address when the family is unspecified" do
|
|
expect(described_class.addresses_for_family(ips, nil)).to eq(ips)
|
|
end
|
|
end
|
|
|
|
# The host is attacker-controlled (it comes from the URL) and rides in the token
|
|
# only for readable errors. It must not be able to smuggle addresses past the
|
|
# SSRF filter by embedding the token's delimiters.
|
|
it "ignores delimiters smuggled into the host" do
|
|
vetted = %w[1.2.3.4 2400:c800::1]
|
|
|
|
[
|
|
"evil|9.9.9.9",
|
|
"evil|0.001|9.9.9.9",
|
|
"a|b|c|d",
|
|
"",
|
|
"9.9.9.9,6.6.6.6",
|
|
].each do |malicious_host|
|
|
token = described_class.encode(malicious_host, vetted)
|
|
|
|
expect(described_class.addresses(token)).to eq(vetted)
|
|
end
|
|
end
|
|
end
|