]> git.ipfire.org Git - thirdparty/pdns.git/blame - regression-tests.recursor-dnssec/test_RPZ.py
rec: Check that incremental updates to RPZ qname triggers work
[thirdparty/pdns.git] / regression-tests.recursor-dnssec / test_RPZ.py
CommitLineData
f9017ec1 1import dns
22cf3506 2import json
f9017ec1 3import os
22cf3506 4import requests
f9017ec1
RG
5import socket
6import struct
7import sys
8import threading
9import time
10
11from recursortests import RecursorTest
12
13class 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 ]
22cf3506
RG
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'),
8340237f
RG
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.'),
22cf3506
RG
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'),
8340237f
RG
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.'),
22cf3506 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),
6da513b2
RG
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'),
d13c4d18
RG
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.'),
22cf3506
RG
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 ]
f9017ec1
RG
121
122 response.answer = records
123 return (newSerial, response)
124
125 def _connectionHandler(self, conn):
126 data = None
127 while True:
128 data = conn.recv(2)
129 if not data:
130 break
131 (datalen,) = struct.unpack("!H", data)
132 data = conn.recv(datalen)
133 if not data:
134 break
135
136 message = dns.message.from_wire(data)
137 if len(message.question) != 1:
138 print('Invalid RPZ query, qdcount is %d' % (len(message.question)))
139 break
140 if not message.question[0].rdtype in [dns.rdatatype.AXFR, dns.rdatatype.IXFR]:
141 print('Invalid RPZ query, qtype is %d' % (message.question.rdtype))
142 break
143 (serial, answer) = self._getAnswer(message)
144 if not answer:
145 print('Unable to get a response for %s %d' % (message.question[0].name, message.question[0].rdtype))
146 break
147
148 wire = answer.to_wire()
149 conn.send(struct.pack("!H", len(wire)))
150 conn.send(wire)
151 self._currentSerial = serial
152 break
153
154 conn.close()
155
156 def _listener(self):
157 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
158 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
159 try:
160 sock.bind(("127.0.0.1", self._serverPort))
161 except socket.error as e:
162 print("Error binding in the RPZ listener: %s" % str(e))
163 sys.exit(1)
164
165 sock.listen(100)
166 while True:
167 try:
168 (conn, _) = sock.accept()
169 thread = threading.Thread(name='RPZ Connection Handler',
170 target=self._connectionHandler,
171 args=[conn])
172 thread.setDaemon(True)
173 thread.start()
174
175 except socket.error as e:
176 print('Error in RPZ socket: %s' % str(e))
177 sock.close()
178
b773359c 179rpzServerPort = 4250
f9017ec1
RG
180rpzServer = RPZServer(rpzServerPort)
181
182class RPZRecursorTest(RecursorTest):
183 """
184 This test makes sure that we correctly update RPZ zones via AXFR then IXFR
185 """
186
187 global rpzServerPort
188 _lua_config_file = """
5f311886
RG
189 -- The first server is a bogus one, to test that we correctly fail over to the second one
190 rpzMaster({'127.0.0.1:9999', '127.0.0.1:%d'}, 'zone.rpz.', { refresh=1 })
f9017ec1 191 """ % (rpzServerPort)
22cf3506
RG
192 _wsPort = 8042
193 _wsTimeout = 2
194 _wsPassword = 'secretpassword'
195 _apiKey = 'secretapikey'
f9017ec1 196 _confdir = 'RPZ'
d19bcbf0
RG
197 _lua_dns_script_file = """
198
199 function prerpz(dq)
200 -- disable the RPZ policy named 'zone.rpz' for AD=1 queries
201 if dq:getDH():getAD() then
202 dq:discardPolicy('zone.rpz.')
203 end
204 return false
205 end
206 """
207
f9017ec1 208 _config_template = """
22cf3506
RG
209auth-zones=example=configs/%s/example.zone
210webserver=yes
211webserver-port=%d
212webserver-address=127.0.0.1
213webserver-password=%s
214api-key=%s
215""" % (_confdir, _wsPort, _wsPassword, _apiKey)
216 _xfrDone = 0
f9017ec1
RG
217
218 @classmethod
219 def generateRecursorConfig(cls, confdir):
220 authzonepath = os.path.join(confdir, 'example.zone')
221 with open(authzonepath, 'w') as authzone:
222 authzone.write("""$ORIGIN example.
223@ 3600 IN SOA {soa}
224a 3600 IN A 192.0.2.42
225b 3600 IN A 192.0.2.42
226c 3600 IN A 192.0.2.42
22cf3506
RG
227d 3600 IN A 192.0.2.42
228e 3600 IN A 192.0.2.42
f9017ec1
RG
229""".format(soa=cls._SOA))
230 super(RPZRecursorTest, cls).generateRecursorConfig(confdir)
231
232 @classmethod
233 def setUpClass(cls):
234
235 cls.setUpSockets()
236 cls.startResponders()
237
238 confdir = os.path.join('configs', cls._confdir)
239 cls.createConfigDir(confdir)
240
241 cls.generateRecursorConfig(confdir)
242 cls.startRecursor(confdir, cls._recursorPort)
243
244 @classmethod
245 def tearDownClass(cls):
246 cls.tearDownRecursor()
247
d19bcbf0 248 def checkBlocked(self, name, shouldBeBlocked=True, adQuery=False):
f9017ec1
RG
249 query = dns.message.make_query(name, 'A', want_dnssec=True)
250 query.flags |= dns.flags.CD
d19bcbf0
RG
251 if adQuery:
252 query.flags |= dns.flags.AD
f9017ec1 253
d13c4d18
RG
254 for method in ("sendUDPQuery", "sendTCPQuery"):
255 sender = getattr(self, method)
256 res = sender(query)
257 self.assertRcodeEqual(res, dns.rcode.NOERROR)
258 if shouldBeBlocked:
259 expected = dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.1')
260 else:
261 expected = dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.42')
262
263 self.assertRRsetInAnswer(res, expected)
f9017ec1 264
d19bcbf0
RG
265 def checkNotBlocked(self, name, adQuery=False):
266 self.checkBlocked(name, False, adQuery)
f9017ec1 267
6da513b2
RG
268 def checkCustom(self, qname, qtype, expected):
269 query = dns.message.make_query(qname, qtype, want_dnssec=True)
270 query.flags |= dns.flags.CD
d13c4d18
RG
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)
6da513b2
RG
276
277 def checkNoData(self, qname, qtype):
d13c4d18
RG
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 checkTruncated(self, qname, qtype='A'):
6da513b2
RG
287 query = dns.message.make_query(qname, qtype, want_dnssec=True)
288 query.flags |= dns.flags.CD
289 res = self.sendUDPQuery(query)
d13c4d18
RG
290 self.assertRcodeEqual(res, dns.rcode.NOERROR)
291 self.assertMessageHasFlags(res, ['QR', 'RA', 'RD', 'CD', 'TC'])
292 self.assertEqual(len(res.answer), 0)
293 self.assertEqual(len(res.authority), 0)
294 self.assertEqual(len(res.additional), 0)
6da513b2 295
d13c4d18
RG
296 res = self.sendTCPQuery(query)
297 self.assertRcodeEqual(res, dns.rcode.NXDOMAIN)
298 self.assertMessageHasFlags(res, ['QR', 'RA', 'RD', 'CD'])
6da513b2 299 self.assertEqual(len(res.answer), 0)
d13c4d18
RG
300 self.assertEqual(len(res.authority), 1)
301 self.assertEqual(len(res.additional), 0)
302
303 def checkDropped(self, qname, qtype='A'):
304 query = dns.message.make_query(qname, qtype, want_dnssec=True)
305 query.flags |= dns.flags.CD
306 for method in ("sendUDPQuery", "sendTCPQuery"):
307 sender = getattr(self, method)
308 res = sender(query)
309 self.assertEqual(res, None)
6da513b2 310
f9017ec1
RG
311 def waitUntilCorrectSerialIsLoaded(self, serial, timeout=5):
312 global rpzServer
313
314 rpzServer.moveToSerial(serial)
315
316 attempts = 0
317 while attempts < timeout:
318 currentSerial = rpzServer.getCurrentSerial()
319 if currentSerial > serial:
320 raise AssertionError("Expected serial %d, got %d" % (serial, currentSerial))
321 if currentSerial == serial:
22cf3506 322 self._xfrDone = self._xfrDone + 1
f9017ec1
RG
323 return
324
325 attempts = attempts + 1
326 time.sleep(1)
327
328 raise AssertionError("Waited %d seconds for the serial to be updated to %d but the serial is still %d" % (timeout, serial, currentSerial))
329
22cf3506
RG
330 def checkRPZStats(self, serial, recordsCount, fullXFRCount, totalXFRCount):
331 headers = {'x-api-key': self._apiKey}
332 url = 'http://127.0.0.1:' + str(self._wsPort) + '/api/v1/servers/localhost/rpzstatistics'
333 r = requests.get(url, headers=headers, timeout=self._wsTimeout)
334 self.assertTrue(r)
335 self.assertEquals(r.status_code, 200)
336 self.assertTrue(r.json())
337 content = r.json()
338 self.assertIn('zone.rpz.', content)
339 zone = content['zone.rpz.']
340 for key in ['last_update', 'records', 'serial', 'transfers_failed', 'transfers_full', 'transfers_success']:
341 self.assertIn(key, zone)
342
343 self.assertEquals(zone['serial'], serial)
344 self.assertEquals(zone['records'], recordsCount)
345 self.assertEquals(zone['transfers_full'], fullXFRCount)
346 self.assertEquals(zone['transfers_success'], totalXFRCount)
347
f9017ec1
RG
348 def testRPZ(self):
349 # first zone, only a should be blocked
350 self.waitUntilCorrectSerialIsLoaded(1)
22cf3506 351 self.checkRPZStats(1, 1, 1, self._xfrDone)
f9017ec1
RG
352 self.checkBlocked('a.example.')
353 self.checkNotBlocked('b.example.')
354 self.checkNotBlocked('c.example.')
355
356 # second zone, a and b should be blocked
357 self.waitUntilCorrectSerialIsLoaded(2)
22cf3506 358 self.checkRPZStats(2, 2, 1, self._xfrDone)
f9017ec1
RG
359 self.checkBlocked('a.example.')
360 self.checkBlocked('b.example.')
361 self.checkNotBlocked('c.example.')
362
363 # third zone, only b should be blocked
364 self.waitUntilCorrectSerialIsLoaded(3)
22cf3506 365 self.checkRPZStats(3, 1, 1, self._xfrDone)
f9017ec1
RG
366 self.checkNotBlocked('a.example.')
367 self.checkBlocked('b.example.')
368 self.checkNotBlocked('c.example.')
369
370 # fourth zone, only c should be blocked
371 self.waitUntilCorrectSerialIsLoaded(4)
22cf3506 372 self.checkRPZStats(4, 1, 1, self._xfrDone)
f9017ec1
RG
373 self.checkNotBlocked('a.example.')
374 self.checkNotBlocked('b.example.')
375 self.checkBlocked('c.example.')
22cf3506
RG
376
377 # fifth zone, we should get a full AXFR this time, and only d should be blocked
378 self.waitUntilCorrectSerialIsLoaded(5)
8340237f 379 self.checkRPZStats(5, 3, 2, self._xfrDone)
22cf3506
RG
380 self.checkNotBlocked('a.example.')
381 self.checkNotBlocked('b.example.')
382 self.checkNotBlocked('c.example.')
383 self.checkBlocked('d.example.')
384
6da513b2 385 # sixth zone, only e should be blocked, f is a local data record
22cf3506 386 self.waitUntilCorrectSerialIsLoaded(6)
6da513b2
RG
387 self.checkRPZStats(6, 2, 2, self._xfrDone)
388 self.checkNotBlocked('a.example.')
389 self.checkNotBlocked('b.example.')
390 self.checkNotBlocked('c.example.')
391 self.checkNotBlocked('d.example.')
392 self.checkCustom('e.example.', 'A', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.1', '192.0.2.2'))
393 self.checkCustom('e.example.', 'MX', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'MX', '10 mx.example.'))
394 self.checkNoData('e.example.', 'AAAA')
395 self.checkCustom('f.example.', 'A', dns.rrset.from_text('f.example.', 0, dns.rdataclass.IN, 'CNAME', 'e.example.'))
396
397 # seventh zone, e should only have one A
398 self.waitUntilCorrectSerialIsLoaded(7)
d13c4d18 399 self.checkRPZStats(7, 4, 2, self._xfrDone)
22cf3506
RG
400 self.checkNotBlocked('a.example.')
401 self.checkNotBlocked('b.example.')
402 self.checkNotBlocked('c.example.')
403 self.checkNotBlocked('d.example.')
6da513b2
RG
404 self.checkCustom('e.example.', 'A', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.2'))
405 self.checkCustom('e.example.', 'MX', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'MX', '10 mx.example.'))
406 self.checkNoData('e.example.', 'AAAA')
407 self.checkCustom('f.example.', 'A', dns.rrset.from_text('f.example.', 0, dns.rdataclass.IN, 'CNAME', 'e.example.'))
d19bcbf0
RG
408 # check that the policy is disabled for AD=1 queries
409 self.checkNotBlocked('e.example.', True)
d13c4d18
RG
410 # check non-custom policies
411 self.checkTruncated('tc.example.')
412 self.checkDropped('drop.example.')