]> git.ipfire.org Git - thirdparty/pdns.git/blame - regression-tests.recursor-dnssec/test_RPZ.py
Merge pull request #13743 from Habbie/auth-api-priority-error
[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=[])
046b69a5 20 listener.daemon = True
f9017ec1
RG
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()
8faf5a90
PD
188 lenprefix = struct.pack("!H", len(wire))
189
190 for b in lenprefix:
191 conn.send(bytes([b]))
192 time.sleep(0.5)
193
f9017ec1
RG
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])
046b69a5 216 thread.daemon = True
f9017ec1
RG
217 thread.start()
218
219 except socket.error as e:
220 print('Error in RPZ socket: %s' % str(e))
221 sock.close()
222
f9017ec1 223class RPZRecursorTest(RecursorTest):
22cf3506
RG
224 _wsPort = 8042
225 _wsTimeout = 2
226 _wsPassword = 'secretpassword'
227 _apiKey = 'secretapikey'
f9017ec1 228 _confdir = 'RPZ'
5b4650e2
PL
229 _auth_zones = {
230 '8': {'threads': 1,
231 'zones': ['ROOT']},
232 '10': {'threads': 1,
233 'zones': ['example']},
234 }
d19bcbf0
RG
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
f9017ec1 246 _config_template = """
22cf3506
RG
247auth-zones=example=configs/%s/example.zone
248webserver=yes
249webserver-port=%d
250webserver-address=127.0.0.1
251webserver-password=%s
252api-key=%s
98b33176 253log-rpz-changes=yes
22cf3506 254""" % (_confdir, _wsPort, _wsPassword, _apiKey)
f9017ec1 255
046b69a5
OM
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
f6a524be
OM
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):
f9017ec1
RG
278 query = dns.message.make_query(name, 'A', want_dnssec=True)
279 query.flags |= dns.flags.CD
d19bcbf0
RG
280 if adQuery:
281 query.flags |= dns.flags.AD
f9017ec1 282
d13c4d18
RG
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)
f6a524be
OM
293 if soa:
294 self.assertAdditionalHasSOA(res)
f89ae456
RG
295 if singleCheck:
296 break
f9017ec1 297
f89ae456
RG
298 def checkNotBlocked(self, name, adQuery=False, singleCheck=False):
299 self.checkBlocked(name, False, adQuery, singleCheck)
f9017ec1 300
f6a524be 301 def checkCustom(self, qname, qtype, expected, soa=False):
6da513b2
RG
302 query = dns.message.make_query(qname, qtype, want_dnssec=True)
303 query.flags |= dns.flags.CD
d13c4d18
RG
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)
f6a524be
OM
309 if soa:
310 self.assertAdditionalHasSOA(res)
6da513b2 311
f6a524be 312 def checkNoData(self, qname, qtype, soa=False):
d13c4d18
RG
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)
f6a524be
OM
320 if soa:
321 self.assertAdditionalHasSOA(res)
d13c4d18 322
98b33176 323 def checkNXD(self, qname, qtype='A'):
d122dac0
RG
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
f6a524be 333 def checkTruncated(self, qname, qtype='A', soa=False):
6da513b2
RG
334 query = dns.message.make_query(qname, qtype, want_dnssec=True)
335 query.flags |= dns.flags.CD
336 res = self.sendUDPQuery(query)
d13c4d18
RG
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)
f6a524be
OM
341 if soa:
342 self.assertAdditionalHasSOA(res)
6da513b2 343
d13c4d18
RG
344 res = self.sendTCPQuery(query)
345 self.assertRcodeEqual(res, dns.rcode.NXDOMAIN)
346 self.assertMessageHasFlags(res, ['QR', 'RA', 'RD', 'CD'])
6da513b2 347 self.assertEqual(len(res.answer), 0)
d13c4d18
RG
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)
6da513b2 358
d122dac0
RG
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)
4bfebc93 364 self.assertEqual(r.status_code, 200)
d122dac0
RG
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
4bfebc93
CH
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)
d122dac0
RG
376
377rpzServerPort = 4250
378rpzServer = RPZServer(rpzServerPort)
379
380class 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
71ada8e0 388 rpzMaster({'127.0.0.1:9999', '127.0.0.1:%d'}, 'zone.rpz.', { refresh=1, includeSOA=true})
d122dac0
RG
389 """ % (rpzServerPort)
390 _confdir = 'RPZXFR'
391 _wsPort = 8042
392 _wsTimeout = 2
393 _wsPassword = 'secretpassword'
394 _apiKey = 'secretapikey'
395 _config_template = """
396auth-zones=example=configs/%s/example.zone
397webserver=yes
398webserver-port=%d
399webserver-address=127.0.0.1
400webserver-password=%s
401api-key=%s
d2c1660a 402disable-packetcache
046b69a5
OM
403allow-notify-from=127.0.0.0/8
404allow-notify-for=zone.rpz
d122dac0
RG
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}
414a 3600 IN A 192.0.2.42
415b 3600 IN A 192.0.2.42
416c 3600 IN A 192.0.2.42
417d 3600 IN A 192.0.2.42
418e 3600 IN A 192.0.2.42
419""".format(soa=cls._SOA))
420 super(RPZRecursorTest, cls).generateRecursorConfig(confdir)
421
f9017ec1
RG
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:
22cf3506 433 self._xfrDone = self._xfrDone + 1
f9017ec1
RG
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):
046b69a5 442 # Fresh RPZ does not need a notify
43e11441 443 self.waitForTCPSocket("127.0.0.1", self._wsPort)
f9017ec1
RG
444 # first zone, only a should be blocked
445 self.waitUntilCorrectSerialIsLoaded(1)
22cf3506 446 self.checkRPZStats(1, 1, 1, self._xfrDone)
f6a524be 447 self.checkBlocked('a.example.', soa=True)
f9017ec1
RG
448 self.checkNotBlocked('b.example.')
449 self.checkNotBlocked('c.example.')
450
451 # second zone, a and b should be blocked
046b69a5 452 self.sendNotify()
f9017ec1 453 self.waitUntilCorrectSerialIsLoaded(2)
22cf3506 454 self.checkRPZStats(2, 2, 1, self._xfrDone)
f6a524be
OM
455 self.checkBlocked('a.example.', soa=True)
456 self.checkBlocked('b.example.', soa=True)
f9017ec1
RG
457 self.checkNotBlocked('c.example.')
458
459 # third zone, only b should be blocked
046b69a5 460 self.sendNotify()
f9017ec1 461 self.waitUntilCorrectSerialIsLoaded(3)
22cf3506 462 self.checkRPZStats(3, 1, 1, self._xfrDone)
f9017ec1 463 self.checkNotBlocked('a.example.')
f6a524be 464 self.checkBlocked('b.example.', soa=True)
f9017ec1
RG
465 self.checkNotBlocked('c.example.')
466
467 # fourth zone, only c should be blocked
046b69a5 468 self.sendNotify()
f9017ec1 469 self.waitUntilCorrectSerialIsLoaded(4)
22cf3506 470 self.checkRPZStats(4, 1, 1, self._xfrDone)
f9017ec1
RG
471 self.checkNotBlocked('a.example.')
472 self.checkNotBlocked('b.example.')
f6a524be 473 self.checkBlocked('c.example.', soa=True)
22cf3506
RG
474
475 # fifth zone, we should get a full AXFR this time, and only d should be blocked
046b69a5 476 self.sendNotify()
22cf3506 477 self.waitUntilCorrectSerialIsLoaded(5)
8340237f 478 self.checkRPZStats(5, 3, 2, self._xfrDone)
22cf3506
RG
479 self.checkNotBlocked('a.example.')
480 self.checkNotBlocked('b.example.')
481 self.checkNotBlocked('c.example.')
f6a524be 482 self.checkBlocked('d.example.', soa=True)
22cf3506 483
6da513b2 484 # sixth zone, only e should be blocked, f is a local data record
046b69a5 485 self.sendNotify()
22cf3506 486 self.waitUntilCorrectSerialIsLoaded(6)
6da513b2
RG
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.')
f6a524be 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)
6da513b2 493 self.checkCustom('e.example.', 'MX', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'MX', '10 mx.example.'))
f6a524be
OM
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)
6da513b2
RG
496
497 # seventh zone, e should only have one A
046b69a5 498 self.sendNotify()
6da513b2 499 self.waitUntilCorrectSerialIsLoaded(7)
d13c4d18 500 self.checkRPZStats(7, 4, 2, self._xfrDone)
22cf3506
RG
501 self.checkNotBlocked('a.example.')
502 self.checkNotBlocked('b.example.')
503 self.checkNotBlocked('c.example.')
504 self.checkNotBlocked('d.example.')
f6a524be
OM
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)
d19bcbf0
RG
509 # check that the policy is disabled for AD=1 queries
510 self.checkNotBlocked('e.example.', True)
d13c4d18 511 # check non-custom policies
f6a524be 512 self.checkTruncated('tc.example.', soa=True)
d13c4d18 513 self.checkDropped('drop.example.')
d122dac0 514
98b33176 515 # eighth zone, all entries should be gone
046b69a5 516 self.sendNotify()
98b33176
RG
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.')
ee2a5356
RG
526 self.checkNXD('drop.example.')
527
528 # 9th zone is a duplicate, it might get skipped
529 global rpzServer
530 rpzServer.moveToSerial(9)
046b69a5 531 self.sendNotify()
ee2a5356 532 time.sleep(3)
046b69a5 533 self.sendNotify()
ee2a5356
RG
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.')
f6a524be 541 self.checkBlocked('f.example.', soa=True)
ee2a5356 542 self.checkNXD('tc.example.')
ba5f46ae
RG
543 self.checkNXD('drop.example.')
544
545 # the next update will update the zone twice
546 rpzServer.moveToSerial(11)
046b69a5 547 self.sendNotify()
ba5f46ae 548 time.sleep(3)
046b69a5 549 self.sendNotify()
ba5f46ae
RG
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.')
f6a524be 558 self.checkBlocked('g.example.', soa=True)
ba5f46ae 559 self.checkNXD('tc.example.')
98b33176
RG
560 self.checkNXD('drop.example.')
561
d122dac0
RG
562class RPZFileRecursorTest(RPZRecursorTest):
563 """
564 This test makes sure that we correctly load RPZ zones from a file
565 """
566
567 _confdir = 'RPZFile'
d122dac0 568 _lua_config_file = """
f6a524be 569 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", includeSOA=true })
d122dac0
RG
570 """ % (_confdir)
571 _config_template = """
572auth-zones=example=configs/%s/example.zone
f89ae456 573""" % (_confdir)
d122dac0
RG
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}
581a 3600 IN A 192.0.2.42
582b 3600 IN A 192.0.2.42
583c 3600 IN A 192.0.2.42
584d 3600 IN A 192.0.2.42
585e 3600 IN A 192.0.2.42
586z 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}
593a.example.zone.rpz. 60 IN A 192.0.2.42
594a.example.zone.rpz. 60 IN A 192.0.2.43
595a.example.zone.rpz. 60 IN TXT "some text"
596drop.example.zone.rpz. 60 IN CNAME rpz-drop.
597z.example.zone.rpz. 60 IN A 192.0.2.1
598tc.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"'))
f6a524be 605 self.checkBlocked('z.example.', soa=True)
d122dac0
RG
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
f6a524be 613 self.checkTruncated('tc.example.', soa=True)
d122dac0
RG
614 self.checkDropped('drop.example.')
615
616class 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'
d122dac0
RG
622 _lua_config_file = """
623 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction })
624 """ % (_confdir)
625 _config_template = """
626auth-zones=example=configs/%s/example.zone
f89ae456 627""" % (_confdir)
d122dac0
RG
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}
635a 3600 IN A 192.0.2.42
636b 3600 IN A 192.0.2.42
637c 3600 IN A 192.0.2.42
638d 3600 IN A 192.0.2.42
639drop 3600 IN A 192.0.2.42
640e 3600 IN A 192.0.2.42
641z 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}
648a.example.zone.rpz. 60 IN A 192.0.2.42
649drop.example.zone.rpz. 60 IN CNAME rpz-drop.
650z.example.zone.rpz. 60 IN A 192.0.2.1
651tc.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
669class 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'
d122dac0
RG
675 _lua_config_file = """
676 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction, defpolOverrideLocalData=false })
677 """ % (_confdir)
678 _config_template = """
679auth-zones=example=configs/%s/example.zone
f89ae456 680""" % (_confdir)
d122dac0
RG
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}
688a 3600 IN A 192.0.2.42
689b 3600 IN A 192.0.2.42
690c 3600 IN A 192.0.2.42
691d 3600 IN A 192.0.2.42
692drop 3600 IN A 192.0.2.42
693e 3600 IN A 192.0.2.42
694z 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}
701a.example.zone.rpz. 60 IN A 192.0.2.42
702a.example.zone.rpz. 60 IN A 192.0.2.43
703a.example.zone.rpz. 60 IN TXT "some text"
704drop.example.zone.rpz. 60 IN CNAME rpz-drop.
705z.example.zone.rpz. 60 IN A 192.0.2.1
706tc.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):
ef2ea4bf 711 # local data entries will not be overridden by the default policy
d122dac0
RG
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.')
1d2777e9 723
f89ae456
RG
724class 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=[])
046b69a5 729 listener.daemon = True
f89ae456
RG
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
770rpzAuthServerPort = 4260
771rpzAuthServer = RPZSimpleAuthServer(rpzAuthServerPort)
772
773class RPZOrderingPrecedenceRecursorTest(RPZRecursorTest):
1d2777e9
RG
774 """
775 This test makes sure that the recursor respects the RPZ ordering precedence rules
776 """
777
778 _confdir = 'RPZOrderingPrecedence'
1d2777e9
RG
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 = """
784auth-zones=example=configs/%s/example.zone
f89ae456
RG
785forward-zones=delegated.example=127.0.0.1:%d
786""" % (_confdir, rpzAuthServerPort)
1d2777e9
RG
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}
794sub.test 3600 IN A 192.0.2.42
fa973749
RG
795passthru-then-blocked-by-higher 3600 IN A 192.0.2.66
796passthru-then-blocked-by-same 3600 IN A 192.0.2.66
797blocked-then-passhtru-by-higher 3600 IN A 192.0.2.100
1d2777e9
RG
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.
fa973749
RG
80532.66.2.0.192.rpz-ip.zone.rpz. 60 IN A 192.0.2.1
80632.100.2.0.192.rpz-ip.zone.rpz. 60 IN CNAME rpz-passthru.
807passthru-then-blocked-by-same.example.zone.rpz. 60 IN CNAME rpz-passthru.
f89ae456 80832.1.0.0.127.rpz-nsip.zone.rpz. 60 IN CNAME rpz-passthru.
1d2777e9
RG
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}
815sub.test.example.com.zone2.rpz. 60 IN CNAME .
fa973749
RG
816passthru-then-blocked-by-higher.example.zone2.rpz. 60 IN CNAME rpz-passthru.
817blocked-then-passhtru-by-higher.example.zone2.rpz. 60 IN A 192.0.2.1
1d2777e9
RG
81832.42.2.0.192.rpz-ip 60 IN CNAME .
819""".format(soa=cls._SOA))
820
f89ae456 821 super(RPZOrderingPrecedenceRecursorTest, cls).generateRecursorConfig(confdir)
1d2777e9 822
fa973749 823 def testRPZOrderingForQNameAndWhitelisting(self):
1d2777e9
RG
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
fa973749
RG
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.
1d2777e9 828 self.checkNotBlocked('sub.test.example.')
fa973749
RG
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.')
f89ae456
RG
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
859class 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 = """
870auth-zones=example=configs/%s/example.zone
871forward-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}
88632.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}
89332.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'))
b7284b4d
RG
905
906
907class 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 = """
918auth-zones=example=configs/%s/example.zone
919forward-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}
928name IN CNAME cname
929cname IN A 192.0.2.255
930custom-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}
937cname.example IN CNAME custom-target.example.
938custom-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
966class 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
b7284b4d
RG
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}
98632.100.2.0.192.rpz-ip IN CNAME .
98732.101.2.0.192.rpz-ip IN CNAME *.
98832.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)
4bfebc93 1005 self.assertEqual(len(res.answer), 0)
b7284b4d
RG
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)
4bfebc93 1019 self.assertEqual(len(res.answer), 0)
b7284b4d
RG
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
4bfebc93 1034 self.assertEqual(len(res.answer), 3)
b7284b4d
RG
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'))
5fb5beff
O
1037
1038class 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 = """
1072auth-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}
1081a 3600 IN A 192.0.2.42
1082b 3600 IN A 192.0.2.42
1083c 3600 IN A 192.0.2.42
1084d 3600 IN A 192.0.2.42
1085e 3600 IN A 192.0.2.42
1086z 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}
1093a.example.zone.rpz. 60 IN A 192.0.2.42
1094a.example.zone.rpz. 60 IN A 192.0.2.43
1095a.example.zone.rpz. 60 IN TXT "some text"
1096drop.example.zone.rpz. 60 IN CNAME rpz-drop.
1097zmod.example.zone.rpz. 60 IN A 192.0.2.1
1098tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
1099nxmod.exmaple.zone.rpz. 60 in CNAME .
1100nodatamod.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.')