]> git.ipfire.org Git - thirdparty/pdns.git/blame - 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
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()
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])
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
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
f6a524be
OM
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):
f9017ec1
RG
270 query = dns.message.make_query(name, 'A', want_dnssec=True)
271 query.flags |= dns.flags.CD
d19bcbf0
RG
272 if adQuery:
273 query.flags |= dns.flags.AD
f9017ec1 274
d13c4d18
RG
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)
f6a524be
OM
285 if soa:
286 self.assertAdditionalHasSOA(res)
f89ae456
RG
287 if singleCheck:
288 break
f9017ec1 289
f89ae456
RG
290 def checkNotBlocked(self, name, adQuery=False, singleCheck=False):
291 self.checkBlocked(name, False, adQuery, singleCheck)
f9017ec1 292
f6a524be 293 def checkCustom(self, qname, qtype, expected, soa=False):
6da513b2
RG
294 query = dns.message.make_query(qname, qtype, want_dnssec=True)
295 query.flags |= dns.flags.CD
d13c4d18
RG
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)
f6a524be
OM
301 if soa:
302 self.assertAdditionalHasSOA(res)
6da513b2 303
f6a524be 304 def checkNoData(self, qname, qtype, soa=False):
d13c4d18
RG
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)
f6a524be
OM
312 if soa:
313 self.assertAdditionalHasSOA(res)
d13c4d18 314
98b33176 315 def checkNXD(self, qname, qtype='A'):
d122dac0
RG
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
f6a524be 325 def checkTruncated(self, qname, qtype='A', soa=False):
6da513b2
RG
326 query = dns.message.make_query(qname, qtype, want_dnssec=True)
327 query.flags |= dns.flags.CD
328 res = self.sendUDPQuery(query)
d13c4d18
RG
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)
f6a524be
OM
333 if soa:
334 self.assertAdditionalHasSOA(res)
6da513b2 335
d13c4d18
RG
336 res = self.sendTCPQuery(query)
337 self.assertRcodeEqual(res, dns.rcode.NXDOMAIN)
338 self.assertMessageHasFlags(res, ['QR', 'RA', 'RD', 'CD'])
6da513b2 339 self.assertEqual(len(res.answer), 0)
d13c4d18
RG
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)
6da513b2 350
d122dac0
RG
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)
4bfebc93 356 self.assertEqual(r.status_code, 200)
d122dac0
RG
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
4bfebc93
CH
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)
d122dac0
RG
368
369rpzServerPort = 4250
370rpzServer = RPZServer(rpzServerPort)
371
372class 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
f6a524be 380 rpzMaster({'127.0.0.1:9999', '127.0.0.1:%d'}, 'zone.rpz.', { refresh=1, includeSOA=true})
d122dac0
RG
381 """ % (rpzServerPort)
382 _confdir = 'RPZXFR'
383 _wsPort = 8042
384 _wsTimeout = 2
385 _wsPassword = 'secretpassword'
386 _apiKey = 'secretapikey'
387 _config_template = """
388auth-zones=example=configs/%s/example.zone
389webserver=yes
390webserver-port=%d
391webserver-address=127.0.0.1
392webserver-password=%s
393api-key=%s
d2c1660a 394disable-packetcache
d122dac0
RG
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}
404a 3600 IN A 192.0.2.42
405b 3600 IN A 192.0.2.42
406c 3600 IN A 192.0.2.42
407d 3600 IN A 192.0.2.42
408e 3600 IN A 192.0.2.42
409""".format(soa=cls._SOA))
410 super(RPZRecursorTest, cls).generateRecursorConfig(confdir)
411
f9017ec1
RG
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:
22cf3506 423 self._xfrDone = self._xfrDone + 1
f9017ec1
RG
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):
43e11441 432 self.waitForTCPSocket("127.0.0.1", self._wsPort)
f9017ec1
RG
433 # first zone, only a should be blocked
434 self.waitUntilCorrectSerialIsLoaded(1)
22cf3506 435 self.checkRPZStats(1, 1, 1, self._xfrDone)
f6a524be 436 self.checkBlocked('a.example.', soa=True)
f9017ec1
RG
437 self.checkNotBlocked('b.example.')
438 self.checkNotBlocked('c.example.')
439
440 # second zone, a and b should be blocked
441 self.waitUntilCorrectSerialIsLoaded(2)
22cf3506 442 self.checkRPZStats(2, 2, 1, self._xfrDone)
f6a524be
OM
443 self.checkBlocked('a.example.', soa=True)
444 self.checkBlocked('b.example.', soa=True)
f9017ec1
RG
445 self.checkNotBlocked('c.example.')
446
447 # third zone, only b should be blocked
448 self.waitUntilCorrectSerialIsLoaded(3)
22cf3506 449 self.checkRPZStats(3, 1, 1, self._xfrDone)
f9017ec1 450 self.checkNotBlocked('a.example.')
f6a524be 451 self.checkBlocked('b.example.', soa=True)
f9017ec1
RG
452 self.checkNotBlocked('c.example.')
453
454 # fourth zone, only c should be blocked
455 self.waitUntilCorrectSerialIsLoaded(4)
22cf3506 456 self.checkRPZStats(4, 1, 1, self._xfrDone)
f9017ec1
RG
457 self.checkNotBlocked('a.example.')
458 self.checkNotBlocked('b.example.')
f6a524be 459 self.checkBlocked('c.example.', soa=True)
22cf3506
RG
460
461 # fifth zone, we should get a full AXFR this time, and only d should be blocked
462 self.waitUntilCorrectSerialIsLoaded(5)
8340237f 463 self.checkRPZStats(5, 3, 2, self._xfrDone)
22cf3506
RG
464 self.checkNotBlocked('a.example.')
465 self.checkNotBlocked('b.example.')
466 self.checkNotBlocked('c.example.')
f6a524be 467 self.checkBlocked('d.example.', soa=True)
22cf3506 468
6da513b2 469 # sixth zone, only e should be blocked, f is a local data record
22cf3506 470 self.waitUntilCorrectSerialIsLoaded(6)
6da513b2
RG
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.')
f6a524be 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)
6da513b2 477 self.checkCustom('e.example.', 'MX', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'MX', '10 mx.example.'))
f6a524be
OM
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)
6da513b2
RG
480
481 # seventh zone, e should only have one A
482 self.waitUntilCorrectSerialIsLoaded(7)
d13c4d18 483 self.checkRPZStats(7, 4, 2, self._xfrDone)
22cf3506
RG
484 self.checkNotBlocked('a.example.')
485 self.checkNotBlocked('b.example.')
486 self.checkNotBlocked('c.example.')
487 self.checkNotBlocked('d.example.')
f6a524be
OM
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)
d19bcbf0
RG
492 # check that the policy is disabled for AD=1 queries
493 self.checkNotBlocked('e.example.', True)
d13c4d18 494 # check non-custom policies
f6a524be 495 self.checkTruncated('tc.example.', soa=True)
d13c4d18 496 self.checkDropped('drop.example.')
d122dac0 497
98b33176
RG
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.')
ee2a5356
RG
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.')
f6a524be 521 self.checkBlocked('f.example.', soa=True)
ee2a5356 522 self.checkNXD('tc.example.')
ba5f46ae
RG
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.')
f6a524be 536 self.checkBlocked('g.example.', soa=True)
ba5f46ae 537 self.checkNXD('tc.example.')
98b33176
RG
538 self.checkNXD('drop.example.')
539
d122dac0
RG
540class RPZFileRecursorTest(RPZRecursorTest):
541 """
542 This test makes sure that we correctly load RPZ zones from a file
543 """
544
545 _confdir = 'RPZFile'
d122dac0 546 _lua_config_file = """
f6a524be 547 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", includeSOA=true })
d122dac0
RG
548 """ % (_confdir)
549 _config_template = """
550auth-zones=example=configs/%s/example.zone
f89ae456 551""" % (_confdir)
d122dac0
RG
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}
559a 3600 IN A 192.0.2.42
560b 3600 IN A 192.0.2.42
561c 3600 IN A 192.0.2.42
562d 3600 IN A 192.0.2.42
563e 3600 IN A 192.0.2.42
564z 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}
571a.example.zone.rpz. 60 IN A 192.0.2.42
572a.example.zone.rpz. 60 IN A 192.0.2.43
573a.example.zone.rpz. 60 IN TXT "some text"
574drop.example.zone.rpz. 60 IN CNAME rpz-drop.
575z.example.zone.rpz. 60 IN A 192.0.2.1
576tc.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"'))
f6a524be 583 self.checkBlocked('z.example.', soa=True)
d122dac0
RG
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
f6a524be 591 self.checkTruncated('tc.example.', soa=True)
d122dac0
RG
592 self.checkDropped('drop.example.')
593
594class 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'
d122dac0
RG
600 _lua_config_file = """
601 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction })
602 """ % (_confdir)
603 _config_template = """
604auth-zones=example=configs/%s/example.zone
f89ae456 605""" % (_confdir)
d122dac0
RG
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}
613a 3600 IN A 192.0.2.42
614b 3600 IN A 192.0.2.42
615c 3600 IN A 192.0.2.42
616d 3600 IN A 192.0.2.42
617drop 3600 IN A 192.0.2.42
618e 3600 IN A 192.0.2.42
619z 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}
626a.example.zone.rpz. 60 IN A 192.0.2.42
627drop.example.zone.rpz. 60 IN CNAME rpz-drop.
628z.example.zone.rpz. 60 IN A 192.0.2.1
629tc.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
647class 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'
d122dac0
RG
653 _lua_config_file = """
654 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction, defpolOverrideLocalData=false })
655 """ % (_confdir)
656 _config_template = """
657auth-zones=example=configs/%s/example.zone
f89ae456 658""" % (_confdir)
d122dac0
RG
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}
666a 3600 IN A 192.0.2.42
667b 3600 IN A 192.0.2.42
668c 3600 IN A 192.0.2.42
669d 3600 IN A 192.0.2.42
670drop 3600 IN A 192.0.2.42
671e 3600 IN A 192.0.2.42
672z 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}
679a.example.zone.rpz. 60 IN A 192.0.2.42
680a.example.zone.rpz. 60 IN A 192.0.2.43
681a.example.zone.rpz. 60 IN TXT "some text"
682drop.example.zone.rpz. 60 IN CNAME rpz-drop.
683z.example.zone.rpz. 60 IN A 192.0.2.1
684tc.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):
ef2ea4bf 689 # local data entries will not be overridden by the default policy
d122dac0
RG
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.')
1d2777e9 701
f89ae456
RG
702class 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
748rpzAuthServerPort = 4260
749rpzAuthServer = RPZSimpleAuthServer(rpzAuthServerPort)
750
751class RPZOrderingPrecedenceRecursorTest(RPZRecursorTest):
1d2777e9
RG
752 """
753 This test makes sure that the recursor respects the RPZ ordering precedence rules
754 """
755
756 _confdir = 'RPZOrderingPrecedence'
1d2777e9
RG
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 = """
762auth-zones=example=configs/%s/example.zone
f89ae456
RG
763forward-zones=delegated.example=127.0.0.1:%d
764""" % (_confdir, rpzAuthServerPort)
1d2777e9
RG
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}
772sub.test 3600 IN A 192.0.2.42
fa973749
RG
773passthru-then-blocked-by-higher 3600 IN A 192.0.2.66
774passthru-then-blocked-by-same 3600 IN A 192.0.2.66
775blocked-then-passhtru-by-higher 3600 IN A 192.0.2.100
1d2777e9
RG
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.
fa973749
RG
78332.66.2.0.192.rpz-ip.zone.rpz. 60 IN A 192.0.2.1
78432.100.2.0.192.rpz-ip.zone.rpz. 60 IN CNAME rpz-passthru.
785passthru-then-blocked-by-same.example.zone.rpz. 60 IN CNAME rpz-passthru.
f89ae456 78632.1.0.0.127.rpz-nsip.zone.rpz. 60 IN CNAME rpz-passthru.
1d2777e9
RG
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}
793sub.test.example.com.zone2.rpz. 60 IN CNAME .
fa973749
RG
794passthru-then-blocked-by-higher.example.zone2.rpz. 60 IN CNAME rpz-passthru.
795blocked-then-passhtru-by-higher.example.zone2.rpz. 60 IN A 192.0.2.1
1d2777e9
RG
79632.42.2.0.192.rpz-ip 60 IN CNAME .
797""".format(soa=cls._SOA))
798
f89ae456 799 super(RPZOrderingPrecedenceRecursorTest, cls).generateRecursorConfig(confdir)
1d2777e9 800
fa973749 801 def testRPZOrderingForQNameAndWhitelisting(self):
1d2777e9
RG
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
fa973749
RG
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.
1d2777e9 806 self.checkNotBlocked('sub.test.example.')
fa973749
RG
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.')
f89ae456
RG
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
837class 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 = """
848auth-zones=example=configs/%s/example.zone
849forward-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}
86432.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}
87132.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'))
b7284b4d
RG
883
884
885class 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 = """
896auth-zones=example=configs/%s/example.zone
897forward-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}
906name IN CNAME cname
907cname IN A 192.0.2.255
908custom-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}
915cname.example IN CNAME custom-target.example.
916custom-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
944class 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
b7284b4d
RG
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}
96432.100.2.0.192.rpz-ip IN CNAME .
96532.101.2.0.192.rpz-ip IN CNAME *.
96632.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)
4bfebc93 983 self.assertEqual(len(res.answer), 0)
b7284b4d
RG
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)
4bfebc93 997 self.assertEqual(len(res.answer), 0)
b7284b4d
RG
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
4bfebc93 1012 self.assertEqual(len(res.answer), 3)
b7284b4d
RG
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'))
5fb5beff
O
1015
1016class 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 = """
1050auth-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}
1059a 3600 IN A 192.0.2.42
1060b 3600 IN A 192.0.2.42
1061c 3600 IN A 192.0.2.42
1062d 3600 IN A 192.0.2.42
1063e 3600 IN A 192.0.2.42
1064z 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}
1071a.example.zone.rpz. 60 IN A 192.0.2.42
1072a.example.zone.rpz. 60 IN A 192.0.2.43
1073a.example.zone.rpz. 60 IN TXT "some text"
1074drop.example.zone.rpz. 60 IN CNAME rpz-drop.
1075zmod.example.zone.rpz. 60 IN A 192.0.2.1
1076tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
1077nxmod.exmaple.zone.rpz. 60 in CNAME .
1078nodatamod.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.')