]> git.ipfire.org Git - thirdparty/pdns.git/blob - regression-tests.auth-py/authtests.py
Make sure we can install unsigned packages.
[thirdparty/pdns.git] / regression-tests.auth-py / authtests.py
1 #!/usr/bin/env python2
2
3 from __future__ import print_function
4 import errno
5 import shutil
6 import os
7 import socket
8 import struct
9 import subprocess
10 import sys
11 import time
12 import unittest
13 import dns
14 import dns.message
15
16 from pprint import pprint
17 from eqdnsmessage import AssertEqualDNSMessageMixin
18
19 class AuthTest(AssertEqualDNSMessageMixin, unittest.TestCase):
20 """
21 Setup auth required for the tests
22 """
23
24 _confdir = 'auth'
25 _authPort = 5300
26
27 _config_params = []
28
29 _config_template_default = """
30 module-dir=../regression-tests/modules
31 daemon=no
32 bind-config={confdir}/named.conf
33 bind-dnssec-db={bind_dnssec_db}
34 socket-dir={confdir}
35 cache-ttl=0
36 negquery-cache-ttl=0
37 query-cache-ttl=0
38 log-dns-queries=yes
39 log-dns-details=yes
40 loglevel=9
41 distributor-threads=1"""
42
43 _config_template = ""
44
45 _root_DS = "63149 13 1 a59da3f5c1b97fcd5fa2b3b2b0ac91d38a60d33a"
46
47 # The default SOA for zones in the authoritative servers
48 _SOA = "ns1.example.net. hostmaster.example.net. 1 3600 1800 1209600 300"
49
50 # The definitions of the zones on the authoritative servers, the key is the
51 # zonename and the value is the zonefile content. several strings are replaced:
52 # - {soa} => value of _SOA
53 # - {prefix} value of _PREFIX
54 _zones = {
55 'example.org': """
56 example.org. 3600 IN SOA {soa}
57 example.org. 3600 IN NS ns1.example.org.
58 example.org. 3600 IN NS ns2.example.org.
59 ns1.example.org. 3600 IN A {prefix}.10
60 ns2.example.org. 3600 IN A {prefix}.11
61 """,
62 }
63
64 _zone_keys = {
65 'example.org': """
66 Private-key-format: v1.2
67 Algorithm: 13 (ECDSAP256SHA256)
68 PrivateKey: Lt0v0Gol3pRUFM7fDdcy0IWN0O/MnEmVPA+VylL8Y4U=
69 """,
70 }
71
72 _auth_cmd = ['authbind',
73 os.environ['PDNS']]
74 _auth_env = {}
75 _auths = {}
76
77 _PREFIX = os.environ['PREFIX']
78
79
80 @classmethod
81 def createConfigDir(cls, confdir):
82 try:
83 shutil.rmtree(confdir)
84 except OSError as e:
85 if e.errno != errno.ENOENT:
86 raise
87 os.mkdir(confdir, 0o755)
88
89 @classmethod
90 def generateAuthZone(cls, confdir, zonename, zonecontent):
91 with open(os.path.join(confdir, '%s.zone' % zonename), 'w') as zonefile:
92 zonefile.write(zonecontent.format(prefix=cls._PREFIX, soa=cls._SOA))
93
94 @classmethod
95 def generateAuthNamedConf(cls, confdir, zones):
96 with open(os.path.join(confdir, 'named.conf'), 'w') as namedconf:
97 namedconf.write("""
98 options {
99 directory "%s";
100 };""" % confdir)
101 for zonename in zones:
102 zone = '.' if zonename == 'ROOT' else zonename
103
104 namedconf.write("""
105 zone "%s" {
106 type master;
107 file "%s.zone";
108 };""" % (zone, zonename))
109
110 @classmethod
111 def generateAuthConfig(cls, confdir):
112 bind_dnssec_db = os.path.join(confdir, 'bind-dnssec.sqlite3')
113
114 params = tuple([getattr(cls, param) for param in cls._config_params])
115
116 with open(os.path.join(confdir, 'pdns.conf'), 'w') as pdnsconf:
117 pdnsconf.write(cls._config_template_default.format(
118 confdir=confdir, prefix=cls._PREFIX,
119 bind_dnssec_db=bind_dnssec_db))
120 pdnsconf.write(cls._config_template % params)
121
122 os.system("sqlite3 ./configs/auth/powerdns.sqlite < ../modules/gsqlite3backend/schema.sqlite3.sql")
123
124 pdnsutilCmd = [os.environ['PDNSUTIL'],
125 '--config-dir=%s' % confdir,
126 'create-bind-db',
127 bind_dnssec_db]
128
129 print(' '.join(pdnsutilCmd))
130 try:
131 subprocess.check_output(pdnsutilCmd, stderr=subprocess.STDOUT)
132 except subprocess.CalledProcessError as e:
133 raise AssertionError('%s failed (%d): %s' % (pdnsutilCmd, e.returncode, e.output))
134
135 @classmethod
136 def secureZone(cls, confdir, zonename, key=None):
137 zone = '.' if zonename == 'ROOT' else zonename
138 if not key:
139 pdnsutilCmd = [os.environ['PDNSUTIL'],
140 '--config-dir=%s' % confdir,
141 'secure-zone',
142 zone]
143 else:
144 keyfile = os.path.join(confdir, 'dnssec.key')
145 with open(keyfile, 'w') as fdKeyfile:
146 fdKeyfile.write(key)
147
148 pdnsutilCmd = [os.environ['PDNSUTIL'],
149 '--config-dir=%s' % confdir,
150 'import-zone-key',
151 zone,
152 keyfile,
153 'active',
154 'ksk']
155
156 print(' '.join(pdnsutilCmd))
157 try:
158 subprocess.check_output(pdnsutilCmd, stderr=subprocess.STDOUT)
159 except subprocess.CalledProcessError as e:
160 raise AssertionError('%s failed (%d): %s' % (pdnsutilCmd, e.returncode, e.output))
161
162 @classmethod
163 def generateAllAuthConfig(cls, confdir):
164 cls.generateAuthConfig(confdir)
165 cls.generateAuthNamedConf(confdir, cls._zones.keys())
166
167 for zonename, zonecontent in cls._zones.items():
168 cls.generateAuthZone(confdir,
169 zonename,
170 zonecontent)
171 if cls._zone_keys.get(zonename, None):
172 cls.secureZone(confdir, zonename, cls._zone_keys.get(zonename))
173
174 @classmethod
175 def startAuth(cls, confdir, ipaddress):
176
177 print("Launching pdns_server..")
178 authcmd = list(cls._auth_cmd)
179 authcmd.append('--config-dir=%s' % confdir)
180 authcmd.append('--local-address=%s' % ipaddress)
181 authcmd.append('--local-port=%s' % cls._authPort)
182 authcmd.append('--loglevel=9')
183 authcmd.append('--enable-lua-records')
184 authcmd.append('--lua-health-checks-interval=1')
185 print(' '.join(authcmd))
186 logFile = os.path.join(confdir, 'pdns.log')
187 with open(logFile, 'w') as fdLog:
188 cls._auths[ipaddress] = subprocess.Popen(authcmd, close_fds=True,
189 stdout=fdLog, stderr=fdLog,
190 env=cls._auth_env)
191
192 time.sleep(2)
193
194 if cls._auths[ipaddress].poll() is not None:
195 try:
196 cls._auths[ipaddress].kill()
197 except OSError as e:
198 if e.errno != errno.ESRCH:
199 raise
200 with open(logFile, 'r') as fdLog:
201 print(fdLog.read())
202 sys.exit(cls._auths[ipaddress].returncode)
203
204 @classmethod
205 def setUpSockets(cls):
206 print("Setting up UDP socket..")
207 cls._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
208 cls._sock.settimeout(2.0)
209 cls._sock.connect((cls._PREFIX + ".1", cls._authPort))
210
211 @classmethod
212 def startResponders(cls):
213 pass
214
215 @classmethod
216 def setUpClass(cls):
217 cls.setUpSockets()
218
219 cls.startResponders()
220
221 confdir = os.path.join('configs', cls._confdir)
222 cls.createConfigDir(confdir)
223
224 cls.generateAllAuthConfig(confdir)
225 cls.startAuth(confdir, cls._PREFIX + ".1")
226
227 print("Launching tests..")
228
229 @classmethod
230 def tearDownClass(cls):
231 cls.tearDownAuth()
232 cls.tearDownResponders()
233
234 @classmethod
235 def tearDownResponders(cls):
236 pass
237
238 @classmethod
239 def tearDownClass(cls):
240 cls.tearDownAuth()
241
242 @classmethod
243 def tearDownAuth(cls):
244 if 'PDNSRECURSOR_FAST_TESTS' in os.environ:
245 delay = 0.1
246 else:
247 delay = 1.0
248
249 for _, auth in cls._auths.items():
250 try:
251 auth.terminate()
252 if auth.poll() is None:
253 time.sleep(delay)
254 if auth.poll() is None:
255 auth.kill()
256 auth.wait()
257 except OSError as e:
258 if e.errno != errno.ESRCH:
259 raise
260
261 @classmethod
262 def sendUDPQuery(cls, query, timeout=2.0, decode=True, fwparams=dict()):
263 if timeout:
264 cls._sock.settimeout(timeout)
265
266 try:
267 cls._sock.send(query.to_wire())
268 data = cls._sock.recv(4096)
269 except socket.timeout:
270 data = None
271 finally:
272 if timeout:
273 cls._sock.settimeout(None)
274
275 message = None
276 if data:
277 if not decode:
278 return data
279 message = dns.message.from_wire(data, **fwparams)
280 return message
281
282 @classmethod
283 def sendTCPQuery(cls, query, timeout=2.0):
284 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
285 if timeout:
286 sock.settimeout(timeout)
287
288 sock.connect(("127.0.0.1", cls._authPort))
289
290 try:
291 wire = query.to_wire()
292 sock.send(struct.pack("!H", len(wire)))
293 sock.send(wire)
294 data = sock.recv(2)
295 if data:
296 (datalen,) = struct.unpack("!H", data)
297 data = sock.recv(datalen)
298 except socket.timeout as e:
299 print("Timeout: %s" % (str(e)))
300 data = None
301 except socket.error as e:
302 print("Network error: %s" % (str(e)))
303 data = None
304 finally:
305 sock.close()
306
307 message = None
308 if data:
309 message = dns.message.from_wire(data)
310 return message
311
312 @classmethod
313 def sendTCPQueryMultiResponse(cls, query, timeout=2.0, count=1):
314 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
315 if timeout:
316 sock.settimeout(timeout)
317
318 sock.connect(("127.0.0.1", cls._authPort))
319
320 try:
321 wire = query.to_wire()
322 sock.send(struct.pack("!H", len(wire)))
323 sock.send(wire)
324 except socket.timeout as e:
325 raise Exception("Timeout: %s" % (str(e)))
326 except socket.error as e:
327 raise Exception("Network error: %s" % (str(e)))
328
329 messages = []
330 for i in range(count):
331 try:
332 data = sock.recv(2)
333 print("got data", repr(data))
334 if data:
335 (datalen,) = struct.unpack("!H", data)
336 data = sock.recv(datalen)
337 messages.append(dns.message.from_wire(data))
338 else:
339 break
340 except socket.timeout as e:
341 raise Exception("Timeout: %s" % (str(e)))
342 except socket.error as e:
343 raise Exception("Network error: %s" % (str(e)))
344
345 return messages
346
347 def setUp(self):
348 # This function is called before every tests
349 super(AuthTest, self).setUp()
350
351 ## Functions for comparisons
352 def assertMessageHasFlags(self, msg, flags, ednsflags=[]):
353 """Asserts that msg has all the flags from flags set
354
355 @param msg: the dns.message.Message to check
356 @param flags: a list of strings with flag mnemonics (like ['RD', 'RA'])
357 @param ednsflags: a list of strings with edns-flag mnemonics (like ['DO'])"""
358
359 if not isinstance(msg, dns.message.Message):
360 raise TypeError("msg is not a dns.message.Message")
361
362 if isinstance(flags, list):
363 for elem in flags:
364 if not isinstance(elem, str):
365 raise TypeError("flags is not a list of strings")
366 else:
367 raise TypeError("flags is not a list of strings")
368
369 if isinstance(ednsflags, list):
370 for elem in ednsflags:
371 if not isinstance(elem, str):
372 raise TypeError("ednsflags is not a list of strings")
373 else:
374 raise TypeError("ednsflags is not a list of strings")
375
376 msgFlags = dns.flags.to_text(msg.flags).split()
377 missingFlags = [flag for flag in flags if flag not in msgFlags]
378
379 msgEdnsFlags = dns.flags.edns_to_text(msg.ednsflags).split()
380 missingEdnsFlags = [ednsflag for ednsflag in ednsflags if ednsflag not in msgEdnsFlags]
381
382 if len(missingFlags) or len(missingEdnsFlags) or len(msgFlags) > len(flags):
383 raise AssertionError("Expected flags '%s' (EDNS: '%s'), found '%s' (EDNS: '%s') in query %s" %
384 (' '.join(flags), ' '.join(ednsflags),
385 ' '.join(msgFlags), ' '.join(msgEdnsFlags),
386 msg.question[0]))
387
388 def assertMessageIsAuthenticated(self, msg):
389 """Asserts that the message has the AD bit set
390
391 @param msg: the dns.message.Message to check"""
392
393 if not isinstance(msg, dns.message.Message):
394 raise TypeError("msg is not a dns.message.Message")
395
396 msgFlags = dns.flags.to_text(msg.flags)
397 self.assertTrue('AD' in msgFlags, "No AD flag found in the message for %s" % msg.question[0].name)
398
399 def assertRRsetInAnswer(self, msg, rrset):
400 """Asserts the rrset (without comparing TTL) exists in the
401 answer section of msg
402
403 @param msg: the dns.message.Message to check
404 @param rrset: a dns.rrset.RRset object"""
405
406 ret = ''
407 if not isinstance(msg, dns.message.Message):
408 raise TypeError("msg is not a dns.message.Message")
409
410 if not isinstance(rrset, dns.rrset.RRset):
411 raise TypeError("rrset is not a dns.rrset.RRset")
412
413 found = False
414 for ans in msg.answer:
415 ret += "%s\n" % ans.to_text()
416 if ans.match(rrset.name, rrset.rdclass, rrset.rdtype, 0, None):
417 self.assertEqual(ans, rrset, "'%s' != '%s'" % (ans.to_text(), rrset.to_text()))
418 found = True
419
420 if not found :
421 raise AssertionError("RRset not found in answer\n\n%s" % ret)
422
423 def sortRRsets(self, rrsets):
424 """Sorts RRsets in a more useful way than dnspython's default behaviour
425
426 @param rrsets: an array of dns.rrset.RRset objects"""
427
428 return sorted(rrsets, key=lambda rrset: (rrset.name, rrset.rdtype))
429
430 def assertAnyRRsetInAnswer(self, msg, rrsets):
431 """Asserts that any of the supplied rrsets exists (without comparing TTL)
432 in the answer section of msg
433
434 @param msg: the dns.message.Message to check
435 @param rrsets: an array of dns.rrset.RRset object"""
436
437 if not isinstance(msg, dns.message.Message):
438 raise TypeError("msg is not a dns.message.Message")
439
440 found = False
441 for rrset in rrsets:
442 if not isinstance(rrset, dns.rrset.RRset):
443 raise TypeError("rrset is not a dns.rrset.RRset")
444 for ans in msg.answer:
445 if ans.match(rrset.name, rrset.rdclass, rrset.rdtype, 0, None):
446 if ans == rrset:
447 found = True
448
449 if not found:
450 raise AssertionError("RRset not found in answer\n%s" %
451 "\n".join(([ans.to_text() for ans in msg.answer])))
452
453 def assertMatchingRRSIGInAnswer(self, msg, coveredRRset, keys=None):
454 """Looks for coveredRRset in the answer section and if there is an RRSIG RRset
455 that covers that RRset. If keys is not None, this function will also try to
456 validate the RRset against the RRSIG
457
458 @param msg: The dns.message.Message to check
459 @param coveredRRset: The RRSet to check for
460 @param keys: a dictionary keyed by dns.name.Name with node or rdataset values to use for validation"""
461
462 if not isinstance(msg, dns.message.Message):
463 raise TypeError("msg is not a dns.message.Message")
464
465 if not isinstance(coveredRRset, dns.rrset.RRset):
466 raise TypeError("coveredRRset is not a dns.rrset.RRset")
467
468 msgRRsigRRSet = None
469 msgRRSet = None
470
471 ret = ''
472 for ans in msg.answer:
473 ret += ans.to_text() + "\n"
474
475 if ans.match(coveredRRset.name, coveredRRset.rdclass, coveredRRset.rdtype, 0, None):
476 msgRRSet = ans
477 if ans.match(coveredRRset.name, dns.rdataclass.IN, dns.rdatatype.RRSIG, coveredRRset.rdtype, None):
478 msgRRsigRRSet = ans
479 if msgRRSet and msgRRsigRRSet:
480 break
481
482 if not msgRRSet:
483 raise AssertionError("RRset for '%s' not found in answer" % msg.question[0].to_text())
484
485 if not msgRRsigRRSet:
486 raise AssertionError("No RRSIGs found in answer for %s:\nFull answer:\n%s" % (msg.question[0].to_text(), ret))
487
488 if keys:
489 try:
490 dns.dnssec.validate(msgRRSet, msgRRsigRRSet.to_rdataset(), keys)
491 except dns.dnssec.ValidationFailure as e:
492 raise AssertionError("Signature validation failed for %s:\n%s" % (msg.question[0].to_text(), e))
493
494 def assertNoRRSIGsInAnswer(self, msg):
495 """Checks if there are _no_ RRSIGs in the answer section of msg"""
496
497 if not isinstance(msg, dns.message.Message):
498 raise TypeError("msg is not a dns.message.Message")
499
500 ret = ""
501 for ans in msg.answer:
502 if ans.rdtype == dns.rdatatype.RRSIG:
503 ret += ans.name.to_text() + "\n"
504
505 if len(ret):
506 raise AssertionError("RRSIG found in answers for:\n%s" % ret)
507
508 def assertAnswerEmpty(self, msg):
509 self.assertTrue(len(msg.answer) == 0, "Data found in the the answer section for %s:\n%s" % (msg.question[0].to_text(), '\n'.join([i.to_text() for i in msg.answer])))
510
511 def assertAnswerNotEmpty(self, msg):
512 self.assertTrue(len(msg.answer) > 0, "Answer is empty")
513
514 def assertRcodeEqual(self, msg, rcode):
515 if not isinstance(msg, dns.message.Message):
516 raise TypeError("msg is not a dns.message.Message but a %s" % type(msg))
517
518 if not isinstance(rcode, int):
519 if isinstance(rcode, str):
520 rcode = dns.rcode.from_text(rcode)
521 else:
522 raise TypeError("rcode is neither a str nor int")
523
524 if msg.rcode() != rcode:
525 msgRcode = dns.rcode._by_value[msg.rcode()]
526 wantedRcode = dns.rcode._by_value[rcode]
527
528 raise AssertionError("Rcode for %s is %s, expected %s." % (msg.question[0].to_text(), msgRcode, wantedRcode))
529
530 def assertAuthorityHasSOA(self, msg):
531 if not isinstance(msg, dns.message.Message):
532 raise TypeError("msg is not a dns.message.Message but a %s" % type(msg))
533
534 found = False
535 for rrset in msg.authority:
536 if rrset.rdtype == dns.rdatatype.SOA:
537 found = True
538 break
539
540 if not found:
541 raise AssertionError("No SOA record found in the authority section:\n%s" % msg.to_text())