]> git.ipfire.org Git - thirdparty/pdns.git/blob - regression-tests.recursor-dnssec/test_RPZ.py
Merge remote-tracking branch 'origin/rec-cname-rpz-4' into rec-cname-rpz
[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 serve 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 check 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 elif newSerial == 11:
146 # IXFR with two deltas, the first one adding a 'g' and the second one removing 'f'
147 records = [
148 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 + 1)),
149 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),
150 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),
151 dns.rrset.from_text('g.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'),
152 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),
153 dns.rrset.from_text('f.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'),
154 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 + 1)),
155 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 + 1))
156 ]
157 # this one has two updates in one
158 newSerial = newSerial + 1
159 self._targetSerial = self._targetSerial + 1
160
161 response.answer = records
162 return (newSerial, response)
163
164 def _connectionHandler(self, conn):
165 data = None
166 while True:
167 data = conn.recv(2)
168 if not data:
169 break
170 (datalen,) = struct.unpack("!H", data)
171 data = conn.recv(datalen)
172 if not data:
173 break
174
175 message = dns.message.from_wire(data)
176 if len(message.question) != 1:
177 print('Invalid RPZ query, qdcount is %d' % (len(message.question)))
178 break
179 if not message.question[0].rdtype in [dns.rdatatype.AXFR, dns.rdatatype.IXFR]:
180 print('Invalid RPZ query, qtype is %d' % (message.question.rdtype))
181 break
182 (serial, answer) = self._getAnswer(message)
183 if not answer:
184 print('Unable to get a response for %s %d' % (message.question[0].name, message.question[0].rdtype))
185 break
186
187 wire = answer.to_wire()
188 conn.send(struct.pack("!H", len(wire)))
189 conn.send(wire)
190 self._currentSerial = serial
191 break
192
193 conn.close()
194
195 def _listener(self):
196 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
197 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
198 try:
199 sock.bind(("127.0.0.1", self._serverPort))
200 except socket.error as e:
201 print("Error binding in the RPZ listener: %s" % str(e))
202 sys.exit(1)
203
204 sock.listen(100)
205 while True:
206 try:
207 (conn, _) = sock.accept()
208 thread = threading.Thread(name='RPZ Connection Handler',
209 target=self._connectionHandler,
210 args=[conn])
211 thread.setDaemon(True)
212 thread.start()
213
214 except socket.error as e:
215 print('Error in RPZ socket: %s' % str(e))
216 sock.close()
217
218 class RPZRecursorTest(RecursorTest):
219 _wsPort = 8042
220 _wsTimeout = 2
221 _wsPassword = 'secretpassword'
222 _apiKey = 'secretapikey'
223 _confdir = 'RPZ'
224 _lua_dns_script_file = """
225
226 function prerpz(dq)
227 -- disable the RPZ policy named 'zone.rpz' for AD=1 queries
228 if dq:getDH():getAD() then
229 dq:discardPolicy('zone.rpz.')
230 end
231 return false
232 end
233 """
234
235 _config_template = """
236 auth-zones=example=configs/%s/example.zone
237 webserver=yes
238 webserver-port=%d
239 webserver-address=127.0.0.1
240 webserver-password=%s
241 api-key=%s
242 log-rpz-changes=yes
243 """ % (_confdir, _wsPort, _wsPassword, _apiKey)
244
245 @classmethod
246 def setUpClass(cls):
247
248 cls.setUpSockets()
249 cls.startResponders()
250
251 confdir = os.path.join('configs', cls._confdir)
252 cls.createConfigDir(confdir)
253
254 cls.generateRecursorConfig(confdir)
255 cls.startRecursor(confdir, cls._recursorPort)
256
257 @classmethod
258 def tearDownClass(cls):
259 cls.tearDownRecursor()
260
261 def checkBlocked(self, name, shouldBeBlocked=True, adQuery=False, singleCheck=False):
262 query = dns.message.make_query(name, 'A', want_dnssec=True)
263 query.flags |= dns.flags.CD
264 if adQuery:
265 query.flags |= dns.flags.AD
266
267 for method in ("sendUDPQuery", "sendTCPQuery"):
268 sender = getattr(self, method)
269 res = sender(query)
270 self.assertRcodeEqual(res, dns.rcode.NOERROR)
271 if shouldBeBlocked:
272 expected = dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.1')
273 else:
274 expected = dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.42')
275
276 self.assertRRsetInAnswer(res, expected)
277 if singleCheck:
278 break
279
280 def checkNotBlocked(self, name, adQuery=False, singleCheck=False):
281 self.checkBlocked(name, False, adQuery, singleCheck)
282
283 def checkCustom(self, qname, qtype, expected):
284 query = dns.message.make_query(qname, qtype, want_dnssec=True)
285 query.flags |= dns.flags.CD
286 for method in ("sendUDPQuery", "sendTCPQuery"):
287 sender = getattr(self, method)
288 res = sender(query)
289 self.assertRcodeEqual(res, dns.rcode.NOERROR)
290 self.assertRRsetInAnswer(res, expected)
291
292 def checkNoData(self, qname, qtype):
293 query = dns.message.make_query(qname, qtype, want_dnssec=True)
294 query.flags |= dns.flags.CD
295 for method in ("sendUDPQuery", "sendTCPQuery"):
296 sender = getattr(self, method)
297 res = sender(query)
298 self.assertRcodeEqual(res, dns.rcode.NOERROR)
299 self.assertEqual(len(res.answer), 0)
300
301 def checkNXD(self, qname, qtype='A'):
302 query = dns.message.make_query(qname, qtype, want_dnssec=True)
303 query.flags |= dns.flags.CD
304 for method in ("sendUDPQuery", "sendTCPQuery"):
305 sender = getattr(self, method)
306 res = sender(query)
307 self.assertRcodeEqual(res, dns.rcode.NXDOMAIN)
308 self.assertEqual(len(res.answer), 0)
309 self.assertEqual(len(res.authority), 1)
310
311 def checkTruncated(self, qname, qtype='A'):
312 query = dns.message.make_query(qname, qtype, want_dnssec=True)
313 query.flags |= dns.flags.CD
314 res = self.sendUDPQuery(query)
315 self.assertRcodeEqual(res, dns.rcode.NOERROR)
316 self.assertMessageHasFlags(res, ['QR', 'RA', 'RD', 'CD', 'TC'])
317 self.assertEqual(len(res.answer), 0)
318 self.assertEqual(len(res.authority), 0)
319 self.assertEqual(len(res.additional), 0)
320
321 res = self.sendTCPQuery(query)
322 self.assertRcodeEqual(res, dns.rcode.NXDOMAIN)
323 self.assertMessageHasFlags(res, ['QR', 'RA', 'RD', 'CD'])
324 self.assertEqual(len(res.answer), 0)
325 self.assertEqual(len(res.authority), 1)
326 self.assertEqual(len(res.additional), 0)
327
328 def checkDropped(self, qname, qtype='A'):
329 query = dns.message.make_query(qname, qtype, want_dnssec=True)
330 query.flags |= dns.flags.CD
331 for method in ("sendUDPQuery", "sendTCPQuery"):
332 sender = getattr(self, method)
333 res = sender(query)
334 self.assertEqual(res, None)
335
336 def checkRPZStats(self, serial, recordsCount, fullXFRCount, totalXFRCount):
337 headers = {'x-api-key': self._apiKey}
338 url = 'http://127.0.0.1:' + str(self._wsPort) + '/api/v1/servers/localhost/rpzstatistics'
339 r = requests.get(url, headers=headers, timeout=self._wsTimeout)
340 self.assertTrue(r)
341 self.assertEquals(r.status_code, 200)
342 self.assertTrue(r.json())
343 content = r.json()
344 self.assertIn('zone.rpz.', content)
345 zone = content['zone.rpz.']
346 for key in ['last_update', 'records', 'serial', 'transfers_failed', 'transfers_full', 'transfers_success']:
347 self.assertIn(key, zone)
348
349 self.assertEquals(zone['serial'], serial)
350 self.assertEquals(zone['records'], recordsCount)
351 self.assertEquals(zone['transfers_full'], fullXFRCount)
352 self.assertEquals(zone['transfers_success'], totalXFRCount)
353
354 rpzServerPort = 4250
355 rpzServer = RPZServer(rpzServerPort)
356
357 class RPZXFRRecursorTest(RPZRecursorTest):
358 """
359 This test makes sure that we correctly update RPZ zones via AXFR then IXFR
360 """
361
362 global rpzServerPort
363 _lua_config_file = """
364 -- The first server is a bogus one, to test that we correctly fail over to the second one
365 rpzMaster({'127.0.0.1:9999', '127.0.0.1:%d'}, 'zone.rpz.', { refresh=1 })
366 """ % (rpzServerPort)
367 _confdir = 'RPZXFR'
368 _wsPort = 8042
369 _wsTimeout = 2
370 _wsPassword = 'secretpassword'
371 _apiKey = 'secretapikey'
372 _config_template = """
373 auth-zones=example=configs/%s/example.zone
374 webserver=yes
375 webserver-port=%d
376 webserver-address=127.0.0.1
377 webserver-password=%s
378 api-key=%s
379 """ % (_confdir, _wsPort, _wsPassword, _apiKey)
380 _xfrDone = 0
381
382 @classmethod
383 def generateRecursorConfig(cls, confdir):
384 authzonepath = os.path.join(confdir, 'example.zone')
385 with open(authzonepath, 'w') as authzone:
386 authzone.write("""$ORIGIN example.
387 @ 3600 IN SOA {soa}
388 a 3600 IN A 192.0.2.42
389 b 3600 IN A 192.0.2.42
390 c 3600 IN A 192.0.2.42
391 d 3600 IN A 192.0.2.42
392 e 3600 IN A 192.0.2.42
393 """.format(soa=cls._SOA))
394 super(RPZRecursorTest, cls).generateRecursorConfig(confdir)
395
396 def waitUntilCorrectSerialIsLoaded(self, serial, timeout=5):
397 global rpzServer
398
399 rpzServer.moveToSerial(serial)
400
401 attempts = 0
402 while attempts < timeout:
403 currentSerial = rpzServer.getCurrentSerial()
404 if currentSerial > serial:
405 raise AssertionError("Expected serial %d, got %d" % (serial, currentSerial))
406 if currentSerial == serial:
407 self._xfrDone = self._xfrDone + 1
408 return
409
410 attempts = attempts + 1
411 time.sleep(1)
412
413 raise AssertionError("Waited %d seconds for the serial to be updated to %d but the serial is still %d" % (timeout, serial, currentSerial))
414
415 def testRPZ(self):
416 # first zone, only a should be blocked
417 self.waitUntilCorrectSerialIsLoaded(1)
418 self.checkRPZStats(1, 1, 1, self._xfrDone)
419 self.checkBlocked('a.example.')
420 self.checkNotBlocked('b.example.')
421 self.checkNotBlocked('c.example.')
422
423 # second zone, a and b should be blocked
424 self.waitUntilCorrectSerialIsLoaded(2)
425 self.checkRPZStats(2, 2, 1, self._xfrDone)
426 self.checkBlocked('a.example.')
427 self.checkBlocked('b.example.')
428 self.checkNotBlocked('c.example.')
429
430 # third zone, only b should be blocked
431 self.waitUntilCorrectSerialIsLoaded(3)
432 self.checkRPZStats(3, 1, 1, self._xfrDone)
433 self.checkNotBlocked('a.example.')
434 self.checkBlocked('b.example.')
435 self.checkNotBlocked('c.example.')
436
437 # fourth zone, only c should be blocked
438 self.waitUntilCorrectSerialIsLoaded(4)
439 self.checkRPZStats(4, 1, 1, self._xfrDone)
440 self.checkNotBlocked('a.example.')
441 self.checkNotBlocked('b.example.')
442 self.checkBlocked('c.example.')
443
444 # fifth zone, we should get a full AXFR this time, and only d should be blocked
445 self.waitUntilCorrectSerialIsLoaded(5)
446 self.checkRPZStats(5, 3, 2, self._xfrDone)
447 self.checkNotBlocked('a.example.')
448 self.checkNotBlocked('b.example.')
449 self.checkNotBlocked('c.example.')
450 self.checkBlocked('d.example.')
451
452 # sixth zone, only e should be blocked, f is a local data record
453 self.waitUntilCorrectSerialIsLoaded(6)
454 self.checkRPZStats(6, 2, 2, self._xfrDone)
455 self.checkNotBlocked('a.example.')
456 self.checkNotBlocked('b.example.')
457 self.checkNotBlocked('c.example.')
458 self.checkNotBlocked('d.example.')
459 self.checkCustom('e.example.', 'A', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.1', '192.0.2.2'))
460 self.checkCustom('e.example.', 'MX', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'MX', '10 mx.example.'))
461 self.checkNoData('e.example.', 'AAAA')
462 self.checkCustom('f.example.', 'A', dns.rrset.from_text('f.example.', 0, dns.rdataclass.IN, 'CNAME', 'e.example.'))
463
464 # seventh zone, e should only have one A
465 self.waitUntilCorrectSerialIsLoaded(7)
466 self.checkRPZStats(7, 4, 2, self._xfrDone)
467 self.checkNotBlocked('a.example.')
468 self.checkNotBlocked('b.example.')
469 self.checkNotBlocked('c.example.')
470 self.checkNotBlocked('d.example.')
471 self.checkCustom('e.example.', 'A', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.2'))
472 self.checkCustom('e.example.', 'MX', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'MX', '10 mx.example.'))
473 self.checkNoData('e.example.', 'AAAA')
474 self.checkCustom('f.example.', 'A', dns.rrset.from_text('f.example.', 0, dns.rdataclass.IN, 'CNAME', 'e.example.'))
475 # check that the policy is disabled for AD=1 queries
476 self.checkNotBlocked('e.example.', True)
477 # check non-custom policies
478 self.checkTruncated('tc.example.')
479 self.checkDropped('drop.example.')
480
481 # eighth zone, all entries should be gone
482 self.waitUntilCorrectSerialIsLoaded(8)
483 self.checkRPZStats(8, 0, 3, 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.checkNXD('f.example.')
490 self.checkNXD('tc.example.')
491 self.checkNXD('drop.example.')
492
493 # 9th zone is a duplicate, it might get skipped
494 global rpzServer
495 rpzServer.moveToSerial(9)
496 time.sleep(3)
497 self.waitUntilCorrectSerialIsLoaded(10)
498 self.checkRPZStats(10, 1, 4, self._xfrDone)
499 self.checkNotBlocked('a.example.')
500 self.checkNotBlocked('b.example.')
501 self.checkNotBlocked('c.example.')
502 self.checkNotBlocked('d.example.')
503 self.checkNotBlocked('e.example.')
504 self.checkBlocked('f.example.')
505 self.checkNXD('tc.example.')
506 self.checkNXD('drop.example.')
507
508 # the next update will update the zone twice
509 rpzServer.moveToSerial(11)
510 time.sleep(3)
511 self.waitUntilCorrectSerialIsLoaded(12)
512 self.checkRPZStats(12, 1, 4, self._xfrDone)
513 self.checkNotBlocked('a.example.')
514 self.checkNotBlocked('b.example.')
515 self.checkNotBlocked('c.example.')
516 self.checkNotBlocked('d.example.')
517 self.checkNotBlocked('e.example.')
518 self.checkNXD('f.example.')
519 self.checkBlocked('g.example.')
520 self.checkNXD('tc.example.')
521 self.checkNXD('drop.example.')
522
523 class RPZFileRecursorTest(RPZRecursorTest):
524 """
525 This test makes sure that we correctly load RPZ zones from a file
526 """
527
528 _confdir = 'RPZFile'
529 _lua_config_file = """
530 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz." })
531 """ % (_confdir)
532 _config_template = """
533 auth-zones=example=configs/%s/example.zone
534 """ % (_confdir)
535
536 @classmethod
537 def generateRecursorConfig(cls, confdir):
538 authzonepath = os.path.join(confdir, 'example.zone')
539 with open(authzonepath, 'w') as authzone:
540 authzone.write("""$ORIGIN example.
541 @ 3600 IN SOA {soa}
542 a 3600 IN A 192.0.2.42
543 b 3600 IN A 192.0.2.42
544 c 3600 IN A 192.0.2.42
545 d 3600 IN A 192.0.2.42
546 e 3600 IN A 192.0.2.42
547 z 3600 IN A 192.0.2.42
548 """.format(soa=cls._SOA))
549
550 rpzFilePath = os.path.join(confdir, 'zone.rpz')
551 with open(rpzFilePath, 'w') as rpzZone:
552 rpzZone.write("""$ORIGIN zone.rpz.
553 @ 3600 IN SOA {soa}
554 a.example.zone.rpz. 60 IN A 192.0.2.42
555 a.example.zone.rpz. 60 IN A 192.0.2.43
556 a.example.zone.rpz. 60 IN TXT "some text"
557 drop.example.zone.rpz. 60 IN CNAME rpz-drop.
558 z.example.zone.rpz. 60 IN A 192.0.2.1
559 tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
560 """.format(soa=cls._SOA))
561 super(RPZFileRecursorTest, cls).generateRecursorConfig(confdir)
562
563 def testRPZ(self):
564 self.checkCustom('a.example.', 'A', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.42', '192.0.2.43'))
565 self.checkCustom('a.example.', 'TXT', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'TXT', '"some text"'))
566 self.checkBlocked('z.example.')
567 self.checkNotBlocked('b.example.')
568 self.checkNotBlocked('c.example.')
569 self.checkNotBlocked('d.example.')
570 self.checkNotBlocked('e.example.')
571 # check that the policy is disabled for AD=1 queries
572 self.checkNotBlocked('z.example.', True)
573 # check non-custom policies
574 self.checkTruncated('tc.example.')
575 self.checkDropped('drop.example.')
576
577 class RPZFileDefaultPolRecursorTest(RPZRecursorTest):
578 """
579 This test makes sure that we correctly load RPZ zones from a file with a default policy
580 """
581
582 _confdir = 'RPZFileDefaultPolicy'
583 _lua_config_file = """
584 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction })
585 """ % (_confdir)
586 _config_template = """
587 auth-zones=example=configs/%s/example.zone
588 """ % (_confdir)
589
590 @classmethod
591 def generateRecursorConfig(cls, confdir):
592 authzonepath = os.path.join(confdir, 'example.zone')
593 with open(authzonepath, 'w') as authzone:
594 authzone.write("""$ORIGIN example.
595 @ 3600 IN SOA {soa}
596 a 3600 IN A 192.0.2.42
597 b 3600 IN A 192.0.2.42
598 c 3600 IN A 192.0.2.42
599 d 3600 IN A 192.0.2.42
600 drop 3600 IN A 192.0.2.42
601 e 3600 IN A 192.0.2.42
602 z 3600 IN A 192.0.2.42
603 """.format(soa=cls._SOA))
604
605 rpzFilePath = os.path.join(confdir, 'zone.rpz')
606 with open(rpzFilePath, 'w') as rpzZone:
607 rpzZone.write("""$ORIGIN zone.rpz.
608 @ 3600 IN SOA {soa}
609 a.example.zone.rpz. 60 IN A 192.0.2.42
610 drop.example.zone.rpz. 60 IN CNAME rpz-drop.
611 z.example.zone.rpz. 60 IN A 192.0.2.1
612 tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
613 """.format(soa=cls._SOA))
614 super(RPZFileDefaultPolRecursorTest, cls).generateRecursorConfig(confdir)
615
616 def testRPZ(self):
617 # local data entries are overridden by default
618 self.checkCustom('a.example.', 'A', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.42'))
619 self.checkNoData('a.example.', 'TXT')
620 # will not be blocked because the default policy overrides local data entries by default
621 self.checkNotBlocked('z.example.')
622 self.checkNotBlocked('b.example.')
623 self.checkNotBlocked('c.example.')
624 self.checkNotBlocked('d.example.')
625 self.checkNotBlocked('e.example.')
626 # check non-local policies, they should be overridden by the default policy
627 self.checkNXD('tc.example.', 'A')
628 self.checkNotBlocked('drop.example.')
629
630 class RPZFileDefaultPolNotOverrideLocalRecursorTest(RPZRecursorTest):
631 """
632 This test makes sure that we correctly load RPZ zones from a file with a default policy, not overriding local data entries
633 """
634
635 _confdir = 'RPZFileDefaultPolicyNotOverrideLocal'
636 _lua_config_file = """
637 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction, defpolOverrideLocalData=false })
638 """ % (_confdir)
639 _config_template = """
640 auth-zones=example=configs/%s/example.zone
641 """ % (_confdir)
642
643 @classmethod
644 def generateRecursorConfig(cls, confdir):
645 authzonepath = os.path.join(confdir, 'example.zone')
646 with open(authzonepath, 'w') as authzone:
647 authzone.write("""$ORIGIN example.
648 @ 3600 IN SOA {soa}
649 a 3600 IN A 192.0.2.42
650 b 3600 IN A 192.0.2.42
651 c 3600 IN A 192.0.2.42
652 d 3600 IN A 192.0.2.42
653 drop 3600 IN A 192.0.2.42
654 e 3600 IN A 192.0.2.42
655 z 3600 IN A 192.0.2.42
656 """.format(soa=cls._SOA))
657
658 rpzFilePath = os.path.join(confdir, 'zone.rpz')
659 with open(rpzFilePath, 'w') as rpzZone:
660 rpzZone.write("""$ORIGIN zone.rpz.
661 @ 3600 IN SOA {soa}
662 a.example.zone.rpz. 60 IN A 192.0.2.42
663 a.example.zone.rpz. 60 IN A 192.0.2.43
664 a.example.zone.rpz. 60 IN TXT "some text"
665 drop.example.zone.rpz. 60 IN CNAME rpz-drop.
666 z.example.zone.rpz. 60 IN A 192.0.2.1
667 tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
668 """.format(soa=cls._SOA))
669 super(RPZFileDefaultPolNotOverrideLocalRecursorTest, cls).generateRecursorConfig(confdir)
670
671 def testRPZ(self):
672 # local data entries will not be overridden by the default policy
673 self.checkCustom('a.example.', 'A', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.42', '192.0.2.43'))
674 self.checkCustom('a.example.', 'TXT', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'TXT', '"some text"'))
675 # will be blocked because the default policy does not override local data entries
676 self.checkBlocked('z.example.')
677 self.checkNotBlocked('b.example.')
678 self.checkNotBlocked('c.example.')
679 self.checkNotBlocked('d.example.')
680 self.checkNotBlocked('e.example.')
681 # check non-local policies, they should be overridden by the default policy
682 self.checkNXD('tc.example.', 'A')
683 self.checkNotBlocked('drop.example.')
684
685 class RPZSimpleAuthServer(object):
686
687 def __init__(self, port):
688 self._serverPort = port
689 listener = threading.Thread(name='RPZ Simple Auth Listener', target=self._listener, args=[])
690 listener.setDaemon(True)
691 listener.start()
692
693 def _getAnswer(self, message):
694
695 response = dns.message.make_response(message)
696 response.flags |= dns.flags.AA
697 records = [
698 dns.rrset.from_text('nsip.delegated.example.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.42')
699 ]
700
701 response.answer = records
702 return response
703
704 def _listener(self):
705 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
706 try:
707 sock.bind(("127.0.0.1", self._serverPort))
708 except socket.error as e:
709 print("Error binding in the RPZ simple auth listener: %s" % str(e))
710 sys.exit(1)
711
712 while True:
713 try:
714 data, addr = sock.recvfrom(4096)
715 message = dns.message.from_wire(data)
716 if len(message.question) != 1:
717 print('Invalid query, qdcount is %d' % (len(message.question)))
718 break
719
720 answer = self._getAnswer(message)
721 if not answer:
722 print('Unable to get a response for %s %d' % (message.question[0].name, message.question[0].rdtype))
723 break
724
725 wire = answer.to_wire()
726 sock.sendto(wire, addr)
727
728 except socket.error as e:
729 print('Error in RPZ simple auth socket: %s' % str(e))
730
731 rpzAuthServerPort = 4260
732 rpzAuthServer = RPZSimpleAuthServer(rpzAuthServerPort)
733
734 class RPZOrderingPrecedenceRecursorTest(RPZRecursorTest):
735 """
736 This test makes sure that the recursor respects the RPZ ordering precedence rules
737 """
738
739 _confdir = 'RPZOrderingPrecedence'
740 _lua_config_file = """
741 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."})
742 rpzFile('configs/%s/zone2.rpz', { policyName="zone2.rpz."})
743 """ % (_confdir, _confdir)
744 _config_template = """
745 auth-zones=example=configs/%s/example.zone
746 forward-zones=delegated.example=127.0.0.1:%d
747 """ % (_confdir, rpzAuthServerPort)
748
749 @classmethod
750 def generateRecursorConfig(cls, confdir):
751 authzonepath = os.path.join(confdir, 'example.zone')
752 with open(authzonepath, 'w') as authzone:
753 authzone.write("""$ORIGIN example.
754 @ 3600 IN SOA {soa}
755 sub.test 3600 IN A 192.0.2.42
756 passthru-then-blocked-by-higher 3600 IN A 192.0.2.66
757 passthru-then-blocked-by-same 3600 IN A 192.0.2.66
758 blocked-then-passhtru-by-higher 3600 IN A 192.0.2.100
759 """.format(soa=cls._SOA))
760
761 rpzFilePath = os.path.join(confdir, 'zone.rpz')
762 with open(rpzFilePath, 'w') as rpzZone:
763 rpzZone.write("""$ORIGIN zone.rpz.
764 @ 3600 IN SOA {soa}
765 *.test.example.zone.rpz. 60 IN CNAME rpz-passthru.
766 32.66.2.0.192.rpz-ip.zone.rpz. 60 IN A 192.0.2.1
767 32.100.2.0.192.rpz-ip.zone.rpz. 60 IN CNAME rpz-passthru.
768 passthru-then-blocked-by-same.example.zone.rpz. 60 IN CNAME rpz-passthru.
769 32.1.0.0.127.rpz-nsip.zone.rpz. 60 IN CNAME rpz-passthru.
770 """.format(soa=cls._SOA))
771
772 rpzFilePath = os.path.join(confdir, 'zone2.rpz')
773 with open(rpzFilePath, 'w') as rpzZone:
774 rpzZone.write("""$ORIGIN zone2.rpz.
775 @ 3600 IN SOA {soa}
776 sub.test.example.com.zone2.rpz. 60 IN CNAME .
777 passthru-then-blocked-by-higher.example.zone2.rpz. 60 IN CNAME rpz-passthru.
778 blocked-then-passhtru-by-higher.example.zone2.rpz. 60 IN A 192.0.2.1
779 32.42.2.0.192.rpz-ip 60 IN CNAME .
780 """.format(soa=cls._SOA))
781
782 super(RPZOrderingPrecedenceRecursorTest, cls).generateRecursorConfig(confdir)
783
784 def testRPZOrderingForQNameAndWhitelisting(self):
785 # we should first match on the qname (the wildcard, not on the exact name since
786 # we respect the order of the RPZ zones), see the pass-thru rule
787 # and only process RPZ rules of higher precedence.
788 # The subsequent rule on the content of the A should therefore not trigger a NXDOMAIN.
789 self.checkNotBlocked('sub.test.example.')
790
791 def testRPZOrderingWhitelistedThenBlockedByHigher(self):
792 # we should first match on the qname from the second RPZ zone,
793 # continue the resolution process, and get blocked by the content of the A record
794 # based on the first RPZ zone, whose priority is higher than the second one.
795 self.checkBlocked('passthru-then-blocked-by-higher.example.')
796
797 def testRPZOrderingWhitelistedThenBlockedBySame(self):
798 # we should first match on the qname from the first RPZ zone,
799 # continue the resolution process, and NOT get blocked by the content of the A record
800 # based on the same RPZ zone, since it's not higher.
801 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'))
802
803 def testRPZOrderBlockedThenWhitelisted(self):
804 # The qname is first blocked by the second RPZ zone
805 # Then, should the resolution process go on, the A record would be whitelisted
806 # by the first zone.
807 # This is what the RPZ specification requires, but we currently decided that we
808 # don't want to leak queries to malicious DNS servers and waste time if the qname is blacklisted.
809 # We might change our opinion at some point, though.
810 self.checkBlocked('blocked-then-passhtru-by-higher.example.')
811
812 def testRPZOrderDelegate(self):
813 # The IP of the NS we are going to contact is whitelisted (passthru) in zone 1,
814 # so even though the record (192.0.2.42) returned by the server is blacklisted
815 # by zone 2, it should not be blocked.
816 # We only test once because after that the answer is cached, so the NS is not contacted
817 # and the whitelist is not applied (yes, NSIP and NSDNAME are brittle).
818 self.checkNotBlocked('nsip.delegated.example.', singleCheck=True)
819
820 class RPZNSIPCustomTest(RPZRecursorTest):
821 """
822 This test makes sure that the recursor handles custom RPZ rules in a NSIP
823 """
824
825 _confdir = 'RPZNSIPCustom'
826 _lua_config_file = """
827 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."})
828 rpzFile('configs/%s/zone2.rpz', { policyName="zone2.rpz."})
829 """ % (_confdir, _confdir)
830 _config_template = """
831 auth-zones=example=configs/%s/example.zone
832 forward-zones=delegated.example=127.0.0.1:%d
833 """ % (_confdir, rpzAuthServerPort)
834
835 @classmethod
836 def generateRecursorConfig(cls, confdir):
837 authzonepath = os.path.join(confdir, 'example.zone')
838 with open(authzonepath, 'w') as authzone:
839 authzone.write("""$ORIGIN example.
840 @ 3600 IN SOA {soa}
841 """.format(soa=cls._SOA))
842
843 rpzFilePath = os.path.join(confdir, 'zone.rpz')
844 with open(rpzFilePath, 'w') as rpzZone:
845 rpzZone.write("""$ORIGIN zone.rpz.
846 @ 3600 IN SOA {soa}
847 32.1.0.0.127.rpz-nsip.zone.rpz. 60 IN A 192.0.2.1
848 """.format(soa=cls._SOA))
849
850 rpzFilePath = os.path.join(confdir, 'zone2.rpz')
851 with open(rpzFilePath, 'w') as rpzZone:
852 rpzZone.write("""$ORIGIN zone2.rpz.
853 @ 3600 IN SOA {soa}
854 32.1.2.0.192.rpz-ip 60 IN CNAME .
855 """.format(soa=cls._SOA))
856
857 super(RPZNSIPCustomTest, cls).generateRecursorConfig(confdir)
858
859 def testRPZDelegate(self):
860 # The IP of the NS we are going to contact should result in a custom record (192.0.2.1) from zone 1,
861 # so even though the record (192.0.2.1) returned by the server is blacklisted
862 # by zone 2, it should not be blocked.
863 # We only test once because after that the answer is cached, so the NS is not contacted
864 # and the whitelist is not applied (yes, NSIP and NSDNAME are brittle).
865 self.checkCustom('nsip.delegated.example.', 'A', dns.rrset.from_text('nsip.delegated.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.1'))
866
867
868 class RPZResponseIPCNameChainCustomTest(RPZRecursorTest):
869 """
870 This test makes sure that the recursor applies response IP rules to records in a CNAME chain,
871 and resolves the target of a custom CNAME.
872 """
873
874 _confdir = 'RPZResponseIPCNameChainCustom'
875 _lua_config_file = """
876 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."})
877 """ % (_confdir)
878 _config_template = """
879 auth-zones=example=configs/%s/example.zone
880 forward-zones=delegated.example=127.0.0.1:%d
881 """ % (_confdir, rpzAuthServerPort)
882
883 @classmethod
884 def generateRecursorConfig(cls, confdir):
885 authzonepath = os.path.join(confdir, 'example.zone')
886 with open(authzonepath, 'w') as authzone:
887 authzone.write("""$ORIGIN example.
888 @ 3600 IN SOA {soa}
889 name IN CNAME cname
890 cname IN A 192.0.2.255
891 custom-target IN A 192.0.2.254
892 """.format(soa=cls._SOA))
893
894 rpzFilePath = os.path.join(confdir, 'zone.rpz')
895 with open(rpzFilePath, 'w') as rpzZone:
896 rpzZone.write("""$ORIGIN zone.rpz.
897 @ 3600 IN SOA {soa}
898 cname.example IN CNAME custom-target.example.
899 custom-target.example IN A 192.0.2.253
900 """.format(soa=cls._SOA))
901
902 super(RPZResponseIPCNameChainCustomTest, cls).generateRecursorConfig(confdir)
903
904 def testRPZChain(self):
905 # we request the A record for 'name.example.', which is a CNAME to 'cname.example'
906 # this one does exist but we have a RPZ rule that should be triggered,
907 # replacing the 'real' CNAME by a CNAME to 'custom-target.example.'
908 # There is a RPZ rule for that name but it should not be triggered, since
909 # the RPZ specs state "Recall that only one policy rule, from among all those matched at all
910 # stages of resolving a CNAME or DNAME chain, can affect the final
911 # response; this is true even if the selected rule has a PASSTHRU
912 # action" in 5.1 "CNAME or DNAME Chain Position" Precedence Rule
913
914 # two times to check the cache
915 for _ in range(2):
916 query = dns.message.make_query('name.example.', 'A', want_dnssec=True)
917 query.flags |= dns.flags.CD
918 for method in ("sendUDPQuery", "sendTCPQuery"):
919 sender = getattr(self, method)
920 res = sender(query)
921 self.assertRcodeEqual(res, dns.rcode.NOERROR)
922 self.assertRRsetInAnswer(res, dns.rrset.from_text('name.example.', 0, dns.rdataclass.IN, 'CNAME', 'cname.example.'))
923 self.assertRRsetInAnswer(res, dns.rrset.from_text('cname.example.', 0, dns.rdataclass.IN, 'CNAME', 'custom-target.example.'))
924 self.assertRRsetInAnswer(res, dns.rrset.from_text('custom-target.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.254'))
925
926
927 class RPZCNameChainCustomTest(RPZRecursorTest):
928 """
929 This test makes sure that the recursor applies QName rules to names in a CNAME chain.
930 No forward or internal auth zones here, as we want to test the real resolution
931 (with QName Minimization).
932 """
933
934 _PREFIX = os.environ['PREFIX']
935 _confdir = 'RPZCNameChainCustom'
936 _lua_config_file = """
937 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."})
938 """ % (_confdir)
939 _config_template = ""
940
941 @classmethod
942 def setUpClass(cls):
943
944 cls.setUpSockets()
945 cls.startResponders()
946
947 confdir = os.path.join('configs', cls._confdir)
948 cls.createConfigDir(confdir)
949
950 cls.generateAllAuthConfig(confdir)
951 cls.startAuth(os.path.join(confdir, "auth-8"), cls._PREFIX + '.8')
952 cls.startAuth(os.path.join(confdir, "auth-10"), cls._PREFIX + '.10')
953
954 cls.generateRecursorConfig(confdir)
955 cls.startRecursor(confdir, cls._recursorPort)
956
957 @classmethod
958 def tearDownClass(cls):
959 cls.tearDownAuth()
960 cls.tearDownRecursor()
961
962 @classmethod
963 def generateRecursorConfig(cls, confdir):
964 rpzFilePath = os.path.join(confdir, 'zone.rpz')
965 with open(rpzFilePath, 'w') as rpzZone:
966 rpzZone.write("""$ORIGIN zone.rpz.
967 @ 3600 IN SOA {soa}
968 32.100.2.0.192.rpz-ip IN CNAME .
969 32.101.2.0.192.rpz-ip IN CNAME *.
970 32.102.2.0.192.rpz-ip IN A 192.0.2.103
971 """.format(soa=cls._SOA))
972
973 super(RPZCNameChainCustomTest, cls).generateRecursorConfig(confdir)
974
975 def testRPZChainNXD(self):
976 # we should match the A at the end of the CNAME chain and
977 # trigger a NXD
978
979 # two times to check the cache
980 for _ in range(2):
981 query = dns.message.make_query('cname-nxd.example.', 'A', want_dnssec=True)
982 query.flags |= dns.flags.CD
983 for method in ("sendUDPQuery", "sendTCPQuery"):
984 sender = getattr(self, method)
985 res = sender(query)
986 self.assertRcodeEqual(res, dns.rcode.NXDOMAIN)
987 self.assertEquals(len(res.answer), 0)
988
989 def testRPZChainNODATA(self):
990 # we should match the A at the end of the CNAME chain and
991 # trigger a NODATA
992
993 # two times to check the cache
994 for _ in range(2):
995 query = dns.message.make_query('cname-nodata.example.', 'A', want_dnssec=True)
996 query.flags |= dns.flags.CD
997 for method in ("sendUDPQuery", "sendTCPQuery"):
998 sender = getattr(self, method)
999 res = sender(query)
1000 self.assertRcodeEqual(res, dns.rcode.NOERROR)
1001 self.assertEquals(len(res.answer), 0)
1002
1003 def testRPZChainCustom(self):
1004 # we should match the A at the end of the CNAME chain and
1005 # get a custom A, replacing the existing one
1006
1007 # two times to check the cache
1008 for _ in range(2):
1009 query = dns.message.make_query('cname-custom-a.example.', 'A', want_dnssec=True)
1010 query.flags |= dns.flags.CD
1011 for method in ("sendUDPQuery", "sendTCPQuery"):
1012 sender = getattr(self, method)
1013 res = sender(query)
1014 self.assertRcodeEqual(res, dns.rcode.NOERROR)
1015 # the original CNAME record is signed
1016 self.assertEquals(len(res.answer), 3)
1017 self.assertRRsetInAnswer(res, dns.rrset.from_text('cname-custom-a.example.', 0, dns.rdataclass.IN, 'CNAME', 'cname-custom-a-target.example.'))
1018 self.assertRRsetInAnswer(res, dns.rrset.from_text('cname-custom-a-target.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.103'))