]> git.ipfire.org Git - thirdparty/pdns.git/blame - regression-tests.dnsdist/dnsdisttests.py
Merge pull request #6402 from chbruyand/dnsdist-issue-5763
[thirdparty/pdns.git] / regression-tests.dnsdist / dnsdisttests.py
CommitLineData
ca404e94
RG
1#!/usr/bin/env python2
2
95f0b802 3import copy
ca404e94
RG
4import os
5import socket
a227f47d 6import ssl
ca404e94
RG
7import struct
8import subprocess
9import sys
10import threading
11import time
12import unittest
5df86a8a 13import clientsubnetoption
b1bec9f0
RG
14import dns
15import dns.message
1ea747c0
RG
16import libnacl
17import libnacl.utils
ca404e94 18
b4f23783
CH
19# Python2/3 compatibility hacks
20if sys.version_info[0] == 2:
21 from Queue import Queue
22 range = xrange
23else:
24 from queue import Queue
25 range = range # allow re-export of the builtin name
26
27
ca404e94
RG
28class DNSDistTest(unittest.TestCase):
29 """
30 Set up a dnsdist instance and responder threads.
31 Queries sent to dnsdist are relayed to the responder threads,
32 who reply with the response provided by the tests themselves
33 on a queue. Responder threads also queue the queries received
34 from dnsdist on a separate queue, allowing the tests to check
35 that the queries sent from dnsdist were as expected.
36 """
37 _dnsDistPort = 5340
b052847c 38 _dnsDistListeningAddr = "127.0.0.1"
ca404e94 39 _testServerPort = 5350
b4f23783
CH
40 _toResponderQueue = Queue()
41 _fromResponderQueue = Queue()
617dfe22 42 _queueTimeout = 1
b1bec9f0 43 _dnsdistStartupDelay = 2.0
ca404e94 44 _dnsdist = None
ec5f5c6b 45 _responsesCounter = {}
b1bec9f0 46 _shutUp = True
18a0e7c6 47 _config_template = """
18a0e7c6
CH
48 """
49 _config_params = ['_testServerPort']
50 _acl = ['127.0.0.1/32']
1ea747c0
RG
51 _consolePort = 5199
52 _consoleKey = None
ca404e94
RG
53
54 @classmethod
55 def startResponders(cls):
56 print("Launching responders..")
ec5f5c6b 57
5df86a8a 58 cls._UDPResponder = threading.Thread(name='UDP Responder', target=cls.UDPResponder, args=[cls._testServerPort, cls._toResponderQueue, cls._fromResponderQueue])
ca404e94
RG
59 cls._UDPResponder.setDaemon(True)
60 cls._UDPResponder.start()
5df86a8a 61 cls._TCPResponder = threading.Thread(name='TCP Responder', target=cls.TCPResponder, args=[cls._testServerPort, cls._toResponderQueue, cls._fromResponderQueue])
ca404e94
RG
62 cls._TCPResponder.setDaemon(True)
63 cls._TCPResponder.start()
64
65 @classmethod
66 def startDNSDist(cls, shutUp=True):
67 print("Launching dnsdist..")
18a0e7c6
CH
68 conffile = 'dnsdist_test.conf'
69 params = tuple([getattr(cls, param) for param in cls._config_params])
70 print(params)
71 with open(conffile, 'w') as conf:
72 conf.write("-- Autogenerated by dnsdisttests.py\n")
73 conf.write(cls._config_template % params)
74
75 dnsdistcmd = [os.environ['DNSDISTBIN'], '-C', conffile,
b052847c 76 '-l', '%s:%d' % (cls._dnsDistListeningAddr, cls._dnsDistPort) ]
18a0e7c6
CH
77 for acl in cls._acl:
78 dnsdistcmd.extend(['--acl', acl])
79 print(' '.join(dnsdistcmd))
80
6b44773a
CH
81 # validate config with --check-config, which sets client=true, possibly exposing bugs.
82 testcmd = dnsdistcmd + ['--check-config']
83 output = subprocess.check_output(testcmd, close_fds=True)
84 if output != b'Configuration \'dnsdist_test.conf\' OK!\n':
85 raise AssertionError('dnsdist --check-config failed: %s' % output)
86
ca404e94
RG
87 if shutUp:
88 with open(os.devnull, 'w') as fdDevNull:
bd64cc44 89 cls._dnsdist = subprocess.Popen(dnsdistcmd, close_fds=True, stdout=fdDevNull)
ca404e94 90 else:
18a0e7c6 91 cls._dnsdist = subprocess.Popen(dnsdistcmd, close_fds=True)
ca404e94 92
0a2087eb
RG
93 if 'DNSDIST_FAST_TESTS' in os.environ:
94 delay = 0.5
95 else:
617dfe22
RG
96 delay = cls._dnsdistStartupDelay
97
0a2087eb 98 time.sleep(delay)
ca404e94
RG
99
100 if cls._dnsdist.poll() is not None:
0a2087eb 101 cls._dnsdist.kill()
ca404e94
RG
102 sys.exit(cls._dnsdist.returncode)
103
104 @classmethod
105 def setUpSockets(cls):
106 print("Setting up UDP socket..")
107 cls._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1ade83b2 108 cls._sock.settimeout(2.0)
ca404e94
RG
109 cls._sock.connect(("127.0.0.1", cls._dnsDistPort))
110
111 @classmethod
112 def setUpClass(cls):
113
114 cls.startResponders()
b1bec9f0 115 cls.startDNSDist(cls._shutUp)
ca404e94
RG
116 cls.setUpSockets()
117
118 print("Launching tests..")
119
120 @classmethod
121 def tearDownClass(cls):
0a2087eb
RG
122 if 'DNSDIST_FAST_TESTS' in os.environ:
123 delay = 0.1
124 else:
b1bec9f0 125 delay = 1.0
ca404e94
RG
126 if cls._dnsdist:
127 cls._dnsdist.terminate()
0a2087eb
RG
128 if cls._dnsdist.poll() is None:
129 time.sleep(delay)
130 if cls._dnsdist.poll() is None:
131 cls._dnsdist.kill()
1ade83b2 132 cls._dnsdist.wait()
ca404e94
RG
133
134 @classmethod
fe1c60f2 135 def _ResponderIncrementCounter(cls):
ec5f5c6b
RG
136 if threading.currentThread().name in cls._responsesCounter:
137 cls._responsesCounter[threading.currentThread().name] += 1
138 else:
139 cls._responsesCounter[threading.currentThread().name] = 1
140
fe1c60f2 141 @classmethod
5df86a8a 142 def _getResponse(cls, request, fromQueue, toQueue):
fe1c60f2
RG
143 response = None
144 if len(request.question) != 1:
145 print("Skipping query with question count %d" % (len(request.question)))
146 return None
147 healthcheck = not str(request.question[0].name).endswith('tests.powerdns.com.')
148 if not healthcheck:
149 cls._ResponderIncrementCounter()
5df86a8a
RG
150 if not fromQueue.empty():
151 response = fromQueue.get(True, cls._queueTimeout)
fe1c60f2
RG
152 if response:
153 response = copy.copy(response)
154 response.id = request.id
5df86a8a 155 toQueue.put(request, True, cls._queueTimeout)
fe1c60f2
RG
156
157 if not response:
158 # unexpected query, or health check
159 response = dns.message.make_response(request)
160
161 return response
162
ec5f5c6b 163 @classmethod
5df86a8a 164 def UDPResponder(cls, port, fromQueue, toQueue, ignoreTrailing=False):
ca404e94
RG
165 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
166 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
ec5f5c6b 167 sock.bind(("127.0.0.1", port))
ca404e94
RG
168 while True:
169 data, addr = sock.recvfrom(4096)
55baa1f2 170 request = dns.message.from_wire(data, ignore_trailing=ignoreTrailing)
5df86a8a 171 response = cls._getResponse(request, fromQueue, toQueue)
55baa1f2 172
fe1c60f2 173 if not response:
ca404e94 174 continue
87c605c4 175
1ade83b2 176 sock.settimeout(2.0)
ca404e94 177 sock.sendto(response.to_wire(), addr)
1ade83b2 178 sock.settimeout(None)
ca404e94
RG
179 sock.close()
180
181 @classmethod
5df86a8a 182 def TCPResponder(cls, port, fromQueue, toQueue, ignoreTrailing=False, multipleResponses=False):
ca404e94
RG
183 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
184 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
185 try:
ec5f5c6b 186 sock.bind(("127.0.0.1", port))
ca404e94
RG
187 except socket.error as e:
188 print("Error binding in the TCP responder: %s" % str(e))
189 sys.exit(1)
190
191 sock.listen(100)
192 while True:
b1bec9f0 193 (conn, _) = sock.accept()
1ade83b2 194 conn.settimeout(2.0)
ca404e94
RG
195 data = conn.recv(2)
196 (datalen,) = struct.unpack("!H", data)
197 data = conn.recv(datalen)
55baa1f2 198 request = dns.message.from_wire(data, ignore_trailing=ignoreTrailing)
5df86a8a 199 response = cls._getResponse(request, fromQueue, toQueue)
55baa1f2 200
fe1c60f2 201 if not response:
548c8b66 202 conn.close()
ca404e94 203 continue
ca404e94
RG
204
205 wire = response.to_wire()
206 conn.send(struct.pack("!H", len(wire)))
207 conn.send(wire)
548c8b66
RG
208
209 while multipleResponses:
5df86a8a 210 if fromQueue.empty():
548c8b66
RG
211 break
212
5df86a8a 213 response = fromQueue.get(True, cls._queueTimeout)
548c8b66
RG
214 if not response:
215 break
216
217 response = copy.copy(response)
218 response.id = request.id
219 wire = response.to_wire()
284d460c
RG
220 try:
221 conn.send(struct.pack("!H", len(wire)))
222 conn.send(wire)
223 except socket.error as e:
224 # some of the tests are going to close
225 # the connection on us, just deal with it
226 break
548c8b66 227
ca404e94 228 conn.close()
548c8b66 229
ca404e94
RG
230 sock.close()
231
232 @classmethod
55baa1f2 233 def sendUDPQuery(cls, query, response, useQueue=True, timeout=2.0, rawQuery=False):
ca404e94 234 if useQueue:
617dfe22 235 cls._toResponderQueue.put(response, True, timeout)
ca404e94
RG
236
237 if timeout:
238 cls._sock.settimeout(timeout)
239
240 try:
55baa1f2
RG
241 if not rawQuery:
242 query = query.to_wire()
243 cls._sock.send(query)
ca404e94 244 data = cls._sock.recv(4096)
b1bec9f0 245 except socket.timeout:
ca404e94
RG
246 data = None
247 finally:
248 if timeout:
249 cls._sock.settimeout(None)
250
251 receivedQuery = None
252 message = None
253 if useQueue and not cls._fromResponderQueue.empty():
617dfe22 254 receivedQuery = cls._fromResponderQueue.get(True, timeout)
ca404e94
RG
255 if data:
256 message = dns.message.from_wire(data)
257 return (receivedQuery, message)
258
259 @classmethod
9396d955 260 def openTCPConnection(cls, timeout=None):
ca404e94 261 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ca404e94
RG
262 if timeout:
263 sock.settimeout(timeout)
264
0a2087eb 265 sock.connect(("127.0.0.1", cls._dnsDistPort))
9396d955 266 return sock
0a2087eb 267
9396d955 268 @classmethod
a227f47d
RG
269 def openTLSConnection(cls, port, serverName, caCert=None, timeout=None):
270 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
271 if timeout:
272 sock.settimeout(timeout)
273
274 # 2.7.9+
275 if hasattr(ssl, 'create_default_context'):
276 sslctx = ssl.create_default_context(cafile=caCert)
277 sslsock = sslctx.wrap_socket(sock, server_hostname=serverName)
278 else:
279 sslsock = ssl.wrap_socket(sock, ca_certs=caCert, cert_reqs=ssl.CERT_REQUIRED)
280
281 sslsock.connect(("127.0.0.1", port))
282 return sslsock
283
284 @classmethod
285 def sendTCPQueryOverConnection(cls, sock, query, rawQuery=False, response=None, timeout=2.0):
9396d955
RG
286 if not rawQuery:
287 wire = query.to_wire()
288 else:
289 wire = query
55baa1f2 290
a227f47d
RG
291 if response:
292 cls._toResponderQueue.put(response, True, timeout)
293
9396d955
RG
294 sock.send(struct.pack("!H", len(wire)))
295 sock.send(wire)
296
297 @classmethod
a227f47d 298 def recvTCPResponseOverConnection(cls, sock, useQueue=False, timeout=2.0):
9396d955
RG
299 message = None
300 data = sock.recv(2)
301 if data:
302 (datalen,) = struct.unpack("!H", data)
303 data = sock.recv(datalen)
ca404e94 304 if data:
9396d955 305 message = dns.message.from_wire(data)
a227f47d
RG
306
307 if useQueue and not cls._fromResponderQueue.empty():
308 receivedQuery = cls._fromResponderQueue.get(True, timeout)
309 return (receivedQuery, message)
310 else:
311 return message
9396d955
RG
312
313 @classmethod
314 def sendTCPQuery(cls, query, response, useQueue=True, timeout=2.0, rawQuery=False):
315 message = None
316 if useQueue:
317 cls._toResponderQueue.put(response, True, timeout)
318
319 sock = cls.openTCPConnection(timeout)
320
321 try:
322 cls.sendTCPQueryOverConnection(sock, query, rawQuery)
323 message = cls.recvTCPResponseOverConnection(sock)
ca404e94
RG
324 except socket.timeout as e:
325 print("Timeout: %s" % (str(e)))
ca404e94
RG
326 except socket.error as e:
327 print("Network error: %s" % (str(e)))
ca404e94
RG
328 finally:
329 sock.close()
330
331 receivedQuery = None
ca404e94 332 if useQueue and not cls._fromResponderQueue.empty():
617dfe22 333 receivedQuery = cls._fromResponderQueue.get(True, timeout)
9396d955 334
ca404e94 335 return (receivedQuery, message)
617dfe22 336
548c8b66
RG
337 @classmethod
338 def sendTCPQueryWithMultipleResponses(cls, query, responses, useQueue=True, timeout=2.0, rawQuery=False):
339 if useQueue:
340 for response in responses:
341 cls._toResponderQueue.put(response, True, timeout)
342 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
343 if timeout:
344 sock.settimeout(timeout)
345
346 sock.connect(("127.0.0.1", cls._dnsDistPort))
347 messages = []
348
349 try:
350 if not rawQuery:
351 wire = query.to_wire()
352 else:
353 wire = query
354
355 sock.send(struct.pack("!H", len(wire)))
356 sock.send(wire)
357 while True:
358 data = sock.recv(2)
359 if not data:
360 break
361 (datalen,) = struct.unpack("!H", data)
362 data = sock.recv(datalen)
363 messages.append(dns.message.from_wire(data))
364
365 except socket.timeout as e:
366 print("Timeout: %s" % (str(e)))
367 except socket.error as e:
368 print("Network error: %s" % (str(e)))
369 finally:
370 sock.close()
371
372 receivedQuery = None
373 if useQueue and not cls._fromResponderQueue.empty():
374 receivedQuery = cls._fromResponderQueue.get(True, timeout)
375 return (receivedQuery, messages)
376
617dfe22
RG
377 def setUp(self):
378 # This function is called before every tests
379
380 # Clear the responses counters
381 for key in self._responsesCounter:
382 self._responsesCounter[key] = 0
383
384 # Make sure the queues are empty, in case
385 # a previous test failed
386 while not self._toResponderQueue.empty():
387 self._toResponderQueue.get(False)
388
389 while not self._fromResponderQueue.empty():
fe1c60f2 390 self._fromResponderQueue.get(False)
1ea747c0 391
3bef39c3
RG
392 @classmethod
393 def clearToResponderQueue(cls):
394 while not cls._toResponderQueue.empty():
395 cls._toResponderQueue.get(False)
396
397 @classmethod
398 def clearFromResponderQueue(cls):
399 while not cls._fromResponderQueue.empty():
400 cls._fromResponderQueue.get(False)
401
402 @classmethod
403 def clearResponderQueues(cls):
404 cls.clearToResponderQueue()
405 cls.clearFromResponderQueue()
406
1ea747c0
RG
407 @staticmethod
408 def generateConsoleKey():
409 return libnacl.utils.salsa_key()
410
411 @classmethod
412 def _encryptConsole(cls, command, nonce):
b4f23783 413 command = command.encode('UTF-8')
1ea747c0
RG
414 if cls._consoleKey is None:
415 return command
416 return libnacl.crypto_secretbox(command, nonce, cls._consoleKey)
417
418 @classmethod
419 def _decryptConsole(cls, command, nonce):
420 if cls._consoleKey is None:
b4f23783
CH
421 result = command
422 else:
423 result = libnacl.crypto_secretbox_open(command, nonce, cls._consoleKey)
424 return result.decode('UTF-8')
1ea747c0
RG
425
426 @classmethod
427 def sendConsoleCommand(cls, command, timeout=1.0):
428 ourNonce = libnacl.utils.rand_nonce()
429 theirNonce = None
430 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
431 if timeout:
432 sock.settimeout(timeout)
433
434 sock.connect(("127.0.0.1", cls._consolePort))
435 sock.send(ourNonce)
436 theirNonce = sock.recv(len(ourNonce))
7b925432 437 if len(theirNonce) != len(ourNonce):
05a5b575 438 print("Received a nonce of size %d, expecting %d, console command will not be sent!" % (len(theirNonce), len(ourNonce)))
7b925432 439 return None
1ea747c0 440
b4f23783 441 halfNonceSize = int(len(ourNonce) / 2)
333ea16e
RG
442 readingNonce = ourNonce[0:halfNonceSize] + theirNonce[halfNonceSize:]
443 writingNonce = theirNonce[0:halfNonceSize] + ourNonce[halfNonceSize:]
333ea16e 444 msg = cls._encryptConsole(command, writingNonce)
1ea747c0
RG
445 sock.send(struct.pack("!I", len(msg)))
446 sock.send(msg)
447 data = sock.recv(4)
448 (responseLen,) = struct.unpack("!I", data)
449 data = sock.recv(responseLen)
333ea16e 450 response = cls._decryptConsole(data, readingNonce)
1ea747c0 451 return response
5df86a8a
RG
452
453 def compareOptions(self, a, b):
454 self.assertEquals(len(a), len(b))
b4f23783 455 for idx in range(len(a)):
5df86a8a
RG
456 self.assertEquals(a[idx], b[idx])
457
458 def checkMessageNoEDNS(self, expected, received):
459 self.assertEquals(expected, received)
460 self.assertEquals(received.edns, -1)
461 self.assertEquals(len(received.options), 0)
462
463 def checkMessageEDNSWithoutECS(self, expected, received, withCookies=0):
464 self.assertEquals(expected, received)
465 self.assertEquals(received.edns, 0)
466 self.assertEquals(len(received.options), withCookies)
467 if withCookies:
468 for option in received.options:
469 self.assertEquals(option.otype, 10)
470
471 def checkMessageEDNSWithECS(self, expected, received):
472 self.assertEquals(expected, received)
473 self.assertEquals(received.edns, 0)
474 self.assertEquals(len(received.options), 1)
475 self.assertEquals(received.options[0].otype, clientsubnetoption.ASSIGNED_OPTION_CODE)
476 self.compareOptions(expected.options, received.options)
477
478 def checkQueryEDNSWithECS(self, expected, received):
479 self.checkMessageEDNSWithECS(expected, received)
480
481 def checkResponseEDNSWithECS(self, expected, received):
482 self.checkMessageEDNSWithECS(expected, received)
483
484 def checkQueryEDNSWithoutECS(self, expected, received):
485 self.checkMessageEDNSWithoutECS(expected, received)
486
487 def checkResponseEDNSWithoutECS(self, expected, received, withCookies=0):
488 self.checkMessageEDNSWithoutECS(expected, received, withCookies)
489
490 def checkQueryNoEDNS(self, expected, received):
491 self.checkMessageNoEDNS(expected, received)
492
493 def checkResponseNoEDNS(self, expected, received):
494 self.checkMessageNoEDNS(expected, received)