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