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