Skip to content

Commit 3d135f9

Browse files
Add an explicit with_udp_and_tcp helper to test_dns.rb
This helper tries to bind UDP and TCP sockets to the same port, by retrying the bind if the randomly-assinged UDP port is already taken in TCP. This fixes a flaky test. [Bug #20403]
1 parent 44ae1e8 commit 3d135f9

File tree

1 file changed

+188
-146
lines changed

1 file changed

+188
-146
lines changed

test/resolv/test_dns.rb

Lines changed: 188 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,50 @@ def with_udp(host, port)
6464
end
6565
end
6666

67+
def with_udp_and_tcp(host, port)
68+
if port == 0
69+
# Automatic port; we might need to retry until we find a port which is free on both UDP _and_ TCP.
70+
retries_remaining = 5
71+
t = nil
72+
u = nil
73+
begin
74+
begin
75+
u = UDPSocket.new
76+
u.bind(host, 0)
77+
_, udp_port, _, _ = u.addr
78+
t = TCPServer.new(host, udp_port)
79+
t.listen(1)
80+
rescue Errno::EADDRINUSE, Errno::EACCES
81+
# ADDRINUSE is what should get thrown if we try and bind a port which is already bound on UNIXen,
82+
# but windows can sometimes throw EACCESS.
83+
# See: https://stackoverflow.com/questions/48478869/cannot-bind-to-some-ports-due-to-permission-denied
84+
retries_remaining -= 1
85+
if retries_remaining > 0
86+
t&.close
87+
t = nil
88+
u&.close
89+
u = nil
90+
retry
91+
end
92+
raise
93+
end
94+
95+
# If we get to this point, we have a valid t & u socket
96+
yield u, t
97+
ensure
98+
t&.close
99+
u&.close
100+
end
101+
else
102+
# Explicitly specified port, don't retry the bind.
103+
with_udp(host, port) do |u|
104+
with_tcp(host, port) do |t|
105+
yield u, t
106+
end
107+
end
108+
end
109+
end
110+
67111
# [ruby-core:65836]
68112
def test_resolve_with_2_ndots
69113
conf = Resolv::DNS::Config.new :nameserver => ['127.0.0.1'], :ndots => 2
@@ -176,156 +220,154 @@ def test_query_ipv4_address_truncated_tcp_fallback
176220

177221
num_records = 50
178222

179-
with_udp('127.0.0.1', 0) {|u|
223+
with_udp_and_tcp('127.0.0.1', 0) {|u, t|
180224
_, server_port, _, server_address = u.addr
181-
with_tcp('127.0.0.1', server_port) {|t|
182-
client_thread = Thread.new {
183-
Resolv::DNS.open(:nameserver_port => [[server_address, server_port]]) {|dns|
184-
dns.getresources("foo.example.org", Resolv::DNS::Resource::IN::A)
185-
}
186-
}
187-
udp_server_thread = Thread.new {
188-
msg, (_, client_port, _, client_address) = Timeout.timeout(5) {u.recvfrom(4096)}
189-
id, word2, qdcount, ancount, nscount, arcount = msg.unpack("nnnnnn")
190-
qr = (word2 & 0x8000) >> 15
191-
opcode = (word2 & 0x7800) >> 11
192-
aa = (word2 & 0x0400) >> 10
193-
tc = (word2 & 0x0200) >> 9
194-
rd = (word2 & 0x0100) >> 8
195-
ra = (word2 & 0x0080) >> 7
196-
z = (word2 & 0x0070) >> 4
197-
rcode = word2 & 0x000f
198-
rest = msg[12..-1]
199-
assert_equal(0, qr) # 0:query 1:response
200-
assert_equal(0, opcode) # 0:QUERY 1:IQUERY 2:STATUS
201-
assert_equal(0, aa) # Authoritative Answer
202-
assert_equal(0, tc) # TrunCation
203-
assert_equal(1, rd) # Recursion Desired
204-
assert_equal(0, ra) # Recursion Available
205-
assert_equal(0, z) # Reserved for future use
206-
assert_equal(0, rcode) # 0:No-error 1:Format-error 2:Server-failure 3:Name-Error 4:Not-Implemented 5:Refused
207-
assert_equal(1, qdcount) # number of entries in the question section.
208-
assert_equal(0, ancount) # number of entries in the answer section.
209-
assert_equal(0, nscount) # number of entries in the authority records section.
210-
assert_equal(0, arcount) # number of entries in the additional records section.
211-
name = [3, "foo", 7, "example", 3, "org", 0].pack("Ca*Ca*Ca*C")
212-
assert_operator(rest, :start_with?, name)
213-
rest = rest[name.length..-1]
214-
assert_equal(4, rest.length)
215-
qtype, _ = rest.unpack("nn")
216-
assert_equal(1, qtype) # A
217-
assert_equal(1, qtype) # IN
218-
id = id
219-
qr = 1
220-
opcode = opcode
221-
aa = 0
222-
tc = 1
223-
rd = rd
224-
ra = 1
225-
z = 0
226-
rcode = 0
227-
qdcount = 0
228-
ancount = num_records
229-
nscount = 0
230-
arcount = 0
231-
word2 = (qr << 15) |
232-
(opcode << 11) |
233-
(aa << 10) |
234-
(tc << 9) |
235-
(rd << 8) |
236-
(ra << 7) |
237-
(z << 4) |
238-
rcode
239-
msg = [id, word2, qdcount, ancount, nscount, arcount].pack("nnnnnn")
240-
type = 1
241-
klass = 1
242-
ttl = 3600
243-
rdlength = 4
244-
num_records.times do |i|
245-
rdata = [192,0,2,i].pack("CCCC") # 192.0.2.x (TEST-NET address) RFC 3330
246-
rr = [name, type, klass, ttl, rdlength, rdata].pack("a*nnNna*")
247-
msg << rr
248-
end
249-
u.send(msg[0...512], 0, client_address, client_port)
250-
}
251-
tcp_server_thread = Thread.new {
252-
ct = t.accept
253-
msg = ct.recv(512)
254-
msg.slice!(0..1) # Size (only for TCP)
255-
id, word2, qdcount, ancount, nscount, arcount = msg.unpack("nnnnnn")
256-
qr = (word2 & 0x8000) >> 15
257-
opcode = (word2 & 0x7800) >> 11
258-
aa = (word2 & 0x0400) >> 10
259-
tc = (word2 & 0x0200) >> 9
260-
rd = (word2 & 0x0100) >> 8
261-
ra = (word2 & 0x0080) >> 7
262-
z = (word2 & 0x0070) >> 4
263-
rcode = word2 & 0x000f
264-
rest = msg[12..-1]
265-
assert_equal(0, qr) # 0:query 1:response
266-
assert_equal(0, opcode) # 0:QUERY 1:IQUERY 2:STATUS
267-
assert_equal(0, aa) # Authoritative Answer
268-
assert_equal(0, tc) # TrunCation
269-
assert_equal(1, rd) # Recursion Desired
270-
assert_equal(0, ra) # Recursion Available
271-
assert_equal(0, z) # Reserved for future use
272-
assert_equal(0, rcode) # 0:No-error 1:Format-error 2:Server-failure 3:Name-Error 4:Not-Implemented 5:Refused
273-
assert_equal(1, qdcount) # number of entries in the question section.
274-
assert_equal(0, ancount) # number of entries in the answer section.
275-
assert_equal(0, nscount) # number of entries in the authority records section.
276-
assert_equal(0, arcount) # number of entries in the additional records section.
277-
name = [3, "foo", 7, "example", 3, "org", 0].pack("Ca*Ca*Ca*C")
278-
assert_operator(rest, :start_with?, name)
279-
rest = rest[name.length..-1]
280-
assert_equal(4, rest.length)
281-
qtype, _ = rest.unpack("nn")
282-
assert_equal(1, qtype) # A
283-
assert_equal(1, qtype) # IN
284-
id = id
285-
qr = 1
286-
opcode = opcode
287-
aa = 0
288-
tc = 0
289-
rd = rd
290-
ra = 1
291-
z = 0
292-
rcode = 0
293-
qdcount = 0
294-
ancount = num_records
295-
nscount = 0
296-
arcount = 0
297-
word2 = (qr << 15) |
298-
(opcode << 11) |
299-
(aa << 10) |
300-
(tc << 9) |
301-
(rd << 8) |
302-
(ra << 7) |
303-
(z << 4) |
304-
rcode
305-
msg = [id, word2, qdcount, ancount, nscount, arcount].pack("nnnnnn")
306-
type = 1
307-
klass = 1
308-
ttl = 3600
309-
rdlength = 4
310-
num_records.times do |i|
311-
rdata = [192,0,2,i].pack("CCCC") # 192.0.2.x (TEST-NET address) RFC 3330
312-
rr = [name, type, klass, ttl, rdlength, rdata].pack("a*nnNna*")
313-
msg << rr
314-
end
315-
msg = "#{[msg.bytesize].pack("n")}#{msg}" # Prefix with size
316-
ct.send(msg, 0)
317-
ct.close
225+
client_thread = Thread.new {
226+
Resolv::DNS.open(:nameserver_port => [[server_address, server_port]]) {|dns|
227+
dns.getresources("foo.example.org", Resolv::DNS::Resource::IN::A)
318228
}
319-
result, _ = assert_join_threads([client_thread, udp_server_thread, tcp_server_thread])
320-
assert_instance_of(Array, result)
321-
assert_equal(50, result.length)
322-
result.each_with_index do |rr, i|
323-
assert_instance_of(Resolv::DNS::Resource::IN::A, rr)
324-
assert_instance_of(Resolv::IPv4, rr.address)
325-
assert_equal("192.0.2.#{i}", rr.address.to_s)
326-
assert_equal(3600, rr.ttl)
229+
}
230+
udp_server_thread = Thread.new {
231+
msg, (_, client_port, _, client_address) = Timeout.timeout(5) {u.recvfrom(4096)}
232+
id, word2, qdcount, ancount, nscount, arcount = msg.unpack("nnnnnn")
233+
qr = (word2 & 0x8000) >> 15
234+
opcode = (word2 & 0x7800) >> 11
235+
aa = (word2 & 0x0400) >> 10
236+
tc = (word2 & 0x0200) >> 9
237+
rd = (word2 & 0x0100) >> 8
238+
ra = (word2 & 0x0080) >> 7
239+
z = (word2 & 0x0070) >> 4
240+
rcode = word2 & 0x000f
241+
rest = msg[12..-1]
242+
assert_equal(0, qr) # 0:query 1:response
243+
assert_equal(0, opcode) # 0:QUERY 1:IQUERY 2:STATUS
244+
assert_equal(0, aa) # Authoritative Answer
245+
assert_equal(0, tc) # TrunCation
246+
assert_equal(1, rd) # Recursion Desired
247+
assert_equal(0, ra) # Recursion Available
248+
assert_equal(0, z) # Reserved for future use
249+
assert_equal(0, rcode) # 0:No-error 1:Format-error 2:Server-failure 3:Name-Error 4:Not-Implemented 5:Refused
250+
assert_equal(1, qdcount) # number of entries in the question section.
251+
assert_equal(0, ancount) # number of entries in the answer section.
252+
assert_equal(0, nscount) # number of entries in the authority records section.
253+
assert_equal(0, arcount) # number of entries in the additional records section.
254+
name = [3, "foo", 7, "example", 3, "org", 0].pack("Ca*Ca*Ca*C")
255+
assert_operator(rest, :start_with?, name)
256+
rest = rest[name.length..-1]
257+
assert_equal(4, rest.length)
258+
qtype, _ = rest.unpack("nn")
259+
assert_equal(1, qtype) # A
260+
assert_equal(1, qtype) # IN
261+
id = id
262+
qr = 1
263+
opcode = opcode
264+
aa = 0
265+
tc = 1
266+
rd = rd
267+
ra = 1
268+
z = 0
269+
rcode = 0
270+
qdcount = 0
271+
ancount = num_records
272+
nscount = 0
273+
arcount = 0
274+
word2 = (qr << 15) |
275+
(opcode << 11) |
276+
(aa << 10) |
277+
(tc << 9) |
278+
(rd << 8) |
279+
(ra << 7) |
280+
(z << 4) |
281+
rcode
282+
msg = [id, word2, qdcount, ancount, nscount, arcount].pack("nnnnnn")
283+
type = 1
284+
klass = 1
285+
ttl = 3600
286+
rdlength = 4
287+
num_records.times do |i|
288+
rdata = [192,0,2,i].pack("CCCC") # 192.0.2.x (TEST-NET address) RFC 3330
289+
rr = [name, type, klass, ttl, rdlength, rdata].pack("a*nnNna*")
290+
msg << rr
327291
end
292+
u.send(msg[0...512], 0, client_address, client_port)
328293
}
294+
tcp_server_thread = Thread.new {
295+
ct = t.accept
296+
msg = ct.recv(512)
297+
msg.slice!(0..1) # Size (only for TCP)
298+
id, word2, qdcount, ancount, nscount, arcount = msg.unpack("nnnnnn")
299+
qr = (word2 & 0x8000) >> 15
300+
opcode = (word2 & 0x7800) >> 11
301+
aa = (word2 & 0x0400) >> 10
302+
tc = (word2 & 0x0200) >> 9
303+
rd = (word2 & 0x0100) >> 8
304+
ra = (word2 & 0x0080) >> 7
305+
z = (word2 & 0x0070) >> 4
306+
rcode = word2 & 0x000f
307+
rest = msg[12..-1]
308+
assert_equal(0, qr) # 0:query 1:response
309+
assert_equal(0, opcode) # 0:QUERY 1:IQUERY 2:STATUS
310+
assert_equal(0, aa) # Authoritative Answer
311+
assert_equal(0, tc) # TrunCation
312+
assert_equal(1, rd) # Recursion Desired
313+
assert_equal(0, ra) # Recursion Available
314+
assert_equal(0, z) # Reserved for future use
315+
assert_equal(0, rcode) # 0:No-error 1:Format-error 2:Server-failure 3:Name-Error 4:Not-Implemented 5:Refused
316+
assert_equal(1, qdcount) # number of entries in the question section.
317+
assert_equal(0, ancount) # number of entries in the answer section.
318+
assert_equal(0, nscount) # number of entries in the authority records section.
319+
assert_equal(0, arcount) # number of entries in the additional records section.
320+
name = [3, "foo", 7, "example", 3, "org", 0].pack("Ca*Ca*Ca*C")
321+
assert_operator(rest, :start_with?, name)
322+
rest = rest[name.length..-1]
323+
assert_equal(4, rest.length)
324+
qtype, _ = rest.unpack("nn")
325+
assert_equal(1, qtype) # A
326+
assert_equal(1, qtype) # IN
327+
id = id
328+
qr = 1
329+
opcode = opcode
330+
aa = 0
331+
tc = 0
332+
rd = rd
333+
ra = 1
334+
z = 0
335+
rcode = 0
336+
qdcount = 0
337+
ancount = num_records
338+
nscount = 0
339+
arcount = 0
340+
word2 = (qr << 15) |
341+
(opcode << 11) |
342+
(aa << 10) |
343+
(tc << 9) |
344+
(rd << 8) |
345+
(ra << 7) |
346+
(z << 4) |
347+
rcode
348+
msg = [id, word2, qdcount, ancount, nscount, arcount].pack("nnnnnn")
349+
type = 1
350+
klass = 1
351+
ttl = 3600
352+
rdlength = 4
353+
num_records.times do |i|
354+
rdata = [192,0,2,i].pack("CCCC") # 192.0.2.x (TEST-NET address) RFC 3330
355+
rr = [name, type, klass, ttl, rdlength, rdata].pack("a*nnNna*")
356+
msg << rr
357+
end
358+
msg = "#{[msg.bytesize].pack("n")}#{msg}" # Prefix with size
359+
ct.send(msg, 0)
360+
ct.close
361+
}
362+
result, _ = assert_join_threads([client_thread, udp_server_thread, tcp_server_thread])
363+
assert_instance_of(Array, result)
364+
assert_equal(50, result.length)
365+
result.each_with_index do |rr, i|
366+
assert_instance_of(Resolv::DNS::Resource::IN::A, rr)
367+
assert_instance_of(Resolv::IPv4, rr.address)
368+
assert_equal("192.0.2.#{i}", rr.address.to_s)
369+
assert_equal(3600, rr.ttl)
370+
end
329371
}
330372
end
331373

0 commit comments

Comments
 (0)