]> git.ipfire.org Git - thirdparty/pdns.git/blob - regression-tests.recursor-dnssec/test_RPZ.py
Merge pull request #9114 from pieterlexis/rec-may-2020
[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 # special case for the 9th update, which might get skipped
56 if oldSerial != self._currentSerial and self._currentSerial != 9:
57 print('Received an IXFR query with an unexpected serial %d, expected %d' % (oldSerial, self._currentSerial))
58 return (None, self._currentSerial)
59
60 newSerial = self._targetSerial
61 if newSerial == 2:
62 records = [
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' % newSerial),
64 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),
65 # no deletion
66 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),
67 dns.rrset.from_text('b.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'),
68 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)
69 ]
70 elif newSerial == 3:
71 records = [
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' % newSerial),
73 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),
74 dns.rrset.from_text('a.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'),
75 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),
76 # no addition
77 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)
78 ]
79 elif newSerial == 4:
80 records = [
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' % newSerial),
82 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),
83 dns.rrset.from_text('b.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'),
84 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),
85 dns.rrset.from_text('c.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'),
86 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)
87 ]
88 elif newSerial == 5:
89 # this one is a bit special, we are answering with a full AXFR
90 records = [
91 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),
92 dns.rrset.from_text('d.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'),
93 dns.rrset.from_text('tc.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-tcp-only.'),
94 dns.rrset.from_text('drop.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-drop.'),
95 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)
96 ]
97 elif newSerial == 6:
98 # back to IXFR
99 records = [
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' % newSerial),
101 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),
102 dns.rrset.from_text('d.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'),
103 dns.rrset.from_text('tc.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-tcp-only.'),
104 dns.rrset.from_text('drop.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-drop.'),
105 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),
106 dns.rrset.from_text('e.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1', '192.0.2.2'),
107 dns.rrset.from_text('e.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.MX, '10 mx.example.'),
108 dns.rrset.from_text('f.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'e.example.'),
109 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)
110 ]
111 elif newSerial == 7:
112 records = [
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' % newSerial),
114 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),
115 dns.rrset.from_text('e.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1', '192.0.2.2'),
116 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),
117 dns.rrset.from_text('e.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.2'),
118 dns.rrset.from_text('tc.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-tcp-only.'),
119 dns.rrset.from_text('drop.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-drop.'),
120 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)
121 ]
122 elif newSerial == 8:
123 # this one is a bit special too, we are answering with a full AXFR and the new zone is empty
124 records = [
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 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)
127 ]
128 elif newSerial == 9:
129 # IXFR inserting a duplicate, we should not crash and skip it
130 records = [
131 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),
132 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),
133 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),
134 dns.rrset.from_text('dup.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-passthru.'),
135 dns.rrset.from_text('dup.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-passthru.'),
136 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)
137 ]
138 elif newSerial == 10:
139 # full AXFR to make sure we are removing the duplicate, adding a record, to the that the update was correctly applied
140 records = [
141 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),
142 dns.rrset.from_text('f.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'),
143 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)
144 ]
145
146 response.answer = records
147 return (newSerial, response)
148
149 def _connectionHandler(self, conn):
150 data = None
151 while True:
152 data = conn.recv(2)
153 if not data:
154 break
155 (datalen,) = struct.unpack("!H", data)
156 data = conn.recv(datalen)
157 if not data:
158 break
159
160 message = dns.message.from_wire(data)
161 if len(message.question) != 1:
162 print('Invalid RPZ query, qdcount is %d' % (len(message.question)))
163 break
164 if not message.question[0].rdtype in [dns.rdatatype.AXFR, dns.rdatatype.IXFR]:
165 print('Invalid RPZ query, qtype is %d' % (message.question.rdtype))
166 break
167 (serial, answer) = self._getAnswer(message)
168 if not answer:
169 print('Unable to get a response for %s %d' % (message.question[0].name, message.question[0].rdtype))
170 break
171
172 wire = answer.to_wire()
173 conn.send(struct.pack("!H", len(wire)))
174 conn.send(wire)
175 self._currentSerial = serial
176 break
177
178 conn.close()
179
180 def _listener(self):
181 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
182 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
183 try:
184 sock.bind(("127.0.0.1", self._serverPort))
185 except socket.error as e:
186 print("Error binding in the RPZ listener: %s" % str(e))
187 sys.exit(1)
188
189 sock.listen(100)
190 while True:
191 try:
192 (conn, _) = sock.accept()
193 thread = threading.Thread(name='RPZ Connection Handler',
194 target=self._connectionHandler,
195 args=[conn])
196 thread.setDaemon(True)
197 thread.start()
198
199 except socket.error as e:
200 print('Error in RPZ socket: %s' % str(e))
201 sock.close()
202
203 class RPZRecursorTest(RecursorTest):
204 _wsPort = 8042
205 _wsTimeout = 2
206 _wsPassword = 'secretpassword'
207 _apiKey = 'secretapikey'
208 _confdir = 'RPZ'
209 _lua_dns_script_file = """
210
211 function prerpz(dq)
212 -- disable the RPZ policy named 'zone.rpz' for AD=1 queries
213 if dq:getDH():getAD() then
214 dq:discardPolicy('zone.rpz.')
215 end
216 return false
217 end
218 """
219
220 _config_template = """
221 auth-zones=example=configs/%s/example.zone
222 webserver=yes
223 webserver-port=%d
224 webserver-address=127.0.0.1
225 webserver-password=%s
226 api-key=%s
227 log-rpz-changes=yes
228 """ % (_confdir, _wsPort, _wsPassword, _apiKey)
229
230 @classmethod
231 def setUpClass(cls):
232
233 cls.setUpSockets()
234 cls.startResponders()
235
236 confdir = os.path.join('configs', cls._confdir)
237 cls.createConfigDir(confdir)
238
239 cls.generateRecursorConfig(confdir)
240 cls.startRecursor(confdir, cls._recursorPort)
241
242 @classmethod
243 def tearDownClass(cls):
244 cls.tearDownRecursor()
245
246 def checkBlocked(self, name, shouldBeBlocked=True, adQuery=False, singleCheck=False):
247 query = dns.message.make_query(name, 'A', want_dnssec=True)
248 query.flags |= dns.flags.CD
249 if adQuery:
250 query.flags |= dns.flags.AD
251
252 for method in ("sendUDPQuery", "sendTCPQuery"):
253 sender = getattr(self, method)
254 res = sender(query)
255 self.assertRcodeEqual(res, dns.rcode.NOERROR)
256 if shouldBeBlocked:
257 expected = dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.1')
258 else:
259 expected = dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.42')
260
261 self.assertRRsetInAnswer(res, expected)
262 if singleCheck:
263 break
264
265 def checkNotBlocked(self, name, adQuery=False, singleCheck=False):
266 self.checkBlocked(name, False, adQuery, singleCheck)
267
268 def checkCustom(self, qname, qtype, expected):
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.NOERROR)
275 self.assertRRsetInAnswer(res, expected)
276
277 def checkNoData(self, qname, qtype):
278 query = dns.message.make_query(qname, qtype, want_dnssec=True)
279 query.flags |= dns.flags.CD
280 for method in ("sendUDPQuery", "sendTCPQuery"):
281 sender = getattr(self, method)
282 res = sender(query)
283 self.assertRcodeEqual(res, dns.rcode.NOERROR)
284 self.assertEqual(len(res.answer), 0)
285
286 def checkNXD(self, qname, qtype='A'):
287 query = dns.message.make_query(qname, qtype, want_dnssec=True)
288 query.flags |= dns.flags.CD
289 for method in ("sendUDPQuery", "sendTCPQuery"):
290 sender = getattr(self, method)
291 res = sender(query)
292 self.assertRcodeEqual(res, dns.rcode.NXDOMAIN)
293 self.assertEqual(len(res.answer), 0)
294 self.assertEqual(len(res.authority), 1)
295
296 def checkTruncated(self, qname, qtype='A'):
297 query = dns.message.make_query(qname, qtype, want_dnssec=True)
298 query.flags |= dns.flags.CD
299 res = self.sendUDPQuery(query)
300 self.assertRcodeEqual(res, dns.rcode.NOERROR)
301 self.assertMessageHasFlags(res, ['QR', 'RA', 'RD', 'CD', 'TC'])
302 self.assertEqual(len(res.answer), 0)
303 self.assertEqual(len(res.authority), 0)
304 self.assertEqual(len(res.additional), 0)
305
306 res = self.sendTCPQuery(query)
307 self.assertRcodeEqual(res, dns.rcode.NXDOMAIN)
308 self.assertMessageHasFlags(res, ['QR', 'RA', 'RD', 'CD'])
309 self.assertEqual(len(res.answer), 0)
310 self.assertEqual(len(res.authority), 1)
311 self.assertEqual(len(res.additional), 0)
312
313 def checkDropped(self, qname, qtype='A'):
314 query = dns.message.make_query(qname, qtype, want_dnssec=True)
315 query.flags |= dns.flags.CD
316 for method in ("sendUDPQuery", "sendTCPQuery"):
317 sender = getattr(self, method)
318 res = sender(query)
319 self.assertEqual(res, None)
320
321 def checkRPZStats(self, serial, recordsCount, fullXFRCount, totalXFRCount):
322 headers = {'x-api-key': self._apiKey}
323 url = 'http://127.0.0.1:' + str(self._wsPort) + '/api/v1/servers/localhost/rpzstatistics'
324 r = requests.get(url, headers=headers, timeout=self._wsTimeout)
325 self.assertTrue(r)
326 self.assertEquals(r.status_code, 200)
327 self.assertTrue(r.json())
328 content = r.json()
329 self.assertIn('zone.rpz.', content)
330 zone = content['zone.rpz.']
331 for key in ['last_update', 'records', 'serial', 'transfers_failed', 'transfers_full', 'transfers_success']:
332 self.assertIn(key, zone)
333
334 self.assertEquals(zone['serial'], serial)
335 self.assertEquals(zone['records'], recordsCount)
336 self.assertEquals(zone['transfers_full'], fullXFRCount)
337 self.assertEquals(zone['transfers_success'], totalXFRCount)
338
339 rpzServerPort = 4250
340 rpzServer = RPZServer(rpzServerPort)
341
342 class RPZXFRRecursorTest(RPZRecursorTest):
343 """
344 This test makes sure that we correctly update RPZ zones via AXFR then IXFR
345 """
346
347 global rpzServerPort
348 _lua_config_file = """
349 -- The first server is a bogus one, to test that we correctly fail over to the second one
350 rpzMaster({'127.0.0.1:9999', '127.0.0.1:%d'}, 'zone.rpz.', { refresh=1 })
351 """ % (rpzServerPort)
352 _confdir = 'RPZXFR'
353 _wsPort = 8042
354 _wsTimeout = 2
355 _wsPassword = 'secretpassword'
356 _apiKey = 'secretapikey'
357 _config_template = """
358 auth-zones=example=configs/%s/example.zone
359 webserver=yes
360 webserver-port=%d
361 webserver-address=127.0.0.1
362 webserver-password=%s
363 api-key=%s
364 """ % (_confdir, _wsPort, _wsPassword, _apiKey)
365 _xfrDone = 0
366
367 @classmethod
368 def generateRecursorConfig(cls, confdir):
369 authzonepath = os.path.join(confdir, 'example.zone')
370 with open(authzonepath, 'w') as authzone:
371 authzone.write("""$ORIGIN example.
372 @ 3600 IN SOA {soa}
373 a 3600 IN A 192.0.2.42
374 b 3600 IN A 192.0.2.42
375 c 3600 IN A 192.0.2.42
376 d 3600 IN A 192.0.2.42
377 e 3600 IN A 192.0.2.42
378 """.format(soa=cls._SOA))
379 super(RPZRecursorTest, cls).generateRecursorConfig(confdir)
380
381 def waitUntilCorrectSerialIsLoaded(self, serial, timeout=5):
382 global rpzServer
383
384 rpzServer.moveToSerial(serial)
385
386 attempts = 0
387 while attempts < timeout:
388 currentSerial = rpzServer.getCurrentSerial()
389 if currentSerial > serial:
390 raise AssertionError("Expected serial %d, got %d" % (serial, currentSerial))
391 if currentSerial == serial:
392 self._xfrDone = self._xfrDone + 1
393 return
394
395 attempts = attempts + 1
396 time.sleep(1)
397
398 raise AssertionError("Waited %d seconds for the serial to be updated to %d but the serial is still %d" % (timeout, serial, currentSerial))
399
400 def testRPZ(self):
401 # first zone, only a should be blocked
402 self.waitUntilCorrectSerialIsLoaded(1)
403 self.checkRPZStats(1, 1, 1, self._xfrDone)
404 self.checkBlocked('a.example.')
405 self.checkNotBlocked('b.example.')
406 self.checkNotBlocked('c.example.')
407
408 # second zone, a and b should be blocked
409 self.waitUntilCorrectSerialIsLoaded(2)
410 self.checkRPZStats(2, 2, 1, self._xfrDone)
411 self.checkBlocked('a.example.')
412 self.checkBlocked('b.example.')
413 self.checkNotBlocked('c.example.')
414
415 # third zone, only b should be blocked
416 self.waitUntilCorrectSerialIsLoaded(3)
417 self.checkRPZStats(3, 1, 1, self._xfrDone)
418 self.checkNotBlocked('a.example.')
419 self.checkBlocked('b.example.')
420 self.checkNotBlocked('c.example.')
421
422 # fourth zone, only c should be blocked
423 self.waitUntilCorrectSerialIsLoaded(4)
424 self.checkRPZStats(4, 1, 1, self._xfrDone)
425 self.checkNotBlocked('a.example.')
426 self.checkNotBlocked('b.example.')
427 self.checkBlocked('c.example.')
428
429 # fifth zone, we should get a full AXFR this time, and only d should be blocked
430 self.waitUntilCorrectSerialIsLoaded(5)
431 self.checkRPZStats(5, 3, 2, self._xfrDone)
432 self.checkNotBlocked('a.example.')
433 self.checkNotBlocked('b.example.')
434 self.checkNotBlocked('c.example.')
435 self.checkBlocked('d.example.')
436
437 # sixth zone, only e should be blocked, f is a local data record
438 self.waitUntilCorrectSerialIsLoaded(6)
439 self.checkRPZStats(6, 2, 2, self._xfrDone)
440 self.checkNotBlocked('a.example.')
441 self.checkNotBlocked('b.example.')
442 self.checkNotBlocked('c.example.')
443 self.checkNotBlocked('d.example.')
444 self.checkCustom('e.example.', 'A', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.1', '192.0.2.2'))
445 self.checkCustom('e.example.', 'MX', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'MX', '10 mx.example.'))
446 self.checkNoData('e.example.', 'AAAA')
447 self.checkCustom('f.example.', 'A', dns.rrset.from_text('f.example.', 0, dns.rdataclass.IN, 'CNAME', 'e.example.'))
448
449 # seventh zone, e should only have one A
450 self.waitUntilCorrectSerialIsLoaded(7)
451 self.checkRPZStats(7, 4, 2, self._xfrDone)
452 self.checkNotBlocked('a.example.')
453 self.checkNotBlocked('b.example.')
454 self.checkNotBlocked('c.example.')
455 self.checkNotBlocked('d.example.')
456 self.checkCustom('e.example.', 'A', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.2'))
457 self.checkCustom('e.example.', 'MX', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'MX', '10 mx.example.'))
458 self.checkNoData('e.example.', 'AAAA')
459 self.checkCustom('f.example.', 'A', dns.rrset.from_text('f.example.', 0, dns.rdataclass.IN, 'CNAME', 'e.example.'))
460 # check that the policy is disabled for AD=1 queries
461 self.checkNotBlocked('e.example.', True)
462 # check non-custom policies
463 self.checkTruncated('tc.example.')
464 self.checkDropped('drop.example.')
465
466 # eighth zone, all entries should be gone
467 self.waitUntilCorrectSerialIsLoaded(8)
468 self.checkRPZStats(8, 0, 3, self._xfrDone)
469 self.checkNotBlocked('a.example.')
470 self.checkNotBlocked('b.example.')
471 self.checkNotBlocked('c.example.')
472 self.checkNotBlocked('d.example.')
473 self.checkNotBlocked('e.example.')
474 self.checkNXD('f.example.')
475 self.checkNXD('tc.example.')
476 self.checkNXD('drop.example.')
477
478 # 9th zone is a duplicate, it might get skipped
479 global rpzServer
480 rpzServer.moveToSerial(9)
481 time.sleep(3)
482 self.waitUntilCorrectSerialIsLoaded(10)
483 self.checkRPZStats(10, 1, 4, self._xfrDone)
484 self.checkNotBlocked('a.example.')
485 self.checkNotBlocked('b.example.')
486 self.checkNotBlocked('c.example.')
487 self.checkNotBlocked('d.example.')
488 self.checkNotBlocked('e.example.')
489 self.checkBlocked('f.example.')
490 self.checkNXD('tc.example.')
491 self.checkNXD('drop.example.')
492
493 class RPZFileRecursorTest(RPZRecursorTest):
494 """
495 This test makes sure that we correctly load RPZ zones from a file
496 """
497
498 _confdir = 'RPZFile'
499 _lua_config_file = """
500 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz." })
501 """ % (_confdir)
502 _config_template = """
503 auth-zones=example=configs/%s/example.zone
504 """ % (_confdir)
505
506 @classmethod
507 def generateRecursorConfig(cls, confdir):
508 authzonepath = os.path.join(confdir, 'example.zone')
509 with open(authzonepath, 'w') as authzone:
510 authzone.write("""$ORIGIN example.
511 @ 3600 IN SOA {soa}
512 a 3600 IN A 192.0.2.42
513 b 3600 IN A 192.0.2.42
514 c 3600 IN A 192.0.2.42
515 d 3600 IN A 192.0.2.42
516 e 3600 IN A 192.0.2.42
517 z 3600 IN A 192.0.2.42
518 """.format(soa=cls._SOA))
519
520 rpzFilePath = os.path.join(confdir, 'zone.rpz')
521 with open(rpzFilePath, 'w') as rpzZone:
522 rpzZone.write("""$ORIGIN zone.rpz.
523 @ 3600 IN SOA {soa}
524 a.example.zone.rpz. 60 IN A 192.0.2.42
525 a.example.zone.rpz. 60 IN A 192.0.2.43
526 a.example.zone.rpz. 60 IN TXT "some text"
527 drop.example.zone.rpz. 60 IN CNAME rpz-drop.
528 z.example.zone.rpz. 60 IN A 192.0.2.1
529 tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
530 """.format(soa=cls._SOA))
531 super(RPZFileRecursorTest, cls).generateRecursorConfig(confdir)
532
533 def testRPZ(self):
534 self.checkCustom('a.example.', 'A', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.42', '192.0.2.43'))
535 self.checkCustom('a.example.', 'TXT', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'TXT', '"some text"'))
536 self.checkBlocked('z.example.')
537 self.checkNotBlocked('b.example.')
538 self.checkNotBlocked('c.example.')
539 self.checkNotBlocked('d.example.')
540 self.checkNotBlocked('e.example.')
541 # check that the policy is disabled for AD=1 queries
542 self.checkNotBlocked('z.example.', True)
543 # check non-custom policies
544 self.checkTruncated('tc.example.')
545 self.checkDropped('drop.example.')
546
547 class RPZFileDefaultPolRecursorTest(RPZRecursorTest):
548 """
549 This test makes sure that we correctly load RPZ zones from a file with a default policy
550 """
551
552 _confdir = 'RPZFileDefaultPolicy'
553 _lua_config_file = """
554 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction })
555 """ % (_confdir)
556 _config_template = """
557 auth-zones=example=configs/%s/example.zone
558 """ % (_confdir)
559
560 @classmethod
561 def generateRecursorConfig(cls, confdir):
562 authzonepath = os.path.join(confdir, 'example.zone')
563 with open(authzonepath, 'w') as authzone:
564 authzone.write("""$ORIGIN example.
565 @ 3600 IN SOA {soa}
566 a 3600 IN A 192.0.2.42
567 b 3600 IN A 192.0.2.42
568 c 3600 IN A 192.0.2.42
569 d 3600 IN A 192.0.2.42
570 drop 3600 IN A 192.0.2.42
571 e 3600 IN A 192.0.2.42
572 z 3600 IN A 192.0.2.42
573 """.format(soa=cls._SOA))
574
575 rpzFilePath = os.path.join(confdir, 'zone.rpz')
576 with open(rpzFilePath, 'w') as rpzZone:
577 rpzZone.write("""$ORIGIN zone.rpz.
578 @ 3600 IN SOA {soa}
579 a.example.zone.rpz. 60 IN A 192.0.2.42
580 drop.example.zone.rpz. 60 IN CNAME rpz-drop.
581 z.example.zone.rpz. 60 IN A 192.0.2.1
582 tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
583 """.format(soa=cls._SOA))
584 super(RPZFileDefaultPolRecursorTest, cls).generateRecursorConfig(confdir)
585
586 def testRPZ(self):
587 # local data entries are overridden by default
588 self.checkCustom('a.example.', 'A', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.42'))
589 self.checkNoData('a.example.', 'TXT')
590 # will not be blocked because the default policy overrides local data entries by default
591 self.checkNotBlocked('z.example.')
592 self.checkNotBlocked('b.example.')
593 self.checkNotBlocked('c.example.')
594 self.checkNotBlocked('d.example.')
595 self.checkNotBlocked('e.example.')
596 # check non-local policies, they should be overridden by the default policy
597 self.checkNXD('tc.example.', 'A')
598 self.checkNotBlocked('drop.example.')
599
600 class RPZFileDefaultPolNotOverrideLocalRecursorTest(RPZRecursorTest):
601 """
602 This test makes sure that we correctly load RPZ zones from a file with a default policy, not overriding local data entries
603 """
604
605 _confdir = 'RPZFileDefaultPolicyNotOverrideLocal'
606 _lua_config_file = """
607 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction, defpolOverrideLocalData=false })
608 """ % (_confdir)
609 _config_template = """
610 auth-zones=example=configs/%s/example.zone
611 """ % (_confdir)
612
613 @classmethod
614 def generateRecursorConfig(cls, confdir):
615 authzonepath = os.path.join(confdir, 'example.zone')
616 with open(authzonepath, 'w') as authzone:
617 authzone.write("""$ORIGIN example.
618 @ 3600 IN SOA {soa}
619 a 3600 IN A 192.0.2.42
620 b 3600 IN A 192.0.2.42
621 c 3600 IN A 192.0.2.42
622 d 3600 IN A 192.0.2.42
623 drop 3600 IN A 192.0.2.42
624 e 3600 IN A 192.0.2.42
625 z 3600 IN A 192.0.2.42
626 """.format(soa=cls._SOA))
627
628 rpzFilePath = os.path.join(confdir, 'zone.rpz')
629 with open(rpzFilePath, 'w') as rpzZone:
630 rpzZone.write("""$ORIGIN zone.rpz.
631 @ 3600 IN SOA {soa}
632 a.example.zone.rpz. 60 IN A 192.0.2.42
633 a.example.zone.rpz. 60 IN A 192.0.2.43
634 a.example.zone.rpz. 60 IN TXT "some text"
635 drop.example.zone.rpz. 60 IN CNAME rpz-drop.
636 z.example.zone.rpz. 60 IN A 192.0.2.1
637 tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
638 """.format(soa=cls._SOA))
639 super(RPZFileDefaultPolNotOverrideLocalRecursorTest, cls).generateRecursorConfig(confdir)
640
641 def testRPZ(self):
642 # local data entries will not be overridden by the default policy
643 self.checkCustom('a.example.', 'A', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.42', '192.0.2.43'))
644 self.checkCustom('a.example.', 'TXT', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'TXT', '"some text"'))
645 # will be blocked because the default policy does not override local data entries
646 self.checkBlocked('z.example.')
647 self.checkNotBlocked('b.example.')
648 self.checkNotBlocked('c.example.')
649 self.checkNotBlocked('d.example.')
650 self.checkNotBlocked('e.example.')
651 # check non-local policies, they should be overridden by the default policy
652 self.checkNXD('tc.example.', 'A')
653 self.checkNotBlocked('drop.example.')
654
655 class RPZSimpleAuthServer(object):
656
657 def __init__(self, port):
658 self._serverPort = port
659 listener = threading.Thread(name='RPZ Simple Auth Listener', target=self._listener, args=[])
660 listener.setDaemon(True)
661 listener.start()
662
663 def _getAnswer(self, message):
664
665 response = dns.message.make_response(message)
666 response.flags |= dns.flags.AA
667 records = [
668 dns.rrset.from_text('nsip.delegated.example.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.42')
669 ]
670
671 response.answer = records
672 return response
673
674 def _listener(self):
675 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
676 try:
677 sock.bind(("127.0.0.1", self._serverPort))
678 except socket.error as e:
679 print("Error binding in the RPZ simple auth listener: %s" % str(e))
680 sys.exit(1)
681
682 while True:
683 try:
684 data, addr = sock.recvfrom(4096)
685 message = dns.message.from_wire(data)
686 if len(message.question) != 1:
687 print('Invalid query, qdcount is %d' % (len(message.question)))
688 break
689
690 answer = self._getAnswer(message)
691 if not answer:
692 print('Unable to get a response for %s %d' % (message.question[0].name, message.question[0].rdtype))
693 break
694
695 wire = answer.to_wire()
696 sock.sendto(wire, addr)
697
698 except socket.error as e:
699 print('Error in RPZ simple auth socket: %s' % str(e))
700
701 rpzAuthServerPort = 4260
702 rpzAuthServer = RPZSimpleAuthServer(rpzAuthServerPort)
703
704 class RPZOrderingPrecedenceRecursorTest(RPZRecursorTest):
705 """
706 This test makes sure that the recursor respects the RPZ ordering precedence rules
707 """
708
709 _confdir = 'RPZOrderingPrecedence'
710 _lua_config_file = """
711 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."})
712 rpzFile('configs/%s/zone2.rpz', { policyName="zone2.rpz."})
713 """ % (_confdir, _confdir)
714 _config_template = """
715 auth-zones=example=configs/%s/example.zone
716 forward-zones=delegated.example=127.0.0.1:%d
717 """ % (_confdir, rpzAuthServerPort)
718
719 @classmethod
720 def generateRecursorConfig(cls, confdir):
721 authzonepath = os.path.join(confdir, 'example.zone')
722 with open(authzonepath, 'w') as authzone:
723 authzone.write("""$ORIGIN example.
724 @ 3600 IN SOA {soa}
725 sub.test 3600 IN A 192.0.2.42
726 passthru-then-blocked-by-higher 3600 IN A 192.0.2.66
727 passthru-then-blocked-by-same 3600 IN A 192.0.2.66
728 blocked-then-passhtru-by-higher 3600 IN A 192.0.2.100
729 """.format(soa=cls._SOA))
730
731 rpzFilePath = os.path.join(confdir, 'zone.rpz')
732 with open(rpzFilePath, 'w') as rpzZone:
733 rpzZone.write("""$ORIGIN zone.rpz.
734 @ 3600 IN SOA {soa}
735 *.test.example.zone.rpz. 60 IN CNAME rpz-passthru.
736 32.66.2.0.192.rpz-ip.zone.rpz. 60 IN A 192.0.2.1
737 32.100.2.0.192.rpz-ip.zone.rpz. 60 IN CNAME rpz-passthru.
738 passthru-then-blocked-by-same.example.zone.rpz. 60 IN CNAME rpz-passthru.
739 32.1.0.0.127.rpz-nsip.zone.rpz. 60 IN CNAME rpz-passthru.
740 """.format(soa=cls._SOA))
741
742 rpzFilePath = os.path.join(confdir, 'zone2.rpz')
743 with open(rpzFilePath, 'w') as rpzZone:
744 rpzZone.write("""$ORIGIN zone2.rpz.
745 @ 3600 IN SOA {soa}
746 sub.test.example.com.zone2.rpz. 60 IN CNAME .
747 passthru-then-blocked-by-higher.example.zone2.rpz. 60 IN CNAME rpz-passthru.
748 blocked-then-passhtru-by-higher.example.zone2.rpz. 60 IN A 192.0.2.1
749 32.42.2.0.192.rpz-ip 60 IN CNAME .
750 """.format(soa=cls._SOA))
751
752 super(RPZOrderingPrecedenceRecursorTest, cls).generateRecursorConfig(confdir)
753
754 def testRPZOrderingForQNameAndWhitelisting(self):
755 # we should first match on the qname (the wildcard, not on the exact name since
756 # we respect the order of the RPZ zones), see the pass-thru rule
757 # and only process RPZ rules of higher precedence.
758 # The subsequent rule on the content of the A should therefore not trigger a NXDOMAIN.
759 self.checkNotBlocked('sub.test.example.')
760
761 def testRPZOrderingWhitelistedThenBlockedByHigher(self):
762 # we should first match on the qname from the second RPZ zone,
763 # continue the resolution process, and get blocked by the content of the A record
764 # based on the first RPZ zone, whose priority is higher than the second one.
765 self.checkBlocked('passthru-then-blocked-by-higher.example.')
766
767 def testRPZOrderingWhitelistedThenBlockedBySame(self):
768 # we should first match on the qname from the first RPZ zone,
769 # continue the resolution process, and NOT get blocked by the content of the A record
770 # based on the same RPZ zone, since it's not higher.
771 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'))
772
773 def testRPZOrderBlockedThenWhitelisted(self):
774 # The qname is first blocked by the second RPZ zone
775 # Then, should the resolution process go on, the A record would be whitelisted
776 # by the first zone.
777 # This is what the RPZ specification requires, but we currently decided that we
778 # don't want to leak queries to malicious DNS servers and waste time if the qname is blacklisted.
779 # We might change our opinion at some point, though.
780 self.checkBlocked('blocked-then-passhtru-by-higher.example.')
781
782 def testRPZOrderDelegate(self):
783 # The IP of the NS we are going to contact is whitelisted (passthru) in zone 1,
784 # so even though the record (192.0.2.42) returned by the server is blacklisted
785 # by zone 2, it should not be blocked.
786 # We only test once because after that the answer is cached, so the NS is not contacted
787 # and the whitelist is not applied (yes, NSIP and NSDNAME are brittle).
788 self.checkNotBlocked('nsip.delegated.example.', singleCheck=True)
789
790 class RPZNSIPCustomTest(RPZRecursorTest):
791 """
792 This test makes sure that the recursor handles custom RPZ rules in a NSIP
793 """
794
795 _confdir = 'RPZNSIPCustom'
796 _lua_config_file = """
797 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."})
798 rpzFile('configs/%s/zone2.rpz', { policyName="zone2.rpz."})
799 """ % (_confdir, _confdir)
800 _config_template = """
801 auth-zones=example=configs/%s/example.zone
802 forward-zones=delegated.example=127.0.0.1:%d
803 """ % (_confdir, rpzAuthServerPort)
804
805 @classmethod
806 def generateRecursorConfig(cls, confdir):
807 authzonepath = os.path.join(confdir, 'example.zone')
808 with open(authzonepath, 'w') as authzone:
809 authzone.write("""$ORIGIN example.
810 @ 3600 IN SOA {soa}
811 """.format(soa=cls._SOA))
812
813 rpzFilePath = os.path.join(confdir, 'zone.rpz')
814 with open(rpzFilePath, 'w') as rpzZone:
815 rpzZone.write("""$ORIGIN zone.rpz.
816 @ 3600 IN SOA {soa}
817 32.1.0.0.127.rpz-nsip.zone.rpz. 60 IN A 192.0.2.1
818 """.format(soa=cls._SOA))
819
820 rpzFilePath = os.path.join(confdir, 'zone2.rpz')
821 with open(rpzFilePath, 'w') as rpzZone:
822 rpzZone.write("""$ORIGIN zone2.rpz.
823 @ 3600 IN SOA {soa}
824 32.1.2.0.192.rpz-ip 60 IN CNAME .
825 """.format(soa=cls._SOA))
826
827 super(RPZNSIPCustomTest, cls).generateRecursorConfig(confdir)
828
829 def testRPZDelegate(self):
830 # The IP of the NS we are going to contact should result in a custom record (192.0.2.1) from zone 1,
831 # so even though the record (192.0.2.1) returned by the server is blacklisted
832 # by zone 2, it should not be blocked.
833 # We only test once because after that the answer is cached, so the NS is not contacted
834 # and the whitelist is not applied (yes, NSIP and NSDNAME are brittle).
835 self.checkCustom('nsip.delegated.example.', 'A', dns.rrset.from_text('nsip.delegated.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.1'))