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