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