8 from dnsdisttests
import DNSDistTest
10 class TestBackendDiscovery(DNSDistTest
):
11 # these ports are hardcoded for now, sorry about that!
12 _noSVCBackendPort
= 10600
13 _svcNoUpgradeBackendPort
= 10601
14 _svcUpgradeDoTBackendPort
= 10602
15 _svcUpgradeDoHBackendPort
= 10603
16 _svcUpgradeDoTBackendDifferentAddrPort1
= 10604
17 _svcUpgradeDoTBackendDifferentAddrPort2
= 10605
18 _svcUpgradeDoTUnreachableBackendPort
= 10606
19 _svcBrokenDNSResponseBackendPort
= 10607
20 _svcUpgradeDoHBackendWithoutPathPort
= 10608
21 _connectionRefusedBackendPort
= 10609
22 _eofBackendPort
= 10610
23 _servfailBackendPort
= 10611
24 _wrongNameBackendPort
= 10612
25 _wrongIDBackendPort
= 10613
26 _tooManyQuestionsBackendPort
= 10614
27 _badQNameBackendPort
= 10615
28 _svcUpgradeDoTNoPortBackendPort
= 10616
29 _svcUpgradeDoHNoPortBackendPort
= 10617
30 _upgradedBackendsPool
= 'upgraded'
32 _consoleKey
= DNSDistTest
.generateConsoleKey()
33 _consoleKeyB64
= base64
.b64encode(_consoleKey
).decode('ascii')
34 _config_params
= ['_consoleKeyB64', '_consolePort', '_noSVCBackendPort', '_svcNoUpgradeBackendPort', '_svcUpgradeDoTBackendPort', '_upgradedBackendsPool', '_svcUpgradeDoHBackendPort', '_svcUpgradeDoTBackendDifferentAddrPort1', '_svcUpgradeDoTBackendDifferentAddrPort2', '_svcUpgradeDoTUnreachableBackendPort', '_svcBrokenDNSResponseBackendPort', '_svcUpgradeDoHBackendWithoutPathPort', '_connectionRefusedBackendPort', '_eofBackendPort', '_servfailBackendPort', '_wrongNameBackendPort', '_wrongIDBackendPort', '_tooManyQuestionsBackendPort', '_badQNameBackendPort', '_svcUpgradeDoTNoPortBackendPort', '_svcUpgradeDoHNoPortBackendPort']
35 _config_template
= """
37 controlSocket("127.0.0.1:%d")
39 setMaxTCPClientThreads(1)
42 newServer{address="127.0.0.1:%s", caStore='ca.pem', autoUpgrade=true, autoUpgradeKeep=false}:setUp()
44 -- SVCB record but no upgrade path available
45 newServer{address="127.0.0.1:%s", caStore='ca.pem', autoUpgrade=true, autoUpgradeKeep=false}:setUp()
47 -- SVCB upgrade to DoT, same address, keep the backend, different pool
48 newServer{address="127.0.0.1:%s", caStore='ca.pem', pool={'', 'another-pool'}, autoUpgrade=true, autoUpgradePool='%s', autoUpgradeKeep=true, source='127.0.0.1@lo'}:setUp()
50 -- SVCB upgrade to DoH, same address, do not keep the backend, same pool
51 newServer{address="127.0.0.1:%s", caStore='ca.pem', pool={'another-pool'}, autoUpgrade=true, autoUpgradeKeep=false}:setUp()
53 -- SVCB upgrade to DoT, different address, certificate is valid for the initial address
54 newServer{address="127.0.0.1:%s", caStore='ca.pem', autoUpgrade=true, autoUpgradeKeep=false}:setUp()
56 -- SVCB upgrade to DoT, different address, certificate is NOT valid for the initial address
57 newServer{address="127.0.0.2:%s", caStore='ca.pem', autoUpgrade=true, autoUpgradeKeep=false}:setUp()
59 -- SVCB upgrade to DoT but upgraded port is not reachable
60 newServer{address="127.0.0.1:%s", caStore='ca.pem', autoUpgrade=true, autoUpgradeKeep=false}:setUp()
62 -- The SVCB response is not valid
63 newServer{address="127.0.0.1:%s", caStore='ca.pem', autoUpgrade=true, autoUpgradeKeep=false}:setUp()
65 -- SVCB upgrade to DoH except the path is not specified
66 newServer{address="127.0.0.1:%s", caStore='ca.pem', autoUpgrade=true, autoUpgradeKeep=false}:setUp()
69 newServer({address="127.0.0.1:%s", caStore='ca.pem', pool={"", "other-pool"}, autoUpgrade=true, source='127.0.0.1@lo'}):setUp()
72 newServer({address="127.0.0.1:%s", caStore='ca.pem', autoUpgrade=true}):setUp()
75 newServer({address="127.0.0.1:%s", autoUpgrade=true}):setUp()
78 newServer({address="127.0.0.1:%s", autoUpgrade=true}):setUp()
81 newServer({address="127.0.0.1:%s", autoUpgrade=true}):setUp()
84 newServer({address="127.0.0.1:%s", autoUpgrade=true}):setUp()
87 newServer({address="127.0.0.1:%s", autoUpgrade=true}):setUp()
89 -- SVCB upgrade to DoT, same address, no port specified via SVCB
90 newServer{address="127.0.0.1:%s", caStore='ca.pem', autoUpgrade=true, autoUpgradeKeep=false}:setUp()
92 -- SVCB upgrade to DoH, same address, no port specified via SVCB
93 newServer{address="127.0.0.1:%s", caStore='ca.pem', autoUpgrade=true, autoUpgradeKeep=false}:setUp()
97 def NoSVCCallback(request
):
98 return dns
.message
.make_response(request
).to_wire()
100 def NoUpgradePathCallback(request
):
101 response
= dns
.message
.make_response(request
)
102 rrset
= dns
.rrset
.from_text(request
.question
[0].name
,
106 '1 no-upgrade. alpn="h3"')
107 response
.answer
.append(rrset
)
108 return response
.to_wire()
110 def UpgradeDoTCallback(request
):
111 response
= dns
.message
.make_response(request
)
112 rrset
= dns
.rrset
.from_text(request
.question
[0].name
,
116 '1 tls.tests.dnsdist.org. alpn="dot" port=10652 ipv4hint=127.0.0.1')
117 response
.answer
.append(rrset
)
118 # add a useless A record for good measure
119 rrset
= dns
.rrset
.from_text(request
.question
[0].name
,
124 response
.answer
.append(rrset
)
125 # plus more useless records in authority
126 rrset
= dns
.rrset
.from_text(request
.question
[0].name
,
131 response
.authority
.append(rrset
)
132 # and finally valid, albeit useless, hints
133 rrset
= dns
.rrset
.from_text('tls.tests.dnsdist.org.',
138 response
.additional
.append(rrset
)
139 rrset
= dns
.rrset
.from_text('tls.tests.dnsdist.org.',
144 response
.additional
.append(rrset
)
145 return response
.to_wire()
147 def UpgradeDoHCallback(request
):
148 response
= dns
.message
.make_response(request
)
149 rrset
= dns
.rrset
.from_text(request
.question
[0].name
,
153 '1 tls.tests.dnsdist.org. alpn="h2" port=10653 ipv4hint=127.0.0.1 key7="/dns-query{?dns}"')
154 response
.answer
.append(rrset
)
155 return response
.to_wire()
157 def UpgradeDoTDifferentAddr1Callback(request
):
158 response
= dns
.message
.make_response(request
)
159 rrset
= dns
.rrset
.from_text(request
.question
[0].name
,
163 '1 tls.tests.dnsdist.org. alpn="dot" port=10654 ipv4hint=127.0.0.2')
164 response
.answer
.append(rrset
)
165 return response
.to_wire()
167 def UpgradeDoTDifferentAddr2Callback(request
):
168 response
= dns
.message
.make_response(request
)
169 rrset
= dns
.rrset
.from_text(request
.question
[0].name
,
173 '1 tls.tests.dnsdist.org. alpn="dot" port=10655 ipv4hint=127.0.0.1')
174 response
.answer
.append(rrset
)
175 return response
.to_wire()
177 def UpgradeDoTUnreachableCallback(request
):
178 response
= dns
.message
.make_response(request
)
179 rrset
= dns
.rrset
.from_text(request
.question
[0].name
,
183 '1 tls.tests.dnsdist.org. alpn="dot" port=10656 ipv4hint=127.0.0.1')
184 response
.answer
.append(rrset
)
185 return response
.to_wire()
187 def BrokenResponseCallback(request
):
188 response
= dns
.message
.make_response(request
)
189 response
.use_edns(edns
=False)
190 response
.question
= []
191 return response
.to_wire()
193 def UpgradeDoHMissingPathCallback(request
):
194 response
= dns
.message
.make_response(request
)
195 rrset
= dns
.rrset
.from_text(request
.question
[0].name
,
199 '1 tls.tests.dnsdist.org. alpn="h2" port=10653 ipv4hint=127.0.0.1')
200 response
.answer
.append(rrset
)
201 return response
.to_wire()
203 def EOFCallback(request
):
206 def ServFailCallback(request
):
207 response
= dns
.message
.make_response(request
)
208 response
.set_rcode(dns
.rcode
.SERVFAIL
)
209 return response
.to_wire()
211 def WrongNameCallback(request
):
212 query
= dns
.message
.make_query('not-the-right-one.', dns
.rdatatype
.SVCB
)
213 response
= dns
.message
.make_response(query
)
214 response
.id = request
.id
215 return response
.to_wire()
217 def WrongIDCallback(request
):
218 response
= dns
.message
.make_response(request
)
219 response
.id = request
.id ^
42
220 return response
.to_wire()
222 def WrongIDCallback(request
):
223 response
= dns
.message
.make_response(request
)
224 response
.id = request
.id ^
42
225 return response
.to_wire()
227 def TooManyQuestionsCallback(request
):
228 response
= dns
.message
.make_response(request
)
229 response
.question
.append(response
.question
[0])
230 return response
.to_wire()
232 def BadQNameCallback(request
):
233 response
= dns
.message
.make_response(request
)
234 wire
= bytearray(response
.to_wire())
235 # mess up the first label length
239 def UpgradeDoTNoPortCallback(request
):
240 response
= dns
.message
.make_response(request
)
241 rrset
= dns
.rrset
.from_text(request
.question
[0].name
,
245 '1 tls.tests.dnsdist.org. alpn="dot" ipv4hint=127.0.0.1')
246 response
.answer
.append(rrset
)
247 return response
.to_wire()
249 def UpgradeDoHNoPortCallback(request
):
250 response
= dns
.message
.make_response(request
)
251 rrset
= dns
.rrset
.from_text(request
.question
[0].name
,
255 '1 tls.tests.dnsdist.org. alpn="h2" ipv4hint=127.0.0.1 key7="/dns-query{?dns}"')
256 response
.answer
.append(rrset
)
257 return response
.to_wire()
260 def startResponders(cls
):
261 tlsContext
= ssl
.SSLContext(ssl
.PROTOCOL_TLS_SERVER
)
262 tlsContext
.load_cert_chain('server.chain', 'server.key')
264 TCPNoSVCResponder
= threading
.Thread(name
='TCP no SVC Responder', target
=cls
.TCPResponder
, args
=[cls
._noSVCBackendPort
, cls
._toResponderQueue
, cls
._fromResponderQueue
, True, False, cls
.NoSVCCallback
])
265 TCPNoSVCResponder
.daemon
= True
266 TCPNoSVCResponder
.start()
268 TCPNoUpgradeResponder
= threading
.Thread(name
='TCP no upgrade Responder', target
=cls
.TCPResponder
, args
=[cls
._svcNoUpgradeBackendPort
, cls
._toResponderQueue
, cls
._fromResponderQueue
, False, False, cls
.NoUpgradePathCallback
])
269 TCPNoUpgradeResponder
.daemon
= True
270 TCPNoUpgradeResponder
.start()
272 # this one is special, does partial writes!
273 TCPUpgradeToDoTResponder
= threading
.Thread(name
='TCP upgrade to DoT Responder', target
=cls
.TCPResponder
, args
=[cls
._svcUpgradeDoTBackendPort
, cls
._toResponderQueue
, cls
._fromResponderQueue
, False, False, cls
.UpgradeDoTCallback
, None, False, '127.0.0.1', True])
274 TCPUpgradeToDoTResponder
.daemon
= True
275 TCPUpgradeToDoTResponder
.start()
276 # and the corresponding DoT responder
277 UpgradedDoTResponder
= threading
.Thread(name
='DoT upgraded Responder', target
=cls
.TCPResponder
, args
=[10652, cls
._toResponderQueue
, cls
._fromResponderQueue
, False, False, None, tlsContext
])
278 UpgradedDoTResponder
.daemon
= True
279 UpgradedDoTResponder
.start()
281 TCPUpgradeToDoHResponder
= threading
.Thread(name
='TCP upgrade to DoH Responder', target
=cls
.TCPResponder
, args
=[cls
._svcUpgradeDoHBackendPort
, cls
._toResponderQueue
, cls
._fromResponderQueue
, False, False, cls
.UpgradeDoHCallback
])
282 TCPUpgradeToDoHResponder
.daemon
= True
283 TCPUpgradeToDoHResponder
.start()
284 # and the corresponding DoH responder
285 UpgradedDOHResponder
= threading
.Thread(name
='DOH Responder', target
=cls
.DOHResponder
, args
=[10653, cls
._toResponderQueue
, cls
._fromResponderQueue
, False, False, None, tlsContext
])
286 UpgradedDOHResponder
.daemon
= True
287 UpgradedDOHResponder
.start()
289 TCPUpgradeToDoTDifferentAddrResponder
= threading
.Thread(name
='TCP upgrade to DoT different addr 1 Responder', target
=cls
.TCPResponder
, args
=[cls
._svcUpgradeDoTBackendDifferentAddrPort
1, cls
._toResponderQueue
, cls
._fromResponderQueue
, False, False, cls
.UpgradeDoTDifferentAddr1Callback
])
290 TCPUpgradeToDoTDifferentAddrResponder
.daemon
= True
291 TCPUpgradeToDoTDifferentAddrResponder
.start()
292 # and the corresponding DoT responder
293 UpgradedDoTResponder
= threading
.Thread(name
='DoT upgraded different addr 1 Responder', target
=cls
.TCPResponder
, args
=[10654, cls
._toResponderQueue
, cls
._fromResponderQueue
, False, False, None, tlsContext
, False, '127.0.0.2'])
294 UpgradedDoTResponder
.daemon
= True
295 UpgradedDoTResponder
.start()
297 TCPUpgradeToDoTDifferentAddrResponder
= threading
.Thread(name
='TCP upgrade to DoT different addr 2 Responder', target
=cls
.TCPResponder
, args
=[cls
._svcUpgradeDoTBackendDifferentAddrPort
2, cls
._toResponderQueue
, cls
._fromResponderQueue
, False, False, cls
.UpgradeDoTDifferentAddr2Callback
, None, False, '127.0.0.2'])
298 TCPUpgradeToDoTDifferentAddrResponder
.daemon
= True
299 TCPUpgradeToDoTDifferentAddrResponder
.start()
300 # and the corresponding DoT responder
301 UpgradedDoTResponder
= threading
.Thread(name
='DoT upgraded different addr 2 Responder', target
=cls
.TCPResponder
, args
=[10655, cls
._toResponderQueue
, cls
._fromResponderQueue
, False, False, None, tlsContext
, False])
302 UpgradedDoTResponder
.daemon
= True
303 UpgradedDoTResponder
.start()
305 TCPUpgradeToUnreachableDoTResponder
= threading
.Thread(name
='TCP upgrade to unreachable DoT Responder', target
=cls
.TCPResponder
, args
=[cls
._svcUpgradeDoTUnreachableBackendPort
, cls
._toResponderQueue
, cls
._fromResponderQueue
, False, False, cls
.UpgradeDoTUnreachableCallback
])
306 TCPUpgradeToUnreachableDoTResponder
.daemon
= True
307 TCPUpgradeToUnreachableDoTResponder
.start()
308 # and NO corresponding DoT responder
309 # this is not a mistake!
311 BrokenResponseResponder
= threading
.Thread(name
='Broken response Responder', target
=cls
.TCPResponder
, args
=[cls
._svcBrokenDNSResponseBackendPort
, cls
._toResponderQueue
, cls
._fromResponderQueue
, False, False, cls
.BrokenResponseCallback
])
312 BrokenResponseResponder
.daemon
= True
313 BrokenResponseResponder
.start()
315 DOHMissingPathResponder
= threading
.Thread(name
='DoH missing path Responder', target
=cls
.TCPResponder
, args
=[cls
._svcUpgradeDoHBackendWithoutPathPort
, cls
._toResponderQueue
, cls
._fromResponderQueue
, False, False, cls
.UpgradeDoHMissingPathCallback
])
316 DOHMissingPathResponder
.daemon
= True
317 DOHMissingPathResponder
.start()
319 EOFResponder
= threading
.Thread(name
='EOF Responder', target
=cls
.TCPResponder
, args
=[cls
._eofBackendPort
, cls
._toResponderQueue
, cls
._fromResponderQueue
, False, False, cls
.EOFCallback
])
320 EOFResponder
.daemon
= True
323 ServFailResponder
= threading
.Thread(name
='ServFail Responder', target
=cls
.TCPResponder
, args
=[cls
._servfailBackendPort
, cls
._toResponderQueue
, cls
._fromResponderQueue
, False, False, cls
.ServFailCallback
])
324 ServFailResponder
.daemon
= True
325 ServFailResponder
.start()
327 WrongNameResponder
= threading
.Thread(name
='Wrong Name Responder', target
=cls
.TCPResponder
, args
=[cls
._wrongNameBackendPort
, cls
._toResponderQueue
, cls
._fromResponderQueue
, False, False, cls
.WrongNameCallback
])
328 WrongNameResponder
.daemon
= True
329 WrongNameResponder
.start()
331 WrongIDResponder
= threading
.Thread(name
='Wrong ID Responder', target
=cls
.TCPResponder
, args
=[cls
._wrongIDBackendPort
, cls
._toResponderQueue
, cls
._fromResponderQueue
, False, False, cls
.WrongIDCallback
])
332 WrongIDResponder
.daemon
= True
333 WrongIDResponder
.start()
335 TooManyQuestionsResponder
= threading
.Thread(name
='Too many questions Responder', target
=cls
.TCPResponder
, args
=[cls
._tooManyQuestionsBackendPort
, cls
._toResponderQueue
, cls
._fromResponderQueue
, False, False, cls
.TooManyQuestionsCallback
])
336 TooManyQuestionsResponder
.daemon
= True
337 TooManyQuestionsResponder
.start()
339 badQNameResponder
= threading
.Thread(name
='Bad QName Responder', target
=cls
.TCPResponder
, args
=[cls
._badQNameBackendPort
, cls
._toResponderQueue
, cls
._fromResponderQueue
, False, False, cls
.BadQNameCallback
])
340 badQNameResponder
.daemon
= True
341 badQNameResponder
.start()
343 TCPUpgradeToDoTNoPortResponder
= threading
.Thread(name
='TCP upgrade to DoT (no port) Responder', target
=cls
.TCPResponder
, args
=[cls
._svcUpgradeDoTNoPortBackendPort
, cls
._toResponderQueue
, cls
._fromResponderQueue
, False, False, cls
.UpgradeDoTNoPortCallback
])
344 TCPUpgradeToDoTNoPortResponder
.daemon
= True
345 TCPUpgradeToDoTNoPortResponder
.start()
347 TCPUpgradeToDoHNoPortResponder
= threading
.Thread(name
='TCP upgrade to DoH (no port) Responder', target
=cls
.TCPResponder
, args
=[cls
._svcUpgradeDoHNoPortBackendPort
, cls
._toResponderQueue
, cls
._fromResponderQueue
, False, False, cls
.UpgradeDoHNoPortCallback
])
348 TCPUpgradeToDoHNoPortResponder
.daemon
= True
349 TCPUpgradeToDoHNoPortResponder
.start()
352 def checkBackendsUpgraded(self
):
353 output
= self
.sendConsoleCommand('showServers()')
357 for line
in output
.splitlines(False):
358 if line
.startswith('#') or line
.startswith('All'):
360 tokens
= line
.split()
361 self
.assertTrue(len(tokens
) == 13 or len(tokens
) == 14)
362 if tokens
[1] == '127.0.0.1:10652':
363 # in this particular case, the upgraded backend
364 # does not replace the existing one and thus
365 # the health-check is forced to auto (or lazy auto)
366 self
.assertEqual(tokens
[2], 'up')
368 self
.assertEqual(tokens
[2], 'UP')
370 if len(tokens
) == 14:
372 backends
[tokens
[1]] = pool
375 '127.0.0.1:10600': '',
376 '127.0.0.1:10601': '',
377 '127.0.0.1:10602': 'another-pool',
378 # 10603 has been upgraded to 10653 and removed
379 # 10604 has been upgraded to 10654 and removed
380 '127.0.0.2:10605': '',
381 '127.0.0.1:10606': '',
382 '127.0.0.1:10607': '',
383 '127.0.0.1:10608': '',
384 '127.0.0.1:10609': 'other-pool',
385 '127.0.0.1:10610': '',
386 '127.0.0.1:10611': '',
387 '127.0.0.1:10612': '',
388 '127.0.0.1:10613': '',
389 '127.0.0.1:10614': '',
390 '127.0.0.1:10615': '',
391 # these two are not upgraded because there is no backend listening on the default ports (443 and 853)
392 '127.0.0.1:10616': '',
393 '127.0.0.1:10617': '',
394 '127.0.0.1:10652': 'upgraded',
395 '127.0.0.1:10653': 'another-pool',
396 '127.0.0.2:10654': ''
399 return backends
== expected
401 def testBackendUpgrade(self
):
403 Backend Discovery: Upgrade
405 # enough time for discovery to happen
406 # 5s is not enough with TSAN
408 if not self
.checkBackendsUpgraded():
409 # let's wait a bit longer
411 self
.assertTrue(self
.checkBackendsUpgraded())
413 class TestBackendDiscoveryByHostname(DNSDistTest
):
414 _consoleKey
= DNSDistTest
.generateConsoleKey()
415 _consoleKeyB64
= base64
.b64encode(_consoleKey
).decode('ascii')
416 _config_params
= ['_consoleKeyB64', '_consolePort']
417 _config_template
= """
419 controlSocket("127.0.0.1:%d")
421 function resolveCB(hostname, ips)
422 print('Got response for '..hostname)
423 for _, ip in ipairs(ips) do
425 newServer(ip:toString())
429 getAddressInfo('dns.quad9.net.', resolveCB)
431 def checkBackends(self
):
432 output
= self
.sendConsoleCommand('showServers()')
435 for line
in output
.splitlines(False):
436 if line
.startswith('#') or line
.startswith('All'):
438 tokens
= line
.split()
439 self
.assertTrue(len(tokens
) == 13 or len(tokens
) == 14)
440 backends
[tokens
[1]] = tokens
[2]
442 if len(backends
) != 4:
445 for expected
in ['9.9.9.9:53', '149.112.112.112:53', '[2620:fe::9]:53', '[2620:fe::fe]:53']:
446 self
.assertIn(expected
, backends
)
447 for backend
in backends
:
448 self
.assertTrue(backends
[backend
])
451 def testBackendFromHostname(self
):
453 Backend Discovery: From hostname
455 # enough time for resolution to happen
457 if not self
.checkBackends():
459 self
.assertTrue(self
.checkBackends())