]> git.ipfire.org Git - thirdparty/pdns.git/blame - regression-tests.recursor-dnssec/test_RPZ.py
rec: Add a regression test for the RPZ updates with several deltas
[thirdparty/pdns.git] / regression-tests.recursor-dnssec / test_RPZ.py
CommitLineData
f9017ec1 1import dns
22cf3506 2import json
f9017ec1 3import os
22cf3506 4import requests
f9017ec1
RG
5import socket
6import struct
7import sys
8import threading
9import time
10
11from recursortests import RecursorTest
12
13class RPZServer(object):
14
15 def __init__(self, port):
16 self._currentSerial = 0
17 self._targetSerial = 1
18 self._serverPort = port
19 listener = threading.Thread(name='RPZ Listener', target=self._listener, args=[])
20 listener.setDaemon(True)
21 listener.start()
22
23 def getCurrentSerial(self):
24 return self._currentSerial
25
26 def moveToSerial(self, newSerial):
27 if newSerial == self._currentSerial:
28 return False
29
30 if newSerial != self._currentSerial + 1:
ba5f46ae 31 raise AssertionError("Asking the RPZ server to serve serial %d, already serving %d" % (newSerial, self._currentSerial))
f9017ec1
RG
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
ee2a5356
RG
55 # special case for the 9th update, which might get skipped
56 if oldSerial != self._currentSerial and self._currentSerial != 9:
f9017ec1
RG
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 ]
22cf3506
RG
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'),
8340237f
RG
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.'),
22cf3506
RG
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'),
8340237f
RG
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.'),
22cf3506 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),
6da513b2
RG
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'),
d13c4d18
RG
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.'),
22cf3506
RG
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 ]
98b33176
RG
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 ]
ee2a5356
RG
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:
ba5f46ae 139 # full AXFR to make sure we are removing the duplicate, adding a record, to check that the update was correctly applied
ee2a5356
RG
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 ]
ba5f46ae
RG
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
f9017ec1
RG
160
161 response.answer = records
162 return (newSerial, response)
163
164 def _connectionHandler(self, conn):
165 data = None
166 while True:
167 data = conn.recv(2)
168 if not data:
169 break
170 (datalen,) = struct.unpack("!H", data)
171 data = conn.recv(datalen)
172 if not data:
173 break
174
175 message = dns.message.from_wire(data)
176 if len(message.question) != 1:
177 print('Invalid RPZ query, qdcount is %d' % (len(message.question)))
178 break
179 if not message.question[0].rdtype in [dns.rdatatype.AXFR, dns.rdatatype.IXFR]:
180 print('Invalid RPZ query, qtype is %d' % (message.question.rdtype))
181 break
182 (serial, answer) = self._getAnswer(message)
183 if not answer:
184 print('Unable to get a response for %s %d' % (message.question[0].name, message.question[0].rdtype))
185 break
186
187 wire = answer.to_wire()
188 conn.send(struct.pack("!H", len(wire)))
189 conn.send(wire)
190 self._currentSerial = serial
191 break
192
193 conn.close()
194
195 def _listener(self):
196 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
197 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
198 try:
199 sock.bind(("127.0.0.1", self._serverPort))
200 except socket.error as e:
201 print("Error binding in the RPZ listener: %s" % str(e))
202 sys.exit(1)
203
204 sock.listen(100)
205 while True:
206 try:
207 (conn, _) = sock.accept()
208 thread = threading.Thread(name='RPZ Connection Handler',
209 target=self._connectionHandler,
210 args=[conn])
211 thread.setDaemon(True)
212 thread.start()
213
214 except socket.error as e:
215 print('Error in RPZ socket: %s' % str(e))
216 sock.close()
217
f9017ec1 218class RPZRecursorTest(RecursorTest):
22cf3506
RG
219 _wsPort = 8042
220 _wsTimeout = 2
221 _wsPassword = 'secretpassword'
222 _apiKey = 'secretapikey'
f9017ec1 223 _confdir = 'RPZ'
d19bcbf0
RG
224 _lua_dns_script_file = """
225
226 function prerpz(dq)
227 -- disable the RPZ policy named 'zone.rpz' for AD=1 queries
228 if dq:getDH():getAD() then
229 dq:discardPolicy('zone.rpz.')
230 end
231 return false
232 end
233 """
234
f9017ec1 235 _config_template = """
22cf3506
RG
236auth-zones=example=configs/%s/example.zone
237webserver=yes
238webserver-port=%d
239webserver-address=127.0.0.1
240webserver-password=%s
241api-key=%s
98b33176 242log-rpz-changes=yes
22cf3506 243""" % (_confdir, _wsPort, _wsPassword, _apiKey)
f9017ec1
RG
244
245 @classmethod
246 def setUpClass(cls):
247
248 cls.setUpSockets()
249 cls.startResponders()
250
251 confdir = os.path.join('configs', cls._confdir)
252 cls.createConfigDir(confdir)
253
254 cls.generateRecursorConfig(confdir)
255 cls.startRecursor(confdir, cls._recursorPort)
256
257 @classmethod
258 def tearDownClass(cls):
259 cls.tearDownRecursor()
260
f89ae456 261 def checkBlocked(self, name, shouldBeBlocked=True, adQuery=False, singleCheck=False):
f9017ec1
RG
262 query = dns.message.make_query(name, 'A', want_dnssec=True)
263 query.flags |= dns.flags.CD
d19bcbf0
RG
264 if adQuery:
265 query.flags |= dns.flags.AD
f9017ec1 266
d13c4d18
RG
267 for method in ("sendUDPQuery", "sendTCPQuery"):
268 sender = getattr(self, method)
269 res = sender(query)
270 self.assertRcodeEqual(res, dns.rcode.NOERROR)
271 if shouldBeBlocked:
272 expected = dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.1')
273 else:
274 expected = dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.42')
275
276 self.assertRRsetInAnswer(res, expected)
f89ae456
RG
277 if singleCheck:
278 break
f9017ec1 279
f89ae456
RG
280 def checkNotBlocked(self, name, adQuery=False, singleCheck=False):
281 self.checkBlocked(name, False, adQuery, singleCheck)
f9017ec1 282
6da513b2
RG
283 def checkCustom(self, qname, qtype, expected):
284 query = dns.message.make_query(qname, qtype, want_dnssec=True)
285 query.flags |= dns.flags.CD
d13c4d18
RG
286 for method in ("sendUDPQuery", "sendTCPQuery"):
287 sender = getattr(self, method)
288 res = sender(query)
289 self.assertRcodeEqual(res, dns.rcode.NOERROR)
290 self.assertRRsetInAnswer(res, expected)
6da513b2
RG
291
292 def checkNoData(self, qname, qtype):
d13c4d18
RG
293 query = dns.message.make_query(qname, qtype, want_dnssec=True)
294 query.flags |= dns.flags.CD
295 for method in ("sendUDPQuery", "sendTCPQuery"):
296 sender = getattr(self, method)
297 res = sender(query)
298 self.assertRcodeEqual(res, dns.rcode.NOERROR)
299 self.assertEqual(len(res.answer), 0)
300
98b33176 301 def checkNXD(self, qname, qtype='A'):
d122dac0
RG
302 query = dns.message.make_query(qname, qtype, want_dnssec=True)
303 query.flags |= dns.flags.CD
304 for method in ("sendUDPQuery", "sendTCPQuery"):
305 sender = getattr(self, method)
306 res = sender(query)
307 self.assertRcodeEqual(res, dns.rcode.NXDOMAIN)
308 self.assertEqual(len(res.answer), 0)
309 self.assertEqual(len(res.authority), 1)
310
d13c4d18 311 def checkTruncated(self, qname, qtype='A'):
6da513b2
RG
312 query = dns.message.make_query(qname, qtype, want_dnssec=True)
313 query.flags |= dns.flags.CD
314 res = self.sendUDPQuery(query)
d13c4d18
RG
315 self.assertRcodeEqual(res, dns.rcode.NOERROR)
316 self.assertMessageHasFlags(res, ['QR', 'RA', 'RD', 'CD', 'TC'])
317 self.assertEqual(len(res.answer), 0)
318 self.assertEqual(len(res.authority), 0)
319 self.assertEqual(len(res.additional), 0)
6da513b2 320
d13c4d18
RG
321 res = self.sendTCPQuery(query)
322 self.assertRcodeEqual(res, dns.rcode.NXDOMAIN)
323 self.assertMessageHasFlags(res, ['QR', 'RA', 'RD', 'CD'])
6da513b2 324 self.assertEqual(len(res.answer), 0)
d13c4d18
RG
325 self.assertEqual(len(res.authority), 1)
326 self.assertEqual(len(res.additional), 0)
327
328 def checkDropped(self, qname, qtype='A'):
329 query = dns.message.make_query(qname, qtype, want_dnssec=True)
330 query.flags |= dns.flags.CD
331 for method in ("sendUDPQuery", "sendTCPQuery"):
332 sender = getattr(self, method)
333 res = sender(query)
334 self.assertEqual(res, None)
6da513b2 335
d122dac0
RG
336 def checkRPZStats(self, serial, recordsCount, fullXFRCount, totalXFRCount):
337 headers = {'x-api-key': self._apiKey}
338 url = 'http://127.0.0.1:' + str(self._wsPort) + '/api/v1/servers/localhost/rpzstatistics'
339 r = requests.get(url, headers=headers, timeout=self._wsTimeout)
340 self.assertTrue(r)
341 self.assertEquals(r.status_code, 200)
342 self.assertTrue(r.json())
343 content = r.json()
344 self.assertIn('zone.rpz.', content)
345 zone = content['zone.rpz.']
346 for key in ['last_update', 'records', 'serial', 'transfers_failed', 'transfers_full', 'transfers_success']:
347 self.assertIn(key, zone)
348
349 self.assertEquals(zone['serial'], serial)
350 self.assertEquals(zone['records'], recordsCount)
351 self.assertEquals(zone['transfers_full'], fullXFRCount)
352 self.assertEquals(zone['transfers_success'], totalXFRCount)
353
354rpzServerPort = 4250
355rpzServer = RPZServer(rpzServerPort)
356
357class RPZXFRRecursorTest(RPZRecursorTest):
358 """
359 This test makes sure that we correctly update RPZ zones via AXFR then IXFR
360 """
361
362 global rpzServerPort
363 _lua_config_file = """
364 -- The first server is a bogus one, to test that we correctly fail over to the second one
365 rpzMaster({'127.0.0.1:9999', '127.0.0.1:%d'}, 'zone.rpz.', { refresh=1 })
366 """ % (rpzServerPort)
367 _confdir = 'RPZXFR'
368 _wsPort = 8042
369 _wsTimeout = 2
370 _wsPassword = 'secretpassword'
371 _apiKey = 'secretapikey'
372 _config_template = """
373auth-zones=example=configs/%s/example.zone
374webserver=yes
375webserver-port=%d
376webserver-address=127.0.0.1
377webserver-password=%s
378api-key=%s
379""" % (_confdir, _wsPort, _wsPassword, _apiKey)
380 _xfrDone = 0
381
382 @classmethod
383 def generateRecursorConfig(cls, confdir):
384 authzonepath = os.path.join(confdir, 'example.zone')
385 with open(authzonepath, 'w') as authzone:
386 authzone.write("""$ORIGIN example.
387@ 3600 IN SOA {soa}
388a 3600 IN A 192.0.2.42
389b 3600 IN A 192.0.2.42
390c 3600 IN A 192.0.2.42
391d 3600 IN A 192.0.2.42
392e 3600 IN A 192.0.2.42
393""".format(soa=cls._SOA))
394 super(RPZRecursorTest, cls).generateRecursorConfig(confdir)
395
f9017ec1
RG
396 def waitUntilCorrectSerialIsLoaded(self, serial, timeout=5):
397 global rpzServer
398
399 rpzServer.moveToSerial(serial)
400
401 attempts = 0
402 while attempts < timeout:
403 currentSerial = rpzServer.getCurrentSerial()
404 if currentSerial > serial:
405 raise AssertionError("Expected serial %d, got %d" % (serial, currentSerial))
406 if currentSerial == serial:
22cf3506 407 self._xfrDone = self._xfrDone + 1
f9017ec1
RG
408 return
409
410 attempts = attempts + 1
411 time.sleep(1)
412
413 raise AssertionError("Waited %d seconds for the serial to be updated to %d but the serial is still %d" % (timeout, serial, currentSerial))
414
415 def testRPZ(self):
416 # first zone, only a should be blocked
417 self.waitUntilCorrectSerialIsLoaded(1)
22cf3506 418 self.checkRPZStats(1, 1, 1, self._xfrDone)
f9017ec1
RG
419 self.checkBlocked('a.example.')
420 self.checkNotBlocked('b.example.')
421 self.checkNotBlocked('c.example.')
422
423 # second zone, a and b should be blocked
424 self.waitUntilCorrectSerialIsLoaded(2)
22cf3506 425 self.checkRPZStats(2, 2, 1, self._xfrDone)
f9017ec1
RG
426 self.checkBlocked('a.example.')
427 self.checkBlocked('b.example.')
428 self.checkNotBlocked('c.example.')
429
430 # third zone, only b should be blocked
431 self.waitUntilCorrectSerialIsLoaded(3)
22cf3506 432 self.checkRPZStats(3, 1, 1, self._xfrDone)
f9017ec1
RG
433 self.checkNotBlocked('a.example.')
434 self.checkBlocked('b.example.')
435 self.checkNotBlocked('c.example.')
436
437 # fourth zone, only c should be blocked
438 self.waitUntilCorrectSerialIsLoaded(4)
22cf3506 439 self.checkRPZStats(4, 1, 1, self._xfrDone)
f9017ec1
RG
440 self.checkNotBlocked('a.example.')
441 self.checkNotBlocked('b.example.')
442 self.checkBlocked('c.example.')
22cf3506
RG
443
444 # fifth zone, we should get a full AXFR this time, and only d should be blocked
445 self.waitUntilCorrectSerialIsLoaded(5)
8340237f 446 self.checkRPZStats(5, 3, 2, self._xfrDone)
22cf3506
RG
447 self.checkNotBlocked('a.example.')
448 self.checkNotBlocked('b.example.')
449 self.checkNotBlocked('c.example.')
450 self.checkBlocked('d.example.')
451
6da513b2 452 # sixth zone, only e should be blocked, f is a local data record
22cf3506 453 self.waitUntilCorrectSerialIsLoaded(6)
6da513b2
RG
454 self.checkRPZStats(6, 2, 2, self._xfrDone)
455 self.checkNotBlocked('a.example.')
456 self.checkNotBlocked('b.example.')
457 self.checkNotBlocked('c.example.')
458 self.checkNotBlocked('d.example.')
459 self.checkCustom('e.example.', 'A', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.1', '192.0.2.2'))
460 self.checkCustom('e.example.', 'MX', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'MX', '10 mx.example.'))
461 self.checkNoData('e.example.', 'AAAA')
462 self.checkCustom('f.example.', 'A', dns.rrset.from_text('f.example.', 0, dns.rdataclass.IN, 'CNAME', 'e.example.'))
463
464 # seventh zone, e should only have one A
465 self.waitUntilCorrectSerialIsLoaded(7)
d13c4d18 466 self.checkRPZStats(7, 4, 2, self._xfrDone)
22cf3506
RG
467 self.checkNotBlocked('a.example.')
468 self.checkNotBlocked('b.example.')
469 self.checkNotBlocked('c.example.')
470 self.checkNotBlocked('d.example.')
6da513b2
RG
471 self.checkCustom('e.example.', 'A', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.2'))
472 self.checkCustom('e.example.', 'MX', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'MX', '10 mx.example.'))
473 self.checkNoData('e.example.', 'AAAA')
474 self.checkCustom('f.example.', 'A', dns.rrset.from_text('f.example.', 0, dns.rdataclass.IN, 'CNAME', 'e.example.'))
d19bcbf0
RG
475 # check that the policy is disabled for AD=1 queries
476 self.checkNotBlocked('e.example.', True)
d13c4d18
RG
477 # check non-custom policies
478 self.checkTruncated('tc.example.')
479 self.checkDropped('drop.example.')
d122dac0 480
98b33176
RG
481 # eighth zone, all entries should be gone
482 self.waitUntilCorrectSerialIsLoaded(8)
483 self.checkRPZStats(8, 0, 3, self._xfrDone)
484 self.checkNotBlocked('a.example.')
485 self.checkNotBlocked('b.example.')
486 self.checkNotBlocked('c.example.')
487 self.checkNotBlocked('d.example.')
488 self.checkNotBlocked('e.example.')
489 self.checkNXD('f.example.')
490 self.checkNXD('tc.example.')
ee2a5356
RG
491 self.checkNXD('drop.example.')
492
493 # 9th zone is a duplicate, it might get skipped
494 global rpzServer
495 rpzServer.moveToSerial(9)
496 time.sleep(3)
497 self.waitUntilCorrectSerialIsLoaded(10)
498 self.checkRPZStats(10, 1, 4, self._xfrDone)
499 self.checkNotBlocked('a.example.')
500 self.checkNotBlocked('b.example.')
501 self.checkNotBlocked('c.example.')
502 self.checkNotBlocked('d.example.')
503 self.checkNotBlocked('e.example.')
504 self.checkBlocked('f.example.')
505 self.checkNXD('tc.example.')
ba5f46ae
RG
506 self.checkNXD('drop.example.')
507
508 # the next update will update the zone twice
509 rpzServer.moveToSerial(11)
510 time.sleep(3)
511 self.waitUntilCorrectSerialIsLoaded(12)
512 self.checkRPZStats(12, 1, 4, self._xfrDone)
513 self.checkNotBlocked('a.example.')
514 self.checkNotBlocked('b.example.')
515 self.checkNotBlocked('c.example.')
516 self.checkNotBlocked('d.example.')
517 self.checkNotBlocked('e.example.')
518 self.checkNXD('f.example.')
519 self.checkBlocked('g.example.')
520 self.checkNXD('tc.example.')
98b33176
RG
521 self.checkNXD('drop.example.')
522
d122dac0
RG
523class RPZFileRecursorTest(RPZRecursorTest):
524 """
525 This test makes sure that we correctly load RPZ zones from a file
526 """
527
528 _confdir = 'RPZFile'
d122dac0
RG
529 _lua_config_file = """
530 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz." })
531 """ % (_confdir)
532 _config_template = """
533auth-zones=example=configs/%s/example.zone
f89ae456 534""" % (_confdir)
d122dac0
RG
535
536 @classmethod
537 def generateRecursorConfig(cls, confdir):
538 authzonepath = os.path.join(confdir, 'example.zone')
539 with open(authzonepath, 'w') as authzone:
540 authzone.write("""$ORIGIN example.
541@ 3600 IN SOA {soa}
542a 3600 IN A 192.0.2.42
543b 3600 IN A 192.0.2.42
544c 3600 IN A 192.0.2.42
545d 3600 IN A 192.0.2.42
546e 3600 IN A 192.0.2.42
547z 3600 IN A 192.0.2.42
548""".format(soa=cls._SOA))
549
550 rpzFilePath = os.path.join(confdir, 'zone.rpz')
551 with open(rpzFilePath, 'w') as rpzZone:
552 rpzZone.write("""$ORIGIN zone.rpz.
553@ 3600 IN SOA {soa}
554a.example.zone.rpz. 60 IN A 192.0.2.42
555a.example.zone.rpz. 60 IN A 192.0.2.43
556a.example.zone.rpz. 60 IN TXT "some text"
557drop.example.zone.rpz. 60 IN CNAME rpz-drop.
558z.example.zone.rpz. 60 IN A 192.0.2.1
559tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
560""".format(soa=cls._SOA))
561 super(RPZFileRecursorTest, cls).generateRecursorConfig(confdir)
562
563 def testRPZ(self):
564 self.checkCustom('a.example.', 'A', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.42', '192.0.2.43'))
565 self.checkCustom('a.example.', 'TXT', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'TXT', '"some text"'))
566 self.checkBlocked('z.example.')
567 self.checkNotBlocked('b.example.')
568 self.checkNotBlocked('c.example.')
569 self.checkNotBlocked('d.example.')
570 self.checkNotBlocked('e.example.')
571 # check that the policy is disabled for AD=1 queries
572 self.checkNotBlocked('z.example.', True)
573 # check non-custom policies
574 self.checkTruncated('tc.example.')
575 self.checkDropped('drop.example.')
576
577class RPZFileDefaultPolRecursorTest(RPZRecursorTest):
578 """
579 This test makes sure that we correctly load RPZ zones from a file with a default policy
580 """
581
582 _confdir = 'RPZFileDefaultPolicy'
d122dac0
RG
583 _lua_config_file = """
584 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction })
585 """ % (_confdir)
586 _config_template = """
587auth-zones=example=configs/%s/example.zone
f89ae456 588""" % (_confdir)
d122dac0
RG
589
590 @classmethod
591 def generateRecursorConfig(cls, confdir):
592 authzonepath = os.path.join(confdir, 'example.zone')
593 with open(authzonepath, 'w') as authzone:
594 authzone.write("""$ORIGIN example.
595@ 3600 IN SOA {soa}
596a 3600 IN A 192.0.2.42
597b 3600 IN A 192.0.2.42
598c 3600 IN A 192.0.2.42
599d 3600 IN A 192.0.2.42
600drop 3600 IN A 192.0.2.42
601e 3600 IN A 192.0.2.42
602z 3600 IN A 192.0.2.42
603""".format(soa=cls._SOA))
604
605 rpzFilePath = os.path.join(confdir, 'zone.rpz')
606 with open(rpzFilePath, 'w') as rpzZone:
607 rpzZone.write("""$ORIGIN zone.rpz.
608@ 3600 IN SOA {soa}
609a.example.zone.rpz. 60 IN A 192.0.2.42
610drop.example.zone.rpz. 60 IN CNAME rpz-drop.
611z.example.zone.rpz. 60 IN A 192.0.2.1
612tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
613""".format(soa=cls._SOA))
614 super(RPZFileDefaultPolRecursorTest, cls).generateRecursorConfig(confdir)
615
616 def testRPZ(self):
617 # local data entries are overridden by default
618 self.checkCustom('a.example.', 'A', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.42'))
619 self.checkNoData('a.example.', 'TXT')
620 # will not be blocked because the default policy overrides local data entries by default
621 self.checkNotBlocked('z.example.')
622 self.checkNotBlocked('b.example.')
623 self.checkNotBlocked('c.example.')
624 self.checkNotBlocked('d.example.')
625 self.checkNotBlocked('e.example.')
626 # check non-local policies, they should be overridden by the default policy
627 self.checkNXD('tc.example.', 'A')
628 self.checkNotBlocked('drop.example.')
629
630class RPZFileDefaultPolNotOverrideLocalRecursorTest(RPZRecursorTest):
631 """
632 This test makes sure that we correctly load RPZ zones from a file with a default policy, not overriding local data entries
633 """
634
635 _confdir = 'RPZFileDefaultPolicyNotOverrideLocal'
d122dac0
RG
636 _lua_config_file = """
637 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction, defpolOverrideLocalData=false })
638 """ % (_confdir)
639 _config_template = """
640auth-zones=example=configs/%s/example.zone
f89ae456 641""" % (_confdir)
d122dac0
RG
642
643 @classmethod
644 def generateRecursorConfig(cls, confdir):
645 authzonepath = os.path.join(confdir, 'example.zone')
646 with open(authzonepath, 'w') as authzone:
647 authzone.write("""$ORIGIN example.
648@ 3600 IN SOA {soa}
649a 3600 IN A 192.0.2.42
650b 3600 IN A 192.0.2.42
651c 3600 IN A 192.0.2.42
652d 3600 IN A 192.0.2.42
653drop 3600 IN A 192.0.2.42
654e 3600 IN A 192.0.2.42
655z 3600 IN A 192.0.2.42
656""".format(soa=cls._SOA))
657
658 rpzFilePath = os.path.join(confdir, 'zone.rpz')
659 with open(rpzFilePath, 'w') as rpzZone:
660 rpzZone.write("""$ORIGIN zone.rpz.
661@ 3600 IN SOA {soa}
662a.example.zone.rpz. 60 IN A 192.0.2.42
663a.example.zone.rpz. 60 IN A 192.0.2.43
664a.example.zone.rpz. 60 IN TXT "some text"
665drop.example.zone.rpz. 60 IN CNAME rpz-drop.
666z.example.zone.rpz. 60 IN A 192.0.2.1
667tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
668""".format(soa=cls._SOA))
669 super(RPZFileDefaultPolNotOverrideLocalRecursorTest, cls).generateRecursorConfig(confdir)
670
671 def testRPZ(self):
ef2ea4bf 672 # local data entries will not be overridden by the default policy
d122dac0
RG
673 self.checkCustom('a.example.', 'A', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.42', '192.0.2.43'))
674 self.checkCustom('a.example.', 'TXT', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'TXT', '"some text"'))
675 # will be blocked because the default policy does not override local data entries
676 self.checkBlocked('z.example.')
677 self.checkNotBlocked('b.example.')
678 self.checkNotBlocked('c.example.')
679 self.checkNotBlocked('d.example.')
680 self.checkNotBlocked('e.example.')
681 # check non-local policies, they should be overridden by the default policy
682 self.checkNXD('tc.example.', 'A')
683 self.checkNotBlocked('drop.example.')
1d2777e9 684
f89ae456
RG
685class RPZSimpleAuthServer(object):
686
687 def __init__(self, port):
688 self._serverPort = port
689 listener = threading.Thread(name='RPZ Simple Auth Listener', target=self._listener, args=[])
690 listener.setDaemon(True)
691 listener.start()
692
693 def _getAnswer(self, message):
694
695 response = dns.message.make_response(message)
696 response.flags |= dns.flags.AA
697 records = [
698 dns.rrset.from_text('nsip.delegated.example.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.42')
699 ]
700
701 response.answer = records
702 return response
703
704 def _listener(self):
705 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
706 try:
707 sock.bind(("127.0.0.1", self._serverPort))
708 except socket.error as e:
709 print("Error binding in the RPZ simple auth listener: %s" % str(e))
710 sys.exit(1)
711
712 while True:
713 try:
714 data, addr = sock.recvfrom(4096)
715 message = dns.message.from_wire(data)
716 if len(message.question) != 1:
717 print('Invalid query, qdcount is %d' % (len(message.question)))
718 break
719
720 answer = self._getAnswer(message)
721 if not answer:
722 print('Unable to get a response for %s %d' % (message.question[0].name, message.question[0].rdtype))
723 break
724
725 wire = answer.to_wire()
726 sock.sendto(wire, addr)
727
728 except socket.error as e:
729 print('Error in RPZ simple auth socket: %s' % str(e))
730
731rpzAuthServerPort = 4260
732rpzAuthServer = RPZSimpleAuthServer(rpzAuthServerPort)
733
734class RPZOrderingPrecedenceRecursorTest(RPZRecursorTest):
1d2777e9
RG
735 """
736 This test makes sure that the recursor respects the RPZ ordering precedence rules
737 """
738
739 _confdir = 'RPZOrderingPrecedence'
1d2777e9
RG
740 _lua_config_file = """
741 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."})
742 rpzFile('configs/%s/zone2.rpz', { policyName="zone2.rpz."})
743 """ % (_confdir, _confdir)
744 _config_template = """
745auth-zones=example=configs/%s/example.zone
f89ae456
RG
746forward-zones=delegated.example=127.0.0.1:%d
747""" % (_confdir, rpzAuthServerPort)
1d2777e9
RG
748
749 @classmethod
750 def generateRecursorConfig(cls, confdir):
751 authzonepath = os.path.join(confdir, 'example.zone')
752 with open(authzonepath, 'w') as authzone:
753 authzone.write("""$ORIGIN example.
754@ 3600 IN SOA {soa}
755sub.test 3600 IN A 192.0.2.42
fa973749
RG
756passthru-then-blocked-by-higher 3600 IN A 192.0.2.66
757passthru-then-blocked-by-same 3600 IN A 192.0.2.66
758blocked-then-passhtru-by-higher 3600 IN A 192.0.2.100
1d2777e9
RG
759""".format(soa=cls._SOA))
760
761 rpzFilePath = os.path.join(confdir, 'zone.rpz')
762 with open(rpzFilePath, 'w') as rpzZone:
763 rpzZone.write("""$ORIGIN zone.rpz.
764@ 3600 IN SOA {soa}
765*.test.example.zone.rpz. 60 IN CNAME rpz-passthru.
fa973749
RG
76632.66.2.0.192.rpz-ip.zone.rpz. 60 IN A 192.0.2.1
76732.100.2.0.192.rpz-ip.zone.rpz. 60 IN CNAME rpz-passthru.
768passthru-then-blocked-by-same.example.zone.rpz. 60 IN CNAME rpz-passthru.
f89ae456 76932.1.0.0.127.rpz-nsip.zone.rpz. 60 IN CNAME rpz-passthru.
1d2777e9
RG
770""".format(soa=cls._SOA))
771
772 rpzFilePath = os.path.join(confdir, 'zone2.rpz')
773 with open(rpzFilePath, 'w') as rpzZone:
774 rpzZone.write("""$ORIGIN zone2.rpz.
775@ 3600 IN SOA {soa}
776sub.test.example.com.zone2.rpz. 60 IN CNAME .
fa973749
RG
777passthru-then-blocked-by-higher.example.zone2.rpz. 60 IN CNAME rpz-passthru.
778blocked-then-passhtru-by-higher.example.zone2.rpz. 60 IN A 192.0.2.1
1d2777e9
RG
77932.42.2.0.192.rpz-ip 60 IN CNAME .
780""".format(soa=cls._SOA))
781
f89ae456 782 super(RPZOrderingPrecedenceRecursorTest, cls).generateRecursorConfig(confdir)
1d2777e9 783
fa973749 784 def testRPZOrderingForQNameAndWhitelisting(self):
1d2777e9
RG
785 # we should first match on the qname (the wildcard, not on the exact name since
786 # we respect the order of the RPZ zones), see the pass-thru rule
fa973749
RG
787 # and only process RPZ rules of higher precedence.
788 # The subsequent rule on the content of the A should therefore not trigger a NXDOMAIN.
1d2777e9 789 self.checkNotBlocked('sub.test.example.')
fa973749
RG
790
791 def testRPZOrderingWhitelistedThenBlockedByHigher(self):
792 # we should first match on the qname from the second RPZ zone,
793 # continue the resolution process, and get blocked by the content of the A record
794 # based on the first RPZ zone, whose priority is higher than the second one.
795 self.checkBlocked('passthru-then-blocked-by-higher.example.')
796
797 def testRPZOrderingWhitelistedThenBlockedBySame(self):
798 # we should first match on the qname from the first RPZ zone,
799 # continue the resolution process, and NOT get blocked by the content of the A record
800 # based on the same RPZ zone, since it's not higher.
801 self.checkCustom('passthru-then-blocked-by-same.example.', 'A', dns.rrset.from_text('passthru-then-blocked-by-same.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.66'))
802
803 def testRPZOrderBlockedThenWhitelisted(self):
804 # The qname is first blocked by the second RPZ zone
805 # Then, should the resolution process go on, the A record would be whitelisted
806 # by the first zone.
807 # This is what the RPZ specification requires, but we currently decided that we
808 # don't want to leak queries to malicious DNS servers and waste time if the qname is blacklisted.
809 # We might change our opinion at some point, though.
810 self.checkBlocked('blocked-then-passhtru-by-higher.example.')
f89ae456
RG
811
812 def testRPZOrderDelegate(self):
813 # The IP of the NS we are going to contact is whitelisted (passthru) in zone 1,
814 # so even though the record (192.0.2.42) returned by the server is blacklisted
815 # by zone 2, it should not be blocked.
816 # We only test once because after that the answer is cached, so the NS is not contacted
817 # and the whitelist is not applied (yes, NSIP and NSDNAME are brittle).
818 self.checkNotBlocked('nsip.delegated.example.', singleCheck=True)
819
820class RPZNSIPCustomTest(RPZRecursorTest):
821 """
822 This test makes sure that the recursor handles custom RPZ rules in a NSIP
823 """
824
825 _confdir = 'RPZNSIPCustom'
826 _lua_config_file = """
827 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."})
828 rpzFile('configs/%s/zone2.rpz', { policyName="zone2.rpz."})
829 """ % (_confdir, _confdir)
830 _config_template = """
831auth-zones=example=configs/%s/example.zone
832forward-zones=delegated.example=127.0.0.1:%d
833""" % (_confdir, rpzAuthServerPort)
834
835 @classmethod
836 def generateRecursorConfig(cls, confdir):
837 authzonepath = os.path.join(confdir, 'example.zone')
838 with open(authzonepath, 'w') as authzone:
839 authzone.write("""$ORIGIN example.
840@ 3600 IN SOA {soa}
841""".format(soa=cls._SOA))
842
843 rpzFilePath = os.path.join(confdir, 'zone.rpz')
844 with open(rpzFilePath, 'w') as rpzZone:
845 rpzZone.write("""$ORIGIN zone.rpz.
846@ 3600 IN SOA {soa}
84732.1.0.0.127.rpz-nsip.zone.rpz. 60 IN A 192.0.2.1
848""".format(soa=cls._SOA))
849
850 rpzFilePath = os.path.join(confdir, 'zone2.rpz')
851 with open(rpzFilePath, 'w') as rpzZone:
852 rpzZone.write("""$ORIGIN zone2.rpz.
853@ 3600 IN SOA {soa}
85432.1.2.0.192.rpz-ip 60 IN CNAME .
855""".format(soa=cls._SOA))
856
857 super(RPZNSIPCustomTest, cls).generateRecursorConfig(confdir)
858
859 def testRPZDelegate(self):
860 # The IP of the NS we are going to contact should result in a custom record (192.0.2.1) from zone 1,
861 # so even though the record (192.0.2.1) returned by the server is blacklisted
862 # by zone 2, it should not be blocked.
863 # We only test once because after that the answer is cached, so the NS is not contacted
864 # and the whitelist is not applied (yes, NSIP and NSDNAME are brittle).
865 self.checkCustom('nsip.delegated.example.', 'A', dns.rrset.from_text('nsip.delegated.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.1'))