Skip to content

Commit eb7a1b5

Browse files
committed
Merge pull request #394 from redis/ipv6
Don't hardcode Ruby connection to IPv4
2 parents 900dbea + 8146e59 commit eb7a1b5

10 files changed

+123
-36
lines changed

lib/redis/connection/ruby.rb

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,9 @@ class TCPSocket < ::Socket
114114

115115
include SocketMixin
116116

117-
def self.connect(host, port, timeout)
118-
# Limit lookup to IPv4, as Redis doesn't yet do IPv6...
119-
addr = ::Socket.getaddrinfo(host, nil, Socket::AF_INET)
120-
sock = new(::Socket.const_get(addr[0][0]), Socket::SOCK_STREAM, 0)
121-
sockaddr = ::Socket.pack_sockaddr_in(port, addr[0][3])
117+
def self.connect_addrinfo(ai, port, timeout)
118+
sock = new(::Socket.const_get(ai[0]), Socket::SOCK_STREAM, 0)
119+
sockaddr = ::Socket.pack_sockaddr_in(port, ai[3])
122120

123121
begin
124122
sock.connect_nonblock(sockaddr)
@@ -135,6 +133,39 @@ def self.connect(host, port, timeout)
135133

136134
sock
137135
end
136+
137+
def self.connect(host, port, timeout)
138+
# Don't pass AI_ADDRCONFIG as flag to getaddrinfo(3)
139+
#
140+
# From the man page for getaddrinfo(3):
141+
#
142+
# If hints.ai_flags includes the AI_ADDRCONFIG flag, then IPv4
143+
# addresses are returned in the list pointed to by res only if the
144+
# local system has at least one IPv4 address configured, and IPv6
145+
# addresses are returned only if the local system has at least one
146+
# IPv6 address configured. The loopback address is not considered
147+
# for this case as valid as a configured address.
148+
#
149+
# We do want the IPv6 loopback address to be returned if applicable,
150+
# even if it is the only configured IPv6 address on the machine.
151+
# Also see: https://github.com/redis/redis-rb/pull/394.
152+
addrinfo = ::Socket.getaddrinfo(host, nil, Socket::AF_UNSPEC, Socket::SOCK_STREAM)
153+
154+
# From the man page for getaddrinfo(3):
155+
#
156+
# Normally, the application should try using the addresses in the
157+
# order in which they are returned. The sorting function used
158+
# within getaddrinfo() is defined in RFC 3484 [...].
159+
#
160+
addrinfo.each_with_index do |ai, i|
161+
begin
162+
return connect_addrinfo(ai, port, timeout)
163+
rescue SystemCallError
164+
# Raise if this was our last attempt.
165+
raise if addrinfo.length == i+1
166+
end
167+
end
168+
end
138169
end
139170

140171
class UNIXSocket < ::Socket

test/commands_on_value_types_test.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def test_flushall
9999

100100
def test_migrate
101101
redis_mock(:migrate => lambda { |*args| args }) do |redis|
102-
options = { :host => "localhost", :port => 1234 }
102+
options = { :host => "127.0.0.1", :port => 1234 }
103103

104104
assert_raise(RuntimeError, /host not specified/) do
105105
redis.migrate("foo", options.reject { |key, _| key == :host })
@@ -114,17 +114,17 @@ def test_migrate
114114

115115
# Test defaults
116116
actual = redis.migrate("foo", options)
117-
expected = ["localhost", "1234", "foo", default_db.to_s, default_timeout.to_s]
117+
expected = ["127.0.0.1", "1234", "foo", default_db.to_s, default_timeout.to_s]
118118
assert_equal expected, actual
119119

120120
# Test db override
121121
actual = redis.migrate("foo", options.merge(:db => default_db + 1))
122-
expected = ["localhost", "1234", "foo", (default_db + 1).to_s, default_timeout.to_s]
122+
expected = ["127.0.0.1", "1234", "foo", (default_db + 1).to_s, default_timeout.to_s]
123123
assert_equal expected, actual
124124

125125
# Test timeout override
126126
actual = redis.migrate("foo", options.merge(:timeout => default_timeout + 1))
127-
expected = ["localhost", "1234", "foo", default_db.to_s, (default_timeout + 1).to_s]
127+
expected = ["127.0.0.1", "1234", "foo", default_db.to_s, (default_timeout + 1).to_s]
128128
assert_equal expected, actual
129129
end
130130
end

test/connection_handling_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ def test_shutdown_with_error_from_multi_exec
157157

158158
def test_slaveof
159159
redis_mock(:slaveof => lambda { |host, port| "+SLAVEOF #{host} #{port}" }) do |redis|
160-
assert_equal "SLAVEOF localhost 6381", redis.slaveof("localhost", 6381)
160+
assert_equal "SLAVEOF somehost 6381", redis.slaveof("somehost", 6381)
161161
end
162162
end
163163

test/distributed_internals_test.rb

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,32 @@ class TestDistributedInternals < Test::Unit::TestCase
77
include Helper::Distributed
88

99
def test_provides_a_meaningful_inspect
10-
nodes = ["redis://localhost:#{PORT}/15", *NODES]
10+
nodes = ["redis://127.0.0.1:#{PORT}/15", *NODES]
1111
redis = Redis::Distributed.new nodes
1212

1313
assert_equal "#<Redis client v#{Redis::VERSION} for #{redis.nodes.map(&:id).join(', ')}>", redis.inspect
1414
end
1515

1616
def test_default_as_urls
17-
nodes = ["redis://localhost:#{PORT}/15", *NODES]
17+
nodes = ["redis://127.0.0.1:#{PORT}/15", *NODES]
1818
redis = Redis::Distributed.new nodes
19-
assert_equal ["redis://localhost:#{PORT}/15", *NODES], redis.nodes.map { |node| node.client.id}
19+
assert_equal ["redis://127.0.0.1:#{PORT}/15", *NODES], redis.nodes.map { |node| node.client.id}
2020
end
2121

2222
def test_default_as_config_hashes
23-
nodes = [OPTIONS.merge(:host => 'localhost'), OPTIONS.merge(:host => 'localhost', :port => PORT.next)]
23+
nodes = [OPTIONS.merge(:host => '127.0.0.1'), OPTIONS.merge(:host => 'somehost', :port => PORT.next)]
2424
redis = Redis::Distributed.new nodes
25-
assert_equal ["redis://localhost:#{PORT}/15","redis://localhost:#{PORT.next}/15"], redis.nodes.map { |node| node.client.id }
25+
assert_equal ["redis://127.0.0.1:#{PORT}/15","redis://somehost:#{PORT.next}/15"], redis.nodes.map { |node| node.client.id }
2626
end
2727

2828
def test_as_mix_and_match
29-
nodes = ["redis://localhost:7389/15", OPTIONS.merge(:host => 'localhost'), OPTIONS.merge(:host => 'localhost', :port => PORT.next)]
29+
nodes = ["redis://127.0.0.1:7389/15", OPTIONS.merge(:host => 'somehost'), OPTIONS.merge(:host => 'somehost', :port => PORT.next)]
3030
redis = Redis::Distributed.new nodes
31-
assert_equal ["redis://localhost:7389/15", "redis://localhost:#{PORT}/15", "redis://localhost:#{PORT.next}/15"], redis.nodes.map { |node| node.client.id }
31+
assert_equal ["redis://127.0.0.1:7389/15", "redis://somehost:#{PORT}/15", "redis://somehost:#{PORT.next}/15"], redis.nodes.map { |node| node.client.id }
3232
end
3333

3434
def test_override_id
35-
nodes = [OPTIONS.merge(:host => 'localhost', :id => "test"), OPTIONS.merge( :host => 'localhost', :port => PORT.next, :id => "test1")]
35+
nodes = [OPTIONS.merge(:host => '127.0.0.1', :id => "test"), OPTIONS.merge( :host => 'somehost', :port => PORT.next, :id => "test1")]
3636
redis = Redis::Distributed.new nodes
3737
assert_equal redis.nodes.first.client.id, "test"
3838
assert_equal redis.nodes.last.client.id, "test1"

test/distributed_key_tags_test.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@ class TestDistributedKeyTags < Test::Unit::TestCase
88
include Helper::Distributed
99

1010
def test_hashes_consistently
11-
r1 = Redis::Distributed.new ["redis://localhost:#{PORT}/15", *NODES]
12-
r2 = Redis::Distributed.new ["redis://localhost:#{PORT}/15", *NODES]
13-
r3 = Redis::Distributed.new ["redis://localhost:#{PORT}/15", *NODES]
11+
r1 = Redis::Distributed.new ["redis://127.0.0.1:#{PORT}/15", *NODES]
12+
r2 = Redis::Distributed.new ["redis://127.0.0.1:#{PORT}/15", *NODES]
13+
r3 = Redis::Distributed.new ["redis://127.0.0.1:#{PORT}/15", *NODES]
1414

1515
assert_equal r1.node_for("foo").id, r2.node_for("foo").id
1616
assert_equal r1.node_for("foo").id, r3.node_for("foo").id
1717
end
1818

1919
def test_allows_clustering_of_keys
2020
r = Redis::Distributed.new(NODES)
21-
r.add_node("redis://localhost:#{PORT}/14")
21+
r.add_node("redis://127.0.0.1:#{PORT}/14")
2222
r.flushdb
2323

2424
100.times do |i|
@@ -29,7 +29,7 @@ def test_allows_clustering_of_keys
2929
end
3030

3131
def test_distributes_keys_if_no_clustering_is_used
32-
r.add_node("redis://localhost:#{PORT}/14")
32+
r.add_node("redis://127.0.0.1:#{PORT}/14")
3333
r.flushdb
3434

3535
r.set "users:1", 1
@@ -40,7 +40,7 @@ def test_distributes_keys_if_no_clustering_is_used
4040

4141
def test_allows_passing_a_custom_tag_extractor
4242
r = Redis::Distributed.new(NODES, :tag => /^(.+?):/)
43-
r.add_node("redis://localhost:#{PORT}/14")
43+
r.add_node("redis://127.0.0.1:#{PORT}/14")
4444
r.flushdb
4545

4646
100.times do |i|

test/distributed_test.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class TestDistributed < Test::Unit::TestCase
77
include Helper::Distributed
88

99
def test_handle_multiple_servers
10-
@r = Redis::Distributed.new ["redis://localhost:#{PORT}/15", *NODES]
10+
@r = Redis::Distributed.new ["redis://127.0.0.1:#{PORT}/15", *NODES]
1111

1212
100.times do |idx|
1313
@r.set(idx.to_s, "foo#{idx}")
@@ -32,9 +32,9 @@ def test_add_nodes
3232
assert_equal 10, @r.nodes[0].client.timeout
3333
assert_equal logger, @r.nodes[0].client.logger
3434

35-
@r.add_node("redis://localhost:6380/14")
35+
@r.add_node("redis://127.0.0.1:6380/14")
3636

37-
assert_equal "localhost", @r.nodes[1].client.host
37+
assert_equal "127.0.0.1", @r.nodes[1].client.host
3838
assert_equal 6380, @r.nodes[1].client.port
3939
assert_equal 14, @r.nodes[1].client.db
4040
assert_equal 10, @r.nodes[1].client.timeout

test/helper.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,13 +151,13 @@ def teardown
151151
end
152152

153153
def redis_mock(commands, options = {}, &blk)
154-
RedisMock.start(commands) do |port|
154+
RedisMock.start(commands, options) do |port|
155155
yield _new_client(options.merge(:port => port))
156156
end
157157
end
158158

159159
def redis_mock_with_handler(handler, options = {}, &blk)
160-
RedisMock.start_with_handler(handler) do |port|
160+
RedisMock.start_with_handler(handler, options) do |port|
161161
yield _new_client(options.merge(:port => port))
162162
end
163163
end

test/internals_test.rb

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,62 @@ def test_does_not_change_self_client_options
343343
assert_equal "foo", redis.client.options[:scheme]
344344
end
345345

346+
def test_resolves_localhost
347+
assert_nothing_raised do
348+
Redis.new(OPTIONS.merge(:host => 'localhost')).ping
349+
end
350+
end
351+
352+
class << self
353+
def af_family_supported(af)
354+
hosts = {
355+
Socket::AF_INET => "127.0.0.1",
356+
Socket::AF_INET6 => "::1",
357+
}
358+
359+
begin
360+
s = Socket.new(af, Socket::SOCK_STREAM, 0)
361+
begin
362+
sa = Socket.pack_sockaddr_in(9999, hosts[af])
363+
s.bind(sa)
364+
yield
365+
rescue Errno::EADDRNOTAVAIL
366+
ensure
367+
s.close
368+
end
369+
rescue Errno::ESOCKTNOSUPPORT
370+
end
371+
end
372+
end
373+
374+
def af_test(host)
375+
commands = {
376+
:ping => lambda { |*_| "+pong" },
377+
}
378+
379+
redis_mock(commands, :host => host) do |redis|
380+
assert_nothing_raised do
381+
redis.ping
382+
end
383+
end
384+
end
385+
386+
driver(:ruby) do
387+
af_family_supported(Socket::AF_INET) do
388+
def test_connect_ipv4
389+
af_test("127.0.0.1")
390+
end
391+
end
392+
end
393+
394+
driver(:ruby) do
395+
af_family_supported(Socket::AF_INET6) do
396+
def test_connect_ipv6
397+
af_test("::1")
398+
end
399+
end
400+
end
401+
346402
def test_can_be_duped_to_create_a_new_connection
347403
clients = r.info["connected_clients"].to_i
348404

test/publish_subscribe_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ def test_unsubscribe_without_a_subscribe
191191
def test_subscribe_past_a_timeout
192192
# For some reason, a thread here doesn't reproduce the issue.
193193
sleep = %{sleep #{OPTIONS[:timeout] * 2}}
194-
publish = %{echo "publish foo bar\r\n" | nc localhost #{OPTIONS[:port]}}
194+
publish = %{echo "publish foo bar\r\n" | nc 127.0.0.1 #{OPTIONS[:port]}}
195195
cmd = [sleep, publish].join("; ")
196196

197197
IO.popen(cmd, "r+") do |pipe|

test/support/redis_mock.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ module RedisMock
44
class Server
55
VERBOSE = false
66

7-
def initialize(port, &block)
8-
@server = TCPServer.new("127.0.0.1", port)
7+
def initialize(port, options = {}, &block)
8+
@server = TCPServer.new(options[:host] || "127.0.0.1", port)
99
@server.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
1010
end
1111

@@ -53,8 +53,8 @@ def run
5353
# # Every connection will be closed immediately
5454
# end
5555
#
56-
def self.start_with_handler(blk)
57-
server = Server.new(MOCK_PORT)
56+
def self.start_with_handler(blk, options = {})
57+
server = Server.new(MOCK_PORT, options)
5858

5959
begin
6060
server.start(&blk)
@@ -75,7 +75,7 @@ def self.start_with_handler(blk)
7575
# assert_equal "PONG", Redis.new(:port => MOCK_PORT).ping
7676
# end
7777
#
78-
def self.start(commands = {}, &blk)
78+
def self.start(commands, options = {}, &blk)
7979
handler = lambda do |session|
8080
while line = session.gets
8181
argv = Array.new(line[1..-3].to_i) do
@@ -110,6 +110,6 @@ def self.start(commands = {}, &blk)
110110
end
111111
end
112112

113-
start_with_handler(handler, &blk)
113+
start_with_handler(handler, options, &blk)
114114
end
115115
end

0 commit comments

Comments
 (0)