Skip to content

Commit c16092f

Browse files
committed
Lookup public IP using HTTP
Both OpenDNS and Google DNS has problems looking up the public IP on IPv6 connections. Lookup up using HTTP (TCP) seems more stable. The lookup time is about 110ms longer with this mechanism. checkip.amazonaws.com for now only resolvs to IPv4 addresses. If that would change we would need to lookup the A address of the domain and connect to that IP manually. It's like 2 more lines of code only, if connect_timeout isn't needed, then it's a bit more involved.
1 parent 43bfeec commit c16092f

File tree

2 files changed

+41
-16
lines changed

2 files changed

+41
-16
lines changed

lib/sparoid.rb

+16-6
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,21 @@ def write_cache
149149
end
150150
end
151151

152-
def public_ip
153-
Resolv::DNS.open(nameserver: ["208.67.222.222", "208.67.220.220"]) do |dns|
154-
dns.getresource("myip.opendns.com", Resolv::DNS::Resource::IN::A).address
152+
def public_ip(host = "checkip.amazonaws.com", port = 80) # rubocop:disable Metrics/MethodLength
153+
Socket.tcp(host, port, connect_timeout: 3) do |sock|
154+
sock.sync = true
155+
sock.print "GET / HTTP/1.1\r\nHost: #{host}\r\nConnection: close\r\n\r\n"
156+
status = sock.readline(chomp: true)
157+
raise(ResolvError, "#{host}:#{port} response: #{status}") unless status.start_with? "HTTP/1.1 200 "
158+
159+
content_length = 0
160+
until (header = sock.readline(chomp: true)).empty?
161+
if (m = header.match(/^Content-Length: (\d+)/))
162+
content_length = m[1].to_i
163+
end
164+
end
165+
ip = sock.read(content_length).chomp
166+
Resolv::IPv4.create ip
155167
end
156168
end
157169

@@ -172,9 +184,7 @@ class ResolvError < Error; end
172184
class Instance
173185
include Sparoid
174186

175-
private
176-
177-
def public_ip
187+
def public_ip(*args)
178188
@public_ip ||= super
179189
end
180190

test/sparoid_test.rb

+25-10
Original file line numberDiff line numberDiff line change
@@ -61,26 +61,41 @@ def test_it_sends_message_with_empty_cache_file
6161
end
6262
end
6363

64-
def test_it_resolves_public_ip_only_once_per_instance
65-
dns = Minitest::Mock.new
66-
dns.expect :getresource, Resolv::IPv4.create("1.1.1.1"), ["myip.opendns.com", Resolv::DNS::Resource::IN::A]
67-
Resolv::DNS.stub(:open, ->(_, &blk) { blk.call dns }) do
68-
s = Sparoid::Instance.new
69-
2.times do
70-
s.send(:public_ip)
71-
end
64+
def test_it_resolves_public_ip_only_once_per_instance # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
65+
server = TCPServer.new "127.0.0.1", 0
66+
host = server.addr[3]
67+
port = server.addr[1]
68+
Thread.new do
69+
client = server.accept
70+
client_ip = client.addr[3]
71+
assert_equal "GET / HTTP/1.1", client.readline(chomp: true)
72+
assert_match "Host: ", client.readline(chomp: true)
73+
assert_equal "Connection: close", client.readline(chomp: true)
74+
75+
client.print "HTTP/1.1 200 OK\r\n"
76+
client.print "Content-Length: #{client_ip.bytesize}\r\n"
77+
client.print "\r\n"
78+
client.print client_ip
79+
client.close
80+
server.close
81+
end
82+
83+
s = Sparoid::Instance.new
84+
2.times do
85+
ip = s.public_ip host, port
86+
assert_equal Resolv::IPv4.create("127.0.0.1"), ip
7287
end
73-
dns.verify
7488
end
7589

7690
def test_it_raises_resolve_error_on_dns_socket_error
7791
key = "0000000000000000000000000000000000000000000000000000000000000000"
7892
hmac_key = "0000000000000000000000000000000000000000000000000000000000000000"
93+
open_for_ip = Resolv::IPv4.create("1.1.1.1")
7994
error = ->(*_) { raise SocketError, "getaddrinfo: Name or service not known" }
8095

8196
Addrinfo.stub(:getaddrinfo, error) do
8297
assert_raises(Sparoid::ResolvError) do
83-
Sparoid::Instance.new.auth(key, hmac_key, "127.0.0.1", 1337)
98+
Sparoid::Instance.new.auth(key, hmac_key, "127.0.0.1", 1337, open_for_ip: open_for_ip)
8499
end
85100
end
86101
end

0 commit comments

Comments
 (0)