Improve instance domain detection logic (#278)

This commit is contained in:
l5y
2025-10-11 18:22:50 +02:00
committed by GitHub
parent a048a83c6c
commit 2240be1f2d
2 changed files with 152 additions and 26 deletions
+121 -3
View File
@@ -122,7 +122,13 @@ def determine_instance_domain
reverse = reverse_dns_domain
return [reverse, :reverse_dns] if reverse
[nil, :unknown]
public_ip = discover_public_ip_address
return [public_ip, :public_ip] if public_ip
protected_ip = discover_protected_ip_address
return [protected_ip, :protected_ip] if protected_ip
[discover_local_ip_address, :local_ip]
end
# Attempt to resolve a hostname via reverse DNS for the current machine.
@@ -156,6 +162,112 @@ def reverse_dns_domain
nil
end
# Locate a globally routable IP address exposed on the current host.
#
# @return [String, nil]
def discover_public_ip_address
address = ip_address_candidates.find { |candidate| public_ip_address?(candidate) }
address&.ip_address
end
# Locate a private IP address (e.g. RFC1918 or unique local) exposed on the current host.
#
# @return [String, nil]
def discover_protected_ip_address
address = ip_address_candidates.find { |candidate| protected_ip_address?(candidate) }
address&.ip_address
end
# Enumerate IP address candidates exposed on the local machine.
#
# @return [Array<Addrinfo>]
def ip_address_candidates
Socket.ip_address_list.select { |addr| addr.respond_to?(:ip?) && addr.ip? }
end
# Determine whether the supplied address is globally routable.
#
# @param addr [Addrinfo]
# @return [Boolean]
def public_ip_address?(addr)
ip = ipaddr_from(addr)
return false unless ip
return false if loopback_address?(addr, ip)
return false if link_local_address?(addr, ip)
return false if private_address?(addr, ip)
return false if unspecified_address?(ip)
true
end
# Determine whether the supplied address is suitable for protected network exposure.
#
# @param addr [Addrinfo]
# @return [Boolean]
def protected_ip_address?(addr)
ip = ipaddr_from(addr)
return false unless ip
return false if loopback_address?(addr, ip)
return false if link_local_address?(addr, ip)
private_address?(addr, ip)
end
# Convert an Addrinfo into an IPAddr instance for further inspection.
#
# @param addr [Addrinfo]
# @return [IPAddr, nil]
def ipaddr_from(addr)
ip = addr.ip_address
return nil if ip.nil? || ip.empty?
IPAddr.new(ip)
rescue IPAddr::InvalidAddressError
nil
end
# Determine whether the address is a loopback interface.
#
# @param addr [Addrinfo]
# @param ip [IPAddr]
# @return [Boolean]
def loopback_address?(addr, ip)
(addr.respond_to?(:ipv4_loopback?) && addr.ipv4_loopback?) ||
(addr.respond_to?(:ipv6_loopback?) && addr.ipv6_loopback?) ||
ip.loopback?
end
# Determine whether the address resides within a link-local range.
#
# @param addr [Addrinfo]
# @param ip [IPAddr]
# @return [Boolean]
def link_local_address?(addr, ip)
(addr.respond_to?(:ipv6_linklocal?) && addr.ipv6_linklocal?) ||
(ip.respond_to?(:link_local?) && ip.link_local?)
end
# Determine whether the address is private (RFC1918 or unique local).
#
# @param addr [Addrinfo]
# @param ip [IPAddr]
# @return [Boolean]
def private_address?(addr, ip)
if addr.respond_to?(:ipv4?) && addr.ipv4? && addr.respond_to?(:ipv4_private?)
addr.ipv4_private?
else
ip.private?
end
end
# Determine whether the address is the unspecified (all zeros) address.
#
# @param ip [IPAddr]
# @return [Boolean]
def unspecified_address?(ip)
(ip.ipv4? || ip.ipv6?) && ip.to_i.zero?
end
# Determine the current application version using ``git describe`` when
# available.
#
@@ -467,8 +579,14 @@ def log_instance_domain_resolution
"Instance domain configured from INSTANCE_DOMAIN environment variable: #{INSTANCE_DOMAIN.inspect}"
when :reverse_dns
"Instance domain resolved via reverse DNS lookup: #{INSTANCE_DOMAIN.inspect}"
when :public_ip
"Instance domain resolved using public IP address: #{INSTANCE_DOMAIN.inspect}"
when :protected_ip
"Instance domain resolved using protected network IP address: #{INSTANCE_DOMAIN.inspect}"
when :local_ip
"Instance domain defaulted to local IP address: #{INSTANCE_DOMAIN.inspect}"
else
"Instance domain could not be determined from the environment or reverse DNS."
"Instance domain could not be determined from the environment or local network."
end
debug_log(message)
@@ -509,7 +627,7 @@ end
#
# @return [String]
def discover_local_ip_address
candidates = Socket.ip_address_list.select { |addr| addr.respond_to?(:ip?) && addr.ip? }
candidates = ip_address_candidates
ipv4 = candidates.find do |addr|
addr.respond_to?(:ipv4?) && addr.ipv4? && !(addr.respond_to?(:ipv4_loopback?) && addr.ipv4_loopback?)
+31 -23
View File
@@ -273,39 +273,47 @@ RSpec.describe "Potato Mesh Sinatra app" do
end
it "falls back to reverse DNS when available" do
address = instance_double(
Addrinfo,
ip?: true,
ipv4_loopback?: false,
ipv6_loopback?: false,
ipv6_linklocal?: false,
ip_address: "203.0.113.10",
)
address = Addrinfo.ip("203.0.113.10")
allow(Socket).to receive(:ip_address_list).and_return([address])
allow(Resolv).to receive(:getname).with("203.0.113.10").and_return("federation.example.com")
allow(Resolv).to receive(:getname).with("203.0.113.10").and_return("chara.htznr.fault.dev")
domain, source = determine_instance_domain
expect(domain).to eq("federation.example.com")
expect(domain).to eq("chara.htznr.fault.dev")
expect(source).to eq(:reverse_dns)
end
it "returns nil when no sources provide a domain" do
address = instance_double(
Addrinfo,
ip?: true,
ipv4_loopback?: false,
ipv6_loopback?: false,
ipv6_linklocal?: false,
ip_address: "198.51.100.12",
)
allow(Socket).to receive(:ip_address_list).and_return([address])
allow(Resolv).to receive(:getname).with("198.51.100.12").and_raise(Resolv::ResolvError)
it "falls back to a public IP address when reverse DNS is unavailable" do
public_address = Addrinfo.ip("203.0.113.20")
allow(Socket).to receive(:ip_address_list).and_return([public_address])
allow(Resolv).to receive(:getname).and_raise(Resolv::ResolvError)
domain, source = determine_instance_domain
expect(domain).to be_nil
expect(source).to eq(:unknown)
expect(domain).to eq("203.0.113.20")
expect(source).to eq(:public_ip)
end
it "falls back to a protected IP address when only private networks exist" do
private_address = Addrinfo.ip("10.0.0.5")
allow(Socket).to receive(:ip_address_list).and_return([private_address])
allow(Resolv).to receive(:getname).and_raise(Resolv::ResolvError)
domain, source = determine_instance_domain
expect(domain).to eq("10.0.0.5")
expect(source).to eq(:protected_ip)
end
it "falls back to a local IP address when no other sources are available" do
loopback_address = Addrinfo.ip("127.0.0.1")
allow(Socket).to receive(:ip_address_list).and_return([loopback_address])
allow(Resolv).to receive(:getname).and_raise(Resolv::ResolvError)
domain, source = determine_instance_domain
expect(domain).to eq("127.0.0.1")
expect(source).to eq(:local_ip)
end
end