]> git.ipfire.org Git - thirdparty/pdns.git/blame - regression-tests.recursor-dnssec/test_AggressiveNSECCache.py
Merge pull request #13062 from Habbie/auth-loglevel-prefix
[thirdparty/pdns.git] / regression-tests.recursor-dnssec / test_AggressiveNSECCache.py
CommitLineData
dd54e182
RG
1import dns
2from recursortests import RecursorTest
3import os
4import requests
5import subprocess
2ec80d48 6import time
811bddf9 7import extendederrors
dd54e182
RG
8
9class AggressiveNSECCacheBase(RecursorTest):
10 __test__ = False
11 _wsPort = 8042
40795092 12 _wsTimeout = 10
dd54e182
RG
13 _wsPassword = 'secretpassword'
14 _apiKey = 'secretapikey'
7d3d2f4f 15 #_recursorStartupDelay = 4.0
dd54e182
RG
16 _config_template = """
17 dnssec=validate
18 aggressive-nsec-cache-size=10000
b199e480 19 nsec3-max-iterations=150
dd54e182
RG
20 webserver=yes
21 webserver-port=%d
22 webserver-address=127.0.0.1
23 webserver-password=%s
24 api-key=%s
7d3d2f4f 25 devonly-regression-test-mode
811bddf9 26 extended-resolution-errors=yes
dd54e182
RG
27 """ % (_wsPort, _wsPassword, _apiKey)
28
29 @classmethod
2ec80d48 30 def wipe(cls):
dd54e182 31 confdir = os.path.join('configs', cls._confdir)
7d3d2f4f
OM
32 # Only wipe examples, as wiping the root triggers root NS refreshes
33 cls.wipeRecursorCache(confdir, "example$")
dd54e182
RG
34
35 def getMetric(self, name):
36 headers = {'x-api-key': self._apiKey}
37 url = 'http://127.0.0.1:' + str(self._wsPort) + '/api/v1/servers/localhost/statistics'
38 r = requests.get(url, headers=headers, timeout=self._wsTimeout)
39 self.assertTrue(r)
4bfebc93 40 self.assertEqual(r.status_code, 200)
dd54e182
RG
41 self.assertTrue(r.json())
42 content = r.json()
43
44 for entry in content:
45 if entry['name'] == name:
46 return int(entry['value'])
47
48 self.assertTrue(False)
49
03f779fd
OM
50 def testNoEDE(self):
51 # This isn't an aggresive cache check, but the strcuture is very similar to the others,
52 # so letys place it here.
53 # It test the issue that an intermediate EDE does not get reported with the final answer
54 # https://github.com/PowerDNS/pdns/pull/12694
55 self.wipe()
56
57 res = self.sendQuery('host1.sub.secure.example.', 'TXT')
58 self.assertRcodeEqual(res, dns.rcode.NOERROR)
59 self.assertAnswerEmpty(res)
60 self.assertAuthorityHasSOA(res)
61 self.assertMessageIsAuthenticated(res)
62 self.assertEqual(res.edns, 0)
63 self.assertEqual(len(res.options), 0)
64
65 res = self.sendQuery('host1.sub.secure.example.', 'A')
66 self.assertRcodeEqual(res, dns.rcode.NOERROR)
67 self.assertMessageIsAuthenticated(res)
68 self.assertEqual(res.edns, 0)
69 self.assertEqual(len(res.options), 0)
70
dd54e182 71 def testNoData(self):
2ec80d48 72 self.wipe()
7d3d2f4f 73
e746a2f6 74 # first we query a nonexistent type, to get the NSEC in our cache
dd54e182
RG
75 entries = self.getMetric('aggressive-nsec-cache-entries')
76 res = self.sendQuery('host1.secure.example.', 'TXT')
77 self.assertRcodeEqual(res, dns.rcode.NOERROR)
78 self.assertAnswerEmpty(res)
79 self.assertAuthorityHasSOA(res)
80 self.assertMessageIsAuthenticated(res)
81 self.assertGreater(self.getMetric('aggressive-nsec-cache-entries'), entries)
82
83 # now we ask for a different type, we should generate the answer from the NSEC,
84 # and no outgoing query should be made
85 nbQueries = self.getMetric('all-outqueries')
86 entries = self.getMetric('aggressive-nsec-cache-entries')
87 res = self.sendQuery('host1.secure.example.', 'AAAA')
88 self.assertRcodeEqual(res, dns.rcode.NOERROR)
89 self.assertAnswerEmpty(res)
90 self.assertAuthorityHasSOA(res)
91 self.assertMessageIsAuthenticated(res)
4bfebc93
CH
92 self.assertEqual(nbQueries, self.getMetric('all-outqueries'))
93 self.assertEqual(self.getMetric('aggressive-nsec-cache-entries'), entries)
811bddf9
OM
94 self.assertEqual(res.edns, 0)
95 self.assertEqual(len(res.options), 1)
96 self.assertEqual(res.options[0].otype, 15)
7f8f0893 97 self.assertEqual(res.options[0], extendederrors.ExtendedErrorOption(29, b'Result synthesized from aggressive NSEC cache (RFC8198)'))
dd54e182
RG
98
99class AggressiveNSECCacheNSEC(AggressiveNSECCacheBase):
100 _confdir = 'AggressiveNSECCacheNSEC'
101 __test__ = True
102
103 # we can't use the same tests for NSEC and NSEC3 because the hashed NSEC3s
104 # do not deny the same names than the non-hashed NSECs do
105 def testNXD(self):
2ec80d48 106 self.wipe()
7d3d2f4f 107
e746a2f6 108 # first we query a nonexistent name, to get the needed NSECs (name + widcard) in our cache
dd54e182
RG
109 entries = self.getMetric('aggressive-nsec-cache-entries')
110 hits = self.getMetric('aggressive-nsec-cache-nsec-hits')
111 res = self.sendQuery('host2.secure.example.', 'TXT')
112 self.assertRcodeEqual(res, dns.rcode.NXDOMAIN)
113 self.assertAnswerEmpty(res)
114 self.assertAuthorityHasSOA(res)
115 self.assertMessageIsAuthenticated(res)
116 self.assertGreater(self.getMetric('aggressive-nsec-cache-entries'), entries)
4bfebc93 117 self.assertEqual(self.getMetric('aggressive-nsec-cache-nsec-hits'), hits)
dd54e182
RG
118
119 # now we ask for a different name that is covered by the NSEC,
120 # we should generate the answer from the NSEC and no outgoing query should be made
121 nbQueries = self.getMetric('all-outqueries')
122 entries = self.getMetric('aggressive-nsec-cache-entries')
123 hits = self.getMetric('aggressive-nsec-cache-nsec-hits')
124 res = self.sendQuery('host3.secure.example.', 'AAAA')
125 self.assertRcodeEqual(res, dns.rcode.NXDOMAIN)
126 self.assertAnswerEmpty(res)
127 self.assertAuthorityHasSOA(res)
128 self.assertMessageIsAuthenticated(res)
4bfebc93
CH
129 self.assertEqual(nbQueries, self.getMetric('all-outqueries'))
130 self.assertEqual(self.getMetric('aggressive-nsec-cache-entries'), entries)
dd54e182 131 self.assertGreater(self.getMetric('aggressive-nsec-cache-nsec-hits'), hits)
811bddf9
OM
132 self.assertEqual(res.edns, 0)
133 self.assertEqual(len(res.options), 1)
134 self.assertEqual(res.options[0].otype, 15)
7f8f0893 135 self.assertEqual(res.options[0], extendederrors.ExtendedErrorOption(29, b'Result synthesized from aggressive NSEC cache (RFC8198)'))
dd54e182
RG
136
137 def testWildcard(self):
2ec80d48 138 self.wipe()
7d3d2f4f 139
e746a2f6 140 # first we query a nonexistent name, but for which a wildcard matches,
dd54e182
RG
141 # to get the NSEC in our cache
142 res = self.sendQuery('test1.wildcard.secure.example.', 'A')
143 expected = dns.rrset.from_text('test1.wildcard.secure.example.', 0, dns.rdataclass.IN, 'A', '{prefix}.10'.format(prefix=self._PREFIX))
144 self.assertRcodeEqual(res, dns.rcode.NOERROR)
145 self.assertMatchingRRSIGInAnswer(res, expected)
146 self.assertMessageIsAuthenticated(res)
147
148 # now we ask for a different name, we should generate the answer from the NSEC and the wildcard,
149 # and no outgoing query should be made
150 hits = self.getMetric('aggressive-nsec-cache-nsec-wc-hits')
151 nbQueries = self.getMetric('all-outqueries')
152 res = self.sendQuery('test2.wildcard.secure.example.', 'A')
153 expected = dns.rrset.from_text('test2.wildcard.secure.example.', 0, dns.rdataclass.IN, 'A', '{prefix}.10'.format(prefix=self._PREFIX))
154 self.assertRcodeEqual(res, dns.rcode.NOERROR)
155 self.assertMatchingRRSIGInAnswer(res, expected)
156 self.assertMessageIsAuthenticated(res)
4bfebc93 157 self.assertEqual(nbQueries, self.getMetric('all-outqueries'))
dd54e182 158 self.assertGreater(self.getMetric('aggressive-nsec-cache-nsec-wc-hits'), hits)
811bddf9
OM
159 self.assertEqual(res.edns, 0)
160 self.assertEqual(len(res.options), 1)
161 self.assertEqual(res.options[0].otype, 15)
7f8f0893 162 self.assertEqual(res.options[0], extendederrors.ExtendedErrorOption(29, b'Result synthesized from aggressive NSEC cache (RFC8198)'))
dd54e182
RG
163
164 # now we ask for a type that does not exist at the wildcard
165 hits = self.getMetric('aggressive-nsec-cache-nsec-hits')
166 nbQueries = self.getMetric('all-outqueries')
167 res = self.sendQuery('test1.wildcard.secure.example.', 'AAAA')
168 self.assertRcodeEqual(res, dns.rcode.NOERROR)
169 self.assertAnswerEmpty(res)
170 self.assertAuthorityHasSOA(res)
171 self.assertMessageIsAuthenticated(res)
4bfebc93 172 self.assertEqual(nbQueries, self.getMetric('all-outqueries'))
dd54e182 173 self.assertGreater(self.getMetric('aggressive-nsec-cache-nsec-hits'), hits)
811bddf9
OM
174 self.assertEqual(res.edns, 0)
175 self.assertEqual(len(res.options), 1)
176 self.assertEqual(res.options[0].otype, 15)
7f8f0893 177 self.assertEqual(res.options[0], extendederrors.ExtendedErrorOption(29, b'Result synthesized from aggressive NSEC cache (RFC8198)'))
dd54e182
RG
178
179 # we can also ask a different type, for a different name that is covered
180 # by the NSEC and matches the wildcard (but the type does not exist)
181 hits = self.getMetric('aggressive-nsec-cache-nsec-wc-hits')
182 nbQueries = self.getMetric('all-outqueries')
183 res = self.sendQuery('test3.wildcard.secure.example.', 'TXT')
184 self.assertRcodeEqual(res, dns.rcode.NOERROR)
185 self.assertAnswerEmpty(res)
186 self.assertAuthorityHasSOA(res)
187 self.assertMessageIsAuthenticated(res)
4bfebc93 188 self.assertEqual(nbQueries, self.getMetric('all-outqueries'))
dd54e182 189 self.assertGreater(self.getMetric('aggressive-nsec-cache-nsec-hits'), hits)
811bddf9
OM
190 self.assertEqual(res.edns, 0)
191 self.assertEqual(len(res.options), 1)
192 self.assertEqual(res.options[0].otype, 15)
7f8f0893 193 self.assertEqual(res.options[0], extendederrors.ExtendedErrorOption(29, b'Result synthesized from aggressive NSEC cache (RFC8198)'))
dd54e182
RG
194
195 def test_Bogus(self):
2ec80d48
OM
196 self.wipe()
197
198 # query a name in example to fill the aggressive negcache
199 res = self.sendQuery('example.', 'A')
200 self.assertRcodeEqual(res, dns.rcode.NOERROR)
201 self.assertAnswerEmpty(res)
202 self.assertEqual(1, self.getMetric('aggressive-nsec-cache-entries'))
203
dd54e182 204 # query a name in a Bogus zone
dd54e182
RG
205 res = self.sendQuery('ted1.bogus.example.', 'A')
206 self.assertRcodeEqual(res, dns.rcode.SERVFAIL)
207 self.assertAnswerEmpty(res)
208
209 # disable validation
210 msg = dns.message.make_query('ted1.bogus.example.', 'A', want_dnssec=True)
211 msg.flags |= dns.flags.CD
212
213 res = self.sendUDPQuery(msg)
214 self.assertRcodeEqual(res, dns.rcode.NXDOMAIN)
215 self.assertAnswerEmpty(res)
216 self.assertAuthorityHasSOA(res)
217
218 # check that we _do not_ use the aggressive NSEC cache
219 nbQueries = self.getMetric('all-outqueries')
220 msg = dns.message.make_query('ted2.bogus.example.', 'A', want_dnssec=True)
221 msg.flags |= dns.flags.CD
222
223 res = self.sendUDPQuery(msg)
224 self.assertRcodeEqual(res, dns.rcode.NXDOMAIN)
225 self.assertAnswerEmpty(res)
226 self.assertAuthorityHasSOA(res)
227 self.assertGreater(self.getMetric('all-outqueries'), nbQueries)
2ec80d48
OM
228
229 # Check that we stil have one aggressive cache entry
230 self.assertEqual(1, self.getMetric('aggressive-nsec-cache-entries'))
811bddf9
OM
231 print(res.options)
232 self.assertEqual(res.edns, 0)
233 self.assertEqual(len(res.options), 1)
234 self.assertEqual(res.options[0].otype, 15)
235 self.assertEqual(res.options[0], extendederrors.ExtendedErrorOption(9, b''))
dd54e182
RG
236
237class AggressiveNSECCacheNSEC3(AggressiveNSECCacheBase):
238 _confdir = 'AggressiveNSECCacheNSEC3'
239 __test__ = True
240
241 @classmethod
242 def secureZone(cls, confdir, zonename, key=None):
243 zone = '.' if zonename == 'ROOT' else zonename
244 if not key:
245 pdnsutilCmd = [os.environ['PDNSUTIL'],
246 '--config-dir=%s' % confdir,
247 'secure-zone',
248 zone]
249 else:
250 keyfile = os.path.join(confdir, 'dnssec.key')
251 with open(keyfile, 'w') as fdKeyfile:
252 fdKeyfile.write(key)
253
254 pdnsutilCmd = [os.environ['PDNSUTIL'],
255 '--config-dir=%s' % confdir,
256 'import-zone-key',
257 zone,
258 keyfile,
259 'active',
260 'ksk']
261
262 print(' '.join(pdnsutilCmd))
263 try:
264 subprocess.check_output(pdnsutilCmd, stderr=subprocess.STDOUT)
265 except subprocess.CalledProcessError as e:
266 raise AssertionError('%s failed (%d): %s' % (pdnsutilCmd, e.returncode, e.output))
267
268 params = "1 0 100 AABBCCDDEEFF112233"
269
270 if zone == "optout.example":
271 params = "1 1 100 AABBCCDDEEFF112233"
272
273 pdnsutilCmd = [os.environ['PDNSUTIL'],
274 '--config-dir=%s' % confdir,
275 'set-nsec3',
276 zone,
277 params]
278
279 print(' '.join(pdnsutilCmd))
280 try:
281 subprocess.check_output(pdnsutilCmd, stderr=subprocess.STDOUT)
282 except subprocess.CalledProcessError as e:
283 raise AssertionError('%s failed (%d): %s' % (pdnsutilCmd, e.returncode, e.output))
284
285 def testNXD(self):
7d3d2f4f 286 self.wipe()
dd54e182 287
e746a2f6 288 # first we query a nonexistent name, to get the needed NSEC3s in our cache
dd54e182
RG
289 res = self.sendQuery('host2.secure.example.', 'TXT')
290 self.assertRcodeEqual(res, dns.rcode.NXDOMAIN)
291 self.assertAnswerEmpty(res)
292 self.assertAuthorityHasSOA(res)
293 self.assertMessageIsAuthenticated(res)
294
295 # now we ask for a different name that is covered by the NSEC3s,
296 # we should generate the answer from the NSEC3s and no outgoing query should be made
297 nbQueries = self.getMetric('all-outqueries')
298 res = self.sendQuery('host6.secure.example.', 'AAAA')
299 self.assertRcodeEqual(res, dns.rcode.NXDOMAIN)
300 self.assertAnswerEmpty(res)
301 self.assertAuthorityHasSOA(res)
302 self.assertMessageIsAuthenticated(res)
4bfebc93 303 self.assertEqual(nbQueries, self.getMetric('all-outqueries'))
811bddf9
OM
304 self.assertEqual(res.edns, 0)
305 self.assertEqual(len(res.options), 1)
306 self.assertEqual(res.options[0].otype, 15)
7f8f0893 307 self.assertEqual(res.options[0], extendederrors.ExtendedErrorOption(29, b'Result synthesized from aggressive NSEC cache (RFC8198)'))
dd54e182
RG
308
309 def testWildcard(self):
7d3d2f4f 310 self.wipe()
dd54e182 311
8a64a821
RG
312 # first let's get the SOA and wildcard NSEC in our cache by asking a name that matches the wildcard
313 # but a type that does not exist
314 res = self.sendQuery('test1.wildcard.secure.example.', 'AAAA')
315 self.assertRcodeEqual(res, dns.rcode.NOERROR)
316 self.assertAnswerEmpty(res)
317 self.assertAuthorityHasSOA(res)
318 self.assertMessageIsAuthenticated(res)
319
e746a2f6 320 # we query a nonexistent name, but for which a wildcard matches,
dd54e182
RG
321 # to get the NSEC3 in our cache
322 res = self.sendQuery('test5.wildcard.secure.example.', 'A')
323 expected = dns.rrset.from_text('test5.wildcard.secure.example.', 0, dns.rdataclass.IN, 'A', '{prefix}.10'.format(prefix=self._PREFIX))
324 self.assertRcodeEqual(res, dns.rcode.NOERROR)
325 self.assertMatchingRRSIGInAnswer(res, expected)
326 self.assertMessageIsAuthenticated(res)
327
328 # now we ask for a different name, we should generate the answer from the NSEC3s and the wildcard,
329 # and no outgoing query should be made
330 nbQueries = self.getMetric('all-outqueries')
331 res = self.sendQuery('test6.wildcard.secure.example.', 'A')
332 expected = dns.rrset.from_text('test6.wildcard.secure.example.', 0, dns.rdataclass.IN, 'A', '{prefix}.10'.format(prefix=self._PREFIX))
333 self.assertRcodeEqual(res, dns.rcode.NOERROR)
334 self.assertMatchingRRSIGInAnswer(res, expected)
335 self.assertMessageIsAuthenticated(res)
4bfebc93 336 self.assertEqual(nbQueries, self.getMetric('all-outqueries'))
811bddf9
OM
337 self.assertEqual(res.edns, 0)
338 self.assertEqual(len(res.options), 1)
339 self.assertEqual(res.options[0].otype, 15)
7f8f0893 340 self.assertEqual(res.options[0], extendederrors.ExtendedErrorOption(29, b'Result synthesized from aggressive NSEC cache (RFC8198)'))
dd54e182
RG
341
342 # now we ask for a type that does not exist at the wildcard
343 nbQueries = self.getMetric('all-outqueries')
344 res = self.sendQuery('test5.wildcard.secure.example.', 'AAAA')
345 self.assertRcodeEqual(res, dns.rcode.NOERROR)
346 self.assertAnswerEmpty(res)
347 self.assertAuthorityHasSOA(res)
348 self.assertMessageIsAuthenticated(res)
4bfebc93 349 self.assertEqual(nbQueries, self.getMetric('all-outqueries'))
811bddf9
OM
350 self.assertEqual(res.edns, 0)
351 self.assertEqual(len(res.options), 1)
352 self.assertEqual(res.options[0].otype, 15)
7f8f0893 353 self.assertEqual(res.options[0], extendederrors.ExtendedErrorOption(29, b'Result synthesized from aggressive NSEC cache (RFC8198)'))
dd54e182
RG
354
355 # we can also ask a different type, for a different name that is covered
356 # by the NSEC3s and matches the wildcard (but the type does not exist)
357 nbQueries = self.getMetric('all-outqueries')
358 res = self.sendQuery('test6.wildcard.secure.example.', 'TXT')
359 self.assertRcodeEqual(res, dns.rcode.NOERROR)
360 self.assertAnswerEmpty(res)
361 self.assertAuthorityHasSOA(res)
362 self.assertMessageIsAuthenticated(res)
4bfebc93 363 self.assertEqual(nbQueries, self.getMetric('all-outqueries'))
811bddf9
OM
364 self.assertEqual(res.edns, 0)
365 self.assertEqual(len(res.options), 1)
366 self.assertEqual(res.options[0].otype, 15)
7f8f0893 367 self.assertEqual(res.options[0], extendederrors.ExtendedErrorOption(29, b'Result synthesized from aggressive NSEC cache (RFC8198)'))
dd54e182
RG
368
369 def test_OptOut(self):
7d3d2f4f
OM
370 self.wipe()
371
dd54e182
RG
372 # query a name in an opt-out zone
373 res = self.sendQuery('ns2.optout.example.', 'A')
374 self.assertRcodeEqual(res, dns.rcode.NXDOMAIN)
375 self.assertAnswerEmpty(res)
376 self.assertAuthorityHasSOA(res)
377
378 # check that we _do not_ use the aggressive NSEC cache
379 nbQueries = self.getMetric('all-outqueries')
380 res = self.sendQuery('ns3.optout.example.', 'A')
381 self.assertRcodeEqual(res, dns.rcode.NXDOMAIN)
382 self.assertAnswerEmpty(res)
383 self.assertAuthorityHasSOA(res)
384 self.assertGreater(self.getMetric('all-outqueries'), nbQueries)
811bddf9
OM
385 self.assertEqual(res.edns, 0)
386 self.assertEqual(len(res.options), 0)