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