]> git.ipfire.org Git - thirdparty/pdns.git/blob - regression-tests.recursor-dnssec/test_RPZ.py
Merge pull request #8094 from mind04/pdns-diff-config
[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, singleCheck=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 if singleCheck:
245 break
246
247 def checkNotBlocked(self, name, adQuery=False, singleCheck=False):
248 self.checkBlocked(name, False, adQuery, singleCheck)
249
250 def checkCustom(self, qname, qtype, expected):
251 query = dns.message.make_query(qname, qtype, want_dnssec=True)
252 query.flags |= dns.flags.CD
253 for method in ("sendUDPQuery", "sendTCPQuery"):
254 sender = getattr(self, method)
255 res = sender(query)
256 self.assertRcodeEqual(res, dns.rcode.NOERROR)
257 self.assertRRsetInAnswer(res, expected)
258
259 def checkNoData(self, qname, qtype):
260 query = dns.message.make_query(qname, qtype, want_dnssec=True)
261 query.flags |= dns.flags.CD
262 for method in ("sendUDPQuery", "sendTCPQuery"):
263 sender = getattr(self, method)
264 res = sender(query)
265 self.assertRcodeEqual(res, dns.rcode.NOERROR)
266 self.assertEqual(len(res.answer), 0)
267
268 def checkNXD(self, qname, qtype='A'):
269 query = dns.message.make_query(qname, qtype, want_dnssec=True)
270 query.flags |= dns.flags.CD
271 for method in ("sendUDPQuery", "sendTCPQuery"):
272 sender = getattr(self, method)
273 res = sender(query)
274 self.assertRcodeEqual(res, dns.rcode.NXDOMAIN)
275 self.assertEqual(len(res.answer), 0)
276 self.assertEqual(len(res.authority), 1)
277
278 def checkTruncated(self, qname, qtype='A'):
279 query = dns.message.make_query(qname, qtype, want_dnssec=True)
280 query.flags |= dns.flags.CD
281 res = self.sendUDPQuery(query)
282 self.assertRcodeEqual(res, dns.rcode.NOERROR)
283 self.assertMessageHasFlags(res, ['QR', 'RA', 'RD', 'CD', 'TC'])
284 self.assertEqual(len(res.answer), 0)
285 self.assertEqual(len(res.authority), 0)
286 self.assertEqual(len(res.additional), 0)
287
288 res = self.sendTCPQuery(query)
289 self.assertRcodeEqual(res, dns.rcode.NXDOMAIN)
290 self.assertMessageHasFlags(res, ['QR', 'RA', 'RD', 'CD'])
291 self.assertEqual(len(res.answer), 0)
292 self.assertEqual(len(res.authority), 1)
293 self.assertEqual(len(res.additional), 0)
294
295 def checkDropped(self, qname, qtype='A'):
296 query = dns.message.make_query(qname, qtype, want_dnssec=True)
297 query.flags |= dns.flags.CD
298 for method in ("sendUDPQuery", "sendTCPQuery"):
299 sender = getattr(self, method)
300 res = sender(query)
301 self.assertEqual(res, None)
302
303 def checkRPZStats(self, serial, recordsCount, fullXFRCount, totalXFRCount):
304 headers = {'x-api-key': self._apiKey}
305 url = 'http://127.0.0.1:' + str(self._wsPort) + '/api/v1/servers/localhost/rpzstatistics'
306 r = requests.get(url, headers=headers, timeout=self._wsTimeout)
307 self.assertTrue(r)
308 self.assertEquals(r.status_code, 200)
309 self.assertTrue(r.json())
310 content = r.json()
311 self.assertIn('zone.rpz.', content)
312 zone = content['zone.rpz.']
313 for key in ['last_update', 'records', 'serial', 'transfers_failed', 'transfers_full', 'transfers_success']:
314 self.assertIn(key, zone)
315
316 self.assertEquals(zone['serial'], serial)
317 self.assertEquals(zone['records'], recordsCount)
318 self.assertEquals(zone['transfers_full'], fullXFRCount)
319 self.assertEquals(zone['transfers_success'], totalXFRCount)
320
321 rpzServerPort = 4250
322 rpzServer = RPZServer(rpzServerPort)
323
324 class RPZXFRRecursorTest(RPZRecursorTest):
325 """
326 This test makes sure that we correctly update RPZ zones via AXFR then IXFR
327 """
328
329 global rpzServerPort
330 _lua_config_file = """
331 -- The first server is a bogus one, to test that we correctly fail over to the second one
332 rpzMaster({'127.0.0.1:9999', '127.0.0.1:%d'}, 'zone.rpz.', { refresh=1 })
333 """ % (rpzServerPort)
334 _confdir = 'RPZXFR'
335 _wsPort = 8042
336 _wsTimeout = 2
337 _wsPassword = 'secretpassword'
338 _apiKey = 'secretapikey'
339 _config_template = """
340 auth-zones=example=configs/%s/example.zone
341 webserver=yes
342 webserver-port=%d
343 webserver-address=127.0.0.1
344 webserver-password=%s
345 api-key=%s
346 """ % (_confdir, _wsPort, _wsPassword, _apiKey)
347 _xfrDone = 0
348
349 @classmethod
350 def generateRecursorConfig(cls, confdir):
351 authzonepath = os.path.join(confdir, 'example.zone')
352 with open(authzonepath, 'w') as authzone:
353 authzone.write("""$ORIGIN example.
354 @ 3600 IN SOA {soa}
355 a 3600 IN A 192.0.2.42
356 b 3600 IN A 192.0.2.42
357 c 3600 IN A 192.0.2.42
358 d 3600 IN A 192.0.2.42
359 e 3600 IN A 192.0.2.42
360 """.format(soa=cls._SOA))
361 super(RPZRecursorTest, cls).generateRecursorConfig(confdir)
362
363 def waitUntilCorrectSerialIsLoaded(self, serial, timeout=5):
364 global rpzServer
365
366 rpzServer.moveToSerial(serial)
367
368 attempts = 0
369 while attempts < timeout:
370 currentSerial = rpzServer.getCurrentSerial()
371 if currentSerial > serial:
372 raise AssertionError("Expected serial %d, got %d" % (serial, currentSerial))
373 if currentSerial == serial:
374 self._xfrDone = self._xfrDone + 1
375 return
376
377 attempts = attempts + 1
378 time.sleep(1)
379
380 raise AssertionError("Waited %d seconds for the serial to be updated to %d but the serial is still %d" % (timeout, serial, currentSerial))
381
382 def testRPZ(self):
383 # first zone, only a should be blocked
384 self.waitUntilCorrectSerialIsLoaded(1)
385 self.checkRPZStats(1, 1, 1, self._xfrDone)
386 self.checkBlocked('a.example.')
387 self.checkNotBlocked('b.example.')
388 self.checkNotBlocked('c.example.')
389
390 # second zone, a and b should be blocked
391 self.waitUntilCorrectSerialIsLoaded(2)
392 self.checkRPZStats(2, 2, 1, self._xfrDone)
393 self.checkBlocked('a.example.')
394 self.checkBlocked('b.example.')
395 self.checkNotBlocked('c.example.')
396
397 # third zone, only b should be blocked
398 self.waitUntilCorrectSerialIsLoaded(3)
399 self.checkRPZStats(3, 1, 1, self._xfrDone)
400 self.checkNotBlocked('a.example.')
401 self.checkBlocked('b.example.')
402 self.checkNotBlocked('c.example.')
403
404 # fourth zone, only c should be blocked
405 self.waitUntilCorrectSerialIsLoaded(4)
406 self.checkRPZStats(4, 1, 1, self._xfrDone)
407 self.checkNotBlocked('a.example.')
408 self.checkNotBlocked('b.example.')
409 self.checkBlocked('c.example.')
410
411 # fifth zone, we should get a full AXFR this time, and only d should be blocked
412 self.waitUntilCorrectSerialIsLoaded(5)
413 self.checkRPZStats(5, 3, 2, self._xfrDone)
414 self.checkNotBlocked('a.example.')
415 self.checkNotBlocked('b.example.')
416 self.checkNotBlocked('c.example.')
417 self.checkBlocked('d.example.')
418
419 # sixth zone, only e should be blocked, f is a local data record
420 self.waitUntilCorrectSerialIsLoaded(6)
421 self.checkRPZStats(6, 2, 2, self._xfrDone)
422 self.checkNotBlocked('a.example.')
423 self.checkNotBlocked('b.example.')
424 self.checkNotBlocked('c.example.')
425 self.checkNotBlocked('d.example.')
426 self.checkCustom('e.example.', 'A', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.1', '192.0.2.2'))
427 self.checkCustom('e.example.', 'MX', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'MX', '10 mx.example.'))
428 self.checkNoData('e.example.', 'AAAA')
429 self.checkCustom('f.example.', 'A', dns.rrset.from_text('f.example.', 0, dns.rdataclass.IN, 'CNAME', 'e.example.'))
430
431 # seventh zone, e should only have one A
432 self.waitUntilCorrectSerialIsLoaded(7)
433 self.checkRPZStats(7, 4, 2, self._xfrDone)
434 self.checkNotBlocked('a.example.')
435 self.checkNotBlocked('b.example.')
436 self.checkNotBlocked('c.example.')
437 self.checkNotBlocked('d.example.')
438 self.checkCustom('e.example.', 'A', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.2'))
439 self.checkCustom('e.example.', 'MX', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'MX', '10 mx.example.'))
440 self.checkNoData('e.example.', 'AAAA')
441 self.checkCustom('f.example.', 'A', dns.rrset.from_text('f.example.', 0, dns.rdataclass.IN, 'CNAME', 'e.example.'))
442 # check that the policy is disabled for AD=1 queries
443 self.checkNotBlocked('e.example.', True)
444 # check non-custom policies
445 self.checkTruncated('tc.example.')
446 self.checkDropped('drop.example.')
447
448 # eighth zone, all entries should be gone
449 self.waitUntilCorrectSerialIsLoaded(8)
450 self.checkRPZStats(8, 0, 3, self._xfrDone)
451 self.checkNotBlocked('a.example.')
452 self.checkNotBlocked('b.example.')
453 self.checkNotBlocked('c.example.')
454 self.checkNotBlocked('d.example.')
455 self.checkNotBlocked('e.example.')
456 self.checkNXD('f.example.')
457 self.checkNXD('tc.example.')
458 self.checkNXD('drop.example.')
459
460 class RPZFileRecursorTest(RPZRecursorTest):
461 """
462 This test makes sure that we correctly load RPZ zones from a file
463 """
464
465 _confdir = 'RPZFile'
466 _lua_config_file = """
467 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz." })
468 """ % (_confdir)
469 _config_template = """
470 auth-zones=example=configs/%s/example.zone
471 """ % (_confdir)
472
473 @classmethod
474 def generateRecursorConfig(cls, confdir):
475 authzonepath = os.path.join(confdir, 'example.zone')
476 with open(authzonepath, 'w') as authzone:
477 authzone.write("""$ORIGIN example.
478 @ 3600 IN SOA {soa}
479 a 3600 IN A 192.0.2.42
480 b 3600 IN A 192.0.2.42
481 c 3600 IN A 192.0.2.42
482 d 3600 IN A 192.0.2.42
483 e 3600 IN A 192.0.2.42
484 z 3600 IN A 192.0.2.42
485 """.format(soa=cls._SOA))
486
487 rpzFilePath = os.path.join(confdir, 'zone.rpz')
488 with open(rpzFilePath, 'w') as rpzZone:
489 rpzZone.write("""$ORIGIN zone.rpz.
490 @ 3600 IN SOA {soa}
491 a.example.zone.rpz. 60 IN A 192.0.2.42
492 a.example.zone.rpz. 60 IN A 192.0.2.43
493 a.example.zone.rpz. 60 IN TXT "some text"
494 drop.example.zone.rpz. 60 IN CNAME rpz-drop.
495 z.example.zone.rpz. 60 IN A 192.0.2.1
496 tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
497 """.format(soa=cls._SOA))
498 super(RPZFileRecursorTest, cls).generateRecursorConfig(confdir)
499
500 def testRPZ(self):
501 self.checkCustom('a.example.', 'A', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.42', '192.0.2.43'))
502 self.checkCustom('a.example.', 'TXT', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'TXT', '"some text"'))
503 self.checkBlocked('z.example.')
504 self.checkNotBlocked('b.example.')
505 self.checkNotBlocked('c.example.')
506 self.checkNotBlocked('d.example.')
507 self.checkNotBlocked('e.example.')
508 # check that the policy is disabled for AD=1 queries
509 self.checkNotBlocked('z.example.', True)
510 # check non-custom policies
511 self.checkTruncated('tc.example.')
512 self.checkDropped('drop.example.')
513
514 class RPZFileDefaultPolRecursorTest(RPZRecursorTest):
515 """
516 This test makes sure that we correctly load RPZ zones from a file with a default policy
517 """
518
519 _confdir = 'RPZFileDefaultPolicy'
520 _lua_config_file = """
521 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction })
522 """ % (_confdir)
523 _config_template = """
524 auth-zones=example=configs/%s/example.zone
525 """ % (_confdir)
526
527 @classmethod
528 def generateRecursorConfig(cls, confdir):
529 authzonepath = os.path.join(confdir, 'example.zone')
530 with open(authzonepath, 'w') as authzone:
531 authzone.write("""$ORIGIN example.
532 @ 3600 IN SOA {soa}
533 a 3600 IN A 192.0.2.42
534 b 3600 IN A 192.0.2.42
535 c 3600 IN A 192.0.2.42
536 d 3600 IN A 192.0.2.42
537 drop 3600 IN A 192.0.2.42
538 e 3600 IN A 192.0.2.42
539 z 3600 IN A 192.0.2.42
540 """.format(soa=cls._SOA))
541
542 rpzFilePath = os.path.join(confdir, 'zone.rpz')
543 with open(rpzFilePath, 'w') as rpzZone:
544 rpzZone.write("""$ORIGIN zone.rpz.
545 @ 3600 IN SOA {soa}
546 a.example.zone.rpz. 60 IN A 192.0.2.42
547 drop.example.zone.rpz. 60 IN CNAME rpz-drop.
548 z.example.zone.rpz. 60 IN A 192.0.2.1
549 tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
550 """.format(soa=cls._SOA))
551 super(RPZFileDefaultPolRecursorTest, cls).generateRecursorConfig(confdir)
552
553 def testRPZ(self):
554 # local data entries are overridden by default
555 self.checkCustom('a.example.', 'A', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.42'))
556 self.checkNoData('a.example.', 'TXT')
557 # will not be blocked because the default policy overrides local data entries by default
558 self.checkNotBlocked('z.example.')
559 self.checkNotBlocked('b.example.')
560 self.checkNotBlocked('c.example.')
561 self.checkNotBlocked('d.example.')
562 self.checkNotBlocked('e.example.')
563 # check non-local policies, they should be overridden by the default policy
564 self.checkNXD('tc.example.', 'A')
565 self.checkNotBlocked('drop.example.')
566
567 class RPZFileDefaultPolNotOverrideLocalRecursorTest(RPZRecursorTest):
568 """
569 This test makes sure that we correctly load RPZ zones from a file with a default policy, not overriding local data entries
570 """
571
572 _confdir = 'RPZFileDefaultPolicyNotOverrideLocal'
573 _lua_config_file = """
574 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction, defpolOverrideLocalData=false })
575 """ % (_confdir)
576 _config_template = """
577 auth-zones=example=configs/%s/example.zone
578 """ % (_confdir)
579
580 @classmethod
581 def generateRecursorConfig(cls, confdir):
582 authzonepath = os.path.join(confdir, 'example.zone')
583 with open(authzonepath, 'w') as authzone:
584 authzone.write("""$ORIGIN example.
585 @ 3600 IN SOA {soa}
586 a 3600 IN A 192.0.2.42
587 b 3600 IN A 192.0.2.42
588 c 3600 IN A 192.0.2.42
589 d 3600 IN A 192.0.2.42
590 drop 3600 IN A 192.0.2.42
591 e 3600 IN A 192.0.2.42
592 z 3600 IN A 192.0.2.42
593 """.format(soa=cls._SOA))
594
595 rpzFilePath = os.path.join(confdir, 'zone.rpz')
596 with open(rpzFilePath, 'w') as rpzZone:
597 rpzZone.write("""$ORIGIN zone.rpz.
598 @ 3600 IN SOA {soa}
599 a.example.zone.rpz. 60 IN A 192.0.2.42
600 a.example.zone.rpz. 60 IN A 192.0.2.43
601 a.example.zone.rpz. 60 IN TXT "some text"
602 drop.example.zone.rpz. 60 IN CNAME rpz-drop.
603 z.example.zone.rpz. 60 IN A 192.0.2.1
604 tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
605 """.format(soa=cls._SOA))
606 super(RPZFileDefaultPolNotOverrideLocalRecursorTest, cls).generateRecursorConfig(confdir)
607
608 def testRPZ(self):
609 # local data entries will not be overridden by the default polic
610 self.checkCustom('a.example.', 'A', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.42', '192.0.2.43'))
611 self.checkCustom('a.example.', 'TXT', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'TXT', '"some text"'))
612 # will be blocked because the default policy does not override local data entries
613 self.checkBlocked('z.example.')
614 self.checkNotBlocked('b.example.')
615 self.checkNotBlocked('c.example.')
616 self.checkNotBlocked('d.example.')
617 self.checkNotBlocked('e.example.')
618 # check non-local policies, they should be overridden by the default policy
619 self.checkNXD('tc.example.', 'A')
620 self.checkNotBlocked('drop.example.')
621
622 class RPZSimpleAuthServer(object):
623
624 def __init__(self, port):
625 self._serverPort = port
626 listener = threading.Thread(name='RPZ Simple Auth Listener', target=self._listener, args=[])
627 listener.setDaemon(True)
628 listener.start()
629
630 def _getAnswer(self, message):
631
632 response = dns.message.make_response(message)
633 response.flags |= dns.flags.AA
634 records = [
635 dns.rrset.from_text('nsip.delegated.example.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.42')
636 ]
637
638 response.answer = records
639 return response
640
641 def _listener(self):
642 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
643 try:
644 sock.bind(("127.0.0.1", self._serverPort))
645 except socket.error as e:
646 print("Error binding in the RPZ simple auth listener: %s" % str(e))
647 sys.exit(1)
648
649 while True:
650 try:
651 data, addr = sock.recvfrom(4096)
652 message = dns.message.from_wire(data)
653 if len(message.question) != 1:
654 print('Invalid query, qdcount is %d' % (len(message.question)))
655 break
656
657 answer = self._getAnswer(message)
658 if not answer:
659 print('Unable to get a response for %s %d' % (message.question[0].name, message.question[0].rdtype))
660 break
661
662 wire = answer.to_wire()
663 sock.sendto(wire, addr)
664
665 except socket.error as e:
666 print('Error in RPZ simple auth socket: %s' % str(e))
667
668 rpzAuthServerPort = 4260
669 rpzAuthServer = RPZSimpleAuthServer(rpzAuthServerPort)
670
671 class RPZOrderingPrecedenceRecursorTest(RPZRecursorTest):
672 """
673 This test makes sure that the recursor respects the RPZ ordering precedence rules
674 """
675
676 _confdir = 'RPZOrderingPrecedence'
677 _lua_config_file = """
678 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."})
679 rpzFile('configs/%s/zone2.rpz', { policyName="zone2.rpz."})
680 """ % (_confdir, _confdir)
681 _config_template = """
682 auth-zones=example=configs/%s/example.zone
683 forward-zones=delegated.example=127.0.0.1:%d
684 """ % (_confdir, rpzAuthServerPort)
685
686 @classmethod
687 def generateRecursorConfig(cls, confdir):
688 authzonepath = os.path.join(confdir, 'example.zone')
689 with open(authzonepath, 'w') as authzone:
690 authzone.write("""$ORIGIN example.
691 @ 3600 IN SOA {soa}
692 sub.test 3600 IN A 192.0.2.42
693 passthru-then-blocked-by-higher 3600 IN A 192.0.2.66
694 passthru-then-blocked-by-same 3600 IN A 192.0.2.66
695 blocked-then-passhtru-by-higher 3600 IN A 192.0.2.100
696 """.format(soa=cls._SOA))
697
698 rpzFilePath = os.path.join(confdir, 'zone.rpz')
699 with open(rpzFilePath, 'w') as rpzZone:
700 rpzZone.write("""$ORIGIN zone.rpz.
701 @ 3600 IN SOA {soa}
702 *.test.example.zone.rpz. 60 IN CNAME rpz-passthru.
703 32.66.2.0.192.rpz-ip.zone.rpz. 60 IN A 192.0.2.1
704 32.100.2.0.192.rpz-ip.zone.rpz. 60 IN CNAME rpz-passthru.
705 passthru-then-blocked-by-same.example.zone.rpz. 60 IN CNAME rpz-passthru.
706 32.1.0.0.127.rpz-nsip.zone.rpz. 60 IN CNAME rpz-passthru.
707 """.format(soa=cls._SOA))
708
709 rpzFilePath = os.path.join(confdir, 'zone2.rpz')
710 with open(rpzFilePath, 'w') as rpzZone:
711 rpzZone.write("""$ORIGIN zone2.rpz.
712 @ 3600 IN SOA {soa}
713 sub.test.example.com.zone2.rpz. 60 IN CNAME .
714 passthru-then-blocked-by-higher.example.zone2.rpz. 60 IN CNAME rpz-passthru.
715 blocked-then-passhtru-by-higher.example.zone2.rpz. 60 IN A 192.0.2.1
716 32.42.2.0.192.rpz-ip 60 IN CNAME .
717 """.format(soa=cls._SOA))
718
719 super(RPZOrderingPrecedenceRecursorTest, cls).generateRecursorConfig(confdir)
720
721 def testRPZOrderingForQNameAndWhitelisting(self):
722 # we should first match on the qname (the wildcard, not on the exact name since
723 # we respect the order of the RPZ zones), see the pass-thru rule
724 # and only process RPZ rules of higher precedence.
725 # The subsequent rule on the content of the A should therefore not trigger a NXDOMAIN.
726 self.checkNotBlocked('sub.test.example.')
727
728 def testRPZOrderingWhitelistedThenBlockedByHigher(self):
729 # we should first match on the qname from the second RPZ zone,
730 # continue the resolution process, and get blocked by the content of the A record
731 # based on the first RPZ zone, whose priority is higher than the second one.
732 self.checkBlocked('passthru-then-blocked-by-higher.example.')
733
734 def testRPZOrderingWhitelistedThenBlockedBySame(self):
735 # we should first match on the qname from the first RPZ zone,
736 # continue the resolution process, and NOT get blocked by the content of the A record
737 # based on the same RPZ zone, since it's not higher.
738 self.checkCustom('passthru-then-blocked-by-same.example.', 'A', dns.rrset.from_text('passthru-then-blocked-by-same.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.66'))
739
740 def testRPZOrderBlockedThenWhitelisted(self):
741 # The qname is first blocked by the second RPZ zone
742 # Then, should the resolution process go on, the A record would be whitelisted
743 # by the first zone.
744 # This is what the RPZ specification requires, but we currently decided that we
745 # don't want to leak queries to malicious DNS servers and waste time if the qname is blacklisted.
746 # We might change our opinion at some point, though.
747 self.checkBlocked('blocked-then-passhtru-by-higher.example.')
748
749 def testRPZOrderDelegate(self):
750 # The IP of the NS we are going to contact is whitelisted (passthru) in zone 1,
751 # so even though the record (192.0.2.42) returned by the server is blacklisted
752 # by zone 2, it should not be blocked.
753 # We only test once because after that the answer is cached, so the NS is not contacted
754 # and the whitelist is not applied (yes, NSIP and NSDNAME are brittle).
755 self.checkNotBlocked('nsip.delegated.example.', singleCheck=True)
756
757 class RPZNSIPCustomTest(RPZRecursorTest):
758 """
759 This test makes sure that the recursor handles custom RPZ rules in a NSIP
760 """
761
762 _confdir = 'RPZNSIPCustom'
763 _lua_config_file = """
764 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."})
765 rpzFile('configs/%s/zone2.rpz', { policyName="zone2.rpz."})
766 """ % (_confdir, _confdir)
767 _config_template = """
768 auth-zones=example=configs/%s/example.zone
769 forward-zones=delegated.example=127.0.0.1:%d
770 """ % (_confdir, rpzAuthServerPort)
771
772 @classmethod
773 def generateRecursorConfig(cls, confdir):
774 authzonepath = os.path.join(confdir, 'example.zone')
775 with open(authzonepath, 'w') as authzone:
776 authzone.write("""$ORIGIN example.
777 @ 3600 IN SOA {soa}
778 """.format(soa=cls._SOA))
779
780 rpzFilePath = os.path.join(confdir, 'zone.rpz')
781 with open(rpzFilePath, 'w') as rpzZone:
782 rpzZone.write("""$ORIGIN zone.rpz.
783 @ 3600 IN SOA {soa}
784 32.1.0.0.127.rpz-nsip.zone.rpz. 60 IN A 192.0.2.1
785 """.format(soa=cls._SOA))
786
787 rpzFilePath = os.path.join(confdir, 'zone2.rpz')
788 with open(rpzFilePath, 'w') as rpzZone:
789 rpzZone.write("""$ORIGIN zone2.rpz.
790 @ 3600 IN SOA {soa}
791 32.1.2.0.192.rpz-ip 60 IN CNAME .
792 """.format(soa=cls._SOA))
793
794 super(RPZNSIPCustomTest, cls).generateRecursorConfig(confdir)
795
796 def testRPZDelegate(self):
797 # The IP of the NS we are going to contact should result in a custom record (192.0.2.1) from zone 1,
798 # so even though the record (192.0.2.1) returned by the server is blacklisted
799 # by zone 2, it should not be blocked.
800 # We only test once because after that the answer is cached, so the NS is not contacted
801 # and the whitelist is not applied (yes, NSIP and NSDNAME are brittle).
802 self.checkCustom('nsip.delegated.example.', 'A', dns.rrset.from_text('nsip.delegated.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.1'))