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