]> git.ipfire.org Git - thirdparty/pdns.git/blob - regression-tests.recursor-dnssec/test_RPZ.py
Merge pull request #8115 from rgacogne/dnsdist-ecs-before-tsig
[thirdparty/pdns.git] / regression-tests.recursor-dnssec / test_RPZ.py
1 import dns
2 import json
3 import os
4 import requests
5 import socket
6 import struct
7 import sys
8 import threading
9 import time
10
11 from recursortests import RecursorTest
12
13 class RPZServer(object):
14
15 def __init__(self, port):
16 self._currentSerial = 0
17 self._targetSerial = 1
18 self._serverPort = port
19 listener = threading.Thread(name='RPZ Listener', target=self._listener, args=[])
20 listener.setDaemon(True)
21 listener.start()
22
23 def getCurrentSerial(self):
24 return self._currentSerial
25
26 def moveToSerial(self, newSerial):
27 if newSerial == self._currentSerial:
28 return False
29
30 if newSerial != self._currentSerial + 1:
31 raise AssertionError("Asking the RPZ server to server serial %d, already serving %d" % (newSerial, self._currentSerial))
32 self._targetSerial = newSerial
33 return True
34
35 def _getAnswer(self, message):
36
37 response = dns.message.make_response(message)
38 records = []
39
40 if message.question[0].rdtype == dns.rdatatype.AXFR:
41 if self._currentSerial != 0:
42 print('Received an AXFR query but IXFR expected because the current serial is %d' % (self._currentSerial))
43 return (None, self._currentSerial)
44
45 newSerial = self._targetSerial
46 records = [
47 dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial),
48 dns.rrset.from_text('a.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'),
49 dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial)
50 ]
51
52 elif message.question[0].rdtype == dns.rdatatype.IXFR:
53 oldSerial = message.authority[0][0].serial
54
55 if oldSerial != self._currentSerial:
56 print('Received an IXFR query with an unexpected serial %d, expected %d' % (oldSerial, self._currentSerial))
57 return (None, self._currentSerial)
58
59 newSerial = self._targetSerial
60 if newSerial == 2:
61 records = [
62 dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial),
63 dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % oldSerial),
64 # no deletion
65 dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial),
66 dns.rrset.from_text('b.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'),
67 dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial)
68 ]
69 elif newSerial == 3:
70 records = [
71 dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial),
72 dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % oldSerial),
73 dns.rrset.from_text('a.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'),
74 dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial),
75 # no addition
76 dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial)
77 ]
78 elif newSerial == 4:
79 records = [
80 dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial),
81 dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % oldSerial),
82 dns.rrset.from_text('b.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'),
83 dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial),
84 dns.rrset.from_text('c.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'),
85 dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial)
86 ]
87 elif newSerial == 5:
88 # this one is a bit special, we are answering with a full AXFR
89 records = [
90 dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial),
91 dns.rrset.from_text('d.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'),
92 dns.rrset.from_text('tc.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-tcp-only.'),
93 dns.rrset.from_text('drop.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-drop.'),
94 dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial)
95 ]
96 elif newSerial == 6:
97 # back to IXFR
98 records = [
99 dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial),
100 dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % oldSerial),
101 dns.rrset.from_text('d.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'),
102 dns.rrset.from_text('tc.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-tcp-only.'),
103 dns.rrset.from_text('drop.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-drop.'),
104 dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial),
105 dns.rrset.from_text('e.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1', '192.0.2.2'),
106 dns.rrset.from_text('e.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.MX, '10 mx.example.'),
107 dns.rrset.from_text('f.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'e.example.'),
108 dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial)
109 ]
110 elif newSerial == 7:
111 records = [
112 dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial),
113 dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % oldSerial),
114 dns.rrset.from_text('e.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1', '192.0.2.2'),
115 dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial),
116 dns.rrset.from_text('e.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.2'),
117 dns.rrset.from_text('tc.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-tcp-only.'),
118 dns.rrset.from_text('drop.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-drop.'),
119 dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial)
120 ]
121 elif newSerial == 8:
122 # this one is a bit special too, we are answering with a full AXFR and the new zone is empty
123 records = [
124 dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial),
125 dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial)
126 ]
127
128 response.answer = records
129 return (newSerial, response)
130
131 def _connectionHandler(self, conn):
132 data = None
133 while True:
134 data = conn.recv(2)
135 if not data:
136 break
137 (datalen,) = struct.unpack("!H", data)
138 data = conn.recv(datalen)
139 if not data:
140 break
141
142 message = dns.message.from_wire(data)
143 if len(message.question) != 1:
144 print('Invalid RPZ query, qdcount is %d' % (len(message.question)))
145 break
146 if not message.question[0].rdtype in [dns.rdatatype.AXFR, dns.rdatatype.IXFR]:
147 print('Invalid RPZ query, qtype is %d' % (message.question.rdtype))
148 break
149 (serial, answer) = self._getAnswer(message)
150 if not answer:
151 print('Unable to get a response for %s %d' % (message.question[0].name, message.question[0].rdtype))
152 break
153
154 wire = answer.to_wire()
155 conn.send(struct.pack("!H", len(wire)))
156 conn.send(wire)
157 self._currentSerial = serial
158 break
159
160 conn.close()
161
162 def _listener(self):
163 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
164 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
165 try:
166 sock.bind(("127.0.0.1", self._serverPort))
167 except socket.error as e:
168 print("Error binding in the RPZ listener: %s" % str(e))
169 sys.exit(1)
170
171 sock.listen(100)
172 while True:
173 try:
174 (conn, _) = sock.accept()
175 thread = threading.Thread(name='RPZ Connection Handler',
176 target=self._connectionHandler,
177 args=[conn])
178 thread.setDaemon(True)
179 thread.start()
180
181 except socket.error as e:
182 print('Error in RPZ socket: %s' % str(e))
183 sock.close()
184
185 class RPZRecursorTest(RecursorTest):
186 _wsPort = 8042
187 _wsTimeout = 2
188 _wsPassword = 'secretpassword'
189 _apiKey = 'secretapikey'
190 _confdir = 'RPZ'
191 _lua_dns_script_file = """
192
193 function prerpz(dq)
194 -- disable the RPZ policy named 'zone.rpz' for AD=1 queries
195 if dq:getDH():getAD() then
196 dq:discardPolicy('zone.rpz.')
197 end
198 return false
199 end
200 """
201
202 _config_template = """
203 auth-zones=example=configs/%s/example.zone
204 webserver=yes
205 webserver-port=%d
206 webserver-address=127.0.0.1
207 webserver-password=%s
208 api-key=%s
209 log-rpz-changes=yes
210 """ % (_confdir, _wsPort, _wsPassword, _apiKey)
211
212 @classmethod
213 def setUpClass(cls):
214
215 cls.setUpSockets()
216 cls.startResponders()
217
218 confdir = os.path.join('configs', cls._confdir)
219 cls.createConfigDir(confdir)
220
221 cls.generateRecursorConfig(confdir)
222 cls.startRecursor(confdir, cls._recursorPort)
223
224 @classmethod
225 def tearDownClass(cls):
226 cls.tearDownRecursor()
227
228 def checkBlocked(self, name, shouldBeBlocked=True, adQuery=False):
229 query = dns.message.make_query(name, 'A', want_dnssec=True)
230 query.flags |= dns.flags.CD
231 if adQuery:
232 query.flags |= dns.flags.AD
233
234 for method in ("sendUDPQuery", "sendTCPQuery"):
235 sender = getattr(self, method)
236 res = sender(query)
237 self.assertRcodeEqual(res, dns.rcode.NOERROR)
238 if shouldBeBlocked:
239 expected = dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.1')
240 else:
241 expected = dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.42')
242
243 self.assertRRsetInAnswer(res, expected)
244
245 def checkNotBlocked(self, name, adQuery=False):
246 self.checkBlocked(name, False, adQuery)
247
248 def checkCustom(self, qname, qtype, expected):
249 query = dns.message.make_query(qname, qtype, want_dnssec=True)
250 query.flags |= dns.flags.CD
251 for method in ("sendUDPQuery", "sendTCPQuery"):
252 sender = getattr(self, method)
253 res = sender(query)
254 self.assertRcodeEqual(res, dns.rcode.NOERROR)
255 self.assertRRsetInAnswer(res, expected)
256
257 def checkNoData(self, qname, qtype):
258 query = dns.message.make_query(qname, qtype, want_dnssec=True)
259 query.flags |= dns.flags.CD
260 for method in ("sendUDPQuery", "sendTCPQuery"):
261 sender = getattr(self, method)
262 res = sender(query)
263 self.assertRcodeEqual(res, dns.rcode.NOERROR)
264 self.assertEqual(len(res.answer), 0)
265
266 def checkNXD(self, qname, qtype='A'):
267 query = dns.message.make_query(qname, qtype, want_dnssec=True)
268 query.flags |= dns.flags.CD
269 for method in ("sendUDPQuery", "sendTCPQuery"):
270 sender = getattr(self, method)
271 res = sender(query)
272 self.assertRcodeEqual(res, dns.rcode.NXDOMAIN)
273 self.assertEqual(len(res.answer), 0)
274 self.assertEqual(len(res.authority), 1)
275
276 def checkTruncated(self, qname, qtype='A'):
277 query = dns.message.make_query(qname, qtype, want_dnssec=True)
278 query.flags |= dns.flags.CD
279 res = self.sendUDPQuery(query)
280 self.assertRcodeEqual(res, dns.rcode.NOERROR)
281 self.assertMessageHasFlags(res, ['QR', 'RA', 'RD', 'CD', 'TC'])
282 self.assertEqual(len(res.answer), 0)
283 self.assertEqual(len(res.authority), 0)
284 self.assertEqual(len(res.additional), 0)
285
286 res = self.sendTCPQuery(query)
287 self.assertRcodeEqual(res, dns.rcode.NXDOMAIN)
288 self.assertMessageHasFlags(res, ['QR', 'RA', 'RD', 'CD'])
289 self.assertEqual(len(res.answer), 0)
290 self.assertEqual(len(res.authority), 1)
291 self.assertEqual(len(res.additional), 0)
292
293 def checkDropped(self, qname, qtype='A'):
294 query = dns.message.make_query(qname, qtype, want_dnssec=True)
295 query.flags |= dns.flags.CD
296 for method in ("sendUDPQuery", "sendTCPQuery"):
297 sender = getattr(self, method)
298 res = sender(query)
299 self.assertEqual(res, None)
300
301 def checkRPZStats(self, serial, recordsCount, fullXFRCount, totalXFRCount):
302 headers = {'x-api-key': self._apiKey}
303 url = 'http://127.0.0.1:' + str(self._wsPort) + '/api/v1/servers/localhost/rpzstatistics'
304 r = requests.get(url, headers=headers, timeout=self._wsTimeout)
305 self.assertTrue(r)
306 self.assertEquals(r.status_code, 200)
307 self.assertTrue(r.json())
308 content = r.json()
309 self.assertIn('zone.rpz.', content)
310 zone = content['zone.rpz.']
311 for key in ['last_update', 'records', 'serial', 'transfers_failed', 'transfers_full', 'transfers_success']:
312 self.assertIn(key, zone)
313
314 self.assertEquals(zone['serial'], serial)
315 self.assertEquals(zone['records'], recordsCount)
316 self.assertEquals(zone['transfers_full'], fullXFRCount)
317 self.assertEquals(zone['transfers_success'], totalXFRCount)
318
319 rpzServerPort = 4250
320 rpzServer = RPZServer(rpzServerPort)
321
322 class RPZXFRRecursorTest(RPZRecursorTest):
323 """
324 This test makes sure that we correctly update RPZ zones via AXFR then IXFR
325 """
326
327 global rpzServerPort
328 _lua_config_file = """
329 -- The first server is a bogus one, to test that we correctly fail over to the second one
330 rpzMaster({'127.0.0.1:9999', '127.0.0.1:%d'}, 'zone.rpz.', { refresh=1 })
331 """ % (rpzServerPort)
332 _confdir = 'RPZXFR'
333 _wsPort = 8042
334 _wsTimeout = 2
335 _wsPassword = 'secretpassword'
336 _apiKey = 'secretapikey'
337 _config_template = """
338 auth-zones=example=configs/%s/example.zone
339 webserver=yes
340 webserver-port=%d
341 webserver-address=127.0.0.1
342 webserver-password=%s
343 api-key=%s
344 """ % (_confdir, _wsPort, _wsPassword, _apiKey)
345 _xfrDone = 0
346
347 @classmethod
348 def generateRecursorConfig(cls, confdir):
349 authzonepath = os.path.join(confdir, 'example.zone')
350 with open(authzonepath, 'w') as authzone:
351 authzone.write("""$ORIGIN example.
352 @ 3600 IN SOA {soa}
353 a 3600 IN A 192.0.2.42
354 b 3600 IN A 192.0.2.42
355 c 3600 IN A 192.0.2.42
356 d 3600 IN A 192.0.2.42
357 e 3600 IN A 192.0.2.42
358 """.format(soa=cls._SOA))
359 super(RPZRecursorTest, cls).generateRecursorConfig(confdir)
360
361 def waitUntilCorrectSerialIsLoaded(self, serial, timeout=5):
362 global rpzServer
363
364 rpzServer.moveToSerial(serial)
365
366 attempts = 0
367 while attempts < timeout:
368 currentSerial = rpzServer.getCurrentSerial()
369 if currentSerial > serial:
370 raise AssertionError("Expected serial %d, got %d" % (serial, currentSerial))
371 if currentSerial == serial:
372 self._xfrDone = self._xfrDone + 1
373 return
374
375 attempts = attempts + 1
376 time.sleep(1)
377
378 raise AssertionError("Waited %d seconds for the serial to be updated to %d but the serial is still %d" % (timeout, serial, currentSerial))
379
380 def testRPZ(self):
381 # first zone, only a should be blocked
382 self.waitUntilCorrectSerialIsLoaded(1)
383 self.checkRPZStats(1, 1, 1, self._xfrDone)
384 self.checkBlocked('a.example.')
385 self.checkNotBlocked('b.example.')
386 self.checkNotBlocked('c.example.')
387
388 # second zone, a and b should be blocked
389 self.waitUntilCorrectSerialIsLoaded(2)
390 self.checkRPZStats(2, 2, 1, self._xfrDone)
391 self.checkBlocked('a.example.')
392 self.checkBlocked('b.example.')
393 self.checkNotBlocked('c.example.')
394
395 # third zone, only b should be blocked
396 self.waitUntilCorrectSerialIsLoaded(3)
397 self.checkRPZStats(3, 1, 1, self._xfrDone)
398 self.checkNotBlocked('a.example.')
399 self.checkBlocked('b.example.')
400 self.checkNotBlocked('c.example.')
401
402 # fourth zone, only c should be blocked
403 self.waitUntilCorrectSerialIsLoaded(4)
404 self.checkRPZStats(4, 1, 1, self._xfrDone)
405 self.checkNotBlocked('a.example.')
406 self.checkNotBlocked('b.example.')
407 self.checkBlocked('c.example.')
408
409 # fifth zone, we should get a full AXFR this time, and only d should be blocked
410 self.waitUntilCorrectSerialIsLoaded(5)
411 self.checkRPZStats(5, 3, 2, self._xfrDone)
412 self.checkNotBlocked('a.example.')
413 self.checkNotBlocked('b.example.')
414 self.checkNotBlocked('c.example.')
415 self.checkBlocked('d.example.')
416
417 # sixth zone, only e should be blocked, f is a local data record
418 self.waitUntilCorrectSerialIsLoaded(6)
419 self.checkRPZStats(6, 2, 2, self._xfrDone)
420 self.checkNotBlocked('a.example.')
421 self.checkNotBlocked('b.example.')
422 self.checkNotBlocked('c.example.')
423 self.checkNotBlocked('d.example.')
424 self.checkCustom('e.example.', 'A', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.1', '192.0.2.2'))
425 self.checkCustom('e.example.', 'MX', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'MX', '10 mx.example.'))
426 self.checkNoData('e.example.', 'AAAA')
427 self.checkCustom('f.example.', 'A', dns.rrset.from_text('f.example.', 0, dns.rdataclass.IN, 'CNAME', 'e.example.'))
428
429 # seventh zone, e should only have one A
430 self.waitUntilCorrectSerialIsLoaded(7)
431 self.checkRPZStats(7, 4, 2, self._xfrDone)
432 self.checkNotBlocked('a.example.')
433 self.checkNotBlocked('b.example.')
434 self.checkNotBlocked('c.example.')
435 self.checkNotBlocked('d.example.')
436 self.checkCustom('e.example.', 'A', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.2'))
437 self.checkCustom('e.example.', 'MX', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'MX', '10 mx.example.'))
438 self.checkNoData('e.example.', 'AAAA')
439 self.checkCustom('f.example.', 'A', dns.rrset.from_text('f.example.', 0, dns.rdataclass.IN, 'CNAME', 'e.example.'))
440 # check that the policy is disabled for AD=1 queries
441 self.checkNotBlocked('e.example.', True)
442 # check non-custom policies
443 self.checkTruncated('tc.example.')
444 self.checkDropped('drop.example.')
445
446 # eighth zone, all entries should be gone
447 self.waitUntilCorrectSerialIsLoaded(8)
448 self.checkRPZStats(8, 0, 3, self._xfrDone)
449 self.checkNotBlocked('a.example.')
450 self.checkNotBlocked('b.example.')
451 self.checkNotBlocked('c.example.')
452 self.checkNotBlocked('d.example.')
453 self.checkNotBlocked('e.example.')
454 self.checkNXD('f.example.')
455 self.checkNXD('tc.example.')
456 self.checkNXD('drop.example.')
457
458 class RPZFileRecursorTest(RPZRecursorTest):
459 """
460 This test makes sure that we correctly load RPZ zones from a file
461 """
462
463 _confdir = 'RPZFile'
464 _wsPort = 8042
465 _wsTimeout = 2
466 _wsPassword = 'secretpassword'
467 _apiKey = 'secretapikey'
468 _lua_config_file = """
469 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz." })
470 """ % (_confdir)
471 _config_template = """
472 auth-zones=example=configs/%s/example.zone
473 webserver=yes
474 webserver-port=%d
475 webserver-address=127.0.0.1
476 webserver-password=%s
477 api-key=%s
478 """ % (_confdir, _wsPort, _wsPassword, _apiKey)
479
480 @classmethod
481 def generateRecursorConfig(cls, confdir):
482 authzonepath = os.path.join(confdir, 'example.zone')
483 with open(authzonepath, 'w') as authzone:
484 authzone.write("""$ORIGIN example.
485 @ 3600 IN SOA {soa}
486 a 3600 IN A 192.0.2.42
487 b 3600 IN A 192.0.2.42
488 c 3600 IN A 192.0.2.42
489 d 3600 IN A 192.0.2.42
490 e 3600 IN A 192.0.2.42
491 z 3600 IN A 192.0.2.42
492 """.format(soa=cls._SOA))
493
494 rpzFilePath = os.path.join(confdir, 'zone.rpz')
495 with open(rpzFilePath, 'w') as rpzZone:
496 rpzZone.write("""$ORIGIN zone.rpz.
497 @ 3600 IN SOA {soa}
498 a.example.zone.rpz. 60 IN A 192.0.2.42
499 a.example.zone.rpz. 60 IN A 192.0.2.43
500 a.example.zone.rpz. 60 IN TXT "some text"
501 drop.example.zone.rpz. 60 IN CNAME rpz-drop.
502 z.example.zone.rpz. 60 IN A 192.0.2.1
503 tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
504 """.format(soa=cls._SOA))
505 super(RPZFileRecursorTest, cls).generateRecursorConfig(confdir)
506
507 def testRPZ(self):
508 self.checkCustom('a.example.', 'A', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.42', '192.0.2.43'))
509 self.checkCustom('a.example.', 'TXT', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'TXT', '"some text"'))
510 self.checkBlocked('z.example.')
511 self.checkNotBlocked('b.example.')
512 self.checkNotBlocked('c.example.')
513 self.checkNotBlocked('d.example.')
514 self.checkNotBlocked('e.example.')
515 # check that the policy is disabled for AD=1 queries
516 self.checkNotBlocked('z.example.', True)
517 # check non-custom policies
518 self.checkTruncated('tc.example.')
519 self.checkDropped('drop.example.')
520
521 class RPZFileDefaultPolRecursorTest(RPZRecursorTest):
522 """
523 This test makes sure that we correctly load RPZ zones from a file with a default policy
524 """
525
526 _confdir = 'RPZFileDefaultPolicy'
527 _wsPort = 8042
528 _wsTimeout = 2
529 _wsPassword = 'secretpassword'
530 _apiKey = 'secretapikey'
531 _lua_config_file = """
532 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction })
533 """ % (_confdir)
534 _config_template = """
535 auth-zones=example=configs/%s/example.zone
536 webserver=yes
537 webserver-port=%d
538 webserver-address=127.0.0.1
539 webserver-password=%s
540 api-key=%s
541 """ % (_confdir, _wsPort, _wsPassword, _apiKey)
542
543 @classmethod
544 def generateRecursorConfig(cls, confdir):
545 authzonepath = os.path.join(confdir, 'example.zone')
546 with open(authzonepath, 'w') as authzone:
547 authzone.write("""$ORIGIN example.
548 @ 3600 IN SOA {soa}
549 a 3600 IN A 192.0.2.42
550 b 3600 IN A 192.0.2.42
551 c 3600 IN A 192.0.2.42
552 d 3600 IN A 192.0.2.42
553 drop 3600 IN A 192.0.2.42
554 e 3600 IN A 192.0.2.42
555 z 3600 IN A 192.0.2.42
556 """.format(soa=cls._SOA))
557
558 rpzFilePath = os.path.join(confdir, 'zone.rpz')
559 with open(rpzFilePath, 'w') as rpzZone:
560 rpzZone.write("""$ORIGIN zone.rpz.
561 @ 3600 IN SOA {soa}
562 a.example.zone.rpz. 60 IN A 192.0.2.42
563 drop.example.zone.rpz. 60 IN CNAME rpz-drop.
564 z.example.zone.rpz. 60 IN A 192.0.2.1
565 tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
566 """.format(soa=cls._SOA))
567 super(RPZFileDefaultPolRecursorTest, cls).generateRecursorConfig(confdir)
568
569 def testRPZ(self):
570 # local data entries are overridden by default
571 self.checkCustom('a.example.', 'A', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.42'))
572 self.checkNoData('a.example.', 'TXT')
573 # will not be blocked because the default policy overrides local data entries by default
574 self.checkNotBlocked('z.example.')
575 self.checkNotBlocked('b.example.')
576 self.checkNotBlocked('c.example.')
577 self.checkNotBlocked('d.example.')
578 self.checkNotBlocked('e.example.')
579 # check non-local policies, they should be overridden by the default policy
580 self.checkNXD('tc.example.', 'A')
581 self.checkNotBlocked('drop.example.')
582
583 class RPZFileDefaultPolNotOverrideLocalRecursorTest(RPZRecursorTest):
584 """
585 This test makes sure that we correctly load RPZ zones from a file with a default policy, not overriding local data entries
586 """
587
588 _confdir = 'RPZFileDefaultPolicyNotOverrideLocal'
589 _wsPort = 8042
590 _wsTimeout = 2
591 _wsPassword = 'secretpassword'
592 _apiKey = 'secretapikey'
593 _lua_config_file = """
594 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction, defpolOverrideLocalData=false })
595 """ % (_confdir)
596 _config_template = """
597 auth-zones=example=configs/%s/example.zone
598 webserver=yes
599 webserver-port=%d
600 webserver-address=127.0.0.1
601 webserver-password=%s
602 api-key=%s
603 """ % (_confdir, _wsPort, _wsPassword, _apiKey)
604
605 @classmethod
606 def generateRecursorConfig(cls, confdir):
607 authzonepath = os.path.join(confdir, 'example.zone')
608 with open(authzonepath, 'w') as authzone:
609 authzone.write("""$ORIGIN example.
610 @ 3600 IN SOA {soa}
611 a 3600 IN A 192.0.2.42
612 b 3600 IN A 192.0.2.42
613 c 3600 IN A 192.0.2.42
614 d 3600 IN A 192.0.2.42
615 drop 3600 IN A 192.0.2.42
616 e 3600 IN A 192.0.2.42
617 z 3600 IN A 192.0.2.42
618 """.format(soa=cls._SOA))
619
620 rpzFilePath = os.path.join(confdir, 'zone.rpz')
621 with open(rpzFilePath, 'w') as rpzZone:
622 rpzZone.write("""$ORIGIN zone.rpz.
623 @ 3600 IN SOA {soa}
624 a.example.zone.rpz. 60 IN A 192.0.2.42
625 a.example.zone.rpz. 60 IN A 192.0.2.43
626 a.example.zone.rpz. 60 IN TXT "some text"
627 drop.example.zone.rpz. 60 IN CNAME rpz-drop.
628 z.example.zone.rpz. 60 IN A 192.0.2.1
629 tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
630 """.format(soa=cls._SOA))
631 super(RPZFileDefaultPolNotOverrideLocalRecursorTest, cls).generateRecursorConfig(confdir)
632
633 def testRPZ(self):
634 # local data entries will not be overridden by the default polic
635 self.checkCustom('a.example.', 'A', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.42', '192.0.2.43'))
636 self.checkCustom('a.example.', 'TXT', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'TXT', '"some text"'))
637 # will be blocked because the default policy does not override local data entries
638 self.checkBlocked('z.example.')
639 self.checkNotBlocked('b.example.')
640 self.checkNotBlocked('c.example.')
641 self.checkNotBlocked('d.example.')
642 self.checkNotBlocked('e.example.')
643 # check non-local policies, they should be overridden by the default policy
644 self.checkNXD('tc.example.', 'A')
645 self.checkNotBlocked('drop.example.')
646
647 class RPZOrderingPrecedenceRecursorTesT(RPZRecursorTest):
648 """
649 This test makes sure that the recursor respects the RPZ ordering precedence rules
650 """
651
652 _confdir = 'RPZOrderingPrecedence'
653 _wsPort = 8042
654 _wsTimeout = 2
655 _wsPassword = 'secretpassword'
656 _apiKey = 'secretapikey'
657 _lua_config_file = """
658 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."})
659 rpzFile('configs/%s/zone2.rpz', { policyName="zone2.rpz."})
660 """ % (_confdir, _confdir)
661 _config_template = """
662 auth-zones=example=configs/%s/example.zone
663 webserver=yes
664 webserver-port=%d
665 webserver-address=127.0.0.1
666 webserver-password=%s
667 api-key=%s
668 """ % (_confdir, _wsPort, _wsPassword, _apiKey)
669
670 @classmethod
671 def generateRecursorConfig(cls, confdir):
672 authzonepath = os.path.join(confdir, 'example.zone')
673 with open(authzonepath, 'w') as authzone:
674 authzone.write("""$ORIGIN example.
675 @ 3600 IN SOA {soa}
676 sub.test 3600 IN A 192.0.2.42
677 """.format(soa=cls._SOA))
678
679 rpzFilePath = os.path.join(confdir, 'zone.rpz')
680 with open(rpzFilePath, 'w') as rpzZone:
681 rpzZone.write("""$ORIGIN zone.rpz.
682 @ 3600 IN SOA {soa}
683 *.test.example.zone.rpz. 60 IN CNAME rpz-passthru.
684 """.format(soa=cls._SOA))
685
686 rpzFilePath = os.path.join(confdir, 'zone2.rpz')
687 with open(rpzFilePath, 'w') as rpzZone:
688 rpzZone.write("""$ORIGIN zone2.rpz.
689 @ 3600 IN SOA {soa}
690 sub.test.example.com.zone2.rpz. 60 IN CNAME .
691 32.42.2.0.192.rpz-ip 60 IN CNAME .
692 """.format(soa=cls._SOA))
693
694 super(RPZOrderingPrecedenceRecursorTesT, cls).generateRecursorConfig(confdir)
695
696 def testRPZ(self):
697 # we should first match on the qname (the wildcard, not on the exact name since
698 # we respect the order of the RPZ zones), see the pass-thru rule
699 # and stop RPZ processing. The subsequent rule on the content of the A
700 # should therefore not trigger a NXDOMAIN.
701 self.checkNotBlocked('sub.test.example.')